pgsql_fdw, FDW for PostgreSQL server
I'd like to propose pgsql_fdw, FDW for external PostgreSQL server, as a
contrib module. I think that this module would be the basis of further
SQL/MED development for core, e.g. join-push-down and ANALYZE support.
I attached three patches for this new FDW. They should be applied in
the order below. I separated these patches so that first (or first two)
can be committed separately.
* fdw_helper_doc.patch provides documents for FDW developers about
helper functions existing in 9.1, so this can be back-patched.
* fdw_helper_funcs.patch provides additional helper functions which
would make manipulation of FDW options easier.
* pgsql_fdw.patch provides new FDW for external PG server.
Here are details of pgsql_fdw.
Name of the wrapper
===================
I used the name "pgsql_fdw" for the wrapper and its derivatives. I
think it would be better to leave contrib/dblink and built-in
postgresql_fdw_validator for backward compatibility, and use new name
for new wrapper.
Or, it might be OK to rename postgresql_fdw_validator to
dblink_validator or something, or fix dblink to use validator of new
wrapper. I'm not sure that dblink should be alone or integrated with
pgsql_fdw...
Connection management
=====================
The pgsql_fdw establishes a new connection when a foreign server is
accessed first for the local session. Established connection is shared
between all foreign scans in the local query, and shared between even
scans in following queries. Connections are discarded when the current
transaction aborts so that unexpected failure won't cause connection
leak. This is implemented with resource owner mechanism.
User can see active connections via pgsql_fdw_connections view, and
discard arbitrary connection via pgsql_fdw_disconnect() function. These
can be done from only same local session.
If local role has changed via SET ROLE or SET SESSION AUTHENTICATION,
pgsql_fdw ignores old role's connections and looks up appropriate
connection for the new role from the pool. If there wasn't suitable
one, pgsql_fdw establishes new connection. When local role has changed
to old role again, pooled connection will be used again.
Unlike contrib/dblink, one foreign server can have only one connection
at a time for one local role. This is because pgsql_fdw doesn't support
named connections.
Cost estimation
===============
The pgsql_fdw executes an EXPLAIN command on remote side for each
PlanForeignScan call. Returned costs and rows are used as local
estimation for the Path with adding connection costs and data transfer
costs.
SELECT optimization
===================
To reduce amount of data transferred from remote server, references to
unnecessary columns are replaced with NULL literal in remote query.
WHERE clause push-down
======================
Some kind of qualifiers in WHERE clause are pushed down to remote server
so that the query result can be reduced. Currently qualifiers which
include any volatile or stable element can't be pushed down. Even with
these limitations, most qualifiers would be pushed down in usual cases.
Cursor mode
===========
The pgsql_fdw switches the way to retrieve result records according to
estimated result rows; use simple SELECT for small result, and use
cursor with DECLARE/FETCH statements for large result. The threshold
is default to 1000, and configurable with FDW option "min_cursor_rows".
In cursor mode, number of rows fetched at once can be controlled by FDW
option "fetch_count".
EXPLAIN output
==============
The pgsql_fdw shows a remote query used for each foreign scan node in
the output of EXPLAIN command with title "Remote SQL". If pgsql_fdw
decided to use cursor for the scan, DECLARE statement is shown.
Regards,
--
Shigeru Hanada
Attachments:
fdw_helper_doc.patchtext/plain; name=fdw_helper_doc.patchDownload
commit 5a2f1314d197c037dfd55ea1dc0395f46333c05d
Author: Shigeru Hanada <shigeru.hanada@gmail.com>
Date: Fri Oct 21 11:54:39 2011 +0900
Add document about helper functions for FDW authors.
diff --git a/doc/src/sgml/fdwhandler.sgml b/doc/src/sgml/fdwhandler.sgml
index 76ff243..db02d13 100644
--- a/doc/src/sgml/fdwhandler.sgml
+++ b/doc/src/sgml/fdwhandler.sgml
@@ -235,4 +235,98 @@ EndForeignScan (ForeignScanState *node);
</sect1>
+ <sect1 id="fdw-helpers">
+ <title>Foreign Data Wrapper Helper Functions</title>
+
+ <para>
+ Several helper functions are exported from core so that authors of FDW
+ can get easy access to attributes of FDW-related objects such as FDW
+ options.
+ </para>
+
+ <para>
+<programlisting>
+ForeignDataWrapper *
+GetForeignDataWrapper(Oid fdwid);
+</programlisting>
+
+ This function returns a <structname>ForeignDataWrapper</structname> object
+ for a foreign-data wrapper with given oid. A
+ <structname>ForeignDataWrapper</structname> object contains oid of the
+ wrapper itself, oid of the owner of the wrapper, name of the wrapper, oid
+ of fdwhandler function, oid of fdwvalidator function, and FDW options in
+ the form of list of <structname>DefElem</structname>.
+ </para>
+
+ <para>
+<programlisting>
+ForeignServer *
+GetForeignServer(Oid serverid);
+</programlisting>
+
+ This function returns a <structname>ForeignServer</structname> object for
+ a foreign server with given oid. A <structname>ForeignServer</structname>
+ object contains oid of the server, oid of the wrapper for the server, oid
+ of the owner of the server, name of the server, type of the server,
+ version of the server, and FDW options in the form of list of
+ <structname>DefElem</structname>.
+ </para>
+
+ <para>
+<programlisting>
+UserMapping *
+GetUserMapping(Oid userid, Oid serverid);
+</programlisting>
+
+ This function returns a <structname>UserMapping</structname> object for a
+ user mapping with given oid pair. A <structname>UserMapping</structname>
+ object contains oid of the user, oid of the server, and FDW options in the
+ form of list of <structname>DefElem</structname>.
+ </para>
+
+ <para>
+<programlisting>
+ForeignTable *
+GetForeignTable(Oid relid);
+</programlisting>
+
+ This function returns a <structname>ForeignTable</structname> object for a
+ foreign table with given oid. A <structname>ForeignTable</structname>
+ object contains oid of the foreign table, oid of the server for the table,
+ and FDW options in the form of list of <structname>DefElem</structname>.
+ </para>
+
+ <para>
+ Some object types have name-based functions.
+ </para>
+
+ <para>
+<programlisting>
+ForeignDataWrapper *
+GetForeignDataWrapperByName(const char *name, bool missing_ok);
+</programlisting>
+
+ This function returns a <structname>ForeignDataWrapper</structname> object
+ for a foreign-data wrapper with given name. If the wrapper is not found,
+ return NULL if missing_ok was true, otherwise raise an error.
+ </para>
+
+ <para>
+<programlisting>
+ForeignServer *
+GetForeignServerByName(const char *name, bool missing_ok);
+</programlisting>
+
+ This function returns a <structname>ForeignServer</structname> object for
+ a foreign server with given name. If the server is not found, return NULL
+ if missing_ok was true, otherwise raise an error.
+ </para>
+
+ <para>
+ To use any of these functions, you need to include
+ <filename>foreign/foreign.h</filename> in your source file.
+ </para>
+
+ </sect1>
+
</chapter>
fdw_helper_funcs.patchtext/plain; name=fdw_helper_funcs.patchDownload
diff --git a/contrib/file_fdw/file_fdw.c b/contrib/file_fdw/file_fdw.c
index 1cf3b3c..4a7ffa6 100644
*** a/contrib/file_fdw/file_fdw.c
--- b/contrib/file_fdw/file_fdw.c
***************
*** 26,32 ****
#include "nodes/makefuncs.h"
#include "optimizer/cost.h"
#include "utils/rel.h"
! #include "utils/syscache.h"
PG_MODULE_MAGIC;
--- 26,32 ----
#include "nodes/makefuncs.h"
#include "optimizer/cost.h"
#include "utils/rel.h"
! #include "utils/lsyscache.h"
PG_MODULE_MAGIC;
*************** get_file_fdw_attribute_options(Oid relid
*** 345,398 ****
/* Retrieve FDW options for all user-defined attributes. */
for (attnum = 1; attnum <= natts; attnum++)
{
! HeapTuple tuple;
! Form_pg_attribute attr;
! Datum datum;
! bool isnull;
/* Skip dropped attributes. */
if (tupleDesc->attrs[attnum - 1]->attisdropped)
continue;
! /*
! * We need the whole pg_attribute tuple not just what is in the
! * tupleDesc, so must do a catalog lookup.
! */
! tuple = SearchSysCache2(ATTNUM,
! RelationGetRelid(rel),
! Int16GetDatum(attnum));
! if (!HeapTupleIsValid(tuple))
! elog(ERROR, "cache lookup failed for attribute %d of relation %u",
! attnum, RelationGetRelid(rel));
! attr = (Form_pg_attribute) GETSTRUCT(tuple);
!
! datum = SysCacheGetAttr(ATTNUM,
! tuple,
! Anum_pg_attribute_attfdwoptions,
! &isnull);
! if (!isnull)
{
! List *options = untransformRelOptions(datum);
! ListCell *lc;
! foreach(lc, options)
{
! DefElem *def = (DefElem *) lfirst(lc);
!
! if (strcmp(def->defname, "force_not_null") == 0)
{
! if (defGetBoolean(def))
! {
! char *attname = pstrdup(NameStr(attr->attname));
! fnncolumns = lappend(fnncolumns, makeString(attname));
! }
}
- /* maybe in future handle other options here */
}
}
-
- ReleaseSysCache(tuple);
}
heap_close(rel, AccessShareLock);
--- 345,373 ----
/* Retrieve FDW options for all user-defined attributes. */
for (attnum = 1; attnum <= natts; attnum++)
{
! List *options;
! ListCell *lc;
/* Skip dropped attributes. */
if (tupleDesc->attrs[attnum - 1]->attisdropped)
continue;
! options = GetForeignColumnOptions(relid, attnum);
! foreach(lc, options)
{
! DefElem *def = (DefElem *) lfirst(lc);
! if (strcmp(def->defname, "force_not_null") == 0)
{
! if (defGetBoolean(def))
{
! char *attname = pstrdup(get_attname(relid, attnum));
! fnncolumns = lappend(fnncolumns, makeString(attname));
}
}
+ /* maybe in future handle other options here */
}
}
heap_close(rel, AccessShareLock);
diff --git a/doc/src/sgml/fdwhandler.sgml b/doc/src/sgml/fdwhandler.sgml
index db02d13..43ad6ed 100644
*** a/doc/src/sgml/fdwhandler.sgml
--- b/doc/src/sgml/fdwhandler.sgml
*************** GetForeignTable(Oid relid);
*** 297,302 ****
--- 297,332 ----
</para>
<para>
+ <programlisting>
+ char *
+ GetFdwOptionValue(Oid relid, AttrNumber attnum, const char *optname);
+ </programlisting>
+
+ This function returns a copy (created in current memory context) of the
+ value of the given option for the given object (relation or its column).
+ If attnum is InvalidAttrNumber, pg_attribute is ignored.
+ If specified option is set in multiple object level, the one in the
+ finest-grained object is used; e.g. priority is given to user mapping
+ over than a foreign server for the mapping or foreign-data wrapper for the
+ server.
+ This function would be useful when you know which option is needed but you
+ don't know which object(s) have it.
+ If you already know the source object, it would be more efficient to use
+ object retrieval functions.
+ </para>
+
+ <para>
+ <programlisting>
+ List *
+ GetForeignTableColumnOptions(Oid relid, AttrNumber attnum);
+ </programlisting>
+
+ This function returns per-column FDW options for a column with given
+ relation oid and attribute number in the form of list of
+ <structname>DefElem</structname>.
+ </para>
+
+ <para>
Some object types have name-based functions.
</para>
diff --git a/src/backend/foreign/foreign.c b/src/backend/foreign/foreign.c
index a7d30a1..df0e373 100644
*** a/src/backend/foreign/foreign.c
--- b/src/backend/foreign/foreign.c
*************** GetForeignTable(Oid relid)
*** 248,253 ****
--- 248,361 ----
/*
+ * If an option entry which matches the given name was found in the given
+ * list, returns a copy of arg string, otherwise returns NULL.
+ */
+ static char *
+ get_options_value(List *options, const char *optname)
+ {
+ ListCell *lc;
+
+ /* Find target option from the list. */
+ foreach (lc, options)
+ {
+ DefElem *def = lfirst(lc);
+
+ if (strcmp(def->defname, optname) == 0)
+ return pstrdup(strVal(def->arg));
+ }
+
+ return NULL;
+ }
+
+ /*
+ * Returns a copy of the value of specified option with searching the option
+ * from appropriate catalog. If an option was stored in multiple object
+ * levels, one in the finest-grained object level is used; lookup order is:
+ * 1) pg_attribute (only when attnum != InvalidAttrNumber)
+ * 2) pg_foreign_table
+ * 3) pg_user_mapping
+ * 4) pg_foreign_server
+ * 5) pg_foreign_data_wrapper
+ * This priority rule would be useful in most cases using FDW options.
+ *
+ * If attnum was InvalidAttrNumber, we don't retrieve FDW optiosn from
+ * pg_attribute.attfdwoptions.
+ */
+ char *
+ GetFdwOptionValue(Oid relid, AttrNumber attnum, const char *optname)
+ {
+ ForeignTable *table = NULL;
+ UserMapping *user = NULL;
+ ForeignServer *server = NULL;
+ ForeignDataWrapper *wrapper = NULL;
+ char *value;
+
+ /* Do we need to use pg_attribute.attfdwoptions too? */
+ if (attnum != InvalidAttrNumber)
+ {
+ value = get_options_value(GetForeignColumnOptions(relid, attnum),
+ optname);
+ if (value != NULL)
+ return value;
+ }
+
+ table = GetForeignTable(relid);
+ value = get_options_value(table->options, optname);
+ if (value != NULL)
+ return value;
+
+ user = GetUserMapping(GetOuterUserId(), table->serverid);
+ value = get_options_value(user->options, optname);
+ if (value != NULL)
+ return value;
+
+ server = GetForeignServer(table->serverid);
+ value = get_options_value(server->options, optname);
+ if (value != NULL)
+ return value;
+
+ wrapper = GetForeignDataWrapper(server->fdwid);
+ value = get_options_value(wrapper->options, optname);
+ if (value != NULL)
+ return value;
+
+ return NULL;
+ }
+
+
+ /*
+ * GetForeignColumnOptions - Get attfdwoptions of given relation/attnum as
+ * list of DefElem.
+ */
+ List *
+ GetForeignColumnOptions(Oid relid, AttrNumber attnum)
+ {
+ List *options;
+ HeapTuple tp;
+ Datum datum;
+ bool isnull;
+
+ tp = SearchSysCache2(ATTNUM,
+ ObjectIdGetDatum(relid),
+ Int16GetDatum(attnum));
+ if (!HeapTupleIsValid(tp))
+ elog(ERROR, "cache lookup failed for attribute %d of relation %u", attnum, relid);
+ datum = SysCacheGetAttr(ATTNUM,
+ tp,
+ Anum_pg_attribute_attfdwoptions,
+ &isnull);
+ if (isnull)
+ options = NIL;
+ else
+ options = untransformRelOptions(datum);
+
+ ReleaseSysCache(tp);
+
+ return options;
+ }
+
+ /*
* GetFdwRoutine - call the specified foreign-data wrapper handler routine
* to get its FdwRoutine struct.
*/
diff --git a/src/include/foreign/foreign.h b/src/include/foreign/foreign.h
index 2c436ae..b6c8d5b 100644
*** a/src/include/foreign/foreign.h
--- b/src/include/foreign/foreign.h
*************** extern ForeignDataWrapper *GetForeignDat
*** 75,80 ****
--- 75,83 ----
extern ForeignDataWrapper *GetForeignDataWrapperByName(const char *name,
bool missing_ok);
extern ForeignTable *GetForeignTable(Oid relid);
+ extern char *GetFdwOptionValue(Oid relid, AttrNumber attnum,
+ const char *optname);
+ extern List *GetForeignColumnOptions(Oid relid, AttrNumber attnum);
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);
pgsql_fdw.patchtext/plain; name=pgsql_fdw.patchDownload
diff --git a/contrib/Makefile b/contrib/Makefile
index 0c238aa..e09e61e 100644
*** a/contrib/Makefile
--- b/contrib/Makefile
*************** SUBDIRS = \
*** 41,46 ****
--- 41,47 ----
pgbench \
pgcrypto \
pgrowlocks \
+ pgsql_fdw \
pgstattuple \
seg \
spi \
diff --git a/contrib/README b/contrib/README
index a1d42a1..d3fa211 100644
*** a/contrib/README
--- b/contrib/README
*************** pgrowlocks -
*** 158,163 ****
--- 158,167 ----
A function to return row locking information
by Tatsuo Ishii <ishii@sraoss.co.jp>
+ pgsql_fdw -
+ Foreign-data wrapper for external PostgreSQL servers.
+ by Shigeru Hanada <shigeru.hanada@gmail.com>
+
pgstattuple -
Functions to return statistics about "dead" tuples and free
space within a table
diff --git a/contrib/pgsql_fdw/.gitignore b/contrib/pgsql_fdw/.gitignore
index ...0854728 .
*** a/contrib/pgsql_fdw/.gitignore
--- b/contrib/pgsql_fdw/.gitignore
***************
*** 0 ****
--- 1,4 ----
+ # Generated subdirectories
+ /results/
+ *.o
+ *.so
diff --git a/contrib/pgsql_fdw/Makefile b/contrib/pgsql_fdw/Makefile
index ...6381365 .
*** a/contrib/pgsql_fdw/Makefile
--- b/contrib/pgsql_fdw/Makefile
***************
*** 0 ****
--- 1,22 ----
+ # contrib/pgsql_fdw/Makefile
+
+ MODULE_big = pgsql_fdw
+ OBJS = pgsql_fdw.o option.o deparse.o connection.o
+ PG_CPPFLAGS = -I$(libpq_srcdir)
+ SHLIB_LINK = $(libpq)
+
+ EXTENSION = pgsql_fdw
+ DATA = pgsql_fdw--1.0.sql
+
+ REGRESS = pgsql_fdw
+
+ ifdef USE_PGXS
+ PG_CONFIG = pg_config
+ PGXS := $(shell $(PG_CONFIG) --pgxs)
+ include $(PGXS)
+ else
+ subdir = contrib/pgsql_fdw
+ top_builddir = ../..
+ include $(top_builddir)/src/Makefile.global
+ include $(top_srcdir)/contrib/contrib-global.mk
+ endif
diff --git a/contrib/pgsql_fdw/connection.c b/contrib/pgsql_fdw/connection.c
index ...a2b88ec .
*** a/contrib/pgsql_fdw/connection.c
--- b/contrib/pgsql_fdw/connection.c
***************
*** 0 ****
--- 1,504 ----
+ /*-------------------------------------------------------------------------
+ *
+ * connection.c
+ * Connection management for pgsql_fdw
+ *
+ * Portions Copyright (c) 2011, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/connection.c
+ *
+ *-------------------------------------------------------------------------
+ */
+ #include "postgres.h"
+
+ #include "catalog/pg_type.h"
+ #include "foreign/foreign.h"
+ #include "funcapi.h"
+ #include "libpq-fe.h"
+ #include "mb/pg_wchar.h"
+ #include "miscadmin.h"
+ #include "utils/array.h"
+ #include "utils/builtins.h"
+ #include "utils/hsearch.h"
+ #include "utils/memutils.h"
+ #include "utils/resowner.h"
+ #include "utils/tuplestore.h"
+
+ #include "pgsql_fdw.h"
+ #include "connection.h"
+
+ /* ============================================================================
+ * Connection management functions
+ * ==========================================================================*/
+
+ /*
+ * Connection cache entry managed with hash table.
+ */
+ typedef struct ConnCacheEntry
+ {
+ /* hash key must be first */
+ Oid serverid; /* oid of foreign server */
+ Oid userid; /* oid of local user */
+
+ 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)
+ {
+ bool found;
+ ConnCacheEntry *entry;
+ ConnCacheEntry key;
+ PGconn *conn = NULL;
+
+ /* initialize connection cache if it isn't */
+ if (FSConnectionHash == NULL)
+ {
+ HASHCTL ctl;
+
+ /* hash key is a pair of oids: serverid and userid */
+ MemSet(&ctl, 0, sizeof(ctl));
+ ctl.keysize = sizeof(Oid) + sizeof(Oid);
+ ctl.entrysize = sizeof(ConnCacheEntry);
+ ctl.hash = tag_hash;
+ ctl.match = memcmp;
+ ctl.keycopy = memcpy;
+ /* allocate FSConnectionHash in the cache context */
+ ctl.hcxt = CacheMemoryContext;
+ FSConnectionHash = hash_create("Foreign Connections", 32,
+ &ctl,
+ HASH_ELEM | HASH_CONTEXT |
+ HASH_FUNCTION | HASH_COMPARE |
+ HASH_KEYCOPY);
+ }
+
+ /* Create key value for the entry. */
+ MemSet(&key, 0, sizeof(key));
+ key.serverid = server->serverid;
+ key.userid = GetOuterUserId();
+
+ /* Is there any cached and valid connection with such key? */
+ entry = hash_search(FSConnectionHash, &key, HASH_ENTER, &found);
+ if (found)
+ {
+ if (entry->conn != NULL)
+ {
+ entry->refs++;
+ elog(DEBUG1,
+ "reuse connection %u/%u (%d)",
+ entry->serverid,
+ entry->userid,
+ entry->refs);
+ 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 ResourceOwner to clean the connection up on error including
+ * user interrupt.
+ */
+ elog(DEBUG1,
+ "create entry for %u/%u (%d)",
+ entry->serverid,
+ entry->userid,
+ entry->refs);
+ 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: key items of entry has been initialized in
+ * hash_search(HASH_ENTER).
+ */
+ entry->refs = 1;
+ entry->conn = conn;
+ elog(DEBUG1,
+ "connected to %u/%u (%d)",
+ entry->serverid,
+ entry->userid,
+ 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 PGconn *
+ connect_pg_server(ForeignServer *server, UserMapping *user)
+ {
+ const char *conname = server->servername;
+ PGconn *conn;
+ PGresult *res;
+ 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. Those two object hold only libpq options.
+ * Extra 3 items are for:
+ * *) fallback_application_name
+ * *) client_encoding
+ * *) NULL termination (end marker)
+ *
+ * Note: We don't omit any parameters even target database might be older
+ * than local, because unexpected parameters are just ignored.
+ */
+ n = list_length(server->options) + list_length(user->options) + 3;
+ 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 += ExtractConnectionOptions(server->options,
+ all_keywords + n, all_values + n);
+ n += ExtractConnectionOptions(user->options,
+ all_keywords + n, all_values + n);
+ all_keywords[n] = all_values[n] = NULL;
+
+ for (i = 0, j = 0; all_keywords[i]; i++)
+ {
+ keywords[j] = all_keywords[i];
+ values[j] = all_values[i];
+ j++;
+ }
+
+ /* Use "pgsql_fdw" as fallback_application_name. */
+ keywords[j] = "fallback_application_name";
+ values[j++] = "pgsql_fdw";
+
+ /* Set client_encoding so that libpq can convert encoding properly. */
+ keywords[j] = "client_encoding";
+ values[j++] = GetDatabaseEncodingName();
+
+ keywords[j] = values[j] = NULL;
+ pfree(all_keywords);
+ pfree(all_values);
+
+ /* 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.")));
+ }
+
+ /*
+ * Start transaction to use cursor to retrieve data separately.
+ */
+ res = PQexec(conn, "BEGIN");
+ if (res == NULL || PQresultStatus(res) != PGRES_COMMAND_OK)
+ elog(ERROR, "could not start transaction");
+
+ 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 sequentially 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;
+ }
+ if (entry != NULL)
+ 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 referrer, unregister it from cache.
+ * TODO: Note that sharing connections requires a mechanism to detect
+ * change of FDW object to invalidate lasting connections.
+ */
+ entry->refs--;
+ elog(DEBUG1,
+ "connection %u/%u released (%d)",
+ entry->serverid,
+ entry->userid,
+ entry->refs);
+ }
+
+ /*
+ * 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(DEBUG1,
+ "closing connection %u/%u",
+ entry->serverid,
+ entry->userid);
+ PQfinish(entry->conn);
+ entry->refs = 0;
+ entry->conn = NULL;
+ }
+ else
+ elog(DEBUG1,
+ "connection %u/%u already closed",
+ entry->serverid,
+ entry->userid);
+ }
+
+ /*
+ * Get list of connections currently active.
+ */
+ Datum pgsql_fdw_get_connections(PG_FUNCTION_ARGS);
+ PG_FUNCTION_INFO_V1(pgsql_fdw_get_connections);
+ Datum
+ pgsql_fdw_get_connections(PG_FUNCTION_ARGS)
+ {
+ ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+ HASH_SEQ_STATUS scan;
+ ConnCacheEntry *entry;
+ MemoryContext oldcontext = CurrentMemoryContext;
+ Tuplestorestate *tuplestore;
+ TupleDesc tupdesc;
+
+ /* We return list of connection with storing them in a Tuplestore. */
+ rsinfo->returnMode = SFRM_Materialize;
+ rsinfo->setResult = NULL;
+ rsinfo->setDesc = NULL;
+
+ /* Create tuplestore and copy of TupleDesc in per-query context. */
+ MemoryContextSwitchTo(rsinfo->econtext->ecxt_per_query_memory);
+
+ tupdesc = CreateTemplateTupleDesc(2, false);
+ TupleDescInitEntry(tupdesc, 1, "srvid", OIDOID, -1, 0);
+ TupleDescInitEntry(tupdesc, 2, "usesysid", OIDOID, -1, 0);
+ rsinfo->setDesc = tupdesc;
+
+ tuplestore = tuplestore_begin_heap(false, false, work_mem);
+ rsinfo->setResult = tuplestore;
+
+ MemoryContextSwitchTo(oldcontext);
+
+ /*
+ * We need to scan sequentially since we use the address to find
+ * appropriate PGconn from the hash table.
+ */
+ if (FSConnectionHash != NULL)
+ {
+ hash_seq_init(&scan, FSConnectionHash);
+ while ((entry = (ConnCacheEntry *) hash_seq_search(&scan)))
+ {
+ Datum values[2];
+ bool nulls[2];
+ HeapTuple tuple;
+
+ elog(DEBUG1, "found: %u/%u", entry->serverid, entry->userid);
+
+ /* Ignore inactive connections */
+ if (PQstatus(entry->conn) != CONNECTION_OK)
+ continue;
+
+ /*
+ * Ignore other users' connections if current user isn't a
+ * superuser.
+ */
+ if (!superuser() && entry->userid != GetUserId())
+ continue;
+
+ values[0] = ObjectIdGetDatum(entry->serverid);
+ values[1] = ObjectIdGetDatum(entry->userid);
+ nulls[0] = false;
+ nulls[1] = false;
+
+ tuple = heap_formtuple(tupdesc, values, nulls);
+ tuplestore_puttuple(tuplestore, tuple);
+ }
+ }
+ tuplestore_donestoring(tuplestore);
+
+ PG_RETURN_VOID();
+ }
+
+ /*
+ * Discard persistent connection designated by given connection name.
+ */
+ Datum pgsql_fdw_disconnect(PG_FUNCTION_ARGS);
+ PG_FUNCTION_INFO_V1(pgsql_fdw_disconnect);
+ Datum
+ pgsql_fdw_disconnect(PG_FUNCTION_ARGS)
+ {
+ Oid serverid = PG_GETARG_OID(0);
+ Oid userid = PG_GETARG_OID(1);
+ ConnCacheEntry key;
+ ConnCacheEntry *entry = NULL;
+ bool found;
+
+ /* Non-superuser can't discard other users' connection. */
+ if (!superuser() && userid != GetOuterUserId())
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_RESOURCES),
+ errmsg("only superuser can discard other user's connection")));
+
+ /*
+ * If no connection has been established, or no such connections, just
+ * return "NG" to indicate nothing has done.
+ */
+ if (FSConnectionHash == NULL)
+ PG_RETURN_TEXT_P(cstring_to_text("NG"));
+
+ key.serverid = serverid;
+ key.userid = userid;
+ entry = hash_search(FSConnectionHash, &key, HASH_FIND, &found);
+ if (!found)
+ PG_RETURN_TEXT_P(cstring_to_text("NG"));
+
+ /* Discard cached connection, and clear reference counter. */
+ PQfinish(entry->conn);
+ entry->refs = 0;
+ entry->conn = NULL;
+ elog(DEBUG1, "closed connection %u/%u", serverid, userid);
+
+ PG_RETURN_TEXT_P(cstring_to_text("OK"));
+ }
diff --git a/contrib/pgsql_fdw/connection.h b/contrib/pgsql_fdw/connection.h
index ...694534f .
*** a/contrib/pgsql_fdw/connection.h
--- b/contrib/pgsql_fdw/connection.h
***************
*** 0 ****
--- 1,25 ----
+ /*-------------------------------------------------------------------------
+ *
+ * connection.h
+ * Connection management for pgsql_fdw
+ *
+ * Portions Copyright (c) 2011, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_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/pgsql_fdw/deparse.c b/contrib/pgsql_fdw/deparse.c
index ...db2b472 .
*** a/contrib/pgsql_fdw/deparse.c
--- b/contrib/pgsql_fdw/deparse.c
***************
*** 0 ****
--- 1,352 ----
+ /*-------------------------------------------------------------------------
+ *
+ * deparse.c
+ * query deparser for PostgreSQL
+ *
+ * Copyright (c) 2011, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/deparse.c
+ *
+ *-------------------------------------------------------------------------
+ */
+ #include "postgres.h"
+
+ #include "access/transam.h"
+ #include "foreign/foreign.h"
+ #include "lib/stringinfo.h"
+ #include "nodes/nodeFuncs.h"
+ #include "nodes/nodes.h"
+ #include "nodes/makefuncs.h"
+ #include "optimizer/clauses.h"
+ #include "optimizer/var.h"
+ #include "parser/parsetree.h"
+ #include "utils/builtins.h"
+ #include "utils/lsyscache.h"
+
+ #include "pgsql_fdw.h"
+
+ /*
+ * Context for walk-through the expression tree.
+ */
+ typedef struct foreign_executable_cxt
+ {
+ PlannerInfo *root;
+ RelOptInfo *foreignrel;
+ } foreign_executable_cxt;
+
+ static bool is_foreign_expr(PlannerInfo *root, RelOptInfo *baserel, Expr *expr);
+ static bool foreign_expr_walker(Node *node, foreign_executable_cxt *context);
+ static bool is_proc_remotely_executable(Oid procid);
+
+ /*
+ * Deparse query representation into SQL statement which suits for remote
+ * PostgreSQL server. Also some of quals in WHERE clause will be pushed down
+ * If they are safe to be evaluated on the remote side.
+ */
+ char *
+ deparseSql(Oid relid, PlannerInfo *root, RelOptInfo *baserel)
+ {
+ StringInfoData foreign_relname;
+ StringInfoData sql; /* builder for SQL statement */
+ bool first;
+ AttrNumber attr;
+ List *attr_used = NIL; /* List of AttNumber used in the query */
+ const char *nspname = NULL; /* plain namespace name */
+ const char *relname = NULL; /* plain relation name */
+ const char *q_nspname; /* quoted namespace name */
+ const char *q_relname; /* quoted relation name */
+ List *foreign_expr = NIL; /* list of Expr* evaluated on remote */
+ int i;
+ List *rtable = NIL;
+ List *context = NIL;
+
+ initStringInfo(&sql);
+ initStringInfo(&foreign_relname);
+
+ /*
+ * First of all, determine which qual can be pushed down.
+ *
+ * The expressions which satisfy is_foreign_expr() are deparsed into WHERE
+ * clause of result SQL string, and they could be removed from PlanState
+ * to avoid duplicate evaluation at ExecScan().
+ *
+ * We never change the quals in the Plan node, because this execution might
+ * be for a PREPAREd statement, thus the quals in the Plan node might be
+ * reused to construct another PlanState for subsequent EXECUTE statement.
+ *
+ * We do this before deparsing SELECT clause because attributes which are
+ * not used in neither reltargetlist nor baserel->baserestrictinfo, quals
+ * evaluated on local, can be replaced with literal "NULL" in the SELECT
+ * clause to reduce overhead of tuple handling tuple and data transfer.
+ */
+ if (baserel->baserestrictinfo != NIL)
+ {
+ ListCell *lc;
+ List *local_qual = NIL;
+
+ foreach (lc, baserel->baserestrictinfo)
+ {
+ RestrictInfo *ri = (RestrictInfo *) lfirst(lc);
+
+ /* Determine whether the qual can be pushed down or not. */
+ if (is_foreign_expr(root, baserel, ri->clause))
+ foreign_expr = lappend(foreign_expr, ri->clause);
+ else
+ {
+ List *attrs;
+
+ /*
+ * We need to know which attributes are used in qual evaluated
+ * on the local server, because they should be listed in the
+ * SELECT clause of remote query. We can ignore attributes
+ * which are referenced only in ORDER BY/GROUP BY clause because
+ * such attributes has already been kept in reltargetlist.
+ */
+ attrs = pull_var_clause((Node *) ri->clause,
+ PVC_RECURSE_AGGREGATES,
+ PVC_RECURSE_PLACEHOLDERS);
+ attr_used = list_union(attr_used, attrs);
+
+ /*
+ * Save the RestrictInfo to replace baserel->baserestrictinfo
+ * afterward.
+ */
+ local_qual = lappend(local_qual, ri);
+ }
+ }
+
+ /*
+ * Remove quals which are going to evaluated on the foreign server to
+ * avoid overhead of duplicated evaluation.
+ */
+ baserel->baserestrictinfo = local_qual;
+ }
+
+ /*
+ * Determine foreign relation's qualified name. This is necessary for
+ * FROM clause and SELECT clause.
+ */
+ nspname = GetFdwOptionValue(relid, InvalidAttrNumber, "nspname");
+ if (nspname == NULL)
+ nspname = get_namespace_name(get_rel_namespace(relid));
+ q_nspname = quote_identifier(nspname);
+
+ relname = GetFdwOptionValue(relid, InvalidAttrNumber, "relname");
+ if (relname == NULL)
+ relname = get_rel_name(relid);
+ q_relname = quote_identifier(relname);
+
+ appendStringInfo(&foreign_relname, "%s.%s", q_nspname, q_relname);
+
+ /*
+ * Create context for the deparse.
+ * We need multi-relation context without PlanState, and alias of each
+ * RangeTblEntry should be modified to be valid on remote side.
+ *
+ * We skip first element of simple_rel_array.
+ */
+ for (i = 1; i < root->simple_rel_array_size; i++)
+ {
+ RangeTblEntry *rte = copyObject(root->simple_rte_array[i]);
+ rtable = lappend(rtable, rte);
+ }
+ context = deparse_context_for_rtelist(rtable);
+
+ /*
+ * deparse SELECT clause
+ *
+ * List attributes which are in either target list or local restriction.
+ * Unused attributes are replace with literal "NULL" for optimization.
+ */
+ appendStringInfo(&sql, "SELECT ");
+ attr_used = list_union(attr_used, baserel->reltargetlist);
+ first = true;
+ for (attr = 1; attr <= baserel->max_attr; attr++)
+ {
+ RangeTblEntry *rte = root->simple_rte_array[baserel->relid];
+ Var *var = NULL;
+ ListCell *lc;
+
+ /* Ignore droppped attributes. */
+ if (get_rte_attribute_is_dropped(rte, attr))
+ continue;
+
+ if (!first)
+ appendStringInfo(&sql, ", ");
+ first = false;
+
+ /*
+ * We use linear search here, but it wouldn't be problem since
+ * attr_used seems to not become so large.
+ */
+ foreach (lc, attr_used)
+ {
+ var = lfirst(lc);
+ if (var->varattno == attr)
+ break;
+ var = NULL;
+ }
+ if (var != NULL)
+ appendStringInfo(&sql, "%s",
+ deparse_expression((Node *) var, context, false, false));
+ else
+ appendStringInfo(&sql, "NULL");
+ }
+ appendStringInfoChar(&sql, ' ');
+
+ /*
+ * deparse FROM clause
+ */
+ appendStringInfo(&sql, "FROM %s", foreign_relname.data);
+
+ /*
+ * deparse WHERE clause
+ */
+ if (foreign_expr != NIL)
+ {
+ Node *node;
+
+ node = (Node *) make_ands_explicit(foreign_expr);
+ appendStringInfo(&sql, " WHERE %s ",
+ deparse_expression(node, context, false, false));
+ list_free(foreign_expr);
+ foreign_expr = NIL;
+ }
+
+ return sql.data;
+ }
+
+ /*
+ * Returns true if expr is safe to be evaluated on the foreign server.
+ */
+ static bool
+ is_foreign_expr(PlannerInfo *root, RelOptInfo *baserel, Expr *expr)
+ {
+ foreign_executable_cxt context;
+ context.root = root;
+ context.foreignrel = baserel;
+
+ /*
+ * An expression which includes any mutable function can't be pushed down
+ * because it's result is not stable. For example, pushing now() down to
+ * remote side would cause confusion from the clock offset.
+ * If we have routine mapping infrastructure in future release, we will be
+ * able to choose function to be pushed down in finer granularity.
+ */
+ if (contain_mutable_functions((Node *) expr))
+ return false;
+
+ /*
+ * Check that the expression consists of nodes which are known as safe to
+ * be pushed down.
+ */
+ if (foreign_expr_walker((Node *) expr, &context))
+ return false;
+
+ return true;
+ }
+
+ /*
+ * Return true if node includes any node which is not known as safe to be
+ * pushed down.
+ */
+ static bool
+ foreign_expr_walker(Node *node, foreign_executable_cxt *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 subtre of the node, if any, should be checked
+ * continuously at the tail of this function.
+ */
+ break;
+ case T_Param:
+ /*
+ * Only external parameters can be pushed down.:
+ */
+ {
+ if (((Param *) node)->paramkind != PARAM_EXTERN)
+ return true;
+ }
+ break;
+ case T_OpExpr:
+ /*
+ * Operators which use non-immutable function can't be pushed down.
+ */
+ {
+ OpExpr *oe = (OpExpr *) node;
+
+ if (!is_proc_remotely_executable(oe->opfuncid))
+ return true;
+
+ /* operands are checked later */
+ }
+ break;
+ case T_FuncExpr:
+ /*
+ * Non-immutable functions can't be pushed down.
+ */
+ {
+ FuncExpr *fe = (FuncExpr *) node;
+
+ if (!is_proc_remotely_executable(fe->funcid))
+ return true;
+
+ /* operands are checked later */
+ }
+ 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;
+ foreign_executable_cxt *f_context;
+
+ f_context = (foreign_executable_cxt *) context;
+ if (var->varno != f_context->foreignrel->relid ||
+ var->varlevelsup != 0)
+ return true;
+ }
+ break;
+ default:
+ {
+ ereport(DEBUG3,
+ (errmsg("expression is too complex"),
+ errdetail("%s", nodeToString(node))));
+ return true;
+ }
+ break;
+ }
+
+ return expression_tree_walker(node, foreign_expr_walker, context);
+ }
+
+ /*
+ * Return true if func is known as safe to be pushed down if all of the
+ * arguments are also known as safe.
+ */
+ static bool
+ is_proc_remotely_executable(Oid procid)
+ {
+ /*
+ * User-defined procedures can't be pushed down.
+ */
+ if (procid >= FirstNormalObjectId)
+ return false;
+
+ return true;
+ }
+
diff --git a/contrib/pgsql_fdw/expected/pgsql_fdw.out b/contrib/pgsql_fdw/expected/pgsql_fdw.out
index ...7bfbfa7 .
*** a/contrib/pgsql_fdw/expected/pgsql_fdw.out
--- b/contrib/pgsql_fdw/expected/pgsql_fdw.out
***************
*** 0 ****
--- 1,406 ----
+ -- ===================================================================
+ -- create FDW objects
+ -- ===================================================================
+ CREATE EXTENSION pgsql_fdw;
+ CREATE SERVER loopback1 FOREIGN DATA WRAPPER pgsql_fdw;
+ CREATE SERVER loopback2 FOREIGN DATA WRAPPER pgsql_fdw
+ OPTIONS (dbname 'contrib_regression');
+ CREATE USER MAPPING FOR public SERVER loopback1
+ OPTIONS (user 'value', password 'value');
+ CREATE USER MAPPING FOR public SERVER loopback2;
+ CREATE FOREIGN TABLE ft1 (
+ c1 int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp
+ ) SERVER loopback2;
+ CREATE FOREIGN TABLE ft2 (
+ c1 int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp
+ ) SERVER loopback2;
+ -- ===================================================================
+ -- create objects used through FDW
+ -- ===================================================================
+ CREATE SCHEMA "S 1";
+ CREATE TABLE "S 1"."T 1" (
+ c1 int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ CONSTRAINT t1_pkey PRIMARY KEY (c1)
+ );
+ NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "t1_pkey" for table "T 1"
+ CREATE TABLE "S 1"."T 2" (
+ c1 int NOT NULL,
+ c2 text,
+ CONSTRAINT t2_pkey PRIMARY KEY (c1)
+ );
+ NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "t2_pkey" for table "T 2"
+ BEGIN;
+ TRUNCATE "S 1"."T 1";
+ INSERT INTO "S 1"."T 1"
+ SELECT id,
+ id % 10,
+ to_char(id, 'FM00000'),
+ '1970-01-01'::timestamptz + ((id % 100) || ' days')::interval,
+ '1970-01-01'::timestamp + ((id % 100) || ' days')::interval
+ FROM generate_series(1, 1000) id;
+ TRUNCATE "S 1"."T 2";
+ INSERT INTO "S 1"."T 2"
+ SELECT id,
+ 'AAA' || to_char(id, 'FM000')
+ FROM generate_series(1, 100) id;
+ COMMIT;
+ -- ===================================================================
+ -- tests for pgsql_fdw_validator
+ -- ===================================================================
+ ALTER FOREIGN DATA WRAPPER pgsql_fdw OPTIONS (host 'value'); -- ERRROR
+ ERROR: invalid option "host"
+ HINT: Valid options in this context are:
+ -- requiressl, krbsrvname and gsslib are omitted because they depend on
+ -- configure option
+ ALTER SERVER loopback1 OPTIONS (
+ authtype 'value',
+ service 'value',
+ connect_timeout 'value',
+ dbname 'value',
+ host 'value',
+ hostaddr 'value',
+ port 'value',
+ --client_encoding 'value',
+ tty 'value',
+ options 'value',
+ application_name 'value',
+ --fallback_application_name 'value',
+ keepalives 'value',
+ keepalives_idle 'value',
+ keepalives_interval 'value',
+ -- requiressl 'value',
+ sslmode 'value',
+ sslcert 'value',
+ sslkey 'value',
+ sslrootcert 'value',
+ sslcrl 'value'
+ --requirepeer 'value',
+ -- krbsrvname 'value',
+ -- gsslib 'value',
+ --replication 'value'
+ );
+ ALTER SERVER loopback1 OPTIONS (user 'value'); -- ERROR
+ ERROR: invalid option "user"
+ HINT: Valid options in this context are: authtype, service, connect_timeout, dbname, host, hostaddr, port, tty, options, application_name, keepalives, keepalives_idle, keepalives_interval, keepalives_count, sslmode, sslcert, sslkey, sslrootcert, sslcrl, requirepeer, min_cursor_rows, fetch_count
+ ALTER SERVER loopback2 OPTIONS (ADD min_cursor_rows '0');
+ ALTER SERVER loopback2 OPTIONS (ADD fetch_count '2');
+ ALTER USER MAPPING FOR public SERVER loopback1
+ OPTIONS (DROP user, DROP password);
+ ALTER USER MAPPING FOR public SERVER loopback1
+ OPTIONS (host 'value'); -- ERROR
+ ERROR: invalid option "host"
+ HINT: Valid options in this context are: user, password
+ ALTER FOREIGN TABLE ft1 OPTIONS (nspname 'S 1', relname 'T 1');
+ ALTER FOREIGN TABLE ft2 OPTIONS (nspname 'S 1', relname 'T 1', min_cursor_rows '10', fetch_count '100');
+ ALTER FOREIGN TABLE ft1 OPTIONS (invalid 'value'); -- ERROR
+ ERROR: invalid option "invalid"
+ HINT: Valid options in this context are: nspname, relname, min_cursor_rows, fetch_count
+ ALTER FOREIGN TABLE ft1 OPTIONS (min_cursor_rows 'a'); -- ERROR
+ ERROR: invalid value for min_cursor_rows: "a"
+ ALTER FOREIGN TABLE ft1 OPTIONS (min_cursor_rows '-1'); -- ERROR
+ ERROR: invalid value for min_cursor_rows: "-1"
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count 'a'); -- ERROR
+ ERROR: invalid value for fetch_count: "a"
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count '0'); -- ERROR
+ ERROR: invalid value for fetch_count: "0"
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count '-1'); -- ERROR
+ ERROR: invalid value for fetch_count: "-1"
+ \dew+
+ List of foreign-data wrappers
+ Name | Owner | Handler | Validator | Access privileges | FDW Options | Description
+ -----------+----------+-------------------+---------------------+-------------------+-------------+-------------
+ pgsql_fdw | postgres | pgsql_fdw_handler | pgsql_fdw_validator | | |
+ (1 row)
+
+ \des+
+ List of foreign servers
+ Name | Owner | Foreign-data wrapper | Access privileges | Type | Version | FDW Options | Description
+ -----------+----------+----------------------+-------------------+------+---------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+-------------
+ loopback1 | postgres | pgsql_fdw | | | | (authtype 'value', service 'value', connect_timeout 'value', dbname 'value', host 'value', hostaddr 'value', port 'value', tty 'value', options 'value', application_name 'value', keepalives 'value', keepalives_idle 'value', keepalives_interval 'value', sslmode 'value', sslcert 'value', sslkey 'value', sslrootcert 'value', sslcrl 'value') |
+ loopback2 | postgres | pgsql_fdw | | | | (dbname 'contrib_regression', min_cursor_rows '0', fetch_count '2') |
+ (2 rows)
+
+ \deu+
+ List of user mappings
+ Server | User name | FDW Options
+ -----------+-----------+-------------
+ loopback1 | public |
+ loopback2 | public |
+ (2 rows)
+
+ \det+
+ List of foreign tables
+ Schema | Table | Server | FDW Options | Description
+ --------+-------+-----------+-------------------------------------------------------------------------+-------------
+ public | ft1 | loopback2 | (nspname 'S 1', relname 'T 1') |
+ public | ft2 | loopback2 | (nspname 'S 1', relname 'T 1', min_cursor_rows '10', fetch_count '100') |
+ (2 rows)
+
+ -- ===================================================================
+ -- simple queries
+ -- ===================================================================
+ -- single table, with/without alias
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
+ QUERY PLAN
+ ----------------------------------------------------------------------
+ Limit
+ Output: c1, c2, c3, c4, c5
+ -> Sort
+ Output: c1, c2, c3, c4, c5
+ Sort Key: ft1.c3, ft1.c1
+ -> Foreign Scan on public.ft1
+ Output: c1, c2, c3, c4, c5
+ Remote SQL: SELECT c1, c2, c3, c4, c5 FROM "S 1"."T 1"
+ (8 rows)
+
+ SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
+ c1 | c2 | c3 | c4 | c5
+ -----+----+-------+------------------------------+--------------------------
+ 101 | 1 | 00101 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970
+ 102 | 2 | 00102 | Sat Jan 03 00:00:00 1970 PST | Sat Jan 03 00:00:00 1970
+ 103 | 3 | 00103 | Sun Jan 04 00:00:00 1970 PST | Sun Jan 04 00:00:00 1970
+ 104 | 4 | 00104 | Mon Jan 05 00:00:00 1970 PST | Mon Jan 05 00:00:00 1970
+ 105 | 5 | 00105 | Tue Jan 06 00:00:00 1970 PST | Tue Jan 06 00:00:00 1970
+ 106 | 6 | 00106 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970
+ 107 | 7 | 00107 | Thu Jan 08 00:00:00 1970 PST | Thu Jan 08 00:00:00 1970
+ 108 | 8 | 00108 | Fri Jan 09 00:00:00 1970 PST | Fri Jan 09 00:00:00 1970
+ 109 | 9 | 00109 | Sat Jan 10 00:00:00 1970 PST | Sat Jan 10 00:00:00 1970
+ 110 | 0 | 00110 | Sun Jan 11 00:00:00 1970 PST | Sun Jan 11 00:00:00 1970
+ (10 rows)
+
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+ QUERY PLAN
+ ----------------------------------------------------------------------
+ Limit
+ Output: c1, c2, c3, c4, c5
+ -> Sort
+ Output: c1, c2, c3, c4, c5
+ Sort Key: t1.c3, t1.c1
+ -> Foreign Scan on public.ft1 t1
+ Output: c1, c2, c3, c4, c5
+ Remote SQL: SELECT c1, c2, c3, c4, c5 FROM "S 1"."T 1"
+ (8 rows)
+
+ SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+ c1 | c2 | c3 | c4 | c5
+ -----+----+-------+------------------------------+--------------------------
+ 101 | 1 | 00101 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970
+ 102 | 2 | 00102 | Sat Jan 03 00:00:00 1970 PST | Sat Jan 03 00:00:00 1970
+ 103 | 3 | 00103 | Sun Jan 04 00:00:00 1970 PST | Sun Jan 04 00:00:00 1970
+ 104 | 4 | 00104 | Mon Jan 05 00:00:00 1970 PST | Mon Jan 05 00:00:00 1970
+ 105 | 5 | 00105 | Tue Jan 06 00:00:00 1970 PST | Tue Jan 06 00:00:00 1970
+ 106 | 6 | 00106 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970
+ 107 | 7 | 00107 | Thu Jan 08 00:00:00 1970 PST | Thu Jan 08 00:00:00 1970
+ 108 | 8 | 00108 | Fri Jan 09 00:00:00 1970 PST | Fri Jan 09 00:00:00 1970
+ 109 | 9 | 00109 | Sat Jan 10 00:00:00 1970 PST | Sat Jan 10 00:00:00 1970
+ 110 | 0 | 00110 | Sun Jan 11 00:00:00 1970 PST | Sun Jan 11 00:00:00 1970
+ (10 rows)
+
+ -- with WHERE clause
+ SELECT * FROM ft1 t1 WHERE t1.c1 = 101;
+ c1 | c2 | c3 | c4 | c5
+ -----+----+-------+------------------------------+--------------------------
+ 101 | 1 | 00101 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970
+ (1 row)
+
+ -- aggregate
+ SELECT COUNT(*) FROM ft1 t1;
+ count
+ -------
+ 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
+ ----+----+-------+------------------------------+--------------------------
+ 1 | 1 | 00001 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970
+ 2 | 2 | 00002 | Sat Jan 03 00:00:00 1970 PST | Sat Jan 03 00:00:00 1970
+ 3 | 3 | 00003 | Sun Jan 04 00:00:00 1970 PST | Sun Jan 04 00:00:00 1970
+ 4 | 4 | 00004 | Mon Jan 05 00:00:00 1970 PST | Mon Jan 05 00:00:00 1970
+ 5 | 5 | 00005 | Tue Jan 06 00:00:00 1970 PST | Tue Jan 06 00:00:00 1970
+ 6 | 6 | 00006 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970
+ 7 | 7 | 00007 | Thu Jan 08 00:00:00 1970 PST | Thu Jan 08 00:00:00 1970
+ 8 | 8 | 00008 | Fri Jan 09 00:00:00 1970 PST | Fri Jan 09 00:00:00 1970
+ 9 | 9 | 00009 | Sat Jan 10 00:00:00 1970 PST | Sat Jan 10 00:00:00 1970
+ 10 | 0 | 00010 | Sun Jan 11 00:00:00 1970 PST | Sun Jan 11 00:00:00 1970
+ (10 rows)
+
+ -- subquery+MAX
+ SELECT * FROM ft1 t1 WHERE t1.c3 = (SELECT MAX(c3) FROM ft2 t2) ORDER BY c1;
+ c1 | c2 | c3 | c4 | c5
+ ------+----+-------+------------------------------+--------------------------
+ 1000 | 0 | 01000 | Thu Jan 01 00:00:00 1970 PST | Thu Jan 01 00:00:00 1970
+ (1 row)
+
+ -- used in CTE
+ WITH t1 AS (SELECT * FROM ft1 WHERE c1 <= 10) SELECT t2.c1, t2.c2, t2.c3, t2.c4 FROM t1, ft2 t2 WHERE t1.c1 = t2.c1 ORDER BY t1.c1;
+ c1 | c2 | c3 | c4
+ ----+----+-------+------------------------------
+ 1 | 1 | 00001 | Fri Jan 02 00:00:00 1970 PST
+ 2 | 2 | 00002 | Sat Jan 03 00:00:00 1970 PST
+ 3 | 3 | 00003 | Sun Jan 04 00:00:00 1970 PST
+ 4 | 4 | 00004 | Mon Jan 05 00:00:00 1970 PST
+ 5 | 5 | 00005 | Tue Jan 06 00:00:00 1970 PST
+ 6 | 6 | 00006 | Wed Jan 07 00:00:00 1970 PST
+ 7 | 7 | 00007 | Thu Jan 08 00:00:00 1970 PST
+ 8 | 8 | 00008 | Fri Jan 09 00:00:00 1970 PST
+ 9 | 9 | 00009 | Sat Jan 10 00:00:00 1970 PST
+ 10 | 0 | 00010 | Sun Jan 11 00:00:00 1970 PST
+ (10 rows)
+
+ -- fixed values
+ SELECT 'fixed', NULL FROM ft1 t1 WHERE c1 = 1;
+ ?column? | ?column?
+ ----------+----------
+ fixed |
+ (1 row)
+
+ -- ===================================================================
+ -- parameterized queries
+ -- ===================================================================
+ -- simple join
+ PREPARE st1(int, int) AS SELECT t1.c3, t2.c3 FROM ft1 t1, ft2 t2 WHERE t1.c1 = $1 AND t2.c1 = $2;
+ EXPLAIN (COSTS false) EXECUTE st1(1, 2);
+ QUERY PLAN
+ ----------------------------------------------------------------------------------------
+ Nested Loop
+ -> Foreign Scan on ft1 t1
+ Remote SQL: SELECT NULL, NULL, c3, NULL, NULL FROM "S 1"."T 1" WHERE (c1 = 1)
+ -> Foreign Scan on ft2 t2
+ Remote SQL: SELECT NULL, NULL, c3, NULL, NULL FROM "S 1"."T 1" WHERE (c1 = 2)
+ (5 rows)
+
+ EXECUTE st1(1, 1);
+ c3 | c3
+ -------+-------
+ 00001 | 00001
+ (1 row)
+
+ EXECUTE st1(101, 101);
+ c3 | c3
+ -------+-------
+ 00101 | 00101
+ (1 row)
+
+ -- subquery using stable function (can't be pushed down)
+ PREPARE st2(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c4) = 6) ORDER BY c1;
+ EXPLAIN (COSTS false) EXECUTE st2(10, 20);
+ QUERY PLAN
+ ------------------------------------------------------------------------------------------------------------------------------------------------
+ Sort
+ Sort Key: t1.c1
+ -> Nested Loop
+ Join Filter: (t1.c3 = t2.c3)
+ -> HashAggregate
+ -> Foreign Scan on ft2 t2
+ Filter: (date_part('dow'::text, c4) = 6::double precision)
+ Remote SQL: DECLARE pgsql_fdw_cursor_6 SCROLL CURSOR FOR SELECT NULL, NULL, c3, c4, NULL FROM "S 1"."T 1" WHERE (c1 > 10)
+ -> Foreign Scan on ft1 t1
+ Remote SQL: SELECT c1, c2, c3, c4, c5 FROM "S 1"."T 1" WHERE (c1 < 20)
+ (10 rows)
+
+ EXECUTE st2(10, 20);
+ c1 | c2 | c3 | c4 | c5
+ ----+----+-------+------------------------------+--------------------------
+ 16 | 6 | 00016 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970
+ (1 row)
+
+ EXECUTE st1(101, 101);
+ c3 | c3
+ -------+-------
+ 00101 | 00101
+ (1 row)
+
+ -- subquery using immutable function (can be pushed down)
+ PREPARE st3(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c5) = 6) ORDER BY c1;
+ EXPLAIN (COSTS false) EXECUTE st3(10, 20);
+ QUERY PLAN
+ --------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Sort
+ Sort Key: t1.c1
+ -> Hash Join
+ Hash Cond: (t1.c3 = t2.c3)
+ -> Foreign Scan on ft1 t1
+ Remote SQL: SELECT c1, c2, c3, c4, c5 FROM "S 1"."T 1" WHERE (c1 < 20)
+ -> Hash
+ -> HashAggregate
+ -> Foreign Scan on ft2 t2
+ Remote SQL: SELECT NULL, NULL, c3, NULL, NULL FROM "S 1"."T 1" WHERE ((c1 > 10) AND (date_part('dow'::text, c5) = 6::double precision))
+ (10 rows)
+
+ EXECUTE st3(10, 20);
+ c1 | c2 | c3 | c4 | c5
+ ----+----+-------+------------------------------+--------------------------
+ 16 | 6 | 00016 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970
+ (1 row)
+
+ EXECUTE st3(20, 30);
+ c1 | c2 | c3 | c4 | c5
+ ----+----+-------+------------------------------+--------------------------
+ 23 | 3 | 00023 | Sat Jan 24 00:00:00 1970 PST | Sat Jan 24 00:00:00 1970
+ (1 row)
+
+ -- cleanup
+ DEALLOCATE st1;
+ DEALLOCATE st2;
+ DEALLOCATE st3;
+ -- ===================================================================
+ -- connection management
+ -- ===================================================================
+ SELECT srvname, usename FROM pgsql_fdw_connections;
+ srvname | usename
+ -----------+----------
+ loopback2 | postgres
+ (1 row)
+
+ SELECT pgsql_fdw_disconnect(srvid, usesysid) FROM pgsql_fdw_get_connections();
+ pgsql_fdw_disconnect
+ ----------------------
+ OK
+ (1 row)
+
+ SELECT srvname, usename FROM pgsql_fdw_connections;
+ srvname | usename
+ ---------+---------
+ (0 rows)
+
+ -- ===================================================================
+ -- cleanup
+ -- ===================================================================
+ DROP EXTENSION pgsql_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 server loopback2
+ drop cascades to user mapping for public
+ drop cascades to foreign table ft1
+ drop cascades to foreign table ft2
diff --git a/contrib/pgsql_fdw/option.c b/contrib/pgsql_fdw/option.c
index ...7463237 .
*** a/contrib/pgsql_fdw/option.c
--- b/contrib/pgsql_fdw/option.c
***************
*** 0 ****
--- 1,271 ----
+ /*-------------------------------------------------------------------------
+ *
+ * option.c
+ * FDW option handling
+ *
+ * Copyright (c) 2011, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/option.c
+ *
+ *-------------------------------------------------------------------------
+ */
+ #include "postgres.h"
+
+ #include "access/reloptions.h"
+ #include "catalog/pg_foreign_data_wrapper.h"
+ #include "catalog/pg_foreign_server.h"
+ #include "catalog/pg_foreign_table.h"
+ #include "catalog/pg_user_mapping.h"
+ #include "fmgr.h"
+ #include "foreign/foreign.h"
+ #include "lib/stringinfo.h"
+ #include "miscadmin.h"
+
+ #include "pgsql_fdw.h"
+
+ /*
+ * SQL functions
+ */
+ extern Datum pgsql_fdw_validator(PG_FUNCTION_ARGS);
+ PG_FUNCTION_INFO_V1(pgsql_fdw_validator);
+
+ /*
+ * Describes the valid options for objects that use this wrapper.
+ */
+ typedef struct PgsqlFdwOption
+ {
+ const char *optname;
+ Oid optcontext; /* Oid of catalog in which options may appear */
+ bool is_libpq_opt; /* true if it's used in libpq */
+ } PgsqlFdwOption;
+
+ /*
+ * Valid options for pgsql_fdw.
+ */
+ static PgsqlFdwOption valid_options[] = {
+
+ /*
+ * Options for libpq connection.
+ * Note: This list should be updated along with PQconninfoOptions in
+ * interfaces/libpq/fe-connect.c, so the order is kept as is.
+ *
+ * Some useless libpq connection options are not accepted by pgsql_fdw:
+ * client_encoding: set to local database encoding automatically
+ * fallback_application_name: fixed to "pgsql_fdw"
+ * replication: pgsql_fdw never be replication client
+ */
+ {"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},
+ #ifdef NOT_USED
+ {"client_encoding", ForeignServerRelationId, true},
+ #endif
+ {"tty", ForeignServerRelationId, true},
+ {"options", ForeignServerRelationId, true},
+ {"application_name", ForeignServerRelationId, true},
+ #ifdef NOT_USED
+ {"fallback_application_name", ForeignServerRelationId, true},
+ #endif
+ {"keepalives", ForeignServerRelationId, true},
+ {"keepalives_idle", ForeignServerRelationId, true},
+ {"keepalives_interval", ForeignServerRelationId, true},
+ {"keepalives_count", ForeignServerRelationId, true},
+ #ifdef USE_SSL
+ {"requiressl", ForeignServerRelationId, true},
+ #endif
+ {"sslmode", ForeignServerRelationId, true},
+ {"sslcert", ForeignServerRelationId, true},
+ {"sslkey", ForeignServerRelationId, true},
+ {"sslrootcert", ForeignServerRelationId, true},
+ {"sslcrl", ForeignServerRelationId, true},
+ {"requirepeer", ForeignServerRelationId, true},
+ #if defined(KRB5) || defined(ENABLE_GSS) || defined(ENABLE_SSPI)
+ {"krbsrvname", ForeignServerRelationId, true},
+ #endif
+ #if defined(ENABLE_GSS) && defined(ENABLE_SSPI)
+ {"gsslib", ForeignServerRelationId, true},
+ #endif
+ #ifdef NOT_USED
+ {"replication", ForeignServerRelationId, true},
+ #endif
+
+ /*
+ * Options for translation of object names.
+ * Note: Per-column options are not supported in 9.1, so we can't translate
+ * column name.
+ */
+ {"nspname", ForeignTableRelationId, false},
+ {"relname", ForeignTableRelationId, false},
+
+ /*
+ * Options for cursor behavior.
+ * These options can be overridden by smaller objects.
+ */
+ {"min_cursor_rows", ForeignTableRelationId, false},
+ {"min_cursor_rows", ForeignServerRelationId, false},
+ {"fetch_count", ForeignTableRelationId, false},
+ {"fetch_count", ForeignServerRelationId, false},
+
+ /* Terminating entry --- MUST BE LAST */
+ {NULL, InvalidOid, false}
+ };
+
+ /*
+ * Helper functions
+ */
+ static bool is_valid_option(const char *optname, Oid context);
+
+ /*
+ * Validate the generic options given to a FOREIGN DATA WRAPPER, SERVER,
+ * USER MAPPING or FOREIGN TABLE that uses pgsql_fdw.
+ *
+ * Raise an ERROR if the option or its value is considered invalid.
+ */
+ Datum
+ pgsql_fdw_validator(PG_FUNCTION_ARGS)
+ {
+ List *options_list = untransformRelOptions(PG_GETARG_DATUM(0));
+ Oid catalog = PG_GETARG_OID(1);
+ ListCell *cell;
+
+ /*
+ * Check that only options supported by pgsql_fdw, and allowed for the
+ * current object type, are given.
+ */
+ foreach(cell, options_list)
+ {
+ DefElem *def = (DefElem *) lfirst(cell);
+
+ if (!is_valid_option(def->defname, catalog))
+ {
+ PgsqlFdwOption *opt;
+ StringInfoData buf;
+
+ /*
+ * Unknown option specified, complain about it. Provide a hint
+ * with list of valid options for the object.
+ */
+ initStringInfo(&buf);
+ for (opt = valid_options; opt->optname; opt++)
+ {
+ if (catalog == opt->optcontext)
+ appendStringInfo(&buf, "%s%s", (buf.len > 0) ? ", " : "",
+ opt->optname);
+ }
+
+ ereport(ERROR,
+ (errcode(ERRCODE_FDW_INVALID_OPTION_NAME),
+ errmsg("invalid option \"%s\"", def->defname),
+ errhint("Valid options in this context are: %s",
+ buf.data)));
+ }
+
+ /* min_cursor_rows be zero or positive digit number. */
+ if (strcmp(def->defname, "min_cursor_rows") == 0)
+ {
+ long value;
+ char *p = NULL;
+
+ value = strtol(strVal(def->arg), &p, 10);
+ if (*p != '\0' || value < 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_FDW_INVALID_ATTRIBUTE_VALUE),
+ errmsg("invalid value for %s: \"%s\"",
+ def->defname, strVal(def->arg))));
+ }
+
+ /* fetch_count be positive digit number. */
+ if (strcmp(def->defname, "fetch_count") == 0)
+ {
+ long value;
+ char *p = NULL;
+
+ value = strtol(strVal(def->arg), &p, 10);
+ if (*p != '\0' || value < 1)
+ ereport(ERROR,
+ (errcode(ERRCODE_FDW_INVALID_ATTRIBUTE_VALUE),
+ errmsg("invalid value for %s: \"%s\"",
+ def->defname, strVal(def->arg))));
+ }
+ }
+
+ /*
+ * We don't care option-specific limitation here; they will be validated at
+ * the execution time.
+ */
+
+ PG_RETURN_VOID();
+ }
+
+ /*
+ * Check if the provided option is one of the valid options.
+ * context is the Oid of the catalog holding the object the option is for.
+ */
+ static bool
+ is_valid_option(const char *optname, Oid context)
+ {
+ PgsqlFdwOption *opt;
+
+ /*
+ * If the option was other than libpq option, look up option table and
+ * determine valid context.
+ */
+ for (opt = valid_options; opt->optname; opt++)
+ {
+ if (context == opt->optcontext && strcmp(opt->optname, optname) == 0)
+ return true;
+ }
+ return false;
+ }
+
+ /*
+ * Check if the provided option is one of the valid options.
+ * context is the Oid of the catalog holding the object the option is for.
+ */
+ static bool
+ is_libpq_option(const char *optname)
+ {
+ PgsqlFdwOption *opt;
+
+ /*
+ * If the option was other than libpq option, look up option table and
+ * determine valid context.
+ */
+ for (opt = valid_options; opt->optname; opt++)
+ {
+ if (strcmp(opt->optname, optname) == 0 && opt->is_libpq_opt)
+ return true;
+ }
+ return false;
+ }
+
+ /*
+ * Generate key-value arrays which includes only libpq options from the list
+ * which contains any kind of options.
+ */
+ int
+ ExtractConnectionOptions(List *defelems, const char **keywords, const char **values)
+ {
+ ListCell *lc;
+ int i;
+
+ i = 0;
+ foreach(lc, defelems)
+ {
+ DefElem *d = (DefElem *) lfirst(lc);
+ if (is_libpq_option(d->defname))
+ {
+ keywords[i] = d->defname;
+ values[i] = strVal(d->arg);
+ i++;
+ }
+ }
+ return i;
+ }
diff --git a/contrib/pgsql_fdw/pgsql_fdw--1.0.sql b/contrib/pgsql_fdw/pgsql_fdw--1.0.sql
index ...87dfa29 .
*** a/contrib/pgsql_fdw/pgsql_fdw--1.0.sql
--- b/contrib/pgsql_fdw/pgsql_fdw--1.0.sql
***************
*** 0 ****
--- 1,36 ----
+ /* contrib/pgsql_fdw/pgsql_fdw--1.0.sql */
+
+ CREATE FUNCTION pgsql_fdw_handler()
+ RETURNS fdw_handler
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C STRICT;
+
+ CREATE FUNCTION pgsql_fdw_validator(text[], oid)
+ RETURNS void
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C STRICT;
+
+ CREATE FOREIGN DATA WRAPPER pgsql_fdw
+ HANDLER pgsql_fdw_handler
+ VALIDATOR pgsql_fdw_validator;
+
+ /* connection management functions and view */
+ CREATE FUNCTION pgsql_fdw_get_connections(out srvid oid, out usesysid oid)
+ RETURNS SETOF record
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C STRICT;
+
+ CREATE FUNCTION pgsql_fdw_disconnect(oid, oid)
+ RETURNS text
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C STRICT;
+
+ CREATE VIEW pgsql_fdw_connections AS
+ SELECT c.srvid srvid,
+ s.srvname srvname,
+ c.usesysid usesysid,
+ pg_get_userbyid(c.usesysid) usename
+ FROM pgsql_fdw_get_connections() c
+ JOIN pg_catalog.pg_foreign_server s ON (s.oid = c.srvid);
+ GRANT SELECT ON pgsql_fdw_connections TO public;
+
diff --git a/contrib/pgsql_fdw/pgsql_fdw.c b/contrib/pgsql_fdw/pgsql_fdw.c
index ...e9dfa74 .
*** a/contrib/pgsql_fdw/pgsql_fdw.c
--- b/contrib/pgsql_fdw/pgsql_fdw.c
***************
*** 0 ****
--- 1,882 ----
+ /*-------------------------------------------------------------------------
+ *
+ * pgsql_fdw.c
+ * foreign-data wrapper for remote PostgreSQL servers.
+ *
+ * Copyright (c) 2011, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/pgsql_fdw.c
+ *
+ *-------------------------------------------------------------------------
+ */
+ #include "postgres.h"
+ #include "fmgr.h"
+
+ #include "catalog/pg_foreign_server.h"
+ #include "catalog/pg_foreign_table.h"
+ #include "commands/explain.h"
+ #include "foreign/fdwapi.h"
+ #include "funcapi.h"
+ #include "miscadmin.h"
+ #include "optimizer/cost.h"
+ #include "utils/lsyscache.h"
+ #include "utils/memutils.h"
+ #include "utils/rel.h"
+
+ #include "pgsql_fdw.h"
+ #include "connection.h"
+
+ PG_MODULE_MAGIC;
+
+ /*
+ * Default fetch count for cursor mode. This can be override by fetch_count
+ * FDW option.
+ */
+ #define DEFAULT_FETCH_COUNT 1000
+
+ /*
+ * Default minimum estimated row count for cursor mode. This can be override
+ * by min_cursor_rows FDW option.
+ * */
+ #define DEFAULT_MIN_CURSOR_ROWS 1000
+
+ /*
+ * Cost to establish a connection.
+ * XXX: should be configurable per server?
+ */
+ #define CONNECTION_COSTS 100.0
+
+ /*
+ * Cost to transfer 1 byte from remote server.
+ * XXX: should be configurable per server?
+ */
+ #define TRANSFER_COSTS_PER_BYTE 0.001
+
+ /*
+ * Cursors which are used together in a local query require different name, so
+ * we use simple incremental name for that purpose. We don't care wrap around
+ * of cursor_id because it's hard to imagine that 2^32 cursors are used in a
+ * query.
+ */
+ #define CURSOR_NAME_FORMAT "pgsql_fdw_cursor_%u"
+ static uint32 cursor_id = 0;
+
+ /*
+ * Index of FDW-private items stored in FdwPlan.
+ */
+ enum FdwPrivateIndex {
+ FdwPrivateSelectSql,
+
+ /* Items for cursor mode */
+ FdwPrivateDeclareSql,
+ FdwPrivateFetchSql,
+ FdwPrivateResetSql,
+ FdwPrivateCloseSql,
+
+ /* # of elements stored in the list fdw_private */
+ FdwPrivateNum,
+ };
+
+ /*
+ * This macro can be used in the executor to determine whether the scan is
+ * using CURSOR or not.
+ */
+ #define USE_CURSOR(fdwplan) \
+ (list_length((fdwplan)->fdw_private) > FdwPrivateDeclareSql)
+
+ /*
+ * Describes an execution state of a foreign scan against a foreign table
+ * using pgsql_fdw.
+ */
+ typedef struct PgsqlFdwExecutionState
+ {
+ FdwPlan *fdwplan; /* FDW-specific planning information */
+ PGconn *conn; /* connection for the scan */
+
+ Oid *param_types; /* type array of external parameter */
+ const char **param_values; /* value array of external parameter */
+
+ int attnum; /* # of non-dropped attribute */
+ char **col_values; /* column value buffer */
+ AttInMetadata *attinmeta; /* attribute metadata */
+
+ Tuplestorestate *tuples; /* result of the scan, partial in cursor mode */
+ bool cursor_opened; /* true if cursor is opened */
+ } PgsqlFdwExecutionState;
+
+ /*
+ * SQL functions
+ */
+ extern Datum pgsql_fdw_handler(PG_FUNCTION_ARGS);
+ PG_FUNCTION_INFO_V1(pgsql_fdw_handler);
+
+ /*
+ * FDW callback routines
+ */
+ static FdwPlan *pgsqlPlanForeignScan(Oid foreigntableid,
+ PlannerInfo *root,
+ RelOptInfo *baserel);
+ static void pgsqlExplainForeignScan(ForeignScanState *node, ExplainState *es);
+ static void pgsqlBeginForeignScan(ForeignScanState *node, int eflags);
+ static TupleTableSlot *pgsqlIterateForeignScan(ForeignScanState *node);
+ static void pgsqlReScanForeignScan(ForeignScanState *node);
+ static void pgsqlEndForeignScan(ForeignScanState *node);
+
+ /*
+ * Helper functions
+ */
+ static void estimate_costs(PlannerInfo *root,
+ RelOptInfo *baserel,
+ const char *sql,
+ Oid serverid,
+ Cost *startup_cost,
+ Cost *total_cost);
+ static void execute_query(ForeignScanState *node);
+ static PGresult *fetch_result(ForeignScanState *node);
+ static void store_result(ForeignScanState *node, PGresult *res);
+
+ /*
+ * Foreign-data wrapper handler function: return a struct with pointers
+ * to my callback routines.
+ */
+ Datum
+ pgsql_fdw_handler(PG_FUNCTION_ARGS)
+ {
+ FdwRoutine *fdwroutine = makeNode(FdwRoutine);
+
+ fdwroutine->PlanForeignScan = pgsqlPlanForeignScan;
+ fdwroutine->ExplainForeignScan = pgsqlExplainForeignScan;
+ fdwroutine->BeginForeignScan = pgsqlBeginForeignScan;
+ fdwroutine->IterateForeignScan = pgsqlIterateForeignScan;
+ fdwroutine->ReScanForeignScan = pgsqlReScanForeignScan;
+ fdwroutine->EndForeignScan = pgsqlEndForeignScan;
+
+ PG_RETURN_POINTER(fdwroutine);
+ }
+
+ /*
+ * pgsqlPlanForeignScan
+ * Create a FdwPlan for a scan on the foreign table
+ */
+ static FdwPlan *
+ pgsqlPlanForeignScan(Oid foreigntableid,
+ PlannerInfo *root,
+ RelOptInfo *baserel)
+ {
+ char *sql;
+ FdwPlan *fdwplan;
+ List *fdw_private = NIL;
+ const char *min_cursor_rows_str;
+ int min_cursor_rows = DEFAULT_MIN_CURSOR_ROWS;
+ ForeignTable *table;
+ ForeignServer *server;
+
+ /* Construct FdwPlan with cost estimates */
+ fdwplan = makeNode(FdwPlan);
+ sql = deparseSql(foreigntableid, root, baserel);
+ table = GetForeignTable(foreigntableid);
+ server = GetForeignServer(table->serverid);
+ estimate_costs(root, baserel, sql, server->serverid,
+ &fdwplan->startup_cost, &fdwplan->total_cost);
+
+ /*
+ * Store plain SELECT statement in private area of FdwPlan. This will be
+ * used for executing remote query and explaining scan.
+ */
+ fdw_private = list_make1(makeString(sql));
+
+ /*
+ * We use SQL-level cursor when the result seems to be large, to limit
+ * memory usage for the result set. Exceptionally we use simple SELECT if
+ * min_cursor_rows is set to 0.
+ */
+ min_cursor_rows_str = GetFdwOptionValue(foreigntableid,
+ InvalidAttrNumber,
+ "min_cursor_rows");
+ if (min_cursor_rows_str != NULL)
+ min_cursor_rows = strtol(min_cursor_rows_str, NULL, 10);
+
+ if (min_cursor_rows != 0 && baserel->rows >= min_cursor_rows)
+ {
+ char name[128]; /* must be larger than format + 10 */
+ StringInfoData cursor;
+ const char *fetch_count_str;
+ int fetch_count = DEFAULT_FETCH_COUNT;
+
+ /* Use specified fetch_count instead of default value, if any. */
+ fetch_count_str = GetFdwOptionValue(foreigntableid,
+ InvalidAttrNumber,
+ "fetch_count");
+ if (fetch_count_str != NULL)
+ fetch_count = strtol(fetch_count_str, NULL, 10);
+ elog(DEBUG1,
+ "relid=%u fetch_count=%d",
+ foreigntableid,
+ fetch_count);
+
+ /* We store some more information in FdwPlan to pass them beyond the
+ * boundary between planner and executor. Finally FdwPlan using cursor
+ * would hold items below:
+ *
+ * 1) plain SELECT statement (already added above)
+ * 2) SQL statement used to declare cursor
+ * 3) SQL statement used to fetch rows from cursor
+ * 4) SQL statement used to reset cursor
+ * 5) SQL statement used to close cursor
+ *
+ * These items are indexed with the enum FdwPrivateIndex, so an item
+ * can be accessed directly via list_nth(). For example of FETCH
+ * statement:
+ * list_nth(fdw_private, FdwPrivateFetchSql)
+ */
+
+ /* Construct cursor name from sequential value */
+ sprintf(name, CURSOR_NAME_FORMAT, cursor_id++);
+
+ /* Construct statement to declare cursor */
+ initStringInfo(&cursor);
+ appendStringInfo(&cursor, "DECLARE %s SCROLL CURSOR FOR %s", name, sql);
+ fdw_private = lappend(fdw_private, makeString(cursor.data));
+
+ /* Construct statement to fetch rows from cursor */
+ initStringInfo(&cursor);
+ appendStringInfo(&cursor, "FETCH %d FROM %s", fetch_count, name);
+ fdw_private = lappend(fdw_private, makeString(cursor.data));
+
+ /* Construct statement to reset cursor */
+ initStringInfo(&cursor);
+ appendStringInfo(&cursor, "MOVE ABSOLUTE 0 FROM %s", name);
+ fdw_private = lappend(fdw_private, makeString(cursor.data));
+
+ /* Construct statement to close cursor */
+ initStringInfo(&cursor);
+ appendStringInfo(&cursor, "CLOSE %s", name);
+ fdw_private = lappend(fdw_private, makeString(cursor.data));
+ }
+
+ /* Store FDW private information into FdwPlan */
+ fdwplan->fdw_private = fdw_private;
+
+ return fdwplan;
+ }
+
+ /*
+ * pgsqlExplainForeignScan
+ * Produce extra output for EXPLAIN
+ */
+ static void
+ pgsqlExplainForeignScan(ForeignScanState *node, ExplainState *es)
+ {
+ FdwPlan *fdwplan;
+ char *sql;
+
+ fdwplan = ((ForeignScan *) node->ss.ps.plan)->fdwplan;
+ if (USE_CURSOR(fdwplan))
+ sql = strVal(list_nth(fdwplan->fdw_private, FdwPrivateDeclareSql));
+ else
+ sql = strVal(list_nth(fdwplan->fdw_private, FdwPrivateSelectSql));
+ ExplainPropertyText("Remote SQL", sql, es);
+ }
+
+ /*
+ * pgsqlBeginForeignScan
+ * Initiate access to a foreign PostgreSQL table.
+ */
+ static void
+ pgsqlBeginForeignScan(ForeignScanState *node, int eflags)
+ {
+ PgsqlFdwExecutionState *festate;
+ PGconn *conn;
+ Oid relid;
+ ForeignTable *table;
+ ForeignServer *server;
+ UserMapping *user;
+ TupleTableSlot *slot = node->ss.ss_ScanTupleSlot;
+
+ /*
+ * Do nothing in EXPLAIN (no ANALYZE) case. node->fdw_state stays NULL.
+ */
+ if (eflags & EXEC_FLAG_EXPLAIN_ONLY)
+ return;
+
+ /*
+ * Save state in node->fdw_state.
+ */
+ festate = (PgsqlFdwExecutionState *) palloc(sizeof(PgsqlFdwExecutionState));
+ festate->fdwplan = ((ForeignScan *) node->ss.ps.plan)->fdwplan;
+
+ /*
+ * Get connection to the foreign server. Connection manager would
+ * establish new connection if necessary.
+ */
+ relid = RelationGetRelid(node->ss.ss_currentRelation);
+ table = GetForeignTable(relid);
+ server = GetForeignServer(table->serverid);
+ user = GetUserMapping(GetOuterUserId(), server->serverid);
+ conn = GetConnection(server, user);
+ festate->conn = conn;
+
+ /* Result will be filled in first Iterate call. */
+ festate->tuples = NULL;
+ festate->cursor_opened = false;
+
+ /* Allocate buffers for column values. */
+ {
+ TupleDesc tupdesc = slot->tts_tupleDescriptor;
+ festate->col_values = palloc(sizeof(char *) * tupdesc->natts);
+ festate->attinmeta = TupleDescGetAttInMetadata(tupdesc);
+ }
+
+ /* Allocate buffers for query parameters. */
+ {
+ ParamListInfo params = node->ss.ps.state->es_param_list_info;
+ int numParams = params ? params->numParams : 0;
+
+ if (numParams > 0)
+ {
+ festate->param_types = palloc0(sizeof(Oid) * numParams);
+ festate->param_values = palloc0(sizeof(char *) * numParams);
+ }
+ else
+ {
+ festate->param_types = NULL;
+ festate->param_values = NULL;
+ }
+ }
+
+
+ /* Store FDW-specific state into ForeignScanState */
+ node->fdw_state = (void *) festate;
+
+ return;
+ }
+
+ /*
+ * pgsqlIterateForeignScan
+ * Retrieve next row from the result set, or clear tuple slot to indicate
+ * EOF.
+ *
+ * Note that using per-query context when retrieving tuples from
+ * tuplestore to ensure that returned tuples can survive until next
+ * iteration because the tuple is released implicitly via ExecClearTuple.
+ * Retrieving a tuple from tuplestore in CurrentMemoryContext (it's a
+ * per-tuple context), ExecClearTuple will free dangling pointer.
+ */
+ static TupleTableSlot *
+ pgsqlIterateForeignScan(ForeignScanState *node)
+ {
+ PgsqlFdwExecutionState *festate;
+ TupleTableSlot *slot = node->ss.ss_ScanTupleSlot;
+ PGresult *res;
+ MemoryContext oldcontext = CurrentMemoryContext;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+
+
+ /*
+ * If this is the first call after Begin, we need to execute remote query.
+ * If the query needs cursor, we declare a cursor at first call and fetch
+ * from it in later calls.
+ */
+ if (festate->tuples == NULL && !festate->cursor_opened)
+ execute_query(node);
+
+ /*
+ * If enough tuples are left in tuplestore, just return next tuple from it.
+ */
+ MemoryContextSwitchTo(node->ss.ps.state->es_query_cxt);
+ if (tuplestore_gettupleslot(festate->tuples, true, false, slot))
+ {
+ MemoryContextSwitchTo(oldcontext);
+ return slot;
+ }
+ MemoryContextSwitchTo(oldcontext);
+
+ /* If the scan doesn't use cursor, the scan has been done. */
+ if (!USE_CURSOR(festate->fdwplan))
+ {
+ ExecClearTuple(slot);
+ return slot;
+ }
+
+ /*
+ * Here we need to clear partial result and fetch next bunch of tuples from
+ * from the cursor for the scan. If the fetch returns no tuple, the scan
+ * has reached the end.
+ */
+ res = fetch_result(node);
+ PG_TRY();
+ {
+ store_result(node, res);
+ PQclear(res);
+ res = NULL;
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+
+ /*
+ * If we got more tuples from the server cursor, return next tuple from
+ * tuplestore.
+ */
+ MemoryContextSwitchTo(node->ss.ps.state->es_query_cxt);
+ if (tuplestore_gettupleslot(festate->tuples, true, false, slot))
+ {
+ MemoryContextSwitchTo(oldcontext);
+ return slot;
+ }
+ MemoryContextSwitchTo(oldcontext);
+
+ /* We don't have any result even in remote server cursor. */
+ ExecClearTuple(slot);
+ return slot;
+ }
+
+ /*
+ * pgsqlReScanForeignScan
+ * - Restart this scan by resetting fetch location.
+ */
+ static void
+ pgsqlReScanForeignScan(ForeignScanState *node)
+ {
+ List *fdw_private;
+ char *sql;
+ PGconn *conn;
+ PGresult *res;
+ PgsqlFdwExecutionState *festate;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+
+ /* Discard fetch results if any. */
+ if (festate->tuples != NULL)
+ {
+ tuplestore_end(festate->tuples);
+ festate->tuples = NULL;
+ }
+
+ /* Reset cursor */
+ if (USE_CURSOR(festate->fdwplan))
+ {
+ fdw_private = festate->fdwplan->fdw_private;
+ conn = festate->conn;
+ sql = strVal(list_nth(fdw_private, FdwPrivateResetSql));
+ res = PQexec(conn, sql);
+ PG_TRY();
+ {
+ if (res == NULL || PQresultStatus(res) != PGRES_COMMAND_OK)
+ {
+ ereport(ERROR,
+ (errmsg("could not rewind cursor"),
+ errdetail("%s", PQerrorMessage(conn)),
+ errhint("%s", sql)));
+ }
+ PQclear(res);
+ res = NULL;
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+ }
+ }
+
+ /*
+ * pgsqlEndForeignScan
+ * Finish scanning foreign table and dispose objects used for this scan
+ */
+ static void
+ pgsqlEndForeignScan(ForeignScanState *node)
+ {
+ List *fdw_private;
+ char *sql;
+ PGconn *conn;
+ PGresult *res;
+ PgsqlFdwExecutionState *festate;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+
+ /* if festate is NULL, we are in EXPLAIN; nothing to do */
+ if (festate == NULL)
+ return;
+
+ /* Discard fetch results */
+ if (festate->tuples != NULL)
+ {
+ tuplestore_end(festate->tuples);
+ festate->tuples = NULL;
+ }
+
+ /* Close cursor */
+ if (USE_CURSOR(festate->fdwplan) && festate->cursor_opened)
+ {
+ fdw_private = festate->fdwplan->fdw_private;
+ conn = festate->conn;
+ sql = strVal(list_nth(fdw_private, FdwPrivateCloseSql));
+ res = PQexec(conn, sql);
+ PG_TRY();
+ {
+ if (res == NULL || PQresultStatus(res) != PGRES_COMMAND_OK)
+ {
+ ereport(ERROR,
+ (errmsg("could not close cursor"),
+ errdetail("%s", PQerrorMessage(conn)),
+ errhint("%s", sql)));
+ }
+ PQclear(res);
+ res = NULL;
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+ }
+
+ ReleaseConnection(festate->conn);
+ }
+
+ /*
+ * Estimate costs of scanning a foreign table.
+ */
+ static void
+ estimate_costs(PlannerInfo *root, RelOptInfo *baserel,
+ const char *sql, Oid serverid,
+ Cost *startup_cost, Cost *total_cost)
+ {
+ ForeignServer *server;
+ UserMapping *user;
+ PGconn *conn = NULL;
+ PGresult *res = NULL;
+ StringInfoData buf;
+ char *plan;
+ char *p;
+ char *endp;
+
+ /*
+ * Get connection to the foreign server. Connection manager would
+ * establish new connection if necessary.
+ */
+ server = GetForeignServer(serverid);
+ user = GetUserMapping(GetOuterUserId(), server->serverid);
+ conn = GetConnection(server, user);
+ initStringInfo(&buf);
+ appendStringInfo(&buf, "EXPLAIN (FORMAT YAML) %s", sql);
+
+ /* remove WHERE clause if the query uses parameter */
+ p = strchr(buf.data, '$');
+ if (p != NULL)
+ {
+ p = strstr(buf.data, "WHERE");
+ if (p != NULL)
+ *p = '\0';
+ }
+
+ PG_TRY();
+ {
+ res = PQexec(conn, buf.data);
+ if (res == NULL || PQresultStatus(res) != PGRES_TUPLES_OK)
+ {
+ char *msg;
+
+ msg = pstrdup(PQerrorMessage(conn));
+ PQclear(res);
+ res = NULL;
+ ereport(ERROR,
+ (errmsg("could not execute EXPLAIN for cost estimation"),
+ errdetail("%s", msg),
+ errhint("%s", sql)));
+ }
+ plan = pstrdup(PQgetvalue(res, 0, 0));
+ PQclear(res);
+ res = NULL;
+ ReleaseConnection(conn);
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+
+ /* extract startup cost from remote plan */
+ p = strstr(plan, "Startup Cost: ");
+ if (p != NULL)
+ {
+ p += strlen("Startup Cost: ");
+ *startup_cost = strtod(p, &endp);
+ if (*endp != '\n' && *endp != '\0')
+ elog(ERROR, "invalid plan format for startup cost%c", *endp);
+ }
+
+ /* extract total cost from remote plan */
+ p = strstr(plan, "Total Cost: ");
+ if (p != NULL)
+ {
+ p += strlen("Total Cost: ");
+ *total_cost = strtod(p, &endp);
+ if (*endp != '\n' && *endp != '\0')
+ elog(ERROR, "invalid plan format for total cost");
+ }
+
+ /* extract # of rows from remote plan */
+ p = strstr(plan, "Plan Rows: ");
+ if (p != NULL)
+ {
+ p += strlen("Plan Rows: ");
+ baserel->rows = strtod(p, &endp);
+ if (*endp != '\n' && *endp != '\0')
+ elog(ERROR, "invalid plan format for plan rows");
+ }
+
+ /* extract average width from remote plan */
+ p = strstr(plan, "Plan Width: ");
+ if (p != NULL)
+ {
+ p += strlen("Plan Width: ");
+ baserel->width = strtol(p, &endp, 10);
+ if (*endp != '\n' && *endp != '\0')
+ elog(ERROR, "invalid plan format for plan width");
+ }
+
+ /* TODO Selectivity of quals pushed down should be considered. */
+
+ /* add cost to establish connection. */
+ *startup_cost += CONNECTION_COSTS;
+ *total_cost += CONNECTION_COSTS;
+
+ /* add cost to transfer result. */
+ *total_cost += TRANSFER_COSTS_PER_BYTE * baserel->width * baserel->tuples;
+ *total_cost += cpu_tuple_cost * baserel->tuples;
+ }
+
+ /*
+ * Execute remote query with current parameters.
+ */
+ static void
+ execute_query(ForeignScanState *node)
+ {
+ FdwPlan *fdwplan;
+ PgsqlFdwExecutionState *festate;
+ ParamListInfo params = node->ss.ps.state->es_param_list_info;
+ int numParams = params ? params->numParams : 0;
+ Oid *types = NULL;
+ const char **values = NULL;
+ char *sql;
+ int required_result;
+ PGconn *conn;
+ PGresult *res;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+ types = festate->param_types;
+ values = festate->param_values;
+
+ /*
+ * Construct parameter array in text format. We don't release memory for
+ * the arrays explicitly, because the memory usage would not be very large,
+ * and anyway they will be released in context cleanup.
+ */
+ if (numParams > 0)
+ {
+ int i;
+
+ 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;
+
+ getTypeOutputInfo(types[i], &out_func_oid, &isvarlena);
+ fmgr_info(out_func_oid, &func);
+ values[i] = OutputFunctionCall(&func, params->params[i].value);
+ }
+ }
+ }
+
+ /*
+ * Execute remote query with parameters.
+ */
+ conn = festate->conn;
+ fdwplan = ((ForeignScan *) node->ss.ps.plan)->fdwplan;
+ if (USE_CURSOR(fdwplan))
+ {
+ sql = strVal(list_nth(fdwplan->fdw_private, FdwPrivateDeclareSql));
+ required_result = PGRES_COMMAND_OK;
+ }
+ else
+ {
+ sql = strVal(list_nth(fdwplan->fdw_private, FdwPrivateSelectSql));
+ required_result = PGRES_TUPLES_OK;
+ }
+ res = PQexecParams(conn, sql, numParams, types, values, NULL, NULL, 0);
+ PG_TRY();
+ {
+ /*
+ * If the query has failed, reporting details is enough here.
+ * Connection(s) which are used by this query (at least used by
+ * pgsql_fdw) will be cleaned up by the foreign connection manager.
+ */
+ if (res == NULL || PQresultStatus(res) != required_result)
+ {
+ ereport(ERROR,
+ (errmsg("could not execute foreign query"),
+ errdetail("%s", PQerrorMessage(conn)),
+ errhint("%s", sql)));
+ }
+
+ /* Fetch first bunch of result if we using cursor. */
+ if (USE_CURSOR(fdwplan))
+ {
+ /* Mark that this scan has opened a cursor. */
+ festate->cursor_opened = true;
+
+ /* Discard result of CURSOR statement and fetch first bunch. */
+ PQclear(res);
+ res = fetch_result(node);
+ }
+
+ /*
+ * Store the result of the query into tuplestore.
+ * We must release PGresult here to avoid memory leak.
+ */
+ store_result(node, res);
+ PQclear(res);
+ res = NULL;
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+ }
+
+ /*
+ * Fetch next partial result from remote server.
+ */
+ static PGresult *
+ fetch_result(ForeignScanState *node)
+ {
+ PgsqlFdwExecutionState *festate;
+ List *fdw_private;
+ char *sql;
+ PGconn *conn;
+ PGresult *res;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+
+ /* retrieve information for fetching result. */
+ fdw_private = festate->fdwplan->fdw_private;
+ sql = strVal(list_nth(fdw_private, FdwPrivateFetchSql));
+ conn = festate->conn;
+ res = PQexec(conn, sql);
+ if (res == NULL || PQresultStatus(res) != PGRES_TUPLES_OK)
+ {
+ ereport(ERROR,
+ (errmsg("could not fetch rows from foreign server"),
+ errdetail("%s", PQerrorMessage(conn)),
+ errhint("%s", sql)));
+ }
+
+ return res;
+ }
+
+ /*
+ * Create tuples from PGresult and store them into tuplestore.
+ */
+ static void
+ store_result(ForeignScanState *node, PGresult *res)
+ {
+ int rows;
+ int row;
+ int i;
+ int nfields;
+ int attnum; /* number of non-dropped columns */
+ Form_pg_attribute *attrs;
+ TupleTableSlot *slot = node->ss.ss_ScanTupleSlot;
+ TupleDesc tupdesc = slot->tts_tupleDescriptor;
+ PgsqlFdwExecutionState *festate;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+ rows = PQntuples(res);
+ nfields = PQnfields(res);
+ attrs = tupdesc->attrs;
+
+ /* First, ensure that the tuplestore is empty. */
+ if (festate->tuples == NULL)
+ {
+ MemoryContext oldcontext = CurrentMemoryContext;
+
+ /*
+ * Create tuplestore to store result of the query in per-query context.
+ * Note that we use this memory context to avoid memory leak in error
+ * cases.
+ */
+ MemoryContextSwitchTo(MessageContext);
+ festate->tuples = tuplestore_begin_heap(false, false, work_mem);
+ MemoryContextSwitchTo(oldcontext);
+ }
+ else
+ {
+ /* We already have tuplestore, just need to clear contents of it. */
+ tuplestore_clear(festate->tuples);
+ }
+
+
+ /* 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)));
+
+ /* put a tuples into the slot */
+ for (row = 0; row < rows; row++)
+ {
+ int j;
+ HeapTuple tuple;
+
+ for (i = 0, j = 0; i < tupdesc->natts; i++)
+ {
+ /* skip dropped columns. */
+ if (attrs[i]->attisdropped)
+ {
+ festate->col_values[i] = NULL;
+ continue;
+ }
+
+ if (PQgetisnull(res, row, j))
+ festate->col_values[i] = NULL;
+ else
+ festate->col_values[i] = PQgetvalue(res, row, 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(festate->attinmeta, festate->col_values);
+ tuplestore_puttuple(festate->tuples, tuple);
+ }
+
+ tuplestore_donestoring(festate->tuples);
+ }
diff --git a/contrib/pgsql_fdw/pgsql_fdw.control b/contrib/pgsql_fdw/pgsql_fdw.control
index ...0a9c8f4 .
*** a/contrib/pgsql_fdw/pgsql_fdw.control
--- b/contrib/pgsql_fdw/pgsql_fdw.control
***************
*** 0 ****
--- 1,5 ----
+ # pgsql_fdw extension
+ comment = 'foreign-data wrapper for remote PostgreSQL servers'
+ default_version = '1.0'
+ module_pathname = '$libdir/pgsql_fdw'
+ relocatable = true
diff --git a/contrib/pgsql_fdw/pgsql_fdw.h b/contrib/pgsql_fdw/pgsql_fdw.h
index ...fb49ffb .
*** a/contrib/pgsql_fdw/pgsql_fdw.h
--- b/contrib/pgsql_fdw/pgsql_fdw.h
***************
*** 0 ****
--- 1,28 ----
+ /*-------------------------------------------------------------------------
+ *
+ * pgsql_fdw.h
+ * foreign-data wrapper for remote PostgreSQL servers.
+ *
+ * Copyright (c) 2011, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/pgsql_fdw.h
+ *
+ *-------------------------------------------------------------------------
+ */
+
+ #ifndef PGSQL_FDW_H
+ #define PGSQL_FDW_H
+
+ #include "postgres.h"
+ #include "nodes/relation.h"
+
+ /* in option.c */
+ int ExtractConnectionOptions(List *defelems,
+ const char **keywords,
+ const char **values);
+
+ /* in deparse.c */
+ char *deparseSql(Oid relid, PlannerInfo *root, RelOptInfo *baserel);
+
+ #endif /* PGSQL_FDW_H */
diff --git a/contrib/pgsql_fdw/sql/pgsql_fdw.sql b/contrib/pgsql_fdw/sql/pgsql_fdw.sql
index ...95a4d54 .
*** a/contrib/pgsql_fdw/sql/pgsql_fdw.sql
--- b/contrib/pgsql_fdw/sql/pgsql_fdw.sql
***************
*** 0 ****
--- 1,175 ----
+ -- ===================================================================
+ -- create FDW objects
+ -- ===================================================================
+
+ CREATE EXTENSION pgsql_fdw;
+
+ CREATE SERVER loopback1 FOREIGN DATA WRAPPER pgsql_fdw;
+ CREATE SERVER loopback2 FOREIGN DATA WRAPPER pgsql_fdw
+ OPTIONS (dbname 'contrib_regression');
+
+ CREATE USER MAPPING FOR public SERVER loopback1
+ OPTIONS (user 'value', password 'value');
+ CREATE USER MAPPING FOR public SERVER loopback2;
+
+ CREATE FOREIGN TABLE ft1 (
+ c1 int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp
+ ) SERVER loopback2;
+
+ CREATE FOREIGN TABLE ft2 (
+ c1 int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp
+ ) SERVER loopback2;
+
+ -- ===================================================================
+ -- create objects used through FDW
+ -- ===================================================================
+ CREATE SCHEMA "S 1";
+ CREATE TABLE "S 1"."T 1" (
+ c1 int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ CONSTRAINT t1_pkey PRIMARY KEY (c1)
+ );
+ CREATE TABLE "S 1"."T 2" (
+ c1 int NOT NULL,
+ c2 text,
+ CONSTRAINT t2_pkey PRIMARY KEY (c1)
+ );
+
+ BEGIN;
+ TRUNCATE "S 1"."T 1";
+ INSERT INTO "S 1"."T 1"
+ SELECT id,
+ id % 10,
+ to_char(id, 'FM00000'),
+ '1970-01-01'::timestamptz + ((id % 100) || ' days')::interval,
+ '1970-01-01'::timestamp + ((id % 100) || ' days')::interval
+ FROM generate_series(1, 1000) id;
+ TRUNCATE "S 1"."T 2";
+ INSERT INTO "S 1"."T 2"
+ SELECT id,
+ 'AAA' || to_char(id, 'FM000')
+ FROM generate_series(1, 100) id;
+ COMMIT;
+
+ -- ===================================================================
+ -- tests for pgsql_fdw_validator
+ -- ===================================================================
+ ALTER FOREIGN DATA WRAPPER pgsql_fdw OPTIONS (host 'value'); -- ERRROR
+ -- requiressl, krbsrvname and gsslib are omitted because they depend on
+ -- configure option
+ ALTER SERVER loopback1 OPTIONS (
+ authtype 'value',
+ service 'value',
+ connect_timeout 'value',
+ dbname 'value',
+ host 'value',
+ hostaddr 'value',
+ port 'value',
+ --client_encoding 'value',
+ tty 'value',
+ options 'value',
+ application_name 'value',
+ --fallback_application_name 'value',
+ keepalives 'value',
+ keepalives_idle 'value',
+ keepalives_interval 'value',
+ -- requiressl 'value',
+ sslmode 'value',
+ sslcert 'value',
+ sslkey 'value',
+ sslrootcert 'value',
+ sslcrl 'value'
+ --requirepeer 'value',
+ -- krbsrvname 'value',
+ -- gsslib 'value',
+ --replication 'value'
+ );
+ ALTER SERVER loopback1 OPTIONS (user 'value'); -- ERROR
+ ALTER SERVER loopback2 OPTIONS (ADD min_cursor_rows '0');
+ ALTER SERVER loopback2 OPTIONS (ADD fetch_count '2');
+ ALTER USER MAPPING FOR public SERVER loopback1
+ OPTIONS (DROP user, DROP password);
+ ALTER USER MAPPING FOR public SERVER loopback1
+ OPTIONS (host 'value'); -- ERROR
+ ALTER FOREIGN TABLE ft1 OPTIONS (nspname 'S 1', relname 'T 1');
+ ALTER FOREIGN TABLE ft2 OPTIONS (nspname 'S 1', relname 'T 1', min_cursor_rows '10', fetch_count '100');
+ ALTER FOREIGN TABLE ft1 OPTIONS (invalid 'value'); -- ERROR
+ ALTER FOREIGN TABLE ft1 OPTIONS (min_cursor_rows 'a'); -- ERROR
+ ALTER FOREIGN TABLE ft1 OPTIONS (min_cursor_rows '-1'); -- ERROR
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count 'a'); -- ERROR
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count '0'); -- ERROR
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count '-1'); -- ERROR
+ \dew+
+ \des+
+ \deu+
+ \det+
+
+ -- ===================================================================
+ -- simple queries
+ -- ===================================================================
+ -- single table, with/without alias
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
+ SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+ SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+ -- with WHERE clause
+ SELECT * FROM ft1 t1 WHERE t1.c1 = 101;
+ -- 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
+ SELECT * FROM ft1 t1 WHERE t1.c3 = (SELECT MAX(c3) FROM ft2 t2) ORDER BY c1;
+ -- used in CTE
+ WITH t1 AS (SELECT * FROM ft1 WHERE c1 <= 10) SELECT t2.c1, t2.c2, t2.c3, t2.c4 FROM t1, ft2 t2 WHERE t1.c1 = t2.c1 ORDER BY t1.c1;
+ -- fixed values
+ SELECT 'fixed', NULL FROM ft1 t1 WHERE c1 = 1;
+
+ -- ===================================================================
+ -- parameterized queries
+ -- ===================================================================
+ -- simple join
+ PREPARE st1(int, int) AS SELECT t1.c3, t2.c3 FROM ft1 t1, ft2 t2 WHERE t1.c1 = $1 AND t2.c1 = $2;
+ EXPLAIN (COSTS false) EXECUTE st1(1, 2);
+ EXECUTE st1(1, 1);
+ EXECUTE st1(101, 101);
+ -- subquery using stable function (can't be pushed down)
+ PREPARE st2(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c4) = 6) ORDER BY c1;
+ EXPLAIN (COSTS false) EXECUTE st2(10, 20);
+ EXECUTE st2(10, 20);
+ EXECUTE st1(101, 101);
+ -- subquery using immutable function (can be pushed down)
+ PREPARE st3(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c5) = 6) ORDER BY c1;
+ EXPLAIN (COSTS false) EXECUTE st3(10, 20);
+ EXECUTE st3(10, 20);
+ EXECUTE st3(20, 30);
+ -- cleanup
+ DEALLOCATE st1;
+ DEALLOCATE st2;
+ DEALLOCATE st3;
+
+ -- ===================================================================
+ -- connection management
+ -- ===================================================================
+ SELECT srvname, usename FROM pgsql_fdw_connections;
+ SELECT pgsql_fdw_disconnect(srvid, usesysid) FROM pgsql_fdw_get_connections();
+ SELECT srvname, usename FROM pgsql_fdw_connections;
+
+ -- ===================================================================
+ -- cleanup
+ -- ===================================================================
+ DROP EXTENSION pgsql_fdw CASCADE;
+
diff --git a/doc/src/sgml/contrib.sgml b/doc/src/sgml/contrib.sgml
index adf09ca..65c7e81 100644
*** a/doc/src/sgml/contrib.sgml
--- b/doc/src/sgml/contrib.sgml
*************** CREATE EXTENSION <replaceable>module_nam
*** 117,122 ****
--- 117,123 ----
&pgcrypto;
&pgfreespacemap;
&pgrowlocks;
+ &pgsql-fdw;
&pgstandby;
&pgstatstatements;
&pgstattuple;
diff --git a/doc/src/sgml/filelist.sgml b/doc/src/sgml/filelist.sgml
index ed39e0b..d72590f 100644
*** a/doc/src/sgml/filelist.sgml
--- b/doc/src/sgml/filelist.sgml
***************
*** 123,128 ****
--- 123,129 ----
<!ENTITY pgcrypto SYSTEM "pgcrypto.sgml">
<!ENTITY pgfreespacemap SYSTEM "pgfreespacemap.sgml">
<!ENTITY pgrowlocks SYSTEM "pgrowlocks.sgml">
+ <!ENTITY pgsql-fdw SYSTEM "pgsql-fdw.sgml">
<!ENTITY pgstandby SYSTEM "pgstandby.sgml">
<!ENTITY pgstatstatements SYSTEM "pgstatstatements.sgml">
<!ENTITY pgstattuple SYSTEM "pgstattuple.sgml">
diff --git a/doc/src/sgml/pgsql-fdw.sgml b/doc/src/sgml/pgsql-fdw.sgml
index ...8d38dd6 .
*** a/doc/src/sgml/pgsql-fdw.sgml
--- b/doc/src/sgml/pgsql-fdw.sgml
***************
*** 0 ****
--- 1,261 ----
+ <!-- doc/src/sgml/pgsql-fdw.sgml -->
+
+ <sect1 id="pgsql-fdw" xreflabel="pgsql_fdw">
+ <title>pgsql_fdw</title>
+
+ <indexterm zone="pgsql-fdw">
+ <primary>pgsql_fdw</primary>
+ </indexterm>
+
+ <para>
+ The <filename>pgsql_fdw</filename> module provides a foreign-data wrapper for
+ external <productname>PostgreSQL</productname> servers.
+ With this module, users can access data stored in external
+ <productname>PostgreSQL</productname> via plain SQL statements.
+ </para>
+
+ <para>
+ The <application>pgsql_fdw</application> can be installed on only
+ <productname>PostgreSQL</productname> 9.1 or later, but it can also access
+ data stored in <productname>PostgreSQL</productname> 9.0.
+ <productname>PostgreSQL</productname>8.4 or older are not available as remote
+ server because <application>pgsql_fdw</application> uses YAML format of
+ <command>EXPLAIN</command> output.
+ </para>
+
+ <para>
+ Note that default wrapper <literal>pgsql_fdw</literal> is created
+ automatically during <command>CREATE EXTENSION</command> command for
+ <application>pgsql_fdw</application>.
+ </para>
+
+ <sect2>
+ <title>FDW Options of pgsql_fdw</title>
+
+ <sect3>
+ <title>Connection Options</title>
+ <para>
+ A foreign server and user mapping created using this wrapper can have
+ <application>libpq</> connection options, expect below:
+
+ <itemizedlist>
+ <listitem><para>authtype</para></listitem>
+ <listitem><para>client_encoding</para></listitem>
+ <listitem><para>tty</para></listitem>
+ <listitem><para>fallback_application_name</para></listitem>
+ <listitem><para>replication</para></listitem>
+ </itemizedlist>
+
+ For details of <application>libpq</> connection options, see
+ <xref linkend="libpq-connect">.
+ </para>
+
+ <para>
+ <literal>user</literal> and <literal>password</literal> can be
+ specified on user mappings, and others can be specified on foreign servers.
+ </para>
+ </sect3>
+
+ <sect3>
+ <title>Object Name Options</title>
+ <para>
+ Foreign tables which were created using this wrapper, or its columns can
+ have object name options. These options can be used to specify the names
+ used in SQL statement sent to remote <productname>PostgreSQL</productname>
+ server. These options are useful when a remote object has different name
+ from corresponding local one.
+ </para>
+
+ <variablelist>
+
+ <varlistentry>
+ <term><literal>nspname</literal></term>
+ <listitem>
+ <para>
+ This option, which can be specified on a foreign table, is used as a
+ namespace (schema) reference in the SQL statement. If this options is
+ omitted, <literal>pg_class.nspname</literal> of the foreign table is
+ used.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>relname</literal></term>
+ <listitem>
+ <para>
+ This option, which can be specified on a foreign table, is used as a
+ relation (table) reference in the SQL statement. If this options is
+ omitted, <literal>pg_class.relname</literal> of the foreign table is
+ used.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ </variablelist>
+
+ </sect3>
+
+ <sect3>
+ <title>Cursor Options</title>
+ <para>
+ The <application>pgsql_fdw</application> switches the way used to retrieve
+ result of remote query according to estimated number of result rows.
+ If the estimation was less than a threshold, the result rows are retrieved
+ at once with simple <command>SELECT</command> statement.
+ In contrast, if the estimation is larger than the threshold, the result
+ rows are fetched separately with a cursor defined by
+ <command>DECLARE</command> statement.
+ </para>
+
+ <para>
+ Users can control this behavior with setting cursor options for a foreign
+ table.
+ </para>
+
+ <variablelist>
+
+ <varlistentry>
+ <term><literal>min_cursor_rows</literal></term>
+ <listitem>
+ <para>
+ This option specifies the minimum estimated number of result rows to use
+ cursor for fetching result. Setting this to 1 means that every query
+ uses cursor, and setting to 0 means that every query uses simple
+ <command>SELECT</command>. This option can be set on a foreign table,
+ and accepts only positive integer value.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>fetch_count</literal></term>
+ <listitem>
+ <para>
+ This option specifies the number of rows fetched at once in cursor mode.
+ This option accepts only integer value larger than zero.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ </variablelist>
+
+ </sect3>
+
+ </sect2>
+
+ <sect2>
+ <title>Connection Management</title>
+
+ <para>
+ The <application>pgsql_fdw</application> establishes a connection to a
+ foreign server in the beginning of the first query which uses a foreign
+ table associated to the foreign server, and reuses the connection following
+ queries and even in following foreign scans in same query.
+
+ You can see the list of active connections via
+ <structname>pgsql_fdw_connections</structname> view. It shows pair of oid
+ and name of server and local role for each active connections established by
+ <application>pgsql_fdw</application>. For security reason, only superuser
+ can see other role's connections.
+ </para>
+
+ <para>
+ Established connections are kept alive until local role changes or the
+ current transaction aborts or user requests so.
+ </para>
+
+ <para>
+ If role has been changed, active connections established as old local role
+ is kept alive but never be reused until locla role has restored to original
+ role. This kind of situation happens with <command>SET ROLE</command> and
+ <command>SET SESSION AUTHORIZATION</command>.
+ </para>
+
+ <para>
+ If current transaction aborts by error or user request, all active
+ connections are disconnected automatically. This behavior avoids possible
+ connection leaks on error.
+ </para>
+
+ <para>
+ You can discard persistent connection at arbitrary timing with
+ <function>pgsql_fdw_disconnect()</function>. It takes server oid and
+ user oid as arguments. This function can handle only connections
+ established in current session; connections established by other backends
+ are not reachable.
+ </para>
+
+ <para>
+ You can discard all active and visible connections in current session with
+ using <structname>pgsql_fdw_connections</structname> and
+ <function>pgsql_fdw_disconnect()</function> together:
+ <synopsis>
+ postgres=# SELECT pgsql_fdw_disconnect(srvid, usesysid) FROM pgsql_fdw_connections;
+ pgsql_fdw_disconnect
+ ----------------------
+ OK
+ OK
+ (2 rows)
+ </synopsis>
+ </para>
+ </sect2>
+
+ <sect2>
+ <title>Transaction Management</title>
+ <para>
+ The <application>pgsql_fdw</application> executes <command>BEGIN</command>
+ command when a new connection has established. This means that all remote
+ queries for a foreign server are executed in a transaction. Since the
+ default transaction isolation level is <literal>READ COMMITTED</literal>,
+ multiple foreign scans in a local query might produce inconsistent results.
+
+ To avoid this inconsistency, you can use <literal>SERIALIZABLE</literal>
+ level for remote transaction with setting
+ <literal>default_transaction_isolation</literal> for the user used for
+ <application>pgsql_fdw</application> connection on remote side.
+ </para>
+ </sect2>
+
+ <sect2>
+ <title>Estimation of Costs and Rows</title>
+ <para>
+ The <application>pgsql_fdw</application> estimates the costs of a foreign
+ scan by adding some basic costs: connection costs, remote query costs and
+ data transfer costs.
+ To get remote query costs <application>pgsql_fdw</application> executes
+ <command>EXPLAIN</command> command on remote server for each foreign scan.
+ </para>
+
+ <para>
+ On the other hand, estimated rows which was returned by
+ <command>EXPLAIN</command> is used for local estimation as-is.
+ </para>
+ </sect2>
+
+ <sect2>
+ <title>EXPLAIN Output</title>
+ <para>
+ For a foreign table using <literal>pgsql_fdw</>, <command>EXPLAIN</> shows
+ a remote SQL statement which is sent to remote
+ <productname>PostgreSQL</productname> server for a ForeignScan plan node.
+ For example:
+ </para>
+ <synopsis>
+ postgres=# EXPLAIN SELECT aid FROM pgbench_accounts WHERE abalance < 0;
+ QUERY PLAN
+ -----------------------------------------------------------------------------------------------
+ Foreign Scan on pgbench_accounts (cost=100.00..8769.00 rows=1 width=4)
+ Remote SQL: SELECT aid, NULL, NULL, NULL FROM public.pgbench_accounts WHERE (abalance < 0)
+ (2 rows)
+ </synopsis>
+ </sect2>
+
+ <sect2>
+ <title>Author</title>
+ <para>
+ Shigeru Hanada <email>shigeru.hanada@gmail.com</email>
+ </para>
+ </sect2>
+
+ </sect1>
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 75923a6..6fda053 100644
*** a/src/backend/utils/adt/ruleutils.c
--- b/src/backend/utils/adt/ruleutils.c
*************** deparse_context_for(const char *aliasnam
*** 2161,2166 ****
--- 2161,2189 ----
return list_make1(dpns);
}
+ /* ----------
+ * deparse_context_for_rtelist - Build deparse context for a single relation
+ *
+ * Given the list of RangeTableEnetry, build deparsing context for an
+ * expression referencing those relations. This is sufficient for uses of
+ * deparse_expression before plan has been created.
+ * ----------
+ */
+ List *
+ deparse_context_for_rtelist(List *rtable)
+ {
+ deparse_namespace *dpns;
+
+ dpns = (deparse_namespace *) palloc0(sizeof(deparse_namespace));
+
+ /* Build a minimal RTE for the rel */
+ dpns->rtable = rtable;
+ dpns->ctes = NIL;
+
+ /* Return a one-deep namespace stack */
+ return list_make1(dpns);
+ }
+
/*
* deparse_context_for_planstate - Build deparse context for a plan
*
diff --git a/src/include/utils/builtins.h b/src/include/utils/builtins.h
index 8a1c82e..a56366f 100644
*** a/src/include/utils/builtins.h
--- b/src/include/utils/builtins.h
*************** extern char *deparse_expression(Node *ex
*** 626,631 ****
--- 626,632 ----
extern List *deparse_context_for(const char *aliasname, Oid relid);
extern List *deparse_context_for_planstate(Node *planstate, List *ancestors,
List *rtable);
+ extern List *deparse_context_for_rtelist(List *rtable);
extern const char *quote_identifier(const char *ident);
extern char *quote_qualified_identifier(const char *qualifier,
const char *ident);
2011/10/25 Shigeru Hanada <shigeru.hanada@gmail.com>:
I'd like to propose pgsql_fdw, FDW for external PostgreSQL server, as a
contrib module. I think that this module would be the basis of further
SQL/MED development for core, e.g. join-push-down and ANALYZE support.
I have not looked at the code itself, but I wonder if we shouldn't
consider making this a part of core-proper, not just a contrib module.
The fact that it isn't *already* available in core surprises a lot of
people...
--
Magnus Hagander
Me: http://www.hagander.net/
Work: http://www.redpill-linpro.com/
Magnus Hagander <magnus@hagander.net> writes:
2011/10/25 Shigeru Hanada <shigeru.hanada@gmail.com>:
I'd like to propose pgsql_fdw, FDW for external PostgreSQL server, as a
contrib module. �I think that this module would be the basis of further
SQL/MED development for core, e.g. join-push-down and ANALYZE support.
I have not looked at the code itself, but I wonder if we shouldn't
consider making this a part of core-proper, not just a contrib module.
The fact that it isn't *already* available in core surprises a lot of
people...
We've just spent a whole lot of blood and sweat on making the extension
mechanism work nicely. I don't understand this urge to not use it.
ATM I'm not sure it's even a good idea to push pgsql_fdw into contrib.
Once we do that its release schedule will get locked to core's ---
wouldn't it be better to keep flexibility for now, while it's in such
active development?
regards, tom lane
On Tue, Oct 25, 2011 at 14:08, Tom Lane <tgl@sss.pgh.pa.us> wrote:
Magnus Hagander <magnus@hagander.net> writes:
2011/10/25 Shigeru Hanada <shigeru.hanada@gmail.com>:
I'd like to propose pgsql_fdw, FDW for external PostgreSQL server, as a
contrib module. I think that this module would be the basis of further
SQL/MED development for core, e.g. join-push-down and ANALYZE support.I have not looked at the code itself, but I wonder if we shouldn't
consider making this a part of core-proper, not just a contrib module.
The fact that it isn't *already* available in core surprises a lot of
people...We've just spent a whole lot of blood and sweat on making the extension
mechanism work nicely. I don't understand this urge to not use it.
We're back to the old discussion, I guess.. I'm happy to see it as an
extension, but I think it should be included with the standard
installation. Like we do with for example pl/pgsql (which I realize
has a dependency on the backend anyway, so it can't be done another
way easily) and pl/perl (which doesn't, AFAIK, so it's a better
example)
ATM I'm not sure it's even a good idea to push pgsql_fdw into contrib.
Once we do that its release schedule will get locked to core's ---
wouldn't it be better to keep flexibility for now, while it's in such
active development?
I would be happy to keep it outside, and integrate it in the final CF
for example :)
--
Magnus Hagander
Me: http://www.hagander.net/
Work: http://www.redpill-linpro.com/
ATM I'm not sure it's even a good idea to push pgsql_fdw into contrib.
Once we do that its release schedule will get locked to core's ---
wouldn't it be better to keep flexibility for now, while it's in such
active development?I would be happy to keep it outside, and integrate it in the final CF
for example :)
Right now, file_fdw is the only FDW module that we have in the core,
however, it is inadequacy to proof the new concept of FDW feature
to utilize external RDBMS, such as join push-down of foreign tables.
I think the pgsql-fdw module also should be included in the core
distribution as a basis of future enhancement, unless we don't
need any working modules when an enhancement of FDW is
proposed.
Thanks,
--
KaiGai Kohei <kaigai@kaigai.gr.jp>
* Kohei KaiGai (kaigai@kaigai.gr.jp) wrote:
Right now, file_fdw is the only FDW module that we have in the core,
Erm, guess I'm a bit confused why we've got that in core while not
putting pgsql_fdw in core. This all gets back to previous discussions
around 'recommended' contrib modules (which should really be installed
by default on the filesystem through the distros, ala Debian's
"recommends:" approach) and 'other' contrib modules.
I'm in favor of making that distinction. I would still have pgsql_fdw,
file_fdw, etc, be packaged more-or-less the same way and still use the
CREATE EXTENTION framework, of course.
It would be nice if we didn't have to lock the release schedule of those
recommended modules to the core release schedule, or even to each other,
but that's a separate issue, imv.
Thanks,
Stephen
On Tue, Oct 25, 2011 at 3:08 PM, Tom Lane <tgl@sss.pgh.pa.us> wrote:
Magnus Hagander <magnus@hagander.net> writes:
2011/10/25 Shigeru Hanada <shigeru.hanada@gmail.com>:
I'd like to propose pgsql_fdw, FDW for external PostgreSQL server, as a
contrib module. I think that this module would be the basis of further
SQL/MED development for core, e.g. join-push-down and ANALYZE support.I have not looked at the code itself, but I wonder if we shouldn't
consider making this a part of core-proper, not just a contrib module.
The fact that it isn't *already* available in core surprises a lot of
people...We've just spent a whole lot of blood and sweat on making the extension
mechanism work nicely. I don't understand this urge to not use it.ATM I'm not sure it's even a good idea to push pgsql_fdw into contrib.
Once we do that its release schedule will get locked to core's ---
wouldn't it be better to keep flexibility for now, while it's in such
active development?
Simple question - do FDW internals need work?
--
marko
(2011/10/25 19:15), Magnus Hagander wrote:
2011/10/25 Shigeru Hanada<shigeru.hanada@gmail.com>:
I'd like to propose pgsql_fdw, FDW for external PostgreSQL server, as a
contrib module. I think that this module would be the basis of further
SQL/MED development for core, e.g. join-push-down and ANALYZE support.I have not looked at the code itself, but I wonder if we shouldn't
consider making this a part of core-proper, not just a contrib module.
The fact that it isn't *already* available in core surprises a lot of
people...
Do you mean that pgsql_fdw should be a built-in extension like plpgsql
so that it's available just after initdb? It would be accomplished with
some more changes:
* Move pgsql_fdw into core, say src/backend/foreign/libpgsql_fdw, and
install dynamically loadable module during "make install" for core. The
pgsql_fdw_handler function can't be included into core binary because we
must avoid liking libpq with server binary directly. This method is
also used for libwalreceiver of replication module.
* Create pgsql_fdw extension during initdb invocation, like plpgsql.
These are not trivial, but not difficult so much. However, I think
contrib would be the appropriate place for pgsql_fdw because it's
(relatively) special feature.
--
Shigeru Hanada
Shigeru Hanada <shigeru.hanada@gmail.com> writes:
(2011/10/25 19:15), Magnus Hagander wrote:
I have not looked at the code itself, but I wonder if we shouldn't
consider making this a part of core-proper, not just a contrib module.
The fact that it isn't *already* available in core surprises a lot of
people...
Do you mean that pgsql_fdw should be a built-in extension like plpgsql
so that it's available just after initdb?
If that was what he meant, I'd vote against it. There are way too many
people who will *not* want their databases configured to be able to
reach out onto the net. This feature should be something that has to be
installed by explicit user action.
regards, tom lane
2011/10/26 Shigeru Hanada <shigeru.hanada@gmail.com>:
(2011/10/25 19:15), Magnus Hagander wrote:
2011/10/25 Shigeru Hanada<shigeru.hanada@gmail.com>:
I'd like to propose pgsql_fdw, FDW for external PostgreSQL server, as a
contrib module. I think that this module would be the basis of further
SQL/MED development for core, e.g. join-push-down and ANALYZE support.I have not looked at the code itself, but I wonder if we shouldn't
consider making this a part of core-proper, not just a contrib module.
The fact that it isn't *already* available in core surprises a lot of
people...Do you mean that pgsql_fdw should be a built-in extension like plpgsql
so that it's available just after initdb? It would be accomplished with
some more changes:* Move pgsql_fdw into core, say src/backend/foreign/libpgsql_fdw, and
install dynamically loadable module during "make install" for core. The
pgsql_fdw_handler function can't be included into core binary because we
must avoid liking libpq with server binary directly. This method is
also used for libwalreceiver of replication module.
* Create pgsql_fdw extension during initdb invocation, like plpgsql.These are not trivial, but not difficult so much. However, I think
contrib would be the appropriate place for pgsql_fdw because it's
(relatively) special feature.
I agree. pgsql_fdw will be a nice feature, but there's no reason to
think that everyone will want it installed by default, and there are
some security reasons to think that they might not. On the flip side,
pushing it out of contrib and onto pgfoundry or whatever makes it
unnecessarily difficult to install, and not as many people will
benefit from it. So contrib seems exactly right to me.
--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
Stephen Frost <sfrost@snowman.net> writes:
I'm in favor of making that distinction. I would still have pgsql_fdw,
file_fdw, etc, be packaged more-or-less the same way and still use the
CREATE EXTENTION framework, of course.
We called that idea “core extension” at the latest hackers meeting, and
Greg Smith had a patch with a first selections of extensions to package
this way.
Regards,
--
Dimitri Fontaine
http://2ndQuadrant.fr PostgreSQL : Expertise, Formation et Support
2011/10/26 Robert Haas <robertmhaas@gmail.com>:
2011/10/26 Shigeru Hanada <shigeru.hanada@gmail.com>:
(2011/10/25 19:15), Magnus Hagander wrote:
2011/10/25 Shigeru Hanada<shigeru.hanada@gmail.com>:
I'd like to propose pgsql_fdw, FDW for external PostgreSQL server, as a
contrib module. I think that this module would be the basis of further
SQL/MED development for core, e.g. join-push-down and ANALYZE support.I have not looked at the code itself, but I wonder if we shouldn't
consider making this a part of core-proper, not just a contrib module.
The fact that it isn't *already* available in core surprises a lot of
people...Do you mean that pgsql_fdw should be a built-in extension like plpgsql
so that it's available just after initdb? It would be accomplished with
some more changes:* Move pgsql_fdw into core, say src/backend/foreign/libpgsql_fdw, and
install dynamically loadable module during "make install" for core. The
pgsql_fdw_handler function can't be included into core binary because we
must avoid liking libpq with server binary directly. This method is
also used for libwalreceiver of replication module.
* Create pgsql_fdw extension during initdb invocation, like plpgsql.These are not trivial, but not difficult so much. However, I think
contrib would be the appropriate place for pgsql_fdw because it's
(relatively) special feature.I agree. pgsql_fdw will be a nice feature, but there's no reason to
think that everyone will want it installed by default, and there are
some security reasons to think that they might not. On the flip side,
pushing it out of contrib and onto pgfoundry or whatever makes it
unnecessarily difficult to install, and not as many people will
benefit from it. So contrib seems exactly right to me.
I also agree. The pgsql_fdw will be worthful to locate in the main tree
as a contrib module. It will give us clear opportunity to test new
features of FDW using RDBMS characteristics; such as join-push-down.
However, it should be a separated discussion whether it shall be installed
by the default.
Thanks,
--
KaiGai Kohei <kaigai@kaigai.gr.jp>
On Wed, Oct 26, 2011 at 16:37, Tom Lane <tgl@sss.pgh.pa.us> wrote:
Shigeru Hanada <shigeru.hanada@gmail.com> writes:
(2011/10/25 19:15), Magnus Hagander wrote:
I have not looked at the code itself, but I wonder if we shouldn't
consider making this a part of core-proper, not just a contrib module.
The fact that it isn't *already* available in core surprises a lot of
people...Do you mean that pgsql_fdw should be a built-in extension like plpgsql
so that it's available just after initdb?If that was what he meant, I'd vote against it. There are way too many
people who will *not* want their databases configured to be able to
reach out onto the net. This feature should be something that has to be
installed by explicit user action.
That is not what I meant.
I meant installed the shared library by defualt, but still require
CREATE EXTENSION.
--
Magnus Hagander
Me: http://www.hagander.net/
Work: http://www.redpill-linpro.com/
On 10/26/2011 12:47 PM, Magnus Hagander wrote:
If that was what he meant, I'd vote against it. There are way too many
people who will *not* want their databases configured to be able to
reach out onto the net. This feature should be something that has to be
installed by explicit user action.That is not what I meant.
I meant installed the shared library by defualt, but still require
CREATE EXTENSION.
I don't see why it should be different from other standard modules, such
as citext or hstore, both of which have pretty wide use, and less
possible security implications than this.
cheers
andrew
On Wed, Oct 26, 2011 at 19:25, Andrew Dunstan <andrew@dunslane.net> wrote:
On 10/26/2011 12:47 PM, Magnus Hagander wrote:
If that was what he meant, I'd vote against it. There are way too many
people who will *not* want their databases configured to be able to
reach out onto the net. This feature should be something that has to be
installed by explicit user action.That is not what I meant.
I meant installed the shared library by defualt, but still require
CREATE EXTENSION.I don't see why it should be different from other standard modules, such as
citext or hstore, both of which have pretty wide use, and less possible
security implications than this.
As I stated earlier, it's really back to the old discussion of
splitting up contrib. This would be the "additional module" part, but
not the "example of how to do things" part of that...
--
Magnus Hagander
Me: http://www.hagander.net/
Work: http://www.redpill-linpro.com/
Magnus Hagander <magnus@hagander.net> writes:
On Wed, Oct 26, 2011 at 16:37, Tom Lane <tgl@sss.pgh.pa.us> wrote:
If that was what he meant, I'd vote against it. �There are way too many
people who will *not* want their databases configured to be able to
reach out onto the net. �This feature should be something that has to be
installed by explicit user action.
That is not what I meant.
I meant installed the shared library by defualt, but still require
CREATE EXTENSION.
Whether the shlib is installed by default is a decision for packagers to
make, not us. At best we could make a recommendation.
regards, tom lane
(2011/10/26 23:57), Kohei KaiGai wrote:
2011/10/26 Robert Haas<robertmhaas@gmail.com>:
I agree. pgsql_fdw will be a nice feature, but there's no reason to
think that everyone will want it installed by default, and there are
some security reasons to think that they might not. On the flip side,
pushing it out of contrib and onto pgfoundry or whatever makes it
unnecessarily difficult to install, and not as many people will
benefit from it. So contrib seems exactly right to me.I also agree. The pgsql_fdw will be worthful to locate in the main tree
as a contrib module. It will give us clear opportunity to test new
features of FDW using RDBMS characteristics; such as join-push-down.
However, it should be a separated discussion whether it shall be installed
by the default.
There seems to be some approvals on pushing pgsql_fdw into main tree
(contrib or core extension, or something else), but not an external
module. There are still some debatable issues, but they would be
meaningless unless pgsql_fdw is qualified for a contrib module. So I'd
like to continue the development of pgsql_fdw as contrib module, at
least for a while.
Please find attached a patch for pgsql_fdw. This patch needs first two
patches attached to OP[1]http://archives.postgresql.org/pgsql-hackers/2011-10/msg01329.php to be applied. (Sorry. gathering patches from
another post must be bothersome work. Should I create new CF items for
fundamental patches?)
[1]: http://archives.postgresql.org/pgsql-hackers/2011-10/msg01329.php
Changes done since last post are:
* add colname FDW option support
* allow some libpq options (authtype and tty) to be specified as server
FDW options
--
Shigeru Hanada
* ポルトガル語 - 自動検出
* 英語
* 日本語
* 英語
* 日本語
<javascript:void(0);>
Attachments:
pgsql_fdw_v2.patchtext/plain; name=pgsql_fdw_v2.patchDownload
diff --git a/contrib/Makefile b/contrib/Makefile
index 0c238aa..e09e61e 100644
*** a/contrib/Makefile
--- b/contrib/Makefile
*************** SUBDIRS = \
*** 41,46 ****
--- 41,47 ----
pgbench \
pgcrypto \
pgrowlocks \
+ pgsql_fdw \
pgstattuple \
seg \
spi \
diff --git a/contrib/README b/contrib/README
index a1d42a1..d3fa211 100644
*** a/contrib/README
--- b/contrib/README
*************** pgrowlocks -
*** 158,163 ****
--- 158,167 ----
A function to return row locking information
by Tatsuo Ishii <ishii@sraoss.co.jp>
+ pgsql_fdw -
+ Foreign-data wrapper for external PostgreSQL servers.
+ by Shigeru Hanada <shigeru.hanada@gmail.com>
+
pgstattuple -
Functions to return statistics about "dead" tuples and free
space within a table
diff --git a/contrib/pgsql_fdw/.gitignore b/contrib/pgsql_fdw/.gitignore
index ...0854728 .
*** a/contrib/pgsql_fdw/.gitignore
--- b/contrib/pgsql_fdw/.gitignore
***************
*** 0 ****
--- 1,4 ----
+ # Generated subdirectories
+ /results/
+ *.o
+ *.so
diff --git a/contrib/pgsql_fdw/Makefile b/contrib/pgsql_fdw/Makefile
index ...6381365 .
*** a/contrib/pgsql_fdw/Makefile
--- b/contrib/pgsql_fdw/Makefile
***************
*** 0 ****
--- 1,22 ----
+ # contrib/pgsql_fdw/Makefile
+
+ MODULE_big = pgsql_fdw
+ OBJS = pgsql_fdw.o option.o deparse.o connection.o
+ PG_CPPFLAGS = -I$(libpq_srcdir)
+ SHLIB_LINK = $(libpq)
+
+ EXTENSION = pgsql_fdw
+ DATA = pgsql_fdw--1.0.sql
+
+ REGRESS = pgsql_fdw
+
+ ifdef USE_PGXS
+ PG_CONFIG = pg_config
+ PGXS := $(shell $(PG_CONFIG) --pgxs)
+ include $(PGXS)
+ else
+ subdir = contrib/pgsql_fdw
+ top_builddir = ../..
+ include $(top_builddir)/src/Makefile.global
+ include $(top_srcdir)/contrib/contrib-global.mk
+ endif
diff --git a/contrib/pgsql_fdw/connection.c b/contrib/pgsql_fdw/connection.c
index ...a2b88ec .
*** a/contrib/pgsql_fdw/connection.c
--- b/contrib/pgsql_fdw/connection.c
***************
*** 0 ****
--- 1,504 ----
+ /*-------------------------------------------------------------------------
+ *
+ * connection.c
+ * Connection management for pgsql_fdw
+ *
+ * Portions Copyright (c) 2011, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/connection.c
+ *
+ *-------------------------------------------------------------------------
+ */
+ #include "postgres.h"
+
+ #include "catalog/pg_type.h"
+ #include "foreign/foreign.h"
+ #include "funcapi.h"
+ #include "libpq-fe.h"
+ #include "mb/pg_wchar.h"
+ #include "miscadmin.h"
+ #include "utils/array.h"
+ #include "utils/builtins.h"
+ #include "utils/hsearch.h"
+ #include "utils/memutils.h"
+ #include "utils/resowner.h"
+ #include "utils/tuplestore.h"
+
+ #include "pgsql_fdw.h"
+ #include "connection.h"
+
+ /* ============================================================================
+ * Connection management functions
+ * ==========================================================================*/
+
+ /*
+ * Connection cache entry managed with hash table.
+ */
+ typedef struct ConnCacheEntry
+ {
+ /* hash key must be first */
+ Oid serverid; /* oid of foreign server */
+ Oid userid; /* oid of local user */
+
+ 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)
+ {
+ bool found;
+ ConnCacheEntry *entry;
+ ConnCacheEntry key;
+ PGconn *conn = NULL;
+
+ /* initialize connection cache if it isn't */
+ if (FSConnectionHash == NULL)
+ {
+ HASHCTL ctl;
+
+ /* hash key is a pair of oids: serverid and userid */
+ MemSet(&ctl, 0, sizeof(ctl));
+ ctl.keysize = sizeof(Oid) + sizeof(Oid);
+ ctl.entrysize = sizeof(ConnCacheEntry);
+ ctl.hash = tag_hash;
+ ctl.match = memcmp;
+ ctl.keycopy = memcpy;
+ /* allocate FSConnectionHash in the cache context */
+ ctl.hcxt = CacheMemoryContext;
+ FSConnectionHash = hash_create("Foreign Connections", 32,
+ &ctl,
+ HASH_ELEM | HASH_CONTEXT |
+ HASH_FUNCTION | HASH_COMPARE |
+ HASH_KEYCOPY);
+ }
+
+ /* Create key value for the entry. */
+ MemSet(&key, 0, sizeof(key));
+ key.serverid = server->serverid;
+ key.userid = GetOuterUserId();
+
+ /* Is there any cached and valid connection with such key? */
+ entry = hash_search(FSConnectionHash, &key, HASH_ENTER, &found);
+ if (found)
+ {
+ if (entry->conn != NULL)
+ {
+ entry->refs++;
+ elog(DEBUG1,
+ "reuse connection %u/%u (%d)",
+ entry->serverid,
+ entry->userid,
+ entry->refs);
+ 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 ResourceOwner to clean the connection up on error including
+ * user interrupt.
+ */
+ elog(DEBUG1,
+ "create entry for %u/%u (%d)",
+ entry->serverid,
+ entry->userid,
+ entry->refs);
+ 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: key items of entry has been initialized in
+ * hash_search(HASH_ENTER).
+ */
+ entry->refs = 1;
+ entry->conn = conn;
+ elog(DEBUG1,
+ "connected to %u/%u (%d)",
+ entry->serverid,
+ entry->userid,
+ 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 PGconn *
+ connect_pg_server(ForeignServer *server, UserMapping *user)
+ {
+ const char *conname = server->servername;
+ PGconn *conn;
+ PGresult *res;
+ 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. Those two object hold only libpq options.
+ * Extra 3 items are for:
+ * *) fallback_application_name
+ * *) client_encoding
+ * *) NULL termination (end marker)
+ *
+ * Note: We don't omit any parameters even target database might be older
+ * than local, because unexpected parameters are just ignored.
+ */
+ n = list_length(server->options) + list_length(user->options) + 3;
+ 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 += ExtractConnectionOptions(server->options,
+ all_keywords + n, all_values + n);
+ n += ExtractConnectionOptions(user->options,
+ all_keywords + n, all_values + n);
+ all_keywords[n] = all_values[n] = NULL;
+
+ for (i = 0, j = 0; all_keywords[i]; i++)
+ {
+ keywords[j] = all_keywords[i];
+ values[j] = all_values[i];
+ j++;
+ }
+
+ /* Use "pgsql_fdw" as fallback_application_name. */
+ keywords[j] = "fallback_application_name";
+ values[j++] = "pgsql_fdw";
+
+ /* Set client_encoding so that libpq can convert encoding properly. */
+ keywords[j] = "client_encoding";
+ values[j++] = GetDatabaseEncodingName();
+
+ keywords[j] = values[j] = NULL;
+ pfree(all_keywords);
+ pfree(all_values);
+
+ /* 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.")));
+ }
+
+ /*
+ * Start transaction to use cursor to retrieve data separately.
+ */
+ res = PQexec(conn, "BEGIN");
+ if (res == NULL || PQresultStatus(res) != PGRES_COMMAND_OK)
+ elog(ERROR, "could not start transaction");
+
+ 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 sequentially 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;
+ }
+ if (entry != NULL)
+ 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 referrer, unregister it from cache.
+ * TODO: Note that sharing connections requires a mechanism to detect
+ * change of FDW object to invalidate lasting connections.
+ */
+ entry->refs--;
+ elog(DEBUG1,
+ "connection %u/%u released (%d)",
+ entry->serverid,
+ entry->userid,
+ entry->refs);
+ }
+
+ /*
+ * 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(DEBUG1,
+ "closing connection %u/%u",
+ entry->serverid,
+ entry->userid);
+ PQfinish(entry->conn);
+ entry->refs = 0;
+ entry->conn = NULL;
+ }
+ else
+ elog(DEBUG1,
+ "connection %u/%u already closed",
+ entry->serverid,
+ entry->userid);
+ }
+
+ /*
+ * Get list of connections currently active.
+ */
+ Datum pgsql_fdw_get_connections(PG_FUNCTION_ARGS);
+ PG_FUNCTION_INFO_V1(pgsql_fdw_get_connections);
+ Datum
+ pgsql_fdw_get_connections(PG_FUNCTION_ARGS)
+ {
+ ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+ HASH_SEQ_STATUS scan;
+ ConnCacheEntry *entry;
+ MemoryContext oldcontext = CurrentMemoryContext;
+ Tuplestorestate *tuplestore;
+ TupleDesc tupdesc;
+
+ /* We return list of connection with storing them in a Tuplestore. */
+ rsinfo->returnMode = SFRM_Materialize;
+ rsinfo->setResult = NULL;
+ rsinfo->setDesc = NULL;
+
+ /* Create tuplestore and copy of TupleDesc in per-query context. */
+ MemoryContextSwitchTo(rsinfo->econtext->ecxt_per_query_memory);
+
+ tupdesc = CreateTemplateTupleDesc(2, false);
+ TupleDescInitEntry(tupdesc, 1, "srvid", OIDOID, -1, 0);
+ TupleDescInitEntry(tupdesc, 2, "usesysid", OIDOID, -1, 0);
+ rsinfo->setDesc = tupdesc;
+
+ tuplestore = tuplestore_begin_heap(false, false, work_mem);
+ rsinfo->setResult = tuplestore;
+
+ MemoryContextSwitchTo(oldcontext);
+
+ /*
+ * We need to scan sequentially since we use the address to find
+ * appropriate PGconn from the hash table.
+ */
+ if (FSConnectionHash != NULL)
+ {
+ hash_seq_init(&scan, FSConnectionHash);
+ while ((entry = (ConnCacheEntry *) hash_seq_search(&scan)))
+ {
+ Datum values[2];
+ bool nulls[2];
+ HeapTuple tuple;
+
+ elog(DEBUG1, "found: %u/%u", entry->serverid, entry->userid);
+
+ /* Ignore inactive connections */
+ if (PQstatus(entry->conn) != CONNECTION_OK)
+ continue;
+
+ /*
+ * Ignore other users' connections if current user isn't a
+ * superuser.
+ */
+ if (!superuser() && entry->userid != GetUserId())
+ continue;
+
+ values[0] = ObjectIdGetDatum(entry->serverid);
+ values[1] = ObjectIdGetDatum(entry->userid);
+ nulls[0] = false;
+ nulls[1] = false;
+
+ tuple = heap_formtuple(tupdesc, values, nulls);
+ tuplestore_puttuple(tuplestore, tuple);
+ }
+ }
+ tuplestore_donestoring(tuplestore);
+
+ PG_RETURN_VOID();
+ }
+
+ /*
+ * Discard persistent connection designated by given connection name.
+ */
+ Datum pgsql_fdw_disconnect(PG_FUNCTION_ARGS);
+ PG_FUNCTION_INFO_V1(pgsql_fdw_disconnect);
+ Datum
+ pgsql_fdw_disconnect(PG_FUNCTION_ARGS)
+ {
+ Oid serverid = PG_GETARG_OID(0);
+ Oid userid = PG_GETARG_OID(1);
+ ConnCacheEntry key;
+ ConnCacheEntry *entry = NULL;
+ bool found;
+
+ /* Non-superuser can't discard other users' connection. */
+ if (!superuser() && userid != GetOuterUserId())
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_RESOURCES),
+ errmsg("only superuser can discard other user's connection")));
+
+ /*
+ * If no connection has been established, or no such connections, just
+ * return "NG" to indicate nothing has done.
+ */
+ if (FSConnectionHash == NULL)
+ PG_RETURN_TEXT_P(cstring_to_text("NG"));
+
+ key.serverid = serverid;
+ key.userid = userid;
+ entry = hash_search(FSConnectionHash, &key, HASH_FIND, &found);
+ if (!found)
+ PG_RETURN_TEXT_P(cstring_to_text("NG"));
+
+ /* Discard cached connection, and clear reference counter. */
+ PQfinish(entry->conn);
+ entry->refs = 0;
+ entry->conn = NULL;
+ elog(DEBUG1, "closed connection %u/%u", serverid, userid);
+
+ PG_RETURN_TEXT_P(cstring_to_text("OK"));
+ }
diff --git a/contrib/pgsql_fdw/connection.h b/contrib/pgsql_fdw/connection.h
index ...694534f .
*** a/contrib/pgsql_fdw/connection.h
--- b/contrib/pgsql_fdw/connection.h
***************
*** 0 ****
--- 1,25 ----
+ /*-------------------------------------------------------------------------
+ *
+ * connection.h
+ * Connection management for pgsql_fdw
+ *
+ * Portions Copyright (c) 2011, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_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/pgsql_fdw/deparse.c b/contrib/pgsql_fdw/deparse.c
index ...c1c74ac .
*** a/contrib/pgsql_fdw/deparse.c
--- b/contrib/pgsql_fdw/deparse.c
***************
*** 0 ****
--- 1,377 ----
+ /*-------------------------------------------------------------------------
+ *
+ * deparse.c
+ * query deparser for PostgreSQL
+ *
+ * Copyright (c) 2011, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/deparse.c
+ *
+ *-------------------------------------------------------------------------
+ */
+ #include "postgres.h"
+
+ #include "access/transam.h"
+ #include "foreign/foreign.h"
+ #include "lib/stringinfo.h"
+ #include "nodes/nodeFuncs.h"
+ #include "nodes/nodes.h"
+ #include "nodes/makefuncs.h"
+ #include "optimizer/clauses.h"
+ #include "optimizer/var.h"
+ #include "parser/parsetree.h"
+ #include "utils/builtins.h"
+ #include "utils/lsyscache.h"
+
+ #include "pgsql_fdw.h"
+
+ /*
+ * Context for walk-through the expression tree.
+ */
+ typedef struct foreign_executable_cxt
+ {
+ PlannerInfo *root;
+ RelOptInfo *foreignrel;
+ } foreign_executable_cxt;
+
+ static bool is_foreign_expr(PlannerInfo *root, RelOptInfo *baserel, Expr *expr);
+ static bool foreign_expr_walker(Node *node, foreign_executable_cxt *context);
+ static bool is_proc_remotely_executable(Oid procid);
+
+ /*
+ * Deparse query representation into SQL statement which suits for remote
+ * PostgreSQL server. Also some of quals in WHERE clause will be pushed down
+ * If they are safe to be evaluated on the remote side.
+ */
+ char *
+ deparseSql(Oid relid, PlannerInfo *root, RelOptInfo *baserel)
+ {
+ StringInfoData foreign_relname;
+ StringInfoData sql; /* builder for SQL statement */
+ bool first;
+ AttrNumber attr;
+ List *attr_used = NIL; /* List of AttNumber used in the query */
+ const char *nspname = NULL; /* plain namespace name */
+ const char *relname = NULL; /* plain relation name */
+ const char *q_nspname; /* quoted namespace name */
+ const char *q_relname; /* quoted relation name */
+ List *foreign_expr = NIL; /* list of Expr* evaluated on remote */
+ int i;
+ List *rtable = NIL;
+ List *context = NIL;
+
+ initStringInfo(&sql);
+ initStringInfo(&foreign_relname);
+
+ /*
+ * First of all, determine which qual can be pushed down.
+ *
+ * The expressions which satisfy is_foreign_expr() are deparsed into WHERE
+ * clause of result SQL string, and they could be removed from PlanState
+ * to avoid duplicate evaluation at ExecScan().
+ *
+ * We never change the quals in the Plan node, because this execution might
+ * be for a PREPAREd statement, thus the quals in the Plan node might be
+ * reused to construct another PlanState for subsequent EXECUTE statement.
+ *
+ * We do this before deparsing SELECT clause because attributes which are
+ * not used in neither reltargetlist nor baserel->baserestrictinfo, quals
+ * evaluated on local, can be replaced with literal "NULL" in the SELECT
+ * clause to reduce overhead of tuple handling tuple and data transfer.
+ */
+ if (baserel->baserestrictinfo != NIL)
+ {
+ ListCell *lc;
+ List *local_qual = NIL;
+
+ foreach (lc, baserel->baserestrictinfo)
+ {
+ RestrictInfo *ri = (RestrictInfo *) lfirst(lc);
+
+ /* Determine whether the qual can be pushed down or not. */
+ if (is_foreign_expr(root, baserel, ri->clause))
+ foreign_expr = lappend(foreign_expr, ri->clause);
+ else
+ {
+ List *attrs;
+
+ /*
+ * We need to know which attributes are used in qual evaluated
+ * on the local server, because they should be listed in the
+ * SELECT clause of remote query. We can ignore attributes
+ * which are referenced only in ORDER BY/GROUP BY clause because
+ * such attributes has already been kept in reltargetlist.
+ */
+ attrs = pull_var_clause((Node *) ri->clause,
+ PVC_RECURSE_AGGREGATES,
+ PVC_RECURSE_PLACEHOLDERS);
+ attr_used = list_union(attr_used, attrs);
+
+ /*
+ * Save the RestrictInfo to replace baserel->baserestrictinfo
+ * afterward.
+ */
+ local_qual = lappend(local_qual, ri);
+ }
+ }
+
+ /*
+ * Remove quals which are going to evaluated on the foreign server to
+ * avoid overhead of duplicated evaluation.
+ */
+ baserel->baserestrictinfo = local_qual;
+ }
+
+ /*
+ * Determine foreign relation's qualified name. This is necessary for
+ * FROM clause and SELECT clause.
+ */
+ nspname = GetFdwOptionValue(relid, InvalidAttrNumber, "nspname");
+ if (nspname == NULL)
+ nspname = get_namespace_name(get_rel_namespace(relid));
+ q_nspname = quote_identifier(nspname);
+
+ relname = GetFdwOptionValue(relid, InvalidAttrNumber, "relname");
+ if (relname == NULL)
+ relname = get_rel_name(relid);
+ q_relname = quote_identifier(relname);
+
+ appendStringInfo(&foreign_relname, "%s.%s", q_nspname, q_relname);
+
+ /*
+ * We need to replace aliasname and colnames of the target relation so that
+ * constructed remote query is valid.
+ *
+ * Note that we skip first empty element of simple_rel_array. See also
+ * comments of simple_rel_array and simple_rte_array for the rationale.
+ */
+ for (i = 1; i < root->simple_rel_array_size; i++)
+ {
+ RangeTblEntry *rte = copyObject(root->simple_rte_array[i]);
+ List *newcolnames = NIL;
+
+ if (i == baserel->relid)
+ {
+ /*
+ * Create new list of column names which is used to deparse remote
+ * query from specified names or local column names. This list is
+ * used by deparse_expression.
+ */
+ for (attr = 1; attr <= baserel->max_attr; attr++)
+ {
+ char *colname;
+
+ /* Ignore droppped attributes. */
+ if (get_rte_attribute_is_dropped(rte, attr))
+ continue;
+
+ colname = GetFdwOptionValue(relid, attr, "colname");
+ if (colname == NULL)
+ colname = strVal(list_nth(rte->eref->colnames, attr - 1));
+ newcolnames = lappend(newcolnames, makeString(colname));
+ }
+ rte->alias = makeAlias(relname, newcolnames);
+ }
+ rtable = lappend(rtable, rte);
+ }
+ context = deparse_context_for_rtelist(rtable);
+
+ /*
+ * deparse SELECT clause
+ *
+ * List attributes which are in either target list or local restriction.
+ * Unused attributes are replaced with a literal "NULL" for optimization.
+ */
+ appendStringInfo(&sql, "SELECT ");
+ attr_used = list_union(attr_used, baserel->reltargetlist);
+ first = true;
+ for (attr = 1; attr <= baserel->max_attr; attr++)
+ {
+ RangeTblEntry *rte = root->simple_rte_array[baserel->relid];
+ Var *var = NULL;
+ ListCell *lc;
+
+ /* Ignore droppped attributes. */
+ if (get_rte_attribute_is_dropped(rte, attr))
+ continue;
+
+ if (!first)
+ appendStringInfo(&sql, ", ");
+ first = false;
+
+ /*
+ * We use linear search here, but it wouldn't be problem since
+ * attr_used seems to not become so large.
+ */
+ foreach (lc, attr_used)
+ {
+ var = lfirst(lc);
+ if (var->varattno == attr)
+ break;
+ var = NULL;
+ }
+ if (var != NULL)
+ appendStringInfo(&sql, "%s",
+ deparse_expression((Node *) var, context, false, false));
+ else
+ appendStringInfo(&sql, "NULL");
+ }
+ appendStringInfoChar(&sql, ' ');
+
+ /*
+ * deparse FROM clause, including alias if any
+ */
+ appendStringInfo(&sql, "FROM %s", foreign_relname.data);
+
+ /*
+ * deparse WHERE clause
+ */
+ if (foreign_expr != NIL)
+ {
+ Node *node;
+
+ node = (Node *) make_ands_explicit(foreign_expr);
+ appendStringInfo(&sql, " WHERE %s ",
+ deparse_expression(node, context, false, false));
+ list_free(foreign_expr);
+ foreign_expr = NIL;
+ }
+
+ elog(DEBUG3, "Remote SQL: %s", sql.data);
+ return sql.data;
+ }
+
+ /*
+ * Returns true if expr is safe to be evaluated on the foreign server.
+ */
+ static bool
+ is_foreign_expr(PlannerInfo *root, RelOptInfo *baserel, Expr *expr)
+ {
+ foreign_executable_cxt context;
+ context.root = root;
+ context.foreignrel = baserel;
+
+ /*
+ * An expression which includes any mutable function can't be pushed down
+ * because it's result is not stable. For example, pushing now() down to
+ * remote side would cause confusion from the clock offset.
+ * If we have routine mapping infrastructure in future release, we will be
+ * able to choose function to be pushed down in finer granularity.
+ */
+ if (contain_mutable_functions((Node *) expr))
+ return false;
+
+ /*
+ * Check that the expression consists of nodes which are known as safe to
+ * be pushed down.
+ */
+ if (foreign_expr_walker((Node *) expr, &context))
+ return false;
+
+ return true;
+ }
+
+ /*
+ * Return true if node includes any node which is not known as safe to be
+ * pushed down.
+ */
+ static bool
+ foreign_expr_walker(Node *node, foreign_executable_cxt *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 subtre of the node, if any, should be checked
+ * continuously at the tail of this function.
+ */
+ break;
+ case T_Param:
+ /*
+ * Only external parameters can be pushed down.:
+ */
+ {
+ if (((Param *) node)->paramkind != PARAM_EXTERN)
+ return true;
+ }
+ break;
+ case T_OpExpr:
+ /*
+ * Operators which use non-immutable function can't be pushed down.
+ */
+ {
+ OpExpr *oe = (OpExpr *) node;
+
+ if (!is_proc_remotely_executable(oe->opfuncid))
+ return true;
+
+ /* operands are checked later */
+ }
+ break;
+ case T_FuncExpr:
+ /*
+ * Non-immutable functions can't be pushed down.
+ */
+ {
+ FuncExpr *fe = (FuncExpr *) node;
+
+ if (!is_proc_remotely_executable(fe->funcid))
+ return true;
+
+ /* operands are checked later */
+ }
+ 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;
+ foreign_executable_cxt *f_context;
+
+ f_context = (foreign_executable_cxt *) context;
+ if (var->varno != f_context->foreignrel->relid ||
+ var->varlevelsup != 0)
+ return true;
+ }
+ break;
+ default:
+ {
+ ereport(DEBUG3,
+ (errmsg("expression is too complex"),
+ errdetail("%s", nodeToString(node))));
+ return true;
+ }
+ break;
+ }
+
+ return expression_tree_walker(node, foreign_expr_walker, context);
+ }
+
+ /*
+ * Return true if func is known as safe to be pushed down if all of the
+ * arguments are also known as safe.
+ */
+ static bool
+ is_proc_remotely_executable(Oid procid)
+ {
+ /*
+ * User-defined procedures can't be pushed down.
+ */
+ if (procid >= FirstNormalObjectId)
+ return false;
+
+ return true;
+ }
+
diff --git a/contrib/pgsql_fdw/expected/pgsql_fdw.out b/contrib/pgsql_fdw/expected/pgsql_fdw.out
index ...514e759 .
*** a/contrib/pgsql_fdw/expected/pgsql_fdw.out
--- b/contrib/pgsql_fdw/expected/pgsql_fdw.out
***************
*** 0 ****
--- 1,408 ----
+ -- ===================================================================
+ -- create FDW objects
+ -- ===================================================================
+ CREATE EXTENSION pgsql_fdw;
+ CREATE SERVER loopback1 FOREIGN DATA WRAPPER pgsql_fdw;
+ CREATE SERVER loopback2 FOREIGN DATA WRAPPER pgsql_fdw
+ OPTIONS (dbname 'contrib_regression');
+ CREATE USER MAPPING FOR public SERVER loopback1
+ OPTIONS (user 'value', password 'value');
+ CREATE USER MAPPING FOR public SERVER loopback2;
+ CREATE FOREIGN TABLE ft1 (
+ c1 int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp
+ ) SERVER loopback2;
+ CREATE FOREIGN TABLE ft2 (
+ c1 int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp
+ ) SERVER loopback2;
+ -- ===================================================================
+ -- create objects used through FDW
+ -- ===================================================================
+ CREATE SCHEMA "S 1";
+ CREATE TABLE "S 1"."T 1" (
+ "C 1" int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ CONSTRAINT t1_pkey PRIMARY KEY ("C 1")
+ );
+ NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "t1_pkey" for table "T 1"
+ CREATE TABLE "S 1"."T 2" (
+ c1 int NOT NULL,
+ c2 text,
+ CONSTRAINT t2_pkey PRIMARY KEY (c1)
+ );
+ NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "t2_pkey" for table "T 2"
+ BEGIN;
+ TRUNCATE "S 1"."T 1";
+ INSERT INTO "S 1"."T 1"
+ SELECT id,
+ id % 10,
+ to_char(id, 'FM00000'),
+ '1970-01-01'::timestamptz + ((id % 100) || ' days')::interval,
+ '1970-01-01'::timestamp + ((id % 100) || ' days')::interval
+ FROM generate_series(1, 1000) id;
+ TRUNCATE "S 1"."T 2";
+ INSERT INTO "S 1"."T 2"
+ SELECT id,
+ 'AAA' || to_char(id, 'FM000')
+ FROM generate_series(1, 100) id;
+ COMMIT;
+ -- ===================================================================
+ -- tests for pgsql_fdw_validator
+ -- ===================================================================
+ ALTER FOREIGN DATA WRAPPER pgsql_fdw OPTIONS (host 'value'); -- ERRROR
+ ERROR: invalid option "host"
+ HINT: Valid options in this context are:
+ -- requiressl, krbsrvname and gsslib are omitted because they depend on
+ -- configure option
+ ALTER SERVER loopback1 OPTIONS (
+ authtype 'value',
+ service 'value',
+ connect_timeout 'value',
+ dbname 'value',
+ host 'value',
+ hostaddr 'value',
+ port 'value',
+ --client_encoding 'value',
+ tty 'value',
+ options 'value',
+ application_name 'value',
+ --fallback_application_name 'value',
+ keepalives 'value',
+ keepalives_idle 'value',
+ keepalives_interval 'value',
+ -- requiressl 'value',
+ sslmode 'value',
+ sslcert 'value',
+ sslkey 'value',
+ sslrootcert 'value',
+ sslcrl 'value'
+ --requirepeer 'value',
+ -- krbsrvname 'value',
+ -- gsslib 'value',
+ --replication 'value'
+ );
+ ALTER SERVER loopback1 OPTIONS (user 'value'); -- ERROR
+ ERROR: invalid option "user"
+ HINT: Valid options in this context are: authtype, service, connect_timeout, dbname, host, hostaddr, port, tty, options, application_name, keepalives, keepalives_idle, keepalives_interval, keepalives_count, sslmode, sslcert, sslkey, sslrootcert, sslcrl, requirepeer, min_cursor_rows, fetch_count
+ ALTER SERVER loopback2 OPTIONS (ADD min_cursor_rows '0');
+ ALTER SERVER loopback2 OPTIONS (ADD fetch_count '2');
+ ALTER USER MAPPING FOR public SERVER loopback1
+ OPTIONS (DROP user, DROP password);
+ ALTER USER MAPPING FOR public SERVER loopback1
+ OPTIONS (host 'value'); -- ERROR
+ ERROR: invalid option "host"
+ HINT: Valid options in this context are: user, password
+ ALTER FOREIGN TABLE ft1 OPTIONS (nspname 'S 1', relname 'T 1');
+ ALTER FOREIGN TABLE ft2 OPTIONS (nspname 'S 1', relname 'T 1', min_cursor_rows '10', fetch_count '100');
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (invalid 'value'); -- ERROR
+ ERROR: invalid option "invalid"
+ HINT: Valid options in this context are: colname
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (colname 'C 1');
+ ALTER FOREIGN TABLE ft2 ALTER COLUMN c1 OPTIONS (colname 'C 1');
+ ALTER FOREIGN TABLE ft1 OPTIONS (min_cursor_rows 'a'); -- ERROR
+ ERROR: invalid value for min_cursor_rows: "a"
+ ALTER FOREIGN TABLE ft1 OPTIONS (min_cursor_rows '-1'); -- ERROR
+ ERROR: invalid value for min_cursor_rows: "-1"
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count 'a'); -- ERROR
+ ERROR: invalid value for fetch_count: "a"
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count '0'); -- ERROR
+ ERROR: invalid value for fetch_count: "0"
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count '-1'); -- ERROR
+ ERROR: invalid value for fetch_count: "-1"
+ \dew+
+ List of foreign-data wrappers
+ Name | Owner | Handler | Validator | Access privileges | FDW Options | Description
+ -----------+----------+-------------------+---------------------+-------------------+-------------+-------------
+ pgsql_fdw | postgres | pgsql_fdw_handler | pgsql_fdw_validator | | |
+ (1 row)
+
+ \des+
+ List of foreign servers
+ Name | Owner | Foreign-data wrapper | Access privileges | Type | Version | FDW Options | Description
+ -----------+----------+----------------------+-------------------+------+---------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+-------------
+ loopback1 | postgres | pgsql_fdw | | | | (authtype 'value', service 'value', connect_timeout 'value', dbname 'value', host 'value', hostaddr 'value', port 'value', tty 'value', options 'value', application_name 'value', keepalives 'value', keepalives_idle 'value', keepalives_interval 'value', sslmode 'value', sslcert 'value', sslkey 'value', sslrootcert 'value', sslcrl 'value') |
+ loopback2 | postgres | pgsql_fdw | | | | (dbname 'contrib_regression', min_cursor_rows '0', fetch_count '2') |
+ (2 rows)
+
+ \deu+
+ List of user mappings
+ Server | User name | FDW Options
+ -----------+-----------+-------------
+ loopback1 | public |
+ loopback2 | public |
+ (2 rows)
+
+ \det+
+ List of foreign tables
+ Schema | Table | Server | FDW Options | Description
+ --------+-------+-----------+-------------------------------------------------------------------------+-------------
+ public | ft1 | loopback2 | (nspname 'S 1', relname 'T 1') |
+ public | ft2 | loopback2 | (nspname 'S 1', relname 'T 1', min_cursor_rows '10', fetch_count '100') |
+ (2 rows)
+
+ -- ===================================================================
+ -- simple queries
+ -- ===================================================================
+ -- single table, with/without alias
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
+ QUERY PLAN
+ -------------------------------------------------------------------------
+ Limit
+ Output: c1, c2, c3, c4, c5
+ -> Sort
+ Output: c1, c2, c3, c4, c5
+ Sort Key: ft1.c3, ft1.c1
+ -> Foreign Scan on public.ft1
+ Output: c1, c2, c3, c4, c5
+ Remote SQL: SELECT "C 1", c2, c3, c4, c5 FROM "S 1"."T 1"
+ (8 rows)
+
+ SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
+ c1 | c2 | c3 | c4 | c5
+ -----+----+-------+------------------------------+--------------------------
+ 101 | 1 | 00101 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970
+ 102 | 2 | 00102 | Sat Jan 03 00:00:00 1970 PST | Sat Jan 03 00:00:00 1970
+ 103 | 3 | 00103 | Sun Jan 04 00:00:00 1970 PST | Sun Jan 04 00:00:00 1970
+ 104 | 4 | 00104 | Mon Jan 05 00:00:00 1970 PST | Mon Jan 05 00:00:00 1970
+ 105 | 5 | 00105 | Tue Jan 06 00:00:00 1970 PST | Tue Jan 06 00:00:00 1970
+ 106 | 6 | 00106 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970
+ 107 | 7 | 00107 | Thu Jan 08 00:00:00 1970 PST | Thu Jan 08 00:00:00 1970
+ 108 | 8 | 00108 | Fri Jan 09 00:00:00 1970 PST | Fri Jan 09 00:00:00 1970
+ 109 | 9 | 00109 | Sat Jan 10 00:00:00 1970 PST | Sat Jan 10 00:00:00 1970
+ 110 | 0 | 00110 | Sun Jan 11 00:00:00 1970 PST | Sun Jan 11 00:00:00 1970
+ (10 rows)
+
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+ QUERY PLAN
+ -------------------------------------------------------------------------
+ Limit
+ Output: c1, c2, c3, c4, c5
+ -> Sort
+ Output: c1, c2, c3, c4, c5
+ Sort Key: t1.c3, t1.c1
+ -> Foreign Scan on public.ft1 t1
+ Output: c1, c2, c3, c4, c5
+ Remote SQL: SELECT "C 1", c2, c3, c4, c5 FROM "S 1"."T 1"
+ (8 rows)
+
+ SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+ c1 | c2 | c3 | c4 | c5
+ -----+----+-------+------------------------------+--------------------------
+ 101 | 1 | 00101 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970
+ 102 | 2 | 00102 | Sat Jan 03 00:00:00 1970 PST | Sat Jan 03 00:00:00 1970
+ 103 | 3 | 00103 | Sun Jan 04 00:00:00 1970 PST | Sun Jan 04 00:00:00 1970
+ 104 | 4 | 00104 | Mon Jan 05 00:00:00 1970 PST | Mon Jan 05 00:00:00 1970
+ 105 | 5 | 00105 | Tue Jan 06 00:00:00 1970 PST | Tue Jan 06 00:00:00 1970
+ 106 | 6 | 00106 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970
+ 107 | 7 | 00107 | Thu Jan 08 00:00:00 1970 PST | Thu Jan 08 00:00:00 1970
+ 108 | 8 | 00108 | Fri Jan 09 00:00:00 1970 PST | Fri Jan 09 00:00:00 1970
+ 109 | 9 | 00109 | Sat Jan 10 00:00:00 1970 PST | Sat Jan 10 00:00:00 1970
+ 110 | 0 | 00110 | Sun Jan 11 00:00:00 1970 PST | Sun Jan 11 00:00:00 1970
+ (10 rows)
+
+ -- with WHERE clause
+ SELECT * FROM ft1 t1 WHERE t1.c1 = 101;
+ c1 | c2 | c3 | c4 | c5
+ -----+----+-------+------------------------------+--------------------------
+ 101 | 1 | 00101 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970
+ (1 row)
+
+ -- aggregate
+ SELECT COUNT(*) FROM ft1 t1;
+ count
+ -------
+ 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
+ ----+----+-------+------------------------------+--------------------------
+ 1 | 1 | 00001 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970
+ 2 | 2 | 00002 | Sat Jan 03 00:00:00 1970 PST | Sat Jan 03 00:00:00 1970
+ 3 | 3 | 00003 | Sun Jan 04 00:00:00 1970 PST | Sun Jan 04 00:00:00 1970
+ 4 | 4 | 00004 | Mon Jan 05 00:00:00 1970 PST | Mon Jan 05 00:00:00 1970
+ 5 | 5 | 00005 | Tue Jan 06 00:00:00 1970 PST | Tue Jan 06 00:00:00 1970
+ 6 | 6 | 00006 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970
+ 7 | 7 | 00007 | Thu Jan 08 00:00:00 1970 PST | Thu Jan 08 00:00:00 1970
+ 8 | 8 | 00008 | Fri Jan 09 00:00:00 1970 PST | Fri Jan 09 00:00:00 1970
+ 9 | 9 | 00009 | Sat Jan 10 00:00:00 1970 PST | Sat Jan 10 00:00:00 1970
+ 10 | 0 | 00010 | Sun Jan 11 00:00:00 1970 PST | Sun Jan 11 00:00:00 1970
+ (10 rows)
+
+ -- subquery+MAX
+ SELECT * FROM ft1 t1 WHERE t1.c3 = (SELECT MAX(c3) FROM ft2 t2) ORDER BY c1;
+ c1 | c2 | c3 | c4 | c5
+ ------+----+-------+------------------------------+--------------------------
+ 1000 | 0 | 01000 | Thu Jan 01 00:00:00 1970 PST | Thu Jan 01 00:00:00 1970
+ (1 row)
+
+ -- used in CTE
+ WITH t1 AS (SELECT * FROM ft1 WHERE c1 <= 10) SELECT t2.c1, t2.c2, t2.c3, t2.c4 FROM t1, ft2 t2 WHERE t1.c1 = t2.c1 ORDER BY t1.c1;
+ c1 | c2 | c3 | c4
+ ----+----+-------+------------------------------
+ 1 | 1 | 00001 | Fri Jan 02 00:00:00 1970 PST
+ 2 | 2 | 00002 | Sat Jan 03 00:00:00 1970 PST
+ 3 | 3 | 00003 | Sun Jan 04 00:00:00 1970 PST
+ 4 | 4 | 00004 | Mon Jan 05 00:00:00 1970 PST
+ 5 | 5 | 00005 | Tue Jan 06 00:00:00 1970 PST
+ 6 | 6 | 00006 | Wed Jan 07 00:00:00 1970 PST
+ 7 | 7 | 00007 | Thu Jan 08 00:00:00 1970 PST
+ 8 | 8 | 00008 | Fri Jan 09 00:00:00 1970 PST
+ 9 | 9 | 00009 | Sat Jan 10 00:00:00 1970 PST
+ 10 | 0 | 00010 | Sun Jan 11 00:00:00 1970 PST
+ (10 rows)
+
+ -- fixed values
+ SELECT 'fixed', NULL FROM ft1 t1 WHERE c1 = 1;
+ ?column? | ?column?
+ ----------+----------
+ fixed |
+ (1 row)
+
+ -- ===================================================================
+ -- parameterized queries
+ -- ===================================================================
+ -- simple join
+ PREPARE st1(int, int) AS SELECT t1.c3, t2.c3 FROM ft1 t1, ft2 t2 WHERE t1.c1 = $1 AND t2.c1 = $2;
+ EXPLAIN (COSTS false) EXECUTE st1(1, 2);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------
+ Nested Loop
+ -> Foreign Scan on ft1 t1
+ Remote SQL: SELECT NULL, NULL, c3, NULL, NULL FROM "S 1"."T 1" WHERE ("C 1" = 1)
+ -> Foreign Scan on ft2 t2
+ Remote SQL: SELECT NULL, NULL, c3, NULL, NULL FROM "S 1"."T 1" WHERE ("C 1" = 2)
+ (5 rows)
+
+ EXECUTE st1(1, 1);
+ c3 | c3
+ -------+-------
+ 00001 | 00001
+ (1 row)
+
+ EXECUTE st1(101, 101);
+ c3 | c3
+ -------+-------
+ 00101 | 00101
+ (1 row)
+
+ -- subquery using stable function (can't be pushed down)
+ PREPARE st2(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c4) = 6) ORDER BY c1;
+ EXPLAIN (COSTS false) EXECUTE st2(10, 20);
+ QUERY PLAN
+ ---------------------------------------------------------------------------------------------------------------------------------------------------
+ Sort
+ Sort Key: t1.c1
+ -> Nested Loop
+ Join Filter: (t1.c3 = t2.c3)
+ -> HashAggregate
+ -> Foreign Scan on ft2 t2
+ Filter: (date_part('dow'::text, c4) = 6::double precision)
+ Remote SQL: DECLARE pgsql_fdw_cursor_6 SCROLL CURSOR FOR SELECT NULL, NULL, c3, c4, NULL FROM "S 1"."T 1" WHERE ("C 1" > 10)
+ -> Foreign Scan on ft1 t1
+ Remote SQL: SELECT "C 1", c2, c3, c4, c5 FROM "S 1"."T 1" WHERE ("C 1" < 20)
+ (10 rows)
+
+ EXECUTE st2(10, 20);
+ c1 | c2 | c3 | c4 | c5
+ ----+----+-------+------------------------------+--------------------------
+ 16 | 6 | 00016 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970
+ (1 row)
+
+ EXECUTE st1(101, 101);
+ c3 | c3
+ -------+-------
+ 00101 | 00101
+ (1 row)
+
+ -- subquery using immutable function (can be pushed down)
+ PREPARE st3(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c5) = 6) ORDER BY c1;
+ EXPLAIN (COSTS false) EXECUTE st3(10, 20);
+ QUERY PLAN
+ -----------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Sort
+ Sort Key: t1.c1
+ -> Hash Join
+ Hash Cond: (t1.c3 = t2.c3)
+ -> Foreign Scan on ft1 t1
+ Remote SQL: SELECT "C 1", c2, c3, c4, c5 FROM "S 1"."T 1" WHERE ("C 1" < 20)
+ -> Hash
+ -> HashAggregate
+ -> Foreign Scan on ft2 t2
+ Remote SQL: SELECT NULL, NULL, c3, NULL, NULL FROM "S 1"."T 1" WHERE (("C 1" > 10) AND (date_part('dow'::text, c5) = 6::double precision))
+ (10 rows)
+
+ EXECUTE st3(10, 20);
+ c1 | c2 | c3 | c4 | c5
+ ----+----+-------+------------------------------+--------------------------
+ 16 | 6 | 00016 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970
+ (1 row)
+
+ EXECUTE st3(20, 30);
+ c1 | c2 | c3 | c4 | c5
+ ----+----+-------+------------------------------+--------------------------
+ 23 | 3 | 00023 | Sat Jan 24 00:00:00 1970 PST | Sat Jan 24 00:00:00 1970
+ (1 row)
+
+ -- cleanup
+ DEALLOCATE st1;
+ DEALLOCATE st2;
+ DEALLOCATE st3;
+ -- ===================================================================
+ -- connection management
+ -- ===================================================================
+ SELECT srvname, usename FROM pgsql_fdw_connections;
+ srvname | usename
+ -----------+----------
+ loopback2 | postgres
+ (1 row)
+
+ SELECT pgsql_fdw_disconnect(srvid, usesysid) FROM pgsql_fdw_get_connections();
+ pgsql_fdw_disconnect
+ ----------------------
+ OK
+ (1 row)
+
+ SELECT srvname, usename FROM pgsql_fdw_connections;
+ srvname | usename
+ ---------+---------
+ (0 rows)
+
+ -- ===================================================================
+ -- cleanup
+ -- ===================================================================
+ DROP EXTENSION pgsql_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 server loopback2
+ drop cascades to user mapping for public
+ drop cascades to foreign table ft1
+ drop cascades to foreign table ft2
diff --git a/contrib/pgsql_fdw/option.c b/contrib/pgsql_fdw/option.c
index ...a62b2ab .
*** a/contrib/pgsql_fdw/option.c
--- b/contrib/pgsql_fdw/option.c
***************
*** 0 ****
--- 1,270 ----
+ /*-------------------------------------------------------------------------
+ *
+ * option.c
+ * FDW option handling
+ *
+ * Copyright (c) 2011, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/option.c
+ *
+ *-------------------------------------------------------------------------
+ */
+ #include "postgres.h"
+
+ #include "access/reloptions.h"
+ #include "catalog/pg_foreign_data_wrapper.h"
+ #include "catalog/pg_foreign_server.h"
+ #include "catalog/pg_foreign_table.h"
+ #include "catalog/pg_user_mapping.h"
+ #include "fmgr.h"
+ #include "foreign/foreign.h"
+ #include "lib/stringinfo.h"
+ #include "miscadmin.h"
+
+ #include "pgsql_fdw.h"
+
+ /*
+ * SQL functions
+ */
+ extern Datum pgsql_fdw_validator(PG_FUNCTION_ARGS);
+ PG_FUNCTION_INFO_V1(pgsql_fdw_validator);
+
+ /*
+ * Describes the valid options for objects that use this wrapper.
+ */
+ typedef struct PgsqlFdwOption
+ {
+ const char *optname;
+ Oid optcontext; /* Oid of catalog in which options may appear */
+ bool is_libpq_opt; /* true if it's used in libpq */
+ } PgsqlFdwOption;
+
+ /*
+ * Valid options for pgsql_fdw.
+ */
+ static PgsqlFdwOption valid_options[] = {
+
+ /*
+ * Options for libpq connection.
+ * Note: This list should be updated along with PQconninfoOptions in
+ * interfaces/libpq/fe-connect.c, so the order is kept as is.
+ *
+ * Some useless libpq connection options are not accepted by pgsql_fdw:
+ * client_encoding: set to local database encoding automatically
+ * fallback_application_name: fixed to "pgsql_fdw"
+ * replication: pgsql_fdw never be replication client
+ */
+ {"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},
+ #ifdef NOT_USED
+ {"client_encoding", ForeignServerRelationId, true},
+ #endif
+ {"tty", ForeignServerRelationId, true},
+ {"options", ForeignServerRelationId, true},
+ {"application_name", ForeignServerRelationId, true},
+ #ifdef NOT_USED
+ {"fallback_application_name", ForeignServerRelationId, true},
+ #endif
+ {"keepalives", ForeignServerRelationId, true},
+ {"keepalives_idle", ForeignServerRelationId, true},
+ {"keepalives_interval", ForeignServerRelationId, true},
+ {"keepalives_count", ForeignServerRelationId, true},
+ #ifdef USE_SSL
+ {"requiressl", ForeignServerRelationId, true},
+ #endif
+ {"sslmode", ForeignServerRelationId, true},
+ {"sslcert", ForeignServerRelationId, true},
+ {"sslkey", ForeignServerRelationId, true},
+ {"sslrootcert", ForeignServerRelationId, true},
+ {"sslcrl", ForeignServerRelationId, true},
+ {"requirepeer", ForeignServerRelationId, true},
+ #if defined(KRB5) || defined(ENABLE_GSS) || defined(ENABLE_SSPI)
+ {"krbsrvname", ForeignServerRelationId, true},
+ #endif
+ #if defined(ENABLE_GSS) && defined(ENABLE_SSPI)
+ {"gsslib", ForeignServerRelationId, true},
+ #endif
+ #ifdef NOT_USED
+ {"replication", ForeignServerRelationId, true},
+ #endif
+
+ /*
+ * Options for translation of object names.
+ */
+ {"nspname", ForeignTableRelationId, false},
+ {"relname", ForeignTableRelationId, false},
+ {"colname", AttributeRelationId, false},
+
+ /*
+ * Options for cursor behavior.
+ * These options can be overridden by smaller objects.
+ */
+ {"min_cursor_rows", ForeignTableRelationId, false},
+ {"min_cursor_rows", ForeignServerRelationId, false},
+ {"fetch_count", ForeignTableRelationId, false},
+ {"fetch_count", ForeignServerRelationId, false},
+
+ /* Terminating entry --- MUST BE LAST */
+ {NULL, InvalidOid, false}
+ };
+
+ /*
+ * Helper functions
+ */
+ static bool is_valid_option(const char *optname, Oid context);
+
+ /*
+ * Validate the generic options given to a FOREIGN DATA WRAPPER, SERVER,
+ * USER MAPPING or FOREIGN TABLE that uses pgsql_fdw.
+ *
+ * Raise an ERROR if the option or its value is considered invalid.
+ */
+ Datum
+ pgsql_fdw_validator(PG_FUNCTION_ARGS)
+ {
+ List *options_list = untransformRelOptions(PG_GETARG_DATUM(0));
+ Oid catalog = PG_GETARG_OID(1);
+ ListCell *cell;
+
+ /*
+ * Check that only options supported by pgsql_fdw, and allowed for the
+ * current object type, are given.
+ */
+ foreach(cell, options_list)
+ {
+ DefElem *def = (DefElem *) lfirst(cell);
+
+ if (!is_valid_option(def->defname, catalog))
+ {
+ PgsqlFdwOption *opt;
+ StringInfoData buf;
+
+ /*
+ * Unknown option specified, complain about it. Provide a hint
+ * with list of valid options for the object.
+ */
+ initStringInfo(&buf);
+ for (opt = valid_options; opt->optname; opt++)
+ {
+ if (catalog == opt->optcontext)
+ appendStringInfo(&buf, "%s%s", (buf.len > 0) ? ", " : "",
+ opt->optname);
+ }
+
+ ereport(ERROR,
+ (errcode(ERRCODE_FDW_INVALID_OPTION_NAME),
+ errmsg("invalid option \"%s\"", def->defname),
+ errhint("Valid options in this context are: %s",
+ buf.data)));
+ }
+
+ /* min_cursor_rows be zero or positive digit number. */
+ if (strcmp(def->defname, "min_cursor_rows") == 0)
+ {
+ long value;
+ char *p = NULL;
+
+ value = strtol(strVal(def->arg), &p, 10);
+ if (*p != '\0' || value < 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_FDW_INVALID_ATTRIBUTE_VALUE),
+ errmsg("invalid value for %s: \"%s\"",
+ def->defname, strVal(def->arg))));
+ }
+
+ /* fetch_count be positive digit number. */
+ if (strcmp(def->defname, "fetch_count") == 0)
+ {
+ long value;
+ char *p = NULL;
+
+ value = strtol(strVal(def->arg), &p, 10);
+ if (*p != '\0' || value < 1)
+ ereport(ERROR,
+ (errcode(ERRCODE_FDW_INVALID_ATTRIBUTE_VALUE),
+ errmsg("invalid value for %s: \"%s\"",
+ def->defname, strVal(def->arg))));
+ }
+ }
+
+ /*
+ * We don't care option-specific limitation here; they will be validated at
+ * the execution time.
+ */
+
+ PG_RETURN_VOID();
+ }
+
+ /*
+ * Check if the provided option is one of the valid options.
+ * context is the Oid of the catalog holding the object the option is for.
+ */
+ static bool
+ is_valid_option(const char *optname, Oid context)
+ {
+ PgsqlFdwOption *opt;
+
+ /*
+ * If the option was other than libpq option, look up option table and
+ * determine valid context.
+ */
+ for (opt = valid_options; opt->optname; opt++)
+ {
+ if (context == opt->optcontext && strcmp(opt->optname, optname) == 0)
+ return true;
+ }
+ return false;
+ }
+
+ /*
+ * Check if the provided option is one of the valid options.
+ * context is the Oid of the catalog holding the object the option is for.
+ */
+ static bool
+ is_libpq_option(const char *optname)
+ {
+ PgsqlFdwOption *opt;
+
+ /*
+ * If the option was other than libpq option, look up option table and
+ * determine valid context.
+ */
+ for (opt = valid_options; opt->optname; opt++)
+ {
+ if (strcmp(opt->optname, optname) == 0 && opt->is_libpq_opt)
+ return true;
+ }
+ return false;
+ }
+
+ /*
+ * Generate key-value arrays which includes only libpq options from the list
+ * which contains any kind of options.
+ */
+ int
+ ExtractConnectionOptions(List *defelems, const char **keywords, const char **values)
+ {
+ ListCell *lc;
+ int i;
+
+ i = 0;
+ foreach(lc, defelems)
+ {
+ DefElem *d = (DefElem *) lfirst(lc);
+ if (is_libpq_option(d->defname))
+ {
+ keywords[i] = d->defname;
+ values[i] = strVal(d->arg);
+ i++;
+ }
+ }
+ return i;
+ }
diff --git a/contrib/pgsql_fdw/pgsql_fdw--1.0.sql b/contrib/pgsql_fdw/pgsql_fdw--1.0.sql
index ...87dfa29 .
*** a/contrib/pgsql_fdw/pgsql_fdw--1.0.sql
--- b/contrib/pgsql_fdw/pgsql_fdw--1.0.sql
***************
*** 0 ****
--- 1,36 ----
+ /* contrib/pgsql_fdw/pgsql_fdw--1.0.sql */
+
+ CREATE FUNCTION pgsql_fdw_handler()
+ RETURNS fdw_handler
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C STRICT;
+
+ CREATE FUNCTION pgsql_fdw_validator(text[], oid)
+ RETURNS void
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C STRICT;
+
+ CREATE FOREIGN DATA WRAPPER pgsql_fdw
+ HANDLER pgsql_fdw_handler
+ VALIDATOR pgsql_fdw_validator;
+
+ /* connection management functions and view */
+ CREATE FUNCTION pgsql_fdw_get_connections(out srvid oid, out usesysid oid)
+ RETURNS SETOF record
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C STRICT;
+
+ CREATE FUNCTION pgsql_fdw_disconnect(oid, oid)
+ RETURNS text
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C STRICT;
+
+ CREATE VIEW pgsql_fdw_connections AS
+ SELECT c.srvid srvid,
+ s.srvname srvname,
+ c.usesysid usesysid,
+ pg_get_userbyid(c.usesysid) usename
+ FROM pgsql_fdw_get_connections() c
+ JOIN pg_catalog.pg_foreign_server s ON (s.oid = c.srvid);
+ GRANT SELECT ON pgsql_fdw_connections TO public;
+
diff --git a/contrib/pgsql_fdw/pgsql_fdw.c b/contrib/pgsql_fdw/pgsql_fdw.c
index ...e9dfa74 .
*** a/contrib/pgsql_fdw/pgsql_fdw.c
--- b/contrib/pgsql_fdw/pgsql_fdw.c
***************
*** 0 ****
--- 1,882 ----
+ /*-------------------------------------------------------------------------
+ *
+ * pgsql_fdw.c
+ * foreign-data wrapper for remote PostgreSQL servers.
+ *
+ * Copyright (c) 2011, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/pgsql_fdw.c
+ *
+ *-------------------------------------------------------------------------
+ */
+ #include "postgres.h"
+ #include "fmgr.h"
+
+ #include "catalog/pg_foreign_server.h"
+ #include "catalog/pg_foreign_table.h"
+ #include "commands/explain.h"
+ #include "foreign/fdwapi.h"
+ #include "funcapi.h"
+ #include "miscadmin.h"
+ #include "optimizer/cost.h"
+ #include "utils/lsyscache.h"
+ #include "utils/memutils.h"
+ #include "utils/rel.h"
+
+ #include "pgsql_fdw.h"
+ #include "connection.h"
+
+ PG_MODULE_MAGIC;
+
+ /*
+ * Default fetch count for cursor mode. This can be override by fetch_count
+ * FDW option.
+ */
+ #define DEFAULT_FETCH_COUNT 1000
+
+ /*
+ * Default minimum estimated row count for cursor mode. This can be override
+ * by min_cursor_rows FDW option.
+ * */
+ #define DEFAULT_MIN_CURSOR_ROWS 1000
+
+ /*
+ * Cost to establish a connection.
+ * XXX: should be configurable per server?
+ */
+ #define CONNECTION_COSTS 100.0
+
+ /*
+ * Cost to transfer 1 byte from remote server.
+ * XXX: should be configurable per server?
+ */
+ #define TRANSFER_COSTS_PER_BYTE 0.001
+
+ /*
+ * Cursors which are used together in a local query require different name, so
+ * we use simple incremental name for that purpose. We don't care wrap around
+ * of cursor_id because it's hard to imagine that 2^32 cursors are used in a
+ * query.
+ */
+ #define CURSOR_NAME_FORMAT "pgsql_fdw_cursor_%u"
+ static uint32 cursor_id = 0;
+
+ /*
+ * Index of FDW-private items stored in FdwPlan.
+ */
+ enum FdwPrivateIndex {
+ FdwPrivateSelectSql,
+
+ /* Items for cursor mode */
+ FdwPrivateDeclareSql,
+ FdwPrivateFetchSql,
+ FdwPrivateResetSql,
+ FdwPrivateCloseSql,
+
+ /* # of elements stored in the list fdw_private */
+ FdwPrivateNum,
+ };
+
+ /*
+ * This macro can be used in the executor to determine whether the scan is
+ * using CURSOR or not.
+ */
+ #define USE_CURSOR(fdwplan) \
+ (list_length((fdwplan)->fdw_private) > FdwPrivateDeclareSql)
+
+ /*
+ * Describes an execution state of a foreign scan against a foreign table
+ * using pgsql_fdw.
+ */
+ typedef struct PgsqlFdwExecutionState
+ {
+ FdwPlan *fdwplan; /* FDW-specific planning information */
+ PGconn *conn; /* connection for the scan */
+
+ Oid *param_types; /* type array of external parameter */
+ const char **param_values; /* value array of external parameter */
+
+ int attnum; /* # of non-dropped attribute */
+ char **col_values; /* column value buffer */
+ AttInMetadata *attinmeta; /* attribute metadata */
+
+ Tuplestorestate *tuples; /* result of the scan, partial in cursor mode */
+ bool cursor_opened; /* true if cursor is opened */
+ } PgsqlFdwExecutionState;
+
+ /*
+ * SQL functions
+ */
+ extern Datum pgsql_fdw_handler(PG_FUNCTION_ARGS);
+ PG_FUNCTION_INFO_V1(pgsql_fdw_handler);
+
+ /*
+ * FDW callback routines
+ */
+ static FdwPlan *pgsqlPlanForeignScan(Oid foreigntableid,
+ PlannerInfo *root,
+ RelOptInfo *baserel);
+ static void pgsqlExplainForeignScan(ForeignScanState *node, ExplainState *es);
+ static void pgsqlBeginForeignScan(ForeignScanState *node, int eflags);
+ static TupleTableSlot *pgsqlIterateForeignScan(ForeignScanState *node);
+ static void pgsqlReScanForeignScan(ForeignScanState *node);
+ static void pgsqlEndForeignScan(ForeignScanState *node);
+
+ /*
+ * Helper functions
+ */
+ static void estimate_costs(PlannerInfo *root,
+ RelOptInfo *baserel,
+ const char *sql,
+ Oid serverid,
+ Cost *startup_cost,
+ Cost *total_cost);
+ static void execute_query(ForeignScanState *node);
+ static PGresult *fetch_result(ForeignScanState *node);
+ static void store_result(ForeignScanState *node, PGresult *res);
+
+ /*
+ * Foreign-data wrapper handler function: return a struct with pointers
+ * to my callback routines.
+ */
+ Datum
+ pgsql_fdw_handler(PG_FUNCTION_ARGS)
+ {
+ FdwRoutine *fdwroutine = makeNode(FdwRoutine);
+
+ fdwroutine->PlanForeignScan = pgsqlPlanForeignScan;
+ fdwroutine->ExplainForeignScan = pgsqlExplainForeignScan;
+ fdwroutine->BeginForeignScan = pgsqlBeginForeignScan;
+ fdwroutine->IterateForeignScan = pgsqlIterateForeignScan;
+ fdwroutine->ReScanForeignScan = pgsqlReScanForeignScan;
+ fdwroutine->EndForeignScan = pgsqlEndForeignScan;
+
+ PG_RETURN_POINTER(fdwroutine);
+ }
+
+ /*
+ * pgsqlPlanForeignScan
+ * Create a FdwPlan for a scan on the foreign table
+ */
+ static FdwPlan *
+ pgsqlPlanForeignScan(Oid foreigntableid,
+ PlannerInfo *root,
+ RelOptInfo *baserel)
+ {
+ char *sql;
+ FdwPlan *fdwplan;
+ List *fdw_private = NIL;
+ const char *min_cursor_rows_str;
+ int min_cursor_rows = DEFAULT_MIN_CURSOR_ROWS;
+ ForeignTable *table;
+ ForeignServer *server;
+
+ /* Construct FdwPlan with cost estimates */
+ fdwplan = makeNode(FdwPlan);
+ sql = deparseSql(foreigntableid, root, baserel);
+ table = GetForeignTable(foreigntableid);
+ server = GetForeignServer(table->serverid);
+ estimate_costs(root, baserel, sql, server->serverid,
+ &fdwplan->startup_cost, &fdwplan->total_cost);
+
+ /*
+ * Store plain SELECT statement in private area of FdwPlan. This will be
+ * used for executing remote query and explaining scan.
+ */
+ fdw_private = list_make1(makeString(sql));
+
+ /*
+ * We use SQL-level cursor when the result seems to be large, to limit
+ * memory usage for the result set. Exceptionally we use simple SELECT if
+ * min_cursor_rows is set to 0.
+ */
+ min_cursor_rows_str = GetFdwOptionValue(foreigntableid,
+ InvalidAttrNumber,
+ "min_cursor_rows");
+ if (min_cursor_rows_str != NULL)
+ min_cursor_rows = strtol(min_cursor_rows_str, NULL, 10);
+
+ if (min_cursor_rows != 0 && baserel->rows >= min_cursor_rows)
+ {
+ char name[128]; /* must be larger than format + 10 */
+ StringInfoData cursor;
+ const char *fetch_count_str;
+ int fetch_count = DEFAULT_FETCH_COUNT;
+
+ /* Use specified fetch_count instead of default value, if any. */
+ fetch_count_str = GetFdwOptionValue(foreigntableid,
+ InvalidAttrNumber,
+ "fetch_count");
+ if (fetch_count_str != NULL)
+ fetch_count = strtol(fetch_count_str, NULL, 10);
+ elog(DEBUG1,
+ "relid=%u fetch_count=%d",
+ foreigntableid,
+ fetch_count);
+
+ /* We store some more information in FdwPlan to pass them beyond the
+ * boundary between planner and executor. Finally FdwPlan using cursor
+ * would hold items below:
+ *
+ * 1) plain SELECT statement (already added above)
+ * 2) SQL statement used to declare cursor
+ * 3) SQL statement used to fetch rows from cursor
+ * 4) SQL statement used to reset cursor
+ * 5) SQL statement used to close cursor
+ *
+ * These items are indexed with the enum FdwPrivateIndex, so an item
+ * can be accessed directly via list_nth(). For example of FETCH
+ * statement:
+ * list_nth(fdw_private, FdwPrivateFetchSql)
+ */
+
+ /* Construct cursor name from sequential value */
+ sprintf(name, CURSOR_NAME_FORMAT, cursor_id++);
+
+ /* Construct statement to declare cursor */
+ initStringInfo(&cursor);
+ appendStringInfo(&cursor, "DECLARE %s SCROLL CURSOR FOR %s", name, sql);
+ fdw_private = lappend(fdw_private, makeString(cursor.data));
+
+ /* Construct statement to fetch rows from cursor */
+ initStringInfo(&cursor);
+ appendStringInfo(&cursor, "FETCH %d FROM %s", fetch_count, name);
+ fdw_private = lappend(fdw_private, makeString(cursor.data));
+
+ /* Construct statement to reset cursor */
+ initStringInfo(&cursor);
+ appendStringInfo(&cursor, "MOVE ABSOLUTE 0 FROM %s", name);
+ fdw_private = lappend(fdw_private, makeString(cursor.data));
+
+ /* Construct statement to close cursor */
+ initStringInfo(&cursor);
+ appendStringInfo(&cursor, "CLOSE %s", name);
+ fdw_private = lappend(fdw_private, makeString(cursor.data));
+ }
+
+ /* Store FDW private information into FdwPlan */
+ fdwplan->fdw_private = fdw_private;
+
+ return fdwplan;
+ }
+
+ /*
+ * pgsqlExplainForeignScan
+ * Produce extra output for EXPLAIN
+ */
+ static void
+ pgsqlExplainForeignScan(ForeignScanState *node, ExplainState *es)
+ {
+ FdwPlan *fdwplan;
+ char *sql;
+
+ fdwplan = ((ForeignScan *) node->ss.ps.plan)->fdwplan;
+ if (USE_CURSOR(fdwplan))
+ sql = strVal(list_nth(fdwplan->fdw_private, FdwPrivateDeclareSql));
+ else
+ sql = strVal(list_nth(fdwplan->fdw_private, FdwPrivateSelectSql));
+ ExplainPropertyText("Remote SQL", sql, es);
+ }
+
+ /*
+ * pgsqlBeginForeignScan
+ * Initiate access to a foreign PostgreSQL table.
+ */
+ static void
+ pgsqlBeginForeignScan(ForeignScanState *node, int eflags)
+ {
+ PgsqlFdwExecutionState *festate;
+ PGconn *conn;
+ Oid relid;
+ ForeignTable *table;
+ ForeignServer *server;
+ UserMapping *user;
+ TupleTableSlot *slot = node->ss.ss_ScanTupleSlot;
+
+ /*
+ * Do nothing in EXPLAIN (no ANALYZE) case. node->fdw_state stays NULL.
+ */
+ if (eflags & EXEC_FLAG_EXPLAIN_ONLY)
+ return;
+
+ /*
+ * Save state in node->fdw_state.
+ */
+ festate = (PgsqlFdwExecutionState *) palloc(sizeof(PgsqlFdwExecutionState));
+ festate->fdwplan = ((ForeignScan *) node->ss.ps.plan)->fdwplan;
+
+ /*
+ * Get connection to the foreign server. Connection manager would
+ * establish new connection if necessary.
+ */
+ relid = RelationGetRelid(node->ss.ss_currentRelation);
+ table = GetForeignTable(relid);
+ server = GetForeignServer(table->serverid);
+ user = GetUserMapping(GetOuterUserId(), server->serverid);
+ conn = GetConnection(server, user);
+ festate->conn = conn;
+
+ /* Result will be filled in first Iterate call. */
+ festate->tuples = NULL;
+ festate->cursor_opened = false;
+
+ /* Allocate buffers for column values. */
+ {
+ TupleDesc tupdesc = slot->tts_tupleDescriptor;
+ festate->col_values = palloc(sizeof(char *) * tupdesc->natts);
+ festate->attinmeta = TupleDescGetAttInMetadata(tupdesc);
+ }
+
+ /* Allocate buffers for query parameters. */
+ {
+ ParamListInfo params = node->ss.ps.state->es_param_list_info;
+ int numParams = params ? params->numParams : 0;
+
+ if (numParams > 0)
+ {
+ festate->param_types = palloc0(sizeof(Oid) * numParams);
+ festate->param_values = palloc0(sizeof(char *) * numParams);
+ }
+ else
+ {
+ festate->param_types = NULL;
+ festate->param_values = NULL;
+ }
+ }
+
+
+ /* Store FDW-specific state into ForeignScanState */
+ node->fdw_state = (void *) festate;
+
+ return;
+ }
+
+ /*
+ * pgsqlIterateForeignScan
+ * Retrieve next row from the result set, or clear tuple slot to indicate
+ * EOF.
+ *
+ * Note that using per-query context when retrieving tuples from
+ * tuplestore to ensure that returned tuples can survive until next
+ * iteration because the tuple is released implicitly via ExecClearTuple.
+ * Retrieving a tuple from tuplestore in CurrentMemoryContext (it's a
+ * per-tuple context), ExecClearTuple will free dangling pointer.
+ */
+ static TupleTableSlot *
+ pgsqlIterateForeignScan(ForeignScanState *node)
+ {
+ PgsqlFdwExecutionState *festate;
+ TupleTableSlot *slot = node->ss.ss_ScanTupleSlot;
+ PGresult *res;
+ MemoryContext oldcontext = CurrentMemoryContext;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+
+
+ /*
+ * If this is the first call after Begin, we need to execute remote query.
+ * If the query needs cursor, we declare a cursor at first call and fetch
+ * from it in later calls.
+ */
+ if (festate->tuples == NULL && !festate->cursor_opened)
+ execute_query(node);
+
+ /*
+ * If enough tuples are left in tuplestore, just return next tuple from it.
+ */
+ MemoryContextSwitchTo(node->ss.ps.state->es_query_cxt);
+ if (tuplestore_gettupleslot(festate->tuples, true, false, slot))
+ {
+ MemoryContextSwitchTo(oldcontext);
+ return slot;
+ }
+ MemoryContextSwitchTo(oldcontext);
+
+ /* If the scan doesn't use cursor, the scan has been done. */
+ if (!USE_CURSOR(festate->fdwplan))
+ {
+ ExecClearTuple(slot);
+ return slot;
+ }
+
+ /*
+ * Here we need to clear partial result and fetch next bunch of tuples from
+ * from the cursor for the scan. If the fetch returns no tuple, the scan
+ * has reached the end.
+ */
+ res = fetch_result(node);
+ PG_TRY();
+ {
+ store_result(node, res);
+ PQclear(res);
+ res = NULL;
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+
+ /*
+ * If we got more tuples from the server cursor, return next tuple from
+ * tuplestore.
+ */
+ MemoryContextSwitchTo(node->ss.ps.state->es_query_cxt);
+ if (tuplestore_gettupleslot(festate->tuples, true, false, slot))
+ {
+ MemoryContextSwitchTo(oldcontext);
+ return slot;
+ }
+ MemoryContextSwitchTo(oldcontext);
+
+ /* We don't have any result even in remote server cursor. */
+ ExecClearTuple(slot);
+ return slot;
+ }
+
+ /*
+ * pgsqlReScanForeignScan
+ * - Restart this scan by resetting fetch location.
+ */
+ static void
+ pgsqlReScanForeignScan(ForeignScanState *node)
+ {
+ List *fdw_private;
+ char *sql;
+ PGconn *conn;
+ PGresult *res;
+ PgsqlFdwExecutionState *festate;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+
+ /* Discard fetch results if any. */
+ if (festate->tuples != NULL)
+ {
+ tuplestore_end(festate->tuples);
+ festate->tuples = NULL;
+ }
+
+ /* Reset cursor */
+ if (USE_CURSOR(festate->fdwplan))
+ {
+ fdw_private = festate->fdwplan->fdw_private;
+ conn = festate->conn;
+ sql = strVal(list_nth(fdw_private, FdwPrivateResetSql));
+ res = PQexec(conn, sql);
+ PG_TRY();
+ {
+ if (res == NULL || PQresultStatus(res) != PGRES_COMMAND_OK)
+ {
+ ereport(ERROR,
+ (errmsg("could not rewind cursor"),
+ errdetail("%s", PQerrorMessage(conn)),
+ errhint("%s", sql)));
+ }
+ PQclear(res);
+ res = NULL;
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+ }
+ }
+
+ /*
+ * pgsqlEndForeignScan
+ * Finish scanning foreign table and dispose objects used for this scan
+ */
+ static void
+ pgsqlEndForeignScan(ForeignScanState *node)
+ {
+ List *fdw_private;
+ char *sql;
+ PGconn *conn;
+ PGresult *res;
+ PgsqlFdwExecutionState *festate;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+
+ /* if festate is NULL, we are in EXPLAIN; nothing to do */
+ if (festate == NULL)
+ return;
+
+ /* Discard fetch results */
+ if (festate->tuples != NULL)
+ {
+ tuplestore_end(festate->tuples);
+ festate->tuples = NULL;
+ }
+
+ /* Close cursor */
+ if (USE_CURSOR(festate->fdwplan) && festate->cursor_opened)
+ {
+ fdw_private = festate->fdwplan->fdw_private;
+ conn = festate->conn;
+ sql = strVal(list_nth(fdw_private, FdwPrivateCloseSql));
+ res = PQexec(conn, sql);
+ PG_TRY();
+ {
+ if (res == NULL || PQresultStatus(res) != PGRES_COMMAND_OK)
+ {
+ ereport(ERROR,
+ (errmsg("could not close cursor"),
+ errdetail("%s", PQerrorMessage(conn)),
+ errhint("%s", sql)));
+ }
+ PQclear(res);
+ res = NULL;
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+ }
+
+ ReleaseConnection(festate->conn);
+ }
+
+ /*
+ * Estimate costs of scanning a foreign table.
+ */
+ static void
+ estimate_costs(PlannerInfo *root, RelOptInfo *baserel,
+ const char *sql, Oid serverid,
+ Cost *startup_cost, Cost *total_cost)
+ {
+ ForeignServer *server;
+ UserMapping *user;
+ PGconn *conn = NULL;
+ PGresult *res = NULL;
+ StringInfoData buf;
+ char *plan;
+ char *p;
+ char *endp;
+
+ /*
+ * Get connection to the foreign server. Connection manager would
+ * establish new connection if necessary.
+ */
+ server = GetForeignServer(serverid);
+ user = GetUserMapping(GetOuterUserId(), server->serverid);
+ conn = GetConnection(server, user);
+ initStringInfo(&buf);
+ appendStringInfo(&buf, "EXPLAIN (FORMAT YAML) %s", sql);
+
+ /* remove WHERE clause if the query uses parameter */
+ p = strchr(buf.data, '$');
+ if (p != NULL)
+ {
+ p = strstr(buf.data, "WHERE");
+ if (p != NULL)
+ *p = '\0';
+ }
+
+ PG_TRY();
+ {
+ res = PQexec(conn, buf.data);
+ if (res == NULL || PQresultStatus(res) != PGRES_TUPLES_OK)
+ {
+ char *msg;
+
+ msg = pstrdup(PQerrorMessage(conn));
+ PQclear(res);
+ res = NULL;
+ ereport(ERROR,
+ (errmsg("could not execute EXPLAIN for cost estimation"),
+ errdetail("%s", msg),
+ errhint("%s", sql)));
+ }
+ plan = pstrdup(PQgetvalue(res, 0, 0));
+ PQclear(res);
+ res = NULL;
+ ReleaseConnection(conn);
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+
+ /* extract startup cost from remote plan */
+ p = strstr(plan, "Startup Cost: ");
+ if (p != NULL)
+ {
+ p += strlen("Startup Cost: ");
+ *startup_cost = strtod(p, &endp);
+ if (*endp != '\n' && *endp != '\0')
+ elog(ERROR, "invalid plan format for startup cost%c", *endp);
+ }
+
+ /* extract total cost from remote plan */
+ p = strstr(plan, "Total Cost: ");
+ if (p != NULL)
+ {
+ p += strlen("Total Cost: ");
+ *total_cost = strtod(p, &endp);
+ if (*endp != '\n' && *endp != '\0')
+ elog(ERROR, "invalid plan format for total cost");
+ }
+
+ /* extract # of rows from remote plan */
+ p = strstr(plan, "Plan Rows: ");
+ if (p != NULL)
+ {
+ p += strlen("Plan Rows: ");
+ baserel->rows = strtod(p, &endp);
+ if (*endp != '\n' && *endp != '\0')
+ elog(ERROR, "invalid plan format for plan rows");
+ }
+
+ /* extract average width from remote plan */
+ p = strstr(plan, "Plan Width: ");
+ if (p != NULL)
+ {
+ p += strlen("Plan Width: ");
+ baserel->width = strtol(p, &endp, 10);
+ if (*endp != '\n' && *endp != '\0')
+ elog(ERROR, "invalid plan format for plan width");
+ }
+
+ /* TODO Selectivity of quals pushed down should be considered. */
+
+ /* add cost to establish connection. */
+ *startup_cost += CONNECTION_COSTS;
+ *total_cost += CONNECTION_COSTS;
+
+ /* add cost to transfer result. */
+ *total_cost += TRANSFER_COSTS_PER_BYTE * baserel->width * baserel->tuples;
+ *total_cost += cpu_tuple_cost * baserel->tuples;
+ }
+
+ /*
+ * Execute remote query with current parameters.
+ */
+ static void
+ execute_query(ForeignScanState *node)
+ {
+ FdwPlan *fdwplan;
+ PgsqlFdwExecutionState *festate;
+ ParamListInfo params = node->ss.ps.state->es_param_list_info;
+ int numParams = params ? params->numParams : 0;
+ Oid *types = NULL;
+ const char **values = NULL;
+ char *sql;
+ int required_result;
+ PGconn *conn;
+ PGresult *res;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+ types = festate->param_types;
+ values = festate->param_values;
+
+ /*
+ * Construct parameter array in text format. We don't release memory for
+ * the arrays explicitly, because the memory usage would not be very large,
+ * and anyway they will be released in context cleanup.
+ */
+ if (numParams > 0)
+ {
+ int i;
+
+ 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;
+
+ getTypeOutputInfo(types[i], &out_func_oid, &isvarlena);
+ fmgr_info(out_func_oid, &func);
+ values[i] = OutputFunctionCall(&func, params->params[i].value);
+ }
+ }
+ }
+
+ /*
+ * Execute remote query with parameters.
+ */
+ conn = festate->conn;
+ fdwplan = ((ForeignScan *) node->ss.ps.plan)->fdwplan;
+ if (USE_CURSOR(fdwplan))
+ {
+ sql = strVal(list_nth(fdwplan->fdw_private, FdwPrivateDeclareSql));
+ required_result = PGRES_COMMAND_OK;
+ }
+ else
+ {
+ sql = strVal(list_nth(fdwplan->fdw_private, FdwPrivateSelectSql));
+ required_result = PGRES_TUPLES_OK;
+ }
+ res = PQexecParams(conn, sql, numParams, types, values, NULL, NULL, 0);
+ PG_TRY();
+ {
+ /*
+ * If the query has failed, reporting details is enough here.
+ * Connection(s) which are used by this query (at least used by
+ * pgsql_fdw) will be cleaned up by the foreign connection manager.
+ */
+ if (res == NULL || PQresultStatus(res) != required_result)
+ {
+ ereport(ERROR,
+ (errmsg("could not execute foreign query"),
+ errdetail("%s", PQerrorMessage(conn)),
+ errhint("%s", sql)));
+ }
+
+ /* Fetch first bunch of result if we using cursor. */
+ if (USE_CURSOR(fdwplan))
+ {
+ /* Mark that this scan has opened a cursor. */
+ festate->cursor_opened = true;
+
+ /* Discard result of CURSOR statement and fetch first bunch. */
+ PQclear(res);
+ res = fetch_result(node);
+ }
+
+ /*
+ * Store the result of the query into tuplestore.
+ * We must release PGresult here to avoid memory leak.
+ */
+ store_result(node, res);
+ PQclear(res);
+ res = NULL;
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+ }
+
+ /*
+ * Fetch next partial result from remote server.
+ */
+ static PGresult *
+ fetch_result(ForeignScanState *node)
+ {
+ PgsqlFdwExecutionState *festate;
+ List *fdw_private;
+ char *sql;
+ PGconn *conn;
+ PGresult *res;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+
+ /* retrieve information for fetching result. */
+ fdw_private = festate->fdwplan->fdw_private;
+ sql = strVal(list_nth(fdw_private, FdwPrivateFetchSql));
+ conn = festate->conn;
+ res = PQexec(conn, sql);
+ if (res == NULL || PQresultStatus(res) != PGRES_TUPLES_OK)
+ {
+ ereport(ERROR,
+ (errmsg("could not fetch rows from foreign server"),
+ errdetail("%s", PQerrorMessage(conn)),
+ errhint("%s", sql)));
+ }
+
+ return res;
+ }
+
+ /*
+ * Create tuples from PGresult and store them into tuplestore.
+ */
+ static void
+ store_result(ForeignScanState *node, PGresult *res)
+ {
+ int rows;
+ int row;
+ int i;
+ int nfields;
+ int attnum; /* number of non-dropped columns */
+ Form_pg_attribute *attrs;
+ TupleTableSlot *slot = node->ss.ss_ScanTupleSlot;
+ TupleDesc tupdesc = slot->tts_tupleDescriptor;
+ PgsqlFdwExecutionState *festate;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+ rows = PQntuples(res);
+ nfields = PQnfields(res);
+ attrs = tupdesc->attrs;
+
+ /* First, ensure that the tuplestore is empty. */
+ if (festate->tuples == NULL)
+ {
+ MemoryContext oldcontext = CurrentMemoryContext;
+
+ /*
+ * Create tuplestore to store result of the query in per-query context.
+ * Note that we use this memory context to avoid memory leak in error
+ * cases.
+ */
+ MemoryContextSwitchTo(MessageContext);
+ festate->tuples = tuplestore_begin_heap(false, false, work_mem);
+ MemoryContextSwitchTo(oldcontext);
+ }
+ else
+ {
+ /* We already have tuplestore, just need to clear contents of it. */
+ tuplestore_clear(festate->tuples);
+ }
+
+
+ /* 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)));
+
+ /* put a tuples into the slot */
+ for (row = 0; row < rows; row++)
+ {
+ int j;
+ HeapTuple tuple;
+
+ for (i = 0, j = 0; i < tupdesc->natts; i++)
+ {
+ /* skip dropped columns. */
+ if (attrs[i]->attisdropped)
+ {
+ festate->col_values[i] = NULL;
+ continue;
+ }
+
+ if (PQgetisnull(res, row, j))
+ festate->col_values[i] = NULL;
+ else
+ festate->col_values[i] = PQgetvalue(res, row, 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(festate->attinmeta, festate->col_values);
+ tuplestore_puttuple(festate->tuples, tuple);
+ }
+
+ tuplestore_donestoring(festate->tuples);
+ }
diff --git a/contrib/pgsql_fdw/pgsql_fdw.control b/contrib/pgsql_fdw/pgsql_fdw.control
index ...0a9c8f4 .
*** a/contrib/pgsql_fdw/pgsql_fdw.control
--- b/contrib/pgsql_fdw/pgsql_fdw.control
***************
*** 0 ****
--- 1,5 ----
+ # pgsql_fdw extension
+ comment = 'foreign-data wrapper for remote PostgreSQL servers'
+ default_version = '1.0'
+ module_pathname = '$libdir/pgsql_fdw'
+ relocatable = true
diff --git a/contrib/pgsql_fdw/pgsql_fdw.h b/contrib/pgsql_fdw/pgsql_fdw.h
index ...fb49ffb .
*** a/contrib/pgsql_fdw/pgsql_fdw.h
--- b/contrib/pgsql_fdw/pgsql_fdw.h
***************
*** 0 ****
--- 1,28 ----
+ /*-------------------------------------------------------------------------
+ *
+ * pgsql_fdw.h
+ * foreign-data wrapper for remote PostgreSQL servers.
+ *
+ * Copyright (c) 2011, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/pgsql_fdw.h
+ *
+ *-------------------------------------------------------------------------
+ */
+
+ #ifndef PGSQL_FDW_H
+ #define PGSQL_FDW_H
+
+ #include "postgres.h"
+ #include "nodes/relation.h"
+
+ /* in option.c */
+ int ExtractConnectionOptions(List *defelems,
+ const char **keywords,
+ const char **values);
+
+ /* in deparse.c */
+ char *deparseSql(Oid relid, PlannerInfo *root, RelOptInfo *baserel);
+
+ #endif /* PGSQL_FDW_H */
diff --git a/contrib/pgsql_fdw/sql/pgsql_fdw.sql b/contrib/pgsql_fdw/sql/pgsql_fdw.sql
index ...c00de92 .
*** a/contrib/pgsql_fdw/sql/pgsql_fdw.sql
--- b/contrib/pgsql_fdw/sql/pgsql_fdw.sql
***************
*** 0 ****
--- 1,177 ----
+ -- ===================================================================
+ -- create FDW objects
+ -- ===================================================================
+
+ CREATE EXTENSION pgsql_fdw;
+
+ CREATE SERVER loopback1 FOREIGN DATA WRAPPER pgsql_fdw;
+ CREATE SERVER loopback2 FOREIGN DATA WRAPPER pgsql_fdw
+ OPTIONS (dbname 'contrib_regression');
+
+ CREATE USER MAPPING FOR public SERVER loopback1
+ OPTIONS (user 'value', password 'value');
+ CREATE USER MAPPING FOR public SERVER loopback2;
+
+ CREATE FOREIGN TABLE ft1 (
+ c1 int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp
+ ) SERVER loopback2;
+
+ CREATE FOREIGN TABLE ft2 (
+ c1 int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp
+ ) SERVER loopback2;
+
+ -- ===================================================================
+ -- create objects used through FDW
+ -- ===================================================================
+ CREATE SCHEMA "S 1";
+ CREATE TABLE "S 1"."T 1" (
+ "C 1" int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ CONSTRAINT t1_pkey PRIMARY KEY ("C 1")
+ );
+ CREATE TABLE "S 1"."T 2" (
+ c1 int NOT NULL,
+ c2 text,
+ CONSTRAINT t2_pkey PRIMARY KEY (c1)
+ );
+
+ BEGIN;
+ TRUNCATE "S 1"."T 1";
+ INSERT INTO "S 1"."T 1"
+ SELECT id,
+ id % 10,
+ to_char(id, 'FM00000'),
+ '1970-01-01'::timestamptz + ((id % 100) || ' days')::interval,
+ '1970-01-01'::timestamp + ((id % 100) || ' days')::interval
+ FROM generate_series(1, 1000) id;
+ TRUNCATE "S 1"."T 2";
+ INSERT INTO "S 1"."T 2"
+ SELECT id,
+ 'AAA' || to_char(id, 'FM000')
+ FROM generate_series(1, 100) id;
+ COMMIT;
+
+ -- ===================================================================
+ -- tests for pgsql_fdw_validator
+ -- ===================================================================
+ ALTER FOREIGN DATA WRAPPER pgsql_fdw OPTIONS (host 'value'); -- ERRROR
+ -- requiressl, krbsrvname and gsslib are omitted because they depend on
+ -- configure option
+ ALTER SERVER loopback1 OPTIONS (
+ authtype 'value',
+ service 'value',
+ connect_timeout 'value',
+ dbname 'value',
+ host 'value',
+ hostaddr 'value',
+ port 'value',
+ --client_encoding 'value',
+ tty 'value',
+ options 'value',
+ application_name 'value',
+ --fallback_application_name 'value',
+ keepalives 'value',
+ keepalives_idle 'value',
+ keepalives_interval 'value',
+ -- requiressl 'value',
+ sslmode 'value',
+ sslcert 'value',
+ sslkey 'value',
+ sslrootcert 'value',
+ sslcrl 'value'
+ --requirepeer 'value',
+ -- krbsrvname 'value',
+ -- gsslib 'value',
+ --replication 'value'
+ );
+ ALTER SERVER loopback1 OPTIONS (user 'value'); -- ERROR
+ ALTER SERVER loopback2 OPTIONS (ADD min_cursor_rows '0');
+ ALTER SERVER loopback2 OPTIONS (ADD fetch_count '2');
+ ALTER USER MAPPING FOR public SERVER loopback1
+ OPTIONS (DROP user, DROP password);
+ ALTER USER MAPPING FOR public SERVER loopback1
+ OPTIONS (host 'value'); -- ERROR
+ ALTER FOREIGN TABLE ft1 OPTIONS (nspname 'S 1', relname 'T 1');
+ ALTER FOREIGN TABLE ft2 OPTIONS (nspname 'S 1', relname 'T 1', min_cursor_rows '10', fetch_count '100');
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (invalid 'value'); -- ERROR
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (colname 'C 1');
+ ALTER FOREIGN TABLE ft2 ALTER COLUMN c1 OPTIONS (colname 'C 1');
+ ALTER FOREIGN TABLE ft1 OPTIONS (min_cursor_rows 'a'); -- ERROR
+ ALTER FOREIGN TABLE ft1 OPTIONS (min_cursor_rows '-1'); -- ERROR
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count 'a'); -- ERROR
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count '0'); -- ERROR
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count '-1'); -- ERROR
+ \dew+
+ \des+
+ \deu+
+ \det+
+
+ -- ===================================================================
+ -- simple queries
+ -- ===================================================================
+ -- single table, with/without alias
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
+ SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+ SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+ -- with WHERE clause
+ SELECT * FROM ft1 t1 WHERE t1.c1 = 101;
+ -- 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
+ SELECT * FROM ft1 t1 WHERE t1.c3 = (SELECT MAX(c3) FROM ft2 t2) ORDER BY c1;
+ -- used in CTE
+ WITH t1 AS (SELECT * FROM ft1 WHERE c1 <= 10) SELECT t2.c1, t2.c2, t2.c3, t2.c4 FROM t1, ft2 t2 WHERE t1.c1 = t2.c1 ORDER BY t1.c1;
+ -- fixed values
+ SELECT 'fixed', NULL FROM ft1 t1 WHERE c1 = 1;
+
+ -- ===================================================================
+ -- parameterized queries
+ -- ===================================================================
+ -- simple join
+ PREPARE st1(int, int) AS SELECT t1.c3, t2.c3 FROM ft1 t1, ft2 t2 WHERE t1.c1 = $1 AND t2.c1 = $2;
+ EXPLAIN (COSTS false) EXECUTE st1(1, 2);
+ EXECUTE st1(1, 1);
+ EXECUTE st1(101, 101);
+ -- subquery using stable function (can't be pushed down)
+ PREPARE st2(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c4) = 6) ORDER BY c1;
+ EXPLAIN (COSTS false) EXECUTE st2(10, 20);
+ EXECUTE st2(10, 20);
+ EXECUTE st1(101, 101);
+ -- subquery using immutable function (can be pushed down)
+ PREPARE st3(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c5) = 6) ORDER BY c1;
+ EXPLAIN (COSTS false) EXECUTE st3(10, 20);
+ EXECUTE st3(10, 20);
+ EXECUTE st3(20, 30);
+ -- cleanup
+ DEALLOCATE st1;
+ DEALLOCATE st2;
+ DEALLOCATE st3;
+
+ -- ===================================================================
+ -- connection management
+ -- ===================================================================
+ SELECT srvname, usename FROM pgsql_fdw_connections;
+ SELECT pgsql_fdw_disconnect(srvid, usesysid) FROM pgsql_fdw_get_connections();
+ SELECT srvname, usename FROM pgsql_fdw_connections;
+
+ -- ===================================================================
+ -- cleanup
+ -- ===================================================================
+ DROP EXTENSION pgsql_fdw CASCADE;
+
diff --git a/doc/src/sgml/contrib.sgml b/doc/src/sgml/contrib.sgml
index adf09ca..65c7e81 100644
*** a/doc/src/sgml/contrib.sgml
--- b/doc/src/sgml/contrib.sgml
*************** CREATE EXTENSION <replaceable>module_nam
*** 117,122 ****
--- 117,123 ----
&pgcrypto;
&pgfreespacemap;
&pgrowlocks;
+ &pgsql-fdw;
&pgstandby;
&pgstatstatements;
&pgstattuple;
diff --git a/doc/src/sgml/filelist.sgml b/doc/src/sgml/filelist.sgml
index ed39e0b..d72590f 100644
*** a/doc/src/sgml/filelist.sgml
--- b/doc/src/sgml/filelist.sgml
***************
*** 123,128 ****
--- 123,129 ----
<!ENTITY pgcrypto SYSTEM "pgcrypto.sgml">
<!ENTITY pgfreespacemap SYSTEM "pgfreespacemap.sgml">
<!ENTITY pgrowlocks SYSTEM "pgrowlocks.sgml">
+ <!ENTITY pgsql-fdw SYSTEM "pgsql-fdw.sgml">
<!ENTITY pgstandby SYSTEM "pgstandby.sgml">
<!ENTITY pgstatstatements SYSTEM "pgstatstatements.sgml">
<!ENTITY pgstattuple SYSTEM "pgstattuple.sgml">
diff --git a/doc/src/sgml/pgsql-fdw.sgml b/doc/src/sgml/pgsql-fdw.sgml
index ...dd2d711 .
*** a/doc/src/sgml/pgsql-fdw.sgml
--- b/doc/src/sgml/pgsql-fdw.sgml
***************
*** 0 ****
--- 1,271 ----
+ <!-- doc/src/sgml/pgsql-fdw.sgml -->
+
+ <sect1 id="pgsql-fdw" xreflabel="pgsql_fdw">
+ <title>pgsql_fdw</title>
+
+ <indexterm zone="pgsql-fdw">
+ <primary>pgsql_fdw</primary>
+ </indexterm>
+
+ <para>
+ The <filename>pgsql_fdw</filename> module provides a foreign-data wrapper for
+ external <productname>PostgreSQL</productname> servers.
+ With this module, users can access data stored in external
+ <productname>PostgreSQL</productname> via plain SQL statements.
+ </para>
+
+ <para>
+ The <application>pgsql_fdw</application> can be installed on only
+ <productname>PostgreSQL</productname> 9.1 or later, but it can also access
+ data stored in <productname>PostgreSQL</productname> 9.0.
+ <productname>PostgreSQL</productname>8.4 or older are not available as remote
+ server because <application>pgsql_fdw</application> uses YAML format of
+ <command>EXPLAIN</command> output.
+ </para>
+
+ <para>
+ Note that default wrapper <literal>pgsql_fdw</literal> is created
+ automatically during <command>CREATE EXTENSION</command> command for
+ <application>pgsql_fdw</application>.
+ </para>
+
+ <sect2>
+ <title>FDW Options of pgsql_fdw</title>
+
+ <sect3>
+ <title>Connection Options</title>
+ <para>
+ A foreign server and user mapping created using this wrapper can have
+ <application>libpq</> connection options, expect below:
+
+ <itemizedlist>
+ <listitem><para>client_encoding</para></listitem>
+ <listitem><para>fallback_application_name</para></listitem>
+ <listitem><para>replication</para></listitem>
+ </itemizedlist>
+
+ For details of <application>libpq</> connection options, see
+ <xref linkend="libpq-connect">.
+ </para>
+
+ <para>
+ <literal>user</literal> and <literal>password</literal> can be
+ specified on user mappings, and others can be specified on foreign servers.
+ </para>
+ </sect3>
+
+ <sect3>
+ <title>Object Name Options</title>
+ <para>
+ Foreign tables which were created using this wrapper, or its columns can
+ have object name options. These options can be used to specify the names
+ used in SQL statement sent to remote <productname>PostgreSQL</productname>
+ server. These options are useful when a remote object has different name
+ from corresponding local one.
+ </para>
+
+ <variablelist>
+
+ <varlistentry>
+ <term><literal>nspname</literal></term>
+ <listitem>
+ <para>
+ This option, which can be specified on a foreign table, is used as a
+ namespace (schema) reference in the SQL statement. If this options is
+ omitted, <literal>pg_class.nspname</literal> of the foreign table is
+ used.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>relname</literal></term>
+ <listitem>
+ <para>
+ This option, which can be specified on a foreign table, is used as a
+ relation (table) reference in the SQL statement. If this options is
+ omitted, <literal>pg_class.relname</literal> of the foreign table is
+ used.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>colname</literal></term>
+ <listitem>
+ <para>
+ This option, which can be specified on a column of a foreign table, is
+ used as a column (attribute) reference in the SQL statement. If this
+ option is omitted, <literal>pg_attribute.attname</literal> of the column
+ of the foreign table is used.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ </variablelist>
+
+ </sect3>
+
+ <sect3>
+ <title>Cursor Options</title>
+ <para>
+ The <application>pgsql_fdw</application> switches the way used to retrieve
+ result of remote query according to estimated number of result rows.
+ If the estimation was less than a threshold, the result rows are retrieved
+ at once with simple <command>SELECT</command> statement.
+ In contrast, if the estimation is larger than the threshold, the result
+ rows are fetched separately with a cursor defined by
+ <command>DECLARE</command> statement.
+ </para>
+
+ <para>
+ Users can control this behavior with setting cursor options for a foreign
+ table.
+ </para>
+
+ <variablelist>
+
+ <varlistentry>
+ <term><literal>min_cursor_rows</literal></term>
+ <listitem>
+ <para>
+ This option specifies the minimum estimated number of result rows to use
+ cursor for fetching result. Setting this to 1 means that every query
+ uses cursor, and setting to 0 means that every query uses simple
+ <command>SELECT</command>. This option can be set on a foreign table,
+ and accepts only positive integer value.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>fetch_count</literal></term>
+ <listitem>
+ <para>
+ This option specifies the number of rows fetched at once in cursor mode.
+ This option accepts only integer value larger than zero.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ </variablelist>
+
+ </sect3>
+
+ </sect2>
+
+ <sect2>
+ <title>Connection Management</title>
+
+ <para>
+ The <application>pgsql_fdw</application> establishes a connection to a
+ foreign server in the beginning of the first query which uses a foreign
+ table associated to the foreign server, and reuses the connection following
+ queries and even in following foreign scans in same query.
+
+ You can see the list of active connections via
+ <structname>pgsql_fdw_connections</structname> view. It shows pair of oid
+ and name of server and local role for each active connections established by
+ <application>pgsql_fdw</application>. For security reason, only superuser
+ can see other role's connections.
+ </para>
+
+ <para>
+ Established connections are kept alive until local role changes or the
+ current transaction aborts or user requests so.
+ </para>
+
+ <para>
+ If role has been changed, active connections established as old local role
+ is kept alive but never be reused until locla role has restored to original
+ role. This kind of situation happens with <command>SET ROLE</command> and
+ <command>SET SESSION AUTHORIZATION</command>.
+ </para>
+
+ <para>
+ If current transaction aborts by error or user request, all active
+ connections are disconnected automatically. This behavior avoids possible
+ connection leaks on error.
+ </para>
+
+ <para>
+ You can discard persistent connection at arbitrary timing with
+ <function>pgsql_fdw_disconnect()</function>. It takes server oid and
+ user oid as arguments. This function can handle only connections
+ established in current session; connections established by other backends
+ are not reachable.
+ </para>
+
+ <para>
+ You can discard all active and visible connections in current session with
+ using <structname>pgsql_fdw_connections</structname> and
+ <function>pgsql_fdw_disconnect()</function> together:
+ <synopsis>
+ postgres=# SELECT pgsql_fdw_disconnect(srvid, usesysid) FROM pgsql_fdw_connections;
+ pgsql_fdw_disconnect
+ ----------------------
+ OK
+ OK
+ (2 rows)
+ </synopsis>
+ </para>
+ </sect2>
+
+ <sect2>
+ <title>Transaction Management</title>
+ <para>
+ The <application>pgsql_fdw</application> executes <command>BEGIN</command>
+ command when a new connection has established. This means that all remote
+ queries for a foreign server are executed in a transaction. Since the
+ default transaction isolation level is <literal>READ COMMITTED</literal>,
+ multiple foreign scans in a local query might produce inconsistent results.
+
+ To avoid this inconsistency, you can use <literal>SERIALIZABLE</literal>
+ level for remote transaction with setting
+ <literal>default_transaction_isolation</literal> for the user used for
+ <application>pgsql_fdw</application> connection on remote side.
+ </para>
+ </sect2>
+
+ <sect2>
+ <title>Estimation of Costs and Rows</title>
+ <para>
+ The <application>pgsql_fdw</application> estimates the costs of a foreign
+ scan by adding some basic costs: connection costs, remote query costs and
+ data transfer costs.
+ To get remote query costs <application>pgsql_fdw</application> executes
+ <command>EXPLAIN</command> command on remote server for each foreign scan.
+ </para>
+
+ <para>
+ On the other hand, estimated rows which was returned by
+ <command>EXPLAIN</command> is used for local estimation as-is.
+ </para>
+ </sect2>
+
+ <sect2>
+ <title>EXPLAIN Output</title>
+ <para>
+ For a foreign table using <literal>pgsql_fdw</>, <command>EXPLAIN</> shows
+ a remote SQL statement which is sent to remote
+ <productname>PostgreSQL</productname> server for a ForeignScan plan node.
+ For example:
+ </para>
+ <synopsis>
+ postgres=# EXPLAIN SELECT aid FROM pgbench_accounts WHERE abalance < 0;
+ QUERY PLAN
+ -----------------------------------------------------------------------------------------------
+ Foreign Scan on pgbench_accounts (cost=100.00..8769.00 rows=1 width=4)
+ Remote SQL: SELECT aid, NULL, NULL, NULL FROM public.pgbench_accounts WHERE (abalance < 0)
+ (2 rows)
+ </synopsis>
+ </sect2>
+
+ <sect2>
+ <title>Author</title>
+ <para>
+ Shigeru Hanada <email>shigeru.hanada@gmail.com</email>
+ </para>
+ </sect2>
+
+ </sect1>
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 75923a6..6fda053 100644
*** a/src/backend/utils/adt/ruleutils.c
--- b/src/backend/utils/adt/ruleutils.c
*************** deparse_context_for(const char *aliasnam
*** 2161,2166 ****
--- 2161,2189 ----
return list_make1(dpns);
}
+ /* ----------
+ * deparse_context_for_rtelist - Build deparse context for a single relation
+ *
+ * Given the list of RangeTableEnetry, build deparsing context for an
+ * expression referencing those relations. This is sufficient for uses of
+ * deparse_expression before plan has been created.
+ * ----------
+ */
+ List *
+ deparse_context_for_rtelist(List *rtable)
+ {
+ deparse_namespace *dpns;
+
+ dpns = (deparse_namespace *) palloc0(sizeof(deparse_namespace));
+
+ /* Build a minimal RTE for the rel */
+ dpns->rtable = rtable;
+ dpns->ctes = NIL;
+
+ /* Return a one-deep namespace stack */
+ return list_make1(dpns);
+ }
+
/*
* deparse_context_for_planstate - Build deparse context for a plan
*
diff --git a/src/include/utils/builtins.h b/src/include/utils/builtins.h
index 8a1c82e..a56366f 100644
*** a/src/include/utils/builtins.h
--- b/src/include/utils/builtins.h
*************** extern char *deparse_expression(Node *ex
*** 626,631 ****
--- 626,632 ----
extern List *deparse_context_for(const char *aliasname, Oid relid);
extern List *deparse_context_for_planstate(Node *planstate, List *ancestors,
List *rtable);
+ extern List *deparse_context_for_rtelist(List *rtable);
extern const char *quote_identifier(const char *ident);
extern char *quote_qualified_identifier(const char *qualifier,
const char *ident);
2011/10/25 Shigeru Hanada <shigeru.hanada@gmail.com>:
Connection management
=====================
The pgsql_fdw establishes a new connection when a foreign server is
accessed first for the local session. Established connection is shared
between all foreign scans in the local query, and shared between even
scans in following queries. Connections are discarded when the current
transaction aborts so that unexpected failure won't cause connection
leak. This is implemented with resource owner mechanism.
I have a doubt here, on sharing connection for each server. What if
there are simultaneous scan on the same plan? Say,
-> Nested Loop
-> Foreign Scan to table T1 on server A
-> Foreign Scan to table T2 on server A
Okay, you are thinking about Foreign Join, so example above is too
simple. But it is always possible to execute such a query if foreign
scan nodes are separated far, isn't it? As far as I see from your
explanation, scan T1 and scan T2 share the same connection. Now join
node scans one row from left (T1) while asking rows from right (T2)
without fetching all the rows from left. If T2 requests to server A,
the connection's result (of T1) is discarded. Am I understand
correctly?
Regards,
--
Hitoshi Harada
On Sat, Oct 29, 2011 at 12:25:46AM -0700, Hitoshi Harada wrote:
I have a doubt here, on sharing connection for each server. What if
there are simultaneous scan on the same plan? Say,-> Nested Loop
-> Foreign Scan to table T1 on server A
-> Foreign Scan to table T2 on server AOkay, you are thinking about Foreign Join, so example above is too
simple. But it is always possible to execute such a query if foreign
scan nodes are separated far, isn't it? As far as I see from your
explanation, scan T1 and scan T2 share the same connection. Now join
node scans one row from left (T1) while asking rows from right (T2)
without fetching all the rows from left. If T2 requests to server A,
the connection's result (of T1) is discarded. Am I understand
correctly?
This would need to be factored in in the cost calculations. For remote
servers there is an overhead per tuple transmitted. So in the above
case it might actually be quicker to do the nested loop locally.
To handle the parallel case you might need to materialise in the inner
loop, that would avoid the double scan. Or we could fix the protocol so
you can stream multiple queries at once.
Actually, you can already do this is you use DECLARE CURSOR for all the
queries upfront and then FETCH as needed. That way you can do it all
over one connection.
Have a nice day,
--
Martijn van Oosterhout <kleptog@svana.org> http://svana.org/kleptog/
He who writes carelessly confesses thereby at the very outset that he does
not attach much importance to his own thoughts.
-- Arthur Schopenhauer
Hitoshi Harada <umi.tanuki@gmail.com> writes:
I have a doubt here, on sharing connection for each server. What if
there are simultaneous scan on the same plan? Say,
-> Nested Loop
-> Foreign Scan to table T1 on server A
-> Foreign Scan to table T2 on server A
Okay, you are thinking about Foreign Join, so example above is too
simple. But it is always possible to execute such a query if foreign
scan nodes are separated far, isn't it? As far as I see from your
explanation, scan T1 and scan T2 share the same connection. Now join
node scans one row from left (T1) while asking rows from right (T2)
without fetching all the rows from left. If T2 requests to server A,
the connection's result (of T1) is discarded. Am I understand
correctly?
I have not looked at the code, but ISTM the way that this has to work is
that you set up a portal for each active scan. Then you can fetch a few
rows at a time from any one of them.
If you're doing this through libpq, it'd be necessary to implement each
scan using a cursor. I'm not sure whether it'd be worth our time to
add more functions to libpq to allow more-direct access to the protocol
portal feature.
regards, tom lane
On Sat, Oct 29, 2011 at 8:13 AM, Tom Lane <tgl@sss.pgh.pa.us> wrote:
Hitoshi Harada <umi.tanuki@gmail.com> writes:
I have a doubt here, on sharing connection for each server. What if
there are simultaneous scan on the same plan? Say,-> Nested Loop
-> Foreign Scan to table T1 on server A
-> Foreign Scan to table T2 on server AOkay, you are thinking about Foreign Join, so example above is too
simple. But it is always possible to execute such a query if foreign
scan nodes are separated far, isn't it? As far as I see from your
explanation, scan T1 and scan T2 share the same connection. Now join
node scans one row from left (T1) while asking rows from right (T2)
without fetching all the rows from left. If T2 requests to server A,
the connection's result (of T1) is discarded. Am I understand
correctly?I have not looked at the code, but ISTM the way that this has to work is
that you set up a portal for each active scan. Then you can fetch a few
rows at a time from any one of them.
Hmm, true. Looking back at the original proposal (neither did I look
at the code,) there seems to be a cursor mode. ISTM it is hard for fdw
to know how the whole plan tree looks, so consequently do we always
cursor regardless of estimated row numbers? I haven't had much
experiences around cursor myself, but is it as efficient as
non-cursor?
Regards,
--
Hitoshi Harada
Hitoshi Harada <umi.tanuki@gmail.com> writes:
On Sat, Oct 29, 2011 at 8:13 AM, Tom Lane <tgl@sss.pgh.pa.us> wrote:
I have not looked at the code, but ISTM the way that this has to work is
that you set up a portal for each active scan. Then you can fetch a few
rows at a time from any one of them.
Hmm, true. Looking back at the original proposal (neither did I look
at the code,) there seems to be a cursor mode. ISTM it is hard for fdw
to know how the whole plan tree looks, so consequently do we always
cursor regardless of estimated row numbers?
I think we have to. Even if we estimate that a given scan will return
only a few rows, what happens if we're wrong? We don't want to blow out
memory on the local server by retrieving gigabytes in one go.
I haven't had much experiences around cursor myself, but is it as
efficient as non-cursor?
No, but if you need max efficiency you shouldn't be using foreign tables
in the first place; they're always going to be expensive to access.
It's likely that making use of native protocol portals (instead of
executing a lot of FETCH commands) would help. But I think we'd be well
advised to do the first pass with just the existing libpq facilities,
and then measure to see where to improve performance.
regards, tom lane
2011/10/29 Hitoshi Harada <umi.tanuki@gmail.com>:
I have a doubt here, on sharing connection for each server. What if
there are simultaneous scan on the same plan? Say,-> Nested Loop
-> Foreign Scan to table T1 on server A
-> Foreign Scan to table T2 on server AOkay, you are thinking about Foreign Join, so example above is too
simple. But it is always possible to execute such a query if foreign
scan nodes are separated far, isn't it? As far as I see from your
explanation, scan T1 and scan T2 share the same connection. Now join
node scans one row from left (T1) while asking rows from right (T2)
without fetching all the rows from left. If T2 requests to server A,
the connection's result (of T1) is discarded. Am I understand
correctly?
I think that sharing a connection doesn't cause any problem.
In cursor mode, using multiple cursors concurrently through one connection
is OK. In SELECT mode, pgsql_fdw executes SELECT statement with
PQexecParams and retrieves whole result *inside* the first Iterate call for
an outer tuple. So libpq connection is already available when another scan
needs to call Iterate function.
--
Shigeru Hanada
2011/10/30 Tom Lane <tgl@sss.pgh.pa.us>:
Hitoshi Harada <umi.tanuki@gmail.com> writes:
On Sat, Oct 29, 2011 at 8:13 AM, Tom Lane <tgl@sss.pgh.pa.us> wrote:
I have not looked at the code, but ISTM the way that this has to work is
that you set up a portal for each active scan. Then you can fetch a few
rows at a time from any one of them.Hmm, true. Looking back at the original proposal (neither did I look
at the code,) there seems to be a cursor mode. ISTM it is hard for fdw
to know how the whole plan tree looks, so consequently do we always
cursor regardless of estimated row numbers?I think we have to. Even if we estimate that a given scan will return
only a few rows, what happens if we're wrong? We don't want to blow out
memory on the local server by retrieving gigabytes in one go.
Oh, I overlooked the possibility of wrong estimation. Old PostgreSQL uses
1000 as default estimation, so big table which has not been analyzed may
crashes the backend.
To ensure the data retrieving safe, we need to get actual amount of result,
maybe by executing SELECT COUNT(*) in planning phase. It sounds too heavy
to do for every scan, and it still lacks actual width.
One possible idea is to change default value of min_cursur_rows option to 0
so that pgsql_fdw uses CURSOR by default, but it seems not enough. I'll
drop simple SELECT mode from first version of pgsql_fdw for safety.
I haven't had much experiences around cursor myself, but is it as
efficient as non-cursor?No, but if you need max efficiency you shouldn't be using foreign tables
in the first place; they're always going to be expensive to access.It's likely that making use of native protocol portals (instead of
executing a lot of FETCH commands) would help. But I think we'd be well
advised to do the first pass with just the existing libpq facilities,
and then measure to see where to improve performance.
I long for protocol-level cursor. :)
--
Shigeru Hanada
(2011/10/30 11:34), Shigeru Hanada wrote:
2011/10/30 Tom Lane<tgl@sss.pgh.pa.us>:
I think we have to. Even if we estimate that a given scan will return
only a few rows, what happens if we're wrong? We don't want to blow out
memory on the local server by retrieving gigabytes in one go.Oh, I overlooked the possibility of wrong estimation. Old PostgreSQL uses
1000 as default estimation, so big table which has not been analyzed may
crashes the backend.To ensure the data retrieving safe, we need to get actual amount of result,
maybe by executing SELECT COUNT(*) in planning phase. It sounds too heavy
to do for every scan, and it still lacks actual width.One possible idea is to change default value of min_cursur_rows option to 0
so that pgsql_fdw uses CURSOR by default, but it seems not enough. I'll
drop simple SELECT mode from first version of pgsql_fdw for safety.
I removed simple SELECT mode from pgsql_fdw, and consequently also
removed min_cursor_rows FDW option. This fix avoids possible memory
exhaustion due to wrong estimation gotten from remote side.
Once libpq has had capability to retrieve arbitrary number of rows from
remote portal at a time without server-side cursor in future, then we
will be able to revive simple SELECT. Then it's enough safe even if we
don't have actual data size, but (maybe) faster than cursor mode because
we can reduce # of SQL commands. Though of course proof of performance
advantage should be shown before such development.
--
Shigeru Hanada
Attachments:
pgsql_fdw_v3.patchtext/plain; name=pgsql_fdw_v3.patchDownload
diff --git a/contrib/Makefile b/contrib/Makefile
index 0c238aa..e09e61e 100644
*** a/contrib/Makefile
--- b/contrib/Makefile
*************** SUBDIRS = \
*** 41,46 ****
--- 41,47 ----
pgbench \
pgcrypto \
pgrowlocks \
+ pgsql_fdw \
pgstattuple \
seg \
spi \
diff --git a/contrib/README b/contrib/README
index a1d42a1..d3fa211 100644
*** a/contrib/README
--- b/contrib/README
*************** pgrowlocks -
*** 158,163 ****
--- 158,167 ----
A function to return row locking information
by Tatsuo Ishii <ishii@sraoss.co.jp>
+ pgsql_fdw -
+ Foreign-data wrapper for external PostgreSQL servers.
+ by Shigeru Hanada <shigeru.hanada@gmail.com>
+
pgstattuple -
Functions to return statistics about "dead" tuples and free
space within a table
diff --git a/contrib/pgsql_fdw/.gitignore b/contrib/pgsql_fdw/.gitignore
index ...0854728 .
*** a/contrib/pgsql_fdw/.gitignore
--- b/contrib/pgsql_fdw/.gitignore
***************
*** 0 ****
--- 1,4 ----
+ # Generated subdirectories
+ /results/
+ *.o
+ *.so
diff --git a/contrib/pgsql_fdw/Makefile b/contrib/pgsql_fdw/Makefile
index ...6381365 .
*** a/contrib/pgsql_fdw/Makefile
--- b/contrib/pgsql_fdw/Makefile
***************
*** 0 ****
--- 1,22 ----
+ # contrib/pgsql_fdw/Makefile
+
+ MODULE_big = pgsql_fdw
+ OBJS = pgsql_fdw.o option.o deparse.o connection.o
+ PG_CPPFLAGS = -I$(libpq_srcdir)
+ SHLIB_LINK = $(libpq)
+
+ EXTENSION = pgsql_fdw
+ DATA = pgsql_fdw--1.0.sql
+
+ REGRESS = pgsql_fdw
+
+ ifdef USE_PGXS
+ PG_CONFIG = pg_config
+ PGXS := $(shell $(PG_CONFIG) --pgxs)
+ include $(PGXS)
+ else
+ subdir = contrib/pgsql_fdw
+ top_builddir = ../..
+ include $(top_builddir)/src/Makefile.global
+ include $(top_srcdir)/contrib/contrib-global.mk
+ endif
diff --git a/contrib/pgsql_fdw/connection.c b/contrib/pgsql_fdw/connection.c
index ...a2b88ec .
*** a/contrib/pgsql_fdw/connection.c
--- b/contrib/pgsql_fdw/connection.c
***************
*** 0 ****
--- 1,504 ----
+ /*-------------------------------------------------------------------------
+ *
+ * connection.c
+ * Connection management for pgsql_fdw
+ *
+ * Portions Copyright (c) 2011, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/connection.c
+ *
+ *-------------------------------------------------------------------------
+ */
+ #include "postgres.h"
+
+ #include "catalog/pg_type.h"
+ #include "foreign/foreign.h"
+ #include "funcapi.h"
+ #include "libpq-fe.h"
+ #include "mb/pg_wchar.h"
+ #include "miscadmin.h"
+ #include "utils/array.h"
+ #include "utils/builtins.h"
+ #include "utils/hsearch.h"
+ #include "utils/memutils.h"
+ #include "utils/resowner.h"
+ #include "utils/tuplestore.h"
+
+ #include "pgsql_fdw.h"
+ #include "connection.h"
+
+ /* ============================================================================
+ * Connection management functions
+ * ==========================================================================*/
+
+ /*
+ * Connection cache entry managed with hash table.
+ */
+ typedef struct ConnCacheEntry
+ {
+ /* hash key must be first */
+ Oid serverid; /* oid of foreign server */
+ Oid userid; /* oid of local user */
+
+ 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)
+ {
+ bool found;
+ ConnCacheEntry *entry;
+ ConnCacheEntry key;
+ PGconn *conn = NULL;
+
+ /* initialize connection cache if it isn't */
+ if (FSConnectionHash == NULL)
+ {
+ HASHCTL ctl;
+
+ /* hash key is a pair of oids: serverid and userid */
+ MemSet(&ctl, 0, sizeof(ctl));
+ ctl.keysize = sizeof(Oid) + sizeof(Oid);
+ ctl.entrysize = sizeof(ConnCacheEntry);
+ ctl.hash = tag_hash;
+ ctl.match = memcmp;
+ ctl.keycopy = memcpy;
+ /* allocate FSConnectionHash in the cache context */
+ ctl.hcxt = CacheMemoryContext;
+ FSConnectionHash = hash_create("Foreign Connections", 32,
+ &ctl,
+ HASH_ELEM | HASH_CONTEXT |
+ HASH_FUNCTION | HASH_COMPARE |
+ HASH_KEYCOPY);
+ }
+
+ /* Create key value for the entry. */
+ MemSet(&key, 0, sizeof(key));
+ key.serverid = server->serverid;
+ key.userid = GetOuterUserId();
+
+ /* Is there any cached and valid connection with such key? */
+ entry = hash_search(FSConnectionHash, &key, HASH_ENTER, &found);
+ if (found)
+ {
+ if (entry->conn != NULL)
+ {
+ entry->refs++;
+ elog(DEBUG1,
+ "reuse connection %u/%u (%d)",
+ entry->serverid,
+ entry->userid,
+ entry->refs);
+ 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 ResourceOwner to clean the connection up on error including
+ * user interrupt.
+ */
+ elog(DEBUG1,
+ "create entry for %u/%u (%d)",
+ entry->serverid,
+ entry->userid,
+ entry->refs);
+ 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: key items of entry has been initialized in
+ * hash_search(HASH_ENTER).
+ */
+ entry->refs = 1;
+ entry->conn = conn;
+ elog(DEBUG1,
+ "connected to %u/%u (%d)",
+ entry->serverid,
+ entry->userid,
+ 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 PGconn *
+ connect_pg_server(ForeignServer *server, UserMapping *user)
+ {
+ const char *conname = server->servername;
+ PGconn *conn;
+ PGresult *res;
+ 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. Those two object hold only libpq options.
+ * Extra 3 items are for:
+ * *) fallback_application_name
+ * *) client_encoding
+ * *) NULL termination (end marker)
+ *
+ * Note: We don't omit any parameters even target database might be older
+ * than local, because unexpected parameters are just ignored.
+ */
+ n = list_length(server->options) + list_length(user->options) + 3;
+ 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 += ExtractConnectionOptions(server->options,
+ all_keywords + n, all_values + n);
+ n += ExtractConnectionOptions(user->options,
+ all_keywords + n, all_values + n);
+ all_keywords[n] = all_values[n] = NULL;
+
+ for (i = 0, j = 0; all_keywords[i]; i++)
+ {
+ keywords[j] = all_keywords[i];
+ values[j] = all_values[i];
+ j++;
+ }
+
+ /* Use "pgsql_fdw" as fallback_application_name. */
+ keywords[j] = "fallback_application_name";
+ values[j++] = "pgsql_fdw";
+
+ /* Set client_encoding so that libpq can convert encoding properly. */
+ keywords[j] = "client_encoding";
+ values[j++] = GetDatabaseEncodingName();
+
+ keywords[j] = values[j] = NULL;
+ pfree(all_keywords);
+ pfree(all_values);
+
+ /* 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.")));
+ }
+
+ /*
+ * Start transaction to use cursor to retrieve data separately.
+ */
+ res = PQexec(conn, "BEGIN");
+ if (res == NULL || PQresultStatus(res) != PGRES_COMMAND_OK)
+ elog(ERROR, "could not start transaction");
+
+ 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 sequentially 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;
+ }
+ if (entry != NULL)
+ 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 referrer, unregister it from cache.
+ * TODO: Note that sharing connections requires a mechanism to detect
+ * change of FDW object to invalidate lasting connections.
+ */
+ entry->refs--;
+ elog(DEBUG1,
+ "connection %u/%u released (%d)",
+ entry->serverid,
+ entry->userid,
+ entry->refs);
+ }
+
+ /*
+ * 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(DEBUG1,
+ "closing connection %u/%u",
+ entry->serverid,
+ entry->userid);
+ PQfinish(entry->conn);
+ entry->refs = 0;
+ entry->conn = NULL;
+ }
+ else
+ elog(DEBUG1,
+ "connection %u/%u already closed",
+ entry->serverid,
+ entry->userid);
+ }
+
+ /*
+ * Get list of connections currently active.
+ */
+ Datum pgsql_fdw_get_connections(PG_FUNCTION_ARGS);
+ PG_FUNCTION_INFO_V1(pgsql_fdw_get_connections);
+ Datum
+ pgsql_fdw_get_connections(PG_FUNCTION_ARGS)
+ {
+ ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+ HASH_SEQ_STATUS scan;
+ ConnCacheEntry *entry;
+ MemoryContext oldcontext = CurrentMemoryContext;
+ Tuplestorestate *tuplestore;
+ TupleDesc tupdesc;
+
+ /* We return list of connection with storing them in a Tuplestore. */
+ rsinfo->returnMode = SFRM_Materialize;
+ rsinfo->setResult = NULL;
+ rsinfo->setDesc = NULL;
+
+ /* Create tuplestore and copy of TupleDesc in per-query context. */
+ MemoryContextSwitchTo(rsinfo->econtext->ecxt_per_query_memory);
+
+ tupdesc = CreateTemplateTupleDesc(2, false);
+ TupleDescInitEntry(tupdesc, 1, "srvid", OIDOID, -1, 0);
+ TupleDescInitEntry(tupdesc, 2, "usesysid", OIDOID, -1, 0);
+ rsinfo->setDesc = tupdesc;
+
+ tuplestore = tuplestore_begin_heap(false, false, work_mem);
+ rsinfo->setResult = tuplestore;
+
+ MemoryContextSwitchTo(oldcontext);
+
+ /*
+ * We need to scan sequentially since we use the address to find
+ * appropriate PGconn from the hash table.
+ */
+ if (FSConnectionHash != NULL)
+ {
+ hash_seq_init(&scan, FSConnectionHash);
+ while ((entry = (ConnCacheEntry *) hash_seq_search(&scan)))
+ {
+ Datum values[2];
+ bool nulls[2];
+ HeapTuple tuple;
+
+ elog(DEBUG1, "found: %u/%u", entry->serverid, entry->userid);
+
+ /* Ignore inactive connections */
+ if (PQstatus(entry->conn) != CONNECTION_OK)
+ continue;
+
+ /*
+ * Ignore other users' connections if current user isn't a
+ * superuser.
+ */
+ if (!superuser() && entry->userid != GetUserId())
+ continue;
+
+ values[0] = ObjectIdGetDatum(entry->serverid);
+ values[1] = ObjectIdGetDatum(entry->userid);
+ nulls[0] = false;
+ nulls[1] = false;
+
+ tuple = heap_formtuple(tupdesc, values, nulls);
+ tuplestore_puttuple(tuplestore, tuple);
+ }
+ }
+ tuplestore_donestoring(tuplestore);
+
+ PG_RETURN_VOID();
+ }
+
+ /*
+ * Discard persistent connection designated by given connection name.
+ */
+ Datum pgsql_fdw_disconnect(PG_FUNCTION_ARGS);
+ PG_FUNCTION_INFO_V1(pgsql_fdw_disconnect);
+ Datum
+ pgsql_fdw_disconnect(PG_FUNCTION_ARGS)
+ {
+ Oid serverid = PG_GETARG_OID(0);
+ Oid userid = PG_GETARG_OID(1);
+ ConnCacheEntry key;
+ ConnCacheEntry *entry = NULL;
+ bool found;
+
+ /* Non-superuser can't discard other users' connection. */
+ if (!superuser() && userid != GetOuterUserId())
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_RESOURCES),
+ errmsg("only superuser can discard other user's connection")));
+
+ /*
+ * If no connection has been established, or no such connections, just
+ * return "NG" to indicate nothing has done.
+ */
+ if (FSConnectionHash == NULL)
+ PG_RETURN_TEXT_P(cstring_to_text("NG"));
+
+ key.serverid = serverid;
+ key.userid = userid;
+ entry = hash_search(FSConnectionHash, &key, HASH_FIND, &found);
+ if (!found)
+ PG_RETURN_TEXT_P(cstring_to_text("NG"));
+
+ /* Discard cached connection, and clear reference counter. */
+ PQfinish(entry->conn);
+ entry->refs = 0;
+ entry->conn = NULL;
+ elog(DEBUG1, "closed connection %u/%u", serverid, userid);
+
+ PG_RETURN_TEXT_P(cstring_to_text("OK"));
+ }
diff --git a/contrib/pgsql_fdw/connection.h b/contrib/pgsql_fdw/connection.h
index ...694534f .
*** a/contrib/pgsql_fdw/connection.h
--- b/contrib/pgsql_fdw/connection.h
***************
*** 0 ****
--- 1,25 ----
+ /*-------------------------------------------------------------------------
+ *
+ * connection.h
+ * Connection management for pgsql_fdw
+ *
+ * Portions Copyright (c) 2011, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_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/pgsql_fdw/deparse.c b/contrib/pgsql_fdw/deparse.c
index ...c1c74ac .
*** a/contrib/pgsql_fdw/deparse.c
--- b/contrib/pgsql_fdw/deparse.c
***************
*** 0 ****
--- 1,377 ----
+ /*-------------------------------------------------------------------------
+ *
+ * deparse.c
+ * query deparser for PostgreSQL
+ *
+ * Copyright (c) 2011, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/deparse.c
+ *
+ *-------------------------------------------------------------------------
+ */
+ #include "postgres.h"
+
+ #include "access/transam.h"
+ #include "foreign/foreign.h"
+ #include "lib/stringinfo.h"
+ #include "nodes/nodeFuncs.h"
+ #include "nodes/nodes.h"
+ #include "nodes/makefuncs.h"
+ #include "optimizer/clauses.h"
+ #include "optimizer/var.h"
+ #include "parser/parsetree.h"
+ #include "utils/builtins.h"
+ #include "utils/lsyscache.h"
+
+ #include "pgsql_fdw.h"
+
+ /*
+ * Context for walk-through the expression tree.
+ */
+ typedef struct foreign_executable_cxt
+ {
+ PlannerInfo *root;
+ RelOptInfo *foreignrel;
+ } foreign_executable_cxt;
+
+ static bool is_foreign_expr(PlannerInfo *root, RelOptInfo *baserel, Expr *expr);
+ static bool foreign_expr_walker(Node *node, foreign_executable_cxt *context);
+ static bool is_proc_remotely_executable(Oid procid);
+
+ /*
+ * Deparse query representation into SQL statement which suits for remote
+ * PostgreSQL server. Also some of quals in WHERE clause will be pushed down
+ * If they are safe to be evaluated on the remote side.
+ */
+ char *
+ deparseSql(Oid relid, PlannerInfo *root, RelOptInfo *baserel)
+ {
+ StringInfoData foreign_relname;
+ StringInfoData sql; /* builder for SQL statement */
+ bool first;
+ AttrNumber attr;
+ List *attr_used = NIL; /* List of AttNumber used in the query */
+ const char *nspname = NULL; /* plain namespace name */
+ const char *relname = NULL; /* plain relation name */
+ const char *q_nspname; /* quoted namespace name */
+ const char *q_relname; /* quoted relation name */
+ List *foreign_expr = NIL; /* list of Expr* evaluated on remote */
+ int i;
+ List *rtable = NIL;
+ List *context = NIL;
+
+ initStringInfo(&sql);
+ initStringInfo(&foreign_relname);
+
+ /*
+ * First of all, determine which qual can be pushed down.
+ *
+ * The expressions which satisfy is_foreign_expr() are deparsed into WHERE
+ * clause of result SQL string, and they could be removed from PlanState
+ * to avoid duplicate evaluation at ExecScan().
+ *
+ * We never change the quals in the Plan node, because this execution might
+ * be for a PREPAREd statement, thus the quals in the Plan node might be
+ * reused to construct another PlanState for subsequent EXECUTE statement.
+ *
+ * We do this before deparsing SELECT clause because attributes which are
+ * not used in neither reltargetlist nor baserel->baserestrictinfo, quals
+ * evaluated on local, can be replaced with literal "NULL" in the SELECT
+ * clause to reduce overhead of tuple handling tuple and data transfer.
+ */
+ if (baserel->baserestrictinfo != NIL)
+ {
+ ListCell *lc;
+ List *local_qual = NIL;
+
+ foreach (lc, baserel->baserestrictinfo)
+ {
+ RestrictInfo *ri = (RestrictInfo *) lfirst(lc);
+
+ /* Determine whether the qual can be pushed down or not. */
+ if (is_foreign_expr(root, baserel, ri->clause))
+ foreign_expr = lappend(foreign_expr, ri->clause);
+ else
+ {
+ List *attrs;
+
+ /*
+ * We need to know which attributes are used in qual evaluated
+ * on the local server, because they should be listed in the
+ * SELECT clause of remote query. We can ignore attributes
+ * which are referenced only in ORDER BY/GROUP BY clause because
+ * such attributes has already been kept in reltargetlist.
+ */
+ attrs = pull_var_clause((Node *) ri->clause,
+ PVC_RECURSE_AGGREGATES,
+ PVC_RECURSE_PLACEHOLDERS);
+ attr_used = list_union(attr_used, attrs);
+
+ /*
+ * Save the RestrictInfo to replace baserel->baserestrictinfo
+ * afterward.
+ */
+ local_qual = lappend(local_qual, ri);
+ }
+ }
+
+ /*
+ * Remove quals which are going to evaluated on the foreign server to
+ * avoid overhead of duplicated evaluation.
+ */
+ baserel->baserestrictinfo = local_qual;
+ }
+
+ /*
+ * Determine foreign relation's qualified name. This is necessary for
+ * FROM clause and SELECT clause.
+ */
+ nspname = GetFdwOptionValue(relid, InvalidAttrNumber, "nspname");
+ if (nspname == NULL)
+ nspname = get_namespace_name(get_rel_namespace(relid));
+ q_nspname = quote_identifier(nspname);
+
+ relname = GetFdwOptionValue(relid, InvalidAttrNumber, "relname");
+ if (relname == NULL)
+ relname = get_rel_name(relid);
+ q_relname = quote_identifier(relname);
+
+ appendStringInfo(&foreign_relname, "%s.%s", q_nspname, q_relname);
+
+ /*
+ * We need to replace aliasname and colnames of the target relation so that
+ * constructed remote query is valid.
+ *
+ * Note that we skip first empty element of simple_rel_array. See also
+ * comments of simple_rel_array and simple_rte_array for the rationale.
+ */
+ for (i = 1; i < root->simple_rel_array_size; i++)
+ {
+ RangeTblEntry *rte = copyObject(root->simple_rte_array[i]);
+ List *newcolnames = NIL;
+
+ if (i == baserel->relid)
+ {
+ /*
+ * Create new list of column names which is used to deparse remote
+ * query from specified names or local column names. This list is
+ * used by deparse_expression.
+ */
+ for (attr = 1; attr <= baserel->max_attr; attr++)
+ {
+ char *colname;
+
+ /* Ignore droppped attributes. */
+ if (get_rte_attribute_is_dropped(rte, attr))
+ continue;
+
+ colname = GetFdwOptionValue(relid, attr, "colname");
+ if (colname == NULL)
+ colname = strVal(list_nth(rte->eref->colnames, attr - 1));
+ newcolnames = lappend(newcolnames, makeString(colname));
+ }
+ rte->alias = makeAlias(relname, newcolnames);
+ }
+ rtable = lappend(rtable, rte);
+ }
+ context = deparse_context_for_rtelist(rtable);
+
+ /*
+ * deparse SELECT clause
+ *
+ * List attributes which are in either target list or local restriction.
+ * Unused attributes are replaced with a literal "NULL" for optimization.
+ */
+ appendStringInfo(&sql, "SELECT ");
+ attr_used = list_union(attr_used, baserel->reltargetlist);
+ first = true;
+ for (attr = 1; attr <= baserel->max_attr; attr++)
+ {
+ RangeTblEntry *rte = root->simple_rte_array[baserel->relid];
+ Var *var = NULL;
+ ListCell *lc;
+
+ /* Ignore droppped attributes. */
+ if (get_rte_attribute_is_dropped(rte, attr))
+ continue;
+
+ if (!first)
+ appendStringInfo(&sql, ", ");
+ first = false;
+
+ /*
+ * We use linear search here, but it wouldn't be problem since
+ * attr_used seems to not become so large.
+ */
+ foreach (lc, attr_used)
+ {
+ var = lfirst(lc);
+ if (var->varattno == attr)
+ break;
+ var = NULL;
+ }
+ if (var != NULL)
+ appendStringInfo(&sql, "%s",
+ deparse_expression((Node *) var, context, false, false));
+ else
+ appendStringInfo(&sql, "NULL");
+ }
+ appendStringInfoChar(&sql, ' ');
+
+ /*
+ * deparse FROM clause, including alias if any
+ */
+ appendStringInfo(&sql, "FROM %s", foreign_relname.data);
+
+ /*
+ * deparse WHERE clause
+ */
+ if (foreign_expr != NIL)
+ {
+ Node *node;
+
+ node = (Node *) make_ands_explicit(foreign_expr);
+ appendStringInfo(&sql, " WHERE %s ",
+ deparse_expression(node, context, false, false));
+ list_free(foreign_expr);
+ foreign_expr = NIL;
+ }
+
+ elog(DEBUG3, "Remote SQL: %s", sql.data);
+ return sql.data;
+ }
+
+ /*
+ * Returns true if expr is safe to be evaluated on the foreign server.
+ */
+ static bool
+ is_foreign_expr(PlannerInfo *root, RelOptInfo *baserel, Expr *expr)
+ {
+ foreign_executable_cxt context;
+ context.root = root;
+ context.foreignrel = baserel;
+
+ /*
+ * An expression which includes any mutable function can't be pushed down
+ * because it's result is not stable. For example, pushing now() down to
+ * remote side would cause confusion from the clock offset.
+ * If we have routine mapping infrastructure in future release, we will be
+ * able to choose function to be pushed down in finer granularity.
+ */
+ if (contain_mutable_functions((Node *) expr))
+ return false;
+
+ /*
+ * Check that the expression consists of nodes which are known as safe to
+ * be pushed down.
+ */
+ if (foreign_expr_walker((Node *) expr, &context))
+ return false;
+
+ return true;
+ }
+
+ /*
+ * Return true if node includes any node which is not known as safe to be
+ * pushed down.
+ */
+ static bool
+ foreign_expr_walker(Node *node, foreign_executable_cxt *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 subtre of the node, if any, should be checked
+ * continuously at the tail of this function.
+ */
+ break;
+ case T_Param:
+ /*
+ * Only external parameters can be pushed down.:
+ */
+ {
+ if (((Param *) node)->paramkind != PARAM_EXTERN)
+ return true;
+ }
+ break;
+ case T_OpExpr:
+ /*
+ * Operators which use non-immutable function can't be pushed down.
+ */
+ {
+ OpExpr *oe = (OpExpr *) node;
+
+ if (!is_proc_remotely_executable(oe->opfuncid))
+ return true;
+
+ /* operands are checked later */
+ }
+ break;
+ case T_FuncExpr:
+ /*
+ * Non-immutable functions can't be pushed down.
+ */
+ {
+ FuncExpr *fe = (FuncExpr *) node;
+
+ if (!is_proc_remotely_executable(fe->funcid))
+ return true;
+
+ /* operands are checked later */
+ }
+ 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;
+ foreign_executable_cxt *f_context;
+
+ f_context = (foreign_executable_cxt *) context;
+ if (var->varno != f_context->foreignrel->relid ||
+ var->varlevelsup != 0)
+ return true;
+ }
+ break;
+ default:
+ {
+ ereport(DEBUG3,
+ (errmsg("expression is too complex"),
+ errdetail("%s", nodeToString(node))));
+ return true;
+ }
+ break;
+ }
+
+ return expression_tree_walker(node, foreign_expr_walker, context);
+ }
+
+ /*
+ * Return true if func is known as safe to be pushed down if all of the
+ * arguments are also known as safe.
+ */
+ static bool
+ is_proc_remotely_executable(Oid procid)
+ {
+ /*
+ * User-defined procedures can't be pushed down.
+ */
+ if (procid >= FirstNormalObjectId)
+ return false;
+
+ return true;
+ }
+
diff --git a/contrib/pgsql_fdw/expected/pgsql_fdw.out b/contrib/pgsql_fdw/expected/pgsql_fdw.out
index ...e913892 .
*** a/contrib/pgsql_fdw/expected/pgsql_fdw.out
--- b/contrib/pgsql_fdw/expected/pgsql_fdw.out
***************
*** 0 ****
--- 1,406 ----
+ -- ===================================================================
+ -- create FDW objects
+ -- ===================================================================
+ CREATE EXTENSION pgsql_fdw;
+ CREATE SERVER loopback1 FOREIGN DATA WRAPPER pgsql_fdw;
+ CREATE SERVER loopback2 FOREIGN DATA WRAPPER pgsql_fdw
+ OPTIONS (dbname 'contrib_regression');
+ CREATE USER MAPPING FOR public SERVER loopback1
+ OPTIONS (user 'value', password 'value');
+ CREATE USER MAPPING FOR public SERVER loopback2;
+ CREATE FOREIGN TABLE ft1 (
+ c1 int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp
+ ) SERVER loopback2;
+ CREATE FOREIGN TABLE ft2 (
+ c1 int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp
+ ) SERVER loopback2;
+ -- ===================================================================
+ -- create objects used through FDW
+ -- ===================================================================
+ CREATE SCHEMA "S 1";
+ CREATE TABLE "S 1"."T 1" (
+ "C 1" int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ CONSTRAINT t1_pkey PRIMARY KEY ("C 1")
+ );
+ NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "t1_pkey" for table "T 1"
+ CREATE TABLE "S 1"."T 2" (
+ c1 int NOT NULL,
+ c2 text,
+ CONSTRAINT t2_pkey PRIMARY KEY (c1)
+ );
+ NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "t2_pkey" for table "T 2"
+ BEGIN;
+ TRUNCATE "S 1"."T 1";
+ INSERT INTO "S 1"."T 1"
+ SELECT id,
+ id % 10,
+ to_char(id, 'FM00000'),
+ '1970-01-01'::timestamptz + ((id % 100) || ' days')::interval,
+ '1970-01-01'::timestamp + ((id % 100) || ' days')::interval
+ FROM generate_series(1, 1000) id;
+ TRUNCATE "S 1"."T 2";
+ INSERT INTO "S 1"."T 2"
+ SELECT id,
+ 'AAA' || to_char(id, 'FM000')
+ FROM generate_series(1, 100) id;
+ COMMIT;
+ -- ===================================================================
+ -- tests for pgsql_fdw_validator
+ -- ===================================================================
+ ALTER FOREIGN DATA WRAPPER pgsql_fdw OPTIONS (host 'value'); -- ERRROR
+ ERROR: invalid option "host"
+ HINT: Valid options in this context are:
+ -- requiressl, krbsrvname and gsslib are omitted because they depend on
+ -- configure option
+ ALTER SERVER loopback1 OPTIONS (
+ authtype 'value',
+ service 'value',
+ connect_timeout 'value',
+ dbname 'value',
+ host 'value',
+ hostaddr 'value',
+ port 'value',
+ --client_encoding 'value',
+ tty 'value',
+ options 'value',
+ application_name 'value',
+ --fallback_application_name 'value',
+ keepalives 'value',
+ keepalives_idle 'value',
+ keepalives_interval 'value',
+ -- requiressl 'value',
+ sslmode 'value',
+ sslcert 'value',
+ sslkey 'value',
+ sslrootcert 'value',
+ sslcrl 'value'
+ --requirepeer 'value',
+ -- krbsrvname 'value',
+ -- gsslib 'value',
+ --replication 'value'
+ );
+ ALTER SERVER loopback1 OPTIONS (user 'value'); -- ERROR
+ ERROR: invalid option "user"
+ HINT: Valid options in this context are: authtype, service, connect_timeout, dbname, host, hostaddr, port, tty, options, application_name, keepalives, keepalives_idle, keepalives_interval, keepalives_count, sslmode, sslcert, sslkey, sslrootcert, sslcrl, requirepeer, fetch_count
+ ALTER SERVER loopback2 OPTIONS (ADD fetch_count '2');
+ ALTER USER MAPPING FOR public SERVER loopback1
+ OPTIONS (DROP user, DROP password);
+ ALTER USER MAPPING FOR public SERVER loopback1
+ OPTIONS (host 'value'); -- ERROR
+ ERROR: invalid option "host"
+ HINT: Valid options in this context are: user, password
+ ALTER FOREIGN TABLE ft1 OPTIONS (nspname 'S 1', relname 'T 1');
+ ALTER FOREIGN TABLE ft2 OPTIONS (nspname 'S 1', relname 'T 1', fetch_count '100');
+ ALTER FOREIGN TABLE ft1 OPTIONS (invalid 'value'); -- ERROR
+ ERROR: invalid option "invalid"
+ HINT: Valid options in this context are: nspname, relname, fetch_count
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count 'a'); -- ERROR
+ ERROR: invalid value for fetch_count: "a"
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count '0'); -- ERROR
+ ERROR: invalid value for fetch_count: "0"
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count '-1'); -- ERROR
+ ERROR: invalid value for fetch_count: "-1"
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (invalid 'value'); -- ERROR
+ ERROR: invalid option "invalid"
+ HINT: Valid options in this context are: colname
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (colname 'C 1');
+ ALTER FOREIGN TABLE ft2 ALTER COLUMN c1 OPTIONS (colname 'C 1');
+ \dew+
+ List of foreign-data wrappers
+ Name | Owner | Handler | Validator | Access privileges | FDW Options | Description
+ -----------+----------+-------------------+---------------------+-------------------+-------------+-------------
+ pgsql_fdw | postgres | pgsql_fdw_handler | pgsql_fdw_validator | | |
+ (1 row)
+
+ \des+
+ List of foreign servers
+ Name | Owner | Foreign-data wrapper | Access privileges | Type | Version | FDW Options | Description
+ -----------+----------+----------------------+-------------------+------+---------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+-------------
+ loopback1 | postgres | pgsql_fdw | | | | (authtype 'value', service 'value', connect_timeout 'value', dbname 'value', host 'value', hostaddr 'value', port 'value', tty 'value', options 'value', application_name 'value', keepalives 'value', keepalives_idle 'value', keepalives_interval 'value', sslmode 'value', sslcert 'value', sslkey 'value', sslrootcert 'value', sslcrl 'value') |
+ loopback2 | postgres | pgsql_fdw | | | | (dbname 'contrib_regression', fetch_count '2') |
+ (2 rows)
+
+ \deu+
+ List of user mappings
+ Server | User name | FDW Options
+ -----------+-----------+-------------
+ loopback1 | public |
+ loopback2 | public |
+ (2 rows)
+
+ \det+
+ List of foreign tables
+ Schema | Table | Server | FDW Options | Description
+ --------+-------+-----------+---------------------------------------------------+-------------
+ public | ft1 | loopback2 | (nspname 'S 1', relname 'T 1') |
+ public | ft2 | loopback2 | (nspname 'S 1', relname 'T 1', fetch_count '100') |
+ (2 rows)
+
+ -- ===================================================================
+ -- simple queries
+ -- ===================================================================
+ -- single table, with/without alias
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
+ QUERY PLAN
+ ----------------------------------------------------------------------------------------------------------------------
+ Limit
+ Output: c1, c2, c3, c4, c5
+ -> Sort
+ Output: c1, c2, c3, c4, c5
+ Sort Key: ft1.c3, ft1.c1
+ -> Foreign Scan on public.ft1
+ Output: c1, c2, c3, c4, c5
+ Remote SQL: DECLARE pgsql_fdw_cursor_0 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5 FROM "S 1"."T 1"
+ (8 rows)
+
+ SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
+ c1 | c2 | c3 | c4 | c5
+ -----+----+-------+------------------------------+--------------------------
+ 101 | 1 | 00101 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970
+ 102 | 2 | 00102 | Sat Jan 03 00:00:00 1970 PST | Sat Jan 03 00:00:00 1970
+ 103 | 3 | 00103 | Sun Jan 04 00:00:00 1970 PST | Sun Jan 04 00:00:00 1970
+ 104 | 4 | 00104 | Mon Jan 05 00:00:00 1970 PST | Mon Jan 05 00:00:00 1970
+ 105 | 5 | 00105 | Tue Jan 06 00:00:00 1970 PST | Tue Jan 06 00:00:00 1970
+ 106 | 6 | 00106 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970
+ 107 | 7 | 00107 | Thu Jan 08 00:00:00 1970 PST | Thu Jan 08 00:00:00 1970
+ 108 | 8 | 00108 | Fri Jan 09 00:00:00 1970 PST | Fri Jan 09 00:00:00 1970
+ 109 | 9 | 00109 | Sat Jan 10 00:00:00 1970 PST | Sat Jan 10 00:00:00 1970
+ 110 | 0 | 00110 | Sun Jan 11 00:00:00 1970 PST | Sun Jan 11 00:00:00 1970
+ (10 rows)
+
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+ QUERY PLAN
+ ----------------------------------------------------------------------------------------------------------------------
+ Limit
+ Output: c1, c2, c3, c4, c5
+ -> Sort
+ Output: c1, c2, c3, c4, c5
+ Sort Key: t1.c3, t1.c1
+ -> Foreign Scan on public.ft1 t1
+ Output: c1, c2, c3, c4, c5
+ Remote SQL: DECLARE pgsql_fdw_cursor_2 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5 FROM "S 1"."T 1"
+ (8 rows)
+
+ SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+ c1 | c2 | c3 | c4 | c5
+ -----+----+-------+------------------------------+--------------------------
+ 101 | 1 | 00101 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970
+ 102 | 2 | 00102 | Sat Jan 03 00:00:00 1970 PST | Sat Jan 03 00:00:00 1970
+ 103 | 3 | 00103 | Sun Jan 04 00:00:00 1970 PST | Sun Jan 04 00:00:00 1970
+ 104 | 4 | 00104 | Mon Jan 05 00:00:00 1970 PST | Mon Jan 05 00:00:00 1970
+ 105 | 5 | 00105 | Tue Jan 06 00:00:00 1970 PST | Tue Jan 06 00:00:00 1970
+ 106 | 6 | 00106 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970
+ 107 | 7 | 00107 | Thu Jan 08 00:00:00 1970 PST | Thu Jan 08 00:00:00 1970
+ 108 | 8 | 00108 | Fri Jan 09 00:00:00 1970 PST | Fri Jan 09 00:00:00 1970
+ 109 | 9 | 00109 | Sat Jan 10 00:00:00 1970 PST | Sat Jan 10 00:00:00 1970
+ 110 | 0 | 00110 | Sun Jan 11 00:00:00 1970 PST | Sun Jan 11 00:00:00 1970
+ (10 rows)
+
+ -- with WHERE clause
+ SELECT * FROM ft1 t1 WHERE t1.c1 = 101;
+ c1 | c2 | c3 | c4 | c5
+ -----+----+-------+------------------------------+--------------------------
+ 101 | 1 | 00101 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970
+ (1 row)
+
+ -- aggregate
+ SELECT COUNT(*) FROM ft1 t1;
+ count
+ -------
+ 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
+ ----+----+-------+------------------------------+--------------------------
+ 1 | 1 | 00001 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970
+ 2 | 2 | 00002 | Sat Jan 03 00:00:00 1970 PST | Sat Jan 03 00:00:00 1970
+ 3 | 3 | 00003 | Sun Jan 04 00:00:00 1970 PST | Sun Jan 04 00:00:00 1970
+ 4 | 4 | 00004 | Mon Jan 05 00:00:00 1970 PST | Mon Jan 05 00:00:00 1970
+ 5 | 5 | 00005 | Tue Jan 06 00:00:00 1970 PST | Tue Jan 06 00:00:00 1970
+ 6 | 6 | 00006 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970
+ 7 | 7 | 00007 | Thu Jan 08 00:00:00 1970 PST | Thu Jan 08 00:00:00 1970
+ 8 | 8 | 00008 | Fri Jan 09 00:00:00 1970 PST | Fri Jan 09 00:00:00 1970
+ 9 | 9 | 00009 | Sat Jan 10 00:00:00 1970 PST | Sat Jan 10 00:00:00 1970
+ 10 | 0 | 00010 | Sun Jan 11 00:00:00 1970 PST | Sun Jan 11 00:00:00 1970
+ (10 rows)
+
+ -- subquery+MAX
+ SELECT * FROM ft1 t1 WHERE t1.c3 = (SELECT MAX(c3) FROM ft2 t2) ORDER BY c1;
+ c1 | c2 | c3 | c4 | c5
+ ------+----+-------+------------------------------+--------------------------
+ 1000 | 0 | 01000 | Thu Jan 01 00:00:00 1970 PST | Thu Jan 01 00:00:00 1970
+ (1 row)
+
+ -- used in CTE
+ WITH t1 AS (SELECT * FROM ft1 WHERE c1 <= 10) SELECT t2.c1, t2.c2, t2.c3, t2.c4 FROM t1, ft2 t2 WHERE t1.c1 = t2.c1 ORDER BY t1.c1;
+ c1 | c2 | c3 | c4
+ ----+----+-------+------------------------------
+ 1 | 1 | 00001 | Fri Jan 02 00:00:00 1970 PST
+ 2 | 2 | 00002 | Sat Jan 03 00:00:00 1970 PST
+ 3 | 3 | 00003 | Sun Jan 04 00:00:00 1970 PST
+ 4 | 4 | 00004 | Mon Jan 05 00:00:00 1970 PST
+ 5 | 5 | 00005 | Tue Jan 06 00:00:00 1970 PST
+ 6 | 6 | 00006 | Wed Jan 07 00:00:00 1970 PST
+ 7 | 7 | 00007 | Thu Jan 08 00:00:00 1970 PST
+ 8 | 8 | 00008 | Fri Jan 09 00:00:00 1970 PST
+ 9 | 9 | 00009 | Sat Jan 10 00:00:00 1970 PST
+ 10 | 0 | 00010 | Sun Jan 11 00:00:00 1970 PST
+ (10 rows)
+
+ -- fixed values
+ SELECT 'fixed', NULL FROM ft1 t1 WHERE c1 = 1;
+ ?column? | ?column?
+ ----------+----------
+ fixed |
+ (1 row)
+
+ -- ===================================================================
+ -- parameterized queries
+ -- ===================================================================
+ -- simple join
+ PREPARE st1(int, int) AS SELECT t1.c3, t2.c3 FROM ft1 t1, ft2 t2 WHERE t1.c1 = $1 AND t2.c1 = $2;
+ EXPLAIN (COSTS false) EXECUTE st1(1, 2);
+ QUERY PLAN
+ -----------------------------------------------------------------------------------------------------------------------------------------
+ Nested Loop
+ -> Foreign Scan on ft1 t1
+ Remote SQL: DECLARE pgsql_fdw_cursor_17 SCROLL CURSOR FOR SELECT NULL, NULL, c3, NULL, NULL FROM "S 1"."T 1" WHERE ("C 1" = 1)
+ -> Foreign Scan on ft2 t2
+ Remote SQL: DECLARE pgsql_fdw_cursor_18 SCROLL CURSOR FOR SELECT NULL, NULL, c3, NULL, NULL FROM "S 1"."T 1" WHERE ("C 1" = 2)
+ (5 rows)
+
+ EXECUTE st1(1, 1);
+ c3 | c3
+ -------+-------
+ 00001 | 00001
+ (1 row)
+
+ EXECUTE st1(101, 101);
+ c3 | c3
+ -------+-------
+ 00101 | 00101
+ (1 row)
+
+ -- subquery using stable function (can't be pushed down)
+ PREPARE st2(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c4) = 6) ORDER BY c1;
+ EXPLAIN (COSTS false) EXECUTE st2(10, 20);
+ QUERY PLAN
+ ----------------------------------------------------------------------------------------------------------------------------------------------------
+ Sort
+ Sort Key: t1.c1
+ -> Nested Loop
+ Join Filter: (t1.c3 = t2.c3)
+ -> HashAggregate
+ -> Foreign Scan on ft2 t2
+ Filter: (date_part('dow'::text, c4) = 6::double precision)
+ Remote SQL: DECLARE pgsql_fdw_cursor_24 SCROLL CURSOR FOR SELECT NULL, NULL, c3, c4, NULL FROM "S 1"."T 1" WHERE ("C 1" > 10)
+ -> Foreign Scan on ft1 t1
+ Remote SQL: DECLARE pgsql_fdw_cursor_23 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5 FROM "S 1"."T 1" WHERE ("C 1" < 20)
+ (10 rows)
+
+ EXECUTE st2(10, 20);
+ c1 | c2 | c3 | c4 | c5
+ ----+----+-------+------------------------------+--------------------------
+ 16 | 6 | 00016 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970
+ (1 row)
+
+ EXECUTE st1(101, 101);
+ c3 | c3
+ -------+-------
+ 00101 | 00101
+ (1 row)
+
+ -- subquery using immutable function (can be pushed down)
+ PREPARE st3(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c5) = 6) ORDER BY c1;
+ EXPLAIN (COSTS false) EXECUTE st3(10, 20);
+ QUERY PLAN
+ ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Sort
+ Sort Key: t1.c1
+ -> Hash Join
+ Hash Cond: (t1.c3 = t2.c3)
+ -> Foreign Scan on ft1 t1
+ Remote SQL: DECLARE pgsql_fdw_cursor_29 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5 FROM "S 1"."T 1" WHERE ("C 1" < 20)
+ -> Hash
+ -> HashAggregate
+ -> Foreign Scan on ft2 t2
+ Remote SQL: DECLARE pgsql_fdw_cursor_30 SCROLL CURSOR FOR SELECT NULL, NULL, c3, NULL, NULL FROM "S 1"."T 1" WHERE (("C 1" > 10) AND (date_part('dow'::text, c5) = 6::double precision))
+ (10 rows)
+
+ EXECUTE st3(10, 20);
+ c1 | c2 | c3 | c4 | c5
+ ----+----+-------+------------------------------+--------------------------
+ 16 | 6 | 00016 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970
+ (1 row)
+
+ EXECUTE st3(20, 30);
+ c1 | c2 | c3 | c4 | c5
+ ----+----+-------+------------------------------+--------------------------
+ 23 | 3 | 00023 | Sat Jan 24 00:00:00 1970 PST | Sat Jan 24 00:00:00 1970
+ (1 row)
+
+ -- cleanup
+ DEALLOCATE st1;
+ DEALLOCATE st2;
+ DEALLOCATE st3;
+ -- ===================================================================
+ -- connection management
+ -- ===================================================================
+ SELECT srvname, usename FROM pgsql_fdw_connections;
+ srvname | usename
+ -----------+----------
+ loopback2 | postgres
+ (1 row)
+
+ SELECT pgsql_fdw_disconnect(srvid, usesysid) FROM pgsql_fdw_get_connections();
+ pgsql_fdw_disconnect
+ ----------------------
+ OK
+ (1 row)
+
+ SELECT srvname, usename FROM pgsql_fdw_connections;
+ srvname | usename
+ ---------+---------
+ (0 rows)
+
+ -- ===================================================================
+ -- cleanup
+ -- ===================================================================
+ DROP EXTENSION pgsql_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 server loopback2
+ drop cascades to user mapping for public
+ drop cascades to foreign table ft1
+ drop cascades to foreign table ft2
diff --git a/contrib/pgsql_fdw/option.c b/contrib/pgsql_fdw/option.c
index ...5eac503 .
*** a/contrib/pgsql_fdw/option.c
--- b/contrib/pgsql_fdw/option.c
***************
*** 0 ****
--- 1,254 ----
+ /*-------------------------------------------------------------------------
+ *
+ * option.c
+ * FDW option handling
+ *
+ * Copyright (c) 2011, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/option.c
+ *
+ *-------------------------------------------------------------------------
+ */
+ #include "postgres.h"
+
+ #include "access/reloptions.h"
+ #include "catalog/pg_foreign_data_wrapper.h"
+ #include "catalog/pg_foreign_server.h"
+ #include "catalog/pg_foreign_table.h"
+ #include "catalog/pg_user_mapping.h"
+ #include "fmgr.h"
+ #include "foreign/foreign.h"
+ #include "lib/stringinfo.h"
+ #include "miscadmin.h"
+
+ #include "pgsql_fdw.h"
+
+ /*
+ * SQL functions
+ */
+ extern Datum pgsql_fdw_validator(PG_FUNCTION_ARGS);
+ PG_FUNCTION_INFO_V1(pgsql_fdw_validator);
+
+ /*
+ * Describes the valid options for objects that use this wrapper.
+ */
+ typedef struct PgsqlFdwOption
+ {
+ const char *optname;
+ Oid optcontext; /* Oid of catalog in which options may appear */
+ bool is_libpq_opt; /* true if it's used in libpq */
+ } PgsqlFdwOption;
+
+ /*
+ * Valid options for pgsql_fdw.
+ */
+ static PgsqlFdwOption valid_options[] = {
+
+ /*
+ * Options for libpq connection.
+ * Note: This list should be updated along with PQconninfoOptions in
+ * interfaces/libpq/fe-connect.c, so the order is kept as is.
+ *
+ * Some useless libpq connection options are not accepted by pgsql_fdw:
+ * client_encoding: set to local database encoding automatically
+ * fallback_application_name: fixed to "pgsql_fdw"
+ * replication: pgsql_fdw never be replication client
+ */
+ {"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},
+ #ifdef NOT_USED
+ {"client_encoding", ForeignServerRelationId, true},
+ #endif
+ {"tty", ForeignServerRelationId, true},
+ {"options", ForeignServerRelationId, true},
+ {"application_name", ForeignServerRelationId, true},
+ #ifdef NOT_USED
+ {"fallback_application_name", ForeignServerRelationId, true},
+ #endif
+ {"keepalives", ForeignServerRelationId, true},
+ {"keepalives_idle", ForeignServerRelationId, true},
+ {"keepalives_interval", ForeignServerRelationId, true},
+ {"keepalives_count", ForeignServerRelationId, true},
+ #ifdef USE_SSL
+ {"requiressl", ForeignServerRelationId, true},
+ #endif
+ {"sslmode", ForeignServerRelationId, true},
+ {"sslcert", ForeignServerRelationId, true},
+ {"sslkey", ForeignServerRelationId, true},
+ {"sslrootcert", ForeignServerRelationId, true},
+ {"sslcrl", ForeignServerRelationId, true},
+ {"requirepeer", ForeignServerRelationId, true},
+ #if defined(KRB5) || defined(ENABLE_GSS) || defined(ENABLE_SSPI)
+ {"krbsrvname", ForeignServerRelationId, true},
+ #endif
+ #if defined(ENABLE_GSS) && defined(ENABLE_SSPI)
+ {"gsslib", ForeignServerRelationId, true},
+ #endif
+ #ifdef NOT_USED
+ {"replication", ForeignServerRelationId, true},
+ #endif
+
+ /*
+ * Options for translation of object names.
+ */
+ {"nspname", ForeignTableRelationId, false},
+ {"relname", ForeignTableRelationId, false},
+ {"colname", AttributeRelationId, false},
+
+ /*
+ * Options for cursor behavior.
+ * These options can be overridden by finer-grained objects.
+ */
+ {"fetch_count", ForeignTableRelationId, false},
+ {"fetch_count", ForeignServerRelationId, false},
+
+ /* Terminating entry --- MUST BE LAST */
+ {NULL, InvalidOid, false}
+ };
+
+ /*
+ * Helper functions
+ */
+ static bool is_valid_option(const char *optname, Oid context);
+
+ /*
+ * Validate the generic options given to a FOREIGN DATA WRAPPER, SERVER,
+ * USER MAPPING or FOREIGN TABLE that uses pgsql_fdw.
+ *
+ * Raise an ERROR if the option or its value is considered invalid.
+ */
+ Datum
+ pgsql_fdw_validator(PG_FUNCTION_ARGS)
+ {
+ List *options_list = untransformRelOptions(PG_GETARG_DATUM(0));
+ Oid catalog = PG_GETARG_OID(1);
+ ListCell *cell;
+
+ /*
+ * Check that only options supported by pgsql_fdw, and allowed for the
+ * current object type, are given.
+ */
+ foreach(cell, options_list)
+ {
+ DefElem *def = (DefElem *) lfirst(cell);
+
+ if (!is_valid_option(def->defname, catalog))
+ {
+ PgsqlFdwOption *opt;
+ StringInfoData buf;
+
+ /*
+ * Unknown option specified, complain about it. Provide a hint
+ * with list of valid options for the object.
+ */
+ initStringInfo(&buf);
+ for (opt = valid_options; opt->optname; opt++)
+ {
+ if (catalog == opt->optcontext)
+ appendStringInfo(&buf, "%s%s", (buf.len > 0) ? ", " : "",
+ opt->optname);
+ }
+
+ ereport(ERROR,
+ (errcode(ERRCODE_FDW_INVALID_OPTION_NAME),
+ errmsg("invalid option \"%s\"", def->defname),
+ errhint("Valid options in this context are: %s",
+ buf.data)));
+ }
+
+ /* fetch_count be positive digit number. */
+ if (strcmp(def->defname, "fetch_count") == 0)
+ {
+ long value;
+ char *p = NULL;
+
+ value = strtol(strVal(def->arg), &p, 10);
+ if (*p != '\0' || value < 1)
+ ereport(ERROR,
+ (errcode(ERRCODE_FDW_INVALID_ATTRIBUTE_VALUE),
+ errmsg("invalid value for %s: \"%s\"",
+ def->defname, strVal(def->arg))));
+ }
+ }
+
+ /*
+ * We don't care option-specific limitation here; they will be validated at
+ * the execution time.
+ */
+
+ PG_RETURN_VOID();
+ }
+
+ /*
+ * Check if the provided option is one of the valid options.
+ * context is the Oid of the catalog holding the object the option is for.
+ */
+ static bool
+ is_valid_option(const char *optname, Oid context)
+ {
+ PgsqlFdwOption *opt;
+
+ /*
+ * If the option was other than libpq option, look up option table and
+ * determine valid context.
+ */
+ for (opt = valid_options; opt->optname; opt++)
+ {
+ if (context == opt->optcontext && strcmp(opt->optname, optname) == 0)
+ return true;
+ }
+ return false;
+ }
+
+ /*
+ * Check if the provided option is one of the valid options.
+ * context is the Oid of the catalog holding the object the option is for.
+ */
+ static bool
+ is_libpq_option(const char *optname)
+ {
+ PgsqlFdwOption *opt;
+
+ /*
+ * If the option was other than libpq option, look up option table and
+ * determine valid context.
+ */
+ for (opt = valid_options; opt->optname; opt++)
+ {
+ if (strcmp(opt->optname, optname) == 0 && opt->is_libpq_opt)
+ return true;
+ }
+ return false;
+ }
+
+ /*
+ * Generate key-value arrays which includes only libpq options from the list
+ * which contains any kind of options.
+ */
+ int
+ ExtractConnectionOptions(List *defelems, const char **keywords, const char **values)
+ {
+ ListCell *lc;
+ int i;
+
+ i = 0;
+ foreach(lc, defelems)
+ {
+ DefElem *d = (DefElem *) lfirst(lc);
+ if (is_libpq_option(d->defname))
+ {
+ keywords[i] = d->defname;
+ values[i] = strVal(d->arg);
+ i++;
+ }
+ }
+ return i;
+ }
diff --git a/contrib/pgsql_fdw/pgsql_fdw--1.0.sql b/contrib/pgsql_fdw/pgsql_fdw--1.0.sql
index ...87dfa29 .
*** a/contrib/pgsql_fdw/pgsql_fdw--1.0.sql
--- b/contrib/pgsql_fdw/pgsql_fdw--1.0.sql
***************
*** 0 ****
--- 1,36 ----
+ /* contrib/pgsql_fdw/pgsql_fdw--1.0.sql */
+
+ CREATE FUNCTION pgsql_fdw_handler()
+ RETURNS fdw_handler
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C STRICT;
+
+ CREATE FUNCTION pgsql_fdw_validator(text[], oid)
+ RETURNS void
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C STRICT;
+
+ CREATE FOREIGN DATA WRAPPER pgsql_fdw
+ HANDLER pgsql_fdw_handler
+ VALIDATOR pgsql_fdw_validator;
+
+ /* connection management functions and view */
+ CREATE FUNCTION pgsql_fdw_get_connections(out srvid oid, out usesysid oid)
+ RETURNS SETOF record
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C STRICT;
+
+ CREATE FUNCTION pgsql_fdw_disconnect(oid, oid)
+ RETURNS text
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C STRICT;
+
+ CREATE VIEW pgsql_fdw_connections AS
+ SELECT c.srvid srvid,
+ s.srvname srvname,
+ c.usesysid usesysid,
+ pg_get_userbyid(c.usesysid) usename
+ FROM pgsql_fdw_get_connections() c
+ JOIN pg_catalog.pg_foreign_server s ON (s.oid = c.srvid);
+ GRANT SELECT ON pgsql_fdw_connections TO public;
+
diff --git a/contrib/pgsql_fdw/pgsql_fdw.c b/contrib/pgsql_fdw/pgsql_fdw.c
index ...18a6d85 .
*** a/contrib/pgsql_fdw/pgsql_fdw.c
--- b/contrib/pgsql_fdw/pgsql_fdw.c
***************
*** 0 ****
--- 1,811 ----
+ /*-------------------------------------------------------------------------
+ *
+ * pgsql_fdw.c
+ * foreign-data wrapper for remote PostgreSQL servers.
+ *
+ * Copyright (c) 2011, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/pgsql_fdw.c
+ *
+ *-------------------------------------------------------------------------
+ */
+ #include "postgres.h"
+ #include "fmgr.h"
+
+ #include "catalog/pg_foreign_server.h"
+ #include "catalog/pg_foreign_table.h"
+ #include "commands/explain.h"
+ #include "foreign/fdwapi.h"
+ #include "funcapi.h"
+ #include "miscadmin.h"
+ #include "optimizer/cost.h"
+ #include "utils/lsyscache.h"
+ #include "utils/memutils.h"
+ #include "utils/rel.h"
+
+ #include "pgsql_fdw.h"
+ #include "connection.h"
+
+ PG_MODULE_MAGIC;
+
+ /*
+ * Default fetch count for cursor. This can be overrideen by fetch_count FDW
+ * option.
+ */
+ #define DEFAULT_FETCH_COUNT 1000
+
+ /*
+ * Cost to establish a connection.
+ * XXX: should be configurable per server?
+ */
+ #define CONNECTION_COSTS 100.0
+
+ /*
+ * Cost to transfer 1 byte from remote server.
+ * XXX: should be configurable per server?
+ */
+ #define TRANSFER_COSTS_PER_BYTE 0.001
+
+ /*
+ * Cursors which are used together in a local query require different name, so
+ * we use simple incremental name for that purpose. We don't care wrap around
+ * of cursor_id because it's hard to imagine that 2^32 cursors are used in a
+ * query.
+ */
+ #define CURSOR_NAME_FORMAT "pgsql_fdw_cursor_%u"
+ static uint32 cursor_id = 0;
+
+ /*
+ * Index of FDW-private items stored in FdwPlan.
+ */
+ enum FdwPrivateIndex {
+ FdwPrivateSelectSql,
+ FdwPrivateDeclareSql,
+ FdwPrivateFetchSql,
+ FdwPrivateResetSql,
+ FdwPrivateCloseSql,
+
+ /* # of elements stored in the list fdw_private */
+ FdwPrivateNum,
+ };
+
+ /*
+ * Describes an execution state of a foreign scan against a foreign table
+ * using pgsql_fdw.
+ */
+ typedef struct PgsqlFdwExecutionState
+ {
+ FdwPlan *fdwplan; /* FDW-specific planning information */
+ PGconn *conn; /* connection for the scan */
+
+ Oid *param_types; /* type array of external parameter */
+ const char **param_values; /* value array of external parameter */
+
+ int attnum; /* # of non-dropped attribute */
+ char **col_values; /* column value buffer */
+ AttInMetadata *attinmeta; /* attribute metadata */
+
+ Tuplestorestate *tuples; /* result of the scan */
+ bool cursor_opened; /* true if cursor has benn opened */
+ } PgsqlFdwExecutionState;
+
+ /*
+ * SQL functions
+ */
+ extern Datum pgsql_fdw_handler(PG_FUNCTION_ARGS);
+ PG_FUNCTION_INFO_V1(pgsql_fdw_handler);
+
+ /*
+ * FDW callback routines
+ */
+ static FdwPlan *pgsqlPlanForeignScan(Oid foreigntableid,
+ PlannerInfo *root,
+ RelOptInfo *baserel);
+ static void pgsqlExplainForeignScan(ForeignScanState *node, ExplainState *es);
+ static void pgsqlBeginForeignScan(ForeignScanState *node, int eflags);
+ static TupleTableSlot *pgsqlIterateForeignScan(ForeignScanState *node);
+ static void pgsqlReScanForeignScan(ForeignScanState *node);
+ static void pgsqlEndForeignScan(ForeignScanState *node);
+
+ /*
+ * Helper functions
+ */
+ static void estimate_costs(PlannerInfo *root,
+ RelOptInfo *baserel,
+ const char *sql,
+ Oid serverid,
+ Cost *startup_cost,
+ Cost *total_cost);
+ static void execute_query(ForeignScanState *node);
+ static PGresult *fetch_result(ForeignScanState *node);
+ static void store_result(ForeignScanState *node, PGresult *res);
+
+ /*
+ * Foreign-data wrapper handler function: return a struct with pointers
+ * to my callback routines.
+ */
+ Datum
+ pgsql_fdw_handler(PG_FUNCTION_ARGS)
+ {
+ FdwRoutine *fdwroutine = makeNode(FdwRoutine);
+
+ fdwroutine->PlanForeignScan = pgsqlPlanForeignScan;
+ fdwroutine->ExplainForeignScan = pgsqlExplainForeignScan;
+ fdwroutine->BeginForeignScan = pgsqlBeginForeignScan;
+ fdwroutine->IterateForeignScan = pgsqlIterateForeignScan;
+ fdwroutine->ReScanForeignScan = pgsqlReScanForeignScan;
+ fdwroutine->EndForeignScan = pgsqlEndForeignScan;
+
+ PG_RETURN_POINTER(fdwroutine);
+ }
+
+ /*
+ * pgsqlPlanForeignScan
+ * Create a FdwPlan for a scan on the foreign table
+ */
+ static FdwPlan *
+ pgsqlPlanForeignScan(Oid foreigntableid,
+ PlannerInfo *root,
+ RelOptInfo *baserel)
+ {
+ char name[128]; /* must be larger than format + 10 */
+ StringInfoData cursor;
+ const char *fetch_count_str;
+ int fetch_count = DEFAULT_FETCH_COUNT;
+ char *sql;
+ FdwPlan *fdwplan;
+ List *fdw_private = NIL;
+ ForeignTable *table;
+ ForeignServer *server;
+
+ /* Construct FdwPlan with cost estimates */
+ fdwplan = makeNode(FdwPlan);
+ sql = deparseSql(foreigntableid, root, baserel);
+ table = GetForeignTable(foreigntableid);
+ server = GetForeignServer(table->serverid);
+ estimate_costs(root, baserel, sql, server->serverid,
+ &fdwplan->startup_cost, &fdwplan->total_cost);
+
+ /*
+ * Store plain SELECT statement in private area of FdwPlan. This will be
+ * used for executing remote query and explaining scan.
+ */
+ fdw_private = list_make1(makeString(sql));
+
+ /* Use specified fetch_count instead of default value, if any. */
+ fetch_count_str = GetFdwOptionValue(foreigntableid,
+ InvalidAttrNumber,
+ "fetch_count");
+ if (fetch_count_str != NULL)
+ fetch_count = strtol(fetch_count_str, NULL, 10);
+ elog(DEBUG1,
+ "relid=%u fetch_count=%d",
+ foreigntableid,
+ fetch_count);
+
+ /*
+ * We store some more information in FdwPlan to pass them beyond the
+ * boundary between planner and executor. Finally FdwPlan using cursor
+ * would hold items below:
+ *
+ * 1) plain SELECT statement (already added above)
+ * 2) SQL statement used to declare cursor
+ * 3) SQL statement used to fetch rows from cursor
+ * 4) SQL statement used to reset cursor
+ * 5) SQL statement used to close cursor
+ *
+ * These items are indexed with the enum FdwPrivateIndex, so an item
+ * can be accessed directly via list_nth(). For example of FETCH
+ * statement:
+ * list_nth(fdw_private, FdwPrivateFetchSql)
+ */
+
+ /* Construct cursor name from sequential value */
+ sprintf(name, CURSOR_NAME_FORMAT, cursor_id++);
+
+ /* Construct statement to declare cursor */
+ initStringInfo(&cursor);
+ appendStringInfo(&cursor, "DECLARE %s SCROLL CURSOR FOR %s", name, sql);
+ fdw_private = lappend(fdw_private, makeString(cursor.data));
+
+ /* Construct statement to fetch rows from cursor */
+ initStringInfo(&cursor);
+ appendStringInfo(&cursor, "FETCH %d FROM %s", fetch_count, name);
+ fdw_private = lappend(fdw_private, makeString(cursor.data));
+
+ /* Construct statement to reset cursor */
+ initStringInfo(&cursor);
+ appendStringInfo(&cursor, "MOVE ABSOLUTE 0 FROM %s", name);
+ fdw_private = lappend(fdw_private, makeString(cursor.data));
+
+ /* Construct statement to close cursor */
+ initStringInfo(&cursor);
+ appendStringInfo(&cursor, "CLOSE %s", name);
+ fdw_private = lappend(fdw_private, makeString(cursor.data));
+
+ /* Store FDW private information into FdwPlan */
+ fdwplan->fdw_private = fdw_private;
+
+ return fdwplan;
+ }
+
+ /*
+ * pgsqlExplainForeignScan
+ * Produce extra output for EXPLAIN
+ */
+ static void
+ pgsqlExplainForeignScan(ForeignScanState *node, ExplainState *es)
+ {
+ FdwPlan *fdwplan;
+ char *sql;
+
+ fdwplan = ((ForeignScan *) node->ss.ps.plan)->fdwplan;
+ sql = strVal(list_nth(fdwplan->fdw_private, FdwPrivateDeclareSql));
+ ExplainPropertyText("Remote SQL", sql, es);
+ }
+
+ /*
+ * pgsqlBeginForeignScan
+ * Initiate access to a foreign PostgreSQL table.
+ */
+ static void
+ pgsqlBeginForeignScan(ForeignScanState *node, int eflags)
+ {
+ PgsqlFdwExecutionState *festate;
+ PGconn *conn;
+ Oid relid;
+ ForeignTable *table;
+ ForeignServer *server;
+ UserMapping *user;
+ TupleTableSlot *slot = node->ss.ss_ScanTupleSlot;
+
+ /*
+ * Do nothing in EXPLAIN (no ANALYZE) case. node->fdw_state stays NULL.
+ */
+ if (eflags & EXEC_FLAG_EXPLAIN_ONLY)
+ return;
+
+ /*
+ * Save state in node->fdw_state.
+ */
+ festate = (PgsqlFdwExecutionState *) palloc(sizeof(PgsqlFdwExecutionState));
+ festate->fdwplan = ((ForeignScan *) node->ss.ps.plan)->fdwplan;
+
+ /*
+ * Get connection to the foreign server. Connection manager would
+ * establish new connection if necessary.
+ */
+ relid = RelationGetRelid(node->ss.ss_currentRelation);
+ table = GetForeignTable(relid);
+ server = GetForeignServer(table->serverid);
+ user = GetUserMapping(GetOuterUserId(), server->serverid);
+ conn = GetConnection(server, user);
+ festate->conn = conn;
+
+ /* Result will be filled in first Iterate call. */
+ festate->tuples = NULL;
+ festate->cursor_opened = false;
+
+ /* Allocate buffers for column values. */
+ {
+ TupleDesc tupdesc = slot->tts_tupleDescriptor;
+ festate->col_values = palloc(sizeof(char *) * tupdesc->natts);
+ festate->attinmeta = TupleDescGetAttInMetadata(tupdesc);
+ }
+
+ /* Allocate buffers for query parameters. */
+ {
+ ParamListInfo params = node->ss.ps.state->es_param_list_info;
+ int numParams = params ? params->numParams : 0;
+
+ if (numParams > 0)
+ {
+ festate->param_types = palloc0(sizeof(Oid) * numParams);
+ festate->param_values = palloc0(sizeof(char *) * numParams);
+ }
+ else
+ {
+ festate->param_types = NULL;
+ festate->param_values = NULL;
+ }
+ }
+
+
+ /* Store FDW-specific state into ForeignScanState */
+ node->fdw_state = (void *) festate;
+
+ return;
+ }
+
+ /*
+ * pgsqlIterateForeignScan
+ * Retrieve next row from the result set, or clear tuple slot to indicate
+ * EOF.
+ *
+ * Note that using per-query context when retrieving tuples from
+ * tuplestore to ensure that returned tuples can survive until next
+ * iteration because the tuple is released implicitly via ExecClearTuple.
+ * Retrieving a tuple from tuplestore in CurrentMemoryContext (it's a
+ * per-tuple context), ExecClearTuple will free dangling pointer.
+ */
+ static TupleTableSlot *
+ pgsqlIterateForeignScan(ForeignScanState *node)
+ {
+ PgsqlFdwExecutionState *festate;
+ TupleTableSlot *slot = node->ss.ss_ScanTupleSlot;
+ PGresult *res;
+ MemoryContext oldcontext = CurrentMemoryContext;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+
+
+ /*
+ * If this is the first call after Begin, we need to execute remote query.
+ * If the query needs cursor, we declare a cursor at first call and fetch
+ * from it in later calls.
+ */
+ if (festate->tuples == NULL && !festate->cursor_opened)
+ execute_query(node);
+
+ /*
+ * If enough tuples are left in tuplestore, just return next tuple from it.
+ */
+ MemoryContextSwitchTo(node->ss.ps.state->es_query_cxt);
+ if (tuplestore_gettupleslot(festate->tuples, true, false, slot))
+ {
+ MemoryContextSwitchTo(oldcontext);
+ return slot;
+ }
+ MemoryContextSwitchTo(oldcontext);
+
+ /*
+ * Here we need to clear partial result and fetch next bunch of tuples from
+ * from the cursor for the scan. If the fetch returns no tuple, the scan
+ * has reached the end.
+ */
+ res = fetch_result(node);
+ PG_TRY();
+ {
+ store_result(node, res);
+ PQclear(res);
+ res = NULL;
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+
+ /*
+ * If we got more tuples from the server cursor, return next tuple from
+ * tuplestore.
+ */
+ MemoryContextSwitchTo(node->ss.ps.state->es_query_cxt);
+ if (tuplestore_gettupleslot(festate->tuples, true, false, slot))
+ {
+ MemoryContextSwitchTo(oldcontext);
+ return slot;
+ }
+ MemoryContextSwitchTo(oldcontext);
+
+ /* We don't have any result even in remote server cursor. */
+ ExecClearTuple(slot);
+ return slot;
+ }
+
+ /*
+ * pgsqlReScanForeignScan
+ * - Restart this scan by resetting fetch location.
+ */
+ static void
+ pgsqlReScanForeignScan(ForeignScanState *node)
+ {
+ List *fdw_private;
+ char *sql;
+ PGconn *conn;
+ PGresult *res;
+ PgsqlFdwExecutionState *festate;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+
+ /* If we have not opened cursor yet, nothing to do. */
+ if (!festate->cursor_opened)
+ return;
+
+ /* Discard fetch results if any. */
+ if (festate->tuples != NULL)
+ tuplestore_clear(festate->tuples);
+
+ /* Reset cursor */
+ fdw_private = festate->fdwplan->fdw_private;
+ conn = festate->conn;
+ sql = strVal(list_nth(fdw_private, FdwPrivateResetSql));
+ res = PQexec(conn, sql);
+ PG_TRY();
+ {
+ if (res == NULL || PQresultStatus(res) != PGRES_COMMAND_OK)
+ {
+ ereport(ERROR,
+ (errmsg("could not rewind cursor"),
+ errdetail("%s", PQerrorMessage(conn)),
+ errhint("%s", sql)));
+ }
+ PQclear(res);
+ res = NULL;
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+ }
+
+ /*
+ * pgsqlEndForeignScan
+ * Finish scanning foreign table and dispose objects used for this scan
+ */
+ static void
+ pgsqlEndForeignScan(ForeignScanState *node)
+ {
+ List *fdw_private;
+ char *sql;
+ PGconn *conn;
+ PGresult *res;
+ PgsqlFdwExecutionState *festate;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+
+ /* if festate is NULL, we are in EXPLAIN; nothing to do */
+ if (festate == NULL)
+ return;
+
+ /* Discard fetch results */
+ if (festate->tuples != NULL)
+ {
+ tuplestore_end(festate->tuples);
+ festate->tuples = NULL;
+ }
+
+ /* Close cursor */
+ fdw_private = festate->fdwplan->fdw_private;
+ conn = festate->conn;
+ sql = strVal(list_nth(fdw_private, FdwPrivateCloseSql));
+ res = PQexec(conn, sql);
+ PG_TRY();
+ {
+ if (res == NULL || PQresultStatus(res) != PGRES_COMMAND_OK)
+ {
+ ereport(ERROR,
+ (errmsg("could not close cursor"),
+ errdetail("%s", PQerrorMessage(conn)),
+ errhint("%s", sql)));
+ }
+ PQclear(res);
+ res = NULL;
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+
+ ReleaseConnection(festate->conn);
+ }
+
+ /*
+ * Estimate costs of scanning a foreign table.
+ */
+ static void
+ estimate_costs(PlannerInfo *root, RelOptInfo *baserel,
+ const char *sql, Oid serverid,
+ Cost *startup_cost, Cost *total_cost)
+ {
+ ForeignServer *server;
+ UserMapping *user;
+ PGconn *conn = NULL;
+ PGresult *res = NULL;
+ StringInfoData buf;
+ char *plan;
+ char *p;
+ char *endp;
+
+ /*
+ * Get connection to the foreign server. Connection manager would
+ * establish new connection if necessary.
+ */
+ server = GetForeignServer(serverid);
+ user = GetUserMapping(GetOuterUserId(), server->serverid);
+ conn = GetConnection(server, user);
+ initStringInfo(&buf);
+ appendStringInfo(&buf, "EXPLAIN (FORMAT YAML) %s", sql);
+
+ PG_TRY();
+ {
+ res = PQexec(conn, buf.data);
+ if (res == NULL || PQresultStatus(res) != PGRES_TUPLES_OK)
+ {
+ char *msg;
+
+ msg = pstrdup(PQerrorMessage(conn));
+ ereport(ERROR,
+ (errmsg("could not execute EXPLAIN for cost estimation"),
+ errdetail("%s", msg),
+ errhint("%s", sql)));
+ }
+ plan = pstrdup(PQgetvalue(res, 0, 0));
+ PQclear(res);
+ res = NULL;
+ ReleaseConnection(conn);
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+
+ /* extract startup cost from remote plan */
+ p = strstr(plan, "Startup Cost: ");
+ if (p != NULL)
+ {
+ p += strlen("Startup Cost: ");
+ *startup_cost = strtod(p, &endp);
+ if (*endp != '\n' && *endp != '\0')
+ elog(ERROR, "invalid plan format for startup cost%c", *endp);
+ }
+
+ /* extract total cost from remote plan */
+ p = strstr(plan, "Total Cost: ");
+ if (p != NULL)
+ {
+ p += strlen("Total Cost: ");
+ *total_cost = strtod(p, &endp);
+ if (*endp != '\n' && *endp != '\0')
+ elog(ERROR, "invalid plan format for total cost");
+ }
+
+ /* extract # of rows from remote plan */
+ p = strstr(plan, "Plan Rows: ");
+ if (p != NULL)
+ {
+ p += strlen("Plan Rows: ");
+ baserel->rows = strtod(p, &endp);
+ if (*endp != '\n' && *endp != '\0')
+ elog(ERROR, "invalid plan format for plan rows");
+ }
+
+ /* extract average width from remote plan */
+ p = strstr(plan, "Plan Width: ");
+ if (p != NULL)
+ {
+ p += strlen("Plan Width: ");
+ baserel->width = strtol(p, &endp, 10);
+ if (*endp != '\n' && *endp != '\0')
+ elog(ERROR, "invalid plan format for plan width");
+ }
+
+ /* TODO Selectivity of quals pushed down should be considered. */
+
+ /* add cost to establish connection. */
+ *startup_cost += CONNECTION_COSTS;
+ *total_cost += CONNECTION_COSTS;
+
+ /* add cost to transfer result. */
+ *total_cost += TRANSFER_COSTS_PER_BYTE * baserel->width * baserel->tuples;
+ *total_cost += cpu_tuple_cost * baserel->tuples;
+ }
+
+ /*
+ * Execute remote query with current parameters.
+ */
+ static void
+ execute_query(ForeignScanState *node)
+ {
+ FdwPlan *fdwplan;
+ PgsqlFdwExecutionState *festate;
+ ParamListInfo params = node->ss.ps.state->es_param_list_info;
+ int numParams = params ? params->numParams : 0;
+ Oid *types = NULL;
+ const char **values = NULL;
+ char *sql;
+ PGconn *conn;
+ PGresult *res;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+ types = festate->param_types;
+ values = festate->param_values;
+
+ /*
+ * Construct parameter array in text format. We don't release memory for
+ * the arrays explicitly, because the memory usage would not be very large,
+ * and anyway they will be released in context cleanup.
+ */
+ if (numParams > 0)
+ {
+ int i;
+
+ 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;
+
+ getTypeOutputInfo(types[i], &out_func_oid, &isvarlena);
+ fmgr_info(out_func_oid, &func);
+ values[i] = OutputFunctionCall(&func, params->params[i].value);
+ }
+ }
+ }
+
+ /*
+ * Execute remote query with parameters.
+ */
+ conn = festate->conn;
+ fdwplan = ((ForeignScan *) node->ss.ps.plan)->fdwplan;
+ sql = strVal(list_nth(fdwplan->fdw_private, FdwPrivateDeclareSql));
+ res = PQexecParams(conn, sql, numParams, types, values, NULL, NULL, 0);
+ PG_TRY();
+ {
+ /*
+ * If the query has failed, reporting details is enough here.
+ * Connection(s) which are used by this query (at least used by
+ * pgsql_fdw) will be cleaned up by the foreign connection manager.
+ */
+ if (res == NULL || PQresultStatus(res) != PGRES_COMMAND_OK)
+ {
+ ereport(ERROR,
+ (errmsg("could not declare cursor"),
+ errdetail("%s", PQerrorMessage(conn)),
+ errhint("%s", sql)));
+ }
+
+ /* Mark that this scan has opened a cursor. */
+ festate->cursor_opened = true;
+
+ /* Discard result of CURSOR statement and fetch first bunch. */
+ PQclear(res);
+ res = fetch_result(node);
+
+ /*
+ * Store the result of the query into tuplestore.
+ * We must release PGresult here to avoid memory leak.
+ */
+ store_result(node, res);
+ PQclear(res);
+ res = NULL;
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+ }
+
+ /*
+ * Fetch next partial result from remote server.
+ */
+ static PGresult *
+ fetch_result(ForeignScanState *node)
+ {
+ PgsqlFdwExecutionState *festate;
+ List *fdw_private;
+ char *sql;
+ PGconn *conn;
+ PGresult *res;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+
+ /* retrieve information for fetching result. */
+ fdw_private = festate->fdwplan->fdw_private;
+ sql = strVal(list_nth(fdw_private, FdwPrivateFetchSql));
+ conn = festate->conn;
+ res = PQexec(conn, sql);
+ if (res == NULL || PQresultStatus(res) != PGRES_TUPLES_OK)
+ {
+ ereport(ERROR,
+ (errmsg("could not fetch rows from foreign server"),
+ errdetail("%s", PQerrorMessage(conn)),
+ errhint("%s", sql)));
+ }
+
+ return res;
+ }
+
+ /*
+ * Create tuples from PGresult and store them into tuplestore.
+ */
+ static void
+ store_result(ForeignScanState *node, PGresult *res)
+ {
+ int rows;
+ int row;
+ int i;
+ int nfields;
+ int attnum; /* number of non-dropped columns */
+ Form_pg_attribute *attrs;
+ TupleTableSlot *slot = node->ss.ss_ScanTupleSlot;
+ TupleDesc tupdesc = slot->tts_tupleDescriptor;
+ PgsqlFdwExecutionState *festate;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+ rows = PQntuples(res);
+ nfields = PQnfields(res);
+ attrs = tupdesc->attrs;
+
+ /* First, ensure that the tuplestore is empty. */
+ if (festate->tuples == NULL)
+ {
+ MemoryContext oldcontext = CurrentMemoryContext;
+
+ /*
+ * Create tuplestore to store result of the query in per-query context.
+ * Note that we use this memory context to avoid memory leak in error
+ * cases.
+ */
+ MemoryContextSwitchTo(MessageContext);
+ festate->tuples = tuplestore_begin_heap(false, false, work_mem);
+ MemoryContextSwitchTo(oldcontext);
+ }
+ else
+ {
+ /* We already have tuplestore, just need to clear contents of it. */
+ tuplestore_clear(festate->tuples);
+ }
+
+
+ /* 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)));
+
+ /* put a tuples into the slot */
+ for (row = 0; row < rows; row++)
+ {
+ int j;
+ HeapTuple tuple;
+
+ for (i = 0, j = 0; i < tupdesc->natts; i++)
+ {
+ /* skip dropped columns. */
+ if (attrs[i]->attisdropped)
+ {
+ festate->col_values[i] = NULL;
+ continue;
+ }
+
+ if (PQgetisnull(res, row, j))
+ festate->col_values[i] = NULL;
+ else
+ festate->col_values[i] = PQgetvalue(res, row, 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(festate->attinmeta, festate->col_values);
+ tuplestore_puttuple(festate->tuples, tuple);
+ }
+
+ tuplestore_donestoring(festate->tuples);
+ }
diff --git a/contrib/pgsql_fdw/pgsql_fdw.control b/contrib/pgsql_fdw/pgsql_fdw.control
index ...0a9c8f4 .
*** a/contrib/pgsql_fdw/pgsql_fdw.control
--- b/contrib/pgsql_fdw/pgsql_fdw.control
***************
*** 0 ****
--- 1,5 ----
+ # pgsql_fdw extension
+ comment = 'foreign-data wrapper for remote PostgreSQL servers'
+ default_version = '1.0'
+ module_pathname = '$libdir/pgsql_fdw'
+ relocatable = true
diff --git a/contrib/pgsql_fdw/pgsql_fdw.h b/contrib/pgsql_fdw/pgsql_fdw.h
index ...fb49ffb .
*** a/contrib/pgsql_fdw/pgsql_fdw.h
--- b/contrib/pgsql_fdw/pgsql_fdw.h
***************
*** 0 ****
--- 1,28 ----
+ /*-------------------------------------------------------------------------
+ *
+ * pgsql_fdw.h
+ * foreign-data wrapper for remote PostgreSQL servers.
+ *
+ * Copyright (c) 2011, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/pgsql_fdw.h
+ *
+ *-------------------------------------------------------------------------
+ */
+
+ #ifndef PGSQL_FDW_H
+ #define PGSQL_FDW_H
+
+ #include "postgres.h"
+ #include "nodes/relation.h"
+
+ /* in option.c */
+ int ExtractConnectionOptions(List *defelems,
+ const char **keywords,
+ const char **values);
+
+ /* in deparse.c */
+ char *deparseSql(Oid relid, PlannerInfo *root, RelOptInfo *baserel);
+
+ #endif /* PGSQL_FDW_H */
diff --git a/contrib/pgsql_fdw/sql/pgsql_fdw.sql b/contrib/pgsql_fdw/sql/pgsql_fdw.sql
index ...3db9606 .
*** a/contrib/pgsql_fdw/sql/pgsql_fdw.sql
--- b/contrib/pgsql_fdw/sql/pgsql_fdw.sql
***************
*** 0 ****
--- 1,175 ----
+ -- ===================================================================
+ -- create FDW objects
+ -- ===================================================================
+
+ CREATE EXTENSION pgsql_fdw;
+
+ CREATE SERVER loopback1 FOREIGN DATA WRAPPER pgsql_fdw;
+ CREATE SERVER loopback2 FOREIGN DATA WRAPPER pgsql_fdw
+ OPTIONS (dbname 'contrib_regression');
+
+ CREATE USER MAPPING FOR public SERVER loopback1
+ OPTIONS (user 'value', password 'value');
+ CREATE USER MAPPING FOR public SERVER loopback2;
+
+ CREATE FOREIGN TABLE ft1 (
+ c1 int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp
+ ) SERVER loopback2;
+
+ CREATE FOREIGN TABLE ft2 (
+ c1 int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp
+ ) SERVER loopback2;
+
+ -- ===================================================================
+ -- create objects used through FDW
+ -- ===================================================================
+ CREATE SCHEMA "S 1";
+ CREATE TABLE "S 1"."T 1" (
+ "C 1" int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ CONSTRAINT t1_pkey PRIMARY KEY ("C 1")
+ );
+ CREATE TABLE "S 1"."T 2" (
+ c1 int NOT NULL,
+ c2 text,
+ CONSTRAINT t2_pkey PRIMARY KEY (c1)
+ );
+
+ BEGIN;
+ TRUNCATE "S 1"."T 1";
+ INSERT INTO "S 1"."T 1"
+ SELECT id,
+ id % 10,
+ to_char(id, 'FM00000'),
+ '1970-01-01'::timestamptz + ((id % 100) || ' days')::interval,
+ '1970-01-01'::timestamp + ((id % 100) || ' days')::interval
+ FROM generate_series(1, 1000) id;
+ TRUNCATE "S 1"."T 2";
+ INSERT INTO "S 1"."T 2"
+ SELECT id,
+ 'AAA' || to_char(id, 'FM000')
+ FROM generate_series(1, 100) id;
+ COMMIT;
+
+ -- ===================================================================
+ -- tests for pgsql_fdw_validator
+ -- ===================================================================
+ ALTER FOREIGN DATA WRAPPER pgsql_fdw OPTIONS (host 'value'); -- ERRROR
+ -- requiressl, krbsrvname and gsslib are omitted because they depend on
+ -- configure option
+ ALTER SERVER loopback1 OPTIONS (
+ authtype 'value',
+ service 'value',
+ connect_timeout 'value',
+ dbname 'value',
+ host 'value',
+ hostaddr 'value',
+ port 'value',
+ --client_encoding 'value',
+ tty 'value',
+ options 'value',
+ application_name 'value',
+ --fallback_application_name 'value',
+ keepalives 'value',
+ keepalives_idle 'value',
+ keepalives_interval 'value',
+ -- requiressl 'value',
+ sslmode 'value',
+ sslcert 'value',
+ sslkey 'value',
+ sslrootcert 'value',
+ sslcrl 'value'
+ --requirepeer 'value',
+ -- krbsrvname 'value',
+ -- gsslib 'value',
+ --replication 'value'
+ );
+ ALTER SERVER loopback1 OPTIONS (user 'value'); -- ERROR
+ ALTER SERVER loopback2 OPTIONS (ADD fetch_count '2');
+ ALTER USER MAPPING FOR public SERVER loopback1
+ OPTIONS (DROP user, DROP password);
+ ALTER USER MAPPING FOR public SERVER loopback1
+ OPTIONS (host 'value'); -- ERROR
+ ALTER FOREIGN TABLE ft1 OPTIONS (nspname 'S 1', relname 'T 1');
+ ALTER FOREIGN TABLE ft2 OPTIONS (nspname 'S 1', relname 'T 1', fetch_count '100');
+ ALTER FOREIGN TABLE ft1 OPTIONS (invalid 'value'); -- ERROR
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count 'a'); -- ERROR
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count '0'); -- ERROR
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count '-1'); -- ERROR
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (invalid 'value'); -- ERROR
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (colname 'C 1');
+ ALTER FOREIGN TABLE ft2 ALTER COLUMN c1 OPTIONS (colname 'C 1');
+ \dew+
+ \des+
+ \deu+
+ \det+
+
+ -- ===================================================================
+ -- simple queries
+ -- ===================================================================
+ -- single table, with/without alias
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
+ SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+ SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+ -- with WHERE clause
+ SELECT * FROM ft1 t1 WHERE t1.c1 = 101;
+ -- 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
+ SELECT * FROM ft1 t1 WHERE t1.c3 = (SELECT MAX(c3) FROM ft2 t2) ORDER BY c1;
+ -- used in CTE
+ WITH t1 AS (SELECT * FROM ft1 WHERE c1 <= 10) SELECT t2.c1, t2.c2, t2.c3, t2.c4 FROM t1, ft2 t2 WHERE t1.c1 = t2.c1 ORDER BY t1.c1;
+ -- fixed values
+ SELECT 'fixed', NULL FROM ft1 t1 WHERE c1 = 1;
+
+ -- ===================================================================
+ -- parameterized queries
+ -- ===================================================================
+ -- simple join
+ PREPARE st1(int, int) AS SELECT t1.c3, t2.c3 FROM ft1 t1, ft2 t2 WHERE t1.c1 = $1 AND t2.c1 = $2;
+ EXPLAIN (COSTS false) EXECUTE st1(1, 2);
+ EXECUTE st1(1, 1);
+ EXECUTE st1(101, 101);
+ -- subquery using stable function (can't be pushed down)
+ PREPARE st2(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c4) = 6) ORDER BY c1;
+ EXPLAIN (COSTS false) EXECUTE st2(10, 20);
+ EXECUTE st2(10, 20);
+ EXECUTE st1(101, 101);
+ -- subquery using immutable function (can be pushed down)
+ PREPARE st3(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c5) = 6) ORDER BY c1;
+ EXPLAIN (COSTS false) EXECUTE st3(10, 20);
+ EXECUTE st3(10, 20);
+ EXECUTE st3(20, 30);
+ -- cleanup
+ DEALLOCATE st1;
+ DEALLOCATE st2;
+ DEALLOCATE st3;
+
+ -- ===================================================================
+ -- connection management
+ -- ===================================================================
+ SELECT srvname, usename FROM pgsql_fdw_connections;
+ SELECT pgsql_fdw_disconnect(srvid, usesysid) FROM pgsql_fdw_get_connections();
+ SELECT srvname, usename FROM pgsql_fdw_connections;
+
+ -- ===================================================================
+ -- cleanup
+ -- ===================================================================
+ DROP EXTENSION pgsql_fdw CASCADE;
+
diff --git a/doc/src/sgml/contrib.sgml b/doc/src/sgml/contrib.sgml
index adf09ca..65c7e81 100644
*** a/doc/src/sgml/contrib.sgml
--- b/doc/src/sgml/contrib.sgml
*************** CREATE EXTENSION <replaceable>module_nam
*** 117,122 ****
--- 117,123 ----
&pgcrypto;
&pgfreespacemap;
&pgrowlocks;
+ &pgsql-fdw;
&pgstandby;
&pgstatstatements;
&pgstattuple;
diff --git a/doc/src/sgml/filelist.sgml b/doc/src/sgml/filelist.sgml
index ed39e0b..d72590f 100644
*** a/doc/src/sgml/filelist.sgml
--- b/doc/src/sgml/filelist.sgml
***************
*** 123,128 ****
--- 123,129 ----
<!ENTITY pgcrypto SYSTEM "pgcrypto.sgml">
<!ENTITY pgfreespacemap SYSTEM "pgfreespacemap.sgml">
<!ENTITY pgrowlocks SYSTEM "pgrowlocks.sgml">
+ <!ENTITY pgsql-fdw SYSTEM "pgsql-fdw.sgml">
<!ENTITY pgstandby SYSTEM "pgstandby.sgml">
<!ENTITY pgstatstatements SYSTEM "pgstatstatements.sgml">
<!ENTITY pgstattuple SYSTEM "pgstattuple.sgml">
diff --git a/doc/src/sgml/pgsql-fdw.sgml b/doc/src/sgml/pgsql-fdw.sgml
index ...f594ed8 .
*** a/doc/src/sgml/pgsql-fdw.sgml
--- b/doc/src/sgml/pgsql-fdw.sgml
***************
*** 0 ****
--- 1,251 ----
+ <!-- doc/src/sgml/pgsql-fdw.sgml -->
+
+ <sect1 id="pgsql-fdw" xreflabel="pgsql_fdw">
+ <title>pgsql_fdw</title>
+
+ <indexterm zone="pgsql-fdw">
+ <primary>pgsql_fdw</primary>
+ </indexterm>
+
+ <para>
+ The <filename>pgsql_fdw</filename> module provides a foreign-data wrapper for
+ external <productname>PostgreSQL</productname> servers.
+ With this module, users can access data stored in external
+ <productname>PostgreSQL</productname> via plain SQL statements.
+ </para>
+
+ <para>
+ The <application>pgsql_fdw</application> can be installed on only
+ <productname>PostgreSQL</productname> 9.1 or later, but it can also access
+ data stored in <productname>PostgreSQL</productname> 9.0.
+ <productname>PostgreSQL</productname>8.4 or older are not available as remote
+ server because <application>pgsql_fdw</application> uses YAML format of
+ <command>EXPLAIN</command> output.
+ </para>
+
+ <para>
+ Note that default wrapper <literal>pgsql_fdw</literal> is created
+ automatically during <command>CREATE EXTENSION</command> command for
+ <application>pgsql_fdw</application>.
+ </para>
+
+ <sect2>
+ <title>FDW Options of pgsql_fdw</title>
+
+ <sect3>
+ <title>Connection Options</title>
+ <para>
+ A foreign server and user mapping created using this wrapper can have
+ <application>libpq</> connection options, expect below:
+
+ <itemizedlist>
+ <listitem><para>client_encoding</para></listitem>
+ <listitem><para>fallback_application_name</para></listitem>
+ <listitem><para>replication</para></listitem>
+ </itemizedlist>
+
+ For details of <application>libpq</> connection options, see
+ <xref linkend="libpq-connect">.
+ </para>
+
+ <para>
+ <literal>user</literal> and <literal>password</literal> can be
+ specified on user mappings, and others can be specified on foreign servers.
+ </para>
+ </sect3>
+
+ <sect3>
+ <title>Object Name Options</title>
+ <para>
+ Foreign tables which were created using this wrapper, or its columns can
+ have object name options. These options can be used to specify the names
+ used in SQL statement sent to remote <productname>PostgreSQL</productname>
+ server. These options are useful when a remote object has different name
+ from corresponding local one.
+ </para>
+
+ <variablelist>
+
+ <varlistentry>
+ <term><literal>nspname</literal></term>
+ <listitem>
+ <para>
+ This option, which can be specified on a foreign table, is used as a
+ namespace (schema) reference in the SQL statement. If this options is
+ omitted, <literal>pg_class.nspname</literal> of the foreign table is
+ used.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>relname</literal></term>
+ <listitem>
+ <para>
+ This option, which can be specified on a foreign table, is used as a
+ relation (table) reference in the SQL statement. If this options is
+ omitted, <literal>pg_class.relname</literal> of the foreign table is
+ used.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>colname</literal></term>
+ <listitem>
+ <para>
+ This option, which can be specified on a column of a foreign table, is
+ used as a column (attribute) reference in the SQL statement. If this
+ option is omitted, <literal>pg_attribute.attname</literal> of the column
+ of the foreign table is used.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ </variablelist>
+
+ </sect3>
+
+ <sect3>
+ <title>Cursor Options</title>
+ <para>
+ The <application>pgsql_fdw</application> always uses cursor to retrieve the
+ result from external server. Users can control the behavior of cursor by
+ setting cursor options to foreign table or foreign server. If an option
+ is set to both objects, finer-grained setting is used. In other words,
+ foreign table's setting overrides foreign server's setting.
+ </para>
+
+ <variablelist>
+
+ <varlistentry>
+ <term><literal>fetch_count</literal></term>
+ <listitem>
+ <para>
+ This option specifies the number of rows to be fetched at a time.
+ This option accepts only integer value larger than zero.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ </variablelist>
+
+ </sect3>
+
+ </sect2>
+
+ <sect2>
+ <title>Connection Management</title>
+
+ <para>
+ The <application>pgsql_fdw</application> establishes a connection to a
+ foreign server in the beginning of the first query which uses a foreign
+ table associated to the foreign server, and reuses the connection following
+ queries and even in following foreign scans in same query.
+
+ You can see the list of active connections via
+ <structname>pgsql_fdw_connections</structname> view. It shows pair of oid
+ and name of server and local role for each active connections established by
+ <application>pgsql_fdw</application>. For security reason, only superuser
+ can see other role's connections.
+ </para>
+
+ <para>
+ Established connections are kept alive until local role changes or the
+ current transaction aborts or user requests so.
+ </para>
+
+ <para>
+ If role has been changed, active connections established as old local role
+ is kept alive but never be reused until locla role has restored to original
+ role. This kind of situation happens with <command>SET ROLE</command> and
+ <command>SET SESSION AUTHORIZATION</command>.
+ </para>
+
+ <para>
+ If current transaction aborts by error or user request, all active
+ connections are disconnected automatically. This behavior avoids possible
+ connection leaks on error.
+ </para>
+
+ <para>
+ You can discard persistent connection at arbitrary timing with
+ <function>pgsql_fdw_disconnect()</function>. It takes server oid and
+ user oid as arguments. This function can handle only connections
+ established in current session; connections established by other backends
+ are not reachable.
+ </para>
+
+ <para>
+ You can discard all active and visible connections in current session with
+ using <structname>pgsql_fdw_connections</structname> and
+ <function>pgsql_fdw_disconnect()</function> together:
+ <synopsis>
+ postgres=# SELECT pgsql_fdw_disconnect(srvid, usesysid) FROM pgsql_fdw_connections;
+ pgsql_fdw_disconnect
+ ----------------------
+ OK
+ OK
+ (2 rows)
+ </synopsis>
+ </para>
+ </sect2>
+
+ <sect2>
+ <title>Transaction Management</title>
+ <para>
+ The <application>pgsql_fdw</application> executes <command>BEGIN</command>
+ command when a new connection has established. This means that all remote
+ queries for a foreign server are executed in a transaction. Since the
+ default transaction isolation level is <literal>READ COMMITTED</literal>,
+ multiple foreign scans in a local query might produce inconsistent results.
+
+ To avoid this inconsistency, you can use <literal>SERIALIZABLE</literal>
+ level for remote transaction with setting
+ <literal>default_transaction_isolation</literal> for the user used for
+ <application>pgsql_fdw</application> connection on remote side.
+ </para>
+ </sect2>
+
+ <sect2>
+ <title>Estimation of Costs and Rows</title>
+ <para>
+ The <application>pgsql_fdw</application> estimates the costs of a foreign
+ scan by adding some basic costs: connection costs, remote query costs and
+ data transfer costs.
+ To get remote query costs <application>pgsql_fdw</application> executes
+ <command>EXPLAIN</command> command on remote server for each foreign scan.
+ </para>
+
+ <para>
+ On the other hand, estimated rows which was returned by
+ <command>EXPLAIN</command> is used for local estimation as-is.
+ </para>
+ </sect2>
+
+ <sect2>
+ <title>EXPLAIN Output</title>
+ <para>
+ For a foreign table using <literal>pgsql_fdw</>, <command>EXPLAIN</> shows
+ a remote SQL statement which is sent to remote
+ <productname>PostgreSQL</productname> server for a ForeignScan plan node.
+ For example:
+ </para>
+ <synopsis>
+ postgres=# EXPLAIN SELECT aid FROM pgbench_accounts WHERE abalance < 0;
+ QUERY PLAN
+ -----------------------------------------------------------------------------------------------
+ Foreign Scan on pgbench_accounts (cost=100.00..8769.00 rows=1 width=4)
+ Remote SQL: SELECT aid, NULL, NULL, NULL FROM public.pgbench_accounts WHERE (abalance < 0)
+ (2 rows)
+ </synopsis>
+ </sect2>
+
+ <sect2>
+ <title>Author</title>
+ <para>
+ Shigeru Hanada <email>shigeru.hanada@gmail.com</email>
+ </para>
+ </sect2>
+
+ </sect1>
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 75923a6..6fda053 100644
*** a/src/backend/utils/adt/ruleutils.c
--- b/src/backend/utils/adt/ruleutils.c
*************** deparse_context_for(const char *aliasnam
*** 2161,2166 ****
--- 2161,2189 ----
return list_make1(dpns);
}
+ /* ----------
+ * deparse_context_for_rtelist - Build deparse context for a single relation
+ *
+ * Given the list of RangeTableEnetry, build deparsing context for an
+ * expression referencing those relations. This is sufficient for uses of
+ * deparse_expression before plan has been created.
+ * ----------
+ */
+ List *
+ deparse_context_for_rtelist(List *rtable)
+ {
+ deparse_namespace *dpns;
+
+ dpns = (deparse_namespace *) palloc0(sizeof(deparse_namespace));
+
+ /* Build a minimal RTE for the rel */
+ dpns->rtable = rtable;
+ dpns->ctes = NIL;
+
+ /* Return a one-deep namespace stack */
+ return list_make1(dpns);
+ }
+
/*
* deparse_context_for_planstate - Build deparse context for a plan
*
diff --git a/src/include/utils/builtins.h b/src/include/utils/builtins.h
index 8a1c82e..a56366f 100644
*** a/src/include/utils/builtins.h
--- b/src/include/utils/builtins.h
*************** extern char *deparse_expression(Node *ex
*** 626,631 ****
--- 626,632 ----
extern List *deparse_context_for(const char *aliasname, Oid relid);
extern List *deparse_context_for_planstate(Node *planstate, List *ancestors,
List *rtable);
+ extern List *deparse_context_for_rtelist(List *rtable);
extern const char *quote_identifier(const char *ident);
extern char *quote_qualified_identifier(const char *qualifier,
const char *ident);
2011/10/31 Shigeru Hanada <shigeru.hanada@gmail.com>:
(2011/10/30 11:34), Shigeru Hanada wrote:
2011/10/30 Tom Lane<tgl@sss.pgh.pa.us>:
I think we have to. Even if we estimate that a given scan will return
only a few rows, what happens if we're wrong? We don't want to blow out
memory on the local server by retrieving gigabytes in one go.Oh, I overlooked the possibility of wrong estimation. Old PostgreSQL uses
1000 as default estimation, so big table which has not been analyzed may
crashes the backend.To ensure the data retrieving safe, we need to get actual amount of result,
maybe by executing SELECT COUNT(*) in planning phase. It sounds too heavy
to do for every scan, and it still lacks actual width.One possible idea is to change default value of min_cursur_rows option to 0
so that pgsql_fdw uses CURSOR by default, but it seems not enough. I'll
drop simple SELECT mode from first version of pgsql_fdw for safety.I removed simple SELECT mode from pgsql_fdw, and consequently also
removed min_cursor_rows FDW option. This fix avoids possible memory
exhaustion due to wrong estimation gotten from remote side.Once libpq has had capability to retrieve arbitrary number of rows from
remote portal at a time without server-side cursor in future, then we
will be able to revive simple SELECT. Then it's enough safe even if we
don't have actual data size, but (maybe) faster than cursor mode because
we can reduce # of SQL commands. Though of course proof of performance
advantage should be shown before such development.
If you need a less SQL commands, then you can increase fetch_count
parameter - default 1000 is maybe too small, maybe 10000 lines as
default (not more).
For more complex queries can be interesting to set a cursor_tuple_fraction
Pavel
Show quoted text
--
Shigeru Hanada--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Hi,
Attached are revised version of pgsql_fdw patches.
fdw_helper_funcs_v2.patch provides some functions which would be useful
to implement FDW, and document about FDW helper functions including
those which exist in 9.1. They are not specific to pgsql_fdw, so I
separated it from pgsql_fdw patch.
pgsql_fdw_v4.patch provides a FDW for PostgreSQL. This patch requires
fdw_helper_funcs_v2.patch has been applied. Changes done since last
version are:
* Default of fetch_count option is increased to 10000, as suggested by
Pavel Stehule.
* Remove unnecessary NULL check before PQresultStatus.
* Evaluate all conditions on local side even if some of them has been
pushed down to remote side, to ensure that results are correct.
* Use fixed costs for queries which contain external parameter, because
such query can't be used in EXPLAIN command.
Regards,
--
Shigeru Hanada
Attachments:
fdw_helper_funcs_v2.patchtext/plain; name=fdw_helper_funcs_v2.patchDownload
diff --git a/contrib/file_fdw/file_fdw.c b/contrib/file_fdw/file_fdw.c
index 1cf3b3c..4a7ffa6 100644
*** a/contrib/file_fdw/file_fdw.c
--- b/contrib/file_fdw/file_fdw.c
***************
*** 26,32 ****
#include "nodes/makefuncs.h"
#include "optimizer/cost.h"
#include "utils/rel.h"
! #include "utils/syscache.h"
PG_MODULE_MAGIC;
--- 26,32 ----
#include "nodes/makefuncs.h"
#include "optimizer/cost.h"
#include "utils/rel.h"
! #include "utils/lsyscache.h"
PG_MODULE_MAGIC;
*************** get_file_fdw_attribute_options(Oid relid
*** 345,398 ****
/* Retrieve FDW options for all user-defined attributes. */
for (attnum = 1; attnum <= natts; attnum++)
{
! HeapTuple tuple;
! Form_pg_attribute attr;
! Datum datum;
! bool isnull;
/* Skip dropped attributes. */
if (tupleDesc->attrs[attnum - 1]->attisdropped)
continue;
! /*
! * We need the whole pg_attribute tuple not just what is in the
! * tupleDesc, so must do a catalog lookup.
! */
! tuple = SearchSysCache2(ATTNUM,
! RelationGetRelid(rel),
! Int16GetDatum(attnum));
! if (!HeapTupleIsValid(tuple))
! elog(ERROR, "cache lookup failed for attribute %d of relation %u",
! attnum, RelationGetRelid(rel));
! attr = (Form_pg_attribute) GETSTRUCT(tuple);
!
! datum = SysCacheGetAttr(ATTNUM,
! tuple,
! Anum_pg_attribute_attfdwoptions,
! &isnull);
! if (!isnull)
{
! List *options = untransformRelOptions(datum);
! ListCell *lc;
! foreach(lc, options)
{
! DefElem *def = (DefElem *) lfirst(lc);
!
! if (strcmp(def->defname, "force_not_null") == 0)
{
! if (defGetBoolean(def))
! {
! char *attname = pstrdup(NameStr(attr->attname));
! fnncolumns = lappend(fnncolumns, makeString(attname));
! }
}
- /* maybe in future handle other options here */
}
}
-
- ReleaseSysCache(tuple);
}
heap_close(rel, AccessShareLock);
--- 345,373 ----
/* Retrieve FDW options for all user-defined attributes. */
for (attnum = 1; attnum <= natts; attnum++)
{
! List *options;
! ListCell *lc;
/* Skip dropped attributes. */
if (tupleDesc->attrs[attnum - 1]->attisdropped)
continue;
! options = GetForeignColumnOptions(relid, attnum);
! foreach(lc, options)
{
! DefElem *def = (DefElem *) lfirst(lc);
! if (strcmp(def->defname, "force_not_null") == 0)
{
! if (defGetBoolean(def))
{
! char *attname = pstrdup(get_attname(relid, attnum));
! fnncolumns = lappend(fnncolumns, makeString(attname));
}
}
+ /* maybe in future handle other options here */
}
}
heap_close(rel, AccessShareLock);
diff --git a/doc/src/sgml/fdwhandler.sgml b/doc/src/sgml/fdwhandler.sgml
index 76ff243..d809cac 100644
*** a/doc/src/sgml/fdwhandler.sgml
--- b/doc/src/sgml/fdwhandler.sgml
*************** EndForeignScan (ForeignScanState *node);
*** 235,238 ****
--- 235,392 ----
</sect1>
+ <sect1 id="fdw-helpers">
+ <title>Foreign Data Wrapper Helper Functions</title>
+
+ <para>
+ Several helper functions are exported from core so that authors of FDW
+ can get easy access to attributes of FDW-related objects such as FDW
+ options.
+ </para>
+
+ <para>
+ <programlisting>
+ ForeignDataWrapper *
+ GetForeignDataWrapper(Oid fdwid);
+ </programlisting>
+
+ This function returns a <structname>ForeignDataWrapper</structname> object
+ for a foreign-data wrapper with given oid. A
+ <structname>ForeignDataWrapper</structname> object contains oid of the
+ wrapper itself, oid of the owner of the wrapper, name of the wrapper, oid
+ of fdwhandler function, oid of fdwvalidator function, and FDW options in
+ the form of list of <structname>DefElem</structname>.
+ </para>
+
+ <para>
+ <programlisting>
+ ForeignServer *
+ GetForeignServer(Oid serverid);
+ </programlisting>
+
+ This function returns a <structname>ForeignServer</structname> object for
+ a foreign server with given oid. A <structname>ForeignServer</structname>
+ object contains oid of the server, oid of the wrapper for the server, oid
+ of the owner of the server, name of the server, type of the server,
+ version of the server, and FDW options in the form of list of
+ <structname>DefElem</structname>.
+ </para>
+
+ <para>
+ <programlisting>
+ UserMapping *
+ GetUserMapping(Oid userid, Oid serverid);
+ </programlisting>
+
+ This function returns a <structname>UserMapping</structname> object for a
+ user mapping with given oid pair. A <structname>UserMapping</structname>
+ object contains oid of the user, oid of the server, and FDW options in the
+ form of list of <structname>DefElem</structname>.
+ </para>
+
+ <para>
+ <programlisting>
+ ForeignTable *
+ GetForeignTable(Oid relid);
+ </programlisting>
+
+ This function returns a <structname>ForeignTable</structname> object for a
+ foreign table with given oid. A <structname>ForeignTable</structname>
+ object contains oid of the foreign table, oid of the server for the table,
+ and FDW options in the form of list of <structname>DefElem</structname>.
+ </para>
+
+ <para>
+ <programlisting>
+ List *
+ GetForeignTableColumnOptions(Oid relid, AttrNumber attnum);
+ </programlisting>
+
+ This function returns per-column FDW options for a column with given
+ relation oid and attribute number in the form of list of
+ <structname>DefElem</structname>.
+ </para>
+
+ <para>
+ Some object types have name-based functions.
+ </para>
+
+ <para>
+ <programlisting>
+ ForeignDataWrapper *
+ GetForeignDataWrapperByName(const char *name, bool missing_ok);
+ </programlisting>
+
+ This function returns a <structname>ForeignDataWrapper</structname> object
+ for a foreign-data wrapper with given name. If the wrapper is not found,
+ return NULL if missing_ok was true, otherwise raise an error.
+ </para>
+
+ <para>
+ <programlisting>
+ ForeignServer *
+ GetForeignServerByName(const char *name, bool missing_ok);
+ </programlisting>
+
+ This function returns a <structname>ForeignServer</structname> object for
+ a foreign server with given name. If the server is not found, return NULL
+ if missing_ok was true, otherwise raise an error.
+ </para>
+
+ <para>
+ <programlisting>
+ char *
+ GetFdwOptionValue(Oid fdwid, Oid serverid, Oid relid, AttrNumber attnum, const char *optname);
+ </programlisting>
+
+ This function returns a copied string (created in current memory context)
+ of the value of a FDW option with given name which is set on a object with
+ given oid and attribute number. This function ignores catalogs if invalid
+ identifir is given for it.
+
+ <itemizedlist>
+ <listitem>
+ <para>
+ If attnum is <literal>InvalidAttrNumber</literal> or relid is
+ <literal>Invalidoid</literal>, <structname>pg_attribute</structname> is
+ ignored.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ If relid is <literal>InvalidOid</literal>,
+ <structname>pg_foreign_table</structname> is ignored.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ If both serverid and relid are <literal>InvalidOid</literal>,
+ <structname>pg_foreign_server</structname> is ignored.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ If all of fdwid, serverid and relid are <literal>InvalidOid</literal>,
+ <structname>pg_foreign_data_wrapper</structname> is ignored.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+
+ <para>
+ If the option with given name is set in multiple object level, the one in
+ the finest-grained object is used; e.g. priority is given to user mappings
+ over than foreign servers.
+ This function would be useful when you know which option is needed but you
+ don't know where it is set. If you already know the source object, it
+ would be more efficient to use object retrieval functions.
+ </para>
+
+ <para>
+ To use any of these functions, you need to include
+ <filename>foreign/foreign.h</filename> in your source file.
+ </para>
+
+ </sect1>
+
</chapter>
diff --git a/src/backend/foreign/foreign.c b/src/backend/foreign/foreign.c
index a7d30a1..b984a5e 100644
*** a/src/backend/foreign/foreign.c
--- b/src/backend/foreign/foreign.c
*************** GetForeignTable(Oid relid)
*** 248,253 ****
--- 248,378 ----
/*
+ * If an option entry which matches the given name was found in the given
+ * list, returns a copy of arg string, otherwise returns NULL.
+ */
+ static char *
+ get_options_value(List *options, const char *optname)
+ {
+ ListCell *lc;
+
+ /* Find target option from the list. */
+ foreach (lc, options)
+ {
+ DefElem *def = lfirst(lc);
+
+ if (strcmp(def->defname, optname) == 0)
+ return pstrdup(strVal(def->arg));
+ }
+
+ return NULL;
+ }
+
+ /*
+ * Returns a copy of the value of specified option with searching the option
+ * from appropriate catalog. If an option was stored in multiple object
+ * levels, one in the finest-grained object level is used; lookup order is:
+ * 1) pg_attribute (only when both attnum and relid are valid)
+ * 2) pg_foreign_table (only when relid is valid)
+ * 3) pg_user_mapping (only when serverid or relid is valid)
+ * 4) pg_foreign_server (only when serverid or relid is valid)
+ * 5) pg_foreign_data_wrapper (only when fdwid or serverid or relid is valid)
+ * This priority rule would be useful in most cases using FDW options.
+ *
+ * If attnum was InvalidAttrNumber, we don't retrieve FDW optiosn from
+ * pg_attribute.attfdwoptions.
+ */
+ char *
+ GetFdwOptionValue(Oid fdwid, Oid serverid, Oid relid, AttrNumber attnum,
+ const char *optname)
+ {
+ ForeignTable *table = NULL;
+ UserMapping *user = NULL;
+ ForeignServer *server = NULL;
+ ForeignDataWrapper *wrapper = NULL;
+ char *value;
+
+ /* Do we need to search pg_attribute? */
+ if (attnum != InvalidAttrNumber && relid != InvalidOid)
+ {
+ value = get_options_value(GetForeignColumnOptions(relid, attnum),
+ optname);
+ if (value != NULL)
+ return value;
+ }
+
+ /* Do we need to search pg_foreign_table? */
+ if (relid != InvalidOid)
+ {
+ table = GetForeignTable(relid);
+ value = get_options_value(table->options, optname);
+ if (value != NULL)
+ return value;
+
+ serverid = table->serverid;
+ }
+
+ /* Do we need to search pg_user_mapping and pg_foreign_server? */
+ if (serverid != InvalidOid)
+ {
+ user = GetUserMapping(GetOuterUserId(), serverid);
+ value = get_options_value(user->options, optname);
+ if (value != NULL)
+ return value;
+
+ server = GetForeignServer(serverid);
+ value = get_options_value(server->options, optname);
+ if (value != NULL)
+ return value;
+
+ fdwid = server->fdwid;
+ }
+
+ /* Do we need to search pg_foreign_data_wrapper? */
+ if (fdwid != InvalidOid)
+ {
+ wrapper = GetForeignDataWrapper(fdwid);
+ value = get_options_value(wrapper->options, optname);
+ if (value != NULL)
+ return value;
+ }
+
+ return NULL;
+ }
+
+
+ /*
+ * GetForeignColumnOptions - Get attfdwoptions of given relation/attnum as
+ * list of DefElem.
+ */
+ List *
+ GetForeignColumnOptions(Oid relid, AttrNumber attnum)
+ {
+ List *options;
+ HeapTuple tp;
+ Datum datum;
+ bool isnull;
+
+ tp = SearchSysCache2(ATTNUM,
+ ObjectIdGetDatum(relid),
+ Int16GetDatum(attnum));
+ if (!HeapTupleIsValid(tp))
+ elog(ERROR, "cache lookup failed for attribute %d of relation %u", attnum, relid);
+ datum = SysCacheGetAttr(ATTNUM,
+ tp,
+ Anum_pg_attribute_attfdwoptions,
+ &isnull);
+ if (isnull)
+ options = NIL;
+ else
+ options = untransformRelOptions(datum);
+
+ ReleaseSysCache(tp);
+
+ return options;
+ }
+
+ /*
* GetFdwRoutine - call the specified foreign-data wrapper handler routine
* to get its FdwRoutine struct.
*/
diff --git a/src/include/foreign/foreign.h b/src/include/foreign/foreign.h
index 2c436ae..6498162 100644
*** a/src/include/foreign/foreign.h
--- b/src/include/foreign/foreign.h
*************** extern ForeignDataWrapper *GetForeignDat
*** 75,80 ****
--- 75,83 ----
extern ForeignDataWrapper *GetForeignDataWrapperByName(const char *name,
bool missing_ok);
extern ForeignTable *GetForeignTable(Oid relid);
+ extern char *GetFdwOptionValue(Oid fdwid, Oid serverid, Oid relid,
+ AttrNumber attnum, const char *optname);
+ extern List *GetForeignColumnOptions(Oid relid, AttrNumber attnum);
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);
pgsql_fdw_v4.patchtext/plain; name=pgsql_fdw_v4.patchDownload
diff --git a/contrib/Makefile b/contrib/Makefile
index 0c238aa..e09e61e 100644
*** a/contrib/Makefile
--- b/contrib/Makefile
*************** SUBDIRS = \
*** 41,46 ****
--- 41,47 ----
pgbench \
pgcrypto \
pgrowlocks \
+ pgsql_fdw \
pgstattuple \
seg \
spi \
diff --git a/contrib/README b/contrib/README
index a1d42a1..d3fa211 100644
*** a/contrib/README
--- b/contrib/README
*************** pgrowlocks -
*** 158,163 ****
--- 158,167 ----
A function to return row locking information
by Tatsuo Ishii <ishii@sraoss.co.jp>
+ pgsql_fdw -
+ Foreign-data wrapper for external PostgreSQL servers.
+ by Shigeru Hanada <shigeru.hanada@gmail.com>
+
pgstattuple -
Functions to return statistics about "dead" tuples and free
space within a table
diff --git a/contrib/pgsql_fdw/.gitignore b/contrib/pgsql_fdw/.gitignore
index ...0854728 .
*** a/contrib/pgsql_fdw/.gitignore
--- b/contrib/pgsql_fdw/.gitignore
***************
*** 0 ****
--- 1,4 ----
+ # Generated subdirectories
+ /results/
+ *.o
+ *.so
diff --git a/contrib/pgsql_fdw/Makefile b/contrib/pgsql_fdw/Makefile
index ...6381365 .
*** a/contrib/pgsql_fdw/Makefile
--- b/contrib/pgsql_fdw/Makefile
***************
*** 0 ****
--- 1,22 ----
+ # contrib/pgsql_fdw/Makefile
+
+ MODULE_big = pgsql_fdw
+ OBJS = pgsql_fdw.o option.o deparse.o connection.o
+ PG_CPPFLAGS = -I$(libpq_srcdir)
+ SHLIB_LINK = $(libpq)
+
+ EXTENSION = pgsql_fdw
+ DATA = pgsql_fdw--1.0.sql
+
+ REGRESS = pgsql_fdw
+
+ ifdef USE_PGXS
+ PG_CONFIG = pg_config
+ PGXS := $(shell $(PG_CONFIG) --pgxs)
+ include $(PGXS)
+ else
+ subdir = contrib/pgsql_fdw
+ top_builddir = ../..
+ include $(top_builddir)/src/Makefile.global
+ include $(top_srcdir)/contrib/contrib-global.mk
+ endif
diff --git a/contrib/pgsql_fdw/connection.c b/contrib/pgsql_fdw/connection.c
index ...b497dab .
*** a/contrib/pgsql_fdw/connection.c
--- b/contrib/pgsql_fdw/connection.c
***************
*** 0 ****
--- 1,508 ----
+ /*-------------------------------------------------------------------------
+ *
+ * connection.c
+ * Connection management for pgsql_fdw
+ *
+ * Portions Copyright (c) 2011, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/connection.c
+ *
+ *-------------------------------------------------------------------------
+ */
+ #include "postgres.h"
+
+ #include "catalog/pg_type.h"
+ #include "foreign/foreign.h"
+ #include "funcapi.h"
+ #include "libpq-fe.h"
+ #include "mb/pg_wchar.h"
+ #include "miscadmin.h"
+ #include "utils/array.h"
+ #include "utils/builtins.h"
+ #include "utils/hsearch.h"
+ #include "utils/memutils.h"
+ #include "utils/resowner.h"
+ #include "utils/tuplestore.h"
+
+ #include "pgsql_fdw.h"
+ #include "connection.h"
+
+ /* ============================================================================
+ * Connection management functions
+ * ==========================================================================*/
+
+ /*
+ * Connection cache entry managed with hash table.
+ */
+ typedef struct ConnCacheEntry
+ {
+ /* hash key must be first */
+ Oid serverid; /* oid of foreign server */
+ Oid userid; /* oid of local user */
+
+ 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)
+ {
+ bool found;
+ ConnCacheEntry *entry;
+ ConnCacheEntry key;
+ PGconn *conn = NULL;
+
+ /* initialize connection cache if it isn't */
+ if (FSConnectionHash == NULL)
+ {
+ HASHCTL ctl;
+
+ /* hash key is a pair of oids: serverid and userid */
+ MemSet(&ctl, 0, sizeof(ctl));
+ ctl.keysize = sizeof(Oid) + sizeof(Oid);
+ ctl.entrysize = sizeof(ConnCacheEntry);
+ ctl.hash = tag_hash;
+ ctl.match = memcmp;
+ ctl.keycopy = memcpy;
+ /* allocate FSConnectionHash in the cache context */
+ ctl.hcxt = CacheMemoryContext;
+ FSConnectionHash = hash_create("Foreign Connections", 32,
+ &ctl,
+ HASH_ELEM | HASH_CONTEXT |
+ HASH_FUNCTION | HASH_COMPARE |
+ HASH_KEYCOPY);
+ }
+
+ /* Create key value for the entry. */
+ MemSet(&key, 0, sizeof(key));
+ key.serverid = server->serverid;
+ key.userid = GetOuterUserId();
+
+ /* Is there any cached and valid connection with such key? */
+ entry = hash_search(FSConnectionHash, &key, HASH_ENTER, &found);
+ if (found)
+ {
+ if (entry->conn != NULL)
+ {
+ entry->refs++;
+ elog(DEBUG1,
+ "reuse connection %u/%u (%d)",
+ entry->serverid,
+ entry->userid,
+ entry->refs);
+ 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 ResourceOwner to clean the connection up on error including
+ * user interrupt.
+ */
+ elog(DEBUG1,
+ "create entry for %u/%u (%d)",
+ entry->serverid,
+ entry->userid,
+ entry->refs);
+ 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: key items of entry has been initialized in
+ * hash_search(HASH_ENTER).
+ */
+ entry->refs = 1;
+ entry->conn = conn;
+ elog(DEBUG1,
+ "connected to %u/%u (%d)",
+ entry->serverid,
+ entry->userid,
+ 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 PGconn *
+ connect_pg_server(ForeignServer *server, UserMapping *user)
+ {
+ const char *conname = server->servername;
+ PGconn *conn;
+ PGresult *res;
+ 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. Those two object hold only libpq options.
+ * Extra 3 items are for:
+ * *) fallback_application_name
+ * *) client_encoding
+ * *) NULL termination (end marker)
+ *
+ * Note: We don't omit any parameters even target database might be older
+ * than local, because unexpected parameters are just ignored.
+ */
+ n = list_length(server->options) + list_length(user->options) + 3;
+ 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 += ExtractConnectionOptions(server->options,
+ all_keywords + n, all_values + n);
+ n += ExtractConnectionOptions(user->options,
+ all_keywords + n, all_values + n);
+ all_keywords[n] = all_values[n] = NULL;
+
+ for (i = 0, j = 0; all_keywords[i]; i++)
+ {
+ keywords[j] = all_keywords[i];
+ values[j] = all_values[i];
+ j++;
+ }
+
+ /* Use "pgsql_fdw" as fallback_application_name. */
+ keywords[j] = "fallback_application_name";
+ values[j++] = "pgsql_fdw";
+
+ /* Set client_encoding so that libpq can convert encoding properly. */
+ keywords[j] = "client_encoding";
+ values[j++] = GetDatabaseEncodingName();
+
+ keywords[j] = values[j] = NULL;
+ pfree(all_keywords);
+ pfree(all_values);
+
+ /* 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.")));
+ }
+
+ /*
+ * Start transaction to use cursor to retrieve data separately.
+ */
+ res = PQexec(conn, "BEGIN");
+ if (PQresultStatus(res) != PGRES_COMMAND_OK)
+ {
+ PQclear(res);
+ elog(ERROR, "could not start transaction");
+ }
+ PQclear(res);
+
+ 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 sequentially 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;
+ }
+ if (entry != NULL)
+ 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 referrer, unregister it from cache.
+ * TODO: Note that sharing connections requires a mechanism to detect
+ * change of FDW object to invalidate lasting connections.
+ */
+ entry->refs--;
+ elog(DEBUG1,
+ "connection %u/%u released (%d)",
+ entry->serverid,
+ entry->userid,
+ entry->refs);
+ }
+
+ /*
+ * 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(DEBUG1,
+ "closing connection %u/%u",
+ entry->serverid,
+ entry->userid);
+ PQfinish(entry->conn);
+ entry->refs = 0;
+ entry->conn = NULL;
+ }
+ else
+ elog(DEBUG1,
+ "connection %u/%u already closed",
+ entry->serverid,
+ entry->userid);
+ }
+
+ /*
+ * Get list of connections currently active.
+ */
+ Datum pgsql_fdw_get_connections(PG_FUNCTION_ARGS);
+ PG_FUNCTION_INFO_V1(pgsql_fdw_get_connections);
+ Datum
+ pgsql_fdw_get_connections(PG_FUNCTION_ARGS)
+ {
+ ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+ HASH_SEQ_STATUS scan;
+ ConnCacheEntry *entry;
+ MemoryContext oldcontext = CurrentMemoryContext;
+ Tuplestorestate *tuplestore;
+ TupleDesc tupdesc;
+
+ /* We return list of connection with storing them in a Tuplestore. */
+ rsinfo->returnMode = SFRM_Materialize;
+ rsinfo->setResult = NULL;
+ rsinfo->setDesc = NULL;
+
+ /* Create tuplestore and copy of TupleDesc in per-query context. */
+ MemoryContextSwitchTo(rsinfo->econtext->ecxt_per_query_memory);
+
+ tupdesc = CreateTemplateTupleDesc(2, false);
+ TupleDescInitEntry(tupdesc, 1, "srvid", OIDOID, -1, 0);
+ TupleDescInitEntry(tupdesc, 2, "usesysid", OIDOID, -1, 0);
+ rsinfo->setDesc = tupdesc;
+
+ tuplestore = tuplestore_begin_heap(false, false, work_mem);
+ rsinfo->setResult = tuplestore;
+
+ MemoryContextSwitchTo(oldcontext);
+
+ /*
+ * We need to scan sequentially since we use the address to find
+ * appropriate PGconn from the hash table.
+ */
+ if (FSConnectionHash != NULL)
+ {
+ hash_seq_init(&scan, FSConnectionHash);
+ while ((entry = (ConnCacheEntry *) hash_seq_search(&scan)))
+ {
+ Datum values[2];
+ bool nulls[2];
+ HeapTuple tuple;
+
+ elog(DEBUG1, "found: %u/%u", entry->serverid, entry->userid);
+
+ /* Ignore inactive connections */
+ if (PQstatus(entry->conn) != CONNECTION_OK)
+ continue;
+
+ /*
+ * Ignore other users' connections if current user isn't a
+ * superuser.
+ */
+ if (!superuser() && entry->userid != GetUserId())
+ continue;
+
+ values[0] = ObjectIdGetDatum(entry->serverid);
+ values[1] = ObjectIdGetDatum(entry->userid);
+ nulls[0] = false;
+ nulls[1] = false;
+
+ tuple = heap_formtuple(tupdesc, values, nulls);
+ tuplestore_puttuple(tuplestore, tuple);
+ }
+ }
+ tuplestore_donestoring(tuplestore);
+
+ PG_RETURN_VOID();
+ }
+
+ /*
+ * Discard persistent connection designated by given connection name.
+ */
+ Datum pgsql_fdw_disconnect(PG_FUNCTION_ARGS);
+ PG_FUNCTION_INFO_V1(pgsql_fdw_disconnect);
+ Datum
+ pgsql_fdw_disconnect(PG_FUNCTION_ARGS)
+ {
+ Oid serverid = PG_GETARG_OID(0);
+ Oid userid = PG_GETARG_OID(1);
+ ConnCacheEntry key;
+ ConnCacheEntry *entry = NULL;
+ bool found;
+
+ /* Non-superuser can't discard other users' connection. */
+ if (!superuser() && userid != GetOuterUserId())
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_RESOURCES),
+ errmsg("only superuser can discard other user's connection")));
+
+ /*
+ * If no connection has been established, or no such connections, just
+ * return "NG" to indicate nothing has done.
+ */
+ if (FSConnectionHash == NULL)
+ PG_RETURN_TEXT_P(cstring_to_text("NG"));
+
+ key.serverid = serverid;
+ key.userid = userid;
+ entry = hash_search(FSConnectionHash, &key, HASH_FIND, &found);
+ if (!found)
+ PG_RETURN_TEXT_P(cstring_to_text("NG"));
+
+ /* Discard cached connection, and clear reference counter. */
+ PQfinish(entry->conn);
+ entry->refs = 0;
+ entry->conn = NULL;
+ elog(DEBUG1, "closed connection %u/%u", serverid, userid);
+
+ PG_RETURN_TEXT_P(cstring_to_text("OK"));
+ }
diff --git a/contrib/pgsql_fdw/connection.h b/contrib/pgsql_fdw/connection.h
index ...694534f .
*** a/contrib/pgsql_fdw/connection.h
--- b/contrib/pgsql_fdw/connection.h
***************
*** 0 ****
--- 1,25 ----
+ /*-------------------------------------------------------------------------
+ *
+ * connection.h
+ * Connection management for pgsql_fdw
+ *
+ * Portions Copyright (c) 2011, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_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/pgsql_fdw/deparse.c b/contrib/pgsql_fdw/deparse.c
index ...b5f6c79 .
*** a/contrib/pgsql_fdw/deparse.c
--- b/contrib/pgsql_fdw/deparse.c
***************
*** 0 ****
--- 1,364 ----
+ /*-------------------------------------------------------------------------
+ *
+ * deparse.c
+ * query deparser for PostgreSQL
+ *
+ * Copyright (c) 2011, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/deparse.c
+ *
+ *-------------------------------------------------------------------------
+ */
+ #include "postgres.h"
+
+ #include "access/transam.h"
+ #include "foreign/foreign.h"
+ #include "lib/stringinfo.h"
+ #include "nodes/nodeFuncs.h"
+ #include "nodes/nodes.h"
+ #include "nodes/makefuncs.h"
+ #include "optimizer/clauses.h"
+ #include "optimizer/var.h"
+ #include "parser/parsetree.h"
+ #include "utils/builtins.h"
+ #include "utils/lsyscache.h"
+
+ #include "pgsql_fdw.h"
+
+ /*
+ * Context for walk-through the expression tree.
+ */
+ typedef struct foreign_executable_cxt
+ {
+ PlannerInfo *root;
+ RelOptInfo *foreignrel;
+ } foreign_executable_cxt;
+
+ static bool is_foreign_expr(PlannerInfo *root, RelOptInfo *baserel, Expr *expr);
+ static bool foreign_expr_walker(Node *node, foreign_executable_cxt *context);
+ static bool is_proc_remotely_executable(Oid procid);
+
+ /*
+ * Deparse query representation into SQL statement which suits for remote
+ * PostgreSQL server. Also some of quals in WHERE clause will be pushed down
+ * If they are safe to be evaluated on the remote side.
+ */
+ char *
+ deparseSql(Oid relid, PlannerInfo *root, RelOptInfo *baserel)
+ {
+ StringInfoData foreign_relname;
+ StringInfoData sql; /* builder for SQL statement */
+ bool first;
+ AttrNumber attr;
+ List *attr_used = NIL; /* List of AttNumber used in the query */
+ const char *nspname = NULL; /* plain namespace name */
+ const char *relname = NULL; /* plain relation name */
+ const char *q_nspname; /* quoted namespace name */
+ const char *q_relname; /* quoted relation name */
+ List *foreign_expr = NIL; /* list of Expr* evaluated on remote */
+ int i;
+ List *rtable = NIL;
+ List *context = NIL;
+
+ initStringInfo(&sql);
+ initStringInfo(&foreign_relname);
+
+ /*
+ * First of all, determine which qual can be pushed down.
+ *
+ * The expressions which satisfy is_foreign_expr() are deparsed into WHERE
+ * clause of result SQL string, and they could be removed from PlanState
+ * to avoid duplicate evaluation at ExecScan().
+ *
+ * We never change the quals in the Plan node, because this execution might
+ * be for a PREPAREd statement, thus the quals in the Plan node might be
+ * reused to construct another PlanState for subsequent EXECUTE statement.
+ *
+ * We do this before deparsing SELECT clause because attributes which are
+ * not used in neither reltargetlist nor baserel->baserestrictinfo, quals
+ * evaluated on local, can be replaced with literal "NULL" in the SELECT
+ * clause to reduce overhead of tuple handling tuple and data transfer.
+ */
+ if (baserel->baserestrictinfo != NIL)
+ {
+ ListCell *lc;
+
+ foreach (lc, baserel->baserestrictinfo)
+ {
+ RestrictInfo *ri = (RestrictInfo *) lfirst(lc);
+ List *attrs;
+
+ /* Determine whether the qual can be pushed down or not. */
+ if (is_foreign_expr(root, baserel, ri->clause))
+ foreign_expr = lappend(foreign_expr, ri->clause);
+
+ /*
+ * We need to know which attributes are used in qual evaluated
+ * on the local server, because they should be listed in the
+ * SELECT clause of remote query. We can ignore attributes
+ * which are referenced only in ORDER BY/GROUP BY clause because
+ * such attributes has already been kept in reltargetlist.
+ */
+ attrs = pull_var_clause((Node *) ri->clause,
+ PVC_RECURSE_AGGREGATES,
+ PVC_RECURSE_PLACEHOLDERS);
+ attr_used = list_union(attr_used, attrs);
+ }
+ }
+
+ /*
+ * Determine foreign relation's qualified name. This is necessary for
+ * FROM clause and SELECT clause.
+ */
+ nspname = GetFdwOptionValue(InvalidOid, InvalidOid, relid,
+ InvalidAttrNumber, "nspname");
+ if (nspname == NULL)
+ nspname = get_namespace_name(get_rel_namespace(relid));
+ q_nspname = quote_identifier(nspname);
+
+ relname = GetFdwOptionValue(InvalidOid, InvalidOid, relid,
+ InvalidAttrNumber, "relname");
+ if (relname == NULL)
+ relname = get_rel_name(relid);
+ q_relname = quote_identifier(relname);
+
+ appendStringInfo(&foreign_relname, "%s.%s", q_nspname, q_relname);
+
+ /*
+ * We need to replace aliasname and colnames of the target relation so that
+ * constructed remote query is valid.
+ *
+ * Note that we skip first empty element of simple_rel_array. See also
+ * comments of simple_rel_array and simple_rte_array for the rationale.
+ */
+ for (i = 1; i < root->simple_rel_array_size; i++)
+ {
+ RangeTblEntry *rte = copyObject(root->simple_rte_array[i]);
+ List *newcolnames = NIL;
+
+ if (i == baserel->relid)
+ {
+ /*
+ * Create new list of column names which is used to deparse remote
+ * query from specified names or local column names. This list is
+ * used by deparse_expression.
+ */
+ for (attr = 1; attr <= baserel->max_attr; attr++)
+ {
+ char *colname;
+
+ /* Ignore dropped attributes. */
+ if (get_rte_attribute_is_dropped(rte, attr))
+ continue;
+
+ colname = GetFdwOptionValue(InvalidOid, InvalidOid, relid,
+ attr, "colname");
+ if (colname == NULL)
+ colname = strVal(list_nth(rte->eref->colnames, attr - 1));
+ newcolnames = lappend(newcolnames, makeString(colname));
+ }
+ rte->alias = makeAlias(relname, newcolnames);
+ }
+ rtable = lappend(rtable, rte);
+ }
+ context = deparse_context_for_rtelist(rtable);
+
+ /*
+ * deparse SELECT clause
+ *
+ * List attributes which are in either target list or local restriction.
+ * Unused attributes are replaced with a literal "NULL" for optimization.
+ */
+ appendStringInfo(&sql, "SELECT ");
+ attr_used = list_union(attr_used, baserel->reltargetlist);
+ first = true;
+ for (attr = 1; attr <= baserel->max_attr; attr++)
+ {
+ RangeTblEntry *rte = root->simple_rte_array[baserel->relid];
+ Var *var = NULL;
+ ListCell *lc;
+
+ /* Ignore dropped attributes. */
+ if (get_rte_attribute_is_dropped(rte, attr))
+ continue;
+
+ if (!first)
+ appendStringInfo(&sql, ", ");
+ first = false;
+
+ /*
+ * We use linear search here, but it wouldn't be problem since
+ * attr_used seems to not become so large.
+ */
+ foreach (lc, attr_used)
+ {
+ var = lfirst(lc);
+ if (var->varattno == attr)
+ break;
+ var = NULL;
+ }
+ if (var != NULL)
+ appendStringInfo(&sql, "%s",
+ deparse_expression((Node *) var, context, false, false));
+ else
+ appendStringInfo(&sql, "NULL");
+ }
+ appendStringInfoChar(&sql, ' ');
+
+ /*
+ * deparse FROM clause, including alias if any
+ */
+ appendStringInfo(&sql, "FROM %s", foreign_relname.data);
+
+ /*
+ * deparse WHERE clause
+ */
+ if (foreign_expr != NIL)
+ {
+ Node *node;
+
+ node = (Node *) make_ands_explicit(foreign_expr);
+ appendStringInfo(&sql, " WHERE %s ",
+ deparse_expression(node, context, false, false));
+ list_free(foreign_expr);
+ foreign_expr = NIL;
+ }
+
+ elog(DEBUG3, "Remote SQL: %s", sql.data);
+ return sql.data;
+ }
+
+ /*
+ * Returns true if expr is safe to be evaluated on the foreign server.
+ */
+ static bool
+ is_foreign_expr(PlannerInfo *root, RelOptInfo *baserel, Expr *expr)
+ {
+ foreign_executable_cxt context;
+ context.root = root;
+ context.foreignrel = baserel;
+
+ /*
+ * An expression which includes any mutable function can't be pushed down
+ * because it's result is not stable. For example, pushing now() down to
+ * remote side would cause confusion from the clock offset.
+ * If we have routine mapping infrastructure in future release, we will be
+ * able to choose function to be pushed down in finer granularity.
+ */
+ if (contain_mutable_functions((Node *) expr))
+ return false;
+
+ /*
+ * Check that the expression consists of nodes which are known as safe to
+ * be pushed down.
+ */
+ if (foreign_expr_walker((Node *) expr, &context))
+ return false;
+
+ return true;
+ }
+
+ /*
+ * Return true if node includes any node which is not known as safe to be
+ * pushed down.
+ */
+ static bool
+ foreign_expr_walker(Node *node, foreign_executable_cxt *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 at the tail of this function.
+ */
+ break;
+ case T_Param:
+ /*
+ * Only external parameters can be pushed down.:
+ */
+ {
+ if (((Param *) node)->paramkind != PARAM_EXTERN)
+ return true;
+ }
+ break;
+ case T_OpExpr:
+ /*
+ * Operators which use non-immutable function can't be pushed down.
+ */
+ {
+ OpExpr *oe = (OpExpr *) node;
+
+ if (!is_proc_remotely_executable(oe->opfuncid))
+ return true;
+
+ /* operands are checked later */
+ }
+ break;
+ case T_FuncExpr:
+ /*
+ * Non-immutable functions can't be pushed down.
+ */
+ {
+ FuncExpr *fe = (FuncExpr *) node;
+
+ if (!is_proc_remotely_executable(fe->funcid))
+ return true;
+
+ /* operands are checked later */
+ }
+ 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;
+ foreign_executable_cxt *f_context;
+
+ f_context = (foreign_executable_cxt *) context;
+ if (var->varno != f_context->foreignrel->relid ||
+ var->varlevelsup != 0)
+ return true;
+ }
+ break;
+ default:
+ {
+ ereport(DEBUG3,
+ (errmsg("expression is too complex"),
+ errdetail("%s", nodeToString(node))));
+ return true;
+ }
+ break;
+ }
+
+ return expression_tree_walker(node, foreign_expr_walker, context);
+ }
+
+ /*
+ * Return true if func is known as safe to be pushed down if all of the
+ * arguments are also known as safe.
+ */
+ static bool
+ is_proc_remotely_executable(Oid procid)
+ {
+ /*
+ * User-defined procedures can't be pushed down.
+ */
+ if (procid >= FirstNormalObjectId)
+ return false;
+
+ return true;
+ }
+
diff --git a/contrib/pgsql_fdw/expected/pgsql_fdw.out b/contrib/pgsql_fdw/expected/pgsql_fdw.out
index ...a26c775 .
*** a/contrib/pgsql_fdw/expected/pgsql_fdw.out
--- b/contrib/pgsql_fdw/expected/pgsql_fdw.out
***************
*** 0 ****
--- 1,462 ----
+ -- ===================================================================
+ -- create FDW objects
+ -- ===================================================================
+ CREATE EXTENSION pgsql_fdw;
+ CREATE SERVER loopback1 FOREIGN DATA WRAPPER pgsql_fdw;
+ CREATE SERVER loopback2 FOREIGN DATA WRAPPER pgsql_fdw
+ OPTIONS (dbname 'contrib_regression');
+ CREATE USER MAPPING FOR public SERVER loopback1
+ OPTIONS (user 'value', password 'value');
+ CREATE USER MAPPING FOR public SERVER loopback2;
+ CREATE FOREIGN TABLE ft1 (
+ c1 int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp
+ ) SERVER loopback2;
+ CREATE FOREIGN TABLE ft2 (
+ c1 int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp
+ ) SERVER loopback2;
+ -- ===================================================================
+ -- create objects used through FDW
+ -- ===================================================================
+ CREATE SCHEMA "S 1";
+ CREATE TABLE "S 1"."T 1" (
+ "C 1" int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ CONSTRAINT t1_pkey PRIMARY KEY ("C 1")
+ );
+ NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "t1_pkey" for table "T 1"
+ CREATE TABLE "S 1"."T 2" (
+ c1 int NOT NULL,
+ c2 text,
+ CONSTRAINT t2_pkey PRIMARY KEY (c1)
+ );
+ NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "t2_pkey" for table "T 2"
+ BEGIN;
+ TRUNCATE "S 1"."T 1";
+ INSERT INTO "S 1"."T 1"
+ SELECT id,
+ id % 10,
+ to_char(id, 'FM00000'),
+ '1970-01-01'::timestamptz + ((id % 100) || ' days')::interval,
+ '1970-01-01'::timestamp + ((id % 100) || ' days')::interval
+ FROM generate_series(1, 1000) id;
+ TRUNCATE "S 1"."T 2";
+ INSERT INTO "S 1"."T 2"
+ SELECT id,
+ 'AAA' || to_char(id, 'FM000')
+ FROM generate_series(1, 100) id;
+ COMMIT;
+ -- ===================================================================
+ -- tests for pgsql_fdw_validator
+ -- ===================================================================
+ ALTER FOREIGN DATA WRAPPER pgsql_fdw OPTIONS (host 'value'); -- ERROR
+ ERROR: invalid option "host"
+ HINT: Valid options in this context are:
+ -- requiressl, krbsrvname and gsslib are omitted because they depend on
+ -- configure option
+ ALTER SERVER loopback1 OPTIONS (
+ authtype 'value',
+ service 'value',
+ connect_timeout 'value',
+ dbname 'value',
+ host 'value',
+ hostaddr 'value',
+ port 'value',
+ --client_encoding 'value',
+ tty 'value',
+ options 'value',
+ application_name 'value',
+ --fallback_application_name 'value',
+ keepalives 'value',
+ keepalives_idle 'value',
+ keepalives_interval 'value',
+ -- requiressl 'value',
+ sslmode 'value',
+ sslcert 'value',
+ sslkey 'value',
+ sslrootcert 'value',
+ sslcrl 'value'
+ --requirepeer 'value',
+ -- krbsrvname 'value',
+ -- gsslib 'value',
+ --replication 'value'
+ );
+ ALTER SERVER loopback1 OPTIONS (user 'value'); -- ERROR
+ ERROR: invalid option "user"
+ HINT: Valid options in this context are: authtype, service, connect_timeout, dbname, host, hostaddr, port, tty, options, application_name, keepalives, keepalives_idle, keepalives_interval, keepalives_count, sslmode, sslcert, sslkey, sslrootcert, sslcrl, requirepeer, fetch_count
+ ALTER SERVER loopback2 OPTIONS (ADD fetch_count '2');
+ ALTER USER MAPPING FOR public SERVER loopback1
+ OPTIONS (DROP user, DROP password);
+ ALTER USER MAPPING FOR public SERVER loopback1
+ OPTIONS (host 'value'); -- ERROR
+ ERROR: invalid option "host"
+ HINT: Valid options in this context are: user, password
+ ALTER FOREIGN TABLE ft1 OPTIONS (nspname 'S 1', relname 'T 1');
+ ALTER FOREIGN TABLE ft2 OPTIONS (nspname 'S 1', relname 'T 1', fetch_count '100');
+ ALTER FOREIGN TABLE ft1 OPTIONS (invalid 'value'); -- ERROR
+ ERROR: invalid option "invalid"
+ HINT: Valid options in this context are: nspname, relname, fetch_count
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count 'a'); -- ERROR
+ ERROR: invalid value for fetch_count: "a"
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count '0'); -- ERROR
+ ERROR: invalid value for fetch_count: "0"
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count '-1'); -- ERROR
+ ERROR: invalid value for fetch_count: "-1"
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (invalid 'value'); -- ERROR
+ ERROR: invalid option "invalid"
+ HINT: Valid options in this context are: colname
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (colname 'C 1');
+ ALTER FOREIGN TABLE ft2 ALTER COLUMN c1 OPTIONS (colname 'C 1');
+ \dew+
+ List of foreign-data wrappers
+ Name | Owner | Handler | Validator | Access privileges | FDW Options | Description
+ -----------+----------+-------------------+---------------------+-------------------+-------------+-------------
+ pgsql_fdw | postgres | pgsql_fdw_handler | pgsql_fdw_validator | | |
+ (1 row)
+
+ \des+
+ List of foreign servers
+ Name | Owner | Foreign-data wrapper | Access privileges | Type | Version | FDW Options | Description
+ -----------+----------+----------------------+-------------------+------+---------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+-------------
+ loopback1 | postgres | pgsql_fdw | | | | (authtype 'value', service 'value', connect_timeout 'value', dbname 'value', host 'value', hostaddr 'value', port 'value', tty 'value', options 'value', application_name 'value', keepalives 'value', keepalives_idle 'value', keepalives_interval 'value', sslmode 'value', sslcert 'value', sslkey 'value', sslrootcert 'value', sslcrl 'value') |
+ loopback2 | postgres | pgsql_fdw | | | | (dbname 'contrib_regression', fetch_count '2') |
+ (2 rows)
+
+ \deu+
+ List of user mappings
+ Server | User name | FDW Options
+ -----------+-----------+-------------
+ loopback1 | public |
+ loopback2 | public |
+ (2 rows)
+
+ \det+
+ List of foreign tables
+ Schema | Table | Server | FDW Options | Description
+ --------+-------+-----------+---------------------------------------------------+-------------
+ public | ft1 | loopback2 | (nspname 'S 1', relname 'T 1') |
+ public | ft2 | loopback2 | (nspname 'S 1', relname 'T 1', fetch_count '100') |
+ (2 rows)
+
+ -- ===================================================================
+ -- simple queries
+ -- ===================================================================
+ -- single table, with/without alias
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
+ QUERY PLAN
+ ----------------------------------------------------------------------------------------------------------------------
+ Limit
+ Output: c1, c2, c3, c4, c5
+ -> Sort
+ Output: c1, c2, c3, c4, c5
+ Sort Key: ft1.c3, ft1.c1
+ -> Foreign Scan on public.ft1
+ Output: c1, c2, c3, c4, c5
+ Remote SQL: DECLARE pgsql_fdw_cursor_0 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5 FROM "S 1"."T 1"
+ (8 rows)
+
+ SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
+ c1 | c2 | c3 | c4 | c5
+ -----+----+-------+------------------------------+--------------------------
+ 101 | 1 | 00101 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970
+ 102 | 2 | 00102 | Sat Jan 03 00:00:00 1970 PST | Sat Jan 03 00:00:00 1970
+ 103 | 3 | 00103 | Sun Jan 04 00:00:00 1970 PST | Sun Jan 04 00:00:00 1970
+ 104 | 4 | 00104 | Mon Jan 05 00:00:00 1970 PST | Mon Jan 05 00:00:00 1970
+ 105 | 5 | 00105 | Tue Jan 06 00:00:00 1970 PST | Tue Jan 06 00:00:00 1970
+ 106 | 6 | 00106 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970
+ 107 | 7 | 00107 | Thu Jan 08 00:00:00 1970 PST | Thu Jan 08 00:00:00 1970
+ 108 | 8 | 00108 | Fri Jan 09 00:00:00 1970 PST | Fri Jan 09 00:00:00 1970
+ 109 | 9 | 00109 | Sat Jan 10 00:00:00 1970 PST | Sat Jan 10 00:00:00 1970
+ 110 | 0 | 00110 | Sun Jan 11 00:00:00 1970 PST | Sun Jan 11 00:00:00 1970
+ (10 rows)
+
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+ QUERY PLAN
+ ----------------------------------------------------------------------------------------------------------------------
+ Limit
+ Output: c1, c2, c3, c4, c5
+ -> Sort
+ Output: c1, c2, c3, c4, c5
+ Sort Key: t1.c3, t1.c1
+ -> Foreign Scan on public.ft1 t1
+ Output: c1, c2, c3, c4, c5
+ Remote SQL: DECLARE pgsql_fdw_cursor_2 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5 FROM "S 1"."T 1"
+ (8 rows)
+
+ SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+ c1 | c2 | c3 | c4 | c5
+ -----+----+-------+------------------------------+--------------------------
+ 101 | 1 | 00101 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970
+ 102 | 2 | 00102 | Sat Jan 03 00:00:00 1970 PST | Sat Jan 03 00:00:00 1970
+ 103 | 3 | 00103 | Sun Jan 04 00:00:00 1970 PST | Sun Jan 04 00:00:00 1970
+ 104 | 4 | 00104 | Mon Jan 05 00:00:00 1970 PST | Mon Jan 05 00:00:00 1970
+ 105 | 5 | 00105 | Tue Jan 06 00:00:00 1970 PST | Tue Jan 06 00:00:00 1970
+ 106 | 6 | 00106 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970
+ 107 | 7 | 00107 | Thu Jan 08 00:00:00 1970 PST | Thu Jan 08 00:00:00 1970
+ 108 | 8 | 00108 | Fri Jan 09 00:00:00 1970 PST | Fri Jan 09 00:00:00 1970
+ 109 | 9 | 00109 | Sat Jan 10 00:00:00 1970 PST | Sat Jan 10 00:00:00 1970
+ 110 | 0 | 00110 | Sun Jan 11 00:00:00 1970 PST | Sun Jan 11 00:00:00 1970
+ (10 rows)
+
+ -- with WHERE clause
+ SELECT * FROM ft1 t1 WHERE t1.c1 = 101;
+ c1 | c2 | c3 | c4 | c5
+ -----+----+-------+------------------------------+--------------------------
+ 101 | 1 | 00101 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970
+ (1 row)
+
+ -- aggregate
+ SELECT COUNT(*) FROM ft1 t1;
+ count
+ -------
+ 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
+ ----+----+-------+------------------------------+--------------------------
+ 1 | 1 | 00001 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970
+ 2 | 2 | 00002 | Sat Jan 03 00:00:00 1970 PST | Sat Jan 03 00:00:00 1970
+ 3 | 3 | 00003 | Sun Jan 04 00:00:00 1970 PST | Sun Jan 04 00:00:00 1970
+ 4 | 4 | 00004 | Mon Jan 05 00:00:00 1970 PST | Mon Jan 05 00:00:00 1970
+ 5 | 5 | 00005 | Tue Jan 06 00:00:00 1970 PST | Tue Jan 06 00:00:00 1970
+ 6 | 6 | 00006 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970
+ 7 | 7 | 00007 | Thu Jan 08 00:00:00 1970 PST | Thu Jan 08 00:00:00 1970
+ 8 | 8 | 00008 | Fri Jan 09 00:00:00 1970 PST | Fri Jan 09 00:00:00 1970
+ 9 | 9 | 00009 | Sat Jan 10 00:00:00 1970 PST | Sat Jan 10 00:00:00 1970
+ 10 | 0 | 00010 | Sun Jan 11 00:00:00 1970 PST | Sun Jan 11 00:00:00 1970
+ (10 rows)
+
+ -- subquery+MAX
+ SELECT * FROM ft1 t1 WHERE t1.c3 = (SELECT MAX(c3) FROM ft2 t2) ORDER BY c1;
+ c1 | c2 | c3 | c4 | c5
+ ------+----+-------+------------------------------+--------------------------
+ 1000 | 0 | 01000 | Thu Jan 01 00:00:00 1970 PST | Thu Jan 01 00:00:00 1970
+ (1 row)
+
+ -- used in CTE
+ WITH t1 AS (SELECT * FROM ft1 WHERE c1 <= 10) SELECT t2.c1, t2.c2, t2.c3, t2.c4 FROM t1, ft2 t2 WHERE t1.c1 = t2.c1 ORDER BY t1.c1;
+ c1 | c2 | c3 | c4
+ ----+----+-------+------------------------------
+ 1 | 1 | 00001 | Fri Jan 02 00:00:00 1970 PST
+ 2 | 2 | 00002 | Sat Jan 03 00:00:00 1970 PST
+ 3 | 3 | 00003 | Sun Jan 04 00:00:00 1970 PST
+ 4 | 4 | 00004 | Mon Jan 05 00:00:00 1970 PST
+ 5 | 5 | 00005 | Tue Jan 06 00:00:00 1970 PST
+ 6 | 6 | 00006 | Wed Jan 07 00:00:00 1970 PST
+ 7 | 7 | 00007 | Thu Jan 08 00:00:00 1970 PST
+ 8 | 8 | 00008 | Fri Jan 09 00:00:00 1970 PST
+ 9 | 9 | 00009 | Sat Jan 10 00:00:00 1970 PST
+ 10 | 0 | 00010 | Sun Jan 11 00:00:00 1970 PST
+ (10 rows)
+
+ -- fixed values
+ SELECT 'fixed', NULL FROM ft1 t1 WHERE c1 = 1;
+ ?column? | ?column?
+ ----------+----------
+ fixed |
+ (1 row)
+
+ -- ===================================================================
+ -- parameterized queries
+ -- ===================================================================
+ -- simple join
+ PREPARE st1(int, int) AS SELECT t1.c3, t2.c3 FROM ft1 t1, ft2 t2 WHERE t1.c1 = $1 AND t2.c1 = $2;
+ EXPLAIN (COSTS false) EXECUTE st1(1, 2);
+ QUERY PLAN
+ ------------------------------------------------------------------------------------------------------------------------------------------
+ Nested Loop
+ -> Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_17 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, NULL, NULL FROM "S 1"."T 1" WHERE ("C 1" = 1)
+ -> Foreign Scan on ft2 t2
+ Filter: (c1 = 2)
+ Remote SQL: DECLARE pgsql_fdw_cursor_18 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, NULL, NULL FROM "S 1"."T 1" WHERE ("C 1" = 2)
+ (7 rows)
+
+ EXECUTE st1(1, 1);
+ c3 | c3
+ -------+-------
+ 00001 | 00001
+ (1 row)
+
+ EXECUTE st1(101, 101);
+ c3 | c3
+ -------+-------
+ 00101 | 00101
+ (1 row)
+
+ -- subquery using stable function (can't be pushed down)
+ PREPARE st2(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c4) = 6) ORDER BY c1;
+ EXPLAIN (COSTS false) EXECUTE st2(10, 20);
+ QUERY PLAN
+ -----------------------------------------------------------------------------------------------------------------------------------------------------
+ Sort
+ Sort Key: t1.c1
+ -> Nested Loop
+ Join Filter: (t1.c3 = t2.c3)
+ -> HashAggregate
+ -> Foreign Scan on ft2 t2
+ Filter: ((c1 > 10) AND (date_part('dow'::text, c4) = 6::double precision))
+ Remote SQL: DECLARE pgsql_fdw_cursor_24 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, c4, NULL FROM "S 1"."T 1" WHERE ("C 1" > 10)
+ -> Foreign Scan on ft1 t1
+ Filter: (c1 < 20)
+ Remote SQL: DECLARE pgsql_fdw_cursor_23 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5 FROM "S 1"."T 1" WHERE ("C 1" < 20)
+ (11 rows)
+
+ EXECUTE st2(10, 20);
+ c1 | c2 | c3 | c4 | c5
+ ----+----+-------+------------------------------+--------------------------
+ 16 | 6 | 00016 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970
+ (1 row)
+
+ EXECUTE st1(101, 101);
+ c3 | c3
+ -------+-------
+ 00101 | 00101
+ (1 row)
+
+ -- subquery using immutable function (can be pushed down)
+ PREPARE st3(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c5) = 6) ORDER BY c1;
+ EXPLAIN (COSTS false) EXECUTE st3(10, 20);
+ QUERY PLAN
+ --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Sort
+ Sort Key: t1.c1
+ -> Hash Join
+ Hash Cond: (t1.c3 = t2.c3)
+ -> Foreign Scan on ft1 t1
+ Filter: (c1 < 20)
+ Remote SQL: DECLARE pgsql_fdw_cursor_29 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5 FROM "S 1"."T 1" WHERE ("C 1" < 20)
+ -> Hash
+ -> HashAggregate
+ -> Foreign Scan on ft2 t2
+ Filter: ((c1 > 10) AND (date_part('dow'::text, c5) = 6::double precision))
+ Remote SQL: DECLARE pgsql_fdw_cursor_30 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, NULL, c5 FROM "S 1"."T 1" WHERE (("C 1" > 10) AND (date_part('dow'::text, c5) = 6::double precision))
+ (12 rows)
+
+ EXECUTE st3(10, 20);
+ c1 | c2 | c3 | c4 | c5
+ ----+----+-------+------------------------------+--------------------------
+ 16 | 6 | 00016 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970
+ (1 row)
+
+ EXECUTE st3(20, 30);
+ c1 | c2 | c3 | c4 | c5
+ ----+----+-------+------------------------------+--------------------------
+ 23 | 3 | 00023 | Sat Jan 24 00:00:00 1970 PST | Sat Jan 24 00:00:00 1970
+ (1 row)
+
+ -- custom plan should be chosen
+ PREPARE st4(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 = $1;
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ ------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_35 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5 FROM "S 1"."T 1" WHERE ("C 1" = 1)
+ (3 rows)
+
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ ------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_36 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5 FROM "S 1"."T 1" WHERE ("C 1" = 1)
+ (3 rows)
+
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ ------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_37 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5 FROM "S 1"."T 1" WHERE ("C 1" = 1)
+ (3 rows)
+
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ ------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_38 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5 FROM "S 1"."T 1" WHERE ("C 1" = 1)
+ (3 rows)
+
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ ------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_39 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5 FROM "S 1"."T 1" WHERE ("C 1" = 1)
+ (3 rows)
+
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ ------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_41 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5 FROM "S 1"."T 1" WHERE ("C 1" = 1)
+ (3 rows)
+
+ -- cleanup
+ DEALLOCATE st1;
+ DEALLOCATE st2;
+ DEALLOCATE st3;
+ DEALLOCATE st4;
+ -- ===================================================================
+ -- connection management
+ -- ===================================================================
+ SELECT srvname, usename FROM pgsql_fdw_connections;
+ srvname | usename
+ -----------+----------
+ loopback2 | postgres
+ (1 row)
+
+ SELECT pgsql_fdw_disconnect(srvid, usesysid) FROM pgsql_fdw_get_connections();
+ pgsql_fdw_disconnect
+ ----------------------
+ OK
+ (1 row)
+
+ SELECT srvname, usename FROM pgsql_fdw_connections;
+ srvname | usename
+ ---------+---------
+ (0 rows)
+
+ -- ===================================================================
+ -- cleanup
+ -- ===================================================================
+ DROP EXTENSION pgsql_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 server loopback2
+ drop cascades to user mapping for public
+ drop cascades to foreign table ft1
+ drop cascades to foreign table ft2
diff --git a/contrib/pgsql_fdw/option.c b/contrib/pgsql_fdw/option.c
index ...e123cba .
*** a/contrib/pgsql_fdw/option.c
--- b/contrib/pgsql_fdw/option.c
***************
*** 0 ****
--- 1,246 ----
+ /*-------------------------------------------------------------------------
+ *
+ * option.c
+ * FDW option handling
+ *
+ * Copyright (c) 2011, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/option.c
+ *
+ *-------------------------------------------------------------------------
+ */
+ #include "postgres.h"
+
+ #include "access/reloptions.h"
+ #include "catalog/pg_foreign_data_wrapper.h"
+ #include "catalog/pg_foreign_server.h"
+ #include "catalog/pg_foreign_table.h"
+ #include "catalog/pg_user_mapping.h"
+ #include "fmgr.h"
+ #include "foreign/foreign.h"
+ #include "lib/stringinfo.h"
+ #include "miscadmin.h"
+
+ #include "pgsql_fdw.h"
+
+ /*
+ * SQL functions
+ */
+ extern Datum pgsql_fdw_validator(PG_FUNCTION_ARGS);
+ PG_FUNCTION_INFO_V1(pgsql_fdw_validator);
+
+ /*
+ * Describes the valid options for objects that use this wrapper.
+ */
+ typedef struct PgsqlFdwOption
+ {
+ const char *optname;
+ Oid optcontext; /* Oid of catalog in which options may appear */
+ bool is_libpq_opt; /* true if it's used in libpq */
+ } PgsqlFdwOption;
+
+ /*
+ * Valid options for pgsql_fdw.
+ */
+ static PgsqlFdwOption valid_options[] = {
+
+ /*
+ * Options for libpq connection.
+ * Note: This list should be updated along with PQconninfoOptions in
+ * interfaces/libpq/fe-connect.c, so the order is kept as is.
+ *
+ * Some useless libpq connection options are not accepted by pgsql_fdw:
+ * client_encoding: set to local database encoding automatically
+ * fallback_application_name: fixed to "pgsql_fdw"
+ * replication: pgsql_fdw never be replication client
+ */
+ {"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},
+ #ifdef NOT_USED
+ {"client_encoding", ForeignServerRelationId, true},
+ #endif
+ {"tty", ForeignServerRelationId, true},
+ {"options", ForeignServerRelationId, true},
+ {"application_name", ForeignServerRelationId, true},
+ #ifdef NOT_USED
+ {"fallback_application_name", ForeignServerRelationId, true},
+ #endif
+ {"keepalives", ForeignServerRelationId, true},
+ {"keepalives_idle", ForeignServerRelationId, true},
+ {"keepalives_interval", ForeignServerRelationId, true},
+ {"keepalives_count", ForeignServerRelationId, true},
+ #ifdef USE_SSL
+ {"requiressl", ForeignServerRelationId, true},
+ #endif
+ {"sslmode", ForeignServerRelationId, true},
+ {"sslcert", ForeignServerRelationId, true},
+ {"sslkey", ForeignServerRelationId, true},
+ {"sslrootcert", ForeignServerRelationId, true},
+ {"sslcrl", ForeignServerRelationId, true},
+ {"requirepeer", ForeignServerRelationId, true},
+ #if defined(KRB5) || defined(ENABLE_GSS) || defined(ENABLE_SSPI)
+ {"krbsrvname", ForeignServerRelationId, true},
+ #endif
+ #if defined(ENABLE_GSS) && defined(ENABLE_SSPI)
+ {"gsslib", ForeignServerRelationId, true},
+ #endif
+ #ifdef NOT_USED
+ {"replication", ForeignServerRelationId, true},
+ #endif
+
+ /*
+ * Options for translation of object names.
+ */
+ {"nspname", ForeignTableRelationId, false},
+ {"relname", ForeignTableRelationId, false},
+ {"colname", AttributeRelationId, false},
+
+ /*
+ * Options for cursor behavior.
+ * These options can be overridden by finer-grained objects.
+ */
+ {"fetch_count", ForeignTableRelationId, false},
+ {"fetch_count", ForeignServerRelationId, false},
+
+ /* Terminating entry --- MUST BE LAST */
+ {NULL, InvalidOid, false}
+ };
+
+ /*
+ * Helper functions
+ */
+ static bool is_valid_option(const char *optname, Oid context);
+
+ /*
+ * Validate the generic options given to a FOREIGN DATA WRAPPER, SERVER,
+ * USER MAPPING or FOREIGN TABLE that uses pgsql_fdw.
+ *
+ * Raise an ERROR if the option or its value is considered invalid.
+ */
+ Datum
+ pgsql_fdw_validator(PG_FUNCTION_ARGS)
+ {
+ List *options_list = untransformRelOptions(PG_GETARG_DATUM(0));
+ Oid catalog = PG_GETARG_OID(1);
+ ListCell *cell;
+
+ /*
+ * Check that only options supported by pgsql_fdw, and allowed for the
+ * current object type, are given.
+ */
+ foreach(cell, options_list)
+ {
+ DefElem *def = (DefElem *) lfirst(cell);
+
+ if (!is_valid_option(def->defname, catalog))
+ {
+ PgsqlFdwOption *opt;
+ StringInfoData buf;
+
+ /*
+ * Unknown option specified, complain about it. Provide a hint
+ * with list of valid options for the object.
+ */
+ initStringInfo(&buf);
+ for (opt = valid_options; opt->optname; opt++)
+ {
+ if (catalog == opt->optcontext)
+ appendStringInfo(&buf, "%s%s", (buf.len > 0) ? ", " : "",
+ opt->optname);
+ }
+
+ ereport(ERROR,
+ (errcode(ERRCODE_FDW_INVALID_OPTION_NAME),
+ errmsg("invalid option \"%s\"", def->defname),
+ errhint("Valid options in this context are: %s",
+ buf.data)));
+ }
+
+ /* fetch_count be positive digit number. */
+ if (strcmp(def->defname, "fetch_count") == 0)
+ {
+ long value;
+ char *p = NULL;
+
+ value = strtol(strVal(def->arg), &p, 10);
+ if (*p != '\0' || value < 1)
+ ereport(ERROR,
+ (errcode(ERRCODE_FDW_INVALID_ATTRIBUTE_VALUE),
+ errmsg("invalid value for %s: \"%s\"",
+ def->defname, strVal(def->arg))));
+ }
+ }
+
+ /*
+ * We don't care option-specific limitation here; they will be validated at
+ * the execution time.
+ */
+
+ PG_RETURN_VOID();
+ }
+
+ /*
+ * Check whether the given option is one of the valid pgsql_fdw options.
+ * context is the Oid of the catalog holding the object the option is for.
+ */
+ static bool
+ is_valid_option(const char *optname, Oid context)
+ {
+ PgsqlFdwOption *opt;
+
+ for (opt = valid_options; opt->optname; opt++)
+ {
+ if (context == opt->optcontext && strcmp(opt->optname, optname) == 0)
+ return true;
+ }
+ return false;
+ }
+
+ /*
+ * Check whether the given option is one of the valid libpq options.
+ * context is the Oid of the catalog holding the object the option is for.
+ */
+ static bool
+ is_libpq_option(const char *optname)
+ {
+ PgsqlFdwOption *opt;
+
+ for (opt = valid_options; opt->optname; opt++)
+ {
+ if (strcmp(opt->optname, optname) == 0 && opt->is_libpq_opt)
+ return true;
+ }
+ return false;
+ }
+
+ /*
+ * Generate key-value arrays which includes only libpq options from the list
+ * which contains any kind of options.
+ */
+ int
+ ExtractConnectionOptions(List *defelems, const char **keywords, const char **values)
+ {
+ ListCell *lc;
+ int i;
+
+ i = 0;
+ foreach(lc, defelems)
+ {
+ DefElem *d = (DefElem *) lfirst(lc);
+ if (is_libpq_option(d->defname))
+ {
+ keywords[i] = d->defname;
+ values[i] = strVal(d->arg);
+ i++;
+ }
+ }
+ return i;
+ }
diff --git a/contrib/pgsql_fdw/pgsql_fdw--1.0.sql b/contrib/pgsql_fdw/pgsql_fdw--1.0.sql
index ...b0ea2b2 .
*** a/contrib/pgsql_fdw/pgsql_fdw--1.0.sql
--- b/contrib/pgsql_fdw/pgsql_fdw--1.0.sql
***************
*** 0 ****
--- 1,39 ----
+ /* contrib/pgsql_fdw/pgsql_fdw--1.0.sql */
+
+ -- complain if script is sourced in psql, rather than via CREATE EXTENSION
+ \echo Use "CREATE EXTENSION pgsql_fdw" to load this file. \quit
+
+ CREATE FUNCTION pgsql_fdw_handler()
+ RETURNS fdw_handler
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C STRICT;
+
+ CREATE FUNCTION pgsql_fdw_validator(text[], oid)
+ RETURNS void
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C STRICT;
+
+ CREATE FOREIGN DATA WRAPPER pgsql_fdw
+ HANDLER pgsql_fdw_handler
+ VALIDATOR pgsql_fdw_validator;
+
+ /* connection management functions and view */
+ CREATE FUNCTION pgsql_fdw_get_connections(out srvid oid, out usesysid oid)
+ RETURNS SETOF record
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C STRICT;
+
+ CREATE FUNCTION pgsql_fdw_disconnect(oid, oid)
+ RETURNS text
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C STRICT;
+
+ CREATE VIEW pgsql_fdw_connections AS
+ SELECT c.srvid srvid,
+ s.srvname srvname,
+ c.usesysid usesysid,
+ pg_get_userbyid(c.usesysid) usename
+ FROM pgsql_fdw_get_connections() c
+ JOIN pg_catalog.pg_foreign_server s ON (s.oid = c.srvid);
+ GRANT SELECT ON pgsql_fdw_connections TO public;
+
diff --git a/contrib/pgsql_fdw/pgsql_fdw.c b/contrib/pgsql_fdw/pgsql_fdw.c
index ...61d20ee .
*** a/contrib/pgsql_fdw/pgsql_fdw.c
--- b/contrib/pgsql_fdw/pgsql_fdw.c
***************
*** 0 ****
--- 1,871 ----
+ /*-------------------------------------------------------------------------
+ *
+ * pgsql_fdw.c
+ * foreign-data wrapper for remote PostgreSQL servers.
+ *
+ * Copyright (c) 2011, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/pgsql_fdw.c
+ *
+ *-------------------------------------------------------------------------
+ */
+ #include "postgres.h"
+ #include "fmgr.h"
+
+ #include "catalog/pg_foreign_server.h"
+ #include "catalog/pg_foreign_table.h"
+ #include "commands/explain.h"
+ #include "foreign/fdwapi.h"
+ #include "funcapi.h"
+ #include "miscadmin.h"
+ #include "nodes/nodeFuncs.h"
+ #include "optimizer/cost.h"
+ #include "optimizer/pathnode.h"
+ #include "utils/lsyscache.h"
+ #include "utils/memutils.h"
+ #include "utils/rel.h"
+
+ #include "pgsql_fdw.h"
+ #include "connection.h"
+
+ PG_MODULE_MAGIC;
+
+ /*
+ * Default fetch count for cursor. This can be overridden by fetch_count FDW
+ * option.
+ */
+ #define DEFAULT_FETCH_COUNT 10000
+
+ /*
+ * Cost to establish a connection.
+ * XXX: should be configurable per server?
+ */
+ #define CONNECTION_COSTS 100.0
+
+ /*
+ * Cost to transfer 1 byte from remote server.
+ * XXX: should be configurable per server?
+ */
+ #define TRANSFER_COSTS_PER_BYTE 0.001
+
+ /*
+ * Cursors which are used together in a local query require different name, so
+ * we use simple incremental name for that purpose. We don't care wrap around
+ * of cursor_id because it's hard to imagine that 2^32 cursors are used in a
+ * query.
+ */
+ #define CURSOR_NAME_FORMAT "pgsql_fdw_cursor_%u"
+ static uint32 cursor_id = 0;
+
+ /*
+ * Index of FDW-private items stored in FdwPlan.
+ */
+ enum FdwPrivateIndex {
+ FdwPrivateSelectSql,
+ FdwPrivateDeclareSql,
+ FdwPrivateFetchSql,
+ FdwPrivateResetSql,
+ FdwPrivateCloseSql,
+
+ /* # of elements stored in the list fdw_private */
+ FdwPrivateNum,
+ };
+
+ /*
+ * Describes an execution state of a foreign scan against a foreign table
+ * using pgsql_fdw.
+ */
+ typedef struct PgsqlFdwExecutionState
+ {
+ FdwPlan *fdwplan; /* FDW-specific planning information */
+ PGconn *conn; /* connection for the scan */
+
+ Oid *param_types; /* type array of external parameter */
+ const char **param_values; /* value array of external parameter */
+
+ int attnum; /* # of non-dropped attribute */
+ char **col_values; /* column value buffer */
+ AttInMetadata *attinmeta; /* attribute metadata */
+
+ Tuplestorestate *tuples; /* result of the scan */
+ bool cursor_opened; /* true if cursor has been opened */
+ } PgsqlFdwExecutionState;
+
+ /*
+ * SQL functions
+ */
+ extern Datum pgsql_fdw_handler(PG_FUNCTION_ARGS);
+ PG_FUNCTION_INFO_V1(pgsql_fdw_handler);
+
+ /*
+ * FDW callback routines
+ */
+ static FdwPlan *pgsqlPlanForeignScan(Oid foreigntableid,
+ PlannerInfo *root,
+ RelOptInfo *baserel);
+ static void pgsqlExplainForeignScan(ForeignScanState *node, ExplainState *es);
+ static void pgsqlBeginForeignScan(ForeignScanState *node, int eflags);
+ static TupleTableSlot *pgsqlIterateForeignScan(ForeignScanState *node);
+ static void pgsqlReScanForeignScan(ForeignScanState *node);
+ static void pgsqlEndForeignScan(ForeignScanState *node);
+
+ /*
+ * Helper functions
+ */
+ static void estimate_costs(PlannerInfo *root,
+ RelOptInfo *baserel,
+ const char *sql,
+ Oid serverid,
+ Cost *startup_cost,
+ Cost *total_cost);
+ static void execute_query(ForeignScanState *node);
+ static PGresult *fetch_result(ForeignScanState *node);
+ static void store_result(ForeignScanState *node, PGresult *res);
+ static bool contain_ext_param(Node *clause);
+ static bool contain_ext_param_walker(Node *node, void *context);
+
+ /*
+ * Foreign-data wrapper handler function: return a struct with pointers
+ * to my callback routines.
+ */
+ Datum
+ pgsql_fdw_handler(PG_FUNCTION_ARGS)
+ {
+ FdwRoutine *fdwroutine = makeNode(FdwRoutine);
+
+ fdwroutine->PlanForeignScan = pgsqlPlanForeignScan;
+ fdwroutine->ExplainForeignScan = pgsqlExplainForeignScan;
+ fdwroutine->BeginForeignScan = pgsqlBeginForeignScan;
+ fdwroutine->IterateForeignScan = pgsqlIterateForeignScan;
+ fdwroutine->ReScanForeignScan = pgsqlReScanForeignScan;
+ fdwroutine->EndForeignScan = pgsqlEndForeignScan;
+
+ PG_RETURN_POINTER(fdwroutine);
+ }
+
+ /*
+ * pgsqlPlanForeignScan
+ * Create a FdwPlan for a scan on the foreign table
+ */
+ static FdwPlan *
+ pgsqlPlanForeignScan(Oid foreigntableid,
+ PlannerInfo *root,
+ RelOptInfo *baserel)
+ {
+ char name[128]; /* must be larger than format + 10 */
+ StringInfoData cursor;
+ const char *fetch_count_str;
+ int fetch_count = DEFAULT_FETCH_COUNT;
+ char *sql;
+ FdwPlan *fdwplan;
+ List *fdw_private = NIL;
+ ForeignTable *table;
+ ForeignServer *server;
+
+ /* Construct FdwPlan with cost estimates */
+ fdwplan = makeNode(FdwPlan);
+ sql = deparseSql(foreigntableid, root, baserel);
+ table = GetForeignTable(foreigntableid);
+ server = GetForeignServer(table->serverid);
+ estimate_costs(root, baserel, sql, server->serverid,
+ &fdwplan->startup_cost, &fdwplan->total_cost);
+
+ /*
+ * Store plain SELECT statement in private area of FdwPlan. This will be
+ * used for executing remote query and explaining scan.
+ */
+ fdw_private = list_make1(makeString(sql));
+
+ /* Use specified fetch_count instead of default value, if any. */
+ fetch_count_str = GetFdwOptionValue(InvalidOid, InvalidOid, foreigntableid,
+ InvalidAttrNumber, "fetch_count");
+ if (fetch_count_str != NULL)
+ fetch_count = strtol(fetch_count_str, NULL, 10);
+ elog(DEBUG1, "relid=%u fetch_count=%d", foreigntableid, fetch_count);
+
+ /*
+ * We store some more information in FdwPlan to pass them beyond the
+ * boundary between planner and executor. Finally FdwPlan using cursor
+ * would hold items below:
+ *
+ * 1) plain SELECT statement (already added above)
+ * 2) SQL statement used to declare cursor
+ * 3) SQL statement used to fetch rows from cursor
+ * 4) SQL statement used to reset cursor
+ * 5) SQL statement used to close cursor
+ *
+ * These items are indexed with the enum FdwPrivateIndex, so an item
+ * can be accessed directly via list_nth(). For example of FETCH
+ * statement:
+ * list_nth(fdw_private, FdwPrivateFetchSql)
+ */
+
+ /* Construct cursor name from sequential value */
+ sprintf(name, CURSOR_NAME_FORMAT, cursor_id++);
+
+ /* Construct statement to declare cursor */
+ initStringInfo(&cursor);
+ appendStringInfo(&cursor, "DECLARE %s SCROLL CURSOR FOR %s", name, sql);
+ fdw_private = lappend(fdw_private, makeString(cursor.data));
+
+ /* Construct statement to fetch rows from cursor */
+ initStringInfo(&cursor);
+ appendStringInfo(&cursor, "FETCH %d FROM %s", fetch_count, name);
+ fdw_private = lappend(fdw_private, makeString(cursor.data));
+
+ /* Construct statement to reset cursor */
+ initStringInfo(&cursor);
+ appendStringInfo(&cursor, "MOVE ABSOLUTE 0 FROM %s", name);
+ fdw_private = lappend(fdw_private, makeString(cursor.data));
+
+ /* Construct statement to close cursor */
+ initStringInfo(&cursor);
+ appendStringInfo(&cursor, "CLOSE %s", name);
+ fdw_private = lappend(fdw_private, makeString(cursor.data));
+
+ /* Store FDW private information into FdwPlan */
+ fdwplan->fdw_private = fdw_private;
+
+ return fdwplan;
+ }
+
+ /*
+ * pgsqlExplainForeignScan
+ * Produce extra output for EXPLAIN
+ */
+ static void
+ pgsqlExplainForeignScan(ForeignScanState *node, ExplainState *es)
+ {
+ FdwPlan *fdwplan;
+ char *sql;
+
+ fdwplan = ((ForeignScan *) node->ss.ps.plan)->fdwplan;
+ sql = strVal(list_nth(fdwplan->fdw_private, FdwPrivateDeclareSql));
+ ExplainPropertyText("Remote SQL", sql, es);
+ }
+
+ /*
+ * pgsqlBeginForeignScan
+ * Initiate access to a foreign PostgreSQL table.
+ */
+ static void
+ pgsqlBeginForeignScan(ForeignScanState *node, int eflags)
+ {
+ PgsqlFdwExecutionState *festate;
+ PGconn *conn;
+ Oid relid;
+ ForeignTable *table;
+ ForeignServer *server;
+ UserMapping *user;
+ TupleTableSlot *slot = node->ss.ss_ScanTupleSlot;
+
+ /*
+ * Do nothing in EXPLAIN (no ANALYZE) case. node->fdw_state stays NULL.
+ */
+ if (eflags & EXEC_FLAG_EXPLAIN_ONLY)
+ return;
+
+ /*
+ * Save state in node->fdw_state.
+ */
+ festate = (PgsqlFdwExecutionState *) palloc(sizeof(PgsqlFdwExecutionState));
+ festate->fdwplan = ((ForeignScan *) node->ss.ps.plan)->fdwplan;
+
+ /*
+ * Get connection to the foreign server. Connection manager would
+ * establish new connection if necessary.
+ */
+ relid = RelationGetRelid(node->ss.ss_currentRelation);
+ table = GetForeignTable(relid);
+ server = GetForeignServer(table->serverid);
+ user = GetUserMapping(GetOuterUserId(), server->serverid);
+ conn = GetConnection(server, user);
+ festate->conn = conn;
+
+ /* Result will be filled in first Iterate call. */
+ festate->tuples = NULL;
+ festate->cursor_opened = false;
+
+ /* Allocate buffers for column values. */
+ {
+ TupleDesc tupdesc = slot->tts_tupleDescriptor;
+ festate->col_values = palloc(sizeof(char *) * tupdesc->natts);
+ festate->attinmeta = TupleDescGetAttInMetadata(tupdesc);
+ }
+
+ /* Allocate buffers for query parameters. */
+ {
+ ParamListInfo params = node->ss.ps.state->es_param_list_info;
+ int numParams = params ? params->numParams : 0;
+
+ if (numParams > 0)
+ {
+ festate->param_types = palloc0(sizeof(Oid) * numParams);
+ festate->param_values = palloc0(sizeof(char *) * numParams);
+ }
+ else
+ {
+ festate->param_types = NULL;
+ festate->param_values = NULL;
+ }
+ }
+
+
+ /* Store FDW-specific state into ForeignScanState */
+ node->fdw_state = (void *) festate;
+
+ return;
+ }
+
+ /*
+ * pgsqlIterateForeignScan
+ * Retrieve next row from the result set, or clear tuple slot to indicate
+ * EOF.
+ *
+ * Note that using per-query context when retrieving tuples from
+ * tuplestore to ensure that returned tuples can survive until next
+ * iteration because the tuple is released implicitly via ExecClearTuple.
+ * Retrieving a tuple from tuplestore in CurrentMemoryContext (it's a
+ * per-tuple context), ExecClearTuple will free dangling pointer.
+ */
+ static TupleTableSlot *
+ pgsqlIterateForeignScan(ForeignScanState *node)
+ {
+ PgsqlFdwExecutionState *festate;
+ TupleTableSlot *slot = node->ss.ss_ScanTupleSlot;
+ PGresult *res;
+ MemoryContext oldcontext = CurrentMemoryContext;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+
+
+ /*
+ * If this is the first call after Begin, we need to execute remote query.
+ * If the query needs cursor, we declare a cursor at first call and fetch
+ * from it in later calls.
+ */
+ if (festate->tuples == NULL && !festate->cursor_opened)
+ execute_query(node);
+
+ /*
+ * If enough tuples are left in tuplestore, just return next tuple from it.
+ */
+ MemoryContextSwitchTo(node->ss.ps.state->es_query_cxt);
+ if (tuplestore_gettupleslot(festate->tuples, true, false, slot))
+ {
+ MemoryContextSwitchTo(oldcontext);
+ return slot;
+ }
+ MemoryContextSwitchTo(oldcontext);
+
+ /*
+ * Here we need to clear partial result and fetch next bunch of tuples from
+ * from the cursor for the scan. If the fetch returns no tuple, the scan
+ * has reached the end.
+ */
+ res = fetch_result(node);
+ PG_TRY();
+ {
+ store_result(node, res);
+ PQclear(res);
+ res = NULL;
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+
+ /*
+ * If we got more tuples from the server cursor, return next tuple from
+ * tuplestore.
+ */
+ MemoryContextSwitchTo(node->ss.ps.state->es_query_cxt);
+ if (tuplestore_gettupleslot(festate->tuples, true, false, slot))
+ {
+ MemoryContextSwitchTo(oldcontext);
+ return slot;
+ }
+ MemoryContextSwitchTo(oldcontext);
+
+ /* We don't have any result even in remote server cursor. */
+ ExecClearTuple(slot);
+ return slot;
+ }
+
+ /*
+ * pgsqlReScanForeignScan
+ * - Restart this scan by resetting fetch location.
+ */
+ static void
+ pgsqlReScanForeignScan(ForeignScanState *node)
+ {
+ List *fdw_private;
+ char *sql;
+ PGconn *conn;
+ PGresult *res;
+ PgsqlFdwExecutionState *festate;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+
+ /* If we have not opened cursor yet, nothing to do. */
+ if (!festate->cursor_opened)
+ return;
+
+ /* Discard fetch results if any. */
+ if (festate->tuples != NULL)
+ tuplestore_clear(festate->tuples);
+
+ /* Reset cursor */
+ fdw_private = festate->fdwplan->fdw_private;
+ conn = festate->conn;
+ sql = strVal(list_nth(fdw_private, FdwPrivateResetSql));
+ res = PQexec(conn, sql);
+ PG_TRY();
+ {
+ if (PQresultStatus(res) != PGRES_COMMAND_OK)
+ {
+ ereport(ERROR,
+ (errmsg("could not rewind cursor"),
+ errdetail("%s", PQerrorMessage(conn)),
+ errhint("%s", sql)));
+ }
+ PQclear(res);
+ res = NULL;
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+ }
+
+ /*
+ * pgsqlEndForeignScan
+ * Finish scanning foreign table and dispose objects used for this scan
+ */
+ static void
+ pgsqlEndForeignScan(ForeignScanState *node)
+ {
+ List *fdw_private;
+ char *sql;
+ PGconn *conn;
+ PGresult *res;
+ PgsqlFdwExecutionState *festate;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+
+ /* if festate is NULL, we are in EXPLAIN; nothing to do */
+ if (festate == NULL)
+ return;
+
+ /* If we have not opened cursor yet, nothing to do. */
+ if (!festate->cursor_opened)
+ return;
+
+ /* Discard fetch results */
+ if (festate->tuples != NULL)
+ {
+ tuplestore_end(festate->tuples);
+ festate->tuples = NULL;
+ }
+
+ /* Close cursor */
+ fdw_private = festate->fdwplan->fdw_private;
+ conn = festate->conn;
+ sql = strVal(list_nth(fdw_private, FdwPrivateCloseSql));
+ res = PQexec(conn, sql);
+ PG_TRY();
+ {
+ if (PQresultStatus(res) != PGRES_COMMAND_OK)
+ {
+ ereport(ERROR,
+ (errmsg("could not close cursor"),
+ errdetail("%s", PQerrorMessage(conn)),
+ errhint("%s", sql)));
+ }
+ PQclear(res);
+ res = NULL;
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+
+ ReleaseConnection(festate->conn);
+ }
+
+ /*
+ * Estimate costs of scanning a foreign table.
+ */
+ static void
+ estimate_costs(PlannerInfo *root, RelOptInfo *baserel,
+ const char *sql, Oid serverid,
+ Cost *startup_cost, Cost *total_cost)
+ {
+ ListCell *lc;
+ ForeignServer *server;
+ UserMapping *user;
+ PGconn *conn = NULL;
+ PGresult *res = NULL;
+ StringInfoData buf;
+ char *plan;
+ char *p;
+ char *endp;
+
+ /*
+ * If the baserestrictinfo contains any Param node with paramkind
+ * PARAM_EXTERNAL, we need result of EXPLAIN for EXECUTE statement, not for
+ * simple SELECT statement. However, we can't get actual parameter values
+ * here, so we use fixed and large costs as second best so that planner
+ * tend to choose custom plan.
+ *
+ * See comments in plancache.c for details of custom plan.
+ */
+ foreach(lc, baserel->baserestrictinfo)
+ {
+ RestrictInfo *rs = (RestrictInfo *) lfirst(lc);
+ if (contain_ext_param((Node *) rs->clause))
+ {
+ *startup_cost = CONNECTION_COSTS;
+ *total_cost = 10000; /* goundless large costs */
+
+ return;
+ }
+ }
+
+ /*
+ * Get connection to the foreign server. Connection manager would
+ * establish new connection if necessary.
+ */
+ server = GetForeignServer(serverid);
+ user = GetUserMapping(GetOuterUserId(), server->serverid);
+ conn = GetConnection(server, user);
+ initStringInfo(&buf);
+ appendStringInfo(&buf, "EXPLAIN (FORMAT YAML) %s", sql);
+
+ PG_TRY();
+ {
+ res = PQexec(conn, buf.data);
+ if (PQresultStatus(res) != PGRES_TUPLES_OK)
+ {
+ char *msg;
+
+ msg = pstrdup(PQerrorMessage(conn));
+ ereport(ERROR,
+ (errmsg("could not execute EXPLAIN for cost estimation"),
+ errdetail("%s", msg),
+ errhint("%s", sql)));
+ }
+ plan = pstrdup(PQgetvalue(res, 0, 0));
+ PQclear(res);
+ res = NULL;
+ ReleaseConnection(conn);
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+
+ /* extract startup cost from remote plan */
+ p = strstr(plan, "Startup Cost: ");
+ if (p != NULL)
+ {
+ p += strlen("Startup Cost: ");
+ *startup_cost = strtod(p, &endp);
+ if (*endp != '\n' && *endp != '\0')
+ elog(ERROR, "invalid plan format for startup cost%c", *endp);
+ }
+
+ /* extract total cost from remote plan */
+ p = strstr(plan, "Total Cost: ");
+ if (p != NULL)
+ {
+ p += strlen("Total Cost: ");
+ *total_cost = strtod(p, &endp);
+ if (*endp != '\n' && *endp != '\0')
+ elog(ERROR, "invalid plan format for total cost");
+ }
+
+ /* extract # of rows from remote plan */
+ p = strstr(plan, "Plan Rows: ");
+ if (p != NULL)
+ {
+ p += strlen("Plan Rows: ");
+ baserel->rows = strtod(p, &endp);
+ if (*endp != '\n' && *endp != '\0')
+ elog(ERROR, "invalid plan format for plan rows");
+ }
+
+ /* extract average width from remote plan */
+ p = strstr(plan, "Plan Width: ");
+ if (p != NULL)
+ {
+ p += strlen("Plan Width: ");
+ baserel->width = strtol(p, &endp, 10);
+ if (*endp != '\n' && *endp != '\0')
+ elog(ERROR, "invalid plan format for plan width");
+ }
+
+ /* TODO Selectivity of quals pushed down should be considered. */
+
+ /* add cost to establish connection. */
+ *startup_cost += CONNECTION_COSTS;
+ *total_cost += CONNECTION_COSTS;
+
+ /* add cost to transfer result. */
+ *total_cost += TRANSFER_COSTS_PER_BYTE * baserel->width * baserel->tuples;
+ *total_cost += cpu_tuple_cost * baserel->tuples;
+ }
+
+ /*
+ * Execute remote query with current parameters.
+ */
+ static void
+ execute_query(ForeignScanState *node)
+ {
+ FdwPlan *fdwplan;
+ PgsqlFdwExecutionState *festate;
+ ParamListInfo params = node->ss.ps.state->es_param_list_info;
+ int numParams = params ? params->numParams : 0;
+ Oid *types = NULL;
+ const char **values = NULL;
+ char *sql;
+ PGconn *conn;
+ PGresult *res;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+ types = festate->param_types;
+ values = festate->param_values;
+
+ /*
+ * Construct parameter array in text format. We don't release memory for
+ * the arrays explicitly, because the memory usage would not be very large,
+ * and anyway they will be released in context cleanup.
+ */
+ if (numParams > 0)
+ {
+ int i;
+
+ 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;
+
+ getTypeOutputInfo(types[i], &out_func_oid, &isvarlena);
+ fmgr_info(out_func_oid, &func);
+ values[i] = OutputFunctionCall(&func, params->params[i].value);
+ }
+ }
+ }
+
+ /*
+ * Execute remote query with parameters.
+ */
+ conn = festate->conn;
+ fdwplan = ((ForeignScan *) node->ss.ps.plan)->fdwplan;
+ sql = strVal(list_nth(fdwplan->fdw_private, FdwPrivateDeclareSql));
+ res = PQexecParams(conn, sql, numParams, types, values, NULL, NULL, 0);
+ PG_TRY();
+ {
+ /*
+ * If the query has failed, reporting details is enough here.
+ * Connection(s) which are used by this query (at least used by
+ * pgsql_fdw) will be cleaned up by the foreign connection manager.
+ */
+ if (PQresultStatus(res) != PGRES_COMMAND_OK)
+ {
+ ereport(ERROR,
+ (errmsg("could not declare cursor"),
+ errdetail("%s", PQerrorMessage(conn)),
+ errhint("%s", sql)));
+ }
+
+ /* Mark that this scan has opened a cursor. */
+ festate->cursor_opened = true;
+
+ /* Discard result of CURSOR statement and fetch first bunch. */
+ PQclear(res);
+ res = fetch_result(node);
+
+ /*
+ * Store the result of the query into tuplestore.
+ * We must release PGresult here to avoid memory leak.
+ */
+ store_result(node, res);
+ PQclear(res);
+ res = NULL;
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+ }
+
+ /*
+ * Fetch next partial result from remote server.
+ */
+ static PGresult *
+ fetch_result(ForeignScanState *node)
+ {
+ PgsqlFdwExecutionState *festate;
+ List *fdw_private;
+ char *sql;
+ PGconn *conn;
+ PGresult *res;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+
+ /* retrieve information for fetching result. */
+ fdw_private = festate->fdwplan->fdw_private;
+ sql = strVal(list_nth(fdw_private, FdwPrivateFetchSql));
+ conn = festate->conn;
+ res = PQexec(conn, sql);
+ if (PQresultStatus(res) != PGRES_TUPLES_OK)
+ {
+ ereport(ERROR,
+ (errmsg("could not fetch rows from foreign server"),
+ errdetail("%s", PQerrorMessage(conn)),
+ errhint("%s", sql)));
+ }
+
+ return res;
+ }
+
+ /*
+ * Create tuples from PGresult and store them into tuplestore.
+ */
+ static void
+ store_result(ForeignScanState *node, PGresult *res)
+ {
+ int rows;
+ int row;
+ int i;
+ int nfields;
+ int attnum; /* number of non-dropped columns */
+ Form_pg_attribute *attrs;
+ TupleTableSlot *slot = node->ss.ss_ScanTupleSlot;
+ TupleDesc tupdesc = slot->tts_tupleDescriptor;
+ PgsqlFdwExecutionState *festate;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+ rows = PQntuples(res);
+ nfields = PQnfields(res);
+ attrs = tupdesc->attrs;
+
+ /* First, ensure that the tuplestore is empty. */
+ if (festate->tuples == NULL)
+ {
+ MemoryContext oldcontext = CurrentMemoryContext;
+
+ /*
+ * Create tuplestore to store result of the query in per-query context.
+ * Note that we use this memory context to avoid memory leak in error
+ * cases.
+ */
+ MemoryContextSwitchTo(MessageContext);
+ festate->tuples = tuplestore_begin_heap(false, false, work_mem);
+ MemoryContextSwitchTo(oldcontext);
+ }
+ else
+ {
+ /* We already have tuplestore, just need to clear contents of it. */
+ tuplestore_clear(festate->tuples);
+ }
+
+
+ /* 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)));
+
+ /* put a tuples into the slot */
+ for (row = 0; row < rows; row++)
+ {
+ int j;
+ HeapTuple tuple;
+
+ for (i = 0, j = 0; i < tupdesc->natts; i++)
+ {
+ /* skip dropped columns. */
+ if (attrs[i]->attisdropped)
+ {
+ festate->col_values[i] = NULL;
+ continue;
+ }
+
+ if (PQgetisnull(res, row, j))
+ festate->col_values[i] = NULL;
+ else
+ festate->col_values[i] = PQgetvalue(res, row, 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(festate->attinmeta, festate->col_values);
+ tuplestore_puttuple(festate->tuples, tuple);
+ }
+
+ tuplestore_donestoring(festate->tuples);
+ }
+
+ /*
+ * contain_ext_param
+ * Recursively search for Param nodes within a clause.
+ *
+ * Returns true if any parameter reference node with relkind PARAM_EXTERN
+ * found.
+ *
+ * This does not descend into subqueries, and so should be used only after
+ * reduction of sublinks to subplans, or in contexts where it's known there
+ * are no subqueries. There mustn't be outer-aggregate references either.
+ *
+ * XXX: These functions could be in core, src/backend/optimizer/util/clauses.c.
+ */
+ static bool
+ contain_ext_param(Node *clause)
+ {
+ return contain_ext_param_walker(clause, NULL);
+ }
+
+ static bool
+ contain_ext_param_walker(Node *node, void *context)
+ {
+ if (node == NULL)
+ return false;
+ if (IsA(node, Param))
+ {
+ Param *param = (Param *) node;
+
+ if (param->paramkind == PARAM_EXTERN)
+ return true; /* abort the tree traversal and return true */
+ }
+ return expression_tree_walker(node, contain_ext_param_walker, context);
+ }
diff --git a/contrib/pgsql_fdw/pgsql_fdw.control b/contrib/pgsql_fdw/pgsql_fdw.control
index ...0a9c8f4 .
*** a/contrib/pgsql_fdw/pgsql_fdw.control
--- b/contrib/pgsql_fdw/pgsql_fdw.control
***************
*** 0 ****
--- 1,5 ----
+ # pgsql_fdw extension
+ comment = 'foreign-data wrapper for remote PostgreSQL servers'
+ default_version = '1.0'
+ module_pathname = '$libdir/pgsql_fdw'
+ relocatable = true
diff --git a/contrib/pgsql_fdw/pgsql_fdw.h b/contrib/pgsql_fdw/pgsql_fdw.h
index ...fb49ffb .
*** a/contrib/pgsql_fdw/pgsql_fdw.h
--- b/contrib/pgsql_fdw/pgsql_fdw.h
***************
*** 0 ****
--- 1,28 ----
+ /*-------------------------------------------------------------------------
+ *
+ * pgsql_fdw.h
+ * foreign-data wrapper for remote PostgreSQL servers.
+ *
+ * Copyright (c) 2011, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/pgsql_fdw.h
+ *
+ *-------------------------------------------------------------------------
+ */
+
+ #ifndef PGSQL_FDW_H
+ #define PGSQL_FDW_H
+
+ #include "postgres.h"
+ #include "nodes/relation.h"
+
+ /* in option.c */
+ int ExtractConnectionOptions(List *defelems,
+ const char **keywords,
+ const char **values);
+
+ /* in deparse.c */
+ char *deparseSql(Oid relid, PlannerInfo *root, RelOptInfo *baserel);
+
+ #endif /* PGSQL_FDW_H */
diff --git a/contrib/pgsql_fdw/sql/pgsql_fdw.sql b/contrib/pgsql_fdw/sql/pgsql_fdw.sql
index ...965193b .
*** a/contrib/pgsql_fdw/sql/pgsql_fdw.sql
--- b/contrib/pgsql_fdw/sql/pgsql_fdw.sql
***************
*** 0 ****
--- 1,184 ----
+ -- ===================================================================
+ -- create FDW objects
+ -- ===================================================================
+
+ CREATE EXTENSION pgsql_fdw;
+
+ CREATE SERVER loopback1 FOREIGN DATA WRAPPER pgsql_fdw;
+ CREATE SERVER loopback2 FOREIGN DATA WRAPPER pgsql_fdw
+ OPTIONS (dbname 'contrib_regression');
+
+ CREATE USER MAPPING FOR public SERVER loopback1
+ OPTIONS (user 'value', password 'value');
+ CREATE USER MAPPING FOR public SERVER loopback2;
+
+ CREATE FOREIGN TABLE ft1 (
+ c1 int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp
+ ) SERVER loopback2;
+
+ CREATE FOREIGN TABLE ft2 (
+ c1 int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp
+ ) SERVER loopback2;
+
+ -- ===================================================================
+ -- create objects used through FDW
+ -- ===================================================================
+ CREATE SCHEMA "S 1";
+ CREATE TABLE "S 1"."T 1" (
+ "C 1" int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ CONSTRAINT t1_pkey PRIMARY KEY ("C 1")
+ );
+ CREATE TABLE "S 1"."T 2" (
+ c1 int NOT NULL,
+ c2 text,
+ CONSTRAINT t2_pkey PRIMARY KEY (c1)
+ );
+
+ BEGIN;
+ TRUNCATE "S 1"."T 1";
+ INSERT INTO "S 1"."T 1"
+ SELECT id,
+ id % 10,
+ to_char(id, 'FM00000'),
+ '1970-01-01'::timestamptz + ((id % 100) || ' days')::interval,
+ '1970-01-01'::timestamp + ((id % 100) || ' days')::interval
+ FROM generate_series(1, 1000) id;
+ TRUNCATE "S 1"."T 2";
+ INSERT INTO "S 1"."T 2"
+ SELECT id,
+ 'AAA' || to_char(id, 'FM000')
+ FROM generate_series(1, 100) id;
+ COMMIT;
+
+ -- ===================================================================
+ -- tests for pgsql_fdw_validator
+ -- ===================================================================
+ ALTER FOREIGN DATA WRAPPER pgsql_fdw OPTIONS (host 'value'); -- ERROR
+ -- requiressl, krbsrvname and gsslib are omitted because they depend on
+ -- configure option
+ ALTER SERVER loopback1 OPTIONS (
+ authtype 'value',
+ service 'value',
+ connect_timeout 'value',
+ dbname 'value',
+ host 'value',
+ hostaddr 'value',
+ port 'value',
+ --client_encoding 'value',
+ tty 'value',
+ options 'value',
+ application_name 'value',
+ --fallback_application_name 'value',
+ keepalives 'value',
+ keepalives_idle 'value',
+ keepalives_interval 'value',
+ -- requiressl 'value',
+ sslmode 'value',
+ sslcert 'value',
+ sslkey 'value',
+ sslrootcert 'value',
+ sslcrl 'value'
+ --requirepeer 'value',
+ -- krbsrvname 'value',
+ -- gsslib 'value',
+ --replication 'value'
+ );
+ ALTER SERVER loopback1 OPTIONS (user 'value'); -- ERROR
+ ALTER SERVER loopback2 OPTIONS (ADD fetch_count '2');
+ ALTER USER MAPPING FOR public SERVER loopback1
+ OPTIONS (DROP user, DROP password);
+ ALTER USER MAPPING FOR public SERVER loopback1
+ OPTIONS (host 'value'); -- ERROR
+ ALTER FOREIGN TABLE ft1 OPTIONS (nspname 'S 1', relname 'T 1');
+ ALTER FOREIGN TABLE ft2 OPTIONS (nspname 'S 1', relname 'T 1', fetch_count '100');
+ ALTER FOREIGN TABLE ft1 OPTIONS (invalid 'value'); -- ERROR
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count 'a'); -- ERROR
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count '0'); -- ERROR
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count '-1'); -- ERROR
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (invalid 'value'); -- ERROR
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (colname 'C 1');
+ ALTER FOREIGN TABLE ft2 ALTER COLUMN c1 OPTIONS (colname 'C 1');
+ \dew+
+ \des+
+ \deu+
+ \det+
+
+ -- ===================================================================
+ -- simple queries
+ -- ===================================================================
+ -- single table, with/without alias
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
+ SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+ SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+ -- with WHERE clause
+ SELECT * FROM ft1 t1 WHERE t1.c1 = 101;
+ -- 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
+ SELECT * FROM ft1 t1 WHERE t1.c3 = (SELECT MAX(c3) FROM ft2 t2) ORDER BY c1;
+ -- used in CTE
+ WITH t1 AS (SELECT * FROM ft1 WHERE c1 <= 10) SELECT t2.c1, t2.c2, t2.c3, t2.c4 FROM t1, ft2 t2 WHERE t1.c1 = t2.c1 ORDER BY t1.c1;
+ -- fixed values
+ SELECT 'fixed', NULL FROM ft1 t1 WHERE c1 = 1;
+
+ -- ===================================================================
+ -- parameterized queries
+ -- ===================================================================
+ -- simple join
+ PREPARE st1(int, int) AS SELECT t1.c3, t2.c3 FROM ft1 t1, ft2 t2 WHERE t1.c1 = $1 AND t2.c1 = $2;
+ EXPLAIN (COSTS false) EXECUTE st1(1, 2);
+ EXECUTE st1(1, 1);
+ EXECUTE st1(101, 101);
+ -- subquery using stable function (can't be pushed down)
+ PREPARE st2(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c4) = 6) ORDER BY c1;
+ EXPLAIN (COSTS false) EXECUTE st2(10, 20);
+ EXECUTE st2(10, 20);
+ EXECUTE st1(101, 101);
+ -- subquery using immutable function (can be pushed down)
+ PREPARE st3(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c5) = 6) ORDER BY c1;
+ EXPLAIN (COSTS false) EXECUTE st3(10, 20);
+ EXECUTE st3(10, 20);
+ EXECUTE st3(20, 30);
+ -- custom plan should be chosen
+ PREPARE st4(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 = $1;
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ -- cleanup
+ DEALLOCATE st1;
+ DEALLOCATE st2;
+ DEALLOCATE st3;
+ DEALLOCATE st4;
+
+ -- ===================================================================
+ -- connection management
+ -- ===================================================================
+ SELECT srvname, usename FROM pgsql_fdw_connections;
+ SELECT pgsql_fdw_disconnect(srvid, usesysid) FROM pgsql_fdw_get_connections();
+ SELECT srvname, usename FROM pgsql_fdw_connections;
+
+ -- ===================================================================
+ -- cleanup
+ -- ===================================================================
+ DROP EXTENSION pgsql_fdw CASCADE;
+
diff --git a/doc/src/sgml/contrib.sgml b/doc/src/sgml/contrib.sgml
index adf09ca..65c7e81 100644
*** a/doc/src/sgml/contrib.sgml
--- b/doc/src/sgml/contrib.sgml
*************** CREATE EXTENSION <replaceable>module_nam
*** 117,122 ****
--- 117,123 ----
&pgcrypto;
&pgfreespacemap;
&pgrowlocks;
+ &pgsql-fdw;
&pgstandby;
&pgstatstatements;
&pgstattuple;
diff --git a/doc/src/sgml/filelist.sgml b/doc/src/sgml/filelist.sgml
index 7a698e5..2dd41f0 100644
*** a/doc/src/sgml/filelist.sgml
--- b/doc/src/sgml/filelist.sgml
***************
*** 124,129 ****
--- 124,130 ----
<!ENTITY pgcrypto SYSTEM "pgcrypto.sgml">
<!ENTITY pgfreespacemap SYSTEM "pgfreespacemap.sgml">
<!ENTITY pgrowlocks SYSTEM "pgrowlocks.sgml">
+ <!ENTITY pgsql-fdw SYSTEM "pgsql-fdw.sgml">
<!ENTITY pgstandby SYSTEM "pgstandby.sgml">
<!ENTITY pgstatstatements SYSTEM "pgstatstatements.sgml">
<!ENTITY pgstattuple SYSTEM "pgstattuple.sgml">
diff --git a/doc/src/sgml/pgsql-fdw.sgml b/doc/src/sgml/pgsql-fdw.sgml
index ...22f795e .
*** a/doc/src/sgml/pgsql-fdw.sgml
--- b/doc/src/sgml/pgsql-fdw.sgml
***************
*** 0 ****
--- 1,252 ----
+ <!-- doc/src/sgml/pgsql-fdw.sgml -->
+
+ <sect1 id="pgsql-fdw" xreflabel="pgsql_fdw">
+ <title>pgsql_fdw</title>
+
+ <indexterm zone="pgsql-fdw">
+ <primary>pgsql_fdw</primary>
+ </indexterm>
+
+ <para>
+ The <filename>pgsql_fdw</filename> module provides a foreign-data wrapper for
+ external <productname>PostgreSQL</productname> servers.
+ With this module, users can access data stored in external
+ <productname>PostgreSQL</productname> via plain SQL statements.
+ </para>
+
+ <para>
+ The <application>pgsql_fdw</application> can be installed on only
+ <productname>PostgreSQL</productname> 9.1 or later, but it can also access
+ data stored in <productname>PostgreSQL</productname> 9.0.
+ <productname>PostgreSQL</productname>8.4 or older are not available as remote
+ server because <application>pgsql_fdw</application> uses YAML format of
+ <command>EXPLAIN</command> output.
+ </para>
+
+ <para>
+ Note that default wrapper <literal>pgsql_fdw</literal> is created
+ automatically during <command>CREATE EXTENSION</command> command for
+ <application>pgsql_fdw</application>.
+ </para>
+
+ <sect2>
+ <title>FDW Options of pgsql_fdw</title>
+
+ <sect3>
+ <title>Connection Options</title>
+ <para>
+ A foreign server and user mapping created using this wrapper can have
+ <application>libpq</> connection options, expect below:
+
+ <itemizedlist>
+ <listitem><para>client_encoding</para></listitem>
+ <listitem><para>fallback_application_name</para></listitem>
+ <listitem><para>replication</para></listitem>
+ </itemizedlist>
+
+ For details of <application>libpq</> connection options, see
+ <xref linkend="libpq-connect">.
+ </para>
+
+ <para>
+ <literal>user</literal> and <literal>password</literal> can be
+ specified on user mappings, and others can be specified on foreign servers.
+ </para>
+ </sect3>
+
+ <sect3>
+ <title>Object Name Options</title>
+ <para>
+ Foreign tables which were created using this wrapper, or its columns can
+ have object name options. These options can be used to specify the names
+ used in SQL statement sent to remote <productname>PostgreSQL</productname>
+ server. These options are useful when a remote object has different name
+ from corresponding local one.
+ </para>
+
+ <variablelist>
+
+ <varlistentry>
+ <term><literal>nspname</literal></term>
+ <listitem>
+ <para>
+ This option, which can be specified on a foreign table, is used as a
+ namespace (schema) reference in the SQL statement. If this options is
+ omitted, <literal>pg_class.nspname</literal> of the foreign table is
+ used.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>relname</literal></term>
+ <listitem>
+ <para>
+ This option, which can be specified on a foreign table, is used as a
+ relation (table) reference in the SQL statement. If this options is
+ omitted, <literal>pg_class.relname</literal> of the foreign table is
+ used.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>colname</literal></term>
+ <listitem>
+ <para>
+ This option, which can be specified on a column of a foreign table, is
+ used as a column (attribute) reference in the SQL statement. If this
+ option is omitted, <literal>pg_attribute.attname</literal> of the column
+ of the foreign table is used.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ </variablelist>
+
+ </sect3>
+
+ <sect3>
+ <title>Cursor Options</title>
+ <para>
+ The <application>pgsql_fdw</application> always uses cursor to retrieve the
+ result from external server. Users can control the behavior of cursor by
+ setting cursor options to foreign table or foreign server. If an option
+ is set to both objects, finer-grained setting is used. In other words,
+ foreign table's setting overrides foreign server's setting.
+ </para>
+
+ <variablelist>
+
+ <varlistentry>
+ <term><literal>fetch_count</literal></term>
+ <listitem>
+ <para>
+ This option specifies the number of rows to be fetched at a time.
+ This option accepts only integer value larger than zero. The default
+ setting is 10000.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ </variablelist>
+
+ </sect3>
+
+ </sect2>
+
+ <sect2>
+ <title>Connection Management</title>
+
+ <para>
+ The <application>pgsql_fdw</application> establishes a connection to a
+ foreign server in the beginning of the first query which uses a foreign
+ table associated to the foreign server, and reuses the connection following
+ queries and even in following foreign scans in same query.
+
+ You can see the list of active connections via
+ <structname>pgsql_fdw_connections</structname> view. It shows pair of oid
+ and name of server and local role for each active connections established by
+ <application>pgsql_fdw</application>. For security reason, only superuser
+ can see other role's connections.
+ </para>
+
+ <para>
+ Established connections are kept alive until local role changes or the
+ current transaction aborts or user requests so.
+ </para>
+
+ <para>
+ If role has been changed, active connections established as old local role
+ is kept alive but never be reused until locla role has restored to original
+ role. This kind of situation happens with <command>SET ROLE</command> and
+ <command>SET SESSION AUTHORIZATION</command>.
+ </para>
+
+ <para>
+ If current transaction aborts by error or user request, all active
+ connections are disconnected automatically. This behavior avoids possible
+ connection leaks on error.
+ </para>
+
+ <para>
+ You can discard persistent connection at arbitrary timing with
+ <function>pgsql_fdw_disconnect()</function>. It takes server oid and
+ user oid as arguments. This function can handle only connections
+ established in current session; connections established by other backends
+ are not reachable.
+ </para>
+
+ <para>
+ You can discard all active and visible connections in current session with
+ using <structname>pgsql_fdw_connections</structname> and
+ <function>pgsql_fdw_disconnect()</function> together:
+ <synopsis>
+ postgres=# SELECT pgsql_fdw_disconnect(srvid, usesysid) FROM pgsql_fdw_connections;
+ pgsql_fdw_disconnect
+ ----------------------
+ OK
+ OK
+ (2 rows)
+ </synopsis>
+ </para>
+ </sect2>
+
+ <sect2>
+ <title>Transaction Management</title>
+ <para>
+ The <application>pgsql_fdw</application> executes <command>BEGIN</command>
+ command when a new connection has established. This means that all remote
+ queries for a foreign server are executed in a transaction. Since the
+ default transaction isolation level is <literal>READ COMMITTED</literal>,
+ multiple foreign scans in a local query might produce inconsistent results.
+
+ To avoid this inconsistency, you can use <literal>SERIALIZABLE</literal>
+ level for remote transaction with setting
+ <literal>default_transaction_isolation</literal> for the user used for
+ <application>pgsql_fdw</application> connection on remote side.
+ </para>
+ </sect2>
+
+ <sect2>
+ <title>Estimation of Costs and Rows</title>
+ <para>
+ The <application>pgsql_fdw</application> estimates the costs of a foreign
+ scan by adding some basic costs: connection costs, remote query costs and
+ data transfer costs.
+ To get remote query costs <application>pgsql_fdw</application> executes
+ <command>EXPLAIN</command> command on remote server for each foreign scan.
+ </para>
+
+ <para>
+ On the other hand, estimated rows which was returned by
+ <command>EXPLAIN</command> is used for local estimation as-is.
+ </para>
+ </sect2>
+
+ <sect2>
+ <title>EXPLAIN Output</title>
+ <para>
+ For a foreign table using <literal>pgsql_fdw</>, <command>EXPLAIN</> shows
+ a remote SQL statement which is sent to remote
+ <productname>PostgreSQL</productname> server for a ForeignScan plan node.
+ For example:
+ </para>
+ <synopsis>
+ postgres=# EXPLAIN SELECT aid FROM pgbench_accounts WHERE abalance < 0;
+ QUERY PLAN
+ -----------------------------------------------------------------------------------------------
+ Foreign Scan on pgbench_accounts (cost=100.00..8769.00 rows=1 width=4)
+ Remote SQL: SELECT aid, NULL, NULL, NULL FROM public.pgbench_accounts WHERE (abalance < 0)
+ (2 rows)
+ </synopsis>
+ </sect2>
+
+ <sect2>
+ <title>Author</title>
+ <para>
+ Shigeru Hanada <email>shigeru.hanada@gmail.com</email>
+ </para>
+ </sect2>
+
+ </sect1>
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 75923a6..0aae9d2 100644
*** a/src/backend/utils/adt/ruleutils.c
--- b/src/backend/utils/adt/ruleutils.c
*************** deparse_context_for(const char *aliasnam
*** 2161,2166 ****
--- 2161,2189 ----
return list_make1(dpns);
}
+ /* ----------
+ * deparse_context_for_rtelist - Build deparse context for a list of RTEs
+ *
+ * Given the list of RangeTableEnetry, build deparsing context for an
+ * expression referencing those relations. This is sufficient for uses of
+ * deparse_expression before plan has been created.
+ * ----------
+ */
+ List *
+ deparse_context_for_rtelist(List *rtable)
+ {
+ deparse_namespace *dpns;
+
+ dpns = (deparse_namespace *) palloc0(sizeof(deparse_namespace));
+
+ /* Build a minimal RTE for the rel */
+ dpns->rtable = rtable;
+ dpns->ctes = NIL;
+
+ /* Return a one-deep namespace stack */
+ return list_make1(dpns);
+ }
+
/*
* deparse_context_for_planstate - Build deparse context for a plan
*
diff --git a/src/include/utils/builtins.h b/src/include/utils/builtins.h
index 8a1c82e..a56366f 100644
*** a/src/include/utils/builtins.h
--- b/src/include/utils/builtins.h
*************** extern char *deparse_expression(Node *ex
*** 626,631 ****
--- 626,632 ----
extern List *deparse_context_for(const char *aliasname, Oid relid);
extern List *deparse_context_for_planstate(Node *planstate, List *ancestors,
List *rtable);
+ extern List *deparse_context_for_rtelist(List *rtable);
extern const char *quote_identifier(const char *ident);
extern char *quote_qualified_identifier(const char *qualifier,
const char *ident);
Hanada-san,
I'm still under reviewing of your patch, so the comment is not overall, sorry.
I'm not sure whether the logic of is_foreign_expr() is appropriate.
It checks oid of the function within FuncExpr and OpExpr to disallow to push
down user-defined functions.
However, if a user-defined operator is implemented based on built-in functions
with different meaning, is it really suitable to push-down?
E.g) It is available to define '=' operator using int4ne, even though
quite nonsense.
So, I'd like to suggest to check oid of the operator; whether it is a
built-in, or not.
On the other hand, this hard-wired restriction may damage to the purpose of
this module; that enables to handle a query on multiple nodes in parallel.
I'm not sure whether it is right design that is_foreign_expr() returns false
when the supplied expr contains mutable functions.
Probably, here are two perspectives. The one want to make sure functions works
with same manner in all nodes. The other want to distribute processing
of queries
as possible as we can. Here is a trade-off between these two perspectives.
So, how about an idea to add a guc variable to control the criteria of
pushing-down.
Thanks,
2011年11月15日17:55 Shigeru Hanada <shigeru.hanada@gmail.com>:
Hi,
Attached are revised version of pgsql_fdw patches.
fdw_helper_funcs_v2.patch provides some functions which would be useful
to implement FDW, and document about FDW helper functions including
those which exist in 9.1. They are not specific to pgsql_fdw, so I
separated it from pgsql_fdw patch.pgsql_fdw_v4.patch provides a FDW for PostgreSQL. This patch requires
fdw_helper_funcs_v2.patch has been applied. Changes done since last
version are:* Default of fetch_count option is increased to 10000, as suggested by
Pavel Stehule.
* Remove unnecessary NULL check before PQresultStatus.
* Evaluate all conditions on local side even if some of them has been
pushed down to remote side, to ensure that results are correct.
* Use fixed costs for queries which contain external parameter, because
such query can't be used in EXPLAIN command.Regards,
--
Shigeru Hanada
--
KaiGai Kohei <kaigai@kaigai.gr.jp>
Hi Hanada-san,
(2011/11/16 1:55), Shigeru Hanada wrote:
Attached are revised version of pgsql_fdw patches.
I'm still under reviewing, so the following is not all. I'm sorry.
estimate_costs() have been implemented to ask a remote postgres server
for the result of EXPLAIN for a remote query to get its costs such as
startup_cost and total_cost. I think this approach is the most accurate
way to get its costs. However, I think it would be rather costly. And
I'm afraid of that it might work only for pgsql_fdw. Because, even if we
are able to obtain such a cost information by EXPLAINing a remote query
at a remote server where a DBMS different from postgres runs, it might
be difficult to incorporate such a cost information with the postgres
cost model due to their possible inconsistency that such a cost
information provided by the EXPLAIN command in the other DBMS might have
different meanings (or different scales) from that provided by the
EXPLAIN command in postgres. So, I think it might be better to estimate
such costs by pgsql_fdw itself without EXPLAINing on the assumption that
a remote postgres server has the same abilities for query optimization,
which is less costly and widely applicable to the other DBMSs, while it,
of course, only works once we have statistics and/or index information
for foreign tables. But AFAIK we eventually want to have those, so I'd
like to propose to use the proposed approach until that time.
Best regards,
Etsuro Fujita
Hi Kaigai-san,
(2011/11/20 2:42), Kohei KaiGai wrote:
I'm still under reviewing of your patch, so the comment is not overall, sorry.
Thanks for the review!
I'm not sure whether the logic of is_foreign_expr() is appropriate.
It checks oid of the function within FuncExpr and OpExpr to disallow to push
down user-defined functions.
However, if a user-defined operator is implemented based on built-in functions
with different meaning, is it really suitable to push-down?
E.g) It is available to define '=' operator using int4ne, even though
quite nonsense.
So, I'd like to suggest to check oid of the operator; whether it is a
built-in, or not.On the other hand, this hard-wired restriction may damage to the purpose of
this module; that enables to handle a query on multiple nodes in parallel.
I'm not sure whether it is right design that is_foreign_expr() returns false
when the supplied expr contains mutable functions.Probably, here are two perspectives. The one want to make sure functions works
with same manner in all nodes. The other want to distribute processing
of queries
as possible as we can. Here is a trade-off between these two perspectives.
So, how about an idea to add a guc variable to control the criteria of
pushing-down.
I agree that allowing users to control which function/operator should be
pushed down is useful, but GUC seems too large as unit of switching
behavior. "Routine Mapping", a mechanism which is defined in SQL/MED
standard, would be the answer for this issue. It can be used to map a
local routine (a procedure or a function) to something on a foreign
server. It is like user mapping, but it has mapping name. Probably it
would have these attributes:
pg_catalog.pg_routine_mapping
rmname name
rmprocid regproc
rmserverid oid
rmfdwoptions text[]
If we have routine mapping, FDW authors can provide default mappings
within extension installation, and users can customize them. Maybe FDWs
will want to push down only functions/operators which have routine
mapping entries, so providing common routine which returns mapping
information of given function/operator, say GetRoutineMapping(procid,
serverid), is useful.
Unfortunately we don't have it at the moment, I'll fix pgsql_fdw so that
it pushes down only built-in operators, including scalar-array operators.
Regards,
--
Shigeru Hanada
Hi Fujita-san,
(2011/11/25 17:27), Etsuro Fujita wrote:
I'm still under reviewing, so the following is not all. I'm sorry.
estimate_costs() have been implemented to ask a remote postgres server
for the result of EXPLAIN for a remote query to get its costs such as
startup_cost and total_cost. I think this approach is the most accurate
way to get its costs. However, I think it would be rather costly. And
I'm afraid of that it might work only for pgsql_fdw.
Indeed. In addition, this approach assumes that cost factors of target
PG server are same as local's ones. pgsql_fdw might have to have cost
factors as FDW options of foreign server.
Because, even if we
are able to obtain such a cost information by EXPLAINing a remote query
at a remote server where a DBMS different from postgres runs, it might
be difficult to incorporate such a cost information with the postgres
cost model due to their possible inconsistency that such a cost
information provided by the EXPLAIN command in the other DBMS might have
different meanings (or different scales) from that provided by the
EXPLAIN command in postgres.
Yes, so implementing cost estimation for other DBMSs accurately would be
very difficult, but AFAIS rows estimation is the most important factor,
so reasonable row count and relatively high startup cost would produce
not-so-bad plan.
So, I think it might be better to estimate
such costs by pgsql_fdw itself without EXPLAINing on the assumption that
a remote postgres server has the same abilities for query optimization,
which is less costly and widely applicable to the other DBMSs, while it,
of course, only works once we have statistics and/or index information
for foreign tables. But AFAIK we eventually want to have those, so I'd
like to propose to use the proposed approach until that time.
Knowledge of foreign indexes also provide information of sort order.
Planner will be able to consider merge join without local sort with such
information. Without foreign index, we have to enumerate possible sort
keys with Blute-Force approach for same result, as mentioned by
Itagaki-san before.
Regards,
--
Shigeru Hanada
2011/11/28 Shigeru Hanada <shigeru.hanada@gmail.com>:
I agree that allowing users to control which function/operator should be
pushed down is useful, but GUC seems too large as unit of switching
behavior. "Routine Mapping", a mechanism which is defined in SQL/MED
standard, would be the answer for this issue. It can be used to map a
local routine (a procedure or a function) to something on a foreign
server. It is like user mapping, but it has mapping name. Probably it
would have these attributes:pg_catalog.pg_routine_mapping
rmname name
rmprocid regproc
rmserverid oid
rmfdwoptions text[]If we have routine mapping, FDW authors can provide default mappings
within extension installation, and users can customize them. Maybe FDWs
will want to push down only functions/operators which have routine
mapping entries, so providing common routine which returns mapping
information of given function/operator, say GetRoutineMapping(procid,
serverid), is useful.Unfortunately we don't have it at the moment, I'll fix pgsql_fdw so that
it pushes down only built-in operators, including scalar-array operators.
One difficulty here is that even very simple operators don't
necessarily mean the same thing on both sides. In my last job we had
a Microsoft SQL database where string equality was case insensitive,
and a PostgreSQL database where it wasn't.
--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
(2011/11/28 20:50), Shigeru Hanada wrote:
(2011/11/25 17:27), Etsuro Fujita wrote:
So, I think it might be better to estimate
such costs by pgsql_fdw itself without EXPLAINing on the assumption that
a remote postgres server has the same abilities for query optimization,
which is less costly and widely applicable to the other DBMSs, while it,
of course, only works once we have statistics and/or index information
for foreign tables. But AFAIK we eventually want to have those, so I'd
like to propose to use the proposed approach until that time.Knowledge of foreign indexes also provide information of sort order.
Planner will be able to consider merge join without local sort with such
information. Without foreign index, we have to enumerate possible sort
keys with Blute-Force approach for same result, as mentioned by
Itagaki-san before.
Yes, with the knowledge of foreign indexes, I think we can take the
approach of thinking multiple plans for a foreign table; the cheapest
unordered plan and the cheapest plan with a given sort order. In
addition, it would be also possible to support
nestloop-with-inner-foreign-indexscans on a foreign table as pointed out
as future work by Tom Lane at PGCon 2011[1]http://www.pgcon.org/2011/schedule/attachments/188_Planner%20talk.pdf.
[1]: http://www.pgcon.org/2011/schedule/attachments/188_Planner%20talk.pdf
Best regards,
Etsuro Fujita
Robert Haas wrote:
2011/11/28 Shigeru Hanada <shigeru.hanada@gmail.com>:
I agree that allowing users to control which function/operator should be
pushed down is useful, but GUC seems too large as unit of switching
behavior. "Routine Mapping", a mechanism which is defined in SQL/MED
standard, would be the answer for this issue. It can be used to map a
local routine (a procedure or a function) to something on a foreign
server. It is like user mapping, but it has mapping name. Probably it
would have these attributes:pg_catalog.pg_routine_mapping
rmname name
rmprocid regproc
rmserverid oid
rmfdwoptions text[]If we have routine mapping, FDW authors can provide default mappings
within extension installation, and users can customize them. Maybe FDWs
will want to push down only functions/operators which have routine
mapping entries, so providing common routine which returns mapping
information of given function/operator, say GetRoutineMapping(procid,
serverid), is useful.Unfortunately we don't have it at the moment, I'll fix pgsql_fdw so that
it pushes down only built-in operators, including scalar-array operators.One difficulty here is that even very simple operators don't
necessarily mean the same thing on both sides. In my last job we had
a Microsoft SQL database where string equality was case insensitive,
and a PostgreSQL database where it wasn't.
I think that this is not always safe even from PostgreSQL to PostgreSQL.
If two databases have different collation, "<" on strings will behave
differently.
Yours,
Laurenz Albe
Sorry for delayed response.
2011/11/29 Albe Laurenz <laurenz.albe@wien.gv.at>:
I think that this is not always safe even from PostgreSQL to PostgreSQL.
If two databases have different collation, "<" on strings will behave
differently.
Indeed. I think that only the owner of foreign table can keep collation
consistent between foreign and local, like data type of column. We need to
support per-column-collation on foreign tables too, or should deny pushing
down condition which is collation-sensitive...
Regards,--
Shigeru Hanada
On 12/07/2011 02:34 AM, Shigeru Hanada wrote:
I think that only the owner of foreign table can keep collation
consistent between foreign and local, like data type of column. We need to
support per-column-collation on foreign tables too, or should deny pushing
down condition which is collation-sensitive...
I am not sure about what next step you were planning here. Are you
thinking to block this sort of push-down in an update to your feature
patch, or does some more serious work on foreign table collation need to
happen first instead?
It looks like there has been some good discussion of this feature here,
but there is still some work needed before it will be ready to commit.
Hanada-san, did you get the feedback you were looking for here yet, or
are there things you still wanted to discuss? It is not clear to me
what happens next; I would appreciate your comment on that.
--
Greg Smith 2ndQuadrant US greg@2ndQuadrant.com Baltimore, MD
PostgreSQL Training, Services, and 24x7 Support www.2ndQuadrant.us
On Wed, Dec 7, 2011 at 2:34 AM, Shigeru Hanada <shigeru.hanada@gmail.com> wrote:
Sorry for delayed response.
2011/11/29 Albe Laurenz <laurenz.albe@wien.gv.at>:
I think that this is not always safe even from PostgreSQL to PostgreSQL.
If two databases have different collation, "<" on strings will behave
differently.Indeed. I think that only the owner of foreign table can keep collation
consistent between foreign and local, like data type of column.
+1.
We need to
support per-column-collation on foreign tables too, or should deny pushing
down condition which is collation-sensitive...
It seems that we already do:
rhaas=# create foreign table ft1 (a text collate "de_DE") server s1;
CREATE FOREIGN TABLE
It does seem like this might not be enough information for the FDW to
make good decisions about pushdown. Even supposing the server on the
other hand is also PostgreSQL, the collation names might not match
(if, say, one is running Windows, and the other, Linux). And even if
they do, there is no guarantee that two collations with the same name
have the same behavior on two different machines; they probably
should, but who knows? And if we're using an FDW to talk to some
other database server, the problem is much worse; it's not clear that
we'll even begin to be able to guess whether the remote side has
compatible semantics. I feel like we might need a system here that
allows for more explicit user control about what to push down vs. not,
rather than assuming we'll be able to figure it out behind the scenes.
--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
(2011/12/12 22:59), Robert Haas wrote:
It does seem like this might not be enough information for the FDW to
make good decisions about pushdown. Even supposing the server on the
other hand is also PostgreSQL, the collation names might not match
(if, say, one is running Windows, and the other, Linux). And even if
they do, there is no guarantee that two collations with the same name
have the same behavior on two different machines; they probably
should, but who knows? And if we're using an FDW to talk to some
other database server, the problem is much worse; it's not clear that
we'll even begin to be able to guess whether the remote side has
compatible semantics. I feel like we might need a system here that
allows for more explicit user control about what to push down vs. not,
rather than assuming we'll be able to figure it out behind the scenes.
Agreed. How about to add a per-column boolean FDW option, say
"pushdown", to pgsql_fdw? Users can tell pgsql_fdw that the column can
be pushed down safely by setting this option to true. IMO default
should be false (no push down).
In most cases, columns with numeric/time-related types are safe to be
pushed down because they are free from collations issue, so users would
want to set to true. OTOH, columns with string types would need some
considerations. Once users have ensured that the column has compatible
semantics, they can set "pushdown=true" for efficiency.
If a condition contains any columns with pushdown=false, that condition
should NOT be pushed down.
This idea is only for pgsql_fdw now, but it can be used for other FDWs
which support push-down, and it would be also useful for ORDER BY
push-down support in future, which is apparently contains collation issue.
Regards,
--
Shigeru Hanada
Shigeru Hanada <shigeru.hanada@gmail.com> writes:
(2011/12/12 22:59), Robert Haas wrote:
... I feel like we might need a system here that
allows for more explicit user control about what to push down vs. not,
rather than assuming we'll be able to figure it out behind the scenes.
Agreed. How about to add a per-column boolean FDW option, say
"pushdown", to pgsql_fdw? Users can tell pgsql_fdw that the column can
be pushed down safely by setting this option to true.
[ itch... ] That doesn't seem like the right level of granularity.
ISTM the problem is with whether specific operators have the same
meaning at the far end as they do locally. If you try to attach the
flag to columns, you have to promise that *every* operator on that
column means what it does locally, which is likely to not be the
case ever if you look hard enough. Plus, having to set the flag on
each individual column of the same datatype seems pretty tedious.
I don't have a better idea to offer at the moment though. Trying
to attach such a property to operators seems impossibly messy too.
If it weren't for the collations issue, I might think that labeling
datatypes as being compatible would be a workable approximation.
regards, tom lane
Tom Lane wrote:
Shigeru Hanada <shigeru.hanada@gmail.com> writes:
(2011/12/12 22:59), Robert Haas wrote:
... I feel like we might need a system here that
allows for more explicit user control about what to push down vs.
not,
rather than assuming we'll be able to figure it out behind the
scenes.
Agreed. How about to add a per-column boolean FDW option, say
"pushdown", to pgsql_fdw? Users can tell pgsql_fdw that the column
can
be pushed down safely by setting this option to true.
[ itch... ] That doesn't seem like the right level of granularity.
ISTM the problem is with whether specific operators have the same
meaning at the far end as they do locally. If you try to attach the
flag to columns, you have to promise that *every* operator on that
column means what it does locally, which is likely to not be the
case ever if you look hard enough. Plus, having to set the flag on
each individual column of the same datatype seems pretty tedious.I don't have a better idea to offer at the moment though. Trying
to attach such a property to operators seems impossibly messy too.
If it weren't for the collations issue, I might think that labeling
datatypes as being compatible would be a workable approximation.
Maybe I'm missing something, but if pushdown worked as follows:
- Push down only system functions and operators on system types.
- Only push down what is guaranteed to work.
then the only things we would miss out on are encoding- or
collation-sensitive string operations.
Is that loss so big that it warrants a lot of effort?
Yours,
Laurenz Albe
On 13.12.2011 11:57, Albe Laurenz wrote:
Tom Lane wrote:
Shigeru Hanada<shigeru.hanada@gmail.com> writes:
(2011/12/12 22:59), Robert Haas wrote:
... I feel like we might need a system here that
allows for more explicit user control about what to push down vs.not,
rather than assuming we'll be able to figure it out behind the
scenes.
Agreed. How about to add a per-column boolean FDW option, say
"pushdown", to pgsql_fdw? Users can tell pgsql_fdw that the columncan
be pushed down safely by setting this option to true.
[ itch... ] That doesn't seem like the right level of granularity.
ISTM the problem is with whether specific operators have the same
meaning at the far end as they do locally. If you try to attach the
flag to columns, you have to promise that *every* operator on that
column means what it does locally, which is likely to not be the
case ever if you look hard enough. Plus, having to set the flag on
each individual column of the same datatype seems pretty tedious.I don't have a better idea to offer at the moment though. Trying
to attach such a property to operators seems impossibly messy too.
If it weren't for the collations issue, I might think that labeling
datatypes as being compatible would be a workable approximation.Maybe I'm missing something, but if pushdown worked as follows:
- Push down only system functions and operators on system types.
- Only push down what is guaranteed to work.then the only things we would miss out on are encoding- or
collation-sensitive string operations.Is that loss so big that it warrants a lot of effort?
The SQL/MED spec handles this with the concept of "routine mappings".
There is syntax for defining which remote "routines", meaning functions,
correspond local functions:
CREATE ROUTINE MAPPING <routine mapping name> FOR <specific routine
designator>
SERVER <foreign server name> [ <generic options> ]
<generic options> is FDW-specific, I'd imagine the idea is to give the
name of the corresponding function in the remote server. It doesn't say
anything about collations, but you could have extra options to specify
that a function can only be mapped under C collation, or whatever.
It seems tedious to specify that per-server, though, so we'll probably
still want to have some smarts in the pgsql_fdw to handle the built-in
functions and types that we know to be safe.
I've been talking about functions here, not operators, on the assumption
that we can look up the function underlying the operator and make the
decisions based on that.
--
Heikki Linnakangas
EnterpriseDB http://www.enterprisedb.com
(2011/12/13 20:04), Heikki Linnakangas wrote:
The SQL/MED spec handles this with the concept of "routine mappings".
There is syntax for defining which remote "routines", meaning functions,
correspond local functions:CREATE ROUTINE MAPPING <routine mapping name> FOR <specific routine
designator>
SERVER <foreign server name> [ <generic options> ]<generic options> is FDW-specific, I'd imagine the idea is to give the
name of the corresponding function in the remote server. It doesn't say
anything about collations, but you could have extra options to specify
that a function can only be mapped under C collation, or whatever.
I considered ROUTINE MAPPING for other RDBMS before, and thought that
having order of parameter in generic options would be necessary. It's
also useful for pgsql_fdw to support pushing down user-defined
functions. Maybe built-in format() function suits for this purpose?
It seems tedious to specify that per-server, though, so we'll probably
still want to have some smarts in the pgsql_fdw to handle the built-in
functions and types that we know to be safe.
One possible idea is having default mapping with serverid = InvalidOid,
and override them with entries which has valid server oid. Such default
mappings can be loaded during CREATE EXTENSION.
I've been talking about functions here, not operators, on the assumption
that we can look up the function underlying the operator and make the
decisions based on that.
It's interesting viewpoint to think operator notation is syntax sugar of
function notation, e.g. "A = B" -> "int4eq(A, B)". Routine mappings
seem to work for operators too.
Regards,
--
Shigeru Hanada
(2011/12/13 18:57), Albe Laurenz wrote:
Maybe I'm missing something, but if pushdown worked as follows:
- Push down only system functions and operators on system types.
- Only push down what is guaranteed to work.
Oh, I didn't care whether system data types. Indeed user defined types
would not be safe to push down.
then the only things we would miss out on are encoding- or
collation-sensitive string operations.Is that loss so big that it warrants a lot of effort?
It depends on the definition of "collation-sensitive". If we define it
as "all operations which might handle any collation-sensitive element",
all functions/operators which take any of character data types (text,
varchar, bpchar, sql_identifier, etc.) are unable to be pushed down.
Regards,
--
Shigeru Hanada
(2011/12/13 14:46), Tom Lane wrote:
Shigeru Hanada<shigeru.hanada@gmail.com> writes:
Agreed. How about to add a per-column boolean FDW option, say
"pushdown", to pgsql_fdw? Users can tell pgsql_fdw that the column can
be pushed down safely by setting this option to true.[ itch... ] That doesn't seem like the right level of granularity.
ISTM the problem is with whether specific operators have the same
meaning at the far end as they do locally. If you try to attach the
flag to columns, you have to promise that *every* operator on that
column means what it does locally, which is likely to not be the
case ever if you look hard enough. Plus, having to set the flag on
each individual column of the same datatype seems pretty tedious.
Indeed, I too think that labeling on each columns is not the best way,
but at that time I thought that it's a practical way, in a way. IOW, I
chose per-column FDW options as a compromise between never-push-down and
indiscriminate-push-down.
Anyway, ISTM that we should consider various mapping for
functions, operators and collations to support push-down in general
way, but it would be hard to accomplish in this CF.
Here I'd like to propose three incremental patches:
1) fdw_helper_funcs_v3.patch: This is not specific to pgsql_fdw, but
probably useful for every FDWs which use FDW options. This patch
provides some functions which help retrieving FDW options from catalogs.
This patch also enhances document about existing FDW helper functions.
2) pgsql_fdw_v5.patch: This patch provides simple pgsql_fdw
which does *NOT* support any push-down. All data in remote table are
retrieved for each foreign scan, and conditions are always evaluated on
local side. This is safe about semantics difference between local and
remote, but very inefficient especially for large remote tables.
3) pgsql_fdw_pushdown_v1.patch: This patch adds limited push-down
capability to pgsql_fdw which is implemented by previous patch. The
criteria for pushing down is little complex. I modified pgsql_fdw to
*NOT* push down conditions which contain any of:
a) expression whose result collation is valid
b) expression whose input collation is valid
c) expression whose result type is user-defined
d) expression which uses user-defined function
e) array expression whose elements has user-defined type
f) expression which uses user-defined operator
g) expression which uses mutable function
As the result, pgsql_fdw can push down very limited conditions such as
numeric comparisons, but it would be still useful. I hope that these
restriction are enough to avoid problems about semantics difference
between remote and local.
To implement d), I added exprFunction to nodefuncs.c which returns Oid
of function which is used in the expression node, but I'm not sure that
it should be there. Should we have it inside pgsql_fdw?
I'd like to thank everyone who commented on this topic!
Regards,
--
Shigeru Hanada
Attachments:
fdw_helper_funcs_v3.patchtext/plain; name=fdw_helper_funcs_v3.patchDownload
diff --git a/contrib/file_fdw/file_fdw.c b/contrib/file_fdw/file_fdw.c
index 1cf3b3c..4a7ffa6 100644
*** a/contrib/file_fdw/file_fdw.c
--- b/contrib/file_fdw/file_fdw.c
***************
*** 26,32 ****
#include "nodes/makefuncs.h"
#include "optimizer/cost.h"
#include "utils/rel.h"
! #include "utils/syscache.h"
PG_MODULE_MAGIC;
--- 26,32 ----
#include "nodes/makefuncs.h"
#include "optimizer/cost.h"
#include "utils/rel.h"
! #include "utils/lsyscache.h"
PG_MODULE_MAGIC;
*************** get_file_fdw_attribute_options(Oid relid
*** 345,398 ****
/* Retrieve FDW options for all user-defined attributes. */
for (attnum = 1; attnum <= natts; attnum++)
{
! HeapTuple tuple;
! Form_pg_attribute attr;
! Datum datum;
! bool isnull;
/* Skip dropped attributes. */
if (tupleDesc->attrs[attnum - 1]->attisdropped)
continue;
! /*
! * We need the whole pg_attribute tuple not just what is in the
! * tupleDesc, so must do a catalog lookup.
! */
! tuple = SearchSysCache2(ATTNUM,
! RelationGetRelid(rel),
! Int16GetDatum(attnum));
! if (!HeapTupleIsValid(tuple))
! elog(ERROR, "cache lookup failed for attribute %d of relation %u",
! attnum, RelationGetRelid(rel));
! attr = (Form_pg_attribute) GETSTRUCT(tuple);
!
! datum = SysCacheGetAttr(ATTNUM,
! tuple,
! Anum_pg_attribute_attfdwoptions,
! &isnull);
! if (!isnull)
{
! List *options = untransformRelOptions(datum);
! ListCell *lc;
! foreach(lc, options)
{
! DefElem *def = (DefElem *) lfirst(lc);
!
! if (strcmp(def->defname, "force_not_null") == 0)
{
! if (defGetBoolean(def))
! {
! char *attname = pstrdup(NameStr(attr->attname));
! fnncolumns = lappend(fnncolumns, makeString(attname));
! }
}
- /* maybe in future handle other options here */
}
}
-
- ReleaseSysCache(tuple);
}
heap_close(rel, AccessShareLock);
--- 345,373 ----
/* Retrieve FDW options for all user-defined attributes. */
for (attnum = 1; attnum <= natts; attnum++)
{
! List *options;
! ListCell *lc;
/* Skip dropped attributes. */
if (tupleDesc->attrs[attnum - 1]->attisdropped)
continue;
! options = GetForeignColumnOptions(relid, attnum);
! foreach(lc, options)
{
! DefElem *def = (DefElem *) lfirst(lc);
! if (strcmp(def->defname, "force_not_null") == 0)
{
! if (defGetBoolean(def))
{
! char *attname = pstrdup(get_attname(relid, attnum));
! fnncolumns = lappend(fnncolumns, makeString(attname));
}
}
+ /* maybe in future handle other options here */
}
}
heap_close(rel, AccessShareLock);
diff --git a/doc/src/sgml/fdwhandler.sgml b/doc/src/sgml/fdwhandler.sgml
index 76ff243..d809cac 100644
*** a/doc/src/sgml/fdwhandler.sgml
--- b/doc/src/sgml/fdwhandler.sgml
*************** EndForeignScan (ForeignScanState *node);
*** 235,238 ****
--- 235,392 ----
</sect1>
+ <sect1 id="fdw-helpers">
+ <title>Foreign Data Wrapper Helper Functions</title>
+
+ <para>
+ Several helper functions are exported from core so that authors of FDW
+ can get easy access to attributes of FDW-related objects such as FDW
+ options.
+ </para>
+
+ <para>
+ <programlisting>
+ ForeignDataWrapper *
+ GetForeignDataWrapper(Oid fdwid);
+ </programlisting>
+
+ This function returns a <structname>ForeignDataWrapper</structname> object
+ for a foreign-data wrapper with given oid. A
+ <structname>ForeignDataWrapper</structname> object contains oid of the
+ wrapper itself, oid of the owner of the wrapper, name of the wrapper, oid
+ of fdwhandler function, oid of fdwvalidator function, and FDW options in
+ the form of list of <structname>DefElem</structname>.
+ </para>
+
+ <para>
+ <programlisting>
+ ForeignServer *
+ GetForeignServer(Oid serverid);
+ </programlisting>
+
+ This function returns a <structname>ForeignServer</structname> object for
+ a foreign server with given oid. A <structname>ForeignServer</structname>
+ object contains oid of the server, oid of the wrapper for the server, oid
+ of the owner of the server, name of the server, type of the server,
+ version of the server, and FDW options in the form of list of
+ <structname>DefElem</structname>.
+ </para>
+
+ <para>
+ <programlisting>
+ UserMapping *
+ GetUserMapping(Oid userid, Oid serverid);
+ </programlisting>
+
+ This function returns a <structname>UserMapping</structname> object for a
+ user mapping with given oid pair. A <structname>UserMapping</structname>
+ object contains oid of the user, oid of the server, and FDW options in the
+ form of list of <structname>DefElem</structname>.
+ </para>
+
+ <para>
+ <programlisting>
+ ForeignTable *
+ GetForeignTable(Oid relid);
+ </programlisting>
+
+ This function returns a <structname>ForeignTable</structname> object for a
+ foreign table with given oid. A <structname>ForeignTable</structname>
+ object contains oid of the foreign table, oid of the server for the table,
+ and FDW options in the form of list of <structname>DefElem</structname>.
+ </para>
+
+ <para>
+ <programlisting>
+ List *
+ GetForeignTableColumnOptions(Oid relid, AttrNumber attnum);
+ </programlisting>
+
+ This function returns per-column FDW options for a column with given
+ relation oid and attribute number in the form of list of
+ <structname>DefElem</structname>.
+ </para>
+
+ <para>
+ Some object types have name-based functions.
+ </para>
+
+ <para>
+ <programlisting>
+ ForeignDataWrapper *
+ GetForeignDataWrapperByName(const char *name, bool missing_ok);
+ </programlisting>
+
+ This function returns a <structname>ForeignDataWrapper</structname> object
+ for a foreign-data wrapper with given name. If the wrapper is not found,
+ return NULL if missing_ok was true, otherwise raise an error.
+ </para>
+
+ <para>
+ <programlisting>
+ ForeignServer *
+ GetForeignServerByName(const char *name, bool missing_ok);
+ </programlisting>
+
+ This function returns a <structname>ForeignServer</structname> object for
+ a foreign server with given name. If the server is not found, return NULL
+ if missing_ok was true, otherwise raise an error.
+ </para>
+
+ <para>
+ <programlisting>
+ char *
+ GetFdwOptionValue(Oid fdwid, Oid serverid, Oid relid, AttrNumber attnum, const char *optname);
+ </programlisting>
+
+ This function returns a copied string (created in current memory context)
+ of the value of a FDW option with given name which is set on a object with
+ given oid and attribute number. This function ignores catalogs if invalid
+ identifir is given for it.
+
+ <itemizedlist>
+ <listitem>
+ <para>
+ If attnum is <literal>InvalidAttrNumber</literal> or relid is
+ <literal>Invalidoid</literal>, <structname>pg_attribute</structname> is
+ ignored.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ If relid is <literal>InvalidOid</literal>,
+ <structname>pg_foreign_table</structname> is ignored.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ If both serverid and relid are <literal>InvalidOid</literal>,
+ <structname>pg_foreign_server</structname> is ignored.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ If all of fdwid, serverid and relid are <literal>InvalidOid</literal>,
+ <structname>pg_foreign_data_wrapper</structname> is ignored.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+
+ <para>
+ If the option with given name is set in multiple object level, the one in
+ the finest-grained object is used; e.g. priority is given to user mappings
+ over than foreign servers.
+ This function would be useful when you know which option is needed but you
+ don't know where it is set. If you already know the source object, it
+ would be more efficient to use object retrieval functions.
+ </para>
+
+ <para>
+ To use any of these functions, you need to include
+ <filename>foreign/foreign.h</filename> in your source file.
+ </para>
+
+ </sect1>
+
</chapter>
diff --git a/src/backend/foreign/foreign.c b/src/backend/foreign/foreign.c
index a7d30a1..b984a5e 100644
*** a/src/backend/foreign/foreign.c
--- b/src/backend/foreign/foreign.c
*************** GetForeignTable(Oid relid)
*** 248,253 ****
--- 248,378 ----
/*
+ * If an option entry which matches the given name was found in the given
+ * list, returns a copy of arg string, otherwise returns NULL.
+ */
+ static char *
+ get_options_value(List *options, const char *optname)
+ {
+ ListCell *lc;
+
+ /* Find target option from the list. */
+ foreach (lc, options)
+ {
+ DefElem *def = lfirst(lc);
+
+ if (strcmp(def->defname, optname) == 0)
+ return pstrdup(strVal(def->arg));
+ }
+
+ return NULL;
+ }
+
+ /*
+ * Returns a copy of the value of specified option with searching the option
+ * from appropriate catalog. If an option was stored in multiple object
+ * levels, one in the finest-grained object level is used; lookup order is:
+ * 1) pg_attribute (only when both attnum and relid are valid)
+ * 2) pg_foreign_table (only when relid is valid)
+ * 3) pg_user_mapping (only when serverid or relid is valid)
+ * 4) pg_foreign_server (only when serverid or relid is valid)
+ * 5) pg_foreign_data_wrapper (only when fdwid or serverid or relid is valid)
+ * This priority rule would be useful in most cases using FDW options.
+ *
+ * If attnum was InvalidAttrNumber, we don't retrieve FDW optiosn from
+ * pg_attribute.attfdwoptions.
+ */
+ char *
+ GetFdwOptionValue(Oid fdwid, Oid serverid, Oid relid, AttrNumber attnum,
+ const char *optname)
+ {
+ ForeignTable *table = NULL;
+ UserMapping *user = NULL;
+ ForeignServer *server = NULL;
+ ForeignDataWrapper *wrapper = NULL;
+ char *value;
+
+ /* Do we need to search pg_attribute? */
+ if (attnum != InvalidAttrNumber && relid != InvalidOid)
+ {
+ value = get_options_value(GetForeignColumnOptions(relid, attnum),
+ optname);
+ if (value != NULL)
+ return value;
+ }
+
+ /* Do we need to search pg_foreign_table? */
+ if (relid != InvalidOid)
+ {
+ table = GetForeignTable(relid);
+ value = get_options_value(table->options, optname);
+ if (value != NULL)
+ return value;
+
+ serverid = table->serverid;
+ }
+
+ /* Do we need to search pg_user_mapping and pg_foreign_server? */
+ if (serverid != InvalidOid)
+ {
+ user = GetUserMapping(GetOuterUserId(), serverid);
+ value = get_options_value(user->options, optname);
+ if (value != NULL)
+ return value;
+
+ server = GetForeignServer(serverid);
+ value = get_options_value(server->options, optname);
+ if (value != NULL)
+ return value;
+
+ fdwid = server->fdwid;
+ }
+
+ /* Do we need to search pg_foreign_data_wrapper? */
+ if (fdwid != InvalidOid)
+ {
+ wrapper = GetForeignDataWrapper(fdwid);
+ value = get_options_value(wrapper->options, optname);
+ if (value != NULL)
+ return value;
+ }
+
+ return NULL;
+ }
+
+
+ /*
+ * GetForeignColumnOptions - Get attfdwoptions of given relation/attnum as
+ * list of DefElem.
+ */
+ List *
+ GetForeignColumnOptions(Oid relid, AttrNumber attnum)
+ {
+ List *options;
+ HeapTuple tp;
+ Datum datum;
+ bool isnull;
+
+ tp = SearchSysCache2(ATTNUM,
+ ObjectIdGetDatum(relid),
+ Int16GetDatum(attnum));
+ if (!HeapTupleIsValid(tp))
+ elog(ERROR, "cache lookup failed for attribute %d of relation %u", attnum, relid);
+ datum = SysCacheGetAttr(ATTNUM,
+ tp,
+ Anum_pg_attribute_attfdwoptions,
+ &isnull);
+ if (isnull)
+ options = NIL;
+ else
+ options = untransformRelOptions(datum);
+
+ ReleaseSysCache(tp);
+
+ return options;
+ }
+
+ /*
* GetFdwRoutine - call the specified foreign-data wrapper handler routine
* to get its FdwRoutine struct.
*/
diff --git a/src/include/foreign/foreign.h b/src/include/foreign/foreign.h
index 2c436ae..6498162 100644
*** a/src/include/foreign/foreign.h
--- b/src/include/foreign/foreign.h
*************** extern ForeignDataWrapper *GetForeignDat
*** 75,80 ****
--- 75,83 ----
extern ForeignDataWrapper *GetForeignDataWrapperByName(const char *name,
bool missing_ok);
extern ForeignTable *GetForeignTable(Oid relid);
+ extern char *GetFdwOptionValue(Oid fdwid, Oid serverid, Oid relid,
+ AttrNumber attnum, const char *optname);
+ extern List *GetForeignColumnOptions(Oid relid, AttrNumber attnum);
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);
pgsql_fdw_v5.patchtext/plain; name=pgsql_fdw_v5.patchDownload
diff --git a/contrib/Makefile b/contrib/Makefile
index 0c238aa..e09e61e 100644
*** a/contrib/Makefile
--- b/contrib/Makefile
*************** SUBDIRS = \
*** 41,46 ****
--- 41,47 ----
pgbench \
pgcrypto \
pgrowlocks \
+ pgsql_fdw \
pgstattuple \
seg \
spi \
diff --git a/contrib/README b/contrib/README
index a1d42a1..d3fa211 100644
*** a/contrib/README
--- b/contrib/README
*************** pgrowlocks -
*** 158,163 ****
--- 158,167 ----
A function to return row locking information
by Tatsuo Ishii <ishii@sraoss.co.jp>
+ pgsql_fdw -
+ Foreign-data wrapper for external PostgreSQL servers.
+ by Shigeru Hanada <shigeru.hanada@gmail.com>
+
pgstattuple -
Functions to return statistics about "dead" tuples and free
space within a table
diff --git a/contrib/pgsql_fdw/.gitignore b/contrib/pgsql_fdw/.gitignore
index ...0854728 .
*** a/contrib/pgsql_fdw/.gitignore
--- b/contrib/pgsql_fdw/.gitignore
***************
*** 0 ****
--- 1,4 ----
+ # Generated subdirectories
+ /results/
+ *.o
+ *.so
diff --git a/contrib/pgsql_fdw/Makefile b/contrib/pgsql_fdw/Makefile
index ...6381365 .
*** a/contrib/pgsql_fdw/Makefile
--- b/contrib/pgsql_fdw/Makefile
***************
*** 0 ****
--- 1,22 ----
+ # contrib/pgsql_fdw/Makefile
+
+ MODULE_big = pgsql_fdw
+ OBJS = pgsql_fdw.o option.o deparse.o connection.o
+ PG_CPPFLAGS = -I$(libpq_srcdir)
+ SHLIB_LINK = $(libpq)
+
+ EXTENSION = pgsql_fdw
+ DATA = pgsql_fdw--1.0.sql
+
+ REGRESS = pgsql_fdw
+
+ ifdef USE_PGXS
+ PG_CONFIG = pg_config
+ PGXS := $(shell $(PG_CONFIG) --pgxs)
+ include $(PGXS)
+ else
+ subdir = contrib/pgsql_fdw
+ top_builddir = ../..
+ include $(top_builddir)/src/Makefile.global
+ include $(top_srcdir)/contrib/contrib-global.mk
+ endif
diff --git a/contrib/pgsql_fdw/connection.c b/contrib/pgsql_fdw/connection.c
index ...b497dab .
*** a/contrib/pgsql_fdw/connection.c
--- b/contrib/pgsql_fdw/connection.c
***************
*** 0 ****
--- 1,508 ----
+ /*-------------------------------------------------------------------------
+ *
+ * connection.c
+ * Connection management for pgsql_fdw
+ *
+ * Portions Copyright (c) 2011, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/connection.c
+ *
+ *-------------------------------------------------------------------------
+ */
+ #include "postgres.h"
+
+ #include "catalog/pg_type.h"
+ #include "foreign/foreign.h"
+ #include "funcapi.h"
+ #include "libpq-fe.h"
+ #include "mb/pg_wchar.h"
+ #include "miscadmin.h"
+ #include "utils/array.h"
+ #include "utils/builtins.h"
+ #include "utils/hsearch.h"
+ #include "utils/memutils.h"
+ #include "utils/resowner.h"
+ #include "utils/tuplestore.h"
+
+ #include "pgsql_fdw.h"
+ #include "connection.h"
+
+ /* ============================================================================
+ * Connection management functions
+ * ==========================================================================*/
+
+ /*
+ * Connection cache entry managed with hash table.
+ */
+ typedef struct ConnCacheEntry
+ {
+ /* hash key must be first */
+ Oid serverid; /* oid of foreign server */
+ Oid userid; /* oid of local user */
+
+ 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)
+ {
+ bool found;
+ ConnCacheEntry *entry;
+ ConnCacheEntry key;
+ PGconn *conn = NULL;
+
+ /* initialize connection cache if it isn't */
+ if (FSConnectionHash == NULL)
+ {
+ HASHCTL ctl;
+
+ /* hash key is a pair of oids: serverid and userid */
+ MemSet(&ctl, 0, sizeof(ctl));
+ ctl.keysize = sizeof(Oid) + sizeof(Oid);
+ ctl.entrysize = sizeof(ConnCacheEntry);
+ ctl.hash = tag_hash;
+ ctl.match = memcmp;
+ ctl.keycopy = memcpy;
+ /* allocate FSConnectionHash in the cache context */
+ ctl.hcxt = CacheMemoryContext;
+ FSConnectionHash = hash_create("Foreign Connections", 32,
+ &ctl,
+ HASH_ELEM | HASH_CONTEXT |
+ HASH_FUNCTION | HASH_COMPARE |
+ HASH_KEYCOPY);
+ }
+
+ /* Create key value for the entry. */
+ MemSet(&key, 0, sizeof(key));
+ key.serverid = server->serverid;
+ key.userid = GetOuterUserId();
+
+ /* Is there any cached and valid connection with such key? */
+ entry = hash_search(FSConnectionHash, &key, HASH_ENTER, &found);
+ if (found)
+ {
+ if (entry->conn != NULL)
+ {
+ entry->refs++;
+ elog(DEBUG1,
+ "reuse connection %u/%u (%d)",
+ entry->serverid,
+ entry->userid,
+ entry->refs);
+ 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 ResourceOwner to clean the connection up on error including
+ * user interrupt.
+ */
+ elog(DEBUG1,
+ "create entry for %u/%u (%d)",
+ entry->serverid,
+ entry->userid,
+ entry->refs);
+ 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: key items of entry has been initialized in
+ * hash_search(HASH_ENTER).
+ */
+ entry->refs = 1;
+ entry->conn = conn;
+ elog(DEBUG1,
+ "connected to %u/%u (%d)",
+ entry->serverid,
+ entry->userid,
+ 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 PGconn *
+ connect_pg_server(ForeignServer *server, UserMapping *user)
+ {
+ const char *conname = server->servername;
+ PGconn *conn;
+ PGresult *res;
+ 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. Those two object hold only libpq options.
+ * Extra 3 items are for:
+ * *) fallback_application_name
+ * *) client_encoding
+ * *) NULL termination (end marker)
+ *
+ * Note: We don't omit any parameters even target database might be older
+ * than local, because unexpected parameters are just ignored.
+ */
+ n = list_length(server->options) + list_length(user->options) + 3;
+ 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 += ExtractConnectionOptions(server->options,
+ all_keywords + n, all_values + n);
+ n += ExtractConnectionOptions(user->options,
+ all_keywords + n, all_values + n);
+ all_keywords[n] = all_values[n] = NULL;
+
+ for (i = 0, j = 0; all_keywords[i]; i++)
+ {
+ keywords[j] = all_keywords[i];
+ values[j] = all_values[i];
+ j++;
+ }
+
+ /* Use "pgsql_fdw" as fallback_application_name. */
+ keywords[j] = "fallback_application_name";
+ values[j++] = "pgsql_fdw";
+
+ /* Set client_encoding so that libpq can convert encoding properly. */
+ keywords[j] = "client_encoding";
+ values[j++] = GetDatabaseEncodingName();
+
+ keywords[j] = values[j] = NULL;
+ pfree(all_keywords);
+ pfree(all_values);
+
+ /* 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.")));
+ }
+
+ /*
+ * Start transaction to use cursor to retrieve data separately.
+ */
+ res = PQexec(conn, "BEGIN");
+ if (PQresultStatus(res) != PGRES_COMMAND_OK)
+ {
+ PQclear(res);
+ elog(ERROR, "could not start transaction");
+ }
+ PQclear(res);
+
+ 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 sequentially 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;
+ }
+ if (entry != NULL)
+ 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 referrer, unregister it from cache.
+ * TODO: Note that sharing connections requires a mechanism to detect
+ * change of FDW object to invalidate lasting connections.
+ */
+ entry->refs--;
+ elog(DEBUG1,
+ "connection %u/%u released (%d)",
+ entry->serverid,
+ entry->userid,
+ entry->refs);
+ }
+
+ /*
+ * 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(DEBUG1,
+ "closing connection %u/%u",
+ entry->serverid,
+ entry->userid);
+ PQfinish(entry->conn);
+ entry->refs = 0;
+ entry->conn = NULL;
+ }
+ else
+ elog(DEBUG1,
+ "connection %u/%u already closed",
+ entry->serverid,
+ entry->userid);
+ }
+
+ /*
+ * Get list of connections currently active.
+ */
+ Datum pgsql_fdw_get_connections(PG_FUNCTION_ARGS);
+ PG_FUNCTION_INFO_V1(pgsql_fdw_get_connections);
+ Datum
+ pgsql_fdw_get_connections(PG_FUNCTION_ARGS)
+ {
+ ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+ HASH_SEQ_STATUS scan;
+ ConnCacheEntry *entry;
+ MemoryContext oldcontext = CurrentMemoryContext;
+ Tuplestorestate *tuplestore;
+ TupleDesc tupdesc;
+
+ /* We return list of connection with storing them in a Tuplestore. */
+ rsinfo->returnMode = SFRM_Materialize;
+ rsinfo->setResult = NULL;
+ rsinfo->setDesc = NULL;
+
+ /* Create tuplestore and copy of TupleDesc in per-query context. */
+ MemoryContextSwitchTo(rsinfo->econtext->ecxt_per_query_memory);
+
+ tupdesc = CreateTemplateTupleDesc(2, false);
+ TupleDescInitEntry(tupdesc, 1, "srvid", OIDOID, -1, 0);
+ TupleDescInitEntry(tupdesc, 2, "usesysid", OIDOID, -1, 0);
+ rsinfo->setDesc = tupdesc;
+
+ tuplestore = tuplestore_begin_heap(false, false, work_mem);
+ rsinfo->setResult = tuplestore;
+
+ MemoryContextSwitchTo(oldcontext);
+
+ /*
+ * We need to scan sequentially since we use the address to find
+ * appropriate PGconn from the hash table.
+ */
+ if (FSConnectionHash != NULL)
+ {
+ hash_seq_init(&scan, FSConnectionHash);
+ while ((entry = (ConnCacheEntry *) hash_seq_search(&scan)))
+ {
+ Datum values[2];
+ bool nulls[2];
+ HeapTuple tuple;
+
+ elog(DEBUG1, "found: %u/%u", entry->serverid, entry->userid);
+
+ /* Ignore inactive connections */
+ if (PQstatus(entry->conn) != CONNECTION_OK)
+ continue;
+
+ /*
+ * Ignore other users' connections if current user isn't a
+ * superuser.
+ */
+ if (!superuser() && entry->userid != GetUserId())
+ continue;
+
+ values[0] = ObjectIdGetDatum(entry->serverid);
+ values[1] = ObjectIdGetDatum(entry->userid);
+ nulls[0] = false;
+ nulls[1] = false;
+
+ tuple = heap_formtuple(tupdesc, values, nulls);
+ tuplestore_puttuple(tuplestore, tuple);
+ }
+ }
+ tuplestore_donestoring(tuplestore);
+
+ PG_RETURN_VOID();
+ }
+
+ /*
+ * Discard persistent connection designated by given connection name.
+ */
+ Datum pgsql_fdw_disconnect(PG_FUNCTION_ARGS);
+ PG_FUNCTION_INFO_V1(pgsql_fdw_disconnect);
+ Datum
+ pgsql_fdw_disconnect(PG_FUNCTION_ARGS)
+ {
+ Oid serverid = PG_GETARG_OID(0);
+ Oid userid = PG_GETARG_OID(1);
+ ConnCacheEntry key;
+ ConnCacheEntry *entry = NULL;
+ bool found;
+
+ /* Non-superuser can't discard other users' connection. */
+ if (!superuser() && userid != GetOuterUserId())
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_RESOURCES),
+ errmsg("only superuser can discard other user's connection")));
+
+ /*
+ * If no connection has been established, or no such connections, just
+ * return "NG" to indicate nothing has done.
+ */
+ if (FSConnectionHash == NULL)
+ PG_RETURN_TEXT_P(cstring_to_text("NG"));
+
+ key.serverid = serverid;
+ key.userid = userid;
+ entry = hash_search(FSConnectionHash, &key, HASH_FIND, &found);
+ if (!found)
+ PG_RETURN_TEXT_P(cstring_to_text("NG"));
+
+ /* Discard cached connection, and clear reference counter. */
+ PQfinish(entry->conn);
+ entry->refs = 0;
+ entry->conn = NULL;
+ elog(DEBUG1, "closed connection %u/%u", serverid, userid);
+
+ PG_RETURN_TEXT_P(cstring_to_text("OK"));
+ }
diff --git a/contrib/pgsql_fdw/connection.h b/contrib/pgsql_fdw/connection.h
index ...694534f .
*** a/contrib/pgsql_fdw/connection.h
--- b/contrib/pgsql_fdw/connection.h
***************
*** 0 ****
--- 1,25 ----
+ /*-------------------------------------------------------------------------
+ *
+ * connection.h
+ * Connection management for pgsql_fdw
+ *
+ * Portions Copyright (c) 2011, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_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/pgsql_fdw/deparse.c b/contrib/pgsql_fdw/deparse.c
index ...def5cb3 .
*** a/contrib/pgsql_fdw/deparse.c
--- b/contrib/pgsql_fdw/deparse.c
***************
*** 0 ****
--- 1,199 ----
+ /*-------------------------------------------------------------------------
+ *
+ * deparse.c
+ * query deparser for PostgreSQL
+ *
+ * Copyright (c) 2011, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/deparse.c
+ *
+ *-------------------------------------------------------------------------
+ */
+ #include "postgres.h"
+
+ #include "access/transam.h"
+ #include "foreign/foreign.h"
+ #include "lib/stringinfo.h"
+ #include "nodes/nodeFuncs.h"
+ #include "nodes/nodes.h"
+ #include "nodes/makefuncs.h"
+ #include "optimizer/clauses.h"
+ #include "optimizer/var.h"
+ #include "parser/parsetree.h"
+ #include "utils/builtins.h"
+ #include "utils/lsyscache.h"
+
+ #include "pgsql_fdw.h"
+
+ /*
+ * Context for walk-through the expression tree.
+ */
+ typedef struct foreign_executable_cxt
+ {
+ PlannerInfo *root;
+ RelOptInfo *foreignrel;
+ } foreign_executable_cxt;
+
+ /*
+ * Deparse query representation into SQL statement which suits for remote
+ * PostgreSQL server.
+ */
+ char *
+ deparseSql(Oid relid, PlannerInfo *root, RelOptInfo *baserel)
+ {
+ StringInfoData foreign_relname;
+ StringInfoData sql; /* builder for SQL statement */
+ bool first;
+ AttrNumber attr;
+ List *attr_used = NIL; /* List of AttNumber used in the query */
+ const char *nspname = NULL; /* plain namespace name */
+ const char *relname = NULL; /* plain relation name */
+ const char *q_nspname; /* quoted namespace name */
+ const char *q_relname; /* quoted relation name */
+ int i;
+ List *rtable = NIL;
+ List *context = NIL;
+
+ initStringInfo(&sql);
+ initStringInfo(&foreign_relname);
+
+ /*
+ * First of all, determine which column should be retrieved for this scan.
+ *
+ * We do this before deparsing SELECT clause because attributes which are
+ * not used in neither reltargetlist nor baserel->baserestrictinfo, quals
+ * evaluated on local, can be replaced with literal "NULL" in the SELECT
+ * clause to reduce overhead of tuple handling tuple and data transfer.
+ */
+ if (baserel->baserestrictinfo != NIL)
+ {
+ ListCell *lc;
+
+ foreach (lc, baserel->baserestrictinfo)
+ {
+ RestrictInfo *ri = (RestrictInfo *) lfirst(lc);
+ List *attrs;
+
+ /*
+ * We need to know which attributes are used in qual evaluated
+ * on the local server, because they should be listed in the
+ * SELECT clause of remote query. We can ignore attributes
+ * which are referenced only in ORDER BY/GROUP BY clause because
+ * such attributes has already been kept in reltargetlist.
+ */
+ attrs = pull_var_clause((Node *) ri->clause,
+ PVC_RECURSE_AGGREGATES,
+ PVC_RECURSE_PLACEHOLDERS);
+ attr_used = list_union(attr_used, attrs);
+ }
+ }
+
+ /*
+ * Determine foreign relation's qualified name. This is necessary for
+ * FROM clause and SELECT clause.
+ */
+ nspname = GetFdwOptionValue(InvalidOid, InvalidOid, relid,
+ InvalidAttrNumber, "nspname");
+ if (nspname == NULL)
+ nspname = get_namespace_name(get_rel_namespace(relid));
+ q_nspname = quote_identifier(nspname);
+
+ relname = GetFdwOptionValue(InvalidOid, InvalidOid, relid,
+ InvalidAttrNumber, "relname");
+ if (relname == NULL)
+ relname = get_rel_name(relid);
+ q_relname = quote_identifier(relname);
+
+ appendStringInfo(&foreign_relname, "%s.%s", q_nspname, q_relname);
+
+ /*
+ * We need to replace aliasname and colnames of the target relation so that
+ * constructed remote query is valid.
+ *
+ * Note that we skip first empty element of simple_rel_array. See also
+ * comments of simple_rel_array and simple_rte_array for the rationale.
+ */
+ for (i = 1; i < root->simple_rel_array_size; i++)
+ {
+ RangeTblEntry *rte = copyObject(root->simple_rte_array[i]);
+ List *newcolnames = NIL;
+
+ if (i == baserel->relid)
+ {
+ /*
+ * Create new list of column names which is used to deparse remote
+ * query from specified names or local column names. This list is
+ * used by deparse_expression.
+ */
+ for (attr = 1; attr <= baserel->max_attr; attr++)
+ {
+ char *colname;
+
+ /* Ignore dropped attributes. */
+ if (get_rte_attribute_is_dropped(rte, attr))
+ continue;
+
+ colname = GetFdwOptionValue(InvalidOid, InvalidOid, relid,
+ attr, "colname");
+ if (colname == NULL)
+ colname = strVal(list_nth(rte->eref->colnames, attr - 1));
+ newcolnames = lappend(newcolnames, makeString(colname));
+ }
+ rte->alias = makeAlias(relname, newcolnames);
+ }
+ rtable = lappend(rtable, rte);
+ }
+ context = deparse_context_for_rtelist(rtable);
+
+ /*
+ * deparse SELECT clause
+ *
+ * List attributes which are in either target list or local restriction.
+ * Unused attributes are replaced with a literal "NULL" for optimization.
+ */
+ appendStringInfo(&sql, "SELECT ");
+ attr_used = list_union(attr_used, baserel->reltargetlist);
+ first = true;
+ for (attr = 1; attr <= baserel->max_attr; attr++)
+ {
+ RangeTblEntry *rte = root->simple_rte_array[baserel->relid];
+ Var *var = NULL;
+ ListCell *lc;
+
+ /* Ignore dropped attributes. */
+ if (get_rte_attribute_is_dropped(rte, attr))
+ continue;
+
+ if (!first)
+ appendStringInfo(&sql, ", ");
+ first = false;
+
+ /*
+ * We use linear search here, but it wouldn't be problem since
+ * attr_used seems to not become so large.
+ */
+ foreach (lc, attr_used)
+ {
+ var = lfirst(lc);
+ if (var->varattno == attr)
+ break;
+ var = NULL;
+ }
+ if (var != NULL)
+ appendStringInfo(&sql, "%s",
+ deparse_expression((Node *) var, context, false, false));
+ else
+ appendStringInfo(&sql, "NULL");
+ }
+ appendStringInfoChar(&sql, ' ');
+
+ /*
+ * deparse FROM clause, including alias if any
+ */
+ appendStringInfo(&sql, "FROM %s", foreign_relname.data);
+
+ elog(DEBUG3, "Remote SQL: %s", sql.data);
+ return sql.data;
+ }
+
diff --git a/contrib/pgsql_fdw/expected/pgsql_fdw.out b/contrib/pgsql_fdw/expected/pgsql_fdw.out
index ...dfac57a .
*** a/contrib/pgsql_fdw/expected/pgsql_fdw.out
--- b/contrib/pgsql_fdw/expected/pgsql_fdw.out
***************
*** 0 ****
--- 1,526 ----
+ -- ===================================================================
+ -- create FDW objects
+ -- ===================================================================
+ CREATE EXTENSION pgsql_fdw;
+ CREATE SERVER loopback1 FOREIGN DATA WRAPPER pgsql_fdw;
+ CREATE SERVER loopback2 FOREIGN DATA WRAPPER pgsql_fdw
+ OPTIONS (dbname 'contrib_regression');
+ CREATE USER MAPPING FOR public SERVER loopback1
+ OPTIONS (user 'value', password 'value');
+ CREATE USER MAPPING FOR public SERVER loopback2;
+ CREATE FOREIGN TABLE ft1 (
+ c1 int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10)
+ ) SERVER loopback2;
+ CREATE FOREIGN TABLE ft2 (
+ c1 int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10)
+ ) SERVER loopback2;
+ -- ===================================================================
+ -- create objects used through FDW
+ -- ===================================================================
+ CREATE SCHEMA "S 1";
+ CREATE TABLE "S 1"."T 1" (
+ "C 1" int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10),
+ CONSTRAINT t1_pkey PRIMARY KEY ("C 1")
+ );
+ NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "t1_pkey" for table "T 1"
+ CREATE TABLE "S 1"."T 2" (
+ c1 int NOT NULL,
+ c2 text,
+ CONSTRAINT t2_pkey PRIMARY KEY (c1)
+ );
+ NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "t2_pkey" for table "T 2"
+ BEGIN;
+ TRUNCATE "S 1"."T 1";
+ INSERT INTO "S 1"."T 1"
+ SELECT id,
+ id % 10,
+ to_char(id, 'FM00000'),
+ '1970-01-01'::timestamptz + ((id % 100) || ' days')::interval,
+ '1970-01-01'::timestamp + ((id % 100) || ' days')::interval,
+ id % 10,
+ id % 10
+ FROM generate_series(1, 1000) id;
+ TRUNCATE "S 1"."T 2";
+ INSERT INTO "S 1"."T 2"
+ SELECT id,
+ 'AAA' || to_char(id, 'FM000')
+ FROM generate_series(1, 100) id;
+ COMMIT;
+ -- ===================================================================
+ -- tests for pgsql_fdw_validator
+ -- ===================================================================
+ ALTER FOREIGN DATA WRAPPER pgsql_fdw OPTIONS (host 'value'); -- ERROR
+ ERROR: invalid option "host"
+ HINT: Valid options in this context are:
+ -- requiressl, krbsrvname and gsslib are omitted because they depend on
+ -- configure option
+ ALTER SERVER loopback1 OPTIONS (
+ authtype 'value',
+ service 'value',
+ connect_timeout 'value',
+ dbname 'value',
+ host 'value',
+ hostaddr 'value',
+ port 'value',
+ --client_encoding 'value',
+ tty 'value',
+ options 'value',
+ application_name 'value',
+ --fallback_application_name 'value',
+ keepalives 'value',
+ keepalives_idle 'value',
+ keepalives_interval 'value',
+ -- requiressl 'value',
+ sslmode 'value',
+ sslcert 'value',
+ sslkey 'value',
+ sslrootcert 'value',
+ sslcrl 'value'
+ --requirepeer 'value',
+ -- krbsrvname 'value',
+ -- gsslib 'value',
+ --replication 'value'
+ );
+ ALTER SERVER loopback1 OPTIONS (user 'value'); -- ERROR
+ ERROR: invalid option "user"
+ HINT: Valid options in this context are: authtype, service, connect_timeout, dbname, host, hostaddr, port, tty, options, application_name, keepalives, keepalives_idle, keepalives_interval, keepalives_count, sslmode, sslcert, sslkey, sslrootcert, sslcrl, requirepeer, fetch_count
+ ALTER SERVER loopback2 OPTIONS (ADD fetch_count '2');
+ ALTER USER MAPPING FOR public SERVER loopback1
+ OPTIONS (DROP user, DROP password);
+ ALTER USER MAPPING FOR public SERVER loopback1
+ OPTIONS (host 'value'); -- ERROR
+ ERROR: invalid option "host"
+ HINT: Valid options in this context are: user, password
+ ALTER FOREIGN TABLE ft1 OPTIONS (nspname 'S 1', relname 'T 1');
+ ALTER FOREIGN TABLE ft2 OPTIONS (nspname 'S 1', relname 'T 1', fetch_count '100');
+ ALTER FOREIGN TABLE ft1 OPTIONS (invalid 'value'); -- ERROR
+ ERROR: invalid option "invalid"
+ HINT: Valid options in this context are: nspname, relname, fetch_count
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count 'a'); -- ERROR
+ ERROR: invalid value for fetch_count: "a"
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count '0'); -- ERROR
+ ERROR: invalid value for fetch_count: "0"
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count '-1'); -- ERROR
+ ERROR: invalid value for fetch_count: "-1"
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (invalid 'value'); -- ERROR
+ ERROR: invalid option "invalid"
+ HINT: Valid options in this context are: colname
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (colname 'C 1');
+ ALTER FOREIGN TABLE ft2 ALTER COLUMN c1 OPTIONS (colname 'C 1');
+ \dew+
+ List of foreign-data wrappers
+ Name | Owner | Handler | Validator | Access privileges | FDW Options | Description
+ -----------+----------+-------------------+---------------------+-------------------+-------------+-------------
+ pgsql_fdw | postgres | pgsql_fdw_handler | pgsql_fdw_validator | | |
+ (1 row)
+
+ \des+
+ List of foreign servers
+ Name | Owner | Foreign-data wrapper | Access privileges | Type | Version | FDW Options | Description
+ -----------+----------+----------------------+-------------------+------+---------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+-------------
+ loopback1 | postgres | pgsql_fdw | | | | (authtype 'value', service 'value', connect_timeout 'value', dbname 'value', host 'value', hostaddr 'value', port 'value', tty 'value', options 'value', application_name 'value', keepalives 'value', keepalives_idle 'value', keepalives_interval 'value', sslmode 'value', sslcert 'value', sslkey 'value', sslrootcert 'value', sslcrl 'value') |
+ loopback2 | postgres | pgsql_fdw | | | | (dbname 'contrib_regression', fetch_count '2') |
+ (2 rows)
+
+ \deu+
+ List of user mappings
+ Server | User name | FDW Options
+ -----------+-----------+-------------
+ loopback1 | public |
+ loopback2 | public |
+ (2 rows)
+
+ \det+
+ List of foreign tables
+ Schema | Table | Server | FDW Options | Description
+ --------+-------+-----------+---------------------------------------------------+-------------
+ public | ft1 | loopback2 | (nspname 'S 1', relname 'T 1') |
+ public | ft2 | loopback2 | (nspname 'S 1', relname 'T 1', fetch_count '100') |
+ (2 rows)
+
+ -- ===================================================================
+ -- simple queries
+ -- ===================================================================
+ -- single table, with/without alias
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
+ QUERY PLAN
+ ------------------------------------------------------------------------------------------------------------------------------
+ Limit
+ Output: c1, c2, c3, c4, c5, c6, c7
+ -> Sort
+ Output: c1, c2, c3, c4, c5, c6, c7
+ Sort Key: ft1.c3, ft1.c1
+ -> Foreign Scan on public.ft1
+ Output: c1, c2, c3, c4, c5, c6, c7
+ Remote SQL: DECLARE pgsql_fdw_cursor_0 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (8 rows)
+
+ SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ -----+----+-------+------------------------------+--------------------------+----+------------
+ 101 | 1 | 00101 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970 | 1 | 1
+ 102 | 2 | 00102 | Sat Jan 03 00:00:00 1970 PST | Sat Jan 03 00:00:00 1970 | 2 | 2
+ 103 | 3 | 00103 | Sun Jan 04 00:00:00 1970 PST | Sun Jan 04 00:00:00 1970 | 3 | 3
+ 104 | 4 | 00104 | Mon Jan 05 00:00:00 1970 PST | Mon Jan 05 00:00:00 1970 | 4 | 4
+ 105 | 5 | 00105 | Tue Jan 06 00:00:00 1970 PST | Tue Jan 06 00:00:00 1970 | 5 | 5
+ 106 | 6 | 00106 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6
+ 107 | 7 | 00107 | Thu Jan 08 00:00:00 1970 PST | Thu Jan 08 00:00:00 1970 | 7 | 7
+ 108 | 8 | 00108 | Fri Jan 09 00:00:00 1970 PST | Fri Jan 09 00:00:00 1970 | 8 | 8
+ 109 | 9 | 00109 | Sat Jan 10 00:00:00 1970 PST | Sat Jan 10 00:00:00 1970 | 9 | 9
+ 110 | 0 | 00110 | Sun Jan 11 00:00:00 1970 PST | Sun Jan 11 00:00:00 1970 | 0 | 0
+ (10 rows)
+
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+ QUERY PLAN
+ ------------------------------------------------------------------------------------------------------------------------------
+ Limit
+ Output: c1, c2, c3, c4, c5, c6, c7
+ -> Sort
+ Output: c1, c2, c3, c4, c5, c6, c7
+ Sort Key: t1.c3, t1.c1
+ -> Foreign Scan on public.ft1 t1
+ Output: c1, c2, c3, c4, c5, c6, c7
+ Remote SQL: DECLARE pgsql_fdw_cursor_2 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (8 rows)
+
+ SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ -----+----+-------+------------------------------+--------------------------+----+------------
+ 101 | 1 | 00101 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970 | 1 | 1
+ 102 | 2 | 00102 | Sat Jan 03 00:00:00 1970 PST | Sat Jan 03 00:00:00 1970 | 2 | 2
+ 103 | 3 | 00103 | Sun Jan 04 00:00:00 1970 PST | Sun Jan 04 00:00:00 1970 | 3 | 3
+ 104 | 4 | 00104 | Mon Jan 05 00:00:00 1970 PST | Mon Jan 05 00:00:00 1970 | 4 | 4
+ 105 | 5 | 00105 | Tue Jan 06 00:00:00 1970 PST | Tue Jan 06 00:00:00 1970 | 5 | 5
+ 106 | 6 | 00106 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6
+ 107 | 7 | 00107 | Thu Jan 08 00:00:00 1970 PST | Thu Jan 08 00:00:00 1970 | 7 | 7
+ 108 | 8 | 00108 | Fri Jan 09 00:00:00 1970 PST | Fri Jan 09 00:00:00 1970 | 8 | 8
+ 109 | 9 | 00109 | Sat Jan 10 00:00:00 1970 PST | Sat Jan 10 00:00:00 1970 | 9 | 9
+ 110 | 0 | 00110 | Sun Jan 11 00:00:00 1970 PST | Sun Jan 11 00:00:00 1970 | 0 | 0
+ (10 rows)
+
+ -- with WHERE clause
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
+ QUERY PLAN
+ ------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on public.ft1 t1
+ Output: c1, c2, c3, c4, c5, c6, c7
+ Filter: ((t1.c1 = 101) AND ((t1.c6)::text = '1'::text) AND (t1.c7 = '1'::bpchar))
+ Remote SQL: DECLARE pgsql_fdw_cursor_4 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (4 rows)
+
+ SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ -----+----+-------+------------------------------+--------------------------+----+------------
+ 101 | 1 | 00101 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970 | 1 | 1
+ (1 row)
+
+ -- aggregate
+ SELECT COUNT(*) FROM ft1 t1;
+ count
+ -------
+ 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
+ ----+----+-------+------------------------------+--------------------------+----+------------
+ 1 | 1 | 00001 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970 | 1 | 1
+ 2 | 2 | 00002 | Sat Jan 03 00:00:00 1970 PST | Sat Jan 03 00:00:00 1970 | 2 | 2
+ 3 | 3 | 00003 | Sun Jan 04 00:00:00 1970 PST | Sun Jan 04 00:00:00 1970 | 3 | 3
+ 4 | 4 | 00004 | Mon Jan 05 00:00:00 1970 PST | Mon Jan 05 00:00:00 1970 | 4 | 4
+ 5 | 5 | 00005 | Tue Jan 06 00:00:00 1970 PST | Tue Jan 06 00:00:00 1970 | 5 | 5
+ 6 | 6 | 00006 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6
+ 7 | 7 | 00007 | Thu Jan 08 00:00:00 1970 PST | Thu Jan 08 00:00:00 1970 | 7 | 7
+ 8 | 8 | 00008 | Fri Jan 09 00:00:00 1970 PST | Fri Jan 09 00:00:00 1970 | 8 | 8
+ 9 | 9 | 00009 | Sat Jan 10 00:00:00 1970 PST | Sat Jan 10 00:00:00 1970 | 9 | 9
+ 10 | 0 | 00010 | Sun Jan 11 00:00:00 1970 PST | Sun Jan 11 00:00:00 1970 | 0 | 0
+ (10 rows)
+
+ -- subquery+MAX
+ SELECT * FROM ft1 t1 WHERE t1.c3 = (SELECT MAX(c3) FROM ft2 t2) ORDER BY c1;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ ------+----+-------+------------------------------+--------------------------+----+------------
+ 1000 | 0 | 01000 | Thu Jan 01 00:00:00 1970 PST | Thu Jan 01 00:00:00 1970 | 0 | 0
+ (1 row)
+
+ -- used in CTE
+ WITH t1 AS (SELECT * FROM ft1 WHERE c1 <= 10) SELECT t2.c1, t2.c2, t2.c3, t2.c4 FROM t1, ft2 t2 WHERE t1.c1 = t2.c1 ORDER BY t1.c1;
+ c1 | c2 | c3 | c4
+ ----+----+-------+------------------------------
+ 1 | 1 | 00001 | Fri Jan 02 00:00:00 1970 PST
+ 2 | 2 | 00002 | Sat Jan 03 00:00:00 1970 PST
+ 3 | 3 | 00003 | Sun Jan 04 00:00:00 1970 PST
+ 4 | 4 | 00004 | Mon Jan 05 00:00:00 1970 PST
+ 5 | 5 | 00005 | Tue Jan 06 00:00:00 1970 PST
+ 6 | 6 | 00006 | Wed Jan 07 00:00:00 1970 PST
+ 7 | 7 | 00007 | Thu Jan 08 00:00:00 1970 PST
+ 8 | 8 | 00008 | Fri Jan 09 00:00:00 1970 PST
+ 9 | 9 | 00009 | Sat Jan 10 00:00:00 1970 PST
+ 10 | 0 | 00010 | Sun Jan 11 00:00:00 1970 PST
+ (10 rows)
+
+ -- fixed values
+ SELECT 'fixed', NULL FROM ft1 t1 WHERE c1 = 1;
+ ?column? | ?column?
+ ----------+----------
+ fixed |
+ (1 row)
+
+ -- user-defined operator/function
+ CREATE FUNCTION pgsql_fdw_abs(int) RETURNS int AS $$
+ BEGIN
+ RETURN abs($1);
+ END
+ $$ LANGUAGE plpgsql IMMUTABLE;
+ CREATE OPERATOR === (
+ LEFTARG = int,
+ RIGHTARG = int,
+ PROCEDURE = int4eq,
+ COMMUTATOR = ===,
+ NEGATOR = !==
+ );
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = pgsql_fdw_abs(t1.c2);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = pgsql_fdw_abs(c2))
+ Remote SQL: DECLARE pgsql_fdw_cursor_18 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 === t1.c2;
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 === c2)
+ Remote SQL: DECLARE pgsql_fdw_cursor_19 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = abs(t1.c2);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = abs(c2))
+ Remote SQL: DECLARE pgsql_fdw_cursor_20 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = t1.c2;
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = c2)
+ Remote SQL: DECLARE pgsql_fdw_cursor_21 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ DROP OPERATOR === (int, int) CASCADE;
+ DROP FUNCTION pgsql_fdw_abs(int);
+ -- ===================================================================
+ -- parameterized queries
+ -- ===================================================================
+ -- simple join
+ PREPARE st1(int, int) AS SELECT t1.c3, t2.c3 FROM ft1 t1, ft2 t2 WHERE t1.c1 = $1 AND t2.c1 = $2;
+ EXPLAIN (COSTS false) EXECUTE st1(1, 2);
+ QUERY PLAN
+ -----------------------------------------------------------------------------------------------------------------------------------------
+ Nested Loop
+ -> Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_22 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, NULL, NULL, NULL, NULL FROM "S 1"."T 1"
+ -> Materialize
+ -> Foreign Scan on ft2 t2
+ Filter: (c1 = 2)
+ Remote SQL: DECLARE pgsql_fdw_cursor_23 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, NULL, NULL, NULL, NULL FROM "S 1"."T 1"
+ (8 rows)
+
+ EXECUTE st1(1, 1);
+ c3 | c3
+ -------+-------
+ 00001 | 00001
+ (1 row)
+
+ EXECUTE st1(101, 101);
+ c3 | c3
+ -------+-------
+ 00101 | 00101
+ (1 row)
+
+ -- subquery using stable function (can't be pushed down)
+ PREPARE st2(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c4) = 6) ORDER BY c1;
+ EXPLAIN (COSTS false) EXECUTE st2(10, 20);
+ QUERY PLAN
+ ---------------------------------------------------------------------------------------------------------------------------------------------
+ Sort
+ Sort Key: t1.c1
+ -> Nested Loop
+ Join Filter: (t1.c3 = t2.c3)
+ -> HashAggregate
+ -> Foreign Scan on ft2 t2
+ Filter: ((c1 > 10) AND (date_part('dow'::text, c4) = 6::double precision))
+ Remote SQL: DECLARE pgsql_fdw_cursor_29 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, c4, NULL, NULL, NULL FROM "S 1"."T 1"
+ -> Foreign Scan on ft1 t1
+ Filter: (c1 < 20)
+ Remote SQL: DECLARE pgsql_fdw_cursor_28 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (11 rows)
+
+ EXECUTE st2(10, 20);
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ ----+----+-------+------------------------------+--------------------------+----+------------
+ 16 | 6 | 00016 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970 | 6 | 6
+ (1 row)
+
+ EXECUTE st1(101, 101);
+ c3 | c3
+ -------+-------
+ 00101 | 00101
+ (1 row)
+
+ -- subquery using immutable function (can be pushed down)
+ PREPARE st3(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c5) = 6) ORDER BY c1;
+ EXPLAIN (COSTS false) EXECUTE st3(10, 20);
+ QUERY PLAN
+ ---------------------------------------------------------------------------------------------------------------------------------------------
+ Sort
+ Sort Key: t1.c1
+ -> Nested Loop
+ Join Filter: (t1.c3 = t2.c3)
+ -> HashAggregate
+ -> Foreign Scan on ft2 t2
+ Filter: ((c1 > 10) AND (date_part('dow'::text, c5) = 6::double precision))
+ Remote SQL: DECLARE pgsql_fdw_cursor_35 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, NULL, c5, NULL, NULL FROM "S 1"."T 1"
+ -> Foreign Scan on ft1 t1
+ Filter: (c1 < 20)
+ Remote SQL: DECLARE pgsql_fdw_cursor_34 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (11 rows)
+
+ EXECUTE st3(10, 20);
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ ----+----+-------+------------------------------+--------------------------+----+------------
+ 16 | 6 | 00016 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970 | 6 | 6
+ (1 row)
+
+ EXECUTE st3(20, 30);
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ ----+----+-------+------------------------------+--------------------------+----+------------
+ 23 | 3 | 00023 | Sat Jan 24 00:00:00 1970 PST | Sat Jan 24 00:00:00 1970 | 3 | 3
+ (1 row)
+
+ -- custom plan should be chosen
+ PREPARE st4(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 = $1;
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_40 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_41 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_42 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_43 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_44 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_46 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ -- cleanup
+ DEALLOCATE st1;
+ DEALLOCATE st2;
+ DEALLOCATE st3;
+ DEALLOCATE st4;
+ -- ===================================================================
+ -- connection management
+ -- ===================================================================
+ SELECT srvname, usename FROM pgsql_fdw_connections;
+ srvname | usename
+ -----------+----------
+ loopback2 | postgres
+ (1 row)
+
+ SELECT pgsql_fdw_disconnect(srvid, usesysid) FROM pgsql_fdw_get_connections();
+ pgsql_fdw_disconnect
+ ----------------------
+ OK
+ (1 row)
+
+ SELECT srvname, usename FROM pgsql_fdw_connections;
+ srvname | usename
+ ---------+---------
+ (0 rows)
+
+ -- ===================================================================
+ -- cleanup
+ -- ===================================================================
+ DROP EXTENSION pgsql_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 server loopback2
+ drop cascades to user mapping for public
+ drop cascades to foreign table ft1
+ drop cascades to foreign table ft2
diff --git a/contrib/pgsql_fdw/option.c b/contrib/pgsql_fdw/option.c
index ...e123cba .
*** a/contrib/pgsql_fdw/option.c
--- b/contrib/pgsql_fdw/option.c
***************
*** 0 ****
--- 1,246 ----
+ /*-------------------------------------------------------------------------
+ *
+ * option.c
+ * FDW option handling
+ *
+ * Copyright (c) 2011, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/option.c
+ *
+ *-------------------------------------------------------------------------
+ */
+ #include "postgres.h"
+
+ #include "access/reloptions.h"
+ #include "catalog/pg_foreign_data_wrapper.h"
+ #include "catalog/pg_foreign_server.h"
+ #include "catalog/pg_foreign_table.h"
+ #include "catalog/pg_user_mapping.h"
+ #include "fmgr.h"
+ #include "foreign/foreign.h"
+ #include "lib/stringinfo.h"
+ #include "miscadmin.h"
+
+ #include "pgsql_fdw.h"
+
+ /*
+ * SQL functions
+ */
+ extern Datum pgsql_fdw_validator(PG_FUNCTION_ARGS);
+ PG_FUNCTION_INFO_V1(pgsql_fdw_validator);
+
+ /*
+ * Describes the valid options for objects that use this wrapper.
+ */
+ typedef struct PgsqlFdwOption
+ {
+ const char *optname;
+ Oid optcontext; /* Oid of catalog in which options may appear */
+ bool is_libpq_opt; /* true if it's used in libpq */
+ } PgsqlFdwOption;
+
+ /*
+ * Valid options for pgsql_fdw.
+ */
+ static PgsqlFdwOption valid_options[] = {
+
+ /*
+ * Options for libpq connection.
+ * Note: This list should be updated along with PQconninfoOptions in
+ * interfaces/libpq/fe-connect.c, so the order is kept as is.
+ *
+ * Some useless libpq connection options are not accepted by pgsql_fdw:
+ * client_encoding: set to local database encoding automatically
+ * fallback_application_name: fixed to "pgsql_fdw"
+ * replication: pgsql_fdw never be replication client
+ */
+ {"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},
+ #ifdef NOT_USED
+ {"client_encoding", ForeignServerRelationId, true},
+ #endif
+ {"tty", ForeignServerRelationId, true},
+ {"options", ForeignServerRelationId, true},
+ {"application_name", ForeignServerRelationId, true},
+ #ifdef NOT_USED
+ {"fallback_application_name", ForeignServerRelationId, true},
+ #endif
+ {"keepalives", ForeignServerRelationId, true},
+ {"keepalives_idle", ForeignServerRelationId, true},
+ {"keepalives_interval", ForeignServerRelationId, true},
+ {"keepalives_count", ForeignServerRelationId, true},
+ #ifdef USE_SSL
+ {"requiressl", ForeignServerRelationId, true},
+ #endif
+ {"sslmode", ForeignServerRelationId, true},
+ {"sslcert", ForeignServerRelationId, true},
+ {"sslkey", ForeignServerRelationId, true},
+ {"sslrootcert", ForeignServerRelationId, true},
+ {"sslcrl", ForeignServerRelationId, true},
+ {"requirepeer", ForeignServerRelationId, true},
+ #if defined(KRB5) || defined(ENABLE_GSS) || defined(ENABLE_SSPI)
+ {"krbsrvname", ForeignServerRelationId, true},
+ #endif
+ #if defined(ENABLE_GSS) && defined(ENABLE_SSPI)
+ {"gsslib", ForeignServerRelationId, true},
+ #endif
+ #ifdef NOT_USED
+ {"replication", ForeignServerRelationId, true},
+ #endif
+
+ /*
+ * Options for translation of object names.
+ */
+ {"nspname", ForeignTableRelationId, false},
+ {"relname", ForeignTableRelationId, false},
+ {"colname", AttributeRelationId, false},
+
+ /*
+ * Options for cursor behavior.
+ * These options can be overridden by finer-grained objects.
+ */
+ {"fetch_count", ForeignTableRelationId, false},
+ {"fetch_count", ForeignServerRelationId, false},
+
+ /* Terminating entry --- MUST BE LAST */
+ {NULL, InvalidOid, false}
+ };
+
+ /*
+ * Helper functions
+ */
+ static bool is_valid_option(const char *optname, Oid context);
+
+ /*
+ * Validate the generic options given to a FOREIGN DATA WRAPPER, SERVER,
+ * USER MAPPING or FOREIGN TABLE that uses pgsql_fdw.
+ *
+ * Raise an ERROR if the option or its value is considered invalid.
+ */
+ Datum
+ pgsql_fdw_validator(PG_FUNCTION_ARGS)
+ {
+ List *options_list = untransformRelOptions(PG_GETARG_DATUM(0));
+ Oid catalog = PG_GETARG_OID(1);
+ ListCell *cell;
+
+ /*
+ * Check that only options supported by pgsql_fdw, and allowed for the
+ * current object type, are given.
+ */
+ foreach(cell, options_list)
+ {
+ DefElem *def = (DefElem *) lfirst(cell);
+
+ if (!is_valid_option(def->defname, catalog))
+ {
+ PgsqlFdwOption *opt;
+ StringInfoData buf;
+
+ /*
+ * Unknown option specified, complain about it. Provide a hint
+ * with list of valid options for the object.
+ */
+ initStringInfo(&buf);
+ for (opt = valid_options; opt->optname; opt++)
+ {
+ if (catalog == opt->optcontext)
+ appendStringInfo(&buf, "%s%s", (buf.len > 0) ? ", " : "",
+ opt->optname);
+ }
+
+ ereport(ERROR,
+ (errcode(ERRCODE_FDW_INVALID_OPTION_NAME),
+ errmsg("invalid option \"%s\"", def->defname),
+ errhint("Valid options in this context are: %s",
+ buf.data)));
+ }
+
+ /* fetch_count be positive digit number. */
+ if (strcmp(def->defname, "fetch_count") == 0)
+ {
+ long value;
+ char *p = NULL;
+
+ value = strtol(strVal(def->arg), &p, 10);
+ if (*p != '\0' || value < 1)
+ ereport(ERROR,
+ (errcode(ERRCODE_FDW_INVALID_ATTRIBUTE_VALUE),
+ errmsg("invalid value for %s: \"%s\"",
+ def->defname, strVal(def->arg))));
+ }
+ }
+
+ /*
+ * We don't care option-specific limitation here; they will be validated at
+ * the execution time.
+ */
+
+ PG_RETURN_VOID();
+ }
+
+ /*
+ * Check whether the given option is one of the valid pgsql_fdw options.
+ * context is the Oid of the catalog holding the object the option is for.
+ */
+ static bool
+ is_valid_option(const char *optname, Oid context)
+ {
+ PgsqlFdwOption *opt;
+
+ for (opt = valid_options; opt->optname; opt++)
+ {
+ if (context == opt->optcontext && strcmp(opt->optname, optname) == 0)
+ return true;
+ }
+ return false;
+ }
+
+ /*
+ * Check whether the given option is one of the valid libpq options.
+ * context is the Oid of the catalog holding the object the option is for.
+ */
+ static bool
+ is_libpq_option(const char *optname)
+ {
+ PgsqlFdwOption *opt;
+
+ for (opt = valid_options; opt->optname; opt++)
+ {
+ if (strcmp(opt->optname, optname) == 0 && opt->is_libpq_opt)
+ return true;
+ }
+ return false;
+ }
+
+ /*
+ * Generate key-value arrays which includes only libpq options from the list
+ * which contains any kind of options.
+ */
+ int
+ ExtractConnectionOptions(List *defelems, const char **keywords, const char **values)
+ {
+ ListCell *lc;
+ int i;
+
+ i = 0;
+ foreach(lc, defelems)
+ {
+ DefElem *d = (DefElem *) lfirst(lc);
+ if (is_libpq_option(d->defname))
+ {
+ keywords[i] = d->defname;
+ values[i] = strVal(d->arg);
+ i++;
+ }
+ }
+ return i;
+ }
diff --git a/contrib/pgsql_fdw/pgsql_fdw--1.0.sql b/contrib/pgsql_fdw/pgsql_fdw--1.0.sql
index ...b0ea2b2 .
*** a/contrib/pgsql_fdw/pgsql_fdw--1.0.sql
--- b/contrib/pgsql_fdw/pgsql_fdw--1.0.sql
***************
*** 0 ****
--- 1,39 ----
+ /* contrib/pgsql_fdw/pgsql_fdw--1.0.sql */
+
+ -- complain if script is sourced in psql, rather than via CREATE EXTENSION
+ \echo Use "CREATE EXTENSION pgsql_fdw" to load this file. \quit
+
+ CREATE FUNCTION pgsql_fdw_handler()
+ RETURNS fdw_handler
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C STRICT;
+
+ CREATE FUNCTION pgsql_fdw_validator(text[], oid)
+ RETURNS void
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C STRICT;
+
+ CREATE FOREIGN DATA WRAPPER pgsql_fdw
+ HANDLER pgsql_fdw_handler
+ VALIDATOR pgsql_fdw_validator;
+
+ /* connection management functions and view */
+ CREATE FUNCTION pgsql_fdw_get_connections(out srvid oid, out usesysid oid)
+ RETURNS SETOF record
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C STRICT;
+
+ CREATE FUNCTION pgsql_fdw_disconnect(oid, oid)
+ RETURNS text
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C STRICT;
+
+ CREATE VIEW pgsql_fdw_connections AS
+ SELECT c.srvid srvid,
+ s.srvname srvname,
+ c.usesysid usesysid,
+ pg_get_userbyid(c.usesysid) usename
+ FROM pgsql_fdw_get_connections() c
+ JOIN pg_catalog.pg_foreign_server s ON (s.oid = c.srvid);
+ GRANT SELECT ON pgsql_fdw_connections TO public;
+
diff --git a/contrib/pgsql_fdw/pgsql_fdw.c b/contrib/pgsql_fdw/pgsql_fdw.c
index ...54ffae9 .
*** a/contrib/pgsql_fdw/pgsql_fdw.c
--- b/contrib/pgsql_fdw/pgsql_fdw.c
***************
*** 0 ****
--- 1,850 ----
+ /*-------------------------------------------------------------------------
+ *
+ * pgsql_fdw.c
+ * foreign-data wrapper for remote PostgreSQL servers.
+ *
+ * Copyright (c) 2011, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/pgsql_fdw.c
+ *
+ *-------------------------------------------------------------------------
+ */
+ #include "postgres.h"
+ #include "fmgr.h"
+
+ #include "catalog/pg_foreign_server.h"
+ #include "catalog/pg_foreign_table.h"
+ #include "commands/explain.h"
+ #include "foreign/fdwapi.h"
+ #include "funcapi.h"
+ #include "miscadmin.h"
+ #include "nodes/nodeFuncs.h"
+ #include "optimizer/cost.h"
+ #include "optimizer/pathnode.h"
+ #include "utils/lsyscache.h"
+ #include "utils/memutils.h"
+ #include "utils/rel.h"
+
+ #include "pgsql_fdw.h"
+ #include "connection.h"
+
+ PG_MODULE_MAGIC;
+
+ /*
+ * Default fetch count for cursor. This can be overridden by fetch_count FDW
+ * option.
+ */
+ #define DEFAULT_FETCH_COUNT 10000
+
+ /*
+ * Cost to establish a connection.
+ * XXX: should be configurable per server?
+ */
+ #define CONNECTION_COSTS 100.0
+
+ /*
+ * Cost to transfer 1 byte from remote server.
+ * XXX: should be configurable per server?
+ */
+ #define TRANSFER_COSTS_PER_BYTE 0.001
+
+ /*
+ * Cursors which are used together in a local query require different name, so
+ * we use simple incremental name for that purpose. We don't care wrap around
+ * of cursor_id because it's hard to imagine that 2^32 cursors are used in a
+ * query.
+ */
+ #define CURSOR_NAME_FORMAT "pgsql_fdw_cursor_%u"
+ static uint32 cursor_id = 0;
+
+ /*
+ * Index of FDW-private items stored in FdwPlan.
+ */
+ enum FdwPrivateIndex {
+ FdwPrivateSelectSql,
+ FdwPrivateDeclareSql,
+ FdwPrivateFetchSql,
+ FdwPrivateResetSql,
+ FdwPrivateCloseSql,
+
+ /* # of elements stored in the list fdw_private */
+ FdwPrivateNum,
+ };
+
+ /*
+ * Describes an execution state of a foreign scan against a foreign table
+ * using pgsql_fdw.
+ */
+ typedef struct PgsqlFdwExecutionState
+ {
+ FdwPlan *fdwplan; /* FDW-specific planning information */
+ PGconn *conn; /* connection for the scan */
+
+ Oid *param_types; /* type array of external parameter */
+ const char **param_values; /* value array of external parameter */
+
+ int attnum; /* # of non-dropped attribute */
+ char **col_values; /* column value buffer */
+ AttInMetadata *attinmeta; /* attribute metadata */
+
+ Tuplestorestate *tuples; /* result of the scan */
+ bool cursor_opened; /* true if cursor has been opened */
+ } PgsqlFdwExecutionState;
+
+ /*
+ * SQL functions
+ */
+ extern Datum pgsql_fdw_handler(PG_FUNCTION_ARGS);
+ PG_FUNCTION_INFO_V1(pgsql_fdw_handler);
+
+ /*
+ * FDW callback routines
+ */
+ static FdwPlan *pgsqlPlanForeignScan(Oid foreigntableid,
+ PlannerInfo *root,
+ RelOptInfo *baserel);
+ static void pgsqlExplainForeignScan(ForeignScanState *node, ExplainState *es);
+ static void pgsqlBeginForeignScan(ForeignScanState *node, int eflags);
+ static TupleTableSlot *pgsqlIterateForeignScan(ForeignScanState *node);
+ static void pgsqlReScanForeignScan(ForeignScanState *node);
+ static void pgsqlEndForeignScan(ForeignScanState *node);
+
+ /*
+ * Helper functions
+ */
+ static void estimate_costs(PlannerInfo *root,
+ RelOptInfo *baserel,
+ const char *sql,
+ Oid serverid,
+ Cost *startup_cost,
+ Cost *total_cost);
+ static void execute_query(ForeignScanState *node);
+ static PGresult *fetch_result(ForeignScanState *node);
+ static void store_result(ForeignScanState *node, PGresult *res);
+ static bool contain_ext_param(Node *clause);
+ static bool contain_ext_param_walker(Node *node, void *context);
+
+ /*
+ * Foreign-data wrapper handler function: return a struct with pointers
+ * to my callback routines.
+ */
+ Datum
+ pgsql_fdw_handler(PG_FUNCTION_ARGS)
+ {
+ FdwRoutine *fdwroutine = makeNode(FdwRoutine);
+
+ fdwroutine->PlanForeignScan = pgsqlPlanForeignScan;
+ fdwroutine->ExplainForeignScan = pgsqlExplainForeignScan;
+ fdwroutine->BeginForeignScan = pgsqlBeginForeignScan;
+ fdwroutine->IterateForeignScan = pgsqlIterateForeignScan;
+ fdwroutine->ReScanForeignScan = pgsqlReScanForeignScan;
+ fdwroutine->EndForeignScan = pgsqlEndForeignScan;
+
+ PG_RETURN_POINTER(fdwroutine);
+ }
+
+ /*
+ * pgsqlPlanForeignScan
+ * Create a FdwPlan for a scan on the foreign table
+ */
+ static FdwPlan *
+ pgsqlPlanForeignScan(Oid foreigntableid,
+ PlannerInfo *root,
+ RelOptInfo *baserel)
+ {
+ char name[128]; /* must be larger than format + 10 */
+ StringInfoData cursor;
+ const char *fetch_count_str;
+ int fetch_count = DEFAULT_FETCH_COUNT;
+ char *sql;
+ FdwPlan *fdwplan;
+ List *fdw_private = NIL;
+ ForeignTable *table;
+ ForeignServer *server;
+
+ /* Construct FdwPlan with cost estimates */
+ fdwplan = makeNode(FdwPlan);
+ sql = deparseSql(foreigntableid, root, baserel);
+ table = GetForeignTable(foreigntableid);
+ server = GetForeignServer(table->serverid);
+ estimate_costs(root, baserel, sql, server->serverid,
+ &fdwplan->startup_cost, &fdwplan->total_cost);
+
+ /*
+ * Store plain SELECT statement in private area of FdwPlan. This will be
+ * used for executing remote query and explaining scan.
+ */
+ fdw_private = list_make1(makeString(sql));
+
+ /* Use specified fetch_count instead of default value, if any. */
+ fetch_count_str = GetFdwOptionValue(InvalidOid, InvalidOid, foreigntableid,
+ InvalidAttrNumber, "fetch_count");
+ if (fetch_count_str != NULL)
+ fetch_count = strtol(fetch_count_str, NULL, 10);
+ elog(DEBUG1, "relid=%u fetch_count=%d", foreigntableid, fetch_count);
+
+ /*
+ * We store some more information in FdwPlan to pass them beyond the
+ * boundary between planner and executor. Finally FdwPlan using cursor
+ * would hold items below:
+ *
+ * 1) plain SELECT statement (already added above)
+ * 2) SQL statement used to declare cursor
+ * 3) SQL statement used to fetch rows from cursor
+ * 4) SQL statement used to reset cursor
+ * 5) SQL statement used to close cursor
+ *
+ * These items are indexed with the enum FdwPrivateIndex, so an item
+ * can be accessed directly via list_nth(). For example of FETCH
+ * statement:
+ * list_nth(fdw_private, FdwPrivateFetchSql)
+ */
+
+ /* Construct cursor name from sequential value */
+ sprintf(name, CURSOR_NAME_FORMAT, cursor_id++);
+
+ /* Construct statement to declare cursor */
+ initStringInfo(&cursor);
+ appendStringInfo(&cursor, "DECLARE %s SCROLL CURSOR FOR %s", name, sql);
+ fdw_private = lappend(fdw_private, makeString(cursor.data));
+
+ /* Construct statement to fetch rows from cursor */
+ initStringInfo(&cursor);
+ appendStringInfo(&cursor, "FETCH %d FROM %s", fetch_count, name);
+ fdw_private = lappend(fdw_private, makeString(cursor.data));
+
+ /* Construct statement to reset cursor */
+ initStringInfo(&cursor);
+ appendStringInfo(&cursor, "MOVE ABSOLUTE 0 FROM %s", name);
+ fdw_private = lappend(fdw_private, makeString(cursor.data));
+
+ /* Construct statement to close cursor */
+ initStringInfo(&cursor);
+ appendStringInfo(&cursor, "CLOSE %s", name);
+ fdw_private = lappend(fdw_private, makeString(cursor.data));
+
+ /* Store FDW private information into FdwPlan */
+ fdwplan->fdw_private = fdw_private;
+
+ return fdwplan;
+ }
+
+ /*
+ * pgsqlExplainForeignScan
+ * Produce extra output for EXPLAIN
+ */
+ static void
+ pgsqlExplainForeignScan(ForeignScanState *node, ExplainState *es)
+ {
+ FdwPlan *fdwplan;
+ char *sql;
+
+ fdwplan = ((ForeignScan *) node->ss.ps.plan)->fdwplan;
+ sql = strVal(list_nth(fdwplan->fdw_private, FdwPrivateDeclareSql));
+ ExplainPropertyText("Remote SQL", sql, es);
+ }
+
+ /*
+ * pgsqlBeginForeignScan
+ * Initiate access to a foreign PostgreSQL table.
+ */
+ static void
+ pgsqlBeginForeignScan(ForeignScanState *node, int eflags)
+ {
+ PgsqlFdwExecutionState *festate;
+ PGconn *conn;
+ Oid relid;
+ ForeignTable *table;
+ ForeignServer *server;
+ UserMapping *user;
+ TupleTableSlot *slot = node->ss.ss_ScanTupleSlot;
+
+ /*
+ * Do nothing in EXPLAIN (no ANALYZE) case. node->fdw_state stays NULL.
+ */
+ if (eflags & EXEC_FLAG_EXPLAIN_ONLY)
+ return;
+
+ /*
+ * Save state in node->fdw_state.
+ */
+ festate = (PgsqlFdwExecutionState *) palloc(sizeof(PgsqlFdwExecutionState));
+ festate->fdwplan = ((ForeignScan *) node->ss.ps.plan)->fdwplan;
+
+ /*
+ * Get connection to the foreign server. Connection manager would
+ * establish new connection if necessary.
+ */
+ relid = RelationGetRelid(node->ss.ss_currentRelation);
+ table = GetForeignTable(relid);
+ server = GetForeignServer(table->serverid);
+ user = GetUserMapping(GetOuterUserId(), server->serverid);
+ conn = GetConnection(server, user);
+ festate->conn = conn;
+
+ /* Result will be filled in first Iterate call. */
+ festate->tuples = NULL;
+ festate->cursor_opened = false;
+
+ /* Allocate buffers for column values. */
+ {
+ TupleDesc tupdesc = slot->tts_tupleDescriptor;
+ festate->col_values = palloc(sizeof(char *) * tupdesc->natts);
+ festate->attinmeta = TupleDescGetAttInMetadata(tupdesc);
+ }
+
+ /* Allocate buffers for query parameters. */
+ {
+ ParamListInfo params = node->ss.ps.state->es_param_list_info;
+ int numParams = params ? params->numParams : 0;
+
+ if (numParams > 0)
+ {
+ festate->param_types = palloc0(sizeof(Oid) * numParams);
+ festate->param_values = palloc0(sizeof(char *) * numParams);
+ }
+ else
+ {
+ festate->param_types = NULL;
+ festate->param_values = NULL;
+ }
+ }
+
+
+ /* Store FDW-specific state into ForeignScanState */
+ node->fdw_state = (void *) festate;
+
+ return;
+ }
+
+ /*
+ * pgsqlIterateForeignScan
+ * Retrieve next row from the result set, or clear tuple slot to indicate
+ * EOF.
+ *
+ * Note that using per-query context when retrieving tuples from
+ * tuplestore to ensure that returned tuples can survive until next
+ * iteration because the tuple is released implicitly via ExecClearTuple.
+ * Retrieving a tuple from tuplestore in CurrentMemoryContext (it's a
+ * per-tuple context), ExecClearTuple will free dangling pointer.
+ */
+ static TupleTableSlot *
+ pgsqlIterateForeignScan(ForeignScanState *node)
+ {
+ PgsqlFdwExecutionState *festate;
+ TupleTableSlot *slot = node->ss.ss_ScanTupleSlot;
+ PGresult *res;
+ MemoryContext oldcontext = CurrentMemoryContext;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+
+
+ /*
+ * If this is the first call after Begin, we need to execute remote query.
+ * If the query needs cursor, we declare a cursor at first call and fetch
+ * from it in later calls.
+ */
+ if (festate->tuples == NULL && !festate->cursor_opened)
+ execute_query(node);
+
+ /*
+ * If enough tuples are left in tuplestore, just return next tuple from it.
+ */
+ MemoryContextSwitchTo(node->ss.ps.state->es_query_cxt);
+ if (tuplestore_gettupleslot(festate->tuples, true, false, slot))
+ {
+ MemoryContextSwitchTo(oldcontext);
+ return slot;
+ }
+ MemoryContextSwitchTo(oldcontext);
+
+ /*
+ * Here we need to clear partial result and fetch next bunch of tuples from
+ * from the cursor for the scan. If the fetch returns no tuple, the scan
+ * has reached the end.
+ */
+ res = fetch_result(node);
+ PG_TRY();
+ {
+ store_result(node, res);
+ PQclear(res);
+ res = NULL;
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+
+ /*
+ * If we got more tuples from the server cursor, return next tuple from
+ * tuplestore.
+ */
+ MemoryContextSwitchTo(node->ss.ps.state->es_query_cxt);
+ if (tuplestore_gettupleslot(festate->tuples, true, false, slot))
+ {
+ MemoryContextSwitchTo(oldcontext);
+ return slot;
+ }
+ MemoryContextSwitchTo(oldcontext);
+
+ /* We don't have any result even in remote server cursor. */
+ ExecClearTuple(slot);
+ return slot;
+ }
+
+ /*
+ * pgsqlReScanForeignScan
+ * - Restart this scan by resetting fetch location.
+ */
+ static void
+ pgsqlReScanForeignScan(ForeignScanState *node)
+ {
+ List *fdw_private;
+ char *sql;
+ PGconn *conn;
+ PGresult *res;
+ PgsqlFdwExecutionState *festate;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+
+ /* If we have not opened cursor yet, nothing to do. */
+ if (!festate->cursor_opened)
+ return;
+
+ /* Discard fetch results if any. */
+ if (festate->tuples != NULL)
+ tuplestore_clear(festate->tuples);
+
+ /* Reset cursor */
+ fdw_private = festate->fdwplan->fdw_private;
+ conn = festate->conn;
+ sql = strVal(list_nth(fdw_private, FdwPrivateResetSql));
+ res = PQexec(conn, sql);
+ PG_TRY();
+ {
+ if (PQresultStatus(res) != PGRES_COMMAND_OK)
+ {
+ ereport(ERROR,
+ (errmsg("could not rewind cursor"),
+ errdetail("%s", PQerrorMessage(conn)),
+ errhint("%s", sql)));
+ }
+ PQclear(res);
+ res = NULL;
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+ }
+
+ /*
+ * pgsqlEndForeignScan
+ * Finish scanning foreign table and dispose objects used for this scan
+ */
+ static void
+ pgsqlEndForeignScan(ForeignScanState *node)
+ {
+ List *fdw_private;
+ char *sql;
+ PGconn *conn;
+ PGresult *res;
+ PgsqlFdwExecutionState *festate;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+
+ /* if festate is NULL, we are in EXPLAIN; nothing to do */
+ if (festate == NULL)
+ return;
+
+ /* If we have not opened cursor yet, nothing to do. */
+ if (!festate->cursor_opened)
+ return;
+
+ /* Discard fetch results */
+ if (festate->tuples != NULL)
+ {
+ tuplestore_end(festate->tuples);
+ festate->tuples = NULL;
+ }
+
+ /* Close cursor */
+ fdw_private = festate->fdwplan->fdw_private;
+ conn = festate->conn;
+ sql = strVal(list_nth(fdw_private, FdwPrivateCloseSql));
+ res = PQexec(conn, sql);
+ PG_TRY();
+ {
+ if (PQresultStatus(res) != PGRES_COMMAND_OK)
+ {
+ ereport(ERROR,
+ (errmsg("could not close cursor"),
+ errdetail("%s", PQerrorMessage(conn)),
+ errhint("%s", sql)));
+ }
+ PQclear(res);
+ res = NULL;
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+
+ ReleaseConnection(festate->conn);
+ }
+
+ /*
+ * Estimate costs of scanning a foreign table.
+ */
+ static void
+ estimate_costs(PlannerInfo *root, RelOptInfo *baserel,
+ const char *sql, Oid serverid,
+ Cost *startup_cost, Cost *total_cost)
+ {
+ ListCell *lc;
+ ForeignServer *server;
+ UserMapping *user;
+ PGconn *conn = NULL;
+ PGresult *res = NULL;
+ StringInfoData buf;
+ char *plan;
+ char *p;
+ int n;
+
+ /*
+ * If the baserestrictinfo contains any Param node with paramkind
+ * PARAM_EXTERNAL, we need result of EXPLAIN for EXECUTE statement, not for
+ * simple SELECT statement. However, we can't get actual parameter values
+ * here, so we use fixed and large costs as second best so that planner
+ * tend to choose custom plan.
+ *
+ * See comments in plancache.c for details of custom plan.
+ */
+ foreach(lc, baserel->baserestrictinfo)
+ {
+ RestrictInfo *rs = (RestrictInfo *) lfirst(lc);
+ if (contain_ext_param((Node *) rs->clause))
+ {
+ *startup_cost = CONNECTION_COSTS;
+ *total_cost = 10000; /* goundless large costs */
+
+ return;
+ }
+ }
+
+ /*
+ * Get connection to the foreign server. Connection manager would
+ * establish new connection if necessary.
+ */
+ server = GetForeignServer(serverid);
+ user = GetUserMapping(GetOuterUserId(), server->serverid);
+ conn = GetConnection(server, user);
+ initStringInfo(&buf);
+ appendStringInfo(&buf, "EXPLAIN %s", sql);
+
+ PG_TRY();
+ {
+ res = PQexec(conn, buf.data);
+ if (PQresultStatus(res) != PGRES_TUPLES_OK || PQntuples(res) == 0)
+ {
+ char *msg;
+
+ msg = pstrdup(PQerrorMessage(conn));
+ ereport(ERROR,
+ (errmsg("could not execute EXPLAIN for cost estimation"),
+ errdetail("%s", msg),
+ errhint("%s", sql)));
+ }
+ plan = pstrdup(PQgetvalue(res, 0, 0));
+ PQclear(res);
+ res = NULL;
+ ReleaseConnection(conn);
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+
+ /*
+ * Find estimation portion from top plan node. Here we search open
+ * parentheses from end of line to avoid finding unexpected parentheses.
+ */
+ p = strrchr(plan, '(');
+ if (p == NULL)
+ elog(ERROR, "wrong EXPLAIN output: %s", plan);
+ n = sscanf(p,
+ "(cost=%lf..%lf rows=%lf width=%d)",
+ startup_cost,
+ total_cost,
+ &baserel->rows,
+ &baserel->width);
+ if (n != 4)
+ elog(ERROR, "could not get estimation from EXPLAIN output");
+
+ /*
+ * TODO Selectivity of quals which are NOT pushed down should be also
+ * considered.
+ */
+
+ /* add cost to establish connection. */
+ *startup_cost += CONNECTION_COSTS;
+ *total_cost += CONNECTION_COSTS;
+
+ /* add cost to transfer result. */
+ *total_cost += TRANSFER_COSTS_PER_BYTE * baserel->width * baserel->tuples;
+ *total_cost += cpu_tuple_cost * baserel->tuples;
+ }
+
+ /*
+ * Execute remote query with current parameters.
+ */
+ static void
+ execute_query(ForeignScanState *node)
+ {
+ FdwPlan *fdwplan;
+ PgsqlFdwExecutionState *festate;
+ ParamListInfo params = node->ss.ps.state->es_param_list_info;
+ int numParams = params ? params->numParams : 0;
+ Oid *types = NULL;
+ const char **values = NULL;
+ char *sql;
+ PGconn *conn;
+ PGresult *res;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+ types = festate->param_types;
+ values = festate->param_values;
+
+ /*
+ * Construct parameter array in text format. We don't release memory for
+ * the arrays explicitly, because the memory usage would not be very large,
+ * and anyway they will be released in context cleanup.
+ */
+ if (numParams > 0)
+ {
+ int i;
+
+ 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;
+
+ getTypeOutputInfo(types[i], &out_func_oid, &isvarlena);
+ fmgr_info(out_func_oid, &func);
+ values[i] = OutputFunctionCall(&func, params->params[i].value);
+ }
+ }
+ }
+
+ /*
+ * Execute remote query with parameters.
+ */
+ conn = festate->conn;
+ fdwplan = ((ForeignScan *) node->ss.ps.plan)->fdwplan;
+ sql = strVal(list_nth(fdwplan->fdw_private, FdwPrivateDeclareSql));
+ res = PQexecParams(conn, sql, numParams, types, values, NULL, NULL, 0);
+ PG_TRY();
+ {
+ /*
+ * If the query has failed, reporting details is enough here.
+ * Connection(s) which are used by this query (at least used by
+ * pgsql_fdw) will be cleaned up by the foreign connection manager.
+ */
+ if (PQresultStatus(res) != PGRES_COMMAND_OK)
+ {
+ ereport(ERROR,
+ (errmsg("could not declare cursor"),
+ errdetail("%s", PQerrorMessage(conn)),
+ errhint("%s", sql)));
+ }
+
+ /* Mark that this scan has opened a cursor. */
+ festate->cursor_opened = true;
+
+ /* Discard result of CURSOR statement and fetch first bunch. */
+ PQclear(res);
+ res = fetch_result(node);
+
+ /*
+ * Store the result of the query into tuplestore.
+ * We must release PGresult here to avoid memory leak.
+ */
+ store_result(node, res);
+ PQclear(res);
+ res = NULL;
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+ }
+
+ /*
+ * Fetch next partial result from remote server.
+ */
+ static PGresult *
+ fetch_result(ForeignScanState *node)
+ {
+ PgsqlFdwExecutionState *festate;
+ List *fdw_private;
+ char *sql;
+ PGconn *conn;
+ PGresult *res;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+
+ /* retrieve information for fetching result. */
+ fdw_private = festate->fdwplan->fdw_private;
+ sql = strVal(list_nth(fdw_private, FdwPrivateFetchSql));
+ conn = festate->conn;
+ res = PQexec(conn, sql);
+ if (PQresultStatus(res) != PGRES_TUPLES_OK)
+ {
+ ereport(ERROR,
+ (errmsg("could not fetch rows from foreign server"),
+ errdetail("%s", PQerrorMessage(conn)),
+ errhint("%s", sql)));
+ }
+
+ return res;
+ }
+
+ /*
+ * Create tuples from PGresult and store them into tuplestore.
+ */
+ static void
+ store_result(ForeignScanState *node, PGresult *res)
+ {
+ int rows;
+ int row;
+ int i;
+ int nfields;
+ int attnum; /* number of non-dropped columns */
+ Form_pg_attribute *attrs;
+ TupleTableSlot *slot = node->ss.ss_ScanTupleSlot;
+ TupleDesc tupdesc = slot->tts_tupleDescriptor;
+ PgsqlFdwExecutionState *festate;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+ rows = PQntuples(res);
+ nfields = PQnfields(res);
+ attrs = tupdesc->attrs;
+
+ /* First, ensure that the tuplestore is empty. */
+ if (festate->tuples == NULL)
+ {
+ MemoryContext oldcontext = CurrentMemoryContext;
+
+ /*
+ * Create tuplestore to store result of the query in per-query context.
+ * Note that we use this memory context to avoid memory leak in error
+ * cases.
+ */
+ MemoryContextSwitchTo(MessageContext);
+ festate->tuples = tuplestore_begin_heap(false, false, work_mem);
+ MemoryContextSwitchTo(oldcontext);
+ }
+ else
+ {
+ /* We already have tuplestore, just need to clear contents of it. */
+ tuplestore_clear(festate->tuples);
+ }
+
+
+ /* 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)));
+
+ /* put a tuples into the slot */
+ for (row = 0; row < rows; row++)
+ {
+ int j;
+ HeapTuple tuple;
+
+ for (i = 0, j = 0; i < tupdesc->natts; i++)
+ {
+ /* skip dropped columns. */
+ if (attrs[i]->attisdropped)
+ {
+ festate->col_values[i] = NULL;
+ continue;
+ }
+
+ if (PQgetisnull(res, row, j))
+ festate->col_values[i] = NULL;
+ else
+ festate->col_values[i] = PQgetvalue(res, row, 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(festate->attinmeta, festate->col_values);
+ tuplestore_puttuple(festate->tuples, tuple);
+ }
+
+ tuplestore_donestoring(festate->tuples);
+ }
+
+ /*
+ * contain_ext_param
+ * Recursively search for Param nodes within a clause.
+ *
+ * Returns true if any parameter reference node with relkind PARAM_EXTERN
+ * found.
+ *
+ * This does not descend into subqueries, and so should be used only after
+ * reduction of sublinks to subplans, or in contexts where it's known there
+ * are no subqueries. There mustn't be outer-aggregate references either.
+ *
+ * XXX: These functions could be in core, src/backend/optimizer/util/clauses.c.
+ */
+ static bool
+ contain_ext_param(Node *clause)
+ {
+ return contain_ext_param_walker(clause, NULL);
+ }
+
+ static bool
+ contain_ext_param_walker(Node *node, void *context)
+ {
+ if (node == NULL)
+ return false;
+ if (IsA(node, Param))
+ {
+ Param *param = (Param *) node;
+
+ if (param->paramkind == PARAM_EXTERN)
+ return true; /* abort the tree traversal and return true */
+ }
+ return expression_tree_walker(node, contain_ext_param_walker, context);
+ }
diff --git a/contrib/pgsql_fdw/pgsql_fdw.control b/contrib/pgsql_fdw/pgsql_fdw.control
index ...0a9c8f4 .
*** a/contrib/pgsql_fdw/pgsql_fdw.control
--- b/contrib/pgsql_fdw/pgsql_fdw.control
***************
*** 0 ****
--- 1,5 ----
+ # pgsql_fdw extension
+ comment = 'foreign-data wrapper for remote PostgreSQL servers'
+ default_version = '1.0'
+ module_pathname = '$libdir/pgsql_fdw'
+ relocatable = true
diff --git a/contrib/pgsql_fdw/pgsql_fdw.h b/contrib/pgsql_fdw/pgsql_fdw.h
index ...fb49ffb .
*** a/contrib/pgsql_fdw/pgsql_fdw.h
--- b/contrib/pgsql_fdw/pgsql_fdw.h
***************
*** 0 ****
--- 1,28 ----
+ /*-------------------------------------------------------------------------
+ *
+ * pgsql_fdw.h
+ * foreign-data wrapper for remote PostgreSQL servers.
+ *
+ * Copyright (c) 2011, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/pgsql_fdw.h
+ *
+ *-------------------------------------------------------------------------
+ */
+
+ #ifndef PGSQL_FDW_H
+ #define PGSQL_FDW_H
+
+ #include "postgres.h"
+ #include "nodes/relation.h"
+
+ /* in option.c */
+ int ExtractConnectionOptions(List *defelems,
+ const char **keywords,
+ const char **values);
+
+ /* in deparse.c */
+ char *deparseSql(Oid relid, PlannerInfo *root, RelOptInfo *baserel);
+
+ #endif /* PGSQL_FDW_H */
diff --git a/contrib/pgsql_fdw/sql/pgsql_fdw.sql b/contrib/pgsql_fdw/sql/pgsql_fdw.sql
index ...7f9f9cc .
*** a/contrib/pgsql_fdw/sql/pgsql_fdw.sql
--- b/contrib/pgsql_fdw/sql/pgsql_fdw.sql
***************
*** 0 ****
--- 1,212 ----
+ -- ===================================================================
+ -- create FDW objects
+ -- ===================================================================
+
+ CREATE EXTENSION pgsql_fdw;
+
+ CREATE SERVER loopback1 FOREIGN DATA WRAPPER pgsql_fdw;
+ CREATE SERVER loopback2 FOREIGN DATA WRAPPER pgsql_fdw
+ OPTIONS (dbname 'contrib_regression');
+
+ CREATE USER MAPPING FOR public SERVER loopback1
+ OPTIONS (user 'value', password 'value');
+ CREATE USER MAPPING FOR public SERVER loopback2;
+
+ CREATE FOREIGN TABLE ft1 (
+ c1 int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10)
+ ) SERVER loopback2;
+
+ CREATE FOREIGN TABLE ft2 (
+ c1 int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10)
+ ) SERVER loopback2;
+
+ -- ===================================================================
+ -- create objects used through FDW
+ -- ===================================================================
+ CREATE SCHEMA "S 1";
+ CREATE TABLE "S 1"."T 1" (
+ "C 1" int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10),
+ CONSTRAINT t1_pkey PRIMARY KEY ("C 1")
+ );
+ CREATE TABLE "S 1"."T 2" (
+ c1 int NOT NULL,
+ c2 text,
+ CONSTRAINT t2_pkey PRIMARY KEY (c1)
+ );
+
+ BEGIN;
+ TRUNCATE "S 1"."T 1";
+ INSERT INTO "S 1"."T 1"
+ SELECT id,
+ id % 10,
+ to_char(id, 'FM00000'),
+ '1970-01-01'::timestamptz + ((id % 100) || ' days')::interval,
+ '1970-01-01'::timestamp + ((id % 100) || ' days')::interval,
+ id % 10,
+ id % 10
+ FROM generate_series(1, 1000) id;
+ TRUNCATE "S 1"."T 2";
+ INSERT INTO "S 1"."T 2"
+ SELECT id,
+ 'AAA' || to_char(id, 'FM000')
+ FROM generate_series(1, 100) id;
+ COMMIT;
+
+ -- ===================================================================
+ -- tests for pgsql_fdw_validator
+ -- ===================================================================
+ ALTER FOREIGN DATA WRAPPER pgsql_fdw OPTIONS (host 'value'); -- ERROR
+ -- requiressl, krbsrvname and gsslib are omitted because they depend on
+ -- configure option
+ ALTER SERVER loopback1 OPTIONS (
+ authtype 'value',
+ service 'value',
+ connect_timeout 'value',
+ dbname 'value',
+ host 'value',
+ hostaddr 'value',
+ port 'value',
+ --client_encoding 'value',
+ tty 'value',
+ options 'value',
+ application_name 'value',
+ --fallback_application_name 'value',
+ keepalives 'value',
+ keepalives_idle 'value',
+ keepalives_interval 'value',
+ -- requiressl 'value',
+ sslmode 'value',
+ sslcert 'value',
+ sslkey 'value',
+ sslrootcert 'value',
+ sslcrl 'value'
+ --requirepeer 'value',
+ -- krbsrvname 'value',
+ -- gsslib 'value',
+ --replication 'value'
+ );
+ ALTER SERVER loopback1 OPTIONS (user 'value'); -- ERROR
+ ALTER SERVER loopback2 OPTIONS (ADD fetch_count '2');
+ ALTER USER MAPPING FOR public SERVER loopback1
+ OPTIONS (DROP user, DROP password);
+ ALTER USER MAPPING FOR public SERVER loopback1
+ OPTIONS (host 'value'); -- ERROR
+ ALTER FOREIGN TABLE ft1 OPTIONS (nspname 'S 1', relname 'T 1');
+ ALTER FOREIGN TABLE ft2 OPTIONS (nspname 'S 1', relname 'T 1', fetch_count '100');
+ ALTER FOREIGN TABLE ft1 OPTIONS (invalid 'value'); -- ERROR
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count 'a'); -- ERROR
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count '0'); -- ERROR
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count '-1'); -- ERROR
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (invalid 'value'); -- ERROR
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (colname 'C 1');
+ ALTER FOREIGN TABLE ft2 ALTER COLUMN c1 OPTIONS (colname 'C 1');
+ \dew+
+ \des+
+ \deu+
+ \det+
+
+ -- ===================================================================
+ -- simple queries
+ -- ===================================================================
+ -- single table, with/without alias
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
+ SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+ SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+ -- with WHERE clause
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
+ SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
+ -- 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
+ SELECT * FROM ft1 t1 WHERE t1.c3 = (SELECT MAX(c3) FROM ft2 t2) ORDER BY c1;
+ -- used in CTE
+ WITH t1 AS (SELECT * FROM ft1 WHERE c1 <= 10) SELECT t2.c1, t2.c2, t2.c3, t2.c4 FROM t1, ft2 t2 WHERE t1.c1 = t2.c1 ORDER BY t1.c1;
+ -- fixed values
+ SELECT 'fixed', NULL FROM ft1 t1 WHERE c1 = 1;
+ -- user-defined operator/function
+ CREATE FUNCTION pgsql_fdw_abs(int) RETURNS int AS $$
+ BEGIN
+ RETURN abs($1);
+ END
+ $$ LANGUAGE plpgsql IMMUTABLE;
+ CREATE OPERATOR === (
+ LEFTARG = int,
+ RIGHTARG = int,
+ PROCEDURE = int4eq,
+ COMMUTATOR = ===,
+ NEGATOR = !==
+ );
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = pgsql_fdw_abs(t1.c2);
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 === t1.c2;
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = abs(t1.c2);
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = t1.c2;
+ DROP OPERATOR === (int, int) CASCADE;
+ DROP FUNCTION pgsql_fdw_abs(int);
+
+ -- ===================================================================
+ -- parameterized queries
+ -- ===================================================================
+ -- simple join
+ PREPARE st1(int, int) AS SELECT t1.c3, t2.c3 FROM ft1 t1, ft2 t2 WHERE t1.c1 = $1 AND t2.c1 = $2;
+ EXPLAIN (COSTS false) EXECUTE st1(1, 2);
+ EXECUTE st1(1, 1);
+ EXECUTE st1(101, 101);
+ -- subquery using stable function (can't be pushed down)
+ PREPARE st2(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c4) = 6) ORDER BY c1;
+ EXPLAIN (COSTS false) EXECUTE st2(10, 20);
+ EXECUTE st2(10, 20);
+ EXECUTE st1(101, 101);
+ -- subquery using immutable function (can be pushed down)
+ PREPARE st3(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c5) = 6) ORDER BY c1;
+ EXPLAIN (COSTS false) EXECUTE st3(10, 20);
+ EXECUTE st3(10, 20);
+ EXECUTE st3(20, 30);
+ -- custom plan should be chosen
+ PREPARE st4(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 = $1;
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ -- cleanup
+ DEALLOCATE st1;
+ DEALLOCATE st2;
+ DEALLOCATE st3;
+ DEALLOCATE st4;
+
+ -- ===================================================================
+ -- connection management
+ -- ===================================================================
+ SELECT srvname, usename FROM pgsql_fdw_connections;
+ SELECT pgsql_fdw_disconnect(srvid, usesysid) FROM pgsql_fdw_get_connections();
+ SELECT srvname, usename FROM pgsql_fdw_connections;
+
+ -- ===================================================================
+ -- cleanup
+ -- ===================================================================
+ DROP EXTENSION pgsql_fdw CASCADE;
+
diff --git a/doc/src/sgml/contrib.sgml b/doc/src/sgml/contrib.sgml
index adf09ca..65c7e81 100644
*** a/doc/src/sgml/contrib.sgml
--- b/doc/src/sgml/contrib.sgml
*************** CREATE EXTENSION <replaceable>module_nam
*** 117,122 ****
--- 117,123 ----
&pgcrypto;
&pgfreespacemap;
&pgrowlocks;
+ &pgsql-fdw;
&pgstandby;
&pgstatstatements;
&pgstattuple;
diff --git a/doc/src/sgml/filelist.sgml b/doc/src/sgml/filelist.sgml
index 7a698e5..2dd41f0 100644
*** a/doc/src/sgml/filelist.sgml
--- b/doc/src/sgml/filelist.sgml
***************
*** 124,129 ****
--- 124,130 ----
<!ENTITY pgcrypto SYSTEM "pgcrypto.sgml">
<!ENTITY pgfreespacemap SYSTEM "pgfreespacemap.sgml">
<!ENTITY pgrowlocks SYSTEM "pgrowlocks.sgml">
+ <!ENTITY pgsql-fdw SYSTEM "pgsql-fdw.sgml">
<!ENTITY pgstandby SYSTEM "pgstandby.sgml">
<!ENTITY pgstatstatements SYSTEM "pgstatstatements.sgml">
<!ENTITY pgstattuple SYSTEM "pgstattuple.sgml">
diff --git a/doc/src/sgml/pgsql-fdw.sgml b/doc/src/sgml/pgsql-fdw.sgml
index ...299f4b6 .
*** a/doc/src/sgml/pgsql-fdw.sgml
--- b/doc/src/sgml/pgsql-fdw.sgml
***************
*** 0 ****
--- 1,244 ----
+ <!-- doc/src/sgml/pgsql-fdw.sgml -->
+
+ <sect1 id="pgsql-fdw" xreflabel="pgsql_fdw">
+ <title>pgsql_fdw</title>
+
+ <indexterm zone="pgsql-fdw">
+ <primary>pgsql_fdw</primary>
+ </indexterm>
+
+ <para>
+ The <filename>pgsql_fdw</filename> module provides a foreign-data wrapper for
+ external <productname>PostgreSQL</productname> servers.
+ With this module, users can access data stored in external
+ <productname>PostgreSQL</productname> via plain SQL statements.
+ </para>
+
+ <para>
+ Note that default wrapper <literal>pgsql_fdw</literal> is created
+ automatically during <command>CREATE EXTENSION</command> command for
+ <application>pgsql_fdw</application>.
+ </para>
+
+ <sect2>
+ <title>FDW Options of pgsql_fdw</title>
+
+ <sect3>
+ <title>Connection Options</title>
+ <para>
+ A foreign server and user mapping created using this wrapper can have
+ <application>libpq</> connection options, expect below:
+
+ <itemizedlist>
+ <listitem><para>client_encoding</para></listitem>
+ <listitem><para>fallback_application_name</para></listitem>
+ <listitem><para>replication</para></listitem>
+ </itemizedlist>
+
+ For details of <application>libpq</> connection options, see
+ <xref linkend="libpq-connect">.
+ </para>
+
+ <para>
+ <literal>user</literal> and <literal>password</literal> can be
+ specified on user mappings, and others can be specified on foreign servers.
+ </para>
+ </sect3>
+
+ <sect3>
+ <title>Object Name Options</title>
+ <para>
+ Foreign tables which were created using this wrapper, or its columns can
+ have object name options. These options can be used to specify the names
+ used in SQL statement sent to remote <productname>PostgreSQL</productname>
+ server. These options are useful when a remote object has different name
+ from corresponding local one.
+ </para>
+
+ <variablelist>
+
+ <varlistentry>
+ <term><literal>nspname</literal></term>
+ <listitem>
+ <para>
+ This option, which can be specified on a foreign table, is used as a
+ namespace (schema) reference in the SQL statement. If this options is
+ omitted, <literal>pg_class.nspname</literal> of the foreign table is
+ used.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>relname</literal></term>
+ <listitem>
+ <para>
+ This option, which can be specified on a foreign table, is used as a
+ relation (table) reference in the SQL statement. If this options is
+ omitted, <literal>pg_class.relname</literal> of the foreign table is
+ used.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>colname</literal></term>
+ <listitem>
+ <para>
+ This option, which can be specified on a column of a foreign table, is
+ used as a column (attribute) reference in the SQL statement. If this
+ option is omitted, <literal>pg_attribute.attname</literal> of the column
+ of the foreign table is used.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ </variablelist>
+
+ </sect3>
+
+ <sect3>
+ <title>Cursor Options</title>
+ <para>
+ The <application>pgsql_fdw</application> always uses cursor to retrieve the
+ result from external server. Users can control the behavior of cursor by
+ setting cursor options to foreign table or foreign server. If an option
+ is set to both objects, finer-grained setting is used. In other words,
+ foreign table's setting overrides foreign server's setting.
+ </para>
+
+ <variablelist>
+
+ <varlistentry>
+ <term><literal>fetch_count</literal></term>
+ <listitem>
+ <para>
+ This option specifies the number of rows to be fetched at a time.
+ This option accepts only integer value larger than zero. The default
+ setting is 10000.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ </variablelist>
+
+ </sect3>
+
+ </sect2>
+
+ <sect2>
+ <title>Connection Management</title>
+
+ <para>
+ The <application>pgsql_fdw</application> establishes a connection to a
+ foreign server in the beginning of the first query which uses a foreign
+ table associated to the foreign server, and reuses the connection following
+ queries and even in following foreign scans in same query.
+
+ You can see the list of active connections via
+ <structname>pgsql_fdw_connections</structname> view. It shows pair of oid
+ and name of server and local role for each active connections established by
+ <application>pgsql_fdw</application>. For security reason, only superuser
+ can see other role's connections.
+ </para>
+
+ <para>
+ Established connections are kept alive until local role changes or the
+ current transaction aborts or user requests so.
+ </para>
+
+ <para>
+ If role has been changed, active connections established as old local role
+ is kept alive but never be reused until locla role has restored to original
+ role. This kind of situation happens with <command>SET ROLE</command> and
+ <command>SET SESSION AUTHORIZATION</command>.
+ </para>
+
+ <para>
+ If current transaction aborts by error or user request, all active
+ connections are disconnected automatically. This behavior avoids possible
+ connection leaks on error.
+ </para>
+
+ <para>
+ You can discard persistent connection at arbitrary timing with
+ <function>pgsql_fdw_disconnect()</function>. It takes server oid and
+ user oid as arguments. This function can handle only connections
+ established in current session; connections established by other backends
+ are not reachable.
+ </para>
+
+ <para>
+ You can discard all active and visible connections in current session with
+ using <structname>pgsql_fdw_connections</structname> and
+ <function>pgsql_fdw_disconnect()</function> together:
+ <synopsis>
+ postgres=# SELECT pgsql_fdw_disconnect(srvid, usesysid) FROM pgsql_fdw_connections;
+ pgsql_fdw_disconnect
+ ----------------------
+ OK
+ OK
+ (2 rows)
+ </synopsis>
+ </para>
+ </sect2>
+
+ <sect2>
+ <title>Transaction Management</title>
+ <para>
+ The <application>pgsql_fdw</application> executes <command>BEGIN</command>
+ command when a new connection has established. This means that all remote
+ queries for a foreign server are executed in a transaction. Since the
+ default transaction isolation level is <literal>READ COMMITTED</literal>,
+ multiple foreign scans in a local query might produce inconsistent results.
+
+ To avoid this inconsistency, you can use <literal>SERIALIZABLE</literal>
+ level for remote transaction with setting
+ <literal>default_transaction_isolation</literal> for the user used for
+ <application>pgsql_fdw</application> connection on remote side.
+ </para>
+ </sect2>
+
+ <sect2>
+ <title>Estimation of Costs and Rows</title>
+ <para>
+ The <application>pgsql_fdw</application> estimates the costs of a foreign
+ scan by adding some basic costs: connection costs, remote query costs and
+ data transfer costs.
+ To get remote query costs <application>pgsql_fdw</application> executes
+ <command>EXPLAIN</command> command on remote server for each foreign scan.
+ </para>
+
+ <para>
+ On the other hand, estimated rows which was returned by
+ <command>EXPLAIN</command> is used for local estimation as-is.
+ </para>
+ </sect2>
+
+ <sect2>
+ <title>EXPLAIN Output</title>
+ <para>
+ For a foreign table using <literal>pgsql_fdw</>, <command>EXPLAIN</> shows
+ a remote SQL statement which is sent to remote
+ <productname>PostgreSQL</productname> server for a ForeignScan plan node.
+ For example:
+ </para>
+ <synopsis>
+ postgres=# EXPLAIN SELECT aid FROM pgbench_accounts WHERE abalance < 0;
+ QUERY PLAN
+ --------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on pgbench_accounts (cost=100.00..8046.37 rows=301037 width=8)
+ Filter: (abalance < 0)
+ Remote SQL: DECLARE pgsql_fdw_cursor_1 SCROLL CURSOR FOR SELECT aid, NULL, abalance, NULL FROM public.pgbench_accounts
+ (3 rows)
+ </synopsis>
+ </sect2>
+
+ <sect2>
+ <title>Author</title>
+ <para>
+ Shigeru Hanada <email>shigeru.hanada@gmail.com</email>
+ </para>
+ </sect2>
+
+ </sect1>
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 29df748..a1de09d 100644
*** a/src/backend/utils/adt/ruleutils.c
--- b/src/backend/utils/adt/ruleutils.c
*************** deparse_context_for(const char *aliasnam
*** 2161,2166 ****
--- 2161,2189 ----
return list_make1(dpns);
}
+ /* ----------
+ * deparse_context_for_rtelist - Build deparse context for a list of RTEs
+ *
+ * Given the list of RangeTableEnetry, build deparsing context for an
+ * expression referencing those relations. This is sufficient for uses of
+ * deparse_expression before plan has been created.
+ * ----------
+ */
+ List *
+ deparse_context_for_rtelist(List *rtable)
+ {
+ deparse_namespace *dpns;
+
+ dpns = (deparse_namespace *) palloc0(sizeof(deparse_namespace));
+
+ /* Build a minimal RTE for the rel */
+ dpns->rtable = rtable;
+ dpns->ctes = NIL;
+
+ /* Return a one-deep namespace stack */
+ return list_make1(dpns);
+ }
+
/*
* deparse_context_for_planstate - Build deparse context for a plan
*
diff --git a/src/include/utils/builtins.h b/src/include/utils/builtins.h
index 994dc53..ccb186b 100644
*** a/src/include/utils/builtins.h
--- b/src/include/utils/builtins.h
*************** extern char *deparse_expression(Node *ex
*** 642,647 ****
--- 642,648 ----
extern List *deparse_context_for(const char *aliasname, Oid relid);
extern List *deparse_context_for_planstate(Node *planstate, List *ancestors,
List *rtable);
+ extern List *deparse_context_for_rtelist(List *rtable);
extern const char *quote_identifier(const char *ident);
extern char *quote_qualified_identifier(const char *qualifier,
const char *ident);
pgsql_fdw_pushdown_v1.patchtext/plain; name=pgsql_fdw_pushdown_v1.patchDownload
diff --git a/contrib/pgsql_fdw/deparse.c b/contrib/pgsql_fdw/deparse.c
index def5cb3..26b410d 100644
*** a/contrib/pgsql_fdw/deparse.c
--- b/contrib/pgsql_fdw/deparse.c
*************** typedef struct foreign_executable_cxt
*** 35,43 ****
RelOptInfo *foreignrel;
} foreign_executable_cxt;
/*
* Deparse query representation into SQL statement which suits for remote
! * PostgreSQL server.
*/
char *
deparseSql(Oid relid, PlannerInfo *root, RelOptInfo *baserel)
--- 35,48 ----
RelOptInfo *foreignrel;
} foreign_executable_cxt;
+ static bool is_foreign_expr(PlannerInfo *root, RelOptInfo *baserel, Expr *expr);
+ static bool foreign_expr_walker(Node *node, foreign_executable_cxt *context);
+ static bool is_builtin(Oid procid);
+
/*
* Deparse query representation into SQL statement which suits for remote
! * PostgreSQL server. Also some of quals in WHERE clause will be pushed down
! * If they are safe to be evaluated on the remote side.
*/
char *
deparseSql(Oid relid, PlannerInfo *root, RelOptInfo *baserel)
*************** deparseSql(Oid relid, PlannerInfo *root,
*** 51,56 ****
--- 56,62 ----
const char *relname = NULL; /* plain relation name */
const char *q_nspname; /* quoted namespace name */
const char *q_relname; /* quoted relation name */
+ List *foreign_expr = NIL; /* list of Expr* evaluated on remote */
int i;
List *rtable = NIL;
List *context = NIL;
*************** deparseSql(Oid relid, PlannerInfo *root,
*** 59,65 ****
initStringInfo(&foreign_relname);
/*
! * First of all, determine which column should be retrieved for this scan.
*
* We do this before deparsing SELECT clause because attributes which are
* not used in neither reltargetlist nor baserel->baserestrictinfo, quals
--- 65,79 ----
initStringInfo(&foreign_relname);
/*
! * First of all, determine which qual can be pushed down.
! *
! * The expressions which satisfy is_foreign_expr() are deparsed into WHERE
! * clause of result SQL string, and they could be removed from PlanState
! * to avoid duplicate evaluation at ExecScan().
! *
! * We never change the quals in the Plan node, because this execution might
! * be for a PREPAREd statement, thus the quals in the Plan node might be
! * reused to construct another PlanState for subsequent EXECUTE statement.
*
* We do this before deparsing SELECT clause because attributes which are
* not used in neither reltargetlist nor baserel->baserestrictinfo, quals
*************** deparseSql(Oid relid, PlannerInfo *root,
*** 75,80 ****
--- 89,98 ----
RestrictInfo *ri = (RestrictInfo *) lfirst(lc);
List *attrs;
+ /* Determine whether the qual can be pushed down or not. */
+ if (is_foreign_expr(root, baserel, ri->clause))
+ foreign_expr = lappend(foreign_expr, ri->clause);
+
/*
* We need to know which attributes are used in qual evaluated
* on the local server, because they should be listed in the
*************** deparseSql(Oid relid, PlannerInfo *root,
*** 193,199 ****
--- 211,408 ----
*/
appendStringInfo(&sql, "FROM %s", foreign_relname.data);
+ /*
+ * deparse WHERE clause
+ */
+ if (foreign_expr != NIL)
+ {
+ Node *node;
+
+ node = (Node *) make_ands_explicit(foreign_expr);
+ appendStringInfo(&sql, " WHERE %s ",
+ deparse_expression(node, context, false, false));
+ list_free(foreign_expr);
+ foreign_expr = NIL;
+ }
+
elog(DEBUG3, "Remote SQL: %s", sql.data);
return sql.data;
}
+ /*
+ * Returns true if expr is safe to be evaluated on the foreign server.
+ */
+ static bool
+ is_foreign_expr(PlannerInfo *root, RelOptInfo *baserel, Expr *expr)
+ {
+ foreign_executable_cxt context;
+ context.root = root;
+ context.foreignrel = baserel;
+
+ /*
+ * An expression which includes any mutable function can't be pushed down
+ * because it's result is not stable. For example, pushing now() down to
+ * remote side would cause confusion from the clock offset.
+ * If we have routine mapping infrastructure in future release, we will be
+ * able to choose function to be pushed down in finer granularity.
+ */
+ if (contain_mutable_functions((Node *) expr))
+ return false;
+
+ /*
+ * Check that the expression consists of nodes which are known as safe to
+ * be pushed down.
+ */
+ if (foreign_expr_walker((Node *) expr, &context))
+ return false;
+
+ return true;
+ }
+
+ /*
+ * Return true if node includes any node which is not known as safe to be
+ * pushed down.
+ */
+ static bool
+ foreign_expr_walker(Node *node, foreign_executable_cxt *context)
+ {
+ Oid func;
+
+ if (node == NULL)
+ return false;
+
+ /*
+ * If given expression has valid collation, it can't be pushed down bacause
+ * it might has uncompatible semantics on remote side.
+ */
+ if (exprCollation(node) != InvalidOid ||
+ exprInputCollation(node) != InvalidOid)
+ return true;
+
+ /*
+ * If return type of given expression is not built-in, it can't be pushed
+ * down bacause it might has uncompatible semantics on remote side.
+ */
+ if (!is_builtin(exprType(node)))
+ return true;
+
+ /*
+ * If function used by the expression is not built-in, it can't be pushed
+ * down bacause it might has uncompatible semantics on remote side.
+ */
+ func = exprFunction(node);
+ if (func != InvalidOid && !is_builtin(func))
+ return true;
+
+ switch (nodeTag(node))
+ {
+ case T_Const:
+ case T_BoolExpr:
+ case T_NullTest:
+ case T_DistinctExpr:
+ case T_RelabelType:
+ case T_FuncExpr:
+ /*
+ * 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 at the tail of this function.
+ */
+ break;
+ case T_Param:
+ /*
+ * Only external parameters can be pushed down.:
+ */
+ {
+ if (((Param *) node)->paramkind != PARAM_EXTERN)
+ return true;
+ }
+ break;
+ case T_ScalarArrayOpExpr:
+ /*
+ * Only built-in operators can be pushed down. In addition,
+ * underlying function must be built-in and immutable, but we don't
+ * check volatility here; such check must be done already with
+ * contain_mutable_functions.
+ */
+ {
+ ScalarArrayOpExpr *oe = (ScalarArrayOpExpr *) node;
+
+ if (!is_builtin(oe->opno))
+ return true;
+
+ /* operands are checked later */
+ }
+ break;
+ case T_OpExpr:
+ /*
+ * Only built-in operators can be pushed down. In addition,
+ * underlying function must be built-in and immutable, but we don't
+ * check volatility here; such check must be done already with
+ * contain_mutable_functions.
+ */
+ {
+ OpExpr *oe = (OpExpr *) node;
+
+ if (!is_builtin(oe->opno))
+ return true;
+
+ /* operands are checked later */
+ }
+ 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;
+ foreign_executable_cxt *f_context;
+
+ f_context = (foreign_executable_cxt *) context;
+ if (var->varno != f_context->foreignrel->relid ||
+ var->varlevelsup != 0)
+ return true;
+ }
+ break;
+ case T_ArrayRef:
+ /*
+ * ArrayRef which holds non-built-in typed elements can't be pushed
+ * down.
+ */
+ {
+ if (!is_builtin(((ArrayRef *) node)->refelemtype))
+ return true;
+ }
+ break;
+ case T_ArrayExpr:
+ /*
+ * ArrayExpr which holds non-built-in typed elements can't be pushed
+ * down.
+ */
+ {
+ if (!is_builtin(((ArrayExpr *) node)->element_typeid))
+ return true;
+ }
+ break;
+ default:
+ {
+ ereport(DEBUG3,
+ (errmsg("expression is too complex"),
+ errdetail("%s", nodeToString(node))));
+ return true;
+ }
+ break;
+ }
+
+ return expression_tree_walker(node, foreign_expr_walker, context);
+ }
+
+ /*
+ * Return true if given object is one of built-in objects.
+ */
+ static bool
+ is_builtin(Oid oid)
+ {
+ return (oid < FirstNormalObjectId);
+ }
+
diff --git a/contrib/pgsql_fdw/expected/pgsql_fdw.out b/contrib/pgsql_fdw/expected/pgsql_fdw.out
index dfac57a..2166fb4 100644
*** a/contrib/pgsql_fdw/expected/pgsql_fdw.out
--- b/contrib/pgsql_fdw/expected/pgsql_fdw.out
*************** SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.
*** 218,229 ****
-- with WHERE clause
EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
! QUERY PLAN
! ------------------------------------------------------------------------------------------------------------------
Foreign Scan on public.ft1 t1
Output: c1, c2, c3, c4, c5, c6, c7
Filter: ((t1.c1 = 101) AND ((t1.c6)::text = '1'::text) AND (t1.c7 = '1'::bpchar))
! Remote SQL: DECLARE pgsql_fdw_cursor_4 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
(4 rows)
SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
--- 218,229 ----
-- with WHERE clause
EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
! QUERY PLAN
! ---------------------------------------------------------------------------------------------------------------------------------------
Foreign Scan on public.ft1 t1
Output: c1, c2, c3, c4, c5, c6, c7
Filter: ((t1.c1 = 101) AND ((t1.c6)::text = '1'::text) AND (t1.c7 = '1'::bpchar))
! Remote SQL: DECLARE pgsql_fdw_cursor_4 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE ("C 1" = 101)
(4 rows)
SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
*************** EXPLAIN (COSTS false) SELECT * FROM ft1
*** 331,349 ****
(3 rows)
EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = abs(t1.c2);
! QUERY PLAN
! -------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
Filter: (c1 = abs(c2))
! Remote SQL: DECLARE pgsql_fdw_cursor_20 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
(3 rows)
EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = t1.c2;
! QUERY PLAN
! -------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
Filter: (c1 = c2)
! Remote SQL: DECLARE pgsql_fdw_cursor_21 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
(3 rows)
DROP OPERATOR === (int, int) CASCADE;
--- 331,349 ----
(3 rows)
EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = abs(t1.c2);
! QUERY PLAN
! --------------------------------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
Filter: (c1 = abs(c2))
! Remote SQL: DECLARE pgsql_fdw_cursor_20 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE ("C 1" = abs(c2))
(3 rows)
EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = t1.c2;
! QUERY PLAN
! ---------------------------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
Filter: (c1 = c2)
! Remote SQL: DECLARE pgsql_fdw_cursor_21 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE ("C 1" = c2)
(3 rows)
DROP OPERATOR === (int, int) CASCADE;
*************** DROP FUNCTION pgsql_fdw_abs(int);
*** 354,370 ****
-- simple join
PREPARE st1(int, int) AS SELECT t1.c3, t2.c3 FROM ft1 t1, ft2 t2 WHERE t1.c1 = $1 AND t2.c1 = $2;
EXPLAIN (COSTS false) EXECUTE st1(1, 2);
! QUERY PLAN
! -----------------------------------------------------------------------------------------------------------------------------------------
Nested Loop
-> Foreign Scan on ft1 t1
Filter: (c1 = 1)
! Remote SQL: DECLARE pgsql_fdw_cursor_22 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, NULL, NULL, NULL, NULL FROM "S 1"."T 1"
! -> Materialize
! -> Foreign Scan on ft2 t2
! Filter: (c1 = 2)
! Remote SQL: DECLARE pgsql_fdw_cursor_23 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, NULL, NULL, NULL, NULL FROM "S 1"."T 1"
! (8 rows)
EXECUTE st1(1, 1);
c3 | c3
--- 354,369 ----
-- simple join
PREPARE st1(int, int) AS SELECT t1.c3, t2.c3 FROM ft1 t1, ft2 t2 WHERE t1.c1 = $1 AND t2.c1 = $2;
EXPLAIN (COSTS false) EXECUTE st1(1, 2);
! QUERY PLAN
! ------------------------------------------------------------------------------------------------------------------------------------------------------
Nested Loop
-> Foreign Scan on ft1 t1
Filter: (c1 = 1)
! Remote SQL: DECLARE pgsql_fdw_cursor_22 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, NULL, NULL, NULL, NULL FROM "S 1"."T 1" WHERE ("C 1" = 1)
! -> Foreign Scan on ft2 t2
! Filter: (c1 = 2)
! Remote SQL: DECLARE pgsql_fdw_cursor_23 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, NULL, NULL, NULL, NULL FROM "S 1"."T 1" WHERE ("C 1" = 2)
! (7 rows)
EXECUTE st1(1, 1);
c3 | c3
*************** EXECUTE st1(101, 101);
*** 381,388 ****
-- subquery using stable function (can't be pushed down)
PREPARE st2(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c4) = 6) ORDER BY c1;
EXPLAIN (COSTS false) EXECUTE st2(10, 20);
! QUERY PLAN
! ---------------------------------------------------------------------------------------------------------------------------------------------
Sort
Sort Key: t1.c1
-> Nested Loop
--- 380,387 ----
-- subquery using stable function (can't be pushed down)
PREPARE st2(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c4) = 6) ORDER BY c1;
EXPLAIN (COSTS false) EXECUTE st2(10, 20);
! QUERY PLAN
! -----------------------------------------------------------------------------------------------------------------------------------------------------------------
Sort
Sort Key: t1.c1
-> Nested Loop
*************** EXPLAIN (COSTS false) EXECUTE st2(10, 20
*** 390,399 ****
-> HashAggregate
-> Foreign Scan on ft2 t2
Filter: ((c1 > 10) AND (date_part('dow'::text, c4) = 6::double precision))
! Remote SQL: DECLARE pgsql_fdw_cursor_29 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, c4, NULL, NULL, NULL FROM "S 1"."T 1"
-> Foreign Scan on ft1 t1
Filter: (c1 < 20)
! Remote SQL: DECLARE pgsql_fdw_cursor_28 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
(11 rows)
EXECUTE st2(10, 20);
--- 389,398 ----
-> HashAggregate
-> Foreign Scan on ft2 t2
Filter: ((c1 > 10) AND (date_part('dow'::text, c4) = 6::double precision))
! Remote SQL: DECLARE pgsql_fdw_cursor_29 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, c4, NULL, NULL, NULL FROM "S 1"."T 1" WHERE ("C 1" > 10)
-> Foreign Scan on ft1 t1
Filter: (c1 < 20)
! Remote SQL: DECLARE pgsql_fdw_cursor_28 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE ("C 1" < 20)
(11 rows)
EXECUTE st2(10, 20);
*************** EXECUTE st1(101, 101);
*** 411,418 ****
-- subquery using immutable function (can be pushed down)
PREPARE st3(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c5) = 6) ORDER BY c1;
EXPLAIN (COSTS false) EXECUTE st3(10, 20);
! QUERY PLAN
! ---------------------------------------------------------------------------------------------------------------------------------------------
Sort
Sort Key: t1.c1
-> Nested Loop
--- 410,417 ----
-- subquery using immutable function (can be pushed down)
PREPARE st3(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c5) = 6) ORDER BY c1;
EXPLAIN (COSTS false) EXECUTE st3(10, 20);
! QUERY PLAN
! -----------------------------------------------------------------------------------------------------------------------------------------------------------------
Sort
Sort Key: t1.c1
-> Nested Loop
*************** EXPLAIN (COSTS false) EXECUTE st3(10, 20
*** 420,429 ****
-> HashAggregate
-> Foreign Scan on ft2 t2
Filter: ((c1 > 10) AND (date_part('dow'::text, c5) = 6::double precision))
! Remote SQL: DECLARE pgsql_fdw_cursor_35 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, NULL, c5, NULL, NULL FROM "S 1"."T 1"
-> Foreign Scan on ft1 t1
Filter: (c1 < 20)
! Remote SQL: DECLARE pgsql_fdw_cursor_34 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
(11 rows)
EXECUTE st3(10, 20);
--- 419,428 ----
-> HashAggregate
-> Foreign Scan on ft2 t2
Filter: ((c1 > 10) AND (date_part('dow'::text, c5) = 6::double precision))
! Remote SQL: DECLARE pgsql_fdw_cursor_35 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, NULL, c5, NULL, NULL FROM "S 1"."T 1" WHERE ("C 1" > 10)
-> Foreign Scan on ft1 t1
Filter: (c1 < 20)
! Remote SQL: DECLARE pgsql_fdw_cursor_34 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE ("C 1" < 20)
(11 rows)
EXECUTE st3(10, 20);
*************** EXECUTE st3(20, 30);
*** 441,491 ****
-- custom plan should be chosen
PREPARE st4(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 = $1;
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! -------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
Filter: (c1 = 1)
! Remote SQL: DECLARE pgsql_fdw_cursor_40 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
(3 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! -------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
Filter: (c1 = 1)
! Remote SQL: DECLARE pgsql_fdw_cursor_41 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
(3 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! -------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
Filter: (c1 = 1)
! Remote SQL: DECLARE pgsql_fdw_cursor_42 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
(3 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! -------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
Filter: (c1 = 1)
! Remote SQL: DECLARE pgsql_fdw_cursor_43 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
(3 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! -------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
Filter: (c1 = 1)
! Remote SQL: DECLARE pgsql_fdw_cursor_44 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
(3 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! -------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
Filter: (c1 = 1)
! Remote SQL: DECLARE pgsql_fdw_cursor_46 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
(3 rows)
-- cleanup
--- 440,490 ----
-- custom plan should be chosen
PREPARE st4(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 = $1;
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! --------------------------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
Filter: (c1 = 1)
! Remote SQL: DECLARE pgsql_fdw_cursor_40 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE ("C 1" = 1)
(3 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! --------------------------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
Filter: (c1 = 1)
! Remote SQL: DECLARE pgsql_fdw_cursor_41 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE ("C 1" = 1)
(3 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! --------------------------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
Filter: (c1 = 1)
! Remote SQL: DECLARE pgsql_fdw_cursor_42 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE ("C 1" = 1)
(3 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! --------------------------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
Filter: (c1 = 1)
! Remote SQL: DECLARE pgsql_fdw_cursor_43 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE ("C 1" = 1)
(3 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! --------------------------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
Filter: (c1 = 1)
! Remote SQL: DECLARE pgsql_fdw_cursor_44 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE ("C 1" = 1)
(3 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! --------------------------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
Filter: (c1 = 1)
! Remote SQL: DECLARE pgsql_fdw_cursor_46 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE ("C 1" = 1)
(3 rows)
-- cleanup
diff --git a/doc/src/sgml/pgsql-fdw.sgml b/doc/src/sgml/pgsql-fdw.sgml
index 299f4b6..1715257 100644
*** a/doc/src/sgml/pgsql-fdw.sgml
--- b/doc/src/sgml/pgsql-fdw.sgml
*************** postgres=# SELECT pgsql_fdw_disconnect(s
*** 225,235 ****
</para>
<synopsis>
postgres=# EXPLAIN SELECT aid FROM pgbench_accounts WHERE abalance < 0;
! QUERY PLAN
! --------------------------------------------------------------------------------------------------------------------------
! Foreign Scan on pgbench_accounts (cost=100.00..8046.37 rows=301037 width=8)
Filter: (abalance < 0)
! Remote SQL: DECLARE pgsql_fdw_cursor_1 SCROLL CURSOR FOR SELECT aid, NULL, abalance, NULL FROM public.pgbench_accounts
(3 rows)
</synopsis>
</sect2>
--- 225,235 ----
</para>
<synopsis>
postgres=# EXPLAIN SELECT aid FROM pgbench_accounts WHERE abalance < 0;
! QUERY PLAN
! -------------------------------------------------------------------------------------------------------------------------------------------------
! Foreign Scan on pgbench_accounts (cost=100.00..8798.96 rows=1 width=97)
Filter: (abalance < 0)
! Remote SQL: DECLARE pgsql_fdw_cursor_2 SCROLL CURSOR FOR SELECT aid, bid, abalance, filler FROM public.pgbench_accounts WHERE (abalance < 0)
(3 rows)
</synopsis>
</sect2>
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index 48b0590..f2acd46 100644
*** a/src/backend/nodes/nodeFuncs.c
--- b/src/backend/nodes/nodeFuncs.c
*************** leftmostLoc(int loc1, int loc2)
*** 1405,1410 ****
--- 1405,1447 ----
return Min(loc1, loc2);
}
+ /*
+ * exprFunction -
+ * returns the Oid of the function used by the expression's.
+ */
+ Oid
+ exprFunction(const Node *expr)
+ {
+ Oid func;
+
+ if (!expr)
+ return InvalidOid;
+
+ switch (nodeTag(expr))
+ {
+ case T_Aggref:
+ func = ((const Aggref *) expr)->aggfnoid;
+ break;
+ case T_WindowFunc:
+ func = ((const WindowFunc *) expr)->winfnoid;
+ break;
+ case T_FuncExpr:
+ func = ((const FuncExpr *) expr)->funcid;
+ break;
+ case T_OpExpr:
+ func = ((const OpExpr *) expr)->opfuncid;
+ break;
+ case T_ScalarArrayOpExpr:
+ func = ((const ScalarArrayOpExpr *) expr)->opfuncid;
+ break;
+ case T_ArrayCoerceExpr:
+ func = ((const ArrayCoerceExpr *) expr)->elemfuncid;
+ break;
+ default:
+ func = InvalidOid;
+ }
+ return func;
+ }
/*
* Standard expression-tree walking support
diff --git a/src/include/nodes/nodeFuncs.h b/src/include/nodes/nodeFuncs.h
index 4980dd8..09d877f 100644
*** a/src/include/nodes/nodeFuncs.h
--- b/src/include/nodes/nodeFuncs.h
*************** extern void exprSetInputCollation(Node *
*** 38,43 ****
--- 38,45 ----
extern int exprLocation(const Node *expr);
+ extern Oid exprFunction(const Node *expr);
+
extern bool expression_tree_walker(Node *node, bool (*walker) (),
void *context);
extern Node *expression_tree_mutator(Node *node, Node *(*mutator) (),
On 12/14/2011 09:02 AM, Shigeru Hanada wrote:
Here I'd like to propose three incremental patches:
1) fdw_helper_funcs_v3.patch...: This is not specific to pgsql_fdw, but
probably useful for every FDWs which use FDW options...
2) pgsql_fdw_v5.patch: This patch provides simple pgsql_fdw
which does *NOT* support any push-down...
3) pgsql_fdw_pushdown_v1.patch: This patch adds limited push-down
capability to pgsql_fdw which is implemented by previous patch...
...
To implement [expression which uses user-defined function], I added exprFunction to nodefuncs.c which returns Oid
of function which is used in the expression node, but I'm not sure that
it should be there. Should we have it inside pgsql_fdw?
After failing to bring some light onto this during my general update,
will try again here. We now have 3 updated patches that refactor things
from how this was originally presented, with one asked implementation
question. There's also a spawned off "Join push-down for foreign tables"
patch off in another thread.
I don't think it's really clear to everyone what state this feature
proposal is in. We've gotten bits of review here from KaiGai and Heikki,
big picture comments from Robert and Tom. Given how these are
structured, is fdw_helper_funcs_v3.patch at the point where it should be
considered for committer review? Maybe pgsql_fdw_v5.patch too?
The others seem to be more in flux to me, due to all the recent pushdown
changes.
--
Greg Smith 2ndQuadrant US greg@2ndQuadrant.com Baltimore, MD
PostgreSQL Training, Services, and 24x7 Support www.2ndQuadrant.us
Hi Harada-san,
I checked the "fdw_helper_funcs_v3.patch", "pgsql_fdw_v5.patch" and
"pgsql_fdw_pushdown_v1.patch". My comments are below.
[BUG]
Even though pgsql_fdw tries to push-down qualifiers being executable
on the remove side at the deparseSql(), it does not remove qualifiers
being pushed down from the baserel->baserestrictinfo, thus, these
qualifiers are eventually executed twice.
See the result of this EXPLAIN.
postgres=# EXPLAIN SELECT * FROM ft1 WHERE a > 2 AND f_leak(b);
QUERY PLAN
------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 (cost=107.43..122.55 rows=410 width=36)
Filter: (f_leak(b) AND (a > 2))
Remote SQL: DECLARE pgsql_fdw_cursor_0 SCROLL CURSOR FOR SELECT
a, b FROM public.t1 WHERE (a > 2)
(3 rows)
My expectation is (a > 2) being executed on the remote-side and f_leak(b)
being executed on the local-side. But, filter of foreign-scan on ft1 has both
of qualifiers. It has to be removed, if a RestrictInfo get pushed-down.
[Design comment]
I'm not sure the reason why store_result() uses MessageContext to save
the Tuplestorestate within PgsqlFdwExecutionState.
The source code comment says it is used to avoid memory leaks in error
cases. I also have a similar experience on implementation of my fdw module,
so, I could understand per-scan context is already cleared at the timing of
resource-release-callback, thus, handlers to external resource have to be
saved on separated memory context.
In my personal opinion, the right design is to declare a memory context for
pgsql_fdw itself, instead of the abuse of existing memory context.
(More wise design is to define sub-memory-context for each foreign-scan,
then, remove the sub-memory-context after release handlers.)
[Design comment]
When "BEGIN" should be issued on the remote-side?
The connect_pg_server() is an only chance to issue "BEGIN" command
at the remote-side on connection being opened. However, the connection
shall be kept unless an error is not raised. Thus, the remote-side will
continue to work within a single transaction block, even if local-side iterates
a pair of "BEGIN" and "COMMIT".
I'd like to suggest to close the transaction block at the timing of either
end of the scan, transaction or sub-transaction.
[Comment to Improve]
Also, which transaction isolation level should be specified in this case?
An idea is its isolation level is specified according to the current isolation
level on the local-side.
(Of course, it is your choice if it is not supported right now.)
[Comment to improve]
It seems to me the design of exprFunction is not future-proof, if we add
a new node type that contains two or more function calls, because it can
return an oid of functions.
I think, the right design is to handle individual node-types within the
large switch statement at foreign_expr_walker().
Of course, it is just my sense.
[Comment to improve]
The pgsql_fdw_handler() allocates FdwRoutine using makeNode(),
then it set function-pointers on each fields.
Why don't you return a pointer to statically declared FdwRoutine
variable being initialized at compile time, like:
static FdwRoutine pgsql_fdw_handler = {
.type = T_FdwRoutine,
.PlanForeignScan = pgsqlPlanForeignScan,
.ExplainForeignScan = pgsqlExplainForeignScan,
.BeginForeignScan = pgsqlBeginForeignScan,
.IterateForeignScan = pgsqlIterateForeignScan,
.ReScanForeignScan = pgsqlReScanForeignScan,
.EndForeignScan = pgsqlEndForeignScan,
};
Datum
pgsql_fdw_handler(PG_FUNCTION_ARGS)
{
PG_RETURN_POINTER(&pgsql_fdw_handler);
}
[Question to implementation]
At pgsqlIterateForeignScan(), it applies null-check on festate->tuples
and bool-checks on festete->cursor_opened.
Do we have a possible scenario that festate->tuples is not null, but
festate->cursor_opened, or an opposite combination?
If null-check on festate->tuples is enough to detect the first call of
the iterate callback, it is not my preference to have redundant flag.
Thanks,
2011年12月14日15:02 Shigeru Hanada <shigeru.hanada@gmail.com>:
(2011/12/13 14:46), Tom Lane wrote:
Shigeru Hanada<shigeru.hanada@gmail.com> writes:
Agreed. How about to add a per-column boolean FDW option, say
"pushdown", to pgsql_fdw? Users can tell pgsql_fdw that the column can
be pushed down safely by setting this option to true.[ itch... ] That doesn't seem like the right level of granularity.
ISTM the problem is with whether specific operators have the same
meaning at the far end as they do locally. If you try to attach the
flag to columns, you have to promise that *every* operator on that
column means what it does locally, which is likely to not be the
case ever if you look hard enough. Plus, having to set the flag on
each individual column of the same datatype seems pretty tedious.Indeed, I too think that labeling on each columns is not the best way,
but at that time I thought that it's a practical way, in a way. IOW, I
chose per-column FDW options as a compromise between never-push-down and
indiscriminate-push-down.Anyway, ISTM that we should consider various mapping for
functions, operators and collations to support push-down in general
way, but it would be hard to accomplish in this CF.Here I'd like to propose three incremental patches:
1) fdw_helper_funcs_v3.patch: This is not specific to pgsql_fdw, but
probably useful for every FDWs which use FDW options. This patch
provides some functions which help retrieving FDW options from catalogs.
This patch also enhances document about existing FDW helper functions.2) pgsql_fdw_v5.patch: This patch provides simple pgsql_fdw
which does *NOT* support any push-down. All data in remote table are
retrieved for each foreign scan, and conditions are always evaluated on
local side. This is safe about semantics difference between local and
remote, but very inefficient especially for large remote tables.3) pgsql_fdw_pushdown_v1.patch: This patch adds limited push-down
capability to pgsql_fdw which is implemented by previous patch. The
criteria for pushing down is little complex. I modified pgsql_fdw to
*NOT* push down conditions which contain any of:a) expression whose result collation is valid
b) expression whose input collation is valid
c) expression whose result type is user-defined
d) expression which uses user-defined function
e) array expression whose elements has user-defined type
f) expression which uses user-defined operator
g) expression which uses mutable functionAs the result, pgsql_fdw can push down very limited conditions such as
numeric comparisons, but it would be still useful. I hope that these
restriction are enough to avoid problems about semantics difference
between remote and local.To implement d), I added exprFunction to nodefuncs.c which returns Oid
of function which is used in the expression node, but I'm not sure that
it should be there. Should we have it inside pgsql_fdw?I'd like to thank everyone who commented on this topic!
Regards,
--
Shigeru Hanada
--
KaiGai Kohei <kaigai@kaigai.gr.jp>
2012/1/29 Kohei KaiGai <kaigai@kaigai.gr.jp>:
Remote SQL: DECLARE pgsql_fdw_cursor_0 SCROLL CURSOR FOR SELECT
a, b FROM public.t1 WHERE (a > 2)
(3 rows)
Shouldn't we be using protocol-level cursors rather than SQL-level cursors?
[Design comment]
When "BEGIN" should be issued on the remote-side?
The connect_pg_server() is an only chance to issue "BEGIN" command
at the remote-side on connection being opened. However, the connection
shall be kept unless an error is not raised. Thus, the remote-side will
continue to work within a single transaction block, even if local-side iterates
a pair of "BEGIN" and "COMMIT".
I'd like to suggest to close the transaction block at the timing of either
end of the scan, transaction or sub-transaction.
I suspect this is ultimately going to need to be configurable. Some
people might want to close the transaction on the remote side ASAP,
while other people might want to hold it open until commit. For a
first version I think it's most likely best to do whatever seems
simplest to code, planning to add more options later.
--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
(2012/01/30 4:39), Kohei KaiGai wrote:
I checked the "fdw_helper_funcs_v3.patch", "pgsql_fdw_v5.patch" and
"pgsql_fdw_pushdown_v1.patch". My comments are below.
Thanks for the review!
[BUG]
Even though pgsql_fdw tries to push-down qualifiers being executable
on the remove side at the deparseSql(), it does not remove qualifiers
being pushed down from the baserel->baserestrictinfo, thus, these
qualifiers are eventually executed twice.See the result of this EXPLAIN.
postgres=# EXPLAIN SELECT * FROM ft1 WHERE a> 2 AND f_leak(b);
QUERY PLAN
------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 (cost=107.43..122.55 rows=410 width=36)
Filter: (f_leak(b) AND (a> 2))
Remote SQL: DECLARE pgsql_fdw_cursor_0 SCROLL CURSOR FOR SELECT
a, b FROM public.t1 WHERE (a> 2)
(3 rows)My expectation is (a> 2) being executed on the remote-side and f_leak(b)
being executed on the local-side. But, filter of foreign-scan on ft1 has both
of qualifiers. It has to be removed, if a RestrictInfo get pushed-down.
It's intentional that pgsql_fdw keeps pushed-down qualifier in
baserestrictinfo, because I saw some undesirable behavior when I
implemented so that with such optimization when plan is reused, but it's
not clear to me now. I'll try to recall what I saw...
BTW, I think evaluating pushed-down qualifiers again on local side is
safe and has no semantic problem, though we must pay little for such
overhead. Do you have concern about performance?
[Design comment]
I'm not sure the reason why store_result() uses MessageContext to save
the Tuplestorestate within PgsqlFdwExecutionState.
The source code comment says it is used to avoid memory leaks in error
cases. I also have a similar experience on implementation of my fdw module,
so, I could understand per-scan context is already cleared at the timing of
resource-release-callback, thus, handlers to external resource have to be
saved on separated memory context.
In my personal opinion, the right design is to declare a memory context for
pgsql_fdw itself, instead of the abuse of existing memory context.
(More wise design is to define sub-memory-context for each foreign-scan,
then, remove the sub-memory-context after release handlers.)
I simply chose built-in context which has enough lifespan, but now I
think that using MessageContext directly is not recommended way. As you
say, creating new context as child of MessageContext for each scan in
BeginForeignScan (or first IterateForeignScan) would be better. Please
see attached patch.
One other option is getting rid of tuplestore by holding result rows as
PGresult, and track it for error cases which might happen.
ResourceOwner callback can be used to release PGresult on error, similar
to PGconn.
[Design comment]
When "BEGIN" should be issued on the remote-side?
The connect_pg_server() is an only chance to issue "BEGIN" command
at the remote-side on connection being opened. However, the connection
shall be kept unless an error is not raised. Thus, the remote-side will
continue to work within a single transaction block, even if local-side iterates
a pair of "BEGIN" and "COMMIT".
I'd like to suggest to close the transaction block at the timing of either
end of the scan, transaction or sub-transaction.
Indeed, remote transactions should be terminated at some timing.
Terminating at the end of a scan seems troublesome because a connection
might be shared by multiple scans in a query. I'd prefer aborting
remote transaction at the end of local query. Please see
abort_remote_tx in attached patch.
[Comment to Improve]
Also, which transaction isolation level should be specified in this case?
An idea is its isolation level is specified according to the current isolation
level on the local-side.
(Of course, it is your choice if it is not supported right now.)
Choosing same as local seems better. Please see start_remote_tx
function in attached patch.
[Comment to improve]
It seems to me the design of exprFunction is not future-proof, if we add
a new node type that contains two or more function calls, because it can
return an oid of functions.
I think, the right design is to handle individual node-types within the
large switch statement at foreign_expr_walker().
Of course, it is just my sense.
You mean that exprFunction should have capability to handle multiple
Oids for one node, maybe return List<oid> or something? IMO it's
overkill at this time.
Though I'm not sure that it's reasonable, but exprInputCollation too
seems to not assume that multiple input collation might be stored in one
node.
[Comment to improve]
The pgsql_fdw_handler() allocates FdwRoutine using makeNode(),
then it set function-pointers on each fields.
Why don't you return a pointer to statically declared FdwRoutine
variable being initialized at compile time, like:static FdwRoutine pgsql_fdw_handler = {
.type = T_FdwRoutine,
.PlanForeignScan = pgsqlPlanForeignScan,
.ExplainForeignScan = pgsqlExplainForeignScan,
.BeginForeignScan = pgsqlBeginForeignScan,
.IterateForeignScan = pgsqlIterateForeignScan,
.ReScanForeignScan = pgsqlReScanForeignScan,
.EndForeignScan = pgsqlEndForeignScan,
};Datum
pgsql_fdw_handler(PG_FUNCTION_ARGS)
{
PG_RETURN_POINTER(&pgsql_fdw_handler);
}
Fixed to static variable without designated initializers, because it's
one of C99 feature. Is there any written policy about using C99
features in PG development? I've read Developer's FAQ (wiki) and code
formatting section of document, but I couldn't find any mention.
[Question to implementation]
At pgsqlIterateForeignScan(), it applies null-check on festate->tuples
and bool-checks on festete->cursor_opened.
Do we have a possible scenario that festate->tuples is not null, but
festate->cursor_opened, or an opposite combination?
If null-check on festate->tuples is enough to detect the first call of
the iterate callback, it is not my preference to have redundant flag.
[ checking... ] No such scenario. It's undesired remain of obsolete
support of simple SELECT mode; pgsql_fdw once had two fetching mode;
simple SELECT mode for small result and CURSOR mode for huge result.
I've removed cursor_opened from PgsqlFdwExecutionState structure.
[Extra change]
In addition to your comments, I found that some regression tests fail
because current planner produces different plan tree from expected
results, and fixed such tests.
Regards,
--
Shigeru Hanada
Attachments:
pgsql_fdw_pushdown_v2.patchtext/plain; name=pgsql_fdw_pushdown_v2.patchDownload
diff --git a/contrib/pgsql_fdw/deparse.c b/contrib/pgsql_fdw/deparse.c
index cf9e775..3645189 100644
*** a/contrib/pgsql_fdw/deparse.c
--- b/contrib/pgsql_fdw/deparse.c
*************** typedef struct foreign_executable_cxt
*** 35,43 ****
RelOptInfo *foreignrel;
} foreign_executable_cxt;
/*
* Deparse query representation into SQL statement which suits for remote
! * PostgreSQL server.
*/
char *
deparseSql(Oid relid, PlannerInfo *root, RelOptInfo *baserel)
--- 35,48 ----
RelOptInfo *foreignrel;
} foreign_executable_cxt;
+ static bool is_foreign_expr(PlannerInfo *root, RelOptInfo *baserel, Expr *expr);
+ static bool foreign_expr_walker(Node *node, foreign_executable_cxt *context);
+ static bool is_builtin(Oid procid);
+
/*
* Deparse query representation into SQL statement which suits for remote
! * PostgreSQL server. Also some of quals in WHERE clause will be pushed down
! * If they are safe to be evaluated on the remote side.
*/
char *
deparseSql(Oid relid, PlannerInfo *root, RelOptInfo *baserel)
*************** deparseSql(Oid relid, PlannerInfo *root,
*** 51,56 ****
--- 56,62 ----
const char *relname = NULL; /* plain relation name */
const char *q_nspname; /* quoted namespace name */
const char *q_relname; /* quoted relation name */
+ List *foreign_expr = NIL; /* list of Expr* evaluated on remote */
int i;
List *rtable = NIL;
List *context = NIL;
*************** deparseSql(Oid relid, PlannerInfo *root,
*** 59,65 ****
initStringInfo(&foreign_relname);
/*
! * First of all, determine which column should be retrieved for this scan.
*
* We do this before deparsing SELECT clause because attributes which are
* not used in neither reltargetlist nor baserel->baserestrictinfo, quals
--- 65,79 ----
initStringInfo(&foreign_relname);
/*
! * First of all, determine which qual can be pushed down.
! *
! * The expressions which satisfy is_foreign_expr() are deparsed into WHERE
! * clause of result SQL string, and they could be removed from PlanState
! * to avoid duplicate evaluation at ExecScan().
! *
! * We never change the quals in the Plan node, because this execution might
! * be for a PREPAREd statement, thus the quals in the Plan node might be
! * reused to construct another PlanState for subsequent EXECUTE statement.
*
* We do this before deparsing SELECT clause because attributes which are
* not used in neither reltargetlist nor baserel->baserestrictinfo, quals
*************** deparseSql(Oid relid, PlannerInfo *root,
*** 75,80 ****
--- 89,98 ----
RestrictInfo *ri = (RestrictInfo *) lfirst(lc);
List *attrs;
+ /* Determine whether the qual can be pushed down or not. */
+ if (is_foreign_expr(root, baserel, ri->clause))
+ foreign_expr = lappend(foreign_expr, ri->clause);
+
/*
* We need to know which attributes are used in qual evaluated
* on the local server, because they should be listed in the
*************** deparseSql(Oid relid, PlannerInfo *root,
*** 193,199 ****
--- 211,408 ----
*/
appendStringInfo(&sql, "FROM %s", foreign_relname.data);
+ /*
+ * deparse WHERE clause
+ */
+ if (foreign_expr != NIL)
+ {
+ Node *node;
+
+ node = (Node *) make_ands_explicit(foreign_expr);
+ appendStringInfo(&sql, " WHERE %s ",
+ deparse_expression(node, context, false, false));
+ list_free(foreign_expr);
+ foreign_expr = NIL;
+ }
+
elog(DEBUG3, "Remote SQL: %s", sql.data);
return sql.data;
}
+ /*
+ * Returns true if expr is safe to be evaluated on the foreign server.
+ */
+ static bool
+ is_foreign_expr(PlannerInfo *root, RelOptInfo *baserel, Expr *expr)
+ {
+ foreign_executable_cxt context;
+ context.root = root;
+ context.foreignrel = baserel;
+
+ /*
+ * An expression which includes any mutable function can't be pushed down
+ * because it's result is not stable. For example, pushing now() down to
+ * remote side would cause confusion from the clock offset.
+ * If we have routine mapping infrastructure in future release, we will be
+ * able to choose function to be pushed down in finer granularity.
+ */
+ if (contain_mutable_functions((Node *) expr))
+ return false;
+
+ /*
+ * Check that the expression consists of nodes which are known as safe to
+ * be pushed down.
+ */
+ if (foreign_expr_walker((Node *) expr, &context))
+ return false;
+
+ return true;
+ }
+
+ /*
+ * Return true if node includes any node which is not known as safe to be
+ * pushed down.
+ */
+ static bool
+ foreign_expr_walker(Node *node, foreign_executable_cxt *context)
+ {
+ Oid func;
+
+ if (node == NULL)
+ return false;
+
+ /*
+ * If given expression has valid collation, it can't be pushed down bacause
+ * it might has uncompatible semantics on remote side.
+ */
+ if (exprCollation(node) != InvalidOid ||
+ exprInputCollation(node) != InvalidOid)
+ return true;
+
+ /*
+ * If return type of given expression is not built-in, it can't be pushed
+ * down bacause it might has uncompatible semantics on remote side.
+ */
+ if (!is_builtin(exprType(node)))
+ return true;
+
+ /*
+ * If function used by the expression is not built-in, it can't be pushed
+ * down bacause it might has uncompatible semantics on remote side.
+ */
+ func = exprFunction(node);
+ if (func != InvalidOid && !is_builtin(func))
+ return true;
+
+ switch (nodeTag(node))
+ {
+ case T_Const:
+ case T_BoolExpr:
+ case T_NullTest:
+ case T_DistinctExpr:
+ case T_RelabelType:
+ case T_FuncExpr:
+ /*
+ * 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 at the tail of this function.
+ */
+ break;
+ case T_Param:
+ /*
+ * Only external parameters can be pushed down.:
+ */
+ {
+ if (((Param *) node)->paramkind != PARAM_EXTERN)
+ return true;
+ }
+ break;
+ case T_ScalarArrayOpExpr:
+ /*
+ * Only built-in operators can be pushed down. In addition,
+ * underlying function must be built-in and immutable, but we don't
+ * check volatility here; such check must be done already with
+ * contain_mutable_functions.
+ */
+ {
+ ScalarArrayOpExpr *oe = (ScalarArrayOpExpr *) node;
+
+ if (!is_builtin(oe->opno))
+ return true;
+
+ /* operands are checked later */
+ }
+ break;
+ case T_OpExpr:
+ /*
+ * Only built-in operators can be pushed down. In addition,
+ * underlying function must be built-in and immutable, but we don't
+ * check volatility here; such check must be done already with
+ * contain_mutable_functions.
+ */
+ {
+ OpExpr *oe = (OpExpr *) node;
+
+ if (!is_builtin(oe->opno))
+ return true;
+
+ /* operands are checked later */
+ }
+ 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;
+ foreign_executable_cxt *f_context;
+
+ f_context = (foreign_executable_cxt *) context;
+ if (var->varno != f_context->foreignrel->relid ||
+ var->varlevelsup != 0)
+ return true;
+ }
+ break;
+ case T_ArrayRef:
+ /*
+ * ArrayRef which holds non-built-in typed elements can't be pushed
+ * down.
+ */
+ {
+ if (!is_builtin(((ArrayRef *) node)->refelemtype))
+ return true;
+ }
+ break;
+ case T_ArrayExpr:
+ /*
+ * ArrayExpr which holds non-built-in typed elements can't be pushed
+ * down.
+ */
+ {
+ if (!is_builtin(((ArrayExpr *) node)->element_typeid))
+ return true;
+ }
+ break;
+ default:
+ {
+ ereport(DEBUG3,
+ (errmsg("expression is too complex"),
+ errdetail("%s", nodeToString(node))));
+ return true;
+ }
+ break;
+ }
+
+ return expression_tree_walker(node, foreign_expr_walker, context);
+ }
+
+ /*
+ * Return true if given object is one of built-in objects.
+ */
+ static bool
+ is_builtin(Oid oid)
+ {
+ return (oid < FirstNormalObjectId);
+ }
+
diff --git a/contrib/pgsql_fdw/expected/pgsql_fdw.out b/contrib/pgsql_fdw/expected/pgsql_fdw.out
index 7aab32c..7a1e9e3 100644
*** a/contrib/pgsql_fdw/expected/pgsql_fdw.out
--- b/contrib/pgsql_fdw/expected/pgsql_fdw.out
*************** SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.
*** 218,229 ****
-- with WHERE clause
EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
! QUERY PLAN
! ------------------------------------------------------------------------------------------------------------------
Foreign Scan on public.ft1 t1
Output: c1, c2, c3, c4, c5, c6, c7
Filter: ((t1.c1 = 101) AND ((t1.c6)::text = '1'::text) AND (t1.c7 = '1'::bpchar))
! Remote SQL: DECLARE pgsql_fdw_cursor_4 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
(4 rows)
SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
--- 218,229 ----
-- with WHERE clause
EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
! QUERY PLAN
! ---------------------------------------------------------------------------------------------------------------------------------------
Foreign Scan on public.ft1 t1
Output: c1, c2, c3, c4, c5, c6, c7
Filter: ((t1.c1 = 101) AND ((t1.c6)::text = '1'::text) AND (t1.c7 = '1'::bpchar))
! Remote SQL: DECLARE pgsql_fdw_cursor_4 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE ("C 1" = 101)
(4 rows)
SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
*************** EXPLAIN (COSTS false) SELECT * FROM ft1
*** 331,349 ****
(3 rows)
EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = abs(t1.c2);
! QUERY PLAN
! -------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
Filter: (c1 = abs(c2))
! Remote SQL: DECLARE pgsql_fdw_cursor_20 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
(3 rows)
EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = t1.c2;
! QUERY PLAN
! -------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
Filter: (c1 = c2)
! Remote SQL: DECLARE pgsql_fdw_cursor_21 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
(3 rows)
DROP OPERATOR === (int, int) CASCADE;
--- 331,349 ----
(3 rows)
EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = abs(t1.c2);
! QUERY PLAN
! --------------------------------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
Filter: (c1 = abs(c2))
! Remote SQL: DECLARE pgsql_fdw_cursor_20 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE ("C 1" = abs(c2))
(3 rows)
EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = t1.c2;
! QUERY PLAN
! ---------------------------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
Filter: (c1 = c2)
! Remote SQL: DECLARE pgsql_fdw_cursor_21 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE ("C 1" = c2)
(3 rows)
DROP OPERATOR === (int, int) CASCADE;
*************** DROP FUNCTION pgsql_fdw_abs(int);
*** 354,370 ****
-- simple join
PREPARE st1(int, int) AS SELECT t1.c3, t2.c3 FROM ft1 t1, ft2 t2 WHERE t1.c1 = $1 AND t2.c1 = $2;
EXPLAIN (COSTS false) EXECUTE st1(1, 2);
! QUERY PLAN
! -----------------------------------------------------------------------------------------------------------------------------------------
Nested Loop
-> Foreign Scan on ft1 t1
Filter: (c1 = 1)
! Remote SQL: DECLARE pgsql_fdw_cursor_22 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, NULL, NULL, NULL, NULL FROM "S 1"."T 1"
! -> Materialize
! -> Foreign Scan on ft2 t2
! Filter: (c1 = 2)
! Remote SQL: DECLARE pgsql_fdw_cursor_23 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, NULL, NULL, NULL, NULL FROM "S 1"."T 1"
! (8 rows)
EXECUTE st1(1, 1);
c3 | c3
--- 354,369 ----
-- simple join
PREPARE st1(int, int) AS SELECT t1.c3, t2.c3 FROM ft1 t1, ft2 t2 WHERE t1.c1 = $1 AND t2.c1 = $2;
EXPLAIN (COSTS false) EXECUTE st1(1, 2);
! QUERY PLAN
! ------------------------------------------------------------------------------------------------------------------------------------------------------
Nested Loop
-> Foreign Scan on ft1 t1
Filter: (c1 = 1)
! Remote SQL: DECLARE pgsql_fdw_cursor_22 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, NULL, NULL, NULL, NULL FROM "S 1"."T 1" WHERE ("C 1" = 1)
! -> Foreign Scan on ft2 t2
! Filter: (c1 = 2)
! Remote SQL: DECLARE pgsql_fdw_cursor_23 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, NULL, NULL, NULL, NULL FROM "S 1"."T 1" WHERE ("C 1" = 2)
! (7 rows)
EXECUTE st1(1, 1);
c3 | c3
*************** EXECUTE st1(101, 101);
*** 381,400 ****
-- subquery using stable function (can't be pushed down)
PREPARE st2(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c4) = 6) ORDER BY c1;
EXPLAIN (COSTS false) EXECUTE st2(10, 20);
! QUERY PLAN
! ---------------------------------------------------------------------------------------------------------------------------------------------------
Sort
Sort Key: t1.c1
-> Hash Join
Hash Cond: (t1.c3 = t2.c3)
-> Foreign Scan on ft1 t1
Filter: (c1 < 20)
! Remote SQL: DECLARE pgsql_fdw_cursor_28 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
-> Hash
-> HashAggregate
-> Foreign Scan on ft2 t2
Filter: ((c1 > 10) AND (date_part('dow'::text, c4) = 6::double precision))
! Remote SQL: DECLARE pgsql_fdw_cursor_29 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, c4, NULL, NULL, NULL FROM "S 1"."T 1"
(12 rows)
EXECUTE st2(10, 20);
--- 380,399 ----
-- subquery using stable function (can't be pushed down)
PREPARE st2(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c4) = 6) ORDER BY c1;
EXPLAIN (COSTS false) EXECUTE st2(10, 20);
! QUERY PLAN
! -----------------------------------------------------------------------------------------------------------------------------------------------------------------------
Sort
Sort Key: t1.c1
-> Hash Join
Hash Cond: (t1.c3 = t2.c3)
-> Foreign Scan on ft1 t1
Filter: (c1 < 20)
! Remote SQL: DECLARE pgsql_fdw_cursor_28 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE ("C 1" < 20)
-> Hash
-> HashAggregate
-> Foreign Scan on ft2 t2
Filter: ((c1 > 10) AND (date_part('dow'::text, c4) = 6::double precision))
! Remote SQL: DECLARE pgsql_fdw_cursor_29 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, c4, NULL, NULL, NULL FROM "S 1"."T 1" WHERE ("C 1" > 10)
(12 rows)
EXECUTE st2(10, 20);
*************** EXECUTE st1(101, 101);
*** 412,431 ****
-- subquery using immutable function (can be pushed down)
PREPARE st3(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c5) = 6) ORDER BY c1;
EXPLAIN (COSTS false) EXECUTE st3(10, 20);
! QUERY PLAN
! ---------------------------------------------------------------------------------------------------------------------------------------------------
Sort
Sort Key: t1.c1
-> Hash Join
Hash Cond: (t1.c3 = t2.c3)
-> Foreign Scan on ft1 t1
Filter: (c1 < 20)
! Remote SQL: DECLARE pgsql_fdw_cursor_34 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
-> Hash
-> HashAggregate
-> Foreign Scan on ft2 t2
Filter: ((c1 > 10) AND (date_part('dow'::text, c5) = 6::double precision))
! Remote SQL: DECLARE pgsql_fdw_cursor_35 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, NULL, c5, NULL, NULL FROM "S 1"."T 1"
(12 rows)
EXECUTE st3(10, 20);
--- 411,430 ----
-- subquery using immutable function (can be pushed down)
PREPARE st3(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c5) = 6) ORDER BY c1;
EXPLAIN (COSTS false) EXECUTE st3(10, 20);
! QUERY PLAN
! -----------------------------------------------------------------------------------------------------------------------------------------------------------------------
Sort
Sort Key: t1.c1
-> Hash Join
Hash Cond: (t1.c3 = t2.c3)
-> Foreign Scan on ft1 t1
Filter: (c1 < 20)
! Remote SQL: DECLARE pgsql_fdw_cursor_34 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE ("C 1" < 20)
-> Hash
-> HashAggregate
-> Foreign Scan on ft2 t2
Filter: ((c1 > 10) AND (date_part('dow'::text, c5) = 6::double precision))
! Remote SQL: DECLARE pgsql_fdw_cursor_35 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, NULL, c5, NULL, NULL FROM "S 1"."T 1" WHERE ("C 1" > 10)
(12 rows)
EXECUTE st3(10, 20);
*************** EXECUTE st3(20, 30);
*** 443,493 ****
-- custom plan should be chosen
PREPARE st4(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 = $1;
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! -------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
Filter: (c1 = 1)
! Remote SQL: DECLARE pgsql_fdw_cursor_40 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
(3 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! -------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
Filter: (c1 = 1)
! Remote SQL: DECLARE pgsql_fdw_cursor_41 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
(3 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! -------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
Filter: (c1 = 1)
! Remote SQL: DECLARE pgsql_fdw_cursor_42 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
(3 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! -------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
Filter: (c1 = 1)
! Remote SQL: DECLARE pgsql_fdw_cursor_43 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
(3 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! -------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
Filter: (c1 = 1)
! Remote SQL: DECLARE pgsql_fdw_cursor_44 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
(3 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! -------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
Filter: (c1 = 1)
! Remote SQL: DECLARE pgsql_fdw_cursor_46 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
(3 rows)
-- cleanup
--- 442,492 ----
-- custom plan should be chosen
PREPARE st4(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 = $1;
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! --------------------------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
Filter: (c1 = 1)
! Remote SQL: DECLARE pgsql_fdw_cursor_40 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE ("C 1" = 1)
(3 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! --------------------------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
Filter: (c1 = 1)
! Remote SQL: DECLARE pgsql_fdw_cursor_41 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE ("C 1" = 1)
(3 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! --------------------------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
Filter: (c1 = 1)
! Remote SQL: DECLARE pgsql_fdw_cursor_42 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE ("C 1" = 1)
(3 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! --------------------------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
Filter: (c1 = 1)
! Remote SQL: DECLARE pgsql_fdw_cursor_43 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE ("C 1" = 1)
(3 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! --------------------------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
Filter: (c1 = 1)
! Remote SQL: DECLARE pgsql_fdw_cursor_44 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE ("C 1" = 1)
(3 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! --------------------------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
Filter: (c1 = 1)
! Remote SQL: DECLARE pgsql_fdw_cursor_46 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE ("C 1" = 1)
(3 rows)
-- cleanup
diff --git a/doc/src/sgml/pgsql-fdw.sgml b/doc/src/sgml/pgsql-fdw.sgml
index 299f4b6..1715257 100644
*** a/doc/src/sgml/pgsql-fdw.sgml
--- b/doc/src/sgml/pgsql-fdw.sgml
*************** postgres=# SELECT pgsql_fdw_disconnect(s
*** 225,235 ****
</para>
<synopsis>
postgres=# EXPLAIN SELECT aid FROM pgbench_accounts WHERE abalance < 0;
! QUERY PLAN
! --------------------------------------------------------------------------------------------------------------------------
! Foreign Scan on pgbench_accounts (cost=100.00..8046.37 rows=301037 width=8)
Filter: (abalance < 0)
! Remote SQL: DECLARE pgsql_fdw_cursor_1 SCROLL CURSOR FOR SELECT aid, NULL, abalance, NULL FROM public.pgbench_accounts
(3 rows)
</synopsis>
</sect2>
--- 225,235 ----
</para>
<synopsis>
postgres=# EXPLAIN SELECT aid FROM pgbench_accounts WHERE abalance < 0;
! QUERY PLAN
! -------------------------------------------------------------------------------------------------------------------------------------------------
! Foreign Scan on pgbench_accounts (cost=100.00..8798.96 rows=1 width=97)
Filter: (abalance < 0)
! Remote SQL: DECLARE pgsql_fdw_cursor_2 SCROLL CURSOR FOR SELECT aid, bid, abalance, filler FROM public.pgbench_accounts WHERE (abalance < 0)
(3 rows)
</synopsis>
</sect2>
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index 51459c4..c017d6b 100644
*** a/src/backend/nodes/nodeFuncs.c
--- b/src/backend/nodes/nodeFuncs.c
*************** leftmostLoc(int loc1, int loc2)
*** 1405,1410 ****
--- 1405,1449 ----
return Min(loc1, loc2);
}
+ /*
+ * exprFunction -
+ * returns the Oid of the function used by the expression.
+ *
+ * Result is InvalidOid if the node type doesn't store this information.
+ */
+ Oid
+ exprFunction(const Node *expr)
+ {
+ Oid func;
+
+ if (!expr)
+ return InvalidOid;
+
+ switch (nodeTag(expr))
+ {
+ case T_Aggref:
+ func = ((const Aggref *) expr)->aggfnoid;
+ break;
+ case T_WindowFunc:
+ func = ((const WindowFunc *) expr)->winfnoid;
+ break;
+ case T_FuncExpr:
+ func = ((const FuncExpr *) expr)->funcid;
+ break;
+ case T_OpExpr:
+ func = ((const OpExpr *) expr)->opfuncid;
+ break;
+ case T_ScalarArrayOpExpr:
+ func = ((const ScalarArrayOpExpr *) expr)->opfuncid;
+ break;
+ case T_ArrayCoerceExpr:
+ func = ((const ArrayCoerceExpr *) expr)->elemfuncid;
+ break;
+ default:
+ func = InvalidOid;
+ }
+ return func;
+ }
/*
* Standard expression-tree walking support
diff --git a/src/include/nodes/nodeFuncs.h b/src/include/nodes/nodeFuncs.h
index def4f31..a3ebe5c 100644
*** a/src/include/nodes/nodeFuncs.h
--- b/src/include/nodes/nodeFuncs.h
*************** extern void exprSetInputCollation(Node *
*** 38,43 ****
--- 38,45 ----
extern int exprLocation(const Node *expr);
+ extern Oid exprFunction(const Node *expr);
+
extern bool expression_tree_walker(Node *node, bool (*walker) (),
void *context);
extern Node *expression_tree_mutator(Node *node, Node *(*mutator) (),
pgsql_fdw_v6.patchtext/plain; name=pgsql_fdw_v6.patchDownload
diff --git a/contrib/Makefile b/contrib/Makefile
index ac0a80a..14aa433 100644
*** a/contrib/Makefile
--- b/contrib/Makefile
*************** SUBDIRS = \
*** 41,46 ****
--- 41,47 ----
pgbench \
pgcrypto \
pgrowlocks \
+ pgsql_fdw \
pgstattuple \
seg \
spi \
diff --git a/contrib/README b/contrib/README
index a1d42a1..d3fa211 100644
*** a/contrib/README
--- b/contrib/README
*************** pgrowlocks -
*** 158,163 ****
--- 158,167 ----
A function to return row locking information
by Tatsuo Ishii <ishii@sraoss.co.jp>
+ pgsql_fdw -
+ Foreign-data wrapper for external PostgreSQL servers.
+ by Shigeru Hanada <shigeru.hanada@gmail.com>
+
pgstattuple -
Functions to return statistics about "dead" tuples and free
space within a table
diff --git a/contrib/pgsql_fdw/.gitignore b/contrib/pgsql_fdw/.gitignore
index ...0854728 .
*** a/contrib/pgsql_fdw/.gitignore
--- b/contrib/pgsql_fdw/.gitignore
***************
*** 0 ****
--- 1,4 ----
+ # Generated subdirectories
+ /results/
+ *.o
+ *.so
diff --git a/contrib/pgsql_fdw/Makefile b/contrib/pgsql_fdw/Makefile
index ...6381365 .
*** a/contrib/pgsql_fdw/Makefile
--- b/contrib/pgsql_fdw/Makefile
***************
*** 0 ****
--- 1,22 ----
+ # contrib/pgsql_fdw/Makefile
+
+ MODULE_big = pgsql_fdw
+ OBJS = pgsql_fdw.o option.o deparse.o connection.o
+ PG_CPPFLAGS = -I$(libpq_srcdir)
+ SHLIB_LINK = $(libpq)
+
+ EXTENSION = pgsql_fdw
+ DATA = pgsql_fdw--1.0.sql
+
+ REGRESS = pgsql_fdw
+
+ ifdef USE_PGXS
+ PG_CONFIG = pg_config
+ PGXS := $(shell $(PG_CONFIG) --pgxs)
+ include $(PGXS)
+ else
+ subdir = contrib/pgsql_fdw
+ top_builddir = ../..
+ include $(top_builddir)/src/Makefile.global
+ include $(top_srcdir)/contrib/contrib-global.mk
+ endif
diff --git a/contrib/pgsql_fdw/connection.c b/contrib/pgsql_fdw/connection.c
index ...99d8fad .
*** a/contrib/pgsql_fdw/connection.c
--- b/contrib/pgsql_fdw/connection.c
***************
*** 0 ****
--- 1,558 ----
+ /*-------------------------------------------------------------------------
+ *
+ * connection.c
+ * Connection management for pgsql_fdw
+ *
+ * Portions Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/connection.c
+ *
+ *-------------------------------------------------------------------------
+ */
+ #include "postgres.h"
+
+ #include "access/xact.h"
+ #include "catalog/pg_type.h"
+ #include "foreign/foreign.h"
+ #include "funcapi.h"
+ #include "libpq-fe.h"
+ #include "mb/pg_wchar.h"
+ #include "miscadmin.h"
+ #include "utils/array.h"
+ #include "utils/builtins.h"
+ #include "utils/hsearch.h"
+ #include "utils/memutils.h"
+ #include "utils/resowner.h"
+ #include "utils/tuplestore.h"
+
+ #include "pgsql_fdw.h"
+ #include "connection.h"
+
+ /* ============================================================================
+ * Connection management functions
+ * ==========================================================================*/
+
+ /*
+ * Connection cache entry managed with hash table.
+ */
+ typedef struct ConnCacheEntry
+ {
+ /* hash key must be first */
+ Oid serverid; /* oid of foreign server */
+ Oid userid; /* oid of local user */
+
+ bool use_tx; /* true when using remote transaction */
+ 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);
+ static void begin_remote_tx(PGconn *conn);
+ static void abort_remote_tx(PGconn *conn);
+
+ /*
+ * 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.
+ *
+ * When use_tx is true, remote transaction is started if caller is the only
+ * user of the connection. Isolation level of the remote transaction is same
+ * as local transaction, and remote transaction will be aborted when last
+ * user release.
+ *
+ * TODO: Note that caching connections requires a mechanism to detect change of
+ * FDW object to invalidate already established connections.
+ */
+ PGconn *
+ GetConnection(ForeignServer *server, UserMapping *user, bool use_tx)
+ {
+ bool found;
+ ConnCacheEntry *entry;
+ ConnCacheEntry key;
+
+ /* initialize connection cache if it isn't */
+ if (FSConnectionHash == NULL)
+ {
+ HASHCTL ctl;
+
+ /* hash key is a pair of oids: serverid and userid */
+ MemSet(&ctl, 0, sizeof(ctl));
+ ctl.keysize = sizeof(Oid) + sizeof(Oid);
+ ctl.entrysize = sizeof(ConnCacheEntry);
+ ctl.hash = tag_hash;
+ ctl.match = memcmp;
+ ctl.keycopy = memcpy;
+ /* allocate FSConnectionHash in the cache context */
+ ctl.hcxt = CacheMemoryContext;
+ FSConnectionHash = hash_create("Foreign Connections", 32,
+ &ctl,
+ HASH_ELEM | HASH_CONTEXT |
+ HASH_FUNCTION | HASH_COMPARE |
+ HASH_KEYCOPY);
+ }
+
+ /* Create key value for the entry. */
+ MemSet(&key, 0, sizeof(key));
+ key.serverid = server->serverid;
+ key.userid = GetOuterUserId();
+
+ /*
+ * Find cached entry for requested connection. If we couldn't find,
+ * callback function of ResourceOwner should be registered to clean the
+ * connection up on error including user interrupt.
+ */
+ entry = hash_search(FSConnectionHash, &key, HASH_ENTER, &found);
+ if (!found)
+ {
+ entry->refs = 0;
+ entry->use_tx = false;
+ entry->conn = NULL;
+ RegisterResourceReleaseCallback(cleanup_connection, entry);
+ }
+
+ /*
+ * We don't check the health of cached connection here, because it would
+ * require some overhead. Broken connection and its cache entry will be
+ * cleaned up when the connection is actually used.
+ */
+
+ /*
+ * If cache entry doesn't have connection, we have to establish new
+ * connection.
+ */
+ if (entry->conn == NULL)
+ {
+ /*
+ * Use PG_TRY block to ensure closing connection on error.
+ */
+ PG_TRY();
+ {
+ /*
+ * Connect to the foreign PostgreSQL server, and store it in cache
+ * entry to keep new connection.
+ * Note: key items of entry has already been initialized in
+ * hash_search(HASH_ENTER).
+ */
+ entry->conn = connect_pg_server(server, user);
+ }
+ PG_CATCH();
+ {
+ /* Clear connection cache entry on error case. */
+ PQfinish(entry->conn);
+ entry->refs = 0;
+ entry->use_tx = false;
+ entry->conn = NULL;
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+ }
+
+ /* Increment connection reference counter. */
+ entry->refs++;
+
+
+ /*
+ * If requester is the only referrer of this connection, start transaction
+ * with the same isolation level as the local transaction we are in.
+ * Before starting transaction, we abort old transaction just in case.
+ * We need to remember whether this connection uses remote transaction to
+ * abort it when this connection is released completely.
+ */
+ if (use_tx && entry->refs == 1)
+ {
+ abort_remote_tx(entry->conn);
+ begin_remote_tx(entry->conn);
+ }
+ entry->use_tx = use_tx;
+
+
+ return entry->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 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. Those two object hold only libpq options.
+ * Extra 3 items are for:
+ * *) fallback_application_name
+ * *) client_encoding
+ * *) NULL termination (end marker)
+ *
+ * Note: We don't omit any parameters even target database might be older
+ * than local, because unexpected parameters are just ignored.
+ */
+ n = list_length(server->options) + list_length(user->options) + 3;
+ 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 += ExtractConnectionOptions(server->options,
+ all_keywords + n, all_values + n);
+ n += ExtractConnectionOptions(user->options,
+ all_keywords + n, all_values + n);
+ all_keywords[n] = all_values[n] = NULL;
+
+ for (i = 0, j = 0; all_keywords[i]; i++)
+ {
+ keywords[j] = all_keywords[i];
+ values[j] = all_values[i];
+ j++;
+ }
+
+ /* Use "pgsql_fdw" as fallback_application_name. */
+ keywords[j] = "fallback_application_name";
+ values[j++] = "pgsql_fdw";
+
+ /* Set client_encoding so that libpq can convert encoding properly. */
+ keywords[j] = "client_encoding";
+ values[j++] = GetDatabaseEncodingName();
+
+ keywords[j] = values[j] = NULL;
+ pfree(all_keywords);
+ pfree(all_values);
+
+ /* 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.")));
+ }
+
+ return conn;
+ }
+
+ /*
+ * Start transaction to use cursor to retrieve data separately.
+ */
+ static void
+ begin_remote_tx(PGconn *conn)
+ {
+ const char *sql;
+ PGresult *res;
+
+ switch (XactIsoLevel)
+ {
+ case XACT_READ_UNCOMMITTED:
+ sql = "START TRANSACTION ISOLATION LEVEL READ UNCOMMITTED";
+ break;
+ case XACT_READ_COMMITTED:
+ sql = "START TRANSACTION ISOLATION LEVEL READ COMMITTED";
+ break;
+ case XACT_REPEATABLE_READ:
+ sql = "START TRANSACTION ISOLATION LEVEL REPEATABLE READ";
+ break;
+ case XACT_SERIALIZABLE:
+ sql = "START TRANSACTION ISOLATION LEVEL SERIALIZABLE";
+ break;
+ default:
+ elog(ERROR, "unexpected isolation level: %d", XactIsoLevel);
+ break;
+ }
+
+ elog(DEBUG3, "starting remote transaction with \"%s\"", sql);
+
+ res = PQexec(conn, sql);
+ if (PQresultStatus(res) != PGRES_COMMAND_OK)
+ {
+ PQclear(res);
+ elog(ERROR, "could not start transaction: %s", PQerrorMessage(conn));
+ }
+ PQclear(res);
+ }
+
+ static void
+ abort_remote_tx(PGconn *conn)
+ {
+ PGresult *res;
+
+ elog(DEBUG3, "aborting remote transaction");
+
+ res = PQexec(conn, "ABORT TRANSACTION");
+ if (PQresultStatus(res) != PGRES_COMMAND_OK)
+ {
+ PQclear(res);
+ elog(ERROR, "could not abort transaction: %s", PQerrorMessage(conn));
+ }
+ PQclear(res);
+ }
+
+ /*
+ * 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 sequentially 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;
+ }
+ if (entry != NULL)
+ hash_seq_term(&scan);
+
+ /*
+ * If this connection uses remote transaction and caller is the last user,
+ * abort remote transaction and forget about it.
+ */
+ if (entry->use_tx && entry->refs == 1)
+ {
+ abort_remote_tx(conn);
+ entry->use_tx = false;
+ }
+
+ /*
+ * If the released connection was an orphan, just close it.
+ */
+ if (entry == NULL)
+ {
+ PQfinish(conn);
+ return;
+ }
+
+ /*
+ * Decrement reference counter of this connection. Even if the caller was
+ * the last referrer, we don't unregister it from cache.
+ */
+ entry->refs--;
+ }
+
+ /*
+ * 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)
+ {
+ PQfinish(entry->conn);
+ entry->refs = 0;
+ entry->conn = NULL;
+ }
+ }
+
+ /*
+ * Get list of connections currently active.
+ */
+ Datum pgsql_fdw_get_connections(PG_FUNCTION_ARGS);
+ PG_FUNCTION_INFO_V1(pgsql_fdw_get_connections);
+ Datum
+ pgsql_fdw_get_connections(PG_FUNCTION_ARGS)
+ {
+ ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+ HASH_SEQ_STATUS scan;
+ ConnCacheEntry *entry;
+ MemoryContext oldcontext = CurrentMemoryContext;
+ Tuplestorestate *tuplestore;
+ TupleDesc tupdesc;
+
+ /* We return list of connection with storing them in a Tuplestore. */
+ rsinfo->returnMode = SFRM_Materialize;
+ rsinfo->setResult = NULL;
+ rsinfo->setDesc = NULL;
+
+ /* Create tuplestore and copy of TupleDesc in per-query context. */
+ MemoryContextSwitchTo(rsinfo->econtext->ecxt_per_query_memory);
+
+ tupdesc = CreateTemplateTupleDesc(2, false);
+ TupleDescInitEntry(tupdesc, 1, "srvid", OIDOID, -1, 0);
+ TupleDescInitEntry(tupdesc, 2, "usesysid", OIDOID, -1, 0);
+ rsinfo->setDesc = tupdesc;
+
+ tuplestore = tuplestore_begin_heap(false, false, work_mem);
+ rsinfo->setResult = tuplestore;
+
+ MemoryContextSwitchTo(oldcontext);
+
+ /*
+ * We need to scan sequentially since we use the address to find
+ * appropriate PGconn from the hash table.
+ */
+ if (FSConnectionHash != NULL)
+ {
+ hash_seq_init(&scan, FSConnectionHash);
+ while ((entry = (ConnCacheEntry *) hash_seq_search(&scan)))
+ {
+ Datum values[2];
+ bool nulls[2];
+ HeapTuple tuple;
+
+ /* Ignore inactive connections */
+ if (PQstatus(entry->conn) != CONNECTION_OK)
+ continue;
+
+ /*
+ * Ignore other users' connections if current user isn't a
+ * superuser.
+ */
+ if (!superuser() && entry->userid != GetUserId())
+ continue;
+
+ values[0] = ObjectIdGetDatum(entry->serverid);
+ values[1] = ObjectIdGetDatum(entry->userid);
+ nulls[0] = false;
+ nulls[1] = false;
+
+ tuple = heap_formtuple(tupdesc, values, nulls);
+ tuplestore_puttuple(tuplestore, tuple);
+ }
+ }
+ tuplestore_donestoring(tuplestore);
+
+ PG_RETURN_VOID();
+ }
+
+ /*
+ * Discard persistent connection designated by given connection name.
+ */
+ Datum pgsql_fdw_disconnect(PG_FUNCTION_ARGS);
+ PG_FUNCTION_INFO_V1(pgsql_fdw_disconnect);
+ Datum
+ pgsql_fdw_disconnect(PG_FUNCTION_ARGS)
+ {
+ Oid serverid = PG_GETARG_OID(0);
+ Oid userid = PG_GETARG_OID(1);
+ ConnCacheEntry key;
+ ConnCacheEntry *entry = NULL;
+ bool found;
+
+ /* Non-superuser can't discard other users' connection. */
+ if (!superuser() && userid != GetOuterUserId())
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_RESOURCES),
+ errmsg("only superuser can discard other user's connection")));
+
+ /*
+ * If no connection has been established, or no such connections, just
+ * return "NG" to indicate nothing has done.
+ */
+ if (FSConnectionHash == NULL)
+ PG_RETURN_TEXT_P(cstring_to_text("NG"));
+
+ key.serverid = serverid;
+ key.userid = userid;
+ entry = hash_search(FSConnectionHash, &key, HASH_FIND, &found);
+ if (!found)
+ PG_RETURN_TEXT_P(cstring_to_text("NG"));
+
+ /* Discard cached connection, and clear reference counter. */
+ PQfinish(entry->conn);
+ entry->refs = 0;
+ entry->conn = NULL;
+
+ PG_RETURN_TEXT_P(cstring_to_text("OK"));
+ }
diff --git a/contrib/pgsql_fdw/connection.h b/contrib/pgsql_fdw/connection.h
index ...4427f56 .
*** a/contrib/pgsql_fdw/connection.h
--- b/contrib/pgsql_fdw/connection.h
***************
*** 0 ****
--- 1,25 ----
+ /*-------------------------------------------------------------------------
+ *
+ * connection.h
+ * Connection management for pgsql_fdw
+ *
+ * Portions Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_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, bool use_tx);
+ void ReleaseConnection(PGconn *conn);
+
+ #endif /* CONNECTION_H */
diff --git a/contrib/pgsql_fdw/deparse.c b/contrib/pgsql_fdw/deparse.c
index ...cf9e775 .
*** a/contrib/pgsql_fdw/deparse.c
--- b/contrib/pgsql_fdw/deparse.c
***************
*** 0 ****
--- 1,199 ----
+ /*-------------------------------------------------------------------------
+ *
+ * deparse.c
+ * query deparser for PostgreSQL
+ *
+ * Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/deparse.c
+ *
+ *-------------------------------------------------------------------------
+ */
+ #include "postgres.h"
+
+ #include "access/transam.h"
+ #include "foreign/foreign.h"
+ #include "lib/stringinfo.h"
+ #include "nodes/nodeFuncs.h"
+ #include "nodes/nodes.h"
+ #include "nodes/makefuncs.h"
+ #include "optimizer/clauses.h"
+ #include "optimizer/var.h"
+ #include "parser/parsetree.h"
+ #include "utils/builtins.h"
+ #include "utils/lsyscache.h"
+
+ #include "pgsql_fdw.h"
+
+ /*
+ * Context for walk-through the expression tree.
+ */
+ typedef struct foreign_executable_cxt
+ {
+ PlannerInfo *root;
+ RelOptInfo *foreignrel;
+ } foreign_executable_cxt;
+
+ /*
+ * Deparse query representation into SQL statement which suits for remote
+ * PostgreSQL server.
+ */
+ char *
+ deparseSql(Oid relid, PlannerInfo *root, RelOptInfo *baserel)
+ {
+ StringInfoData foreign_relname;
+ StringInfoData sql; /* builder for SQL statement */
+ bool first;
+ AttrNumber attr;
+ List *attr_used = NIL; /* List of AttNumber used in the query */
+ const char *nspname = NULL; /* plain namespace name */
+ const char *relname = NULL; /* plain relation name */
+ const char *q_nspname; /* quoted namespace name */
+ const char *q_relname; /* quoted relation name */
+ int i;
+ List *rtable = NIL;
+ List *context = NIL;
+
+ initStringInfo(&sql);
+ initStringInfo(&foreign_relname);
+
+ /*
+ * First of all, determine which column should be retrieved for this scan.
+ *
+ * We do this before deparsing SELECT clause because attributes which are
+ * not used in neither reltargetlist nor baserel->baserestrictinfo, quals
+ * evaluated on local, can be replaced with literal "NULL" in the SELECT
+ * clause to reduce overhead of tuple handling tuple and data transfer.
+ */
+ if (baserel->baserestrictinfo != NIL)
+ {
+ ListCell *lc;
+
+ foreach (lc, baserel->baserestrictinfo)
+ {
+ RestrictInfo *ri = (RestrictInfo *) lfirst(lc);
+ List *attrs;
+
+ /*
+ * We need to know which attributes are used in qual evaluated
+ * on the local server, because they should be listed in the
+ * SELECT clause of remote query. We can ignore attributes
+ * which are referenced only in ORDER BY/GROUP BY clause because
+ * such attributes has already been kept in reltargetlist.
+ */
+ attrs = pull_var_clause((Node *) ri->clause,
+ PVC_RECURSE_AGGREGATES,
+ PVC_RECURSE_PLACEHOLDERS);
+ attr_used = list_union(attr_used, attrs);
+ }
+ }
+
+ /*
+ * Determine foreign relation's qualified name. This is necessary for
+ * FROM clause and SELECT clause.
+ */
+ nspname = GetFdwOptionValue(InvalidOid, InvalidOid, relid,
+ InvalidAttrNumber, "nspname");
+ if (nspname == NULL)
+ nspname = get_namespace_name(get_rel_namespace(relid));
+ q_nspname = quote_identifier(nspname);
+
+ relname = GetFdwOptionValue(InvalidOid, InvalidOid, relid,
+ InvalidAttrNumber, "relname");
+ if (relname == NULL)
+ relname = get_rel_name(relid);
+ q_relname = quote_identifier(relname);
+
+ appendStringInfo(&foreign_relname, "%s.%s", q_nspname, q_relname);
+
+ /*
+ * We need to replace aliasname and colnames of the target relation so that
+ * constructed remote query is valid.
+ *
+ * Note that we skip first empty element of simple_rel_array. See also
+ * comments of simple_rel_array and simple_rte_array for the rationale.
+ */
+ for (i = 1; i < root->simple_rel_array_size; i++)
+ {
+ RangeTblEntry *rte = copyObject(root->simple_rte_array[i]);
+ List *newcolnames = NIL;
+
+ if (i == baserel->relid)
+ {
+ /*
+ * Create new list of column names which is used to deparse remote
+ * query from specified names or local column names. This list is
+ * used by deparse_expression.
+ */
+ for (attr = 1; attr <= baserel->max_attr; attr++)
+ {
+ char *colname;
+
+ /* Ignore dropped attributes. */
+ if (get_rte_attribute_is_dropped(rte, attr))
+ continue;
+
+ colname = GetFdwOptionValue(InvalidOid, InvalidOid, relid,
+ attr, "colname");
+ if (colname == NULL)
+ colname = strVal(list_nth(rte->eref->colnames, attr - 1));
+ newcolnames = lappend(newcolnames, makeString(colname));
+ }
+ rte->alias = makeAlias(relname, newcolnames);
+ }
+ rtable = lappend(rtable, rte);
+ }
+ context = deparse_context_for_rtelist(rtable);
+
+ /*
+ * deparse SELECT clause
+ *
+ * List attributes which are in either target list or local restriction.
+ * Unused attributes are replaced with a literal "NULL" for optimization.
+ */
+ appendStringInfo(&sql, "SELECT ");
+ attr_used = list_union(attr_used, baserel->reltargetlist);
+ first = true;
+ for (attr = 1; attr <= baserel->max_attr; attr++)
+ {
+ RangeTblEntry *rte = root->simple_rte_array[baserel->relid];
+ Var *var = NULL;
+ ListCell *lc;
+
+ /* Ignore dropped attributes. */
+ if (get_rte_attribute_is_dropped(rte, attr))
+ continue;
+
+ if (!first)
+ appendStringInfo(&sql, ", ");
+ first = false;
+
+ /*
+ * We use linear search here, but it wouldn't be problem since
+ * attr_used seems to not become so large.
+ */
+ foreach (lc, attr_used)
+ {
+ var = lfirst(lc);
+ if (var->varattno == attr)
+ break;
+ var = NULL;
+ }
+ if (var != NULL)
+ appendStringInfo(&sql, "%s",
+ deparse_expression((Node *) var, context, false, false));
+ else
+ appendStringInfo(&sql, "NULL");
+ }
+ appendStringInfoChar(&sql, ' ');
+
+ /*
+ * deparse FROM clause, including alias if any
+ */
+ appendStringInfo(&sql, "FROM %s", foreign_relname.data);
+
+ elog(DEBUG3, "Remote SQL: %s", sql.data);
+ return sql.data;
+ }
+
diff --git a/contrib/pgsql_fdw/expected/pgsql_fdw.out b/contrib/pgsql_fdw/expected/pgsql_fdw.out
index ...7aab32c .
*** a/contrib/pgsql_fdw/expected/pgsql_fdw.out
--- b/contrib/pgsql_fdw/expected/pgsql_fdw.out
***************
*** 0 ****
--- 1,528 ----
+ -- ===================================================================
+ -- create FDW objects
+ -- ===================================================================
+ CREATE EXTENSION pgsql_fdw;
+ CREATE SERVER loopback1 FOREIGN DATA WRAPPER pgsql_fdw;
+ CREATE SERVER loopback2 FOREIGN DATA WRAPPER pgsql_fdw
+ OPTIONS (dbname 'contrib_regression');
+ CREATE USER MAPPING FOR public SERVER loopback1
+ OPTIONS (user 'value', password 'value');
+ CREATE USER MAPPING FOR public SERVER loopback2;
+ CREATE FOREIGN TABLE ft1 (
+ c1 int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10)
+ ) SERVER loopback2;
+ CREATE FOREIGN TABLE ft2 (
+ c1 int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10)
+ ) SERVER loopback2;
+ -- ===================================================================
+ -- create objects used through FDW
+ -- ===================================================================
+ CREATE SCHEMA "S 1";
+ CREATE TABLE "S 1"."T 1" (
+ "C 1" int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10),
+ CONSTRAINT t1_pkey PRIMARY KEY ("C 1")
+ );
+ NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "t1_pkey" for table "T 1"
+ CREATE TABLE "S 1"."T 2" (
+ c1 int NOT NULL,
+ c2 text,
+ CONSTRAINT t2_pkey PRIMARY KEY (c1)
+ );
+ NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "t2_pkey" for table "T 2"
+ BEGIN;
+ TRUNCATE "S 1"."T 1";
+ INSERT INTO "S 1"."T 1"
+ SELECT id,
+ id % 10,
+ to_char(id, 'FM00000'),
+ '1970-01-01'::timestamptz + ((id % 100) || ' days')::interval,
+ '1970-01-01'::timestamp + ((id % 100) || ' days')::interval,
+ id % 10,
+ id % 10
+ FROM generate_series(1, 1000) id;
+ TRUNCATE "S 1"."T 2";
+ INSERT INTO "S 1"."T 2"
+ SELECT id,
+ 'AAA' || to_char(id, 'FM000')
+ FROM generate_series(1, 100) id;
+ COMMIT;
+ -- ===================================================================
+ -- tests for pgsql_fdw_validator
+ -- ===================================================================
+ ALTER FOREIGN DATA WRAPPER pgsql_fdw OPTIONS (host 'value'); -- ERROR
+ ERROR: invalid option "host"
+ HINT: Valid options in this context are:
+ -- requiressl, krbsrvname and gsslib are omitted because they depend on
+ -- configure option
+ ALTER SERVER loopback1 OPTIONS (
+ authtype 'value',
+ service 'value',
+ connect_timeout 'value',
+ dbname 'value',
+ host 'value',
+ hostaddr 'value',
+ port 'value',
+ --client_encoding 'value',
+ tty 'value',
+ options 'value',
+ application_name 'value',
+ --fallback_application_name 'value',
+ keepalives 'value',
+ keepalives_idle 'value',
+ keepalives_interval 'value',
+ -- requiressl 'value',
+ sslmode 'value',
+ sslcert 'value',
+ sslkey 'value',
+ sslrootcert 'value',
+ sslcrl 'value'
+ --requirepeer 'value',
+ -- krbsrvname 'value',
+ -- gsslib 'value',
+ --replication 'value'
+ );
+ ALTER SERVER loopback1 OPTIONS (user 'value'); -- ERROR
+ ERROR: invalid option "user"
+ HINT: Valid options in this context are: authtype, service, connect_timeout, dbname, host, hostaddr, port, tty, options, application_name, keepalives, keepalives_idle, keepalives_interval, keepalives_count, sslmode, sslcert, sslkey, sslrootcert, sslcrl, requirepeer, fetch_count
+ ALTER SERVER loopback2 OPTIONS (ADD fetch_count '2');
+ ALTER USER MAPPING FOR public SERVER loopback1
+ OPTIONS (DROP user, DROP password);
+ ALTER USER MAPPING FOR public SERVER loopback1
+ OPTIONS (host 'value'); -- ERROR
+ ERROR: invalid option "host"
+ HINT: Valid options in this context are: user, password
+ ALTER FOREIGN TABLE ft1 OPTIONS (nspname 'S 1', relname 'T 1');
+ ALTER FOREIGN TABLE ft2 OPTIONS (nspname 'S 1', relname 'T 1', fetch_count '100');
+ ALTER FOREIGN TABLE ft1 OPTIONS (invalid 'value'); -- ERROR
+ ERROR: invalid option "invalid"
+ HINT: Valid options in this context are: nspname, relname, fetch_count
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count 'a'); -- ERROR
+ ERROR: invalid value for fetch_count: "a"
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count '0'); -- ERROR
+ ERROR: invalid value for fetch_count: "0"
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count '-1'); -- ERROR
+ ERROR: invalid value for fetch_count: "-1"
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (invalid 'value'); -- ERROR
+ ERROR: invalid option "invalid"
+ HINT: Valid options in this context are: colname
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (colname 'C 1');
+ ALTER FOREIGN TABLE ft2 ALTER COLUMN c1 OPTIONS (colname 'C 1');
+ \dew+
+ List of foreign-data wrappers
+ Name | Owner | Handler | Validator | Access privileges | FDW Options | Description
+ -----------+----------+-------------------+---------------------+-------------------+-------------+-------------
+ pgsql_fdw | postgres | pgsql_fdw_handler | pgsql_fdw_validator | | |
+ (1 row)
+
+ \des+
+ List of foreign servers
+ Name | Owner | Foreign-data wrapper | Access privileges | Type | Version | FDW Options | Description
+ -----------+----------+----------------------+-------------------+------+---------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+-------------
+ loopback1 | postgres | pgsql_fdw | | | | (authtype 'value', service 'value', connect_timeout 'value', dbname 'value', host 'value', hostaddr 'value', port 'value', tty 'value', options 'value', application_name 'value', keepalives 'value', keepalives_idle 'value', keepalives_interval 'value', sslmode 'value', sslcert 'value', sslkey 'value', sslrootcert 'value', sslcrl 'value') |
+ loopback2 | postgres | pgsql_fdw | | | | (dbname 'contrib_regression', fetch_count '2') |
+ (2 rows)
+
+ \deu+
+ List of user mappings
+ Server | User name | FDW Options
+ -----------+-----------+-------------
+ loopback1 | public |
+ loopback2 | public |
+ (2 rows)
+
+ \det+
+ List of foreign tables
+ Schema | Table | Server | FDW Options | Description
+ --------+-------+-----------+---------------------------------------------------+-------------
+ public | ft1 | loopback2 | (nspname 'S 1', relname 'T 1') |
+ public | ft2 | loopback2 | (nspname 'S 1', relname 'T 1', fetch_count '100') |
+ (2 rows)
+
+ -- ===================================================================
+ -- simple queries
+ -- ===================================================================
+ -- single table, with/without alias
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
+ QUERY PLAN
+ ------------------------------------------------------------------------------------------------------------------------------
+ Limit
+ Output: c1, c2, c3, c4, c5, c6, c7
+ -> Sort
+ Output: c1, c2, c3, c4, c5, c6, c7
+ Sort Key: ft1.c3, ft1.c1
+ -> Foreign Scan on public.ft1
+ Output: c1, c2, c3, c4, c5, c6, c7
+ Remote SQL: DECLARE pgsql_fdw_cursor_0 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (8 rows)
+
+ SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ -----+----+-------+------------------------------+--------------------------+----+------------
+ 101 | 1 | 00101 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970 | 1 | 1
+ 102 | 2 | 00102 | Sat Jan 03 00:00:00 1970 PST | Sat Jan 03 00:00:00 1970 | 2 | 2
+ 103 | 3 | 00103 | Sun Jan 04 00:00:00 1970 PST | Sun Jan 04 00:00:00 1970 | 3 | 3
+ 104 | 4 | 00104 | Mon Jan 05 00:00:00 1970 PST | Mon Jan 05 00:00:00 1970 | 4 | 4
+ 105 | 5 | 00105 | Tue Jan 06 00:00:00 1970 PST | Tue Jan 06 00:00:00 1970 | 5 | 5
+ 106 | 6 | 00106 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6
+ 107 | 7 | 00107 | Thu Jan 08 00:00:00 1970 PST | Thu Jan 08 00:00:00 1970 | 7 | 7
+ 108 | 8 | 00108 | Fri Jan 09 00:00:00 1970 PST | Fri Jan 09 00:00:00 1970 | 8 | 8
+ 109 | 9 | 00109 | Sat Jan 10 00:00:00 1970 PST | Sat Jan 10 00:00:00 1970 | 9 | 9
+ 110 | 0 | 00110 | Sun Jan 11 00:00:00 1970 PST | Sun Jan 11 00:00:00 1970 | 0 | 0
+ (10 rows)
+
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+ QUERY PLAN
+ ------------------------------------------------------------------------------------------------------------------------------
+ Limit
+ Output: c1, c2, c3, c4, c5, c6, c7
+ -> Sort
+ Output: c1, c2, c3, c4, c5, c6, c7
+ Sort Key: t1.c3, t1.c1
+ -> Foreign Scan on public.ft1 t1
+ Output: c1, c2, c3, c4, c5, c6, c7
+ Remote SQL: DECLARE pgsql_fdw_cursor_2 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (8 rows)
+
+ SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ -----+----+-------+------------------------------+--------------------------+----+------------
+ 101 | 1 | 00101 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970 | 1 | 1
+ 102 | 2 | 00102 | Sat Jan 03 00:00:00 1970 PST | Sat Jan 03 00:00:00 1970 | 2 | 2
+ 103 | 3 | 00103 | Sun Jan 04 00:00:00 1970 PST | Sun Jan 04 00:00:00 1970 | 3 | 3
+ 104 | 4 | 00104 | Mon Jan 05 00:00:00 1970 PST | Mon Jan 05 00:00:00 1970 | 4 | 4
+ 105 | 5 | 00105 | Tue Jan 06 00:00:00 1970 PST | Tue Jan 06 00:00:00 1970 | 5 | 5
+ 106 | 6 | 00106 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6
+ 107 | 7 | 00107 | Thu Jan 08 00:00:00 1970 PST | Thu Jan 08 00:00:00 1970 | 7 | 7
+ 108 | 8 | 00108 | Fri Jan 09 00:00:00 1970 PST | Fri Jan 09 00:00:00 1970 | 8 | 8
+ 109 | 9 | 00109 | Sat Jan 10 00:00:00 1970 PST | Sat Jan 10 00:00:00 1970 | 9 | 9
+ 110 | 0 | 00110 | Sun Jan 11 00:00:00 1970 PST | Sun Jan 11 00:00:00 1970 | 0 | 0
+ (10 rows)
+
+ -- with WHERE clause
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
+ QUERY PLAN
+ ------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on public.ft1 t1
+ Output: c1, c2, c3, c4, c5, c6, c7
+ Filter: ((t1.c1 = 101) AND ((t1.c6)::text = '1'::text) AND (t1.c7 = '1'::bpchar))
+ Remote SQL: DECLARE pgsql_fdw_cursor_4 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (4 rows)
+
+ SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ -----+----+-------+------------------------------+--------------------------+----+------------
+ 101 | 1 | 00101 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970 | 1 | 1
+ (1 row)
+
+ -- aggregate
+ SELECT COUNT(*) FROM ft1 t1;
+ count
+ -------
+ 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
+ ----+----+-------+------------------------------+--------------------------+----+------------
+ 1 | 1 | 00001 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970 | 1 | 1
+ 2 | 2 | 00002 | Sat Jan 03 00:00:00 1970 PST | Sat Jan 03 00:00:00 1970 | 2 | 2
+ 3 | 3 | 00003 | Sun Jan 04 00:00:00 1970 PST | Sun Jan 04 00:00:00 1970 | 3 | 3
+ 4 | 4 | 00004 | Mon Jan 05 00:00:00 1970 PST | Mon Jan 05 00:00:00 1970 | 4 | 4
+ 5 | 5 | 00005 | Tue Jan 06 00:00:00 1970 PST | Tue Jan 06 00:00:00 1970 | 5 | 5
+ 6 | 6 | 00006 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6
+ 7 | 7 | 00007 | Thu Jan 08 00:00:00 1970 PST | Thu Jan 08 00:00:00 1970 | 7 | 7
+ 8 | 8 | 00008 | Fri Jan 09 00:00:00 1970 PST | Fri Jan 09 00:00:00 1970 | 8 | 8
+ 9 | 9 | 00009 | Sat Jan 10 00:00:00 1970 PST | Sat Jan 10 00:00:00 1970 | 9 | 9
+ 10 | 0 | 00010 | Sun Jan 11 00:00:00 1970 PST | Sun Jan 11 00:00:00 1970 | 0 | 0
+ (10 rows)
+
+ -- subquery+MAX
+ SELECT * FROM ft1 t1 WHERE t1.c3 = (SELECT MAX(c3) FROM ft2 t2) ORDER BY c1;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ ------+----+-------+------------------------------+--------------------------+----+------------
+ 1000 | 0 | 01000 | Thu Jan 01 00:00:00 1970 PST | Thu Jan 01 00:00:00 1970 | 0 | 0
+ (1 row)
+
+ -- used in CTE
+ WITH t1 AS (SELECT * FROM ft1 WHERE c1 <= 10) SELECT t2.c1, t2.c2, t2.c3, t2.c4 FROM t1, ft2 t2 WHERE t1.c1 = t2.c1 ORDER BY t1.c1;
+ c1 | c2 | c3 | c4
+ ----+----+-------+------------------------------
+ 1 | 1 | 00001 | Fri Jan 02 00:00:00 1970 PST
+ 2 | 2 | 00002 | Sat Jan 03 00:00:00 1970 PST
+ 3 | 3 | 00003 | Sun Jan 04 00:00:00 1970 PST
+ 4 | 4 | 00004 | Mon Jan 05 00:00:00 1970 PST
+ 5 | 5 | 00005 | Tue Jan 06 00:00:00 1970 PST
+ 6 | 6 | 00006 | Wed Jan 07 00:00:00 1970 PST
+ 7 | 7 | 00007 | Thu Jan 08 00:00:00 1970 PST
+ 8 | 8 | 00008 | Fri Jan 09 00:00:00 1970 PST
+ 9 | 9 | 00009 | Sat Jan 10 00:00:00 1970 PST
+ 10 | 0 | 00010 | Sun Jan 11 00:00:00 1970 PST
+ (10 rows)
+
+ -- fixed values
+ SELECT 'fixed', NULL FROM ft1 t1 WHERE c1 = 1;
+ ?column? | ?column?
+ ----------+----------
+ fixed |
+ (1 row)
+
+ -- user-defined operator/function
+ CREATE FUNCTION pgsql_fdw_abs(int) RETURNS int AS $$
+ BEGIN
+ RETURN abs($1);
+ END
+ $$ LANGUAGE plpgsql IMMUTABLE;
+ CREATE OPERATOR === (
+ LEFTARG = int,
+ RIGHTARG = int,
+ PROCEDURE = int4eq,
+ COMMUTATOR = ===,
+ NEGATOR = !==
+ );
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = pgsql_fdw_abs(t1.c2);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = pgsql_fdw_abs(c2))
+ Remote SQL: DECLARE pgsql_fdw_cursor_18 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 === t1.c2;
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 === c2)
+ Remote SQL: DECLARE pgsql_fdw_cursor_19 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = abs(t1.c2);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = abs(c2))
+ Remote SQL: DECLARE pgsql_fdw_cursor_20 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = t1.c2;
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = c2)
+ Remote SQL: DECLARE pgsql_fdw_cursor_21 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ DROP OPERATOR === (int, int) CASCADE;
+ DROP FUNCTION pgsql_fdw_abs(int);
+ -- ===================================================================
+ -- parameterized queries
+ -- ===================================================================
+ -- simple join
+ PREPARE st1(int, int) AS SELECT t1.c3, t2.c3 FROM ft1 t1, ft2 t2 WHERE t1.c1 = $1 AND t2.c1 = $2;
+ EXPLAIN (COSTS false) EXECUTE st1(1, 2);
+ QUERY PLAN
+ -----------------------------------------------------------------------------------------------------------------------------------------
+ Nested Loop
+ -> Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_22 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, NULL, NULL, NULL, NULL FROM "S 1"."T 1"
+ -> Materialize
+ -> Foreign Scan on ft2 t2
+ Filter: (c1 = 2)
+ Remote SQL: DECLARE pgsql_fdw_cursor_23 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, NULL, NULL, NULL, NULL FROM "S 1"."T 1"
+ (8 rows)
+
+ EXECUTE st1(1, 1);
+ c3 | c3
+ -------+-------
+ 00001 | 00001
+ (1 row)
+
+ EXECUTE st1(101, 101);
+ c3 | c3
+ -------+-------
+ 00101 | 00101
+ (1 row)
+
+ -- subquery using stable function (can't be pushed down)
+ PREPARE st2(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c4) = 6) ORDER BY c1;
+ EXPLAIN (COSTS false) EXECUTE st2(10, 20);
+ QUERY PLAN
+ ---------------------------------------------------------------------------------------------------------------------------------------------------
+ Sort
+ Sort Key: t1.c1
+ -> Hash Join
+ Hash Cond: (t1.c3 = t2.c3)
+ -> Foreign Scan on ft1 t1
+ Filter: (c1 < 20)
+ Remote SQL: DECLARE pgsql_fdw_cursor_28 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ -> Hash
+ -> HashAggregate
+ -> Foreign Scan on ft2 t2
+ Filter: ((c1 > 10) AND (date_part('dow'::text, c4) = 6::double precision))
+ Remote SQL: DECLARE pgsql_fdw_cursor_29 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, c4, NULL, NULL, NULL FROM "S 1"."T 1"
+ (12 rows)
+
+ EXECUTE st2(10, 20);
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ ----+----+-------+------------------------------+--------------------------+----+------------
+ 16 | 6 | 00016 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970 | 6 | 6
+ (1 row)
+
+ EXECUTE st1(101, 101);
+ c3 | c3
+ -------+-------
+ 00101 | 00101
+ (1 row)
+
+ -- subquery using immutable function (can be pushed down)
+ PREPARE st3(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c5) = 6) ORDER BY c1;
+ EXPLAIN (COSTS false) EXECUTE st3(10, 20);
+ QUERY PLAN
+ ---------------------------------------------------------------------------------------------------------------------------------------------------
+ Sort
+ Sort Key: t1.c1
+ -> Hash Join
+ Hash Cond: (t1.c3 = t2.c3)
+ -> Foreign Scan on ft1 t1
+ Filter: (c1 < 20)
+ Remote SQL: DECLARE pgsql_fdw_cursor_34 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ -> Hash
+ -> HashAggregate
+ -> Foreign Scan on ft2 t2
+ Filter: ((c1 > 10) AND (date_part('dow'::text, c5) = 6::double precision))
+ Remote SQL: DECLARE pgsql_fdw_cursor_35 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, NULL, c5, NULL, NULL FROM "S 1"."T 1"
+ (12 rows)
+
+ EXECUTE st3(10, 20);
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ ----+----+-------+------------------------------+--------------------------+----+------------
+ 16 | 6 | 00016 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970 | 6 | 6
+ (1 row)
+
+ EXECUTE st3(20, 30);
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ ----+----+-------+------------------------------+--------------------------+----+------------
+ 23 | 3 | 00023 | Sat Jan 24 00:00:00 1970 PST | Sat Jan 24 00:00:00 1970 | 3 | 3
+ (1 row)
+
+ -- custom plan should be chosen
+ PREPARE st4(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 = $1;
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_40 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_41 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_42 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_43 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_44 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_46 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ -- cleanup
+ DEALLOCATE st1;
+ DEALLOCATE st2;
+ DEALLOCATE st3;
+ DEALLOCATE st4;
+ -- ===================================================================
+ -- connection management
+ -- ===================================================================
+ SELECT srvname, usename FROM pgsql_fdw_connections;
+ srvname | usename
+ -----------+----------
+ loopback2 | postgres
+ (1 row)
+
+ SELECT pgsql_fdw_disconnect(srvid, usesysid) FROM pgsql_fdw_get_connections();
+ pgsql_fdw_disconnect
+ ----------------------
+ OK
+ (1 row)
+
+ SELECT srvname, usename FROM pgsql_fdw_connections;
+ srvname | usename
+ ---------+---------
+ (0 rows)
+
+ -- ===================================================================
+ -- cleanup
+ -- ===================================================================
+ DROP EXTENSION pgsql_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 server loopback2
+ drop cascades to user mapping for public
+ drop cascades to foreign table ft1
+ drop cascades to foreign table ft2
diff --git a/contrib/pgsql_fdw/option.c b/contrib/pgsql_fdw/option.c
index ...65e3c54 .
*** a/contrib/pgsql_fdw/option.c
--- b/contrib/pgsql_fdw/option.c
***************
*** 0 ****
--- 1,246 ----
+ /*-------------------------------------------------------------------------
+ *
+ * option.c
+ * FDW option handling
+ *
+ * Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/option.c
+ *
+ *-------------------------------------------------------------------------
+ */
+ #include "postgres.h"
+
+ #include "access/reloptions.h"
+ #include "catalog/pg_foreign_data_wrapper.h"
+ #include "catalog/pg_foreign_server.h"
+ #include "catalog/pg_foreign_table.h"
+ #include "catalog/pg_user_mapping.h"
+ #include "fmgr.h"
+ #include "foreign/foreign.h"
+ #include "lib/stringinfo.h"
+ #include "miscadmin.h"
+
+ #include "pgsql_fdw.h"
+
+ /*
+ * SQL functions
+ */
+ extern Datum pgsql_fdw_validator(PG_FUNCTION_ARGS);
+ PG_FUNCTION_INFO_V1(pgsql_fdw_validator);
+
+ /*
+ * Describes the valid options for objects that use this wrapper.
+ */
+ typedef struct PgsqlFdwOption
+ {
+ const char *optname;
+ Oid optcontext; /* Oid of catalog in which options may appear */
+ bool is_libpq_opt; /* true if it's used in libpq */
+ } PgsqlFdwOption;
+
+ /*
+ * Valid options for pgsql_fdw.
+ */
+ static PgsqlFdwOption valid_options[] = {
+
+ /*
+ * Options for libpq connection.
+ * Note: This list should be updated along with PQconninfoOptions in
+ * interfaces/libpq/fe-connect.c, so the order is kept as is.
+ *
+ * Some useless libpq connection options are not accepted by pgsql_fdw:
+ * client_encoding: set to local database encoding automatically
+ * fallback_application_name: fixed to "pgsql_fdw"
+ * replication: pgsql_fdw never be replication client
+ */
+ {"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},
+ #ifdef NOT_USED
+ {"client_encoding", ForeignServerRelationId, true},
+ #endif
+ {"tty", ForeignServerRelationId, true},
+ {"options", ForeignServerRelationId, true},
+ {"application_name", ForeignServerRelationId, true},
+ #ifdef NOT_USED
+ {"fallback_application_name", ForeignServerRelationId, true},
+ #endif
+ {"keepalives", ForeignServerRelationId, true},
+ {"keepalives_idle", ForeignServerRelationId, true},
+ {"keepalives_interval", ForeignServerRelationId, true},
+ {"keepalives_count", ForeignServerRelationId, true},
+ #ifdef USE_SSL
+ {"requiressl", ForeignServerRelationId, true},
+ #endif
+ {"sslmode", ForeignServerRelationId, true},
+ {"sslcert", ForeignServerRelationId, true},
+ {"sslkey", ForeignServerRelationId, true},
+ {"sslrootcert", ForeignServerRelationId, true},
+ {"sslcrl", ForeignServerRelationId, true},
+ {"requirepeer", ForeignServerRelationId, true},
+ #if defined(KRB5) || defined(ENABLE_GSS) || defined(ENABLE_SSPI)
+ {"krbsrvname", ForeignServerRelationId, true},
+ #endif
+ #if defined(ENABLE_GSS) && defined(ENABLE_SSPI)
+ {"gsslib", ForeignServerRelationId, true},
+ #endif
+ #ifdef NOT_USED
+ {"replication", ForeignServerRelationId, true},
+ #endif
+
+ /*
+ * Options for translation of object names.
+ */
+ {"nspname", ForeignTableRelationId, false},
+ {"relname", ForeignTableRelationId, false},
+ {"colname", AttributeRelationId, false},
+
+ /*
+ * Options for cursor behavior.
+ * These options can be overridden by finer-grained objects.
+ */
+ {"fetch_count", ForeignTableRelationId, false},
+ {"fetch_count", ForeignServerRelationId, false},
+
+ /* Terminating entry --- MUST BE LAST */
+ {NULL, InvalidOid, false}
+ };
+
+ /*
+ * Helper functions
+ */
+ static bool is_valid_option(const char *optname, Oid context);
+
+ /*
+ * Validate the generic options given to a FOREIGN DATA WRAPPER, SERVER,
+ * USER MAPPING or FOREIGN TABLE that uses pgsql_fdw.
+ *
+ * Raise an ERROR if the option or its value is considered invalid.
+ */
+ Datum
+ pgsql_fdw_validator(PG_FUNCTION_ARGS)
+ {
+ List *options_list = untransformRelOptions(PG_GETARG_DATUM(0));
+ Oid catalog = PG_GETARG_OID(1);
+ ListCell *cell;
+
+ /*
+ * Check that only options supported by pgsql_fdw, and allowed for the
+ * current object type, are given.
+ */
+ foreach(cell, options_list)
+ {
+ DefElem *def = (DefElem *) lfirst(cell);
+
+ if (!is_valid_option(def->defname, catalog))
+ {
+ PgsqlFdwOption *opt;
+ StringInfoData buf;
+
+ /*
+ * Unknown option specified, complain about it. Provide a hint
+ * with list of valid options for the object.
+ */
+ initStringInfo(&buf);
+ for (opt = valid_options; opt->optname; opt++)
+ {
+ if (catalog == opt->optcontext)
+ appendStringInfo(&buf, "%s%s", (buf.len > 0) ? ", " : "",
+ opt->optname);
+ }
+
+ ereport(ERROR,
+ (errcode(ERRCODE_FDW_INVALID_OPTION_NAME),
+ errmsg("invalid option \"%s\"", def->defname),
+ errhint("Valid options in this context are: %s",
+ buf.data)));
+ }
+
+ /* fetch_count be positive digit number. */
+ if (strcmp(def->defname, "fetch_count") == 0)
+ {
+ long value;
+ char *p = NULL;
+
+ value = strtol(strVal(def->arg), &p, 10);
+ if (*p != '\0' || value < 1)
+ ereport(ERROR,
+ (errcode(ERRCODE_FDW_INVALID_ATTRIBUTE_VALUE),
+ errmsg("invalid value for %s: \"%s\"",
+ def->defname, strVal(def->arg))));
+ }
+ }
+
+ /*
+ * We don't care option-specific limitation here; they will be validated at
+ * the execution time.
+ */
+
+ PG_RETURN_VOID();
+ }
+
+ /*
+ * Check whether the given option is one of the valid pgsql_fdw options.
+ * context is the Oid of the catalog holding the object the option is for.
+ */
+ static bool
+ is_valid_option(const char *optname, Oid context)
+ {
+ PgsqlFdwOption *opt;
+
+ for (opt = valid_options; opt->optname; opt++)
+ {
+ if (context == opt->optcontext && strcmp(opt->optname, optname) == 0)
+ return true;
+ }
+ return false;
+ }
+
+ /*
+ * Check whether the given option is one of the valid libpq options.
+ * context is the Oid of the catalog holding the object the option is for.
+ */
+ static bool
+ is_libpq_option(const char *optname)
+ {
+ PgsqlFdwOption *opt;
+
+ for (opt = valid_options; opt->optname; opt++)
+ {
+ if (strcmp(opt->optname, optname) == 0 && opt->is_libpq_opt)
+ return true;
+ }
+ return false;
+ }
+
+ /*
+ * Generate key-value arrays which includes only libpq options from the list
+ * which contains any kind of options.
+ */
+ int
+ ExtractConnectionOptions(List *defelems, const char **keywords, const char **values)
+ {
+ ListCell *lc;
+ int i;
+
+ i = 0;
+ foreach(lc, defelems)
+ {
+ DefElem *d = (DefElem *) lfirst(lc);
+ if (is_libpq_option(d->defname))
+ {
+ keywords[i] = d->defname;
+ values[i] = strVal(d->arg);
+ i++;
+ }
+ }
+ return i;
+ }
diff --git a/contrib/pgsql_fdw/pgsql_fdw--1.0.sql b/contrib/pgsql_fdw/pgsql_fdw--1.0.sql
index ...b0ea2b2 .
*** a/contrib/pgsql_fdw/pgsql_fdw--1.0.sql
--- b/contrib/pgsql_fdw/pgsql_fdw--1.0.sql
***************
*** 0 ****
--- 1,39 ----
+ /* contrib/pgsql_fdw/pgsql_fdw--1.0.sql */
+
+ -- complain if script is sourced in psql, rather than via CREATE EXTENSION
+ \echo Use "CREATE EXTENSION pgsql_fdw" to load this file. \quit
+
+ CREATE FUNCTION pgsql_fdw_handler()
+ RETURNS fdw_handler
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C STRICT;
+
+ CREATE FUNCTION pgsql_fdw_validator(text[], oid)
+ RETURNS void
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C STRICT;
+
+ CREATE FOREIGN DATA WRAPPER pgsql_fdw
+ HANDLER pgsql_fdw_handler
+ VALIDATOR pgsql_fdw_validator;
+
+ /* connection management functions and view */
+ CREATE FUNCTION pgsql_fdw_get_connections(out srvid oid, out usesysid oid)
+ RETURNS SETOF record
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C STRICT;
+
+ CREATE FUNCTION pgsql_fdw_disconnect(oid, oid)
+ RETURNS text
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C STRICT;
+
+ CREATE VIEW pgsql_fdw_connections AS
+ SELECT c.srvid srvid,
+ s.srvname srvname,
+ c.usesysid usesysid,
+ pg_get_userbyid(c.usesysid) usename
+ FROM pgsql_fdw_get_connections() c
+ JOIN pg_catalog.pg_foreign_server s ON (s.oid = c.srvid);
+ GRANT SELECT ON pgsql_fdw_connections TO public;
+
diff --git a/contrib/pgsql_fdw/pgsql_fdw.c b/contrib/pgsql_fdw/pgsql_fdw.c
index ...9ded3e1 .
*** a/contrib/pgsql_fdw/pgsql_fdw.c
--- b/contrib/pgsql_fdw/pgsql_fdw.c
***************
*** 0 ****
--- 1,859 ----
+ /*-------------------------------------------------------------------------
+ *
+ * pgsql_fdw.c
+ * foreign-data wrapper for remote PostgreSQL servers.
+ *
+ * Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/pgsql_fdw.c
+ *
+ *-------------------------------------------------------------------------
+ */
+ #include "postgres.h"
+ #include "fmgr.h"
+
+ #include "catalog/pg_foreign_server.h"
+ #include "catalog/pg_foreign_table.h"
+ #include "commands/explain.h"
+ #include "foreign/fdwapi.h"
+ #include "funcapi.h"
+ #include "miscadmin.h"
+ #include "nodes/nodeFuncs.h"
+ #include "optimizer/cost.h"
+ #include "optimizer/pathnode.h"
+ #include "utils/lsyscache.h"
+ #include "utils/memutils.h"
+ #include "utils/rel.h"
+
+ #include "pgsql_fdw.h"
+ #include "connection.h"
+
+ PG_MODULE_MAGIC;
+
+ /*
+ * Default fetch count for cursor. This can be overridden by fetch_count FDW
+ * option.
+ */
+ #define DEFAULT_FETCH_COUNT 10000
+
+ /*
+ * Cost to establish a connection.
+ * XXX: should be configurable per server?
+ */
+ #define CONNECTION_COSTS 100.0
+
+ /*
+ * Cost to transfer 1 byte from remote server.
+ * XXX: should be configurable per server?
+ */
+ #define TRANSFER_COSTS_PER_BYTE 0.001
+
+ /*
+ * Cursors which are used together in a local query require different name, so
+ * we use simple incremental name for that purpose. We don't care wrap around
+ * of cursor_id because it's hard to imagine that 2^32 cursors are used in a
+ * query.
+ */
+ #define CURSOR_NAME_FORMAT "pgsql_fdw_cursor_%u"
+ static uint32 cursor_id = 0;
+
+ /*
+ * Index of FDW-private items stored in FdwPlan.
+ */
+ enum FdwPrivateIndex {
+ FdwPrivateSelectSql,
+ FdwPrivateDeclareSql,
+ FdwPrivateFetchSql,
+ FdwPrivateResetSql,
+ FdwPrivateCloseSql,
+
+ /* # of elements stored in the list fdw_private */
+ FdwPrivateNum,
+ };
+
+ /*
+ * Describes an execution state of a foreign scan against a foreign table
+ * using pgsql_fdw.
+ */
+ typedef struct PgsqlFdwExecutionState
+ {
+ MemoryContext scan_cxt; /* context for per-scan lifespan data */
+
+ FdwPlan *fdwplan; /* FDW-specific planning information */
+ PGconn *conn; /* connection for the scan */
+
+ Oid *param_types; /* type array of external parameter */
+ const char **param_values; /* value array of external parameter */
+
+ int attnum; /* # of non-dropped attribute */
+ char **col_values; /* column value buffer */
+ AttInMetadata *attinmeta; /* attribute metadata */
+
+ Tuplestorestate *tuples; /* result of the scan */
+ } PgsqlFdwExecutionState;
+
+ /*
+ * SQL functions
+ */
+ extern Datum pgsql_fdw_handler(PG_FUNCTION_ARGS);
+ PG_FUNCTION_INFO_V1(pgsql_fdw_handler);
+
+ /*
+ * FDW callback routines
+ */
+ static FdwPlan *pgsqlPlanForeignScan(Oid foreigntableid,
+ PlannerInfo *root,
+ RelOptInfo *baserel);
+ static void pgsqlExplainForeignScan(ForeignScanState *node, ExplainState *es);
+ static void pgsqlBeginForeignScan(ForeignScanState *node, int eflags);
+ static TupleTableSlot *pgsqlIterateForeignScan(ForeignScanState *node);
+ static void pgsqlReScanForeignScan(ForeignScanState *node);
+ static void pgsqlEndForeignScan(ForeignScanState *node);
+
+ /*
+ * Helper functions
+ */
+ static void estimate_costs(PlannerInfo *root,
+ RelOptInfo *baserel,
+ const char *sql,
+ Oid serverid,
+ Cost *startup_cost,
+ Cost *total_cost);
+ static void execute_query(ForeignScanState *node);
+ static PGresult *fetch_result(ForeignScanState *node);
+ static void store_result(ForeignScanState *node, PGresult *res);
+ static bool contain_ext_param(Node *clause);
+ static bool contain_ext_param_walker(Node *node, void *context);
+
+ /*
+ * The content of FdwRoutine of pgsql_fdw is never changed, so we use one
+ * static object for all scan.
+ */
+ static FdwRoutine fdwroutine = {
+ T_FdwRoutine,
+ pgsqlPlanForeignScan,
+ pgsqlExplainForeignScan,
+ pgsqlBeginForeignScan,
+ pgsqlIterateForeignScan,
+ pgsqlReScanForeignScan,
+ pgsqlEndForeignScan,
+ };
+
+ /*
+ * Foreign-data wrapper handler function: return a struct with pointers
+ * to my callback routines.
+ */
+ Datum
+ pgsql_fdw_handler(PG_FUNCTION_ARGS)
+ {
+ PG_RETURN_POINTER(&fdwroutine);
+ }
+
+ /*
+ * pgsqlPlanForeignScan
+ * Create a FdwPlan for a scan on the foreign table
+ */
+ static FdwPlan *
+ pgsqlPlanForeignScan(Oid foreigntableid,
+ PlannerInfo *root,
+ RelOptInfo *baserel)
+ {
+ char name[128]; /* must be larger than format + 10 */
+ StringInfoData cursor;
+ const char *fetch_count_str;
+ int fetch_count = DEFAULT_FETCH_COUNT;
+ char *sql;
+ FdwPlan *fdwplan;
+ List *fdw_private = NIL;
+ ForeignTable *table;
+ ForeignServer *server;
+
+ /* Construct FdwPlan with cost estimates */
+ fdwplan = makeNode(FdwPlan);
+ sql = deparseSql(foreigntableid, root, baserel);
+ table = GetForeignTable(foreigntableid);
+ server = GetForeignServer(table->serverid);
+ estimate_costs(root, baserel, sql, server->serverid,
+ &fdwplan->startup_cost, &fdwplan->total_cost);
+
+ /*
+ * Store plain SELECT statement in private area of FdwPlan. This will be
+ * used for executing remote query and explaining scan.
+ */
+ fdw_private = list_make1(makeString(sql));
+
+ /* Use specified fetch_count instead of default value, if any. */
+ fetch_count_str = GetFdwOptionValue(InvalidOid, InvalidOid, foreigntableid,
+ InvalidAttrNumber, "fetch_count");
+ if (fetch_count_str != NULL)
+ fetch_count = strtol(fetch_count_str, NULL, 10);
+ elog(DEBUG1, "relid=%u fetch_count=%d", foreigntableid, fetch_count);
+
+ /*
+ * We store some more information in FdwPlan to pass them beyond the
+ * boundary between planner and executor. Finally FdwPlan using cursor
+ * would hold items below:
+ *
+ * 1) plain SELECT statement (already added above)
+ * 2) SQL statement used to declare cursor
+ * 3) SQL statement used to fetch rows from cursor
+ * 4) SQL statement used to reset cursor
+ * 5) SQL statement used to close cursor
+ *
+ * These items are indexed with the enum FdwPrivateIndex, so an item
+ * can be accessed directly via list_nth(). For example of FETCH
+ * statement:
+ * list_nth(fdw_private, FdwPrivateFetchSql)
+ */
+
+ /* Construct cursor name from sequential value */
+ sprintf(name, CURSOR_NAME_FORMAT, cursor_id++);
+
+ /* Construct statement to declare cursor */
+ initStringInfo(&cursor);
+ appendStringInfo(&cursor, "DECLARE %s SCROLL CURSOR FOR %s", name, sql);
+ fdw_private = lappend(fdw_private, makeString(cursor.data));
+
+ /* Construct statement to fetch rows from cursor */
+ initStringInfo(&cursor);
+ appendStringInfo(&cursor, "FETCH %d FROM %s", fetch_count, name);
+ fdw_private = lappend(fdw_private, makeString(cursor.data));
+
+ /* Construct statement to reset cursor */
+ initStringInfo(&cursor);
+ appendStringInfo(&cursor, "MOVE ABSOLUTE 0 FROM %s", name);
+ fdw_private = lappend(fdw_private, makeString(cursor.data));
+
+ /* Construct statement to close cursor */
+ initStringInfo(&cursor);
+ appendStringInfo(&cursor, "CLOSE %s", name);
+ fdw_private = lappend(fdw_private, makeString(cursor.data));
+
+ /* Store FDW private information into FdwPlan */
+ fdwplan->fdw_private = fdw_private;
+
+ return fdwplan;
+ }
+
+ /*
+ * pgsqlExplainForeignScan
+ * Produce extra output for EXPLAIN
+ */
+ static void
+ pgsqlExplainForeignScan(ForeignScanState *node, ExplainState *es)
+ {
+ FdwPlan *fdwplan;
+ char *sql;
+
+ fdwplan = ((ForeignScan *) node->ss.ps.plan)->fdwplan;
+ sql = strVal(list_nth(fdwplan->fdw_private, FdwPrivateDeclareSql));
+ ExplainPropertyText("Remote SQL", sql, es);
+ }
+
+ /*
+ * pgsqlBeginForeignScan
+ * Initiate access to a foreign PostgreSQL table.
+ */
+ static void
+ pgsqlBeginForeignScan(ForeignScanState *node, int eflags)
+ {
+ PgsqlFdwExecutionState *festate;
+ PGconn *conn;
+ Oid relid;
+ ForeignTable *table;
+ ForeignServer *server;
+ UserMapping *user;
+ TupleTableSlot *slot = node->ss.ss_ScanTupleSlot;
+
+ /*
+ * Do nothing in EXPLAIN (no ANALYZE) case. node->fdw_state stays NULL.
+ */
+ if (eflags & EXEC_FLAG_EXPLAIN_ONLY)
+ return;
+
+ /*
+ * Save state in node->fdw_state.
+ */
+ festate = (PgsqlFdwExecutionState *) palloc(sizeof(PgsqlFdwExecutionState));
+ festate->fdwplan = ((ForeignScan *) node->ss.ps.plan)->fdwplan;
+
+ /*
+ * Create context for per-scan tuplestore.
+ */
+ festate->scan_cxt = AllocSetContextCreate(MessageContext,
+ "pgsql_fdw",
+ ALLOCSET_DEFAULT_MINSIZE,
+ ALLOCSET_DEFAULT_INITSIZE,
+ ALLOCSET_DEFAULT_MAXSIZE);
+
+ /*
+ * Get connection to the foreign server. Connection manager would
+ * establish new connection if necessary.
+ */
+ relid = RelationGetRelid(node->ss.ss_currentRelation);
+ table = GetForeignTable(relid);
+ server = GetForeignServer(table->serverid);
+ user = GetUserMapping(GetOuterUserId(), server->serverid);
+ conn = GetConnection(server, user, true);
+ festate->conn = conn;
+
+ /* Result will be filled in first Iterate call. */
+ festate->tuples = NULL;
+
+ /* Allocate buffers for column values. */
+ {
+ TupleDesc tupdesc = slot->tts_tupleDescriptor;
+ festate->col_values = palloc(sizeof(char *) * tupdesc->natts);
+ festate->attinmeta = TupleDescGetAttInMetadata(tupdesc);
+ }
+
+ /* Allocate buffers for query parameters. */
+ {
+ ParamListInfo params = node->ss.ps.state->es_param_list_info;
+ int numParams = params ? params->numParams : 0;
+
+ if (numParams > 0)
+ {
+ festate->param_types = palloc0(sizeof(Oid) * numParams);
+ festate->param_values = palloc0(sizeof(char *) * numParams);
+ }
+ else
+ {
+ festate->param_types = NULL;
+ festate->param_values = NULL;
+ }
+ }
+
+
+ /* Store FDW-specific state into ForeignScanState */
+ node->fdw_state = (void *) festate;
+
+ return;
+ }
+
+ /*
+ * pgsqlIterateForeignScan
+ * Retrieve next row from the result set, or clear tuple slot to indicate
+ * EOF.
+ *
+ * Note that using per-query context when retrieving tuples from
+ * tuplestore to ensure that returned tuples can survive until next
+ * iteration because the tuple is released implicitly via ExecClearTuple.
+ * Retrieving a tuple from tuplestore in CurrentMemoryContext (it's a
+ * per-tuple context), ExecClearTuple will free dangling pointer.
+ */
+ static TupleTableSlot *
+ pgsqlIterateForeignScan(ForeignScanState *node)
+ {
+ PgsqlFdwExecutionState *festate;
+ TupleTableSlot *slot = node->ss.ss_ScanTupleSlot;
+ PGresult *res;
+ MemoryContext oldcontext = CurrentMemoryContext;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+
+
+ /*
+ * If this is the first call after Begin, we need to execute remote query.
+ * If the query needs cursor, we declare a cursor at first call and fetch
+ * from it in later calls.
+ */
+ if (festate->tuples == NULL)
+ execute_query(node);
+
+ /*
+ * If enough tuples are left in tuplestore, just return next tuple from it.
+ */
+ MemoryContextSwitchTo(node->ss.ps.state->es_query_cxt);
+ if (tuplestore_gettupleslot(festate->tuples, true, false, slot))
+ {
+ MemoryContextSwitchTo(oldcontext);
+ return slot;
+ }
+ MemoryContextSwitchTo(oldcontext);
+
+ /*
+ * Here we need to clear partial result and fetch next bunch of tuples from
+ * from the cursor for the scan. If the fetch returns no tuple, the scan
+ * has reached the end.
+ */
+ res = fetch_result(node);
+ PG_TRY();
+ {
+ store_result(node, res);
+ PQclear(res);
+ res = NULL;
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+
+ /*
+ * If we got more tuples from the server cursor, return next tuple from
+ * tuplestore.
+ */
+ MemoryContextSwitchTo(node->ss.ps.state->es_query_cxt);
+ if (tuplestore_gettupleslot(festate->tuples, true, false, slot))
+ {
+ MemoryContextSwitchTo(oldcontext);
+ return slot;
+ }
+ MemoryContextSwitchTo(oldcontext);
+
+ /* We don't have any result even in remote server cursor. */
+ ExecClearTuple(slot);
+ return slot;
+ }
+
+ /*
+ * pgsqlReScanForeignScan
+ * - Restart this scan by resetting fetch location.
+ */
+ static void
+ pgsqlReScanForeignScan(ForeignScanState *node)
+ {
+ List *fdw_private;
+ char *sql;
+ PGconn *conn;
+ PGresult *res;
+ PgsqlFdwExecutionState *festate;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+
+ /* If we have not opened cursor yet, nothing to do. */
+ if (festate->tuples == NULL)
+ return;
+
+ /* Discard fetch results if any. */
+ tuplestore_clear(festate->tuples);
+
+ /* Reset cursor */
+ fdw_private = festate->fdwplan->fdw_private;
+ conn = festate->conn;
+ sql = strVal(list_nth(fdw_private, FdwPrivateResetSql));
+ res = PQexec(conn, sql);
+ PG_TRY();
+ {
+ if (PQresultStatus(res) != PGRES_COMMAND_OK)
+ {
+ ereport(ERROR,
+ (errmsg("could not rewind cursor"),
+ errdetail("%s", PQerrorMessage(conn)),
+ errhint("%s", sql)));
+ }
+ PQclear(res);
+ res = NULL;
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+ }
+
+ /*
+ * pgsqlEndForeignScan
+ * Finish scanning foreign table and dispose objects used for this scan
+ */
+ static void
+ pgsqlEndForeignScan(ForeignScanState *node)
+ {
+ List *fdw_private;
+ char *sql;
+ PGconn *conn;
+ PGresult *res;
+ PgsqlFdwExecutionState *festate;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+
+ /* if festate is NULL, we are in EXPLAIN; nothing to do */
+ if (festate == NULL)
+ return;
+
+ /* If we have not opened cursor yet, nothing to do. */
+ if (festate->tuples == NULL)
+ return;
+
+ /* Discard fetch results */
+ tuplestore_end(festate->tuples);
+ festate->tuples = NULL;
+
+ /* Close cursor */
+ fdw_private = festate->fdwplan->fdw_private;
+ conn = festate->conn;
+ sql = strVal(list_nth(fdw_private, FdwPrivateCloseSql));
+ res = PQexec(conn, sql);
+ PG_TRY();
+ {
+ if (PQresultStatus(res) != PGRES_COMMAND_OK)
+ {
+ ereport(ERROR,
+ (errmsg("could not close cursor"),
+ errdetail("%s", PQerrorMessage(conn)),
+ errhint("%s", sql)));
+ }
+ PQclear(res);
+ res = NULL;
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+
+ ReleaseConnection(festate->conn);
+
+ MemoryContextDelete(festate->scan_cxt);
+ festate->scan_cxt = NULL;
+ }
+
+ /*
+ * Estimate costs of scanning a foreign table.
+ */
+ static void
+ estimate_costs(PlannerInfo *root, RelOptInfo *baserel,
+ const char *sql, Oid serverid,
+ Cost *startup_cost, Cost *total_cost)
+ {
+ ListCell *lc;
+ ForeignServer *server;
+ UserMapping *user;
+ PGconn *conn = NULL;
+ PGresult *res = NULL;
+ StringInfoData buf;
+ char *plan;
+ char *p;
+ int n;
+
+ /*
+ * If the baserestrictinfo contains any Param node with paramkind
+ * PARAM_EXTERNAL, we need result of EXPLAIN for EXECUTE statement, not for
+ * simple SELECT statement. However, we can't get actual parameter values
+ * here, so we use fixed and large costs as second best so that planner
+ * tend to choose custom plan.
+ *
+ * See comments in plancache.c for details of custom plan.
+ */
+ foreach(lc, baserel->baserestrictinfo)
+ {
+ RestrictInfo *rs = (RestrictInfo *) lfirst(lc);
+ if (contain_ext_param((Node *) rs->clause))
+ {
+ *startup_cost = CONNECTION_COSTS;
+ *total_cost = 10000; /* groundless large costs */
+
+ return;
+ }
+ }
+
+ /*
+ * Get connection to the foreign server. Connection manager would
+ * establish new connection if necessary.
+ */
+ server = GetForeignServer(serverid);
+ user = GetUserMapping(GetOuterUserId(), server->serverid);
+ conn = GetConnection(server, user, false);
+ initStringInfo(&buf);
+ appendStringInfo(&buf, "EXPLAIN %s", sql);
+
+ PG_TRY();
+ {
+ res = PQexec(conn, buf.data);
+ if (PQresultStatus(res) != PGRES_TUPLES_OK || PQntuples(res) == 0)
+ {
+ char *msg;
+
+ msg = pstrdup(PQerrorMessage(conn));
+ ereport(ERROR,
+ (errmsg("could not execute EXPLAIN for cost estimation"),
+ errdetail("%s", msg),
+ errhint("%s", sql)));
+ }
+
+ /*
+ * Find estimation portion from top plan node. Here we search opening
+ * parentheses from the end of the line to avoid finding unexpected
+ * parentheses.
+ */
+ plan = PQgetvalue(res, 0, 0);
+ p = strrchr(plan, '(');
+ if (p == NULL)
+ elog(ERROR, "wrong EXPLAIN output: %s", plan);
+ n = sscanf(p,
+ "(cost=%lf..%lf rows=%lf width=%d)",
+ startup_cost, total_cost, &baserel->rows, &baserel->width);
+ if (n != 4)
+ elog(ERROR, "could not get estimation from EXPLAIN output");
+
+ PQclear(res);
+ res = NULL;
+ ReleaseConnection(conn);
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+
+ /*
+ * TODO Selectivity of quals which are NOT pushed down should be also
+ * considered.
+ */
+
+ /* add cost to establish connection. */
+ *startup_cost += CONNECTION_COSTS;
+ *total_cost += CONNECTION_COSTS;
+
+ /* add cost to transfer result. */
+ *total_cost += TRANSFER_COSTS_PER_BYTE * baserel->width * baserel->tuples;
+ *total_cost += cpu_tuple_cost * baserel->tuples;
+ }
+
+ /*
+ * Execute remote query with current parameters.
+ */
+ static void
+ execute_query(ForeignScanState *node)
+ {
+ FdwPlan *fdwplan;
+ PgsqlFdwExecutionState *festate;
+ ParamListInfo params = node->ss.ps.state->es_param_list_info;
+ int numParams = params ? params->numParams : 0;
+ Oid *types = NULL;
+ const char **values = NULL;
+ char *sql;
+ PGconn *conn;
+ PGresult *res;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+ types = festate->param_types;
+ values = festate->param_values;
+
+ /*
+ * Construct parameter array in text format. We don't release memory for
+ * the arrays explicitly, because the memory usage would not be very large,
+ * and anyway they will be released in context cleanup.
+ */
+ if (numParams > 0)
+ {
+ int i;
+
+ 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;
+
+ getTypeOutputInfo(types[i], &out_func_oid, &isvarlena);
+ fmgr_info(out_func_oid, &func);
+ values[i] = OutputFunctionCall(&func, params->params[i].value);
+ }
+ }
+ }
+
+ /*
+ * Execute remote query with parameters.
+ */
+ conn = festate->conn;
+ fdwplan = ((ForeignScan *) node->ss.ps.plan)->fdwplan;
+ sql = strVal(list_nth(fdwplan->fdw_private, FdwPrivateDeclareSql));
+ res = PQexecParams(conn, sql, numParams, types, values, NULL, NULL, 0);
+ PG_TRY();
+ {
+ /*
+ * If the query has failed, reporting details is enough here.
+ * Connection(s) which are used by this query (at least used by
+ * pgsql_fdw) will be cleaned up by the foreign connection manager.
+ */
+ if (PQresultStatus(res) != PGRES_COMMAND_OK)
+ {
+ ereport(ERROR,
+ (errmsg("could not declare cursor"),
+ errdetail("%s", PQerrorMessage(conn)),
+ errhint("%s", sql)));
+ }
+
+ /* Discard result of CURSOR statement and fetch first bunch. */
+ PQclear(res);
+ res = fetch_result(node);
+
+ /*
+ * Store the result of the query into tuplestore.
+ * We must release PGresult here to avoid memory leak.
+ */
+ store_result(node, res);
+ PQclear(res);
+ res = NULL;
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+ }
+
+ /*
+ * Fetch next partial result from remote server.
+ */
+ static PGresult *
+ fetch_result(ForeignScanState *node)
+ {
+ PgsqlFdwExecutionState *festate;
+ List *fdw_private;
+ char *sql;
+ PGconn *conn;
+ PGresult *res;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+
+ /* retrieve information for fetching result. */
+ fdw_private = festate->fdwplan->fdw_private;
+ sql = strVal(list_nth(fdw_private, FdwPrivateFetchSql));
+ conn = festate->conn;
+ res = PQexec(conn, sql);
+ if (PQresultStatus(res) != PGRES_TUPLES_OK)
+ {
+ ereport(ERROR,
+ (errmsg("could not fetch rows from foreign server"),
+ errdetail("%s", PQerrorMessage(conn)),
+ errhint("%s", sql)));
+ }
+
+ return res;
+ }
+
+ /*
+ * Create tuples from PGresult and store them into tuplestore.
+ */
+ static void
+ store_result(ForeignScanState *node, PGresult *res)
+ {
+ int rows;
+ int row;
+ int i;
+ int nfields;
+ int attnum; /* number of non-dropped columns */
+ Form_pg_attribute *attrs;
+ TupleTableSlot *slot = node->ss.ss_ScanTupleSlot;
+ TupleDesc tupdesc = slot->tts_tupleDescriptor;
+ PgsqlFdwExecutionState *festate;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+ rows = PQntuples(res);
+ nfields = PQnfields(res);
+ attrs = tupdesc->attrs;
+
+ /* First, ensure that the tuplestore is empty. */
+ if (festate->tuples == NULL)
+ {
+ MemoryContext oldcontext = CurrentMemoryContext;
+
+ /*
+ * Create tuplestore to store result of the query in per-query context.
+ * Note that we use this memory context to avoid memory leak in error
+ * cases.
+ */
+ MemoryContextSwitchTo(festate->scan_cxt);
+ festate->tuples = tuplestore_begin_heap(false, false, work_mem);
+ MemoryContextSwitchTo(oldcontext);
+ }
+ else
+ {
+ /* We already have tuplestore, just need to clear contents of it. */
+ tuplestore_clear(festate->tuples);
+ }
+
+
+ /* 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)));
+
+ /* put a tuples into the slot */
+ for (row = 0; row < rows; row++)
+ {
+ int j;
+ HeapTuple tuple;
+
+ for (i = 0, j = 0; i < tupdesc->natts; i++)
+ {
+ /* skip dropped columns. */
+ if (attrs[i]->attisdropped)
+ {
+ festate->col_values[i] = NULL;
+ continue;
+ }
+
+ if (PQgetisnull(res, row, j))
+ festate->col_values[i] = NULL;
+ else
+ festate->col_values[i] = PQgetvalue(res, row, 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(festate->attinmeta, festate->col_values);
+ tuplestore_puttuple(festate->tuples, tuple);
+ }
+
+ tuplestore_donestoring(festate->tuples);
+ }
+
+ /*
+ * contain_ext_param
+ * Recursively search for Param nodes within a clause.
+ *
+ * Returns true if any parameter reference node with relkind PARAM_EXTERN
+ * found.
+ *
+ * This does not descend into subqueries, and so should be used only after
+ * reduction of sublinks to subplans, or in contexts where it's known there
+ * are no subqueries. There mustn't be outer-aggregate references either.
+ *
+ * XXX: These functions could be in core, src/backend/optimizer/util/clauses.c.
+ */
+ static bool
+ contain_ext_param(Node *clause)
+ {
+ return contain_ext_param_walker(clause, NULL);
+ }
+
+ static bool
+ contain_ext_param_walker(Node *node, void *context)
+ {
+ if (node == NULL)
+ return false;
+ if (IsA(node, Param))
+ {
+ Param *param = (Param *) node;
+
+ if (param->paramkind == PARAM_EXTERN)
+ return true; /* abort the tree traversal and return true */
+ }
+ return expression_tree_walker(node, contain_ext_param_walker, context);
+ }
diff --git a/contrib/pgsql_fdw/pgsql_fdw.control b/contrib/pgsql_fdw/pgsql_fdw.control
index ...0a9c8f4 .
*** a/contrib/pgsql_fdw/pgsql_fdw.control
--- b/contrib/pgsql_fdw/pgsql_fdw.control
***************
*** 0 ****
--- 1,5 ----
+ # pgsql_fdw extension
+ comment = 'foreign-data wrapper for remote PostgreSQL servers'
+ default_version = '1.0'
+ module_pathname = '$libdir/pgsql_fdw'
+ relocatable = true
diff --git a/contrib/pgsql_fdw/pgsql_fdw.h b/contrib/pgsql_fdw/pgsql_fdw.h
index ...f6394ce .
*** a/contrib/pgsql_fdw/pgsql_fdw.h
--- b/contrib/pgsql_fdw/pgsql_fdw.h
***************
*** 0 ****
--- 1,28 ----
+ /*-------------------------------------------------------------------------
+ *
+ * pgsql_fdw.h
+ * foreign-data wrapper for remote PostgreSQL servers.
+ *
+ * Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/pgsql_fdw.h
+ *
+ *-------------------------------------------------------------------------
+ */
+
+ #ifndef PGSQL_FDW_H
+ #define PGSQL_FDW_H
+
+ #include "postgres.h"
+ #include "nodes/relation.h"
+
+ /* in option.c */
+ int ExtractConnectionOptions(List *defelems,
+ const char **keywords,
+ const char **values);
+
+ /* in deparse.c */
+ char *deparseSql(Oid relid, PlannerInfo *root, RelOptInfo *baserel);
+
+ #endif /* PGSQL_FDW_H */
diff --git a/contrib/pgsql_fdw/sql/pgsql_fdw.sql b/contrib/pgsql_fdw/sql/pgsql_fdw.sql
index ...7f9f9cc .
*** a/contrib/pgsql_fdw/sql/pgsql_fdw.sql
--- b/contrib/pgsql_fdw/sql/pgsql_fdw.sql
***************
*** 0 ****
--- 1,212 ----
+ -- ===================================================================
+ -- create FDW objects
+ -- ===================================================================
+
+ CREATE EXTENSION pgsql_fdw;
+
+ CREATE SERVER loopback1 FOREIGN DATA WRAPPER pgsql_fdw;
+ CREATE SERVER loopback2 FOREIGN DATA WRAPPER pgsql_fdw
+ OPTIONS (dbname 'contrib_regression');
+
+ CREATE USER MAPPING FOR public SERVER loopback1
+ OPTIONS (user 'value', password 'value');
+ CREATE USER MAPPING FOR public SERVER loopback2;
+
+ CREATE FOREIGN TABLE ft1 (
+ c1 int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10)
+ ) SERVER loopback2;
+
+ CREATE FOREIGN TABLE ft2 (
+ c1 int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10)
+ ) SERVER loopback2;
+
+ -- ===================================================================
+ -- create objects used through FDW
+ -- ===================================================================
+ CREATE SCHEMA "S 1";
+ CREATE TABLE "S 1"."T 1" (
+ "C 1" int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10),
+ CONSTRAINT t1_pkey PRIMARY KEY ("C 1")
+ );
+ CREATE TABLE "S 1"."T 2" (
+ c1 int NOT NULL,
+ c2 text,
+ CONSTRAINT t2_pkey PRIMARY KEY (c1)
+ );
+
+ BEGIN;
+ TRUNCATE "S 1"."T 1";
+ INSERT INTO "S 1"."T 1"
+ SELECT id,
+ id % 10,
+ to_char(id, 'FM00000'),
+ '1970-01-01'::timestamptz + ((id % 100) || ' days')::interval,
+ '1970-01-01'::timestamp + ((id % 100) || ' days')::interval,
+ id % 10,
+ id % 10
+ FROM generate_series(1, 1000) id;
+ TRUNCATE "S 1"."T 2";
+ INSERT INTO "S 1"."T 2"
+ SELECT id,
+ 'AAA' || to_char(id, 'FM000')
+ FROM generate_series(1, 100) id;
+ COMMIT;
+
+ -- ===================================================================
+ -- tests for pgsql_fdw_validator
+ -- ===================================================================
+ ALTER FOREIGN DATA WRAPPER pgsql_fdw OPTIONS (host 'value'); -- ERROR
+ -- requiressl, krbsrvname and gsslib are omitted because they depend on
+ -- configure option
+ ALTER SERVER loopback1 OPTIONS (
+ authtype 'value',
+ service 'value',
+ connect_timeout 'value',
+ dbname 'value',
+ host 'value',
+ hostaddr 'value',
+ port 'value',
+ --client_encoding 'value',
+ tty 'value',
+ options 'value',
+ application_name 'value',
+ --fallback_application_name 'value',
+ keepalives 'value',
+ keepalives_idle 'value',
+ keepalives_interval 'value',
+ -- requiressl 'value',
+ sslmode 'value',
+ sslcert 'value',
+ sslkey 'value',
+ sslrootcert 'value',
+ sslcrl 'value'
+ --requirepeer 'value',
+ -- krbsrvname 'value',
+ -- gsslib 'value',
+ --replication 'value'
+ );
+ ALTER SERVER loopback1 OPTIONS (user 'value'); -- ERROR
+ ALTER SERVER loopback2 OPTIONS (ADD fetch_count '2');
+ ALTER USER MAPPING FOR public SERVER loopback1
+ OPTIONS (DROP user, DROP password);
+ ALTER USER MAPPING FOR public SERVER loopback1
+ OPTIONS (host 'value'); -- ERROR
+ ALTER FOREIGN TABLE ft1 OPTIONS (nspname 'S 1', relname 'T 1');
+ ALTER FOREIGN TABLE ft2 OPTIONS (nspname 'S 1', relname 'T 1', fetch_count '100');
+ ALTER FOREIGN TABLE ft1 OPTIONS (invalid 'value'); -- ERROR
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count 'a'); -- ERROR
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count '0'); -- ERROR
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count '-1'); -- ERROR
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (invalid 'value'); -- ERROR
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (colname 'C 1');
+ ALTER FOREIGN TABLE ft2 ALTER COLUMN c1 OPTIONS (colname 'C 1');
+ \dew+
+ \des+
+ \deu+
+ \det+
+
+ -- ===================================================================
+ -- simple queries
+ -- ===================================================================
+ -- single table, with/without alias
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
+ SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+ SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+ -- with WHERE clause
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
+ SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
+ -- 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
+ SELECT * FROM ft1 t1 WHERE t1.c3 = (SELECT MAX(c3) FROM ft2 t2) ORDER BY c1;
+ -- used in CTE
+ WITH t1 AS (SELECT * FROM ft1 WHERE c1 <= 10) SELECT t2.c1, t2.c2, t2.c3, t2.c4 FROM t1, ft2 t2 WHERE t1.c1 = t2.c1 ORDER BY t1.c1;
+ -- fixed values
+ SELECT 'fixed', NULL FROM ft1 t1 WHERE c1 = 1;
+ -- user-defined operator/function
+ CREATE FUNCTION pgsql_fdw_abs(int) RETURNS int AS $$
+ BEGIN
+ RETURN abs($1);
+ END
+ $$ LANGUAGE plpgsql IMMUTABLE;
+ CREATE OPERATOR === (
+ LEFTARG = int,
+ RIGHTARG = int,
+ PROCEDURE = int4eq,
+ COMMUTATOR = ===,
+ NEGATOR = !==
+ );
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = pgsql_fdw_abs(t1.c2);
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 === t1.c2;
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = abs(t1.c2);
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = t1.c2;
+ DROP OPERATOR === (int, int) CASCADE;
+ DROP FUNCTION pgsql_fdw_abs(int);
+
+ -- ===================================================================
+ -- parameterized queries
+ -- ===================================================================
+ -- simple join
+ PREPARE st1(int, int) AS SELECT t1.c3, t2.c3 FROM ft1 t1, ft2 t2 WHERE t1.c1 = $1 AND t2.c1 = $2;
+ EXPLAIN (COSTS false) EXECUTE st1(1, 2);
+ EXECUTE st1(1, 1);
+ EXECUTE st1(101, 101);
+ -- subquery using stable function (can't be pushed down)
+ PREPARE st2(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c4) = 6) ORDER BY c1;
+ EXPLAIN (COSTS false) EXECUTE st2(10, 20);
+ EXECUTE st2(10, 20);
+ EXECUTE st1(101, 101);
+ -- subquery using immutable function (can be pushed down)
+ PREPARE st3(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c5) = 6) ORDER BY c1;
+ EXPLAIN (COSTS false) EXECUTE st3(10, 20);
+ EXECUTE st3(10, 20);
+ EXECUTE st3(20, 30);
+ -- custom plan should be chosen
+ PREPARE st4(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 = $1;
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ -- cleanup
+ DEALLOCATE st1;
+ DEALLOCATE st2;
+ DEALLOCATE st3;
+ DEALLOCATE st4;
+
+ -- ===================================================================
+ -- connection management
+ -- ===================================================================
+ SELECT srvname, usename FROM pgsql_fdw_connections;
+ SELECT pgsql_fdw_disconnect(srvid, usesysid) FROM pgsql_fdw_get_connections();
+ SELECT srvname, usename FROM pgsql_fdw_connections;
+
+ -- ===================================================================
+ -- cleanup
+ -- ===================================================================
+ DROP EXTENSION pgsql_fdw CASCADE;
+
diff --git a/doc/src/sgml/contrib.sgml b/doc/src/sgml/contrib.sgml
index d4da4ee..32056d1 100644
*** a/doc/src/sgml/contrib.sgml
--- b/doc/src/sgml/contrib.sgml
*************** CREATE EXTENSION <replaceable>module_nam
*** 117,122 ****
--- 117,123 ----
&pgcrypto;
&pgfreespacemap;
&pgrowlocks;
+ &pgsql-fdw;
&pgstandby;
&pgstatstatements;
&pgstattuple;
diff --git a/doc/src/sgml/filelist.sgml b/doc/src/sgml/filelist.sgml
index b5d3c6d..de686ee 100644
*** a/doc/src/sgml/filelist.sgml
--- b/doc/src/sgml/filelist.sgml
***************
*** 125,130 ****
--- 125,131 ----
<!ENTITY pgcrypto SYSTEM "pgcrypto.sgml">
<!ENTITY pgfreespacemap SYSTEM "pgfreespacemap.sgml">
<!ENTITY pgrowlocks SYSTEM "pgrowlocks.sgml">
+ <!ENTITY pgsql-fdw SYSTEM "pgsql-fdw.sgml">
<!ENTITY pgstandby SYSTEM "pgstandby.sgml">
<!ENTITY pgstatstatements SYSTEM "pgstatstatements.sgml">
<!ENTITY pgstattuple SYSTEM "pgstattuple.sgml">
diff --git a/doc/src/sgml/pgsql-fdw.sgml b/doc/src/sgml/pgsql-fdw.sgml
index ...299f4b6 .
*** a/doc/src/sgml/pgsql-fdw.sgml
--- b/doc/src/sgml/pgsql-fdw.sgml
***************
*** 0 ****
--- 1,244 ----
+ <!-- doc/src/sgml/pgsql-fdw.sgml -->
+
+ <sect1 id="pgsql-fdw" xreflabel="pgsql_fdw">
+ <title>pgsql_fdw</title>
+
+ <indexterm zone="pgsql-fdw">
+ <primary>pgsql_fdw</primary>
+ </indexterm>
+
+ <para>
+ The <filename>pgsql_fdw</filename> module provides a foreign-data wrapper for
+ external <productname>PostgreSQL</productname> servers.
+ With this module, users can access data stored in external
+ <productname>PostgreSQL</productname> via plain SQL statements.
+ </para>
+
+ <para>
+ Note that default wrapper <literal>pgsql_fdw</literal> is created
+ automatically during <command>CREATE EXTENSION</command> command for
+ <application>pgsql_fdw</application>.
+ </para>
+
+ <sect2>
+ <title>FDW Options of pgsql_fdw</title>
+
+ <sect3>
+ <title>Connection Options</title>
+ <para>
+ A foreign server and user mapping created using this wrapper can have
+ <application>libpq</> connection options, expect below:
+
+ <itemizedlist>
+ <listitem><para>client_encoding</para></listitem>
+ <listitem><para>fallback_application_name</para></listitem>
+ <listitem><para>replication</para></listitem>
+ </itemizedlist>
+
+ For details of <application>libpq</> connection options, see
+ <xref linkend="libpq-connect">.
+ </para>
+
+ <para>
+ <literal>user</literal> and <literal>password</literal> can be
+ specified on user mappings, and others can be specified on foreign servers.
+ </para>
+ </sect3>
+
+ <sect3>
+ <title>Object Name Options</title>
+ <para>
+ Foreign tables which were created using this wrapper, or its columns can
+ have object name options. These options can be used to specify the names
+ used in SQL statement sent to remote <productname>PostgreSQL</productname>
+ server. These options are useful when a remote object has different name
+ from corresponding local one.
+ </para>
+
+ <variablelist>
+
+ <varlistentry>
+ <term><literal>nspname</literal></term>
+ <listitem>
+ <para>
+ This option, which can be specified on a foreign table, is used as a
+ namespace (schema) reference in the SQL statement. If this options is
+ omitted, <literal>pg_class.nspname</literal> of the foreign table is
+ used.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>relname</literal></term>
+ <listitem>
+ <para>
+ This option, which can be specified on a foreign table, is used as a
+ relation (table) reference in the SQL statement. If this options is
+ omitted, <literal>pg_class.relname</literal> of the foreign table is
+ used.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>colname</literal></term>
+ <listitem>
+ <para>
+ This option, which can be specified on a column of a foreign table, is
+ used as a column (attribute) reference in the SQL statement. If this
+ option is omitted, <literal>pg_attribute.attname</literal> of the column
+ of the foreign table is used.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ </variablelist>
+
+ </sect3>
+
+ <sect3>
+ <title>Cursor Options</title>
+ <para>
+ The <application>pgsql_fdw</application> always uses cursor to retrieve the
+ result from external server. Users can control the behavior of cursor by
+ setting cursor options to foreign table or foreign server. If an option
+ is set to both objects, finer-grained setting is used. In other words,
+ foreign table's setting overrides foreign server's setting.
+ </para>
+
+ <variablelist>
+
+ <varlistentry>
+ <term><literal>fetch_count</literal></term>
+ <listitem>
+ <para>
+ This option specifies the number of rows to be fetched at a time.
+ This option accepts only integer value larger than zero. The default
+ setting is 10000.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ </variablelist>
+
+ </sect3>
+
+ </sect2>
+
+ <sect2>
+ <title>Connection Management</title>
+
+ <para>
+ The <application>pgsql_fdw</application> establishes a connection to a
+ foreign server in the beginning of the first query which uses a foreign
+ table associated to the foreign server, and reuses the connection following
+ queries and even in following foreign scans in same query.
+
+ You can see the list of active connections via
+ <structname>pgsql_fdw_connections</structname> view. It shows pair of oid
+ and name of server and local role for each active connections established by
+ <application>pgsql_fdw</application>. For security reason, only superuser
+ can see other role's connections.
+ </para>
+
+ <para>
+ Established connections are kept alive until local role changes or the
+ current transaction aborts or user requests so.
+ </para>
+
+ <para>
+ If role has been changed, active connections established as old local role
+ is kept alive but never be reused until locla role has restored to original
+ role. This kind of situation happens with <command>SET ROLE</command> and
+ <command>SET SESSION AUTHORIZATION</command>.
+ </para>
+
+ <para>
+ If current transaction aborts by error or user request, all active
+ connections are disconnected automatically. This behavior avoids possible
+ connection leaks on error.
+ </para>
+
+ <para>
+ You can discard persistent connection at arbitrary timing with
+ <function>pgsql_fdw_disconnect()</function>. It takes server oid and
+ user oid as arguments. This function can handle only connections
+ established in current session; connections established by other backends
+ are not reachable.
+ </para>
+
+ <para>
+ You can discard all active and visible connections in current session with
+ using <structname>pgsql_fdw_connections</structname> and
+ <function>pgsql_fdw_disconnect()</function> together:
+ <synopsis>
+ postgres=# SELECT pgsql_fdw_disconnect(srvid, usesysid) FROM pgsql_fdw_connections;
+ pgsql_fdw_disconnect
+ ----------------------
+ OK
+ OK
+ (2 rows)
+ </synopsis>
+ </para>
+ </sect2>
+
+ <sect2>
+ <title>Transaction Management</title>
+ <para>
+ The <application>pgsql_fdw</application> executes <command>BEGIN</command>
+ command when a new connection has established. This means that all remote
+ queries for a foreign server are executed in a transaction. Since the
+ default transaction isolation level is <literal>READ COMMITTED</literal>,
+ multiple foreign scans in a local query might produce inconsistent results.
+
+ To avoid this inconsistency, you can use <literal>SERIALIZABLE</literal>
+ level for remote transaction with setting
+ <literal>default_transaction_isolation</literal> for the user used for
+ <application>pgsql_fdw</application> connection on remote side.
+ </para>
+ </sect2>
+
+ <sect2>
+ <title>Estimation of Costs and Rows</title>
+ <para>
+ The <application>pgsql_fdw</application> estimates the costs of a foreign
+ scan by adding some basic costs: connection costs, remote query costs and
+ data transfer costs.
+ To get remote query costs <application>pgsql_fdw</application> executes
+ <command>EXPLAIN</command> command on remote server for each foreign scan.
+ </para>
+
+ <para>
+ On the other hand, estimated rows which was returned by
+ <command>EXPLAIN</command> is used for local estimation as-is.
+ </para>
+ </sect2>
+
+ <sect2>
+ <title>EXPLAIN Output</title>
+ <para>
+ For a foreign table using <literal>pgsql_fdw</>, <command>EXPLAIN</> shows
+ a remote SQL statement which is sent to remote
+ <productname>PostgreSQL</productname> server for a ForeignScan plan node.
+ For example:
+ </para>
+ <synopsis>
+ postgres=# EXPLAIN SELECT aid FROM pgbench_accounts WHERE abalance < 0;
+ QUERY PLAN
+ --------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on pgbench_accounts (cost=100.00..8046.37 rows=301037 width=8)
+ Filter: (abalance < 0)
+ Remote SQL: DECLARE pgsql_fdw_cursor_1 SCROLL CURSOR FOR SELECT aid, NULL, abalance, NULL FROM public.pgbench_accounts
+ (3 rows)
+ </synopsis>
+ </sect2>
+
+ <sect2>
+ <title>Author</title>
+ <para>
+ Shigeru Hanada <email>shigeru.hanada@gmail.com</email>
+ </para>
+ </sect2>
+
+ </sect1>
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 9ad54c5..c2a5a7b 100644
*** a/src/backend/utils/adt/ruleutils.c
--- b/src/backend/utils/adt/ruleutils.c
*************** deparse_context_for(const char *aliasnam
*** 2161,2166 ****
--- 2161,2189 ----
return list_make1(dpns);
}
+ /* ----------
+ * deparse_context_for_rtelist - Build deparse context for a list of RTEs
+ *
+ * Given the list of RangeTableEnetry, build deparsing context for an
+ * expression referencing those relations. This is sufficient for uses of
+ * deparse_expression before plan has been created.
+ * ----------
+ */
+ List *
+ deparse_context_for_rtelist(List *rtable)
+ {
+ deparse_namespace *dpns;
+
+ dpns = (deparse_namespace *) palloc0(sizeof(deparse_namespace));
+
+ /* Build a minimal RTE for the rel */
+ dpns->rtable = rtable;
+ dpns->ctes = NIL;
+
+ /* Return a one-deep namespace stack */
+ return list_make1(dpns);
+ }
+
/*
* deparse_context_for_planstate - Build deparse context for a plan
*
diff --git a/src/include/utils/builtins.h b/src/include/utils/builtins.h
index 68179d5..e4bd8eb 100644
*** a/src/include/utils/builtins.h
--- b/src/include/utils/builtins.h
*************** extern char *deparse_expression(Node *ex
*** 648,653 ****
--- 648,654 ----
extern List *deparse_context_for(const char *aliasname, Oid relid);
extern List *deparse_context_for_planstate(Node *planstate, List *ancestors,
List *rtable);
+ extern List *deparse_context_for_rtelist(List *rtable);
extern const char *quote_identifier(const char *ident);
extern char *quote_qualified_identifier(const char *qualifier,
const char *ident);
fdw_helper_v4.patchtext/plain; name=fdw_helper_v4.patchDownload
diff --git a/contrib/file_fdw/file_fdw.c b/contrib/file_fdw/file_fdw.c
index 46394a8..a522e36 100644
*** a/contrib/file_fdw/file_fdw.c
--- b/contrib/file_fdw/file_fdw.c
***************
*** 26,32 ****
#include "nodes/makefuncs.h"
#include "optimizer/cost.h"
#include "utils/rel.h"
! #include "utils/syscache.h"
PG_MODULE_MAGIC;
--- 26,32 ----
#include "nodes/makefuncs.h"
#include "optimizer/cost.h"
#include "utils/rel.h"
! #include "utils/lsyscache.h"
PG_MODULE_MAGIC;
*************** get_file_fdw_attribute_options(Oid relid
*** 345,398 ****
/* Retrieve FDW options for all user-defined attributes. */
for (attnum = 1; attnum <= natts; attnum++)
{
! HeapTuple tuple;
! Form_pg_attribute attr;
! Datum datum;
! bool isnull;
/* Skip dropped attributes. */
if (tupleDesc->attrs[attnum - 1]->attisdropped)
continue;
! /*
! * We need the whole pg_attribute tuple not just what is in the
! * tupleDesc, so must do a catalog lookup.
! */
! tuple = SearchSysCache2(ATTNUM,
! RelationGetRelid(rel),
! Int16GetDatum(attnum));
! if (!HeapTupleIsValid(tuple))
! elog(ERROR, "cache lookup failed for attribute %d of relation %u",
! attnum, RelationGetRelid(rel));
! attr = (Form_pg_attribute) GETSTRUCT(tuple);
!
! datum = SysCacheGetAttr(ATTNUM,
! tuple,
! Anum_pg_attribute_attfdwoptions,
! &isnull);
! if (!isnull)
{
! List *options = untransformRelOptions(datum);
! ListCell *lc;
! foreach(lc, options)
{
! DefElem *def = (DefElem *) lfirst(lc);
!
! if (strcmp(def->defname, "force_not_null") == 0)
{
! if (defGetBoolean(def))
! {
! char *attname = pstrdup(NameStr(attr->attname));
! fnncolumns = lappend(fnncolumns, makeString(attname));
! }
}
- /* maybe in future handle other options here */
}
}
-
- ReleaseSysCache(tuple);
}
heap_close(rel, AccessShareLock);
--- 345,373 ----
/* Retrieve FDW options for all user-defined attributes. */
for (attnum = 1; attnum <= natts; attnum++)
{
! List *options;
! ListCell *lc;
/* Skip dropped attributes. */
if (tupleDesc->attrs[attnum - 1]->attisdropped)
continue;
! options = GetForeignColumnOptions(relid, attnum);
! foreach(lc, options)
{
! DefElem *def = (DefElem *) lfirst(lc);
! if (strcmp(def->defname, "force_not_null") == 0)
{
! if (defGetBoolean(def))
{
! char *attname = pstrdup(get_attname(relid, attnum));
! fnncolumns = lappend(fnncolumns, makeString(attname));
}
}
+ /* maybe in future handle other options here */
}
}
heap_close(rel, AccessShareLock);
diff --git a/doc/src/sgml/fdwhandler.sgml b/doc/src/sgml/fdwhandler.sgml
index 76ff243..d809cac 100644
*** a/doc/src/sgml/fdwhandler.sgml
--- b/doc/src/sgml/fdwhandler.sgml
*************** EndForeignScan (ForeignScanState *node);
*** 235,238 ****
--- 235,392 ----
</sect1>
+ <sect1 id="fdw-helpers">
+ <title>Foreign Data Wrapper Helper Functions</title>
+
+ <para>
+ Several helper functions are exported from core so that authors of FDW
+ can get easy access to attributes of FDW-related objects such as FDW
+ options.
+ </para>
+
+ <para>
+ <programlisting>
+ ForeignDataWrapper *
+ GetForeignDataWrapper(Oid fdwid);
+ </programlisting>
+
+ This function returns a <structname>ForeignDataWrapper</structname> object
+ for a foreign-data wrapper with given oid. A
+ <structname>ForeignDataWrapper</structname> object contains oid of the
+ wrapper itself, oid of the owner of the wrapper, name of the wrapper, oid
+ of fdwhandler function, oid of fdwvalidator function, and FDW options in
+ the form of list of <structname>DefElem</structname>.
+ </para>
+
+ <para>
+ <programlisting>
+ ForeignServer *
+ GetForeignServer(Oid serverid);
+ </programlisting>
+
+ This function returns a <structname>ForeignServer</structname> object for
+ a foreign server with given oid. A <structname>ForeignServer</structname>
+ object contains oid of the server, oid of the wrapper for the server, oid
+ of the owner of the server, name of the server, type of the server,
+ version of the server, and FDW options in the form of list of
+ <structname>DefElem</structname>.
+ </para>
+
+ <para>
+ <programlisting>
+ UserMapping *
+ GetUserMapping(Oid userid, Oid serverid);
+ </programlisting>
+
+ This function returns a <structname>UserMapping</structname> object for a
+ user mapping with given oid pair. A <structname>UserMapping</structname>
+ object contains oid of the user, oid of the server, and FDW options in the
+ form of list of <structname>DefElem</structname>.
+ </para>
+
+ <para>
+ <programlisting>
+ ForeignTable *
+ GetForeignTable(Oid relid);
+ </programlisting>
+
+ This function returns a <structname>ForeignTable</structname> object for a
+ foreign table with given oid. A <structname>ForeignTable</structname>
+ object contains oid of the foreign table, oid of the server for the table,
+ and FDW options in the form of list of <structname>DefElem</structname>.
+ </para>
+
+ <para>
+ <programlisting>
+ List *
+ GetForeignTableColumnOptions(Oid relid, AttrNumber attnum);
+ </programlisting>
+
+ This function returns per-column FDW options for a column with given
+ relation oid and attribute number in the form of list of
+ <structname>DefElem</structname>.
+ </para>
+
+ <para>
+ Some object types have name-based functions.
+ </para>
+
+ <para>
+ <programlisting>
+ ForeignDataWrapper *
+ GetForeignDataWrapperByName(const char *name, bool missing_ok);
+ </programlisting>
+
+ This function returns a <structname>ForeignDataWrapper</structname> object
+ for a foreign-data wrapper with given name. If the wrapper is not found,
+ return NULL if missing_ok was true, otherwise raise an error.
+ </para>
+
+ <para>
+ <programlisting>
+ ForeignServer *
+ GetForeignServerByName(const char *name, bool missing_ok);
+ </programlisting>
+
+ This function returns a <structname>ForeignServer</structname> object for
+ a foreign server with given name. If the server is not found, return NULL
+ if missing_ok was true, otherwise raise an error.
+ </para>
+
+ <para>
+ <programlisting>
+ char *
+ GetFdwOptionValue(Oid fdwid, Oid serverid, Oid relid, AttrNumber attnum, const char *optname);
+ </programlisting>
+
+ This function returns a copied string (created in current memory context)
+ of the value of a FDW option with given name which is set on a object with
+ given oid and attribute number. This function ignores catalogs if invalid
+ identifir is given for it.
+
+ <itemizedlist>
+ <listitem>
+ <para>
+ If attnum is <literal>InvalidAttrNumber</literal> or relid is
+ <literal>Invalidoid</literal>, <structname>pg_attribute</structname> is
+ ignored.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ If relid is <literal>InvalidOid</literal>,
+ <structname>pg_foreign_table</structname> is ignored.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ If both serverid and relid are <literal>InvalidOid</literal>,
+ <structname>pg_foreign_server</structname> is ignored.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ If all of fdwid, serverid and relid are <literal>InvalidOid</literal>,
+ <structname>pg_foreign_data_wrapper</structname> is ignored.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+
+ <para>
+ If the option with given name is set in multiple object level, the one in
+ the finest-grained object is used; e.g. priority is given to user mappings
+ over than foreign servers.
+ This function would be useful when you know which option is needed but you
+ don't know where it is set. If you already know the source object, it
+ would be more efficient to use object retrieval functions.
+ </para>
+
+ <para>
+ To use any of these functions, you need to include
+ <filename>foreign/foreign.h</filename> in your source file.
+ </para>
+
+ </sect1>
+
</chapter>
diff --git a/src/backend/foreign/foreign.c b/src/backend/foreign/foreign.c
index c4c2a61..8e98f0f 100644
*** a/src/backend/foreign/foreign.c
--- b/src/backend/foreign/foreign.c
*************** GetForeignTable(Oid relid)
*** 248,253 ****
--- 248,378 ----
/*
+ * If an option entry which matches the given name was found in the given
+ * list, returns a copy of arg string, otherwise returns NULL.
+ */
+ static char *
+ get_options_value(List *options, const char *optname)
+ {
+ ListCell *lc;
+
+ /* Find target option from the list. */
+ foreach (lc, options)
+ {
+ DefElem *def = lfirst(lc);
+
+ if (strcmp(def->defname, optname) == 0)
+ return pstrdup(strVal(def->arg));
+ }
+
+ return NULL;
+ }
+
+ /*
+ * Returns a copy of the value of specified option with searching the option
+ * from appropriate catalog. If an option was stored in multiple object
+ * levels, one in the finest-grained object level is used; lookup order is:
+ * 1) pg_attribute (only when both attnum and relid are valid)
+ * 2) pg_foreign_table (only when relid is valid)
+ * 3) pg_user_mapping (only when serverid or relid is valid)
+ * 4) pg_foreign_server (only when serverid or relid is valid)
+ * 5) pg_foreign_data_wrapper (only when fdwid or serverid or relid is valid)
+ * This priority rule would be useful in most cases using FDW options.
+ *
+ * If attnum was InvalidAttrNumber, we don't retrieve FDW optiosn from
+ * pg_attribute.attfdwoptions.
+ */
+ char *
+ GetFdwOptionValue(Oid fdwid, Oid serverid, Oid relid, AttrNumber attnum,
+ const char *optname)
+ {
+ ForeignTable *table = NULL;
+ UserMapping *user = NULL;
+ ForeignServer *server = NULL;
+ ForeignDataWrapper *wrapper = NULL;
+ char *value;
+
+ /* Do we need to search pg_attribute? */
+ if (attnum != InvalidAttrNumber && relid != InvalidOid)
+ {
+ value = get_options_value(GetForeignColumnOptions(relid, attnum),
+ optname);
+ if (value != NULL)
+ return value;
+ }
+
+ /* Do we need to search pg_foreign_table? */
+ if (relid != InvalidOid)
+ {
+ table = GetForeignTable(relid);
+ value = get_options_value(table->options, optname);
+ if (value != NULL)
+ return value;
+
+ serverid = table->serverid;
+ }
+
+ /* Do we need to search pg_user_mapping and pg_foreign_server? */
+ if (serverid != InvalidOid)
+ {
+ user = GetUserMapping(GetOuterUserId(), serverid);
+ value = get_options_value(user->options, optname);
+ if (value != NULL)
+ return value;
+
+ server = GetForeignServer(serverid);
+ value = get_options_value(server->options, optname);
+ if (value != NULL)
+ return value;
+
+ fdwid = server->fdwid;
+ }
+
+ /* Do we need to search pg_foreign_data_wrapper? */
+ if (fdwid != InvalidOid)
+ {
+ wrapper = GetForeignDataWrapper(fdwid);
+ value = get_options_value(wrapper->options, optname);
+ if (value != NULL)
+ return value;
+ }
+
+ return NULL;
+ }
+
+
+ /*
+ * GetForeignColumnOptions - Get attfdwoptions of given relation/attnum as
+ * list of DefElem.
+ */
+ List *
+ GetForeignColumnOptions(Oid relid, AttrNumber attnum)
+ {
+ List *options;
+ HeapTuple tp;
+ Datum datum;
+ bool isnull;
+
+ tp = SearchSysCache2(ATTNUM,
+ ObjectIdGetDatum(relid),
+ Int16GetDatum(attnum));
+ if (!HeapTupleIsValid(tp))
+ elog(ERROR, "cache lookup failed for attribute %d of relation %u", attnum, relid);
+ datum = SysCacheGetAttr(ATTNUM,
+ tp,
+ Anum_pg_attribute_attfdwoptions,
+ &isnull);
+ if (isnull)
+ options = NIL;
+ else
+ options = untransformRelOptions(datum);
+
+ ReleaseSysCache(tp);
+
+ return options;
+ }
+
+ /*
* GetFdwRoutine - call the specified foreign-data wrapper handler routine
* to get its FdwRoutine struct.
*/
diff --git a/src/include/foreign/foreign.h b/src/include/foreign/foreign.h
index 191122d..e25b871 100644
*** a/src/include/foreign/foreign.h
--- b/src/include/foreign/foreign.h
*************** extern ForeignDataWrapper *GetForeignDat
*** 75,80 ****
--- 75,83 ----
extern ForeignDataWrapper *GetForeignDataWrapperByName(const char *name,
bool missing_ok);
extern ForeignTable *GetForeignTable(Oid relid);
+ extern char *GetFdwOptionValue(Oid fdwid, Oid serverid, Oid relid,
+ AttrNumber attnum, const char *optname);
+ extern List *GetForeignColumnOptions(Oid relid, AttrNumber attnum);
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);
(2012/02/01 3:56), Robert Haas wrote:
2012/1/29 Kohei KaiGai<kaigai@kaigai.gr.jp>:
Remote SQL: DECLARE pgsql_fdw_cursor_0 SCROLL CURSOR FOR SELECT
a, b FROM public.t1 WHERE (a> 2)
(3 rows)Shouldn't we be using protocol-level cursors rather than SQL-level cursors?
Yes, we should, if we have protocol-level cursor :)
I checked libpq interface but I couldn't find any function for
protocol-level cursor.
[Design comment]
When "BEGIN" should be issued on the remote-side?
The connect_pg_server() is an only chance to issue "BEGIN" command
at the remote-side on connection being opened. However, the connection
shall be kept unless an error is not raised. Thus, the remote-side will
continue to work within a single transaction block, even if local-side iterates
a pair of "BEGIN" and "COMMIT".
I'd like to suggest to close the transaction block at the timing of either
end of the scan, transaction or sub-transaction.I suspect this is ultimately going to need to be configurable. Some
people might want to close the transaction on the remote side ASAP,
while other people might want to hold it open until commit. For a
first version I think it's most likely best to do whatever seems
simplest to code, planning to add more options later.
I fixed pgsql_fdw to abort remote transaction at the end of each local
query. I chose this timing because local query might include multiple
scans on same foreign server. I think this would be "ASAP" timing in
your comment.
It would be useful to make length of remote transaction same as local's,
I'll try RegisterXactCallback for this purpose, though we need to
preload FDW module to catch BEGIN preceding query using foreign tables.
--
Shigeru Hanada
On Tue, Jan 31, 2012 at 8:56 PM, Robert Haas <robertmhaas@gmail.com> wrote:
2012/1/29 Kohei KaiGai <kaigai@kaigai.gr.jp>:
Remote SQL: DECLARE pgsql_fdw_cursor_0 SCROLL CURSOR FOR SELECT
a, b FROM public.t1 WHERE (a > 2)
(3 rows)Shouldn't we be using protocol-level cursors rather than SQL-level cursors?
I think you want this instead:
https://commitfest.postgresql.org/action/patch_view?id=769
--
marko
2012年2月1日12:15 Shigeru Hanada <shigeru.hanada@gmail.com>:
(2012/01/30 4:39), Kohei KaiGai wrote:
I checked the "fdw_helper_funcs_v3.patch", "pgsql_fdw_v5.patch" and
"pgsql_fdw_pushdown_v1.patch". My comments are below.Thanks for the review!
[BUG]
Even though pgsql_fdw tries to push-down qualifiers being executable
on the remove side at the deparseSql(), it does not remove qualifiers
being pushed down from the baserel->baserestrictinfo, thus, these
qualifiers are eventually executed twice.See the result of this EXPLAIN.
postgres=# EXPLAIN SELECT * FROM ft1 WHERE a> 2 AND f_leak(b);
QUERY PLAN
------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 (cost=107.43..122.55 rows=410 width=36)
Filter: (f_leak(b) AND (a> 2))
Remote SQL: DECLARE pgsql_fdw_cursor_0 SCROLL CURSOR FOR SELECT
a, b FROM public.t1 WHERE (a> 2)
(3 rows)My expectation is (a> 2) being executed on the remote-side and f_leak(b)
being executed on the local-side. But, filter of foreign-scan on ft1 has both
of qualifiers. It has to be removed, if a RestrictInfo get pushed-down.It's intentional that pgsql_fdw keeps pushed-down qualifier in
baserestrictinfo, because I saw some undesirable behavior when I
implemented so that with such optimization when plan is reused, but it's
not clear to me now. I'll try to recall what I saw...BTW, I think evaluating pushed-down qualifiers again on local side is
safe and has no semantic problem, though we must pay little for such
overhead. Do you have concern about performance?
Yes. In my opinion, one significant benefit of pgsql_fdw is to execute
qualifiers on the distributed nodes; that enables to utilize multiple
CPU resources efficiently.
Duplicate checks are reliable way to keep invisible tuples being filtered
out, indeed. But it shall degrade one competitive characteristics of the
pgsql_fdw.
https://github.com/kaigai/pg_strom/blob/master/plan.c#L693
In my module, qualifiers being executable on device side are detached
from the baserel->baserestrictinfo, and remaining qualifiers are chained
to the list.
The is_device_executable_qual() is equivalent to is_foreign_expr() in
the pgsql_fdw module.
Of course, it is your decision, and I might miss something.
BTW, what is the undesirable behavior on your previous implementation?
[Design comment]
I'm not sure the reason why store_result() uses MessageContext to save
the Tuplestorestate within PgsqlFdwExecutionState.
The source code comment says it is used to avoid memory leaks in error
cases. I also have a similar experience on implementation of my fdw module,
so, I could understand per-scan context is already cleared at the timing of
resource-release-callback, thus, handlers to external resource have to be
saved on separated memory context.
In my personal opinion, the right design is to declare a memory context for
pgsql_fdw itself, instead of the abuse of existing memory context.
(More wise design is to define sub-memory-context for each foreign-scan,
then, remove the sub-memory-context after release handlers.)I simply chose built-in context which has enough lifespan, but now I
think that using MessageContext directly is not recommended way. As you
say, creating new context as child of MessageContext for each scan in
BeginForeignScan (or first IterateForeignScan) would be better. Please
see attached patch.One other option is getting rid of tuplestore by holding result rows as
PGresult, and track it for error cases which might happen.
ResourceOwner callback can be used to release PGresult on error, similar
to PGconn.
If we could have set of results on per-query memory context (thus,
no need to explicit release on error timing), it is more ideal design.
It it possible to implement based on the libpq APIs?
Please note that per-query memory context is already released on
ResourceOwner callback is launched, so, it is unavailable to implement
if libpq requires to release some resource.
[Design comment]
When "BEGIN" should be issued on the remote-side?
The connect_pg_server() is an only chance to issue "BEGIN" command
at the remote-side on connection being opened. However, the connection
shall be kept unless an error is not raised. Thus, the remote-side will
continue to work within a single transaction block, even if local-side iterates
a pair of "BEGIN" and "COMMIT".
I'd like to suggest to close the transaction block at the timing of either
end of the scan, transaction or sub-transaction.Indeed, remote transactions should be terminated at some timing.
Terminating at the end of a scan seems troublesome because a connection
might be shared by multiple scans in a query. I'd prefer aborting
remote transaction at the end of local query. Please see
abort_remote_tx in attached patch.
It seems to me abort_remote_tx in ReleaseConnection() is reasonable.
However, isn't it needed to have ABORT in GetConnection() at first time?
[Comment to Improve]
Also, which transaction isolation level should be specified in this case?
An idea is its isolation level is specified according to the current isolation
level on the local-side.
(Of course, it is your choice if it is not supported right now.)Choosing same as local seems better. Please see start_remote_tx
function in attached patch.
It seems to me reasonable.
[Comment to improve]
It seems to me the design of exprFunction is not future-proof, if we add
a new node type that contains two or more function calls, because it can
return an oid of functions.
I think, the right design is to handle individual node-types within the
large switch statement at foreign_expr_walker().
Of course, it is just my sense.You mean that exprFunction should have capability to handle multiple
Oids for one node, maybe return List<oid> or something? IMO it's
overkill at this time.
Though I'm not sure that it's reasonable, but exprInputCollation too
seems to not assume that multiple input collation might be stored in one
node.
I'm still skeptical on the current logic to determine whether qualifier
is available to push down, or not.
For example, it checks oid of operator and oid of function being
invoked on this operator at OpExpr.
However, the purpose of this check is to ensure same result on
execution of qualifier, thus it restricts qualifiers to be pushed down
built-in database objects.
So, isn't it enough to check whether oid of OpExpr is built-in, or not?
(If oid of operator is built-in, its function is also built-in. Right?)
[Comment to improve]
The pgsql_fdw_handler() allocates FdwRoutine using makeNode(),
then it set function-pointers on each fields.
Why don't you return a pointer to statically declared FdwRoutine
variable being initialized at compile time, like:static FdwRoutine pgsql_fdw_handler = {
.type = T_FdwRoutine,
.PlanForeignScan = pgsqlPlanForeignScan,
.ExplainForeignScan = pgsqlExplainForeignScan,
.BeginForeignScan = pgsqlBeginForeignScan,
.IterateForeignScan = pgsqlIterateForeignScan,
.ReScanForeignScan = pgsqlReScanForeignScan,
.EndForeignScan = pgsqlEndForeignScan,
};Datum
pgsql_fdw_handler(PG_FUNCTION_ARGS)
{
PG_RETURN_POINTER(&pgsql_fdw_handler);
}Fixed to static variable without designated initializers, because it's
one of C99 feature. Is there any written policy about using C99
features in PG development? I've read Developer's FAQ (wiki) and code
formatting section of document, but I couldn't find any mention.
OK. It was my wrong suggestion.
[Question to implementation]
At pgsqlIterateForeignScan(), it applies null-check on festate->tuples
and bool-checks on festete->cursor_opened.
Do we have a possible scenario that festate->tuples is not null, but
festate->cursor_opened, or an opposite combination?
If null-check on festate->tuples is enough to detect the first call of
the iterate callback, it is not my preference to have redundant flag.[ checking... ] No such scenario. It's undesired remain of obsolete
support of simple SELECT mode; pgsql_fdw once had two fetching mode;
simple SELECT mode for small result and CURSOR mode for huge result.
I've removed cursor_opened from PgsqlFdwExecutionState structure.
OK, I understood the background of this design.
[Extra change]
In addition to your comments, I found that some regression tests fail
because current planner produces different plan tree from expected
results, and fixed such tests.
Thanks,
--
KaiGai Kohei <kaigai@kaigai.gr.jp>
2012/2/2 Marko Kreen <markokr@gmail.com>:
I think you want this instead:
 https://commitfest.postgresql.org/action/patch_view?id=769
Somehow I've missed this cool feature. Thanks for the suggestion!
--
Shigeru Hanada
Thanks for the comments!
(2012/02/06 5:08), Kohei KaiGai wrote:
Yes. In my opinion, one significant benefit of pgsql_fdw is to execute
qualifiers on the distributed nodes; that enables to utilize multiple
CPU resources efficiently.
Duplicate checks are reliable way to keep invisible tuples being filtered
out, indeed. But it shall degrade one competitive characteristics of the
pgsql_fdw.https://github.com/kaigai/pg_strom/blob/master/plan.c#L693
In my module, qualifiers being executable on device side are detached
from the baserel->baserestrictinfo, and remaining qualifiers are chained
to the list.
The is_device_executable_qual() is equivalent to is_foreign_expr() in
the pgsql_fdw module.
Agreed, I too think that pushed-down qualifiers should not be evaluated
on local side again from the viewpoint of performance.
Of course, it is your decision, and I might miss something.
BTW, what is the undesirable behavior on your previous implementation?
In early development, maybe during testing PREPARE/EXECUTE or DECALRE, I
saw that iterated execution of foreign scan produce wrong result which
includes rows which are NOT match pushed-down qualifiers. And, at last,
I could recall what happened at that time. It was just trivial bug I
made. Perhaps I've removed pushed-down qualifiers in Path generation
phase, so generated plan node has lost qualifiers permanently.
In short, I'll remove remote qualifiers from baserestrictinfo, like
pg_storm.
[Design comment]
I'm not sure the reason why store_result() uses MessageContext to save
the Tuplestorestate within PgsqlFdwExecutionState.
The source code comment says it is used to avoid memory leaks in error
cases. I also have a similar experience on implementation of my fdw module,
so, I could understand per-scan context is already cleared at the timing of
resource-release-callback, thus, handlers to external resource have to be
saved on separated memory context.
In my personal opinion, the right design is to declare a memory context for
pgsql_fdw itself, instead of the abuse of existing memory context.
(More wise design is to define sub-memory-context for each foreign-scan,
then, remove the sub-memory-context after release handlers.)I simply chose built-in context which has enough lifespan, but now I
think that using MessageContext directly is not recommended way. As you
say, creating new context as child of MessageContext for each scan in
BeginForeignScan (or first IterateForeignScan) would be better. Please
see attached patch.One other option is getting rid of tuplestore by holding result rows as
PGresult, and track it for error cases which might happen.
ResourceOwner callback can be used to release PGresult on error, similar
to PGconn.If we could have set of results on per-query memory context (thus,
no need to explicit release on error timing), it is more ideal design.
It it possible to implement based on the libpq APIs?
Currently no, so I used tuplestore even though it needs coping results.
However, Kyotaro Horiguchi's patch might make it possible. I'm reading
his patch to determine whether it suits pgsql_fdw.
http://archives.postgresql.org/message-id/20120202143057.GA12434@gmail.com
Please note that per-query memory context is already released on
ResourceOwner callback is launched, so, it is unavailable to implement
if libpq requires to release some resource.
I see. We need to use context which has longer lifetime if we want to
track malloc'ed PQresult. I already use CacheContext for connection
pooling, so linking PGreslts to its source connection would be a solutions.
[Design comment]
When "BEGIN" should be issued on the remote-side?
The connect_pg_server() is an only chance to issue "BEGIN" command
at the remote-side on connection being opened. However, the connection
shall be kept unless an error is not raised. Thus, the remote-side will
continue to work within a single transaction block, even if local-side iterates
a pair of "BEGIN" and "COMMIT".
I'd like to suggest to close the transaction block at the timing of either
end of the scan, transaction or sub-transaction.Indeed, remote transactions should be terminated at some timing.
Terminating at the end of a scan seems troublesome because a connection
might be shared by multiple scans in a query. I'd prefer aborting
remote transaction at the end of local query. Please see
abort_remote_tx in attached patch.It seems to me abort_remote_tx in ReleaseConnection() is reasonable.
However, isn't it needed to have ABORT in GetConnection() at first time?
Hm, forcing overhead of aborting transaction to all local queries is
unreasonable. Redundant BEGIN doesn't cause error but just generate
WARNING, so I'll remove abort_remote_tx preceding begin_remote_tx.
[Comment to improve]
It seems to me the design of exprFunction is not future-proof, if we add
a new node type that contains two or more function calls, because it can
return an oid of functions.
I think, the right design is to handle individual node-types within the
large switch statement at foreign_expr_walker().
Of course, it is just my sense.You mean that exprFunction should have capability to handle multiple
Oids for one node, maybe return List<oid> or something? IMO it's
overkill at this time.
Though I'm not sure that it's reasonable, but exprInputCollation too
seems to not assume that multiple input collation might be stored in one
node.I'm still skeptical on the current logic to determine whether qualifier
is available to push down, or not.For example, it checks oid of operator and oid of function being
invoked on this operator at OpExpr.
However, the purpose of this check is to ensure same result on
execution of qualifier, thus it restricts qualifiers to be pushed down
built-in database objects.
So, isn't it enough to check whether oid of OpExpr is built-in, or not?
(If oid of operator is built-in, its function is also built-in. Right?)
Basically yes. If a built-in operator has different semantics on remote
side (probably pg_operator catalog has been changed manually by
superuser), it can be said that nothing is reliable in the context of
pgsql_fdw. I added checking function's oid of operators just in case,
and now it seems to me paranoid concern...
I'll post revised patches soon.
Regards,
--
Shigeru Hanada
(2012/02/06 17:37), Shigeru Hanada wrote:
I'll post revised patches soon.
Attached revised patches. Changes from last version are below.
[fdw_helper_v3.patch]
none
[pgsql_fdw_v7.patch]
* Don't abort remote transaction before starting remote transaction.
* Add _PQ_init/_PQ_fini functions
* Fix some typos in comments.
* Revise transaction management section in document.
* Update EXPLAIN sample in document.
[pgsql_fdw_]
* Avoid redundant evaluation of pushed-down quals.
* Fix some typos in comments.
* Update EXPLAIN sample in document.
Regards,
--
Shigeru Hanada
Attachments:
fdw_helper_v5.patchtext/plain; name=fdw_helper_v5.patchDownload
diff --git a/contrib/file_fdw/file_fdw.c b/contrib/file_fdw/file_fdw.c
index 46394a8..a522e36 100644
*** a/contrib/file_fdw/file_fdw.c
--- b/contrib/file_fdw/file_fdw.c
***************
*** 26,32 ****
#include "nodes/makefuncs.h"
#include "optimizer/cost.h"
#include "utils/rel.h"
! #include "utils/syscache.h"
PG_MODULE_MAGIC;
--- 26,32 ----
#include "nodes/makefuncs.h"
#include "optimizer/cost.h"
#include "utils/rel.h"
! #include "utils/lsyscache.h"
PG_MODULE_MAGIC;
*************** get_file_fdw_attribute_options(Oid relid
*** 345,398 ****
/* Retrieve FDW options for all user-defined attributes. */
for (attnum = 1; attnum <= natts; attnum++)
{
! HeapTuple tuple;
! Form_pg_attribute attr;
! Datum datum;
! bool isnull;
/* Skip dropped attributes. */
if (tupleDesc->attrs[attnum - 1]->attisdropped)
continue;
! /*
! * We need the whole pg_attribute tuple not just what is in the
! * tupleDesc, so must do a catalog lookup.
! */
! tuple = SearchSysCache2(ATTNUM,
! RelationGetRelid(rel),
! Int16GetDatum(attnum));
! if (!HeapTupleIsValid(tuple))
! elog(ERROR, "cache lookup failed for attribute %d of relation %u",
! attnum, RelationGetRelid(rel));
! attr = (Form_pg_attribute) GETSTRUCT(tuple);
!
! datum = SysCacheGetAttr(ATTNUM,
! tuple,
! Anum_pg_attribute_attfdwoptions,
! &isnull);
! if (!isnull)
{
! List *options = untransformRelOptions(datum);
! ListCell *lc;
! foreach(lc, options)
{
! DefElem *def = (DefElem *) lfirst(lc);
!
! if (strcmp(def->defname, "force_not_null") == 0)
{
! if (defGetBoolean(def))
! {
! char *attname = pstrdup(NameStr(attr->attname));
! fnncolumns = lappend(fnncolumns, makeString(attname));
! }
}
- /* maybe in future handle other options here */
}
}
-
- ReleaseSysCache(tuple);
}
heap_close(rel, AccessShareLock);
--- 345,373 ----
/* Retrieve FDW options for all user-defined attributes. */
for (attnum = 1; attnum <= natts; attnum++)
{
! List *options;
! ListCell *lc;
/* Skip dropped attributes. */
if (tupleDesc->attrs[attnum - 1]->attisdropped)
continue;
! options = GetForeignColumnOptions(relid, attnum);
! foreach(lc, options)
{
! DefElem *def = (DefElem *) lfirst(lc);
! if (strcmp(def->defname, "force_not_null") == 0)
{
! if (defGetBoolean(def))
{
! char *attname = pstrdup(get_attname(relid, attnum));
! fnncolumns = lappend(fnncolumns, makeString(attname));
}
}
+ /* maybe in future handle other options here */
}
}
heap_close(rel, AccessShareLock);
diff --git a/doc/src/sgml/fdwhandler.sgml b/doc/src/sgml/fdwhandler.sgml
index 76ff243..d809cac 100644
*** a/doc/src/sgml/fdwhandler.sgml
--- b/doc/src/sgml/fdwhandler.sgml
*************** EndForeignScan (ForeignScanState *node);
*** 235,238 ****
--- 235,392 ----
</sect1>
+ <sect1 id="fdw-helpers">
+ <title>Foreign Data Wrapper Helper Functions</title>
+
+ <para>
+ Several helper functions are exported from core so that authors of FDW
+ can get easy access to attributes of FDW-related objects such as FDW
+ options.
+ </para>
+
+ <para>
+ <programlisting>
+ ForeignDataWrapper *
+ GetForeignDataWrapper(Oid fdwid);
+ </programlisting>
+
+ This function returns a <structname>ForeignDataWrapper</structname> object
+ for a foreign-data wrapper with given oid. A
+ <structname>ForeignDataWrapper</structname> object contains oid of the
+ wrapper itself, oid of the owner of the wrapper, name of the wrapper, oid
+ of fdwhandler function, oid of fdwvalidator function, and FDW options in
+ the form of list of <structname>DefElem</structname>.
+ </para>
+
+ <para>
+ <programlisting>
+ ForeignServer *
+ GetForeignServer(Oid serverid);
+ </programlisting>
+
+ This function returns a <structname>ForeignServer</structname> object for
+ a foreign server with given oid. A <structname>ForeignServer</structname>
+ object contains oid of the server, oid of the wrapper for the server, oid
+ of the owner of the server, name of the server, type of the server,
+ version of the server, and FDW options in the form of list of
+ <structname>DefElem</structname>.
+ </para>
+
+ <para>
+ <programlisting>
+ UserMapping *
+ GetUserMapping(Oid userid, Oid serverid);
+ </programlisting>
+
+ This function returns a <structname>UserMapping</structname> object for a
+ user mapping with given oid pair. A <structname>UserMapping</structname>
+ object contains oid of the user, oid of the server, and FDW options in the
+ form of list of <structname>DefElem</structname>.
+ </para>
+
+ <para>
+ <programlisting>
+ ForeignTable *
+ GetForeignTable(Oid relid);
+ </programlisting>
+
+ This function returns a <structname>ForeignTable</structname> object for a
+ foreign table with given oid. A <structname>ForeignTable</structname>
+ object contains oid of the foreign table, oid of the server for the table,
+ and FDW options in the form of list of <structname>DefElem</structname>.
+ </para>
+
+ <para>
+ <programlisting>
+ List *
+ GetForeignTableColumnOptions(Oid relid, AttrNumber attnum);
+ </programlisting>
+
+ This function returns per-column FDW options for a column with given
+ relation oid and attribute number in the form of list of
+ <structname>DefElem</structname>.
+ </para>
+
+ <para>
+ Some object types have name-based functions.
+ </para>
+
+ <para>
+ <programlisting>
+ ForeignDataWrapper *
+ GetForeignDataWrapperByName(const char *name, bool missing_ok);
+ </programlisting>
+
+ This function returns a <structname>ForeignDataWrapper</structname> object
+ for a foreign-data wrapper with given name. If the wrapper is not found,
+ return NULL if missing_ok was true, otherwise raise an error.
+ </para>
+
+ <para>
+ <programlisting>
+ ForeignServer *
+ GetForeignServerByName(const char *name, bool missing_ok);
+ </programlisting>
+
+ This function returns a <structname>ForeignServer</structname> object for
+ a foreign server with given name. If the server is not found, return NULL
+ if missing_ok was true, otherwise raise an error.
+ </para>
+
+ <para>
+ <programlisting>
+ char *
+ GetFdwOptionValue(Oid fdwid, Oid serverid, Oid relid, AttrNumber attnum, const char *optname);
+ </programlisting>
+
+ This function returns a copied string (created in current memory context)
+ of the value of a FDW option with given name which is set on a object with
+ given oid and attribute number. This function ignores catalogs if invalid
+ identifir is given for it.
+
+ <itemizedlist>
+ <listitem>
+ <para>
+ If attnum is <literal>InvalidAttrNumber</literal> or relid is
+ <literal>Invalidoid</literal>, <structname>pg_attribute</structname> is
+ ignored.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ If relid is <literal>InvalidOid</literal>,
+ <structname>pg_foreign_table</structname> is ignored.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ If both serverid and relid are <literal>InvalidOid</literal>,
+ <structname>pg_foreign_server</structname> is ignored.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ If all of fdwid, serverid and relid are <literal>InvalidOid</literal>,
+ <structname>pg_foreign_data_wrapper</structname> is ignored.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+
+ <para>
+ If the option with given name is set in multiple object level, the one in
+ the finest-grained object is used; e.g. priority is given to user mappings
+ over than foreign servers.
+ This function would be useful when you know which option is needed but you
+ don't know where it is set. If you already know the source object, it
+ would be more efficient to use object retrieval functions.
+ </para>
+
+ <para>
+ To use any of these functions, you need to include
+ <filename>foreign/foreign.h</filename> in your source file.
+ </para>
+
+ </sect1>
+
</chapter>
diff --git a/src/backend/foreign/foreign.c b/src/backend/foreign/foreign.c
index c4c2a61..8e98f0f 100644
*** a/src/backend/foreign/foreign.c
--- b/src/backend/foreign/foreign.c
*************** GetForeignTable(Oid relid)
*** 248,253 ****
--- 248,378 ----
/*
+ * If an option entry which matches the given name was found in the given
+ * list, returns a copy of arg string, otherwise returns NULL.
+ */
+ static char *
+ get_options_value(List *options, const char *optname)
+ {
+ ListCell *lc;
+
+ /* Find target option from the list. */
+ foreach (lc, options)
+ {
+ DefElem *def = lfirst(lc);
+
+ if (strcmp(def->defname, optname) == 0)
+ return pstrdup(strVal(def->arg));
+ }
+
+ return NULL;
+ }
+
+ /*
+ * Returns a copy of the value of specified option with searching the option
+ * from appropriate catalog. If an option was stored in multiple object
+ * levels, one in the finest-grained object level is used; lookup order is:
+ * 1) pg_attribute (only when both attnum and relid are valid)
+ * 2) pg_foreign_table (only when relid is valid)
+ * 3) pg_user_mapping (only when serverid or relid is valid)
+ * 4) pg_foreign_server (only when serverid or relid is valid)
+ * 5) pg_foreign_data_wrapper (only when fdwid or serverid or relid is valid)
+ * This priority rule would be useful in most cases using FDW options.
+ *
+ * If attnum was InvalidAttrNumber, we don't retrieve FDW optiosn from
+ * pg_attribute.attfdwoptions.
+ */
+ char *
+ GetFdwOptionValue(Oid fdwid, Oid serverid, Oid relid, AttrNumber attnum,
+ const char *optname)
+ {
+ ForeignTable *table = NULL;
+ UserMapping *user = NULL;
+ ForeignServer *server = NULL;
+ ForeignDataWrapper *wrapper = NULL;
+ char *value;
+
+ /* Do we need to search pg_attribute? */
+ if (attnum != InvalidAttrNumber && relid != InvalidOid)
+ {
+ value = get_options_value(GetForeignColumnOptions(relid, attnum),
+ optname);
+ if (value != NULL)
+ return value;
+ }
+
+ /* Do we need to search pg_foreign_table? */
+ if (relid != InvalidOid)
+ {
+ table = GetForeignTable(relid);
+ value = get_options_value(table->options, optname);
+ if (value != NULL)
+ return value;
+
+ serverid = table->serverid;
+ }
+
+ /* Do we need to search pg_user_mapping and pg_foreign_server? */
+ if (serverid != InvalidOid)
+ {
+ user = GetUserMapping(GetOuterUserId(), serverid);
+ value = get_options_value(user->options, optname);
+ if (value != NULL)
+ return value;
+
+ server = GetForeignServer(serverid);
+ value = get_options_value(server->options, optname);
+ if (value != NULL)
+ return value;
+
+ fdwid = server->fdwid;
+ }
+
+ /* Do we need to search pg_foreign_data_wrapper? */
+ if (fdwid != InvalidOid)
+ {
+ wrapper = GetForeignDataWrapper(fdwid);
+ value = get_options_value(wrapper->options, optname);
+ if (value != NULL)
+ return value;
+ }
+
+ return NULL;
+ }
+
+
+ /*
+ * GetForeignColumnOptions - Get attfdwoptions of given relation/attnum as
+ * list of DefElem.
+ */
+ List *
+ GetForeignColumnOptions(Oid relid, AttrNumber attnum)
+ {
+ List *options;
+ HeapTuple tp;
+ Datum datum;
+ bool isnull;
+
+ tp = SearchSysCache2(ATTNUM,
+ ObjectIdGetDatum(relid),
+ Int16GetDatum(attnum));
+ if (!HeapTupleIsValid(tp))
+ elog(ERROR, "cache lookup failed for attribute %d of relation %u", attnum, relid);
+ datum = SysCacheGetAttr(ATTNUM,
+ tp,
+ Anum_pg_attribute_attfdwoptions,
+ &isnull);
+ if (isnull)
+ options = NIL;
+ else
+ options = untransformRelOptions(datum);
+
+ ReleaseSysCache(tp);
+
+ return options;
+ }
+
+ /*
* GetFdwRoutine - call the specified foreign-data wrapper handler routine
* to get its FdwRoutine struct.
*/
diff --git a/src/include/foreign/foreign.h b/src/include/foreign/foreign.h
index 191122d..e25b871 100644
*** a/src/include/foreign/foreign.h
--- b/src/include/foreign/foreign.h
*************** extern ForeignDataWrapper *GetForeignDat
*** 75,80 ****
--- 75,83 ----
extern ForeignDataWrapper *GetForeignDataWrapperByName(const char *name,
bool missing_ok);
extern ForeignTable *GetForeignTable(Oid relid);
+ extern char *GetFdwOptionValue(Oid fdwid, Oid serverid, Oid relid,
+ AttrNumber attnum, const char *optname);
+ extern List *GetForeignColumnOptions(Oid relid, AttrNumber attnum);
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);
pgsql_fdw_v7.patchtext/plain; name=pgsql_fdw_v7.patchDownload
diff --git a/contrib/Makefile b/contrib/Makefile
index ac0a80a..14aa433 100644
*** a/contrib/Makefile
--- b/contrib/Makefile
*************** SUBDIRS = \
*** 41,46 ****
--- 41,47 ----
pgbench \
pgcrypto \
pgrowlocks \
+ pgsql_fdw \
pgstattuple \
seg \
spi \
diff --git a/contrib/README b/contrib/README
index a1d42a1..d3fa211 100644
*** a/contrib/README
--- b/contrib/README
*************** pgrowlocks -
*** 158,163 ****
--- 158,167 ----
A function to return row locking information
by Tatsuo Ishii <ishii@sraoss.co.jp>
+ pgsql_fdw -
+ Foreign-data wrapper for external PostgreSQL servers.
+ by Shigeru Hanada <shigeru.hanada@gmail.com>
+
pgstattuple -
Functions to return statistics about "dead" tuples and free
space within a table
diff --git a/contrib/pgsql_fdw/.gitignore b/contrib/pgsql_fdw/.gitignore
index ...0854728 .
*** a/contrib/pgsql_fdw/.gitignore
--- b/contrib/pgsql_fdw/.gitignore
***************
*** 0 ****
--- 1,4 ----
+ # Generated subdirectories
+ /results/
+ *.o
+ *.so
diff --git a/contrib/pgsql_fdw/Makefile b/contrib/pgsql_fdw/Makefile
index ...6381365 .
*** a/contrib/pgsql_fdw/Makefile
--- b/contrib/pgsql_fdw/Makefile
***************
*** 0 ****
--- 1,22 ----
+ # contrib/pgsql_fdw/Makefile
+
+ MODULE_big = pgsql_fdw
+ OBJS = pgsql_fdw.o option.o deparse.o connection.o
+ PG_CPPFLAGS = -I$(libpq_srcdir)
+ SHLIB_LINK = $(libpq)
+
+ EXTENSION = pgsql_fdw
+ DATA = pgsql_fdw--1.0.sql
+
+ REGRESS = pgsql_fdw
+
+ ifdef USE_PGXS
+ PG_CONFIG = pg_config
+ PGXS := $(shell $(PG_CONFIG) --pgxs)
+ include $(PGXS)
+ else
+ subdir = contrib/pgsql_fdw
+ top_builddir = ../..
+ include $(top_builddir)/src/Makefile.global
+ include $(top_srcdir)/contrib/contrib-global.mk
+ endif
diff --git a/contrib/pgsql_fdw/connection.c b/contrib/pgsql_fdw/connection.c
index ...6ede259 .
*** a/contrib/pgsql_fdw/connection.c
--- b/contrib/pgsql_fdw/connection.c
***************
*** 0 ****
--- 1,553 ----
+ /*-------------------------------------------------------------------------
+ *
+ * connection.c
+ * Connection management for pgsql_fdw
+ *
+ * Portions Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/connection.c
+ *
+ *-------------------------------------------------------------------------
+ */
+ #include "postgres.h"
+
+ #include "access/xact.h"
+ #include "catalog/pg_type.h"
+ #include "foreign/foreign.h"
+ #include "funcapi.h"
+ #include "libpq-fe.h"
+ #include "mb/pg_wchar.h"
+ #include "miscadmin.h"
+ #include "utils/array.h"
+ #include "utils/builtins.h"
+ #include "utils/hsearch.h"
+ #include "utils/memutils.h"
+ #include "utils/resowner.h"
+ #include "utils/tuplestore.h"
+
+ #include "pgsql_fdw.h"
+ #include "connection.h"
+
+ /* ============================================================================
+ * Connection management functions
+ * ==========================================================================*/
+
+ /*
+ * Connection cache entry managed with hash table.
+ */
+ typedef struct ConnCacheEntry
+ {
+ /* hash key must be first */
+ Oid serverid; /* oid of foreign server */
+ Oid userid; /* oid of local user */
+
+ bool use_tx; /* true when using remote transaction */
+ 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);
+ static void begin_remote_tx(PGconn *conn);
+ static void abort_remote_tx(PGconn *conn);
+
+ /*
+ * 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.
+ *
+ * When use_tx is true, remote transaction is started if caller is the only
+ * user of the connection. Isolation level of the remote transaction is same
+ * as local transaction, and remote transaction will be aborted when last
+ * user release.
+ *
+ * TODO: Note that caching connections requires a mechanism to detect change of
+ * FDW object to invalidate already established connections.
+ */
+ PGconn *
+ GetConnection(ForeignServer *server, UserMapping *user, bool use_tx)
+ {
+ bool found;
+ ConnCacheEntry *entry;
+ ConnCacheEntry key;
+
+ /* initialize connection cache if it isn't */
+ if (FSConnectionHash == NULL)
+ {
+ HASHCTL ctl;
+
+ /* hash key is a pair of oids: serverid and userid */
+ MemSet(&ctl, 0, sizeof(ctl));
+ ctl.keysize = sizeof(Oid) + sizeof(Oid);
+ ctl.entrysize = sizeof(ConnCacheEntry);
+ ctl.hash = tag_hash;
+ ctl.match = memcmp;
+ ctl.keycopy = memcpy;
+ /* allocate FSConnectionHash in the cache context */
+ ctl.hcxt = CacheMemoryContext;
+ FSConnectionHash = hash_create("Foreign Connections", 32,
+ &ctl,
+ HASH_ELEM | HASH_CONTEXT |
+ HASH_FUNCTION | HASH_COMPARE |
+ HASH_KEYCOPY);
+ }
+
+ /* Create key value for the entry. */
+ MemSet(&key, 0, sizeof(key));
+ key.serverid = server->serverid;
+ key.userid = GetOuterUserId();
+
+ /*
+ * Find cached entry for requested connection. If we couldn't find,
+ * callback function of ResourceOwner should be registered to clean the
+ * connection up on error including user interrupt.
+ */
+ entry = hash_search(FSConnectionHash, &key, HASH_ENTER, &found);
+ if (!found)
+ {
+ entry->refs = 0;
+ entry->use_tx = false;
+ entry->conn = NULL;
+ RegisterResourceReleaseCallback(cleanup_connection, entry);
+ }
+
+ /*
+ * We don't check the health of cached connection here, because it would
+ * require some overhead. Broken connection and its cache entry will be
+ * cleaned up when the connection is actually used.
+ */
+
+ /*
+ * If cache entry doesn't have connection, we have to establish new
+ * connection.
+ */
+ if (entry->conn == NULL)
+ {
+ /*
+ * Use PG_TRY block to ensure closing connection on error.
+ */
+ PG_TRY();
+ {
+ /*
+ * Connect to the foreign PostgreSQL server, and store it in cache
+ * entry to keep new connection.
+ * Note: key items of entry has already been initialized in
+ * hash_search(HASH_ENTER).
+ */
+ entry->conn = connect_pg_server(server, user);
+ }
+ PG_CATCH();
+ {
+ /* Clear connection cache entry on error case. */
+ PQfinish(entry->conn);
+ entry->refs = 0;
+ entry->use_tx = false;
+ entry->conn = NULL;
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+ }
+
+ /* Increase connection reference counter. */
+ entry->refs++;
+
+ /*
+ * If requester is the only referrer of this connection, start transaction
+ * with the same isolation level as the local transaction we are in.
+ * We need to remember whether this connection uses remote transaction to
+ * abort it when this connection is released completely.
+ */
+ if (use_tx && entry->refs == 1)
+ begin_remote_tx(entry->conn);
+ entry->use_tx = use_tx;
+
+
+ return entry->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 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. Those two object hold only libpq options.
+ * Extra 3 items are for:
+ * *) fallback_application_name
+ * *) client_encoding
+ * *) NULL termination (end marker)
+ *
+ * Note: We don't omit any parameters even target database might be older
+ * than local, because unexpected parameters are just ignored.
+ */
+ n = list_length(server->options) + list_length(user->options) + 3;
+ 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 += ExtractConnectionOptions(server->options,
+ all_keywords + n, all_values + n);
+ n += ExtractConnectionOptions(user->options,
+ all_keywords + n, all_values + n);
+ all_keywords[n] = all_values[n] = NULL;
+
+ for (i = 0, j = 0; all_keywords[i]; i++)
+ {
+ keywords[j] = all_keywords[i];
+ values[j] = all_values[i];
+ j++;
+ }
+
+ /* Use "pgsql_fdw" as fallback_application_name. */
+ keywords[j] = "fallback_application_name";
+ values[j++] = "pgsql_fdw";
+
+ /* Set client_encoding so that libpq can convert encoding properly. */
+ keywords[j] = "client_encoding";
+ values[j++] = GetDatabaseEncodingName();
+
+ keywords[j] = values[j] = NULL;
+ pfree(all_keywords);
+ pfree(all_values);
+
+ /* 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.")));
+ }
+
+ return conn;
+ }
+
+ /*
+ * Start transaction to use cursor to retrieve data separately.
+ */
+ static void
+ begin_remote_tx(PGconn *conn)
+ {
+ const char *sql;
+ PGresult *res;
+
+ switch (XactIsoLevel)
+ {
+ case XACT_READ_UNCOMMITTED:
+ sql = "START TRANSACTION ISOLATION LEVEL READ UNCOMMITTED";
+ break;
+ case XACT_READ_COMMITTED:
+ sql = "START TRANSACTION ISOLATION LEVEL READ COMMITTED";
+ break;
+ case XACT_REPEATABLE_READ:
+ sql = "START TRANSACTION ISOLATION LEVEL REPEATABLE READ";
+ break;
+ case XACT_SERIALIZABLE:
+ sql = "START TRANSACTION ISOLATION LEVEL SERIALIZABLE";
+ break;
+ default:
+ elog(ERROR, "unexpected isolation level: %d", XactIsoLevel);
+ break;
+ }
+
+ elog(DEBUG3, "starting remote transaction with \"%s\"", sql);
+
+ res = PQexec(conn, sql);
+ if (PQresultStatus(res) != PGRES_COMMAND_OK)
+ {
+ PQclear(res);
+ elog(ERROR, "could not start transaction: %s", PQerrorMessage(conn));
+ }
+ PQclear(res);
+ }
+
+ static void
+ abort_remote_tx(PGconn *conn)
+ {
+ PGresult *res;
+
+ elog(DEBUG3, "aborting remote transaction");
+
+ res = PQexec(conn, "ABORT TRANSACTION");
+ if (PQresultStatus(res) != PGRES_COMMAND_OK)
+ {
+ PQclear(res);
+ elog(ERROR, "could not abort transaction: %s", PQerrorMessage(conn));
+ }
+ PQclear(res);
+ }
+
+ /*
+ * 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 sequentially 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;
+ }
+ if (entry != NULL)
+ hash_seq_term(&scan);
+
+ /*
+ * If this connection uses remote transaction and caller is the last user,
+ * abort remote transaction and forget about it.
+ */
+ if (entry->use_tx && entry->refs == 1)
+ {
+ abort_remote_tx(conn);
+ entry->use_tx = false;
+ }
+
+ /*
+ * If the released connection was an orphan, just close it.
+ */
+ if (entry == NULL)
+ {
+ PQfinish(conn);
+ return;
+ }
+
+ /*
+ * Decrease reference counter of this connection. Even if the caller was
+ * the last referrer, we don't unregister it from cache.
+ */
+ entry->refs--;
+ }
+
+ /*
+ * 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)
+ {
+ PQfinish(entry->conn);
+ entry->refs = 0;
+ entry->conn = NULL;
+ }
+ }
+
+ /*
+ * Get list of connections currently active.
+ */
+ Datum pgsql_fdw_get_connections(PG_FUNCTION_ARGS);
+ PG_FUNCTION_INFO_V1(pgsql_fdw_get_connections);
+ Datum
+ pgsql_fdw_get_connections(PG_FUNCTION_ARGS)
+ {
+ ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+ HASH_SEQ_STATUS scan;
+ ConnCacheEntry *entry;
+ MemoryContext oldcontext = CurrentMemoryContext;
+ Tuplestorestate *tuplestore;
+ TupleDesc tupdesc;
+
+ /* We return list of connection with storing them in a Tuplestore. */
+ rsinfo->returnMode = SFRM_Materialize;
+ rsinfo->setResult = NULL;
+ rsinfo->setDesc = NULL;
+
+ /* Create tuplestore and copy of TupleDesc in per-query context. */
+ MemoryContextSwitchTo(rsinfo->econtext->ecxt_per_query_memory);
+
+ tupdesc = CreateTemplateTupleDesc(2, false);
+ TupleDescInitEntry(tupdesc, 1, "srvid", OIDOID, -1, 0);
+ TupleDescInitEntry(tupdesc, 2, "usesysid", OIDOID, -1, 0);
+ rsinfo->setDesc = tupdesc;
+
+ tuplestore = tuplestore_begin_heap(false, false, work_mem);
+ rsinfo->setResult = tuplestore;
+
+ MemoryContextSwitchTo(oldcontext);
+
+ /*
+ * We need to scan sequentially since we use the address to find
+ * appropriate PGconn from the hash table.
+ */
+ if (FSConnectionHash != NULL)
+ {
+ hash_seq_init(&scan, FSConnectionHash);
+ while ((entry = (ConnCacheEntry *) hash_seq_search(&scan)))
+ {
+ Datum values[2];
+ bool nulls[2];
+ HeapTuple tuple;
+
+ /* Ignore inactive connections */
+ if (PQstatus(entry->conn) != CONNECTION_OK)
+ continue;
+
+ /*
+ * Ignore other users' connections if current user isn't a
+ * superuser.
+ */
+ if (!superuser() && entry->userid != GetUserId())
+ continue;
+
+ values[0] = ObjectIdGetDatum(entry->serverid);
+ values[1] = ObjectIdGetDatum(entry->userid);
+ nulls[0] = false;
+ nulls[1] = false;
+
+ tuple = heap_formtuple(tupdesc, values, nulls);
+ tuplestore_puttuple(tuplestore, tuple);
+ }
+ }
+ tuplestore_donestoring(tuplestore);
+
+ PG_RETURN_VOID();
+ }
+
+ /*
+ * Discard persistent connection designated by given connection name.
+ */
+ Datum pgsql_fdw_disconnect(PG_FUNCTION_ARGS);
+ PG_FUNCTION_INFO_V1(pgsql_fdw_disconnect);
+ Datum
+ pgsql_fdw_disconnect(PG_FUNCTION_ARGS)
+ {
+ Oid serverid = PG_GETARG_OID(0);
+ Oid userid = PG_GETARG_OID(1);
+ ConnCacheEntry key;
+ ConnCacheEntry *entry = NULL;
+ bool found;
+
+ /* Non-superuser can't discard other users' connection. */
+ if (!superuser() && userid != GetOuterUserId())
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_RESOURCES),
+ errmsg("only superuser can discard other user's connection")));
+
+ /*
+ * If no connection has been established, or no such connections, just
+ * return "NG" to indicate nothing has done.
+ */
+ if (FSConnectionHash == NULL)
+ PG_RETURN_TEXT_P(cstring_to_text("NG"));
+
+ key.serverid = serverid;
+ key.userid = userid;
+ entry = hash_search(FSConnectionHash, &key, HASH_FIND, &found);
+ if (!found)
+ PG_RETURN_TEXT_P(cstring_to_text("NG"));
+
+ /* Discard cached connection, and clear reference counter. */
+ PQfinish(entry->conn);
+ entry->refs = 0;
+ entry->conn = NULL;
+
+ PG_RETURN_TEXT_P(cstring_to_text("OK"));
+ }
diff --git a/contrib/pgsql_fdw/connection.h b/contrib/pgsql_fdw/connection.h
index ...4427f56 .
*** a/contrib/pgsql_fdw/connection.h
--- b/contrib/pgsql_fdw/connection.h
***************
*** 0 ****
--- 1,25 ----
+ /*-------------------------------------------------------------------------
+ *
+ * connection.h
+ * Connection management for pgsql_fdw
+ *
+ * Portions Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_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, bool use_tx);
+ void ReleaseConnection(PGconn *conn);
+
+ #endif /* CONNECTION_H */
diff --git a/contrib/pgsql_fdw/deparse.c b/contrib/pgsql_fdw/deparse.c
index ...cf9e775 .
*** a/contrib/pgsql_fdw/deparse.c
--- b/contrib/pgsql_fdw/deparse.c
***************
*** 0 ****
--- 1,199 ----
+ /*-------------------------------------------------------------------------
+ *
+ * deparse.c
+ * query deparser for PostgreSQL
+ *
+ * Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/deparse.c
+ *
+ *-------------------------------------------------------------------------
+ */
+ #include "postgres.h"
+
+ #include "access/transam.h"
+ #include "foreign/foreign.h"
+ #include "lib/stringinfo.h"
+ #include "nodes/nodeFuncs.h"
+ #include "nodes/nodes.h"
+ #include "nodes/makefuncs.h"
+ #include "optimizer/clauses.h"
+ #include "optimizer/var.h"
+ #include "parser/parsetree.h"
+ #include "utils/builtins.h"
+ #include "utils/lsyscache.h"
+
+ #include "pgsql_fdw.h"
+
+ /*
+ * Context for walk-through the expression tree.
+ */
+ typedef struct foreign_executable_cxt
+ {
+ PlannerInfo *root;
+ RelOptInfo *foreignrel;
+ } foreign_executable_cxt;
+
+ /*
+ * Deparse query representation into SQL statement which suits for remote
+ * PostgreSQL server.
+ */
+ char *
+ deparseSql(Oid relid, PlannerInfo *root, RelOptInfo *baserel)
+ {
+ StringInfoData foreign_relname;
+ StringInfoData sql; /* builder for SQL statement */
+ bool first;
+ AttrNumber attr;
+ List *attr_used = NIL; /* List of AttNumber used in the query */
+ const char *nspname = NULL; /* plain namespace name */
+ const char *relname = NULL; /* plain relation name */
+ const char *q_nspname; /* quoted namespace name */
+ const char *q_relname; /* quoted relation name */
+ int i;
+ List *rtable = NIL;
+ List *context = NIL;
+
+ initStringInfo(&sql);
+ initStringInfo(&foreign_relname);
+
+ /*
+ * First of all, determine which column should be retrieved for this scan.
+ *
+ * We do this before deparsing SELECT clause because attributes which are
+ * not used in neither reltargetlist nor baserel->baserestrictinfo, quals
+ * evaluated on local, can be replaced with literal "NULL" in the SELECT
+ * clause to reduce overhead of tuple handling tuple and data transfer.
+ */
+ if (baserel->baserestrictinfo != NIL)
+ {
+ ListCell *lc;
+
+ foreach (lc, baserel->baserestrictinfo)
+ {
+ RestrictInfo *ri = (RestrictInfo *) lfirst(lc);
+ List *attrs;
+
+ /*
+ * We need to know which attributes are used in qual evaluated
+ * on the local server, because they should be listed in the
+ * SELECT clause of remote query. We can ignore attributes
+ * which are referenced only in ORDER BY/GROUP BY clause because
+ * such attributes has already been kept in reltargetlist.
+ */
+ attrs = pull_var_clause((Node *) ri->clause,
+ PVC_RECURSE_AGGREGATES,
+ PVC_RECURSE_PLACEHOLDERS);
+ attr_used = list_union(attr_used, attrs);
+ }
+ }
+
+ /*
+ * Determine foreign relation's qualified name. This is necessary for
+ * FROM clause and SELECT clause.
+ */
+ nspname = GetFdwOptionValue(InvalidOid, InvalidOid, relid,
+ InvalidAttrNumber, "nspname");
+ if (nspname == NULL)
+ nspname = get_namespace_name(get_rel_namespace(relid));
+ q_nspname = quote_identifier(nspname);
+
+ relname = GetFdwOptionValue(InvalidOid, InvalidOid, relid,
+ InvalidAttrNumber, "relname");
+ if (relname == NULL)
+ relname = get_rel_name(relid);
+ q_relname = quote_identifier(relname);
+
+ appendStringInfo(&foreign_relname, "%s.%s", q_nspname, q_relname);
+
+ /*
+ * We need to replace aliasname and colnames of the target relation so that
+ * constructed remote query is valid.
+ *
+ * Note that we skip first empty element of simple_rel_array. See also
+ * comments of simple_rel_array and simple_rte_array for the rationale.
+ */
+ for (i = 1; i < root->simple_rel_array_size; i++)
+ {
+ RangeTblEntry *rte = copyObject(root->simple_rte_array[i]);
+ List *newcolnames = NIL;
+
+ if (i == baserel->relid)
+ {
+ /*
+ * Create new list of column names which is used to deparse remote
+ * query from specified names or local column names. This list is
+ * used by deparse_expression.
+ */
+ for (attr = 1; attr <= baserel->max_attr; attr++)
+ {
+ char *colname;
+
+ /* Ignore dropped attributes. */
+ if (get_rte_attribute_is_dropped(rte, attr))
+ continue;
+
+ colname = GetFdwOptionValue(InvalidOid, InvalidOid, relid,
+ attr, "colname");
+ if (colname == NULL)
+ colname = strVal(list_nth(rte->eref->colnames, attr - 1));
+ newcolnames = lappend(newcolnames, makeString(colname));
+ }
+ rte->alias = makeAlias(relname, newcolnames);
+ }
+ rtable = lappend(rtable, rte);
+ }
+ context = deparse_context_for_rtelist(rtable);
+
+ /*
+ * deparse SELECT clause
+ *
+ * List attributes which are in either target list or local restriction.
+ * Unused attributes are replaced with a literal "NULL" for optimization.
+ */
+ appendStringInfo(&sql, "SELECT ");
+ attr_used = list_union(attr_used, baserel->reltargetlist);
+ first = true;
+ for (attr = 1; attr <= baserel->max_attr; attr++)
+ {
+ RangeTblEntry *rte = root->simple_rte_array[baserel->relid];
+ Var *var = NULL;
+ ListCell *lc;
+
+ /* Ignore dropped attributes. */
+ if (get_rte_attribute_is_dropped(rte, attr))
+ continue;
+
+ if (!first)
+ appendStringInfo(&sql, ", ");
+ first = false;
+
+ /*
+ * We use linear search here, but it wouldn't be problem since
+ * attr_used seems to not become so large.
+ */
+ foreach (lc, attr_used)
+ {
+ var = lfirst(lc);
+ if (var->varattno == attr)
+ break;
+ var = NULL;
+ }
+ if (var != NULL)
+ appendStringInfo(&sql, "%s",
+ deparse_expression((Node *) var, context, false, false));
+ else
+ appendStringInfo(&sql, "NULL");
+ }
+ appendStringInfoChar(&sql, ' ');
+
+ /*
+ * deparse FROM clause, including alias if any
+ */
+ appendStringInfo(&sql, "FROM %s", foreign_relname.data);
+
+ elog(DEBUG3, "Remote SQL: %s", sql.data);
+ return sql.data;
+ }
+
diff --git a/contrib/pgsql_fdw/expected/pgsql_fdw.out b/contrib/pgsql_fdw/expected/pgsql_fdw.out
index ...7aab32c .
*** a/contrib/pgsql_fdw/expected/pgsql_fdw.out
--- b/contrib/pgsql_fdw/expected/pgsql_fdw.out
***************
*** 0 ****
--- 1,528 ----
+ -- ===================================================================
+ -- create FDW objects
+ -- ===================================================================
+ CREATE EXTENSION pgsql_fdw;
+ CREATE SERVER loopback1 FOREIGN DATA WRAPPER pgsql_fdw;
+ CREATE SERVER loopback2 FOREIGN DATA WRAPPER pgsql_fdw
+ OPTIONS (dbname 'contrib_regression');
+ CREATE USER MAPPING FOR public SERVER loopback1
+ OPTIONS (user 'value', password 'value');
+ CREATE USER MAPPING FOR public SERVER loopback2;
+ CREATE FOREIGN TABLE ft1 (
+ c1 int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10)
+ ) SERVER loopback2;
+ CREATE FOREIGN TABLE ft2 (
+ c1 int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10)
+ ) SERVER loopback2;
+ -- ===================================================================
+ -- create objects used through FDW
+ -- ===================================================================
+ CREATE SCHEMA "S 1";
+ CREATE TABLE "S 1"."T 1" (
+ "C 1" int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10),
+ CONSTRAINT t1_pkey PRIMARY KEY ("C 1")
+ );
+ NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "t1_pkey" for table "T 1"
+ CREATE TABLE "S 1"."T 2" (
+ c1 int NOT NULL,
+ c2 text,
+ CONSTRAINT t2_pkey PRIMARY KEY (c1)
+ );
+ NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "t2_pkey" for table "T 2"
+ BEGIN;
+ TRUNCATE "S 1"."T 1";
+ INSERT INTO "S 1"."T 1"
+ SELECT id,
+ id % 10,
+ to_char(id, 'FM00000'),
+ '1970-01-01'::timestamptz + ((id % 100) || ' days')::interval,
+ '1970-01-01'::timestamp + ((id % 100) || ' days')::interval,
+ id % 10,
+ id % 10
+ FROM generate_series(1, 1000) id;
+ TRUNCATE "S 1"."T 2";
+ INSERT INTO "S 1"."T 2"
+ SELECT id,
+ 'AAA' || to_char(id, 'FM000')
+ FROM generate_series(1, 100) id;
+ COMMIT;
+ -- ===================================================================
+ -- tests for pgsql_fdw_validator
+ -- ===================================================================
+ ALTER FOREIGN DATA WRAPPER pgsql_fdw OPTIONS (host 'value'); -- ERROR
+ ERROR: invalid option "host"
+ HINT: Valid options in this context are:
+ -- requiressl, krbsrvname and gsslib are omitted because they depend on
+ -- configure option
+ ALTER SERVER loopback1 OPTIONS (
+ authtype 'value',
+ service 'value',
+ connect_timeout 'value',
+ dbname 'value',
+ host 'value',
+ hostaddr 'value',
+ port 'value',
+ --client_encoding 'value',
+ tty 'value',
+ options 'value',
+ application_name 'value',
+ --fallback_application_name 'value',
+ keepalives 'value',
+ keepalives_idle 'value',
+ keepalives_interval 'value',
+ -- requiressl 'value',
+ sslmode 'value',
+ sslcert 'value',
+ sslkey 'value',
+ sslrootcert 'value',
+ sslcrl 'value'
+ --requirepeer 'value',
+ -- krbsrvname 'value',
+ -- gsslib 'value',
+ --replication 'value'
+ );
+ ALTER SERVER loopback1 OPTIONS (user 'value'); -- ERROR
+ ERROR: invalid option "user"
+ HINT: Valid options in this context are: authtype, service, connect_timeout, dbname, host, hostaddr, port, tty, options, application_name, keepalives, keepalives_idle, keepalives_interval, keepalives_count, sslmode, sslcert, sslkey, sslrootcert, sslcrl, requirepeer, fetch_count
+ ALTER SERVER loopback2 OPTIONS (ADD fetch_count '2');
+ ALTER USER MAPPING FOR public SERVER loopback1
+ OPTIONS (DROP user, DROP password);
+ ALTER USER MAPPING FOR public SERVER loopback1
+ OPTIONS (host 'value'); -- ERROR
+ ERROR: invalid option "host"
+ HINT: Valid options in this context are: user, password
+ ALTER FOREIGN TABLE ft1 OPTIONS (nspname 'S 1', relname 'T 1');
+ ALTER FOREIGN TABLE ft2 OPTIONS (nspname 'S 1', relname 'T 1', fetch_count '100');
+ ALTER FOREIGN TABLE ft1 OPTIONS (invalid 'value'); -- ERROR
+ ERROR: invalid option "invalid"
+ HINT: Valid options in this context are: nspname, relname, fetch_count
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count 'a'); -- ERROR
+ ERROR: invalid value for fetch_count: "a"
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count '0'); -- ERROR
+ ERROR: invalid value for fetch_count: "0"
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count '-1'); -- ERROR
+ ERROR: invalid value for fetch_count: "-1"
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (invalid 'value'); -- ERROR
+ ERROR: invalid option "invalid"
+ HINT: Valid options in this context are: colname
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (colname 'C 1');
+ ALTER FOREIGN TABLE ft2 ALTER COLUMN c1 OPTIONS (colname 'C 1');
+ \dew+
+ List of foreign-data wrappers
+ Name | Owner | Handler | Validator | Access privileges | FDW Options | Description
+ -----------+----------+-------------------+---------------------+-------------------+-------------+-------------
+ pgsql_fdw | postgres | pgsql_fdw_handler | pgsql_fdw_validator | | |
+ (1 row)
+
+ \des+
+ List of foreign servers
+ Name | Owner | Foreign-data wrapper | Access privileges | Type | Version | FDW Options | Description
+ -----------+----------+----------------------+-------------------+------+---------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+-------------
+ loopback1 | postgres | pgsql_fdw | | | | (authtype 'value', service 'value', connect_timeout 'value', dbname 'value', host 'value', hostaddr 'value', port 'value', tty 'value', options 'value', application_name 'value', keepalives 'value', keepalives_idle 'value', keepalives_interval 'value', sslmode 'value', sslcert 'value', sslkey 'value', sslrootcert 'value', sslcrl 'value') |
+ loopback2 | postgres | pgsql_fdw | | | | (dbname 'contrib_regression', fetch_count '2') |
+ (2 rows)
+
+ \deu+
+ List of user mappings
+ Server | User name | FDW Options
+ -----------+-----------+-------------
+ loopback1 | public |
+ loopback2 | public |
+ (2 rows)
+
+ \det+
+ List of foreign tables
+ Schema | Table | Server | FDW Options | Description
+ --------+-------+-----------+---------------------------------------------------+-------------
+ public | ft1 | loopback2 | (nspname 'S 1', relname 'T 1') |
+ public | ft2 | loopback2 | (nspname 'S 1', relname 'T 1', fetch_count '100') |
+ (2 rows)
+
+ -- ===================================================================
+ -- simple queries
+ -- ===================================================================
+ -- single table, with/without alias
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
+ QUERY PLAN
+ ------------------------------------------------------------------------------------------------------------------------------
+ Limit
+ Output: c1, c2, c3, c4, c5, c6, c7
+ -> Sort
+ Output: c1, c2, c3, c4, c5, c6, c7
+ Sort Key: ft1.c3, ft1.c1
+ -> Foreign Scan on public.ft1
+ Output: c1, c2, c3, c4, c5, c6, c7
+ Remote SQL: DECLARE pgsql_fdw_cursor_0 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (8 rows)
+
+ SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ -----+----+-------+------------------------------+--------------------------+----+------------
+ 101 | 1 | 00101 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970 | 1 | 1
+ 102 | 2 | 00102 | Sat Jan 03 00:00:00 1970 PST | Sat Jan 03 00:00:00 1970 | 2 | 2
+ 103 | 3 | 00103 | Sun Jan 04 00:00:00 1970 PST | Sun Jan 04 00:00:00 1970 | 3 | 3
+ 104 | 4 | 00104 | Mon Jan 05 00:00:00 1970 PST | Mon Jan 05 00:00:00 1970 | 4 | 4
+ 105 | 5 | 00105 | Tue Jan 06 00:00:00 1970 PST | Tue Jan 06 00:00:00 1970 | 5 | 5
+ 106 | 6 | 00106 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6
+ 107 | 7 | 00107 | Thu Jan 08 00:00:00 1970 PST | Thu Jan 08 00:00:00 1970 | 7 | 7
+ 108 | 8 | 00108 | Fri Jan 09 00:00:00 1970 PST | Fri Jan 09 00:00:00 1970 | 8 | 8
+ 109 | 9 | 00109 | Sat Jan 10 00:00:00 1970 PST | Sat Jan 10 00:00:00 1970 | 9 | 9
+ 110 | 0 | 00110 | Sun Jan 11 00:00:00 1970 PST | Sun Jan 11 00:00:00 1970 | 0 | 0
+ (10 rows)
+
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+ QUERY PLAN
+ ------------------------------------------------------------------------------------------------------------------------------
+ Limit
+ Output: c1, c2, c3, c4, c5, c6, c7
+ -> Sort
+ Output: c1, c2, c3, c4, c5, c6, c7
+ Sort Key: t1.c3, t1.c1
+ -> Foreign Scan on public.ft1 t1
+ Output: c1, c2, c3, c4, c5, c6, c7
+ Remote SQL: DECLARE pgsql_fdw_cursor_2 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (8 rows)
+
+ SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ -----+----+-------+------------------------------+--------------------------+----+------------
+ 101 | 1 | 00101 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970 | 1 | 1
+ 102 | 2 | 00102 | Sat Jan 03 00:00:00 1970 PST | Sat Jan 03 00:00:00 1970 | 2 | 2
+ 103 | 3 | 00103 | Sun Jan 04 00:00:00 1970 PST | Sun Jan 04 00:00:00 1970 | 3 | 3
+ 104 | 4 | 00104 | Mon Jan 05 00:00:00 1970 PST | Mon Jan 05 00:00:00 1970 | 4 | 4
+ 105 | 5 | 00105 | Tue Jan 06 00:00:00 1970 PST | Tue Jan 06 00:00:00 1970 | 5 | 5
+ 106 | 6 | 00106 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6
+ 107 | 7 | 00107 | Thu Jan 08 00:00:00 1970 PST | Thu Jan 08 00:00:00 1970 | 7 | 7
+ 108 | 8 | 00108 | Fri Jan 09 00:00:00 1970 PST | Fri Jan 09 00:00:00 1970 | 8 | 8
+ 109 | 9 | 00109 | Sat Jan 10 00:00:00 1970 PST | Sat Jan 10 00:00:00 1970 | 9 | 9
+ 110 | 0 | 00110 | Sun Jan 11 00:00:00 1970 PST | Sun Jan 11 00:00:00 1970 | 0 | 0
+ (10 rows)
+
+ -- with WHERE clause
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
+ QUERY PLAN
+ ------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on public.ft1 t1
+ Output: c1, c2, c3, c4, c5, c6, c7
+ Filter: ((t1.c1 = 101) AND ((t1.c6)::text = '1'::text) AND (t1.c7 = '1'::bpchar))
+ Remote SQL: DECLARE pgsql_fdw_cursor_4 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (4 rows)
+
+ SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ -----+----+-------+------------------------------+--------------------------+----+------------
+ 101 | 1 | 00101 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970 | 1 | 1
+ (1 row)
+
+ -- aggregate
+ SELECT COUNT(*) FROM ft1 t1;
+ count
+ -------
+ 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
+ ----+----+-------+------------------------------+--------------------------+----+------------
+ 1 | 1 | 00001 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970 | 1 | 1
+ 2 | 2 | 00002 | Sat Jan 03 00:00:00 1970 PST | Sat Jan 03 00:00:00 1970 | 2 | 2
+ 3 | 3 | 00003 | Sun Jan 04 00:00:00 1970 PST | Sun Jan 04 00:00:00 1970 | 3 | 3
+ 4 | 4 | 00004 | Mon Jan 05 00:00:00 1970 PST | Mon Jan 05 00:00:00 1970 | 4 | 4
+ 5 | 5 | 00005 | Tue Jan 06 00:00:00 1970 PST | Tue Jan 06 00:00:00 1970 | 5 | 5
+ 6 | 6 | 00006 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6
+ 7 | 7 | 00007 | Thu Jan 08 00:00:00 1970 PST | Thu Jan 08 00:00:00 1970 | 7 | 7
+ 8 | 8 | 00008 | Fri Jan 09 00:00:00 1970 PST | Fri Jan 09 00:00:00 1970 | 8 | 8
+ 9 | 9 | 00009 | Sat Jan 10 00:00:00 1970 PST | Sat Jan 10 00:00:00 1970 | 9 | 9
+ 10 | 0 | 00010 | Sun Jan 11 00:00:00 1970 PST | Sun Jan 11 00:00:00 1970 | 0 | 0
+ (10 rows)
+
+ -- subquery+MAX
+ SELECT * FROM ft1 t1 WHERE t1.c3 = (SELECT MAX(c3) FROM ft2 t2) ORDER BY c1;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ ------+----+-------+------------------------------+--------------------------+----+------------
+ 1000 | 0 | 01000 | Thu Jan 01 00:00:00 1970 PST | Thu Jan 01 00:00:00 1970 | 0 | 0
+ (1 row)
+
+ -- used in CTE
+ WITH t1 AS (SELECT * FROM ft1 WHERE c1 <= 10) SELECT t2.c1, t2.c2, t2.c3, t2.c4 FROM t1, ft2 t2 WHERE t1.c1 = t2.c1 ORDER BY t1.c1;
+ c1 | c2 | c3 | c4
+ ----+----+-------+------------------------------
+ 1 | 1 | 00001 | Fri Jan 02 00:00:00 1970 PST
+ 2 | 2 | 00002 | Sat Jan 03 00:00:00 1970 PST
+ 3 | 3 | 00003 | Sun Jan 04 00:00:00 1970 PST
+ 4 | 4 | 00004 | Mon Jan 05 00:00:00 1970 PST
+ 5 | 5 | 00005 | Tue Jan 06 00:00:00 1970 PST
+ 6 | 6 | 00006 | Wed Jan 07 00:00:00 1970 PST
+ 7 | 7 | 00007 | Thu Jan 08 00:00:00 1970 PST
+ 8 | 8 | 00008 | Fri Jan 09 00:00:00 1970 PST
+ 9 | 9 | 00009 | Sat Jan 10 00:00:00 1970 PST
+ 10 | 0 | 00010 | Sun Jan 11 00:00:00 1970 PST
+ (10 rows)
+
+ -- fixed values
+ SELECT 'fixed', NULL FROM ft1 t1 WHERE c1 = 1;
+ ?column? | ?column?
+ ----------+----------
+ fixed |
+ (1 row)
+
+ -- user-defined operator/function
+ CREATE FUNCTION pgsql_fdw_abs(int) RETURNS int AS $$
+ BEGIN
+ RETURN abs($1);
+ END
+ $$ LANGUAGE plpgsql IMMUTABLE;
+ CREATE OPERATOR === (
+ LEFTARG = int,
+ RIGHTARG = int,
+ PROCEDURE = int4eq,
+ COMMUTATOR = ===,
+ NEGATOR = !==
+ );
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = pgsql_fdw_abs(t1.c2);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = pgsql_fdw_abs(c2))
+ Remote SQL: DECLARE pgsql_fdw_cursor_18 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 === t1.c2;
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 === c2)
+ Remote SQL: DECLARE pgsql_fdw_cursor_19 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = abs(t1.c2);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = abs(c2))
+ Remote SQL: DECLARE pgsql_fdw_cursor_20 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = t1.c2;
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = c2)
+ Remote SQL: DECLARE pgsql_fdw_cursor_21 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ DROP OPERATOR === (int, int) CASCADE;
+ DROP FUNCTION pgsql_fdw_abs(int);
+ -- ===================================================================
+ -- parameterized queries
+ -- ===================================================================
+ -- simple join
+ PREPARE st1(int, int) AS SELECT t1.c3, t2.c3 FROM ft1 t1, ft2 t2 WHERE t1.c1 = $1 AND t2.c1 = $2;
+ EXPLAIN (COSTS false) EXECUTE st1(1, 2);
+ QUERY PLAN
+ -----------------------------------------------------------------------------------------------------------------------------------------
+ Nested Loop
+ -> Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_22 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, NULL, NULL, NULL, NULL FROM "S 1"."T 1"
+ -> Materialize
+ -> Foreign Scan on ft2 t2
+ Filter: (c1 = 2)
+ Remote SQL: DECLARE pgsql_fdw_cursor_23 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, NULL, NULL, NULL, NULL FROM "S 1"."T 1"
+ (8 rows)
+
+ EXECUTE st1(1, 1);
+ c3 | c3
+ -------+-------
+ 00001 | 00001
+ (1 row)
+
+ EXECUTE st1(101, 101);
+ c3 | c3
+ -------+-------
+ 00101 | 00101
+ (1 row)
+
+ -- subquery using stable function (can't be pushed down)
+ PREPARE st2(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c4) = 6) ORDER BY c1;
+ EXPLAIN (COSTS false) EXECUTE st2(10, 20);
+ QUERY PLAN
+ ---------------------------------------------------------------------------------------------------------------------------------------------------
+ Sort
+ Sort Key: t1.c1
+ -> Hash Join
+ Hash Cond: (t1.c3 = t2.c3)
+ -> Foreign Scan on ft1 t1
+ Filter: (c1 < 20)
+ Remote SQL: DECLARE pgsql_fdw_cursor_28 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ -> Hash
+ -> HashAggregate
+ -> Foreign Scan on ft2 t2
+ Filter: ((c1 > 10) AND (date_part('dow'::text, c4) = 6::double precision))
+ Remote SQL: DECLARE pgsql_fdw_cursor_29 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, c4, NULL, NULL, NULL FROM "S 1"."T 1"
+ (12 rows)
+
+ EXECUTE st2(10, 20);
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ ----+----+-------+------------------------------+--------------------------+----+------------
+ 16 | 6 | 00016 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970 | 6 | 6
+ (1 row)
+
+ EXECUTE st1(101, 101);
+ c3 | c3
+ -------+-------
+ 00101 | 00101
+ (1 row)
+
+ -- subquery using immutable function (can be pushed down)
+ PREPARE st3(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c5) = 6) ORDER BY c1;
+ EXPLAIN (COSTS false) EXECUTE st3(10, 20);
+ QUERY PLAN
+ ---------------------------------------------------------------------------------------------------------------------------------------------------
+ Sort
+ Sort Key: t1.c1
+ -> Hash Join
+ Hash Cond: (t1.c3 = t2.c3)
+ -> Foreign Scan on ft1 t1
+ Filter: (c1 < 20)
+ Remote SQL: DECLARE pgsql_fdw_cursor_34 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ -> Hash
+ -> HashAggregate
+ -> Foreign Scan on ft2 t2
+ Filter: ((c1 > 10) AND (date_part('dow'::text, c5) = 6::double precision))
+ Remote SQL: DECLARE pgsql_fdw_cursor_35 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, NULL, c5, NULL, NULL FROM "S 1"."T 1"
+ (12 rows)
+
+ EXECUTE st3(10, 20);
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ ----+----+-------+------------------------------+--------------------------+----+------------
+ 16 | 6 | 00016 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970 | 6 | 6
+ (1 row)
+
+ EXECUTE st3(20, 30);
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ ----+----+-------+------------------------------+--------------------------+----+------------
+ 23 | 3 | 00023 | Sat Jan 24 00:00:00 1970 PST | Sat Jan 24 00:00:00 1970 | 3 | 3
+ (1 row)
+
+ -- custom plan should be chosen
+ PREPARE st4(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 = $1;
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_40 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_41 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_42 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_43 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_44 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_46 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ -- cleanup
+ DEALLOCATE st1;
+ DEALLOCATE st2;
+ DEALLOCATE st3;
+ DEALLOCATE st4;
+ -- ===================================================================
+ -- connection management
+ -- ===================================================================
+ SELECT srvname, usename FROM pgsql_fdw_connections;
+ srvname | usename
+ -----------+----------
+ loopback2 | postgres
+ (1 row)
+
+ SELECT pgsql_fdw_disconnect(srvid, usesysid) FROM pgsql_fdw_get_connections();
+ pgsql_fdw_disconnect
+ ----------------------
+ OK
+ (1 row)
+
+ SELECT srvname, usename FROM pgsql_fdw_connections;
+ srvname | usename
+ ---------+---------
+ (0 rows)
+
+ -- ===================================================================
+ -- cleanup
+ -- ===================================================================
+ DROP EXTENSION pgsql_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 server loopback2
+ drop cascades to user mapping for public
+ drop cascades to foreign table ft1
+ drop cascades to foreign table ft2
diff --git a/contrib/pgsql_fdw/option.c b/contrib/pgsql_fdw/option.c
index ...65e3c54 .
*** a/contrib/pgsql_fdw/option.c
--- b/contrib/pgsql_fdw/option.c
***************
*** 0 ****
--- 1,246 ----
+ /*-------------------------------------------------------------------------
+ *
+ * option.c
+ * FDW option handling
+ *
+ * Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/option.c
+ *
+ *-------------------------------------------------------------------------
+ */
+ #include "postgres.h"
+
+ #include "access/reloptions.h"
+ #include "catalog/pg_foreign_data_wrapper.h"
+ #include "catalog/pg_foreign_server.h"
+ #include "catalog/pg_foreign_table.h"
+ #include "catalog/pg_user_mapping.h"
+ #include "fmgr.h"
+ #include "foreign/foreign.h"
+ #include "lib/stringinfo.h"
+ #include "miscadmin.h"
+
+ #include "pgsql_fdw.h"
+
+ /*
+ * SQL functions
+ */
+ extern Datum pgsql_fdw_validator(PG_FUNCTION_ARGS);
+ PG_FUNCTION_INFO_V1(pgsql_fdw_validator);
+
+ /*
+ * Describes the valid options for objects that use this wrapper.
+ */
+ typedef struct PgsqlFdwOption
+ {
+ const char *optname;
+ Oid optcontext; /* Oid of catalog in which options may appear */
+ bool is_libpq_opt; /* true if it's used in libpq */
+ } PgsqlFdwOption;
+
+ /*
+ * Valid options for pgsql_fdw.
+ */
+ static PgsqlFdwOption valid_options[] = {
+
+ /*
+ * Options for libpq connection.
+ * Note: This list should be updated along with PQconninfoOptions in
+ * interfaces/libpq/fe-connect.c, so the order is kept as is.
+ *
+ * Some useless libpq connection options are not accepted by pgsql_fdw:
+ * client_encoding: set to local database encoding automatically
+ * fallback_application_name: fixed to "pgsql_fdw"
+ * replication: pgsql_fdw never be replication client
+ */
+ {"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},
+ #ifdef NOT_USED
+ {"client_encoding", ForeignServerRelationId, true},
+ #endif
+ {"tty", ForeignServerRelationId, true},
+ {"options", ForeignServerRelationId, true},
+ {"application_name", ForeignServerRelationId, true},
+ #ifdef NOT_USED
+ {"fallback_application_name", ForeignServerRelationId, true},
+ #endif
+ {"keepalives", ForeignServerRelationId, true},
+ {"keepalives_idle", ForeignServerRelationId, true},
+ {"keepalives_interval", ForeignServerRelationId, true},
+ {"keepalives_count", ForeignServerRelationId, true},
+ #ifdef USE_SSL
+ {"requiressl", ForeignServerRelationId, true},
+ #endif
+ {"sslmode", ForeignServerRelationId, true},
+ {"sslcert", ForeignServerRelationId, true},
+ {"sslkey", ForeignServerRelationId, true},
+ {"sslrootcert", ForeignServerRelationId, true},
+ {"sslcrl", ForeignServerRelationId, true},
+ {"requirepeer", ForeignServerRelationId, true},
+ #if defined(KRB5) || defined(ENABLE_GSS) || defined(ENABLE_SSPI)
+ {"krbsrvname", ForeignServerRelationId, true},
+ #endif
+ #if defined(ENABLE_GSS) && defined(ENABLE_SSPI)
+ {"gsslib", ForeignServerRelationId, true},
+ #endif
+ #ifdef NOT_USED
+ {"replication", ForeignServerRelationId, true},
+ #endif
+
+ /*
+ * Options for translation of object names.
+ */
+ {"nspname", ForeignTableRelationId, false},
+ {"relname", ForeignTableRelationId, false},
+ {"colname", AttributeRelationId, false},
+
+ /*
+ * Options for cursor behavior.
+ * These options can be overridden by finer-grained objects.
+ */
+ {"fetch_count", ForeignTableRelationId, false},
+ {"fetch_count", ForeignServerRelationId, false},
+
+ /* Terminating entry --- MUST BE LAST */
+ {NULL, InvalidOid, false}
+ };
+
+ /*
+ * Helper functions
+ */
+ static bool is_valid_option(const char *optname, Oid context);
+
+ /*
+ * Validate the generic options given to a FOREIGN DATA WRAPPER, SERVER,
+ * USER MAPPING or FOREIGN TABLE that uses pgsql_fdw.
+ *
+ * Raise an ERROR if the option or its value is considered invalid.
+ */
+ Datum
+ pgsql_fdw_validator(PG_FUNCTION_ARGS)
+ {
+ List *options_list = untransformRelOptions(PG_GETARG_DATUM(0));
+ Oid catalog = PG_GETARG_OID(1);
+ ListCell *cell;
+
+ /*
+ * Check that only options supported by pgsql_fdw, and allowed for the
+ * current object type, are given.
+ */
+ foreach(cell, options_list)
+ {
+ DefElem *def = (DefElem *) lfirst(cell);
+
+ if (!is_valid_option(def->defname, catalog))
+ {
+ PgsqlFdwOption *opt;
+ StringInfoData buf;
+
+ /*
+ * Unknown option specified, complain about it. Provide a hint
+ * with list of valid options for the object.
+ */
+ initStringInfo(&buf);
+ for (opt = valid_options; opt->optname; opt++)
+ {
+ if (catalog == opt->optcontext)
+ appendStringInfo(&buf, "%s%s", (buf.len > 0) ? ", " : "",
+ opt->optname);
+ }
+
+ ereport(ERROR,
+ (errcode(ERRCODE_FDW_INVALID_OPTION_NAME),
+ errmsg("invalid option \"%s\"", def->defname),
+ errhint("Valid options in this context are: %s",
+ buf.data)));
+ }
+
+ /* fetch_count be positive digit number. */
+ if (strcmp(def->defname, "fetch_count") == 0)
+ {
+ long value;
+ char *p = NULL;
+
+ value = strtol(strVal(def->arg), &p, 10);
+ if (*p != '\0' || value < 1)
+ ereport(ERROR,
+ (errcode(ERRCODE_FDW_INVALID_ATTRIBUTE_VALUE),
+ errmsg("invalid value for %s: \"%s\"",
+ def->defname, strVal(def->arg))));
+ }
+ }
+
+ /*
+ * We don't care option-specific limitation here; they will be validated at
+ * the execution time.
+ */
+
+ PG_RETURN_VOID();
+ }
+
+ /*
+ * Check whether the given option is one of the valid pgsql_fdw options.
+ * context is the Oid of the catalog holding the object the option is for.
+ */
+ static bool
+ is_valid_option(const char *optname, Oid context)
+ {
+ PgsqlFdwOption *opt;
+
+ for (opt = valid_options; opt->optname; opt++)
+ {
+ if (context == opt->optcontext && strcmp(opt->optname, optname) == 0)
+ return true;
+ }
+ return false;
+ }
+
+ /*
+ * Check whether the given option is one of the valid libpq options.
+ * context is the Oid of the catalog holding the object the option is for.
+ */
+ static bool
+ is_libpq_option(const char *optname)
+ {
+ PgsqlFdwOption *opt;
+
+ for (opt = valid_options; opt->optname; opt++)
+ {
+ if (strcmp(opt->optname, optname) == 0 && opt->is_libpq_opt)
+ return true;
+ }
+ return false;
+ }
+
+ /*
+ * Generate key-value arrays which includes only libpq options from the list
+ * which contains any kind of options.
+ */
+ int
+ ExtractConnectionOptions(List *defelems, const char **keywords, const char **values)
+ {
+ ListCell *lc;
+ int i;
+
+ i = 0;
+ foreach(lc, defelems)
+ {
+ DefElem *d = (DefElem *) lfirst(lc);
+ if (is_libpq_option(d->defname))
+ {
+ keywords[i] = d->defname;
+ values[i] = strVal(d->arg);
+ i++;
+ }
+ }
+ return i;
+ }
diff --git a/contrib/pgsql_fdw/pgsql_fdw--1.0.sql b/contrib/pgsql_fdw/pgsql_fdw--1.0.sql
index ...b0ea2b2 .
*** a/contrib/pgsql_fdw/pgsql_fdw--1.0.sql
--- b/contrib/pgsql_fdw/pgsql_fdw--1.0.sql
***************
*** 0 ****
--- 1,39 ----
+ /* contrib/pgsql_fdw/pgsql_fdw--1.0.sql */
+
+ -- complain if script is sourced in psql, rather than via CREATE EXTENSION
+ \echo Use "CREATE EXTENSION pgsql_fdw" to load this file. \quit
+
+ CREATE FUNCTION pgsql_fdw_handler()
+ RETURNS fdw_handler
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C STRICT;
+
+ CREATE FUNCTION pgsql_fdw_validator(text[], oid)
+ RETURNS void
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C STRICT;
+
+ CREATE FOREIGN DATA WRAPPER pgsql_fdw
+ HANDLER pgsql_fdw_handler
+ VALIDATOR pgsql_fdw_validator;
+
+ /* connection management functions and view */
+ CREATE FUNCTION pgsql_fdw_get_connections(out srvid oid, out usesysid oid)
+ RETURNS SETOF record
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C STRICT;
+
+ CREATE FUNCTION pgsql_fdw_disconnect(oid, oid)
+ RETURNS text
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C STRICT;
+
+ CREATE VIEW pgsql_fdw_connections AS
+ SELECT c.srvid srvid,
+ s.srvname srvname,
+ c.usesysid usesysid,
+ pg_get_userbyid(c.usesysid) usename
+ FROM pgsql_fdw_get_connections() c
+ JOIN pg_catalog.pg_foreign_server s ON (s.oid = c.srvid);
+ GRANT SELECT ON pgsql_fdw_connections TO public;
+
diff --git a/contrib/pgsql_fdw/pgsql_fdw.c b/contrib/pgsql_fdw/pgsql_fdw.c
index ...a649669 .
*** a/contrib/pgsql_fdw/pgsql_fdw.c
--- b/contrib/pgsql_fdw/pgsql_fdw.c
***************
*** 0 ****
--- 1,879 ----
+ /*-------------------------------------------------------------------------
+ *
+ * pgsql_fdw.c
+ * foreign-data wrapper for remote PostgreSQL servers.
+ *
+ * Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/pgsql_fdw.c
+ *
+ *-------------------------------------------------------------------------
+ */
+ #include "postgres.h"
+ #include "fmgr.h"
+
+ #include "catalog/pg_foreign_server.h"
+ #include "catalog/pg_foreign_table.h"
+ #include "commands/explain.h"
+ #include "foreign/fdwapi.h"
+ #include "funcapi.h"
+ #include "miscadmin.h"
+ #include "nodes/nodeFuncs.h"
+ #include "optimizer/cost.h"
+ #include "optimizer/pathnode.h"
+ #include "utils/lsyscache.h"
+ #include "utils/memutils.h"
+ #include "utils/rel.h"
+
+ #include "pgsql_fdw.h"
+ #include "connection.h"
+
+ PG_MODULE_MAGIC;
+
+ /*
+ * Default fetch count for cursor. This can be overridden by fetch_count FDW
+ * option.
+ */
+ #define DEFAULT_FETCH_COUNT 10000
+
+ /*
+ * Cost to establish a connection.
+ * XXX: should be configurable per server?
+ */
+ #define CONNECTION_COSTS 100.0
+
+ /*
+ * Cost to transfer 1 byte from remote server.
+ * XXX: should be configurable per server?
+ */
+ #define TRANSFER_COSTS_PER_BYTE 0.001
+
+ /*
+ * Cursors which are used together in a local query require different name, so
+ * we use simple incremental name for that purpose. We don't care wrap around
+ * of cursor_id because it's hard to imagine that 2^32 cursors are used in a
+ * query.
+ */
+ #define CURSOR_NAME_FORMAT "pgsql_fdw_cursor_%u"
+ static uint32 cursor_id = 0;
+
+ /*
+ * Index of FDW-private items stored in FdwPlan.
+ */
+ enum FdwPrivateIndex {
+ FdwPrivateSelectSql,
+ FdwPrivateDeclareSql,
+ FdwPrivateFetchSql,
+ FdwPrivateResetSql,
+ FdwPrivateCloseSql,
+
+ /* # of elements stored in the list fdw_private */
+ FdwPrivateNum,
+ };
+
+ /*
+ * Describes an execution state of a foreign scan against a foreign table
+ * using pgsql_fdw.
+ */
+ typedef struct PgsqlFdwExecutionState
+ {
+ MemoryContext scan_cxt; /* context for per-scan lifespan data */
+
+ FdwPlan *fdwplan; /* FDW-specific planning information */
+ PGconn *conn; /* connection for the scan */
+
+ Oid *param_types; /* type array of external parameter */
+ const char **param_values; /* value array of external parameter */
+
+ int attnum; /* # of non-dropped attribute */
+ char **col_values; /* column value buffer */
+ AttInMetadata *attinmeta; /* attribute metadata */
+
+ Tuplestorestate *tuples; /* result of the scan */
+ } PgsqlFdwExecutionState;
+
+ /*
+ * SQL functions
+ */
+ extern Datum pgsql_fdw_handler(PG_FUNCTION_ARGS);
+ PG_FUNCTION_INFO_V1(pgsql_fdw_handler);
+
+ /*
+ * FDW callback routines
+ */
+ static FdwPlan *pgsqlPlanForeignScan(Oid foreigntableid,
+ PlannerInfo *root,
+ RelOptInfo *baserel);
+ static void pgsqlExplainForeignScan(ForeignScanState *node, ExplainState *es);
+ static void pgsqlBeginForeignScan(ForeignScanState *node, int eflags);
+ static TupleTableSlot *pgsqlIterateForeignScan(ForeignScanState *node);
+ static void pgsqlReScanForeignScan(ForeignScanState *node);
+ static void pgsqlEndForeignScan(ForeignScanState *node);
+
+ /*
+ * Helper functions
+ */
+ static void estimate_costs(PlannerInfo *root,
+ RelOptInfo *baserel,
+ const char *sql,
+ Oid serverid,
+ Cost *startup_cost,
+ Cost *total_cost);
+ static void execute_query(ForeignScanState *node);
+ static PGresult *fetch_result(ForeignScanState *node);
+ static void store_result(ForeignScanState *node, PGresult *res);
+ static bool contain_ext_param(Node *clause);
+ static bool contain_ext_param_walker(Node *node, void *context);
+
+ /* Exported functions, but not written in pgsql_fdw.h. */
+ void _PG_init(void);
+ void _PG_fini(void);
+
+ /*
+ * Module-specific initialization.
+ */
+ void
+ _PG_init(void)
+ {
+ }
+
+ /*
+ * Module-specific clean up.
+ */
+ void
+ _PG_fini(void)
+ {
+ }
+
+ /*
+ * The content of FdwRoutine of pgsql_fdw is never changed, so we use one
+ * static object for all scan.
+ */
+ static FdwRoutine fdwroutine = {
+ T_FdwRoutine,
+ pgsqlPlanForeignScan,
+ pgsqlExplainForeignScan,
+ pgsqlBeginForeignScan,
+ pgsqlIterateForeignScan,
+ pgsqlReScanForeignScan,
+ pgsqlEndForeignScan,
+ };
+
+ /*
+ * Foreign-data wrapper handler function: return a struct with pointers
+ * to my callback routines.
+ */
+ Datum
+ pgsql_fdw_handler(PG_FUNCTION_ARGS)
+ {
+ PG_RETURN_POINTER(&fdwroutine);
+ }
+
+ /*
+ * pgsqlPlanForeignScan
+ * Create a FdwPlan for a scan on the foreign table
+ */
+ static FdwPlan *
+ pgsqlPlanForeignScan(Oid foreigntableid,
+ PlannerInfo *root,
+ RelOptInfo *baserel)
+ {
+ char name[128]; /* must be larger than format + 10 */
+ StringInfoData cursor;
+ const char *fetch_count_str;
+ int fetch_count = DEFAULT_FETCH_COUNT;
+ char *sql;
+ FdwPlan *fdwplan;
+ List *fdw_private = NIL;
+ ForeignTable *table;
+ ForeignServer *server;
+
+ /* Construct FdwPlan with cost estimates */
+ fdwplan = makeNode(FdwPlan);
+ sql = deparseSql(foreigntableid, root, baserel);
+ table = GetForeignTable(foreigntableid);
+ server = GetForeignServer(table->serverid);
+ estimate_costs(root, baserel, sql, server->serverid,
+ &fdwplan->startup_cost, &fdwplan->total_cost);
+
+ /*
+ * Store plain SELECT statement in private area of FdwPlan. This will be
+ * used for executing remote query and explaining scan.
+ */
+ fdw_private = list_make1(makeString(sql));
+
+ /* Use specified fetch_count instead of default value, if any. */
+ fetch_count_str = GetFdwOptionValue(InvalidOid, InvalidOid, foreigntableid,
+ InvalidAttrNumber, "fetch_count");
+ if (fetch_count_str != NULL)
+ fetch_count = strtol(fetch_count_str, NULL, 10);
+ elog(DEBUG1, "relid=%u fetch_count=%d", foreigntableid, fetch_count);
+
+ /*
+ * We store some more information in FdwPlan to pass them beyond the
+ * boundary between planner and executor. Finally FdwPlan using cursor
+ * would hold items below:
+ *
+ * 1) plain SELECT statement (already added above)
+ * 2) SQL statement used to declare cursor
+ * 3) SQL statement used to fetch rows from cursor
+ * 4) SQL statement used to reset cursor
+ * 5) SQL statement used to close cursor
+ *
+ * These items are indexed with the enum FdwPrivateIndex, so an item
+ * can be accessed directly via list_nth(). For example of FETCH
+ * statement:
+ * list_nth(fdw_private, FdwPrivateFetchSql)
+ */
+
+ /* Construct cursor name from sequential value */
+ sprintf(name, CURSOR_NAME_FORMAT, cursor_id++);
+
+ /* Construct statement to declare cursor */
+ initStringInfo(&cursor);
+ appendStringInfo(&cursor, "DECLARE %s SCROLL CURSOR FOR %s", name, sql);
+ fdw_private = lappend(fdw_private, makeString(cursor.data));
+
+ /* Construct statement to fetch rows from cursor */
+ initStringInfo(&cursor);
+ appendStringInfo(&cursor, "FETCH %d FROM %s", fetch_count, name);
+ fdw_private = lappend(fdw_private, makeString(cursor.data));
+
+ /* Construct statement to reset cursor */
+ initStringInfo(&cursor);
+ appendStringInfo(&cursor, "MOVE ABSOLUTE 0 FROM %s", name);
+ fdw_private = lappend(fdw_private, makeString(cursor.data));
+
+ /* Construct statement to close cursor */
+ initStringInfo(&cursor);
+ appendStringInfo(&cursor, "CLOSE %s", name);
+ fdw_private = lappend(fdw_private, makeString(cursor.data));
+
+ /* Store FDW private information into FdwPlan */
+ fdwplan->fdw_private = fdw_private;
+
+ return fdwplan;
+ }
+
+ /*
+ * pgsqlExplainForeignScan
+ * Produce extra output for EXPLAIN
+ */
+ static void
+ pgsqlExplainForeignScan(ForeignScanState *node, ExplainState *es)
+ {
+ FdwPlan *fdwplan;
+ char *sql;
+
+ fdwplan = ((ForeignScan *) node->ss.ps.plan)->fdwplan;
+ sql = strVal(list_nth(fdwplan->fdw_private, FdwPrivateDeclareSql));
+ ExplainPropertyText("Remote SQL", sql, es);
+ }
+
+ /*
+ * pgsqlBeginForeignScan
+ * Initiate access to a foreign PostgreSQL table.
+ */
+ static void
+ pgsqlBeginForeignScan(ForeignScanState *node, int eflags)
+ {
+ PgsqlFdwExecutionState *festate;
+ PGconn *conn;
+ Oid relid;
+ ForeignTable *table;
+ ForeignServer *server;
+ UserMapping *user;
+ TupleTableSlot *slot = node->ss.ss_ScanTupleSlot;
+
+ /*
+ * Do nothing in EXPLAIN (no ANALYZE) case. node->fdw_state stays NULL.
+ */
+ if (eflags & EXEC_FLAG_EXPLAIN_ONLY)
+ return;
+
+ /*
+ * Save state in node->fdw_state.
+ */
+ festate = (PgsqlFdwExecutionState *) palloc(sizeof(PgsqlFdwExecutionState));
+ festate->fdwplan = ((ForeignScan *) node->ss.ps.plan)->fdwplan;
+
+ /*
+ * Create context for per-scan tuplestore.
+ */
+ festate->scan_cxt = AllocSetContextCreate(MessageContext,
+ "pgsql_fdw",
+ ALLOCSET_DEFAULT_MINSIZE,
+ ALLOCSET_DEFAULT_INITSIZE,
+ ALLOCSET_DEFAULT_MAXSIZE);
+
+ /*
+ * Get connection to the foreign server. Connection manager would
+ * establish new connection if necessary.
+ */
+ relid = RelationGetRelid(node->ss.ss_currentRelation);
+ table = GetForeignTable(relid);
+ server = GetForeignServer(table->serverid);
+ user = GetUserMapping(GetOuterUserId(), server->serverid);
+ conn = GetConnection(server, user, true);
+ festate->conn = conn;
+
+ /* Result will be filled in first Iterate call. */
+ festate->tuples = NULL;
+
+ /* Allocate buffers for column values. */
+ {
+ TupleDesc tupdesc = slot->tts_tupleDescriptor;
+ festate->col_values = palloc(sizeof(char *) * tupdesc->natts);
+ festate->attinmeta = TupleDescGetAttInMetadata(tupdesc);
+ }
+
+ /* Allocate buffers for query parameters. */
+ {
+ ParamListInfo params = node->ss.ps.state->es_param_list_info;
+ int numParams = params ? params->numParams : 0;
+
+ if (numParams > 0)
+ {
+ festate->param_types = palloc0(sizeof(Oid) * numParams);
+ festate->param_values = palloc0(sizeof(char *) * numParams);
+ }
+ else
+ {
+ festate->param_types = NULL;
+ festate->param_values = NULL;
+ }
+ }
+
+
+ /* Store FDW-specific state into ForeignScanState */
+ node->fdw_state = (void *) festate;
+
+ return;
+ }
+
+ /*
+ * pgsqlIterateForeignScan
+ * Retrieve next row from the result set, or clear tuple slot to indicate
+ * EOF.
+ *
+ * Note that using per-query context when retrieving tuples from
+ * tuplestore to ensure that returned tuples can survive until next
+ * iteration because the tuple is released implicitly via ExecClearTuple.
+ * Retrieving a tuple from tuplestore in CurrentMemoryContext (it's a
+ * per-tuple context), ExecClearTuple will free dangling pointer.
+ */
+ static TupleTableSlot *
+ pgsqlIterateForeignScan(ForeignScanState *node)
+ {
+ PgsqlFdwExecutionState *festate;
+ TupleTableSlot *slot = node->ss.ss_ScanTupleSlot;
+ PGresult *res;
+ MemoryContext oldcontext = CurrentMemoryContext;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+
+
+ /*
+ * If this is the first call after Begin, we need to execute remote query.
+ * If the query needs cursor, we declare a cursor at first call and fetch
+ * from it in later calls.
+ */
+ if (festate->tuples == NULL)
+ execute_query(node);
+
+ /*
+ * If enough tuples are left in tuplestore, just return next tuple from it.
+ */
+ MemoryContextSwitchTo(node->ss.ps.state->es_query_cxt);
+ if (tuplestore_gettupleslot(festate->tuples, true, false, slot))
+ {
+ MemoryContextSwitchTo(oldcontext);
+ return slot;
+ }
+ MemoryContextSwitchTo(oldcontext);
+
+ /*
+ * Here we need to clear partial result and fetch next bunch of tuples from
+ * from the cursor for the scan. If the fetch returns no tuple, the scan
+ * has reached the end.
+ */
+ res = fetch_result(node);
+ PG_TRY();
+ {
+ store_result(node, res);
+ PQclear(res);
+ res = NULL;
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+
+ /*
+ * If we got more tuples from the server cursor, return next tuple from
+ * tuplestore.
+ */
+ MemoryContextSwitchTo(node->ss.ps.state->es_query_cxt);
+ if (tuplestore_gettupleslot(festate->tuples, true, false, slot))
+ {
+ MemoryContextSwitchTo(oldcontext);
+ return slot;
+ }
+ MemoryContextSwitchTo(oldcontext);
+
+ /* We don't have any result even in remote server cursor. */
+ ExecClearTuple(slot);
+ return slot;
+ }
+
+ /*
+ * pgsqlReScanForeignScan
+ * - Restart this scan by resetting fetch location.
+ */
+ static void
+ pgsqlReScanForeignScan(ForeignScanState *node)
+ {
+ List *fdw_private;
+ char *sql;
+ PGconn *conn;
+ PGresult *res;
+ PgsqlFdwExecutionState *festate;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+
+ /* If we have not opened cursor yet, nothing to do. */
+ if (festate->tuples == NULL)
+ return;
+
+ /* Discard fetch results if any. */
+ tuplestore_clear(festate->tuples);
+
+ /* Reset cursor */
+ fdw_private = festate->fdwplan->fdw_private;
+ conn = festate->conn;
+ sql = strVal(list_nth(fdw_private, FdwPrivateResetSql));
+ res = PQexec(conn, sql);
+ PG_TRY();
+ {
+ if (PQresultStatus(res) != PGRES_COMMAND_OK)
+ {
+ ereport(ERROR,
+ (errmsg("could not rewind cursor"),
+ errdetail("%s", PQerrorMessage(conn)),
+ errhint("%s", sql)));
+ }
+ PQclear(res);
+ res = NULL;
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+ }
+
+ /*
+ * pgsqlEndForeignScan
+ * Finish scanning foreign table and dispose objects used for this scan
+ */
+ static void
+ pgsqlEndForeignScan(ForeignScanState *node)
+ {
+ List *fdw_private;
+ char *sql;
+ PGconn *conn;
+ PGresult *res;
+ PgsqlFdwExecutionState *festate;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+
+ /* if festate is NULL, we are in EXPLAIN; nothing to do */
+ if (festate == NULL)
+ return;
+
+ /* If we have not opened cursor yet, nothing to do. */
+ if (festate->tuples == NULL)
+ return;
+
+ /* Discard fetch results */
+ tuplestore_end(festate->tuples);
+ festate->tuples = NULL;
+
+ /* Close cursor */
+ fdw_private = festate->fdwplan->fdw_private;
+ conn = festate->conn;
+ sql = strVal(list_nth(fdw_private, FdwPrivateCloseSql));
+ res = PQexec(conn, sql);
+ PG_TRY();
+ {
+ if (PQresultStatus(res) != PGRES_COMMAND_OK)
+ {
+ ereport(ERROR,
+ (errmsg("could not close cursor"),
+ errdetail("%s", PQerrorMessage(conn)),
+ errhint("%s", sql)));
+ }
+ PQclear(res);
+ res = NULL;
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+
+ ReleaseConnection(festate->conn);
+
+ MemoryContextDelete(festate->scan_cxt);
+ festate->scan_cxt = NULL;
+ }
+
+ /*
+ * Estimate costs of scanning a foreign table.
+ */
+ static void
+ estimate_costs(PlannerInfo *root, RelOptInfo *baserel,
+ const char *sql, Oid serverid,
+ Cost *startup_cost, Cost *total_cost)
+ {
+ ListCell *lc;
+ ForeignServer *server;
+ UserMapping *user;
+ PGconn *conn = NULL;
+ PGresult *res = NULL;
+ StringInfoData buf;
+ char *plan;
+ char *p;
+ int n;
+
+ /*
+ * If the baserestrictinfo contains any Param node with paramkind
+ * PARAM_EXTERNAL, we need result of EXPLAIN for EXECUTE statement, not for
+ * simple SELECT statement. However, we can't get actual parameter values
+ * here, so we use fixed and large costs as second best so that planner
+ * tend to choose custom plan.
+ *
+ * See comments in plancache.c for details of custom plan.
+ */
+ foreach(lc, baserel->baserestrictinfo)
+ {
+ RestrictInfo *rs = (RestrictInfo *) lfirst(lc);
+ if (contain_ext_param((Node *) rs->clause))
+ {
+ *startup_cost = CONNECTION_COSTS;
+ *total_cost = 10000; /* groundless large costs */
+
+ return;
+ }
+ }
+
+ /*
+ * Get connection to the foreign server. Connection manager would
+ * establish new connection if necessary.
+ */
+ server = GetForeignServer(serverid);
+ user = GetUserMapping(GetOuterUserId(), server->serverid);
+ conn = GetConnection(server, user, false);
+ initStringInfo(&buf);
+ appendStringInfo(&buf, "EXPLAIN %s", sql);
+
+ PG_TRY();
+ {
+ res = PQexec(conn, buf.data);
+ if (PQresultStatus(res) != PGRES_TUPLES_OK || PQntuples(res) == 0)
+ {
+ char *msg;
+
+ msg = pstrdup(PQerrorMessage(conn));
+ ereport(ERROR,
+ (errmsg("could not execute EXPLAIN for cost estimation"),
+ errdetail("%s", msg),
+ errhint("%s", sql)));
+ }
+
+ /*
+ * Find estimation portion from top plan node. Here we search opening
+ * parentheses from the end of the line to avoid finding unexpected
+ * parentheses.
+ */
+ plan = PQgetvalue(res, 0, 0);
+ p = strrchr(plan, '(');
+ if (p == NULL)
+ elog(ERROR, "wrong EXPLAIN output: %s", plan);
+ n = sscanf(p,
+ "(cost=%lf..%lf rows=%lf width=%d)",
+ startup_cost, total_cost, &baserel->rows, &baserel->width);
+ if (n != 4)
+ elog(ERROR, "could not get estimation from EXPLAIN output");
+
+ PQclear(res);
+ res = NULL;
+ ReleaseConnection(conn);
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+
+ /*
+ * TODO Selectivity of quals which are NOT pushed down should be also
+ * considered.
+ */
+
+ /* add cost to establish connection. */
+ *startup_cost += CONNECTION_COSTS;
+ *total_cost += CONNECTION_COSTS;
+
+ /* add cost to transfer result. */
+ *total_cost += TRANSFER_COSTS_PER_BYTE * baserel->width * baserel->tuples;
+ *total_cost += cpu_tuple_cost * baserel->tuples;
+ }
+
+ /*
+ * Execute remote query with current parameters.
+ */
+ static void
+ execute_query(ForeignScanState *node)
+ {
+ FdwPlan *fdwplan;
+ PgsqlFdwExecutionState *festate;
+ ParamListInfo params = node->ss.ps.state->es_param_list_info;
+ int numParams = params ? params->numParams : 0;
+ Oid *types = NULL;
+ const char **values = NULL;
+ char *sql;
+ PGconn *conn;
+ PGresult *res;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+ types = festate->param_types;
+ values = festate->param_values;
+
+ /*
+ * Construct parameter array in text format. We don't release memory for
+ * the arrays explicitly, because the memory usage would not be very large,
+ * and anyway they will be released in context cleanup.
+ */
+ if (numParams > 0)
+ {
+ int i;
+
+ 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;
+
+ getTypeOutputInfo(types[i], &out_func_oid, &isvarlena);
+ fmgr_info(out_func_oid, &func);
+ values[i] = OutputFunctionCall(&func, params->params[i].value);
+ }
+ }
+ }
+
+ /*
+ * Execute remote query with parameters.
+ */
+ conn = festate->conn;
+ fdwplan = ((ForeignScan *) node->ss.ps.plan)->fdwplan;
+ sql = strVal(list_nth(fdwplan->fdw_private, FdwPrivateDeclareSql));
+ res = PQexecParams(conn, sql, numParams, types, values, NULL, NULL, 0);
+ PG_TRY();
+ {
+ /*
+ * If the query has failed, reporting details is enough here.
+ * Connection(s) which are used by this query (at least used by
+ * pgsql_fdw) will be cleaned up by the foreign connection manager.
+ */
+ if (PQresultStatus(res) != PGRES_COMMAND_OK)
+ {
+ ereport(ERROR,
+ (errmsg("could not declare cursor"),
+ errdetail("%s", PQerrorMessage(conn)),
+ errhint("%s", sql)));
+ }
+
+ /* Discard result of CURSOR statement and fetch first bunch. */
+ PQclear(res);
+ res = fetch_result(node);
+
+ /*
+ * Store the result of the query into tuplestore.
+ * We must release PGresult here to avoid memory leak.
+ */
+ store_result(node, res);
+ PQclear(res);
+ res = NULL;
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+ }
+
+ /*
+ * Fetch next partial result from remote server.
+ */
+ static PGresult *
+ fetch_result(ForeignScanState *node)
+ {
+ PgsqlFdwExecutionState *festate;
+ List *fdw_private;
+ char *sql;
+ PGconn *conn;
+ PGresult *res;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+
+ /* retrieve information for fetching result. */
+ fdw_private = festate->fdwplan->fdw_private;
+ sql = strVal(list_nth(fdw_private, FdwPrivateFetchSql));
+ conn = festate->conn;
+ res = PQexec(conn, sql);
+ if (PQresultStatus(res) != PGRES_TUPLES_OK)
+ {
+ ereport(ERROR,
+ (errmsg("could not fetch rows from foreign server"),
+ errdetail("%s", PQerrorMessage(conn)),
+ errhint("%s", sql)));
+ }
+
+ return res;
+ }
+
+ /*
+ * Create tuples from PGresult and store them into tuplestore.
+ */
+ static void
+ store_result(ForeignScanState *node, PGresult *res)
+ {
+ int rows;
+ int row;
+ int i;
+ int nfields;
+ int attnum; /* number of non-dropped columns */
+ Form_pg_attribute *attrs;
+ TupleTableSlot *slot = node->ss.ss_ScanTupleSlot;
+ TupleDesc tupdesc = slot->tts_tupleDescriptor;
+ PgsqlFdwExecutionState *festate;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+ rows = PQntuples(res);
+ nfields = PQnfields(res);
+ attrs = tupdesc->attrs;
+
+ /* First, ensure that the tuplestore is empty. */
+ if (festate->tuples == NULL)
+ {
+ MemoryContext oldcontext = CurrentMemoryContext;
+
+ /*
+ * Create tuplestore to store result of the query in per-query context.
+ * Note that we use this memory context to avoid memory leak in error
+ * cases.
+ */
+ MemoryContextSwitchTo(festate->scan_cxt);
+ festate->tuples = tuplestore_begin_heap(false, false, work_mem);
+ MemoryContextSwitchTo(oldcontext);
+ }
+ else
+ {
+ /* We already have tuplestore, just need to clear contents of it. */
+ tuplestore_clear(festate->tuples);
+ }
+
+
+ /* 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)));
+
+ /* put a tuples into the slot */
+ for (row = 0; row < rows; row++)
+ {
+ int j;
+ HeapTuple tuple;
+
+ for (i = 0, j = 0; i < tupdesc->natts; i++)
+ {
+ /* skip dropped columns. */
+ if (attrs[i]->attisdropped)
+ {
+ festate->col_values[i] = NULL;
+ continue;
+ }
+
+ if (PQgetisnull(res, row, j))
+ festate->col_values[i] = NULL;
+ else
+ festate->col_values[i] = PQgetvalue(res, row, 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(festate->attinmeta, festate->col_values);
+ tuplestore_puttuple(festate->tuples, tuple);
+ }
+
+ tuplestore_donestoring(festate->tuples);
+ }
+
+ /*
+ * contain_ext_param
+ * Recursively search for Param nodes within a clause.
+ *
+ * Returns true if any parameter reference node with relkind PARAM_EXTERN
+ * found.
+ *
+ * This does not descend into subqueries, and so should be used only after
+ * reduction of sublinks to subplans, or in contexts where it's known there
+ * are no subqueries. There mustn't be outer-aggregate references either.
+ *
+ * XXX: These functions could be in core, src/backend/optimizer/util/clauses.c.
+ */
+ static bool
+ contain_ext_param(Node *clause)
+ {
+ return contain_ext_param_walker(clause, NULL);
+ }
+
+ static bool
+ contain_ext_param_walker(Node *node, void *context)
+ {
+ if (node == NULL)
+ return false;
+ if (IsA(node, Param))
+ {
+ Param *param = (Param *) node;
+
+ if (param->paramkind == PARAM_EXTERN)
+ return true; /* abort the tree traversal and return true */
+ }
+ return expression_tree_walker(node, contain_ext_param_walker, context);
+ }
diff --git a/contrib/pgsql_fdw/pgsql_fdw.control b/contrib/pgsql_fdw/pgsql_fdw.control
index ...0a9c8f4 .
*** a/contrib/pgsql_fdw/pgsql_fdw.control
--- b/contrib/pgsql_fdw/pgsql_fdw.control
***************
*** 0 ****
--- 1,5 ----
+ # pgsql_fdw extension
+ comment = 'foreign-data wrapper for remote PostgreSQL servers'
+ default_version = '1.0'
+ module_pathname = '$libdir/pgsql_fdw'
+ relocatable = true
diff --git a/contrib/pgsql_fdw/pgsql_fdw.h b/contrib/pgsql_fdw/pgsql_fdw.h
index ...f6394ce .
*** a/contrib/pgsql_fdw/pgsql_fdw.h
--- b/contrib/pgsql_fdw/pgsql_fdw.h
***************
*** 0 ****
--- 1,28 ----
+ /*-------------------------------------------------------------------------
+ *
+ * pgsql_fdw.h
+ * foreign-data wrapper for remote PostgreSQL servers.
+ *
+ * Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/pgsql_fdw.h
+ *
+ *-------------------------------------------------------------------------
+ */
+
+ #ifndef PGSQL_FDW_H
+ #define PGSQL_FDW_H
+
+ #include "postgres.h"
+ #include "nodes/relation.h"
+
+ /* in option.c */
+ int ExtractConnectionOptions(List *defelems,
+ const char **keywords,
+ const char **values);
+
+ /* in deparse.c */
+ char *deparseSql(Oid relid, PlannerInfo *root, RelOptInfo *baserel);
+
+ #endif /* PGSQL_FDW_H */
diff --git a/contrib/pgsql_fdw/sql/pgsql_fdw.sql b/contrib/pgsql_fdw/sql/pgsql_fdw.sql
index ...7f9f9cc .
*** a/contrib/pgsql_fdw/sql/pgsql_fdw.sql
--- b/contrib/pgsql_fdw/sql/pgsql_fdw.sql
***************
*** 0 ****
--- 1,212 ----
+ -- ===================================================================
+ -- create FDW objects
+ -- ===================================================================
+
+ CREATE EXTENSION pgsql_fdw;
+
+ CREATE SERVER loopback1 FOREIGN DATA WRAPPER pgsql_fdw;
+ CREATE SERVER loopback2 FOREIGN DATA WRAPPER pgsql_fdw
+ OPTIONS (dbname 'contrib_regression');
+
+ CREATE USER MAPPING FOR public SERVER loopback1
+ OPTIONS (user 'value', password 'value');
+ CREATE USER MAPPING FOR public SERVER loopback2;
+
+ CREATE FOREIGN TABLE ft1 (
+ c1 int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10)
+ ) SERVER loopback2;
+
+ CREATE FOREIGN TABLE ft2 (
+ c1 int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10)
+ ) SERVER loopback2;
+
+ -- ===================================================================
+ -- create objects used through FDW
+ -- ===================================================================
+ CREATE SCHEMA "S 1";
+ CREATE TABLE "S 1"."T 1" (
+ "C 1" int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10),
+ CONSTRAINT t1_pkey PRIMARY KEY ("C 1")
+ );
+ CREATE TABLE "S 1"."T 2" (
+ c1 int NOT NULL,
+ c2 text,
+ CONSTRAINT t2_pkey PRIMARY KEY (c1)
+ );
+
+ BEGIN;
+ TRUNCATE "S 1"."T 1";
+ INSERT INTO "S 1"."T 1"
+ SELECT id,
+ id % 10,
+ to_char(id, 'FM00000'),
+ '1970-01-01'::timestamptz + ((id % 100) || ' days')::interval,
+ '1970-01-01'::timestamp + ((id % 100) || ' days')::interval,
+ id % 10,
+ id % 10
+ FROM generate_series(1, 1000) id;
+ TRUNCATE "S 1"."T 2";
+ INSERT INTO "S 1"."T 2"
+ SELECT id,
+ 'AAA' || to_char(id, 'FM000')
+ FROM generate_series(1, 100) id;
+ COMMIT;
+
+ -- ===================================================================
+ -- tests for pgsql_fdw_validator
+ -- ===================================================================
+ ALTER FOREIGN DATA WRAPPER pgsql_fdw OPTIONS (host 'value'); -- ERROR
+ -- requiressl, krbsrvname and gsslib are omitted because they depend on
+ -- configure option
+ ALTER SERVER loopback1 OPTIONS (
+ authtype 'value',
+ service 'value',
+ connect_timeout 'value',
+ dbname 'value',
+ host 'value',
+ hostaddr 'value',
+ port 'value',
+ --client_encoding 'value',
+ tty 'value',
+ options 'value',
+ application_name 'value',
+ --fallback_application_name 'value',
+ keepalives 'value',
+ keepalives_idle 'value',
+ keepalives_interval 'value',
+ -- requiressl 'value',
+ sslmode 'value',
+ sslcert 'value',
+ sslkey 'value',
+ sslrootcert 'value',
+ sslcrl 'value'
+ --requirepeer 'value',
+ -- krbsrvname 'value',
+ -- gsslib 'value',
+ --replication 'value'
+ );
+ ALTER SERVER loopback1 OPTIONS (user 'value'); -- ERROR
+ ALTER SERVER loopback2 OPTIONS (ADD fetch_count '2');
+ ALTER USER MAPPING FOR public SERVER loopback1
+ OPTIONS (DROP user, DROP password);
+ ALTER USER MAPPING FOR public SERVER loopback1
+ OPTIONS (host 'value'); -- ERROR
+ ALTER FOREIGN TABLE ft1 OPTIONS (nspname 'S 1', relname 'T 1');
+ ALTER FOREIGN TABLE ft2 OPTIONS (nspname 'S 1', relname 'T 1', fetch_count '100');
+ ALTER FOREIGN TABLE ft1 OPTIONS (invalid 'value'); -- ERROR
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count 'a'); -- ERROR
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count '0'); -- ERROR
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count '-1'); -- ERROR
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (invalid 'value'); -- ERROR
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (colname 'C 1');
+ ALTER FOREIGN TABLE ft2 ALTER COLUMN c1 OPTIONS (colname 'C 1');
+ \dew+
+ \des+
+ \deu+
+ \det+
+
+ -- ===================================================================
+ -- simple queries
+ -- ===================================================================
+ -- single table, with/without alias
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
+ SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+ SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+ -- with WHERE clause
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
+ SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
+ -- 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
+ SELECT * FROM ft1 t1 WHERE t1.c3 = (SELECT MAX(c3) FROM ft2 t2) ORDER BY c1;
+ -- used in CTE
+ WITH t1 AS (SELECT * FROM ft1 WHERE c1 <= 10) SELECT t2.c1, t2.c2, t2.c3, t2.c4 FROM t1, ft2 t2 WHERE t1.c1 = t2.c1 ORDER BY t1.c1;
+ -- fixed values
+ SELECT 'fixed', NULL FROM ft1 t1 WHERE c1 = 1;
+ -- user-defined operator/function
+ CREATE FUNCTION pgsql_fdw_abs(int) RETURNS int AS $$
+ BEGIN
+ RETURN abs($1);
+ END
+ $$ LANGUAGE plpgsql IMMUTABLE;
+ CREATE OPERATOR === (
+ LEFTARG = int,
+ RIGHTARG = int,
+ PROCEDURE = int4eq,
+ COMMUTATOR = ===,
+ NEGATOR = !==
+ );
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = pgsql_fdw_abs(t1.c2);
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 === t1.c2;
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = abs(t1.c2);
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = t1.c2;
+ DROP OPERATOR === (int, int) CASCADE;
+ DROP FUNCTION pgsql_fdw_abs(int);
+
+ -- ===================================================================
+ -- parameterized queries
+ -- ===================================================================
+ -- simple join
+ PREPARE st1(int, int) AS SELECT t1.c3, t2.c3 FROM ft1 t1, ft2 t2 WHERE t1.c1 = $1 AND t2.c1 = $2;
+ EXPLAIN (COSTS false) EXECUTE st1(1, 2);
+ EXECUTE st1(1, 1);
+ EXECUTE st1(101, 101);
+ -- subquery using stable function (can't be pushed down)
+ PREPARE st2(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c4) = 6) ORDER BY c1;
+ EXPLAIN (COSTS false) EXECUTE st2(10, 20);
+ EXECUTE st2(10, 20);
+ EXECUTE st1(101, 101);
+ -- subquery using immutable function (can be pushed down)
+ PREPARE st3(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c5) = 6) ORDER BY c1;
+ EXPLAIN (COSTS false) EXECUTE st3(10, 20);
+ EXECUTE st3(10, 20);
+ EXECUTE st3(20, 30);
+ -- custom plan should be chosen
+ PREPARE st4(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 = $1;
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ -- cleanup
+ DEALLOCATE st1;
+ DEALLOCATE st2;
+ DEALLOCATE st3;
+ DEALLOCATE st4;
+
+ -- ===================================================================
+ -- connection management
+ -- ===================================================================
+ SELECT srvname, usename FROM pgsql_fdw_connections;
+ SELECT pgsql_fdw_disconnect(srvid, usesysid) FROM pgsql_fdw_get_connections();
+ SELECT srvname, usename FROM pgsql_fdw_connections;
+
+ -- ===================================================================
+ -- cleanup
+ -- ===================================================================
+ DROP EXTENSION pgsql_fdw CASCADE;
+
diff --git a/doc/src/sgml/contrib.sgml b/doc/src/sgml/contrib.sgml
index d4da4ee..32056d1 100644
*** a/doc/src/sgml/contrib.sgml
--- b/doc/src/sgml/contrib.sgml
*************** CREATE EXTENSION <replaceable>module_nam
*** 117,122 ****
--- 117,123 ----
&pgcrypto;
&pgfreespacemap;
&pgrowlocks;
+ &pgsql-fdw;
&pgstandby;
&pgstatstatements;
&pgstattuple;
diff --git a/doc/src/sgml/filelist.sgml b/doc/src/sgml/filelist.sgml
index b5d3c6d..de686ee 100644
*** a/doc/src/sgml/filelist.sgml
--- b/doc/src/sgml/filelist.sgml
***************
*** 125,130 ****
--- 125,131 ----
<!ENTITY pgcrypto SYSTEM "pgcrypto.sgml">
<!ENTITY pgfreespacemap SYSTEM "pgfreespacemap.sgml">
<!ENTITY pgrowlocks SYSTEM "pgrowlocks.sgml">
+ <!ENTITY pgsql-fdw SYSTEM "pgsql-fdw.sgml">
<!ENTITY pgstandby SYSTEM "pgstandby.sgml">
<!ENTITY pgstatstatements SYSTEM "pgstatstatements.sgml">
<!ENTITY pgstattuple SYSTEM "pgstattuple.sgml">
diff --git a/doc/src/sgml/pgsql-fdw.sgml b/doc/src/sgml/pgsql-fdw.sgml
index ...c9d5d8f .
*** a/doc/src/sgml/pgsql-fdw.sgml
--- b/doc/src/sgml/pgsql-fdw.sgml
***************
*** 0 ****
--- 1,253 ----
+ <!-- doc/src/sgml/pgsql-fdw.sgml -->
+
+ <sect1 id="pgsql-fdw" xreflabel="pgsql_fdw">
+ <title>pgsql_fdw</title>
+
+ <indexterm zone="pgsql-fdw">
+ <primary>pgsql_fdw</primary>
+ </indexterm>
+
+ <para>
+ The <filename>pgsql_fdw</filename> module provides a foreign-data wrapper for
+ external <productname>PostgreSQL</productname> servers.
+ With this module, users can access data stored in external
+ <productname>PostgreSQL</productname> via plain SQL statements.
+ </para>
+
+ <para>
+ Note that default wrapper <literal>pgsql_fdw</literal> is created
+ automatically during <command>CREATE EXTENSION</command> command for
+ <application>pgsql_fdw</application>.
+ </para>
+
+ <sect2>
+ <title>FDW Options of pgsql_fdw</title>
+
+ <sect3>
+ <title>Connection Options</title>
+ <para>
+ A foreign server and user mapping created using this wrapper can have
+ <application>libpq</> connection options, expect below:
+
+ <itemizedlist>
+ <listitem><para>client_encoding</para></listitem>
+ <listitem><para>fallback_application_name</para></listitem>
+ <listitem><para>replication</para></listitem>
+ </itemizedlist>
+
+ For details of <application>libpq</> connection options, see
+ <xref linkend="libpq-connect">.
+ </para>
+
+ <para>
+ <literal>user</literal> and <literal>password</literal> can be
+ specified on user mappings, and others can be specified on foreign servers.
+ </para>
+ </sect3>
+
+ <sect3>
+ <title>Object Name Options</title>
+ <para>
+ Foreign tables which were created using this wrapper, or its columns can
+ have object name options. These options can be used to specify the names
+ used in SQL statement sent to remote <productname>PostgreSQL</productname>
+ server. These options are useful when a remote object has different name
+ from corresponding local one.
+ </para>
+
+ <variablelist>
+
+ <varlistentry>
+ <term><literal>nspname</literal></term>
+ <listitem>
+ <para>
+ This option, which can be specified on a foreign table, is used as a
+ namespace (schema) reference in the SQL statement. If this options is
+ omitted, <literal>pg_class.nspname</literal> of the foreign table is
+ used.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>relname</literal></term>
+ <listitem>
+ <para>
+ This option, which can be specified on a foreign table, is used as a
+ relation (table) reference in the SQL statement. If this options is
+ omitted, <literal>pg_class.relname</literal> of the foreign table is
+ used.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>colname</literal></term>
+ <listitem>
+ <para>
+ This option, which can be specified on a column of a foreign table, is
+ used as a column (attribute) reference in the SQL statement. If this
+ option is omitted, <literal>pg_attribute.attname</literal> of the column
+ of the foreign table is used.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ </variablelist>
+
+ </sect3>
+
+ <sect3>
+ <title>Cursor Options</title>
+ <para>
+ The <application>pgsql_fdw</application> always uses cursor to retrieve the
+ result from external server. Users can control the behavior of cursor by
+ setting cursor options to foreign table or foreign server. If an option
+ is set to both objects, finer-grained setting is used. In other words,
+ foreign table's setting overrides foreign server's setting.
+ </para>
+
+ <variablelist>
+
+ <varlistentry>
+ <term><literal>fetch_count</literal></term>
+ <listitem>
+ <para>
+ This option specifies the number of rows to be fetched at a time.
+ This option accepts only integer value larger than zero. The default
+ setting is 10000.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ </variablelist>
+
+ </sect3>
+
+ </sect2>
+
+ <sect2>
+ <title>Connection Management</title>
+
+ <para>
+ The <application>pgsql_fdw</application> establishes a connection to a
+ foreign server in the beginning of the first query which uses a foreign
+ table associated to the foreign server, and reuses the connection following
+ queries and even in following foreign scans in same query.
+
+ You can see the list of active connections via
+ <structname>pgsql_fdw_connections</structname> view. It shows pair of oid
+ and name of server and local role for each active connections established by
+ <application>pgsql_fdw</application>. For security reason, only superuser
+ can see other role's connections.
+ </para>
+
+ <para>
+ Established connections are kept alive until local role changes or the
+ current transaction aborts or user requests so.
+ </para>
+
+ <para>
+ If role has been changed, active connections established as old local role
+ is kept alive but never be reused until local role has restored to original
+ role. This kind of situation happens with <command>SET ROLE</command> and
+ <command>SET SESSION AUTHORIZATION</command>.
+ </para>
+
+ <para>
+ If current transaction aborts by error or user request, all active
+ connections are disconnected automatically. This behavior avoids possible
+ connection leaks on error.
+ </para>
+
+ <para>
+ You can discard persistent connection at arbitrary timing with
+ <function>pgsql_fdw_disconnect()</function>. It takes server oid and
+ user oid as arguments. This function can handle only connections
+ established in current session; connections established by other backends
+ are not reachable.
+ </para>
+
+ <para>
+ You can discard all active and visible connections in current session with
+ using <structname>pgsql_fdw_connections</structname> and
+ <function>pgsql_fdw_disconnect()</function> together:
+ <synopsis>
+ postgres=# SELECT pgsql_fdw_disconnect(srvid, usesysid) FROM pgsql_fdw_connections;
+ pgsql_fdw_disconnect
+ ----------------------
+ OK
+ OK
+ (2 rows)
+ </synopsis>
+ </para>
+ </sect2>
+
+ <sect2>
+ <title>Transaction Management</title>
+ <para>
+ The <application>pgsql_fdw</application> begins remote transaction at the
+ beginning of a local query, and terminates it with <command>ABORT</command>
+ at the end of the local query. This means that all foreign scans on a
+ foreign server in a local query are executed in one transaction.
+ The transaction isolation level used for remote transaction is same as
+ local transaction.
+ </para>
+ <para>
+ Note that even if the isolation level of local transaction was
+ <literal>SERIALIZABLE</literal> or <literal>REPEATABLE READ</literal>,
+ series of one query might produce different result, because foreign scans
+ in different local queries are executed in different remote transactions.
+ For instance, when client started a local transaction
+ explicitly with isolation level <literal>SERIALIZABLE</literal>, and
+ executed same local query which contains a foreign table which references
+ foreign data which is updated frequently, latter result would be different
+ from former result.
+ </para>
+ <para>
+ This restriction might be relaxed in future release.
+ </para>
+ </sect2>
+
+ <sect2>
+ <title>Estimation of Costs and Rows</title>
+ <para>
+ The <application>pgsql_fdw</application> estimates the costs of a foreign
+ scan by adding up some basic costs: connection costs, remote query costs
+ and data transfer costs.
+ To get remote query costs, <application>pgsql_fdw</application> executes
+ <command>EXPLAIN</command> command on remote server for each foreign scan.
+ </para>
+ <para>
+ On the other hand, estimated rows which was returned by
+ <command>EXPLAIN</command> is used for local estimation as-is.
+ </para>
+ </sect2>
+
+ <sect2>
+ <title>EXPLAIN Output</title>
+ <para>
+ For a foreign table using <literal>pgsql_fdw</>, <command>EXPLAIN</> shows
+ a remote SQL statement which is sent to remote
+ <productname>PostgreSQL</productname> server for a ForeignScan plan node.
+ For example:
+ </para>
+ <synopsis>
+ postgres=# EXPLAIN SELECT aid FROM pgbench_accounts WHERE abalance < 0;
+ QUERY PLAN
+ --------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on pgbench_accounts (cost=100.00..8105.13 rows=302613 width=8)
+ Filter: (abalance < 0)
+ Remote SQL: DECLARE pgsql_fdw_cursor_3 SCROLL CURSOR FOR SELECT aid, NULL, abalance, NULL FROM public.pgbench_accounts
+ (3 rows)
+ </synopsis>
+ </sect2>
+
+ <sect2>
+ <title>Author</title>
+ <para>
+ Shigeru Hanada <email>shigeru.hanada@gmail.com</email>
+ </para>
+ </sect2>
+
+ </sect1>
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 9ad54c5..c2a5a7b 100644
*** a/src/backend/utils/adt/ruleutils.c
--- b/src/backend/utils/adt/ruleutils.c
*************** deparse_context_for(const char *aliasnam
*** 2161,2166 ****
--- 2161,2189 ----
return list_make1(dpns);
}
+ /* ----------
+ * deparse_context_for_rtelist - Build deparse context for a list of RTEs
+ *
+ * Given the list of RangeTableEnetry, build deparsing context for an
+ * expression referencing those relations. This is sufficient for uses of
+ * deparse_expression before plan has been created.
+ * ----------
+ */
+ List *
+ deparse_context_for_rtelist(List *rtable)
+ {
+ deparse_namespace *dpns;
+
+ dpns = (deparse_namespace *) palloc0(sizeof(deparse_namespace));
+
+ /* Build a minimal RTE for the rel */
+ dpns->rtable = rtable;
+ dpns->ctes = NIL;
+
+ /* Return a one-deep namespace stack */
+ return list_make1(dpns);
+ }
+
/*
* deparse_context_for_planstate - Build deparse context for a plan
*
diff --git a/src/include/utils/builtins.h b/src/include/utils/builtins.h
index 2c331ce..dc6932a 100644
*** a/src/include/utils/builtins.h
--- b/src/include/utils/builtins.h
*************** extern char *deparse_expression(Node *ex
*** 648,653 ****
--- 648,654 ----
extern List *deparse_context_for(const char *aliasname, Oid relid);
extern List *deparse_context_for_planstate(Node *planstate, List *ancestors,
List *rtable);
+ extern List *deparse_context_for_rtelist(List *rtable);
extern const char *quote_identifier(const char *ident);
extern char *quote_qualified_identifier(const char *qualifier,
const char *ident);
pgsql_fdw_pushdown_v3.patchtext/plain; name=pgsql_fdw_pushdown_v3.patchDownload
diff --git a/contrib/pgsql_fdw/deparse.c b/contrib/pgsql_fdw/deparse.c
index cf9e775..594743f 100644
*** a/contrib/pgsql_fdw/deparse.c
--- b/contrib/pgsql_fdw/deparse.c
*************** typedef struct foreign_executable_cxt
*** 35,46 ****
RelOptInfo *foreignrel;
} foreign_executable_cxt;
/*
* Deparse query representation into SQL statement which suits for remote
! * PostgreSQL server.
*/
char *
! deparseSql(Oid relid, PlannerInfo *root, RelOptInfo *baserel)
{
StringInfoData foreign_relname;
StringInfoData sql; /* builder for SQL statement */
--- 35,53 ----
RelOptInfo *foreignrel;
} foreign_executable_cxt;
+ static bool is_foreign_expr(PlannerInfo *root, RelOptInfo *baserel, Expr *expr);
+ static bool foreign_expr_walker(Node *node, foreign_executable_cxt *context);
+ static bool is_builtin(Oid procid);
+ static bool contain_ext_param(Node *clause);
+ static bool contain_ext_param_walker(Node *node, void *context);
+
/*
* Deparse query representation into SQL statement which suits for remote
! * PostgreSQL server. Also some of quals in WHERE clause will be pushed down
! * If they are safe to be evaluated on the remote side.
*/
char *
! deparseSql(Oid relid, PlannerInfo *root, RelOptInfo *baserel, bool *has_param)
{
StringInfoData foreign_relname;
StringInfoData sql; /* builder for SQL statement */
*************** deparseSql(Oid relid, PlannerInfo *root,
*** 51,56 ****
--- 58,64 ----
const char *relname = NULL; /* plain relation name */
const char *q_nspname; /* quoted namespace name */
const char *q_relname; /* quoted relation name */
+ List *foreign_expr = NIL; /* list of Expr* evaluated on remote */
int i;
List *rtable = NIL;
List *context = NIL;
*************** deparseSql(Oid relid, PlannerInfo *root,
*** 59,73 ****
initStringInfo(&foreign_relname);
/*
! * First of all, determine which column should be retrieved for this scan.
*
* We do this before deparsing SELECT clause because attributes which are
* not used in neither reltargetlist nor baserel->baserestrictinfo, quals
* evaluated on local, can be replaced with literal "NULL" in the SELECT
* clause to reduce overhead of tuple handling tuple and data transfer.
*/
if (baserel->baserestrictinfo != NIL)
{
ListCell *lc;
foreach (lc, baserel->baserestrictinfo)
--- 67,91 ----
initStringInfo(&foreign_relname);
/*
! * First of all, determine which qual can be pushed down.
! *
! * The expressions which satisfy is_foreign_expr() are deparsed into WHERE
! * clause of result SQL string, and they could be removed from PlanState
! * to avoid duplicate evaluation at ExecScan().
! *
! * We never change the quals in the Plan node, because this execution might
! * be for a PREPAREd statement, thus the quals in the Plan node might be
! * reused to construct another PlanState for subsequent EXECUTE statement.
*
* We do this before deparsing SELECT clause because attributes which are
* not used in neither reltargetlist nor baserel->baserestrictinfo, quals
* evaluated on local, can be replaced with literal "NULL" in the SELECT
* clause to reduce overhead of tuple handling tuple and data transfer.
*/
+ *has_param = false;
if (baserel->baserestrictinfo != NIL)
{
+ List *local_qual = NIL;
ListCell *lc;
foreach (lc, baserel->baserestrictinfo)
*************** deparseSql(Oid relid, PlannerInfo *root,
*** 76,81 ****
--- 94,113 ----
List *attrs;
/*
+ * Determine whether the qual can be pushed down or not. If a qual
+ * can be pushed down and it contains external param, tell that to
+ * caller in order to fall back to fixed costs.
+ */
+ if (is_foreign_expr(root, baserel, ri->clause))
+ {
+ foreign_expr = lappend(foreign_expr, ri->clause);
+ if (contain_ext_param((Node*) ri->clause))
+ *has_param = true;
+ }
+ else
+ local_qual = lappend(local_qual, ri);
+
+ /*
* We need to know which attributes are used in qual evaluated
* on the local server, because they should be listed in the
* SELECT clause of remote query. We can ignore attributes
*************** deparseSql(Oid relid, PlannerInfo *root,
*** 87,92 ****
--- 119,130 ----
PVC_RECURSE_PLACEHOLDERS);
attr_used = list_union(attr_used, attrs);
}
+
+ /*
+ * Remove pushed down qualifiers from baserestrictinfo to avoid
+ * redundant evaluation.
+ */
+ baserel->baserestrictinfo = local_qual;
}
/*
*************** deparseSql(Oid relid, PlannerInfo *root,
*** 193,199 ****
--- 231,461 ----
*/
appendStringInfo(&sql, "FROM %s", foreign_relname.data);
+ /*
+ * deparse WHERE clause
+ */
+ if (foreign_expr != NIL)
+ {
+ Node *node;
+
+ node = (Node *) make_ands_explicit(foreign_expr);
+ appendStringInfo(&sql, " WHERE %s ",
+ deparse_expression(node, context, false, false));
+ list_free(foreign_expr);
+ foreign_expr = NIL;
+ }
+
elog(DEBUG3, "Remote SQL: %s", sql.data);
return sql.data;
}
+ /*
+ * Returns true if expr is safe to be evaluated on the foreign server.
+ */
+ static bool
+ is_foreign_expr(PlannerInfo *root, RelOptInfo *baserel, Expr *expr)
+ {
+ foreign_executable_cxt context;
+ context.root = root;
+ context.foreignrel = baserel;
+
+ /*
+ * An expression which includes any mutable function can't be pushed down
+ * because it's result is not stable. For example, pushing now() down to
+ * remote side would cause confusion from the clock offset.
+ * If we have routine mapping infrastructure in future release, we will be
+ * able to choose function to be pushed down in finer granularity.
+ */
+ if (contain_mutable_functions((Node *) expr))
+ return false;
+
+ /*
+ * Check that the expression consists of nodes which are known as safe to
+ * be pushed down.
+ */
+ if (foreign_expr_walker((Node *) expr, &context))
+ return false;
+
+ return true;
+ }
+
+ /*
+ * Return true if node includes any node which is not known as safe to be
+ * pushed down.
+ */
+ static bool
+ foreign_expr_walker(Node *node, foreign_executable_cxt *context)
+ {
+ Oid func;
+
+ if (node == NULL)
+ return false;
+
+ /*
+ * If given expression has valid collation, it can't be pushed down because
+ * it might has incompatible semantics on remote side.
+ */
+ if (exprCollation(node) != InvalidOid ||
+ exprInputCollation(node) != InvalidOid)
+ return true;
+
+ /*
+ * If return type of given expression is not built-in, it can't be pushed
+ * down because it might has incompatible semantics on remote side.
+ */
+ if (!is_builtin(exprType(node)))
+ return true;
+
+ /*
+ * If function used by the expression is not built-in, it can't be pushed
+ * down because it might has incompatible semantics on remote side.
+ */
+ func = exprFunction(node);
+ if (func != InvalidOid && !is_builtin(func))
+ return true;
+
+ switch (nodeTag(node))
+ {
+ case T_Const:
+ case T_BoolExpr:
+ case T_NullTest:
+ case T_DistinctExpr:
+ case T_RelabelType:
+ case T_FuncExpr:
+ /*
+ * 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 at the tail of this function.
+ */
+ break;
+ case T_Param:
+ /*
+ * Only external parameters can be pushed down.:
+ */
+ {
+ if (((Param *) node)->paramkind != PARAM_EXTERN)
+ return true;
+ }
+ break;
+ case T_ScalarArrayOpExpr:
+ /*
+ * Only built-in operators can be pushed down. In addition,
+ * underlying function must be built-in and immutable, but we don't
+ * check volatility here; such check must be done already with
+ * contain_mutable_functions.
+ */
+ {
+ ScalarArrayOpExpr *oe = (ScalarArrayOpExpr *) node;
+
+ if (!is_builtin(oe->opno))
+ return true;
+
+ /* operands are checked later */
+ }
+ break;
+ case T_OpExpr:
+ /*
+ * Only built-in operators can be pushed down. In addition,
+ * underlying function must be built-in and immutable, but we don't
+ * check volatility here; such check must be done already with
+ * contain_mutable_functions.
+ */
+ {
+ OpExpr *oe = (OpExpr *) node;
+
+ if (!is_builtin(oe->opno))
+ return true;
+
+ /* operands are checked later */
+ }
+ 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;
+ foreign_executable_cxt *f_context;
+
+ f_context = (foreign_executable_cxt *) context;
+ if (var->varno != f_context->foreignrel->relid ||
+ var->varlevelsup != 0)
+ return true;
+ }
+ break;
+ case T_ArrayRef:
+ /*
+ * ArrayRef which holds non-built-in typed elements can't be pushed
+ * down.
+ */
+ {
+ if (!is_builtin(((ArrayRef *) node)->refelemtype))
+ return true;
+ }
+ break;
+ case T_ArrayExpr:
+ /*
+ * ArrayExpr which holds non-built-in typed elements can't be pushed
+ * down.
+ */
+ {
+ if (!is_builtin(((ArrayExpr *) node)->element_typeid))
+ return true;
+ }
+ break;
+ default:
+ {
+ ereport(DEBUG3,
+ (errmsg("expression is too complex"),
+ errdetail("%s", nodeToString(node))));
+ return true;
+ }
+ break;
+ }
+
+ return expression_tree_walker(node, foreign_expr_walker, context);
+ }
+
+ /*
+ * Return true if given object is one of built-in objects.
+ */
+ static bool
+ is_builtin(Oid oid)
+ {
+ return (oid < FirstNormalObjectId);
+ }
+
+ /*
+ * contain_ext_param
+ * Recursively search for Param nodes within a clause.
+ *
+ * Returns true if any parameter reference node with relkind PARAM_EXTERN
+ * found.
+ *
+ * This does not descend into subqueries, and so should be used only after
+ * reduction of sublinks to subplans, or in contexts where it's known there
+ * are no subqueries. There mustn't be outer-aggregate references either.
+ *
+ * XXX: These functions could be in core, src/backend/optimizer/util/clauses.c.
+ */
+ static bool
+ contain_ext_param(Node *clause)
+ {
+ return contain_ext_param_walker(clause, NULL);
+ }
+
+ static bool
+ contain_ext_param_walker(Node *node, void *context)
+ {
+ if (node == NULL)
+ return false;
+ if (IsA(node, Param))
+ {
+ Param *param = (Param *) node;
+
+ if (param->paramkind == PARAM_EXTERN)
+ return true; /* abort the tree traversal and return true */
+ }
+ return expression_tree_walker(node, contain_ext_param_walker, context);
+ }
diff --git a/contrib/pgsql_fdw/expected/pgsql_fdw.out b/contrib/pgsql_fdw/expected/pgsql_fdw.out
index 7aab32c..afd9e19 100644
*** a/contrib/pgsql_fdw/expected/pgsql_fdw.out
--- b/contrib/pgsql_fdw/expected/pgsql_fdw.out
*************** SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.
*** 218,229 ****
-- with WHERE clause
EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
! QUERY PLAN
! ------------------------------------------------------------------------------------------------------------------
Foreign Scan on public.ft1 t1
Output: c1, c2, c3, c4, c5, c6, c7
! Filter: ((t1.c1 = 101) AND ((t1.c6)::text = '1'::text) AND (t1.c7 = '1'::bpchar))
! Remote SQL: DECLARE pgsql_fdw_cursor_4 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
(4 rows)
SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
--- 218,229 ----
-- with WHERE clause
EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
! QUERY PLAN
! ---------------------------------------------------------------------------------------------------------------------------------------
Foreign Scan on public.ft1 t1
Output: c1, c2, c3, c4, c5, c6, c7
! Filter: (((t1.c6)::text = '1'::text) AND (t1.c7 = '1'::bpchar))
! Remote SQL: DECLARE pgsql_fdw_cursor_4 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE ("C 1" = 101)
(4 rows)
SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
*************** EXPLAIN (COSTS false) SELECT * FROM ft1
*** 331,350 ****
(3 rows)
EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = abs(t1.c2);
! QUERY PLAN
! -------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Filter: (c1 = abs(c2))
! Remote SQL: DECLARE pgsql_fdw_cursor_20 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
! (3 rows)
EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = t1.c2;
! QUERY PLAN
! -------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Filter: (c1 = c2)
! Remote SQL: DECLARE pgsql_fdw_cursor_21 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
! (3 rows)
DROP OPERATOR === (int, int) CASCADE;
DROP FUNCTION pgsql_fdw_abs(int);
--- 331,348 ----
(3 rows)
EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = abs(t1.c2);
! QUERY PLAN
! --------------------------------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Remote SQL: DECLARE pgsql_fdw_cursor_20 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE ("C 1" = abs(c2))
! (2 rows)
EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = t1.c2;
! QUERY PLAN
! ---------------------------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Remote SQL: DECLARE pgsql_fdw_cursor_21 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE ("C 1" = c2)
! (2 rows)
DROP OPERATOR === (int, int) CASCADE;
DROP FUNCTION pgsql_fdw_abs(int);
*************** DROP FUNCTION pgsql_fdw_abs(int);
*** 354,370 ****
-- simple join
PREPARE st1(int, int) AS SELECT t1.c3, t2.c3 FROM ft1 t1, ft2 t2 WHERE t1.c1 = $1 AND t2.c1 = $2;
EXPLAIN (COSTS false) EXECUTE st1(1, 2);
! QUERY PLAN
! -----------------------------------------------------------------------------------------------------------------------------------------
Nested Loop
-> Foreign Scan on ft1 t1
! Filter: (c1 = 1)
! Remote SQL: DECLARE pgsql_fdw_cursor_22 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, NULL, NULL, NULL, NULL FROM "S 1"."T 1"
! -> Materialize
! -> Foreign Scan on ft2 t2
! Filter: (c1 = 2)
! Remote SQL: DECLARE pgsql_fdw_cursor_23 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, NULL, NULL, NULL, NULL FROM "S 1"."T 1"
! (8 rows)
EXECUTE st1(1, 1);
c3 | c3
--- 352,365 ----
-- simple join
PREPARE st1(int, int) AS SELECT t1.c3, t2.c3 FROM ft1 t1, ft2 t2 WHERE t1.c1 = $1 AND t2.c1 = $2;
EXPLAIN (COSTS false) EXECUTE st1(1, 2);
! QUERY PLAN
! ------------------------------------------------------------------------------------------------------------------------------------------------------
Nested Loop
-> Foreign Scan on ft1 t1
! Remote SQL: DECLARE pgsql_fdw_cursor_22 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, NULL, NULL, NULL, NULL FROM "S 1"."T 1" WHERE ("C 1" = 1)
! -> Foreign Scan on ft2 t2
! Remote SQL: DECLARE pgsql_fdw_cursor_23 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, NULL, NULL, NULL, NULL FROM "S 1"."T 1" WHERE ("C 1" = 2)
! (5 rows)
EXECUTE st1(1, 1);
c3 | c3
*************** EXECUTE st1(101, 101);
*** 381,401 ****
-- subquery using stable function (can't be pushed down)
PREPARE st2(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c4) = 6) ORDER BY c1;
EXPLAIN (COSTS false) EXECUTE st2(10, 20);
! QUERY PLAN
! ---------------------------------------------------------------------------------------------------------------------------------------------------
Sort
Sort Key: t1.c1
-> Hash Join
Hash Cond: (t1.c3 = t2.c3)
-> Foreign Scan on ft1 t1
! Filter: (c1 < 20)
! Remote SQL: DECLARE pgsql_fdw_cursor_28 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
-> Hash
-> HashAggregate
-> Foreign Scan on ft2 t2
! Filter: ((c1 > 10) AND (date_part('dow'::text, c4) = 6::double precision))
! Remote SQL: DECLARE pgsql_fdw_cursor_29 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, c4, NULL, NULL, NULL FROM "S 1"."T 1"
! (12 rows)
EXECUTE st2(10, 20);
c1 | c2 | c3 | c4 | c5 | c6 | c7
--- 376,395 ----
-- subquery using stable function (can't be pushed down)
PREPARE st2(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c4) = 6) ORDER BY c1;
EXPLAIN (COSTS false) EXECUTE st2(10, 20);
! QUERY PLAN
! -----------------------------------------------------------------------------------------------------------------------------------------------------------------------
Sort
Sort Key: t1.c1
-> Hash Join
Hash Cond: (t1.c3 = t2.c3)
-> Foreign Scan on ft1 t1
! Remote SQL: DECLARE pgsql_fdw_cursor_28 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE ("C 1" < 20)
-> Hash
-> HashAggregate
-> Foreign Scan on ft2 t2
! Filter: (date_part('dow'::text, c4) = 6::double precision)
! Remote SQL: DECLARE pgsql_fdw_cursor_29 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, c4, NULL, NULL, NULL FROM "S 1"."T 1" WHERE ("C 1" > 10)
! (11 rows)
EXECUTE st2(10, 20);
c1 | c2 | c3 | c4 | c5 | c6 | c7
*************** EXECUTE st1(101, 101);
*** 412,432 ****
-- subquery using immutable function (can be pushed down)
PREPARE st3(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c5) = 6) ORDER BY c1;
EXPLAIN (COSTS false) EXECUTE st3(10, 20);
! QUERY PLAN
! ---------------------------------------------------------------------------------------------------------------------------------------------------
Sort
Sort Key: t1.c1
-> Hash Join
Hash Cond: (t1.c3 = t2.c3)
-> Foreign Scan on ft1 t1
! Filter: (c1 < 20)
! Remote SQL: DECLARE pgsql_fdw_cursor_34 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
-> Hash
-> HashAggregate
-> Foreign Scan on ft2 t2
! Filter: ((c1 > 10) AND (date_part('dow'::text, c5) = 6::double precision))
! Remote SQL: DECLARE pgsql_fdw_cursor_35 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, NULL, c5, NULL, NULL FROM "S 1"."T 1"
! (12 rows)
EXECUTE st3(10, 20);
c1 | c2 | c3 | c4 | c5 | c6 | c7
--- 406,425 ----
-- subquery using immutable function (can be pushed down)
PREPARE st3(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c5) = 6) ORDER BY c1;
EXPLAIN (COSTS false) EXECUTE st3(10, 20);
! QUERY PLAN
! -----------------------------------------------------------------------------------------------------------------------------------------------------------------------
Sort
Sort Key: t1.c1
-> Hash Join
Hash Cond: (t1.c3 = t2.c3)
-> Foreign Scan on ft1 t1
! Remote SQL: DECLARE pgsql_fdw_cursor_34 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE ("C 1" < 20)
-> Hash
-> HashAggregate
-> Foreign Scan on ft2 t2
! Filter: (date_part('dow'::text, c5) = 6::double precision)
! Remote SQL: DECLARE pgsql_fdw_cursor_35 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, NULL, c5, NULL, NULL FROM "S 1"."T 1" WHERE ("C 1" > 10)
! (11 rows)
EXECUTE st3(10, 20);
c1 | c2 | c3 | c4 | c5 | c6 | c7
*************** EXECUTE st3(20, 30);
*** 443,494 ****
-- custom plan should be chosen
PREPARE st4(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 = $1;
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! -------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Filter: (c1 = 1)
! Remote SQL: DECLARE pgsql_fdw_cursor_40 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
! (3 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! -------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Filter: (c1 = 1)
! Remote SQL: DECLARE pgsql_fdw_cursor_41 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
! (3 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! -------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Filter: (c1 = 1)
! Remote SQL: DECLARE pgsql_fdw_cursor_42 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
! (3 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! -------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Filter: (c1 = 1)
! Remote SQL: DECLARE pgsql_fdw_cursor_43 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
! (3 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! -------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Filter: (c1 = 1)
! Remote SQL: DECLARE pgsql_fdw_cursor_44 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
! (3 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! -------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Filter: (c1 = 1)
! Remote SQL: DECLARE pgsql_fdw_cursor_46 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
! (3 rows)
-- cleanup
DEALLOCATE st1;
--- 436,481 ----
-- custom plan should be chosen
PREPARE st4(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 = $1;
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! --------------------------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Remote SQL: DECLARE pgsql_fdw_cursor_40 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE ("C 1" = 1)
! (2 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! --------------------------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Remote SQL: DECLARE pgsql_fdw_cursor_41 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE ("C 1" = 1)
! (2 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! --------------------------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Remote SQL: DECLARE pgsql_fdw_cursor_42 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE ("C 1" = 1)
! (2 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! --------------------------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Remote SQL: DECLARE pgsql_fdw_cursor_43 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE ("C 1" = 1)
! (2 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! --------------------------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Remote SQL: DECLARE pgsql_fdw_cursor_44 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE ("C 1" = 1)
! (2 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! --------------------------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Remote SQL: DECLARE pgsql_fdw_cursor_46 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE ("C 1" = 1)
! (2 rows)
-- cleanup
DEALLOCATE st1;
diff --git a/contrib/pgsql_fdw/pgsql_fdw.c b/contrib/pgsql_fdw/pgsql_fdw.c
index a649669..86abd0a 100644
*** a/contrib/pgsql_fdw/pgsql_fdw.c
--- b/contrib/pgsql_fdw/pgsql_fdw.c
*************** static void estimate_costs(PlannerInfo *
*** 123,130 ****
static void execute_query(ForeignScanState *node);
static PGresult *fetch_result(ForeignScanState *node);
static void store_result(ForeignScanState *node, PGresult *res);
- static bool contain_ext_param(Node *clause);
- static bool contain_ext_param_walker(Node *node, void *context);
/* Exported functions, but not written in pgsql_fdw.h. */
void _PG_init(void);
--- 123,128 ----
*************** pgsqlPlanForeignScan(Oid foreigntableid,
*** 188,201 ****
List *fdw_private = NIL;
ForeignTable *table;
ForeignServer *server;
/* Construct FdwPlan with cost estimates */
fdwplan = makeNode(FdwPlan);
! sql = deparseSql(foreigntableid, root, baserel);
table = GetForeignTable(foreigntableid);
server = GetForeignServer(table->serverid);
! estimate_costs(root, baserel, sql, server->serverid,
! &fdwplan->startup_cost, &fdwplan->total_cost);
/*
* Store plain SELECT statement in private area of FdwPlan. This will be
--- 186,216 ----
List *fdw_private = NIL;
ForeignTable *table;
ForeignServer *server;
+ bool has_param;
/* Construct FdwPlan with cost estimates */
fdwplan = makeNode(FdwPlan);
! sql = deparseSql(foreigntableid, root, baserel, &has_param);
table = GetForeignTable(foreigntableid);
server = GetForeignServer(table->serverid);
!
! /*
! * If the baserestrictinfo contains any Param node with paramkind
! * PARAM_EXTERNAL as part of push-down-able condition, we need to
! * groundless fixed costs because we can't get actual parameter values
! * here, so we use fixed and large costs as second best so that planner
! * tend to choose custom plan.
! *
! * See comments in plancache.c for details of custom plan.
! */
! if (has_param)
! {
! fdwplan->startup_cost = CONNECTION_COSTS;
! fdwplan->total_cost = 10000; /* groundless large costs */
! }
! else
! estimate_costs(root, baserel, sql, server->serverid,
! &fdwplan->startup_cost, &fdwplan->total_cost);
/*
* Store plain SELECT statement in private area of FdwPlan. This will be
*************** estimate_costs(PlannerInfo *root, RelOpt
*** 541,547 ****
const char *sql, Oid serverid,
Cost *startup_cost, Cost *total_cost)
{
- ListCell *lc;
ForeignServer *server;
UserMapping *user;
PGconn *conn = NULL;
--- 556,561 ----
*************** estimate_costs(PlannerInfo *root, RelOpt
*** 552,578 ****
int n;
/*
- * If the baserestrictinfo contains any Param node with paramkind
- * PARAM_EXTERNAL, we need result of EXPLAIN for EXECUTE statement, not for
- * simple SELECT statement. However, we can't get actual parameter values
- * here, so we use fixed and large costs as second best so that planner
- * tend to choose custom plan.
- *
- * See comments in plancache.c for details of custom plan.
- */
- foreach(lc, baserel->baserestrictinfo)
- {
- RestrictInfo *rs = (RestrictInfo *) lfirst(lc);
- if (contain_ext_param((Node *) rs->clause))
- {
- *startup_cost = CONNECTION_COSTS;
- *total_cost = 10000; /* groundless large costs */
-
- return;
- }
- }
-
- /*
* Get connection to the foreign server. Connection manager would
* establish new connection if necessary.
*/
--- 566,571 ----
*************** store_result(ForeignScanState *node, PGr
*** 843,879 ****
tuplestore_donestoring(festate->tuples);
}
-
- /*
- * contain_ext_param
- * Recursively search for Param nodes within a clause.
- *
- * Returns true if any parameter reference node with relkind PARAM_EXTERN
- * found.
- *
- * This does not descend into subqueries, and so should be used only after
- * reduction of sublinks to subplans, or in contexts where it's known there
- * are no subqueries. There mustn't be outer-aggregate references either.
- *
- * XXX: These functions could be in core, src/backend/optimizer/util/clauses.c.
- */
- static bool
- contain_ext_param(Node *clause)
- {
- return contain_ext_param_walker(clause, NULL);
- }
-
- static bool
- contain_ext_param_walker(Node *node, void *context)
- {
- if (node == NULL)
- return false;
- if (IsA(node, Param))
- {
- Param *param = (Param *) node;
-
- if (param->paramkind == PARAM_EXTERN)
- return true; /* abort the tree traversal and return true */
- }
- return expression_tree_walker(node, contain_ext_param_walker, context);
- }
--- 836,838 ----
diff --git a/contrib/pgsql_fdw/pgsql_fdw.h b/contrib/pgsql_fdw/pgsql_fdw.h
index f6394ce..690d58b 100644
*** a/contrib/pgsql_fdw/pgsql_fdw.h
--- b/contrib/pgsql_fdw/pgsql_fdw.h
*************** int ExtractConnectionOptions(List *defel
*** 23,28 ****
const char **values);
/* in deparse.c */
! char *deparseSql(Oid relid, PlannerInfo *root, RelOptInfo *baserel);
#endif /* PGSQL_FDW_H */
--- 23,31 ----
const char **values);
/* in deparse.c */
! char *deparseSql(Oid relid,
! PlannerInfo *root,
! RelOptInfo *baserel,
! bool *has_param);
#endif /* PGSQL_FDW_H */
diff --git a/doc/src/sgml/pgsql-fdw.sgml b/doc/src/sgml/pgsql-fdw.sgml
index c9d5d8f..df2e7f6 100644
*** a/doc/src/sgml/pgsql-fdw.sgml
--- b/doc/src/sgml/pgsql-fdw.sgml
*************** postgres=# SELECT pgsql_fdw_disconnect(s
*** 234,245 ****
</para>
<synopsis>
postgres=# EXPLAIN SELECT aid FROM pgbench_accounts WHERE abalance < 0;
! QUERY PLAN
! --------------------------------------------------------------------------------------------------------------------------
! Foreign Scan on pgbench_accounts (cost=100.00..8105.13 rows=302613 width=8)
! Filter: (abalance < 0)
! Remote SQL: DECLARE pgsql_fdw_cursor_3 SCROLL CURSOR FOR SELECT aid, NULL, abalance, NULL FROM public.pgbench_accounts
! (3 rows)
</synopsis>
</sect2>
--- 234,244 ----
</para>
<synopsis>
postgres=# EXPLAIN SELECT aid FROM pgbench_accounts WHERE abalance < 0;
! QUERY PLAN
! ------------------------------------------------------------------------------------------------------------------------------------------------
! Foreign Scan on pgbench_accounts (cost=100.00..8861.66 rows=518 width=8)
! Remote SQL: DECLARE pgsql_fdw_cursor_1 SCROLL CURSOR FOR SELECT aid, NULL, abalance, NULL FROM public.pgbench_accounts WHERE (abalance < 0)
! (2 rows)
</synopsis>
</sect2>
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index 51459c4..c017d6b 100644
*** a/src/backend/nodes/nodeFuncs.c
--- b/src/backend/nodes/nodeFuncs.c
*************** leftmostLoc(int loc1, int loc2)
*** 1405,1410 ****
--- 1405,1449 ----
return Min(loc1, loc2);
}
+ /*
+ * exprFunction -
+ * returns the Oid of the function used by the expression.
+ *
+ * Result is InvalidOid if the node type doesn't store this information.
+ */
+ Oid
+ exprFunction(const Node *expr)
+ {
+ Oid func;
+
+ if (!expr)
+ return InvalidOid;
+
+ switch (nodeTag(expr))
+ {
+ case T_Aggref:
+ func = ((const Aggref *) expr)->aggfnoid;
+ break;
+ case T_WindowFunc:
+ func = ((const WindowFunc *) expr)->winfnoid;
+ break;
+ case T_FuncExpr:
+ func = ((const FuncExpr *) expr)->funcid;
+ break;
+ case T_OpExpr:
+ func = ((const OpExpr *) expr)->opfuncid;
+ break;
+ case T_ScalarArrayOpExpr:
+ func = ((const ScalarArrayOpExpr *) expr)->opfuncid;
+ break;
+ case T_ArrayCoerceExpr:
+ func = ((const ArrayCoerceExpr *) expr)->elemfuncid;
+ break;
+ default:
+ func = InvalidOid;
+ }
+ return func;
+ }
/*
* Standard expression-tree walking support
diff --git a/src/include/nodes/nodeFuncs.h b/src/include/nodes/nodeFuncs.h
index def4f31..a3ebe5c 100644
*** a/src/include/nodes/nodeFuncs.h
--- b/src/include/nodes/nodeFuncs.h
*************** extern void exprSetInputCollation(Node *
*** 38,43 ****
--- 38,45 ----
extern int exprLocation(const Node *expr);
+ extern Oid exprFunction(const Node *expr);
+
extern bool expression_tree_walker(Node *node, bool (*walker) (),
void *context);
extern Node *expression_tree_mutator(Node *node, Node *(*mutator) (),
(2012/02/02 18:24), Marko Kreen wrote:
I think you want this instead:
With modified version of pgsql_fdw which uses row processor to retrieve
result tuples, I found significant performance gain on simple read-only
pgbench, though scale factor was very small (-s 3). Executed command
was "pgbench -S -n -c 5 T 30".
Average tps (excluding connections establishing) of 3 times measurements
are:
pgsql_fdw with SQL-cursor : 622
pgsql_fdw with Row Processor : 1219 - 2.0x faster than SQL-cursor
w/o pgsql_fdw(direct access) : 7719 - 6.3x faster than Row Processor
Row processor was almost 2x faster than SQL-cursor! I'm looking forward
to this feature.
In addition to performance gain, of course memory usage was kept at very
low level. :)
Regards,
--
Shigeru Hanada
(2012/02/08 20:51), Shigeru Hanada wrote:
Attached revised patches. Changes from last version are below.
<snip>
I've found and fixed a bug which generates wrong remote query when any
column of a foreign table has been dropped. Also regression test for
this case is added.
I attached only pgsql_fdw_v8.patch, because pgsql_fdw_pushdown_v3.patch
in last post still can be applied onto v8 patch.
Regards,
--
Shigeru Hanada
Attachments:
pgsql_fdw_v8.patchtext/plain; name=pgsql_fdw_v8.patchDownload
diff --git a/contrib/Makefile b/contrib/Makefile
index ac0a80a..14aa433 100644
*** a/contrib/Makefile
--- b/contrib/Makefile
*************** SUBDIRS = \
*** 41,46 ****
--- 41,47 ----
pgbench \
pgcrypto \
pgrowlocks \
+ pgsql_fdw \
pgstattuple \
seg \
spi \
diff --git a/contrib/README b/contrib/README
index a1d42a1..d3fa211 100644
*** a/contrib/README
--- b/contrib/README
*************** pgrowlocks -
*** 158,163 ****
--- 158,167 ----
A function to return row locking information
by Tatsuo Ishii <ishii@sraoss.co.jp>
+ pgsql_fdw -
+ Foreign-data wrapper for external PostgreSQL servers.
+ by Shigeru Hanada <shigeru.hanada@gmail.com>
+
pgstattuple -
Functions to return statistics about "dead" tuples and free
space within a table
diff --git a/contrib/pgsql_fdw/.gitignore b/contrib/pgsql_fdw/.gitignore
index ...0854728 .
*** a/contrib/pgsql_fdw/.gitignore
--- b/contrib/pgsql_fdw/.gitignore
***************
*** 0 ****
--- 1,4 ----
+ # Generated subdirectories
+ /results/
+ *.o
+ *.so
diff --git a/contrib/pgsql_fdw/Makefile b/contrib/pgsql_fdw/Makefile
index ...6381365 .
*** a/contrib/pgsql_fdw/Makefile
--- b/contrib/pgsql_fdw/Makefile
***************
*** 0 ****
--- 1,22 ----
+ # contrib/pgsql_fdw/Makefile
+
+ MODULE_big = pgsql_fdw
+ OBJS = pgsql_fdw.o option.o deparse.o connection.o
+ PG_CPPFLAGS = -I$(libpq_srcdir)
+ SHLIB_LINK = $(libpq)
+
+ EXTENSION = pgsql_fdw
+ DATA = pgsql_fdw--1.0.sql
+
+ REGRESS = pgsql_fdw
+
+ ifdef USE_PGXS
+ PG_CONFIG = pg_config
+ PGXS := $(shell $(PG_CONFIG) --pgxs)
+ include $(PGXS)
+ else
+ subdir = contrib/pgsql_fdw
+ top_builddir = ../..
+ include $(top_builddir)/src/Makefile.global
+ include $(top_srcdir)/contrib/contrib-global.mk
+ endif
diff --git a/contrib/pgsql_fdw/connection.c b/contrib/pgsql_fdw/connection.c
index ...6ede259 .
*** a/contrib/pgsql_fdw/connection.c
--- b/contrib/pgsql_fdw/connection.c
***************
*** 0 ****
--- 1,553 ----
+ /*-------------------------------------------------------------------------
+ *
+ * connection.c
+ * Connection management for pgsql_fdw
+ *
+ * Portions Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/connection.c
+ *
+ *-------------------------------------------------------------------------
+ */
+ #include "postgres.h"
+
+ #include "access/xact.h"
+ #include "catalog/pg_type.h"
+ #include "foreign/foreign.h"
+ #include "funcapi.h"
+ #include "libpq-fe.h"
+ #include "mb/pg_wchar.h"
+ #include "miscadmin.h"
+ #include "utils/array.h"
+ #include "utils/builtins.h"
+ #include "utils/hsearch.h"
+ #include "utils/memutils.h"
+ #include "utils/resowner.h"
+ #include "utils/tuplestore.h"
+
+ #include "pgsql_fdw.h"
+ #include "connection.h"
+
+ /* ============================================================================
+ * Connection management functions
+ * ==========================================================================*/
+
+ /*
+ * Connection cache entry managed with hash table.
+ */
+ typedef struct ConnCacheEntry
+ {
+ /* hash key must be first */
+ Oid serverid; /* oid of foreign server */
+ Oid userid; /* oid of local user */
+
+ bool use_tx; /* true when using remote transaction */
+ 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);
+ static void begin_remote_tx(PGconn *conn);
+ static void abort_remote_tx(PGconn *conn);
+
+ /*
+ * 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.
+ *
+ * When use_tx is true, remote transaction is started if caller is the only
+ * user of the connection. Isolation level of the remote transaction is same
+ * as local transaction, and remote transaction will be aborted when last
+ * user release.
+ *
+ * TODO: Note that caching connections requires a mechanism to detect change of
+ * FDW object to invalidate already established connections.
+ */
+ PGconn *
+ GetConnection(ForeignServer *server, UserMapping *user, bool use_tx)
+ {
+ bool found;
+ ConnCacheEntry *entry;
+ ConnCacheEntry key;
+
+ /* initialize connection cache if it isn't */
+ if (FSConnectionHash == NULL)
+ {
+ HASHCTL ctl;
+
+ /* hash key is a pair of oids: serverid and userid */
+ MemSet(&ctl, 0, sizeof(ctl));
+ ctl.keysize = sizeof(Oid) + sizeof(Oid);
+ ctl.entrysize = sizeof(ConnCacheEntry);
+ ctl.hash = tag_hash;
+ ctl.match = memcmp;
+ ctl.keycopy = memcpy;
+ /* allocate FSConnectionHash in the cache context */
+ ctl.hcxt = CacheMemoryContext;
+ FSConnectionHash = hash_create("Foreign Connections", 32,
+ &ctl,
+ HASH_ELEM | HASH_CONTEXT |
+ HASH_FUNCTION | HASH_COMPARE |
+ HASH_KEYCOPY);
+ }
+
+ /* Create key value for the entry. */
+ MemSet(&key, 0, sizeof(key));
+ key.serverid = server->serverid;
+ key.userid = GetOuterUserId();
+
+ /*
+ * Find cached entry for requested connection. If we couldn't find,
+ * callback function of ResourceOwner should be registered to clean the
+ * connection up on error including user interrupt.
+ */
+ entry = hash_search(FSConnectionHash, &key, HASH_ENTER, &found);
+ if (!found)
+ {
+ entry->refs = 0;
+ entry->use_tx = false;
+ entry->conn = NULL;
+ RegisterResourceReleaseCallback(cleanup_connection, entry);
+ }
+
+ /*
+ * We don't check the health of cached connection here, because it would
+ * require some overhead. Broken connection and its cache entry will be
+ * cleaned up when the connection is actually used.
+ */
+
+ /*
+ * If cache entry doesn't have connection, we have to establish new
+ * connection.
+ */
+ if (entry->conn == NULL)
+ {
+ /*
+ * Use PG_TRY block to ensure closing connection on error.
+ */
+ PG_TRY();
+ {
+ /*
+ * Connect to the foreign PostgreSQL server, and store it in cache
+ * entry to keep new connection.
+ * Note: key items of entry has already been initialized in
+ * hash_search(HASH_ENTER).
+ */
+ entry->conn = connect_pg_server(server, user);
+ }
+ PG_CATCH();
+ {
+ /* Clear connection cache entry on error case. */
+ PQfinish(entry->conn);
+ entry->refs = 0;
+ entry->use_tx = false;
+ entry->conn = NULL;
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+ }
+
+ /* Increase connection reference counter. */
+ entry->refs++;
+
+ /*
+ * If requester is the only referrer of this connection, start transaction
+ * with the same isolation level as the local transaction we are in.
+ * We need to remember whether this connection uses remote transaction to
+ * abort it when this connection is released completely.
+ */
+ if (use_tx && entry->refs == 1)
+ begin_remote_tx(entry->conn);
+ entry->use_tx = use_tx;
+
+
+ return entry->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 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. Those two object hold only libpq options.
+ * Extra 3 items are for:
+ * *) fallback_application_name
+ * *) client_encoding
+ * *) NULL termination (end marker)
+ *
+ * Note: We don't omit any parameters even target database might be older
+ * than local, because unexpected parameters are just ignored.
+ */
+ n = list_length(server->options) + list_length(user->options) + 3;
+ 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 += ExtractConnectionOptions(server->options,
+ all_keywords + n, all_values + n);
+ n += ExtractConnectionOptions(user->options,
+ all_keywords + n, all_values + n);
+ all_keywords[n] = all_values[n] = NULL;
+
+ for (i = 0, j = 0; all_keywords[i]; i++)
+ {
+ keywords[j] = all_keywords[i];
+ values[j] = all_values[i];
+ j++;
+ }
+
+ /* Use "pgsql_fdw" as fallback_application_name. */
+ keywords[j] = "fallback_application_name";
+ values[j++] = "pgsql_fdw";
+
+ /* Set client_encoding so that libpq can convert encoding properly. */
+ keywords[j] = "client_encoding";
+ values[j++] = GetDatabaseEncodingName();
+
+ keywords[j] = values[j] = NULL;
+ pfree(all_keywords);
+ pfree(all_values);
+
+ /* 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.")));
+ }
+
+ return conn;
+ }
+
+ /*
+ * Start transaction to use cursor to retrieve data separately.
+ */
+ static void
+ begin_remote_tx(PGconn *conn)
+ {
+ const char *sql;
+ PGresult *res;
+
+ switch (XactIsoLevel)
+ {
+ case XACT_READ_UNCOMMITTED:
+ sql = "START TRANSACTION ISOLATION LEVEL READ UNCOMMITTED";
+ break;
+ case XACT_READ_COMMITTED:
+ sql = "START TRANSACTION ISOLATION LEVEL READ COMMITTED";
+ break;
+ case XACT_REPEATABLE_READ:
+ sql = "START TRANSACTION ISOLATION LEVEL REPEATABLE READ";
+ break;
+ case XACT_SERIALIZABLE:
+ sql = "START TRANSACTION ISOLATION LEVEL SERIALIZABLE";
+ break;
+ default:
+ elog(ERROR, "unexpected isolation level: %d", XactIsoLevel);
+ break;
+ }
+
+ elog(DEBUG3, "starting remote transaction with \"%s\"", sql);
+
+ res = PQexec(conn, sql);
+ if (PQresultStatus(res) != PGRES_COMMAND_OK)
+ {
+ PQclear(res);
+ elog(ERROR, "could not start transaction: %s", PQerrorMessage(conn));
+ }
+ PQclear(res);
+ }
+
+ static void
+ abort_remote_tx(PGconn *conn)
+ {
+ PGresult *res;
+
+ elog(DEBUG3, "aborting remote transaction");
+
+ res = PQexec(conn, "ABORT TRANSACTION");
+ if (PQresultStatus(res) != PGRES_COMMAND_OK)
+ {
+ PQclear(res);
+ elog(ERROR, "could not abort transaction: %s", PQerrorMessage(conn));
+ }
+ PQclear(res);
+ }
+
+ /*
+ * 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 sequentially 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;
+ }
+ if (entry != NULL)
+ hash_seq_term(&scan);
+
+ /*
+ * If this connection uses remote transaction and caller is the last user,
+ * abort remote transaction and forget about it.
+ */
+ if (entry->use_tx && entry->refs == 1)
+ {
+ abort_remote_tx(conn);
+ entry->use_tx = false;
+ }
+
+ /*
+ * If the released connection was an orphan, just close it.
+ */
+ if (entry == NULL)
+ {
+ PQfinish(conn);
+ return;
+ }
+
+ /*
+ * Decrease reference counter of this connection. Even if the caller was
+ * the last referrer, we don't unregister it from cache.
+ */
+ entry->refs--;
+ }
+
+ /*
+ * 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)
+ {
+ PQfinish(entry->conn);
+ entry->refs = 0;
+ entry->conn = NULL;
+ }
+ }
+
+ /*
+ * Get list of connections currently active.
+ */
+ Datum pgsql_fdw_get_connections(PG_FUNCTION_ARGS);
+ PG_FUNCTION_INFO_V1(pgsql_fdw_get_connections);
+ Datum
+ pgsql_fdw_get_connections(PG_FUNCTION_ARGS)
+ {
+ ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+ HASH_SEQ_STATUS scan;
+ ConnCacheEntry *entry;
+ MemoryContext oldcontext = CurrentMemoryContext;
+ Tuplestorestate *tuplestore;
+ TupleDesc tupdesc;
+
+ /* We return list of connection with storing them in a Tuplestore. */
+ rsinfo->returnMode = SFRM_Materialize;
+ rsinfo->setResult = NULL;
+ rsinfo->setDesc = NULL;
+
+ /* Create tuplestore and copy of TupleDesc in per-query context. */
+ MemoryContextSwitchTo(rsinfo->econtext->ecxt_per_query_memory);
+
+ tupdesc = CreateTemplateTupleDesc(2, false);
+ TupleDescInitEntry(tupdesc, 1, "srvid", OIDOID, -1, 0);
+ TupleDescInitEntry(tupdesc, 2, "usesysid", OIDOID, -1, 0);
+ rsinfo->setDesc = tupdesc;
+
+ tuplestore = tuplestore_begin_heap(false, false, work_mem);
+ rsinfo->setResult = tuplestore;
+
+ MemoryContextSwitchTo(oldcontext);
+
+ /*
+ * We need to scan sequentially since we use the address to find
+ * appropriate PGconn from the hash table.
+ */
+ if (FSConnectionHash != NULL)
+ {
+ hash_seq_init(&scan, FSConnectionHash);
+ while ((entry = (ConnCacheEntry *) hash_seq_search(&scan)))
+ {
+ Datum values[2];
+ bool nulls[2];
+ HeapTuple tuple;
+
+ /* Ignore inactive connections */
+ if (PQstatus(entry->conn) != CONNECTION_OK)
+ continue;
+
+ /*
+ * Ignore other users' connections if current user isn't a
+ * superuser.
+ */
+ if (!superuser() && entry->userid != GetUserId())
+ continue;
+
+ values[0] = ObjectIdGetDatum(entry->serverid);
+ values[1] = ObjectIdGetDatum(entry->userid);
+ nulls[0] = false;
+ nulls[1] = false;
+
+ tuple = heap_formtuple(tupdesc, values, nulls);
+ tuplestore_puttuple(tuplestore, tuple);
+ }
+ }
+ tuplestore_donestoring(tuplestore);
+
+ PG_RETURN_VOID();
+ }
+
+ /*
+ * Discard persistent connection designated by given connection name.
+ */
+ Datum pgsql_fdw_disconnect(PG_FUNCTION_ARGS);
+ PG_FUNCTION_INFO_V1(pgsql_fdw_disconnect);
+ Datum
+ pgsql_fdw_disconnect(PG_FUNCTION_ARGS)
+ {
+ Oid serverid = PG_GETARG_OID(0);
+ Oid userid = PG_GETARG_OID(1);
+ ConnCacheEntry key;
+ ConnCacheEntry *entry = NULL;
+ bool found;
+
+ /* Non-superuser can't discard other users' connection. */
+ if (!superuser() && userid != GetOuterUserId())
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_RESOURCES),
+ errmsg("only superuser can discard other user's connection")));
+
+ /*
+ * If no connection has been established, or no such connections, just
+ * return "NG" to indicate nothing has done.
+ */
+ if (FSConnectionHash == NULL)
+ PG_RETURN_TEXT_P(cstring_to_text("NG"));
+
+ key.serverid = serverid;
+ key.userid = userid;
+ entry = hash_search(FSConnectionHash, &key, HASH_FIND, &found);
+ if (!found)
+ PG_RETURN_TEXT_P(cstring_to_text("NG"));
+
+ /* Discard cached connection, and clear reference counter. */
+ PQfinish(entry->conn);
+ entry->refs = 0;
+ entry->conn = NULL;
+
+ PG_RETURN_TEXT_P(cstring_to_text("OK"));
+ }
diff --git a/contrib/pgsql_fdw/connection.h b/contrib/pgsql_fdw/connection.h
index ...4427f56 .
*** a/contrib/pgsql_fdw/connection.h
--- b/contrib/pgsql_fdw/connection.h
***************
*** 0 ****
--- 1,25 ----
+ /*-------------------------------------------------------------------------
+ *
+ * connection.h
+ * Connection management for pgsql_fdw
+ *
+ * Portions Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_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, bool use_tx);
+ void ReleaseConnection(PGconn *conn);
+
+ #endif /* CONNECTION_H */
diff --git a/contrib/pgsql_fdw/deparse.c b/contrib/pgsql_fdw/deparse.c
index ...feea7b3 .
*** a/contrib/pgsql_fdw/deparse.c
--- b/contrib/pgsql_fdw/deparse.c
***************
*** 0 ****
--- 1,210 ----
+ /*-------------------------------------------------------------------------
+ *
+ * deparse.c
+ * query deparser for PostgreSQL
+ *
+ * Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/deparse.c
+ *
+ *-------------------------------------------------------------------------
+ */
+ #include "postgres.h"
+
+ #include "access/transam.h"
+ #include "foreign/foreign.h"
+ #include "lib/stringinfo.h"
+ #include "nodes/nodeFuncs.h"
+ #include "nodes/nodes.h"
+ #include "nodes/makefuncs.h"
+ #include "optimizer/clauses.h"
+ #include "optimizer/var.h"
+ #include "parser/parsetree.h"
+ #include "utils/builtins.h"
+ #include "utils/lsyscache.h"
+
+ #include "pgsql_fdw.h"
+
+ /*
+ * Context for walk-through the expression tree.
+ */
+ typedef struct foreign_executable_cxt
+ {
+ PlannerInfo *root;
+ RelOptInfo *foreignrel;
+ } foreign_executable_cxt;
+
+ /*
+ * Deparse query representation into SQL statement which suits for remote
+ * PostgreSQL server.
+ */
+ char *
+ deparseSql(Oid relid, PlannerInfo *root, RelOptInfo *baserel)
+ {
+ StringInfoData foreign_relname;
+ StringInfoData sql; /* builder for SQL statement */
+ bool first;
+ AttrNumber attr;
+ List *attr_used = NIL; /* List of AttNumber used in the query */
+ const char *nspname = NULL; /* plain namespace name */
+ const char *relname = NULL; /* plain relation name */
+ const char *q_nspname; /* quoted namespace name */
+ const char *q_relname; /* quoted relation name */
+ int i;
+ List *rtable = NIL;
+ List *context = NIL;
+
+ initStringInfo(&sql);
+ initStringInfo(&foreign_relname);
+
+ /*
+ * First of all, determine which column should be retrieved for this scan.
+ *
+ * We do this before deparsing SELECT clause because attributes which are
+ * not used in neither reltargetlist nor baserel->baserestrictinfo, quals
+ * evaluated on local, can be replaced with literal "NULL" in the SELECT
+ * clause to reduce overhead of tuple handling tuple and data transfer.
+ */
+ if (baserel->baserestrictinfo != NIL)
+ {
+ ListCell *lc;
+
+ foreach (lc, baserel->baserestrictinfo)
+ {
+ RestrictInfo *ri = (RestrictInfo *) lfirst(lc);
+ List *attrs;
+
+ /*
+ * We need to know which attributes are used in qual evaluated
+ * on the local server, because they should be listed in the
+ * SELECT clause of remote query. We can ignore attributes
+ * which are referenced only in ORDER BY/GROUP BY clause because
+ * such attributes has already been kept in reltargetlist.
+ */
+ attrs = pull_var_clause((Node *) ri->clause,
+ PVC_RECURSE_AGGREGATES,
+ PVC_RECURSE_PLACEHOLDERS);
+ attr_used = list_union(attr_used, attrs);
+ }
+ }
+
+ /*
+ * Determine foreign relation's qualified name. This is necessary for
+ * FROM clause and SELECT clause.
+ */
+ nspname = GetFdwOptionValue(InvalidOid, InvalidOid, relid,
+ InvalidAttrNumber, "nspname");
+ if (nspname == NULL)
+ nspname = get_namespace_name(get_rel_namespace(relid));
+ q_nspname = quote_identifier(nspname);
+
+ relname = GetFdwOptionValue(InvalidOid, InvalidOid, relid,
+ InvalidAttrNumber, "relname");
+ if (relname == NULL)
+ relname = get_rel_name(relid);
+ q_relname = quote_identifier(relname);
+
+ appendStringInfo(&foreign_relname, "%s.%s", q_nspname, q_relname);
+
+ /*
+ * We need to replace aliasname and colnames of the target relation so that
+ * constructed remote query is valid.
+ *
+ * Note that we skip first empty element of simple_rel_array. See also
+ * comments of simple_rel_array and simple_rte_array for the rationale.
+ */
+ for (i = 1; i < root->simple_rel_array_size; i++)
+ {
+ RangeTblEntry *rte = copyObject(root->simple_rte_array[i]);
+ List *newcolnames = NIL;
+
+ if (i == baserel->relid)
+ {
+ /*
+ * Create new list of column names which is used to deparse remote
+ * query from specified names or local column names. This list is
+ * used by deparse_expression.
+ */
+ for (attr = 1; attr <= baserel->max_attr; attr++)
+ {
+ char *colname = NULL;
+
+ /*
+ * Get colname option for each undropped attribute. If colname
+ * option is not supplied, use local column name in remote
+ * query.
+ */
+ if (get_rte_attribute_is_dropped(rte, attr))
+ colname = "";
+ else
+ {
+ colname = GetFdwOptionValue(InvalidOid, InvalidOid, relid,
+ attr, "colname");
+ if (colname == NULL)
+ colname = strVal(list_nth(rte->eref->colnames,
+ attr - 1));
+ }
+ newcolnames = lappend(newcolnames, makeString(colname));
+ }
+ rte->alias = makeAlias(relname, newcolnames);
+ }
+ rtable = lappend(rtable, rte);
+ }
+ context = deparse_context_for_rtelist(rtable);
+
+ /*
+ * deparse SELECT clause
+ *
+ * List attributes which are in either target list or local restriction.
+ * Unused attributes are replaced with a literal "NULL" for optimization.
+ *
+ * Note that nothing is added for dropped columns, though tuple constructor
+ * function requires entries for dropped columns. Such entries must be
+ * initialized with NULL before calling tuple constructor.
+ */
+ appendStringInfo(&sql, "SELECT ");
+ attr_used = list_union(attr_used, baserel->reltargetlist);
+ first = true;
+ for (attr = 1; attr <= baserel->max_attr; attr++)
+ {
+ RangeTblEntry *rte = root->simple_rte_array[baserel->relid];
+ Var *var = NULL;
+ ListCell *lc;
+
+ /* Ignore dropped attributes. */
+ if (get_rte_attribute_is_dropped(rte, attr))
+ continue;
+
+ if (!first)
+ appendStringInfo(&sql, ", ");
+ first = false;
+
+ /*
+ * We use linear search here, but it wouldn't be problem since
+ * attr_used seems to not become so large.
+ */
+ foreach (lc, attr_used)
+ {
+ var = lfirst(lc);
+ if (var->varattno == attr)
+ break;
+ var = NULL;
+ }
+ if (var != NULL)
+ appendStringInfo(&sql, "%s",
+ deparse_expression((Node *) var, context, false, false));
+ else
+ appendStringInfo(&sql, "NULL");
+ }
+ appendStringInfoChar(&sql, ' ');
+
+ /*
+ * deparse FROM clause, including alias if any
+ */
+ appendStringInfo(&sql, "FROM %s", foreign_relname.data);
+
+ elog(DEBUG3, "Remote SQL: %s", sql.data);
+ return sql.data;
+ }
+
diff --git a/contrib/pgsql_fdw/expected/pgsql_fdw.out b/contrib/pgsql_fdw/expected/pgsql_fdw.out
index ...9f2e282 .
*** a/contrib/pgsql_fdw/expected/pgsql_fdw.out
--- b/contrib/pgsql_fdw/expected/pgsql_fdw.out
***************
*** 0 ****
--- 1,532 ----
+ -- ===================================================================
+ -- create FDW objects
+ -- ===================================================================
+ CREATE EXTENSION pgsql_fdw;
+ CREATE SERVER loopback1 FOREIGN DATA WRAPPER pgsql_fdw;
+ CREATE SERVER loopback2 FOREIGN DATA WRAPPER pgsql_fdw
+ OPTIONS (dbname 'contrib_regression');
+ CREATE USER MAPPING FOR public SERVER loopback1
+ OPTIONS (user 'value', password 'value');
+ CREATE USER MAPPING FOR public SERVER loopback2;
+ CREATE FOREIGN TABLE ft1 (
+ c0 int,
+ c1 int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10)
+ ) SERVER loopback2;
+ ALTER FOREIGN TABLE ft1 DROP COLUMN c0;
+ CREATE FOREIGN TABLE ft2 (
+ c0 int,
+ c1 int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10)
+ ) SERVER loopback2;
+ ALTER FOREIGN TABLE ft2 DROP COLUMN c0;
+ -- ===================================================================
+ -- create objects used through FDW
+ -- ===================================================================
+ CREATE SCHEMA "S 1";
+ CREATE TABLE "S 1"."T 1" (
+ "C 1" int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10),
+ CONSTRAINT t1_pkey PRIMARY KEY ("C 1")
+ );
+ NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "t1_pkey" for table "T 1"
+ CREATE TABLE "S 1"."T 2" (
+ c1 int NOT NULL,
+ c2 text,
+ CONSTRAINT t2_pkey PRIMARY KEY (c1)
+ );
+ NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "t2_pkey" for table "T 2"
+ BEGIN;
+ TRUNCATE "S 1"."T 1";
+ INSERT INTO "S 1"."T 1"
+ SELECT id,
+ id % 10,
+ to_char(id, 'FM00000'),
+ '1970-01-01'::timestamptz + ((id % 100) || ' days')::interval,
+ '1970-01-01'::timestamp + ((id % 100) || ' days')::interval,
+ id % 10,
+ id % 10
+ FROM generate_series(1, 1000) id;
+ TRUNCATE "S 1"."T 2";
+ INSERT INTO "S 1"."T 2"
+ SELECT id,
+ 'AAA' || to_char(id, 'FM000')
+ FROM generate_series(1, 100) id;
+ COMMIT;
+ -- ===================================================================
+ -- tests for pgsql_fdw_validator
+ -- ===================================================================
+ ALTER FOREIGN DATA WRAPPER pgsql_fdw OPTIONS (host 'value'); -- ERROR
+ ERROR: invalid option "host"
+ HINT: Valid options in this context are:
+ -- requiressl, krbsrvname and gsslib are omitted because they depend on
+ -- configure option
+ ALTER SERVER loopback1 OPTIONS (
+ authtype 'value',
+ service 'value',
+ connect_timeout 'value',
+ dbname 'value',
+ host 'value',
+ hostaddr 'value',
+ port 'value',
+ --client_encoding 'value',
+ tty 'value',
+ options 'value',
+ application_name 'value',
+ --fallback_application_name 'value',
+ keepalives 'value',
+ keepalives_idle 'value',
+ keepalives_interval 'value',
+ -- requiressl 'value',
+ sslmode 'value',
+ sslcert 'value',
+ sslkey 'value',
+ sslrootcert 'value',
+ sslcrl 'value'
+ --requirepeer 'value',
+ -- krbsrvname 'value',
+ -- gsslib 'value',
+ --replication 'value'
+ );
+ ALTER SERVER loopback1 OPTIONS (user 'value'); -- ERROR
+ ERROR: invalid option "user"
+ HINT: Valid options in this context are: authtype, service, connect_timeout, dbname, host, hostaddr, port, tty, options, application_name, keepalives, keepalives_idle, keepalives_interval, keepalives_count, sslmode, sslcert, sslkey, sslrootcert, sslcrl, requirepeer, fetch_count
+ ALTER SERVER loopback2 OPTIONS (ADD fetch_count '2');
+ ALTER USER MAPPING FOR public SERVER loopback1
+ OPTIONS (DROP user, DROP password);
+ ALTER USER MAPPING FOR public SERVER loopback1
+ OPTIONS (host 'value'); -- ERROR
+ ERROR: invalid option "host"
+ HINT: Valid options in this context are: user, password
+ ALTER FOREIGN TABLE ft1 OPTIONS (nspname 'S 1', relname 'T 1');
+ ALTER FOREIGN TABLE ft2 OPTIONS (nspname 'S 1', relname 'T 1', fetch_count '100');
+ ALTER FOREIGN TABLE ft1 OPTIONS (invalid 'value'); -- ERROR
+ ERROR: invalid option "invalid"
+ HINT: Valid options in this context are: nspname, relname, fetch_count
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count 'a'); -- ERROR
+ ERROR: invalid value for fetch_count: "a"
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count '0'); -- ERROR
+ ERROR: invalid value for fetch_count: "0"
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count '-1'); -- ERROR
+ ERROR: invalid value for fetch_count: "-1"
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (invalid 'value'); -- ERROR
+ ERROR: invalid option "invalid"
+ HINT: Valid options in this context are: colname
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (colname 'C 1');
+ ALTER FOREIGN TABLE ft2 ALTER COLUMN c1 OPTIONS (colname 'C 1');
+ \dew+
+ List of foreign-data wrappers
+ Name | Owner | Handler | Validator | Access privileges | FDW Options | Description
+ -----------+----------+-------------------+---------------------+-------------------+-------------+-------------
+ pgsql_fdw | postgres | pgsql_fdw_handler | pgsql_fdw_validator | | |
+ (1 row)
+
+ \des+
+ List of foreign servers
+ Name | Owner | Foreign-data wrapper | Access privileges | Type | Version | FDW Options | Description
+ -----------+----------+----------------------+-------------------+------+---------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+-------------
+ loopback1 | postgres | pgsql_fdw | | | | (authtype 'value', service 'value', connect_timeout 'value', dbname 'value', host 'value', hostaddr 'value', port 'value', tty 'value', options 'value', application_name 'value', keepalives 'value', keepalives_idle 'value', keepalives_interval 'value', sslmode 'value', sslcert 'value', sslkey 'value', sslrootcert 'value', sslcrl 'value') |
+ loopback2 | postgres | pgsql_fdw | | | | (dbname 'contrib_regression', fetch_count '2') |
+ (2 rows)
+
+ \deu+
+ List of user mappings
+ Server | User name | FDW Options
+ -----------+-----------+-------------
+ loopback1 | public |
+ loopback2 | public |
+ (2 rows)
+
+ \det+
+ List of foreign tables
+ Schema | Table | Server | FDW Options | Description
+ --------+-------+-----------+---------------------------------------------------+-------------
+ public | ft1 | loopback2 | (nspname 'S 1', relname 'T 1') |
+ public | ft2 | loopback2 | (nspname 'S 1', relname 'T 1', fetch_count '100') |
+ (2 rows)
+
+ -- ===================================================================
+ -- simple queries
+ -- ===================================================================
+ -- single table, with/without alias
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
+ QUERY PLAN
+ ------------------------------------------------------------------------------------------------------------------------------
+ Limit
+ Output: c1, c2, c3, c4, c5, c6, c7
+ -> Sort
+ Output: c1, c2, c3, c4, c5, c6, c7
+ Sort Key: ft1.c3, ft1.c1
+ -> Foreign Scan on public.ft1
+ Output: c1, c2, c3, c4, c5, c6, c7
+ Remote SQL: DECLARE pgsql_fdw_cursor_0 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (8 rows)
+
+ SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ -----+----+-------+------------------------------+--------------------------+----+------------
+ 101 | 1 | 00101 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970 | 1 | 1
+ 102 | 2 | 00102 | Sat Jan 03 00:00:00 1970 PST | Sat Jan 03 00:00:00 1970 | 2 | 2
+ 103 | 3 | 00103 | Sun Jan 04 00:00:00 1970 PST | Sun Jan 04 00:00:00 1970 | 3 | 3
+ 104 | 4 | 00104 | Mon Jan 05 00:00:00 1970 PST | Mon Jan 05 00:00:00 1970 | 4 | 4
+ 105 | 5 | 00105 | Tue Jan 06 00:00:00 1970 PST | Tue Jan 06 00:00:00 1970 | 5 | 5
+ 106 | 6 | 00106 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6
+ 107 | 7 | 00107 | Thu Jan 08 00:00:00 1970 PST | Thu Jan 08 00:00:00 1970 | 7 | 7
+ 108 | 8 | 00108 | Fri Jan 09 00:00:00 1970 PST | Fri Jan 09 00:00:00 1970 | 8 | 8
+ 109 | 9 | 00109 | Sat Jan 10 00:00:00 1970 PST | Sat Jan 10 00:00:00 1970 | 9 | 9
+ 110 | 0 | 00110 | Sun Jan 11 00:00:00 1970 PST | Sun Jan 11 00:00:00 1970 | 0 | 0
+ (10 rows)
+
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+ QUERY PLAN
+ ------------------------------------------------------------------------------------------------------------------------------
+ Limit
+ Output: c1, c2, c3, c4, c5, c6, c7
+ -> Sort
+ Output: c1, c2, c3, c4, c5, c6, c7
+ Sort Key: t1.c3, t1.c1
+ -> Foreign Scan on public.ft1 t1
+ Output: c1, c2, c3, c4, c5, c6, c7
+ Remote SQL: DECLARE pgsql_fdw_cursor_2 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (8 rows)
+
+ SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ -----+----+-------+------------------------------+--------------------------+----+------------
+ 101 | 1 | 00101 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970 | 1 | 1
+ 102 | 2 | 00102 | Sat Jan 03 00:00:00 1970 PST | Sat Jan 03 00:00:00 1970 | 2 | 2
+ 103 | 3 | 00103 | Sun Jan 04 00:00:00 1970 PST | Sun Jan 04 00:00:00 1970 | 3 | 3
+ 104 | 4 | 00104 | Mon Jan 05 00:00:00 1970 PST | Mon Jan 05 00:00:00 1970 | 4 | 4
+ 105 | 5 | 00105 | Tue Jan 06 00:00:00 1970 PST | Tue Jan 06 00:00:00 1970 | 5 | 5
+ 106 | 6 | 00106 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6
+ 107 | 7 | 00107 | Thu Jan 08 00:00:00 1970 PST | Thu Jan 08 00:00:00 1970 | 7 | 7
+ 108 | 8 | 00108 | Fri Jan 09 00:00:00 1970 PST | Fri Jan 09 00:00:00 1970 | 8 | 8
+ 109 | 9 | 00109 | Sat Jan 10 00:00:00 1970 PST | Sat Jan 10 00:00:00 1970 | 9 | 9
+ 110 | 0 | 00110 | Sun Jan 11 00:00:00 1970 PST | Sun Jan 11 00:00:00 1970 | 0 | 0
+ (10 rows)
+
+ -- with WHERE clause
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
+ QUERY PLAN
+ ------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on public.ft1 t1
+ Output: c1, c2, c3, c4, c5, c6, c7
+ Filter: ((t1.c1 = 101) AND ((t1.c6)::text = '1'::text) AND (t1.c7 = '1'::bpchar))
+ Remote SQL: DECLARE pgsql_fdw_cursor_4 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (4 rows)
+
+ SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ -----+----+-------+------------------------------+--------------------------+----+------------
+ 101 | 1 | 00101 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970 | 1 | 1
+ (1 row)
+
+ -- aggregate
+ SELECT COUNT(*) FROM ft1 t1;
+ count
+ -------
+ 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
+ ----+----+-------+------------------------------+--------------------------+----+------------
+ 1 | 1 | 00001 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970 | 1 | 1
+ 2 | 2 | 00002 | Sat Jan 03 00:00:00 1970 PST | Sat Jan 03 00:00:00 1970 | 2 | 2
+ 3 | 3 | 00003 | Sun Jan 04 00:00:00 1970 PST | Sun Jan 04 00:00:00 1970 | 3 | 3
+ 4 | 4 | 00004 | Mon Jan 05 00:00:00 1970 PST | Mon Jan 05 00:00:00 1970 | 4 | 4
+ 5 | 5 | 00005 | Tue Jan 06 00:00:00 1970 PST | Tue Jan 06 00:00:00 1970 | 5 | 5
+ 6 | 6 | 00006 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6
+ 7 | 7 | 00007 | Thu Jan 08 00:00:00 1970 PST | Thu Jan 08 00:00:00 1970 | 7 | 7
+ 8 | 8 | 00008 | Fri Jan 09 00:00:00 1970 PST | Fri Jan 09 00:00:00 1970 | 8 | 8
+ 9 | 9 | 00009 | Sat Jan 10 00:00:00 1970 PST | Sat Jan 10 00:00:00 1970 | 9 | 9
+ 10 | 0 | 00010 | Sun Jan 11 00:00:00 1970 PST | Sun Jan 11 00:00:00 1970 | 0 | 0
+ (10 rows)
+
+ -- subquery+MAX
+ SELECT * FROM ft1 t1 WHERE t1.c3 = (SELECT MAX(c3) FROM ft2 t2) ORDER BY c1;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ ------+----+-------+------------------------------+--------------------------+----+------------
+ 1000 | 0 | 01000 | Thu Jan 01 00:00:00 1970 PST | Thu Jan 01 00:00:00 1970 | 0 | 0
+ (1 row)
+
+ -- used in CTE
+ WITH t1 AS (SELECT * FROM ft1 WHERE c1 <= 10) SELECT t2.c1, t2.c2, t2.c3, t2.c4 FROM t1, ft2 t2 WHERE t1.c1 = t2.c1 ORDER BY t1.c1;
+ c1 | c2 | c3 | c4
+ ----+----+-------+------------------------------
+ 1 | 1 | 00001 | Fri Jan 02 00:00:00 1970 PST
+ 2 | 2 | 00002 | Sat Jan 03 00:00:00 1970 PST
+ 3 | 3 | 00003 | Sun Jan 04 00:00:00 1970 PST
+ 4 | 4 | 00004 | Mon Jan 05 00:00:00 1970 PST
+ 5 | 5 | 00005 | Tue Jan 06 00:00:00 1970 PST
+ 6 | 6 | 00006 | Wed Jan 07 00:00:00 1970 PST
+ 7 | 7 | 00007 | Thu Jan 08 00:00:00 1970 PST
+ 8 | 8 | 00008 | Fri Jan 09 00:00:00 1970 PST
+ 9 | 9 | 00009 | Sat Jan 10 00:00:00 1970 PST
+ 10 | 0 | 00010 | Sun Jan 11 00:00:00 1970 PST
+ (10 rows)
+
+ -- fixed values
+ SELECT 'fixed', NULL FROM ft1 t1 WHERE c1 = 1;
+ ?column? | ?column?
+ ----------+----------
+ fixed |
+ (1 row)
+
+ -- user-defined operator/function
+ CREATE FUNCTION pgsql_fdw_abs(int) RETURNS int AS $$
+ BEGIN
+ RETURN abs($1);
+ END
+ $$ LANGUAGE plpgsql IMMUTABLE;
+ CREATE OPERATOR === (
+ LEFTARG = int,
+ RIGHTARG = int,
+ PROCEDURE = int4eq,
+ COMMUTATOR = ===,
+ NEGATOR = !==
+ );
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = pgsql_fdw_abs(t1.c2);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = pgsql_fdw_abs(c2))
+ Remote SQL: DECLARE pgsql_fdw_cursor_18 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 === t1.c2;
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 === c2)
+ Remote SQL: DECLARE pgsql_fdw_cursor_19 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = abs(t1.c2);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = abs(c2))
+ Remote SQL: DECLARE pgsql_fdw_cursor_20 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = t1.c2;
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = c2)
+ Remote SQL: DECLARE pgsql_fdw_cursor_21 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ DROP OPERATOR === (int, int) CASCADE;
+ DROP FUNCTION pgsql_fdw_abs(int);
+ -- ===================================================================
+ -- parameterized queries
+ -- ===================================================================
+ -- simple join
+ PREPARE st1(int, int) AS SELECT t1.c3, t2.c3 FROM ft1 t1, ft2 t2 WHERE t1.c1 = $1 AND t2.c1 = $2;
+ EXPLAIN (COSTS false) EXECUTE st1(1, 2);
+ QUERY PLAN
+ -----------------------------------------------------------------------------------------------------------------------------------------
+ Nested Loop
+ -> Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_22 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, NULL, NULL, NULL, NULL FROM "S 1"."T 1"
+ -> Materialize
+ -> Foreign Scan on ft2 t2
+ Filter: (c1 = 2)
+ Remote SQL: DECLARE pgsql_fdw_cursor_23 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, NULL, NULL, NULL, NULL FROM "S 1"."T 1"
+ (8 rows)
+
+ EXECUTE st1(1, 1);
+ c3 | c3
+ -------+-------
+ 00001 | 00001
+ (1 row)
+
+ EXECUTE st1(101, 101);
+ c3 | c3
+ -------+-------
+ 00101 | 00101
+ (1 row)
+
+ -- subquery using stable function (can't be pushed down)
+ PREPARE st2(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c4) = 6) ORDER BY c1;
+ EXPLAIN (COSTS false) EXECUTE st2(10, 20);
+ QUERY PLAN
+ ---------------------------------------------------------------------------------------------------------------------------------------------------
+ Sort
+ Sort Key: t1.c1
+ -> Hash Join
+ Hash Cond: (t1.c3 = t2.c3)
+ -> Foreign Scan on ft1 t1
+ Filter: (c1 < 20)
+ Remote SQL: DECLARE pgsql_fdw_cursor_28 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ -> Hash
+ -> HashAggregate
+ -> Foreign Scan on ft2 t2
+ Filter: ((c1 > 10) AND (date_part('dow'::text, c4) = 6::double precision))
+ Remote SQL: DECLARE pgsql_fdw_cursor_29 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, c4, NULL, NULL, NULL FROM "S 1"."T 1"
+ (12 rows)
+
+ EXECUTE st2(10, 20);
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ ----+----+-------+------------------------------+--------------------------+----+------------
+ 16 | 6 | 00016 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970 | 6 | 6
+ (1 row)
+
+ EXECUTE st1(101, 101);
+ c3 | c3
+ -------+-------
+ 00101 | 00101
+ (1 row)
+
+ -- subquery using immutable function (can be pushed down)
+ PREPARE st3(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c5) = 6) ORDER BY c1;
+ EXPLAIN (COSTS false) EXECUTE st3(10, 20);
+ QUERY PLAN
+ ---------------------------------------------------------------------------------------------------------------------------------------------------
+ Sort
+ Sort Key: t1.c1
+ -> Hash Join
+ Hash Cond: (t1.c3 = t2.c3)
+ -> Foreign Scan on ft1 t1
+ Filter: (c1 < 20)
+ Remote SQL: DECLARE pgsql_fdw_cursor_34 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ -> Hash
+ -> HashAggregate
+ -> Foreign Scan on ft2 t2
+ Filter: ((c1 > 10) AND (date_part('dow'::text, c5) = 6::double precision))
+ Remote SQL: DECLARE pgsql_fdw_cursor_35 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, NULL, c5, NULL, NULL FROM "S 1"."T 1"
+ (12 rows)
+
+ EXECUTE st3(10, 20);
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ ----+----+-------+------------------------------+--------------------------+----+------------
+ 16 | 6 | 00016 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970 | 6 | 6
+ (1 row)
+
+ EXECUTE st3(20, 30);
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ ----+----+-------+------------------------------+--------------------------+----+------------
+ 23 | 3 | 00023 | Sat Jan 24 00:00:00 1970 PST | Sat Jan 24 00:00:00 1970 | 3 | 3
+ (1 row)
+
+ -- custom plan should be chosen
+ PREPARE st4(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 = $1;
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_40 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_41 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_42 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_43 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_44 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_46 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ -- cleanup
+ DEALLOCATE st1;
+ DEALLOCATE st2;
+ DEALLOCATE st3;
+ DEALLOCATE st4;
+ -- ===================================================================
+ -- connection management
+ -- ===================================================================
+ SELECT srvname, usename FROM pgsql_fdw_connections;
+ srvname | usename
+ -----------+----------
+ loopback2 | postgres
+ (1 row)
+
+ SELECT pgsql_fdw_disconnect(srvid, usesysid) FROM pgsql_fdw_get_connections();
+ pgsql_fdw_disconnect
+ ----------------------
+ OK
+ (1 row)
+
+ SELECT srvname, usename FROM pgsql_fdw_connections;
+ srvname | usename
+ ---------+---------
+ (0 rows)
+
+ -- ===================================================================
+ -- cleanup
+ -- ===================================================================
+ DROP EXTENSION pgsql_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 server loopback2
+ drop cascades to user mapping for public
+ drop cascades to foreign table ft1
+ drop cascades to foreign table ft2
diff --git a/contrib/pgsql_fdw/option.c b/contrib/pgsql_fdw/option.c
index ...65e3c54 .
*** a/contrib/pgsql_fdw/option.c
--- b/contrib/pgsql_fdw/option.c
***************
*** 0 ****
--- 1,246 ----
+ /*-------------------------------------------------------------------------
+ *
+ * option.c
+ * FDW option handling
+ *
+ * Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/option.c
+ *
+ *-------------------------------------------------------------------------
+ */
+ #include "postgres.h"
+
+ #include "access/reloptions.h"
+ #include "catalog/pg_foreign_data_wrapper.h"
+ #include "catalog/pg_foreign_server.h"
+ #include "catalog/pg_foreign_table.h"
+ #include "catalog/pg_user_mapping.h"
+ #include "fmgr.h"
+ #include "foreign/foreign.h"
+ #include "lib/stringinfo.h"
+ #include "miscadmin.h"
+
+ #include "pgsql_fdw.h"
+
+ /*
+ * SQL functions
+ */
+ extern Datum pgsql_fdw_validator(PG_FUNCTION_ARGS);
+ PG_FUNCTION_INFO_V1(pgsql_fdw_validator);
+
+ /*
+ * Describes the valid options for objects that use this wrapper.
+ */
+ typedef struct PgsqlFdwOption
+ {
+ const char *optname;
+ Oid optcontext; /* Oid of catalog in which options may appear */
+ bool is_libpq_opt; /* true if it's used in libpq */
+ } PgsqlFdwOption;
+
+ /*
+ * Valid options for pgsql_fdw.
+ */
+ static PgsqlFdwOption valid_options[] = {
+
+ /*
+ * Options for libpq connection.
+ * Note: This list should be updated along with PQconninfoOptions in
+ * interfaces/libpq/fe-connect.c, so the order is kept as is.
+ *
+ * Some useless libpq connection options are not accepted by pgsql_fdw:
+ * client_encoding: set to local database encoding automatically
+ * fallback_application_name: fixed to "pgsql_fdw"
+ * replication: pgsql_fdw never be replication client
+ */
+ {"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},
+ #ifdef NOT_USED
+ {"client_encoding", ForeignServerRelationId, true},
+ #endif
+ {"tty", ForeignServerRelationId, true},
+ {"options", ForeignServerRelationId, true},
+ {"application_name", ForeignServerRelationId, true},
+ #ifdef NOT_USED
+ {"fallback_application_name", ForeignServerRelationId, true},
+ #endif
+ {"keepalives", ForeignServerRelationId, true},
+ {"keepalives_idle", ForeignServerRelationId, true},
+ {"keepalives_interval", ForeignServerRelationId, true},
+ {"keepalives_count", ForeignServerRelationId, true},
+ #ifdef USE_SSL
+ {"requiressl", ForeignServerRelationId, true},
+ #endif
+ {"sslmode", ForeignServerRelationId, true},
+ {"sslcert", ForeignServerRelationId, true},
+ {"sslkey", ForeignServerRelationId, true},
+ {"sslrootcert", ForeignServerRelationId, true},
+ {"sslcrl", ForeignServerRelationId, true},
+ {"requirepeer", ForeignServerRelationId, true},
+ #if defined(KRB5) || defined(ENABLE_GSS) || defined(ENABLE_SSPI)
+ {"krbsrvname", ForeignServerRelationId, true},
+ #endif
+ #if defined(ENABLE_GSS) && defined(ENABLE_SSPI)
+ {"gsslib", ForeignServerRelationId, true},
+ #endif
+ #ifdef NOT_USED
+ {"replication", ForeignServerRelationId, true},
+ #endif
+
+ /*
+ * Options for translation of object names.
+ */
+ {"nspname", ForeignTableRelationId, false},
+ {"relname", ForeignTableRelationId, false},
+ {"colname", AttributeRelationId, false},
+
+ /*
+ * Options for cursor behavior.
+ * These options can be overridden by finer-grained objects.
+ */
+ {"fetch_count", ForeignTableRelationId, false},
+ {"fetch_count", ForeignServerRelationId, false},
+
+ /* Terminating entry --- MUST BE LAST */
+ {NULL, InvalidOid, false}
+ };
+
+ /*
+ * Helper functions
+ */
+ static bool is_valid_option(const char *optname, Oid context);
+
+ /*
+ * Validate the generic options given to a FOREIGN DATA WRAPPER, SERVER,
+ * USER MAPPING or FOREIGN TABLE that uses pgsql_fdw.
+ *
+ * Raise an ERROR if the option or its value is considered invalid.
+ */
+ Datum
+ pgsql_fdw_validator(PG_FUNCTION_ARGS)
+ {
+ List *options_list = untransformRelOptions(PG_GETARG_DATUM(0));
+ Oid catalog = PG_GETARG_OID(1);
+ ListCell *cell;
+
+ /*
+ * Check that only options supported by pgsql_fdw, and allowed for the
+ * current object type, are given.
+ */
+ foreach(cell, options_list)
+ {
+ DefElem *def = (DefElem *) lfirst(cell);
+
+ if (!is_valid_option(def->defname, catalog))
+ {
+ PgsqlFdwOption *opt;
+ StringInfoData buf;
+
+ /*
+ * Unknown option specified, complain about it. Provide a hint
+ * with list of valid options for the object.
+ */
+ initStringInfo(&buf);
+ for (opt = valid_options; opt->optname; opt++)
+ {
+ if (catalog == opt->optcontext)
+ appendStringInfo(&buf, "%s%s", (buf.len > 0) ? ", " : "",
+ opt->optname);
+ }
+
+ ereport(ERROR,
+ (errcode(ERRCODE_FDW_INVALID_OPTION_NAME),
+ errmsg("invalid option \"%s\"", def->defname),
+ errhint("Valid options in this context are: %s",
+ buf.data)));
+ }
+
+ /* fetch_count be positive digit number. */
+ if (strcmp(def->defname, "fetch_count") == 0)
+ {
+ long value;
+ char *p = NULL;
+
+ value = strtol(strVal(def->arg), &p, 10);
+ if (*p != '\0' || value < 1)
+ ereport(ERROR,
+ (errcode(ERRCODE_FDW_INVALID_ATTRIBUTE_VALUE),
+ errmsg("invalid value for %s: \"%s\"",
+ def->defname, strVal(def->arg))));
+ }
+ }
+
+ /*
+ * We don't care option-specific limitation here; they will be validated at
+ * the execution time.
+ */
+
+ PG_RETURN_VOID();
+ }
+
+ /*
+ * Check whether the given option is one of the valid pgsql_fdw options.
+ * context is the Oid of the catalog holding the object the option is for.
+ */
+ static bool
+ is_valid_option(const char *optname, Oid context)
+ {
+ PgsqlFdwOption *opt;
+
+ for (opt = valid_options; opt->optname; opt++)
+ {
+ if (context == opt->optcontext && strcmp(opt->optname, optname) == 0)
+ return true;
+ }
+ return false;
+ }
+
+ /*
+ * Check whether the given option is one of the valid libpq options.
+ * context is the Oid of the catalog holding the object the option is for.
+ */
+ static bool
+ is_libpq_option(const char *optname)
+ {
+ PgsqlFdwOption *opt;
+
+ for (opt = valid_options; opt->optname; opt++)
+ {
+ if (strcmp(opt->optname, optname) == 0 && opt->is_libpq_opt)
+ return true;
+ }
+ return false;
+ }
+
+ /*
+ * Generate key-value arrays which includes only libpq options from the list
+ * which contains any kind of options.
+ */
+ int
+ ExtractConnectionOptions(List *defelems, const char **keywords, const char **values)
+ {
+ ListCell *lc;
+ int i;
+
+ i = 0;
+ foreach(lc, defelems)
+ {
+ DefElem *d = (DefElem *) lfirst(lc);
+ if (is_libpq_option(d->defname))
+ {
+ keywords[i] = d->defname;
+ values[i] = strVal(d->arg);
+ i++;
+ }
+ }
+ return i;
+ }
diff --git a/contrib/pgsql_fdw/pgsql_fdw--1.0.sql b/contrib/pgsql_fdw/pgsql_fdw--1.0.sql
index ...b0ea2b2 .
*** a/contrib/pgsql_fdw/pgsql_fdw--1.0.sql
--- b/contrib/pgsql_fdw/pgsql_fdw--1.0.sql
***************
*** 0 ****
--- 1,39 ----
+ /* contrib/pgsql_fdw/pgsql_fdw--1.0.sql */
+
+ -- complain if script is sourced in psql, rather than via CREATE EXTENSION
+ \echo Use "CREATE EXTENSION pgsql_fdw" to load this file. \quit
+
+ CREATE FUNCTION pgsql_fdw_handler()
+ RETURNS fdw_handler
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C STRICT;
+
+ CREATE FUNCTION pgsql_fdw_validator(text[], oid)
+ RETURNS void
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C STRICT;
+
+ CREATE FOREIGN DATA WRAPPER pgsql_fdw
+ HANDLER pgsql_fdw_handler
+ VALIDATOR pgsql_fdw_validator;
+
+ /* connection management functions and view */
+ CREATE FUNCTION pgsql_fdw_get_connections(out srvid oid, out usesysid oid)
+ RETURNS SETOF record
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C STRICT;
+
+ CREATE FUNCTION pgsql_fdw_disconnect(oid, oid)
+ RETURNS text
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C STRICT;
+
+ CREATE VIEW pgsql_fdw_connections AS
+ SELECT c.srvid srvid,
+ s.srvname srvname,
+ c.usesysid usesysid,
+ pg_get_userbyid(c.usesysid) usename
+ FROM pgsql_fdw_get_connections() c
+ JOIN pg_catalog.pg_foreign_server s ON (s.oid = c.srvid);
+ GRANT SELECT ON pgsql_fdw_connections TO public;
+
diff --git a/contrib/pgsql_fdw/pgsql_fdw.c b/contrib/pgsql_fdw/pgsql_fdw.c
index ...4e2ce84 .
*** a/contrib/pgsql_fdw/pgsql_fdw.c
--- b/contrib/pgsql_fdw/pgsql_fdw.c
***************
*** 0 ****
--- 1,885 ----
+ /*-------------------------------------------------------------------------
+ *
+ * pgsql_fdw.c
+ * foreign-data wrapper for remote PostgreSQL servers.
+ *
+ * Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/pgsql_fdw.c
+ *
+ *-------------------------------------------------------------------------
+ */
+ #include "postgres.h"
+ #include "fmgr.h"
+
+ #include "catalog/pg_foreign_server.h"
+ #include "catalog/pg_foreign_table.h"
+ #include "commands/explain.h"
+ #include "foreign/fdwapi.h"
+ #include "funcapi.h"
+ #include "miscadmin.h"
+ #include "nodes/nodeFuncs.h"
+ #include "optimizer/cost.h"
+ #include "optimizer/pathnode.h"
+ #include "utils/lsyscache.h"
+ #include "utils/memutils.h"
+ #include "utils/rel.h"
+
+ #include "pgsql_fdw.h"
+ #include "connection.h"
+
+ PG_MODULE_MAGIC;
+
+ /*
+ * Default fetch count for cursor. This can be overridden by fetch_count FDW
+ * option.
+ */
+ #define DEFAULT_FETCH_COUNT 10000
+
+ /*
+ * Cost to establish a connection.
+ * XXX: should be configurable per server?
+ */
+ #define CONNECTION_COSTS 100.0
+
+ /*
+ * Cost to transfer 1 byte from remote server.
+ * XXX: should be configurable per server?
+ */
+ #define TRANSFER_COSTS_PER_BYTE 0.001
+
+ /*
+ * Cursors which are used together in a local query require different name, so
+ * we use simple incremental name for that purpose. We don't care wrap around
+ * of cursor_id because it's hard to imagine that 2^32 cursors are used in a
+ * query.
+ */
+ #define CURSOR_NAME_FORMAT "pgsql_fdw_cursor_%u"
+ static uint32 cursor_id = 0;
+
+ /*
+ * Index of FDW-private items stored in FdwPlan.
+ */
+ enum FdwPrivateIndex {
+ FdwPrivateSelectSql,
+ FdwPrivateDeclareSql,
+ FdwPrivateFetchSql,
+ FdwPrivateResetSql,
+ FdwPrivateCloseSql,
+
+ /* # of elements stored in the list fdw_private */
+ FdwPrivateNum,
+ };
+
+ /*
+ * Describes an execution state of a foreign scan against a foreign table
+ * using pgsql_fdw.
+ */
+ typedef struct PgsqlFdwExecutionState
+ {
+ MemoryContext scan_cxt; /* context for per-scan lifespan data */
+
+ FdwPlan *fdwplan; /* FDW-specific planning information */
+ PGconn *conn; /* connection for the scan */
+
+ Oid *param_types; /* type array of external parameter */
+ const char **param_values; /* value array of external parameter */
+
+ int attnum; /* # of non-dropped attribute */
+ char **col_values; /* column value buffer */
+ AttInMetadata *attinmeta; /* attribute metadata */
+
+ Tuplestorestate *tuples; /* result of the scan */
+ } PgsqlFdwExecutionState;
+
+ /*
+ * SQL functions
+ */
+ extern Datum pgsql_fdw_handler(PG_FUNCTION_ARGS);
+ PG_FUNCTION_INFO_V1(pgsql_fdw_handler);
+
+ /*
+ * FDW callback routines
+ */
+ static FdwPlan *pgsqlPlanForeignScan(Oid foreigntableid,
+ PlannerInfo *root,
+ RelOptInfo *baserel);
+ static void pgsqlExplainForeignScan(ForeignScanState *node, ExplainState *es);
+ static void pgsqlBeginForeignScan(ForeignScanState *node, int eflags);
+ static TupleTableSlot *pgsqlIterateForeignScan(ForeignScanState *node);
+ static void pgsqlReScanForeignScan(ForeignScanState *node);
+ static void pgsqlEndForeignScan(ForeignScanState *node);
+
+ /*
+ * Helper functions
+ */
+ static void estimate_costs(PlannerInfo *root,
+ RelOptInfo *baserel,
+ const char *sql,
+ Oid serverid,
+ Cost *startup_cost,
+ Cost *total_cost);
+ static void execute_query(ForeignScanState *node);
+ static PGresult *fetch_result(ForeignScanState *node);
+ static void store_result(ForeignScanState *node, PGresult *res);
+ static bool contain_ext_param(Node *clause);
+ static bool contain_ext_param_walker(Node *node, void *context);
+
+ /* Exported functions, but not written in pgsql_fdw.h. */
+ void _PG_init(void);
+ void _PG_fini(void);
+
+ /*
+ * Module-specific initialization.
+ */
+ void
+ _PG_init(void)
+ {
+ }
+
+ /*
+ * Module-specific clean up.
+ */
+ void
+ _PG_fini(void)
+ {
+ }
+
+ /*
+ * The content of FdwRoutine of pgsql_fdw is never changed, so we use one
+ * static object for all scan.
+ */
+ static FdwRoutine fdwroutine = {
+
+ /* Node type */
+ T_FdwRoutine,
+
+ /* Required handler functions. */
+ pgsqlPlanForeignScan,
+ pgsqlExplainForeignScan,
+ pgsqlBeginForeignScan,
+ pgsqlIterateForeignScan,
+ pgsqlReScanForeignScan,
+ pgsqlEndForeignScan,
+
+ /* Optional handler functions. */
+ };
+
+ /*
+ * Foreign-data wrapper handler function: return a struct with pointers
+ * to my callback routines.
+ */
+ Datum
+ pgsql_fdw_handler(PG_FUNCTION_ARGS)
+ {
+ PG_RETURN_POINTER(&fdwroutine);
+ }
+
+ /*
+ * pgsqlPlanForeignScan
+ * Create a FdwPlan for a scan on the foreign table
+ */
+ static FdwPlan *
+ pgsqlPlanForeignScan(Oid foreigntableid,
+ PlannerInfo *root,
+ RelOptInfo *baserel)
+ {
+ char name[128]; /* must be larger than format + 10 */
+ StringInfoData cursor;
+ const char *fetch_count_str;
+ int fetch_count = DEFAULT_FETCH_COUNT;
+ char *sql;
+ FdwPlan *fdwplan;
+ List *fdw_private = NIL;
+ ForeignTable *table;
+ ForeignServer *server;
+
+ /* Construct FdwPlan with cost estimates */
+ fdwplan = makeNode(FdwPlan);
+ sql = deparseSql(foreigntableid, root, baserel);
+ table = GetForeignTable(foreigntableid);
+ server = GetForeignServer(table->serverid);
+ estimate_costs(root, baserel, sql, server->serverid,
+ &fdwplan->startup_cost, &fdwplan->total_cost);
+
+ /*
+ * Store plain SELECT statement in private area of FdwPlan. This will be
+ * used for executing remote query and explaining scan.
+ */
+ fdw_private = list_make1(makeString(sql));
+
+ /* Use specified fetch_count instead of default value, if any. */
+ fetch_count_str = GetFdwOptionValue(InvalidOid, InvalidOid, foreigntableid,
+ InvalidAttrNumber, "fetch_count");
+ if (fetch_count_str != NULL)
+ fetch_count = strtol(fetch_count_str, NULL, 10);
+ elog(DEBUG1, "relid=%u fetch_count=%d", foreigntableid, fetch_count);
+
+ /*
+ * We store some more information in FdwPlan to pass them beyond the
+ * boundary between planner and executor. Finally FdwPlan using cursor
+ * would hold items below:
+ *
+ * 1) plain SELECT statement (already added above)
+ * 2) SQL statement used to declare cursor
+ * 3) SQL statement used to fetch rows from cursor
+ * 4) SQL statement used to reset cursor
+ * 5) SQL statement used to close cursor
+ *
+ * These items are indexed with the enum FdwPrivateIndex, so an item
+ * can be accessed directly via list_nth(). For example of FETCH
+ * statement:
+ * list_nth(fdw_private, FdwPrivateFetchSql)
+ */
+
+ /* Construct cursor name from sequential value */
+ sprintf(name, CURSOR_NAME_FORMAT, cursor_id++);
+
+ /* Construct statement to declare cursor */
+ initStringInfo(&cursor);
+ appendStringInfo(&cursor, "DECLARE %s SCROLL CURSOR FOR %s", name, sql);
+ fdw_private = lappend(fdw_private, makeString(cursor.data));
+
+ /* Construct statement to fetch rows from cursor */
+ initStringInfo(&cursor);
+ appendStringInfo(&cursor, "FETCH %d FROM %s", fetch_count, name);
+ fdw_private = lappend(fdw_private, makeString(cursor.data));
+
+ /* Construct statement to reset cursor */
+ initStringInfo(&cursor);
+ appendStringInfo(&cursor, "MOVE ABSOLUTE 0 FROM %s", name);
+ fdw_private = lappend(fdw_private, makeString(cursor.data));
+
+ /* Construct statement to close cursor */
+ initStringInfo(&cursor);
+ appendStringInfo(&cursor, "CLOSE %s", name);
+ fdw_private = lappend(fdw_private, makeString(cursor.data));
+
+ /* Store FDW private information into FdwPlan */
+ fdwplan->fdw_private = fdw_private;
+
+ return fdwplan;
+ }
+
+ /*
+ * pgsqlExplainForeignScan
+ * Produce extra output for EXPLAIN
+ */
+ static void
+ pgsqlExplainForeignScan(ForeignScanState *node, ExplainState *es)
+ {
+ FdwPlan *fdwplan;
+ char *sql;
+
+ fdwplan = ((ForeignScan *) node->ss.ps.plan)->fdwplan;
+ sql = strVal(list_nth(fdwplan->fdw_private, FdwPrivateDeclareSql));
+ ExplainPropertyText("Remote SQL", sql, es);
+ }
+
+ /*
+ * pgsqlBeginForeignScan
+ * Initiate access to a foreign PostgreSQL table.
+ */
+ static void
+ pgsqlBeginForeignScan(ForeignScanState *node, int eflags)
+ {
+ PgsqlFdwExecutionState *festate;
+ PGconn *conn;
+ Oid relid;
+ ForeignTable *table;
+ ForeignServer *server;
+ UserMapping *user;
+ TupleTableSlot *slot = node->ss.ss_ScanTupleSlot;
+
+ /*
+ * Do nothing in EXPLAIN (no ANALYZE) case. node->fdw_state stays NULL.
+ */
+ if (eflags & EXEC_FLAG_EXPLAIN_ONLY)
+ return;
+
+ /*
+ * Save state in node->fdw_state.
+ */
+ festate = (PgsqlFdwExecutionState *) palloc(sizeof(PgsqlFdwExecutionState));
+ festate->fdwplan = ((ForeignScan *) node->ss.ps.plan)->fdwplan;
+
+ /*
+ * Create context for per-scan tuplestore.
+ */
+ festate->scan_cxt = AllocSetContextCreate(MessageContext,
+ "pgsql_fdw",
+ ALLOCSET_DEFAULT_MINSIZE,
+ ALLOCSET_DEFAULT_INITSIZE,
+ ALLOCSET_DEFAULT_MAXSIZE);
+
+ /*
+ * Get connection to the foreign server. Connection manager would
+ * establish new connection if necessary.
+ */
+ relid = RelationGetRelid(node->ss.ss_currentRelation);
+ table = GetForeignTable(relid);
+ server = GetForeignServer(table->serverid);
+ user = GetUserMapping(GetOuterUserId(), server->serverid);
+ conn = GetConnection(server, user, true);
+ festate->conn = conn;
+
+ /* Result will be filled in first Iterate call. */
+ festate->tuples = NULL;
+
+ /* Allocate buffers for column values. */
+ {
+ TupleDesc tupdesc = slot->tts_tupleDescriptor;
+ festate->col_values = palloc(sizeof(char *) * tupdesc->natts);
+ festate->attinmeta = TupleDescGetAttInMetadata(tupdesc);
+ }
+
+ /* Allocate buffers for query parameters. */
+ {
+ ParamListInfo params = node->ss.ps.state->es_param_list_info;
+ int numParams = params ? params->numParams : 0;
+
+ if (numParams > 0)
+ {
+ festate->param_types = palloc0(sizeof(Oid) * numParams);
+ festate->param_values = palloc0(sizeof(char *) * numParams);
+ }
+ else
+ {
+ festate->param_types = NULL;
+ festate->param_values = NULL;
+ }
+ }
+
+
+ /* Store FDW-specific state into ForeignScanState */
+ node->fdw_state = (void *) festate;
+
+ return;
+ }
+
+ /*
+ * pgsqlIterateForeignScan
+ * Retrieve next row from the result set, or clear tuple slot to indicate
+ * EOF.
+ *
+ * Note that using per-query context when retrieving tuples from
+ * tuplestore to ensure that returned tuples can survive until next
+ * iteration because the tuple is released implicitly via ExecClearTuple.
+ * Retrieving a tuple from tuplestore in CurrentMemoryContext (it's a
+ * per-tuple context), ExecClearTuple will free dangling pointer.
+ */
+ static TupleTableSlot *
+ pgsqlIterateForeignScan(ForeignScanState *node)
+ {
+ PgsqlFdwExecutionState *festate;
+ TupleTableSlot *slot = node->ss.ss_ScanTupleSlot;
+ PGresult *res;
+ MemoryContext oldcontext = CurrentMemoryContext;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+
+
+ /*
+ * If this is the first call after Begin, we need to execute remote query.
+ * If the query needs cursor, we declare a cursor at first call and fetch
+ * from it in later calls.
+ */
+ if (festate->tuples == NULL)
+ execute_query(node);
+
+ /*
+ * If enough tuples are left in tuplestore, just return next tuple from it.
+ */
+ MemoryContextSwitchTo(node->ss.ps.state->es_query_cxt);
+ if (tuplestore_gettupleslot(festate->tuples, true, false, slot))
+ {
+ MemoryContextSwitchTo(oldcontext);
+ return slot;
+ }
+ MemoryContextSwitchTo(oldcontext);
+
+ /*
+ * Here we need to clear partial result and fetch next bunch of tuples from
+ * from the cursor for the scan. If the fetch returns no tuple, the scan
+ * has reached the end.
+ */
+ res = fetch_result(node);
+ PG_TRY();
+ {
+ store_result(node, res);
+ PQclear(res);
+ res = NULL;
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+
+ /*
+ * If we got more tuples from the server cursor, return next tuple from
+ * tuplestore.
+ */
+ MemoryContextSwitchTo(node->ss.ps.state->es_query_cxt);
+ if (tuplestore_gettupleslot(festate->tuples, true, false, slot))
+ {
+ MemoryContextSwitchTo(oldcontext);
+ return slot;
+ }
+ MemoryContextSwitchTo(oldcontext);
+
+ /* We don't have any result even in remote server cursor. */
+ ExecClearTuple(slot);
+ return slot;
+ }
+
+ /*
+ * pgsqlReScanForeignScan
+ * - Restart this scan by resetting fetch location.
+ */
+ static void
+ pgsqlReScanForeignScan(ForeignScanState *node)
+ {
+ List *fdw_private;
+ char *sql;
+ PGconn *conn;
+ PGresult *res;
+ PgsqlFdwExecutionState *festate;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+
+ /* If we have not opened cursor yet, nothing to do. */
+ if (festate->tuples == NULL)
+ return;
+
+ /* Discard fetch results if any. */
+ tuplestore_clear(festate->tuples);
+
+ /* Reset cursor */
+ fdw_private = festate->fdwplan->fdw_private;
+ conn = festate->conn;
+ sql = strVal(list_nth(fdw_private, FdwPrivateResetSql));
+ res = PQexec(conn, sql);
+ PG_TRY();
+ {
+ if (PQresultStatus(res) != PGRES_COMMAND_OK)
+ {
+ ereport(ERROR,
+ (errmsg("could not rewind cursor"),
+ errdetail("%s", PQerrorMessage(conn)),
+ errhint("%s", sql)));
+ }
+ PQclear(res);
+ res = NULL;
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+ }
+
+ /*
+ * pgsqlEndForeignScan
+ * Finish scanning foreign table and dispose objects used for this scan
+ */
+ static void
+ pgsqlEndForeignScan(ForeignScanState *node)
+ {
+ List *fdw_private;
+ char *sql;
+ PGconn *conn;
+ PGresult *res;
+ PgsqlFdwExecutionState *festate;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+
+ /* if festate is NULL, we are in EXPLAIN; nothing to do */
+ if (festate == NULL)
+ return;
+
+ /* If we have not opened cursor yet, nothing to do. */
+ if (festate->tuples == NULL)
+ return;
+
+ /* Discard fetch results */
+ tuplestore_end(festate->tuples);
+ festate->tuples = NULL;
+
+ /* Close cursor */
+ fdw_private = festate->fdwplan->fdw_private;
+ conn = festate->conn;
+ sql = strVal(list_nth(fdw_private, FdwPrivateCloseSql));
+ res = PQexec(conn, sql);
+ PG_TRY();
+ {
+ if (PQresultStatus(res) != PGRES_COMMAND_OK)
+ {
+ ereport(ERROR,
+ (errmsg("could not close cursor"),
+ errdetail("%s", PQerrorMessage(conn)),
+ errhint("%s", sql)));
+ }
+ PQclear(res);
+ res = NULL;
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+
+ ReleaseConnection(festate->conn);
+
+ MemoryContextDelete(festate->scan_cxt);
+ festate->scan_cxt = NULL;
+ }
+
+ /*
+ * Estimate costs of scanning a foreign table.
+ */
+ static void
+ estimate_costs(PlannerInfo *root, RelOptInfo *baserel,
+ const char *sql, Oid serverid,
+ Cost *startup_cost, Cost *total_cost)
+ {
+ ListCell *lc;
+ ForeignServer *server;
+ UserMapping *user;
+ PGconn *conn = NULL;
+ PGresult *res = NULL;
+ StringInfoData buf;
+ char *plan;
+ char *p;
+ int n;
+
+ /*
+ * If the baserestrictinfo contains any Param node with paramkind
+ * PARAM_EXTERNAL, we need result of EXPLAIN for EXECUTE statement, not for
+ * simple SELECT statement. However, we can't get actual parameter values
+ * here, so we use fixed and large costs as second best so that planner
+ * tend to choose custom plan.
+ *
+ * See comments in plancache.c for details of custom plan.
+ */
+ foreach(lc, baserel->baserestrictinfo)
+ {
+ RestrictInfo *rs = (RestrictInfo *) lfirst(lc);
+ if (contain_ext_param((Node *) rs->clause))
+ {
+ *startup_cost = CONNECTION_COSTS;
+ *total_cost = 10000; /* groundless large costs */
+
+ return;
+ }
+ }
+
+ /*
+ * Get connection to the foreign server. Connection manager would
+ * establish new connection if necessary.
+ */
+ server = GetForeignServer(serverid);
+ user = GetUserMapping(GetOuterUserId(), server->serverid);
+ conn = GetConnection(server, user, false);
+ initStringInfo(&buf);
+ appendStringInfo(&buf, "EXPLAIN %s", sql);
+
+ PG_TRY();
+ {
+ res = PQexec(conn, buf.data);
+ if (PQresultStatus(res) != PGRES_TUPLES_OK || PQntuples(res) == 0)
+ {
+ char *msg;
+
+ msg = pstrdup(PQerrorMessage(conn));
+ ereport(ERROR,
+ (errmsg("could not execute EXPLAIN for cost estimation"),
+ errdetail("%s", msg),
+ errhint("%s", sql)));
+ }
+
+ /*
+ * Find estimation portion from top plan node. Here we search opening
+ * parentheses from the end of the line to avoid finding unexpected
+ * parentheses.
+ */
+ plan = PQgetvalue(res, 0, 0);
+ p = strrchr(plan, '(');
+ if (p == NULL)
+ elog(ERROR, "wrong EXPLAIN output: %s", plan);
+ n = sscanf(p,
+ "(cost=%lf..%lf rows=%lf width=%d)",
+ startup_cost, total_cost, &baserel->rows, &baserel->width);
+ if (n != 4)
+ elog(ERROR, "could not get estimation from EXPLAIN output");
+
+ PQclear(res);
+ res = NULL;
+ ReleaseConnection(conn);
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+
+ /*
+ * TODO Selectivity of quals which are NOT pushed down should be also
+ * considered.
+ */
+
+ /* add cost to establish connection. */
+ *startup_cost += CONNECTION_COSTS;
+ *total_cost += CONNECTION_COSTS;
+
+ /* add cost to transfer result. */
+ *total_cost += TRANSFER_COSTS_PER_BYTE * baserel->width * baserel->tuples;
+ *total_cost += cpu_tuple_cost * baserel->tuples;
+ }
+
+ /*
+ * Execute remote query with current parameters.
+ */
+ static void
+ execute_query(ForeignScanState *node)
+ {
+ FdwPlan *fdwplan;
+ PgsqlFdwExecutionState *festate;
+ ParamListInfo params = node->ss.ps.state->es_param_list_info;
+ int numParams = params ? params->numParams : 0;
+ Oid *types = NULL;
+ const char **values = NULL;
+ char *sql;
+ PGconn *conn;
+ PGresult *res;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+ types = festate->param_types;
+ values = festate->param_values;
+
+ /*
+ * Construct parameter array in text format. We don't release memory for
+ * the arrays explicitly, because the memory usage would not be very large,
+ * and anyway they will be released in context cleanup.
+ */
+ if (numParams > 0)
+ {
+ int i;
+
+ 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;
+
+ getTypeOutputInfo(types[i], &out_func_oid, &isvarlena);
+ fmgr_info(out_func_oid, &func);
+ values[i] = OutputFunctionCall(&func, params->params[i].value);
+ }
+ }
+ }
+
+ /*
+ * Execute remote query with parameters.
+ */
+ conn = festate->conn;
+ fdwplan = ((ForeignScan *) node->ss.ps.plan)->fdwplan;
+ sql = strVal(list_nth(fdwplan->fdw_private, FdwPrivateDeclareSql));
+ res = PQexecParams(conn, sql, numParams, types, values, NULL, NULL, 0);
+ PG_TRY();
+ {
+ /*
+ * If the query has failed, reporting details is enough here.
+ * Connection(s) which are used by this query (at least used by
+ * pgsql_fdw) will be cleaned up by the foreign connection manager.
+ */
+ if (PQresultStatus(res) != PGRES_COMMAND_OK)
+ {
+ ereport(ERROR,
+ (errmsg("could not declare cursor"),
+ errdetail("%s", PQerrorMessage(conn)),
+ errhint("%s", sql)));
+ }
+
+ /* Discard result of CURSOR statement and fetch first bunch. */
+ PQclear(res);
+ res = fetch_result(node);
+
+ /*
+ * Store the result of the query into tuplestore.
+ * We must release PGresult here to avoid memory leak.
+ */
+ store_result(node, res);
+ PQclear(res);
+ res = NULL;
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+ }
+
+ /*
+ * Fetch next partial result from remote server.
+ */
+ static PGresult *
+ fetch_result(ForeignScanState *node)
+ {
+ PgsqlFdwExecutionState *festate;
+ List *fdw_private;
+ char *sql;
+ PGconn *conn;
+ PGresult *res;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+
+ /* retrieve information for fetching result. */
+ fdw_private = festate->fdwplan->fdw_private;
+ sql = strVal(list_nth(fdw_private, FdwPrivateFetchSql));
+ conn = festate->conn;
+ res = PQexec(conn, sql);
+ if (PQresultStatus(res) != PGRES_TUPLES_OK)
+ {
+ ereport(ERROR,
+ (errmsg("could not fetch rows from foreign server"),
+ errdetail("%s", PQerrorMessage(conn)),
+ errhint("%s", sql)));
+ }
+
+ return res;
+ }
+
+ /*
+ * Create tuples from PGresult and store them into tuplestore.
+ */
+ static void
+ store_result(ForeignScanState *node, PGresult *res)
+ {
+ int rows;
+ int row;
+ int i;
+ int nfields;
+ int attnum; /* number of non-dropped columns */
+ Form_pg_attribute *attrs;
+ TupleTableSlot *slot = node->ss.ss_ScanTupleSlot;
+ TupleDesc tupdesc = slot->tts_tupleDescriptor;
+ PgsqlFdwExecutionState *festate;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+ rows = PQntuples(res);
+ nfields = PQnfields(res);
+ attrs = tupdesc->attrs;
+
+ /* First, ensure that the tuplestore is empty. */
+ if (festate->tuples == NULL)
+ {
+ MemoryContext oldcontext = CurrentMemoryContext;
+
+ /*
+ * Create tuplestore to store result of the query in per-query context.
+ * Note that we use this memory context to avoid memory leak in error
+ * cases.
+ */
+ MemoryContextSwitchTo(festate->scan_cxt);
+ festate->tuples = tuplestore_begin_heap(false, false, work_mem);
+ MemoryContextSwitchTo(oldcontext);
+ }
+ else
+ {
+ /* We already have tuplestore, just need to clear contents of it. */
+ tuplestore_clear(festate->tuples);
+ }
+
+
+ /* 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)));
+
+ /* put a tuples into the slot */
+ for (row = 0; row < rows; row++)
+ {
+ int j;
+ HeapTuple tuple;
+
+ for (i = 0, j = 0; i < tupdesc->natts; i++)
+ {
+ /* skip dropped columns. */
+ if (attrs[i]->attisdropped)
+ {
+ festate->col_values[i] = NULL;
+ continue;
+ }
+
+ if (PQgetisnull(res, row, j))
+ festate->col_values[i] = NULL;
+ else
+ festate->col_values[i] = PQgetvalue(res, row, 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(festate->attinmeta, festate->col_values);
+ tuplestore_puttuple(festate->tuples, tuple);
+ }
+
+ tuplestore_donestoring(festate->tuples);
+ }
+
+ /*
+ * contain_ext_param
+ * Recursively search for Param nodes within a clause.
+ *
+ * Returns true if any parameter reference node with relkind PARAM_EXTERN
+ * found.
+ *
+ * This does not descend into subqueries, and so should be used only after
+ * reduction of sublinks to subplans, or in contexts where it's known there
+ * are no subqueries. There mustn't be outer-aggregate references either.
+ *
+ * XXX: These functions could be in core, src/backend/optimizer/util/clauses.c.
+ */
+ static bool
+ contain_ext_param(Node *clause)
+ {
+ return contain_ext_param_walker(clause, NULL);
+ }
+
+ static bool
+ contain_ext_param_walker(Node *node, void *context)
+ {
+ if (node == NULL)
+ return false;
+ if (IsA(node, Param))
+ {
+ Param *param = (Param *) node;
+
+ if (param->paramkind == PARAM_EXTERN)
+ return true; /* abort the tree traversal and return true */
+ }
+ return expression_tree_walker(node, contain_ext_param_walker, context);
+ }
diff --git a/contrib/pgsql_fdw/pgsql_fdw.control b/contrib/pgsql_fdw/pgsql_fdw.control
index ...0a9c8f4 .
*** a/contrib/pgsql_fdw/pgsql_fdw.control
--- b/contrib/pgsql_fdw/pgsql_fdw.control
***************
*** 0 ****
--- 1,5 ----
+ # pgsql_fdw extension
+ comment = 'foreign-data wrapper for remote PostgreSQL servers'
+ default_version = '1.0'
+ module_pathname = '$libdir/pgsql_fdw'
+ relocatable = true
diff --git a/contrib/pgsql_fdw/pgsql_fdw.h b/contrib/pgsql_fdw/pgsql_fdw.h
index ...f6394ce .
*** a/contrib/pgsql_fdw/pgsql_fdw.h
--- b/contrib/pgsql_fdw/pgsql_fdw.h
***************
*** 0 ****
--- 1,28 ----
+ /*-------------------------------------------------------------------------
+ *
+ * pgsql_fdw.h
+ * foreign-data wrapper for remote PostgreSQL servers.
+ *
+ * Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/pgsql_fdw.h
+ *
+ *-------------------------------------------------------------------------
+ */
+
+ #ifndef PGSQL_FDW_H
+ #define PGSQL_FDW_H
+
+ #include "postgres.h"
+ #include "nodes/relation.h"
+
+ /* in option.c */
+ int ExtractConnectionOptions(List *defelems,
+ const char **keywords,
+ const char **values);
+
+ /* in deparse.c */
+ char *deparseSql(Oid relid, PlannerInfo *root, RelOptInfo *baserel);
+
+ #endif /* PGSQL_FDW_H */
diff --git a/contrib/pgsql_fdw/sql/pgsql_fdw.sql b/contrib/pgsql_fdw/sql/pgsql_fdw.sql
index ...f55bb14 .
*** a/contrib/pgsql_fdw/sql/pgsql_fdw.sql
--- b/contrib/pgsql_fdw/sql/pgsql_fdw.sql
***************
*** 0 ****
--- 1,216 ----
+ -- ===================================================================
+ -- create FDW objects
+ -- ===================================================================
+
+ CREATE EXTENSION pgsql_fdw;
+
+ CREATE SERVER loopback1 FOREIGN DATA WRAPPER pgsql_fdw;
+ CREATE SERVER loopback2 FOREIGN DATA WRAPPER pgsql_fdw
+ OPTIONS (dbname 'contrib_regression');
+
+ CREATE USER MAPPING FOR public SERVER loopback1
+ OPTIONS (user 'value', password 'value');
+ CREATE USER MAPPING FOR public SERVER loopback2;
+
+ CREATE FOREIGN TABLE ft1 (
+ c0 int,
+ c1 int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10)
+ ) SERVER loopback2;
+ ALTER FOREIGN TABLE ft1 DROP COLUMN c0;
+
+ CREATE FOREIGN TABLE ft2 (
+ c0 int,
+ c1 int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10)
+ ) SERVER loopback2;
+ ALTER FOREIGN TABLE ft2 DROP COLUMN c0;
+
+ -- ===================================================================
+ -- create objects used through FDW
+ -- ===================================================================
+ CREATE SCHEMA "S 1";
+ CREATE TABLE "S 1"."T 1" (
+ "C 1" int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10),
+ CONSTRAINT t1_pkey PRIMARY KEY ("C 1")
+ );
+ CREATE TABLE "S 1"."T 2" (
+ c1 int NOT NULL,
+ c2 text,
+ CONSTRAINT t2_pkey PRIMARY KEY (c1)
+ );
+
+ BEGIN;
+ TRUNCATE "S 1"."T 1";
+ INSERT INTO "S 1"."T 1"
+ SELECT id,
+ id % 10,
+ to_char(id, 'FM00000'),
+ '1970-01-01'::timestamptz + ((id % 100) || ' days')::interval,
+ '1970-01-01'::timestamp + ((id % 100) || ' days')::interval,
+ id % 10,
+ id % 10
+ FROM generate_series(1, 1000) id;
+ TRUNCATE "S 1"."T 2";
+ INSERT INTO "S 1"."T 2"
+ SELECT id,
+ 'AAA' || to_char(id, 'FM000')
+ FROM generate_series(1, 100) id;
+ COMMIT;
+
+ -- ===================================================================
+ -- tests for pgsql_fdw_validator
+ -- ===================================================================
+ ALTER FOREIGN DATA WRAPPER pgsql_fdw OPTIONS (host 'value'); -- ERROR
+ -- requiressl, krbsrvname and gsslib are omitted because they depend on
+ -- configure option
+ ALTER SERVER loopback1 OPTIONS (
+ authtype 'value',
+ service 'value',
+ connect_timeout 'value',
+ dbname 'value',
+ host 'value',
+ hostaddr 'value',
+ port 'value',
+ --client_encoding 'value',
+ tty 'value',
+ options 'value',
+ application_name 'value',
+ --fallback_application_name 'value',
+ keepalives 'value',
+ keepalives_idle 'value',
+ keepalives_interval 'value',
+ -- requiressl 'value',
+ sslmode 'value',
+ sslcert 'value',
+ sslkey 'value',
+ sslrootcert 'value',
+ sslcrl 'value'
+ --requirepeer 'value',
+ -- krbsrvname 'value',
+ -- gsslib 'value',
+ --replication 'value'
+ );
+ ALTER SERVER loopback1 OPTIONS (user 'value'); -- ERROR
+ ALTER SERVER loopback2 OPTIONS (ADD fetch_count '2');
+ ALTER USER MAPPING FOR public SERVER loopback1
+ OPTIONS (DROP user, DROP password);
+ ALTER USER MAPPING FOR public SERVER loopback1
+ OPTIONS (host 'value'); -- ERROR
+ ALTER FOREIGN TABLE ft1 OPTIONS (nspname 'S 1', relname 'T 1');
+ ALTER FOREIGN TABLE ft2 OPTIONS (nspname 'S 1', relname 'T 1', fetch_count '100');
+ ALTER FOREIGN TABLE ft1 OPTIONS (invalid 'value'); -- ERROR
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count 'a'); -- ERROR
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count '0'); -- ERROR
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count '-1'); -- ERROR
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (invalid 'value'); -- ERROR
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (colname 'C 1');
+ ALTER FOREIGN TABLE ft2 ALTER COLUMN c1 OPTIONS (colname 'C 1');
+ \dew+
+ \des+
+ \deu+
+ \det+
+
+ -- ===================================================================
+ -- simple queries
+ -- ===================================================================
+ -- single table, with/without alias
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
+ SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+ SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+ -- with WHERE clause
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
+ SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
+ -- 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
+ SELECT * FROM ft1 t1 WHERE t1.c3 = (SELECT MAX(c3) FROM ft2 t2) ORDER BY c1;
+ -- used in CTE
+ WITH t1 AS (SELECT * FROM ft1 WHERE c1 <= 10) SELECT t2.c1, t2.c2, t2.c3, t2.c4 FROM t1, ft2 t2 WHERE t1.c1 = t2.c1 ORDER BY t1.c1;
+ -- fixed values
+ SELECT 'fixed', NULL FROM ft1 t1 WHERE c1 = 1;
+ -- user-defined operator/function
+ CREATE FUNCTION pgsql_fdw_abs(int) RETURNS int AS $$
+ BEGIN
+ RETURN abs($1);
+ END
+ $$ LANGUAGE plpgsql IMMUTABLE;
+ CREATE OPERATOR === (
+ LEFTARG = int,
+ RIGHTARG = int,
+ PROCEDURE = int4eq,
+ COMMUTATOR = ===,
+ NEGATOR = !==
+ );
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = pgsql_fdw_abs(t1.c2);
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 === t1.c2;
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = abs(t1.c2);
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = t1.c2;
+ DROP OPERATOR === (int, int) CASCADE;
+ DROP FUNCTION pgsql_fdw_abs(int);
+
+ -- ===================================================================
+ -- parameterized queries
+ -- ===================================================================
+ -- simple join
+ PREPARE st1(int, int) AS SELECT t1.c3, t2.c3 FROM ft1 t1, ft2 t2 WHERE t1.c1 = $1 AND t2.c1 = $2;
+ EXPLAIN (COSTS false) EXECUTE st1(1, 2);
+ EXECUTE st1(1, 1);
+ EXECUTE st1(101, 101);
+ -- subquery using stable function (can't be pushed down)
+ PREPARE st2(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c4) = 6) ORDER BY c1;
+ EXPLAIN (COSTS false) EXECUTE st2(10, 20);
+ EXECUTE st2(10, 20);
+ EXECUTE st1(101, 101);
+ -- subquery using immutable function (can be pushed down)
+ PREPARE st3(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c5) = 6) ORDER BY c1;
+ EXPLAIN (COSTS false) EXECUTE st3(10, 20);
+ EXECUTE st3(10, 20);
+ EXECUTE st3(20, 30);
+ -- custom plan should be chosen
+ PREPARE st4(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 = $1;
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ -- cleanup
+ DEALLOCATE st1;
+ DEALLOCATE st2;
+ DEALLOCATE st3;
+ DEALLOCATE st4;
+
+ -- ===================================================================
+ -- connection management
+ -- ===================================================================
+ SELECT srvname, usename FROM pgsql_fdw_connections;
+ SELECT pgsql_fdw_disconnect(srvid, usesysid) FROM pgsql_fdw_get_connections();
+ SELECT srvname, usename FROM pgsql_fdw_connections;
+
+ -- ===================================================================
+ -- cleanup
+ -- ===================================================================
+ DROP EXTENSION pgsql_fdw CASCADE;
+
diff --git a/doc/src/sgml/contrib.sgml b/doc/src/sgml/contrib.sgml
index d4da4ee..32056d1 100644
*** a/doc/src/sgml/contrib.sgml
--- b/doc/src/sgml/contrib.sgml
*************** CREATE EXTENSION <replaceable>module_nam
*** 117,122 ****
--- 117,123 ----
&pgcrypto;
&pgfreespacemap;
&pgrowlocks;
+ &pgsql-fdw;
&pgstandby;
&pgstatstatements;
&pgstattuple;
diff --git a/doc/src/sgml/filelist.sgml b/doc/src/sgml/filelist.sgml
index b5d3c6d..de686ee 100644
*** a/doc/src/sgml/filelist.sgml
--- b/doc/src/sgml/filelist.sgml
***************
*** 125,130 ****
--- 125,131 ----
<!ENTITY pgcrypto SYSTEM "pgcrypto.sgml">
<!ENTITY pgfreespacemap SYSTEM "pgfreespacemap.sgml">
<!ENTITY pgrowlocks SYSTEM "pgrowlocks.sgml">
+ <!ENTITY pgsql-fdw SYSTEM "pgsql-fdw.sgml">
<!ENTITY pgstandby SYSTEM "pgstandby.sgml">
<!ENTITY pgstatstatements SYSTEM "pgstatstatements.sgml">
<!ENTITY pgstattuple SYSTEM "pgstattuple.sgml">
diff --git a/doc/src/sgml/pgsql-fdw.sgml b/doc/src/sgml/pgsql-fdw.sgml
index ...c9d5d8f .
*** a/doc/src/sgml/pgsql-fdw.sgml
--- b/doc/src/sgml/pgsql-fdw.sgml
***************
*** 0 ****
--- 1,253 ----
+ <!-- doc/src/sgml/pgsql-fdw.sgml -->
+
+ <sect1 id="pgsql-fdw" xreflabel="pgsql_fdw">
+ <title>pgsql_fdw</title>
+
+ <indexterm zone="pgsql-fdw">
+ <primary>pgsql_fdw</primary>
+ </indexterm>
+
+ <para>
+ The <filename>pgsql_fdw</filename> module provides a foreign-data wrapper for
+ external <productname>PostgreSQL</productname> servers.
+ With this module, users can access data stored in external
+ <productname>PostgreSQL</productname> via plain SQL statements.
+ </para>
+
+ <para>
+ Note that default wrapper <literal>pgsql_fdw</literal> is created
+ automatically during <command>CREATE EXTENSION</command> command for
+ <application>pgsql_fdw</application>.
+ </para>
+
+ <sect2>
+ <title>FDW Options of pgsql_fdw</title>
+
+ <sect3>
+ <title>Connection Options</title>
+ <para>
+ A foreign server and user mapping created using this wrapper can have
+ <application>libpq</> connection options, expect below:
+
+ <itemizedlist>
+ <listitem><para>client_encoding</para></listitem>
+ <listitem><para>fallback_application_name</para></listitem>
+ <listitem><para>replication</para></listitem>
+ </itemizedlist>
+
+ For details of <application>libpq</> connection options, see
+ <xref linkend="libpq-connect">.
+ </para>
+
+ <para>
+ <literal>user</literal> and <literal>password</literal> can be
+ specified on user mappings, and others can be specified on foreign servers.
+ </para>
+ </sect3>
+
+ <sect3>
+ <title>Object Name Options</title>
+ <para>
+ Foreign tables which were created using this wrapper, or its columns can
+ have object name options. These options can be used to specify the names
+ used in SQL statement sent to remote <productname>PostgreSQL</productname>
+ server. These options are useful when a remote object has different name
+ from corresponding local one.
+ </para>
+
+ <variablelist>
+
+ <varlistentry>
+ <term><literal>nspname</literal></term>
+ <listitem>
+ <para>
+ This option, which can be specified on a foreign table, is used as a
+ namespace (schema) reference in the SQL statement. If this options is
+ omitted, <literal>pg_class.nspname</literal> of the foreign table is
+ used.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>relname</literal></term>
+ <listitem>
+ <para>
+ This option, which can be specified on a foreign table, is used as a
+ relation (table) reference in the SQL statement. If this options is
+ omitted, <literal>pg_class.relname</literal> of the foreign table is
+ used.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>colname</literal></term>
+ <listitem>
+ <para>
+ This option, which can be specified on a column of a foreign table, is
+ used as a column (attribute) reference in the SQL statement. If this
+ option is omitted, <literal>pg_attribute.attname</literal> of the column
+ of the foreign table is used.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ </variablelist>
+
+ </sect3>
+
+ <sect3>
+ <title>Cursor Options</title>
+ <para>
+ The <application>pgsql_fdw</application> always uses cursor to retrieve the
+ result from external server. Users can control the behavior of cursor by
+ setting cursor options to foreign table or foreign server. If an option
+ is set to both objects, finer-grained setting is used. In other words,
+ foreign table's setting overrides foreign server's setting.
+ </para>
+
+ <variablelist>
+
+ <varlistentry>
+ <term><literal>fetch_count</literal></term>
+ <listitem>
+ <para>
+ This option specifies the number of rows to be fetched at a time.
+ This option accepts only integer value larger than zero. The default
+ setting is 10000.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ </variablelist>
+
+ </sect3>
+
+ </sect2>
+
+ <sect2>
+ <title>Connection Management</title>
+
+ <para>
+ The <application>pgsql_fdw</application> establishes a connection to a
+ foreign server in the beginning of the first query which uses a foreign
+ table associated to the foreign server, and reuses the connection following
+ queries and even in following foreign scans in same query.
+
+ You can see the list of active connections via
+ <structname>pgsql_fdw_connections</structname> view. It shows pair of oid
+ and name of server and local role for each active connections established by
+ <application>pgsql_fdw</application>. For security reason, only superuser
+ can see other role's connections.
+ </para>
+
+ <para>
+ Established connections are kept alive until local role changes or the
+ current transaction aborts or user requests so.
+ </para>
+
+ <para>
+ If role has been changed, active connections established as old local role
+ is kept alive but never be reused until local role has restored to original
+ role. This kind of situation happens with <command>SET ROLE</command> and
+ <command>SET SESSION AUTHORIZATION</command>.
+ </para>
+
+ <para>
+ If current transaction aborts by error or user request, all active
+ connections are disconnected automatically. This behavior avoids possible
+ connection leaks on error.
+ </para>
+
+ <para>
+ You can discard persistent connection at arbitrary timing with
+ <function>pgsql_fdw_disconnect()</function>. It takes server oid and
+ user oid as arguments. This function can handle only connections
+ established in current session; connections established by other backends
+ are not reachable.
+ </para>
+
+ <para>
+ You can discard all active and visible connections in current session with
+ using <structname>pgsql_fdw_connections</structname> and
+ <function>pgsql_fdw_disconnect()</function> together:
+ <synopsis>
+ postgres=# SELECT pgsql_fdw_disconnect(srvid, usesysid) FROM pgsql_fdw_connections;
+ pgsql_fdw_disconnect
+ ----------------------
+ OK
+ OK
+ (2 rows)
+ </synopsis>
+ </para>
+ </sect2>
+
+ <sect2>
+ <title>Transaction Management</title>
+ <para>
+ The <application>pgsql_fdw</application> begins remote transaction at the
+ beginning of a local query, and terminates it with <command>ABORT</command>
+ at the end of the local query. This means that all foreign scans on a
+ foreign server in a local query are executed in one transaction.
+ The transaction isolation level used for remote transaction is same as
+ local transaction.
+ </para>
+ <para>
+ Note that even if the isolation level of local transaction was
+ <literal>SERIALIZABLE</literal> or <literal>REPEATABLE READ</literal>,
+ series of one query might produce different result, because foreign scans
+ in different local queries are executed in different remote transactions.
+ For instance, when client started a local transaction
+ explicitly with isolation level <literal>SERIALIZABLE</literal>, and
+ executed same local query which contains a foreign table which references
+ foreign data which is updated frequently, latter result would be different
+ from former result.
+ </para>
+ <para>
+ This restriction might be relaxed in future release.
+ </para>
+ </sect2>
+
+ <sect2>
+ <title>Estimation of Costs and Rows</title>
+ <para>
+ The <application>pgsql_fdw</application> estimates the costs of a foreign
+ scan by adding up some basic costs: connection costs, remote query costs
+ and data transfer costs.
+ To get remote query costs, <application>pgsql_fdw</application> executes
+ <command>EXPLAIN</command> command on remote server for each foreign scan.
+ </para>
+ <para>
+ On the other hand, estimated rows which was returned by
+ <command>EXPLAIN</command> is used for local estimation as-is.
+ </para>
+ </sect2>
+
+ <sect2>
+ <title>EXPLAIN Output</title>
+ <para>
+ For a foreign table using <literal>pgsql_fdw</>, <command>EXPLAIN</> shows
+ a remote SQL statement which is sent to remote
+ <productname>PostgreSQL</productname> server for a ForeignScan plan node.
+ For example:
+ </para>
+ <synopsis>
+ postgres=# EXPLAIN SELECT aid FROM pgbench_accounts WHERE abalance < 0;
+ QUERY PLAN
+ --------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on pgbench_accounts (cost=100.00..8105.13 rows=302613 width=8)
+ Filter: (abalance < 0)
+ Remote SQL: DECLARE pgsql_fdw_cursor_3 SCROLL CURSOR FOR SELECT aid, NULL, abalance, NULL FROM public.pgbench_accounts
+ (3 rows)
+ </synopsis>
+ </sect2>
+
+ <sect2>
+ <title>Author</title>
+ <para>
+ Shigeru Hanada <email>shigeru.hanada@gmail.com</email>
+ </para>
+ </sect2>
+
+ </sect1>
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 9ad54c5..c2a5a7b 100644
*** a/src/backend/utils/adt/ruleutils.c
--- b/src/backend/utils/adt/ruleutils.c
*************** deparse_context_for(const char *aliasnam
*** 2161,2166 ****
--- 2161,2189 ----
return list_make1(dpns);
}
+ /* ----------
+ * deparse_context_for_rtelist - Build deparse context for a list of RTEs
+ *
+ * Given the list of RangeTableEnetry, build deparsing context for an
+ * expression referencing those relations. This is sufficient for uses of
+ * deparse_expression before plan has been created.
+ * ----------
+ */
+ List *
+ deparse_context_for_rtelist(List *rtable)
+ {
+ deparse_namespace *dpns;
+
+ dpns = (deparse_namespace *) palloc0(sizeof(deparse_namespace));
+
+ /* Build a minimal RTE for the rel */
+ dpns->rtable = rtable;
+ dpns->ctes = NIL;
+
+ /* Return a one-deep namespace stack */
+ return list_make1(dpns);
+ }
+
/*
* deparse_context_for_planstate - Build deparse context for a plan
*
diff --git a/src/include/utils/builtins.h b/src/include/utils/builtins.h
index 2c331ce..dc6932a 100644
*** a/src/include/utils/builtins.h
--- b/src/include/utils/builtins.h
*************** extern char *deparse_expression(Node *ex
*** 648,653 ****
--- 648,654 ----
extern List *deparse_context_for(const char *aliasname, Oid relid);
extern List *deparse_context_for_planstate(Node *planstate, List *ancestors,
List *rtable);
+ extern List *deparse_context_for_rtelist(List *rtable);
extern const char *quote_identifier(const char *ident);
extern char *quote_qualified_identifier(const char *qualifier,
const char *ident);
(2012/02/10 20:39), Shigeru Hanada wrote:
(2012/02/08 20:51), Shigeru Hanada wrote:
Attached revised patches. Changes from last version are below.
<snip>
I've found and fixed a bug which generates wrong remote query when any
column of a foreign table has been dropped. Also regression test for
this case is added.I attached only pgsql_fdw_v8.patch, because pgsql_fdw_pushdown_v3.patch
in last post still can be applied onto v8 patch.Regards,
The patches have been applied, but role-related regression tests failed
in my environment. I fixed it in a similar fashion of
/src/test/regress/sql/foreign_data.sql. Please find attached a updated
patch for the regression tests.
BTW, What do you think about this?
http://archives.postgresql.org/pgsql-hackers/2012-01/msg00229.php
Best regards,
Etsuro Fujita
Attachments:
pgsql_fdw.sql.patchtext/plain; name=pgsql_fdw.sql.patchDownload
*** sql/pgsql_fdw.sql.orig 2012-02-13 19:52:08.000000000 +0900
--- sql/pgsql_fdw.sql 2012-02-13 19:44:17.000000000 +0900
***************
*** 2,7 ****
--- 2,19 ----
-- create FDW objects
-- ===================================================================
+ -- Clean up in case a prior regression run failed
+
+ -- Suppress NOTICE messages when roles don't exist
+ SET client_min_messages TO 'error';
+
+ DROP ROLE IF EXISTS pgsql_fdw_user;
+
+ RESET client_min_messages;
+
+ CREATE ROLE pgsql_fdw_user LOGIN SUPERUSER;
+ SET SESSION AUTHORIZATION 'pgsql_fdw_user';
+
CREATE EXTENSION pgsql_fdw;
CREATE SERVER loopback1 FOREIGN DATA WRAPPER pgsql_fdw;
***************
*** 168,173 ****
--- 180,186 ----
EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = abs(t1.c2);
EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = t1.c2;
DROP OPERATOR === (int, int) CASCADE;
+ DROP OPERATOR !== (int, int) CASCADE;
DROP FUNCTION pgsql_fdw_abs(int);
-- ===================================================================
***************
*** 212,216 ****
-- ===================================================================
-- cleanup
-- ===================================================================
DROP EXTENSION pgsql_fdw CASCADE;
!
--- 225,231 ----
-- ===================================================================
-- cleanup
-- ===================================================================
+ DROP SCHEMA "S 1" CASCADE;
DROP EXTENSION pgsql_fdw CASCADE;
! \c
! DROP ROLE pgsql_fdw_user;
pgsql_fdw.out.patchtext/plain; name=pgsql_fdw.out.patchDownload
*** expected/pgsql_fdw.out.orig 2012-02-13 19:52:03.000000000 +0900
--- expected/pgsql_fdw.out 2012-02-13 19:51:49.000000000 +0900
***************
*** 1,6 ****
--- 1,13 ----
-- ===================================================================
-- create FDW objects
-- ===================================================================
+ -- Clean up in case a prior regression run failed
+ -- Suppress NOTICE messages when roles don't exist
+ SET client_min_messages TO 'error';
+ DROP ROLE IF EXISTS pgsql_fdw_user;
+ RESET client_min_messages;
+ CREATE ROLE pgsql_fdw_user LOGIN SUPERUSER;
+ SET SESSION AUTHORIZATION 'pgsql_fdw_user';
CREATE EXTENSION pgsql_fdw;
CREATE SERVER loopback1 FOREIGN DATA WRAPPER pgsql_fdw;
CREATE SERVER loopback2 FOREIGN DATA WRAPPER pgsql_fdw
***************
*** 130,147 ****
ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (colname 'C 1');
ALTER FOREIGN TABLE ft2 ALTER COLUMN c1 OPTIONS (colname 'C 1');
\dew+
! List of foreign-data wrappers
! Name | Owner | Handler | Validator | Access privileges | FDW Options | Description
! -----------+----------+-------------------+---------------------+-------------------+-------------+-------------
! pgsql_fdw | postgres | pgsql_fdw_handler | pgsql_fdw_validator | | |
(1 row)
\des+
! List of foreign servers
! Name | Owner | Foreign-data wrapper | Access privileges | Type | Version | FDW Options | Description
! -----------+----------+----------------------+-------------------+------+---------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+-------------
! loopback1 | postgres | pgsql_fdw | | | | (authtype 'value', service 'value', connect_timeout 'value', dbname 'value', host 'value', hostaddr 'value', port 'value', tty 'value', options 'value', application_name 'value', keepalives 'value', keepalives_idle 'value', keepalives_interval 'value', sslmode 'value', sslcert 'value', sslkey 'value', sslrootcert 'value', sslcrl 'value') |
! loopback2 | postgres | pgsql_fdw | | | | (dbname 'contrib_regression', fetch_count '2') |
(2 rows)
\deu+
--- 137,154 ----
ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (colname 'C 1');
ALTER FOREIGN TABLE ft2 ALTER COLUMN c1 OPTIONS (colname 'C 1');
\dew+
! List of foreign-data wrappers
! Name | Owner | Handler | Validator | Access privileges | FDW Options | Description
! -----------+----------------+-------------------+---------------------+-------------------+-------------+-------------
! pgsql_fdw | pgsql_fdw_user | pgsql_fdw_handler | pgsql_fdw_validator | | |
(1 row)
\des+
! List of foreign servers
! Name | Owner | Foreign-data wrapper | Access privileges | Type | Version | FDW Options | Description
! -----------+----------------+----------------------+-------------------+------+---------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+-------------
! loopback1 | pgsql_fdw_user | pgsql_fdw | | | | (authtype 'value', service 'value', connect_timeout 'value', dbname 'value', host 'value', hostaddr 'value', port 'value', tty 'value', options 'value', application_name 'value', keepalives 'value', keepalives_idle 'value', keepalives_interval 'value', sslmode 'value', sslcert 'value', sslkey 'value', sslrootcert 'value', sslcrl 'value') |
! loopback2 | pgsql_fdw_user | pgsql_fdw | | | | (dbname 'contrib_regression', fetch_count '2') |
(2 rows)
\deu+
***************
*** 349,354 ****
--- 356,362 ----
(2 rows)
DROP OPERATOR === (int, int) CASCADE;
+ DROP OPERATOR !== (int, int) CASCADE;
DROP FUNCTION pgsql_fdw_abs(int);
-- ===================================================================
-- parameterized queries
***************
*** 490,498 ****
-- connection management
-- ===================================================================
SELECT srvname, usename FROM pgsql_fdw_connections;
! srvname | usename
! -----------+----------
! loopback2 | postgres
(1 row)
SELECT pgsql_fdw_disconnect(srvid, usesysid) FROM pgsql_fdw_get_connections();
--- 498,506 ----
-- connection management
-- ===================================================================
SELECT srvname, usename FROM pgsql_fdw_connections;
! srvname | usename
! -----------+----------------
! loopback2 | pgsql_fdw_user
(1 row)
SELECT pgsql_fdw_disconnect(srvid, usesysid) FROM pgsql_fdw_get_connections();
***************
*** 509,514 ****
--- 517,526 ----
-- ===================================================================
-- cleanup
-- ===================================================================
+ DROP SCHEMA "S 1" CASCADE;
+ NOTICE: drop cascades to 2 other objects
+ DETAIL: drop cascades to table "S 1"."T 1"
+ drop cascades to table "S 1"."T 2"
DROP EXTENSION pgsql_fdw CASCADE;
NOTICE: drop cascades to 6 other objects
DETAIL: drop cascades to server loopback1
***************
*** 517,519 ****
--- 529,533 ----
drop cascades to user mapping for public
drop cascades to foreign table ft1
drop cascades to foreign table ft2
+ \c
+ DROP ROLE pgsql_fdw_user;
(2012/02/13 20:50), Etsuro Fujita wrote:
The patches have been applied, but role-related regression tests failed
in my environment. I fixed it in a similar fashion of
/src/test/regress/sql/foreign_data.sql. Please find attached a updated
patch for the regression tests.
Good catch, thanks. I'll revise pgsql_fdw tests little more.
BTW, What do you think about this?
http://archives.postgresql.org/pgsql-hackers/2012-01/msg00229.php
I'm sorry that I've left the thread unfinished... I've given up to
propose Join-push-down of foreign tables for 9.2, because it will take a
while to achieve general semantics mapping for join push-down and WHERE
clause push-down. For 9.2, I'm proposing pgsql_fdw which has WHERE
clause push-down for built-in elements which are free from collation.
I'd like to go back to that item after 9.2 development enters beta or
RC, hopefully :)
--
Shigeru Hanada
(2012/02/14 15:15), Shigeru Hanada wrote:
(2012/02/13 20:50), Etsuro Fujita wrote:
The patches have been applied, but role-related regression tests failed
in my environment. I fixed it in a similar fashion of
/src/test/regress/sql/foreign_data.sql. Please find attached a updated
patch for the regression tests.Good catch, thanks. I'll revise pgsql_fdw tests little more.
BTW, What do you think about this?
http://archives.postgresql.org/pgsql-hackers/2012-01/msg00229.php
I'm sorry that I've left the thread unfinished... I've given up to
propose Join-push-down of foreign tables for 9.2, because it will take a
while to achieve general semantics mapping for join push-down and WHERE
clause push-down. For 9.2, I'm proposing pgsql_fdw which has WHERE
clause push-down for built-in elements which are free from collation.
I'd like to go back to that item after 9.2 development enters beta or
RC, hopefully :)
OK. But my question was about the PlanForeignScan API. As discussed at
that thread, it would have to change the PlanForeignScan API to let the
FDW generate multiple paths and dump them all to add_path instead of
returning a FdwPlan struct. With this change, I think it would also
have to add a new FDW API that is called from create_foreignscan_plan()
and lets the FDW generate foreignscan plan for the base relation scanned
by the best path choosed by postgres optimizer for itself. What do you
think about it?
Best regards,
Etsuro Fujita
(2012/02/14 15:15), Shigeru Hanada wrote:
Good catch, thanks. I'll revise pgsql_fdw tests little more.
Here are the updated patches. In addition to Fujita-san's comment, I
moved DROP OPERATOR statements to clean up section of test script.
Regards,
--
Shigeru Hanada
Attachments:
fdw_helper_v5.patchtext/plain; name=fdw_helper_v5.patchDownload
diff --git a/contrib/file_fdw/file_fdw.c b/contrib/file_fdw/file_fdw.c
index 46394a8..a522e36 100644
*** a/contrib/file_fdw/file_fdw.c
--- b/contrib/file_fdw/file_fdw.c
***************
*** 26,32 ****
#include "nodes/makefuncs.h"
#include "optimizer/cost.h"
#include "utils/rel.h"
! #include "utils/syscache.h"
PG_MODULE_MAGIC;
--- 26,32 ----
#include "nodes/makefuncs.h"
#include "optimizer/cost.h"
#include "utils/rel.h"
! #include "utils/lsyscache.h"
PG_MODULE_MAGIC;
*************** get_file_fdw_attribute_options(Oid relid
*** 345,398 ****
/* Retrieve FDW options for all user-defined attributes. */
for (attnum = 1; attnum <= natts; attnum++)
{
! HeapTuple tuple;
! Form_pg_attribute attr;
! Datum datum;
! bool isnull;
/* Skip dropped attributes. */
if (tupleDesc->attrs[attnum - 1]->attisdropped)
continue;
! /*
! * We need the whole pg_attribute tuple not just what is in the
! * tupleDesc, so must do a catalog lookup.
! */
! tuple = SearchSysCache2(ATTNUM,
! RelationGetRelid(rel),
! Int16GetDatum(attnum));
! if (!HeapTupleIsValid(tuple))
! elog(ERROR, "cache lookup failed for attribute %d of relation %u",
! attnum, RelationGetRelid(rel));
! attr = (Form_pg_attribute) GETSTRUCT(tuple);
!
! datum = SysCacheGetAttr(ATTNUM,
! tuple,
! Anum_pg_attribute_attfdwoptions,
! &isnull);
! if (!isnull)
{
! List *options = untransformRelOptions(datum);
! ListCell *lc;
! foreach(lc, options)
{
! DefElem *def = (DefElem *) lfirst(lc);
!
! if (strcmp(def->defname, "force_not_null") == 0)
{
! if (defGetBoolean(def))
! {
! char *attname = pstrdup(NameStr(attr->attname));
! fnncolumns = lappend(fnncolumns, makeString(attname));
! }
}
- /* maybe in future handle other options here */
}
}
-
- ReleaseSysCache(tuple);
}
heap_close(rel, AccessShareLock);
--- 345,373 ----
/* Retrieve FDW options for all user-defined attributes. */
for (attnum = 1; attnum <= natts; attnum++)
{
! List *options;
! ListCell *lc;
/* Skip dropped attributes. */
if (tupleDesc->attrs[attnum - 1]->attisdropped)
continue;
! options = GetForeignColumnOptions(relid, attnum);
! foreach(lc, options)
{
! DefElem *def = (DefElem *) lfirst(lc);
! if (strcmp(def->defname, "force_not_null") == 0)
{
! if (defGetBoolean(def))
{
! char *attname = pstrdup(get_attname(relid, attnum));
! fnncolumns = lappend(fnncolumns, makeString(attname));
}
}
+ /* maybe in future handle other options here */
}
}
heap_close(rel, AccessShareLock);
diff --git a/doc/src/sgml/fdwhandler.sgml b/doc/src/sgml/fdwhandler.sgml
index 76ff243..d809cac 100644
*** a/doc/src/sgml/fdwhandler.sgml
--- b/doc/src/sgml/fdwhandler.sgml
*************** EndForeignScan (ForeignScanState *node);
*** 235,238 ****
--- 235,392 ----
</sect1>
+ <sect1 id="fdw-helpers">
+ <title>Foreign Data Wrapper Helper Functions</title>
+
+ <para>
+ Several helper functions are exported from core so that authors of FDW
+ can get easy access to attributes of FDW-related objects such as FDW
+ options.
+ </para>
+
+ <para>
+ <programlisting>
+ ForeignDataWrapper *
+ GetForeignDataWrapper(Oid fdwid);
+ </programlisting>
+
+ This function returns a <structname>ForeignDataWrapper</structname> object
+ for a foreign-data wrapper with given oid. A
+ <structname>ForeignDataWrapper</structname> object contains oid of the
+ wrapper itself, oid of the owner of the wrapper, name of the wrapper, oid
+ of fdwhandler function, oid of fdwvalidator function, and FDW options in
+ the form of list of <structname>DefElem</structname>.
+ </para>
+
+ <para>
+ <programlisting>
+ ForeignServer *
+ GetForeignServer(Oid serverid);
+ </programlisting>
+
+ This function returns a <structname>ForeignServer</structname> object for
+ a foreign server with given oid. A <structname>ForeignServer</structname>
+ object contains oid of the server, oid of the wrapper for the server, oid
+ of the owner of the server, name of the server, type of the server,
+ version of the server, and FDW options in the form of list of
+ <structname>DefElem</structname>.
+ </para>
+
+ <para>
+ <programlisting>
+ UserMapping *
+ GetUserMapping(Oid userid, Oid serverid);
+ </programlisting>
+
+ This function returns a <structname>UserMapping</structname> object for a
+ user mapping with given oid pair. A <structname>UserMapping</structname>
+ object contains oid of the user, oid of the server, and FDW options in the
+ form of list of <structname>DefElem</structname>.
+ </para>
+
+ <para>
+ <programlisting>
+ ForeignTable *
+ GetForeignTable(Oid relid);
+ </programlisting>
+
+ This function returns a <structname>ForeignTable</structname> object for a
+ foreign table with given oid. A <structname>ForeignTable</structname>
+ object contains oid of the foreign table, oid of the server for the table,
+ and FDW options in the form of list of <structname>DefElem</structname>.
+ </para>
+
+ <para>
+ <programlisting>
+ List *
+ GetForeignTableColumnOptions(Oid relid, AttrNumber attnum);
+ </programlisting>
+
+ This function returns per-column FDW options for a column with given
+ relation oid and attribute number in the form of list of
+ <structname>DefElem</structname>.
+ </para>
+
+ <para>
+ Some object types have name-based functions.
+ </para>
+
+ <para>
+ <programlisting>
+ ForeignDataWrapper *
+ GetForeignDataWrapperByName(const char *name, bool missing_ok);
+ </programlisting>
+
+ This function returns a <structname>ForeignDataWrapper</structname> object
+ for a foreign-data wrapper with given name. If the wrapper is not found,
+ return NULL if missing_ok was true, otherwise raise an error.
+ </para>
+
+ <para>
+ <programlisting>
+ ForeignServer *
+ GetForeignServerByName(const char *name, bool missing_ok);
+ </programlisting>
+
+ This function returns a <structname>ForeignServer</structname> object for
+ a foreign server with given name. If the server is not found, return NULL
+ if missing_ok was true, otherwise raise an error.
+ </para>
+
+ <para>
+ <programlisting>
+ char *
+ GetFdwOptionValue(Oid fdwid, Oid serverid, Oid relid, AttrNumber attnum, const char *optname);
+ </programlisting>
+
+ This function returns a copied string (created in current memory context)
+ of the value of a FDW option with given name which is set on a object with
+ given oid and attribute number. This function ignores catalogs if invalid
+ identifir is given for it.
+
+ <itemizedlist>
+ <listitem>
+ <para>
+ If attnum is <literal>InvalidAttrNumber</literal> or relid is
+ <literal>Invalidoid</literal>, <structname>pg_attribute</structname> is
+ ignored.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ If relid is <literal>InvalidOid</literal>,
+ <structname>pg_foreign_table</structname> is ignored.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ If both serverid and relid are <literal>InvalidOid</literal>,
+ <structname>pg_foreign_server</structname> is ignored.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ If all of fdwid, serverid and relid are <literal>InvalidOid</literal>,
+ <structname>pg_foreign_data_wrapper</structname> is ignored.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+
+ <para>
+ If the option with given name is set in multiple object level, the one in
+ the finest-grained object is used; e.g. priority is given to user mappings
+ over than foreign servers.
+ This function would be useful when you know which option is needed but you
+ don't know where it is set. If you already know the source object, it
+ would be more efficient to use object retrieval functions.
+ </para>
+
+ <para>
+ To use any of these functions, you need to include
+ <filename>foreign/foreign.h</filename> in your source file.
+ </para>
+
+ </sect1>
+
</chapter>
diff --git a/src/backend/foreign/foreign.c b/src/backend/foreign/foreign.c
index c4c2a61..8e98f0f 100644
*** a/src/backend/foreign/foreign.c
--- b/src/backend/foreign/foreign.c
*************** GetForeignTable(Oid relid)
*** 248,253 ****
--- 248,378 ----
/*
+ * If an option entry which matches the given name was found in the given
+ * list, returns a copy of arg string, otherwise returns NULL.
+ */
+ static char *
+ get_options_value(List *options, const char *optname)
+ {
+ ListCell *lc;
+
+ /* Find target option from the list. */
+ foreach (lc, options)
+ {
+ DefElem *def = lfirst(lc);
+
+ if (strcmp(def->defname, optname) == 0)
+ return pstrdup(strVal(def->arg));
+ }
+
+ return NULL;
+ }
+
+ /*
+ * Returns a copy of the value of specified option with searching the option
+ * from appropriate catalog. If an option was stored in multiple object
+ * levels, one in the finest-grained object level is used; lookup order is:
+ * 1) pg_attribute (only when both attnum and relid are valid)
+ * 2) pg_foreign_table (only when relid is valid)
+ * 3) pg_user_mapping (only when serverid or relid is valid)
+ * 4) pg_foreign_server (only when serverid or relid is valid)
+ * 5) pg_foreign_data_wrapper (only when fdwid or serverid or relid is valid)
+ * This priority rule would be useful in most cases using FDW options.
+ *
+ * If attnum was InvalidAttrNumber, we don't retrieve FDW optiosn from
+ * pg_attribute.attfdwoptions.
+ */
+ char *
+ GetFdwOptionValue(Oid fdwid, Oid serverid, Oid relid, AttrNumber attnum,
+ const char *optname)
+ {
+ ForeignTable *table = NULL;
+ UserMapping *user = NULL;
+ ForeignServer *server = NULL;
+ ForeignDataWrapper *wrapper = NULL;
+ char *value;
+
+ /* Do we need to search pg_attribute? */
+ if (attnum != InvalidAttrNumber && relid != InvalidOid)
+ {
+ value = get_options_value(GetForeignColumnOptions(relid, attnum),
+ optname);
+ if (value != NULL)
+ return value;
+ }
+
+ /* Do we need to search pg_foreign_table? */
+ if (relid != InvalidOid)
+ {
+ table = GetForeignTable(relid);
+ value = get_options_value(table->options, optname);
+ if (value != NULL)
+ return value;
+
+ serverid = table->serverid;
+ }
+
+ /* Do we need to search pg_user_mapping and pg_foreign_server? */
+ if (serverid != InvalidOid)
+ {
+ user = GetUserMapping(GetOuterUserId(), serverid);
+ value = get_options_value(user->options, optname);
+ if (value != NULL)
+ return value;
+
+ server = GetForeignServer(serverid);
+ value = get_options_value(server->options, optname);
+ if (value != NULL)
+ return value;
+
+ fdwid = server->fdwid;
+ }
+
+ /* Do we need to search pg_foreign_data_wrapper? */
+ if (fdwid != InvalidOid)
+ {
+ wrapper = GetForeignDataWrapper(fdwid);
+ value = get_options_value(wrapper->options, optname);
+ if (value != NULL)
+ return value;
+ }
+
+ return NULL;
+ }
+
+
+ /*
+ * GetForeignColumnOptions - Get attfdwoptions of given relation/attnum as
+ * list of DefElem.
+ */
+ List *
+ GetForeignColumnOptions(Oid relid, AttrNumber attnum)
+ {
+ List *options;
+ HeapTuple tp;
+ Datum datum;
+ bool isnull;
+
+ tp = SearchSysCache2(ATTNUM,
+ ObjectIdGetDatum(relid),
+ Int16GetDatum(attnum));
+ if (!HeapTupleIsValid(tp))
+ elog(ERROR, "cache lookup failed for attribute %d of relation %u", attnum, relid);
+ datum = SysCacheGetAttr(ATTNUM,
+ tp,
+ Anum_pg_attribute_attfdwoptions,
+ &isnull);
+ if (isnull)
+ options = NIL;
+ else
+ options = untransformRelOptions(datum);
+
+ ReleaseSysCache(tp);
+
+ return options;
+ }
+
+ /*
* GetFdwRoutine - call the specified foreign-data wrapper handler routine
* to get its FdwRoutine struct.
*/
diff --git a/src/include/foreign/foreign.h b/src/include/foreign/foreign.h
index 191122d..e25b871 100644
*** a/src/include/foreign/foreign.h
--- b/src/include/foreign/foreign.h
*************** extern ForeignDataWrapper *GetForeignDat
*** 75,80 ****
--- 75,83 ----
extern ForeignDataWrapper *GetForeignDataWrapperByName(const char *name,
bool missing_ok);
extern ForeignTable *GetForeignTable(Oid relid);
+ extern char *GetFdwOptionValue(Oid fdwid, Oid serverid, Oid relid,
+ AttrNumber attnum, const char *optname);
+ extern List *GetForeignColumnOptions(Oid relid, AttrNumber attnum);
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);
pgsql_fdw_v9.patchtext/plain; name=pgsql_fdw_v9.patchDownload
diff --git a/contrib/Makefile b/contrib/Makefile
index ac0a80a..14aa433 100644
*** a/contrib/Makefile
--- b/contrib/Makefile
*************** SUBDIRS = \
*** 41,46 ****
--- 41,47 ----
pgbench \
pgcrypto \
pgrowlocks \
+ pgsql_fdw \
pgstattuple \
seg \
spi \
diff --git a/contrib/README b/contrib/README
index a1d42a1..d3fa211 100644
*** a/contrib/README
--- b/contrib/README
*************** pgrowlocks -
*** 158,163 ****
--- 158,167 ----
A function to return row locking information
by Tatsuo Ishii <ishii@sraoss.co.jp>
+ pgsql_fdw -
+ Foreign-data wrapper for external PostgreSQL servers.
+ by Shigeru Hanada <shigeru.hanada@gmail.com>
+
pgstattuple -
Functions to return statistics about "dead" tuples and free
space within a table
diff --git a/contrib/pgsql_fdw/.gitignore b/contrib/pgsql_fdw/.gitignore
index ...0854728 .
*** a/contrib/pgsql_fdw/.gitignore
--- b/contrib/pgsql_fdw/.gitignore
***************
*** 0 ****
--- 1,4 ----
+ # Generated subdirectories
+ /results/
+ *.o
+ *.so
diff --git a/contrib/pgsql_fdw/Makefile b/contrib/pgsql_fdw/Makefile
index ...6381365 .
*** a/contrib/pgsql_fdw/Makefile
--- b/contrib/pgsql_fdw/Makefile
***************
*** 0 ****
--- 1,22 ----
+ # contrib/pgsql_fdw/Makefile
+
+ MODULE_big = pgsql_fdw
+ OBJS = pgsql_fdw.o option.o deparse.o connection.o
+ PG_CPPFLAGS = -I$(libpq_srcdir)
+ SHLIB_LINK = $(libpq)
+
+ EXTENSION = pgsql_fdw
+ DATA = pgsql_fdw--1.0.sql
+
+ REGRESS = pgsql_fdw
+
+ ifdef USE_PGXS
+ PG_CONFIG = pg_config
+ PGXS := $(shell $(PG_CONFIG) --pgxs)
+ include $(PGXS)
+ else
+ subdir = contrib/pgsql_fdw
+ top_builddir = ../..
+ include $(top_builddir)/src/Makefile.global
+ include $(top_srcdir)/contrib/contrib-global.mk
+ endif
diff --git a/contrib/pgsql_fdw/connection.c b/contrib/pgsql_fdw/connection.c
index ...6ede259 .
*** a/contrib/pgsql_fdw/connection.c
--- b/contrib/pgsql_fdw/connection.c
***************
*** 0 ****
--- 1,553 ----
+ /*-------------------------------------------------------------------------
+ *
+ * connection.c
+ * Connection management for pgsql_fdw
+ *
+ * Portions Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/connection.c
+ *
+ *-------------------------------------------------------------------------
+ */
+ #include "postgres.h"
+
+ #include "access/xact.h"
+ #include "catalog/pg_type.h"
+ #include "foreign/foreign.h"
+ #include "funcapi.h"
+ #include "libpq-fe.h"
+ #include "mb/pg_wchar.h"
+ #include "miscadmin.h"
+ #include "utils/array.h"
+ #include "utils/builtins.h"
+ #include "utils/hsearch.h"
+ #include "utils/memutils.h"
+ #include "utils/resowner.h"
+ #include "utils/tuplestore.h"
+
+ #include "pgsql_fdw.h"
+ #include "connection.h"
+
+ /* ============================================================================
+ * Connection management functions
+ * ==========================================================================*/
+
+ /*
+ * Connection cache entry managed with hash table.
+ */
+ typedef struct ConnCacheEntry
+ {
+ /* hash key must be first */
+ Oid serverid; /* oid of foreign server */
+ Oid userid; /* oid of local user */
+
+ bool use_tx; /* true when using remote transaction */
+ 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);
+ static void begin_remote_tx(PGconn *conn);
+ static void abort_remote_tx(PGconn *conn);
+
+ /*
+ * 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.
+ *
+ * When use_tx is true, remote transaction is started if caller is the only
+ * user of the connection. Isolation level of the remote transaction is same
+ * as local transaction, and remote transaction will be aborted when last
+ * user release.
+ *
+ * TODO: Note that caching connections requires a mechanism to detect change of
+ * FDW object to invalidate already established connections.
+ */
+ PGconn *
+ GetConnection(ForeignServer *server, UserMapping *user, bool use_tx)
+ {
+ bool found;
+ ConnCacheEntry *entry;
+ ConnCacheEntry key;
+
+ /* initialize connection cache if it isn't */
+ if (FSConnectionHash == NULL)
+ {
+ HASHCTL ctl;
+
+ /* hash key is a pair of oids: serverid and userid */
+ MemSet(&ctl, 0, sizeof(ctl));
+ ctl.keysize = sizeof(Oid) + sizeof(Oid);
+ ctl.entrysize = sizeof(ConnCacheEntry);
+ ctl.hash = tag_hash;
+ ctl.match = memcmp;
+ ctl.keycopy = memcpy;
+ /* allocate FSConnectionHash in the cache context */
+ ctl.hcxt = CacheMemoryContext;
+ FSConnectionHash = hash_create("Foreign Connections", 32,
+ &ctl,
+ HASH_ELEM | HASH_CONTEXT |
+ HASH_FUNCTION | HASH_COMPARE |
+ HASH_KEYCOPY);
+ }
+
+ /* Create key value for the entry. */
+ MemSet(&key, 0, sizeof(key));
+ key.serverid = server->serverid;
+ key.userid = GetOuterUserId();
+
+ /*
+ * Find cached entry for requested connection. If we couldn't find,
+ * callback function of ResourceOwner should be registered to clean the
+ * connection up on error including user interrupt.
+ */
+ entry = hash_search(FSConnectionHash, &key, HASH_ENTER, &found);
+ if (!found)
+ {
+ entry->refs = 0;
+ entry->use_tx = false;
+ entry->conn = NULL;
+ RegisterResourceReleaseCallback(cleanup_connection, entry);
+ }
+
+ /*
+ * We don't check the health of cached connection here, because it would
+ * require some overhead. Broken connection and its cache entry will be
+ * cleaned up when the connection is actually used.
+ */
+
+ /*
+ * If cache entry doesn't have connection, we have to establish new
+ * connection.
+ */
+ if (entry->conn == NULL)
+ {
+ /*
+ * Use PG_TRY block to ensure closing connection on error.
+ */
+ PG_TRY();
+ {
+ /*
+ * Connect to the foreign PostgreSQL server, and store it in cache
+ * entry to keep new connection.
+ * Note: key items of entry has already been initialized in
+ * hash_search(HASH_ENTER).
+ */
+ entry->conn = connect_pg_server(server, user);
+ }
+ PG_CATCH();
+ {
+ /* Clear connection cache entry on error case. */
+ PQfinish(entry->conn);
+ entry->refs = 0;
+ entry->use_tx = false;
+ entry->conn = NULL;
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+ }
+
+ /* Increase connection reference counter. */
+ entry->refs++;
+
+ /*
+ * If requester is the only referrer of this connection, start transaction
+ * with the same isolation level as the local transaction we are in.
+ * We need to remember whether this connection uses remote transaction to
+ * abort it when this connection is released completely.
+ */
+ if (use_tx && entry->refs == 1)
+ begin_remote_tx(entry->conn);
+ entry->use_tx = use_tx;
+
+
+ return entry->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 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. Those two object hold only libpq options.
+ * Extra 3 items are for:
+ * *) fallback_application_name
+ * *) client_encoding
+ * *) NULL termination (end marker)
+ *
+ * Note: We don't omit any parameters even target database might be older
+ * than local, because unexpected parameters are just ignored.
+ */
+ n = list_length(server->options) + list_length(user->options) + 3;
+ 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 += ExtractConnectionOptions(server->options,
+ all_keywords + n, all_values + n);
+ n += ExtractConnectionOptions(user->options,
+ all_keywords + n, all_values + n);
+ all_keywords[n] = all_values[n] = NULL;
+
+ for (i = 0, j = 0; all_keywords[i]; i++)
+ {
+ keywords[j] = all_keywords[i];
+ values[j] = all_values[i];
+ j++;
+ }
+
+ /* Use "pgsql_fdw" as fallback_application_name. */
+ keywords[j] = "fallback_application_name";
+ values[j++] = "pgsql_fdw";
+
+ /* Set client_encoding so that libpq can convert encoding properly. */
+ keywords[j] = "client_encoding";
+ values[j++] = GetDatabaseEncodingName();
+
+ keywords[j] = values[j] = NULL;
+ pfree(all_keywords);
+ pfree(all_values);
+
+ /* 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.")));
+ }
+
+ return conn;
+ }
+
+ /*
+ * Start transaction to use cursor to retrieve data separately.
+ */
+ static void
+ begin_remote_tx(PGconn *conn)
+ {
+ const char *sql;
+ PGresult *res;
+
+ switch (XactIsoLevel)
+ {
+ case XACT_READ_UNCOMMITTED:
+ sql = "START TRANSACTION ISOLATION LEVEL READ UNCOMMITTED";
+ break;
+ case XACT_READ_COMMITTED:
+ sql = "START TRANSACTION ISOLATION LEVEL READ COMMITTED";
+ break;
+ case XACT_REPEATABLE_READ:
+ sql = "START TRANSACTION ISOLATION LEVEL REPEATABLE READ";
+ break;
+ case XACT_SERIALIZABLE:
+ sql = "START TRANSACTION ISOLATION LEVEL SERIALIZABLE";
+ break;
+ default:
+ elog(ERROR, "unexpected isolation level: %d", XactIsoLevel);
+ break;
+ }
+
+ elog(DEBUG3, "starting remote transaction with \"%s\"", sql);
+
+ res = PQexec(conn, sql);
+ if (PQresultStatus(res) != PGRES_COMMAND_OK)
+ {
+ PQclear(res);
+ elog(ERROR, "could not start transaction: %s", PQerrorMessage(conn));
+ }
+ PQclear(res);
+ }
+
+ static void
+ abort_remote_tx(PGconn *conn)
+ {
+ PGresult *res;
+
+ elog(DEBUG3, "aborting remote transaction");
+
+ res = PQexec(conn, "ABORT TRANSACTION");
+ if (PQresultStatus(res) != PGRES_COMMAND_OK)
+ {
+ PQclear(res);
+ elog(ERROR, "could not abort transaction: %s", PQerrorMessage(conn));
+ }
+ PQclear(res);
+ }
+
+ /*
+ * 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 sequentially 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;
+ }
+ if (entry != NULL)
+ hash_seq_term(&scan);
+
+ /*
+ * If this connection uses remote transaction and caller is the last user,
+ * abort remote transaction and forget about it.
+ */
+ if (entry->use_tx && entry->refs == 1)
+ {
+ abort_remote_tx(conn);
+ entry->use_tx = false;
+ }
+
+ /*
+ * If the released connection was an orphan, just close it.
+ */
+ if (entry == NULL)
+ {
+ PQfinish(conn);
+ return;
+ }
+
+ /*
+ * Decrease reference counter of this connection. Even if the caller was
+ * the last referrer, we don't unregister it from cache.
+ */
+ entry->refs--;
+ }
+
+ /*
+ * 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)
+ {
+ PQfinish(entry->conn);
+ entry->refs = 0;
+ entry->conn = NULL;
+ }
+ }
+
+ /*
+ * Get list of connections currently active.
+ */
+ Datum pgsql_fdw_get_connections(PG_FUNCTION_ARGS);
+ PG_FUNCTION_INFO_V1(pgsql_fdw_get_connections);
+ Datum
+ pgsql_fdw_get_connections(PG_FUNCTION_ARGS)
+ {
+ ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+ HASH_SEQ_STATUS scan;
+ ConnCacheEntry *entry;
+ MemoryContext oldcontext = CurrentMemoryContext;
+ Tuplestorestate *tuplestore;
+ TupleDesc tupdesc;
+
+ /* We return list of connection with storing them in a Tuplestore. */
+ rsinfo->returnMode = SFRM_Materialize;
+ rsinfo->setResult = NULL;
+ rsinfo->setDesc = NULL;
+
+ /* Create tuplestore and copy of TupleDesc in per-query context. */
+ MemoryContextSwitchTo(rsinfo->econtext->ecxt_per_query_memory);
+
+ tupdesc = CreateTemplateTupleDesc(2, false);
+ TupleDescInitEntry(tupdesc, 1, "srvid", OIDOID, -1, 0);
+ TupleDescInitEntry(tupdesc, 2, "usesysid", OIDOID, -1, 0);
+ rsinfo->setDesc = tupdesc;
+
+ tuplestore = tuplestore_begin_heap(false, false, work_mem);
+ rsinfo->setResult = tuplestore;
+
+ MemoryContextSwitchTo(oldcontext);
+
+ /*
+ * We need to scan sequentially since we use the address to find
+ * appropriate PGconn from the hash table.
+ */
+ if (FSConnectionHash != NULL)
+ {
+ hash_seq_init(&scan, FSConnectionHash);
+ while ((entry = (ConnCacheEntry *) hash_seq_search(&scan)))
+ {
+ Datum values[2];
+ bool nulls[2];
+ HeapTuple tuple;
+
+ /* Ignore inactive connections */
+ if (PQstatus(entry->conn) != CONNECTION_OK)
+ continue;
+
+ /*
+ * Ignore other users' connections if current user isn't a
+ * superuser.
+ */
+ if (!superuser() && entry->userid != GetUserId())
+ continue;
+
+ values[0] = ObjectIdGetDatum(entry->serverid);
+ values[1] = ObjectIdGetDatum(entry->userid);
+ nulls[0] = false;
+ nulls[1] = false;
+
+ tuple = heap_formtuple(tupdesc, values, nulls);
+ tuplestore_puttuple(tuplestore, tuple);
+ }
+ }
+ tuplestore_donestoring(tuplestore);
+
+ PG_RETURN_VOID();
+ }
+
+ /*
+ * Discard persistent connection designated by given connection name.
+ */
+ Datum pgsql_fdw_disconnect(PG_FUNCTION_ARGS);
+ PG_FUNCTION_INFO_V1(pgsql_fdw_disconnect);
+ Datum
+ pgsql_fdw_disconnect(PG_FUNCTION_ARGS)
+ {
+ Oid serverid = PG_GETARG_OID(0);
+ Oid userid = PG_GETARG_OID(1);
+ ConnCacheEntry key;
+ ConnCacheEntry *entry = NULL;
+ bool found;
+
+ /* Non-superuser can't discard other users' connection. */
+ if (!superuser() && userid != GetOuterUserId())
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_RESOURCES),
+ errmsg("only superuser can discard other user's connection")));
+
+ /*
+ * If no connection has been established, or no such connections, just
+ * return "NG" to indicate nothing has done.
+ */
+ if (FSConnectionHash == NULL)
+ PG_RETURN_TEXT_P(cstring_to_text("NG"));
+
+ key.serverid = serverid;
+ key.userid = userid;
+ entry = hash_search(FSConnectionHash, &key, HASH_FIND, &found);
+ if (!found)
+ PG_RETURN_TEXT_P(cstring_to_text("NG"));
+
+ /* Discard cached connection, and clear reference counter. */
+ PQfinish(entry->conn);
+ entry->refs = 0;
+ entry->conn = NULL;
+
+ PG_RETURN_TEXT_P(cstring_to_text("OK"));
+ }
diff --git a/contrib/pgsql_fdw/connection.h b/contrib/pgsql_fdw/connection.h
index ...4427f56 .
*** a/contrib/pgsql_fdw/connection.h
--- b/contrib/pgsql_fdw/connection.h
***************
*** 0 ****
--- 1,25 ----
+ /*-------------------------------------------------------------------------
+ *
+ * connection.h
+ * Connection management for pgsql_fdw
+ *
+ * Portions Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_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, bool use_tx);
+ void ReleaseConnection(PGconn *conn);
+
+ #endif /* CONNECTION_H */
diff --git a/contrib/pgsql_fdw/deparse.c b/contrib/pgsql_fdw/deparse.c
index ...feea7b3 .
*** a/contrib/pgsql_fdw/deparse.c
--- b/contrib/pgsql_fdw/deparse.c
***************
*** 0 ****
--- 1,210 ----
+ /*-------------------------------------------------------------------------
+ *
+ * deparse.c
+ * query deparser for PostgreSQL
+ *
+ * Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/deparse.c
+ *
+ *-------------------------------------------------------------------------
+ */
+ #include "postgres.h"
+
+ #include "access/transam.h"
+ #include "foreign/foreign.h"
+ #include "lib/stringinfo.h"
+ #include "nodes/nodeFuncs.h"
+ #include "nodes/nodes.h"
+ #include "nodes/makefuncs.h"
+ #include "optimizer/clauses.h"
+ #include "optimizer/var.h"
+ #include "parser/parsetree.h"
+ #include "utils/builtins.h"
+ #include "utils/lsyscache.h"
+
+ #include "pgsql_fdw.h"
+
+ /*
+ * Context for walk-through the expression tree.
+ */
+ typedef struct foreign_executable_cxt
+ {
+ PlannerInfo *root;
+ RelOptInfo *foreignrel;
+ } foreign_executable_cxt;
+
+ /*
+ * Deparse query representation into SQL statement which suits for remote
+ * PostgreSQL server.
+ */
+ char *
+ deparseSql(Oid relid, PlannerInfo *root, RelOptInfo *baserel)
+ {
+ StringInfoData foreign_relname;
+ StringInfoData sql; /* builder for SQL statement */
+ bool first;
+ AttrNumber attr;
+ List *attr_used = NIL; /* List of AttNumber used in the query */
+ const char *nspname = NULL; /* plain namespace name */
+ const char *relname = NULL; /* plain relation name */
+ const char *q_nspname; /* quoted namespace name */
+ const char *q_relname; /* quoted relation name */
+ int i;
+ List *rtable = NIL;
+ List *context = NIL;
+
+ initStringInfo(&sql);
+ initStringInfo(&foreign_relname);
+
+ /*
+ * First of all, determine which column should be retrieved for this scan.
+ *
+ * We do this before deparsing SELECT clause because attributes which are
+ * not used in neither reltargetlist nor baserel->baserestrictinfo, quals
+ * evaluated on local, can be replaced with literal "NULL" in the SELECT
+ * clause to reduce overhead of tuple handling tuple and data transfer.
+ */
+ if (baserel->baserestrictinfo != NIL)
+ {
+ ListCell *lc;
+
+ foreach (lc, baserel->baserestrictinfo)
+ {
+ RestrictInfo *ri = (RestrictInfo *) lfirst(lc);
+ List *attrs;
+
+ /*
+ * We need to know which attributes are used in qual evaluated
+ * on the local server, because they should be listed in the
+ * SELECT clause of remote query. We can ignore attributes
+ * which are referenced only in ORDER BY/GROUP BY clause because
+ * such attributes has already been kept in reltargetlist.
+ */
+ attrs = pull_var_clause((Node *) ri->clause,
+ PVC_RECURSE_AGGREGATES,
+ PVC_RECURSE_PLACEHOLDERS);
+ attr_used = list_union(attr_used, attrs);
+ }
+ }
+
+ /*
+ * Determine foreign relation's qualified name. This is necessary for
+ * FROM clause and SELECT clause.
+ */
+ nspname = GetFdwOptionValue(InvalidOid, InvalidOid, relid,
+ InvalidAttrNumber, "nspname");
+ if (nspname == NULL)
+ nspname = get_namespace_name(get_rel_namespace(relid));
+ q_nspname = quote_identifier(nspname);
+
+ relname = GetFdwOptionValue(InvalidOid, InvalidOid, relid,
+ InvalidAttrNumber, "relname");
+ if (relname == NULL)
+ relname = get_rel_name(relid);
+ q_relname = quote_identifier(relname);
+
+ appendStringInfo(&foreign_relname, "%s.%s", q_nspname, q_relname);
+
+ /*
+ * We need to replace aliasname and colnames of the target relation so that
+ * constructed remote query is valid.
+ *
+ * Note that we skip first empty element of simple_rel_array. See also
+ * comments of simple_rel_array and simple_rte_array for the rationale.
+ */
+ for (i = 1; i < root->simple_rel_array_size; i++)
+ {
+ RangeTblEntry *rte = copyObject(root->simple_rte_array[i]);
+ List *newcolnames = NIL;
+
+ if (i == baserel->relid)
+ {
+ /*
+ * Create new list of column names which is used to deparse remote
+ * query from specified names or local column names. This list is
+ * used by deparse_expression.
+ */
+ for (attr = 1; attr <= baserel->max_attr; attr++)
+ {
+ char *colname = NULL;
+
+ /*
+ * Get colname option for each undropped attribute. If colname
+ * option is not supplied, use local column name in remote
+ * query.
+ */
+ if (get_rte_attribute_is_dropped(rte, attr))
+ colname = "";
+ else
+ {
+ colname = GetFdwOptionValue(InvalidOid, InvalidOid, relid,
+ attr, "colname");
+ if (colname == NULL)
+ colname = strVal(list_nth(rte->eref->colnames,
+ attr - 1));
+ }
+ newcolnames = lappend(newcolnames, makeString(colname));
+ }
+ rte->alias = makeAlias(relname, newcolnames);
+ }
+ rtable = lappend(rtable, rte);
+ }
+ context = deparse_context_for_rtelist(rtable);
+
+ /*
+ * deparse SELECT clause
+ *
+ * List attributes which are in either target list or local restriction.
+ * Unused attributes are replaced with a literal "NULL" for optimization.
+ *
+ * Note that nothing is added for dropped columns, though tuple constructor
+ * function requires entries for dropped columns. Such entries must be
+ * initialized with NULL before calling tuple constructor.
+ */
+ appendStringInfo(&sql, "SELECT ");
+ attr_used = list_union(attr_used, baserel->reltargetlist);
+ first = true;
+ for (attr = 1; attr <= baserel->max_attr; attr++)
+ {
+ RangeTblEntry *rte = root->simple_rte_array[baserel->relid];
+ Var *var = NULL;
+ ListCell *lc;
+
+ /* Ignore dropped attributes. */
+ if (get_rte_attribute_is_dropped(rte, attr))
+ continue;
+
+ if (!first)
+ appendStringInfo(&sql, ", ");
+ first = false;
+
+ /*
+ * We use linear search here, but it wouldn't be problem since
+ * attr_used seems to not become so large.
+ */
+ foreach (lc, attr_used)
+ {
+ var = lfirst(lc);
+ if (var->varattno == attr)
+ break;
+ var = NULL;
+ }
+ if (var != NULL)
+ appendStringInfo(&sql, "%s",
+ deparse_expression((Node *) var, context, false, false));
+ else
+ appendStringInfo(&sql, "NULL");
+ }
+ appendStringInfoChar(&sql, ' ');
+
+ /*
+ * deparse FROM clause, including alias if any
+ */
+ appendStringInfo(&sql, "FROM %s", foreign_relname.data);
+
+ elog(DEBUG3, "Remote SQL: %s", sql.data);
+ return sql.data;
+ }
+
diff --git a/contrib/pgsql_fdw/expected/pgsql_fdw.out b/contrib/pgsql_fdw/expected/pgsql_fdw.out
index ...7fe20e7 .
*** a/contrib/pgsql_fdw/expected/pgsql_fdw.out
--- b/contrib/pgsql_fdw/expected/pgsql_fdw.out
***************
*** 0 ****
--- 1,546 ----
+ -- ===================================================================
+ -- create FDW objects
+ -- ===================================================================
+ -- Clean up in case a prior regression run failed
+ -- Suppress NOTICE messages when roles don't exist
+ SET client_min_messages TO 'error';
+ DROP ROLE IF EXISTS pgsql_fdw_user;
+ RESET client_min_messages;
+ CREATE ROLE pgsql_fdw_user LOGIN SUPERUSER;
+ SET SESSION AUTHORIZATION 'pgsql_fdw_user';
+ CREATE EXTENSION pgsql_fdw;
+ CREATE SERVER loopback1 FOREIGN DATA WRAPPER pgsql_fdw;
+ CREATE SERVER loopback2 FOREIGN DATA WRAPPER pgsql_fdw
+ OPTIONS (dbname 'contrib_regression');
+ CREATE USER MAPPING FOR public SERVER loopback1
+ OPTIONS (user 'value', password 'value');
+ CREATE USER MAPPING FOR pgsql_fdw_user SERVER loopback2;
+ CREATE FOREIGN TABLE ft1 (
+ c0 int,
+ c1 int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10)
+ ) SERVER loopback2;
+ ALTER FOREIGN TABLE ft1 DROP COLUMN c0;
+ CREATE FOREIGN TABLE ft2 (
+ c0 int,
+ c1 int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10)
+ ) SERVER loopback2;
+ ALTER FOREIGN TABLE ft2 DROP COLUMN c0;
+ -- ===================================================================
+ -- create objects used through FDW
+ -- ===================================================================
+ CREATE SCHEMA "S 1";
+ CREATE TABLE "S 1"."T 1" (
+ "C 1" int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10),
+ CONSTRAINT t1_pkey PRIMARY KEY ("C 1")
+ );
+ NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "t1_pkey" for table "T 1"
+ CREATE TABLE "S 1"."T 2" (
+ c1 int NOT NULL,
+ c2 text,
+ CONSTRAINT t2_pkey PRIMARY KEY (c1)
+ );
+ NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "t2_pkey" for table "T 2"
+ BEGIN;
+ TRUNCATE "S 1"."T 1";
+ INSERT INTO "S 1"."T 1"
+ SELECT id,
+ id % 10,
+ to_char(id, 'FM00000'),
+ '1970-01-01'::timestamptz + ((id % 100) || ' days')::interval,
+ '1970-01-01'::timestamp + ((id % 100) || ' days')::interval,
+ id % 10,
+ id % 10
+ FROM generate_series(1, 1000) id;
+ TRUNCATE "S 1"."T 2";
+ INSERT INTO "S 1"."T 2"
+ SELECT id,
+ 'AAA' || to_char(id, 'FM000')
+ FROM generate_series(1, 100) id;
+ COMMIT;
+ -- ===================================================================
+ -- tests for pgsql_fdw_validator
+ -- ===================================================================
+ ALTER FOREIGN DATA WRAPPER pgsql_fdw OPTIONS (host 'value'); -- ERROR
+ ERROR: invalid option "host"
+ HINT: Valid options in this context are:
+ -- requiressl, krbsrvname and gsslib are omitted because they depend on
+ -- configure option
+ ALTER SERVER loopback1 OPTIONS (
+ authtype 'value',
+ service 'value',
+ connect_timeout 'value',
+ dbname 'value',
+ host 'value',
+ hostaddr 'value',
+ port 'value',
+ --client_encoding 'value',
+ tty 'value',
+ options 'value',
+ application_name 'value',
+ --fallback_application_name 'value',
+ keepalives 'value',
+ keepalives_idle 'value',
+ keepalives_interval 'value',
+ -- requiressl 'value',
+ sslmode 'value',
+ sslcert 'value',
+ sslkey 'value',
+ sslrootcert 'value',
+ sslcrl 'value'
+ --requirepeer 'value',
+ -- krbsrvname 'value',
+ -- gsslib 'value',
+ --replication 'value'
+ );
+ ALTER SERVER loopback1 OPTIONS (user 'value'); -- ERROR
+ ERROR: invalid option "user"
+ HINT: Valid options in this context are: authtype, service, connect_timeout, dbname, host, hostaddr, port, tty, options, application_name, keepalives, keepalives_idle, keepalives_interval, keepalives_count, sslmode, sslcert, sslkey, sslrootcert, sslcrl, requirepeer, fetch_count
+ ALTER SERVER loopback2 OPTIONS (ADD fetch_count '2');
+ ALTER USER MAPPING FOR public SERVER loopback1
+ OPTIONS (DROP user, DROP password);
+ ALTER USER MAPPING FOR public SERVER loopback1
+ OPTIONS (host 'value'); -- ERROR
+ ERROR: invalid option "host"
+ HINT: Valid options in this context are: user, password
+ ALTER FOREIGN TABLE ft1 OPTIONS (nspname 'S 1', relname 'T 1');
+ ALTER FOREIGN TABLE ft2 OPTIONS (nspname 'S 1', relname 'T 1', fetch_count '100');
+ ALTER FOREIGN TABLE ft1 OPTIONS (invalid 'value'); -- ERROR
+ ERROR: invalid option "invalid"
+ HINT: Valid options in this context are: nspname, relname, fetch_count
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count 'a'); -- ERROR
+ ERROR: invalid value for fetch_count: "a"
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count '0'); -- ERROR
+ ERROR: invalid value for fetch_count: "0"
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count '-1'); -- ERROR
+ ERROR: invalid value for fetch_count: "-1"
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (invalid 'value'); -- ERROR
+ ERROR: invalid option "invalid"
+ HINT: Valid options in this context are: colname
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (colname 'C 1');
+ ALTER FOREIGN TABLE ft2 ALTER COLUMN c1 OPTIONS (colname 'C 1');
+ \dew+
+ List of foreign-data wrappers
+ Name | Owner | Handler | Validator | Access privileges | FDW Options | Description
+ -----------+----------------+-------------------+---------------------+-------------------+-------------+-------------
+ pgsql_fdw | pgsql_fdw_user | pgsql_fdw_handler | pgsql_fdw_validator | | |
+ (1 row)
+
+ \des+
+ List of foreign servers
+ Name | Owner | Foreign-data wrapper | Access privileges | Type | Version | FDW Options | Description
+ -----------+----------------+----------------------+-------------------+------+---------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+-------------
+ loopback1 | pgsql_fdw_user | pgsql_fdw | | | | (authtype 'value', service 'value', connect_timeout 'value', dbname 'value', host 'value', hostaddr 'value', port 'value', tty 'value', options 'value', application_name 'value', keepalives 'value', keepalives_idle 'value', keepalives_interval 'value', sslmode 'value', sslcert 'value', sslkey 'value', sslrootcert 'value', sslcrl 'value') |
+ loopback2 | pgsql_fdw_user | pgsql_fdw | | | | (dbname 'contrib_regression', fetch_count '2') |
+ (2 rows)
+
+ \deu+
+ List of user mappings
+ Server | User name | FDW Options
+ -----------+----------------+-------------
+ loopback1 | public |
+ loopback2 | pgsql_fdw_user |
+ (2 rows)
+
+ \det+
+ List of foreign tables
+ Schema | Table | Server | FDW Options | Description
+ --------+-------+-----------+---------------------------------------------------+-------------
+ public | ft1 | loopback2 | (nspname 'S 1', relname 'T 1') |
+ public | ft2 | loopback2 | (nspname 'S 1', relname 'T 1', fetch_count '100') |
+ (2 rows)
+
+ -- ===================================================================
+ -- simple queries
+ -- ===================================================================
+ -- single table, with/without alias
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
+ QUERY PLAN
+ ------------------------------------------------------------------------------------------------------------------------------
+ Limit
+ Output: c1, c2, c3, c4, c5, c6, c7
+ -> Sort
+ Output: c1, c2, c3, c4, c5, c6, c7
+ Sort Key: ft1.c3, ft1.c1
+ -> Foreign Scan on public.ft1
+ Output: c1, c2, c3, c4, c5, c6, c7
+ Remote SQL: DECLARE pgsql_fdw_cursor_0 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (8 rows)
+
+ SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ -----+----+-------+------------------------------+--------------------------+----+------------
+ 101 | 1 | 00101 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970 | 1 | 1
+ 102 | 2 | 00102 | Sat Jan 03 00:00:00 1970 PST | Sat Jan 03 00:00:00 1970 | 2 | 2
+ 103 | 3 | 00103 | Sun Jan 04 00:00:00 1970 PST | Sun Jan 04 00:00:00 1970 | 3 | 3
+ 104 | 4 | 00104 | Mon Jan 05 00:00:00 1970 PST | Mon Jan 05 00:00:00 1970 | 4 | 4
+ 105 | 5 | 00105 | Tue Jan 06 00:00:00 1970 PST | Tue Jan 06 00:00:00 1970 | 5 | 5
+ 106 | 6 | 00106 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6
+ 107 | 7 | 00107 | Thu Jan 08 00:00:00 1970 PST | Thu Jan 08 00:00:00 1970 | 7 | 7
+ 108 | 8 | 00108 | Fri Jan 09 00:00:00 1970 PST | Fri Jan 09 00:00:00 1970 | 8 | 8
+ 109 | 9 | 00109 | Sat Jan 10 00:00:00 1970 PST | Sat Jan 10 00:00:00 1970 | 9 | 9
+ 110 | 0 | 00110 | Sun Jan 11 00:00:00 1970 PST | Sun Jan 11 00:00:00 1970 | 0 | 0
+ (10 rows)
+
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+ QUERY PLAN
+ ------------------------------------------------------------------------------------------------------------------------------
+ Limit
+ Output: c1, c2, c3, c4, c5, c6, c7
+ -> Sort
+ Output: c1, c2, c3, c4, c5, c6, c7
+ Sort Key: t1.c3, t1.c1
+ -> Foreign Scan on public.ft1 t1
+ Output: c1, c2, c3, c4, c5, c6, c7
+ Remote SQL: DECLARE pgsql_fdw_cursor_2 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (8 rows)
+
+ SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ -----+----+-------+------------------------------+--------------------------+----+------------
+ 101 | 1 | 00101 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970 | 1 | 1
+ 102 | 2 | 00102 | Sat Jan 03 00:00:00 1970 PST | Sat Jan 03 00:00:00 1970 | 2 | 2
+ 103 | 3 | 00103 | Sun Jan 04 00:00:00 1970 PST | Sun Jan 04 00:00:00 1970 | 3 | 3
+ 104 | 4 | 00104 | Mon Jan 05 00:00:00 1970 PST | Mon Jan 05 00:00:00 1970 | 4 | 4
+ 105 | 5 | 00105 | Tue Jan 06 00:00:00 1970 PST | Tue Jan 06 00:00:00 1970 | 5 | 5
+ 106 | 6 | 00106 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6
+ 107 | 7 | 00107 | Thu Jan 08 00:00:00 1970 PST | Thu Jan 08 00:00:00 1970 | 7 | 7
+ 108 | 8 | 00108 | Fri Jan 09 00:00:00 1970 PST | Fri Jan 09 00:00:00 1970 | 8 | 8
+ 109 | 9 | 00109 | Sat Jan 10 00:00:00 1970 PST | Sat Jan 10 00:00:00 1970 | 9 | 9
+ 110 | 0 | 00110 | Sun Jan 11 00:00:00 1970 PST | Sun Jan 11 00:00:00 1970 | 0 | 0
+ (10 rows)
+
+ -- with WHERE clause
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
+ QUERY PLAN
+ ------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on public.ft1 t1
+ Output: c1, c2, c3, c4, c5, c6, c7
+ Filter: ((t1.c1 = 101) AND ((t1.c6)::text = '1'::text) AND (t1.c7 = '1'::bpchar))
+ Remote SQL: DECLARE pgsql_fdw_cursor_4 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (4 rows)
+
+ SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ -----+----+-------+------------------------------+--------------------------+----+------------
+ 101 | 1 | 00101 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970 | 1 | 1
+ (1 row)
+
+ -- aggregate
+ SELECT COUNT(*) FROM ft1 t1;
+ count
+ -------
+ 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
+ ----+----+-------+------------------------------+--------------------------+----+------------
+ 1 | 1 | 00001 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970 | 1 | 1
+ 2 | 2 | 00002 | Sat Jan 03 00:00:00 1970 PST | Sat Jan 03 00:00:00 1970 | 2 | 2
+ 3 | 3 | 00003 | Sun Jan 04 00:00:00 1970 PST | Sun Jan 04 00:00:00 1970 | 3 | 3
+ 4 | 4 | 00004 | Mon Jan 05 00:00:00 1970 PST | Mon Jan 05 00:00:00 1970 | 4 | 4
+ 5 | 5 | 00005 | Tue Jan 06 00:00:00 1970 PST | Tue Jan 06 00:00:00 1970 | 5 | 5
+ 6 | 6 | 00006 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6
+ 7 | 7 | 00007 | Thu Jan 08 00:00:00 1970 PST | Thu Jan 08 00:00:00 1970 | 7 | 7
+ 8 | 8 | 00008 | Fri Jan 09 00:00:00 1970 PST | Fri Jan 09 00:00:00 1970 | 8 | 8
+ 9 | 9 | 00009 | Sat Jan 10 00:00:00 1970 PST | Sat Jan 10 00:00:00 1970 | 9 | 9
+ 10 | 0 | 00010 | Sun Jan 11 00:00:00 1970 PST | Sun Jan 11 00:00:00 1970 | 0 | 0
+ (10 rows)
+
+ -- subquery+MAX
+ SELECT * FROM ft1 t1 WHERE t1.c3 = (SELECT MAX(c3) FROM ft2 t2) ORDER BY c1;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ ------+----+-------+------------------------------+--------------------------+----+------------
+ 1000 | 0 | 01000 | Thu Jan 01 00:00:00 1970 PST | Thu Jan 01 00:00:00 1970 | 0 | 0
+ (1 row)
+
+ -- used in CTE
+ WITH t1 AS (SELECT * FROM ft1 WHERE c1 <= 10) SELECT t2.c1, t2.c2, t2.c3, t2.c4 FROM t1, ft2 t2 WHERE t1.c1 = t2.c1 ORDER BY t1.c1;
+ c1 | c2 | c3 | c4
+ ----+----+-------+------------------------------
+ 1 | 1 | 00001 | Fri Jan 02 00:00:00 1970 PST
+ 2 | 2 | 00002 | Sat Jan 03 00:00:00 1970 PST
+ 3 | 3 | 00003 | Sun Jan 04 00:00:00 1970 PST
+ 4 | 4 | 00004 | Mon Jan 05 00:00:00 1970 PST
+ 5 | 5 | 00005 | Tue Jan 06 00:00:00 1970 PST
+ 6 | 6 | 00006 | Wed Jan 07 00:00:00 1970 PST
+ 7 | 7 | 00007 | Thu Jan 08 00:00:00 1970 PST
+ 8 | 8 | 00008 | Fri Jan 09 00:00:00 1970 PST
+ 9 | 9 | 00009 | Sat Jan 10 00:00:00 1970 PST
+ 10 | 0 | 00010 | Sun Jan 11 00:00:00 1970 PST
+ (10 rows)
+
+ -- fixed values
+ SELECT 'fixed', NULL FROM ft1 t1 WHERE c1 = 1;
+ ?column? | ?column?
+ ----------+----------
+ fixed |
+ (1 row)
+
+ -- user-defined operator/function
+ CREATE FUNCTION pgsql_fdw_abs(int) RETURNS int AS $$
+ BEGIN
+ RETURN abs($1);
+ END
+ $$ LANGUAGE plpgsql IMMUTABLE;
+ CREATE OPERATOR === (
+ LEFTARG = int,
+ RIGHTARG = int,
+ PROCEDURE = int4eq,
+ COMMUTATOR = ===,
+ NEGATOR = !==
+ );
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = pgsql_fdw_abs(t1.c2);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = pgsql_fdw_abs(c2))
+ Remote SQL: DECLARE pgsql_fdw_cursor_18 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 === t1.c2;
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 === c2)
+ Remote SQL: DECLARE pgsql_fdw_cursor_19 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = abs(t1.c2);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = abs(c2))
+ Remote SQL: DECLARE pgsql_fdw_cursor_20 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = t1.c2;
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = c2)
+ Remote SQL: DECLARE pgsql_fdw_cursor_21 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ -- ===================================================================
+ -- parameterized queries
+ -- ===================================================================
+ -- simple join
+ PREPARE st1(int, int) AS SELECT t1.c3, t2.c3 FROM ft1 t1, ft2 t2 WHERE t1.c1 = $1 AND t2.c1 = $2;
+ EXPLAIN (COSTS false) EXECUTE st1(1, 2);
+ QUERY PLAN
+ -----------------------------------------------------------------------------------------------------------------------------------------
+ Nested Loop
+ -> Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_22 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, NULL, NULL, NULL, NULL FROM "S 1"."T 1"
+ -> Materialize
+ -> Foreign Scan on ft2 t2
+ Filter: (c1 = 2)
+ Remote SQL: DECLARE pgsql_fdw_cursor_23 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, NULL, NULL, NULL, NULL FROM "S 1"."T 1"
+ (8 rows)
+
+ EXECUTE st1(1, 1);
+ c3 | c3
+ -------+-------
+ 00001 | 00001
+ (1 row)
+
+ EXECUTE st1(101, 101);
+ c3 | c3
+ -------+-------
+ 00101 | 00101
+ (1 row)
+
+ -- subquery using stable function (can't be pushed down)
+ PREPARE st2(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c4) = 6) ORDER BY c1;
+ EXPLAIN (COSTS false) EXECUTE st2(10, 20);
+ QUERY PLAN
+ ---------------------------------------------------------------------------------------------------------------------------------------------------
+ Sort
+ Sort Key: t1.c1
+ -> Hash Join
+ Hash Cond: (t1.c3 = t2.c3)
+ -> Foreign Scan on ft1 t1
+ Filter: (c1 < 20)
+ Remote SQL: DECLARE pgsql_fdw_cursor_28 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ -> Hash
+ -> HashAggregate
+ -> Foreign Scan on ft2 t2
+ Filter: ((c1 > 10) AND (date_part('dow'::text, c4) = 6::double precision))
+ Remote SQL: DECLARE pgsql_fdw_cursor_29 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, c4, NULL, NULL, NULL FROM "S 1"."T 1"
+ (12 rows)
+
+ EXECUTE st2(10, 20);
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ ----+----+-------+------------------------------+--------------------------+----+------------
+ 16 | 6 | 00016 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970 | 6 | 6
+ (1 row)
+
+ EXECUTE st1(101, 101);
+ c3 | c3
+ -------+-------
+ 00101 | 00101
+ (1 row)
+
+ -- subquery using immutable function (can be pushed down)
+ PREPARE st3(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c5) = 6) ORDER BY c1;
+ EXPLAIN (COSTS false) EXECUTE st3(10, 20);
+ QUERY PLAN
+ ---------------------------------------------------------------------------------------------------------------------------------------------------
+ Sort
+ Sort Key: t1.c1
+ -> Hash Join
+ Hash Cond: (t1.c3 = t2.c3)
+ -> Foreign Scan on ft1 t1
+ Filter: (c1 < 20)
+ Remote SQL: DECLARE pgsql_fdw_cursor_34 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ -> Hash
+ -> HashAggregate
+ -> Foreign Scan on ft2 t2
+ Filter: ((c1 > 10) AND (date_part('dow'::text, c5) = 6::double precision))
+ Remote SQL: DECLARE pgsql_fdw_cursor_35 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, NULL, c5, NULL, NULL FROM "S 1"."T 1"
+ (12 rows)
+
+ EXECUTE st3(10, 20);
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ ----+----+-------+------------------------------+--------------------------+----+------------
+ 16 | 6 | 00016 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970 | 6 | 6
+ (1 row)
+
+ EXECUTE st3(20, 30);
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ ----+----+-------+------------------------------+--------------------------+----+------------
+ 23 | 3 | 00023 | Sat Jan 24 00:00:00 1970 PST | Sat Jan 24 00:00:00 1970 | 3 | 3
+ (1 row)
+
+ -- custom plan should be chosen
+ PREPARE st4(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 = $1;
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_40 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_41 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_42 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_43 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_44 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_46 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ -- cleanup
+ DEALLOCATE st1;
+ DEALLOCATE st2;
+ DEALLOCATE st3;
+ DEALLOCATE st4;
+ -- ===================================================================
+ -- connection management
+ -- ===================================================================
+ SELECT srvname, usename FROM pgsql_fdw_connections;
+ srvname | usename
+ -----------+----------------
+ loopback2 | pgsql_fdw_user
+ (1 row)
+
+ SELECT pgsql_fdw_disconnect(srvid, usesysid) FROM pgsql_fdw_get_connections();
+ pgsql_fdw_disconnect
+ ----------------------
+ OK
+ (1 row)
+
+ SELECT srvname, usename FROM pgsql_fdw_connections;
+ srvname | usename
+ ---------+---------
+ (0 rows)
+
+ -- ===================================================================
+ -- cleanup
+ -- ===================================================================
+ DROP OPERATOR === (int, int) CASCADE;
+ DROP OPERATOR !== (int, int) CASCADE;
+ DROP FUNCTION pgsql_fdw_abs(int);
+ DROP SCHEMA "S 1" CASCADE;
+ NOTICE: drop cascades to 2 other objects
+ DETAIL: drop cascades to table "S 1"."T 1"
+ drop cascades to table "S 1"."T 2"
+ DROP EXTENSION pgsql_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 server loopback2
+ drop cascades to user mapping for pgsql_fdw_user
+ drop cascades to foreign table ft1
+ drop cascades to foreign table ft2
+ \c
+ DROP ROLE pgsql_fdw_user;
diff --git a/contrib/pgsql_fdw/option.c b/contrib/pgsql_fdw/option.c
index ...65e3c54 .
*** a/contrib/pgsql_fdw/option.c
--- b/contrib/pgsql_fdw/option.c
***************
*** 0 ****
--- 1,246 ----
+ /*-------------------------------------------------------------------------
+ *
+ * option.c
+ * FDW option handling
+ *
+ * Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/option.c
+ *
+ *-------------------------------------------------------------------------
+ */
+ #include "postgres.h"
+
+ #include "access/reloptions.h"
+ #include "catalog/pg_foreign_data_wrapper.h"
+ #include "catalog/pg_foreign_server.h"
+ #include "catalog/pg_foreign_table.h"
+ #include "catalog/pg_user_mapping.h"
+ #include "fmgr.h"
+ #include "foreign/foreign.h"
+ #include "lib/stringinfo.h"
+ #include "miscadmin.h"
+
+ #include "pgsql_fdw.h"
+
+ /*
+ * SQL functions
+ */
+ extern Datum pgsql_fdw_validator(PG_FUNCTION_ARGS);
+ PG_FUNCTION_INFO_V1(pgsql_fdw_validator);
+
+ /*
+ * Describes the valid options for objects that use this wrapper.
+ */
+ typedef struct PgsqlFdwOption
+ {
+ const char *optname;
+ Oid optcontext; /* Oid of catalog in which options may appear */
+ bool is_libpq_opt; /* true if it's used in libpq */
+ } PgsqlFdwOption;
+
+ /*
+ * Valid options for pgsql_fdw.
+ */
+ static PgsqlFdwOption valid_options[] = {
+
+ /*
+ * Options for libpq connection.
+ * Note: This list should be updated along with PQconninfoOptions in
+ * interfaces/libpq/fe-connect.c, so the order is kept as is.
+ *
+ * Some useless libpq connection options are not accepted by pgsql_fdw:
+ * client_encoding: set to local database encoding automatically
+ * fallback_application_name: fixed to "pgsql_fdw"
+ * replication: pgsql_fdw never be replication client
+ */
+ {"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},
+ #ifdef NOT_USED
+ {"client_encoding", ForeignServerRelationId, true},
+ #endif
+ {"tty", ForeignServerRelationId, true},
+ {"options", ForeignServerRelationId, true},
+ {"application_name", ForeignServerRelationId, true},
+ #ifdef NOT_USED
+ {"fallback_application_name", ForeignServerRelationId, true},
+ #endif
+ {"keepalives", ForeignServerRelationId, true},
+ {"keepalives_idle", ForeignServerRelationId, true},
+ {"keepalives_interval", ForeignServerRelationId, true},
+ {"keepalives_count", ForeignServerRelationId, true},
+ #ifdef USE_SSL
+ {"requiressl", ForeignServerRelationId, true},
+ #endif
+ {"sslmode", ForeignServerRelationId, true},
+ {"sslcert", ForeignServerRelationId, true},
+ {"sslkey", ForeignServerRelationId, true},
+ {"sslrootcert", ForeignServerRelationId, true},
+ {"sslcrl", ForeignServerRelationId, true},
+ {"requirepeer", ForeignServerRelationId, true},
+ #if defined(KRB5) || defined(ENABLE_GSS) || defined(ENABLE_SSPI)
+ {"krbsrvname", ForeignServerRelationId, true},
+ #endif
+ #if defined(ENABLE_GSS) && defined(ENABLE_SSPI)
+ {"gsslib", ForeignServerRelationId, true},
+ #endif
+ #ifdef NOT_USED
+ {"replication", ForeignServerRelationId, true},
+ #endif
+
+ /*
+ * Options for translation of object names.
+ */
+ {"nspname", ForeignTableRelationId, false},
+ {"relname", ForeignTableRelationId, false},
+ {"colname", AttributeRelationId, false},
+
+ /*
+ * Options for cursor behavior.
+ * These options can be overridden by finer-grained objects.
+ */
+ {"fetch_count", ForeignTableRelationId, false},
+ {"fetch_count", ForeignServerRelationId, false},
+
+ /* Terminating entry --- MUST BE LAST */
+ {NULL, InvalidOid, false}
+ };
+
+ /*
+ * Helper functions
+ */
+ static bool is_valid_option(const char *optname, Oid context);
+
+ /*
+ * Validate the generic options given to a FOREIGN DATA WRAPPER, SERVER,
+ * USER MAPPING or FOREIGN TABLE that uses pgsql_fdw.
+ *
+ * Raise an ERROR if the option or its value is considered invalid.
+ */
+ Datum
+ pgsql_fdw_validator(PG_FUNCTION_ARGS)
+ {
+ List *options_list = untransformRelOptions(PG_GETARG_DATUM(0));
+ Oid catalog = PG_GETARG_OID(1);
+ ListCell *cell;
+
+ /*
+ * Check that only options supported by pgsql_fdw, and allowed for the
+ * current object type, are given.
+ */
+ foreach(cell, options_list)
+ {
+ DefElem *def = (DefElem *) lfirst(cell);
+
+ if (!is_valid_option(def->defname, catalog))
+ {
+ PgsqlFdwOption *opt;
+ StringInfoData buf;
+
+ /*
+ * Unknown option specified, complain about it. Provide a hint
+ * with list of valid options for the object.
+ */
+ initStringInfo(&buf);
+ for (opt = valid_options; opt->optname; opt++)
+ {
+ if (catalog == opt->optcontext)
+ appendStringInfo(&buf, "%s%s", (buf.len > 0) ? ", " : "",
+ opt->optname);
+ }
+
+ ereport(ERROR,
+ (errcode(ERRCODE_FDW_INVALID_OPTION_NAME),
+ errmsg("invalid option \"%s\"", def->defname),
+ errhint("Valid options in this context are: %s",
+ buf.data)));
+ }
+
+ /* fetch_count be positive digit number. */
+ if (strcmp(def->defname, "fetch_count") == 0)
+ {
+ long value;
+ char *p = NULL;
+
+ value = strtol(strVal(def->arg), &p, 10);
+ if (*p != '\0' || value < 1)
+ ereport(ERROR,
+ (errcode(ERRCODE_FDW_INVALID_ATTRIBUTE_VALUE),
+ errmsg("invalid value for %s: \"%s\"",
+ def->defname, strVal(def->arg))));
+ }
+ }
+
+ /*
+ * We don't care option-specific limitation here; they will be validated at
+ * the execution time.
+ */
+
+ PG_RETURN_VOID();
+ }
+
+ /*
+ * Check whether the given option is one of the valid pgsql_fdw options.
+ * context is the Oid of the catalog holding the object the option is for.
+ */
+ static bool
+ is_valid_option(const char *optname, Oid context)
+ {
+ PgsqlFdwOption *opt;
+
+ for (opt = valid_options; opt->optname; opt++)
+ {
+ if (context == opt->optcontext && strcmp(opt->optname, optname) == 0)
+ return true;
+ }
+ return false;
+ }
+
+ /*
+ * Check whether the given option is one of the valid libpq options.
+ * context is the Oid of the catalog holding the object the option is for.
+ */
+ static bool
+ is_libpq_option(const char *optname)
+ {
+ PgsqlFdwOption *opt;
+
+ for (opt = valid_options; opt->optname; opt++)
+ {
+ if (strcmp(opt->optname, optname) == 0 && opt->is_libpq_opt)
+ return true;
+ }
+ return false;
+ }
+
+ /*
+ * Generate key-value arrays which includes only libpq options from the list
+ * which contains any kind of options.
+ */
+ int
+ ExtractConnectionOptions(List *defelems, const char **keywords, const char **values)
+ {
+ ListCell *lc;
+ int i;
+
+ i = 0;
+ foreach(lc, defelems)
+ {
+ DefElem *d = (DefElem *) lfirst(lc);
+ if (is_libpq_option(d->defname))
+ {
+ keywords[i] = d->defname;
+ values[i] = strVal(d->arg);
+ i++;
+ }
+ }
+ return i;
+ }
diff --git a/contrib/pgsql_fdw/pgsql_fdw--1.0.sql b/contrib/pgsql_fdw/pgsql_fdw--1.0.sql
index ...b0ea2b2 .
*** a/contrib/pgsql_fdw/pgsql_fdw--1.0.sql
--- b/contrib/pgsql_fdw/pgsql_fdw--1.0.sql
***************
*** 0 ****
--- 1,39 ----
+ /* contrib/pgsql_fdw/pgsql_fdw--1.0.sql */
+
+ -- complain if script is sourced in psql, rather than via CREATE EXTENSION
+ \echo Use "CREATE EXTENSION pgsql_fdw" to load this file. \quit
+
+ CREATE FUNCTION pgsql_fdw_handler()
+ RETURNS fdw_handler
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C STRICT;
+
+ CREATE FUNCTION pgsql_fdw_validator(text[], oid)
+ RETURNS void
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C STRICT;
+
+ CREATE FOREIGN DATA WRAPPER pgsql_fdw
+ HANDLER pgsql_fdw_handler
+ VALIDATOR pgsql_fdw_validator;
+
+ /* connection management functions and view */
+ CREATE FUNCTION pgsql_fdw_get_connections(out srvid oid, out usesysid oid)
+ RETURNS SETOF record
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C STRICT;
+
+ CREATE FUNCTION pgsql_fdw_disconnect(oid, oid)
+ RETURNS text
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C STRICT;
+
+ CREATE VIEW pgsql_fdw_connections AS
+ SELECT c.srvid srvid,
+ s.srvname srvname,
+ c.usesysid usesysid,
+ pg_get_userbyid(c.usesysid) usename
+ FROM pgsql_fdw_get_connections() c
+ JOIN pg_catalog.pg_foreign_server s ON (s.oid = c.srvid);
+ GRANT SELECT ON pgsql_fdw_connections TO public;
+
diff --git a/contrib/pgsql_fdw/pgsql_fdw.c b/contrib/pgsql_fdw/pgsql_fdw.c
index ...4e2ce84 .
*** a/contrib/pgsql_fdw/pgsql_fdw.c
--- b/contrib/pgsql_fdw/pgsql_fdw.c
***************
*** 0 ****
--- 1,885 ----
+ /*-------------------------------------------------------------------------
+ *
+ * pgsql_fdw.c
+ * foreign-data wrapper for remote PostgreSQL servers.
+ *
+ * Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/pgsql_fdw.c
+ *
+ *-------------------------------------------------------------------------
+ */
+ #include "postgres.h"
+ #include "fmgr.h"
+
+ #include "catalog/pg_foreign_server.h"
+ #include "catalog/pg_foreign_table.h"
+ #include "commands/explain.h"
+ #include "foreign/fdwapi.h"
+ #include "funcapi.h"
+ #include "miscadmin.h"
+ #include "nodes/nodeFuncs.h"
+ #include "optimizer/cost.h"
+ #include "optimizer/pathnode.h"
+ #include "utils/lsyscache.h"
+ #include "utils/memutils.h"
+ #include "utils/rel.h"
+
+ #include "pgsql_fdw.h"
+ #include "connection.h"
+
+ PG_MODULE_MAGIC;
+
+ /*
+ * Default fetch count for cursor. This can be overridden by fetch_count FDW
+ * option.
+ */
+ #define DEFAULT_FETCH_COUNT 10000
+
+ /*
+ * Cost to establish a connection.
+ * XXX: should be configurable per server?
+ */
+ #define CONNECTION_COSTS 100.0
+
+ /*
+ * Cost to transfer 1 byte from remote server.
+ * XXX: should be configurable per server?
+ */
+ #define TRANSFER_COSTS_PER_BYTE 0.001
+
+ /*
+ * Cursors which are used together in a local query require different name, so
+ * we use simple incremental name for that purpose. We don't care wrap around
+ * of cursor_id because it's hard to imagine that 2^32 cursors are used in a
+ * query.
+ */
+ #define CURSOR_NAME_FORMAT "pgsql_fdw_cursor_%u"
+ static uint32 cursor_id = 0;
+
+ /*
+ * Index of FDW-private items stored in FdwPlan.
+ */
+ enum FdwPrivateIndex {
+ FdwPrivateSelectSql,
+ FdwPrivateDeclareSql,
+ FdwPrivateFetchSql,
+ FdwPrivateResetSql,
+ FdwPrivateCloseSql,
+
+ /* # of elements stored in the list fdw_private */
+ FdwPrivateNum,
+ };
+
+ /*
+ * Describes an execution state of a foreign scan against a foreign table
+ * using pgsql_fdw.
+ */
+ typedef struct PgsqlFdwExecutionState
+ {
+ MemoryContext scan_cxt; /* context for per-scan lifespan data */
+
+ FdwPlan *fdwplan; /* FDW-specific planning information */
+ PGconn *conn; /* connection for the scan */
+
+ Oid *param_types; /* type array of external parameter */
+ const char **param_values; /* value array of external parameter */
+
+ int attnum; /* # of non-dropped attribute */
+ char **col_values; /* column value buffer */
+ AttInMetadata *attinmeta; /* attribute metadata */
+
+ Tuplestorestate *tuples; /* result of the scan */
+ } PgsqlFdwExecutionState;
+
+ /*
+ * SQL functions
+ */
+ extern Datum pgsql_fdw_handler(PG_FUNCTION_ARGS);
+ PG_FUNCTION_INFO_V1(pgsql_fdw_handler);
+
+ /*
+ * FDW callback routines
+ */
+ static FdwPlan *pgsqlPlanForeignScan(Oid foreigntableid,
+ PlannerInfo *root,
+ RelOptInfo *baserel);
+ static void pgsqlExplainForeignScan(ForeignScanState *node, ExplainState *es);
+ static void pgsqlBeginForeignScan(ForeignScanState *node, int eflags);
+ static TupleTableSlot *pgsqlIterateForeignScan(ForeignScanState *node);
+ static void pgsqlReScanForeignScan(ForeignScanState *node);
+ static void pgsqlEndForeignScan(ForeignScanState *node);
+
+ /*
+ * Helper functions
+ */
+ static void estimate_costs(PlannerInfo *root,
+ RelOptInfo *baserel,
+ const char *sql,
+ Oid serverid,
+ Cost *startup_cost,
+ Cost *total_cost);
+ static void execute_query(ForeignScanState *node);
+ static PGresult *fetch_result(ForeignScanState *node);
+ static void store_result(ForeignScanState *node, PGresult *res);
+ static bool contain_ext_param(Node *clause);
+ static bool contain_ext_param_walker(Node *node, void *context);
+
+ /* Exported functions, but not written in pgsql_fdw.h. */
+ void _PG_init(void);
+ void _PG_fini(void);
+
+ /*
+ * Module-specific initialization.
+ */
+ void
+ _PG_init(void)
+ {
+ }
+
+ /*
+ * Module-specific clean up.
+ */
+ void
+ _PG_fini(void)
+ {
+ }
+
+ /*
+ * The content of FdwRoutine of pgsql_fdw is never changed, so we use one
+ * static object for all scan.
+ */
+ static FdwRoutine fdwroutine = {
+
+ /* Node type */
+ T_FdwRoutine,
+
+ /* Required handler functions. */
+ pgsqlPlanForeignScan,
+ pgsqlExplainForeignScan,
+ pgsqlBeginForeignScan,
+ pgsqlIterateForeignScan,
+ pgsqlReScanForeignScan,
+ pgsqlEndForeignScan,
+
+ /* Optional handler functions. */
+ };
+
+ /*
+ * Foreign-data wrapper handler function: return a struct with pointers
+ * to my callback routines.
+ */
+ Datum
+ pgsql_fdw_handler(PG_FUNCTION_ARGS)
+ {
+ PG_RETURN_POINTER(&fdwroutine);
+ }
+
+ /*
+ * pgsqlPlanForeignScan
+ * Create a FdwPlan for a scan on the foreign table
+ */
+ static FdwPlan *
+ pgsqlPlanForeignScan(Oid foreigntableid,
+ PlannerInfo *root,
+ RelOptInfo *baserel)
+ {
+ char name[128]; /* must be larger than format + 10 */
+ StringInfoData cursor;
+ const char *fetch_count_str;
+ int fetch_count = DEFAULT_FETCH_COUNT;
+ char *sql;
+ FdwPlan *fdwplan;
+ List *fdw_private = NIL;
+ ForeignTable *table;
+ ForeignServer *server;
+
+ /* Construct FdwPlan with cost estimates */
+ fdwplan = makeNode(FdwPlan);
+ sql = deparseSql(foreigntableid, root, baserel);
+ table = GetForeignTable(foreigntableid);
+ server = GetForeignServer(table->serverid);
+ estimate_costs(root, baserel, sql, server->serverid,
+ &fdwplan->startup_cost, &fdwplan->total_cost);
+
+ /*
+ * Store plain SELECT statement in private area of FdwPlan. This will be
+ * used for executing remote query and explaining scan.
+ */
+ fdw_private = list_make1(makeString(sql));
+
+ /* Use specified fetch_count instead of default value, if any. */
+ fetch_count_str = GetFdwOptionValue(InvalidOid, InvalidOid, foreigntableid,
+ InvalidAttrNumber, "fetch_count");
+ if (fetch_count_str != NULL)
+ fetch_count = strtol(fetch_count_str, NULL, 10);
+ elog(DEBUG1, "relid=%u fetch_count=%d", foreigntableid, fetch_count);
+
+ /*
+ * We store some more information in FdwPlan to pass them beyond the
+ * boundary between planner and executor. Finally FdwPlan using cursor
+ * would hold items below:
+ *
+ * 1) plain SELECT statement (already added above)
+ * 2) SQL statement used to declare cursor
+ * 3) SQL statement used to fetch rows from cursor
+ * 4) SQL statement used to reset cursor
+ * 5) SQL statement used to close cursor
+ *
+ * These items are indexed with the enum FdwPrivateIndex, so an item
+ * can be accessed directly via list_nth(). For example of FETCH
+ * statement:
+ * list_nth(fdw_private, FdwPrivateFetchSql)
+ */
+
+ /* Construct cursor name from sequential value */
+ sprintf(name, CURSOR_NAME_FORMAT, cursor_id++);
+
+ /* Construct statement to declare cursor */
+ initStringInfo(&cursor);
+ appendStringInfo(&cursor, "DECLARE %s SCROLL CURSOR FOR %s", name, sql);
+ fdw_private = lappend(fdw_private, makeString(cursor.data));
+
+ /* Construct statement to fetch rows from cursor */
+ initStringInfo(&cursor);
+ appendStringInfo(&cursor, "FETCH %d FROM %s", fetch_count, name);
+ fdw_private = lappend(fdw_private, makeString(cursor.data));
+
+ /* Construct statement to reset cursor */
+ initStringInfo(&cursor);
+ appendStringInfo(&cursor, "MOVE ABSOLUTE 0 FROM %s", name);
+ fdw_private = lappend(fdw_private, makeString(cursor.data));
+
+ /* Construct statement to close cursor */
+ initStringInfo(&cursor);
+ appendStringInfo(&cursor, "CLOSE %s", name);
+ fdw_private = lappend(fdw_private, makeString(cursor.data));
+
+ /* Store FDW private information into FdwPlan */
+ fdwplan->fdw_private = fdw_private;
+
+ return fdwplan;
+ }
+
+ /*
+ * pgsqlExplainForeignScan
+ * Produce extra output for EXPLAIN
+ */
+ static void
+ pgsqlExplainForeignScan(ForeignScanState *node, ExplainState *es)
+ {
+ FdwPlan *fdwplan;
+ char *sql;
+
+ fdwplan = ((ForeignScan *) node->ss.ps.plan)->fdwplan;
+ sql = strVal(list_nth(fdwplan->fdw_private, FdwPrivateDeclareSql));
+ ExplainPropertyText("Remote SQL", sql, es);
+ }
+
+ /*
+ * pgsqlBeginForeignScan
+ * Initiate access to a foreign PostgreSQL table.
+ */
+ static void
+ pgsqlBeginForeignScan(ForeignScanState *node, int eflags)
+ {
+ PgsqlFdwExecutionState *festate;
+ PGconn *conn;
+ Oid relid;
+ ForeignTable *table;
+ ForeignServer *server;
+ UserMapping *user;
+ TupleTableSlot *slot = node->ss.ss_ScanTupleSlot;
+
+ /*
+ * Do nothing in EXPLAIN (no ANALYZE) case. node->fdw_state stays NULL.
+ */
+ if (eflags & EXEC_FLAG_EXPLAIN_ONLY)
+ return;
+
+ /*
+ * Save state in node->fdw_state.
+ */
+ festate = (PgsqlFdwExecutionState *) palloc(sizeof(PgsqlFdwExecutionState));
+ festate->fdwplan = ((ForeignScan *) node->ss.ps.plan)->fdwplan;
+
+ /*
+ * Create context for per-scan tuplestore.
+ */
+ festate->scan_cxt = AllocSetContextCreate(MessageContext,
+ "pgsql_fdw",
+ ALLOCSET_DEFAULT_MINSIZE,
+ ALLOCSET_DEFAULT_INITSIZE,
+ ALLOCSET_DEFAULT_MAXSIZE);
+
+ /*
+ * Get connection to the foreign server. Connection manager would
+ * establish new connection if necessary.
+ */
+ relid = RelationGetRelid(node->ss.ss_currentRelation);
+ table = GetForeignTable(relid);
+ server = GetForeignServer(table->serverid);
+ user = GetUserMapping(GetOuterUserId(), server->serverid);
+ conn = GetConnection(server, user, true);
+ festate->conn = conn;
+
+ /* Result will be filled in first Iterate call. */
+ festate->tuples = NULL;
+
+ /* Allocate buffers for column values. */
+ {
+ TupleDesc tupdesc = slot->tts_tupleDescriptor;
+ festate->col_values = palloc(sizeof(char *) * tupdesc->natts);
+ festate->attinmeta = TupleDescGetAttInMetadata(tupdesc);
+ }
+
+ /* Allocate buffers for query parameters. */
+ {
+ ParamListInfo params = node->ss.ps.state->es_param_list_info;
+ int numParams = params ? params->numParams : 0;
+
+ if (numParams > 0)
+ {
+ festate->param_types = palloc0(sizeof(Oid) * numParams);
+ festate->param_values = palloc0(sizeof(char *) * numParams);
+ }
+ else
+ {
+ festate->param_types = NULL;
+ festate->param_values = NULL;
+ }
+ }
+
+
+ /* Store FDW-specific state into ForeignScanState */
+ node->fdw_state = (void *) festate;
+
+ return;
+ }
+
+ /*
+ * pgsqlIterateForeignScan
+ * Retrieve next row from the result set, or clear tuple slot to indicate
+ * EOF.
+ *
+ * Note that using per-query context when retrieving tuples from
+ * tuplestore to ensure that returned tuples can survive until next
+ * iteration because the tuple is released implicitly via ExecClearTuple.
+ * Retrieving a tuple from tuplestore in CurrentMemoryContext (it's a
+ * per-tuple context), ExecClearTuple will free dangling pointer.
+ */
+ static TupleTableSlot *
+ pgsqlIterateForeignScan(ForeignScanState *node)
+ {
+ PgsqlFdwExecutionState *festate;
+ TupleTableSlot *slot = node->ss.ss_ScanTupleSlot;
+ PGresult *res;
+ MemoryContext oldcontext = CurrentMemoryContext;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+
+
+ /*
+ * If this is the first call after Begin, we need to execute remote query.
+ * If the query needs cursor, we declare a cursor at first call and fetch
+ * from it in later calls.
+ */
+ if (festate->tuples == NULL)
+ execute_query(node);
+
+ /*
+ * If enough tuples are left in tuplestore, just return next tuple from it.
+ */
+ MemoryContextSwitchTo(node->ss.ps.state->es_query_cxt);
+ if (tuplestore_gettupleslot(festate->tuples, true, false, slot))
+ {
+ MemoryContextSwitchTo(oldcontext);
+ return slot;
+ }
+ MemoryContextSwitchTo(oldcontext);
+
+ /*
+ * Here we need to clear partial result and fetch next bunch of tuples from
+ * from the cursor for the scan. If the fetch returns no tuple, the scan
+ * has reached the end.
+ */
+ res = fetch_result(node);
+ PG_TRY();
+ {
+ store_result(node, res);
+ PQclear(res);
+ res = NULL;
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+
+ /*
+ * If we got more tuples from the server cursor, return next tuple from
+ * tuplestore.
+ */
+ MemoryContextSwitchTo(node->ss.ps.state->es_query_cxt);
+ if (tuplestore_gettupleslot(festate->tuples, true, false, slot))
+ {
+ MemoryContextSwitchTo(oldcontext);
+ return slot;
+ }
+ MemoryContextSwitchTo(oldcontext);
+
+ /* We don't have any result even in remote server cursor. */
+ ExecClearTuple(slot);
+ return slot;
+ }
+
+ /*
+ * pgsqlReScanForeignScan
+ * - Restart this scan by resetting fetch location.
+ */
+ static void
+ pgsqlReScanForeignScan(ForeignScanState *node)
+ {
+ List *fdw_private;
+ char *sql;
+ PGconn *conn;
+ PGresult *res;
+ PgsqlFdwExecutionState *festate;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+
+ /* If we have not opened cursor yet, nothing to do. */
+ if (festate->tuples == NULL)
+ return;
+
+ /* Discard fetch results if any. */
+ tuplestore_clear(festate->tuples);
+
+ /* Reset cursor */
+ fdw_private = festate->fdwplan->fdw_private;
+ conn = festate->conn;
+ sql = strVal(list_nth(fdw_private, FdwPrivateResetSql));
+ res = PQexec(conn, sql);
+ PG_TRY();
+ {
+ if (PQresultStatus(res) != PGRES_COMMAND_OK)
+ {
+ ereport(ERROR,
+ (errmsg("could not rewind cursor"),
+ errdetail("%s", PQerrorMessage(conn)),
+ errhint("%s", sql)));
+ }
+ PQclear(res);
+ res = NULL;
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+ }
+
+ /*
+ * pgsqlEndForeignScan
+ * Finish scanning foreign table and dispose objects used for this scan
+ */
+ static void
+ pgsqlEndForeignScan(ForeignScanState *node)
+ {
+ List *fdw_private;
+ char *sql;
+ PGconn *conn;
+ PGresult *res;
+ PgsqlFdwExecutionState *festate;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+
+ /* if festate is NULL, we are in EXPLAIN; nothing to do */
+ if (festate == NULL)
+ return;
+
+ /* If we have not opened cursor yet, nothing to do. */
+ if (festate->tuples == NULL)
+ return;
+
+ /* Discard fetch results */
+ tuplestore_end(festate->tuples);
+ festate->tuples = NULL;
+
+ /* Close cursor */
+ fdw_private = festate->fdwplan->fdw_private;
+ conn = festate->conn;
+ sql = strVal(list_nth(fdw_private, FdwPrivateCloseSql));
+ res = PQexec(conn, sql);
+ PG_TRY();
+ {
+ if (PQresultStatus(res) != PGRES_COMMAND_OK)
+ {
+ ereport(ERROR,
+ (errmsg("could not close cursor"),
+ errdetail("%s", PQerrorMessage(conn)),
+ errhint("%s", sql)));
+ }
+ PQclear(res);
+ res = NULL;
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+
+ ReleaseConnection(festate->conn);
+
+ MemoryContextDelete(festate->scan_cxt);
+ festate->scan_cxt = NULL;
+ }
+
+ /*
+ * Estimate costs of scanning a foreign table.
+ */
+ static void
+ estimate_costs(PlannerInfo *root, RelOptInfo *baserel,
+ const char *sql, Oid serverid,
+ Cost *startup_cost, Cost *total_cost)
+ {
+ ListCell *lc;
+ ForeignServer *server;
+ UserMapping *user;
+ PGconn *conn = NULL;
+ PGresult *res = NULL;
+ StringInfoData buf;
+ char *plan;
+ char *p;
+ int n;
+
+ /*
+ * If the baserestrictinfo contains any Param node with paramkind
+ * PARAM_EXTERNAL, we need result of EXPLAIN for EXECUTE statement, not for
+ * simple SELECT statement. However, we can't get actual parameter values
+ * here, so we use fixed and large costs as second best so that planner
+ * tend to choose custom plan.
+ *
+ * See comments in plancache.c for details of custom plan.
+ */
+ foreach(lc, baserel->baserestrictinfo)
+ {
+ RestrictInfo *rs = (RestrictInfo *) lfirst(lc);
+ if (contain_ext_param((Node *) rs->clause))
+ {
+ *startup_cost = CONNECTION_COSTS;
+ *total_cost = 10000; /* groundless large costs */
+
+ return;
+ }
+ }
+
+ /*
+ * Get connection to the foreign server. Connection manager would
+ * establish new connection if necessary.
+ */
+ server = GetForeignServer(serverid);
+ user = GetUserMapping(GetOuterUserId(), server->serverid);
+ conn = GetConnection(server, user, false);
+ initStringInfo(&buf);
+ appendStringInfo(&buf, "EXPLAIN %s", sql);
+
+ PG_TRY();
+ {
+ res = PQexec(conn, buf.data);
+ if (PQresultStatus(res) != PGRES_TUPLES_OK || PQntuples(res) == 0)
+ {
+ char *msg;
+
+ msg = pstrdup(PQerrorMessage(conn));
+ ereport(ERROR,
+ (errmsg("could not execute EXPLAIN for cost estimation"),
+ errdetail("%s", msg),
+ errhint("%s", sql)));
+ }
+
+ /*
+ * Find estimation portion from top plan node. Here we search opening
+ * parentheses from the end of the line to avoid finding unexpected
+ * parentheses.
+ */
+ plan = PQgetvalue(res, 0, 0);
+ p = strrchr(plan, '(');
+ if (p == NULL)
+ elog(ERROR, "wrong EXPLAIN output: %s", plan);
+ n = sscanf(p,
+ "(cost=%lf..%lf rows=%lf width=%d)",
+ startup_cost, total_cost, &baserel->rows, &baserel->width);
+ if (n != 4)
+ elog(ERROR, "could not get estimation from EXPLAIN output");
+
+ PQclear(res);
+ res = NULL;
+ ReleaseConnection(conn);
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+
+ /*
+ * TODO Selectivity of quals which are NOT pushed down should be also
+ * considered.
+ */
+
+ /* add cost to establish connection. */
+ *startup_cost += CONNECTION_COSTS;
+ *total_cost += CONNECTION_COSTS;
+
+ /* add cost to transfer result. */
+ *total_cost += TRANSFER_COSTS_PER_BYTE * baserel->width * baserel->tuples;
+ *total_cost += cpu_tuple_cost * baserel->tuples;
+ }
+
+ /*
+ * Execute remote query with current parameters.
+ */
+ static void
+ execute_query(ForeignScanState *node)
+ {
+ FdwPlan *fdwplan;
+ PgsqlFdwExecutionState *festate;
+ ParamListInfo params = node->ss.ps.state->es_param_list_info;
+ int numParams = params ? params->numParams : 0;
+ Oid *types = NULL;
+ const char **values = NULL;
+ char *sql;
+ PGconn *conn;
+ PGresult *res;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+ types = festate->param_types;
+ values = festate->param_values;
+
+ /*
+ * Construct parameter array in text format. We don't release memory for
+ * the arrays explicitly, because the memory usage would not be very large,
+ * and anyway they will be released in context cleanup.
+ */
+ if (numParams > 0)
+ {
+ int i;
+
+ 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;
+
+ getTypeOutputInfo(types[i], &out_func_oid, &isvarlena);
+ fmgr_info(out_func_oid, &func);
+ values[i] = OutputFunctionCall(&func, params->params[i].value);
+ }
+ }
+ }
+
+ /*
+ * Execute remote query with parameters.
+ */
+ conn = festate->conn;
+ fdwplan = ((ForeignScan *) node->ss.ps.plan)->fdwplan;
+ sql = strVal(list_nth(fdwplan->fdw_private, FdwPrivateDeclareSql));
+ res = PQexecParams(conn, sql, numParams, types, values, NULL, NULL, 0);
+ PG_TRY();
+ {
+ /*
+ * If the query has failed, reporting details is enough here.
+ * Connection(s) which are used by this query (at least used by
+ * pgsql_fdw) will be cleaned up by the foreign connection manager.
+ */
+ if (PQresultStatus(res) != PGRES_COMMAND_OK)
+ {
+ ereport(ERROR,
+ (errmsg("could not declare cursor"),
+ errdetail("%s", PQerrorMessage(conn)),
+ errhint("%s", sql)));
+ }
+
+ /* Discard result of CURSOR statement and fetch first bunch. */
+ PQclear(res);
+ res = fetch_result(node);
+
+ /*
+ * Store the result of the query into tuplestore.
+ * We must release PGresult here to avoid memory leak.
+ */
+ store_result(node, res);
+ PQclear(res);
+ res = NULL;
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+ }
+
+ /*
+ * Fetch next partial result from remote server.
+ */
+ static PGresult *
+ fetch_result(ForeignScanState *node)
+ {
+ PgsqlFdwExecutionState *festate;
+ List *fdw_private;
+ char *sql;
+ PGconn *conn;
+ PGresult *res;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+
+ /* retrieve information for fetching result. */
+ fdw_private = festate->fdwplan->fdw_private;
+ sql = strVal(list_nth(fdw_private, FdwPrivateFetchSql));
+ conn = festate->conn;
+ res = PQexec(conn, sql);
+ if (PQresultStatus(res) != PGRES_TUPLES_OK)
+ {
+ ereport(ERROR,
+ (errmsg("could not fetch rows from foreign server"),
+ errdetail("%s", PQerrorMessage(conn)),
+ errhint("%s", sql)));
+ }
+
+ return res;
+ }
+
+ /*
+ * Create tuples from PGresult and store them into tuplestore.
+ */
+ static void
+ store_result(ForeignScanState *node, PGresult *res)
+ {
+ int rows;
+ int row;
+ int i;
+ int nfields;
+ int attnum; /* number of non-dropped columns */
+ Form_pg_attribute *attrs;
+ TupleTableSlot *slot = node->ss.ss_ScanTupleSlot;
+ TupleDesc tupdesc = slot->tts_tupleDescriptor;
+ PgsqlFdwExecutionState *festate;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+ rows = PQntuples(res);
+ nfields = PQnfields(res);
+ attrs = tupdesc->attrs;
+
+ /* First, ensure that the tuplestore is empty. */
+ if (festate->tuples == NULL)
+ {
+ MemoryContext oldcontext = CurrentMemoryContext;
+
+ /*
+ * Create tuplestore to store result of the query in per-query context.
+ * Note that we use this memory context to avoid memory leak in error
+ * cases.
+ */
+ MemoryContextSwitchTo(festate->scan_cxt);
+ festate->tuples = tuplestore_begin_heap(false, false, work_mem);
+ MemoryContextSwitchTo(oldcontext);
+ }
+ else
+ {
+ /* We already have tuplestore, just need to clear contents of it. */
+ tuplestore_clear(festate->tuples);
+ }
+
+
+ /* 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)));
+
+ /* put a tuples into the slot */
+ for (row = 0; row < rows; row++)
+ {
+ int j;
+ HeapTuple tuple;
+
+ for (i = 0, j = 0; i < tupdesc->natts; i++)
+ {
+ /* skip dropped columns. */
+ if (attrs[i]->attisdropped)
+ {
+ festate->col_values[i] = NULL;
+ continue;
+ }
+
+ if (PQgetisnull(res, row, j))
+ festate->col_values[i] = NULL;
+ else
+ festate->col_values[i] = PQgetvalue(res, row, 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(festate->attinmeta, festate->col_values);
+ tuplestore_puttuple(festate->tuples, tuple);
+ }
+
+ tuplestore_donestoring(festate->tuples);
+ }
+
+ /*
+ * contain_ext_param
+ * Recursively search for Param nodes within a clause.
+ *
+ * Returns true if any parameter reference node with relkind PARAM_EXTERN
+ * found.
+ *
+ * This does not descend into subqueries, and so should be used only after
+ * reduction of sublinks to subplans, or in contexts where it's known there
+ * are no subqueries. There mustn't be outer-aggregate references either.
+ *
+ * XXX: These functions could be in core, src/backend/optimizer/util/clauses.c.
+ */
+ static bool
+ contain_ext_param(Node *clause)
+ {
+ return contain_ext_param_walker(clause, NULL);
+ }
+
+ static bool
+ contain_ext_param_walker(Node *node, void *context)
+ {
+ if (node == NULL)
+ return false;
+ if (IsA(node, Param))
+ {
+ Param *param = (Param *) node;
+
+ if (param->paramkind == PARAM_EXTERN)
+ return true; /* abort the tree traversal and return true */
+ }
+ return expression_tree_walker(node, contain_ext_param_walker, context);
+ }
diff --git a/contrib/pgsql_fdw/pgsql_fdw.control b/contrib/pgsql_fdw/pgsql_fdw.control
index ...0a9c8f4 .
*** a/contrib/pgsql_fdw/pgsql_fdw.control
--- b/contrib/pgsql_fdw/pgsql_fdw.control
***************
*** 0 ****
--- 1,5 ----
+ # pgsql_fdw extension
+ comment = 'foreign-data wrapper for remote PostgreSQL servers'
+ default_version = '1.0'
+ module_pathname = '$libdir/pgsql_fdw'
+ relocatable = true
diff --git a/contrib/pgsql_fdw/pgsql_fdw.h b/contrib/pgsql_fdw/pgsql_fdw.h
index ...f6394ce .
*** a/contrib/pgsql_fdw/pgsql_fdw.h
--- b/contrib/pgsql_fdw/pgsql_fdw.h
***************
*** 0 ****
--- 1,28 ----
+ /*-------------------------------------------------------------------------
+ *
+ * pgsql_fdw.h
+ * foreign-data wrapper for remote PostgreSQL servers.
+ *
+ * Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/pgsql_fdw.h
+ *
+ *-------------------------------------------------------------------------
+ */
+
+ #ifndef PGSQL_FDW_H
+ #define PGSQL_FDW_H
+
+ #include "postgres.h"
+ #include "nodes/relation.h"
+
+ /* in option.c */
+ int ExtractConnectionOptions(List *defelems,
+ const char **keywords,
+ const char **values);
+
+ /* in deparse.c */
+ char *deparseSql(Oid relid, PlannerInfo *root, RelOptInfo *baserel);
+
+ #endif /* PGSQL_FDW_H */
diff --git a/contrib/pgsql_fdw/sql/pgsql_fdw.sql b/contrib/pgsql_fdw/sql/pgsql_fdw.sql
index ...42d26fd .
*** a/contrib/pgsql_fdw/sql/pgsql_fdw.sql
--- b/contrib/pgsql_fdw/sql/pgsql_fdw.sql
***************
*** 0 ****
--- 1,231 ----
+ -- ===================================================================
+ -- create FDW objects
+ -- ===================================================================
+
+ -- Clean up in case a prior regression run failed
+
+ -- Suppress NOTICE messages when roles don't exist
+ SET client_min_messages TO 'error';
+
+ DROP ROLE IF EXISTS pgsql_fdw_user;
+
+ RESET client_min_messages;
+
+ CREATE ROLE pgsql_fdw_user LOGIN SUPERUSER;
+ SET SESSION AUTHORIZATION 'pgsql_fdw_user';
+
+ CREATE EXTENSION pgsql_fdw;
+
+ CREATE SERVER loopback1 FOREIGN DATA WRAPPER pgsql_fdw;
+ CREATE SERVER loopback2 FOREIGN DATA WRAPPER pgsql_fdw
+ OPTIONS (dbname 'contrib_regression');
+
+ CREATE USER MAPPING FOR public SERVER loopback1
+ OPTIONS (user 'value', password 'value');
+ CREATE USER MAPPING FOR pgsql_fdw_user SERVER loopback2;
+
+ CREATE FOREIGN TABLE ft1 (
+ c0 int,
+ c1 int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10)
+ ) SERVER loopback2;
+ ALTER FOREIGN TABLE ft1 DROP COLUMN c0;
+
+ CREATE FOREIGN TABLE ft2 (
+ c0 int,
+ c1 int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10)
+ ) SERVER loopback2;
+ ALTER FOREIGN TABLE ft2 DROP COLUMN c0;
+
+ -- ===================================================================
+ -- create objects used through FDW
+ -- ===================================================================
+ CREATE SCHEMA "S 1";
+ CREATE TABLE "S 1"."T 1" (
+ "C 1" int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10),
+ CONSTRAINT t1_pkey PRIMARY KEY ("C 1")
+ );
+ CREATE TABLE "S 1"."T 2" (
+ c1 int NOT NULL,
+ c2 text,
+ CONSTRAINT t2_pkey PRIMARY KEY (c1)
+ );
+
+ BEGIN;
+ TRUNCATE "S 1"."T 1";
+ INSERT INTO "S 1"."T 1"
+ SELECT id,
+ id % 10,
+ to_char(id, 'FM00000'),
+ '1970-01-01'::timestamptz + ((id % 100) || ' days')::interval,
+ '1970-01-01'::timestamp + ((id % 100) || ' days')::interval,
+ id % 10,
+ id % 10
+ FROM generate_series(1, 1000) id;
+ TRUNCATE "S 1"."T 2";
+ INSERT INTO "S 1"."T 2"
+ SELECT id,
+ 'AAA' || to_char(id, 'FM000')
+ FROM generate_series(1, 100) id;
+ COMMIT;
+
+ -- ===================================================================
+ -- tests for pgsql_fdw_validator
+ -- ===================================================================
+ ALTER FOREIGN DATA WRAPPER pgsql_fdw OPTIONS (host 'value'); -- ERROR
+ -- requiressl, krbsrvname and gsslib are omitted because they depend on
+ -- configure option
+ ALTER SERVER loopback1 OPTIONS (
+ authtype 'value',
+ service 'value',
+ connect_timeout 'value',
+ dbname 'value',
+ host 'value',
+ hostaddr 'value',
+ port 'value',
+ --client_encoding 'value',
+ tty 'value',
+ options 'value',
+ application_name 'value',
+ --fallback_application_name 'value',
+ keepalives 'value',
+ keepalives_idle 'value',
+ keepalives_interval 'value',
+ -- requiressl 'value',
+ sslmode 'value',
+ sslcert 'value',
+ sslkey 'value',
+ sslrootcert 'value',
+ sslcrl 'value'
+ --requirepeer 'value',
+ -- krbsrvname 'value',
+ -- gsslib 'value',
+ --replication 'value'
+ );
+ ALTER SERVER loopback1 OPTIONS (user 'value'); -- ERROR
+ ALTER SERVER loopback2 OPTIONS (ADD fetch_count '2');
+ ALTER USER MAPPING FOR public SERVER loopback1
+ OPTIONS (DROP user, DROP password);
+ ALTER USER MAPPING FOR public SERVER loopback1
+ OPTIONS (host 'value'); -- ERROR
+ ALTER FOREIGN TABLE ft1 OPTIONS (nspname 'S 1', relname 'T 1');
+ ALTER FOREIGN TABLE ft2 OPTIONS (nspname 'S 1', relname 'T 1', fetch_count '100');
+ ALTER FOREIGN TABLE ft1 OPTIONS (invalid 'value'); -- ERROR
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count 'a'); -- ERROR
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count '0'); -- ERROR
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count '-1'); -- ERROR
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (invalid 'value'); -- ERROR
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (colname 'C 1');
+ ALTER FOREIGN TABLE ft2 ALTER COLUMN c1 OPTIONS (colname 'C 1');
+ \dew+
+ \des+
+ \deu+
+ \det+
+
+ -- ===================================================================
+ -- simple queries
+ -- ===================================================================
+ -- single table, with/without alias
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
+ SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+ SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+ -- with WHERE clause
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
+ SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
+ -- 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
+ SELECT * FROM ft1 t1 WHERE t1.c3 = (SELECT MAX(c3) FROM ft2 t2) ORDER BY c1;
+ -- used in CTE
+ WITH t1 AS (SELECT * FROM ft1 WHERE c1 <= 10) SELECT t2.c1, t2.c2, t2.c3, t2.c4 FROM t1, ft2 t2 WHERE t1.c1 = t2.c1 ORDER BY t1.c1;
+ -- fixed values
+ SELECT 'fixed', NULL FROM ft1 t1 WHERE c1 = 1;
+ -- user-defined operator/function
+ CREATE FUNCTION pgsql_fdw_abs(int) RETURNS int AS $$
+ BEGIN
+ RETURN abs($1);
+ END
+ $$ LANGUAGE plpgsql IMMUTABLE;
+ CREATE OPERATOR === (
+ LEFTARG = int,
+ RIGHTARG = int,
+ PROCEDURE = int4eq,
+ COMMUTATOR = ===,
+ NEGATOR = !==
+ );
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = pgsql_fdw_abs(t1.c2);
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 === t1.c2;
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = abs(t1.c2);
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = t1.c2;
+
+ -- ===================================================================
+ -- parameterized queries
+ -- ===================================================================
+ -- simple join
+ PREPARE st1(int, int) AS SELECT t1.c3, t2.c3 FROM ft1 t1, ft2 t2 WHERE t1.c1 = $1 AND t2.c1 = $2;
+ EXPLAIN (COSTS false) EXECUTE st1(1, 2);
+ EXECUTE st1(1, 1);
+ EXECUTE st1(101, 101);
+ -- subquery using stable function (can't be pushed down)
+ PREPARE st2(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c4) = 6) ORDER BY c1;
+ EXPLAIN (COSTS false) EXECUTE st2(10, 20);
+ EXECUTE st2(10, 20);
+ EXECUTE st1(101, 101);
+ -- subquery using immutable function (can be pushed down)
+ PREPARE st3(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c5) = 6) ORDER BY c1;
+ EXPLAIN (COSTS false) EXECUTE st3(10, 20);
+ EXECUTE st3(10, 20);
+ EXECUTE st3(20, 30);
+ -- custom plan should be chosen
+ PREPARE st4(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 = $1;
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ -- cleanup
+ DEALLOCATE st1;
+ DEALLOCATE st2;
+ DEALLOCATE st3;
+ DEALLOCATE st4;
+
+ -- ===================================================================
+ -- connection management
+ -- ===================================================================
+ SELECT srvname, usename FROM pgsql_fdw_connections;
+ SELECT pgsql_fdw_disconnect(srvid, usesysid) FROM pgsql_fdw_get_connections();
+ SELECT srvname, usename FROM pgsql_fdw_connections;
+
+ -- ===================================================================
+ -- cleanup
+ -- ===================================================================
+ DROP OPERATOR === (int, int) CASCADE;
+ DROP OPERATOR !== (int, int) CASCADE;
+ DROP FUNCTION pgsql_fdw_abs(int);
+ DROP SCHEMA "S 1" CASCADE;
+ DROP EXTENSION pgsql_fdw CASCADE;
+ \c
+ DROP ROLE pgsql_fdw_user;
diff --git a/doc/src/sgml/contrib.sgml b/doc/src/sgml/contrib.sgml
index d4da4ee..32056d1 100644
*** a/doc/src/sgml/contrib.sgml
--- b/doc/src/sgml/contrib.sgml
*************** CREATE EXTENSION <replaceable>module_nam
*** 117,122 ****
--- 117,123 ----
&pgcrypto;
&pgfreespacemap;
&pgrowlocks;
+ &pgsql-fdw;
&pgstandby;
&pgstatstatements;
&pgstattuple;
diff --git a/doc/src/sgml/filelist.sgml b/doc/src/sgml/filelist.sgml
index b5d3c6d..de686ee 100644
*** a/doc/src/sgml/filelist.sgml
--- b/doc/src/sgml/filelist.sgml
***************
*** 125,130 ****
--- 125,131 ----
<!ENTITY pgcrypto SYSTEM "pgcrypto.sgml">
<!ENTITY pgfreespacemap SYSTEM "pgfreespacemap.sgml">
<!ENTITY pgrowlocks SYSTEM "pgrowlocks.sgml">
+ <!ENTITY pgsql-fdw SYSTEM "pgsql-fdw.sgml">
<!ENTITY pgstandby SYSTEM "pgstandby.sgml">
<!ENTITY pgstatstatements SYSTEM "pgstatstatements.sgml">
<!ENTITY pgstattuple SYSTEM "pgstattuple.sgml">
diff --git a/doc/src/sgml/pgsql-fdw.sgml b/doc/src/sgml/pgsql-fdw.sgml
index ...c9d5d8f .
*** a/doc/src/sgml/pgsql-fdw.sgml
--- b/doc/src/sgml/pgsql-fdw.sgml
***************
*** 0 ****
--- 1,253 ----
+ <!-- doc/src/sgml/pgsql-fdw.sgml -->
+
+ <sect1 id="pgsql-fdw" xreflabel="pgsql_fdw">
+ <title>pgsql_fdw</title>
+
+ <indexterm zone="pgsql-fdw">
+ <primary>pgsql_fdw</primary>
+ </indexterm>
+
+ <para>
+ The <filename>pgsql_fdw</filename> module provides a foreign-data wrapper for
+ external <productname>PostgreSQL</productname> servers.
+ With this module, users can access data stored in external
+ <productname>PostgreSQL</productname> via plain SQL statements.
+ </para>
+
+ <para>
+ Note that default wrapper <literal>pgsql_fdw</literal> is created
+ automatically during <command>CREATE EXTENSION</command> command for
+ <application>pgsql_fdw</application>.
+ </para>
+
+ <sect2>
+ <title>FDW Options of pgsql_fdw</title>
+
+ <sect3>
+ <title>Connection Options</title>
+ <para>
+ A foreign server and user mapping created using this wrapper can have
+ <application>libpq</> connection options, expect below:
+
+ <itemizedlist>
+ <listitem><para>client_encoding</para></listitem>
+ <listitem><para>fallback_application_name</para></listitem>
+ <listitem><para>replication</para></listitem>
+ </itemizedlist>
+
+ For details of <application>libpq</> connection options, see
+ <xref linkend="libpq-connect">.
+ </para>
+
+ <para>
+ <literal>user</literal> and <literal>password</literal> can be
+ specified on user mappings, and others can be specified on foreign servers.
+ </para>
+ </sect3>
+
+ <sect3>
+ <title>Object Name Options</title>
+ <para>
+ Foreign tables which were created using this wrapper, or its columns can
+ have object name options. These options can be used to specify the names
+ used in SQL statement sent to remote <productname>PostgreSQL</productname>
+ server. These options are useful when a remote object has different name
+ from corresponding local one.
+ </para>
+
+ <variablelist>
+
+ <varlistentry>
+ <term><literal>nspname</literal></term>
+ <listitem>
+ <para>
+ This option, which can be specified on a foreign table, is used as a
+ namespace (schema) reference in the SQL statement. If this options is
+ omitted, <literal>pg_class.nspname</literal> of the foreign table is
+ used.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>relname</literal></term>
+ <listitem>
+ <para>
+ This option, which can be specified on a foreign table, is used as a
+ relation (table) reference in the SQL statement. If this options is
+ omitted, <literal>pg_class.relname</literal> of the foreign table is
+ used.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>colname</literal></term>
+ <listitem>
+ <para>
+ This option, which can be specified on a column of a foreign table, is
+ used as a column (attribute) reference in the SQL statement. If this
+ option is omitted, <literal>pg_attribute.attname</literal> of the column
+ of the foreign table is used.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ </variablelist>
+
+ </sect3>
+
+ <sect3>
+ <title>Cursor Options</title>
+ <para>
+ The <application>pgsql_fdw</application> always uses cursor to retrieve the
+ result from external server. Users can control the behavior of cursor by
+ setting cursor options to foreign table or foreign server. If an option
+ is set to both objects, finer-grained setting is used. In other words,
+ foreign table's setting overrides foreign server's setting.
+ </para>
+
+ <variablelist>
+
+ <varlistentry>
+ <term><literal>fetch_count</literal></term>
+ <listitem>
+ <para>
+ This option specifies the number of rows to be fetched at a time.
+ This option accepts only integer value larger than zero. The default
+ setting is 10000.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ </variablelist>
+
+ </sect3>
+
+ </sect2>
+
+ <sect2>
+ <title>Connection Management</title>
+
+ <para>
+ The <application>pgsql_fdw</application> establishes a connection to a
+ foreign server in the beginning of the first query which uses a foreign
+ table associated to the foreign server, and reuses the connection following
+ queries and even in following foreign scans in same query.
+
+ You can see the list of active connections via
+ <structname>pgsql_fdw_connections</structname> view. It shows pair of oid
+ and name of server and local role for each active connections established by
+ <application>pgsql_fdw</application>. For security reason, only superuser
+ can see other role's connections.
+ </para>
+
+ <para>
+ Established connections are kept alive until local role changes or the
+ current transaction aborts or user requests so.
+ </para>
+
+ <para>
+ If role has been changed, active connections established as old local role
+ is kept alive but never be reused until local role has restored to original
+ role. This kind of situation happens with <command>SET ROLE</command> and
+ <command>SET SESSION AUTHORIZATION</command>.
+ </para>
+
+ <para>
+ If current transaction aborts by error or user request, all active
+ connections are disconnected automatically. This behavior avoids possible
+ connection leaks on error.
+ </para>
+
+ <para>
+ You can discard persistent connection at arbitrary timing with
+ <function>pgsql_fdw_disconnect()</function>. It takes server oid and
+ user oid as arguments. This function can handle only connections
+ established in current session; connections established by other backends
+ are not reachable.
+ </para>
+
+ <para>
+ You can discard all active and visible connections in current session with
+ using <structname>pgsql_fdw_connections</structname> and
+ <function>pgsql_fdw_disconnect()</function> together:
+ <synopsis>
+ postgres=# SELECT pgsql_fdw_disconnect(srvid, usesysid) FROM pgsql_fdw_connections;
+ pgsql_fdw_disconnect
+ ----------------------
+ OK
+ OK
+ (2 rows)
+ </synopsis>
+ </para>
+ </sect2>
+
+ <sect2>
+ <title>Transaction Management</title>
+ <para>
+ The <application>pgsql_fdw</application> begins remote transaction at the
+ beginning of a local query, and terminates it with <command>ABORT</command>
+ at the end of the local query. This means that all foreign scans on a
+ foreign server in a local query are executed in one transaction.
+ The transaction isolation level used for remote transaction is same as
+ local transaction.
+ </para>
+ <para>
+ Note that even if the isolation level of local transaction was
+ <literal>SERIALIZABLE</literal> or <literal>REPEATABLE READ</literal>,
+ series of one query might produce different result, because foreign scans
+ in different local queries are executed in different remote transactions.
+ For instance, when client started a local transaction
+ explicitly with isolation level <literal>SERIALIZABLE</literal>, and
+ executed same local query which contains a foreign table which references
+ foreign data which is updated frequently, latter result would be different
+ from former result.
+ </para>
+ <para>
+ This restriction might be relaxed in future release.
+ </para>
+ </sect2>
+
+ <sect2>
+ <title>Estimation of Costs and Rows</title>
+ <para>
+ The <application>pgsql_fdw</application> estimates the costs of a foreign
+ scan by adding up some basic costs: connection costs, remote query costs
+ and data transfer costs.
+ To get remote query costs, <application>pgsql_fdw</application> executes
+ <command>EXPLAIN</command> command on remote server for each foreign scan.
+ </para>
+ <para>
+ On the other hand, estimated rows which was returned by
+ <command>EXPLAIN</command> is used for local estimation as-is.
+ </para>
+ </sect2>
+
+ <sect2>
+ <title>EXPLAIN Output</title>
+ <para>
+ For a foreign table using <literal>pgsql_fdw</>, <command>EXPLAIN</> shows
+ a remote SQL statement which is sent to remote
+ <productname>PostgreSQL</productname> server for a ForeignScan plan node.
+ For example:
+ </para>
+ <synopsis>
+ postgres=# EXPLAIN SELECT aid FROM pgbench_accounts WHERE abalance < 0;
+ QUERY PLAN
+ --------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on pgbench_accounts (cost=100.00..8105.13 rows=302613 width=8)
+ Filter: (abalance < 0)
+ Remote SQL: DECLARE pgsql_fdw_cursor_3 SCROLL CURSOR FOR SELECT aid, NULL, abalance, NULL FROM public.pgbench_accounts
+ (3 rows)
+ </synopsis>
+ </sect2>
+
+ <sect2>
+ <title>Author</title>
+ <para>
+ Shigeru Hanada <email>shigeru.hanada@gmail.com</email>
+ </para>
+ </sect2>
+
+ </sect1>
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 9ad54c5..c2a5a7b 100644
*** a/src/backend/utils/adt/ruleutils.c
--- b/src/backend/utils/adt/ruleutils.c
*************** deparse_context_for(const char *aliasnam
*** 2161,2166 ****
--- 2161,2189 ----
return list_make1(dpns);
}
+ /* ----------
+ * deparse_context_for_rtelist - Build deparse context for a list of RTEs
+ *
+ * Given the list of RangeTableEnetry, build deparsing context for an
+ * expression referencing those relations. This is sufficient for uses of
+ * deparse_expression before plan has been created.
+ * ----------
+ */
+ List *
+ deparse_context_for_rtelist(List *rtable)
+ {
+ deparse_namespace *dpns;
+
+ dpns = (deparse_namespace *) palloc0(sizeof(deparse_namespace));
+
+ /* Build a minimal RTE for the rel */
+ dpns->rtable = rtable;
+ dpns->ctes = NIL;
+
+ /* Return a one-deep namespace stack */
+ return list_make1(dpns);
+ }
+
/*
* deparse_context_for_planstate - Build deparse context for a plan
*
diff --git a/src/include/utils/builtins.h b/src/include/utils/builtins.h
index 2c331ce..dc6932a 100644
*** a/src/include/utils/builtins.h
--- b/src/include/utils/builtins.h
*************** extern char *deparse_expression(Node *ex
*** 648,653 ****
--- 648,654 ----
extern List *deparse_context_for(const char *aliasname, Oid relid);
extern List *deparse_context_for_planstate(Node *planstate, List *ancestors,
List *rtable);
+ extern List *deparse_context_for_rtelist(List *rtable);
extern const char *quote_identifier(const char *ident);
extern char *quote_qualified_identifier(const char *qualifier,
const char *ident);
pgsql_fdw_pushdown_v5.patchtext/plain; name=pgsql_fdw_pushdown_v5.patchDownload
diff --git a/contrib/pgsql_fdw/deparse.c b/contrib/pgsql_fdw/deparse.c
index feea7b3..d73a511 100644
*** a/contrib/pgsql_fdw/deparse.c
--- b/contrib/pgsql_fdw/deparse.c
*************** typedef struct foreign_executable_cxt
*** 35,46 ****
RelOptInfo *foreignrel;
} foreign_executable_cxt;
/*
* Deparse query representation into SQL statement which suits for remote
! * PostgreSQL server.
*/
char *
! deparseSql(Oid relid, PlannerInfo *root, RelOptInfo *baserel)
{
StringInfoData foreign_relname;
StringInfoData sql; /* builder for SQL statement */
--- 35,53 ----
RelOptInfo *foreignrel;
} foreign_executable_cxt;
+ static bool is_foreign_expr(PlannerInfo *root, RelOptInfo *baserel, Expr *expr);
+ static bool foreign_expr_walker(Node *node, foreign_executable_cxt *context);
+ static bool is_builtin(Oid procid);
+ static bool contain_ext_param(Node *clause);
+ static bool contain_ext_param_walker(Node *node, void *context);
+
/*
* Deparse query representation into SQL statement which suits for remote
! * PostgreSQL server. Also some of quals in WHERE clause will be pushed down
! * If they are safe to be evaluated on the remote side.
*/
char *
! deparseSql(Oid relid, PlannerInfo *root, RelOptInfo *baserel, bool *has_param)
{
StringInfoData foreign_relname;
StringInfoData sql; /* builder for SQL statement */
*************** deparseSql(Oid relid, PlannerInfo *root,
*** 51,56 ****
--- 58,64 ----
const char *relname = NULL; /* plain relation name */
const char *q_nspname; /* quoted namespace name */
const char *q_relname; /* quoted relation name */
+ List *foreign_expr = NIL; /* list of Expr* evaluated on remote */
int i;
List *rtable = NIL;
List *context = NIL;
*************** deparseSql(Oid relid, PlannerInfo *root,
*** 59,73 ****
initStringInfo(&foreign_relname);
/*
! * First of all, determine which column should be retrieved for this scan.
*
* We do this before deparsing SELECT clause because attributes which are
* not used in neither reltargetlist nor baserel->baserestrictinfo, quals
* evaluated on local, can be replaced with literal "NULL" in the SELECT
* clause to reduce overhead of tuple handling tuple and data transfer.
*/
if (baserel->baserestrictinfo != NIL)
{
ListCell *lc;
foreach (lc, baserel->baserestrictinfo)
--- 67,91 ----
initStringInfo(&foreign_relname);
/*
! * First of all, determine which qual can be pushed down.
! *
! * The expressions which satisfy is_foreign_expr() are deparsed into WHERE
! * clause of result SQL string, and they could be removed from PlanState
! * to avoid duplicate evaluation at ExecScan().
! *
! * We never change the quals in the Plan node, because this execution might
! * be for a PREPAREd statement, thus the quals in the Plan node might be
! * reused to construct another PlanState for subsequent EXECUTE statement.
*
* We do this before deparsing SELECT clause because attributes which are
* not used in neither reltargetlist nor baserel->baserestrictinfo, quals
* evaluated on local, can be replaced with literal "NULL" in the SELECT
* clause to reduce overhead of tuple handling tuple and data transfer.
*/
+ *has_param = false;
if (baserel->baserestrictinfo != NIL)
{
+ List *local_qual = NIL;
ListCell *lc;
foreach (lc, baserel->baserestrictinfo)
*************** deparseSql(Oid relid, PlannerInfo *root,
*** 76,81 ****
--- 94,113 ----
List *attrs;
/*
+ * Determine whether the qual can be pushed down or not. If a qual
+ * can be pushed down and it contains external param, tell that to
+ * caller in order to fall back to fixed costs.
+ */
+ if (is_foreign_expr(root, baserel, ri->clause))
+ {
+ foreign_expr = lappend(foreign_expr, ri->clause);
+ if (contain_ext_param((Node*) ri->clause))
+ *has_param = true;
+ }
+ else
+ local_qual = lappend(local_qual, ri);
+
+ /*
* We need to know which attributes are used in qual evaluated
* on the local server, because they should be listed in the
* SELECT clause of remote query. We can ignore attributes
*************** deparseSql(Oid relid, PlannerInfo *root,
*** 87,92 ****
--- 119,130 ----
PVC_RECURSE_PLACEHOLDERS);
attr_used = list_union(attr_used, attrs);
}
+
+ /*
+ * Remove pushed down qualifiers from baserestrictinfo to avoid
+ * redundant evaluation.
+ */
+ baserel->baserestrictinfo = local_qual;
}
/*
*************** deparseSql(Oid relid, PlannerInfo *root,
*** 204,210 ****
--- 242,472 ----
*/
appendStringInfo(&sql, "FROM %s", foreign_relname.data);
+ /*
+ * deparse WHERE clause
+ */
+ if (foreign_expr != NIL)
+ {
+ Node *node;
+
+ node = (Node *) make_ands_explicit(foreign_expr);
+ appendStringInfo(&sql, " WHERE %s ",
+ deparse_expression(node, context, false, false));
+ list_free(foreign_expr);
+ foreign_expr = NIL;
+ }
+
elog(DEBUG3, "Remote SQL: %s", sql.data);
return sql.data;
}
+ /*
+ * Returns true if expr is safe to be evaluated on the foreign server.
+ */
+ static bool
+ is_foreign_expr(PlannerInfo *root, RelOptInfo *baserel, Expr *expr)
+ {
+ foreign_executable_cxt context;
+ context.root = root;
+ context.foreignrel = baserel;
+
+ /*
+ * An expression which includes any mutable function can't be pushed down
+ * because it's result is not stable. For example, pushing now() down to
+ * remote side would cause confusion from the clock offset.
+ * If we have routine mapping infrastructure in future release, we will be
+ * able to choose function to be pushed down in finer granularity.
+ */
+ if (contain_mutable_functions((Node *) expr))
+ return false;
+
+ /*
+ * Check that the expression consists of nodes which are known as safe to
+ * be pushed down.
+ */
+ if (foreign_expr_walker((Node *) expr, &context))
+ return false;
+
+ return true;
+ }
+
+ /*
+ * Return true if node includes any node which is not known as safe to be
+ * pushed down.
+ */
+ static bool
+ foreign_expr_walker(Node *node, foreign_executable_cxt *context)
+ {
+ Oid func;
+
+ if (node == NULL)
+ return false;
+
+ /*
+ * If given expression has valid collation, it can't be pushed down because
+ * it might has incompatible semantics on remote side.
+ */
+ if (exprCollation(node) != InvalidOid ||
+ exprInputCollation(node) != InvalidOid)
+ return true;
+
+ /*
+ * If return type of given expression is not built-in, it can't be pushed
+ * down because it might has incompatible semantics on remote side.
+ */
+ if (!is_builtin(exprType(node)))
+ return true;
+
+ /*
+ * If function used by the expression is not built-in, it can't be pushed
+ * down because it might has incompatible semantics on remote side.
+ */
+ func = exprFunction(node);
+ if (func != InvalidOid && !is_builtin(func))
+ return true;
+
+ switch (nodeTag(node))
+ {
+ case T_Const:
+ case T_BoolExpr:
+ case T_NullTest:
+ case T_DistinctExpr:
+ case T_RelabelType:
+ case T_FuncExpr:
+ /*
+ * 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 at the tail of this function.
+ */
+ break;
+ case T_Param:
+ /*
+ * Only external parameters can be pushed down.:
+ */
+ {
+ if (((Param *) node)->paramkind != PARAM_EXTERN)
+ return true;
+ }
+ break;
+ case T_ScalarArrayOpExpr:
+ /*
+ * Only built-in operators can be pushed down. In addition,
+ * underlying function must be built-in and immutable, but we don't
+ * check volatility here; such check must be done already with
+ * contain_mutable_functions.
+ */
+ {
+ ScalarArrayOpExpr *oe = (ScalarArrayOpExpr *) node;
+
+ if (!is_builtin(oe->opno))
+ return true;
+
+ /* operands are checked later */
+ }
+ break;
+ case T_OpExpr:
+ /*
+ * Only built-in operators can be pushed down. In addition,
+ * underlying function must be built-in and immutable, but we don't
+ * check volatility here; such check must be done already with
+ * contain_mutable_functions.
+ */
+ {
+ OpExpr *oe = (OpExpr *) node;
+
+ if (!is_builtin(oe->opno))
+ return true;
+
+ /* operands are checked later */
+ }
+ 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;
+ foreign_executable_cxt *f_context;
+
+ f_context = (foreign_executable_cxt *) context;
+ if (var->varno != f_context->foreignrel->relid ||
+ var->varlevelsup != 0)
+ return true;
+ }
+ break;
+ case T_ArrayRef:
+ /*
+ * ArrayRef which holds non-built-in typed elements can't be pushed
+ * down.
+ */
+ {
+ if (!is_builtin(((ArrayRef *) node)->refelemtype))
+ return true;
+ }
+ break;
+ case T_ArrayExpr:
+ /*
+ * ArrayExpr which holds non-built-in typed elements can't be pushed
+ * down.
+ */
+ {
+ if (!is_builtin(((ArrayExpr *) node)->element_typeid))
+ return true;
+ }
+ break;
+ default:
+ {
+ ereport(DEBUG3,
+ (errmsg("expression is too complex"),
+ errdetail("%s", nodeToString(node))));
+ return true;
+ }
+ break;
+ }
+
+ return expression_tree_walker(node, foreign_expr_walker, context);
+ }
+
+ /*
+ * Return true if given object is one of built-in objects.
+ */
+ static bool
+ is_builtin(Oid oid)
+ {
+ return (oid < FirstNormalObjectId);
+ }
+
+ /*
+ * contain_ext_param
+ * Recursively search for Param nodes within a clause.
+ *
+ * Returns true if any parameter reference node with relkind PARAM_EXTERN
+ * found.
+ *
+ * This does not descend into subqueries, and so should be used only after
+ * reduction of sublinks to subplans, or in contexts where it's known there
+ * are no subqueries. There mustn't be outer-aggregate references either.
+ *
+ * XXX: These functions could be in core, src/backend/optimizer/util/clauses.c.
+ */
+ static bool
+ contain_ext_param(Node *clause)
+ {
+ return contain_ext_param_walker(clause, NULL);
+ }
+
+ static bool
+ contain_ext_param_walker(Node *node, void *context)
+ {
+ if (node == NULL)
+ return false;
+ if (IsA(node, Param))
+ {
+ Param *param = (Param *) node;
+
+ if (param->paramkind == PARAM_EXTERN)
+ return true; /* abort the tree traversal and return true */
+ }
+ return expression_tree_walker(node, contain_ext_param_walker, context);
+ }
diff --git a/contrib/pgsql_fdw/expected/pgsql_fdw.out b/contrib/pgsql_fdw/expected/pgsql_fdw.out
index 7fe20e7..1c027ad 100644
*** a/contrib/pgsql_fdw/expected/pgsql_fdw.out
--- b/contrib/pgsql_fdw/expected/pgsql_fdw.out
*************** SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.
*** 229,240 ****
-- with WHERE clause
EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
! QUERY PLAN
! ------------------------------------------------------------------------------------------------------------------
Foreign Scan on public.ft1 t1
Output: c1, c2, c3, c4, c5, c6, c7
! Filter: ((t1.c1 = 101) AND ((t1.c6)::text = '1'::text) AND (t1.c7 = '1'::bpchar))
! Remote SQL: DECLARE pgsql_fdw_cursor_4 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
(4 rows)
SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
--- 229,240 ----
-- with WHERE clause
EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
! QUERY PLAN
! ---------------------------------------------------------------------------------------------------------------------------------------
Foreign Scan on public.ft1 t1
Output: c1, c2, c3, c4, c5, c6, c7
! Filter: (((t1.c6)::text = '1'::text) AND (t1.c7 = '1'::bpchar))
! Remote SQL: DECLARE pgsql_fdw_cursor_4 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE ("C 1" = 101)
(4 rows)
SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
*************** EXPLAIN (COSTS false) SELECT * FROM ft1
*** 342,361 ****
(3 rows)
EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = abs(t1.c2);
! QUERY PLAN
! -------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Filter: (c1 = abs(c2))
! Remote SQL: DECLARE pgsql_fdw_cursor_20 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
! (3 rows)
EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = t1.c2;
! QUERY PLAN
! -------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Filter: (c1 = c2)
! Remote SQL: DECLARE pgsql_fdw_cursor_21 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
! (3 rows)
-- ===================================================================
-- parameterized queries
--- 342,359 ----
(3 rows)
EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = abs(t1.c2);
! QUERY PLAN
! --------------------------------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Remote SQL: DECLARE pgsql_fdw_cursor_20 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE ("C 1" = abs(c2))
! (2 rows)
EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = t1.c2;
! QUERY PLAN
! ---------------------------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Remote SQL: DECLARE pgsql_fdw_cursor_21 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE ("C 1" = c2)
! (2 rows)
-- ===================================================================
-- parameterized queries
*************** EXPLAIN (COSTS false) SELECT * FROM ft1
*** 363,379 ****
-- simple join
PREPARE st1(int, int) AS SELECT t1.c3, t2.c3 FROM ft1 t1, ft2 t2 WHERE t1.c1 = $1 AND t2.c1 = $2;
EXPLAIN (COSTS false) EXECUTE st1(1, 2);
! QUERY PLAN
! -----------------------------------------------------------------------------------------------------------------------------------------
Nested Loop
-> Foreign Scan on ft1 t1
! Filter: (c1 = 1)
! Remote SQL: DECLARE pgsql_fdw_cursor_22 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, NULL, NULL, NULL, NULL FROM "S 1"."T 1"
! -> Materialize
! -> Foreign Scan on ft2 t2
! Filter: (c1 = 2)
! Remote SQL: DECLARE pgsql_fdw_cursor_23 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, NULL, NULL, NULL, NULL FROM "S 1"."T 1"
! (8 rows)
EXECUTE st1(1, 1);
c3 | c3
--- 361,374 ----
-- simple join
PREPARE st1(int, int) AS SELECT t1.c3, t2.c3 FROM ft1 t1, ft2 t2 WHERE t1.c1 = $1 AND t2.c1 = $2;
EXPLAIN (COSTS false) EXECUTE st1(1, 2);
! QUERY PLAN
! ------------------------------------------------------------------------------------------------------------------------------------------------------
Nested Loop
-> Foreign Scan on ft1 t1
! Remote SQL: DECLARE pgsql_fdw_cursor_22 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, NULL, NULL, NULL, NULL FROM "S 1"."T 1" WHERE ("C 1" = 1)
! -> Foreign Scan on ft2 t2
! Remote SQL: DECLARE pgsql_fdw_cursor_23 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, NULL, NULL, NULL, NULL FROM "S 1"."T 1" WHERE ("C 1" = 2)
! (5 rows)
EXECUTE st1(1, 1);
c3 | c3
*************** EXECUTE st1(101, 101);
*** 390,410 ****
-- subquery using stable function (can't be pushed down)
PREPARE st2(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c4) = 6) ORDER BY c1;
EXPLAIN (COSTS false) EXECUTE st2(10, 20);
! QUERY PLAN
! ---------------------------------------------------------------------------------------------------------------------------------------------------
Sort
Sort Key: t1.c1
-> Hash Join
Hash Cond: (t1.c3 = t2.c3)
-> Foreign Scan on ft1 t1
! Filter: (c1 < 20)
! Remote SQL: DECLARE pgsql_fdw_cursor_28 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
-> Hash
-> HashAggregate
-> Foreign Scan on ft2 t2
! Filter: ((c1 > 10) AND (date_part('dow'::text, c4) = 6::double precision))
! Remote SQL: DECLARE pgsql_fdw_cursor_29 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, c4, NULL, NULL, NULL FROM "S 1"."T 1"
! (12 rows)
EXECUTE st2(10, 20);
c1 | c2 | c3 | c4 | c5 | c6 | c7
--- 385,404 ----
-- subquery using stable function (can't be pushed down)
PREPARE st2(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c4) = 6) ORDER BY c1;
EXPLAIN (COSTS false) EXECUTE st2(10, 20);
! QUERY PLAN
! -----------------------------------------------------------------------------------------------------------------------------------------------------------------------
Sort
Sort Key: t1.c1
-> Hash Join
Hash Cond: (t1.c3 = t2.c3)
-> Foreign Scan on ft1 t1
! Remote SQL: DECLARE pgsql_fdw_cursor_28 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE ("C 1" < 20)
-> Hash
-> HashAggregate
-> Foreign Scan on ft2 t2
! Filter: (date_part('dow'::text, c4) = 6::double precision)
! Remote SQL: DECLARE pgsql_fdw_cursor_29 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, c4, NULL, NULL, NULL FROM "S 1"."T 1" WHERE ("C 1" > 10)
! (11 rows)
EXECUTE st2(10, 20);
c1 | c2 | c3 | c4 | c5 | c6 | c7
*************** EXECUTE st1(101, 101);
*** 421,441 ****
-- subquery using immutable function (can be pushed down)
PREPARE st3(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c5) = 6) ORDER BY c1;
EXPLAIN (COSTS false) EXECUTE st3(10, 20);
! QUERY PLAN
! ---------------------------------------------------------------------------------------------------------------------------------------------------
Sort
Sort Key: t1.c1
-> Hash Join
Hash Cond: (t1.c3 = t2.c3)
-> Foreign Scan on ft1 t1
! Filter: (c1 < 20)
! Remote SQL: DECLARE pgsql_fdw_cursor_34 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
-> Hash
-> HashAggregate
-> Foreign Scan on ft2 t2
! Filter: ((c1 > 10) AND (date_part('dow'::text, c5) = 6::double precision))
! Remote SQL: DECLARE pgsql_fdw_cursor_35 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, NULL, c5, NULL, NULL FROM "S 1"."T 1"
! (12 rows)
EXECUTE st3(10, 20);
c1 | c2 | c3 | c4 | c5 | c6 | c7
--- 415,434 ----
-- subquery using immutable function (can be pushed down)
PREPARE st3(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c5) = 6) ORDER BY c1;
EXPLAIN (COSTS false) EXECUTE st3(10, 20);
! QUERY PLAN
! -----------------------------------------------------------------------------------------------------------------------------------------------------------------------
Sort
Sort Key: t1.c1
-> Hash Join
Hash Cond: (t1.c3 = t2.c3)
-> Foreign Scan on ft1 t1
! Remote SQL: DECLARE pgsql_fdw_cursor_34 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE ("C 1" < 20)
-> Hash
-> HashAggregate
-> Foreign Scan on ft2 t2
! Filter: (date_part('dow'::text, c5) = 6::double precision)
! Remote SQL: DECLARE pgsql_fdw_cursor_35 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, NULL, c5, NULL, NULL FROM "S 1"."T 1" WHERE ("C 1" > 10)
! (11 rows)
EXECUTE st3(10, 20);
c1 | c2 | c3 | c4 | c5 | c6 | c7
*************** EXECUTE st3(20, 30);
*** 452,503 ****
-- custom plan should be chosen
PREPARE st4(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 = $1;
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! -------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Filter: (c1 = 1)
! Remote SQL: DECLARE pgsql_fdw_cursor_40 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
! (3 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! -------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Filter: (c1 = 1)
! Remote SQL: DECLARE pgsql_fdw_cursor_41 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
! (3 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! -------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Filter: (c1 = 1)
! Remote SQL: DECLARE pgsql_fdw_cursor_42 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
! (3 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! -------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Filter: (c1 = 1)
! Remote SQL: DECLARE pgsql_fdw_cursor_43 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
! (3 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! -------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Filter: (c1 = 1)
! Remote SQL: DECLARE pgsql_fdw_cursor_44 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
! (3 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! -------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Filter: (c1 = 1)
! Remote SQL: DECLARE pgsql_fdw_cursor_46 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
! (3 rows)
-- cleanup
DEALLOCATE st1;
--- 445,490 ----
-- custom plan should be chosen
PREPARE st4(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 = $1;
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! --------------------------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Remote SQL: DECLARE pgsql_fdw_cursor_40 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE ("C 1" = 1)
! (2 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! --------------------------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Remote SQL: DECLARE pgsql_fdw_cursor_41 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE ("C 1" = 1)
! (2 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! --------------------------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Remote SQL: DECLARE pgsql_fdw_cursor_42 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE ("C 1" = 1)
! (2 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! --------------------------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Remote SQL: DECLARE pgsql_fdw_cursor_43 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE ("C 1" = 1)
! (2 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! --------------------------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Remote SQL: DECLARE pgsql_fdw_cursor_44 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE ("C 1" = 1)
! (2 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! --------------------------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Remote SQL: DECLARE pgsql_fdw_cursor_46 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE ("C 1" = 1)
! (2 rows)
-- cleanup
DEALLOCATE st1;
diff --git a/contrib/pgsql_fdw/pgsql_fdw.c b/contrib/pgsql_fdw/pgsql_fdw.c
index 4e2ce84..5480686 100644
*** a/contrib/pgsql_fdw/pgsql_fdw.c
--- b/contrib/pgsql_fdw/pgsql_fdw.c
*************** static void estimate_costs(PlannerInfo *
*** 123,130 ****
static void execute_query(ForeignScanState *node);
static PGresult *fetch_result(ForeignScanState *node);
static void store_result(ForeignScanState *node, PGresult *res);
- static bool contain_ext_param(Node *clause);
- static bool contain_ext_param_walker(Node *node, void *context);
/* Exported functions, but not written in pgsql_fdw.h. */
void _PG_init(void);
--- 123,128 ----
*************** pgsqlPlanForeignScan(Oid foreigntableid,
*** 194,207 ****
List *fdw_private = NIL;
ForeignTable *table;
ForeignServer *server;
/* Construct FdwPlan with cost estimates */
fdwplan = makeNode(FdwPlan);
! sql = deparseSql(foreigntableid, root, baserel);
table = GetForeignTable(foreigntableid);
server = GetForeignServer(table->serverid);
! estimate_costs(root, baserel, sql, server->serverid,
! &fdwplan->startup_cost, &fdwplan->total_cost);
/*
* Store plain SELECT statement in private area of FdwPlan. This will be
--- 192,222 ----
List *fdw_private = NIL;
ForeignTable *table;
ForeignServer *server;
+ bool has_param;
/* Construct FdwPlan with cost estimates */
fdwplan = makeNode(FdwPlan);
! sql = deparseSql(foreigntableid, root, baserel, &has_param);
table = GetForeignTable(foreigntableid);
server = GetForeignServer(table->serverid);
!
! /*
! * If the baserestrictinfo contains any Param node with paramkind
! * PARAM_EXTERNAL as part of push-down-able condition, we need to
! * groundless fixed costs because we can't get actual parameter values
! * here, so we use fixed and large costs as second best so that planner
! * tend to choose custom plan.
! *
! * See comments in plancache.c for details of custom plan.
! */
! if (has_param)
! {
! fdwplan->startup_cost = CONNECTION_COSTS;
! fdwplan->total_cost = 10000; /* groundless large costs */
! }
! else
! estimate_costs(root, baserel, sql, server->serverid,
! &fdwplan->startup_cost, &fdwplan->total_cost);
/*
* Store plain SELECT statement in private area of FdwPlan. This will be
*************** estimate_costs(PlannerInfo *root, RelOpt
*** 547,553 ****
const char *sql, Oid serverid,
Cost *startup_cost, Cost *total_cost)
{
- ListCell *lc;
ForeignServer *server;
UserMapping *user;
PGconn *conn = NULL;
--- 562,567 ----
*************** estimate_costs(PlannerInfo *root, RelOpt
*** 558,584 ****
int n;
/*
- * If the baserestrictinfo contains any Param node with paramkind
- * PARAM_EXTERNAL, we need result of EXPLAIN for EXECUTE statement, not for
- * simple SELECT statement. However, we can't get actual parameter values
- * here, so we use fixed and large costs as second best so that planner
- * tend to choose custom plan.
- *
- * See comments in plancache.c for details of custom plan.
- */
- foreach(lc, baserel->baserestrictinfo)
- {
- RestrictInfo *rs = (RestrictInfo *) lfirst(lc);
- if (contain_ext_param((Node *) rs->clause))
- {
- *startup_cost = CONNECTION_COSTS;
- *total_cost = 10000; /* groundless large costs */
-
- return;
- }
- }
-
- /*
* Get connection to the foreign server. Connection manager would
* establish new connection if necessary.
*/
--- 572,577 ----
*************** store_result(ForeignScanState *node, PGr
*** 849,885 ****
tuplestore_donestoring(festate->tuples);
}
-
- /*
- * contain_ext_param
- * Recursively search for Param nodes within a clause.
- *
- * Returns true if any parameter reference node with relkind PARAM_EXTERN
- * found.
- *
- * This does not descend into subqueries, and so should be used only after
- * reduction of sublinks to subplans, or in contexts where it's known there
- * are no subqueries. There mustn't be outer-aggregate references either.
- *
- * XXX: These functions could be in core, src/backend/optimizer/util/clauses.c.
- */
- static bool
- contain_ext_param(Node *clause)
- {
- return contain_ext_param_walker(clause, NULL);
- }
-
- static bool
- contain_ext_param_walker(Node *node, void *context)
- {
- if (node == NULL)
- return false;
- if (IsA(node, Param))
- {
- Param *param = (Param *) node;
-
- if (param->paramkind == PARAM_EXTERN)
- return true; /* abort the tree traversal and return true */
- }
- return expression_tree_walker(node, contain_ext_param_walker, context);
- }
--- 842,844 ----
diff --git a/contrib/pgsql_fdw/pgsql_fdw.h b/contrib/pgsql_fdw/pgsql_fdw.h
index f6394ce..690d58b 100644
*** a/contrib/pgsql_fdw/pgsql_fdw.h
--- b/contrib/pgsql_fdw/pgsql_fdw.h
*************** int ExtractConnectionOptions(List *defel
*** 23,28 ****
const char **values);
/* in deparse.c */
! char *deparseSql(Oid relid, PlannerInfo *root, RelOptInfo *baserel);
#endif /* PGSQL_FDW_H */
--- 23,31 ----
const char **values);
/* in deparse.c */
! char *deparseSql(Oid relid,
! PlannerInfo *root,
! RelOptInfo *baserel,
! bool *has_param);
#endif /* PGSQL_FDW_H */
diff --git a/doc/src/sgml/pgsql-fdw.sgml b/doc/src/sgml/pgsql-fdw.sgml
index c9d5d8f..df2e7f6 100644
*** a/doc/src/sgml/pgsql-fdw.sgml
--- b/doc/src/sgml/pgsql-fdw.sgml
*************** postgres=# SELECT pgsql_fdw_disconnect(s
*** 234,245 ****
</para>
<synopsis>
postgres=# EXPLAIN SELECT aid FROM pgbench_accounts WHERE abalance < 0;
! QUERY PLAN
! --------------------------------------------------------------------------------------------------------------------------
! Foreign Scan on pgbench_accounts (cost=100.00..8105.13 rows=302613 width=8)
! Filter: (abalance < 0)
! Remote SQL: DECLARE pgsql_fdw_cursor_3 SCROLL CURSOR FOR SELECT aid, NULL, abalance, NULL FROM public.pgbench_accounts
! (3 rows)
</synopsis>
</sect2>
--- 234,244 ----
</para>
<synopsis>
postgres=# EXPLAIN SELECT aid FROM pgbench_accounts WHERE abalance < 0;
! QUERY PLAN
! ------------------------------------------------------------------------------------------------------------------------------------------------
! Foreign Scan on pgbench_accounts (cost=100.00..8861.66 rows=518 width=8)
! Remote SQL: DECLARE pgsql_fdw_cursor_1 SCROLL CURSOR FOR SELECT aid, NULL, abalance, NULL FROM public.pgbench_accounts WHERE (abalance < 0)
! (2 rows)
</synopsis>
</sect2>
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index 51459c4..c017d6b 100644
*** a/src/backend/nodes/nodeFuncs.c
--- b/src/backend/nodes/nodeFuncs.c
*************** leftmostLoc(int loc1, int loc2)
*** 1405,1410 ****
--- 1405,1449 ----
return Min(loc1, loc2);
}
+ /*
+ * exprFunction -
+ * returns the Oid of the function used by the expression.
+ *
+ * Result is InvalidOid if the node type doesn't store this information.
+ */
+ Oid
+ exprFunction(const Node *expr)
+ {
+ Oid func;
+
+ if (!expr)
+ return InvalidOid;
+
+ switch (nodeTag(expr))
+ {
+ case T_Aggref:
+ func = ((const Aggref *) expr)->aggfnoid;
+ break;
+ case T_WindowFunc:
+ func = ((const WindowFunc *) expr)->winfnoid;
+ break;
+ case T_FuncExpr:
+ func = ((const FuncExpr *) expr)->funcid;
+ break;
+ case T_OpExpr:
+ func = ((const OpExpr *) expr)->opfuncid;
+ break;
+ case T_ScalarArrayOpExpr:
+ func = ((const ScalarArrayOpExpr *) expr)->opfuncid;
+ break;
+ case T_ArrayCoerceExpr:
+ func = ((const ArrayCoerceExpr *) expr)->elemfuncid;
+ break;
+ default:
+ func = InvalidOid;
+ }
+ return func;
+ }
/*
* Standard expression-tree walking support
diff --git a/src/include/nodes/nodeFuncs.h b/src/include/nodes/nodeFuncs.h
index def4f31..a3ebe5c 100644
*** a/src/include/nodes/nodeFuncs.h
--- b/src/include/nodes/nodeFuncs.h
*************** extern void exprSetInputCollation(Node *
*** 38,43 ****
--- 38,45 ----
extern int exprLocation(const Node *expr);
+ extern Oid exprFunction(const Node *expr);
+
extern bool expression_tree_walker(Node *node, bool (*walker) (),
void *context);
extern Node *expression_tree_mutator(Node *node, Node *(*mutator) (),
(2012/02/14 17:40), Etsuro Fujita wrote:
OK. But my question was about the PlanForeignScan API.
Sorry for misunderstanding. :(
As discussed at
that thread, it would have to change the PlanForeignScan API to let the
FDW generate multiple paths and dump them all to add_path instead of
returning a FdwPlan struct. With this change, I think it would also
have to add a new FDW API that is called from create_foreignscan_plan()
and lets the FDW generate foreignscan plan for the base relation scanned
by the best path choosed by postgres optimizer for itself. What do you
think about it?
Though I have only random thoughts about this issue at the moment...
Multiple valuable Paths for a scan of a foreign table by FDW, but
changing PlanForeignScan to return list of FdwPlan in 9.2 seems too
hasty. It would need more consideration about general interface for
possible results such as:
* Full output (no WHERE push-down) is expensive on both remote and transfer.
* Filtered output (WHERE push-down) has cheap total costs when only few
rows come through the filter.
* Ordered output (ORDER BY push-down) is expensive on remote, but has
chance to omit upper Sort node.
* Aggregated output (GROUP BY push-down) is expensive on remote, but
have chance to omit upper Agg node, and reduces data transfer.
* Limited output (LIMIT/OFFSET push-down) can reduce data transfer, and
have chance to omit upper Limit node.
Currently FDWs can consider only first two, AFAIK. If FDW generates
multiple FdwPlan (Full and Filtered) and sets different start-up costs
and total costs to them (may be former has higher start-up and lower
total than latter), planner would choose better for the whole plan.
In addition to changing FdwRoutine, it seems worth changing FdwPlan too
so that FDWs can return more information to planner, such as pathkeys
and rows, for each possible path.
In short, I have some ideas to enhance foreign table scans, but IMO they
are half-baked and we don't have enough time to achieve them for 9.2.
Regards,
--
Shigeru Hanada
(2012/02/14 19:42), Shigeru Hanada wrote:
(2012/02/14 17:40), Etsuro Fujita wrote:
As discussed at
that thread, it would have to change the PlanForeignScan API to let the
FDW generate multiple paths and dump them all to add_path instead of
returning a FdwPlan struct. With this change, I think it would also
have to add a new FDW API that is called from create_foreignscan_plan()
and lets the FDW generate foreignscan plan for the base relation scanned
by the best path choosed by postgres optimizer for itself. What do you
think about it?
In short, I have some ideas to enhance foreign table scans, but IMO they
are half-baked and we don't have enough time to achieve them for 9.2.
OK. Thank you for your answer.
Best regards,
Etsuro Fujita
Shigeru Hanada <shigeru.hanada@gmail.com> writes:
(2012/02/14 17:40), Etsuro Fujita wrote:
As discussed at
that thread, it would have to change the PlanForeignScan API to let the
FDW generate multiple paths and dump them all to add_path instead of
returning a FdwPlan struct.
Multiple valuable Paths for a scan of a foreign table by FDW, but
changing PlanForeignScan to return list of FdwPlan in 9.2 seems too
hasty.
I would really like to see that happen in 9.2, because the longer we let
that mistake live, the harder it will be to change. More and more FDWs
are getting written. I don't think it's that hard to do: we just have
to agree that PlanForeignScan should return void and call add_path for
itself, possibly more than once. If we do that, I'm inclined to think
we cou;d get rid of the separate Node type FdwPlan, and just incorporate
"List *fdw_private" into ForeignPath and ForeignScan.
This does mean that FDWs will be a bit more tightly coupled to the
planner, because they'll have to change whenever we add new fields to
struct Path; but that is not really something that happens often.
regards, tom lane
(2012/02/14 23:50), Tom Lane wrote:
Shigeru Hanada<shigeru.hanada@gmail.com> writes:
(2012/02/14 17:40), Etsuro Fujita wrote:
As discussed at
that thread, it would have to change the PlanForeignScan API to let the
FDW generate multiple paths and dump them all to add_path instead of
returning a FdwPlan struct.Multiple valuable Paths for a scan of a foreign table by FDW, but
changing PlanForeignScan to return list of FdwPlan in 9.2 seems too
hasty.I would really like to see that happen in 9.2, because the longer we let
that mistake live, the harder it will be to change. More and more FDWs
are getting written. I don't think it's that hard to do: we just have
to agree that PlanForeignScan should return void and call add_path for
itself, possibly more than once.
Agreed. I fixed the PlanForeignScan API. Please find attached a patch.
If we do that, I'm inclined to think
we cou;d get rid of the separate Node type FdwPlan, and just incorporate
"List *fdw_private" into ForeignPath and ForeignScan.
+1 While the patch retains the struct FdwPlan, I would like to get rid
of it at next version of the patch.
Best regards,
Etsuro Fujita
Attachments:
postgresql-fdw.patchtext/plain; name=postgresql-fdw.patchDownload
*** a/contrib/file_fdw/file_fdw.c
--- b/contrib/file_fdw/file_fdw.c
***************
*** 25,30 ****
--- 25,31 ----
#include "miscadmin.h"
#include "nodes/makefuncs.h"
#include "optimizer/cost.h"
+ #include "optimizer/pathnode.h"
#include "utils/rel.h"
#include "utils/syscache.h"
***************
*** 93,99 **** PG_FUNCTION_INFO_V1(file_fdw_validator);
/*
* FDW callback routines
*/
! static FdwPlan *filePlanForeignScan(Oid foreigntableid,
PlannerInfo *root,
RelOptInfo *baserel);
static void fileExplainForeignScan(ForeignScanState *node, ExplainState *es);
--- 94,100 ----
/*
* FDW callback routines
*/
! static void filePlanForeignScan(Oid foreigntableid,
PlannerInfo *root,
RelOptInfo *baserel);
static void fileExplainForeignScan(ForeignScanState *node, ExplainState *es);
***************
*** 406,432 **** get_file_fdw_attribute_options(Oid relid)
/*
* filePlanForeignScan
! * Create a FdwPlan for a scan on the foreign table
*/
! static FdwPlan *
filePlanForeignScan(Oid foreigntableid,
PlannerInfo *root,
RelOptInfo *baserel)
{
FdwPlan *fdwplan;
char *filename;
List *options;
/* Fetch options --- we only need filename at this point */
fileGetOptions(foreigntableid, &filename, &options);
/* Construct FdwPlan with cost estimates */
fdwplan = makeNode(FdwPlan);
estimate_costs(root, baserel, filename,
&fdwplan->startup_cost, &fdwplan->total_cost);
- fdwplan->fdw_private = NIL; /* not used */
! return fdwplan;
}
/*
--- 407,447 ----
/*
* filePlanForeignScan
! * Create the (single) path for a scan on the foreign table
*/
! static void
filePlanForeignScan(Oid foreigntableid,
PlannerInfo *root,
RelOptInfo *baserel)
{
+ ForeignPath *pathnode = makeNode(ForeignPath);
FdwPlan *fdwplan;
char *filename;
List *options;
+ pathnode->path.pathtype = T_ForeignScan;
+ pathnode->path.parent = baserel;
+ pathnode->path.pathkeys = NIL; /* result is always unordered */
+ pathnode->path.required_outer = NULL;
+ pathnode->path.param_clauses = NIL;
+
/* Fetch options --- we only need filename at this point */
fileGetOptions(foreigntableid, &filename, &options);
/* Construct FdwPlan with cost estimates */
fdwplan = makeNode(FdwPlan);
+ fdwplan->fdw_private = NIL; /* not used */
estimate_costs(root, baserel, filename,
&fdwplan->startup_cost, &fdwplan->total_cost);
! pathnode->fdwplan = fdwplan;
!
! /* Use costs estimated by FDW */
! pathnode->path.rows = baserel->rows;
! pathnode->path.startup_cost = fdwplan->startup_cost;
! pathnode->path.total_cost = fdwplan->total_cost;
!
! add_path(baserel, (Path *) pathnode);
}
/*
*** a/doc/src/sgml/fdwhandler.sgml
--- b/doc/src/sgml/fdwhandler.sgml
***************
*** 88,108 ****
<para>
<programlisting>
! FdwPlan *
PlanForeignScan (Oid foreigntableid,
PlannerInfo *root,
RelOptInfo *baserel);
</programlisting>
! Plan a scan on a foreign table. This is called when a query is planned.
<literal>foreigntableid</> is the <structname>pg_class</> OID of the
foreign table. <literal>root</> is the planner's global information
about the query, and <literal>baserel</> is the planner's information
about this table.
! The function must return a palloc'd struct that contains cost estimates
! plus any FDW-private information that is needed to execute the foreign
! scan at a later time. (Note that the private information must be
! represented in a form that <function>copyObject</> knows how to copy.)
</para>
<para>
--- 88,114 ----
<para>
<programlisting>
! void
PlanForeignScan (Oid foreigntableid,
PlannerInfo *root,
RelOptInfo *baserel);
</programlisting>
! Create the access paths for a scan on a foreign table. This is called
! when a query is planned.
<literal>foreigntableid</> is the <structname>pg_class</> OID of the
foreign table. <literal>root</> is the planner's global information
about the query, and <literal>baserel</> is the planner's information
about this table.
! The function must generate at least one access path for a scan on the
! foreign table and call <function>add_path</> to add the access path to
! <literal>baserel->pathlist</>. The function may generate multiple
! access paths for a scan on the foreign table and add them to
! <literal>baserel->pathlist</>. Each access path generated should
! contain cost estimates plus any FDW-private information that is needed
! to execute the foreign scan at a later time.
! (Note that the private information must be represented in a form that
! <function>copyObject</> knows how to copy.)
</para>
<para>
*** a/src/backend/optimizer/path/allpaths.c
--- b/src/backend/optimizer/path/allpaths.c
***************
*** 399,411 **** set_foreign_size(PlannerInfo *root, RelOptInfo *rel, RangeTblEntry *rte)
/*
* set_foreign_pathlist
! * Build the (single) access path for a foreign table RTE
*/
static void
set_foreign_pathlist(PlannerInfo *root, RelOptInfo *rel, RangeTblEntry *rte)
{
! /* Generate appropriate path */
! add_path(rel, (Path *) create_foreignscan_path(root, rel));
/* Select cheapest path (pretty easy in this case...) */
set_cheapest(rel);
--- 399,411 ----
/*
* set_foreign_pathlist
! * Build one or more access paths for a foreign table RTE
*/
static void
set_foreign_pathlist(PlannerInfo *root, RelOptInfo *rel, RangeTblEntry *rte)
{
! /* Generate appropriate paths */
! create_foreignscan_path(root, rel);
/* Select cheapest path (pretty easy in this case...) */
set_cheapest(rel);
*** a/src/backend/optimizer/util/pathnode.c
--- b/src/backend/optimizer/util/pathnode.c
***************
*** 1764,1803 **** create_worktablescan_path(PlannerInfo *root, RelOptInfo *rel)
/*
* create_foreignscan_path
! * Creates a path corresponding to a scan of a foreign table,
! * returning the pathnode.
*/
! ForeignPath *
create_foreignscan_path(PlannerInfo *root, RelOptInfo *rel)
{
- ForeignPath *pathnode = makeNode(ForeignPath);
RangeTblEntry *rte;
FdwRoutine *fdwroutine;
- FdwPlan *fdwplan;
-
- pathnode->path.pathtype = T_ForeignScan;
- pathnode->path.parent = rel;
- pathnode->path.pathkeys = NIL; /* result is always unordered */
- pathnode->path.required_outer = NULL;
- pathnode->path.param_clauses = NIL;
/* Get FDW's callback info */
rte = planner_rt_fetch(rel->relid, root);
fdwroutine = GetFdwRoutineByRelId(rte->relid);
/* Let the FDW do its planning */
! fdwplan = fdwroutine->PlanForeignScan(rte->relid, root, rel);
! if (fdwplan == NULL || !IsA(fdwplan, FdwPlan))
! elog(ERROR, "foreign-data wrapper PlanForeignScan function for relation %u did not return an FdwPlan struct",
! rte->relid);
! pathnode->fdwplan = fdwplan;
!
! /* use costs estimated by FDW */
! pathnode->path.rows = rel->rows;
! pathnode->path.startup_cost = fdwplan->startup_cost;
! pathnode->path.total_cost = fdwplan->total_cost;
!
! return pathnode;
}
/*
--- 1764,1784 ----
/*
* create_foreignscan_path
! * Creates one or more paths corresponding to a scan of a foreign table,
! * returning void.
*/
! void
create_foreignscan_path(PlannerInfo *root, RelOptInfo *rel)
{
RangeTblEntry *rte;
FdwRoutine *fdwroutine;
/* Get FDW's callback info */
rte = planner_rt_fetch(rel->relid, root);
fdwroutine = GetFdwRoutineByRelId(rte->relid);
/* Let the FDW do its planning */
! fdwroutine->PlanForeignScan(rte->relid, root, rel);
}
/*
*** a/src/include/foreign/fdwapi.h
--- b/src/include/foreign/fdwapi.h
***************
*** 52,58 **** typedef struct FdwPlan
* Callback function signatures --- see fdwhandler.sgml for more info.
*/
! typedef FdwPlan *(*PlanForeignScan_function) (Oid foreigntableid,
PlannerInfo *root,
RelOptInfo *baserel);
--- 52,58 ----
* Callback function signatures --- see fdwhandler.sgml for more info.
*/
! typedef void (*PlanForeignScan_function) (Oid foreigntableid,
PlannerInfo *root,
RelOptInfo *baserel);
*** a/src/include/optimizer/pathnode.h
--- b/src/include/optimizer/pathnode.h
***************
*** 68,74 **** extern Path *create_functionscan_path(PlannerInfo *root, RelOptInfo *rel);
extern Path *create_valuesscan_path(PlannerInfo *root, RelOptInfo *rel);
extern Path *create_ctescan_path(PlannerInfo *root, RelOptInfo *rel);
extern Path *create_worktablescan_path(PlannerInfo *root, RelOptInfo *rel);
! extern ForeignPath *create_foreignscan_path(PlannerInfo *root, RelOptInfo *rel);
extern Relids calc_nestloop_required_outer(Path *outer_path, Path *inner_path);
extern Relids calc_non_nestloop_required_outer(Path *outer_path, Path *inner_path);
--- 68,74 ----
extern Path *create_valuesscan_path(PlannerInfo *root, RelOptInfo *rel);
extern Path *create_ctescan_path(PlannerInfo *root, RelOptInfo *rel);
extern Path *create_worktablescan_path(PlannerInfo *root, RelOptInfo *rel);
! extern void create_foreignscan_path(PlannerInfo *root, RelOptInfo *rel);
extern Relids calc_nestloop_required_outer(Path *outer_path, Path *inner_path);
extern Relids calc_non_nestloop_required_outer(Path *outer_path, Path *inner_path);
Harada-san,
I checked the v9 patch, however, it still has some uncertain implementation.
[memory context of tuple store]
It calls tuplestore_begin_heap() under the memory context of
festate->scan_cxt at pgsqlBeginForeignScan.
On the other hand, tuplestore_gettupleslot() is called under the
memory context of festate->tuples.
I could not find a callback functions being invoked on errors,
so I doubt the memory objects acquired within tuplestore_begin_heap()
shall be leaked, even though it is my suggestion to create a sub-context
under the existing one.
In my opinion, it is a good choice to use es_query_cxt of the supplied EState.
What does prevent to apply this per-query memory context?
You mention about PGresult being malloc()'ed. However, it seems to me
fetch_result() and store_result() once copy the contents on malloc()'ed
area to the palloc()'ed area, and PQresult is released on an error using
PG_TRY() ... PG_CATCH() block.
[Minor comments]
Please set NULL to "sql" variable at begin_remote_tx().
Compiler raises a warnning due to references of uninitialized variable,
even though the code path never run.
It potentially causes a problem in case when fetch_result() raises an
error because of unexpected status (!= PGRES_TUPLES_OK).
One code path is not protected with PG_TRY(), and other code path
will call PQclear towards already released PQresult.
Although it is just a preference of mine, is the exprFunction necessary?
It seems to me, the point of push-down check is whether the supplied
node is built-in object, or not. So, an sufficient check is is_builtin() onto
FuncExpr->funcid, OpExpr->opno, ScalarArrayOpExpr->opno and so on.
It does not depend on whether the function implementing these nodes
are built-in or not.
Thanks,
2012年2月14日9:09 Shigeru Hanada <shigeru.hanada@gmail.com>:
(2012/02/14 15:15), Shigeru Hanada wrote:
Good catch, thanks. I'll revise pgsql_fdw tests little more.
Here are the updated patches. In addition to Fujita-san's comment, I
moved DROP OPERATOR statements to clean up section of test script.Regards,
--
Shigeru Hanada
--
KaiGai Kohei <kaigai@kaigai.gr.jp>
Kaigai-san,
Thanks for the review. Attached patches are revised version, though
only fdw_helper_v5.patch is unchanged.
(2012/02/16 0:09), Kohei KaiGai wrote:
[memory context of tuple store]
It calls tuplestore_begin_heap() under the memory context of
festate->scan_cxt at pgsqlBeginForeignScan.
Yes, it's because tuplestore uses a context which was current when
tuplestore_begin_heap was called. I want to use per-scan context for
tuplestore, to keep its content tuples alive through the scan.
On the other hand, tuplestore_gettupleslot() is called under the
memory context of festate->tuples.
Yes, result tuples to be returned to executor should be allocated in
per-scan context and live until next IterateForeignScan (or
EndForeignScan), because such tuple will be released via ExecClearTuple
in next IterateForeignScan call. If we don't switch context to per-scan
context, result tuple is allocated in per-tuple context and cause
double-free and server crash.
I could not find a callback functions being invoked on errors,
so I doubt the memory objects acquired within tuplestore_begin_heap()
shall be leaked, even though it is my suggestion to create a sub-context
under the existing one.
How do you confirmed that no callback function is invoked on errors? I
think that memory objects acquired within tuplestore_begin_heap (I guess
you mean excluding stored tuples, right?) are released during cleanup of
aborted transaction. I tested that by adding elog(ERROR) to the tail of
store_result() for intentional error, and execute large query 100 times
in a session. I saw VIRT value (via top command) comes down to constant
level after every query.
In my opinion, it is a good choice to use es_query_cxt of the supplied EState.
What does prevent to apply this per-query memory context?
Ah, I've confused context management of pgsql_fdw... I fixed pgsql_fdw
to create per-scan context as a child of es_query_cxt in
BeginForeignScan, and use it for tuplestore of the scan. So, tuplestore
and its contents are released correctly at EndForeignScan, or cleanup of
aborted transaction in error case.
You mention about PGresult being malloc()'ed. However, it seems to me
fetch_result() and store_result() once copy the contents on malloc()'ed
area to the palloc()'ed area, and PQresult is released on an error using
PG_TRY() ... PG_CATCH() block.
During thinking about this comment, I found double-free bug of PGresult
in execute_query, thanks :)
But, sorry, I'm not sure what the concern you show here is. The reason
why copying tuples from malloc'ed area to palloc'ed area is to release
PGresult before returning from the IterateForeingScan call. The reason
why using PG_TRY block is to sure that PGresult is released before jump
back to upstream in error case.
[Minor comments]
Please set NULL to "sql" variable at begin_remote_tx().
Compiler raises a warnning due to references of uninitialized variable,
even though the code path never run.
Fixed. BTW, just out of curiosity, which compiler do you use? My
compiler ,gcc (GCC) 4.6.0 20110603 (Red Hat 4.6.0-10) on Fedora 15,
doesn't warn it.
It potentially causes a problem in case when fetch_result() raises an
error because of unexpected status (!= PGRES_TUPLES_OK).
One code path is not protected with PG_TRY(), and other code path
will call PQclear towards already released PQresult.
Fixed.
Although it is just a preference of mine, is the exprFunction necessary?
It seems to me, the point of push-down check is whether the supplied
node is built-in object, or not. So, an sufficient check is is_builtin() onto
FuncExpr->funcid, OpExpr->opno, ScalarArrayOpExpr->opno and so on.
It does not depend on whether the function implementing these nodes
are built-in or not.
Got rid of exprFunction and fixed foreign_expr_walker to check function
oid in each case label.
Regards,
--
Shigeru Hanada
Attachments:
fdw_helper_v5.patchtext/plain; name=fdw_helper_v5.patchDownload
diff --git a/contrib/file_fdw/file_fdw.c b/contrib/file_fdw/file_fdw.c
index 46394a8..a522e36 100644
*** a/contrib/file_fdw/file_fdw.c
--- b/contrib/file_fdw/file_fdw.c
***************
*** 26,32 ****
#include "nodes/makefuncs.h"
#include "optimizer/cost.h"
#include "utils/rel.h"
! #include "utils/syscache.h"
PG_MODULE_MAGIC;
--- 26,32 ----
#include "nodes/makefuncs.h"
#include "optimizer/cost.h"
#include "utils/rel.h"
! #include "utils/lsyscache.h"
PG_MODULE_MAGIC;
*************** get_file_fdw_attribute_options(Oid relid
*** 345,398 ****
/* Retrieve FDW options for all user-defined attributes. */
for (attnum = 1; attnum <= natts; attnum++)
{
! HeapTuple tuple;
! Form_pg_attribute attr;
! Datum datum;
! bool isnull;
/* Skip dropped attributes. */
if (tupleDesc->attrs[attnum - 1]->attisdropped)
continue;
! /*
! * We need the whole pg_attribute tuple not just what is in the
! * tupleDesc, so must do a catalog lookup.
! */
! tuple = SearchSysCache2(ATTNUM,
! RelationGetRelid(rel),
! Int16GetDatum(attnum));
! if (!HeapTupleIsValid(tuple))
! elog(ERROR, "cache lookup failed for attribute %d of relation %u",
! attnum, RelationGetRelid(rel));
! attr = (Form_pg_attribute) GETSTRUCT(tuple);
!
! datum = SysCacheGetAttr(ATTNUM,
! tuple,
! Anum_pg_attribute_attfdwoptions,
! &isnull);
! if (!isnull)
{
! List *options = untransformRelOptions(datum);
! ListCell *lc;
! foreach(lc, options)
{
! DefElem *def = (DefElem *) lfirst(lc);
!
! if (strcmp(def->defname, "force_not_null") == 0)
{
! if (defGetBoolean(def))
! {
! char *attname = pstrdup(NameStr(attr->attname));
! fnncolumns = lappend(fnncolumns, makeString(attname));
! }
}
- /* maybe in future handle other options here */
}
}
-
- ReleaseSysCache(tuple);
}
heap_close(rel, AccessShareLock);
--- 345,373 ----
/* Retrieve FDW options for all user-defined attributes. */
for (attnum = 1; attnum <= natts; attnum++)
{
! List *options;
! ListCell *lc;
/* Skip dropped attributes. */
if (tupleDesc->attrs[attnum - 1]->attisdropped)
continue;
! options = GetForeignColumnOptions(relid, attnum);
! foreach(lc, options)
{
! DefElem *def = (DefElem *) lfirst(lc);
! if (strcmp(def->defname, "force_not_null") == 0)
{
! if (defGetBoolean(def))
{
! char *attname = pstrdup(get_attname(relid, attnum));
! fnncolumns = lappend(fnncolumns, makeString(attname));
}
}
+ /* maybe in future handle other options here */
}
}
heap_close(rel, AccessShareLock);
diff --git a/doc/src/sgml/fdwhandler.sgml b/doc/src/sgml/fdwhandler.sgml
index 76ff243..d809cac 100644
*** a/doc/src/sgml/fdwhandler.sgml
--- b/doc/src/sgml/fdwhandler.sgml
*************** EndForeignScan (ForeignScanState *node);
*** 235,238 ****
--- 235,392 ----
</sect1>
+ <sect1 id="fdw-helpers">
+ <title>Foreign Data Wrapper Helper Functions</title>
+
+ <para>
+ Several helper functions are exported from core so that authors of FDW
+ can get easy access to attributes of FDW-related objects such as FDW
+ options.
+ </para>
+
+ <para>
+ <programlisting>
+ ForeignDataWrapper *
+ GetForeignDataWrapper(Oid fdwid);
+ </programlisting>
+
+ This function returns a <structname>ForeignDataWrapper</structname> object
+ for a foreign-data wrapper with given oid. A
+ <structname>ForeignDataWrapper</structname> object contains oid of the
+ wrapper itself, oid of the owner of the wrapper, name of the wrapper, oid
+ of fdwhandler function, oid of fdwvalidator function, and FDW options in
+ the form of list of <structname>DefElem</structname>.
+ </para>
+
+ <para>
+ <programlisting>
+ ForeignServer *
+ GetForeignServer(Oid serverid);
+ </programlisting>
+
+ This function returns a <structname>ForeignServer</structname> object for
+ a foreign server with given oid. A <structname>ForeignServer</structname>
+ object contains oid of the server, oid of the wrapper for the server, oid
+ of the owner of the server, name of the server, type of the server,
+ version of the server, and FDW options in the form of list of
+ <structname>DefElem</structname>.
+ </para>
+
+ <para>
+ <programlisting>
+ UserMapping *
+ GetUserMapping(Oid userid, Oid serverid);
+ </programlisting>
+
+ This function returns a <structname>UserMapping</structname> object for a
+ user mapping with given oid pair. A <structname>UserMapping</structname>
+ object contains oid of the user, oid of the server, and FDW options in the
+ form of list of <structname>DefElem</structname>.
+ </para>
+
+ <para>
+ <programlisting>
+ ForeignTable *
+ GetForeignTable(Oid relid);
+ </programlisting>
+
+ This function returns a <structname>ForeignTable</structname> object for a
+ foreign table with given oid. A <structname>ForeignTable</structname>
+ object contains oid of the foreign table, oid of the server for the table,
+ and FDW options in the form of list of <structname>DefElem</structname>.
+ </para>
+
+ <para>
+ <programlisting>
+ List *
+ GetForeignTableColumnOptions(Oid relid, AttrNumber attnum);
+ </programlisting>
+
+ This function returns per-column FDW options for a column with given
+ relation oid and attribute number in the form of list of
+ <structname>DefElem</structname>.
+ </para>
+
+ <para>
+ Some object types have name-based functions.
+ </para>
+
+ <para>
+ <programlisting>
+ ForeignDataWrapper *
+ GetForeignDataWrapperByName(const char *name, bool missing_ok);
+ </programlisting>
+
+ This function returns a <structname>ForeignDataWrapper</structname> object
+ for a foreign-data wrapper with given name. If the wrapper is not found,
+ return NULL if missing_ok was true, otherwise raise an error.
+ </para>
+
+ <para>
+ <programlisting>
+ ForeignServer *
+ GetForeignServerByName(const char *name, bool missing_ok);
+ </programlisting>
+
+ This function returns a <structname>ForeignServer</structname> object for
+ a foreign server with given name. If the server is not found, return NULL
+ if missing_ok was true, otherwise raise an error.
+ </para>
+
+ <para>
+ <programlisting>
+ char *
+ GetFdwOptionValue(Oid fdwid, Oid serverid, Oid relid, AttrNumber attnum, const char *optname);
+ </programlisting>
+
+ This function returns a copied string (created in current memory context)
+ of the value of a FDW option with given name which is set on a object with
+ given oid and attribute number. This function ignores catalogs if invalid
+ identifir is given for it.
+
+ <itemizedlist>
+ <listitem>
+ <para>
+ If attnum is <literal>InvalidAttrNumber</literal> or relid is
+ <literal>Invalidoid</literal>, <structname>pg_attribute</structname> is
+ ignored.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ If relid is <literal>InvalidOid</literal>,
+ <structname>pg_foreign_table</structname> is ignored.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ If both serverid and relid are <literal>InvalidOid</literal>,
+ <structname>pg_foreign_server</structname> is ignored.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ If all of fdwid, serverid and relid are <literal>InvalidOid</literal>,
+ <structname>pg_foreign_data_wrapper</structname> is ignored.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+
+ <para>
+ If the option with given name is set in multiple object level, the one in
+ the finest-grained object is used; e.g. priority is given to user mappings
+ over than foreign servers.
+ This function would be useful when you know which option is needed but you
+ don't know where it is set. If you already know the source object, it
+ would be more efficient to use object retrieval functions.
+ </para>
+
+ <para>
+ To use any of these functions, you need to include
+ <filename>foreign/foreign.h</filename> in your source file.
+ </para>
+
+ </sect1>
+
</chapter>
diff --git a/src/backend/foreign/foreign.c b/src/backend/foreign/foreign.c
index c4c2a61..8e98f0f 100644
*** a/src/backend/foreign/foreign.c
--- b/src/backend/foreign/foreign.c
*************** GetForeignTable(Oid relid)
*** 248,253 ****
--- 248,378 ----
/*
+ * If an option entry which matches the given name was found in the given
+ * list, returns a copy of arg string, otherwise returns NULL.
+ */
+ static char *
+ get_options_value(List *options, const char *optname)
+ {
+ ListCell *lc;
+
+ /* Find target option from the list. */
+ foreach (lc, options)
+ {
+ DefElem *def = lfirst(lc);
+
+ if (strcmp(def->defname, optname) == 0)
+ return pstrdup(strVal(def->arg));
+ }
+
+ return NULL;
+ }
+
+ /*
+ * Returns a copy of the value of specified option with searching the option
+ * from appropriate catalog. If an option was stored in multiple object
+ * levels, one in the finest-grained object level is used; lookup order is:
+ * 1) pg_attribute (only when both attnum and relid are valid)
+ * 2) pg_foreign_table (only when relid is valid)
+ * 3) pg_user_mapping (only when serverid or relid is valid)
+ * 4) pg_foreign_server (only when serverid or relid is valid)
+ * 5) pg_foreign_data_wrapper (only when fdwid or serverid or relid is valid)
+ * This priority rule would be useful in most cases using FDW options.
+ *
+ * If attnum was InvalidAttrNumber, we don't retrieve FDW optiosn from
+ * pg_attribute.attfdwoptions.
+ */
+ char *
+ GetFdwOptionValue(Oid fdwid, Oid serverid, Oid relid, AttrNumber attnum,
+ const char *optname)
+ {
+ ForeignTable *table = NULL;
+ UserMapping *user = NULL;
+ ForeignServer *server = NULL;
+ ForeignDataWrapper *wrapper = NULL;
+ char *value;
+
+ /* Do we need to search pg_attribute? */
+ if (attnum != InvalidAttrNumber && relid != InvalidOid)
+ {
+ value = get_options_value(GetForeignColumnOptions(relid, attnum),
+ optname);
+ if (value != NULL)
+ return value;
+ }
+
+ /* Do we need to search pg_foreign_table? */
+ if (relid != InvalidOid)
+ {
+ table = GetForeignTable(relid);
+ value = get_options_value(table->options, optname);
+ if (value != NULL)
+ return value;
+
+ serverid = table->serverid;
+ }
+
+ /* Do we need to search pg_user_mapping and pg_foreign_server? */
+ if (serverid != InvalidOid)
+ {
+ user = GetUserMapping(GetOuterUserId(), serverid);
+ value = get_options_value(user->options, optname);
+ if (value != NULL)
+ return value;
+
+ server = GetForeignServer(serverid);
+ value = get_options_value(server->options, optname);
+ if (value != NULL)
+ return value;
+
+ fdwid = server->fdwid;
+ }
+
+ /* Do we need to search pg_foreign_data_wrapper? */
+ if (fdwid != InvalidOid)
+ {
+ wrapper = GetForeignDataWrapper(fdwid);
+ value = get_options_value(wrapper->options, optname);
+ if (value != NULL)
+ return value;
+ }
+
+ return NULL;
+ }
+
+
+ /*
+ * GetForeignColumnOptions - Get attfdwoptions of given relation/attnum as
+ * list of DefElem.
+ */
+ List *
+ GetForeignColumnOptions(Oid relid, AttrNumber attnum)
+ {
+ List *options;
+ HeapTuple tp;
+ Datum datum;
+ bool isnull;
+
+ tp = SearchSysCache2(ATTNUM,
+ ObjectIdGetDatum(relid),
+ Int16GetDatum(attnum));
+ if (!HeapTupleIsValid(tp))
+ elog(ERROR, "cache lookup failed for attribute %d of relation %u", attnum, relid);
+ datum = SysCacheGetAttr(ATTNUM,
+ tp,
+ Anum_pg_attribute_attfdwoptions,
+ &isnull);
+ if (isnull)
+ options = NIL;
+ else
+ options = untransformRelOptions(datum);
+
+ ReleaseSysCache(tp);
+
+ return options;
+ }
+
+ /*
* GetFdwRoutine - call the specified foreign-data wrapper handler routine
* to get its FdwRoutine struct.
*/
diff --git a/src/include/foreign/foreign.h b/src/include/foreign/foreign.h
index 191122d..e25b871 100644
*** a/src/include/foreign/foreign.h
--- b/src/include/foreign/foreign.h
*************** extern ForeignDataWrapper *GetForeignDat
*** 75,80 ****
--- 75,83 ----
extern ForeignDataWrapper *GetForeignDataWrapperByName(const char *name,
bool missing_ok);
extern ForeignTable *GetForeignTable(Oid relid);
+ extern char *GetFdwOptionValue(Oid fdwid, Oid serverid, Oid relid,
+ AttrNumber attnum, const char *optname);
+ extern List *GetForeignColumnOptions(Oid relid, AttrNumber attnum);
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);
pgsql_fdw_v10.patchtext/plain; name=pgsql_fdw_v10.patchDownload
diff --git a/contrib/Makefile b/contrib/Makefile
index ac0a80a..14aa433 100644
*** a/contrib/Makefile
--- b/contrib/Makefile
*************** SUBDIRS = \
*** 41,46 ****
--- 41,47 ----
pgbench \
pgcrypto \
pgrowlocks \
+ pgsql_fdw \
pgstattuple \
seg \
spi \
diff --git a/contrib/README b/contrib/README
index a1d42a1..d3fa211 100644
*** a/contrib/README
--- b/contrib/README
*************** pgrowlocks -
*** 158,163 ****
--- 158,167 ----
A function to return row locking information
by Tatsuo Ishii <ishii@sraoss.co.jp>
+ pgsql_fdw -
+ Foreign-data wrapper for external PostgreSQL servers.
+ by Shigeru Hanada <shigeru.hanada@gmail.com>
+
pgstattuple -
Functions to return statistics about "dead" tuples and free
space within a table
diff --git a/contrib/pgsql_fdw/.gitignore b/contrib/pgsql_fdw/.gitignore
index ...0854728 .
*** a/contrib/pgsql_fdw/.gitignore
--- b/contrib/pgsql_fdw/.gitignore
***************
*** 0 ****
--- 1,4 ----
+ # Generated subdirectories
+ /results/
+ *.o
+ *.so
diff --git a/contrib/pgsql_fdw/Makefile b/contrib/pgsql_fdw/Makefile
index ...6381365 .
*** a/contrib/pgsql_fdw/Makefile
--- b/contrib/pgsql_fdw/Makefile
***************
*** 0 ****
--- 1,22 ----
+ # contrib/pgsql_fdw/Makefile
+
+ MODULE_big = pgsql_fdw
+ OBJS = pgsql_fdw.o option.o deparse.o connection.o
+ PG_CPPFLAGS = -I$(libpq_srcdir)
+ SHLIB_LINK = $(libpq)
+
+ EXTENSION = pgsql_fdw
+ DATA = pgsql_fdw--1.0.sql
+
+ REGRESS = pgsql_fdw
+
+ ifdef USE_PGXS
+ PG_CONFIG = pg_config
+ PGXS := $(shell $(PG_CONFIG) --pgxs)
+ include $(PGXS)
+ else
+ subdir = contrib/pgsql_fdw
+ top_builddir = ../..
+ include $(top_builddir)/src/Makefile.global
+ include $(top_srcdir)/contrib/contrib-global.mk
+ endif
diff --git a/contrib/pgsql_fdw/connection.c b/contrib/pgsql_fdw/connection.c
index ...48a5f1e .
*** a/contrib/pgsql_fdw/connection.c
--- b/contrib/pgsql_fdw/connection.c
***************
*** 0 ****
--- 1,553 ----
+ /*-------------------------------------------------------------------------
+ *
+ * connection.c
+ * Connection management for pgsql_fdw
+ *
+ * Portions Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/connection.c
+ *
+ *-------------------------------------------------------------------------
+ */
+ #include "postgres.h"
+
+ #include "access/xact.h"
+ #include "catalog/pg_type.h"
+ #include "foreign/foreign.h"
+ #include "funcapi.h"
+ #include "libpq-fe.h"
+ #include "mb/pg_wchar.h"
+ #include "miscadmin.h"
+ #include "utils/array.h"
+ #include "utils/builtins.h"
+ #include "utils/hsearch.h"
+ #include "utils/memutils.h"
+ #include "utils/resowner.h"
+ #include "utils/tuplestore.h"
+
+ #include "pgsql_fdw.h"
+ #include "connection.h"
+
+ /* ============================================================================
+ * Connection management functions
+ * ==========================================================================*/
+
+ /*
+ * Connection cache entry managed with hash table.
+ */
+ typedef struct ConnCacheEntry
+ {
+ /* hash key must be first */
+ Oid serverid; /* oid of foreign server */
+ Oid userid; /* oid of local user */
+
+ bool use_tx; /* true when using remote transaction */
+ 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);
+ static void begin_remote_tx(PGconn *conn);
+ static void abort_remote_tx(PGconn *conn);
+
+ /*
+ * 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.
+ *
+ * When use_tx is true, remote transaction is started if caller is the only
+ * user of the connection. Isolation level of the remote transaction is same
+ * as local transaction, and remote transaction will be aborted when last
+ * user release.
+ *
+ * TODO: Note that caching connections requires a mechanism to detect change of
+ * FDW object to invalidate already established connections.
+ */
+ PGconn *
+ GetConnection(ForeignServer *server, UserMapping *user, bool use_tx)
+ {
+ bool found;
+ ConnCacheEntry *entry;
+ ConnCacheEntry key;
+
+ /* initialize connection cache if it isn't */
+ if (FSConnectionHash == NULL)
+ {
+ HASHCTL ctl;
+
+ /* hash key is a pair of oids: serverid and userid */
+ MemSet(&ctl, 0, sizeof(ctl));
+ ctl.keysize = sizeof(Oid) + sizeof(Oid);
+ ctl.entrysize = sizeof(ConnCacheEntry);
+ ctl.hash = tag_hash;
+ ctl.match = memcmp;
+ ctl.keycopy = memcpy;
+ /* allocate FSConnectionHash in the cache context */
+ ctl.hcxt = CacheMemoryContext;
+ FSConnectionHash = hash_create("Foreign Connections", 32,
+ &ctl,
+ HASH_ELEM | HASH_CONTEXT |
+ HASH_FUNCTION | HASH_COMPARE |
+ HASH_KEYCOPY);
+ }
+
+ /* Create key value for the entry. */
+ MemSet(&key, 0, sizeof(key));
+ key.serverid = server->serverid;
+ key.userid = GetOuterUserId();
+
+ /*
+ * Find cached entry for requested connection. If we couldn't find,
+ * callback function of ResourceOwner should be registered to clean the
+ * connection up on error including user interrupt.
+ */
+ entry = hash_search(FSConnectionHash, &key, HASH_ENTER, &found);
+ if (!found)
+ {
+ entry->refs = 0;
+ entry->use_tx = false;
+ entry->conn = NULL;
+ RegisterResourceReleaseCallback(cleanup_connection, entry);
+ }
+
+ /*
+ * We don't check the health of cached connection here, because it would
+ * require some overhead. Broken connection and its cache entry will be
+ * cleaned up when the connection is actually used.
+ */
+
+ /*
+ * If cache entry doesn't have connection, we have to establish new
+ * connection.
+ */
+ if (entry->conn == NULL)
+ {
+ /*
+ * Use PG_TRY block to ensure closing connection on error.
+ */
+ PG_TRY();
+ {
+ /*
+ * Connect to the foreign PostgreSQL server, and store it in cache
+ * entry to keep new connection.
+ * Note: key items of entry has already been initialized in
+ * hash_search(HASH_ENTER).
+ */
+ entry->conn = connect_pg_server(server, user);
+ }
+ PG_CATCH();
+ {
+ /* Clear connection cache entry on error case. */
+ PQfinish(entry->conn);
+ entry->refs = 0;
+ entry->use_tx = false;
+ entry->conn = NULL;
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+ }
+
+ /* Increase connection reference counter. */
+ entry->refs++;
+
+ /*
+ * If requester is the only referrer of this connection, start transaction
+ * with the same isolation level as the local transaction we are in.
+ * We need to remember whether this connection uses remote transaction to
+ * abort it when this connection is released completely.
+ */
+ if (use_tx && entry->refs == 1)
+ begin_remote_tx(entry->conn);
+ entry->use_tx = use_tx;
+
+
+ return entry->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 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. Those two object hold only libpq options.
+ * Extra 3 items are for:
+ * *) fallback_application_name
+ * *) client_encoding
+ * *) NULL termination (end marker)
+ *
+ * Note: We don't omit any parameters even target database might be older
+ * than local, because unexpected parameters are just ignored.
+ */
+ n = list_length(server->options) + list_length(user->options) + 3;
+ 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 += ExtractConnectionOptions(server->options,
+ all_keywords + n, all_values + n);
+ n += ExtractConnectionOptions(user->options,
+ all_keywords + n, all_values + n);
+ all_keywords[n] = all_values[n] = NULL;
+
+ for (i = 0, j = 0; all_keywords[i]; i++)
+ {
+ keywords[j] = all_keywords[i];
+ values[j] = all_values[i];
+ j++;
+ }
+
+ /* Use "pgsql_fdw" as fallback_application_name. */
+ keywords[j] = "fallback_application_name";
+ values[j++] = "pgsql_fdw";
+
+ /* Set client_encoding so that libpq can convert encoding properly. */
+ keywords[j] = "client_encoding";
+ values[j++] = GetDatabaseEncodingName();
+
+ keywords[j] = values[j] = NULL;
+ pfree(all_keywords);
+ pfree(all_values);
+
+ /* 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.")));
+ }
+
+ return conn;
+ }
+
+ /*
+ * Start transaction to use cursor to retrieve data separately.
+ */
+ static void
+ begin_remote_tx(PGconn *conn)
+ {
+ const char *sql = NULL; /* keep compiler quiet. */
+ PGresult *res;
+
+ switch (XactIsoLevel)
+ {
+ case XACT_READ_UNCOMMITTED:
+ sql = "START TRANSACTION ISOLATION LEVEL READ UNCOMMITTED";
+ break;
+ case XACT_READ_COMMITTED:
+ sql = "START TRANSACTION ISOLATION LEVEL READ COMMITTED";
+ break;
+ case XACT_REPEATABLE_READ:
+ sql = "START TRANSACTION ISOLATION LEVEL REPEATABLE READ";
+ break;
+ case XACT_SERIALIZABLE:
+ sql = "START TRANSACTION ISOLATION LEVEL SERIALIZABLE";
+ break;
+ default:
+ elog(ERROR, "unexpected isolation level: %d", XactIsoLevel);
+ break;
+ }
+
+ elog(DEBUG3, "starting remote transaction with \"%s\"", sql);
+
+ res = PQexec(conn, sql);
+ if (PQresultStatus(res) != PGRES_COMMAND_OK)
+ {
+ PQclear(res);
+ elog(ERROR, "could not start transaction: %s", PQerrorMessage(conn));
+ }
+ PQclear(res);
+ }
+
+ static void
+ abort_remote_tx(PGconn *conn)
+ {
+ PGresult *res;
+
+ elog(DEBUG3, "aborting remote transaction");
+
+ res = PQexec(conn, "ABORT TRANSACTION");
+ if (PQresultStatus(res) != PGRES_COMMAND_OK)
+ {
+ PQclear(res);
+ elog(ERROR, "could not abort transaction: %s", PQerrorMessage(conn));
+ }
+ PQclear(res);
+ }
+
+ /*
+ * 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 sequentially 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;
+ }
+ if (entry != NULL)
+ hash_seq_term(&scan);
+
+ /*
+ * If this connection uses remote transaction and caller is the last user,
+ * abort remote transaction and forget about it.
+ */
+ if (entry->use_tx && entry->refs == 1)
+ {
+ abort_remote_tx(conn);
+ entry->use_tx = false;
+ }
+
+ /*
+ * If the released connection was an orphan, just close it.
+ */
+ if (entry == NULL)
+ {
+ PQfinish(conn);
+ return;
+ }
+
+ /*
+ * Decrease reference counter of this connection. Even if the caller was
+ * the last referrer, we don't unregister it from cache.
+ */
+ entry->refs--;
+ }
+
+ /*
+ * 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)
+ {
+ PQfinish(entry->conn);
+ entry->refs = 0;
+ entry->conn = NULL;
+ }
+ }
+
+ /*
+ * Get list of connections currently active.
+ */
+ Datum pgsql_fdw_get_connections(PG_FUNCTION_ARGS);
+ PG_FUNCTION_INFO_V1(pgsql_fdw_get_connections);
+ Datum
+ pgsql_fdw_get_connections(PG_FUNCTION_ARGS)
+ {
+ ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+ HASH_SEQ_STATUS scan;
+ ConnCacheEntry *entry;
+ MemoryContext oldcontext = CurrentMemoryContext;
+ Tuplestorestate *tuplestore;
+ TupleDesc tupdesc;
+
+ /* We return list of connection with storing them in a Tuplestore. */
+ rsinfo->returnMode = SFRM_Materialize;
+ rsinfo->setResult = NULL;
+ rsinfo->setDesc = NULL;
+
+ /* Create tuplestore and copy of TupleDesc in per-query context. */
+ MemoryContextSwitchTo(rsinfo->econtext->ecxt_per_query_memory);
+
+ tupdesc = CreateTemplateTupleDesc(2, false);
+ TupleDescInitEntry(tupdesc, 1, "srvid", OIDOID, -1, 0);
+ TupleDescInitEntry(tupdesc, 2, "usesysid", OIDOID, -1, 0);
+ rsinfo->setDesc = tupdesc;
+
+ tuplestore = tuplestore_begin_heap(false, false, work_mem);
+ rsinfo->setResult = tuplestore;
+
+ MemoryContextSwitchTo(oldcontext);
+
+ /*
+ * We need to scan sequentially since we use the address to find
+ * appropriate PGconn from the hash table.
+ */
+ if (FSConnectionHash != NULL)
+ {
+ hash_seq_init(&scan, FSConnectionHash);
+ while ((entry = (ConnCacheEntry *) hash_seq_search(&scan)))
+ {
+ Datum values[2];
+ bool nulls[2];
+ HeapTuple tuple;
+
+ /* Ignore inactive connections */
+ if (PQstatus(entry->conn) != CONNECTION_OK)
+ continue;
+
+ /*
+ * Ignore other users' connections if current user isn't a
+ * superuser.
+ */
+ if (!superuser() && entry->userid != GetUserId())
+ continue;
+
+ values[0] = ObjectIdGetDatum(entry->serverid);
+ values[1] = ObjectIdGetDatum(entry->userid);
+ nulls[0] = false;
+ nulls[1] = false;
+
+ tuple = heap_formtuple(tupdesc, values, nulls);
+ tuplestore_puttuple(tuplestore, tuple);
+ }
+ }
+ tuplestore_donestoring(tuplestore);
+
+ PG_RETURN_VOID();
+ }
+
+ /*
+ * Discard persistent connection designated by given connection name.
+ */
+ Datum pgsql_fdw_disconnect(PG_FUNCTION_ARGS);
+ PG_FUNCTION_INFO_V1(pgsql_fdw_disconnect);
+ Datum
+ pgsql_fdw_disconnect(PG_FUNCTION_ARGS)
+ {
+ Oid serverid = PG_GETARG_OID(0);
+ Oid userid = PG_GETARG_OID(1);
+ ConnCacheEntry key;
+ ConnCacheEntry *entry = NULL;
+ bool found;
+
+ /* Non-superuser can't discard other users' connection. */
+ if (!superuser() && userid != GetOuterUserId())
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_RESOURCES),
+ errmsg("only superuser can discard other user's connection")));
+
+ /*
+ * If no connection has been established, or no such connections, just
+ * return "NG" to indicate nothing has done.
+ */
+ if (FSConnectionHash == NULL)
+ PG_RETURN_TEXT_P(cstring_to_text("NG"));
+
+ key.serverid = serverid;
+ key.userid = userid;
+ entry = hash_search(FSConnectionHash, &key, HASH_FIND, &found);
+ if (!found)
+ PG_RETURN_TEXT_P(cstring_to_text("NG"));
+
+ /* Discard cached connection, and clear reference counter. */
+ PQfinish(entry->conn);
+ entry->refs = 0;
+ entry->conn = NULL;
+
+ PG_RETURN_TEXT_P(cstring_to_text("OK"));
+ }
diff --git a/contrib/pgsql_fdw/connection.h b/contrib/pgsql_fdw/connection.h
index ...4427f56 .
*** a/contrib/pgsql_fdw/connection.h
--- b/contrib/pgsql_fdw/connection.h
***************
*** 0 ****
--- 1,25 ----
+ /*-------------------------------------------------------------------------
+ *
+ * connection.h
+ * Connection management for pgsql_fdw
+ *
+ * Portions Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_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, bool use_tx);
+ void ReleaseConnection(PGconn *conn);
+
+ #endif /* CONNECTION_H */
diff --git a/contrib/pgsql_fdw/deparse.c b/contrib/pgsql_fdw/deparse.c
index ...7ca2767 .
*** a/contrib/pgsql_fdw/deparse.c
--- b/contrib/pgsql_fdw/deparse.c
***************
*** 0 ****
--- 1,210 ----
+ /*-------------------------------------------------------------------------
+ *
+ * deparse.c
+ * query deparser for PostgreSQL
+ *
+ * Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/deparse.c
+ *
+ *-------------------------------------------------------------------------
+ */
+ #include "postgres.h"
+
+ #include "access/transam.h"
+ #include "foreign/foreign.h"
+ #include "lib/stringinfo.h"
+ #include "nodes/nodeFuncs.h"
+ #include "nodes/nodes.h"
+ #include "nodes/makefuncs.h"
+ #include "optimizer/clauses.h"
+ #include "optimizer/var.h"
+ #include "parser/parsetree.h"
+ #include "utils/builtins.h"
+ #include "utils/lsyscache.h"
+
+ #include "pgsql_fdw.h"
+
+ /*
+ * Context for walk-through the expression tree.
+ */
+ typedef struct foreign_executable_cxt
+ {
+ PlannerInfo *root;
+ RelOptInfo *foreignrel;
+ } foreign_executable_cxt;
+
+ /*
+ * Deparse query representation into SQL statement which suits for remote
+ * PostgreSQL server.
+ */
+ char *
+ deparseSql(Oid relid, PlannerInfo *root, RelOptInfo *baserel)
+ {
+ StringInfoData foreign_relname;
+ StringInfoData sql; /* builder for SQL statement */
+ bool first;
+ AttrNumber attr;
+ List *attr_used = NIL; /* List of AttNumber used in the query */
+ const char *nspname = NULL; /* plain namespace name */
+ const char *relname = NULL; /* plain relation name */
+ const char *q_nspname; /* quoted namespace name */
+ const char *q_relname; /* quoted relation name */
+ int i;
+ List *rtable = NIL;
+ List *context = NIL;
+
+ initStringInfo(&sql);
+ initStringInfo(&foreign_relname);
+
+ /*
+ * First of all, determine which column should be retrieved for this scan.
+ *
+ * We do this before deparsing SELECT clause because attributes which are
+ * not used in neither reltargetlist nor baserel->baserestrictinfo, quals
+ * evaluated on local, can be replaced with literal "NULL" in the SELECT
+ * clause to reduce overhead of tuple handling tuple and data transfer.
+ */
+ if (baserel->baserestrictinfo != NIL)
+ {
+ ListCell *lc;
+
+ foreach (lc, baserel->baserestrictinfo)
+ {
+ RestrictInfo *ri = (RestrictInfo *) lfirst(lc);
+ List *attrs;
+
+ /*
+ * We need to know which attributes are used in qual evaluated
+ * on the local server, because they should be listed in the
+ * SELECT clause of remote query. We can ignore attributes
+ * which are referenced only in ORDER BY/GROUP BY clause because
+ * such attributes has already been kept in reltargetlist.
+ */
+ attrs = pull_var_clause((Node *) ri->clause,
+ PVC_RECURSE_AGGREGATES,
+ PVC_RECURSE_PLACEHOLDERS);
+ attr_used = list_union(attr_used, attrs);
+ }
+ }
+
+ /*
+ * Determine foreign relation's qualified name. This is necessary for
+ * FROM clause and SELECT clause.
+ */
+ nspname = GetFdwOptionValue(InvalidOid, InvalidOid, relid,
+ InvalidAttrNumber, "nspname");
+ if (nspname == NULL)
+ nspname = get_namespace_name(get_rel_namespace(relid));
+ q_nspname = quote_identifier(nspname);
+
+ relname = GetFdwOptionValue(InvalidOid, InvalidOid, relid,
+ InvalidAttrNumber, "relname");
+ if (relname == NULL)
+ relname = get_rel_name(relid);
+ q_relname = quote_identifier(relname);
+
+ appendStringInfo(&foreign_relname, "%s.%s", q_nspname, q_relname);
+
+ /*
+ * We need to replace aliasname and colnames of the target relation so that
+ * constructed remote query is valid.
+ *
+ * Note that we skip first empty element of simple_rel_array. See also
+ * comments of simple_rel_array and simple_rte_array for the rationale.
+ */
+ for (i = 1; i < root->simple_rel_array_size; i++)
+ {
+ RangeTblEntry *rte = copyObject(root->simple_rte_array[i]);
+ List *newcolnames = NIL;
+
+ if (i == baserel->relid)
+ {
+ /*
+ * Create new list of column names which is used to deparse remote
+ * query from specified names or local column names. This list is
+ * used by deparse_expression.
+ */
+ for (attr = 1; attr <= baserel->max_attr; attr++)
+ {
+ char *colname = NULL;
+
+ /*
+ * Get colname option for each undropped attribute. If colname
+ * option is not supplied, use local column name in remote
+ * query.
+ */
+ if (get_rte_attribute_is_dropped(rte, attr))
+ colname = "";
+ else
+ {
+ colname = GetFdwOptionValue(InvalidOid, InvalidOid, relid,
+ attr, "colname");
+ if (colname == NULL)
+ colname = strVal(list_nth(rte->eref->colnames,
+ attr - 1));
+ }
+ newcolnames = lappend(newcolnames, makeString(colname));
+ }
+ rte->alias = makeAlias(relname, newcolnames);
+ }
+ rtable = lappend(rtable, rte);
+ }
+ context = deparse_context_for_rtelist(rtable);
+
+ /*
+ * deparse SELECT clause
+ *
+ * List attributes which are in either target list or local restriction.
+ * Unused attributes are replaced with a literal "NULL" for optimization.
+ *
+ * Note that nothing is added for dropped columns, though tuple constructor
+ * function requires entries for dropped columns. Such entries must be
+ * initialized with NULL before calling tuple constructor.
+ */
+ appendStringInfo(&sql, "SELECT ");
+ attr_used = list_union(attr_used, baserel->reltargetlist);
+ first = true;
+ for (attr = 1; attr <= baserel->max_attr; attr++)
+ {
+ RangeTblEntry *rte = root->simple_rte_array[baserel->relid];
+ Var *var = NULL;
+ ListCell *lc;
+
+ /* Ignore dropped attributes. */
+ if (get_rte_attribute_is_dropped(rte, attr))
+ continue;
+
+ if (!first)
+ appendStringInfo(&sql, ", ");
+ first = false;
+
+ /*
+ * We use linear search here, but it wouldn't be problem since
+ * attr_used seems to not become so large.
+ */
+ foreach (lc, attr_used)
+ {
+ var = lfirst(lc);
+ if (var->varattno == attr)
+ break;
+ var = NULL;
+ }
+ if (var != NULL)
+ appendStringInfo(&sql, "%s",
+ deparse_expression((Node *) var, context, false, false));
+ else
+ appendStringInfo(&sql, "NULL");
+ }
+ appendStringInfoChar(&sql, ' ');
+
+ /*
+ * deparse FROM clause, including alias if any
+ */
+ appendStringInfo(&sql, "FROM %s", foreign_relname.data);
+
+ elog(DEBUG3, "Remote SQL: %s", sql.data);
+ return sql.data;
+ }
+
diff --git a/contrib/pgsql_fdw/expected/pgsql_fdw.out b/contrib/pgsql_fdw/expected/pgsql_fdw.out
index ...7fe20e7 .
*** a/contrib/pgsql_fdw/expected/pgsql_fdw.out
--- b/contrib/pgsql_fdw/expected/pgsql_fdw.out
***************
*** 0 ****
--- 1,546 ----
+ -- ===================================================================
+ -- create FDW objects
+ -- ===================================================================
+ -- Clean up in case a prior regression run failed
+ -- Suppress NOTICE messages when roles don't exist
+ SET client_min_messages TO 'error';
+ DROP ROLE IF EXISTS pgsql_fdw_user;
+ RESET client_min_messages;
+ CREATE ROLE pgsql_fdw_user LOGIN SUPERUSER;
+ SET SESSION AUTHORIZATION 'pgsql_fdw_user';
+ CREATE EXTENSION pgsql_fdw;
+ CREATE SERVER loopback1 FOREIGN DATA WRAPPER pgsql_fdw;
+ CREATE SERVER loopback2 FOREIGN DATA WRAPPER pgsql_fdw
+ OPTIONS (dbname 'contrib_regression');
+ CREATE USER MAPPING FOR public SERVER loopback1
+ OPTIONS (user 'value', password 'value');
+ CREATE USER MAPPING FOR pgsql_fdw_user SERVER loopback2;
+ CREATE FOREIGN TABLE ft1 (
+ c0 int,
+ c1 int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10)
+ ) SERVER loopback2;
+ ALTER FOREIGN TABLE ft1 DROP COLUMN c0;
+ CREATE FOREIGN TABLE ft2 (
+ c0 int,
+ c1 int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10)
+ ) SERVER loopback2;
+ ALTER FOREIGN TABLE ft2 DROP COLUMN c0;
+ -- ===================================================================
+ -- create objects used through FDW
+ -- ===================================================================
+ CREATE SCHEMA "S 1";
+ CREATE TABLE "S 1"."T 1" (
+ "C 1" int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10),
+ CONSTRAINT t1_pkey PRIMARY KEY ("C 1")
+ );
+ NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "t1_pkey" for table "T 1"
+ CREATE TABLE "S 1"."T 2" (
+ c1 int NOT NULL,
+ c2 text,
+ CONSTRAINT t2_pkey PRIMARY KEY (c1)
+ );
+ NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "t2_pkey" for table "T 2"
+ BEGIN;
+ TRUNCATE "S 1"."T 1";
+ INSERT INTO "S 1"."T 1"
+ SELECT id,
+ id % 10,
+ to_char(id, 'FM00000'),
+ '1970-01-01'::timestamptz + ((id % 100) || ' days')::interval,
+ '1970-01-01'::timestamp + ((id % 100) || ' days')::interval,
+ id % 10,
+ id % 10
+ FROM generate_series(1, 1000) id;
+ TRUNCATE "S 1"."T 2";
+ INSERT INTO "S 1"."T 2"
+ SELECT id,
+ 'AAA' || to_char(id, 'FM000')
+ FROM generate_series(1, 100) id;
+ COMMIT;
+ -- ===================================================================
+ -- tests for pgsql_fdw_validator
+ -- ===================================================================
+ ALTER FOREIGN DATA WRAPPER pgsql_fdw OPTIONS (host 'value'); -- ERROR
+ ERROR: invalid option "host"
+ HINT: Valid options in this context are:
+ -- requiressl, krbsrvname and gsslib are omitted because they depend on
+ -- configure option
+ ALTER SERVER loopback1 OPTIONS (
+ authtype 'value',
+ service 'value',
+ connect_timeout 'value',
+ dbname 'value',
+ host 'value',
+ hostaddr 'value',
+ port 'value',
+ --client_encoding 'value',
+ tty 'value',
+ options 'value',
+ application_name 'value',
+ --fallback_application_name 'value',
+ keepalives 'value',
+ keepalives_idle 'value',
+ keepalives_interval 'value',
+ -- requiressl 'value',
+ sslmode 'value',
+ sslcert 'value',
+ sslkey 'value',
+ sslrootcert 'value',
+ sslcrl 'value'
+ --requirepeer 'value',
+ -- krbsrvname 'value',
+ -- gsslib 'value',
+ --replication 'value'
+ );
+ ALTER SERVER loopback1 OPTIONS (user 'value'); -- ERROR
+ ERROR: invalid option "user"
+ HINT: Valid options in this context are: authtype, service, connect_timeout, dbname, host, hostaddr, port, tty, options, application_name, keepalives, keepalives_idle, keepalives_interval, keepalives_count, sslmode, sslcert, sslkey, sslrootcert, sslcrl, requirepeer, fetch_count
+ ALTER SERVER loopback2 OPTIONS (ADD fetch_count '2');
+ ALTER USER MAPPING FOR public SERVER loopback1
+ OPTIONS (DROP user, DROP password);
+ ALTER USER MAPPING FOR public SERVER loopback1
+ OPTIONS (host 'value'); -- ERROR
+ ERROR: invalid option "host"
+ HINT: Valid options in this context are: user, password
+ ALTER FOREIGN TABLE ft1 OPTIONS (nspname 'S 1', relname 'T 1');
+ ALTER FOREIGN TABLE ft2 OPTIONS (nspname 'S 1', relname 'T 1', fetch_count '100');
+ ALTER FOREIGN TABLE ft1 OPTIONS (invalid 'value'); -- ERROR
+ ERROR: invalid option "invalid"
+ HINT: Valid options in this context are: nspname, relname, fetch_count
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count 'a'); -- ERROR
+ ERROR: invalid value for fetch_count: "a"
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count '0'); -- ERROR
+ ERROR: invalid value for fetch_count: "0"
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count '-1'); -- ERROR
+ ERROR: invalid value for fetch_count: "-1"
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (invalid 'value'); -- ERROR
+ ERROR: invalid option "invalid"
+ HINT: Valid options in this context are: colname
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (colname 'C 1');
+ ALTER FOREIGN TABLE ft2 ALTER COLUMN c1 OPTIONS (colname 'C 1');
+ \dew+
+ List of foreign-data wrappers
+ Name | Owner | Handler | Validator | Access privileges | FDW Options | Description
+ -----------+----------------+-------------------+---------------------+-------------------+-------------+-------------
+ pgsql_fdw | pgsql_fdw_user | pgsql_fdw_handler | pgsql_fdw_validator | | |
+ (1 row)
+
+ \des+
+ List of foreign servers
+ Name | Owner | Foreign-data wrapper | Access privileges | Type | Version | FDW Options | Description
+ -----------+----------------+----------------------+-------------------+------+---------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+-------------
+ loopback1 | pgsql_fdw_user | pgsql_fdw | | | | (authtype 'value', service 'value', connect_timeout 'value', dbname 'value', host 'value', hostaddr 'value', port 'value', tty 'value', options 'value', application_name 'value', keepalives 'value', keepalives_idle 'value', keepalives_interval 'value', sslmode 'value', sslcert 'value', sslkey 'value', sslrootcert 'value', sslcrl 'value') |
+ loopback2 | pgsql_fdw_user | pgsql_fdw | | | | (dbname 'contrib_regression', fetch_count '2') |
+ (2 rows)
+
+ \deu+
+ List of user mappings
+ Server | User name | FDW Options
+ -----------+----------------+-------------
+ loopback1 | public |
+ loopback2 | pgsql_fdw_user |
+ (2 rows)
+
+ \det+
+ List of foreign tables
+ Schema | Table | Server | FDW Options | Description
+ --------+-------+-----------+---------------------------------------------------+-------------
+ public | ft1 | loopback2 | (nspname 'S 1', relname 'T 1') |
+ public | ft2 | loopback2 | (nspname 'S 1', relname 'T 1', fetch_count '100') |
+ (2 rows)
+
+ -- ===================================================================
+ -- simple queries
+ -- ===================================================================
+ -- single table, with/without alias
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
+ QUERY PLAN
+ ------------------------------------------------------------------------------------------------------------------------------
+ Limit
+ Output: c1, c2, c3, c4, c5, c6, c7
+ -> Sort
+ Output: c1, c2, c3, c4, c5, c6, c7
+ Sort Key: ft1.c3, ft1.c1
+ -> Foreign Scan on public.ft1
+ Output: c1, c2, c3, c4, c5, c6, c7
+ Remote SQL: DECLARE pgsql_fdw_cursor_0 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (8 rows)
+
+ SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ -----+----+-------+------------------------------+--------------------------+----+------------
+ 101 | 1 | 00101 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970 | 1 | 1
+ 102 | 2 | 00102 | Sat Jan 03 00:00:00 1970 PST | Sat Jan 03 00:00:00 1970 | 2 | 2
+ 103 | 3 | 00103 | Sun Jan 04 00:00:00 1970 PST | Sun Jan 04 00:00:00 1970 | 3 | 3
+ 104 | 4 | 00104 | Mon Jan 05 00:00:00 1970 PST | Mon Jan 05 00:00:00 1970 | 4 | 4
+ 105 | 5 | 00105 | Tue Jan 06 00:00:00 1970 PST | Tue Jan 06 00:00:00 1970 | 5 | 5
+ 106 | 6 | 00106 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6
+ 107 | 7 | 00107 | Thu Jan 08 00:00:00 1970 PST | Thu Jan 08 00:00:00 1970 | 7 | 7
+ 108 | 8 | 00108 | Fri Jan 09 00:00:00 1970 PST | Fri Jan 09 00:00:00 1970 | 8 | 8
+ 109 | 9 | 00109 | Sat Jan 10 00:00:00 1970 PST | Sat Jan 10 00:00:00 1970 | 9 | 9
+ 110 | 0 | 00110 | Sun Jan 11 00:00:00 1970 PST | Sun Jan 11 00:00:00 1970 | 0 | 0
+ (10 rows)
+
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+ QUERY PLAN
+ ------------------------------------------------------------------------------------------------------------------------------
+ Limit
+ Output: c1, c2, c3, c4, c5, c6, c7
+ -> Sort
+ Output: c1, c2, c3, c4, c5, c6, c7
+ Sort Key: t1.c3, t1.c1
+ -> Foreign Scan on public.ft1 t1
+ Output: c1, c2, c3, c4, c5, c6, c7
+ Remote SQL: DECLARE pgsql_fdw_cursor_2 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (8 rows)
+
+ SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ -----+----+-------+------------------------------+--------------------------+----+------------
+ 101 | 1 | 00101 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970 | 1 | 1
+ 102 | 2 | 00102 | Sat Jan 03 00:00:00 1970 PST | Sat Jan 03 00:00:00 1970 | 2 | 2
+ 103 | 3 | 00103 | Sun Jan 04 00:00:00 1970 PST | Sun Jan 04 00:00:00 1970 | 3 | 3
+ 104 | 4 | 00104 | Mon Jan 05 00:00:00 1970 PST | Mon Jan 05 00:00:00 1970 | 4 | 4
+ 105 | 5 | 00105 | Tue Jan 06 00:00:00 1970 PST | Tue Jan 06 00:00:00 1970 | 5 | 5
+ 106 | 6 | 00106 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6
+ 107 | 7 | 00107 | Thu Jan 08 00:00:00 1970 PST | Thu Jan 08 00:00:00 1970 | 7 | 7
+ 108 | 8 | 00108 | Fri Jan 09 00:00:00 1970 PST | Fri Jan 09 00:00:00 1970 | 8 | 8
+ 109 | 9 | 00109 | Sat Jan 10 00:00:00 1970 PST | Sat Jan 10 00:00:00 1970 | 9 | 9
+ 110 | 0 | 00110 | Sun Jan 11 00:00:00 1970 PST | Sun Jan 11 00:00:00 1970 | 0 | 0
+ (10 rows)
+
+ -- with WHERE clause
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
+ QUERY PLAN
+ ------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on public.ft1 t1
+ Output: c1, c2, c3, c4, c5, c6, c7
+ Filter: ((t1.c1 = 101) AND ((t1.c6)::text = '1'::text) AND (t1.c7 = '1'::bpchar))
+ Remote SQL: DECLARE pgsql_fdw_cursor_4 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (4 rows)
+
+ SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ -----+----+-------+------------------------------+--------------------------+----+------------
+ 101 | 1 | 00101 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970 | 1 | 1
+ (1 row)
+
+ -- aggregate
+ SELECT COUNT(*) FROM ft1 t1;
+ count
+ -------
+ 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
+ ----+----+-------+------------------------------+--------------------------+----+------------
+ 1 | 1 | 00001 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970 | 1 | 1
+ 2 | 2 | 00002 | Sat Jan 03 00:00:00 1970 PST | Sat Jan 03 00:00:00 1970 | 2 | 2
+ 3 | 3 | 00003 | Sun Jan 04 00:00:00 1970 PST | Sun Jan 04 00:00:00 1970 | 3 | 3
+ 4 | 4 | 00004 | Mon Jan 05 00:00:00 1970 PST | Mon Jan 05 00:00:00 1970 | 4 | 4
+ 5 | 5 | 00005 | Tue Jan 06 00:00:00 1970 PST | Tue Jan 06 00:00:00 1970 | 5 | 5
+ 6 | 6 | 00006 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6
+ 7 | 7 | 00007 | Thu Jan 08 00:00:00 1970 PST | Thu Jan 08 00:00:00 1970 | 7 | 7
+ 8 | 8 | 00008 | Fri Jan 09 00:00:00 1970 PST | Fri Jan 09 00:00:00 1970 | 8 | 8
+ 9 | 9 | 00009 | Sat Jan 10 00:00:00 1970 PST | Sat Jan 10 00:00:00 1970 | 9 | 9
+ 10 | 0 | 00010 | Sun Jan 11 00:00:00 1970 PST | Sun Jan 11 00:00:00 1970 | 0 | 0
+ (10 rows)
+
+ -- subquery+MAX
+ SELECT * FROM ft1 t1 WHERE t1.c3 = (SELECT MAX(c3) FROM ft2 t2) ORDER BY c1;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ ------+----+-------+------------------------------+--------------------------+----+------------
+ 1000 | 0 | 01000 | Thu Jan 01 00:00:00 1970 PST | Thu Jan 01 00:00:00 1970 | 0 | 0
+ (1 row)
+
+ -- used in CTE
+ WITH t1 AS (SELECT * FROM ft1 WHERE c1 <= 10) SELECT t2.c1, t2.c2, t2.c3, t2.c4 FROM t1, ft2 t2 WHERE t1.c1 = t2.c1 ORDER BY t1.c1;
+ c1 | c2 | c3 | c4
+ ----+----+-------+------------------------------
+ 1 | 1 | 00001 | Fri Jan 02 00:00:00 1970 PST
+ 2 | 2 | 00002 | Sat Jan 03 00:00:00 1970 PST
+ 3 | 3 | 00003 | Sun Jan 04 00:00:00 1970 PST
+ 4 | 4 | 00004 | Mon Jan 05 00:00:00 1970 PST
+ 5 | 5 | 00005 | Tue Jan 06 00:00:00 1970 PST
+ 6 | 6 | 00006 | Wed Jan 07 00:00:00 1970 PST
+ 7 | 7 | 00007 | Thu Jan 08 00:00:00 1970 PST
+ 8 | 8 | 00008 | Fri Jan 09 00:00:00 1970 PST
+ 9 | 9 | 00009 | Sat Jan 10 00:00:00 1970 PST
+ 10 | 0 | 00010 | Sun Jan 11 00:00:00 1970 PST
+ (10 rows)
+
+ -- fixed values
+ SELECT 'fixed', NULL FROM ft1 t1 WHERE c1 = 1;
+ ?column? | ?column?
+ ----------+----------
+ fixed |
+ (1 row)
+
+ -- user-defined operator/function
+ CREATE FUNCTION pgsql_fdw_abs(int) RETURNS int AS $$
+ BEGIN
+ RETURN abs($1);
+ END
+ $$ LANGUAGE plpgsql IMMUTABLE;
+ CREATE OPERATOR === (
+ LEFTARG = int,
+ RIGHTARG = int,
+ PROCEDURE = int4eq,
+ COMMUTATOR = ===,
+ NEGATOR = !==
+ );
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = pgsql_fdw_abs(t1.c2);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = pgsql_fdw_abs(c2))
+ Remote SQL: DECLARE pgsql_fdw_cursor_18 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 === t1.c2;
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 === c2)
+ Remote SQL: DECLARE pgsql_fdw_cursor_19 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = abs(t1.c2);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = abs(c2))
+ Remote SQL: DECLARE pgsql_fdw_cursor_20 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = t1.c2;
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = c2)
+ Remote SQL: DECLARE pgsql_fdw_cursor_21 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ -- ===================================================================
+ -- parameterized queries
+ -- ===================================================================
+ -- simple join
+ PREPARE st1(int, int) AS SELECT t1.c3, t2.c3 FROM ft1 t1, ft2 t2 WHERE t1.c1 = $1 AND t2.c1 = $2;
+ EXPLAIN (COSTS false) EXECUTE st1(1, 2);
+ QUERY PLAN
+ -----------------------------------------------------------------------------------------------------------------------------------------
+ Nested Loop
+ -> Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_22 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, NULL, NULL, NULL, NULL FROM "S 1"."T 1"
+ -> Materialize
+ -> Foreign Scan on ft2 t2
+ Filter: (c1 = 2)
+ Remote SQL: DECLARE pgsql_fdw_cursor_23 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, NULL, NULL, NULL, NULL FROM "S 1"."T 1"
+ (8 rows)
+
+ EXECUTE st1(1, 1);
+ c3 | c3
+ -------+-------
+ 00001 | 00001
+ (1 row)
+
+ EXECUTE st1(101, 101);
+ c3 | c3
+ -------+-------
+ 00101 | 00101
+ (1 row)
+
+ -- subquery using stable function (can't be pushed down)
+ PREPARE st2(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c4) = 6) ORDER BY c1;
+ EXPLAIN (COSTS false) EXECUTE st2(10, 20);
+ QUERY PLAN
+ ---------------------------------------------------------------------------------------------------------------------------------------------------
+ Sort
+ Sort Key: t1.c1
+ -> Hash Join
+ Hash Cond: (t1.c3 = t2.c3)
+ -> Foreign Scan on ft1 t1
+ Filter: (c1 < 20)
+ Remote SQL: DECLARE pgsql_fdw_cursor_28 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ -> Hash
+ -> HashAggregate
+ -> Foreign Scan on ft2 t2
+ Filter: ((c1 > 10) AND (date_part('dow'::text, c4) = 6::double precision))
+ Remote SQL: DECLARE pgsql_fdw_cursor_29 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, c4, NULL, NULL, NULL FROM "S 1"."T 1"
+ (12 rows)
+
+ EXECUTE st2(10, 20);
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ ----+----+-------+------------------------------+--------------------------+----+------------
+ 16 | 6 | 00016 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970 | 6 | 6
+ (1 row)
+
+ EXECUTE st1(101, 101);
+ c3 | c3
+ -------+-------
+ 00101 | 00101
+ (1 row)
+
+ -- subquery using immutable function (can be pushed down)
+ PREPARE st3(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c5) = 6) ORDER BY c1;
+ EXPLAIN (COSTS false) EXECUTE st3(10, 20);
+ QUERY PLAN
+ ---------------------------------------------------------------------------------------------------------------------------------------------------
+ Sort
+ Sort Key: t1.c1
+ -> Hash Join
+ Hash Cond: (t1.c3 = t2.c3)
+ -> Foreign Scan on ft1 t1
+ Filter: (c1 < 20)
+ Remote SQL: DECLARE pgsql_fdw_cursor_34 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ -> Hash
+ -> HashAggregate
+ -> Foreign Scan on ft2 t2
+ Filter: ((c1 > 10) AND (date_part('dow'::text, c5) = 6::double precision))
+ Remote SQL: DECLARE pgsql_fdw_cursor_35 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, NULL, c5, NULL, NULL FROM "S 1"."T 1"
+ (12 rows)
+
+ EXECUTE st3(10, 20);
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ ----+----+-------+------------------------------+--------------------------+----+------------
+ 16 | 6 | 00016 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970 | 6 | 6
+ (1 row)
+
+ EXECUTE st3(20, 30);
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ ----+----+-------+------------------------------+--------------------------+----+------------
+ 23 | 3 | 00023 | Sat Jan 24 00:00:00 1970 PST | Sat Jan 24 00:00:00 1970 | 3 | 3
+ (1 row)
+
+ -- custom plan should be chosen
+ PREPARE st4(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 = $1;
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_40 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_41 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_42 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_43 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_44 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_46 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ -- cleanup
+ DEALLOCATE st1;
+ DEALLOCATE st2;
+ DEALLOCATE st3;
+ DEALLOCATE st4;
+ -- ===================================================================
+ -- connection management
+ -- ===================================================================
+ SELECT srvname, usename FROM pgsql_fdw_connections;
+ srvname | usename
+ -----------+----------------
+ loopback2 | pgsql_fdw_user
+ (1 row)
+
+ SELECT pgsql_fdw_disconnect(srvid, usesysid) FROM pgsql_fdw_get_connections();
+ pgsql_fdw_disconnect
+ ----------------------
+ OK
+ (1 row)
+
+ SELECT srvname, usename FROM pgsql_fdw_connections;
+ srvname | usename
+ ---------+---------
+ (0 rows)
+
+ -- ===================================================================
+ -- cleanup
+ -- ===================================================================
+ DROP OPERATOR === (int, int) CASCADE;
+ DROP OPERATOR !== (int, int) CASCADE;
+ DROP FUNCTION pgsql_fdw_abs(int);
+ DROP SCHEMA "S 1" CASCADE;
+ NOTICE: drop cascades to 2 other objects
+ DETAIL: drop cascades to table "S 1"."T 1"
+ drop cascades to table "S 1"."T 2"
+ DROP EXTENSION pgsql_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 server loopback2
+ drop cascades to user mapping for pgsql_fdw_user
+ drop cascades to foreign table ft1
+ drop cascades to foreign table ft2
+ \c
+ DROP ROLE pgsql_fdw_user;
diff --git a/contrib/pgsql_fdw/option.c b/contrib/pgsql_fdw/option.c
index ...f0fd1fe .
*** a/contrib/pgsql_fdw/option.c
--- b/contrib/pgsql_fdw/option.c
***************
*** 0 ****
--- 1,246 ----
+ /*-------------------------------------------------------------------------
+ *
+ * option.c
+ * FDW option handling
+ *
+ * Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/option.c
+ *
+ *-------------------------------------------------------------------------
+ */
+ #include "postgres.h"
+
+ #include "access/reloptions.h"
+ #include "catalog/pg_foreign_data_wrapper.h"
+ #include "catalog/pg_foreign_server.h"
+ #include "catalog/pg_foreign_table.h"
+ #include "catalog/pg_user_mapping.h"
+ #include "fmgr.h"
+ #include "foreign/foreign.h"
+ #include "lib/stringinfo.h"
+ #include "miscadmin.h"
+
+ #include "pgsql_fdw.h"
+
+ /*
+ * SQL functions
+ */
+ extern Datum pgsql_fdw_validator(PG_FUNCTION_ARGS);
+ PG_FUNCTION_INFO_V1(pgsql_fdw_validator);
+
+ /*
+ * Describes the valid options for objects that use this wrapper.
+ */
+ typedef struct PgsqlFdwOption
+ {
+ const char *optname;
+ Oid optcontext; /* Oid of catalog in which options may appear */
+ bool is_libpq_opt; /* true if it's used in libpq */
+ } PgsqlFdwOption;
+
+ /*
+ * Valid options for pgsql_fdw.
+ */
+ static PgsqlFdwOption valid_options[] = {
+
+ /*
+ * Options for libpq connection.
+ * Note: This list should be updated along with PQconninfoOptions in
+ * interfaces/libpq/fe-connect.c, so the order is kept as is.
+ *
+ * Some useless libpq connection options are not accepted by pgsql_fdw:
+ * client_encoding: set to local database encoding automatically
+ * fallback_application_name: fixed to "pgsql_fdw"
+ * replication: pgsql_fdw never be replication client
+ */
+ {"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},
+ #ifdef NOT_USED
+ {"client_encoding", ForeignServerRelationId, true},
+ #endif
+ {"tty", ForeignServerRelationId, true},
+ {"options", ForeignServerRelationId, true},
+ {"application_name", ForeignServerRelationId, true},
+ #ifdef NOT_USED
+ {"fallback_application_name", ForeignServerRelationId, true},
+ #endif
+ {"keepalives", ForeignServerRelationId, true},
+ {"keepalives_idle", ForeignServerRelationId, true},
+ {"keepalives_interval", ForeignServerRelationId, true},
+ {"keepalives_count", ForeignServerRelationId, true},
+ #ifdef USE_SSL
+ {"requiressl", ForeignServerRelationId, true},
+ #endif
+ {"sslmode", ForeignServerRelationId, true},
+ {"sslcert", ForeignServerRelationId, true},
+ {"sslkey", ForeignServerRelationId, true},
+ {"sslrootcert", ForeignServerRelationId, true},
+ {"sslcrl", ForeignServerRelationId, true},
+ {"requirepeer", ForeignServerRelationId, true},
+ #if defined(KRB5) || defined(ENABLE_GSS) || defined(ENABLE_SSPI)
+ {"krbsrvname", ForeignServerRelationId, true},
+ #endif
+ #if defined(ENABLE_GSS) && defined(ENABLE_SSPI)
+ {"gsslib", ForeignServerRelationId, true},
+ #endif
+ #ifdef NOT_USED
+ {"replication", ForeignServerRelationId, true},
+ #endif
+
+ /*
+ * Options for translation of object names.
+ */
+ {"nspname", ForeignTableRelationId, false},
+ {"relname", ForeignTableRelationId, false},
+ {"colname", AttributeRelationId, false},
+
+ /*
+ * Options for cursor behavior.
+ * These options can be overridden by finer-grained objects.
+ */
+ {"fetch_count", ForeignTableRelationId, false},
+ {"fetch_count", ForeignServerRelationId, false},
+
+ /* Terminating entry --- MUST BE LAST */
+ {NULL, InvalidOid, false}
+ };
+
+ /*
+ * Helper functions
+ */
+ static bool is_valid_option(const char *optname, Oid context);
+
+ /*
+ * Validate the generic options given to a FOREIGN DATA WRAPPER, SERVER,
+ * USER MAPPING or FOREIGN TABLE that uses pgsql_fdw.
+ *
+ * Raise an ERROR if the option or its value is considered invalid.
+ */
+ Datum
+ pgsql_fdw_validator(PG_FUNCTION_ARGS)
+ {
+ List *options_list = untransformRelOptions(PG_GETARG_DATUM(0));
+ Oid catalog = PG_GETARG_OID(1);
+ ListCell *cell;
+
+ /*
+ * Check that only options supported by pgsql_fdw, and allowed for the
+ * current object type, are given.
+ */
+ foreach(cell, options_list)
+ {
+ DefElem *def = (DefElem *) lfirst(cell);
+
+ if (!is_valid_option(def->defname, catalog))
+ {
+ PgsqlFdwOption *opt;
+ StringInfoData buf;
+
+ /*
+ * Unknown option specified, complain about it. Provide a hint
+ * with list of valid options for the object.
+ */
+ initStringInfo(&buf);
+ for (opt = valid_options; opt->optname; opt++)
+ {
+ if (catalog == opt->optcontext)
+ appendStringInfo(&buf, "%s%s", (buf.len > 0) ? ", " : "",
+ opt->optname);
+ }
+
+ ereport(ERROR,
+ (errcode(ERRCODE_FDW_INVALID_OPTION_NAME),
+ errmsg("invalid option \"%s\"", def->defname),
+ errhint("Valid options in this context are: %s",
+ buf.data)));
+ }
+
+ /* fetch_count be positive digit number. */
+ if (strcmp(def->defname, "fetch_count") == 0)
+ {
+ long value;
+ char *p = NULL;
+
+ value = strtol(strVal(def->arg), &p, 10);
+ if (*p != '\0' || value < 1)
+ ereport(ERROR,
+ (errcode(ERRCODE_FDW_INVALID_ATTRIBUTE_VALUE),
+ errmsg("invalid value for %s: \"%s\"",
+ def->defname, strVal(def->arg))));
+ }
+ }
+
+ /*
+ * We don't care option-specific limitation here; they will be validated at
+ * the execution time.
+ */
+
+ PG_RETURN_VOID();
+ }
+
+ /*
+ * Check whether the given option is one of the valid pgsql_fdw options.
+ * context is the Oid of the catalog holding the object the option is for.
+ */
+ static bool
+ is_valid_option(const char *optname, Oid context)
+ {
+ PgsqlFdwOption *opt;
+
+ for (opt = valid_options; opt->optname; opt++)
+ {
+ if (context == opt->optcontext && strcmp(opt->optname, optname) == 0)
+ return true;
+ }
+ return false;
+ }
+
+ /*
+ * Check whether the given option is one of the valid libpq options.
+ * context is the Oid of the catalog holding the object the option is for.
+ */
+ static bool
+ is_libpq_option(const char *optname)
+ {
+ PgsqlFdwOption *opt;
+
+ for (opt = valid_options; opt->optname; opt++)
+ {
+ if (strcmp(opt->optname, optname) == 0 && opt->is_libpq_opt)
+ return true;
+ }
+ return false;
+ }
+
+ /*
+ * Generate key-value arrays which includes only libpq options from the list
+ * which contains any kind of options.
+ */
+ int
+ ExtractConnectionOptions(List *defelems, const char **keywords, const char **values)
+ {
+ ListCell *lc;
+ int i;
+
+ i = 0;
+ foreach(lc, defelems)
+ {
+ DefElem *d = (DefElem *) lfirst(lc);
+ if (is_libpq_option(d->defname))
+ {
+ keywords[i] = d->defname;
+ values[i] = strVal(d->arg);
+ i++;
+ }
+ }
+ return i;
+ }
diff --git a/contrib/pgsql_fdw/pgsql_fdw--1.0.sql b/contrib/pgsql_fdw/pgsql_fdw--1.0.sql
index ...b0ea2b2 .
*** a/contrib/pgsql_fdw/pgsql_fdw--1.0.sql
--- b/contrib/pgsql_fdw/pgsql_fdw--1.0.sql
***************
*** 0 ****
--- 1,39 ----
+ /* contrib/pgsql_fdw/pgsql_fdw--1.0.sql */
+
+ -- complain if script is sourced in psql, rather than via CREATE EXTENSION
+ \echo Use "CREATE EXTENSION pgsql_fdw" to load this file. \quit
+
+ CREATE FUNCTION pgsql_fdw_handler()
+ RETURNS fdw_handler
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C STRICT;
+
+ CREATE FUNCTION pgsql_fdw_validator(text[], oid)
+ RETURNS void
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C STRICT;
+
+ CREATE FOREIGN DATA WRAPPER pgsql_fdw
+ HANDLER pgsql_fdw_handler
+ VALIDATOR pgsql_fdw_validator;
+
+ /* connection management functions and view */
+ CREATE FUNCTION pgsql_fdw_get_connections(out srvid oid, out usesysid oid)
+ RETURNS SETOF record
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C STRICT;
+
+ CREATE FUNCTION pgsql_fdw_disconnect(oid, oid)
+ RETURNS text
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C STRICT;
+
+ CREATE VIEW pgsql_fdw_connections AS
+ SELECT c.srvid srvid,
+ s.srvname srvname,
+ c.usesysid usesysid,
+ pg_get_userbyid(c.usesysid) usename
+ FROM pgsql_fdw_get_connections() c
+ JOIN pg_catalog.pg_foreign_server s ON (s.oid = c.srvid);
+ GRANT SELECT ON pgsql_fdw_connections TO public;
+
diff --git a/contrib/pgsql_fdw/pgsql_fdw.c b/contrib/pgsql_fdw/pgsql_fdw.c
index ...b92258a .
*** a/contrib/pgsql_fdw/pgsql_fdw.c
--- b/contrib/pgsql_fdw/pgsql_fdw.c
***************
*** 0 ****
--- 1,895 ----
+ /*-------------------------------------------------------------------------
+ *
+ * pgsql_fdw.c
+ * foreign-data wrapper for remote PostgreSQL servers.
+ *
+ * Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/pgsql_fdw.c
+ *
+ *-------------------------------------------------------------------------
+ */
+ #include "postgres.h"
+ #include "fmgr.h"
+
+ #include "catalog/pg_foreign_server.h"
+ #include "catalog/pg_foreign_table.h"
+ #include "commands/explain.h"
+ #include "foreign/fdwapi.h"
+ #include "funcapi.h"
+ #include "miscadmin.h"
+ #include "nodes/nodeFuncs.h"
+ #include "optimizer/cost.h"
+ #include "optimizer/pathnode.h"
+ #include "utils/lsyscache.h"
+ #include "utils/memutils.h"
+ #include "utils/rel.h"
+
+ #include "pgsql_fdw.h"
+ #include "connection.h"
+
+ PG_MODULE_MAGIC;
+
+ /*
+ * Default fetch count for cursor. This can be overridden by fetch_count FDW
+ * option.
+ */
+ #define DEFAULT_FETCH_COUNT 10000
+
+ /*
+ * Cost to establish a connection.
+ * XXX: should be configurable per server?
+ */
+ #define CONNECTION_COSTS 100.0
+
+ /*
+ * Cost to transfer 1 byte from remote server.
+ * XXX: should be configurable per server?
+ */
+ #define TRANSFER_COSTS_PER_BYTE 0.001
+
+ /*
+ * Cursors which are used together in a local query require different name, so
+ * we use simple incremental name for that purpose. We don't care wrap around
+ * of cursor_id because it's hard to imagine that 2^32 cursors are used in a
+ * query.
+ */
+ #define CURSOR_NAME_FORMAT "pgsql_fdw_cursor_%u"
+ static uint32 cursor_id = 0;
+
+ /*
+ * Index of FDW-private items stored in FdwPlan.
+ */
+ enum FdwPrivateIndex {
+ FdwPrivateSelectSql,
+ FdwPrivateDeclareSql,
+ FdwPrivateFetchSql,
+ FdwPrivateResetSql,
+ FdwPrivateCloseSql,
+
+ /* # of elements stored in the list fdw_private */
+ FdwPrivateNum,
+ };
+
+ /*
+ * Describes an execution state of a foreign scan against a foreign table
+ * using pgsql_fdw.
+ */
+ typedef struct PgsqlFdwExecutionState
+ {
+ MemoryContext scan_cxt; /* context for per-scan lifespan data */
+
+ FdwPlan *fdwplan; /* FDW-specific planning information */
+ PGconn *conn; /* connection for the scan */
+
+ Oid *param_types; /* type array of external parameter */
+ const char **param_values; /* value array of external parameter */
+
+ int attnum; /* # of non-dropped attribute */
+ char **col_values; /* column value buffer */
+ AttInMetadata *attinmeta; /* attribute metadata */
+
+ Tuplestorestate *tuples; /* result of the scan */
+ } PgsqlFdwExecutionState;
+
+ /*
+ * SQL functions
+ */
+ extern Datum pgsql_fdw_handler(PG_FUNCTION_ARGS);
+ PG_FUNCTION_INFO_V1(pgsql_fdw_handler);
+
+ /*
+ * FDW callback routines
+ */
+ static FdwPlan *pgsqlPlanForeignScan(Oid foreigntableid,
+ PlannerInfo *root,
+ RelOptInfo *baserel);
+ static void pgsqlExplainForeignScan(ForeignScanState *node, ExplainState *es);
+ static void pgsqlBeginForeignScan(ForeignScanState *node, int eflags);
+ static TupleTableSlot *pgsqlIterateForeignScan(ForeignScanState *node);
+ static void pgsqlReScanForeignScan(ForeignScanState *node);
+ static void pgsqlEndForeignScan(ForeignScanState *node);
+
+ /*
+ * Helper functions
+ */
+ static void estimate_costs(PlannerInfo *root,
+ RelOptInfo *baserel,
+ const char *sql,
+ Oid serverid,
+ Cost *startup_cost,
+ Cost *total_cost);
+ static void execute_query(ForeignScanState *node);
+ static PGresult *fetch_result(ForeignScanState *node);
+ static void store_result(ForeignScanState *node, PGresult *res);
+ static bool contain_ext_param(Node *clause);
+ static bool contain_ext_param_walker(Node *node, void *context);
+
+ /* Exported functions, but not written in pgsql_fdw.h. */
+ void _PG_init(void);
+ void _PG_fini(void);
+
+ /*
+ * Module-specific initialization.
+ */
+ void
+ _PG_init(void)
+ {
+ }
+
+ /*
+ * Module-specific clean up.
+ */
+ void
+ _PG_fini(void)
+ {
+ }
+
+ /*
+ * The content of FdwRoutine of pgsql_fdw is never changed, so we use one
+ * static object for all scan.
+ */
+ static FdwRoutine fdwroutine = {
+
+ /* Node type */
+ T_FdwRoutine,
+
+ /* Required handler functions. */
+ pgsqlPlanForeignScan,
+ pgsqlExplainForeignScan,
+ pgsqlBeginForeignScan,
+ pgsqlIterateForeignScan,
+ pgsqlReScanForeignScan,
+ pgsqlEndForeignScan,
+
+ /* Optional handler functions. */
+ };
+
+ /*
+ * Foreign-data wrapper handler function: return a struct with pointers
+ * to my callback routines.
+ */
+ Datum
+ pgsql_fdw_handler(PG_FUNCTION_ARGS)
+ {
+ PG_RETURN_POINTER(&fdwroutine);
+ }
+
+ /*
+ * pgsqlPlanForeignScan
+ * Create a FdwPlan for a scan on the foreign table
+ */
+ static FdwPlan *
+ pgsqlPlanForeignScan(Oid foreigntableid,
+ PlannerInfo *root,
+ RelOptInfo *baserel)
+ {
+ char name[128]; /* must be larger than format + 10 */
+ StringInfoData cursor;
+ const char *fetch_count_str;
+ int fetch_count = DEFAULT_FETCH_COUNT;
+ char *sql;
+ FdwPlan *fdwplan;
+ List *fdw_private = NIL;
+ ForeignTable *table;
+ ForeignServer *server;
+
+ /* Construct FdwPlan with cost estimates */
+ fdwplan = makeNode(FdwPlan);
+ sql = deparseSql(foreigntableid, root, baserel);
+ table = GetForeignTable(foreigntableid);
+ server = GetForeignServer(table->serverid);
+ estimate_costs(root, baserel, sql, server->serverid,
+ &fdwplan->startup_cost, &fdwplan->total_cost);
+
+ /*
+ * Store plain SELECT statement in private area of FdwPlan. This will be
+ * used for executing remote query and explaining scan.
+ */
+ fdw_private = list_make1(makeString(sql));
+
+ /* Use specified fetch_count instead of default value, if any. */
+ fetch_count_str = GetFdwOptionValue(InvalidOid, InvalidOid, foreigntableid,
+ InvalidAttrNumber, "fetch_count");
+ if (fetch_count_str != NULL)
+ fetch_count = strtol(fetch_count_str, NULL, 10);
+ elog(DEBUG1, "relid=%u fetch_count=%d", foreigntableid, fetch_count);
+
+ /*
+ * We store some more information in FdwPlan to pass them beyond the
+ * boundary between planner and executor. Finally FdwPlan using cursor
+ * would hold items below:
+ *
+ * 1) plain SELECT statement (already added above)
+ * 2) SQL statement used to declare cursor
+ * 3) SQL statement used to fetch rows from cursor
+ * 4) SQL statement used to reset cursor
+ * 5) SQL statement used to close cursor
+ *
+ * These items are indexed with the enum FdwPrivateIndex, so an item
+ * can be accessed directly via list_nth(). For example of FETCH
+ * statement:
+ * list_nth(fdw_private, FdwPrivateFetchSql)
+ */
+
+ /* Construct cursor name from sequential value */
+ sprintf(name, CURSOR_NAME_FORMAT, cursor_id++);
+
+ /* Construct statement to declare cursor */
+ initStringInfo(&cursor);
+ appendStringInfo(&cursor, "DECLARE %s SCROLL CURSOR FOR %s", name, sql);
+ fdw_private = lappend(fdw_private, makeString(cursor.data));
+
+ /* Construct statement to fetch rows from cursor */
+ initStringInfo(&cursor);
+ appendStringInfo(&cursor, "FETCH %d FROM %s", fetch_count, name);
+ fdw_private = lappend(fdw_private, makeString(cursor.data));
+
+ /* Construct statement to reset cursor */
+ initStringInfo(&cursor);
+ appendStringInfo(&cursor, "MOVE ABSOLUTE 0 FROM %s", name);
+ fdw_private = lappend(fdw_private, makeString(cursor.data));
+
+ /* Construct statement to close cursor */
+ initStringInfo(&cursor);
+ appendStringInfo(&cursor, "CLOSE %s", name);
+ fdw_private = lappend(fdw_private, makeString(cursor.data));
+
+ /* Store FDW private information into FdwPlan */
+ fdwplan->fdw_private = fdw_private;
+
+ return fdwplan;
+ }
+
+ /*
+ * pgsqlExplainForeignScan
+ * Produce extra output for EXPLAIN
+ */
+ static void
+ pgsqlExplainForeignScan(ForeignScanState *node, ExplainState *es)
+ {
+ FdwPlan *fdwplan;
+ char *sql;
+
+ fdwplan = ((ForeignScan *) node->ss.ps.plan)->fdwplan;
+ sql = strVal(list_nth(fdwplan->fdw_private, FdwPrivateDeclareSql));
+ ExplainPropertyText("Remote SQL", sql, es);
+ }
+
+ /*
+ * pgsqlBeginForeignScan
+ * Initiate access to a foreign PostgreSQL table.
+ */
+ static void
+ pgsqlBeginForeignScan(ForeignScanState *node, int eflags)
+ {
+ PgsqlFdwExecutionState *festate;
+ PGconn *conn;
+ Oid relid;
+ ForeignTable *table;
+ ForeignServer *server;
+ UserMapping *user;
+ TupleTableSlot *slot = node->ss.ss_ScanTupleSlot;
+
+ /*
+ * Do nothing in EXPLAIN (no ANALYZE) case. node->fdw_state stays NULL.
+ */
+ if (eflags & EXEC_FLAG_EXPLAIN_ONLY)
+ return;
+
+ /*
+ * Save state in node->fdw_state.
+ */
+ festate = (PgsqlFdwExecutionState *) palloc(sizeof(PgsqlFdwExecutionState));
+ festate->fdwplan = ((ForeignScan *) node->ss.ps.plan)->fdwplan;
+
+ /*
+ * Create context for per-scan tuplestore under per-query context.
+ */
+ festate->scan_cxt = AllocSetContextCreate(node->ss.ps.state->es_query_cxt,
+ "pgsql_fdw",
+ ALLOCSET_DEFAULT_MINSIZE,
+ ALLOCSET_DEFAULT_INITSIZE,
+ ALLOCSET_DEFAULT_MAXSIZE);
+
+ /*
+ * Get connection to the foreign server. Connection manager would
+ * establish new connection if necessary.
+ */
+ relid = RelationGetRelid(node->ss.ss_currentRelation);
+ table = GetForeignTable(relid);
+ server = GetForeignServer(table->serverid);
+ user = GetUserMapping(GetOuterUserId(), server->serverid);
+ conn = GetConnection(server, user, true);
+ festate->conn = conn;
+
+ /* Result will be filled in first Iterate call. */
+ festate->tuples = NULL;
+
+ /* Allocate buffers for column values. */
+ {
+ TupleDesc tupdesc = slot->tts_tupleDescriptor;
+ festate->col_values = palloc(sizeof(char *) * tupdesc->natts);
+ festate->attinmeta = TupleDescGetAttInMetadata(tupdesc);
+ }
+
+ /* Allocate buffers for query parameters. */
+ {
+ ParamListInfo params = node->ss.ps.state->es_param_list_info;
+ int numParams = params ? params->numParams : 0;
+
+ if (numParams > 0)
+ {
+ festate->param_types = palloc0(sizeof(Oid) * numParams);
+ festate->param_values = palloc0(sizeof(char *) * numParams);
+ }
+ else
+ {
+ festate->param_types = NULL;
+ festate->param_values = NULL;
+ }
+ }
+
+ /* Store FDW-specific state into ForeignScanState */
+ node->fdw_state = (void *) festate;
+
+ return;
+ }
+
+ /*
+ * pgsqlIterateForeignScan
+ * Retrieve next row from the result set, or clear tuple slot to indicate
+ * EOF.
+ *
+ * Note that using per-query context when retrieving tuples from
+ * tuplestore to ensure that returned tuples can survive until next
+ * iteration because the tuple is released implicitly via ExecClearTuple.
+ * Retrieving a tuple from tuplestore in CurrentMemoryContext (it's a
+ * per-tuple context), ExecClearTuple will free dangling pointer.
+ */
+ static TupleTableSlot *
+ pgsqlIterateForeignScan(ForeignScanState *node)
+ {
+ PgsqlFdwExecutionState *festate;
+ TupleTableSlot *slot = node->ss.ss_ScanTupleSlot;
+ PGresult *res;
+ MemoryContext oldcontext = CurrentMemoryContext;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+
+ /*
+ * If this is the first call after Begin, we need to execute remote query.
+ * If the query needs cursor, we declare a cursor at first call and fetch
+ * from it in later calls.
+ */
+ if (festate->tuples == NULL)
+ execute_query(node);
+
+ /*
+ * If enough tuples are left in tuplestore, just return next tuple from it.
+ *
+ * It is necessary to switch to per-scan context to make returned tuple
+ * valid until next IterateForeignScan call, because it will be released
+ * with ExecClearTuple then. Otherwise, picked tuple is allocated in
+ * per-tuple context, and double-free of that tuple might happen.
+ */
+ MemoryContextSwitchTo(festate->scan_cxt);
+ if (tuplestore_gettupleslot(festate->tuples, true, false, slot))
+ {
+ MemoryContextSwitchTo(oldcontext);
+ return slot;
+ }
+ MemoryContextSwitchTo(oldcontext);
+
+ /*
+ * Here we need to clear partial result and fetch next bunch of tuples from
+ * from the cursor for the scan. If the fetch returns no tuple, the scan
+ * has reached the end.
+ */
+ res = fetch_result(node);
+ PG_TRY();
+ {
+ store_result(node, res);
+ PQclear(res);
+ res = NULL;
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+
+ /*
+ * If we got more tuples from the server cursor, return next tuple from
+ * tuplestore.
+ */
+ MemoryContextSwitchTo(festate->scan_cxt);
+ if (tuplestore_gettupleslot(festate->tuples, true, false, slot))
+ {
+ MemoryContextSwitchTo(oldcontext);
+ return slot;
+ }
+ MemoryContextSwitchTo(oldcontext);
+
+ /* We don't have any result even in remote server cursor. */
+ ExecClearTuple(slot);
+ return slot;
+ }
+
+ /*
+ * pgsqlReScanForeignScan
+ * - Restart this scan by resetting fetch location.
+ */
+ static void
+ pgsqlReScanForeignScan(ForeignScanState *node)
+ {
+ List *fdw_private;
+ char *sql;
+ PGconn *conn;
+ PGresult *res;
+ PgsqlFdwExecutionState *festate;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+
+ /* If we have not opened cursor yet, nothing to do. */
+ if (festate->tuples == NULL)
+ return;
+
+ /* Discard fetch results if any. */
+ tuplestore_clear(festate->tuples);
+
+ /* Reset cursor */
+ fdw_private = festate->fdwplan->fdw_private;
+ conn = festate->conn;
+ sql = strVal(list_nth(fdw_private, FdwPrivateResetSql));
+ res = PQexec(conn, sql);
+ PG_TRY();
+ {
+ if (PQresultStatus(res) != PGRES_COMMAND_OK)
+ {
+ ereport(ERROR,
+ (errmsg("could not rewind cursor"),
+ errdetail("%s", PQerrorMessage(conn)),
+ errhint("%s", sql)));
+ }
+ PQclear(res);
+ res = NULL;
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+ }
+
+ /*
+ * pgsqlEndForeignScan
+ * Finish scanning foreign table and dispose objects used for this scan
+ */
+ static void
+ pgsqlEndForeignScan(ForeignScanState *node)
+ {
+ List *fdw_private;
+ char *sql;
+ PGconn *conn;
+ PGresult *res;
+ PgsqlFdwExecutionState *festate;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+
+ /* if festate is NULL, we are in EXPLAIN; nothing to do */
+ if (festate == NULL)
+ return;
+
+ /* If we have not opened cursor yet, nothing to do. */
+ if (festate->tuples == NULL)
+ return;
+
+ /* Discard fetch results */
+ tuplestore_end(festate->tuples);
+ festate->tuples = NULL;
+
+ /* Close cursor */
+ fdw_private = festate->fdwplan->fdw_private;
+ conn = festate->conn;
+ sql = strVal(list_nth(fdw_private, FdwPrivateCloseSql));
+ res = PQexec(conn, sql);
+ PG_TRY();
+ {
+ if (PQresultStatus(res) != PGRES_COMMAND_OK)
+ {
+ ereport(ERROR,
+ (errmsg("could not close cursor"),
+ errdetail("%s", PQerrorMessage(conn)),
+ errhint("%s", sql)));
+ }
+ PQclear(res);
+ res = NULL;
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+
+ ReleaseConnection(festate->conn);
+
+ MemoryContextDelete(festate->scan_cxt);
+ festate->scan_cxt = NULL;
+ }
+
+ /*
+ * Estimate costs of scanning a foreign table.
+ */
+ static void
+ estimate_costs(PlannerInfo *root, RelOptInfo *baserel,
+ const char *sql, Oid serverid,
+ Cost *startup_cost, Cost *total_cost)
+ {
+ ListCell *lc;
+ ForeignServer *server;
+ UserMapping *user;
+ PGconn *conn = NULL;
+ PGresult *res = NULL;
+ StringInfoData buf;
+ char *plan;
+ char *p;
+ int n;
+
+ /*
+ * If the baserestrictinfo contains any Param node with paramkind
+ * PARAM_EXTERNAL, we need result of EXPLAIN for EXECUTE statement, not for
+ * simple SELECT statement. However, we can't get actual parameter values
+ * here, so we use fixed and large costs as second best so that planner
+ * tend to choose custom plan.
+ *
+ * See comments in plancache.c for details of custom plan.
+ */
+ foreach(lc, baserel->baserestrictinfo)
+ {
+ RestrictInfo *rs = (RestrictInfo *) lfirst(lc);
+ if (contain_ext_param((Node *) rs->clause))
+ {
+ *startup_cost = CONNECTION_COSTS;
+ *total_cost = 10000; /* groundless large costs */
+
+ return;
+ }
+ }
+
+ /*
+ * Get connection to the foreign server. Connection manager would
+ * establish new connection if necessary.
+ */
+ server = GetForeignServer(serverid);
+ user = GetUserMapping(GetOuterUserId(), server->serverid);
+ conn = GetConnection(server, user, false);
+ initStringInfo(&buf);
+ appendStringInfo(&buf, "EXPLAIN %s", sql);
+
+ PG_TRY();
+ {
+ res = PQexec(conn, buf.data);
+ if (PQresultStatus(res) != PGRES_TUPLES_OK || PQntuples(res) == 0)
+ {
+ char *msg;
+
+ msg = pstrdup(PQerrorMessage(conn));
+ ereport(ERROR,
+ (errmsg("could not execute EXPLAIN for cost estimation"),
+ errdetail("%s", msg),
+ errhint("%s", sql)));
+ }
+
+ /*
+ * Find estimation portion from top plan node. Here we search opening
+ * parentheses from the end of the line to avoid finding unexpected
+ * parentheses.
+ */
+ plan = PQgetvalue(res, 0, 0);
+ p = strrchr(plan, '(');
+ if (p == NULL)
+ elog(ERROR, "wrong EXPLAIN output: %s", plan);
+ n = sscanf(p,
+ "(cost=%lf..%lf rows=%lf width=%d)",
+ startup_cost, total_cost, &baserel->rows, &baserel->width);
+ if (n != 4)
+ elog(ERROR, "could not get estimation from EXPLAIN output");
+
+ PQclear(res);
+ res = NULL;
+ ReleaseConnection(conn);
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+
+ /*
+ * TODO Selectivity of quals which are NOT pushed down should be also
+ * considered.
+ */
+
+ /* add cost to establish connection. */
+ *startup_cost += CONNECTION_COSTS;
+ *total_cost += CONNECTION_COSTS;
+
+ /* add cost to transfer result. */
+ *total_cost += TRANSFER_COSTS_PER_BYTE * baserel->width * baserel->tuples;
+ *total_cost += cpu_tuple_cost * baserel->tuples;
+ }
+
+ /*
+ * Execute remote query with current parameters.
+ */
+ static void
+ execute_query(ForeignScanState *node)
+ {
+ FdwPlan *fdwplan;
+ PgsqlFdwExecutionState *festate;
+ ParamListInfo params = node->ss.ps.state->es_param_list_info;
+ int numParams = params ? params->numParams : 0;
+ Oid *types = NULL;
+ const char **values = NULL;
+ char *sql;
+ PGconn *conn;
+ PGresult *res;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+ types = festate->param_types;
+ values = festate->param_values;
+
+ /*
+ * Construct parameter array in text format. We don't release memory for
+ * the arrays explicitly, because the memory usage would not be very large,
+ * and anyway they will be released in context cleanup.
+ */
+ if (numParams > 0)
+ {
+ int i;
+
+ 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;
+
+ getTypeOutputInfo(types[i], &out_func_oid, &isvarlena);
+ fmgr_info(out_func_oid, &func);
+ values[i] = OutputFunctionCall(&func, params->params[i].value);
+ }
+ }
+ }
+
+ /*
+ * Execute remote query with parameters.
+ */
+ conn = festate->conn;
+ fdwplan = ((ForeignScan *) node->ss.ps.plan)->fdwplan;
+ sql = strVal(list_nth(fdwplan->fdw_private, FdwPrivateDeclareSql));
+ res = PQexecParams(conn, sql, numParams, types, values, NULL, NULL, 0);
+ PG_TRY();
+ {
+ /*
+ * If the query has failed, reporting details is enough here.
+ * Connection(s) which are used by this query (at least used by
+ * pgsql_fdw) will be cleaned up by the foreign connection manager.
+ */
+ if (PQresultStatus(res) != PGRES_COMMAND_OK)
+ {
+ ereport(ERROR,
+ (errmsg("could not declare cursor"),
+ errdetail("%s", PQerrorMessage(conn)),
+ errhint("%s", sql)));
+ }
+
+ /* Discard result of DECLARE statement. */
+ PQclear(res);
+ res = NULL;
+
+ /* Fetch first bunch of the result and store them into tuplestore. */
+ res = fetch_result(node);
+ store_result(node, res);
+ PQclear(res);
+ res = NULL;
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+ }
+
+ /*
+ * Fetch next partial result from remote server.
+ */
+ static PGresult *
+ fetch_result(ForeignScanState *node)
+ {
+ PgsqlFdwExecutionState *festate;
+ List *fdw_private;
+ char *sql;
+ PGconn *conn;
+ PGresult *res;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+
+ /* Retrieve information for fetching result. */
+ fdw_private = festate->fdwplan->fdw_private;
+ sql = strVal(list_nth(fdw_private, FdwPrivateFetchSql));
+ conn = festate->conn;
+
+ /*
+ * Fetch result from remote server. In error case, we must release
+ * PGresult in this function to avoid memory leak because caller can't
+ * get the reference.
+ */
+ res = PQexec(conn, sql);
+ if (PQresultStatus(res) != PGRES_TUPLES_OK)
+ {
+ PQclear(res);
+ ereport(ERROR,
+ (errmsg("could not fetch rows from foreign server"),
+ errdetail("%s", PQerrorMessage(conn)),
+ errhint("%s", sql)));
+ }
+
+ return res;
+ }
+
+ /*
+ * Create tuples from PGresult and store them into tuplestore.
+ *
+ * Caller must use PG_TRY block to catch exception and release PGresult
+ * surely.
+ */
+ static void
+ store_result(ForeignScanState *node, PGresult *res)
+ {
+ int rows;
+ int row;
+ int i;
+ int nfields;
+ int attnum; /* number of non-dropped columns */
+ Form_pg_attribute *attrs;
+ TupleTableSlot *slot = node->ss.ss_ScanTupleSlot;
+ TupleDesc tupdesc = slot->tts_tupleDescriptor;
+ PgsqlFdwExecutionState *festate;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+ rows = PQntuples(res);
+ nfields = PQnfields(res);
+ attrs = tupdesc->attrs;
+
+ /* First, ensure that the tuplestore is empty. */
+ if (festate->tuples == NULL)
+ {
+ MemoryContext oldcontext = CurrentMemoryContext;
+
+ /*
+ * Create tuplestore to store result of the query in per-query context.
+ * Note that we use this memory context to avoid memory leak in error
+ * cases.
+ */
+ MemoryContextSwitchTo(festate->scan_cxt);
+ festate->tuples = tuplestore_begin_heap(false, false, work_mem);
+ MemoryContextSwitchTo(oldcontext);
+ }
+ else
+ {
+ /* We already have tuplestore, just need to clear contents of it. */
+ tuplestore_clear(festate->tuples);
+ }
+
+ /* 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)));
+
+ /* put a tuples into the slot */
+ for (row = 0; row < rows; row++)
+ {
+ int j;
+ HeapTuple tuple;
+
+ for (i = 0, j = 0; i < tupdesc->natts; i++)
+ {
+ /* skip dropped columns. */
+ if (attrs[i]->attisdropped)
+ {
+ festate->col_values[i] = NULL;
+ continue;
+ }
+
+ if (PQgetisnull(res, row, j))
+ festate->col_values[i] = NULL;
+ else
+ festate->col_values[i] = PQgetvalue(res, row, 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(festate->attinmeta, festate->col_values);
+ tuplestore_puttuple(festate->tuples, tuple);
+ }
+
+ tuplestore_donestoring(festate->tuples);
+ }
+
+ /*
+ * contain_ext_param
+ * Recursively search for Param nodes within a clause.
+ *
+ * Returns true if any parameter reference node with relkind PARAM_EXTERN
+ * found.
+ *
+ * This does not descend into subqueries, and so should be used only after
+ * reduction of sublinks to subplans, or in contexts where it's known there
+ * are no subqueries. There mustn't be outer-aggregate references either.
+ *
+ * XXX: These functions could be in core, src/backend/optimizer/util/clauses.c.
+ */
+ static bool
+ contain_ext_param(Node *clause)
+ {
+ return contain_ext_param_walker(clause, NULL);
+ }
+
+ static bool
+ contain_ext_param_walker(Node *node, void *context)
+ {
+ if (node == NULL)
+ return false;
+ if (IsA(node, Param))
+ {
+ Param *param = (Param *) node;
+
+ if (param->paramkind == PARAM_EXTERN)
+ return true; /* abort the tree traversal and return true */
+ }
+ return expression_tree_walker(node, contain_ext_param_walker, context);
+ }
diff --git a/contrib/pgsql_fdw/pgsql_fdw.control b/contrib/pgsql_fdw/pgsql_fdw.control
index ...0a9c8f4 .
*** a/contrib/pgsql_fdw/pgsql_fdw.control
--- b/contrib/pgsql_fdw/pgsql_fdw.control
***************
*** 0 ****
--- 1,5 ----
+ # pgsql_fdw extension
+ comment = 'foreign-data wrapper for remote PostgreSQL servers'
+ default_version = '1.0'
+ module_pathname = '$libdir/pgsql_fdw'
+ relocatable = true
diff --git a/contrib/pgsql_fdw/pgsql_fdw.h b/contrib/pgsql_fdw/pgsql_fdw.h
index ...f6394ce .
*** a/contrib/pgsql_fdw/pgsql_fdw.h
--- b/contrib/pgsql_fdw/pgsql_fdw.h
***************
*** 0 ****
--- 1,28 ----
+ /*-------------------------------------------------------------------------
+ *
+ * pgsql_fdw.h
+ * foreign-data wrapper for remote PostgreSQL servers.
+ *
+ * Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/pgsql_fdw.h
+ *
+ *-------------------------------------------------------------------------
+ */
+
+ #ifndef PGSQL_FDW_H
+ #define PGSQL_FDW_H
+
+ #include "postgres.h"
+ #include "nodes/relation.h"
+
+ /* in option.c */
+ int ExtractConnectionOptions(List *defelems,
+ const char **keywords,
+ const char **values);
+
+ /* in deparse.c */
+ char *deparseSql(Oid relid, PlannerInfo *root, RelOptInfo *baserel);
+
+ #endif /* PGSQL_FDW_H */
diff --git a/contrib/pgsql_fdw/sql/pgsql_fdw.sql b/contrib/pgsql_fdw/sql/pgsql_fdw.sql
index ...42d26fd .
*** a/contrib/pgsql_fdw/sql/pgsql_fdw.sql
--- b/contrib/pgsql_fdw/sql/pgsql_fdw.sql
***************
*** 0 ****
--- 1,231 ----
+ -- ===================================================================
+ -- create FDW objects
+ -- ===================================================================
+
+ -- Clean up in case a prior regression run failed
+
+ -- Suppress NOTICE messages when roles don't exist
+ SET client_min_messages TO 'error';
+
+ DROP ROLE IF EXISTS pgsql_fdw_user;
+
+ RESET client_min_messages;
+
+ CREATE ROLE pgsql_fdw_user LOGIN SUPERUSER;
+ SET SESSION AUTHORIZATION 'pgsql_fdw_user';
+
+ CREATE EXTENSION pgsql_fdw;
+
+ CREATE SERVER loopback1 FOREIGN DATA WRAPPER pgsql_fdw;
+ CREATE SERVER loopback2 FOREIGN DATA WRAPPER pgsql_fdw
+ OPTIONS (dbname 'contrib_regression');
+
+ CREATE USER MAPPING FOR public SERVER loopback1
+ OPTIONS (user 'value', password 'value');
+ CREATE USER MAPPING FOR pgsql_fdw_user SERVER loopback2;
+
+ CREATE FOREIGN TABLE ft1 (
+ c0 int,
+ c1 int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10)
+ ) SERVER loopback2;
+ ALTER FOREIGN TABLE ft1 DROP COLUMN c0;
+
+ CREATE FOREIGN TABLE ft2 (
+ c0 int,
+ c1 int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10)
+ ) SERVER loopback2;
+ ALTER FOREIGN TABLE ft2 DROP COLUMN c0;
+
+ -- ===================================================================
+ -- create objects used through FDW
+ -- ===================================================================
+ CREATE SCHEMA "S 1";
+ CREATE TABLE "S 1"."T 1" (
+ "C 1" int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10),
+ CONSTRAINT t1_pkey PRIMARY KEY ("C 1")
+ );
+ CREATE TABLE "S 1"."T 2" (
+ c1 int NOT NULL,
+ c2 text,
+ CONSTRAINT t2_pkey PRIMARY KEY (c1)
+ );
+
+ BEGIN;
+ TRUNCATE "S 1"."T 1";
+ INSERT INTO "S 1"."T 1"
+ SELECT id,
+ id % 10,
+ to_char(id, 'FM00000'),
+ '1970-01-01'::timestamptz + ((id % 100) || ' days')::interval,
+ '1970-01-01'::timestamp + ((id % 100) || ' days')::interval,
+ id % 10,
+ id % 10
+ FROM generate_series(1, 1000) id;
+ TRUNCATE "S 1"."T 2";
+ INSERT INTO "S 1"."T 2"
+ SELECT id,
+ 'AAA' || to_char(id, 'FM000')
+ FROM generate_series(1, 100) id;
+ COMMIT;
+
+ -- ===================================================================
+ -- tests for pgsql_fdw_validator
+ -- ===================================================================
+ ALTER FOREIGN DATA WRAPPER pgsql_fdw OPTIONS (host 'value'); -- ERROR
+ -- requiressl, krbsrvname and gsslib are omitted because they depend on
+ -- configure option
+ ALTER SERVER loopback1 OPTIONS (
+ authtype 'value',
+ service 'value',
+ connect_timeout 'value',
+ dbname 'value',
+ host 'value',
+ hostaddr 'value',
+ port 'value',
+ --client_encoding 'value',
+ tty 'value',
+ options 'value',
+ application_name 'value',
+ --fallback_application_name 'value',
+ keepalives 'value',
+ keepalives_idle 'value',
+ keepalives_interval 'value',
+ -- requiressl 'value',
+ sslmode 'value',
+ sslcert 'value',
+ sslkey 'value',
+ sslrootcert 'value',
+ sslcrl 'value'
+ --requirepeer 'value',
+ -- krbsrvname 'value',
+ -- gsslib 'value',
+ --replication 'value'
+ );
+ ALTER SERVER loopback1 OPTIONS (user 'value'); -- ERROR
+ ALTER SERVER loopback2 OPTIONS (ADD fetch_count '2');
+ ALTER USER MAPPING FOR public SERVER loopback1
+ OPTIONS (DROP user, DROP password);
+ ALTER USER MAPPING FOR public SERVER loopback1
+ OPTIONS (host 'value'); -- ERROR
+ ALTER FOREIGN TABLE ft1 OPTIONS (nspname 'S 1', relname 'T 1');
+ ALTER FOREIGN TABLE ft2 OPTIONS (nspname 'S 1', relname 'T 1', fetch_count '100');
+ ALTER FOREIGN TABLE ft1 OPTIONS (invalid 'value'); -- ERROR
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count 'a'); -- ERROR
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count '0'); -- ERROR
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count '-1'); -- ERROR
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (invalid 'value'); -- ERROR
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (colname 'C 1');
+ ALTER FOREIGN TABLE ft2 ALTER COLUMN c1 OPTIONS (colname 'C 1');
+ \dew+
+ \des+
+ \deu+
+ \det+
+
+ -- ===================================================================
+ -- simple queries
+ -- ===================================================================
+ -- single table, with/without alias
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
+ SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+ SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+ -- with WHERE clause
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
+ SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
+ -- 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
+ SELECT * FROM ft1 t1 WHERE t1.c3 = (SELECT MAX(c3) FROM ft2 t2) ORDER BY c1;
+ -- used in CTE
+ WITH t1 AS (SELECT * FROM ft1 WHERE c1 <= 10) SELECT t2.c1, t2.c2, t2.c3, t2.c4 FROM t1, ft2 t2 WHERE t1.c1 = t2.c1 ORDER BY t1.c1;
+ -- fixed values
+ SELECT 'fixed', NULL FROM ft1 t1 WHERE c1 = 1;
+ -- user-defined operator/function
+ CREATE FUNCTION pgsql_fdw_abs(int) RETURNS int AS $$
+ BEGIN
+ RETURN abs($1);
+ END
+ $$ LANGUAGE plpgsql IMMUTABLE;
+ CREATE OPERATOR === (
+ LEFTARG = int,
+ RIGHTARG = int,
+ PROCEDURE = int4eq,
+ COMMUTATOR = ===,
+ NEGATOR = !==
+ );
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = pgsql_fdw_abs(t1.c2);
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 === t1.c2;
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = abs(t1.c2);
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = t1.c2;
+
+ -- ===================================================================
+ -- parameterized queries
+ -- ===================================================================
+ -- simple join
+ PREPARE st1(int, int) AS SELECT t1.c3, t2.c3 FROM ft1 t1, ft2 t2 WHERE t1.c1 = $1 AND t2.c1 = $2;
+ EXPLAIN (COSTS false) EXECUTE st1(1, 2);
+ EXECUTE st1(1, 1);
+ EXECUTE st1(101, 101);
+ -- subquery using stable function (can't be pushed down)
+ PREPARE st2(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c4) = 6) ORDER BY c1;
+ EXPLAIN (COSTS false) EXECUTE st2(10, 20);
+ EXECUTE st2(10, 20);
+ EXECUTE st1(101, 101);
+ -- subquery using immutable function (can be pushed down)
+ PREPARE st3(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c5) = 6) ORDER BY c1;
+ EXPLAIN (COSTS false) EXECUTE st3(10, 20);
+ EXECUTE st3(10, 20);
+ EXECUTE st3(20, 30);
+ -- custom plan should be chosen
+ PREPARE st4(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 = $1;
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ -- cleanup
+ DEALLOCATE st1;
+ DEALLOCATE st2;
+ DEALLOCATE st3;
+ DEALLOCATE st4;
+
+ -- ===================================================================
+ -- connection management
+ -- ===================================================================
+ SELECT srvname, usename FROM pgsql_fdw_connections;
+ SELECT pgsql_fdw_disconnect(srvid, usesysid) FROM pgsql_fdw_get_connections();
+ SELECT srvname, usename FROM pgsql_fdw_connections;
+
+ -- ===================================================================
+ -- cleanup
+ -- ===================================================================
+ DROP OPERATOR === (int, int) CASCADE;
+ DROP OPERATOR !== (int, int) CASCADE;
+ DROP FUNCTION pgsql_fdw_abs(int);
+ DROP SCHEMA "S 1" CASCADE;
+ DROP EXTENSION pgsql_fdw CASCADE;
+ \c
+ DROP ROLE pgsql_fdw_user;
diff --git a/doc/src/sgml/contrib.sgml b/doc/src/sgml/contrib.sgml
index d4da4ee..32056d1 100644
*** a/doc/src/sgml/contrib.sgml
--- b/doc/src/sgml/contrib.sgml
*************** CREATE EXTENSION <replaceable>module_nam
*** 117,122 ****
--- 117,123 ----
&pgcrypto;
&pgfreespacemap;
&pgrowlocks;
+ &pgsql-fdw;
&pgstandby;
&pgstatstatements;
&pgstattuple;
diff --git a/doc/src/sgml/filelist.sgml b/doc/src/sgml/filelist.sgml
index b5d3c6d..de686ee 100644
*** a/doc/src/sgml/filelist.sgml
--- b/doc/src/sgml/filelist.sgml
***************
*** 125,130 ****
--- 125,131 ----
<!ENTITY pgcrypto SYSTEM "pgcrypto.sgml">
<!ENTITY pgfreespacemap SYSTEM "pgfreespacemap.sgml">
<!ENTITY pgrowlocks SYSTEM "pgrowlocks.sgml">
+ <!ENTITY pgsql-fdw SYSTEM "pgsql-fdw.sgml">
<!ENTITY pgstandby SYSTEM "pgstandby.sgml">
<!ENTITY pgstatstatements SYSTEM "pgstatstatements.sgml">
<!ENTITY pgstattuple SYSTEM "pgstattuple.sgml">
diff --git a/doc/src/sgml/pgsql-fdw.sgml b/doc/src/sgml/pgsql-fdw.sgml
index ...678e0c6 .
*** a/doc/src/sgml/pgsql-fdw.sgml
--- b/doc/src/sgml/pgsql-fdw.sgml
***************
*** 0 ****
--- 1,253 ----
+ <!-- doc/src/sgml/pgsql-fdw.sgml -->
+
+ <sect1 id="pgsql-fdw" xreflabel="pgsql_fdw">
+ <title>pgsql_fdw</title>
+
+ <indexterm zone="pgsql-fdw">
+ <primary>pgsql_fdw</primary>
+ </indexterm>
+
+ <para>
+ The <filename>pgsql_fdw</filename> module provides a foreign-data wrapper for
+ external <productname>PostgreSQL</productname> servers.
+ With this module, users can access data stored in external
+ <productname>PostgreSQL</productname> via plain SQL statements.
+ </para>
+
+ <para>
+ Note that default wrapper <literal>pgsql_fdw</literal> is created
+ automatically during <command>CREATE EXTENSION</command> command for
+ <application>pgsql_fdw</application>.
+ </para>
+
+ <sect2>
+ <title>FDW Options of pgsql_fdw</title>
+
+ <sect3>
+ <title>Connection Options</title>
+ <para>
+ A foreign server and user mapping created using this wrapper can have
+ <application>libpq</> connection options, expect below:
+
+ <itemizedlist>
+ <listitem><para>client_encoding</para></listitem>
+ <listitem><para>fallback_application_name</para></listitem>
+ <listitem><para>replication</para></listitem>
+ </itemizedlist>
+
+ For details of <application>libpq</> connection options, see
+ <xref linkend="libpq-connect">.
+ </para>
+
+ <para>
+ <literal>user</literal> and <literal>password</literal> can be
+ specified on user mappings, and others can be specified on foreign servers.
+ </para>
+ </sect3>
+
+ <sect3>
+ <title>Object Name Options</title>
+ <para>
+ Foreign tables which were created using this wrapper, or its columns can
+ have object name options. These options can be used to specify the names
+ used in SQL statement sent to remote <productname>PostgreSQL</productname>
+ server. These options are useful when a remote object has different name
+ from corresponding local one.
+ </para>
+
+ <variablelist>
+
+ <varlistentry>
+ <term><literal>nspname</literal></term>
+ <listitem>
+ <para>
+ This option, which can be specified on a foreign table, is used as a
+ namespace (schema) reference in the SQL statement. If this options is
+ omitted, <literal>pg_class.nspname</literal> of the foreign table is
+ used.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>relname</literal></term>
+ <listitem>
+ <para>
+ This option, which can be specified on a foreign table, is used as a
+ relation (table) reference in the SQL statement. If this options is
+ omitted, <literal>pg_class.relname</literal> of the foreign table is
+ used.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>colname</literal></term>
+ <listitem>
+ <para>
+ This option, which can be specified on a column of a foreign table, is
+ used as a column (attribute) reference in the SQL statement. If this
+ option is omitted, <literal>pg_attribute.attname</literal> of the column
+ of the foreign table is used.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ </variablelist>
+
+ </sect3>
+
+ <sect3>
+ <title>Cursor Options</title>
+ <para>
+ The <application>pgsql_fdw</application> always uses cursor to retrieve the
+ result from external server. Users can control the behavior of cursor by
+ setting cursor options to foreign table or foreign server. If an option
+ is set to both objects, finer-grained setting is used. In other words,
+ foreign table's setting overrides foreign server's setting.
+ </para>
+
+ <variablelist>
+
+ <varlistentry>
+ <term><literal>fetch_count</literal></term>
+ <listitem>
+ <para>
+ This option specifies the number of rows to be fetched at a time.
+ This option accepts only integer value larger than zero. The default
+ setting is 10000.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ </variablelist>
+
+ </sect3>
+
+ </sect2>
+
+ <sect2>
+ <title>Connection Management</title>
+
+ <para>
+ The <application>pgsql_fdw</application> establishes a connection to a
+ foreign server in the beginning of the first query which uses a foreign
+ table associated to the foreign server, and reuses the connection following
+ queries and even in following foreign scans in same query.
+
+ You can see the list of active connections via
+ <structname>pgsql_fdw_connections</structname> view. It shows pair of oid
+ and name of server and local role for each active connections established by
+ <application>pgsql_fdw</application>. For security reason, only superuser
+ can see other role's connections.
+ </para>
+
+ <para>
+ Established connections are kept alive until local role changes or the
+ current transaction aborts or user requests so.
+ </para>
+
+ <para>
+ If role has been changed, active connections established as old local role
+ is kept alive but never be reused until local role has restored to original
+ role. This kind of situation happens with <command>SET ROLE</command> and
+ <command>SET SESSION AUTHORIZATION</command>.
+ </para>
+
+ <para>
+ If current transaction aborts by error or user request, all active
+ connections are disconnected automatically. This behavior avoids possible
+ connection leaks on error.
+ </para>
+
+ <para>
+ You can discard persistent connection at arbitrary timing with
+ <function>pgsql_fdw_disconnect()</function>. It takes server oid and
+ user oid as arguments. This function can handle only connections
+ established in current session; connections established by other backends
+ are not reachable.
+ </para>
+
+ <para>
+ You can discard all active and visible connections in current session with
+ using <structname>pgsql_fdw_connections</structname> and
+ <function>pgsql_fdw_disconnect()</function> together:
+ <synopsis>
+ postgres=# SELECT pgsql_fdw_disconnect(srvid, usesysid) FROM pgsql_fdw_connections;
+ pgsql_fdw_disconnect
+ ----------------------
+ OK
+ OK
+ (2 rows)
+ </synopsis>
+ </para>
+ </sect2>
+
+ <sect2>
+ <title>Transaction Management</title>
+ <para>
+ The <application>pgsql_fdw</application> begins remote transaction at the
+ beginning of a local query, and terminates it with <command>ABORT</command>
+ at the end of the local query. This means that all foreign scans on a
+ foreign server in a local query are executed in one transaction.
+ The transaction isolation level used for remote transaction is same as
+ local transaction.
+ </para>
+ <para>
+ Note that even if the isolation level of local transaction was
+ <literal>SERIALIZABLE</literal> or <literal>REPEATABLE READ</literal>,
+ series of one query might produce different result, because foreign scans
+ in different local queries are executed in different remote transactions.
+ For instance, when client started a local transaction
+ explicitly with isolation level <literal>SERIALIZABLE</literal>, and
+ executed same local query which contains a foreign table which references
+ foreign data which is updated frequently, latter result would be different
+ from former result.
+ </para>
+ <para>
+ This restriction might be relaxed in future release.
+ </para>
+ </sect2>
+
+ <sect2>
+ <title>Estimation of Costs and Rows</title>
+ <para>
+ The <application>pgsql_fdw</application> estimates the costs of a foreign
+ scan by adding up some basic costs: connection costs, remote query costs
+ and data transfer costs.
+ To get remote query costs, <application>pgsql_fdw</application> executes
+ <command>EXPLAIN</command> command on remote server for each foreign scan.
+ </para>
+ <para>
+ On the other hand, estimated rows which was returned by
+ <command>EXPLAIN</command> is used for local estimation as-is.
+ </para>
+ </sect2>
+
+ <sect2>
+ <title>EXPLAIN Output</title>
+ <para>
+ For a foreign table using <literal>pgsql_fdw</>, <command>EXPLAIN</> shows
+ a remote SQL statement which is sent to remote
+ <productname>PostgreSQL</productname> server for a ForeignScan plan node.
+ For example:
+ </para>
+ <synopsis>
+ postgres=# EXPLAIN SELECT aid FROM pgbench_accounts WHERE abalance < 0;
+ QUERY PLAN
+ --------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on pgbench_accounts (cost=100.00..8105.13 rows=302613 width=8)
+ Filter: (abalance < 0)
+ Remote SQL: DECLARE pgsql_fdw_cursor_3 SCROLL CURSOR FOR SELECT aid, NULL, abalance, NULL FROM public.pgbench_accounts
+ (3 rows)
+ </synopsis>
+ </sect2>
+
+ <sect2>
+ <title>Author</title>
+ <para>
+ Shigeru Hanada <email>shigeru.hanada@gmail.com</email>
+ </para>
+ </sect2>
+
+ </sect1>
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 9ad54c5..c2a5a7b 100644
*** a/src/backend/utils/adt/ruleutils.c
--- b/src/backend/utils/adt/ruleutils.c
*************** deparse_context_for(const char *aliasnam
*** 2161,2166 ****
--- 2161,2189 ----
return list_make1(dpns);
}
+ /* ----------
+ * deparse_context_for_rtelist - Build deparse context for a list of RTEs
+ *
+ * Given the list of RangeTableEnetry, build deparsing context for an
+ * expression referencing those relations. This is sufficient for uses of
+ * deparse_expression before plan has been created.
+ * ----------
+ */
+ List *
+ deparse_context_for_rtelist(List *rtable)
+ {
+ deparse_namespace *dpns;
+
+ dpns = (deparse_namespace *) palloc0(sizeof(deparse_namespace));
+
+ /* Build a minimal RTE for the rel */
+ dpns->rtable = rtable;
+ dpns->ctes = NIL;
+
+ /* Return a one-deep namespace stack */
+ return list_make1(dpns);
+ }
+
/*
* deparse_context_for_planstate - Build deparse context for a plan
*
diff --git a/src/include/utils/builtins.h b/src/include/utils/builtins.h
index 2c331ce..dc6932a 100644
*** a/src/include/utils/builtins.h
--- b/src/include/utils/builtins.h
*************** extern char *deparse_expression(Node *ex
*** 648,653 ****
--- 648,654 ----
extern List *deparse_context_for(const char *aliasname, Oid relid);
extern List *deparse_context_for_planstate(Node *planstate, List *ancestors,
List *rtable);
+ extern List *deparse_context_for_rtelist(List *rtable);
extern const char *quote_identifier(const char *ident);
extern char *quote_qualified_identifier(const char *qualifier,
const char *ident);
pgsql_fdw_pushdown_v6.patchtext/plain; name=pgsql_fdw_pushdown_v6.patchDownload
diff --git a/contrib/pgsql_fdw/deparse.c b/contrib/pgsql_fdw/deparse.c
index 7ca2767..044f057 100644
*** a/contrib/pgsql_fdw/deparse.c
--- b/contrib/pgsql_fdw/deparse.c
*************** typedef struct foreign_executable_cxt
*** 35,46 ****
RelOptInfo *foreignrel;
} foreign_executable_cxt;
/*
* Deparse query representation into SQL statement which suits for remote
! * PostgreSQL server.
*/
char *
! deparseSql(Oid relid, PlannerInfo *root, RelOptInfo *baserel)
{
StringInfoData foreign_relname;
StringInfoData sql; /* builder for SQL statement */
--- 35,53 ----
RelOptInfo *foreignrel;
} foreign_executable_cxt;
+ static bool is_foreign_expr(PlannerInfo *root, RelOptInfo *baserel, Expr *expr);
+ static bool foreign_expr_walker(Node *node, foreign_executable_cxt *context);
+ static bool is_builtin(Oid procid);
+ static bool contain_ext_param(Node *clause);
+ static bool contain_ext_param_walker(Node *node, void *context);
+
/*
* Deparse query representation into SQL statement which suits for remote
! * PostgreSQL server. Also some of quals in WHERE clause will be pushed down
! * If they are safe to be evaluated on the remote side.
*/
char *
! deparseSql(Oid relid, PlannerInfo *root, RelOptInfo *baserel, bool *has_param)
{
StringInfoData foreign_relname;
StringInfoData sql; /* builder for SQL statement */
*************** deparseSql(Oid relid, PlannerInfo *root,
*** 51,56 ****
--- 58,64 ----
const char *relname = NULL; /* plain relation name */
const char *q_nspname; /* quoted namespace name */
const char *q_relname; /* quoted relation name */
+ List *foreign_expr = NIL; /* list of Expr* evaluated on remote */
int i;
List *rtable = NIL;
List *context = NIL;
*************** deparseSql(Oid relid, PlannerInfo *root,
*** 59,73 ****
initStringInfo(&foreign_relname);
/*
! * First of all, determine which column should be retrieved for this scan.
*
* We do this before deparsing SELECT clause because attributes which are
* not used in neither reltargetlist nor baserel->baserestrictinfo, quals
* evaluated on local, can be replaced with literal "NULL" in the SELECT
* clause to reduce overhead of tuple handling tuple and data transfer.
*/
if (baserel->baserestrictinfo != NIL)
{
ListCell *lc;
foreach (lc, baserel->baserestrictinfo)
--- 67,91 ----
initStringInfo(&foreign_relname);
/*
! * First of all, determine which qual can be pushed down.
! *
! * The expressions which satisfy is_foreign_expr() are deparsed into WHERE
! * clause of result SQL string, and they could be removed from PlanState
! * to avoid duplicate evaluation at ExecScan().
! *
! * We never change the quals in the Plan node, because this execution might
! * be for a PREPAREd statement, thus the quals in the Plan node might be
! * reused to construct another PlanState for subsequent EXECUTE statement.
*
* We do this before deparsing SELECT clause because attributes which are
* not used in neither reltargetlist nor baserel->baserestrictinfo, quals
* evaluated on local, can be replaced with literal "NULL" in the SELECT
* clause to reduce overhead of tuple handling tuple and data transfer.
*/
+ *has_param = false;
if (baserel->baserestrictinfo != NIL)
{
+ List *local_qual = NIL;
ListCell *lc;
foreach (lc, baserel->baserestrictinfo)
*************** deparseSql(Oid relid, PlannerInfo *root,
*** 76,81 ****
--- 94,113 ----
List *attrs;
/*
+ * Determine whether the qual can be pushed down or not. If a qual
+ * can be pushed down and it contains external param, tell that to
+ * caller in order to fall back to fixed costs.
+ */
+ if (is_foreign_expr(root, baserel, ri->clause))
+ {
+ foreign_expr = lappend(foreign_expr, ri->clause);
+ if (contain_ext_param((Node*) ri->clause))
+ *has_param = true;
+ }
+ else
+ local_qual = lappend(local_qual, ri);
+
+ /*
* We need to know which attributes are used in qual evaluated
* on the local server, because they should be listed in the
* SELECT clause of remote query. We can ignore attributes
*************** deparseSql(Oid relid, PlannerInfo *root,
*** 87,92 ****
--- 119,130 ----
PVC_RECURSE_PLACEHOLDERS);
attr_used = list_union(attr_used, attrs);
}
+
+ /*
+ * Remove pushed down qualifiers from baserestrictinfo to avoid
+ * redundant evaluation.
+ */
+ baserel->baserestrictinfo = local_qual;
}
/*
*************** deparseSql(Oid relid, PlannerInfo *root,
*** 204,210 ****
--- 242,473 ----
*/
appendStringInfo(&sql, "FROM %s", foreign_relname.data);
+ /*
+ * deparse WHERE clause
+ */
+ if (foreign_expr != NIL)
+ {
+ Node *node;
+
+ node = (Node *) make_ands_explicit(foreign_expr);
+ appendStringInfo(&sql, " WHERE %s ",
+ deparse_expression(node, context, false, false));
+ list_free(foreign_expr);
+ foreign_expr = NIL;
+ }
+
elog(DEBUG3, "Remote SQL: %s", sql.data);
return sql.data;
}
+ /*
+ * Returns true if expr is safe to be evaluated on the foreign server.
+ */
+ static bool
+ is_foreign_expr(PlannerInfo *root, RelOptInfo *baserel, Expr *expr)
+ {
+ foreign_executable_cxt context;
+ context.root = root;
+ context.foreignrel = baserel;
+
+ /*
+ * An expression which includes any mutable function can't be pushed down
+ * because it's result is not stable. For example, pushing now() down to
+ * remote side would cause confusion from the clock offset.
+ * If we have routine mapping infrastructure in future release, we will be
+ * able to choose function to be pushed down in finer granularity.
+ */
+ if (contain_mutable_functions((Node *) expr))
+ return false;
+
+ /*
+ * Check that the expression consists of nodes which are known as safe to
+ * be pushed down.
+ */
+ if (foreign_expr_walker((Node *) expr, &context))
+ return false;
+
+ return true;
+ }
+
+ /*
+ * Return true if node includes any node which is not known as safe to be
+ * pushed down.
+ */
+ static bool
+ foreign_expr_walker(Node *node, foreign_executable_cxt *context)
+ {
+ if (node == NULL)
+ return false;
+
+ /*
+ * If given expression has valid collation, it can't be pushed down because
+ * it might has incompatible semantics on remote side.
+ */
+ if (exprCollation(node) != InvalidOid ||
+ exprInputCollation(node) != InvalidOid)
+ return true;
+
+ /*
+ * If return type of given expression is not built-in, it can't be pushed
+ * down because it might has incompatible semantics on remote side.
+ */
+ if (!is_builtin(exprType(node)))
+ return true;
+
+ switch (nodeTag(node))
+ {
+ case T_Const:
+ case T_BoolExpr:
+ case T_NullTest:
+ case T_DistinctExpr:
+ case T_RelabelType:
+ /*
+ * 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 at the tail of this function.
+ */
+ break;
+ /*
+ * If function used by the expression is not built-in, it can't be
+ * pushed down because it might has incompatible semantics on remote
+ * side.
+ */
+ case T_FuncExpr:
+ {
+ FuncExpr *fe = (FuncExpr *) node;
+ if (!is_builtin(fe->funcid))
+ return true;
+ }
+ break;
+ case T_Param:
+ /*
+ * Only external parameters can be pushed down.:
+ */
+ {
+ if (((Param *) node)->paramkind != PARAM_EXTERN)
+ return true;
+ }
+ break;
+ case T_ScalarArrayOpExpr:
+ /*
+ * Only built-in operators can be pushed down. In addition,
+ * underlying function must be built-in and immutable, but we don't
+ * check volatility here; such check must be done already with
+ * contain_mutable_functions.
+ */
+ {
+ ScalarArrayOpExpr *oe = (ScalarArrayOpExpr *) node;
+
+ if (!is_builtin(oe->opno) || !is_builtin(oe->opfuncid))
+ return true;
+
+ /* operands are checked later */
+ }
+ break;
+ case T_OpExpr:
+ /*
+ * Only built-in operators can be pushed down. In addition,
+ * underlying function must be built-in and immutable, but we don't
+ * check volatility here; such check must be done already with
+ * contain_mutable_functions.
+ */
+ {
+ OpExpr *oe = (OpExpr *) node;
+
+ if (!is_builtin(oe->opno) || !is_builtin(oe->opfuncid))
+ return true;
+
+ /* operands are checked later */
+ }
+ 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;
+ foreign_executable_cxt *f_context;
+
+ f_context = (foreign_executable_cxt *) context;
+ if (var->varno != f_context->foreignrel->relid ||
+ var->varlevelsup != 0)
+ return true;
+ }
+ break;
+ case T_ArrayRef:
+ /*
+ * ArrayRef which holds non-built-in typed elements can't be pushed
+ * down.
+ */
+ {
+ if (!is_builtin(((ArrayRef *) node)->refelemtype))
+ return true;
+ }
+ break;
+ case T_ArrayExpr:
+ /*
+ * ArrayExpr which holds non-built-in typed elements can't be pushed
+ * down.
+ */
+ {
+ if (!is_builtin(((ArrayExpr *) node)->element_typeid))
+ return true;
+ }
+ break;
+ default:
+ {
+ ereport(DEBUG3,
+ (errmsg("expression is too complex"),
+ errdetail("%s", nodeToString(node))));
+ return true;
+ }
+ break;
+ }
+
+ return expression_tree_walker(node, foreign_expr_walker, context);
+ }
+
+ /*
+ * Return true if given object is one of built-in objects.
+ */
+ static bool
+ is_builtin(Oid oid)
+ {
+ return (oid < FirstNormalObjectId);
+ }
+
+ /*
+ * contain_ext_param
+ * Recursively search for Param nodes within a clause.
+ *
+ * Returns true if any parameter reference node with relkind PARAM_EXTERN
+ * found.
+ *
+ * This does not descend into subqueries, and so should be used only after
+ * reduction of sublinks to subplans, or in contexts where it's known there
+ * are no subqueries. There mustn't be outer-aggregate references either.
+ *
+ * XXX: These functions could be in core, src/backend/optimizer/util/clauses.c.
+ */
+ static bool
+ contain_ext_param(Node *clause)
+ {
+ return contain_ext_param_walker(clause, NULL);
+ }
+
+ static bool
+ contain_ext_param_walker(Node *node, void *context)
+ {
+ if (node == NULL)
+ return false;
+ if (IsA(node, Param))
+ {
+ Param *param = (Param *) node;
+
+ if (param->paramkind == PARAM_EXTERN)
+ return true; /* abort the tree traversal and return true */
+ }
+ return expression_tree_walker(node, contain_ext_param_walker, context);
+ }
diff --git a/contrib/pgsql_fdw/expected/pgsql_fdw.out b/contrib/pgsql_fdw/expected/pgsql_fdw.out
index 7fe20e7..1c027ad 100644
*** a/contrib/pgsql_fdw/expected/pgsql_fdw.out
--- b/contrib/pgsql_fdw/expected/pgsql_fdw.out
*************** SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.
*** 229,240 ****
-- with WHERE clause
EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
! QUERY PLAN
! ------------------------------------------------------------------------------------------------------------------
Foreign Scan on public.ft1 t1
Output: c1, c2, c3, c4, c5, c6, c7
! Filter: ((t1.c1 = 101) AND ((t1.c6)::text = '1'::text) AND (t1.c7 = '1'::bpchar))
! Remote SQL: DECLARE pgsql_fdw_cursor_4 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
(4 rows)
SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
--- 229,240 ----
-- with WHERE clause
EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
! QUERY PLAN
! ---------------------------------------------------------------------------------------------------------------------------------------
Foreign Scan on public.ft1 t1
Output: c1, c2, c3, c4, c5, c6, c7
! Filter: (((t1.c6)::text = '1'::text) AND (t1.c7 = '1'::bpchar))
! Remote SQL: DECLARE pgsql_fdw_cursor_4 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE ("C 1" = 101)
(4 rows)
SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
*************** EXPLAIN (COSTS false) SELECT * FROM ft1
*** 342,361 ****
(3 rows)
EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = abs(t1.c2);
! QUERY PLAN
! -------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Filter: (c1 = abs(c2))
! Remote SQL: DECLARE pgsql_fdw_cursor_20 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
! (3 rows)
EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = t1.c2;
! QUERY PLAN
! -------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Filter: (c1 = c2)
! Remote SQL: DECLARE pgsql_fdw_cursor_21 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
! (3 rows)
-- ===================================================================
-- parameterized queries
--- 342,359 ----
(3 rows)
EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = abs(t1.c2);
! QUERY PLAN
! --------------------------------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Remote SQL: DECLARE pgsql_fdw_cursor_20 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE ("C 1" = abs(c2))
! (2 rows)
EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = t1.c2;
! QUERY PLAN
! ---------------------------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Remote SQL: DECLARE pgsql_fdw_cursor_21 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE ("C 1" = c2)
! (2 rows)
-- ===================================================================
-- parameterized queries
*************** EXPLAIN (COSTS false) SELECT * FROM ft1
*** 363,379 ****
-- simple join
PREPARE st1(int, int) AS SELECT t1.c3, t2.c3 FROM ft1 t1, ft2 t2 WHERE t1.c1 = $1 AND t2.c1 = $2;
EXPLAIN (COSTS false) EXECUTE st1(1, 2);
! QUERY PLAN
! -----------------------------------------------------------------------------------------------------------------------------------------
Nested Loop
-> Foreign Scan on ft1 t1
! Filter: (c1 = 1)
! Remote SQL: DECLARE pgsql_fdw_cursor_22 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, NULL, NULL, NULL, NULL FROM "S 1"."T 1"
! -> Materialize
! -> Foreign Scan on ft2 t2
! Filter: (c1 = 2)
! Remote SQL: DECLARE pgsql_fdw_cursor_23 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, NULL, NULL, NULL, NULL FROM "S 1"."T 1"
! (8 rows)
EXECUTE st1(1, 1);
c3 | c3
--- 361,374 ----
-- simple join
PREPARE st1(int, int) AS SELECT t1.c3, t2.c3 FROM ft1 t1, ft2 t2 WHERE t1.c1 = $1 AND t2.c1 = $2;
EXPLAIN (COSTS false) EXECUTE st1(1, 2);
! QUERY PLAN
! ------------------------------------------------------------------------------------------------------------------------------------------------------
Nested Loop
-> Foreign Scan on ft1 t1
! Remote SQL: DECLARE pgsql_fdw_cursor_22 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, NULL, NULL, NULL, NULL FROM "S 1"."T 1" WHERE ("C 1" = 1)
! -> Foreign Scan on ft2 t2
! Remote SQL: DECLARE pgsql_fdw_cursor_23 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, NULL, NULL, NULL, NULL FROM "S 1"."T 1" WHERE ("C 1" = 2)
! (5 rows)
EXECUTE st1(1, 1);
c3 | c3
*************** EXECUTE st1(101, 101);
*** 390,410 ****
-- subquery using stable function (can't be pushed down)
PREPARE st2(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c4) = 6) ORDER BY c1;
EXPLAIN (COSTS false) EXECUTE st2(10, 20);
! QUERY PLAN
! ---------------------------------------------------------------------------------------------------------------------------------------------------
Sort
Sort Key: t1.c1
-> Hash Join
Hash Cond: (t1.c3 = t2.c3)
-> Foreign Scan on ft1 t1
! Filter: (c1 < 20)
! Remote SQL: DECLARE pgsql_fdw_cursor_28 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
-> Hash
-> HashAggregate
-> Foreign Scan on ft2 t2
! Filter: ((c1 > 10) AND (date_part('dow'::text, c4) = 6::double precision))
! Remote SQL: DECLARE pgsql_fdw_cursor_29 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, c4, NULL, NULL, NULL FROM "S 1"."T 1"
! (12 rows)
EXECUTE st2(10, 20);
c1 | c2 | c3 | c4 | c5 | c6 | c7
--- 385,404 ----
-- subquery using stable function (can't be pushed down)
PREPARE st2(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c4) = 6) ORDER BY c1;
EXPLAIN (COSTS false) EXECUTE st2(10, 20);
! QUERY PLAN
! -----------------------------------------------------------------------------------------------------------------------------------------------------------------------
Sort
Sort Key: t1.c1
-> Hash Join
Hash Cond: (t1.c3 = t2.c3)
-> Foreign Scan on ft1 t1
! Remote SQL: DECLARE pgsql_fdw_cursor_28 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE ("C 1" < 20)
-> Hash
-> HashAggregate
-> Foreign Scan on ft2 t2
! Filter: (date_part('dow'::text, c4) = 6::double precision)
! Remote SQL: DECLARE pgsql_fdw_cursor_29 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, c4, NULL, NULL, NULL FROM "S 1"."T 1" WHERE ("C 1" > 10)
! (11 rows)
EXECUTE st2(10, 20);
c1 | c2 | c3 | c4 | c5 | c6 | c7
*************** EXECUTE st1(101, 101);
*** 421,441 ****
-- subquery using immutable function (can be pushed down)
PREPARE st3(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c5) = 6) ORDER BY c1;
EXPLAIN (COSTS false) EXECUTE st3(10, 20);
! QUERY PLAN
! ---------------------------------------------------------------------------------------------------------------------------------------------------
Sort
Sort Key: t1.c1
-> Hash Join
Hash Cond: (t1.c3 = t2.c3)
-> Foreign Scan on ft1 t1
! Filter: (c1 < 20)
! Remote SQL: DECLARE pgsql_fdw_cursor_34 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
-> Hash
-> HashAggregate
-> Foreign Scan on ft2 t2
! Filter: ((c1 > 10) AND (date_part('dow'::text, c5) = 6::double precision))
! Remote SQL: DECLARE pgsql_fdw_cursor_35 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, NULL, c5, NULL, NULL FROM "S 1"."T 1"
! (12 rows)
EXECUTE st3(10, 20);
c1 | c2 | c3 | c4 | c5 | c6 | c7
--- 415,434 ----
-- subquery using immutable function (can be pushed down)
PREPARE st3(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c5) = 6) ORDER BY c1;
EXPLAIN (COSTS false) EXECUTE st3(10, 20);
! QUERY PLAN
! -----------------------------------------------------------------------------------------------------------------------------------------------------------------------
Sort
Sort Key: t1.c1
-> Hash Join
Hash Cond: (t1.c3 = t2.c3)
-> Foreign Scan on ft1 t1
! Remote SQL: DECLARE pgsql_fdw_cursor_34 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE ("C 1" < 20)
-> Hash
-> HashAggregate
-> Foreign Scan on ft2 t2
! Filter: (date_part('dow'::text, c5) = 6::double precision)
! Remote SQL: DECLARE pgsql_fdw_cursor_35 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, NULL, c5, NULL, NULL FROM "S 1"."T 1" WHERE ("C 1" > 10)
! (11 rows)
EXECUTE st3(10, 20);
c1 | c2 | c3 | c4 | c5 | c6 | c7
*************** EXECUTE st3(20, 30);
*** 452,503 ****
-- custom plan should be chosen
PREPARE st4(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 = $1;
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! -------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Filter: (c1 = 1)
! Remote SQL: DECLARE pgsql_fdw_cursor_40 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
! (3 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! -------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Filter: (c1 = 1)
! Remote SQL: DECLARE pgsql_fdw_cursor_41 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
! (3 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! -------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Filter: (c1 = 1)
! Remote SQL: DECLARE pgsql_fdw_cursor_42 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
! (3 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! -------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Filter: (c1 = 1)
! Remote SQL: DECLARE pgsql_fdw_cursor_43 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
! (3 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! -------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Filter: (c1 = 1)
! Remote SQL: DECLARE pgsql_fdw_cursor_44 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
! (3 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! -------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Filter: (c1 = 1)
! Remote SQL: DECLARE pgsql_fdw_cursor_46 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
! (3 rows)
-- cleanup
DEALLOCATE st1;
--- 445,490 ----
-- custom plan should be chosen
PREPARE st4(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 = $1;
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! --------------------------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Remote SQL: DECLARE pgsql_fdw_cursor_40 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE ("C 1" = 1)
! (2 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! --------------------------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Remote SQL: DECLARE pgsql_fdw_cursor_41 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE ("C 1" = 1)
! (2 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! --------------------------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Remote SQL: DECLARE pgsql_fdw_cursor_42 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE ("C 1" = 1)
! (2 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! --------------------------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Remote SQL: DECLARE pgsql_fdw_cursor_43 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE ("C 1" = 1)
! (2 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! --------------------------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Remote SQL: DECLARE pgsql_fdw_cursor_44 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE ("C 1" = 1)
! (2 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! --------------------------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Remote SQL: DECLARE pgsql_fdw_cursor_46 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE ("C 1" = 1)
! (2 rows)
-- cleanup
DEALLOCATE st1;
diff --git a/contrib/pgsql_fdw/pgsql_fdw.c b/contrib/pgsql_fdw/pgsql_fdw.c
index b92258a..3f35b05 100644
*** a/contrib/pgsql_fdw/pgsql_fdw.c
--- b/contrib/pgsql_fdw/pgsql_fdw.c
*************** static void estimate_costs(PlannerInfo *
*** 123,130 ****
static void execute_query(ForeignScanState *node);
static PGresult *fetch_result(ForeignScanState *node);
static void store_result(ForeignScanState *node, PGresult *res);
- static bool contain_ext_param(Node *clause);
- static bool contain_ext_param_walker(Node *node, void *context);
/* Exported functions, but not written in pgsql_fdw.h. */
void _PG_init(void);
--- 123,128 ----
*************** pgsqlPlanForeignScan(Oid foreigntableid,
*** 194,207 ****
List *fdw_private = NIL;
ForeignTable *table;
ForeignServer *server;
/* Construct FdwPlan with cost estimates */
fdwplan = makeNode(FdwPlan);
! sql = deparseSql(foreigntableid, root, baserel);
table = GetForeignTable(foreigntableid);
server = GetForeignServer(table->serverid);
! estimate_costs(root, baserel, sql, server->serverid,
! &fdwplan->startup_cost, &fdwplan->total_cost);
/*
* Store plain SELECT statement in private area of FdwPlan. This will be
--- 192,222 ----
List *fdw_private = NIL;
ForeignTable *table;
ForeignServer *server;
+ bool has_param;
/* Construct FdwPlan with cost estimates */
fdwplan = makeNode(FdwPlan);
! sql = deparseSql(foreigntableid, root, baserel, &has_param);
table = GetForeignTable(foreigntableid);
server = GetForeignServer(table->serverid);
!
! /*
! * If the baserestrictinfo contains any Param node with paramkind
! * PARAM_EXTERNAL as part of push-down-able condition, we need to
! * groundless fixed costs because we can't get actual parameter values
! * here, so we use fixed and large costs as second best so that planner
! * tend to choose custom plan.
! *
! * See comments in plancache.c for details of custom plan.
! */
! if (has_param)
! {
! fdwplan->startup_cost = CONNECTION_COSTS;
! fdwplan->total_cost = 10000; /* groundless large costs */
! }
! else
! estimate_costs(root, baserel, sql, server->serverid,
! &fdwplan->startup_cost, &fdwplan->total_cost);
/*
* Store plain SELECT statement in private area of FdwPlan. This will be
*************** estimate_costs(PlannerInfo *root, RelOpt
*** 550,556 ****
const char *sql, Oid serverid,
Cost *startup_cost, Cost *total_cost)
{
- ListCell *lc;
ForeignServer *server;
UserMapping *user;
PGconn *conn = NULL;
--- 565,570 ----
*************** estimate_costs(PlannerInfo *root, RelOpt
*** 561,587 ****
int n;
/*
- * If the baserestrictinfo contains any Param node with paramkind
- * PARAM_EXTERNAL, we need result of EXPLAIN for EXECUTE statement, not for
- * simple SELECT statement. However, we can't get actual parameter values
- * here, so we use fixed and large costs as second best so that planner
- * tend to choose custom plan.
- *
- * See comments in plancache.c for details of custom plan.
- */
- foreach(lc, baserel->baserestrictinfo)
- {
- RestrictInfo *rs = (RestrictInfo *) lfirst(lc);
- if (contain_ext_param((Node *) rs->clause))
- {
- *startup_cost = CONNECTION_COSTS;
- *total_cost = 10000; /* groundless large costs */
-
- return;
- }
- }
-
- /*
* Get connection to the foreign server. Connection manager would
* establish new connection if necessary.
*/
--- 575,580 ----
*************** store_result(ForeignScanState *node, PGr
*** 859,895 ****
tuplestore_donestoring(festate->tuples);
}
-
- /*
- * contain_ext_param
- * Recursively search for Param nodes within a clause.
- *
- * Returns true if any parameter reference node with relkind PARAM_EXTERN
- * found.
- *
- * This does not descend into subqueries, and so should be used only after
- * reduction of sublinks to subplans, or in contexts where it's known there
- * are no subqueries. There mustn't be outer-aggregate references either.
- *
- * XXX: These functions could be in core, src/backend/optimizer/util/clauses.c.
- */
- static bool
- contain_ext_param(Node *clause)
- {
- return contain_ext_param_walker(clause, NULL);
- }
-
- static bool
- contain_ext_param_walker(Node *node, void *context)
- {
- if (node == NULL)
- return false;
- if (IsA(node, Param))
- {
- Param *param = (Param *) node;
-
- if (param->paramkind == PARAM_EXTERN)
- return true; /* abort the tree traversal and return true */
- }
- return expression_tree_walker(node, contain_ext_param_walker, context);
- }
--- 852,854 ----
diff --git a/contrib/pgsql_fdw/pgsql_fdw.h b/contrib/pgsql_fdw/pgsql_fdw.h
index f6394ce..690d58b 100644
*** a/contrib/pgsql_fdw/pgsql_fdw.h
--- b/contrib/pgsql_fdw/pgsql_fdw.h
*************** int ExtractConnectionOptions(List *defel
*** 23,28 ****
const char **values);
/* in deparse.c */
! char *deparseSql(Oid relid, PlannerInfo *root, RelOptInfo *baserel);
#endif /* PGSQL_FDW_H */
--- 23,31 ----
const char **values);
/* in deparse.c */
! char *deparseSql(Oid relid,
! PlannerInfo *root,
! RelOptInfo *baserel,
! bool *has_param);
#endif /* PGSQL_FDW_H */
diff --git a/doc/src/sgml/pgsql-fdw.sgml b/doc/src/sgml/pgsql-fdw.sgml
index 678e0c6..6a48c60 100644
*** a/doc/src/sgml/pgsql-fdw.sgml
--- b/doc/src/sgml/pgsql-fdw.sgml
*************** postgres=# SELECT pgsql_fdw_disconnect(s
*** 234,245 ****
</para>
<synopsis>
postgres=# EXPLAIN SELECT aid FROM pgbench_accounts WHERE abalance < 0;
! QUERY PLAN
! --------------------------------------------------------------------------------------------------------------------------
! Foreign Scan on pgbench_accounts (cost=100.00..8105.13 rows=302613 width=8)
! Filter: (abalance < 0)
! Remote SQL: DECLARE pgsql_fdw_cursor_3 SCROLL CURSOR FOR SELECT aid, NULL, abalance, NULL FROM public.pgbench_accounts
! (3 rows)
</synopsis>
</sect2>
--- 234,244 ----
</para>
<synopsis>
postgres=# EXPLAIN SELECT aid FROM pgbench_accounts WHERE abalance < 0;
! QUERY PLAN
! ------------------------------------------------------------------------------------------------------------------------------------------------
! Foreign Scan on pgbench_accounts (cost=100.00..8861.66 rows=518 width=8)
! Remote SQL: DECLARE pgsql_fdw_cursor_1 SCROLL CURSOR FOR SELECT aid, NULL, abalance, NULL FROM public.pgbench_accounts WHERE (abalance < 0)
! (2 rows)
</synopsis>
</sect2>
Shigeru Hanada wrote:
Thanks for the review. Attached patches are revised version, though
only fdw_helper_v5.patch is unchanged.
Two questions:
- Is it on purpose that you can specify all SSL client options
except "sslcompression"?
- Since a rescan is done by rewinding the cursor, is it necessary
to have any other remote isolation level than READ COMMITED?
There is only one query issued per transaction.
Yours,
Laurenz Albe
I found a strange behavior with v10. Is it available to reproduce?
In case of "ftbl" is declared as follows:
postgres=# select * FROM ftbl;
a | b
---+-----
1 | aaa
2 | bbb
3 | ccc
4 | ddd
5 | eee
(5 rows)
I tried to raise an error on remote side.
postgres=# select * FROM ftbl WHERE 100 / (a - 3) > 0;
The connection to the server was lost. Attempting reset: Failed.
The connection to the server was lost. Attempting reset: Failed.
!> \q
Its call-trace was:
(gdb) bt
#0 0x00000031030810a4 in free () from /lib64/libc.so.6
#1 0x00007f2caa620bd9 in PQclear (res=0x2102500) at fe-exec.c:679
#2 0x00007f2caa83c4db in execute_query (node=0x20f20a0) at pgsql_fdw.c:722
#3 0x00007f2caa83c64a in pgsqlIterateForeignScan (node=0x20f20a0)
at pgsql_fdw.c:402
#4 0x00000000005c120f in ForeignNext (node=0x20f20a0) at nodeForeignscan.c:50
#5 0x00000000005a9b37 in ExecScanFetch (recheckMtd=0x5c11c0 <ForeignRecheck>,
accessMtd=0x5c11d0 <ForeignNext>, node=0x20f20a0) at execScan.c:82
#6 ExecScan (node=0x20f20a0, accessMtd=0x5c11d0 <ForeignNext>,
recheckMtd=0x5c11c0 <ForeignRecheck>) at execScan.c:132
#7 0x00000000005a2128 in ExecProcNode (node=0x20f20a0) at execProcnode.c:441
#8 0x000000000059edc2 in ExecutePlan (dest=0x210f280,
direction=<optimized out>, numberTuples=0, sendTuples=1 '\001',
operation=CMD_SELECT, planstate=0x20f20a0, estate=0x20f1f88)
at execMain.c:1449
This is the PG_CATCH block at execute_query(). fetch_result() raises
an error, then it shall be catched to release PGresult.
Although "res" should be NULL at this point, PQclear was called with
a non-zero value according to the call trace.
More strangely, I tried to inject elog(INFO, ...) to show the value of "res"
at this point. Then, it become unavailable to reproduce when I tried to
show the pointer of "res" with elog(INFO, "&res = %p", &res);
Why the "res" has a non-zero value, even though it was cleared prior
to fetch_result() and an error was raised within this function?
Thanks,
2012年2月16日13:41 Shigeru Hanada <shigeru.hanada@gmail.com>:
Kaigai-san,
Thanks for the review. Attached patches are revised version, though
only fdw_helper_v5.patch is unchanged.(2012/02/16 0:09), Kohei KaiGai wrote:
[memory context of tuple store]
It calls tuplestore_begin_heap() under the memory context of
festate->scan_cxt at pgsqlBeginForeignScan.Yes, it's because tuplestore uses a context which was current when
tuplestore_begin_heap was called. I want to use per-scan context for
tuplestore, to keep its content tuples alive through the scan.On the other hand, tuplestore_gettupleslot() is called under the
memory context of festate->tuples.Yes, result tuples to be returned to executor should be allocated in
per-scan context and live until next IterateForeignScan (or
EndForeignScan), because such tuple will be released via ExecClearTuple
in next IterateForeignScan call. If we don't switch context to per-scan
context, result tuple is allocated in per-tuple context and cause
double-free and server crash.I could not find a callback functions being invoked on errors,
so I doubt the memory objects acquired within tuplestore_begin_heap()
shall be leaked, even though it is my suggestion to create a sub-context
under the existing one.How do you confirmed that no callback function is invoked on errors? I
think that memory objects acquired within tuplestore_begin_heap (I guess
you mean excluding stored tuples, right?) are released during cleanup of
aborted transaction. I tested that by adding elog(ERROR) to the tail of
store_result() for intentional error, and execute large query 100 times
in a session. I saw VIRT value (via top command) comes down to constant
level after every query.In my opinion, it is a good choice to use es_query_cxt of the supplied EState.
What does prevent to apply this per-query memory context?Ah, I've confused context management of pgsql_fdw... I fixed pgsql_fdw
to create per-scan context as a child of es_query_cxt in
BeginForeignScan, and use it for tuplestore of the scan. So, tuplestore
and its contents are released correctly at EndForeignScan, or cleanup of
aborted transaction in error case.You mention about PGresult being malloc()'ed. However, it seems to me
fetch_result() and store_result() once copy the contents on malloc()'ed
area to the palloc()'ed area, and PQresult is released on an error using
PG_TRY() ... PG_CATCH() block.During thinking about this comment, I found double-free bug of PGresult
in execute_query, thanks :)But, sorry, I'm not sure what the concern you show here is. The reason
why copying tuples from malloc'ed area to palloc'ed area is to release
PGresult before returning from the IterateForeingScan call. The reason
why using PG_TRY block is to sure that PGresult is released before jump
back to upstream in error case.[Minor comments]
Please set NULL to "sql" variable at begin_remote_tx().
Compiler raises a warnning due to references of uninitialized variable,
even though the code path never run.Fixed. BTW, just out of curiosity, which compiler do you use? My
compiler ,gcc (GCC) 4.6.0 20110603 (Red Hat 4.6.0-10) on Fedora 15,
doesn't warn it.It potentially causes a problem in case when fetch_result() raises an
error because of unexpected status (!= PGRES_TUPLES_OK).
One code path is not protected with PG_TRY(), and other code path
will call PQclear towards already released PQresult.Fixed.
Although it is just a preference of mine, is the exprFunction necessary?
It seems to me, the point of push-down check is whether the supplied
node is built-in object, or not. So, an sufficient check is is_builtin() onto
FuncExpr->funcid, OpExpr->opno, ScalarArrayOpExpr->opno and so on.
It does not depend on whether the function implementing these nodes
are built-in or not.Got rid of exprFunction and fixed foreign_expr_walker to check function
oid in each case label.Regards,
--
Shigeru Hanada
--
KaiGai Kohei <kaigai@kaigai.gr.jp>
2012年2月16日13:41 Shigeru Hanada <shigeru.hanada@gmail.com>:
Kaigai-san,
Thanks for the review. Attached patches are revised version, though
only fdw_helper_v5.patch is unchanged.(2012/02/16 0:09), Kohei KaiGai wrote:
[memory context of tuple store]
It calls tuplestore_begin_heap() under the memory context of
festate->scan_cxt at pgsqlBeginForeignScan.Yes, it's because tuplestore uses a context which was current when
tuplestore_begin_heap was called. I want to use per-scan context for
tuplestore, to keep its content tuples alive through the scan.On the other hand, tuplestore_gettupleslot() is called under the
memory context of festate->tuples.Yes, result tuples to be returned to executor should be allocated in
per-scan context and live until next IterateForeignScan (or
EndForeignScan), because such tuple will be released via ExecClearTuple
in next IterateForeignScan call. If we don't switch context to per-scan
context, result tuple is allocated in per-tuple context and cause
double-free and server crash.I could not find a callback functions being invoked on errors,
so I doubt the memory objects acquired within tuplestore_begin_heap()
shall be leaked, even though it is my suggestion to create a sub-context
under the existing one.How do you confirmed that no callback function is invoked on errors? I
think that memory objects acquired within tuplestore_begin_heap (I guess
you mean excluding stored tuples, right?) are released during cleanup of
aborted transaction. I tested that by adding elog(ERROR) to the tail of
store_result() for intentional error, and execute large query 100 times
in a session. I saw VIRT value (via top command) comes down to constant
level after every query.
Oops, I overlooked the point where MessageContext and its children get
reset. However, as its name, I don't believe it was right usage of memory
context.
As the latest version doing, es_query_cxt is the right way to acquire
memory object with per-query duration.
In my opinion, it is a good choice to use es_query_cxt of the supplied EState.
What does prevent to apply this per-query memory context?Ah, I've confused context management of pgsql_fdw... I fixed pgsql_fdw
to create per-scan context as a child of es_query_cxt in
BeginForeignScan, and use it for tuplestore of the scan. So, tuplestore
and its contents are released correctly at EndForeignScan, or cleanup of
aborted transaction in error case.
I believe it is right direction.
You mention about PGresult being malloc()'ed. However, it seems to me
fetch_result() and store_result() once copy the contents on malloc()'ed
area to the palloc()'ed area, and PQresult is released on an error using
PG_TRY() ... PG_CATCH() block.During thinking about this comment, I found double-free bug of PGresult
in execute_query, thanks :)
Unfortunately, I found the strange behavior around this code.
I doubt an interaction between longjmp and compiler optimization,
but it is not certain right now.
I'd like to push this patch to committer reviews after this problem got closed.
Right now, I don't have comments on this patch any more.
But, sorry, I'm not sure what the concern you show here is. The reason
why copying tuples from malloc'ed area to palloc'ed area is to release
PGresult before returning from the IterateForeingScan call. The reason
why using PG_TRY block is to sure that PGresult is released before jump
back to upstream in error case.[Minor comments]
Please set NULL to "sql" variable at begin_remote_tx().
Compiler raises a warnning due to references of uninitialized variable,
even though the code path never run.Fixed. BTW, just out of curiosity, which compiler do you use? My
compiler ,gcc (GCC) 4.6.0 20110603 (Red Hat 4.6.0-10) on Fedora 15,
doesn't warn it.
I uses Fedora 16, and GCC 4.6.2.
[kaigai@iwashi pgsql_fdw]$ gcc --version
gcc (GCC) 4.6.2 20111027 (Red Hat 4.6.2-1)
It is not a matter related to compiler version, but common manner in
PostgreSQL code. You can likely found source code comments
like "/* keep compiler quiet */"
Thanks,
--
KaiGai Kohei <kaigai@kaigai.gr.jp>
(2012/02/17 0:15), Albe Laurenz wrote:
Shigeru Hanada wrote:
Thanks for the review. Attached patches are revised version, though
only fdw_helper_v5.patch is unchanged.Two questions:
- Is it on purpose that you can specify all SSL client options
except "sslcompression"?
No, just an oversight. Good catch.
- Since a rescan is done by rewinding the cursor, is it necessary
to have any other remote isolation level than READ COMMITED?
There is only one query issued per transaction.
If multiple foreign tables on a foreign server is used in a local query,
multiple queries are executed in a remote transaction. So IMO isolation
levels are useful even if remote query is executed only once.
Regards,
--
Shigeru Hanada
(2012/02/17 2:02), Kohei KaiGai wrote:
I found a strange behavior with v10. Is it available to reproduce?
<snip>
I tried to raise an error on remote side.
postgres=# select * FROM ftbl WHERE 100 / (a - 3)> 0;
The connection to the server was lost. Attempting reset: Failed.
The connection to the server was lost. Attempting reset: Failed.
!> \q
I could reproduce the error by omitting CFLAGS=-O0 from configure
option. I usually this for coding environment so that gdb debugging
works correctly, so I haven't noticed this issue. I should test
optimized environment too...
Expected result in that case is:
postgres=# select * from pgbench_accounts where 100 / (aid - 3) > 0;
ERROR: could not fetch rows from foreign server
DETAIL: ERROR: division by zero
HINT: FETCH 10000 FROM pgsql_fdw_cursor_0
postgres=#
This is the PG_CATCH block at execute_query(). fetch_result() raises
an error, then it shall be catched to release PGresult.
Although "res" should be NULL at this point, PQclear was called with
a non-zero value according to the call trace.More strangely, I tried to inject elog(INFO, ...) to show the value of "res"
at this point. Then, it become unavailable to reproduce when I tried to
show the pointer of "res" with elog(INFO, "&res = %p",&res);Why the "res" has a non-zero value, even though it was cleared prior
to fetch_result() and an error was raised within this function?
I've found the the problem is uninitialized PGresult variables.
Uninitialized PGresult pointer is used in some places, so its value is
garbage in PG_CATCH block when assignment code has been interrupted by
longjmp.
Probably recommended style would be like this:
<pseudo_code>
PGresult *res = NULL; /* must be NULL in PG_CATCH */
PG_TRY();
{
res = func_might_throw_exception();
if (PQstatus(res) != PGRES_xxx_OK)
{
/* error handling, pass message to caller */
ereport(ERROR, ...);
}
/* success case, use result of query and release it */
...
PQclear(res);
}
PG_CATCH();
{
PQclear(res);
PG_RE_THROW();
/* caller should catch this exception. */
}
</pseudo_code>
I misunderstood that PGresult pointer always has valid value after that
line, because I had wrote assignment of PGresult pointer before PG_TRY
block. Fixes for this issue are:
(1) Initialize PGresult pointer with NULL, if it is used in PG_CATCH.
(2) Move PGresult assignment into PG_TRY block so that we can get
compiler warning of uninitialized variable, just in case.
Please find attached a patch including fixes for this issue.
Regards,
--
Shigeru Hanada
Attachments:
pgsql_fdw_v11.patchtext/plain; name=pgsql_fdw_v11.patchDownload
diff --git a/contrib/Makefile b/contrib/Makefile
index ac0a80a..14aa433 100644
*** a/contrib/Makefile
--- b/contrib/Makefile
*************** SUBDIRS = \
*** 41,46 ****
--- 41,47 ----
pgbench \
pgcrypto \
pgrowlocks \
+ pgsql_fdw \
pgstattuple \
seg \
spi \
diff --git a/contrib/README b/contrib/README
index a1d42a1..d3fa211 100644
*** a/contrib/README
--- b/contrib/README
*************** pgrowlocks -
*** 158,163 ****
--- 158,167 ----
A function to return row locking information
by Tatsuo Ishii <ishii@sraoss.co.jp>
+ pgsql_fdw -
+ Foreign-data wrapper for external PostgreSQL servers.
+ by Shigeru Hanada <shigeru.hanada@gmail.com>
+
pgstattuple -
Functions to return statistics about "dead" tuples and free
space within a table
diff --git a/contrib/pgsql_fdw/.gitignore b/contrib/pgsql_fdw/.gitignore
index ...0854728 .
*** a/contrib/pgsql_fdw/.gitignore
--- b/contrib/pgsql_fdw/.gitignore
***************
*** 0 ****
--- 1,4 ----
+ # Generated subdirectories
+ /results/
+ *.o
+ *.so
diff --git a/contrib/pgsql_fdw/Makefile b/contrib/pgsql_fdw/Makefile
index ...6381365 .
*** a/contrib/pgsql_fdw/Makefile
--- b/contrib/pgsql_fdw/Makefile
***************
*** 0 ****
--- 1,22 ----
+ # contrib/pgsql_fdw/Makefile
+
+ MODULE_big = pgsql_fdw
+ OBJS = pgsql_fdw.o option.o deparse.o connection.o
+ PG_CPPFLAGS = -I$(libpq_srcdir)
+ SHLIB_LINK = $(libpq)
+
+ EXTENSION = pgsql_fdw
+ DATA = pgsql_fdw--1.0.sql
+
+ REGRESS = pgsql_fdw
+
+ ifdef USE_PGXS
+ PG_CONFIG = pg_config
+ PGXS := $(shell $(PG_CONFIG) --pgxs)
+ include $(PGXS)
+ else
+ subdir = contrib/pgsql_fdw
+ top_builddir = ../..
+ include $(top_builddir)/src/Makefile.global
+ include $(top_srcdir)/contrib/contrib-global.mk
+ endif
diff --git a/contrib/pgsql_fdw/connection.c b/contrib/pgsql_fdw/connection.c
index ...48a5f1e .
*** a/contrib/pgsql_fdw/connection.c
--- b/contrib/pgsql_fdw/connection.c
***************
*** 0 ****
--- 1,553 ----
+ /*-------------------------------------------------------------------------
+ *
+ * connection.c
+ * Connection management for pgsql_fdw
+ *
+ * Portions Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/connection.c
+ *
+ *-------------------------------------------------------------------------
+ */
+ #include "postgres.h"
+
+ #include "access/xact.h"
+ #include "catalog/pg_type.h"
+ #include "foreign/foreign.h"
+ #include "funcapi.h"
+ #include "libpq-fe.h"
+ #include "mb/pg_wchar.h"
+ #include "miscadmin.h"
+ #include "utils/array.h"
+ #include "utils/builtins.h"
+ #include "utils/hsearch.h"
+ #include "utils/memutils.h"
+ #include "utils/resowner.h"
+ #include "utils/tuplestore.h"
+
+ #include "pgsql_fdw.h"
+ #include "connection.h"
+
+ /* ============================================================================
+ * Connection management functions
+ * ==========================================================================*/
+
+ /*
+ * Connection cache entry managed with hash table.
+ */
+ typedef struct ConnCacheEntry
+ {
+ /* hash key must be first */
+ Oid serverid; /* oid of foreign server */
+ Oid userid; /* oid of local user */
+
+ bool use_tx; /* true when using remote transaction */
+ 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);
+ static void begin_remote_tx(PGconn *conn);
+ static void abort_remote_tx(PGconn *conn);
+
+ /*
+ * 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.
+ *
+ * When use_tx is true, remote transaction is started if caller is the only
+ * user of the connection. Isolation level of the remote transaction is same
+ * as local transaction, and remote transaction will be aborted when last
+ * user release.
+ *
+ * TODO: Note that caching connections requires a mechanism to detect change of
+ * FDW object to invalidate already established connections.
+ */
+ PGconn *
+ GetConnection(ForeignServer *server, UserMapping *user, bool use_tx)
+ {
+ bool found;
+ ConnCacheEntry *entry;
+ ConnCacheEntry key;
+
+ /* initialize connection cache if it isn't */
+ if (FSConnectionHash == NULL)
+ {
+ HASHCTL ctl;
+
+ /* hash key is a pair of oids: serverid and userid */
+ MemSet(&ctl, 0, sizeof(ctl));
+ ctl.keysize = sizeof(Oid) + sizeof(Oid);
+ ctl.entrysize = sizeof(ConnCacheEntry);
+ ctl.hash = tag_hash;
+ ctl.match = memcmp;
+ ctl.keycopy = memcpy;
+ /* allocate FSConnectionHash in the cache context */
+ ctl.hcxt = CacheMemoryContext;
+ FSConnectionHash = hash_create("Foreign Connections", 32,
+ &ctl,
+ HASH_ELEM | HASH_CONTEXT |
+ HASH_FUNCTION | HASH_COMPARE |
+ HASH_KEYCOPY);
+ }
+
+ /* Create key value for the entry. */
+ MemSet(&key, 0, sizeof(key));
+ key.serverid = server->serverid;
+ key.userid = GetOuterUserId();
+
+ /*
+ * Find cached entry for requested connection. If we couldn't find,
+ * callback function of ResourceOwner should be registered to clean the
+ * connection up on error including user interrupt.
+ */
+ entry = hash_search(FSConnectionHash, &key, HASH_ENTER, &found);
+ if (!found)
+ {
+ entry->refs = 0;
+ entry->use_tx = false;
+ entry->conn = NULL;
+ RegisterResourceReleaseCallback(cleanup_connection, entry);
+ }
+
+ /*
+ * We don't check the health of cached connection here, because it would
+ * require some overhead. Broken connection and its cache entry will be
+ * cleaned up when the connection is actually used.
+ */
+
+ /*
+ * If cache entry doesn't have connection, we have to establish new
+ * connection.
+ */
+ if (entry->conn == NULL)
+ {
+ /*
+ * Use PG_TRY block to ensure closing connection on error.
+ */
+ PG_TRY();
+ {
+ /*
+ * Connect to the foreign PostgreSQL server, and store it in cache
+ * entry to keep new connection.
+ * Note: key items of entry has already been initialized in
+ * hash_search(HASH_ENTER).
+ */
+ entry->conn = connect_pg_server(server, user);
+ }
+ PG_CATCH();
+ {
+ /* Clear connection cache entry on error case. */
+ PQfinish(entry->conn);
+ entry->refs = 0;
+ entry->use_tx = false;
+ entry->conn = NULL;
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+ }
+
+ /* Increase connection reference counter. */
+ entry->refs++;
+
+ /*
+ * If requester is the only referrer of this connection, start transaction
+ * with the same isolation level as the local transaction we are in.
+ * We need to remember whether this connection uses remote transaction to
+ * abort it when this connection is released completely.
+ */
+ if (use_tx && entry->refs == 1)
+ begin_remote_tx(entry->conn);
+ entry->use_tx = use_tx;
+
+
+ return entry->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 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. Those two object hold only libpq options.
+ * Extra 3 items are for:
+ * *) fallback_application_name
+ * *) client_encoding
+ * *) NULL termination (end marker)
+ *
+ * Note: We don't omit any parameters even target database might be older
+ * than local, because unexpected parameters are just ignored.
+ */
+ n = list_length(server->options) + list_length(user->options) + 3;
+ 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 += ExtractConnectionOptions(server->options,
+ all_keywords + n, all_values + n);
+ n += ExtractConnectionOptions(user->options,
+ all_keywords + n, all_values + n);
+ all_keywords[n] = all_values[n] = NULL;
+
+ for (i = 0, j = 0; all_keywords[i]; i++)
+ {
+ keywords[j] = all_keywords[i];
+ values[j] = all_values[i];
+ j++;
+ }
+
+ /* Use "pgsql_fdw" as fallback_application_name. */
+ keywords[j] = "fallback_application_name";
+ values[j++] = "pgsql_fdw";
+
+ /* Set client_encoding so that libpq can convert encoding properly. */
+ keywords[j] = "client_encoding";
+ values[j++] = GetDatabaseEncodingName();
+
+ keywords[j] = values[j] = NULL;
+ pfree(all_keywords);
+ pfree(all_values);
+
+ /* 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.")));
+ }
+
+ return conn;
+ }
+
+ /*
+ * Start transaction to use cursor to retrieve data separately.
+ */
+ static void
+ begin_remote_tx(PGconn *conn)
+ {
+ const char *sql = NULL; /* keep compiler quiet. */
+ PGresult *res;
+
+ switch (XactIsoLevel)
+ {
+ case XACT_READ_UNCOMMITTED:
+ sql = "START TRANSACTION ISOLATION LEVEL READ UNCOMMITTED";
+ break;
+ case XACT_READ_COMMITTED:
+ sql = "START TRANSACTION ISOLATION LEVEL READ COMMITTED";
+ break;
+ case XACT_REPEATABLE_READ:
+ sql = "START TRANSACTION ISOLATION LEVEL REPEATABLE READ";
+ break;
+ case XACT_SERIALIZABLE:
+ sql = "START TRANSACTION ISOLATION LEVEL SERIALIZABLE";
+ break;
+ default:
+ elog(ERROR, "unexpected isolation level: %d", XactIsoLevel);
+ break;
+ }
+
+ elog(DEBUG3, "starting remote transaction with \"%s\"", sql);
+
+ res = PQexec(conn, sql);
+ if (PQresultStatus(res) != PGRES_COMMAND_OK)
+ {
+ PQclear(res);
+ elog(ERROR, "could not start transaction: %s", PQerrorMessage(conn));
+ }
+ PQclear(res);
+ }
+
+ static void
+ abort_remote_tx(PGconn *conn)
+ {
+ PGresult *res;
+
+ elog(DEBUG3, "aborting remote transaction");
+
+ res = PQexec(conn, "ABORT TRANSACTION");
+ if (PQresultStatus(res) != PGRES_COMMAND_OK)
+ {
+ PQclear(res);
+ elog(ERROR, "could not abort transaction: %s", PQerrorMessage(conn));
+ }
+ PQclear(res);
+ }
+
+ /*
+ * 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 sequentially 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;
+ }
+ if (entry != NULL)
+ hash_seq_term(&scan);
+
+ /*
+ * If this connection uses remote transaction and caller is the last user,
+ * abort remote transaction and forget about it.
+ */
+ if (entry->use_tx && entry->refs == 1)
+ {
+ abort_remote_tx(conn);
+ entry->use_tx = false;
+ }
+
+ /*
+ * If the released connection was an orphan, just close it.
+ */
+ if (entry == NULL)
+ {
+ PQfinish(conn);
+ return;
+ }
+
+ /*
+ * Decrease reference counter of this connection. Even if the caller was
+ * the last referrer, we don't unregister it from cache.
+ */
+ entry->refs--;
+ }
+
+ /*
+ * 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)
+ {
+ PQfinish(entry->conn);
+ entry->refs = 0;
+ entry->conn = NULL;
+ }
+ }
+
+ /*
+ * Get list of connections currently active.
+ */
+ Datum pgsql_fdw_get_connections(PG_FUNCTION_ARGS);
+ PG_FUNCTION_INFO_V1(pgsql_fdw_get_connections);
+ Datum
+ pgsql_fdw_get_connections(PG_FUNCTION_ARGS)
+ {
+ ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+ HASH_SEQ_STATUS scan;
+ ConnCacheEntry *entry;
+ MemoryContext oldcontext = CurrentMemoryContext;
+ Tuplestorestate *tuplestore;
+ TupleDesc tupdesc;
+
+ /* We return list of connection with storing them in a Tuplestore. */
+ rsinfo->returnMode = SFRM_Materialize;
+ rsinfo->setResult = NULL;
+ rsinfo->setDesc = NULL;
+
+ /* Create tuplestore and copy of TupleDesc in per-query context. */
+ MemoryContextSwitchTo(rsinfo->econtext->ecxt_per_query_memory);
+
+ tupdesc = CreateTemplateTupleDesc(2, false);
+ TupleDescInitEntry(tupdesc, 1, "srvid", OIDOID, -1, 0);
+ TupleDescInitEntry(tupdesc, 2, "usesysid", OIDOID, -1, 0);
+ rsinfo->setDesc = tupdesc;
+
+ tuplestore = tuplestore_begin_heap(false, false, work_mem);
+ rsinfo->setResult = tuplestore;
+
+ MemoryContextSwitchTo(oldcontext);
+
+ /*
+ * We need to scan sequentially since we use the address to find
+ * appropriate PGconn from the hash table.
+ */
+ if (FSConnectionHash != NULL)
+ {
+ hash_seq_init(&scan, FSConnectionHash);
+ while ((entry = (ConnCacheEntry *) hash_seq_search(&scan)))
+ {
+ Datum values[2];
+ bool nulls[2];
+ HeapTuple tuple;
+
+ /* Ignore inactive connections */
+ if (PQstatus(entry->conn) != CONNECTION_OK)
+ continue;
+
+ /*
+ * Ignore other users' connections if current user isn't a
+ * superuser.
+ */
+ if (!superuser() && entry->userid != GetUserId())
+ continue;
+
+ values[0] = ObjectIdGetDatum(entry->serverid);
+ values[1] = ObjectIdGetDatum(entry->userid);
+ nulls[0] = false;
+ nulls[1] = false;
+
+ tuple = heap_formtuple(tupdesc, values, nulls);
+ tuplestore_puttuple(tuplestore, tuple);
+ }
+ }
+ tuplestore_donestoring(tuplestore);
+
+ PG_RETURN_VOID();
+ }
+
+ /*
+ * Discard persistent connection designated by given connection name.
+ */
+ Datum pgsql_fdw_disconnect(PG_FUNCTION_ARGS);
+ PG_FUNCTION_INFO_V1(pgsql_fdw_disconnect);
+ Datum
+ pgsql_fdw_disconnect(PG_FUNCTION_ARGS)
+ {
+ Oid serverid = PG_GETARG_OID(0);
+ Oid userid = PG_GETARG_OID(1);
+ ConnCacheEntry key;
+ ConnCacheEntry *entry = NULL;
+ bool found;
+
+ /* Non-superuser can't discard other users' connection. */
+ if (!superuser() && userid != GetOuterUserId())
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_RESOURCES),
+ errmsg("only superuser can discard other user's connection")));
+
+ /*
+ * If no connection has been established, or no such connections, just
+ * return "NG" to indicate nothing has done.
+ */
+ if (FSConnectionHash == NULL)
+ PG_RETURN_TEXT_P(cstring_to_text("NG"));
+
+ key.serverid = serverid;
+ key.userid = userid;
+ entry = hash_search(FSConnectionHash, &key, HASH_FIND, &found);
+ if (!found)
+ PG_RETURN_TEXT_P(cstring_to_text("NG"));
+
+ /* Discard cached connection, and clear reference counter. */
+ PQfinish(entry->conn);
+ entry->refs = 0;
+ entry->conn = NULL;
+
+ PG_RETURN_TEXT_P(cstring_to_text("OK"));
+ }
diff --git a/contrib/pgsql_fdw/connection.h b/contrib/pgsql_fdw/connection.h
index ...4427f56 .
*** a/contrib/pgsql_fdw/connection.h
--- b/contrib/pgsql_fdw/connection.h
***************
*** 0 ****
--- 1,25 ----
+ /*-------------------------------------------------------------------------
+ *
+ * connection.h
+ * Connection management for pgsql_fdw
+ *
+ * Portions Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_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, bool use_tx);
+ void ReleaseConnection(PGconn *conn);
+
+ #endif /* CONNECTION_H */
diff --git a/contrib/pgsql_fdw/deparse.c b/contrib/pgsql_fdw/deparse.c
index ...7ca2767 .
*** a/contrib/pgsql_fdw/deparse.c
--- b/contrib/pgsql_fdw/deparse.c
***************
*** 0 ****
--- 1,210 ----
+ /*-------------------------------------------------------------------------
+ *
+ * deparse.c
+ * query deparser for PostgreSQL
+ *
+ * Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/deparse.c
+ *
+ *-------------------------------------------------------------------------
+ */
+ #include "postgres.h"
+
+ #include "access/transam.h"
+ #include "foreign/foreign.h"
+ #include "lib/stringinfo.h"
+ #include "nodes/nodeFuncs.h"
+ #include "nodes/nodes.h"
+ #include "nodes/makefuncs.h"
+ #include "optimizer/clauses.h"
+ #include "optimizer/var.h"
+ #include "parser/parsetree.h"
+ #include "utils/builtins.h"
+ #include "utils/lsyscache.h"
+
+ #include "pgsql_fdw.h"
+
+ /*
+ * Context for walk-through the expression tree.
+ */
+ typedef struct foreign_executable_cxt
+ {
+ PlannerInfo *root;
+ RelOptInfo *foreignrel;
+ } foreign_executable_cxt;
+
+ /*
+ * Deparse query representation into SQL statement which suits for remote
+ * PostgreSQL server.
+ */
+ char *
+ deparseSql(Oid relid, PlannerInfo *root, RelOptInfo *baserel)
+ {
+ StringInfoData foreign_relname;
+ StringInfoData sql; /* builder for SQL statement */
+ bool first;
+ AttrNumber attr;
+ List *attr_used = NIL; /* List of AttNumber used in the query */
+ const char *nspname = NULL; /* plain namespace name */
+ const char *relname = NULL; /* plain relation name */
+ const char *q_nspname; /* quoted namespace name */
+ const char *q_relname; /* quoted relation name */
+ int i;
+ List *rtable = NIL;
+ List *context = NIL;
+
+ initStringInfo(&sql);
+ initStringInfo(&foreign_relname);
+
+ /*
+ * First of all, determine which column should be retrieved for this scan.
+ *
+ * We do this before deparsing SELECT clause because attributes which are
+ * not used in neither reltargetlist nor baserel->baserestrictinfo, quals
+ * evaluated on local, can be replaced with literal "NULL" in the SELECT
+ * clause to reduce overhead of tuple handling tuple and data transfer.
+ */
+ if (baserel->baserestrictinfo != NIL)
+ {
+ ListCell *lc;
+
+ foreach (lc, baserel->baserestrictinfo)
+ {
+ RestrictInfo *ri = (RestrictInfo *) lfirst(lc);
+ List *attrs;
+
+ /*
+ * We need to know which attributes are used in qual evaluated
+ * on the local server, because they should be listed in the
+ * SELECT clause of remote query. We can ignore attributes
+ * which are referenced only in ORDER BY/GROUP BY clause because
+ * such attributes has already been kept in reltargetlist.
+ */
+ attrs = pull_var_clause((Node *) ri->clause,
+ PVC_RECURSE_AGGREGATES,
+ PVC_RECURSE_PLACEHOLDERS);
+ attr_used = list_union(attr_used, attrs);
+ }
+ }
+
+ /*
+ * Determine foreign relation's qualified name. This is necessary for
+ * FROM clause and SELECT clause.
+ */
+ nspname = GetFdwOptionValue(InvalidOid, InvalidOid, relid,
+ InvalidAttrNumber, "nspname");
+ if (nspname == NULL)
+ nspname = get_namespace_name(get_rel_namespace(relid));
+ q_nspname = quote_identifier(nspname);
+
+ relname = GetFdwOptionValue(InvalidOid, InvalidOid, relid,
+ InvalidAttrNumber, "relname");
+ if (relname == NULL)
+ relname = get_rel_name(relid);
+ q_relname = quote_identifier(relname);
+
+ appendStringInfo(&foreign_relname, "%s.%s", q_nspname, q_relname);
+
+ /*
+ * We need to replace aliasname and colnames of the target relation so that
+ * constructed remote query is valid.
+ *
+ * Note that we skip first empty element of simple_rel_array. See also
+ * comments of simple_rel_array and simple_rte_array for the rationale.
+ */
+ for (i = 1; i < root->simple_rel_array_size; i++)
+ {
+ RangeTblEntry *rte = copyObject(root->simple_rte_array[i]);
+ List *newcolnames = NIL;
+
+ if (i == baserel->relid)
+ {
+ /*
+ * Create new list of column names which is used to deparse remote
+ * query from specified names or local column names. This list is
+ * used by deparse_expression.
+ */
+ for (attr = 1; attr <= baserel->max_attr; attr++)
+ {
+ char *colname = NULL;
+
+ /*
+ * Get colname option for each undropped attribute. If colname
+ * option is not supplied, use local column name in remote
+ * query.
+ */
+ if (get_rte_attribute_is_dropped(rte, attr))
+ colname = "";
+ else
+ {
+ colname = GetFdwOptionValue(InvalidOid, InvalidOid, relid,
+ attr, "colname");
+ if (colname == NULL)
+ colname = strVal(list_nth(rte->eref->colnames,
+ attr - 1));
+ }
+ newcolnames = lappend(newcolnames, makeString(colname));
+ }
+ rte->alias = makeAlias(relname, newcolnames);
+ }
+ rtable = lappend(rtable, rte);
+ }
+ context = deparse_context_for_rtelist(rtable);
+
+ /*
+ * deparse SELECT clause
+ *
+ * List attributes which are in either target list or local restriction.
+ * Unused attributes are replaced with a literal "NULL" for optimization.
+ *
+ * Note that nothing is added for dropped columns, though tuple constructor
+ * function requires entries for dropped columns. Such entries must be
+ * initialized with NULL before calling tuple constructor.
+ */
+ appendStringInfo(&sql, "SELECT ");
+ attr_used = list_union(attr_used, baserel->reltargetlist);
+ first = true;
+ for (attr = 1; attr <= baserel->max_attr; attr++)
+ {
+ RangeTblEntry *rte = root->simple_rte_array[baserel->relid];
+ Var *var = NULL;
+ ListCell *lc;
+
+ /* Ignore dropped attributes. */
+ if (get_rte_attribute_is_dropped(rte, attr))
+ continue;
+
+ if (!first)
+ appendStringInfo(&sql, ", ");
+ first = false;
+
+ /*
+ * We use linear search here, but it wouldn't be problem since
+ * attr_used seems to not become so large.
+ */
+ foreach (lc, attr_used)
+ {
+ var = lfirst(lc);
+ if (var->varattno == attr)
+ break;
+ var = NULL;
+ }
+ if (var != NULL)
+ appendStringInfo(&sql, "%s",
+ deparse_expression((Node *) var, context, false, false));
+ else
+ appendStringInfo(&sql, "NULL");
+ }
+ appendStringInfoChar(&sql, ' ');
+
+ /*
+ * deparse FROM clause, including alias if any
+ */
+ appendStringInfo(&sql, "FROM %s", foreign_relname.data);
+
+ elog(DEBUG3, "Remote SQL: %s", sql.data);
+ return sql.data;
+ }
+
diff --git a/contrib/pgsql_fdw/expected/pgsql_fdw.out b/contrib/pgsql_fdw/expected/pgsql_fdw.out
index ...dfe6351 .
*** a/contrib/pgsql_fdw/expected/pgsql_fdw.out
--- b/contrib/pgsql_fdw/expected/pgsql_fdw.out
***************
*** 0 ****
--- 1,547 ----
+ -- ===================================================================
+ -- create FDW objects
+ -- ===================================================================
+ -- Clean up in case a prior regression run failed
+ -- Suppress NOTICE messages when roles don't exist
+ SET client_min_messages TO 'error';
+ DROP ROLE IF EXISTS pgsql_fdw_user;
+ RESET client_min_messages;
+ CREATE ROLE pgsql_fdw_user LOGIN SUPERUSER;
+ SET SESSION AUTHORIZATION 'pgsql_fdw_user';
+ CREATE EXTENSION pgsql_fdw;
+ CREATE SERVER loopback1 FOREIGN DATA WRAPPER pgsql_fdw;
+ CREATE SERVER loopback2 FOREIGN DATA WRAPPER pgsql_fdw
+ OPTIONS (dbname 'contrib_regression');
+ CREATE USER MAPPING FOR public SERVER loopback1
+ OPTIONS (user 'value', password 'value');
+ CREATE USER MAPPING FOR pgsql_fdw_user SERVER loopback2;
+ CREATE FOREIGN TABLE ft1 (
+ c0 int,
+ c1 int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10)
+ ) SERVER loopback2;
+ ALTER FOREIGN TABLE ft1 DROP COLUMN c0;
+ CREATE FOREIGN TABLE ft2 (
+ c0 int,
+ c1 int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10)
+ ) SERVER loopback2;
+ ALTER FOREIGN TABLE ft2 DROP COLUMN c0;
+ -- ===================================================================
+ -- create objects used through FDW
+ -- ===================================================================
+ CREATE SCHEMA "S 1";
+ CREATE TABLE "S 1"."T 1" (
+ "C 1" int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10),
+ CONSTRAINT t1_pkey PRIMARY KEY ("C 1")
+ );
+ NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "t1_pkey" for table "T 1"
+ CREATE TABLE "S 1"."T 2" (
+ c1 int NOT NULL,
+ c2 text,
+ CONSTRAINT t2_pkey PRIMARY KEY (c1)
+ );
+ NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "t2_pkey" for table "T 2"
+ BEGIN;
+ TRUNCATE "S 1"."T 1";
+ INSERT INTO "S 1"."T 1"
+ SELECT id,
+ id % 10,
+ to_char(id, 'FM00000'),
+ '1970-01-01'::timestamptz + ((id % 100) || ' days')::interval,
+ '1970-01-01'::timestamp + ((id % 100) || ' days')::interval,
+ id % 10,
+ id % 10
+ FROM generate_series(1, 1000) id;
+ TRUNCATE "S 1"."T 2";
+ INSERT INTO "S 1"."T 2"
+ SELECT id,
+ 'AAA' || to_char(id, 'FM000')
+ FROM generate_series(1, 100) id;
+ COMMIT;
+ -- ===================================================================
+ -- tests for pgsql_fdw_validator
+ -- ===================================================================
+ ALTER FOREIGN DATA WRAPPER pgsql_fdw OPTIONS (host 'value'); -- ERROR
+ ERROR: invalid option "host"
+ HINT: Valid options in this context are:
+ -- requiressl, krbsrvname and gsslib are omitted because they depend on
+ -- configure option
+ ALTER SERVER loopback1 OPTIONS (
+ authtype 'value',
+ service 'value',
+ connect_timeout 'value',
+ dbname 'value',
+ host 'value',
+ hostaddr 'value',
+ port 'value',
+ --client_encoding 'value',
+ tty 'value',
+ options 'value',
+ application_name 'value',
+ --fallback_application_name 'value',
+ keepalives 'value',
+ keepalives_idle 'value',
+ keepalives_interval 'value',
+ -- requiressl 'value',
+ sslcompression 'value',
+ sslmode 'value',
+ sslcert 'value',
+ sslkey 'value',
+ sslrootcert 'value',
+ sslcrl 'value'
+ --requirepeer 'value',
+ -- krbsrvname 'value',
+ -- gsslib 'value',
+ --replication 'value'
+ );
+ ALTER SERVER loopback1 OPTIONS (user 'value'); -- ERROR
+ ERROR: invalid option "user"
+ HINT: Valid options in this context are: authtype, service, connect_timeout, dbname, host, hostaddr, port, tty, options, application_name, keepalives, keepalives_idle, keepalives_interval, keepalives_count, sslcompression, sslmode, sslcert, sslkey, sslrootcert, sslcrl, requirepeer, fetch_count
+ ALTER SERVER loopback2 OPTIONS (ADD fetch_count '2');
+ ALTER USER MAPPING FOR public SERVER loopback1
+ OPTIONS (DROP user, DROP password);
+ ALTER USER MAPPING FOR public SERVER loopback1
+ OPTIONS (host 'value'); -- ERROR
+ ERROR: invalid option "host"
+ HINT: Valid options in this context are: user, password
+ ALTER FOREIGN TABLE ft1 OPTIONS (nspname 'S 1', relname 'T 1');
+ ALTER FOREIGN TABLE ft2 OPTIONS (nspname 'S 1', relname 'T 1', fetch_count '100');
+ ALTER FOREIGN TABLE ft1 OPTIONS (invalid 'value'); -- ERROR
+ ERROR: invalid option "invalid"
+ HINT: Valid options in this context are: nspname, relname, fetch_count
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count 'a'); -- ERROR
+ ERROR: invalid value for fetch_count: "a"
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count '0'); -- ERROR
+ ERROR: invalid value for fetch_count: "0"
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count '-1'); -- ERROR
+ ERROR: invalid value for fetch_count: "-1"
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (invalid 'value'); -- ERROR
+ ERROR: invalid option "invalid"
+ HINT: Valid options in this context are: colname
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (colname 'C 1');
+ ALTER FOREIGN TABLE ft2 ALTER COLUMN c1 OPTIONS (colname 'C 1');
+ \dew+
+ List of foreign-data wrappers
+ Name | Owner | Handler | Validator | Access privileges | FDW Options | Description
+ -----------+----------------+-------------------+---------------------+-------------------+-------------+-------------
+ pgsql_fdw | pgsql_fdw_user | pgsql_fdw_handler | pgsql_fdw_validator | | |
+ (1 row)
+
+ \des+
+ List of foreign servers
+ Name | Owner | Foreign-data wrapper | Access privileges | Type | Version | FDW Options | Description
+ -----------+----------------+----------------------+-------------------+------+---------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+-------------
+ loopback1 | pgsql_fdw_user | pgsql_fdw | | | | (authtype 'value', service 'value', connect_timeout 'value', dbname 'value', host 'value', hostaddr 'value', port 'value', tty 'value', options 'value', application_name 'value', keepalives 'value', keepalives_idle 'value', keepalives_interval 'value', sslcompression 'value', sslmode 'value', sslcert 'value', sslkey 'value', sslrootcert 'value', sslcrl 'value') |
+ loopback2 | pgsql_fdw_user | pgsql_fdw | | | | (dbname 'contrib_regression', fetch_count '2') |
+ (2 rows)
+
+ \deu+
+ List of user mappings
+ Server | User name | FDW Options
+ -----------+----------------+-------------
+ loopback1 | public |
+ loopback2 | pgsql_fdw_user |
+ (2 rows)
+
+ \det+
+ List of foreign tables
+ Schema | Table | Server | FDW Options | Description
+ --------+-------+-----------+---------------------------------------------------+-------------
+ public | ft1 | loopback2 | (nspname 'S 1', relname 'T 1') |
+ public | ft2 | loopback2 | (nspname 'S 1', relname 'T 1', fetch_count '100') |
+ (2 rows)
+
+ -- ===================================================================
+ -- simple queries
+ -- ===================================================================
+ -- single table, with/without alias
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
+ QUERY PLAN
+ ------------------------------------------------------------------------------------------------------------------------------
+ Limit
+ Output: c1, c2, c3, c4, c5, c6, c7
+ -> Sort
+ Output: c1, c2, c3, c4, c5, c6, c7
+ Sort Key: ft1.c3, ft1.c1
+ -> Foreign Scan on public.ft1
+ Output: c1, c2, c3, c4, c5, c6, c7
+ Remote SQL: DECLARE pgsql_fdw_cursor_0 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (8 rows)
+
+ SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ -----+----+-------+------------------------------+--------------------------+----+------------
+ 101 | 1 | 00101 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970 | 1 | 1
+ 102 | 2 | 00102 | Sat Jan 03 00:00:00 1970 PST | Sat Jan 03 00:00:00 1970 | 2 | 2
+ 103 | 3 | 00103 | Sun Jan 04 00:00:00 1970 PST | Sun Jan 04 00:00:00 1970 | 3 | 3
+ 104 | 4 | 00104 | Mon Jan 05 00:00:00 1970 PST | Mon Jan 05 00:00:00 1970 | 4 | 4
+ 105 | 5 | 00105 | Tue Jan 06 00:00:00 1970 PST | Tue Jan 06 00:00:00 1970 | 5 | 5
+ 106 | 6 | 00106 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6
+ 107 | 7 | 00107 | Thu Jan 08 00:00:00 1970 PST | Thu Jan 08 00:00:00 1970 | 7 | 7
+ 108 | 8 | 00108 | Fri Jan 09 00:00:00 1970 PST | Fri Jan 09 00:00:00 1970 | 8 | 8
+ 109 | 9 | 00109 | Sat Jan 10 00:00:00 1970 PST | Sat Jan 10 00:00:00 1970 | 9 | 9
+ 110 | 0 | 00110 | Sun Jan 11 00:00:00 1970 PST | Sun Jan 11 00:00:00 1970 | 0 | 0
+ (10 rows)
+
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+ QUERY PLAN
+ ------------------------------------------------------------------------------------------------------------------------------
+ Limit
+ Output: c1, c2, c3, c4, c5, c6, c7
+ -> Sort
+ Output: c1, c2, c3, c4, c5, c6, c7
+ Sort Key: t1.c3, t1.c1
+ -> Foreign Scan on public.ft1 t1
+ Output: c1, c2, c3, c4, c5, c6, c7
+ Remote SQL: DECLARE pgsql_fdw_cursor_2 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (8 rows)
+
+ SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ -----+----+-------+------------------------------+--------------------------+----+------------
+ 101 | 1 | 00101 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970 | 1 | 1
+ 102 | 2 | 00102 | Sat Jan 03 00:00:00 1970 PST | Sat Jan 03 00:00:00 1970 | 2 | 2
+ 103 | 3 | 00103 | Sun Jan 04 00:00:00 1970 PST | Sun Jan 04 00:00:00 1970 | 3 | 3
+ 104 | 4 | 00104 | Mon Jan 05 00:00:00 1970 PST | Mon Jan 05 00:00:00 1970 | 4 | 4
+ 105 | 5 | 00105 | Tue Jan 06 00:00:00 1970 PST | Tue Jan 06 00:00:00 1970 | 5 | 5
+ 106 | 6 | 00106 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6
+ 107 | 7 | 00107 | Thu Jan 08 00:00:00 1970 PST | Thu Jan 08 00:00:00 1970 | 7 | 7
+ 108 | 8 | 00108 | Fri Jan 09 00:00:00 1970 PST | Fri Jan 09 00:00:00 1970 | 8 | 8
+ 109 | 9 | 00109 | Sat Jan 10 00:00:00 1970 PST | Sat Jan 10 00:00:00 1970 | 9 | 9
+ 110 | 0 | 00110 | Sun Jan 11 00:00:00 1970 PST | Sun Jan 11 00:00:00 1970 | 0 | 0
+ (10 rows)
+
+ -- with WHERE clause
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
+ QUERY PLAN
+ ------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on public.ft1 t1
+ Output: c1, c2, c3, c4, c5, c6, c7
+ Filter: ((t1.c1 = 101) AND ((t1.c6)::text = '1'::text) AND (t1.c7 = '1'::bpchar))
+ Remote SQL: DECLARE pgsql_fdw_cursor_4 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (4 rows)
+
+ SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ -----+----+-------+------------------------------+--------------------------+----+------------
+ 101 | 1 | 00101 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970 | 1 | 1
+ (1 row)
+
+ -- aggregate
+ SELECT COUNT(*) FROM ft1 t1;
+ count
+ -------
+ 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
+ ----+----+-------+------------------------------+--------------------------+----+------------
+ 1 | 1 | 00001 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970 | 1 | 1
+ 2 | 2 | 00002 | Sat Jan 03 00:00:00 1970 PST | Sat Jan 03 00:00:00 1970 | 2 | 2
+ 3 | 3 | 00003 | Sun Jan 04 00:00:00 1970 PST | Sun Jan 04 00:00:00 1970 | 3 | 3
+ 4 | 4 | 00004 | Mon Jan 05 00:00:00 1970 PST | Mon Jan 05 00:00:00 1970 | 4 | 4
+ 5 | 5 | 00005 | Tue Jan 06 00:00:00 1970 PST | Tue Jan 06 00:00:00 1970 | 5 | 5
+ 6 | 6 | 00006 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6
+ 7 | 7 | 00007 | Thu Jan 08 00:00:00 1970 PST | Thu Jan 08 00:00:00 1970 | 7 | 7
+ 8 | 8 | 00008 | Fri Jan 09 00:00:00 1970 PST | Fri Jan 09 00:00:00 1970 | 8 | 8
+ 9 | 9 | 00009 | Sat Jan 10 00:00:00 1970 PST | Sat Jan 10 00:00:00 1970 | 9 | 9
+ 10 | 0 | 00010 | Sun Jan 11 00:00:00 1970 PST | Sun Jan 11 00:00:00 1970 | 0 | 0
+ (10 rows)
+
+ -- subquery+MAX
+ SELECT * FROM ft1 t1 WHERE t1.c3 = (SELECT MAX(c3) FROM ft2 t2) ORDER BY c1;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ ------+----+-------+------------------------------+--------------------------+----+------------
+ 1000 | 0 | 01000 | Thu Jan 01 00:00:00 1970 PST | Thu Jan 01 00:00:00 1970 | 0 | 0
+ (1 row)
+
+ -- used in CTE
+ WITH t1 AS (SELECT * FROM ft1 WHERE c1 <= 10) SELECT t2.c1, t2.c2, t2.c3, t2.c4 FROM t1, ft2 t2 WHERE t1.c1 = t2.c1 ORDER BY t1.c1;
+ c1 | c2 | c3 | c4
+ ----+----+-------+------------------------------
+ 1 | 1 | 00001 | Fri Jan 02 00:00:00 1970 PST
+ 2 | 2 | 00002 | Sat Jan 03 00:00:00 1970 PST
+ 3 | 3 | 00003 | Sun Jan 04 00:00:00 1970 PST
+ 4 | 4 | 00004 | Mon Jan 05 00:00:00 1970 PST
+ 5 | 5 | 00005 | Tue Jan 06 00:00:00 1970 PST
+ 6 | 6 | 00006 | Wed Jan 07 00:00:00 1970 PST
+ 7 | 7 | 00007 | Thu Jan 08 00:00:00 1970 PST
+ 8 | 8 | 00008 | Fri Jan 09 00:00:00 1970 PST
+ 9 | 9 | 00009 | Sat Jan 10 00:00:00 1970 PST
+ 10 | 0 | 00010 | Sun Jan 11 00:00:00 1970 PST
+ (10 rows)
+
+ -- fixed values
+ SELECT 'fixed', NULL FROM ft1 t1 WHERE c1 = 1;
+ ?column? | ?column?
+ ----------+----------
+ fixed |
+ (1 row)
+
+ -- user-defined operator/function
+ CREATE FUNCTION pgsql_fdw_abs(int) RETURNS int AS $$
+ BEGIN
+ RETURN abs($1);
+ END
+ $$ LANGUAGE plpgsql IMMUTABLE;
+ CREATE OPERATOR === (
+ LEFTARG = int,
+ RIGHTARG = int,
+ PROCEDURE = int4eq,
+ COMMUTATOR = ===,
+ NEGATOR = !==
+ );
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = pgsql_fdw_abs(t1.c2);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = pgsql_fdw_abs(c2))
+ Remote SQL: DECLARE pgsql_fdw_cursor_18 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 === t1.c2;
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 === c2)
+ Remote SQL: DECLARE pgsql_fdw_cursor_19 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = abs(t1.c2);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = abs(c2))
+ Remote SQL: DECLARE pgsql_fdw_cursor_20 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = t1.c2;
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = c2)
+ Remote SQL: DECLARE pgsql_fdw_cursor_21 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ -- ===================================================================
+ -- parameterized queries
+ -- ===================================================================
+ -- simple join
+ PREPARE st1(int, int) AS SELECT t1.c3, t2.c3 FROM ft1 t1, ft2 t2 WHERE t1.c1 = $1 AND t2.c1 = $2;
+ EXPLAIN (COSTS false) EXECUTE st1(1, 2);
+ QUERY PLAN
+ -----------------------------------------------------------------------------------------------------------------------------------------
+ Nested Loop
+ -> Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_22 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, NULL, NULL, NULL, NULL FROM "S 1"."T 1"
+ -> Materialize
+ -> Foreign Scan on ft2 t2
+ Filter: (c1 = 2)
+ Remote SQL: DECLARE pgsql_fdw_cursor_23 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, NULL, NULL, NULL, NULL FROM "S 1"."T 1"
+ (8 rows)
+
+ EXECUTE st1(1, 1);
+ c3 | c3
+ -------+-------
+ 00001 | 00001
+ (1 row)
+
+ EXECUTE st1(101, 101);
+ c3 | c3
+ -------+-------
+ 00101 | 00101
+ (1 row)
+
+ -- subquery using stable function (can't be pushed down)
+ PREPARE st2(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c4) = 6) ORDER BY c1;
+ EXPLAIN (COSTS false) EXECUTE st2(10, 20);
+ QUERY PLAN
+ ---------------------------------------------------------------------------------------------------------------------------------------------------
+ Sort
+ Sort Key: t1.c1
+ -> Hash Join
+ Hash Cond: (t1.c3 = t2.c3)
+ -> Foreign Scan on ft1 t1
+ Filter: (c1 < 20)
+ Remote SQL: DECLARE pgsql_fdw_cursor_28 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ -> Hash
+ -> HashAggregate
+ -> Foreign Scan on ft2 t2
+ Filter: ((c1 > 10) AND (date_part('dow'::text, c4) = 6::double precision))
+ Remote SQL: DECLARE pgsql_fdw_cursor_29 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, c4, NULL, NULL, NULL FROM "S 1"."T 1"
+ (12 rows)
+
+ EXECUTE st2(10, 20);
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ ----+----+-------+------------------------------+--------------------------+----+------------
+ 16 | 6 | 00016 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970 | 6 | 6
+ (1 row)
+
+ EXECUTE st1(101, 101);
+ c3 | c3
+ -------+-------
+ 00101 | 00101
+ (1 row)
+
+ -- subquery using immutable function (can be pushed down)
+ PREPARE st3(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c5) = 6) ORDER BY c1;
+ EXPLAIN (COSTS false) EXECUTE st3(10, 20);
+ QUERY PLAN
+ ---------------------------------------------------------------------------------------------------------------------------------------------------
+ Sort
+ Sort Key: t1.c1
+ -> Hash Join
+ Hash Cond: (t1.c3 = t2.c3)
+ -> Foreign Scan on ft1 t1
+ Filter: (c1 < 20)
+ Remote SQL: DECLARE pgsql_fdw_cursor_34 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ -> Hash
+ -> HashAggregate
+ -> Foreign Scan on ft2 t2
+ Filter: ((c1 > 10) AND (date_part('dow'::text, c5) = 6::double precision))
+ Remote SQL: DECLARE pgsql_fdw_cursor_35 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, NULL, c5, NULL, NULL FROM "S 1"."T 1"
+ (12 rows)
+
+ EXECUTE st3(10, 20);
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ ----+----+-------+------------------------------+--------------------------+----+------------
+ 16 | 6 | 00016 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970 | 6 | 6
+ (1 row)
+
+ EXECUTE st3(20, 30);
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ ----+----+-------+------------------------------+--------------------------+----+------------
+ 23 | 3 | 00023 | Sat Jan 24 00:00:00 1970 PST | Sat Jan 24 00:00:00 1970 | 3 | 3
+ (1 row)
+
+ -- custom plan should be chosen
+ PREPARE st4(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 = $1;
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_40 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_41 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_42 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_43 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_44 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_46 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ -- cleanup
+ DEALLOCATE st1;
+ DEALLOCATE st2;
+ DEALLOCATE st3;
+ DEALLOCATE st4;
+ -- ===================================================================
+ -- connection management
+ -- ===================================================================
+ SELECT srvname, usename FROM pgsql_fdw_connections;
+ srvname | usename
+ -----------+----------------
+ loopback2 | pgsql_fdw_user
+ (1 row)
+
+ SELECT pgsql_fdw_disconnect(srvid, usesysid) FROM pgsql_fdw_get_connections();
+ pgsql_fdw_disconnect
+ ----------------------
+ OK
+ (1 row)
+
+ SELECT srvname, usename FROM pgsql_fdw_connections;
+ srvname | usename
+ ---------+---------
+ (0 rows)
+
+ -- ===================================================================
+ -- cleanup
+ -- ===================================================================
+ DROP OPERATOR === (int, int) CASCADE;
+ DROP OPERATOR !== (int, int) CASCADE;
+ DROP FUNCTION pgsql_fdw_abs(int);
+ DROP SCHEMA "S 1" CASCADE;
+ NOTICE: drop cascades to 2 other objects
+ DETAIL: drop cascades to table "S 1"."T 1"
+ drop cascades to table "S 1"."T 2"
+ DROP EXTENSION pgsql_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 server loopback2
+ drop cascades to user mapping for pgsql_fdw_user
+ drop cascades to foreign table ft1
+ drop cascades to foreign table ft2
+ \c
+ DROP ROLE pgsql_fdw_user;
diff --git a/contrib/pgsql_fdw/option.c b/contrib/pgsql_fdw/option.c
index ...7e22795 .
*** a/contrib/pgsql_fdw/option.c
--- b/contrib/pgsql_fdw/option.c
***************
*** 0 ****
--- 1,247 ----
+ /*-------------------------------------------------------------------------
+ *
+ * option.c
+ * FDW option handling
+ *
+ * Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/option.c
+ *
+ *-------------------------------------------------------------------------
+ */
+ #include "postgres.h"
+
+ #include "access/reloptions.h"
+ #include "catalog/pg_foreign_data_wrapper.h"
+ #include "catalog/pg_foreign_server.h"
+ #include "catalog/pg_foreign_table.h"
+ #include "catalog/pg_user_mapping.h"
+ #include "fmgr.h"
+ #include "foreign/foreign.h"
+ #include "lib/stringinfo.h"
+ #include "miscadmin.h"
+
+ #include "pgsql_fdw.h"
+
+ /*
+ * SQL functions
+ */
+ extern Datum pgsql_fdw_validator(PG_FUNCTION_ARGS);
+ PG_FUNCTION_INFO_V1(pgsql_fdw_validator);
+
+ /*
+ * Describes the valid options for objects that use this wrapper.
+ */
+ typedef struct PgsqlFdwOption
+ {
+ const char *optname;
+ Oid optcontext; /* Oid of catalog in which options may appear */
+ bool is_libpq_opt; /* true if it's used in libpq */
+ } PgsqlFdwOption;
+
+ /*
+ * Valid options for pgsql_fdw.
+ */
+ static PgsqlFdwOption valid_options[] = {
+
+ /*
+ * Options for libpq connection.
+ * Note: This list should be updated along with PQconninfoOptions in
+ * interfaces/libpq/fe-connect.c, so the order is kept as is.
+ *
+ * Some useless libpq connection options are not accepted by pgsql_fdw:
+ * client_encoding: set to local database encoding automatically
+ * fallback_application_name: fixed to "pgsql_fdw"
+ * replication: pgsql_fdw never be replication client
+ */
+ {"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},
+ #ifdef NOT_USED
+ {"client_encoding", ForeignServerRelationId, true},
+ #endif
+ {"tty", ForeignServerRelationId, true},
+ {"options", ForeignServerRelationId, true},
+ {"application_name", ForeignServerRelationId, true},
+ #ifdef NOT_USED
+ {"fallback_application_name", ForeignServerRelationId, true},
+ #endif
+ {"keepalives", ForeignServerRelationId, true},
+ {"keepalives_idle", ForeignServerRelationId, true},
+ {"keepalives_interval", ForeignServerRelationId, true},
+ {"keepalives_count", ForeignServerRelationId, true},
+ #ifdef USE_SSL
+ {"requiressl", ForeignServerRelationId, true},
+ #endif
+ {"sslcompression", ForeignServerRelationId, true},
+ {"sslmode", ForeignServerRelationId, true},
+ {"sslcert", ForeignServerRelationId, true},
+ {"sslkey", ForeignServerRelationId, true},
+ {"sslrootcert", ForeignServerRelationId, true},
+ {"sslcrl", ForeignServerRelationId, true},
+ {"requirepeer", ForeignServerRelationId, true},
+ #if defined(KRB5) || defined(ENABLE_GSS) || defined(ENABLE_SSPI)
+ {"krbsrvname", ForeignServerRelationId, true},
+ #endif
+ #if defined(ENABLE_GSS) && defined(ENABLE_SSPI)
+ {"gsslib", ForeignServerRelationId, true},
+ #endif
+ #ifdef NOT_USED
+ {"replication", ForeignServerRelationId, true},
+ #endif
+
+ /*
+ * Options for translation of object names.
+ */
+ {"nspname", ForeignTableRelationId, false},
+ {"relname", ForeignTableRelationId, false},
+ {"colname", AttributeRelationId, false},
+
+ /*
+ * Options for cursor behavior.
+ * These options can be overridden by finer-grained objects.
+ */
+ {"fetch_count", ForeignTableRelationId, false},
+ {"fetch_count", ForeignServerRelationId, false},
+
+ /* Terminating entry --- MUST BE LAST */
+ {NULL, InvalidOid, false}
+ };
+
+ /*
+ * Helper functions
+ */
+ static bool is_valid_option(const char *optname, Oid context);
+
+ /*
+ * Validate the generic options given to a FOREIGN DATA WRAPPER, SERVER,
+ * USER MAPPING or FOREIGN TABLE that uses pgsql_fdw.
+ *
+ * Raise an ERROR if the option or its value is considered invalid.
+ */
+ Datum
+ pgsql_fdw_validator(PG_FUNCTION_ARGS)
+ {
+ List *options_list = untransformRelOptions(PG_GETARG_DATUM(0));
+ Oid catalog = PG_GETARG_OID(1);
+ ListCell *cell;
+
+ /*
+ * Check that only options supported by pgsql_fdw, and allowed for the
+ * current object type, are given.
+ */
+ foreach(cell, options_list)
+ {
+ DefElem *def = (DefElem *) lfirst(cell);
+
+ if (!is_valid_option(def->defname, catalog))
+ {
+ PgsqlFdwOption *opt;
+ StringInfoData buf;
+
+ /*
+ * Unknown option specified, complain about it. Provide a hint
+ * with list of valid options for the object.
+ */
+ initStringInfo(&buf);
+ for (opt = valid_options; opt->optname; opt++)
+ {
+ if (catalog == opt->optcontext)
+ appendStringInfo(&buf, "%s%s", (buf.len > 0) ? ", " : "",
+ opt->optname);
+ }
+
+ ereport(ERROR,
+ (errcode(ERRCODE_FDW_INVALID_OPTION_NAME),
+ errmsg("invalid option \"%s\"", def->defname),
+ errhint("Valid options in this context are: %s",
+ buf.data)));
+ }
+
+ /* fetch_count be positive digit number. */
+ if (strcmp(def->defname, "fetch_count") == 0)
+ {
+ long value;
+ char *p = NULL;
+
+ value = strtol(strVal(def->arg), &p, 10);
+ if (*p != '\0' || value < 1)
+ ereport(ERROR,
+ (errcode(ERRCODE_FDW_INVALID_ATTRIBUTE_VALUE),
+ errmsg("invalid value for %s: \"%s\"",
+ def->defname, strVal(def->arg))));
+ }
+ }
+
+ /*
+ * We don't care option-specific limitation here; they will be validated at
+ * the execution time.
+ */
+
+ PG_RETURN_VOID();
+ }
+
+ /*
+ * Check whether the given option is one of the valid pgsql_fdw options.
+ * context is the Oid of the catalog holding the object the option is for.
+ */
+ static bool
+ is_valid_option(const char *optname, Oid context)
+ {
+ PgsqlFdwOption *opt;
+
+ for (opt = valid_options; opt->optname; opt++)
+ {
+ if (context == opt->optcontext && strcmp(opt->optname, optname) == 0)
+ return true;
+ }
+ return false;
+ }
+
+ /*
+ * Check whether the given option is one of the valid libpq options.
+ * context is the Oid of the catalog holding the object the option is for.
+ */
+ static bool
+ is_libpq_option(const char *optname)
+ {
+ PgsqlFdwOption *opt;
+
+ for (opt = valid_options; opt->optname; opt++)
+ {
+ if (strcmp(opt->optname, optname) == 0 && opt->is_libpq_opt)
+ return true;
+ }
+ return false;
+ }
+
+ /*
+ * Generate key-value arrays which includes only libpq options from the list
+ * which contains any kind of options.
+ */
+ int
+ ExtractConnectionOptions(List *defelems, const char **keywords, const char **values)
+ {
+ ListCell *lc;
+ int i;
+
+ i = 0;
+ foreach(lc, defelems)
+ {
+ DefElem *d = (DefElem *) lfirst(lc);
+ if (is_libpq_option(d->defname))
+ {
+ keywords[i] = d->defname;
+ values[i] = strVal(d->arg);
+ i++;
+ }
+ }
+ return i;
+ }
diff --git a/contrib/pgsql_fdw/pgsql_fdw--1.0.sql b/contrib/pgsql_fdw/pgsql_fdw--1.0.sql
index ...b0ea2b2 .
*** a/contrib/pgsql_fdw/pgsql_fdw--1.0.sql
--- b/contrib/pgsql_fdw/pgsql_fdw--1.0.sql
***************
*** 0 ****
--- 1,39 ----
+ /* contrib/pgsql_fdw/pgsql_fdw--1.0.sql */
+
+ -- complain if script is sourced in psql, rather than via CREATE EXTENSION
+ \echo Use "CREATE EXTENSION pgsql_fdw" to load this file. \quit
+
+ CREATE FUNCTION pgsql_fdw_handler()
+ RETURNS fdw_handler
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C STRICT;
+
+ CREATE FUNCTION pgsql_fdw_validator(text[], oid)
+ RETURNS void
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C STRICT;
+
+ CREATE FOREIGN DATA WRAPPER pgsql_fdw
+ HANDLER pgsql_fdw_handler
+ VALIDATOR pgsql_fdw_validator;
+
+ /* connection management functions and view */
+ CREATE FUNCTION pgsql_fdw_get_connections(out srvid oid, out usesysid oid)
+ RETURNS SETOF record
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C STRICT;
+
+ CREATE FUNCTION pgsql_fdw_disconnect(oid, oid)
+ RETURNS text
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C STRICT;
+
+ CREATE VIEW pgsql_fdw_connections AS
+ SELECT c.srvid srvid,
+ s.srvname srvname,
+ c.usesysid usesysid,
+ pg_get_userbyid(c.usesysid) usename
+ FROM pgsql_fdw_get_connections() c
+ JOIN pg_catalog.pg_foreign_server s ON (s.oid = c.srvid);
+ GRANT SELECT ON pgsql_fdw_connections TO public;
+
diff --git a/contrib/pgsql_fdw/pgsql_fdw.c b/contrib/pgsql_fdw/pgsql_fdw.c
index ...50f927b .
*** a/contrib/pgsql_fdw/pgsql_fdw.c
--- b/contrib/pgsql_fdw/pgsql_fdw.c
***************
*** 0 ****
--- 1,907 ----
+ /*-------------------------------------------------------------------------
+ *
+ * pgsql_fdw.c
+ * foreign-data wrapper for remote PostgreSQL servers.
+ *
+ * Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/pgsql_fdw.c
+ *
+ *-------------------------------------------------------------------------
+ */
+ #include "postgres.h"
+ #include "fmgr.h"
+
+ #include "catalog/pg_foreign_server.h"
+ #include "catalog/pg_foreign_table.h"
+ #include "commands/explain.h"
+ #include "foreign/fdwapi.h"
+ #include "funcapi.h"
+ #include "miscadmin.h"
+ #include "nodes/nodeFuncs.h"
+ #include "optimizer/cost.h"
+ #include "optimizer/pathnode.h"
+ #include "utils/lsyscache.h"
+ #include "utils/memutils.h"
+ #include "utils/rel.h"
+
+ #include "pgsql_fdw.h"
+ #include "connection.h"
+
+ PG_MODULE_MAGIC;
+
+ /*
+ * Default fetch count for cursor. This can be overridden by fetch_count FDW
+ * option.
+ */
+ #define DEFAULT_FETCH_COUNT 10000
+
+ /*
+ * Cost to establish a connection.
+ * XXX: should be configurable per server?
+ */
+ #define CONNECTION_COSTS 100.0
+
+ /*
+ * Cost to transfer 1 byte from remote server.
+ * XXX: should be configurable per server?
+ */
+ #define TRANSFER_COSTS_PER_BYTE 0.001
+
+ /*
+ * Cursors which are used together in a local query require different name, so
+ * we use simple incremental name for that purpose. We don't care wrap around
+ * of cursor_id because it's hard to imagine that 2^32 cursors are used in a
+ * query.
+ */
+ #define CURSOR_NAME_FORMAT "pgsql_fdw_cursor_%u"
+ static uint32 cursor_id = 0;
+
+ /*
+ * Index of FDW-private items stored in FdwPlan.
+ */
+ enum FdwPrivateIndex {
+ FdwPrivateSelectSql,
+ FdwPrivateDeclareSql,
+ FdwPrivateFetchSql,
+ FdwPrivateResetSql,
+ FdwPrivateCloseSql,
+
+ /* # of elements stored in the list fdw_private */
+ FdwPrivateNum,
+ };
+
+ /*
+ * Describes an execution state of a foreign scan against a foreign table
+ * using pgsql_fdw.
+ */
+ typedef struct PgsqlFdwExecutionState
+ {
+ MemoryContext scan_cxt; /* context for per-scan lifespan data */
+
+ FdwPlan *fdwplan; /* FDW-specific planning information */
+ PGconn *conn; /* connection for the scan */
+
+ Oid *param_types; /* type array of external parameter */
+ const char **param_values; /* value array of external parameter */
+
+ int attnum; /* # of non-dropped attribute */
+ char **col_values; /* column value buffer */
+ AttInMetadata *attinmeta; /* attribute metadata */
+
+ Tuplestorestate *tuples; /* result of the scan */
+ } PgsqlFdwExecutionState;
+
+ /*
+ * SQL functions
+ */
+ extern Datum pgsql_fdw_handler(PG_FUNCTION_ARGS);
+ PG_FUNCTION_INFO_V1(pgsql_fdw_handler);
+
+ /*
+ * FDW callback routines
+ */
+ static FdwPlan *pgsqlPlanForeignScan(Oid foreigntableid,
+ PlannerInfo *root,
+ RelOptInfo *baserel);
+ static void pgsqlExplainForeignScan(ForeignScanState *node, ExplainState *es);
+ static void pgsqlBeginForeignScan(ForeignScanState *node, int eflags);
+ static TupleTableSlot *pgsqlIterateForeignScan(ForeignScanState *node);
+ static void pgsqlReScanForeignScan(ForeignScanState *node);
+ static void pgsqlEndForeignScan(ForeignScanState *node);
+
+ /*
+ * Helper functions
+ */
+ static void estimate_costs(PlannerInfo *root,
+ RelOptInfo *baserel,
+ const char *sql,
+ Oid serverid,
+ Cost *startup_cost,
+ Cost *total_cost);
+ static void execute_query(ForeignScanState *node);
+ static PGresult *fetch_result(ForeignScanState *node);
+ static void store_result(ForeignScanState *node, PGresult *res);
+ static bool contain_ext_param(Node *clause);
+ static bool contain_ext_param_walker(Node *node, void *context);
+
+ /* Exported functions, but not written in pgsql_fdw.h. */
+ void _PG_init(void);
+ void _PG_fini(void);
+
+ /*
+ * Module-specific initialization.
+ */
+ void
+ _PG_init(void)
+ {
+ }
+
+ /*
+ * Module-specific clean up.
+ */
+ void
+ _PG_fini(void)
+ {
+ }
+
+ /*
+ * The content of FdwRoutine of pgsql_fdw is never changed, so we use one
+ * static object for all scan.
+ */
+ static FdwRoutine fdwroutine = {
+
+ /* Node type */
+ T_FdwRoutine,
+
+ /* Required handler functions. */
+ pgsqlPlanForeignScan,
+ pgsqlExplainForeignScan,
+ pgsqlBeginForeignScan,
+ pgsqlIterateForeignScan,
+ pgsqlReScanForeignScan,
+ pgsqlEndForeignScan,
+
+ /* Optional handler functions. */
+ };
+
+ /*
+ * Foreign-data wrapper handler function: return a struct with pointers
+ * to my callback routines.
+ */
+ Datum
+ pgsql_fdw_handler(PG_FUNCTION_ARGS)
+ {
+ PG_RETURN_POINTER(&fdwroutine);
+ }
+
+ /*
+ * pgsqlPlanForeignScan
+ * Create a FdwPlan for a scan on the foreign table
+ */
+ static FdwPlan *
+ pgsqlPlanForeignScan(Oid foreigntableid,
+ PlannerInfo *root,
+ RelOptInfo *baserel)
+ {
+ char name[128]; /* must be larger than format + 10 */
+ StringInfoData cursor;
+ const char *fetch_count_str;
+ int fetch_count = DEFAULT_FETCH_COUNT;
+ char *sql;
+ FdwPlan *fdwplan;
+ List *fdw_private = NIL;
+ ForeignTable *table;
+ ForeignServer *server;
+
+ /* Construct FdwPlan with cost estimates */
+ fdwplan = makeNode(FdwPlan);
+ sql = deparseSql(foreigntableid, root, baserel);
+ table = GetForeignTable(foreigntableid);
+ server = GetForeignServer(table->serverid);
+ estimate_costs(root, baserel, sql, server->serverid,
+ &fdwplan->startup_cost, &fdwplan->total_cost);
+
+ /*
+ * Store plain SELECT statement in private area of FdwPlan. This will be
+ * used for executing remote query and explaining scan.
+ */
+ fdw_private = list_make1(makeString(sql));
+
+ /* Use specified fetch_count instead of default value, if any. */
+ fetch_count_str = GetFdwOptionValue(InvalidOid, InvalidOid, foreigntableid,
+ InvalidAttrNumber, "fetch_count");
+ if (fetch_count_str != NULL)
+ fetch_count = strtol(fetch_count_str, NULL, 10);
+ elog(DEBUG1, "relid=%u fetch_count=%d", foreigntableid, fetch_count);
+
+ /*
+ * We store some more information in FdwPlan to pass them beyond the
+ * boundary between planner and executor. Finally FdwPlan using cursor
+ * would hold items below:
+ *
+ * 1) plain SELECT statement (already added above)
+ * 2) SQL statement used to declare cursor
+ * 3) SQL statement used to fetch rows from cursor
+ * 4) SQL statement used to reset cursor
+ * 5) SQL statement used to close cursor
+ *
+ * These items are indexed with the enum FdwPrivateIndex, so an item
+ * can be accessed directly via list_nth(). For example of FETCH
+ * statement:
+ * list_nth(fdw_private, FdwPrivateFetchSql)
+ */
+
+ /* Construct cursor name from sequential value */
+ sprintf(name, CURSOR_NAME_FORMAT, cursor_id++);
+
+ /* Construct statement to declare cursor */
+ initStringInfo(&cursor);
+ appendStringInfo(&cursor, "DECLARE %s SCROLL CURSOR FOR %s", name, sql);
+ fdw_private = lappend(fdw_private, makeString(cursor.data));
+
+ /* Construct statement to fetch rows from cursor */
+ initStringInfo(&cursor);
+ appendStringInfo(&cursor, "FETCH %d FROM %s", fetch_count, name);
+ fdw_private = lappend(fdw_private, makeString(cursor.data));
+
+ /* Construct statement to reset cursor */
+ initStringInfo(&cursor);
+ appendStringInfo(&cursor, "MOVE ABSOLUTE 0 FROM %s", name);
+ fdw_private = lappend(fdw_private, makeString(cursor.data));
+
+ /* Construct statement to close cursor */
+ initStringInfo(&cursor);
+ appendStringInfo(&cursor, "CLOSE %s", name);
+ fdw_private = lappend(fdw_private, makeString(cursor.data));
+
+ /* Store FDW private information into FdwPlan */
+ fdwplan->fdw_private = fdw_private;
+
+ return fdwplan;
+ }
+
+ /*
+ * pgsqlExplainForeignScan
+ * Produce extra output for EXPLAIN
+ */
+ static void
+ pgsqlExplainForeignScan(ForeignScanState *node, ExplainState *es)
+ {
+ FdwPlan *fdwplan;
+ char *sql;
+
+ fdwplan = ((ForeignScan *) node->ss.ps.plan)->fdwplan;
+ sql = strVal(list_nth(fdwplan->fdw_private, FdwPrivateDeclareSql));
+ ExplainPropertyText("Remote SQL", sql, es);
+ }
+
+ /*
+ * pgsqlBeginForeignScan
+ * Initiate access to a foreign PostgreSQL table.
+ */
+ static void
+ pgsqlBeginForeignScan(ForeignScanState *node, int eflags)
+ {
+ PgsqlFdwExecutionState *festate;
+ PGconn *conn;
+ Oid relid;
+ ForeignTable *table;
+ ForeignServer *server;
+ UserMapping *user;
+ TupleTableSlot *slot = node->ss.ss_ScanTupleSlot;
+
+ /*
+ * Do nothing in EXPLAIN (no ANALYZE) case. node->fdw_state stays NULL.
+ */
+ if (eflags & EXEC_FLAG_EXPLAIN_ONLY)
+ return;
+
+ /*
+ * Save state in node->fdw_state.
+ */
+ festate = (PgsqlFdwExecutionState *) palloc(sizeof(PgsqlFdwExecutionState));
+ festate->fdwplan = ((ForeignScan *) node->ss.ps.plan)->fdwplan;
+
+ /*
+ * Create context for per-scan tuplestore under per-query context.
+ */
+ festate->scan_cxt = AllocSetContextCreate(node->ss.ps.state->es_query_cxt,
+ "pgsql_fdw",
+ ALLOCSET_DEFAULT_MINSIZE,
+ ALLOCSET_DEFAULT_INITSIZE,
+ ALLOCSET_DEFAULT_MAXSIZE);
+
+ /*
+ * Get connection to the foreign server. Connection manager would
+ * establish new connection if necessary.
+ */
+ relid = RelationGetRelid(node->ss.ss_currentRelation);
+ table = GetForeignTable(relid);
+ server = GetForeignServer(table->serverid);
+ user = GetUserMapping(GetOuterUserId(), server->serverid);
+ conn = GetConnection(server, user, true);
+ festate->conn = conn;
+
+ /* Result will be filled in first Iterate call. */
+ festate->tuples = NULL;
+
+ /* Allocate buffers for column values. */
+ {
+ TupleDesc tupdesc = slot->tts_tupleDescriptor;
+ festate->col_values = palloc(sizeof(char *) * tupdesc->natts);
+ festate->attinmeta = TupleDescGetAttInMetadata(tupdesc);
+ }
+
+ /* Allocate buffers for query parameters. */
+ {
+ ParamListInfo params = node->ss.ps.state->es_param_list_info;
+ int numParams = params ? params->numParams : 0;
+
+ if (numParams > 0)
+ {
+ festate->param_types = palloc0(sizeof(Oid) * numParams);
+ festate->param_values = palloc0(sizeof(char *) * numParams);
+ }
+ else
+ {
+ festate->param_types = NULL;
+ festate->param_values = NULL;
+ }
+ }
+
+ /* Store FDW-specific state into ForeignScanState */
+ node->fdw_state = (void *) festate;
+
+ return;
+ }
+
+ /*
+ * pgsqlIterateForeignScan
+ * Retrieve next row from the result set, or clear tuple slot to indicate
+ * EOF.
+ *
+ * Note that using per-query context when retrieving tuples from
+ * tuplestore to ensure that returned tuples can survive until next
+ * iteration because the tuple is released implicitly via ExecClearTuple.
+ * Retrieving a tuple from tuplestore in CurrentMemoryContext (it's a
+ * per-tuple context), ExecClearTuple will free dangling pointer.
+ */
+ static TupleTableSlot *
+ pgsqlIterateForeignScan(ForeignScanState *node)
+ {
+ PgsqlFdwExecutionState *festate;
+ TupleTableSlot *slot = node->ss.ss_ScanTupleSlot;
+ PGresult *res = NULL;
+ MemoryContext oldcontext = CurrentMemoryContext;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+
+ /*
+ * If this is the first call after Begin, we need to execute remote query.
+ * If the query needs cursor, we declare a cursor at first call and fetch
+ * from it in later calls.
+ */
+ if (festate->tuples == NULL)
+ execute_query(node);
+
+ /*
+ * If enough tuples are left in tuplestore, just return next tuple from it.
+ *
+ * It is necessary to switch to per-scan context to make returned tuple
+ * valid until next IterateForeignScan call, because it will be released
+ * with ExecClearTuple then. Otherwise, picked tuple is allocated in
+ * per-tuple context, and double-free of that tuple might happen.
+ */
+ MemoryContextSwitchTo(festate->scan_cxt);
+ if (tuplestore_gettupleslot(festate->tuples, true, false, slot))
+ {
+ MemoryContextSwitchTo(oldcontext);
+ return slot;
+ }
+ MemoryContextSwitchTo(oldcontext);
+
+ /*
+ * Here we need to clear partial result and fetch next bunch of tuples from
+ * from the cursor for the scan. If the fetch returns no tuple, the scan
+ * has reached the end.
+ *
+ * PGresult must be released before leaving this function.
+ */
+ PG_TRY();
+ {
+ res = fetch_result(node);
+ store_result(node, res);
+ PQclear(res);
+ res = NULL;
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+
+ /*
+ * If we got more tuples from the server cursor, return next tuple from
+ * tuplestore.
+ */
+ MemoryContextSwitchTo(festate->scan_cxt);
+ if (tuplestore_gettupleslot(festate->tuples, true, false, slot))
+ {
+ MemoryContextSwitchTo(oldcontext);
+ return slot;
+ }
+ MemoryContextSwitchTo(oldcontext);
+
+ /* We don't have any result even in remote server cursor. */
+ ExecClearTuple(slot);
+ return slot;
+ }
+
+ /*
+ * pgsqlReScanForeignScan
+ * - Restart this scan by resetting fetch location.
+ */
+ static void
+ pgsqlReScanForeignScan(ForeignScanState *node)
+ {
+ List *fdw_private;
+ char *sql;
+ PGconn *conn;
+ PGresult *res = NULL;
+ PgsqlFdwExecutionState *festate;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+
+ /* If we have not opened cursor yet, nothing to do. */
+ if (festate->tuples == NULL)
+ return;
+
+ /* Discard fetch results if any. */
+ tuplestore_clear(festate->tuples);
+
+ /* PGresult must be released before leaving this function. */
+ PG_TRY();
+ {
+ /* Reset cursor */
+ fdw_private = festate->fdwplan->fdw_private;
+ conn = festate->conn;
+ sql = strVal(list_nth(fdw_private, FdwPrivateResetSql));
+ res = PQexec(conn, sql);
+ if (PQresultStatus(res) != PGRES_COMMAND_OK)
+ {
+ ereport(ERROR,
+ (errmsg("could not rewind cursor"),
+ errdetail("%s", PQerrorMessage(conn)),
+ errhint("%s", sql)));
+ }
+ PQclear(res);
+ res = NULL;
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+ }
+
+ /*
+ * pgsqlEndForeignScan
+ * Finish scanning foreign table and dispose objects used for this scan
+ */
+ static void
+ pgsqlEndForeignScan(ForeignScanState *node)
+ {
+ List *fdw_private;
+ char *sql;
+ PGconn *conn;
+ PGresult *res = NULL;
+ PgsqlFdwExecutionState *festate;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+
+ /* if festate is NULL, we are in EXPLAIN; nothing to do */
+ if (festate == NULL)
+ return;
+
+ /* If we have not opened cursor yet, nothing to do. */
+ if (festate->tuples == NULL)
+ return;
+
+ /* Discard fetch results */
+ tuplestore_end(festate->tuples);
+ festate->tuples = NULL;
+
+ /* PGresult must be released before leaving this function. */
+ PG_TRY();
+ {
+ /* Close cursor */
+ fdw_private = festate->fdwplan->fdw_private;
+ conn = festate->conn;
+ sql = strVal(list_nth(fdw_private, FdwPrivateCloseSql));
+ res = PQexec(conn, sql);
+ if (PQresultStatus(res) != PGRES_COMMAND_OK)
+ {
+ ereport(ERROR,
+ (errmsg("could not close cursor"),
+ errdetail("%s", PQerrorMessage(conn)),
+ errhint("%s", sql)));
+ }
+ PQclear(res);
+ res = NULL;
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+
+ ReleaseConnection(festate->conn);
+
+ MemoryContextDelete(festate->scan_cxt);
+ festate->scan_cxt = NULL;
+ }
+
+ /*
+ * Estimate costs of scanning a foreign table.
+ */
+ static void
+ estimate_costs(PlannerInfo *root, RelOptInfo *baserel,
+ const char *sql, Oid serverid,
+ Cost *startup_cost, Cost *total_cost)
+ {
+ ListCell *lc;
+ ForeignServer *server;
+ UserMapping *user;
+ PGconn *conn = NULL;
+ PGresult *res = NULL;
+ StringInfoData buf;
+ char *plan;
+ char *p;
+ int n;
+
+ /*
+ * If the baserestrictinfo contains any Param node with paramkind
+ * PARAM_EXTERNAL, we need result of EXPLAIN for EXECUTE statement, not for
+ * simple SELECT statement. However, we can't get actual parameter values
+ * here, so we use fixed and large costs as second best so that planner
+ * tend to choose custom plan.
+ *
+ * See comments in plancache.c for details of custom plan.
+ */
+ foreach(lc, baserel->baserestrictinfo)
+ {
+ RestrictInfo *rs = (RestrictInfo *) lfirst(lc);
+ if (contain_ext_param((Node *) rs->clause))
+ {
+ *startup_cost = CONNECTION_COSTS;
+ *total_cost = 10000; /* groundless large costs */
+
+ return;
+ }
+ }
+
+ /*
+ * Get connection to the foreign server. Connection manager would
+ * establish new connection if necessary.
+ */
+ server = GetForeignServer(serverid);
+ user = GetUserMapping(GetOuterUserId(), server->serverid);
+ conn = GetConnection(server, user, false);
+ initStringInfo(&buf);
+ appendStringInfo(&buf, "EXPLAIN %s", sql);
+
+ /* PGresult must be released before leaving this function. */
+ PG_TRY();
+ {
+ res = PQexec(conn, buf.data);
+ if (PQresultStatus(res) != PGRES_TUPLES_OK || PQntuples(res) == 0)
+ {
+ char *msg;
+
+ msg = pstrdup(PQerrorMessage(conn));
+ ereport(ERROR,
+ (errmsg("could not execute EXPLAIN for cost estimation"),
+ errdetail("%s", msg),
+ errhint("%s", sql)));
+ }
+
+ /*
+ * Find estimation portion from top plan node. Here we search opening
+ * parentheses from the end of the line to avoid finding unexpected
+ * parentheses.
+ */
+ plan = PQgetvalue(res, 0, 0);
+ p = strrchr(plan, '(');
+ if (p == NULL)
+ elog(ERROR, "wrong EXPLAIN output: %s", plan);
+ n = sscanf(p,
+ "(cost=%lf..%lf rows=%lf width=%d)",
+ startup_cost, total_cost, &baserel->rows, &baserel->width);
+ if (n != 4)
+ elog(ERROR, "could not get estimation from EXPLAIN output");
+
+ PQclear(res);
+ res = NULL;
+ ReleaseConnection(conn);
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+
+ /*
+ * TODO Selectivity of quals which are NOT pushed down should be also
+ * considered.
+ */
+
+ /* add cost to establish connection. */
+ *startup_cost += CONNECTION_COSTS;
+ *total_cost += CONNECTION_COSTS;
+
+ /* add cost to transfer result. */
+ *total_cost += TRANSFER_COSTS_PER_BYTE * baserel->width * baserel->tuples;
+ *total_cost += cpu_tuple_cost * baserel->tuples;
+ }
+
+ /*
+ * Execute remote query with current parameters.
+ */
+ static void
+ execute_query(ForeignScanState *node)
+ {
+ FdwPlan *fdwplan;
+ PgsqlFdwExecutionState *festate;
+ ParamListInfo params = node->ss.ps.state->es_param_list_info;
+ int numParams = params ? params->numParams : 0;
+ Oid *types = NULL;
+ const char **values = NULL;
+ char *sql;
+ PGconn *conn;
+ PGresult *res = NULL;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+ types = festate->param_types;
+ values = festate->param_values;
+
+ /*
+ * Construct parameter array in text format. We don't release memory for
+ * the arrays explicitly, because the memory usage would not be very large,
+ * and anyway they will be released in context cleanup.
+ */
+ if (numParams > 0)
+ {
+ int i;
+
+ 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;
+
+ getTypeOutputInfo(types[i], &out_func_oid, &isvarlena);
+ fmgr_info(out_func_oid, &func);
+ values[i] = OutputFunctionCall(&func, params->params[i].value);
+ }
+ }
+ }
+
+ /* PGresult must be released before leaving this function. */
+ PG_TRY();
+ {
+ /*
+ * Execute remote query with parameters.
+ */
+ conn = festate->conn;
+ fdwplan = ((ForeignScan *) node->ss.ps.plan)->fdwplan;
+ sql = strVal(list_nth(fdwplan->fdw_private, FdwPrivateDeclareSql));
+ res = PQexecParams(conn, sql, numParams, types, values, NULL, NULL, 0);
+
+ /*
+ * If the query has failed, reporting details is enough here.
+ * Connection(s) which are used by this query (at least used by
+ * pgsql_fdw) will be cleaned up by the foreign connection manager.
+ */
+ if (PQresultStatus(res) != PGRES_COMMAND_OK)
+ {
+ ereport(ERROR,
+ (errmsg("could not declare cursor"),
+ errdetail("%s", PQerrorMessage(conn)),
+ errhint("%s", sql)));
+ }
+
+ /* Discard result of DECLARE statement. */
+ PQclear(res);
+ res = NULL;
+
+ /* Fetch first bunch of the result and store them into tuplestore. */
+ res = fetch_result(node);
+ store_result(node, res);
+ PQclear(res);
+ res = NULL;
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+ }
+
+ /*
+ * Fetch next partial result from remote server.
+ *
+ * Once this function has returned result records as PGresult, caller is
+ * responsible to release it, so caller should put codes which might throw
+ * exception in PG_TRY block. When an exception has been caught, release
+ * PGresult and re-throw the exception in PG_CATCH block.
+ */
+ static PGresult *
+ fetch_result(ForeignScanState *node)
+ {
+ PgsqlFdwExecutionState *festate;
+ List *fdw_private;
+ char *sql;
+ PGconn *conn;
+ PGresult *res;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+
+ /* Retrieve information for fetching result. */
+ fdw_private = festate->fdwplan->fdw_private;
+ sql = strVal(list_nth(fdw_private, FdwPrivateFetchSql));
+ conn = festate->conn;
+
+ /*
+ * Fetch result from remote server. In error case, we must release
+ * PGresult in this function to avoid memory leak because caller can't
+ * get the reference.
+ */
+ res = PQexec(conn, sql);
+ if (PQresultStatus(res) != PGRES_TUPLES_OK)
+ {
+ PQclear(res);
+ ereport(ERROR,
+ (errmsg("could not fetch rows from foreign server"),
+ errdetail("%s", PQerrorMessage(conn)),
+ errhint("%s", sql)));
+ }
+
+ return res;
+ }
+
+ /*
+ * Create tuples from PGresult and store them into tuplestore.
+ *
+ * Caller must use PG_TRY block to catch exception and release PGresult
+ * surely.
+ */
+ static void
+ store_result(ForeignScanState *node, PGresult *res)
+ {
+ int rows;
+ int row;
+ int i;
+ int nfields;
+ int attnum; /* number of non-dropped columns */
+ Form_pg_attribute *attrs;
+ TupleTableSlot *slot = node->ss.ss_ScanTupleSlot;
+ TupleDesc tupdesc = slot->tts_tupleDescriptor;
+ PgsqlFdwExecutionState *festate;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+ rows = PQntuples(res);
+ nfields = PQnfields(res);
+ attrs = tupdesc->attrs;
+
+ /* First, ensure that the tuplestore is empty. */
+ if (festate->tuples == NULL)
+ {
+ MemoryContext oldcontext = CurrentMemoryContext;
+
+ /*
+ * Create tuplestore to store result of the query in per-query context.
+ * Note that we use this memory context to avoid memory leak in error
+ * cases.
+ */
+ MemoryContextSwitchTo(festate->scan_cxt);
+ festate->tuples = tuplestore_begin_heap(false, false, work_mem);
+ MemoryContextSwitchTo(oldcontext);
+ }
+ else
+ {
+ /* We already have tuplestore, just need to clear contents of it. */
+ tuplestore_clear(festate->tuples);
+ }
+
+ /* 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)));
+
+ /* put a tuples into the slot */
+ for (row = 0; row < rows; row++)
+ {
+ int j;
+ HeapTuple tuple;
+
+ for (i = 0, j = 0; i < tupdesc->natts; i++)
+ {
+ /* skip dropped columns. */
+ if (attrs[i]->attisdropped)
+ {
+ festate->col_values[i] = NULL;
+ continue;
+ }
+
+ if (PQgetisnull(res, row, j))
+ festate->col_values[i] = NULL;
+ else
+ festate->col_values[i] = PQgetvalue(res, row, 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(festate->attinmeta, festate->col_values);
+ tuplestore_puttuple(festate->tuples, tuple);
+ }
+
+ tuplestore_donestoring(festate->tuples);
+ }
+
+ /*
+ * contain_ext_param
+ * Recursively search for Param nodes within a clause.
+ *
+ * Returns true if any parameter reference node with relkind PARAM_EXTERN
+ * found.
+ *
+ * This does not descend into subqueries, and so should be used only after
+ * reduction of sublinks to subplans, or in contexts where it's known there
+ * are no subqueries. There mustn't be outer-aggregate references either.
+ *
+ * XXX: These functions could be in core, src/backend/optimizer/util/clauses.c.
+ */
+ static bool
+ contain_ext_param(Node *clause)
+ {
+ return contain_ext_param_walker(clause, NULL);
+ }
+
+ static bool
+ contain_ext_param_walker(Node *node, void *context)
+ {
+ if (node == NULL)
+ return false;
+ if (IsA(node, Param))
+ {
+ Param *param = (Param *) node;
+
+ if (param->paramkind == PARAM_EXTERN)
+ return true; /* abort the tree traversal and return true */
+ }
+ return expression_tree_walker(node, contain_ext_param_walker, context);
+ }
diff --git a/contrib/pgsql_fdw/pgsql_fdw.control b/contrib/pgsql_fdw/pgsql_fdw.control
index ...0a9c8f4 .
*** a/contrib/pgsql_fdw/pgsql_fdw.control
--- b/contrib/pgsql_fdw/pgsql_fdw.control
***************
*** 0 ****
--- 1,5 ----
+ # pgsql_fdw extension
+ comment = 'foreign-data wrapper for remote PostgreSQL servers'
+ default_version = '1.0'
+ module_pathname = '$libdir/pgsql_fdw'
+ relocatable = true
diff --git a/contrib/pgsql_fdw/pgsql_fdw.h b/contrib/pgsql_fdw/pgsql_fdw.h
index ...f6394ce .
*** a/contrib/pgsql_fdw/pgsql_fdw.h
--- b/contrib/pgsql_fdw/pgsql_fdw.h
***************
*** 0 ****
--- 1,28 ----
+ /*-------------------------------------------------------------------------
+ *
+ * pgsql_fdw.h
+ * foreign-data wrapper for remote PostgreSQL servers.
+ *
+ * Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/pgsql_fdw.h
+ *
+ *-------------------------------------------------------------------------
+ */
+
+ #ifndef PGSQL_FDW_H
+ #define PGSQL_FDW_H
+
+ #include "postgres.h"
+ #include "nodes/relation.h"
+
+ /* in option.c */
+ int ExtractConnectionOptions(List *defelems,
+ const char **keywords,
+ const char **values);
+
+ /* in deparse.c */
+ char *deparseSql(Oid relid, PlannerInfo *root, RelOptInfo *baserel);
+
+ #endif /* PGSQL_FDW_H */
diff --git a/contrib/pgsql_fdw/sql/pgsql_fdw.sql b/contrib/pgsql_fdw/sql/pgsql_fdw.sql
index ...f257b34 .
*** a/contrib/pgsql_fdw/sql/pgsql_fdw.sql
--- b/contrib/pgsql_fdw/sql/pgsql_fdw.sql
***************
*** 0 ****
--- 1,232 ----
+ -- ===================================================================
+ -- create FDW objects
+ -- ===================================================================
+
+ -- Clean up in case a prior regression run failed
+
+ -- Suppress NOTICE messages when roles don't exist
+ SET client_min_messages TO 'error';
+
+ DROP ROLE IF EXISTS pgsql_fdw_user;
+
+ RESET client_min_messages;
+
+ CREATE ROLE pgsql_fdw_user LOGIN SUPERUSER;
+ SET SESSION AUTHORIZATION 'pgsql_fdw_user';
+
+ CREATE EXTENSION pgsql_fdw;
+
+ CREATE SERVER loopback1 FOREIGN DATA WRAPPER pgsql_fdw;
+ CREATE SERVER loopback2 FOREIGN DATA WRAPPER pgsql_fdw
+ OPTIONS (dbname 'contrib_regression');
+
+ CREATE USER MAPPING FOR public SERVER loopback1
+ OPTIONS (user 'value', password 'value');
+ CREATE USER MAPPING FOR pgsql_fdw_user SERVER loopback2;
+
+ CREATE FOREIGN TABLE ft1 (
+ c0 int,
+ c1 int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10)
+ ) SERVER loopback2;
+ ALTER FOREIGN TABLE ft1 DROP COLUMN c0;
+
+ CREATE FOREIGN TABLE ft2 (
+ c0 int,
+ c1 int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10)
+ ) SERVER loopback2;
+ ALTER FOREIGN TABLE ft2 DROP COLUMN c0;
+
+ -- ===================================================================
+ -- create objects used through FDW
+ -- ===================================================================
+ CREATE SCHEMA "S 1";
+ CREATE TABLE "S 1"."T 1" (
+ "C 1" int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10),
+ CONSTRAINT t1_pkey PRIMARY KEY ("C 1")
+ );
+ CREATE TABLE "S 1"."T 2" (
+ c1 int NOT NULL,
+ c2 text,
+ CONSTRAINT t2_pkey PRIMARY KEY (c1)
+ );
+
+ BEGIN;
+ TRUNCATE "S 1"."T 1";
+ INSERT INTO "S 1"."T 1"
+ SELECT id,
+ id % 10,
+ to_char(id, 'FM00000'),
+ '1970-01-01'::timestamptz + ((id % 100) || ' days')::interval,
+ '1970-01-01'::timestamp + ((id % 100) || ' days')::interval,
+ id % 10,
+ id % 10
+ FROM generate_series(1, 1000) id;
+ TRUNCATE "S 1"."T 2";
+ INSERT INTO "S 1"."T 2"
+ SELECT id,
+ 'AAA' || to_char(id, 'FM000')
+ FROM generate_series(1, 100) id;
+ COMMIT;
+
+ -- ===================================================================
+ -- tests for pgsql_fdw_validator
+ -- ===================================================================
+ ALTER FOREIGN DATA WRAPPER pgsql_fdw OPTIONS (host 'value'); -- ERROR
+ -- requiressl, krbsrvname and gsslib are omitted because they depend on
+ -- configure option
+ ALTER SERVER loopback1 OPTIONS (
+ authtype 'value',
+ service 'value',
+ connect_timeout 'value',
+ dbname 'value',
+ host 'value',
+ hostaddr 'value',
+ port 'value',
+ --client_encoding 'value',
+ tty 'value',
+ options 'value',
+ application_name 'value',
+ --fallback_application_name 'value',
+ keepalives 'value',
+ keepalives_idle 'value',
+ keepalives_interval 'value',
+ -- requiressl 'value',
+ sslcompression 'value',
+ sslmode 'value',
+ sslcert 'value',
+ sslkey 'value',
+ sslrootcert 'value',
+ sslcrl 'value'
+ --requirepeer 'value',
+ -- krbsrvname 'value',
+ -- gsslib 'value',
+ --replication 'value'
+ );
+ ALTER SERVER loopback1 OPTIONS (user 'value'); -- ERROR
+ ALTER SERVER loopback2 OPTIONS (ADD fetch_count '2');
+ ALTER USER MAPPING FOR public SERVER loopback1
+ OPTIONS (DROP user, DROP password);
+ ALTER USER MAPPING FOR public SERVER loopback1
+ OPTIONS (host 'value'); -- ERROR
+ ALTER FOREIGN TABLE ft1 OPTIONS (nspname 'S 1', relname 'T 1');
+ ALTER FOREIGN TABLE ft2 OPTIONS (nspname 'S 1', relname 'T 1', fetch_count '100');
+ ALTER FOREIGN TABLE ft1 OPTIONS (invalid 'value'); -- ERROR
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count 'a'); -- ERROR
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count '0'); -- ERROR
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count '-1'); -- ERROR
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (invalid 'value'); -- ERROR
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (colname 'C 1');
+ ALTER FOREIGN TABLE ft2 ALTER COLUMN c1 OPTIONS (colname 'C 1');
+ \dew+
+ \des+
+ \deu+
+ \det+
+
+ -- ===================================================================
+ -- simple queries
+ -- ===================================================================
+ -- single table, with/without alias
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
+ SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+ SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+ -- with WHERE clause
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
+ SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
+ -- 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
+ SELECT * FROM ft1 t1 WHERE t1.c3 = (SELECT MAX(c3) FROM ft2 t2) ORDER BY c1;
+ -- used in CTE
+ WITH t1 AS (SELECT * FROM ft1 WHERE c1 <= 10) SELECT t2.c1, t2.c2, t2.c3, t2.c4 FROM t1, ft2 t2 WHERE t1.c1 = t2.c1 ORDER BY t1.c1;
+ -- fixed values
+ SELECT 'fixed', NULL FROM ft1 t1 WHERE c1 = 1;
+ -- user-defined operator/function
+ CREATE FUNCTION pgsql_fdw_abs(int) RETURNS int AS $$
+ BEGIN
+ RETURN abs($1);
+ END
+ $$ LANGUAGE plpgsql IMMUTABLE;
+ CREATE OPERATOR === (
+ LEFTARG = int,
+ RIGHTARG = int,
+ PROCEDURE = int4eq,
+ COMMUTATOR = ===,
+ NEGATOR = !==
+ );
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = pgsql_fdw_abs(t1.c2);
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 === t1.c2;
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = abs(t1.c2);
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = t1.c2;
+
+ -- ===================================================================
+ -- parameterized queries
+ -- ===================================================================
+ -- simple join
+ PREPARE st1(int, int) AS SELECT t1.c3, t2.c3 FROM ft1 t1, ft2 t2 WHERE t1.c1 = $1 AND t2.c1 = $2;
+ EXPLAIN (COSTS false) EXECUTE st1(1, 2);
+ EXECUTE st1(1, 1);
+ EXECUTE st1(101, 101);
+ -- subquery using stable function (can't be pushed down)
+ PREPARE st2(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c4) = 6) ORDER BY c1;
+ EXPLAIN (COSTS false) EXECUTE st2(10, 20);
+ EXECUTE st2(10, 20);
+ EXECUTE st1(101, 101);
+ -- subquery using immutable function (can be pushed down)
+ PREPARE st3(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c5) = 6) ORDER BY c1;
+ EXPLAIN (COSTS false) EXECUTE st3(10, 20);
+ EXECUTE st3(10, 20);
+ EXECUTE st3(20, 30);
+ -- custom plan should be chosen
+ PREPARE st4(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 = $1;
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ -- cleanup
+ DEALLOCATE st1;
+ DEALLOCATE st2;
+ DEALLOCATE st3;
+ DEALLOCATE st4;
+
+ -- ===================================================================
+ -- connection management
+ -- ===================================================================
+ SELECT srvname, usename FROM pgsql_fdw_connections;
+ SELECT pgsql_fdw_disconnect(srvid, usesysid) FROM pgsql_fdw_get_connections();
+ SELECT srvname, usename FROM pgsql_fdw_connections;
+
+ -- ===================================================================
+ -- cleanup
+ -- ===================================================================
+ DROP OPERATOR === (int, int) CASCADE;
+ DROP OPERATOR !== (int, int) CASCADE;
+ DROP FUNCTION pgsql_fdw_abs(int);
+ DROP SCHEMA "S 1" CASCADE;
+ DROP EXTENSION pgsql_fdw CASCADE;
+ \c
+ DROP ROLE pgsql_fdw_user;
diff --git a/doc/src/sgml/contrib.sgml b/doc/src/sgml/contrib.sgml
index d4da4ee..32056d1 100644
*** a/doc/src/sgml/contrib.sgml
--- b/doc/src/sgml/contrib.sgml
*************** CREATE EXTENSION <replaceable>module_nam
*** 117,122 ****
--- 117,123 ----
&pgcrypto;
&pgfreespacemap;
&pgrowlocks;
+ &pgsql-fdw;
&pgstandby;
&pgstatstatements;
&pgstattuple;
diff --git a/doc/src/sgml/filelist.sgml b/doc/src/sgml/filelist.sgml
index b5d3c6d..de686ee 100644
*** a/doc/src/sgml/filelist.sgml
--- b/doc/src/sgml/filelist.sgml
***************
*** 125,130 ****
--- 125,131 ----
<!ENTITY pgcrypto SYSTEM "pgcrypto.sgml">
<!ENTITY pgfreespacemap SYSTEM "pgfreespacemap.sgml">
<!ENTITY pgrowlocks SYSTEM "pgrowlocks.sgml">
+ <!ENTITY pgsql-fdw SYSTEM "pgsql-fdw.sgml">
<!ENTITY pgstandby SYSTEM "pgstandby.sgml">
<!ENTITY pgstatstatements SYSTEM "pgstatstatements.sgml">
<!ENTITY pgstattuple SYSTEM "pgstattuple.sgml">
diff --git a/doc/src/sgml/pgsql-fdw.sgml b/doc/src/sgml/pgsql-fdw.sgml
index ...678e0c6 .
*** a/doc/src/sgml/pgsql-fdw.sgml
--- b/doc/src/sgml/pgsql-fdw.sgml
***************
*** 0 ****
--- 1,253 ----
+ <!-- doc/src/sgml/pgsql-fdw.sgml -->
+
+ <sect1 id="pgsql-fdw" xreflabel="pgsql_fdw">
+ <title>pgsql_fdw</title>
+
+ <indexterm zone="pgsql-fdw">
+ <primary>pgsql_fdw</primary>
+ </indexterm>
+
+ <para>
+ The <filename>pgsql_fdw</filename> module provides a foreign-data wrapper for
+ external <productname>PostgreSQL</productname> servers.
+ With this module, users can access data stored in external
+ <productname>PostgreSQL</productname> via plain SQL statements.
+ </para>
+
+ <para>
+ Note that default wrapper <literal>pgsql_fdw</literal> is created
+ automatically during <command>CREATE EXTENSION</command> command for
+ <application>pgsql_fdw</application>.
+ </para>
+
+ <sect2>
+ <title>FDW Options of pgsql_fdw</title>
+
+ <sect3>
+ <title>Connection Options</title>
+ <para>
+ A foreign server and user mapping created using this wrapper can have
+ <application>libpq</> connection options, expect below:
+
+ <itemizedlist>
+ <listitem><para>client_encoding</para></listitem>
+ <listitem><para>fallback_application_name</para></listitem>
+ <listitem><para>replication</para></listitem>
+ </itemizedlist>
+
+ For details of <application>libpq</> connection options, see
+ <xref linkend="libpq-connect">.
+ </para>
+
+ <para>
+ <literal>user</literal> and <literal>password</literal> can be
+ specified on user mappings, and others can be specified on foreign servers.
+ </para>
+ </sect3>
+
+ <sect3>
+ <title>Object Name Options</title>
+ <para>
+ Foreign tables which were created using this wrapper, or its columns can
+ have object name options. These options can be used to specify the names
+ used in SQL statement sent to remote <productname>PostgreSQL</productname>
+ server. These options are useful when a remote object has different name
+ from corresponding local one.
+ </para>
+
+ <variablelist>
+
+ <varlistentry>
+ <term><literal>nspname</literal></term>
+ <listitem>
+ <para>
+ This option, which can be specified on a foreign table, is used as a
+ namespace (schema) reference in the SQL statement. If this options is
+ omitted, <literal>pg_class.nspname</literal> of the foreign table is
+ used.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>relname</literal></term>
+ <listitem>
+ <para>
+ This option, which can be specified on a foreign table, is used as a
+ relation (table) reference in the SQL statement. If this options is
+ omitted, <literal>pg_class.relname</literal> of the foreign table is
+ used.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>colname</literal></term>
+ <listitem>
+ <para>
+ This option, which can be specified on a column of a foreign table, is
+ used as a column (attribute) reference in the SQL statement. If this
+ option is omitted, <literal>pg_attribute.attname</literal> of the column
+ of the foreign table is used.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ </variablelist>
+
+ </sect3>
+
+ <sect3>
+ <title>Cursor Options</title>
+ <para>
+ The <application>pgsql_fdw</application> always uses cursor to retrieve the
+ result from external server. Users can control the behavior of cursor by
+ setting cursor options to foreign table or foreign server. If an option
+ is set to both objects, finer-grained setting is used. In other words,
+ foreign table's setting overrides foreign server's setting.
+ </para>
+
+ <variablelist>
+
+ <varlistentry>
+ <term><literal>fetch_count</literal></term>
+ <listitem>
+ <para>
+ This option specifies the number of rows to be fetched at a time.
+ This option accepts only integer value larger than zero. The default
+ setting is 10000.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ </variablelist>
+
+ </sect3>
+
+ </sect2>
+
+ <sect2>
+ <title>Connection Management</title>
+
+ <para>
+ The <application>pgsql_fdw</application> establishes a connection to a
+ foreign server in the beginning of the first query which uses a foreign
+ table associated to the foreign server, and reuses the connection following
+ queries and even in following foreign scans in same query.
+
+ You can see the list of active connections via
+ <structname>pgsql_fdw_connections</structname> view. It shows pair of oid
+ and name of server and local role for each active connections established by
+ <application>pgsql_fdw</application>. For security reason, only superuser
+ can see other role's connections.
+ </para>
+
+ <para>
+ Established connections are kept alive until local role changes or the
+ current transaction aborts or user requests so.
+ </para>
+
+ <para>
+ If role has been changed, active connections established as old local role
+ is kept alive but never be reused until local role has restored to original
+ role. This kind of situation happens with <command>SET ROLE</command> and
+ <command>SET SESSION AUTHORIZATION</command>.
+ </para>
+
+ <para>
+ If current transaction aborts by error or user request, all active
+ connections are disconnected automatically. This behavior avoids possible
+ connection leaks on error.
+ </para>
+
+ <para>
+ You can discard persistent connection at arbitrary timing with
+ <function>pgsql_fdw_disconnect()</function>. It takes server oid and
+ user oid as arguments. This function can handle only connections
+ established in current session; connections established by other backends
+ are not reachable.
+ </para>
+
+ <para>
+ You can discard all active and visible connections in current session with
+ using <structname>pgsql_fdw_connections</structname> and
+ <function>pgsql_fdw_disconnect()</function> together:
+ <synopsis>
+ postgres=# SELECT pgsql_fdw_disconnect(srvid, usesysid) FROM pgsql_fdw_connections;
+ pgsql_fdw_disconnect
+ ----------------------
+ OK
+ OK
+ (2 rows)
+ </synopsis>
+ </para>
+ </sect2>
+
+ <sect2>
+ <title>Transaction Management</title>
+ <para>
+ The <application>pgsql_fdw</application> begins remote transaction at the
+ beginning of a local query, and terminates it with <command>ABORT</command>
+ at the end of the local query. This means that all foreign scans on a
+ foreign server in a local query are executed in one transaction.
+ The transaction isolation level used for remote transaction is same as
+ local transaction.
+ </para>
+ <para>
+ Note that even if the isolation level of local transaction was
+ <literal>SERIALIZABLE</literal> or <literal>REPEATABLE READ</literal>,
+ series of one query might produce different result, because foreign scans
+ in different local queries are executed in different remote transactions.
+ For instance, when client started a local transaction
+ explicitly with isolation level <literal>SERIALIZABLE</literal>, and
+ executed same local query which contains a foreign table which references
+ foreign data which is updated frequently, latter result would be different
+ from former result.
+ </para>
+ <para>
+ This restriction might be relaxed in future release.
+ </para>
+ </sect2>
+
+ <sect2>
+ <title>Estimation of Costs and Rows</title>
+ <para>
+ The <application>pgsql_fdw</application> estimates the costs of a foreign
+ scan by adding up some basic costs: connection costs, remote query costs
+ and data transfer costs.
+ To get remote query costs, <application>pgsql_fdw</application> executes
+ <command>EXPLAIN</command> command on remote server for each foreign scan.
+ </para>
+ <para>
+ On the other hand, estimated rows which was returned by
+ <command>EXPLAIN</command> is used for local estimation as-is.
+ </para>
+ </sect2>
+
+ <sect2>
+ <title>EXPLAIN Output</title>
+ <para>
+ For a foreign table using <literal>pgsql_fdw</>, <command>EXPLAIN</> shows
+ a remote SQL statement which is sent to remote
+ <productname>PostgreSQL</productname> server for a ForeignScan plan node.
+ For example:
+ </para>
+ <synopsis>
+ postgres=# EXPLAIN SELECT aid FROM pgbench_accounts WHERE abalance < 0;
+ QUERY PLAN
+ --------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on pgbench_accounts (cost=100.00..8105.13 rows=302613 width=8)
+ Filter: (abalance < 0)
+ Remote SQL: DECLARE pgsql_fdw_cursor_3 SCROLL CURSOR FOR SELECT aid, NULL, abalance, NULL FROM public.pgbench_accounts
+ (3 rows)
+ </synopsis>
+ </sect2>
+
+ <sect2>
+ <title>Author</title>
+ <para>
+ Shigeru Hanada <email>shigeru.hanada@gmail.com</email>
+ </para>
+ </sect2>
+
+ </sect1>
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 9ad54c5..c2a5a7b 100644
*** a/src/backend/utils/adt/ruleutils.c
--- b/src/backend/utils/adt/ruleutils.c
*************** deparse_context_for(const char *aliasnam
*** 2161,2166 ****
--- 2161,2189 ----
return list_make1(dpns);
}
+ /* ----------
+ * deparse_context_for_rtelist - Build deparse context for a list of RTEs
+ *
+ * Given the list of RangeTableEnetry, build deparsing context for an
+ * expression referencing those relations. This is sufficient for uses of
+ * deparse_expression before plan has been created.
+ * ----------
+ */
+ List *
+ deparse_context_for_rtelist(List *rtable)
+ {
+ deparse_namespace *dpns;
+
+ dpns = (deparse_namespace *) palloc0(sizeof(deparse_namespace));
+
+ /* Build a minimal RTE for the rel */
+ dpns->rtable = rtable;
+ dpns->ctes = NIL;
+
+ /* Return a one-deep namespace stack */
+ return list_make1(dpns);
+ }
+
/*
* deparse_context_for_planstate - Build deparse context for a plan
*
diff --git a/src/include/utils/builtins.h b/src/include/utils/builtins.h
index 2c331ce..dc6932a 100644
*** a/src/include/utils/builtins.h
--- b/src/include/utils/builtins.h
*************** extern char *deparse_expression(Node *ex
*** 648,653 ****
--- 648,654 ----
extern List *deparse_context_for(const char *aliasname, Oid relid);
extern List *deparse_context_for_planstate(Node *planstate, List *ancestors,
List *rtable);
+ extern List *deparse_context_for_rtelist(List *rtable);
extern const char *quote_identifier(const char *ident);
extern char *quote_qualified_identifier(const char *qualifier,
const char *ident);
Shigeru Hanada wrote:
- Since a rescan is done by rewinding the cursor, is it necessary
to have any other remote isolation level than READ COMMITED?
There is only one query issued per transaction.If multiple foreign tables on a foreign server is used in a local
query,
multiple queries are executed in a remote transaction. So IMO
isolation
levels are useful even if remote query is executed only once.
Oh, I see. You are right.
Yours,
Laurenz Albe
2012年2月17日6:08 Shigeru Hanada <shigeru.hanada@gmail.com>:
(2012/02/17 2:02), Kohei KaiGai wrote:
I found a strange behavior with v10. Is it available to reproduce?
<snip>
I tried to raise an error on remote side.
postgres=# select * FROM ftbl WHERE 100 / (a - 3)> 0;
The connection to the server was lost. Attempting reset: Failed.
The connection to the server was lost. Attempting reset: Failed.
!> \qI could reproduce the error by omitting CFLAGS=-O0 from configure
option. I usually this for coding environment so that gdb debugging
works correctly, so I haven't noticed this issue. I should test
optimized environment too...Expected result in that case is:
postgres=# select * from pgbench_accounts where 100 / (aid - 3) > 0;
ERROR: could not fetch rows from foreign server
DETAIL: ERROR: division by zeroHINT: FETCH 10000 FROM pgsql_fdw_cursor_0
postgres=#This is the PG_CATCH block at execute_query(). fetch_result() raises
an error, then it shall be catched to release PGresult.
Although "res" should be NULL at this point, PQclear was called with
a non-zero value according to the call trace.More strangely, I tried to inject elog(INFO, ...) to show the value of "res"
at this point. Then, it become unavailable to reproduce when I tried to
show the pointer of "res" with elog(INFO, "&res = %p",&res);Why the "res" has a non-zero value, even though it was cleared prior
to fetch_result() and an error was raised within this function?I've found the the problem is uninitialized PGresult variables.
Uninitialized PGresult pointer is used in some places, so its value is
garbage in PG_CATCH block when assignment code has been interrupted by
longjmp.Probably recommended style would be like this:
<pseudo_code>
PGresult *res = NULL; /* must be NULL in PG_CATCH */PG_TRY();
{
res = func_might_throw_exception();
if (PQstatus(res) != PGRES_xxx_OK)
{
/* error handling, pass message to caller */
ereport(ERROR, ...);
}/* success case, use result of query and release it */
...
PQclear(res);
}
PG_CATCH();
{
PQclear(res);
PG_RE_THROW();
/* caller should catch this exception. */
}
</pseudo_code>I misunderstood that PGresult pointer always has valid value after that
line, because I had wrote assignment of PGresult pointer before PG_TRY
block. Fixes for this issue are:(1) Initialize PGresult pointer with NULL, if it is used in PG_CATCH.
(2) Move PGresult assignment into PG_TRY block so that we can get
compiler warning of uninitialized variable, just in case.Please find attached a patch including fixes for this issue.
I marked this patch as "Ready for Committer", since I have nothing to
comment any more.
I'd like committer help to review this patch and it get merged.
Thanks,
--
KaiGai Kohei <kaigai@kaigai.gr.jp>
I wrote:
Shigeru Hanada wrote:
- Since a rescan is done by rewinding the cursor, is it necessary
to have any other remote isolation level than READ COMMITED?
There is only one query issued per transaction.If multiple foreign tables on a foreign server is used in a local
query,
multiple queries are executed in a remote transaction. So IMO
isolation
levels are useful even if remote query is executed only once.
Oh, I see. You are right.
I thought some more about this and changed my mind.
If your query involves foreign scans on two foreign tables on the same
foreign server, these should always see the same snapshot, because
that's how it works with two scans in one query on local tables.
So I think it should be REPEATABLE READ in all cases - SERIALIZABLE
is not necessary as long as all you do is read.
Yours,
Laurenz Albe
"Albe Laurenz" <laurenz.albe@wien.gv.at> wrote:
If your query involves foreign scans on two foreign tables on the
same foreign server, these should always see the same snapshot,
because that's how it works with two scans in one query on local
tables.
That makes sense.
So I think it should be REPEATABLE READ in all cases -
SERIALIZABLE is not necessary as long as all you do is read.
That depends on whether you only want to see states of the database
which are consistent with later states of the database and any
invariants enforced by triggers or other software. See this example
of how a read-only transaction can see a bogus state at REPEATABLE
READ or less strict transaction isolation:
http://wiki.postgresql.org/wiki/SSI#Read_Only_Transactions
Perhaps if the transaction using the pgsql_fdw is running at the
SERIALIZABLE transaction isolation level, it should run the queries
at the that level, otherwise at REPEATABLE READ.
-Kevin
Kevin Grittner wrote:
If your query involves foreign scans on two foreign tables on the
same foreign server, these should always see the same snapshot,
because that's how it works with two scans in one query on local
tables.That makes sense.
So I think it should be REPEATABLE READ in all cases -
SERIALIZABLE is not necessary as long as all you do is read.That depends on whether you only want to see states of the database
which are consistent with later states of the database and any
invariants enforced by triggers or other software. See this example
of how a read-only transaction can see a bogus state at REPEATABLE
READ or less strict transaction isolation:http://wiki.postgresql.org/wiki/SSI#Read_Only_Transactions
Perhaps if the transaction using the pgsql_fdw is running at the
SERIALIZABLE transaction isolation level, it should run the queries
at the that level, otherwise at REPEATABLE READ.
I read the example carefully, and it seems to me that it is necessary
for the read-only transaction (T3) to be SERIALIZABLE so that
T1 is aborted and the state that T3 saw remains valid.
If I understand right, I agree with your correction.
Yours,
Laurenz Albe
"Albe Laurenz" <laurenz.albe@wien.gv.at> wrote:
I read the example carefully, and it seems to me that it is
necessary for the read-only transaction (T3) to be SERIALIZABLE so
that T1 is aborted and the state that T3 saw remains valid.
Correct.
If I understand right, I agree with your correction.
:-)
-Kevin
2012/02/21 0:58 "Kevin Grittner" <Kevin.Grittner@wicourts.gov>:
"Albe Laurenz" <laurenz.albe@wien.gv.at> wrote:
I read the example carefully, and it seems to me that it is
necessary for the read-only transaction (T3)v to be SERIALIZABLE so
that T1 is aborted and the state that T3 saw remains valid.Correct.
Hm, agreed that isolation levels < REPEATABLE READ are not sufficient for
pgsql_fdw's usage. I'll examine the example and fix pgsql_fdw.
Thanks.
(2012/02/15 20:50), Etsuro Fujita wrote:
(2012/02/14 23:50), Tom Lane wrote:
(2012/02/14 17:40), Etsuro Fujita wrote:
As discussed at
that thread, it would have to change the PlanForeignScan API to let the
FDW generate multiple paths and dump them all to add_path instead of
returning a FdwPlan struct.
I would really like to see that happen in 9.2, because the longer we let
that mistake live, the harder it will be to change. More and more FDWs
are getting written. I don't think it's that hard to do: we just have
to agree that PlanForeignScan should return void and call add_path for
itself, possibly more than once.Agreed. I fixed the PlanForeignScan API. Please find attached a patch.
If we do that, I'm inclined to think
we cou;d get rid of the separate Node type FdwPlan, and just incorporate
"List *fdw_private" into ForeignPath and ForeignScan.+1 While the patch retains the struct FdwPlan, I would like to get rid
of it at next version of the patch.
Please find attached an updated version of the patch.
Best regards,
Etsuro Fujita
Attachments:
postgresql-fdw-v2.patchtext/plain; name=postgresql-fdw-v2.patchDownload
*** a/contrib/file_fdw/file_fdw.c
--- b/contrib/file_fdw/file_fdw.c
***************
*** 25,30 ****
--- 25,31 ----
#include "miscadmin.h"
#include "nodes/makefuncs.h"
#include "optimizer/cost.h"
+ #include "optimizer/pathnode.h"
#include "utils/rel.h"
#include "utils/syscache.h"
***************
*** 93,99 **** PG_FUNCTION_INFO_V1(file_fdw_validator);
/*
* FDW callback routines
*/
! static FdwPlan *filePlanForeignScan(Oid foreigntableid,
PlannerInfo *root,
RelOptInfo *baserel);
static void fileExplainForeignScan(ForeignScanState *node, ExplainState *es);
--- 94,100 ----
/*
* FDW callback routines
*/
! static void filePlanForeignScan(Oid foreigntableid,
PlannerInfo *root,
RelOptInfo *baserel);
static void fileExplainForeignScan(ForeignScanState *node, ExplainState *es);
***************
*** 406,432 **** get_file_fdw_attribute_options(Oid relid)
/*
* filePlanForeignScan
! * Create a FdwPlan for a scan on the foreign table
*/
! static FdwPlan *
filePlanForeignScan(Oid foreigntableid,
PlannerInfo *root,
RelOptInfo *baserel)
{
! FdwPlan *fdwplan;
char *filename;
List *options;
/* Fetch options --- we only need filename at this point */
fileGetOptions(foreigntableid, &filename, &options);
! /* Construct FdwPlan with cost estimates */
! fdwplan = makeNode(FdwPlan);
estimate_costs(root, baserel, filename,
! &fdwplan->startup_cost, &fdwplan->total_cost);
! fdwplan->fdw_private = NIL; /* not used */
! return fdwplan;
}
/*
--- 407,439 ----
/*
* filePlanForeignScan
! * Create the (single) path for a scan on the foreign table
*/
! static void
filePlanForeignScan(Oid foreigntableid,
PlannerInfo *root,
RelOptInfo *baserel)
{
! ForeignPath *pathnode = makeNode(ForeignPath);
char *filename;
List *options;
+ pathnode->path.pathtype = T_ForeignScan;
+ pathnode->path.parent = baserel;
+ pathnode->path.pathkeys = NIL; /* result is always unordered */
+ pathnode->path.required_outer = NULL;
+ pathnode->path.param_clauses = NIL;
+ pathnode->fdw_private = NIL; /* not used */
+
/* Fetch options --- we only need filename at this point */
fileGetOptions(foreigntableid, &filename, &options);
! /* Estimate costs of scanning the foreign table */
estimate_costs(root, baserel, filename,
! &pathnode->path.startup_cost, &pathnode->path.total_cost);
! pathnode->path.rows = baserel->rows;
! add_path(baserel, (Path *) pathnode);
}
/*
*** a/doc/src/sgml/fdwhandler.sgml
--- b/doc/src/sgml/fdwhandler.sgml
***************
*** 88,108 ****
<para>
<programlisting>
! FdwPlan *
PlanForeignScan (Oid foreigntableid,
PlannerInfo *root,
RelOptInfo *baserel);
</programlisting>
! Plan a scan on a foreign table. This is called when a query is planned.
<literal>foreigntableid</> is the <structname>pg_class</> OID of the
foreign table. <literal>root</> is the planner's global information
about the query, and <literal>baserel</> is the planner's information
about this table.
! The function must return a palloc'd struct that contains cost estimates
! plus any FDW-private information that is needed to execute the foreign
! scan at a later time. (Note that the private information must be
! represented in a form that <function>copyObject</> knows how to copy.)
</para>
<para>
--- 88,114 ----
<para>
<programlisting>
! void
PlanForeignScan (Oid foreigntableid,
PlannerInfo *root,
RelOptInfo *baserel);
</programlisting>
! Create the access paths for a scan on a foreign table. This is called
! when a query is planned.
<literal>foreigntableid</> is the <structname>pg_class</> OID of the
foreign table. <literal>root</> is the planner's global information
about the query, and <literal>baserel</> is the planner's information
about this table.
! The function must generate at least one access path for a scan on the
! foreign table and call <function>add_path</> to add it to
! <literal>baserel->pathlist</>. The function may generate multiple
! access paths for a scan on the foreign table and add them to
! <literal>baserel->pathlist</> using <function>add_path</>. Each
! access path generated should contain cost estimates plus any FDW-private
! information that is needed to execute the foreign scan at a later time.
! (Note that the private information must be represented in a form that
! <function>copyObject</> knows how to copy.)
</para>
<para>
***************
*** 159,167 **** BeginForeignScan (ForeignScanState *node,
its <structfield>fdw_state</> field is still NULL. Information about
the table to scan is accessible through the
<structname>ForeignScanState</> node (in particular, from the underlying
! <structname>ForeignScan</> plan node, which contains a pointer to the
! <structname>FdwPlan</> structure returned by
! <function>PlanForeignScan</>).
</para>
<para>
--- 165,172 ----
its <structfield>fdw_state</> field is still NULL. Information about
the table to scan is accessible through the
<structname>ForeignScanState</> node (in particular, from the underlying
! <structname>ForeignScan</> plan node, which contains FDW-private
! information about it provided by <function>PlanForeignScan</>).
</para>
<para>
***************
*** 228,236 **** EndForeignScan (ForeignScanState *node);
</para>
<para>
! The <structname>FdwRoutine</> and <structname>FdwPlan</> struct types
! are declared in <filename>src/include/foreign/fdwapi.h</>, which see
! for additional details.
</para>
</sect1>
--- 233,241 ----
</para>
<para>
! The <structname>FdwRoutine</> struct type is declared
! in <filename>src/include/foreign/fdwapi.h</>, which see for additional
! details.
</para>
</sect1>
*** a/src/backend/nodes/copyfuncs.c
--- b/src/backend/nodes/copyfuncs.c
***************
*** 591,611 **** _copyForeignScan(const ForeignScan *from)
* copy remainder of node
*/
COPY_SCALAR_FIELD(fsSystemCol);
- COPY_NODE_FIELD(fdwplan);
-
- return newnode;
- }
-
- /*
- * _copyFdwPlan
- */
- static FdwPlan *
- _copyFdwPlan(const FdwPlan *from)
- {
- FdwPlan *newnode = makeNode(FdwPlan);
-
- COPY_SCALAR_FIELD(startup_cost);
- COPY_SCALAR_FIELD(total_cost);
COPY_NODE_FIELD(fdw_private);
return newnode;
--- 591,596 ----
***************
*** 3841,3849 **** copyObject(const void *from)
case T_ForeignScan:
retval = _copyForeignScan(from);
break;
- case T_FdwPlan:
- retval = _copyFdwPlan(from);
- break;
case T_Join:
retval = _copyJoin(from);
break;
--- 3826,3831 ----
*** a/src/backend/nodes/outfuncs.c
--- b/src/backend/nodes/outfuncs.c
***************
*** 558,573 **** _outForeignScan(StringInfo str, const ForeignScan *node)
_outScanInfo(str, (const Scan *) node);
WRITE_BOOL_FIELD(fsSystemCol);
- WRITE_NODE_FIELD(fdwplan);
- }
-
- static void
- _outFdwPlan(StringInfo str, const FdwPlan *node)
- {
- WRITE_NODE_TYPE("FDWPLAN");
-
- WRITE_FLOAT_FIELD(startup_cost, "%.2f");
- WRITE_FLOAT_FIELD(total_cost, "%.2f");
WRITE_NODE_FIELD(fdw_private);
}
--- 558,563 ----
***************
*** 1572,1578 **** _outForeignPath(StringInfo str, const ForeignPath *node)
_outPathInfo(str, (const Path *) node);
! WRITE_NODE_FIELD(fdwplan);
}
static void
--- 1562,1568 ----
_outPathInfo(str, (const Path *) node);
! WRITE_NODE_FIELD(fdw_private);
}
static void
***************
*** 2744,2752 **** _outNode(StringInfo str, const void *obj)
case T_ForeignScan:
_outForeignScan(str, obj);
break;
- case T_FdwPlan:
- _outFdwPlan(str, obj);
- break;
case T_Join:
_outJoin(str, obj);
break;
--- 2734,2739 ----
*** a/src/backend/optimizer/path/allpaths.c
--- b/src/backend/optimizer/path/allpaths.c
***************
*** 399,411 **** set_foreign_size(PlannerInfo *root, RelOptInfo *rel, RangeTblEntry *rte)
/*
* set_foreign_pathlist
! * Build the (single) access path for a foreign table RTE
*/
static void
set_foreign_pathlist(PlannerInfo *root, RelOptInfo *rel, RangeTblEntry *rte)
{
! /* Generate appropriate path */
! add_path(rel, (Path *) create_foreignscan_path(root, rel));
/* Select cheapest path (pretty easy in this case...) */
set_cheapest(rel);
--- 399,411 ----
/*
* set_foreign_pathlist
! * Build one or more access paths for a foreign table RTE
*/
static void
set_foreign_pathlist(PlannerInfo *root, RelOptInfo *rel, RangeTblEntry *rte)
{
! /* Generate appropriate paths */
! create_foreignscan_path(root, rel);
/* Select cheapest path (pretty easy in this case...) */
set_cheapest(rel);
*** a/src/backend/optimizer/plan/createplan.c
--- b/src/backend/optimizer/plan/createplan.c
***************
*** 121,127 **** static CteScan *make_ctescan(List *qptlist, List *qpqual,
static WorkTableScan *make_worktablescan(List *qptlist, List *qpqual,
Index scanrelid, int wtParam);
static ForeignScan *make_foreignscan(List *qptlist, List *qpqual,
! Index scanrelid, bool fsSystemCol, FdwPlan *fdwplan);
static BitmapAnd *make_bitmap_and(List *bitmapplans);
static BitmapOr *make_bitmap_or(List *bitmapplans);
static NestLoop *make_nestloop(List *tlist,
--- 121,127 ----
static WorkTableScan *make_worktablescan(List *qptlist, List *qpqual,
Index scanrelid, int wtParam);
static ForeignScan *make_foreignscan(List *qptlist, List *qpqual,
! Index scanrelid, bool fsSystemCol, List *fdw_private);
static BitmapAnd *make_bitmap_and(List *bitmapplans);
static BitmapOr *make_bitmap_or(List *bitmapplans);
static NestLoop *make_nestloop(List *tlist,
***************
*** 1847,1853 **** create_foreignscan_plan(PlannerInfo *root, ForeignPath *best_path,
scan_clauses,
scan_relid,
fsSystemCol,
! best_path->fdwplan);
copy_path_costsize(&scan_plan->scan.plan, &best_path->path);
--- 1847,1853 ----
scan_clauses,
scan_relid,
fsSystemCol,
! best_path->fdw_private);
copy_path_costsize(&scan_plan->scan.plan, &best_path->path);
***************
*** 3189,3195 **** make_foreignscan(List *qptlist,
List *qpqual,
Index scanrelid,
bool fsSystemCol,
! FdwPlan *fdwplan)
{
ForeignScan *node = makeNode(ForeignScan);
Plan *plan = &node->scan.plan;
--- 3189,3195 ----
List *qpqual,
Index scanrelid,
bool fsSystemCol,
! List *fdw_private)
{
ForeignScan *node = makeNode(ForeignScan);
Plan *plan = &node->scan.plan;
***************
*** 3201,3207 **** make_foreignscan(List *qptlist,
plan->righttree = NULL;
node->scan.scanrelid = scanrelid;
node->fsSystemCol = fsSystemCol;
! node->fdwplan = fdwplan;
return node;
}
--- 3201,3207 ----
plan->righttree = NULL;
node->scan.scanrelid = scanrelid;
node->fsSystemCol = fsSystemCol;
! node->fdw_private = fdw_private;
return node;
}
*** a/src/backend/optimizer/util/pathnode.c
--- b/src/backend/optimizer/util/pathnode.c
***************
*** 1764,1803 **** create_worktablescan_path(PlannerInfo *root, RelOptInfo *rel)
/*
* create_foreignscan_path
! * Creates a path corresponding to a scan of a foreign table,
! * returning the pathnode.
*/
! ForeignPath *
create_foreignscan_path(PlannerInfo *root, RelOptInfo *rel)
{
- ForeignPath *pathnode = makeNode(ForeignPath);
RangeTblEntry *rte;
FdwRoutine *fdwroutine;
- FdwPlan *fdwplan;
-
- pathnode->path.pathtype = T_ForeignScan;
- pathnode->path.parent = rel;
- pathnode->path.pathkeys = NIL; /* result is always unordered */
- pathnode->path.required_outer = NULL;
- pathnode->path.param_clauses = NIL;
/* Get FDW's callback info */
rte = planner_rt_fetch(rel->relid, root);
fdwroutine = GetFdwRoutineByRelId(rte->relid);
/* Let the FDW do its planning */
! fdwplan = fdwroutine->PlanForeignScan(rte->relid, root, rel);
! if (fdwplan == NULL || !IsA(fdwplan, FdwPlan))
! elog(ERROR, "foreign-data wrapper PlanForeignScan function for relation %u did not return an FdwPlan struct",
! rte->relid);
! pathnode->fdwplan = fdwplan;
!
! /* use costs estimated by FDW */
! pathnode->path.rows = rel->rows;
! pathnode->path.startup_cost = fdwplan->startup_cost;
! pathnode->path.total_cost = fdwplan->total_cost;
!
! return pathnode;
}
/*
--- 1764,1786 ----
/*
* create_foreignscan_path
! * Creates one or more paths corresponding to a scan of a foreign table,
! * returning void.
*/
! void
create_foreignscan_path(PlannerInfo *root, RelOptInfo *rel)
{
RangeTblEntry *rte;
FdwRoutine *fdwroutine;
/* Get FDW's callback info */
rte = planner_rt_fetch(rel->relid, root);
fdwroutine = GetFdwRoutineByRelId(rte->relid);
/* Let the FDW do its planning */
! fdwroutine->PlanForeignScan(rte->relid, root, rel);
! if (rel->pathlist == NIL)
! elog(ERROR, "could not devise a query plan for the given query");
}
/*
*** a/src/include/foreign/fdwapi.h
--- b/src/include/foreign/fdwapi.h
***************
*** 20,58 **** struct ExplainState;
/*
- * FdwPlan is the information returned to the planner by PlanForeignScan.
- */
- typedef struct FdwPlan
- {
- NodeTag type;
-
- /*
- * Cost estimation info. The startup_cost is time before retrieving the
- * first row, so it should include costs of connecting to the remote host,
- * sending over the query, etc. Note that PlanForeignScan also ought to
- * set baserel->rows and baserel->width if it can produce any usable
- * estimates of those values.
- */
- Cost startup_cost; /* cost expended before fetching any tuples */
- Cost total_cost; /* total cost (assuming all tuples fetched) */
-
- /*
- * FDW private data, which will be available at execution time.
- *
- * Note that everything in this list must be copiable by copyObject(). One
- * way to store an arbitrary blob of bytes is to represent it as a bytea
- * Const. Usually, though, you'll be better off choosing a representation
- * that can be dumped usefully by nodeToString().
- */
- List *fdw_private;
- } FdwPlan;
-
-
- /*
* Callback function signatures --- see fdwhandler.sgml for more info.
*/
! typedef FdwPlan *(*PlanForeignScan_function) (Oid foreigntableid,
PlannerInfo *root,
RelOptInfo *baserel);
--- 20,29 ----
/*
* Callback function signatures --- see fdwhandler.sgml for more info.
*/
! typedef void (*PlanForeignScan_function) (Oid foreigntableid,
PlannerInfo *root,
RelOptInfo *baserel);
*** a/src/include/nodes/nodes.h
--- b/src/include/nodes/nodes.h
***************
*** 62,68 **** typedef enum NodeTag
T_CteScan,
T_WorkTableScan,
T_ForeignScan,
- T_FdwPlan,
T_Join,
T_NestLoop,
T_MergeJoin,
--- 62,67 ----
*** a/src/include/nodes/plannodes.h
--- b/src/include/nodes/plannodes.h
***************
*** 468,475 **** typedef struct ForeignScan
{
Scan scan;
bool fsSystemCol; /* true if any "system column" is needed */
! /* use struct pointer to avoid including fdwapi.h here */
! struct FdwPlan *fdwplan;
} ForeignScan;
--- 468,474 ----
{
Scan scan;
bool fsSystemCol; /* true if any "system column" is needed */
! List *fdw_private;
} ForeignScan;
*** a/src/include/nodes/relation.h
--- b/src/include/nodes/relation.h
***************
*** 794,805 **** typedef struct TidPath
/*
* ForeignPath represents a scan of a foreign table
*/
typedef struct ForeignPath
{
Path path;
! /* use struct pointer to avoid including fdwapi.h here */
! struct FdwPlan *fdwplan;
} ForeignPath;
/*
--- 794,810 ----
/*
* ForeignPath represents a scan of a foreign table
+ *
+ * fdw_private is a list of FDW private data, which will be available at
+ * execution time. Note that everything in this list must be copiable
+ * by copyObject(). One way to store an arbitrary blob of bytes is to
+ * represent it as a bytea Const. Usually, though, you'll be better off
+ * choosing a representation that can be dumped usefully by nodeToString().
*/
typedef struct ForeignPath
{
Path path;
! List *fdw_private;
} ForeignPath;
/*
*** a/src/include/optimizer/pathnode.h
--- b/src/include/optimizer/pathnode.h
***************
*** 68,74 **** extern Path *create_functionscan_path(PlannerInfo *root, RelOptInfo *rel);
extern Path *create_valuesscan_path(PlannerInfo *root, RelOptInfo *rel);
extern Path *create_ctescan_path(PlannerInfo *root, RelOptInfo *rel);
extern Path *create_worktablescan_path(PlannerInfo *root, RelOptInfo *rel);
! extern ForeignPath *create_foreignscan_path(PlannerInfo *root, RelOptInfo *rel);
extern Relids calc_nestloop_required_outer(Path *outer_path, Path *inner_path);
extern Relids calc_non_nestloop_required_outer(Path *outer_path, Path *inner_path);
--- 68,74 ----
extern Path *create_valuesscan_path(PlannerInfo *root, RelOptInfo *rel);
extern Path *create_ctescan_path(PlannerInfo *root, RelOptInfo *rel);
extern Path *create_worktablescan_path(PlannerInfo *root, RelOptInfo *rel);
! extern void create_foreignscan_path(PlannerInfo *root, RelOptInfo *rel);
extern Relids calc_nestloop_required_outer(Path *outer_path, Path *inner_path);
extern Relids calc_non_nestloop_required_outer(Path *outer_path, Path *inner_path);
(2012/02/21 8:07), Shigeru Hanada wrote:
Hm, agreed that isolation levels< REPEATABLE READ are not sufficient for
pgsql_fdw's usage. I'll examine the example and fix pgsql_fdw.
Attached patch uses "safe" isolation level for remote transactions.
After this change, pgsql_fdw uses levels below:
local | remote
------------------+-----------------
SERIALIZABLE | SERIALIZABLE
REPEATABLE READ | REPEATABLE READ
READ COMMITTED | REPEATABLE READ
READ UNCOMMITTED | REPEATABLE READ
Please review document of pgsql_fdw too.
In addition, I've removed #ifdef from options.c, so that we can specify
libpq options about SSL and Kerberos even if such feature has not been
configured on the environment where pgsql_fdw was built. This change
also avoids regression test failure on environment where any of
--with-openssl or --with-krb5 was specified for configure script.
Regards,
--
Shigeru Hanada
Attachments:
pgsql_fdw_v12.patchtext/plain; name=pgsql_fdw_v12.patchDownload
diff --git a/contrib/Makefile b/contrib/Makefile
index ac0a80a..14aa433 100644
*** a/contrib/Makefile
--- b/contrib/Makefile
*************** SUBDIRS = \
*** 41,46 ****
--- 41,47 ----
pgbench \
pgcrypto \
pgrowlocks \
+ pgsql_fdw \
pgstattuple \
seg \
spi \
diff --git a/contrib/README b/contrib/README
index a1d42a1..d3fa211 100644
*** a/contrib/README
--- b/contrib/README
*************** pgrowlocks -
*** 158,163 ****
--- 158,167 ----
A function to return row locking information
by Tatsuo Ishii <ishii@sraoss.co.jp>
+ pgsql_fdw -
+ Foreign-data wrapper for external PostgreSQL servers.
+ by Shigeru Hanada <shigeru.hanada@gmail.com>
+
pgstattuple -
Functions to return statistics about "dead" tuples and free
space within a table
diff --git a/contrib/pgsql_fdw/.gitignore b/contrib/pgsql_fdw/.gitignore
index ...0854728 .
*** a/contrib/pgsql_fdw/.gitignore
--- b/contrib/pgsql_fdw/.gitignore
***************
*** 0 ****
--- 1,4 ----
+ # Generated subdirectories
+ /results/
+ *.o
+ *.so
diff --git a/contrib/pgsql_fdw/Makefile b/contrib/pgsql_fdw/Makefile
index ...6381365 .
*** a/contrib/pgsql_fdw/Makefile
--- b/contrib/pgsql_fdw/Makefile
***************
*** 0 ****
--- 1,22 ----
+ # contrib/pgsql_fdw/Makefile
+
+ MODULE_big = pgsql_fdw
+ OBJS = pgsql_fdw.o option.o deparse.o connection.o
+ PG_CPPFLAGS = -I$(libpq_srcdir)
+ SHLIB_LINK = $(libpq)
+
+ EXTENSION = pgsql_fdw
+ DATA = pgsql_fdw--1.0.sql
+
+ REGRESS = pgsql_fdw
+
+ ifdef USE_PGXS
+ PG_CONFIG = pg_config
+ PGXS := $(shell $(PG_CONFIG) --pgxs)
+ include $(PGXS)
+ else
+ subdir = contrib/pgsql_fdw
+ top_builddir = ../..
+ include $(top_builddir)/src/Makefile.global
+ include $(top_srcdir)/contrib/contrib-global.mk
+ endif
diff --git a/contrib/pgsql_fdw/connection.c b/contrib/pgsql_fdw/connection.c
index ...4f3e5a6 .
*** a/contrib/pgsql_fdw/connection.c
--- b/contrib/pgsql_fdw/connection.c
***************
*** 0 ****
--- 1,549 ----
+ /*-------------------------------------------------------------------------
+ *
+ * connection.c
+ * Connection management for pgsql_fdw
+ *
+ * Portions Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/connection.c
+ *
+ *-------------------------------------------------------------------------
+ */
+ #include "postgres.h"
+
+ #include "access/xact.h"
+ #include "catalog/pg_type.h"
+ #include "foreign/foreign.h"
+ #include "funcapi.h"
+ #include "libpq-fe.h"
+ #include "mb/pg_wchar.h"
+ #include "miscadmin.h"
+ #include "utils/array.h"
+ #include "utils/builtins.h"
+ #include "utils/hsearch.h"
+ #include "utils/memutils.h"
+ #include "utils/resowner.h"
+ #include "utils/tuplestore.h"
+
+ #include "pgsql_fdw.h"
+ #include "connection.h"
+
+ /* ============================================================================
+ * Connection management functions
+ * ==========================================================================*/
+
+ /*
+ * Connection cache entry managed with hash table.
+ */
+ typedef struct ConnCacheEntry
+ {
+ /* hash key must be first */
+ Oid serverid; /* oid of foreign server */
+ Oid userid; /* oid of local user */
+
+ bool use_tx; /* true when using remote transaction */
+ 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);
+ static void begin_remote_tx(PGconn *conn);
+ static void abort_remote_tx(PGconn *conn);
+
+ /*
+ * 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.
+ *
+ * When use_tx is true, remote transaction is started if caller is the only
+ * user of the connection. Isolation level of the remote transaction is same
+ * as local transaction, and remote transaction will be aborted when last
+ * user release.
+ *
+ * TODO: Note that caching connections requires a mechanism to detect change of
+ * FDW object to invalidate already established connections.
+ */
+ PGconn *
+ GetConnection(ForeignServer *server, UserMapping *user, bool use_tx)
+ {
+ bool found;
+ ConnCacheEntry *entry;
+ ConnCacheEntry key;
+
+ /* initialize connection cache if it isn't */
+ if (FSConnectionHash == NULL)
+ {
+ HASHCTL ctl;
+
+ /* hash key is a pair of oids: serverid and userid */
+ MemSet(&ctl, 0, sizeof(ctl));
+ ctl.keysize = sizeof(Oid) + sizeof(Oid);
+ ctl.entrysize = sizeof(ConnCacheEntry);
+ ctl.hash = tag_hash;
+ ctl.match = memcmp;
+ ctl.keycopy = memcpy;
+ /* allocate FSConnectionHash in the cache context */
+ ctl.hcxt = CacheMemoryContext;
+ FSConnectionHash = hash_create("Foreign Connections", 32,
+ &ctl,
+ HASH_ELEM | HASH_CONTEXT |
+ HASH_FUNCTION | HASH_COMPARE |
+ HASH_KEYCOPY);
+ }
+
+ /* Create key value for the entry. */
+ MemSet(&key, 0, sizeof(key));
+ key.serverid = server->serverid;
+ key.userid = GetOuterUserId();
+
+ /*
+ * Find cached entry for requested connection. If we couldn't find,
+ * callback function of ResourceOwner should be registered to clean the
+ * connection up on error including user interrupt.
+ */
+ entry = hash_search(FSConnectionHash, &key, HASH_ENTER, &found);
+ if (!found)
+ {
+ entry->refs = 0;
+ entry->use_tx = false;
+ entry->conn = NULL;
+ RegisterResourceReleaseCallback(cleanup_connection, entry);
+ }
+
+ /*
+ * We don't check the health of cached connection here, because it would
+ * require some overhead. Broken connection and its cache entry will be
+ * cleaned up when the connection is actually used.
+ */
+
+ /*
+ * If cache entry doesn't have connection, we have to establish new
+ * connection.
+ */
+ if (entry->conn == NULL)
+ {
+ /*
+ * Use PG_TRY block to ensure closing connection on error.
+ */
+ PG_TRY();
+ {
+ /*
+ * Connect to the foreign PostgreSQL server, and store it in cache
+ * entry to keep new connection.
+ * Note: key items of entry has already been initialized in
+ * hash_search(HASH_ENTER).
+ */
+ entry->conn = connect_pg_server(server, user);
+ }
+ PG_CATCH();
+ {
+ /* Clear connection cache entry on error case. */
+ PQfinish(entry->conn);
+ entry->refs = 0;
+ entry->use_tx = false;
+ entry->conn = NULL;
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+ }
+
+ /* Increase connection reference counter. */
+ entry->refs++;
+
+ /*
+ * If requester is the only referrer of this connection, start transaction
+ * with the same isolation level as the local transaction we are in.
+ * We need to remember whether this connection uses remote transaction to
+ * abort it when this connection is released completely.
+ */
+ if (use_tx && entry->refs == 1)
+ begin_remote_tx(entry->conn);
+ entry->use_tx = use_tx;
+
+
+ return entry->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 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. Those two object hold only libpq options.
+ * Extra 3 items are for:
+ * *) fallback_application_name
+ * *) client_encoding
+ * *) NULL termination (end marker)
+ *
+ * Note: We don't omit any parameters even target database might be older
+ * than local, because unexpected parameters are just ignored.
+ */
+ n = list_length(server->options) + list_length(user->options) + 3;
+ 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 += ExtractConnectionOptions(server->options,
+ all_keywords + n, all_values + n);
+ n += ExtractConnectionOptions(user->options,
+ all_keywords + n, all_values + n);
+ all_keywords[n] = all_values[n] = NULL;
+
+ for (i = 0, j = 0; all_keywords[i]; i++)
+ {
+ keywords[j] = all_keywords[i];
+ values[j] = all_values[i];
+ j++;
+ }
+
+ /* Use "pgsql_fdw" as fallback_application_name. */
+ keywords[j] = "fallback_application_name";
+ values[j++] = "pgsql_fdw";
+
+ /* Set client_encoding so that libpq can convert encoding properly. */
+ keywords[j] = "client_encoding";
+ values[j++] = GetDatabaseEncodingName();
+
+ keywords[j] = values[j] = NULL;
+ pfree(all_keywords);
+ pfree(all_values);
+
+ /* 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.")));
+ }
+
+ return conn;
+ }
+
+ /*
+ * Start transaction to use cursor to retrieve data separately.
+ */
+ static void
+ begin_remote_tx(PGconn *conn)
+ {
+ const char *sql = NULL; /* keep compiler quiet. */
+ PGresult *res;
+
+ switch (XactIsoLevel)
+ {
+ case XACT_READ_UNCOMMITTED:
+ case XACT_READ_COMMITTED:
+ case XACT_REPEATABLE_READ:
+ sql = "START TRANSACTION ISOLATION LEVEL REPEATABLE READ";
+ break;
+ case XACT_SERIALIZABLE:
+ sql = "START TRANSACTION ISOLATION LEVEL SERIALIZABLE";
+ break;
+ default:
+ elog(ERROR, "unexpected isolation level: %d", XactIsoLevel);
+ break;
+ }
+
+ elog(DEBUG3, "starting remote transaction with \"%s\"", sql);
+
+ res = PQexec(conn, sql);
+ if (PQresultStatus(res) != PGRES_COMMAND_OK)
+ {
+ PQclear(res);
+ elog(ERROR, "could not start transaction: %s", PQerrorMessage(conn));
+ }
+ PQclear(res);
+ }
+
+ static void
+ abort_remote_tx(PGconn *conn)
+ {
+ PGresult *res;
+
+ elog(DEBUG3, "aborting remote transaction");
+
+ res = PQexec(conn, "ABORT TRANSACTION");
+ if (PQresultStatus(res) != PGRES_COMMAND_OK)
+ {
+ PQclear(res);
+ elog(ERROR, "could not abort transaction: %s", PQerrorMessage(conn));
+ }
+ PQclear(res);
+ }
+
+ /*
+ * 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 sequentially 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;
+ }
+ if (entry != NULL)
+ hash_seq_term(&scan);
+
+ /*
+ * If this connection uses remote transaction and caller is the last user,
+ * abort remote transaction and forget about it.
+ */
+ if (entry->use_tx && entry->refs == 1)
+ {
+ abort_remote_tx(conn);
+ entry->use_tx = false;
+ }
+
+ /*
+ * If the released connection was an orphan, just close it.
+ */
+ if (entry == NULL)
+ {
+ PQfinish(conn);
+ return;
+ }
+
+ /*
+ * Decrease reference counter of this connection. Even if the caller was
+ * the last referrer, we don't unregister it from cache.
+ */
+ entry->refs--;
+ }
+
+ /*
+ * 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)
+ {
+ PQfinish(entry->conn);
+ entry->refs = 0;
+ entry->conn = NULL;
+ }
+ }
+
+ /*
+ * Get list of connections currently active.
+ */
+ Datum pgsql_fdw_get_connections(PG_FUNCTION_ARGS);
+ PG_FUNCTION_INFO_V1(pgsql_fdw_get_connections);
+ Datum
+ pgsql_fdw_get_connections(PG_FUNCTION_ARGS)
+ {
+ ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+ HASH_SEQ_STATUS scan;
+ ConnCacheEntry *entry;
+ MemoryContext oldcontext = CurrentMemoryContext;
+ Tuplestorestate *tuplestore;
+ TupleDesc tupdesc;
+
+ /* We return list of connection with storing them in a Tuplestore. */
+ rsinfo->returnMode = SFRM_Materialize;
+ rsinfo->setResult = NULL;
+ rsinfo->setDesc = NULL;
+
+ /* Create tuplestore and copy of TupleDesc in per-query context. */
+ MemoryContextSwitchTo(rsinfo->econtext->ecxt_per_query_memory);
+
+ tupdesc = CreateTemplateTupleDesc(2, false);
+ TupleDescInitEntry(tupdesc, 1, "srvid", OIDOID, -1, 0);
+ TupleDescInitEntry(tupdesc, 2, "usesysid", OIDOID, -1, 0);
+ rsinfo->setDesc = tupdesc;
+
+ tuplestore = tuplestore_begin_heap(false, false, work_mem);
+ rsinfo->setResult = tuplestore;
+
+ MemoryContextSwitchTo(oldcontext);
+
+ /*
+ * We need to scan sequentially since we use the address to find
+ * appropriate PGconn from the hash table.
+ */
+ if (FSConnectionHash != NULL)
+ {
+ hash_seq_init(&scan, FSConnectionHash);
+ while ((entry = (ConnCacheEntry *) hash_seq_search(&scan)))
+ {
+ Datum values[2];
+ bool nulls[2];
+ HeapTuple tuple;
+
+ /* Ignore inactive connections */
+ if (PQstatus(entry->conn) != CONNECTION_OK)
+ continue;
+
+ /*
+ * Ignore other users' connections if current user isn't a
+ * superuser.
+ */
+ if (!superuser() && entry->userid != GetUserId())
+ continue;
+
+ values[0] = ObjectIdGetDatum(entry->serverid);
+ values[1] = ObjectIdGetDatum(entry->userid);
+ nulls[0] = false;
+ nulls[1] = false;
+
+ tuple = heap_formtuple(tupdesc, values, nulls);
+ tuplestore_puttuple(tuplestore, tuple);
+ }
+ }
+ tuplestore_donestoring(tuplestore);
+
+ PG_RETURN_VOID();
+ }
+
+ /*
+ * Discard persistent connection designated by given connection name.
+ */
+ Datum pgsql_fdw_disconnect(PG_FUNCTION_ARGS);
+ PG_FUNCTION_INFO_V1(pgsql_fdw_disconnect);
+ Datum
+ pgsql_fdw_disconnect(PG_FUNCTION_ARGS)
+ {
+ Oid serverid = PG_GETARG_OID(0);
+ Oid userid = PG_GETARG_OID(1);
+ ConnCacheEntry key;
+ ConnCacheEntry *entry = NULL;
+ bool found;
+
+ /* Non-superuser can't discard other users' connection. */
+ if (!superuser() && userid != GetOuterUserId())
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_RESOURCES),
+ errmsg("only superuser can discard other user's connection")));
+
+ /*
+ * If no connection has been established, or no such connections, just
+ * return "NG" to indicate nothing has done.
+ */
+ if (FSConnectionHash == NULL)
+ PG_RETURN_TEXT_P(cstring_to_text("NG"));
+
+ key.serverid = serverid;
+ key.userid = userid;
+ entry = hash_search(FSConnectionHash, &key, HASH_FIND, &found);
+ if (!found)
+ PG_RETURN_TEXT_P(cstring_to_text("NG"));
+
+ /* Discard cached connection, and clear reference counter. */
+ PQfinish(entry->conn);
+ entry->refs = 0;
+ entry->conn = NULL;
+
+ PG_RETURN_TEXT_P(cstring_to_text("OK"));
+ }
diff --git a/contrib/pgsql_fdw/connection.h b/contrib/pgsql_fdw/connection.h
index ...4427f56 .
*** a/contrib/pgsql_fdw/connection.h
--- b/contrib/pgsql_fdw/connection.h
***************
*** 0 ****
--- 1,25 ----
+ /*-------------------------------------------------------------------------
+ *
+ * connection.h
+ * Connection management for pgsql_fdw
+ *
+ * Portions Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_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, bool use_tx);
+ void ReleaseConnection(PGconn *conn);
+
+ #endif /* CONNECTION_H */
diff --git a/contrib/pgsql_fdw/deparse.c b/contrib/pgsql_fdw/deparse.c
index ...7ca2767 .
*** a/contrib/pgsql_fdw/deparse.c
--- b/contrib/pgsql_fdw/deparse.c
***************
*** 0 ****
--- 1,210 ----
+ /*-------------------------------------------------------------------------
+ *
+ * deparse.c
+ * query deparser for PostgreSQL
+ *
+ * Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/deparse.c
+ *
+ *-------------------------------------------------------------------------
+ */
+ #include "postgres.h"
+
+ #include "access/transam.h"
+ #include "foreign/foreign.h"
+ #include "lib/stringinfo.h"
+ #include "nodes/nodeFuncs.h"
+ #include "nodes/nodes.h"
+ #include "nodes/makefuncs.h"
+ #include "optimizer/clauses.h"
+ #include "optimizer/var.h"
+ #include "parser/parsetree.h"
+ #include "utils/builtins.h"
+ #include "utils/lsyscache.h"
+
+ #include "pgsql_fdw.h"
+
+ /*
+ * Context for walk-through the expression tree.
+ */
+ typedef struct foreign_executable_cxt
+ {
+ PlannerInfo *root;
+ RelOptInfo *foreignrel;
+ } foreign_executable_cxt;
+
+ /*
+ * Deparse query representation into SQL statement which suits for remote
+ * PostgreSQL server.
+ */
+ char *
+ deparseSql(Oid relid, PlannerInfo *root, RelOptInfo *baserel)
+ {
+ StringInfoData foreign_relname;
+ StringInfoData sql; /* builder for SQL statement */
+ bool first;
+ AttrNumber attr;
+ List *attr_used = NIL; /* List of AttNumber used in the query */
+ const char *nspname = NULL; /* plain namespace name */
+ const char *relname = NULL; /* plain relation name */
+ const char *q_nspname; /* quoted namespace name */
+ const char *q_relname; /* quoted relation name */
+ int i;
+ List *rtable = NIL;
+ List *context = NIL;
+
+ initStringInfo(&sql);
+ initStringInfo(&foreign_relname);
+
+ /*
+ * First of all, determine which column should be retrieved for this scan.
+ *
+ * We do this before deparsing SELECT clause because attributes which are
+ * not used in neither reltargetlist nor baserel->baserestrictinfo, quals
+ * evaluated on local, can be replaced with literal "NULL" in the SELECT
+ * clause to reduce overhead of tuple handling tuple and data transfer.
+ */
+ if (baserel->baserestrictinfo != NIL)
+ {
+ ListCell *lc;
+
+ foreach (lc, baserel->baserestrictinfo)
+ {
+ RestrictInfo *ri = (RestrictInfo *) lfirst(lc);
+ List *attrs;
+
+ /*
+ * We need to know which attributes are used in qual evaluated
+ * on the local server, because they should be listed in the
+ * SELECT clause of remote query. We can ignore attributes
+ * which are referenced only in ORDER BY/GROUP BY clause because
+ * such attributes has already been kept in reltargetlist.
+ */
+ attrs = pull_var_clause((Node *) ri->clause,
+ PVC_RECURSE_AGGREGATES,
+ PVC_RECURSE_PLACEHOLDERS);
+ attr_used = list_union(attr_used, attrs);
+ }
+ }
+
+ /*
+ * Determine foreign relation's qualified name. This is necessary for
+ * FROM clause and SELECT clause.
+ */
+ nspname = GetFdwOptionValue(InvalidOid, InvalidOid, relid,
+ InvalidAttrNumber, "nspname");
+ if (nspname == NULL)
+ nspname = get_namespace_name(get_rel_namespace(relid));
+ q_nspname = quote_identifier(nspname);
+
+ relname = GetFdwOptionValue(InvalidOid, InvalidOid, relid,
+ InvalidAttrNumber, "relname");
+ if (relname == NULL)
+ relname = get_rel_name(relid);
+ q_relname = quote_identifier(relname);
+
+ appendStringInfo(&foreign_relname, "%s.%s", q_nspname, q_relname);
+
+ /*
+ * We need to replace aliasname and colnames of the target relation so that
+ * constructed remote query is valid.
+ *
+ * Note that we skip first empty element of simple_rel_array. See also
+ * comments of simple_rel_array and simple_rte_array for the rationale.
+ */
+ for (i = 1; i < root->simple_rel_array_size; i++)
+ {
+ RangeTblEntry *rte = copyObject(root->simple_rte_array[i]);
+ List *newcolnames = NIL;
+
+ if (i == baserel->relid)
+ {
+ /*
+ * Create new list of column names which is used to deparse remote
+ * query from specified names or local column names. This list is
+ * used by deparse_expression.
+ */
+ for (attr = 1; attr <= baserel->max_attr; attr++)
+ {
+ char *colname = NULL;
+
+ /*
+ * Get colname option for each undropped attribute. If colname
+ * option is not supplied, use local column name in remote
+ * query.
+ */
+ if (get_rte_attribute_is_dropped(rte, attr))
+ colname = "";
+ else
+ {
+ colname = GetFdwOptionValue(InvalidOid, InvalidOid, relid,
+ attr, "colname");
+ if (colname == NULL)
+ colname = strVal(list_nth(rte->eref->colnames,
+ attr - 1));
+ }
+ newcolnames = lappend(newcolnames, makeString(colname));
+ }
+ rte->alias = makeAlias(relname, newcolnames);
+ }
+ rtable = lappend(rtable, rte);
+ }
+ context = deparse_context_for_rtelist(rtable);
+
+ /*
+ * deparse SELECT clause
+ *
+ * List attributes which are in either target list or local restriction.
+ * Unused attributes are replaced with a literal "NULL" for optimization.
+ *
+ * Note that nothing is added for dropped columns, though tuple constructor
+ * function requires entries for dropped columns. Such entries must be
+ * initialized with NULL before calling tuple constructor.
+ */
+ appendStringInfo(&sql, "SELECT ");
+ attr_used = list_union(attr_used, baserel->reltargetlist);
+ first = true;
+ for (attr = 1; attr <= baserel->max_attr; attr++)
+ {
+ RangeTblEntry *rte = root->simple_rte_array[baserel->relid];
+ Var *var = NULL;
+ ListCell *lc;
+
+ /* Ignore dropped attributes. */
+ if (get_rte_attribute_is_dropped(rte, attr))
+ continue;
+
+ if (!first)
+ appendStringInfo(&sql, ", ");
+ first = false;
+
+ /*
+ * We use linear search here, but it wouldn't be problem since
+ * attr_used seems to not become so large.
+ */
+ foreach (lc, attr_used)
+ {
+ var = lfirst(lc);
+ if (var->varattno == attr)
+ break;
+ var = NULL;
+ }
+ if (var != NULL)
+ appendStringInfo(&sql, "%s",
+ deparse_expression((Node *) var, context, false, false));
+ else
+ appendStringInfo(&sql, "NULL");
+ }
+ appendStringInfoChar(&sql, ' ');
+
+ /*
+ * deparse FROM clause, including alias if any
+ */
+ appendStringInfo(&sql, "FROM %s", foreign_relname.data);
+
+ elog(DEBUG3, "Remote SQL: %s", sql.data);
+ return sql.data;
+ }
+
diff --git a/contrib/pgsql_fdw/expected/pgsql_fdw.out b/contrib/pgsql_fdw/expected/pgsql_fdw.out
index ...0e2289c .
*** a/contrib/pgsql_fdw/expected/pgsql_fdw.out
--- b/contrib/pgsql_fdw/expected/pgsql_fdw.out
***************
*** 0 ****
--- 1,547 ----
+ -- ===================================================================
+ -- create FDW objects
+ -- ===================================================================
+ -- Clean up in case a prior regression run failed
+ -- Suppress NOTICE messages when roles don't exist
+ SET client_min_messages TO 'error';
+ DROP ROLE IF EXISTS pgsql_fdw_user;
+ RESET client_min_messages;
+ CREATE ROLE pgsql_fdw_user LOGIN SUPERUSER;
+ SET SESSION AUTHORIZATION 'pgsql_fdw_user';
+ CREATE EXTENSION pgsql_fdw;
+ CREATE SERVER loopback1 FOREIGN DATA WRAPPER pgsql_fdw;
+ CREATE SERVER loopback2 FOREIGN DATA WRAPPER pgsql_fdw
+ OPTIONS (dbname 'contrib_regression');
+ CREATE USER MAPPING FOR public SERVER loopback1
+ OPTIONS (user 'value', password 'value');
+ CREATE USER MAPPING FOR pgsql_fdw_user SERVER loopback2;
+ CREATE FOREIGN TABLE ft1 (
+ c0 int,
+ c1 int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10)
+ ) SERVER loopback2;
+ ALTER FOREIGN TABLE ft1 DROP COLUMN c0;
+ CREATE FOREIGN TABLE ft2 (
+ c0 int,
+ c1 int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10)
+ ) SERVER loopback2;
+ ALTER FOREIGN TABLE ft2 DROP COLUMN c0;
+ -- ===================================================================
+ -- create objects used through FDW
+ -- ===================================================================
+ CREATE SCHEMA "S 1";
+ CREATE TABLE "S 1"."T 1" (
+ "C 1" int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10),
+ CONSTRAINT t1_pkey PRIMARY KEY ("C 1")
+ );
+ NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "t1_pkey" for table "T 1"
+ CREATE TABLE "S 1"."T 2" (
+ c1 int NOT NULL,
+ c2 text,
+ CONSTRAINT t2_pkey PRIMARY KEY (c1)
+ );
+ NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "t2_pkey" for table "T 2"
+ BEGIN;
+ TRUNCATE "S 1"."T 1";
+ INSERT INTO "S 1"."T 1"
+ SELECT id,
+ id % 10,
+ to_char(id, 'FM00000'),
+ '1970-01-01'::timestamptz + ((id % 100) || ' days')::interval,
+ '1970-01-01'::timestamp + ((id % 100) || ' days')::interval,
+ id % 10,
+ id % 10
+ FROM generate_series(1, 1000) id;
+ TRUNCATE "S 1"."T 2";
+ INSERT INTO "S 1"."T 2"
+ SELECT id,
+ 'AAA' || to_char(id, 'FM000')
+ FROM generate_series(1, 100) id;
+ COMMIT;
+ -- ===================================================================
+ -- tests for pgsql_fdw_validator
+ -- ===================================================================
+ ALTER FOREIGN DATA WRAPPER pgsql_fdw OPTIONS (host 'value'); -- ERROR
+ ERROR: invalid option "host"
+ HINT: Valid options in this context are:
+ -- requiressl, krbsrvname and gsslib are omitted because they depend on
+ -- configure option
+ ALTER SERVER loopback1 OPTIONS (
+ authtype 'value',
+ service 'value',
+ connect_timeout 'value',
+ dbname 'value',
+ host 'value',
+ hostaddr 'value',
+ port 'value',
+ --client_encoding 'value',
+ tty 'value',
+ options 'value',
+ application_name 'value',
+ --fallback_application_name 'value',
+ keepalives 'value',
+ keepalives_idle 'value',
+ keepalives_interval 'value',
+ -- requiressl 'value',
+ sslcompression 'value',
+ sslmode 'value',
+ sslcert 'value',
+ sslkey 'value',
+ sslrootcert 'value',
+ sslcrl 'value'
+ --requirepeer 'value',
+ -- krbsrvname 'value',
+ -- gsslib 'value',
+ --replication 'value'
+ );
+ ALTER SERVER loopback1 OPTIONS (user 'value'); -- ERROR
+ ERROR: invalid option "user"
+ HINT: Valid options in this context are: authtype, service, connect_timeout, dbname, host, hostaddr, port, tty, options, application_name, keepalives, keepalives_idle, keepalives_interval, keepalives_count, requiressl, sslcompression, sslmode, sslcert, sslkey, sslrootcert, sslcrl, requirepeer, krbsrvname, gsslib, fetch_count
+ ALTER SERVER loopback2 OPTIONS (ADD fetch_count '2');
+ ALTER USER MAPPING FOR public SERVER loopback1
+ OPTIONS (DROP user, DROP password);
+ ALTER USER MAPPING FOR public SERVER loopback1
+ OPTIONS (host 'value'); -- ERROR
+ ERROR: invalid option "host"
+ HINT: Valid options in this context are: user, password
+ ALTER FOREIGN TABLE ft1 OPTIONS (nspname 'S 1', relname 'T 1');
+ ALTER FOREIGN TABLE ft2 OPTIONS (nspname 'S 1', relname 'T 1', fetch_count '100');
+ ALTER FOREIGN TABLE ft1 OPTIONS (invalid 'value'); -- ERROR
+ ERROR: invalid option "invalid"
+ HINT: Valid options in this context are: nspname, relname, fetch_count
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count 'a'); -- ERROR
+ ERROR: invalid value for fetch_count: "a"
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count '0'); -- ERROR
+ ERROR: invalid value for fetch_count: "0"
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count '-1'); -- ERROR
+ ERROR: invalid value for fetch_count: "-1"
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (invalid 'value'); -- ERROR
+ ERROR: invalid option "invalid"
+ HINT: Valid options in this context are: colname
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (colname 'C 1');
+ ALTER FOREIGN TABLE ft2 ALTER COLUMN c1 OPTIONS (colname 'C 1');
+ \dew+
+ List of foreign-data wrappers
+ Name | Owner | Handler | Validator | Access privileges | FDW Options | Description
+ -----------+----------------+-------------------+---------------------+-------------------+-------------+-------------
+ pgsql_fdw | pgsql_fdw_user | pgsql_fdw_handler | pgsql_fdw_validator | | |
+ (1 row)
+
+ \des+
+ List of foreign servers
+ Name | Owner | Foreign-data wrapper | Access privileges | Type | Version | FDW Options | Description
+ -----------+----------------+----------------------+-------------------+------+---------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+-------------
+ loopback1 | pgsql_fdw_user | pgsql_fdw | | | | (authtype 'value', service 'value', connect_timeout 'value', dbname 'value', host 'value', hostaddr 'value', port 'value', tty 'value', options 'value', application_name 'value', keepalives 'value', keepalives_idle 'value', keepalives_interval 'value', sslcompression 'value', sslmode 'value', sslcert 'value', sslkey 'value', sslrootcert 'value', sslcrl 'value') |
+ loopback2 | pgsql_fdw_user | pgsql_fdw | | | | (dbname 'contrib_regression', fetch_count '2') |
+ (2 rows)
+
+ \deu+
+ List of user mappings
+ Server | User name | FDW Options
+ -----------+----------------+-------------
+ loopback1 | public |
+ loopback2 | pgsql_fdw_user |
+ (2 rows)
+
+ \det+
+ List of foreign tables
+ Schema | Table | Server | FDW Options | Description
+ --------+-------+-----------+---------------------------------------------------+-------------
+ public | ft1 | loopback2 | (nspname 'S 1', relname 'T 1') |
+ public | ft2 | loopback2 | (nspname 'S 1', relname 'T 1', fetch_count '100') |
+ (2 rows)
+
+ -- ===================================================================
+ -- simple queries
+ -- ===================================================================
+ -- single table, with/without alias
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
+ QUERY PLAN
+ ------------------------------------------------------------------------------------------------------------------------------
+ Limit
+ Output: c1, c2, c3, c4, c5, c6, c7
+ -> Sort
+ Output: c1, c2, c3, c4, c5, c6, c7
+ Sort Key: ft1.c3, ft1.c1
+ -> Foreign Scan on public.ft1
+ Output: c1, c2, c3, c4, c5, c6, c7
+ Remote SQL: DECLARE pgsql_fdw_cursor_0 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (8 rows)
+
+ SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ -----+----+-------+------------------------------+--------------------------+----+------------
+ 101 | 1 | 00101 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970 | 1 | 1
+ 102 | 2 | 00102 | Sat Jan 03 00:00:00 1970 PST | Sat Jan 03 00:00:00 1970 | 2 | 2
+ 103 | 3 | 00103 | Sun Jan 04 00:00:00 1970 PST | Sun Jan 04 00:00:00 1970 | 3 | 3
+ 104 | 4 | 00104 | Mon Jan 05 00:00:00 1970 PST | Mon Jan 05 00:00:00 1970 | 4 | 4
+ 105 | 5 | 00105 | Tue Jan 06 00:00:00 1970 PST | Tue Jan 06 00:00:00 1970 | 5 | 5
+ 106 | 6 | 00106 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6
+ 107 | 7 | 00107 | Thu Jan 08 00:00:00 1970 PST | Thu Jan 08 00:00:00 1970 | 7 | 7
+ 108 | 8 | 00108 | Fri Jan 09 00:00:00 1970 PST | Fri Jan 09 00:00:00 1970 | 8 | 8
+ 109 | 9 | 00109 | Sat Jan 10 00:00:00 1970 PST | Sat Jan 10 00:00:00 1970 | 9 | 9
+ 110 | 0 | 00110 | Sun Jan 11 00:00:00 1970 PST | Sun Jan 11 00:00:00 1970 | 0 | 0
+ (10 rows)
+
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+ QUERY PLAN
+ ------------------------------------------------------------------------------------------------------------------------------
+ Limit
+ Output: c1, c2, c3, c4, c5, c6, c7
+ -> Sort
+ Output: c1, c2, c3, c4, c5, c6, c7
+ Sort Key: t1.c3, t1.c1
+ -> Foreign Scan on public.ft1 t1
+ Output: c1, c2, c3, c4, c5, c6, c7
+ Remote SQL: DECLARE pgsql_fdw_cursor_2 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (8 rows)
+
+ SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ -----+----+-------+------------------------------+--------------------------+----+------------
+ 101 | 1 | 00101 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970 | 1 | 1
+ 102 | 2 | 00102 | Sat Jan 03 00:00:00 1970 PST | Sat Jan 03 00:00:00 1970 | 2 | 2
+ 103 | 3 | 00103 | Sun Jan 04 00:00:00 1970 PST | Sun Jan 04 00:00:00 1970 | 3 | 3
+ 104 | 4 | 00104 | Mon Jan 05 00:00:00 1970 PST | Mon Jan 05 00:00:00 1970 | 4 | 4
+ 105 | 5 | 00105 | Tue Jan 06 00:00:00 1970 PST | Tue Jan 06 00:00:00 1970 | 5 | 5
+ 106 | 6 | 00106 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6
+ 107 | 7 | 00107 | Thu Jan 08 00:00:00 1970 PST | Thu Jan 08 00:00:00 1970 | 7 | 7
+ 108 | 8 | 00108 | Fri Jan 09 00:00:00 1970 PST | Fri Jan 09 00:00:00 1970 | 8 | 8
+ 109 | 9 | 00109 | Sat Jan 10 00:00:00 1970 PST | Sat Jan 10 00:00:00 1970 | 9 | 9
+ 110 | 0 | 00110 | Sun Jan 11 00:00:00 1970 PST | Sun Jan 11 00:00:00 1970 | 0 | 0
+ (10 rows)
+
+ -- with WHERE clause
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
+ QUERY PLAN
+ ------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on public.ft1 t1
+ Output: c1, c2, c3, c4, c5, c6, c7
+ Filter: ((t1.c1 = 101) AND ((t1.c6)::text = '1'::text) AND (t1.c7 = '1'::bpchar))
+ Remote SQL: DECLARE pgsql_fdw_cursor_4 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (4 rows)
+
+ SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ -----+----+-------+------------------------------+--------------------------+----+------------
+ 101 | 1 | 00101 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970 | 1 | 1
+ (1 row)
+
+ -- aggregate
+ SELECT COUNT(*) FROM ft1 t1;
+ count
+ -------
+ 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
+ ----+----+-------+------------------------------+--------------------------+----+------------
+ 1 | 1 | 00001 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970 | 1 | 1
+ 2 | 2 | 00002 | Sat Jan 03 00:00:00 1970 PST | Sat Jan 03 00:00:00 1970 | 2 | 2
+ 3 | 3 | 00003 | Sun Jan 04 00:00:00 1970 PST | Sun Jan 04 00:00:00 1970 | 3 | 3
+ 4 | 4 | 00004 | Mon Jan 05 00:00:00 1970 PST | Mon Jan 05 00:00:00 1970 | 4 | 4
+ 5 | 5 | 00005 | Tue Jan 06 00:00:00 1970 PST | Tue Jan 06 00:00:00 1970 | 5 | 5
+ 6 | 6 | 00006 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6
+ 7 | 7 | 00007 | Thu Jan 08 00:00:00 1970 PST | Thu Jan 08 00:00:00 1970 | 7 | 7
+ 8 | 8 | 00008 | Fri Jan 09 00:00:00 1970 PST | Fri Jan 09 00:00:00 1970 | 8 | 8
+ 9 | 9 | 00009 | Sat Jan 10 00:00:00 1970 PST | Sat Jan 10 00:00:00 1970 | 9 | 9
+ 10 | 0 | 00010 | Sun Jan 11 00:00:00 1970 PST | Sun Jan 11 00:00:00 1970 | 0 | 0
+ (10 rows)
+
+ -- subquery+MAX
+ SELECT * FROM ft1 t1 WHERE t1.c3 = (SELECT MAX(c3) FROM ft2 t2) ORDER BY c1;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ ------+----+-------+------------------------------+--------------------------+----+------------
+ 1000 | 0 | 01000 | Thu Jan 01 00:00:00 1970 PST | Thu Jan 01 00:00:00 1970 | 0 | 0
+ (1 row)
+
+ -- used in CTE
+ WITH t1 AS (SELECT * FROM ft1 WHERE c1 <= 10) SELECT t2.c1, t2.c2, t2.c3, t2.c4 FROM t1, ft2 t2 WHERE t1.c1 = t2.c1 ORDER BY t1.c1;
+ c1 | c2 | c3 | c4
+ ----+----+-------+------------------------------
+ 1 | 1 | 00001 | Fri Jan 02 00:00:00 1970 PST
+ 2 | 2 | 00002 | Sat Jan 03 00:00:00 1970 PST
+ 3 | 3 | 00003 | Sun Jan 04 00:00:00 1970 PST
+ 4 | 4 | 00004 | Mon Jan 05 00:00:00 1970 PST
+ 5 | 5 | 00005 | Tue Jan 06 00:00:00 1970 PST
+ 6 | 6 | 00006 | Wed Jan 07 00:00:00 1970 PST
+ 7 | 7 | 00007 | Thu Jan 08 00:00:00 1970 PST
+ 8 | 8 | 00008 | Fri Jan 09 00:00:00 1970 PST
+ 9 | 9 | 00009 | Sat Jan 10 00:00:00 1970 PST
+ 10 | 0 | 00010 | Sun Jan 11 00:00:00 1970 PST
+ (10 rows)
+
+ -- fixed values
+ SELECT 'fixed', NULL FROM ft1 t1 WHERE c1 = 1;
+ ?column? | ?column?
+ ----------+----------
+ fixed |
+ (1 row)
+
+ -- user-defined operator/function
+ CREATE FUNCTION pgsql_fdw_abs(int) RETURNS int AS $$
+ BEGIN
+ RETURN abs($1);
+ END
+ $$ LANGUAGE plpgsql IMMUTABLE;
+ CREATE OPERATOR === (
+ LEFTARG = int,
+ RIGHTARG = int,
+ PROCEDURE = int4eq,
+ COMMUTATOR = ===,
+ NEGATOR = !==
+ );
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = pgsql_fdw_abs(t1.c2);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = pgsql_fdw_abs(c2))
+ Remote SQL: DECLARE pgsql_fdw_cursor_18 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 === t1.c2;
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 === c2)
+ Remote SQL: DECLARE pgsql_fdw_cursor_19 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = abs(t1.c2);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = abs(c2))
+ Remote SQL: DECLARE pgsql_fdw_cursor_20 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = t1.c2;
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = c2)
+ Remote SQL: DECLARE pgsql_fdw_cursor_21 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ -- ===================================================================
+ -- parameterized queries
+ -- ===================================================================
+ -- simple join
+ PREPARE st1(int, int) AS SELECT t1.c3, t2.c3 FROM ft1 t1, ft2 t2 WHERE t1.c1 = $1 AND t2.c1 = $2;
+ EXPLAIN (COSTS false) EXECUTE st1(1, 2);
+ QUERY PLAN
+ -----------------------------------------------------------------------------------------------------------------------------------------
+ Nested Loop
+ -> Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_22 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, NULL, NULL, NULL, NULL FROM "S 1"."T 1"
+ -> Materialize
+ -> Foreign Scan on ft2 t2
+ Filter: (c1 = 2)
+ Remote SQL: DECLARE pgsql_fdw_cursor_23 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, NULL, NULL, NULL, NULL FROM "S 1"."T 1"
+ (8 rows)
+
+ EXECUTE st1(1, 1);
+ c3 | c3
+ -------+-------
+ 00001 | 00001
+ (1 row)
+
+ EXECUTE st1(101, 101);
+ c3 | c3
+ -------+-------
+ 00101 | 00101
+ (1 row)
+
+ -- subquery using stable function (can't be pushed down)
+ PREPARE st2(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c4) = 6) ORDER BY c1;
+ EXPLAIN (COSTS false) EXECUTE st2(10, 20);
+ QUERY PLAN
+ ---------------------------------------------------------------------------------------------------------------------------------------------------
+ Sort
+ Sort Key: t1.c1
+ -> Hash Join
+ Hash Cond: (t1.c3 = t2.c3)
+ -> Foreign Scan on ft1 t1
+ Filter: (c1 < 20)
+ Remote SQL: DECLARE pgsql_fdw_cursor_28 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ -> Hash
+ -> HashAggregate
+ -> Foreign Scan on ft2 t2
+ Filter: ((c1 > 10) AND (date_part('dow'::text, c4) = 6::double precision))
+ Remote SQL: DECLARE pgsql_fdw_cursor_29 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, c4, NULL, NULL, NULL FROM "S 1"."T 1"
+ (12 rows)
+
+ EXECUTE st2(10, 20);
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ ----+----+-------+------------------------------+--------------------------+----+------------
+ 16 | 6 | 00016 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970 | 6 | 6
+ (1 row)
+
+ EXECUTE st1(101, 101);
+ c3 | c3
+ -------+-------
+ 00101 | 00101
+ (1 row)
+
+ -- subquery using immutable function (can be pushed down)
+ PREPARE st3(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c5) = 6) ORDER BY c1;
+ EXPLAIN (COSTS false) EXECUTE st3(10, 20);
+ QUERY PLAN
+ ---------------------------------------------------------------------------------------------------------------------------------------------------
+ Sort
+ Sort Key: t1.c1
+ -> Hash Join
+ Hash Cond: (t1.c3 = t2.c3)
+ -> Foreign Scan on ft1 t1
+ Filter: (c1 < 20)
+ Remote SQL: DECLARE pgsql_fdw_cursor_34 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ -> Hash
+ -> HashAggregate
+ -> Foreign Scan on ft2 t2
+ Filter: ((c1 > 10) AND (date_part('dow'::text, c5) = 6::double precision))
+ Remote SQL: DECLARE pgsql_fdw_cursor_35 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, NULL, c5, NULL, NULL FROM "S 1"."T 1"
+ (12 rows)
+
+ EXECUTE st3(10, 20);
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ ----+----+-------+------------------------------+--------------------------+----+------------
+ 16 | 6 | 00016 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970 | 6 | 6
+ (1 row)
+
+ EXECUTE st3(20, 30);
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ ----+----+-------+------------------------------+--------------------------+----+------------
+ 23 | 3 | 00023 | Sat Jan 24 00:00:00 1970 PST | Sat Jan 24 00:00:00 1970 | 3 | 3
+ (1 row)
+
+ -- custom plan should be chosen
+ PREPARE st4(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 = $1;
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_40 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_41 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_42 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_43 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_44 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_46 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ -- cleanup
+ DEALLOCATE st1;
+ DEALLOCATE st2;
+ DEALLOCATE st3;
+ DEALLOCATE st4;
+ -- ===================================================================
+ -- connection management
+ -- ===================================================================
+ SELECT srvname, usename FROM pgsql_fdw_connections;
+ srvname | usename
+ -----------+----------------
+ loopback2 | pgsql_fdw_user
+ (1 row)
+
+ SELECT pgsql_fdw_disconnect(srvid, usesysid) FROM pgsql_fdw_get_connections();
+ pgsql_fdw_disconnect
+ ----------------------
+ OK
+ (1 row)
+
+ SELECT srvname, usename FROM pgsql_fdw_connections;
+ srvname | usename
+ ---------+---------
+ (0 rows)
+
+ -- ===================================================================
+ -- cleanup
+ -- ===================================================================
+ DROP OPERATOR === (int, int) CASCADE;
+ DROP OPERATOR !== (int, int) CASCADE;
+ DROP FUNCTION pgsql_fdw_abs(int);
+ DROP SCHEMA "S 1" CASCADE;
+ NOTICE: drop cascades to 2 other objects
+ DETAIL: drop cascades to table "S 1"."T 1"
+ drop cascades to table "S 1"."T 2"
+ DROP EXTENSION pgsql_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 server loopback2
+ drop cascades to user mapping for pgsql_fdw_user
+ drop cascades to foreign table ft1
+ drop cascades to foreign table ft2
+ \c
+ DROP ROLE pgsql_fdw_user;
diff --git a/contrib/pgsql_fdw/option.c b/contrib/pgsql_fdw/option.c
index ...13035c1 .
*** a/contrib/pgsql_fdw/option.c
--- b/contrib/pgsql_fdw/option.c
***************
*** 0 ****
--- 1,241 ----
+ /*-------------------------------------------------------------------------
+ *
+ * option.c
+ * FDW option handling
+ *
+ * Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/option.c
+ *
+ *-------------------------------------------------------------------------
+ */
+ #include "postgres.h"
+
+ #include "access/reloptions.h"
+ #include "catalog/pg_foreign_data_wrapper.h"
+ #include "catalog/pg_foreign_server.h"
+ #include "catalog/pg_foreign_table.h"
+ #include "catalog/pg_user_mapping.h"
+ #include "fmgr.h"
+ #include "foreign/foreign.h"
+ #include "lib/stringinfo.h"
+ #include "miscadmin.h"
+
+ #include "pgsql_fdw.h"
+
+ /*
+ * SQL functions
+ */
+ extern Datum pgsql_fdw_validator(PG_FUNCTION_ARGS);
+ PG_FUNCTION_INFO_V1(pgsql_fdw_validator);
+
+ /*
+ * Describes the valid options for objects that use this wrapper.
+ */
+ typedef struct PgsqlFdwOption
+ {
+ const char *optname;
+ Oid optcontext; /* Oid of catalog in which options may appear */
+ bool is_libpq_opt; /* true if it's used in libpq */
+ } PgsqlFdwOption;
+
+ /*
+ * Valid options for pgsql_fdw.
+ */
+ static PgsqlFdwOption valid_options[] = {
+
+ /*
+ * Options for libpq connection.
+ * Note: This list should be updated along with PQconninfoOptions in
+ * interfaces/libpq/fe-connect.c, so the order is kept as is.
+ *
+ * Some useless libpq connection options are not accepted by pgsql_fdw:
+ * client_encoding: set to local database encoding automatically
+ * fallback_application_name: fixed to "pgsql_fdw"
+ * replication: pgsql_fdw never be replication client
+ */
+ {"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},
+ #ifdef NOT_USED
+ {"client_encoding", ForeignServerRelationId, true},
+ #endif
+ {"tty", ForeignServerRelationId, true},
+ {"options", ForeignServerRelationId, true},
+ {"application_name", ForeignServerRelationId, true},
+ #ifdef NOT_USED
+ {"fallback_application_name", ForeignServerRelationId, true},
+ #endif
+ {"keepalives", ForeignServerRelationId, true},
+ {"keepalives_idle", ForeignServerRelationId, true},
+ {"keepalives_interval", ForeignServerRelationId, true},
+ {"keepalives_count", ForeignServerRelationId, true},
+ {"requiressl", ForeignServerRelationId, true},
+ {"sslcompression", ForeignServerRelationId, true},
+ {"sslmode", ForeignServerRelationId, true},
+ {"sslcert", ForeignServerRelationId, true},
+ {"sslkey", ForeignServerRelationId, true},
+ {"sslrootcert", ForeignServerRelationId, true},
+ {"sslcrl", ForeignServerRelationId, true},
+ {"requirepeer", ForeignServerRelationId, true},
+ {"krbsrvname", ForeignServerRelationId, true},
+ {"gsslib", ForeignServerRelationId, true},
+ #ifdef NOT_USED
+ {"replication", ForeignServerRelationId, true},
+ #endif
+
+ /*
+ * Options for translation of object names.
+ */
+ {"nspname", ForeignTableRelationId, false},
+ {"relname", ForeignTableRelationId, false},
+ {"colname", AttributeRelationId, false},
+
+ /*
+ * Options for cursor behavior.
+ * These options can be overridden by finer-grained objects.
+ */
+ {"fetch_count", ForeignTableRelationId, false},
+ {"fetch_count", ForeignServerRelationId, false},
+
+ /* Terminating entry --- MUST BE LAST */
+ {NULL, InvalidOid, false}
+ };
+
+ /*
+ * Helper functions
+ */
+ static bool is_valid_option(const char *optname, Oid context);
+
+ /*
+ * Validate the generic options given to a FOREIGN DATA WRAPPER, SERVER,
+ * USER MAPPING or FOREIGN TABLE that uses pgsql_fdw.
+ *
+ * Raise an ERROR if the option or its value is considered invalid.
+ */
+ Datum
+ pgsql_fdw_validator(PG_FUNCTION_ARGS)
+ {
+ List *options_list = untransformRelOptions(PG_GETARG_DATUM(0));
+ Oid catalog = PG_GETARG_OID(1);
+ ListCell *cell;
+
+ /*
+ * Check that only options supported by pgsql_fdw, and allowed for the
+ * current object type, are given.
+ */
+ foreach(cell, options_list)
+ {
+ DefElem *def = (DefElem *) lfirst(cell);
+
+ if (!is_valid_option(def->defname, catalog))
+ {
+ PgsqlFdwOption *opt;
+ StringInfoData buf;
+
+ /*
+ * Unknown option specified, complain about it. Provide a hint
+ * with list of valid options for the object.
+ */
+ initStringInfo(&buf);
+ for (opt = valid_options; opt->optname; opt++)
+ {
+ if (catalog == opt->optcontext)
+ appendStringInfo(&buf, "%s%s", (buf.len > 0) ? ", " : "",
+ opt->optname);
+ }
+
+ ereport(ERROR,
+ (errcode(ERRCODE_FDW_INVALID_OPTION_NAME),
+ errmsg("invalid option \"%s\"", def->defname),
+ errhint("Valid options in this context are: %s",
+ buf.data)));
+ }
+
+ /* fetch_count be positive digit number. */
+ if (strcmp(def->defname, "fetch_count") == 0)
+ {
+ long value;
+ char *p = NULL;
+
+ value = strtol(strVal(def->arg), &p, 10);
+ if (*p != '\0' || value < 1)
+ ereport(ERROR,
+ (errcode(ERRCODE_FDW_INVALID_ATTRIBUTE_VALUE),
+ errmsg("invalid value for %s: \"%s\"",
+ def->defname, strVal(def->arg))));
+ }
+ }
+
+ /*
+ * We don't care option-specific limitation here; they will be validated at
+ * the execution time.
+ */
+
+ PG_RETURN_VOID();
+ }
+
+ /*
+ * Check whether the given option is one of the valid pgsql_fdw options.
+ * context is the Oid of the catalog holding the object the option is for.
+ */
+ static bool
+ is_valid_option(const char *optname, Oid context)
+ {
+ PgsqlFdwOption *opt;
+
+ for (opt = valid_options; opt->optname; opt++)
+ {
+ if (context == opt->optcontext && strcmp(opt->optname, optname) == 0)
+ return true;
+ }
+ return false;
+ }
+
+ /*
+ * Check whether the given option is one of the valid libpq options.
+ * context is the Oid of the catalog holding the object the option is for.
+ */
+ static bool
+ is_libpq_option(const char *optname)
+ {
+ PgsqlFdwOption *opt;
+
+ for (opt = valid_options; opt->optname; opt++)
+ {
+ if (strcmp(opt->optname, optname) == 0 && opt->is_libpq_opt)
+ return true;
+ }
+ return false;
+ }
+
+ /*
+ * Generate key-value arrays which includes only libpq options from the list
+ * which contains any kind of options.
+ */
+ int
+ ExtractConnectionOptions(List *defelems, const char **keywords, const char **values)
+ {
+ ListCell *lc;
+ int i;
+
+ i = 0;
+ foreach(lc, defelems)
+ {
+ DefElem *d = (DefElem *) lfirst(lc);
+ if (is_libpq_option(d->defname))
+ {
+ keywords[i] = d->defname;
+ values[i] = strVal(d->arg);
+ i++;
+ }
+ }
+ return i;
+ }
diff --git a/contrib/pgsql_fdw/pgsql_fdw--1.0.sql b/contrib/pgsql_fdw/pgsql_fdw--1.0.sql
index ...b0ea2b2 .
*** a/contrib/pgsql_fdw/pgsql_fdw--1.0.sql
--- b/contrib/pgsql_fdw/pgsql_fdw--1.0.sql
***************
*** 0 ****
--- 1,39 ----
+ /* contrib/pgsql_fdw/pgsql_fdw--1.0.sql */
+
+ -- complain if script is sourced in psql, rather than via CREATE EXTENSION
+ \echo Use "CREATE EXTENSION pgsql_fdw" to load this file. \quit
+
+ CREATE FUNCTION pgsql_fdw_handler()
+ RETURNS fdw_handler
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C STRICT;
+
+ CREATE FUNCTION pgsql_fdw_validator(text[], oid)
+ RETURNS void
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C STRICT;
+
+ CREATE FOREIGN DATA WRAPPER pgsql_fdw
+ HANDLER pgsql_fdw_handler
+ VALIDATOR pgsql_fdw_validator;
+
+ /* connection management functions and view */
+ CREATE FUNCTION pgsql_fdw_get_connections(out srvid oid, out usesysid oid)
+ RETURNS SETOF record
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C STRICT;
+
+ CREATE FUNCTION pgsql_fdw_disconnect(oid, oid)
+ RETURNS text
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C STRICT;
+
+ CREATE VIEW pgsql_fdw_connections AS
+ SELECT c.srvid srvid,
+ s.srvname srvname,
+ c.usesysid usesysid,
+ pg_get_userbyid(c.usesysid) usename
+ FROM pgsql_fdw_get_connections() c
+ JOIN pg_catalog.pg_foreign_server s ON (s.oid = c.srvid);
+ GRANT SELECT ON pgsql_fdw_connections TO public;
+
diff --git a/contrib/pgsql_fdw/pgsql_fdw.c b/contrib/pgsql_fdw/pgsql_fdw.c
index ...50f927b .
*** a/contrib/pgsql_fdw/pgsql_fdw.c
--- b/contrib/pgsql_fdw/pgsql_fdw.c
***************
*** 0 ****
--- 1,907 ----
+ /*-------------------------------------------------------------------------
+ *
+ * pgsql_fdw.c
+ * foreign-data wrapper for remote PostgreSQL servers.
+ *
+ * Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/pgsql_fdw.c
+ *
+ *-------------------------------------------------------------------------
+ */
+ #include "postgres.h"
+ #include "fmgr.h"
+
+ #include "catalog/pg_foreign_server.h"
+ #include "catalog/pg_foreign_table.h"
+ #include "commands/explain.h"
+ #include "foreign/fdwapi.h"
+ #include "funcapi.h"
+ #include "miscadmin.h"
+ #include "nodes/nodeFuncs.h"
+ #include "optimizer/cost.h"
+ #include "optimizer/pathnode.h"
+ #include "utils/lsyscache.h"
+ #include "utils/memutils.h"
+ #include "utils/rel.h"
+
+ #include "pgsql_fdw.h"
+ #include "connection.h"
+
+ PG_MODULE_MAGIC;
+
+ /*
+ * Default fetch count for cursor. This can be overridden by fetch_count FDW
+ * option.
+ */
+ #define DEFAULT_FETCH_COUNT 10000
+
+ /*
+ * Cost to establish a connection.
+ * XXX: should be configurable per server?
+ */
+ #define CONNECTION_COSTS 100.0
+
+ /*
+ * Cost to transfer 1 byte from remote server.
+ * XXX: should be configurable per server?
+ */
+ #define TRANSFER_COSTS_PER_BYTE 0.001
+
+ /*
+ * Cursors which are used together in a local query require different name, so
+ * we use simple incremental name for that purpose. We don't care wrap around
+ * of cursor_id because it's hard to imagine that 2^32 cursors are used in a
+ * query.
+ */
+ #define CURSOR_NAME_FORMAT "pgsql_fdw_cursor_%u"
+ static uint32 cursor_id = 0;
+
+ /*
+ * Index of FDW-private items stored in FdwPlan.
+ */
+ enum FdwPrivateIndex {
+ FdwPrivateSelectSql,
+ FdwPrivateDeclareSql,
+ FdwPrivateFetchSql,
+ FdwPrivateResetSql,
+ FdwPrivateCloseSql,
+
+ /* # of elements stored in the list fdw_private */
+ FdwPrivateNum,
+ };
+
+ /*
+ * Describes an execution state of a foreign scan against a foreign table
+ * using pgsql_fdw.
+ */
+ typedef struct PgsqlFdwExecutionState
+ {
+ MemoryContext scan_cxt; /* context for per-scan lifespan data */
+
+ FdwPlan *fdwplan; /* FDW-specific planning information */
+ PGconn *conn; /* connection for the scan */
+
+ Oid *param_types; /* type array of external parameter */
+ const char **param_values; /* value array of external parameter */
+
+ int attnum; /* # of non-dropped attribute */
+ char **col_values; /* column value buffer */
+ AttInMetadata *attinmeta; /* attribute metadata */
+
+ Tuplestorestate *tuples; /* result of the scan */
+ } PgsqlFdwExecutionState;
+
+ /*
+ * SQL functions
+ */
+ extern Datum pgsql_fdw_handler(PG_FUNCTION_ARGS);
+ PG_FUNCTION_INFO_V1(pgsql_fdw_handler);
+
+ /*
+ * FDW callback routines
+ */
+ static FdwPlan *pgsqlPlanForeignScan(Oid foreigntableid,
+ PlannerInfo *root,
+ RelOptInfo *baserel);
+ static void pgsqlExplainForeignScan(ForeignScanState *node, ExplainState *es);
+ static void pgsqlBeginForeignScan(ForeignScanState *node, int eflags);
+ static TupleTableSlot *pgsqlIterateForeignScan(ForeignScanState *node);
+ static void pgsqlReScanForeignScan(ForeignScanState *node);
+ static void pgsqlEndForeignScan(ForeignScanState *node);
+
+ /*
+ * Helper functions
+ */
+ static void estimate_costs(PlannerInfo *root,
+ RelOptInfo *baserel,
+ const char *sql,
+ Oid serverid,
+ Cost *startup_cost,
+ Cost *total_cost);
+ static void execute_query(ForeignScanState *node);
+ static PGresult *fetch_result(ForeignScanState *node);
+ static void store_result(ForeignScanState *node, PGresult *res);
+ static bool contain_ext_param(Node *clause);
+ static bool contain_ext_param_walker(Node *node, void *context);
+
+ /* Exported functions, but not written in pgsql_fdw.h. */
+ void _PG_init(void);
+ void _PG_fini(void);
+
+ /*
+ * Module-specific initialization.
+ */
+ void
+ _PG_init(void)
+ {
+ }
+
+ /*
+ * Module-specific clean up.
+ */
+ void
+ _PG_fini(void)
+ {
+ }
+
+ /*
+ * The content of FdwRoutine of pgsql_fdw is never changed, so we use one
+ * static object for all scan.
+ */
+ static FdwRoutine fdwroutine = {
+
+ /* Node type */
+ T_FdwRoutine,
+
+ /* Required handler functions. */
+ pgsqlPlanForeignScan,
+ pgsqlExplainForeignScan,
+ pgsqlBeginForeignScan,
+ pgsqlIterateForeignScan,
+ pgsqlReScanForeignScan,
+ pgsqlEndForeignScan,
+
+ /* Optional handler functions. */
+ };
+
+ /*
+ * Foreign-data wrapper handler function: return a struct with pointers
+ * to my callback routines.
+ */
+ Datum
+ pgsql_fdw_handler(PG_FUNCTION_ARGS)
+ {
+ PG_RETURN_POINTER(&fdwroutine);
+ }
+
+ /*
+ * pgsqlPlanForeignScan
+ * Create a FdwPlan for a scan on the foreign table
+ */
+ static FdwPlan *
+ pgsqlPlanForeignScan(Oid foreigntableid,
+ PlannerInfo *root,
+ RelOptInfo *baserel)
+ {
+ char name[128]; /* must be larger than format + 10 */
+ StringInfoData cursor;
+ const char *fetch_count_str;
+ int fetch_count = DEFAULT_FETCH_COUNT;
+ char *sql;
+ FdwPlan *fdwplan;
+ List *fdw_private = NIL;
+ ForeignTable *table;
+ ForeignServer *server;
+
+ /* Construct FdwPlan with cost estimates */
+ fdwplan = makeNode(FdwPlan);
+ sql = deparseSql(foreigntableid, root, baserel);
+ table = GetForeignTable(foreigntableid);
+ server = GetForeignServer(table->serverid);
+ estimate_costs(root, baserel, sql, server->serverid,
+ &fdwplan->startup_cost, &fdwplan->total_cost);
+
+ /*
+ * Store plain SELECT statement in private area of FdwPlan. This will be
+ * used for executing remote query and explaining scan.
+ */
+ fdw_private = list_make1(makeString(sql));
+
+ /* Use specified fetch_count instead of default value, if any. */
+ fetch_count_str = GetFdwOptionValue(InvalidOid, InvalidOid, foreigntableid,
+ InvalidAttrNumber, "fetch_count");
+ if (fetch_count_str != NULL)
+ fetch_count = strtol(fetch_count_str, NULL, 10);
+ elog(DEBUG1, "relid=%u fetch_count=%d", foreigntableid, fetch_count);
+
+ /*
+ * We store some more information in FdwPlan to pass them beyond the
+ * boundary between planner and executor. Finally FdwPlan using cursor
+ * would hold items below:
+ *
+ * 1) plain SELECT statement (already added above)
+ * 2) SQL statement used to declare cursor
+ * 3) SQL statement used to fetch rows from cursor
+ * 4) SQL statement used to reset cursor
+ * 5) SQL statement used to close cursor
+ *
+ * These items are indexed with the enum FdwPrivateIndex, so an item
+ * can be accessed directly via list_nth(). For example of FETCH
+ * statement:
+ * list_nth(fdw_private, FdwPrivateFetchSql)
+ */
+
+ /* Construct cursor name from sequential value */
+ sprintf(name, CURSOR_NAME_FORMAT, cursor_id++);
+
+ /* Construct statement to declare cursor */
+ initStringInfo(&cursor);
+ appendStringInfo(&cursor, "DECLARE %s SCROLL CURSOR FOR %s", name, sql);
+ fdw_private = lappend(fdw_private, makeString(cursor.data));
+
+ /* Construct statement to fetch rows from cursor */
+ initStringInfo(&cursor);
+ appendStringInfo(&cursor, "FETCH %d FROM %s", fetch_count, name);
+ fdw_private = lappend(fdw_private, makeString(cursor.data));
+
+ /* Construct statement to reset cursor */
+ initStringInfo(&cursor);
+ appendStringInfo(&cursor, "MOVE ABSOLUTE 0 FROM %s", name);
+ fdw_private = lappend(fdw_private, makeString(cursor.data));
+
+ /* Construct statement to close cursor */
+ initStringInfo(&cursor);
+ appendStringInfo(&cursor, "CLOSE %s", name);
+ fdw_private = lappend(fdw_private, makeString(cursor.data));
+
+ /* Store FDW private information into FdwPlan */
+ fdwplan->fdw_private = fdw_private;
+
+ return fdwplan;
+ }
+
+ /*
+ * pgsqlExplainForeignScan
+ * Produce extra output for EXPLAIN
+ */
+ static void
+ pgsqlExplainForeignScan(ForeignScanState *node, ExplainState *es)
+ {
+ FdwPlan *fdwplan;
+ char *sql;
+
+ fdwplan = ((ForeignScan *) node->ss.ps.plan)->fdwplan;
+ sql = strVal(list_nth(fdwplan->fdw_private, FdwPrivateDeclareSql));
+ ExplainPropertyText("Remote SQL", sql, es);
+ }
+
+ /*
+ * pgsqlBeginForeignScan
+ * Initiate access to a foreign PostgreSQL table.
+ */
+ static void
+ pgsqlBeginForeignScan(ForeignScanState *node, int eflags)
+ {
+ PgsqlFdwExecutionState *festate;
+ PGconn *conn;
+ Oid relid;
+ ForeignTable *table;
+ ForeignServer *server;
+ UserMapping *user;
+ TupleTableSlot *slot = node->ss.ss_ScanTupleSlot;
+
+ /*
+ * Do nothing in EXPLAIN (no ANALYZE) case. node->fdw_state stays NULL.
+ */
+ if (eflags & EXEC_FLAG_EXPLAIN_ONLY)
+ return;
+
+ /*
+ * Save state in node->fdw_state.
+ */
+ festate = (PgsqlFdwExecutionState *) palloc(sizeof(PgsqlFdwExecutionState));
+ festate->fdwplan = ((ForeignScan *) node->ss.ps.plan)->fdwplan;
+
+ /*
+ * Create context for per-scan tuplestore under per-query context.
+ */
+ festate->scan_cxt = AllocSetContextCreate(node->ss.ps.state->es_query_cxt,
+ "pgsql_fdw",
+ ALLOCSET_DEFAULT_MINSIZE,
+ ALLOCSET_DEFAULT_INITSIZE,
+ ALLOCSET_DEFAULT_MAXSIZE);
+
+ /*
+ * Get connection to the foreign server. Connection manager would
+ * establish new connection if necessary.
+ */
+ relid = RelationGetRelid(node->ss.ss_currentRelation);
+ table = GetForeignTable(relid);
+ server = GetForeignServer(table->serverid);
+ user = GetUserMapping(GetOuterUserId(), server->serverid);
+ conn = GetConnection(server, user, true);
+ festate->conn = conn;
+
+ /* Result will be filled in first Iterate call. */
+ festate->tuples = NULL;
+
+ /* Allocate buffers for column values. */
+ {
+ TupleDesc tupdesc = slot->tts_tupleDescriptor;
+ festate->col_values = palloc(sizeof(char *) * tupdesc->natts);
+ festate->attinmeta = TupleDescGetAttInMetadata(tupdesc);
+ }
+
+ /* Allocate buffers for query parameters. */
+ {
+ ParamListInfo params = node->ss.ps.state->es_param_list_info;
+ int numParams = params ? params->numParams : 0;
+
+ if (numParams > 0)
+ {
+ festate->param_types = palloc0(sizeof(Oid) * numParams);
+ festate->param_values = palloc0(sizeof(char *) * numParams);
+ }
+ else
+ {
+ festate->param_types = NULL;
+ festate->param_values = NULL;
+ }
+ }
+
+ /* Store FDW-specific state into ForeignScanState */
+ node->fdw_state = (void *) festate;
+
+ return;
+ }
+
+ /*
+ * pgsqlIterateForeignScan
+ * Retrieve next row from the result set, or clear tuple slot to indicate
+ * EOF.
+ *
+ * Note that using per-query context when retrieving tuples from
+ * tuplestore to ensure that returned tuples can survive until next
+ * iteration because the tuple is released implicitly via ExecClearTuple.
+ * Retrieving a tuple from tuplestore in CurrentMemoryContext (it's a
+ * per-tuple context), ExecClearTuple will free dangling pointer.
+ */
+ static TupleTableSlot *
+ pgsqlIterateForeignScan(ForeignScanState *node)
+ {
+ PgsqlFdwExecutionState *festate;
+ TupleTableSlot *slot = node->ss.ss_ScanTupleSlot;
+ PGresult *res = NULL;
+ MemoryContext oldcontext = CurrentMemoryContext;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+
+ /*
+ * If this is the first call after Begin, we need to execute remote query.
+ * If the query needs cursor, we declare a cursor at first call and fetch
+ * from it in later calls.
+ */
+ if (festate->tuples == NULL)
+ execute_query(node);
+
+ /*
+ * If enough tuples are left in tuplestore, just return next tuple from it.
+ *
+ * It is necessary to switch to per-scan context to make returned tuple
+ * valid until next IterateForeignScan call, because it will be released
+ * with ExecClearTuple then. Otherwise, picked tuple is allocated in
+ * per-tuple context, and double-free of that tuple might happen.
+ */
+ MemoryContextSwitchTo(festate->scan_cxt);
+ if (tuplestore_gettupleslot(festate->tuples, true, false, slot))
+ {
+ MemoryContextSwitchTo(oldcontext);
+ return slot;
+ }
+ MemoryContextSwitchTo(oldcontext);
+
+ /*
+ * Here we need to clear partial result and fetch next bunch of tuples from
+ * from the cursor for the scan. If the fetch returns no tuple, the scan
+ * has reached the end.
+ *
+ * PGresult must be released before leaving this function.
+ */
+ PG_TRY();
+ {
+ res = fetch_result(node);
+ store_result(node, res);
+ PQclear(res);
+ res = NULL;
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+
+ /*
+ * If we got more tuples from the server cursor, return next tuple from
+ * tuplestore.
+ */
+ MemoryContextSwitchTo(festate->scan_cxt);
+ if (tuplestore_gettupleslot(festate->tuples, true, false, slot))
+ {
+ MemoryContextSwitchTo(oldcontext);
+ return slot;
+ }
+ MemoryContextSwitchTo(oldcontext);
+
+ /* We don't have any result even in remote server cursor. */
+ ExecClearTuple(slot);
+ return slot;
+ }
+
+ /*
+ * pgsqlReScanForeignScan
+ * - Restart this scan by resetting fetch location.
+ */
+ static void
+ pgsqlReScanForeignScan(ForeignScanState *node)
+ {
+ List *fdw_private;
+ char *sql;
+ PGconn *conn;
+ PGresult *res = NULL;
+ PgsqlFdwExecutionState *festate;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+
+ /* If we have not opened cursor yet, nothing to do. */
+ if (festate->tuples == NULL)
+ return;
+
+ /* Discard fetch results if any. */
+ tuplestore_clear(festate->tuples);
+
+ /* PGresult must be released before leaving this function. */
+ PG_TRY();
+ {
+ /* Reset cursor */
+ fdw_private = festate->fdwplan->fdw_private;
+ conn = festate->conn;
+ sql = strVal(list_nth(fdw_private, FdwPrivateResetSql));
+ res = PQexec(conn, sql);
+ if (PQresultStatus(res) != PGRES_COMMAND_OK)
+ {
+ ereport(ERROR,
+ (errmsg("could not rewind cursor"),
+ errdetail("%s", PQerrorMessage(conn)),
+ errhint("%s", sql)));
+ }
+ PQclear(res);
+ res = NULL;
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+ }
+
+ /*
+ * pgsqlEndForeignScan
+ * Finish scanning foreign table and dispose objects used for this scan
+ */
+ static void
+ pgsqlEndForeignScan(ForeignScanState *node)
+ {
+ List *fdw_private;
+ char *sql;
+ PGconn *conn;
+ PGresult *res = NULL;
+ PgsqlFdwExecutionState *festate;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+
+ /* if festate is NULL, we are in EXPLAIN; nothing to do */
+ if (festate == NULL)
+ return;
+
+ /* If we have not opened cursor yet, nothing to do. */
+ if (festate->tuples == NULL)
+ return;
+
+ /* Discard fetch results */
+ tuplestore_end(festate->tuples);
+ festate->tuples = NULL;
+
+ /* PGresult must be released before leaving this function. */
+ PG_TRY();
+ {
+ /* Close cursor */
+ fdw_private = festate->fdwplan->fdw_private;
+ conn = festate->conn;
+ sql = strVal(list_nth(fdw_private, FdwPrivateCloseSql));
+ res = PQexec(conn, sql);
+ if (PQresultStatus(res) != PGRES_COMMAND_OK)
+ {
+ ereport(ERROR,
+ (errmsg("could not close cursor"),
+ errdetail("%s", PQerrorMessage(conn)),
+ errhint("%s", sql)));
+ }
+ PQclear(res);
+ res = NULL;
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+
+ ReleaseConnection(festate->conn);
+
+ MemoryContextDelete(festate->scan_cxt);
+ festate->scan_cxt = NULL;
+ }
+
+ /*
+ * Estimate costs of scanning a foreign table.
+ */
+ static void
+ estimate_costs(PlannerInfo *root, RelOptInfo *baserel,
+ const char *sql, Oid serverid,
+ Cost *startup_cost, Cost *total_cost)
+ {
+ ListCell *lc;
+ ForeignServer *server;
+ UserMapping *user;
+ PGconn *conn = NULL;
+ PGresult *res = NULL;
+ StringInfoData buf;
+ char *plan;
+ char *p;
+ int n;
+
+ /*
+ * If the baserestrictinfo contains any Param node with paramkind
+ * PARAM_EXTERNAL, we need result of EXPLAIN for EXECUTE statement, not for
+ * simple SELECT statement. However, we can't get actual parameter values
+ * here, so we use fixed and large costs as second best so that planner
+ * tend to choose custom plan.
+ *
+ * See comments in plancache.c for details of custom plan.
+ */
+ foreach(lc, baserel->baserestrictinfo)
+ {
+ RestrictInfo *rs = (RestrictInfo *) lfirst(lc);
+ if (contain_ext_param((Node *) rs->clause))
+ {
+ *startup_cost = CONNECTION_COSTS;
+ *total_cost = 10000; /* groundless large costs */
+
+ return;
+ }
+ }
+
+ /*
+ * Get connection to the foreign server. Connection manager would
+ * establish new connection if necessary.
+ */
+ server = GetForeignServer(serverid);
+ user = GetUserMapping(GetOuterUserId(), server->serverid);
+ conn = GetConnection(server, user, false);
+ initStringInfo(&buf);
+ appendStringInfo(&buf, "EXPLAIN %s", sql);
+
+ /* PGresult must be released before leaving this function. */
+ PG_TRY();
+ {
+ res = PQexec(conn, buf.data);
+ if (PQresultStatus(res) != PGRES_TUPLES_OK || PQntuples(res) == 0)
+ {
+ char *msg;
+
+ msg = pstrdup(PQerrorMessage(conn));
+ ereport(ERROR,
+ (errmsg("could not execute EXPLAIN for cost estimation"),
+ errdetail("%s", msg),
+ errhint("%s", sql)));
+ }
+
+ /*
+ * Find estimation portion from top plan node. Here we search opening
+ * parentheses from the end of the line to avoid finding unexpected
+ * parentheses.
+ */
+ plan = PQgetvalue(res, 0, 0);
+ p = strrchr(plan, '(');
+ if (p == NULL)
+ elog(ERROR, "wrong EXPLAIN output: %s", plan);
+ n = sscanf(p,
+ "(cost=%lf..%lf rows=%lf width=%d)",
+ startup_cost, total_cost, &baserel->rows, &baserel->width);
+ if (n != 4)
+ elog(ERROR, "could not get estimation from EXPLAIN output");
+
+ PQclear(res);
+ res = NULL;
+ ReleaseConnection(conn);
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+
+ /*
+ * TODO Selectivity of quals which are NOT pushed down should be also
+ * considered.
+ */
+
+ /* add cost to establish connection. */
+ *startup_cost += CONNECTION_COSTS;
+ *total_cost += CONNECTION_COSTS;
+
+ /* add cost to transfer result. */
+ *total_cost += TRANSFER_COSTS_PER_BYTE * baserel->width * baserel->tuples;
+ *total_cost += cpu_tuple_cost * baserel->tuples;
+ }
+
+ /*
+ * Execute remote query with current parameters.
+ */
+ static void
+ execute_query(ForeignScanState *node)
+ {
+ FdwPlan *fdwplan;
+ PgsqlFdwExecutionState *festate;
+ ParamListInfo params = node->ss.ps.state->es_param_list_info;
+ int numParams = params ? params->numParams : 0;
+ Oid *types = NULL;
+ const char **values = NULL;
+ char *sql;
+ PGconn *conn;
+ PGresult *res = NULL;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+ types = festate->param_types;
+ values = festate->param_values;
+
+ /*
+ * Construct parameter array in text format. We don't release memory for
+ * the arrays explicitly, because the memory usage would not be very large,
+ * and anyway they will be released in context cleanup.
+ */
+ if (numParams > 0)
+ {
+ int i;
+
+ 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;
+
+ getTypeOutputInfo(types[i], &out_func_oid, &isvarlena);
+ fmgr_info(out_func_oid, &func);
+ values[i] = OutputFunctionCall(&func, params->params[i].value);
+ }
+ }
+ }
+
+ /* PGresult must be released before leaving this function. */
+ PG_TRY();
+ {
+ /*
+ * Execute remote query with parameters.
+ */
+ conn = festate->conn;
+ fdwplan = ((ForeignScan *) node->ss.ps.plan)->fdwplan;
+ sql = strVal(list_nth(fdwplan->fdw_private, FdwPrivateDeclareSql));
+ res = PQexecParams(conn, sql, numParams, types, values, NULL, NULL, 0);
+
+ /*
+ * If the query has failed, reporting details is enough here.
+ * Connection(s) which are used by this query (at least used by
+ * pgsql_fdw) will be cleaned up by the foreign connection manager.
+ */
+ if (PQresultStatus(res) != PGRES_COMMAND_OK)
+ {
+ ereport(ERROR,
+ (errmsg("could not declare cursor"),
+ errdetail("%s", PQerrorMessage(conn)),
+ errhint("%s", sql)));
+ }
+
+ /* Discard result of DECLARE statement. */
+ PQclear(res);
+ res = NULL;
+
+ /* Fetch first bunch of the result and store them into tuplestore. */
+ res = fetch_result(node);
+ store_result(node, res);
+ PQclear(res);
+ res = NULL;
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+ }
+
+ /*
+ * Fetch next partial result from remote server.
+ *
+ * Once this function has returned result records as PGresult, caller is
+ * responsible to release it, so caller should put codes which might throw
+ * exception in PG_TRY block. When an exception has been caught, release
+ * PGresult and re-throw the exception in PG_CATCH block.
+ */
+ static PGresult *
+ fetch_result(ForeignScanState *node)
+ {
+ PgsqlFdwExecutionState *festate;
+ List *fdw_private;
+ char *sql;
+ PGconn *conn;
+ PGresult *res;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+
+ /* Retrieve information for fetching result. */
+ fdw_private = festate->fdwplan->fdw_private;
+ sql = strVal(list_nth(fdw_private, FdwPrivateFetchSql));
+ conn = festate->conn;
+
+ /*
+ * Fetch result from remote server. In error case, we must release
+ * PGresult in this function to avoid memory leak because caller can't
+ * get the reference.
+ */
+ res = PQexec(conn, sql);
+ if (PQresultStatus(res) != PGRES_TUPLES_OK)
+ {
+ PQclear(res);
+ ereport(ERROR,
+ (errmsg("could not fetch rows from foreign server"),
+ errdetail("%s", PQerrorMessage(conn)),
+ errhint("%s", sql)));
+ }
+
+ return res;
+ }
+
+ /*
+ * Create tuples from PGresult and store them into tuplestore.
+ *
+ * Caller must use PG_TRY block to catch exception and release PGresult
+ * surely.
+ */
+ static void
+ store_result(ForeignScanState *node, PGresult *res)
+ {
+ int rows;
+ int row;
+ int i;
+ int nfields;
+ int attnum; /* number of non-dropped columns */
+ Form_pg_attribute *attrs;
+ TupleTableSlot *slot = node->ss.ss_ScanTupleSlot;
+ TupleDesc tupdesc = slot->tts_tupleDescriptor;
+ PgsqlFdwExecutionState *festate;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+ rows = PQntuples(res);
+ nfields = PQnfields(res);
+ attrs = tupdesc->attrs;
+
+ /* First, ensure that the tuplestore is empty. */
+ if (festate->tuples == NULL)
+ {
+ MemoryContext oldcontext = CurrentMemoryContext;
+
+ /*
+ * Create tuplestore to store result of the query in per-query context.
+ * Note that we use this memory context to avoid memory leak in error
+ * cases.
+ */
+ MemoryContextSwitchTo(festate->scan_cxt);
+ festate->tuples = tuplestore_begin_heap(false, false, work_mem);
+ MemoryContextSwitchTo(oldcontext);
+ }
+ else
+ {
+ /* We already have tuplestore, just need to clear contents of it. */
+ tuplestore_clear(festate->tuples);
+ }
+
+ /* 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)));
+
+ /* put a tuples into the slot */
+ for (row = 0; row < rows; row++)
+ {
+ int j;
+ HeapTuple tuple;
+
+ for (i = 0, j = 0; i < tupdesc->natts; i++)
+ {
+ /* skip dropped columns. */
+ if (attrs[i]->attisdropped)
+ {
+ festate->col_values[i] = NULL;
+ continue;
+ }
+
+ if (PQgetisnull(res, row, j))
+ festate->col_values[i] = NULL;
+ else
+ festate->col_values[i] = PQgetvalue(res, row, 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(festate->attinmeta, festate->col_values);
+ tuplestore_puttuple(festate->tuples, tuple);
+ }
+
+ tuplestore_donestoring(festate->tuples);
+ }
+
+ /*
+ * contain_ext_param
+ * Recursively search for Param nodes within a clause.
+ *
+ * Returns true if any parameter reference node with relkind PARAM_EXTERN
+ * found.
+ *
+ * This does not descend into subqueries, and so should be used only after
+ * reduction of sublinks to subplans, or in contexts where it's known there
+ * are no subqueries. There mustn't be outer-aggregate references either.
+ *
+ * XXX: These functions could be in core, src/backend/optimizer/util/clauses.c.
+ */
+ static bool
+ contain_ext_param(Node *clause)
+ {
+ return contain_ext_param_walker(clause, NULL);
+ }
+
+ static bool
+ contain_ext_param_walker(Node *node, void *context)
+ {
+ if (node == NULL)
+ return false;
+ if (IsA(node, Param))
+ {
+ Param *param = (Param *) node;
+
+ if (param->paramkind == PARAM_EXTERN)
+ return true; /* abort the tree traversal and return true */
+ }
+ return expression_tree_walker(node, contain_ext_param_walker, context);
+ }
diff --git a/contrib/pgsql_fdw/pgsql_fdw.control b/contrib/pgsql_fdw/pgsql_fdw.control
index ...0a9c8f4 .
*** a/contrib/pgsql_fdw/pgsql_fdw.control
--- b/contrib/pgsql_fdw/pgsql_fdw.control
***************
*** 0 ****
--- 1,5 ----
+ # pgsql_fdw extension
+ comment = 'foreign-data wrapper for remote PostgreSQL servers'
+ default_version = '1.0'
+ module_pathname = '$libdir/pgsql_fdw'
+ relocatable = true
diff --git a/contrib/pgsql_fdw/pgsql_fdw.h b/contrib/pgsql_fdw/pgsql_fdw.h
index ...f6394ce .
*** a/contrib/pgsql_fdw/pgsql_fdw.h
--- b/contrib/pgsql_fdw/pgsql_fdw.h
***************
*** 0 ****
--- 1,28 ----
+ /*-------------------------------------------------------------------------
+ *
+ * pgsql_fdw.h
+ * foreign-data wrapper for remote PostgreSQL servers.
+ *
+ * Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/pgsql_fdw.h
+ *
+ *-------------------------------------------------------------------------
+ */
+
+ #ifndef PGSQL_FDW_H
+ #define PGSQL_FDW_H
+
+ #include "postgres.h"
+ #include "nodes/relation.h"
+
+ /* in option.c */
+ int ExtractConnectionOptions(List *defelems,
+ const char **keywords,
+ const char **values);
+
+ /* in deparse.c */
+ char *deparseSql(Oid relid, PlannerInfo *root, RelOptInfo *baserel);
+
+ #endif /* PGSQL_FDW_H */
diff --git a/contrib/pgsql_fdw/sql/pgsql_fdw.sql b/contrib/pgsql_fdw/sql/pgsql_fdw.sql
index ...f257b34 .
*** a/contrib/pgsql_fdw/sql/pgsql_fdw.sql
--- b/contrib/pgsql_fdw/sql/pgsql_fdw.sql
***************
*** 0 ****
--- 1,232 ----
+ -- ===================================================================
+ -- create FDW objects
+ -- ===================================================================
+
+ -- Clean up in case a prior regression run failed
+
+ -- Suppress NOTICE messages when roles don't exist
+ SET client_min_messages TO 'error';
+
+ DROP ROLE IF EXISTS pgsql_fdw_user;
+
+ RESET client_min_messages;
+
+ CREATE ROLE pgsql_fdw_user LOGIN SUPERUSER;
+ SET SESSION AUTHORIZATION 'pgsql_fdw_user';
+
+ CREATE EXTENSION pgsql_fdw;
+
+ CREATE SERVER loopback1 FOREIGN DATA WRAPPER pgsql_fdw;
+ CREATE SERVER loopback2 FOREIGN DATA WRAPPER pgsql_fdw
+ OPTIONS (dbname 'contrib_regression');
+
+ CREATE USER MAPPING FOR public SERVER loopback1
+ OPTIONS (user 'value', password 'value');
+ CREATE USER MAPPING FOR pgsql_fdw_user SERVER loopback2;
+
+ CREATE FOREIGN TABLE ft1 (
+ c0 int,
+ c1 int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10)
+ ) SERVER loopback2;
+ ALTER FOREIGN TABLE ft1 DROP COLUMN c0;
+
+ CREATE FOREIGN TABLE ft2 (
+ c0 int,
+ c1 int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10)
+ ) SERVER loopback2;
+ ALTER FOREIGN TABLE ft2 DROP COLUMN c0;
+
+ -- ===================================================================
+ -- create objects used through FDW
+ -- ===================================================================
+ CREATE SCHEMA "S 1";
+ CREATE TABLE "S 1"."T 1" (
+ "C 1" int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10),
+ CONSTRAINT t1_pkey PRIMARY KEY ("C 1")
+ );
+ CREATE TABLE "S 1"."T 2" (
+ c1 int NOT NULL,
+ c2 text,
+ CONSTRAINT t2_pkey PRIMARY KEY (c1)
+ );
+
+ BEGIN;
+ TRUNCATE "S 1"."T 1";
+ INSERT INTO "S 1"."T 1"
+ SELECT id,
+ id % 10,
+ to_char(id, 'FM00000'),
+ '1970-01-01'::timestamptz + ((id % 100) || ' days')::interval,
+ '1970-01-01'::timestamp + ((id % 100) || ' days')::interval,
+ id % 10,
+ id % 10
+ FROM generate_series(1, 1000) id;
+ TRUNCATE "S 1"."T 2";
+ INSERT INTO "S 1"."T 2"
+ SELECT id,
+ 'AAA' || to_char(id, 'FM000')
+ FROM generate_series(1, 100) id;
+ COMMIT;
+
+ -- ===================================================================
+ -- tests for pgsql_fdw_validator
+ -- ===================================================================
+ ALTER FOREIGN DATA WRAPPER pgsql_fdw OPTIONS (host 'value'); -- ERROR
+ -- requiressl, krbsrvname and gsslib are omitted because they depend on
+ -- configure option
+ ALTER SERVER loopback1 OPTIONS (
+ authtype 'value',
+ service 'value',
+ connect_timeout 'value',
+ dbname 'value',
+ host 'value',
+ hostaddr 'value',
+ port 'value',
+ --client_encoding 'value',
+ tty 'value',
+ options 'value',
+ application_name 'value',
+ --fallback_application_name 'value',
+ keepalives 'value',
+ keepalives_idle 'value',
+ keepalives_interval 'value',
+ -- requiressl 'value',
+ sslcompression 'value',
+ sslmode 'value',
+ sslcert 'value',
+ sslkey 'value',
+ sslrootcert 'value',
+ sslcrl 'value'
+ --requirepeer 'value',
+ -- krbsrvname 'value',
+ -- gsslib 'value',
+ --replication 'value'
+ );
+ ALTER SERVER loopback1 OPTIONS (user 'value'); -- ERROR
+ ALTER SERVER loopback2 OPTIONS (ADD fetch_count '2');
+ ALTER USER MAPPING FOR public SERVER loopback1
+ OPTIONS (DROP user, DROP password);
+ ALTER USER MAPPING FOR public SERVER loopback1
+ OPTIONS (host 'value'); -- ERROR
+ ALTER FOREIGN TABLE ft1 OPTIONS (nspname 'S 1', relname 'T 1');
+ ALTER FOREIGN TABLE ft2 OPTIONS (nspname 'S 1', relname 'T 1', fetch_count '100');
+ ALTER FOREIGN TABLE ft1 OPTIONS (invalid 'value'); -- ERROR
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count 'a'); -- ERROR
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count '0'); -- ERROR
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count '-1'); -- ERROR
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (invalid 'value'); -- ERROR
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (colname 'C 1');
+ ALTER FOREIGN TABLE ft2 ALTER COLUMN c1 OPTIONS (colname 'C 1');
+ \dew+
+ \des+
+ \deu+
+ \det+
+
+ -- ===================================================================
+ -- simple queries
+ -- ===================================================================
+ -- single table, with/without alias
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
+ SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+ SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+ -- with WHERE clause
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
+ SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
+ -- 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
+ SELECT * FROM ft1 t1 WHERE t1.c3 = (SELECT MAX(c3) FROM ft2 t2) ORDER BY c1;
+ -- used in CTE
+ WITH t1 AS (SELECT * FROM ft1 WHERE c1 <= 10) SELECT t2.c1, t2.c2, t2.c3, t2.c4 FROM t1, ft2 t2 WHERE t1.c1 = t2.c1 ORDER BY t1.c1;
+ -- fixed values
+ SELECT 'fixed', NULL FROM ft1 t1 WHERE c1 = 1;
+ -- user-defined operator/function
+ CREATE FUNCTION pgsql_fdw_abs(int) RETURNS int AS $$
+ BEGIN
+ RETURN abs($1);
+ END
+ $$ LANGUAGE plpgsql IMMUTABLE;
+ CREATE OPERATOR === (
+ LEFTARG = int,
+ RIGHTARG = int,
+ PROCEDURE = int4eq,
+ COMMUTATOR = ===,
+ NEGATOR = !==
+ );
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = pgsql_fdw_abs(t1.c2);
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 === t1.c2;
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = abs(t1.c2);
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = t1.c2;
+
+ -- ===================================================================
+ -- parameterized queries
+ -- ===================================================================
+ -- simple join
+ PREPARE st1(int, int) AS SELECT t1.c3, t2.c3 FROM ft1 t1, ft2 t2 WHERE t1.c1 = $1 AND t2.c1 = $2;
+ EXPLAIN (COSTS false) EXECUTE st1(1, 2);
+ EXECUTE st1(1, 1);
+ EXECUTE st1(101, 101);
+ -- subquery using stable function (can't be pushed down)
+ PREPARE st2(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c4) = 6) ORDER BY c1;
+ EXPLAIN (COSTS false) EXECUTE st2(10, 20);
+ EXECUTE st2(10, 20);
+ EXECUTE st1(101, 101);
+ -- subquery using immutable function (can be pushed down)
+ PREPARE st3(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c5) = 6) ORDER BY c1;
+ EXPLAIN (COSTS false) EXECUTE st3(10, 20);
+ EXECUTE st3(10, 20);
+ EXECUTE st3(20, 30);
+ -- custom plan should be chosen
+ PREPARE st4(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 = $1;
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ -- cleanup
+ DEALLOCATE st1;
+ DEALLOCATE st2;
+ DEALLOCATE st3;
+ DEALLOCATE st4;
+
+ -- ===================================================================
+ -- connection management
+ -- ===================================================================
+ SELECT srvname, usename FROM pgsql_fdw_connections;
+ SELECT pgsql_fdw_disconnect(srvid, usesysid) FROM pgsql_fdw_get_connections();
+ SELECT srvname, usename FROM pgsql_fdw_connections;
+
+ -- ===================================================================
+ -- cleanup
+ -- ===================================================================
+ DROP OPERATOR === (int, int) CASCADE;
+ DROP OPERATOR !== (int, int) CASCADE;
+ DROP FUNCTION pgsql_fdw_abs(int);
+ DROP SCHEMA "S 1" CASCADE;
+ DROP EXTENSION pgsql_fdw CASCADE;
+ \c
+ DROP ROLE pgsql_fdw_user;
diff --git a/doc/src/sgml/contrib.sgml b/doc/src/sgml/contrib.sgml
index d4da4ee..32056d1 100644
*** a/doc/src/sgml/contrib.sgml
--- b/doc/src/sgml/contrib.sgml
*************** CREATE EXTENSION <replaceable>module_nam
*** 117,122 ****
--- 117,123 ----
&pgcrypto;
&pgfreespacemap;
&pgrowlocks;
+ &pgsql-fdw;
&pgstandby;
&pgstatstatements;
&pgstattuple;
diff --git a/doc/src/sgml/filelist.sgml b/doc/src/sgml/filelist.sgml
index b5d3c6d..de686ee 100644
*** a/doc/src/sgml/filelist.sgml
--- b/doc/src/sgml/filelist.sgml
***************
*** 125,130 ****
--- 125,131 ----
<!ENTITY pgcrypto SYSTEM "pgcrypto.sgml">
<!ENTITY pgfreespacemap SYSTEM "pgfreespacemap.sgml">
<!ENTITY pgrowlocks SYSTEM "pgrowlocks.sgml">
+ <!ENTITY pgsql-fdw SYSTEM "pgsql-fdw.sgml">
<!ENTITY pgstandby SYSTEM "pgstandby.sgml">
<!ENTITY pgstatstatements SYSTEM "pgstatstatements.sgml">
<!ENTITY pgstattuple SYSTEM "pgstattuple.sgml">
diff --git a/doc/src/sgml/pgsql-fdw.sgml b/doc/src/sgml/pgsql-fdw.sgml
index ...30fa7f5 .
*** a/doc/src/sgml/pgsql-fdw.sgml
--- b/doc/src/sgml/pgsql-fdw.sgml
***************
*** 0 ****
--- 1,261 ----
+ <!-- doc/src/sgml/pgsql-fdw.sgml -->
+
+ <sect1 id="pgsql-fdw" xreflabel="pgsql_fdw">
+ <title>pgsql_fdw</title>
+
+ <indexterm zone="pgsql-fdw">
+ <primary>pgsql_fdw</primary>
+ </indexterm>
+
+ <para>
+ The <filename>pgsql_fdw</filename> module provides a foreign-data wrapper for
+ external <productname>PostgreSQL</productname> servers.
+ With this module, users can access data stored in external
+ <productname>PostgreSQL</productname> via plain SQL statements.
+ </para>
+
+ <para>
+ Note that default wrapper <literal>pgsql_fdw</literal> is created
+ automatically during <command>CREATE EXTENSION</command> command for
+ <application>pgsql_fdw</application>.
+ </para>
+
+ <sect2>
+ <title>FDW Options of pgsql_fdw</title>
+
+ <sect3>
+ <title>Connection Options</title>
+ <para>
+ A foreign server and user mapping created using this wrapper can have
+ <application>libpq</> connection options, expect below:
+
+ <itemizedlist>
+ <listitem><para>client_encoding</para></listitem>
+ <listitem><para>fallback_application_name</para></listitem>
+ <listitem><para>replication</para></listitem>
+ </itemizedlist>
+
+ For details of <application>libpq</> connection options, see
+ <xref linkend="libpq-connect">.
+ </para>
+
+ <para>
+ <literal>user</literal> and <literal>password</literal> can be
+ specified on user mappings, and others can be specified on foreign servers.
+ </para>
+ </sect3>
+
+ <sect3>
+ <title>Object Name Options</title>
+ <para>
+ Foreign tables which were created using this wrapper, or its columns can
+ have object name options. These options can be used to specify the names
+ used in SQL statement sent to remote <productname>PostgreSQL</productname>
+ server. These options are useful when a remote object has different name
+ from corresponding local one.
+ </para>
+
+ <variablelist>
+
+ <varlistentry>
+ <term><literal>nspname</literal></term>
+ <listitem>
+ <para>
+ This option, which can be specified on a foreign table, is used as a
+ namespace (schema) reference in the SQL statement. If this options is
+ omitted, <literal>pg_class.nspname</literal> of the foreign table is
+ used.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>relname</literal></term>
+ <listitem>
+ <para>
+ This option, which can be specified on a foreign table, is used as a
+ relation (table) reference in the SQL statement. If this options is
+ omitted, <literal>pg_class.relname</literal> of the foreign table is
+ used.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>colname</literal></term>
+ <listitem>
+ <para>
+ This option, which can be specified on a column of a foreign table, is
+ used as a column (attribute) reference in the SQL statement. If this
+ option is omitted, <literal>pg_attribute.attname</literal> of the column
+ of the foreign table is used.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ </variablelist>
+
+ </sect3>
+
+ <sect3>
+ <title>Cursor Options</title>
+ <para>
+ The <application>pgsql_fdw</application> always uses cursor to retrieve the
+ result from external server. Users can control the behavior of cursor by
+ setting cursor options to foreign table or foreign server. If an option
+ is set to both objects, finer-grained setting is used. In other words,
+ foreign table's setting overrides foreign server's setting.
+ </para>
+
+ <variablelist>
+
+ <varlistentry>
+ <term><literal>fetch_count</literal></term>
+ <listitem>
+ <para>
+ This option specifies the number of rows to be fetched at a time.
+ This option accepts only integer value larger than zero. The default
+ setting is 10000.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ </variablelist>
+
+ </sect3>
+
+ </sect2>
+
+ <sect2>
+ <title>Connection Management</title>
+
+ <para>
+ The <application>pgsql_fdw</application> establishes a connection to a
+ foreign server in the beginning of the first query which uses a foreign
+ table associated to the foreign server, and reuses the connection following
+ queries and even in following foreign scans in same query.
+
+ You can see the list of active connections via
+ <structname>pgsql_fdw_connections</structname> view. It shows pair of oid
+ and name of server and local role for each active connections established by
+ <application>pgsql_fdw</application>. For security reason, only superuser
+ can see other role's connections.
+ </para>
+
+ <para>
+ Established connections are kept alive until local role changes or the
+ current transaction aborts or user requests so.
+ </para>
+
+ <para>
+ If role has been changed, active connections established as old local role
+ is kept alive but never be reused until local role has restored to original
+ role. This kind of situation happens with <command>SET ROLE</command> and
+ <command>SET SESSION AUTHORIZATION</command>.
+ </para>
+
+ <para>
+ If current transaction aborts by error or user request, all active
+ connections are disconnected automatically. This behavior avoids possible
+ connection leaks on error.
+ </para>
+
+ <para>
+ You can discard persistent connection at arbitrary timing with
+ <function>pgsql_fdw_disconnect()</function>. It takes server oid and
+ user oid as arguments. This function can handle only connections
+ established in current session; connections established by other backends
+ are not reachable.
+ </para>
+
+ <para>
+ You can discard all active and visible connections in current session with
+ using <structname>pgsql_fdw_connections</structname> and
+ <function>pgsql_fdw_disconnect()</function> together:
+ <synopsis>
+ postgres=# SELECT pgsql_fdw_disconnect(srvid, usesysid) FROM pgsql_fdw_connections;
+ pgsql_fdw_disconnect
+ ----------------------
+ OK
+ OK
+ (2 rows)
+ </synopsis>
+ </para>
+ </sect2>
+
+ <sect2>
+ <title>Transaction Management</title>
+ <para>
+ The <application>pgsql_fdw</application> begins remote transaction at the
+ beginning of a local query, and terminates it with <command>ABORT</command>
+ at the end of the local query. This means that all foreign scans on a
+ foreign server in a local query are executed in one transaction.
+ If isolation level of local transaction is <literal>SERIALIZABLE</literal>,
+ <literal>SERIALIZABLE</literal> is used for remote transaction. Otherwise,
+ if isolation level of local transaction is one of
+ <literal>READ UNCOMMITTED</literal>, <literal>READ COMMITTED</literal> or
+ <literal>REPEATABLE READ</literal>, then <literal>REPEATABLE READ</literal>
+ is used for remote transaction.
+ <literal>READ UNCOMMITTED</literal> and <literal>READ COMMITTED</literal>
+ are never used for remote transaction, because even
+ <literal>READ COMMITTED</literal> transaction might produce inconsistent
+ results, if remote data have been updated between two remote queries.
+ </para>
+ <para>
+ Note that even if the isolation level of local transaction was
+ <literal>SERIALIZABLE</literal> or <literal>REPEATABLE READ</literal>,
+ series of one query might produce different result, because foreign scans
+ in different local queries are executed in different remote transactions.
+ For instance, when client started a local transaction
+ explicitly with isolation level <literal>SERIALIZABLE</literal>, and
+ executed same local query which contains a foreign table which references
+ foreign data which is updated frequently, latter result would be different
+ from former result.
+ </para>
+ <para>
+ This restriction might be relaxed in future release.
+ </para>
+ </sect2>
+
+ <sect2>
+ <title>Estimation of Costs and Rows</title>
+ <para>
+ The <application>pgsql_fdw</application> estimates the costs of a foreign
+ scan by adding up some basic costs: connection costs, remote query costs
+ and data transfer costs.
+ To get remote query costs, <application>pgsql_fdw</application> executes
+ <command>EXPLAIN</command> command on remote server for each foreign scan.
+ </para>
+ <para>
+ On the other hand, estimated rows which was returned by
+ <command>EXPLAIN</command> is used for local estimation as-is.
+ </para>
+ </sect2>
+
+ <sect2>
+ <title>EXPLAIN Output</title>
+ <para>
+ For a foreign table using <literal>pgsql_fdw</>, <command>EXPLAIN</> shows
+ a remote SQL statement which is sent to remote
+ <productname>PostgreSQL</productname> server for a ForeignScan plan node.
+ For example:
+ </para>
+ <synopsis>
+ postgres=# EXPLAIN SELECT aid FROM pgbench_accounts WHERE abalance < 0;
+ QUERY PLAN
+ --------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on pgbench_accounts (cost=100.00..8105.13 rows=302613 width=8)
+ Filter: (abalance < 0)
+ Remote SQL: DECLARE pgsql_fdw_cursor_3 SCROLL CURSOR FOR SELECT aid, NULL, abalance, NULL FROM public.pgbench_accounts
+ (3 rows)
+ </synopsis>
+ </sect2>
+
+ <sect2>
+ <title>Author</title>
+ <para>
+ Shigeru Hanada <email>shigeru.hanada@gmail.com</email>
+ </para>
+ </sect2>
+
+ </sect1>
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 64ba8ec..94b2ea9 100644
*** a/src/backend/utils/adt/ruleutils.c
--- b/src/backend/utils/adt/ruleutils.c
*************** deparse_context_for(const char *aliasnam
*** 2181,2186 ****
--- 2181,2209 ----
return list_make1(dpns);
}
+ /* ----------
+ * deparse_context_for_rtelist - Build deparse context for a list of RTEs
+ *
+ * Given the list of RangeTableEnetry, build deparsing context for an
+ * expression referencing those relations. This is sufficient for uses of
+ * deparse_expression before plan has been created.
+ * ----------
+ */
+ List *
+ deparse_context_for_rtelist(List *rtable)
+ {
+ deparse_namespace *dpns;
+
+ dpns = (deparse_namespace *) palloc0(sizeof(deparse_namespace));
+
+ /* Build a minimal RTE for the rel */
+ dpns->rtable = rtable;
+ dpns->ctes = NIL;
+
+ /* Return a one-deep namespace stack */
+ return list_make1(dpns);
+ }
+
/*
* deparse_context_for_planstate - Build deparse context for a plan
*
diff --git a/src/include/utils/builtins.h b/src/include/utils/builtins.h
index fe253bc..8ad4d48 100644
*** a/src/include/utils/builtins.h
--- b/src/include/utils/builtins.h
*************** extern char *deparse_expression(Node *ex
*** 649,654 ****
--- 649,655 ----
extern List *deparse_context_for(const char *aliasname, Oid relid);
extern List *deparse_context_for_planstate(Node *planstate, List *ancestors,
List *rtable);
+ extern List *deparse_context_for_rtelist(List *rtable);
extern const char *quote_identifier(const char *ident);
extern char *quote_qualified_identifier(const char *qualifier,
const char *ident);
Could we name this "postgresql_fdw" instead? We already have several
${productname}_fdw out there, and I don't want to get in the business of
having to guess variant spellings.
(2012/02/25 7:31), Peter Eisentraut wrote:
Could we name this "postgresql_fdw" instead? We already have several
${productname}_fdw out there, and I don't want to get in the business of
having to guess variant spellings.
I worry name conflict with existing postgresql_fdw_validator, which is
implemented in backend binary and used by contrib/dblink. I thought
that we should use another name for PostgreSQL FDW unless we can change
specification of dblink connection string.
--
Shigeru Hanada
On Fri, Feb 24, 2012 at 5:31 PM, Peter Eisentraut <peter_e@gmx.net> wrote:
Could we name this "postgresql_fdw" instead? We already have several
${productname}_fdw out there, and I don't want to get in the business of
having to guess variant spellings.
If you don't like variant spellings, having anything to do with
PostgreSQL, aka Postgres, and usually discussed on the pgsql-* mailing
lists, is probably a bad idea.
Go Postgre!
--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
Robert Haas <robertmhaas@gmail.com> writes:
On Fri, Feb 24, 2012 at 5:31 PM, Peter Eisentraut <peter_e@gmx.net> wrote:
Could we name this "postgresql_fdw" instead? �We already have several
${productname}_fdw out there, and I don't want to get in the business of
having to guess variant spellings.
If you don't like variant spellings, having anything to do with
PostgreSQL, aka Postgres, and usually discussed on the pgsql-* mailing
lists, is probably a bad idea.
[ snicker ] But still, Peter has a point: pgsql is not a name for the
product, it's at best an abbreviation. We aren't calling the other
thing orcl_fdw or ora_fdw.
I think either postgres_fdw or postgresql_fdw would be fine.
regards, tom lane
On Tue, Feb 28, 2012 at 11:02 AM, Tom Lane <tgl@sss.pgh.pa.us> wrote:
Robert Haas <robertmhaas@gmail.com> writes:
On Fri, Feb 24, 2012 at 5:31 PM, Peter Eisentraut <peter_e@gmx.net> wrote:
Could we name this "postgresql_fdw" instead? We already have several
${productname}_fdw out there, and I don't want to get in the business of
having to guess variant spellings.If you don't like variant spellings, having anything to do with
PostgreSQL, aka Postgres, and usually discussed on the pgsql-* mailing
lists, is probably a bad idea.[ snicker ] But still, Peter has a point: pgsql is not a name for the
product, it's at best an abbreviation. We aren't calling the other
thing orcl_fdw or ora_fdw.I think either postgres_fdw or postgresql_fdw would be fine.
I liked the shorter name, myself, but I'm not going to make a big deal about it.
--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
On tis, 2012-02-28 at 11:20 -0500, Robert Haas wrote:
[ snicker ] But still, Peter has a point: pgsql is not a name for
the
product, it's at best an abbreviation. We aren't calling the other
thing orcl_fdw or ora_fdw.I think either postgres_fdw or postgresql_fdw would be fine.
I liked the shorter name, myself, but I'm not going to make a big deal
about it.
Let's at least be clear about the reasons here. The fact that
postgresql_fdw_validator exists means (a) there is a possible naming
conflict that has not been discussed yet, and/or (b) the name is already
settled and we need to think of a way to make postgresql_fdw_validator
work with the new actual FDW.
On Feb 28, 2012, at 8:20 AM, Robert Haas wrote:
I liked the shorter name, myself, but I'm not going to make a big deal about it.
pg_ is used quite a bit. what about pg_fdw?
David
(2012/02/29 4:07), Peter Eisentraut wrote:
Let's at least be clear about the reasons here. The fact that
postgresql_fdw_validator exists means (a) there is a possible naming
conflict that has not been discussed yet, and/or (b) the name is already
settled and we need to think of a way to make postgresql_fdw_validator
work with the new actual FDW.
We can avoid conflict of name by using postgres_fdw or pgsql_fdw, but it
doesn't solve fundamental issue. ISTM that maintaining two similar
validators is wasteful and confusing, and FDW for PostgreSQL should be
just one, at least in the context of core distribution.
Current pgsql_fdw_validator accepts every FDW options which is accepted
by postgresql_fdw_validator, and additionally accepts FDW specific
options such as fetch_count. So, if dblink can ignore unknown FDW
options, pgsql_fdw_validator can be used to create foreign servers for
dblink connection.
How about removing postgresql_fdw_validator from backend binary, and
changing dblink to use contrib/postgresql_fdw's validator? It breaks
some backward compatibility and requires contrib/postgresql_fdw to be
installed before using contrib/dblink with foreign servers, but ISTM
that it doesn't become so serious.
Of course dblink is still available by itself if user specifies
connection information with "key = value" string, not with server name.
One concern is how to avoid duplicated list of valid libpq options.
Adding new libpq function, like below, which returns 1 when given name
is a valid libpq option would help.
int PQisValidOption(const char *keyword);
--
Shigeru Hanada
2012/2/29 Shigeru Hanada <shigeru.hanada@gmail.com>:
(2012/02/29 4:07), Peter Eisentraut wrote:
Let's at least be clear about the reasons here. The fact that
postgresql_fdw_validator exists means (a) there is a possible naming
conflict that has not been discussed yet, and/or (b) the name is already
settled and we need to think of a way to make postgresql_fdw_validator
work with the new actual FDW.We can avoid conflict of name by using postgres_fdw or pgsql_fdw, but it
doesn't solve fundamental issue. ISTM that maintaining two similar
validators is wasteful and confusing, and FDW for PostgreSQL should be
just one, at least in the context of core distribution.Current pgsql_fdw_validator accepts every FDW options which is accepted
by postgresql_fdw_validator, and additionally accepts FDW specific
options such as fetch_count. So, if dblink can ignore unknown FDW
options, pgsql_fdw_validator can be used to create foreign servers for
dblink connection.How about removing postgresql_fdw_validator from backend binary, and
changing dblink to use contrib/postgresql_fdw's validator? It breaks
some backward compatibility and requires contrib/postgresql_fdw to be
installed before using contrib/dblink with foreign servers, but ISTM
that it doesn't become so serious.
+1
pavel stehule
Show quoted text
Of course dblink is still available by itself if user specifies
connection information with "key = value" string, not with server name.One concern is how to avoid duplicated list of valid libpq options.
Adding new libpq function, like below, which returns 1 when given name
is a valid libpq option would help.int PQisValidOption(const char *keyword);
--
Shigeru Hanada--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Shigeru Hanada <shigeru.hanada@gmail.com> writes:
How about removing postgresql_fdw_validator from backend binary, and
changing dblink to use contrib/postgresql_fdw's validator? It breaks
some backward compatibility and requires contrib/postgresql_fdw to be
installed before using contrib/dblink with foreign servers, but ISTM
that it doesn't become so serious.
I don't think that creating such a dependency is acceptable.
Even if we didn't mind the dependency, you said yourself that
contrib/postgresql_fdw's validator will accept stuff that's not
appropriate for dblink.
If we don't think postgresql_fdw_validator belongs in core after all,
we should just move it to dblink.
regards, tom lane
(2012/03/01 0:33), Tom Lane wrote:
I don't think that creating such a dependency is acceptable.
Even if we didn't mind the dependency, you said yourself that
contrib/postgresql_fdw's validator will accept stuff that's not
appropriate for dblink.
Agreed. I think that these two contrib modules (and all FDW modules)
should have individual validator for each to avoid undesirable
dependency and naming conflict, and such validator function should be
inside each module, but not in core.
How about moving postgresql_fdw_validator into dblink, with renaming to
dblink_fdw_validator? Attached patch achieves such changes. I've left
postgresql_fdw_validator" in foreign_data regression test section, so
that foreign_data section can still check whether FDW DDLs invoke
validator function. I used the name "postgresql_fdw_validator" for test
validator to make change as little as possible.
This change requires dblink to have new function, so its version should
be bumped to 1.1.
These changes have no direct relation to PostgreSQL FDW, so this patch
can be applied by itself. If this patch has been applied, I'll rename
pgsql_fdw to postgresql_fdw which contains product name fully spelled out.
--
Shigeru Hanada
Attachments:
move_validator.patchtext/plain; name=move_validator.patchDownload
diff --git a/contrib/dblink/Makefile b/contrib/dblink/Makefile
index ac63748..a27db88 100644
*** a/contrib/dblink/Makefile
--- b/contrib/dblink/Makefile
*************** SHLIB_LINK = $(libpq)
*** 7,13 ****
SHLIB_PREREQS = submake-libpq
EXTENSION = dblink
! DATA = dblink--1.0.sql dblink--unpackaged--1.0.sql
REGRESS = dblink
--- 7,13 ----
SHLIB_PREREQS = submake-libpq
EXTENSION = dblink
! DATA = dblink--1.1.sql dblink--1.0--1.1.sql dblink--unpackaged--1.0.sql
REGRESS = dblink
diff --git a/contrib/dblink/dblink--1.0--1.1.sql b/contrib/dblink/dblink--1.0--1.1.sql
index ...3dd02a0 .
*** a/contrib/dblink/dblink--1.0--1.1.sql
--- b/contrib/dblink/dblink--1.0--1.1.sql
***************
*** 0 ****
--- 1,17 ----
+ /* contrib/dblink/dblink--1.0--1.1.sql */
+
+ -- complain if script is sourced in psql, rather than via ALTER EXTENSION
+ \echo Use "ALTER EXTENSION dblink UPDATE" to load this file. \quit
+
+ /* First we have to create validator function */
+ CREATE FUNCTION dblink_fdw_validator(
+ options text[],
+ catalog oid
+ )
+ RETURNS boolean
+ AS 'MODULE_PATHNAME', 'dblink_fdw_validator'
+ LANGUAGE C STRICT;
+
+ /* Then we add the validator */
+ ALTER EXTENSION dblink ADD FUNCTION dblink_fdw_validator(text[], oid);
+
diff --git a/contrib/dblink/dblink--1.1.sql b/contrib/dblink/dblink--1.1.sql
index ...ec02498 .
*** a/contrib/dblink/dblink--1.1.sql
--- b/contrib/dblink/dblink--1.1.sql
***************
*** 0 ****
--- 1,231 ----
+ /* contrib/dblink/dblink--1.1.sql */
+
+ -- complain if script is sourced in psql, rather than via CREATE EXTENSION
+ \echo Use "CREATE EXTENSION dblink" to load this file. \quit
+
+ -- dblink_connect now restricts non-superusers to password
+ -- authenticated connections
+ CREATE FUNCTION dblink_connect (text)
+ RETURNS text
+ AS 'MODULE_PATHNAME','dblink_connect'
+ LANGUAGE C STRICT;
+
+ CREATE FUNCTION dblink_connect (text, text)
+ RETURNS text
+ AS 'MODULE_PATHNAME','dblink_connect'
+ LANGUAGE C STRICT;
+
+ -- dblink_connect_u allows non-superusers to use
+ -- non-password authenticated connections, but initially
+ -- privileges are revoked from public
+ CREATE FUNCTION dblink_connect_u (text)
+ RETURNS text
+ AS 'MODULE_PATHNAME','dblink_connect'
+ LANGUAGE C STRICT SECURITY DEFINER;
+
+ CREATE FUNCTION dblink_connect_u (text, text)
+ RETURNS text
+ AS 'MODULE_PATHNAME','dblink_connect'
+ LANGUAGE C STRICT SECURITY DEFINER;
+
+ REVOKE ALL ON FUNCTION dblink_connect_u (text) FROM public;
+ REVOKE ALL ON FUNCTION dblink_connect_u (text, text) FROM public;
+
+ CREATE FUNCTION dblink_disconnect ()
+ RETURNS text
+ AS 'MODULE_PATHNAME','dblink_disconnect'
+ LANGUAGE C STRICT;
+
+ CREATE FUNCTION dblink_disconnect (text)
+ RETURNS text
+ AS 'MODULE_PATHNAME','dblink_disconnect'
+ LANGUAGE C STRICT;
+
+ CREATE FUNCTION dblink_open (text, text)
+ RETURNS text
+ AS 'MODULE_PATHNAME','dblink_open'
+ LANGUAGE C STRICT;
+
+ CREATE FUNCTION dblink_open (text, text, boolean)
+ RETURNS text
+ AS 'MODULE_PATHNAME','dblink_open'
+ LANGUAGE C STRICT;
+
+ CREATE FUNCTION dblink_open (text, text, text)
+ RETURNS text
+ AS 'MODULE_PATHNAME','dblink_open'
+ LANGUAGE C STRICT;
+
+ CREATE FUNCTION dblink_open (text, text, text, boolean)
+ RETURNS text
+ AS 'MODULE_PATHNAME','dblink_open'
+ LANGUAGE C STRICT;
+
+ CREATE FUNCTION dblink_fetch (text, int)
+ RETURNS setof record
+ AS 'MODULE_PATHNAME','dblink_fetch'
+ LANGUAGE C STRICT;
+
+ CREATE FUNCTION dblink_fetch (text, int, boolean)
+ RETURNS setof record
+ AS 'MODULE_PATHNAME','dblink_fetch'
+ LANGUAGE C STRICT;
+
+ CREATE FUNCTION dblink_fetch (text, text, int)
+ RETURNS setof record
+ AS 'MODULE_PATHNAME','dblink_fetch'
+ LANGUAGE C STRICT;
+
+ CREATE FUNCTION dblink_fetch (text, text, int, boolean)
+ RETURNS setof record
+ AS 'MODULE_PATHNAME','dblink_fetch'
+ LANGUAGE C STRICT;
+
+ CREATE FUNCTION dblink_close (text)
+ RETURNS text
+ AS 'MODULE_PATHNAME','dblink_close'
+ LANGUAGE C STRICT;
+
+ CREATE FUNCTION dblink_close (text, boolean)
+ RETURNS text
+ AS 'MODULE_PATHNAME','dblink_close'
+ LANGUAGE C STRICT;
+
+ CREATE FUNCTION dblink_close (text, text)
+ RETURNS text
+ AS 'MODULE_PATHNAME','dblink_close'
+ LANGUAGE C STRICT;
+
+ CREATE FUNCTION dblink_close (text, text, boolean)
+ RETURNS text
+ AS 'MODULE_PATHNAME','dblink_close'
+ LANGUAGE C STRICT;
+
+ CREATE FUNCTION dblink (text, text)
+ RETURNS setof record
+ AS 'MODULE_PATHNAME','dblink_record'
+ LANGUAGE C STRICT;
+
+ CREATE FUNCTION dblink (text, text, boolean)
+ RETURNS setof record
+ AS 'MODULE_PATHNAME','dblink_record'
+ LANGUAGE C STRICT;
+
+ CREATE FUNCTION dblink (text)
+ RETURNS setof record
+ AS 'MODULE_PATHNAME','dblink_record'
+ LANGUAGE C STRICT;
+
+ CREATE FUNCTION dblink (text, boolean)
+ RETURNS setof record
+ AS 'MODULE_PATHNAME','dblink_record'
+ LANGUAGE C STRICT;
+
+ CREATE FUNCTION dblink_exec (text, text)
+ RETURNS text
+ AS 'MODULE_PATHNAME','dblink_exec'
+ LANGUAGE C STRICT;
+
+ CREATE FUNCTION dblink_exec (text, text, boolean)
+ RETURNS text
+ AS 'MODULE_PATHNAME','dblink_exec'
+ LANGUAGE C STRICT;
+
+ CREATE FUNCTION dblink_exec (text)
+ RETURNS text
+ AS 'MODULE_PATHNAME','dblink_exec'
+ LANGUAGE C STRICT;
+
+ CREATE FUNCTION dblink_exec (text,boolean)
+ RETURNS text
+ AS 'MODULE_PATHNAME','dblink_exec'
+ LANGUAGE C STRICT;
+
+ CREATE TYPE dblink_pkey_results AS (position int, colname text);
+
+ CREATE FUNCTION dblink_get_pkey (text)
+ RETURNS setof dblink_pkey_results
+ AS 'MODULE_PATHNAME','dblink_get_pkey'
+ LANGUAGE C STRICT;
+
+ CREATE FUNCTION dblink_build_sql_insert (text, int2vector, int, _text, _text)
+ RETURNS text
+ AS 'MODULE_PATHNAME','dblink_build_sql_insert'
+ LANGUAGE C STRICT;
+
+ CREATE FUNCTION dblink_build_sql_delete (text, int2vector, int, _text)
+ RETURNS text
+ AS 'MODULE_PATHNAME','dblink_build_sql_delete'
+ LANGUAGE C STRICT;
+
+ CREATE FUNCTION dblink_build_sql_update (text, int2vector, int, _text, _text)
+ RETURNS text
+ AS 'MODULE_PATHNAME','dblink_build_sql_update'
+ LANGUAGE C STRICT;
+
+ CREATE FUNCTION dblink_current_query ()
+ RETURNS text
+ AS 'MODULE_PATHNAME','dblink_current_query'
+ LANGUAGE C;
+
+ CREATE FUNCTION dblink_send_query(text, text)
+ RETURNS int4
+ AS 'MODULE_PATHNAME', 'dblink_send_query'
+ LANGUAGE C STRICT;
+
+ CREATE FUNCTION dblink_is_busy(text)
+ RETURNS int4
+ AS 'MODULE_PATHNAME', 'dblink_is_busy'
+ LANGUAGE C STRICT;
+
+ CREATE FUNCTION dblink_get_result(text)
+ RETURNS SETOF record
+ AS 'MODULE_PATHNAME', 'dblink_get_result'
+ LANGUAGE C STRICT;
+
+ CREATE FUNCTION dblink_get_result(text, bool)
+ RETURNS SETOF record
+ AS 'MODULE_PATHNAME', 'dblink_get_result'
+ LANGUAGE C STRICT;
+
+ CREATE FUNCTION dblink_get_connections()
+ RETURNS text[]
+ AS 'MODULE_PATHNAME', 'dblink_get_connections'
+ LANGUAGE C;
+
+ CREATE FUNCTION dblink_cancel_query(text)
+ RETURNS text
+ AS 'MODULE_PATHNAME', 'dblink_cancel_query'
+ LANGUAGE C STRICT;
+
+ CREATE FUNCTION dblink_error_message(text)
+ RETURNS text
+ AS 'MODULE_PATHNAME', 'dblink_error_message'
+ LANGUAGE C STRICT;
+
+ CREATE FUNCTION dblink_get_notify(
+ OUT notify_name TEXT,
+ OUT be_pid INT4,
+ OUT extra TEXT
+ )
+ RETURNS setof record
+ AS 'MODULE_PATHNAME', 'dblink_get_notify'
+ LANGUAGE C STRICT;
+
+ CREATE FUNCTION dblink_get_notify(
+ conname TEXT,
+ OUT notify_name TEXT,
+ OUT be_pid INT4,
+ OUT extra TEXT
+ )
+ RETURNS setof record
+ AS 'MODULE_PATHNAME', 'dblink_get_notify'
+ LANGUAGE C STRICT;
+
+ CREATE FUNCTION dblink_fdw_validator(
+ options text[],
+ catalog oid
+ )
+ RETURNS boolean
+ AS 'MODULE_PATHNAME', 'dblink_fdw_validator'
+ LANGUAGE C STRICT;
diff --git a/contrib/dblink/dblink.c b/contrib/dblink/dblink.c
index 36a8e3e..1687c53 100644
*** a/contrib/dblink/dblink.c
--- b/contrib/dblink/dblink.c
***************
*** 36,43 ****
--- 36,46 ----
#include "libpq-fe.h"
#include "funcapi.h"
+ #include "access/reloptions.h"
#include "catalog/indexing.h"
#include "catalog/namespace.h"
+ #include "catalog/pg_foreign_server.h"
+ #include "catalog/pg_user_mapping.h"
#include "catalog/pg_type.h"
#include "executor/spi.h"
#include "foreign/foreign.h"
*************** validate_pkattnums(Relation rel,
*** 2411,2413 ****
--- 2414,2516 ----
errmsg("invalid attribute number %d", pkattnum)));
}
}
+
+
+ /*
+ * Describes the valid options for postgresql FDW, server, and user mapping.
+ */
+ struct ConnectionOption
+ {
+ const char *optname;
+ Oid optcontext; /* Oid of catalog in which option may appear */
+ };
+
+ /*
+ * Copied from fe-connect.c PQconninfoOptions.
+ *
+ * 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}
+ };
+
+
+ /*
+ * Check if the provided option is one of libpq conninfo 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)
+ {
+ struct ConnectionOption *opt;
+
+ for (opt = libpq_conninfo_options; opt->optname; opt++)
+ if (context == opt->optcontext && strcmp(opt->optname, option) == 0)
+ return true;
+ return false;
+ }
+
+
+ /*
+ * Validate the generic option given to SERVER or USER MAPPING.
+ * Raise an ERROR if the option or its value is considered
+ * invalid.
+ *
+ * Valid server options are all libpq conninfo options except
+ * user and password -- these may only appear in USER MAPPING options.
+ */
+ PG_FUNCTION_INFO_V1(dblink_fdw_validator);
+ Datum
+ dblink_fdw_validator(PG_FUNCTION_ARGS)
+ {
+ List *options_list = untransformRelOptions(PG_GETARG_DATUM(0));
+ Oid catalog = PG_GETARG_OID(1);
+
+ ListCell *cell;
+
+ foreach(cell, options_list)
+ {
+ DefElem *def = lfirst(cell);
+
+ if (!is_conninfo_option(def->defname, catalog))
+ {
+ struct ConnectionOption *opt;
+ StringInfoData buf;
+
+ /*
+ * Unknown option specified, complain about it. Provide a hint
+ * with list of valid options for the object.
+ */
+ initStringInfo(&buf);
+ for (opt = libpq_conninfo_options; opt->optname; opt++)
+ if (catalog == opt->optcontext)
+ appendStringInfo(&buf, "%s%s", (buf.len > 0) ? ", " : "",
+ opt->optname);
+
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("invalid option \"%s\"", def->defname),
+ errhint("Valid options in this context are: %s",
+ buf.data)));
+
+ PG_RETURN_BOOL(false);
+ }
+ }
+
+ PG_RETURN_BOOL(true);
+ }
diff --git a/contrib/dblink/dblink.control b/contrib/dblink/dblink.control
index 4333a9b..39f439a 100644
*** a/contrib/dblink/dblink.control
--- b/contrib/dblink/dblink.control
***************
*** 1,5 ****
# dblink extension
comment = 'connect to other PostgreSQL databases from within a database'
! default_version = '1.0'
module_pathname = '$libdir/dblink'
relocatable = true
--- 1,5 ----
# dblink extension
comment = 'connect to other PostgreSQL databases from within a database'
! default_version = '1.1'
module_pathname = '$libdir/dblink'
relocatable = true
diff --git a/contrib/dblink/dblink.h b/contrib/dblink/dblink.h
index 935d283..fd11658 100644
*** a/contrib/dblink/dblink.h
--- b/contrib/dblink/dblink.h
*************** extern Datum dblink_build_sql_delete(PG_
*** 58,62 ****
--- 58,63 ----
extern Datum dblink_build_sql_update(PG_FUNCTION_ARGS);
extern Datum dblink_current_query(PG_FUNCTION_ARGS);
extern Datum dblink_get_notify(PG_FUNCTION_ARGS);
+ extern Datum dblink_fdw_validator(PG_FUNCTION_ARGS);
#endif /* DBLINK_H */
diff --git a/contrib/dblink/expected/dblink.out b/contrib/dblink/expected/dblink.out
index 511dd5e..0605354 100644
*** a/contrib/dblink/expected/dblink.out
--- b/contrib/dblink/expected/dblink.out
*************** SELECT dblink_disconnect('dtest1');
*** 785,792 ****
-- test foreign data wrapper functionality
CREATE USER dblink_regression_test;
! CREATE FOREIGN DATA WRAPPER postgresql;
! CREATE SERVER fdtest FOREIGN DATA WRAPPER postgresql OPTIONS (dbname 'contrib_regression');
CREATE USER MAPPING FOR public SERVER fdtest;
GRANT USAGE ON FOREIGN SERVER fdtest TO dblink_regression_test;
GRANT EXECUTE ON FUNCTION dblink_connect_u(text, text) TO dblink_regression_test;
--- 785,792 ----
-- test foreign data wrapper functionality
CREATE USER dblink_regression_test;
! CREATE FOREIGN DATA WRAPPER dblink_fdw VALIDATOR dblink_fdw_validator;
! CREATE SERVER fdtest FOREIGN DATA WRAPPER dblink_fdw OPTIONS (dbname 'contrib_regression');
CREATE USER MAPPING FOR public SERVER fdtest;
GRANT USAGE ON FOREIGN SERVER fdtest TO dblink_regression_test;
GRANT EXECUTE ON FUNCTION dblink_connect_u(text, text) TO dblink_regression_test;
*************** REVOKE EXECUTE ON FUNCTION dblink_connec
*** 825,831 ****
DROP USER dblink_regression_test;
DROP USER MAPPING FOR public SERVER fdtest;
DROP SERVER fdtest;
! DROP FOREIGN DATA WRAPPER postgresql;
-- test asynchronous notifications
SELECT dblink_connect('dbname=contrib_regression');
dblink_connect
--- 825,831 ----
DROP USER dblink_regression_test;
DROP USER MAPPING FOR public SERVER fdtest;
DROP SERVER fdtest;
! DROP FOREIGN DATA WRAPPER dblink_fdw;
-- test asynchronous notifications
SELECT dblink_connect('dbname=contrib_regression');
dblink_connect
diff --git a/contrib/dblink/sql/dblink.sql b/contrib/dblink/sql/dblink.sql
index 8c8ffe2..288ebf8 100644
*** a/contrib/dblink/sql/dblink.sql
--- b/contrib/dblink/sql/dblink.sql
*************** SELECT dblink_disconnect('dtest1');
*** 361,368 ****
-- test foreign data wrapper functionality
CREATE USER dblink_regression_test;
! CREATE FOREIGN DATA WRAPPER postgresql;
! CREATE SERVER fdtest FOREIGN DATA WRAPPER postgresql OPTIONS (dbname 'contrib_regression');
CREATE USER MAPPING FOR public SERVER fdtest;
GRANT USAGE ON FOREIGN SERVER fdtest TO dblink_regression_test;
GRANT EXECUTE ON FUNCTION dblink_connect_u(text, text) TO dblink_regression_test;
--- 361,368 ----
-- test foreign data wrapper functionality
CREATE USER dblink_regression_test;
! CREATE FOREIGN DATA WRAPPER dblink_fdw VALIDATOR dblink_fdw_validator;
! CREATE SERVER fdtest FOREIGN DATA WRAPPER dblink_fdw OPTIONS (dbname 'contrib_regression');
CREATE USER MAPPING FOR public SERVER fdtest;
GRANT USAGE ON FOREIGN SERVER fdtest TO dblink_regression_test;
GRANT EXECUTE ON FUNCTION dblink_connect_u(text, text) TO dblink_regression_test;
*************** REVOKE EXECUTE ON FUNCTION dblink_connec
*** 381,387 ****
DROP USER dblink_regression_test;
DROP USER MAPPING FOR public SERVER fdtest;
DROP SERVER fdtest;
! DROP FOREIGN DATA WRAPPER postgresql;
-- test asynchronous notifications
SELECT dblink_connect('dbname=contrib_regression');
--- 381,387 ----
DROP USER dblink_regression_test;
DROP USER MAPPING FOR public SERVER fdtest;
DROP SERVER fdtest;
! DROP FOREIGN DATA WRAPPER dblink_fdw;
-- test asynchronous notifications
SELECT dblink_connect('dbname=contrib_regression');
diff --git a/doc/src/sgml/dblink.sgml b/doc/src/sgml/dblink.sgml
index 855495c..613f447 100644
*** a/doc/src/sgml/dblink.sgml
--- b/doc/src/sgml/dblink.sgml
*************** dblink_connect(text connname, text conns
*** 47,55 ****
<para>
The connection string may also be the name of an existing foreign
server. It is recommended to use
! the <function>postgresql_fdw_validator</function> when defining
! the corresponding foreign-data wrapper. See the example below, as
! well as the following:
<simplelist type="inline">
<member><xref linkend="sql-createforeigndatawrapper"></member>
<member><xref linkend="sql-createserver"></member>
--- 47,58 ----
<para>
The connection string may also be the name of an existing foreign
server. It is recommended to use
! the <function>dblink_fdw_validator</function> when defining
! the corresponding foreign-data wrapper.
! This validator function was added in <productname>PostgreSQL</> 9.2, and
! it has been called <function>postgresql_fdw_validator</function> in older
! versions.
! See the example below, as well as the following:
<simplelist type="inline">
<member><xref linkend="sql-createforeigndatawrapper"></member>
<member><xref linkend="sql-createserver"></member>
*************** SELECT dblink_connect('myconn', 'dbname=
*** 136,143 ****
-- DETAIL: Non-superuser cannot connect if the server does not request a password.
-- HINT: Target server's authentication method must be changed.
CREATE USER dblink_regression_test WITH PASSWORD 'secret';
! CREATE FOREIGN DATA WRAPPER postgresql VALIDATOR postgresql_fdw_validator;
! CREATE SERVER fdtest FOREIGN DATA WRAPPER postgresql OPTIONS (hostaddr '127.0.0.1', dbname 'contrib_regression');
CREATE USER MAPPING FOR dblink_regression_test SERVER fdtest OPTIONS (user 'dblink_regression_test', password 'secret');
GRANT USAGE ON FOREIGN SERVER fdtest TO dblink_regression_test;
--- 139,146 ----
-- DETAIL: Non-superuser cannot connect if the server does not request a password.
-- HINT: Target server's authentication method must be changed.
CREATE USER dblink_regression_test WITH PASSWORD 'secret';
! CREATE FOREIGN DATA WRAPPER dblink_fdw VALIDATOR dblink_fdw_validator;
! CREATE SERVER fdtest FOREIGN DATA WRAPPER dblink_fdw OPTIONS (hostaddr '127.0.0.1', dbname 'contrib_regression');
CREATE USER MAPPING FOR dblink_regression_test SERVER fdtest OPTIONS (user 'dblink_regression_test', password 'secret');
GRANT USAGE ON FOREIGN SERVER fdtest TO dblink_regression_test;
*************** REVOKE SELECT ON TABLE foo FROM dblink_
*** 173,179 ****
DROP USER MAPPING FOR dblink_regression_test SERVER fdtest;
DROP USER dblink_regression_test;
DROP SERVER fdtest;
! DROP FOREIGN DATA WRAPPER postgresql;
</screen>
</refsect1>
</refentry>
--- 176,182 ----
DROP USER MAPPING FOR dblink_regression_test SERVER fdtest;
DROP USER dblink_regression_test;
DROP SERVER fdtest;
! DROP FOREIGN DATA WRAPPER dblink_fdw;
</screen>
</refsect1>
</refentry>
diff --git a/doc/src/sgml/ref/create_foreign_data_wrapper.sgml b/doc/src/sgml/ref/create_foreign_data_wrapper.sgml
index 804fb47..0d346fd 100644
*** a/doc/src/sgml/ref/create_foreign_data_wrapper.sgml
--- b/doc/src/sgml/ref/create_foreign_data_wrapper.sgml
*************** CREATE FOREIGN DATA WRAPPER <replaceable
*** 123,133 ****
</para>
<para>
! There is one built-in foreign-data wrapper validator function
! provided:
! <filename>postgresql_fdw_validator</filename>, which accepts
options corresponding to <application>libpq</> connection
! parameters.
</para>
</refsect1>
--- 123,134 ----
</para>
<para>
! Until <productname>PostgreSQL</productname> 9.1, there was one built-in
! foreign-data wrapper validator function provided:
! <function>postgresql_fdw_validator</function>, which accepts
options corresponding to <application>libpq</> connection
! parameters. But it has been moved to <filename>contrib/dblink</> with
! renaming to <function>dblink_fdw_validator</function>.
</para>
</refsect1>
diff --git a/src/backend/foreign/foreign.c b/src/backend/foreign/foreign.c
index c4c2a61..6831c70 100644
*** a/src/backend/foreign/foreign.c
--- b/src/backend/foreign/foreign.c
***************
*** 25,31 ****
extern Datum pg_options_to_table(PG_FUNCTION_ARGS);
- extern Datum postgresql_fdw_validator(PG_FUNCTION_ARGS);
--- 25,30 ----
*************** pg_options_to_table(PG_FUNCTION_ARGS)
*** 401,504 ****
/*
- * Describes the valid options for postgresql FDW, server, and user mapping.
- */
- struct ConnectionOption
- {
- const char *optname;
- Oid optcontext; /* Oid of catalog in which option may appear */
- };
-
- /*
- * Copied from fe-connect.c PQconninfoOptions.
- *
- * 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}
- };
-
-
- /*
- * Check if the provided option is one of libpq conninfo 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)
- {
- struct ConnectionOption *opt;
-
- for (opt = libpq_conninfo_options; opt->optname; opt++)
- if (context == opt->optcontext && strcmp(opt->optname, option) == 0)
- return true;
- return false;
- }
-
-
- /*
- * Validate the generic option given to SERVER or USER MAPPING.
- * Raise an ERROR if the option or its value is considered
- * invalid.
- *
- * Valid server options are all libpq conninfo options except
- * user and password -- these may only appear in USER MAPPING options.
- */
- Datum
- postgresql_fdw_validator(PG_FUNCTION_ARGS)
- {
- List *options_list = untransformRelOptions(PG_GETARG_DATUM(0));
- Oid catalog = PG_GETARG_OID(1);
-
- ListCell *cell;
-
- foreach(cell, options_list)
- {
- DefElem *def = lfirst(cell);
-
- if (!is_conninfo_option(def->defname, catalog))
- {
- struct ConnectionOption *opt;
- StringInfoData buf;
-
- /*
- * Unknown option specified, complain about it. Provide a hint
- * with list of valid options for the object.
- */
- initStringInfo(&buf);
- for (opt = libpq_conninfo_options; opt->optname; opt++)
- if (catalog == opt->optcontext)
- appendStringInfo(&buf, "%s%s", (buf.len > 0) ? ", " : "",
- opt->optname);
-
- ereport(ERROR,
- (errcode(ERRCODE_SYNTAX_ERROR),
- errmsg("invalid option \"%s\"", def->defname),
- errhint("Valid options in this context are: %s",
- buf.data)));
-
- PG_RETURN_BOOL(false);
- }
- }
-
- PG_RETURN_BOOL(true);
- }
-
- /*
* get_foreign_data_wrapper_oid - given a FDW name, look up the OID
*
* If missing_ok is false, throw an error if name not found. If true, just
--- 400,405 ----
diff --git a/src/include/catalog/pg_proc.h b/src/include/catalog/pg_proc.h
index 8700d0d..8970998 100644
*** a/src/include/catalog/pg_proc.h
--- b/src/include/catalog/pg_proc.h
*************** DESCR("filenode identifier of relation")
*** 3399,3407 ****
DATA(insert OID = 3034 ( pg_relation_filepath PGNSP PGUID 12 1 0 0 0 f f f f t f s 1 0 25 "2205" _null_ _null_ _null_ _null_ pg_relation_filepath _null_ _null_ _null_ ));
DESCR("file path of relation");
- DATA(insert OID = 2316 ( postgresql_fdw_validator PGNSP PGUID 12 1 0 0 0 f f f f t f i 2 0 16 "1009 26" _null_ _null_ _null_ _null_ postgresql_fdw_validator _null_ _null_ _null_));
- DESCR("(internal)");
-
DATA(insert OID = 2290 ( record_in PGNSP PGUID 12 1 0 0 0 f f f f t f s 3 0 2249 "2275 26 23" _null_ _null_ _null_ _null_ record_in _null_ _null_ _null_ ));
DESCR("I/O");
DATA(insert OID = 2291 ( record_out PGNSP PGUID 12 1 0 0 0 f f f f t f s 1 0 2275 "2249" _null_ _null_ _null_ _null_ record_out _null_ _null_ _null_ ));
--- 3399,3404 ----
diff --git a/src/test/regress/expected/foreign_data.out b/src/test/regress/expected/foreign_data.out
index ba86883..161fbe7 100644
*** a/src/test/regress/expected/foreign_data.out
--- b/src/test/regress/expected/foreign_data.out
*************** CREATE ROLE regress_test_role2;
*** 13,18 ****
--- 13,33 ----
CREATE ROLE regress_test_role_super SUPERUSER;
CREATE ROLE regress_test_indirect;
CREATE ROLE unprivileged_role;
+ -- validator for this test
+ CREATE FUNCTION postgresql_fdw_validator(text[], oid) RETURNS boolean AS $$
+ DECLARE
+ BEGIN
+ IF $1::text LIKE '%foo%' THEN
+ RAISE 'invalid option "foo"'
+ USING HINT = 'Valid options in this context are: authtype, service, connect_timeout, dbname, host, hostaddr, port, tty, options, requiressl, sslmode, gsslib';
+ ELSIF $1::text LIKE '%username%' THEN
+ RAISE 'invalid option "username"'
+ USING HINT = 'Valid options in this context are: user, password';
+ END IF;
+
+ RETURN true;
+ END
+ $$ LANGUAGE plpgsql;
CREATE FOREIGN DATA WRAPPER dummy;
COMMENT ON FOREIGN DATA WRAPPER dummy IS 'useless';
CREATE FOREIGN DATA WRAPPER postgresql VALIDATOR postgresql_fdw_validator;
*************** DROP FOREIGN DATA WRAPPER postgresql CAS
*** 1201,1206 ****
--- 1216,1222 ----
DROP FOREIGN DATA WRAPPER dummy CASCADE;
NOTICE: drop cascades to server s0
\c
+ DROP FUNCTION postgresql_fdw_validator(text[], oid);
DROP ROLE foreign_data_user;
-- At this point we should have no wrappers, no servers, and no mappings.
SELECT fdwname, fdwhandler, fdwvalidator, fdwoptions FROM pg_foreign_data_wrapper;
diff --git a/src/test/regress/sql/foreign_data.sql b/src/test/regress/sql/foreign_data.sql
index 0c95672..9cf9ca7 100644
*** a/src/test/regress/sql/foreign_data.sql
--- b/src/test/regress/sql/foreign_data.sql
*************** CREATE ROLE regress_test_role_super SUPE
*** 20,25 ****
--- 20,41 ----
CREATE ROLE regress_test_indirect;
CREATE ROLE unprivileged_role;
+ -- validator for this test
+ CREATE FUNCTION postgresql_fdw_validator(text[], oid) RETURNS boolean AS $$
+ DECLARE
+ BEGIN
+ IF $1::text LIKE '%foo%' THEN
+ RAISE 'invalid option "foo"'
+ USING HINT = 'Valid options in this context are: authtype, service, connect_timeout, dbname, host, hostaddr, port, tty, options, requiressl, sslmode, gsslib';
+ ELSIF $1::text LIKE '%username%' THEN
+ RAISE 'invalid option "username"'
+ USING HINT = 'Valid options in this context are: user, password';
+ END IF;
+
+ RETURN true;
+ END
+ $$ LANGUAGE plpgsql;
+
CREATE FOREIGN DATA WRAPPER dummy;
COMMENT ON FOREIGN DATA WRAPPER dummy IS 'useless';
CREATE FOREIGN DATA WRAPPER postgresql VALIDATOR postgresql_fdw_validator;
*************** DROP ROLE regress_test_role2;
*** 495,500 ****
--- 511,517 ----
DROP FOREIGN DATA WRAPPER postgresql CASCADE;
DROP FOREIGN DATA WRAPPER dummy CASCADE;
\c
+ DROP FUNCTION postgresql_fdw_validator(text[], oid);
DROP ROLE foreign_data_user;
-- At this point we should have no wrappers, no servers, and no mappings.
On tor, 2012-03-01 at 20:56 +0900, Shigeru Hanada wrote:
How about moving postgresql_fdw_validator into dblink,
That's probably a good move. If this were C++, we might try to subclass
this whole thing a bit, to avoid code duplication, but I don't see an
easy way to do that here.
with renaming to dblink_fdw_validator?
Well, it's not the validator of the dblink_fdw, so maybe something like
basic_postgresql_fdw_validator.
2012/3/2 Peter Eisentraut <peter_e@gmx.net>:
with renaming to dblink_fdw_validator?
Well, it's not the validator of the dblink_fdw, so maybe something like
basic_postgresql_fdw_validator.
-1 for same reason. It's not the validator of basic_postgresql_fdw.
Using "fdw" in the name of validator which doesn't have actual FDW might
confuse users. Rather dblink_validator or libpq_option_validator is better?
One possible another idea is creating dblink_fdw which uses the
validator during "CREATE EXTENSION dblink" for users who store
connection information in FDW objects.
--
Shigeru Hanada
(2012/02/21 20:25), Etsuro Fujita wrote:
Please find attached an updated version of the patch.
This v2 patch can be applied on HEAD cleanly. Compile completed with
only one expected warning of scan.c, and all regression tests for both
core and contrib modules passed.
This patch allows FDWs to return multiple ForeignPath nodes per a
PlanForeignScan call. It also get rid of FdwPlan, FDW-private
information container, by replacing with simple List.
I've reviewed the patch closely, and have some comments about its design.
Basically a create_foo_path is responsible for creating a node object
with a particular Path-derived type, but this patch changes
create_foreignscan_path to just call PlanForeignScan and return void.
This change seems breaking module design. IMO create_foreignscan_path
should return just one ForeignPath node per a call, so calling add_path
multiple times should be done in somewhere else. I think
set_foreign_pathlist suites for it, because set_foo_pathlist functions
are responsible for building possible paths for a RangeTblEntry, as
comment of set_foreign_pathlist says.
/*
* set_foreign_pathlist
* Build one or more access paths for a foreign table RTE
*/
In this design, FDW authors can implement PlanForeignScan by repeating
steps below for each possible scan path for a foreign table:
(1) create a template ForeignPath node with create_foreignscan_path
(2) customize the path as FDW wants, e.g. push down WHERE clause
(3) store FDW-private info
(4) estimate costs of the path
(5) call add_path to add the path to RelOptInfo
Current design doesn't allow FDWs to provide multiple paths which have
different local filtering from each other, because all paths share a
RelOptInfo and baserestrictinfo in it. I think this restriction
wouldn't be a serious problem.
Please find attached a patch implementing the design above.
--
Shigeru Hanada
Attachments:
postgresql-fdw-v3.patchtext/plain; name=postgresql-fdw-v3.patchDownload
diff --git a/contrib/file_fdw/file_fdw.c b/contrib/file_fdw/file_fdw.c
index 46394a8..bb541e3 100644
*** a/contrib/file_fdw/file_fdw.c
--- b/contrib/file_fdw/file_fdw.c
***************
*** 25,30 ****
--- 25,31 ----
#include "miscadmin.h"
#include "nodes/makefuncs.h"
#include "optimizer/cost.h"
+ #include "optimizer/pathnode.h"
#include "utils/rel.h"
#include "utils/syscache.h"
*************** PG_FUNCTION_INFO_V1(file_fdw_validator);
*** 93,99 ****
/*
* FDW callback routines
*/
! static FdwPlan *filePlanForeignScan(Oid foreigntableid,
PlannerInfo *root,
RelOptInfo *baserel);
static void fileExplainForeignScan(ForeignScanState *node, ExplainState *es);
--- 94,100 ----
/*
* FDW callback routines
*/
! static void filePlanForeignScan(Oid foreigntableid,
PlannerInfo *root,
RelOptInfo *baserel);
static void fileExplainForeignScan(ForeignScanState *node, ExplainState *es);
*************** get_file_fdw_attribute_options(Oid relid
*** 406,432 ****
/*
* filePlanForeignScan
! * Create a FdwPlan for a scan on the foreign table
*/
! static FdwPlan *
filePlanForeignScan(Oid foreigntableid,
PlannerInfo *root,
RelOptInfo *baserel)
{
! FdwPlan *fdwplan;
char *filename;
List *options;
/* Fetch options --- we only need filename at this point */
fileGetOptions(foreigntableid, &filename, &options);
! /* Construct FdwPlan with cost estimates */
! fdwplan = makeNode(FdwPlan);
estimate_costs(root, baserel, filename,
! &fdwplan->startup_cost, &fdwplan->total_cost);
! fdwplan->fdw_private = NIL; /* not used */
! return fdwplan;
}
/*
--- 407,443 ----
/*
* filePlanForeignScan
! * Create possible access paths for a scan on the foreign table
! *
! * Currently we don't support any push-down feature, so there is only one
! * possible access path, which simply returns all records in the order in
! * the data file.
*/
! static void
filePlanForeignScan(Oid foreigntableid,
PlannerInfo *root,
RelOptInfo *baserel)
{
! ForeignPath *pathnode = makeNode(ForeignPath);
char *filename;
List *options;
/* Fetch options --- we only need filename at this point */
fileGetOptions(foreigntableid, &filename, &options);
! /* Create a ForeignPath node and add it as only one possible path. */
! pathnode = create_foreignscan_path(root, baserel);
! pathnode->fdw_private = NIL;
estimate_costs(root, baserel, filename,
! &pathnode->path.startup_cost, &pathnode->path.total_cost);
! pathnode->path.rows = baserel->rows;
! add_path(baserel, (Path *) pathnode);
! /*
! * If data file was sorted, and we knew it somehow, we can create another
! * ForeignPath node with valid pathkeys to tell planner that sorting this
! * file by such key is not expensive.
! */
}
/*
diff --git a/doc/src/sgml/fdwhandler.sgml b/doc/src/sgml/fdwhandler.sgml
index 76ff243..63fe8bd 100644
*** a/doc/src/sgml/fdwhandler.sgml
--- b/doc/src/sgml/fdwhandler.sgml
***************
*** 88,108 ****
<para>
<programlisting>
! FdwPlan *
PlanForeignScan (Oid foreigntableid,
PlannerInfo *root,
RelOptInfo *baserel);
</programlisting>
! Plan a scan on a foreign table. This is called when a query is planned.
<literal>foreigntableid</> is the <structname>pg_class</> OID of the
foreign table. <literal>root</> is the planner's global information
about the query, and <literal>baserel</> is the planner's information
about this table.
! The function must return a palloc'd struct that contains cost estimates
! plus any FDW-private information that is needed to execute the foreign
! scan at a later time. (Note that the private information must be
! represented in a form that <function>copyObject</> knows how to copy.)
</para>
<para>
--- 88,117 ----
<para>
<programlisting>
! void
PlanForeignScan (Oid foreigntableid,
PlannerInfo *root,
RelOptInfo *baserel);
</programlisting>
! Create possible access paths for a scan on a foreign table. This is
! called when a query is planned.
<literal>foreigntableid</> is the <structname>pg_class</> OID of the
foreign table. <literal>root</> is the planner's global information
about the query, and <literal>baserel</> is the planner's information
about this table.
! </para>
!
! <para>
! The function must generate at least one access path for a scan on the
! foreign table and call <function>add_path</> to add it to
! <literal>baserel->pathlist</>. The function may generate extra
! access paths, e.g. a path which has valid <literal>pathkeys</> for sorted
! result. Each access path should contain cost estimates, and can contain
! any FDW-private information that is needed to execute the foreign scan at
! a later time.
! (Note that the private information must be represented in a form that
! <function>copyObject</> knows how to copy.)
</para>
<para>
*************** BeginForeignScan (ForeignScanState *node
*** 159,167 ****
its <structfield>fdw_state</> field is still NULL. Information about
the table to scan is accessible through the
<structname>ForeignScanState</> node (in particular, from the underlying
! <structname>ForeignScan</> plan node, which contains a pointer to the
! <structname>FdwPlan</> structure returned by
! <function>PlanForeignScan</>).
</para>
<para>
--- 168,175 ----
its <structfield>fdw_state</> field is still NULL. Information about
the table to scan is accessible through the
<structname>ForeignScanState</> node (in particular, from the underlying
! <structname>ForeignScan</> plan node, which contains FDW-private
! information about it provided by <function>PlanForeignScan</>).
</para>
<para>
*************** EndForeignScan (ForeignScanState *node);
*** 228,236 ****
</para>
<para>
! The <structname>FdwRoutine</> and <structname>FdwPlan</> struct types
! are declared in <filename>src/include/foreign/fdwapi.h</>, which see
! for additional details.
</para>
</sect1>
--- 236,244 ----
</para>
<para>
! The <structname>FdwRoutine</> struct type is declared
! in <filename>src/include/foreign/fdwapi.h</>, which see for additional
! details.
</para>
</sect1>
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index 7fec4db..9b2f688 100644
*** a/src/backend/nodes/copyfuncs.c
--- b/src/backend/nodes/copyfuncs.c
*************** _copyForeignScan(const ForeignScan *from
*** 591,611 ****
* copy remainder of node
*/
COPY_SCALAR_FIELD(fsSystemCol);
- COPY_NODE_FIELD(fdwplan);
-
- return newnode;
- }
-
- /*
- * _copyFdwPlan
- */
- static FdwPlan *
- _copyFdwPlan(const FdwPlan *from)
- {
- FdwPlan *newnode = makeNode(FdwPlan);
-
- COPY_SCALAR_FIELD(startup_cost);
- COPY_SCALAR_FIELD(total_cost);
COPY_NODE_FIELD(fdw_private);
return newnode;
--- 591,596 ----
*************** copyObject(const void *from)
*** 3842,3850 ****
case T_ForeignScan:
retval = _copyForeignScan(from);
break;
- case T_FdwPlan:
- retval = _copyFdwPlan(from);
- break;
case T_Join:
retval = _copyJoin(from);
break;
--- 3827,3832 ----
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index 25a215e..08b74fb 100644
*** a/src/backend/nodes/outfuncs.c
--- b/src/backend/nodes/outfuncs.c
*************** _outForeignScan(StringInfo str, const Fo
*** 558,573 ****
_outScanInfo(str, (const Scan *) node);
WRITE_BOOL_FIELD(fsSystemCol);
- WRITE_NODE_FIELD(fdwplan);
- }
-
- static void
- _outFdwPlan(StringInfo str, const FdwPlan *node)
- {
- WRITE_NODE_TYPE("FDWPLAN");
-
- WRITE_FLOAT_FIELD(startup_cost, "%.2f");
- WRITE_FLOAT_FIELD(total_cost, "%.2f");
WRITE_NODE_FIELD(fdw_private);
}
--- 558,563 ----
*************** _outForeignPath(StringInfo str, const Fo
*** 1572,1578 ****
_outPathInfo(str, (const Path *) node);
! WRITE_NODE_FIELD(fdwplan);
}
static void
--- 1562,1568 ----
_outPathInfo(str, (const Path *) node);
! WRITE_NODE_FIELD(fdw_private);
}
static void
*************** _outNode(StringInfo str, const void *obj
*** 2745,2753 ****
case T_ForeignScan:
_outForeignScan(str, obj);
break;
- case T_FdwPlan:
- _outFdwPlan(str, obj);
- break;
case T_Join:
_outJoin(str, obj);
break;
--- 2735,2740 ----
diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c
index 8f03417..060d49d 100644
*** a/src/backend/optimizer/path/allpaths.c
--- b/src/backend/optimizer/path/allpaths.c
***************
*** 18,23 ****
--- 18,24 ----
#include <math.h>
#include "catalog/pg_class.h"
+ #include "foreign/fdwapi.h"
#include "nodes/nodeFuncs.h"
#ifdef OPTIMIZER_DEBUG
#include "nodes/print.h"
*************** set_foreign_size(PlannerInfo *root, RelO
*** 399,411 ****
/*
* set_foreign_pathlist
! * Build the (single) access path for a foreign table RTE
*/
static void
set_foreign_pathlist(PlannerInfo *root, RelOptInfo *rel, RangeTblEntry *rte)
{
! /* Generate appropriate path */
! add_path(rel, (Path *) create_foreignscan_path(root, rel));
/* Select cheapest path (pretty easy in this case...) */
set_cheapest(rel);
--- 400,414 ----
/*
* set_foreign_pathlist
! * Build access paths for a foreign table RTE
*/
static void
set_foreign_pathlist(PlannerInfo *root, RelOptInfo *rel, RangeTblEntry *rte)
{
! FdwRoutine *fdwroutine;
!
! fdwroutine = GetFdwRoutineByRelId(rte->relid);
! fdwroutine->PlanForeignScan(rte->relid, root, rel);
/* Select cheapest path (pretty easy in this case...) */
set_cheapest(rel);
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index 9ac0c99..c348fd6 100644
*** a/src/backend/optimizer/plan/createplan.c
--- b/src/backend/optimizer/plan/createplan.c
*************** static CteScan *make_ctescan(List *qptli
*** 121,127 ****
static WorkTableScan *make_worktablescan(List *qptlist, List *qpqual,
Index scanrelid, int wtParam);
static ForeignScan *make_foreignscan(List *qptlist, List *qpqual,
! Index scanrelid, bool fsSystemCol, FdwPlan *fdwplan);
static BitmapAnd *make_bitmap_and(List *bitmapplans);
static BitmapOr *make_bitmap_or(List *bitmapplans);
static NestLoop *make_nestloop(List *tlist,
--- 121,127 ----
static WorkTableScan *make_worktablescan(List *qptlist, List *qpqual,
Index scanrelid, int wtParam);
static ForeignScan *make_foreignscan(List *qptlist, List *qpqual,
! Index scanrelid, bool fsSystemCol, List *fdw_private);
static BitmapAnd *make_bitmap_and(List *bitmapplans);
static BitmapOr *make_bitmap_or(List *bitmapplans);
static NestLoop *make_nestloop(List *tlist,
*************** create_foreignscan_plan(PlannerInfo *roo
*** 1847,1853 ****
scan_clauses,
scan_relid,
fsSystemCol,
! best_path->fdwplan);
copy_path_costsize(&scan_plan->scan.plan, &best_path->path);
--- 1847,1853 ----
scan_clauses,
scan_relid,
fsSystemCol,
! best_path->fdw_private);
copy_path_costsize(&scan_plan->scan.plan, &best_path->path);
*************** make_foreignscan(List *qptlist,
*** 3189,3195 ****
List *qpqual,
Index scanrelid,
bool fsSystemCol,
! FdwPlan *fdwplan)
{
ForeignScan *node = makeNode(ForeignScan);
Plan *plan = &node->scan.plan;
--- 3189,3195 ----
List *qpqual,
Index scanrelid,
bool fsSystemCol,
! List *fdw_private)
{
ForeignScan *node = makeNode(ForeignScan);
Plan *plan = &node->scan.plan;
*************** make_foreignscan(List *qptlist,
*** 3201,3207 ****
plan->righttree = NULL;
node->scan.scanrelid = scanrelid;
node->fsSystemCol = fsSystemCol;
! node->fdwplan = fdwplan;
return node;
}
--- 3201,3207 ----
plan->righttree = NULL;
node->scan.scanrelid = scanrelid;
node->fsSystemCol = fsSystemCol;
! node->fdw_private = fdw_private;
return node;
}
diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.c
index d29b454..9c285a9 100644
*** a/src/backend/optimizer/util/pathnode.c
--- b/src/backend/optimizer/util/pathnode.c
*************** create_worktablescan_path(PlannerInfo *r
*** 1764,1801 ****
/*
* create_foreignscan_path
! * Creates a path corresponding to a scan of a foreign table,
! * returning the pathnode.
*/
ForeignPath *
create_foreignscan_path(PlannerInfo *root, RelOptInfo *rel)
{
ForeignPath *pathnode = makeNode(ForeignPath);
- RangeTblEntry *rte;
- FdwRoutine *fdwroutine;
- FdwPlan *fdwplan;
pathnode->path.pathtype = T_ForeignScan;
pathnode->path.parent = rel;
! pathnode->path.pathkeys = NIL; /* result is always unordered */
pathnode->path.required_outer = NULL;
pathnode->path.param_clauses = NIL;
! /* Get FDW's callback info */
! rte = planner_rt_fetch(rel->relid, root);
! fdwroutine = GetFdwRoutineByRelId(rte->relid);
!
! /* Let the FDW do its planning */
! fdwplan = fdwroutine->PlanForeignScan(rte->relid, root, rel);
! if (fdwplan == NULL || !IsA(fdwplan, FdwPlan))
! elog(ERROR, "foreign-data wrapper PlanForeignScan function for relation %u did not return an FdwPlan struct",
! rte->relid);
! pathnode->fdwplan = fdwplan;
!
! /* use costs estimated by FDW */
! pathnode->path.rows = rel->rows;
! pathnode->path.startup_cost = fdwplan->startup_cost;
! pathnode->path.total_cost = fdwplan->total_cost;
return pathnode;
}
--- 1764,1795 ----
/*
* create_foreignscan_path
! * Creates a path corresponding to a scan of a foreign table, and returns
! * the pathnode.
! *
! * It's assumed that this function is called from PlanForeignScan
! * imeplementation of FDWs, but not from planner directly; planner never
! * calls this function directly. Generated ForeignPath node has valid
! * values for only requried fields, so each FDW should set optional fields
! * as they want.
! *
! * If FDW needs to pass FDW-specific information from planner to executor,
! * fdw_private field is available. Its type is List *, so you can store
! * Node subclass objects. Planner will copy fdw_private of cheapest
! * ForeignPath to ForeignScan plan node.
*/
ForeignPath *
create_foreignscan_path(PlannerInfo *root, RelOptInfo *rel)
{
ForeignPath *pathnode = makeNode(ForeignPath);
pathnode->path.pathtype = T_ForeignScan;
pathnode->path.parent = rel;
! pathnode->path.pathkeys = NIL;
pathnode->path.required_outer = NULL;
pathnode->path.param_clauses = NIL;
! pathnode->fdw_private = NIL;
return pathnode;
}
diff --git a/src/include/foreign/fdwapi.h b/src/include/foreign/fdwapi.h
index 3696623..4831e7c 100644
*** a/src/include/foreign/fdwapi.h
--- b/src/include/foreign/fdwapi.h
*************** struct ExplainState;
*** 20,58 ****
/*
- * FdwPlan is the information returned to the planner by PlanForeignScan.
- */
- typedef struct FdwPlan
- {
- NodeTag type;
-
- /*
- * Cost estimation info. The startup_cost is time before retrieving the
- * first row, so it should include costs of connecting to the remote host,
- * sending over the query, etc. Note that PlanForeignScan also ought to
- * set baserel->rows and baserel->width if it can produce any usable
- * estimates of those values.
- */
- Cost startup_cost; /* cost expended before fetching any tuples */
- Cost total_cost; /* total cost (assuming all tuples fetched) */
-
- /*
- * FDW private data, which will be available at execution time.
- *
- * Note that everything in this list must be copiable by copyObject(). One
- * way to store an arbitrary blob of bytes is to represent it as a bytea
- * Const. Usually, though, you'll be better off choosing a representation
- * that can be dumped usefully by nodeToString().
- */
- List *fdw_private;
- } FdwPlan;
-
-
- /*
* Callback function signatures --- see fdwhandler.sgml for more info.
*/
! typedef FdwPlan *(*PlanForeignScan_function) (Oid foreigntableid,
PlannerInfo *root,
RelOptInfo *baserel);
--- 20,29 ----
/*
* Callback function signatures --- see fdwhandler.sgml for more info.
*/
! typedef void (*PlanForeignScan_function) (Oid foreigntableid,
PlannerInfo *root,
RelOptInfo *baserel);
diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h
index 0e7d184..905458f 100644
*** a/src/include/nodes/nodes.h
--- b/src/include/nodes/nodes.h
*************** typedef enum NodeTag
*** 62,68 ****
T_CteScan,
T_WorkTableScan,
T_ForeignScan,
- T_FdwPlan,
T_Join,
T_NestLoop,
T_MergeJoin,
--- 62,67 ----
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 7d90b91..23d9256 100644
*** a/src/include/nodes/plannodes.h
--- b/src/include/nodes/plannodes.h
*************** typedef struct ForeignScan
*** 468,475 ****
{
Scan scan;
bool fsSystemCol; /* true if any "system column" is needed */
! /* use struct pointer to avoid including fdwapi.h here */
! struct FdwPlan *fdwplan;
} ForeignScan;
--- 468,474 ----
{
Scan scan;
bool fsSystemCol; /* true if any "system column" is needed */
! List *fdw_private;
} ForeignScan;
diff --git a/src/include/nodes/relation.h b/src/include/nodes/relation.h
index 6ba920a..1acc4f8 100644
*** a/src/include/nodes/relation.h
--- b/src/include/nodes/relation.h
*************** typedef struct TidPath
*** 794,805 ****
/*
* ForeignPath represents a scan of a foreign table
*/
typedef struct ForeignPath
{
Path path;
! /* use struct pointer to avoid including fdwapi.h here */
! struct FdwPlan *fdwplan;
} ForeignPath;
/*
--- 794,810 ----
/*
* ForeignPath represents a scan of a foreign table
+ *
+ * fdw_private is a list of FDW private data, which will be available at
+ * execution time. Note that everything in this list must be copiable
+ * by copyObject(). One way to store an arbitrary blob of bytes is to
+ * represent it as a bytea Const. Usually, though, you'll be better off
+ * choosing a representation that can be dumped usefully by nodeToString().
*/
typedef struct ForeignPath
{
Path path;
! List *fdw_private;
} ForeignPath;
/*
(2012/03/05 18:21), Shigeru Hanada wrote:
(2012/02/21 20:25), Etsuro Fujita wrote:
Please find attached an updated version of the patch.
This v2 patch can be applied on HEAD cleanly. Compile completed with
only one expected warning of scan.c, and all regression tests for both
core and contrib modules passed.This patch allows FDWs to return multiple ForeignPath nodes per a
PlanForeignScan call. It also get rid of FdwPlan, FDW-private
information container, by replacing with simple List.I've reviewed the patch closely, and have some comments about its design.
Thank you for your review.
Basically a create_foo_path is responsible for creating a node object
with a particular Path-derived type, but this patch changes
create_foreignscan_path to just call PlanForeignScan and return void.
This change seems breaking module design.
create_index_path builds multiple index paths for a plain relation. How
about renaming the function to create_foreign_paths?
Best regards,
Etsuro Fujita
(2012/03/05 21:00), Etsuro Fujita wrote:
(2012/03/05 18:21), Shigeru Hanada wrote:
(2012/02/21 20:25), Etsuro Fujita wrote:
Please find attached an updated version of the patch.
This v2 patch can be applied on HEAD cleanly. Compile completed with
only one expected warning of scan.c, and all regression tests for both
core and contrib modules passed.This patch allows FDWs to return multiple ForeignPath nodes per a
PlanForeignScan call. It also get rid of FdwPlan, FDW-private
information container, by replacing with simple List.I've reviewed the patch closely, and have some comments about its design.
Thank you for your review.
Basically a create_foo_path is responsible for creating a node object
with a particular Path-derived type, but this patch changes
create_foreignscan_path to just call PlanForeignScan and return void.
This change seems breaking module design.create_index_path builds multiple index paths for a plain relation. How
about renaming the function to create_foreign_paths?
I meant "create_foreignscan_paths". I'm sorry about that.
Best regards,
Etsuro Fujita
(2012/03/05 21:05), Etsuro Fujita wrote:
(2012/03/05 21:00), Etsuro Fujita wrote:
create_index_path builds multiple index paths for a plain relation. How
about renaming the function to create_foreign_paths?I meant "create_foreignscan_paths". I'm sorry about that.
Perhaps you are confusing create_index_path with create_index_paths.
Former creates a IndexScan path node (so it's similar to
create_foreignscan_path), and latter builds multiple IndexScan paths for
a plain relation.
So, just renaming create_foreignscan_path to plural form seems missing
the point.
--
Shigeru Hanada
Shigeru Hanada <shigeru.hanada@gmail.com> writes:
So, just renaming create_foreignscan_path to plural form seems missing
the point.
I agree that that wouldn't be an improvement. What bothers me about the
patch's version of this function is that it just creates a content-free
Path node and leaves it to the caller to fill in everything. That
doesn't accomplish much, and it leaves the caller very exposed to errors
of omission. It's also unlike the other create_xxx_path functions,
which generally hand back a completed Path ready to pass to add_path.
I'm inclined to think that if we provide this function in core at all,
it should take a parameter list long enough to let it fill in the Path
completely. That would imply that any future changes in Path structs
would result in a change in the parameter list, which would break
callers --- but it would break them in an obvious way that the C
compiler would complain about. If we leave it as-is, those same callers
would be broken silently, because they'd just be failing to fill in
the new Path fields.
regards, tom lane
I wrote:
I'm inclined to think that if we provide this function in core at all,
it should take a parameter list long enough to let it fill in the Path
completely. That would imply that any future changes in Path structs
would result in a change in the parameter list, which would break
callers --- but it would break them in an obvious way that the C
compiler would complain about. If we leave it as-is, those same callers
would be broken silently, because they'd just be failing to fill in
the new Path fields.
I've committed the PlanForeignScan API change, with that change and
some other minor editorialization. The pgsql_fdw patch now needs an
update, so I set it back to Waiting On Author state.
regards, tom lane
Shigeru Hanada wrote:
[pgsql_fdw_v12.patch]
I know this is not the latest version, but I played around with it and
tickled a bug.
It seems to have a problem with rolled back subtransactions.
test=> \d+ remote
Foreign table "laurenz.remote"
Column | Type | Modifiers | FDW Options | Storage | Description
--------+---------+-----------+-------------+----------+-------------
id | integer | not null | | plain |
val | text | not null | | extended |
Server: loopback
FDW Options: (nspname 'laurenz', relname 'local')
Has OIDs: no
test=> BEGIN;
test=> DECLARE x CURSOR FOR SELECT * FROM remote;
DEBUG: Remote SQL: SELECT id, val FROM laurenz.local
DEBUG: relid=16423 fetch_count=10000
DEBUG: starting remote transaction with "START TRANSACTION ISOLATION
LEVEL REPEATABLE READ"
test=> FETCH x;
id | val
----+-----
1 | one
(1 row)
test=> SAVEPOINT z;
test=> ERROR OUT;
ERROR: syntax error at or near "ERROR"
LINE 1: ERROR OUT;
test=> ROLLBACK TO SAVEPOINT z;
test=> FETCH x;
id | val
----+-----
2 | two
(1 row)
test=> COMMIT;
ERROR: could not close cursor
DETAIL: no connection to the server
HINT: CLOSE pgsql_fdw_cursor_0
The error message reported is not consistent, at one attempt the backend
crashed.
Yours,
Laurenz Albe
(2012/03/06 6:19), Tom Lane wrote:
I've committed the PlanForeignScan API change, with that change and
some other minor editorialization. The pgsql_fdw patch now needs an
update, so I set it back to Waiting On Author state.
Thanks.
I've revised pgsql_fdw to catch up to this change, but I'll post those
patches after fixing the bug reported by Albe Laurenz.
BTW, what I did for this change is also needed for other existing FDWs
to make them available on 9.2. So, I'd like to add how to change FDWs
for 9.2 to SQL/MED wiki page, where probably most of the FDW authors check.
Regards,
--
Shigeru Hanada
(2012/03/06 19:09), Albe Laurenz wrote:
I know this is not the latest version, but I played around with it and
tickled a bug.
It seems to have a problem with rolled back subtransactions.
Thanks for the report!
The problem was in cleanup_connection, which is called at end of
transactions. Connection should be closed only when the trigger is a
top level transaction and it's aborting, but isTopLevel flag was not
checked. I fixed the bug and added regression tests for such cases.
Attached patches also contains changes to catch up to the redesign of
PlanForeignScan.
Regards,
--
Shigeru Hanada
Attachments:
pgsql_fdw_v13.patchtext/plain; name=pgsql_fdw_v13.patchDownload
diff --git a/contrib/Makefile b/contrib/Makefile
index ac0a80a..14aa433 100644
*** a/contrib/Makefile
--- b/contrib/Makefile
*************** SUBDIRS = \
*** 41,46 ****
--- 41,47 ----
pgbench \
pgcrypto \
pgrowlocks \
+ pgsql_fdw \
pgstattuple \
seg \
spi \
diff --git a/contrib/README b/contrib/README
index a1d42a1..d3fa211 100644
*** a/contrib/README
--- b/contrib/README
*************** pgrowlocks -
*** 158,163 ****
--- 158,167 ----
A function to return row locking information
by Tatsuo Ishii <ishii@sraoss.co.jp>
+ pgsql_fdw -
+ Foreign-data wrapper for external PostgreSQL servers.
+ by Shigeru Hanada <shigeru.hanada@gmail.com>
+
pgstattuple -
Functions to return statistics about "dead" tuples and free
space within a table
diff --git a/contrib/pgsql_fdw/.gitignore b/contrib/pgsql_fdw/.gitignore
index ...0854728 .
*** a/contrib/pgsql_fdw/.gitignore
--- b/contrib/pgsql_fdw/.gitignore
***************
*** 0 ****
--- 1,4 ----
+ # Generated subdirectories
+ /results/
+ *.o
+ *.so
diff --git a/contrib/pgsql_fdw/Makefile b/contrib/pgsql_fdw/Makefile
index ...6381365 .
*** a/contrib/pgsql_fdw/Makefile
--- b/contrib/pgsql_fdw/Makefile
***************
*** 0 ****
--- 1,22 ----
+ # contrib/pgsql_fdw/Makefile
+
+ MODULE_big = pgsql_fdw
+ OBJS = pgsql_fdw.o option.o deparse.o connection.o
+ PG_CPPFLAGS = -I$(libpq_srcdir)
+ SHLIB_LINK = $(libpq)
+
+ EXTENSION = pgsql_fdw
+ DATA = pgsql_fdw--1.0.sql
+
+ REGRESS = pgsql_fdw
+
+ ifdef USE_PGXS
+ PG_CONFIG = pg_config
+ PGXS := $(shell $(PG_CONFIG) --pgxs)
+ include $(PGXS)
+ else
+ subdir = contrib/pgsql_fdw
+ top_builddir = ../..
+ include $(top_builddir)/src/Makefile.global
+ include $(top_srcdir)/contrib/contrib-global.mk
+ endif
diff --git a/contrib/pgsql_fdw/connection.c b/contrib/pgsql_fdw/connection.c
index ...4a4e30c .
*** a/contrib/pgsql_fdw/connection.c
--- b/contrib/pgsql_fdw/connection.c
***************
*** 0 ****
--- 1,556 ----
+ /*-------------------------------------------------------------------------
+ *
+ * connection.c
+ * Connection management for pgsql_fdw
+ *
+ * Portions Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/connection.c
+ *
+ *-------------------------------------------------------------------------
+ */
+ #include "postgres.h"
+
+ #include "access/xact.h"
+ #include "catalog/pg_type.h"
+ #include "foreign/foreign.h"
+ #include "funcapi.h"
+ #include "libpq-fe.h"
+ #include "mb/pg_wchar.h"
+ #include "miscadmin.h"
+ #include "utils/array.h"
+ #include "utils/builtins.h"
+ #include "utils/hsearch.h"
+ #include "utils/memutils.h"
+ #include "utils/resowner.h"
+ #include "utils/tuplestore.h"
+
+ #include "pgsql_fdw.h"
+ #include "connection.h"
+
+ /* ============================================================================
+ * Connection management functions
+ * ==========================================================================*/
+
+ /*
+ * Connection cache entry managed with hash table.
+ */
+ typedef struct ConnCacheEntry
+ {
+ /* hash key must be first */
+ Oid serverid; /* oid of foreign server */
+ Oid userid; /* oid of local user */
+
+ bool use_tx; /* true when using remote transaction */
+ 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);
+ static void begin_remote_tx(PGconn *conn);
+ static void abort_remote_tx(PGconn *conn);
+
+ /*
+ * 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.
+ *
+ * When use_tx is true, remote transaction is started if caller is the only
+ * user of the connection. Isolation level of the remote transaction is same
+ * as local transaction, and remote transaction will be aborted when last
+ * user release.
+ *
+ * TODO: Note that caching connections requires a mechanism to detect change of
+ * FDW object to invalidate already established connections.
+ */
+ PGconn *
+ GetConnection(ForeignServer *server, UserMapping *user, bool use_tx)
+ {
+ bool found;
+ ConnCacheEntry *entry;
+ ConnCacheEntry key;
+
+ /* initialize connection cache if it isn't */
+ if (FSConnectionHash == NULL)
+ {
+ HASHCTL ctl;
+
+ /* hash key is a pair of oids: serverid and userid */
+ MemSet(&ctl, 0, sizeof(ctl));
+ ctl.keysize = sizeof(Oid) + sizeof(Oid);
+ ctl.entrysize = sizeof(ConnCacheEntry);
+ ctl.hash = tag_hash;
+ ctl.match = memcmp;
+ ctl.keycopy = memcpy;
+ /* allocate FSConnectionHash in the cache context */
+ ctl.hcxt = CacheMemoryContext;
+ FSConnectionHash = hash_create("Foreign Connections", 32,
+ &ctl,
+ HASH_ELEM | HASH_CONTEXT |
+ HASH_FUNCTION | HASH_COMPARE |
+ HASH_KEYCOPY);
+ }
+
+ /* Create key value for the entry. */
+ MemSet(&key, 0, sizeof(key));
+ key.serverid = server->serverid;
+ key.userid = GetOuterUserId();
+
+ /*
+ * Find cached entry for requested connection. If we couldn't find,
+ * callback function of ResourceOwner should be registered to clean the
+ * connection up on error including user interrupt.
+ */
+ entry = hash_search(FSConnectionHash, &key, HASH_ENTER, &found);
+ if (!found)
+ {
+ entry->refs = 0;
+ entry->use_tx = false;
+ entry->conn = NULL;
+ RegisterResourceReleaseCallback(cleanup_connection, entry);
+ }
+
+ /*
+ * We don't check the health of cached connection here, because it would
+ * require some overhead. Broken connection and its cache entry will be
+ * cleaned up when the connection is actually used.
+ */
+
+ /*
+ * If cache entry doesn't have connection, we have to establish new
+ * connection.
+ */
+ if (entry->conn == NULL)
+ {
+ /*
+ * Use PG_TRY block to ensure closing connection on error.
+ */
+ PG_TRY();
+ {
+ /*
+ * Connect to the foreign PostgreSQL server, and store it in cache
+ * entry to keep new connection.
+ * Note: key items of entry has already been initialized in
+ * hash_search(HASH_ENTER).
+ */
+ entry->conn = connect_pg_server(server, user);
+ }
+ PG_CATCH();
+ {
+ /* Clear connection cache entry on error case. */
+ PQfinish(entry->conn);
+ entry->refs = 0;
+ entry->use_tx = false;
+ entry->conn = NULL;
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+ }
+
+ /* Increase connection reference counter. */
+ entry->refs++;
+
+ /*
+ * If requester is the only referrer of this connection, start transaction
+ * with the same isolation level as the local transaction we are in.
+ * We need to remember whether this connection uses remote transaction to
+ * abort it when this connection is released completely.
+ */
+ if (use_tx && entry->refs == 1)
+ begin_remote_tx(entry->conn);
+ entry->use_tx = use_tx;
+
+
+ return entry->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 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. Those two object hold only libpq options.
+ * Extra 3 items are for:
+ * *) fallback_application_name
+ * *) client_encoding
+ * *) NULL termination (end marker)
+ *
+ * Note: We don't omit any parameters even target database might be older
+ * than local, because unexpected parameters are just ignored.
+ */
+ n = list_length(server->options) + list_length(user->options) + 3;
+ 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 += ExtractConnectionOptions(server->options,
+ all_keywords + n, all_values + n);
+ n += ExtractConnectionOptions(user->options,
+ all_keywords + n, all_values + n);
+ all_keywords[n] = all_values[n] = NULL;
+
+ for (i = 0, j = 0; all_keywords[i]; i++)
+ {
+ keywords[j] = all_keywords[i];
+ values[j] = all_values[i];
+ j++;
+ }
+
+ /* Use "pgsql_fdw" as fallback_application_name. */
+ keywords[j] = "fallback_application_name";
+ values[j++] = "pgsql_fdw";
+
+ /* Set client_encoding so that libpq can convert encoding properly. */
+ keywords[j] = "client_encoding";
+ values[j++] = GetDatabaseEncodingName();
+
+ keywords[j] = values[j] = NULL;
+ pfree(all_keywords);
+ pfree(all_values);
+
+ /* 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.")));
+ }
+
+ return conn;
+ }
+
+ /*
+ * Start transaction to use cursor to retrieve data separately.
+ */
+ static void
+ begin_remote_tx(PGconn *conn)
+ {
+ const char *sql = NULL; /* keep compiler quiet. */
+ PGresult *res;
+
+ switch (XactIsoLevel)
+ {
+ case XACT_READ_UNCOMMITTED:
+ case XACT_READ_COMMITTED:
+ case XACT_REPEATABLE_READ:
+ sql = "START TRANSACTION ISOLATION LEVEL REPEATABLE READ";
+ break;
+ case XACT_SERIALIZABLE:
+ sql = "START TRANSACTION ISOLATION LEVEL SERIALIZABLE";
+ break;
+ default:
+ elog(ERROR, "unexpected isolation level: %d", XactIsoLevel);
+ break;
+ }
+
+ elog(DEBUG3, "starting remote transaction with \"%s\"", sql);
+
+ res = PQexec(conn, sql);
+ if (PQresultStatus(res) != PGRES_COMMAND_OK)
+ {
+ PQclear(res);
+ elog(ERROR, "could not start transaction: %s", PQerrorMessage(conn));
+ }
+ PQclear(res);
+ }
+
+ static void
+ abort_remote_tx(PGconn *conn)
+ {
+ PGresult *res;
+
+ elog(DEBUG3, "aborting remote transaction");
+
+ res = PQexec(conn, "ABORT TRANSACTION");
+ if (PQresultStatus(res) != PGRES_COMMAND_OK)
+ {
+ PQclear(res);
+ elog(ERROR, "could not abort transaction: %s", PQerrorMessage(conn));
+ }
+ PQclear(res);
+ }
+
+ /*
+ * 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 sequentially 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;
+ }
+ if (entry != NULL)
+ hash_seq_term(&scan);
+
+ /*
+ * If this connection uses remote transaction and caller is the last user,
+ * abort remote transaction and forget about it.
+ */
+ if (entry->use_tx && entry->refs == 1)
+ {
+ abort_remote_tx(conn);
+ entry->use_tx = false;
+ }
+
+ /*
+ * If the released connection was an orphan, just close it.
+ */
+ if (entry == NULL)
+ {
+ PQfinish(conn);
+ return;
+ }
+
+ /*
+ * Decrease reference counter of this connection. Even if the caller was
+ * the last referrer, we don't unregister it from cache.
+ */
+ entry->refs--;
+ }
+
+ /*
+ * 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 need to clean up at end of subtransactions, because they might
+ * be recovered to consistent state with savepoints.
+ */
+ if (!isTopLevel)
+ 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)
+ {
+ PQfinish(entry->conn);
+ entry->refs = 0;
+ entry->conn = NULL;
+ }
+ }
+
+ /*
+ * Get list of connections currently active.
+ */
+ Datum pgsql_fdw_get_connections(PG_FUNCTION_ARGS);
+ PG_FUNCTION_INFO_V1(pgsql_fdw_get_connections);
+ Datum
+ pgsql_fdw_get_connections(PG_FUNCTION_ARGS)
+ {
+ ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+ HASH_SEQ_STATUS scan;
+ ConnCacheEntry *entry;
+ MemoryContext oldcontext = CurrentMemoryContext;
+ Tuplestorestate *tuplestore;
+ TupleDesc tupdesc;
+
+ /* We return list of connection with storing them in a Tuplestore. */
+ rsinfo->returnMode = SFRM_Materialize;
+ rsinfo->setResult = NULL;
+ rsinfo->setDesc = NULL;
+
+ /* Create tuplestore and copy of TupleDesc in per-query context. */
+ MemoryContextSwitchTo(rsinfo->econtext->ecxt_per_query_memory);
+
+ tupdesc = CreateTemplateTupleDesc(2, false);
+ TupleDescInitEntry(tupdesc, 1, "srvid", OIDOID, -1, 0);
+ TupleDescInitEntry(tupdesc, 2, "usesysid", OIDOID, -1, 0);
+ rsinfo->setDesc = tupdesc;
+
+ tuplestore = tuplestore_begin_heap(false, false, work_mem);
+ rsinfo->setResult = tuplestore;
+
+ MemoryContextSwitchTo(oldcontext);
+
+ /*
+ * We need to scan sequentially since we use the address to find
+ * appropriate PGconn from the hash table.
+ */
+ if (FSConnectionHash != NULL)
+ {
+ hash_seq_init(&scan, FSConnectionHash);
+ while ((entry = (ConnCacheEntry *) hash_seq_search(&scan)))
+ {
+ Datum values[2];
+ bool nulls[2];
+ HeapTuple tuple;
+
+ /* Ignore inactive connections */
+ if (PQstatus(entry->conn) != CONNECTION_OK)
+ continue;
+
+ /*
+ * Ignore other users' connections if current user isn't a
+ * superuser.
+ */
+ if (!superuser() && entry->userid != GetUserId())
+ continue;
+
+ values[0] = ObjectIdGetDatum(entry->serverid);
+ values[1] = ObjectIdGetDatum(entry->userid);
+ nulls[0] = false;
+ nulls[1] = false;
+
+ tuple = heap_formtuple(tupdesc, values, nulls);
+ tuplestore_puttuple(tuplestore, tuple);
+ }
+ }
+ tuplestore_donestoring(tuplestore);
+
+ PG_RETURN_VOID();
+ }
+
+ /*
+ * Discard persistent connection designated by given connection name.
+ */
+ Datum pgsql_fdw_disconnect(PG_FUNCTION_ARGS);
+ PG_FUNCTION_INFO_V1(pgsql_fdw_disconnect);
+ Datum
+ pgsql_fdw_disconnect(PG_FUNCTION_ARGS)
+ {
+ Oid serverid = PG_GETARG_OID(0);
+ Oid userid = PG_GETARG_OID(1);
+ ConnCacheEntry key;
+ ConnCacheEntry *entry = NULL;
+ bool found;
+
+ /* Non-superuser can't discard other users' connection. */
+ if (!superuser() && userid != GetOuterUserId())
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_RESOURCES),
+ errmsg("only superuser can discard other user's connection")));
+
+ /*
+ * If no connection has been established, or no such connections, just
+ * return "NG" to indicate nothing has done.
+ */
+ if (FSConnectionHash == NULL)
+ PG_RETURN_TEXT_P(cstring_to_text("NG"));
+
+ key.serverid = serverid;
+ key.userid = userid;
+ entry = hash_search(FSConnectionHash, &key, HASH_FIND, &found);
+ if (!found)
+ PG_RETURN_TEXT_P(cstring_to_text("NG"));
+
+ /* Discard cached connection, and clear reference counter. */
+ PQfinish(entry->conn);
+ entry->refs = 0;
+ entry->conn = NULL;
+
+ PG_RETURN_TEXT_P(cstring_to_text("OK"));
+ }
diff --git a/contrib/pgsql_fdw/connection.h b/contrib/pgsql_fdw/connection.h
index ...4427f56 .
*** a/contrib/pgsql_fdw/connection.h
--- b/contrib/pgsql_fdw/connection.h
***************
*** 0 ****
--- 1,25 ----
+ /*-------------------------------------------------------------------------
+ *
+ * connection.h
+ * Connection management for pgsql_fdw
+ *
+ * Portions Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_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, bool use_tx);
+ void ReleaseConnection(PGconn *conn);
+
+ #endif /* CONNECTION_H */
diff --git a/contrib/pgsql_fdw/deparse.c b/contrib/pgsql_fdw/deparse.c
index ...7ca2767 .
*** a/contrib/pgsql_fdw/deparse.c
--- b/contrib/pgsql_fdw/deparse.c
***************
*** 0 ****
--- 1,210 ----
+ /*-------------------------------------------------------------------------
+ *
+ * deparse.c
+ * query deparser for PostgreSQL
+ *
+ * Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/deparse.c
+ *
+ *-------------------------------------------------------------------------
+ */
+ #include "postgres.h"
+
+ #include "access/transam.h"
+ #include "foreign/foreign.h"
+ #include "lib/stringinfo.h"
+ #include "nodes/nodeFuncs.h"
+ #include "nodes/nodes.h"
+ #include "nodes/makefuncs.h"
+ #include "optimizer/clauses.h"
+ #include "optimizer/var.h"
+ #include "parser/parsetree.h"
+ #include "utils/builtins.h"
+ #include "utils/lsyscache.h"
+
+ #include "pgsql_fdw.h"
+
+ /*
+ * Context for walk-through the expression tree.
+ */
+ typedef struct foreign_executable_cxt
+ {
+ PlannerInfo *root;
+ RelOptInfo *foreignrel;
+ } foreign_executable_cxt;
+
+ /*
+ * Deparse query representation into SQL statement which suits for remote
+ * PostgreSQL server.
+ */
+ char *
+ deparseSql(Oid relid, PlannerInfo *root, RelOptInfo *baserel)
+ {
+ StringInfoData foreign_relname;
+ StringInfoData sql; /* builder for SQL statement */
+ bool first;
+ AttrNumber attr;
+ List *attr_used = NIL; /* List of AttNumber used in the query */
+ const char *nspname = NULL; /* plain namespace name */
+ const char *relname = NULL; /* plain relation name */
+ const char *q_nspname; /* quoted namespace name */
+ const char *q_relname; /* quoted relation name */
+ int i;
+ List *rtable = NIL;
+ List *context = NIL;
+
+ initStringInfo(&sql);
+ initStringInfo(&foreign_relname);
+
+ /*
+ * First of all, determine which column should be retrieved for this scan.
+ *
+ * We do this before deparsing SELECT clause because attributes which are
+ * not used in neither reltargetlist nor baserel->baserestrictinfo, quals
+ * evaluated on local, can be replaced with literal "NULL" in the SELECT
+ * clause to reduce overhead of tuple handling tuple and data transfer.
+ */
+ if (baserel->baserestrictinfo != NIL)
+ {
+ ListCell *lc;
+
+ foreach (lc, baserel->baserestrictinfo)
+ {
+ RestrictInfo *ri = (RestrictInfo *) lfirst(lc);
+ List *attrs;
+
+ /*
+ * We need to know which attributes are used in qual evaluated
+ * on the local server, because they should be listed in the
+ * SELECT clause of remote query. We can ignore attributes
+ * which are referenced only in ORDER BY/GROUP BY clause because
+ * such attributes has already been kept in reltargetlist.
+ */
+ attrs = pull_var_clause((Node *) ri->clause,
+ PVC_RECURSE_AGGREGATES,
+ PVC_RECURSE_PLACEHOLDERS);
+ attr_used = list_union(attr_used, attrs);
+ }
+ }
+
+ /*
+ * Determine foreign relation's qualified name. This is necessary for
+ * FROM clause and SELECT clause.
+ */
+ nspname = GetFdwOptionValue(InvalidOid, InvalidOid, relid,
+ InvalidAttrNumber, "nspname");
+ if (nspname == NULL)
+ nspname = get_namespace_name(get_rel_namespace(relid));
+ q_nspname = quote_identifier(nspname);
+
+ relname = GetFdwOptionValue(InvalidOid, InvalidOid, relid,
+ InvalidAttrNumber, "relname");
+ if (relname == NULL)
+ relname = get_rel_name(relid);
+ q_relname = quote_identifier(relname);
+
+ appendStringInfo(&foreign_relname, "%s.%s", q_nspname, q_relname);
+
+ /*
+ * We need to replace aliasname and colnames of the target relation so that
+ * constructed remote query is valid.
+ *
+ * Note that we skip first empty element of simple_rel_array. See also
+ * comments of simple_rel_array and simple_rte_array for the rationale.
+ */
+ for (i = 1; i < root->simple_rel_array_size; i++)
+ {
+ RangeTblEntry *rte = copyObject(root->simple_rte_array[i]);
+ List *newcolnames = NIL;
+
+ if (i == baserel->relid)
+ {
+ /*
+ * Create new list of column names which is used to deparse remote
+ * query from specified names or local column names. This list is
+ * used by deparse_expression.
+ */
+ for (attr = 1; attr <= baserel->max_attr; attr++)
+ {
+ char *colname = NULL;
+
+ /*
+ * Get colname option for each undropped attribute. If colname
+ * option is not supplied, use local column name in remote
+ * query.
+ */
+ if (get_rte_attribute_is_dropped(rte, attr))
+ colname = "";
+ else
+ {
+ colname = GetFdwOptionValue(InvalidOid, InvalidOid, relid,
+ attr, "colname");
+ if (colname == NULL)
+ colname = strVal(list_nth(rte->eref->colnames,
+ attr - 1));
+ }
+ newcolnames = lappend(newcolnames, makeString(colname));
+ }
+ rte->alias = makeAlias(relname, newcolnames);
+ }
+ rtable = lappend(rtable, rte);
+ }
+ context = deparse_context_for_rtelist(rtable);
+
+ /*
+ * deparse SELECT clause
+ *
+ * List attributes which are in either target list or local restriction.
+ * Unused attributes are replaced with a literal "NULL" for optimization.
+ *
+ * Note that nothing is added for dropped columns, though tuple constructor
+ * function requires entries for dropped columns. Such entries must be
+ * initialized with NULL before calling tuple constructor.
+ */
+ appendStringInfo(&sql, "SELECT ");
+ attr_used = list_union(attr_used, baserel->reltargetlist);
+ first = true;
+ for (attr = 1; attr <= baserel->max_attr; attr++)
+ {
+ RangeTblEntry *rte = root->simple_rte_array[baserel->relid];
+ Var *var = NULL;
+ ListCell *lc;
+
+ /* Ignore dropped attributes. */
+ if (get_rte_attribute_is_dropped(rte, attr))
+ continue;
+
+ if (!first)
+ appendStringInfo(&sql, ", ");
+ first = false;
+
+ /*
+ * We use linear search here, but it wouldn't be problem since
+ * attr_used seems to not become so large.
+ */
+ foreach (lc, attr_used)
+ {
+ var = lfirst(lc);
+ if (var->varattno == attr)
+ break;
+ var = NULL;
+ }
+ if (var != NULL)
+ appendStringInfo(&sql, "%s",
+ deparse_expression((Node *) var, context, false, false));
+ else
+ appendStringInfo(&sql, "NULL");
+ }
+ appendStringInfoChar(&sql, ' ');
+
+ /*
+ * deparse FROM clause, including alias if any
+ */
+ appendStringInfo(&sql, "FROM %s", foreign_relname.data);
+
+ elog(DEBUG3, "Remote SQL: %s", sql.data);
+ return sql.data;
+ }
+
diff --git a/contrib/pgsql_fdw/expected/pgsql_fdw.out b/contrib/pgsql_fdw/expected/pgsql_fdw.out
index ...c1e2e82 .
*** a/contrib/pgsql_fdw/expected/pgsql_fdw.out
--- b/contrib/pgsql_fdw/expected/pgsql_fdw.out
***************
*** 0 ****
--- 1,592 ----
+ -- ===================================================================
+ -- create FDW objects
+ -- ===================================================================
+ -- Clean up in case a prior regression run failed
+ -- Suppress NOTICE messages when roles don't exist
+ SET client_min_messages TO 'error';
+ DROP ROLE IF EXISTS pgsql_fdw_user;
+ RESET client_min_messages;
+ CREATE ROLE pgsql_fdw_user LOGIN SUPERUSER;
+ SET SESSION AUTHORIZATION 'pgsql_fdw_user';
+ CREATE EXTENSION pgsql_fdw;
+ CREATE SERVER loopback1 FOREIGN DATA WRAPPER pgsql_fdw;
+ CREATE SERVER loopback2 FOREIGN DATA WRAPPER pgsql_fdw
+ OPTIONS (dbname 'contrib_regression');
+ CREATE USER MAPPING FOR public SERVER loopback1
+ OPTIONS (user 'value', password 'value');
+ CREATE USER MAPPING FOR pgsql_fdw_user SERVER loopback2;
+ CREATE FOREIGN TABLE ft1 (
+ c0 int,
+ c1 int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10)
+ ) SERVER loopback2;
+ ALTER FOREIGN TABLE ft1 DROP COLUMN c0;
+ CREATE FOREIGN TABLE ft2 (
+ c0 int,
+ c1 int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10)
+ ) SERVER loopback2;
+ ALTER FOREIGN TABLE ft2 DROP COLUMN c0;
+ -- ===================================================================
+ -- create objects used through FDW
+ -- ===================================================================
+ CREATE SCHEMA "S 1";
+ CREATE TABLE "S 1"."T 1" (
+ "C 1" int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10),
+ CONSTRAINT t1_pkey PRIMARY KEY ("C 1")
+ );
+ NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "t1_pkey" for table "T 1"
+ CREATE TABLE "S 1"."T 2" (
+ c1 int NOT NULL,
+ c2 text,
+ CONSTRAINT t2_pkey PRIMARY KEY (c1)
+ );
+ NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "t2_pkey" for table "T 2"
+ BEGIN;
+ TRUNCATE "S 1"."T 1";
+ INSERT INTO "S 1"."T 1"
+ SELECT id,
+ id % 10,
+ to_char(id, 'FM00000'),
+ '1970-01-01'::timestamptz + ((id % 100) || ' days')::interval,
+ '1970-01-01'::timestamp + ((id % 100) || ' days')::interval,
+ id % 10,
+ id % 10
+ FROM generate_series(1, 1000) id;
+ TRUNCATE "S 1"."T 2";
+ INSERT INTO "S 1"."T 2"
+ SELECT id,
+ 'AAA' || to_char(id, 'FM000')
+ FROM generate_series(1, 100) id;
+ COMMIT;
+ -- ===================================================================
+ -- tests for pgsql_fdw_validator
+ -- ===================================================================
+ ALTER FOREIGN DATA WRAPPER pgsql_fdw OPTIONS (host 'value'); -- ERROR
+ ERROR: invalid option "host"
+ HINT: Valid options in this context are:
+ -- requiressl, krbsrvname and gsslib are omitted because they depend on
+ -- configure option
+ ALTER SERVER loopback1 OPTIONS (
+ authtype 'value',
+ service 'value',
+ connect_timeout 'value',
+ dbname 'value',
+ host 'value',
+ hostaddr 'value',
+ port 'value',
+ --client_encoding 'value',
+ tty 'value',
+ options 'value',
+ application_name 'value',
+ --fallback_application_name 'value',
+ keepalives 'value',
+ keepalives_idle 'value',
+ keepalives_interval 'value',
+ -- requiressl 'value',
+ sslcompression 'value',
+ sslmode 'value',
+ sslcert 'value',
+ sslkey 'value',
+ sslrootcert 'value',
+ sslcrl 'value'
+ --requirepeer 'value',
+ -- krbsrvname 'value',
+ -- gsslib 'value',
+ --replication 'value'
+ );
+ ALTER SERVER loopback1 OPTIONS (user 'value'); -- ERROR
+ ERROR: invalid option "user"
+ HINT: Valid options in this context are: authtype, service, connect_timeout, dbname, host, hostaddr, port, tty, options, application_name, keepalives, keepalives_idle, keepalives_interval, keepalives_count, requiressl, sslcompression, sslmode, sslcert, sslkey, sslrootcert, sslcrl, requirepeer, krbsrvname, gsslib, fetch_count
+ ALTER SERVER loopback2 OPTIONS (ADD fetch_count '2');
+ ALTER USER MAPPING FOR public SERVER loopback1
+ OPTIONS (DROP user, DROP password);
+ ALTER USER MAPPING FOR public SERVER loopback1
+ OPTIONS (host 'value'); -- ERROR
+ ERROR: invalid option "host"
+ HINT: Valid options in this context are: user, password
+ ALTER FOREIGN TABLE ft1 OPTIONS (nspname 'S 1', relname 'T 1');
+ ALTER FOREIGN TABLE ft2 OPTIONS (nspname 'S 1', relname 'T 1', fetch_count '100');
+ ALTER FOREIGN TABLE ft1 OPTIONS (invalid 'value'); -- ERROR
+ ERROR: invalid option "invalid"
+ HINT: Valid options in this context are: nspname, relname, fetch_count
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count 'a'); -- ERROR
+ ERROR: invalid value for fetch_count: "a"
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count '0'); -- ERROR
+ ERROR: invalid value for fetch_count: "0"
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count '-1'); -- ERROR
+ ERROR: invalid value for fetch_count: "-1"
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (invalid 'value'); -- ERROR
+ ERROR: invalid option "invalid"
+ HINT: Valid options in this context are: colname
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (colname 'C 1');
+ ALTER FOREIGN TABLE ft2 ALTER COLUMN c1 OPTIONS (colname 'C 1');
+ \dew+
+ List of foreign-data wrappers
+ Name | Owner | Handler | Validator | Access privileges | FDW Options | Description
+ -----------+----------------+-------------------+---------------------+-------------------+-------------+-------------
+ pgsql_fdw | pgsql_fdw_user | pgsql_fdw_handler | pgsql_fdw_validator | | |
+ (1 row)
+
+ \des+
+ List of foreign servers
+ Name | Owner | Foreign-data wrapper | Access privileges | Type | Version | FDW Options | Description
+ -----------+----------------+----------------------+-------------------+------+---------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+-------------
+ loopback1 | pgsql_fdw_user | pgsql_fdw | | | | (authtype 'value', service 'value', connect_timeout 'value', dbname 'value', host 'value', hostaddr 'value', port 'value', tty 'value', options 'value', application_name 'value', keepalives 'value', keepalives_idle 'value', keepalives_interval 'value', sslcompression 'value', sslmode 'value', sslcert 'value', sslkey 'value', sslrootcert 'value', sslcrl 'value') |
+ loopback2 | pgsql_fdw_user | pgsql_fdw | | | | (dbname 'contrib_regression', fetch_count '2') |
+ (2 rows)
+
+ \deu+
+ List of user mappings
+ Server | User name | FDW Options
+ -----------+----------------+-------------
+ loopback1 | public |
+ loopback2 | pgsql_fdw_user |
+ (2 rows)
+
+ \det+
+ List of foreign tables
+ Schema | Table | Server | FDW Options | Description
+ --------+-------+-----------+---------------------------------------------------+-------------
+ public | ft1 | loopback2 | (nspname 'S 1', relname 'T 1') |
+ public | ft2 | loopback2 | (nspname 'S 1', relname 'T 1', fetch_count '100') |
+ (2 rows)
+
+ -- ===================================================================
+ -- simple queries
+ -- ===================================================================
+ -- single table, with/without alias
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
+ QUERY PLAN
+ ------------------------------------------------------------------------------------------------------------------------------
+ Limit
+ Output: c1, c2, c3, c4, c5, c6, c7
+ -> Sort
+ Output: c1, c2, c3, c4, c5, c6, c7
+ Sort Key: ft1.c3, ft1.c1
+ -> Foreign Scan on public.ft1
+ Output: c1, c2, c3, c4, c5, c6, c7
+ Remote SQL: DECLARE pgsql_fdw_cursor_0 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (8 rows)
+
+ SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ -----+----+-------+------------------------------+--------------------------+----+------------
+ 101 | 1 | 00101 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970 | 1 | 1
+ 102 | 2 | 00102 | Sat Jan 03 00:00:00 1970 PST | Sat Jan 03 00:00:00 1970 | 2 | 2
+ 103 | 3 | 00103 | Sun Jan 04 00:00:00 1970 PST | Sun Jan 04 00:00:00 1970 | 3 | 3
+ 104 | 4 | 00104 | Mon Jan 05 00:00:00 1970 PST | Mon Jan 05 00:00:00 1970 | 4 | 4
+ 105 | 5 | 00105 | Tue Jan 06 00:00:00 1970 PST | Tue Jan 06 00:00:00 1970 | 5 | 5
+ 106 | 6 | 00106 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6
+ 107 | 7 | 00107 | Thu Jan 08 00:00:00 1970 PST | Thu Jan 08 00:00:00 1970 | 7 | 7
+ 108 | 8 | 00108 | Fri Jan 09 00:00:00 1970 PST | Fri Jan 09 00:00:00 1970 | 8 | 8
+ 109 | 9 | 00109 | Sat Jan 10 00:00:00 1970 PST | Sat Jan 10 00:00:00 1970 | 9 | 9
+ 110 | 0 | 00110 | Sun Jan 11 00:00:00 1970 PST | Sun Jan 11 00:00:00 1970 | 0 | 0
+ (10 rows)
+
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+ QUERY PLAN
+ ------------------------------------------------------------------------------------------------------------------------------
+ Limit
+ Output: c1, c2, c3, c4, c5, c6, c7
+ -> Sort
+ Output: c1, c2, c3, c4, c5, c6, c7
+ Sort Key: t1.c3, t1.c1
+ -> Foreign Scan on public.ft1 t1
+ Output: c1, c2, c3, c4, c5, c6, c7
+ Remote SQL: DECLARE pgsql_fdw_cursor_2 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (8 rows)
+
+ SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ -----+----+-------+------------------------------+--------------------------+----+------------
+ 101 | 1 | 00101 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970 | 1 | 1
+ 102 | 2 | 00102 | Sat Jan 03 00:00:00 1970 PST | Sat Jan 03 00:00:00 1970 | 2 | 2
+ 103 | 3 | 00103 | Sun Jan 04 00:00:00 1970 PST | Sun Jan 04 00:00:00 1970 | 3 | 3
+ 104 | 4 | 00104 | Mon Jan 05 00:00:00 1970 PST | Mon Jan 05 00:00:00 1970 | 4 | 4
+ 105 | 5 | 00105 | Tue Jan 06 00:00:00 1970 PST | Tue Jan 06 00:00:00 1970 | 5 | 5
+ 106 | 6 | 00106 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6
+ 107 | 7 | 00107 | Thu Jan 08 00:00:00 1970 PST | Thu Jan 08 00:00:00 1970 | 7 | 7
+ 108 | 8 | 00108 | Fri Jan 09 00:00:00 1970 PST | Fri Jan 09 00:00:00 1970 | 8 | 8
+ 109 | 9 | 00109 | Sat Jan 10 00:00:00 1970 PST | Sat Jan 10 00:00:00 1970 | 9 | 9
+ 110 | 0 | 00110 | Sun Jan 11 00:00:00 1970 PST | Sun Jan 11 00:00:00 1970 | 0 | 0
+ (10 rows)
+
+ -- with WHERE clause
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
+ QUERY PLAN
+ ------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on public.ft1 t1
+ Output: c1, c2, c3, c4, c5, c6, c7
+ Filter: ((t1.c1 = 101) AND ((t1.c6)::text = '1'::text) AND (t1.c7 = '1'::bpchar))
+ Remote SQL: DECLARE pgsql_fdw_cursor_4 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (4 rows)
+
+ SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ -----+----+-------+------------------------------+--------------------------+----+------------
+ 101 | 1 | 00101 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970 | 1 | 1
+ (1 row)
+
+ -- aggregate
+ SELECT COUNT(*) FROM ft1 t1;
+ count
+ -------
+ 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
+ ----+----+-------+------------------------------+--------------------------+----+------------
+ 1 | 1 | 00001 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970 | 1 | 1
+ 2 | 2 | 00002 | Sat Jan 03 00:00:00 1970 PST | Sat Jan 03 00:00:00 1970 | 2 | 2
+ 3 | 3 | 00003 | Sun Jan 04 00:00:00 1970 PST | Sun Jan 04 00:00:00 1970 | 3 | 3
+ 4 | 4 | 00004 | Mon Jan 05 00:00:00 1970 PST | Mon Jan 05 00:00:00 1970 | 4 | 4
+ 5 | 5 | 00005 | Tue Jan 06 00:00:00 1970 PST | Tue Jan 06 00:00:00 1970 | 5 | 5
+ 6 | 6 | 00006 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6
+ 7 | 7 | 00007 | Thu Jan 08 00:00:00 1970 PST | Thu Jan 08 00:00:00 1970 | 7 | 7
+ 8 | 8 | 00008 | Fri Jan 09 00:00:00 1970 PST | Fri Jan 09 00:00:00 1970 | 8 | 8
+ 9 | 9 | 00009 | Sat Jan 10 00:00:00 1970 PST | Sat Jan 10 00:00:00 1970 | 9 | 9
+ 10 | 0 | 00010 | Sun Jan 11 00:00:00 1970 PST | Sun Jan 11 00:00:00 1970 | 0 | 0
+ (10 rows)
+
+ -- subquery+MAX
+ SELECT * FROM ft1 t1 WHERE t1.c3 = (SELECT MAX(c3) FROM ft2 t2) ORDER BY c1;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ ------+----+-------+------------------------------+--------------------------+----+------------
+ 1000 | 0 | 01000 | Thu Jan 01 00:00:00 1970 PST | Thu Jan 01 00:00:00 1970 | 0 | 0
+ (1 row)
+
+ -- used in CTE
+ WITH t1 AS (SELECT * FROM ft1 WHERE c1 <= 10) SELECT t2.c1, t2.c2, t2.c3, t2.c4 FROM t1, ft2 t2 WHERE t1.c1 = t2.c1 ORDER BY t1.c1;
+ c1 | c2 | c3 | c4
+ ----+----+-------+------------------------------
+ 1 | 1 | 00001 | Fri Jan 02 00:00:00 1970 PST
+ 2 | 2 | 00002 | Sat Jan 03 00:00:00 1970 PST
+ 3 | 3 | 00003 | Sun Jan 04 00:00:00 1970 PST
+ 4 | 4 | 00004 | Mon Jan 05 00:00:00 1970 PST
+ 5 | 5 | 00005 | Tue Jan 06 00:00:00 1970 PST
+ 6 | 6 | 00006 | Wed Jan 07 00:00:00 1970 PST
+ 7 | 7 | 00007 | Thu Jan 08 00:00:00 1970 PST
+ 8 | 8 | 00008 | Fri Jan 09 00:00:00 1970 PST
+ 9 | 9 | 00009 | Sat Jan 10 00:00:00 1970 PST
+ 10 | 0 | 00010 | Sun Jan 11 00:00:00 1970 PST
+ (10 rows)
+
+ -- fixed values
+ SELECT 'fixed', NULL FROM ft1 t1 WHERE c1 = 1;
+ ?column? | ?column?
+ ----------+----------
+ fixed |
+ (1 row)
+
+ -- user-defined operator/function
+ CREATE FUNCTION pgsql_fdw_abs(int) RETURNS int AS $$
+ BEGIN
+ RETURN abs($1);
+ END
+ $$ LANGUAGE plpgsql IMMUTABLE;
+ CREATE OPERATOR === (
+ LEFTARG = int,
+ RIGHTARG = int,
+ PROCEDURE = int4eq,
+ COMMUTATOR = ===,
+ NEGATOR = !==
+ );
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = pgsql_fdw_abs(t1.c2);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = pgsql_fdw_abs(c2))
+ Remote SQL: DECLARE pgsql_fdw_cursor_18 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 === t1.c2;
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 === c2)
+ Remote SQL: DECLARE pgsql_fdw_cursor_19 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = abs(t1.c2);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = abs(c2))
+ Remote SQL: DECLARE pgsql_fdw_cursor_20 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = t1.c2;
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = c2)
+ Remote SQL: DECLARE pgsql_fdw_cursor_21 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ -- ===================================================================
+ -- parameterized queries
+ -- ===================================================================
+ -- simple join
+ PREPARE st1(int, int) AS SELECT t1.c3, t2.c3 FROM ft1 t1, ft2 t2 WHERE t1.c1 = $1 AND t2.c1 = $2;
+ EXPLAIN (COSTS false) EXECUTE st1(1, 2);
+ QUERY PLAN
+ -----------------------------------------------------------------------------------------------------------------------------------------
+ Nested Loop
+ -> Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_22 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, NULL, NULL, NULL, NULL FROM "S 1"."T 1"
+ -> Materialize
+ -> Foreign Scan on ft2 t2
+ Filter: (c1 = 2)
+ Remote SQL: DECLARE pgsql_fdw_cursor_23 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, NULL, NULL, NULL, NULL FROM "S 1"."T 1"
+ (8 rows)
+
+ EXECUTE st1(1, 1);
+ c3 | c3
+ -------+-------
+ 00001 | 00001
+ (1 row)
+
+ EXECUTE st1(101, 101);
+ c3 | c3
+ -------+-------
+ 00101 | 00101
+ (1 row)
+
+ -- subquery using stable function (can't be pushed down)
+ PREPARE st2(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c4) = 6) ORDER BY c1;
+ EXPLAIN (COSTS false) EXECUTE st2(10, 20);
+ QUERY PLAN
+ ---------------------------------------------------------------------------------------------------------------------------------------------------
+ Sort
+ Sort Key: t1.c1
+ -> Hash Join
+ Hash Cond: (t1.c3 = t2.c3)
+ -> Foreign Scan on ft1 t1
+ Filter: (c1 < 20)
+ Remote SQL: DECLARE pgsql_fdw_cursor_28 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ -> Hash
+ -> HashAggregate
+ -> Foreign Scan on ft2 t2
+ Filter: ((c1 > 10) AND (date_part('dow'::text, c4) = 6::double precision))
+ Remote SQL: DECLARE pgsql_fdw_cursor_29 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, c4, NULL, NULL, NULL FROM "S 1"."T 1"
+ (12 rows)
+
+ EXECUTE st2(10, 20);
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ ----+----+-------+------------------------------+--------------------------+----+------------
+ 16 | 6 | 00016 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970 | 6 | 6
+ (1 row)
+
+ EXECUTE st1(101, 101);
+ c3 | c3
+ -------+-------
+ 00101 | 00101
+ (1 row)
+
+ -- subquery using immutable function (can be pushed down)
+ PREPARE st3(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c5) = 6) ORDER BY c1;
+ EXPLAIN (COSTS false) EXECUTE st3(10, 20);
+ QUERY PLAN
+ ---------------------------------------------------------------------------------------------------------------------------------------------------
+ Sort
+ Sort Key: t1.c1
+ -> Hash Join
+ Hash Cond: (t1.c3 = t2.c3)
+ -> Foreign Scan on ft1 t1
+ Filter: (c1 < 20)
+ Remote SQL: DECLARE pgsql_fdw_cursor_34 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ -> Hash
+ -> HashAggregate
+ -> Foreign Scan on ft2 t2
+ Filter: ((c1 > 10) AND (date_part('dow'::text, c5) = 6::double precision))
+ Remote SQL: DECLARE pgsql_fdw_cursor_35 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, NULL, c5, NULL, NULL FROM "S 1"."T 1"
+ (12 rows)
+
+ EXECUTE st3(10, 20);
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ ----+----+-------+------------------------------+--------------------------+----+------------
+ 16 | 6 | 00016 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970 | 6 | 6
+ (1 row)
+
+ EXECUTE st3(20, 30);
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ ----+----+-------+------------------------------+--------------------------+----+------------
+ 23 | 3 | 00023 | Sat Jan 24 00:00:00 1970 PST | Sat Jan 24 00:00:00 1970 | 3 | 3
+ (1 row)
+
+ -- custom plan should be chosen
+ PREPARE st4(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 = $1;
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_40 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_41 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_42 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_43 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_44 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = $1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_45 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ -- cleanup
+ DEALLOCATE st1;
+ DEALLOCATE st2;
+ DEALLOCATE st3;
+ DEALLOCATE st4;
+ -- ===================================================================
+ -- connection management
+ -- ===================================================================
+ SELECT srvname, usename FROM pgsql_fdw_connections;
+ srvname | usename
+ -----------+----------------
+ loopback2 | pgsql_fdw_user
+ (1 row)
+
+ SELECT pgsql_fdw_disconnect(srvid, usesysid) FROM pgsql_fdw_get_connections();
+ pgsql_fdw_disconnect
+ ----------------------
+ OK
+ (1 row)
+
+ SELECT srvname, usename FROM pgsql_fdw_connections;
+ srvname | usename
+ ---------+---------
+ (0 rows)
+
+ -- ===================================================================
+ -- subtransaction
+ -- ===================================================================
+ BEGIN;
+ DECLARE c CURSOR FOR SELECT * FROM ft1 ORDER BY c1;
+ FETCH c;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ ----+----+-------+------------------------------+--------------------------+----+------------
+ 1 | 1 | 00001 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970 | 1 | 1
+ (1 row)
+
+ SAVEPOINT s;
+ ERROR OUT; -- ERROR
+ ERROR: syntax error at or near "ERROR"
+ LINE 1: ERROR OUT;
+ ^
+ ROLLBACK TO s;
+ SELECT srvname FROM pgsql_fdw_connections;
+ srvname
+ -----------
+ loopback2
+ (1 row)
+
+ FETCH c;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ ----+----+-------+------------------------------+--------------------------+----+------------
+ 2 | 2 | 00002 | Sat Jan 03 00:00:00 1970 PST | Sat Jan 03 00:00:00 1970 | 2 | 2
+ (1 row)
+
+ COMMIT;
+ SELECT srvname FROM pgsql_fdw_connections;
+ srvname
+ -----------
+ loopback2
+ (1 row)
+
+ ERROR OUT; -- ERROR
+ ERROR: syntax error at or near "ERROR"
+ LINE 1: ERROR OUT;
+ ^
+ SELECT srvname FROM pgsql_fdw_connections;
+ srvname
+ ---------
+ (0 rows)
+
+ -- ===================================================================
+ -- cleanup
+ -- ===================================================================
+ DROP OPERATOR === (int, int) CASCADE;
+ DROP OPERATOR !== (int, int) CASCADE;
+ DROP FUNCTION pgsql_fdw_abs(int);
+ DROP SCHEMA "S 1" CASCADE;
+ NOTICE: drop cascades to 2 other objects
+ DETAIL: drop cascades to table "S 1"."T 1"
+ drop cascades to table "S 1"."T 2"
+ DROP EXTENSION pgsql_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 server loopback2
+ drop cascades to user mapping for pgsql_fdw_user
+ drop cascades to foreign table ft1
+ drop cascades to foreign table ft2
+ \c
+ DROP ROLE pgsql_fdw_user;
diff --git a/contrib/pgsql_fdw/option.c b/contrib/pgsql_fdw/option.c
index ...13035c1 .
*** a/contrib/pgsql_fdw/option.c
--- b/contrib/pgsql_fdw/option.c
***************
*** 0 ****
--- 1,241 ----
+ /*-------------------------------------------------------------------------
+ *
+ * option.c
+ * FDW option handling
+ *
+ * Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/option.c
+ *
+ *-------------------------------------------------------------------------
+ */
+ #include "postgres.h"
+
+ #include "access/reloptions.h"
+ #include "catalog/pg_foreign_data_wrapper.h"
+ #include "catalog/pg_foreign_server.h"
+ #include "catalog/pg_foreign_table.h"
+ #include "catalog/pg_user_mapping.h"
+ #include "fmgr.h"
+ #include "foreign/foreign.h"
+ #include "lib/stringinfo.h"
+ #include "miscadmin.h"
+
+ #include "pgsql_fdw.h"
+
+ /*
+ * SQL functions
+ */
+ extern Datum pgsql_fdw_validator(PG_FUNCTION_ARGS);
+ PG_FUNCTION_INFO_V1(pgsql_fdw_validator);
+
+ /*
+ * Describes the valid options for objects that use this wrapper.
+ */
+ typedef struct PgsqlFdwOption
+ {
+ const char *optname;
+ Oid optcontext; /* Oid of catalog in which options may appear */
+ bool is_libpq_opt; /* true if it's used in libpq */
+ } PgsqlFdwOption;
+
+ /*
+ * Valid options for pgsql_fdw.
+ */
+ static PgsqlFdwOption valid_options[] = {
+
+ /*
+ * Options for libpq connection.
+ * Note: This list should be updated along with PQconninfoOptions in
+ * interfaces/libpq/fe-connect.c, so the order is kept as is.
+ *
+ * Some useless libpq connection options are not accepted by pgsql_fdw:
+ * client_encoding: set to local database encoding automatically
+ * fallback_application_name: fixed to "pgsql_fdw"
+ * replication: pgsql_fdw never be replication client
+ */
+ {"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},
+ #ifdef NOT_USED
+ {"client_encoding", ForeignServerRelationId, true},
+ #endif
+ {"tty", ForeignServerRelationId, true},
+ {"options", ForeignServerRelationId, true},
+ {"application_name", ForeignServerRelationId, true},
+ #ifdef NOT_USED
+ {"fallback_application_name", ForeignServerRelationId, true},
+ #endif
+ {"keepalives", ForeignServerRelationId, true},
+ {"keepalives_idle", ForeignServerRelationId, true},
+ {"keepalives_interval", ForeignServerRelationId, true},
+ {"keepalives_count", ForeignServerRelationId, true},
+ {"requiressl", ForeignServerRelationId, true},
+ {"sslcompression", ForeignServerRelationId, true},
+ {"sslmode", ForeignServerRelationId, true},
+ {"sslcert", ForeignServerRelationId, true},
+ {"sslkey", ForeignServerRelationId, true},
+ {"sslrootcert", ForeignServerRelationId, true},
+ {"sslcrl", ForeignServerRelationId, true},
+ {"requirepeer", ForeignServerRelationId, true},
+ {"krbsrvname", ForeignServerRelationId, true},
+ {"gsslib", ForeignServerRelationId, true},
+ #ifdef NOT_USED
+ {"replication", ForeignServerRelationId, true},
+ #endif
+
+ /*
+ * Options for translation of object names.
+ */
+ {"nspname", ForeignTableRelationId, false},
+ {"relname", ForeignTableRelationId, false},
+ {"colname", AttributeRelationId, false},
+
+ /*
+ * Options for cursor behavior.
+ * These options can be overridden by finer-grained objects.
+ */
+ {"fetch_count", ForeignTableRelationId, false},
+ {"fetch_count", ForeignServerRelationId, false},
+
+ /* Terminating entry --- MUST BE LAST */
+ {NULL, InvalidOid, false}
+ };
+
+ /*
+ * Helper functions
+ */
+ static bool is_valid_option(const char *optname, Oid context);
+
+ /*
+ * Validate the generic options given to a FOREIGN DATA WRAPPER, SERVER,
+ * USER MAPPING or FOREIGN TABLE that uses pgsql_fdw.
+ *
+ * Raise an ERROR if the option or its value is considered invalid.
+ */
+ Datum
+ pgsql_fdw_validator(PG_FUNCTION_ARGS)
+ {
+ List *options_list = untransformRelOptions(PG_GETARG_DATUM(0));
+ Oid catalog = PG_GETARG_OID(1);
+ ListCell *cell;
+
+ /*
+ * Check that only options supported by pgsql_fdw, and allowed for the
+ * current object type, are given.
+ */
+ foreach(cell, options_list)
+ {
+ DefElem *def = (DefElem *) lfirst(cell);
+
+ if (!is_valid_option(def->defname, catalog))
+ {
+ PgsqlFdwOption *opt;
+ StringInfoData buf;
+
+ /*
+ * Unknown option specified, complain about it. Provide a hint
+ * with list of valid options for the object.
+ */
+ initStringInfo(&buf);
+ for (opt = valid_options; opt->optname; opt++)
+ {
+ if (catalog == opt->optcontext)
+ appendStringInfo(&buf, "%s%s", (buf.len > 0) ? ", " : "",
+ opt->optname);
+ }
+
+ ereport(ERROR,
+ (errcode(ERRCODE_FDW_INVALID_OPTION_NAME),
+ errmsg("invalid option \"%s\"", def->defname),
+ errhint("Valid options in this context are: %s",
+ buf.data)));
+ }
+
+ /* fetch_count be positive digit number. */
+ if (strcmp(def->defname, "fetch_count") == 0)
+ {
+ long value;
+ char *p = NULL;
+
+ value = strtol(strVal(def->arg), &p, 10);
+ if (*p != '\0' || value < 1)
+ ereport(ERROR,
+ (errcode(ERRCODE_FDW_INVALID_ATTRIBUTE_VALUE),
+ errmsg("invalid value for %s: \"%s\"",
+ def->defname, strVal(def->arg))));
+ }
+ }
+
+ /*
+ * We don't care option-specific limitation here; they will be validated at
+ * the execution time.
+ */
+
+ PG_RETURN_VOID();
+ }
+
+ /*
+ * Check whether the given option is one of the valid pgsql_fdw options.
+ * context is the Oid of the catalog holding the object the option is for.
+ */
+ static bool
+ is_valid_option(const char *optname, Oid context)
+ {
+ PgsqlFdwOption *opt;
+
+ for (opt = valid_options; opt->optname; opt++)
+ {
+ if (context == opt->optcontext && strcmp(opt->optname, optname) == 0)
+ return true;
+ }
+ return false;
+ }
+
+ /*
+ * Check whether the given option is one of the valid libpq options.
+ * context is the Oid of the catalog holding the object the option is for.
+ */
+ static bool
+ is_libpq_option(const char *optname)
+ {
+ PgsqlFdwOption *opt;
+
+ for (opt = valid_options; opt->optname; opt++)
+ {
+ if (strcmp(opt->optname, optname) == 0 && opt->is_libpq_opt)
+ return true;
+ }
+ return false;
+ }
+
+ /*
+ * Generate key-value arrays which includes only libpq options from the list
+ * which contains any kind of options.
+ */
+ int
+ ExtractConnectionOptions(List *defelems, const char **keywords, const char **values)
+ {
+ ListCell *lc;
+ int i;
+
+ i = 0;
+ foreach(lc, defelems)
+ {
+ DefElem *d = (DefElem *) lfirst(lc);
+ if (is_libpq_option(d->defname))
+ {
+ keywords[i] = d->defname;
+ values[i] = strVal(d->arg);
+ i++;
+ }
+ }
+ return i;
+ }
diff --git a/contrib/pgsql_fdw/pgsql_fdw--1.0.sql b/contrib/pgsql_fdw/pgsql_fdw--1.0.sql
index ...b0ea2b2 .
*** a/contrib/pgsql_fdw/pgsql_fdw--1.0.sql
--- b/contrib/pgsql_fdw/pgsql_fdw--1.0.sql
***************
*** 0 ****
--- 1,39 ----
+ /* contrib/pgsql_fdw/pgsql_fdw--1.0.sql */
+
+ -- complain if script is sourced in psql, rather than via CREATE EXTENSION
+ \echo Use "CREATE EXTENSION pgsql_fdw" to load this file. \quit
+
+ CREATE FUNCTION pgsql_fdw_handler()
+ RETURNS fdw_handler
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C STRICT;
+
+ CREATE FUNCTION pgsql_fdw_validator(text[], oid)
+ RETURNS void
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C STRICT;
+
+ CREATE FOREIGN DATA WRAPPER pgsql_fdw
+ HANDLER pgsql_fdw_handler
+ VALIDATOR pgsql_fdw_validator;
+
+ /* connection management functions and view */
+ CREATE FUNCTION pgsql_fdw_get_connections(out srvid oid, out usesysid oid)
+ RETURNS SETOF record
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C STRICT;
+
+ CREATE FUNCTION pgsql_fdw_disconnect(oid, oid)
+ RETURNS text
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C STRICT;
+
+ CREATE VIEW pgsql_fdw_connections AS
+ SELECT c.srvid srvid,
+ s.srvname srvname,
+ c.usesysid usesysid,
+ pg_get_userbyid(c.usesysid) usename
+ FROM pgsql_fdw_get_connections() c
+ JOIN pg_catalog.pg_foreign_server s ON (s.oid = c.srvid);
+ GRANT SELECT ON pgsql_fdw_connections TO public;
+
diff --git a/contrib/pgsql_fdw/pgsql_fdw.c b/contrib/pgsql_fdw/pgsql_fdw.c
index ...8bf3fc0 .
*** a/contrib/pgsql_fdw/pgsql_fdw.c
--- b/contrib/pgsql_fdw/pgsql_fdw.c
***************
*** 0 ****
--- 1,859 ----
+ /*-------------------------------------------------------------------------
+ *
+ * pgsql_fdw.c
+ * foreign-data wrapper for remote PostgreSQL servers.
+ *
+ * Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/pgsql_fdw.c
+ *
+ *-------------------------------------------------------------------------
+ */
+ #include "postgres.h"
+ #include "fmgr.h"
+
+ #include "catalog/pg_foreign_server.h"
+ #include "catalog/pg_foreign_table.h"
+ #include "commands/explain.h"
+ #include "foreign/fdwapi.h"
+ #include "funcapi.h"
+ #include "miscadmin.h"
+ #include "nodes/nodeFuncs.h"
+ #include "optimizer/cost.h"
+ #include "optimizer/pathnode.h"
+ #include "utils/lsyscache.h"
+ #include "utils/memutils.h"
+ #include "utils/rel.h"
+
+ #include "pgsql_fdw.h"
+ #include "connection.h"
+
+ PG_MODULE_MAGIC;
+
+ /*
+ * Default fetch count for cursor. This can be overridden by fetch_count FDW
+ * option.
+ */
+ #define DEFAULT_FETCH_COUNT 10000
+
+ /*
+ * Cost to establish a connection.
+ * XXX: should be configurable per server?
+ */
+ #define CONNECTION_COSTS 100.0
+
+ /*
+ * Cost to transfer 1 byte from remote server.
+ * XXX: should be configurable per server?
+ */
+ #define TRANSFER_COSTS_PER_BYTE 0.001
+
+ /*
+ * Cursors which are used together in a local query require different name, so
+ * we use simple incremental name for that purpose. We don't care wrap around
+ * of cursor_id because it's hard to imagine that 2^32 cursors are used in a
+ * query.
+ */
+ #define CURSOR_NAME_FORMAT "pgsql_fdw_cursor_%u"
+ static uint32 cursor_id = 0;
+
+ /*
+ * Index of FDW-private information, such as remote SQL statements, which are
+ * stored in fdw_private list.
+ */
+ enum FdwPrivateIndex {
+ FdwPrivateSelectSql,
+ FdwPrivateDeclareSql,
+ FdwPrivateFetchSql,
+ FdwPrivateResetSql,
+ FdwPrivateCloseSql,
+
+ /* # of elements stored in the list fdw_private */
+ FdwPrivateNum,
+ };
+
+ /*
+ * Describes an execution state of a foreign scan against a foreign table
+ * using pgsql_fdw.
+ */
+ typedef struct PgsqlFdwExecutionState
+ {
+ MemoryContext scan_cxt; /* context for per-scan lifespan data */
+
+ List *fdw_private; /* FDW-private information passed to executor */
+ PGconn *conn; /* connection for the scan */
+
+ Oid *param_types; /* type array of external parameter */
+ const char **param_values; /* value array of external parameter */
+
+ int attnum; /* # of non-dropped attribute */
+ char **col_values; /* column value buffer */
+ AttInMetadata *attinmeta; /* attribute metadata */
+
+ Tuplestorestate *tuples; /* result of the scan */
+ } PgsqlFdwExecutionState;
+
+ /*
+ * SQL functions
+ */
+ extern Datum pgsql_fdw_handler(PG_FUNCTION_ARGS);
+ PG_FUNCTION_INFO_V1(pgsql_fdw_handler);
+
+ /*
+ * FDW callback routines
+ */
+ static void pgsqlPlanForeignScan(Oid foreigntableid,
+ PlannerInfo *root,
+ RelOptInfo *baserel);
+ static void pgsqlExplainForeignScan(ForeignScanState *node, ExplainState *es);
+ static void pgsqlBeginForeignScan(ForeignScanState *node, int eflags);
+ static TupleTableSlot *pgsqlIterateForeignScan(ForeignScanState *node);
+ static void pgsqlReScanForeignScan(ForeignScanState *node);
+ static void pgsqlEndForeignScan(ForeignScanState *node);
+
+ /*
+ * Helper functions
+ */
+ static void estimate_costs(PlannerInfo *root,
+ RelOptInfo *baserel,
+ const char *sql,
+ Oid serverid,
+ Cost *startup_cost,
+ Cost *total_cost);
+ static void execute_query(ForeignScanState *node);
+ static PGresult *fetch_result(ForeignScanState *node);
+ static void store_result(ForeignScanState *node, PGresult *res);
+
+ /* Exported functions, but not written in pgsql_fdw.h. */
+ void _PG_init(void);
+ void _PG_fini(void);
+
+ /*
+ * Module-specific initialization.
+ */
+ void
+ _PG_init(void)
+ {
+ }
+
+ /*
+ * Module-specific clean up.
+ */
+ void
+ _PG_fini(void)
+ {
+ }
+
+ /*
+ * The content of FdwRoutine of pgsql_fdw is never changed, so we use one
+ * static object for all scan.
+ */
+ static FdwRoutine fdwroutine = {
+
+ /* Node type */
+ T_FdwRoutine,
+
+ /* Required handler functions. */
+ pgsqlPlanForeignScan,
+ pgsqlExplainForeignScan,
+ pgsqlBeginForeignScan,
+ pgsqlIterateForeignScan,
+ pgsqlReScanForeignScan,
+ pgsqlEndForeignScan,
+
+ /* Optional handler functions. */
+ };
+
+ /*
+ * Foreign-data wrapper handler function: return a struct with pointers
+ * to my callback routines.
+ */
+ Datum
+ pgsql_fdw_handler(PG_FUNCTION_ARGS)
+ {
+ PG_RETURN_POINTER(&fdwroutine);
+ }
+
+ /*
+ * pgsqlPlanForeignScan
+ * Create possible scan paths for a scan on the foreign table
+ */
+ static void
+ pgsqlPlanForeignScan(Oid foreigntableid,
+ PlannerInfo *root,
+ RelOptInfo *baserel)
+ {
+ Cost startup_cost;
+ Cost total_cost;
+ char name[128]; /* must be larger than format + 10 */
+ StringInfoData cursor;
+ const char *fetch_count_str;
+ int fetch_count = DEFAULT_FETCH_COUNT;
+ char *sql;
+ List *fdw_private = NIL;
+ ForeignTable *table;
+ ForeignServer *server;
+
+ /*
+ * Generate remote query string, and estimate cost of this scan.
+ */
+ sql = deparseSql(foreigntableid, root, baserel);
+ table = GetForeignTable(foreigntableid);
+ server = GetForeignServer(table->serverid);
+ estimate_costs(root, baserel, sql, server->serverid,
+ &startup_cost, &total_cost);
+
+ /*
+ * Store plain SELECT statement in private area of FdwPlan. This will be
+ * used for executing remote query and explaining scan.
+ */
+ fdw_private = list_make1(makeString(sql));
+
+ /* Use specified fetch_count instead of default value, if any. */
+ fetch_count_str = GetFdwOptionValue(InvalidOid, InvalidOid, foreigntableid,
+ InvalidAttrNumber, "fetch_count");
+ if (fetch_count_str != NULL)
+ fetch_count = strtol(fetch_count_str, NULL, 10);
+ elog(DEBUG1, "relid=%u fetch_count=%d", foreigntableid, fetch_count);
+
+ /*
+ * We store some more information in FdwPlan to pass them beyond the
+ * boundary between planner and executor. Finally FdwPlan using cursor
+ * would hold items below:
+ *
+ * 1) plain SELECT statement (already added above)
+ * 2) SQL statement used to declare cursor
+ * 3) SQL statement used to fetch rows from cursor
+ * 4) SQL statement used to reset cursor
+ * 5) SQL statement used to close cursor
+ *
+ * These items are indexed with the enum FdwPrivateIndex, so an item
+ * can be accessed directly via list_nth(). For example of FETCH
+ * statement:
+ * list_nth(fdw_private, FdwPrivateFetchSql)
+ */
+
+ /* Construct cursor name from sequential value */
+ sprintf(name, CURSOR_NAME_FORMAT, cursor_id++);
+
+ /* Construct statement to declare cursor */
+ initStringInfo(&cursor);
+ appendStringInfo(&cursor, "DECLARE %s SCROLL CURSOR FOR %s", name, sql);
+ fdw_private = lappend(fdw_private, makeString(cursor.data));
+
+ /* Construct statement to fetch rows from cursor */
+ initStringInfo(&cursor);
+ appendStringInfo(&cursor, "FETCH %d FROM %s", fetch_count, name);
+ fdw_private = lappend(fdw_private, makeString(cursor.data));
+
+ /* Construct statement to reset cursor */
+ initStringInfo(&cursor);
+ appendStringInfo(&cursor, "MOVE ABSOLUTE 0 FROM %s", name);
+ fdw_private = lappend(fdw_private, makeString(cursor.data));
+
+ /* Construct statement to close cursor */
+ initStringInfo(&cursor);
+ appendStringInfo(&cursor, "CLOSE %s", name);
+ fdw_private = lappend(fdw_private, makeString(cursor.data));
+
+ /* Create ForeignScan path node for this scan, and add it to baserel. */
+ add_path(baserel, (Path *)
+ create_foreignscan_path(root, baserel,
+ baserel->rows,
+ startup_cost,
+ total_cost,
+ NIL, /* no pathkeys */
+ NULL, /* no outer rel eigher */
+ NIL, /* no param clause */
+ fdw_private)); /* store remote SQL */
+ }
+
+ /*
+ * pgsqlExplainForeignScan
+ * Produce extra output for EXPLAIN
+ */
+ static void
+ pgsqlExplainForeignScan(ForeignScanState *node, ExplainState *es)
+ {
+ List *fdw_private;
+ char *sql;
+
+ fdw_private = ((ForeignScan *) node->ss.ps.plan)->fdw_private;
+ sql = strVal(list_nth(fdw_private, FdwPrivateDeclareSql));
+ ExplainPropertyText("Remote SQL", sql, es);
+ }
+
+ /*
+ * pgsqlBeginForeignScan
+ * Initiate access to a foreign PostgreSQL table.
+ */
+ static void
+ pgsqlBeginForeignScan(ForeignScanState *node, int eflags)
+ {
+ PgsqlFdwExecutionState *festate;
+ PGconn *conn;
+ Oid relid;
+ ForeignTable *table;
+ ForeignServer *server;
+ UserMapping *user;
+ TupleTableSlot *slot = node->ss.ss_ScanTupleSlot;
+
+ /*
+ * Do nothing in EXPLAIN (no ANALYZE) case. node->fdw_state stays NULL.
+ */
+ if (eflags & EXEC_FLAG_EXPLAIN_ONLY)
+ return;
+
+ /*
+ * Save state in node->fdw_state.
+ */
+ festate = (PgsqlFdwExecutionState *) palloc(sizeof(PgsqlFdwExecutionState));
+ festate->fdw_private = ((ForeignScan *) node->ss.ps.plan)->fdw_private;
+
+ /*
+ * Create context for per-scan tuplestore under per-query context.
+ */
+ festate->scan_cxt = AllocSetContextCreate(node->ss.ps.state->es_query_cxt,
+ "pgsql_fdw",
+ ALLOCSET_DEFAULT_MINSIZE,
+ ALLOCSET_DEFAULT_INITSIZE,
+ ALLOCSET_DEFAULT_MAXSIZE);
+
+ /*
+ * Get connection to the foreign server. Connection manager would
+ * establish new connection if necessary.
+ */
+ relid = RelationGetRelid(node->ss.ss_currentRelation);
+ table = GetForeignTable(relid);
+ server = GetForeignServer(table->serverid);
+ user = GetUserMapping(GetOuterUserId(), server->serverid);
+ conn = GetConnection(server, user, true);
+ festate->conn = conn;
+
+ /* Result will be filled in first Iterate call. */
+ festate->tuples = NULL;
+
+ /* Allocate buffers for column values. */
+ {
+ TupleDesc tupdesc = slot->tts_tupleDescriptor;
+ festate->col_values = palloc(sizeof(char *) * tupdesc->natts);
+ festate->attinmeta = TupleDescGetAttInMetadata(tupdesc);
+ }
+
+ /* Allocate buffers for query parameters. */
+ {
+ ParamListInfo params = node->ss.ps.state->es_param_list_info;
+ int numParams = params ? params->numParams : 0;
+
+ if (numParams > 0)
+ {
+ festate->param_types = palloc0(sizeof(Oid) * numParams);
+ festate->param_values = palloc0(sizeof(char *) * numParams);
+ }
+ else
+ {
+ festate->param_types = NULL;
+ festate->param_values = NULL;
+ }
+ }
+
+ /* Store FDW-specific state into ForeignScanState */
+ node->fdw_state = (void *) festate;
+
+ return;
+ }
+
+ /*
+ * pgsqlIterateForeignScan
+ * Retrieve next row from the result set, or clear tuple slot to indicate
+ * EOF.
+ *
+ * Note that using per-query context when retrieving tuples from
+ * tuplestore to ensure that returned tuples can survive until next
+ * iteration because the tuple is released implicitly via ExecClearTuple.
+ * Retrieving a tuple from tuplestore in CurrentMemoryContext (it's a
+ * per-tuple context), ExecClearTuple will free dangling pointer.
+ */
+ static TupleTableSlot *
+ pgsqlIterateForeignScan(ForeignScanState *node)
+ {
+ PgsqlFdwExecutionState *festate;
+ TupleTableSlot *slot = node->ss.ss_ScanTupleSlot;
+ PGresult *res = NULL;
+ MemoryContext oldcontext = CurrentMemoryContext;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+
+ /*
+ * If this is the first call after Begin, we need to execute remote query.
+ * If the query needs cursor, we declare a cursor at first call and fetch
+ * from it in later calls.
+ */
+ if (festate->tuples == NULL)
+ execute_query(node);
+
+ /*
+ * If enough tuples are left in tuplestore, just return next tuple from it.
+ *
+ * It is necessary to switch to per-scan context to make returned tuple
+ * valid until next IterateForeignScan call, because it will be released
+ * with ExecClearTuple then. Otherwise, picked tuple is allocated in
+ * per-tuple context, and double-free of that tuple might happen.
+ */
+ MemoryContextSwitchTo(festate->scan_cxt);
+ if (tuplestore_gettupleslot(festate->tuples, true, false, slot))
+ {
+ MemoryContextSwitchTo(oldcontext);
+ return slot;
+ }
+ MemoryContextSwitchTo(oldcontext);
+
+ /*
+ * Here we need to clear partial result and fetch next bunch of tuples from
+ * from the cursor for the scan. If the fetch returns no tuple, the scan
+ * has reached the end.
+ *
+ * PGresult must be released before leaving this function.
+ */
+ PG_TRY();
+ {
+ res = fetch_result(node);
+ store_result(node, res);
+ PQclear(res);
+ res = NULL;
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+
+ /*
+ * If we got more tuples from the server cursor, return next tuple from
+ * tuplestore.
+ */
+ MemoryContextSwitchTo(festate->scan_cxt);
+ if (tuplestore_gettupleslot(festate->tuples, true, false, slot))
+ {
+ MemoryContextSwitchTo(oldcontext);
+ return slot;
+ }
+ MemoryContextSwitchTo(oldcontext);
+
+ /* We don't have any result even in remote server cursor. */
+ ExecClearTuple(slot);
+ return slot;
+ }
+
+ /*
+ * pgsqlReScanForeignScan
+ * - Restart this scan by resetting fetch location.
+ */
+ static void
+ pgsqlReScanForeignScan(ForeignScanState *node)
+ {
+ List *fdw_private;
+ char *sql;
+ PGconn *conn;
+ PGresult *res = NULL;
+ PgsqlFdwExecutionState *festate;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+
+ /* If we have not opened cursor yet, nothing to do. */
+ if (festate->tuples == NULL)
+ return;
+
+ /* Discard fetch results if any. */
+ tuplestore_clear(festate->tuples);
+
+ /* PGresult must be released before leaving this function. */
+ PG_TRY();
+ {
+ /* Reset cursor */
+ fdw_private = festate->fdw_private;
+ conn = festate->conn;
+ sql = strVal(list_nth(fdw_private, FdwPrivateResetSql));
+ res = PQexec(conn, sql);
+ if (PQresultStatus(res) != PGRES_COMMAND_OK)
+ {
+ ereport(ERROR,
+ (errmsg("could not rewind cursor"),
+ errdetail("%s", PQerrorMessage(conn)),
+ errhint("%s", sql)));
+ }
+ PQclear(res);
+ res = NULL;
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+ }
+
+ /*
+ * pgsqlEndForeignScan
+ * Finish scanning foreign table and dispose objects used for this scan
+ */
+ static void
+ pgsqlEndForeignScan(ForeignScanState *node)
+ {
+ List *fdw_private;
+ char *sql;
+ PGconn *conn;
+ PGresult *res = NULL;
+ PgsqlFdwExecutionState *festate;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+
+ /* if festate is NULL, we are in EXPLAIN; nothing to do */
+ if (festate == NULL)
+ return;
+
+ /* If we have not opened cursor yet, nothing to do. */
+ if (festate->tuples == NULL)
+ return;
+
+ /* Discard fetch results */
+ tuplestore_end(festate->tuples);
+ festate->tuples = NULL;
+
+ /* PGresult must be released before leaving this function. */
+ PG_TRY();
+ {
+ /* Close cursor */
+ fdw_private = festate->fdw_private;
+ conn = festate->conn;
+ sql = strVal(list_nth(fdw_private, FdwPrivateCloseSql));
+ res = PQexec(conn, sql);
+ if (PQresultStatus(res) != PGRES_COMMAND_OK)
+ {
+ ereport(ERROR,
+ (errmsg("could not close cursor"),
+ errdetail("%s", PQerrorMessage(conn)),
+ errhint("%s", sql)));
+ }
+ PQclear(res);
+ res = NULL;
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+
+ ReleaseConnection(festate->conn);
+
+ MemoryContextDelete(festate->scan_cxt);
+ festate->scan_cxt = NULL;
+ }
+
+ /*
+ * Estimate costs of scanning a foreign table.
+ */
+ static void
+ estimate_costs(PlannerInfo *root, RelOptInfo *baserel,
+ const char *sql, Oid serverid,
+ Cost *startup_cost, Cost *total_cost)
+ {
+ ForeignServer *server;
+ UserMapping *user;
+ PGconn *conn = NULL;
+ PGresult *res = NULL;
+ StringInfoData buf;
+ char *plan;
+ char *p;
+ int n;
+
+ /*
+ * Get connection to the foreign server. Connection manager would
+ * establish new connection if necessary.
+ */
+ server = GetForeignServer(serverid);
+ user = GetUserMapping(GetOuterUserId(), server->serverid);
+ conn = GetConnection(server, user, false);
+ initStringInfo(&buf);
+ appendStringInfo(&buf, "EXPLAIN %s", sql);
+
+ /* PGresult must be released before leaving this function. */
+ PG_TRY();
+ {
+ res = PQexec(conn, buf.data);
+ if (PQresultStatus(res) != PGRES_TUPLES_OK || PQntuples(res) == 0)
+ {
+ char *msg;
+
+ msg = pstrdup(PQerrorMessage(conn));
+ ereport(ERROR,
+ (errmsg("could not execute EXPLAIN for cost estimation"),
+ errdetail("%s", msg),
+ errhint("%s", sql)));
+ }
+
+ /*
+ * Find estimation portion from top plan node. Here we search opening
+ * parentheses from the end of the line to avoid finding unexpected
+ * parentheses.
+ */
+ plan = PQgetvalue(res, 0, 0);
+ p = strrchr(plan, '(');
+ if (p == NULL)
+ elog(ERROR, "wrong EXPLAIN output: %s", plan);
+ n = sscanf(p,
+ "(cost=%lf..%lf rows=%lf width=%d)",
+ startup_cost, total_cost, &baserel->rows, &baserel->width);
+ if (n != 4)
+ elog(ERROR, "could not get estimation from EXPLAIN output");
+
+ PQclear(res);
+ res = NULL;
+ ReleaseConnection(conn);
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+
+ /*
+ * TODO Selectivity of quals which are NOT pushed down should be also
+ * considered.
+ */
+
+ /* add cost to establish connection. */
+ *startup_cost += CONNECTION_COSTS;
+ *total_cost += CONNECTION_COSTS;
+
+ /* add cost to transfer result. */
+ *total_cost += TRANSFER_COSTS_PER_BYTE * baserel->width * baserel->tuples;
+ *total_cost += cpu_tuple_cost * baserel->tuples;
+ }
+
+ /*
+ * Execute remote query with current parameters.
+ */
+ static void
+ execute_query(ForeignScanState *node)
+ {
+ List *fdw_private;
+ PgsqlFdwExecutionState *festate;
+ ParamListInfo params = node->ss.ps.state->es_param_list_info;
+ int numParams = params ? params->numParams : 0;
+ Oid *types = NULL;
+ const char **values = NULL;
+ char *sql;
+ PGconn *conn;
+ PGresult *res = NULL;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+ types = festate->param_types;
+ values = festate->param_values;
+
+ /*
+ * Construct parameter array in text format. We don't release memory for
+ * the arrays explicitly, because the memory usage would not be very large,
+ * and anyway they will be released in context cleanup.
+ */
+ if (numParams > 0)
+ {
+ int i;
+
+ 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;
+
+ getTypeOutputInfo(types[i], &out_func_oid, &isvarlena);
+ fmgr_info(out_func_oid, &func);
+ values[i] = OutputFunctionCall(&func, params->params[i].value);
+ }
+ }
+ }
+
+ /* PGresult must be released before leaving this function. */
+ PG_TRY();
+ {
+ /*
+ * Execute remote query with parameters.
+ */
+ conn = festate->conn;
+ fdw_private = ((ForeignScan *) node->ss.ps.plan)->fdw_private;
+ sql = strVal(list_nth(fdw_private, FdwPrivateDeclareSql));
+ res = PQexecParams(conn, sql, numParams, types, values, NULL, NULL, 0);
+
+ /*
+ * If the query has failed, reporting details is enough here.
+ * Connection(s) which are used by this query (at least used by
+ * pgsql_fdw) will be cleaned up by the foreign connection manager.
+ */
+ if (PQresultStatus(res) != PGRES_COMMAND_OK)
+ {
+ ereport(ERROR,
+ (errmsg("could not declare cursor"),
+ errdetail("%s", PQerrorMessage(conn)),
+ errhint("%s", sql)));
+ }
+
+ /* Discard result of DECLARE statement. */
+ PQclear(res);
+ res = NULL;
+
+ /* Fetch first bunch of the result and store them into tuplestore. */
+ res = fetch_result(node);
+ store_result(node, res);
+ PQclear(res);
+ res = NULL;
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+ }
+
+ /*
+ * Fetch next partial result from remote server.
+ *
+ * Once this function has returned result records as PGresult, caller is
+ * responsible to release it, so caller should put codes which might throw
+ * exception in PG_TRY block. When an exception has been caught, release
+ * PGresult and re-throw the exception in PG_CATCH block.
+ */
+ static PGresult *
+ fetch_result(ForeignScanState *node)
+ {
+ PgsqlFdwExecutionState *festate;
+ List *fdw_private;
+ char *sql;
+ PGconn *conn;
+ PGresult *res;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+
+ /* Retrieve information for fetching result. */
+ fdw_private = festate->fdw_private;
+ sql = strVal(list_nth(fdw_private, FdwPrivateFetchSql));
+ conn = festate->conn;
+
+ /*
+ * Fetch result from remote server. In error case, we must release
+ * PGresult in this function to avoid memory leak because caller can't
+ * get the reference.
+ */
+ res = PQexec(conn, sql);
+ if (PQresultStatus(res) != PGRES_TUPLES_OK)
+ {
+ PQclear(res);
+ ereport(ERROR,
+ (errmsg("could not fetch rows from foreign server"),
+ errdetail("%s", PQerrorMessage(conn)),
+ errhint("%s", sql)));
+ }
+
+ return res;
+ }
+
+ /*
+ * Create tuples from PGresult and store them into tuplestore.
+ *
+ * Caller must use PG_TRY block to catch exception and release PGresult
+ * surely.
+ */
+ static void
+ store_result(ForeignScanState *node, PGresult *res)
+ {
+ int rows;
+ int row;
+ int i;
+ int nfields;
+ int attnum; /* number of non-dropped columns */
+ Form_pg_attribute *attrs;
+ TupleTableSlot *slot = node->ss.ss_ScanTupleSlot;
+ TupleDesc tupdesc = slot->tts_tupleDescriptor;
+ PgsqlFdwExecutionState *festate;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+ rows = PQntuples(res);
+ nfields = PQnfields(res);
+ attrs = tupdesc->attrs;
+
+ /* First, ensure that the tuplestore is empty. */
+ if (festate->tuples == NULL)
+ {
+ MemoryContext oldcontext = CurrentMemoryContext;
+
+ /*
+ * Create tuplestore to store result of the query in per-query context.
+ * Note that we use this memory context to avoid memory leak in error
+ * cases.
+ */
+ MemoryContextSwitchTo(festate->scan_cxt);
+ festate->tuples = tuplestore_begin_heap(false, false, work_mem);
+ MemoryContextSwitchTo(oldcontext);
+ }
+ else
+ {
+ /* We already have tuplestore, just need to clear contents of it. */
+ tuplestore_clear(festate->tuples);
+ }
+
+ /* 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)));
+
+ /* put a tuples into the slot */
+ for (row = 0; row < rows; row++)
+ {
+ int j;
+ HeapTuple tuple;
+
+ for (i = 0, j = 0; i < tupdesc->natts; i++)
+ {
+ /* skip dropped columns. */
+ if (attrs[i]->attisdropped)
+ {
+ festate->col_values[i] = NULL;
+ continue;
+ }
+
+ if (PQgetisnull(res, row, j))
+ festate->col_values[i] = NULL;
+ else
+ festate->col_values[i] = PQgetvalue(res, row, 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(festate->attinmeta, festate->col_values);
+ tuplestore_puttuple(festate->tuples, tuple);
+ }
+
+ tuplestore_donestoring(festate->tuples);
+ }
+
diff --git a/contrib/pgsql_fdw/pgsql_fdw.control b/contrib/pgsql_fdw/pgsql_fdw.control
index ...0a9c8f4 .
*** a/contrib/pgsql_fdw/pgsql_fdw.control
--- b/contrib/pgsql_fdw/pgsql_fdw.control
***************
*** 0 ****
--- 1,5 ----
+ # pgsql_fdw extension
+ comment = 'foreign-data wrapper for remote PostgreSQL servers'
+ default_version = '1.0'
+ module_pathname = '$libdir/pgsql_fdw'
+ relocatable = true
diff --git a/contrib/pgsql_fdw/pgsql_fdw.h b/contrib/pgsql_fdw/pgsql_fdw.h
index ...f6394ce .
*** a/contrib/pgsql_fdw/pgsql_fdw.h
--- b/contrib/pgsql_fdw/pgsql_fdw.h
***************
*** 0 ****
--- 1,28 ----
+ /*-------------------------------------------------------------------------
+ *
+ * pgsql_fdw.h
+ * foreign-data wrapper for remote PostgreSQL servers.
+ *
+ * Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/pgsql_fdw.h
+ *
+ *-------------------------------------------------------------------------
+ */
+
+ #ifndef PGSQL_FDW_H
+ #define PGSQL_FDW_H
+
+ #include "postgres.h"
+ #include "nodes/relation.h"
+
+ /* in option.c */
+ int ExtractConnectionOptions(List *defelems,
+ const char **keywords,
+ const char **values);
+
+ /* in deparse.c */
+ char *deparseSql(Oid relid, PlannerInfo *root, RelOptInfo *baserel);
+
+ #endif /* PGSQL_FDW_H */
diff --git a/contrib/pgsql_fdw/sql/pgsql_fdw.sql b/contrib/pgsql_fdw/sql/pgsql_fdw.sql
index ...b3fac96 .
*** a/contrib/pgsql_fdw/sql/pgsql_fdw.sql
--- b/contrib/pgsql_fdw/sql/pgsql_fdw.sql
***************
*** 0 ****
--- 1,248 ----
+ -- ===================================================================
+ -- create FDW objects
+ -- ===================================================================
+
+ -- Clean up in case a prior regression run failed
+
+ -- Suppress NOTICE messages when roles don't exist
+ SET client_min_messages TO 'error';
+
+ DROP ROLE IF EXISTS pgsql_fdw_user;
+
+ RESET client_min_messages;
+
+ CREATE ROLE pgsql_fdw_user LOGIN SUPERUSER;
+ SET SESSION AUTHORIZATION 'pgsql_fdw_user';
+
+ CREATE EXTENSION pgsql_fdw;
+
+ CREATE SERVER loopback1 FOREIGN DATA WRAPPER pgsql_fdw;
+ CREATE SERVER loopback2 FOREIGN DATA WRAPPER pgsql_fdw
+ OPTIONS (dbname 'contrib_regression');
+
+ CREATE USER MAPPING FOR public SERVER loopback1
+ OPTIONS (user 'value', password 'value');
+ CREATE USER MAPPING FOR pgsql_fdw_user SERVER loopback2;
+
+ CREATE FOREIGN TABLE ft1 (
+ c0 int,
+ c1 int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10)
+ ) SERVER loopback2;
+ ALTER FOREIGN TABLE ft1 DROP COLUMN c0;
+
+ CREATE FOREIGN TABLE ft2 (
+ c0 int,
+ c1 int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10)
+ ) SERVER loopback2;
+ ALTER FOREIGN TABLE ft2 DROP COLUMN c0;
+
+ -- ===================================================================
+ -- create objects used through FDW
+ -- ===================================================================
+ CREATE SCHEMA "S 1";
+ CREATE TABLE "S 1"."T 1" (
+ "C 1" int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10),
+ CONSTRAINT t1_pkey PRIMARY KEY ("C 1")
+ );
+ CREATE TABLE "S 1"."T 2" (
+ c1 int NOT NULL,
+ c2 text,
+ CONSTRAINT t2_pkey PRIMARY KEY (c1)
+ );
+
+ BEGIN;
+ TRUNCATE "S 1"."T 1";
+ INSERT INTO "S 1"."T 1"
+ SELECT id,
+ id % 10,
+ to_char(id, 'FM00000'),
+ '1970-01-01'::timestamptz + ((id % 100) || ' days')::interval,
+ '1970-01-01'::timestamp + ((id % 100) || ' days')::interval,
+ id % 10,
+ id % 10
+ FROM generate_series(1, 1000) id;
+ TRUNCATE "S 1"."T 2";
+ INSERT INTO "S 1"."T 2"
+ SELECT id,
+ 'AAA' || to_char(id, 'FM000')
+ FROM generate_series(1, 100) id;
+ COMMIT;
+
+ -- ===================================================================
+ -- tests for pgsql_fdw_validator
+ -- ===================================================================
+ ALTER FOREIGN DATA WRAPPER pgsql_fdw OPTIONS (host 'value'); -- ERROR
+ -- requiressl, krbsrvname and gsslib are omitted because they depend on
+ -- configure option
+ ALTER SERVER loopback1 OPTIONS (
+ authtype 'value',
+ service 'value',
+ connect_timeout 'value',
+ dbname 'value',
+ host 'value',
+ hostaddr 'value',
+ port 'value',
+ --client_encoding 'value',
+ tty 'value',
+ options 'value',
+ application_name 'value',
+ --fallback_application_name 'value',
+ keepalives 'value',
+ keepalives_idle 'value',
+ keepalives_interval 'value',
+ -- requiressl 'value',
+ sslcompression 'value',
+ sslmode 'value',
+ sslcert 'value',
+ sslkey 'value',
+ sslrootcert 'value',
+ sslcrl 'value'
+ --requirepeer 'value',
+ -- krbsrvname 'value',
+ -- gsslib 'value',
+ --replication 'value'
+ );
+ ALTER SERVER loopback1 OPTIONS (user 'value'); -- ERROR
+ ALTER SERVER loopback2 OPTIONS (ADD fetch_count '2');
+ ALTER USER MAPPING FOR public SERVER loopback1
+ OPTIONS (DROP user, DROP password);
+ ALTER USER MAPPING FOR public SERVER loopback1
+ OPTIONS (host 'value'); -- ERROR
+ ALTER FOREIGN TABLE ft1 OPTIONS (nspname 'S 1', relname 'T 1');
+ ALTER FOREIGN TABLE ft2 OPTIONS (nspname 'S 1', relname 'T 1', fetch_count '100');
+ ALTER FOREIGN TABLE ft1 OPTIONS (invalid 'value'); -- ERROR
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count 'a'); -- ERROR
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count '0'); -- ERROR
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count '-1'); -- ERROR
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (invalid 'value'); -- ERROR
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (colname 'C 1');
+ ALTER FOREIGN TABLE ft2 ALTER COLUMN c1 OPTIONS (colname 'C 1');
+ \dew+
+ \des+
+ \deu+
+ \det+
+
+ -- ===================================================================
+ -- simple queries
+ -- ===================================================================
+ -- single table, with/without alias
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
+ SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+ SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+ -- with WHERE clause
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
+ SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
+ -- 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
+ SELECT * FROM ft1 t1 WHERE t1.c3 = (SELECT MAX(c3) FROM ft2 t2) ORDER BY c1;
+ -- used in CTE
+ WITH t1 AS (SELECT * FROM ft1 WHERE c1 <= 10) SELECT t2.c1, t2.c2, t2.c3, t2.c4 FROM t1, ft2 t2 WHERE t1.c1 = t2.c1 ORDER BY t1.c1;
+ -- fixed values
+ SELECT 'fixed', NULL FROM ft1 t1 WHERE c1 = 1;
+ -- user-defined operator/function
+ CREATE FUNCTION pgsql_fdw_abs(int) RETURNS int AS $$
+ BEGIN
+ RETURN abs($1);
+ END
+ $$ LANGUAGE plpgsql IMMUTABLE;
+ CREATE OPERATOR === (
+ LEFTARG = int,
+ RIGHTARG = int,
+ PROCEDURE = int4eq,
+ COMMUTATOR = ===,
+ NEGATOR = !==
+ );
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = pgsql_fdw_abs(t1.c2);
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 === t1.c2;
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = abs(t1.c2);
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = t1.c2;
+
+ -- ===================================================================
+ -- parameterized queries
+ -- ===================================================================
+ -- simple join
+ PREPARE st1(int, int) AS SELECT t1.c3, t2.c3 FROM ft1 t1, ft2 t2 WHERE t1.c1 = $1 AND t2.c1 = $2;
+ EXPLAIN (COSTS false) EXECUTE st1(1, 2);
+ EXECUTE st1(1, 1);
+ EXECUTE st1(101, 101);
+ -- subquery using stable function (can't be pushed down)
+ PREPARE st2(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c4) = 6) ORDER BY c1;
+ EXPLAIN (COSTS false) EXECUTE st2(10, 20);
+ EXECUTE st2(10, 20);
+ EXECUTE st1(101, 101);
+ -- subquery using immutable function (can be pushed down)
+ PREPARE st3(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c5) = 6) ORDER BY c1;
+ EXPLAIN (COSTS false) EXECUTE st3(10, 20);
+ EXECUTE st3(10, 20);
+ EXECUTE st3(20, 30);
+ -- custom plan should be chosen
+ PREPARE st4(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 = $1;
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ -- cleanup
+ DEALLOCATE st1;
+ DEALLOCATE st2;
+ DEALLOCATE st3;
+ DEALLOCATE st4;
+
+ -- ===================================================================
+ -- connection management
+ -- ===================================================================
+ SELECT srvname, usename FROM pgsql_fdw_connections;
+ SELECT pgsql_fdw_disconnect(srvid, usesysid) FROM pgsql_fdw_get_connections();
+ SELECT srvname, usename FROM pgsql_fdw_connections;
+
+ -- ===================================================================
+ -- subtransaction
+ -- ===================================================================
+ BEGIN;
+ DECLARE c CURSOR FOR SELECT * FROM ft1 ORDER BY c1;
+ FETCH c;
+ SAVEPOINT s;
+ ERROR OUT; -- ERROR
+ ROLLBACK TO s;
+ SELECT srvname FROM pgsql_fdw_connections;
+ FETCH c;
+ COMMIT;
+ SELECT srvname FROM pgsql_fdw_connections;
+ ERROR OUT; -- ERROR
+ SELECT srvname FROM pgsql_fdw_connections;
+
+ -- ===================================================================
+ -- cleanup
+ -- ===================================================================
+ DROP OPERATOR === (int, int) CASCADE;
+ DROP OPERATOR !== (int, int) CASCADE;
+ DROP FUNCTION pgsql_fdw_abs(int);
+ DROP SCHEMA "S 1" CASCADE;
+ DROP EXTENSION pgsql_fdw CASCADE;
+ \c
+ DROP ROLE pgsql_fdw_user;
diff --git a/doc/src/sgml/contrib.sgml b/doc/src/sgml/contrib.sgml
index d4da4ee..32056d1 100644
*** a/doc/src/sgml/contrib.sgml
--- b/doc/src/sgml/contrib.sgml
*************** CREATE EXTENSION <replaceable>module_nam
*** 117,122 ****
--- 117,123 ----
&pgcrypto;
&pgfreespacemap;
&pgrowlocks;
+ &pgsql-fdw;
&pgstandby;
&pgstatstatements;
&pgstattuple;
diff --git a/doc/src/sgml/filelist.sgml b/doc/src/sgml/filelist.sgml
index b5d3c6d..de686ee 100644
*** a/doc/src/sgml/filelist.sgml
--- b/doc/src/sgml/filelist.sgml
***************
*** 125,130 ****
--- 125,131 ----
<!ENTITY pgcrypto SYSTEM "pgcrypto.sgml">
<!ENTITY pgfreespacemap SYSTEM "pgfreespacemap.sgml">
<!ENTITY pgrowlocks SYSTEM "pgrowlocks.sgml">
+ <!ENTITY pgsql-fdw SYSTEM "pgsql-fdw.sgml">
<!ENTITY pgstandby SYSTEM "pgstandby.sgml">
<!ENTITY pgstatstatements SYSTEM "pgstatstatements.sgml">
<!ENTITY pgstattuple SYSTEM "pgstattuple.sgml">
diff --git a/doc/src/sgml/pgsql-fdw.sgml b/doc/src/sgml/pgsql-fdw.sgml
index ...30fa7f5 .
*** a/doc/src/sgml/pgsql-fdw.sgml
--- b/doc/src/sgml/pgsql-fdw.sgml
***************
*** 0 ****
--- 1,261 ----
+ <!-- doc/src/sgml/pgsql-fdw.sgml -->
+
+ <sect1 id="pgsql-fdw" xreflabel="pgsql_fdw">
+ <title>pgsql_fdw</title>
+
+ <indexterm zone="pgsql-fdw">
+ <primary>pgsql_fdw</primary>
+ </indexterm>
+
+ <para>
+ The <filename>pgsql_fdw</filename> module provides a foreign-data wrapper for
+ external <productname>PostgreSQL</productname> servers.
+ With this module, users can access data stored in external
+ <productname>PostgreSQL</productname> via plain SQL statements.
+ </para>
+
+ <para>
+ Note that default wrapper <literal>pgsql_fdw</literal> is created
+ automatically during <command>CREATE EXTENSION</command> command for
+ <application>pgsql_fdw</application>.
+ </para>
+
+ <sect2>
+ <title>FDW Options of pgsql_fdw</title>
+
+ <sect3>
+ <title>Connection Options</title>
+ <para>
+ A foreign server and user mapping created using this wrapper can have
+ <application>libpq</> connection options, expect below:
+
+ <itemizedlist>
+ <listitem><para>client_encoding</para></listitem>
+ <listitem><para>fallback_application_name</para></listitem>
+ <listitem><para>replication</para></listitem>
+ </itemizedlist>
+
+ For details of <application>libpq</> connection options, see
+ <xref linkend="libpq-connect">.
+ </para>
+
+ <para>
+ <literal>user</literal> and <literal>password</literal> can be
+ specified on user mappings, and others can be specified on foreign servers.
+ </para>
+ </sect3>
+
+ <sect3>
+ <title>Object Name Options</title>
+ <para>
+ Foreign tables which were created using this wrapper, or its columns can
+ have object name options. These options can be used to specify the names
+ used in SQL statement sent to remote <productname>PostgreSQL</productname>
+ server. These options are useful when a remote object has different name
+ from corresponding local one.
+ </para>
+
+ <variablelist>
+
+ <varlistentry>
+ <term><literal>nspname</literal></term>
+ <listitem>
+ <para>
+ This option, which can be specified on a foreign table, is used as a
+ namespace (schema) reference in the SQL statement. If this options is
+ omitted, <literal>pg_class.nspname</literal> of the foreign table is
+ used.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>relname</literal></term>
+ <listitem>
+ <para>
+ This option, which can be specified on a foreign table, is used as a
+ relation (table) reference in the SQL statement. If this options is
+ omitted, <literal>pg_class.relname</literal> of the foreign table is
+ used.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>colname</literal></term>
+ <listitem>
+ <para>
+ This option, which can be specified on a column of a foreign table, is
+ used as a column (attribute) reference in the SQL statement. If this
+ option is omitted, <literal>pg_attribute.attname</literal> of the column
+ of the foreign table is used.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ </variablelist>
+
+ </sect3>
+
+ <sect3>
+ <title>Cursor Options</title>
+ <para>
+ The <application>pgsql_fdw</application> always uses cursor to retrieve the
+ result from external server. Users can control the behavior of cursor by
+ setting cursor options to foreign table or foreign server. If an option
+ is set to both objects, finer-grained setting is used. In other words,
+ foreign table's setting overrides foreign server's setting.
+ </para>
+
+ <variablelist>
+
+ <varlistentry>
+ <term><literal>fetch_count</literal></term>
+ <listitem>
+ <para>
+ This option specifies the number of rows to be fetched at a time.
+ This option accepts only integer value larger than zero. The default
+ setting is 10000.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ </variablelist>
+
+ </sect3>
+
+ </sect2>
+
+ <sect2>
+ <title>Connection Management</title>
+
+ <para>
+ The <application>pgsql_fdw</application> establishes a connection to a
+ foreign server in the beginning of the first query which uses a foreign
+ table associated to the foreign server, and reuses the connection following
+ queries and even in following foreign scans in same query.
+
+ You can see the list of active connections via
+ <structname>pgsql_fdw_connections</structname> view. It shows pair of oid
+ and name of server and local role for each active connections established by
+ <application>pgsql_fdw</application>. For security reason, only superuser
+ can see other role's connections.
+ </para>
+
+ <para>
+ Established connections are kept alive until local role changes or the
+ current transaction aborts or user requests so.
+ </para>
+
+ <para>
+ If role has been changed, active connections established as old local role
+ is kept alive but never be reused until local role has restored to original
+ role. This kind of situation happens with <command>SET ROLE</command> and
+ <command>SET SESSION AUTHORIZATION</command>.
+ </para>
+
+ <para>
+ If current transaction aborts by error or user request, all active
+ connections are disconnected automatically. This behavior avoids possible
+ connection leaks on error.
+ </para>
+
+ <para>
+ You can discard persistent connection at arbitrary timing with
+ <function>pgsql_fdw_disconnect()</function>. It takes server oid and
+ user oid as arguments. This function can handle only connections
+ established in current session; connections established by other backends
+ are not reachable.
+ </para>
+
+ <para>
+ You can discard all active and visible connections in current session with
+ using <structname>pgsql_fdw_connections</structname> and
+ <function>pgsql_fdw_disconnect()</function> together:
+ <synopsis>
+ postgres=# SELECT pgsql_fdw_disconnect(srvid, usesysid) FROM pgsql_fdw_connections;
+ pgsql_fdw_disconnect
+ ----------------------
+ OK
+ OK
+ (2 rows)
+ </synopsis>
+ </para>
+ </sect2>
+
+ <sect2>
+ <title>Transaction Management</title>
+ <para>
+ The <application>pgsql_fdw</application> begins remote transaction at the
+ beginning of a local query, and terminates it with <command>ABORT</command>
+ at the end of the local query. This means that all foreign scans on a
+ foreign server in a local query are executed in one transaction.
+ If isolation level of local transaction is <literal>SERIALIZABLE</literal>,
+ <literal>SERIALIZABLE</literal> is used for remote transaction. Otherwise,
+ if isolation level of local transaction is one of
+ <literal>READ UNCOMMITTED</literal>, <literal>READ COMMITTED</literal> or
+ <literal>REPEATABLE READ</literal>, then <literal>REPEATABLE READ</literal>
+ is used for remote transaction.
+ <literal>READ UNCOMMITTED</literal> and <literal>READ COMMITTED</literal>
+ are never used for remote transaction, because even
+ <literal>READ COMMITTED</literal> transaction might produce inconsistent
+ results, if remote data have been updated between two remote queries.
+ </para>
+ <para>
+ Note that even if the isolation level of local transaction was
+ <literal>SERIALIZABLE</literal> or <literal>REPEATABLE READ</literal>,
+ series of one query might produce different result, because foreign scans
+ in different local queries are executed in different remote transactions.
+ For instance, when client started a local transaction
+ explicitly with isolation level <literal>SERIALIZABLE</literal>, and
+ executed same local query which contains a foreign table which references
+ foreign data which is updated frequently, latter result would be different
+ from former result.
+ </para>
+ <para>
+ This restriction might be relaxed in future release.
+ </para>
+ </sect2>
+
+ <sect2>
+ <title>Estimation of Costs and Rows</title>
+ <para>
+ The <application>pgsql_fdw</application> estimates the costs of a foreign
+ scan by adding up some basic costs: connection costs, remote query costs
+ and data transfer costs.
+ To get remote query costs, <application>pgsql_fdw</application> executes
+ <command>EXPLAIN</command> command on remote server for each foreign scan.
+ </para>
+ <para>
+ On the other hand, estimated rows which was returned by
+ <command>EXPLAIN</command> is used for local estimation as-is.
+ </para>
+ </sect2>
+
+ <sect2>
+ <title>EXPLAIN Output</title>
+ <para>
+ For a foreign table using <literal>pgsql_fdw</>, <command>EXPLAIN</> shows
+ a remote SQL statement which is sent to remote
+ <productname>PostgreSQL</productname> server for a ForeignScan plan node.
+ For example:
+ </para>
+ <synopsis>
+ postgres=# EXPLAIN SELECT aid FROM pgbench_accounts WHERE abalance < 0;
+ QUERY PLAN
+ --------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on pgbench_accounts (cost=100.00..8105.13 rows=302613 width=8)
+ Filter: (abalance < 0)
+ Remote SQL: DECLARE pgsql_fdw_cursor_3 SCROLL CURSOR FOR SELECT aid, NULL, abalance, NULL FROM public.pgbench_accounts
+ (3 rows)
+ </synopsis>
+ </sect2>
+
+ <sect2>
+ <title>Author</title>
+ <para>
+ Shigeru Hanada <email>shigeru.hanada@gmail.com</email>
+ </para>
+ </sect2>
+
+ </sect1>
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 64ba8ec..94b2ea9 100644
*** a/src/backend/utils/adt/ruleutils.c
--- b/src/backend/utils/adt/ruleutils.c
*************** deparse_context_for(const char *aliasnam
*** 2181,2186 ****
--- 2181,2209 ----
return list_make1(dpns);
}
+ /* ----------
+ * deparse_context_for_rtelist - Build deparse context for a list of RTEs
+ *
+ * Given the list of RangeTableEnetry, build deparsing context for an
+ * expression referencing those relations. This is sufficient for uses of
+ * deparse_expression before plan has been created.
+ * ----------
+ */
+ List *
+ deparse_context_for_rtelist(List *rtable)
+ {
+ deparse_namespace *dpns;
+
+ dpns = (deparse_namespace *) palloc0(sizeof(deparse_namespace));
+
+ /* Build a minimal RTE for the rel */
+ dpns->rtable = rtable;
+ dpns->ctes = NIL;
+
+ /* Return a one-deep namespace stack */
+ return list_make1(dpns);
+ }
+
/*
* deparse_context_for_planstate - Build deparse context for a plan
*
diff --git a/src/include/utils/builtins.h b/src/include/utils/builtins.h
index 9fda7ad..4cf0489 100644
*** a/src/include/utils/builtins.h
--- b/src/include/utils/builtins.h
*************** extern char *deparse_expression(Node *ex
*** 650,655 ****
--- 650,656 ----
extern List *deparse_context_for(const char *aliasname, Oid relid);
extern List *deparse_context_for_planstate(Node *planstate, List *ancestors,
List *rtable);
+ extern List *deparse_context_for_rtelist(List *rtable);
extern const char *quote_identifier(const char *ident);
extern char *quote_qualified_identifier(const char *qualifier,
const char *ident);
pgsql_fdw_pushdown_v8.patchtext/plain; name=pgsql_fdw_pushdown_v8.patchDownload
diff --git a/contrib/pgsql_fdw/deparse.c b/contrib/pgsql_fdw/deparse.c
index 7ca2767..044f057 100644
*** a/contrib/pgsql_fdw/deparse.c
--- b/contrib/pgsql_fdw/deparse.c
*************** typedef struct foreign_executable_cxt
*** 35,46 ****
RelOptInfo *foreignrel;
} foreign_executable_cxt;
/*
* Deparse query representation into SQL statement which suits for remote
! * PostgreSQL server.
*/
char *
! deparseSql(Oid relid, PlannerInfo *root, RelOptInfo *baserel)
{
StringInfoData foreign_relname;
StringInfoData sql; /* builder for SQL statement */
--- 35,53 ----
RelOptInfo *foreignrel;
} foreign_executable_cxt;
+ static bool is_foreign_expr(PlannerInfo *root, RelOptInfo *baserel, Expr *expr);
+ static bool foreign_expr_walker(Node *node, foreign_executable_cxt *context);
+ static bool is_builtin(Oid procid);
+ static bool contain_ext_param(Node *clause);
+ static bool contain_ext_param_walker(Node *node, void *context);
+
/*
* Deparse query representation into SQL statement which suits for remote
! * PostgreSQL server. Also some of quals in WHERE clause will be pushed down
! * If they are safe to be evaluated on the remote side.
*/
char *
! deparseSql(Oid relid, PlannerInfo *root, RelOptInfo *baserel, bool *has_param)
{
StringInfoData foreign_relname;
StringInfoData sql; /* builder for SQL statement */
*************** deparseSql(Oid relid, PlannerInfo *root,
*** 51,56 ****
--- 58,64 ----
const char *relname = NULL; /* plain relation name */
const char *q_nspname; /* quoted namespace name */
const char *q_relname; /* quoted relation name */
+ List *foreign_expr = NIL; /* list of Expr* evaluated on remote */
int i;
List *rtable = NIL;
List *context = NIL;
*************** deparseSql(Oid relid, PlannerInfo *root,
*** 59,73 ****
initStringInfo(&foreign_relname);
/*
! * First of all, determine which column should be retrieved for this scan.
*
* We do this before deparsing SELECT clause because attributes which are
* not used in neither reltargetlist nor baserel->baserestrictinfo, quals
* evaluated on local, can be replaced with literal "NULL" in the SELECT
* clause to reduce overhead of tuple handling tuple and data transfer.
*/
if (baserel->baserestrictinfo != NIL)
{
ListCell *lc;
foreach (lc, baserel->baserestrictinfo)
--- 67,91 ----
initStringInfo(&foreign_relname);
/*
! * First of all, determine which qual can be pushed down.
! *
! * The expressions which satisfy is_foreign_expr() are deparsed into WHERE
! * clause of result SQL string, and they could be removed from PlanState
! * to avoid duplicate evaluation at ExecScan().
! *
! * We never change the quals in the Plan node, because this execution might
! * be for a PREPAREd statement, thus the quals in the Plan node might be
! * reused to construct another PlanState for subsequent EXECUTE statement.
*
* We do this before deparsing SELECT clause because attributes which are
* not used in neither reltargetlist nor baserel->baserestrictinfo, quals
* evaluated on local, can be replaced with literal "NULL" in the SELECT
* clause to reduce overhead of tuple handling tuple and data transfer.
*/
+ *has_param = false;
if (baserel->baserestrictinfo != NIL)
{
+ List *local_qual = NIL;
ListCell *lc;
foreach (lc, baserel->baserestrictinfo)
*************** deparseSql(Oid relid, PlannerInfo *root,
*** 76,81 ****
--- 94,113 ----
List *attrs;
/*
+ * Determine whether the qual can be pushed down or not. If a qual
+ * can be pushed down and it contains external param, tell that to
+ * caller in order to fall back to fixed costs.
+ */
+ if (is_foreign_expr(root, baserel, ri->clause))
+ {
+ foreign_expr = lappend(foreign_expr, ri->clause);
+ if (contain_ext_param((Node*) ri->clause))
+ *has_param = true;
+ }
+ else
+ local_qual = lappend(local_qual, ri);
+
+ /*
* We need to know which attributes are used in qual evaluated
* on the local server, because they should be listed in the
* SELECT clause of remote query. We can ignore attributes
*************** deparseSql(Oid relid, PlannerInfo *root,
*** 87,92 ****
--- 119,130 ----
PVC_RECURSE_PLACEHOLDERS);
attr_used = list_union(attr_used, attrs);
}
+
+ /*
+ * Remove pushed down qualifiers from baserestrictinfo to avoid
+ * redundant evaluation.
+ */
+ baserel->baserestrictinfo = local_qual;
}
/*
*************** deparseSql(Oid relid, PlannerInfo *root,
*** 204,210 ****
--- 242,473 ----
*/
appendStringInfo(&sql, "FROM %s", foreign_relname.data);
+ /*
+ * deparse WHERE clause
+ */
+ if (foreign_expr != NIL)
+ {
+ Node *node;
+
+ node = (Node *) make_ands_explicit(foreign_expr);
+ appendStringInfo(&sql, " WHERE %s ",
+ deparse_expression(node, context, false, false));
+ list_free(foreign_expr);
+ foreign_expr = NIL;
+ }
+
elog(DEBUG3, "Remote SQL: %s", sql.data);
return sql.data;
}
+ /*
+ * Returns true if expr is safe to be evaluated on the foreign server.
+ */
+ static bool
+ is_foreign_expr(PlannerInfo *root, RelOptInfo *baserel, Expr *expr)
+ {
+ foreign_executable_cxt context;
+ context.root = root;
+ context.foreignrel = baserel;
+
+ /*
+ * An expression which includes any mutable function can't be pushed down
+ * because it's result is not stable. For example, pushing now() down to
+ * remote side would cause confusion from the clock offset.
+ * If we have routine mapping infrastructure in future release, we will be
+ * able to choose function to be pushed down in finer granularity.
+ */
+ if (contain_mutable_functions((Node *) expr))
+ return false;
+
+ /*
+ * Check that the expression consists of nodes which are known as safe to
+ * be pushed down.
+ */
+ if (foreign_expr_walker((Node *) expr, &context))
+ return false;
+
+ return true;
+ }
+
+ /*
+ * Return true if node includes any node which is not known as safe to be
+ * pushed down.
+ */
+ static bool
+ foreign_expr_walker(Node *node, foreign_executable_cxt *context)
+ {
+ if (node == NULL)
+ return false;
+
+ /*
+ * If given expression has valid collation, it can't be pushed down because
+ * it might has incompatible semantics on remote side.
+ */
+ if (exprCollation(node) != InvalidOid ||
+ exprInputCollation(node) != InvalidOid)
+ return true;
+
+ /*
+ * If return type of given expression is not built-in, it can't be pushed
+ * down because it might has incompatible semantics on remote side.
+ */
+ if (!is_builtin(exprType(node)))
+ return true;
+
+ switch (nodeTag(node))
+ {
+ case T_Const:
+ case T_BoolExpr:
+ case T_NullTest:
+ case T_DistinctExpr:
+ case T_RelabelType:
+ /*
+ * 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 at the tail of this function.
+ */
+ break;
+ /*
+ * If function used by the expression is not built-in, it can't be
+ * pushed down because it might has incompatible semantics on remote
+ * side.
+ */
+ case T_FuncExpr:
+ {
+ FuncExpr *fe = (FuncExpr *) node;
+ if (!is_builtin(fe->funcid))
+ return true;
+ }
+ break;
+ case T_Param:
+ /*
+ * Only external parameters can be pushed down.:
+ */
+ {
+ if (((Param *) node)->paramkind != PARAM_EXTERN)
+ return true;
+ }
+ break;
+ case T_ScalarArrayOpExpr:
+ /*
+ * Only built-in operators can be pushed down. In addition,
+ * underlying function must be built-in and immutable, but we don't
+ * check volatility here; such check must be done already with
+ * contain_mutable_functions.
+ */
+ {
+ ScalarArrayOpExpr *oe = (ScalarArrayOpExpr *) node;
+
+ if (!is_builtin(oe->opno) || !is_builtin(oe->opfuncid))
+ return true;
+
+ /* operands are checked later */
+ }
+ break;
+ case T_OpExpr:
+ /*
+ * Only built-in operators can be pushed down. In addition,
+ * underlying function must be built-in and immutable, but we don't
+ * check volatility here; such check must be done already with
+ * contain_mutable_functions.
+ */
+ {
+ OpExpr *oe = (OpExpr *) node;
+
+ if (!is_builtin(oe->opno) || !is_builtin(oe->opfuncid))
+ return true;
+
+ /* operands are checked later */
+ }
+ 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;
+ foreign_executable_cxt *f_context;
+
+ f_context = (foreign_executable_cxt *) context;
+ if (var->varno != f_context->foreignrel->relid ||
+ var->varlevelsup != 0)
+ return true;
+ }
+ break;
+ case T_ArrayRef:
+ /*
+ * ArrayRef which holds non-built-in typed elements can't be pushed
+ * down.
+ */
+ {
+ if (!is_builtin(((ArrayRef *) node)->refelemtype))
+ return true;
+ }
+ break;
+ case T_ArrayExpr:
+ /*
+ * ArrayExpr which holds non-built-in typed elements can't be pushed
+ * down.
+ */
+ {
+ if (!is_builtin(((ArrayExpr *) node)->element_typeid))
+ return true;
+ }
+ break;
+ default:
+ {
+ ereport(DEBUG3,
+ (errmsg("expression is too complex"),
+ errdetail("%s", nodeToString(node))));
+ return true;
+ }
+ break;
+ }
+
+ return expression_tree_walker(node, foreign_expr_walker, context);
+ }
+
+ /*
+ * Return true if given object is one of built-in objects.
+ */
+ static bool
+ is_builtin(Oid oid)
+ {
+ return (oid < FirstNormalObjectId);
+ }
+
+ /*
+ * contain_ext_param
+ * Recursively search for Param nodes within a clause.
+ *
+ * Returns true if any parameter reference node with relkind PARAM_EXTERN
+ * found.
+ *
+ * This does not descend into subqueries, and so should be used only after
+ * reduction of sublinks to subplans, or in contexts where it's known there
+ * are no subqueries. There mustn't be outer-aggregate references either.
+ *
+ * XXX: These functions could be in core, src/backend/optimizer/util/clauses.c.
+ */
+ static bool
+ contain_ext_param(Node *clause)
+ {
+ return contain_ext_param_walker(clause, NULL);
+ }
+
+ static bool
+ contain_ext_param_walker(Node *node, void *context)
+ {
+ if (node == NULL)
+ return false;
+ if (IsA(node, Param))
+ {
+ Param *param = (Param *) node;
+
+ if (param->paramkind == PARAM_EXTERN)
+ return true; /* abort the tree traversal and return true */
+ }
+ return expression_tree_walker(node, contain_ext_param_walker, context);
+ }
diff --git a/contrib/pgsql_fdw/expected/pgsql_fdw.out b/contrib/pgsql_fdw/expected/pgsql_fdw.out
index c1e2e82..51b347e 100644
*** a/contrib/pgsql_fdw/expected/pgsql_fdw.out
--- b/contrib/pgsql_fdw/expected/pgsql_fdw.out
*************** SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.
*** 230,241 ****
-- with WHERE clause
EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
! QUERY PLAN
! ------------------------------------------------------------------------------------------------------------------
Foreign Scan on public.ft1 t1
Output: c1, c2, c3, c4, c5, c6, c7
! Filter: ((t1.c1 = 101) AND ((t1.c6)::text = '1'::text) AND (t1.c7 = '1'::bpchar))
! Remote SQL: DECLARE pgsql_fdw_cursor_4 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
(4 rows)
SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
--- 230,241 ----
-- with WHERE clause
EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
! QUERY PLAN
! ---------------------------------------------------------------------------------------------------------------------------------------
Foreign Scan on public.ft1 t1
Output: c1, c2, c3, c4, c5, c6, c7
! Filter: (((t1.c6)::text = '1'::text) AND (t1.c7 = '1'::bpchar))
! Remote SQL: DECLARE pgsql_fdw_cursor_4 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE ("C 1" = 101)
(4 rows)
SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
*************** EXPLAIN (COSTS false) SELECT * FROM ft1
*** 343,362 ****
(3 rows)
EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = abs(t1.c2);
! QUERY PLAN
! -------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Filter: (c1 = abs(c2))
! Remote SQL: DECLARE pgsql_fdw_cursor_20 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
! (3 rows)
EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = t1.c2;
! QUERY PLAN
! -------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Filter: (c1 = c2)
! Remote SQL: DECLARE pgsql_fdw_cursor_21 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
! (3 rows)
-- ===================================================================
-- parameterized queries
--- 343,360 ----
(3 rows)
EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = abs(t1.c2);
! QUERY PLAN
! --------------------------------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Remote SQL: DECLARE pgsql_fdw_cursor_20 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE ("C 1" = abs(c2))
! (2 rows)
EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = t1.c2;
! QUERY PLAN
! ---------------------------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Remote SQL: DECLARE pgsql_fdw_cursor_21 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE ("C 1" = c2)
! (2 rows)
-- ===================================================================
-- parameterized queries
*************** EXPLAIN (COSTS false) SELECT * FROM ft1
*** 364,380 ****
-- simple join
PREPARE st1(int, int) AS SELECT t1.c3, t2.c3 FROM ft1 t1, ft2 t2 WHERE t1.c1 = $1 AND t2.c1 = $2;
EXPLAIN (COSTS false) EXECUTE st1(1, 2);
! QUERY PLAN
! -----------------------------------------------------------------------------------------------------------------------------------------
Nested Loop
-> Foreign Scan on ft1 t1
! Filter: (c1 = 1)
! Remote SQL: DECLARE pgsql_fdw_cursor_22 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, NULL, NULL, NULL, NULL FROM "S 1"."T 1"
! -> Materialize
! -> Foreign Scan on ft2 t2
! Filter: (c1 = 2)
! Remote SQL: DECLARE pgsql_fdw_cursor_23 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, NULL, NULL, NULL, NULL FROM "S 1"."T 1"
! (8 rows)
EXECUTE st1(1, 1);
c3 | c3
--- 362,375 ----
-- simple join
PREPARE st1(int, int) AS SELECT t1.c3, t2.c3 FROM ft1 t1, ft2 t2 WHERE t1.c1 = $1 AND t2.c1 = $2;
EXPLAIN (COSTS false) EXECUTE st1(1, 2);
! QUERY PLAN
! ------------------------------------------------------------------------------------------------------------------------------------------------------
Nested Loop
-> Foreign Scan on ft1 t1
! Remote SQL: DECLARE pgsql_fdw_cursor_22 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, NULL, NULL, NULL, NULL FROM "S 1"."T 1" WHERE ("C 1" = 1)
! -> Foreign Scan on ft2 t2
! Remote SQL: DECLARE pgsql_fdw_cursor_23 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, NULL, NULL, NULL, NULL FROM "S 1"."T 1" WHERE ("C 1" = 2)
! (5 rows)
EXECUTE st1(1, 1);
c3 | c3
*************** EXECUTE st1(101, 101);
*** 391,411 ****
-- subquery using stable function (can't be pushed down)
PREPARE st2(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c4) = 6) ORDER BY c1;
EXPLAIN (COSTS false) EXECUTE st2(10, 20);
! QUERY PLAN
! ---------------------------------------------------------------------------------------------------------------------------------------------------
Sort
Sort Key: t1.c1
-> Hash Join
Hash Cond: (t1.c3 = t2.c3)
-> Foreign Scan on ft1 t1
! Filter: (c1 < 20)
! Remote SQL: DECLARE pgsql_fdw_cursor_28 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
-> Hash
-> HashAggregate
-> Foreign Scan on ft2 t2
! Filter: ((c1 > 10) AND (date_part('dow'::text, c4) = 6::double precision))
! Remote SQL: DECLARE pgsql_fdw_cursor_29 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, c4, NULL, NULL, NULL FROM "S 1"."T 1"
! (12 rows)
EXECUTE st2(10, 20);
c1 | c2 | c3 | c4 | c5 | c6 | c7
--- 386,405 ----
-- subquery using stable function (can't be pushed down)
PREPARE st2(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c4) = 6) ORDER BY c1;
EXPLAIN (COSTS false) EXECUTE st2(10, 20);
! QUERY PLAN
! -----------------------------------------------------------------------------------------------------------------------------------------------------------------------
Sort
Sort Key: t1.c1
-> Hash Join
Hash Cond: (t1.c3 = t2.c3)
-> Foreign Scan on ft1 t1
! Remote SQL: DECLARE pgsql_fdw_cursor_28 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE ("C 1" < 20)
-> Hash
-> HashAggregate
-> Foreign Scan on ft2 t2
! Filter: (date_part('dow'::text, c4) = 6::double precision)
! Remote SQL: DECLARE pgsql_fdw_cursor_29 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, c4, NULL, NULL, NULL FROM "S 1"."T 1" WHERE ("C 1" > 10)
! (11 rows)
EXECUTE st2(10, 20);
c1 | c2 | c3 | c4 | c5 | c6 | c7
*************** EXECUTE st1(101, 101);
*** 422,442 ****
-- subquery using immutable function (can be pushed down)
PREPARE st3(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c5) = 6) ORDER BY c1;
EXPLAIN (COSTS false) EXECUTE st3(10, 20);
! QUERY PLAN
! ---------------------------------------------------------------------------------------------------------------------------------------------------
Sort
Sort Key: t1.c1
-> Hash Join
Hash Cond: (t1.c3 = t2.c3)
-> Foreign Scan on ft1 t1
! Filter: (c1 < 20)
! Remote SQL: DECLARE pgsql_fdw_cursor_34 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
-> Hash
-> HashAggregate
-> Foreign Scan on ft2 t2
! Filter: ((c1 > 10) AND (date_part('dow'::text, c5) = 6::double precision))
! Remote SQL: DECLARE pgsql_fdw_cursor_35 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, NULL, c5, NULL, NULL FROM "S 1"."T 1"
! (12 rows)
EXECUTE st3(10, 20);
c1 | c2 | c3 | c4 | c5 | c6 | c7
--- 416,435 ----
-- subquery using immutable function (can be pushed down)
PREPARE st3(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c5) = 6) ORDER BY c1;
EXPLAIN (COSTS false) EXECUTE st3(10, 20);
! QUERY PLAN
! -----------------------------------------------------------------------------------------------------------------------------------------------------------------------
Sort
Sort Key: t1.c1
-> Hash Join
Hash Cond: (t1.c3 = t2.c3)
-> Foreign Scan on ft1 t1
! Remote SQL: DECLARE pgsql_fdw_cursor_34 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE ("C 1" < 20)
-> Hash
-> HashAggregate
-> Foreign Scan on ft2 t2
! Filter: (date_part('dow'::text, c5) = 6::double precision)
! Remote SQL: DECLARE pgsql_fdw_cursor_35 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, NULL, c5, NULL, NULL FROM "S 1"."T 1" WHERE ("C 1" > 10)
! (11 rows)
EXECUTE st3(10, 20);
c1 | c2 | c3 | c4 | c5 | c6 | c7
*************** EXECUTE st3(20, 30);
*** 453,504 ****
-- custom plan should be chosen
PREPARE st4(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 = $1;
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! -------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Filter: (c1 = 1)
! Remote SQL: DECLARE pgsql_fdw_cursor_40 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
! (3 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! -------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Filter: (c1 = 1)
! Remote SQL: DECLARE pgsql_fdw_cursor_41 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
! (3 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! -------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Filter: (c1 = 1)
! Remote SQL: DECLARE pgsql_fdw_cursor_42 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
! (3 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! -------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Filter: (c1 = 1)
! Remote SQL: DECLARE pgsql_fdw_cursor_43 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
! (3 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! -------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Filter: (c1 = 1)
! Remote SQL: DECLARE pgsql_fdw_cursor_44 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
! (3 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! -------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Filter: (c1 = $1)
! Remote SQL: DECLARE pgsql_fdw_cursor_45 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
! (3 rows)
-- cleanup
DEALLOCATE st1;
--- 446,491 ----
-- custom plan should be chosen
PREPARE st4(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 = $1;
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! --------------------------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Remote SQL: DECLARE pgsql_fdw_cursor_40 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE ("C 1" = 1)
! (2 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! --------------------------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Remote SQL: DECLARE pgsql_fdw_cursor_41 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE ("C 1" = 1)
! (2 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! --------------------------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Remote SQL: DECLARE pgsql_fdw_cursor_42 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE ("C 1" = 1)
! (2 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! --------------------------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Remote SQL: DECLARE pgsql_fdw_cursor_43 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE ("C 1" = 1)
! (2 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! --------------------------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Remote SQL: DECLARE pgsql_fdw_cursor_44 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE ("C 1" = 1)
! (2 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! --------------------------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Remote SQL: DECLARE pgsql_fdw_cursor_46 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE ("C 1" = 1)
! (2 rows)
-- cleanup
DEALLOCATE st1;
diff --git a/contrib/pgsql_fdw/pgsql_fdw.c b/contrib/pgsql_fdw/pgsql_fdw.c
index 8bf3fc0..c12b0a7 100644
*** a/contrib/pgsql_fdw/pgsql_fdw.c
--- b/contrib/pgsql_fdw/pgsql_fdw.c
*************** pgsqlPlanForeignScan(Oid foreigntableid,
*** 194,208 ****
List *fdw_private = NIL;
ForeignTable *table;
ForeignServer *server;
/*
* Generate remote query string, and estimate cost of this scan.
*/
! sql = deparseSql(foreigntableid, root, baserel);
! table = GetForeignTable(foreigntableid);
! server = GetForeignServer(table->serverid);
! estimate_costs(root, baserel, sql, server->serverid,
! &startup_cost, &total_cost);
/*
* Store plain SELECT statement in private area of FdwPlan. This will be
--- 194,225 ----
List *fdw_private = NIL;
ForeignTable *table;
ForeignServer *server;
+ bool has_param;
/*
* Generate remote query string, and estimate cost of this scan.
+ *
+ * If the baserestrictinfo contains any Param node with paramkind
+ * PARAM_EXTERNAL as part of push-down-able condition, we need to
+ * groundless fixed costs because we can't get actual parameter values
+ * here, so we use fixed and large costs as second best so that planner
+ * tend to choose custom plan.
+ *
+ * See comments in plancache.c for details of custom plan.
*/
! sql = deparseSql(foreigntableid, root, baserel, &has_param);
! if (has_param)
! {
! startup_cost = CONNECTION_COSTS;
! total_cost = 10000; /* groundless large costs */
! }
! else
! {
! table = GetForeignTable(foreigntableid);
! server = GetForeignServer(table->serverid);
! estimate_costs(root, baserel, sql, server->serverid,
! &startup_cost, &total_cost);
! }
/*
* Store plain SELECT statement in private area of FdwPlan. This will be
diff --git a/contrib/pgsql_fdw/pgsql_fdw.h b/contrib/pgsql_fdw/pgsql_fdw.h
index f6394ce..690d58b 100644
*** a/contrib/pgsql_fdw/pgsql_fdw.h
--- b/contrib/pgsql_fdw/pgsql_fdw.h
*************** int ExtractConnectionOptions(List *defel
*** 23,28 ****
const char **values);
/* in deparse.c */
! char *deparseSql(Oid relid, PlannerInfo *root, RelOptInfo *baserel);
#endif /* PGSQL_FDW_H */
--- 23,31 ----
const char **values);
/* in deparse.c */
! char *deparseSql(Oid relid,
! PlannerInfo *root,
! RelOptInfo *baserel,
! bool *has_param);
#endif /* PGSQL_FDW_H */
diff --git a/doc/src/sgml/pgsql-fdw.sgml b/doc/src/sgml/pgsql-fdw.sgml
index 30fa7f5..2375d8f 100644
*** a/doc/src/sgml/pgsql-fdw.sgml
--- b/doc/src/sgml/pgsql-fdw.sgml
*************** postgres=# SELECT pgsql_fdw_disconnect(s
*** 242,253 ****
</para>
<synopsis>
postgres=# EXPLAIN SELECT aid FROM pgbench_accounts WHERE abalance < 0;
! QUERY PLAN
! --------------------------------------------------------------------------------------------------------------------------
! Foreign Scan on pgbench_accounts (cost=100.00..8105.13 rows=302613 width=8)
! Filter: (abalance < 0)
! Remote SQL: DECLARE pgsql_fdw_cursor_3 SCROLL CURSOR FOR SELECT aid, NULL, abalance, NULL FROM public.pgbench_accounts
! (3 rows)
</synopsis>
</sect2>
--- 242,252 ----
</para>
<synopsis>
postgres=# EXPLAIN SELECT aid FROM pgbench_accounts WHERE abalance < 0;
! QUERY PLAN
! ------------------------------------------------------------------------------------------------------------------------------------------------
! Foreign Scan on pgbench_accounts (cost=100.00..8861.66 rows=518 width=8)
! Remote SQL: DECLARE pgsql_fdw_cursor_1 SCROLL CURSOR FOR SELECT aid, NULL, abalance, NULL FROM public.pgbench_accounts WHERE (abalance < 0)
! (2 rows)
</synopsis>
</sect2>
Shigeru Hanada wrote:
Connection should be closed only when the trigger is a
top level transaction and it's aborting, but isTopLevel flag was not
checked. I fixed the bug and added regression tests for such cases.
I wondered about that - is it really necessary to close the remote
connection? Wouldn't a ROLLBACK on the remote connection be good enough?
Yours,
Laurenz Albe
Peter Eisentraut <peter_e@gmx.net> writes:
On tor, 2012-03-01 at 20:56 +0900, Shigeru Hanada wrote:
How about moving postgresql_fdw_validator into dblink,
That's probably a good move. If this were C++, we might try to subclass
this whole thing a bit, to avoid code duplication, but I don't see an
easy way to do that here.
with renaming to dblink_fdw_validator?
Well, it's not the validator of the dblink_fdw, so maybe something like
basic_postgresql_fdw_validator.
I don't understand this objection. If we move it into dblink, then it
*is* dblink's validator, and nobody else's.
A bigger issue with postgresql_fdw_validator is that it supposes that
the core backend is authoritative as to what options libpq supports,
which is bad design on its face. It would be much more sensible for
dblink to be asking libpq what options libpq supports, say via
PQconndefaults().
We might find that we have to leave postgresql_fdw_validator as-is
for backwards compatibility reasons (in particular, being able to load
existing FDW definitions) but I think we should migrate away from using
it.
regards, tom lane
Shigeru Hanada <shigeru.hanada@gmail.com> writes:
Attached patches also contains changes to catch up to the redesign of
PlanForeignScan.
I started to look at this patch, which soon led me back to the
prerequisite patch fdw_helper_v5.patch (that is the last version you
posted of that one, right?). I can see the value of
GetForeignColumnOptions, but ISTM that GetFdwOptionValue is poorly
designed and will accomplish little except to encourage inefficient
searching. The original version was even worse, but even in the current
version there is no way to avoid a useless scan of table-level options
when you are looking for a column-level option. Also, it's inefficient
when looking for several option values, as in this extract from
pgsql_fdw_v13.patch,
+ nspname = GetFdwOptionValue(InvalidOid, InvalidOid, relid,
+ InvalidAttrNumber, "nspname");
+ if (nspname == NULL)
+ nspname = get_namespace_name(get_rel_namespace(relid));
+ q_nspname = quote_identifier(nspname);
+
+ relname = GetFdwOptionValue(InvalidOid, InvalidOid, relid,
+ InvalidAttrNumber, "relname");
+ if (relname == NULL)
+ relname = get_rel_name(relid);
+ q_relname = quote_identifier(relname);
where we are going to uselessly run GetForeignTable twice.
If we had a lot of options that could usefully be specified at multiple
levels of the foreign-objects hierarchy, it might be appropriate to have
a search function defined like this; but the existing samples of FDWs
don't seem to support the idea that that's going to be common. It looks
to me like the vast majority of options make sense at exactly one level.
So I'm thinking we should forget GetFdwOptionValue and just expect the
callers to search the option lists for the appropriate object(s). It
might be worth providing get_options_value() as an exported function,
though surely there's not that much to it.
Another issue that get_options_value ignores defGetString() which is
what it really ought to be using, instead of assuming strVal() is
appropriate. ISTM that it would also encourage people to take shortcuts
where they should be using functions like defGetBoolean() etc. Not
quite sure what we should do about that; maybe we need to provide
several variants of the function that are appropriate for different
option datatypes.
regards, tom lane
(2012/03/07 9:01), Tom Lane wrote:
I started to look at this patch, which soon led me back to the
prerequisite patch fdw_helper_v5.patch (that is the last version you
posted of that one, right?).
Thanks for the review. Yes, v5 is the last version of fdw_helper patch.
I can see the value of
GetForeignColumnOptions, but ISTM that GetFdwOptionValue is poorly
designed and will accomplish little except to encourage inefficient
searching. The original version was even worse, but even in the current
version there is no way to avoid a useless scan of table-level options
when you are looking for a column-level option. Also, it's inefficient
when looking for several option values, as in this extract from
pgsql_fdw_v13.patch,+ nspname = GetFdwOptionValue(InvalidOid, InvalidOid, relid, + InvalidAttrNumber, "nspname"); + if (nspname == NULL) + nspname = get_namespace_name(get_rel_namespace(relid)); + q_nspname = quote_identifier(nspname); + + relname = GetFdwOptionValue(InvalidOid, InvalidOid, relid, + InvalidAttrNumber, "relname"); + if (relname == NULL) + relname = get_rel_name(relid); + q_relname = quote_identifier(relname);where we are going to uselessly run GetForeignTable twice.
In addition, request for fetch_count option value via
GetFdwOptionValue() uselessly runs GetUserMapping() and
GetForeignDataWrapper() too, they are obvious waste of clocks and memory.
If we had a lot of options that could usefully be specified at multiple
levels of the foreign-objects hierarchy, it might be appropriate to have
a search function defined like this; but the existing samples of FDWs
don't seem to support the idea that that's going to be common. It looks
to me like the vast majority of options make sense at exactly one level.
Yes, I added GetFdwOptionValue() to provide an easy way to obtain the
value of a particular FDW option which might be stored in multiple
levels of foreign-objects hierarchy without looping per object. I used
it in pgsql_fdw to get value of fetch_count option, which can be stored
in server and/or foreign table, but it seems the only one use case now.
So I'm thinking we should forget GetFdwOptionValue and just expect the
callers to search the option lists for the appropriate object(s). It
might be worth providing get_options_value() as an exported function,
though surely there's not that much to it.
Agreed. Attached fdw_helper patch doesn't contain GetFdwOptionValue()
any more, and pgsql_fdw patch accesses only necessary catalogs.
Another issue that get_options_value ignores defGetString() which is
what it really ought to be using, instead of assuming strVal() is
appropriate. ISTM that it would also encourage people to take shortcuts
where they should be using functions like defGetBoolean() etc. Not
quite sure what we should do about that; maybe we need to provide
several variants of the function that are appropriate for different
option datatypes.
strVal() used in pgsql_fdw were replaced with defGetString(). It seems
to me that it's enough.
Regards,
--
Shigeru Hanada
Attachments:
fdw_helper_v6.patchtext/plain; name=fdw_helper_v6.patchDownload
diff --git a/contrib/file_fdw/file_fdw.c b/contrib/file_fdw/file_fdw.c
index c2faa62..0193aae 100644
*** a/contrib/file_fdw/file_fdw.c
--- b/contrib/file_fdw/file_fdw.c
***************
*** 27,33 ****
#include "optimizer/cost.h"
#include "optimizer/pathnode.h"
#include "utils/rel.h"
! #include "utils/syscache.h"
PG_MODULE_MAGIC;
--- 27,33 ----
#include "optimizer/cost.h"
#include "optimizer/pathnode.h"
#include "utils/rel.h"
! #include "utils/lsyscache.h"
PG_MODULE_MAGIC;
*************** get_file_fdw_attribute_options(Oid relid
*** 346,399 ****
/* Retrieve FDW options for all user-defined attributes. */
for (attnum = 1; attnum <= natts; attnum++)
{
! HeapTuple tuple;
! Form_pg_attribute attr;
! Datum datum;
! bool isnull;
/* Skip dropped attributes. */
if (tupleDesc->attrs[attnum - 1]->attisdropped)
continue;
! /*
! * We need the whole pg_attribute tuple not just what is in the
! * tupleDesc, so must do a catalog lookup.
! */
! tuple = SearchSysCache2(ATTNUM,
! RelationGetRelid(rel),
! Int16GetDatum(attnum));
! if (!HeapTupleIsValid(tuple))
! elog(ERROR, "cache lookup failed for attribute %d of relation %u",
! attnum, RelationGetRelid(rel));
! attr = (Form_pg_attribute) GETSTRUCT(tuple);
!
! datum = SysCacheGetAttr(ATTNUM,
! tuple,
! Anum_pg_attribute_attfdwoptions,
! &isnull);
! if (!isnull)
{
! List *options = untransformRelOptions(datum);
! ListCell *lc;
! foreach(lc, options)
{
! DefElem *def = (DefElem *) lfirst(lc);
!
! if (strcmp(def->defname, "force_not_null") == 0)
{
! if (defGetBoolean(def))
! {
! char *attname = pstrdup(NameStr(attr->attname));
! fnncolumns = lappend(fnncolumns, makeString(attname));
! }
}
- /* maybe in future handle other options here */
}
}
-
- ReleaseSysCache(tuple);
}
heap_close(rel, AccessShareLock);
--- 346,374 ----
/* Retrieve FDW options for all user-defined attributes. */
for (attnum = 1; attnum <= natts; attnum++)
{
! List *options;
! ListCell *lc;
/* Skip dropped attributes. */
if (tupleDesc->attrs[attnum - 1]->attisdropped)
continue;
! options = GetForeignColumnOptions(relid, attnum);
! foreach(lc, options)
{
! DefElem *def = (DefElem *) lfirst(lc);
! if (strcmp(def->defname, "force_not_null") == 0)
{
! if (defGetBoolean(def))
{
! char *attname = pstrdup(get_attname(relid, attnum));
! fnncolumns = lappend(fnncolumns, makeString(attname));
}
}
+ /* maybe in future handle other options here */
}
}
heap_close(rel, AccessShareLock);
diff --git a/doc/src/sgml/fdwhandler.sgml b/doc/src/sgml/fdwhandler.sgml
index 12c5f75..7900484 100644
*** a/doc/src/sgml/fdwhandler.sgml
--- b/doc/src/sgml/fdwhandler.sgml
*************** EndForeignScan (ForeignScanState *node);
*** 244,247 ****
--- 244,352 ----
</sect1>
+ <sect1 id="fdw-helpers">
+ <title>Foreign Data Wrapper Helper Functions</title>
+
+ <para>
+ Several helper functions are exported from core so that authors of FDW
+ can get easy access to attributes of FDW-related objects such as FDW
+ options.
+ </para>
+
+ <para>
+ <programlisting>
+ ForeignDataWrapper *
+ GetForeignDataWrapper(Oid fdwid);
+ </programlisting>
+
+ This function returns a <structname>ForeignDataWrapper</structname> object
+ for a foreign-data wrapper with given oid. A
+ <structname>ForeignDataWrapper</structname> object contains oid of the
+ wrapper itself, oid of the owner of the wrapper, name of the wrapper, oid
+ of fdwhandler function, oid of fdwvalidator function, and FDW options in
+ the form of list of <structname>DefElem</structname>.
+ </para>
+
+ <para>
+ <programlisting>
+ ForeignServer *
+ GetForeignServer(Oid serverid);
+ </programlisting>
+
+ This function returns a <structname>ForeignServer</structname> object for
+ a foreign server with given oid. A <structname>ForeignServer</structname>
+ object contains oid of the server, oid of the wrapper for the server, oid
+ of the owner of the server, name of the server, type of the server,
+ version of the server, and FDW options in the form of list of
+ <structname>DefElem</structname>.
+ </para>
+
+ <para>
+ <programlisting>
+ UserMapping *
+ GetUserMapping(Oid userid, Oid serverid);
+ </programlisting>
+
+ This function returns a <structname>UserMapping</structname> object for a
+ user mapping with given oid pair. A <structname>UserMapping</structname>
+ object contains oid of the user, oid of the server, and FDW options in the
+ form of list of <structname>DefElem</structname>.
+ </para>
+
+ <para>
+ <programlisting>
+ ForeignTable *
+ GetForeignTable(Oid relid);
+ </programlisting>
+
+ This function returns a <structname>ForeignTable</structname> object for a
+ foreign table with given oid. A <structname>ForeignTable</structname>
+ object contains oid of the foreign table, oid of the server for the table,
+ and FDW options in the form of list of <structname>DefElem</structname>.
+ </para>
+
+ <para>
+ <programlisting>
+ List *
+ GetForeignTableColumnOptions(Oid relid, AttrNumber attnum);
+ </programlisting>
+
+ This function returns per-column FDW options for a column with given
+ relation oid and attribute number in the form of list of
+ <structname>DefElem</structname>.
+ </para>
+
+ <para>
+ Some object types have name-based functions.
+ </para>
+
+ <para>
+ <programlisting>
+ ForeignDataWrapper *
+ GetForeignDataWrapperByName(const char *name, bool missing_ok);
+ </programlisting>
+
+ This function returns a <structname>ForeignDataWrapper</structname> object
+ for a foreign-data wrapper with given name. If the wrapper is not found,
+ return NULL if missing_ok was true, otherwise raise an error.
+ </para>
+
+ <para>
+ <programlisting>
+ ForeignServer *
+ GetForeignServerByName(const char *name, bool missing_ok);
+ </programlisting>
+
+ This function returns a <structname>ForeignServer</structname> object for
+ a foreign server with given name. If the server is not found, return NULL
+ if missing_ok was true, otherwise raise an error.
+ </para>
+
+ <para>
+ To use any of these functions, you need to include
+ <filename>foreign/foreign.h</filename> in your source file.
+ </para>
+
+ </sect1>
+
</chapter>
diff --git a/src/backend/foreign/foreign.c b/src/backend/foreign/foreign.c
index c4c2a61..4db26b5 100644
*** a/src/backend/foreign/foreign.c
--- b/src/backend/foreign/foreign.c
***************
*** 17,22 ****
--- 17,23 ----
#include "catalog/pg_foreign_server.h"
#include "catalog/pg_foreign_table.h"
#include "catalog/pg_user_mapping.h"
+ #include "commands/defrem.h"
#include "foreign/fdwapi.h"
#include "foreign/foreign.h"
#include "miscadmin.h"
*************** GetForeignTable(Oid relid)
*** 246,251 ****
--- 247,282 ----
return ft;
}
+ /*
+ * GetForeignColumnOptions - Get attfdwoptions of given relation/attnum as
+ * list of DefElem.
+ */
+ List *
+ GetForeignColumnOptions(Oid relid, AttrNumber attnum)
+ {
+ List *options;
+ HeapTuple tp;
+ Datum datum;
+ bool isnull;
+
+ tp = SearchSysCache2(ATTNUM,
+ ObjectIdGetDatum(relid),
+ Int16GetDatum(attnum));
+ if (!HeapTupleIsValid(tp))
+ elog(ERROR, "cache lookup failed for attribute %d of relation %u", attnum, relid);
+ datum = SysCacheGetAttr(ATTNUM,
+ tp,
+ Anum_pg_attribute_attfdwoptions,
+ &isnull);
+ if (isnull)
+ options = NIL;
+ else
+ options = untransformRelOptions(datum);
+
+ ReleaseSysCache(tp);
+
+ return options;
+ }
/*
* GetFdwRoutine - call the specified foreign-data wrapper handler routine
diff --git a/src/include/foreign/foreign.h b/src/include/foreign/foreign.h
index 191122d..4a4ce23 100644
*** a/src/include/foreign/foreign.h
--- b/src/include/foreign/foreign.h
*************** extern ForeignDataWrapper *GetForeignDat
*** 75,80 ****
--- 75,81 ----
extern ForeignDataWrapper *GetForeignDataWrapperByName(const char *name,
bool missing_ok);
extern ForeignTable *GetForeignTable(Oid relid);
+ extern List *GetForeignColumnOptions(Oid relid, AttrNumber attnum);
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);
pgsql_fdw_v14.patchtext/plain; name=pgsql_fdw_v14.patchDownload
diff --git a/contrib/Makefile b/contrib/Makefile
index ac0a80a..14aa433 100644
*** a/contrib/Makefile
--- b/contrib/Makefile
*************** SUBDIRS = \
*** 41,46 ****
--- 41,47 ----
pgbench \
pgcrypto \
pgrowlocks \
+ pgsql_fdw \
pgstattuple \
seg \
spi \
diff --git a/contrib/README b/contrib/README
index a1d42a1..d3fa211 100644
*** a/contrib/README
--- b/contrib/README
*************** pgrowlocks -
*** 158,163 ****
--- 158,167 ----
A function to return row locking information
by Tatsuo Ishii <ishii@sraoss.co.jp>
+ pgsql_fdw -
+ Foreign-data wrapper for external PostgreSQL servers.
+ by Shigeru Hanada <shigeru.hanada@gmail.com>
+
pgstattuple -
Functions to return statistics about "dead" tuples and free
space within a table
diff --git a/contrib/pgsql_fdw/.gitignore b/contrib/pgsql_fdw/.gitignore
index ...0854728 .
*** a/contrib/pgsql_fdw/.gitignore
--- b/contrib/pgsql_fdw/.gitignore
***************
*** 0 ****
--- 1,4 ----
+ # Generated subdirectories
+ /results/
+ *.o
+ *.so
diff --git a/contrib/pgsql_fdw/Makefile b/contrib/pgsql_fdw/Makefile
index ...6381365 .
*** a/contrib/pgsql_fdw/Makefile
--- b/contrib/pgsql_fdw/Makefile
***************
*** 0 ****
--- 1,22 ----
+ # contrib/pgsql_fdw/Makefile
+
+ MODULE_big = pgsql_fdw
+ OBJS = pgsql_fdw.o option.o deparse.o connection.o
+ PG_CPPFLAGS = -I$(libpq_srcdir)
+ SHLIB_LINK = $(libpq)
+
+ EXTENSION = pgsql_fdw
+ DATA = pgsql_fdw--1.0.sql
+
+ REGRESS = pgsql_fdw
+
+ ifdef USE_PGXS
+ PG_CONFIG = pg_config
+ PGXS := $(shell $(PG_CONFIG) --pgxs)
+ include $(PGXS)
+ else
+ subdir = contrib/pgsql_fdw
+ top_builddir = ../..
+ include $(top_builddir)/src/Makefile.global
+ include $(top_srcdir)/contrib/contrib-global.mk
+ endif
diff --git a/contrib/pgsql_fdw/connection.c b/contrib/pgsql_fdw/connection.c
index ...4a4e30c .
*** a/contrib/pgsql_fdw/connection.c
--- b/contrib/pgsql_fdw/connection.c
***************
*** 0 ****
--- 1,556 ----
+ /*-------------------------------------------------------------------------
+ *
+ * connection.c
+ * Connection management for pgsql_fdw
+ *
+ * Portions Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/connection.c
+ *
+ *-------------------------------------------------------------------------
+ */
+ #include "postgres.h"
+
+ #include "access/xact.h"
+ #include "catalog/pg_type.h"
+ #include "foreign/foreign.h"
+ #include "funcapi.h"
+ #include "libpq-fe.h"
+ #include "mb/pg_wchar.h"
+ #include "miscadmin.h"
+ #include "utils/array.h"
+ #include "utils/builtins.h"
+ #include "utils/hsearch.h"
+ #include "utils/memutils.h"
+ #include "utils/resowner.h"
+ #include "utils/tuplestore.h"
+
+ #include "pgsql_fdw.h"
+ #include "connection.h"
+
+ /* ============================================================================
+ * Connection management functions
+ * ==========================================================================*/
+
+ /*
+ * Connection cache entry managed with hash table.
+ */
+ typedef struct ConnCacheEntry
+ {
+ /* hash key must be first */
+ Oid serverid; /* oid of foreign server */
+ Oid userid; /* oid of local user */
+
+ bool use_tx; /* true when using remote transaction */
+ 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);
+ static void begin_remote_tx(PGconn *conn);
+ static void abort_remote_tx(PGconn *conn);
+
+ /*
+ * 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.
+ *
+ * When use_tx is true, remote transaction is started if caller is the only
+ * user of the connection. Isolation level of the remote transaction is same
+ * as local transaction, and remote transaction will be aborted when last
+ * user release.
+ *
+ * TODO: Note that caching connections requires a mechanism to detect change of
+ * FDW object to invalidate already established connections.
+ */
+ PGconn *
+ GetConnection(ForeignServer *server, UserMapping *user, bool use_tx)
+ {
+ bool found;
+ ConnCacheEntry *entry;
+ ConnCacheEntry key;
+
+ /* initialize connection cache if it isn't */
+ if (FSConnectionHash == NULL)
+ {
+ HASHCTL ctl;
+
+ /* hash key is a pair of oids: serverid and userid */
+ MemSet(&ctl, 0, sizeof(ctl));
+ ctl.keysize = sizeof(Oid) + sizeof(Oid);
+ ctl.entrysize = sizeof(ConnCacheEntry);
+ ctl.hash = tag_hash;
+ ctl.match = memcmp;
+ ctl.keycopy = memcpy;
+ /* allocate FSConnectionHash in the cache context */
+ ctl.hcxt = CacheMemoryContext;
+ FSConnectionHash = hash_create("Foreign Connections", 32,
+ &ctl,
+ HASH_ELEM | HASH_CONTEXT |
+ HASH_FUNCTION | HASH_COMPARE |
+ HASH_KEYCOPY);
+ }
+
+ /* Create key value for the entry. */
+ MemSet(&key, 0, sizeof(key));
+ key.serverid = server->serverid;
+ key.userid = GetOuterUserId();
+
+ /*
+ * Find cached entry for requested connection. If we couldn't find,
+ * callback function of ResourceOwner should be registered to clean the
+ * connection up on error including user interrupt.
+ */
+ entry = hash_search(FSConnectionHash, &key, HASH_ENTER, &found);
+ if (!found)
+ {
+ entry->refs = 0;
+ entry->use_tx = false;
+ entry->conn = NULL;
+ RegisterResourceReleaseCallback(cleanup_connection, entry);
+ }
+
+ /*
+ * We don't check the health of cached connection here, because it would
+ * require some overhead. Broken connection and its cache entry will be
+ * cleaned up when the connection is actually used.
+ */
+
+ /*
+ * If cache entry doesn't have connection, we have to establish new
+ * connection.
+ */
+ if (entry->conn == NULL)
+ {
+ /*
+ * Use PG_TRY block to ensure closing connection on error.
+ */
+ PG_TRY();
+ {
+ /*
+ * Connect to the foreign PostgreSQL server, and store it in cache
+ * entry to keep new connection.
+ * Note: key items of entry has already been initialized in
+ * hash_search(HASH_ENTER).
+ */
+ entry->conn = connect_pg_server(server, user);
+ }
+ PG_CATCH();
+ {
+ /* Clear connection cache entry on error case. */
+ PQfinish(entry->conn);
+ entry->refs = 0;
+ entry->use_tx = false;
+ entry->conn = NULL;
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+ }
+
+ /* Increase connection reference counter. */
+ entry->refs++;
+
+ /*
+ * If requester is the only referrer of this connection, start transaction
+ * with the same isolation level as the local transaction we are in.
+ * We need to remember whether this connection uses remote transaction to
+ * abort it when this connection is released completely.
+ */
+ if (use_tx && entry->refs == 1)
+ begin_remote_tx(entry->conn);
+ entry->use_tx = use_tx;
+
+
+ return entry->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 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. Those two object hold only libpq options.
+ * Extra 3 items are for:
+ * *) fallback_application_name
+ * *) client_encoding
+ * *) NULL termination (end marker)
+ *
+ * Note: We don't omit any parameters even target database might be older
+ * than local, because unexpected parameters are just ignored.
+ */
+ n = list_length(server->options) + list_length(user->options) + 3;
+ 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 += ExtractConnectionOptions(server->options,
+ all_keywords + n, all_values + n);
+ n += ExtractConnectionOptions(user->options,
+ all_keywords + n, all_values + n);
+ all_keywords[n] = all_values[n] = NULL;
+
+ for (i = 0, j = 0; all_keywords[i]; i++)
+ {
+ keywords[j] = all_keywords[i];
+ values[j] = all_values[i];
+ j++;
+ }
+
+ /* Use "pgsql_fdw" as fallback_application_name. */
+ keywords[j] = "fallback_application_name";
+ values[j++] = "pgsql_fdw";
+
+ /* Set client_encoding so that libpq can convert encoding properly. */
+ keywords[j] = "client_encoding";
+ values[j++] = GetDatabaseEncodingName();
+
+ keywords[j] = values[j] = NULL;
+ pfree(all_keywords);
+ pfree(all_values);
+
+ /* 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.")));
+ }
+
+ return conn;
+ }
+
+ /*
+ * Start transaction to use cursor to retrieve data separately.
+ */
+ static void
+ begin_remote_tx(PGconn *conn)
+ {
+ const char *sql = NULL; /* keep compiler quiet. */
+ PGresult *res;
+
+ switch (XactIsoLevel)
+ {
+ case XACT_READ_UNCOMMITTED:
+ case XACT_READ_COMMITTED:
+ case XACT_REPEATABLE_READ:
+ sql = "START TRANSACTION ISOLATION LEVEL REPEATABLE READ";
+ break;
+ case XACT_SERIALIZABLE:
+ sql = "START TRANSACTION ISOLATION LEVEL SERIALIZABLE";
+ break;
+ default:
+ elog(ERROR, "unexpected isolation level: %d", XactIsoLevel);
+ break;
+ }
+
+ elog(DEBUG3, "starting remote transaction with \"%s\"", sql);
+
+ res = PQexec(conn, sql);
+ if (PQresultStatus(res) != PGRES_COMMAND_OK)
+ {
+ PQclear(res);
+ elog(ERROR, "could not start transaction: %s", PQerrorMessage(conn));
+ }
+ PQclear(res);
+ }
+
+ static void
+ abort_remote_tx(PGconn *conn)
+ {
+ PGresult *res;
+
+ elog(DEBUG3, "aborting remote transaction");
+
+ res = PQexec(conn, "ABORT TRANSACTION");
+ if (PQresultStatus(res) != PGRES_COMMAND_OK)
+ {
+ PQclear(res);
+ elog(ERROR, "could not abort transaction: %s", PQerrorMessage(conn));
+ }
+ PQclear(res);
+ }
+
+ /*
+ * 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 sequentially 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;
+ }
+ if (entry != NULL)
+ hash_seq_term(&scan);
+
+ /*
+ * If this connection uses remote transaction and caller is the last user,
+ * abort remote transaction and forget about it.
+ */
+ if (entry->use_tx && entry->refs == 1)
+ {
+ abort_remote_tx(conn);
+ entry->use_tx = false;
+ }
+
+ /*
+ * If the released connection was an orphan, just close it.
+ */
+ if (entry == NULL)
+ {
+ PQfinish(conn);
+ return;
+ }
+
+ /*
+ * Decrease reference counter of this connection. Even if the caller was
+ * the last referrer, we don't unregister it from cache.
+ */
+ entry->refs--;
+ }
+
+ /*
+ * 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 need to clean up at end of subtransactions, because they might
+ * be recovered to consistent state with savepoints.
+ */
+ if (!isTopLevel)
+ 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)
+ {
+ PQfinish(entry->conn);
+ entry->refs = 0;
+ entry->conn = NULL;
+ }
+ }
+
+ /*
+ * Get list of connections currently active.
+ */
+ Datum pgsql_fdw_get_connections(PG_FUNCTION_ARGS);
+ PG_FUNCTION_INFO_V1(pgsql_fdw_get_connections);
+ Datum
+ pgsql_fdw_get_connections(PG_FUNCTION_ARGS)
+ {
+ ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+ HASH_SEQ_STATUS scan;
+ ConnCacheEntry *entry;
+ MemoryContext oldcontext = CurrentMemoryContext;
+ Tuplestorestate *tuplestore;
+ TupleDesc tupdesc;
+
+ /* We return list of connection with storing them in a Tuplestore. */
+ rsinfo->returnMode = SFRM_Materialize;
+ rsinfo->setResult = NULL;
+ rsinfo->setDesc = NULL;
+
+ /* Create tuplestore and copy of TupleDesc in per-query context. */
+ MemoryContextSwitchTo(rsinfo->econtext->ecxt_per_query_memory);
+
+ tupdesc = CreateTemplateTupleDesc(2, false);
+ TupleDescInitEntry(tupdesc, 1, "srvid", OIDOID, -1, 0);
+ TupleDescInitEntry(tupdesc, 2, "usesysid", OIDOID, -1, 0);
+ rsinfo->setDesc = tupdesc;
+
+ tuplestore = tuplestore_begin_heap(false, false, work_mem);
+ rsinfo->setResult = tuplestore;
+
+ MemoryContextSwitchTo(oldcontext);
+
+ /*
+ * We need to scan sequentially since we use the address to find
+ * appropriate PGconn from the hash table.
+ */
+ if (FSConnectionHash != NULL)
+ {
+ hash_seq_init(&scan, FSConnectionHash);
+ while ((entry = (ConnCacheEntry *) hash_seq_search(&scan)))
+ {
+ Datum values[2];
+ bool nulls[2];
+ HeapTuple tuple;
+
+ /* Ignore inactive connections */
+ if (PQstatus(entry->conn) != CONNECTION_OK)
+ continue;
+
+ /*
+ * Ignore other users' connections if current user isn't a
+ * superuser.
+ */
+ if (!superuser() && entry->userid != GetUserId())
+ continue;
+
+ values[0] = ObjectIdGetDatum(entry->serverid);
+ values[1] = ObjectIdGetDatum(entry->userid);
+ nulls[0] = false;
+ nulls[1] = false;
+
+ tuple = heap_formtuple(tupdesc, values, nulls);
+ tuplestore_puttuple(tuplestore, tuple);
+ }
+ }
+ tuplestore_donestoring(tuplestore);
+
+ PG_RETURN_VOID();
+ }
+
+ /*
+ * Discard persistent connection designated by given connection name.
+ */
+ Datum pgsql_fdw_disconnect(PG_FUNCTION_ARGS);
+ PG_FUNCTION_INFO_V1(pgsql_fdw_disconnect);
+ Datum
+ pgsql_fdw_disconnect(PG_FUNCTION_ARGS)
+ {
+ Oid serverid = PG_GETARG_OID(0);
+ Oid userid = PG_GETARG_OID(1);
+ ConnCacheEntry key;
+ ConnCacheEntry *entry = NULL;
+ bool found;
+
+ /* Non-superuser can't discard other users' connection. */
+ if (!superuser() && userid != GetOuterUserId())
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_RESOURCES),
+ errmsg("only superuser can discard other user's connection")));
+
+ /*
+ * If no connection has been established, or no such connections, just
+ * return "NG" to indicate nothing has done.
+ */
+ if (FSConnectionHash == NULL)
+ PG_RETURN_TEXT_P(cstring_to_text("NG"));
+
+ key.serverid = serverid;
+ key.userid = userid;
+ entry = hash_search(FSConnectionHash, &key, HASH_FIND, &found);
+ if (!found)
+ PG_RETURN_TEXT_P(cstring_to_text("NG"));
+
+ /* Discard cached connection, and clear reference counter. */
+ PQfinish(entry->conn);
+ entry->refs = 0;
+ entry->conn = NULL;
+
+ PG_RETURN_TEXT_P(cstring_to_text("OK"));
+ }
diff --git a/contrib/pgsql_fdw/connection.h b/contrib/pgsql_fdw/connection.h
index ...4427f56 .
*** a/contrib/pgsql_fdw/connection.h
--- b/contrib/pgsql_fdw/connection.h
***************
*** 0 ****
--- 1,25 ----
+ /*-------------------------------------------------------------------------
+ *
+ * connection.h
+ * Connection management for pgsql_fdw
+ *
+ * Portions Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_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, bool use_tx);
+ void ReleaseConnection(PGconn *conn);
+
+ #endif /* CONNECTION_H */
diff --git a/contrib/pgsql_fdw/deparse.c b/contrib/pgsql_fdw/deparse.c
index ...de49211 .
*** a/contrib/pgsql_fdw/deparse.c
--- b/contrib/pgsql_fdw/deparse.c
***************
*** 0 ****
--- 1,233 ----
+ /*-------------------------------------------------------------------------
+ *
+ * deparse.c
+ * query deparser for PostgreSQL
+ *
+ * Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/deparse.c
+ *
+ *-------------------------------------------------------------------------
+ */
+ #include "postgres.h"
+
+ #include "access/transam.h"
+ #include "commands/defrem.h"
+ #include "foreign/foreign.h"
+ #include "lib/stringinfo.h"
+ #include "nodes/nodeFuncs.h"
+ #include "nodes/nodes.h"
+ #include "nodes/makefuncs.h"
+ #include "optimizer/clauses.h"
+ #include "optimizer/var.h"
+ #include "parser/parsetree.h"
+ #include "utils/builtins.h"
+ #include "utils/lsyscache.h"
+
+ #include "pgsql_fdw.h"
+
+ /*
+ * Context for walk-through the expression tree.
+ */
+ typedef struct foreign_executable_cxt
+ {
+ PlannerInfo *root;
+ RelOptInfo *foreignrel;
+ } foreign_executable_cxt;
+
+ /*
+ * Deparse query representation into SQL statement which suits for remote
+ * PostgreSQL server.
+ */
+ char *
+ deparseSql(Oid relid,
+ PlannerInfo *root,
+ RelOptInfo *baserel,
+ ForeignTable *table)
+ {
+ StringInfoData foreign_relname;
+ StringInfoData sql; /* builder for SQL statement */
+ bool first;
+ AttrNumber attr;
+ List *attr_used = NIL; /* List of AttNumber used in the query */
+ ListCell *lc;
+ const char *nspname = NULL; /* plain namespace name */
+ const char *relname = NULL; /* plain relation name */
+ const char *q_nspname; /* quoted namespace name */
+ const char *q_relname; /* quoted relation name */
+ int i;
+ List *rtable = NIL;
+ List *context = NIL;
+
+ initStringInfo(&sql);
+ initStringInfo(&foreign_relname);
+
+ /*
+ * First of all, determine which column should be retrieved for this scan.
+ *
+ * We do this before deparsing SELECT clause because attributes which are
+ * not used in neither reltargetlist nor baserel->baserestrictinfo, quals
+ * evaluated on local, can be replaced with literal "NULL" in the SELECT
+ * clause to reduce overhead of tuple handling tuple and data transfer.
+ */
+ if (baserel->baserestrictinfo != NIL)
+ {
+ ListCell *lc;
+
+ foreach (lc, baserel->baserestrictinfo)
+ {
+ RestrictInfo *ri = (RestrictInfo *) lfirst(lc);
+ List *attrs;
+
+ /*
+ * We need to know which attributes are used in qual evaluated
+ * on the local server, because they should be listed in the
+ * SELECT clause of remote query. We can ignore attributes
+ * which are referenced only in ORDER BY/GROUP BY clause because
+ * such attributes has already been kept in reltargetlist.
+ */
+ attrs = pull_var_clause((Node *) ri->clause,
+ PVC_RECURSE_AGGREGATES,
+ PVC_RECURSE_PLACEHOLDERS);
+ attr_used = list_union(attr_used, attrs);
+ }
+ }
+
+ /*
+ * Determine foreign relation's qualified name. This is necessary for
+ * FROM clause and SELECT clause.
+ */
+ foreach(lc, table->options)
+ {
+ DefElem *def = (DefElem *) lfirst(lc);
+
+ if (strcmp(def->defname, "nspname") == 0)
+ nspname = defGetString(def);
+ else if (strcmp(def->defname, "relname") == 0)
+ relname = defGetString(def);
+ }
+
+ if (nspname == NULL)
+ nspname = get_namespace_name(get_rel_namespace(relid));
+ q_nspname = quote_identifier(nspname);
+
+ if (relname == NULL)
+ relname = get_rel_name(relid);
+ q_relname = quote_identifier(relname);
+
+ appendStringInfo(&foreign_relname, "%s.%s", q_nspname, q_relname);
+
+ /*
+ * We need to replace aliasname and colnames of the target relation so that
+ * constructed remote query is valid.
+ *
+ * Note that we skip first empty element of simple_rel_array. See also
+ * comments of simple_rel_array and simple_rte_array for the rationale.
+ */
+ for (i = 1; i < root->simple_rel_array_size; i++)
+ {
+ RangeTblEntry *rte = copyObject(root->simple_rte_array[i]);
+ List *newcolnames = NIL;
+
+ if (i == baserel->relid)
+ {
+ /*
+ * Create new list of column names which is used to deparse remote
+ * query from specified names or local column names. This list is
+ * used by deparse_expression.
+ */
+ for (attr = 1; attr <= baserel->max_attr; attr++)
+ {
+ char *colname = NULL;
+
+ /*
+ * Get colname option for each undropped attribute. If colname
+ * option is not supplied, use local column name in remote
+ * query.
+ */
+ if (get_rte_attribute_is_dropped(rte, attr))
+ colname = "";
+ else
+ {
+ List *options;
+ ListCell *lc;
+
+ options = GetForeignColumnOptions(relid, attr);
+ foreach(lc, options)
+ {
+ DefElem *def = (DefElem *) lfirst(lc);
+
+ if (strcmp(def->defname, "colname") == 0)
+ {
+ colname = defGetString(def);
+ break;
+ }
+ }
+ if (colname == NULL)
+ colname = strVal(list_nth(rte->eref->colnames,
+ attr - 1));
+ }
+ newcolnames = lappend(newcolnames, makeString(colname));
+ }
+ rte->alias = makeAlias(relname, newcolnames);
+ }
+ rtable = lappend(rtable, rte);
+ }
+ context = deparse_context_for_rtelist(rtable);
+
+ /*
+ * deparse SELECT clause
+ *
+ * List attributes which are in either target list or local restriction.
+ * Unused attributes are replaced with a literal "NULL" for optimization.
+ *
+ * Note that nothing is added for dropped columns, though tuple constructor
+ * function requires entries for dropped columns. Such entries must be
+ * initialized with NULL before calling tuple constructor.
+ */
+ appendStringInfo(&sql, "SELECT ");
+ attr_used = list_union(attr_used, baserel->reltargetlist);
+ first = true;
+ for (attr = 1; attr <= baserel->max_attr; attr++)
+ {
+ RangeTblEntry *rte = root->simple_rte_array[baserel->relid];
+ Var *var = NULL;
+ ListCell *lc;
+
+ /* Ignore dropped attributes. */
+ if (get_rte_attribute_is_dropped(rte, attr))
+ continue;
+
+ if (!first)
+ appendStringInfo(&sql, ", ");
+ first = false;
+
+ /*
+ * We use linear search here, but it wouldn't be problem since
+ * attr_used seems to not become so large.
+ */
+ foreach (lc, attr_used)
+ {
+ var = lfirst(lc);
+ if (var->varattno == attr)
+ break;
+ var = NULL;
+ }
+ if (var != NULL)
+ appendStringInfo(&sql, "%s",
+ deparse_expression((Node *) var, context, false, false));
+ else
+ appendStringInfo(&sql, "NULL");
+ }
+ appendStringInfoChar(&sql, ' ');
+
+ /*
+ * deparse FROM clause, including alias if any
+ */
+ appendStringInfo(&sql, "FROM %s", foreign_relname.data);
+
+ elog(DEBUG3, "Remote SQL: %s", sql.data);
+ return sql.data;
+ }
+
diff --git a/contrib/pgsql_fdw/expected/pgsql_fdw.out b/contrib/pgsql_fdw/expected/pgsql_fdw.out
index ...c1e2e82 .
*** a/contrib/pgsql_fdw/expected/pgsql_fdw.out
--- b/contrib/pgsql_fdw/expected/pgsql_fdw.out
***************
*** 0 ****
--- 1,592 ----
+ -- ===================================================================
+ -- create FDW objects
+ -- ===================================================================
+ -- Clean up in case a prior regression run failed
+ -- Suppress NOTICE messages when roles don't exist
+ SET client_min_messages TO 'error';
+ DROP ROLE IF EXISTS pgsql_fdw_user;
+ RESET client_min_messages;
+ CREATE ROLE pgsql_fdw_user LOGIN SUPERUSER;
+ SET SESSION AUTHORIZATION 'pgsql_fdw_user';
+ CREATE EXTENSION pgsql_fdw;
+ CREATE SERVER loopback1 FOREIGN DATA WRAPPER pgsql_fdw;
+ CREATE SERVER loopback2 FOREIGN DATA WRAPPER pgsql_fdw
+ OPTIONS (dbname 'contrib_regression');
+ CREATE USER MAPPING FOR public SERVER loopback1
+ OPTIONS (user 'value', password 'value');
+ CREATE USER MAPPING FOR pgsql_fdw_user SERVER loopback2;
+ CREATE FOREIGN TABLE ft1 (
+ c0 int,
+ c1 int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10)
+ ) SERVER loopback2;
+ ALTER FOREIGN TABLE ft1 DROP COLUMN c0;
+ CREATE FOREIGN TABLE ft2 (
+ c0 int,
+ c1 int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10)
+ ) SERVER loopback2;
+ ALTER FOREIGN TABLE ft2 DROP COLUMN c0;
+ -- ===================================================================
+ -- create objects used through FDW
+ -- ===================================================================
+ CREATE SCHEMA "S 1";
+ CREATE TABLE "S 1"."T 1" (
+ "C 1" int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10),
+ CONSTRAINT t1_pkey PRIMARY KEY ("C 1")
+ );
+ NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "t1_pkey" for table "T 1"
+ CREATE TABLE "S 1"."T 2" (
+ c1 int NOT NULL,
+ c2 text,
+ CONSTRAINT t2_pkey PRIMARY KEY (c1)
+ );
+ NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "t2_pkey" for table "T 2"
+ BEGIN;
+ TRUNCATE "S 1"."T 1";
+ INSERT INTO "S 1"."T 1"
+ SELECT id,
+ id % 10,
+ to_char(id, 'FM00000'),
+ '1970-01-01'::timestamptz + ((id % 100) || ' days')::interval,
+ '1970-01-01'::timestamp + ((id % 100) || ' days')::interval,
+ id % 10,
+ id % 10
+ FROM generate_series(1, 1000) id;
+ TRUNCATE "S 1"."T 2";
+ INSERT INTO "S 1"."T 2"
+ SELECT id,
+ 'AAA' || to_char(id, 'FM000')
+ FROM generate_series(1, 100) id;
+ COMMIT;
+ -- ===================================================================
+ -- tests for pgsql_fdw_validator
+ -- ===================================================================
+ ALTER FOREIGN DATA WRAPPER pgsql_fdw OPTIONS (host 'value'); -- ERROR
+ ERROR: invalid option "host"
+ HINT: Valid options in this context are:
+ -- requiressl, krbsrvname and gsslib are omitted because they depend on
+ -- configure option
+ ALTER SERVER loopback1 OPTIONS (
+ authtype 'value',
+ service 'value',
+ connect_timeout 'value',
+ dbname 'value',
+ host 'value',
+ hostaddr 'value',
+ port 'value',
+ --client_encoding 'value',
+ tty 'value',
+ options 'value',
+ application_name 'value',
+ --fallback_application_name 'value',
+ keepalives 'value',
+ keepalives_idle 'value',
+ keepalives_interval 'value',
+ -- requiressl 'value',
+ sslcompression 'value',
+ sslmode 'value',
+ sslcert 'value',
+ sslkey 'value',
+ sslrootcert 'value',
+ sslcrl 'value'
+ --requirepeer 'value',
+ -- krbsrvname 'value',
+ -- gsslib 'value',
+ --replication 'value'
+ );
+ ALTER SERVER loopback1 OPTIONS (user 'value'); -- ERROR
+ ERROR: invalid option "user"
+ HINT: Valid options in this context are: authtype, service, connect_timeout, dbname, host, hostaddr, port, tty, options, application_name, keepalives, keepalives_idle, keepalives_interval, keepalives_count, requiressl, sslcompression, sslmode, sslcert, sslkey, sslrootcert, sslcrl, requirepeer, krbsrvname, gsslib, fetch_count
+ ALTER SERVER loopback2 OPTIONS (ADD fetch_count '2');
+ ALTER USER MAPPING FOR public SERVER loopback1
+ OPTIONS (DROP user, DROP password);
+ ALTER USER MAPPING FOR public SERVER loopback1
+ OPTIONS (host 'value'); -- ERROR
+ ERROR: invalid option "host"
+ HINT: Valid options in this context are: user, password
+ ALTER FOREIGN TABLE ft1 OPTIONS (nspname 'S 1', relname 'T 1');
+ ALTER FOREIGN TABLE ft2 OPTIONS (nspname 'S 1', relname 'T 1', fetch_count '100');
+ ALTER FOREIGN TABLE ft1 OPTIONS (invalid 'value'); -- ERROR
+ ERROR: invalid option "invalid"
+ HINT: Valid options in this context are: nspname, relname, fetch_count
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count 'a'); -- ERROR
+ ERROR: invalid value for fetch_count: "a"
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count '0'); -- ERROR
+ ERROR: invalid value for fetch_count: "0"
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count '-1'); -- ERROR
+ ERROR: invalid value for fetch_count: "-1"
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (invalid 'value'); -- ERROR
+ ERROR: invalid option "invalid"
+ HINT: Valid options in this context are: colname
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (colname 'C 1');
+ ALTER FOREIGN TABLE ft2 ALTER COLUMN c1 OPTIONS (colname 'C 1');
+ \dew+
+ List of foreign-data wrappers
+ Name | Owner | Handler | Validator | Access privileges | FDW Options | Description
+ -----------+----------------+-------------------+---------------------+-------------------+-------------+-------------
+ pgsql_fdw | pgsql_fdw_user | pgsql_fdw_handler | pgsql_fdw_validator | | |
+ (1 row)
+
+ \des+
+ List of foreign servers
+ Name | Owner | Foreign-data wrapper | Access privileges | Type | Version | FDW Options | Description
+ -----------+----------------+----------------------+-------------------+------+---------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+-------------
+ loopback1 | pgsql_fdw_user | pgsql_fdw | | | | (authtype 'value', service 'value', connect_timeout 'value', dbname 'value', host 'value', hostaddr 'value', port 'value', tty 'value', options 'value', application_name 'value', keepalives 'value', keepalives_idle 'value', keepalives_interval 'value', sslcompression 'value', sslmode 'value', sslcert 'value', sslkey 'value', sslrootcert 'value', sslcrl 'value') |
+ loopback2 | pgsql_fdw_user | pgsql_fdw | | | | (dbname 'contrib_regression', fetch_count '2') |
+ (2 rows)
+
+ \deu+
+ List of user mappings
+ Server | User name | FDW Options
+ -----------+----------------+-------------
+ loopback1 | public |
+ loopback2 | pgsql_fdw_user |
+ (2 rows)
+
+ \det+
+ List of foreign tables
+ Schema | Table | Server | FDW Options | Description
+ --------+-------+-----------+---------------------------------------------------+-------------
+ public | ft1 | loopback2 | (nspname 'S 1', relname 'T 1') |
+ public | ft2 | loopback2 | (nspname 'S 1', relname 'T 1', fetch_count '100') |
+ (2 rows)
+
+ -- ===================================================================
+ -- simple queries
+ -- ===================================================================
+ -- single table, with/without alias
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
+ QUERY PLAN
+ ------------------------------------------------------------------------------------------------------------------------------
+ Limit
+ Output: c1, c2, c3, c4, c5, c6, c7
+ -> Sort
+ Output: c1, c2, c3, c4, c5, c6, c7
+ Sort Key: ft1.c3, ft1.c1
+ -> Foreign Scan on public.ft1
+ Output: c1, c2, c3, c4, c5, c6, c7
+ Remote SQL: DECLARE pgsql_fdw_cursor_0 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (8 rows)
+
+ SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ -----+----+-------+------------------------------+--------------------------+----+------------
+ 101 | 1 | 00101 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970 | 1 | 1
+ 102 | 2 | 00102 | Sat Jan 03 00:00:00 1970 PST | Sat Jan 03 00:00:00 1970 | 2 | 2
+ 103 | 3 | 00103 | Sun Jan 04 00:00:00 1970 PST | Sun Jan 04 00:00:00 1970 | 3 | 3
+ 104 | 4 | 00104 | Mon Jan 05 00:00:00 1970 PST | Mon Jan 05 00:00:00 1970 | 4 | 4
+ 105 | 5 | 00105 | Tue Jan 06 00:00:00 1970 PST | Tue Jan 06 00:00:00 1970 | 5 | 5
+ 106 | 6 | 00106 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6
+ 107 | 7 | 00107 | Thu Jan 08 00:00:00 1970 PST | Thu Jan 08 00:00:00 1970 | 7 | 7
+ 108 | 8 | 00108 | Fri Jan 09 00:00:00 1970 PST | Fri Jan 09 00:00:00 1970 | 8 | 8
+ 109 | 9 | 00109 | Sat Jan 10 00:00:00 1970 PST | Sat Jan 10 00:00:00 1970 | 9 | 9
+ 110 | 0 | 00110 | Sun Jan 11 00:00:00 1970 PST | Sun Jan 11 00:00:00 1970 | 0 | 0
+ (10 rows)
+
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+ QUERY PLAN
+ ------------------------------------------------------------------------------------------------------------------------------
+ Limit
+ Output: c1, c2, c3, c4, c5, c6, c7
+ -> Sort
+ Output: c1, c2, c3, c4, c5, c6, c7
+ Sort Key: t1.c3, t1.c1
+ -> Foreign Scan on public.ft1 t1
+ Output: c1, c2, c3, c4, c5, c6, c7
+ Remote SQL: DECLARE pgsql_fdw_cursor_2 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (8 rows)
+
+ SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ -----+----+-------+------------------------------+--------------------------+----+------------
+ 101 | 1 | 00101 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970 | 1 | 1
+ 102 | 2 | 00102 | Sat Jan 03 00:00:00 1970 PST | Sat Jan 03 00:00:00 1970 | 2 | 2
+ 103 | 3 | 00103 | Sun Jan 04 00:00:00 1970 PST | Sun Jan 04 00:00:00 1970 | 3 | 3
+ 104 | 4 | 00104 | Mon Jan 05 00:00:00 1970 PST | Mon Jan 05 00:00:00 1970 | 4 | 4
+ 105 | 5 | 00105 | Tue Jan 06 00:00:00 1970 PST | Tue Jan 06 00:00:00 1970 | 5 | 5
+ 106 | 6 | 00106 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6
+ 107 | 7 | 00107 | Thu Jan 08 00:00:00 1970 PST | Thu Jan 08 00:00:00 1970 | 7 | 7
+ 108 | 8 | 00108 | Fri Jan 09 00:00:00 1970 PST | Fri Jan 09 00:00:00 1970 | 8 | 8
+ 109 | 9 | 00109 | Sat Jan 10 00:00:00 1970 PST | Sat Jan 10 00:00:00 1970 | 9 | 9
+ 110 | 0 | 00110 | Sun Jan 11 00:00:00 1970 PST | Sun Jan 11 00:00:00 1970 | 0 | 0
+ (10 rows)
+
+ -- with WHERE clause
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
+ QUERY PLAN
+ ------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on public.ft1 t1
+ Output: c1, c2, c3, c4, c5, c6, c7
+ Filter: ((t1.c1 = 101) AND ((t1.c6)::text = '1'::text) AND (t1.c7 = '1'::bpchar))
+ Remote SQL: DECLARE pgsql_fdw_cursor_4 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (4 rows)
+
+ SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ -----+----+-------+------------------------------+--------------------------+----+------------
+ 101 | 1 | 00101 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970 | 1 | 1
+ (1 row)
+
+ -- aggregate
+ SELECT COUNT(*) FROM ft1 t1;
+ count
+ -------
+ 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
+ ----+----+-------+------------------------------+--------------------------+----+------------
+ 1 | 1 | 00001 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970 | 1 | 1
+ 2 | 2 | 00002 | Sat Jan 03 00:00:00 1970 PST | Sat Jan 03 00:00:00 1970 | 2 | 2
+ 3 | 3 | 00003 | Sun Jan 04 00:00:00 1970 PST | Sun Jan 04 00:00:00 1970 | 3 | 3
+ 4 | 4 | 00004 | Mon Jan 05 00:00:00 1970 PST | Mon Jan 05 00:00:00 1970 | 4 | 4
+ 5 | 5 | 00005 | Tue Jan 06 00:00:00 1970 PST | Tue Jan 06 00:00:00 1970 | 5 | 5
+ 6 | 6 | 00006 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6
+ 7 | 7 | 00007 | Thu Jan 08 00:00:00 1970 PST | Thu Jan 08 00:00:00 1970 | 7 | 7
+ 8 | 8 | 00008 | Fri Jan 09 00:00:00 1970 PST | Fri Jan 09 00:00:00 1970 | 8 | 8
+ 9 | 9 | 00009 | Sat Jan 10 00:00:00 1970 PST | Sat Jan 10 00:00:00 1970 | 9 | 9
+ 10 | 0 | 00010 | Sun Jan 11 00:00:00 1970 PST | Sun Jan 11 00:00:00 1970 | 0 | 0
+ (10 rows)
+
+ -- subquery+MAX
+ SELECT * FROM ft1 t1 WHERE t1.c3 = (SELECT MAX(c3) FROM ft2 t2) ORDER BY c1;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ ------+----+-------+------------------------------+--------------------------+----+------------
+ 1000 | 0 | 01000 | Thu Jan 01 00:00:00 1970 PST | Thu Jan 01 00:00:00 1970 | 0 | 0
+ (1 row)
+
+ -- used in CTE
+ WITH t1 AS (SELECT * FROM ft1 WHERE c1 <= 10) SELECT t2.c1, t2.c2, t2.c3, t2.c4 FROM t1, ft2 t2 WHERE t1.c1 = t2.c1 ORDER BY t1.c1;
+ c1 | c2 | c3 | c4
+ ----+----+-------+------------------------------
+ 1 | 1 | 00001 | Fri Jan 02 00:00:00 1970 PST
+ 2 | 2 | 00002 | Sat Jan 03 00:00:00 1970 PST
+ 3 | 3 | 00003 | Sun Jan 04 00:00:00 1970 PST
+ 4 | 4 | 00004 | Mon Jan 05 00:00:00 1970 PST
+ 5 | 5 | 00005 | Tue Jan 06 00:00:00 1970 PST
+ 6 | 6 | 00006 | Wed Jan 07 00:00:00 1970 PST
+ 7 | 7 | 00007 | Thu Jan 08 00:00:00 1970 PST
+ 8 | 8 | 00008 | Fri Jan 09 00:00:00 1970 PST
+ 9 | 9 | 00009 | Sat Jan 10 00:00:00 1970 PST
+ 10 | 0 | 00010 | Sun Jan 11 00:00:00 1970 PST
+ (10 rows)
+
+ -- fixed values
+ SELECT 'fixed', NULL FROM ft1 t1 WHERE c1 = 1;
+ ?column? | ?column?
+ ----------+----------
+ fixed |
+ (1 row)
+
+ -- user-defined operator/function
+ CREATE FUNCTION pgsql_fdw_abs(int) RETURNS int AS $$
+ BEGIN
+ RETURN abs($1);
+ END
+ $$ LANGUAGE plpgsql IMMUTABLE;
+ CREATE OPERATOR === (
+ LEFTARG = int,
+ RIGHTARG = int,
+ PROCEDURE = int4eq,
+ COMMUTATOR = ===,
+ NEGATOR = !==
+ );
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = pgsql_fdw_abs(t1.c2);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = pgsql_fdw_abs(c2))
+ Remote SQL: DECLARE pgsql_fdw_cursor_18 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 === t1.c2;
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 === c2)
+ Remote SQL: DECLARE pgsql_fdw_cursor_19 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = abs(t1.c2);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = abs(c2))
+ Remote SQL: DECLARE pgsql_fdw_cursor_20 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = t1.c2;
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = c2)
+ Remote SQL: DECLARE pgsql_fdw_cursor_21 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ -- ===================================================================
+ -- parameterized queries
+ -- ===================================================================
+ -- simple join
+ PREPARE st1(int, int) AS SELECT t1.c3, t2.c3 FROM ft1 t1, ft2 t2 WHERE t1.c1 = $1 AND t2.c1 = $2;
+ EXPLAIN (COSTS false) EXECUTE st1(1, 2);
+ QUERY PLAN
+ -----------------------------------------------------------------------------------------------------------------------------------------
+ Nested Loop
+ -> Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_22 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, NULL, NULL, NULL, NULL FROM "S 1"."T 1"
+ -> Materialize
+ -> Foreign Scan on ft2 t2
+ Filter: (c1 = 2)
+ Remote SQL: DECLARE pgsql_fdw_cursor_23 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, NULL, NULL, NULL, NULL FROM "S 1"."T 1"
+ (8 rows)
+
+ EXECUTE st1(1, 1);
+ c3 | c3
+ -------+-------
+ 00001 | 00001
+ (1 row)
+
+ EXECUTE st1(101, 101);
+ c3 | c3
+ -------+-------
+ 00101 | 00101
+ (1 row)
+
+ -- subquery using stable function (can't be pushed down)
+ PREPARE st2(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c4) = 6) ORDER BY c1;
+ EXPLAIN (COSTS false) EXECUTE st2(10, 20);
+ QUERY PLAN
+ ---------------------------------------------------------------------------------------------------------------------------------------------------
+ Sort
+ Sort Key: t1.c1
+ -> Hash Join
+ Hash Cond: (t1.c3 = t2.c3)
+ -> Foreign Scan on ft1 t1
+ Filter: (c1 < 20)
+ Remote SQL: DECLARE pgsql_fdw_cursor_28 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ -> Hash
+ -> HashAggregate
+ -> Foreign Scan on ft2 t2
+ Filter: ((c1 > 10) AND (date_part('dow'::text, c4) = 6::double precision))
+ Remote SQL: DECLARE pgsql_fdw_cursor_29 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, c4, NULL, NULL, NULL FROM "S 1"."T 1"
+ (12 rows)
+
+ EXECUTE st2(10, 20);
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ ----+----+-------+------------------------------+--------------------------+----+------------
+ 16 | 6 | 00016 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970 | 6 | 6
+ (1 row)
+
+ EXECUTE st1(101, 101);
+ c3 | c3
+ -------+-------
+ 00101 | 00101
+ (1 row)
+
+ -- subquery using immutable function (can be pushed down)
+ PREPARE st3(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c5) = 6) ORDER BY c1;
+ EXPLAIN (COSTS false) EXECUTE st3(10, 20);
+ QUERY PLAN
+ ---------------------------------------------------------------------------------------------------------------------------------------------------
+ Sort
+ Sort Key: t1.c1
+ -> Hash Join
+ Hash Cond: (t1.c3 = t2.c3)
+ -> Foreign Scan on ft1 t1
+ Filter: (c1 < 20)
+ Remote SQL: DECLARE pgsql_fdw_cursor_34 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ -> Hash
+ -> HashAggregate
+ -> Foreign Scan on ft2 t2
+ Filter: ((c1 > 10) AND (date_part('dow'::text, c5) = 6::double precision))
+ Remote SQL: DECLARE pgsql_fdw_cursor_35 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, NULL, c5, NULL, NULL FROM "S 1"."T 1"
+ (12 rows)
+
+ EXECUTE st3(10, 20);
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ ----+----+-------+------------------------------+--------------------------+----+------------
+ 16 | 6 | 00016 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970 | 6 | 6
+ (1 row)
+
+ EXECUTE st3(20, 30);
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ ----+----+-------+------------------------------+--------------------------+----+------------
+ 23 | 3 | 00023 | Sat Jan 24 00:00:00 1970 PST | Sat Jan 24 00:00:00 1970 | 3 | 3
+ (1 row)
+
+ -- custom plan should be chosen
+ PREPARE st4(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 = $1;
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_40 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_41 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_42 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_43 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_44 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = $1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_45 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ -- cleanup
+ DEALLOCATE st1;
+ DEALLOCATE st2;
+ DEALLOCATE st3;
+ DEALLOCATE st4;
+ -- ===================================================================
+ -- connection management
+ -- ===================================================================
+ SELECT srvname, usename FROM pgsql_fdw_connections;
+ srvname | usename
+ -----------+----------------
+ loopback2 | pgsql_fdw_user
+ (1 row)
+
+ SELECT pgsql_fdw_disconnect(srvid, usesysid) FROM pgsql_fdw_get_connections();
+ pgsql_fdw_disconnect
+ ----------------------
+ OK
+ (1 row)
+
+ SELECT srvname, usename FROM pgsql_fdw_connections;
+ srvname | usename
+ ---------+---------
+ (0 rows)
+
+ -- ===================================================================
+ -- subtransaction
+ -- ===================================================================
+ BEGIN;
+ DECLARE c CURSOR FOR SELECT * FROM ft1 ORDER BY c1;
+ FETCH c;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ ----+----+-------+------------------------------+--------------------------+----+------------
+ 1 | 1 | 00001 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970 | 1 | 1
+ (1 row)
+
+ SAVEPOINT s;
+ ERROR OUT; -- ERROR
+ ERROR: syntax error at or near "ERROR"
+ LINE 1: ERROR OUT;
+ ^
+ ROLLBACK TO s;
+ SELECT srvname FROM pgsql_fdw_connections;
+ srvname
+ -----------
+ loopback2
+ (1 row)
+
+ FETCH c;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ ----+----+-------+------------------------------+--------------------------+----+------------
+ 2 | 2 | 00002 | Sat Jan 03 00:00:00 1970 PST | Sat Jan 03 00:00:00 1970 | 2 | 2
+ (1 row)
+
+ COMMIT;
+ SELECT srvname FROM pgsql_fdw_connections;
+ srvname
+ -----------
+ loopback2
+ (1 row)
+
+ ERROR OUT; -- ERROR
+ ERROR: syntax error at or near "ERROR"
+ LINE 1: ERROR OUT;
+ ^
+ SELECT srvname FROM pgsql_fdw_connections;
+ srvname
+ ---------
+ (0 rows)
+
+ -- ===================================================================
+ -- cleanup
+ -- ===================================================================
+ DROP OPERATOR === (int, int) CASCADE;
+ DROP OPERATOR !== (int, int) CASCADE;
+ DROP FUNCTION pgsql_fdw_abs(int);
+ DROP SCHEMA "S 1" CASCADE;
+ NOTICE: drop cascades to 2 other objects
+ DETAIL: drop cascades to table "S 1"."T 1"
+ drop cascades to table "S 1"."T 2"
+ DROP EXTENSION pgsql_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 server loopback2
+ drop cascades to user mapping for pgsql_fdw_user
+ drop cascades to foreign table ft1
+ drop cascades to foreign table ft2
+ \c
+ DROP ROLE pgsql_fdw_user;
diff --git a/contrib/pgsql_fdw/option.c b/contrib/pgsql_fdw/option.c
index ...a2a8927 .
*** a/contrib/pgsql_fdw/option.c
--- b/contrib/pgsql_fdw/option.c
***************
*** 0 ****
--- 1,242 ----
+ /*-------------------------------------------------------------------------
+ *
+ * option.c
+ * FDW option handling
+ *
+ * Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/option.c
+ *
+ *-------------------------------------------------------------------------
+ */
+ #include "postgres.h"
+
+ #include "access/reloptions.h"
+ #include "catalog/pg_foreign_data_wrapper.h"
+ #include "catalog/pg_foreign_server.h"
+ #include "catalog/pg_foreign_table.h"
+ #include "catalog/pg_user_mapping.h"
+ #include "commands/defrem.h"
+ #include "fmgr.h"
+ #include "foreign/foreign.h"
+ #include "lib/stringinfo.h"
+ #include "miscadmin.h"
+
+ #include "pgsql_fdw.h"
+
+ /*
+ * SQL functions
+ */
+ extern Datum pgsql_fdw_validator(PG_FUNCTION_ARGS);
+ PG_FUNCTION_INFO_V1(pgsql_fdw_validator);
+
+ /*
+ * Describes the valid options for objects that use this wrapper.
+ */
+ typedef struct PgsqlFdwOption
+ {
+ const char *optname;
+ Oid optcontext; /* Oid of catalog in which options may appear */
+ bool is_libpq_opt; /* true if it's used in libpq */
+ } PgsqlFdwOption;
+
+ /*
+ * Valid options for pgsql_fdw.
+ */
+ static PgsqlFdwOption valid_options[] = {
+
+ /*
+ * Options for libpq connection.
+ * Note: This list should be updated along with PQconninfoOptions in
+ * interfaces/libpq/fe-connect.c, so the order is kept as is.
+ *
+ * Some useless libpq connection options are not accepted by pgsql_fdw:
+ * client_encoding: set to local database encoding automatically
+ * fallback_application_name: fixed to "pgsql_fdw"
+ * replication: pgsql_fdw never be replication client
+ */
+ {"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},
+ #ifdef NOT_USED
+ {"client_encoding", ForeignServerRelationId, true},
+ #endif
+ {"tty", ForeignServerRelationId, true},
+ {"options", ForeignServerRelationId, true},
+ {"application_name", ForeignServerRelationId, true},
+ #ifdef NOT_USED
+ {"fallback_application_name", ForeignServerRelationId, true},
+ #endif
+ {"keepalives", ForeignServerRelationId, true},
+ {"keepalives_idle", ForeignServerRelationId, true},
+ {"keepalives_interval", ForeignServerRelationId, true},
+ {"keepalives_count", ForeignServerRelationId, true},
+ {"requiressl", ForeignServerRelationId, true},
+ {"sslcompression", ForeignServerRelationId, true},
+ {"sslmode", ForeignServerRelationId, true},
+ {"sslcert", ForeignServerRelationId, true},
+ {"sslkey", ForeignServerRelationId, true},
+ {"sslrootcert", ForeignServerRelationId, true},
+ {"sslcrl", ForeignServerRelationId, true},
+ {"requirepeer", ForeignServerRelationId, true},
+ {"krbsrvname", ForeignServerRelationId, true},
+ {"gsslib", ForeignServerRelationId, true},
+ #ifdef NOT_USED
+ {"replication", ForeignServerRelationId, true},
+ #endif
+
+ /*
+ * Options for translation of object names.
+ */
+ {"nspname", ForeignTableRelationId, false},
+ {"relname", ForeignTableRelationId, false},
+ {"colname", AttributeRelationId, false},
+
+ /*
+ * Options for cursor behavior.
+ * These options can be overridden by finer-grained objects.
+ */
+ {"fetch_count", ForeignTableRelationId, false},
+ {"fetch_count", ForeignServerRelationId, false},
+
+ /* Terminating entry --- MUST BE LAST */
+ {NULL, InvalidOid, false}
+ };
+
+ /*
+ * Helper functions
+ */
+ static bool is_valid_option(const char *optname, Oid context);
+
+ /*
+ * Validate the generic options given to a FOREIGN DATA WRAPPER, SERVER,
+ * USER MAPPING or FOREIGN TABLE that uses pgsql_fdw.
+ *
+ * Raise an ERROR if the option or its value is considered invalid.
+ */
+ Datum
+ pgsql_fdw_validator(PG_FUNCTION_ARGS)
+ {
+ List *options_list = untransformRelOptions(PG_GETARG_DATUM(0));
+ Oid catalog = PG_GETARG_OID(1);
+ ListCell *cell;
+
+ /*
+ * Check that only options supported by pgsql_fdw, and allowed for the
+ * current object type, are given.
+ */
+ foreach(cell, options_list)
+ {
+ DefElem *def = (DefElem *) lfirst(cell);
+
+ if (!is_valid_option(def->defname, catalog))
+ {
+ PgsqlFdwOption *opt;
+ StringInfoData buf;
+
+ /*
+ * Unknown option specified, complain about it. Provide a hint
+ * with list of valid options for the object.
+ */
+ initStringInfo(&buf);
+ for (opt = valid_options; opt->optname; opt++)
+ {
+ if (catalog == opt->optcontext)
+ appendStringInfo(&buf, "%s%s", (buf.len > 0) ? ", " : "",
+ opt->optname);
+ }
+
+ ereport(ERROR,
+ (errcode(ERRCODE_FDW_INVALID_OPTION_NAME),
+ errmsg("invalid option \"%s\"", def->defname),
+ errhint("Valid options in this context are: %s",
+ buf.data)));
+ }
+
+ /* fetch_count be positive digit number. */
+ if (strcmp(def->defname, "fetch_count") == 0)
+ {
+ long value;
+ char *p = NULL;
+
+ value = strtol(defGetString(def), &p, 10);
+ if (*p != '\0' || value < 1)
+ ereport(ERROR,
+ (errcode(ERRCODE_FDW_INVALID_ATTRIBUTE_VALUE),
+ errmsg("invalid value for %s: \"%s\"",
+ def->defname, defGetString(def))));
+ }
+ }
+
+ /*
+ * We don't care option-specific limitation here; they will be validated at
+ * the execution time.
+ */
+
+ PG_RETURN_VOID();
+ }
+
+ /*
+ * Check whether the given option is one of the valid pgsql_fdw options.
+ * context is the Oid of the catalog holding the object the option is for.
+ */
+ static bool
+ is_valid_option(const char *optname, Oid context)
+ {
+ PgsqlFdwOption *opt;
+
+ for (opt = valid_options; opt->optname; opt++)
+ {
+ if (context == opt->optcontext && strcmp(opt->optname, optname) == 0)
+ return true;
+ }
+ return false;
+ }
+
+ /*
+ * Check whether the given option is one of the valid libpq options.
+ * context is the Oid of the catalog holding the object the option is for.
+ */
+ static bool
+ is_libpq_option(const char *optname)
+ {
+ PgsqlFdwOption *opt;
+
+ for (opt = valid_options; opt->optname; opt++)
+ {
+ if (strcmp(opt->optname, optname) == 0 && opt->is_libpq_opt)
+ return true;
+ }
+ return false;
+ }
+
+ /*
+ * Generate key-value arrays which includes only libpq options from the list
+ * which contains any kind of options.
+ */
+ int
+ ExtractConnectionOptions(List *defelems, const char **keywords, const char **values)
+ {
+ ListCell *lc;
+ int i;
+
+ i = 0;
+ foreach(lc, defelems)
+ {
+ DefElem *d = (DefElem *) lfirst(lc);
+ if (is_libpq_option(d->defname))
+ {
+ keywords[i] = d->defname;
+ values[i] = defGetString(d);
+ i++;
+ }
+ }
+ return i;
+ }
diff --git a/contrib/pgsql_fdw/pgsql_fdw--1.0.sql b/contrib/pgsql_fdw/pgsql_fdw--1.0.sql
index ...b0ea2b2 .
*** a/contrib/pgsql_fdw/pgsql_fdw--1.0.sql
--- b/contrib/pgsql_fdw/pgsql_fdw--1.0.sql
***************
*** 0 ****
--- 1,39 ----
+ /* contrib/pgsql_fdw/pgsql_fdw--1.0.sql */
+
+ -- complain if script is sourced in psql, rather than via CREATE EXTENSION
+ \echo Use "CREATE EXTENSION pgsql_fdw" to load this file. \quit
+
+ CREATE FUNCTION pgsql_fdw_handler()
+ RETURNS fdw_handler
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C STRICT;
+
+ CREATE FUNCTION pgsql_fdw_validator(text[], oid)
+ RETURNS void
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C STRICT;
+
+ CREATE FOREIGN DATA WRAPPER pgsql_fdw
+ HANDLER pgsql_fdw_handler
+ VALIDATOR pgsql_fdw_validator;
+
+ /* connection management functions and view */
+ CREATE FUNCTION pgsql_fdw_get_connections(out srvid oid, out usesysid oid)
+ RETURNS SETOF record
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C STRICT;
+
+ CREATE FUNCTION pgsql_fdw_disconnect(oid, oid)
+ RETURNS text
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C STRICT;
+
+ CREATE VIEW pgsql_fdw_connections AS
+ SELECT c.srvid srvid,
+ s.srvname srvname,
+ c.usesysid usesysid,
+ pg_get_userbyid(c.usesysid) usename
+ FROM pgsql_fdw_get_connections() c
+ JOIN pg_catalog.pg_foreign_server s ON (s.oid = c.srvid);
+ GRANT SELECT ON pgsql_fdw_connections TO public;
+
diff --git a/contrib/pgsql_fdw/pgsql_fdw.c b/contrib/pgsql_fdw/pgsql_fdw.c
index ...d0d6767 .
*** a/contrib/pgsql_fdw/pgsql_fdw.c
--- b/contrib/pgsql_fdw/pgsql_fdw.c
***************
*** 0 ****
--- 1,880 ----
+ /*-------------------------------------------------------------------------
+ *
+ * pgsql_fdw.c
+ * foreign-data wrapper for remote PostgreSQL servers.
+ *
+ * Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/pgsql_fdw.c
+ *
+ *-------------------------------------------------------------------------
+ */
+ #include "postgres.h"
+ #include "fmgr.h"
+
+ #include "catalog/pg_foreign_server.h"
+ #include "catalog/pg_foreign_table.h"
+ #include "commands/defrem.h"
+ #include "commands/explain.h"
+ #include "foreign/fdwapi.h"
+ #include "funcapi.h"
+ #include "miscadmin.h"
+ #include "nodes/nodeFuncs.h"
+ #include "optimizer/cost.h"
+ #include "optimizer/pathnode.h"
+ #include "utils/lsyscache.h"
+ #include "utils/memutils.h"
+ #include "utils/rel.h"
+
+ #include "pgsql_fdw.h"
+ #include "connection.h"
+
+ PG_MODULE_MAGIC;
+
+ /*
+ * Default fetch count for cursor. This can be overridden by fetch_count FDW
+ * option.
+ */
+ #define DEFAULT_FETCH_COUNT 10000
+
+ /*
+ * Cost to establish a connection.
+ * XXX: should be configurable per server?
+ */
+ #define CONNECTION_COSTS 100.0
+
+ /*
+ * Cost to transfer 1 byte from remote server.
+ * XXX: should be configurable per server?
+ */
+ #define TRANSFER_COSTS_PER_BYTE 0.001
+
+ /*
+ * Cursors which are used together in a local query require different name, so
+ * we use simple incremental name for that purpose. We don't care wrap around
+ * of cursor_id because it's hard to imagine that 2^32 cursors are used in a
+ * query.
+ */
+ #define CURSOR_NAME_FORMAT "pgsql_fdw_cursor_%u"
+ static uint32 cursor_id = 0;
+
+ /*
+ * Index of FDW-private information, such as remote SQL statements, which are
+ * stored in fdw_private list.
+ */
+ enum FdwPrivateIndex {
+ FdwPrivateSelectSql,
+ FdwPrivateDeclareSql,
+ FdwPrivateFetchSql,
+ FdwPrivateResetSql,
+ FdwPrivateCloseSql,
+
+ /* # of elements stored in the list fdw_private */
+ FdwPrivateNum,
+ };
+
+ /*
+ * Describes an execution state of a foreign scan against a foreign table
+ * using pgsql_fdw.
+ */
+ typedef struct PgsqlFdwExecutionState
+ {
+ MemoryContext scan_cxt; /* context for per-scan lifespan data */
+
+ List *fdw_private; /* FDW-private information passed to executor */
+ PGconn *conn; /* connection for the scan */
+
+ Oid *param_types; /* type array of external parameter */
+ const char **param_values; /* value array of external parameter */
+
+ int attnum; /* # of non-dropped attribute */
+ char **col_values; /* column value buffer */
+ AttInMetadata *attinmeta; /* attribute metadata */
+
+ Tuplestorestate *tuples; /* result of the scan */
+ } PgsqlFdwExecutionState;
+
+ /*
+ * SQL functions
+ */
+ extern Datum pgsql_fdw_handler(PG_FUNCTION_ARGS);
+ PG_FUNCTION_INFO_V1(pgsql_fdw_handler);
+
+ /*
+ * FDW callback routines
+ */
+ static void pgsqlPlanForeignScan(Oid foreigntableid,
+ PlannerInfo *root,
+ RelOptInfo *baserel);
+ static void pgsqlExplainForeignScan(ForeignScanState *node, ExplainState *es);
+ static void pgsqlBeginForeignScan(ForeignScanState *node, int eflags);
+ static TupleTableSlot *pgsqlIterateForeignScan(ForeignScanState *node);
+ static void pgsqlReScanForeignScan(ForeignScanState *node);
+ static void pgsqlEndForeignScan(ForeignScanState *node);
+
+ /*
+ * Helper functions
+ */
+ static void estimate_costs(PlannerInfo *root,
+ RelOptInfo *baserel,
+ const char *sql,
+ Oid serverid,
+ Cost *startup_cost,
+ Cost *total_cost);
+ static void execute_query(ForeignScanState *node);
+ static PGresult *fetch_result(ForeignScanState *node);
+ static void store_result(ForeignScanState *node, PGresult *res);
+
+ /* Exported functions, but not written in pgsql_fdw.h. */
+ void _PG_init(void);
+ void _PG_fini(void);
+
+ /*
+ * Module-specific initialization.
+ */
+ void
+ _PG_init(void)
+ {
+ }
+
+ /*
+ * Module-specific clean up.
+ */
+ void
+ _PG_fini(void)
+ {
+ }
+
+ /*
+ * The content of FdwRoutine of pgsql_fdw is never changed, so we use one
+ * static object for all scan.
+ */
+ static FdwRoutine fdwroutine = {
+
+ /* Node type */
+ T_FdwRoutine,
+
+ /* Required handler functions. */
+ pgsqlPlanForeignScan,
+ pgsqlExplainForeignScan,
+ pgsqlBeginForeignScan,
+ pgsqlIterateForeignScan,
+ pgsqlReScanForeignScan,
+ pgsqlEndForeignScan,
+
+ /* Optional handler functions. */
+ };
+
+ /*
+ * Foreign-data wrapper handler function: return a struct with pointers
+ * to my callback routines.
+ */
+ Datum
+ pgsql_fdw_handler(PG_FUNCTION_ARGS)
+ {
+ PG_RETURN_POINTER(&fdwroutine);
+ }
+
+ /*
+ * pgsqlPlanForeignScan
+ * Create possible scan paths for a scan on the foreign table
+ */
+ static void
+ pgsqlPlanForeignScan(Oid foreigntableid,
+ PlannerInfo *root,
+ RelOptInfo *baserel)
+ {
+ ListCell *lc;
+ Cost startup_cost;
+ Cost total_cost;
+ char name[128]; /* must be larger than format + 10 */
+ StringInfoData cursor;
+ DefElem *def;
+ int fetch_count = DEFAULT_FETCH_COUNT;
+ char *sql;
+ List *fdw_private = NIL;
+ ForeignTable *table;
+ ForeignServer *server;
+
+ /*
+ * Generate remote query string, and estimate cost of this scan.
+ */
+ table = GetForeignTable(foreigntableid);
+ server = GetForeignServer(table->serverid);
+ sql = deparseSql(foreigntableid, root, baserel, table);
+ estimate_costs(root, baserel, sql, server->serverid,
+ &startup_cost, &total_cost);
+
+ /*
+ * Store plain SELECT statement in private area of FdwPlan. This will be
+ * used for executing remote query and explaining scan.
+ */
+ fdw_private = list_make1(makeString(sql));
+
+ /*
+ * Use specified fetch_count instead of default value, if any. If both
+ * table and server have that option, use table's setting.
+ */
+ def = NULL;
+ foreach(lc, table->options)
+ {
+ def = (DefElem *) lfirst(lc);
+ if (strcmp(def->defname, "fetch_count") == 0)
+ break;
+
+ }
+ if (lc != NULL)
+ {
+ foreach(lc, server->options)
+ {
+ def = (DefElem *) lfirst(lc);
+ if (strcmp(def->defname, "fetch_count") == 0)
+ break;
+
+ }
+ }
+ if (lc != NULL)
+ fetch_count = strtol(defGetString(def), NULL, 10);
+ elog(DEBUG1, "relid=%u fetch_count=%d", foreigntableid, fetch_count);
+
+ /*
+ * We store some more information in FdwPlan to pass them beyond the
+ * boundary between planner and executor. Finally FdwPlan using cursor
+ * would hold items below:
+ *
+ * 1) plain SELECT statement (already added above)
+ * 2) SQL statement used to declare cursor
+ * 3) SQL statement used to fetch rows from cursor
+ * 4) SQL statement used to reset cursor
+ * 5) SQL statement used to close cursor
+ *
+ * These items are indexed with the enum FdwPrivateIndex, so an item
+ * can be accessed directly via list_nth(). For example of FETCH
+ * statement:
+ * list_nth(fdw_private, FdwPrivateFetchSql)
+ */
+
+ /* Construct cursor name from sequential value */
+ sprintf(name, CURSOR_NAME_FORMAT, cursor_id++);
+
+ /* Construct statement to declare cursor */
+ initStringInfo(&cursor);
+ appendStringInfo(&cursor, "DECLARE %s SCROLL CURSOR FOR %s", name, sql);
+ fdw_private = lappend(fdw_private, makeString(cursor.data));
+
+ /* Construct statement to fetch rows from cursor */
+ initStringInfo(&cursor);
+ appendStringInfo(&cursor, "FETCH %d FROM %s", fetch_count, name);
+ fdw_private = lappend(fdw_private, makeString(cursor.data));
+
+ /* Construct statement to reset cursor */
+ initStringInfo(&cursor);
+ appendStringInfo(&cursor, "MOVE ABSOLUTE 0 FROM %s", name);
+ fdw_private = lappend(fdw_private, makeString(cursor.data));
+
+ /* Construct statement to close cursor */
+ initStringInfo(&cursor);
+ appendStringInfo(&cursor, "CLOSE %s", name);
+ fdw_private = lappend(fdw_private, makeString(cursor.data));
+
+ /* Create ForeignScan path node for this scan, and add it to baserel. */
+ add_path(baserel, (Path *)
+ create_foreignscan_path(root, baserel,
+ baserel->rows,
+ startup_cost,
+ total_cost,
+ NIL, /* no pathkeys */
+ NULL, /* no outer rel eigher */
+ NIL, /* no param clause */
+ fdw_private)); /* store remote SQL */
+ }
+
+ /*
+ * pgsqlExplainForeignScan
+ * Produce extra output for EXPLAIN
+ */
+ static void
+ pgsqlExplainForeignScan(ForeignScanState *node, ExplainState *es)
+ {
+ List *fdw_private;
+ char *sql;
+
+ fdw_private = ((ForeignScan *) node->ss.ps.plan)->fdw_private;
+ sql = strVal(list_nth(fdw_private, FdwPrivateDeclareSql));
+ ExplainPropertyText("Remote SQL", sql, es);
+ }
+
+ /*
+ * pgsqlBeginForeignScan
+ * Initiate access to a foreign PostgreSQL table.
+ */
+ static void
+ pgsqlBeginForeignScan(ForeignScanState *node, int eflags)
+ {
+ PgsqlFdwExecutionState *festate;
+ PGconn *conn;
+ Oid relid;
+ ForeignTable *table;
+ ForeignServer *server;
+ UserMapping *user;
+ TupleTableSlot *slot = node->ss.ss_ScanTupleSlot;
+
+ /*
+ * Do nothing in EXPLAIN (no ANALYZE) case. node->fdw_state stays NULL.
+ */
+ if (eflags & EXEC_FLAG_EXPLAIN_ONLY)
+ return;
+
+ /*
+ * Save state in node->fdw_state.
+ */
+ festate = (PgsqlFdwExecutionState *) palloc(sizeof(PgsqlFdwExecutionState));
+ festate->fdw_private = ((ForeignScan *) node->ss.ps.plan)->fdw_private;
+
+ /*
+ * Create context for per-scan tuplestore under per-query context.
+ */
+ festate->scan_cxt = AllocSetContextCreate(node->ss.ps.state->es_query_cxt,
+ "pgsql_fdw",
+ ALLOCSET_DEFAULT_MINSIZE,
+ ALLOCSET_DEFAULT_INITSIZE,
+ ALLOCSET_DEFAULT_MAXSIZE);
+
+ /*
+ * Get connection to the foreign server. Connection manager would
+ * establish new connection if necessary.
+ */
+ relid = RelationGetRelid(node->ss.ss_currentRelation);
+ table = GetForeignTable(relid);
+ server = GetForeignServer(table->serverid);
+ user = GetUserMapping(GetOuterUserId(), server->serverid);
+ conn = GetConnection(server, user, true);
+ festate->conn = conn;
+
+ /* Result will be filled in first Iterate call. */
+ festate->tuples = NULL;
+
+ /* Allocate buffers for column values. */
+ {
+ TupleDesc tupdesc = slot->tts_tupleDescriptor;
+ festate->col_values = palloc(sizeof(char *) * tupdesc->natts);
+ festate->attinmeta = TupleDescGetAttInMetadata(tupdesc);
+ }
+
+ /* Allocate buffers for query parameters. */
+ {
+ ParamListInfo params = node->ss.ps.state->es_param_list_info;
+ int numParams = params ? params->numParams : 0;
+
+ if (numParams > 0)
+ {
+ festate->param_types = palloc0(sizeof(Oid) * numParams);
+ festate->param_values = palloc0(sizeof(char *) * numParams);
+ }
+ else
+ {
+ festate->param_types = NULL;
+ festate->param_values = NULL;
+ }
+ }
+
+ /* Store FDW-specific state into ForeignScanState */
+ node->fdw_state = (void *) festate;
+
+ return;
+ }
+
+ /*
+ * pgsqlIterateForeignScan
+ * Retrieve next row from the result set, or clear tuple slot to indicate
+ * EOF.
+ *
+ * Note that using per-query context when retrieving tuples from
+ * tuplestore to ensure that returned tuples can survive until next
+ * iteration because the tuple is released implicitly via ExecClearTuple.
+ * Retrieving a tuple from tuplestore in CurrentMemoryContext (it's a
+ * per-tuple context), ExecClearTuple will free dangling pointer.
+ */
+ static TupleTableSlot *
+ pgsqlIterateForeignScan(ForeignScanState *node)
+ {
+ PgsqlFdwExecutionState *festate;
+ TupleTableSlot *slot = node->ss.ss_ScanTupleSlot;
+ PGresult *res = NULL;
+ MemoryContext oldcontext = CurrentMemoryContext;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+
+ /*
+ * If this is the first call after Begin, we need to execute remote query.
+ * If the query needs cursor, we declare a cursor at first call and fetch
+ * from it in later calls.
+ */
+ if (festate->tuples == NULL)
+ execute_query(node);
+
+ /*
+ * If enough tuples are left in tuplestore, just return next tuple from it.
+ *
+ * It is necessary to switch to per-scan context to make returned tuple
+ * valid until next IterateForeignScan call, because it will be released
+ * with ExecClearTuple then. Otherwise, picked tuple is allocated in
+ * per-tuple context, and double-free of that tuple might happen.
+ */
+ MemoryContextSwitchTo(festate->scan_cxt);
+ if (tuplestore_gettupleslot(festate->tuples, true, false, slot))
+ {
+ MemoryContextSwitchTo(oldcontext);
+ return slot;
+ }
+ MemoryContextSwitchTo(oldcontext);
+
+ /*
+ * Here we need to clear partial result and fetch next bunch of tuples from
+ * from the cursor for the scan. If the fetch returns no tuple, the scan
+ * has reached the end.
+ *
+ * PGresult must be released before leaving this function.
+ */
+ PG_TRY();
+ {
+ res = fetch_result(node);
+ store_result(node, res);
+ PQclear(res);
+ res = NULL;
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+
+ /*
+ * If we got more tuples from the server cursor, return next tuple from
+ * tuplestore.
+ */
+ MemoryContextSwitchTo(festate->scan_cxt);
+ if (tuplestore_gettupleslot(festate->tuples, true, false, slot))
+ {
+ MemoryContextSwitchTo(oldcontext);
+ return slot;
+ }
+ MemoryContextSwitchTo(oldcontext);
+
+ /* We don't have any result even in remote server cursor. */
+ ExecClearTuple(slot);
+ return slot;
+ }
+
+ /*
+ * pgsqlReScanForeignScan
+ * - Restart this scan by resetting fetch location.
+ */
+ static void
+ pgsqlReScanForeignScan(ForeignScanState *node)
+ {
+ List *fdw_private;
+ char *sql;
+ PGconn *conn;
+ PGresult *res = NULL;
+ PgsqlFdwExecutionState *festate;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+
+ /* If we have not opened cursor yet, nothing to do. */
+ if (festate->tuples == NULL)
+ return;
+
+ /* Discard fetch results if any. */
+ tuplestore_clear(festate->tuples);
+
+ /* PGresult must be released before leaving this function. */
+ PG_TRY();
+ {
+ /* Reset cursor */
+ fdw_private = festate->fdw_private;
+ conn = festate->conn;
+ sql = strVal(list_nth(fdw_private, FdwPrivateResetSql));
+ res = PQexec(conn, sql);
+ if (PQresultStatus(res) != PGRES_COMMAND_OK)
+ {
+ ereport(ERROR,
+ (errmsg("could not rewind cursor"),
+ errdetail("%s", PQerrorMessage(conn)),
+ errhint("%s", sql)));
+ }
+ PQclear(res);
+ res = NULL;
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+ }
+
+ /*
+ * pgsqlEndForeignScan
+ * Finish scanning foreign table and dispose objects used for this scan
+ */
+ static void
+ pgsqlEndForeignScan(ForeignScanState *node)
+ {
+ List *fdw_private;
+ char *sql;
+ PGconn *conn;
+ PGresult *res = NULL;
+ PgsqlFdwExecutionState *festate;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+
+ /* if festate is NULL, we are in EXPLAIN; nothing to do */
+ if (festate == NULL)
+ return;
+
+ /* If we have not opened cursor yet, nothing to do. */
+ if (festate->tuples == NULL)
+ return;
+
+ /* Discard fetch results */
+ tuplestore_end(festate->tuples);
+ festate->tuples = NULL;
+
+ /* PGresult must be released before leaving this function. */
+ PG_TRY();
+ {
+ /* Close cursor */
+ fdw_private = festate->fdw_private;
+ conn = festate->conn;
+ sql = strVal(list_nth(fdw_private, FdwPrivateCloseSql));
+ res = PQexec(conn, sql);
+ if (PQresultStatus(res) != PGRES_COMMAND_OK)
+ {
+ ereport(ERROR,
+ (errmsg("could not close cursor"),
+ errdetail("%s", PQerrorMessage(conn)),
+ errhint("%s", sql)));
+ }
+ PQclear(res);
+ res = NULL;
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+
+ ReleaseConnection(festate->conn);
+
+ MemoryContextDelete(festate->scan_cxt);
+ festate->scan_cxt = NULL;
+ }
+
+ /*
+ * Estimate costs of scanning a foreign table.
+ */
+ static void
+ estimate_costs(PlannerInfo *root, RelOptInfo *baserel,
+ const char *sql, Oid serverid,
+ Cost *startup_cost, Cost *total_cost)
+ {
+ ForeignServer *server;
+ UserMapping *user;
+ PGconn *conn = NULL;
+ PGresult *res = NULL;
+ StringInfoData buf;
+ char *plan;
+ char *p;
+ int n;
+
+ /*
+ * Get connection to the foreign server. Connection manager would
+ * establish new connection if necessary.
+ */
+ server = GetForeignServer(serverid);
+ user = GetUserMapping(GetOuterUserId(), server->serverid);
+ conn = GetConnection(server, user, false);
+ initStringInfo(&buf);
+ appendStringInfo(&buf, "EXPLAIN %s", sql);
+
+ /* PGresult must be released before leaving this function. */
+ PG_TRY();
+ {
+ res = PQexec(conn, buf.data);
+ if (PQresultStatus(res) != PGRES_TUPLES_OK || PQntuples(res) == 0)
+ {
+ char *msg;
+
+ msg = pstrdup(PQerrorMessage(conn));
+ ereport(ERROR,
+ (errmsg("could not execute EXPLAIN for cost estimation"),
+ errdetail("%s", msg),
+ errhint("%s", sql)));
+ }
+
+ /*
+ * Find estimation portion from top plan node. Here we search opening
+ * parentheses from the end of the line to avoid finding unexpected
+ * parentheses.
+ */
+ plan = PQgetvalue(res, 0, 0);
+ p = strrchr(plan, '(');
+ if (p == NULL)
+ elog(ERROR, "wrong EXPLAIN output: %s", plan);
+ n = sscanf(p,
+ "(cost=%lf..%lf rows=%lf width=%d)",
+ startup_cost, total_cost, &baserel->rows, &baserel->width);
+ if (n != 4)
+ elog(ERROR, "could not get estimation from EXPLAIN output");
+
+ PQclear(res);
+ res = NULL;
+ ReleaseConnection(conn);
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+
+ /*
+ * TODO Selectivity of quals which are NOT pushed down should be also
+ * considered.
+ */
+
+ /* add cost to establish connection. */
+ *startup_cost += CONNECTION_COSTS;
+ *total_cost += CONNECTION_COSTS;
+
+ /* add cost to transfer result. */
+ *total_cost += TRANSFER_COSTS_PER_BYTE * baserel->width * baserel->tuples;
+ *total_cost += cpu_tuple_cost * baserel->tuples;
+ }
+
+ /*
+ * Execute remote query with current parameters.
+ */
+ static void
+ execute_query(ForeignScanState *node)
+ {
+ List *fdw_private;
+ PgsqlFdwExecutionState *festate;
+ ParamListInfo params = node->ss.ps.state->es_param_list_info;
+ int numParams = params ? params->numParams : 0;
+ Oid *types = NULL;
+ const char **values = NULL;
+ char *sql;
+ PGconn *conn;
+ PGresult *res = NULL;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+ types = festate->param_types;
+ values = festate->param_values;
+
+ /*
+ * Construct parameter array in text format. We don't release memory for
+ * the arrays explicitly, because the memory usage would not be very large,
+ * and anyway they will be released in context cleanup.
+ */
+ if (numParams > 0)
+ {
+ int i;
+
+ 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;
+
+ getTypeOutputInfo(types[i], &out_func_oid, &isvarlena);
+ fmgr_info(out_func_oid, &func);
+ values[i] = OutputFunctionCall(&func, params->params[i].value);
+ }
+ }
+ }
+
+ /* PGresult must be released before leaving this function. */
+ PG_TRY();
+ {
+ /*
+ * Execute remote query with parameters.
+ */
+ conn = festate->conn;
+ fdw_private = ((ForeignScan *) node->ss.ps.plan)->fdw_private;
+ sql = strVal(list_nth(fdw_private, FdwPrivateDeclareSql));
+ res = PQexecParams(conn, sql, numParams, types, values, NULL, NULL, 0);
+
+ /*
+ * If the query has failed, reporting details is enough here.
+ * Connection(s) which are used by this query (at least used by
+ * pgsql_fdw) will be cleaned up by the foreign connection manager.
+ */
+ if (PQresultStatus(res) != PGRES_COMMAND_OK)
+ {
+ ereport(ERROR,
+ (errmsg("could not declare cursor"),
+ errdetail("%s", PQerrorMessage(conn)),
+ errhint("%s", sql)));
+ }
+
+ /* Discard result of DECLARE statement. */
+ PQclear(res);
+ res = NULL;
+
+ /* Fetch first bunch of the result and store them into tuplestore. */
+ res = fetch_result(node);
+ store_result(node, res);
+ PQclear(res);
+ res = NULL;
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+ }
+
+ /*
+ * Fetch next partial result from remote server.
+ *
+ * Once this function has returned result records as PGresult, caller is
+ * responsible to release it, so caller should put codes which might throw
+ * exception in PG_TRY block. When an exception has been caught, release
+ * PGresult and re-throw the exception in PG_CATCH block.
+ */
+ static PGresult *
+ fetch_result(ForeignScanState *node)
+ {
+ PgsqlFdwExecutionState *festate;
+ List *fdw_private;
+ char *sql;
+ PGconn *conn;
+ PGresult *res;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+
+ /* Retrieve information for fetching result. */
+ fdw_private = festate->fdw_private;
+ sql = strVal(list_nth(fdw_private, FdwPrivateFetchSql));
+ conn = festate->conn;
+
+ /*
+ * Fetch result from remote server. In error case, we must release
+ * PGresult in this function to avoid memory leak because caller can't
+ * get the reference.
+ */
+ res = PQexec(conn, sql);
+ if (PQresultStatus(res) != PGRES_TUPLES_OK)
+ {
+ PQclear(res);
+ ereport(ERROR,
+ (errmsg("could not fetch rows from foreign server"),
+ errdetail("%s", PQerrorMessage(conn)),
+ errhint("%s", sql)));
+ }
+
+ return res;
+ }
+
+ /*
+ * Create tuples from PGresult and store them into tuplestore.
+ *
+ * Caller must use PG_TRY block to catch exception and release PGresult
+ * surely.
+ */
+ static void
+ store_result(ForeignScanState *node, PGresult *res)
+ {
+ int rows;
+ int row;
+ int i;
+ int nfields;
+ int attnum; /* number of non-dropped columns */
+ Form_pg_attribute *attrs;
+ TupleTableSlot *slot = node->ss.ss_ScanTupleSlot;
+ TupleDesc tupdesc = slot->tts_tupleDescriptor;
+ PgsqlFdwExecutionState *festate;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+ rows = PQntuples(res);
+ nfields = PQnfields(res);
+ attrs = tupdesc->attrs;
+
+ /* First, ensure that the tuplestore is empty. */
+ if (festate->tuples == NULL)
+ {
+ MemoryContext oldcontext = CurrentMemoryContext;
+
+ /*
+ * Create tuplestore to store result of the query in per-query context.
+ * Note that we use this memory context to avoid memory leak in error
+ * cases.
+ */
+ MemoryContextSwitchTo(festate->scan_cxt);
+ festate->tuples = tuplestore_begin_heap(false, false, work_mem);
+ MemoryContextSwitchTo(oldcontext);
+ }
+ else
+ {
+ /* We already have tuplestore, just need to clear contents of it. */
+ tuplestore_clear(festate->tuples);
+ }
+
+ /* 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)));
+
+ /* put a tuples into the slot */
+ for (row = 0; row < rows; row++)
+ {
+ int j;
+ HeapTuple tuple;
+
+ for (i = 0, j = 0; i < tupdesc->natts; i++)
+ {
+ /* skip dropped columns. */
+ if (attrs[i]->attisdropped)
+ {
+ festate->col_values[i] = NULL;
+ continue;
+ }
+
+ if (PQgetisnull(res, row, j))
+ festate->col_values[i] = NULL;
+ else
+ festate->col_values[i] = PQgetvalue(res, row, 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(festate->attinmeta, festate->col_values);
+ tuplestore_puttuple(festate->tuples, tuple);
+ }
+
+ tuplestore_donestoring(festate->tuples);
+ }
+
diff --git a/contrib/pgsql_fdw/pgsql_fdw.control b/contrib/pgsql_fdw/pgsql_fdw.control
index ...0a9c8f4 .
*** a/contrib/pgsql_fdw/pgsql_fdw.control
--- b/contrib/pgsql_fdw/pgsql_fdw.control
***************
*** 0 ****
--- 1,5 ----
+ # pgsql_fdw extension
+ comment = 'foreign-data wrapper for remote PostgreSQL servers'
+ default_version = '1.0'
+ module_pathname = '$libdir/pgsql_fdw'
+ relocatable = true
diff --git a/contrib/pgsql_fdw/pgsql_fdw.h b/contrib/pgsql_fdw/pgsql_fdw.h
index ...78a6abf .
*** a/contrib/pgsql_fdw/pgsql_fdw.h
--- b/contrib/pgsql_fdw/pgsql_fdw.h
***************
*** 0 ****
--- 1,32 ----
+ /*-------------------------------------------------------------------------
+ *
+ * pgsql_fdw.h
+ * foreign-data wrapper for remote PostgreSQL servers.
+ *
+ * Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/pgsql_fdw.h
+ *
+ *-------------------------------------------------------------------------
+ */
+
+ #ifndef PGSQL_FDW_H
+ #define PGSQL_FDW_H
+
+ #include "postgres.h"
+ #include "foreign/foreign.h"
+ #include "nodes/relation.h"
+
+ /* in option.c */
+ int ExtractConnectionOptions(List *defelems,
+ const char **keywords,
+ const char **values);
+
+ /* in deparse.c */
+ char *deparseSql(Oid relid,
+ PlannerInfo *root,
+ RelOptInfo *baserel,
+ ForeignTable *table);
+
+ #endif /* PGSQL_FDW_H */
diff --git a/contrib/pgsql_fdw/sql/pgsql_fdw.sql b/contrib/pgsql_fdw/sql/pgsql_fdw.sql
index ...b3fac96 .
*** a/contrib/pgsql_fdw/sql/pgsql_fdw.sql
--- b/contrib/pgsql_fdw/sql/pgsql_fdw.sql
***************
*** 0 ****
--- 1,248 ----
+ -- ===================================================================
+ -- create FDW objects
+ -- ===================================================================
+
+ -- Clean up in case a prior regression run failed
+
+ -- Suppress NOTICE messages when roles don't exist
+ SET client_min_messages TO 'error';
+
+ DROP ROLE IF EXISTS pgsql_fdw_user;
+
+ RESET client_min_messages;
+
+ CREATE ROLE pgsql_fdw_user LOGIN SUPERUSER;
+ SET SESSION AUTHORIZATION 'pgsql_fdw_user';
+
+ CREATE EXTENSION pgsql_fdw;
+
+ CREATE SERVER loopback1 FOREIGN DATA WRAPPER pgsql_fdw;
+ CREATE SERVER loopback2 FOREIGN DATA WRAPPER pgsql_fdw
+ OPTIONS (dbname 'contrib_regression');
+
+ CREATE USER MAPPING FOR public SERVER loopback1
+ OPTIONS (user 'value', password 'value');
+ CREATE USER MAPPING FOR pgsql_fdw_user SERVER loopback2;
+
+ CREATE FOREIGN TABLE ft1 (
+ c0 int,
+ c1 int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10)
+ ) SERVER loopback2;
+ ALTER FOREIGN TABLE ft1 DROP COLUMN c0;
+
+ CREATE FOREIGN TABLE ft2 (
+ c0 int,
+ c1 int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10)
+ ) SERVER loopback2;
+ ALTER FOREIGN TABLE ft2 DROP COLUMN c0;
+
+ -- ===================================================================
+ -- create objects used through FDW
+ -- ===================================================================
+ CREATE SCHEMA "S 1";
+ CREATE TABLE "S 1"."T 1" (
+ "C 1" int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10),
+ CONSTRAINT t1_pkey PRIMARY KEY ("C 1")
+ );
+ CREATE TABLE "S 1"."T 2" (
+ c1 int NOT NULL,
+ c2 text,
+ CONSTRAINT t2_pkey PRIMARY KEY (c1)
+ );
+
+ BEGIN;
+ TRUNCATE "S 1"."T 1";
+ INSERT INTO "S 1"."T 1"
+ SELECT id,
+ id % 10,
+ to_char(id, 'FM00000'),
+ '1970-01-01'::timestamptz + ((id % 100) || ' days')::interval,
+ '1970-01-01'::timestamp + ((id % 100) || ' days')::interval,
+ id % 10,
+ id % 10
+ FROM generate_series(1, 1000) id;
+ TRUNCATE "S 1"."T 2";
+ INSERT INTO "S 1"."T 2"
+ SELECT id,
+ 'AAA' || to_char(id, 'FM000')
+ FROM generate_series(1, 100) id;
+ COMMIT;
+
+ -- ===================================================================
+ -- tests for pgsql_fdw_validator
+ -- ===================================================================
+ ALTER FOREIGN DATA WRAPPER pgsql_fdw OPTIONS (host 'value'); -- ERROR
+ -- requiressl, krbsrvname and gsslib are omitted because they depend on
+ -- configure option
+ ALTER SERVER loopback1 OPTIONS (
+ authtype 'value',
+ service 'value',
+ connect_timeout 'value',
+ dbname 'value',
+ host 'value',
+ hostaddr 'value',
+ port 'value',
+ --client_encoding 'value',
+ tty 'value',
+ options 'value',
+ application_name 'value',
+ --fallback_application_name 'value',
+ keepalives 'value',
+ keepalives_idle 'value',
+ keepalives_interval 'value',
+ -- requiressl 'value',
+ sslcompression 'value',
+ sslmode 'value',
+ sslcert 'value',
+ sslkey 'value',
+ sslrootcert 'value',
+ sslcrl 'value'
+ --requirepeer 'value',
+ -- krbsrvname 'value',
+ -- gsslib 'value',
+ --replication 'value'
+ );
+ ALTER SERVER loopback1 OPTIONS (user 'value'); -- ERROR
+ ALTER SERVER loopback2 OPTIONS (ADD fetch_count '2');
+ ALTER USER MAPPING FOR public SERVER loopback1
+ OPTIONS (DROP user, DROP password);
+ ALTER USER MAPPING FOR public SERVER loopback1
+ OPTIONS (host 'value'); -- ERROR
+ ALTER FOREIGN TABLE ft1 OPTIONS (nspname 'S 1', relname 'T 1');
+ ALTER FOREIGN TABLE ft2 OPTIONS (nspname 'S 1', relname 'T 1', fetch_count '100');
+ ALTER FOREIGN TABLE ft1 OPTIONS (invalid 'value'); -- ERROR
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count 'a'); -- ERROR
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count '0'); -- ERROR
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count '-1'); -- ERROR
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (invalid 'value'); -- ERROR
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (colname 'C 1');
+ ALTER FOREIGN TABLE ft2 ALTER COLUMN c1 OPTIONS (colname 'C 1');
+ \dew+
+ \des+
+ \deu+
+ \det+
+
+ -- ===================================================================
+ -- simple queries
+ -- ===================================================================
+ -- single table, with/without alias
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
+ SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+ SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+ -- with WHERE clause
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
+ SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
+ -- 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
+ SELECT * FROM ft1 t1 WHERE t1.c3 = (SELECT MAX(c3) FROM ft2 t2) ORDER BY c1;
+ -- used in CTE
+ WITH t1 AS (SELECT * FROM ft1 WHERE c1 <= 10) SELECT t2.c1, t2.c2, t2.c3, t2.c4 FROM t1, ft2 t2 WHERE t1.c1 = t2.c1 ORDER BY t1.c1;
+ -- fixed values
+ SELECT 'fixed', NULL FROM ft1 t1 WHERE c1 = 1;
+ -- user-defined operator/function
+ CREATE FUNCTION pgsql_fdw_abs(int) RETURNS int AS $$
+ BEGIN
+ RETURN abs($1);
+ END
+ $$ LANGUAGE plpgsql IMMUTABLE;
+ CREATE OPERATOR === (
+ LEFTARG = int,
+ RIGHTARG = int,
+ PROCEDURE = int4eq,
+ COMMUTATOR = ===,
+ NEGATOR = !==
+ );
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = pgsql_fdw_abs(t1.c2);
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 === t1.c2;
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = abs(t1.c2);
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = t1.c2;
+
+ -- ===================================================================
+ -- parameterized queries
+ -- ===================================================================
+ -- simple join
+ PREPARE st1(int, int) AS SELECT t1.c3, t2.c3 FROM ft1 t1, ft2 t2 WHERE t1.c1 = $1 AND t2.c1 = $2;
+ EXPLAIN (COSTS false) EXECUTE st1(1, 2);
+ EXECUTE st1(1, 1);
+ EXECUTE st1(101, 101);
+ -- subquery using stable function (can't be pushed down)
+ PREPARE st2(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c4) = 6) ORDER BY c1;
+ EXPLAIN (COSTS false) EXECUTE st2(10, 20);
+ EXECUTE st2(10, 20);
+ EXECUTE st1(101, 101);
+ -- subquery using immutable function (can be pushed down)
+ PREPARE st3(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c5) = 6) ORDER BY c1;
+ EXPLAIN (COSTS false) EXECUTE st3(10, 20);
+ EXECUTE st3(10, 20);
+ EXECUTE st3(20, 30);
+ -- custom plan should be chosen
+ PREPARE st4(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 = $1;
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ -- cleanup
+ DEALLOCATE st1;
+ DEALLOCATE st2;
+ DEALLOCATE st3;
+ DEALLOCATE st4;
+
+ -- ===================================================================
+ -- connection management
+ -- ===================================================================
+ SELECT srvname, usename FROM pgsql_fdw_connections;
+ SELECT pgsql_fdw_disconnect(srvid, usesysid) FROM pgsql_fdw_get_connections();
+ SELECT srvname, usename FROM pgsql_fdw_connections;
+
+ -- ===================================================================
+ -- subtransaction
+ -- ===================================================================
+ BEGIN;
+ DECLARE c CURSOR FOR SELECT * FROM ft1 ORDER BY c1;
+ FETCH c;
+ SAVEPOINT s;
+ ERROR OUT; -- ERROR
+ ROLLBACK TO s;
+ SELECT srvname FROM pgsql_fdw_connections;
+ FETCH c;
+ COMMIT;
+ SELECT srvname FROM pgsql_fdw_connections;
+ ERROR OUT; -- ERROR
+ SELECT srvname FROM pgsql_fdw_connections;
+
+ -- ===================================================================
+ -- cleanup
+ -- ===================================================================
+ DROP OPERATOR === (int, int) CASCADE;
+ DROP OPERATOR !== (int, int) CASCADE;
+ DROP FUNCTION pgsql_fdw_abs(int);
+ DROP SCHEMA "S 1" CASCADE;
+ DROP EXTENSION pgsql_fdw CASCADE;
+ \c
+ DROP ROLE pgsql_fdw_user;
diff --git a/doc/src/sgml/contrib.sgml b/doc/src/sgml/contrib.sgml
index d4da4ee..32056d1 100644
*** a/doc/src/sgml/contrib.sgml
--- b/doc/src/sgml/contrib.sgml
*************** CREATE EXTENSION <replaceable>module_nam
*** 117,122 ****
--- 117,123 ----
&pgcrypto;
&pgfreespacemap;
&pgrowlocks;
+ &pgsql-fdw;
&pgstandby;
&pgstatstatements;
&pgstattuple;
diff --git a/doc/src/sgml/filelist.sgml b/doc/src/sgml/filelist.sgml
index b5d3c6d..de686ee 100644
*** a/doc/src/sgml/filelist.sgml
--- b/doc/src/sgml/filelist.sgml
***************
*** 125,130 ****
--- 125,131 ----
<!ENTITY pgcrypto SYSTEM "pgcrypto.sgml">
<!ENTITY pgfreespacemap SYSTEM "pgfreespacemap.sgml">
<!ENTITY pgrowlocks SYSTEM "pgrowlocks.sgml">
+ <!ENTITY pgsql-fdw SYSTEM "pgsql-fdw.sgml">
<!ENTITY pgstandby SYSTEM "pgstandby.sgml">
<!ENTITY pgstatstatements SYSTEM "pgstatstatements.sgml">
<!ENTITY pgstattuple SYSTEM "pgstattuple.sgml">
diff --git a/doc/src/sgml/pgsql-fdw.sgml b/doc/src/sgml/pgsql-fdw.sgml
index ...30fa7f5 .
*** a/doc/src/sgml/pgsql-fdw.sgml
--- b/doc/src/sgml/pgsql-fdw.sgml
***************
*** 0 ****
--- 1,261 ----
+ <!-- doc/src/sgml/pgsql-fdw.sgml -->
+
+ <sect1 id="pgsql-fdw" xreflabel="pgsql_fdw">
+ <title>pgsql_fdw</title>
+
+ <indexterm zone="pgsql-fdw">
+ <primary>pgsql_fdw</primary>
+ </indexterm>
+
+ <para>
+ The <filename>pgsql_fdw</filename> module provides a foreign-data wrapper for
+ external <productname>PostgreSQL</productname> servers.
+ With this module, users can access data stored in external
+ <productname>PostgreSQL</productname> via plain SQL statements.
+ </para>
+
+ <para>
+ Note that default wrapper <literal>pgsql_fdw</literal> is created
+ automatically during <command>CREATE EXTENSION</command> command for
+ <application>pgsql_fdw</application>.
+ </para>
+
+ <sect2>
+ <title>FDW Options of pgsql_fdw</title>
+
+ <sect3>
+ <title>Connection Options</title>
+ <para>
+ A foreign server and user mapping created using this wrapper can have
+ <application>libpq</> connection options, expect below:
+
+ <itemizedlist>
+ <listitem><para>client_encoding</para></listitem>
+ <listitem><para>fallback_application_name</para></listitem>
+ <listitem><para>replication</para></listitem>
+ </itemizedlist>
+
+ For details of <application>libpq</> connection options, see
+ <xref linkend="libpq-connect">.
+ </para>
+
+ <para>
+ <literal>user</literal> and <literal>password</literal> can be
+ specified on user mappings, and others can be specified on foreign servers.
+ </para>
+ </sect3>
+
+ <sect3>
+ <title>Object Name Options</title>
+ <para>
+ Foreign tables which were created using this wrapper, or its columns can
+ have object name options. These options can be used to specify the names
+ used in SQL statement sent to remote <productname>PostgreSQL</productname>
+ server. These options are useful when a remote object has different name
+ from corresponding local one.
+ </para>
+
+ <variablelist>
+
+ <varlistentry>
+ <term><literal>nspname</literal></term>
+ <listitem>
+ <para>
+ This option, which can be specified on a foreign table, is used as a
+ namespace (schema) reference in the SQL statement. If this options is
+ omitted, <literal>pg_class.nspname</literal> of the foreign table is
+ used.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>relname</literal></term>
+ <listitem>
+ <para>
+ This option, which can be specified on a foreign table, is used as a
+ relation (table) reference in the SQL statement. If this options is
+ omitted, <literal>pg_class.relname</literal> of the foreign table is
+ used.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>colname</literal></term>
+ <listitem>
+ <para>
+ This option, which can be specified on a column of a foreign table, is
+ used as a column (attribute) reference in the SQL statement. If this
+ option is omitted, <literal>pg_attribute.attname</literal> of the column
+ of the foreign table is used.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ </variablelist>
+
+ </sect3>
+
+ <sect3>
+ <title>Cursor Options</title>
+ <para>
+ The <application>pgsql_fdw</application> always uses cursor to retrieve the
+ result from external server. Users can control the behavior of cursor by
+ setting cursor options to foreign table or foreign server. If an option
+ is set to both objects, finer-grained setting is used. In other words,
+ foreign table's setting overrides foreign server's setting.
+ </para>
+
+ <variablelist>
+
+ <varlistentry>
+ <term><literal>fetch_count</literal></term>
+ <listitem>
+ <para>
+ This option specifies the number of rows to be fetched at a time.
+ This option accepts only integer value larger than zero. The default
+ setting is 10000.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ </variablelist>
+
+ </sect3>
+
+ </sect2>
+
+ <sect2>
+ <title>Connection Management</title>
+
+ <para>
+ The <application>pgsql_fdw</application> establishes a connection to a
+ foreign server in the beginning of the first query which uses a foreign
+ table associated to the foreign server, and reuses the connection following
+ queries and even in following foreign scans in same query.
+
+ You can see the list of active connections via
+ <structname>pgsql_fdw_connections</structname> view. It shows pair of oid
+ and name of server and local role for each active connections established by
+ <application>pgsql_fdw</application>. For security reason, only superuser
+ can see other role's connections.
+ </para>
+
+ <para>
+ Established connections are kept alive until local role changes or the
+ current transaction aborts or user requests so.
+ </para>
+
+ <para>
+ If role has been changed, active connections established as old local role
+ is kept alive but never be reused until local role has restored to original
+ role. This kind of situation happens with <command>SET ROLE</command> and
+ <command>SET SESSION AUTHORIZATION</command>.
+ </para>
+
+ <para>
+ If current transaction aborts by error or user request, all active
+ connections are disconnected automatically. This behavior avoids possible
+ connection leaks on error.
+ </para>
+
+ <para>
+ You can discard persistent connection at arbitrary timing with
+ <function>pgsql_fdw_disconnect()</function>. It takes server oid and
+ user oid as arguments. This function can handle only connections
+ established in current session; connections established by other backends
+ are not reachable.
+ </para>
+
+ <para>
+ You can discard all active and visible connections in current session with
+ using <structname>pgsql_fdw_connections</structname> and
+ <function>pgsql_fdw_disconnect()</function> together:
+ <synopsis>
+ postgres=# SELECT pgsql_fdw_disconnect(srvid, usesysid) FROM pgsql_fdw_connections;
+ pgsql_fdw_disconnect
+ ----------------------
+ OK
+ OK
+ (2 rows)
+ </synopsis>
+ </para>
+ </sect2>
+
+ <sect2>
+ <title>Transaction Management</title>
+ <para>
+ The <application>pgsql_fdw</application> begins remote transaction at the
+ beginning of a local query, and terminates it with <command>ABORT</command>
+ at the end of the local query. This means that all foreign scans on a
+ foreign server in a local query are executed in one transaction.
+ If isolation level of local transaction is <literal>SERIALIZABLE</literal>,
+ <literal>SERIALIZABLE</literal> is used for remote transaction. Otherwise,
+ if isolation level of local transaction is one of
+ <literal>READ UNCOMMITTED</literal>, <literal>READ COMMITTED</literal> or
+ <literal>REPEATABLE READ</literal>, then <literal>REPEATABLE READ</literal>
+ is used for remote transaction.
+ <literal>READ UNCOMMITTED</literal> and <literal>READ COMMITTED</literal>
+ are never used for remote transaction, because even
+ <literal>READ COMMITTED</literal> transaction might produce inconsistent
+ results, if remote data have been updated between two remote queries.
+ </para>
+ <para>
+ Note that even if the isolation level of local transaction was
+ <literal>SERIALIZABLE</literal> or <literal>REPEATABLE READ</literal>,
+ series of one query might produce different result, because foreign scans
+ in different local queries are executed in different remote transactions.
+ For instance, when client started a local transaction
+ explicitly with isolation level <literal>SERIALIZABLE</literal>, and
+ executed same local query which contains a foreign table which references
+ foreign data which is updated frequently, latter result would be different
+ from former result.
+ </para>
+ <para>
+ This restriction might be relaxed in future release.
+ </para>
+ </sect2>
+
+ <sect2>
+ <title>Estimation of Costs and Rows</title>
+ <para>
+ The <application>pgsql_fdw</application> estimates the costs of a foreign
+ scan by adding up some basic costs: connection costs, remote query costs
+ and data transfer costs.
+ To get remote query costs, <application>pgsql_fdw</application> executes
+ <command>EXPLAIN</command> command on remote server for each foreign scan.
+ </para>
+ <para>
+ On the other hand, estimated rows which was returned by
+ <command>EXPLAIN</command> is used for local estimation as-is.
+ </para>
+ </sect2>
+
+ <sect2>
+ <title>EXPLAIN Output</title>
+ <para>
+ For a foreign table using <literal>pgsql_fdw</>, <command>EXPLAIN</> shows
+ a remote SQL statement which is sent to remote
+ <productname>PostgreSQL</productname> server for a ForeignScan plan node.
+ For example:
+ </para>
+ <synopsis>
+ postgres=# EXPLAIN SELECT aid FROM pgbench_accounts WHERE abalance < 0;
+ QUERY PLAN
+ --------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on pgbench_accounts (cost=100.00..8105.13 rows=302613 width=8)
+ Filter: (abalance < 0)
+ Remote SQL: DECLARE pgsql_fdw_cursor_3 SCROLL CURSOR FOR SELECT aid, NULL, abalance, NULL FROM public.pgbench_accounts
+ (3 rows)
+ </synopsis>
+ </sect2>
+
+ <sect2>
+ <title>Author</title>
+ <para>
+ Shigeru Hanada <email>shigeru.hanada@gmail.com</email>
+ </para>
+ </sect2>
+
+ </sect1>
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 64ba8ec..94b2ea9 100644
*** a/src/backend/utils/adt/ruleutils.c
--- b/src/backend/utils/adt/ruleutils.c
*************** deparse_context_for(const char *aliasnam
*** 2181,2186 ****
--- 2181,2209 ----
return list_make1(dpns);
}
+ /* ----------
+ * deparse_context_for_rtelist - Build deparse context for a list of RTEs
+ *
+ * Given the list of RangeTableEnetry, build deparsing context for an
+ * expression referencing those relations. This is sufficient for uses of
+ * deparse_expression before plan has been created.
+ * ----------
+ */
+ List *
+ deparse_context_for_rtelist(List *rtable)
+ {
+ deparse_namespace *dpns;
+
+ dpns = (deparse_namespace *) palloc0(sizeof(deparse_namespace));
+
+ /* Build a minimal RTE for the rel */
+ dpns->rtable = rtable;
+ dpns->ctes = NIL;
+
+ /* Return a one-deep namespace stack */
+ return list_make1(dpns);
+ }
+
/*
* deparse_context_for_planstate - Build deparse context for a plan
*
diff --git a/src/include/utils/builtins.h b/src/include/utils/builtins.h
index 9fda7ad..4cf0489 100644
*** a/src/include/utils/builtins.h
--- b/src/include/utils/builtins.h
*************** extern char *deparse_expression(Node *ex
*** 650,655 ****
--- 650,656 ----
extern List *deparse_context_for(const char *aliasname, Oid relid);
extern List *deparse_context_for_planstate(Node *planstate, List *ancestors,
List *rtable);
+ extern List *deparse_context_for_rtelist(List *rtable);
extern const char *quote_identifier(const char *ident);
extern char *quote_qualified_identifier(const char *qualifier,
const char *ident);
pgsql_fdw_pushdown_v9.patchtext/plain; name=pgsql_fdw_pushdown_v9.patchDownload
diff --git a/contrib/pgsql_fdw/deparse.c b/contrib/pgsql_fdw/deparse.c
index de49211..a023f44 100644
*** a/contrib/pgsql_fdw/deparse.c
--- b/contrib/pgsql_fdw/deparse.c
*************** typedef struct foreign_executable_cxt
*** 36,50 ****
RelOptInfo *foreignrel;
} foreign_executable_cxt;
/*
* Deparse query representation into SQL statement which suits for remote
! * PostgreSQL server.
*/
char *
deparseSql(Oid relid,
PlannerInfo *root,
RelOptInfo *baserel,
! ForeignTable *table)
{
StringInfoData foreign_relname;
StringInfoData sql; /* builder for SQL statement */
--- 36,58 ----
RelOptInfo *foreignrel;
} foreign_executable_cxt;
+ static bool is_foreign_expr(PlannerInfo *root, RelOptInfo *baserel, Expr *expr);
+ static bool foreign_expr_walker(Node *node, foreign_executable_cxt *context);
+ static bool is_builtin(Oid procid);
+ static bool contain_ext_param(Node *clause);
+ static bool contain_ext_param_walker(Node *node, void *context);
+
/*
* Deparse query representation into SQL statement which suits for remote
! * PostgreSQL server. Also some of quals in WHERE clause will be pushed down
! * If they are safe to be evaluated on the remote side.
*/
char *
deparseSql(Oid relid,
PlannerInfo *root,
RelOptInfo *baserel,
! ForeignTable *table,
! bool *has_param)
{
StringInfoData foreign_relname;
StringInfoData sql; /* builder for SQL statement */
*************** deparseSql(Oid relid,
*** 56,61 ****
--- 64,70 ----
const char *relname = NULL; /* plain relation name */
const char *q_nspname; /* quoted namespace name */
const char *q_relname; /* quoted relation name */
+ List *foreign_expr = NIL; /* list of Expr* evaluated on remote */
int i;
List *rtable = NIL;
List *context = NIL;
*************** deparseSql(Oid relid,
*** 64,78 ****
initStringInfo(&foreign_relname);
/*
! * First of all, determine which column should be retrieved for this scan.
*
* We do this before deparsing SELECT clause because attributes which are
* not used in neither reltargetlist nor baserel->baserestrictinfo, quals
* evaluated on local, can be replaced with literal "NULL" in the SELECT
* clause to reduce overhead of tuple handling tuple and data transfer.
*/
if (baserel->baserestrictinfo != NIL)
{
ListCell *lc;
foreach (lc, baserel->baserestrictinfo)
--- 73,97 ----
initStringInfo(&foreign_relname);
/*
! * First of all, determine which qual can be pushed down.
! *
! * The expressions which satisfy is_foreign_expr() are deparsed into WHERE
! * clause of result SQL string, and they could be removed from PlanState
! * to avoid duplicate evaluation at ExecScan().
! *
! * We never change the quals in the Plan node, because this execution might
! * be for a PREPAREd statement, thus the quals in the Plan node might be
! * reused to construct another PlanState for subsequent EXECUTE statement.
*
* We do this before deparsing SELECT clause because attributes which are
* not used in neither reltargetlist nor baserel->baserestrictinfo, quals
* evaluated on local, can be replaced with literal "NULL" in the SELECT
* clause to reduce overhead of tuple handling tuple and data transfer.
*/
+ *has_param = false;
if (baserel->baserestrictinfo != NIL)
{
+ List *local_qual = NIL;
ListCell *lc;
foreach (lc, baserel->baserestrictinfo)
*************** deparseSql(Oid relid,
*** 81,86 ****
--- 100,119 ----
List *attrs;
/*
+ * Determine whether the qual can be pushed down or not. If a qual
+ * can be pushed down and it contains external param, tell that to
+ * caller in order to fall back to fixed costs.
+ */
+ if (is_foreign_expr(root, baserel, ri->clause))
+ {
+ foreign_expr = lappend(foreign_expr, ri->clause);
+ if (contain_ext_param((Node*) ri->clause))
+ *has_param = true;
+ }
+ else
+ local_qual = lappend(local_qual, ri);
+
+ /*
* We need to know which attributes are used in qual evaluated
* on the local server, because they should be listed in the
* SELECT clause of remote query. We can ignore attributes
*************** deparseSql(Oid relid,
*** 92,97 ****
--- 125,136 ----
PVC_RECURSE_PLACEHOLDERS);
attr_used = list_union(attr_used, attrs);
}
+
+ /*
+ * Remove pushed down qualifiers from baserestrictinfo to avoid
+ * redundant evaluation.
+ */
+ baserel->baserestrictinfo = local_qual;
}
/*
*************** deparseSql(Oid relid,
*** 227,233 ****
--- 266,497 ----
*/
appendStringInfo(&sql, "FROM %s", foreign_relname.data);
+ /*
+ * deparse WHERE clause
+ */
+ if (foreign_expr != NIL)
+ {
+ Node *node;
+
+ node = (Node *) make_ands_explicit(foreign_expr);
+ appendStringInfo(&sql, " WHERE %s ",
+ deparse_expression(node, context, false, false));
+ list_free(foreign_expr);
+ foreign_expr = NIL;
+ }
+
elog(DEBUG3, "Remote SQL: %s", sql.data);
return sql.data;
}
+ /*
+ * Returns true if expr is safe to be evaluated on the foreign server.
+ */
+ static bool
+ is_foreign_expr(PlannerInfo *root, RelOptInfo *baserel, Expr *expr)
+ {
+ foreign_executable_cxt context;
+ context.root = root;
+ context.foreignrel = baserel;
+
+ /*
+ * An expression which includes any mutable function can't be pushed down
+ * because it's result is not stable. For example, pushing now() down to
+ * remote side would cause confusion from the clock offset.
+ * If we have routine mapping infrastructure in future release, we will be
+ * able to choose function to be pushed down in finer granularity.
+ */
+ if (contain_mutable_functions((Node *) expr))
+ return false;
+
+ /*
+ * Check that the expression consists of nodes which are known as safe to
+ * be pushed down.
+ */
+ if (foreign_expr_walker((Node *) expr, &context))
+ return false;
+
+ return true;
+ }
+
+ /*
+ * Return true if node includes any node which is not known as safe to be
+ * pushed down.
+ */
+ static bool
+ foreign_expr_walker(Node *node, foreign_executable_cxt *context)
+ {
+ if (node == NULL)
+ return false;
+
+ /*
+ * If given expression has valid collation, it can't be pushed down because
+ * it might has incompatible semantics on remote side.
+ */
+ if (exprCollation(node) != InvalidOid ||
+ exprInputCollation(node) != InvalidOid)
+ return true;
+
+ /*
+ * If return type of given expression is not built-in, it can't be pushed
+ * down because it might has incompatible semantics on remote side.
+ */
+ if (!is_builtin(exprType(node)))
+ return true;
+
+ switch (nodeTag(node))
+ {
+ case T_Const:
+ case T_BoolExpr:
+ case T_NullTest:
+ case T_DistinctExpr:
+ case T_RelabelType:
+ /*
+ * 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 at the tail of this function.
+ */
+ break;
+ /*
+ * If function used by the expression is not built-in, it can't be
+ * pushed down because it might has incompatible semantics on remote
+ * side.
+ */
+ case T_FuncExpr:
+ {
+ FuncExpr *fe = (FuncExpr *) node;
+ if (!is_builtin(fe->funcid))
+ return true;
+ }
+ break;
+ case T_Param:
+ /*
+ * Only external parameters can be pushed down.:
+ */
+ {
+ if (((Param *) node)->paramkind != PARAM_EXTERN)
+ return true;
+ }
+ break;
+ case T_ScalarArrayOpExpr:
+ /*
+ * Only built-in operators can be pushed down. In addition,
+ * underlying function must be built-in and immutable, but we don't
+ * check volatility here; such check must be done already with
+ * contain_mutable_functions.
+ */
+ {
+ ScalarArrayOpExpr *oe = (ScalarArrayOpExpr *) node;
+
+ if (!is_builtin(oe->opno) || !is_builtin(oe->opfuncid))
+ return true;
+
+ /* operands are checked later */
+ }
+ break;
+ case T_OpExpr:
+ /*
+ * Only built-in operators can be pushed down. In addition,
+ * underlying function must be built-in and immutable, but we don't
+ * check volatility here; such check must be done already with
+ * contain_mutable_functions.
+ */
+ {
+ OpExpr *oe = (OpExpr *) node;
+
+ if (!is_builtin(oe->opno) || !is_builtin(oe->opfuncid))
+ return true;
+
+ /* operands are checked later */
+ }
+ 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;
+ foreign_executable_cxt *f_context;
+
+ f_context = (foreign_executable_cxt *) context;
+ if (var->varno != f_context->foreignrel->relid ||
+ var->varlevelsup != 0)
+ return true;
+ }
+ break;
+ case T_ArrayRef:
+ /*
+ * ArrayRef which holds non-built-in typed elements can't be pushed
+ * down.
+ */
+ {
+ if (!is_builtin(((ArrayRef *) node)->refelemtype))
+ return true;
+ }
+ break;
+ case T_ArrayExpr:
+ /*
+ * ArrayExpr which holds non-built-in typed elements can't be pushed
+ * down.
+ */
+ {
+ if (!is_builtin(((ArrayExpr *) node)->element_typeid))
+ return true;
+ }
+ break;
+ default:
+ {
+ ereport(DEBUG3,
+ (errmsg("expression is too complex"),
+ errdetail("%s", nodeToString(node))));
+ return true;
+ }
+ break;
+ }
+
+ return expression_tree_walker(node, foreign_expr_walker, context);
+ }
+
+ /*
+ * Return true if given object is one of built-in objects.
+ */
+ static bool
+ is_builtin(Oid oid)
+ {
+ return (oid < FirstNormalObjectId);
+ }
+
+ /*
+ * contain_ext_param
+ * Recursively search for Param nodes within a clause.
+ *
+ * Returns true if any parameter reference node with relkind PARAM_EXTERN
+ * found.
+ *
+ * This does not descend into subqueries, and so should be used only after
+ * reduction of sublinks to subplans, or in contexts where it's known there
+ * are no subqueries. There mustn't be outer-aggregate references either.
+ *
+ * XXX: These functions could be in core, src/backend/optimizer/util/clauses.c.
+ */
+ static bool
+ contain_ext_param(Node *clause)
+ {
+ return contain_ext_param_walker(clause, NULL);
+ }
+
+ static bool
+ contain_ext_param_walker(Node *node, void *context)
+ {
+ if (node == NULL)
+ return false;
+ if (IsA(node, Param))
+ {
+ Param *param = (Param *) node;
+
+ if (param->paramkind == PARAM_EXTERN)
+ return true; /* abort the tree traversal and return true */
+ }
+ return expression_tree_walker(node, contain_ext_param_walker, context);
+ }
diff --git a/contrib/pgsql_fdw/expected/pgsql_fdw.out b/contrib/pgsql_fdw/expected/pgsql_fdw.out
index c1e2e82..51b347e 100644
*** a/contrib/pgsql_fdw/expected/pgsql_fdw.out
--- b/contrib/pgsql_fdw/expected/pgsql_fdw.out
*************** SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.
*** 230,241 ****
-- with WHERE clause
EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
! QUERY PLAN
! ------------------------------------------------------------------------------------------------------------------
Foreign Scan on public.ft1 t1
Output: c1, c2, c3, c4, c5, c6, c7
! Filter: ((t1.c1 = 101) AND ((t1.c6)::text = '1'::text) AND (t1.c7 = '1'::bpchar))
! Remote SQL: DECLARE pgsql_fdw_cursor_4 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
(4 rows)
SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
--- 230,241 ----
-- with WHERE clause
EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
! QUERY PLAN
! ---------------------------------------------------------------------------------------------------------------------------------------
Foreign Scan on public.ft1 t1
Output: c1, c2, c3, c4, c5, c6, c7
! Filter: (((t1.c6)::text = '1'::text) AND (t1.c7 = '1'::bpchar))
! Remote SQL: DECLARE pgsql_fdw_cursor_4 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE ("C 1" = 101)
(4 rows)
SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
*************** EXPLAIN (COSTS false) SELECT * FROM ft1
*** 343,362 ****
(3 rows)
EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = abs(t1.c2);
! QUERY PLAN
! -------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Filter: (c1 = abs(c2))
! Remote SQL: DECLARE pgsql_fdw_cursor_20 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
! (3 rows)
EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = t1.c2;
! QUERY PLAN
! -------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Filter: (c1 = c2)
! Remote SQL: DECLARE pgsql_fdw_cursor_21 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
! (3 rows)
-- ===================================================================
-- parameterized queries
--- 343,360 ----
(3 rows)
EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = abs(t1.c2);
! QUERY PLAN
! --------------------------------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Remote SQL: DECLARE pgsql_fdw_cursor_20 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE ("C 1" = abs(c2))
! (2 rows)
EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = t1.c2;
! QUERY PLAN
! ---------------------------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Remote SQL: DECLARE pgsql_fdw_cursor_21 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE ("C 1" = c2)
! (2 rows)
-- ===================================================================
-- parameterized queries
*************** EXPLAIN (COSTS false) SELECT * FROM ft1
*** 364,380 ****
-- simple join
PREPARE st1(int, int) AS SELECT t1.c3, t2.c3 FROM ft1 t1, ft2 t2 WHERE t1.c1 = $1 AND t2.c1 = $2;
EXPLAIN (COSTS false) EXECUTE st1(1, 2);
! QUERY PLAN
! -----------------------------------------------------------------------------------------------------------------------------------------
Nested Loop
-> Foreign Scan on ft1 t1
! Filter: (c1 = 1)
! Remote SQL: DECLARE pgsql_fdw_cursor_22 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, NULL, NULL, NULL, NULL FROM "S 1"."T 1"
! -> Materialize
! -> Foreign Scan on ft2 t2
! Filter: (c1 = 2)
! Remote SQL: DECLARE pgsql_fdw_cursor_23 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, NULL, NULL, NULL, NULL FROM "S 1"."T 1"
! (8 rows)
EXECUTE st1(1, 1);
c3 | c3
--- 362,375 ----
-- simple join
PREPARE st1(int, int) AS SELECT t1.c3, t2.c3 FROM ft1 t1, ft2 t2 WHERE t1.c1 = $1 AND t2.c1 = $2;
EXPLAIN (COSTS false) EXECUTE st1(1, 2);
! QUERY PLAN
! ------------------------------------------------------------------------------------------------------------------------------------------------------
Nested Loop
-> Foreign Scan on ft1 t1
! Remote SQL: DECLARE pgsql_fdw_cursor_22 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, NULL, NULL, NULL, NULL FROM "S 1"."T 1" WHERE ("C 1" = 1)
! -> Foreign Scan on ft2 t2
! Remote SQL: DECLARE pgsql_fdw_cursor_23 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, NULL, NULL, NULL, NULL FROM "S 1"."T 1" WHERE ("C 1" = 2)
! (5 rows)
EXECUTE st1(1, 1);
c3 | c3
*************** EXECUTE st1(101, 101);
*** 391,411 ****
-- subquery using stable function (can't be pushed down)
PREPARE st2(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c4) = 6) ORDER BY c1;
EXPLAIN (COSTS false) EXECUTE st2(10, 20);
! QUERY PLAN
! ---------------------------------------------------------------------------------------------------------------------------------------------------
Sort
Sort Key: t1.c1
-> Hash Join
Hash Cond: (t1.c3 = t2.c3)
-> Foreign Scan on ft1 t1
! Filter: (c1 < 20)
! Remote SQL: DECLARE pgsql_fdw_cursor_28 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
-> Hash
-> HashAggregate
-> Foreign Scan on ft2 t2
! Filter: ((c1 > 10) AND (date_part('dow'::text, c4) = 6::double precision))
! Remote SQL: DECLARE pgsql_fdw_cursor_29 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, c4, NULL, NULL, NULL FROM "S 1"."T 1"
! (12 rows)
EXECUTE st2(10, 20);
c1 | c2 | c3 | c4 | c5 | c6 | c7
--- 386,405 ----
-- subquery using stable function (can't be pushed down)
PREPARE st2(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c4) = 6) ORDER BY c1;
EXPLAIN (COSTS false) EXECUTE st2(10, 20);
! QUERY PLAN
! -----------------------------------------------------------------------------------------------------------------------------------------------------------------------
Sort
Sort Key: t1.c1
-> Hash Join
Hash Cond: (t1.c3 = t2.c3)
-> Foreign Scan on ft1 t1
! Remote SQL: DECLARE pgsql_fdw_cursor_28 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE ("C 1" < 20)
-> Hash
-> HashAggregate
-> Foreign Scan on ft2 t2
! Filter: (date_part('dow'::text, c4) = 6::double precision)
! Remote SQL: DECLARE pgsql_fdw_cursor_29 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, c4, NULL, NULL, NULL FROM "S 1"."T 1" WHERE ("C 1" > 10)
! (11 rows)
EXECUTE st2(10, 20);
c1 | c2 | c3 | c4 | c5 | c6 | c7
*************** EXECUTE st1(101, 101);
*** 422,442 ****
-- subquery using immutable function (can be pushed down)
PREPARE st3(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c5) = 6) ORDER BY c1;
EXPLAIN (COSTS false) EXECUTE st3(10, 20);
! QUERY PLAN
! ---------------------------------------------------------------------------------------------------------------------------------------------------
Sort
Sort Key: t1.c1
-> Hash Join
Hash Cond: (t1.c3 = t2.c3)
-> Foreign Scan on ft1 t1
! Filter: (c1 < 20)
! Remote SQL: DECLARE pgsql_fdw_cursor_34 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
-> Hash
-> HashAggregate
-> Foreign Scan on ft2 t2
! Filter: ((c1 > 10) AND (date_part('dow'::text, c5) = 6::double precision))
! Remote SQL: DECLARE pgsql_fdw_cursor_35 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, NULL, c5, NULL, NULL FROM "S 1"."T 1"
! (12 rows)
EXECUTE st3(10, 20);
c1 | c2 | c3 | c4 | c5 | c6 | c7
--- 416,435 ----
-- subquery using immutable function (can be pushed down)
PREPARE st3(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c5) = 6) ORDER BY c1;
EXPLAIN (COSTS false) EXECUTE st3(10, 20);
! QUERY PLAN
! -----------------------------------------------------------------------------------------------------------------------------------------------------------------------
Sort
Sort Key: t1.c1
-> Hash Join
Hash Cond: (t1.c3 = t2.c3)
-> Foreign Scan on ft1 t1
! Remote SQL: DECLARE pgsql_fdw_cursor_34 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE ("C 1" < 20)
-> Hash
-> HashAggregate
-> Foreign Scan on ft2 t2
! Filter: (date_part('dow'::text, c5) = 6::double precision)
! Remote SQL: DECLARE pgsql_fdw_cursor_35 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, NULL, c5, NULL, NULL FROM "S 1"."T 1" WHERE ("C 1" > 10)
! (11 rows)
EXECUTE st3(10, 20);
c1 | c2 | c3 | c4 | c5 | c6 | c7
*************** EXECUTE st3(20, 30);
*** 453,504 ****
-- custom plan should be chosen
PREPARE st4(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 = $1;
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! -------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Filter: (c1 = 1)
! Remote SQL: DECLARE pgsql_fdw_cursor_40 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
! (3 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! -------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Filter: (c1 = 1)
! Remote SQL: DECLARE pgsql_fdw_cursor_41 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
! (3 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! -------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Filter: (c1 = 1)
! Remote SQL: DECLARE pgsql_fdw_cursor_42 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
! (3 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! -------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Filter: (c1 = 1)
! Remote SQL: DECLARE pgsql_fdw_cursor_43 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
! (3 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! -------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Filter: (c1 = 1)
! Remote SQL: DECLARE pgsql_fdw_cursor_44 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
! (3 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! -------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Filter: (c1 = $1)
! Remote SQL: DECLARE pgsql_fdw_cursor_45 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
! (3 rows)
-- cleanup
DEALLOCATE st1;
--- 446,491 ----
-- custom plan should be chosen
PREPARE st4(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 = $1;
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! --------------------------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Remote SQL: DECLARE pgsql_fdw_cursor_40 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE ("C 1" = 1)
! (2 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! --------------------------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Remote SQL: DECLARE pgsql_fdw_cursor_41 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE ("C 1" = 1)
! (2 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! --------------------------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Remote SQL: DECLARE pgsql_fdw_cursor_42 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE ("C 1" = 1)
! (2 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! --------------------------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Remote SQL: DECLARE pgsql_fdw_cursor_43 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE ("C 1" = 1)
! (2 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! --------------------------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Remote SQL: DECLARE pgsql_fdw_cursor_44 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE ("C 1" = 1)
! (2 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! --------------------------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Remote SQL: DECLARE pgsql_fdw_cursor_46 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE ("C 1" = 1)
! (2 rows)
-- cleanup
DEALLOCATE st1;
diff --git a/contrib/pgsql_fdw/pgsql_fdw.c b/contrib/pgsql_fdw/pgsql_fdw.c
index d0d6767..7c39307 100644
*** a/contrib/pgsql_fdw/pgsql_fdw.c
--- b/contrib/pgsql_fdw/pgsql_fdw.c
*************** pgsqlPlanForeignScan(Oid foreigntableid,
*** 196,210 ****
List *fdw_private = NIL;
ForeignTable *table;
ForeignServer *server;
/*
* Generate remote query string, and estimate cost of this scan.
*/
table = GetForeignTable(foreigntableid);
! server = GetForeignServer(table->serverid);
! sql = deparseSql(foreigntableid, root, baserel, table);
! estimate_costs(root, baserel, sql, server->serverid,
! &startup_cost, &total_cost);
/*
* Store plain SELECT statement in private area of FdwPlan. This will be
--- 196,227 ----
List *fdw_private = NIL;
ForeignTable *table;
ForeignServer *server;
+ bool has_param;
/*
* Generate remote query string, and estimate cost of this scan.
+ *
+ * If the baserestrictinfo contains any Param node with paramkind
+ * PARAM_EXTERNAL as part of push-down-able condition, we need to
+ * groundless fixed costs because we can't get actual parameter values
+ * here, so we use fixed and large costs as second best so that planner
+ * tend to choose custom plan.
+ *
+ * See comments in plancache.c for details of custom plan.
*/
table = GetForeignTable(foreigntableid);
! sql = deparseSql(foreigntableid, root, baserel, table, &has_param);
! if (has_param)
! {
! startup_cost = CONNECTION_COSTS;
! total_cost = 10000; /* groundless large costs */
! }
! else
! {
! server = GetForeignServer(table->serverid);
! estimate_costs(root, baserel, sql, server->serverid,
! &startup_cost, &total_cost);
! }
/*
* Store plain SELECT statement in private area of FdwPlan. This will be
diff --git a/contrib/pgsql_fdw/pgsql_fdw.h b/contrib/pgsql_fdw/pgsql_fdw.h
index 78a6abf..020d282 100644
*** a/contrib/pgsql_fdw/pgsql_fdw.h
--- b/contrib/pgsql_fdw/pgsql_fdw.h
*************** int ExtractConnectionOptions(List *defel
*** 27,32 ****
char *deparseSql(Oid relid,
PlannerInfo *root,
RelOptInfo *baserel,
! ForeignTable *table);
#endif /* PGSQL_FDW_H */
--- 27,33 ----
char *deparseSql(Oid relid,
PlannerInfo *root,
RelOptInfo *baserel,
! ForeignTable *table,
! bool *has_param);
#endif /* PGSQL_FDW_H */
diff --git a/doc/src/sgml/pgsql-fdw.sgml b/doc/src/sgml/pgsql-fdw.sgml
index 30fa7f5..2375d8f 100644
*** a/doc/src/sgml/pgsql-fdw.sgml
--- b/doc/src/sgml/pgsql-fdw.sgml
*************** postgres=# SELECT pgsql_fdw_disconnect(s
*** 242,253 ****
</para>
<synopsis>
postgres=# EXPLAIN SELECT aid FROM pgbench_accounts WHERE abalance < 0;
! QUERY PLAN
! --------------------------------------------------------------------------------------------------------------------------
! Foreign Scan on pgbench_accounts (cost=100.00..8105.13 rows=302613 width=8)
! Filter: (abalance < 0)
! Remote SQL: DECLARE pgsql_fdw_cursor_3 SCROLL CURSOR FOR SELECT aid, NULL, abalance, NULL FROM public.pgbench_accounts
! (3 rows)
</synopsis>
</sect2>
--- 242,252 ----
</para>
<synopsis>
postgres=# EXPLAIN SELECT aid FROM pgbench_accounts WHERE abalance < 0;
! QUERY PLAN
! ------------------------------------------------------------------------------------------------------------------------------------------------
! Foreign Scan on pgbench_accounts (cost=100.00..8861.66 rows=518 width=8)
! Remote SQL: DECLARE pgsql_fdw_cursor_1 SCROLL CURSOR FOR SELECT aid, NULL, abalance, NULL FROM public.pgbench_accounts WHERE (abalance < 0)
! (2 rows)
</synopsis>
</sect2>
On tis, 2012-03-06 at 13:39 -0500, Tom Lane wrote:
A bigger issue with postgresql_fdw_validator is that it supposes that
the core backend is authoritative as to what options libpq supports,
which is bad design on its face. It would be much more sensible for
dblink to be asking libpq what options libpq supports, say via
PQconndefaults().
The validator for the proposed FDW suffers from the same problem.
Shigeru Hanada <shigeru.hanada@gmail.com> writes:
Agreed. Attached fdw_helper patch doesn't contain GetFdwOptionValue()
any more, and pgsql_fdw patch accesses only necessary catalogs.
I've committed the fdw_helper part of this, with some very minor
improvements.
regards, tom lane
(2012/03/07 21:47), Shigeru Hanada wrote:
Agreed. Attached fdw_helper patch doesn't contain GetFdwOptionValue()
any more, and pgsql_fdw patch accesses only necessary catalogs.
Oops, I've missed some bugs. Attached patch fixes them.
1) foreign table's fetch_count options is always ignored
2) If given connection is not managed by connection cache,
ReleaseConnection() crashes backend process by NULL dereference.
Regards,
--
Shigeru Hanada
Attachments:
pgsql_fdw_v15.patchtext/plain; name=pgsql_fdw_v15.patchDownload
diff --git a/contrib/Makefile b/contrib/Makefile
index ac0a80a..14aa433 100644
*** a/contrib/Makefile
--- b/contrib/Makefile
*************** SUBDIRS = \
*** 41,46 ****
--- 41,47 ----
pgbench \
pgcrypto \
pgrowlocks \
+ pgsql_fdw \
pgstattuple \
seg \
spi \
diff --git a/contrib/README b/contrib/README
index a1d42a1..d3fa211 100644
*** a/contrib/README
--- b/contrib/README
*************** pgrowlocks -
*** 158,163 ****
--- 158,167 ----
A function to return row locking information
by Tatsuo Ishii <ishii@sraoss.co.jp>
+ pgsql_fdw -
+ Foreign-data wrapper for external PostgreSQL servers.
+ by Shigeru Hanada <shigeru.hanada@gmail.com>
+
pgstattuple -
Functions to return statistics about "dead" tuples and free
space within a table
diff --git a/contrib/pgsql_fdw/.gitignore b/contrib/pgsql_fdw/.gitignore
index ...0854728 .
*** a/contrib/pgsql_fdw/.gitignore
--- b/contrib/pgsql_fdw/.gitignore
***************
*** 0 ****
--- 1,4 ----
+ # Generated subdirectories
+ /results/
+ *.o
+ *.so
diff --git a/contrib/pgsql_fdw/Makefile b/contrib/pgsql_fdw/Makefile
index ...6381365 .
*** a/contrib/pgsql_fdw/Makefile
--- b/contrib/pgsql_fdw/Makefile
***************
*** 0 ****
--- 1,22 ----
+ # contrib/pgsql_fdw/Makefile
+
+ MODULE_big = pgsql_fdw
+ OBJS = pgsql_fdw.o option.o deparse.o connection.o
+ PG_CPPFLAGS = -I$(libpq_srcdir)
+ SHLIB_LINK = $(libpq)
+
+ EXTENSION = pgsql_fdw
+ DATA = pgsql_fdw--1.0.sql
+
+ REGRESS = pgsql_fdw
+
+ ifdef USE_PGXS
+ PG_CONFIG = pg_config
+ PGXS := $(shell $(PG_CONFIG) --pgxs)
+ include $(PGXS)
+ else
+ subdir = contrib/pgsql_fdw
+ top_builddir = ../..
+ include $(top_builddir)/src/Makefile.global
+ include $(top_srcdir)/contrib/contrib-global.mk
+ endif
diff --git a/contrib/pgsql_fdw/connection.c b/contrib/pgsql_fdw/connection.c
index ...c971bd7 .
*** a/contrib/pgsql_fdw/connection.c
--- b/contrib/pgsql_fdw/connection.c
***************
*** 0 ****
--- 1,555 ----
+ /*-------------------------------------------------------------------------
+ *
+ * connection.c
+ * Connection management for pgsql_fdw
+ *
+ * Portions Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/connection.c
+ *
+ *-------------------------------------------------------------------------
+ */
+ #include "postgres.h"
+
+ #include "access/xact.h"
+ #include "catalog/pg_type.h"
+ #include "foreign/foreign.h"
+ #include "funcapi.h"
+ #include "libpq-fe.h"
+ #include "mb/pg_wchar.h"
+ #include "miscadmin.h"
+ #include "utils/array.h"
+ #include "utils/builtins.h"
+ #include "utils/hsearch.h"
+ #include "utils/memutils.h"
+ #include "utils/resowner.h"
+ #include "utils/tuplestore.h"
+
+ #include "pgsql_fdw.h"
+ #include "connection.h"
+
+ /* ============================================================================
+ * Connection management functions
+ * ==========================================================================*/
+
+ /*
+ * Connection cache entry managed with hash table.
+ */
+ typedef struct ConnCacheEntry
+ {
+ /* hash key must be first */
+ Oid serverid; /* oid of foreign server */
+ Oid userid; /* oid of local user */
+
+ bool use_tx; /* true when using remote transaction */
+ 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);
+ static void begin_remote_tx(PGconn *conn);
+ static void abort_remote_tx(PGconn *conn);
+
+ /*
+ * 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.
+ *
+ * When use_tx is true, remote transaction is started if caller is the only
+ * user of the connection. Isolation level of the remote transaction is same
+ * as local transaction, and remote transaction will be aborted when last
+ * user release.
+ *
+ * TODO: Note that caching connections requires a mechanism to detect change of
+ * FDW object to invalidate already established connections.
+ */
+ PGconn *
+ GetConnection(ForeignServer *server, UserMapping *user, bool use_tx)
+ {
+ bool found;
+ ConnCacheEntry *entry;
+ ConnCacheEntry key;
+
+ /* initialize connection cache if it isn't */
+ if (FSConnectionHash == NULL)
+ {
+ HASHCTL ctl;
+
+ /* hash key is a pair of oids: serverid and userid */
+ MemSet(&ctl, 0, sizeof(ctl));
+ ctl.keysize = sizeof(Oid) + sizeof(Oid);
+ ctl.entrysize = sizeof(ConnCacheEntry);
+ ctl.hash = tag_hash;
+ ctl.match = memcmp;
+ ctl.keycopy = memcpy;
+ /* allocate FSConnectionHash in the cache context */
+ ctl.hcxt = CacheMemoryContext;
+ FSConnectionHash = hash_create("Foreign Connections", 32,
+ &ctl,
+ HASH_ELEM | HASH_CONTEXT |
+ HASH_FUNCTION | HASH_COMPARE |
+ HASH_KEYCOPY);
+ }
+
+ /* Create key value for the entry. */
+ MemSet(&key, 0, sizeof(key));
+ key.serverid = server->serverid;
+ key.userid = GetOuterUserId();
+
+ /*
+ * Find cached entry for requested connection. If we couldn't find,
+ * callback function of ResourceOwner should be registered to clean the
+ * connection up on error including user interrupt.
+ */
+ entry = hash_search(FSConnectionHash, &key, HASH_ENTER, &found);
+ if (!found)
+ {
+ entry->refs = 0;
+ entry->use_tx = false;
+ entry->conn = NULL;
+ RegisterResourceReleaseCallback(cleanup_connection, entry);
+ }
+
+ /*
+ * We don't check the health of cached connection here, because it would
+ * require some overhead. Broken connection and its cache entry will be
+ * cleaned up when the connection is actually used.
+ */
+
+ /*
+ * If cache entry doesn't have connection, we have to establish new
+ * connection.
+ */
+ if (entry->conn == NULL)
+ {
+ /*
+ * Use PG_TRY block to ensure closing connection on error.
+ */
+ PG_TRY();
+ {
+ /*
+ * Connect to the foreign PostgreSQL server, and store it in cache
+ * entry to keep new connection.
+ * Note: key items of entry has already been initialized in
+ * hash_search(HASH_ENTER).
+ */
+ entry->conn = connect_pg_server(server, user);
+ }
+ PG_CATCH();
+ {
+ /* Clear connection cache entry on error case. */
+ PQfinish(entry->conn);
+ entry->refs = 0;
+ entry->use_tx = false;
+ entry->conn = NULL;
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+ }
+
+ /* Increase connection reference counter. */
+ entry->refs++;
+
+ /*
+ * If requester is the only referrer of this connection, start transaction
+ * with the same isolation level as the local transaction we are in.
+ * We need to remember whether this connection uses remote transaction to
+ * abort it when this connection is released completely.
+ */
+ if (use_tx && entry->refs == 1)
+ begin_remote_tx(entry->conn);
+ entry->use_tx = use_tx;
+
+
+ return entry->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 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. Those two object hold only libpq options.
+ * Extra 3 items are for:
+ * *) fallback_application_name
+ * *) client_encoding
+ * *) NULL termination (end marker)
+ *
+ * Note: We don't omit any parameters even target database might be older
+ * than local, because unexpected parameters are just ignored.
+ */
+ n = list_length(server->options) + list_length(user->options) + 3;
+ 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 += ExtractConnectionOptions(server->options,
+ all_keywords + n, all_values + n);
+ n += ExtractConnectionOptions(user->options,
+ all_keywords + n, all_values + n);
+ all_keywords[n] = all_values[n] = NULL;
+
+ for (i = 0, j = 0; all_keywords[i]; i++)
+ {
+ keywords[j] = all_keywords[i];
+ values[j] = all_values[i];
+ j++;
+ }
+
+ /* Use "pgsql_fdw" as fallback_application_name. */
+ keywords[j] = "fallback_application_name";
+ values[j++] = "pgsql_fdw";
+
+ /* Set client_encoding so that libpq can convert encoding properly. */
+ keywords[j] = "client_encoding";
+ values[j++] = GetDatabaseEncodingName();
+
+ keywords[j] = values[j] = NULL;
+ pfree(all_keywords);
+ pfree(all_values);
+
+ /* 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.")));
+ }
+
+ return conn;
+ }
+
+ /*
+ * Start transaction to use cursor to retrieve data separately.
+ */
+ static void
+ begin_remote_tx(PGconn *conn)
+ {
+ const char *sql = NULL; /* keep compiler quiet. */
+ PGresult *res;
+
+ switch (XactIsoLevel)
+ {
+ case XACT_READ_UNCOMMITTED:
+ case XACT_READ_COMMITTED:
+ case XACT_REPEATABLE_READ:
+ sql = "START TRANSACTION ISOLATION LEVEL REPEATABLE READ";
+ break;
+ case XACT_SERIALIZABLE:
+ sql = "START TRANSACTION ISOLATION LEVEL SERIALIZABLE";
+ break;
+ default:
+ elog(ERROR, "unexpected isolation level: %d", XactIsoLevel);
+ break;
+ }
+
+ elog(DEBUG3, "starting remote transaction with \"%s\"", sql);
+
+ res = PQexec(conn, sql);
+ if (PQresultStatus(res) != PGRES_COMMAND_OK)
+ {
+ PQclear(res);
+ elog(ERROR, "could not start transaction: %s", PQerrorMessage(conn));
+ }
+ PQclear(res);
+ }
+
+ static void
+ abort_remote_tx(PGconn *conn)
+ {
+ PGresult *res;
+
+ elog(DEBUG3, "aborting remote transaction");
+
+ res = PQexec(conn, "ABORT TRANSACTION");
+ if (PQresultStatus(res) != PGRES_COMMAND_OK)
+ {
+ PQclear(res);
+ elog(ERROR, "could not abort transaction: %s", PQerrorMessage(conn));
+ }
+ PQclear(res);
+ }
+
+ /*
+ * 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 sequentially 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)
+ {
+ hash_seq_term(&scan);
+ break;
+ }
+ }
+
+ /* If the released connection was an orphan, just close it. */
+ if (entry == NULL)
+ {
+ PQfinish(conn);
+ return;
+ }
+
+ /*
+ * If this connection uses remote transaction and caller is the last user,
+ * abort remote transaction and forget about it.
+ */
+ if (entry->use_tx && entry->refs == 1)
+ {
+ abort_remote_tx(conn);
+ entry->use_tx = false;
+ }
+
+ /*
+ * Decrease reference counter of this connection. Even if the caller was
+ * the last referrer, we don't unregister it from cache.
+ */
+ entry->refs--;
+ }
+
+ /*
+ * 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 need to clean up at end of subtransactions, because they might
+ * be recovered to consistent state with savepoints.
+ */
+ if (!isTopLevel)
+ return;
+
+ /*
+ * Here, it must be after abort of top level transaction. Disconnect and
+ * forget every referrer.
+ */
+ if (entry->conn != NULL)
+ {
+ PQfinish(entry->conn);
+ entry->refs = 0;
+ entry->conn = NULL;
+ }
+ }
+
+ /*
+ * Get list of connections currently active.
+ */
+ Datum pgsql_fdw_get_connections(PG_FUNCTION_ARGS);
+ PG_FUNCTION_INFO_V1(pgsql_fdw_get_connections);
+ Datum
+ pgsql_fdw_get_connections(PG_FUNCTION_ARGS)
+ {
+ ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+ HASH_SEQ_STATUS scan;
+ ConnCacheEntry *entry;
+ MemoryContext oldcontext = CurrentMemoryContext;
+ Tuplestorestate *tuplestore;
+ TupleDesc tupdesc;
+
+ /* We return list of connection with storing them in a Tuplestore. */
+ rsinfo->returnMode = SFRM_Materialize;
+ rsinfo->setResult = NULL;
+ rsinfo->setDesc = NULL;
+
+ /* Create tuplestore and copy of TupleDesc in per-query context. */
+ MemoryContextSwitchTo(rsinfo->econtext->ecxt_per_query_memory);
+
+ tupdesc = CreateTemplateTupleDesc(2, false);
+ TupleDescInitEntry(tupdesc, 1, "srvid", OIDOID, -1, 0);
+ TupleDescInitEntry(tupdesc, 2, "usesysid", OIDOID, -1, 0);
+ rsinfo->setDesc = tupdesc;
+
+ tuplestore = tuplestore_begin_heap(false, false, work_mem);
+ rsinfo->setResult = tuplestore;
+
+ MemoryContextSwitchTo(oldcontext);
+
+ /*
+ * We need to scan sequentially since we use the address to find
+ * appropriate PGconn from the hash table.
+ */
+ if (FSConnectionHash != NULL)
+ {
+ hash_seq_init(&scan, FSConnectionHash);
+ while ((entry = (ConnCacheEntry *) hash_seq_search(&scan)))
+ {
+ Datum values[2];
+ bool nulls[2];
+ HeapTuple tuple;
+
+ /* Ignore inactive connections */
+ if (PQstatus(entry->conn) != CONNECTION_OK)
+ continue;
+
+ /*
+ * Ignore other users' connections if current user isn't a
+ * superuser.
+ */
+ if (!superuser() && entry->userid != GetUserId())
+ continue;
+
+ values[0] = ObjectIdGetDatum(entry->serverid);
+ values[1] = ObjectIdGetDatum(entry->userid);
+ nulls[0] = false;
+ nulls[1] = false;
+
+ tuple = heap_formtuple(tupdesc, values, nulls);
+ tuplestore_puttuple(tuplestore, tuple);
+ }
+ }
+ tuplestore_donestoring(tuplestore);
+
+ PG_RETURN_VOID();
+ }
+
+ /*
+ * Discard persistent connection designated by given connection name.
+ */
+ Datum pgsql_fdw_disconnect(PG_FUNCTION_ARGS);
+ PG_FUNCTION_INFO_V1(pgsql_fdw_disconnect);
+ Datum
+ pgsql_fdw_disconnect(PG_FUNCTION_ARGS)
+ {
+ Oid serverid = PG_GETARG_OID(0);
+ Oid userid = PG_GETARG_OID(1);
+ ConnCacheEntry key;
+ ConnCacheEntry *entry = NULL;
+ bool found;
+
+ /* Non-superuser can't discard other users' connection. */
+ if (!superuser() && userid != GetOuterUserId())
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_RESOURCES),
+ errmsg("only superuser can discard other user's connection")));
+
+ /*
+ * If no connection has been established, or no such connections, just
+ * return "NG" to indicate nothing has done.
+ */
+ if (FSConnectionHash == NULL)
+ PG_RETURN_TEXT_P(cstring_to_text("NG"));
+
+ key.serverid = serverid;
+ key.userid = userid;
+ entry = hash_search(FSConnectionHash, &key, HASH_FIND, &found);
+ if (!found)
+ PG_RETURN_TEXT_P(cstring_to_text("NG"));
+
+ /* Discard cached connection, and clear reference counter. */
+ PQfinish(entry->conn);
+ entry->refs = 0;
+ entry->conn = NULL;
+
+ PG_RETURN_TEXT_P(cstring_to_text("OK"));
+ }
diff --git a/contrib/pgsql_fdw/connection.h b/contrib/pgsql_fdw/connection.h
index ...4427f56 .
*** a/contrib/pgsql_fdw/connection.h
--- b/contrib/pgsql_fdw/connection.h
***************
*** 0 ****
--- 1,25 ----
+ /*-------------------------------------------------------------------------
+ *
+ * connection.h
+ * Connection management for pgsql_fdw
+ *
+ * Portions Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_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, bool use_tx);
+ void ReleaseConnection(PGconn *conn);
+
+ #endif /* CONNECTION_H */
diff --git a/contrib/pgsql_fdw/deparse.c b/contrib/pgsql_fdw/deparse.c
index ...de49211 .
*** a/contrib/pgsql_fdw/deparse.c
--- b/contrib/pgsql_fdw/deparse.c
***************
*** 0 ****
--- 1,233 ----
+ /*-------------------------------------------------------------------------
+ *
+ * deparse.c
+ * query deparser for PostgreSQL
+ *
+ * Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/deparse.c
+ *
+ *-------------------------------------------------------------------------
+ */
+ #include "postgres.h"
+
+ #include "access/transam.h"
+ #include "commands/defrem.h"
+ #include "foreign/foreign.h"
+ #include "lib/stringinfo.h"
+ #include "nodes/nodeFuncs.h"
+ #include "nodes/nodes.h"
+ #include "nodes/makefuncs.h"
+ #include "optimizer/clauses.h"
+ #include "optimizer/var.h"
+ #include "parser/parsetree.h"
+ #include "utils/builtins.h"
+ #include "utils/lsyscache.h"
+
+ #include "pgsql_fdw.h"
+
+ /*
+ * Context for walk-through the expression tree.
+ */
+ typedef struct foreign_executable_cxt
+ {
+ PlannerInfo *root;
+ RelOptInfo *foreignrel;
+ } foreign_executable_cxt;
+
+ /*
+ * Deparse query representation into SQL statement which suits for remote
+ * PostgreSQL server.
+ */
+ char *
+ deparseSql(Oid relid,
+ PlannerInfo *root,
+ RelOptInfo *baserel,
+ ForeignTable *table)
+ {
+ StringInfoData foreign_relname;
+ StringInfoData sql; /* builder for SQL statement */
+ bool first;
+ AttrNumber attr;
+ List *attr_used = NIL; /* List of AttNumber used in the query */
+ ListCell *lc;
+ const char *nspname = NULL; /* plain namespace name */
+ const char *relname = NULL; /* plain relation name */
+ const char *q_nspname; /* quoted namespace name */
+ const char *q_relname; /* quoted relation name */
+ int i;
+ List *rtable = NIL;
+ List *context = NIL;
+
+ initStringInfo(&sql);
+ initStringInfo(&foreign_relname);
+
+ /*
+ * First of all, determine which column should be retrieved for this scan.
+ *
+ * We do this before deparsing SELECT clause because attributes which are
+ * not used in neither reltargetlist nor baserel->baserestrictinfo, quals
+ * evaluated on local, can be replaced with literal "NULL" in the SELECT
+ * clause to reduce overhead of tuple handling tuple and data transfer.
+ */
+ if (baserel->baserestrictinfo != NIL)
+ {
+ ListCell *lc;
+
+ foreach (lc, baserel->baserestrictinfo)
+ {
+ RestrictInfo *ri = (RestrictInfo *) lfirst(lc);
+ List *attrs;
+
+ /*
+ * We need to know which attributes are used in qual evaluated
+ * on the local server, because they should be listed in the
+ * SELECT clause of remote query. We can ignore attributes
+ * which are referenced only in ORDER BY/GROUP BY clause because
+ * such attributes has already been kept in reltargetlist.
+ */
+ attrs = pull_var_clause((Node *) ri->clause,
+ PVC_RECURSE_AGGREGATES,
+ PVC_RECURSE_PLACEHOLDERS);
+ attr_used = list_union(attr_used, attrs);
+ }
+ }
+
+ /*
+ * Determine foreign relation's qualified name. This is necessary for
+ * FROM clause and SELECT clause.
+ */
+ foreach(lc, table->options)
+ {
+ DefElem *def = (DefElem *) lfirst(lc);
+
+ if (strcmp(def->defname, "nspname") == 0)
+ nspname = defGetString(def);
+ else if (strcmp(def->defname, "relname") == 0)
+ relname = defGetString(def);
+ }
+
+ if (nspname == NULL)
+ nspname = get_namespace_name(get_rel_namespace(relid));
+ q_nspname = quote_identifier(nspname);
+
+ if (relname == NULL)
+ relname = get_rel_name(relid);
+ q_relname = quote_identifier(relname);
+
+ appendStringInfo(&foreign_relname, "%s.%s", q_nspname, q_relname);
+
+ /*
+ * We need to replace aliasname and colnames of the target relation so that
+ * constructed remote query is valid.
+ *
+ * Note that we skip first empty element of simple_rel_array. See also
+ * comments of simple_rel_array and simple_rte_array for the rationale.
+ */
+ for (i = 1; i < root->simple_rel_array_size; i++)
+ {
+ RangeTblEntry *rte = copyObject(root->simple_rte_array[i]);
+ List *newcolnames = NIL;
+
+ if (i == baserel->relid)
+ {
+ /*
+ * Create new list of column names which is used to deparse remote
+ * query from specified names or local column names. This list is
+ * used by deparse_expression.
+ */
+ for (attr = 1; attr <= baserel->max_attr; attr++)
+ {
+ char *colname = NULL;
+
+ /*
+ * Get colname option for each undropped attribute. If colname
+ * option is not supplied, use local column name in remote
+ * query.
+ */
+ if (get_rte_attribute_is_dropped(rte, attr))
+ colname = "";
+ else
+ {
+ List *options;
+ ListCell *lc;
+
+ options = GetForeignColumnOptions(relid, attr);
+ foreach(lc, options)
+ {
+ DefElem *def = (DefElem *) lfirst(lc);
+
+ if (strcmp(def->defname, "colname") == 0)
+ {
+ colname = defGetString(def);
+ break;
+ }
+ }
+ if (colname == NULL)
+ colname = strVal(list_nth(rte->eref->colnames,
+ attr - 1));
+ }
+ newcolnames = lappend(newcolnames, makeString(colname));
+ }
+ rte->alias = makeAlias(relname, newcolnames);
+ }
+ rtable = lappend(rtable, rte);
+ }
+ context = deparse_context_for_rtelist(rtable);
+
+ /*
+ * deparse SELECT clause
+ *
+ * List attributes which are in either target list or local restriction.
+ * Unused attributes are replaced with a literal "NULL" for optimization.
+ *
+ * Note that nothing is added for dropped columns, though tuple constructor
+ * function requires entries for dropped columns. Such entries must be
+ * initialized with NULL before calling tuple constructor.
+ */
+ appendStringInfo(&sql, "SELECT ");
+ attr_used = list_union(attr_used, baserel->reltargetlist);
+ first = true;
+ for (attr = 1; attr <= baserel->max_attr; attr++)
+ {
+ RangeTblEntry *rte = root->simple_rte_array[baserel->relid];
+ Var *var = NULL;
+ ListCell *lc;
+
+ /* Ignore dropped attributes. */
+ if (get_rte_attribute_is_dropped(rte, attr))
+ continue;
+
+ if (!first)
+ appendStringInfo(&sql, ", ");
+ first = false;
+
+ /*
+ * We use linear search here, but it wouldn't be problem since
+ * attr_used seems to not become so large.
+ */
+ foreach (lc, attr_used)
+ {
+ var = lfirst(lc);
+ if (var->varattno == attr)
+ break;
+ var = NULL;
+ }
+ if (var != NULL)
+ appendStringInfo(&sql, "%s",
+ deparse_expression((Node *) var, context, false, false));
+ else
+ appendStringInfo(&sql, "NULL");
+ }
+ appendStringInfoChar(&sql, ' ');
+
+ /*
+ * deparse FROM clause, including alias if any
+ */
+ appendStringInfo(&sql, "FROM %s", foreign_relname.data);
+
+ elog(DEBUG3, "Remote SQL: %s", sql.data);
+ return sql.data;
+ }
+
diff --git a/contrib/pgsql_fdw/expected/pgsql_fdw.out b/contrib/pgsql_fdw/expected/pgsql_fdw.out
index ...c1e2e82 .
*** a/contrib/pgsql_fdw/expected/pgsql_fdw.out
--- b/contrib/pgsql_fdw/expected/pgsql_fdw.out
***************
*** 0 ****
--- 1,592 ----
+ -- ===================================================================
+ -- create FDW objects
+ -- ===================================================================
+ -- Clean up in case a prior regression run failed
+ -- Suppress NOTICE messages when roles don't exist
+ SET client_min_messages TO 'error';
+ DROP ROLE IF EXISTS pgsql_fdw_user;
+ RESET client_min_messages;
+ CREATE ROLE pgsql_fdw_user LOGIN SUPERUSER;
+ SET SESSION AUTHORIZATION 'pgsql_fdw_user';
+ CREATE EXTENSION pgsql_fdw;
+ CREATE SERVER loopback1 FOREIGN DATA WRAPPER pgsql_fdw;
+ CREATE SERVER loopback2 FOREIGN DATA WRAPPER pgsql_fdw
+ OPTIONS (dbname 'contrib_regression');
+ CREATE USER MAPPING FOR public SERVER loopback1
+ OPTIONS (user 'value', password 'value');
+ CREATE USER MAPPING FOR pgsql_fdw_user SERVER loopback2;
+ CREATE FOREIGN TABLE ft1 (
+ c0 int,
+ c1 int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10)
+ ) SERVER loopback2;
+ ALTER FOREIGN TABLE ft1 DROP COLUMN c0;
+ CREATE FOREIGN TABLE ft2 (
+ c0 int,
+ c1 int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10)
+ ) SERVER loopback2;
+ ALTER FOREIGN TABLE ft2 DROP COLUMN c0;
+ -- ===================================================================
+ -- create objects used through FDW
+ -- ===================================================================
+ CREATE SCHEMA "S 1";
+ CREATE TABLE "S 1"."T 1" (
+ "C 1" int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10),
+ CONSTRAINT t1_pkey PRIMARY KEY ("C 1")
+ );
+ NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "t1_pkey" for table "T 1"
+ CREATE TABLE "S 1"."T 2" (
+ c1 int NOT NULL,
+ c2 text,
+ CONSTRAINT t2_pkey PRIMARY KEY (c1)
+ );
+ NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "t2_pkey" for table "T 2"
+ BEGIN;
+ TRUNCATE "S 1"."T 1";
+ INSERT INTO "S 1"."T 1"
+ SELECT id,
+ id % 10,
+ to_char(id, 'FM00000'),
+ '1970-01-01'::timestamptz + ((id % 100) || ' days')::interval,
+ '1970-01-01'::timestamp + ((id % 100) || ' days')::interval,
+ id % 10,
+ id % 10
+ FROM generate_series(1, 1000) id;
+ TRUNCATE "S 1"."T 2";
+ INSERT INTO "S 1"."T 2"
+ SELECT id,
+ 'AAA' || to_char(id, 'FM000')
+ FROM generate_series(1, 100) id;
+ COMMIT;
+ -- ===================================================================
+ -- tests for pgsql_fdw_validator
+ -- ===================================================================
+ ALTER FOREIGN DATA WRAPPER pgsql_fdw OPTIONS (host 'value'); -- ERROR
+ ERROR: invalid option "host"
+ HINT: Valid options in this context are:
+ -- requiressl, krbsrvname and gsslib are omitted because they depend on
+ -- configure option
+ ALTER SERVER loopback1 OPTIONS (
+ authtype 'value',
+ service 'value',
+ connect_timeout 'value',
+ dbname 'value',
+ host 'value',
+ hostaddr 'value',
+ port 'value',
+ --client_encoding 'value',
+ tty 'value',
+ options 'value',
+ application_name 'value',
+ --fallback_application_name 'value',
+ keepalives 'value',
+ keepalives_idle 'value',
+ keepalives_interval 'value',
+ -- requiressl 'value',
+ sslcompression 'value',
+ sslmode 'value',
+ sslcert 'value',
+ sslkey 'value',
+ sslrootcert 'value',
+ sslcrl 'value'
+ --requirepeer 'value',
+ -- krbsrvname 'value',
+ -- gsslib 'value',
+ --replication 'value'
+ );
+ ALTER SERVER loopback1 OPTIONS (user 'value'); -- ERROR
+ ERROR: invalid option "user"
+ HINT: Valid options in this context are: authtype, service, connect_timeout, dbname, host, hostaddr, port, tty, options, application_name, keepalives, keepalives_idle, keepalives_interval, keepalives_count, requiressl, sslcompression, sslmode, sslcert, sslkey, sslrootcert, sslcrl, requirepeer, krbsrvname, gsslib, fetch_count
+ ALTER SERVER loopback2 OPTIONS (ADD fetch_count '2');
+ ALTER USER MAPPING FOR public SERVER loopback1
+ OPTIONS (DROP user, DROP password);
+ ALTER USER MAPPING FOR public SERVER loopback1
+ OPTIONS (host 'value'); -- ERROR
+ ERROR: invalid option "host"
+ HINT: Valid options in this context are: user, password
+ ALTER FOREIGN TABLE ft1 OPTIONS (nspname 'S 1', relname 'T 1');
+ ALTER FOREIGN TABLE ft2 OPTIONS (nspname 'S 1', relname 'T 1', fetch_count '100');
+ ALTER FOREIGN TABLE ft1 OPTIONS (invalid 'value'); -- ERROR
+ ERROR: invalid option "invalid"
+ HINT: Valid options in this context are: nspname, relname, fetch_count
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count 'a'); -- ERROR
+ ERROR: invalid value for fetch_count: "a"
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count '0'); -- ERROR
+ ERROR: invalid value for fetch_count: "0"
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count '-1'); -- ERROR
+ ERROR: invalid value for fetch_count: "-1"
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (invalid 'value'); -- ERROR
+ ERROR: invalid option "invalid"
+ HINT: Valid options in this context are: colname
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (colname 'C 1');
+ ALTER FOREIGN TABLE ft2 ALTER COLUMN c1 OPTIONS (colname 'C 1');
+ \dew+
+ List of foreign-data wrappers
+ Name | Owner | Handler | Validator | Access privileges | FDW Options | Description
+ -----------+----------------+-------------------+---------------------+-------------------+-------------+-------------
+ pgsql_fdw | pgsql_fdw_user | pgsql_fdw_handler | pgsql_fdw_validator | | |
+ (1 row)
+
+ \des+
+ List of foreign servers
+ Name | Owner | Foreign-data wrapper | Access privileges | Type | Version | FDW Options | Description
+ -----------+----------------+----------------------+-------------------+------+---------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+-------------
+ loopback1 | pgsql_fdw_user | pgsql_fdw | | | | (authtype 'value', service 'value', connect_timeout 'value', dbname 'value', host 'value', hostaddr 'value', port 'value', tty 'value', options 'value', application_name 'value', keepalives 'value', keepalives_idle 'value', keepalives_interval 'value', sslcompression 'value', sslmode 'value', sslcert 'value', sslkey 'value', sslrootcert 'value', sslcrl 'value') |
+ loopback2 | pgsql_fdw_user | pgsql_fdw | | | | (dbname 'contrib_regression', fetch_count '2') |
+ (2 rows)
+
+ \deu+
+ List of user mappings
+ Server | User name | FDW Options
+ -----------+----------------+-------------
+ loopback1 | public |
+ loopback2 | pgsql_fdw_user |
+ (2 rows)
+
+ \det+
+ List of foreign tables
+ Schema | Table | Server | FDW Options | Description
+ --------+-------+-----------+---------------------------------------------------+-------------
+ public | ft1 | loopback2 | (nspname 'S 1', relname 'T 1') |
+ public | ft2 | loopback2 | (nspname 'S 1', relname 'T 1', fetch_count '100') |
+ (2 rows)
+
+ -- ===================================================================
+ -- simple queries
+ -- ===================================================================
+ -- single table, with/without alias
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
+ QUERY PLAN
+ ------------------------------------------------------------------------------------------------------------------------------
+ Limit
+ Output: c1, c2, c3, c4, c5, c6, c7
+ -> Sort
+ Output: c1, c2, c3, c4, c5, c6, c7
+ Sort Key: ft1.c3, ft1.c1
+ -> Foreign Scan on public.ft1
+ Output: c1, c2, c3, c4, c5, c6, c7
+ Remote SQL: DECLARE pgsql_fdw_cursor_0 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (8 rows)
+
+ SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ -----+----+-------+------------------------------+--------------------------+----+------------
+ 101 | 1 | 00101 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970 | 1 | 1
+ 102 | 2 | 00102 | Sat Jan 03 00:00:00 1970 PST | Sat Jan 03 00:00:00 1970 | 2 | 2
+ 103 | 3 | 00103 | Sun Jan 04 00:00:00 1970 PST | Sun Jan 04 00:00:00 1970 | 3 | 3
+ 104 | 4 | 00104 | Mon Jan 05 00:00:00 1970 PST | Mon Jan 05 00:00:00 1970 | 4 | 4
+ 105 | 5 | 00105 | Tue Jan 06 00:00:00 1970 PST | Tue Jan 06 00:00:00 1970 | 5 | 5
+ 106 | 6 | 00106 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6
+ 107 | 7 | 00107 | Thu Jan 08 00:00:00 1970 PST | Thu Jan 08 00:00:00 1970 | 7 | 7
+ 108 | 8 | 00108 | Fri Jan 09 00:00:00 1970 PST | Fri Jan 09 00:00:00 1970 | 8 | 8
+ 109 | 9 | 00109 | Sat Jan 10 00:00:00 1970 PST | Sat Jan 10 00:00:00 1970 | 9 | 9
+ 110 | 0 | 00110 | Sun Jan 11 00:00:00 1970 PST | Sun Jan 11 00:00:00 1970 | 0 | 0
+ (10 rows)
+
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+ QUERY PLAN
+ ------------------------------------------------------------------------------------------------------------------------------
+ Limit
+ Output: c1, c2, c3, c4, c5, c6, c7
+ -> Sort
+ Output: c1, c2, c3, c4, c5, c6, c7
+ Sort Key: t1.c3, t1.c1
+ -> Foreign Scan on public.ft1 t1
+ Output: c1, c2, c3, c4, c5, c6, c7
+ Remote SQL: DECLARE pgsql_fdw_cursor_2 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (8 rows)
+
+ SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ -----+----+-------+------------------------------+--------------------------+----+------------
+ 101 | 1 | 00101 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970 | 1 | 1
+ 102 | 2 | 00102 | Sat Jan 03 00:00:00 1970 PST | Sat Jan 03 00:00:00 1970 | 2 | 2
+ 103 | 3 | 00103 | Sun Jan 04 00:00:00 1970 PST | Sun Jan 04 00:00:00 1970 | 3 | 3
+ 104 | 4 | 00104 | Mon Jan 05 00:00:00 1970 PST | Mon Jan 05 00:00:00 1970 | 4 | 4
+ 105 | 5 | 00105 | Tue Jan 06 00:00:00 1970 PST | Tue Jan 06 00:00:00 1970 | 5 | 5
+ 106 | 6 | 00106 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6
+ 107 | 7 | 00107 | Thu Jan 08 00:00:00 1970 PST | Thu Jan 08 00:00:00 1970 | 7 | 7
+ 108 | 8 | 00108 | Fri Jan 09 00:00:00 1970 PST | Fri Jan 09 00:00:00 1970 | 8 | 8
+ 109 | 9 | 00109 | Sat Jan 10 00:00:00 1970 PST | Sat Jan 10 00:00:00 1970 | 9 | 9
+ 110 | 0 | 00110 | Sun Jan 11 00:00:00 1970 PST | Sun Jan 11 00:00:00 1970 | 0 | 0
+ (10 rows)
+
+ -- with WHERE clause
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
+ QUERY PLAN
+ ------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on public.ft1 t1
+ Output: c1, c2, c3, c4, c5, c6, c7
+ Filter: ((t1.c1 = 101) AND ((t1.c6)::text = '1'::text) AND (t1.c7 = '1'::bpchar))
+ Remote SQL: DECLARE pgsql_fdw_cursor_4 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (4 rows)
+
+ SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ -----+----+-------+------------------------------+--------------------------+----+------------
+ 101 | 1 | 00101 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970 | 1 | 1
+ (1 row)
+
+ -- aggregate
+ SELECT COUNT(*) FROM ft1 t1;
+ count
+ -------
+ 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
+ ----+----+-------+------------------------------+--------------------------+----+------------
+ 1 | 1 | 00001 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970 | 1 | 1
+ 2 | 2 | 00002 | Sat Jan 03 00:00:00 1970 PST | Sat Jan 03 00:00:00 1970 | 2 | 2
+ 3 | 3 | 00003 | Sun Jan 04 00:00:00 1970 PST | Sun Jan 04 00:00:00 1970 | 3 | 3
+ 4 | 4 | 00004 | Mon Jan 05 00:00:00 1970 PST | Mon Jan 05 00:00:00 1970 | 4 | 4
+ 5 | 5 | 00005 | Tue Jan 06 00:00:00 1970 PST | Tue Jan 06 00:00:00 1970 | 5 | 5
+ 6 | 6 | 00006 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6
+ 7 | 7 | 00007 | Thu Jan 08 00:00:00 1970 PST | Thu Jan 08 00:00:00 1970 | 7 | 7
+ 8 | 8 | 00008 | Fri Jan 09 00:00:00 1970 PST | Fri Jan 09 00:00:00 1970 | 8 | 8
+ 9 | 9 | 00009 | Sat Jan 10 00:00:00 1970 PST | Sat Jan 10 00:00:00 1970 | 9 | 9
+ 10 | 0 | 00010 | Sun Jan 11 00:00:00 1970 PST | Sun Jan 11 00:00:00 1970 | 0 | 0
+ (10 rows)
+
+ -- subquery+MAX
+ SELECT * FROM ft1 t1 WHERE t1.c3 = (SELECT MAX(c3) FROM ft2 t2) ORDER BY c1;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ ------+----+-------+------------------------------+--------------------------+----+------------
+ 1000 | 0 | 01000 | Thu Jan 01 00:00:00 1970 PST | Thu Jan 01 00:00:00 1970 | 0 | 0
+ (1 row)
+
+ -- used in CTE
+ WITH t1 AS (SELECT * FROM ft1 WHERE c1 <= 10) SELECT t2.c1, t2.c2, t2.c3, t2.c4 FROM t1, ft2 t2 WHERE t1.c1 = t2.c1 ORDER BY t1.c1;
+ c1 | c2 | c3 | c4
+ ----+----+-------+------------------------------
+ 1 | 1 | 00001 | Fri Jan 02 00:00:00 1970 PST
+ 2 | 2 | 00002 | Sat Jan 03 00:00:00 1970 PST
+ 3 | 3 | 00003 | Sun Jan 04 00:00:00 1970 PST
+ 4 | 4 | 00004 | Mon Jan 05 00:00:00 1970 PST
+ 5 | 5 | 00005 | Tue Jan 06 00:00:00 1970 PST
+ 6 | 6 | 00006 | Wed Jan 07 00:00:00 1970 PST
+ 7 | 7 | 00007 | Thu Jan 08 00:00:00 1970 PST
+ 8 | 8 | 00008 | Fri Jan 09 00:00:00 1970 PST
+ 9 | 9 | 00009 | Sat Jan 10 00:00:00 1970 PST
+ 10 | 0 | 00010 | Sun Jan 11 00:00:00 1970 PST
+ (10 rows)
+
+ -- fixed values
+ SELECT 'fixed', NULL FROM ft1 t1 WHERE c1 = 1;
+ ?column? | ?column?
+ ----------+----------
+ fixed |
+ (1 row)
+
+ -- user-defined operator/function
+ CREATE FUNCTION pgsql_fdw_abs(int) RETURNS int AS $$
+ BEGIN
+ RETURN abs($1);
+ END
+ $$ LANGUAGE plpgsql IMMUTABLE;
+ CREATE OPERATOR === (
+ LEFTARG = int,
+ RIGHTARG = int,
+ PROCEDURE = int4eq,
+ COMMUTATOR = ===,
+ NEGATOR = !==
+ );
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = pgsql_fdw_abs(t1.c2);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = pgsql_fdw_abs(c2))
+ Remote SQL: DECLARE pgsql_fdw_cursor_18 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 === t1.c2;
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 === c2)
+ Remote SQL: DECLARE pgsql_fdw_cursor_19 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = abs(t1.c2);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = abs(c2))
+ Remote SQL: DECLARE pgsql_fdw_cursor_20 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = t1.c2;
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = c2)
+ Remote SQL: DECLARE pgsql_fdw_cursor_21 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ -- ===================================================================
+ -- parameterized queries
+ -- ===================================================================
+ -- simple join
+ PREPARE st1(int, int) AS SELECT t1.c3, t2.c3 FROM ft1 t1, ft2 t2 WHERE t1.c1 = $1 AND t2.c1 = $2;
+ EXPLAIN (COSTS false) EXECUTE st1(1, 2);
+ QUERY PLAN
+ -----------------------------------------------------------------------------------------------------------------------------------------
+ Nested Loop
+ -> Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_22 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, NULL, NULL, NULL, NULL FROM "S 1"."T 1"
+ -> Materialize
+ -> Foreign Scan on ft2 t2
+ Filter: (c1 = 2)
+ Remote SQL: DECLARE pgsql_fdw_cursor_23 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, NULL, NULL, NULL, NULL FROM "S 1"."T 1"
+ (8 rows)
+
+ EXECUTE st1(1, 1);
+ c3 | c3
+ -------+-------
+ 00001 | 00001
+ (1 row)
+
+ EXECUTE st1(101, 101);
+ c3 | c3
+ -------+-------
+ 00101 | 00101
+ (1 row)
+
+ -- subquery using stable function (can't be pushed down)
+ PREPARE st2(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c4) = 6) ORDER BY c1;
+ EXPLAIN (COSTS false) EXECUTE st2(10, 20);
+ QUERY PLAN
+ ---------------------------------------------------------------------------------------------------------------------------------------------------
+ Sort
+ Sort Key: t1.c1
+ -> Hash Join
+ Hash Cond: (t1.c3 = t2.c3)
+ -> Foreign Scan on ft1 t1
+ Filter: (c1 < 20)
+ Remote SQL: DECLARE pgsql_fdw_cursor_28 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ -> Hash
+ -> HashAggregate
+ -> Foreign Scan on ft2 t2
+ Filter: ((c1 > 10) AND (date_part('dow'::text, c4) = 6::double precision))
+ Remote SQL: DECLARE pgsql_fdw_cursor_29 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, c4, NULL, NULL, NULL FROM "S 1"."T 1"
+ (12 rows)
+
+ EXECUTE st2(10, 20);
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ ----+----+-------+------------------------------+--------------------------+----+------------
+ 16 | 6 | 00016 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970 | 6 | 6
+ (1 row)
+
+ EXECUTE st1(101, 101);
+ c3 | c3
+ -------+-------
+ 00101 | 00101
+ (1 row)
+
+ -- subquery using immutable function (can be pushed down)
+ PREPARE st3(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c5) = 6) ORDER BY c1;
+ EXPLAIN (COSTS false) EXECUTE st3(10, 20);
+ QUERY PLAN
+ ---------------------------------------------------------------------------------------------------------------------------------------------------
+ Sort
+ Sort Key: t1.c1
+ -> Hash Join
+ Hash Cond: (t1.c3 = t2.c3)
+ -> Foreign Scan on ft1 t1
+ Filter: (c1 < 20)
+ Remote SQL: DECLARE pgsql_fdw_cursor_34 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ -> Hash
+ -> HashAggregate
+ -> Foreign Scan on ft2 t2
+ Filter: ((c1 > 10) AND (date_part('dow'::text, c5) = 6::double precision))
+ Remote SQL: DECLARE pgsql_fdw_cursor_35 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, NULL, c5, NULL, NULL FROM "S 1"."T 1"
+ (12 rows)
+
+ EXECUTE st3(10, 20);
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ ----+----+-------+------------------------------+--------------------------+----+------------
+ 16 | 6 | 00016 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970 | 6 | 6
+ (1 row)
+
+ EXECUTE st3(20, 30);
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ ----+----+-------+------------------------------+--------------------------+----+------------
+ 23 | 3 | 00023 | Sat Jan 24 00:00:00 1970 PST | Sat Jan 24 00:00:00 1970 | 3 | 3
+ (1 row)
+
+ -- custom plan should be chosen
+ PREPARE st4(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 = $1;
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_40 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_41 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_42 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_43 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_44 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = $1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_45 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ -- cleanup
+ DEALLOCATE st1;
+ DEALLOCATE st2;
+ DEALLOCATE st3;
+ DEALLOCATE st4;
+ -- ===================================================================
+ -- connection management
+ -- ===================================================================
+ SELECT srvname, usename FROM pgsql_fdw_connections;
+ srvname | usename
+ -----------+----------------
+ loopback2 | pgsql_fdw_user
+ (1 row)
+
+ SELECT pgsql_fdw_disconnect(srvid, usesysid) FROM pgsql_fdw_get_connections();
+ pgsql_fdw_disconnect
+ ----------------------
+ OK
+ (1 row)
+
+ SELECT srvname, usename FROM pgsql_fdw_connections;
+ srvname | usename
+ ---------+---------
+ (0 rows)
+
+ -- ===================================================================
+ -- subtransaction
+ -- ===================================================================
+ BEGIN;
+ DECLARE c CURSOR FOR SELECT * FROM ft1 ORDER BY c1;
+ FETCH c;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ ----+----+-------+------------------------------+--------------------------+----+------------
+ 1 | 1 | 00001 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970 | 1 | 1
+ (1 row)
+
+ SAVEPOINT s;
+ ERROR OUT; -- ERROR
+ ERROR: syntax error at or near "ERROR"
+ LINE 1: ERROR OUT;
+ ^
+ ROLLBACK TO s;
+ SELECT srvname FROM pgsql_fdw_connections;
+ srvname
+ -----------
+ loopback2
+ (1 row)
+
+ FETCH c;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ ----+----+-------+------------------------------+--------------------------+----+------------
+ 2 | 2 | 00002 | Sat Jan 03 00:00:00 1970 PST | Sat Jan 03 00:00:00 1970 | 2 | 2
+ (1 row)
+
+ COMMIT;
+ SELECT srvname FROM pgsql_fdw_connections;
+ srvname
+ -----------
+ loopback2
+ (1 row)
+
+ ERROR OUT; -- ERROR
+ ERROR: syntax error at or near "ERROR"
+ LINE 1: ERROR OUT;
+ ^
+ SELECT srvname FROM pgsql_fdw_connections;
+ srvname
+ ---------
+ (0 rows)
+
+ -- ===================================================================
+ -- cleanup
+ -- ===================================================================
+ DROP OPERATOR === (int, int) CASCADE;
+ DROP OPERATOR !== (int, int) CASCADE;
+ DROP FUNCTION pgsql_fdw_abs(int);
+ DROP SCHEMA "S 1" CASCADE;
+ NOTICE: drop cascades to 2 other objects
+ DETAIL: drop cascades to table "S 1"."T 1"
+ drop cascades to table "S 1"."T 2"
+ DROP EXTENSION pgsql_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 server loopback2
+ drop cascades to user mapping for pgsql_fdw_user
+ drop cascades to foreign table ft1
+ drop cascades to foreign table ft2
+ \c
+ DROP ROLE pgsql_fdw_user;
diff --git a/contrib/pgsql_fdw/option.c b/contrib/pgsql_fdw/option.c
index ...a2a8927 .
*** a/contrib/pgsql_fdw/option.c
--- b/contrib/pgsql_fdw/option.c
***************
*** 0 ****
--- 1,242 ----
+ /*-------------------------------------------------------------------------
+ *
+ * option.c
+ * FDW option handling
+ *
+ * Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/option.c
+ *
+ *-------------------------------------------------------------------------
+ */
+ #include "postgres.h"
+
+ #include "access/reloptions.h"
+ #include "catalog/pg_foreign_data_wrapper.h"
+ #include "catalog/pg_foreign_server.h"
+ #include "catalog/pg_foreign_table.h"
+ #include "catalog/pg_user_mapping.h"
+ #include "commands/defrem.h"
+ #include "fmgr.h"
+ #include "foreign/foreign.h"
+ #include "lib/stringinfo.h"
+ #include "miscadmin.h"
+
+ #include "pgsql_fdw.h"
+
+ /*
+ * SQL functions
+ */
+ extern Datum pgsql_fdw_validator(PG_FUNCTION_ARGS);
+ PG_FUNCTION_INFO_V1(pgsql_fdw_validator);
+
+ /*
+ * Describes the valid options for objects that use this wrapper.
+ */
+ typedef struct PgsqlFdwOption
+ {
+ const char *optname;
+ Oid optcontext; /* Oid of catalog in which options may appear */
+ bool is_libpq_opt; /* true if it's used in libpq */
+ } PgsqlFdwOption;
+
+ /*
+ * Valid options for pgsql_fdw.
+ */
+ static PgsqlFdwOption valid_options[] = {
+
+ /*
+ * Options for libpq connection.
+ * Note: This list should be updated along with PQconninfoOptions in
+ * interfaces/libpq/fe-connect.c, so the order is kept as is.
+ *
+ * Some useless libpq connection options are not accepted by pgsql_fdw:
+ * client_encoding: set to local database encoding automatically
+ * fallback_application_name: fixed to "pgsql_fdw"
+ * replication: pgsql_fdw never be replication client
+ */
+ {"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},
+ #ifdef NOT_USED
+ {"client_encoding", ForeignServerRelationId, true},
+ #endif
+ {"tty", ForeignServerRelationId, true},
+ {"options", ForeignServerRelationId, true},
+ {"application_name", ForeignServerRelationId, true},
+ #ifdef NOT_USED
+ {"fallback_application_name", ForeignServerRelationId, true},
+ #endif
+ {"keepalives", ForeignServerRelationId, true},
+ {"keepalives_idle", ForeignServerRelationId, true},
+ {"keepalives_interval", ForeignServerRelationId, true},
+ {"keepalives_count", ForeignServerRelationId, true},
+ {"requiressl", ForeignServerRelationId, true},
+ {"sslcompression", ForeignServerRelationId, true},
+ {"sslmode", ForeignServerRelationId, true},
+ {"sslcert", ForeignServerRelationId, true},
+ {"sslkey", ForeignServerRelationId, true},
+ {"sslrootcert", ForeignServerRelationId, true},
+ {"sslcrl", ForeignServerRelationId, true},
+ {"requirepeer", ForeignServerRelationId, true},
+ {"krbsrvname", ForeignServerRelationId, true},
+ {"gsslib", ForeignServerRelationId, true},
+ #ifdef NOT_USED
+ {"replication", ForeignServerRelationId, true},
+ #endif
+
+ /*
+ * Options for translation of object names.
+ */
+ {"nspname", ForeignTableRelationId, false},
+ {"relname", ForeignTableRelationId, false},
+ {"colname", AttributeRelationId, false},
+
+ /*
+ * Options for cursor behavior.
+ * These options can be overridden by finer-grained objects.
+ */
+ {"fetch_count", ForeignTableRelationId, false},
+ {"fetch_count", ForeignServerRelationId, false},
+
+ /* Terminating entry --- MUST BE LAST */
+ {NULL, InvalidOid, false}
+ };
+
+ /*
+ * Helper functions
+ */
+ static bool is_valid_option(const char *optname, Oid context);
+
+ /*
+ * Validate the generic options given to a FOREIGN DATA WRAPPER, SERVER,
+ * USER MAPPING or FOREIGN TABLE that uses pgsql_fdw.
+ *
+ * Raise an ERROR if the option or its value is considered invalid.
+ */
+ Datum
+ pgsql_fdw_validator(PG_FUNCTION_ARGS)
+ {
+ List *options_list = untransformRelOptions(PG_GETARG_DATUM(0));
+ Oid catalog = PG_GETARG_OID(1);
+ ListCell *cell;
+
+ /*
+ * Check that only options supported by pgsql_fdw, and allowed for the
+ * current object type, are given.
+ */
+ foreach(cell, options_list)
+ {
+ DefElem *def = (DefElem *) lfirst(cell);
+
+ if (!is_valid_option(def->defname, catalog))
+ {
+ PgsqlFdwOption *opt;
+ StringInfoData buf;
+
+ /*
+ * Unknown option specified, complain about it. Provide a hint
+ * with list of valid options for the object.
+ */
+ initStringInfo(&buf);
+ for (opt = valid_options; opt->optname; opt++)
+ {
+ if (catalog == opt->optcontext)
+ appendStringInfo(&buf, "%s%s", (buf.len > 0) ? ", " : "",
+ opt->optname);
+ }
+
+ ereport(ERROR,
+ (errcode(ERRCODE_FDW_INVALID_OPTION_NAME),
+ errmsg("invalid option \"%s\"", def->defname),
+ errhint("Valid options in this context are: %s",
+ buf.data)));
+ }
+
+ /* fetch_count be positive digit number. */
+ if (strcmp(def->defname, "fetch_count") == 0)
+ {
+ long value;
+ char *p = NULL;
+
+ value = strtol(defGetString(def), &p, 10);
+ if (*p != '\0' || value < 1)
+ ereport(ERROR,
+ (errcode(ERRCODE_FDW_INVALID_ATTRIBUTE_VALUE),
+ errmsg("invalid value for %s: \"%s\"",
+ def->defname, defGetString(def))));
+ }
+ }
+
+ /*
+ * We don't care option-specific limitation here; they will be validated at
+ * the execution time.
+ */
+
+ PG_RETURN_VOID();
+ }
+
+ /*
+ * Check whether the given option is one of the valid pgsql_fdw options.
+ * context is the Oid of the catalog holding the object the option is for.
+ */
+ static bool
+ is_valid_option(const char *optname, Oid context)
+ {
+ PgsqlFdwOption *opt;
+
+ for (opt = valid_options; opt->optname; opt++)
+ {
+ if (context == opt->optcontext && strcmp(opt->optname, optname) == 0)
+ return true;
+ }
+ return false;
+ }
+
+ /*
+ * Check whether the given option is one of the valid libpq options.
+ * context is the Oid of the catalog holding the object the option is for.
+ */
+ static bool
+ is_libpq_option(const char *optname)
+ {
+ PgsqlFdwOption *opt;
+
+ for (opt = valid_options; opt->optname; opt++)
+ {
+ if (strcmp(opt->optname, optname) == 0 && opt->is_libpq_opt)
+ return true;
+ }
+ return false;
+ }
+
+ /*
+ * Generate key-value arrays which includes only libpq options from the list
+ * which contains any kind of options.
+ */
+ int
+ ExtractConnectionOptions(List *defelems, const char **keywords, const char **values)
+ {
+ ListCell *lc;
+ int i;
+
+ i = 0;
+ foreach(lc, defelems)
+ {
+ DefElem *d = (DefElem *) lfirst(lc);
+ if (is_libpq_option(d->defname))
+ {
+ keywords[i] = d->defname;
+ values[i] = defGetString(d);
+ i++;
+ }
+ }
+ return i;
+ }
diff --git a/contrib/pgsql_fdw/pgsql_fdw--1.0.sql b/contrib/pgsql_fdw/pgsql_fdw--1.0.sql
index ...b0ea2b2 .
*** a/contrib/pgsql_fdw/pgsql_fdw--1.0.sql
--- b/contrib/pgsql_fdw/pgsql_fdw--1.0.sql
***************
*** 0 ****
--- 1,39 ----
+ /* contrib/pgsql_fdw/pgsql_fdw--1.0.sql */
+
+ -- complain if script is sourced in psql, rather than via CREATE EXTENSION
+ \echo Use "CREATE EXTENSION pgsql_fdw" to load this file. \quit
+
+ CREATE FUNCTION pgsql_fdw_handler()
+ RETURNS fdw_handler
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C STRICT;
+
+ CREATE FUNCTION pgsql_fdw_validator(text[], oid)
+ RETURNS void
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C STRICT;
+
+ CREATE FOREIGN DATA WRAPPER pgsql_fdw
+ HANDLER pgsql_fdw_handler
+ VALIDATOR pgsql_fdw_validator;
+
+ /* connection management functions and view */
+ CREATE FUNCTION pgsql_fdw_get_connections(out srvid oid, out usesysid oid)
+ RETURNS SETOF record
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C STRICT;
+
+ CREATE FUNCTION pgsql_fdw_disconnect(oid, oid)
+ RETURNS text
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C STRICT;
+
+ CREATE VIEW pgsql_fdw_connections AS
+ SELECT c.srvid srvid,
+ s.srvname srvname,
+ c.usesysid usesysid,
+ pg_get_userbyid(c.usesysid) usename
+ FROM pgsql_fdw_get_connections() c
+ JOIN pg_catalog.pg_foreign_server s ON (s.oid = c.srvid);
+ GRANT SELECT ON pgsql_fdw_connections TO public;
+
diff --git a/contrib/pgsql_fdw/pgsql_fdw.c b/contrib/pgsql_fdw/pgsql_fdw.c
index ...7ac37f8 .
*** a/contrib/pgsql_fdw/pgsql_fdw.c
--- b/contrib/pgsql_fdw/pgsql_fdw.c
***************
*** 0 ****
--- 1,879 ----
+ /*-------------------------------------------------------------------------
+ *
+ * pgsql_fdw.c
+ * foreign-data wrapper for remote PostgreSQL servers.
+ *
+ * Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/pgsql_fdw.c
+ *
+ *-------------------------------------------------------------------------
+ */
+ #include "postgres.h"
+ #include "fmgr.h"
+
+ #include "catalog/pg_foreign_server.h"
+ #include "catalog/pg_foreign_table.h"
+ #include "commands/defrem.h"
+ #include "commands/explain.h"
+ #include "foreign/fdwapi.h"
+ #include "funcapi.h"
+ #include "miscadmin.h"
+ #include "nodes/nodeFuncs.h"
+ #include "optimizer/cost.h"
+ #include "optimizer/pathnode.h"
+ #include "utils/lsyscache.h"
+ #include "utils/memutils.h"
+ #include "utils/rel.h"
+
+ #include "pgsql_fdw.h"
+ #include "connection.h"
+
+ PG_MODULE_MAGIC;
+
+ /*
+ * Default fetch count for cursor. This can be overridden by fetch_count FDW
+ * option.
+ */
+ #define DEFAULT_FETCH_COUNT 10000
+
+ /*
+ * Cost to establish a connection.
+ * XXX: should be configurable per server?
+ */
+ #define CONNECTION_COSTS 100.0
+
+ /*
+ * Cost to transfer 1 byte from remote server.
+ * XXX: should be configurable per server?
+ */
+ #define TRANSFER_COSTS_PER_BYTE 0.001
+
+ /*
+ * Cursors which are used together in a local query require different name, so
+ * we use simple incremental name for that purpose. We don't care wrap around
+ * of cursor_id because it's hard to imagine that 2^32 cursors are used in a
+ * query.
+ */
+ #define CURSOR_NAME_FORMAT "pgsql_fdw_cursor_%u"
+ static uint32 cursor_id = 0;
+
+ /*
+ * Index of FDW-private information, such as remote SQL statements, which are
+ * stored in fdw_private list.
+ */
+ enum FdwPrivateIndex {
+ FdwPrivateSelectSql,
+ FdwPrivateDeclareSql,
+ FdwPrivateFetchSql,
+ FdwPrivateResetSql,
+ FdwPrivateCloseSql,
+
+ /* # of elements stored in the list fdw_private */
+ FdwPrivateNum,
+ };
+
+ /*
+ * Describes an execution state of a foreign scan against a foreign table
+ * using pgsql_fdw.
+ */
+ typedef struct PgsqlFdwExecutionState
+ {
+ MemoryContext scan_cxt; /* context for per-scan lifespan data */
+
+ List *fdw_private; /* FDW-private information passed to executor */
+ PGconn *conn; /* connection for the scan */
+
+ Oid *param_types; /* type array of external parameter */
+ const char **param_values; /* value array of external parameter */
+
+ int attnum; /* # of non-dropped attribute */
+ char **col_values; /* column value buffer */
+ AttInMetadata *attinmeta; /* attribute metadata */
+
+ Tuplestorestate *tuples; /* result of the scan */
+ } PgsqlFdwExecutionState;
+
+ /*
+ * SQL functions
+ */
+ extern Datum pgsql_fdw_handler(PG_FUNCTION_ARGS);
+ PG_FUNCTION_INFO_V1(pgsql_fdw_handler);
+
+ /*
+ * FDW callback routines
+ */
+ static void pgsqlPlanForeignScan(Oid foreigntableid,
+ PlannerInfo *root,
+ RelOptInfo *baserel);
+ static void pgsqlExplainForeignScan(ForeignScanState *node, ExplainState *es);
+ static void pgsqlBeginForeignScan(ForeignScanState *node, int eflags);
+ static TupleTableSlot *pgsqlIterateForeignScan(ForeignScanState *node);
+ static void pgsqlReScanForeignScan(ForeignScanState *node);
+ static void pgsqlEndForeignScan(ForeignScanState *node);
+
+ /*
+ * Helper functions
+ */
+ static void estimate_costs(PlannerInfo *root,
+ RelOptInfo *baserel,
+ const char *sql,
+ Oid serverid,
+ Cost *startup_cost,
+ Cost *total_cost);
+ static void execute_query(ForeignScanState *node);
+ static PGresult *fetch_result(ForeignScanState *node);
+ static void store_result(ForeignScanState *node, PGresult *res);
+
+ /* Exported functions, but not written in pgsql_fdw.h. */
+ void _PG_init(void);
+ void _PG_fini(void);
+
+ /*
+ * Module-specific initialization.
+ */
+ void
+ _PG_init(void)
+ {
+ }
+
+ /*
+ * Module-specific clean up.
+ */
+ void
+ _PG_fini(void)
+ {
+ }
+
+ /*
+ * The content of FdwRoutine of pgsql_fdw is never changed, so we use one
+ * static object for all scan.
+ */
+ static FdwRoutine fdwroutine = {
+
+ /* Node type */
+ T_FdwRoutine,
+
+ /* Required handler functions. */
+ pgsqlPlanForeignScan,
+ pgsqlExplainForeignScan,
+ pgsqlBeginForeignScan,
+ pgsqlIterateForeignScan,
+ pgsqlReScanForeignScan,
+ pgsqlEndForeignScan,
+
+ /* Optional handler functions. */
+ };
+
+ /*
+ * Foreign-data wrapper handler function: return a struct with pointers
+ * to my callback routines.
+ */
+ Datum
+ pgsql_fdw_handler(PG_FUNCTION_ARGS)
+ {
+ PG_RETURN_POINTER(&fdwroutine);
+ }
+
+ /*
+ * pgsqlPlanForeignScan
+ * Create possible scan paths for a scan on the foreign table
+ */
+ static void
+ pgsqlPlanForeignScan(Oid foreigntableid,
+ PlannerInfo *root,
+ RelOptInfo *baserel)
+ {
+ ListCell *lc;
+ Cost startup_cost;
+ Cost total_cost;
+ char name[128]; /* must be larger than format + 10 */
+ StringInfoData cursor;
+ DefElem *def;
+ int fetch_count = DEFAULT_FETCH_COUNT;
+ char *sql;
+ List *fdw_private = NIL;
+ ForeignTable *table;
+ ForeignServer *server;
+
+ /*
+ * Generate remote query string, and estimate cost of this scan.
+ */
+ table = GetForeignTable(foreigntableid);
+ server = GetForeignServer(table->serverid);
+ sql = deparseSql(foreigntableid, root, baserel, table);
+ estimate_costs(root, baserel, sql, server->serverid,
+ &startup_cost, &total_cost);
+
+ /*
+ * Store plain SELECT statement in private area of FdwPlan. This will be
+ * used for executing remote query and explaining scan.
+ */
+ fdw_private = list_make1(makeString(sql));
+
+ /*
+ * Use specified fetch_count instead of default value, if any. Foreign
+ * table option overrides server option.
+ */
+ foreach(lc, table->options)
+ {
+ def = (DefElem *) lfirst(lc);
+ if (strcmp(def->defname, "fetch_count") == 0)
+ break;
+
+ }
+ if (lc == NULL)
+ {
+ foreach(lc, server->options)
+ {
+ def = (DefElem *) lfirst(lc);
+ if (strcmp(def->defname, "fetch_count") == 0)
+ break;
+
+ }
+ }
+ if (lc != NULL)
+ fetch_count = strtol(defGetString(def), NULL, 10);
+ elog(DEBUG1, "relid=%u fetch_count=%d", foreigntableid, fetch_count);
+
+ /*
+ * We store some more information in FdwPlan to pass them beyond the
+ * boundary between planner and executor. Finally FdwPlan using cursor
+ * would hold items below:
+ *
+ * 1) plain SELECT statement (already added above)
+ * 2) SQL statement used to declare cursor
+ * 3) SQL statement used to fetch rows from cursor
+ * 4) SQL statement used to reset cursor
+ * 5) SQL statement used to close cursor
+ *
+ * These items are indexed with the enum FdwPrivateIndex, so an item
+ * can be accessed directly via list_nth(). For example of FETCH
+ * statement:
+ * list_nth(fdw_private, FdwPrivateFetchSql)
+ */
+
+ /* Construct cursor name from sequential value */
+ sprintf(name, CURSOR_NAME_FORMAT, cursor_id++);
+
+ /* Construct statement to declare cursor */
+ initStringInfo(&cursor);
+ appendStringInfo(&cursor, "DECLARE %s SCROLL CURSOR FOR %s", name, sql);
+ fdw_private = lappend(fdw_private, makeString(cursor.data));
+
+ /* Construct statement to fetch rows from cursor */
+ initStringInfo(&cursor);
+ appendStringInfo(&cursor, "FETCH %d FROM %s", fetch_count, name);
+ fdw_private = lappend(fdw_private, makeString(cursor.data));
+
+ /* Construct statement to reset cursor */
+ initStringInfo(&cursor);
+ appendStringInfo(&cursor, "MOVE ABSOLUTE 0 FROM %s", name);
+ fdw_private = lappend(fdw_private, makeString(cursor.data));
+
+ /* Construct statement to close cursor */
+ initStringInfo(&cursor);
+ appendStringInfo(&cursor, "CLOSE %s", name);
+ fdw_private = lappend(fdw_private, makeString(cursor.data));
+
+ /* Create ForeignScan path node for this scan, and add it to baserel. */
+ add_path(baserel, (Path *)
+ create_foreignscan_path(root, baserel,
+ baserel->rows,
+ startup_cost,
+ total_cost,
+ NIL, /* no pathkeys */
+ NULL, /* no outer rel eigher */
+ NIL, /* no param clause */
+ fdw_private)); /* store remote SQL */
+ }
+
+ /*
+ * pgsqlExplainForeignScan
+ * Produce extra output for EXPLAIN
+ */
+ static void
+ pgsqlExplainForeignScan(ForeignScanState *node, ExplainState *es)
+ {
+ List *fdw_private;
+ char *sql;
+
+ fdw_private = ((ForeignScan *) node->ss.ps.plan)->fdw_private;
+ sql = strVal(list_nth(fdw_private, FdwPrivateDeclareSql));
+ ExplainPropertyText("Remote SQL", sql, es);
+ }
+
+ /*
+ * pgsqlBeginForeignScan
+ * Initiate access to a foreign PostgreSQL table.
+ */
+ static void
+ pgsqlBeginForeignScan(ForeignScanState *node, int eflags)
+ {
+ PgsqlFdwExecutionState *festate;
+ PGconn *conn;
+ Oid relid;
+ ForeignTable *table;
+ ForeignServer *server;
+ UserMapping *user;
+ TupleTableSlot *slot = node->ss.ss_ScanTupleSlot;
+
+ /*
+ * Do nothing in EXPLAIN (no ANALYZE) case. node->fdw_state stays NULL.
+ */
+ if (eflags & EXEC_FLAG_EXPLAIN_ONLY)
+ return;
+
+ /*
+ * Save state in node->fdw_state.
+ */
+ festate = (PgsqlFdwExecutionState *) palloc(sizeof(PgsqlFdwExecutionState));
+ festate->fdw_private = ((ForeignScan *) node->ss.ps.plan)->fdw_private;
+
+ /*
+ * Create context for per-scan tuplestore under per-query context.
+ */
+ festate->scan_cxt = AllocSetContextCreate(node->ss.ps.state->es_query_cxt,
+ "pgsql_fdw",
+ ALLOCSET_DEFAULT_MINSIZE,
+ ALLOCSET_DEFAULT_INITSIZE,
+ ALLOCSET_DEFAULT_MAXSIZE);
+
+ /*
+ * Get connection to the foreign server. Connection manager would
+ * establish new connection if necessary.
+ */
+ relid = RelationGetRelid(node->ss.ss_currentRelation);
+ table = GetForeignTable(relid);
+ server = GetForeignServer(table->serverid);
+ user = GetUserMapping(GetOuterUserId(), server->serverid);
+ conn = GetConnection(server, user, true);
+ festate->conn = conn;
+
+ /* Result will be filled in first Iterate call. */
+ festate->tuples = NULL;
+
+ /* Allocate buffers for column values. */
+ {
+ TupleDesc tupdesc = slot->tts_tupleDescriptor;
+ festate->col_values = palloc(sizeof(char *) * tupdesc->natts);
+ festate->attinmeta = TupleDescGetAttInMetadata(tupdesc);
+ }
+
+ /* Allocate buffers for query parameters. */
+ {
+ ParamListInfo params = node->ss.ps.state->es_param_list_info;
+ int numParams = params ? params->numParams : 0;
+
+ if (numParams > 0)
+ {
+ festate->param_types = palloc0(sizeof(Oid) * numParams);
+ festate->param_values = palloc0(sizeof(char *) * numParams);
+ }
+ else
+ {
+ festate->param_types = NULL;
+ festate->param_values = NULL;
+ }
+ }
+
+ /* Store FDW-specific state into ForeignScanState */
+ node->fdw_state = (void *) festate;
+
+ return;
+ }
+
+ /*
+ * pgsqlIterateForeignScan
+ * Retrieve next row from the result set, or clear tuple slot to indicate
+ * EOF.
+ *
+ * Note that using per-query context when retrieving tuples from
+ * tuplestore to ensure that returned tuples can survive until next
+ * iteration because the tuple is released implicitly via ExecClearTuple.
+ * Retrieving a tuple from tuplestore in CurrentMemoryContext (it's a
+ * per-tuple context), ExecClearTuple will free dangling pointer.
+ */
+ static TupleTableSlot *
+ pgsqlIterateForeignScan(ForeignScanState *node)
+ {
+ PgsqlFdwExecutionState *festate;
+ TupleTableSlot *slot = node->ss.ss_ScanTupleSlot;
+ PGresult *res = NULL;
+ MemoryContext oldcontext = CurrentMemoryContext;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+
+ /*
+ * If this is the first call after Begin, we need to execute remote query.
+ * If the query needs cursor, we declare a cursor at first call and fetch
+ * from it in later calls.
+ */
+ if (festate->tuples == NULL)
+ execute_query(node);
+
+ /*
+ * If enough tuples are left in tuplestore, just return next tuple from it.
+ *
+ * It is necessary to switch to per-scan context to make returned tuple
+ * valid until next IterateForeignScan call, because it will be released
+ * with ExecClearTuple then. Otherwise, picked tuple is allocated in
+ * per-tuple context, and double-free of that tuple might happen.
+ */
+ MemoryContextSwitchTo(festate->scan_cxt);
+ if (tuplestore_gettupleslot(festate->tuples, true, false, slot))
+ {
+ MemoryContextSwitchTo(oldcontext);
+ return slot;
+ }
+ MemoryContextSwitchTo(oldcontext);
+
+ /*
+ * Here we need to clear partial result and fetch next bunch of tuples from
+ * from the cursor for the scan. If the fetch returns no tuple, the scan
+ * has reached the end.
+ *
+ * PGresult must be released before leaving this function.
+ */
+ PG_TRY();
+ {
+ res = fetch_result(node);
+ store_result(node, res);
+ PQclear(res);
+ res = NULL;
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+
+ /*
+ * If we got more tuples from the server cursor, return next tuple from
+ * tuplestore.
+ */
+ MemoryContextSwitchTo(festate->scan_cxt);
+ if (tuplestore_gettupleslot(festate->tuples, true, false, slot))
+ {
+ MemoryContextSwitchTo(oldcontext);
+ return slot;
+ }
+ MemoryContextSwitchTo(oldcontext);
+
+ /* We don't have any result even in remote server cursor. */
+ ExecClearTuple(slot);
+ return slot;
+ }
+
+ /*
+ * pgsqlReScanForeignScan
+ * - Restart this scan by resetting fetch location.
+ */
+ static void
+ pgsqlReScanForeignScan(ForeignScanState *node)
+ {
+ List *fdw_private;
+ char *sql;
+ PGconn *conn;
+ PGresult *res = NULL;
+ PgsqlFdwExecutionState *festate;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+
+ /* If we have not opened cursor yet, nothing to do. */
+ if (festate->tuples == NULL)
+ return;
+
+ /* Discard fetch results if any. */
+ tuplestore_clear(festate->tuples);
+
+ /* PGresult must be released before leaving this function. */
+ PG_TRY();
+ {
+ /* Reset cursor */
+ fdw_private = festate->fdw_private;
+ conn = festate->conn;
+ sql = strVal(list_nth(fdw_private, FdwPrivateResetSql));
+ res = PQexec(conn, sql);
+ if (PQresultStatus(res) != PGRES_COMMAND_OK)
+ {
+ ereport(ERROR,
+ (errmsg("could not rewind cursor"),
+ errdetail("%s", PQerrorMessage(conn)),
+ errhint("%s", sql)));
+ }
+ PQclear(res);
+ res = NULL;
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+ }
+
+ /*
+ * pgsqlEndForeignScan
+ * Finish scanning foreign table and dispose objects used for this scan
+ */
+ static void
+ pgsqlEndForeignScan(ForeignScanState *node)
+ {
+ List *fdw_private;
+ char *sql;
+ PGconn *conn;
+ PGresult *res = NULL;
+ PgsqlFdwExecutionState *festate;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+
+ /* if festate is NULL, we are in EXPLAIN; nothing to do */
+ if (festate == NULL)
+ return;
+
+ /* If we have not opened cursor yet, nothing to do. */
+ if (festate->tuples == NULL)
+ return;
+
+ /* Discard fetch results */
+ tuplestore_end(festate->tuples);
+ festate->tuples = NULL;
+
+ /* PGresult must be released before leaving this function. */
+ PG_TRY();
+ {
+ /* Close cursor */
+ fdw_private = festate->fdw_private;
+ conn = festate->conn;
+ sql = strVal(list_nth(fdw_private, FdwPrivateCloseSql));
+ res = PQexec(conn, sql);
+ if (PQresultStatus(res) != PGRES_COMMAND_OK)
+ {
+ ereport(ERROR,
+ (errmsg("could not close cursor"),
+ errdetail("%s", PQerrorMessage(conn)),
+ errhint("%s", sql)));
+ }
+ PQclear(res);
+ res = NULL;
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+
+ ReleaseConnection(festate->conn);
+
+ MemoryContextDelete(festate->scan_cxt);
+ festate->scan_cxt = NULL;
+ }
+
+ /*
+ * Estimate costs of scanning a foreign table.
+ */
+ static void
+ estimate_costs(PlannerInfo *root, RelOptInfo *baserel,
+ const char *sql, Oid serverid,
+ Cost *startup_cost, Cost *total_cost)
+ {
+ ForeignServer *server;
+ UserMapping *user;
+ PGconn *conn = NULL;
+ PGresult *res = NULL;
+ StringInfoData buf;
+ char *plan;
+ char *p;
+ int n;
+
+ /*
+ * Get connection to the foreign server. Connection manager would
+ * establish new connection if necessary.
+ */
+ server = GetForeignServer(serverid);
+ user = GetUserMapping(GetOuterUserId(), server->serverid);
+ conn = GetConnection(server, user, false);
+ initStringInfo(&buf);
+ appendStringInfo(&buf, "EXPLAIN %s", sql);
+
+ /* PGresult must be released before leaving this function. */
+ PG_TRY();
+ {
+ res = PQexec(conn, buf.data);
+ if (PQresultStatus(res) != PGRES_TUPLES_OK || PQntuples(res) == 0)
+ {
+ char *msg;
+
+ msg = pstrdup(PQerrorMessage(conn));
+ ereport(ERROR,
+ (errmsg("could not execute EXPLAIN for cost estimation"),
+ errdetail("%s", msg),
+ errhint("%s", sql)));
+ }
+
+ /*
+ * Find estimation portion from top plan node. Here we search opening
+ * parentheses from the end of the line to avoid finding unexpected
+ * parentheses.
+ */
+ plan = PQgetvalue(res, 0, 0);
+ p = strrchr(plan, '(');
+ if (p == NULL)
+ elog(ERROR, "wrong EXPLAIN output: %s", plan);
+ n = sscanf(p,
+ "(cost=%lf..%lf rows=%lf width=%d)",
+ startup_cost, total_cost, &baserel->rows, &baserel->width);
+ if (n != 4)
+ elog(ERROR, "could not get estimation from EXPLAIN output");
+
+ PQclear(res);
+ res = NULL;
+ ReleaseConnection(conn);
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+
+ /*
+ * TODO Selectivity of quals which are NOT pushed down should be also
+ * considered.
+ */
+
+ /* add cost to establish connection. */
+ *startup_cost += CONNECTION_COSTS;
+ *total_cost += CONNECTION_COSTS;
+
+ /* add cost to transfer result. */
+ *total_cost += TRANSFER_COSTS_PER_BYTE * baserel->width * baserel->tuples;
+ *total_cost += cpu_tuple_cost * baserel->tuples;
+ }
+
+ /*
+ * Execute remote query with current parameters.
+ */
+ static void
+ execute_query(ForeignScanState *node)
+ {
+ List *fdw_private;
+ PgsqlFdwExecutionState *festate;
+ ParamListInfo params = node->ss.ps.state->es_param_list_info;
+ int numParams = params ? params->numParams : 0;
+ Oid *types = NULL;
+ const char **values = NULL;
+ char *sql;
+ PGconn *conn;
+ PGresult *res = NULL;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+ types = festate->param_types;
+ values = festate->param_values;
+
+ /*
+ * Construct parameter array in text format. We don't release memory for
+ * the arrays explicitly, because the memory usage would not be very large,
+ * and anyway they will be released in context cleanup.
+ */
+ if (numParams > 0)
+ {
+ int i;
+
+ 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;
+
+ getTypeOutputInfo(types[i], &out_func_oid, &isvarlena);
+ fmgr_info(out_func_oid, &func);
+ values[i] = OutputFunctionCall(&func, params->params[i].value);
+ }
+ }
+ }
+
+ /* PGresult must be released before leaving this function. */
+ PG_TRY();
+ {
+ /*
+ * Execute remote query with parameters.
+ */
+ conn = festate->conn;
+ fdw_private = ((ForeignScan *) node->ss.ps.plan)->fdw_private;
+ sql = strVal(list_nth(fdw_private, FdwPrivateDeclareSql));
+ res = PQexecParams(conn, sql, numParams, types, values, NULL, NULL, 0);
+
+ /*
+ * If the query has failed, reporting details is enough here.
+ * Connection(s) which are used by this query (at least used by
+ * pgsql_fdw) will be cleaned up by the foreign connection manager.
+ */
+ if (PQresultStatus(res) != PGRES_COMMAND_OK)
+ {
+ ereport(ERROR,
+ (errmsg("could not declare cursor"),
+ errdetail("%s", PQerrorMessage(conn)),
+ errhint("%s", sql)));
+ }
+
+ /* Discard result of DECLARE statement. */
+ PQclear(res);
+ res = NULL;
+
+ /* Fetch first bunch of the result and store them into tuplestore. */
+ res = fetch_result(node);
+ store_result(node, res);
+ PQclear(res);
+ res = NULL;
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+ }
+
+ /*
+ * Fetch next partial result from remote server.
+ *
+ * Once this function has returned result records as PGresult, caller is
+ * responsible to release it, so caller should put codes which might throw
+ * exception in PG_TRY block. When an exception has been caught, release
+ * PGresult and re-throw the exception in PG_CATCH block.
+ */
+ static PGresult *
+ fetch_result(ForeignScanState *node)
+ {
+ PgsqlFdwExecutionState *festate;
+ List *fdw_private;
+ char *sql;
+ PGconn *conn;
+ PGresult *res;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+
+ /* Retrieve information for fetching result. */
+ fdw_private = festate->fdw_private;
+ sql = strVal(list_nth(fdw_private, FdwPrivateFetchSql));
+ conn = festate->conn;
+
+ /*
+ * Fetch result from remote server. In error case, we must release
+ * PGresult in this function to avoid memory leak because caller can't
+ * get the reference.
+ */
+ res = PQexec(conn, sql);
+ if (PQresultStatus(res) != PGRES_TUPLES_OK)
+ {
+ PQclear(res);
+ ereport(ERROR,
+ (errmsg("could not fetch rows from foreign server"),
+ errdetail("%s", PQerrorMessage(conn)),
+ errhint("%s", sql)));
+ }
+
+ return res;
+ }
+
+ /*
+ * Create tuples from PGresult and store them into tuplestore.
+ *
+ * Caller must use PG_TRY block to catch exception and release PGresult
+ * surely.
+ */
+ static void
+ store_result(ForeignScanState *node, PGresult *res)
+ {
+ int rows;
+ int row;
+ int i;
+ int nfields;
+ int attnum; /* number of non-dropped columns */
+ Form_pg_attribute *attrs;
+ TupleTableSlot *slot = node->ss.ss_ScanTupleSlot;
+ TupleDesc tupdesc = slot->tts_tupleDescriptor;
+ PgsqlFdwExecutionState *festate;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+ rows = PQntuples(res);
+ nfields = PQnfields(res);
+ attrs = tupdesc->attrs;
+
+ /* First, ensure that the tuplestore is empty. */
+ if (festate->tuples == NULL)
+ {
+ MemoryContext oldcontext = CurrentMemoryContext;
+
+ /*
+ * Create tuplestore to store result of the query in per-query context.
+ * Note that we use this memory context to avoid memory leak in error
+ * cases.
+ */
+ MemoryContextSwitchTo(festate->scan_cxt);
+ festate->tuples = tuplestore_begin_heap(false, false, work_mem);
+ MemoryContextSwitchTo(oldcontext);
+ }
+ else
+ {
+ /* We already have tuplestore, just need to clear contents of it. */
+ tuplestore_clear(festate->tuples);
+ }
+
+ /* 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)));
+
+ /* put a tuples into the slot */
+ for (row = 0; row < rows; row++)
+ {
+ int j;
+ HeapTuple tuple;
+
+ for (i = 0, j = 0; i < tupdesc->natts; i++)
+ {
+ /* skip dropped columns. */
+ if (attrs[i]->attisdropped)
+ {
+ festate->col_values[i] = NULL;
+ continue;
+ }
+
+ if (PQgetisnull(res, row, j))
+ festate->col_values[i] = NULL;
+ else
+ festate->col_values[i] = PQgetvalue(res, row, 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(festate->attinmeta, festate->col_values);
+ tuplestore_puttuple(festate->tuples, tuple);
+ }
+
+ tuplestore_donestoring(festate->tuples);
+ }
+
diff --git a/contrib/pgsql_fdw/pgsql_fdw.control b/contrib/pgsql_fdw/pgsql_fdw.control
index ...0a9c8f4 .
*** a/contrib/pgsql_fdw/pgsql_fdw.control
--- b/contrib/pgsql_fdw/pgsql_fdw.control
***************
*** 0 ****
--- 1,5 ----
+ # pgsql_fdw extension
+ comment = 'foreign-data wrapper for remote PostgreSQL servers'
+ default_version = '1.0'
+ module_pathname = '$libdir/pgsql_fdw'
+ relocatable = true
diff --git a/contrib/pgsql_fdw/pgsql_fdw.h b/contrib/pgsql_fdw/pgsql_fdw.h
index ...78a6abf .
*** a/contrib/pgsql_fdw/pgsql_fdw.h
--- b/contrib/pgsql_fdw/pgsql_fdw.h
***************
*** 0 ****
--- 1,32 ----
+ /*-------------------------------------------------------------------------
+ *
+ * pgsql_fdw.h
+ * foreign-data wrapper for remote PostgreSQL servers.
+ *
+ * Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/pgsql_fdw.h
+ *
+ *-------------------------------------------------------------------------
+ */
+
+ #ifndef PGSQL_FDW_H
+ #define PGSQL_FDW_H
+
+ #include "postgres.h"
+ #include "foreign/foreign.h"
+ #include "nodes/relation.h"
+
+ /* in option.c */
+ int ExtractConnectionOptions(List *defelems,
+ const char **keywords,
+ const char **values);
+
+ /* in deparse.c */
+ char *deparseSql(Oid relid,
+ PlannerInfo *root,
+ RelOptInfo *baserel,
+ ForeignTable *table);
+
+ #endif /* PGSQL_FDW_H */
diff --git a/contrib/pgsql_fdw/sql/pgsql_fdw.sql b/contrib/pgsql_fdw/sql/pgsql_fdw.sql
index ...b3fac96 .
*** a/contrib/pgsql_fdw/sql/pgsql_fdw.sql
--- b/contrib/pgsql_fdw/sql/pgsql_fdw.sql
***************
*** 0 ****
--- 1,248 ----
+ -- ===================================================================
+ -- create FDW objects
+ -- ===================================================================
+
+ -- Clean up in case a prior regression run failed
+
+ -- Suppress NOTICE messages when roles don't exist
+ SET client_min_messages TO 'error';
+
+ DROP ROLE IF EXISTS pgsql_fdw_user;
+
+ RESET client_min_messages;
+
+ CREATE ROLE pgsql_fdw_user LOGIN SUPERUSER;
+ SET SESSION AUTHORIZATION 'pgsql_fdw_user';
+
+ CREATE EXTENSION pgsql_fdw;
+
+ CREATE SERVER loopback1 FOREIGN DATA WRAPPER pgsql_fdw;
+ CREATE SERVER loopback2 FOREIGN DATA WRAPPER pgsql_fdw
+ OPTIONS (dbname 'contrib_regression');
+
+ CREATE USER MAPPING FOR public SERVER loopback1
+ OPTIONS (user 'value', password 'value');
+ CREATE USER MAPPING FOR pgsql_fdw_user SERVER loopback2;
+
+ CREATE FOREIGN TABLE ft1 (
+ c0 int,
+ c1 int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10)
+ ) SERVER loopback2;
+ ALTER FOREIGN TABLE ft1 DROP COLUMN c0;
+
+ CREATE FOREIGN TABLE ft2 (
+ c0 int,
+ c1 int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10)
+ ) SERVER loopback2;
+ ALTER FOREIGN TABLE ft2 DROP COLUMN c0;
+
+ -- ===================================================================
+ -- create objects used through FDW
+ -- ===================================================================
+ CREATE SCHEMA "S 1";
+ CREATE TABLE "S 1"."T 1" (
+ "C 1" int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10),
+ CONSTRAINT t1_pkey PRIMARY KEY ("C 1")
+ );
+ CREATE TABLE "S 1"."T 2" (
+ c1 int NOT NULL,
+ c2 text,
+ CONSTRAINT t2_pkey PRIMARY KEY (c1)
+ );
+
+ BEGIN;
+ TRUNCATE "S 1"."T 1";
+ INSERT INTO "S 1"."T 1"
+ SELECT id,
+ id % 10,
+ to_char(id, 'FM00000'),
+ '1970-01-01'::timestamptz + ((id % 100) || ' days')::interval,
+ '1970-01-01'::timestamp + ((id % 100) || ' days')::interval,
+ id % 10,
+ id % 10
+ FROM generate_series(1, 1000) id;
+ TRUNCATE "S 1"."T 2";
+ INSERT INTO "S 1"."T 2"
+ SELECT id,
+ 'AAA' || to_char(id, 'FM000')
+ FROM generate_series(1, 100) id;
+ COMMIT;
+
+ -- ===================================================================
+ -- tests for pgsql_fdw_validator
+ -- ===================================================================
+ ALTER FOREIGN DATA WRAPPER pgsql_fdw OPTIONS (host 'value'); -- ERROR
+ -- requiressl, krbsrvname and gsslib are omitted because they depend on
+ -- configure option
+ ALTER SERVER loopback1 OPTIONS (
+ authtype 'value',
+ service 'value',
+ connect_timeout 'value',
+ dbname 'value',
+ host 'value',
+ hostaddr 'value',
+ port 'value',
+ --client_encoding 'value',
+ tty 'value',
+ options 'value',
+ application_name 'value',
+ --fallback_application_name 'value',
+ keepalives 'value',
+ keepalives_idle 'value',
+ keepalives_interval 'value',
+ -- requiressl 'value',
+ sslcompression 'value',
+ sslmode 'value',
+ sslcert 'value',
+ sslkey 'value',
+ sslrootcert 'value',
+ sslcrl 'value'
+ --requirepeer 'value',
+ -- krbsrvname 'value',
+ -- gsslib 'value',
+ --replication 'value'
+ );
+ ALTER SERVER loopback1 OPTIONS (user 'value'); -- ERROR
+ ALTER SERVER loopback2 OPTIONS (ADD fetch_count '2');
+ ALTER USER MAPPING FOR public SERVER loopback1
+ OPTIONS (DROP user, DROP password);
+ ALTER USER MAPPING FOR public SERVER loopback1
+ OPTIONS (host 'value'); -- ERROR
+ ALTER FOREIGN TABLE ft1 OPTIONS (nspname 'S 1', relname 'T 1');
+ ALTER FOREIGN TABLE ft2 OPTIONS (nspname 'S 1', relname 'T 1', fetch_count '100');
+ ALTER FOREIGN TABLE ft1 OPTIONS (invalid 'value'); -- ERROR
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count 'a'); -- ERROR
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count '0'); -- ERROR
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count '-1'); -- ERROR
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (invalid 'value'); -- ERROR
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (colname 'C 1');
+ ALTER FOREIGN TABLE ft2 ALTER COLUMN c1 OPTIONS (colname 'C 1');
+ \dew+
+ \des+
+ \deu+
+ \det+
+
+ -- ===================================================================
+ -- simple queries
+ -- ===================================================================
+ -- single table, with/without alias
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
+ SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+ SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+ -- with WHERE clause
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
+ SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
+ -- 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
+ SELECT * FROM ft1 t1 WHERE t1.c3 = (SELECT MAX(c3) FROM ft2 t2) ORDER BY c1;
+ -- used in CTE
+ WITH t1 AS (SELECT * FROM ft1 WHERE c1 <= 10) SELECT t2.c1, t2.c2, t2.c3, t2.c4 FROM t1, ft2 t2 WHERE t1.c1 = t2.c1 ORDER BY t1.c1;
+ -- fixed values
+ SELECT 'fixed', NULL FROM ft1 t1 WHERE c1 = 1;
+ -- user-defined operator/function
+ CREATE FUNCTION pgsql_fdw_abs(int) RETURNS int AS $$
+ BEGIN
+ RETURN abs($1);
+ END
+ $$ LANGUAGE plpgsql IMMUTABLE;
+ CREATE OPERATOR === (
+ LEFTARG = int,
+ RIGHTARG = int,
+ PROCEDURE = int4eq,
+ COMMUTATOR = ===,
+ NEGATOR = !==
+ );
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = pgsql_fdw_abs(t1.c2);
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 === t1.c2;
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = abs(t1.c2);
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = t1.c2;
+
+ -- ===================================================================
+ -- parameterized queries
+ -- ===================================================================
+ -- simple join
+ PREPARE st1(int, int) AS SELECT t1.c3, t2.c3 FROM ft1 t1, ft2 t2 WHERE t1.c1 = $1 AND t2.c1 = $2;
+ EXPLAIN (COSTS false) EXECUTE st1(1, 2);
+ EXECUTE st1(1, 1);
+ EXECUTE st1(101, 101);
+ -- subquery using stable function (can't be pushed down)
+ PREPARE st2(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c4) = 6) ORDER BY c1;
+ EXPLAIN (COSTS false) EXECUTE st2(10, 20);
+ EXECUTE st2(10, 20);
+ EXECUTE st1(101, 101);
+ -- subquery using immutable function (can be pushed down)
+ PREPARE st3(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c5) = 6) ORDER BY c1;
+ EXPLAIN (COSTS false) EXECUTE st3(10, 20);
+ EXECUTE st3(10, 20);
+ EXECUTE st3(20, 30);
+ -- custom plan should be chosen
+ PREPARE st4(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 = $1;
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ -- cleanup
+ DEALLOCATE st1;
+ DEALLOCATE st2;
+ DEALLOCATE st3;
+ DEALLOCATE st4;
+
+ -- ===================================================================
+ -- connection management
+ -- ===================================================================
+ SELECT srvname, usename FROM pgsql_fdw_connections;
+ SELECT pgsql_fdw_disconnect(srvid, usesysid) FROM pgsql_fdw_get_connections();
+ SELECT srvname, usename FROM pgsql_fdw_connections;
+
+ -- ===================================================================
+ -- subtransaction
+ -- ===================================================================
+ BEGIN;
+ DECLARE c CURSOR FOR SELECT * FROM ft1 ORDER BY c1;
+ FETCH c;
+ SAVEPOINT s;
+ ERROR OUT; -- ERROR
+ ROLLBACK TO s;
+ SELECT srvname FROM pgsql_fdw_connections;
+ FETCH c;
+ COMMIT;
+ SELECT srvname FROM pgsql_fdw_connections;
+ ERROR OUT; -- ERROR
+ SELECT srvname FROM pgsql_fdw_connections;
+
+ -- ===================================================================
+ -- cleanup
+ -- ===================================================================
+ DROP OPERATOR === (int, int) CASCADE;
+ DROP OPERATOR !== (int, int) CASCADE;
+ DROP FUNCTION pgsql_fdw_abs(int);
+ DROP SCHEMA "S 1" CASCADE;
+ DROP EXTENSION pgsql_fdw CASCADE;
+ \c
+ DROP ROLE pgsql_fdw_user;
diff --git a/doc/src/sgml/contrib.sgml b/doc/src/sgml/contrib.sgml
index d4da4ee..32056d1 100644
*** a/doc/src/sgml/contrib.sgml
--- b/doc/src/sgml/contrib.sgml
*************** CREATE EXTENSION <replaceable>module_nam
*** 117,122 ****
--- 117,123 ----
&pgcrypto;
&pgfreespacemap;
&pgrowlocks;
+ &pgsql-fdw;
&pgstandby;
&pgstatstatements;
&pgstattuple;
diff --git a/doc/src/sgml/filelist.sgml b/doc/src/sgml/filelist.sgml
index b5d3c6d..de686ee 100644
*** a/doc/src/sgml/filelist.sgml
--- b/doc/src/sgml/filelist.sgml
***************
*** 125,130 ****
--- 125,131 ----
<!ENTITY pgcrypto SYSTEM "pgcrypto.sgml">
<!ENTITY pgfreespacemap SYSTEM "pgfreespacemap.sgml">
<!ENTITY pgrowlocks SYSTEM "pgrowlocks.sgml">
+ <!ENTITY pgsql-fdw SYSTEM "pgsql-fdw.sgml">
<!ENTITY pgstandby SYSTEM "pgstandby.sgml">
<!ENTITY pgstatstatements SYSTEM "pgstatstatements.sgml">
<!ENTITY pgstattuple SYSTEM "pgstattuple.sgml">
diff --git a/doc/src/sgml/pgsql-fdw.sgml b/doc/src/sgml/pgsql-fdw.sgml
index ...30fa7f5 .
*** a/doc/src/sgml/pgsql-fdw.sgml
--- b/doc/src/sgml/pgsql-fdw.sgml
***************
*** 0 ****
--- 1,261 ----
+ <!-- doc/src/sgml/pgsql-fdw.sgml -->
+
+ <sect1 id="pgsql-fdw" xreflabel="pgsql_fdw">
+ <title>pgsql_fdw</title>
+
+ <indexterm zone="pgsql-fdw">
+ <primary>pgsql_fdw</primary>
+ </indexterm>
+
+ <para>
+ The <filename>pgsql_fdw</filename> module provides a foreign-data wrapper for
+ external <productname>PostgreSQL</productname> servers.
+ With this module, users can access data stored in external
+ <productname>PostgreSQL</productname> via plain SQL statements.
+ </para>
+
+ <para>
+ Note that default wrapper <literal>pgsql_fdw</literal> is created
+ automatically during <command>CREATE EXTENSION</command> command for
+ <application>pgsql_fdw</application>.
+ </para>
+
+ <sect2>
+ <title>FDW Options of pgsql_fdw</title>
+
+ <sect3>
+ <title>Connection Options</title>
+ <para>
+ A foreign server and user mapping created using this wrapper can have
+ <application>libpq</> connection options, expect below:
+
+ <itemizedlist>
+ <listitem><para>client_encoding</para></listitem>
+ <listitem><para>fallback_application_name</para></listitem>
+ <listitem><para>replication</para></listitem>
+ </itemizedlist>
+
+ For details of <application>libpq</> connection options, see
+ <xref linkend="libpq-connect">.
+ </para>
+
+ <para>
+ <literal>user</literal> and <literal>password</literal> can be
+ specified on user mappings, and others can be specified on foreign servers.
+ </para>
+ </sect3>
+
+ <sect3>
+ <title>Object Name Options</title>
+ <para>
+ Foreign tables which were created using this wrapper, or its columns can
+ have object name options. These options can be used to specify the names
+ used in SQL statement sent to remote <productname>PostgreSQL</productname>
+ server. These options are useful when a remote object has different name
+ from corresponding local one.
+ </para>
+
+ <variablelist>
+
+ <varlistentry>
+ <term><literal>nspname</literal></term>
+ <listitem>
+ <para>
+ This option, which can be specified on a foreign table, is used as a
+ namespace (schema) reference in the SQL statement. If this options is
+ omitted, <literal>pg_class.nspname</literal> of the foreign table is
+ used.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>relname</literal></term>
+ <listitem>
+ <para>
+ This option, which can be specified on a foreign table, is used as a
+ relation (table) reference in the SQL statement. If this options is
+ omitted, <literal>pg_class.relname</literal> of the foreign table is
+ used.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>colname</literal></term>
+ <listitem>
+ <para>
+ This option, which can be specified on a column of a foreign table, is
+ used as a column (attribute) reference in the SQL statement. If this
+ option is omitted, <literal>pg_attribute.attname</literal> of the column
+ of the foreign table is used.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ </variablelist>
+
+ </sect3>
+
+ <sect3>
+ <title>Cursor Options</title>
+ <para>
+ The <application>pgsql_fdw</application> always uses cursor to retrieve the
+ result from external server. Users can control the behavior of cursor by
+ setting cursor options to foreign table or foreign server. If an option
+ is set to both objects, finer-grained setting is used. In other words,
+ foreign table's setting overrides foreign server's setting.
+ </para>
+
+ <variablelist>
+
+ <varlistentry>
+ <term><literal>fetch_count</literal></term>
+ <listitem>
+ <para>
+ This option specifies the number of rows to be fetched at a time.
+ This option accepts only integer value larger than zero. The default
+ setting is 10000.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ </variablelist>
+
+ </sect3>
+
+ </sect2>
+
+ <sect2>
+ <title>Connection Management</title>
+
+ <para>
+ The <application>pgsql_fdw</application> establishes a connection to a
+ foreign server in the beginning of the first query which uses a foreign
+ table associated to the foreign server, and reuses the connection following
+ queries and even in following foreign scans in same query.
+
+ You can see the list of active connections via
+ <structname>pgsql_fdw_connections</structname> view. It shows pair of oid
+ and name of server and local role for each active connections established by
+ <application>pgsql_fdw</application>. For security reason, only superuser
+ can see other role's connections.
+ </para>
+
+ <para>
+ Established connections are kept alive until local role changes or the
+ current transaction aborts or user requests so.
+ </para>
+
+ <para>
+ If role has been changed, active connections established as old local role
+ is kept alive but never be reused until local role has restored to original
+ role. This kind of situation happens with <command>SET ROLE</command> and
+ <command>SET SESSION AUTHORIZATION</command>.
+ </para>
+
+ <para>
+ If current transaction aborts by error or user request, all active
+ connections are disconnected automatically. This behavior avoids possible
+ connection leaks on error.
+ </para>
+
+ <para>
+ You can discard persistent connection at arbitrary timing with
+ <function>pgsql_fdw_disconnect()</function>. It takes server oid and
+ user oid as arguments. This function can handle only connections
+ established in current session; connections established by other backends
+ are not reachable.
+ </para>
+
+ <para>
+ You can discard all active and visible connections in current session with
+ using <structname>pgsql_fdw_connections</structname> and
+ <function>pgsql_fdw_disconnect()</function> together:
+ <synopsis>
+ postgres=# SELECT pgsql_fdw_disconnect(srvid, usesysid) FROM pgsql_fdw_connections;
+ pgsql_fdw_disconnect
+ ----------------------
+ OK
+ OK
+ (2 rows)
+ </synopsis>
+ </para>
+ </sect2>
+
+ <sect2>
+ <title>Transaction Management</title>
+ <para>
+ The <application>pgsql_fdw</application> begins remote transaction at the
+ beginning of a local query, and terminates it with <command>ABORT</command>
+ at the end of the local query. This means that all foreign scans on a
+ foreign server in a local query are executed in one transaction.
+ If isolation level of local transaction is <literal>SERIALIZABLE</literal>,
+ <literal>SERIALIZABLE</literal> is used for remote transaction. Otherwise,
+ if isolation level of local transaction is one of
+ <literal>READ UNCOMMITTED</literal>, <literal>READ COMMITTED</literal> or
+ <literal>REPEATABLE READ</literal>, then <literal>REPEATABLE READ</literal>
+ is used for remote transaction.
+ <literal>READ UNCOMMITTED</literal> and <literal>READ COMMITTED</literal>
+ are never used for remote transaction, because even
+ <literal>READ COMMITTED</literal> transaction might produce inconsistent
+ results, if remote data have been updated between two remote queries.
+ </para>
+ <para>
+ Note that even if the isolation level of local transaction was
+ <literal>SERIALIZABLE</literal> or <literal>REPEATABLE READ</literal>,
+ series of one query might produce different result, because foreign scans
+ in different local queries are executed in different remote transactions.
+ For instance, when client started a local transaction
+ explicitly with isolation level <literal>SERIALIZABLE</literal>, and
+ executed same local query which contains a foreign table which references
+ foreign data which is updated frequently, latter result would be different
+ from former result.
+ </para>
+ <para>
+ This restriction might be relaxed in future release.
+ </para>
+ </sect2>
+
+ <sect2>
+ <title>Estimation of Costs and Rows</title>
+ <para>
+ The <application>pgsql_fdw</application> estimates the costs of a foreign
+ scan by adding up some basic costs: connection costs, remote query costs
+ and data transfer costs.
+ To get remote query costs, <application>pgsql_fdw</application> executes
+ <command>EXPLAIN</command> command on remote server for each foreign scan.
+ </para>
+ <para>
+ On the other hand, estimated rows which was returned by
+ <command>EXPLAIN</command> is used for local estimation as-is.
+ </para>
+ </sect2>
+
+ <sect2>
+ <title>EXPLAIN Output</title>
+ <para>
+ For a foreign table using <literal>pgsql_fdw</>, <command>EXPLAIN</> shows
+ a remote SQL statement which is sent to remote
+ <productname>PostgreSQL</productname> server for a ForeignScan plan node.
+ For example:
+ </para>
+ <synopsis>
+ postgres=# EXPLAIN SELECT aid FROM pgbench_accounts WHERE abalance < 0;
+ QUERY PLAN
+ --------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on pgbench_accounts (cost=100.00..8105.13 rows=302613 width=8)
+ Filter: (abalance < 0)
+ Remote SQL: DECLARE pgsql_fdw_cursor_3 SCROLL CURSOR FOR SELECT aid, NULL, abalance, NULL FROM public.pgbench_accounts
+ (3 rows)
+ </synopsis>
+ </sect2>
+
+ <sect2>
+ <title>Author</title>
+ <para>
+ Shigeru Hanada <email>shigeru.hanada@gmail.com</email>
+ </para>
+ </sect2>
+
+ </sect1>
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 64ba8ec..94b2ea9 100644
*** a/src/backend/utils/adt/ruleutils.c
--- b/src/backend/utils/adt/ruleutils.c
*************** deparse_context_for(const char *aliasnam
*** 2181,2186 ****
--- 2181,2209 ----
return list_make1(dpns);
}
+ /* ----------
+ * deparse_context_for_rtelist - Build deparse context for a list of RTEs
+ *
+ * Given the list of RangeTableEnetry, build deparsing context for an
+ * expression referencing those relations. This is sufficient for uses of
+ * deparse_expression before plan has been created.
+ * ----------
+ */
+ List *
+ deparse_context_for_rtelist(List *rtable)
+ {
+ deparse_namespace *dpns;
+
+ dpns = (deparse_namespace *) palloc0(sizeof(deparse_namespace));
+
+ /* Build a minimal RTE for the rel */
+ dpns->rtable = rtable;
+ dpns->ctes = NIL;
+
+ /* Return a one-deep namespace stack */
+ return list_make1(dpns);
+ }
+
/*
* deparse_context_for_planstate - Build deparse context for a plan
*
diff --git a/src/include/utils/builtins.h b/src/include/utils/builtins.h
index 9fda7ad..4cf0489 100644
*** a/src/include/utils/builtins.h
--- b/src/include/utils/builtins.h
*************** extern char *deparse_expression(Node *ex
*** 650,655 ****
--- 650,656 ----
extern List *deparse_context_for(const char *aliasname, Oid relid);
extern List *deparse_context_for_planstate(Node *planstate, List *ancestors,
List *rtable);
+ extern List *deparse_context_for_rtelist(List *rtable);
extern const char *quote_identifier(const char *ident);
extern char *quote_qualified_identifier(const char *qualifier,
const char *ident);
Shigeru Hanada <shigeru.hanada@gmail.com> writes:
[ pgsql_fdw_v15.patch ]
I've been looking at this patch a little bit over the past day or so.
I'm pretty unhappy with deparse.c --- it seems like a real kluge,
inefficient and full of corner-case bugs. After some thought I believe
that you're ultimately going to have to abandon depending on ruleutils.c
for reverse-listing services, and it would be best to bite that bullet
now and rewrite this code from scratch. ruleutils.c is serving two
masters already (rule-dumping and EXPLAIN) and it's not going to be
practical to tweak its behavior further for this usage; yet there are
all sorts of clear problems that you are going to run into, boiling down
to the fact that names on the remote end aren't necessarily the same as
names on the local end. For instance, ruleutils.c is not going to be
helpful at schema-qualifying function names in a way that's correct for
the foreign server environment. Another issue is that as soon as you
try to push down join clauses for parameterized paths, you are going to
want Vars of other relations to be printed as parameters ($n, most
likely) and ruleutils is not going to know to do that. Seeing that
semantic constraints will greatly limit the set of node types that can
ever be pushed down anyway, I think it's likely to be easiest to just
write your own node-printing code and not even try to use ruleutils.c.
There are a couple of other points that make me think we need to revisit
the PlanForeignScan API definition some more, too. First, deparse.c is
far from cheap. While this doesn't matter greatly as long as there's
only one possible path for a foreign table, as soon as you want to
create more than one it's going to be annoying to do all that work N
times and then throw away N-1 of the results. I also choked on the fact
that the pushdown patch thinks it can modify baserel->baserestrictinfo.
That might accidentally fail to malfunction right now, but it's never
going to scale to multiple paths with potentially different sets of
remotely-applied constraints. So I'm thinking we really need to let
FDWs in on the Path versus Plan distinction --- that is, a Path just
needs to be a cheap summary of a way to do things, and then at
createplan.c time you convert the selected Path into a full-fledged
Plan. Most of the work done in deparse.c could be postponed to
createplan time and done only once, even with multiple paths.
The baserestrictinfo hack would be unnecessary too if the FDW had more
direct control over generation of the ForeignScan plan node.
Another thing I'm thinking we should let FDWs in on is the distinction
between rowcount estimation and path generation. When we did the first
API design last year it was okay to expect a single call to do both,
but as of a couple months ago allpaths.c does those steps in two
separate passes over the baserels, and it'd be handy if FDWs would
cooperate.
So we need to break down what PlanForeignScan currently does into three
separate steps. The first idea that comes to mind is to call them
GetForeignRelSize, GetForeignPaths, GetForeignPlan; but maybe somebody
has a better idea for names?
regards, tom lane
I wrote:
There are a couple of other points that make me think we need to revisit
the PlanForeignScan API definition some more, too. ...
So we need to break down what PlanForeignScan currently does into three
separate steps. The first idea that comes to mind is to call them
GetForeignRelSize, GetForeignPaths, GetForeignPlan; but maybe somebody
has a better idea for names?
Attached is a draft patch for that. While I was working on this
I realized that we were very far short of allowing FDWs to set up
expressions of their choice for execution; there was nothing for that in
setrefs.c, nor some other places that need to post-process expressions.
I had originally supposed that fdw_private could just contain some
expression trees, but that wasn't going to work without post-processing.
So this patch attempts to cover that too, by breaking what had been
fdw_private into a "private" part and an "fdw_exprs" list that will be
subject to expression post-processing. (The alternative to this would
be to do post-processing on all of fdw_private, but that would
considerably restrict what can be in fdw_private, so it seemed better
to decree two separate fields.)
Working on this also helped me identify some other things that had been
subliminally bothering me about pgsql_fdw's qual pushdown code. That
patch is set up with the idea of pushing entire quals (boolean
RestrictInfo expressions) across to the remote side, but I think that
is probably the wrong granularity, or at least not the only mechanism
we should have. IMO it is more important to provide a structure similar
to index quals; that is, what you want to identify is RestrictInfo
expressions of the form
remote_variable operator local_expression
where the operator has to be one that the remote can execute with the
same semantics as we think it has, but the only real restriction on the
local_expression is that it be stable, because we'll execute it locally
and send only its result value across to the remote. (The SQL sent to
the remote looks like "remote_variable operator $1", or some such.)
Thus, to take an example that's said to be unsafe in the existing code
comments, there's no problem at all with
remote_timestamp_col = now()
as long as we execute now() locally.
There might be some value in pushing entire quals across too, for
clauses like "remote_variable_1 = remote_variable_2", but I believe
that these are not nearly as important as "variable = constant" and
"variable = join_variable" cases. Consider that when dealing with a
local table, only the latter two cases can be accelerated by indexes.
regards, tom lane
(2012/03/09 14:00), Tom Lane wrote:
I wrote:
There are a couple of other points that make me think we need to revisit
the PlanForeignScan API definition some more, too. ...
So we need to break down what PlanForeignScan currently does into three
separate steps. The first idea that comes to mind is to call them
GetForeignRelSize, GetForeignPaths, GetForeignPlan; but maybe somebody
has a better idea for names?Attached is a draft patch for that.
1. FilefdwPlanState.pages and FileFdwPlanState.ntuples seems redundant.
Why not use RelOptInfo.pages and RelOptInfo.tuples?
2. IMHO RelOptInfo.fdw_private seems confusing. How about renaming it
to e.g., RelOptInfo.fdw_state?
Attached is a patch for the draft patch.
Best regards,
Etsuro Fujita
Attachments:
postgresql-fdwapi.patchtext/plain; name=postgresql-fdwapi.patchDownload
*** a/contrib/file_fdw/file_fdw.c
--- b/contrib/file_fdw/file_fdw.c
***************
*** 74,87 **** static const struct FileFdwOption valid_options[] = {
};
/*
! * FDW-specific information for RelOptInfo.fdw_private.
*/
typedef struct FileFdwPlanState
{
char *filename; /* file to read */
List *options; /* merged COPY options, excluding filename */
- BlockNumber pages; /* estimate of file's physical size */
- double ntuples; /* estimate of number of rows in file */
} FileFdwPlanState;
/*
--- 74,85 ----
};
/*
! * FDW-specific information for RelOptInfo.fdw_state.
*/
typedef struct FileFdwPlanState
{
char *filename; /* file to read */
List *options; /* merged COPY options, excluding filename */
} FileFdwPlanState;
/*
***************
*** 132,140 **** static void fileGetOptions(Oid foreigntableid,
char **filename, List **other_options);
static List *get_file_fdw_attribute_options(Oid relid);
static void estimate_size(PlannerInfo *root, RelOptInfo *baserel,
! FileFdwPlanState *fdw_private);
static void estimate_costs(PlannerInfo *root, RelOptInfo *baserel,
- FileFdwPlanState *fdw_private,
Cost *startup_cost, Cost *total_cost);
--- 130,137 ----
char **filename, List **other_options);
static List *get_file_fdw_attribute_options(Oid relid);
static void estimate_size(PlannerInfo *root, RelOptInfo *baserel,
! FileFdwPlanState *fpstate);
static void estimate_costs(PlannerInfo *root, RelOptInfo *baserel,
Cost *startup_cost, Cost *total_cost);
***************
*** 415,433 **** fileGetForeignRelSize(PlannerInfo *root,
RelOptInfo *baserel,
Oid foreigntableid)
{
! FileFdwPlanState *fdw_private;
/*
* Fetch options. We only need filename at this point, but we might
* as well get everything and not need to re-fetch it later in planning.
*/
! fdw_private = (FileFdwPlanState *) palloc(sizeof(FileFdwPlanState));
! fileGetOptions(foreigntableid,
! &fdw_private->filename, &fdw_private->options);
! baserel->fdw_private = (void *) fdw_private;
/* Estimate relation size */
! estimate_size(root, baserel, fdw_private);
}
/*
--- 412,429 ----
RelOptInfo *baserel,
Oid foreigntableid)
{
! FileFdwPlanState *fpstate;
/*
* Fetch options. We only need filename at this point, but we might
* as well get everything and not need to re-fetch it later in planning.
*/
! fpstate = (FileFdwPlanState *) palloc(sizeof(FileFdwPlanState));
! fileGetOptions(foreigntableid, &fpstate->filename, &fpstate->options);
! baserel->fdw_state = (void *) fpstate;
/* Estimate relation size */
! estimate_size(root, baserel, fpstate);
}
/*
***************
*** 443,455 **** fileGetForeignPaths(PlannerInfo *root,
RelOptInfo *baserel,
Oid foreigntableid)
{
- FileFdwPlanState *fdw_private = (FileFdwPlanState *) baserel->fdw_private;
Cost startup_cost;
Cost total_cost;
/* Estimate costs */
! estimate_costs(root, baserel, fdw_private,
! &startup_cost, &total_cost);
/* Create a ForeignPath node and add it as only possible path */
add_path(baserel, (Path *)
--- 439,449 ----
RelOptInfo *baserel,
Oid foreigntableid)
{
Cost startup_cost;
Cost total_cost;
/* Estimate costs */
! estimate_costs(root, baserel, &startup_cost, &total_cost);
/* Create a ForeignPath node and add it as only possible path */
add_path(baserel, (Path *)
***************
*** 647,659 **** fileReScanForeignScan(ForeignScanState *node)
/*
* Estimate size of a foreign table.
*
! * The main result is returned in baserel->rows. We also set
! * fdw_private->pages and fdw_private->ntuples for later use in the cost
! * calculation.
*/
static void
estimate_size(PlannerInfo *root, RelOptInfo *baserel,
! FileFdwPlanState *fdw_private)
{
struct stat stat_buf;
BlockNumber pages;
--- 641,652 ----
/*
* Estimate size of a foreign table.
*
! * The main result is returned in baserel->rows. We also set baserel->pages
! * and baserel->tuples for later use in the cost calculation.
*/
static void
estimate_size(PlannerInfo *root, RelOptInfo *baserel,
! FileFdwPlanState *fpstate)
{
struct stat stat_buf;
BlockNumber pages;
***************
*** 665,671 **** estimate_size(PlannerInfo *root, RelOptInfo *baserel,
* Get size of the file. It might not be there at plan time, though, in
* which case we have to use a default estimate.
*/
! if (stat(fdw_private->filename, &stat_buf) < 0)
stat_buf.st_size = 10 * BLCKSZ;
/*
--- 658,664 ----
* Get size of the file. It might not be there at plan time, though, in
* which case we have to use a default estimate.
*/
! if (stat(fpstate->filename, &stat_buf) < 0)
stat_buf.st_size = 10 * BLCKSZ;
/*
***************
*** 675,681 **** estimate_size(PlannerInfo *root, RelOptInfo *baserel,
if (pages < 1)
pages = 1;
! fdw_private->pages = pages;
/*
* Estimate the number of tuples in the file. We back into this estimate
--- 668,674 ----
if (pages < 1)
pages = 1;
! baserel->pages = pages;
/*
* Estimate the number of tuples in the file. We back into this estimate
***************
*** 688,694 **** estimate_size(PlannerInfo *root, RelOptInfo *baserel,
ntuples = clamp_row_est((double) stat_buf.st_size / (double) tuple_width);
! fdw_private->ntuples = ntuples;
/*
* Now estimate the number of rows returned by the scan after applying the
--- 681,687 ----
ntuples = clamp_row_est((double) stat_buf.st_size / (double) tuple_width);
! baserel->tuples = ntuples;
/*
* Now estimate the number of rows returned by the scan after applying the
***************
*** 715,725 **** estimate_size(PlannerInfo *root, RelOptInfo *baserel,
*/
static void
estimate_costs(PlannerInfo *root, RelOptInfo *baserel,
- FileFdwPlanState *fdw_private,
Cost *startup_cost, Cost *total_cost)
{
! BlockNumber pages = fdw_private->pages;
! double ntuples = fdw_private->ntuples;
Cost run_cost = 0;
Cost cpu_per_tuple;
--- 708,717 ----
*/
static void
estimate_costs(PlannerInfo *root, RelOptInfo *baserel,
Cost *startup_cost, Cost *total_cost)
{
! BlockNumber pages = baserel->pages;
! double ntuples = baserel->tuples;
Cost run_cost = 0;
Cost cpu_per_tuple;
*** a/doc/src/sgml/fdwhandler.sgml
--- b/doc/src/sgml/fdwhandler.sgml
***************
*** 126,132 **** GetForeignRelSize (PlannerInfo *root,
</para>
<para>
! <literal>baserel->fdw_private</> is a <type>void</> pointer that is
available for use by FDW planning functions. It can be used to pass
information forward from <function>GetForeignRelSize</> to
<function>GetForeignPaths</> and/or <function>GetForeignPaths</> to
--- 126,132 ----
</para>
<para>
! <literal>baserel->fdw_state</> is a <type>void</> pointer that is
available for use by FDW planning functions. It can be used to pass
information forward from <function>GetForeignRelSize</> to
<function>GetForeignPaths</> and/or <function>GetForeignPaths</> to
*** a/src/backend/optimizer/util/relnode.c
--- b/src/backend/optimizer/util/relnode.c
***************
*** 114,120 **** build_simple_rel(PlannerInfo *root, int relid, RelOptKind reloptkind)
rel->subplan = NULL;
rel->subroot = NULL;
rel->fdwroutine = NULL;
! rel->fdw_private = NULL;
rel->baserestrictinfo = NIL;
rel->baserestrictcost.startup = 0;
rel->baserestrictcost.per_tuple = 0;
--- 114,120 ----
rel->subplan = NULL;
rel->subroot = NULL;
rel->fdwroutine = NULL;
! rel->fdw_state = NULL;
rel->baserestrictinfo = NIL;
rel->baserestrictcost.startup = 0;
rel->baserestrictcost.per_tuple = 0;
***************
*** 369,375 **** build_join_rel(PlannerInfo *root,
joinrel->subplan = NULL;
joinrel->subroot = NULL;
joinrel->fdwroutine = NULL;
! joinrel->fdw_private = NULL;
joinrel->baserestrictinfo = NIL;
joinrel->baserestrictcost.startup = 0;
joinrel->baserestrictcost.per_tuple = 0;
--- 369,375 ----
joinrel->subplan = NULL;
joinrel->subroot = NULL;
joinrel->fdwroutine = NULL;
! joinrel->fdw_state = NULL;
joinrel->baserestrictinfo = NIL;
joinrel->baserestrictcost.startup = 0;
joinrel->baserestrictcost.per_tuple = 0;
*** a/src/include/nodes/relation.h
--- b/src/include/nodes/relation.h
***************
*** 422,428 **** typedef struct RelOptInfo
PlannerInfo *subroot; /* if subquery */
/* use "struct FdwRoutine" to avoid including fdwapi.h here */
struct FdwRoutine *fdwroutine; /* if foreign table */
! void *fdw_private; /* if foreign table */
/* used by various scans and joins: */
List *baserestrictinfo; /* RestrictInfo structures (if base
--- 422,428 ----
PlannerInfo *subroot; /* if subquery */
/* use "struct FdwRoutine" to avoid including fdwapi.h here */
struct FdwRoutine *fdwroutine; /* if foreign table */
! void *fdw_state; /* if foreign table */
/* used by various scans and joins: */
List *baserestrictinfo; /* RestrictInfo structures (if base
Etsuro Fujita <fujita.etsuro@lab.ntt.co.jp> writes:
(2012/03/09 14:00), Tom Lane wrote:
Attached is a draft patch for that.
1. FilefdwPlanState.pages and FileFdwPlanState.ntuples seems redundant.
Why not use RelOptInfo.pages and RelOptInfo.tuples?
I intentionally avoided setting RelOptInfo.pages because that would have
other effects on planning (cf total_table_pages or whatever it's
called). It's possible that that would actually be desirable, depending
on whether you think the external file should be counted as part of the
query's disk-access footprint; but it would take some investigation to
conclude that, which I didn't feel like doing right now. Likewise, I'm
not sure offhand what side effects might occur from using
RelOptInfo.tuples, and didn't want to change file_fdw's behavior without
more checking.
2. IMHO RelOptInfo.fdw_private seems confusing. How about renaming it
to e.g., RelOptInfo.fdw_state?
Why is that better? It seems just as open to confusion with another
field (ie, the execution-time fdw_state). I thought for a little bit
about trying to give different names to all four of the fdw private
fields (RelOptInfo, Path, Plan, PlanState) but it's not obvious what
naming rule to use, and at least the last two of those can't be changed
without breaking existing FDW code.
regards, tom lane
I've not read whole of the patch yet, but I have basic questions.
1) IIUC, GetForeignRelSize should set baserel->rows to the number of
rows the ForeignScan node returns to upper node, but not the number
of rows FDW returns to core executor, right?
BTW, once Fujita-san's ANALYZE support patch is merged, we will be
able to get rows estimatation easily by calling clauselist_selectivity
with baserel->tuples and baserestrictinfo. Otherwise, pgsql_fdw
would still need to execute EXPLAIN on remote side to get meaningful
rows estimation.
2) ISTM that pgsql_fdw needs to execute EXPLAIN on remote side for each
possible remote query to get meaningful costs estimation, and it
requires pgsql_fdw to generate SQL statements in GetForeignPaths.
I worry that I've misunderstood intention of your design because
you've mentioned postponing SQL deparsing to createplan time.
I'll read the document and patch, and fix pgsql_fdw so that it can
work with new API. As for now, I think that pgsqlPlanForeignScan
should be separated like below:
GetForeignRelSize
1) Retrieve catalog infomation via GetForeignFoo funcitons.
2) Generate simple remote query which has no WHERE clause.
3) Execute EXPLAIN of simple query, and get rows and costs estimation.
4) Set baserel->rows.
All information above are stored in baserel->fdw_private to use them
in subsequent GetForeignPaths.
If ANALYZE of foreign tables is supported, we can postpone 2) and 3)
to GetForeignPaths.
GetForeignPaths
1) Repeat for each possible remote query:
1-1) Generate remote query, such as with-WHERE and with-ORDER BY.
1-2) Execute EXPLAIN of generated query, and get costs estimation
(rows estimation is ignored because it's useless in planning).
1-3) Call add_path and create_foreignscan_path for the query.
GetForeignPlan
1) Create fdw_exprs from baserestrictinfo, with removing clauses
which are pushed down by selected path.
Regards,
--
Shigeru Hanada
(2012/03/09 1:18), Tom Lane wrote:
I've been looking at this patch a little bit over the past day or so.
I'm pretty unhappy with deparse.c --- it seems like a real kluge,
inefficient and full of corner-case bugs. After some thought I believe
that you're ultimately going to have to abandon depending on ruleutils.c
for reverse-listing services, and it would be best to bite that bullet
now and rewrite this code from scratch.
Thanks for the review. Agreed to write own depraser for pgsql_fdw
which handles nodes which can be pushed down. Every SQL-based FDW
which constructs SQL statement for each local query would need such
module inside.
BTW, pgsql_fdw pushes only built-in objects which have no collation
effect down to remote side, because user-defined objects might have
different semantics on remote end. In future, such deparser will
need some mechanism to map local object (or expression?) to remote
one, like ROUTINE MAPPING, as discussed before. But it seems ok to
assume that built-in objects have same name and semantics on remote
end.
There are a couple of other points that make me think we need to revisit
the PlanForeignScan API definition some more, too. First, deparse.c is
far from cheap. While this doesn't matter greatly as long as there's
only one possible path for a foreign table, as soon as you want to
create more than one it's going to be annoying to do all that work N
times and then throw away N-1 of the results.
Indeed deprase.c is not cheap, but I think that pgsql_fdw can avoid
redundant works by deparsing SQL statement separately, unless we need
to consider join-push-down. Possible parts are SELECT, FROM, WHERE
and ORDER BY clauses. Simplest path uses SELECT and FROM, and other
paths can be built by copying necessary clauses into individual
buffers.
Comments to the rest part are in my another reply to your recent post.
Regards,
--
Shigeru Hanada
(2012/03/06 23:47), Albe Laurenz wrote:
Shigeru Hanada wrote:
Connection should be closed only when the trigger is a
top level transaction and it's aborting, but isTopLevel flag was not
checked. I fixed the bug and added regression tests for such cases.I wondered about that - is it really necessary to close the remote
connection? Wouldn't a ROLLBACK on the remote connection be good enough?
Rolling back remote transaction seems enough, when the error comes
from local reason and remote connection is still available. However,
I'd rather disconnect always to keep error handling simple and
centralized in cleanup_connection.
Regards,
--
Shigeru Hanada
Shigeru Hanada <shigeru.hanada@gmail.com> writes:
I've not read whole of the patch yet, but I have basic questions.
1) IIUC, GetForeignRelSize should set baserel->rows to the number of
rows the ForeignScan node returns to upper node, but not the number
of rows FDW returns to core executor, right?
It should be the number of rows estimated to pass the baserestrictinfo
restriction clauses, so yeah, not the same as what the FDW would return,
except in cases where all the restriction clauses are handled internally
by the FDW.
BTW, once Fujita-san's ANALYZE support patch is merged, we will be
able to get rows estimatation easily by calling clauselist_selectivity
with baserel->tuples and baserestrictinfo. Otherwise, pgsql_fdw
would still need to execute EXPLAIN on remote side to get meaningful
rows estimation.
Yeah, one of the issues for that patch is how we see it coexisting with
the option of doing a remote-side EXPLAIN.
2) ISTM that pgsql_fdw needs to execute EXPLAIN on remote side for each
possible remote query to get meaningful costs estimation, and it
requires pgsql_fdw to generate SQL statements in GetForeignPaths.
I worry that I've misunderstood intention of your design because
you've mentioned postponing SQL deparsing to createplan time.
If you want to get the cost estimates that way, then yes, you'd be
needing to do some SQL-statement-construction earlier than final plan
generation. But it's not apparent to me that those statements would
necessarily be the same as, or even very similar to, what the final
queries would be. For instance, you'd probably try to reduce parameters
to constants for estimation purposes.
GetForeignPaths
1) Repeat for each possible remote query:
1-1) Generate remote query, such as with-WHERE and with-ORDER BY.
1-2) Execute EXPLAIN of generated query, and get costs estimation
(rows estimation is ignored because it's useless in planning).
Why do you say that?
regards, tom lane
Shigeru Hanada <shigeru.hanada@gmail.com> writes:
Thanks for the review. Agreed to write own depraser for pgsql_fdw
which handles nodes which can be pushed down. Every SQL-based FDW
which constructs SQL statement for each local query would need such
module inside.
Yeah. That's kind of annoying, and the first thing you think of is that
we ought to find a way to share that code somehow. But I think it's
folly to try to design a shared implementation until we have some
concrete implementations to compare. An Oracle FDW, for instance, would
need to emit SQL code with many differences in detail from pgsql_fdw.
It's not clear to me whether a shared implementation is even practical,
but for sure I don't want to try to build it before we've done some
prototype single-purpose implementations.
regards, tom lane
On Sat, Mar 10, 2012 at 11:38:51AM -0500, Tom Lane wrote:
Shigeru Hanada <shigeru.hanada@gmail.com> writes:
Thanks for the review. Agreed to write own depraser for pgsql_fdw
which handles nodes which can be pushed down. Every SQL-based FDW
which constructs SQL statement for each local query would need such
module inside.Yeah. That's kind of annoying, and the first thing you think of is that
we ought to find a way to share that code somehow. But I think it's
folly to try to design a shared implementation until we have some
concrete implementations to compare. An Oracle FDW, for instance, would
need to emit SQL code with many differences in detail from pgsql_fdw.
It's not clear to me whether a shared implementation is even practical,
but for sure I don't want to try to build it before we've done some
prototype single-purpose implementations.
FWIW, this sounds like the "compiler" mechanism in SQLalchemy for
turning SQL node trees into strings. The basic idea is you define
functions for converting nodes to strings. Stuff like "And" and "Or"
works for every database, but then "dialects" can override different
things.
So you have for Postgres: Node(foo) => $1, but to other databases
perhaps :field1. But most of the other code can be shared..
http://docs.sqlalchemy.org/en/latest/core/compiler.html
In my experience it works well for generating custom constructs. They
have compilers for 11 different database engines, so it seems flexible
enough. Mind you, they also handle DDL mapping (where most of the
variation is) and datatype translations, which seems a lot further than
we need here.
Have a nice day,
--
Martijn van Oosterhout <kleptog@svana.org> http://svana.org/kleptog/
He who writes carelessly confesses thereby at the very outset that he does
not attach much importance to his own thoughts.
-- Arthur Schopenhauer
(2012/03/09 23:48), Tom Lane wrote:
Etsuro Fujita<fujita.etsuro@lab.ntt.co.jp> writes:
Thank you for your answer.
1. FilefdwPlanState.pages and FileFdwPlanState.ntuples seems redundant.
Why not use RelOptInfo.pages and RelOptInfo.tuples?I intentionally avoided setting RelOptInfo.pages because that would have
other effects on planning (cf total_table_pages or whatever it's
called). It's possible that that would actually be desirable, depending
on whether you think the external file should be counted as part of the
query's disk-access footprint; but it would take some investigation to
conclude that, which I didn't feel like doing right now. Likewise, I'm
not sure offhand what side effects might occur from using
RelOptInfo.tuples, and didn't want to change file_fdw's behavior without
more checking.
OK
2. IMHO RelOptInfo.fdw_private seems confusing. How about renaming it
to e.g., RelOptInfo.fdw_state?Why is that better? It seems just as open to confusion with another
field (ie, the execution-time fdw_state).
I thought the risk. However, I feel that the naming of
RelOptInfo.fdw_state is not so bad because it is used only at the query
planning time, not used along with the execution-time fdw_private. The
naming of RelOptInfo.fdw_private seems as open to confusion to me
because it would have to be used along with Path.fdw_private or
Plan.fdw_private in FDW's functions at the planning time, while I guess
that the contents of RelOptInfo.fdw_private are relatively far from the
ones of fdw_private of Path and Plan.
Best regards,
Etsuro Fujita
(2012/03/12 13:04), Etsuro Fujita wrote:
(2012/03/09 23:48), Tom Lane wrote:
Etsuro Fujita<fujita.etsuro@lab.ntt.co.jp> writes:
2. IMHO RelOptInfo.fdw_private seems confusing. How about renaming it
to e.g., RelOptInfo.fdw_state?Why is that better? It seems just as open to confusion with another
field (ie, the execution-time fdw_state).I thought the risk. However, I feel that the naming of
RelOptInfo.fdw_state is not so bad because it is used only at the query
planning time, not used along with the execution-time fdw_private.
I wrote the execution-time fdw_private by mistake. I meant the
execution-time fdw_state. I'm sorry about that.
Best regards,
Etsuro Fujita
Tom Lane wrote:
Shigeru Hanada <shigeru.hanada@gmail.com> writes:
Thanks for the review. Agreed to write own depraser for pgsql_fdw
which handles nodes which can be pushed down. Every SQL-based FDW
which constructs SQL statement for each local query would need such
module inside.Yeah. That's kind of annoying, and the first thing you think of is
that
we ought to find a way to share that code somehow. But I think it's
folly to try to design a shared implementation until we have some
concrete implementations to compare. An Oracle FDW, for instance,
would
need to emit SQL code with many differences in detail from pgsql_fdw.
It's not clear to me whether a shared implementation is even
practical,
but for sure I don't want to try to build it before we've done some
prototype single-purpose implementations.
Having written something like that for Oracle, I tend to share that
opinion. Anything general-purpose enough to cater for every whim and
oddity of the remote system would probably be so unwieldy that it
wouldn't be much easier to use it than to write the whole thing from
scratch. To illustrate this, a few examples from the Oracle case:
- Empty strings have different semantics in Oracle (to wit, they mean
NULL).
So you can push down all string constants except empty strings.
- Oracle can only represent intervals with just year and month
or with just day of month and smaller fields. So you can either
punt on intervals or translate only the ones that fit the bill.
- You can push down "-" for date arithmetic except when both
operands on the Oracle side are of type DATE, because that would
result in a NUMERIC value (number of days between).
Yours,
Laurenz Albe
I fixed pgsql_fdw to use new FDW API. Also I fixed to use own deparser
to generate remote queries, so these patch don't contain changes for
backend codes any more. Now planning foreign scan is done in the steps
below:
1) in GerForeignRelSize, pgsql_fdw generates simple SELECT statement
which has no WHERE clause from given information such as PlannerInfo and
RelOptInfo. This query string is used to execute remote EXPLAIN in
order to estimate number of rows which will be returned from the scan.
Remote EXPLAIN doesn't return such number directly, so pgsql_fdw calls
set_baserel_size_estimates function which calculates number of rows with
selectivity of quals and reltuples. It also estimates width, but
currently no statistic is available for foreign tables, so pgsql_fdw
overrides it with width value provided by remote EXPLAIN. If we support
ANALYZE for foreign tables, we would be able to defer EXPLAIN until
GetForeignPaths, because local statistics are enough information to
estimate number of rows returned by the scan.
2) in GetForeignPaths, first, pgsql_fdw considers a very simple Path
which doesn't push any expression down to remote end. It is like
SeqScan for regular tables. Backend will filter the result with all
quals in baserestrictinfo. In push-down supported version, pgsql_fdw
also considers another Path which pushes conditions as much as possible.
This would reduce the amount of data transfer, and clocks used to
convert strings to tuples at local side. pgsql_fdw emits another
EXPLAIN for this path, though we might be able to omit. Planner can
choose either path with basis of their costs. If pgsql_fdw can know
that the remote table is indexed, pgsql_fdw would be able to consider
sorted Path for each remote index.
3) in GetForeignPlan, pgsql_fdw creates only one ForeignScan node from
given best_path. Currently pgsql_fdw uses SQL-level cursor in order to
avoid out-of-memory by huge result set, so we need to construct several
SQL statements for the plan. Old implementation has created SQL
statements in Path phase, but now it's deferred until Plan phase. This
change would avoid possible unnecessary string operation. I worry that
I've misunderstood the purpose of fdw_exprs...
Although the patches are still WIP, especially in WHERE push-down part,
but I'd like to post them so that I can get feedback of the design as
soon as possible.
(2012/03/11 1:34), Tom Lane wrote:
1) IIUC, GetForeignRelSize should set baserel->rows to the number of
rows the ForeignScan node returns to upper node, but not the number
of rows FDW returns to core executor, right?It should be the number of rows estimated to pass the baserestrictinfo
restriction clauses, so yeah, not the same as what the FDW would return,
except in cases where all the restriction clauses are handled internally
by the FDW.BTW, once Fujita-san's ANALYZE support patch is merged, we will be
able to get rows estimatation easily by calling clauselist_selectivity
with baserel->tuples and baserestrictinfo. Otherwise, pgsql_fdw
would still need to execute EXPLAIN on remote side to get meaningful
rows estimation.Yeah, one of the issues for that patch is how we see it coexisting with
the option of doing a remote-side EXPLAIN.
It seems not so easy to determine whether remote EXPLAIN is better from
local statistics. An easy way is having a per-relation FDW option like
"use_local_stats" or something for pgsql_fdw, but it doesn't sound right
because other FDWs have same problem...
2) ISTM that pgsql_fdw needs to execute EXPLAIN on remote side for each
possible remote query to get meaningful costs estimation, and it
requires pgsql_fdw to generate SQL statements in GetForeignPaths.
I worry that I've misunderstood intention of your design because
you've mentioned postponing SQL deparsing to createplan time.If you want to get the cost estimates that way, then yes, you'd be
needing to do some SQL-statement-construction earlier than final plan
generation. But it's not apparent to me that those statements would
necessarily be the same as, or even very similar to, what the final
queries would be. For instance, you'd probably try to reduce parameters
to constants for estimation purposes.
Hm, I though that using queries same as final ones has no overhead, if
we don't need to deparse clauses redundantly. In current
implementation, WHERE clause is deparsed only once for a scan, and basis
of the query (SELLECT ... FROM ...) is also deparseed only once. Indeed
string copy is not avoidable, but I feel that's not big problem. Thoughts?
Regards,
--
Shigeru Hanada
Attachments:
pgsql_fdw_v16.patchtext/plain; name=pgsql_fdw_v16.patchDownload
diff --git a/contrib/Makefile b/contrib/Makefile
index ac0a80a..14aa433 100644
*** a/contrib/Makefile
--- b/contrib/Makefile
*************** SUBDIRS = \
*** 41,46 ****
--- 41,47 ----
pgbench \
pgcrypto \
pgrowlocks \
+ pgsql_fdw \
pgstattuple \
seg \
spi \
diff --git a/contrib/README b/contrib/README
index a1d42a1..d3fa211 100644
*** a/contrib/README
--- b/contrib/README
*************** pgrowlocks -
*** 158,163 ****
--- 158,167 ----
A function to return row locking information
by Tatsuo Ishii <ishii@sraoss.co.jp>
+ pgsql_fdw -
+ Foreign-data wrapper for external PostgreSQL servers.
+ by Shigeru Hanada <shigeru.hanada@gmail.com>
+
pgstattuple -
Functions to return statistics about "dead" tuples and free
space within a table
diff --git a/contrib/pgsql_fdw/.gitignore b/contrib/pgsql_fdw/.gitignore
index ...0854728 .
*** a/contrib/pgsql_fdw/.gitignore
--- b/contrib/pgsql_fdw/.gitignore
***************
*** 0 ****
--- 1,4 ----
+ # Generated subdirectories
+ /results/
+ *.o
+ *.so
diff --git a/contrib/pgsql_fdw/Makefile b/contrib/pgsql_fdw/Makefile
index ...6381365 .
*** a/contrib/pgsql_fdw/Makefile
--- b/contrib/pgsql_fdw/Makefile
***************
*** 0 ****
--- 1,22 ----
+ # contrib/pgsql_fdw/Makefile
+
+ MODULE_big = pgsql_fdw
+ OBJS = pgsql_fdw.o option.o deparse.o connection.o
+ PG_CPPFLAGS = -I$(libpq_srcdir)
+ SHLIB_LINK = $(libpq)
+
+ EXTENSION = pgsql_fdw
+ DATA = pgsql_fdw--1.0.sql
+
+ REGRESS = pgsql_fdw
+
+ ifdef USE_PGXS
+ PG_CONFIG = pg_config
+ PGXS := $(shell $(PG_CONFIG) --pgxs)
+ include $(PGXS)
+ else
+ subdir = contrib/pgsql_fdw
+ top_builddir = ../..
+ include $(top_builddir)/src/Makefile.global
+ include $(top_srcdir)/contrib/contrib-global.mk
+ endif
diff --git a/contrib/pgsql_fdw/connection.c b/contrib/pgsql_fdw/connection.c
index ...c971bd7 .
*** a/contrib/pgsql_fdw/connection.c
--- b/contrib/pgsql_fdw/connection.c
***************
*** 0 ****
--- 1,555 ----
+ /*-------------------------------------------------------------------------
+ *
+ * connection.c
+ * Connection management for pgsql_fdw
+ *
+ * Portions Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/connection.c
+ *
+ *-------------------------------------------------------------------------
+ */
+ #include "postgres.h"
+
+ #include "access/xact.h"
+ #include "catalog/pg_type.h"
+ #include "foreign/foreign.h"
+ #include "funcapi.h"
+ #include "libpq-fe.h"
+ #include "mb/pg_wchar.h"
+ #include "miscadmin.h"
+ #include "utils/array.h"
+ #include "utils/builtins.h"
+ #include "utils/hsearch.h"
+ #include "utils/memutils.h"
+ #include "utils/resowner.h"
+ #include "utils/tuplestore.h"
+
+ #include "pgsql_fdw.h"
+ #include "connection.h"
+
+ /* ============================================================================
+ * Connection management functions
+ * ==========================================================================*/
+
+ /*
+ * Connection cache entry managed with hash table.
+ */
+ typedef struct ConnCacheEntry
+ {
+ /* hash key must be first */
+ Oid serverid; /* oid of foreign server */
+ Oid userid; /* oid of local user */
+
+ bool use_tx; /* true when using remote transaction */
+ 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);
+ static void begin_remote_tx(PGconn *conn);
+ static void abort_remote_tx(PGconn *conn);
+
+ /*
+ * 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.
+ *
+ * When use_tx is true, remote transaction is started if caller is the only
+ * user of the connection. Isolation level of the remote transaction is same
+ * as local transaction, and remote transaction will be aborted when last
+ * user release.
+ *
+ * TODO: Note that caching connections requires a mechanism to detect change of
+ * FDW object to invalidate already established connections.
+ */
+ PGconn *
+ GetConnection(ForeignServer *server, UserMapping *user, bool use_tx)
+ {
+ bool found;
+ ConnCacheEntry *entry;
+ ConnCacheEntry key;
+
+ /* initialize connection cache if it isn't */
+ if (FSConnectionHash == NULL)
+ {
+ HASHCTL ctl;
+
+ /* hash key is a pair of oids: serverid and userid */
+ MemSet(&ctl, 0, sizeof(ctl));
+ ctl.keysize = sizeof(Oid) + sizeof(Oid);
+ ctl.entrysize = sizeof(ConnCacheEntry);
+ ctl.hash = tag_hash;
+ ctl.match = memcmp;
+ ctl.keycopy = memcpy;
+ /* allocate FSConnectionHash in the cache context */
+ ctl.hcxt = CacheMemoryContext;
+ FSConnectionHash = hash_create("Foreign Connections", 32,
+ &ctl,
+ HASH_ELEM | HASH_CONTEXT |
+ HASH_FUNCTION | HASH_COMPARE |
+ HASH_KEYCOPY);
+ }
+
+ /* Create key value for the entry. */
+ MemSet(&key, 0, sizeof(key));
+ key.serverid = server->serverid;
+ key.userid = GetOuterUserId();
+
+ /*
+ * Find cached entry for requested connection. If we couldn't find,
+ * callback function of ResourceOwner should be registered to clean the
+ * connection up on error including user interrupt.
+ */
+ entry = hash_search(FSConnectionHash, &key, HASH_ENTER, &found);
+ if (!found)
+ {
+ entry->refs = 0;
+ entry->use_tx = false;
+ entry->conn = NULL;
+ RegisterResourceReleaseCallback(cleanup_connection, entry);
+ }
+
+ /*
+ * We don't check the health of cached connection here, because it would
+ * require some overhead. Broken connection and its cache entry will be
+ * cleaned up when the connection is actually used.
+ */
+
+ /*
+ * If cache entry doesn't have connection, we have to establish new
+ * connection.
+ */
+ if (entry->conn == NULL)
+ {
+ /*
+ * Use PG_TRY block to ensure closing connection on error.
+ */
+ PG_TRY();
+ {
+ /*
+ * Connect to the foreign PostgreSQL server, and store it in cache
+ * entry to keep new connection.
+ * Note: key items of entry has already been initialized in
+ * hash_search(HASH_ENTER).
+ */
+ entry->conn = connect_pg_server(server, user);
+ }
+ PG_CATCH();
+ {
+ /* Clear connection cache entry on error case. */
+ PQfinish(entry->conn);
+ entry->refs = 0;
+ entry->use_tx = false;
+ entry->conn = NULL;
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+ }
+
+ /* Increase connection reference counter. */
+ entry->refs++;
+
+ /*
+ * If requester is the only referrer of this connection, start transaction
+ * with the same isolation level as the local transaction we are in.
+ * We need to remember whether this connection uses remote transaction to
+ * abort it when this connection is released completely.
+ */
+ if (use_tx && entry->refs == 1)
+ begin_remote_tx(entry->conn);
+ entry->use_tx = use_tx;
+
+
+ return entry->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 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. Those two object hold only libpq options.
+ * Extra 3 items are for:
+ * *) fallback_application_name
+ * *) client_encoding
+ * *) NULL termination (end marker)
+ *
+ * Note: We don't omit any parameters even target database might be older
+ * than local, because unexpected parameters are just ignored.
+ */
+ n = list_length(server->options) + list_length(user->options) + 3;
+ 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 += ExtractConnectionOptions(server->options,
+ all_keywords + n, all_values + n);
+ n += ExtractConnectionOptions(user->options,
+ all_keywords + n, all_values + n);
+ all_keywords[n] = all_values[n] = NULL;
+
+ for (i = 0, j = 0; all_keywords[i]; i++)
+ {
+ keywords[j] = all_keywords[i];
+ values[j] = all_values[i];
+ j++;
+ }
+
+ /* Use "pgsql_fdw" as fallback_application_name. */
+ keywords[j] = "fallback_application_name";
+ values[j++] = "pgsql_fdw";
+
+ /* Set client_encoding so that libpq can convert encoding properly. */
+ keywords[j] = "client_encoding";
+ values[j++] = GetDatabaseEncodingName();
+
+ keywords[j] = values[j] = NULL;
+ pfree(all_keywords);
+ pfree(all_values);
+
+ /* 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.")));
+ }
+
+ return conn;
+ }
+
+ /*
+ * Start transaction to use cursor to retrieve data separately.
+ */
+ static void
+ begin_remote_tx(PGconn *conn)
+ {
+ const char *sql = NULL; /* keep compiler quiet. */
+ PGresult *res;
+
+ switch (XactIsoLevel)
+ {
+ case XACT_READ_UNCOMMITTED:
+ case XACT_READ_COMMITTED:
+ case XACT_REPEATABLE_READ:
+ sql = "START TRANSACTION ISOLATION LEVEL REPEATABLE READ";
+ break;
+ case XACT_SERIALIZABLE:
+ sql = "START TRANSACTION ISOLATION LEVEL SERIALIZABLE";
+ break;
+ default:
+ elog(ERROR, "unexpected isolation level: %d", XactIsoLevel);
+ break;
+ }
+
+ elog(DEBUG3, "starting remote transaction with \"%s\"", sql);
+
+ res = PQexec(conn, sql);
+ if (PQresultStatus(res) != PGRES_COMMAND_OK)
+ {
+ PQclear(res);
+ elog(ERROR, "could not start transaction: %s", PQerrorMessage(conn));
+ }
+ PQclear(res);
+ }
+
+ static void
+ abort_remote_tx(PGconn *conn)
+ {
+ PGresult *res;
+
+ elog(DEBUG3, "aborting remote transaction");
+
+ res = PQexec(conn, "ABORT TRANSACTION");
+ if (PQresultStatus(res) != PGRES_COMMAND_OK)
+ {
+ PQclear(res);
+ elog(ERROR, "could not abort transaction: %s", PQerrorMessage(conn));
+ }
+ PQclear(res);
+ }
+
+ /*
+ * 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 sequentially 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)
+ {
+ hash_seq_term(&scan);
+ break;
+ }
+ }
+
+ /* If the released connection was an orphan, just close it. */
+ if (entry == NULL)
+ {
+ PQfinish(conn);
+ return;
+ }
+
+ /*
+ * If this connection uses remote transaction and caller is the last user,
+ * abort remote transaction and forget about it.
+ */
+ if (entry->use_tx && entry->refs == 1)
+ {
+ abort_remote_tx(conn);
+ entry->use_tx = false;
+ }
+
+ /*
+ * Decrease reference counter of this connection. Even if the caller was
+ * the last referrer, we don't unregister it from cache.
+ */
+ entry->refs--;
+ }
+
+ /*
+ * 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 need to clean up at end of subtransactions, because they might
+ * be recovered to consistent state with savepoints.
+ */
+ if (!isTopLevel)
+ return;
+
+ /*
+ * Here, it must be after abort of top level transaction. Disconnect and
+ * forget every referrer.
+ */
+ if (entry->conn != NULL)
+ {
+ PQfinish(entry->conn);
+ entry->refs = 0;
+ entry->conn = NULL;
+ }
+ }
+
+ /*
+ * Get list of connections currently active.
+ */
+ Datum pgsql_fdw_get_connections(PG_FUNCTION_ARGS);
+ PG_FUNCTION_INFO_V1(pgsql_fdw_get_connections);
+ Datum
+ pgsql_fdw_get_connections(PG_FUNCTION_ARGS)
+ {
+ ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+ HASH_SEQ_STATUS scan;
+ ConnCacheEntry *entry;
+ MemoryContext oldcontext = CurrentMemoryContext;
+ Tuplestorestate *tuplestore;
+ TupleDesc tupdesc;
+
+ /* We return list of connection with storing them in a Tuplestore. */
+ rsinfo->returnMode = SFRM_Materialize;
+ rsinfo->setResult = NULL;
+ rsinfo->setDesc = NULL;
+
+ /* Create tuplestore and copy of TupleDesc in per-query context. */
+ MemoryContextSwitchTo(rsinfo->econtext->ecxt_per_query_memory);
+
+ tupdesc = CreateTemplateTupleDesc(2, false);
+ TupleDescInitEntry(tupdesc, 1, "srvid", OIDOID, -1, 0);
+ TupleDescInitEntry(tupdesc, 2, "usesysid", OIDOID, -1, 0);
+ rsinfo->setDesc = tupdesc;
+
+ tuplestore = tuplestore_begin_heap(false, false, work_mem);
+ rsinfo->setResult = tuplestore;
+
+ MemoryContextSwitchTo(oldcontext);
+
+ /*
+ * We need to scan sequentially since we use the address to find
+ * appropriate PGconn from the hash table.
+ */
+ if (FSConnectionHash != NULL)
+ {
+ hash_seq_init(&scan, FSConnectionHash);
+ while ((entry = (ConnCacheEntry *) hash_seq_search(&scan)))
+ {
+ Datum values[2];
+ bool nulls[2];
+ HeapTuple tuple;
+
+ /* Ignore inactive connections */
+ if (PQstatus(entry->conn) != CONNECTION_OK)
+ continue;
+
+ /*
+ * Ignore other users' connections if current user isn't a
+ * superuser.
+ */
+ if (!superuser() && entry->userid != GetUserId())
+ continue;
+
+ values[0] = ObjectIdGetDatum(entry->serverid);
+ values[1] = ObjectIdGetDatum(entry->userid);
+ nulls[0] = false;
+ nulls[1] = false;
+
+ tuple = heap_formtuple(tupdesc, values, nulls);
+ tuplestore_puttuple(tuplestore, tuple);
+ }
+ }
+ tuplestore_donestoring(tuplestore);
+
+ PG_RETURN_VOID();
+ }
+
+ /*
+ * Discard persistent connection designated by given connection name.
+ */
+ Datum pgsql_fdw_disconnect(PG_FUNCTION_ARGS);
+ PG_FUNCTION_INFO_V1(pgsql_fdw_disconnect);
+ Datum
+ pgsql_fdw_disconnect(PG_FUNCTION_ARGS)
+ {
+ Oid serverid = PG_GETARG_OID(0);
+ Oid userid = PG_GETARG_OID(1);
+ ConnCacheEntry key;
+ ConnCacheEntry *entry = NULL;
+ bool found;
+
+ /* Non-superuser can't discard other users' connection. */
+ if (!superuser() && userid != GetOuterUserId())
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_RESOURCES),
+ errmsg("only superuser can discard other user's connection")));
+
+ /*
+ * If no connection has been established, or no such connections, just
+ * return "NG" to indicate nothing has done.
+ */
+ if (FSConnectionHash == NULL)
+ PG_RETURN_TEXT_P(cstring_to_text("NG"));
+
+ key.serverid = serverid;
+ key.userid = userid;
+ entry = hash_search(FSConnectionHash, &key, HASH_FIND, &found);
+ if (!found)
+ PG_RETURN_TEXT_P(cstring_to_text("NG"));
+
+ /* Discard cached connection, and clear reference counter. */
+ PQfinish(entry->conn);
+ entry->refs = 0;
+ entry->conn = NULL;
+
+ PG_RETURN_TEXT_P(cstring_to_text("OK"));
+ }
diff --git a/contrib/pgsql_fdw/connection.h b/contrib/pgsql_fdw/connection.h
index ...4427f56 .
*** a/contrib/pgsql_fdw/connection.h
--- b/contrib/pgsql_fdw/connection.h
***************
*** 0 ****
--- 1,25 ----
+ /*-------------------------------------------------------------------------
+ *
+ * connection.h
+ * Connection management for pgsql_fdw
+ *
+ * Portions Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_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, bool use_tx);
+ void ReleaseConnection(PGconn *conn);
+
+ #endif /* CONNECTION_H */
diff --git a/contrib/pgsql_fdw/deparse.c b/contrib/pgsql_fdw/deparse.c
index ...beb62fe .
*** a/contrib/pgsql_fdw/deparse.c
--- b/contrib/pgsql_fdw/deparse.c
***************
*** 0 ****
--- 1,290 ----
+ /*-------------------------------------------------------------------------
+ *
+ * deparse.c
+ * query deparser for PostgreSQL
+ *
+ * Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/deparse.c
+ *
+ *-------------------------------------------------------------------------
+ */
+ #include "postgres.h"
+
+ #include "access/transam.h"
+ #include "catalog/pg_class.h"
+ #include "commands/defrem.h"
+ #include "foreign/foreign.h"
+ #include "lib/stringinfo.h"
+ #include "nodes/nodeFuncs.h"
+ #include "nodes/nodes.h"
+ #include "nodes/makefuncs.h"
+ #include "optimizer/clauses.h"
+ #include "optimizer/var.h"
+ #include "parser/parsetree.h"
+ #include "utils/builtins.h"
+ #include "utils/lsyscache.h"
+
+ #include "pgsql_fdw.h"
+
+ /*
+ * Context for walk-through the expression tree.
+ */
+ typedef struct foreign_executable_cxt
+ {
+ PlannerInfo *root;
+ RelOptInfo *foreignrel;
+ } foreign_executable_cxt;
+
+ /*
+ * Get string representation which can be used in SQL statement from a node.
+ */
+ static void deparseRelation(StringInfo buf, Oid relid, PlannerInfo *root,
+ bool need_prefix);
+ static void deparseVar(StringInfo buf, Var *node, PlannerInfo *root,
+ RelOptInfo *baserel, bool need_prefix);
+
+ /*
+ * Deparse query representation into SQL statement which suits for remote
+ * PostgreSQL server. This function just creates simple query string which
+ * consists of only SELECT and FROM clause.
+ */
+ char *
+ deparseSimpleSql(Oid relid,
+ PlannerInfo *root,
+ RelOptInfo *baserel,
+ ForeignTable *table)
+ {
+ StringInfoData foreign_relname;
+ StringInfoData sql; /* builder for SQL statement */
+ bool first;
+ AttrNumber attr;
+ List *attr_used = NIL; /* List of AttNumber used in the query */
+
+ initStringInfo(&sql);
+ initStringInfo(&foreign_relname);
+
+ /*
+ * First of all, determine which column should be retrieved for this scan.
+ *
+ * We do this before deparsing SELECT clause because attributes which are
+ * not used in neither reltargetlist nor baserel->baserestrictinfo, quals
+ * evaluated on local, can be replaced with literal "NULL" in the SELECT
+ * clause to reduce overhead of tuple handling tuple and data transfer.
+ */
+ if (baserel->baserestrictinfo != NIL)
+ {
+ ListCell *lc;
+
+ foreach (lc, baserel->baserestrictinfo)
+ {
+ RestrictInfo *ri = (RestrictInfo *) lfirst(lc);
+ List *attrs;
+
+ /*
+ * We need to know which attributes are used in qual evaluated
+ * on the local server, because they should be listed in the
+ * SELECT clause of remote query. We can ignore attributes
+ * which are referenced only in ORDER BY/GROUP BY clause because
+ * such attributes has already been kept in reltargetlist.
+ */
+ attrs = pull_var_clause((Node *) ri->clause,
+ PVC_RECURSE_AGGREGATES,
+ PVC_RECURSE_PLACEHOLDERS);
+ attr_used = list_union(attr_used, attrs);
+ }
+ }
+
+ /*
+ * deparse SELECT clause
+ *
+ * List attributes which are in either target list or local restriction.
+ * Unused attributes are replaced with a literal "NULL" for optimization.
+ *
+ * Note that nothing is added for dropped columns, though tuple constructor
+ * function requires entries for dropped columns. Such entries must be
+ * initialized with NULL before calling tuple constructor.
+ */
+ appendStringInfo(&sql, "SELECT ");
+ attr_used = list_union(attr_used, baserel->reltargetlist);
+ first = true;
+ for (attr = 1; attr <= baserel->max_attr; attr++)
+ {
+ RangeTblEntry *rte = root->simple_rte_array[baserel->relid];
+ Var *var = NULL;
+ ListCell *lc;
+
+ /* Ignore dropped attributes. */
+ if (get_rte_attribute_is_dropped(rte, attr))
+ continue;
+
+ if (!first)
+ appendStringInfo(&sql, ", ");
+ first = false;
+
+ /*
+ * We use linear search here, but it wouldn't be problem since
+ * attr_used seems to not become so large.
+ */
+ foreach (lc, attr_used)
+ {
+ var = lfirst(lc);
+ if (var->varattno == attr)
+ break;
+ var = NULL;
+ }
+ if (var != NULL)
+ deparseVar(&sql, var, root, baserel, false);
+ else
+ appendStringInfo(&sql, "NULL");
+ }
+ appendStringInfoChar(&sql, ' ');
+
+ /*
+ * deparse FROM clause, including alias if any
+ */
+ appendStringInfo(&sql, "FROM ");
+ deparseRelation(&sql, table->relid, root, true);
+
+ elog(DEBUG3, "Remote SQL: %s", sql.data);
+ return sql.data;
+ }
+
+ /*
+ * Deparse node into buf, with relation qualifier if need_prefix was true. If
+ * node is a column of a foreign table, use value of colname FDW option (if any)
+ * instead of attribute name.
+ */
+ static void
+ deparseVar(StringInfo buf,
+ Var *node,
+ PlannerInfo *root,
+ RelOptInfo *baserel,
+ bool need_prefix)
+ {
+ RangeTblEntry *rte;
+ char *colname = NULL;
+ const char *q_colname = NULL;
+ List *options;
+ ListCell *lc;
+
+ /* node must not be any of OUTER_VAR,INNER_VAR and INDEX_VAR. */
+ Assert(node->varno >= 1 && node->varno <= root->simple_rel_array_size);
+
+ /* Get RangeTblEntry from array in PlannerInfo. */
+ rte = root->simple_rte_array[node->varno];
+
+ /*
+ * If the node is a column of a foreign table, and it has colname FDW
+ * option, use its value.
+ */
+ if (rte->relkind == RELKIND_FOREIGN_TABLE)
+ {
+ options = GetForeignColumnOptions(rte->relid, node->varattno);
+ foreach(lc, options)
+ {
+ DefElem *def = (DefElem *) lfirst(lc);
+
+ if (strcmp(def->defname, "colname") == 0)
+ {
+ colname = defGetString(def);
+ break;
+ }
+ }
+ }
+
+ /*
+ * If the node refers a column of a regular table or it doesn't have colname
+ * FDW option, use attribute name.
+ */
+ if (colname == NULL)
+ colname = get_attname(rte->relid, node->varattno);
+
+ if (need_prefix)
+ {
+ char *aliasname;
+ const char *q_aliasname;
+
+ if (rte->eref != NULL && rte->eref->aliasname != NULL)
+ aliasname = rte->eref->aliasname;
+ else if (rte->alias != NULL && rte->alias->aliasname != NULL)
+ aliasname = rte->alias->aliasname;
+
+ q_aliasname = quote_identifier(aliasname);
+ appendStringInfo(buf, "%s.", q_aliasname);
+ }
+
+ q_colname = quote_identifier(colname);
+ appendStringInfo(buf, "%s", q_colname);
+ }
+
+ /*
+ * Deparse table which has relid as oid into buf, with schema qualifier if
+ * need_prefix was true. If relid points a foreign table, use value of relname
+ * FDW option (if any) instead of relation's name. Similarly, nspname FDW
+ * option overrides schema name.
+ */
+ static void
+ deparseRelation(StringInfo buf,
+ Oid relid,
+ PlannerInfo *root,
+ bool need_prefix)
+ {
+ int i;
+ RangeTblEntry *rte = NULL;
+ ListCell *lc;
+ const char *nspname = NULL; /* plain namespace name */
+ const char *relname = NULL; /* plain relation name */
+ const char *q_nspname; /* quoted namespace name */
+ const char *q_relname; /* quoted relation name */
+
+ /* Find RangeTblEntry for the relation from PlannerInfo. */
+ for (i = 1; i < root->simple_rel_array_size; i++)
+ {
+ if (root->simple_rte_array[i]->relid == relid)
+ {
+ rte = root->simple_rte_array[i];
+ break;
+ }
+ }
+ if (rte == NULL)
+ elog(ERROR, "relation with OID %u is not used in the query", relid);
+
+ /* If target is a foreign table, obtain additional catalog information. */
+ if (rte->relkind == RELKIND_FOREIGN_TABLE)
+ {
+ ForeignTable *table = GetForeignTable(rte->relid);
+
+ /*
+ * Use value of FDW options if any, instead of the name of object
+ * itself.
+ */
+ foreach(lc, table->options)
+ {
+ DefElem *def = (DefElem *) lfirst(lc);
+
+ if (need_prefix && strcmp(def->defname, "nspname") == 0)
+ nspname = defGetString(def);
+ else if (strcmp(def->defname, "relname") == 0)
+ relname = defGetString(def);
+ }
+ }
+
+ /* Quote each identifier, if necessary. */
+ if (need_prefix)
+ {
+ if (nspname == NULL)
+ nspname = get_namespace_name(get_rel_namespace(relid));
+ q_nspname = quote_identifier(nspname);
+ }
+
+ if (relname == NULL)
+ relname = get_rel_name(relid);
+ q_relname = quote_identifier(relname);
+
+ /* Construct relation reference into the buffer. */
+ if (need_prefix)
+ appendStringInfo(buf, "%s.", q_nspname);
+ appendStringInfo(buf, "%s", q_relname);
+ }
diff --git a/contrib/pgsql_fdw/expected/pgsql_fdw.out b/contrib/pgsql_fdw/expected/pgsql_fdw.out
index ...5bac241 .
*** a/contrib/pgsql_fdw/expected/pgsql_fdw.out
--- b/contrib/pgsql_fdw/expected/pgsql_fdw.out
***************
*** 0 ****
--- 1,592 ----
+ -- ===================================================================
+ -- create FDW objects
+ -- ===================================================================
+ -- Clean up in case a prior regression run failed
+ -- Suppress NOTICE messages when roles don't exist
+ SET client_min_messages TO 'error';
+ DROP ROLE IF EXISTS pgsql_fdw_user;
+ RESET client_min_messages;
+ CREATE ROLE pgsql_fdw_user LOGIN SUPERUSER;
+ SET SESSION AUTHORIZATION 'pgsql_fdw_user';
+ CREATE EXTENSION pgsql_fdw;
+ CREATE SERVER loopback1 FOREIGN DATA WRAPPER pgsql_fdw;
+ CREATE SERVER loopback2 FOREIGN DATA WRAPPER pgsql_fdw
+ OPTIONS (dbname 'contrib_regression');
+ CREATE USER MAPPING FOR public SERVER loopback1
+ OPTIONS (user 'value', password 'value');
+ CREATE USER MAPPING FOR pgsql_fdw_user SERVER loopback2;
+ CREATE FOREIGN TABLE ft1 (
+ c0 int,
+ c1 int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10)
+ ) SERVER loopback2;
+ ALTER FOREIGN TABLE ft1 DROP COLUMN c0;
+ CREATE FOREIGN TABLE ft2 (
+ c0 int,
+ c1 int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10)
+ ) SERVER loopback2;
+ ALTER FOREIGN TABLE ft2 DROP COLUMN c0;
+ -- ===================================================================
+ -- create objects used through FDW
+ -- ===================================================================
+ CREATE SCHEMA "S 1";
+ CREATE TABLE "S 1"."T 1" (
+ "C 1" int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10),
+ CONSTRAINT t1_pkey PRIMARY KEY ("C 1")
+ );
+ NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "t1_pkey" for table "T 1"
+ CREATE TABLE "S 1"."T 2" (
+ c1 int NOT NULL,
+ c2 text,
+ CONSTRAINT t2_pkey PRIMARY KEY (c1)
+ );
+ NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "t2_pkey" for table "T 2"
+ BEGIN;
+ TRUNCATE "S 1"."T 1";
+ INSERT INTO "S 1"."T 1"
+ SELECT id,
+ id % 10,
+ to_char(id, 'FM00000'),
+ '1970-01-01'::timestamptz + ((id % 100) || ' days')::interval,
+ '1970-01-01'::timestamp + ((id % 100) || ' days')::interval,
+ id % 10,
+ id % 10
+ FROM generate_series(1, 1000) id;
+ TRUNCATE "S 1"."T 2";
+ INSERT INTO "S 1"."T 2"
+ SELECT id,
+ 'AAA' || to_char(id, 'FM000')
+ FROM generate_series(1, 100) id;
+ COMMIT;
+ -- ===================================================================
+ -- tests for pgsql_fdw_validator
+ -- ===================================================================
+ ALTER FOREIGN DATA WRAPPER pgsql_fdw OPTIONS (host 'value'); -- ERROR
+ ERROR: invalid option "host"
+ HINT: Valid options in this context are:
+ -- requiressl, krbsrvname and gsslib are omitted because they depend on
+ -- configure option
+ ALTER SERVER loopback1 OPTIONS (
+ authtype 'value',
+ service 'value',
+ connect_timeout 'value',
+ dbname 'value',
+ host 'value',
+ hostaddr 'value',
+ port 'value',
+ --client_encoding 'value',
+ tty 'value',
+ options 'value',
+ application_name 'value',
+ --fallback_application_name 'value',
+ keepalives 'value',
+ keepalives_idle 'value',
+ keepalives_interval 'value',
+ -- requiressl 'value',
+ sslcompression 'value',
+ sslmode 'value',
+ sslcert 'value',
+ sslkey 'value',
+ sslrootcert 'value',
+ sslcrl 'value'
+ --requirepeer 'value',
+ -- krbsrvname 'value',
+ -- gsslib 'value',
+ --replication 'value'
+ );
+ ALTER SERVER loopback1 OPTIONS (user 'value'); -- ERROR
+ ERROR: invalid option "user"
+ HINT: Valid options in this context are: authtype, service, connect_timeout, dbname, host, hostaddr, port, tty, options, application_name, keepalives, keepalives_idle, keepalives_interval, keepalives_count, requiressl, sslcompression, sslmode, sslcert, sslkey, sslrootcert, sslcrl, requirepeer, krbsrvname, gsslib, fetch_count
+ ALTER SERVER loopback2 OPTIONS (ADD fetch_count '2');
+ ALTER USER MAPPING FOR public SERVER loopback1
+ OPTIONS (DROP user, DROP password);
+ ALTER USER MAPPING FOR public SERVER loopback1
+ OPTIONS (host 'value'); -- ERROR
+ ERROR: invalid option "host"
+ HINT: Valid options in this context are: user, password
+ ALTER FOREIGN TABLE ft1 OPTIONS (nspname 'S 1', relname 'T 1');
+ ALTER FOREIGN TABLE ft2 OPTIONS (nspname 'S 1', relname 'T 1', fetch_count '100');
+ ALTER FOREIGN TABLE ft1 OPTIONS (invalid 'value'); -- ERROR
+ ERROR: invalid option "invalid"
+ HINT: Valid options in this context are: nspname, relname, fetch_count
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count 'a'); -- ERROR
+ ERROR: invalid value for fetch_count: "a"
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count '0'); -- ERROR
+ ERROR: invalid value for fetch_count: "0"
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count '-1'); -- ERROR
+ ERROR: invalid value for fetch_count: "-1"
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (invalid 'value'); -- ERROR
+ ERROR: invalid option "invalid"
+ HINT: Valid options in this context are: colname
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (colname 'C 1');
+ ALTER FOREIGN TABLE ft2 ALTER COLUMN c1 OPTIONS (colname 'C 1');
+ \dew+
+ List of foreign-data wrappers
+ Name | Owner | Handler | Validator | Access privileges | FDW Options | Description
+ -----------+----------------+-------------------+---------------------+-------------------+-------------+-------------
+ pgsql_fdw | pgsql_fdw_user | pgsql_fdw_handler | pgsql_fdw_validator | | |
+ (1 row)
+
+ \des+
+ List of foreign servers
+ Name | Owner | Foreign-data wrapper | Access privileges | Type | Version | FDW Options | Description
+ -----------+----------------+----------------------+-------------------+------+---------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+-------------
+ loopback1 | pgsql_fdw_user | pgsql_fdw | | | | (authtype 'value', service 'value', connect_timeout 'value', dbname 'value', host 'value', hostaddr 'value', port 'value', tty 'value', options 'value', application_name 'value', keepalives 'value', keepalives_idle 'value', keepalives_interval 'value', sslcompression 'value', sslmode 'value', sslcert 'value', sslkey 'value', sslrootcert 'value', sslcrl 'value') |
+ loopback2 | pgsql_fdw_user | pgsql_fdw | | | | (dbname 'contrib_regression', fetch_count '2') |
+ (2 rows)
+
+ \deu+
+ List of user mappings
+ Server | User name | FDW Options
+ -----------+----------------+-------------
+ loopback1 | public |
+ loopback2 | pgsql_fdw_user |
+ (2 rows)
+
+ \det+
+ List of foreign tables
+ Schema | Table | Server | FDW Options | Description
+ --------+-------+-----------+---------------------------------------------------+-------------
+ public | ft1 | loopback2 | (nspname 'S 1', relname 'T 1') |
+ public | ft2 | loopback2 | (nspname 'S 1', relname 'T 1', fetch_count '100') |
+ (2 rows)
+
+ -- ===================================================================
+ -- simple queries
+ -- ===================================================================
+ -- single table, with/without alias
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
+ QUERY PLAN
+ ------------------------------------------------------------------------------------------------------------------------------
+ Limit
+ Output: c1, c2, c3, c4, c5, c6, c7
+ -> Sort
+ Output: c1, c2, c3, c4, c5, c6, c7
+ Sort Key: ft1.c3, ft1.c1
+ -> Foreign Scan on public.ft1
+ Output: c1, c2, c3, c4, c5, c6, c7
+ Remote SQL: DECLARE pgsql_fdw_cursor_0 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (8 rows)
+
+ SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ -----+----+-------+------------------------------+--------------------------+----+------------
+ 101 | 1 | 00101 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970 | 1 | 1
+ 102 | 2 | 00102 | Sat Jan 03 00:00:00 1970 PST | Sat Jan 03 00:00:00 1970 | 2 | 2
+ 103 | 3 | 00103 | Sun Jan 04 00:00:00 1970 PST | Sun Jan 04 00:00:00 1970 | 3 | 3
+ 104 | 4 | 00104 | Mon Jan 05 00:00:00 1970 PST | Mon Jan 05 00:00:00 1970 | 4 | 4
+ 105 | 5 | 00105 | Tue Jan 06 00:00:00 1970 PST | Tue Jan 06 00:00:00 1970 | 5 | 5
+ 106 | 6 | 00106 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6
+ 107 | 7 | 00107 | Thu Jan 08 00:00:00 1970 PST | Thu Jan 08 00:00:00 1970 | 7 | 7
+ 108 | 8 | 00108 | Fri Jan 09 00:00:00 1970 PST | Fri Jan 09 00:00:00 1970 | 8 | 8
+ 109 | 9 | 00109 | Sat Jan 10 00:00:00 1970 PST | Sat Jan 10 00:00:00 1970 | 9 | 9
+ 110 | 0 | 00110 | Sun Jan 11 00:00:00 1970 PST | Sun Jan 11 00:00:00 1970 | 0 | 0
+ (10 rows)
+
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+ QUERY PLAN
+ ------------------------------------------------------------------------------------------------------------------------------
+ Limit
+ Output: c1, c2, c3, c4, c5, c6, c7
+ -> Sort
+ Output: c1, c2, c3, c4, c5, c6, c7
+ Sort Key: t1.c3, t1.c1
+ -> Foreign Scan on public.ft1 t1
+ Output: c1, c2, c3, c4, c5, c6, c7
+ Remote SQL: DECLARE pgsql_fdw_cursor_2 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (8 rows)
+
+ SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ -----+----+-------+------------------------------+--------------------------+----+------------
+ 101 | 1 | 00101 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970 | 1 | 1
+ 102 | 2 | 00102 | Sat Jan 03 00:00:00 1970 PST | Sat Jan 03 00:00:00 1970 | 2 | 2
+ 103 | 3 | 00103 | Sun Jan 04 00:00:00 1970 PST | Sun Jan 04 00:00:00 1970 | 3 | 3
+ 104 | 4 | 00104 | Mon Jan 05 00:00:00 1970 PST | Mon Jan 05 00:00:00 1970 | 4 | 4
+ 105 | 5 | 00105 | Tue Jan 06 00:00:00 1970 PST | Tue Jan 06 00:00:00 1970 | 5 | 5
+ 106 | 6 | 00106 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6
+ 107 | 7 | 00107 | Thu Jan 08 00:00:00 1970 PST | Thu Jan 08 00:00:00 1970 | 7 | 7
+ 108 | 8 | 00108 | Fri Jan 09 00:00:00 1970 PST | Fri Jan 09 00:00:00 1970 | 8 | 8
+ 109 | 9 | 00109 | Sat Jan 10 00:00:00 1970 PST | Sat Jan 10 00:00:00 1970 | 9 | 9
+ 110 | 0 | 00110 | Sun Jan 11 00:00:00 1970 PST | Sun Jan 11 00:00:00 1970 | 0 | 0
+ (10 rows)
+
+ -- with WHERE clause
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
+ QUERY PLAN
+ ------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on public.ft1 t1
+ Output: c1, c2, c3, c4, c5, c6, c7
+ Filter: ((t1.c1 = 101) AND ((t1.c6)::text = '1'::text) AND (t1.c7 = '1'::bpchar))
+ Remote SQL: DECLARE pgsql_fdw_cursor_4 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (4 rows)
+
+ SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ -----+----+-------+------------------------------+--------------------------+----+------------
+ 101 | 1 | 00101 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970 | 1 | 1
+ (1 row)
+
+ -- aggregate
+ SELECT COUNT(*) FROM ft1 t1;
+ count
+ -------
+ 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
+ ----+----+-------+------------------------------+--------------------------+----+------------
+ 1 | 1 | 00001 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970 | 1 | 1
+ 2 | 2 | 00002 | Sat Jan 03 00:00:00 1970 PST | Sat Jan 03 00:00:00 1970 | 2 | 2
+ 3 | 3 | 00003 | Sun Jan 04 00:00:00 1970 PST | Sun Jan 04 00:00:00 1970 | 3 | 3
+ 4 | 4 | 00004 | Mon Jan 05 00:00:00 1970 PST | Mon Jan 05 00:00:00 1970 | 4 | 4
+ 5 | 5 | 00005 | Tue Jan 06 00:00:00 1970 PST | Tue Jan 06 00:00:00 1970 | 5 | 5
+ 6 | 6 | 00006 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6
+ 7 | 7 | 00007 | Thu Jan 08 00:00:00 1970 PST | Thu Jan 08 00:00:00 1970 | 7 | 7
+ 8 | 8 | 00008 | Fri Jan 09 00:00:00 1970 PST | Fri Jan 09 00:00:00 1970 | 8 | 8
+ 9 | 9 | 00009 | Sat Jan 10 00:00:00 1970 PST | Sat Jan 10 00:00:00 1970 | 9 | 9
+ 10 | 0 | 00010 | Sun Jan 11 00:00:00 1970 PST | Sun Jan 11 00:00:00 1970 | 0 | 0
+ (10 rows)
+
+ -- subquery+MAX
+ SELECT * FROM ft1 t1 WHERE t1.c3 = (SELECT MAX(c3) FROM ft2 t2) ORDER BY c1;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ ------+----+-------+------------------------------+--------------------------+----+------------
+ 1000 | 0 | 01000 | Thu Jan 01 00:00:00 1970 PST | Thu Jan 01 00:00:00 1970 | 0 | 0
+ (1 row)
+
+ -- used in CTE
+ WITH t1 AS (SELECT * FROM ft1 WHERE c1 <= 10) SELECT t2.c1, t2.c2, t2.c3, t2.c4 FROM t1, ft2 t2 WHERE t1.c1 = t2.c1 ORDER BY t1.c1;
+ c1 | c2 | c3 | c4
+ ----+----+-------+------------------------------
+ 1 | 1 | 00001 | Fri Jan 02 00:00:00 1970 PST
+ 2 | 2 | 00002 | Sat Jan 03 00:00:00 1970 PST
+ 3 | 3 | 00003 | Sun Jan 04 00:00:00 1970 PST
+ 4 | 4 | 00004 | Mon Jan 05 00:00:00 1970 PST
+ 5 | 5 | 00005 | Tue Jan 06 00:00:00 1970 PST
+ 6 | 6 | 00006 | Wed Jan 07 00:00:00 1970 PST
+ 7 | 7 | 00007 | Thu Jan 08 00:00:00 1970 PST
+ 8 | 8 | 00008 | Fri Jan 09 00:00:00 1970 PST
+ 9 | 9 | 00009 | Sat Jan 10 00:00:00 1970 PST
+ 10 | 0 | 00010 | Sun Jan 11 00:00:00 1970 PST
+ (10 rows)
+
+ -- fixed values
+ SELECT 'fixed', NULL FROM ft1 t1 WHERE c1 = 1;
+ ?column? | ?column?
+ ----------+----------
+ fixed |
+ (1 row)
+
+ -- user-defined operator/function
+ CREATE FUNCTION pgsql_fdw_abs(int) RETURNS int AS $$
+ BEGIN
+ RETURN abs($1);
+ END
+ $$ LANGUAGE plpgsql IMMUTABLE;
+ CREATE OPERATOR === (
+ LEFTARG = int,
+ RIGHTARG = int,
+ PROCEDURE = int4eq,
+ COMMUTATOR = ===,
+ NEGATOR = !==
+ );
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = pgsql_fdw_abs(t1.c2);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = pgsql_fdw_abs(c2))
+ Remote SQL: DECLARE pgsql_fdw_cursor_16 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 === t1.c2;
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 === c2)
+ Remote SQL: DECLARE pgsql_fdw_cursor_17 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = abs(t1.c2);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = abs(c2))
+ Remote SQL: DECLARE pgsql_fdw_cursor_18 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = t1.c2;
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = c2)
+ Remote SQL: DECLARE pgsql_fdw_cursor_19 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ -- ===================================================================
+ -- parameterized queries
+ -- ===================================================================
+ -- simple join
+ PREPARE st1(int, int) AS SELECT t1.c3, t2.c3 FROM ft1 t1, ft2 t2 WHERE t1.c1 = $1 AND t2.c1 = $2;
+ EXPLAIN (COSTS false) EXECUTE st1(1, 2);
+ QUERY PLAN
+ -----------------------------------------------------------------------------------------------------------------------------------------
+ Nested Loop
+ -> Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_20 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, NULL, NULL, NULL, NULL FROM "S 1"."T 1"
+ -> Materialize
+ -> Foreign Scan on ft2 t2
+ Filter: (c1 = 2)
+ Remote SQL: DECLARE pgsql_fdw_cursor_21 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, NULL, NULL, NULL, NULL FROM "S 1"."T 1"
+ (8 rows)
+
+ EXECUTE st1(1, 1);
+ c3 | c3
+ -------+-------
+ 00001 | 00001
+ (1 row)
+
+ EXECUTE st1(101, 101);
+ c3 | c3
+ -------+-------
+ 00101 | 00101
+ (1 row)
+
+ -- subquery using stable function (can't be pushed down)
+ PREPARE st2(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c4) = 6) ORDER BY c1;
+ EXPLAIN (COSTS false) EXECUTE st2(10, 20);
+ QUERY PLAN
+ ---------------------------------------------------------------------------------------------------------------------------------------------------
+ Sort
+ Sort Key: t1.c1
+ -> Hash Join
+ Hash Cond: (t1.c3 = t2.c3)
+ -> Foreign Scan on ft1 t1
+ Filter: (c1 < 20)
+ Remote SQL: DECLARE pgsql_fdw_cursor_26 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ -> Hash
+ -> HashAggregate
+ -> Foreign Scan on ft2 t2
+ Filter: ((c1 > 10) AND (date_part('dow'::text, c4) = 6::double precision))
+ Remote SQL: DECLARE pgsql_fdw_cursor_27 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, c4, NULL, NULL, NULL FROM "S 1"."T 1"
+ (12 rows)
+
+ EXECUTE st2(10, 20);
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ ----+----+-------+------------------------------+--------------------------+----+------------
+ 16 | 6 | 00016 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970 | 6 | 6
+ (1 row)
+
+ EXECUTE st1(101, 101);
+ c3 | c3
+ -------+-------
+ 00101 | 00101
+ (1 row)
+
+ -- subquery using immutable function (can be pushed down)
+ PREPARE st3(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c5) = 6) ORDER BY c1;
+ EXPLAIN (COSTS false) EXECUTE st3(10, 20);
+ QUERY PLAN
+ ---------------------------------------------------------------------------------------------------------------------------------------------------
+ Sort
+ Sort Key: t1.c1
+ -> Hash Join
+ Hash Cond: (t1.c3 = t2.c3)
+ -> Foreign Scan on ft1 t1
+ Filter: (c1 < 20)
+ Remote SQL: DECLARE pgsql_fdw_cursor_32 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ -> Hash
+ -> HashAggregate
+ -> Foreign Scan on ft2 t2
+ Filter: ((c1 > 10) AND (date_part('dow'::text, c5) = 6::double precision))
+ Remote SQL: DECLARE pgsql_fdw_cursor_33 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, NULL, c5, NULL, NULL FROM "S 1"."T 1"
+ (12 rows)
+
+ EXECUTE st3(10, 20);
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ ----+----+-------+------------------------------+--------------------------+----+------------
+ 16 | 6 | 00016 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970 | 6 | 6
+ (1 row)
+
+ EXECUTE st3(20, 30);
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ ----+----+-------+------------------------------+--------------------------+----+------------
+ 23 | 3 | 00023 | Sat Jan 24 00:00:00 1970 PST | Sat Jan 24 00:00:00 1970 | 3 | 3
+ (1 row)
+
+ -- custom plan should be chosen
+ PREPARE st4(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 = $1;
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_38 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_39 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_40 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_41 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_42 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = $1)
+ Remote SQL: DECLARE pgsql_fdw_cursor_43 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ -- cleanup
+ DEALLOCATE st1;
+ DEALLOCATE st2;
+ DEALLOCATE st3;
+ DEALLOCATE st4;
+ -- ===================================================================
+ -- connection management
+ -- ===================================================================
+ SELECT srvname, usename FROM pgsql_fdw_connections;
+ srvname | usename
+ -----------+----------------
+ loopback2 | pgsql_fdw_user
+ (1 row)
+
+ SELECT pgsql_fdw_disconnect(srvid, usesysid) FROM pgsql_fdw_get_connections();
+ pgsql_fdw_disconnect
+ ----------------------
+ OK
+ (1 row)
+
+ SELECT srvname, usename FROM pgsql_fdw_connections;
+ srvname | usename
+ ---------+---------
+ (0 rows)
+
+ -- ===================================================================
+ -- subtransaction
+ -- ===================================================================
+ BEGIN;
+ DECLARE c CURSOR FOR SELECT * FROM ft1 ORDER BY c1;
+ FETCH c;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ ----+----+-------+------------------------------+--------------------------+----+------------
+ 1 | 1 | 00001 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970 | 1 | 1
+ (1 row)
+
+ SAVEPOINT s;
+ ERROR OUT; -- ERROR
+ ERROR: syntax error at or near "ERROR"
+ LINE 1: ERROR OUT;
+ ^
+ ROLLBACK TO s;
+ SELECT srvname FROM pgsql_fdw_connections;
+ srvname
+ -----------
+ loopback2
+ (1 row)
+
+ FETCH c;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ ----+----+-------+------------------------------+--------------------------+----+------------
+ 2 | 2 | 00002 | Sat Jan 03 00:00:00 1970 PST | Sat Jan 03 00:00:00 1970 | 2 | 2
+ (1 row)
+
+ COMMIT;
+ SELECT srvname FROM pgsql_fdw_connections;
+ srvname
+ -----------
+ loopback2
+ (1 row)
+
+ ERROR OUT; -- ERROR
+ ERROR: syntax error at or near "ERROR"
+ LINE 1: ERROR OUT;
+ ^
+ SELECT srvname FROM pgsql_fdw_connections;
+ srvname
+ ---------
+ (0 rows)
+
+ -- ===================================================================
+ -- cleanup
+ -- ===================================================================
+ DROP OPERATOR === (int, int) CASCADE;
+ DROP OPERATOR !== (int, int) CASCADE;
+ DROP FUNCTION pgsql_fdw_abs(int);
+ DROP SCHEMA "S 1" CASCADE;
+ NOTICE: drop cascades to 2 other objects
+ DETAIL: drop cascades to table "S 1"."T 1"
+ drop cascades to table "S 1"."T 2"
+ DROP EXTENSION pgsql_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 server loopback2
+ drop cascades to user mapping for pgsql_fdw_user
+ drop cascades to foreign table ft1
+ drop cascades to foreign table ft2
+ \c
+ DROP ROLE pgsql_fdw_user;
diff --git a/contrib/pgsql_fdw/option.c b/contrib/pgsql_fdw/option.c
index ...a2a8927 .
*** a/contrib/pgsql_fdw/option.c
--- b/contrib/pgsql_fdw/option.c
***************
*** 0 ****
--- 1,242 ----
+ /*-------------------------------------------------------------------------
+ *
+ * option.c
+ * FDW option handling
+ *
+ * Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/option.c
+ *
+ *-------------------------------------------------------------------------
+ */
+ #include "postgres.h"
+
+ #include "access/reloptions.h"
+ #include "catalog/pg_foreign_data_wrapper.h"
+ #include "catalog/pg_foreign_server.h"
+ #include "catalog/pg_foreign_table.h"
+ #include "catalog/pg_user_mapping.h"
+ #include "commands/defrem.h"
+ #include "fmgr.h"
+ #include "foreign/foreign.h"
+ #include "lib/stringinfo.h"
+ #include "miscadmin.h"
+
+ #include "pgsql_fdw.h"
+
+ /*
+ * SQL functions
+ */
+ extern Datum pgsql_fdw_validator(PG_FUNCTION_ARGS);
+ PG_FUNCTION_INFO_V1(pgsql_fdw_validator);
+
+ /*
+ * Describes the valid options for objects that use this wrapper.
+ */
+ typedef struct PgsqlFdwOption
+ {
+ const char *optname;
+ Oid optcontext; /* Oid of catalog in which options may appear */
+ bool is_libpq_opt; /* true if it's used in libpq */
+ } PgsqlFdwOption;
+
+ /*
+ * Valid options for pgsql_fdw.
+ */
+ static PgsqlFdwOption valid_options[] = {
+
+ /*
+ * Options for libpq connection.
+ * Note: This list should be updated along with PQconninfoOptions in
+ * interfaces/libpq/fe-connect.c, so the order is kept as is.
+ *
+ * Some useless libpq connection options are not accepted by pgsql_fdw:
+ * client_encoding: set to local database encoding automatically
+ * fallback_application_name: fixed to "pgsql_fdw"
+ * replication: pgsql_fdw never be replication client
+ */
+ {"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},
+ #ifdef NOT_USED
+ {"client_encoding", ForeignServerRelationId, true},
+ #endif
+ {"tty", ForeignServerRelationId, true},
+ {"options", ForeignServerRelationId, true},
+ {"application_name", ForeignServerRelationId, true},
+ #ifdef NOT_USED
+ {"fallback_application_name", ForeignServerRelationId, true},
+ #endif
+ {"keepalives", ForeignServerRelationId, true},
+ {"keepalives_idle", ForeignServerRelationId, true},
+ {"keepalives_interval", ForeignServerRelationId, true},
+ {"keepalives_count", ForeignServerRelationId, true},
+ {"requiressl", ForeignServerRelationId, true},
+ {"sslcompression", ForeignServerRelationId, true},
+ {"sslmode", ForeignServerRelationId, true},
+ {"sslcert", ForeignServerRelationId, true},
+ {"sslkey", ForeignServerRelationId, true},
+ {"sslrootcert", ForeignServerRelationId, true},
+ {"sslcrl", ForeignServerRelationId, true},
+ {"requirepeer", ForeignServerRelationId, true},
+ {"krbsrvname", ForeignServerRelationId, true},
+ {"gsslib", ForeignServerRelationId, true},
+ #ifdef NOT_USED
+ {"replication", ForeignServerRelationId, true},
+ #endif
+
+ /*
+ * Options for translation of object names.
+ */
+ {"nspname", ForeignTableRelationId, false},
+ {"relname", ForeignTableRelationId, false},
+ {"colname", AttributeRelationId, false},
+
+ /*
+ * Options for cursor behavior.
+ * These options can be overridden by finer-grained objects.
+ */
+ {"fetch_count", ForeignTableRelationId, false},
+ {"fetch_count", ForeignServerRelationId, false},
+
+ /* Terminating entry --- MUST BE LAST */
+ {NULL, InvalidOid, false}
+ };
+
+ /*
+ * Helper functions
+ */
+ static bool is_valid_option(const char *optname, Oid context);
+
+ /*
+ * Validate the generic options given to a FOREIGN DATA WRAPPER, SERVER,
+ * USER MAPPING or FOREIGN TABLE that uses pgsql_fdw.
+ *
+ * Raise an ERROR if the option or its value is considered invalid.
+ */
+ Datum
+ pgsql_fdw_validator(PG_FUNCTION_ARGS)
+ {
+ List *options_list = untransformRelOptions(PG_GETARG_DATUM(0));
+ Oid catalog = PG_GETARG_OID(1);
+ ListCell *cell;
+
+ /*
+ * Check that only options supported by pgsql_fdw, and allowed for the
+ * current object type, are given.
+ */
+ foreach(cell, options_list)
+ {
+ DefElem *def = (DefElem *) lfirst(cell);
+
+ if (!is_valid_option(def->defname, catalog))
+ {
+ PgsqlFdwOption *opt;
+ StringInfoData buf;
+
+ /*
+ * Unknown option specified, complain about it. Provide a hint
+ * with list of valid options for the object.
+ */
+ initStringInfo(&buf);
+ for (opt = valid_options; opt->optname; opt++)
+ {
+ if (catalog == opt->optcontext)
+ appendStringInfo(&buf, "%s%s", (buf.len > 0) ? ", " : "",
+ opt->optname);
+ }
+
+ ereport(ERROR,
+ (errcode(ERRCODE_FDW_INVALID_OPTION_NAME),
+ errmsg("invalid option \"%s\"", def->defname),
+ errhint("Valid options in this context are: %s",
+ buf.data)));
+ }
+
+ /* fetch_count be positive digit number. */
+ if (strcmp(def->defname, "fetch_count") == 0)
+ {
+ long value;
+ char *p = NULL;
+
+ value = strtol(defGetString(def), &p, 10);
+ if (*p != '\0' || value < 1)
+ ereport(ERROR,
+ (errcode(ERRCODE_FDW_INVALID_ATTRIBUTE_VALUE),
+ errmsg("invalid value for %s: \"%s\"",
+ def->defname, defGetString(def))));
+ }
+ }
+
+ /*
+ * We don't care option-specific limitation here; they will be validated at
+ * the execution time.
+ */
+
+ PG_RETURN_VOID();
+ }
+
+ /*
+ * Check whether the given option is one of the valid pgsql_fdw options.
+ * context is the Oid of the catalog holding the object the option is for.
+ */
+ static bool
+ is_valid_option(const char *optname, Oid context)
+ {
+ PgsqlFdwOption *opt;
+
+ for (opt = valid_options; opt->optname; opt++)
+ {
+ if (context == opt->optcontext && strcmp(opt->optname, optname) == 0)
+ return true;
+ }
+ return false;
+ }
+
+ /*
+ * Check whether the given option is one of the valid libpq options.
+ * context is the Oid of the catalog holding the object the option is for.
+ */
+ static bool
+ is_libpq_option(const char *optname)
+ {
+ PgsqlFdwOption *opt;
+
+ for (opt = valid_options; opt->optname; opt++)
+ {
+ if (strcmp(opt->optname, optname) == 0 && opt->is_libpq_opt)
+ return true;
+ }
+ return false;
+ }
+
+ /*
+ * Generate key-value arrays which includes only libpq options from the list
+ * which contains any kind of options.
+ */
+ int
+ ExtractConnectionOptions(List *defelems, const char **keywords, const char **values)
+ {
+ ListCell *lc;
+ int i;
+
+ i = 0;
+ foreach(lc, defelems)
+ {
+ DefElem *d = (DefElem *) lfirst(lc);
+ if (is_libpq_option(d->defname))
+ {
+ keywords[i] = d->defname;
+ values[i] = defGetString(d);
+ i++;
+ }
+ }
+ return i;
+ }
diff --git a/contrib/pgsql_fdw/pgsql_fdw--1.0.sql b/contrib/pgsql_fdw/pgsql_fdw--1.0.sql
index ...b0ea2b2 .
*** a/contrib/pgsql_fdw/pgsql_fdw--1.0.sql
--- b/contrib/pgsql_fdw/pgsql_fdw--1.0.sql
***************
*** 0 ****
--- 1,39 ----
+ /* contrib/pgsql_fdw/pgsql_fdw--1.0.sql */
+
+ -- complain if script is sourced in psql, rather than via CREATE EXTENSION
+ \echo Use "CREATE EXTENSION pgsql_fdw" to load this file. \quit
+
+ CREATE FUNCTION pgsql_fdw_handler()
+ RETURNS fdw_handler
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C STRICT;
+
+ CREATE FUNCTION pgsql_fdw_validator(text[], oid)
+ RETURNS void
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C STRICT;
+
+ CREATE FOREIGN DATA WRAPPER pgsql_fdw
+ HANDLER pgsql_fdw_handler
+ VALIDATOR pgsql_fdw_validator;
+
+ /* connection management functions and view */
+ CREATE FUNCTION pgsql_fdw_get_connections(out srvid oid, out usesysid oid)
+ RETURNS SETOF record
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C STRICT;
+
+ CREATE FUNCTION pgsql_fdw_disconnect(oid, oid)
+ RETURNS text
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C STRICT;
+
+ CREATE VIEW pgsql_fdw_connections AS
+ SELECT c.srvid srvid,
+ s.srvname srvname,
+ c.usesysid usesysid,
+ pg_get_userbyid(c.usesysid) usename
+ FROM pgsql_fdw_get_connections() c
+ JOIN pg_catalog.pg_foreign_server s ON (s.oid = c.srvid);
+ GRANT SELECT ON pgsql_fdw_connections TO public;
+
diff --git a/contrib/pgsql_fdw/pgsql_fdw.c b/contrib/pgsql_fdw/pgsql_fdw.c
index ...370d240 .
*** a/contrib/pgsql_fdw/pgsql_fdw.c
--- b/contrib/pgsql_fdw/pgsql_fdw.c
***************
*** 0 ****
--- 1,1014 ----
+ /*-------------------------------------------------------------------------
+ *
+ * pgsql_fdw.c
+ * foreign-data wrapper for remote PostgreSQL servers.
+ *
+ * Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/pgsql_fdw.c
+ *
+ *-------------------------------------------------------------------------
+ */
+ #include "postgres.h"
+ #include "fmgr.h"
+
+ #include "catalog/pg_foreign_server.h"
+ #include "catalog/pg_foreign_table.h"
+ #include "commands/defrem.h"
+ #include "commands/explain.h"
+ #include "foreign/fdwapi.h"
+ #include "funcapi.h"
+ #include "miscadmin.h"
+ #include "nodes/nodeFuncs.h"
+ #include "optimizer/cost.h"
+ #include "optimizer/pathnode.h"
+ #include "optimizer/planmain.h"
+ #include "optimizer/restrictinfo.h"
+ #include "utils/lsyscache.h"
+ #include "utils/memutils.h"
+ #include "utils/rel.h"
+
+ #include "pgsql_fdw.h"
+ #include "connection.h"
+
+ PG_MODULE_MAGIC;
+
+ /*
+ * Default fetch count for cursor. This can be overridden by fetch_count FDW
+ * option.
+ */
+ #define DEFAULT_FETCH_COUNT 10000
+
+ /*
+ * Cost to establish a connection.
+ * XXX: should be configurable per server?
+ */
+ #define CONNECTION_COSTS 100.0
+
+ /*
+ * Cost to transfer 1 byte from remote server.
+ * XXX: should be configurable per server?
+ */
+ #define TRANSFER_COSTS_PER_BYTE 0.001
+
+ /*
+ * Cursors which are used together in a local query require different name, so
+ * we use simple incremental name for that purpose. We don't care wrap around
+ * of cursor_id because it's hard to imagine that 2^32 cursors are used in a
+ * query.
+ */
+ #define CURSOR_NAME_FORMAT "pgsql_fdw_cursor_%u"
+ static uint32 cursor_id = 0;
+
+ /*
+ * FDW-specific information for RelOptInfo.fdw_private. This is used to pass
+ * information from pgsqlGetForeignRelSize to pgsqlGetForeignPaths.
+ */
+ typedef struct PgsqlFdwPlanState {
+ /*
+ * These are generated in GetForeignRelSize, and also used in subsequent
+ * GetForeignPaths.
+ * XXX If we can have enough statistics of remote data, we can defer
+ * estimating cost to GetForeignPaths, so then these would be unnecessary.
+ */
+ char *sql;
+ Cost startup_cost;
+ Cost total_cost;
+ } PgsqlFdwPlanState;
+
+ /*
+ * Index of FDW-private information, such as remote SQL statements, which are
+ * stored in fdw_private list.
+ */
+ enum FdwPrivateIndex {
+ FdwPrivateSelectSql,
+ FdwPrivateDeclareSql,
+ FdwPrivateFetchSql,
+ FdwPrivateResetSql,
+ FdwPrivateCloseSql,
+
+ /* # of elements stored in the list fdw_private */
+ FdwPrivateNum,
+ };
+
+ /*
+ * Describes an execution state of a foreign scan against a foreign table
+ * using pgsql_fdw.
+ */
+ typedef struct PgsqlFdwExecutionState
+ {
+ MemoryContext scan_cxt; /* context for per-scan lifespan data */
+
+ List *fdw_private; /* FDW-private information */
+ PGconn *conn; /* connection for the scan */
+
+ Oid *param_types; /* type array of external parameter */
+ const char **param_values; /* value array of external parameter */
+
+ int attnum; /* # of non-dropped attribute */
+ char **col_values; /* column value buffer */
+ AttInMetadata *attinmeta; /* attribute metadata */
+
+ Tuplestorestate *tuples; /* result of the scan */
+ } PgsqlFdwExecutionState;
+
+ /*
+ * SQL functions
+ */
+ extern Datum pgsql_fdw_handler(PG_FUNCTION_ARGS);
+ PG_FUNCTION_INFO_V1(pgsql_fdw_handler);
+
+ /*
+ * FDW callback routines
+ */
+ static void pgsqlGetForeignRelSize(PlannerInfo *root,
+ RelOptInfo *baserel,
+ Oid foreigntableid);
+ static void pgsqlGetForeignPaths(PlannerInfo *root,
+ RelOptInfo *baserel,
+ Oid foreigntableid);
+ static ForeignScan *pgsqlGetForeignPlan(PlannerInfo *root,
+ RelOptInfo *baserel,
+ Oid foreigntableid,
+ ForeignPath *best_path,
+ List *tlist,
+ List *scan_clauses);
+ static void pgsqlExplainForeignScan(ForeignScanState *node, ExplainState *es);
+ static void pgsqlBeginForeignScan(ForeignScanState *node, int eflags);
+ static TupleTableSlot *pgsqlIterateForeignScan(ForeignScanState *node);
+ static void pgsqlReScanForeignScan(ForeignScanState *node);
+ static void pgsqlEndForeignScan(ForeignScanState *node);
+
+ /*
+ * Helper functions
+ */
+ static void get_remote_estimate(const char *sql,
+ PGconn *conn,
+ double *rows,
+ int *width,
+ Cost *startup_cost,
+ Cost *total_cost);
+ static void adjust_costs(double rows, int width,
+ Cost *startup_cost, Cost *total_cost);
+ static void execute_query(ForeignScanState *node);
+ static PGresult *fetch_result(ForeignScanState *node);
+ static void store_result(ForeignScanState *node, PGresult *res);
+
+ /* Exported functions, but not written in pgsql_fdw.h. */
+ void _PG_init(void);
+ void _PG_fini(void);
+
+ /*
+ * Module-specific initialization.
+ */
+ void
+ _PG_init(void)
+ {
+ }
+
+ /*
+ * Module-specific clean up.
+ */
+ void
+ _PG_fini(void)
+ {
+ }
+
+ /*
+ * The content of FdwRoutine of pgsql_fdw is never changed, so we use one
+ * static object for all scan.
+ */
+ static FdwRoutine fdwroutine = {
+
+ /* Node type */
+ T_FdwRoutine,
+
+ /* Required handler functions. */
+ pgsqlGetForeignRelSize,
+ pgsqlGetForeignPaths,
+ pgsqlGetForeignPlan,
+ pgsqlExplainForeignScan,
+ pgsqlBeginForeignScan,
+ pgsqlIterateForeignScan,
+ pgsqlReScanForeignScan,
+ pgsqlEndForeignScan,
+
+ /* Optional handler functions. */
+ };
+
+ /*
+ * Foreign-data wrapper handler function: return a struct with pointers
+ * to my callback routines.
+ */
+ Datum
+ pgsql_fdw_handler(PG_FUNCTION_ARGS)
+ {
+ PG_RETURN_POINTER(&fdwroutine);
+ }
+
+ /*
+ * pgsqlGetForeignRelSize
+ * Estimate # of rows and width of the result of the scan
+ *
+ * If we have had enough statistics about foreign data, we can get good
+ * estimate of rows and width of this scan by calling
+ * set_baserel_size_estimates(), therefore we can avoid executing expensive
+ * EXPLAIN on remote side. To achieve this, we need ANALYZE support for
+ * foreign tables by backend, and FDW-specific implementation of updating
+ * statistics. But we don't have them at this moment, so estimate # of result
+ * rows on the basis of remote estimate for unconditional query and selectivity
+ * of the clauses.
+ */
+ static void
+ pgsqlGetForeignRelSize(PlannerInfo *root,
+ RelOptInfo *baserel,
+ Oid foreigntableid)
+ {
+ char *sql;
+ ForeignTable *table;
+ ForeignServer *server;
+ UserMapping *user;
+ PGconn *conn;
+ double rows;
+ int width;
+ Cost startup_cost;
+ Cost total_cost;
+ PgsqlFdwPlanState *planstate;
+
+ /* Retrieve catalog objects which are necessary to estimate rows. */
+ table = GetForeignTable(foreigntableid);
+ server = GetForeignServer(table->serverid);
+ user = GetUserMapping(GetOuterUserId(), server->serverid);
+
+ /*
+ * Create plain SELECT statement with no WHERE clause for this scan to
+ * obtain meaningful rows estimation by executing EXPLAIN on remote server.
+ */
+ sql = deparseSimpleSql(foreigntableid, root, baserel, table);
+ conn = GetConnection(server, user, false);
+ get_remote_estimate(sql, conn, &rows, &width, &startup_cost, &total_cost);
+ ReleaseConnection(conn);
+
+ /*
+ * Estimate # of rows by calling set_baserel_size_estimates() with setting
+ * baserel->tuples to the rows estimation of remote EXPLAIN. We override
+ * width estimation because set_baserel_size_estimates estimates it on the
+ * basis of statistics which we don't actually have.
+ */
+ baserel->tuples = rows;
+ set_baserel_size_estimates(root, baserel);
+ baserel->width = width;
+
+ /*
+ * Pack obtained information into a object and store it in FDW-private area
+ * of RelOptInfo to pass them to subsequent functions.
+ */
+ planstate = palloc(sizeof(PgsqlFdwExecutionState));
+ planstate->sql = sql;
+ planstate->startup_cost = startup_cost;
+ planstate->total_cost = total_cost;
+ baserel->fdw_private = (void *) planstate;
+ }
+
+ /*
+ * pgsqlGetForeignPaths
+ * Create possible scan paths for a scan on the foreign table
+ */
+ static void
+ pgsqlGetForeignPaths(PlannerInfo *root,
+ RelOptInfo *baserel,
+ Oid foreigntableid)
+ {
+ PgsqlFdwPlanState *planstate = (PgsqlFdwPlanState *) baserel->fdw_private;
+ List *fdw_private = NIL;
+ ForeignPath *path;
+
+ /*
+ * Cost estimation should be modified to respect cost of establishing
+ * connection and transferring data.
+ */
+ adjust_costs(baserel->rows,
+ baserel->width,
+ &planstate->startup_cost,
+ &planstate->total_cost);
+
+ /*
+ * Create simplest ForeignScan path node, counterpart of SeqScan for
+ * regular tables, for this scan, and add it to baserel.
+ */
+ fdw_private = list_make1(makeString(planstate->sql));
+ path = create_foreignscan_path(root, baserel,
+ baserel->rows,
+ planstate->startup_cost,
+ planstate->total_cost,
+ NIL, /* no pathkeys */
+ NULL, /* no outer rel either */
+ NIL, /* no param clause */
+ fdw_private); /* pass only simple SQL */
+ add_path(baserel, (Path *) path);
+
+ /*
+ * XXX We can consider sorted path here if we know that foreign table is
+ * indexed on remote end. For this purpose, should we support FOREIGN
+ * INDEX to represent possible sets of sort keys which are relatively
+ * efficient?
+ */
+ }
+
+ /*
+ * pgsqlGetForeignPlan
+ * Create ForeignScan plan node which implements selected best path
+ */
+ static ForeignScan *
+ pgsqlGetForeignPlan(PlannerInfo *root,
+ RelOptInfo *baserel,
+ Oid foreigntableid,
+ ForeignPath *best_path,
+ List *tlist,
+ List *scan_clauses)
+ {
+ Index scan_relid = baserel->relid;
+ List *fdw_private = best_path->fdw_private;
+ ForeignTable *table;
+ ForeignServer *server;
+ ListCell *lc;
+ char name[128]; /* must be larger than format + 10 */
+ StringInfoData cursor;
+ DefElem *def;
+ int fetch_count = DEFAULT_FETCH_COUNT;
+ char *sql;
+
+ /*
+ * We have no native ability to evaluate restriction clauses, so we just
+ * put all the scan_clauses into the plan node's qual list for the
+ * executor to check. So all we have to do here is strip RestrictInfo
+ * nodes from the clauses and ignore pseudoconstants (which will be
+ * handled elsewhere).
+ */
+ scan_clauses = extract_actual_clauses(scan_clauses, false);
+
+ /*
+ * Use specified fetch_count instead of default value, if any. Foreign
+ * table option overrides server option.
+ */
+ table = GetForeignTable(foreigntableid);
+ foreach(lc, table->options)
+ {
+ def = (DefElem *) lfirst(lc);
+ if (strcmp(def->defname, "fetch_count") == 0)
+ break;
+
+ }
+ if (lc == NULL)
+ {
+ server = GetForeignServer(table->serverid);
+ foreach(lc, server->options)
+ {
+ def = (DefElem *) lfirst(lc);
+ if (strcmp(def->defname, "fetch_count") == 0)
+ break;
+
+ }
+ }
+ if (lc != NULL)
+ fetch_count = strtol(defGetString(def), NULL, 10);
+ elog(DEBUG1, "relid=%u fetch_count=%d", foreigntableid, fetch_count);
+
+ /*
+ * We store some more information in FdwPlan to pass them beyond the
+ * boundary between planner and executor. Finally FdwPlan using cursor
+ * would hold items below:
+ *
+ * 1) plain SELECT statement (already added above)
+ * 2) SQL statement used to declare cursor
+ * 3) SQL statement used to fetch rows from cursor
+ * 4) SQL statement used to reset cursor
+ * 5) SQL statement used to close cursor
+ *
+ * These items are indexed with the enum FdwPrivateIndex, so an item
+ * can be accessed directly via list_nth(). For example of FETCH
+ * statement:
+ * list_nth(fdw_private, FdwPrivateFetchSql)
+ */
+ sql = strVal(list_nth(fdw_private, FdwPrivateSelectSql));
+
+ /* Construct cursor name from sequential value */
+ sprintf(name, CURSOR_NAME_FORMAT, cursor_id++);
+
+ /* Construct statement to declare cursor */
+ initStringInfo(&cursor);
+ appendStringInfo(&cursor, "DECLARE %s SCROLL CURSOR FOR %s", name, sql);
+ fdw_private = lappend(fdw_private, makeString(cursor.data));
+
+ /* Construct statement to fetch rows from cursor */
+ initStringInfo(&cursor);
+ appendStringInfo(&cursor, "FETCH %d FROM %s", fetch_count, name);
+ fdw_private = lappend(fdw_private, makeString(cursor.data));
+
+ /* Construct statement to reset cursor */
+ initStringInfo(&cursor);
+ appendStringInfo(&cursor, "MOVE ABSOLUTE 0 FROM %s", name);
+ fdw_private = lappend(fdw_private, makeString(cursor.data));
+
+ /* Construct statement to close cursor */
+ initStringInfo(&cursor);
+ appendStringInfo(&cursor, "CLOSE %s", name);
+ fdw_private = lappend(fdw_private, makeString(cursor.data));
+
+ /* Create the ForeignScan node with fdw_private of selected path. */
+ return make_foreignscan(tlist,
+ scan_clauses,
+ scan_relid,
+ NIL,
+ fdw_private);
+ }
+
+ /*
+ * pgsqlExplainForeignScan
+ * Produce extra output for EXPLAIN
+ */
+ static void
+ pgsqlExplainForeignScan(ForeignScanState *node, ExplainState *es)
+ {
+ List *fdw_private;
+ char *sql;
+
+ fdw_private = ((ForeignScan *) node->ss.ps.plan)->fdw_private;
+ sql = strVal(list_nth(fdw_private, FdwPrivateDeclareSql));
+ ExplainPropertyText("Remote SQL", sql, es);
+ }
+
+ /*
+ * pgsqlBeginForeignScan
+ * Initiate access to a foreign PostgreSQL table.
+ */
+ static void
+ pgsqlBeginForeignScan(ForeignScanState *node, int eflags)
+ {
+ PgsqlFdwExecutionState *festate;
+ PGconn *conn;
+ Oid relid;
+ ForeignTable *table;
+ ForeignServer *server;
+ UserMapping *user;
+ TupleTableSlot *slot = node->ss.ss_ScanTupleSlot;
+
+ /*
+ * Do nothing in EXPLAIN (no ANALYZE) case. node->fdw_state stays NULL.
+ */
+ if (eflags & EXEC_FLAG_EXPLAIN_ONLY)
+ return;
+
+ /*
+ * Save state in node->fdw_state.
+ */
+ festate = (PgsqlFdwExecutionState *) palloc(sizeof(PgsqlFdwExecutionState));
+ festate->fdw_private = ((ForeignScan *) node->ss.ps.plan)->fdw_private;
+
+ /*
+ * Create context for per-scan tuplestore under per-query context.
+ */
+ festate->scan_cxt = AllocSetContextCreate(node->ss.ps.state->es_query_cxt,
+ "pgsql_fdw",
+ ALLOCSET_DEFAULT_MINSIZE,
+ ALLOCSET_DEFAULT_INITSIZE,
+ ALLOCSET_DEFAULT_MAXSIZE);
+
+ /*
+ * Get connection to the foreign server. Connection manager would
+ * establish new connection if necessary.
+ */
+ relid = RelationGetRelid(node->ss.ss_currentRelation);
+ table = GetForeignTable(relid);
+ server = GetForeignServer(table->serverid);
+ user = GetUserMapping(GetOuterUserId(), server->serverid);
+ conn = GetConnection(server, user, true);
+ festate->conn = conn;
+
+ /* Result will be filled in first Iterate call. */
+ festate->tuples = NULL;
+
+ /* Allocate buffers for column values. */
+ {
+ TupleDesc tupdesc = slot->tts_tupleDescriptor;
+ festate->col_values = palloc(sizeof(char *) * tupdesc->natts);
+ festate->attinmeta = TupleDescGetAttInMetadata(tupdesc);
+ }
+
+ /* Allocate buffers for query parameters. */
+ {
+ ParamListInfo params = node->ss.ps.state->es_param_list_info;
+ int numParams = params ? params->numParams : 0;
+
+ if (numParams > 0)
+ {
+ festate->param_types = palloc0(sizeof(Oid) * numParams);
+ festate->param_values = palloc0(sizeof(char *) * numParams);
+ }
+ else
+ {
+ festate->param_types = NULL;
+ festate->param_values = NULL;
+ }
+ }
+
+ /* Store FDW-specific state into ForeignScanState */
+ node->fdw_state = (void *) festate;
+
+ return;
+ }
+
+ /*
+ * pgsqlIterateForeignScan
+ * Retrieve next row from the result set, or clear tuple slot to indicate
+ * EOF.
+ *
+ * Note that using per-query context when retrieving tuples from
+ * tuplestore to ensure that returned tuples can survive until next
+ * iteration because the tuple is released implicitly via ExecClearTuple.
+ * Retrieving a tuple from tuplestore in CurrentMemoryContext (it's a
+ * per-tuple context), ExecClearTuple will free dangling pointer.
+ */
+ static TupleTableSlot *
+ pgsqlIterateForeignScan(ForeignScanState *node)
+ {
+ PgsqlFdwExecutionState *festate;
+ TupleTableSlot *slot = node->ss.ss_ScanTupleSlot;
+ PGresult *res = NULL;
+ MemoryContext oldcontext = CurrentMemoryContext;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+
+ /*
+ * If this is the first call after Begin, we need to execute remote query.
+ * If the query needs cursor, we declare a cursor at first call and fetch
+ * from it in later calls.
+ */
+ if (festate->tuples == NULL)
+ execute_query(node);
+
+ /*
+ * If enough tuples are left in tuplestore, just return next tuple from it.
+ *
+ * It is necessary to switch to per-scan context to make returned tuple
+ * valid until next IterateForeignScan call, because it will be released
+ * with ExecClearTuple then. Otherwise, picked tuple is allocated in
+ * per-tuple context, and double-free of that tuple might happen.
+ */
+ MemoryContextSwitchTo(festate->scan_cxt);
+ if (tuplestore_gettupleslot(festate->tuples, true, false, slot))
+ {
+ MemoryContextSwitchTo(oldcontext);
+ return slot;
+ }
+ MemoryContextSwitchTo(oldcontext);
+
+ /*
+ * Here we need to clear partial result and fetch next bunch of tuples from
+ * from the cursor for the scan. If the fetch returns no tuple, the scan
+ * has reached the end.
+ *
+ * PGresult must be released before leaving this function.
+ */
+ PG_TRY();
+ {
+ res = fetch_result(node);
+ store_result(node, res);
+ PQclear(res);
+ res = NULL;
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+
+ /*
+ * If we got more tuples from the server cursor, return next tuple from
+ * tuplestore.
+ */
+ MemoryContextSwitchTo(festate->scan_cxt);
+ if (tuplestore_gettupleslot(festate->tuples, true, false, slot))
+ {
+ MemoryContextSwitchTo(oldcontext);
+ return slot;
+ }
+ MemoryContextSwitchTo(oldcontext);
+
+ /* We don't have any result even in remote server cursor. */
+ ExecClearTuple(slot);
+ return slot;
+ }
+
+ /*
+ * pgsqlReScanForeignScan
+ * - Restart this scan by resetting fetch location.
+ */
+ static void
+ pgsqlReScanForeignScan(ForeignScanState *node)
+ {
+ List *fdw_private;
+ char *sql;
+ PGconn *conn;
+ PGresult *res = NULL;
+ PgsqlFdwExecutionState *festate;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+
+ /* If we have not opened cursor yet, nothing to do. */
+ if (festate->tuples == NULL)
+ return;
+
+ /* Discard fetch results if any. */
+ tuplestore_clear(festate->tuples);
+
+ /* PGresult must be released before leaving this function. */
+ PG_TRY();
+ {
+ /* Reset cursor */
+ fdw_private = festate->fdw_private;
+ conn = festate->conn;
+ sql = strVal(list_nth(fdw_private, FdwPrivateResetSql));
+ res = PQexec(conn, sql);
+ if (PQresultStatus(res) != PGRES_COMMAND_OK)
+ {
+ ereport(ERROR,
+ (errmsg("could not rewind cursor"),
+ errdetail("%s", PQerrorMessage(conn)),
+ errhint("%s", sql)));
+ }
+ PQclear(res);
+ res = NULL;
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+ }
+
+ /*
+ * pgsqlEndForeignScan
+ * Finish scanning foreign table and dispose objects used for this scan
+ */
+ static void
+ pgsqlEndForeignScan(ForeignScanState *node)
+ {
+ List *fdw_private;
+ char *sql;
+ PGconn *conn;
+ PGresult *res = NULL;
+ PgsqlFdwExecutionState *festate;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+
+ /* if festate is NULL, we are in EXPLAIN; nothing to do */
+ if (festate == NULL)
+ return;
+
+ /* If we have not opened cursor yet, nothing to do. */
+ if (festate->tuples == NULL)
+ return;
+
+ /* Discard fetch results */
+ tuplestore_end(festate->tuples);
+ festate->tuples = NULL;
+
+ /* PGresult must be released before leaving this function. */
+ PG_TRY();
+ {
+ /* Close cursor */
+ fdw_private = festate->fdw_private;
+ conn = festate->conn;
+ sql = strVal(list_nth(fdw_private, FdwPrivateCloseSql));
+ res = PQexec(conn, sql);
+ if (PQresultStatus(res) != PGRES_COMMAND_OK)
+ {
+ ereport(ERROR,
+ (errmsg("could not close cursor"),
+ errdetail("%s", PQerrorMessage(conn)),
+ errhint("%s", sql)));
+ }
+ PQclear(res);
+ res = NULL;
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+
+ ReleaseConnection(festate->conn);
+
+ MemoryContextDelete(festate->scan_cxt);
+ festate->scan_cxt = NULL;
+ }
+
+ /*
+ * Estimate costs of executing given SQL statement.
+ */
+ static void
+ get_remote_estimate(const char *sql, PGconn *conn,
+ double *rows, int *width,
+ Cost *startup_cost, Cost *total_cost)
+ {
+ PGresult *res = NULL;
+ StringInfoData buf;
+ char *plan;
+ char *p;
+ int n;
+
+ /*
+ * Construct EXPLAIN statement with given SQL statement.
+ */
+ initStringInfo(&buf);
+ appendStringInfo(&buf, "EXPLAIN %s", sql);
+
+ /* PGresult must be released before leaving this function. */
+ PG_TRY();
+ {
+ res = PQexec(conn, buf.data);
+ if (PQresultStatus(res) != PGRES_TUPLES_OK || PQntuples(res) == 0)
+ {
+ char *msg;
+
+ msg = pstrdup(PQerrorMessage(conn));
+ ereport(ERROR,
+ (errmsg("could not execute EXPLAIN for cost estimation"),
+ errdetail("%s", msg),
+ errhint("%s", sql)));
+ }
+
+ /*
+ * Find estimation portion from top plan node. Here we search opening
+ * parentheses from the end of the line to avoid finding unexpected
+ * parentheses.
+ */
+ plan = PQgetvalue(res, 0, 0);
+ p = strrchr(plan, '(');
+ if (p == NULL)
+ elog(ERROR, "wrong EXPLAIN output: %s", plan);
+ n = sscanf(p,
+ "(cost=%lf..%lf rows=%lf width=%d)",
+ startup_cost, total_cost, rows, width);
+ if (n != 4)
+ elog(ERROR, "could not get estimation from EXPLAIN output");
+
+ PQclear(res);
+ res = NULL;
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+ }
+
+ /*
+ * Adjust costs estimated on remote end with some overheads such as connection
+ * and data transfer.
+ */
+ static void
+ adjust_costs(double rows, int width, Cost *startup_cost, Cost *total_cost)
+ {
+ /*
+ * TODO Selectivity of quals which are NOT pushed down should be also
+ * considered.
+ */
+
+ /* add cost to establish connection. */
+ *startup_cost += CONNECTION_COSTS;
+ *total_cost += CONNECTION_COSTS;
+
+ /* add cost to transfer result. */
+ *total_cost += TRANSFER_COSTS_PER_BYTE * width * rows;
+ *total_cost += cpu_tuple_cost * rows;
+ }
+
+ /*
+ * Execute remote query with current parameters.
+ */
+ static void
+ execute_query(ForeignScanState *node)
+ {
+ List *fdw_private;
+ PgsqlFdwExecutionState *festate;
+ ParamListInfo params = node->ss.ps.state->es_param_list_info;
+ int numParams = params ? params->numParams : 0;
+ Oid *types = NULL;
+ const char **values = NULL;
+ char *sql;
+ PGconn *conn;
+ PGresult *res = NULL;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+ types = festate->param_types;
+ values = festate->param_values;
+
+ /*
+ * Construct parameter array in text format. We don't release memory for
+ * the arrays explicitly, because the memory usage would not be very large,
+ * and anyway they will be released in context cleanup.
+ */
+ if (numParams > 0)
+ {
+ int i;
+
+ 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;
+
+ getTypeOutputInfo(types[i], &out_func_oid, &isvarlena);
+ fmgr_info(out_func_oid, &func);
+ values[i] = OutputFunctionCall(&func, params->params[i].value);
+ }
+ }
+ }
+
+ /* PGresult must be released before leaving this function. */
+ PG_TRY();
+ {
+ /*
+ * Execute remote query with parameters.
+ */
+ conn = festate->conn;
+ fdw_private = ((ForeignScan *) node->ss.ps.plan)->fdw_private;
+ sql = strVal(list_nth(fdw_private, FdwPrivateDeclareSql));
+ res = PQexecParams(conn, sql, numParams, types, values, NULL, NULL, 0);
+
+ /*
+ * If the query has failed, reporting details is enough here.
+ * Connection(s) which are used by this query (at least used by
+ * pgsql_fdw) will be cleaned up by the foreign connection manager.
+ */
+ if (PQresultStatus(res) != PGRES_COMMAND_OK)
+ {
+ ereport(ERROR,
+ (errmsg("could not declare cursor"),
+ errdetail("%s", PQerrorMessage(conn)),
+ errhint("%s", sql)));
+ }
+
+ /* Discard result of DECLARE statement. */
+ PQclear(res);
+ res = NULL;
+
+ /* Fetch first bunch of the result and store them into tuplestore. */
+ res = fetch_result(node);
+ store_result(node, res);
+ PQclear(res);
+ res = NULL;
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+ }
+
+ /*
+ * Fetch next partial result from remote server.
+ *
+ * Once this function has returned result records as PGresult, caller is
+ * responsible to release it, so caller should put codes which might throw
+ * exception in PG_TRY block. When an exception has been caught, release
+ * PGresult and re-throw the exception in PG_CATCH block.
+ */
+ static PGresult *
+ fetch_result(ForeignScanState *node)
+ {
+ PgsqlFdwExecutionState *festate;
+ List *fdw_private;
+ char *sql;
+ PGconn *conn;
+ PGresult *res;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+
+ /* Retrieve information for fetching result. */
+ fdw_private = festate->fdw_private;
+ sql = strVal(list_nth(fdw_private, FdwPrivateFetchSql));
+ conn = festate->conn;
+
+ /*
+ * Fetch result from remote server. In error case, we must release
+ * PGresult in this function to avoid memory leak because caller can't
+ * get the reference.
+ */
+ res = PQexec(conn, sql);
+ if (PQresultStatus(res) != PGRES_TUPLES_OK)
+ {
+ PQclear(res);
+ ereport(ERROR,
+ (errmsg("could not fetch rows from foreign server"),
+ errdetail("%s", PQerrorMessage(conn)),
+ errhint("%s", sql)));
+ }
+
+ return res;
+ }
+
+ /*
+ * Create tuples from PGresult and store them into tuplestore.
+ *
+ * Caller must use PG_TRY block to catch exception and release PGresult
+ * surely.
+ */
+ static void
+ store_result(ForeignScanState *node, PGresult *res)
+ {
+ int rows;
+ int row;
+ int i;
+ int nfields;
+ int attnum; /* number of non-dropped columns */
+ Form_pg_attribute *attrs;
+ TupleTableSlot *slot = node->ss.ss_ScanTupleSlot;
+ TupleDesc tupdesc = slot->tts_tupleDescriptor;
+ PgsqlFdwExecutionState *festate;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+ rows = PQntuples(res);
+ nfields = PQnfields(res);
+ attrs = tupdesc->attrs;
+
+ /* First, ensure that the tuplestore is empty. */
+ if (festate->tuples == NULL)
+ {
+ MemoryContext oldcontext = CurrentMemoryContext;
+
+ /*
+ * Create tuplestore to store result of the query in per-query context.
+ * Note that we use this memory context to avoid memory leak in error
+ * cases.
+ */
+ MemoryContextSwitchTo(festate->scan_cxt);
+ festate->tuples = tuplestore_begin_heap(false, false, work_mem);
+ MemoryContextSwitchTo(oldcontext);
+ }
+ else
+ {
+ /* We already have tuplestore, just need to clear contents of it. */
+ tuplestore_clear(festate->tuples);
+ }
+
+ /* 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)));
+
+ /* put a tuples into the slot */
+ for (row = 0; row < rows; row++)
+ {
+ int j;
+ HeapTuple tuple;
+
+ for (i = 0, j = 0; i < tupdesc->natts; i++)
+ {
+ /* skip dropped columns. */
+ if (attrs[i]->attisdropped)
+ {
+ festate->col_values[i] = NULL;
+ continue;
+ }
+
+ if (PQgetisnull(res, row, j))
+ festate->col_values[i] = NULL;
+ else
+ festate->col_values[i] = PQgetvalue(res, row, 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(festate->attinmeta, festate->col_values);
+ tuplestore_puttuple(festate->tuples, tuple);
+ }
+
+ tuplestore_donestoring(festate->tuples);
+ }
+
diff --git a/contrib/pgsql_fdw/pgsql_fdw.control b/contrib/pgsql_fdw/pgsql_fdw.control
index ...0a9c8f4 .
*** a/contrib/pgsql_fdw/pgsql_fdw.control
--- b/contrib/pgsql_fdw/pgsql_fdw.control
***************
*** 0 ****
--- 1,5 ----
+ # pgsql_fdw extension
+ comment = 'foreign-data wrapper for remote PostgreSQL servers'
+ default_version = '1.0'
+ module_pathname = '$libdir/pgsql_fdw'
+ relocatable = true
diff --git a/contrib/pgsql_fdw/pgsql_fdw.h b/contrib/pgsql_fdw/pgsql_fdw.h
index ...6eb99d5 .
*** a/contrib/pgsql_fdw/pgsql_fdw.h
--- b/contrib/pgsql_fdw/pgsql_fdw.h
***************
*** 0 ****
--- 1,32 ----
+ /*-------------------------------------------------------------------------
+ *
+ * pgsql_fdw.h
+ * foreign-data wrapper for remote PostgreSQL servers.
+ *
+ * Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/pgsql_fdw.h
+ *
+ *-------------------------------------------------------------------------
+ */
+
+ #ifndef PGSQL_FDW_H
+ #define PGSQL_FDW_H
+
+ #include "postgres.h"
+ #include "foreign/foreign.h"
+ #include "nodes/relation.h"
+
+ /* in option.c */
+ int ExtractConnectionOptions(List *defelems,
+ const char **keywords,
+ const char **values);
+
+ /* in deparse.c */
+ char *deparseSimpleSql(Oid relid,
+ PlannerInfo *root,
+ RelOptInfo *baserel,
+ ForeignTable *table);
+
+ #endif /* PGSQL_FDW_H */
diff --git a/contrib/pgsql_fdw/sql/pgsql_fdw.sql b/contrib/pgsql_fdw/sql/pgsql_fdw.sql
index ...b3fac96 .
*** a/contrib/pgsql_fdw/sql/pgsql_fdw.sql
--- b/contrib/pgsql_fdw/sql/pgsql_fdw.sql
***************
*** 0 ****
--- 1,248 ----
+ -- ===================================================================
+ -- create FDW objects
+ -- ===================================================================
+
+ -- Clean up in case a prior regression run failed
+
+ -- Suppress NOTICE messages when roles don't exist
+ SET client_min_messages TO 'error';
+
+ DROP ROLE IF EXISTS pgsql_fdw_user;
+
+ RESET client_min_messages;
+
+ CREATE ROLE pgsql_fdw_user LOGIN SUPERUSER;
+ SET SESSION AUTHORIZATION 'pgsql_fdw_user';
+
+ CREATE EXTENSION pgsql_fdw;
+
+ CREATE SERVER loopback1 FOREIGN DATA WRAPPER pgsql_fdw;
+ CREATE SERVER loopback2 FOREIGN DATA WRAPPER pgsql_fdw
+ OPTIONS (dbname 'contrib_regression');
+
+ CREATE USER MAPPING FOR public SERVER loopback1
+ OPTIONS (user 'value', password 'value');
+ CREATE USER MAPPING FOR pgsql_fdw_user SERVER loopback2;
+
+ CREATE FOREIGN TABLE ft1 (
+ c0 int,
+ c1 int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10)
+ ) SERVER loopback2;
+ ALTER FOREIGN TABLE ft1 DROP COLUMN c0;
+
+ CREATE FOREIGN TABLE ft2 (
+ c0 int,
+ c1 int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10)
+ ) SERVER loopback2;
+ ALTER FOREIGN TABLE ft2 DROP COLUMN c0;
+
+ -- ===================================================================
+ -- create objects used through FDW
+ -- ===================================================================
+ CREATE SCHEMA "S 1";
+ CREATE TABLE "S 1"."T 1" (
+ "C 1" int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10),
+ CONSTRAINT t1_pkey PRIMARY KEY ("C 1")
+ );
+ CREATE TABLE "S 1"."T 2" (
+ c1 int NOT NULL,
+ c2 text,
+ CONSTRAINT t2_pkey PRIMARY KEY (c1)
+ );
+
+ BEGIN;
+ TRUNCATE "S 1"."T 1";
+ INSERT INTO "S 1"."T 1"
+ SELECT id,
+ id % 10,
+ to_char(id, 'FM00000'),
+ '1970-01-01'::timestamptz + ((id % 100) || ' days')::interval,
+ '1970-01-01'::timestamp + ((id % 100) || ' days')::interval,
+ id % 10,
+ id % 10
+ FROM generate_series(1, 1000) id;
+ TRUNCATE "S 1"."T 2";
+ INSERT INTO "S 1"."T 2"
+ SELECT id,
+ 'AAA' || to_char(id, 'FM000')
+ FROM generate_series(1, 100) id;
+ COMMIT;
+
+ -- ===================================================================
+ -- tests for pgsql_fdw_validator
+ -- ===================================================================
+ ALTER FOREIGN DATA WRAPPER pgsql_fdw OPTIONS (host 'value'); -- ERROR
+ -- requiressl, krbsrvname and gsslib are omitted because they depend on
+ -- configure option
+ ALTER SERVER loopback1 OPTIONS (
+ authtype 'value',
+ service 'value',
+ connect_timeout 'value',
+ dbname 'value',
+ host 'value',
+ hostaddr 'value',
+ port 'value',
+ --client_encoding 'value',
+ tty 'value',
+ options 'value',
+ application_name 'value',
+ --fallback_application_name 'value',
+ keepalives 'value',
+ keepalives_idle 'value',
+ keepalives_interval 'value',
+ -- requiressl 'value',
+ sslcompression 'value',
+ sslmode 'value',
+ sslcert 'value',
+ sslkey 'value',
+ sslrootcert 'value',
+ sslcrl 'value'
+ --requirepeer 'value',
+ -- krbsrvname 'value',
+ -- gsslib 'value',
+ --replication 'value'
+ );
+ ALTER SERVER loopback1 OPTIONS (user 'value'); -- ERROR
+ ALTER SERVER loopback2 OPTIONS (ADD fetch_count '2');
+ ALTER USER MAPPING FOR public SERVER loopback1
+ OPTIONS (DROP user, DROP password);
+ ALTER USER MAPPING FOR public SERVER loopback1
+ OPTIONS (host 'value'); -- ERROR
+ ALTER FOREIGN TABLE ft1 OPTIONS (nspname 'S 1', relname 'T 1');
+ ALTER FOREIGN TABLE ft2 OPTIONS (nspname 'S 1', relname 'T 1', fetch_count '100');
+ ALTER FOREIGN TABLE ft1 OPTIONS (invalid 'value'); -- ERROR
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count 'a'); -- ERROR
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count '0'); -- ERROR
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count '-1'); -- ERROR
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (invalid 'value'); -- ERROR
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (colname 'C 1');
+ ALTER FOREIGN TABLE ft2 ALTER COLUMN c1 OPTIONS (colname 'C 1');
+ \dew+
+ \des+
+ \deu+
+ \det+
+
+ -- ===================================================================
+ -- simple queries
+ -- ===================================================================
+ -- single table, with/without alias
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
+ SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+ SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+ -- with WHERE clause
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
+ SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
+ -- 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
+ SELECT * FROM ft1 t1 WHERE t1.c3 = (SELECT MAX(c3) FROM ft2 t2) ORDER BY c1;
+ -- used in CTE
+ WITH t1 AS (SELECT * FROM ft1 WHERE c1 <= 10) SELECT t2.c1, t2.c2, t2.c3, t2.c4 FROM t1, ft2 t2 WHERE t1.c1 = t2.c1 ORDER BY t1.c1;
+ -- fixed values
+ SELECT 'fixed', NULL FROM ft1 t1 WHERE c1 = 1;
+ -- user-defined operator/function
+ CREATE FUNCTION pgsql_fdw_abs(int) RETURNS int AS $$
+ BEGIN
+ RETURN abs($1);
+ END
+ $$ LANGUAGE plpgsql IMMUTABLE;
+ CREATE OPERATOR === (
+ LEFTARG = int,
+ RIGHTARG = int,
+ PROCEDURE = int4eq,
+ COMMUTATOR = ===,
+ NEGATOR = !==
+ );
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = pgsql_fdw_abs(t1.c2);
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 === t1.c2;
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = abs(t1.c2);
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = t1.c2;
+
+ -- ===================================================================
+ -- parameterized queries
+ -- ===================================================================
+ -- simple join
+ PREPARE st1(int, int) AS SELECT t1.c3, t2.c3 FROM ft1 t1, ft2 t2 WHERE t1.c1 = $1 AND t2.c1 = $2;
+ EXPLAIN (COSTS false) EXECUTE st1(1, 2);
+ EXECUTE st1(1, 1);
+ EXECUTE st1(101, 101);
+ -- subquery using stable function (can't be pushed down)
+ PREPARE st2(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c4) = 6) ORDER BY c1;
+ EXPLAIN (COSTS false) EXECUTE st2(10, 20);
+ EXECUTE st2(10, 20);
+ EXECUTE st1(101, 101);
+ -- subquery using immutable function (can be pushed down)
+ PREPARE st3(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c5) = 6) ORDER BY c1;
+ EXPLAIN (COSTS false) EXECUTE st3(10, 20);
+ EXECUTE st3(10, 20);
+ EXECUTE st3(20, 30);
+ -- custom plan should be chosen
+ PREPARE st4(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 = $1;
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ -- cleanup
+ DEALLOCATE st1;
+ DEALLOCATE st2;
+ DEALLOCATE st3;
+ DEALLOCATE st4;
+
+ -- ===================================================================
+ -- connection management
+ -- ===================================================================
+ SELECT srvname, usename FROM pgsql_fdw_connections;
+ SELECT pgsql_fdw_disconnect(srvid, usesysid) FROM pgsql_fdw_get_connections();
+ SELECT srvname, usename FROM pgsql_fdw_connections;
+
+ -- ===================================================================
+ -- subtransaction
+ -- ===================================================================
+ BEGIN;
+ DECLARE c CURSOR FOR SELECT * FROM ft1 ORDER BY c1;
+ FETCH c;
+ SAVEPOINT s;
+ ERROR OUT; -- ERROR
+ ROLLBACK TO s;
+ SELECT srvname FROM pgsql_fdw_connections;
+ FETCH c;
+ COMMIT;
+ SELECT srvname FROM pgsql_fdw_connections;
+ ERROR OUT; -- ERROR
+ SELECT srvname FROM pgsql_fdw_connections;
+
+ -- ===================================================================
+ -- cleanup
+ -- ===================================================================
+ DROP OPERATOR === (int, int) CASCADE;
+ DROP OPERATOR !== (int, int) CASCADE;
+ DROP FUNCTION pgsql_fdw_abs(int);
+ DROP SCHEMA "S 1" CASCADE;
+ DROP EXTENSION pgsql_fdw CASCADE;
+ \c
+ DROP ROLE pgsql_fdw_user;
diff --git a/doc/src/sgml/contrib.sgml b/doc/src/sgml/contrib.sgml
index d4da4ee..32056d1 100644
*** a/doc/src/sgml/contrib.sgml
--- b/doc/src/sgml/contrib.sgml
*************** CREATE EXTENSION <replaceable>module_nam
*** 117,122 ****
--- 117,123 ----
&pgcrypto;
&pgfreespacemap;
&pgrowlocks;
+ &pgsql-fdw;
&pgstandby;
&pgstatstatements;
&pgstattuple;
diff --git a/doc/src/sgml/filelist.sgml b/doc/src/sgml/filelist.sgml
index b5d3c6d..de686ee 100644
*** a/doc/src/sgml/filelist.sgml
--- b/doc/src/sgml/filelist.sgml
***************
*** 125,130 ****
--- 125,131 ----
<!ENTITY pgcrypto SYSTEM "pgcrypto.sgml">
<!ENTITY pgfreespacemap SYSTEM "pgfreespacemap.sgml">
<!ENTITY pgrowlocks SYSTEM "pgrowlocks.sgml">
+ <!ENTITY pgsql-fdw SYSTEM "pgsql-fdw.sgml">
<!ENTITY pgstandby SYSTEM "pgstandby.sgml">
<!ENTITY pgstatstatements SYSTEM "pgstatstatements.sgml">
<!ENTITY pgstattuple SYSTEM "pgstattuple.sgml">
diff --git a/doc/src/sgml/pgsql-fdw.sgml b/doc/src/sgml/pgsql-fdw.sgml
index ...30fa7f5 .
*** a/doc/src/sgml/pgsql-fdw.sgml
--- b/doc/src/sgml/pgsql-fdw.sgml
***************
*** 0 ****
--- 1,261 ----
+ <!-- doc/src/sgml/pgsql-fdw.sgml -->
+
+ <sect1 id="pgsql-fdw" xreflabel="pgsql_fdw">
+ <title>pgsql_fdw</title>
+
+ <indexterm zone="pgsql-fdw">
+ <primary>pgsql_fdw</primary>
+ </indexterm>
+
+ <para>
+ The <filename>pgsql_fdw</filename> module provides a foreign-data wrapper for
+ external <productname>PostgreSQL</productname> servers.
+ With this module, users can access data stored in external
+ <productname>PostgreSQL</productname> via plain SQL statements.
+ </para>
+
+ <para>
+ Note that default wrapper <literal>pgsql_fdw</literal> is created
+ automatically during <command>CREATE EXTENSION</command> command for
+ <application>pgsql_fdw</application>.
+ </para>
+
+ <sect2>
+ <title>FDW Options of pgsql_fdw</title>
+
+ <sect3>
+ <title>Connection Options</title>
+ <para>
+ A foreign server and user mapping created using this wrapper can have
+ <application>libpq</> connection options, expect below:
+
+ <itemizedlist>
+ <listitem><para>client_encoding</para></listitem>
+ <listitem><para>fallback_application_name</para></listitem>
+ <listitem><para>replication</para></listitem>
+ </itemizedlist>
+
+ For details of <application>libpq</> connection options, see
+ <xref linkend="libpq-connect">.
+ </para>
+
+ <para>
+ <literal>user</literal> and <literal>password</literal> can be
+ specified on user mappings, and others can be specified on foreign servers.
+ </para>
+ </sect3>
+
+ <sect3>
+ <title>Object Name Options</title>
+ <para>
+ Foreign tables which were created using this wrapper, or its columns can
+ have object name options. These options can be used to specify the names
+ used in SQL statement sent to remote <productname>PostgreSQL</productname>
+ server. These options are useful when a remote object has different name
+ from corresponding local one.
+ </para>
+
+ <variablelist>
+
+ <varlistentry>
+ <term><literal>nspname</literal></term>
+ <listitem>
+ <para>
+ This option, which can be specified on a foreign table, is used as a
+ namespace (schema) reference in the SQL statement. If this options is
+ omitted, <literal>pg_class.nspname</literal> of the foreign table is
+ used.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>relname</literal></term>
+ <listitem>
+ <para>
+ This option, which can be specified on a foreign table, is used as a
+ relation (table) reference in the SQL statement. If this options is
+ omitted, <literal>pg_class.relname</literal> of the foreign table is
+ used.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>colname</literal></term>
+ <listitem>
+ <para>
+ This option, which can be specified on a column of a foreign table, is
+ used as a column (attribute) reference in the SQL statement. If this
+ option is omitted, <literal>pg_attribute.attname</literal> of the column
+ of the foreign table is used.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ </variablelist>
+
+ </sect3>
+
+ <sect3>
+ <title>Cursor Options</title>
+ <para>
+ The <application>pgsql_fdw</application> always uses cursor to retrieve the
+ result from external server. Users can control the behavior of cursor by
+ setting cursor options to foreign table or foreign server. If an option
+ is set to both objects, finer-grained setting is used. In other words,
+ foreign table's setting overrides foreign server's setting.
+ </para>
+
+ <variablelist>
+
+ <varlistentry>
+ <term><literal>fetch_count</literal></term>
+ <listitem>
+ <para>
+ This option specifies the number of rows to be fetched at a time.
+ This option accepts only integer value larger than zero. The default
+ setting is 10000.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ </variablelist>
+
+ </sect3>
+
+ </sect2>
+
+ <sect2>
+ <title>Connection Management</title>
+
+ <para>
+ The <application>pgsql_fdw</application> establishes a connection to a
+ foreign server in the beginning of the first query which uses a foreign
+ table associated to the foreign server, and reuses the connection following
+ queries and even in following foreign scans in same query.
+
+ You can see the list of active connections via
+ <structname>pgsql_fdw_connections</structname> view. It shows pair of oid
+ and name of server and local role for each active connections established by
+ <application>pgsql_fdw</application>. For security reason, only superuser
+ can see other role's connections.
+ </para>
+
+ <para>
+ Established connections are kept alive until local role changes or the
+ current transaction aborts or user requests so.
+ </para>
+
+ <para>
+ If role has been changed, active connections established as old local role
+ is kept alive but never be reused until local role has restored to original
+ role. This kind of situation happens with <command>SET ROLE</command> and
+ <command>SET SESSION AUTHORIZATION</command>.
+ </para>
+
+ <para>
+ If current transaction aborts by error or user request, all active
+ connections are disconnected automatically. This behavior avoids possible
+ connection leaks on error.
+ </para>
+
+ <para>
+ You can discard persistent connection at arbitrary timing with
+ <function>pgsql_fdw_disconnect()</function>. It takes server oid and
+ user oid as arguments. This function can handle only connections
+ established in current session; connections established by other backends
+ are not reachable.
+ </para>
+
+ <para>
+ You can discard all active and visible connections in current session with
+ using <structname>pgsql_fdw_connections</structname> and
+ <function>pgsql_fdw_disconnect()</function> together:
+ <synopsis>
+ postgres=# SELECT pgsql_fdw_disconnect(srvid, usesysid) FROM pgsql_fdw_connections;
+ pgsql_fdw_disconnect
+ ----------------------
+ OK
+ OK
+ (2 rows)
+ </synopsis>
+ </para>
+ </sect2>
+
+ <sect2>
+ <title>Transaction Management</title>
+ <para>
+ The <application>pgsql_fdw</application> begins remote transaction at the
+ beginning of a local query, and terminates it with <command>ABORT</command>
+ at the end of the local query. This means that all foreign scans on a
+ foreign server in a local query are executed in one transaction.
+ If isolation level of local transaction is <literal>SERIALIZABLE</literal>,
+ <literal>SERIALIZABLE</literal> is used for remote transaction. Otherwise,
+ if isolation level of local transaction is one of
+ <literal>READ UNCOMMITTED</literal>, <literal>READ COMMITTED</literal> or
+ <literal>REPEATABLE READ</literal>, then <literal>REPEATABLE READ</literal>
+ is used for remote transaction.
+ <literal>READ UNCOMMITTED</literal> and <literal>READ COMMITTED</literal>
+ are never used for remote transaction, because even
+ <literal>READ COMMITTED</literal> transaction might produce inconsistent
+ results, if remote data have been updated between two remote queries.
+ </para>
+ <para>
+ Note that even if the isolation level of local transaction was
+ <literal>SERIALIZABLE</literal> or <literal>REPEATABLE READ</literal>,
+ series of one query might produce different result, because foreign scans
+ in different local queries are executed in different remote transactions.
+ For instance, when client started a local transaction
+ explicitly with isolation level <literal>SERIALIZABLE</literal>, and
+ executed same local query which contains a foreign table which references
+ foreign data which is updated frequently, latter result would be different
+ from former result.
+ </para>
+ <para>
+ This restriction might be relaxed in future release.
+ </para>
+ </sect2>
+
+ <sect2>
+ <title>Estimation of Costs and Rows</title>
+ <para>
+ The <application>pgsql_fdw</application> estimates the costs of a foreign
+ scan by adding up some basic costs: connection costs, remote query costs
+ and data transfer costs.
+ To get remote query costs, <application>pgsql_fdw</application> executes
+ <command>EXPLAIN</command> command on remote server for each foreign scan.
+ </para>
+ <para>
+ On the other hand, estimated rows which was returned by
+ <command>EXPLAIN</command> is used for local estimation as-is.
+ </para>
+ </sect2>
+
+ <sect2>
+ <title>EXPLAIN Output</title>
+ <para>
+ For a foreign table using <literal>pgsql_fdw</>, <command>EXPLAIN</> shows
+ a remote SQL statement which is sent to remote
+ <productname>PostgreSQL</productname> server for a ForeignScan plan node.
+ For example:
+ </para>
+ <synopsis>
+ postgres=# EXPLAIN SELECT aid FROM pgbench_accounts WHERE abalance < 0;
+ QUERY PLAN
+ --------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on pgbench_accounts (cost=100.00..8105.13 rows=302613 width=8)
+ Filter: (abalance < 0)
+ Remote SQL: DECLARE pgsql_fdw_cursor_3 SCROLL CURSOR FOR SELECT aid, NULL, abalance, NULL FROM public.pgbench_accounts
+ (3 rows)
+ </synopsis>
+ </sect2>
+
+ <sect2>
+ <title>Author</title>
+ <para>
+ Shigeru Hanada <email>shigeru.hanada@gmail.com</email>
+ </para>
+ </sect2>
+
+ </sect1>
pgsql_fdw_pushdown_v10.patchtext/plain; name=pgsql_fdw_pushdown_v10.patchDownload
diff --git a/contrib/pgsql_fdw/deparse.c b/contrib/pgsql_fdw/deparse.c
index beb62fe..a9585e9 100644
*** a/contrib/pgsql_fdw/deparse.c
--- b/contrib/pgsql_fdw/deparse.c
***************
*** 14,19 ****
--- 14,21 ----
#include "access/transam.h"
#include "catalog/pg_class.h"
+ #include "catalog/pg_operator.h"
+ #include "catalog/pg_type.h"
#include "commands/defrem.h"
#include "foreign/foreign.h"
#include "lib/stringinfo.h"
***************
*** 22,30 ****
--- 24,34 ----
#include "nodes/makefuncs.h"
#include "optimizer/clauses.h"
#include "optimizer/var.h"
+ #include "parser/parser.h"
#include "parser/parsetree.h"
#include "utils/builtins.h"
#include "utils/lsyscache.h"
+ #include "utils/syscache.h"
#include "pgsql_fdw.h"
*************** static void deparseRelation(StringInfo b
*** 44,49 ****
--- 48,81 ----
bool need_prefix);
static void deparseVar(StringInfo buf, Var *node, PlannerInfo *root,
RelOptInfo *baserel, bool need_prefix);
+ static void deparseConst(StringInfo buf, Const *node, PlannerInfo *root,
+ RelOptInfo *baserel);
+ static void deparseBoolExpr(StringInfo buf, BoolExpr *node, PlannerInfo *root,
+ RelOptInfo *baserel);
+ static void deparseNullTest(StringInfo buf, NullTest *node, PlannerInfo *root,
+ RelOptInfo *baserel);
+ static void deparseDistinctExpr(StringInfo buf, DistinctExpr *node,
+ PlannerInfo *root, RelOptInfo *baserel);
+ static void deparseRelabelType(StringInfo buf, RelabelType *node,
+ PlannerInfo *root, RelOptInfo *baserel);
+ static void deparseFuncExpr(StringInfo buf, FuncExpr *node, PlannerInfo *root,
+ RelOptInfo *baserel);
+ static void deparseParam(StringInfo buf, Param *node, PlannerInfo *root,
+ RelOptInfo *baserel);
+ static void deparseScalarArrayOpExpr(StringInfo buf, ScalarArrayOpExpr *node,
+ PlannerInfo *root, RelOptInfo *baserel);
+ static void deparseOpExpr(StringInfo buf, OpExpr *node, PlannerInfo *root,
+ RelOptInfo *baserel);
+ static void deparseArrayRef(StringInfo buf, ArrayRef *node, PlannerInfo *root,
+ RelOptInfo *baserel);
+ static void deparseArrayExpr(StringInfo buf, ArrayExpr *node, PlannerInfo *root, RelOptInfo *baserel);
+
+ /*
+ * Determine whether an expression can be evaluated on remote side safely.
+ */
+ static bool is_foreign_expr(PlannerInfo *root, RelOptInfo *baserel, Expr *expr);
+ static bool foreign_expr_walker(Node *node, foreign_executable_cxt *context);
+ static bool is_builtin(Oid procid);
/*
* Deparse query representation into SQL statement which suits for remote
*************** deparseSimpleSql(Oid relid,
*** 152,157 ****
--- 184,268 ----
}
/*
+ * Examine baserestrictinfo of baserel, and extract expressions which can be
+ * evaluated on remote side safely. This function never changes
+ * baserestrictinfo.
+ */
+ List *
+ extractRemoteExprs(Oid relid,
+ PlannerInfo *root,
+ RelOptInfo *baserel)
+ {
+ ListCell *lc;
+ List *exprs = NIL;
+
+ foreach(lc, baserel->baserestrictinfo)
+ {
+ RestrictInfo *ri = (RestrictInfo *) lfirst(lc);
+
+ if (is_foreign_expr(root, baserel, ri->clause))
+ exprs = lappend(exprs, ri->clause);
+ }
+
+ return exprs;
+ }
+
+ void
+ deparseExpr(StringInfo buf, Expr *node, PlannerInfo *root, RelOptInfo *baserel)
+ {
+ /*
+ * This part must be match foreign_expr_walker.
+ */
+ switch (nodeTag(node))
+ {
+ case T_Const:
+ deparseConst(buf, (Const *) node, root, baserel);
+ break;
+ case T_BoolExpr:
+ deparseBoolExpr(buf, (BoolExpr *) node, root, baserel);
+ break;
+ case T_NullTest:
+ deparseNullTest(buf, (NullTest *) node, root, baserel);
+ break;
+ case T_DistinctExpr:
+ deparseDistinctExpr(buf, (DistinctExpr *) node, root, baserel);
+ break;
+ case T_RelabelType:
+ deparseRelabelType(buf, (RelabelType *) node, root, baserel);
+ break;
+ case T_FuncExpr:
+ deparseFuncExpr(buf, (FuncExpr *) node, root, baserel);
+ break;
+ case T_Param:
+ deparseParam(buf, (Param *) node, root, baserel);
+ break;
+ case T_ScalarArrayOpExpr:
+ deparseScalarArrayOpExpr(buf, (ScalarArrayOpExpr *) node, root,
+ baserel);
+ break;
+ case T_OpExpr:
+ deparseOpExpr(buf, (OpExpr *) node, root, baserel);
+ break;
+ case T_Var:
+ deparseVar(buf, (Var *) node, root, baserel, false);
+ break;
+ case T_ArrayRef:
+ deparseArrayRef(buf, (ArrayRef *) node, root, baserel);
+ break;
+ case T_ArrayExpr:
+ deparseArrayExpr(buf, (ArrayExpr *) node, root, baserel);
+ break;
+ default:
+ {
+ ereport(ERROR,
+ (errmsg("unsupported expression for deparse"),
+ errdetail("%s", nodeToString(node))));
+ }
+ break;
+ }
+ }
+
+ /*
* Deparse node into buf, with relation qualifier if need_prefix was true. If
* node is a column of a foreign table, use value of colname FDW option (if any)
* instead of attribute name.
*************** deparseRelation(StringInfo buf,
*** 288,290 ****
--- 399,811 ----
appendStringInfo(buf, "%s.", q_nspname);
appendStringInfo(buf, "%s", q_relname);
}
+
+ static void
+ deparseConst(StringInfo buf,
+ Const *node,
+ PlannerInfo *root,
+ RelOptInfo *baserel)
+ {
+ Oid typoutput;
+ bool typIsVarlena;
+ char *extval;
+
+ if (node->constisnull)
+ {
+ appendStringInfo(buf, "NULL");
+ return;
+ }
+
+ getTypeOutputInfo(node->consttype,
+ &typoutput, &typIsVarlena);
+ extval = OidOutputFunctionCall(typoutput, node->constvalue);
+
+ switch (node->consttype)
+ {
+ case INT2OID:
+ case INT4OID:
+ case INT8OID:
+ case OIDOID:
+ case FLOAT4OID:
+ case FLOAT8OID:
+ case NUMERICOID:
+ {
+ /*
+ * No need to quote unless they contain special values such as
+ * 'Nan'.
+ */
+ if (strspn(extval, "0123456789+-eE.") == strlen(extval))
+ {
+ if (extval[0] == '+' || extval[0] == '-')
+ appendStringInfo(buf, "(%s)", extval);
+ else
+ appendStringInfoString(buf, extval);
+ }
+ else
+ appendStringInfo(buf, "'%s'", extval);
+ }
+ break;
+ case BITOID:
+ case VARBITOID:
+ appendStringInfo(buf, "B'%s'", extval);
+ break;
+ case BOOLOID:
+ if (strcmp(extval, "t") == 0)
+ appendStringInfoString(buf, "true");
+ else
+ appendStringInfoString(buf, "false");
+ break;
+
+ default:
+ {
+ const char *valptr;
+
+ appendStringInfoChar(buf, '\'');
+ for (valptr = extval; *valptr; valptr++)
+ {
+ char ch = *valptr;
+
+ /*
+ * standard_conforming_strings of remote session should be
+ * set to similar value as local session.
+ */
+ if (SQL_STR_DOUBLE(ch, !standard_conforming_strings))
+ appendStringInfoChar(buf, ch);
+ appendStringInfoChar(buf, ch);
+ }
+ appendStringInfoChar(buf, '\'');
+ }
+ break;
+ }
+ }
+
+ static void
+ deparseBoolExpr(StringInfo buf,
+ BoolExpr *node,
+ PlannerInfo *root,
+ RelOptInfo *baserel)
+ {
+ ListCell *lc;
+ char *op;
+ bool first;
+
+ switch (node->boolop)
+ {
+ case AND_EXPR:
+ op = "AND";
+ break;
+ case OR_EXPR:
+ op = "OR";
+ break;
+ case NOT_EXPR:
+ appendStringInfo(buf, "(NOT ");
+ deparseExpr(buf, list_nth(node->args, 0), root, baserel);
+ appendStringInfo(buf, ")");
+ return;
+ }
+
+ first = true;
+ appendStringInfo(buf, "(");
+ foreach(lc, node->args)
+ {
+ if (!first)
+ appendStringInfo(buf, " %s ", op);
+ deparseExpr(buf, (Expr *) lfirst(lc), root, baserel);
+ first = false;
+ }
+ appendStringInfo(buf, ")");
+ }
+
+ static void
+ deparseNullTest(StringInfo buf,
+ NullTest *node,
+ PlannerInfo *root,
+ RelOptInfo *baserel)
+ {
+ elog(ERROR, "NullTest is not supported");
+ }
+
+ static void
+ deparseDistinctExpr(StringInfo buf,
+ DistinctExpr *node,
+ PlannerInfo *root,
+ RelOptInfo *baserel)
+ {
+ elog(ERROR, "DistinctExpr not supported");
+ }
+
+ static void
+ deparseRelabelType(StringInfo buf,
+ RelabelType *node,
+ PlannerInfo *root,
+ RelOptInfo *baserel)
+ {
+ elog(ERROR, "RelabelType is not supported");
+ }
+
+ static void
+ deparseFuncExpr(StringInfo buf,
+ FuncExpr *node,
+ PlannerInfo *root,
+ RelOptInfo *baserel)
+ {
+ elog(ERROR, "FuncExpr is not supported");
+ }
+
+ static void
+ deparseParam(StringInfo buf,
+ Param *node,
+ PlannerInfo *root,
+ RelOptInfo *baserel)
+ {
+ elog(ERROR, "Param is not supported");
+ }
+
+ static void
+ deparseScalarArrayOpExpr(StringInfo buf,
+ ScalarArrayOpExpr *node,
+ PlannerInfo *root,
+ RelOptInfo *baserel)
+ {
+ elog(ERROR, "ScalarArrayOpExpr is not supported");
+ }
+
+ static void
+ deparseOpExpr(StringInfo buf,
+ OpExpr *node,
+ PlannerInfo *root,
+ RelOptInfo *baserel)
+ {
+ HeapTuple tuple;
+ Form_pg_operator form;
+ char *opname;
+ char oprkind;
+ ListCell *arg = list_head(node->args);
+
+ /* Retieve necessary information about the operator from system catalog. */
+ tuple = SearchSysCache1(OPEROID, ObjectIdGetDatum(node->opno));
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "cache lookup failed for operator %u", node->opno);
+ form = (Form_pg_operator) GETSTRUCT(tuple);
+ opname = NameStr(form->oprname);
+ oprkind = form->oprkind;
+ ReleaseSysCache(tuple);
+
+ Assert((oprkind == 'r' && list_length(node->args) == 1) ||
+ (oprkind == 'l' && list_length(node->args) == 1) ||
+ (oprkind == 'b' && list_length(node->args) == 2));
+
+ appendStringInfo(buf, "(");
+ if (oprkind == 'b' || oprkind == 'r')
+ {
+ deparseExpr(buf, lfirst(arg), root, baserel);
+ arg = lnext(arg);
+ }
+
+ appendStringInfo(buf, " %s ", opname);
+
+ if (oprkind == 'b' || oprkind == 'r')
+ deparseExpr(buf, lfirst(arg), root, baserel);
+ appendStringInfo(buf, ")");
+ }
+
+ static void
+ deparseArrayRef(StringInfo buf,
+ ArrayRef *node,
+ PlannerInfo *root,
+ RelOptInfo *baserel)
+ {
+ elog(ERROR, "ArrayRef is not supported");
+ }
+
+ static void
+ deparseArrayExpr(StringInfo buf,
+ ArrayExpr *node,
+ PlannerInfo *root,
+ RelOptInfo *baserel)
+ {
+ elog(ERROR, "ArrayExpr is not supported");
+ }
+
+ /*
+ * Returns true if expr is safe to be evaluated on the foreign server.
+ */
+ static bool
+ is_foreign_expr(PlannerInfo *root, RelOptInfo *baserel, Expr *expr)
+ {
+ foreign_executable_cxt context;
+ context.root = root;
+ context.foreignrel = baserel;
+
+ /*
+ * An expression which includes any mutable function can't be pushed down
+ * because it's result is not stable. For example, pushing now() down to
+ * remote side would cause confusion from the clock offset.
+ * If we have routine mapping infrastructure in future release, we will be
+ * able to choose function to be pushed down in finer granularity.
+ */
+ if (contain_mutable_functions((Node *) expr))
+ return false;
+
+ /*
+ * Check that the expression consists of nodes which are known as safe to
+ * be pushed down.
+ */
+ if (foreign_expr_walker((Node *) expr, &context))
+ return false;
+
+ return true;
+ }
+
+ /*
+ * Return true if node includes any node which is not known as safe to be
+ * pushed down.
+ */
+ static bool
+ foreign_expr_walker(Node *node, foreign_executable_cxt *context)
+ {
+ if (node == NULL)
+ return false;
+
+ /*
+ * If given expression has valid collation, it can't be pushed down because
+ * it might has incompatible semantics on remote side.
+ */
+ if (exprCollation(node) != InvalidOid ||
+ exprInputCollation(node) != InvalidOid)
+ return true;
+
+ /*
+ * If return type of given expression is not built-in, it can't be pushed
+ * down because it might has incompatible semantics on remote side.
+ */
+ if (!is_builtin(exprType(node)))
+ return true;
+
+ switch (nodeTag(node))
+ {
+ case T_Const:
+ case T_BoolExpr:
+ break;
+ #ifdef NOT_SUPPORTED
+ case T_NullTest:
+ case T_DistinctExpr:
+ case T_RelabelType:
+ /*
+ * 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 at the tail of this function.
+ */
+ break;
+ /*
+ * If function used by the expression is not built-in, it can't be
+ * pushed down because it might has incompatible semantics on remote
+ * side.
+ */
+ case T_FuncExpr:
+ {
+ FuncExpr *fe = (FuncExpr *) node;
+ if (!is_builtin(fe->funcid))
+ return true;
+ }
+ break;
+ case T_Param:
+ /*
+ * Only external parameters can be pushed down.:
+ */
+ {
+ if (((Param *) node)->paramkind != PARAM_EXTERN)
+ return true;
+ }
+ break;
+ case T_ScalarArrayOpExpr:
+ /*
+ * Only built-in operators can be pushed down. In addition,
+ * underlying function must be built-in and immutable, but we don't
+ * check volatility here; such check must be done already with
+ * contain_mutable_functions.
+ */
+ {
+ ScalarArrayOpExpr *oe = (ScalarArrayOpExpr *) node;
+
+ if (!is_builtin(oe->opno) || !is_builtin(oe->opfuncid))
+ return true;
+
+ /* operands are checked later */
+ }
+ break;
+ #endif
+ case T_OpExpr:
+ /*
+ * Only built-in operators can be pushed down. In addition,
+ * underlying function must be built-in and immutable, but we don't
+ * check volatility here; such check must be done already with
+ * contain_mutable_functions.
+ */
+ {
+ OpExpr *oe = (OpExpr *) node;
+
+ if (!is_builtin(oe->opno) || !is_builtin(oe->opfuncid))
+ return true;
+
+ /* operands are checked later */
+ }
+ 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;
+ foreign_executable_cxt *f_context;
+
+ f_context = (foreign_executable_cxt *) context;
+ if (var->varno != f_context->foreignrel->relid ||
+ var->varlevelsup != 0)
+ return true;
+ }
+ break;
+ #ifdef NOT_SUPPORTED
+ case T_ArrayRef:
+ /*
+ * ArrayRef which holds non-built-in typed elements can't be pushed
+ * down.
+ */
+ {
+ if (!is_builtin(((ArrayRef *) node)->refelemtype))
+ return true;
+ }
+ break;
+ case T_ArrayExpr:
+ /*
+ * ArrayExpr which holds non-built-in typed elements can't be pushed
+ * down.
+ */
+ {
+ if (!is_builtin(((ArrayExpr *) node)->element_typeid))
+ return true;
+ }
+ break;
+ #endif
+ default:
+ {
+ ereport(DEBUG3,
+ (errmsg("expression is too complex"),
+ errdetail("%s", nodeToString(node))));
+ return true;
+ }
+ break;
+ }
+
+ return expression_tree_walker(node, foreign_expr_walker, context);
+ }
+
+ /*
+ * Return true if given object is one of built-in objects.
+ */
+ static bool
+ is_builtin(Oid oid)
+ {
+ return (oid < FirstNormalObjectId);
+ }
diff --git a/contrib/pgsql_fdw/expected/pgsql_fdw.out b/contrib/pgsql_fdw/expected/pgsql_fdw.out
index 5bac241..d60c768 100644
*** a/contrib/pgsql_fdw/expected/pgsql_fdw.out
--- b/contrib/pgsql_fdw/expected/pgsql_fdw.out
*************** SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.
*** 230,241 ****
-- with WHERE clause
EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
! QUERY PLAN
! ------------------------------------------------------------------------------------------------------------------
Foreign Scan on public.ft1 t1
Output: c1, c2, c3, c4, c5, c6, c7
Filter: ((t1.c1 = 101) AND ((t1.c6)::text = '1'::text) AND (t1.c7 = '1'::bpchar))
! Remote SQL: DECLARE pgsql_fdw_cursor_4 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
(4 rows)
SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
--- 230,241 ----
-- with WHERE clause
EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
! QUERY PLAN
! ----------------------------------------------------------------------------------------------------------------------------------------
Foreign Scan on public.ft1 t1
Output: c1, c2, c3, c4, c5, c6, c7
Filter: ((t1.c1 = 101) AND ((t1.c6)::text = '1'::text) AND (t1.c7 = '1'::bpchar))
! Remote SQL: DECLARE pgsql_fdw_cursor_4 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" = 101))
(4 rows)
SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
*************** EXPLAIN (COSTS false) SELECT * FROM ft1
*** 351,361 ****
(3 rows)
EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = t1.c2;
! QUERY PLAN
! -------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
Filter: (c1 = c2)
! Remote SQL: DECLARE pgsql_fdw_cursor_19 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
(3 rows)
-- ===================================================================
--- 351,361 ----
(3 rows)
EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = t1.c2;
! QUERY PLAN
! ----------------------------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
Filter: (c1 = c2)
! Remote SQL: DECLARE pgsql_fdw_cursor_19 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" = c2))
(3 rows)
-- ===================================================================
*************** EXPLAIN (COSTS false) SELECT * FROM ft1
*** 364,380 ****
-- simple join
PREPARE st1(int, int) AS SELECT t1.c3, t2.c3 FROM ft1 t1, ft2 t2 WHERE t1.c1 = $1 AND t2.c1 = $2;
EXPLAIN (COSTS false) EXECUTE st1(1, 2);
! QUERY PLAN
! -----------------------------------------------------------------------------------------------------------------------------------------
Nested Loop
-> Foreign Scan on ft1 t1
Filter: (c1 = 1)
! Remote SQL: DECLARE pgsql_fdw_cursor_20 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, NULL, NULL, NULL, NULL FROM "S 1"."T 1"
! -> Materialize
! -> Foreign Scan on ft2 t2
! Filter: (c1 = 2)
! Remote SQL: DECLARE pgsql_fdw_cursor_21 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, NULL, NULL, NULL, NULL FROM "S 1"."T 1"
! (8 rows)
EXECUTE st1(1, 1);
c3 | c3
--- 364,379 ----
-- simple join
PREPARE st1(int, int) AS SELECT t1.c3, t2.c3 FROM ft1 t1, ft2 t2 WHERE t1.c1 = $1 AND t2.c1 = $2;
EXPLAIN (COSTS false) EXECUTE st1(1, 2);
! QUERY PLAN
! -------------------------------------------------------------------------------------------------------------------------------------------------------
Nested Loop
-> Foreign Scan on ft1 t1
Filter: (c1 = 1)
! Remote SQL: DECLARE pgsql_fdw_cursor_20 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, NULL, NULL, NULL, NULL FROM "S 1"."T 1" WHERE (("C 1" = 1))
! -> Foreign Scan on ft2 t2
! Filter: (c1 = 2)
! Remote SQL: DECLARE pgsql_fdw_cursor_21 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, NULL, NULL, NULL, NULL FROM "S 1"."T 1" WHERE (("C 1" = 2))
! (7 rows)
EXECUTE st1(1, 1);
c3 | c3
*************** EXECUTE st1(101, 101);
*** 391,410 ****
-- subquery using stable function (can't be pushed down)
PREPARE st2(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c4) = 6) ORDER BY c1;
EXPLAIN (COSTS false) EXECUTE st2(10, 20);
! QUERY PLAN
! ---------------------------------------------------------------------------------------------------------------------------------------------------
Sort
Sort Key: t1.c1
-> Hash Join
Hash Cond: (t1.c3 = t2.c3)
-> Foreign Scan on ft1 t1
Filter: (c1 < 20)
! Remote SQL: DECLARE pgsql_fdw_cursor_26 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
-> Hash
-> HashAggregate
-> Foreign Scan on ft2 t2
Filter: ((c1 > 10) AND (date_part('dow'::text, c4) = 6::double precision))
! Remote SQL: DECLARE pgsql_fdw_cursor_27 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, c4, NULL, NULL, NULL FROM "S 1"."T 1"
(12 rows)
EXECUTE st2(10, 20);
--- 390,409 ----
-- subquery using stable function (can't be pushed down)
PREPARE st2(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c4) = 6) ORDER BY c1;
EXPLAIN (COSTS false) EXECUTE st2(10, 20);
! QUERY PLAN
! ------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Sort
Sort Key: t1.c1
-> Hash Join
Hash Cond: (t1.c3 = t2.c3)
-> Foreign Scan on ft1 t1
Filter: (c1 < 20)
! Remote SQL: DECLARE pgsql_fdw_cursor_26 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" < 20))
-> Hash
-> HashAggregate
-> Foreign Scan on ft2 t2
Filter: ((c1 > 10) AND (date_part('dow'::text, c4) = 6::double precision))
! Remote SQL: DECLARE pgsql_fdw_cursor_27 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, c4, NULL, NULL, NULL FROM "S 1"."T 1" WHERE (("C 1" > 10))
(12 rows)
EXECUTE st2(10, 20);
*************** EXECUTE st1(101, 101);
*** 422,441 ****
-- subquery using immutable function (can be pushed down)
PREPARE st3(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c5) = 6) ORDER BY c1;
EXPLAIN (COSTS false) EXECUTE st3(10, 20);
! QUERY PLAN
! ---------------------------------------------------------------------------------------------------------------------------------------------------
Sort
Sort Key: t1.c1
-> Hash Join
Hash Cond: (t1.c3 = t2.c3)
-> Foreign Scan on ft1 t1
Filter: (c1 < 20)
! Remote SQL: DECLARE pgsql_fdw_cursor_32 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
-> Hash
-> HashAggregate
-> Foreign Scan on ft2 t2
Filter: ((c1 > 10) AND (date_part('dow'::text, c5) = 6::double precision))
! Remote SQL: DECLARE pgsql_fdw_cursor_33 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, NULL, c5, NULL, NULL FROM "S 1"."T 1"
(12 rows)
EXECUTE st3(10, 20);
--- 421,440 ----
-- subquery using immutable function (can be pushed down)
PREPARE st3(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c5) = 6) ORDER BY c1;
EXPLAIN (COSTS false) EXECUTE st3(10, 20);
! QUERY PLAN
! ------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Sort
Sort Key: t1.c1
-> Hash Join
Hash Cond: (t1.c3 = t2.c3)
-> Foreign Scan on ft1 t1
Filter: (c1 < 20)
! Remote SQL: DECLARE pgsql_fdw_cursor_32 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" < 20))
-> Hash
-> HashAggregate
-> Foreign Scan on ft2 t2
Filter: ((c1 > 10) AND (date_part('dow'::text, c5) = 6::double precision))
! Remote SQL: DECLARE pgsql_fdw_cursor_33 SCROLL CURSOR FOR SELECT "C 1", NULL, c3, NULL, c5, NULL, NULL FROM "S 1"."T 1" WHERE (("C 1" > 10))
(12 rows)
EXECUTE st3(10, 20);
*************** EXECUTE st3(20, 30);
*** 453,503 ****
-- custom plan should be chosen
PREPARE st4(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 = $1;
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! -------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
Filter: (c1 = 1)
! Remote SQL: DECLARE pgsql_fdw_cursor_38 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
(3 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! -------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
Filter: (c1 = 1)
! Remote SQL: DECLARE pgsql_fdw_cursor_39 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
(3 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! -------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
Filter: (c1 = 1)
! Remote SQL: DECLARE pgsql_fdw_cursor_40 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
(3 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! -------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
Filter: (c1 = 1)
! Remote SQL: DECLARE pgsql_fdw_cursor_41 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
(3 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! -------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
Filter: (c1 = 1)
! Remote SQL: DECLARE pgsql_fdw_cursor_42 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
(3 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! -------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Filter: (c1 = $1)
! Remote SQL: DECLARE pgsql_fdw_cursor_43 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
(3 rows)
-- cleanup
--- 452,502 ----
-- custom plan should be chosen
PREPARE st4(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 = $1;
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! ---------------------------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
Filter: (c1 = 1)
! Remote SQL: DECLARE pgsql_fdw_cursor_38 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" = 1))
(3 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! ---------------------------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
Filter: (c1 = 1)
! Remote SQL: DECLARE pgsql_fdw_cursor_39 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" = 1))
(3 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! ---------------------------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
Filter: (c1 = 1)
! Remote SQL: DECLARE pgsql_fdw_cursor_40 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" = 1))
(3 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! ---------------------------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
Filter: (c1 = 1)
! Remote SQL: DECLARE pgsql_fdw_cursor_41 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" = 1))
(3 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! ---------------------------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
Filter: (c1 = 1)
! Remote SQL: DECLARE pgsql_fdw_cursor_42 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" = 1))
(3 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! ---------------------------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Filter: (c1 = 1)
! Remote SQL: DECLARE pgsql_fdw_cursor_44 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" = 1))
(3 rows)
-- cleanup
diff --git a/contrib/pgsql_fdw/pgsql_fdw.c b/contrib/pgsql_fdw/pgsql_fdw.c
index 370d240..7746ab9 100644
*** a/contrib/pgsql_fdw/pgsql_fdw.c
--- b/contrib/pgsql_fdw/pgsql_fdw.c
*************** typedef struct PgsqlFdwPlanState {
*** 75,80 ****
--- 75,84 ----
char *sql;
Cost startup_cost;
Cost total_cost;
+
+ ForeignTable *table;
+ ForeignServer *server;
+ UserMapping *user;
} PgsqlFdwPlanState;
/*
*************** typedef struct PgsqlFdwPlanState {
*** 82,87 ****
--- 86,92 ----
* stored in fdw_private list.
*/
enum FdwPrivateIndex {
+ FdwPrivateFdwExprs,
FdwPrivateSelectSql,
FdwPrivateDeclareSql,
FdwPrivateFetchSql,
*************** pgsqlGetForeignRelSize(PlannerInfo *root
*** 268,273 ****
--- 273,281 ----
planstate->sql = sql;
planstate->startup_cost = startup_cost;
planstate->total_cost = total_cost;
+ planstate->table = table;
+ planstate->server = server;
+ planstate->user = user;
baserel->fdw_private = (void *) planstate;
}
*************** pgsqlGetForeignPaths(PlannerInfo *root,
*** 283,288 ****
--- 291,305 ----
PgsqlFdwPlanState *planstate = (PgsqlFdwPlanState *) baserel->fdw_private;
List *fdw_private = NIL;
ForeignPath *path;
+ StringInfoData buf;
+ PGconn *conn;
+ double rows;
+ int width;
+ Cost startup_cost;
+ Cost total_cost;
+ bool first = true;
+ ListCell *lc;
+ List *fdw_exprs;
/*
* Cost estimation should be modified to respect cost of establishing
*************** pgsqlGetForeignPaths(PlannerInfo *root,
*** 297,303 ****
* Create simplest ForeignScan path node, counterpart of SeqScan for
* regular tables, for this scan, and add it to baserel.
*/
! fdw_private = list_make1(makeString(planstate->sql));
path = create_foreignscan_path(root, baserel,
baserel->rows,
planstate->startup_cost,
--- 314,321 ----
* Create simplest ForeignScan path node, counterpart of SeqScan for
* regular tables, for this scan, and add it to baserel.
*/
! fdw_private = lappend(fdw_private, NIL); /* no fdw_exprs */
! fdw_private = lappend(fdw_private, makeString(planstate->sql));
path = create_foreignscan_path(root, baserel,
baserel->rows,
planstate->startup_cost,
*************** pgsqlGetForeignPaths(PlannerInfo *root,
*** 309,314 ****
--- 327,389 ----
add_path(baserel, (Path *) path);
/*
+ * Create a ForeignScan path with WHERE push-down.
+ */
+ fdw_exprs = extractRemoteExprs(foreigntableid, root, baserel);
+ if (fdw_exprs != NIL)
+ {
+ initStringInfo(&buf);
+ appendStringInfo(&buf, "%s WHERE (", planstate->sql);
+
+ foreach(lc, fdw_exprs)
+ {
+ Expr *expr = (Expr *) lfirst(lc);
+
+ /* Connect expressions with "AND". */
+ if (!first)
+ appendStringInfo(&buf, ") AND (");
+
+ deparseExpr(&buf, expr, root, baserel);
+ first = false;
+ }
+ appendStringInfoChar(&buf, ')');
+
+ /*
+ * Cost estimation should be modified to respect cost of establishing
+ * connection and transferring data.
+ */
+ conn = GetConnection(planstate->server, planstate->user, false);
+ get_remote_estimate(buf.data,
+ conn,
+ &rows,
+ &width,
+ &startup_cost,
+ &total_cost);
+ ReleaseConnection(conn);
+ adjust_costs(baserel->rows,
+ baserel->width,
+ &planstate->startup_cost,
+ &planstate->total_cost);
+
+ /*
+ * Create simplest ForeignScan path node, counterpart of SeqScan for
+ * regular tables, for this scan, and add it to baserel.
+ */
+ fdw_private = NIL;
+ fdw_private = lappend(fdw_private, fdw_exprs);
+ fdw_private = lappend(fdw_private, makeString(buf.data));
+ path = create_foreignscan_path(root, baserel,
+ rows,
+ startup_cost,
+ total_cost,
+ NIL, /* no pathkeys */
+ NULL, /* no outer rel either */
+ NIL, /* no param clause */
+ fdw_private); /* pass SQL */
+ add_path(baserel, (Path *) path);
+ }
+
+ /*
* XXX We can consider sorted path here if we know that foreign table is
* indexed on remote end. For this purpose, should we support FOREIGN
* INDEX to represent possible sets of sort keys which are relatively
*************** pgsqlGetForeignPlan(PlannerInfo *root,
*** 338,343 ****
--- 413,419 ----
DefElem *def;
int fetch_count = DEFAULT_FETCH_COUNT;
char *sql;
+ List *fdw_exprs = NIL;
/*
* We have no native ability to evaluate restriction clauses, so we just
*************** pgsqlGetForeignPlan(PlannerInfo *root,
*** 380,396 ****
* boundary between planner and executor. Finally FdwPlan using cursor
* would hold items below:
*
! * 1) plain SELECT statement (already added above)
! * 2) SQL statement used to declare cursor
! * 3) SQL statement used to fetch rows from cursor
! * 4) SQL statement used to reset cursor
! * 5) SQL statement used to close cursor
*
* These items are indexed with the enum FdwPrivateIndex, so an item
* can be accessed directly via list_nth(). For example of FETCH
* statement:
* list_nth(fdw_private, FdwPrivateFetchSql)
*/
sql = strVal(list_nth(fdw_private, FdwPrivateSelectSql));
/* Construct cursor name from sequential value */
--- 456,474 ----
* boundary between planner and executor. Finally FdwPlan using cursor
* would hold items below:
*
! * 1) expressions which are pushed down
! * 2) plain SELECT statement (already added above)
! * 3) SQL statement used to declare cursor
! * 4) SQL statement used to fetch rows from cursor
! * 5) SQL statement used to reset cursor
! * 6) SQL statement used to close cursor
*
* These items are indexed with the enum FdwPrivateIndex, so an item
* can be accessed directly via list_nth(). For example of FETCH
* statement:
* list_nth(fdw_private, FdwPrivateFetchSql)
*/
+ fdw_exprs = list_nth(fdw_private, FdwPrivateFdwExprs);
sql = strVal(list_nth(fdw_private, FdwPrivateSelectSql));
/* Construct cursor name from sequential value */
*************** pgsqlGetForeignPlan(PlannerInfo *root,
*** 420,426 ****
return make_foreignscan(tlist,
scan_clauses,
scan_relid,
! NIL,
fdw_private);
}
--- 498,504 ----
return make_foreignscan(tlist,
scan_clauses,
scan_relid,
! fdw_exprs,
fdw_private);
}
diff --git a/contrib/pgsql_fdw/pgsql_fdw.h b/contrib/pgsql_fdw/pgsql_fdw.h
index 6eb99d5..b7d1310 100644
*** a/contrib/pgsql_fdw/pgsql_fdw.h
--- b/contrib/pgsql_fdw/pgsql_fdw.h
*************** char *deparseSimpleSql(Oid relid,
*** 28,32 ****
--- 28,39 ----
PlannerInfo *root,
RelOptInfo *baserel,
ForeignTable *table);
+ List *extractRemoteExprs(Oid relid,
+ PlannerInfo *root,
+ RelOptInfo *baserel);
+ void deparseExpr(StringInfo buf,
+ Expr *expr,
+ PlannerInfo *root,
+ RelOptInfo *baserel);
#endif /* PGSQL_FDW_H */
(2012/03/15 23:06), Shigeru HANADA wrote:
Although the patches are still WIP, especially in WHERE push-down part,
but I'd like to post them so that I can get feedback of the design as
soon as possible.
I've implemented pgsql_fdw's own deparser and enhanced some features
since last post. Please apply attached patches in the order below:
* pgsql_fdw_v17.patch
- Adds pgsql_fdw as contrib module
* pgsql_fdw_pushdown_v10.patch
- Adds WHERE push down capability to pgsql_fdw
* pgsql_fdw_analyze_v1.patch
- Adds pgsql_fdw_analyze function for updating local stats
Changes from previous version
=============================
1) Don't use remote EXPLAIN for cost/rows estimation, so now planner
estimates result rows and costs on the basis of local statistics such as
pg_class and pg_statistic. To update local statistics, I added
pgsql_fdw_analyze() SQL function which updates local statistics of a
foreign table by retrieving remote statistics, such as pg_class and
pg_statistic, via libpq. This would make the planning of pgsql_fdw
simple and fast. This function can be easily modified to handle ANALYZE
command invoked for a foreign table (Fujita-san is proposing this as
common feature in another thread).
2) Defer planning stuffs as long as possible to clarify the role of each
function. Currently GetRelSize just estimates result rows from local
statistics, and GetPaths adds only one path which represents SeqScan on
remote side. As result of this change, PgsqlFdwPlanState struct is
obsolete.
3) Implement pgsql_fdw's own deparser which pushes down collation-free
and immutable expressions in local WHERE clause. This means that most
of numeric conditions can be pushed down, but conditions using character
types are not.
Most of nodes are deparsed in straightforward way, but OpExpr is not.
OpExpr is deparsed with OPERATOR() notation to specify operator's
schema explicitly. This would prevent us from possible search_path problem.
[local query]
WHERE -col = -1
[remote query]
WHERE ((OPERATOR(pg_class.-) col) OPERATOR(pg_class.=) 1)
4) Pushed down quals are not evaluated on local side again. When
creating ForeignScan node for chosen best path, pushed down expressions
are removed from "qpqual" parameter of make_foreignscan, so a qualifier
is evaluated only once at local or remote.
5) EXPLAIN on pgsql_fdw foreign tables show simple SELECT statement.
DECLARE statement including cursor name is still available in VERBOSE
mode. (I feel that showing DECLARE always is little noisy...)
Regards,
--
Shigeru HANADA
Attachments:
pgsql_fdw_v17.patchtext/plain; charset=Shift_JIS; name=pgsql_fdw_v17.patchDownload
diff --git a/contrib/Makefile b/contrib/Makefile
index ac0a80a..14aa433 100644
*** a/contrib/Makefile
--- b/contrib/Makefile
*************** SUBDIRS = \
*** 41,46 ****
--- 41,47 ----
pgbench \
pgcrypto \
pgrowlocks \
+ pgsql_fdw \
pgstattuple \
seg \
spi \
diff --git a/contrib/README b/contrib/README
index a1d42a1..d3fa211 100644
*** a/contrib/README
--- b/contrib/README
*************** pgrowlocks -
*** 158,163 ****
--- 158,167 ----
A function to return row locking information
by Tatsuo Ishii <ishii@sraoss.co.jp>
+ pgsql_fdw -
+ Foreign-data wrapper for external PostgreSQL servers.
+ by Shigeru Hanada <shigeru.hanada@gmail.com>
+
pgstattuple -
Functions to return statistics about "dead" tuples and free
space within a table
diff --git a/contrib/pgsql_fdw/.gitignore b/contrib/pgsql_fdw/.gitignore
index ...0854728 .
*** a/contrib/pgsql_fdw/.gitignore
--- b/contrib/pgsql_fdw/.gitignore
***************
*** 0 ****
--- 1,4 ----
+ # Generated subdirectories
+ /results/
+ *.o
+ *.so
diff --git a/contrib/pgsql_fdw/Makefile b/contrib/pgsql_fdw/Makefile
index ...6381365 .
*** a/contrib/pgsql_fdw/Makefile
--- b/contrib/pgsql_fdw/Makefile
***************
*** 0 ****
--- 1,22 ----
+ # contrib/pgsql_fdw/Makefile
+
+ MODULE_big = pgsql_fdw
+ OBJS = pgsql_fdw.o option.o deparse.o connection.o
+ PG_CPPFLAGS = -I$(libpq_srcdir)
+ SHLIB_LINK = $(libpq)
+
+ EXTENSION = pgsql_fdw
+ DATA = pgsql_fdw--1.0.sql
+
+ REGRESS = pgsql_fdw
+
+ ifdef USE_PGXS
+ PG_CONFIG = pg_config
+ PGXS := $(shell $(PG_CONFIG) --pgxs)
+ include $(PGXS)
+ else
+ subdir = contrib/pgsql_fdw
+ top_builddir = ../..
+ include $(top_builddir)/src/Makefile.global
+ include $(top_srcdir)/contrib/contrib-global.mk
+ endif
diff --git a/contrib/pgsql_fdw/connection.c b/contrib/pgsql_fdw/connection.c
index ...c971bd7 .
*** a/contrib/pgsql_fdw/connection.c
--- b/contrib/pgsql_fdw/connection.c
***************
*** 0 ****
--- 1,555 ----
+ /*-------------------------------------------------------------------------
+ *
+ * connection.c
+ * Connection management for pgsql_fdw
+ *
+ * Portions Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/connection.c
+ *
+ *-------------------------------------------------------------------------
+ */
+ #include "postgres.h"
+
+ #include "access/xact.h"
+ #include "catalog/pg_type.h"
+ #include "foreign/foreign.h"
+ #include "funcapi.h"
+ #include "libpq-fe.h"
+ #include "mb/pg_wchar.h"
+ #include "miscadmin.h"
+ #include "utils/array.h"
+ #include "utils/builtins.h"
+ #include "utils/hsearch.h"
+ #include "utils/memutils.h"
+ #include "utils/resowner.h"
+ #include "utils/tuplestore.h"
+
+ #include "pgsql_fdw.h"
+ #include "connection.h"
+
+ /* ============================================================================
+ * Connection management functions
+ * ==========================================================================*/
+
+ /*
+ * Connection cache entry managed with hash table.
+ */
+ typedef struct ConnCacheEntry
+ {
+ /* hash key must be first */
+ Oid serverid; /* oid of foreign server */
+ Oid userid; /* oid of local user */
+
+ bool use_tx; /* true when using remote transaction */
+ 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);
+ static void begin_remote_tx(PGconn *conn);
+ static void abort_remote_tx(PGconn *conn);
+
+ /*
+ * 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.
+ *
+ * When use_tx is true, remote transaction is started if caller is the only
+ * user of the connection. Isolation level of the remote transaction is same
+ * as local transaction, and remote transaction will be aborted when last
+ * user release.
+ *
+ * TODO: Note that caching connections requires a mechanism to detect change of
+ * FDW object to invalidate already established connections.
+ */
+ PGconn *
+ GetConnection(ForeignServer *server, UserMapping *user, bool use_tx)
+ {
+ bool found;
+ ConnCacheEntry *entry;
+ ConnCacheEntry key;
+
+ /* initialize connection cache if it isn't */
+ if (FSConnectionHash == NULL)
+ {
+ HASHCTL ctl;
+
+ /* hash key is a pair of oids: serverid and userid */
+ MemSet(&ctl, 0, sizeof(ctl));
+ ctl.keysize = sizeof(Oid) + sizeof(Oid);
+ ctl.entrysize = sizeof(ConnCacheEntry);
+ ctl.hash = tag_hash;
+ ctl.match = memcmp;
+ ctl.keycopy = memcpy;
+ /* allocate FSConnectionHash in the cache context */
+ ctl.hcxt = CacheMemoryContext;
+ FSConnectionHash = hash_create("Foreign Connections", 32,
+ &ctl,
+ HASH_ELEM | HASH_CONTEXT |
+ HASH_FUNCTION | HASH_COMPARE |
+ HASH_KEYCOPY);
+ }
+
+ /* Create key value for the entry. */
+ MemSet(&key, 0, sizeof(key));
+ key.serverid = server->serverid;
+ key.userid = GetOuterUserId();
+
+ /*
+ * Find cached entry for requested connection. If we couldn't find,
+ * callback function of ResourceOwner should be registered to clean the
+ * connection up on error including user interrupt.
+ */
+ entry = hash_search(FSConnectionHash, &key, HASH_ENTER, &found);
+ if (!found)
+ {
+ entry->refs = 0;
+ entry->use_tx = false;
+ entry->conn = NULL;
+ RegisterResourceReleaseCallback(cleanup_connection, entry);
+ }
+
+ /*
+ * We don't check the health of cached connection here, because it would
+ * require some overhead. Broken connection and its cache entry will be
+ * cleaned up when the connection is actually used.
+ */
+
+ /*
+ * If cache entry doesn't have connection, we have to establish new
+ * connection.
+ */
+ if (entry->conn == NULL)
+ {
+ /*
+ * Use PG_TRY block to ensure closing connection on error.
+ */
+ PG_TRY();
+ {
+ /*
+ * Connect to the foreign PostgreSQL server, and store it in cache
+ * entry to keep new connection.
+ * Note: key items of entry has already been initialized in
+ * hash_search(HASH_ENTER).
+ */
+ entry->conn = connect_pg_server(server, user);
+ }
+ PG_CATCH();
+ {
+ /* Clear connection cache entry on error case. */
+ PQfinish(entry->conn);
+ entry->refs = 0;
+ entry->use_tx = false;
+ entry->conn = NULL;
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+ }
+
+ /* Increase connection reference counter. */
+ entry->refs++;
+
+ /*
+ * If requester is the only referrer of this connection, start transaction
+ * with the same isolation level as the local transaction we are in.
+ * We need to remember whether this connection uses remote transaction to
+ * abort it when this connection is released completely.
+ */
+ if (use_tx && entry->refs == 1)
+ begin_remote_tx(entry->conn);
+ entry->use_tx = use_tx;
+
+
+ return entry->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 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. Those two object hold only libpq options.
+ * Extra 3 items are for:
+ * *) fallback_application_name
+ * *) client_encoding
+ * *) NULL termination (end marker)
+ *
+ * Note: We don't omit any parameters even target database might be older
+ * than local, because unexpected parameters are just ignored.
+ */
+ n = list_length(server->options) + list_length(user->options) + 3;
+ 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 += ExtractConnectionOptions(server->options,
+ all_keywords + n, all_values + n);
+ n += ExtractConnectionOptions(user->options,
+ all_keywords + n, all_values + n);
+ all_keywords[n] = all_values[n] = NULL;
+
+ for (i = 0, j = 0; all_keywords[i]; i++)
+ {
+ keywords[j] = all_keywords[i];
+ values[j] = all_values[i];
+ j++;
+ }
+
+ /* Use "pgsql_fdw" as fallback_application_name. */
+ keywords[j] = "fallback_application_name";
+ values[j++] = "pgsql_fdw";
+
+ /* Set client_encoding so that libpq can convert encoding properly. */
+ keywords[j] = "client_encoding";
+ values[j++] = GetDatabaseEncodingName();
+
+ keywords[j] = values[j] = NULL;
+ pfree(all_keywords);
+ pfree(all_values);
+
+ /* 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.")));
+ }
+
+ return conn;
+ }
+
+ /*
+ * Start transaction to use cursor to retrieve data separately.
+ */
+ static void
+ begin_remote_tx(PGconn *conn)
+ {
+ const char *sql = NULL; /* keep compiler quiet. */
+ PGresult *res;
+
+ switch (XactIsoLevel)
+ {
+ case XACT_READ_UNCOMMITTED:
+ case XACT_READ_COMMITTED:
+ case XACT_REPEATABLE_READ:
+ sql = "START TRANSACTION ISOLATION LEVEL REPEATABLE READ";
+ break;
+ case XACT_SERIALIZABLE:
+ sql = "START TRANSACTION ISOLATION LEVEL SERIALIZABLE";
+ break;
+ default:
+ elog(ERROR, "unexpected isolation level: %d", XactIsoLevel);
+ break;
+ }
+
+ elog(DEBUG3, "starting remote transaction with \"%s\"", sql);
+
+ res = PQexec(conn, sql);
+ if (PQresultStatus(res) != PGRES_COMMAND_OK)
+ {
+ PQclear(res);
+ elog(ERROR, "could not start transaction: %s", PQerrorMessage(conn));
+ }
+ PQclear(res);
+ }
+
+ static void
+ abort_remote_tx(PGconn *conn)
+ {
+ PGresult *res;
+
+ elog(DEBUG3, "aborting remote transaction");
+
+ res = PQexec(conn, "ABORT TRANSACTION");
+ if (PQresultStatus(res) != PGRES_COMMAND_OK)
+ {
+ PQclear(res);
+ elog(ERROR, "could not abort transaction: %s", PQerrorMessage(conn));
+ }
+ PQclear(res);
+ }
+
+ /*
+ * 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 sequentially 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)
+ {
+ hash_seq_term(&scan);
+ break;
+ }
+ }
+
+ /* If the released connection was an orphan, just close it. */
+ if (entry == NULL)
+ {
+ PQfinish(conn);
+ return;
+ }
+
+ /*
+ * If this connection uses remote transaction and caller is the last user,
+ * abort remote transaction and forget about it.
+ */
+ if (entry->use_tx && entry->refs == 1)
+ {
+ abort_remote_tx(conn);
+ entry->use_tx = false;
+ }
+
+ /*
+ * Decrease reference counter of this connection. Even if the caller was
+ * the last referrer, we don't unregister it from cache.
+ */
+ entry->refs--;
+ }
+
+ /*
+ * 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 need to clean up at end of subtransactions, because they might
+ * be recovered to consistent state with savepoints.
+ */
+ if (!isTopLevel)
+ return;
+
+ /*
+ * Here, it must be after abort of top level transaction. Disconnect and
+ * forget every referrer.
+ */
+ if (entry->conn != NULL)
+ {
+ PQfinish(entry->conn);
+ entry->refs = 0;
+ entry->conn = NULL;
+ }
+ }
+
+ /*
+ * Get list of connections currently active.
+ */
+ Datum pgsql_fdw_get_connections(PG_FUNCTION_ARGS);
+ PG_FUNCTION_INFO_V1(pgsql_fdw_get_connections);
+ Datum
+ pgsql_fdw_get_connections(PG_FUNCTION_ARGS)
+ {
+ ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+ HASH_SEQ_STATUS scan;
+ ConnCacheEntry *entry;
+ MemoryContext oldcontext = CurrentMemoryContext;
+ Tuplestorestate *tuplestore;
+ TupleDesc tupdesc;
+
+ /* We return list of connection with storing them in a Tuplestore. */
+ rsinfo->returnMode = SFRM_Materialize;
+ rsinfo->setResult = NULL;
+ rsinfo->setDesc = NULL;
+
+ /* Create tuplestore and copy of TupleDesc in per-query context. */
+ MemoryContextSwitchTo(rsinfo->econtext->ecxt_per_query_memory);
+
+ tupdesc = CreateTemplateTupleDesc(2, false);
+ TupleDescInitEntry(tupdesc, 1, "srvid", OIDOID, -1, 0);
+ TupleDescInitEntry(tupdesc, 2, "usesysid", OIDOID, -1, 0);
+ rsinfo->setDesc = tupdesc;
+
+ tuplestore = tuplestore_begin_heap(false, false, work_mem);
+ rsinfo->setResult = tuplestore;
+
+ MemoryContextSwitchTo(oldcontext);
+
+ /*
+ * We need to scan sequentially since we use the address to find
+ * appropriate PGconn from the hash table.
+ */
+ if (FSConnectionHash != NULL)
+ {
+ hash_seq_init(&scan, FSConnectionHash);
+ while ((entry = (ConnCacheEntry *) hash_seq_search(&scan)))
+ {
+ Datum values[2];
+ bool nulls[2];
+ HeapTuple tuple;
+
+ /* Ignore inactive connections */
+ if (PQstatus(entry->conn) != CONNECTION_OK)
+ continue;
+
+ /*
+ * Ignore other users' connections if current user isn't a
+ * superuser.
+ */
+ if (!superuser() && entry->userid != GetUserId())
+ continue;
+
+ values[0] = ObjectIdGetDatum(entry->serverid);
+ values[1] = ObjectIdGetDatum(entry->userid);
+ nulls[0] = false;
+ nulls[1] = false;
+
+ tuple = heap_formtuple(tupdesc, values, nulls);
+ tuplestore_puttuple(tuplestore, tuple);
+ }
+ }
+ tuplestore_donestoring(tuplestore);
+
+ PG_RETURN_VOID();
+ }
+
+ /*
+ * Discard persistent connection designated by given connection name.
+ */
+ Datum pgsql_fdw_disconnect(PG_FUNCTION_ARGS);
+ PG_FUNCTION_INFO_V1(pgsql_fdw_disconnect);
+ Datum
+ pgsql_fdw_disconnect(PG_FUNCTION_ARGS)
+ {
+ Oid serverid = PG_GETARG_OID(0);
+ Oid userid = PG_GETARG_OID(1);
+ ConnCacheEntry key;
+ ConnCacheEntry *entry = NULL;
+ bool found;
+
+ /* Non-superuser can't discard other users' connection. */
+ if (!superuser() && userid != GetOuterUserId())
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_RESOURCES),
+ errmsg("only superuser can discard other user's connection")));
+
+ /*
+ * If no connection has been established, or no such connections, just
+ * return "NG" to indicate nothing has done.
+ */
+ if (FSConnectionHash == NULL)
+ PG_RETURN_TEXT_P(cstring_to_text("NG"));
+
+ key.serverid = serverid;
+ key.userid = userid;
+ entry = hash_search(FSConnectionHash, &key, HASH_FIND, &found);
+ if (!found)
+ PG_RETURN_TEXT_P(cstring_to_text("NG"));
+
+ /* Discard cached connection, and clear reference counter. */
+ PQfinish(entry->conn);
+ entry->refs = 0;
+ entry->conn = NULL;
+
+ PG_RETURN_TEXT_P(cstring_to_text("OK"));
+ }
diff --git a/contrib/pgsql_fdw/connection.h b/contrib/pgsql_fdw/connection.h
index ...4427f56 .
*** a/contrib/pgsql_fdw/connection.h
--- b/contrib/pgsql_fdw/connection.h
***************
*** 0 ****
--- 1,25 ----
+ /*-------------------------------------------------------------------------
+ *
+ * connection.h
+ * Connection management for pgsql_fdw
+ *
+ * Portions Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_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, bool use_tx);
+ void ReleaseConnection(PGconn *conn);
+
+ #endif /* CONNECTION_H */
diff --git a/contrib/pgsql_fdw/deparse.c b/contrib/pgsql_fdw/deparse.c
index ...ca3356d .
*** a/contrib/pgsql_fdw/deparse.c
--- b/contrib/pgsql_fdw/deparse.c
***************
*** 0 ****
--- 1,289 ----
+ /*-------------------------------------------------------------------------
+ *
+ * deparse.c
+ * query deparser for PostgreSQL
+ *
+ * Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/deparse.c
+ *
+ *-------------------------------------------------------------------------
+ */
+ #include "postgres.h"
+
+ #include "access/transam.h"
+ #include "catalog/pg_class.h"
+ #include "commands/defrem.h"
+ #include "foreign/foreign.h"
+ #include "lib/stringinfo.h"
+ #include "nodes/nodeFuncs.h"
+ #include "nodes/nodes.h"
+ #include "nodes/makefuncs.h"
+ #include "optimizer/clauses.h"
+ #include "optimizer/var.h"
+ #include "parser/parsetree.h"
+ #include "utils/builtins.h"
+ #include "utils/lsyscache.h"
+
+ #include "pgsql_fdw.h"
+
+ /*
+ * Context for walk-through the expression tree.
+ */
+ typedef struct foreign_executable_cxt
+ {
+ PlannerInfo *root;
+ RelOptInfo *foreignrel;
+ } foreign_executable_cxt;
+
+ /*
+ * Get string representation which can be used in SQL statement from a node.
+ */
+ static void deparseRelation(StringInfo buf, Oid relid, PlannerInfo *root,
+ bool need_prefix);
+ static void deparseVar(StringInfo buf, Var *node, PlannerInfo *root,
+ bool need_prefix);
+
+ /*
+ * Deparse query representation into SQL statement which suits for remote
+ * PostgreSQL server. This function just creates simple query string which
+ * consists of only SELECT and FROM clause.
+ */
+ char *
+ deparseSimpleSql(Oid relid,
+ PlannerInfo *root,
+ RelOptInfo *baserel,
+ ForeignTable *table)
+ {
+ StringInfoData foreign_relname;
+ StringInfoData sql; /* builder for SQL statement */
+ bool first;
+ AttrNumber attr;
+ List *attr_used = NIL; /* List of AttNumber used in the query */
+
+ initStringInfo(&sql);
+ initStringInfo(&foreign_relname);
+
+ /*
+ * First of all, determine which column should be retrieved for this scan.
+ *
+ * We do this before deparsing SELECT clause because attributes which are
+ * not used in neither reltargetlist nor baserel->baserestrictinfo, quals
+ * evaluated on local, can be replaced with literal "NULL" in the SELECT
+ * clause to reduce overhead of tuple handling tuple and data transfer.
+ */
+ if (baserel->baserestrictinfo != NIL)
+ {
+ ListCell *lc;
+
+ foreach (lc, baserel->baserestrictinfo)
+ {
+ RestrictInfo *ri = (RestrictInfo *) lfirst(lc);
+ List *attrs;
+
+ /*
+ * We need to know which attributes are used in qual evaluated
+ * on the local server, because they should be listed in the
+ * SELECT clause of remote query. We can ignore attributes
+ * which are referenced only in ORDER BY/GROUP BY clause because
+ * such attributes has already been kept in reltargetlist.
+ */
+ attrs = pull_var_clause((Node *) ri->clause,
+ PVC_RECURSE_AGGREGATES,
+ PVC_RECURSE_PLACEHOLDERS);
+ attr_used = list_union(attr_used, attrs);
+ }
+ }
+
+ /*
+ * deparse SELECT clause
+ *
+ * List attributes which are in either target list or local restriction.
+ * Unused attributes are replaced with a literal "NULL" for optimization.
+ *
+ * Note that nothing is added for dropped columns, though tuple constructor
+ * function requires entries for dropped columns. Such entries must be
+ * initialized with NULL before calling tuple constructor.
+ */
+ appendStringInfo(&sql, "SELECT ");
+ attr_used = list_union(attr_used, baserel->reltargetlist);
+ first = true;
+ for (attr = 1; attr <= baserel->max_attr; attr++)
+ {
+ RangeTblEntry *rte = root->simple_rte_array[baserel->relid];
+ Var *var = NULL;
+ ListCell *lc;
+
+ /* Ignore dropped attributes. */
+ if (get_rte_attribute_is_dropped(rte, attr))
+ continue;
+
+ if (!first)
+ appendStringInfo(&sql, ", ");
+ first = false;
+
+ /*
+ * We use linear search here, but it wouldn't be problem since
+ * attr_used seems to not become so large.
+ */
+ foreach (lc, attr_used)
+ {
+ var = lfirst(lc);
+ if (var->varattno == attr)
+ break;
+ var = NULL;
+ }
+ if (var != NULL)
+ deparseVar(&sql, var, root, false);
+ else
+ appendStringInfo(&sql, "NULL");
+ }
+ appendStringInfoChar(&sql, ' ');
+
+ /*
+ * deparse FROM clause, including alias if any
+ */
+ appendStringInfo(&sql, "FROM ");
+ deparseRelation(&sql, table->relid, root, true);
+
+ elog(DEBUG3, "Remote SQL: %s", sql.data);
+ return sql.data;
+ }
+
+ /*
+ * Deparse node into buf, with relation qualifier if need_prefix was true. If
+ * node is a column of a foreign table, use value of colname FDW option (if any)
+ * instead of attribute name.
+ */
+ static void
+ deparseVar(StringInfo buf,
+ Var *node,
+ PlannerInfo *root,
+ bool need_prefix)
+ {
+ RangeTblEntry *rte;
+ char *colname = NULL;
+ const char *q_colname = NULL;
+ List *options;
+ ListCell *lc;
+
+ /* node must not be any of OUTER_VAR,INNER_VAR and INDEX_VAR. */
+ Assert(node->varno >= 1 && node->varno <= root->simple_rel_array_size);
+
+ /* Get RangeTblEntry from array in PlannerInfo. */
+ rte = root->simple_rte_array[node->varno];
+
+ /*
+ * If the node is a column of a foreign table, and it has colname FDW
+ * option, use its value.
+ */
+ if (rte->relkind == RELKIND_FOREIGN_TABLE)
+ {
+ options = GetForeignColumnOptions(rte->relid, node->varattno);
+ foreach(lc, options)
+ {
+ DefElem *def = (DefElem *) lfirst(lc);
+
+ if (strcmp(def->defname, "colname") == 0)
+ {
+ colname = defGetString(def);
+ break;
+ }
+ }
+ }
+
+ /*
+ * If the node refers a column of a regular table or it doesn't have colname
+ * FDW option, use attribute name.
+ */
+ if (colname == NULL)
+ colname = get_attname(rte->relid, node->varattno);
+
+ if (need_prefix)
+ {
+ char *aliasname;
+ const char *q_aliasname;
+
+ if (rte->eref != NULL && rte->eref->aliasname != NULL)
+ aliasname = rte->eref->aliasname;
+ else if (rte->alias != NULL && rte->alias->aliasname != NULL)
+ aliasname = rte->alias->aliasname;
+
+ q_aliasname = quote_identifier(aliasname);
+ appendStringInfo(buf, "%s.", q_aliasname);
+ }
+
+ q_colname = quote_identifier(colname);
+ appendStringInfo(buf, "%s", q_colname);
+ }
+
+ /*
+ * Deparse table which has relid as oid into buf, with schema qualifier if
+ * need_prefix was true. If relid points a foreign table, use value of relname
+ * FDW option (if any) instead of relation's name. Similarly, nspname FDW
+ * option overrides schema name.
+ */
+ static void
+ deparseRelation(StringInfo buf,
+ Oid relid,
+ PlannerInfo *root,
+ bool need_prefix)
+ {
+ int i;
+ RangeTblEntry *rte = NULL;
+ ListCell *lc;
+ const char *nspname = NULL; /* plain namespace name */
+ const char *relname = NULL; /* plain relation name */
+ const char *q_nspname; /* quoted namespace name */
+ const char *q_relname; /* quoted relation name */
+
+ /* Find RangeTblEntry for the relation from PlannerInfo. */
+ for (i = 1; i < root->simple_rel_array_size; i++)
+ {
+ if (root->simple_rte_array[i]->relid == relid)
+ {
+ rte = root->simple_rte_array[i];
+ break;
+ }
+ }
+ if (rte == NULL)
+ elog(ERROR, "relation with OID %u is not used in the query", relid);
+
+ /* If target is a foreign table, obtain additional catalog information. */
+ if (rte->relkind == RELKIND_FOREIGN_TABLE)
+ {
+ ForeignTable *table = GetForeignTable(rte->relid);
+
+ /*
+ * Use value of FDW options if any, instead of the name of object
+ * itself.
+ */
+ foreach(lc, table->options)
+ {
+ DefElem *def = (DefElem *) lfirst(lc);
+
+ if (need_prefix && strcmp(def->defname, "nspname") == 0)
+ nspname = defGetString(def);
+ else if (strcmp(def->defname, "relname") == 0)
+ relname = defGetString(def);
+ }
+ }
+
+ /* Quote each identifier, if necessary. */
+ if (need_prefix)
+ {
+ if (nspname == NULL)
+ nspname = get_namespace_name(get_rel_namespace(relid));
+ q_nspname = quote_identifier(nspname);
+ }
+
+ if (relname == NULL)
+ relname = get_rel_name(relid);
+ q_relname = quote_identifier(relname);
+
+ /* Construct relation reference into the buffer. */
+ if (need_prefix)
+ appendStringInfo(buf, "%s.", q_nspname);
+ appendStringInfo(buf, "%s", q_relname);
+ }
diff --git a/contrib/pgsql_fdw/expected/pgsql_fdw.out b/contrib/pgsql_fdw/expected/pgsql_fdw.out
index ...fdd908c .
*** a/contrib/pgsql_fdw/expected/pgsql_fdw.out
--- b/contrib/pgsql_fdw/expected/pgsql_fdw.out
***************
*** 0 ****
--- 1,589 ----
+ -- ===================================================================
+ -- create FDW objects
+ -- ===================================================================
+ -- Clean up in case a prior regression run failed
+ -- Suppress NOTICE messages when roles don't exist
+ SET client_min_messages TO 'error';
+ DROP ROLE IF EXISTS pgsql_fdw_user;
+ RESET client_min_messages;
+ CREATE ROLE pgsql_fdw_user LOGIN SUPERUSER;
+ SET SESSION AUTHORIZATION 'pgsql_fdw_user';
+ CREATE EXTENSION pgsql_fdw;
+ CREATE SERVER loopback1 FOREIGN DATA WRAPPER pgsql_fdw;
+ CREATE SERVER loopback2 FOREIGN DATA WRAPPER pgsql_fdw
+ OPTIONS (dbname 'contrib_regression');
+ CREATE USER MAPPING FOR public SERVER loopback1
+ OPTIONS (user 'value', password 'value');
+ CREATE USER MAPPING FOR pgsql_fdw_user SERVER loopback2;
+ CREATE FOREIGN TABLE ft1 (
+ c0 int,
+ c1 int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10)
+ ) SERVER loopback2;
+ ALTER FOREIGN TABLE ft1 DROP COLUMN c0;
+ CREATE FOREIGN TABLE ft2 (
+ c0 int,
+ c1 int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10)
+ ) SERVER loopback2;
+ ALTER FOREIGN TABLE ft2 DROP COLUMN c0;
+ -- ===================================================================
+ -- create objects used through FDW
+ -- ===================================================================
+ CREATE SCHEMA "S 1";
+ CREATE TABLE "S 1"."T 1" (
+ "C 1" int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10),
+ CONSTRAINT t1_pkey PRIMARY KEY ("C 1")
+ );
+ NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "t1_pkey" for table "T 1"
+ CREATE TABLE "S 1"."T 2" (
+ c1 int NOT NULL,
+ c2 text,
+ CONSTRAINT t2_pkey PRIMARY KEY (c1)
+ );
+ NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "t2_pkey" for table "T 2"
+ BEGIN;
+ TRUNCATE "S 1"."T 1";
+ INSERT INTO "S 1"."T 1"
+ SELECT id,
+ id % 10,
+ to_char(id, 'FM00000'),
+ '1970-01-01'::timestamptz + ((id % 100) || ' days')::interval,
+ '1970-01-01'::timestamp + ((id % 100) || ' days')::interval,
+ id % 10,
+ id % 10
+ FROM generate_series(1, 1000) id;
+ TRUNCATE "S 1"."T 2";
+ INSERT INTO "S 1"."T 2"
+ SELECT id,
+ 'AAA' || to_char(id, 'FM000')
+ FROM generate_series(1, 100) id;
+ COMMIT;
+ -- ===================================================================
+ -- tests for pgsql_fdw_validator
+ -- ===================================================================
+ ALTER FOREIGN DATA WRAPPER pgsql_fdw OPTIONS (host 'value'); -- ERROR
+ ERROR: invalid option "host"
+ HINT: Valid options in this context are:
+ -- requiressl, krbsrvname and gsslib are omitted because they depend on
+ -- configure option
+ ALTER SERVER loopback1 OPTIONS (
+ authtype 'value',
+ service 'value',
+ connect_timeout 'value',
+ dbname 'value',
+ host 'value',
+ hostaddr 'value',
+ port 'value',
+ --client_encoding 'value',
+ tty 'value',
+ options 'value',
+ application_name 'value',
+ --fallback_application_name 'value',
+ keepalives 'value',
+ keepalives_idle 'value',
+ keepalives_interval 'value',
+ -- requiressl 'value',
+ sslcompression 'value',
+ sslmode 'value',
+ sslcert 'value',
+ sslkey 'value',
+ sslrootcert 'value',
+ sslcrl 'value'
+ --requirepeer 'value',
+ -- krbsrvname 'value',
+ -- gsslib 'value',
+ --replication 'value'
+ );
+ ALTER SERVER loopback1 OPTIONS (user 'value'); -- ERROR
+ ERROR: invalid option "user"
+ HINT: Valid options in this context are: authtype, service, connect_timeout, dbname, host, hostaddr, port, tty, options, application_name, keepalives, keepalives_idle, keepalives_interval, keepalives_count, requiressl, sslcompression, sslmode, sslcert, sslkey, sslrootcert, sslcrl, requirepeer, krbsrvname, gsslib, fetch_count
+ ALTER SERVER loopback2 OPTIONS (ADD fetch_count '2');
+ ALTER USER MAPPING FOR public SERVER loopback1
+ OPTIONS (DROP user, DROP password);
+ ALTER USER MAPPING FOR public SERVER loopback1
+ OPTIONS (host 'value'); -- ERROR
+ ERROR: invalid option "host"
+ HINT: Valid options in this context are: user, password
+ ALTER FOREIGN TABLE ft1 OPTIONS (nspname 'S 1', relname 'T 1');
+ ALTER FOREIGN TABLE ft2 OPTIONS (nspname 'S 1', relname 'T 1', fetch_count '100');
+ ALTER FOREIGN TABLE ft1 OPTIONS (invalid 'value'); -- ERROR
+ ERROR: invalid option "invalid"
+ HINT: Valid options in this context are: nspname, relname, fetch_count
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count 'a'); -- ERROR
+ ERROR: invalid value for fetch_count: "a"
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count '0'); -- ERROR
+ ERROR: invalid value for fetch_count: "0"
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count '-1'); -- ERROR
+ ERROR: invalid value for fetch_count: "-1"
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (invalid 'value'); -- ERROR
+ ERROR: invalid option "invalid"
+ HINT: Valid options in this context are: colname
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (colname 'C 1');
+ ALTER FOREIGN TABLE ft2 ALTER COLUMN c1 OPTIONS (colname 'C 1');
+ \dew+
+ List of foreign-data wrappers
+ Name | Owner | Handler | Validator | Access privileges | FDW Options | Description
+ -----------+----------------+-------------------+---------------------+-------------------+-------------+-------------
+ pgsql_fdw | pgsql_fdw_user | pgsql_fdw_handler | pgsql_fdw_validator | | |
+ (1 row)
+
+ \des+
+ List of foreign servers
+ Name | Owner | Foreign-data wrapper | Access privileges | Type | Version | FDW Options | Description
+ -----------+----------------+----------------------+-------------------+------+---------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+-------------
+ loopback1 | pgsql_fdw_user | pgsql_fdw | | | | (authtype 'value', service 'value', connect_timeout 'value', dbname 'value', host 'value', hostaddr 'value', port 'value', tty 'value', options 'value', application_name 'value', keepalives 'value', keepalives_idle 'value', keepalives_interval 'value', sslcompression 'value', sslmode 'value', sslcert 'value', sslkey 'value', sslrootcert 'value', sslcrl 'value') |
+ loopback2 | pgsql_fdw_user | pgsql_fdw | | | | (dbname 'contrib_regression', fetch_count '2') |
+ (2 rows)
+
+ \deu+
+ List of user mappings
+ Server | User name | FDW Options
+ -----------+----------------+-------------
+ loopback1 | public |
+ loopback2 | pgsql_fdw_user |
+ (2 rows)
+
+ \det+
+ List of foreign tables
+ Schema | Table | Server | FDW Options | Description
+ --------+-------+-----------+---------------------------------------------------+-------------
+ public | ft1 | loopback2 | (nspname 'S 1', relname 'T 1') |
+ public | ft2 | loopback2 | (nspname 'S 1', relname 'T 1', fetch_count '100') |
+ (2 rows)
+
+ -- ===================================================================
+ -- simple queries
+ -- ===================================================================
+ -- single table, with/without alias
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
+ QUERY PLAN
+ ------------------------------------------------------------------------------------------------------------------------------
+ Limit
+ Output: c1, c2, c3, c4, c5, c6, c7
+ -> Sort
+ Output: c1, c2, c3, c4, c5, c6, c7
+ Sort Key: ft1.c3, ft1.c1
+ -> Foreign Scan on public.ft1
+ Output: c1, c2, c3, c4, c5, c6, c7
+ Remote SQL: DECLARE pgsql_fdw_cursor_0 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (8 rows)
+
+ SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ -----+----+-------+------------------------------+--------------------------+----+------------
+ 101 | 1 | 00101 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970 | 1 | 1
+ 102 | 2 | 00102 | Sat Jan 03 00:00:00 1970 PST | Sat Jan 03 00:00:00 1970 | 2 | 2
+ 103 | 3 | 00103 | Sun Jan 04 00:00:00 1970 PST | Sun Jan 04 00:00:00 1970 | 3 | 3
+ 104 | 4 | 00104 | Mon Jan 05 00:00:00 1970 PST | Mon Jan 05 00:00:00 1970 | 4 | 4
+ 105 | 5 | 00105 | Tue Jan 06 00:00:00 1970 PST | Tue Jan 06 00:00:00 1970 | 5 | 5
+ 106 | 6 | 00106 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6
+ 107 | 7 | 00107 | Thu Jan 08 00:00:00 1970 PST | Thu Jan 08 00:00:00 1970 | 7 | 7
+ 108 | 8 | 00108 | Fri Jan 09 00:00:00 1970 PST | Fri Jan 09 00:00:00 1970 | 8 | 8
+ 109 | 9 | 00109 | Sat Jan 10 00:00:00 1970 PST | Sat Jan 10 00:00:00 1970 | 9 | 9
+ 110 | 0 | 00110 | Sun Jan 11 00:00:00 1970 PST | Sun Jan 11 00:00:00 1970 | 0 | 0
+ (10 rows)
+
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+ QUERY PLAN
+ ------------------------------------------------------------------------------------------------------------------------------
+ Limit
+ Output: c1, c2, c3, c4, c5, c6, c7
+ -> Sort
+ Output: c1, c2, c3, c4, c5, c6, c7
+ Sort Key: t1.c3, t1.c1
+ -> Foreign Scan on public.ft1 t1
+ Output: c1, c2, c3, c4, c5, c6, c7
+ Remote SQL: DECLARE pgsql_fdw_cursor_2 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (8 rows)
+
+ SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ -----+----+-------+------------------------------+--------------------------+----+------------
+ 101 | 1 | 00101 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970 | 1 | 1
+ 102 | 2 | 00102 | Sat Jan 03 00:00:00 1970 PST | Sat Jan 03 00:00:00 1970 | 2 | 2
+ 103 | 3 | 00103 | Sun Jan 04 00:00:00 1970 PST | Sun Jan 04 00:00:00 1970 | 3 | 3
+ 104 | 4 | 00104 | Mon Jan 05 00:00:00 1970 PST | Mon Jan 05 00:00:00 1970 | 4 | 4
+ 105 | 5 | 00105 | Tue Jan 06 00:00:00 1970 PST | Tue Jan 06 00:00:00 1970 | 5 | 5
+ 106 | 6 | 00106 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6
+ 107 | 7 | 00107 | Thu Jan 08 00:00:00 1970 PST | Thu Jan 08 00:00:00 1970 | 7 | 7
+ 108 | 8 | 00108 | Fri Jan 09 00:00:00 1970 PST | Fri Jan 09 00:00:00 1970 | 8 | 8
+ 109 | 9 | 00109 | Sat Jan 10 00:00:00 1970 PST | Sat Jan 10 00:00:00 1970 | 9 | 9
+ 110 | 0 | 00110 | Sun Jan 11 00:00:00 1970 PST | Sun Jan 11 00:00:00 1970 | 0 | 0
+ (10 rows)
+
+ -- with WHERE clause
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
+ QUERY PLAN
+ ------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on public.ft1 t1
+ Output: c1, c2, c3, c4, c5, c6, c7
+ Filter: ((t1.c1 = 101) AND ((t1.c6)::text = '1'::text) AND (t1.c7 = '1'::bpchar))
+ Remote SQL: DECLARE pgsql_fdw_cursor_4 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (4 rows)
+
+ SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ -----+----+-------+------------------------------+--------------------------+----+------------
+ 101 | 1 | 00101 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970 | 1 | 1
+ (1 row)
+
+ -- aggregate
+ SELECT COUNT(*) FROM ft1 t1;
+ count
+ -------
+ 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
+ ----+----+-------+------------------------------+--------------------------+----+------------
+ 1 | 1 | 00001 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970 | 1 | 1
+ 2 | 2 | 00002 | Sat Jan 03 00:00:00 1970 PST | Sat Jan 03 00:00:00 1970 | 2 | 2
+ 3 | 3 | 00003 | Sun Jan 04 00:00:00 1970 PST | Sun Jan 04 00:00:00 1970 | 3 | 3
+ 4 | 4 | 00004 | Mon Jan 05 00:00:00 1970 PST | Mon Jan 05 00:00:00 1970 | 4 | 4
+ 5 | 5 | 00005 | Tue Jan 06 00:00:00 1970 PST | Tue Jan 06 00:00:00 1970 | 5 | 5
+ 6 | 6 | 00006 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6
+ 7 | 7 | 00007 | Thu Jan 08 00:00:00 1970 PST | Thu Jan 08 00:00:00 1970 | 7 | 7
+ 8 | 8 | 00008 | Fri Jan 09 00:00:00 1970 PST | Fri Jan 09 00:00:00 1970 | 8 | 8
+ 9 | 9 | 00009 | Sat Jan 10 00:00:00 1970 PST | Sat Jan 10 00:00:00 1970 | 9 | 9
+ 10 | 0 | 00010 | Sun Jan 11 00:00:00 1970 PST | Sun Jan 11 00:00:00 1970 | 0 | 0
+ (10 rows)
+
+ -- subquery+MAX
+ SELECT * FROM ft1 t1 WHERE t1.c3 = (SELECT MAX(c3) FROM ft2 t2) ORDER BY c1;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ ------+----+-------+------------------------------+--------------------------+----+------------
+ 1000 | 0 | 01000 | Thu Jan 01 00:00:00 1970 PST | Thu Jan 01 00:00:00 1970 | 0 | 0
+ (1 row)
+
+ -- used in CTE
+ WITH t1 AS (SELECT * FROM ft1 WHERE c1 <= 10) SELECT t2.c1, t2.c2, t2.c3, t2.c4 FROM t1, ft2 t2 WHERE t1.c1 = t2.c1 ORDER BY t1.c1;
+ c1 | c2 | c3 | c4
+ ----+----+-------+------------------------------
+ 1 | 1 | 00001 | Fri Jan 02 00:00:00 1970 PST
+ 2 | 2 | 00002 | Sat Jan 03 00:00:00 1970 PST
+ 3 | 3 | 00003 | Sun Jan 04 00:00:00 1970 PST
+ 4 | 4 | 00004 | Mon Jan 05 00:00:00 1970 PST
+ 5 | 5 | 00005 | Tue Jan 06 00:00:00 1970 PST
+ 6 | 6 | 00006 | Wed Jan 07 00:00:00 1970 PST
+ 7 | 7 | 00007 | Thu Jan 08 00:00:00 1970 PST
+ 8 | 8 | 00008 | Fri Jan 09 00:00:00 1970 PST
+ 9 | 9 | 00009 | Sat Jan 10 00:00:00 1970 PST
+ 10 | 0 | 00010 | Sun Jan 11 00:00:00 1970 PST
+ (10 rows)
+
+ -- fixed values
+ SELECT 'fixed', NULL FROM ft1 t1 WHERE c1 = 1;
+ ?column? | ?column?
+ ----------+----------
+ fixed |
+ (1 row)
+
+ -- user-defined operator/function
+ CREATE FUNCTION pgsql_fdw_abs(int) RETURNS int AS $$
+ BEGIN
+ RETURN abs($1);
+ END
+ $$ LANGUAGE plpgsql IMMUTABLE;
+ CREATE OPERATOR === (
+ LEFTARG = int,
+ RIGHTARG = int,
+ PROCEDURE = int4eq,
+ COMMUTATOR = ===,
+ NEGATOR = !==
+ );
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = pgsql_fdw_abs(t1.c2);
+ QUERY PLAN
+ ---------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = pgsql_fdw_abs(c2))
+ Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 === t1.c2;
+ QUERY PLAN
+ ---------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 === c2)
+ Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = abs(t1.c2);
+ QUERY PLAN
+ ---------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = abs(c2))
+ Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = t1.c2;
+ QUERY PLAN
+ ---------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = c2)
+ Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ -- ===================================================================
+ -- parameterized queries
+ -- ===================================================================
+ -- simple join
+ PREPARE st1(int, int) AS SELECT t1.c3, t2.c3 FROM ft1 t1, ft2 t2 WHERE t1.c1 = $1 AND t2.c1 = $2;
+ EXPLAIN (COSTS false) EXECUTE st1(1, 2);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------
+ Nested Loop
+ -> Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: SELECT "C 1", NULL, c3, NULL, NULL, NULL, NULL FROM "S 1"."T 1"
+ -> Foreign Scan on ft2 t2
+ Filter: (c1 = 2)
+ Remote SQL: SELECT "C 1", NULL, c3, NULL, NULL, NULL, NULL FROM "S 1"."T 1"
+ (7 rows)
+
+ EXECUTE st1(1, 1);
+ c3 | c3
+ -------+-------
+ 00001 | 00001
+ (1 row)
+
+ EXECUTE st1(101, 101);
+ c3 | c3
+ -------+-------
+ 00101 | 00101
+ (1 row)
+
+ -- subquery using stable function (can't be pushed down)
+ PREPARE st2(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c4) = 6) ORDER BY c1;
+ EXPLAIN (COSTS false) EXECUTE st2(10, 20);
+ QUERY PLAN
+ ------------------------------------------------------------------------------------------------
+ Sort
+ Sort Key: t1.c1
+ -> Hash Semi Join
+ Hash Cond: (t1.c3 = t2.c3)
+ -> Foreign Scan on ft1 t1
+ Filter: (c1 < 20)
+ Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ -> Hash
+ -> Foreign Scan on ft2 t2
+ Filter: ((c1 > 10) AND (date_part('dow'::text, c4) = 6::double precision))
+ Remote SQL: SELECT "C 1", NULL, c3, c4, NULL, NULL, NULL FROM "S 1"."T 1"
+ (11 rows)
+
+ EXECUTE st2(10, 20);
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ ----+----+-------+------------------------------+--------------------------+----+------------
+ 16 | 6 | 00016 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970 | 6 | 6
+ (1 row)
+
+ EXECUTE st1(101, 101);
+ c3 | c3
+ -------+-------
+ 00101 | 00101
+ (1 row)
+
+ -- subquery using immutable function (can be pushed down)
+ PREPARE st3(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c5) = 6) ORDER BY c1;
+ EXPLAIN (COSTS false) EXECUTE st3(10, 20);
+ QUERY PLAN
+ ------------------------------------------------------------------------------------------------
+ Sort
+ Sort Key: t1.c1
+ -> Hash Semi Join
+ Hash Cond: (t1.c3 = t2.c3)
+ -> Foreign Scan on ft1 t1
+ Filter: (c1 < 20)
+ Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ -> Hash
+ -> Foreign Scan on ft2 t2
+ Filter: ((c1 > 10) AND (date_part('dow'::text, c5) = 6::double precision))
+ Remote SQL: SELECT "C 1", NULL, c3, NULL, c5, NULL, NULL FROM "S 1"."T 1"
+ (11 rows)
+
+ EXECUTE st3(10, 20);
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ ----+----+-------+------------------------------+--------------------------+----+------------
+ 16 | 6 | 00016 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970 | 6 | 6
+ (1 row)
+
+ EXECUTE st3(20, 30);
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ ----+----+-------+------------------------------+--------------------------+----+------------
+ 23 | 3 | 00023 | Sat Jan 24 00:00:00 1970 PST | Sat Jan 24 00:00:00 1970 | 3 | 3
+ (1 row)
+
+ -- custom plan should be chosen
+ PREPARE st4(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 = $1;
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ ---------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ ---------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ ---------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ ---------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ ---------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ ---------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = $1)
+ Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ -- cleanup
+ DEALLOCATE st1;
+ DEALLOCATE st2;
+ DEALLOCATE st3;
+ DEALLOCATE st4;
+ -- ===================================================================
+ -- connection management
+ -- ===================================================================
+ SELECT srvname, usename FROM pgsql_fdw_connections;
+ srvname | usename
+ -----------+----------------
+ loopback2 | pgsql_fdw_user
+ (1 row)
+
+ SELECT pgsql_fdw_disconnect(srvid, usesysid) FROM pgsql_fdw_get_connections();
+ pgsql_fdw_disconnect
+ ----------------------
+ OK
+ (1 row)
+
+ SELECT srvname, usename FROM pgsql_fdw_connections;
+ srvname | usename
+ ---------+---------
+ (0 rows)
+
+ -- ===================================================================
+ -- subtransaction
+ -- ===================================================================
+ BEGIN;
+ DECLARE c CURSOR FOR SELECT * FROM ft1 ORDER BY c1;
+ FETCH c;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ ----+----+-------+------------------------------+--------------------------+----+------------
+ 1 | 1 | 00001 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970 | 1 | 1
+ (1 row)
+
+ SAVEPOINT s;
+ ERROR OUT; -- ERROR
+ ERROR: syntax error at or near "ERROR"
+ LINE 1: ERROR OUT;
+ ^
+ ROLLBACK TO s;
+ SELECT srvname FROM pgsql_fdw_connections;
+ srvname
+ -----------
+ loopback2
+ (1 row)
+
+ FETCH c;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ ----+----+-------+------------------------------+--------------------------+----+------------
+ 2 | 2 | 00002 | Sat Jan 03 00:00:00 1970 PST | Sat Jan 03 00:00:00 1970 | 2 | 2
+ (1 row)
+
+ COMMIT;
+ SELECT srvname FROM pgsql_fdw_connections;
+ srvname
+ -----------
+ loopback2
+ (1 row)
+
+ ERROR OUT; -- ERROR
+ ERROR: syntax error at or near "ERROR"
+ LINE 1: ERROR OUT;
+ ^
+ SELECT srvname FROM pgsql_fdw_connections;
+ srvname
+ ---------
+ (0 rows)
+
+ -- ===================================================================
+ -- cleanup
+ -- ===================================================================
+ DROP OPERATOR === (int, int) CASCADE;
+ DROP OPERATOR !== (int, int) CASCADE;
+ DROP FUNCTION pgsql_fdw_abs(int);
+ DROP SCHEMA "S 1" CASCADE;
+ NOTICE: drop cascades to 2 other objects
+ DETAIL: drop cascades to table "S 1"."T 1"
+ drop cascades to table "S 1"."T 2"
+ DROP EXTENSION pgsql_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 server loopback2
+ drop cascades to user mapping for pgsql_fdw_user
+ drop cascades to foreign table ft1
+ drop cascades to foreign table ft2
+ \c
+ DROP ROLE pgsql_fdw_user;
diff --git a/contrib/pgsql_fdw/option.c b/contrib/pgsql_fdw/option.c
index ...a2a8927 .
*** a/contrib/pgsql_fdw/option.c
--- b/contrib/pgsql_fdw/option.c
***************
*** 0 ****
--- 1,242 ----
+ /*-------------------------------------------------------------------------
+ *
+ * option.c
+ * FDW option handling
+ *
+ * Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/option.c
+ *
+ *-------------------------------------------------------------------------
+ */
+ #include "postgres.h"
+
+ #include "access/reloptions.h"
+ #include "catalog/pg_foreign_data_wrapper.h"
+ #include "catalog/pg_foreign_server.h"
+ #include "catalog/pg_foreign_table.h"
+ #include "catalog/pg_user_mapping.h"
+ #include "commands/defrem.h"
+ #include "fmgr.h"
+ #include "foreign/foreign.h"
+ #include "lib/stringinfo.h"
+ #include "miscadmin.h"
+
+ #include "pgsql_fdw.h"
+
+ /*
+ * SQL functions
+ */
+ extern Datum pgsql_fdw_validator(PG_FUNCTION_ARGS);
+ PG_FUNCTION_INFO_V1(pgsql_fdw_validator);
+
+ /*
+ * Describes the valid options for objects that use this wrapper.
+ */
+ typedef struct PgsqlFdwOption
+ {
+ const char *optname;
+ Oid optcontext; /* Oid of catalog in which options may appear */
+ bool is_libpq_opt; /* true if it's used in libpq */
+ } PgsqlFdwOption;
+
+ /*
+ * Valid options for pgsql_fdw.
+ */
+ static PgsqlFdwOption valid_options[] = {
+
+ /*
+ * Options for libpq connection.
+ * Note: This list should be updated along with PQconninfoOptions in
+ * interfaces/libpq/fe-connect.c, so the order is kept as is.
+ *
+ * Some useless libpq connection options are not accepted by pgsql_fdw:
+ * client_encoding: set to local database encoding automatically
+ * fallback_application_name: fixed to "pgsql_fdw"
+ * replication: pgsql_fdw never be replication client
+ */
+ {"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},
+ #ifdef NOT_USED
+ {"client_encoding", ForeignServerRelationId, true},
+ #endif
+ {"tty", ForeignServerRelationId, true},
+ {"options", ForeignServerRelationId, true},
+ {"application_name", ForeignServerRelationId, true},
+ #ifdef NOT_USED
+ {"fallback_application_name", ForeignServerRelationId, true},
+ #endif
+ {"keepalives", ForeignServerRelationId, true},
+ {"keepalives_idle", ForeignServerRelationId, true},
+ {"keepalives_interval", ForeignServerRelationId, true},
+ {"keepalives_count", ForeignServerRelationId, true},
+ {"requiressl", ForeignServerRelationId, true},
+ {"sslcompression", ForeignServerRelationId, true},
+ {"sslmode", ForeignServerRelationId, true},
+ {"sslcert", ForeignServerRelationId, true},
+ {"sslkey", ForeignServerRelationId, true},
+ {"sslrootcert", ForeignServerRelationId, true},
+ {"sslcrl", ForeignServerRelationId, true},
+ {"requirepeer", ForeignServerRelationId, true},
+ {"krbsrvname", ForeignServerRelationId, true},
+ {"gsslib", ForeignServerRelationId, true},
+ #ifdef NOT_USED
+ {"replication", ForeignServerRelationId, true},
+ #endif
+
+ /*
+ * Options for translation of object names.
+ */
+ {"nspname", ForeignTableRelationId, false},
+ {"relname", ForeignTableRelationId, false},
+ {"colname", AttributeRelationId, false},
+
+ /*
+ * Options for cursor behavior.
+ * These options can be overridden by finer-grained objects.
+ */
+ {"fetch_count", ForeignTableRelationId, false},
+ {"fetch_count", ForeignServerRelationId, false},
+
+ /* Terminating entry --- MUST BE LAST */
+ {NULL, InvalidOid, false}
+ };
+
+ /*
+ * Helper functions
+ */
+ static bool is_valid_option(const char *optname, Oid context);
+
+ /*
+ * Validate the generic options given to a FOREIGN DATA WRAPPER, SERVER,
+ * USER MAPPING or FOREIGN TABLE that uses pgsql_fdw.
+ *
+ * Raise an ERROR if the option or its value is considered invalid.
+ */
+ Datum
+ pgsql_fdw_validator(PG_FUNCTION_ARGS)
+ {
+ List *options_list = untransformRelOptions(PG_GETARG_DATUM(0));
+ Oid catalog = PG_GETARG_OID(1);
+ ListCell *cell;
+
+ /*
+ * Check that only options supported by pgsql_fdw, and allowed for the
+ * current object type, are given.
+ */
+ foreach(cell, options_list)
+ {
+ DefElem *def = (DefElem *) lfirst(cell);
+
+ if (!is_valid_option(def->defname, catalog))
+ {
+ PgsqlFdwOption *opt;
+ StringInfoData buf;
+
+ /*
+ * Unknown option specified, complain about it. Provide a hint
+ * with list of valid options for the object.
+ */
+ initStringInfo(&buf);
+ for (opt = valid_options; opt->optname; opt++)
+ {
+ if (catalog == opt->optcontext)
+ appendStringInfo(&buf, "%s%s", (buf.len > 0) ? ", " : "",
+ opt->optname);
+ }
+
+ ereport(ERROR,
+ (errcode(ERRCODE_FDW_INVALID_OPTION_NAME),
+ errmsg("invalid option \"%s\"", def->defname),
+ errhint("Valid options in this context are: %s",
+ buf.data)));
+ }
+
+ /* fetch_count be positive digit number. */
+ if (strcmp(def->defname, "fetch_count") == 0)
+ {
+ long value;
+ char *p = NULL;
+
+ value = strtol(defGetString(def), &p, 10);
+ if (*p != '\0' || value < 1)
+ ereport(ERROR,
+ (errcode(ERRCODE_FDW_INVALID_ATTRIBUTE_VALUE),
+ errmsg("invalid value for %s: \"%s\"",
+ def->defname, defGetString(def))));
+ }
+ }
+
+ /*
+ * We don't care option-specific limitation here; they will be validated at
+ * the execution time.
+ */
+
+ PG_RETURN_VOID();
+ }
+
+ /*
+ * Check whether the given option is one of the valid pgsql_fdw options.
+ * context is the Oid of the catalog holding the object the option is for.
+ */
+ static bool
+ is_valid_option(const char *optname, Oid context)
+ {
+ PgsqlFdwOption *opt;
+
+ for (opt = valid_options; opt->optname; opt++)
+ {
+ if (context == opt->optcontext && strcmp(opt->optname, optname) == 0)
+ return true;
+ }
+ return false;
+ }
+
+ /*
+ * Check whether the given option is one of the valid libpq options.
+ * context is the Oid of the catalog holding the object the option is for.
+ */
+ static bool
+ is_libpq_option(const char *optname)
+ {
+ PgsqlFdwOption *opt;
+
+ for (opt = valid_options; opt->optname; opt++)
+ {
+ if (strcmp(opt->optname, optname) == 0 && opt->is_libpq_opt)
+ return true;
+ }
+ return false;
+ }
+
+ /*
+ * Generate key-value arrays which includes only libpq options from the list
+ * which contains any kind of options.
+ */
+ int
+ ExtractConnectionOptions(List *defelems, const char **keywords, const char **values)
+ {
+ ListCell *lc;
+ int i;
+
+ i = 0;
+ foreach(lc, defelems)
+ {
+ DefElem *d = (DefElem *) lfirst(lc);
+ if (is_libpq_option(d->defname))
+ {
+ keywords[i] = d->defname;
+ values[i] = defGetString(d);
+ i++;
+ }
+ }
+ return i;
+ }
diff --git a/contrib/pgsql_fdw/pgsql_fdw--1.0.sql b/contrib/pgsql_fdw/pgsql_fdw--1.0.sql
index ...b0ea2b2 .
*** a/contrib/pgsql_fdw/pgsql_fdw--1.0.sql
--- b/contrib/pgsql_fdw/pgsql_fdw--1.0.sql
***************
*** 0 ****
--- 1,39 ----
+ /* contrib/pgsql_fdw/pgsql_fdw--1.0.sql */
+
+ -- complain if script is sourced in psql, rather than via CREATE EXTENSION
+ \echo Use "CREATE EXTENSION pgsql_fdw" to load this file. \quit
+
+ CREATE FUNCTION pgsql_fdw_handler()
+ RETURNS fdw_handler
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C STRICT;
+
+ CREATE FUNCTION pgsql_fdw_validator(text[], oid)
+ RETURNS void
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C STRICT;
+
+ CREATE FOREIGN DATA WRAPPER pgsql_fdw
+ HANDLER pgsql_fdw_handler
+ VALIDATOR pgsql_fdw_validator;
+
+ /* connection management functions and view */
+ CREATE FUNCTION pgsql_fdw_get_connections(out srvid oid, out usesysid oid)
+ RETURNS SETOF record
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C STRICT;
+
+ CREATE FUNCTION pgsql_fdw_disconnect(oid, oid)
+ RETURNS text
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C STRICT;
+
+ CREATE VIEW pgsql_fdw_connections AS
+ SELECT c.srvid srvid,
+ s.srvname srvname,
+ c.usesysid usesysid,
+ pg_get_userbyid(c.usesysid) usename
+ FROM pgsql_fdw_get_connections() c
+ JOIN pg_catalog.pg_foreign_server s ON (s.oid = c.srvid);
+ GRANT SELECT ON pgsql_fdw_connections TO public;
+
diff --git a/contrib/pgsql_fdw/pgsql_fdw.c b/contrib/pgsql_fdw/pgsql_fdw.c
index ...df7f407 .
*** a/contrib/pgsql_fdw/pgsql_fdw.c
--- b/contrib/pgsql_fdw/pgsql_fdw.c
***************
*** 0 ****
--- 1,891 ----
+ /*-------------------------------------------------------------------------
+ *
+ * pgsql_fdw.c
+ * foreign-data wrapper for remote PostgreSQL servers.
+ *
+ * Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/pgsql_fdw.c
+ *
+ *-------------------------------------------------------------------------
+ */
+ #include "postgres.h"
+ #include "fmgr.h"
+
+ #include "catalog/pg_foreign_server.h"
+ #include "catalog/pg_foreign_table.h"
+ #include "commands/defrem.h"
+ #include "commands/explain.h"
+ #include "foreign/fdwapi.h"
+ #include "funcapi.h"
+ #include "miscadmin.h"
+ #include "nodes/nodeFuncs.h"
+ #include "optimizer/cost.h"
+ #include "optimizer/pathnode.h"
+ #include "optimizer/planmain.h"
+ #include "optimizer/restrictinfo.h"
+ #include "utils/lsyscache.h"
+ #include "utils/memutils.h"
+ #include "utils/rel.h"
+
+ #include "pgsql_fdw.h"
+ #include "connection.h"
+
+ PG_MODULE_MAGIC;
+
+ /*
+ * Default fetch count for cursor. This can be overridden by fetch_count FDW
+ * option.
+ */
+ #define DEFAULT_FETCH_COUNT 10000
+
+ /*
+ * Cost to establish a connection.
+ * XXX: should be configurable per server?
+ */
+ #define CONNECTION_COSTS 100.0
+
+ /*
+ * Cost to transfer 1 byte from remote server.
+ * XXX: should be configurable per server?
+ */
+ #define TRANSFER_COSTS_PER_BYTE 0.001
+
+ /*
+ * Cursors which are used together in a local query require different name, so
+ * we use simple incremental name for that purpose. We don't care wrap around
+ * of cursor_id because it's hard to imagine that 2^32 cursors are used in a
+ * query.
+ */
+ #define CURSOR_NAME_FORMAT "pgsql_fdw_cursor_%u"
+ static uint32 cursor_id = 0;
+
+ /*
+ * Index of FDW-private information stored in fdw_private list.
+ *
+ * We store various information in ForeignScan.fdw_private to pass them beyond
+ * the boundary between planner and executor. Finally FdwPlan using cursor
+ * would hold items below:
+ *
+ * 1) plain SELECT statement
+ * 2) SQL statement used to declare cursor
+ * 3) SQL statement used to fetch rows from cursor
+ * 5) SQL statement used to reset cursor
+ * 5) SQL statement used to close cursor
+ *
+ * These items are indexed with the enum FdwPrivateIndex, so an item
+ * can be accessed directly via list_nth(). For example of FETCH
+ * statement:
+ * list_nth(fdw_private, FdwPrivateFetchSql)
+ */
+ enum FdwPrivateIndex {
+ /* SQL statements */
+ FdwPrivateSelectSql,
+ FdwPrivateDeclareSql,
+ FdwPrivateFetchSql,
+ FdwPrivateResetSql,
+ FdwPrivateCloseSql,
+
+ /* # of elements stored in the list fdw_private */
+ FdwPrivateNum,
+ };
+
+ /*
+ * Describes an execution state of a foreign scan against a foreign table
+ * using pgsql_fdw.
+ */
+ typedef struct PgsqlFdwExecutionState
+ {
+ MemoryContext scan_cxt; /* context for per-scan lifespan data */
+
+ List *fdw_private; /* FDW-private information */
+ PGconn *conn; /* connection for the scan */
+
+ Oid *param_types; /* type array of external parameter */
+ const char **param_values; /* value array of external parameter */
+
+ int attnum; /* # of non-dropped attribute */
+ char **col_values; /* column value buffer */
+ AttInMetadata *attinmeta; /* attribute metadata */
+
+ Tuplestorestate *tuples; /* result of the scan */
+ } PgsqlFdwExecutionState;
+
+ /*
+ * SQL functions
+ */
+ extern Datum pgsql_fdw_handler(PG_FUNCTION_ARGS);
+ PG_FUNCTION_INFO_V1(pgsql_fdw_handler);
+
+ /*
+ * FDW callback routines
+ */
+ static void pgsqlGetForeignRelSize(PlannerInfo *root,
+ RelOptInfo *baserel,
+ Oid foreigntableid);
+ static void pgsqlGetForeignPaths(PlannerInfo *root,
+ RelOptInfo *baserel,
+ Oid foreigntableid);
+ static ForeignScan *pgsqlGetForeignPlan(PlannerInfo *root,
+ RelOptInfo *baserel,
+ Oid foreigntableid,
+ ForeignPath *best_path,
+ List *tlist,
+ List *scan_clauses);
+ static void pgsqlExplainForeignScan(ForeignScanState *node, ExplainState *es);
+ static void pgsqlBeginForeignScan(ForeignScanState *node, int eflags);
+ static TupleTableSlot *pgsqlIterateForeignScan(ForeignScanState *node);
+ static void pgsqlReScanForeignScan(ForeignScanState *node);
+ static void pgsqlEndForeignScan(ForeignScanState *node);
+
+ /*
+ * Helper functions
+ */
+ static void adjust_costs(double rows, int width,
+ Cost *startup_cost, Cost *total_cost);
+ static void execute_query(ForeignScanState *node);
+ static PGresult *fetch_result(ForeignScanState *node);
+ static void store_result(ForeignScanState *node, PGresult *res);
+
+ /* Exported functions, but not written in pgsql_fdw.h. */
+ void _PG_init(void);
+ void _PG_fini(void);
+
+ /*
+ * Module-specific initialization.
+ */
+ void
+ _PG_init(void)
+ {
+ }
+
+ /*
+ * Module-specific clean up.
+ */
+ void
+ _PG_fini(void)
+ {
+ }
+
+ /*
+ * The content of FdwRoutine of pgsql_fdw is never changed, so we use one
+ * static object for all scan.
+ */
+ static FdwRoutine fdwroutine = {
+
+ /* Node type */
+ T_FdwRoutine,
+
+ /* Required handler functions. */
+ pgsqlGetForeignRelSize,
+ pgsqlGetForeignPaths,
+ pgsqlGetForeignPlan,
+ pgsqlExplainForeignScan,
+ pgsqlBeginForeignScan,
+ pgsqlIterateForeignScan,
+ pgsqlReScanForeignScan,
+ pgsqlEndForeignScan,
+
+ /* Optional handler functions. */
+ };
+
+ /*
+ * Foreign-data wrapper handler function: return a struct with pointers
+ * to my callback routines.
+ */
+ Datum
+ pgsql_fdw_handler(PG_FUNCTION_ARGS)
+ {
+ PG_RETURN_POINTER(&fdwroutine);
+ }
+
+ /*
+ * pgsqlGetForeignRelSize
+ * Estimate # of rows and width of the result of the scan
+ */
+ static void
+ pgsqlGetForeignRelSize(PlannerInfo *root,
+ RelOptInfo *baserel,
+ Oid foreigntableid)
+ {
+ /* Estimate # of rows by calling set_baserel_size_estimates(). */
+ set_baserel_size_estimates(root, baserel);
+ }
+
+ /*
+ * pgsqlGetForeignPaths
+ * Create possible scan paths for a scan on the foreign table
+ */
+ static void
+ pgsqlGetForeignPaths(PlannerInfo *root,
+ RelOptInfo *baserel,
+ Oid foreigntableid)
+ {
+ Path *seqscan;
+ ForeignPath *path;
+ Cost startup_cost;
+ Cost total_cost;
+
+ /*
+ * First, we estimate rough cost with assuming sequential scan is
+ * performed on remote side. Those cost estimation should be modified
+ * to respect cost of establishing connection and transferring data.
+ * This design might cause overestimate, but it seems better than
+ * underestimate.
+ */
+ seqscan = create_seqscan_path(root, baserel);
+ cost_seqscan(seqscan, root, baserel);
+ startup_cost = seqscan->startup_cost;
+ total_cost = seqscan->total_cost;
+ adjust_costs(baserel->rows, baserel->width, &startup_cost, &total_cost);
+
+ /*
+ * Create simplest ForeignScan path node and add it to baserel. This path
+ * corresponds to SeqScan path of regular tables.
+ */
+ path = create_foreignscan_path(root, baserel,
+ baserel->rows,
+ startup_cost,
+ total_cost,
+ NIL, /* no pathkeys */
+ NULL, /* no outer rel either */
+ NIL, /* no param clause */
+ NIL); /* no FDW private */
+ add_path(baserel, (Path *) path);
+
+ /*
+ * XXX We can consider sorted path or parameterized path here if we know
+ * that foreign table is indexed on remote end. For this purpose, we
+ * might have to support FOREIGN INDEX to represent possible sets of sort
+ * keys and/or filtering.
+ */
+ }
+
+ /*
+ * pgsqlGetForeignPlan
+ * Create ForeignScan plan node which implements selected best path
+ */
+ static ForeignScan *
+ pgsqlGetForeignPlan(PlannerInfo *root,
+ RelOptInfo *baserel,
+ Oid foreigntableid,
+ ForeignPath *best_path,
+ List *tlist,
+ List *scan_clauses)
+ {
+ Index scan_relid = baserel->relid;
+ List *fdw_private = NIL;
+ ListCell *lc;
+ char name[128]; /* must be larger than format + 10 */
+ StringInfoData cursor;
+ DefElem *def;
+ int fetch_count = DEFAULT_FETCH_COUNT;
+ char *sql;
+ ForeignTable *table;
+ ForeignServer *server;
+
+ /*
+ * We have no native ability to evaluate restriction clauses, so we just
+ * put all the scan_clauses into the plan node's qual list for the
+ * executor to check. So all we have to do here is strip RestrictInfo
+ * nodes from the clauses and ignore pseudoconstants (which will be
+ * handled elsewhere).
+ */
+ scan_clauses = extract_actual_clauses(scan_clauses, false);
+
+ /*
+ * Use specified fetch_count instead of default value, if any. Foreign
+ * table option overrides server option.
+ */
+ table = GetForeignTable(foreigntableid);
+ foreach(lc, table->options)
+ {
+ def = (DefElem *) lfirst(lc);
+ if (strcmp(def->defname, "fetch_count") == 0)
+ break;
+
+ }
+ if (lc == NULL)
+ {
+ server = GetForeignServer(table->serverid);
+ foreach(lc, server->options)
+ {
+ def = (DefElem *) lfirst(lc);
+ if (strcmp(def->defname, "fetch_count") == 0)
+ break;
+
+ }
+ }
+ if (lc != NULL)
+ fetch_count = strtol(defGetString(def), NULL, 10);
+ elog(DEBUG1, "relid=%u fetch_count=%d", foreigntableid, fetch_count);
+
+ /*
+ * Construct simple remote query which has no WHERE clause.
+ */
+ sql = deparseSimpleSql(foreigntableid, root, baserel, table);
+ fdw_private = lappend(fdw_private, makeString(sql));
+
+ /* Construct cursor name from sequential value */
+ sprintf(name, CURSOR_NAME_FORMAT, cursor_id++);
+
+ /* Construct statement to declare cursor */
+ initStringInfo(&cursor);
+ appendStringInfo(&cursor, "DECLARE %s SCROLL CURSOR FOR %s", name, sql);
+ fdw_private = lappend(fdw_private, makeString(cursor.data));
+
+ /* Construct statement to fetch rows from cursor */
+ initStringInfo(&cursor);
+ appendStringInfo(&cursor, "FETCH %d FROM %s", fetch_count, name);
+ fdw_private = lappend(fdw_private, makeString(cursor.data));
+
+ /* Construct statement to reset cursor */
+ initStringInfo(&cursor);
+ appendStringInfo(&cursor, "MOVE ABSOLUTE 0 FROM %s", name);
+ fdw_private = lappend(fdw_private, makeString(cursor.data));
+
+ /* Construct statement to close cursor */
+ initStringInfo(&cursor);
+ appendStringInfo(&cursor, "CLOSE %s", name);
+ fdw_private = lappend(fdw_private, makeString(cursor.data));
+
+ /* Create the ForeignScan node with fdw_private of selected path. */
+ return make_foreignscan(tlist,
+ scan_clauses,
+ scan_relid,
+ NIL,
+ fdw_private);
+ }
+
+ /*
+ * pgsqlExplainForeignScan
+ * Produce extra output for EXPLAIN
+ */
+ static void
+ pgsqlExplainForeignScan(ForeignScanState *node, ExplainState *es)
+ {
+ List *fdw_private;
+ char *sql;
+
+ /* CURSOR declaration is shown in only VERBOSE mode. */
+ fdw_private = ((ForeignScan *) node->ss.ps.plan)->fdw_private;
+ if (es->verbose)
+ sql = strVal(list_nth(fdw_private, FdwPrivateDeclareSql));
+ else
+ sql = strVal(list_nth(fdw_private, FdwPrivateSelectSql));
+ ExplainPropertyText("Remote SQL", sql, es);
+ }
+
+ /*
+ * pgsqlBeginForeignScan
+ * Initiate access to a foreign PostgreSQL table.
+ */
+ static void
+ pgsqlBeginForeignScan(ForeignScanState *node, int eflags)
+ {
+ PgsqlFdwExecutionState *festate;
+ PGconn *conn;
+ Oid relid;
+ ForeignTable *table;
+ ForeignServer *server;
+ UserMapping *user;
+ TupleTableSlot *slot = node->ss.ss_ScanTupleSlot;
+
+ /*
+ * Do nothing in EXPLAIN (no ANALYZE) case. node->fdw_state stays NULL.
+ */
+ if (eflags & EXEC_FLAG_EXPLAIN_ONLY)
+ return;
+
+ /*
+ * Save state in node->fdw_state.
+ */
+ festate = (PgsqlFdwExecutionState *) palloc(sizeof(PgsqlFdwExecutionState));
+ festate->fdw_private = ((ForeignScan *) node->ss.ps.plan)->fdw_private;
+
+ /*
+ * Create context for per-scan tuplestore under per-query context.
+ */
+ festate->scan_cxt = AllocSetContextCreate(node->ss.ps.state->es_query_cxt,
+ "pgsql_fdw",
+ ALLOCSET_DEFAULT_MINSIZE,
+ ALLOCSET_DEFAULT_INITSIZE,
+ ALLOCSET_DEFAULT_MAXSIZE);
+
+ /*
+ * Get connection to the foreign server. Connection manager would
+ * establish new connection if necessary.
+ */
+ relid = RelationGetRelid(node->ss.ss_currentRelation);
+ table = GetForeignTable(relid);
+ server = GetForeignServer(table->serverid);
+ user = GetUserMapping(GetOuterUserId(), server->serverid);
+ conn = GetConnection(server, user, true);
+ festate->conn = conn;
+
+ /* Result will be filled in first Iterate call. */
+ festate->tuples = NULL;
+
+ /* Allocate buffers for column values. */
+ {
+ TupleDesc tupdesc = slot->tts_tupleDescriptor;
+ festate->col_values = palloc(sizeof(char *) * tupdesc->natts);
+ festate->attinmeta = TupleDescGetAttInMetadata(tupdesc);
+ }
+
+ /* Allocate buffers for query parameters. */
+ {
+ ParamListInfo params = node->ss.ps.state->es_param_list_info;
+ int numParams = params ? params->numParams : 0;
+
+ if (numParams > 0)
+ {
+ festate->param_types = palloc0(sizeof(Oid) * numParams);
+ festate->param_values = palloc0(sizeof(char *) * numParams);
+ }
+ else
+ {
+ festate->param_types = NULL;
+ festate->param_values = NULL;
+ }
+ }
+
+ /* Store FDW-specific state into ForeignScanState */
+ node->fdw_state = (void *) festate;
+
+ return;
+ }
+
+ /*
+ * pgsqlIterateForeignScan
+ * Retrieve next row from the result set, or clear tuple slot to indicate
+ * EOF.
+ *
+ * Note that using per-query context when retrieving tuples from
+ * tuplestore to ensure that returned tuples can survive until next
+ * iteration because the tuple is released implicitly via ExecClearTuple.
+ * Retrieving a tuple from tuplestore in CurrentMemoryContext (it's a
+ * per-tuple context), ExecClearTuple will free dangling pointer.
+ */
+ static TupleTableSlot *
+ pgsqlIterateForeignScan(ForeignScanState *node)
+ {
+ PgsqlFdwExecutionState *festate;
+ TupleTableSlot *slot = node->ss.ss_ScanTupleSlot;
+ PGresult *res = NULL;
+ MemoryContext oldcontext = CurrentMemoryContext;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+
+ /*
+ * If this is the first call after Begin, we need to execute remote query.
+ * If the query needs cursor, we declare a cursor at first call and fetch
+ * from it in later calls.
+ */
+ if (festate->tuples == NULL)
+ execute_query(node);
+
+ /*
+ * If enough tuples are left in tuplestore, just return next tuple from it.
+ *
+ * It is necessary to switch to per-scan context to make returned tuple
+ * valid until next IterateForeignScan call, because it will be released
+ * with ExecClearTuple then. Otherwise, picked tuple is allocated in
+ * per-tuple context, and double-free of that tuple might happen.
+ */
+ MemoryContextSwitchTo(festate->scan_cxt);
+ if (tuplestore_gettupleslot(festate->tuples, true, false, slot))
+ {
+ MemoryContextSwitchTo(oldcontext);
+ return slot;
+ }
+ MemoryContextSwitchTo(oldcontext);
+
+ /*
+ * Here we need to clear partial result and fetch next bunch of tuples from
+ * from the cursor for the scan. If the fetch returns no tuple, the scan
+ * has reached the end.
+ *
+ * PGresult must be released before leaving this function.
+ */
+ PG_TRY();
+ {
+ res = fetch_result(node);
+ store_result(node, res);
+ PQclear(res);
+ res = NULL;
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+
+ /*
+ * If we got more tuples from the server cursor, return next tuple from
+ * tuplestore.
+ */
+ MemoryContextSwitchTo(festate->scan_cxt);
+ if (tuplestore_gettupleslot(festate->tuples, true, false, slot))
+ {
+ MemoryContextSwitchTo(oldcontext);
+ return slot;
+ }
+ MemoryContextSwitchTo(oldcontext);
+
+ /* We don't have any result even in remote server cursor. */
+ ExecClearTuple(slot);
+ return slot;
+ }
+
+ /*
+ * pgsqlReScanForeignScan
+ * - Restart this scan by resetting fetch location.
+ */
+ static void
+ pgsqlReScanForeignScan(ForeignScanState *node)
+ {
+ List *fdw_private;
+ char *sql;
+ PGconn *conn;
+ PGresult *res = NULL;
+ PgsqlFdwExecutionState *festate;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+
+ /* If we have not opened cursor yet, nothing to do. */
+ if (festate->tuples == NULL)
+ return;
+
+ /* Discard fetch results if any. */
+ tuplestore_clear(festate->tuples);
+
+ /* PGresult must be released before leaving this function. */
+ PG_TRY();
+ {
+ /* Reset cursor */
+ fdw_private = festate->fdw_private;
+ conn = festate->conn;
+ sql = strVal(list_nth(fdw_private, FdwPrivateResetSql));
+ res = PQexec(conn, sql);
+ if (PQresultStatus(res) != PGRES_COMMAND_OK)
+ {
+ ereport(ERROR,
+ (errmsg("could not rewind cursor"),
+ errdetail("%s", PQerrorMessage(conn)),
+ errhint("%s", sql)));
+ }
+ PQclear(res);
+ res = NULL;
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+ }
+
+ /*
+ * pgsqlEndForeignScan
+ * Finish scanning foreign table and dispose objects used for this scan
+ */
+ static void
+ pgsqlEndForeignScan(ForeignScanState *node)
+ {
+ List *fdw_private;
+ char *sql;
+ PGconn *conn;
+ PGresult *res = NULL;
+ PgsqlFdwExecutionState *festate;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+
+ /* if festate is NULL, we are in EXPLAIN; nothing to do */
+ if (festate == NULL)
+ return;
+
+ /* If we have not opened cursor yet, nothing to do. */
+ if (festate->tuples == NULL)
+ return;
+
+ /* Discard fetch results */
+ tuplestore_end(festate->tuples);
+ festate->tuples = NULL;
+
+ /* PGresult must be released before leaving this function. */
+ PG_TRY();
+ {
+ /* Close cursor */
+ fdw_private = festate->fdw_private;
+ conn = festate->conn;
+ sql = strVal(list_nth(fdw_private, FdwPrivateCloseSql));
+ res = PQexec(conn, sql);
+ if (PQresultStatus(res) != PGRES_COMMAND_OK)
+ {
+ ereport(ERROR,
+ (errmsg("could not close cursor"),
+ errdetail("%s", PQerrorMessage(conn)),
+ errhint("%s", sql)));
+ }
+ PQclear(res);
+ res = NULL;
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+
+ ReleaseConnection(festate->conn);
+
+ MemoryContextDelete(festate->scan_cxt);
+ festate->scan_cxt = NULL;
+ }
+
+ /*
+ * Adjust costs estimated on remote end with some overheads such as connection
+ * and data transfer.
+ */
+ static void
+ adjust_costs(double rows, int width, Cost *startup_cost, Cost *total_cost)
+ {
+ /*
+ * TODO Selectivity of quals which are NOT pushed down should be also
+ * considered.
+ */
+
+ /* add cost to establish connection. */
+ *startup_cost += CONNECTION_COSTS;
+ *total_cost += CONNECTION_COSTS;
+
+ /* add cost to transfer result. */
+ *total_cost += TRANSFER_COSTS_PER_BYTE * width * rows;
+ *total_cost += cpu_tuple_cost * rows;
+ }
+
+ /*
+ * Execute remote query with current parameters.
+ */
+ static void
+ execute_query(ForeignScanState *node)
+ {
+ List *fdw_private;
+ PgsqlFdwExecutionState *festate;
+ ParamListInfo params = node->ss.ps.state->es_param_list_info;
+ int numParams = params ? params->numParams : 0;
+ Oid *types = NULL;
+ const char **values = NULL;
+ char *sql;
+ PGconn *conn;
+ PGresult *res = NULL;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+ types = festate->param_types;
+ values = festate->param_values;
+
+ /*
+ * Construct parameter array in text format. We don't release memory for
+ * the arrays explicitly, because the memory usage would not be very large,
+ * and anyway they will be released in context cleanup.
+ */
+ if (numParams > 0)
+ {
+ int i;
+
+ 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;
+
+ getTypeOutputInfo(types[i], &out_func_oid, &isvarlena);
+ fmgr_info(out_func_oid, &func);
+ values[i] = OutputFunctionCall(&func, params->params[i].value);
+ }
+ }
+ }
+
+ /* PGresult must be released before leaving this function. */
+ PG_TRY();
+ {
+ /*
+ * Execute remote query with parameters.
+ */
+ conn = festate->conn;
+ fdw_private = ((ForeignScan *) node->ss.ps.plan)->fdw_private;
+ sql = strVal(list_nth(fdw_private, FdwPrivateDeclareSql));
+ res = PQexecParams(conn, sql, numParams, types, values, NULL, NULL, 0);
+
+ /*
+ * If the query has failed, reporting details is enough here.
+ * Connection(s) which are used by this query (at least used by
+ * pgsql_fdw) will be cleaned up by the foreign connection manager.
+ */
+ if (PQresultStatus(res) != PGRES_COMMAND_OK)
+ {
+ ereport(ERROR,
+ (errmsg("could not declare cursor"),
+ errdetail("%s", PQerrorMessage(conn)),
+ errhint("%s", sql)));
+ }
+
+ /* Discard result of DECLARE statement. */
+ PQclear(res);
+ res = NULL;
+
+ /* Fetch first bunch of the result and store them into tuplestore. */
+ res = fetch_result(node);
+ store_result(node, res);
+ PQclear(res);
+ res = NULL;
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+ }
+
+ /*
+ * Fetch next partial result from remote server.
+ *
+ * Once this function has returned result records as PGresult, caller is
+ * responsible to release it, so caller should put codes which might throw
+ * exception in PG_TRY block. When an exception has been caught, release
+ * PGresult and re-throw the exception in PG_CATCH block.
+ */
+ static PGresult *
+ fetch_result(ForeignScanState *node)
+ {
+ PgsqlFdwExecutionState *festate;
+ List *fdw_private;
+ char *sql;
+ PGconn *conn;
+ PGresult *res;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+
+ /* Retrieve information for fetching result. */
+ fdw_private = festate->fdw_private;
+ sql = strVal(list_nth(fdw_private, FdwPrivateFetchSql));
+ conn = festate->conn;
+
+ /*
+ * Fetch result from remote server. In error case, we must release
+ * PGresult in this function to avoid memory leak because caller can't
+ * get the reference.
+ */
+ res = PQexec(conn, sql);
+ if (PQresultStatus(res) != PGRES_TUPLES_OK)
+ {
+ PQclear(res);
+ ereport(ERROR,
+ (errmsg("could not fetch rows from foreign server"),
+ errdetail("%s", PQerrorMessage(conn)),
+ errhint("%s", sql)));
+ }
+
+ return res;
+ }
+
+ /*
+ * Create tuples from PGresult and store them into tuplestore.
+ *
+ * Caller must use PG_TRY block to catch exception and release PGresult
+ * surely.
+ */
+ static void
+ store_result(ForeignScanState *node, PGresult *res)
+ {
+ int rows;
+ int row;
+ int i;
+ int nfields;
+ int attnum; /* number of non-dropped columns */
+ Form_pg_attribute *attrs;
+ TupleTableSlot *slot = node->ss.ss_ScanTupleSlot;
+ TupleDesc tupdesc = slot->tts_tupleDescriptor;
+ PgsqlFdwExecutionState *festate;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+ rows = PQntuples(res);
+ nfields = PQnfields(res);
+ attrs = tupdesc->attrs;
+
+ /* First, ensure that the tuplestore is empty. */
+ if (festate->tuples == NULL)
+ {
+ MemoryContext oldcontext = CurrentMemoryContext;
+
+ /*
+ * Create tuplestore to store result of the query in per-query context.
+ * Note that we use this memory context to avoid memory leak in error
+ * cases.
+ */
+ MemoryContextSwitchTo(festate->scan_cxt);
+ festate->tuples = tuplestore_begin_heap(false, false, work_mem);
+ MemoryContextSwitchTo(oldcontext);
+ }
+ else
+ {
+ /* We already have tuplestore, just need to clear contents of it. */
+ tuplestore_clear(festate->tuples);
+ }
+
+ /* 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)));
+
+ /* put a tuples into the slot */
+ for (row = 0; row < rows; row++)
+ {
+ int j;
+ HeapTuple tuple;
+
+ for (i = 0, j = 0; i < tupdesc->natts; i++)
+ {
+ /* skip dropped columns. */
+ if (attrs[i]->attisdropped)
+ {
+ festate->col_values[i] = NULL;
+ continue;
+ }
+
+ if (PQgetisnull(res, row, j))
+ festate->col_values[i] = NULL;
+ else
+ festate->col_values[i] = PQgetvalue(res, row, 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(festate->attinmeta, festate->col_values);
+ tuplestore_puttuple(festate->tuples, tuple);
+ }
+
+ tuplestore_donestoring(festate->tuples);
+ }
+
diff --git a/contrib/pgsql_fdw/pgsql_fdw.control b/contrib/pgsql_fdw/pgsql_fdw.control
index ...0a9c8f4 .
*** a/contrib/pgsql_fdw/pgsql_fdw.control
--- b/contrib/pgsql_fdw/pgsql_fdw.control
***************
*** 0 ****
--- 1,5 ----
+ # pgsql_fdw extension
+ comment = 'foreign-data wrapper for remote PostgreSQL servers'
+ default_version = '1.0'
+ module_pathname = '$libdir/pgsql_fdw'
+ relocatable = true
diff --git a/contrib/pgsql_fdw/pgsql_fdw.h b/contrib/pgsql_fdw/pgsql_fdw.h
index ...6eb99d5 .
*** a/contrib/pgsql_fdw/pgsql_fdw.h
--- b/contrib/pgsql_fdw/pgsql_fdw.h
***************
*** 0 ****
--- 1,32 ----
+ /*-------------------------------------------------------------------------
+ *
+ * pgsql_fdw.h
+ * foreign-data wrapper for remote PostgreSQL servers.
+ *
+ * Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/pgsql_fdw.h
+ *
+ *-------------------------------------------------------------------------
+ */
+
+ #ifndef PGSQL_FDW_H
+ #define PGSQL_FDW_H
+
+ #include "postgres.h"
+ #include "foreign/foreign.h"
+ #include "nodes/relation.h"
+
+ /* in option.c */
+ int ExtractConnectionOptions(List *defelems,
+ const char **keywords,
+ const char **values);
+
+ /* in deparse.c */
+ char *deparseSimpleSql(Oid relid,
+ PlannerInfo *root,
+ RelOptInfo *baserel,
+ ForeignTable *table);
+
+ #endif /* PGSQL_FDW_H */
diff --git a/contrib/pgsql_fdw/sql/pgsql_fdw.sql b/contrib/pgsql_fdw/sql/pgsql_fdw.sql
index ...b3fac96 .
*** a/contrib/pgsql_fdw/sql/pgsql_fdw.sql
--- b/contrib/pgsql_fdw/sql/pgsql_fdw.sql
***************
*** 0 ****
--- 1,248 ----
+ -- ===================================================================
+ -- create FDW objects
+ -- ===================================================================
+
+ -- Clean up in case a prior regression run failed
+
+ -- Suppress NOTICE messages when roles don't exist
+ SET client_min_messages TO 'error';
+
+ DROP ROLE IF EXISTS pgsql_fdw_user;
+
+ RESET client_min_messages;
+
+ CREATE ROLE pgsql_fdw_user LOGIN SUPERUSER;
+ SET SESSION AUTHORIZATION 'pgsql_fdw_user';
+
+ CREATE EXTENSION pgsql_fdw;
+
+ CREATE SERVER loopback1 FOREIGN DATA WRAPPER pgsql_fdw;
+ CREATE SERVER loopback2 FOREIGN DATA WRAPPER pgsql_fdw
+ OPTIONS (dbname 'contrib_regression');
+
+ CREATE USER MAPPING FOR public SERVER loopback1
+ OPTIONS (user 'value', password 'value');
+ CREATE USER MAPPING FOR pgsql_fdw_user SERVER loopback2;
+
+ CREATE FOREIGN TABLE ft1 (
+ c0 int,
+ c1 int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10)
+ ) SERVER loopback2;
+ ALTER FOREIGN TABLE ft1 DROP COLUMN c0;
+
+ CREATE FOREIGN TABLE ft2 (
+ c0 int,
+ c1 int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10)
+ ) SERVER loopback2;
+ ALTER FOREIGN TABLE ft2 DROP COLUMN c0;
+
+ -- ===================================================================
+ -- create objects used through FDW
+ -- ===================================================================
+ CREATE SCHEMA "S 1";
+ CREATE TABLE "S 1"."T 1" (
+ "C 1" int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10),
+ CONSTRAINT t1_pkey PRIMARY KEY ("C 1")
+ );
+ CREATE TABLE "S 1"."T 2" (
+ c1 int NOT NULL,
+ c2 text,
+ CONSTRAINT t2_pkey PRIMARY KEY (c1)
+ );
+
+ BEGIN;
+ TRUNCATE "S 1"."T 1";
+ INSERT INTO "S 1"."T 1"
+ SELECT id,
+ id % 10,
+ to_char(id, 'FM00000'),
+ '1970-01-01'::timestamptz + ((id % 100) || ' days')::interval,
+ '1970-01-01'::timestamp + ((id % 100) || ' days')::interval,
+ id % 10,
+ id % 10
+ FROM generate_series(1, 1000) id;
+ TRUNCATE "S 1"."T 2";
+ INSERT INTO "S 1"."T 2"
+ SELECT id,
+ 'AAA' || to_char(id, 'FM000')
+ FROM generate_series(1, 100) id;
+ COMMIT;
+
+ -- ===================================================================
+ -- tests for pgsql_fdw_validator
+ -- ===================================================================
+ ALTER FOREIGN DATA WRAPPER pgsql_fdw OPTIONS (host 'value'); -- ERROR
+ -- requiressl, krbsrvname and gsslib are omitted because they depend on
+ -- configure option
+ ALTER SERVER loopback1 OPTIONS (
+ authtype 'value',
+ service 'value',
+ connect_timeout 'value',
+ dbname 'value',
+ host 'value',
+ hostaddr 'value',
+ port 'value',
+ --client_encoding 'value',
+ tty 'value',
+ options 'value',
+ application_name 'value',
+ --fallback_application_name 'value',
+ keepalives 'value',
+ keepalives_idle 'value',
+ keepalives_interval 'value',
+ -- requiressl 'value',
+ sslcompression 'value',
+ sslmode 'value',
+ sslcert 'value',
+ sslkey 'value',
+ sslrootcert 'value',
+ sslcrl 'value'
+ --requirepeer 'value',
+ -- krbsrvname 'value',
+ -- gsslib 'value',
+ --replication 'value'
+ );
+ ALTER SERVER loopback1 OPTIONS (user 'value'); -- ERROR
+ ALTER SERVER loopback2 OPTIONS (ADD fetch_count '2');
+ ALTER USER MAPPING FOR public SERVER loopback1
+ OPTIONS (DROP user, DROP password);
+ ALTER USER MAPPING FOR public SERVER loopback1
+ OPTIONS (host 'value'); -- ERROR
+ ALTER FOREIGN TABLE ft1 OPTIONS (nspname 'S 1', relname 'T 1');
+ ALTER FOREIGN TABLE ft2 OPTIONS (nspname 'S 1', relname 'T 1', fetch_count '100');
+ ALTER FOREIGN TABLE ft1 OPTIONS (invalid 'value'); -- ERROR
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count 'a'); -- ERROR
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count '0'); -- ERROR
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count '-1'); -- ERROR
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (invalid 'value'); -- ERROR
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (colname 'C 1');
+ ALTER FOREIGN TABLE ft2 ALTER COLUMN c1 OPTIONS (colname 'C 1');
+ \dew+
+ \des+
+ \deu+
+ \det+
+
+ -- ===================================================================
+ -- simple queries
+ -- ===================================================================
+ -- single table, with/without alias
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
+ SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+ SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+ -- with WHERE clause
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
+ SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
+ -- 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
+ SELECT * FROM ft1 t1 WHERE t1.c3 = (SELECT MAX(c3) FROM ft2 t2) ORDER BY c1;
+ -- used in CTE
+ WITH t1 AS (SELECT * FROM ft1 WHERE c1 <= 10) SELECT t2.c1, t2.c2, t2.c3, t2.c4 FROM t1, ft2 t2 WHERE t1.c1 = t2.c1 ORDER BY t1.c1;
+ -- fixed values
+ SELECT 'fixed', NULL FROM ft1 t1 WHERE c1 = 1;
+ -- user-defined operator/function
+ CREATE FUNCTION pgsql_fdw_abs(int) RETURNS int AS $$
+ BEGIN
+ RETURN abs($1);
+ END
+ $$ LANGUAGE plpgsql IMMUTABLE;
+ CREATE OPERATOR === (
+ LEFTARG = int,
+ RIGHTARG = int,
+ PROCEDURE = int4eq,
+ COMMUTATOR = ===,
+ NEGATOR = !==
+ );
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = pgsql_fdw_abs(t1.c2);
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 === t1.c2;
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = abs(t1.c2);
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = t1.c2;
+
+ -- ===================================================================
+ -- parameterized queries
+ -- ===================================================================
+ -- simple join
+ PREPARE st1(int, int) AS SELECT t1.c3, t2.c3 FROM ft1 t1, ft2 t2 WHERE t1.c1 = $1 AND t2.c1 = $2;
+ EXPLAIN (COSTS false) EXECUTE st1(1, 2);
+ EXECUTE st1(1, 1);
+ EXECUTE st1(101, 101);
+ -- subquery using stable function (can't be pushed down)
+ PREPARE st2(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c4) = 6) ORDER BY c1;
+ EXPLAIN (COSTS false) EXECUTE st2(10, 20);
+ EXECUTE st2(10, 20);
+ EXECUTE st1(101, 101);
+ -- subquery using immutable function (can be pushed down)
+ PREPARE st3(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c5) = 6) ORDER BY c1;
+ EXPLAIN (COSTS false) EXECUTE st3(10, 20);
+ EXECUTE st3(10, 20);
+ EXECUTE st3(20, 30);
+ -- custom plan should be chosen
+ PREPARE st4(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 = $1;
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ -- cleanup
+ DEALLOCATE st1;
+ DEALLOCATE st2;
+ DEALLOCATE st3;
+ DEALLOCATE st4;
+
+ -- ===================================================================
+ -- connection management
+ -- ===================================================================
+ SELECT srvname, usename FROM pgsql_fdw_connections;
+ SELECT pgsql_fdw_disconnect(srvid, usesysid) FROM pgsql_fdw_get_connections();
+ SELECT srvname, usename FROM pgsql_fdw_connections;
+
+ -- ===================================================================
+ -- subtransaction
+ -- ===================================================================
+ BEGIN;
+ DECLARE c CURSOR FOR SELECT * FROM ft1 ORDER BY c1;
+ FETCH c;
+ SAVEPOINT s;
+ ERROR OUT; -- ERROR
+ ROLLBACK TO s;
+ SELECT srvname FROM pgsql_fdw_connections;
+ FETCH c;
+ COMMIT;
+ SELECT srvname FROM pgsql_fdw_connections;
+ ERROR OUT; -- ERROR
+ SELECT srvname FROM pgsql_fdw_connections;
+
+ -- ===================================================================
+ -- cleanup
+ -- ===================================================================
+ DROP OPERATOR === (int, int) CASCADE;
+ DROP OPERATOR !== (int, int) CASCADE;
+ DROP FUNCTION pgsql_fdw_abs(int);
+ DROP SCHEMA "S 1" CASCADE;
+ DROP EXTENSION pgsql_fdw CASCADE;
+ \c
+ DROP ROLE pgsql_fdw_user;
diff --git a/doc/src/sgml/contrib.sgml b/doc/src/sgml/contrib.sgml
index d4da4ee..32056d1 100644
*** a/doc/src/sgml/contrib.sgml
--- b/doc/src/sgml/contrib.sgml
*************** CREATE EXTENSION <replaceable>module_nam
*** 117,122 ****
--- 117,123 ----
&pgcrypto;
&pgfreespacemap;
&pgrowlocks;
+ &pgsql-fdw;
&pgstandby;
&pgstatstatements;
&pgstattuple;
diff --git a/doc/src/sgml/filelist.sgml b/doc/src/sgml/filelist.sgml
index b5d3c6d..de686ee 100644
*** a/doc/src/sgml/filelist.sgml
--- b/doc/src/sgml/filelist.sgml
***************
*** 125,130 ****
--- 125,131 ----
<!ENTITY pgcrypto SYSTEM "pgcrypto.sgml">
<!ENTITY pgfreespacemap SYSTEM "pgfreespacemap.sgml">
<!ENTITY pgrowlocks SYSTEM "pgrowlocks.sgml">
+ <!ENTITY pgsql-fdw SYSTEM "pgsql-fdw.sgml">
<!ENTITY pgstandby SYSTEM "pgstandby.sgml">
<!ENTITY pgstatstatements SYSTEM "pgstatstatements.sgml">
<!ENTITY pgstattuple SYSTEM "pgstattuple.sgml">
diff --git a/doc/src/sgml/pgsql-fdw.sgml b/doc/src/sgml/pgsql-fdw.sgml
index ...eb69f01 .
*** a/doc/src/sgml/pgsql-fdw.sgml
--- b/doc/src/sgml/pgsql-fdw.sgml
***************
*** 0 ****
--- 1,265 ----
+ <!-- doc/src/sgml/pgsql-fdw.sgml -->
+
+ <sect1 id="pgsql-fdw" xreflabel="pgsql_fdw">
+ <title>pgsql_fdw</title>
+
+ <indexterm zone="pgsql-fdw">
+ <primary>pgsql_fdw</primary>
+ </indexterm>
+
+ <para>
+ The <filename>pgsql_fdw</filename> module provides a foreign-data wrapper for
+ external <productname>PostgreSQL</productname> servers.
+ With this module, users can access data stored in external
+ <productname>PostgreSQL</productname> via plain SQL statements.
+ </para>
+
+ <para>
+ Note that default wrapper <literal>pgsql_fdw</literal> is created
+ automatically during <command>CREATE EXTENSION</command> command for
+ <application>pgsql_fdw</application>.
+ </para>
+
+ <sect2>
+ <title>FDW Options of pgsql_fdw</title>
+
+ <sect3>
+ <title>Connection Options</title>
+ <para>
+ A foreign server and user mapping created using this wrapper can have
+ <application>libpq</> connection options, expect below:
+
+ <itemizedlist>
+ <listitem><para>client_encoding</para></listitem>
+ <listitem><para>fallback_application_name</para></listitem>
+ <listitem><para>replication</para></listitem>
+ </itemizedlist>
+
+ For details of <application>libpq</> connection options, see
+ <xref linkend="libpq-connect">.
+ </para>
+
+ <para>
+ <literal>user</literal> and <literal>password</literal> can be
+ specified on user mappings, and others can be specified on foreign servers.
+ </para>
+ </sect3>
+
+ <sect3>
+ <title>Object Name Options</title>
+ <para>
+ Foreign tables which were created using this wrapper, or its columns can
+ have object name options. These options can be used to specify the names
+ used in SQL statement sent to remote <productname>PostgreSQL</productname>
+ server. These options are useful when a remote object has different name
+ from corresponding local one.
+ </para>
+
+ <variablelist>
+
+ <varlistentry>
+ <term><literal>nspname</literal></term>
+ <listitem>
+ <para>
+ This option, which can be specified on a foreign table, is used as a
+ namespace (schema) reference in the SQL statement. If this options is
+ omitted, <literal>pg_class.nspname</literal> of the foreign table is
+ used.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>relname</literal></term>
+ <listitem>
+ <para>
+ This option, which can be specified on a foreign table, is used as a
+ relation (table) reference in the SQL statement. If this options is
+ omitted, <literal>pg_class.relname</literal> of the foreign table is
+ used.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>colname</literal></term>
+ <listitem>
+ <para>
+ This option, which can be specified on a column of a foreign table, is
+ used as a column (attribute) reference in the SQL statement. If this
+ option is omitted, <literal>pg_attribute.attname</literal> of the column
+ of the foreign table is used.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ </variablelist>
+
+ </sect3>
+
+ <sect3>
+ <title>Cursor Options</title>
+ <para>
+ The <application>pgsql_fdw</application> always uses cursor to retrieve the
+ result from external server. Users can control the behavior of cursor by
+ setting cursor options to foreign table or foreign server. If an option
+ is set to both objects, finer-grained setting is used. In other words,
+ foreign table's setting overrides foreign server's setting.
+ </para>
+
+ <variablelist>
+
+ <varlistentry>
+ <term><literal>fetch_count</literal></term>
+ <listitem>
+ <para>
+ This option specifies the number of rows to be fetched at a time.
+ This option accepts only integer value larger than zero. The default
+ setting is 10000.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ </variablelist>
+
+ </sect3>
+
+ </sect2>
+
+ <sect2>
+ <title>Connection Management</title>
+
+ <para>
+ The <application>pgsql_fdw</application> establishes a connection to a
+ foreign server in the beginning of the first query which uses a foreign
+ table associated to the foreign server, and reuses the connection following
+ queries and even in following foreign scans in same query.
+
+ You can see the list of active connections via
+ <structname>pgsql_fdw_connections</structname> view. It shows pair of oid
+ and name of server and local role for each active connections established by
+ <application>pgsql_fdw</application>. For security reason, only superuser
+ can see other role's connections.
+ </para>
+
+ <para>
+ Established connections are kept alive until local role changes or the
+ current transaction aborts or user requests so.
+ </para>
+
+ <para>
+ If role has been changed, active connections established as old local role
+ is kept alive but never be reused until local role has restored to original
+ role. This kind of situation happens with <command>SET ROLE</command> and
+ <command>SET SESSION AUTHORIZATION</command>.
+ </para>
+
+ <para>
+ If current transaction aborts by error or user request, all active
+ connections are disconnected automatically. This behavior avoids possible
+ connection leaks on error.
+ </para>
+
+ <para>
+ You can discard persistent connection at arbitrary timing with
+ <function>pgsql_fdw_disconnect()</function>. It takes server oid and
+ user oid as arguments. This function can handle only connections
+ established in current session; connections established by other backends
+ are not reachable.
+ </para>
+
+ <para>
+ You can discard all active and visible connections in current session with
+ using <structname>pgsql_fdw_connections</structname> and
+ <function>pgsql_fdw_disconnect()</function> together:
+ <synopsis>
+ postgres=# SELECT pgsql_fdw_disconnect(srvid, usesysid) FROM pgsql_fdw_connections;
+ pgsql_fdw_disconnect
+ ----------------------
+ OK
+ OK
+ (2 rows)
+ </synopsis>
+ </para>
+ </sect2>
+
+ <sect2>
+ <title>Transaction Management</title>
+ <para>
+ The <application>pgsql_fdw</application> begins remote transaction at the
+ beginning of a local query, and terminates it with <command>ABORT</command>
+ at the end of the local query. This means that all foreign scans on a
+ foreign server in a local query are executed in one transaction.
+ If isolation level of local transaction is <literal>SERIALIZABLE</literal>,
+ <literal>SERIALIZABLE</literal> is used for remote transaction. Otherwise,
+ if isolation level of local transaction is one of
+ <literal>READ UNCOMMITTED</literal>, <literal>READ COMMITTED</literal> or
+ <literal>REPEATABLE READ</literal>, then <literal>REPEATABLE READ</literal>
+ is used for remote transaction.
+ <literal>READ UNCOMMITTED</literal> and <literal>READ COMMITTED</literal>
+ are never used for remote transaction, because even
+ <literal>READ COMMITTED</literal> transaction might produce inconsistent
+ results, if remote data have been updated between two remote queries.
+ </para>
+ <para>
+ Note that even if the isolation level of local transaction was
+ <literal>SERIALIZABLE</literal> or <literal>REPEATABLE READ</literal>,
+ series of one query might produce different result, because foreign scans
+ in different local queries are executed in different remote transactions.
+ For instance, when client started a local transaction
+ explicitly with isolation level <literal>SERIALIZABLE</literal>, and
+ executed same local query which contains a foreign table which references
+ foreign data which is updated frequently, latter result would be different
+ from former result.
+ </para>
+ <para>
+ This restriction might be relaxed in future release.
+ </para>
+ </sect2>
+
+ <sect2>
+ <title>Estimation of Costs and Rows</title>
+ <para>
+ The <application>pgsql_fdw</application> estimates the costs of a foreign
+ scan by adding up some basic costs: connection costs, remote query costs
+ and data transfer costs.
+ To get remote query costs, <application>pgsql_fdw</application> executes
+ <command>EXPLAIN</command> command on remote server for each foreign scan.
+ </para>
+ <para>
+ On the other hand, estimated rows which was returned by
+ <command>EXPLAIN</command> is used for local estimation as-is.
+ </para>
+ </sect2>
+
+ <sect2>
+ <title>EXPLAIN Output</title>
+ <para>
+ For a foreign table using <literal>pgsql_fdw</>, <command>EXPLAIN</> shows
+ a remote SQL statement which is sent to remote
+ <productname>PostgreSQL</productname> server for a ForeignScan plan node.
+ For example:
+ </para>
+ <synopsis>
+ postgres=# EXPLAIN SELECT aid FROM pgbench_accounts WHERE abalance < 0;
+ QUERY PLAN
+ --------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on pgbench_accounts (cost=100.00..8105.13 rows=302613 width=8)
+ Filter: (abalance < 0)
+ Remote SQL: SELECT aid, NULL, abalance, NULL FROM public.pgbench_accounts
+ (3 rows)
+ </synopsis>
+ <para>
+ When you specify <literal>VERBOSE</> option, you can see actual DECLARE
+ statement with cursor name which is used for the scan.
+ </para>
+ </sect2>
+
+ <sect2>
+ <title>Author</title>
+ <para>
+ Shigeru Hanada <email>shigeru.hanada@gmail.com</email>
+ </para>
+ </sect2>
+
+ </sect1>
pgsql_fdw_pushdown_v10.patchtext/plain; charset=Shift_JIS; name=pgsql_fdw_pushdown_v10.patchDownload
diff --git a/contrib/pgsql_fdw/deparse.c b/contrib/pgsql_fdw/deparse.c
index ca3356d..e7a6303 100644
*** a/contrib/pgsql_fdw/deparse.c
--- b/contrib/pgsql_fdw/deparse.c
***************
*** 14,19 ****
--- 14,21 ----
#include "access/transam.h"
#include "catalog/pg_class.h"
+ #include "catalog/pg_operator.h"
+ #include "catalog/pg_type.h"
#include "commands/defrem.h"
#include "foreign/foreign.h"
#include "lib/stringinfo.h"
***************
*** 22,30 ****
--- 24,34 ----
#include "nodes/makefuncs.h"
#include "optimizer/clauses.h"
#include "optimizer/var.h"
+ #include "parser/parser.h"
#include "parser/parsetree.h"
#include "utils/builtins.h"
#include "utils/lsyscache.h"
+ #include "utils/syscache.h"
#include "pgsql_fdw.h"
*************** static void deparseRelation(StringInfo b
*** 44,49 ****
--- 48,74 ----
bool need_prefix);
static void deparseVar(StringInfo buf, Var *node, PlannerInfo *root,
bool need_prefix);
+ static void deparseConst(StringInfo buf, Const *node, PlannerInfo *root);
+ static void deparseBoolExpr(StringInfo buf, BoolExpr *node, PlannerInfo *root);
+ static void deparseNullTest(StringInfo buf, NullTest *node, PlannerInfo *root);
+ static void deparseDistinctExpr(StringInfo buf, DistinctExpr *node,
+ PlannerInfo *root);
+ static void deparseRelabelType(StringInfo buf, RelabelType *node,
+ PlannerInfo *root);
+ static void deparseFuncExpr(StringInfo buf, FuncExpr *node, PlannerInfo *root);
+ static void deparseParam(StringInfo buf, Param *node, PlannerInfo *root);
+ static void deparseScalarArrayOpExpr(StringInfo buf, ScalarArrayOpExpr *node,
+ PlannerInfo *root);
+ static void deparseOpExpr(StringInfo buf, OpExpr *node, PlannerInfo *root);
+ static void deparseArrayRef(StringInfo buf, ArrayRef *node, PlannerInfo *root);
+ static void deparseArrayExpr(StringInfo buf, ArrayExpr *node, PlannerInfo *root);
+
+ /*
+ * Determine whether an expression can be evaluated on remote side safely.
+ */
+ static bool is_foreign_expr(PlannerInfo *root, RelOptInfo *baserel, Expr *expr);
+ static bool foreign_expr_walker(Node *node, foreign_executable_cxt *context);
+ static bool is_builtin(Oid procid);
/*
* Deparse query representation into SQL statement which suits for remote
*************** deparseSimpleSql(Oid relid,
*** 152,157 ****
--- 177,273 ----
}
/*
+ * Examine baserestrictinfo of baserel, and extract expressions which can be
+ * evaluated on remote side safely. This function never changes
+ * baserestrictinfo.
+ */
+ List *
+ extractRemoteExprs(PlannerInfo *root,
+ RelOptInfo *baserel,
+ List **local_exprs)
+ {
+ ListCell *lc;
+ List *exprs = NIL;
+
+ foreach(lc, baserel->baserestrictinfo)
+ {
+ RestrictInfo *ri = (RestrictInfo *) lfirst(lc);
+
+ if (is_foreign_expr(root, baserel, ri->clause))
+ exprs = lappend(exprs, ri->clause);
+ else
+ {
+ if (local_exprs != NULL)
+ *local_exprs = lappend(*local_exprs, ri->clause);
+ }
+
+ }
+
+ return exprs;
+ }
+
+ /*
+ * Deparse given expression into buf. Actual string operation is delegated to
+ * node-type-specific functions.
+ *
+ * Note that switch statement of this function MUST match the one in
+ * foreign_expr_walker to avoid unsupported error..
+ */
+ void
+ deparseExpr(StringInfo buf, Expr *node, PlannerInfo *root)
+ {
+ /*
+ * This part must be match foreign_expr_walker.
+ */
+ switch (nodeTag(node))
+ {
+ case T_Const:
+ deparseConst(buf, (Const *) node, root);
+ break;
+ case T_BoolExpr:
+ deparseBoolExpr(buf, (BoolExpr *) node, root);
+ break;
+ case T_NullTest:
+ deparseNullTest(buf, (NullTest *) node, root);
+ break;
+ case T_DistinctExpr:
+ deparseDistinctExpr(buf, (DistinctExpr *) node, root);
+ break;
+ case T_RelabelType:
+ deparseRelabelType(buf, (RelabelType *) node, root);
+ break;
+ case T_FuncExpr:
+ deparseFuncExpr(buf, (FuncExpr *) node, root);
+ break;
+ case T_Param:
+ deparseParam(buf, (Param *) node, root);
+ break;
+ case T_ScalarArrayOpExpr:
+ deparseScalarArrayOpExpr(buf, (ScalarArrayOpExpr *) node, root);
+ break;
+ case T_OpExpr:
+ deparseOpExpr(buf, (OpExpr *) node, root);
+ break;
+ case T_Var:
+ deparseVar(buf, (Var *) node, root, false);
+ break;
+ case T_ArrayRef:
+ deparseArrayRef(buf, (ArrayRef *) node, root);
+ break;
+ case T_ArrayExpr:
+ deparseArrayExpr(buf, (ArrayExpr *) node, root);
+ break;
+ default:
+ {
+ ereport(ERROR,
+ (errmsg("unsupported expression for deparse"),
+ errdetail("%s", nodeToString(node))));
+ }
+ break;
+ }
+ }
+
+ /*
* Deparse node into buf, with relation qualifier if need_prefix was true. If
* node is a column of a foreign table, use value of colname FDW option (if any)
* instead of attribute name.
*************** deparseRelation(StringInfo buf,
*** 287,289 ****
--- 403,1093 ----
appendStringInfo(buf, "%s.", q_nspname);
appendStringInfo(buf, "%s", q_relname);
}
+
+ /*
+ * Deparse given constant value into buf. This function have to be kept in
+ * sync with get_const_expr.
+ */
+ static void
+ deparseConst(StringInfo buf,
+ Const *node,
+ PlannerInfo *root)
+ {
+ Oid typoutput;
+ bool typIsVarlena;
+ char *extval;
+ bool isfloat = false;
+ bool needlabel;
+
+ if (node->constisnull)
+ {
+ appendStringInfo(buf, "NULL");
+ return;
+ }
+
+ getTypeOutputInfo(node->consttype,
+ &typoutput, &typIsVarlena);
+ extval = OidOutputFunctionCall(typoutput, node->constvalue);
+
+ switch (node->consttype)
+ {
+ case ANYARRAYOID:
+ case ANYNONARRAYOID:
+ elog(ERROR, "anyarray and anyenum are not supported");
+ break;
+ case INT2OID:
+ case INT4OID:
+ case INT8OID:
+ case OIDOID:
+ case FLOAT4OID:
+ case FLOAT8OID:
+ case NUMERICOID:
+ {
+ /*
+ * No need to quote unless they contain special values such as
+ * 'Nan'.
+ */
+ if (strspn(extval, "0123456789+-eE.") == strlen(extval))
+ {
+ if (extval[0] == '+' || extval[0] == '-')
+ appendStringInfo(buf, "(%s)", extval);
+ else
+ appendStringInfoString(buf, extval);
+ if (strcspn(extval, "eE.") != strlen(extval))
+ isfloat = true; /* it looks like a float */
+ }
+ else
+ appendStringInfo(buf, "'%s'", extval);
+ }
+ break;
+ case BITOID:
+ case VARBITOID:
+ appendStringInfo(buf, "B'%s'", extval);
+ break;
+ case BOOLOID:
+ if (strcmp(extval, "t") == 0)
+ appendStringInfoString(buf, "true");
+ else
+ appendStringInfoString(buf, "false");
+ break;
+
+ default:
+ {
+ const char *valptr;
+
+ appendStringInfoChar(buf, '\'');
+ for (valptr = extval; *valptr; valptr++)
+ {
+ char ch = *valptr;
+
+ /*
+ * standard_conforming_strings of remote session should be
+ * set to similar value as local session.
+ */
+ if (SQL_STR_DOUBLE(ch, !standard_conforming_strings))
+ appendStringInfoChar(buf, ch);
+ appendStringInfoChar(buf, ch);
+ }
+ appendStringInfoChar(buf, '\'');
+ }
+ break;
+ }
+
+ /*
+ * Append ::typename unless the constant will be implicitly typed as the
+ * right type when it is read in.
+ *
+ * XXX this code has to be kept in sync with the behavior of the parser,
+ * especially make_const.
+ */
+ switch (node->consttype)
+ {
+ case BOOLOID:
+ case INT4OID:
+ case UNKNOWNOID:
+ needlabel = false;
+ break;
+ case NUMERICOID:
+ needlabel = !isfloat || (node->consttypmod >= 0);
+ break;
+ default:
+ needlabel = true;
+ break;
+ }
+ if (needlabel)
+ {
+ appendStringInfo(buf, "::%s",
+ format_type_with_typemod(node->consttype,
+ node->consttypmod));
+ }
+ }
+
+ static void
+ deparseBoolExpr(StringInfo buf,
+ BoolExpr *node,
+ PlannerInfo *root)
+ {
+ ListCell *lc;
+ char *op;
+ bool first;
+
+ switch (node->boolop)
+ {
+ case AND_EXPR:
+ op = "AND";
+ break;
+ case OR_EXPR:
+ op = "OR";
+ break;
+ case NOT_EXPR:
+ appendStringInfo(buf, "(NOT ");
+ deparseExpr(buf, list_nth(node->args, 0), root);
+ appendStringInfo(buf, ")");
+ return;
+ }
+
+ first = true;
+ appendStringInfo(buf, "(");
+ foreach(lc, node->args)
+ {
+ if (!first)
+ appendStringInfo(buf, " %s ", op);
+ deparseExpr(buf, (Expr *) lfirst(lc), root);
+ first = false;
+ }
+ appendStringInfo(buf, ")");
+ }
+
+ /*
+ * Deparse given IS [NOT] NULL test expression into buf.
+ */
+ static void
+ deparseNullTest(StringInfo buf,
+ NullTest *node,
+ PlannerInfo *root)
+ {
+ appendStringInfoChar(buf, '(');
+ deparseExpr(buf, node->arg, root);
+ if (node->nulltesttype == IS_NULL)
+ appendStringInfo(buf, " IS NULL)");
+ else
+ appendStringInfo(buf, " IS NOT NULL)");
+ }
+
+ static void
+ deparseDistinctExpr(StringInfo buf,
+ DistinctExpr *node,
+ PlannerInfo *root)
+ {
+ Assert(list_length(node->args) == 2);
+
+ deparseExpr(buf, linitial(node->args), root);
+ appendStringInfo(buf, " IS DISTINCT FROM ");
+ deparseExpr(buf, lsecond(node->args), root);
+ }
+
+ static void
+ deparseRelabelType(StringInfo buf,
+ RelabelType *node,
+ PlannerInfo *root)
+ {
+ char *typname;
+
+ Assert(node->arg);
+
+ /* We don't need to deparse cast when argument has same type as result. */
+ if (IsA(node->arg, Const) &&
+ ((Const *) node->arg)->consttype == node->resulttype &&
+ ((Const *) node->arg)->consttypmod == -1)
+ {
+ deparseExpr(buf, node->arg, root);
+ return;
+ }
+
+ typname = format_type_with_typemod(node->resulttype, node->resulttypmod);
+ appendStringInfoChar(buf, '(');
+ deparseExpr(buf, node->arg, root);
+ appendStringInfo(buf, ")::%s", typname);
+ }
+
+ /*
+ * Deparse given node which represents a function call into buf. We treat only
+ * explicit function call and explicit cast (coerce), because others are
+ * processed on remote side if necessary.
+ *
+ * Function name (and type name) is always qualified by schema name to avoid
+ * problems caused by different setting of search_path on remote side.
+ */
+ static void
+ deparseFuncExpr(StringInfo buf,
+ FuncExpr *node,
+ PlannerInfo *root)
+ {
+ Oid pronamespace;
+ const char *schemaname;
+ const char *funcname;
+ ListCell *arg;
+ bool first;
+
+ pronamespace = get_func_namespace(node->funcid);
+ schemaname = quote_identifier(get_namespace_name(pronamespace));
+ funcname = quote_identifier(get_func_name(node->funcid));
+
+ if (node->funcformat == COERCE_EXPLICIT_CALL)
+ {
+ /* Function call, deparse all arguments recursively. */
+ appendStringInfo(buf, "%s.%s(", schemaname, funcname);
+ first = true;
+ foreach(arg, node->args)
+ {
+ if (!first)
+ appendStringInfo(buf, ", ");
+ deparseExpr(buf, lfirst(arg), root);
+ first = false;
+ }
+ appendStringInfoChar(buf, ')');
+ }
+ else if (node->funcformat == COERCE_EXPLICIT_CAST)
+ {
+ /* Explicit cast, deparse only first argument. */
+ appendStringInfoChar(buf, '(');
+ deparseExpr(buf, linitial(node->args), root);
+ appendStringInfo(buf, ")::%s", funcname);
+ }
+ else
+ {
+ /* Implicit cast, deparse only first argument. */
+ deparseExpr(buf, linitial(node->args), root);
+ }
+ }
+
+ /*
+ * Deparse given Param node into buf.
+ *
+ * We don't renumber parameter id, because skipping $1 is not cause problem
+ * as far as we pass through all arguments.
+ */
+ static void
+ deparseParam(StringInfo buf,
+ Param *node,
+ PlannerInfo *root)
+ {
+ Assert(node->paramkind == PARAM_EXTERN);
+
+ appendStringInfo(buf, "$%d", node->paramid);
+ }
+
+ /*
+ * Deparse given ScalarArrayOpExpr expression into buf. To avoid problems
+ * around priority of operations, we always parenthesize the arguments. Also we
+ * use OPERATOR(schema.operator) notation to determine remote operator exactly.
+ */
+ static void
+ deparseScalarArrayOpExpr(StringInfo buf,
+ ScalarArrayOpExpr *node,
+ PlannerInfo *root)
+ {
+ HeapTuple tuple;
+ Form_pg_operator form;
+ const char *opnspname;
+ char *opname;
+ Expr *arg1;
+ Expr *arg2;
+
+ /* Retrieve necessary information about the operator from system catalog. */
+ tuple = SearchSysCache1(OPEROID, ObjectIdGetDatum(node->opno));
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "cache lookup failed for operator %u", node->opno);
+ form = (Form_pg_operator) GETSTRUCT(tuple);
+ /* opname is not a SQL identifier, so we don't need to quote it. */
+ opname = NameStr(form->oprname);
+ opnspname = quote_identifier(get_namespace_name(form->oprnamespace));
+ ReleaseSysCache(tuple);
+
+ /* Sanity check. */
+ Assert(list_length(node->args) == 2);
+
+ /* Always parenthesize the expression. */
+ appendStringInfoChar(buf, '(');
+
+ /* Extract operands. */
+ arg1 = linitial(node->args);
+ arg2 = lsecond(node->args);
+
+ /* Deparse fully qualified operator name. */
+ deparseExpr(buf, arg1, root);
+ appendStringInfo(buf, " OPERATOR(%s.%s) %s (",
+ opnspname, opname, node->useOr ? "ANY" : "ALL");
+ deparseExpr(buf, arg2, root);
+ appendStringInfoChar(buf, ')');
+
+ /* Always parenthesize the expression. */
+ appendStringInfoChar(buf, ')');
+ }
+
+ /*
+ * Deparse given operator expression into buf. To avoid problems around
+ * priority of operations, we always parenthesize the arguments. Also we use
+ * OPERATOR(schema.operator) notation to determine remote operator exactly.
+ */
+ static void
+ deparseOpExpr(StringInfo buf,
+ OpExpr *node,
+ PlannerInfo *root)
+ {
+ HeapTuple tuple;
+ Form_pg_operator form;
+ const char *opnspname;
+ char *opname;
+ char oprkind;
+ ListCell *arg;
+
+ /* Retrieve necessary information about the operator from system catalog. */
+ tuple = SearchSysCache1(OPEROID, ObjectIdGetDatum(node->opno));
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "cache lookup failed for operator %u", node->opno);
+ form = (Form_pg_operator) GETSTRUCT(tuple);
+ opnspname = quote_identifier(get_namespace_name(form->oprnamespace));
+ /* opname is not a SQL identifier, so we don't need to quote it. */
+ opname = NameStr(form->oprname);
+ oprkind = form->oprkind;
+ ReleaseSysCache(tuple);
+
+ /* Sanity check. */
+ Assert((oprkind == 'r' && list_length(node->args) == 1) ||
+ (oprkind == 'l' && list_length(node->args) == 1) ||
+ (oprkind == 'b' && list_length(node->args) == 2));
+
+ /* Always parenthesize the expression. */
+ appendStringInfoChar(buf, '(');
+
+ /* Deparse first operand. */
+ arg = list_head(node->args);
+ if (oprkind == 'r' || oprkind == 'b')
+ {
+ deparseExpr(buf, lfirst(arg), root);
+ appendStringInfoChar(buf, ' ');
+ }
+
+ /* Deparse fully qualified operator name. */
+ appendStringInfo(buf, "OPERATOR(%s.%s)", opnspname, opname);
+
+ /* Deparse last operand. */
+ arg = list_tail(node->args);
+ if (oprkind == 'l' || oprkind == 'b')
+ {
+ appendStringInfoChar(buf, ' ');
+ deparseExpr(buf, lfirst(arg), root);
+ }
+
+ appendStringInfoChar(buf, ')');
+ }
+
+ static void
+ deparseArrayRef(StringInfo buf,
+ ArrayRef *node,
+ PlannerInfo *root)
+ {
+ ListCell *lowlist_item;
+ ListCell *uplist_item;
+
+ /* Always parenthesize the expression. */
+ appendStringInfoChar(buf, '(');
+
+ /* Deparse referenced array expression first. */
+ appendStringInfoChar(buf, '(');
+ deparseExpr(buf, node->refexpr, root);
+ appendStringInfoChar(buf, ')');
+
+ /* Deparse subscripts expression. */
+ lowlist_item = list_head(node->reflowerindexpr); /* could be NULL */
+ foreach(uplist_item, node->refupperindexpr)
+ {
+ appendStringInfoChar(buf, '[');
+ if (lowlist_item)
+ {
+ deparseExpr(buf, lfirst(lowlist_item), root);
+ appendStringInfoChar(buf, ':');
+ lowlist_item = lnext(lowlist_item);
+ }
+ deparseExpr(buf, lfirst(uplist_item), root);
+ appendStringInfoChar(buf, ']');
+ }
+
+ appendStringInfoChar(buf, ')');
+ }
+
+
+ /*
+ * Deparse given array of something into buf.
+ */
+ static void
+ deparseArrayExpr(StringInfo buf,
+ ArrayExpr *node,
+ PlannerInfo *root)
+ {
+ ListCell *lc;
+ bool first = true;
+
+ appendStringInfo(buf, "ARRAY[");
+ foreach(lc, node->elements)
+ {
+ if (!first)
+ appendStringInfo(buf, ", ");
+ deparseExpr(buf, lfirst(lc), root);
+
+ first = false;
+ }
+ appendStringInfoChar(buf, ']');
+
+ /* If the array is empty, we need explicit cast to the array type. */
+ if (node->elements == NIL)
+ {
+ char *typname;
+
+ typname = format_type_with_typemod(node->array_typeid, -1);
+ appendStringInfo(buf, "::%s", typname);
+ }
+ }
+
+ /*
+ * Returns true if expr is safe to be evaluated on the foreign server.
+ */
+ static bool
+ is_foreign_expr(PlannerInfo *root, RelOptInfo *baserel, Expr *expr)
+ {
+ foreign_executable_cxt context;
+ context.root = root;
+ context.foreignrel = baserel;
+
+ /*
+ * An expression which includes any mutable function can't be pushed down
+ * because it's result is not stable. For example, pushing now() down to
+ * remote side would cause confusion from the clock offset.
+ * If we have routine mapping infrastructure in future release, we will be
+ * able to choose function to be pushed down in finer granularity.
+ */
+ if (contain_mutable_functions((Node *) expr))
+ return false;
+
+ /*
+ * Check that the expression consists of nodes which are known as safe to
+ * be pushed down.
+ */
+ if (foreign_expr_walker((Node *) expr, &context))
+ return false;
+
+ return true;
+ }
+
+ /*
+ * Return true if node includes any node which is not known as safe to be
+ * pushed down.
+ */
+ static bool
+ foreign_expr_walker(Node *node, foreign_executable_cxt *context)
+ {
+ if (node == NULL)
+ return false;
+
+ /*
+ * Special case handling for List; expression_tree_walker handles List as
+ * well as other Expr nodes. For instance, List is used in RestrictInfo
+ * for args of FuncExpr node.
+ *
+ * Although the comments of expression_tree_walker mention that
+ * RangeTblRef, FromExpr, JoinExpr, and SetOperationStmt are handled as
+ * well, but we don't care them because they are not used in RestrictInfo.
+ * If one of them was passed into, default label catches it and give up
+ * traversing.
+ */
+ if (IsA(node, List))
+ {
+ ListCell *lc;
+
+ foreach(lc, (List *) node)
+ {
+ if (foreign_expr_walker(lfirst(lc), context))
+ return true;
+ }
+ return false;
+ }
+
+ /*
+ * If given expression has valid collation, it can't be pushed down because
+ * it might has incompatible semantics on remote side.
+ */
+ if (exprCollation(node) != InvalidOid ||
+ exprInputCollation(node) != InvalidOid)
+ return true;
+
+ /*
+ * If return type of given expression is not built-in, it can't be pushed
+ * down because it might has incompatible semantics on remote side.
+ */
+ if (!is_builtin(exprType(node)))
+ return true;
+
+ switch (nodeTag(node))
+ {
+ case T_Const:
+ /*
+ * Using anyarray and/or anyenum in remote query is not supported.
+ */
+ if (((Const *) node)->consttype == ANYARRAYOID ||
+ ((Const *) node)->consttype == ANYNONARRAYOID)
+ return true;
+ break;
+ case T_BoolExpr:
+ case T_NullTest:
+ case T_DistinctExpr:
+ case T_RelabelType:
+ /*
+ * 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 at the tail of this function.
+ */
+ break;
+ /*
+ * If function used by the expression is not built-in, it can't be
+ * pushed down because it might has incompatible semantics on remote
+ * side.
+ */
+ case T_FuncExpr:
+ {
+ FuncExpr *fe = (FuncExpr *) node;
+ if (!is_builtin(fe->funcid))
+ return true;
+ }
+ break;
+ case T_Param:
+ /*
+ * Only external parameters can be pushed down.:
+ */
+ {
+ if (((Param *) node)->paramkind != PARAM_EXTERN)
+ return true;
+ }
+ break;
+ case T_ScalarArrayOpExpr:
+ /*
+ * Only built-in operators can be pushed down. In addition,
+ * underlying function must be built-in and immutable, but we don't
+ * check volatility here; such check must be done already with
+ * contain_mutable_functions.
+ */
+ {
+ ScalarArrayOpExpr *oe = (ScalarArrayOpExpr *) node;
+
+ if (!is_builtin(oe->opno) || !is_builtin(oe->opfuncid))
+ return true;
+
+ /* operands are checked later */
+ }
+ break;
+ case T_OpExpr:
+ /*
+ * Only built-in operators can be pushed down. In addition,
+ * underlying function must be built-in and immutable, but we don't
+ * check volatility here; such check must be done already with
+ * contain_mutable_functions.
+ */
+ {
+ OpExpr *oe = (OpExpr *) node;
+
+ if (!is_builtin(oe->opno) || !is_builtin(oe->opfuncid))
+ return true;
+
+ /* operands are checked later */
+ }
+ 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;
+ foreign_executable_cxt *f_context;
+
+ f_context = (foreign_executable_cxt *) context;
+ if (var->varno != f_context->foreignrel->relid ||
+ var->varlevelsup != 0)
+ return true;
+ }
+ break;
+ case T_ArrayRef:
+ /*
+ * ArrayRef which holds non-built-in typed elements can't be pushed
+ * down.
+ */
+ {
+ ArrayRef *ar = (ArrayRef *) node;;
+
+ if (!is_builtin(ar->refelemtype))
+ return true;
+
+ /* Assignment should not be in restrictions. */
+ if (ar->refassgnexpr != NULL)
+ return true;
+ }
+ break;
+ case T_ArrayExpr:
+ /*
+ * ArrayExpr which holds non-built-in typed elements can't be pushed
+ * down.
+ */
+ {
+ if (!is_builtin(((ArrayExpr *) node)->element_typeid))
+ return true;
+ }
+ break;
+ default:
+ {
+ ereport(DEBUG3,
+ (errmsg("expression is too complex"),
+ errdetail("%s", nodeToString(node))));
+ return true;
+ }
+ break;
+ }
+
+ return expression_tree_walker(node, foreign_expr_walker, context);
+ }
+
+ /*
+ * Return true if given object is one of built-in objects.
+ */
+ static bool
+ is_builtin(Oid oid)
+ {
+ return (oid < FirstNormalObjectId);
+ }
+
+ /*
+ * Deparse exprs to WHERE clause string and append it into buf.
+ *
+ * Each element in exprs must be a boolean expression.
+ */
+ void
+ deparseWhereClause(StringInfo buf,
+ List *exprs,
+ PlannerInfo *root)
+ {
+ ListCell *lc;
+ bool first = true;
+
+ appendStringInfo(buf, " WHERE (");
+ foreach(lc, exprs)
+ {
+ Expr *expr = (Expr *) lfirst(lc);
+
+ /* Connect expressions with "AND". */
+ if (!first)
+ appendStringInfo(buf, ") AND (");
+
+ deparseExpr(buf, expr, root);
+ first = false;
+ }
+ appendStringInfoChar(buf, ')');
+ }
diff --git a/contrib/pgsql_fdw/expected/pgsql_fdw.out b/contrib/pgsql_fdw/expected/pgsql_fdw.out
index fdd908c..5560931 100644
*** a/contrib/pgsql_fdw/expected/pgsql_fdw.out
--- b/contrib/pgsql_fdw/expected/pgsql_fdw.out
*************** SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.
*** 230,241 ****
-- with WHERE clause
EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
! QUERY PLAN
! ------------------------------------------------------------------------------------------------------------------
Foreign Scan on public.ft1 t1
Output: c1, c2, c3, c4, c5, c6, c7
! Filter: ((t1.c1 = 101) AND ((t1.c6)::text = '1'::text) AND (t1.c7 = '1'::bpchar))
! Remote SQL: DECLARE pgsql_fdw_cursor_4 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
(4 rows)
SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
--- 230,241 ----
-- with WHERE clause
EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
! QUERY PLAN
! -------------------------------------------------------------------------------------------------------------------------------------------------------------
Foreign Scan on public.ft1 t1
Output: c1, c2, c3, c4, c5, c6, c7
! Filter: (((t1.c6)::text = '1'::text) AND (t1.c7 = '1'::bpchar))
! Remote SQL: DECLARE pgsql_fdw_cursor_4 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.=) 101))
(4 rows)
SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
*************** EXPLAIN (COSTS false) SELECT * FROM ft1
*** 343,362 ****
(3 rows)
EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = abs(t1.c2);
! QUERY PLAN
! ---------------------------------------------------------------------
Foreign Scan on ft1 t1
! Filter: (c1 = abs(c2))
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
! (3 rows)
EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = t1.c2;
! QUERY PLAN
! ---------------------------------------------------------------------
Foreign Scan on ft1 t1
! Filter: (c1 = c2)
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
! (3 rows)
-- ===================================================================
-- parameterized queries
--- 343,433 ----
(3 rows)
EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = abs(t1.c2);
! QUERY PLAN
! -------------------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.=) pg_catalog.abs(c2)))
! (2 rows)
EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = t1.c2;
! QUERY PLAN
! ---------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.=) c2))
! (2 rows)
!
! -- ===================================================================
! -- WHERE push down
! -- ===================================================================
! EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = 1; -- Var, OpExpr(b), Const
! QUERY PLAN
! --------------------------------------------------------------------------------------------------------------
! Foreign Scan on ft1 t1
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.=) 1))
! (2 rows)
!
! EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = 100 AND t1.c2 = 0; -- BoolExpr
! QUERY PLAN
! ----------------------------------------------------------------------------------------------------------------------------------------------------
! Foreign Scan on ft1 t1
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.=) 100)) AND ((c2 OPERATOR(pg_catalog.=) 0))
! (2 rows)
!
! EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE c1 IS NULL; -- NullTest
! QUERY PLAN
! ---------------------------------------------------------------------------------------------
! Foreign Scan on ft1 t1
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" IS NULL))
! (2 rows)
!
! EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE c1 IS NOT NULL; -- NullTest
! QUERY PLAN
! -------------------------------------------------------------------------------------------------
! Foreign Scan on ft1 t1
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" IS NOT NULL))
! (2 rows)
!
! EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE round(abs(c1), 0) = 1; -- FuncExpr
! QUERY PLAN
! ------------------------------------------------------------------------------------------------------------------------------------------------------------
! Foreign Scan on ft1 t1
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE ((pg_catalog.round(pg_catalog.abs("C 1"), 0) OPERATOR(pg_catalog.=) 1::numeric))
! (2 rows)
!
! EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE c1 = -c1; -- OpExpr(l)
! QUERY PLAN
! -------------------------------------------------------------------------------------------------------------------------------------------
! Foreign Scan on ft1 t1
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.=) (OPERATOR(pg_catalog.-) "C 1")))
! (2 rows)
!
! EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE 1 = c1!; -- OpExpr(r)
! QUERY PLAN
! ------------------------------------------------------------------------------------------------------------------------------------------------
! Foreign Scan on ft1 t1
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE ((1::numeric OPERATOR(pg_catalog.=) ("C 1" OPERATOR(pg_catalog.!))))
! (2 rows)
!
! EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE (c1 IS NOT NULL) IS DISTINCT FROM (c1 IS NOT NULL); -- DistinctExpr
! QUERY PLAN
! --------------------------------------------------------------------------------------------------------------------------------------
! Foreign Scan on ft1 t1
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" IS NOT NULL) IS DISTINCT FROM ("C 1" IS NOT NULL))
! (2 rows)
!
! EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE c1 = ANY(ARRAY[c2, 1, c1 + 0]); -- ScalarArrayOpExpr
! QUERY PLAN
! -----------------------------------------------------------------------------------------------------------------------------------------------------------------
! Foreign Scan on ft1 t1
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.=) ANY (ARRAY[c2, 1, ("C 1" OPERATOR(pg_catalog.+) 0)])))
! (2 rows)
!
! EXPLAIN (COSTS false) SELECT * FROM ft1 ft WHERE c1 = (ARRAY[c1,c2,3])[1]; -- ArrayRef
! QUERY PLAN
! ---------------------------------------------------------------------------------------------------------------------------------------
! Foreign Scan on ft1 ft
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.=) ((ARRAY["C 1", c2, 3])[1])))
! (2 rows)
-- ===================================================================
-- parameterized queries
*************** EXPLAIN (COSTS false) SELECT * FROM ft1
*** 364,379 ****
-- simple join
PREPARE st1(int, int) AS SELECT t1.c3, t2.c3 FROM ft1 t1, ft2 t2 WHERE t1.c1 = $1 AND t2.c1 = $2;
EXPLAIN (COSTS false) EXECUTE st1(1, 2);
! QUERY PLAN
! -------------------------------------------------------------------------------------
Nested Loop
-> Foreign Scan on ft1 t1
! Filter: (c1 = 1)
! Remote SQL: SELECT "C 1", NULL, c3, NULL, NULL, NULL, NULL FROM "S 1"."T 1"
-> Foreign Scan on ft2 t2
! Filter: (c1 = 2)
! Remote SQL: SELECT "C 1", NULL, c3, NULL, NULL, NULL, NULL FROM "S 1"."T 1"
! (7 rows)
EXECUTE st1(1, 1);
c3 | c3
--- 435,448 ----
-- simple join
PREPARE st1(int, int) AS SELECT t1.c3, t2.c3 FROM ft1 t1, ft2 t2 WHERE t1.c1 = $1 AND t2.c1 = $2;
EXPLAIN (COSTS false) EXECUTE st1(1, 2);
! QUERY PLAN
! ------------------------------------------------------------------------------------------------------------------------------
Nested Loop
-> Foreign Scan on ft1 t1
! Remote SQL: SELECT "C 1", NULL, c3, NULL, NULL, NULL, NULL FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.=) 1))
-> Foreign Scan on ft2 t2
! Remote SQL: SELECT "C 1", NULL, c3, NULL, NULL, NULL, NULL FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.=) 2))
! (5 rows)
EXECUTE st1(1, 1);
c3 | c3
*************** EXECUTE st1(101, 101);
*** 390,409 ****
-- subquery using stable function (can't be pushed down)
PREPARE st2(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c4) = 6) ORDER BY c1;
EXPLAIN (COSTS false) EXECUTE st2(10, 20);
! QUERY PLAN
! ------------------------------------------------------------------------------------------------
Sort
Sort Key: t1.c1
-> Hash Semi Join
Hash Cond: (t1.c3 = t2.c3)
-> Foreign Scan on ft1 t1
! Filter: (c1 < 20)
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
-> Hash
-> Foreign Scan on ft2 t2
! Filter: ((c1 > 10) AND (date_part('dow'::text, c4) = 6::double precision))
! Remote SQL: SELECT "C 1", NULL, c3, c4, NULL, NULL, NULL FROM "S 1"."T 1"
! (11 rows)
EXECUTE st2(10, 20);
c1 | c2 | c3 | c4 | c5 | c6 | c7
--- 459,477 ----
-- subquery using stable function (can't be pushed down)
PREPARE st2(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c4) = 6) ORDER BY c1;
EXPLAIN (COSTS false) EXECUTE st2(10, 20);
! QUERY PLAN
! -----------------------------------------------------------------------------------------------------------------------------------------
Sort
Sort Key: t1.c1
-> Hash Semi Join
Hash Cond: (t1.c3 = t2.c3)
-> Foreign Scan on ft1 t1
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.<) 20))
-> Hash
-> Foreign Scan on ft2 t2
! Filter: (date_part('dow'::text, c4) = 6::double precision)
! Remote SQL: SELECT "C 1", NULL, c3, c4, NULL, NULL, NULL FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.>) 10))
! (10 rows)
EXECUTE st2(10, 20);
c1 | c2 | c3 | c4 | c5 | c6 | c7
*************** EXECUTE st1(101, 101);
*** 420,439 ****
-- subquery using immutable function (can be pushed down)
PREPARE st3(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c5) = 6) ORDER BY c1;
EXPLAIN (COSTS false) EXECUTE st3(10, 20);
! QUERY PLAN
! ------------------------------------------------------------------------------------------------
Sort
Sort Key: t1.c1
-> Hash Semi Join
Hash Cond: (t1.c3 = t2.c3)
-> Foreign Scan on ft1 t1
! Filter: (c1 < 20)
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
-> Hash
-> Foreign Scan on ft2 t2
! Filter: ((c1 > 10) AND (date_part('dow'::text, c5) = 6::double precision))
! Remote SQL: SELECT "C 1", NULL, c3, NULL, c5, NULL, NULL FROM "S 1"."T 1"
! (11 rows)
EXECUTE st3(10, 20);
c1 | c2 | c3 | c4 | c5 | c6 | c7
--- 488,506 ----
-- subquery using immutable function (can be pushed down)
PREPARE st3(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c5) = 6) ORDER BY c1;
EXPLAIN (COSTS false) EXECUTE st3(10, 20);
! QUERY PLAN
! -----------------------------------------------------------------------------------------------------------------------------------------
Sort
Sort Key: t1.c1
-> Hash Semi Join
Hash Cond: (t1.c3 = t2.c3)
-> Foreign Scan on ft1 t1
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.<) 20))
-> Hash
-> Foreign Scan on ft2 t2
! Filter: (date_part('dow'::text, c5) = 6::double precision)
! Remote SQL: SELECT "C 1", NULL, c3, NULL, c5, NULL, NULL FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.>) 10))
! (10 rows)
EXECUTE st3(10, 20);
c1 | c2 | c3 | c4 | c5 | c6 | c7
*************** EXECUTE st3(20, 30);
*** 450,501 ****
-- custom plan should be chosen
PREPARE st4(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 = $1;
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! ---------------------------------------------------------------------
Foreign Scan on ft1 t1
! Filter: (c1 = 1)
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
! (3 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! ---------------------------------------------------------------------
Foreign Scan on ft1 t1
! Filter: (c1 = 1)
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
! (3 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! ---------------------------------------------------------------------
Foreign Scan on ft1 t1
! Filter: (c1 = 1)
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
! (3 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! ---------------------------------------------------------------------
Foreign Scan on ft1 t1
! Filter: (c1 = 1)
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
! (3 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! ---------------------------------------------------------------------
Foreign Scan on ft1 t1
! Filter: (c1 = 1)
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
! (3 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! ---------------------------------------------------------------------
Foreign Scan on ft1 t1
! Filter: (c1 = $1)
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
! (3 rows)
-- cleanup
DEALLOCATE st1;
--- 517,562 ----
-- custom plan should be chosen
PREPARE st4(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 = $1;
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! --------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.=) 1))
! (2 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! --------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.=) 1))
! (2 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! --------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.=) 1))
! (2 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! --------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.=) 1))
! (2 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! --------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.=) 1))
! (2 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! ---------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.=) $1))
! (2 rows)
-- cleanup
DEALLOCATE st1;
diff --git a/contrib/pgsql_fdw/pgsql_fdw.c b/contrib/pgsql_fdw/pgsql_fdw.c
index df7f407..4916a45 100644
*** a/contrib/pgsql_fdw/pgsql_fdw.c
--- b/contrib/pgsql_fdw/pgsql_fdw.c
***************
*** 17,26 ****
#include "catalog/pg_foreign_table.h"
#include "commands/defrem.h"
#include "commands/explain.h"
! #include "foreign/fdwapi.h"
#include "funcapi.h"
#include "miscadmin.h"
- #include "nodes/nodeFuncs.h"
#include "optimizer/cost.h"
#include "optimizer/pathnode.h"
#include "optimizer/planmain.h"
--- 17,25 ----
#include "catalog/pg_foreign_table.h"
#include "commands/defrem.h"
#include "commands/explain.h"
! #include "foreign/foreign.h"
#include "funcapi.h"
#include "miscadmin.h"
#include "optimizer/cost.h"
#include "optimizer/pathnode.h"
#include "optimizer/planmain.h"
*************** PG_MODULE_MAGIC;
*** 61,66 ****
--- 60,70 ----
#define CURSOR_NAME_FORMAT "pgsql_fdw_cursor_%u"
static uint32 cursor_id = 0;
+ /* Convenient macros for accessing the first record of PGresult. */
+ #define PGRES_VAL0(col) (PQgetvalue(res, 0, (col)))
+ #define PGRES_NULL0(col) (PQgetisnull(res, 0, (col)))
+
+
/*
* Index of FDW-private information stored in fdw_private list.
*
*************** static uint32 cursor_id = 0;
*** 68,78 ****
* the boundary between planner and executor. Finally FdwPlan using cursor
* would hold items below:
*
! * 1) plain SELECT statement
! * 2) SQL statement used to declare cursor
! * 3) SQL statement used to fetch rows from cursor
* 5) SQL statement used to reset cursor
! * 5) SQL statement used to close cursor
*
* These items are indexed with the enum FdwPrivateIndex, so an item
* can be accessed directly via list_nth(). For example of FETCH
--- 72,83 ----
* the boundary between planner and executor. Finally FdwPlan using cursor
* would hold items below:
*
! * 1) expressions which are pushed down
! * 2) plain SELECT statement
! * 3) SQL statement used to declare cursor
! * 4) SQL statement used to fetch rows from cursor
* 5) SQL statement used to reset cursor
! * 6) SQL statement used to close cursor
*
* These items are indexed with the enum FdwPrivateIndex, so an item
* can be accessed directly via list_nth(). For example of FETCH
*************** static uint32 cursor_id = 0;
*** 80,85 ****
--- 85,93 ----
* list_nth(fdw_private, FdwPrivateFetchSql)
*/
enum FdwPrivateIndex {
+ /* Planning information */
+ FdwPrivateFdwExprs,
+
/* SQL statements */
FdwPrivateSelectSql,
FdwPrivateDeclareSql,
*************** pgsqlGetForeignPlan(PlannerInfo *root,
*** 282,298 ****
DefElem *def;
int fetch_count = DEFAULT_FETCH_COUNT;
char *sql;
ForeignTable *table;
ForeignServer *server;
/*
! * We have no native ability to evaluate restriction clauses, so we just
! * put all the scan_clauses into the plan node's qual list for the
! * executor to check. So all we have to do here is strip RestrictInfo
! * nodes from the clauses and ignore pseudoconstants (which will be
! * handled elsewhere).
*/
scan_clauses = extract_actual_clauses(scan_clauses, false);
/*
* Use specified fetch_count instead of default value, if any. Foreign
--- 290,312 ----
DefElem *def;
int fetch_count = DEFAULT_FETCH_COUNT;
char *sql;
+ List *fdw_exprs = NIL;
+ List *local_exprs = NIL;
ForeignTable *table;
ForeignServer *server;
/*
! * We have native ability to evaluate restriction clauses on remote side,
! * so we split clauses into local portion and remote portion. Before
! * that, we strip RestrictInfo nodes from the clauses and ignore
! * pseudoconstants (which will be handled elsewhere).
! *
! * Extracted list of expressions is stored in fdw_private of ForeignScan
! * for possible reference in executor handlers.
*/
scan_clauses = extract_actual_clauses(scan_clauses, false);
+ fdw_exprs = extractRemoteExprs(root, baserel, &local_exprs);
+ fdw_private = lappend(fdw_private, fdw_exprs);
/*
* Use specified fetch_count instead of default value, if any. Foreign
*************** pgsqlGetForeignPlan(PlannerInfo *root,
*** 322,330 ****
elog(DEBUG1, "relid=%u fetch_count=%d", foreigntableid, fetch_count);
/*
! * Construct simple remote query which has no WHERE clause.
*/
sql = deparseSimpleSql(foreigntableid, root, baserel, table);
fdw_private = lappend(fdw_private, makeString(sql));
/* Construct cursor name from sequential value */
--- 336,354 ----
elog(DEBUG1, "relid=%u fetch_count=%d", foreigntableid, fetch_count);
/*
! * Construct remote query, and add WHERE clause if we have any expression
! * which can be pushed down to remote side safely.
*/
sql = deparseSimpleSql(foreigntableid, root, baserel, table);
+ if (list_length(fdw_exprs) > 0)
+ {
+ StringInfoData buf;
+
+ initStringInfo(&buf);
+ appendStringInfo(&buf, "%s", sql);
+ deparseWhereClause(&buf, fdw_exprs, root);
+ sql = buf.data;
+ }
fdw_private = lappend(fdw_private, makeString(sql));
/* Construct cursor name from sequential value */
*************** pgsqlGetForeignPlan(PlannerInfo *root,
*** 350,360 ****
appendStringInfo(&cursor, "CLOSE %s", name);
fdw_private = lappend(fdw_private, makeString(cursor.data));
! /* Create the ForeignScan node with fdw_private of selected path. */
return make_foreignscan(tlist,
! scan_clauses,
scan_relid,
! NIL,
fdw_private);
}
--- 374,393 ----
appendStringInfo(&cursor, "CLOSE %s", name);
fdw_private = lappend(fdw_private, makeString(cursor.data));
! /*
! * Create the ForeignScan node from target list, local filtering
! * expressions, remote filtering expressions, and FDW private information.
! *
! * We remove expressions which are evaluated on remote side from qual of
! * the scan node to avoid redundant filtering. Such filter reduction
! * can be done only here, done after choosing best path, because
! * baserestrictinfo in RelOptInfo is shared by all possible paths until
! * best path is chosen.
! */
return make_foreignscan(tlist,
! local_exprs,
scan_relid,
! fdw_exprs,
fdw_private);
}
diff --git a/contrib/pgsql_fdw/pgsql_fdw.h b/contrib/pgsql_fdw/pgsql_fdw.h
index 6eb99d5..5bd672a 100644
*** a/contrib/pgsql_fdw/pgsql_fdw.h
--- b/contrib/pgsql_fdw/pgsql_fdw.h
***************
*** 15,20 ****
--- 15,21 ----
#define PGSQL_FDW_H
#include "postgres.h"
+ #include "foreign/fdwapi.h"
#include "foreign/foreign.h"
#include "nodes/relation.h"
*************** char *deparseSimpleSql(Oid relid,
*** 28,32 ****
--- 29,42 ----
PlannerInfo *root,
RelOptInfo *baserel,
ForeignTable *table);
+ void deparseWhereClause(StringInfo buf,
+ List *exprs,
+ PlannerInfo *root);
+ List *extractRemoteExprs(PlannerInfo *root,
+ RelOptInfo *baserel,
+ List **local_exprs);
+ void deparseExpr(StringInfo buf,
+ Expr *expr,
+ PlannerInfo *root);
#endif /* PGSQL_FDW_H */
diff --git a/contrib/pgsql_fdw/sql/pgsql_fdw.sql b/contrib/pgsql_fdw/sql/pgsql_fdw.sql
index b3fac96..b7cd71d 100644
*** a/contrib/pgsql_fdw/sql/pgsql_fdw.sql
--- b/contrib/pgsql_fdw/sql/pgsql_fdw.sql
*************** EXPLAIN (COSTS false) SELECT * FROM ft1
*** 182,187 ****
--- 182,201 ----
EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = t1.c2;
-- ===================================================================
+ -- WHERE push down
+ -- ===================================================================
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = 1; -- Var, OpExpr(b), Const
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = 100 AND t1.c2 = 0; -- BoolExpr
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE c1 IS NULL; -- NullTest
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE c1 IS NOT NULL; -- NullTest
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE round(abs(c1), 0) = 1; -- FuncExpr
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE c1 = -c1; -- OpExpr(l)
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE 1 = c1!; -- OpExpr(r)
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE (c1 IS NOT NULL) IS DISTINCT FROM (c1 IS NOT NULL); -- DistinctExpr
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE c1 = ANY(ARRAY[c2, 1, c1 + 0]); -- ScalarArrayOpExpr
+ EXPLAIN (COSTS false) SELECT * FROM ft1 ft WHERE c1 = (ARRAY[c1,c2,3])[1]; -- ArrayRef
+
+ -- ===================================================================
-- parameterized queries
-- ===================================================================
-- simple join
pgsql_fdw_analyze_v1.patchtext/plain; charset=Shift_JIS; name=pgsql_fdw_analyze_v1.patchDownload
diff --git a/contrib/pgsql_fdw/expected/pgsql_fdw.out b/contrib/pgsql_fdw/expected/pgsql_fdw.out
index 5560931..bd6aa73 100644
*** a/contrib/pgsql_fdw/expected/pgsql_fdw.out
--- b/contrib/pgsql_fdw/expected/pgsql_fdw.out
*************** INSERT INTO "S 1"."T 2"
*** 75,80 ****
--- 75,81 ----
'AAA' || to_char(id, 'FM000')
FROM generate_series(1, 100) id;
COMMIT;
+ ANALYZE "S 1"."T 1";
-- ===================================================================
-- tests for pgsql_fdw_validator
-- ===================================================================
*************** SELECT srvname, usename FROM pgsql_fdw_c
*** 584,589 ****
--- 585,606 ----
(0 rows)
-- ===================================================================
+ -- statistics management
+ -- ===================================================================
+ DELETE FROM pg_statistic WHERE starelid = '"S 1"."T 1"'::regclass AND staattnum IN (1, 3);
+ SELECT pgsql_fdw_analyze('ft1');
+ pgsql_fdw_analyze
+ -------------------
+ 5
+ (1 row)
+
+ SELECT count(*) FROM pg_statistic WHERE starelid = 'ft1'::regclass;
+ count
+ -------
+ 5
+ (1 row)
+
+ -- ===================================================================
-- subtransaction
-- ===================================================================
BEGIN;
diff --git a/contrib/pgsql_fdw/pgsql_fdw--1.0.sql b/contrib/pgsql_fdw/pgsql_fdw--1.0.sql
index b0ea2b2..033cd6f 100644
*** a/contrib/pgsql_fdw/pgsql_fdw--1.0.sql
--- b/contrib/pgsql_fdw/pgsql_fdw--1.0.sql
*************** SELECT c.srvid srvid,
*** 37,39 ****
--- 37,45 ----
JOIN pg_catalog.pg_foreign_server s ON (s.oid = c.srvid);
GRANT SELECT ON pgsql_fdw_connections TO public;
+ /* statistics management function */
+ CREATE FUNCTION pgsql_fdw_analyze(regclass)
+ RETURNS int
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C;
+
diff --git a/contrib/pgsql_fdw/pgsql_fdw.c b/contrib/pgsql_fdw/pgsql_fdw.c
index 4916a45..06a3603 100644
*** a/contrib/pgsql_fdw/pgsql_fdw.c
--- b/contrib/pgsql_fdw/pgsql_fdw.c
***************
*** 13,32 ****
#include "postgres.h"
#include "fmgr.h"
#include "catalog/pg_foreign_server.h"
#include "catalog/pg_foreign_table.h"
#include "commands/defrem.h"
#include "commands/explain.h"
! #include "foreign/foreign.h"
#include "funcapi.h"
#include "miscadmin.h"
#include "optimizer/cost.h"
#include "optimizer/pathnode.h"
#include "optimizer/planmain.h"
#include "optimizer/restrictinfo.h"
#include "utils/lsyscache.h"
#include "utils/memutils.h"
#include "utils/rel.h"
#include "pgsql_fdw.h"
#include "connection.h"
--- 13,37 ----
#include "postgres.h"
#include "fmgr.h"
+ #include "access/transam.h"
#include "catalog/pg_foreign_server.h"
#include "catalog/pg_foreign_table.h"
#include "commands/defrem.h"
#include "commands/explain.h"
! #include "commands/vacuum.h"
#include "funcapi.h"
#include "miscadmin.h"
#include "optimizer/cost.h"
#include "optimizer/pathnode.h"
#include "optimizer/planmain.h"
#include "optimizer/restrictinfo.h"
+ #include "parser/parse_type.h"
+ #include "utils/array.h"
+ #include "utils/builtins.h"
#include "utils/lsyscache.h"
#include "utils/memutils.h"
#include "utils/rel.h"
+ #include "utils/syscache.h"
#include "pgsql_fdw.h"
#include "connection.h"
*************** typedef struct PgsqlFdwExecutionState
*** 125,130 ****
--- 130,137 ----
*/
extern Datum pgsql_fdw_handler(PG_FUNCTION_ARGS);
PG_FUNCTION_INFO_V1(pgsql_fdw_handler);
+ extern Datum pgsql_fdw_analyze(PG_FUNCTION_ARGS);
+ PG_FUNCTION_INFO_V1(pgsql_fdw_analyze);
/*
* FDW callback routines
*************** static void adjust_costs(double rows, in
*** 155,160 ****
--- 162,168 ----
static void execute_query(ForeignScanState *node);
static PGresult *fetch_result(ForeignScanState *node);
static void store_result(ForeignScanState *node, PGresult *res);
+ static void store_remote_stats(PGresult *res, VacAttrStats *stats);
/* Exported functions, but not written in pgsql_fdw.h. */
void _PG_init(void);
*************** pgsql_fdw_handler(PG_FUNCTION_ARGS)
*** 211,216 ****
--- 219,229 ----
/*
* pgsqlGetForeignRelSize
* Estimate # of rows and width of the result of the scan
+ *
+ * Since we would have enough statistics about foreign data on local side by
+ * calling pgsql_fdw_analyze() for foreign table of pgsql_fdw, we can get good
+ * estimate of rows and width of this scan by calling
+ * set_baserel_size_estimates().
*/
static void
pgsqlGetForeignRelSize(PlannerInfo *root,
*************** store_result(ForeignScanState *node, PGr
*** 922,924 ****
--- 935,1523 ----
tuplestore_donestoring(festate->tuples);
}
+ /*
+ * Update statistics of a foreign table managed by pgsql_fdw by copying remote
+ * statistics into local pg_statistic and pg_class. If remote version is lower
+ * than 9.2, columns for slot 5 are ignored.
+ */
+ Datum
+ pgsql_fdw_analyze(PG_FUNCTION_ARGS)
+ {
+ Oid relid = PG_GETARG_OID(0);
+ Relation relation;
+ ListCell *lc;
+ int nstats = 0;
+ MemoryContext anl_context;
+ MemoryContext caller_context;
+ const char *nspname;
+ const char *relname;
+ ForeignTable *table;
+ ForeignServer *server;
+ ForeignDataWrapper *wrapper;
+ FdwRoutine *routine;
+ UserMapping *user;
+ PGconn *conn;
+ PGresult *res = NULL;
+ StringInfoData sql;
+ char *endp;
+ double reltuples;
+ int relpages;
+ int attr_cnt;
+ int tcnt;
+ int i;
+ VacAttrStats **vacattrstats;
+
+ /* Relation oid is required. */
+ if (PG_ARGISNULL(0))
+ ereport(ERROR,
+ (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
+ errmsg("relation oid must not be NULL")));
+
+ /* Use short-lived expression context. */
+ anl_context = AllocSetContextCreate(CurrentMemoryContext,
+ "Analyze",
+ ALLOCSET_DEFAULT_MINSIZE,
+ ALLOCSET_DEFAULT_INITSIZE,
+ ALLOCSET_DEFAULT_MAXSIZE);
+ caller_context = MemoryContextSwitchTo(anl_context);
+
+ /*
+ * Open the relation, getting ShareUpdateExclusiveLock to ensure that two
+ * ANALYZE function don't run on it concurrently. Of course, target must
+ * be a foreign table of pgsql_fdw.
+ */
+ relation = relation_open(relid, ShareUpdateExclusiveLock);
+ if (get_rel_relkind(relid) != RELKIND_FOREIGN_TABLE)
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("\"%s\" is not a foreign table",
+ RelationGetRelationName(relation))));
+
+ table = GetForeignTable(relid);
+ server = GetForeignServer(table->serverid);
+ wrapper = GetForeignDataWrapper(server->fdwid);
+ routine = GetFdwRoutine(wrapper->fdwhandler);
+ if (routine != &fdwroutine)
+ ereport(ERROR,
+ (errcode(ERRCODE_FDW_TABLE_NOT_FOUND),
+ errmsg("foreign table \"%s\" cannot handled by pgsql_fdw",
+ RelationGetRelationName(relation))));
+
+ /*
+ * Establish connection against the foreign server to retrieve remote
+ * statistics data.
+ */
+ user = GetUserMapping(GetOuterUserId(), table->serverid);
+ conn = GetConnection(server, user, false);
+
+ /* Construct SQL statement to retrieve per-relation statistics. */
+ foreach(lc, table->options)
+ {
+ DefElem *def = (DefElem *) lfirst(lc);
+
+ if (strcmp(def->defname, "nspname") == 0)
+ nspname = defGetString(def);
+ else if (strcmp(def->defname, "relname") == 0)
+ relname = defGetString(def);
+
+ }
+ if (nspname == NULL)
+ nspname = get_namespace_name(get_rel_namespace(relid));
+ nspname = quote_identifier(nspname);
+ if (relname == NULL)
+ relname = get_rel_name(relid);
+ relname = quote_identifier(relname);
+
+ /*
+ * Get per-relation statistics from remote pg_class. We don't fetch
+ * relallvisible, which is used for estimation about index-only scan,
+ * because index-only scan is not available on foreign tables.
+ *
+ * XXX Should we get summary of whole inherit-tree for inherited table?
+ */
+ initStringInfo(&sql);
+ appendStringInfo(&sql,
+ "SELECT relpages, "
+ " reltuples "
+ " FROM pg_catalog.pg_class "
+ " WHERE oid = '%s.%s'::regclass",
+ nspname, relname);
+
+ PG_TRY();
+ {
+ res = PQexec(conn, sql.data);
+
+ if (PQresultStatus(res) != PGRES_TUPLES_OK)
+ ereport(ERROR,
+ (errmsg("could not get per-table statistics"),
+ errdetail("%s", PQerrorMessage(conn))));
+ if (PQntuples(res) != 1)
+ ereport(ERROR,
+ (errmsg("invalid number of tuples: %d", PQntuples(res))));
+
+ relpages = strtol(PGRES_VAL0(0), &endp, 10);
+ if (*endp != '\0')
+ ereport(ERROR,
+ (errmsg("invalid format for relpages: %s", PGRES_VAL0(0))));
+ reltuples = strtod(PGRES_VAL0(1), &endp);
+ if (*endp != '\0')
+ ereport(ERROR,
+ (errmsg("invalid format for reltuples: %s", PGRES_VAL0(1))));
+ PQclear(res);
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+
+ /*
+ * Get per-column statistics from remote pg_statistic. Note that some of
+ * local columns might be dropped.
+ *
+ * FIXME When porting this routine as ANALYZE handler, we need to care
+ * vacstmt->va_cols here.
+ *
+ */
+ #ifdef NOT_USED
+ if (va_cols != NIL)
+ {
+ ListCell *le;
+
+ vacattrstats = (VacAttrStats **) palloc(list_length(va_cols) *
+ sizeof(VacAttrStats *));
+ tcnt = 0;
+ foreach(le, va_cols)
+ {
+ char *col = strVal(lfirst(le));
+
+ i = attnameAttNum(relation, col, false);
+ if (i == InvalidAttrNumber)
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_COLUMN),
+ errmsg("column \"%s\" of relation \"%s\" does not exist",
+ col, RelationGetRelationName(relation))));
+ vacattrstats[tcnt] = examine_attribute(relation, i, NULL);
+ if (vacattrstats[tcnt] != NULL)
+ tcnt++;
+ }
+ attr_cnt = tcnt;
+ }
+ else
+ #endif
+ {
+ attr_cnt = relation->rd_att->natts;
+ vacattrstats = (VacAttrStats **)
+ palloc(attr_cnt * sizeof(VacAttrStats *));
+ tcnt = 0;
+ for (i = 1; i <= attr_cnt; i++)
+ {
+ vacattrstats[tcnt] = examine_attribute(relation, i, NULL);
+ if (vacattrstats[tcnt] != NULL)
+ tcnt++;
+ }
+ attr_cnt = tcnt;
+ }
+
+ /* Retrieve statistics for each requested column from foreign server. */
+ for (i = 0; i < attr_cnt; i++)
+ {
+ List *options;
+ ListCell *lc;
+ const char *colname = NULL;
+ StringInfoData sql_stat;
+
+ /* Use colname FDW option to determine remote attribute, if any. */
+ options = GetForeignColumnOptions(relid,
+ vacattrstats[i]->attr->attnum);
+ foreach(lc, options)
+ {
+ DefElem *def = (DefElem *) lfirst(lc);
+
+ if (strcmp(def->defname, "colname") == 0)
+ {
+ colname = defGetString(def);
+ break;
+ }
+ }
+ if (colname == NULL)
+ colname = get_attname(relation->rd_id,
+ vacattrstats[i]->attr->attnum);
+
+ /* No need to quote column name here. */
+ initStringInfo(&sql_stat);
+ if (PQserverVersion(conn) >= 90200)
+ {
+ appendStringInfo(&sql_stat,
+ "SELECT starelid,"
+ " staattnum,"
+ /* fixed value is used for stainherit, see below */
+ " stanullfrac,"
+ " stawidth,"
+ " stadistinct,"
+ " stakind1,"
+ " stakind2,"
+ " stakind3,"
+ " stakind4,"
+ " stakind5,"
+ " staop1,"
+ " staop2,"
+ " staop3,"
+ " staop4,"
+ " staop5,"
+ " stanumbers1,"
+ " stanumbers2,"
+ " stanumbers3,"
+ " stanumbers4,"
+ " stanumbers5,"
+ " stavalues1,"
+ " stavalues2,"
+ " stavalues3,"
+ " stavalues4,"
+ " stavalues5"
+ " FROM pg_catalog.pg_statistic "
+ " WHERE starelid = '%s.%s'::regclass "
+ " AND staattnum = ("
+ " SELECT attnum "
+ " FROM pg_catalog.pg_attribute "
+ " WHERE attrelid = '%s.%s'::regclass "
+ " AND attname = '%s')",
+ nspname, relname, nspname, relname, colname);
+ }
+ else
+ {
+ appendStringInfo(&sql_stat,
+ "SELECT starelid,"
+ " staattnum,"
+ /* fixed value is used for stainherit, see below */
+ " stanullfrac,"
+ " stawidth,"
+ " stadistinct,"
+ " stakind1,"
+ " stakind2,"
+ " stakind3,"
+ " stakind4,"
+ " staop1,"
+ " staop2,"
+ " staop3,"
+ " staop4,"
+ " stanumbers1,"
+ " stanumbers2,"
+ " stanumbers3,"
+ " stanumbers4,"
+ " stavalues1,"
+ " stavalues2,"
+ " stavalues3,"
+ " stavalues4"
+ " FROM pg_catalog.pg_statistic "
+ " WHERE starelid = '%s.%s'::regclass "
+ " AND staattnum = ("
+ " SELECT attnum "
+ " FROM pg_catalog.pg_attribute "
+ " WHERE attrelid = '%s.%s'::regclass "
+ " AND attname = '%s')",
+ nspname, relname, nspname, relname, colname);
+ }
+
+ /*
+ * If foreign server is PG 9.0 or later, inherited tables might have
+ * two sets of statistics; one contains information about the table
+ * itself, and another contains information about whole inherit-tree
+ * topped by the table.
+ *
+ * We prefer statistics of whole inherit-tree if any, because parent
+ * table returns contents of descendants too. However, we overwrite
+ * stainherit by "false" for local statistics, because foreign tables
+ * can't be parent table.
+ */
+ if (PQserverVersion(conn) >= 90000)
+ appendStringInfo(&sql_stat, "ORDER BY stainherit DESC LIMIT 1");
+
+ res = NULL;
+ PG_TRY();
+ {
+ res = PQexec(conn, sql_stat.data);
+ pfree(sql_stat.data);
+ if (PQresultStatus(res) != PGRES_TUPLES_OK)
+ ereport(ERROR,
+ (errmsg("could not get per-column statistics"),
+ errdetail("%s", PQerrorMessage(conn))));
+
+ /* If remote table doesn't have any statistic, skip this column. */
+ if (PQntuples(res) > 0)
+ {
+ store_remote_stats(res, vacattrstats[i]);
+ nstats++;
+ }
+
+ PQclear(res);
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+ }
+
+ /* Disconnect from foreign server. */
+ ReleaseConnection(conn);
+
+ /* Update local pg_statistic with retrieved statistics. */
+ vac_update_attstats(RelationGetRelid(relation),
+ false,
+ attr_cnt,
+ vacattrstats);
+
+ /*
+ * Update local pg_class with retrieved statistics. We set relallvisible
+ * to 0 always.
+ */
+ vac_update_relstats(relation,
+ relpages,
+ reltuples,
+ 0,
+ false,
+ InvalidTransactionId);
+
+ /* Restore memory context. */
+ MemoryContextSwitchTo(caller_context);
+ MemoryContextDelete(anl_context);
+
+ #ifdef NOT_USED
+ /* Print verbose messages. */
+ if (vacstmt->options & VACOPT_VERBOSE)
+ elog(INFO, "%s: relpages=%d, reltuples=%f, %d per-column stats copied",
+ vacstmt->relation->relname,
+ relpages,
+ reltuples,
+ nstats);
+ #endif
+
+ /* Close the relation with releasing the lock. */
+ relation_close(relation, ShareUpdateExclusiveLock);
+
+ PG_RETURN_INT32(nstats);
+ }
+
+ /*
+ * Read text representation of remote statistics from res and store them into
+ * stats with conversion to internal representation.
+ */
+ static void
+ store_remote_stats(PGresult *res, VacAttrStats *stats)
+ {
+ Type float4array_type;
+ Type textarray_type;
+ Oid elemtypeid;
+ Type elemtype;
+ Oid in_func_oid;
+ Form_pg_type attrtype;
+ Oid typeioparam;
+ FmgrInfo in_function;
+
+ int i_stanullfrac;
+ int i_stawidth;
+ int i_stadistinct;
+ int i_stakind[STATISTIC_NUM_SLOTS];
+ int i_staop[STATISTIC_NUM_SLOTS];
+ int i_stanumbers[STATISTIC_NUM_SLOTS];
+ int i_stavalues[STATISTIC_NUM_SLOTS];
+ int i;
+
+ /* Get information of actual type of array element. */
+ elemtypeid = stats->attr->atttypid;
+ elemtype = typeidType(elemtypeid);
+ getTypeInputInfo(elemtypeid, &in_func_oid, &typeioparam);
+ fmgr_info(in_func_oid, &in_function);
+
+ attrtype = (Form_pg_type) palloc(sizeof(FormData_pg_type));
+ memcpy(attrtype, GETSTRUCT(elemtype), sizeof(FormData_pg_type));
+
+ i_stanullfrac = PQfnumber(res, "stanullfrac");
+ i_stawidth = PQfnumber(res, "stawidth");
+ i_stadistinct = PQfnumber(res, "stadistinct");
+ i_stakind[0] = PQfnumber(res, "stakind1");
+ i_stakind[1] = PQfnumber(res, "stakind2");
+ i_stakind[2] = PQfnumber(res, "stakind3");
+ i_stakind[3] = PQfnumber(res, "stakind4");
+ i_stakind[4] = PQfnumber(res, "stakind5");
+ i_staop[0] = PQfnumber(res, "staop1");
+ i_staop[1] = PQfnumber(res, "staop2");
+ i_staop[2] = PQfnumber(res, "staop3");
+ i_staop[3] = PQfnumber(res, "staop4");
+ i_staop[4] = PQfnumber(res, "staop5");
+ i_stanumbers[0] = PQfnumber(res, "stanumbers1");
+ i_stanumbers[1] = PQfnumber(res, "stanumbers2");
+ i_stanumbers[2] = PQfnumber(res, "stanumbers3");
+ i_stanumbers[3] = PQfnumber(res, "stanumbers4");
+ i_stanumbers[4] = PQfnumber(res, "stanumbers5");
+ i_stavalues[0] = PQfnumber(res, "stavalues1");
+ i_stavalues[1] = PQfnumber(res, "stavalues2");
+ i_stavalues[2] = PQfnumber(res, "stavalues3");
+ i_stavalues[3] = PQfnumber(res, "stavalues4");
+ i_stavalues[4] = PQfnumber(res, "stavalues5");
+
+ /*
+ * If statistics are found, mark this column to be updated later
+ * and fill with retrieved remote statistics.
+ *
+ * For scalar values, just copying values is enough, but we have to
+ * be conscious the types of elements for array values. libpq
+ * gives us an array value in a string representation, such as
+ * "{foo, bar}", so we need to (1)separate an array string into
+ * elements, (2)convert each element into Datum representation, and
+ * (3) create a Datum representation of array from Datum elements.
+ *
+ * Note: OID of operator (staopN) MUST match between remote and local.
+ */
+ stats->stats_valid = true;
+ stats->stanullfrac = strtof(PGRES_VAL0(i_stanullfrac), NULL);
+ stats->stawidth = strtol(PGRES_VAL0(i_stawidth), NULL, 10);
+ stats->stadistinct = strtof(PGRES_VAL0(i_stadistinct), NULL);
+
+ /*
+ * Get type information for stanumbers and stavalues. We assume stavalues
+ * as array of text during extracting elements, and then extracted
+ * elements are treated as the type of foreign table attributes.
+ */
+ float4array_type = typeidType(FLOAT4ARRAYOID);
+ textarray_type = typeidType(TEXTARRAYOID);
+
+ for (i = 0; i < STATISTIC_NUM_SLOTS; i++)
+ {
+ int atttypmod = 0;
+ int j;
+ Datum tnumbers = 0;
+ ArrayType *array;
+ int nitems;
+ bits8 *bitmap;
+ int bitmask;
+ char *p;
+
+ /* PostgreSQL 9.1 and older don't have stakind5. */
+ if (i_stakind[i] < 0 || i_staop[i] < 0 ||
+ i_stanumbers[i] < 0 || i_stavalues[i] < 0)
+ continue;
+
+ stats->stakind[i] = strtol(PGRES_VAL0(i_stakind[i]), NULL, 10);
+ stats->staop[i] = strtol(PGRES_VAL0(i_staop[i]), NULL, 10);
+
+ /*
+ * Extract element values of stanumbersN from text representation of
+ * array, if any.
+ */
+ if (!PGRES_NULL0(i_stanumbers[i]))
+ {
+ /* Separate array string into each float4 elements. */
+ tnumbers = stringTypeDatum(float4array_type,
+ PGRES_VAL0(i_stanumbers[i]),
+ -1);
+ array = DatumGetArrayTypeP(tnumbers);
+ nitems = ArrayGetNItems(ARR_NDIM(array), ARR_DIMS(array));
+
+ bitmap = ARR_NULLBITMAP(array);
+ bitmask = 1;
+
+ stats->numnumbers[i] = nitems;
+ stats->stanumbers[i] = (float4 *) palloc(nitems * sizeof(float4));
+ for (j = 0; j < nitems; j++)
+ {
+ p = ARR_DATA_PTR(array);
+
+ if (bitmap && (*bitmap & bitmask) == 0)
+ {
+ /* nulls can be ignored */
+ }
+ else
+ {
+ stats->stanumbers[i][j] = * (float4 *) ARR_DATA_PTR(array);
+ p = p + sizeof(float4);
+ p = (char *) att_align_nominal(p, 'i');
+ }
+
+ /* advance bitmap pointer if any */
+ if (bitmap)
+ {
+ bitmask <<= 1;
+ if (bitmask == 0x100)
+ {
+ bitmap++;
+ bitmask = 1;
+ }
+ }
+ }
+ }
+ else
+ {
+ stats->numnumbers[i] = 0;
+ stats->stanumbers[i] = NULL;
+ }
+
+ /*
+ * Extract element values of stavaluesN from text representation of
+ * array, if any.
+ */
+ if (!PGRES_NULL0(i_stavalues[i]))
+ {
+ /*
+ * Separate array string into each elements string, and convert
+ * each element to actual column data type.
+ */
+ tnumbers = stringTypeDatum(textarray_type,
+ PGRES_VAL0(i_stavalues[i]),
+ -1);
+ array = DatumGetArrayTypeP(tnumbers);
+ nitems = ArrayGetNItems(ARR_NDIM(array), ARR_DIMS(array));
+
+ bitmap = ARR_NULLBITMAP(array);
+ bitmask = 1;
+ p = ARR_DATA_PTR(array);
+
+ stats->numvalues[i] = nitems;
+ stats->stavalues[i] = (Datum *) palloc(nitems * sizeof(Datum));
+
+ for (j = 0; j < nitems; j++)
+ {
+ char *string;
+
+ if (bitmap && (*bitmap & bitmask) == 0)
+ {
+ /* nulls can be ignored */
+ }
+ else
+ {
+ string = text_to_cstring((text *) p);
+ stats->stavalues[i][j] = InputFunctionCall(&in_function,
+ string,
+ typeioparam,
+ atttypmod);
+ p = p + VARSIZE_ANY(p);
+ p = (char *) att_align_nominal(p, 'i');
+ }
+
+ /* advance bitmap pointer if any */
+ if (bitmap)
+ {
+ bitmask <<= 1;
+ if (bitmask == 0x100)
+ {
+ bitmap++;
+ bitmask = 1;
+ }
+ }
+ }
+ }
+ else
+ {
+ stats->numvalues[i] = 0;
+ stats->stavalues[i] = NULL;
+ }
+ }
+
+ /* Clean up */
+ ReleaseSysCache(elemtype);
+ ReleaseSysCache(textarray_type);
+ ReleaseSysCache(float4array_type);
+ }
diff --git a/contrib/pgsql_fdw/sql/pgsql_fdw.sql b/contrib/pgsql_fdw/sql/pgsql_fdw.sql
index b7cd71d..eef1cfa 100644
*** a/contrib/pgsql_fdw/sql/pgsql_fdw.sql
--- b/contrib/pgsql_fdw/sql/pgsql_fdw.sql
*************** INSERT INTO "S 1"."T 2"
*** 85,90 ****
--- 85,91 ----
'AAA' || to_char(id, 'FM000')
FROM generate_series(1, 100) id;
COMMIT;
+ ANALYZE "S 1"."T 1";
-- ===================================================================
-- tests for pgsql_fdw_validator
*************** SELECT pgsql_fdw_disconnect(srvid, usesy
*** 235,240 ****
--- 236,248 ----
SELECT srvname, usename FROM pgsql_fdw_connections;
-- ===================================================================
+ -- statistics management
+ -- ===================================================================
+ DELETE FROM pg_statistic WHERE starelid = '"S 1"."T 1"'::regclass AND staattnum IN (1, 3);
+ SELECT pgsql_fdw_analyze('ft1');
+ SELECT count(*) FROM pg_statistic WHERE starelid = 'ft1'::regclass;
+
+ -- ===================================================================
-- subtransaction
-- ===================================================================
BEGIN;
diff --git a/doc/src/sgml/pgsql-fdw.sgml b/doc/src/sgml/pgsql-fdw.sgml
index eb69f01..2c16c43 100644
*** a/doc/src/sgml/pgsql-fdw.sgml
--- b/doc/src/sgml/pgsql-fdw.sgml
*************** postgres=# SELECT pgsql_fdw_disconnect(s
*** 218,223 ****
--- 218,244 ----
</sect2>
<sect2>
+ <title>Statistics Management</title>
+ <para>
+ <application>pgsql_fdw</application> provides a function
+ <function>pgsql_fdw_analyze()</function> which updates local statistics
+ stored in <structname>pg_statistic</structname> and
+ <structname>pg_class</structname> by copying remote statistics. This
+ function takes a regclass argument of target foreign table, and returns
+ number of attributes of which per-attribute statistics are updated. Thus,
+ this function may return number less than attribute count when remote table
+ doesn't have statistics for all attributes.
+ </para>
+ <note>
+ <para>
+ Number of statistics slots in <structname>pg_statistic</structname> is
+ increased to 5 in 9.2, so this function copies only slot 1 to 4 from older
+ versions.
+ </para>
+ </note>
+ </sect2>
+
+ <sect2>
<title>Estimation of Costs and Rows</title>
<para>
The <application>pgsql_fdw</application> estimates the costs of a foreign
diff --git a/src/backend/commands/analyze.c b/src/backend/commands/analyze.c
index 9cd6e67..21b1bf1 100644
*** a/src/backend/commands/analyze.c
--- b/src/backend/commands/analyze.c
*************** static void compute_index_stats(Relation
*** 94,101 ****
AnlIndexData *indexdata, int nindexes,
HeapTuple *rows, int numrows,
MemoryContext col_context);
- static VacAttrStats *examine_attribute(Relation onerel, int attnum,
- Node *index_expr);
static int acquire_sample_rows(Relation onerel, HeapTuple *rows,
int targrows, double *totalrows, double *totaldeadrows);
static double random_fract(void);
--- 94,99 ----
*************** static int compare_rows(const void *a, c
*** 105,112 ****
static int acquire_inherited_sample_rows(Relation onerel,
HeapTuple *rows, int targrows,
double *totalrows, double *totaldeadrows);
- static void update_attstats(Oid relid, bool inh,
- int natts, VacAttrStats **vacattrstats);
static Datum std_fetch_func(VacAttrStatsP stats, int rownum, bool *isNull);
static Datum ind_fetch_func(VacAttrStatsP stats, int rownum, bool *isNull);
--- 103,108 ----
*************** analyze_rel(Oid relid, VacuumStmt *vacst
*** 209,215 ****
}
/*
! * We can ANALYZE any table except pg_statistic. See update_attstats
*/
if (RelationGetRelid(onerel) == StatisticRelationId)
{
--- 205,211 ----
}
/*
! * We can ANALYZE any table except pg_statistic. See vac_update_attstats
*/
if (RelationGetRelid(onerel) == StatisticRelationId)
{
*************** analyze_rel(Oid relid, VacuumStmt *vacst
*** 239,245 ****
* Close source relation now, but keep lock so that no one deletes it
* before we commit. (If someone did, they'd fail to clean up the entries
* we made in pg_statistic. Also, releasing the lock before commit would
! * expose us to concurrent-update failures in update_attstats.)
*/
relation_close(onerel, NoLock);
--- 235,241 ----
* Close source relation now, but keep lock so that no one deletes it
* before we commit. (If someone did, they'd fail to clean up the entries
* we made in pg_statistic. Also, releasing the lock before commit would
! * expose us to concurrent-update failures in vac_update_attstats.)
*/
relation_close(onerel, NoLock);
*************** do_analyze_rel(Relation onerel, VacuumSt
*** 514,528 ****
* previous statistics for the target columns. (If there are stats in
* pg_statistic for columns we didn't process, we leave them alone.)
*/
! update_attstats(RelationGetRelid(onerel), inh,
! attr_cnt, vacattrstats);
for (ind = 0; ind < nindexes; ind++)
{
AnlIndexData *thisdata = &indexdata[ind];
! update_attstats(RelationGetRelid(Irel[ind]), false,
! thisdata->attr_cnt, thisdata->vacattrstats);
}
}
--- 510,524 ----
* previous statistics for the target columns. (If there are stats in
* pg_statistic for columns we didn't process, we leave them alone.)
*/
! vac_update_attstats(RelationGetRelid(onerel), inh,
! attr_cnt, vacattrstats);
for (ind = 0; ind < nindexes; ind++)
{
AnlIndexData *thisdata = &indexdata[ind];
! vac_update_attstats(RelationGetRelid(Irel[ind]), false,
! thisdata->attr_cnt, thisdata->vacattrstats);
}
}
*************** compute_index_stats(Relation onerel, dou
*** 805,811 ****
* If index_expr isn't NULL, then we're trying to analyze an expression index,
* and index_expr is the expression tree representing the column's data.
*/
! static VacAttrStats *
examine_attribute(Relation onerel, int attnum, Node *index_expr)
{
Form_pg_attribute attr = onerel->rd_att->attrs[attnum - 1];
--- 801,807 ----
* If index_expr isn't NULL, then we're trying to analyze an expression index,
* and index_expr is the expression tree representing the column's data.
*/
! VacAttrStats *
examine_attribute(Relation onerel, int attnum, Node *index_expr)
{
Form_pg_attribute attr = onerel->rd_att->attrs[attnum - 1];
*************** acquire_inherited_sample_rows(Relation o
*** 1538,1544 ****
/*
! * update_attstats() -- update attribute statistics for one relation
*
* Statistics are stored in several places: the pg_class row for the
* relation has stats about the whole relation, and there is a
--- 1534,1540 ----
/*
! * vac_update_attstats() -- update attribute statistics for one relation
*
* Statistics are stored in several places: the pg_class row for the
* relation has stats about the whole relation, and there is a
*************** acquire_inherited_sample_rows(Relation o
*** 1559,1566 ****
* ANALYZE the same table concurrently. Presently, we lock that out
* by taking a self-exclusive lock on the relation in analyze_rel().
*/
! static void
! update_attstats(Oid relid, bool inh, int natts, VacAttrStats **vacattrstats)
{
Relation sd;
int attno;
--- 1555,1562 ----
* ANALYZE the same table concurrently. Presently, we lock that out
* by taking a self-exclusive lock on the relation in analyze_rel().
*/
! void
! vac_update_attstats(Oid relid, bool inh, int natts, VacAttrStats **vacattrstats)
{
Relation sd;
int attno;
diff --git a/src/include/commands/vacuum.h b/src/include/commands/vacuum.h
index 3deee66..da9bf8a 100644
*** a/src/include/commands/vacuum.h
--- b/src/include/commands/vacuum.h
*************** extern void vac_update_relstats(Relation
*** 154,159 ****
--- 154,161 ----
BlockNumber num_all_visible_pages,
bool hasindex,
TransactionId frozenxid);
+ extern void vac_update_attstats(Oid relid, bool inh,
+ int natts, VacAttrStats **vacattrstats);
extern void vacuum_set_xid_limits(int freeze_min_age, int freeze_table_age,
bool sharedRel,
TransactionId *oldestXmin,
*************** extern void lazy_vacuum_rel(Relation one
*** 170,174 ****
--- 172,178 ----
extern void analyze_rel(Oid relid, VacuumStmt *vacstmt,
BufferAccessStrategy bstrategy);
extern bool std_typanalyze(VacAttrStats *stats);
+ extern VacAttrStats *examine_attribute(Relation onerel, int attnum,
+ Node *index_expr);
#endif /* VACUUM_H */
Shigeru HANADA wrote:
I've implemented pgsql_fdw's own deparser and enhanced some features
since last post. Please apply attached patches in the order below:
Changes from previous version
=============================1) Don't use remote EXPLAIN for cost/rows estimation, so now planner
estimates result rows and costs on the basis of local statistics such
as
pg_class and pg_statistic. To update local statistics, I added
pgsql_fdw_analyze() SQL function which updates local statistics of a
foreign table by retrieving remote statistics, such as pg_class and
pg_statistic, via libpq. This would make the planning of pgsql_fdw
simple and fast. This function can be easily modified to handle
ANALYZE
command invoked for a foreign table (Fujita-san is proposing this as
common feature in another thread).2) Defer planning stuffs as long as possible to clarify the role of
each
function. Currently GetRelSize just estimates result rows from local
statistics, and GetPaths adds only one path which represents SeqScan
on
remote side. As result of this change, PgsqlFdwPlanState struct is
obsolete.
I see the advantage of being able to do all this locally, but
I think there are a lot of downsides too:
- You have an additional maintenance task if you want to keep
statistics for remote tables accurate. I understand that this may
get better in a future release.
- You depend on the structure of pg_statistic, which means a potential
incompatibility between server versions. You can add cases to
pgsql_fdw_analyze to cater for changes, but that is cumbersome and
will
only help for later PostgreSQL versions connecting to earlier ones.
- Planning and execution will change (improve, of course) between server
versions. The local planner may choose an inferior plan based on a
wrong assumption of how a certain query can be handled on the remote.
- You have no statistics if the foreign table points to a view on the
remote system.
My gut feeling is that planning should be done by the server which
will execute the query.
3) Implement pgsql_fdw's own deparser which pushes down collation-free
and immutable expressions in local WHERE clause. This means that most
of numeric conditions can be pushed down, but conditions using
character
types are not.
I understand that this is simple and practical, but it is a pity that
this excludes equality and inequality conditions on strings.
Correct me if I am wrong, but I thought that these work the same
regardless of the collation.
Yours,
Laurenz Albe
Thanks for the comments.
(2012/03/27 18:36), Albe Laurenz wrote:
2) Defer planning stuffs as long as possible to clarify the role of
each
function. Currently GetRelSize just estimates result rows from local
statistics, and GetPaths adds only one path which represents SeqScanon
remote side. As result of this change, PgsqlFdwPlanState struct is
obsolete.I see the advantage of being able to do all this locally, but
I think there are a lot of downsides too:
- You have an additional maintenance task if you want to keep
statistics for remote tables accurate. I understand that this may
get better in a future release.
- You depend on the structure of pg_statistic, which means a potential
incompatibility between server versions. You can add cases to
pgsql_fdw_analyze to cater for changes, but that is cumbersome and
will
only help for later PostgreSQL versions connecting to earlier ones.
- Planning and execution will change (improve, of course) between server
versions. The local planner may choose an inferior plan based on a
wrong assumption of how a certain query can be handled on the remote.
- You have no statistics if the foreign table points to a view on the
remote system.
Especially for 2nd and 4th, generating pg_statistic records without
calling do_analyze_rel() seems unpractical in multiple version
environment. As you pointed out, I've missed another
semantics-different problem here. We would have to use do_analyze_rel()
and custom sampling function which returns sample rows from remote data
source, if we want to have statistics of foreign data locally. This
method would be available for most of FDWs, but requires some changes in
core. [I'll comment on Fujita-san's ANALYZE patch about this issue soon.]
My gut feeling is that planning should be done by the server which
will execute the query.
Agreed, if selectivity of both local filtering and remote filtering were
available, we can estimate result rows correctly and choose better plan.
How about getting # of rows estimate by executing EXPLAIN for
fully-fledged remote query (IOW, contains pushed-down WHERE clause), and
estimate selectivity of local filter on the basis of the statistics
which are generated by FDW via do_analyze_rel() and FDW-specific
sampling function? In this design, we would be able to use quite
correct rows estimate because we can consider filtering stuffs done on
each side separately, though it requires expensive remote EXPLAIN for
each possible path.
3) Implement pgsql_fdw's own deparser which pushes down collation-free
and immutable expressions in local WHERE clause. This means that most
of numeric conditions can be pushed down, but conditions usingcharacter
types are not.
I understand that this is simple and practical, but it is a pity that
this excludes equality and inequality conditions on strings.
Correct me if I am wrong, but I thought that these work the same
regardless of the collation.
You are right, built-in equality and inequality operators don't cause
collation problem. Perhaps allowing them would cover significant cases
of string comparison, but I'm not sure how to determine whether an
operator is = or != in generic way. We might have to hold list of oid
for collation-safe operator/functions until we support ROUTINE MAPPING
or something like that... Anyway, I'll fix pgsql_fdw to allow = and !=
for character types.
Regards,
--
Shigeru Hanada
2012/3/26 Shigeru HANADA <shigeru.hanada@gmail.com>:
(2012/03/15 23:06), Shigeru HANADA wrote:
Although the patches are still WIP, especially in WHERE push-down part,
but I'd like to post them so that I can get feedback of the design as
soon as possible.I've implemented pgsql_fdw's own deparser and enhanced some features
since last post. Please apply attached patches in the order below:* pgsql_fdw_v17.patch
- Adds pgsql_fdw as contrib module
* pgsql_fdw_pushdown_v10.patch
- Adds WHERE push down capability to pgsql_fdw
* pgsql_fdw_analyze_v1.patch
- Adds pgsql_fdw_analyze function for updating local stats
Hmm... I've applied them using the latest Git master, and in the order
specified, but I can't build the contrib module. What am I doing
wrong?
It starts off with things like:
../../src/Makefile.global:420: warning: overriding commands for target
`submake-libpq'
../../src/Makefile.global:420: warning: ignoring old commands for
target `submake-libpq'
and later:
pgsql_fdw.c: In function ‘pgsqlGetForeignPlan’:
pgsql_fdw.c:321:2: warning: implicit declaration of function
‘extractRemoteExprs’ [-Wimplicit-function-declaration]
pgsql_fdw.c:321:12: warning: assignment makes pointer from integer
without a cast [enabled by default]
pgsql_fdw.c:362:3: warning: implicit declaration of function
‘deparseWhereClause’ [-Wimplicit-function-declaration]
pgsql_fdw.c: At top level:
pgsql_fdw.c:972:1: error: redefinition of ‘Pg_magic_func’
pgsql_fdw.c:39:1: note: previous definition of ‘Pg_magic_func’ was here
--
Thom
Shigeru HANADA wrote:
My gut feeling is that planning should be done by the server which
will execute the query.Agreed, if selectivity of both local filtering and remote filtering
were
available, we can estimate result rows correctly and choose better
plan.
How about getting # of rows estimate by executing EXPLAIN for
fully-fledged remote query (IOW, contains pushed-down WHERE clause),
and
estimate selectivity of local filter on the basis of the statistics
which are generated by FDW via do_analyze_rel() and FDW-specific
sampling function? In this design, we would be able to use quite
correct rows estimate because we can consider filtering stuffs done on
each side separately, though it requires expensive remote EXPLAIN for
each possible path.
That sounds nice.
How would that work with a query that has one condition that could be
pushed down and one that has to be filtered locally?
Would you use the (local) statistics for the full table or can you
somehow account for the fact that rows have already been filtered
out remotely, which might influence the distribution?
You are right, built-in equality and inequality operators don't cause
collation problem. Perhaps allowing them would cover significant
cases
of string comparison, but I'm not sure how to determine whether an
operator is = or != in generic way. We might have to hold list of oid
for collation-safe operator/functions until we support ROUTINE MAPPING
or something like that... Anyway, I'll fix pgsql_fdw to allow = and
!=
for character types.
I believe that this covers a significant percentage of real-world cases.
I'd think that every built-in operator with name "=" or "<>" could
be pushed down.
Yours,
Laurenz Albe
(2012/03/27 20:32), Thom Brown wrote:
2012/3/26 Shigeru HANADA<shigeru.hanada@gmail.com>:
* pgsql_fdw_v17.patch
- Adds pgsql_fdw as contrib module
* pgsql_fdw_pushdown_v10.patch
- Adds WHERE push down capability to pgsql_fdw
* pgsql_fdw_analyze_v1.patch
- Adds pgsql_fdw_analyze function for updating local statsHmm... I've applied them using the latest Git master, and in the order
specified, but I can't build the contrib module. What am I doing
wrong?
I'm sorry, but I couldn't reproduce the errors with this procedure.
$ git checkout master
$ git pull upstream master # make master branch up-to-date
$ git clean -fd # remove files for other branches
$ make clean # just in case
$ patch -p1 < /path/to/pgsql_fdw_v17.patch
$ patch -p1 < /path/to/pgsql_fdw_pushdown_v10.patch
$ patch -p1 < /path/to/pgsql_fdw_analyze_v1.patch
$ make # make core first for libpq et al.
$ cd contrib/pgsql_fdw
$ make # pgsql_fdw
Please try "git clean" and "make clean", if you have not.
FWIW, I'm using GNU Make 3.82 and gcc 4.6.0 on Fedora 15.
Regards,
--
Shigeru HANADA
2012/3/28 Shigeru HANADA <shigeru.hanada@gmail.com>:
(2012/03/27 20:32), Thom Brown wrote:
2012/3/26 Shigeru HANADA<shigeru.hanada@gmail.com>:
* pgsql_fdw_v17.patch
- Adds pgsql_fdw as contrib module
* pgsql_fdw_pushdown_v10.patch
- Adds WHERE push down capability to pgsql_fdw
* pgsql_fdw_analyze_v1.patch
- Adds pgsql_fdw_analyze function for updating local statsHmm... I've applied them using the latest Git master, and in the order
specified, but I can't build the contrib module. What am I doing
wrong?I'm sorry, but I couldn't reproduce the errors with this procedure.
$ git checkout master
$ git pull upstream master # make master branch up-to-date
$ git clean -fd # remove files for other branches
$ make clean # just in case
$ patch -p1 < /path/to/pgsql_fdw_v17.patch
$ patch -p1 < /path/to/pgsql_fdw_pushdown_v10.patch
$ patch -p1 < /path/to/pgsql_fdw_analyze_v1.patch
$ make # make core first for libpq et al.
$ cd contrib/pgsql_fdw
$ make # pgsql_fdwPlease try "git clean" and "make clean", if you have not.
FWIW, I'm using GNU Make 3.82 and gcc 4.6.0 on Fedora 15.
I had done a make clean, git stash and git clean -f, but I didn't try
git clean -fd. For some reason it's working now.
Thanks
--
Thom
I wrote:
How about getting # of rows estimate by executing EXPLAIN for
fully-fledged remote query (IOW, contains pushed-down WHERE clause),
and
estimate selectivity of local filter on the basis of the statistics
which are generated by FDW via do_analyze_rel() and FDW-specific
sampling function? In this design, we would be able to use quite
correct rows estimate because we can consider filtering stuffs done
on
each side separately, though it requires expensive remote EXPLAIN for
each possible path.That sounds nice.
... but it still suffers from the problems of local statistics
for remote tables I pointed out.
I think that these shortcomings are not justified by the gain of
one client-server round trip less during planning. I'd prefer
if pgsql_fdw were not dependent on remote statistics stored in the
local database.
Yours,
Laurenz Albe
On 28 March 2012 08:13, Thom Brown <thom@linux.com> wrote:
2012/3/28 Shigeru HANADA <shigeru.hanada@gmail.com>:
(2012/03/27 20:32), Thom Brown wrote:
2012/3/26 Shigeru HANADA<shigeru.hanada@gmail.com>:
* pgsql_fdw_v17.patch
- Adds pgsql_fdw as contrib module
* pgsql_fdw_pushdown_v10.patch
- Adds WHERE push down capability to pgsql_fdw
* pgsql_fdw_analyze_v1.patch
- Adds pgsql_fdw_analyze function for updating local statsHmm... I've applied them using the latest Git master, and in the order
specified, but I can't build the contrib module. What am I doing
wrong?I'm sorry, but I couldn't reproduce the errors with this procedure.
$ git checkout master
$ git pull upstream master # make master branch up-to-date
$ git clean -fd # remove files for other branches
$ make clean # just in case
$ patch -p1 < /path/to/pgsql_fdw_v17.patch
$ patch -p1 < /path/to/pgsql_fdw_pushdown_v10.patch
$ patch -p1 < /path/to/pgsql_fdw_analyze_v1.patch
$ make # make core first for libpq et al.
$ cd contrib/pgsql_fdw
$ make # pgsql_fdwPlease try "git clean" and "make clean", if you have not.
FWIW, I'm using GNU Make 3.82 and gcc 4.6.0 on Fedora 15.I had done a make clean, git stash and git clean -f, but I didn't try
git clean -fd. For some reason it's working now.
Hmm.. I'm getting some rather odd errors though:
thom@test=# select * from stuff limit 3 ;
DEBUG: StartTransactionCommand
DEBUG: StartTransaction
DEBUG: name: unnamed; blockState: DEFAULT; state: INPROGR,
xid/subid/cid: 0/1/0, nestlvl: 1, children:
LOG: statement: select * from stuff limit 3 ;
DEBUG: relid=16402 fetch_count=10000
DEBUG: Remote SQL: SELECT id, stuff, age FROM public.stuff
DEBUG: starting remote transaction with "START TRANSACTION ISOLATION
LEVEL REPEATABLE READ"
ERROR: could not declare cursor
DETAIL: ERROR: relation "public.stuff" does not exist
LINE 1: ...or_6 SCROLL CURSOR FOR SELECT id, stuff, age FROM public.stu...
^
HINT: DECLARE pgsql_fdw_cursor_6 SCROLL CURSOR FOR SELECT id, stuff,
age FROM public.stuff
The table in question indeed doesn't exist, but I'm confused as to why
the user is being exposed to such messages.
And more troublesome:
(local select on foreign server):
test=# select * from stuff limit 3 ;
id | thing | age
----+----------+-----
1 | STANDARD | 30
2 | STANDARD | 29
3 | STANDARD | 12
(3 rows)
(foreign select on foreign server):
thom@test=# select * from stuff limit 3 ;
id | stuff | age
----+-----------------+-----
1 | (1,STANDARD,30) | 30
2 | (2,STANDARD,29) | 29
3 | (3,STANDARD,12) | 12
(3 rows)
The row expansion seems to incorrectly rewrite the column without a
table prefix if both column and table name are identical.
--
Thom
On 28 March 2012 08:39, Thom Brown <thom@linux.com> wrote:
On 28 March 2012 08:13, Thom Brown <thom@linux.com> wrote:
2012/3/28 Shigeru HANADA <shigeru.hanada@gmail.com>:
(2012/03/27 20:32), Thom Brown wrote:
2012/3/26 Shigeru HANADA<shigeru.hanada@gmail.com>:
* pgsql_fdw_v17.patch
- Adds pgsql_fdw as contrib module
* pgsql_fdw_pushdown_v10.patch
- Adds WHERE push down capability to pgsql_fdw
* pgsql_fdw_analyze_v1.patch
- Adds pgsql_fdw_analyze function for updating local statsHmm... I've applied them using the latest Git master, and in the order
specified, but I can't build the contrib module. What am I doing
wrong?I'm sorry, but I couldn't reproduce the errors with this procedure.
$ git checkout master
$ git pull upstream master # make master branch up-to-date
$ git clean -fd # remove files for other branches
$ make clean # just in case
$ patch -p1 < /path/to/pgsql_fdw_v17.patch
$ patch -p1 < /path/to/pgsql_fdw_pushdown_v10.patch
$ patch -p1 < /path/to/pgsql_fdw_analyze_v1.patch
$ make # make core first for libpq et al.
$ cd contrib/pgsql_fdw
$ make # pgsql_fdwPlease try "git clean" and "make clean", if you have not.
FWIW, I'm using GNU Make 3.82 and gcc 4.6.0 on Fedora 15.I had done a make clean, git stash and git clean -f, but I didn't try
git clean -fd. For some reason it's working now.Hmm.. I'm getting some rather odd errors though:
thom@test=# select * from stuff limit 3 ;
DEBUG: StartTransactionCommand
DEBUG: StartTransaction
DEBUG: name: unnamed; blockState: DEFAULT; state: INPROGR,
xid/subid/cid: 0/1/0, nestlvl: 1, children:
LOG: statement: select * from stuff limit 3 ;
DEBUG: relid=16402 fetch_count=10000
DEBUG: Remote SQL: SELECT id, stuff, age FROM public.stuff
DEBUG: starting remote transaction with "START TRANSACTION ISOLATION
LEVEL REPEATABLE READ"
ERROR: could not declare cursor
DETAIL: ERROR: relation "public.stuff" does not exist
LINE 1: ...or_6 SCROLL CURSOR FOR SELECT id, stuff, age FROM public.stu...
^HINT: DECLARE pgsql_fdw_cursor_6 SCROLL CURSOR FOR SELECT id, stuff,
age FROM public.stuffThe table in question indeed doesn't exist, but I'm confused as to why
the user is being exposed to such messages.And more troublesome:
(local select on foreign server):
test=# select * from stuff limit 3 ;
id | thing | age
----+----------+-----
1 | STANDARD | 30
2 | STANDARD | 29
3 | STANDARD | 12
(3 rows)(foreign select on foreign server):
thom@test=# select * from stuff limit 3 ;
id | stuff | age
----+-----------------+-----
1 | (1,STANDARD,30) | 30
2 | (2,STANDARD,29) | 29
3 | (3,STANDARD,12) | 12
(3 rows)The row expansion seems to incorrectly rewrite the column without a
table prefix if both column and table name are identical.
Actually, correction. The foreign table definition names the column
the same as the table. I accidentally omitted the 'thing' column and
instead substituted it with the table name in the definition.
Original table definition on foreign server:
create table stuff (id serial primary key, thing text, age int);
Foreign table definition:
create foreign table stuff (id int not null, stuff text, age int) server pgsql;
So it appears I'm allowed to use the table as a column in this
context. So please disregard my complaint.
--
Thom
I wrote:
Changes from previous version
=============================1) Don't use remote EXPLAIN for cost/rows estimation, so now planner
estimates result rows and costs on the basis of local statistics such
as pg_class and pg_statistic. To update local statistics, I added
pgsql_fdw_analyze() SQL function which updates local statistics of a
foreign table by retrieving remote statistics, such as pg_class and
pg_statistic, via libpq. This would make the planning of pgsql_fdw
simple and fast. This function can be easily modified to handle
ANALYZE command invoked for a foreign table (Fujita-san is proposing
this as common feature in another thread).
I see the advantage of being able to do all this locally, but
I think there are a lot of downsides too:
I found another limitation of this approach:
pgsql_fdw_analyze() has to run as a user who can update
pg_statistic, and this user needs a user mapping to a remote
user who can read pg_statistic.
This is not necessary for normal operation and needs
to be configured specifically for getting remote statistics.
This is cumbersome, and people might be unhappy to have to
create user mappings for highly privileged remote users.
Yours,
Laurenz Albe
(2012/03/28 16:18), Albe Laurenz wrote:
I wrote:
How about getting # of rows estimate by executing EXPLAIN for
fully-fledged remote query (IOW, contains pushed-down WHERE clause),and
estimate selectivity of local filter on the basis of the statistics
which are generated by FDW via do_analyze_rel() and FDW-specific
sampling function? In this design, we would be able to use quite
correct rows estimate because we can consider filtering stuffs doneon
each side separately, though it requires expensive remote EXPLAIN for
each possible path.That sounds nice.
... but it still suffers from the problems of local statistics
for remote tables I pointed out.
I guess that you mean about these issues you wrote in earlier post, so
I'd like to comment on them.
- You have an additional maintenance task if you want to keep
statistics for remote tables accurate. I understand that this may
get better in a future release.
I'm not sure that's what you meant, but we need to execute remote
ANALYZE before calling pgsql_fdw_analyze() to keep local statistics
accurate. IMO DBAs are responsible to execute remote ANALYZE at
appropriate timing, so pgsql_fdw_analyze (or handler function for
ANALYZE) should just collect statistics from remote side.
- You depend on the structure of pg_statistic, which means a potential
incompatibility between server versions. You can add cases to
pgsql_fdw_analyze to cater for changes, but that is cumbersome and
will
only help for later PostgreSQL versions connecting to earlier ones.
Indeed. Like pg_dump, pgsql_fdw should aware of different server
version if we choose copying statistics. Difference of catalog
structure is very easy to track and cope with, but if meanings of values
or the way to calculate statistics are changed, pgsql_fdw would need
very complex codes to convert values from different version.
I don't know such example, but IMO we should assume that statistics are
valid for only same version (at least major version). After all, I'd
prefer collecting sample data by pgsql_fdw and leaving statistics
generation to local backend.
- Planning and execution will change (improve, of course) between server
versions. The local planner may choose an inferior plan based on a
wrong assumption of how a certain query can be handled on the remote.
Hm, I don't worry about detail of remote planning so much, because
remote server would do its best for a query given by pgsql_fdw. Also
local planner would do its best for given estimation (rows, width and
costs). One concern is that remote cost factors might be different from
local's, so FDW option which represents cost conversion coefficient (1.0
means that remote cost has same weight as local) might be useful.
- You have no statistics if the foreign table points to a view on the
remote system.
ISTM that this would be enough reason to give up copying remote stats to
local. We don't provide SELECT push-down nor GROUP BY push-down at
present, so users would want to create views which contain function call
in SELECT clauses.
I think that these shortcomings are not justified by the gain of
one client-server round trip less during planning. I'd prefer
if pgsql_fdw were not dependent on remote statistics stored in the
local database.
I too prefer if pgsql_fdw doesn't fully depend on statistics of foreign
data, but IMO having statistics of foreign data which were calculated in
the way same as local data seems still useful for estimation about local
filtering. Even if we have no statistics of foreign data on local side,
still we would be able to create plans on the basis of default
selectivity for each expression, as same as regular tables.
Regards,
--
Shigeru HANADA
(2012/03/28 21:07), Albe Laurenz wrote:
I found another limitation of this approach:
pgsql_fdw_analyze() has to run as a user who can update
pg_statistic, and this user needs a user mapping to a remote
user who can read pg_statistic.This is not necessary for normal operation and needs
to be configured specifically for getting remote statistics.
This is cumbersome, and people might be unhappy to have to
create user mappings for highly privileged remote users.
Agreed. After all, supporting ANALYZE command for foreign tables seems
the only way to obtain local statistics of foreign data without granting
privileges too much. ANALYZE is allowed to only the owner of the table
or a superuser, so assuming that an invoker has valid user mapping for a
remote user who can read corresponding foreign data seems reasonable.
ANALYZE support for foreign tables is proposed by Fujita-san in current
CF, so I'd like to push it.
Regards,
--
Shigeru HANADA
Attached are latest version of pgsql_fdw patches. Note that
pgsql_fdw_analyze.patch is only for test the effect of local statistics.
Please apply patches in the order below:
(1) pgsql_fdw_v18.patch
(2) pgsql_fdw_pushdown_v11.patch
(3) pgsql_fdw_analyze.patch (if you want to try local stats)
(2012/03/27 20:49), Albe Laurenz wrote:
How about getting # of rows estimate by executing EXPLAIN for
fully-fledged remote query (IOW, contains pushed-down WHERE clause),and
estimate selectivity of local filter on the basis of the statistics
which are generated by FDW via do_analyze_rel() and FDW-specific
sampling function? In this design, we would be able to use quite
correct rows estimate because we can consider filtering stuffs done on
each side separately, though it requires expensive remote EXPLAIN for
each possible path.That sounds nice.
How would that work with a query that has one condition that could be
pushed down and one that has to be filtered locally?Would you use the (local) statistics for the full table or can you
somehow account for the fact that rows have already been filtered
out remotely, which might influence the distribution?
First of all, pgsql_fdw sorts conditions into three groups:
(A) can be pushed down, and contains no Param node
(B) can be pushed down, and contains Param node
(C) can not be pushed down
Then pgsql_fdw generates SELECT statement which contains only (A), and
execute EXPLAIN for that query to get rough estimate. Then, pgsql
estimates selectivity of (B) and (C) by calling
clauselist_selectivity(). Finally pgsql_fdw multiply rough estimate,
selectivity of (B) and (C). Thus we can get estimate of # of rows
returned by the scan.
Aside that, before executing actual query, pgsql_fdw appends (B) to the
remote query string to use external parameter. Conditions in (C) are
evaluated on local side anyway.
You are right, built-in equality and inequality operators don't cause
collation problem. Perhaps allowing them would cover significantcases
of string comparison, but I'm not sure how to determine whether an
operator is = or != in generic way. We might have to hold list of oid
for collation-safe operator/functions until we support ROUTINE MAPPING
or something like that... Anyway, I'll fix pgsql_fdw to allow = and!=
for character types.
I believe that this covers a significant percentage of real-world cases.
I'd think that every built-in operator with name "=" or "<>" could
be pushed down.
I've fixed pgsql_fdw to push down "=" and "<>" even if they take
collatable type as operands. Now collation check is done for only input
type of OpExpr and ScalarArrayOpExpr. In addition to the two, operators
below seem safe to push down.
concatenate : ||
LIKE : ~~, ~~*, !~~, !~~*
POSIX regex : ~, ~*, !~, !~*
Text Search : @@
Regards,
--
Shigeru HANADA
Attachments:
pgsql_fdw_pushdown_v11.patchtext/plain; charset=Shift_JIS; name=pgsql_fdw_pushdown_v11.patchDownload
diff --git a/contrib/pgsql_fdw/deparse.c b/contrib/pgsql_fdw/deparse.c
index 8e79232..28fad0a 100644
*** a/contrib/pgsql_fdw/deparse.c
--- b/contrib/pgsql_fdw/deparse.c
***************
*** 14,19 ****
--- 14,21 ----
#include "access/transam.h"
#include "catalog/pg_class.h"
+ #include "catalog/pg_operator.h"
+ #include "catalog/pg_type.h"
#include "commands/defrem.h"
#include "foreign/foreign.h"
#include "lib/stringinfo.h"
***************
*** 22,30 ****
--- 24,34 ----
#include "nodes/makefuncs.h"
#include "optimizer/clauses.h"
#include "optimizer/var.h"
+ #include "parser/parser.h"
#include "parser/parsetree.h"
#include "utils/builtins.h"
#include "utils/lsyscache.h"
+ #include "utils/syscache.h"
#include "pgsql_fdw.h"
*************** typedef struct foreign_executable_cxt
*** 35,40 ****
--- 39,45 ----
{
PlannerInfo *root;
RelOptInfo *foreignrel;
+ bool has_param;
} foreign_executable_cxt;
/*
*************** static void deparseRelation(StringInfo b
*** 44,49 ****
--- 49,76 ----
bool need_prefix);
static void deparseVar(StringInfo buf, Var *node, PlannerInfo *root,
bool need_prefix);
+ static void deparseConst(StringInfo buf, Const *node, PlannerInfo *root);
+ static void deparseBoolExpr(StringInfo buf, BoolExpr *node, PlannerInfo *root);
+ static void deparseNullTest(StringInfo buf, NullTest *node, PlannerInfo *root);
+ static void deparseDistinctExpr(StringInfo buf, DistinctExpr *node,
+ PlannerInfo *root);
+ static void deparseRelabelType(StringInfo buf, RelabelType *node,
+ PlannerInfo *root);
+ static void deparseFuncExpr(StringInfo buf, FuncExpr *node, PlannerInfo *root);
+ static void deparseParam(StringInfo buf, Param *node, PlannerInfo *root);
+ static void deparseScalarArrayOpExpr(StringInfo buf, ScalarArrayOpExpr *node,
+ PlannerInfo *root);
+ static void deparseOpExpr(StringInfo buf, OpExpr *node, PlannerInfo *root);
+ static void deparseArrayRef(StringInfo buf, ArrayRef *node, PlannerInfo *root);
+ static void deparseArrayExpr(StringInfo buf, ArrayExpr *node, PlannerInfo *root);
+
+ /*
+ * Determine whether an expression can be evaluated on remote side safely.
+ */
+ static bool is_foreign_expr(PlannerInfo *root, RelOptInfo *baserel, Expr *expr,
+ bool *has_param);
+ static bool foreign_expr_walker(Node *node, foreign_executable_cxt *context);
+ static bool is_builtin(Oid procid);
/*
* Deparse query representation into SQL statement which suits for remote
*************** deparseSimpleSql(StringInfo buf,
*** 151,156 ****
--- 178,284 ----
}
/*
+ * Examine each element in the list baserestrictinfo of baserel, and sort them
+ * into three groups: remote_conds contains conditions which can be evaluated
+ * - remote_conds is push-down safe, and don't contain any Param node
+ * - param_conds is push-down safe, but contain some Param node
+ * - local_conds is not push-down safe
+ *
+ * Only remote_conds can be used in remote EXPLAIN, and remote_conds and
+ * param_conds can be used in final remote query.
+ */
+ void
+ sortConditions(PlannerInfo *root,
+ RelOptInfo *baserel,
+ List **remote_conds,
+ List **param_conds,
+ List **local_conds)
+ {
+ ListCell *lc;
+ bool has_param;
+
+ Assert(remote_conds);
+ Assert(param_conds);
+ Assert(local_conds);
+
+ foreach(lc, baserel->baserestrictinfo)
+ {
+ RestrictInfo *ri = (RestrictInfo *) lfirst(lc);
+
+ if (is_foreign_expr(root, baserel, ri->clause, &has_param))
+ {
+ if (has_param)
+ *param_conds = lappend(*param_conds, ri);
+ else
+ *remote_conds = lappend(*remote_conds, ri);
+ }
+ else
+ *local_conds = lappend(*local_conds, ri);
+ }
+ }
+
+ /*
+ * Deparse given expression into buf. Actual string operation is delegated to
+ * node-type-specific functions.
+ *
+ * Note that switch statement of this function MUST match the one in
+ * foreign_expr_walker to avoid unsupported error..
+ */
+ void
+ deparseExpr(StringInfo buf, Expr *node, PlannerInfo *root)
+ {
+ /*
+ * This part must be match foreign_expr_walker.
+ */
+ switch (nodeTag(node))
+ {
+ case T_Const:
+ deparseConst(buf, (Const *) node, root);
+ break;
+ case T_BoolExpr:
+ deparseBoolExpr(buf, (BoolExpr *) node, root);
+ break;
+ case T_NullTest:
+ deparseNullTest(buf, (NullTest *) node, root);
+ break;
+ case T_DistinctExpr:
+ deparseDistinctExpr(buf, (DistinctExpr *) node, root);
+ break;
+ case T_RelabelType:
+ deparseRelabelType(buf, (RelabelType *) node, root);
+ break;
+ case T_FuncExpr:
+ deparseFuncExpr(buf, (FuncExpr *) node, root);
+ break;
+ case T_Param:
+ deparseParam(buf, (Param *) node, root);
+ break;
+ case T_ScalarArrayOpExpr:
+ deparseScalarArrayOpExpr(buf, (ScalarArrayOpExpr *) node, root);
+ break;
+ case T_OpExpr:
+ deparseOpExpr(buf, (OpExpr *) node, root);
+ break;
+ case T_Var:
+ deparseVar(buf, (Var *) node, root, false);
+ break;
+ case T_ArrayRef:
+ deparseArrayRef(buf, (ArrayRef *) node, root);
+ break;
+ case T_ArrayExpr:
+ deparseArrayExpr(buf, (ArrayExpr *) node, root);
+ break;
+ default:
+ {
+ ereport(ERROR,
+ (errmsg("unsupported expression for deparse"),
+ errdetail("%s", nodeToString(node))));
+ }
+ break;
+ }
+ }
+
+ /*
* Deparse node into buf, with relation qualifier if need_prefix was true. If
* node is a column of a foreign table, use value of colname FDW option (if any)
* instead of attribute name.
*************** deparseRelation(StringInfo buf,
*** 286,288 ****
--- 414,1145 ----
appendStringInfo(buf, "%s.", q_nspname);
appendStringInfo(buf, "%s", q_relname);
}
+
+ /*
+ * Deparse given constant value into buf. This function have to be kept in
+ * sync with get_const_expr.
+ */
+ static void
+ deparseConst(StringInfo buf,
+ Const *node,
+ PlannerInfo *root)
+ {
+ Oid typoutput;
+ bool typIsVarlena;
+ char *extval;
+ bool isfloat = false;
+ bool needlabel;
+
+ if (node->constisnull)
+ {
+ appendStringInfo(buf, "NULL");
+ return;
+ }
+
+ getTypeOutputInfo(node->consttype,
+ &typoutput, &typIsVarlena);
+ extval = OidOutputFunctionCall(typoutput, node->constvalue);
+
+ switch (node->consttype)
+ {
+ case ANYARRAYOID:
+ case ANYNONARRAYOID:
+ elog(ERROR, "anyarray and anyenum are not supported");
+ break;
+ case INT2OID:
+ case INT4OID:
+ case INT8OID:
+ case OIDOID:
+ case FLOAT4OID:
+ case FLOAT8OID:
+ case NUMERICOID:
+ {
+ /*
+ * No need to quote unless they contain special values such as
+ * 'Nan'.
+ */
+ if (strspn(extval, "0123456789+-eE.") == strlen(extval))
+ {
+ if (extval[0] == '+' || extval[0] == '-')
+ appendStringInfo(buf, "(%s)", extval);
+ else
+ appendStringInfoString(buf, extval);
+ if (strcspn(extval, "eE.") != strlen(extval))
+ isfloat = true; /* it looks like a float */
+ }
+ else
+ appendStringInfo(buf, "'%s'", extval);
+ }
+ break;
+ case BITOID:
+ case VARBITOID:
+ appendStringInfo(buf, "B'%s'", extval);
+ break;
+ case BOOLOID:
+ if (strcmp(extval, "t") == 0)
+ appendStringInfoString(buf, "true");
+ else
+ appendStringInfoString(buf, "false");
+ break;
+
+ default:
+ {
+ const char *valptr;
+
+ appendStringInfoChar(buf, '\'');
+ for (valptr = extval; *valptr; valptr++)
+ {
+ char ch = *valptr;
+
+ /*
+ * standard_conforming_strings of remote session should be
+ * set to similar value as local session.
+ */
+ if (SQL_STR_DOUBLE(ch, !standard_conforming_strings))
+ appendStringInfoChar(buf, ch);
+ appendStringInfoChar(buf, ch);
+ }
+ appendStringInfoChar(buf, '\'');
+ }
+ break;
+ }
+
+ /*
+ * Append ::typename unless the constant will be implicitly typed as the
+ * right type when it is read in.
+ *
+ * XXX this code has to be kept in sync with the behavior of the parser,
+ * especially make_const.
+ */
+ switch (node->consttype)
+ {
+ case BOOLOID:
+ case INT4OID:
+ case UNKNOWNOID:
+ needlabel = false;
+ break;
+ case NUMERICOID:
+ needlabel = !isfloat || (node->consttypmod >= 0);
+ break;
+ default:
+ needlabel = true;
+ break;
+ }
+ if (needlabel)
+ {
+ appendStringInfo(buf, "::%s",
+ format_type_with_typemod(node->consttype,
+ node->consttypmod));
+ }
+ }
+
+ static void
+ deparseBoolExpr(StringInfo buf,
+ BoolExpr *node,
+ PlannerInfo *root)
+ {
+ ListCell *lc;
+ char *op;
+ bool first;
+
+ switch (node->boolop)
+ {
+ case AND_EXPR:
+ op = "AND";
+ break;
+ case OR_EXPR:
+ op = "OR";
+ break;
+ case NOT_EXPR:
+ appendStringInfo(buf, "(NOT ");
+ deparseExpr(buf, list_nth(node->args, 0), root);
+ appendStringInfo(buf, ")");
+ return;
+ }
+
+ first = true;
+ appendStringInfo(buf, "(");
+ foreach(lc, node->args)
+ {
+ if (!first)
+ appendStringInfo(buf, " %s ", op);
+ deparseExpr(buf, (Expr *) lfirst(lc), root);
+ first = false;
+ }
+ appendStringInfo(buf, ")");
+ }
+
+ /*
+ * Deparse given IS [NOT] NULL test expression into buf.
+ */
+ static void
+ deparseNullTest(StringInfo buf,
+ NullTest *node,
+ PlannerInfo *root)
+ {
+ appendStringInfoChar(buf, '(');
+ deparseExpr(buf, node->arg, root);
+ if (node->nulltesttype == IS_NULL)
+ appendStringInfo(buf, " IS NULL)");
+ else
+ appendStringInfo(buf, " IS NOT NULL)");
+ }
+
+ static void
+ deparseDistinctExpr(StringInfo buf,
+ DistinctExpr *node,
+ PlannerInfo *root)
+ {
+ Assert(list_length(node->args) == 2);
+
+ deparseExpr(buf, linitial(node->args), root);
+ appendStringInfo(buf, " IS DISTINCT FROM ");
+ deparseExpr(buf, lsecond(node->args), root);
+ }
+
+ static void
+ deparseRelabelType(StringInfo buf,
+ RelabelType *node,
+ PlannerInfo *root)
+ {
+ char *typname;
+
+ Assert(node->arg);
+
+ /* We don't need to deparse cast when argument has same type as result. */
+ if (IsA(node->arg, Const) &&
+ ((Const *) node->arg)->consttype == node->resulttype &&
+ ((Const *) node->arg)->consttypmod == -1)
+ {
+ deparseExpr(buf, node->arg, root);
+ return;
+ }
+
+ typname = format_type_with_typemod(node->resulttype, node->resulttypmod);
+ appendStringInfoChar(buf, '(');
+ deparseExpr(buf, node->arg, root);
+ appendStringInfo(buf, ")::%s", typname);
+ }
+
+ /*
+ * Deparse given node which represents a function call into buf. We treat only
+ * explicit function call and explicit cast (coerce), because others are
+ * processed on remote side if necessary.
+ *
+ * Function name (and type name) is always qualified by schema name to avoid
+ * problems caused by different setting of search_path on remote side.
+ */
+ static void
+ deparseFuncExpr(StringInfo buf,
+ FuncExpr *node,
+ PlannerInfo *root)
+ {
+ Oid pronamespace;
+ const char *schemaname;
+ const char *funcname;
+ ListCell *arg;
+ bool first;
+
+ pronamespace = get_func_namespace(node->funcid);
+ schemaname = quote_identifier(get_namespace_name(pronamespace));
+ funcname = quote_identifier(get_func_name(node->funcid));
+
+ if (node->funcformat == COERCE_EXPLICIT_CALL)
+ {
+ /* Function call, deparse all arguments recursively. */
+ appendStringInfo(buf, "%s.%s(", schemaname, funcname);
+ first = true;
+ foreach(arg, node->args)
+ {
+ if (!first)
+ appendStringInfo(buf, ", ");
+ deparseExpr(buf, lfirst(arg), root);
+ first = false;
+ }
+ appendStringInfoChar(buf, ')');
+ }
+ else if (node->funcformat == COERCE_EXPLICIT_CAST)
+ {
+ /* Explicit cast, deparse only first argument. */
+ appendStringInfoChar(buf, '(');
+ deparseExpr(buf, linitial(node->args), root);
+ appendStringInfo(buf, ")::%s", funcname);
+ }
+ else
+ {
+ /* Implicit cast, deparse only first argument. */
+ deparseExpr(buf, linitial(node->args), root);
+ }
+ }
+
+ /*
+ * Deparse given Param node into buf.
+ *
+ * We don't renumber parameter id, because skipping $1 is not cause problem
+ * as far as we pass through all arguments.
+ */
+ static void
+ deparseParam(StringInfo buf,
+ Param *node,
+ PlannerInfo *root)
+ {
+ Assert(node->paramkind == PARAM_EXTERN);
+
+ appendStringInfo(buf, "$%d", node->paramid);
+ }
+
+ /*
+ * Deparse given ScalarArrayOpExpr expression into buf. To avoid problems
+ * around priority of operations, we always parenthesize the arguments. Also we
+ * use OPERATOR(schema.operator) notation to determine remote operator exactly.
+ */
+ static void
+ deparseScalarArrayOpExpr(StringInfo buf,
+ ScalarArrayOpExpr *node,
+ PlannerInfo *root)
+ {
+ HeapTuple tuple;
+ Form_pg_operator form;
+ const char *opnspname;
+ char *opname;
+ Expr *arg1;
+ Expr *arg2;
+
+ /* Retrieve necessary information about the operator from system catalog. */
+ tuple = SearchSysCache1(OPEROID, ObjectIdGetDatum(node->opno));
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "cache lookup failed for operator %u", node->opno);
+ form = (Form_pg_operator) GETSTRUCT(tuple);
+ /* opname is not a SQL identifier, so we don't need to quote it. */
+ opname = NameStr(form->oprname);
+ opnspname = quote_identifier(get_namespace_name(form->oprnamespace));
+ ReleaseSysCache(tuple);
+
+ /* Sanity check. */
+ Assert(list_length(node->args) == 2);
+
+ /* Always parenthesize the expression. */
+ appendStringInfoChar(buf, '(');
+
+ /* Extract operands. */
+ arg1 = linitial(node->args);
+ arg2 = lsecond(node->args);
+
+ /* Deparse fully qualified operator name. */
+ deparseExpr(buf, arg1, root);
+ appendStringInfo(buf, " OPERATOR(%s.%s) %s (",
+ opnspname, opname, node->useOr ? "ANY" : "ALL");
+ deparseExpr(buf, arg2, root);
+ appendStringInfoChar(buf, ')');
+
+ /* Always parenthesize the expression. */
+ appendStringInfoChar(buf, ')');
+ }
+
+ /*
+ * Deparse given operator expression into buf. To avoid problems around
+ * priority of operations, we always parenthesize the arguments. Also we use
+ * OPERATOR(schema.operator) notation to determine remote operator exactly.
+ */
+ static void
+ deparseOpExpr(StringInfo buf,
+ OpExpr *node,
+ PlannerInfo *root)
+ {
+ HeapTuple tuple;
+ Form_pg_operator form;
+ const char *opnspname;
+ char *opname;
+ char oprkind;
+ ListCell *arg;
+
+ /* Retrieve necessary information about the operator from system catalog. */
+ tuple = SearchSysCache1(OPEROID, ObjectIdGetDatum(node->opno));
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "cache lookup failed for operator %u", node->opno);
+ form = (Form_pg_operator) GETSTRUCT(tuple);
+ opnspname = quote_identifier(get_namespace_name(form->oprnamespace));
+ /* opname is not a SQL identifier, so we don't need to quote it. */
+ opname = NameStr(form->oprname);
+ oprkind = form->oprkind;
+ ReleaseSysCache(tuple);
+
+ /* Sanity check. */
+ Assert((oprkind == 'r' && list_length(node->args) == 1) ||
+ (oprkind == 'l' && list_length(node->args) == 1) ||
+ (oprkind == 'b' && list_length(node->args) == 2));
+
+ /* Always parenthesize the expression. */
+ appendStringInfoChar(buf, '(');
+
+ /* Deparse first operand. */
+ arg = list_head(node->args);
+ if (oprkind == 'r' || oprkind == 'b')
+ {
+ deparseExpr(buf, lfirst(arg), root);
+ appendStringInfoChar(buf, ' ');
+ }
+
+ /* Deparse fully qualified operator name. */
+ appendStringInfo(buf, "OPERATOR(%s.%s)", opnspname, opname);
+
+ /* Deparse last operand. */
+ arg = list_tail(node->args);
+ if (oprkind == 'l' || oprkind == 'b')
+ {
+ appendStringInfoChar(buf, ' ');
+ deparseExpr(buf, lfirst(arg), root);
+ }
+
+ appendStringInfoChar(buf, ')');
+ }
+
+ static void
+ deparseArrayRef(StringInfo buf,
+ ArrayRef *node,
+ PlannerInfo *root)
+ {
+ ListCell *lowlist_item;
+ ListCell *uplist_item;
+
+ /* Always parenthesize the expression. */
+ appendStringInfoChar(buf, '(');
+
+ /* Deparse referenced array expression first. */
+ appendStringInfoChar(buf, '(');
+ deparseExpr(buf, node->refexpr, root);
+ appendStringInfoChar(buf, ')');
+
+ /* Deparse subscripts expression. */
+ lowlist_item = list_head(node->reflowerindexpr); /* could be NULL */
+ foreach(uplist_item, node->refupperindexpr)
+ {
+ appendStringInfoChar(buf, '[');
+ if (lowlist_item)
+ {
+ deparseExpr(buf, lfirst(lowlist_item), root);
+ appendStringInfoChar(buf, ':');
+ lowlist_item = lnext(lowlist_item);
+ }
+ deparseExpr(buf, lfirst(uplist_item), root);
+ appendStringInfoChar(buf, ']');
+ }
+
+ appendStringInfoChar(buf, ')');
+ }
+
+
+ /*
+ * Deparse given array of something into buf.
+ */
+ static void
+ deparseArrayExpr(StringInfo buf,
+ ArrayExpr *node,
+ PlannerInfo *root)
+ {
+ ListCell *lc;
+ bool first = true;
+
+ appendStringInfo(buf, "ARRAY[");
+ foreach(lc, node->elements)
+ {
+ if (!first)
+ appendStringInfo(buf, ", ");
+ deparseExpr(buf, lfirst(lc), root);
+
+ first = false;
+ }
+ appendStringInfoChar(buf, ']');
+
+ /* If the array is empty, we need explicit cast to the array type. */
+ if (node->elements == NIL)
+ {
+ char *typname;
+
+ typname = format_type_with_typemod(node->array_typeid, -1);
+ appendStringInfo(buf, "::%s", typname);
+ }
+ }
+
+ /*
+ * Returns true if given expr is safe to evaluate on the foreign server. If
+ * result is true, extra information has_param tells whether given expression
+ * contains any Param node. This is useful to determine whether the expression
+ * can be used in remote EXPLAIN.
+ */
+ static bool
+ is_foreign_expr(PlannerInfo *root,
+ RelOptInfo *baserel,
+ Expr *expr,
+ bool *has_param)
+ {
+ foreign_executable_cxt context;
+ context.root = root;
+ context.foreignrel = baserel;
+ context.has_param = false;
+
+ /*
+ * An expression which includes any mutable function can't be pushed down
+ * because it's result is not stable. For example, pushing now() down to
+ * remote side would cause confusion from the clock offset.
+ * If we have routine mapping infrastructure in future release, we will be
+ * able to choose function to be pushed down in finer granularity.
+ */
+ if (contain_mutable_functions((Node *) expr))
+ return false;
+
+ /*
+ * Check that the expression consists of nodes which are known as safe to
+ * be pushed down.
+ */
+ if (foreign_expr_walker((Node *) expr, &context))
+ return false;
+
+ /*
+ * Tell caller whether the given expression contains any Param node, which
+ * can't be used in EXPLAIN statement before executor starts.
+ */
+ *has_param = context.has_param;
+
+ return true;
+ }
+
+ /*
+ * Return true if node includes any node which is not known as safe to be
+ * pushed down.
+ */
+ static bool
+ foreign_expr_walker(Node *node, foreign_executable_cxt *context)
+ {
+ if (node == NULL)
+ return false;
+
+ /*
+ * Special case handling for List; expression_tree_walker handles List as
+ * well as other Expr nodes. For instance, List is used in RestrictInfo
+ * for args of FuncExpr node.
+ *
+ * Although the comments of expression_tree_walker mention that
+ * RangeTblRef, FromExpr, JoinExpr, and SetOperationStmt are handled as
+ * well, but we don't care them because they are not used in RestrictInfo.
+ * If one of them was passed into, default label catches it and give up
+ * traversing.
+ */
+ if (IsA(node, List))
+ {
+ ListCell *lc;
+
+ foreach(lc, (List *) node)
+ {
+ if (foreign_expr_walker(lfirst(lc), context))
+ return true;
+ }
+ return false;
+ }
+
+ /*
+ * If return type of given expression is not built-in, it can't be pushed
+ * down because it might has incompatible semantics on remote side.
+ */
+ if (!is_builtin(exprType(node)))
+ return true;
+
+ switch (nodeTag(node))
+ {
+ case T_Const:
+ /*
+ * Using anyarray and/or anyenum in remote query is not supported.
+ */
+ if (((Const *) node)->consttype == ANYARRAYOID ||
+ ((Const *) node)->consttype == ANYNONARRAYOID)
+ return true;
+ break;
+ case T_BoolExpr:
+ case T_NullTest:
+ case T_DistinctExpr:
+ case T_RelabelType:
+ /*
+ * 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 at the tail of this function.
+ */
+ break;
+ /*
+ * If function used by the expression is not built-in, it can't be
+ * pushed down because it might has incompatible semantics on remote
+ * side.
+ */
+ case T_FuncExpr:
+ {
+ FuncExpr *fe = (FuncExpr *) node;
+ if (!is_builtin(fe->funcid))
+ return true;
+ }
+ break;
+ case T_Param:
+ /*
+ * Only external parameters can be pushed down.:
+ */
+ {
+ if (((Param *) node)->paramkind != PARAM_EXTERN)
+ return true;
+
+ /* Mark that this expression contains Param node. */
+ context->has_param = true;
+ }
+ break;
+ case T_ScalarArrayOpExpr:
+ /*
+ * Only built-in operators can be pushed down. In addition,
+ * underlying function must be built-in and immutable, but we don't
+ * check volatility here; such check must be done already with
+ * contain_mutable_functions.
+ */
+ {
+ ScalarArrayOpExpr *oe = (ScalarArrayOpExpr *) node;
+
+ if (!is_builtin(oe->opno) || !is_builtin(oe->opfuncid))
+ return true;
+
+ /*
+ * If the operator takes collatable type as operands, we push
+ * down only "=" and "<>" which are not affected by collation.
+ * Other operators might be safe about collation, but these two
+ * seem enogh to cover practical use cases.
+ */
+ if (exprInputCollation(node) != InvalidOid)
+ {
+ char *opname = get_opname(oe->opno);
+
+ if (strcmp(opname, "=") != 0 && strcmp(opname, "<>") != 0)
+ return true;
+ }
+
+ /* operands are checked later */
+ }
+ break;
+ case T_OpExpr:
+ /*
+ * Only built-in operators can be pushed down. In addition,
+ * underlying function must be built-in and immutable, but we don't
+ * check volatility here; such check must be done already with
+ * contain_mutable_functions.
+ */
+ {
+ OpExpr *oe = (OpExpr *) node;
+
+ if (!is_builtin(oe->opno) || !is_builtin(oe->opfuncid))
+ return true;
+
+ /*
+ * If the operator takes collatable type as operands, we push
+ * down only "=" and "<>" which are not affected by collation.
+ * Other operators might be safe about collation, but these two
+ * seem enogh to cover practical use cases.
+ */
+ if (exprInputCollation(node) != InvalidOid)
+ {
+ char *opname = get_opname(oe->opno);
+
+ if (strcmp(opname, "=") != 0 && strcmp(opname, "<>") != 0)
+ return true;
+ }
+
+ /* operands are checked later */
+ }
+ 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;
+ foreign_executable_cxt *f_context;
+
+ f_context = (foreign_executable_cxt *) context;
+ if (var->varno != f_context->foreignrel->relid ||
+ var->varlevelsup != 0)
+ return true;
+ }
+ break;
+ case T_ArrayRef:
+ /*
+ * ArrayRef which holds non-built-in typed elements can't be pushed
+ * down.
+ */
+ {
+ ArrayRef *ar = (ArrayRef *) node;;
+
+ if (!is_builtin(ar->refelemtype))
+ return true;
+
+ /* Assignment should not be in restrictions. */
+ if (ar->refassgnexpr != NULL)
+ return true;
+ }
+ break;
+ case T_ArrayExpr:
+ /*
+ * ArrayExpr which holds non-built-in typed elements can't be pushed
+ * down.
+ */
+ {
+ if (!is_builtin(((ArrayExpr *) node)->element_typeid))
+ return true;
+ }
+ break;
+ default:
+ {
+ ereport(DEBUG3,
+ (errmsg("expression is too complex"),
+ errdetail("%s", nodeToString(node))));
+ return true;
+ }
+ break;
+ }
+
+ return expression_tree_walker(node, foreign_expr_walker, context);
+ }
+
+ /*
+ * Return true if given object is one of built-in objects.
+ */
+ static bool
+ is_builtin(Oid oid)
+ {
+ return (oid < FirstNormalObjectId);
+ }
+
+ /*
+ * Deparse WHERE clause from given list of RestrictInfo and append them to buf.
+ * We assume that buf already holds a SQL statement which ends with valid WHERE
+ * clause.
+ */
+ void
+ appendWhereClause(StringInfo buf,
+ bool has_where,
+ List *exprs,
+ PlannerInfo *root)
+ {
+ bool first = true;
+ ListCell *lc;
+
+ if (!has_where)
+ appendStringInfo(buf, " WHERE ");
+
+ foreach(lc, exprs)
+ {
+ RestrictInfo *ri = (RestrictInfo *) lfirst(lc);
+
+ /* Connect expressions with "AND" and parenthesize whole condition. */
+ if (!first)
+ appendStringInfo(buf, " AND ");
+
+ appendStringInfoChar(buf, '(');
+ deparseExpr(buf, ri->clause, root);
+ appendStringInfoChar(buf, ')');
+
+ first = false;
+ }
+ }
diff --git a/contrib/pgsql_fdw/expected/pgsql_fdw.out b/contrib/pgsql_fdw/expected/pgsql_fdw.out
index 67fd979..cdbdd63 100644
*** a/contrib/pgsql_fdw/expected/pgsql_fdw.out
--- b/contrib/pgsql_fdw/expected/pgsql_fdw.out
*************** SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.
*** 229,244 ****
(10 rows)
-- with WHERE clause
! EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
! QUERY PLAN
! ------------------------------------------------------------------------------------------------------------------
Foreign Scan on public.ft1 t1
Output: c1, c2, c3, c4, c5, c6, c7
! Filter: ((t1.c1 = 101) AND ((t1.c6)::text = '1'::text) AND (t1.c7 = '1'::bpchar))
! Remote SQL: DECLARE pgsql_fdw_cursor_4 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
(4 rows)
! SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
c1 | c2 | c3 | c4 | c5 | c6 | c7
-----+----+-------+------------------------------+--------------------------+----+------------
101 | 1 | 00101 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970 | 1 | 1
--- 229,244 ----
(10 rows)
-- with WHERE clause
! EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 >= '1';
! QUERY PLAN
! -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Foreign Scan on public.ft1 t1
Output: c1, c2, c3, c4, c5, c6, c7
! Filter: (t1.c7 >= '1'::bpchar)
! Remote SQL: DECLARE pgsql_fdw_cursor_4 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.=) 101)) AND (((c6)::text OPERATOR(pg_catalog.=) '1'::text))
(4 rows)
! SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 >= '1';
c1 | c2 | c3 | c4 | c5 | c6 | c7
-----+----+-------+------------------------------+--------------------------+----+------------
101 | 1 | 00101 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970 | 1 | 1
*************** EXPLAIN (COSTS false) SELECT * FROM ft1
*** 343,362 ****
(3 rows)
EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = abs(t1.c2);
! QUERY PLAN
! ---------------------------------------------------------------------
Foreign Scan on ft1 t1
! Filter: (c1 = abs(c2))
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
! (3 rows)
EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = t1.c2;
! QUERY PLAN
! ---------------------------------------------------------------------
Foreign Scan on ft1 t1
! Filter: (c1 = c2)
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
! (3 rows)
-- ===================================================================
-- parameterized queries
--- 343,433 ----
(3 rows)
EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = abs(t1.c2);
! QUERY PLAN
! -------------------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.=) pg_catalog.abs(c2)))
! (2 rows)
EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = t1.c2;
! QUERY PLAN
! ---------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.=) c2))
! (2 rows)
!
! -- ===================================================================
! -- WHERE push down
! -- ===================================================================
! EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = 1; -- Var, OpExpr(b), Const
! QUERY PLAN
! --------------------------------------------------------------------------------------------------------------
! Foreign Scan on ft1 t1
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.=) 1))
! (2 rows)
!
! EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = 100 AND t1.c2 = 0; -- BoolExpr
! QUERY PLAN
! ----------------------------------------------------------------------------------------------------------------------------------------------------
! Foreign Scan on ft1 t1
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.=) 100)) AND ((c2 OPERATOR(pg_catalog.=) 0))
! (2 rows)
!
! EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE c1 IS NULL; -- NullTest
! QUERY PLAN
! ---------------------------------------------------------------------------------------------
! Foreign Scan on ft1 t1
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" IS NULL))
! (2 rows)
!
! EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE c1 IS NOT NULL; -- NullTest
! QUERY PLAN
! -------------------------------------------------------------------------------------------------
! Foreign Scan on ft1 t1
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" IS NOT NULL))
! (2 rows)
!
! EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE round(abs(c1), 0) = 1; -- FuncExpr
! QUERY PLAN
! ------------------------------------------------------------------------------------------------------------------------------------------------------------
! Foreign Scan on ft1 t1
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE ((pg_catalog.round(pg_catalog.abs("C 1"), 0) OPERATOR(pg_catalog.=) 1::numeric))
! (2 rows)
!
! EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE c1 = -c1; -- OpExpr(l)
! QUERY PLAN
! -------------------------------------------------------------------------------------------------------------------------------------------
! Foreign Scan on ft1 t1
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.=) (OPERATOR(pg_catalog.-) "C 1")))
! (2 rows)
!
! EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE 1 = c1!; -- OpExpr(r)
! QUERY PLAN
! ------------------------------------------------------------------------------------------------------------------------------------------------
! Foreign Scan on ft1 t1
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE ((1::numeric OPERATOR(pg_catalog.=) ("C 1" OPERATOR(pg_catalog.!))))
! (2 rows)
!
! EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE (c1 IS NOT NULL) IS DISTINCT FROM (c1 IS NOT NULL); -- DistinctExpr
! QUERY PLAN
! --------------------------------------------------------------------------------------------------------------------------------------
! Foreign Scan on ft1 t1
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" IS NOT NULL) IS DISTINCT FROM ("C 1" IS NOT NULL))
! (2 rows)
!
! EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE c1 = ANY(ARRAY[c2, 1, c1 + 0]); -- ScalarArrayOpExpr
! QUERY PLAN
! -----------------------------------------------------------------------------------------------------------------------------------------------------------------
! Foreign Scan on ft1 t1
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.=) ANY (ARRAY[c2, 1, ("C 1" OPERATOR(pg_catalog.+) 0)])))
! (2 rows)
!
! EXPLAIN (COSTS false) SELECT * FROM ft1 ft WHERE c1 = (ARRAY[c1,c2,3])[1]; -- ArrayRef
! QUERY PLAN
! ---------------------------------------------------------------------------------------------------------------------------------------
! Foreign Scan on ft1 ft
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.=) ((ARRAY["C 1", c2, 3])[1])))
! (2 rows)
-- ===================================================================
-- parameterized queries
*************** EXPLAIN (COSTS false) SELECT * FROM ft1
*** 364,380 ****
-- simple join
PREPARE st1(int, int) AS SELECT t1.c3, t2.c3 FROM ft1 t1, ft2 t2 WHERE t1.c1 = $1 AND t2.c1 = $2;
EXPLAIN (COSTS false) EXECUTE st1(1, 2);
! QUERY PLAN
! -------------------------------------------------------------------------------------------
Nested Loop
-> Foreign Scan on ft1 t1
! Filter: (c1 = 1)
! Remote SQL: SELECT "C 1", NULL, c3, NULL, NULL, NULL, NULL FROM "S 1"."T 1"
! -> Materialize
! -> Foreign Scan on ft2 t2
! Filter: (c1 = 2)
! Remote SQL: SELECT "C 1", NULL, c3, NULL, NULL, NULL, NULL FROM "S 1"."T 1"
! (8 rows)
EXECUTE st1(1, 1);
c3 | c3
--- 435,448 ----
-- simple join
PREPARE st1(int, int) AS SELECT t1.c3, t2.c3 FROM ft1 t1, ft2 t2 WHERE t1.c1 = $1 AND t2.c1 = $2;
EXPLAIN (COSTS false) EXECUTE st1(1, 2);
! QUERY PLAN
! ------------------------------------------------------------------------------------------------------------------------------
Nested Loop
-> Foreign Scan on ft1 t1
! Remote SQL: SELECT "C 1", NULL, c3, NULL, NULL, NULL, NULL FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.=) 1))
! -> Foreign Scan on ft2 t2
! Remote SQL: SELECT "C 1", NULL, c3, NULL, NULL, NULL, NULL FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.=) 2))
! (5 rows)
EXECUTE st1(1, 1);
c3 | c3
*************** EXECUTE st1(101, 101);
*** 391,411 ****
-- subquery using stable function (can't be pushed down)
PREPARE st2(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c4) = 6) ORDER BY c1;
EXPLAIN (COSTS false) EXECUTE st2(10, 20);
! QUERY PLAN
! ------------------------------------------------------------------------------------------------------
Sort
Sort Key: t1.c1
-> Hash Join
Hash Cond: (t1.c3 = t2.c3)
-> Foreign Scan on ft1 t1
! Filter: (c1 < 20)
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
-> Hash
-> HashAggregate
-> Foreign Scan on ft2 t2
! Filter: ((c1 > 10) AND (date_part('dow'::text, c4) = 6::double precision))
! Remote SQL: SELECT "C 1", NULL, c3, c4, NULL, NULL, NULL FROM "S 1"."T 1"
! (12 rows)
EXECUTE st2(10, 20);
c1 | c2 | c3 | c4 | c5 | c6 | c7
--- 459,478 ----
-- subquery using stable function (can't be pushed down)
PREPARE st2(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c4) = 6) ORDER BY c1;
EXPLAIN (COSTS false) EXECUTE st2(10, 20);
! QUERY PLAN
! -----------------------------------------------------------------------------------------------------------------------------------------------
Sort
Sort Key: t1.c1
-> Hash Join
Hash Cond: (t1.c3 = t2.c3)
-> Foreign Scan on ft1 t1
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.<) 20))
-> Hash
-> HashAggregate
-> Foreign Scan on ft2 t2
! Filter: (date_part('dow'::text, c4) = 6::double precision)
! Remote SQL: SELECT "C 1", NULL, c3, c4, NULL, NULL, NULL FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.>) 10))
! (11 rows)
EXECUTE st2(10, 20);
c1 | c2 | c3 | c4 | c5 | c6 | c7
*************** EXECUTE st1(101, 101);
*** 422,442 ****
-- subquery using immutable function (can be pushed down)
PREPARE st3(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c5) = 6) ORDER BY c1;
EXPLAIN (COSTS false) EXECUTE st3(10, 20);
! QUERY PLAN
! ------------------------------------------------------------------------------------------------------
Sort
Sort Key: t1.c1
! -> Hash Join
! Hash Cond: (t1.c3 = t2.c3)
-> Foreign Scan on ft1 t1
! Filter: (c1 < 20)
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
! -> Hash
! -> HashAggregate
! -> Foreign Scan on ft2 t2
! Filter: ((c1 > 10) AND (date_part('dow'::text, c5) = 6::double precision))
! Remote SQL: SELECT "C 1", NULL, c3, NULL, c5, NULL, NULL FROM "S 1"."T 1"
! (12 rows)
EXECUTE st3(10, 20);
c1 | c2 | c3 | c4 | c5 | c6 | c7
--- 489,506 ----
-- subquery using immutable function (can be pushed down)
PREPARE st3(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c5) = 6) ORDER BY c1;
EXPLAIN (COSTS false) EXECUTE st3(10, 20);
! QUERY PLAN
! ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Sort
Sort Key: t1.c1
! -> Nested Loop Semi Join
! Join Filter: (t1.c3 = t2.c3)
-> Foreign Scan on ft1 t1
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.<) 20))
! -> Materialize
! -> Foreign Scan on ft2 t2
! Remote SQL: SELECT "C 1", NULL, c3, NULL, c5, NULL, NULL FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.>) 10)) AND ((pg_catalog.date_part('dow'::text, c5) OPERATOR(pg_catalog.=) 6::double precision))
! (9 rows)
EXECUTE st3(10, 20);
c1 | c2 | c3 | c4 | c5 | c6 | c7
*************** EXECUTE st3(20, 30);
*** 453,504 ****
-- custom plan should be chosen
PREPARE st4(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 = $1;
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! ---------------------------------------------------------------------
Foreign Scan on ft1 t1
! Filter: (c1 = 1)
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
! (3 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! ---------------------------------------------------------------------
Foreign Scan on ft1 t1
! Filter: (c1 = 1)
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
! (3 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! ---------------------------------------------------------------------
Foreign Scan on ft1 t1
! Filter: (c1 = 1)
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
! (3 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! ---------------------------------------------------------------------
Foreign Scan on ft1 t1
! Filter: (c1 = 1)
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
! (3 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! ---------------------------------------------------------------------
Foreign Scan on ft1 t1
! Filter: (c1 = 1)
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
! (3 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! ---------------------------------------------------------------------
Foreign Scan on ft1 t1
! Filter: (c1 = $1)
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
! (3 rows)
-- cleanup
DEALLOCATE st1;
--- 517,562 ----
-- custom plan should be chosen
PREPARE st4(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 = $1;
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! --------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.=) 1))
! (2 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! --------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.=) 1))
! (2 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! --------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.=) 1))
! (2 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! --------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.=) 1))
! (2 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! --------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.=) 1))
! (2 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! ---------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.=) $1))
! (2 rows)
-- cleanup
DEALLOCATE st1;
diff --git a/contrib/pgsql_fdw/pgsql_fdw.c b/contrib/pgsql_fdw/pgsql_fdw.c
index 61527a9..2424fb4 100644
*** a/contrib/pgsql_fdw/pgsql_fdw.c
--- b/contrib/pgsql_fdw/pgsql_fdw.c
***************
*** 17,23 ****
#include "catalog/pg_foreign_table.h"
#include "commands/defrem.h"
#include "commands/explain.h"
- #include "foreign/fdwapi.h"
#include "funcapi.h"
#include "miscadmin.h"
#include "optimizer/cost.h"
--- 17,22 ----
*************** PG_MODULE_MAGIC;
*** 60,65 ****
--- 59,69 ----
#define CURSOR_NAME_FORMAT "pgsql_fdw_cursor_%u"
static uint32 cursor_id = 0;
+ /* Convenient macros for accessing the first record of PGresult. */
+ #define PGRES_VAL0(col) (PQgetvalue(res, 0, (col)))
+ #define PGRES_NULL0(col) (PQgetisnull(res, 0, (col)))
+
+
/*
* FDW-specific information for RelOptInfo.fdw_private. This is used to pass
* information from pgsqlGetForeignRelSize to pgsqlGetForeignPaths.
*************** typedef struct PgsqlFdwPlanState {
*** 70,77 ****
--- 74,85 ----
* GetForeignPaths.
*/
StringInfoData sql;
+ bool has_where;
Cost startup_cost;
Cost total_cost;
+ List *remote_conds;
+ List *param_conds;
+ List *local_conds;
/* Cached catalog information. */
ForeignTable *table;
*************** pgsqlGetForeignRelSize(PlannerInfo *root
*** 241,246 ****
--- 249,255 ----
RelOptInfo *baserel,
Oid foreigntableid)
{
+ PgsqlFdwPlanState *fpstate;
StringInfo sql;
ForeignTable *table;
ForeignServer *server;
*************** pgsqlGetForeignRelSize(PlannerInfo *root
*** 250,256 ****
int width;
Cost startup_cost;
Cost total_cost;
! PgsqlFdwPlanState *fpstate;
Selectivity sel;
/*
--- 259,267 ----
int width;
Cost startup_cost;
Cost total_cost;
! List *remote_conds = NIL;
! List *param_conds = NIL;
! List *local_conds = NIL;
Selectivity sel;
/*
*************** pgsqlGetForeignRelSize(PlannerInfo *root
*** 260,265 ****
--- 271,277 ----
fpstate = palloc0(sizeof(PgsqlFdwPlanState));
initStringInfo(&fpstate->sql);
sql = &fpstate->sql;
+ fpstate->has_where = false;
/* Retrieve catalog objects which are necessary to estimate rows. */
table = GetForeignTable(foreigntableid);
*************** pgsqlGetForeignRelSize(PlannerInfo *root
*** 267,289 ****
user = GetUserMapping(GetOuterUserId(), server->serverid);
/*
! * Create plain SELECT statement with no WHERE clause for this scan in
! * order to obtain meaningful rows estimation by executing EXPLAIN on
! * remote server.
*/
deparseSimpleSql(sql, foreigntableid, root, baserel, table);
conn = GetConnection(server, user, false);
get_remote_estimate(sql->data, conn, &rows, &width,
&startup_cost, &total_cost);
ReleaseConnection(conn);
/*
! * Estimate selectivity of local filtering by calling
! * clauselist_selectivity() against baserestrictinfo, and modify rows
! * estimate with it.
*/
! sel = clauselist_selectivity(root, baserel->baserestrictinfo,
! baserel->relid, JOIN_INNER, NULL);
baserel->rows = rows * sel;
baserel->width = width;
--- 279,318 ----
user = GetUserMapping(GetOuterUserId(), server->serverid);
/*
! * Construct remote query which consists of SELECT, FROM, and WHERE
! * clauses, but conditions contain any Param node are excluded because
! * placeholder can't be used in EXPLAIN statement. Such conditions are
! * appended later.
*/
+ sortConditions(root, baserel, &remote_conds, ¶m_conds, &local_conds);
deparseSimpleSql(sql, foreigntableid, root, baserel, table);
+ if (list_length(remote_conds) > 0)
+ {
+ appendWhereClause(sql, fpstate->has_where, remote_conds, root);
+ fpstate->has_where = true;
+ }
conn = GetConnection(server, user, false);
get_remote_estimate(sql->data, conn, &rows, &width,
&startup_cost, &total_cost);
ReleaseConnection(conn);
+ if (list_length(param_conds) > 0)
+ {
+ appendWhereClause(sql, fpstate->has_where, param_conds, root);
+ fpstate->has_where = true;
+ }
/*
! * Estimate selectivity of conditions which are not used in remote EXPLAIN
! * by calling clauselist_selectivity(). The best we can do for
! * parameterized condition is to estimate selectivity on the basis of local
! * statistics. When we actually obtain result rows, such conditions are
! * deparsed into remote query and reduce rows transferred.
*/
! sel = 1.0;
! sel *= clauselist_selectivity(root, param_conds,
! baserel->relid, JOIN_INNER, NULL);
! sel *= clauselist_selectivity(root, local_conds,
! baserel->relid, JOIN_INNER, NULL);
baserel->rows = rows * sel;
baserel->width = width;
*************** pgsqlGetForeignRelSize(PlannerInfo *root
*** 293,298 ****
--- 322,330 ----
*/
fpstate->startup_cost = startup_cost;
fpstate->total_cost = total_cost;
+ fpstate->remote_conds = remote_conds;
+ fpstate->param_conds = param_conds;
+ fpstate->local_conds = local_conds;
fpstate->table = table;
fpstate->server = server;
baserel->fdw_private = (void *) fpstate;
*************** pgsqlGetForeignPlan(PlannerInfo *root,
*** 373,387 ****
char *sql;
ForeignTable *table;
ForeignServer *server;
/*
! * We have no native ability to evaluate restriction clauses, so we just
! * put all the scan_clauses into the plan node's qual list for the
! * executor to check. So all we have to do here is strip RestrictInfo
! * nodes from the clauses and ignore pseudoconstants (which will be
! * handled elsewhere).
*/
! scan_clauses = extract_actual_clauses(scan_clauses, false);
/*
* Use specified fetch_count instead of default value, if any. Foreign
--- 405,425 ----
char *sql;
ForeignTable *table;
ForeignServer *server;
+ List *fdw_exprs = NIL;
+ List *local_exprs = NIL;
/*
! * We need lists of Expr other than the lists of RestrictInfo. Now we can
! * merge remote_conds and param_conds into fdw_exprs, because they are
! * evaluated on remote side for actual remote query.
*/
! foreach(lc, fpstate->remote_conds)
! fdw_exprs = lappend(fdw_exprs, ((RestrictInfo *) lfirst(lc))->clause);
! foreach(lc, fpstate->param_conds)
! fdw_exprs = lappend(fdw_exprs, ((RestrictInfo *) lfirst(lc))->clause);
! foreach(lc, fpstate->local_conds)
! local_exprs = lappend(local_exprs,
! ((RestrictInfo *) lfirst(lc))->clause);
/*
* Use specified fetch_count instead of default value, if any. Foreign
*************** pgsqlGetForeignPlan(PlannerInfo *root,
*** 410,418 ****
fetch_count = strtol(defGetString(def), NULL, 10);
elog(DEBUG1, "relid=%u fetch_count=%d", foreigntableid, fetch_count);
! /*
! * Construct cursor name with sequential value.
! */
sprintf(name, CURSOR_NAME_FORMAT, cursor_id++);
/*
--- 448,454 ----
fetch_count = strtol(defGetString(def), NULL, 10);
elog(DEBUG1, "relid=%u fetch_count=%d", foreigntableid, fetch_count);
! /* Construct cursor name from sequential value */
sprintf(name, CURSOR_NAME_FORMAT, cursor_id++);
/*
*************** pgsqlGetForeignPlan(PlannerInfo *root,
*** 439,449 ****
appendStringInfo(&cursor, "CLOSE %s", name);
fdw_private = lappend(fdw_private, makeString(cursor.data));
! /* Create the ForeignScan node with fdw_private of selected path. */
return make_foreignscan(tlist,
! scan_clauses,
scan_relid,
! NIL,
fdw_private);
}
--- 475,494 ----
appendStringInfo(&cursor, "CLOSE %s", name);
fdw_private = lappend(fdw_private, makeString(cursor.data));
! /*
! * Create the ForeignScan node from target list, local filtering
! * expressions, remote filtering expressions, and FDW private information.
! *
! * We remove expressions which are evaluated on remote side from qual of
! * the scan node to avoid redundant filtering. Such filter reduction
! * can be done only here, done after choosing best path, because
! * baserestrictinfo in RelOptInfo is shared by all possible paths until
! * best path is chosen.
! */
return make_foreignscan(tlist,
! local_exprs,
scan_relid,
! fdw_exprs,
fdw_private);
}
diff --git a/contrib/pgsql_fdw/pgsql_fdw.h b/contrib/pgsql_fdw/pgsql_fdw.h
index 234904e..eaf2b61 100644
*** a/contrib/pgsql_fdw/pgsql_fdw.h
--- b/contrib/pgsql_fdw/pgsql_fdw.h
***************
*** 15,20 ****
--- 15,21 ----
#define PGSQL_FDW_H
#include "postgres.h"
+ #include "foreign/fdwapi.h"
#include "foreign/foreign.h"
#include "nodes/relation.h"
*************** void deparseSimpleSql(StringInfo buf,
*** 29,33 ****
--- 30,46 ----
PlannerInfo *root,
RelOptInfo *baserel,
ForeignTable *table);
+ void appendWhereClause(StringInfo buf,
+ bool has_where,
+ List *exprs,
+ PlannerInfo *root);
+ void sortConditions(PlannerInfo *root,
+ RelOptInfo *baserel,
+ List **remote_conds,
+ List **param_conds,
+ List **local_conds);
+ void deparseExpr(StringInfo buf,
+ Expr *expr,
+ PlannerInfo *root);
#endif /* PGSQL_FDW_H */
diff --git a/contrib/pgsql_fdw/sql/pgsql_fdw.sql b/contrib/pgsql_fdw/sql/pgsql_fdw.sql
index b3fac96..23a44df 100644
*** a/contrib/pgsql_fdw/sql/pgsql_fdw.sql
--- b/contrib/pgsql_fdw/sql/pgsql_fdw.sql
*************** SELECT * FROM ft1 ORDER BY c3, c1 OFFSET
*** 149,156 ****
EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
-- with WHERE clause
! EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
! SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
-- aggregate
SELECT COUNT(*) FROM ft1 t1;
-- join two tables
--- 149,156 ----
EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
-- with WHERE clause
! EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 >= '1';
! SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 >= '1';
-- aggregate
SELECT COUNT(*) FROM ft1 t1;
-- join two tables
*************** EXPLAIN (COSTS false) SELECT * FROM ft1
*** 182,187 ****
--- 182,201 ----
EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = t1.c2;
-- ===================================================================
+ -- WHERE push down
+ -- ===================================================================
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = 1; -- Var, OpExpr(b), Const
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = 100 AND t1.c2 = 0; -- BoolExpr
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE c1 IS NULL; -- NullTest
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE c1 IS NOT NULL; -- NullTest
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE round(abs(c1), 0) = 1; -- FuncExpr
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE c1 = -c1; -- OpExpr(l)
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE 1 = c1!; -- OpExpr(r)
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE (c1 IS NOT NULL) IS DISTINCT FROM (c1 IS NOT NULL); -- DistinctExpr
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE c1 = ANY(ARRAY[c2, 1, c1 + 0]); -- ScalarArrayOpExpr
+ EXPLAIN (COSTS false) SELECT * FROM ft1 ft WHERE c1 = (ARRAY[c1,c2,3])[1]; -- ArrayRef
+
+ -- ===================================================================
-- parameterized queries
-- ===================================================================
-- simple join
pgsql_fdw_analyze.patchtext/plain; charset=Shift_JIS; name=pgsql_fdw_analyze.patchDownload
diff --git a/contrib/pgsql_fdw/expected/pgsql_fdw.out b/contrib/pgsql_fdw/expected/pgsql_fdw.out
index cdbdd63..62a2c0e 100644
*** a/contrib/pgsql_fdw/expected/pgsql_fdw.out
--- b/contrib/pgsql_fdw/expected/pgsql_fdw.out
*************** INSERT INTO "S 1"."T 2"
*** 75,80 ****
--- 75,81 ----
'AAA' || to_char(id, 'FM000')
FROM generate_series(1, 100) id;
COMMIT;
+ ANALYZE "S 1"."T 1";
-- ===================================================================
-- tests for pgsql_fdw_validator
-- ===================================================================
*************** EXECUTE st1(101, 101);
*** 459,478 ****
-- subquery using stable function (can't be pushed down)
PREPARE st2(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c4) = 6) ORDER BY c1;
EXPLAIN (COSTS false) EXECUTE st2(10, 20);
! QUERY PLAN
! -----------------------------------------------------------------------------------------------------------------------------------------------
Sort
Sort Key: t1.c1
! -> Hash Join
! Hash Cond: (t1.c3 = t2.c3)
-> Foreign Scan on ft1 t1
Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.<) 20))
! -> Hash
! -> HashAggregate
! -> Foreign Scan on ft2 t2
! Filter: (date_part('dow'::text, c4) = 6::double precision)
! Remote SQL: SELECT "C 1", NULL, c3, c4, NULL, NULL, NULL FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.>) 10))
! (11 rows)
EXECUTE st2(10, 20);
c1 | c2 | c3 | c4 | c5 | c6 | c7
--- 460,478 ----
-- subquery using stable function (can't be pushed down)
PREPARE st2(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c4) = 6) ORDER BY c1;
EXPLAIN (COSTS false) EXECUTE st2(10, 20);
! QUERY PLAN
! -----------------------------------------------------------------------------------------------------------------------------------------
Sort
Sort Key: t1.c1
! -> Nested Loop Semi Join
! Join Filter: (t1.c3 = t2.c3)
-> Foreign Scan on ft1 t1
Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.<) 20))
! -> Materialize
! -> Foreign Scan on ft2 t2
! Filter: (date_part('dow'::text, c4) = 6::double precision)
! Remote SQL: SELECT "C 1", NULL, c3, c4, NULL, NULL, NULL FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.>) 10))
! (10 rows)
EXECUTE st2(10, 20);
c1 | c2 | c3 | c4 | c5 | c6 | c7
*************** EXPLAIN (COSTS false) EXECUTE st4(1);
*** 552,561 ****
(2 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! ---------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.=) $1))
(2 rows)
-- cleanup
--- 552,561 ----
(2 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! --------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.=) 1))
(2 rows)
-- cleanup
*************** SELECT srvname, usename FROM pgsql_fdw_c
*** 584,589 ****
--- 584,605 ----
(0 rows)
-- ===================================================================
+ -- statistics management
+ -- ===================================================================
+ DELETE FROM pg_statistic WHERE starelid = '"S 1"."T 1"'::regclass AND staattnum IN (1, 3);
+ SELECT pgsql_fdw_analyze('ft1');
+ pgsql_fdw_analyze
+ -------------------
+ 5
+ (1 row)
+
+ SELECT count(*) FROM pg_statistic WHERE starelid = 'ft1'::regclass;
+ count
+ -------
+ 5
+ (1 row)
+
+ -- ===================================================================
-- subtransaction
-- ===================================================================
BEGIN;
diff --git a/contrib/pgsql_fdw/pgsql_fdw--1.0.sql b/contrib/pgsql_fdw/pgsql_fdw--1.0.sql
index b0ea2b2..033cd6f 100644
*** a/contrib/pgsql_fdw/pgsql_fdw--1.0.sql
--- b/contrib/pgsql_fdw/pgsql_fdw--1.0.sql
*************** SELECT c.srvid srvid,
*** 37,39 ****
--- 37,45 ----
JOIN pg_catalog.pg_foreign_server s ON (s.oid = c.srvid);
GRANT SELECT ON pgsql_fdw_connections TO public;
+ /* statistics management function */
+ CREATE FUNCTION pgsql_fdw_analyze(regclass)
+ RETURNS int
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C;
+
diff --git a/contrib/pgsql_fdw/pgsql_fdw.c b/contrib/pgsql_fdw/pgsql_fdw.c
index 2424fb4..22675e1 100644
*** a/contrib/pgsql_fdw/pgsql_fdw.c
--- b/contrib/pgsql_fdw/pgsql_fdw.c
***************
*** 13,31 ****
--- 13,37 ----
#include "postgres.h"
#include "fmgr.h"
+ #include "access/transam.h"
#include "catalog/pg_foreign_server.h"
#include "catalog/pg_foreign_table.h"
#include "commands/defrem.h"
#include "commands/explain.h"
+ #include "commands/vacuum.h"
#include "funcapi.h"
#include "miscadmin.h"
#include "optimizer/cost.h"
#include "optimizer/pathnode.h"
#include "optimizer/planmain.h"
#include "optimizer/restrictinfo.h"
+ #include "parser/parse_type.h"
+ #include "utils/array.h"
+ #include "utils/builtins.h"
#include "utils/lsyscache.h"
#include "utils/memutils.h"
#include "utils/rel.h"
+ #include "utils/syscache.h"
#include "pgsql_fdw.h"
#include "connection.h"
*************** typedef struct PgsqlFdwExecutionState
*** 142,147 ****
--- 148,155 ----
*/
extern Datum pgsql_fdw_handler(PG_FUNCTION_ARGS);
PG_FUNCTION_INFO_V1(pgsql_fdw_handler);
+ extern Datum pgsql_fdw_analyze(PG_FUNCTION_ARGS);
+ PG_FUNCTION_INFO_V1(pgsql_fdw_analyze);
/*
* FDW callback routines
*************** static void adjust_costs(double rows, in
*** 178,183 ****
--- 186,192 ----
static void execute_query(ForeignScanState *node);
static PGresult *fetch_result(ForeignScanState *node);
static void store_result(ForeignScanState *node, PGresult *res);
+ static void store_remote_stats(PGresult *res, VacAttrStats *stats);
/* Exported functions, but not written in pgsql_fdw.h. */
void _PG_init(void);
*************** store_result(ForeignScanState *node, PGr
*** 1085,1087 ****
--- 1094,1682 ----
tuplestore_donestoring(festate->tuples);
}
+ /*
+ * Update statistics of a foreign table managed by pgsql_fdw by copying remote
+ * statistics into local pg_statistic and pg_class. If remote version is lower
+ * than 9.2, columns for slot 5 are ignored.
+ */
+ Datum
+ pgsql_fdw_analyze(PG_FUNCTION_ARGS)
+ {
+ Oid relid = PG_GETARG_OID(0);
+ Relation relation;
+ ListCell *lc;
+ int nstats = 0;
+ MemoryContext anl_context;
+ MemoryContext caller_context;
+ const char *nspname;
+ const char *relname;
+ ForeignTable *table;
+ ForeignServer *server;
+ ForeignDataWrapper *wrapper;
+ FdwRoutine *routine;
+ UserMapping *user;
+ PGconn *conn;
+ PGresult *res = NULL;
+ StringInfoData sql;
+ char *endp;
+ double reltuples;
+ int relpages;
+ int attr_cnt;
+ int tcnt;
+ int i;
+ VacAttrStats **vacattrstats;
+
+ /* Relation oid is required. */
+ if (PG_ARGISNULL(0))
+ ereport(ERROR,
+ (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
+ errmsg("relation oid must not be NULL")));
+
+ /* Use short-lived expression context. */
+ anl_context = AllocSetContextCreate(CurrentMemoryContext,
+ "Analyze",
+ ALLOCSET_DEFAULT_MINSIZE,
+ ALLOCSET_DEFAULT_INITSIZE,
+ ALLOCSET_DEFAULT_MAXSIZE);
+ caller_context = MemoryContextSwitchTo(anl_context);
+
+ /*
+ * Open the relation, getting ShareUpdateExclusiveLock to ensure that two
+ * ANALYZE function don't run on it concurrently. Of course, target must
+ * be a foreign table of pgsql_fdw.
+ */
+ relation = relation_open(relid, ShareUpdateExclusiveLock);
+ if (get_rel_relkind(relid) != RELKIND_FOREIGN_TABLE)
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("\"%s\" is not a foreign table",
+ RelationGetRelationName(relation))));
+
+ table = GetForeignTable(relid);
+ server = GetForeignServer(table->serverid);
+ wrapper = GetForeignDataWrapper(server->fdwid);
+ routine = GetFdwRoutine(wrapper->fdwhandler);
+ if (routine != &fdwroutine)
+ ereport(ERROR,
+ (errcode(ERRCODE_FDW_TABLE_NOT_FOUND),
+ errmsg("foreign table \"%s\" cannot handled by pgsql_fdw",
+ RelationGetRelationName(relation))));
+
+ /*
+ * Establish connection against the foreign server to retrieve remote
+ * statistics data.
+ */
+ user = GetUserMapping(GetOuterUserId(), table->serverid);
+ conn = GetConnection(server, user, false);
+
+ /* Construct SQL statement to retrieve per-relation statistics. */
+ foreach(lc, table->options)
+ {
+ DefElem *def = (DefElem *) lfirst(lc);
+
+ if (strcmp(def->defname, "nspname") == 0)
+ nspname = defGetString(def);
+ else if (strcmp(def->defname, "relname") == 0)
+ relname = defGetString(def);
+
+ }
+ if (nspname == NULL)
+ nspname = get_namespace_name(get_rel_namespace(relid));
+ nspname = quote_identifier(nspname);
+ if (relname == NULL)
+ relname = get_rel_name(relid);
+ relname = quote_identifier(relname);
+
+ /*
+ * Get per-relation statistics from remote pg_class. We don't fetch
+ * relallvisible, which is used for estimation about index-only scan,
+ * because index-only scan is not available on foreign tables.
+ *
+ * XXX Should we get summary of whole inherit-tree for inherited table?
+ */
+ initStringInfo(&sql);
+ appendStringInfo(&sql,
+ "SELECT relpages, "
+ " reltuples "
+ " FROM pg_catalog.pg_class "
+ " WHERE oid = '%s.%s'::regclass",
+ nspname, relname);
+
+ PG_TRY();
+ {
+ res = PQexec(conn, sql.data);
+
+ if (PQresultStatus(res) != PGRES_TUPLES_OK)
+ ereport(ERROR,
+ (errmsg("could not get per-table statistics"),
+ errdetail("%s", PQerrorMessage(conn))));
+ if (PQntuples(res) != 1)
+ ereport(ERROR,
+ (errmsg("invalid number of tuples: %d", PQntuples(res))));
+
+ relpages = strtol(PGRES_VAL0(0), &endp, 10);
+ if (*endp != '\0')
+ ereport(ERROR,
+ (errmsg("invalid format for relpages: %s", PGRES_VAL0(0))));
+ reltuples = strtod(PGRES_VAL0(1), &endp);
+ if (*endp != '\0')
+ ereport(ERROR,
+ (errmsg("invalid format for reltuples: %s", PGRES_VAL0(1))));
+ PQclear(res);
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+
+ /*
+ * Get per-column statistics from remote pg_statistic. Note that some of
+ * local columns might be dropped.
+ *
+ * FIXME When porting this routine as ANALYZE handler, we need to care
+ * vacstmt->va_cols here.
+ *
+ */
+ #ifdef NOT_USED
+ if (va_cols != NIL)
+ {
+ ListCell *le;
+
+ vacattrstats = (VacAttrStats **) palloc(list_length(va_cols) *
+ sizeof(VacAttrStats *));
+ tcnt = 0;
+ foreach(le, va_cols)
+ {
+ char *col = strVal(lfirst(le));
+
+ i = attnameAttNum(relation, col, false);
+ if (i == InvalidAttrNumber)
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_COLUMN),
+ errmsg("column \"%s\" of relation \"%s\" does not exist",
+ col, RelationGetRelationName(relation))));
+ vacattrstats[tcnt] = examine_attribute(relation, i, NULL);
+ if (vacattrstats[tcnt] != NULL)
+ tcnt++;
+ }
+ attr_cnt = tcnt;
+ }
+ else
+ #endif
+ {
+ attr_cnt = relation->rd_att->natts;
+ vacattrstats = (VacAttrStats **)
+ palloc(attr_cnt * sizeof(VacAttrStats *));
+ tcnt = 0;
+ for (i = 1; i <= attr_cnt; i++)
+ {
+ vacattrstats[tcnt] = examine_attribute(relation, i, NULL);
+ if (vacattrstats[tcnt] != NULL)
+ tcnt++;
+ }
+ attr_cnt = tcnt;
+ }
+
+ /* Retrieve statistics for each requested column from foreign server. */
+ for (i = 0; i < attr_cnt; i++)
+ {
+ List *options;
+ ListCell *lc;
+ const char *colname = NULL;
+ StringInfoData sql_stat;
+
+ /* Use colname FDW option to determine remote attribute, if any. */
+ options = GetForeignColumnOptions(relid,
+ vacattrstats[i]->attr->attnum);
+ foreach(lc, options)
+ {
+ DefElem *def = (DefElem *) lfirst(lc);
+
+ if (strcmp(def->defname, "colname") == 0)
+ {
+ colname = defGetString(def);
+ break;
+ }
+ }
+ if (colname == NULL)
+ colname = get_attname(relation->rd_id,
+ vacattrstats[i]->attr->attnum);
+
+ /* No need to quote column name here. */
+ initStringInfo(&sql_stat);
+ if (PQserverVersion(conn) >= 90200)
+ {
+ appendStringInfo(&sql_stat,
+ "SELECT starelid,"
+ " staattnum,"
+ /* fixed value is used for stainherit, see below */
+ " stanullfrac,"
+ " stawidth,"
+ " stadistinct,"
+ " stakind1,"
+ " stakind2,"
+ " stakind3,"
+ " stakind4,"
+ " stakind5,"
+ " staop1,"
+ " staop2,"
+ " staop3,"
+ " staop4,"
+ " staop5,"
+ " stanumbers1,"
+ " stanumbers2,"
+ " stanumbers3,"
+ " stanumbers4,"
+ " stanumbers5,"
+ " stavalues1,"
+ " stavalues2,"
+ " stavalues3,"
+ " stavalues4,"
+ " stavalues5"
+ " FROM pg_catalog.pg_statistic "
+ " WHERE starelid = '%s.%s'::regclass "
+ " AND staattnum = ("
+ " SELECT attnum "
+ " FROM pg_catalog.pg_attribute "
+ " WHERE attrelid = '%s.%s'::regclass "
+ " AND attname = '%s')",
+ nspname, relname, nspname, relname, colname);
+ }
+ else
+ {
+ appendStringInfo(&sql_stat,
+ "SELECT starelid,"
+ " staattnum,"
+ /* fixed value is used for stainherit, see below */
+ " stanullfrac,"
+ " stawidth,"
+ " stadistinct,"
+ " stakind1,"
+ " stakind2,"
+ " stakind3,"
+ " stakind4,"
+ " staop1,"
+ " staop2,"
+ " staop3,"
+ " staop4,"
+ " stanumbers1,"
+ " stanumbers2,"
+ " stanumbers3,"
+ " stanumbers4,"
+ " stavalues1,"
+ " stavalues2,"
+ " stavalues3,"
+ " stavalues4"
+ " FROM pg_catalog.pg_statistic "
+ " WHERE starelid = '%s.%s'::regclass "
+ " AND staattnum = ("
+ " SELECT attnum "
+ " FROM pg_catalog.pg_attribute "
+ " WHERE attrelid = '%s.%s'::regclass "
+ " AND attname = '%s')",
+ nspname, relname, nspname, relname, colname);
+ }
+
+ /*
+ * If foreign server is PG 9.0 or later, inherited tables might have
+ * two sets of statistics; one contains information about the table
+ * itself, and another contains information about whole inherit-tree
+ * topped by the table.
+ *
+ * We prefer statistics of whole inherit-tree if any, because parent
+ * table returns contents of descendants too. However, we overwrite
+ * stainherit by "false" for local statistics, because foreign tables
+ * can't be parent table.
+ */
+ if (PQserverVersion(conn) >= 90000)
+ appendStringInfo(&sql_stat, "ORDER BY stainherit DESC LIMIT 1");
+
+ res = NULL;
+ PG_TRY();
+ {
+ res = PQexec(conn, sql_stat.data);
+ pfree(sql_stat.data);
+ if (PQresultStatus(res) != PGRES_TUPLES_OK)
+ ereport(ERROR,
+ (errmsg("could not get per-column statistics"),
+ errdetail("%s", PQerrorMessage(conn))));
+
+ /* If remote table doesn't have any statistic, skip this column. */
+ if (PQntuples(res) > 0)
+ {
+ store_remote_stats(res, vacattrstats[i]);
+ nstats++;
+ }
+
+ PQclear(res);
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+ }
+
+ /* Disconnect from foreign server. */
+ ReleaseConnection(conn);
+
+ /* Update local pg_statistic with retrieved statistics. */
+ vac_update_attstats(RelationGetRelid(relation),
+ false,
+ attr_cnt,
+ vacattrstats);
+
+ /*
+ * Update local pg_class with retrieved statistics. We set relallvisible
+ * to 0 always.
+ */
+ vac_update_relstats(relation,
+ relpages,
+ reltuples,
+ 0,
+ false,
+ InvalidTransactionId);
+
+ /* Restore memory context. */
+ MemoryContextSwitchTo(caller_context);
+ MemoryContextDelete(anl_context);
+
+ #ifdef NOT_USED
+ /* Print verbose messages. */
+ if (vacstmt->options & VACOPT_VERBOSE)
+ elog(INFO, "%s: relpages=%d, reltuples=%f, %d per-column stats copied",
+ vacstmt->relation->relname,
+ relpages,
+ reltuples,
+ nstats);
+ #endif
+
+ /* Close the relation with releasing the lock. */
+ relation_close(relation, ShareUpdateExclusiveLock);
+
+ PG_RETURN_INT32(nstats);
+ }
+
+ /*
+ * Read text representation of remote statistics from res and store them into
+ * stats with conversion to internal representation.
+ */
+ static void
+ store_remote_stats(PGresult *res, VacAttrStats *stats)
+ {
+ Type float4array_type;
+ Type textarray_type;
+ Oid elemtypeid;
+ Type elemtype;
+ Oid in_func_oid;
+ Form_pg_type attrtype;
+ Oid typeioparam;
+ FmgrInfo in_function;
+
+ int i_stanullfrac;
+ int i_stawidth;
+ int i_stadistinct;
+ int i_stakind[STATISTIC_NUM_SLOTS];
+ int i_staop[STATISTIC_NUM_SLOTS];
+ int i_stanumbers[STATISTIC_NUM_SLOTS];
+ int i_stavalues[STATISTIC_NUM_SLOTS];
+ int i;
+
+ /* Get information of actual type of array element. */
+ elemtypeid = stats->attr->atttypid;
+ elemtype = typeidType(elemtypeid);
+ getTypeInputInfo(elemtypeid, &in_func_oid, &typeioparam);
+ fmgr_info(in_func_oid, &in_function);
+
+ attrtype = (Form_pg_type) palloc(sizeof(FormData_pg_type));
+ memcpy(attrtype, GETSTRUCT(elemtype), sizeof(FormData_pg_type));
+
+ i_stanullfrac = PQfnumber(res, "stanullfrac");
+ i_stawidth = PQfnumber(res, "stawidth");
+ i_stadistinct = PQfnumber(res, "stadistinct");
+ i_stakind[0] = PQfnumber(res, "stakind1");
+ i_stakind[1] = PQfnumber(res, "stakind2");
+ i_stakind[2] = PQfnumber(res, "stakind3");
+ i_stakind[3] = PQfnumber(res, "stakind4");
+ i_stakind[4] = PQfnumber(res, "stakind5");
+ i_staop[0] = PQfnumber(res, "staop1");
+ i_staop[1] = PQfnumber(res, "staop2");
+ i_staop[2] = PQfnumber(res, "staop3");
+ i_staop[3] = PQfnumber(res, "staop4");
+ i_staop[4] = PQfnumber(res, "staop5");
+ i_stanumbers[0] = PQfnumber(res, "stanumbers1");
+ i_stanumbers[1] = PQfnumber(res, "stanumbers2");
+ i_stanumbers[2] = PQfnumber(res, "stanumbers3");
+ i_stanumbers[3] = PQfnumber(res, "stanumbers4");
+ i_stanumbers[4] = PQfnumber(res, "stanumbers5");
+ i_stavalues[0] = PQfnumber(res, "stavalues1");
+ i_stavalues[1] = PQfnumber(res, "stavalues2");
+ i_stavalues[2] = PQfnumber(res, "stavalues3");
+ i_stavalues[3] = PQfnumber(res, "stavalues4");
+ i_stavalues[4] = PQfnumber(res, "stavalues5");
+
+ /*
+ * If statistics are found, mark this column to be updated later
+ * and fill with retrieved remote statistics.
+ *
+ * For scalar values, just copying values is enough, but we have to
+ * be conscious the types of elements for array values. libpq
+ * gives us an array value in a string representation, such as
+ * "{foo, bar}", so we need to (1)separate an array string into
+ * elements, (2)convert each element into Datum representation, and
+ * (3) create a Datum representation of array from Datum elements.
+ *
+ * Note: OID of operator (staopN) MUST match between remote and local.
+ */
+ stats->stats_valid = true;
+ stats->stanullfrac = strtof(PGRES_VAL0(i_stanullfrac), NULL);
+ stats->stawidth = strtol(PGRES_VAL0(i_stawidth), NULL, 10);
+ stats->stadistinct = strtof(PGRES_VAL0(i_stadistinct), NULL);
+
+ /*
+ * Get type information for stanumbers and stavalues. We assume stavalues
+ * as array of text during extracting elements, and then extracted
+ * elements are treated as the type of foreign table attributes.
+ */
+ float4array_type = typeidType(FLOAT4ARRAYOID);
+ textarray_type = typeidType(TEXTARRAYOID);
+
+ for (i = 0; i < STATISTIC_NUM_SLOTS; i++)
+ {
+ int atttypmod = 0;
+ int j;
+ Datum tnumbers = 0;
+ ArrayType *array;
+ int nitems;
+ bits8 *bitmap;
+ int bitmask;
+ char *p;
+
+ /* PostgreSQL 9.1 and older don't have stakind5. */
+ if (i_stakind[i] < 0 || i_staop[i] < 0 ||
+ i_stanumbers[i] < 0 || i_stavalues[i] < 0)
+ continue;
+
+ stats->stakind[i] = strtol(PGRES_VAL0(i_stakind[i]), NULL, 10);
+ stats->staop[i] = strtol(PGRES_VAL0(i_staop[i]), NULL, 10);
+
+ /*
+ * Extract element values of stanumbersN from text representation of
+ * array, if any.
+ */
+ if (!PGRES_NULL0(i_stanumbers[i]))
+ {
+ /* Separate array string into each float4 elements. */
+ tnumbers = stringTypeDatum(float4array_type,
+ PGRES_VAL0(i_stanumbers[i]),
+ -1);
+ array = DatumGetArrayTypeP(tnumbers);
+ nitems = ArrayGetNItems(ARR_NDIM(array), ARR_DIMS(array));
+
+ bitmap = ARR_NULLBITMAP(array);
+ bitmask = 1;
+
+ stats->numnumbers[i] = nitems;
+ stats->stanumbers[i] = (float4 *) palloc(nitems * sizeof(float4));
+ for (j = 0; j < nitems; j++)
+ {
+ p = ARR_DATA_PTR(array);
+
+ if (bitmap && (*bitmap & bitmask) == 0)
+ {
+ /* nulls can be ignored */
+ }
+ else
+ {
+ stats->stanumbers[i][j] = * (float4 *) ARR_DATA_PTR(array);
+ p = p + sizeof(float4);
+ p = (char *) att_align_nominal(p, 'i');
+ }
+
+ /* advance bitmap pointer if any */
+ if (bitmap)
+ {
+ bitmask <<= 1;
+ if (bitmask == 0x100)
+ {
+ bitmap++;
+ bitmask = 1;
+ }
+ }
+ }
+ }
+ else
+ {
+ stats->numnumbers[i] = 0;
+ stats->stanumbers[i] = NULL;
+ }
+
+ /*
+ * Extract element values of stavaluesN from text representation of
+ * array, if any.
+ */
+ if (!PGRES_NULL0(i_stavalues[i]))
+ {
+ /*
+ * Separate array string into each elements string, and convert
+ * each element to actual column data type.
+ */
+ tnumbers = stringTypeDatum(textarray_type,
+ PGRES_VAL0(i_stavalues[i]),
+ -1);
+ array = DatumGetArrayTypeP(tnumbers);
+ nitems = ArrayGetNItems(ARR_NDIM(array), ARR_DIMS(array));
+
+ bitmap = ARR_NULLBITMAP(array);
+ bitmask = 1;
+ p = ARR_DATA_PTR(array);
+
+ stats->numvalues[i] = nitems;
+ stats->stavalues[i] = (Datum *) palloc(nitems * sizeof(Datum));
+
+ for (j = 0; j < nitems; j++)
+ {
+ char *string;
+
+ if (bitmap && (*bitmap & bitmask) == 0)
+ {
+ /* nulls can be ignored */
+ }
+ else
+ {
+ string = text_to_cstring((text *) p);
+ stats->stavalues[i][j] = InputFunctionCall(&in_function,
+ string,
+ typeioparam,
+ atttypmod);
+ p = p + VARSIZE_ANY(p);
+ p = (char *) att_align_nominal(p, 'i');
+ }
+
+ /* advance bitmap pointer if any */
+ if (bitmap)
+ {
+ bitmask <<= 1;
+ if (bitmask == 0x100)
+ {
+ bitmap++;
+ bitmask = 1;
+ }
+ }
+ }
+ }
+ else
+ {
+ stats->numvalues[i] = 0;
+ stats->stavalues[i] = NULL;
+ }
+ }
+
+ /* Clean up */
+ ReleaseSysCache(elemtype);
+ ReleaseSysCache(textarray_type);
+ ReleaseSysCache(float4array_type);
+ }
diff --git a/contrib/pgsql_fdw/sql/pgsql_fdw.sql b/contrib/pgsql_fdw/sql/pgsql_fdw.sql
index 23a44df..ec6f292 100644
*** a/contrib/pgsql_fdw/sql/pgsql_fdw.sql
--- b/contrib/pgsql_fdw/sql/pgsql_fdw.sql
*************** INSERT INTO "S 1"."T 2"
*** 85,90 ****
--- 85,91 ----
'AAA' || to_char(id, 'FM000')
FROM generate_series(1, 100) id;
COMMIT;
+ ANALYZE "S 1"."T 1";
-- ===================================================================
-- tests for pgsql_fdw_validator
*************** SELECT pgsql_fdw_disconnect(srvid, usesy
*** 235,240 ****
--- 236,248 ----
SELECT srvname, usename FROM pgsql_fdw_connections;
-- ===================================================================
+ -- statistics management
+ -- ===================================================================
+ DELETE FROM pg_statistic WHERE starelid = '"S 1"."T 1"'::regclass AND staattnum IN (1, 3);
+ SELECT pgsql_fdw_analyze('ft1');
+ SELECT count(*) FROM pg_statistic WHERE starelid = 'ft1'::regclass;
+
+ -- ===================================================================
-- subtransaction
-- ===================================================================
BEGIN;
diff --git a/doc/src/sgml/pgsql-fdw.sgml b/doc/src/sgml/pgsql-fdw.sgml
index eb69f01..2c16c43 100644
*** a/doc/src/sgml/pgsql-fdw.sgml
--- b/doc/src/sgml/pgsql-fdw.sgml
*************** postgres=# SELECT pgsql_fdw_disconnect(s
*** 218,223 ****
--- 218,244 ----
</sect2>
<sect2>
+ <title>Statistics Management</title>
+ <para>
+ <application>pgsql_fdw</application> provides a function
+ <function>pgsql_fdw_analyze()</function> which updates local statistics
+ stored in <structname>pg_statistic</structname> and
+ <structname>pg_class</structname> by copying remote statistics. This
+ function takes a regclass argument of target foreign table, and returns
+ number of attributes of which per-attribute statistics are updated. Thus,
+ this function may return number less than attribute count when remote table
+ doesn't have statistics for all attributes.
+ </para>
+ <note>
+ <para>
+ Number of statistics slots in <structname>pg_statistic</structname> is
+ increased to 5 in 9.2, so this function copies only slot 1 to 4 from older
+ versions.
+ </para>
+ </note>
+ </sect2>
+
+ <sect2>
<title>Estimation of Costs and Rows</title>
<para>
The <application>pgsql_fdw</application> estimates the costs of a foreign
diff --git a/src/backend/commands/analyze.c b/src/backend/commands/analyze.c
index 9cd6e67..21b1bf1 100644
*** a/src/backend/commands/analyze.c
--- b/src/backend/commands/analyze.c
*************** static void compute_index_stats(Relation
*** 94,101 ****
AnlIndexData *indexdata, int nindexes,
HeapTuple *rows, int numrows,
MemoryContext col_context);
- static VacAttrStats *examine_attribute(Relation onerel, int attnum,
- Node *index_expr);
static int acquire_sample_rows(Relation onerel, HeapTuple *rows,
int targrows, double *totalrows, double *totaldeadrows);
static double random_fract(void);
--- 94,99 ----
*************** static int compare_rows(const void *a, c
*** 105,112 ****
static int acquire_inherited_sample_rows(Relation onerel,
HeapTuple *rows, int targrows,
double *totalrows, double *totaldeadrows);
- static void update_attstats(Oid relid, bool inh,
- int natts, VacAttrStats **vacattrstats);
static Datum std_fetch_func(VacAttrStatsP stats, int rownum, bool *isNull);
static Datum ind_fetch_func(VacAttrStatsP stats, int rownum, bool *isNull);
--- 103,108 ----
*************** analyze_rel(Oid relid, VacuumStmt *vacst
*** 209,215 ****
}
/*
! * We can ANALYZE any table except pg_statistic. See update_attstats
*/
if (RelationGetRelid(onerel) == StatisticRelationId)
{
--- 205,211 ----
}
/*
! * We can ANALYZE any table except pg_statistic. See vac_update_attstats
*/
if (RelationGetRelid(onerel) == StatisticRelationId)
{
*************** analyze_rel(Oid relid, VacuumStmt *vacst
*** 239,245 ****
* Close source relation now, but keep lock so that no one deletes it
* before we commit. (If someone did, they'd fail to clean up the entries
* we made in pg_statistic. Also, releasing the lock before commit would
! * expose us to concurrent-update failures in update_attstats.)
*/
relation_close(onerel, NoLock);
--- 235,241 ----
* Close source relation now, but keep lock so that no one deletes it
* before we commit. (If someone did, they'd fail to clean up the entries
* we made in pg_statistic. Also, releasing the lock before commit would
! * expose us to concurrent-update failures in vac_update_attstats.)
*/
relation_close(onerel, NoLock);
*************** do_analyze_rel(Relation onerel, VacuumSt
*** 514,528 ****
* previous statistics for the target columns. (If there are stats in
* pg_statistic for columns we didn't process, we leave them alone.)
*/
! update_attstats(RelationGetRelid(onerel), inh,
! attr_cnt, vacattrstats);
for (ind = 0; ind < nindexes; ind++)
{
AnlIndexData *thisdata = &indexdata[ind];
! update_attstats(RelationGetRelid(Irel[ind]), false,
! thisdata->attr_cnt, thisdata->vacattrstats);
}
}
--- 510,524 ----
* previous statistics for the target columns. (If there are stats in
* pg_statistic for columns we didn't process, we leave them alone.)
*/
! vac_update_attstats(RelationGetRelid(onerel), inh,
! attr_cnt, vacattrstats);
for (ind = 0; ind < nindexes; ind++)
{
AnlIndexData *thisdata = &indexdata[ind];
! vac_update_attstats(RelationGetRelid(Irel[ind]), false,
! thisdata->attr_cnt, thisdata->vacattrstats);
}
}
*************** compute_index_stats(Relation onerel, dou
*** 805,811 ****
* If index_expr isn't NULL, then we're trying to analyze an expression index,
* and index_expr is the expression tree representing the column's data.
*/
! static VacAttrStats *
examine_attribute(Relation onerel, int attnum, Node *index_expr)
{
Form_pg_attribute attr = onerel->rd_att->attrs[attnum - 1];
--- 801,807 ----
* If index_expr isn't NULL, then we're trying to analyze an expression index,
* and index_expr is the expression tree representing the column's data.
*/
! VacAttrStats *
examine_attribute(Relation onerel, int attnum, Node *index_expr)
{
Form_pg_attribute attr = onerel->rd_att->attrs[attnum - 1];
*************** acquire_inherited_sample_rows(Relation o
*** 1538,1544 ****
/*
! * update_attstats() -- update attribute statistics for one relation
*
* Statistics are stored in several places: the pg_class row for the
* relation has stats about the whole relation, and there is a
--- 1534,1540 ----
/*
! * vac_update_attstats() -- update attribute statistics for one relation
*
* Statistics are stored in several places: the pg_class row for the
* relation has stats about the whole relation, and there is a
*************** acquire_inherited_sample_rows(Relation o
*** 1559,1566 ****
* ANALYZE the same table concurrently. Presently, we lock that out
* by taking a self-exclusive lock on the relation in analyze_rel().
*/
! static void
! update_attstats(Oid relid, bool inh, int natts, VacAttrStats **vacattrstats)
{
Relation sd;
int attno;
--- 1555,1562 ----
* ANALYZE the same table concurrently. Presently, we lock that out
* by taking a self-exclusive lock on the relation in analyze_rel().
*/
! void
! vac_update_attstats(Oid relid, bool inh, int natts, VacAttrStats **vacattrstats)
{
Relation sd;
int attno;
diff --git a/src/include/commands/vacuum.h b/src/include/commands/vacuum.h
index 3deee66..da9bf8a 100644
*** a/src/include/commands/vacuum.h
--- b/src/include/commands/vacuum.h
*************** extern void vac_update_relstats(Relation
*** 154,159 ****
--- 154,161 ----
BlockNumber num_all_visible_pages,
bool hasindex,
TransactionId frozenxid);
+ extern void vac_update_attstats(Oid relid, bool inh,
+ int natts, VacAttrStats **vacattrstats);
extern void vacuum_set_xid_limits(int freeze_min_age, int freeze_table_age,
bool sharedRel,
TransactionId *oldestXmin,
*************** extern void lazy_vacuum_rel(Relation one
*** 170,174 ****
--- 172,178 ----
extern void analyze_rel(Oid relid, VacuumStmt *vacstmt,
BufferAccessStrategy bstrategy);
extern bool std_typanalyze(VacAttrStats *stats);
+ extern VacAttrStats *examine_attribute(Relation onerel, int attnum,
+ Node *index_expr);
#endif /* VACUUM_H */
pgsql_fdw_v18.patchtext/plain; charset=Shift_JIS; name=pgsql_fdw_v18.patchDownload
diff --git a/contrib/Makefile b/contrib/Makefile
index d230451..9bc35dd 100644
*** a/contrib/Makefile
--- b/contrib/Makefile
*************** SUBDIRS = \
*** 42,47 ****
--- 42,48 ----
pgbench \
pgcrypto \
pgrowlocks \
+ pgsql_fdw \
pgstattuple \
seg \
spi \
diff --git a/contrib/README b/contrib/README
index a1d42a1..d3fa211 100644
*** a/contrib/README
--- b/contrib/README
*************** pgrowlocks -
*** 158,163 ****
--- 158,167 ----
A function to return row locking information
by Tatsuo Ishii <ishii@sraoss.co.jp>
+ pgsql_fdw -
+ Foreign-data wrapper for external PostgreSQL servers.
+ by Shigeru Hanada <shigeru.hanada@gmail.com>
+
pgstattuple -
Functions to return statistics about "dead" tuples and free
space within a table
diff --git a/contrib/pgsql_fdw/.gitignore b/contrib/pgsql_fdw/.gitignore
index ...0854728 .
*** a/contrib/pgsql_fdw/.gitignore
--- b/contrib/pgsql_fdw/.gitignore
***************
*** 0 ****
--- 1,4 ----
+ # Generated subdirectories
+ /results/
+ *.o
+ *.so
diff --git a/contrib/pgsql_fdw/Makefile b/contrib/pgsql_fdw/Makefile
index ...6381365 .
*** a/contrib/pgsql_fdw/Makefile
--- b/contrib/pgsql_fdw/Makefile
***************
*** 0 ****
--- 1,22 ----
+ # contrib/pgsql_fdw/Makefile
+
+ MODULE_big = pgsql_fdw
+ OBJS = pgsql_fdw.o option.o deparse.o connection.o
+ PG_CPPFLAGS = -I$(libpq_srcdir)
+ SHLIB_LINK = $(libpq)
+
+ EXTENSION = pgsql_fdw
+ DATA = pgsql_fdw--1.0.sql
+
+ REGRESS = pgsql_fdw
+
+ ifdef USE_PGXS
+ PG_CONFIG = pg_config
+ PGXS := $(shell $(PG_CONFIG) --pgxs)
+ include $(PGXS)
+ else
+ subdir = contrib/pgsql_fdw
+ top_builddir = ../..
+ include $(top_builddir)/src/Makefile.global
+ include $(top_srcdir)/contrib/contrib-global.mk
+ endif
diff --git a/contrib/pgsql_fdw/connection.c b/contrib/pgsql_fdw/connection.c
index ...c971bd7 .
*** a/contrib/pgsql_fdw/connection.c
--- b/contrib/pgsql_fdw/connection.c
***************
*** 0 ****
--- 1,555 ----
+ /*-------------------------------------------------------------------------
+ *
+ * connection.c
+ * Connection management for pgsql_fdw
+ *
+ * Portions Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/connection.c
+ *
+ *-------------------------------------------------------------------------
+ */
+ #include "postgres.h"
+
+ #include "access/xact.h"
+ #include "catalog/pg_type.h"
+ #include "foreign/foreign.h"
+ #include "funcapi.h"
+ #include "libpq-fe.h"
+ #include "mb/pg_wchar.h"
+ #include "miscadmin.h"
+ #include "utils/array.h"
+ #include "utils/builtins.h"
+ #include "utils/hsearch.h"
+ #include "utils/memutils.h"
+ #include "utils/resowner.h"
+ #include "utils/tuplestore.h"
+
+ #include "pgsql_fdw.h"
+ #include "connection.h"
+
+ /* ============================================================================
+ * Connection management functions
+ * ==========================================================================*/
+
+ /*
+ * Connection cache entry managed with hash table.
+ */
+ typedef struct ConnCacheEntry
+ {
+ /* hash key must be first */
+ Oid serverid; /* oid of foreign server */
+ Oid userid; /* oid of local user */
+
+ bool use_tx; /* true when using remote transaction */
+ 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);
+ static void begin_remote_tx(PGconn *conn);
+ static void abort_remote_tx(PGconn *conn);
+
+ /*
+ * 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.
+ *
+ * When use_tx is true, remote transaction is started if caller is the only
+ * user of the connection. Isolation level of the remote transaction is same
+ * as local transaction, and remote transaction will be aborted when last
+ * user release.
+ *
+ * TODO: Note that caching connections requires a mechanism to detect change of
+ * FDW object to invalidate already established connections.
+ */
+ PGconn *
+ GetConnection(ForeignServer *server, UserMapping *user, bool use_tx)
+ {
+ bool found;
+ ConnCacheEntry *entry;
+ ConnCacheEntry key;
+
+ /* initialize connection cache if it isn't */
+ if (FSConnectionHash == NULL)
+ {
+ HASHCTL ctl;
+
+ /* hash key is a pair of oids: serverid and userid */
+ MemSet(&ctl, 0, sizeof(ctl));
+ ctl.keysize = sizeof(Oid) + sizeof(Oid);
+ ctl.entrysize = sizeof(ConnCacheEntry);
+ ctl.hash = tag_hash;
+ ctl.match = memcmp;
+ ctl.keycopy = memcpy;
+ /* allocate FSConnectionHash in the cache context */
+ ctl.hcxt = CacheMemoryContext;
+ FSConnectionHash = hash_create("Foreign Connections", 32,
+ &ctl,
+ HASH_ELEM | HASH_CONTEXT |
+ HASH_FUNCTION | HASH_COMPARE |
+ HASH_KEYCOPY);
+ }
+
+ /* Create key value for the entry. */
+ MemSet(&key, 0, sizeof(key));
+ key.serverid = server->serverid;
+ key.userid = GetOuterUserId();
+
+ /*
+ * Find cached entry for requested connection. If we couldn't find,
+ * callback function of ResourceOwner should be registered to clean the
+ * connection up on error including user interrupt.
+ */
+ entry = hash_search(FSConnectionHash, &key, HASH_ENTER, &found);
+ if (!found)
+ {
+ entry->refs = 0;
+ entry->use_tx = false;
+ entry->conn = NULL;
+ RegisterResourceReleaseCallback(cleanup_connection, entry);
+ }
+
+ /*
+ * We don't check the health of cached connection here, because it would
+ * require some overhead. Broken connection and its cache entry will be
+ * cleaned up when the connection is actually used.
+ */
+
+ /*
+ * If cache entry doesn't have connection, we have to establish new
+ * connection.
+ */
+ if (entry->conn == NULL)
+ {
+ /*
+ * Use PG_TRY block to ensure closing connection on error.
+ */
+ PG_TRY();
+ {
+ /*
+ * Connect to the foreign PostgreSQL server, and store it in cache
+ * entry to keep new connection.
+ * Note: key items of entry has already been initialized in
+ * hash_search(HASH_ENTER).
+ */
+ entry->conn = connect_pg_server(server, user);
+ }
+ PG_CATCH();
+ {
+ /* Clear connection cache entry on error case. */
+ PQfinish(entry->conn);
+ entry->refs = 0;
+ entry->use_tx = false;
+ entry->conn = NULL;
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+ }
+
+ /* Increase connection reference counter. */
+ entry->refs++;
+
+ /*
+ * If requester is the only referrer of this connection, start transaction
+ * with the same isolation level as the local transaction we are in.
+ * We need to remember whether this connection uses remote transaction to
+ * abort it when this connection is released completely.
+ */
+ if (use_tx && entry->refs == 1)
+ begin_remote_tx(entry->conn);
+ entry->use_tx = use_tx;
+
+
+ return entry->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 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. Those two object hold only libpq options.
+ * Extra 3 items are for:
+ * *) fallback_application_name
+ * *) client_encoding
+ * *) NULL termination (end marker)
+ *
+ * Note: We don't omit any parameters even target database might be older
+ * than local, because unexpected parameters are just ignored.
+ */
+ n = list_length(server->options) + list_length(user->options) + 3;
+ 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 += ExtractConnectionOptions(server->options,
+ all_keywords + n, all_values + n);
+ n += ExtractConnectionOptions(user->options,
+ all_keywords + n, all_values + n);
+ all_keywords[n] = all_values[n] = NULL;
+
+ for (i = 0, j = 0; all_keywords[i]; i++)
+ {
+ keywords[j] = all_keywords[i];
+ values[j] = all_values[i];
+ j++;
+ }
+
+ /* Use "pgsql_fdw" as fallback_application_name. */
+ keywords[j] = "fallback_application_name";
+ values[j++] = "pgsql_fdw";
+
+ /* Set client_encoding so that libpq can convert encoding properly. */
+ keywords[j] = "client_encoding";
+ values[j++] = GetDatabaseEncodingName();
+
+ keywords[j] = values[j] = NULL;
+ pfree(all_keywords);
+ pfree(all_values);
+
+ /* 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.")));
+ }
+
+ return conn;
+ }
+
+ /*
+ * Start transaction to use cursor to retrieve data separately.
+ */
+ static void
+ begin_remote_tx(PGconn *conn)
+ {
+ const char *sql = NULL; /* keep compiler quiet. */
+ PGresult *res;
+
+ switch (XactIsoLevel)
+ {
+ case XACT_READ_UNCOMMITTED:
+ case XACT_READ_COMMITTED:
+ case XACT_REPEATABLE_READ:
+ sql = "START TRANSACTION ISOLATION LEVEL REPEATABLE READ";
+ break;
+ case XACT_SERIALIZABLE:
+ sql = "START TRANSACTION ISOLATION LEVEL SERIALIZABLE";
+ break;
+ default:
+ elog(ERROR, "unexpected isolation level: %d", XactIsoLevel);
+ break;
+ }
+
+ elog(DEBUG3, "starting remote transaction with \"%s\"", sql);
+
+ res = PQexec(conn, sql);
+ if (PQresultStatus(res) != PGRES_COMMAND_OK)
+ {
+ PQclear(res);
+ elog(ERROR, "could not start transaction: %s", PQerrorMessage(conn));
+ }
+ PQclear(res);
+ }
+
+ static void
+ abort_remote_tx(PGconn *conn)
+ {
+ PGresult *res;
+
+ elog(DEBUG3, "aborting remote transaction");
+
+ res = PQexec(conn, "ABORT TRANSACTION");
+ if (PQresultStatus(res) != PGRES_COMMAND_OK)
+ {
+ PQclear(res);
+ elog(ERROR, "could not abort transaction: %s", PQerrorMessage(conn));
+ }
+ PQclear(res);
+ }
+
+ /*
+ * 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 sequentially 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)
+ {
+ hash_seq_term(&scan);
+ break;
+ }
+ }
+
+ /* If the released connection was an orphan, just close it. */
+ if (entry == NULL)
+ {
+ PQfinish(conn);
+ return;
+ }
+
+ /*
+ * If this connection uses remote transaction and caller is the last user,
+ * abort remote transaction and forget about it.
+ */
+ if (entry->use_tx && entry->refs == 1)
+ {
+ abort_remote_tx(conn);
+ entry->use_tx = false;
+ }
+
+ /*
+ * Decrease reference counter of this connection. Even if the caller was
+ * the last referrer, we don't unregister it from cache.
+ */
+ entry->refs--;
+ }
+
+ /*
+ * 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 need to clean up at end of subtransactions, because they might
+ * be recovered to consistent state with savepoints.
+ */
+ if (!isTopLevel)
+ return;
+
+ /*
+ * Here, it must be after abort of top level transaction. Disconnect and
+ * forget every referrer.
+ */
+ if (entry->conn != NULL)
+ {
+ PQfinish(entry->conn);
+ entry->refs = 0;
+ entry->conn = NULL;
+ }
+ }
+
+ /*
+ * Get list of connections currently active.
+ */
+ Datum pgsql_fdw_get_connections(PG_FUNCTION_ARGS);
+ PG_FUNCTION_INFO_V1(pgsql_fdw_get_connections);
+ Datum
+ pgsql_fdw_get_connections(PG_FUNCTION_ARGS)
+ {
+ ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+ HASH_SEQ_STATUS scan;
+ ConnCacheEntry *entry;
+ MemoryContext oldcontext = CurrentMemoryContext;
+ Tuplestorestate *tuplestore;
+ TupleDesc tupdesc;
+
+ /* We return list of connection with storing them in a Tuplestore. */
+ rsinfo->returnMode = SFRM_Materialize;
+ rsinfo->setResult = NULL;
+ rsinfo->setDesc = NULL;
+
+ /* Create tuplestore and copy of TupleDesc in per-query context. */
+ MemoryContextSwitchTo(rsinfo->econtext->ecxt_per_query_memory);
+
+ tupdesc = CreateTemplateTupleDesc(2, false);
+ TupleDescInitEntry(tupdesc, 1, "srvid", OIDOID, -1, 0);
+ TupleDescInitEntry(tupdesc, 2, "usesysid", OIDOID, -1, 0);
+ rsinfo->setDesc = tupdesc;
+
+ tuplestore = tuplestore_begin_heap(false, false, work_mem);
+ rsinfo->setResult = tuplestore;
+
+ MemoryContextSwitchTo(oldcontext);
+
+ /*
+ * We need to scan sequentially since we use the address to find
+ * appropriate PGconn from the hash table.
+ */
+ if (FSConnectionHash != NULL)
+ {
+ hash_seq_init(&scan, FSConnectionHash);
+ while ((entry = (ConnCacheEntry *) hash_seq_search(&scan)))
+ {
+ Datum values[2];
+ bool nulls[2];
+ HeapTuple tuple;
+
+ /* Ignore inactive connections */
+ if (PQstatus(entry->conn) != CONNECTION_OK)
+ continue;
+
+ /*
+ * Ignore other users' connections if current user isn't a
+ * superuser.
+ */
+ if (!superuser() && entry->userid != GetUserId())
+ continue;
+
+ values[0] = ObjectIdGetDatum(entry->serverid);
+ values[1] = ObjectIdGetDatum(entry->userid);
+ nulls[0] = false;
+ nulls[1] = false;
+
+ tuple = heap_formtuple(tupdesc, values, nulls);
+ tuplestore_puttuple(tuplestore, tuple);
+ }
+ }
+ tuplestore_donestoring(tuplestore);
+
+ PG_RETURN_VOID();
+ }
+
+ /*
+ * Discard persistent connection designated by given connection name.
+ */
+ Datum pgsql_fdw_disconnect(PG_FUNCTION_ARGS);
+ PG_FUNCTION_INFO_V1(pgsql_fdw_disconnect);
+ Datum
+ pgsql_fdw_disconnect(PG_FUNCTION_ARGS)
+ {
+ Oid serverid = PG_GETARG_OID(0);
+ Oid userid = PG_GETARG_OID(1);
+ ConnCacheEntry key;
+ ConnCacheEntry *entry = NULL;
+ bool found;
+
+ /* Non-superuser can't discard other users' connection. */
+ if (!superuser() && userid != GetOuterUserId())
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_RESOURCES),
+ errmsg("only superuser can discard other user's connection")));
+
+ /*
+ * If no connection has been established, or no such connections, just
+ * return "NG" to indicate nothing has done.
+ */
+ if (FSConnectionHash == NULL)
+ PG_RETURN_TEXT_P(cstring_to_text("NG"));
+
+ key.serverid = serverid;
+ key.userid = userid;
+ entry = hash_search(FSConnectionHash, &key, HASH_FIND, &found);
+ if (!found)
+ PG_RETURN_TEXT_P(cstring_to_text("NG"));
+
+ /* Discard cached connection, and clear reference counter. */
+ PQfinish(entry->conn);
+ entry->refs = 0;
+ entry->conn = NULL;
+
+ PG_RETURN_TEXT_P(cstring_to_text("OK"));
+ }
diff --git a/contrib/pgsql_fdw/connection.h b/contrib/pgsql_fdw/connection.h
index ...4427f56 .
*** a/contrib/pgsql_fdw/connection.h
--- b/contrib/pgsql_fdw/connection.h
***************
*** 0 ****
--- 1,25 ----
+ /*-------------------------------------------------------------------------
+ *
+ * connection.h
+ * Connection management for pgsql_fdw
+ *
+ * Portions Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_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, bool use_tx);
+ void ReleaseConnection(PGconn *conn);
+
+ #endif /* CONNECTION_H */
diff --git a/contrib/pgsql_fdw/deparse.c b/contrib/pgsql_fdw/deparse.c
index ...8e79232 .
*** a/contrib/pgsql_fdw/deparse.c
--- b/contrib/pgsql_fdw/deparse.c
***************
*** 0 ****
--- 1,288 ----
+ /*-------------------------------------------------------------------------
+ *
+ * deparse.c
+ * query deparser for PostgreSQL
+ *
+ * Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/deparse.c
+ *
+ *-------------------------------------------------------------------------
+ */
+ #include "postgres.h"
+
+ #include "access/transam.h"
+ #include "catalog/pg_class.h"
+ #include "commands/defrem.h"
+ #include "foreign/foreign.h"
+ #include "lib/stringinfo.h"
+ #include "nodes/nodeFuncs.h"
+ #include "nodes/nodes.h"
+ #include "nodes/makefuncs.h"
+ #include "optimizer/clauses.h"
+ #include "optimizer/var.h"
+ #include "parser/parsetree.h"
+ #include "utils/builtins.h"
+ #include "utils/lsyscache.h"
+
+ #include "pgsql_fdw.h"
+
+ /*
+ * Context for walk-through the expression tree.
+ */
+ typedef struct foreign_executable_cxt
+ {
+ PlannerInfo *root;
+ RelOptInfo *foreignrel;
+ } foreign_executable_cxt;
+
+ /*
+ * Get string representation which can be used in SQL statement from a node.
+ */
+ static void deparseRelation(StringInfo buf, Oid relid, PlannerInfo *root,
+ bool need_prefix);
+ static void deparseVar(StringInfo buf, Var *node, PlannerInfo *root,
+ bool need_prefix);
+
+ /*
+ * Deparse query representation into SQL statement which suits for remote
+ * PostgreSQL server. This function basically creates simple query string
+ * which consists of only SELECT, FROM clauses.
+ */
+ void
+ deparseSimpleSql(StringInfo buf,
+ Oid relid,
+ PlannerInfo *root,
+ RelOptInfo *baserel,
+ ForeignTable *table)
+ {
+ StringInfoData foreign_relname;
+ bool first;
+ AttrNumber attr;
+ List *attr_used = NIL; /* List of AttNumber used in the query */
+
+ initStringInfo(buf);
+ initStringInfo(&foreign_relname);
+
+ /*
+ * First of all, determine which column should be retrieved for this scan.
+ *
+ * We do this before deparsing SELECT clause because attributes which are
+ * not used in neither reltargetlist nor baserel->baserestrictinfo, quals
+ * evaluated on local, can be replaced with literal "NULL" in the SELECT
+ * clause to reduce overhead of tuple handling tuple and data transfer.
+ */
+ if (baserel->baserestrictinfo != NIL)
+ {
+ ListCell *lc;
+
+ foreach (lc, baserel->baserestrictinfo)
+ {
+ RestrictInfo *ri = (RestrictInfo *) lfirst(lc);
+ List *attrs;
+
+ /*
+ * We need to know which attributes are used in qual evaluated
+ * on the local server, because they should be listed in the
+ * SELECT clause of remote query. We can ignore attributes
+ * which are referenced only in ORDER BY/GROUP BY clause because
+ * such attributes has already been kept in reltargetlist.
+ */
+ attrs = pull_var_clause((Node *) ri->clause,
+ PVC_RECURSE_AGGREGATES,
+ PVC_RECURSE_PLACEHOLDERS);
+ attr_used = list_union(attr_used, attrs);
+ }
+ }
+
+ /*
+ * deparse SELECT clause
+ *
+ * List attributes which are in either target list or local restriction.
+ * Unused attributes are replaced with a literal "NULL" for optimization.
+ *
+ * Note that nothing is added for dropped columns, though tuple constructor
+ * function requires entries for dropped columns. Such entries must be
+ * initialized with NULL before calling tuple constructor.
+ */
+ appendStringInfo(buf, "SELECT ");
+ attr_used = list_union(attr_used, baserel->reltargetlist);
+ first = true;
+ for (attr = 1; attr <= baserel->max_attr; attr++)
+ {
+ RangeTblEntry *rte = root->simple_rte_array[baserel->relid];
+ Var *var = NULL;
+ ListCell *lc;
+
+ /* Ignore dropped attributes. */
+ if (get_rte_attribute_is_dropped(rte, attr))
+ continue;
+
+ if (!first)
+ appendStringInfo(buf, ", ");
+ first = false;
+
+ /*
+ * We use linear search here, but it wouldn't be problem since
+ * attr_used seems to not become so large.
+ */
+ foreach (lc, attr_used)
+ {
+ var = lfirst(lc);
+ if (var->varattno == attr)
+ break;
+ var = NULL;
+ }
+ if (var != NULL)
+ deparseVar(buf, var, root, false);
+ else
+ appendStringInfo(buf, "NULL");
+ }
+ appendStringInfoChar(buf, ' ');
+
+ /*
+ * deparse FROM clause, including alias if any
+ */
+ appendStringInfo(buf, "FROM ");
+ deparseRelation(buf, table->relid, root, true);
+
+ elog(DEBUG3, "Remote SQL: %s", buf->data);
+ }
+
+ /*
+ * Deparse node into buf, with relation qualifier if need_prefix was true. If
+ * node is a column of a foreign table, use value of colname FDW option (if any)
+ * instead of attribute name.
+ */
+ static void
+ deparseVar(StringInfo buf,
+ Var *node,
+ PlannerInfo *root,
+ bool need_prefix)
+ {
+ RangeTblEntry *rte;
+ char *colname = NULL;
+ const char *q_colname = NULL;
+ List *options;
+ ListCell *lc;
+
+ /* node must not be any of OUTER_VAR,INNER_VAR and INDEX_VAR. */
+ Assert(node->varno >= 1 && node->varno <= root->simple_rel_array_size);
+
+ /* Get RangeTblEntry from array in PlannerInfo. */
+ rte = root->simple_rte_array[node->varno];
+
+ /*
+ * If the node is a column of a foreign table, and it has colname FDW
+ * option, use its value.
+ */
+ if (rte->relkind == RELKIND_FOREIGN_TABLE)
+ {
+ options = GetForeignColumnOptions(rte->relid, node->varattno);
+ foreach(lc, options)
+ {
+ DefElem *def = (DefElem *) lfirst(lc);
+
+ if (strcmp(def->defname, "colname") == 0)
+ {
+ colname = defGetString(def);
+ break;
+ }
+ }
+ }
+
+ /*
+ * If the node refers a column of a regular table or it doesn't have colname
+ * FDW option, use attribute name.
+ */
+ if (colname == NULL)
+ colname = get_attname(rte->relid, node->varattno);
+
+ if (need_prefix)
+ {
+ char *aliasname;
+ const char *q_aliasname;
+
+ if (rte->eref != NULL && rte->eref->aliasname != NULL)
+ aliasname = rte->eref->aliasname;
+ else if (rte->alias != NULL && rte->alias->aliasname != NULL)
+ aliasname = rte->alias->aliasname;
+
+ q_aliasname = quote_identifier(aliasname);
+ appendStringInfo(buf, "%s.", q_aliasname);
+ }
+
+ q_colname = quote_identifier(colname);
+ appendStringInfo(buf, "%s", q_colname);
+ }
+
+ /*
+ * Deparse table which has relid as oid into buf, with schema qualifier if
+ * need_prefix was true. If relid points a foreign table, use value of relname
+ * FDW option (if any) instead of relation's name. Similarly, nspname FDW
+ * option overrides schema name.
+ */
+ static void
+ deparseRelation(StringInfo buf,
+ Oid relid,
+ PlannerInfo *root,
+ bool need_prefix)
+ {
+ int i;
+ RangeTblEntry *rte = NULL;
+ ListCell *lc;
+ const char *nspname = NULL; /* plain namespace name */
+ const char *relname = NULL; /* plain relation name */
+ const char *q_nspname; /* quoted namespace name */
+ const char *q_relname; /* quoted relation name */
+
+ /* Find RangeTblEntry for the relation from PlannerInfo. */
+ for (i = 1; i < root->simple_rel_array_size; i++)
+ {
+ if (root->simple_rte_array[i]->relid == relid)
+ {
+ rte = root->simple_rte_array[i];
+ break;
+ }
+ }
+ if (rte == NULL)
+ elog(ERROR, "relation with OID %u is not used in the query", relid);
+
+ /* If target is a foreign table, obtain additional catalog information. */
+ if (rte->relkind == RELKIND_FOREIGN_TABLE)
+ {
+ ForeignTable *table = GetForeignTable(rte->relid);
+
+ /*
+ * Use value of FDW options if any, instead of the name of object
+ * itself.
+ */
+ foreach(lc, table->options)
+ {
+ DefElem *def = (DefElem *) lfirst(lc);
+
+ if (need_prefix && strcmp(def->defname, "nspname") == 0)
+ nspname = defGetString(def);
+ else if (strcmp(def->defname, "relname") == 0)
+ relname = defGetString(def);
+ }
+ }
+
+ /* Quote each identifier, if necessary. */
+ if (need_prefix)
+ {
+ if (nspname == NULL)
+ nspname = get_namespace_name(get_rel_namespace(relid));
+ q_nspname = quote_identifier(nspname);
+ }
+
+ if (relname == NULL)
+ relname = get_rel_name(relid);
+ q_relname = quote_identifier(relname);
+
+ /* Construct relation reference into the buffer. */
+ if (need_prefix)
+ appendStringInfo(buf, "%s.", q_nspname);
+ appendStringInfo(buf, "%s", q_relname);
+ }
diff --git a/contrib/pgsql_fdw/expected/pgsql_fdw.out b/contrib/pgsql_fdw/expected/pgsql_fdw.out
index ...67fd979 .
*** a/contrib/pgsql_fdw/expected/pgsql_fdw.out
--- b/contrib/pgsql_fdw/expected/pgsql_fdw.out
***************
*** 0 ****
--- 1,592 ----
+ -- ===================================================================
+ -- create FDW objects
+ -- ===================================================================
+ -- Clean up in case a prior regression run failed
+ -- Suppress NOTICE messages when roles don't exist
+ SET client_min_messages TO 'error';
+ DROP ROLE IF EXISTS pgsql_fdw_user;
+ RESET client_min_messages;
+ CREATE ROLE pgsql_fdw_user LOGIN SUPERUSER;
+ SET SESSION AUTHORIZATION 'pgsql_fdw_user';
+ CREATE EXTENSION pgsql_fdw;
+ CREATE SERVER loopback1 FOREIGN DATA WRAPPER pgsql_fdw;
+ CREATE SERVER loopback2 FOREIGN DATA WRAPPER pgsql_fdw
+ OPTIONS (dbname 'contrib_regression');
+ CREATE USER MAPPING FOR public SERVER loopback1
+ OPTIONS (user 'value', password 'value');
+ CREATE USER MAPPING FOR pgsql_fdw_user SERVER loopback2;
+ CREATE FOREIGN TABLE ft1 (
+ c0 int,
+ c1 int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10)
+ ) SERVER loopback2;
+ ALTER FOREIGN TABLE ft1 DROP COLUMN c0;
+ CREATE FOREIGN TABLE ft2 (
+ c0 int,
+ c1 int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10)
+ ) SERVER loopback2;
+ ALTER FOREIGN TABLE ft2 DROP COLUMN c0;
+ -- ===================================================================
+ -- create objects used through FDW
+ -- ===================================================================
+ CREATE SCHEMA "S 1";
+ CREATE TABLE "S 1"."T 1" (
+ "C 1" int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10),
+ CONSTRAINT t1_pkey PRIMARY KEY ("C 1")
+ );
+ NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "t1_pkey" for table "T 1"
+ CREATE TABLE "S 1"."T 2" (
+ c1 int NOT NULL,
+ c2 text,
+ CONSTRAINT t2_pkey PRIMARY KEY (c1)
+ );
+ NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "t2_pkey" for table "T 2"
+ BEGIN;
+ TRUNCATE "S 1"."T 1";
+ INSERT INTO "S 1"."T 1"
+ SELECT id,
+ id % 10,
+ to_char(id, 'FM00000'),
+ '1970-01-01'::timestamptz + ((id % 100) || ' days')::interval,
+ '1970-01-01'::timestamp + ((id % 100) || ' days')::interval,
+ id % 10,
+ id % 10
+ FROM generate_series(1, 1000) id;
+ TRUNCATE "S 1"."T 2";
+ INSERT INTO "S 1"."T 2"
+ SELECT id,
+ 'AAA' || to_char(id, 'FM000')
+ FROM generate_series(1, 100) id;
+ COMMIT;
+ -- ===================================================================
+ -- tests for pgsql_fdw_validator
+ -- ===================================================================
+ ALTER FOREIGN DATA WRAPPER pgsql_fdw OPTIONS (host 'value'); -- ERROR
+ ERROR: invalid option "host"
+ HINT: Valid options in this context are:
+ -- requiressl, krbsrvname and gsslib are omitted because they depend on
+ -- configure option
+ ALTER SERVER loopback1 OPTIONS (
+ authtype 'value',
+ service 'value',
+ connect_timeout 'value',
+ dbname 'value',
+ host 'value',
+ hostaddr 'value',
+ port 'value',
+ --client_encoding 'value',
+ tty 'value',
+ options 'value',
+ application_name 'value',
+ --fallback_application_name 'value',
+ keepalives 'value',
+ keepalives_idle 'value',
+ keepalives_interval 'value',
+ -- requiressl 'value',
+ sslcompression 'value',
+ sslmode 'value',
+ sslcert 'value',
+ sslkey 'value',
+ sslrootcert 'value',
+ sslcrl 'value'
+ --requirepeer 'value',
+ -- krbsrvname 'value',
+ -- gsslib 'value',
+ --replication 'value'
+ );
+ ALTER SERVER loopback1 OPTIONS (user 'value'); -- ERROR
+ ERROR: invalid option "user"
+ HINT: Valid options in this context are: authtype, service, connect_timeout, dbname, host, hostaddr, port, tty, options, application_name, keepalives, keepalives_idle, keepalives_interval, keepalives_count, requiressl, sslcompression, sslmode, sslcert, sslkey, sslrootcert, sslcrl, requirepeer, krbsrvname, gsslib, fetch_count
+ ALTER SERVER loopback2 OPTIONS (ADD fetch_count '2');
+ ALTER USER MAPPING FOR public SERVER loopback1
+ OPTIONS (DROP user, DROP password);
+ ALTER USER MAPPING FOR public SERVER loopback1
+ OPTIONS (host 'value'); -- ERROR
+ ERROR: invalid option "host"
+ HINT: Valid options in this context are: user, password
+ ALTER FOREIGN TABLE ft1 OPTIONS (nspname 'S 1', relname 'T 1');
+ ALTER FOREIGN TABLE ft2 OPTIONS (nspname 'S 1', relname 'T 1', fetch_count '100');
+ ALTER FOREIGN TABLE ft1 OPTIONS (invalid 'value'); -- ERROR
+ ERROR: invalid option "invalid"
+ HINT: Valid options in this context are: nspname, relname, fetch_count
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count 'a'); -- ERROR
+ ERROR: invalid value for fetch_count: "a"
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count '0'); -- ERROR
+ ERROR: invalid value for fetch_count: "0"
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count '-1'); -- ERROR
+ ERROR: invalid value for fetch_count: "-1"
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (invalid 'value'); -- ERROR
+ ERROR: invalid option "invalid"
+ HINT: Valid options in this context are: colname
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (colname 'C 1');
+ ALTER FOREIGN TABLE ft2 ALTER COLUMN c1 OPTIONS (colname 'C 1');
+ \dew+
+ List of foreign-data wrappers
+ Name | Owner | Handler | Validator | Access privileges | FDW Options | Description
+ -----------+----------------+-------------------+---------------------+-------------------+-------------+-------------
+ pgsql_fdw | pgsql_fdw_user | pgsql_fdw_handler | pgsql_fdw_validator | | |
+ (1 row)
+
+ \des+
+ List of foreign servers
+ Name | Owner | Foreign-data wrapper | Access privileges | Type | Version | FDW Options | Description
+ -----------+----------------+----------------------+-------------------+------+---------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+-------------
+ loopback1 | pgsql_fdw_user | pgsql_fdw | | | | (authtype 'value', service 'value', connect_timeout 'value', dbname 'value', host 'value', hostaddr 'value', port 'value', tty 'value', options 'value', application_name 'value', keepalives 'value', keepalives_idle 'value', keepalives_interval 'value', sslcompression 'value', sslmode 'value', sslcert 'value', sslkey 'value', sslrootcert 'value', sslcrl 'value') |
+ loopback2 | pgsql_fdw_user | pgsql_fdw | | | | (dbname 'contrib_regression', fetch_count '2') |
+ (2 rows)
+
+ \deu+
+ List of user mappings
+ Server | User name | FDW Options
+ -----------+----------------+-------------
+ loopback1 | public |
+ loopback2 | pgsql_fdw_user |
+ (2 rows)
+
+ \det+
+ List of foreign tables
+ Schema | Table | Server | FDW Options | Description
+ --------+-------+-----------+---------------------------------------------------+-------------
+ public | ft1 | loopback2 | (nspname 'S 1', relname 'T 1') |
+ public | ft2 | loopback2 | (nspname 'S 1', relname 'T 1', fetch_count '100') |
+ (2 rows)
+
+ -- ===================================================================
+ -- simple queries
+ -- ===================================================================
+ -- single table, with/without alias
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
+ QUERY PLAN
+ ------------------------------------------------------------------------------------------------------------------------------
+ Limit
+ Output: c1, c2, c3, c4, c5, c6, c7
+ -> Sort
+ Output: c1, c2, c3, c4, c5, c6, c7
+ Sort Key: ft1.c3, ft1.c1
+ -> Foreign Scan on public.ft1
+ Output: c1, c2, c3, c4, c5, c6, c7
+ Remote SQL: DECLARE pgsql_fdw_cursor_0 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (8 rows)
+
+ SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ -----+----+-------+------------------------------+--------------------------+----+------------
+ 101 | 1 | 00101 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970 | 1 | 1
+ 102 | 2 | 00102 | Sat Jan 03 00:00:00 1970 PST | Sat Jan 03 00:00:00 1970 | 2 | 2
+ 103 | 3 | 00103 | Sun Jan 04 00:00:00 1970 PST | Sun Jan 04 00:00:00 1970 | 3 | 3
+ 104 | 4 | 00104 | Mon Jan 05 00:00:00 1970 PST | Mon Jan 05 00:00:00 1970 | 4 | 4
+ 105 | 5 | 00105 | Tue Jan 06 00:00:00 1970 PST | Tue Jan 06 00:00:00 1970 | 5 | 5
+ 106 | 6 | 00106 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6
+ 107 | 7 | 00107 | Thu Jan 08 00:00:00 1970 PST | Thu Jan 08 00:00:00 1970 | 7 | 7
+ 108 | 8 | 00108 | Fri Jan 09 00:00:00 1970 PST | Fri Jan 09 00:00:00 1970 | 8 | 8
+ 109 | 9 | 00109 | Sat Jan 10 00:00:00 1970 PST | Sat Jan 10 00:00:00 1970 | 9 | 9
+ 110 | 0 | 00110 | Sun Jan 11 00:00:00 1970 PST | Sun Jan 11 00:00:00 1970 | 0 | 0
+ (10 rows)
+
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+ QUERY PLAN
+ ------------------------------------------------------------------------------------------------------------------------------
+ Limit
+ Output: c1, c2, c3, c4, c5, c6, c7
+ -> Sort
+ Output: c1, c2, c3, c4, c5, c6, c7
+ Sort Key: t1.c3, t1.c1
+ -> Foreign Scan on public.ft1 t1
+ Output: c1, c2, c3, c4, c5, c6, c7
+ Remote SQL: DECLARE pgsql_fdw_cursor_2 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (8 rows)
+
+ SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ -----+----+-------+------------------------------+--------------------------+----+------------
+ 101 | 1 | 00101 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970 | 1 | 1
+ 102 | 2 | 00102 | Sat Jan 03 00:00:00 1970 PST | Sat Jan 03 00:00:00 1970 | 2 | 2
+ 103 | 3 | 00103 | Sun Jan 04 00:00:00 1970 PST | Sun Jan 04 00:00:00 1970 | 3 | 3
+ 104 | 4 | 00104 | Mon Jan 05 00:00:00 1970 PST | Mon Jan 05 00:00:00 1970 | 4 | 4
+ 105 | 5 | 00105 | Tue Jan 06 00:00:00 1970 PST | Tue Jan 06 00:00:00 1970 | 5 | 5
+ 106 | 6 | 00106 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6
+ 107 | 7 | 00107 | Thu Jan 08 00:00:00 1970 PST | Thu Jan 08 00:00:00 1970 | 7 | 7
+ 108 | 8 | 00108 | Fri Jan 09 00:00:00 1970 PST | Fri Jan 09 00:00:00 1970 | 8 | 8
+ 109 | 9 | 00109 | Sat Jan 10 00:00:00 1970 PST | Sat Jan 10 00:00:00 1970 | 9 | 9
+ 110 | 0 | 00110 | Sun Jan 11 00:00:00 1970 PST | Sun Jan 11 00:00:00 1970 | 0 | 0
+ (10 rows)
+
+ -- with WHERE clause
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
+ QUERY PLAN
+ ------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on public.ft1 t1
+ Output: c1, c2, c3, c4, c5, c6, c7
+ Filter: ((t1.c1 = 101) AND ((t1.c6)::text = '1'::text) AND (t1.c7 = '1'::bpchar))
+ Remote SQL: DECLARE pgsql_fdw_cursor_4 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (4 rows)
+
+ SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ -----+----+-------+------------------------------+--------------------------+----+------------
+ 101 | 1 | 00101 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970 | 1 | 1
+ (1 row)
+
+ -- aggregate
+ SELECT COUNT(*) FROM ft1 t1;
+ count
+ -------
+ 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
+ ----+----+-------+------------------------------+--------------------------+----+------------
+ 1 | 1 | 00001 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970 | 1 | 1
+ 2 | 2 | 00002 | Sat Jan 03 00:00:00 1970 PST | Sat Jan 03 00:00:00 1970 | 2 | 2
+ 3 | 3 | 00003 | Sun Jan 04 00:00:00 1970 PST | Sun Jan 04 00:00:00 1970 | 3 | 3
+ 4 | 4 | 00004 | Mon Jan 05 00:00:00 1970 PST | Mon Jan 05 00:00:00 1970 | 4 | 4
+ 5 | 5 | 00005 | Tue Jan 06 00:00:00 1970 PST | Tue Jan 06 00:00:00 1970 | 5 | 5
+ 6 | 6 | 00006 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6
+ 7 | 7 | 00007 | Thu Jan 08 00:00:00 1970 PST | Thu Jan 08 00:00:00 1970 | 7 | 7
+ 8 | 8 | 00008 | Fri Jan 09 00:00:00 1970 PST | Fri Jan 09 00:00:00 1970 | 8 | 8
+ 9 | 9 | 00009 | Sat Jan 10 00:00:00 1970 PST | Sat Jan 10 00:00:00 1970 | 9 | 9
+ 10 | 0 | 00010 | Sun Jan 11 00:00:00 1970 PST | Sun Jan 11 00:00:00 1970 | 0 | 0
+ (10 rows)
+
+ -- subquery+MAX
+ SELECT * FROM ft1 t1 WHERE t1.c3 = (SELECT MAX(c3) FROM ft2 t2) ORDER BY c1;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ ------+----+-------+------------------------------+--------------------------+----+------------
+ 1000 | 0 | 01000 | Thu Jan 01 00:00:00 1970 PST | Thu Jan 01 00:00:00 1970 | 0 | 0
+ (1 row)
+
+ -- used in CTE
+ WITH t1 AS (SELECT * FROM ft1 WHERE c1 <= 10) SELECT t2.c1, t2.c2, t2.c3, t2.c4 FROM t1, ft2 t2 WHERE t1.c1 = t2.c1 ORDER BY t1.c1;
+ c1 | c2 | c3 | c4
+ ----+----+-------+------------------------------
+ 1 | 1 | 00001 | Fri Jan 02 00:00:00 1970 PST
+ 2 | 2 | 00002 | Sat Jan 03 00:00:00 1970 PST
+ 3 | 3 | 00003 | Sun Jan 04 00:00:00 1970 PST
+ 4 | 4 | 00004 | Mon Jan 05 00:00:00 1970 PST
+ 5 | 5 | 00005 | Tue Jan 06 00:00:00 1970 PST
+ 6 | 6 | 00006 | Wed Jan 07 00:00:00 1970 PST
+ 7 | 7 | 00007 | Thu Jan 08 00:00:00 1970 PST
+ 8 | 8 | 00008 | Fri Jan 09 00:00:00 1970 PST
+ 9 | 9 | 00009 | Sat Jan 10 00:00:00 1970 PST
+ 10 | 0 | 00010 | Sun Jan 11 00:00:00 1970 PST
+ (10 rows)
+
+ -- fixed values
+ SELECT 'fixed', NULL FROM ft1 t1 WHERE c1 = 1;
+ ?column? | ?column?
+ ----------+----------
+ fixed |
+ (1 row)
+
+ -- user-defined operator/function
+ CREATE FUNCTION pgsql_fdw_abs(int) RETURNS int AS $$
+ BEGIN
+ RETURN abs($1);
+ END
+ $$ LANGUAGE plpgsql IMMUTABLE;
+ CREATE OPERATOR === (
+ LEFTARG = int,
+ RIGHTARG = int,
+ PROCEDURE = int4eq,
+ COMMUTATOR = ===,
+ NEGATOR = !==
+ );
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = pgsql_fdw_abs(t1.c2);
+ QUERY PLAN
+ ---------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = pgsql_fdw_abs(c2))
+ Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 === t1.c2;
+ QUERY PLAN
+ ---------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 === c2)
+ Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = abs(t1.c2);
+ QUERY PLAN
+ ---------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = abs(c2))
+ Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = t1.c2;
+ QUERY PLAN
+ ---------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = c2)
+ Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ -- ===================================================================
+ -- parameterized queries
+ -- ===================================================================
+ -- simple join
+ PREPARE st1(int, int) AS SELECT t1.c3, t2.c3 FROM ft1 t1, ft2 t2 WHERE t1.c1 = $1 AND t2.c1 = $2;
+ EXPLAIN (COSTS false) EXECUTE st1(1, 2);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------
+ Nested Loop
+ -> Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: SELECT "C 1", NULL, c3, NULL, NULL, NULL, NULL FROM "S 1"."T 1"
+ -> Materialize
+ -> Foreign Scan on ft2 t2
+ Filter: (c1 = 2)
+ Remote SQL: SELECT "C 1", NULL, c3, NULL, NULL, NULL, NULL FROM "S 1"."T 1"
+ (8 rows)
+
+ EXECUTE st1(1, 1);
+ c3 | c3
+ -------+-------
+ 00001 | 00001
+ (1 row)
+
+ EXECUTE st1(101, 101);
+ c3 | c3
+ -------+-------
+ 00101 | 00101
+ (1 row)
+
+ -- subquery using stable function (can't be pushed down)
+ PREPARE st2(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c4) = 6) ORDER BY c1;
+ EXPLAIN (COSTS false) EXECUTE st2(10, 20);
+ QUERY PLAN
+ ------------------------------------------------------------------------------------------------------
+ Sort
+ Sort Key: t1.c1
+ -> Hash Join
+ Hash Cond: (t1.c3 = t2.c3)
+ -> Foreign Scan on ft1 t1
+ Filter: (c1 < 20)
+ Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ -> Hash
+ -> HashAggregate
+ -> Foreign Scan on ft2 t2
+ Filter: ((c1 > 10) AND (date_part('dow'::text, c4) = 6::double precision))
+ Remote SQL: SELECT "C 1", NULL, c3, c4, NULL, NULL, NULL FROM "S 1"."T 1"
+ (12 rows)
+
+ EXECUTE st2(10, 20);
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ ----+----+-------+------------------------------+--------------------------+----+------------
+ 16 | 6 | 00016 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970 | 6 | 6
+ (1 row)
+
+ EXECUTE st1(101, 101);
+ c3 | c3
+ -------+-------
+ 00101 | 00101
+ (1 row)
+
+ -- subquery using immutable function (can be pushed down)
+ PREPARE st3(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c5) = 6) ORDER BY c1;
+ EXPLAIN (COSTS false) EXECUTE st3(10, 20);
+ QUERY PLAN
+ ------------------------------------------------------------------------------------------------------
+ Sort
+ Sort Key: t1.c1
+ -> Hash Join
+ Hash Cond: (t1.c3 = t2.c3)
+ -> Foreign Scan on ft1 t1
+ Filter: (c1 < 20)
+ Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ -> Hash
+ -> HashAggregate
+ -> Foreign Scan on ft2 t2
+ Filter: ((c1 > 10) AND (date_part('dow'::text, c5) = 6::double precision))
+ Remote SQL: SELECT "C 1", NULL, c3, NULL, c5, NULL, NULL FROM "S 1"."T 1"
+ (12 rows)
+
+ EXECUTE st3(10, 20);
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ ----+----+-------+------------------------------+--------------------------+----+------------
+ 16 | 6 | 00016 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970 | 6 | 6
+ (1 row)
+
+ EXECUTE st3(20, 30);
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ ----+----+-------+------------------------------+--------------------------+----+------------
+ 23 | 3 | 00023 | Sat Jan 24 00:00:00 1970 PST | Sat Jan 24 00:00:00 1970 | 3 | 3
+ (1 row)
+
+ -- custom plan should be chosen
+ PREPARE st4(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 = $1;
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ ---------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ ---------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ ---------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ ---------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ ---------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ ---------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = $1)
+ Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ -- cleanup
+ DEALLOCATE st1;
+ DEALLOCATE st2;
+ DEALLOCATE st3;
+ DEALLOCATE st4;
+ -- ===================================================================
+ -- connection management
+ -- ===================================================================
+ SELECT srvname, usename FROM pgsql_fdw_connections;
+ srvname | usename
+ -----------+----------------
+ loopback2 | pgsql_fdw_user
+ (1 row)
+
+ SELECT pgsql_fdw_disconnect(srvid, usesysid) FROM pgsql_fdw_get_connections();
+ pgsql_fdw_disconnect
+ ----------------------
+ OK
+ (1 row)
+
+ SELECT srvname, usename FROM pgsql_fdw_connections;
+ srvname | usename
+ ---------+---------
+ (0 rows)
+
+ -- ===================================================================
+ -- subtransaction
+ -- ===================================================================
+ BEGIN;
+ DECLARE c CURSOR FOR SELECT * FROM ft1 ORDER BY c1;
+ FETCH c;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ ----+----+-------+------------------------------+--------------------------+----+------------
+ 1 | 1 | 00001 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970 | 1 | 1
+ (1 row)
+
+ SAVEPOINT s;
+ ERROR OUT; -- ERROR
+ ERROR: syntax error at or near "ERROR"
+ LINE 1: ERROR OUT;
+ ^
+ ROLLBACK TO s;
+ SELECT srvname FROM pgsql_fdw_connections;
+ srvname
+ -----------
+ loopback2
+ (1 row)
+
+ FETCH c;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ ----+----+-------+------------------------------+--------------------------+----+------------
+ 2 | 2 | 00002 | Sat Jan 03 00:00:00 1970 PST | Sat Jan 03 00:00:00 1970 | 2 | 2
+ (1 row)
+
+ COMMIT;
+ SELECT srvname FROM pgsql_fdw_connections;
+ srvname
+ -----------
+ loopback2
+ (1 row)
+
+ ERROR OUT; -- ERROR
+ ERROR: syntax error at or near "ERROR"
+ LINE 1: ERROR OUT;
+ ^
+ SELECT srvname FROM pgsql_fdw_connections;
+ srvname
+ ---------
+ (0 rows)
+
+ -- ===================================================================
+ -- cleanup
+ -- ===================================================================
+ DROP OPERATOR === (int, int) CASCADE;
+ DROP OPERATOR !== (int, int) CASCADE;
+ DROP FUNCTION pgsql_fdw_abs(int);
+ DROP SCHEMA "S 1" CASCADE;
+ NOTICE: drop cascades to 2 other objects
+ DETAIL: drop cascades to table "S 1"."T 1"
+ drop cascades to table "S 1"."T 2"
+ DROP EXTENSION pgsql_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 server loopback2
+ drop cascades to user mapping for pgsql_fdw_user
+ drop cascades to foreign table ft1
+ drop cascades to foreign table ft2
+ \c
+ DROP ROLE pgsql_fdw_user;
diff --git a/contrib/pgsql_fdw/option.c b/contrib/pgsql_fdw/option.c
index ...a2a8927 .
*** a/contrib/pgsql_fdw/option.c
--- b/contrib/pgsql_fdw/option.c
***************
*** 0 ****
--- 1,242 ----
+ /*-------------------------------------------------------------------------
+ *
+ * option.c
+ * FDW option handling
+ *
+ * Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/option.c
+ *
+ *-------------------------------------------------------------------------
+ */
+ #include "postgres.h"
+
+ #include "access/reloptions.h"
+ #include "catalog/pg_foreign_data_wrapper.h"
+ #include "catalog/pg_foreign_server.h"
+ #include "catalog/pg_foreign_table.h"
+ #include "catalog/pg_user_mapping.h"
+ #include "commands/defrem.h"
+ #include "fmgr.h"
+ #include "foreign/foreign.h"
+ #include "lib/stringinfo.h"
+ #include "miscadmin.h"
+
+ #include "pgsql_fdw.h"
+
+ /*
+ * SQL functions
+ */
+ extern Datum pgsql_fdw_validator(PG_FUNCTION_ARGS);
+ PG_FUNCTION_INFO_V1(pgsql_fdw_validator);
+
+ /*
+ * Describes the valid options for objects that use this wrapper.
+ */
+ typedef struct PgsqlFdwOption
+ {
+ const char *optname;
+ Oid optcontext; /* Oid of catalog in which options may appear */
+ bool is_libpq_opt; /* true if it's used in libpq */
+ } PgsqlFdwOption;
+
+ /*
+ * Valid options for pgsql_fdw.
+ */
+ static PgsqlFdwOption valid_options[] = {
+
+ /*
+ * Options for libpq connection.
+ * Note: This list should be updated along with PQconninfoOptions in
+ * interfaces/libpq/fe-connect.c, so the order is kept as is.
+ *
+ * Some useless libpq connection options are not accepted by pgsql_fdw:
+ * client_encoding: set to local database encoding automatically
+ * fallback_application_name: fixed to "pgsql_fdw"
+ * replication: pgsql_fdw never be replication client
+ */
+ {"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},
+ #ifdef NOT_USED
+ {"client_encoding", ForeignServerRelationId, true},
+ #endif
+ {"tty", ForeignServerRelationId, true},
+ {"options", ForeignServerRelationId, true},
+ {"application_name", ForeignServerRelationId, true},
+ #ifdef NOT_USED
+ {"fallback_application_name", ForeignServerRelationId, true},
+ #endif
+ {"keepalives", ForeignServerRelationId, true},
+ {"keepalives_idle", ForeignServerRelationId, true},
+ {"keepalives_interval", ForeignServerRelationId, true},
+ {"keepalives_count", ForeignServerRelationId, true},
+ {"requiressl", ForeignServerRelationId, true},
+ {"sslcompression", ForeignServerRelationId, true},
+ {"sslmode", ForeignServerRelationId, true},
+ {"sslcert", ForeignServerRelationId, true},
+ {"sslkey", ForeignServerRelationId, true},
+ {"sslrootcert", ForeignServerRelationId, true},
+ {"sslcrl", ForeignServerRelationId, true},
+ {"requirepeer", ForeignServerRelationId, true},
+ {"krbsrvname", ForeignServerRelationId, true},
+ {"gsslib", ForeignServerRelationId, true},
+ #ifdef NOT_USED
+ {"replication", ForeignServerRelationId, true},
+ #endif
+
+ /*
+ * Options for translation of object names.
+ */
+ {"nspname", ForeignTableRelationId, false},
+ {"relname", ForeignTableRelationId, false},
+ {"colname", AttributeRelationId, false},
+
+ /*
+ * Options for cursor behavior.
+ * These options can be overridden by finer-grained objects.
+ */
+ {"fetch_count", ForeignTableRelationId, false},
+ {"fetch_count", ForeignServerRelationId, false},
+
+ /* Terminating entry --- MUST BE LAST */
+ {NULL, InvalidOid, false}
+ };
+
+ /*
+ * Helper functions
+ */
+ static bool is_valid_option(const char *optname, Oid context);
+
+ /*
+ * Validate the generic options given to a FOREIGN DATA WRAPPER, SERVER,
+ * USER MAPPING or FOREIGN TABLE that uses pgsql_fdw.
+ *
+ * Raise an ERROR if the option or its value is considered invalid.
+ */
+ Datum
+ pgsql_fdw_validator(PG_FUNCTION_ARGS)
+ {
+ List *options_list = untransformRelOptions(PG_GETARG_DATUM(0));
+ Oid catalog = PG_GETARG_OID(1);
+ ListCell *cell;
+
+ /*
+ * Check that only options supported by pgsql_fdw, and allowed for the
+ * current object type, are given.
+ */
+ foreach(cell, options_list)
+ {
+ DefElem *def = (DefElem *) lfirst(cell);
+
+ if (!is_valid_option(def->defname, catalog))
+ {
+ PgsqlFdwOption *opt;
+ StringInfoData buf;
+
+ /*
+ * Unknown option specified, complain about it. Provide a hint
+ * with list of valid options for the object.
+ */
+ initStringInfo(&buf);
+ for (opt = valid_options; opt->optname; opt++)
+ {
+ if (catalog == opt->optcontext)
+ appendStringInfo(&buf, "%s%s", (buf.len > 0) ? ", " : "",
+ opt->optname);
+ }
+
+ ereport(ERROR,
+ (errcode(ERRCODE_FDW_INVALID_OPTION_NAME),
+ errmsg("invalid option \"%s\"", def->defname),
+ errhint("Valid options in this context are: %s",
+ buf.data)));
+ }
+
+ /* fetch_count be positive digit number. */
+ if (strcmp(def->defname, "fetch_count") == 0)
+ {
+ long value;
+ char *p = NULL;
+
+ value = strtol(defGetString(def), &p, 10);
+ if (*p != '\0' || value < 1)
+ ereport(ERROR,
+ (errcode(ERRCODE_FDW_INVALID_ATTRIBUTE_VALUE),
+ errmsg("invalid value for %s: \"%s\"",
+ def->defname, defGetString(def))));
+ }
+ }
+
+ /*
+ * We don't care option-specific limitation here; they will be validated at
+ * the execution time.
+ */
+
+ PG_RETURN_VOID();
+ }
+
+ /*
+ * Check whether the given option is one of the valid pgsql_fdw options.
+ * context is the Oid of the catalog holding the object the option is for.
+ */
+ static bool
+ is_valid_option(const char *optname, Oid context)
+ {
+ PgsqlFdwOption *opt;
+
+ for (opt = valid_options; opt->optname; opt++)
+ {
+ if (context == opt->optcontext && strcmp(opt->optname, optname) == 0)
+ return true;
+ }
+ return false;
+ }
+
+ /*
+ * Check whether the given option is one of the valid libpq options.
+ * context is the Oid of the catalog holding the object the option is for.
+ */
+ static bool
+ is_libpq_option(const char *optname)
+ {
+ PgsqlFdwOption *opt;
+
+ for (opt = valid_options; opt->optname; opt++)
+ {
+ if (strcmp(opt->optname, optname) == 0 && opt->is_libpq_opt)
+ return true;
+ }
+ return false;
+ }
+
+ /*
+ * Generate key-value arrays which includes only libpq options from the list
+ * which contains any kind of options.
+ */
+ int
+ ExtractConnectionOptions(List *defelems, const char **keywords, const char **values)
+ {
+ ListCell *lc;
+ int i;
+
+ i = 0;
+ foreach(lc, defelems)
+ {
+ DefElem *d = (DefElem *) lfirst(lc);
+ if (is_libpq_option(d->defname))
+ {
+ keywords[i] = d->defname;
+ values[i] = defGetString(d);
+ i++;
+ }
+ }
+ return i;
+ }
diff --git a/contrib/pgsql_fdw/pgsql_fdw--1.0.sql b/contrib/pgsql_fdw/pgsql_fdw--1.0.sql
index ...b0ea2b2 .
*** a/contrib/pgsql_fdw/pgsql_fdw--1.0.sql
--- b/contrib/pgsql_fdw/pgsql_fdw--1.0.sql
***************
*** 0 ****
--- 1,39 ----
+ /* contrib/pgsql_fdw/pgsql_fdw--1.0.sql */
+
+ -- complain if script is sourced in psql, rather than via CREATE EXTENSION
+ \echo Use "CREATE EXTENSION pgsql_fdw" to load this file. \quit
+
+ CREATE FUNCTION pgsql_fdw_handler()
+ RETURNS fdw_handler
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C STRICT;
+
+ CREATE FUNCTION pgsql_fdw_validator(text[], oid)
+ RETURNS void
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C STRICT;
+
+ CREATE FOREIGN DATA WRAPPER pgsql_fdw
+ HANDLER pgsql_fdw_handler
+ VALIDATOR pgsql_fdw_validator;
+
+ /* connection management functions and view */
+ CREATE FUNCTION pgsql_fdw_get_connections(out srvid oid, out usesysid oid)
+ RETURNS SETOF record
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C STRICT;
+
+ CREATE FUNCTION pgsql_fdw_disconnect(oid, oid)
+ RETURNS text
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C STRICT;
+
+ CREATE VIEW pgsql_fdw_connections AS
+ SELECT c.srvid srvid,
+ s.srvname srvname,
+ c.usesysid usesysid,
+ pg_get_userbyid(c.usesysid) usename
+ FROM pgsql_fdw_get_connections() c
+ JOIN pg_catalog.pg_foreign_server s ON (s.oid = c.srvid);
+ GRANT SELECT ON pgsql_fdw_connections TO public;
+
diff --git a/contrib/pgsql_fdw/pgsql_fdw.c b/contrib/pgsql_fdw/pgsql_fdw.c
index ...61527a9 .
*** a/contrib/pgsql_fdw/pgsql_fdw.c
--- b/contrib/pgsql_fdw/pgsql_fdw.c
***************
*** 0 ****
--- 1,1042 ----
+ /*-------------------------------------------------------------------------
+ *
+ * pgsql_fdw.c
+ * foreign-data wrapper for remote PostgreSQL servers.
+ *
+ * Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/pgsql_fdw.c
+ *
+ *-------------------------------------------------------------------------
+ */
+ #include "postgres.h"
+ #include "fmgr.h"
+
+ #include "catalog/pg_foreign_server.h"
+ #include "catalog/pg_foreign_table.h"
+ #include "commands/defrem.h"
+ #include "commands/explain.h"
+ #include "foreign/fdwapi.h"
+ #include "funcapi.h"
+ #include "miscadmin.h"
+ #include "optimizer/cost.h"
+ #include "optimizer/pathnode.h"
+ #include "optimizer/planmain.h"
+ #include "optimizer/restrictinfo.h"
+ #include "utils/lsyscache.h"
+ #include "utils/memutils.h"
+ #include "utils/rel.h"
+
+ #include "pgsql_fdw.h"
+ #include "connection.h"
+
+ PG_MODULE_MAGIC;
+
+ /*
+ * Default fetch count for cursor. This can be overridden by fetch_count FDW
+ * option.
+ */
+ #define DEFAULT_FETCH_COUNT 10000
+
+ /*
+ * Cost to establish a connection.
+ * XXX: should be configurable per server?
+ */
+ #define CONNECTION_COSTS 100.0
+
+ /*
+ * Cost to transfer 1 byte from remote server.
+ * XXX: should be configurable per server?
+ */
+ #define TRANSFER_COSTS_PER_BYTE 0.001
+
+ /*
+ * Cursors which are used together in a local query require different name, so
+ * we use simple incremental name for that purpose. We don't care wrap around
+ * of cursor_id because it's hard to imagine that 2^32 cursors are used in a
+ * query.
+ */
+ #define CURSOR_NAME_FORMAT "pgsql_fdw_cursor_%u"
+ static uint32 cursor_id = 0;
+
+ /*
+ * FDW-specific information for RelOptInfo.fdw_private. This is used to pass
+ * information from pgsqlGetForeignRelSize to pgsqlGetForeignPaths.
+ */
+ typedef struct PgsqlFdwPlanState {
+ /*
+ * These are generated in GetForeignRelSize, and also used in subsequent
+ * GetForeignPaths.
+ */
+ StringInfoData sql;
+ Cost startup_cost;
+ Cost total_cost;
+
+ /* Cached catalog information. */
+ ForeignTable *table;
+ ForeignServer *server;
+ } PgsqlFdwPlanState;
+
+ /*
+ * Index of FDW-private information stored in fdw_private list.
+ *
+ * We store various information in ForeignScan.fdw_private to pass them beyond
+ * the boundary between planner and executor. Finally FdwPlan using cursor
+ * would hold items below:
+ *
+ * 1) plain SELECT statement
+ * 2) SQL statement used to declare cursor
+ * 3) SQL statement used to fetch rows from cursor
+ * 4) SQL statement used to reset cursor
+ * 5) SQL statement used to close cursor
+ *
+ * These items are indexed with the enum FdwPrivateIndex, so an item
+ * can be accessed directly via list_nth(). For example of FETCH
+ * statement:
+ * list_nth(fdw_private, FdwPrivateFetchSql)
+ */
+ enum FdwPrivateIndex {
+ /* SQL statements */
+ FdwPrivateSelectSql,
+ FdwPrivateDeclareSql,
+ FdwPrivateFetchSql,
+ FdwPrivateResetSql,
+ FdwPrivateCloseSql,
+
+ /* # of elements stored in the list fdw_private */
+ FdwPrivateNum,
+ };
+
+ /*
+ * Describes an execution state of a foreign scan against a foreign table
+ * using pgsql_fdw.
+ */
+ typedef struct PgsqlFdwExecutionState
+ {
+ MemoryContext scan_cxt; /* context for per-scan lifespan data */
+
+ List *fdw_private; /* FDW-private information */
+ PGconn *conn; /* connection for the scan */
+
+ Oid *param_types; /* type array of external parameter */
+ const char **param_values; /* value array of external parameter */
+
+ int attnum; /* # of non-dropped attribute */
+ char **col_values; /* column value buffer */
+ AttInMetadata *attinmeta; /* attribute metadata */
+
+ Tuplestorestate *tuples; /* result of the scan */
+ } PgsqlFdwExecutionState;
+
+ /*
+ * SQL functions
+ */
+ extern Datum pgsql_fdw_handler(PG_FUNCTION_ARGS);
+ PG_FUNCTION_INFO_V1(pgsql_fdw_handler);
+
+ /*
+ * FDW callback routines
+ */
+ static void pgsqlGetForeignRelSize(PlannerInfo *root,
+ RelOptInfo *baserel,
+ Oid foreigntableid);
+ static void pgsqlGetForeignPaths(PlannerInfo *root,
+ RelOptInfo *baserel,
+ Oid foreigntableid);
+ static ForeignScan *pgsqlGetForeignPlan(PlannerInfo *root,
+ RelOptInfo *baserel,
+ Oid foreigntableid,
+ ForeignPath *best_path,
+ List *tlist,
+ List *scan_clauses);
+ static void pgsqlExplainForeignScan(ForeignScanState *node, ExplainState *es);
+ static void pgsqlBeginForeignScan(ForeignScanState *node, int eflags);
+ static TupleTableSlot *pgsqlIterateForeignScan(ForeignScanState *node);
+ static void pgsqlReScanForeignScan(ForeignScanState *node);
+ static void pgsqlEndForeignScan(ForeignScanState *node);
+
+ /*
+ * Helper functions
+ */
+ static void get_remote_estimate(const char *sql,
+ PGconn *conn,
+ double *rows,
+ int *width,
+ Cost *startup_cost,
+ Cost *total_cost);
+ static void adjust_costs(double rows, int width,
+ Cost *startup_cost, Cost *total_cost);
+ static void execute_query(ForeignScanState *node);
+ static PGresult *fetch_result(ForeignScanState *node);
+ static void store_result(ForeignScanState *node, PGresult *res);
+
+ /* Exported functions, but not written in pgsql_fdw.h. */
+ void _PG_init(void);
+ void _PG_fini(void);
+
+ /*
+ * Module-specific initialization.
+ */
+ void
+ _PG_init(void)
+ {
+ }
+
+ /*
+ * Module-specific clean up.
+ */
+ void
+ _PG_fini(void)
+ {
+ }
+
+ /*
+ * The content of FdwRoutine of pgsql_fdw is never changed, so we use one
+ * static object for all scan.
+ */
+ static FdwRoutine fdwroutine = {
+
+ /* Node type */
+ T_FdwRoutine,
+
+ /* Required handler functions. */
+ pgsqlGetForeignRelSize,
+ pgsqlGetForeignPaths,
+ pgsqlGetForeignPlan,
+ pgsqlExplainForeignScan,
+ pgsqlBeginForeignScan,
+ pgsqlIterateForeignScan,
+ pgsqlReScanForeignScan,
+ pgsqlEndForeignScan,
+
+ /* Optional handler functions. */
+ };
+
+ /*
+ * Foreign-data wrapper handler function: return a struct with pointers
+ * to my callback routines.
+ */
+ Datum
+ pgsql_fdw_handler(PG_FUNCTION_ARGS)
+ {
+ PG_RETURN_POINTER(&fdwroutine);
+ }
+
+ /*
+ * pgsqlGetForeignRelSize
+ * Estimate # of rows and width of the result of the scan
+ *
+ * Here we estimate number of rows returned by the scan in two steps. In the
+ * first step, we execute remote EXPLAIN command to obtain the number of rows
+ * returned from remote side. In the second step, we calculate the selectivity
+ * of the filtering done on local side, and modify first estimate.
+ *
+ * We have to get some catalog objects and generate remote query string here,
+ * so we store such expensive information in FDW private area of RelOptInfo and
+ * pass them to subsequent functions for reuse.
+ */
+ static void
+ pgsqlGetForeignRelSize(PlannerInfo *root,
+ RelOptInfo *baserel,
+ Oid foreigntableid)
+ {
+ StringInfo sql;
+ ForeignTable *table;
+ ForeignServer *server;
+ UserMapping *user;
+ PGconn *conn;
+ double rows;
+ int width;
+ Cost startup_cost;
+ Cost total_cost;
+ PgsqlFdwPlanState *fpstate;
+ Selectivity sel;
+
+ /*
+ * We use PgsqlFdwPlanState to pass various information to subsequent
+ * functions.
+ */
+ fpstate = palloc0(sizeof(PgsqlFdwPlanState));
+ initStringInfo(&fpstate->sql);
+ sql = &fpstate->sql;
+
+ /* Retrieve catalog objects which are necessary to estimate rows. */
+ table = GetForeignTable(foreigntableid);
+ server = GetForeignServer(table->serverid);
+ user = GetUserMapping(GetOuterUserId(), server->serverid);
+
+ /*
+ * Create plain SELECT statement with no WHERE clause for this scan in
+ * order to obtain meaningful rows estimation by executing EXPLAIN on
+ * remote server.
+ */
+ deparseSimpleSql(sql, foreigntableid, root, baserel, table);
+ conn = GetConnection(server, user, false);
+ get_remote_estimate(sql->data, conn, &rows, &width,
+ &startup_cost, &total_cost);
+ ReleaseConnection(conn);
+
+ /*
+ * Estimate selectivity of local filtering by calling
+ * clauselist_selectivity() against baserestrictinfo, and modify rows
+ * estimate with it.
+ */
+ sel = clauselist_selectivity(root, baserel->baserestrictinfo,
+ baserel->relid, JOIN_INNER, NULL);
+ baserel->rows = rows * sel;
+ baserel->width = width;
+
+ /*
+ * Pack obtained information into a object and store it in FDW-private area
+ * of RelOptInfo to pass them to subsequent functions.
+ */
+ fpstate->startup_cost = startup_cost;
+ fpstate->total_cost = total_cost;
+ fpstate->table = table;
+ fpstate->server = server;
+ baserel->fdw_private = (void *) fpstate;
+ }
+
+ /*
+ * pgsqlGetForeignPaths
+ * Create possible scan paths for a scan on the foreign table
+ */
+ static void
+ pgsqlGetForeignPaths(PlannerInfo *root,
+ RelOptInfo *baserel,
+ Oid foreigntableid)
+ {
+ PgsqlFdwPlanState *fpstate = (PgsqlFdwPlanState *) baserel->fdw_private;
+ ForeignPath *path;
+ Cost startup_cost;
+ Cost total_cost;
+ List *fdw_private;
+
+ /*
+ * We have cost values which are estimated on remote side, so use them to
+ * estimate better costs which respect various stuffs to complete the scan,
+ * such as sending query, transferring result, and local filtering.
+ *
+ * XXX We assume that remote cost factors are same as local, but it might
+ * be worth to make configurable.
+ */
+ startup_cost = fpstate->startup_cost;
+ total_cost = fpstate->total_cost;
+ adjust_costs(baserel->rows, baserel->width, &startup_cost, &total_cost);
+
+ /* Construct list of SQL statements and bind it with the path. */
+ fdw_private = lappend(NIL, makeString(fpstate->sql.data));
+
+ /*
+ * Create simplest ForeignScan path node and add it to baserel. This path
+ * corresponds to SeqScan path of regular tables.
+ */
+ path = create_foreignscan_path(root, baserel,
+ baserel->rows,
+ startup_cost,
+ total_cost,
+ NIL, /* no pathkeys */
+ NULL, /* no outer rel either */
+ NIL, /* no param clause */
+ fdw_private);
+ add_path(baserel, (Path *) path);
+
+ /*
+ * XXX We can consider sorted path or parameterized path here if we know
+ * that foreign table is indexed on remote end. For this purpose, we
+ * might have to support FOREIGN INDEX to represent possible sets of sort
+ * keys and/or filtering.
+ */
+ }
+
+ /*
+ * pgsqlGetForeignPlan
+ * Create ForeignScan plan node which implements selected best path
+ */
+ static ForeignScan *
+ pgsqlGetForeignPlan(PlannerInfo *root,
+ RelOptInfo *baserel,
+ Oid foreigntableid,
+ ForeignPath *best_path,
+ List *tlist,
+ List *scan_clauses)
+ {
+ PgsqlFdwPlanState *fpstate = (PgsqlFdwPlanState *) baserel->fdw_private;
+ Index scan_relid = baserel->relid;
+ List *fdw_private = NIL;
+ ListCell *lc;
+ char name[128]; /* must be larger than format + 10 */
+ StringInfoData cursor;
+ DefElem *def;
+ int fetch_count = DEFAULT_FETCH_COUNT;
+ char *sql;
+ ForeignTable *table;
+ ForeignServer *server;
+
+ /*
+ * We have no native ability to evaluate restriction clauses, so we just
+ * put all the scan_clauses into the plan node's qual list for the
+ * executor to check. So all we have to do here is strip RestrictInfo
+ * nodes from the clauses and ignore pseudoconstants (which will be
+ * handled elsewhere).
+ */
+ scan_clauses = extract_actual_clauses(scan_clauses, false);
+
+ /*
+ * Use specified fetch_count instead of default value, if any. Foreign
+ * table option overrides server option.
+ */
+ table = fpstate->table;
+ foreach(lc, table->options)
+ {
+ def = (DefElem *) lfirst(lc);
+ if (strcmp(def->defname, "fetch_count") == 0)
+ break;
+
+ }
+ if (lc == NULL)
+ {
+ server = fpstate->server;
+ foreach(lc, server->options)
+ {
+ def = (DefElem *) lfirst(lc);
+ if (strcmp(def->defname, "fetch_count") == 0)
+ break;
+
+ }
+ }
+ if (lc != NULL)
+ fetch_count = strtol(defGetString(def), NULL, 10);
+ elog(DEBUG1, "relid=%u fetch_count=%d", foreigntableid, fetch_count);
+
+ /*
+ * Construct cursor name with sequential value.
+ */
+ sprintf(name, CURSOR_NAME_FORMAT, cursor_id++);
+
+ /*
+ * Construct CURSOR statements from plain remote query, and make a list
+ * contains all of them to pass them to executor with plan node for later
+ * use.
+ */
+ sql = fpstate->sql.data;
+ fdw_private = lappend(fdw_private, makeString(sql));
+
+ initStringInfo(&cursor);
+ appendStringInfo(&cursor, "DECLARE %s SCROLL CURSOR FOR %s", name, sql);
+ fdw_private = lappend(fdw_private, makeString(cursor.data));
+
+ initStringInfo(&cursor);
+ appendStringInfo(&cursor, "FETCH %d FROM %s", fetch_count, name);
+ fdw_private = lappend(fdw_private, makeString(cursor.data));
+
+ initStringInfo(&cursor);
+ appendStringInfo(&cursor, "MOVE ABSOLUTE 0 FROM %s", name);
+ fdw_private = lappend(fdw_private, makeString(cursor.data));
+
+ initStringInfo(&cursor);
+ appendStringInfo(&cursor, "CLOSE %s", name);
+ fdw_private = lappend(fdw_private, makeString(cursor.data));
+
+ /* Create the ForeignScan node with fdw_private of selected path. */
+ return make_foreignscan(tlist,
+ scan_clauses,
+ scan_relid,
+ NIL,
+ fdw_private);
+ }
+
+ /*
+ * pgsqlExplainForeignScan
+ * Produce extra output for EXPLAIN
+ */
+ static void
+ pgsqlExplainForeignScan(ForeignScanState *node, ExplainState *es)
+ {
+ List *fdw_private;
+ char *sql;
+
+ /* CURSOR declaration is shown in only VERBOSE mode. */
+ fdw_private = ((ForeignScan *) node->ss.ps.plan)->fdw_private;
+ if (es->verbose)
+ sql = strVal(list_nth(fdw_private, FdwPrivateDeclareSql));
+ else
+ sql = strVal(list_nth(fdw_private, FdwPrivateSelectSql));
+ ExplainPropertyText("Remote SQL", sql, es);
+ }
+
+ /*
+ * pgsqlBeginForeignScan
+ * Initiate access to a foreign PostgreSQL table.
+ */
+ static void
+ pgsqlBeginForeignScan(ForeignScanState *node, int eflags)
+ {
+ PgsqlFdwExecutionState *festate;
+ PGconn *conn;
+ Oid relid;
+ ForeignTable *table;
+ ForeignServer *server;
+ UserMapping *user;
+ TupleTableSlot *slot = node->ss.ss_ScanTupleSlot;
+
+ /*
+ * Do nothing in EXPLAIN (no ANALYZE) case. node->fdw_state stays NULL.
+ */
+ if (eflags & EXEC_FLAG_EXPLAIN_ONLY)
+ return;
+
+ /*
+ * Save state in node->fdw_state.
+ */
+ festate = (PgsqlFdwExecutionState *) palloc(sizeof(PgsqlFdwExecutionState));
+ festate->fdw_private = ((ForeignScan *) node->ss.ps.plan)->fdw_private;
+
+ /*
+ * Create context for per-scan tuplestore under per-query context.
+ */
+ festate->scan_cxt = AllocSetContextCreate(node->ss.ps.state->es_query_cxt,
+ "pgsql_fdw",
+ ALLOCSET_DEFAULT_MINSIZE,
+ ALLOCSET_DEFAULT_INITSIZE,
+ ALLOCSET_DEFAULT_MAXSIZE);
+
+ /*
+ * Get connection to the foreign server. Connection manager would
+ * establish new connection if necessary.
+ */
+ relid = RelationGetRelid(node->ss.ss_currentRelation);
+ table = GetForeignTable(relid);
+ server = GetForeignServer(table->serverid);
+ user = GetUserMapping(GetOuterUserId(), server->serverid);
+ conn = GetConnection(server, user, true);
+ festate->conn = conn;
+
+ /* Result will be filled in first Iterate call. */
+ festate->tuples = NULL;
+
+ /* Allocate buffers for column values. */
+ {
+ TupleDesc tupdesc = slot->tts_tupleDescriptor;
+ festate->col_values = palloc(sizeof(char *) * tupdesc->natts);
+ festate->attinmeta = TupleDescGetAttInMetadata(tupdesc);
+ }
+
+ /* Allocate buffers for query parameters. */
+ {
+ ParamListInfo params = node->ss.ps.state->es_param_list_info;
+ int numParams = params ? params->numParams : 0;
+
+ if (numParams > 0)
+ {
+ festate->param_types = palloc0(sizeof(Oid) * numParams);
+ festate->param_values = palloc0(sizeof(char *) * numParams);
+ }
+ else
+ {
+ festate->param_types = NULL;
+ festate->param_values = NULL;
+ }
+ }
+
+ /* Store FDW-specific state into ForeignScanState */
+ node->fdw_state = (void *) festate;
+
+ return;
+ }
+
+ /*
+ * pgsqlIterateForeignScan
+ * Retrieve next row from the result set, or clear tuple slot to indicate
+ * EOF.
+ *
+ * Note that using per-query context when retrieving tuples from
+ * tuplestore to ensure that returned tuples can survive until next
+ * iteration because the tuple is released implicitly via ExecClearTuple.
+ * Retrieving a tuple from tuplestore in CurrentMemoryContext (it's a
+ * per-tuple context), ExecClearTuple will free dangling pointer.
+ */
+ static TupleTableSlot *
+ pgsqlIterateForeignScan(ForeignScanState *node)
+ {
+ PgsqlFdwExecutionState *festate;
+ TupleTableSlot *slot = node->ss.ss_ScanTupleSlot;
+ PGresult *res = NULL;
+ MemoryContext oldcontext = CurrentMemoryContext;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+
+ /*
+ * If this is the first call after Begin, we need to execute remote query.
+ *
+ * Since we needs cursor to prevent out of memory, we declare a cursor at
+ * the first call and fetch from it in later calls.
+ */
+ if (festate->tuples == NULL)
+ execute_query(node);
+
+ /*
+ * If enough tuples are left in tuplestore, just return next tuple from it.
+ *
+ * It is necessary to switch to per-scan context to make returned tuple
+ * valid until next IterateForeignScan call, because it will be released
+ * with ExecClearTuple then. Otherwise, picked tuple is allocated in
+ * per-tuple context, and double-free of that tuple might happen.
+ */
+ MemoryContextSwitchTo(festate->scan_cxt);
+ if (tuplestore_gettupleslot(festate->tuples, true, false, slot))
+ {
+ MemoryContextSwitchTo(oldcontext);
+ return slot;
+ }
+ MemoryContextSwitchTo(oldcontext);
+
+ /*
+ * Here we need to clear partial result and fetch next bunch of tuples from
+ * from the cursor for the scan. If the fetch returns no tuple, the scan
+ * has reached the end.
+ *
+ * PGresult must be released before leaving this function.
+ */
+ PG_TRY();
+ {
+ res = fetch_result(node);
+ store_result(node, res);
+ PQclear(res);
+ res = NULL;
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+
+ /*
+ * If we got more tuples from the server cursor, return next tuple from
+ * tuplestore.
+ */
+ MemoryContextSwitchTo(festate->scan_cxt);
+ if (tuplestore_gettupleslot(festate->tuples, true, false, slot))
+ {
+ MemoryContextSwitchTo(oldcontext);
+ return slot;
+ }
+ MemoryContextSwitchTo(oldcontext);
+
+ /* We don't have any result even in remote server cursor. */
+ ExecClearTuple(slot);
+ return slot;
+ }
+
+ /*
+ * pgsqlReScanForeignScan
+ * - Restart this scan by resetting fetch location.
+ */
+ static void
+ pgsqlReScanForeignScan(ForeignScanState *node)
+ {
+ List *fdw_private;
+ char *sql;
+ PGconn *conn;
+ PGresult *res = NULL;
+ PgsqlFdwExecutionState *festate;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+
+ /* If we have not opened cursor yet, nothing to do. */
+ if (festate->tuples == NULL)
+ return;
+
+ /* Discard fetch results if any. */
+ tuplestore_clear(festate->tuples);
+
+ /* PGresult must be released before leaving this function. */
+ PG_TRY();
+ {
+ /* Reset cursor */
+ fdw_private = festate->fdw_private;
+ conn = festate->conn;
+ sql = strVal(list_nth(fdw_private, FdwPrivateResetSql));
+ res = PQexec(conn, sql);
+ if (PQresultStatus(res) != PGRES_COMMAND_OK)
+ {
+ ereport(ERROR,
+ (errmsg("could not rewind cursor"),
+ errdetail("%s", PQerrorMessage(conn)),
+ errhint("%s", sql)));
+ }
+ PQclear(res);
+ res = NULL;
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+ }
+
+ /*
+ * pgsqlEndForeignScan
+ * Finish scanning foreign table and dispose objects used for this scan
+ */
+ static void
+ pgsqlEndForeignScan(ForeignScanState *node)
+ {
+ List *fdw_private;
+ char *sql;
+ PGconn *conn;
+ PGresult *res = NULL;
+ PgsqlFdwExecutionState *festate;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+
+ /* if festate is NULL, we are in EXPLAIN; nothing to do */
+ if (festate == NULL)
+ return;
+
+ /* If we have not opened cursor yet, nothing to do. */
+ if (festate->tuples == NULL)
+ return;
+
+ /* Discard fetch results */
+ tuplestore_end(festate->tuples);
+ festate->tuples = NULL;
+
+ /* PGresult must be released before leaving this function. */
+ PG_TRY();
+ {
+ /* Close cursor */
+ fdw_private = festate->fdw_private;
+ conn = festate->conn;
+ sql = strVal(list_nth(fdw_private, FdwPrivateCloseSql));
+ res = PQexec(conn, sql);
+ if (PQresultStatus(res) != PGRES_COMMAND_OK)
+ {
+ ereport(ERROR,
+ (errmsg("could not close cursor"),
+ errdetail("%s", PQerrorMessage(conn)),
+ errhint("%s", sql)));
+ }
+ PQclear(res);
+ res = NULL;
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+
+ ReleaseConnection(festate->conn);
+
+ MemoryContextDelete(festate->scan_cxt);
+ festate->scan_cxt = NULL;
+ }
+
+ /*
+ * Estimate costs of executing given SQL statement.
+ */
+ static void
+ get_remote_estimate(const char *sql, PGconn *conn,
+ double *rows, int *width,
+ Cost *startup_cost, Cost *total_cost)
+ {
+ PGresult *res = NULL;
+ StringInfoData buf;
+ char *plan;
+ char *p;
+ int n;
+
+ /*
+ * Construct EXPLAIN statement with given SQL statement.
+ */
+ initStringInfo(&buf);
+ appendStringInfo(&buf, "EXPLAIN %s", sql);
+
+ /* PGresult must be released before leaving this function. */
+ PG_TRY();
+ {
+ res = PQexec(conn, buf.data);
+ if (PQresultStatus(res) != PGRES_TUPLES_OK || PQntuples(res) == 0)
+ {
+ char *msg;
+
+ msg = pstrdup(PQerrorMessage(conn));
+ ereport(ERROR,
+ (errmsg("could not execute EXPLAIN for cost estimation"),
+ errdetail("%s", msg),
+ errhint("%s", sql)));
+ }
+
+ /*
+ * Find estimation portion from top plan node. Here we search opening
+ * parentheses from the end of the line to avoid finding unexpected
+ * parentheses.
+ */
+ plan = PQgetvalue(res, 0, 0);
+ p = strrchr(plan, '(');
+ if (p == NULL)
+ elog(ERROR, "wrong EXPLAIN output: %s", plan);
+ n = sscanf(p,
+ "(cost=%lf..%lf rows=%lf width=%d)",
+ startup_cost, total_cost, rows, width);
+ if (n != 4)
+ elog(ERROR, "could not get estimation from EXPLAIN output");
+
+ PQclear(res);
+ res = NULL;
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+ }
+
+ /*
+ * Adjust costs estimated on remote end with some overheads such as connection
+ * and data transfer.
+ */
+ static void
+ adjust_costs(double rows, int width, Cost *startup_cost, Cost *total_cost)
+ {
+ /*
+ * TODO Selectivity of quals which are NOT pushed down should be also
+ * considered.
+ */
+
+ /* add cost to establish connection. */
+ *startup_cost += CONNECTION_COSTS;
+ *total_cost += CONNECTION_COSTS;
+
+ /* add cost to transfer result. */
+ *total_cost += TRANSFER_COSTS_PER_BYTE * width * rows;
+ *total_cost += cpu_tuple_cost * rows;
+ }
+
+ /*
+ * Execute remote query with current parameters.
+ */
+ static void
+ execute_query(ForeignScanState *node)
+ {
+ List *fdw_private;
+ PgsqlFdwExecutionState *festate;
+ ParamListInfo params = node->ss.ps.state->es_param_list_info;
+ int numParams = params ? params->numParams : 0;
+ Oid *types = NULL;
+ const char **values = NULL;
+ char *sql;
+ PGconn *conn;
+ PGresult *res = NULL;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+ types = festate->param_types;
+ values = festate->param_values;
+
+ /*
+ * Construct parameter array in text format. We don't release memory for
+ * the arrays explicitly, because the memory usage would not be very large,
+ * and anyway they will be released in context cleanup.
+ */
+ if (numParams > 0)
+ {
+ int i;
+
+ 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;
+
+ getTypeOutputInfo(types[i], &out_func_oid, &isvarlena);
+ fmgr_info(out_func_oid, &func);
+ values[i] = OutputFunctionCall(&func, params->params[i].value);
+ }
+ }
+ }
+
+ /* PGresult must be released before leaving this function. */
+ PG_TRY();
+ {
+ /*
+ * Execute remote query with parameters.
+ */
+ conn = festate->conn;
+ fdw_private = ((ForeignScan *) node->ss.ps.plan)->fdw_private;
+ sql = strVal(list_nth(fdw_private, FdwPrivateDeclareSql));
+ res = PQexecParams(conn, sql, numParams, types, values, NULL, NULL, 0);
+
+ /*
+ * If the query has failed, reporting details is enough here.
+ * Connection(s) which are used by this query (at least used by
+ * pgsql_fdw) will be cleaned up by the foreign connection manager.
+ */
+ if (PQresultStatus(res) != PGRES_COMMAND_OK)
+ {
+ ereport(ERROR,
+ (errmsg("could not declare cursor"),
+ errdetail("%s", PQerrorMessage(conn)),
+ errhint("%s", sql)));
+ }
+
+ /* Discard result of DECLARE statement. */
+ PQclear(res);
+ res = NULL;
+
+ /* Fetch first bunch of the result and store them into tuplestore. */
+ res = fetch_result(node);
+ store_result(node, res);
+ PQclear(res);
+ res = NULL;
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+ }
+
+ /*
+ * Fetch next partial result from remote server.
+ *
+ * Once this function has returned result records as PGresult, caller is
+ * responsible to release it, so caller should put codes which might throw
+ * exception in PG_TRY block. When an exception has been caught, release
+ * PGresult and re-throw the exception in PG_CATCH block.
+ */
+ static PGresult *
+ fetch_result(ForeignScanState *node)
+ {
+ PgsqlFdwExecutionState *festate;
+ List *fdw_private;
+ char *sql;
+ PGconn *conn;
+ PGresult *res;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+
+ /* Retrieve information for fetching result. */
+ fdw_private = festate->fdw_private;
+ sql = strVal(list_nth(fdw_private, FdwPrivateFetchSql));
+ conn = festate->conn;
+
+ /*
+ * Fetch result from remote server. In error case, we must release
+ * PGresult in this function to avoid memory leak because caller can't
+ * get the reference.
+ */
+ res = PQexec(conn, sql);
+ if (PQresultStatus(res) != PGRES_TUPLES_OK)
+ {
+ PQclear(res);
+ ereport(ERROR,
+ (errmsg("could not fetch rows from foreign server"),
+ errdetail("%s", PQerrorMessage(conn)),
+ errhint("%s", sql)));
+ }
+
+ return res;
+ }
+
+ /*
+ * Create tuples from PGresult and store them into tuplestore.
+ *
+ * Caller must use PG_TRY block to catch exception and release PGresult
+ * surely.
+ */
+ static void
+ store_result(ForeignScanState *node, PGresult *res)
+ {
+ int rows;
+ int row;
+ int i;
+ int nfields;
+ int attnum; /* number of non-dropped columns */
+ Form_pg_attribute *attrs;
+ TupleTableSlot *slot = node->ss.ss_ScanTupleSlot;
+ TupleDesc tupdesc = slot->tts_tupleDescriptor;
+ PgsqlFdwExecutionState *festate;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+ rows = PQntuples(res);
+ nfields = PQnfields(res);
+ attrs = tupdesc->attrs;
+
+ /* First, ensure that the tuplestore is empty. */
+ if (festate->tuples == NULL)
+ {
+ MemoryContext oldcontext = CurrentMemoryContext;
+
+ /*
+ * Create tuplestore to store result of the query in per-query context.
+ * Note that we use this memory context to avoid memory leak in error
+ * cases.
+ */
+ MemoryContextSwitchTo(festate->scan_cxt);
+ festate->tuples = tuplestore_begin_heap(false, false, work_mem);
+ MemoryContextSwitchTo(oldcontext);
+ }
+ else
+ {
+ /* We already have tuplestore, just need to clear contents of it. */
+ tuplestore_clear(festate->tuples);
+ }
+
+ /* 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)));
+
+ /* put a tuples into the slot */
+ for (row = 0; row < rows; row++)
+ {
+ int j;
+ HeapTuple tuple;
+
+ for (i = 0, j = 0; i < tupdesc->natts; i++)
+ {
+ /* skip dropped columns. */
+ if (attrs[i]->attisdropped)
+ {
+ festate->col_values[i] = NULL;
+ continue;
+ }
+
+ if (PQgetisnull(res, row, j))
+ festate->col_values[i] = NULL;
+ else
+ festate->col_values[i] = PQgetvalue(res, row, 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(festate->attinmeta, festate->col_values);
+ tuplestore_puttuple(festate->tuples, tuple);
+ }
+
+ tuplestore_donestoring(festate->tuples);
+ }
+
diff --git a/contrib/pgsql_fdw/pgsql_fdw.control b/contrib/pgsql_fdw/pgsql_fdw.control
index ...0a9c8f4 .
*** a/contrib/pgsql_fdw/pgsql_fdw.control
--- b/contrib/pgsql_fdw/pgsql_fdw.control
***************
*** 0 ****
--- 1,5 ----
+ # pgsql_fdw extension
+ comment = 'foreign-data wrapper for remote PostgreSQL servers'
+ default_version = '1.0'
+ module_pathname = '$libdir/pgsql_fdw'
+ relocatable = true
diff --git a/contrib/pgsql_fdw/pgsql_fdw.h b/contrib/pgsql_fdw/pgsql_fdw.h
index ...234904e .
*** a/contrib/pgsql_fdw/pgsql_fdw.h
--- b/contrib/pgsql_fdw/pgsql_fdw.h
***************
*** 0 ****
--- 1,33 ----
+ /*-------------------------------------------------------------------------
+ *
+ * pgsql_fdw.h
+ * foreign-data wrapper for remote PostgreSQL servers.
+ *
+ * Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/pgsql_fdw.h
+ *
+ *-------------------------------------------------------------------------
+ */
+
+ #ifndef PGSQL_FDW_H
+ #define PGSQL_FDW_H
+
+ #include "postgres.h"
+ #include "foreign/foreign.h"
+ #include "nodes/relation.h"
+
+ /* in option.c */
+ int ExtractConnectionOptions(List *defelems,
+ const char **keywords,
+ const char **values);
+
+ /* in deparse.c */
+ void deparseSimpleSql(StringInfo buf,
+ Oid relid,
+ PlannerInfo *root,
+ RelOptInfo *baserel,
+ ForeignTable *table);
+
+ #endif /* PGSQL_FDW_H */
diff --git a/contrib/pgsql_fdw/sql/pgsql_fdw.sql b/contrib/pgsql_fdw/sql/pgsql_fdw.sql
index ...b3fac96 .
*** a/contrib/pgsql_fdw/sql/pgsql_fdw.sql
--- b/contrib/pgsql_fdw/sql/pgsql_fdw.sql
***************
*** 0 ****
--- 1,248 ----
+ -- ===================================================================
+ -- create FDW objects
+ -- ===================================================================
+
+ -- Clean up in case a prior regression run failed
+
+ -- Suppress NOTICE messages when roles don't exist
+ SET client_min_messages TO 'error';
+
+ DROP ROLE IF EXISTS pgsql_fdw_user;
+
+ RESET client_min_messages;
+
+ CREATE ROLE pgsql_fdw_user LOGIN SUPERUSER;
+ SET SESSION AUTHORIZATION 'pgsql_fdw_user';
+
+ CREATE EXTENSION pgsql_fdw;
+
+ CREATE SERVER loopback1 FOREIGN DATA WRAPPER pgsql_fdw;
+ CREATE SERVER loopback2 FOREIGN DATA WRAPPER pgsql_fdw
+ OPTIONS (dbname 'contrib_regression');
+
+ CREATE USER MAPPING FOR public SERVER loopback1
+ OPTIONS (user 'value', password 'value');
+ CREATE USER MAPPING FOR pgsql_fdw_user SERVER loopback2;
+
+ CREATE FOREIGN TABLE ft1 (
+ c0 int,
+ c1 int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10)
+ ) SERVER loopback2;
+ ALTER FOREIGN TABLE ft1 DROP COLUMN c0;
+
+ CREATE FOREIGN TABLE ft2 (
+ c0 int,
+ c1 int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10)
+ ) SERVER loopback2;
+ ALTER FOREIGN TABLE ft2 DROP COLUMN c0;
+
+ -- ===================================================================
+ -- create objects used through FDW
+ -- ===================================================================
+ CREATE SCHEMA "S 1";
+ CREATE TABLE "S 1"."T 1" (
+ "C 1" int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10),
+ CONSTRAINT t1_pkey PRIMARY KEY ("C 1")
+ );
+ CREATE TABLE "S 1"."T 2" (
+ c1 int NOT NULL,
+ c2 text,
+ CONSTRAINT t2_pkey PRIMARY KEY (c1)
+ );
+
+ BEGIN;
+ TRUNCATE "S 1"."T 1";
+ INSERT INTO "S 1"."T 1"
+ SELECT id,
+ id % 10,
+ to_char(id, 'FM00000'),
+ '1970-01-01'::timestamptz + ((id % 100) || ' days')::interval,
+ '1970-01-01'::timestamp + ((id % 100) || ' days')::interval,
+ id % 10,
+ id % 10
+ FROM generate_series(1, 1000) id;
+ TRUNCATE "S 1"."T 2";
+ INSERT INTO "S 1"."T 2"
+ SELECT id,
+ 'AAA' || to_char(id, 'FM000')
+ FROM generate_series(1, 100) id;
+ COMMIT;
+
+ -- ===================================================================
+ -- tests for pgsql_fdw_validator
+ -- ===================================================================
+ ALTER FOREIGN DATA WRAPPER pgsql_fdw OPTIONS (host 'value'); -- ERROR
+ -- requiressl, krbsrvname and gsslib are omitted because they depend on
+ -- configure option
+ ALTER SERVER loopback1 OPTIONS (
+ authtype 'value',
+ service 'value',
+ connect_timeout 'value',
+ dbname 'value',
+ host 'value',
+ hostaddr 'value',
+ port 'value',
+ --client_encoding 'value',
+ tty 'value',
+ options 'value',
+ application_name 'value',
+ --fallback_application_name 'value',
+ keepalives 'value',
+ keepalives_idle 'value',
+ keepalives_interval 'value',
+ -- requiressl 'value',
+ sslcompression 'value',
+ sslmode 'value',
+ sslcert 'value',
+ sslkey 'value',
+ sslrootcert 'value',
+ sslcrl 'value'
+ --requirepeer 'value',
+ -- krbsrvname 'value',
+ -- gsslib 'value',
+ --replication 'value'
+ );
+ ALTER SERVER loopback1 OPTIONS (user 'value'); -- ERROR
+ ALTER SERVER loopback2 OPTIONS (ADD fetch_count '2');
+ ALTER USER MAPPING FOR public SERVER loopback1
+ OPTIONS (DROP user, DROP password);
+ ALTER USER MAPPING FOR public SERVER loopback1
+ OPTIONS (host 'value'); -- ERROR
+ ALTER FOREIGN TABLE ft1 OPTIONS (nspname 'S 1', relname 'T 1');
+ ALTER FOREIGN TABLE ft2 OPTIONS (nspname 'S 1', relname 'T 1', fetch_count '100');
+ ALTER FOREIGN TABLE ft1 OPTIONS (invalid 'value'); -- ERROR
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count 'a'); -- ERROR
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count '0'); -- ERROR
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count '-1'); -- ERROR
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (invalid 'value'); -- ERROR
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (colname 'C 1');
+ ALTER FOREIGN TABLE ft2 ALTER COLUMN c1 OPTIONS (colname 'C 1');
+ \dew+
+ \des+
+ \deu+
+ \det+
+
+ -- ===================================================================
+ -- simple queries
+ -- ===================================================================
+ -- single table, with/without alias
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
+ SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+ SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+ -- with WHERE clause
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
+ SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
+ -- 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
+ SELECT * FROM ft1 t1 WHERE t1.c3 = (SELECT MAX(c3) FROM ft2 t2) ORDER BY c1;
+ -- used in CTE
+ WITH t1 AS (SELECT * FROM ft1 WHERE c1 <= 10) SELECT t2.c1, t2.c2, t2.c3, t2.c4 FROM t1, ft2 t2 WHERE t1.c1 = t2.c1 ORDER BY t1.c1;
+ -- fixed values
+ SELECT 'fixed', NULL FROM ft1 t1 WHERE c1 = 1;
+ -- user-defined operator/function
+ CREATE FUNCTION pgsql_fdw_abs(int) RETURNS int AS $$
+ BEGIN
+ RETURN abs($1);
+ END
+ $$ LANGUAGE plpgsql IMMUTABLE;
+ CREATE OPERATOR === (
+ LEFTARG = int,
+ RIGHTARG = int,
+ PROCEDURE = int4eq,
+ COMMUTATOR = ===,
+ NEGATOR = !==
+ );
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = pgsql_fdw_abs(t1.c2);
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 === t1.c2;
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = abs(t1.c2);
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = t1.c2;
+
+ -- ===================================================================
+ -- parameterized queries
+ -- ===================================================================
+ -- simple join
+ PREPARE st1(int, int) AS SELECT t1.c3, t2.c3 FROM ft1 t1, ft2 t2 WHERE t1.c1 = $1 AND t2.c1 = $2;
+ EXPLAIN (COSTS false) EXECUTE st1(1, 2);
+ EXECUTE st1(1, 1);
+ EXECUTE st1(101, 101);
+ -- subquery using stable function (can't be pushed down)
+ PREPARE st2(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c4) = 6) ORDER BY c1;
+ EXPLAIN (COSTS false) EXECUTE st2(10, 20);
+ EXECUTE st2(10, 20);
+ EXECUTE st1(101, 101);
+ -- subquery using immutable function (can be pushed down)
+ PREPARE st3(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c5) = 6) ORDER BY c1;
+ EXPLAIN (COSTS false) EXECUTE st3(10, 20);
+ EXECUTE st3(10, 20);
+ EXECUTE st3(20, 30);
+ -- custom plan should be chosen
+ PREPARE st4(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 = $1;
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ -- cleanup
+ DEALLOCATE st1;
+ DEALLOCATE st2;
+ DEALLOCATE st3;
+ DEALLOCATE st4;
+
+ -- ===================================================================
+ -- connection management
+ -- ===================================================================
+ SELECT srvname, usename FROM pgsql_fdw_connections;
+ SELECT pgsql_fdw_disconnect(srvid, usesysid) FROM pgsql_fdw_get_connections();
+ SELECT srvname, usename FROM pgsql_fdw_connections;
+
+ -- ===================================================================
+ -- subtransaction
+ -- ===================================================================
+ BEGIN;
+ DECLARE c CURSOR FOR SELECT * FROM ft1 ORDER BY c1;
+ FETCH c;
+ SAVEPOINT s;
+ ERROR OUT; -- ERROR
+ ROLLBACK TO s;
+ SELECT srvname FROM pgsql_fdw_connections;
+ FETCH c;
+ COMMIT;
+ SELECT srvname FROM pgsql_fdw_connections;
+ ERROR OUT; -- ERROR
+ SELECT srvname FROM pgsql_fdw_connections;
+
+ -- ===================================================================
+ -- cleanup
+ -- ===================================================================
+ DROP OPERATOR === (int, int) CASCADE;
+ DROP OPERATOR !== (int, int) CASCADE;
+ DROP FUNCTION pgsql_fdw_abs(int);
+ DROP SCHEMA "S 1" CASCADE;
+ DROP EXTENSION pgsql_fdw CASCADE;
+ \c
+ DROP ROLE pgsql_fdw_user;
diff --git a/doc/src/sgml/contrib.sgml b/doc/src/sgml/contrib.sgml
index 97031dd..c4dd9c3 100644
*** a/doc/src/sgml/contrib.sgml
--- b/doc/src/sgml/contrib.sgml
*************** CREATE EXTENSION <replaceable>module_nam
*** 117,122 ****
--- 117,123 ----
&pgcrypto;
&pgfreespacemap;
&pgrowlocks;
+ &pgsql-fdw;
&pgstandby;
&pgstatstatements;
&pgstattuple;
diff --git a/doc/src/sgml/filelist.sgml b/doc/src/sgml/filelist.sgml
index 428a167..f3ff180 100644
*** a/doc/src/sgml/filelist.sgml
--- b/doc/src/sgml/filelist.sgml
***************
*** 125,130 ****
--- 125,131 ----
<!ENTITY pgcrypto SYSTEM "pgcrypto.sgml">
<!ENTITY pgfreespacemap SYSTEM "pgfreespacemap.sgml">
<!ENTITY pgrowlocks SYSTEM "pgrowlocks.sgml">
+ <!ENTITY pgsql-fdw SYSTEM "pgsql-fdw.sgml">
<!ENTITY pgstandby SYSTEM "pgstandby.sgml">
<!ENTITY pgstatstatements SYSTEM "pgstatstatements.sgml">
<!ENTITY pgstattuple SYSTEM "pgstattuple.sgml">
diff --git a/doc/src/sgml/pgsql-fdw.sgml b/doc/src/sgml/pgsql-fdw.sgml
index ...eb69f01 .
*** a/doc/src/sgml/pgsql-fdw.sgml
--- b/doc/src/sgml/pgsql-fdw.sgml
***************
*** 0 ****
--- 1,265 ----
+ <!-- doc/src/sgml/pgsql-fdw.sgml -->
+
+ <sect1 id="pgsql-fdw" xreflabel="pgsql_fdw">
+ <title>pgsql_fdw</title>
+
+ <indexterm zone="pgsql-fdw">
+ <primary>pgsql_fdw</primary>
+ </indexterm>
+
+ <para>
+ The <filename>pgsql_fdw</filename> module provides a foreign-data wrapper for
+ external <productname>PostgreSQL</productname> servers.
+ With this module, users can access data stored in external
+ <productname>PostgreSQL</productname> via plain SQL statements.
+ </para>
+
+ <para>
+ Note that default wrapper <literal>pgsql_fdw</literal> is created
+ automatically during <command>CREATE EXTENSION</command> command for
+ <application>pgsql_fdw</application>.
+ </para>
+
+ <sect2>
+ <title>FDW Options of pgsql_fdw</title>
+
+ <sect3>
+ <title>Connection Options</title>
+ <para>
+ A foreign server and user mapping created using this wrapper can have
+ <application>libpq</> connection options, expect below:
+
+ <itemizedlist>
+ <listitem><para>client_encoding</para></listitem>
+ <listitem><para>fallback_application_name</para></listitem>
+ <listitem><para>replication</para></listitem>
+ </itemizedlist>
+
+ For details of <application>libpq</> connection options, see
+ <xref linkend="libpq-connect">.
+ </para>
+
+ <para>
+ <literal>user</literal> and <literal>password</literal> can be
+ specified on user mappings, and others can be specified on foreign servers.
+ </para>
+ </sect3>
+
+ <sect3>
+ <title>Object Name Options</title>
+ <para>
+ Foreign tables which were created using this wrapper, or its columns can
+ have object name options. These options can be used to specify the names
+ used in SQL statement sent to remote <productname>PostgreSQL</productname>
+ server. These options are useful when a remote object has different name
+ from corresponding local one.
+ </para>
+
+ <variablelist>
+
+ <varlistentry>
+ <term><literal>nspname</literal></term>
+ <listitem>
+ <para>
+ This option, which can be specified on a foreign table, is used as a
+ namespace (schema) reference in the SQL statement. If this options is
+ omitted, <literal>pg_class.nspname</literal> of the foreign table is
+ used.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>relname</literal></term>
+ <listitem>
+ <para>
+ This option, which can be specified on a foreign table, is used as a
+ relation (table) reference in the SQL statement. If this options is
+ omitted, <literal>pg_class.relname</literal> of the foreign table is
+ used.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>colname</literal></term>
+ <listitem>
+ <para>
+ This option, which can be specified on a column of a foreign table, is
+ used as a column (attribute) reference in the SQL statement. If this
+ option is omitted, <literal>pg_attribute.attname</literal> of the column
+ of the foreign table is used.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ </variablelist>
+
+ </sect3>
+
+ <sect3>
+ <title>Cursor Options</title>
+ <para>
+ The <application>pgsql_fdw</application> always uses cursor to retrieve the
+ result from external server. Users can control the behavior of cursor by
+ setting cursor options to foreign table or foreign server. If an option
+ is set to both objects, finer-grained setting is used. In other words,
+ foreign table's setting overrides foreign server's setting.
+ </para>
+
+ <variablelist>
+
+ <varlistentry>
+ <term><literal>fetch_count</literal></term>
+ <listitem>
+ <para>
+ This option specifies the number of rows to be fetched at a time.
+ This option accepts only integer value larger than zero. The default
+ setting is 10000.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ </variablelist>
+
+ </sect3>
+
+ </sect2>
+
+ <sect2>
+ <title>Connection Management</title>
+
+ <para>
+ The <application>pgsql_fdw</application> establishes a connection to a
+ foreign server in the beginning of the first query which uses a foreign
+ table associated to the foreign server, and reuses the connection following
+ queries and even in following foreign scans in same query.
+
+ You can see the list of active connections via
+ <structname>pgsql_fdw_connections</structname> view. It shows pair of oid
+ and name of server and local role for each active connections established by
+ <application>pgsql_fdw</application>. For security reason, only superuser
+ can see other role's connections.
+ </para>
+
+ <para>
+ Established connections are kept alive until local role changes or the
+ current transaction aborts or user requests so.
+ </para>
+
+ <para>
+ If role has been changed, active connections established as old local role
+ is kept alive but never be reused until local role has restored to original
+ role. This kind of situation happens with <command>SET ROLE</command> and
+ <command>SET SESSION AUTHORIZATION</command>.
+ </para>
+
+ <para>
+ If current transaction aborts by error or user request, all active
+ connections are disconnected automatically. This behavior avoids possible
+ connection leaks on error.
+ </para>
+
+ <para>
+ You can discard persistent connection at arbitrary timing with
+ <function>pgsql_fdw_disconnect()</function>. It takes server oid and
+ user oid as arguments. This function can handle only connections
+ established in current session; connections established by other backends
+ are not reachable.
+ </para>
+
+ <para>
+ You can discard all active and visible connections in current session with
+ using <structname>pgsql_fdw_connections</structname> and
+ <function>pgsql_fdw_disconnect()</function> together:
+ <synopsis>
+ postgres=# SELECT pgsql_fdw_disconnect(srvid, usesysid) FROM pgsql_fdw_connections;
+ pgsql_fdw_disconnect
+ ----------------------
+ OK
+ OK
+ (2 rows)
+ </synopsis>
+ </para>
+ </sect2>
+
+ <sect2>
+ <title>Transaction Management</title>
+ <para>
+ The <application>pgsql_fdw</application> begins remote transaction at the
+ beginning of a local query, and terminates it with <command>ABORT</command>
+ at the end of the local query. This means that all foreign scans on a
+ foreign server in a local query are executed in one transaction.
+ If isolation level of local transaction is <literal>SERIALIZABLE</literal>,
+ <literal>SERIALIZABLE</literal> is used for remote transaction. Otherwise,
+ if isolation level of local transaction is one of
+ <literal>READ UNCOMMITTED</literal>, <literal>READ COMMITTED</literal> or
+ <literal>REPEATABLE READ</literal>, then <literal>REPEATABLE READ</literal>
+ is used for remote transaction.
+ <literal>READ UNCOMMITTED</literal> and <literal>READ COMMITTED</literal>
+ are never used for remote transaction, because even
+ <literal>READ COMMITTED</literal> transaction might produce inconsistent
+ results, if remote data have been updated between two remote queries.
+ </para>
+ <para>
+ Note that even if the isolation level of local transaction was
+ <literal>SERIALIZABLE</literal> or <literal>REPEATABLE READ</literal>,
+ series of one query might produce different result, because foreign scans
+ in different local queries are executed in different remote transactions.
+ For instance, when client started a local transaction
+ explicitly with isolation level <literal>SERIALIZABLE</literal>, and
+ executed same local query which contains a foreign table which references
+ foreign data which is updated frequently, latter result would be different
+ from former result.
+ </para>
+ <para>
+ This restriction might be relaxed in future release.
+ </para>
+ </sect2>
+
+ <sect2>
+ <title>Estimation of Costs and Rows</title>
+ <para>
+ The <application>pgsql_fdw</application> estimates the costs of a foreign
+ scan by adding up some basic costs: connection costs, remote query costs
+ and data transfer costs.
+ To get remote query costs, <application>pgsql_fdw</application> executes
+ <command>EXPLAIN</command> command on remote server for each foreign scan.
+ </para>
+ <para>
+ On the other hand, estimated rows which was returned by
+ <command>EXPLAIN</command> is used for local estimation as-is.
+ </para>
+ </sect2>
+
+ <sect2>
+ <title>EXPLAIN Output</title>
+ <para>
+ For a foreign table using <literal>pgsql_fdw</>, <command>EXPLAIN</> shows
+ a remote SQL statement which is sent to remote
+ <productname>PostgreSQL</productname> server for a ForeignScan plan node.
+ For example:
+ </para>
+ <synopsis>
+ postgres=# EXPLAIN SELECT aid FROM pgbench_accounts WHERE abalance < 0;
+ QUERY PLAN
+ --------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on pgbench_accounts (cost=100.00..8105.13 rows=302613 width=8)
+ Filter: (abalance < 0)
+ Remote SQL: SELECT aid, NULL, abalance, NULL FROM public.pgbench_accounts
+ (3 rows)
+ </synopsis>
+ <para>
+ When you specify <literal>VERBOSE</> option, you can see actual DECLARE
+ statement with cursor name which is used for the scan.
+ </para>
+ </sect2>
+
+ <sect2>
+ <title>Author</title>
+ <para>
+ Shigeru Hanada <email>shigeru.hanada@gmail.com</email>
+ </para>
+ </sect2>
+
+ </sect1>
Shigeru HANADA wrote:
Attached are latest version of pgsql_fdw patches. Note that
pgsql_fdw_analyze.patch is only for test the effect of local
statistics.
Please apply patches in the order below:
(1) pgsql_fdw_v18.patch
(2) pgsql_fdw_pushdown_v11.patch
(3) pgsql_fdw_analyze.patch (if you want to try local stats)
Since Kohei KaiGai doesn't post a review, I'll have a go.
The patch applies and compiles fine without warnings and passes
regression tests.
I found bugs in the analyze functions:
In pgsql_fdw_analyze:
nspname and relname are not initialized to NULL.
This causes failure if the corresponding option is not set
on the foreign table.
In store_remote_stats:
atttypmod is initialized to 0 and never changed.
This causes the following error for columns of type "interval":
ERROR: unrecognized interval typmod: 0
During a foreign scan, type input functions are used to convert
the text representation of values. If a foreign table is misconfigured,
you can get error messages from these functions, like:
ERROR: invalid input syntax for type double precision: "etwas"
or
ERROR: value too long for type character varying(3)
It might me nice for finding problems if the message were
something like:
ERROR: cannot convert data in foreign scan of "tablename", column "col"
in row 42
DETAIL: ERROR: value too long for type character varying(3)
As stated previously, I don't think that using local stats on
foreign tables is a win. The other patches work fine for me, and
I'd be happy if that could go into 9.2.
Once the two bugs above are fixed, should I mark it "ready for
committer"?
Yours,
Laurenz Albe
(2012/04/03 22:31), Albe Laurenz wrote:
Shigeru HANADA wrote:
Attached are latest version of pgsql_fdw patches. Note that
pgsql_fdw_analyze.patch is only for test the effect of localstatistics.
Please apply patches in the order below:
(1) pgsql_fdw_v18.patch
(2) pgsql_fdw_pushdown_v11.patch
(3) pgsql_fdw_analyze.patch (if you want to try local stats)Since Kohei KaiGai doesn't post a review, I'll have a go.
The patch applies and compiles fine without warnings and passes
regression tests.
Thanks for the review.
I found bugs in the analyze functions:
In pgsql_fdw_analyze:
nspname and relname are not initialized to NULL.
This causes failure if the corresponding option is not set
on the foreign table.In store_remote_stats:
atttypmod is initialized to 0 and never changed.
This causes the following error for columns of type "interval":
ERROR: unrecognized interval typmod: 0
Oops, sorry for silly bugs. However, we won't need to care them anyway,
because coping remote stats to local side seems not practical way to
obtain local statistics. As you mentioned in another sub-thread,
copying remote stats contains some design-level problems such as
privileges and version difference.
(2012/03/28 21:07), Albe Laurenz wrote:
I found another limitation of this approach:
pgsql_fdw_analyze() has to run as a user who can update
pg_statistic, and this user needs a user mapping to a remote
user who can read pg_statistic.This is not necessary for normal operation and needs
to be configured specifically for getting remote statistics.
This is cumbersome, and people might be unhappy to have to
create user mappings for highly privileged remote users.
So, if we want to have statistics of remote data on local side, getting
actual data from remote and calculate statistics on local side with same
(or similar) routines as local tables seems better. I'd like to
separate this issue because it belongs to ANALYZE support patch which is
proposed by Fujita-san.
During a foreign scan, type input functions are used to convert
the text representation of values. If a foreign table is misconfigured,
you can get error messages from these functions, like:ERROR: invalid input syntax for type double precision: "etwas"
or
ERROR: value too long for type character varying(3)It might me nice for finding problems if the message were
something like:ERROR: cannot convert data in foreign scan of "tablename", column "col"
in row 42
DETAIL: ERROR: value too long for type character varying(3)
Agreed. How about showing context information with errcontext() in
addition to main error message? Of course, identifiers are quoted if
necessary. This way doesn't need additional PG_TRY block, so overhead
would be relatively cheap.
postgres=# SELECT * FROM ft1 WHERE c1 = 1; -- ERROR
ERROR: invalid input syntax for integer: "1970-01-02 17:00:00+09"
CONTEXT: column c4 of foreign table ft1
Showing index of the row seems overkill, because most cause of this kind
of error is wrong configuration, as you say, and users would be able to
address the issue without knowing which record caused the error.
As stated previously, I don't think that using local stats on
foreign tables is a win. The other patches work fine for me, and
I'd be happy if that could go into 9.2.
I have opposite opinion on this issue because we need to do some of
filtering on local side. We can leave cost/rows estimation to remote
side about WHERE expressions which are pushed down, but we need
selectivity of extra filtering done on local side. For such purpose,
having local stats of foreign data seems reasonable and useful.
Of course, it has downside that we need to execute explicit ANALYZE for
foreign tables which would cause full sequential scan on remote tables,
in addition to ANALYZE for remote tables done on remote side as usual
maintenance work.
Once the two bugs above are fixed, should I mark it "ready for
committer"?
Attached patch contains changes below:
pgsql_fdw_v19.patch
- show context of data conversion error
- move codes for fetch_count FDW option to option.c
(refactoring)
pgsql_fdw_pushdown_v12.patch
- make deparseExpr function static (refactoring)
I also attached pgsql_fdw_analyze for only testing the effect of local
statistics. It contains both backend's ANALYZE command support and
pgsql_fdw's ANALYZE support.
Regards,
--
Shigeru HANADA
Attachments:
pgsql_fdw_v19.patchtext/plain; charset=Shift_JIS; name=pgsql_fdw_v19.patchDownload
diff --git a/contrib/Makefile b/contrib/Makefile
index d230451..9bc35dd 100644
*** a/contrib/Makefile
--- b/contrib/Makefile
*************** SUBDIRS = \
*** 42,47 ****
--- 42,48 ----
pgbench \
pgcrypto \
pgrowlocks \
+ pgsql_fdw \
pgstattuple \
seg \
spi \
diff --git a/contrib/README b/contrib/README
index a1d42a1..d3fa211 100644
*** a/contrib/README
--- b/contrib/README
*************** pgrowlocks -
*** 158,163 ****
--- 158,167 ----
A function to return row locking information
by Tatsuo Ishii <ishii@sraoss.co.jp>
+ pgsql_fdw -
+ Foreign-data wrapper for external PostgreSQL servers.
+ by Shigeru Hanada <shigeru.hanada@gmail.com>
+
pgstattuple -
Functions to return statistics about "dead" tuples and free
space within a table
diff --git a/contrib/pgsql_fdw/.gitignore b/contrib/pgsql_fdw/.gitignore
index ...0854728 .
*** a/contrib/pgsql_fdw/.gitignore
--- b/contrib/pgsql_fdw/.gitignore
***************
*** 0 ****
--- 1,4 ----
+ # Generated subdirectories
+ /results/
+ *.o
+ *.so
diff --git a/contrib/pgsql_fdw/Makefile b/contrib/pgsql_fdw/Makefile
index ...6381365 .
*** a/contrib/pgsql_fdw/Makefile
--- b/contrib/pgsql_fdw/Makefile
***************
*** 0 ****
--- 1,22 ----
+ # contrib/pgsql_fdw/Makefile
+
+ MODULE_big = pgsql_fdw
+ OBJS = pgsql_fdw.o option.o deparse.o connection.o
+ PG_CPPFLAGS = -I$(libpq_srcdir)
+ SHLIB_LINK = $(libpq)
+
+ EXTENSION = pgsql_fdw
+ DATA = pgsql_fdw--1.0.sql
+
+ REGRESS = pgsql_fdw
+
+ ifdef USE_PGXS
+ PG_CONFIG = pg_config
+ PGXS := $(shell $(PG_CONFIG) --pgxs)
+ include $(PGXS)
+ else
+ subdir = contrib/pgsql_fdw
+ top_builddir = ../..
+ include $(top_builddir)/src/Makefile.global
+ include $(top_srcdir)/contrib/contrib-global.mk
+ endif
diff --git a/contrib/pgsql_fdw/connection.c b/contrib/pgsql_fdw/connection.c
index ...c971bd7 .
*** a/contrib/pgsql_fdw/connection.c
--- b/contrib/pgsql_fdw/connection.c
***************
*** 0 ****
--- 1,555 ----
+ /*-------------------------------------------------------------------------
+ *
+ * connection.c
+ * Connection management for pgsql_fdw
+ *
+ * Portions Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/connection.c
+ *
+ *-------------------------------------------------------------------------
+ */
+ #include "postgres.h"
+
+ #include "access/xact.h"
+ #include "catalog/pg_type.h"
+ #include "foreign/foreign.h"
+ #include "funcapi.h"
+ #include "libpq-fe.h"
+ #include "mb/pg_wchar.h"
+ #include "miscadmin.h"
+ #include "utils/array.h"
+ #include "utils/builtins.h"
+ #include "utils/hsearch.h"
+ #include "utils/memutils.h"
+ #include "utils/resowner.h"
+ #include "utils/tuplestore.h"
+
+ #include "pgsql_fdw.h"
+ #include "connection.h"
+
+ /* ============================================================================
+ * Connection management functions
+ * ==========================================================================*/
+
+ /*
+ * Connection cache entry managed with hash table.
+ */
+ typedef struct ConnCacheEntry
+ {
+ /* hash key must be first */
+ Oid serverid; /* oid of foreign server */
+ Oid userid; /* oid of local user */
+
+ bool use_tx; /* true when using remote transaction */
+ 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);
+ static void begin_remote_tx(PGconn *conn);
+ static void abort_remote_tx(PGconn *conn);
+
+ /*
+ * 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.
+ *
+ * When use_tx is true, remote transaction is started if caller is the only
+ * user of the connection. Isolation level of the remote transaction is same
+ * as local transaction, and remote transaction will be aborted when last
+ * user release.
+ *
+ * TODO: Note that caching connections requires a mechanism to detect change of
+ * FDW object to invalidate already established connections.
+ */
+ PGconn *
+ GetConnection(ForeignServer *server, UserMapping *user, bool use_tx)
+ {
+ bool found;
+ ConnCacheEntry *entry;
+ ConnCacheEntry key;
+
+ /* initialize connection cache if it isn't */
+ if (FSConnectionHash == NULL)
+ {
+ HASHCTL ctl;
+
+ /* hash key is a pair of oids: serverid and userid */
+ MemSet(&ctl, 0, sizeof(ctl));
+ ctl.keysize = sizeof(Oid) + sizeof(Oid);
+ ctl.entrysize = sizeof(ConnCacheEntry);
+ ctl.hash = tag_hash;
+ ctl.match = memcmp;
+ ctl.keycopy = memcpy;
+ /* allocate FSConnectionHash in the cache context */
+ ctl.hcxt = CacheMemoryContext;
+ FSConnectionHash = hash_create("Foreign Connections", 32,
+ &ctl,
+ HASH_ELEM | HASH_CONTEXT |
+ HASH_FUNCTION | HASH_COMPARE |
+ HASH_KEYCOPY);
+ }
+
+ /* Create key value for the entry. */
+ MemSet(&key, 0, sizeof(key));
+ key.serverid = server->serverid;
+ key.userid = GetOuterUserId();
+
+ /*
+ * Find cached entry for requested connection. If we couldn't find,
+ * callback function of ResourceOwner should be registered to clean the
+ * connection up on error including user interrupt.
+ */
+ entry = hash_search(FSConnectionHash, &key, HASH_ENTER, &found);
+ if (!found)
+ {
+ entry->refs = 0;
+ entry->use_tx = false;
+ entry->conn = NULL;
+ RegisterResourceReleaseCallback(cleanup_connection, entry);
+ }
+
+ /*
+ * We don't check the health of cached connection here, because it would
+ * require some overhead. Broken connection and its cache entry will be
+ * cleaned up when the connection is actually used.
+ */
+
+ /*
+ * If cache entry doesn't have connection, we have to establish new
+ * connection.
+ */
+ if (entry->conn == NULL)
+ {
+ /*
+ * Use PG_TRY block to ensure closing connection on error.
+ */
+ PG_TRY();
+ {
+ /*
+ * Connect to the foreign PostgreSQL server, and store it in cache
+ * entry to keep new connection.
+ * Note: key items of entry has already been initialized in
+ * hash_search(HASH_ENTER).
+ */
+ entry->conn = connect_pg_server(server, user);
+ }
+ PG_CATCH();
+ {
+ /* Clear connection cache entry on error case. */
+ PQfinish(entry->conn);
+ entry->refs = 0;
+ entry->use_tx = false;
+ entry->conn = NULL;
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+ }
+
+ /* Increase connection reference counter. */
+ entry->refs++;
+
+ /*
+ * If requester is the only referrer of this connection, start transaction
+ * with the same isolation level as the local transaction we are in.
+ * We need to remember whether this connection uses remote transaction to
+ * abort it when this connection is released completely.
+ */
+ if (use_tx && entry->refs == 1)
+ begin_remote_tx(entry->conn);
+ entry->use_tx = use_tx;
+
+
+ return entry->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 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. Those two object hold only libpq options.
+ * Extra 3 items are for:
+ * *) fallback_application_name
+ * *) client_encoding
+ * *) NULL termination (end marker)
+ *
+ * Note: We don't omit any parameters even target database might be older
+ * than local, because unexpected parameters are just ignored.
+ */
+ n = list_length(server->options) + list_length(user->options) + 3;
+ 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 += ExtractConnectionOptions(server->options,
+ all_keywords + n, all_values + n);
+ n += ExtractConnectionOptions(user->options,
+ all_keywords + n, all_values + n);
+ all_keywords[n] = all_values[n] = NULL;
+
+ for (i = 0, j = 0; all_keywords[i]; i++)
+ {
+ keywords[j] = all_keywords[i];
+ values[j] = all_values[i];
+ j++;
+ }
+
+ /* Use "pgsql_fdw" as fallback_application_name. */
+ keywords[j] = "fallback_application_name";
+ values[j++] = "pgsql_fdw";
+
+ /* Set client_encoding so that libpq can convert encoding properly. */
+ keywords[j] = "client_encoding";
+ values[j++] = GetDatabaseEncodingName();
+
+ keywords[j] = values[j] = NULL;
+ pfree(all_keywords);
+ pfree(all_values);
+
+ /* 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.")));
+ }
+
+ return conn;
+ }
+
+ /*
+ * Start transaction to use cursor to retrieve data separately.
+ */
+ static void
+ begin_remote_tx(PGconn *conn)
+ {
+ const char *sql = NULL; /* keep compiler quiet. */
+ PGresult *res;
+
+ switch (XactIsoLevel)
+ {
+ case XACT_READ_UNCOMMITTED:
+ case XACT_READ_COMMITTED:
+ case XACT_REPEATABLE_READ:
+ sql = "START TRANSACTION ISOLATION LEVEL REPEATABLE READ";
+ break;
+ case XACT_SERIALIZABLE:
+ sql = "START TRANSACTION ISOLATION LEVEL SERIALIZABLE";
+ break;
+ default:
+ elog(ERROR, "unexpected isolation level: %d", XactIsoLevel);
+ break;
+ }
+
+ elog(DEBUG3, "starting remote transaction with \"%s\"", sql);
+
+ res = PQexec(conn, sql);
+ if (PQresultStatus(res) != PGRES_COMMAND_OK)
+ {
+ PQclear(res);
+ elog(ERROR, "could not start transaction: %s", PQerrorMessage(conn));
+ }
+ PQclear(res);
+ }
+
+ static void
+ abort_remote_tx(PGconn *conn)
+ {
+ PGresult *res;
+
+ elog(DEBUG3, "aborting remote transaction");
+
+ res = PQexec(conn, "ABORT TRANSACTION");
+ if (PQresultStatus(res) != PGRES_COMMAND_OK)
+ {
+ PQclear(res);
+ elog(ERROR, "could not abort transaction: %s", PQerrorMessage(conn));
+ }
+ PQclear(res);
+ }
+
+ /*
+ * 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 sequentially 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)
+ {
+ hash_seq_term(&scan);
+ break;
+ }
+ }
+
+ /* If the released connection was an orphan, just close it. */
+ if (entry == NULL)
+ {
+ PQfinish(conn);
+ return;
+ }
+
+ /*
+ * If this connection uses remote transaction and caller is the last user,
+ * abort remote transaction and forget about it.
+ */
+ if (entry->use_tx && entry->refs == 1)
+ {
+ abort_remote_tx(conn);
+ entry->use_tx = false;
+ }
+
+ /*
+ * Decrease reference counter of this connection. Even if the caller was
+ * the last referrer, we don't unregister it from cache.
+ */
+ entry->refs--;
+ }
+
+ /*
+ * 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 need to clean up at end of subtransactions, because they might
+ * be recovered to consistent state with savepoints.
+ */
+ if (!isTopLevel)
+ return;
+
+ /*
+ * Here, it must be after abort of top level transaction. Disconnect and
+ * forget every referrer.
+ */
+ if (entry->conn != NULL)
+ {
+ PQfinish(entry->conn);
+ entry->refs = 0;
+ entry->conn = NULL;
+ }
+ }
+
+ /*
+ * Get list of connections currently active.
+ */
+ Datum pgsql_fdw_get_connections(PG_FUNCTION_ARGS);
+ PG_FUNCTION_INFO_V1(pgsql_fdw_get_connections);
+ Datum
+ pgsql_fdw_get_connections(PG_FUNCTION_ARGS)
+ {
+ ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+ HASH_SEQ_STATUS scan;
+ ConnCacheEntry *entry;
+ MemoryContext oldcontext = CurrentMemoryContext;
+ Tuplestorestate *tuplestore;
+ TupleDesc tupdesc;
+
+ /* We return list of connection with storing them in a Tuplestore. */
+ rsinfo->returnMode = SFRM_Materialize;
+ rsinfo->setResult = NULL;
+ rsinfo->setDesc = NULL;
+
+ /* Create tuplestore and copy of TupleDesc in per-query context. */
+ MemoryContextSwitchTo(rsinfo->econtext->ecxt_per_query_memory);
+
+ tupdesc = CreateTemplateTupleDesc(2, false);
+ TupleDescInitEntry(tupdesc, 1, "srvid", OIDOID, -1, 0);
+ TupleDescInitEntry(tupdesc, 2, "usesysid", OIDOID, -1, 0);
+ rsinfo->setDesc = tupdesc;
+
+ tuplestore = tuplestore_begin_heap(false, false, work_mem);
+ rsinfo->setResult = tuplestore;
+
+ MemoryContextSwitchTo(oldcontext);
+
+ /*
+ * We need to scan sequentially since we use the address to find
+ * appropriate PGconn from the hash table.
+ */
+ if (FSConnectionHash != NULL)
+ {
+ hash_seq_init(&scan, FSConnectionHash);
+ while ((entry = (ConnCacheEntry *) hash_seq_search(&scan)))
+ {
+ Datum values[2];
+ bool nulls[2];
+ HeapTuple tuple;
+
+ /* Ignore inactive connections */
+ if (PQstatus(entry->conn) != CONNECTION_OK)
+ continue;
+
+ /*
+ * Ignore other users' connections if current user isn't a
+ * superuser.
+ */
+ if (!superuser() && entry->userid != GetUserId())
+ continue;
+
+ values[0] = ObjectIdGetDatum(entry->serverid);
+ values[1] = ObjectIdGetDatum(entry->userid);
+ nulls[0] = false;
+ nulls[1] = false;
+
+ tuple = heap_formtuple(tupdesc, values, nulls);
+ tuplestore_puttuple(tuplestore, tuple);
+ }
+ }
+ tuplestore_donestoring(tuplestore);
+
+ PG_RETURN_VOID();
+ }
+
+ /*
+ * Discard persistent connection designated by given connection name.
+ */
+ Datum pgsql_fdw_disconnect(PG_FUNCTION_ARGS);
+ PG_FUNCTION_INFO_V1(pgsql_fdw_disconnect);
+ Datum
+ pgsql_fdw_disconnect(PG_FUNCTION_ARGS)
+ {
+ Oid serverid = PG_GETARG_OID(0);
+ Oid userid = PG_GETARG_OID(1);
+ ConnCacheEntry key;
+ ConnCacheEntry *entry = NULL;
+ bool found;
+
+ /* Non-superuser can't discard other users' connection. */
+ if (!superuser() && userid != GetOuterUserId())
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_RESOURCES),
+ errmsg("only superuser can discard other user's connection")));
+
+ /*
+ * If no connection has been established, or no such connections, just
+ * return "NG" to indicate nothing has done.
+ */
+ if (FSConnectionHash == NULL)
+ PG_RETURN_TEXT_P(cstring_to_text("NG"));
+
+ key.serverid = serverid;
+ key.userid = userid;
+ entry = hash_search(FSConnectionHash, &key, HASH_FIND, &found);
+ if (!found)
+ PG_RETURN_TEXT_P(cstring_to_text("NG"));
+
+ /* Discard cached connection, and clear reference counter. */
+ PQfinish(entry->conn);
+ entry->refs = 0;
+ entry->conn = NULL;
+
+ PG_RETURN_TEXT_P(cstring_to_text("OK"));
+ }
diff --git a/contrib/pgsql_fdw/connection.h b/contrib/pgsql_fdw/connection.h
index ...4427f56 .
*** a/contrib/pgsql_fdw/connection.h
--- b/contrib/pgsql_fdw/connection.h
***************
*** 0 ****
--- 1,25 ----
+ /*-------------------------------------------------------------------------
+ *
+ * connection.h
+ * Connection management for pgsql_fdw
+ *
+ * Portions Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_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, bool use_tx);
+ void ReleaseConnection(PGconn *conn);
+
+ #endif /* CONNECTION_H */
diff --git a/contrib/pgsql_fdw/deparse.c b/contrib/pgsql_fdw/deparse.c
index ...8e79232 .
*** a/contrib/pgsql_fdw/deparse.c
--- b/contrib/pgsql_fdw/deparse.c
***************
*** 0 ****
--- 1,288 ----
+ /*-------------------------------------------------------------------------
+ *
+ * deparse.c
+ * query deparser for PostgreSQL
+ *
+ * Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/deparse.c
+ *
+ *-------------------------------------------------------------------------
+ */
+ #include "postgres.h"
+
+ #include "access/transam.h"
+ #include "catalog/pg_class.h"
+ #include "commands/defrem.h"
+ #include "foreign/foreign.h"
+ #include "lib/stringinfo.h"
+ #include "nodes/nodeFuncs.h"
+ #include "nodes/nodes.h"
+ #include "nodes/makefuncs.h"
+ #include "optimizer/clauses.h"
+ #include "optimizer/var.h"
+ #include "parser/parsetree.h"
+ #include "utils/builtins.h"
+ #include "utils/lsyscache.h"
+
+ #include "pgsql_fdw.h"
+
+ /*
+ * Context for walk-through the expression tree.
+ */
+ typedef struct foreign_executable_cxt
+ {
+ PlannerInfo *root;
+ RelOptInfo *foreignrel;
+ } foreign_executable_cxt;
+
+ /*
+ * Get string representation which can be used in SQL statement from a node.
+ */
+ static void deparseRelation(StringInfo buf, Oid relid, PlannerInfo *root,
+ bool need_prefix);
+ static void deparseVar(StringInfo buf, Var *node, PlannerInfo *root,
+ bool need_prefix);
+
+ /*
+ * Deparse query representation into SQL statement which suits for remote
+ * PostgreSQL server. This function basically creates simple query string
+ * which consists of only SELECT, FROM clauses.
+ */
+ void
+ deparseSimpleSql(StringInfo buf,
+ Oid relid,
+ PlannerInfo *root,
+ RelOptInfo *baserel,
+ ForeignTable *table)
+ {
+ StringInfoData foreign_relname;
+ bool first;
+ AttrNumber attr;
+ List *attr_used = NIL; /* List of AttNumber used in the query */
+
+ initStringInfo(buf);
+ initStringInfo(&foreign_relname);
+
+ /*
+ * First of all, determine which column should be retrieved for this scan.
+ *
+ * We do this before deparsing SELECT clause because attributes which are
+ * not used in neither reltargetlist nor baserel->baserestrictinfo, quals
+ * evaluated on local, can be replaced with literal "NULL" in the SELECT
+ * clause to reduce overhead of tuple handling tuple and data transfer.
+ */
+ if (baserel->baserestrictinfo != NIL)
+ {
+ ListCell *lc;
+
+ foreach (lc, baserel->baserestrictinfo)
+ {
+ RestrictInfo *ri = (RestrictInfo *) lfirst(lc);
+ List *attrs;
+
+ /*
+ * We need to know which attributes are used in qual evaluated
+ * on the local server, because they should be listed in the
+ * SELECT clause of remote query. We can ignore attributes
+ * which are referenced only in ORDER BY/GROUP BY clause because
+ * such attributes has already been kept in reltargetlist.
+ */
+ attrs = pull_var_clause((Node *) ri->clause,
+ PVC_RECURSE_AGGREGATES,
+ PVC_RECURSE_PLACEHOLDERS);
+ attr_used = list_union(attr_used, attrs);
+ }
+ }
+
+ /*
+ * deparse SELECT clause
+ *
+ * List attributes which are in either target list or local restriction.
+ * Unused attributes are replaced with a literal "NULL" for optimization.
+ *
+ * Note that nothing is added for dropped columns, though tuple constructor
+ * function requires entries for dropped columns. Such entries must be
+ * initialized with NULL before calling tuple constructor.
+ */
+ appendStringInfo(buf, "SELECT ");
+ attr_used = list_union(attr_used, baserel->reltargetlist);
+ first = true;
+ for (attr = 1; attr <= baserel->max_attr; attr++)
+ {
+ RangeTblEntry *rte = root->simple_rte_array[baserel->relid];
+ Var *var = NULL;
+ ListCell *lc;
+
+ /* Ignore dropped attributes. */
+ if (get_rte_attribute_is_dropped(rte, attr))
+ continue;
+
+ if (!first)
+ appendStringInfo(buf, ", ");
+ first = false;
+
+ /*
+ * We use linear search here, but it wouldn't be problem since
+ * attr_used seems to not become so large.
+ */
+ foreach (lc, attr_used)
+ {
+ var = lfirst(lc);
+ if (var->varattno == attr)
+ break;
+ var = NULL;
+ }
+ if (var != NULL)
+ deparseVar(buf, var, root, false);
+ else
+ appendStringInfo(buf, "NULL");
+ }
+ appendStringInfoChar(buf, ' ');
+
+ /*
+ * deparse FROM clause, including alias if any
+ */
+ appendStringInfo(buf, "FROM ");
+ deparseRelation(buf, table->relid, root, true);
+
+ elog(DEBUG3, "Remote SQL: %s", buf->data);
+ }
+
+ /*
+ * Deparse node into buf, with relation qualifier if need_prefix was true. If
+ * node is a column of a foreign table, use value of colname FDW option (if any)
+ * instead of attribute name.
+ */
+ static void
+ deparseVar(StringInfo buf,
+ Var *node,
+ PlannerInfo *root,
+ bool need_prefix)
+ {
+ RangeTblEntry *rte;
+ char *colname = NULL;
+ const char *q_colname = NULL;
+ List *options;
+ ListCell *lc;
+
+ /* node must not be any of OUTER_VAR,INNER_VAR and INDEX_VAR. */
+ Assert(node->varno >= 1 && node->varno <= root->simple_rel_array_size);
+
+ /* Get RangeTblEntry from array in PlannerInfo. */
+ rte = root->simple_rte_array[node->varno];
+
+ /*
+ * If the node is a column of a foreign table, and it has colname FDW
+ * option, use its value.
+ */
+ if (rte->relkind == RELKIND_FOREIGN_TABLE)
+ {
+ options = GetForeignColumnOptions(rte->relid, node->varattno);
+ foreach(lc, options)
+ {
+ DefElem *def = (DefElem *) lfirst(lc);
+
+ if (strcmp(def->defname, "colname") == 0)
+ {
+ colname = defGetString(def);
+ break;
+ }
+ }
+ }
+
+ /*
+ * If the node refers a column of a regular table or it doesn't have colname
+ * FDW option, use attribute name.
+ */
+ if (colname == NULL)
+ colname = get_attname(rte->relid, node->varattno);
+
+ if (need_prefix)
+ {
+ char *aliasname;
+ const char *q_aliasname;
+
+ if (rte->eref != NULL && rte->eref->aliasname != NULL)
+ aliasname = rte->eref->aliasname;
+ else if (rte->alias != NULL && rte->alias->aliasname != NULL)
+ aliasname = rte->alias->aliasname;
+
+ q_aliasname = quote_identifier(aliasname);
+ appendStringInfo(buf, "%s.", q_aliasname);
+ }
+
+ q_colname = quote_identifier(colname);
+ appendStringInfo(buf, "%s", q_colname);
+ }
+
+ /*
+ * Deparse table which has relid as oid into buf, with schema qualifier if
+ * need_prefix was true. If relid points a foreign table, use value of relname
+ * FDW option (if any) instead of relation's name. Similarly, nspname FDW
+ * option overrides schema name.
+ */
+ static void
+ deparseRelation(StringInfo buf,
+ Oid relid,
+ PlannerInfo *root,
+ bool need_prefix)
+ {
+ int i;
+ RangeTblEntry *rte = NULL;
+ ListCell *lc;
+ const char *nspname = NULL; /* plain namespace name */
+ const char *relname = NULL; /* plain relation name */
+ const char *q_nspname; /* quoted namespace name */
+ const char *q_relname; /* quoted relation name */
+
+ /* Find RangeTblEntry for the relation from PlannerInfo. */
+ for (i = 1; i < root->simple_rel_array_size; i++)
+ {
+ if (root->simple_rte_array[i]->relid == relid)
+ {
+ rte = root->simple_rte_array[i];
+ break;
+ }
+ }
+ if (rte == NULL)
+ elog(ERROR, "relation with OID %u is not used in the query", relid);
+
+ /* If target is a foreign table, obtain additional catalog information. */
+ if (rte->relkind == RELKIND_FOREIGN_TABLE)
+ {
+ ForeignTable *table = GetForeignTable(rte->relid);
+
+ /*
+ * Use value of FDW options if any, instead of the name of object
+ * itself.
+ */
+ foreach(lc, table->options)
+ {
+ DefElem *def = (DefElem *) lfirst(lc);
+
+ if (need_prefix && strcmp(def->defname, "nspname") == 0)
+ nspname = defGetString(def);
+ else if (strcmp(def->defname, "relname") == 0)
+ relname = defGetString(def);
+ }
+ }
+
+ /* Quote each identifier, if necessary. */
+ if (need_prefix)
+ {
+ if (nspname == NULL)
+ nspname = get_namespace_name(get_rel_namespace(relid));
+ q_nspname = quote_identifier(nspname);
+ }
+
+ if (relname == NULL)
+ relname = get_rel_name(relid);
+ q_relname = quote_identifier(relname);
+
+ /* Construct relation reference into the buffer. */
+ if (need_prefix)
+ appendStringInfo(buf, "%s.", q_nspname);
+ appendStringInfo(buf, "%s", q_relname);
+ }
diff --git a/contrib/pgsql_fdw/expected/pgsql_fdw.out b/contrib/pgsql_fdw/expected/pgsql_fdw.out
index ...4252cc0 .
*** a/contrib/pgsql_fdw/expected/pgsql_fdw.out
--- b/contrib/pgsql_fdw/expected/pgsql_fdw.out
***************
*** 0 ****
--- 1,600 ----
+ -- ===================================================================
+ -- create FDW objects
+ -- ===================================================================
+ -- Clean up in case a prior regression run failed
+ -- Suppress NOTICE messages when roles don't exist
+ SET client_min_messages TO 'error';
+ DROP ROLE IF EXISTS pgsql_fdw_user;
+ RESET client_min_messages;
+ CREATE ROLE pgsql_fdw_user LOGIN SUPERUSER;
+ SET SESSION AUTHORIZATION 'pgsql_fdw_user';
+ CREATE EXTENSION pgsql_fdw;
+ CREATE SERVER loopback1 FOREIGN DATA WRAPPER pgsql_fdw;
+ CREATE SERVER loopback2 FOREIGN DATA WRAPPER pgsql_fdw
+ OPTIONS (dbname 'contrib_regression');
+ CREATE USER MAPPING FOR public SERVER loopback1
+ OPTIONS (user 'value', password 'value');
+ CREATE USER MAPPING FOR pgsql_fdw_user SERVER loopback2;
+ CREATE FOREIGN TABLE ft1 (
+ c0 int,
+ c1 int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10)
+ ) SERVER loopback2;
+ ALTER FOREIGN TABLE ft1 DROP COLUMN c0;
+ CREATE FOREIGN TABLE ft2 (
+ c0 int,
+ c1 int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10)
+ ) SERVER loopback2;
+ ALTER FOREIGN TABLE ft2 DROP COLUMN c0;
+ -- ===================================================================
+ -- create objects used through FDW
+ -- ===================================================================
+ CREATE SCHEMA "S 1";
+ CREATE TABLE "S 1"."T 1" (
+ "C 1" int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10),
+ CONSTRAINT t1_pkey PRIMARY KEY ("C 1")
+ );
+ NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "t1_pkey" for table "T 1"
+ CREATE TABLE "S 1"."T 2" (
+ c1 int NOT NULL,
+ c2 text,
+ CONSTRAINT t2_pkey PRIMARY KEY (c1)
+ );
+ NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "t2_pkey" for table "T 2"
+ BEGIN;
+ TRUNCATE "S 1"."T 1";
+ INSERT INTO "S 1"."T 1"
+ SELECT id,
+ id % 10,
+ to_char(id, 'FM00000'),
+ '1970-01-01'::timestamptz + ((id % 100) || ' days')::interval,
+ '1970-01-01'::timestamp + ((id % 100) || ' days')::interval,
+ id % 10,
+ id % 10
+ FROM generate_series(1, 1000) id;
+ TRUNCATE "S 1"."T 2";
+ INSERT INTO "S 1"."T 2"
+ SELECT id,
+ 'AAA' || to_char(id, 'FM000')
+ FROM generate_series(1, 100) id;
+ COMMIT;
+ -- ===================================================================
+ -- tests for pgsql_fdw_validator
+ -- ===================================================================
+ ALTER FOREIGN DATA WRAPPER pgsql_fdw OPTIONS (host 'value'); -- ERROR
+ ERROR: invalid option "host"
+ HINT: Valid options in this context are:
+ -- requiressl, krbsrvname and gsslib are omitted because they depend on
+ -- configure option
+ ALTER SERVER loopback1 OPTIONS (
+ authtype 'value',
+ service 'value',
+ connect_timeout 'value',
+ dbname 'value',
+ host 'value',
+ hostaddr 'value',
+ port 'value',
+ --client_encoding 'value',
+ tty 'value',
+ options 'value',
+ application_name 'value',
+ --fallback_application_name 'value',
+ keepalives 'value',
+ keepalives_idle 'value',
+ keepalives_interval 'value',
+ -- requiressl 'value',
+ sslcompression 'value',
+ sslmode 'value',
+ sslcert 'value',
+ sslkey 'value',
+ sslrootcert 'value',
+ sslcrl 'value'
+ --requirepeer 'value',
+ -- krbsrvname 'value',
+ -- gsslib 'value',
+ --replication 'value'
+ );
+ ALTER SERVER loopback1 OPTIONS (user 'value'); -- ERROR
+ ERROR: invalid option "user"
+ HINT: Valid options in this context are: authtype, service, connect_timeout, dbname, host, hostaddr, port, tty, options, application_name, keepalives, keepalives_idle, keepalives_interval, keepalives_count, requiressl, sslcompression, sslmode, sslcert, sslkey, sslrootcert, sslcrl, requirepeer, krbsrvname, gsslib, fetch_count
+ ALTER SERVER loopback2 OPTIONS (ADD fetch_count '2');
+ ALTER USER MAPPING FOR public SERVER loopback1
+ OPTIONS (DROP user, DROP password);
+ ALTER USER MAPPING FOR public SERVER loopback1
+ OPTIONS (host 'value'); -- ERROR
+ ERROR: invalid option "host"
+ HINT: Valid options in this context are: user, password
+ ALTER FOREIGN TABLE ft1 OPTIONS (nspname 'S 1', relname 'T 1');
+ ALTER FOREIGN TABLE ft2 OPTIONS (nspname 'S 1', relname 'T 1', fetch_count '100');
+ ALTER FOREIGN TABLE ft1 OPTIONS (invalid 'value'); -- ERROR
+ ERROR: invalid option "invalid"
+ HINT: Valid options in this context are: nspname, relname, fetch_count
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count 'a'); -- ERROR
+ ERROR: invalid value for fetch_count: "a"
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count '0'); -- ERROR
+ ERROR: invalid value for fetch_count: "0"
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count '-1'); -- ERROR
+ ERROR: invalid value for fetch_count: "-1"
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (invalid 'value'); -- ERROR
+ ERROR: invalid option "invalid"
+ HINT: Valid options in this context are: colname
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (colname 'C 1');
+ ALTER FOREIGN TABLE ft2 ALTER COLUMN c1 OPTIONS (colname 'C 1');
+ \dew+
+ List of foreign-data wrappers
+ Name | Owner | Handler | Validator | Access privileges | FDW Options | Description
+ -----------+----------------+-------------------+---------------------+-------------------+-------------+-------------
+ pgsql_fdw | pgsql_fdw_user | pgsql_fdw_handler | pgsql_fdw_validator | | |
+ (1 row)
+
+ \des+
+ List of foreign servers
+ Name | Owner | Foreign-data wrapper | Access privileges | Type | Version | FDW Options | Description
+ -----------+----------------+----------------------+-------------------+------+---------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+-------------
+ loopback1 | pgsql_fdw_user | pgsql_fdw | | | | (authtype 'value', service 'value', connect_timeout 'value', dbname 'value', host 'value', hostaddr 'value', port 'value', tty 'value', options 'value', application_name 'value', keepalives 'value', keepalives_idle 'value', keepalives_interval 'value', sslcompression 'value', sslmode 'value', sslcert 'value', sslkey 'value', sslrootcert 'value', sslcrl 'value') |
+ loopback2 | pgsql_fdw_user | pgsql_fdw | | | | (dbname 'contrib_regression', fetch_count '2') |
+ (2 rows)
+
+ \deu+
+ List of user mappings
+ Server | User name | FDW Options
+ -----------+----------------+-------------
+ loopback1 | public |
+ loopback2 | pgsql_fdw_user |
+ (2 rows)
+
+ \det+
+ List of foreign tables
+ Schema | Table | Server | FDW Options | Description
+ --------+-------+-----------+---------------------------------------------------+-------------
+ public | ft1 | loopback2 | (nspname 'S 1', relname 'T 1') |
+ public | ft2 | loopback2 | (nspname 'S 1', relname 'T 1', fetch_count '100') |
+ (2 rows)
+
+ -- ===================================================================
+ -- simple queries
+ -- ===================================================================
+ -- single table, with/without alias
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
+ QUERY PLAN
+ ------------------------------------------------------------------------------------------------------------------------------
+ Limit
+ Output: c1, c2, c3, c4, c5, c6, c7
+ -> Sort
+ Output: c1, c2, c3, c4, c5, c6, c7
+ Sort Key: ft1.c3, ft1.c1
+ -> Foreign Scan on public.ft1
+ Output: c1, c2, c3, c4, c5, c6, c7
+ Remote SQL: DECLARE pgsql_fdw_cursor_0 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (8 rows)
+
+ SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ -----+----+-------+------------------------------+--------------------------+----+------------
+ 101 | 1 | 00101 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970 | 1 | 1
+ 102 | 2 | 00102 | Sat Jan 03 00:00:00 1970 PST | Sat Jan 03 00:00:00 1970 | 2 | 2
+ 103 | 3 | 00103 | Sun Jan 04 00:00:00 1970 PST | Sun Jan 04 00:00:00 1970 | 3 | 3
+ 104 | 4 | 00104 | Mon Jan 05 00:00:00 1970 PST | Mon Jan 05 00:00:00 1970 | 4 | 4
+ 105 | 5 | 00105 | Tue Jan 06 00:00:00 1970 PST | Tue Jan 06 00:00:00 1970 | 5 | 5
+ 106 | 6 | 00106 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6
+ 107 | 7 | 00107 | Thu Jan 08 00:00:00 1970 PST | Thu Jan 08 00:00:00 1970 | 7 | 7
+ 108 | 8 | 00108 | Fri Jan 09 00:00:00 1970 PST | Fri Jan 09 00:00:00 1970 | 8 | 8
+ 109 | 9 | 00109 | Sat Jan 10 00:00:00 1970 PST | Sat Jan 10 00:00:00 1970 | 9 | 9
+ 110 | 0 | 00110 | Sun Jan 11 00:00:00 1970 PST | Sun Jan 11 00:00:00 1970 | 0 | 0
+ (10 rows)
+
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+ QUERY PLAN
+ ------------------------------------------------------------------------------------------------------------------------------
+ Limit
+ Output: c1, c2, c3, c4, c5, c6, c7
+ -> Sort
+ Output: c1, c2, c3, c4, c5, c6, c7
+ Sort Key: t1.c3, t1.c1
+ -> Foreign Scan on public.ft1 t1
+ Output: c1, c2, c3, c4, c5, c6, c7
+ Remote SQL: DECLARE pgsql_fdw_cursor_2 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (8 rows)
+
+ SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ -----+----+-------+------------------------------+--------------------------+----+------------
+ 101 | 1 | 00101 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970 | 1 | 1
+ 102 | 2 | 00102 | Sat Jan 03 00:00:00 1970 PST | Sat Jan 03 00:00:00 1970 | 2 | 2
+ 103 | 3 | 00103 | Sun Jan 04 00:00:00 1970 PST | Sun Jan 04 00:00:00 1970 | 3 | 3
+ 104 | 4 | 00104 | Mon Jan 05 00:00:00 1970 PST | Mon Jan 05 00:00:00 1970 | 4 | 4
+ 105 | 5 | 00105 | Tue Jan 06 00:00:00 1970 PST | Tue Jan 06 00:00:00 1970 | 5 | 5
+ 106 | 6 | 00106 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6
+ 107 | 7 | 00107 | Thu Jan 08 00:00:00 1970 PST | Thu Jan 08 00:00:00 1970 | 7 | 7
+ 108 | 8 | 00108 | Fri Jan 09 00:00:00 1970 PST | Fri Jan 09 00:00:00 1970 | 8 | 8
+ 109 | 9 | 00109 | Sat Jan 10 00:00:00 1970 PST | Sat Jan 10 00:00:00 1970 | 9 | 9
+ 110 | 0 | 00110 | Sun Jan 11 00:00:00 1970 PST | Sun Jan 11 00:00:00 1970 | 0 | 0
+ (10 rows)
+
+ -- with WHERE clause
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
+ QUERY PLAN
+ ------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on public.ft1 t1
+ Output: c1, c2, c3, c4, c5, c6, c7
+ Filter: ((t1.c1 = 101) AND ((t1.c6)::text = '1'::text) AND (t1.c7 = '1'::bpchar))
+ Remote SQL: DECLARE pgsql_fdw_cursor_4 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (4 rows)
+
+ SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ -----+----+-------+------------------------------+--------------------------+----+------------
+ 101 | 1 | 00101 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970 | 1 | 1
+ (1 row)
+
+ -- aggregate
+ SELECT COUNT(*) FROM ft1 t1;
+ count
+ -------
+ 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
+ ----+----+-------+------------------------------+--------------------------+----+------------
+ 1 | 1 | 00001 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970 | 1 | 1
+ 2 | 2 | 00002 | Sat Jan 03 00:00:00 1970 PST | Sat Jan 03 00:00:00 1970 | 2 | 2
+ 3 | 3 | 00003 | Sun Jan 04 00:00:00 1970 PST | Sun Jan 04 00:00:00 1970 | 3 | 3
+ 4 | 4 | 00004 | Mon Jan 05 00:00:00 1970 PST | Mon Jan 05 00:00:00 1970 | 4 | 4
+ 5 | 5 | 00005 | Tue Jan 06 00:00:00 1970 PST | Tue Jan 06 00:00:00 1970 | 5 | 5
+ 6 | 6 | 00006 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6
+ 7 | 7 | 00007 | Thu Jan 08 00:00:00 1970 PST | Thu Jan 08 00:00:00 1970 | 7 | 7
+ 8 | 8 | 00008 | Fri Jan 09 00:00:00 1970 PST | Fri Jan 09 00:00:00 1970 | 8 | 8
+ 9 | 9 | 00009 | Sat Jan 10 00:00:00 1970 PST | Sat Jan 10 00:00:00 1970 | 9 | 9
+ 10 | 0 | 00010 | Sun Jan 11 00:00:00 1970 PST | Sun Jan 11 00:00:00 1970 | 0 | 0
+ (10 rows)
+
+ -- subquery+MAX
+ SELECT * FROM ft1 t1 WHERE t1.c3 = (SELECT MAX(c3) FROM ft2 t2) ORDER BY c1;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ ------+----+-------+------------------------------+--------------------------+----+------------
+ 1000 | 0 | 01000 | Thu Jan 01 00:00:00 1970 PST | Thu Jan 01 00:00:00 1970 | 0 | 0
+ (1 row)
+
+ -- used in CTE
+ WITH t1 AS (SELECT * FROM ft1 WHERE c1 <= 10) SELECT t2.c1, t2.c2, t2.c3, t2.c4 FROM t1, ft2 t2 WHERE t1.c1 = t2.c1 ORDER BY t1.c1;
+ c1 | c2 | c3 | c4
+ ----+----+-------+------------------------------
+ 1 | 1 | 00001 | Fri Jan 02 00:00:00 1970 PST
+ 2 | 2 | 00002 | Sat Jan 03 00:00:00 1970 PST
+ 3 | 3 | 00003 | Sun Jan 04 00:00:00 1970 PST
+ 4 | 4 | 00004 | Mon Jan 05 00:00:00 1970 PST
+ 5 | 5 | 00005 | Tue Jan 06 00:00:00 1970 PST
+ 6 | 6 | 00006 | Wed Jan 07 00:00:00 1970 PST
+ 7 | 7 | 00007 | Thu Jan 08 00:00:00 1970 PST
+ 8 | 8 | 00008 | Fri Jan 09 00:00:00 1970 PST
+ 9 | 9 | 00009 | Sat Jan 10 00:00:00 1970 PST
+ 10 | 0 | 00010 | Sun Jan 11 00:00:00 1970 PST
+ (10 rows)
+
+ -- fixed values
+ SELECT 'fixed', NULL FROM ft1 t1 WHERE c1 = 1;
+ ?column? | ?column?
+ ----------+----------
+ fixed |
+ (1 row)
+
+ -- user-defined operator/function
+ CREATE FUNCTION pgsql_fdw_abs(int) RETURNS int AS $$
+ BEGIN
+ RETURN abs($1);
+ END
+ $$ LANGUAGE plpgsql IMMUTABLE;
+ CREATE OPERATOR === (
+ LEFTARG = int,
+ RIGHTARG = int,
+ PROCEDURE = int4eq,
+ COMMUTATOR = ===,
+ NEGATOR = !==
+ );
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = pgsql_fdw_abs(t1.c2);
+ QUERY PLAN
+ ---------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = pgsql_fdw_abs(c2))
+ Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 === t1.c2;
+ QUERY PLAN
+ ---------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 === c2)
+ Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = abs(t1.c2);
+ QUERY PLAN
+ ---------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = abs(c2))
+ Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = t1.c2;
+ QUERY PLAN
+ ---------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = c2)
+ Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ -- ===================================================================
+ -- parameterized queries
+ -- ===================================================================
+ -- simple join
+ PREPARE st1(int, int) AS SELECT t1.c3, t2.c3 FROM ft1 t1, ft2 t2 WHERE t1.c1 = $1 AND t2.c1 = $2;
+ EXPLAIN (COSTS false) EXECUTE st1(1, 2);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------
+ Nested Loop
+ -> Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: SELECT "C 1", NULL, c3, NULL, NULL, NULL, NULL FROM "S 1"."T 1"
+ -> Materialize
+ -> Foreign Scan on ft2 t2
+ Filter: (c1 = 2)
+ Remote SQL: SELECT "C 1", NULL, c3, NULL, NULL, NULL, NULL FROM "S 1"."T 1"
+ (8 rows)
+
+ EXECUTE st1(1, 1);
+ c3 | c3
+ -------+-------
+ 00001 | 00001
+ (1 row)
+
+ EXECUTE st1(101, 101);
+ c3 | c3
+ -------+-------
+ 00101 | 00101
+ (1 row)
+
+ -- subquery using stable function (can't be pushed down)
+ PREPARE st2(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c4) = 6) ORDER BY c1;
+ EXPLAIN (COSTS false) EXECUTE st2(10, 20);
+ QUERY PLAN
+ ------------------------------------------------------------------------------------------------------
+ Sort
+ Sort Key: t1.c1
+ -> Hash Join
+ Hash Cond: (t1.c3 = t2.c3)
+ -> Foreign Scan on ft1 t1
+ Filter: (c1 < 20)
+ Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ -> Hash
+ -> HashAggregate
+ -> Foreign Scan on ft2 t2
+ Filter: ((c1 > 10) AND (date_part('dow'::text, c4) = 6::double precision))
+ Remote SQL: SELECT "C 1", NULL, c3, c4, NULL, NULL, NULL FROM "S 1"."T 1"
+ (12 rows)
+
+ EXECUTE st2(10, 20);
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ ----+----+-------+------------------------------+--------------------------+----+------------
+ 16 | 6 | 00016 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970 | 6 | 6
+ (1 row)
+
+ EXECUTE st1(101, 101);
+ c3 | c3
+ -------+-------
+ 00101 | 00101
+ (1 row)
+
+ -- subquery using immutable function (can be pushed down)
+ PREPARE st3(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c5) = 6) ORDER BY c1;
+ EXPLAIN (COSTS false) EXECUTE st3(10, 20);
+ QUERY PLAN
+ ------------------------------------------------------------------------------------------------------
+ Sort
+ Sort Key: t1.c1
+ -> Hash Join
+ Hash Cond: (t1.c3 = t2.c3)
+ -> Foreign Scan on ft1 t1
+ Filter: (c1 < 20)
+ Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ -> Hash
+ -> HashAggregate
+ -> Foreign Scan on ft2 t2
+ Filter: ((c1 > 10) AND (date_part('dow'::text, c5) = 6::double precision))
+ Remote SQL: SELECT "C 1", NULL, c3, NULL, c5, NULL, NULL FROM "S 1"."T 1"
+ (12 rows)
+
+ EXECUTE st3(10, 20);
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ ----+----+-------+------------------------------+--------------------------+----+------------
+ 16 | 6 | 00016 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970 | 6 | 6
+ (1 row)
+
+ EXECUTE st3(20, 30);
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ ----+----+-------+------------------------------+--------------------------+----+------------
+ 23 | 3 | 00023 | Sat Jan 24 00:00:00 1970 PST | Sat Jan 24 00:00:00 1970 | 3 | 3
+ (1 row)
+
+ -- custom plan should be chosen
+ PREPARE st4(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 = $1;
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ ---------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ ---------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ ---------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ ---------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ ---------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ ---------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = $1)
+ Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ -- cleanup
+ DEALLOCATE st1;
+ DEALLOCATE st2;
+ DEALLOCATE st3;
+ DEALLOCATE st4;
+ -- ===================================================================
+ -- connection management
+ -- ===================================================================
+ SELECT srvname, usename FROM pgsql_fdw_connections;
+ srvname | usename
+ -----------+----------------
+ loopback2 | pgsql_fdw_user
+ (1 row)
+
+ SELECT pgsql_fdw_disconnect(srvid, usesysid) FROM pgsql_fdw_get_connections();
+ pgsql_fdw_disconnect
+ ----------------------
+ OK
+ (1 row)
+
+ SELECT srvname, usename FROM pgsql_fdw_connections;
+ srvname | usename
+ ---------+---------
+ (0 rows)
+
+ -- ===================================================================
+ -- conversion error
+ -- ===================================================================
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c4 TYPE int;
+ SELECT * FROM ft1 WHERE c1 = 1; -- ERROR
+ ERROR: invalid input syntax for integer: "1970-01-02 17:00:00+09"
+ CONTEXT: column c4 of foreign table ft1
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c4 TYPE timestamptz;
+ -- ===================================================================
+ -- subtransaction
+ -- ===================================================================
+ BEGIN;
+ DECLARE c CURSOR FOR SELECT * FROM ft1 ORDER BY c1;
+ FETCH c;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ ----+----+-------+------------------------------+--------------------------+----+------------
+ 1 | 1 | 00001 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970 | 1 | 1
+ (1 row)
+
+ SAVEPOINT s;
+ ERROR OUT; -- ERROR
+ ERROR: syntax error at or near "ERROR"
+ LINE 1: ERROR OUT;
+ ^
+ ROLLBACK TO s;
+ SELECT srvname FROM pgsql_fdw_connections;
+ srvname
+ -----------
+ loopback2
+ (1 row)
+
+ FETCH c;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ ----+----+-------+------------------------------+--------------------------+----+------------
+ 2 | 2 | 00002 | Sat Jan 03 00:00:00 1970 PST | Sat Jan 03 00:00:00 1970 | 2 | 2
+ (1 row)
+
+ COMMIT;
+ SELECT srvname FROM pgsql_fdw_connections;
+ srvname
+ -----------
+ loopback2
+ (1 row)
+
+ ERROR OUT; -- ERROR
+ ERROR: syntax error at or near "ERROR"
+ LINE 1: ERROR OUT;
+ ^
+ SELECT srvname FROM pgsql_fdw_connections;
+ srvname
+ ---------
+ (0 rows)
+
+ -- ===================================================================
+ -- cleanup
+ -- ===================================================================
+ DROP OPERATOR === (int, int) CASCADE;
+ DROP OPERATOR !== (int, int) CASCADE;
+ DROP FUNCTION pgsql_fdw_abs(int);
+ DROP SCHEMA "S 1" CASCADE;
+ NOTICE: drop cascades to 2 other objects
+ DETAIL: drop cascades to table "S 1"."T 1"
+ drop cascades to table "S 1"."T 2"
+ DROP EXTENSION pgsql_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 server loopback2
+ drop cascades to user mapping for pgsql_fdw_user
+ drop cascades to foreign table ft1
+ drop cascades to foreign table ft2
+ \c
+ DROP ROLE pgsql_fdw_user;
diff --git a/contrib/pgsql_fdw/option.c b/contrib/pgsql_fdw/option.c
index ...542ef01 .
*** a/contrib/pgsql_fdw/option.c
--- b/contrib/pgsql_fdw/option.c
***************
*** 0 ****
--- 1,285 ----
+ /*-------------------------------------------------------------------------
+ *
+ * option.c
+ * FDW option handling
+ *
+ * Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/option.c
+ *
+ *-------------------------------------------------------------------------
+ */
+ #include "postgres.h"
+
+ #include "access/reloptions.h"
+ #include "catalog/pg_foreign_data_wrapper.h"
+ #include "catalog/pg_foreign_server.h"
+ #include "catalog/pg_foreign_table.h"
+ #include "catalog/pg_user_mapping.h"
+ #include "commands/defrem.h"
+ #include "fmgr.h"
+ #include "foreign/foreign.h"
+ #include "lib/stringinfo.h"
+ #include "miscadmin.h"
+
+ #include "pgsql_fdw.h"
+
+ /*
+ * Default fetch count for cursor. This can be overridden by fetch_count FDW
+ * option.
+ */
+ #define DEFAULT_FETCH_COUNT 10000
+
+ /*
+ * SQL functions
+ */
+ extern Datum pgsql_fdw_validator(PG_FUNCTION_ARGS);
+ PG_FUNCTION_INFO_V1(pgsql_fdw_validator);
+
+ /*
+ * Describes the valid options for objects that use this wrapper.
+ */
+ typedef struct PgsqlFdwOption
+ {
+ const char *optname;
+ Oid optcontext; /* Oid of catalog in which options may appear */
+ bool is_libpq_opt; /* true if it's used in libpq */
+ } PgsqlFdwOption;
+
+ /*
+ * Valid options for pgsql_fdw.
+ */
+ static PgsqlFdwOption valid_options[] = {
+
+ /*
+ * Options for libpq connection.
+ * Note: This list should be updated along with PQconninfoOptions in
+ * interfaces/libpq/fe-connect.c, so the order is kept as is.
+ *
+ * Some useless libpq connection options are not accepted by pgsql_fdw:
+ * client_encoding: set to local database encoding automatically
+ * fallback_application_name: fixed to "pgsql_fdw"
+ * replication: pgsql_fdw never be replication client
+ */
+ {"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},
+ #ifdef NOT_USED
+ {"client_encoding", ForeignServerRelationId, true},
+ #endif
+ {"tty", ForeignServerRelationId, true},
+ {"options", ForeignServerRelationId, true},
+ {"application_name", ForeignServerRelationId, true},
+ #ifdef NOT_USED
+ {"fallback_application_name", ForeignServerRelationId, true},
+ #endif
+ {"keepalives", ForeignServerRelationId, true},
+ {"keepalives_idle", ForeignServerRelationId, true},
+ {"keepalives_interval", ForeignServerRelationId, true},
+ {"keepalives_count", ForeignServerRelationId, true},
+ {"requiressl", ForeignServerRelationId, true},
+ {"sslcompression", ForeignServerRelationId, true},
+ {"sslmode", ForeignServerRelationId, true},
+ {"sslcert", ForeignServerRelationId, true},
+ {"sslkey", ForeignServerRelationId, true},
+ {"sslrootcert", ForeignServerRelationId, true},
+ {"sslcrl", ForeignServerRelationId, true},
+ {"requirepeer", ForeignServerRelationId, true},
+ {"krbsrvname", ForeignServerRelationId, true},
+ {"gsslib", ForeignServerRelationId, true},
+ #ifdef NOT_USED
+ {"replication", ForeignServerRelationId, true},
+ #endif
+
+ /*
+ * Options for translation of object names.
+ */
+ {"nspname", ForeignTableRelationId, false},
+ {"relname", ForeignTableRelationId, false},
+ {"colname", AttributeRelationId, false},
+
+ /*
+ * Options for cursor behavior.
+ * These options can be overridden by finer-grained objects.
+ */
+ {"fetch_count", ForeignTableRelationId, false},
+ {"fetch_count", ForeignServerRelationId, false},
+
+ /* Terminating entry --- MUST BE LAST */
+ {NULL, InvalidOid, false}
+ };
+
+ /*
+ * Helper functions
+ */
+ static bool is_valid_option(const char *optname, Oid context);
+
+ /*
+ * Validate the generic options given to a FOREIGN DATA WRAPPER, SERVER,
+ * USER MAPPING or FOREIGN TABLE that uses pgsql_fdw.
+ *
+ * Raise an ERROR if the option or its value is considered invalid.
+ */
+ Datum
+ pgsql_fdw_validator(PG_FUNCTION_ARGS)
+ {
+ List *options_list = untransformRelOptions(PG_GETARG_DATUM(0));
+ Oid catalog = PG_GETARG_OID(1);
+ ListCell *cell;
+
+ /*
+ * Check that only options supported by pgsql_fdw, and allowed for the
+ * current object type, are given.
+ */
+ foreach(cell, options_list)
+ {
+ DefElem *def = (DefElem *) lfirst(cell);
+
+ if (!is_valid_option(def->defname, catalog))
+ {
+ PgsqlFdwOption *opt;
+ StringInfoData buf;
+
+ /*
+ * Unknown option specified, complain about it. Provide a hint
+ * with list of valid options for the object.
+ */
+ initStringInfo(&buf);
+ for (opt = valid_options; opt->optname; opt++)
+ {
+ if (catalog == opt->optcontext)
+ appendStringInfo(&buf, "%s%s", (buf.len > 0) ? ", " : "",
+ opt->optname);
+ }
+
+ ereport(ERROR,
+ (errcode(ERRCODE_FDW_INVALID_OPTION_NAME),
+ errmsg("invalid option \"%s\"", def->defname),
+ errhint("Valid options in this context are: %s",
+ buf.data)));
+ }
+
+ /* fetch_count be positive digit number. */
+ if (strcmp(def->defname, "fetch_count") == 0)
+ {
+ long value;
+ char *p = NULL;
+
+ value = strtol(defGetString(def), &p, 10);
+ if (*p != '\0' || value < 1)
+ ereport(ERROR,
+ (errcode(ERRCODE_FDW_INVALID_ATTRIBUTE_VALUE),
+ errmsg("invalid value for %s: \"%s\"",
+ def->defname, defGetString(def))));
+ }
+ }
+
+ /*
+ * We don't care option-specific limitation here; they will be validated at
+ * the execution time.
+ */
+
+ PG_RETURN_VOID();
+ }
+
+ /*
+ * Check whether the given option is one of the valid pgsql_fdw options.
+ * context is the Oid of the catalog holding the object the option is for.
+ */
+ static bool
+ is_valid_option(const char *optname, Oid context)
+ {
+ PgsqlFdwOption *opt;
+
+ for (opt = valid_options; opt->optname; opt++)
+ {
+ if (context == opt->optcontext && strcmp(opt->optname, optname) == 0)
+ return true;
+ }
+ return false;
+ }
+
+ /*
+ * Check whether the given option is one of the valid libpq options.
+ * context is the Oid of the catalog holding the object the option is for.
+ */
+ static bool
+ is_libpq_option(const char *optname)
+ {
+ PgsqlFdwOption *opt;
+
+ for (opt = valid_options; opt->optname; opt++)
+ {
+ if (strcmp(opt->optname, optname) == 0 && opt->is_libpq_opt)
+ return true;
+ }
+ return false;
+ }
+
+ /*
+ * Generate key-value arrays which includes only libpq options from the list
+ * which contains any kind of options.
+ */
+ int
+ ExtractConnectionOptions(List *defelems, const char **keywords, const char **values)
+ {
+ ListCell *lc;
+ int i;
+
+ i = 0;
+ foreach(lc, defelems)
+ {
+ DefElem *d = (DefElem *) lfirst(lc);
+ if (is_libpq_option(d->defname))
+ {
+ keywords[i] = d->defname;
+ values[i] = defGetString(d);
+ i++;
+ }
+ }
+ return i;
+ }
+
+ /*
+ * Return fetch_count which should be used for the foreign table.
+ */
+ int
+ GetFetchCountOption(ForeignTable *table, ForeignServer *server)
+ {
+ int fetch_count = DEFAULT_FETCH_COUNT;
+ ListCell *lc;
+ DefElem *def;
+
+ /*
+ * Use specified fetch_count instead of default value, if any. Foreign
+ * table option overrides server option.
+ */
+ foreach(lc, table->options)
+ {
+ def = (DefElem *) lfirst(lc);
+ if (strcmp(def->defname, "fetch_count") == 0)
+ break;
+
+ }
+ if (lc == NULL)
+ {
+ foreach(lc, server->options)
+ {
+ def = (DefElem *) lfirst(lc);
+ if (strcmp(def->defname, "fetch_count") == 0)
+ break;
+
+ }
+ }
+ if (lc != NULL)
+ fetch_count = strtol(defGetString(def), NULL, 10);
+
+ return fetch_count;
+ }
diff --git a/contrib/pgsql_fdw/pgsql_fdw--1.0.sql b/contrib/pgsql_fdw/pgsql_fdw--1.0.sql
index ...b0ea2b2 .
*** a/contrib/pgsql_fdw/pgsql_fdw--1.0.sql
--- b/contrib/pgsql_fdw/pgsql_fdw--1.0.sql
***************
*** 0 ****
--- 1,39 ----
+ /* contrib/pgsql_fdw/pgsql_fdw--1.0.sql */
+
+ -- complain if script is sourced in psql, rather than via CREATE EXTENSION
+ \echo Use "CREATE EXTENSION pgsql_fdw" to load this file. \quit
+
+ CREATE FUNCTION pgsql_fdw_handler()
+ RETURNS fdw_handler
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C STRICT;
+
+ CREATE FUNCTION pgsql_fdw_validator(text[], oid)
+ RETURNS void
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C STRICT;
+
+ CREATE FOREIGN DATA WRAPPER pgsql_fdw
+ HANDLER pgsql_fdw_handler
+ VALIDATOR pgsql_fdw_validator;
+
+ /* connection management functions and view */
+ CREATE FUNCTION pgsql_fdw_get_connections(out srvid oid, out usesysid oid)
+ RETURNS SETOF record
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C STRICT;
+
+ CREATE FUNCTION pgsql_fdw_disconnect(oid, oid)
+ RETURNS text
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C STRICT;
+
+ CREATE VIEW pgsql_fdw_connections AS
+ SELECT c.srvid srvid,
+ s.srvname srvname,
+ c.usesysid usesysid,
+ pg_get_userbyid(c.usesysid) usename
+ FROM pgsql_fdw_get_connections() c
+ JOIN pg_catalog.pg_foreign_server s ON (s.oid = c.srvid);
+ GRANT SELECT ON pgsql_fdw_connections TO public;
+
diff --git a/contrib/pgsql_fdw/pgsql_fdw.c b/contrib/pgsql_fdw/pgsql_fdw.c
index ...17693e0 .
*** a/contrib/pgsql_fdw/pgsql_fdw.c
--- b/contrib/pgsql_fdw/pgsql_fdw.c
***************
*** 0 ****
--- 1,1067 ----
+ /*-------------------------------------------------------------------------
+ *
+ * pgsql_fdw.c
+ * foreign-data wrapper for remote PostgreSQL servers.
+ *
+ * Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/pgsql_fdw.c
+ *
+ *-------------------------------------------------------------------------
+ */
+ #include "postgres.h"
+ #include "fmgr.h"
+
+ #include "catalog/pg_foreign_server.h"
+ #include "catalog/pg_foreign_table.h"
+ #include "commands/defrem.h"
+ #include "commands/explain.h"
+ #include "foreign/fdwapi.h"
+ #include "funcapi.h"
+ #include "miscadmin.h"
+ #include "optimizer/cost.h"
+ #include "optimizer/pathnode.h"
+ #include "optimizer/planmain.h"
+ #include "optimizer/restrictinfo.h"
+ #include "utils/builtins.h"
+ #include "utils/lsyscache.h"
+ #include "utils/memutils.h"
+ #include "utils/rel.h"
+
+ #include "pgsql_fdw.h"
+ #include "connection.h"
+
+ PG_MODULE_MAGIC;
+
+ /*
+ * Cost to establish a connection.
+ * XXX: should be configurable per server?
+ */
+ #define CONNECTION_COSTS 100.0
+
+ /*
+ * Cost to transfer 1 byte from remote server.
+ * XXX: should be configurable per server?
+ */
+ #define TRANSFER_COSTS_PER_BYTE 0.001
+
+ /*
+ * Cursors which are used together in a local query require different name, so
+ * we use simple incremental name for that purpose. We don't care wrap around
+ * of cursor_id because it's hard to imagine that 2^32 cursors are used in a
+ * query.
+ */
+ #define CURSOR_NAME_FORMAT "pgsql_fdw_cursor_%u"
+ static uint32 cursor_id = 0;
+
+ /*
+ * FDW-specific information for RelOptInfo.fdw_private. This is used to pass
+ * information from pgsqlGetForeignRelSize to pgsqlGetForeignPaths.
+ */
+ typedef struct PgsqlFdwPlanState {
+ /*
+ * These are generated in GetForeignRelSize, and also used in subsequent
+ * GetForeignPaths.
+ */
+ StringInfoData sql;
+ Cost startup_cost;
+ Cost total_cost;
+
+ /* Cached catalog information. */
+ ForeignTable *table;
+ ForeignServer *server;
+ } PgsqlFdwPlanState;
+
+ /*
+ * Index of FDW-private information stored in fdw_private list.
+ *
+ * We store various information in ForeignScan.fdw_private to pass them beyond
+ * the boundary between planner and executor. Finally FdwPlan using cursor
+ * would hold items below:
+ *
+ * 1) plain SELECT statement
+ * 2) SQL statement used to declare cursor
+ * 3) SQL statement used to fetch rows from cursor
+ * 4) SQL statement used to reset cursor
+ * 5) SQL statement used to close cursor
+ *
+ * These items are indexed with the enum FdwPrivateIndex, so an item
+ * can be accessed directly via list_nth(). For example of FETCH
+ * statement:
+ * list_nth(fdw_private, FdwPrivateFetchSql)
+ */
+ enum FdwPrivateIndex {
+ /* SQL statements */
+ FdwPrivateSelectSql,
+ FdwPrivateDeclareSql,
+ FdwPrivateFetchSql,
+ FdwPrivateResetSql,
+ FdwPrivateCloseSql,
+
+ /* # of elements stored in the list fdw_private */
+ FdwPrivateNum,
+ };
+
+ /*
+ * Describes an execution state of a foreign scan against a foreign table
+ * using pgsql_fdw.
+ */
+ typedef struct PgsqlFdwExecutionState
+ {
+ List *fdw_private; /* FDW-private information */
+
+ /* for remote query execution */
+ PGconn *conn; /* connection for the scan */
+ Oid *param_types; /* type array of external parameter */
+ const char **param_values; /* value array of external parameter */
+
+ /* for tuple generation. */
+ AttrNumber attnum; /* # of non-dropped attribute */
+ Datum *values; /* column value buffer */
+ bool *nulls; /* column null indicator buffer */
+ AttInMetadata *attinmeta; /* attribute metadata */
+
+ /* for storing result tuples */
+ MemoryContext scan_cxt; /* context for per-scan lifespan data */
+ Tuplestorestate *tuples; /* result of the scan */
+
+ /* for error handling. */
+ Oid relid; /* oid of the foreign table */
+ AttrNumber cur_attno; /* attribute number corrently processing */
+ } PgsqlFdwExecutionState;
+
+ /*
+ * SQL functions
+ */
+ extern Datum pgsql_fdw_handler(PG_FUNCTION_ARGS);
+ PG_FUNCTION_INFO_V1(pgsql_fdw_handler);
+
+ /*
+ * FDW callback routines
+ */
+ static void pgsqlGetForeignRelSize(PlannerInfo *root,
+ RelOptInfo *baserel,
+ Oid foreigntableid);
+ static void pgsqlGetForeignPaths(PlannerInfo *root,
+ RelOptInfo *baserel,
+ Oid foreigntableid);
+ static ForeignScan *pgsqlGetForeignPlan(PlannerInfo *root,
+ RelOptInfo *baserel,
+ Oid foreigntableid,
+ ForeignPath *best_path,
+ List *tlist,
+ List *scan_clauses);
+ static void pgsqlExplainForeignScan(ForeignScanState *node, ExplainState *es);
+ static void pgsqlBeginForeignScan(ForeignScanState *node, int eflags);
+ static TupleTableSlot *pgsqlIterateForeignScan(ForeignScanState *node);
+ static void pgsqlReScanForeignScan(ForeignScanState *node);
+ static void pgsqlEndForeignScan(ForeignScanState *node);
+
+ /*
+ * Helper functions
+ */
+ static void get_remote_estimate(const char *sql,
+ PGconn *conn,
+ double *rows,
+ int *width,
+ Cost *startup_cost,
+ Cost *total_cost);
+ static void adjust_costs(double rows, int width,
+ Cost *startup_cost, Cost *total_cost);
+ static void execute_query(ForeignScanState *node);
+ static PGresult *fetch_result(ForeignScanState *node);
+ static void store_result(ForeignScanState *node, PGresult *res);
+ static void pgsql_fdw_error_callback(void *arg);
+
+ /* Exported functions, but not written in pgsql_fdw.h. */
+ void _PG_init(void);
+ void _PG_fini(void);
+
+ /*
+ * Module-specific initialization.
+ */
+ void
+ _PG_init(void)
+ {
+ }
+
+ /*
+ * Module-specific clean up.
+ */
+ void
+ _PG_fini(void)
+ {
+ }
+
+ /*
+ * The content of FdwRoutine of pgsql_fdw is never changed, so we use one
+ * static object for all scan.
+ */
+ static FdwRoutine fdwroutine = {
+
+ /* Node type */
+ T_FdwRoutine,
+
+ /* Required handler functions. */
+ pgsqlGetForeignRelSize,
+ pgsqlGetForeignPaths,
+ pgsqlGetForeignPlan,
+ pgsqlExplainForeignScan,
+ pgsqlBeginForeignScan,
+ pgsqlIterateForeignScan,
+ pgsqlReScanForeignScan,
+ pgsqlEndForeignScan,
+
+ /* Optional handler functions. */
+ };
+
+ /*
+ * Foreign-data wrapper handler function: return a struct with pointers
+ * to my callback routines.
+ */
+ Datum
+ pgsql_fdw_handler(PG_FUNCTION_ARGS)
+ {
+ PG_RETURN_POINTER(&fdwroutine);
+ }
+
+ /*
+ * pgsqlGetForeignRelSize
+ * Estimate # of rows and width of the result of the scan
+ *
+ * Here we estimate number of rows returned by the scan in two steps. In the
+ * first step, we execute remote EXPLAIN command to obtain the number of rows
+ * returned from remote side. In the second step, we calculate the selectivity
+ * of the filtering done on local side, and modify first estimate.
+ *
+ * We have to get some catalog objects and generate remote query string here,
+ * so we store such expensive information in FDW private area of RelOptInfo and
+ * pass them to subsequent functions for reuse.
+ */
+ static void
+ pgsqlGetForeignRelSize(PlannerInfo *root,
+ RelOptInfo *baserel,
+ Oid foreigntableid)
+ {
+ StringInfo sql;
+ ForeignTable *table;
+ ForeignServer *server;
+ UserMapping *user;
+ PGconn *conn;
+ double rows;
+ int width;
+ Cost startup_cost;
+ Cost total_cost;
+ PgsqlFdwPlanState *fpstate;
+ Selectivity sel;
+
+ /*
+ * We use PgsqlFdwPlanState to pass various information to subsequent
+ * functions.
+ */
+ fpstate = palloc0(sizeof(PgsqlFdwPlanState));
+ initStringInfo(&fpstate->sql);
+ sql = &fpstate->sql;
+
+ /* Retrieve catalog objects which are necessary to estimate rows. */
+ table = GetForeignTable(foreigntableid);
+ server = GetForeignServer(table->serverid);
+ user = GetUserMapping(GetOuterUserId(), server->serverid);
+
+ /*
+ * Create plain SELECT statement with no WHERE clause for this scan in
+ * order to obtain meaningful rows estimation by executing EXPLAIN on
+ * remote server.
+ */
+ deparseSimpleSql(sql, foreigntableid, root, baserel, table);
+ conn = GetConnection(server, user, false);
+ get_remote_estimate(sql->data, conn, &rows, &width,
+ &startup_cost, &total_cost);
+ ReleaseConnection(conn);
+
+ /*
+ * Estimate selectivity of local filtering by calling
+ * clauselist_selectivity() against baserestrictinfo, and modify rows
+ * estimate with it.
+ */
+ sel = clauselist_selectivity(root, baserel->baserestrictinfo,
+ baserel->relid, JOIN_INNER, NULL);
+ baserel->rows = rows * sel;
+ baserel->width = width;
+
+ /*
+ * Pack obtained information into a object and store it in FDW-private area
+ * of RelOptInfo to pass them to subsequent functions.
+ */
+ fpstate->startup_cost = startup_cost;
+ fpstate->total_cost = total_cost;
+ fpstate->table = table;
+ fpstate->server = server;
+ baserel->fdw_private = (void *) fpstate;
+ }
+
+ /*
+ * pgsqlGetForeignPaths
+ * Create possible scan paths for a scan on the foreign table
+ */
+ static void
+ pgsqlGetForeignPaths(PlannerInfo *root,
+ RelOptInfo *baserel,
+ Oid foreigntableid)
+ {
+ PgsqlFdwPlanState *fpstate = (PgsqlFdwPlanState *) baserel->fdw_private;
+ ForeignPath *path;
+ Cost startup_cost;
+ Cost total_cost;
+ List *fdw_private;
+
+ /*
+ * We have cost values which are estimated on remote side, so use them to
+ * estimate better costs which respect various stuffs to complete the scan,
+ * such as sending query, transferring result, and local filtering.
+ *
+ * XXX We assume that remote cost factors are same as local, but it might
+ * be worth to make configurable.
+ */
+ startup_cost = fpstate->startup_cost;
+ total_cost = fpstate->total_cost;
+ adjust_costs(baserel->rows, baserel->width, &startup_cost, &total_cost);
+
+ /* Construct list of SQL statements and bind it with the path. */
+ fdw_private = lappend(NIL, makeString(fpstate->sql.data));
+
+ /*
+ * Create simplest ForeignScan path node and add it to baserel. This path
+ * corresponds to SeqScan path of regular tables.
+ */
+ path = create_foreignscan_path(root, baserel,
+ baserel->rows,
+ startup_cost,
+ total_cost,
+ NIL, /* no pathkeys */
+ NULL, /* no outer rel either */
+ NIL, /* no param clause */
+ fdw_private);
+ add_path(baserel, (Path *) path);
+
+ /*
+ * XXX We can consider sorted path or parameterized path here if we know
+ * that foreign table is indexed on remote end. For this purpose, we
+ * might have to support FOREIGN INDEX to represent possible sets of sort
+ * keys and/or filtering.
+ */
+ }
+
+ /*
+ * pgsqlGetForeignPlan
+ * Create ForeignScan plan node which implements selected best path
+ */
+ static ForeignScan *
+ pgsqlGetForeignPlan(PlannerInfo *root,
+ RelOptInfo *baserel,
+ Oid foreigntableid,
+ ForeignPath *best_path,
+ List *tlist,
+ List *scan_clauses)
+ {
+ PgsqlFdwPlanState *fpstate = (PgsqlFdwPlanState *) baserel->fdw_private;
+ Index scan_relid = baserel->relid;
+ List *fdw_private = NIL;
+ char name[128]; /* must be larger than format + 10 */
+ StringInfoData cursor;
+ int fetch_count;
+ char *sql;
+
+ /*
+ * We have no native ability to evaluate restriction clauses, so we just
+ * put all the scan_clauses into the plan node's qual list for the
+ * executor to check. So all we have to do here is strip RestrictInfo
+ * nodes from the clauses and ignore pseudoconstants (which will be
+ * handled elsewhere).
+ */
+ scan_clauses = extract_actual_clauses(scan_clauses, false);
+ fetch_count = GetFetchCountOption(fpstate->table, fpstate->server);
+ elog(DEBUG1, "relid=%u fetch_count=%d", foreigntableid, fetch_count);
+
+ /*
+ * Construct cursor name with sequential value.
+ */
+ sprintf(name, CURSOR_NAME_FORMAT, cursor_id++);
+
+ /*
+ * Construct CURSOR statements from plain remote query, and make a list
+ * contains all of them to pass them to executor with plan node for later
+ * use.
+ */
+ sql = fpstate->sql.data;
+ fdw_private = lappend(fdw_private, makeString(sql));
+
+ initStringInfo(&cursor);
+ appendStringInfo(&cursor, "DECLARE %s SCROLL CURSOR FOR %s", name, sql);
+ fdw_private = lappend(fdw_private, makeString(cursor.data));
+
+ initStringInfo(&cursor);
+ appendStringInfo(&cursor, "FETCH %d FROM %s", fetch_count, name);
+ fdw_private = lappend(fdw_private, makeString(cursor.data));
+
+ initStringInfo(&cursor);
+ appendStringInfo(&cursor, "MOVE ABSOLUTE 0 FROM %s", name);
+ fdw_private = lappend(fdw_private, makeString(cursor.data));
+
+ initStringInfo(&cursor);
+ appendStringInfo(&cursor, "CLOSE %s", name);
+ fdw_private = lappend(fdw_private, makeString(cursor.data));
+
+ /* Create the ForeignScan node with fdw_private of selected path. */
+ return make_foreignscan(tlist,
+ scan_clauses,
+ scan_relid,
+ NIL,
+ fdw_private);
+ }
+
+ /*
+ * pgsqlExplainForeignScan
+ * Produce extra output for EXPLAIN
+ */
+ static void
+ pgsqlExplainForeignScan(ForeignScanState *node, ExplainState *es)
+ {
+ List *fdw_private;
+ char *sql;
+
+ /* CURSOR declaration is shown in only VERBOSE mode. */
+ fdw_private = ((ForeignScan *) node->ss.ps.plan)->fdw_private;
+ if (es->verbose)
+ sql = strVal(list_nth(fdw_private, FdwPrivateDeclareSql));
+ else
+ sql = strVal(list_nth(fdw_private, FdwPrivateSelectSql));
+ ExplainPropertyText("Remote SQL", sql, es);
+ }
+
+ /*
+ * pgsqlBeginForeignScan
+ * Initiate access to a foreign PostgreSQL table.
+ */
+ static void
+ pgsqlBeginForeignScan(ForeignScanState *node, int eflags)
+ {
+ PgsqlFdwExecutionState *festate;
+ PGconn *conn;
+ Oid relid;
+ ForeignTable *table;
+ ForeignServer *server;
+ UserMapping *user;
+ TupleTableSlot *slot = node->ss.ss_ScanTupleSlot;
+
+ /*
+ * Do nothing in EXPLAIN (no ANALYZE) case. node->fdw_state stays NULL.
+ */
+ if (eflags & EXEC_FLAG_EXPLAIN_ONLY)
+ return;
+
+ /*
+ * Save state in node->fdw_state.
+ */
+ festate = (PgsqlFdwExecutionState *) palloc(sizeof(PgsqlFdwExecutionState));
+ festate->fdw_private = ((ForeignScan *) node->ss.ps.plan)->fdw_private;
+
+ /*
+ * Create context for per-scan tuplestore under per-query context.
+ */
+ festate->scan_cxt = AllocSetContextCreate(node->ss.ps.state->es_query_cxt,
+ "pgsql_fdw",
+ ALLOCSET_DEFAULT_MINSIZE,
+ ALLOCSET_DEFAULT_INITSIZE,
+ ALLOCSET_DEFAULT_MAXSIZE);
+
+ /*
+ * Get connection to the foreign server. Connection manager would
+ * establish new connection if necessary.
+ */
+ relid = RelationGetRelid(node->ss.ss_currentRelation);
+ table = GetForeignTable(relid);
+ server = GetForeignServer(table->serverid);
+ user = GetUserMapping(GetOuterUserId(), server->serverid);
+ conn = GetConnection(server, user, true);
+ festate->conn = conn;
+
+ /* Result will be filled in first Iterate call. */
+ festate->tuples = NULL;
+
+ /* Allocate buffers for column values. */
+ {
+ TupleDesc tupdesc = slot->tts_tupleDescriptor;
+ festate->values = palloc(sizeof(Datum) * tupdesc->natts);
+ festate->nulls = palloc(sizeof(bool) * tupdesc->natts);
+ festate->attinmeta = TupleDescGetAttInMetadata(tupdesc);
+ }
+
+ /* Allocate buffers for query parameters. */
+ {
+ ParamListInfo params = node->ss.ps.state->es_param_list_info;
+ int numParams = params ? params->numParams : 0;
+
+ if (numParams > 0)
+ {
+ festate->param_types = palloc0(sizeof(Oid) * numParams);
+ festate->param_values = palloc0(sizeof(char *) * numParams);
+ }
+ else
+ {
+ festate->param_types = NULL;
+ festate->param_values = NULL;
+ }
+ }
+
+ /* Remember which foreign table we are scanning. */
+ festate->relid = relid;
+
+ /* Store FDW-specific state into ForeignScanState */
+ node->fdw_state = (void *) festate;
+
+ return;
+ }
+
+ /*
+ * pgsqlIterateForeignScan
+ * Retrieve next row from the result set, or clear tuple slot to indicate
+ * EOF.
+ *
+ * Note that using per-query context when retrieving tuples from
+ * tuplestore to ensure that returned tuples can survive until next
+ * iteration because the tuple is released implicitly via ExecClearTuple.
+ * Retrieving a tuple from tuplestore in CurrentMemoryContext (it's a
+ * per-tuple context), ExecClearTuple will free dangling pointer.
+ */
+ static TupleTableSlot *
+ pgsqlIterateForeignScan(ForeignScanState *node)
+ {
+ PgsqlFdwExecutionState *festate;
+ TupleTableSlot *slot = node->ss.ss_ScanTupleSlot;
+ PGresult *res = NULL;
+ MemoryContext oldcontext = CurrentMemoryContext;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+
+ /*
+ * If this is the first call after Begin, we need to execute remote query.
+ *
+ * Since we needs cursor to prevent out of memory, we declare a cursor at
+ * the first call and fetch from it in later calls.
+ */
+ if (festate->tuples == NULL)
+ execute_query(node);
+
+ /*
+ * If enough tuples are left in tuplestore, just return next tuple from it.
+ *
+ * It is necessary to switch to per-scan context to make returned tuple
+ * valid until next IterateForeignScan call, because it will be released
+ * with ExecClearTuple then. Otherwise, picked tuple is allocated in
+ * per-tuple context, and double-free of that tuple might happen.
+ */
+ MemoryContextSwitchTo(festate->scan_cxt);
+ if (tuplestore_gettupleslot(festate->tuples, true, false, slot))
+ {
+ MemoryContextSwitchTo(oldcontext);
+ return slot;
+ }
+ MemoryContextSwitchTo(oldcontext);
+
+ /*
+ * Here we need to clear partial result and fetch next bunch of tuples from
+ * from the cursor for the scan. If the fetch returns no tuple, the scan
+ * has reached the end.
+ *
+ * PGresult must be released before leaving this function.
+ */
+ PG_TRY();
+ {
+ res = fetch_result(node);
+ store_result(node, res);
+ PQclear(res);
+ res = NULL;
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+
+ /*
+ * If we got more tuples from the server cursor, return next tuple from
+ * tuplestore.
+ */
+ MemoryContextSwitchTo(festate->scan_cxt);
+ if (tuplestore_gettupleslot(festate->tuples, true, false, slot))
+ {
+ MemoryContextSwitchTo(oldcontext);
+ return slot;
+ }
+ MemoryContextSwitchTo(oldcontext);
+
+ /* We don't have any result even in remote server cursor. */
+ ExecClearTuple(slot);
+ return slot;
+ }
+
+ /*
+ * pgsqlReScanForeignScan
+ * - Restart this scan by resetting fetch location.
+ */
+ static void
+ pgsqlReScanForeignScan(ForeignScanState *node)
+ {
+ List *fdw_private;
+ char *sql;
+ PGconn *conn;
+ PGresult *res = NULL;
+ PgsqlFdwExecutionState *festate;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+
+ /* If we have not opened cursor yet, nothing to do. */
+ if (festate->tuples == NULL)
+ return;
+
+ /* Discard fetch results if any. */
+ tuplestore_clear(festate->tuples);
+
+ /* PGresult must be released before leaving this function. */
+ PG_TRY();
+ {
+ /* Reset cursor */
+ fdw_private = festate->fdw_private;
+ conn = festate->conn;
+ sql = strVal(list_nth(fdw_private, FdwPrivateResetSql));
+ res = PQexec(conn, sql);
+ if (PQresultStatus(res) != PGRES_COMMAND_OK)
+ {
+ ereport(ERROR,
+ (errmsg("could not rewind cursor"),
+ errdetail("%s", PQerrorMessage(conn)),
+ errhint("%s", sql)));
+ }
+ PQclear(res);
+ res = NULL;
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+ }
+
+ /*
+ * pgsqlEndForeignScan
+ * Finish scanning foreign table and dispose objects used for this scan
+ */
+ static void
+ pgsqlEndForeignScan(ForeignScanState *node)
+ {
+ List *fdw_private;
+ char *sql;
+ PGconn *conn;
+ PGresult *res = NULL;
+ PgsqlFdwExecutionState *festate;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+
+ /* if festate is NULL, we are in EXPLAIN; nothing to do */
+ if (festate == NULL)
+ return;
+
+ /* If we have not opened cursor yet, nothing to do. */
+ if (festate->tuples == NULL)
+ return;
+
+ /* Discard fetch results */
+ tuplestore_end(festate->tuples);
+ festate->tuples = NULL;
+
+ /* PGresult must be released before leaving this function. */
+ PG_TRY();
+ {
+ /* Close cursor */
+ fdw_private = festate->fdw_private;
+ conn = festate->conn;
+ sql = strVal(list_nth(fdw_private, FdwPrivateCloseSql));
+ res = PQexec(conn, sql);
+ if (PQresultStatus(res) != PGRES_COMMAND_OK)
+ {
+ ereport(ERROR,
+ (errmsg("could not close cursor"),
+ errdetail("%s", PQerrorMessage(conn)),
+ errhint("%s", sql)));
+ }
+ PQclear(res);
+ res = NULL;
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+
+ ReleaseConnection(festate->conn);
+
+ MemoryContextDelete(festate->scan_cxt);
+ festate->scan_cxt = NULL;
+ }
+
+ /*
+ * Estimate costs of executing given SQL statement.
+ */
+ static void
+ get_remote_estimate(const char *sql, PGconn *conn,
+ double *rows, int *width,
+ Cost *startup_cost, Cost *total_cost)
+ {
+ PGresult *res = NULL;
+ StringInfoData buf;
+ char *plan;
+ char *p;
+ int n;
+
+ /*
+ * Construct EXPLAIN statement with given SQL statement.
+ */
+ initStringInfo(&buf);
+ appendStringInfo(&buf, "EXPLAIN %s", sql);
+
+ /* PGresult must be released before leaving this function. */
+ PG_TRY();
+ {
+ res = PQexec(conn, buf.data);
+ if (PQresultStatus(res) != PGRES_TUPLES_OK || PQntuples(res) == 0)
+ {
+ char *msg;
+
+ msg = pstrdup(PQerrorMessage(conn));
+ ereport(ERROR,
+ (errmsg("could not execute EXPLAIN for cost estimation"),
+ errdetail("%s", msg),
+ errhint("%s", sql)));
+ }
+
+ /*
+ * Find estimation portion from top plan node. Here we search opening
+ * parentheses from the end of the line to avoid finding unexpected
+ * parentheses.
+ */
+ plan = PQgetvalue(res, 0, 0);
+ p = strrchr(plan, '(');
+ if (p == NULL)
+ elog(ERROR, "wrong EXPLAIN output: %s", plan);
+ n = sscanf(p,
+ "(cost=%lf..%lf rows=%lf width=%d)",
+ startup_cost, total_cost, rows, width);
+ if (n != 4)
+ elog(ERROR, "could not get estimation from EXPLAIN output");
+
+ PQclear(res);
+ res = NULL;
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+ }
+
+ /*
+ * Adjust costs estimated on remote end with some overheads such as connection
+ * and data transfer.
+ */
+ static void
+ adjust_costs(double rows, int width, Cost *startup_cost, Cost *total_cost)
+ {
+ /*
+ * TODO Selectivity of quals which are NOT pushed down should be also
+ * considered.
+ */
+
+ /* add cost to establish connection. */
+ *startup_cost += CONNECTION_COSTS;
+ *total_cost += CONNECTION_COSTS;
+
+ /* add cost to transfer result. */
+ *total_cost += TRANSFER_COSTS_PER_BYTE * width * rows;
+ *total_cost += cpu_tuple_cost * rows;
+ }
+
+ /*
+ * Execute remote query with current parameters.
+ */
+ static void
+ execute_query(ForeignScanState *node)
+ {
+ List *fdw_private;
+ PgsqlFdwExecutionState *festate;
+ ParamListInfo params = node->ss.ps.state->es_param_list_info;
+ int numParams = params ? params->numParams : 0;
+ Oid *types = NULL;
+ const char **values = NULL;
+ char *sql;
+ PGconn *conn;
+ PGresult *res = NULL;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+ types = festate->param_types;
+ values = festate->param_values;
+
+ /*
+ * Construct parameter array in text format. We don't release memory for
+ * the arrays explicitly, because the memory usage would not be very large,
+ * and anyway they will be released in context cleanup.
+ */
+ if (numParams > 0)
+ {
+ int i;
+
+ 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;
+
+ getTypeOutputInfo(types[i], &out_func_oid, &isvarlena);
+ fmgr_info(out_func_oid, &func);
+ values[i] = OutputFunctionCall(&func, params->params[i].value);
+ }
+ }
+ }
+
+ /* PGresult must be released before leaving this function. */
+ PG_TRY();
+ {
+ /*
+ * Execute remote query with parameters.
+ */
+ conn = festate->conn;
+ fdw_private = ((ForeignScan *) node->ss.ps.plan)->fdw_private;
+ sql = strVal(list_nth(fdw_private, FdwPrivateDeclareSql));
+ res = PQexecParams(conn, sql, numParams, types, values, NULL, NULL, 0);
+
+ /*
+ * If the query has failed, reporting details is enough here.
+ * Connection(s) which are used by this query (at least used by
+ * pgsql_fdw) will be cleaned up by the foreign connection manager.
+ */
+ if (PQresultStatus(res) != PGRES_COMMAND_OK)
+ {
+ ereport(ERROR,
+ (errmsg("could not declare cursor"),
+ errdetail("%s", PQerrorMessage(conn)),
+ errhint("%s", sql)));
+ }
+
+ /* Discard result of DECLARE statement. */
+ PQclear(res);
+ res = NULL;
+
+ /* Fetch first bunch of the result and store them into tuplestore. */
+ res = fetch_result(node);
+ store_result(node, res);
+ PQclear(res);
+ res = NULL;
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+ }
+
+ /*
+ * Fetch next partial result from remote server.
+ *
+ * Once this function has returned result records as PGresult, caller is
+ * responsible to release it, so caller should put codes which might throw
+ * exception in PG_TRY block. When an exception has been caught, release
+ * PGresult and re-throw the exception in PG_CATCH block.
+ */
+ static PGresult *
+ fetch_result(ForeignScanState *node)
+ {
+ PgsqlFdwExecutionState *festate;
+ List *fdw_private;
+ char *sql;
+ PGconn *conn;
+ PGresult *res;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+
+ /* Retrieve information for fetching result. */
+ fdw_private = festate->fdw_private;
+ sql = strVal(list_nth(fdw_private, FdwPrivateFetchSql));
+ conn = festate->conn;
+
+ /*
+ * Fetch result from remote server. In error case, we must release
+ * PGresult in this function to avoid memory leak because caller can't
+ * get the reference.
+ */
+ res = PQexec(conn, sql);
+ if (PQresultStatus(res) != PGRES_TUPLES_OK)
+ {
+ PQclear(res);
+ ereport(ERROR,
+ (errmsg("could not fetch rows from foreign server"),
+ errdetail("%s", PQerrorMessage(conn)),
+ errhint("%s", sql)));
+ }
+
+ return res;
+ }
+
+ /*
+ * Create tuples from PGresult and store them into tuplestore.
+ *
+ * Caller must use PG_TRY block to catch exception and release PGresult
+ * surely.
+ */
+ static void
+ store_result(ForeignScanState *node, PGresult *res)
+ {
+ int rows;
+ int row;
+ int i;
+ int nfields;
+ int attnum; /* number of non-dropped columns */
+ Form_pg_attribute *attrs;
+ TupleTableSlot *slot = node->ss.ss_ScanTupleSlot;
+ TupleDesc tupdesc = slot->tts_tupleDescriptor;
+ PgsqlFdwExecutionState *festate;
+ AttInMetadata *attinmeta;
+ ErrorContextCallback errcontext;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+ rows = PQntuples(res);
+ nfields = PQnfields(res);
+ attrs = tupdesc->attrs;
+ attinmeta = festate->attinmeta;
+
+ /* First, ensure that the tuplestore is empty. */
+ if (festate->tuples == NULL)
+ {
+ MemoryContext oldcontext = CurrentMemoryContext;
+
+ /*
+ * Create tuplestore to store result of the query in per-query context.
+ * Note that we use this memory context to avoid memory leak in error
+ * cases.
+ */
+ MemoryContextSwitchTo(festate->scan_cxt);
+ festate->tuples = tuplestore_begin_heap(false, false, work_mem);
+ MemoryContextSwitchTo(oldcontext);
+ }
+ else
+ {
+ /* We already have tuplestore, just need to clear contents of it. */
+ tuplestore_clear(festate->tuples);
+ }
+
+ /* 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)));
+
+ /*
+ * Set up callback to identify error column. We don't set callback right
+ * row because it should be set only during column value conversion.
+ */
+ errcontext.callback = pgsql_fdw_error_callback;
+ errcontext.arg = (void *) festate;
+
+ /* put a tuples into the slot */
+ for (row = 0; row < rows; row++)
+ {
+ int j;
+ HeapTuple tuple;
+
+ /* Install callback function for error. */
+ errcontext.previous = error_context_stack;
+ error_context_stack = &errcontext;
+
+ for (i = 0, j = 0; i < tupdesc->natts; i++)
+ {
+ /* skip dropped columns. */
+ if (attrs[i]->attisdropped)
+ {
+ festate->nulls[i] = true;
+ continue;
+ }
+
+ /*
+ * Set NULL indicator, and convert text representation to internal
+ * representation if any.
+ */
+ if (PQgetisnull(res, row, j))
+ festate->nulls[i] = true;
+ else
+ {
+ Datum value;
+
+ festate->cur_attno = i + 1; /* first attribute has index 1 */
+ festate->nulls[i] = false;
+ value = InputFunctionCall(&attinmeta->attinfuncs[i],
+ PQgetvalue(res, row, j),
+ attinmeta->attioparams[i],
+ attinmeta->atttypmods[i]);
+ festate->values[i] = value;
+ }
+ j++;
+ }
+
+ /* Uninstall error callback function. */
+ error_context_stack = errcontext.previous;
+
+ /*
+ * 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 = heap_form_tuple(tupdesc, festate->values, festate->nulls);
+ tuplestore_puttuple(festate->tuples, tuple);
+ }
+
+ tuplestore_donestoring(festate->tuples);
+ }
+
+ /*
+ * Callback function which is called when error occurs during column value
+ * conversion. Print names of column and relation.
+ */
+ static void
+ pgsql_fdw_error_callback(void *arg)
+ {
+ PgsqlFdwExecutionState *festate = (PgsqlFdwExecutionState *) arg;
+ const char *relname;
+ const char *colname;
+
+ relname = get_rel_name(festate->relid);
+ colname = get_attname(festate->relid, festate->cur_attno);
+ errcontext("column %s of foreign table %s",
+ quote_identifier(colname), quote_identifier(relname));
+ }
diff --git a/contrib/pgsql_fdw/pgsql_fdw.control b/contrib/pgsql_fdw/pgsql_fdw.control
index ...0a9c8f4 .
*** a/contrib/pgsql_fdw/pgsql_fdw.control
--- b/contrib/pgsql_fdw/pgsql_fdw.control
***************
*** 0 ****
--- 1,5 ----
+ # pgsql_fdw extension
+ comment = 'foreign-data wrapper for remote PostgreSQL servers'
+ default_version = '1.0'
+ module_pathname = '$libdir/pgsql_fdw'
+ relocatable = true
diff --git a/contrib/pgsql_fdw/pgsql_fdw.h b/contrib/pgsql_fdw/pgsql_fdw.h
index ...c67006b .
*** a/contrib/pgsql_fdw/pgsql_fdw.h
--- b/contrib/pgsql_fdw/pgsql_fdw.h
***************
*** 0 ****
--- 1,34 ----
+ /*-------------------------------------------------------------------------
+ *
+ * pgsql_fdw.h
+ * foreign-data wrapper for remote PostgreSQL servers.
+ *
+ * Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/pgsql_fdw.h
+ *
+ *-------------------------------------------------------------------------
+ */
+
+ #ifndef PGSQL_FDW_H
+ #define PGSQL_FDW_H
+
+ #include "postgres.h"
+ #include "foreign/foreign.h"
+ #include "nodes/relation.h"
+
+ /* in option.c */
+ int ExtractConnectionOptions(List *defelems,
+ const char **keywords,
+ const char **values);
+ int GetFetchCountOption(ForeignTable *table, ForeignServer *server);
+
+ /* in deparse.c */
+ void deparseSimpleSql(StringInfo buf,
+ Oid relid,
+ PlannerInfo *root,
+ RelOptInfo *baserel,
+ ForeignTable *table);
+
+ #endif /* PGSQL_FDW_H */
diff --git a/contrib/pgsql_fdw/sql/pgsql_fdw.sql b/contrib/pgsql_fdw/sql/pgsql_fdw.sql
index ...905cc2e .
*** a/contrib/pgsql_fdw/sql/pgsql_fdw.sql
--- b/contrib/pgsql_fdw/sql/pgsql_fdw.sql
***************
*** 0 ****
--- 1,255 ----
+ -- ===================================================================
+ -- create FDW objects
+ -- ===================================================================
+
+ -- Clean up in case a prior regression run failed
+
+ -- Suppress NOTICE messages when roles don't exist
+ SET client_min_messages TO 'error';
+
+ DROP ROLE IF EXISTS pgsql_fdw_user;
+
+ RESET client_min_messages;
+
+ CREATE ROLE pgsql_fdw_user LOGIN SUPERUSER;
+ SET SESSION AUTHORIZATION 'pgsql_fdw_user';
+
+ CREATE EXTENSION pgsql_fdw;
+
+ CREATE SERVER loopback1 FOREIGN DATA WRAPPER pgsql_fdw;
+ CREATE SERVER loopback2 FOREIGN DATA WRAPPER pgsql_fdw
+ OPTIONS (dbname 'contrib_regression');
+
+ CREATE USER MAPPING FOR public SERVER loopback1
+ OPTIONS (user 'value', password 'value');
+ CREATE USER MAPPING FOR pgsql_fdw_user SERVER loopback2;
+
+ CREATE FOREIGN TABLE ft1 (
+ c0 int,
+ c1 int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10)
+ ) SERVER loopback2;
+ ALTER FOREIGN TABLE ft1 DROP COLUMN c0;
+
+ CREATE FOREIGN TABLE ft2 (
+ c0 int,
+ c1 int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10)
+ ) SERVER loopback2;
+ ALTER FOREIGN TABLE ft2 DROP COLUMN c0;
+
+ -- ===================================================================
+ -- create objects used through FDW
+ -- ===================================================================
+ CREATE SCHEMA "S 1";
+ CREATE TABLE "S 1"."T 1" (
+ "C 1" int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10),
+ CONSTRAINT t1_pkey PRIMARY KEY ("C 1")
+ );
+ CREATE TABLE "S 1"."T 2" (
+ c1 int NOT NULL,
+ c2 text,
+ CONSTRAINT t2_pkey PRIMARY KEY (c1)
+ );
+
+ BEGIN;
+ TRUNCATE "S 1"."T 1";
+ INSERT INTO "S 1"."T 1"
+ SELECT id,
+ id % 10,
+ to_char(id, 'FM00000'),
+ '1970-01-01'::timestamptz + ((id % 100) || ' days')::interval,
+ '1970-01-01'::timestamp + ((id % 100) || ' days')::interval,
+ id % 10,
+ id % 10
+ FROM generate_series(1, 1000) id;
+ TRUNCATE "S 1"."T 2";
+ INSERT INTO "S 1"."T 2"
+ SELECT id,
+ 'AAA' || to_char(id, 'FM000')
+ FROM generate_series(1, 100) id;
+ COMMIT;
+
+ -- ===================================================================
+ -- tests for pgsql_fdw_validator
+ -- ===================================================================
+ ALTER FOREIGN DATA WRAPPER pgsql_fdw OPTIONS (host 'value'); -- ERROR
+ -- requiressl, krbsrvname and gsslib are omitted because they depend on
+ -- configure option
+ ALTER SERVER loopback1 OPTIONS (
+ authtype 'value',
+ service 'value',
+ connect_timeout 'value',
+ dbname 'value',
+ host 'value',
+ hostaddr 'value',
+ port 'value',
+ --client_encoding 'value',
+ tty 'value',
+ options 'value',
+ application_name 'value',
+ --fallback_application_name 'value',
+ keepalives 'value',
+ keepalives_idle 'value',
+ keepalives_interval 'value',
+ -- requiressl 'value',
+ sslcompression 'value',
+ sslmode 'value',
+ sslcert 'value',
+ sslkey 'value',
+ sslrootcert 'value',
+ sslcrl 'value'
+ --requirepeer 'value',
+ -- krbsrvname 'value',
+ -- gsslib 'value',
+ --replication 'value'
+ );
+ ALTER SERVER loopback1 OPTIONS (user 'value'); -- ERROR
+ ALTER SERVER loopback2 OPTIONS (ADD fetch_count '2');
+ ALTER USER MAPPING FOR public SERVER loopback1
+ OPTIONS (DROP user, DROP password);
+ ALTER USER MAPPING FOR public SERVER loopback1
+ OPTIONS (host 'value'); -- ERROR
+ ALTER FOREIGN TABLE ft1 OPTIONS (nspname 'S 1', relname 'T 1');
+ ALTER FOREIGN TABLE ft2 OPTIONS (nspname 'S 1', relname 'T 1', fetch_count '100');
+ ALTER FOREIGN TABLE ft1 OPTIONS (invalid 'value'); -- ERROR
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count 'a'); -- ERROR
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count '0'); -- ERROR
+ ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count '-1'); -- ERROR
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (invalid 'value'); -- ERROR
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (colname 'C 1');
+ ALTER FOREIGN TABLE ft2 ALTER COLUMN c1 OPTIONS (colname 'C 1');
+ \dew+
+ \des+
+ \deu+
+ \det+
+
+ -- ===================================================================
+ -- simple queries
+ -- ===================================================================
+ -- single table, with/without alias
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
+ SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+ SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+ -- with WHERE clause
+ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
+ SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
+ -- 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
+ SELECT * FROM ft1 t1 WHERE t1.c3 = (SELECT MAX(c3) FROM ft2 t2) ORDER BY c1;
+ -- used in CTE
+ WITH t1 AS (SELECT * FROM ft1 WHERE c1 <= 10) SELECT t2.c1, t2.c2, t2.c3, t2.c4 FROM t1, ft2 t2 WHERE t1.c1 = t2.c1 ORDER BY t1.c1;
+ -- fixed values
+ SELECT 'fixed', NULL FROM ft1 t1 WHERE c1 = 1;
+ -- user-defined operator/function
+ CREATE FUNCTION pgsql_fdw_abs(int) RETURNS int AS $$
+ BEGIN
+ RETURN abs($1);
+ END
+ $$ LANGUAGE plpgsql IMMUTABLE;
+ CREATE OPERATOR === (
+ LEFTARG = int,
+ RIGHTARG = int,
+ PROCEDURE = int4eq,
+ COMMUTATOR = ===,
+ NEGATOR = !==
+ );
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = pgsql_fdw_abs(t1.c2);
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 === t1.c2;
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = abs(t1.c2);
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = t1.c2;
+
+ -- ===================================================================
+ -- parameterized queries
+ -- ===================================================================
+ -- simple join
+ PREPARE st1(int, int) AS SELECT t1.c3, t2.c3 FROM ft1 t1, ft2 t2 WHERE t1.c1 = $1 AND t2.c1 = $2;
+ EXPLAIN (COSTS false) EXECUTE st1(1, 2);
+ EXECUTE st1(1, 1);
+ EXECUTE st1(101, 101);
+ -- subquery using stable function (can't be pushed down)
+ PREPARE st2(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c4) = 6) ORDER BY c1;
+ EXPLAIN (COSTS false) EXECUTE st2(10, 20);
+ EXECUTE st2(10, 20);
+ EXECUTE st1(101, 101);
+ -- subquery using immutable function (can be pushed down)
+ PREPARE st3(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c5) = 6) ORDER BY c1;
+ EXPLAIN (COSTS false) EXECUTE st3(10, 20);
+ EXECUTE st3(10, 20);
+ EXECUTE st3(20, 30);
+ -- custom plan should be chosen
+ PREPARE st4(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 = $1;
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ -- cleanup
+ DEALLOCATE st1;
+ DEALLOCATE st2;
+ DEALLOCATE st3;
+ DEALLOCATE st4;
+
+ -- ===================================================================
+ -- connection management
+ -- ===================================================================
+ SELECT srvname, usename FROM pgsql_fdw_connections;
+ SELECT pgsql_fdw_disconnect(srvid, usesysid) FROM pgsql_fdw_get_connections();
+ SELECT srvname, usename FROM pgsql_fdw_connections;
+
+ -- ===================================================================
+ -- conversion error
+ -- ===================================================================
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c4 TYPE int;
+ SELECT * FROM ft1 WHERE c1 = 1; -- ERROR
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c4 TYPE timestamptz;
+
+ -- ===================================================================
+ -- subtransaction
+ -- ===================================================================
+ BEGIN;
+ DECLARE c CURSOR FOR SELECT * FROM ft1 ORDER BY c1;
+ FETCH c;
+ SAVEPOINT s;
+ ERROR OUT; -- ERROR
+ ROLLBACK TO s;
+ SELECT srvname FROM pgsql_fdw_connections;
+ FETCH c;
+ COMMIT;
+ SELECT srvname FROM pgsql_fdw_connections;
+ ERROR OUT; -- ERROR
+ SELECT srvname FROM pgsql_fdw_connections;
+
+ -- ===================================================================
+ -- cleanup
+ -- ===================================================================
+ DROP OPERATOR === (int, int) CASCADE;
+ DROP OPERATOR !== (int, int) CASCADE;
+ DROP FUNCTION pgsql_fdw_abs(int);
+ DROP SCHEMA "S 1" CASCADE;
+ DROP EXTENSION pgsql_fdw CASCADE;
+ \c
+ DROP ROLE pgsql_fdw_user;
diff --git a/doc/src/sgml/contrib.sgml b/doc/src/sgml/contrib.sgml
index 97031dd..c4dd9c3 100644
*** a/doc/src/sgml/contrib.sgml
--- b/doc/src/sgml/contrib.sgml
*************** CREATE EXTENSION <replaceable>module_nam
*** 117,122 ****
--- 117,123 ----
&pgcrypto;
&pgfreespacemap;
&pgrowlocks;
+ &pgsql-fdw;
&pgstandby;
&pgstatstatements;
&pgstattuple;
diff --git a/doc/src/sgml/filelist.sgml b/doc/src/sgml/filelist.sgml
index 428a167..f3ff180 100644
*** a/doc/src/sgml/filelist.sgml
--- b/doc/src/sgml/filelist.sgml
***************
*** 125,130 ****
--- 125,131 ----
<!ENTITY pgcrypto SYSTEM "pgcrypto.sgml">
<!ENTITY pgfreespacemap SYSTEM "pgfreespacemap.sgml">
<!ENTITY pgrowlocks SYSTEM "pgrowlocks.sgml">
+ <!ENTITY pgsql-fdw SYSTEM "pgsql-fdw.sgml">
<!ENTITY pgstandby SYSTEM "pgstandby.sgml">
<!ENTITY pgstatstatements SYSTEM "pgstatstatements.sgml">
<!ENTITY pgstattuple SYSTEM "pgstattuple.sgml">
diff --git a/doc/src/sgml/pgsql-fdw.sgml b/doc/src/sgml/pgsql-fdw.sgml
index ...eb69f01 .
*** a/doc/src/sgml/pgsql-fdw.sgml
--- b/doc/src/sgml/pgsql-fdw.sgml
***************
*** 0 ****
--- 1,265 ----
+ <!-- doc/src/sgml/pgsql-fdw.sgml -->
+
+ <sect1 id="pgsql-fdw" xreflabel="pgsql_fdw">
+ <title>pgsql_fdw</title>
+
+ <indexterm zone="pgsql-fdw">
+ <primary>pgsql_fdw</primary>
+ </indexterm>
+
+ <para>
+ The <filename>pgsql_fdw</filename> module provides a foreign-data wrapper for
+ external <productname>PostgreSQL</productname> servers.
+ With this module, users can access data stored in external
+ <productname>PostgreSQL</productname> via plain SQL statements.
+ </para>
+
+ <para>
+ Note that default wrapper <literal>pgsql_fdw</literal> is created
+ automatically during <command>CREATE EXTENSION</command> command for
+ <application>pgsql_fdw</application>.
+ </para>
+
+ <sect2>
+ <title>FDW Options of pgsql_fdw</title>
+
+ <sect3>
+ <title>Connection Options</title>
+ <para>
+ A foreign server and user mapping created using this wrapper can have
+ <application>libpq</> connection options, expect below:
+
+ <itemizedlist>
+ <listitem><para>client_encoding</para></listitem>
+ <listitem><para>fallback_application_name</para></listitem>
+ <listitem><para>replication</para></listitem>
+ </itemizedlist>
+
+ For details of <application>libpq</> connection options, see
+ <xref linkend="libpq-connect">.
+ </para>
+
+ <para>
+ <literal>user</literal> and <literal>password</literal> can be
+ specified on user mappings, and others can be specified on foreign servers.
+ </para>
+ </sect3>
+
+ <sect3>
+ <title>Object Name Options</title>
+ <para>
+ Foreign tables which were created using this wrapper, or its columns can
+ have object name options. These options can be used to specify the names
+ used in SQL statement sent to remote <productname>PostgreSQL</productname>
+ server. These options are useful when a remote object has different name
+ from corresponding local one.
+ </para>
+
+ <variablelist>
+
+ <varlistentry>
+ <term><literal>nspname</literal></term>
+ <listitem>
+ <para>
+ This option, which can be specified on a foreign table, is used as a
+ namespace (schema) reference in the SQL statement. If this options is
+ omitted, <literal>pg_class.nspname</literal> of the foreign table is
+ used.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>relname</literal></term>
+ <listitem>
+ <para>
+ This option, which can be specified on a foreign table, is used as a
+ relation (table) reference in the SQL statement. If this options is
+ omitted, <literal>pg_class.relname</literal> of the foreign table is
+ used.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>colname</literal></term>
+ <listitem>
+ <para>
+ This option, which can be specified on a column of a foreign table, is
+ used as a column (attribute) reference in the SQL statement. If this
+ option is omitted, <literal>pg_attribute.attname</literal> of the column
+ of the foreign table is used.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ </variablelist>
+
+ </sect3>
+
+ <sect3>
+ <title>Cursor Options</title>
+ <para>
+ The <application>pgsql_fdw</application> always uses cursor to retrieve the
+ result from external server. Users can control the behavior of cursor by
+ setting cursor options to foreign table or foreign server. If an option
+ is set to both objects, finer-grained setting is used. In other words,
+ foreign table's setting overrides foreign server's setting.
+ </para>
+
+ <variablelist>
+
+ <varlistentry>
+ <term><literal>fetch_count</literal></term>
+ <listitem>
+ <para>
+ This option specifies the number of rows to be fetched at a time.
+ This option accepts only integer value larger than zero. The default
+ setting is 10000.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ </variablelist>
+
+ </sect3>
+
+ </sect2>
+
+ <sect2>
+ <title>Connection Management</title>
+
+ <para>
+ The <application>pgsql_fdw</application> establishes a connection to a
+ foreign server in the beginning of the first query which uses a foreign
+ table associated to the foreign server, and reuses the connection following
+ queries and even in following foreign scans in same query.
+
+ You can see the list of active connections via
+ <structname>pgsql_fdw_connections</structname> view. It shows pair of oid
+ and name of server and local role for each active connections established by
+ <application>pgsql_fdw</application>. For security reason, only superuser
+ can see other role's connections.
+ </para>
+
+ <para>
+ Established connections are kept alive until local role changes or the
+ current transaction aborts or user requests so.
+ </para>
+
+ <para>
+ If role has been changed, active connections established as old local role
+ is kept alive but never be reused until local role has restored to original
+ role. This kind of situation happens with <command>SET ROLE</command> and
+ <command>SET SESSION AUTHORIZATION</command>.
+ </para>
+
+ <para>
+ If current transaction aborts by error or user request, all active
+ connections are disconnected automatically. This behavior avoids possible
+ connection leaks on error.
+ </para>
+
+ <para>
+ You can discard persistent connection at arbitrary timing with
+ <function>pgsql_fdw_disconnect()</function>. It takes server oid and
+ user oid as arguments. This function can handle only connections
+ established in current session; connections established by other backends
+ are not reachable.
+ </para>
+
+ <para>
+ You can discard all active and visible connections in current session with
+ using <structname>pgsql_fdw_connections</structname> and
+ <function>pgsql_fdw_disconnect()</function> together:
+ <synopsis>
+ postgres=# SELECT pgsql_fdw_disconnect(srvid, usesysid) FROM pgsql_fdw_connections;
+ pgsql_fdw_disconnect
+ ----------------------
+ OK
+ OK
+ (2 rows)
+ </synopsis>
+ </para>
+ </sect2>
+
+ <sect2>
+ <title>Transaction Management</title>
+ <para>
+ The <application>pgsql_fdw</application> begins remote transaction at the
+ beginning of a local query, and terminates it with <command>ABORT</command>
+ at the end of the local query. This means that all foreign scans on a
+ foreign server in a local query are executed in one transaction.
+ If isolation level of local transaction is <literal>SERIALIZABLE</literal>,
+ <literal>SERIALIZABLE</literal> is used for remote transaction. Otherwise,
+ if isolation level of local transaction is one of
+ <literal>READ UNCOMMITTED</literal>, <literal>READ COMMITTED</literal> or
+ <literal>REPEATABLE READ</literal>, then <literal>REPEATABLE READ</literal>
+ is used for remote transaction.
+ <literal>READ UNCOMMITTED</literal> and <literal>READ COMMITTED</literal>
+ are never used for remote transaction, because even
+ <literal>READ COMMITTED</literal> transaction might produce inconsistent
+ results, if remote data have been updated between two remote queries.
+ </para>
+ <para>
+ Note that even if the isolation level of local transaction was
+ <literal>SERIALIZABLE</literal> or <literal>REPEATABLE READ</literal>,
+ series of one query might produce different result, because foreign scans
+ in different local queries are executed in different remote transactions.
+ For instance, when client started a local transaction
+ explicitly with isolation level <literal>SERIALIZABLE</literal>, and
+ executed same local query which contains a foreign table which references
+ foreign data which is updated frequently, latter result would be different
+ from former result.
+ </para>
+ <para>
+ This restriction might be relaxed in future release.
+ </para>
+ </sect2>
+
+ <sect2>
+ <title>Estimation of Costs and Rows</title>
+ <para>
+ The <application>pgsql_fdw</application> estimates the costs of a foreign
+ scan by adding up some basic costs: connection costs, remote query costs
+ and data transfer costs.
+ To get remote query costs, <application>pgsql_fdw</application> executes
+ <command>EXPLAIN</command> command on remote server for each foreign scan.
+ </para>
+ <para>
+ On the other hand, estimated rows which was returned by
+ <command>EXPLAIN</command> is used for local estimation as-is.
+ </para>
+ </sect2>
+
+ <sect2>
+ <title>EXPLAIN Output</title>
+ <para>
+ For a foreign table using <literal>pgsql_fdw</>, <command>EXPLAIN</> shows
+ a remote SQL statement which is sent to remote
+ <productname>PostgreSQL</productname> server for a ForeignScan plan node.
+ For example:
+ </para>
+ <synopsis>
+ postgres=# EXPLAIN SELECT aid FROM pgbench_accounts WHERE abalance < 0;
+ QUERY PLAN
+ --------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on pgbench_accounts (cost=100.00..8105.13 rows=302613 width=8)
+ Filter: (abalance < 0)
+ Remote SQL: SELECT aid, NULL, abalance, NULL FROM public.pgbench_accounts
+ (3 rows)
+ </synopsis>
+ <para>
+ When you specify <literal>VERBOSE</> option, you can see actual DECLARE
+ statement with cursor name which is used for the scan.
+ </para>
+ </sect2>
+
+ <sect2>
+ <title>Author</title>
+ <para>
+ Shigeru Hanada <email>shigeru.hanada@gmail.com</email>
+ </para>
+ </sect2>
+
+ </sect1>
pgsql_fdw_pushdown_v12.patchtext/plain; charset=Shift_JIS; name=pgsql_fdw_pushdown_v12.patchDownload
diff --git a/contrib/pgsql_fdw/deparse.c b/contrib/pgsql_fdw/deparse.c
index 8e79232..6f8f753 100644
*** a/contrib/pgsql_fdw/deparse.c
--- b/contrib/pgsql_fdw/deparse.c
***************
*** 14,19 ****
--- 14,21 ----
#include "access/transam.h"
#include "catalog/pg_class.h"
+ #include "catalog/pg_operator.h"
+ #include "catalog/pg_type.h"
#include "commands/defrem.h"
#include "foreign/foreign.h"
#include "lib/stringinfo.h"
***************
*** 22,30 ****
--- 24,34 ----
#include "nodes/makefuncs.h"
#include "optimizer/clauses.h"
#include "optimizer/var.h"
+ #include "parser/parser.h"
#include "parser/parsetree.h"
#include "utils/builtins.h"
#include "utils/lsyscache.h"
+ #include "utils/syscache.h"
#include "pgsql_fdw.h"
*************** typedef struct foreign_executable_cxt
*** 35,49 ****
--- 39,77 ----
{
PlannerInfo *root;
RelOptInfo *foreignrel;
+ bool has_param;
} foreign_executable_cxt;
/*
* Get string representation which can be used in SQL statement from a node.
*/
+ static void deparseExpr(StringInfo buf, Expr *expr, PlannerInfo *root);
static void deparseRelation(StringInfo buf, Oid relid, PlannerInfo *root,
bool need_prefix);
static void deparseVar(StringInfo buf, Var *node, PlannerInfo *root,
bool need_prefix);
+ static void deparseConst(StringInfo buf, Const *node, PlannerInfo *root);
+ static void deparseBoolExpr(StringInfo buf, BoolExpr *node, PlannerInfo *root);
+ static void deparseNullTest(StringInfo buf, NullTest *node, PlannerInfo *root);
+ static void deparseDistinctExpr(StringInfo buf, DistinctExpr *node,
+ PlannerInfo *root);
+ static void deparseRelabelType(StringInfo buf, RelabelType *node,
+ PlannerInfo *root);
+ static void deparseFuncExpr(StringInfo buf, FuncExpr *node, PlannerInfo *root);
+ static void deparseParam(StringInfo buf, Param *node, PlannerInfo *root);
+ static void deparseScalarArrayOpExpr(StringInfo buf, ScalarArrayOpExpr *node,
+ PlannerInfo *root);
+ static void deparseOpExpr(StringInfo buf, OpExpr *node, PlannerInfo *root);
+ static void deparseArrayRef(StringInfo buf, ArrayRef *node, PlannerInfo *root);
+ static void deparseArrayExpr(StringInfo buf, ArrayExpr *node, PlannerInfo *root);
+
+ /*
+ * Determine whether an expression can be evaluated on remote side safely.
+ */
+ static bool is_foreign_expr(PlannerInfo *root, RelOptInfo *baserel, Expr *expr,
+ bool *has_param);
+ static bool foreign_expr_walker(Node *node, foreign_executable_cxt *context);
+ static bool is_builtin(Oid procid);
/*
* Deparse query representation into SQL statement which suits for remote
*************** deparseSimpleSql(StringInfo buf,
*** 151,156 ****
--- 179,285 ----
}
/*
+ * Examine each element in the list baserestrictinfo of baserel, and sort them
+ * into three groups: remote_conds contains conditions which can be evaluated
+ * - remote_conds is push-down safe, and don't contain any Param node
+ * - param_conds is push-down safe, but contain some Param node
+ * - local_conds is not push-down safe
+ *
+ * Only remote_conds can be used in remote EXPLAIN, and remote_conds and
+ * param_conds can be used in final remote query.
+ */
+ void
+ sortConditions(PlannerInfo *root,
+ RelOptInfo *baserel,
+ List **remote_conds,
+ List **param_conds,
+ List **local_conds)
+ {
+ ListCell *lc;
+ bool has_param;
+
+ Assert(remote_conds);
+ Assert(param_conds);
+ Assert(local_conds);
+
+ foreach(lc, baserel->baserestrictinfo)
+ {
+ RestrictInfo *ri = (RestrictInfo *) lfirst(lc);
+
+ if (is_foreign_expr(root, baserel, ri->clause, &has_param))
+ {
+ if (has_param)
+ *param_conds = lappend(*param_conds, ri);
+ else
+ *remote_conds = lappend(*remote_conds, ri);
+ }
+ else
+ *local_conds = lappend(*local_conds, ri);
+ }
+ }
+
+ /*
+ * Deparse given expression into buf. Actual string operation is delegated to
+ * node-type-specific functions.
+ *
+ * Note that switch statement of this function MUST match the one in
+ * foreign_expr_walker to avoid unsupported error..
+ */
+ static void
+ deparseExpr(StringInfo buf, Expr *node, PlannerInfo *root)
+ {
+ /*
+ * This part must be match foreign_expr_walker.
+ */
+ switch (nodeTag(node))
+ {
+ case T_Const:
+ deparseConst(buf, (Const *) node, root);
+ break;
+ case T_BoolExpr:
+ deparseBoolExpr(buf, (BoolExpr *) node, root);
+ break;
+ case T_NullTest:
+ deparseNullTest(buf, (NullTest *) node, root);
+ break;
+ case T_DistinctExpr:
+ deparseDistinctExpr(buf, (DistinctExpr *) node, root);
+ break;
+ case T_RelabelType:
+ deparseRelabelType(buf, (RelabelType *) node, root);
+ break;
+ case T_FuncExpr:
+ deparseFuncExpr(buf, (FuncExpr *) node, root);
+ break;
+ case T_Param:
+ deparseParam(buf, (Param *) node, root);
+ break;
+ case T_ScalarArrayOpExpr:
+ deparseScalarArrayOpExpr(buf, (ScalarArrayOpExpr *) node, root);
+ break;
+ case T_OpExpr:
+ deparseOpExpr(buf, (OpExpr *) node, root);
+ break;
+ case T_Var:
+ deparseVar(buf, (Var *) node, root, false);
+ break;
+ case T_ArrayRef:
+ deparseArrayRef(buf, (ArrayRef *) node, root);
+ break;
+ case T_ArrayExpr:
+ deparseArrayExpr(buf, (ArrayExpr *) node, root);
+ break;
+ default:
+ {
+ ereport(ERROR,
+ (errmsg("unsupported expression for deparse"),
+ errdetail("%s", nodeToString(node))));
+ }
+ break;
+ }
+ }
+
+ /*
* Deparse node into buf, with relation qualifier if need_prefix was true. If
* node is a column of a foreign table, use value of colname FDW option (if any)
* instead of attribute name.
*************** deparseRelation(StringInfo buf,
*** 286,288 ****
--- 415,1146 ----
appendStringInfo(buf, "%s.", q_nspname);
appendStringInfo(buf, "%s", q_relname);
}
+
+ /*
+ * Deparse given constant value into buf. This function have to be kept in
+ * sync with get_const_expr.
+ */
+ static void
+ deparseConst(StringInfo buf,
+ Const *node,
+ PlannerInfo *root)
+ {
+ Oid typoutput;
+ bool typIsVarlena;
+ char *extval;
+ bool isfloat = false;
+ bool needlabel;
+
+ if (node->constisnull)
+ {
+ appendStringInfo(buf, "NULL");
+ return;
+ }
+
+ getTypeOutputInfo(node->consttype,
+ &typoutput, &typIsVarlena);
+ extval = OidOutputFunctionCall(typoutput, node->constvalue);
+
+ switch (node->consttype)
+ {
+ case ANYARRAYOID:
+ case ANYNONARRAYOID:
+ elog(ERROR, "anyarray and anyenum are not supported");
+ break;
+ case INT2OID:
+ case INT4OID:
+ case INT8OID:
+ case OIDOID:
+ case FLOAT4OID:
+ case FLOAT8OID:
+ case NUMERICOID:
+ {
+ /*
+ * No need to quote unless they contain special values such as
+ * 'Nan'.
+ */
+ if (strspn(extval, "0123456789+-eE.") == strlen(extval))
+ {
+ if (extval[0] == '+' || extval[0] == '-')
+ appendStringInfo(buf, "(%s)", extval);
+ else
+ appendStringInfoString(buf, extval);
+ if (strcspn(extval, "eE.") != strlen(extval))
+ isfloat = true; /* it looks like a float */
+ }
+ else
+ appendStringInfo(buf, "'%s'", extval);
+ }
+ break;
+ case BITOID:
+ case VARBITOID:
+ appendStringInfo(buf, "B'%s'", extval);
+ break;
+ case BOOLOID:
+ if (strcmp(extval, "t") == 0)
+ appendStringInfoString(buf, "true");
+ else
+ appendStringInfoString(buf, "false");
+ break;
+
+ default:
+ {
+ const char *valptr;
+
+ appendStringInfoChar(buf, '\'');
+ for (valptr = extval; *valptr; valptr++)
+ {
+ char ch = *valptr;
+
+ /*
+ * standard_conforming_strings of remote session should be
+ * set to similar value as local session.
+ */
+ if (SQL_STR_DOUBLE(ch, !standard_conforming_strings))
+ appendStringInfoChar(buf, ch);
+ appendStringInfoChar(buf, ch);
+ }
+ appendStringInfoChar(buf, '\'');
+ }
+ break;
+ }
+
+ /*
+ * Append ::typename unless the constant will be implicitly typed as the
+ * right type when it is read in.
+ *
+ * XXX this code has to be kept in sync with the behavior of the parser,
+ * especially make_const.
+ */
+ switch (node->consttype)
+ {
+ case BOOLOID:
+ case INT4OID:
+ case UNKNOWNOID:
+ needlabel = false;
+ break;
+ case NUMERICOID:
+ needlabel = !isfloat || (node->consttypmod >= 0);
+ break;
+ default:
+ needlabel = true;
+ break;
+ }
+ if (needlabel)
+ {
+ appendStringInfo(buf, "::%s",
+ format_type_with_typemod(node->consttype,
+ node->consttypmod));
+ }
+ }
+
+ static void
+ deparseBoolExpr(StringInfo buf,
+ BoolExpr *node,
+ PlannerInfo *root)
+ {
+ ListCell *lc;
+ char *op;
+ bool first;
+
+ switch (node->boolop)
+ {
+ case AND_EXPR:
+ op = "AND";
+ break;
+ case OR_EXPR:
+ op = "OR";
+ break;
+ case NOT_EXPR:
+ appendStringInfo(buf, "(NOT ");
+ deparseExpr(buf, list_nth(node->args, 0), root);
+ appendStringInfo(buf, ")");
+ return;
+ }
+
+ first = true;
+ appendStringInfo(buf, "(");
+ foreach(lc, node->args)
+ {
+ if (!first)
+ appendStringInfo(buf, " %s ", op);
+ deparseExpr(buf, (Expr *) lfirst(lc), root);
+ first = false;
+ }
+ appendStringInfo(buf, ")");
+ }
+
+ /*
+ * Deparse given IS [NOT] NULL test expression into buf.
+ */
+ static void
+ deparseNullTest(StringInfo buf,
+ NullTest *node,
+ PlannerInfo *root)
+ {
+ appendStringInfoChar(buf, '(');
+ deparseExpr(buf, node->arg, root);
+ if (node->nulltesttype == IS_NULL)
+ appendStringInfo(buf, " IS NULL)");
+ else
+ appendStringInfo(buf, " IS NOT NULL)");
+ }
+
+ static void
+ deparseDistinctExpr(StringInfo buf,
+ DistinctExpr *node,
+ PlannerInfo *root)
+ {
+ Assert(list_length(node->args) == 2);
+
+ deparseExpr(buf, linitial(node->args), root);
+ appendStringInfo(buf, " IS DISTINCT FROM ");
+ deparseExpr(buf, lsecond(node->args), root);
+ }
+
+ static void
+ deparseRelabelType(StringInfo buf,
+ RelabelType *node,
+ PlannerInfo *root)
+ {
+ char *typname;
+
+ Assert(node->arg);
+
+ /* We don't need to deparse cast when argument has same type as result. */
+ if (IsA(node->arg, Const) &&
+ ((Const *) node->arg)->consttype == node->resulttype &&
+ ((Const *) node->arg)->consttypmod == -1)
+ {
+ deparseExpr(buf, node->arg, root);
+ return;
+ }
+
+ typname = format_type_with_typemod(node->resulttype, node->resulttypmod);
+ appendStringInfoChar(buf, '(');
+ deparseExpr(buf, node->arg, root);
+ appendStringInfo(buf, ")::%s", typname);
+ }
+
+ /*
+ * Deparse given node which represents a function call into buf. We treat only
+ * explicit function call and explicit cast (coerce), because others are
+ * processed on remote side if necessary.
+ *
+ * Function name (and type name) is always qualified by schema name to avoid
+ * problems caused by different setting of search_path on remote side.
+ */
+ static void
+ deparseFuncExpr(StringInfo buf,
+ FuncExpr *node,
+ PlannerInfo *root)
+ {
+ Oid pronamespace;
+ const char *schemaname;
+ const char *funcname;
+ ListCell *arg;
+ bool first;
+
+ pronamespace = get_func_namespace(node->funcid);
+ schemaname = quote_identifier(get_namespace_name(pronamespace));
+ funcname = quote_identifier(get_func_name(node->funcid));
+
+ if (node->funcformat == COERCE_EXPLICIT_CALL)
+ {
+ /* Function call, deparse all arguments recursively. */
+ appendStringInfo(buf, "%s.%s(", schemaname, funcname);
+ first = true;
+ foreach(arg, node->args)
+ {
+ if (!first)
+ appendStringInfo(buf, ", ");
+ deparseExpr(buf, lfirst(arg), root);
+ first = false;
+ }
+ appendStringInfoChar(buf, ')');
+ }
+ else if (node->funcformat == COERCE_EXPLICIT_CAST)
+ {
+ /* Explicit cast, deparse only first argument. */
+ appendStringInfoChar(buf, '(');
+ deparseExpr(buf, linitial(node->args), root);
+ appendStringInfo(buf, ")::%s", funcname);
+ }
+ else
+ {
+ /* Implicit cast, deparse only first argument. */
+ deparseExpr(buf, linitial(node->args), root);
+ }
+ }
+
+ /*
+ * Deparse given Param node into buf.
+ *
+ * We don't renumber parameter id, because skipping $1 is not cause problem
+ * as far as we pass through all arguments.
+ */
+ static void
+ deparseParam(StringInfo buf,
+ Param *node,
+ PlannerInfo *root)
+ {
+ Assert(node->paramkind == PARAM_EXTERN);
+
+ appendStringInfo(buf, "$%d", node->paramid);
+ }
+
+ /*
+ * Deparse given ScalarArrayOpExpr expression into buf. To avoid problems
+ * around priority of operations, we always parenthesize the arguments. Also we
+ * use OPERATOR(schema.operator) notation to determine remote operator exactly.
+ */
+ static void
+ deparseScalarArrayOpExpr(StringInfo buf,
+ ScalarArrayOpExpr *node,
+ PlannerInfo *root)
+ {
+ HeapTuple tuple;
+ Form_pg_operator form;
+ const char *opnspname;
+ char *opname;
+ Expr *arg1;
+ Expr *arg2;
+
+ /* Retrieve necessary information about the operator from system catalog. */
+ tuple = SearchSysCache1(OPEROID, ObjectIdGetDatum(node->opno));
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "cache lookup failed for operator %u", node->opno);
+ form = (Form_pg_operator) GETSTRUCT(tuple);
+ /* opname is not a SQL identifier, so we don't need to quote it. */
+ opname = NameStr(form->oprname);
+ opnspname = quote_identifier(get_namespace_name(form->oprnamespace));
+ ReleaseSysCache(tuple);
+
+ /* Sanity check. */
+ Assert(list_length(node->args) == 2);
+
+ /* Always parenthesize the expression. */
+ appendStringInfoChar(buf, '(');
+
+ /* Extract operands. */
+ arg1 = linitial(node->args);
+ arg2 = lsecond(node->args);
+
+ /* Deparse fully qualified operator name. */
+ deparseExpr(buf, arg1, root);
+ appendStringInfo(buf, " OPERATOR(%s.%s) %s (",
+ opnspname, opname, node->useOr ? "ANY" : "ALL");
+ deparseExpr(buf, arg2, root);
+ appendStringInfoChar(buf, ')');
+
+ /* Always parenthesize the expression. */
+ appendStringInfoChar(buf, ')');
+ }
+
+ /*
+ * Deparse given operator expression into buf. To avoid problems around
+ * priority of operations, we always parenthesize the arguments. Also we use
+ * OPERATOR(schema.operator) notation to determine remote operator exactly.
+ */
+ static void
+ deparseOpExpr(StringInfo buf,
+ OpExpr *node,
+ PlannerInfo *root)
+ {
+ HeapTuple tuple;
+ Form_pg_operator form;
+ const char *opnspname;
+ char *opname;
+ char oprkind;
+ ListCell *arg;
+
+ /* Retrieve necessary information about the operator from system catalog. */
+ tuple = SearchSysCache1(OPEROID, ObjectIdGetDatum(node->opno));
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "cache lookup failed for operator %u", node->opno);
+ form = (Form_pg_operator) GETSTRUCT(tuple);
+ opnspname = quote_identifier(get_namespace_name(form->oprnamespace));
+ /* opname is not a SQL identifier, so we don't need to quote it. */
+ opname = NameStr(form->oprname);
+ oprkind = form->oprkind;
+ ReleaseSysCache(tuple);
+
+ /* Sanity check. */
+ Assert((oprkind == 'r' && list_length(node->args) == 1) ||
+ (oprkind == 'l' && list_length(node->args) == 1) ||
+ (oprkind == 'b' && list_length(node->args) == 2));
+
+ /* Always parenthesize the expression. */
+ appendStringInfoChar(buf, '(');
+
+ /* Deparse first operand. */
+ arg = list_head(node->args);
+ if (oprkind == 'r' || oprkind == 'b')
+ {
+ deparseExpr(buf, lfirst(arg), root);
+ appendStringInfoChar(buf, ' ');
+ }
+
+ /* Deparse fully qualified operator name. */
+ appendStringInfo(buf, "OPERATOR(%s.%s)", opnspname, opname);
+
+ /* Deparse last operand. */
+ arg = list_tail(node->args);
+ if (oprkind == 'l' || oprkind == 'b')
+ {
+ appendStringInfoChar(buf, ' ');
+ deparseExpr(buf, lfirst(arg), root);
+ }
+
+ appendStringInfoChar(buf, ')');
+ }
+
+ static void
+ deparseArrayRef(StringInfo buf,
+ ArrayRef *node,
+ PlannerInfo *root)
+ {
+ ListCell *lowlist_item;
+ ListCell *uplist_item;
+
+ /* Always parenthesize the expression. */
+ appendStringInfoChar(buf, '(');
+
+ /* Deparse referenced array expression first. */
+ appendStringInfoChar(buf, '(');
+ deparseExpr(buf, node->refexpr, root);
+ appendStringInfoChar(buf, ')');
+
+ /* Deparse subscripts expression. */
+ lowlist_item = list_head(node->reflowerindexpr); /* could be NULL */
+ foreach(uplist_item, node->refupperindexpr)
+ {
+ appendStringInfoChar(buf, '[');
+ if (lowlist_item)
+ {
+ deparseExpr(buf, lfirst(lowlist_item), root);
+ appendStringInfoChar(buf, ':');
+ lowlist_item = lnext(lowlist_item);
+ }
+ deparseExpr(buf, lfirst(uplist_item), root);
+ appendStringInfoChar(buf, ']');
+ }
+
+ appendStringInfoChar(buf, ')');
+ }
+
+
+ /*
+ * Deparse given array of something into buf.
+ */
+ static void
+ deparseArrayExpr(StringInfo buf,
+ ArrayExpr *node,
+ PlannerInfo *root)
+ {
+ ListCell *lc;
+ bool first = true;
+
+ appendStringInfo(buf, "ARRAY[");
+ foreach(lc, node->elements)
+ {
+ if (!first)
+ appendStringInfo(buf, ", ");
+ deparseExpr(buf, lfirst(lc), root);
+
+ first = false;
+ }
+ appendStringInfoChar(buf, ']');
+
+ /* If the array is empty, we need explicit cast to the array type. */
+ if (node->elements == NIL)
+ {
+ char *typname;
+
+ typname = format_type_with_typemod(node->array_typeid, -1);
+ appendStringInfo(buf, "::%s", typname);
+ }
+ }
+
+ /*
+ * Returns true if given expr is safe to evaluate on the foreign server. If
+ * result is true, extra information has_param tells whether given expression
+ * contains any Param node. This is useful to determine whether the expression
+ * can be used in remote EXPLAIN.
+ */
+ static bool
+ is_foreign_expr(PlannerInfo *root,
+ RelOptInfo *baserel,
+ Expr *expr,
+ bool *has_param)
+ {
+ foreign_executable_cxt context;
+ context.root = root;
+ context.foreignrel = baserel;
+ context.has_param = false;
+
+ /*
+ * An expression which includes any mutable function can't be pushed down
+ * because it's result is not stable. For example, pushing now() down to
+ * remote side would cause confusion from the clock offset.
+ * If we have routine mapping infrastructure in future release, we will be
+ * able to choose function to be pushed down in finer granularity.
+ */
+ if (contain_mutable_functions((Node *) expr))
+ return false;
+
+ /*
+ * Check that the expression consists of nodes which are known as safe to
+ * be pushed down.
+ */
+ if (foreign_expr_walker((Node *) expr, &context))
+ return false;
+
+ /*
+ * Tell caller whether the given expression contains any Param node, which
+ * can't be used in EXPLAIN statement before executor starts.
+ */
+ *has_param = context.has_param;
+
+ return true;
+ }
+
+ /*
+ * Return true if node includes any node which is not known as safe to be
+ * pushed down.
+ */
+ static bool
+ foreign_expr_walker(Node *node, foreign_executable_cxt *context)
+ {
+ if (node == NULL)
+ return false;
+
+ /*
+ * Special case handling for List; expression_tree_walker handles List as
+ * well as other Expr nodes. For instance, List is used in RestrictInfo
+ * for args of FuncExpr node.
+ *
+ * Although the comments of expression_tree_walker mention that
+ * RangeTblRef, FromExpr, JoinExpr, and SetOperationStmt are handled as
+ * well, but we don't care them because they are not used in RestrictInfo.
+ * If one of them was passed into, default label catches it and give up
+ * traversing.
+ */
+ if (IsA(node, List))
+ {
+ ListCell *lc;
+
+ foreach(lc, (List *) node)
+ {
+ if (foreign_expr_walker(lfirst(lc), context))
+ return true;
+ }
+ return false;
+ }
+
+ /*
+ * If return type of given expression is not built-in, it can't be pushed
+ * down because it might has incompatible semantics on remote side.
+ */
+ if (!is_builtin(exprType(node)))
+ return true;
+
+ switch (nodeTag(node))
+ {
+ case T_Const:
+ /*
+ * Using anyarray and/or anyenum in remote query is not supported.
+ */
+ if (((Const *) node)->consttype == ANYARRAYOID ||
+ ((Const *) node)->consttype == ANYNONARRAYOID)
+ return true;
+ break;
+ case T_BoolExpr:
+ case T_NullTest:
+ case T_DistinctExpr:
+ case T_RelabelType:
+ /*
+ * 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 at the tail of this function.
+ */
+ break;
+ /*
+ * If function used by the expression is not built-in, it can't be
+ * pushed down because it might has incompatible semantics on remote
+ * side.
+ */
+ case T_FuncExpr:
+ {
+ FuncExpr *fe = (FuncExpr *) node;
+ if (!is_builtin(fe->funcid))
+ return true;
+ }
+ break;
+ case T_Param:
+ /*
+ * Only external parameters can be pushed down.:
+ */
+ {
+ if (((Param *) node)->paramkind != PARAM_EXTERN)
+ return true;
+
+ /* Mark that this expression contains Param node. */
+ context->has_param = true;
+ }
+ break;
+ case T_ScalarArrayOpExpr:
+ /*
+ * Only built-in operators can be pushed down. In addition,
+ * underlying function must be built-in and immutable, but we don't
+ * check volatility here; such check must be done already with
+ * contain_mutable_functions.
+ */
+ {
+ ScalarArrayOpExpr *oe = (ScalarArrayOpExpr *) node;
+
+ if (!is_builtin(oe->opno) || !is_builtin(oe->opfuncid))
+ return true;
+
+ /*
+ * If the operator takes collatable type as operands, we push
+ * down only "=" and "<>" which are not affected by collation.
+ * Other operators might be safe about collation, but these two
+ * seem enogh to cover practical use cases.
+ */
+ if (exprInputCollation(node) != InvalidOid)
+ {
+ char *opname = get_opname(oe->opno);
+
+ if (strcmp(opname, "=") != 0 && strcmp(opname, "<>") != 0)
+ return true;
+ }
+
+ /* operands are checked later */
+ }
+ break;
+ case T_OpExpr:
+ /*
+ * Only built-in operators can be pushed down. In addition,
+ * underlying function must be built-in and immutable, but we don't
+ * check volatility here; such check must be done already with
+ * contain_mutable_functions.
+ */
+ {
+ OpExpr *oe = (OpExpr *) node;
+
+ if (!is_builtin(oe->opno) || !is_builtin(oe->opfuncid))
+ return true;
+
+ /*
+ * If the operator takes collatable type as operands, we push
+ * down only "=" and "<>" which are not affected by collation.
+ * Other operators might be safe about collation, but these two
+ * seem enogh to cover practical use cases.
+ */
+ if (exprInputCollation(node) != InvalidOid)
+ {
+ char *opname = get_opname(oe->opno);
+
+ if (strcmp(opname, "=") != 0 && strcmp(opname, "<>") != 0)
+ return true;
+ }
+
+ /* operands are checked later */
+ }
+ 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;
+ foreign_executable_cxt *f_context;
+
+ f_context = (foreign_executable_cxt *) context;
+ if (var->varno != f_context->foreignrel->relid ||
+ var->varlevelsup != 0)
+ return true;
+ }
+ break;
+ case T_ArrayRef:
+ /*
+ * ArrayRef which holds non-built-in typed elements can't be pushed
+ * down.
+ */
+ {
+ ArrayRef *ar = (ArrayRef *) node;;
+
+ if (!is_builtin(ar->refelemtype))
+ return true;
+
+ /* Assignment should not be in restrictions. */
+ if (ar->refassgnexpr != NULL)
+ return true;
+ }
+ break;
+ case T_ArrayExpr:
+ /*
+ * ArrayExpr which holds non-built-in typed elements can't be pushed
+ * down.
+ */
+ {
+ if (!is_builtin(((ArrayExpr *) node)->element_typeid))
+ return true;
+ }
+ break;
+ default:
+ {
+ ereport(DEBUG3,
+ (errmsg("expression is too complex"),
+ errdetail("%s", nodeToString(node))));
+ return true;
+ }
+ break;
+ }
+
+ return expression_tree_walker(node, foreign_expr_walker, context);
+ }
+
+ /*
+ * Return true if given object is one of built-in objects.
+ */
+ static bool
+ is_builtin(Oid oid)
+ {
+ return (oid < FirstNormalObjectId);
+ }
+
+ /*
+ * Deparse WHERE clause from given list of RestrictInfo and append them to buf.
+ * We assume that buf already holds a SQL statement which ends with valid WHERE
+ * clause.
+ */
+ void
+ appendWhereClause(StringInfo buf,
+ bool has_where,
+ List *exprs,
+ PlannerInfo *root)
+ {
+ bool first = true;
+ ListCell *lc;
+
+ if (!has_where)
+ appendStringInfo(buf, " WHERE ");
+
+ foreach(lc, exprs)
+ {
+ RestrictInfo *ri = (RestrictInfo *) lfirst(lc);
+
+ /* Connect expressions with "AND" and parenthesize whole condition. */
+ if (!first)
+ appendStringInfo(buf, " AND ");
+
+ appendStringInfoChar(buf, '(');
+ deparseExpr(buf, ri->clause, root);
+ appendStringInfoChar(buf, ')');
+
+ first = false;
+ }
+ }
diff --git a/contrib/pgsql_fdw/expected/pgsql_fdw.out b/contrib/pgsql_fdw/expected/pgsql_fdw.out
index 4252cc0..8e50614 100644
*** a/contrib/pgsql_fdw/expected/pgsql_fdw.out
--- b/contrib/pgsql_fdw/expected/pgsql_fdw.out
*************** SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.
*** 229,244 ****
(10 rows)
-- with WHERE clause
! EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
! QUERY PLAN
! ------------------------------------------------------------------------------------------------------------------
Foreign Scan on public.ft1 t1
Output: c1, c2, c3, c4, c5, c6, c7
! Filter: ((t1.c1 = 101) AND ((t1.c6)::text = '1'::text) AND (t1.c7 = '1'::bpchar))
! Remote SQL: DECLARE pgsql_fdw_cursor_4 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
(4 rows)
! SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
c1 | c2 | c3 | c4 | c5 | c6 | c7
-----+----+-------+------------------------------+--------------------------+----+------------
101 | 1 | 00101 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970 | 1 | 1
--- 229,244 ----
(10 rows)
-- with WHERE clause
! EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 >= '1';
! QUERY PLAN
! -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Foreign Scan on public.ft1 t1
Output: c1, c2, c3, c4, c5, c6, c7
! Filter: (t1.c7 >= '1'::bpchar)
! Remote SQL: DECLARE pgsql_fdw_cursor_4 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.=) 101)) AND (((c6)::text OPERATOR(pg_catalog.=) '1'::text))
(4 rows)
! SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 >= '1';
c1 | c2 | c3 | c4 | c5 | c6 | c7
-----+----+-------+------------------------------+--------------------------+----+------------
101 | 1 | 00101 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970 | 1 | 1
*************** EXPLAIN (COSTS false) SELECT * FROM ft1
*** 343,362 ****
(3 rows)
EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = abs(t1.c2);
! QUERY PLAN
! ---------------------------------------------------------------------
Foreign Scan on ft1 t1
! Filter: (c1 = abs(c2))
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
! (3 rows)
EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = t1.c2;
! QUERY PLAN
! ---------------------------------------------------------------------
Foreign Scan on ft1 t1
! Filter: (c1 = c2)
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
! (3 rows)
-- ===================================================================
-- parameterized queries
--- 343,433 ----
(3 rows)
EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = abs(t1.c2);
! QUERY PLAN
! -------------------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.=) pg_catalog.abs(c2)))
! (2 rows)
EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = t1.c2;
! QUERY PLAN
! ---------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.=) c2))
! (2 rows)
!
! -- ===================================================================
! -- WHERE push down
! -- ===================================================================
! EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = 1; -- Var, OpExpr(b), Const
! QUERY PLAN
! --------------------------------------------------------------------------------------------------------------
! Foreign Scan on ft1 t1
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.=) 1))
! (2 rows)
!
! EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = 100 AND t1.c2 = 0; -- BoolExpr
! QUERY PLAN
! ----------------------------------------------------------------------------------------------------------------------------------------------------
! Foreign Scan on ft1 t1
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.=) 100)) AND ((c2 OPERATOR(pg_catalog.=) 0))
! (2 rows)
!
! EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE c1 IS NULL; -- NullTest
! QUERY PLAN
! ---------------------------------------------------------------------------------------------
! Foreign Scan on ft1 t1
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" IS NULL))
! (2 rows)
!
! EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE c1 IS NOT NULL; -- NullTest
! QUERY PLAN
! -------------------------------------------------------------------------------------------------
! Foreign Scan on ft1 t1
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" IS NOT NULL))
! (2 rows)
!
! EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE round(abs(c1), 0) = 1; -- FuncExpr
! QUERY PLAN
! ------------------------------------------------------------------------------------------------------------------------------------------------------------
! Foreign Scan on ft1 t1
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE ((pg_catalog.round(pg_catalog.abs("C 1"), 0) OPERATOR(pg_catalog.=) 1::numeric))
! (2 rows)
!
! EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE c1 = -c1; -- OpExpr(l)
! QUERY PLAN
! -------------------------------------------------------------------------------------------------------------------------------------------
! Foreign Scan on ft1 t1
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.=) (OPERATOR(pg_catalog.-) "C 1")))
! (2 rows)
!
! EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE 1 = c1!; -- OpExpr(r)
! QUERY PLAN
! ------------------------------------------------------------------------------------------------------------------------------------------------
! Foreign Scan on ft1 t1
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE ((1::numeric OPERATOR(pg_catalog.=) ("C 1" OPERATOR(pg_catalog.!))))
! (2 rows)
!
! EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE (c1 IS NOT NULL) IS DISTINCT FROM (c1 IS NOT NULL); -- DistinctExpr
! QUERY PLAN
! --------------------------------------------------------------------------------------------------------------------------------------
! Foreign Scan on ft1 t1
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" IS NOT NULL) IS DISTINCT FROM ("C 1" IS NOT NULL))
! (2 rows)
!
! EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE c1 = ANY(ARRAY[c2, 1, c1 + 0]); -- ScalarArrayOpExpr
! QUERY PLAN
! -----------------------------------------------------------------------------------------------------------------------------------------------------------------
! Foreign Scan on ft1 t1
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.=) ANY (ARRAY[c2, 1, ("C 1" OPERATOR(pg_catalog.+) 0)])))
! (2 rows)
!
! EXPLAIN (COSTS false) SELECT * FROM ft1 ft WHERE c1 = (ARRAY[c1,c2,3])[1]; -- ArrayRef
! QUERY PLAN
! ---------------------------------------------------------------------------------------------------------------------------------------
! Foreign Scan on ft1 ft
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.=) ((ARRAY["C 1", c2, 3])[1])))
! (2 rows)
-- ===================================================================
-- parameterized queries
*************** EXPLAIN (COSTS false) SELECT * FROM ft1
*** 364,380 ****
-- simple join
PREPARE st1(int, int) AS SELECT t1.c3, t2.c3 FROM ft1 t1, ft2 t2 WHERE t1.c1 = $1 AND t2.c1 = $2;
EXPLAIN (COSTS false) EXECUTE st1(1, 2);
! QUERY PLAN
! -------------------------------------------------------------------------------------------
Nested Loop
-> Foreign Scan on ft1 t1
! Filter: (c1 = 1)
! Remote SQL: SELECT "C 1", NULL, c3, NULL, NULL, NULL, NULL FROM "S 1"."T 1"
! -> Materialize
! -> Foreign Scan on ft2 t2
! Filter: (c1 = 2)
! Remote SQL: SELECT "C 1", NULL, c3, NULL, NULL, NULL, NULL FROM "S 1"."T 1"
! (8 rows)
EXECUTE st1(1, 1);
c3 | c3
--- 435,448 ----
-- simple join
PREPARE st1(int, int) AS SELECT t1.c3, t2.c3 FROM ft1 t1, ft2 t2 WHERE t1.c1 = $1 AND t2.c1 = $2;
EXPLAIN (COSTS false) EXECUTE st1(1, 2);
! QUERY PLAN
! ------------------------------------------------------------------------------------------------------------------------------
Nested Loop
-> Foreign Scan on ft1 t1
! Remote SQL: SELECT "C 1", NULL, c3, NULL, NULL, NULL, NULL FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.=) 1))
! -> Foreign Scan on ft2 t2
! Remote SQL: SELECT "C 1", NULL, c3, NULL, NULL, NULL, NULL FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.=) 2))
! (5 rows)
EXECUTE st1(1, 1);
c3 | c3
*************** EXECUTE st1(101, 101);
*** 391,411 ****
-- subquery using stable function (can't be pushed down)
PREPARE st2(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c4) = 6) ORDER BY c1;
EXPLAIN (COSTS false) EXECUTE st2(10, 20);
! QUERY PLAN
! ------------------------------------------------------------------------------------------------------
Sort
Sort Key: t1.c1
-> Hash Join
Hash Cond: (t1.c3 = t2.c3)
-> Foreign Scan on ft1 t1
! Filter: (c1 < 20)
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
-> Hash
-> HashAggregate
-> Foreign Scan on ft2 t2
! Filter: ((c1 > 10) AND (date_part('dow'::text, c4) = 6::double precision))
! Remote SQL: SELECT "C 1", NULL, c3, c4, NULL, NULL, NULL FROM "S 1"."T 1"
! (12 rows)
EXECUTE st2(10, 20);
c1 | c2 | c3 | c4 | c5 | c6 | c7
--- 459,478 ----
-- subquery using stable function (can't be pushed down)
PREPARE st2(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c4) = 6) ORDER BY c1;
EXPLAIN (COSTS false) EXECUTE st2(10, 20);
! QUERY PLAN
! -----------------------------------------------------------------------------------------------------------------------------------------------
Sort
Sort Key: t1.c1
-> Hash Join
Hash Cond: (t1.c3 = t2.c3)
-> Foreign Scan on ft1 t1
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.<) 20))
-> Hash
-> HashAggregate
-> Foreign Scan on ft2 t2
! Filter: (date_part('dow'::text, c4) = 6::double precision)
! Remote SQL: SELECT "C 1", NULL, c3, c4, NULL, NULL, NULL FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.>) 10))
! (11 rows)
EXECUTE st2(10, 20);
c1 | c2 | c3 | c4 | c5 | c6 | c7
*************** EXECUTE st1(101, 101);
*** 422,442 ****
-- subquery using immutable function (can be pushed down)
PREPARE st3(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c5) = 6) ORDER BY c1;
EXPLAIN (COSTS false) EXECUTE st3(10, 20);
! QUERY PLAN
! ------------------------------------------------------------------------------------------------------
Sort
Sort Key: t1.c1
! -> Hash Join
! Hash Cond: (t1.c3 = t2.c3)
-> Foreign Scan on ft1 t1
! Filter: (c1 < 20)
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
! -> Hash
! -> HashAggregate
! -> Foreign Scan on ft2 t2
! Filter: ((c1 > 10) AND (date_part('dow'::text, c5) = 6::double precision))
! Remote SQL: SELECT "C 1", NULL, c3, NULL, c5, NULL, NULL FROM "S 1"."T 1"
! (12 rows)
EXECUTE st3(10, 20);
c1 | c2 | c3 | c4 | c5 | c6 | c7
--- 489,506 ----
-- subquery using immutable function (can be pushed down)
PREPARE st3(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c5) = 6) ORDER BY c1;
EXPLAIN (COSTS false) EXECUTE st3(10, 20);
! QUERY PLAN
! ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Sort
Sort Key: t1.c1
! -> Nested Loop Semi Join
! Join Filter: (t1.c3 = t2.c3)
-> Foreign Scan on ft1 t1
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.<) 20))
! -> Materialize
! -> Foreign Scan on ft2 t2
! Remote SQL: SELECT "C 1", NULL, c3, NULL, c5, NULL, NULL FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.>) 10)) AND ((pg_catalog.date_part('dow'::text, c5) OPERATOR(pg_catalog.=) 6::double precision))
! (9 rows)
EXECUTE st3(10, 20);
c1 | c2 | c3 | c4 | c5 | c6 | c7
*************** EXECUTE st3(20, 30);
*** 453,504 ****
-- custom plan should be chosen
PREPARE st4(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 = $1;
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! ---------------------------------------------------------------------
Foreign Scan on ft1 t1
! Filter: (c1 = 1)
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
! (3 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! ---------------------------------------------------------------------
Foreign Scan on ft1 t1
! Filter: (c1 = 1)
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
! (3 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! ---------------------------------------------------------------------
Foreign Scan on ft1 t1
! Filter: (c1 = 1)
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
! (3 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! ---------------------------------------------------------------------
Foreign Scan on ft1 t1
! Filter: (c1 = 1)
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
! (3 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! ---------------------------------------------------------------------
Foreign Scan on ft1 t1
! Filter: (c1 = 1)
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
! (3 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! ---------------------------------------------------------------------
Foreign Scan on ft1 t1
! Filter: (c1 = $1)
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
! (3 rows)
-- cleanup
DEALLOCATE st1;
--- 517,562 ----
-- custom plan should be chosen
PREPARE st4(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 = $1;
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! --------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.=) 1))
! (2 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! --------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.=) 1))
! (2 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! --------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.=) 1))
! (2 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! --------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.=) 1))
! (2 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! --------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.=) 1))
! (2 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! ---------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.=) $1))
! (2 rows)
-- cleanup
DEALLOCATE st1;
diff --git a/contrib/pgsql_fdw/pgsql_fdw.c b/contrib/pgsql_fdw/pgsql_fdw.c
index 17693e0..8975955 100644
*** a/contrib/pgsql_fdw/pgsql_fdw.c
--- b/contrib/pgsql_fdw/pgsql_fdw.c
***************
*** 17,23 ****
#include "catalog/pg_foreign_table.h"
#include "commands/defrem.h"
#include "commands/explain.h"
- #include "foreign/fdwapi.h"
#include "funcapi.h"
#include "miscadmin.h"
#include "optimizer/cost.h"
--- 17,22 ----
*************** PG_MODULE_MAGIC;
*** 55,60 ****
--- 54,64 ----
#define CURSOR_NAME_FORMAT "pgsql_fdw_cursor_%u"
static uint32 cursor_id = 0;
+ /* Convenient macros for accessing the first record of PGresult. */
+ #define PGRES_VAL0(col) (PQgetvalue(res, 0, (col)))
+ #define PGRES_NULL0(col) (PQgetisnull(res, 0, (col)))
+
+
/*
* FDW-specific information for RelOptInfo.fdw_private. This is used to pass
* information from pgsqlGetForeignRelSize to pgsqlGetForeignPaths.
*************** typedef struct PgsqlFdwPlanState {
*** 65,72 ****
--- 69,80 ----
* GetForeignPaths.
*/
StringInfoData sql;
+ bool has_where;
Cost startup_cost;
Cost total_cost;
+ List *remote_conds;
+ List *param_conds;
+ List *local_conds;
/* Cached catalog information. */
ForeignTable *table;
*************** pgsqlGetForeignRelSize(PlannerInfo *root
*** 244,249 ****
--- 252,258 ----
RelOptInfo *baserel,
Oid foreigntableid)
{
+ PgsqlFdwPlanState *fpstate;
StringInfo sql;
ForeignTable *table;
ForeignServer *server;
*************** pgsqlGetForeignRelSize(PlannerInfo *root
*** 253,259 ****
int width;
Cost startup_cost;
Cost total_cost;
! PgsqlFdwPlanState *fpstate;
Selectivity sel;
/*
--- 262,270 ----
int width;
Cost startup_cost;
Cost total_cost;
! List *remote_conds = NIL;
! List *param_conds = NIL;
! List *local_conds = NIL;
Selectivity sel;
/*
*************** pgsqlGetForeignRelSize(PlannerInfo *root
*** 263,268 ****
--- 274,280 ----
fpstate = palloc0(sizeof(PgsqlFdwPlanState));
initStringInfo(&fpstate->sql);
sql = &fpstate->sql;
+ fpstate->has_where = false;
/* Retrieve catalog objects which are necessary to estimate rows. */
table = GetForeignTable(foreigntableid);
*************** pgsqlGetForeignRelSize(PlannerInfo *root
*** 270,292 ****
user = GetUserMapping(GetOuterUserId(), server->serverid);
/*
! * Create plain SELECT statement with no WHERE clause for this scan in
! * order to obtain meaningful rows estimation by executing EXPLAIN on
! * remote server.
*/
deparseSimpleSql(sql, foreigntableid, root, baserel, table);
conn = GetConnection(server, user, false);
get_remote_estimate(sql->data, conn, &rows, &width,
&startup_cost, &total_cost);
ReleaseConnection(conn);
/*
! * Estimate selectivity of local filtering by calling
! * clauselist_selectivity() against baserestrictinfo, and modify rows
! * estimate with it.
*/
! sel = clauselist_selectivity(root, baserel->baserestrictinfo,
! baserel->relid, JOIN_INNER, NULL);
baserel->rows = rows * sel;
baserel->width = width;
--- 282,321 ----
user = GetUserMapping(GetOuterUserId(), server->serverid);
/*
! * Construct remote query which consists of SELECT, FROM, and WHERE
! * clauses, but conditions contain any Param node are excluded because
! * placeholder can't be used in EXPLAIN statement. Such conditions are
! * appended later.
*/
+ sortConditions(root, baserel, &remote_conds, ¶m_conds, &local_conds);
deparseSimpleSql(sql, foreigntableid, root, baserel, table);
+ if (list_length(remote_conds) > 0)
+ {
+ appendWhereClause(sql, fpstate->has_where, remote_conds, root);
+ fpstate->has_where = true;
+ }
conn = GetConnection(server, user, false);
get_remote_estimate(sql->data, conn, &rows, &width,
&startup_cost, &total_cost);
ReleaseConnection(conn);
+ if (list_length(param_conds) > 0)
+ {
+ appendWhereClause(sql, fpstate->has_where, param_conds, root);
+ fpstate->has_where = true;
+ }
/*
! * Estimate selectivity of conditions which are not used in remote EXPLAIN
! * by calling clauselist_selectivity(). The best we can do for
! * parameterized condition is to estimate selectivity on the basis of local
! * statistics. When we actually obtain result rows, such conditions are
! * deparsed into remote query and reduce rows transferred.
*/
! sel = 1.0;
! sel *= clauselist_selectivity(root, param_conds,
! baserel->relid, JOIN_INNER, NULL);
! sel *= clauselist_selectivity(root, local_conds,
! baserel->relid, JOIN_INNER, NULL);
baserel->rows = rows * sel;
baserel->width = width;
*************** pgsqlGetForeignRelSize(PlannerInfo *root
*** 296,301 ****
--- 325,333 ----
*/
fpstate->startup_cost = startup_cost;
fpstate->total_cost = total_cost;
+ fpstate->remote_conds = remote_conds;
+ fpstate->param_conds = param_conds;
+ fpstate->local_conds = local_conds;
fpstate->table = table;
fpstate->server = server;
baserel->fdw_private = (void *) fpstate;
*************** pgsqlGetForeignPlan(PlannerInfo *root,
*** 372,392 ****
StringInfoData cursor;
int fetch_count;
char *sql;
/*
! * We have no native ability to evaluate restriction clauses, so we just
! * put all the scan_clauses into the plan node's qual list for the
! * executor to check. So all we have to do here is strip RestrictInfo
! * nodes from the clauses and ignore pseudoconstants (which will be
! * handled elsewhere).
*/
! scan_clauses = extract_actual_clauses(scan_clauses, false);
fetch_count = GetFetchCountOption(fpstate->table, fpstate->server);
elog(DEBUG1, "relid=%u fetch_count=%d", foreigntableid, fetch_count);
! /*
! * Construct cursor name with sequential value.
! */
sprintf(name, CURSOR_NAME_FORMAT, cursor_id++);
/*
--- 404,429 ----
StringInfoData cursor;
int fetch_count;
char *sql;
+ List *fdw_exprs = NIL;
+ List *local_exprs = NIL;
+ ListCell *lc;
/*
! * We need lists of Expr other than the lists of RestrictInfo. Now we can
! * merge remote_conds and param_conds into fdw_exprs, because they are
! * evaluated on remote side for actual remote query.
*/
! foreach(lc, fpstate->remote_conds)
! fdw_exprs = lappend(fdw_exprs, ((RestrictInfo *) lfirst(lc))->clause);
! foreach(lc, fpstate->param_conds)
! fdw_exprs = lappend(fdw_exprs, ((RestrictInfo *) lfirst(lc))->clause);
! foreach(lc, fpstate->local_conds)
! local_exprs = lappend(local_exprs,
! ((RestrictInfo *) lfirst(lc))->clause);
fetch_count = GetFetchCountOption(fpstate->table, fpstate->server);
elog(DEBUG1, "relid=%u fetch_count=%d", foreigntableid, fetch_count);
! /* Construct cursor name from sequential value */
sprintf(name, CURSOR_NAME_FORMAT, cursor_id++);
/*
*************** pgsqlGetForeignPlan(PlannerInfo *root,
*** 413,423 ****
appendStringInfo(&cursor, "CLOSE %s", name);
fdw_private = lappend(fdw_private, makeString(cursor.data));
! /* Create the ForeignScan node with fdw_private of selected path. */
return make_foreignscan(tlist,
! scan_clauses,
scan_relid,
! NIL,
fdw_private);
}
--- 450,469 ----
appendStringInfo(&cursor, "CLOSE %s", name);
fdw_private = lappend(fdw_private, makeString(cursor.data));
! /*
! * Create the ForeignScan node from target list, local filtering
! * expressions, remote filtering expressions, and FDW private information.
! *
! * We remove expressions which are evaluated on remote side from qual of
! * the scan node to avoid redundant filtering. Such filter reduction
! * can be done only here, done after choosing best path, because
! * baserestrictinfo in RelOptInfo is shared by all possible paths until
! * best path is chosen.
! */
return make_foreignscan(tlist,
! local_exprs,
scan_relid,
! fdw_exprs,
fdw_private);
}
diff --git a/contrib/pgsql_fdw/pgsql_fdw.h b/contrib/pgsql_fdw/pgsql_fdw.h
index c67006b..5487d52 100644
*** a/contrib/pgsql_fdw/pgsql_fdw.h
--- b/contrib/pgsql_fdw/pgsql_fdw.h
***************
*** 15,20 ****
--- 15,21 ----
#define PGSQL_FDW_H
#include "postgres.h"
+ #include "foreign/fdwapi.h"
#include "foreign/foreign.h"
#include "nodes/relation.h"
*************** void deparseSimpleSql(StringInfo buf,
*** 30,34 ****
--- 31,44 ----
PlannerInfo *root,
RelOptInfo *baserel,
ForeignTable *table);
+ void appendWhereClause(StringInfo buf,
+ bool has_where,
+ List *exprs,
+ PlannerInfo *root);
+ void sortConditions(PlannerInfo *root,
+ RelOptInfo *baserel,
+ List **remote_conds,
+ List **param_conds,
+ List **local_conds);
#endif /* PGSQL_FDW_H */
diff --git a/contrib/pgsql_fdw/sql/pgsql_fdw.sql b/contrib/pgsql_fdw/sql/pgsql_fdw.sql
index 905cc2e..6692af7 100644
*** a/contrib/pgsql_fdw/sql/pgsql_fdw.sql
--- b/contrib/pgsql_fdw/sql/pgsql_fdw.sql
*************** SELECT * FROM ft1 ORDER BY c3, c1 OFFSET
*** 149,156 ****
EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
-- with WHERE clause
! EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
! SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 = '1';
-- aggregate
SELECT COUNT(*) FROM ft1 t1;
-- join two tables
--- 149,156 ----
EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
-- with WHERE clause
! EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 >= '1';
! SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 >= '1';
-- aggregate
SELECT COUNT(*) FROM ft1 t1;
-- join two tables
*************** EXPLAIN (COSTS false) SELECT * FROM ft1
*** 182,187 ****
--- 182,201 ----
EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = t1.c2;
-- ===================================================================
+ -- WHERE push down
+ -- ===================================================================
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = 1; -- Var, OpExpr(b), Const
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = 100 AND t1.c2 = 0; -- BoolExpr
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE c1 IS NULL; -- NullTest
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE c1 IS NOT NULL; -- NullTest
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE round(abs(c1), 0) = 1; -- FuncExpr
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE c1 = -c1; -- OpExpr(l)
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE 1 = c1!; -- OpExpr(r)
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE (c1 IS NOT NULL) IS DISTINCT FROM (c1 IS NOT NULL); -- DistinctExpr
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE c1 = ANY(ARRAY[c2, 1, c1 + 0]); -- ScalarArrayOpExpr
+ EXPLAIN (COSTS false) SELECT * FROM ft1 ft WHERE c1 = (ARRAY[c1,c2,3])[1]; -- ArrayRef
+
+ -- ===================================================================
-- parameterized queries
-- ===================================================================
-- simple join
pgsql_fdw_analyze.patchtext/plain; charset=Shift_JIS; name=pgsql_fdw_analyze.patchDownload
diff --git a/contrib/file_fdw/file_fdw.c b/contrib/file_fdw/file_fdw.c
index e890770..f84a01f 100644
*** a/contrib/file_fdw/file_fdw.c
--- b/contrib/file_fdw/file_fdw.c
***************
*** 20,25 ****
--- 20,26 ----
#include "commands/copy.h"
#include "commands/defrem.h"
#include "commands/explain.h"
+ #include "commands/vacuum.h"
#include "foreign/fdwapi.h"
#include "foreign/foreign.h"
#include "miscadmin.h"
*************** static void fileBeginForeignScan(Foreign
*** 123,128 ****
--- 124,132 ----
static TupleTableSlot *fileIterateForeignScan(ForeignScanState *node);
static void fileReScanForeignScan(ForeignScanState *node);
static void fileEndForeignScan(ForeignScanState *node);
+ static void fileAnalyzeForeignTable(Relation onerel,
+ VacuumStmt *vacstmt,
+ int elevel);
/*
* Helper functions
*************** static void estimate_size(PlannerInfo *r
*** 136,142 ****
static void estimate_costs(PlannerInfo *root, RelOptInfo *baserel,
FileFdwPlanState *fdw_private,
Cost *startup_cost, Cost *total_cost);
!
/*
* Foreign-data wrapper handler function: return a struct with pointers
--- 140,149 ----
static void estimate_costs(PlannerInfo *root, RelOptInfo *baserel,
FileFdwPlanState *fdw_private,
Cost *startup_cost, Cost *total_cost);
! static int acquire_sample_rows(Relation onerel,
! HeapTuple *rows, int targrows,
! double *totalrows, double *totaldeadrows,
! BlockNumber *totalpages, int elevel);
/*
* Foreign-data wrapper handler function: return a struct with pointers
*************** file_fdw_handler(PG_FUNCTION_ARGS)
*** 155,160 ****
--- 162,168 ----
fdwroutine->IterateForeignScan = fileIterateForeignScan;
fdwroutine->ReScanForeignScan = fileReScanForeignScan;
fdwroutine->EndForeignScan = fileEndForeignScan;
+ fdwroutine->AnalyzeForeignTable = fileAnalyzeForeignTable;
PG_RETURN_POINTER(fdwroutine);
}
*************** estimate_size(PlannerInfo *root, RelOptI
*** 662,693 ****
double nrows;
/*
! * Get size of the file. It might not be there at plan time, though, in
! * which case we have to use a default estimate.
*/
! if (stat(fdw_private->filename, &stat_buf) < 0)
! stat_buf.st_size = 10 * BLCKSZ;
! /*
! * Convert size to pages for use in I/O cost estimate later.
! */
! pages = (stat_buf.st_size + (BLCKSZ - 1)) / BLCKSZ;
! if (pages < 1)
! pages = 1;
! fdw_private->pages = pages;
! /*
! * Estimate the number of tuples in the file. We back into this estimate
! * using the planner's idea of the relation width; which is bogus if not
! * all columns are being read, not to mention that the text representation
! * of a row probably isn't the same size as its internal representation.
! * FIXME later.
! */
! tuple_width = MAXALIGN(baserel->width) + MAXALIGN(sizeof(HeapTupleHeaderData));
! ntuples = clamp_row_est((double) stat_buf.st_size / (double) tuple_width);
fdw_private->ntuples = ntuples;
/*
--- 670,716 ----
double nrows;
/*
! * Use statistics stored in pg_class as is if any. Otherwise, calculate
! * them from file size and average tuple width.
*/
! if (baserel->pages > 0)
! {
! pages = baserel->pages;
! ntuples = baserel->tuples;
! }
! else
! {
! /*
! * Get size of the file. It might not be there at plan time, though,
! * in which case we have to use a default estimate.
! */
! if (stat(fdw_private->filename, &stat_buf) < 0)
! stat_buf.st_size = 10 * BLCKSZ;
! /*
! * Convert size to pages for use in I/O cost estimate later.
! */
! pages = (stat_buf.st_size + (BLCKSZ - 1)) / BLCKSZ;
! if (pages < 1)
! pages = 1;
! /*
! * Estimate the number of tuples in the file. We back into this
! * estimate using the planner's idea of the relation width; which is
! * bogus if not all columns are being read, not to mention that the
! * text representation of a row probably isn't the same size as its
! * internal representation. FIXME later.
! */
! tuple_width = MAXALIGN(baserel->width) +
! MAXALIGN(sizeof(HeapTupleHeaderData));
! ntuples = clamp_row_est((double) stat_buf.st_size /
! (double) tuple_width);
! }
+ /* Pass estimates to subsequent functions via FileFdwPlanState. */
+ fdw_private->pages = pages;
fdw_private->ntuples = ntuples;
/*
*************** estimate_size(PlannerInfo *root, RelOptI
*** 709,714 ****
--- 732,747 ----
}
/*
+ * fileAnalyzeForeignTable
+ * Analyze foreign table
+ */
+ static void
+ fileAnalyzeForeignTable(Relation onerel, VacuumStmt *vacstmt, int elevel)
+ {
+ do_analyze_rel(onerel, vacstmt, elevel, false, acquire_sample_rows);
+ }
+
+ /*
* Estimate costs of scanning a foreign table.
*
* Results are returned in *startup_cost and *total_cost.
*************** estimate_costs(PlannerInfo *root, RelOpt
*** 736,738 ****
--- 769,957 ----
run_cost += cpu_per_tuple * ntuples;
*total_cost = *startup_cost + run_cost;
}
+
+ /*
+ * acquire_sample_rows -- acquire a random sample of rows from the table
+ *
+ * Selected rows are returned in the caller-allocated array rows[], which must
+ * have at least targrows entries. The actual number of rows selected is
+ * returned as the function result. We also count the number of valid rows in
+ * the table, and return it into *totalrows.
+ *
+ * The returned list of tuples is in order by physical position in the table.
+ * (We will rely on this later to derive correlation estimates.)
+ */
+ static int
+ acquire_sample_rows(Relation onerel, HeapTuple *rows, int targrows,
+ double *totalrows, double *totaldeadrows,
+ BlockNumber *totalpages, int elevel)
+ {
+ int numrows = 0;
+ int invalrows = 0; /* total # rows violating
+ the NOT NULL constraints */
+ double validrows = 0; /* total # rows collected */
+ double rowstoskip = -1; /* -1 means not set yet */
+ double rstate;
+ HeapTuple tuple;
+ TupleDesc tupDesc;
+ TupleConstr *constr;
+ int natts;
+ int attrChk;
+ Datum *values;
+ bool *nulls;
+ bool found;
+ bool sample_it = false;
+ char *filename;
+ struct stat stat_buf;
+ List *options;
+ CopyState cstate;
+ ErrorContextCallback errcontext;
+
+ Assert(onerel);
+ Assert(targrows > 0);
+
+ tupDesc = RelationGetDescr(onerel);
+ constr = tupDesc->constr;
+ natts = tupDesc->natts;
+ values = (Datum *) palloc(tupDesc->natts * sizeof(Datum));
+ nulls = (bool *) palloc(tupDesc->natts * sizeof(bool));
+
+ /* Fetch options of foreign table */
+ fileGetOptions(RelationGetRelid(onerel), &filename, &options);
+
+ /*
+ * Get size of the file.
+ */
+ if (stat(filename, &stat_buf) < 0)
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not stat file \"%s\": %m",
+ filename)));
+
+ /*
+ * Convert size to pages for use in I/O cost estimate.
+ */
+ *totalpages = (stat_buf.st_size + (BLCKSZ - 1)) / BLCKSZ;
+ if (*totalpages < 1)
+ *totalpages = 1;
+
+ /*
+ * Create CopyState from FDW options. We always acquire all columns, so
+ * as to match the expected ScanTupleSlot signature.
+ */
+ cstate = BeginCopyFrom(onerel, filename, NIL, options);
+
+ /* Prepare for sampling rows */
+ rstate = init_selection_state(targrows);
+
+ /* Set up callback to identify error line number. */
+ errcontext.callback = CopyFromErrorCallback;
+ errcontext.arg = (void *) cstate;
+ errcontext.previous = error_context_stack;
+ error_context_stack = &errcontext;
+
+ for (;;)
+ {
+ sample_it = true;
+
+ /*
+ * Check for user-requested abort.
+ */
+ CHECK_FOR_INTERRUPTS();
+
+ found = NextCopyFrom(cstate, NULL, values, nulls, NULL);
+
+ if (!found)
+ break;
+
+ tuple = heap_form_tuple(tupDesc, values, nulls);
+
+ if (constr && constr->has_not_null)
+ {
+ for (attrChk = 1; attrChk <= natts; attrChk++)
+ {
+ if (onerel->rd_att->attrs[attrChk - 1]->attnotnull &&
+ heap_attisnull(tuple, attrChk))
+ {
+ sample_it = false;
+ break;
+ }
+ }
+ }
+
+ if (!sample_it)
+ {
+ invalrows += 1;
+ heap_freetuple(tuple);
+ continue;
+ }
+
+ /*
+ * The first targrows sample rows are simply copied into the
+ * reservoir. Then we start replacing tuples in the sample
+ * until we reach the end of the relation. This algorithm is
+ * from Jeff Vitter's paper (see full citation below). It
+ * works by repeatedly computing the number of tuples to skip
+ * before selecting a tuple, which replaces a randomly chosen
+ * element of the reservoir (current set of tuples). At all
+ * times the reservoir is a true random sample of the tuples
+ * we've passed over so far, so when we fall off the end of
+ * the relation we're done.
+ */
+ if (numrows < targrows)
+ rows[numrows++] = heap_copytuple(tuple);
+ else
+ {
+ /*
+ * t in Vitter's paper is the number of records already
+ * processed. If we need to compute a new S value, we
+ * must use the not-yet-incremented value of samplerows as
+ * t.
+ */
+ if (rowstoskip < 0)
+ rowstoskip = get_next_S(validrows, targrows, &rstate);
+
+ if (rowstoskip <= 0)
+ {
+ /*
+ * Found a suitable tuple, so save it, replacing one
+ * old tuple at random
+ */
+ int k = (int) (targrows * random_fract());
+
+ Assert(k >= 0 && k < targrows);
+ heap_freetuple(rows[k]);
+ rows[k] = heap_copytuple(tuple);
+ }
+
+ rowstoskip -= 1;
+ }
+
+ validrows += 1;
+ heap_freetuple(tuple);
+ }
+
+ /* Remove error callback. */
+ error_context_stack = errcontext.previous;
+
+ *totalrows = validrows;
+ *totaldeadrows = 0;
+
+ EndCopyFrom(cstate);
+
+ pfree(values);
+ pfree(nulls);
+
+ /*
+ * Emit some interesting relation info
+ */
+ ereport(elevel,
+ (errmsg("\"%s\": scanned, "
+ "containing %d valid rows and %d invalid rows; "
+ "%d rows in sample, %d total rows",
+ RelationGetRelationName(onerel),
+ (int) validrows, invalrows,
+ numrows, (int) *totalrows)));
+
+ return numrows;
+ }
diff --git a/contrib/file_fdw/input/file_fdw.source b/contrib/file_fdw/input/file_fdw.source
index 8e3d553..21b6fb4 100644
*** a/contrib/file_fdw/input/file_fdw.source
--- b/contrib/file_fdw/input/file_fdw.source
*************** EXECUTE st(100);
*** 111,116 ****
--- 111,121 ----
EXECUTE st(100);
DEALLOCATE st;
+ -- statistics collection tests
+ ANALYZE agg_csv;
+ SELECT relpages, reltuples FROM pg_class WHERE relname = 'agg_csv';
+ SELECT * FROM pg_stats WHERE tablename = 'agg_csv';
+
-- tableoid
SELECT tableoid::regclass, b FROM agg_csv;
diff --git a/contrib/file_fdw/output/file_fdw.source b/contrib/file_fdw/output/file_fdw.source
index 84f0750..fe0d67f 100644
*** a/contrib/file_fdw/output/file_fdw.source
--- b/contrib/file_fdw/output/file_fdw.source
*************** EXECUTE st(100);
*** 174,179 ****
--- 174,194 ----
(1 row)
DEALLOCATE st;
+ -- statistics collection tests
+ ANALYZE agg_csv;
+ SELECT relpages, reltuples FROM pg_class WHERE relname = 'agg_csv';
+ relpages | reltuples
+ ----------+-----------
+ 1 | 3
+ (1 row)
+
+ SELECT * FROM pg_stats WHERE tablename = 'agg_csv';
+ schemaname | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation
+ ------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+-------------------------+-------------
+ public | agg_csv | a | f | 0 | 2 | -1 | | | {0,42,100} | -0.5
+ public | agg_csv | b | f | 0 | 4 | -1 | | | {0.09561,99.097,324.78} | 0.5
+ (2 rows)
+
-- tableoid
SELECT tableoid::regclass, b FROM agg_csv;
tableoid | b
diff --git a/contrib/pgsql_fdw/deparse.c b/contrib/pgsql_fdw/deparse.c
index 6f8f753..f4a422c 100644
*** a/contrib/pgsql_fdw/deparse.c
--- b/contrib/pgsql_fdw/deparse.c
*************** typedef struct foreign_executable_cxt
*** 46,53 ****
* Get string representation which can be used in SQL statement from a node.
*/
static void deparseExpr(StringInfo buf, Expr *expr, PlannerInfo *root);
! static void deparseRelation(StringInfo buf, Oid relid, PlannerInfo *root,
! bool need_prefix);
static void deparseVar(StringInfo buf, Var *node, PlannerInfo *root,
bool need_prefix);
static void deparseConst(StringInfo buf, Const *node, PlannerInfo *root);
--- 46,52 ----
* Get string representation which can be used in SQL statement from a node.
*/
static void deparseExpr(StringInfo buf, Expr *expr, PlannerInfo *root);
! static void deparseRelation(StringInfo buf, Oid relid, bool need_prefix);
static void deparseVar(StringInfo buf, Var *node, PlannerInfo *root,
bool need_prefix);
static void deparseConst(StringInfo buf, Const *node, PlannerInfo *root);
*************** static bool is_builtin(Oid procid);
*** 77,89 ****
* Deparse query representation into SQL statement which suits for remote
* PostgreSQL server. This function basically creates simple query string
* which consists of only SELECT, FROM clauses.
*/
void
deparseSimpleSql(StringInfo buf,
Oid relid,
PlannerInfo *root,
! RelOptInfo *baserel,
! ForeignTable *table)
{
StringInfoData foreign_relname;
bool first;
--- 76,90 ----
* Deparse query representation into SQL statement which suits for remote
* PostgreSQL server. This function basically creates simple query string
* which consists of only SELECT, FROM clauses.
+ *
+ * Parameters root and baserel are optional; when either of them was NULL,
+ * SELECT clause is simply "SELECT *".
*/
void
deparseSimpleSql(StringInfo buf,
Oid relid,
PlannerInfo *root,
! RelOptInfo *baserel)
{
StringInfoData foreign_relname;
bool first;
*************** deparseSimpleSql(StringInfo buf,
*** 101,107 ****
* evaluated on local, can be replaced with literal "NULL" in the SELECT
* clause to reduce overhead of tuple handling tuple and data transfer.
*/
! if (baserel->baserestrictinfo != NIL)
{
ListCell *lc;
--- 102,108 ----
* evaluated on local, can be replaced with literal "NULL" in the SELECT
* clause to reduce overhead of tuple handling tuple and data transfer.
*/
! if (baserel != NULL)
{
ListCell *lc;
*************** deparseSimpleSql(StringInfo buf,
*** 134,179 ****
* function requires entries for dropped columns. Such entries must be
* initialized with NULL before calling tuple constructor.
*/
! appendStringInfo(buf, "SELECT ");
! attr_used = list_union(attr_used, baserel->reltargetlist);
! first = true;
! for (attr = 1; attr <= baserel->max_attr; attr++)
{
! RangeTblEntry *rte = root->simple_rte_array[baserel->relid];
! Var *var = NULL;
! ListCell *lc;
! /* Ignore dropped attributes. */
! if (get_rte_attribute_is_dropped(rte, attr))
! continue;
! if (!first)
! appendStringInfo(buf, ", ");
! first = false;
! /*
! * We use linear search here, but it wouldn't be problem since
! * attr_used seems to not become so large.
! */
! foreach (lc, attr_used)
! {
! var = lfirst(lc);
! if (var->varattno == attr)
! break;
! var = NULL;
}
! if (var != NULL)
! deparseVar(buf, var, root, false);
! else
! appendStringInfo(buf, "NULL");
}
- appendStringInfoChar(buf, ' ');
/*
* deparse FROM clause, including alias if any
*/
appendStringInfo(buf, "FROM ");
! deparseRelation(buf, table->relid, root, true);
elog(DEBUG3, "Remote SQL: %s", buf->data);
}
--- 135,187 ----
* function requires entries for dropped columns. Such entries must be
* initialized with NULL before calling tuple constructor.
*/
! if (root == NULL || baserel == NULL)
{
! appendStringInfo(buf, "SELECT * ");
! }
! else
! {
! appendStringInfo(buf, "SELECT ");
! attr_used = list_union(attr_used, baserel->reltargetlist);
! first = true;
! for (attr = 1; attr <= baserel->max_attr; attr++)
! {
! RangeTblEntry *rte = root->simple_rte_array[baserel->relid];
! Var *var = NULL;
! ListCell *lc;
! /* Ignore dropped attributes. */
! if (get_rte_attribute_is_dropped(rte, attr))
! continue;
! if (!first)
! appendStringInfo(buf, ", ");
! first = false;
! /*
! * We use linear search here, but it wouldn't be problem since
! * attr_used seems to not become so large.
! */
! foreach (lc, attr_used)
! {
! var = lfirst(lc);
! if (var->varattno == attr)
! break;
! var = NULL;
! }
! if (var != NULL)
! deparseVar(buf, var, root, false);
! else
! appendStringInfo(buf, "NULL");
}
! appendStringInfoChar(buf, ' ');
}
/*
* deparse FROM clause, including alias if any
*/
appendStringInfo(buf, "FROM ");
! deparseRelation(buf, relid, true);
elog(DEBUG3, "Remote SQL: %s", buf->data);
}
*************** sortConditions(PlannerInfo *root,
*** 219,224 ****
--- 227,298 ----
}
/*
+ * Deparse SELECT statement to acquire sample rows of given relation into buf.
+ */
+ void
+ deparseAnalyzeSql(StringInfo buf, Relation rel)
+ {
+ Oid relid = RelationGetRelid(rel);
+ TupleDesc tupdesc = RelationGetDescr(rel);
+ int i;
+ char *colname;
+ List *options;
+ ListCell *lc;
+ bool first = true;
+ char *nspname;
+ char *relname;
+ ForeignTable *table;
+
+ /* Deparse SELECT clause, use attribute name or colname option. */
+ appendStringInfo(buf, "SELECT ");
+ for (i = 0; i < tupdesc->natts; i++)
+ {
+ if (tupdesc->attrs[i]->attisdropped)
+ continue;
+
+ colname = NameStr(tupdesc->attrs[i]->attname);
+ options = GetForeignColumnOptions(relid, tupdesc->attrs[i]->attnum);
+
+ foreach(lc, options)
+ {
+ DefElem *def= (DefElem *) lfirst(lc);
+
+ if (strcmp(def->defname, "colname") == 0)
+ {
+ colname = defGetString(def);
+ break;
+ }
+ }
+
+ if (!first)
+ appendStringInfo(buf, ", ");
+ appendStringInfo(buf, "%s", quote_identifier(colname));
+ first = false;
+ }
+
+ /*
+ * Deparse FROM clause, use namespace and relation name, or use nspname and
+ * colname options respectively.
+ */
+ nspname = get_namespace_name(get_rel_namespace(relid));
+ relname = get_rel_name(relid);
+ table = GetForeignTable(relid);
+ foreach(lc, table->options)
+ {
+ DefElem *def= (DefElem *) lfirst(lc);
+
+ if (strcmp(def->defname, "nspname") == 0)
+ nspname = defGetString(def);
+ else if (strcmp(def->defname, "relname") == 0)
+ relname = defGetString(def);
+ }
+
+ appendStringInfo(buf, " FROM %s.%s", quote_identifier(nspname),
+ quote_identifier(relname));
+ }
+
+
+ /*
* Deparse given expression into buf. Actual string operation is delegated to
* node-type-specific functions.
*
*************** deparseVar(StringInfo buf,
*** 353,387 ****
* option overrides schema name.
*/
static void
! deparseRelation(StringInfo buf,
! Oid relid,
! PlannerInfo *root,
! bool need_prefix)
{
- int i;
- RangeTblEntry *rte = NULL;
ListCell *lc;
const char *nspname = NULL; /* plain namespace name */
const char *relname = NULL; /* plain relation name */
const char *q_nspname; /* quoted namespace name */
const char *q_relname; /* quoted relation name */
- /* Find RangeTblEntry for the relation from PlannerInfo. */
- for (i = 1; i < root->simple_rel_array_size; i++)
- {
- if (root->simple_rte_array[i]->relid == relid)
- {
- rte = root->simple_rte_array[i];
- break;
- }
- }
- if (rte == NULL)
- elog(ERROR, "relation with OID %u is not used in the query", relid);
-
/* If target is a foreign table, obtain additional catalog information. */
! if (rte->relkind == RELKIND_FOREIGN_TABLE)
{
! ForeignTable *table = GetForeignTable(rte->relid);
/*
* Use value of FDW options if any, instead of the name of object
--- 427,444 ----
* option overrides schema name.
*/
static void
! deparseRelation(StringInfo buf, Oid relid, bool need_prefix)
{
ListCell *lc;
const char *nspname = NULL; /* plain namespace name */
const char *relname = NULL; /* plain relation name */
const char *q_nspname; /* quoted namespace name */
const char *q_relname; /* quoted relation name */
/* If target is a foreign table, obtain additional catalog information. */
! if (get_rel_relkind(relid) == RELKIND_FOREIGN_TABLE)
{
! ForeignTable *table = GetForeignTable(relid);
/*
* Use value of FDW options if any, instead of the name of object
diff --git a/contrib/pgsql_fdw/expected/pgsql_fdw.out b/contrib/pgsql_fdw/expected/pgsql_fdw.out
index 8e50614..7a6001b 100644
*** a/contrib/pgsql_fdw/expected/pgsql_fdw.out
--- b/contrib/pgsql_fdw/expected/pgsql_fdw.out
*************** INSERT INTO "S 1"."T 2"
*** 75,80 ****
--- 75,81 ----
'AAA' || to_char(id, 'FM000')
FROM generate_series(1, 100) id;
COMMIT;
+ ANALYZE;
-- ===================================================================
-- tests for pgsql_fdw_validator
-- ===================================================================
*************** ALTER FOREIGN TABLE ft2 ALTER COLUMN c1
*** 169,174 ****
--- 170,198 ----
(2 rows)
-- ===================================================================
+ -- ANALYZE
+ -- ===================================================================
+ ANALYZE ft1;
+ ANALYZE ft2;
+ SELECT relpages, reltuples from pg_class where oid = 'ft1'::regclass;
+ relpages | reltuples
+ ----------+-----------
+ 0 | 1000
+ (1 row)
+
+ SELECT staattnum, count(*) FROM pg_statistic WHERE starelid = 'ft1'::regclass GROUP BY staattnum ORDER BY staattnum;
+ staattnum | count
+ -----------+-------
+ 2 | 1
+ 3 | 1
+ 4 | 1
+ 5 | 1
+ 6 | 1
+ 7 | 1
+ 8 | 1
+ (7 rows)
+
+ -- ===================================================================
-- simple queries
-- ===================================================================
-- single table, with/without alias
*************** EXECUTE st1(101, 101);
*** 459,478 ****
-- subquery using stable function (can't be pushed down)
PREPARE st2(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c4) = 6) ORDER BY c1;
EXPLAIN (COSTS false) EXECUTE st2(10, 20);
! QUERY PLAN
! -----------------------------------------------------------------------------------------------------------------------------------------------
Sort
Sort Key: t1.c1
! -> Hash Join
! Hash Cond: (t1.c3 = t2.c3)
-> Foreign Scan on ft1 t1
Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.<) 20))
! -> Hash
! -> HashAggregate
! -> Foreign Scan on ft2 t2
! Filter: (date_part('dow'::text, c4) = 6::double precision)
! Remote SQL: SELECT "C 1", NULL, c3, c4, NULL, NULL, NULL FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.>) 10))
! (11 rows)
EXECUTE st2(10, 20);
c1 | c2 | c3 | c4 | c5 | c6 | c7
--- 483,501 ----
-- subquery using stable function (can't be pushed down)
PREPARE st2(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c4) = 6) ORDER BY c1;
EXPLAIN (COSTS false) EXECUTE st2(10, 20);
! QUERY PLAN
! -----------------------------------------------------------------------------------------------------------------------------------------
Sort
Sort Key: t1.c1
! -> Nested Loop Semi Join
! Join Filter: (t1.c3 = t2.c3)
-> Foreign Scan on ft1 t1
Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.<) 20))
! -> Materialize
! -> Foreign Scan on ft2 t2
! Filter: (date_part('dow'::text, c4) = 6::double precision)
! Remote SQL: SELECT "C 1", NULL, c3, c4, NULL, NULL, NULL FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.>) 10))
! (10 rows)
EXECUTE st2(10, 20);
c1 | c2 | c3 | c4 | c5 | c6 | c7
*************** EXPLAIN (COSTS false) EXECUTE st4(1);
*** 552,561 ****
(2 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! ---------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.=) $1))
(2 rows)
-- cleanup
--- 575,584 ----
(2 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
! QUERY PLAN
! --------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.=) 1))
(2 rows)
-- cleanup
diff --git a/contrib/pgsql_fdw/pgsql_fdw.c b/contrib/pgsql_fdw/pgsql_fdw.c
index 8975955..0412c2c 100644
*** a/contrib/pgsql_fdw/pgsql_fdw.c
--- b/contrib/pgsql_fdw/pgsql_fdw.c
***************
*** 17,22 ****
--- 17,23 ----
#include "catalog/pg_foreign_table.h"
#include "commands/defrem.h"
#include "commands/explain.h"
+ #include "commands/vacuum.h"
#include "funcapi.h"
#include "miscadmin.h"
#include "optimizer/cost.h"
*************** static TupleTableSlot *pgsqlIterateForei
*** 166,171 ****
--- 167,176 ----
static void pgsqlReScanForeignScan(ForeignScanState *node);
static void pgsqlEndForeignScan(ForeignScanState *node);
+ static void pgsqlAnalyzeForeignTable(Relation relation,
+ VacuumStmt *vacstmt,
+ int elevel);
+
/*
* Helper functions
*/
*************** static void execute_query(ForeignScanSta
*** 181,186 ****
--- 186,195 ----
static PGresult *fetch_result(ForeignScanState *node);
static void store_result(ForeignScanState *node, PGresult *res);
static void pgsql_fdw_error_callback(void *arg);
+ static int pgsql_acquire_sample_rows(Relation onerel, HeapTuple *rows,
+ int targrows, double *totalrows,
+ double *totaldeadrows,
+ BlockNumber *totalpages, int elevel);
/* Exported functions, but not written in pgsql_fdw.h. */
void _PG_init(void);
*************** static FdwRoutine fdwroutine = {
*** 222,227 ****
--- 231,237 ----
pgsqlEndForeignScan,
/* Optional handler functions. */
+ pgsqlAnalyzeForeignTable,
};
/*
*************** pgsqlGetForeignRelSize(PlannerInfo *root
*** 288,294 ****
* appended later.
*/
sortConditions(root, baserel, &remote_conds, ¶m_conds, &local_conds);
! deparseSimpleSql(sql, foreigntableid, root, baserel, table);
if (list_length(remote_conds) > 0)
{
appendWhereClause(sql, fpstate->has_where, remote_conds, root);
--- 298,304 ----
* appended later.
*/
sortConditions(root, baserel, &remote_conds, ¶m_conds, &local_conds);
! deparseSimpleSql(sql, foreigntableid, root, baserel);
if (list_length(remote_conds) > 0)
{
appendWhereClause(sql, fpstate->has_where, remote_conds, root);
*************** pgsqlEndForeignScan(ForeignScanState *no
*** 761,766 ****
--- 771,786 ----
}
/*
+ * Collect statistics of a foreign table, and store the result in system
+ * catalogs.
+ */
+ static void
+ pgsqlAnalyzeForeignTable(Relation relation, VacuumStmt *vacstmt, int elevel)
+ {
+ do_analyze_rel(relation, vacstmt, elevel, false, pgsql_acquire_sample_rows);
+ }
+
+ /*
* Estimate costs of executing given SQL statement.
*/
static void
*************** pgsql_fdw_error_callback(void *arg)
*** 1111,1113 ****
--- 1131,1344 ----
errcontext("column %s of foreign table %s",
quote_identifier(colname), quote_identifier(relname));
}
+
+ /*
+ * Acquire a random sample of rows from foreign table managed by pgsql_fdw.
+ *
+ * pgsql_fdw doesn't provide direct access to remote buffer, so we exeucte
+ * simple SELECT statement which retrieves whole rows from remote side, and
+ * pick some samples from them.
+ */
+ static int
+ pgsql_acquire_sample_rows(Relation onerel, HeapTuple *rows, int targrows,
+ double *totalrows, double *totaldeadrows,
+ BlockNumber *totalpages, int elevel)
+ {
+ StringInfoData sql;
+ StringInfoData buf;
+ ForeignTable *table;
+ ForeignServer *server;
+ UserMapping *user;
+ int fetch_count;
+ PGconn *conn = NULL;
+ PGresult *res = NULL;
+ int row;
+ int i;
+ int j;
+ /* variables for tuple creation */
+ TupleDesc tupdesc;
+ AttInMetadata *attinmeta;
+ HeapTuple tuple;
+ char **values;
+ /* variables for sampling */
+ int numrows = 0;
+ double samplerows = 0;
+ double rowstoskip = -1;
+ double rstate;
+
+ /* Prepare for sampling rows */
+ rstate = init_selection_state(targrows);
+
+ /* Prepare tuple construction. */
+ tupdesc = onerel->rd_att;
+ attinmeta = TupleDescGetAttInMetadata(tupdesc);
+ values = (char **) palloc(sizeof(char *) * tupdesc->natts);
+
+ /*
+ * Construct SELECT statement which retrieves whole rows from remote. We
+ * can't avoid running sequential scan on remote side to get practical
+ * statistics, so this seems reasonable compromise.
+ */
+ initStringInfo(&sql);
+ deparseAnalyzeSql(&sql, onerel);
+ initStringInfo(&buf);
+ appendStringInfo(&buf, "DECLARE pgsql_fdw_cur CURSOR FOR %s", sql.data);
+
+ table = GetForeignTable(onerel->rd_id);
+ server = GetForeignServer(table->serverid);
+ user = GetUserMapping(GetOuterUserId(), server->serverid);
+ conn = GetConnection(server, user, true);
+
+ /*
+ * Acquire sample rows from the result set.
+ */
+ PG_TRY();
+ {
+ /* Declare non-scrollable cursor for analyze. */
+ res = PQexec(conn, buf.data);
+ if (PQresultStatus(res) != PGRES_COMMAND_OK)
+ ereport(ERROR,
+ (errmsg("could not declare cursor"),
+ errdetail("%s", PQerrorMessage(conn)),
+ errhint("%s", buf.data)));
+ PQclear(res);
+ res = NULL;
+
+ /* Execute FETCH statement until all rows have been retrieved. */
+ resetStringInfo(&buf);
+ fetch_count = GetFetchCountOption(table, server);
+ appendStringInfo(&buf, "FETCH %d FROM pgsql_fdw_cur", fetch_count);
+ while (true)
+ {
+ /*
+ * ANALYZE against foreign tables are not done in processes of
+ * vacuum, so here we use CHECK_FOR_INTERRUPTS instead of
+ * vacuum_delay_point().
+ */
+ CHECK_FOR_INTERRUPTS();
+
+ /* Fetch next bunch of results from remote side. */
+ res = PQexec(conn, buf.data);
+ if (PQresultStatus(res) != PGRES_TUPLES_OK)
+ ereport(ERROR,
+ (errmsg("could not fetch rows from foreign server"),
+ errdetail("%s", PQerrorMessage(conn)),
+ errhint("%s", buf.data)));
+ if (PQntuples(res) == 0)
+ break;
+
+ /* Loop though the result set and pick samples up. */
+ for (row = 0; row < PQntuples(res); row++)
+ {
+ if (numrows < targrows)
+ {
+ /*
+ * Create sample tuple from the result, and append to the
+ * tuple buffer. Note that i and j point entries in
+ * catalog and PGresult respectively.
+ */
+ for (i = 0, j = 0; i < tupdesc->natts; i++)
+ {
+ if (tupdesc->attrs[i]->attisdropped)
+ continue;
+
+ if (PQgetisnull(res, row, j))
+ values[i] = NULL;
+ else
+ values[i] = PQgetvalue(res, row, j);
+ j++;
+ }
+ tuple = BuildTupleFromCStrings(attinmeta, values);
+ rows[numrows++] = tuple;
+ }
+ else
+ {
+ /*
+ * The first targrows sample rows are simply copied into
+ * the reservoir. Then we start replacing tuples in the
+ * sample until we reach the end of the relation. This
+ * algorithm is from Jeff Vitter's paper, similarly to
+ * acquire_sample_rows in analyze.c.
+ *
+ * We don't have block-wise accessibility, so every row in
+ * the PGresult is possible to be sample.
+ */
+ if (rowstoskip < 0)
+ rowstoskip = get_next_S(samplerows, targrows, &rstate);
+
+ if (rowstoskip <= 0)
+ {
+ int k = (int) (targrows * random_fract());
+
+ Assert(k >= 0 && k < targrows);
+
+ /*
+ * Create sample tuple from the result, and replace at
+ * random.
+ */
+ heap_freetuple(rows[k]);
+ for (i = 0, j = 0; i < tupdesc->natts; i++)
+ {
+ if (tupdesc->attrs[i]->attisdropped)
+ continue;
+
+ if (PQgetisnull(res, row, j))
+ values[i] = NULL;
+ else
+ values[i] = PQgetvalue(res, row, j);
+ j++;
+ }
+ tuple = BuildTupleFromCStrings(attinmeta, values);
+ rows[k] = tuple;
+ }
+
+ rowstoskip -= 1;
+ }
+
+ samplerows += 1;
+ }
+
+ PQclear(res);
+ res = NULL;
+ }
+
+ /* Close the cursor. */
+ res = PQexec(conn, "CLOSE pgsql_fdw_cur");
+ if (PQresultStatus(res) != PGRES_COMMAND_OK)
+ ereport(ERROR,
+ (errmsg("could not close cursor"),
+ errdetail("%s", PQerrorMessage(conn))));
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+
+ ReleaseConnection(conn);
+
+ /* We assume that we have no dead tuple. */
+ *totaldeadrows = 0.0;
+
+ /* We've retrieved all living tuples from foreign server. */
+ *totalrows = samplerows;
+
+ /*
+ * We don't update pg_class.relpages because we don't care that in
+ * planning at all.
+ */
+
+ /*
+ * Emit some interesting relation info
+ */
+ ereport(elevel,
+ (errmsg("\"%s\": scanned with \"%s\", "
+ "containing %.0f live rows and %.0f dead rows; "
+ "%d rows in sample, %.0f estimated total rows",
+ RelationGetRelationName(onerel), sql.data,
+ samplerows, 0.0,
+ numrows, samplerows)));
+
+ return numrows;
+ }
diff --git a/contrib/pgsql_fdw/pgsql_fdw.h b/contrib/pgsql_fdw/pgsql_fdw.h
index 5487d52..8ca58d4 100644
*** a/contrib/pgsql_fdw/pgsql_fdw.h
--- b/contrib/pgsql_fdw/pgsql_fdw.h
*************** int GetFetchCountOption(ForeignTable *ta
*** 29,36 ****
void deparseSimpleSql(StringInfo buf,
Oid relid,
PlannerInfo *root,
! RelOptInfo *baserel,
! ForeignTable *table);
void appendWhereClause(StringInfo buf,
bool has_where,
List *exprs,
--- 29,35 ----
void deparseSimpleSql(StringInfo buf,
Oid relid,
PlannerInfo *root,
! RelOptInfo *baserel);
void appendWhereClause(StringInfo buf,
bool has_where,
List *exprs,
*************** void sortConditions(PlannerInfo *root,
*** 40,44 ****
--- 39,44 ----
List **remote_conds,
List **param_conds,
List **local_conds);
+ void deparseAnalyzeSql(StringInfo buf, Relation rel);
#endif /* PGSQL_FDW_H */
diff --git a/contrib/pgsql_fdw/sql/pgsql_fdw.sql b/contrib/pgsql_fdw/sql/pgsql_fdw.sql
index 6692af7..f0c282a 100644
*** a/contrib/pgsql_fdw/sql/pgsql_fdw.sql
--- b/contrib/pgsql_fdw/sql/pgsql_fdw.sql
*************** INSERT INTO "S 1"."T 2"
*** 85,90 ****
--- 85,91 ----
'AAA' || to_char(id, 'FM000')
FROM generate_series(1, 100) id;
COMMIT;
+ ANALYZE;
-- ===================================================================
-- tests for pgsql_fdw_validator
*************** ALTER FOREIGN TABLE ft2 ALTER COLUMN c1
*** 141,146 ****
--- 142,155 ----
\det+
-- ===================================================================
+ -- ANALYZE
+ -- ===================================================================
+ ANALYZE ft1;
+ ANALYZE ft2;
+ SELECT relpages, reltuples from pg_class where oid = 'ft1'::regclass;
+ SELECT staattnum, count(*) FROM pg_statistic WHERE starelid = 'ft1'::regclass GROUP BY staattnum ORDER BY staattnum;
+
+ -- ===================================================================
-- simple queries
-- ===================================================================
-- single table, with/without alias
diff --git a/doc/src/sgml/fdwhandler.sgml b/doc/src/sgml/fdwhandler.sgml
index f7bf3d8..4f962b7 100644
*** a/doc/src/sgml/fdwhandler.sgml
--- b/doc/src/sgml/fdwhandler.sgml
*************** EndForeignScan (ForeignScanState *node);
*** 277,282 ****
--- 277,305 ----
</para>
<para>
+ <programlisting>
+ void
+ AnalyzeForeignTable (Relation onerel,
+ VacuumStmt *vacstmt,
+ int elevel);
+ </programlisting>
+
+ Collect statistics on a foreign table and store the results in the
+ pg_class and pg_statistics system catalogs.
+ This is optional, and if implemented, called when <command>ANALYZE</>
+ command is run. The statistics are used by the query planner in order to
+ make good choices of query plans.
+ </para>
+
+ <para>
+ The function can be implemented by writing a sampling function that
+ acquires a random sample of rows from an external data source and
+ then by calling <function>do_analyze_rel</>, where you should pass
+ the sampling function as an argument.
+ The function must be set to NULL if it isn't implemented.
+ </para>
+
+ <para>
The <structname>FdwRoutine</> struct type is declared in
<filename>src/include/foreign/fdwapi.h</>, which see for additional
details.
diff --git a/doc/src/sgml/maintenance.sgml b/doc/src/sgml/maintenance.sgml
index 93c3ff5..54d0838 100644
*** a/doc/src/sgml/maintenance.sgml
--- b/doc/src/sgml/maintenance.sgml
***************
*** 284,289 ****
--- 284,293 ----
<command>ANALYZE</> strictly as a function of the number of rows
inserted or updated; it has no knowledge of whether that will lead
to meaningful statistical changes.
+ Note that the autovacuum daemon does not issue <command>ANALYZE</>
+ commands on foreign tables. It is recommended to run manually-managed
+ <command>ANALYZE</> commands as needed, which typically are executed
+ according to a schedule by cron or Task Scheduler scripts.
</para>
<para>
diff --git a/doc/src/sgml/ref/alter_foreign_table.sgml b/doc/src/sgml/ref/alter_foreign_table.sgml
index c4cdaa8..af5c0a8 100644
*** a/doc/src/sgml/ref/alter_foreign_table.sgml
--- b/doc/src/sgml/ref/alter_foreign_table.sgml
*************** ALTER FOREIGN TABLE [ IF EXISTS ] <repla
*** 36,41 ****
--- 36,44 ----
DROP [ COLUMN ] [ IF EXISTS ] <replaceable class="PARAMETER">column</replaceable> [ RESTRICT | CASCADE ]
ALTER [ COLUMN ] <replaceable class="PARAMETER">column</replaceable> [ SET DATA ] TYPE <replaceable class="PARAMETER">type</replaceable>
ALTER [ COLUMN ] <replaceable class="PARAMETER">column</replaceable> { SET | DROP } NOT NULL
+ ALTER [ COLUMN ] <replaceable class="PARAMETER">column</replaceable> SET STATISTICS <replaceable class="PARAMETER">integer</replaceable>
+ ALTER [ COLUMN ] <replaceable class="PARAMETER">column</replaceable> SET ( <replaceable class="PARAMETER">attribute_option</replaceable> = <replaceable class="PARAMETER">value</replaceable> [, ... ] )
+ ALTER [ COLUMN ] <replaceable class="PARAMETER">column</replaceable> RESET ( <replaceable class="PARAMETER">attribute_option</replaceable> [, ... ] )
ALTER [ COLUMN ] <replaceable class="PARAMETER">column</replaceable> OPTIONS ( [ ADD | SET | DROP ] <replaceable class="PARAMETER">option</replaceable> ['<replaceable class="PARAMETER">value</replaceable>'] [, ... ])
OWNER TO <replaceable class="PARAMETER">new_owner</replaceable>
OPTIONS ( [ ADD | SET | DROP ] <replaceable class="PARAMETER">option</replaceable> ['<replaceable class="PARAMETER">value</replaceable>'] [, ... ])
*************** ALTER FOREIGN TABLE [ IF EXISTS ] <repla
*** 104,109 ****
--- 107,156 ----
</varlistentry>
<varlistentry>
+ <term><literal>SET STATISTICS</literal></term>
+ <listitem>
+ <para>
+ This form
+ sets the per-column statistics-gathering target for subsequent
+ <xref linkend="sql-analyze"> operations.
+ The target can be set in the range 0 to 10000; alternatively, set it
+ to -1 to revert to using the system default statistics
+ target (<xref linkend="guc-default-statistics-target">).
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>SET ( <replaceable class="PARAMETER">attribute_option</replaceable> = <replaceable class="PARAMETER">value</replaceable> [, ... ] )</literal></term>
+ <term><literal>RESET ( <replaceable class="PARAMETER">attribute_option</replaceable> [, ... ] )</literal></term>
+ <listitem>
+ <para>
+ This form
+ sets or resets a per-attribute option. Currently, the only defined
+ per-attribute option is <literal>n_distinct</>, which overrides
+ the number-of-distinct-values estimates made by subsequent
+ <xref linkend="sql-analyze"> operations.
+ When set to a positive value, <command>ANALYZE</> will assume that
+ the column contains exactly the specified number of distinct nonnull
+ values.
+ When set to a negative value, which must be greater than or equal
+ to -1, <command>ANALYZE</> will assume that the number of distinct
+ nonnull values in the column is linear in the size of the foreign
+ table; the exact count is to be computed by multiplying the estimated
+ foreign table size by the absolute value of the given number.
+ For example,
+ a value of -1 implies that all values in the column are distinct,
+ while a value of -0.5 implies that each value appears twice on the
+ average.
+ This can be useful when the size of the foreign table changes over
+ time, since the multiplication by the number of rows in the foreign
+ table is not performed until query planning time. Specify a value
+ of 0 to revert to estimating the number of distinct values normally.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
<term><literal>OWNER</literal></term>
<listitem>
<para>
diff --git a/doc/src/sgml/ref/analyze.sgml b/doc/src/sgml/ref/analyze.sgml
index 8c9057b..524a1c9 100644
*** a/doc/src/sgml/ref/analyze.sgml
--- b/doc/src/sgml/ref/analyze.sgml
*************** ANALYZE [ VERBOSE ] [ <replaceable class
*** 39,47 ****
<para>
With no parameter, <command>ANALYZE</command> examines every table in the
! current database. With a parameter, <command>ANALYZE</command> examines
! only that table. It is further possible to give a list of column names,
! in which case only the statistics for those columns are collected.
</para>
</refsect1>
--- 39,49 ----
<para>
With no parameter, <command>ANALYZE</command> examines every table in the
! current database except for foreign tables. With a parameter, <command>
! ANALYZE</command> examines only that table. For a foreign table, it is
! necessary to specify the name of that table. It is further possible to
! give a list of column names, in which case only the statistics for those
! columns are collected.
</para>
</refsect1>
*************** ANALYZE [ VERBOSE ] [ <replaceable class
*** 63,69 ****
<listitem>
<para>
The name (possibly schema-qualified) of a specific table to
! analyze. Defaults to all tables in the current database.
</para>
</listitem>
</varlistentry>
--- 65,72 ----
<listitem>
<para>
The name (possibly schema-qualified) of a specific table to
! analyze. Defaults to all tables in the current database except
! for foreign tables.
</para>
</listitem>
</varlistentry>
*************** ANALYZE [ VERBOSE ] [ <replaceable class
*** 137,143 ****
In rare situations, this non-determinism will cause the planner's
choices of query plans to change after <command>ANALYZE</command> is run.
To avoid this, raise the amount of statistics collected by
! <command>ANALYZE</command>, as described below.
</para>
<para>
--- 140,148 ----
In rare situations, this non-determinism will cause the planner's
choices of query plans to change after <command>ANALYZE</command> is run.
To avoid this, raise the amount of statistics collected by
! <command>ANALYZE</command>, as described below. Note that the time
! needed to analyze on foreign tables depends on the implementation of
! the foreign data wrapper via which such tables are attached.
</para>
<para>
diff --git a/src/backend/commands/analyze.c b/src/backend/commands/analyze.c
index 9cd6e67..5579dee 100644
*** a/src/backend/commands/analyze.c
--- b/src/backend/commands/analyze.c
***************
*** 23,28 ****
--- 23,29 ----
#include "access/xact.h"
#include "catalog/index.h"
#include "catalog/indexing.h"
+ #include "catalog/pg_class.h"
#include "catalog/pg_collation.h"
#include "catalog/pg_inherits_fn.h"
#include "catalog/pg_namespace.h"
***************
*** 30,35 ****
--- 31,38 ----
#include "commands/tablecmds.h"
#include "commands/vacuum.h"
#include "executor/executor.h"
+ #include "foreign/foreign.h"
+ #include "foreign/fdwapi.h"
#include "miscadmin.h"
#include "nodes/nodeFuncs.h"
#include "parser/parse_oper.h"
*************** typedef struct AnlIndexData
*** 78,91 ****
int default_statistics_target = 100;
/* A few variables that don't seem worth passing around as parameters */
- static int elevel = -1;
-
static MemoryContext anl_context = NULL;
static BufferAccessStrategy vac_strategy;
- static void do_analyze_rel(Relation onerel, VacuumStmt *vacstmt, bool inh);
static void BlockSampler_Init(BlockSampler bs, BlockNumber nblocks,
int samplesize);
static bool BlockSampler_HasMore(BlockSampler bs);
--- 81,91 ----
*************** static void compute_index_stats(Relation
*** 96,112 ****
MemoryContext col_context);
static VacAttrStats *examine_attribute(Relation onerel, int attnum,
Node *index_expr);
! static int acquire_sample_rows(Relation onerel, HeapTuple *rows,
! int targrows, double *totalrows, double *totaldeadrows);
! static double random_fract(void);
! static double init_selection_state(int n);
! static double get_next_S(double t, int n, double *stateptr);
! static int compare_rows(const void *a, const void *b);
static int acquire_inherited_sample_rows(Relation onerel,
HeapTuple *rows, int targrows,
! double *totalrows, double *totaldeadrows);
static void update_attstats(Oid relid, bool inh,
int natts, VacAttrStats **vacattrstats);
static Datum std_fetch_func(VacAttrStatsP stats, int rownum, bool *isNull);
static Datum ind_fetch_func(VacAttrStatsP stats, int rownum, bool *isNull);
--- 96,112 ----
MemoryContext col_context);
static VacAttrStats *examine_attribute(Relation onerel, int attnum,
Node *index_expr);
! static int acquire_sample_rows(Relation onerel,
! HeapTuple *rows, int targrows,
! double *totalrows, double *totaldeadrows,
! BlockNumber *totalpages, int elevel);
static int acquire_inherited_sample_rows(Relation onerel,
HeapTuple *rows, int targrows,
! double *totalrows, double *totaldeadrows,
! BlockNumber *totalpages, int elevel);
static void update_attstats(Oid relid, bool inh,
int natts, VacAttrStats **vacattrstats);
+ static int compare_rows(const void *a, const void *b);
static Datum std_fetch_func(VacAttrStatsP stats, int rownum, bool *isNull);
static Datum ind_fetch_func(VacAttrStatsP stats, int rownum, bool *isNull);
*************** static Datum ind_fetch_func(VacAttrStats
*** 117,123 ****
--- 117,125 ----
void
analyze_rel(Oid relid, VacuumStmt *vacstmt, BufferAccessStrategy bstrategy)
{
+ int elevel;
Relation onerel;
+ FdwRoutine *fdwroutine;
/* Set up static variables */
if (vacstmt->options & VACOPT_VERBOSE)
*************** analyze_rel(Oid relid, VacuumStmt *vacst
*** 182,191 ****
}
/*
! * Check that it's a plain table; we used to do this in get_rel_oids() but
! * seems safer to check after we've locked the relation.
*/
! if (onerel->rd_rel->relkind != RELKIND_RELATION)
{
/* No need for a WARNING if we already complained during VACUUM */
if (!(vacstmt->options & VACOPT_VACUUM))
--- 184,195 ----
}
/*
! * Check that it's a plain table or foreign table; we used to do this
! * in get_rel_oids() but seems safer to check after we've locked the
! * relation.
*/
! if (onerel->rd_rel->relkind != RELKIND_RELATION &&
! onerel->rd_rel->relkind != RELKIND_FOREIGN_TABLE)
{
/* No need for a WARNING if we already complained during VACUUM */
if (!(vacstmt->options & VACOPT_VACUUM))
*************** analyze_rel(Oid relid, VacuumStmt *vacst
*** 209,215 ****
}
/*
! * We can ANALYZE any table except pg_statistic. See update_attstats
*/
if (RelationGetRelid(onerel) == StatisticRelationId)
{
--- 213,221 ----
}
/*
! * We can ANALYZE any table except pg_statistic. See update_attstats.
! * In addition, we can ANALYZE foreign tables if AnalyzeForeignTable
! * callback routines of underlying foreign-data wrappers are implemented.
*/
if (RelationGetRelid(onerel) == StatisticRelationId)
{
*************** analyze_rel(Oid relid, VacuumStmt *vacst
*** 217,222 ****
--- 223,242 ----
return;
}
+ if (onerel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+ {
+ fdwroutine = GetFdwRoutineByRelId(RelationGetRelid(onerel));
+
+ if (fdwroutine->AnalyzeForeignTable == NULL)
+ {
+ ereport(WARNING,
+ (errmsg("skipping \"%s\" --- underlying foreign-data wrapper cannot analyze it",
+ RelationGetRelationName(onerel))));
+ relation_close(onerel, ShareUpdateExclusiveLock);
+ return;
+ }
+ }
+
/*
* OK, let's do it. First let other backends know I'm in ANALYZE.
*/
*************** analyze_rel(Oid relid, VacuumStmt *vacst
*** 224,239 ****
MyPgXact->vacuumFlags |= PROC_IN_ANALYZE;
LWLockRelease(ProcArrayLock);
! /*
! * Do the normal non-recursive ANALYZE.
! */
! do_analyze_rel(onerel, vacstmt, false);
! /*
! * If there are child tables, do recursive ANALYZE.
! */
! if (onerel->rd_rel->relhassubclass)
! do_analyze_rel(onerel, vacstmt, true);
/*
* Close source relation now, but keep lock so that no one deletes it
--- 244,281 ----
MyPgXact->vacuumFlags |= PROC_IN_ANALYZE;
LWLockRelease(ProcArrayLock);
! if (onerel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
! {
! ereport(elevel,
! (errmsg("analyzing \"%s.%s\"",
! get_namespace_name(RelationGetNamespace(onerel)),
! RelationGetRelationName(onerel))));
! fdwroutine->AnalyzeForeignTable(onerel, vacstmt, elevel);
! }
! else
! {
! /*
! * Do the normal non-recursive ANALYZE.
! */
! ereport(elevel,
! (errmsg("analyzing \"%s.%s\"",
! get_namespace_name(RelationGetNamespace(onerel)),
! RelationGetRelationName(onerel))));
! do_analyze_rel(onerel, vacstmt, elevel, false, acquire_sample_rows);
! /*
! * If there are child tables, do recursive ANALYZE.
! */
! if (onerel->rd_rel->relhassubclass)
! {
! ereport(elevel,
! (errmsg("analyzing \"%s.%s\" inheritance tree",
! get_namespace_name(RelationGetNamespace(onerel)),
! RelationGetRelationName(onerel))));
! do_analyze_rel(onerel, vacstmt, elevel, true,
! acquire_inherited_sample_rows);
! }
! }
/*
* Close source relation now, but keep lock so that no one deletes it
*************** analyze_rel(Oid relid, VacuumStmt *vacst
*** 255,262 ****
/*
* do_analyze_rel() -- analyze one relation, recursively or not
*/
! static void
! do_analyze_rel(Relation onerel, VacuumStmt *vacstmt, bool inh)
{
int attr_cnt,
tcnt,
--- 297,305 ----
/*
* do_analyze_rel() -- analyze one relation, recursively or not
*/
! void
! do_analyze_rel(Relation onerel, VacuumStmt *vacstmt, int elevel,
! bool inh, SampleRowAcquireFunc acquirefunc)
{
int attr_cnt,
tcnt,
*************** do_analyze_rel(Relation onerel, VacuumSt
*** 271,276 ****
--- 314,320 ----
numrows;
double totalrows,
totaldeadrows;
+ BlockNumber totalpages;
HeapTuple *rows;
PGRUsage ru0;
TimestampTz starttime = 0;
*************** do_analyze_rel(Relation onerel, VacuumSt
*** 279,295 ****
int save_sec_context;
int save_nestlevel;
- if (inh)
- ereport(elevel,
- (errmsg("analyzing \"%s.%s\" inheritance tree",
- get_namespace_name(RelationGetNamespace(onerel)),
- RelationGetRelationName(onerel))));
- else
- ereport(elevel,
- (errmsg("analyzing \"%s.%s\"",
- get_namespace_name(RelationGetNamespace(onerel)),
- RelationGetRelationName(onerel))));
-
/*
* Set up a working context so that we can easily free whatever junk gets
* created.
--- 323,328 ----
*************** do_analyze_rel(Relation onerel, VacuumSt
*** 447,457 ****
*/
rows = (HeapTuple *) palloc(targrows * sizeof(HeapTuple));
if (inh)
! numrows = acquire_inherited_sample_rows(onerel, rows, targrows,
! &totalrows, &totaldeadrows);
else
! numrows = acquire_sample_rows(onerel, rows, targrows,
! &totalrows, &totaldeadrows);
/*
* Compute the statistics. Temporary results during the calculations for
--- 480,492 ----
*/
rows = (HeapTuple *) palloc(targrows * sizeof(HeapTuple));
if (inh)
! numrows = acquirefunc(onerel, rows, targrows,
! &totalrows, &totaldeadrows,
! NULL, elevel);
else
! numrows = acquirefunc(onerel, rows, targrows,
! &totalrows, &totaldeadrows,
! &totalpages, elevel);
/*
* Compute the statistics. Temporary results during the calculations for
*************** do_analyze_rel(Relation onerel, VacuumSt
*** 532,538 ****
*/
if (!inh)
vac_update_relstats(onerel,
! RelationGetNumberOfBlocks(onerel),
totalrows,
visibilitymap_count(onerel),
hasindex,
--- 567,573 ----
*/
if (!inh)
vac_update_relstats(onerel,
! totalpages,
totalrows,
visibilitymap_count(onerel),
hasindex,
*************** BlockSampler_Next(BlockSampler bs)
*** 1015,1021 ****
*/
static int
acquire_sample_rows(Relation onerel, HeapTuple *rows, int targrows,
! double *totalrows, double *totaldeadrows)
{
int numrows = 0; /* # rows now in reservoir */
double samplerows = 0; /* total # rows collected */
--- 1050,1057 ----
*/
static int
acquire_sample_rows(Relation onerel, HeapTuple *rows, int targrows,
! double *totalrows, double *totaldeadrows,
! BlockNumber *totalpages, int elevel)
{
int numrows = 0; /* # rows now in reservoir */
double samplerows = 0; /* total # rows collected */
*************** acquire_sample_rows(Relation onerel, Hea
*** 1030,1035 ****
--- 1066,1073 ----
Assert(targrows > 0);
totalblocks = RelationGetNumberOfBlocks(onerel);
+ if (totalpages)
+ *totalpages = totalblocks;
/* Need a cutoff xmin for HeapTupleSatisfiesVacuum */
OldestXmin = GetOldestXmin(onerel->rd_rel->relisshared, true);
*************** acquire_sample_rows(Relation onerel, Hea
*** 1252,1258 ****
}
/* Select a random value R uniformly distributed in (0 - 1) */
! static double
random_fract(void)
{
return ((double) random() + 1) / ((double) MAX_RANDOM_VALUE + 2);
--- 1290,1296 ----
}
/* Select a random value R uniformly distributed in (0 - 1) */
! double
random_fract(void)
{
return ((double) random() + 1) / ((double) MAX_RANDOM_VALUE + 2);
*************** random_fract(void)
*** 1272,1285 ****
* determines the number of records to skip before the next record is
* processed.
*/
! static double
init_selection_state(int n)
{
/* Initial value of W (for use when Algorithm Z is first applied) */
return exp(-log(random_fract()) / n);
}
! static double
get_next_S(double t, int n, double *stateptr)
{
double S;
--- 1310,1323 ----
* determines the number of records to skip before the next record is
* processed.
*/
! double
init_selection_state(int n)
{
/* Initial value of W (for use when Algorithm Z is first applied) */
return exp(-log(random_fract()) / n);
}
! double
get_next_S(double t, int n, double *stateptr)
{
double S;
*************** compare_rows(const void *a, const void *
*** 1395,1401 ****
*/
static int
acquire_inherited_sample_rows(Relation onerel, HeapTuple *rows, int targrows,
! double *totalrows, double *totaldeadrows)
{
List *tableOIDs;
Relation *rels;
--- 1433,1440 ----
*/
static int
acquire_inherited_sample_rows(Relation onerel, HeapTuple *rows, int targrows,
! double *totalrows, double *totaldeadrows,
! BlockNumber *totalpages, int elevel)
{
List *tableOIDs;
Relation *rels;
*************** acquire_inherited_sample_rows(Relation o
*** 1458,1463 ****
--- 1497,1504 ----
totalblocks += relblocks[nrels];
nrels++;
}
+ if (totalpages)
+ *totalpages = totalblocks;
/*
* Now sample rows from each relation, proportionally to its fraction of
*************** acquire_inherited_sample_rows(Relation o
*** 1491,1497 ****
rows + numrows,
childtargrows,
&trows,
! &tdrows);
/* We may need to convert from child's rowtype to parent's */
if (childrows > 0 &&
--- 1532,1540 ----
rows + numrows,
childtargrows,
&trows,
! &tdrows,
! NULL,
! elevel);
/* We may need to convert from child's rowtype to parent's */
if (childrows > 0 &&
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 9853686..3031496 100644
*** a/src/backend/commands/tablecmds.c
--- b/src/backend/commands/tablecmds.c
*************** static void ATPrepSetStatistics(Relation
*** 320,325 ****
--- 320,327 ----
Node *newValue, LOCKMODE lockmode);
static void ATExecSetStatistics(Relation rel, const char *colName,
Node *newValue, LOCKMODE lockmode);
+ static void ATPrepSetOptions(Relation rel, const char *colName,
+ Node *options, LOCKMODE lockmode);
static void ATExecSetOptions(Relation rel, const char *colName,
Node *options, bool isReset, LOCKMODE lockmode);
static void ATExecSetStorage(Relation rel, const char *colName,
*************** ATPrepCmd(List **wqueue, Relation rel, A
*** 3021,3027 ****
break;
case AT_SetOptions: /* ALTER COLUMN SET ( options ) */
case AT_ResetOptions: /* ALTER COLUMN RESET ( options ) */
! ATSimplePermissions(rel, ATT_TABLE | ATT_INDEX);
/* This command never recurses */
pass = AT_PASS_MISC;
break;
--- 3023,3030 ----
break;
case AT_SetOptions: /* ALTER COLUMN SET ( options ) */
case AT_ResetOptions: /* ALTER COLUMN RESET ( options ) */
! ATSimplePermissions(rel, ATT_TABLE | ATT_INDEX | ATT_FOREIGN_TABLE);
! ATPrepSetOptions(rel, cmd->name, cmd->def, lockmode);
/* This command never recurses */
pass = AT_PASS_MISC;
break;
*************** ATPrepSetStatistics(Relation rel, const
*** 4999,5008 ****
* allowSystemTableMods to be turned on.
*/
if (rel->rd_rel->relkind != RELKIND_RELATION &&
! rel->rd_rel->relkind != RELKIND_INDEX)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
! errmsg("\"%s\" is not a table or index",
RelationGetRelationName(rel))));
/* Permissions checks */
--- 5002,5012 ----
* allowSystemTableMods to be turned on.
*/
if (rel->rd_rel->relkind != RELKIND_RELATION &&
! rel->rd_rel->relkind != RELKIND_INDEX &&
! rel->rd_rel->relkind != RELKIND_FOREIGN_TABLE)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
! errmsg("\"%s\" is not a table, index or foreign table",
RelationGetRelationName(rel))));
/* Permissions checks */
*************** ATExecSetStatistics(Relation rel, const
*** 5071,5076 ****
--- 5075,5100 ----
}
static void
+ ATPrepSetOptions(Relation rel, const char *colName, Node *options,
+ LOCKMODE lockmode)
+ {
+ if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+ {
+ ListCell *cell;
+
+ foreach(cell, (List *) options)
+ {
+ DefElem *def = (DefElem *) lfirst(cell);
+
+ if (pg_strcasecmp(def->defname, "n_distinct_inherited") == 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("cannot set \"n_distinct_inherited\" for foreign tables")));
+ }
+ }
+ }
+
+ static void
ATExecSetOptions(Relation rel, const char *colName, Node *options,
bool isReset, LOCKMODE lockmode)
{
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index dc2248b..b3d2078 100644
*** a/src/bin/psql/describe.c
--- b/src/bin/psql/describe.c
*************** describeOneTableDetails(const char *sche
*** 1104,1110 ****
bool printTableInitialized = false;
int i;
char *view_def = NULL;
! char *headers[6];
char **seq_values = NULL;
char **modifiers = NULL;
char **ptr;
--- 1104,1110 ----
bool printTableInitialized = false;
int i;
char *view_def = NULL;
! char *headers[7];
char **seq_values = NULL;
char **modifiers = NULL;
char **ptr;
*************** describeOneTableDetails(const char *sche
*** 1395,1401 ****
if (verbose)
{
headers[cols++] = gettext_noop("Storage");
! if (tableinfo.relkind == 'r')
headers[cols++] = gettext_noop("Stats target");
/* Column comments, if the relkind supports this feature. */
if (tableinfo.relkind == 'r' || tableinfo.relkind == 'v' ||
--- 1395,1401 ----
if (verbose)
{
headers[cols++] = gettext_noop("Storage");
! if (tableinfo.relkind == 'r' || tableinfo.relkind == 'f')
headers[cols++] = gettext_noop("Stats target");
/* Column comments, if the relkind supports this feature. */
if (tableinfo.relkind == 'r' || tableinfo.relkind == 'v' ||
*************** describeOneTableDetails(const char *sche
*** 1498,1504 ****
false, false);
/* Statistics target, if the relkind supports this feature */
! if (tableinfo.relkind == 'r')
{
printTableAddCell(&cont, PQgetvalue(res, i, firstvcol + 1),
false, false);
--- 1498,1504 ----
false, false);
/* Statistics target, if the relkind supports this feature */
! if (tableinfo.relkind == 'r' || tableinfo.relkind == 'f')
{
printTableAddCell(&cont, PQgetvalue(res, i, firstvcol + 1),
false, false);
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 975d655..d113adf 100644
*** a/src/bin/psql/tab-complete.c
--- b/src/bin/psql/tab-complete.c
*************** static const SchemaQuery Query_for_list_
*** 409,414 ****
--- 409,429 ----
NULL
};
+ static const SchemaQuery Query_for_list_of_tf = {
+ /* catname */
+ "pg_catalog.pg_class c",
+ /* selcondition */
+ "c.relkind IN ('r', 'f')",
+ /* viscondition */
+ "pg_catalog.pg_table_is_visible(c.oid)",
+ /* namespace */
+ "c.relnamespace",
+ /* result */
+ "pg_catalog.quote_ident(c.relname)",
+ /* qualresult */
+ NULL
+ };
+
static const SchemaQuery Query_for_list_of_views = {
/* catname */
"pg_catalog.pg_class c",
*************** psql_completion(char *text, int start, i
*** 2833,2839 ****
/* ANALYZE */
/* If the previous word is ANALYZE, produce list of tables */
else if (pg_strcasecmp(prev_wd, "ANALYZE") == 0)
! COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables, NULL);
/* WHERE */
/* Simple case of the word before the where being the table name */
--- 2848,2854 ----
/* ANALYZE */
/* If the previous word is ANALYZE, produce list of tables */
else if (pg_strcasecmp(prev_wd, "ANALYZE") == 0)
! COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tf, NULL);
/* WHERE */
/* Simple case of the word before the where being the table name */
diff --git a/src/include/commands/vacuum.h b/src/include/commands/vacuum.h
index 3deee66..3e89e82 100644
*** a/src/include/commands/vacuum.h
--- b/src/include/commands/vacuum.h
*************** extern void lazy_vacuum_rel(Relation one
*** 170,174 ****
--- 170,183 ----
extern void analyze_rel(Oid relid, VacuumStmt *vacstmt,
BufferAccessStrategy bstrategy);
extern bool std_typanalyze(VacAttrStats *stats);
+ typedef int (*SampleRowAcquireFunc) (Relation onerel, HeapTuple *rows,
+ int targrows, double *totalrows,
+ double *totaldeadrows,
+ BlockNumber *totalpages, int elevel);
+ extern void do_analyze_rel(Relation onerel, VacuumStmt *vacstmt, int elevel,
+ bool inh, SampleRowAcquireFunc acquirefunc);
+ extern double random_fract(void);
+ extern double init_selection_state(int n);
+ extern double get_next_S(double t, int n, double *stateptr);
#endif /* VACUUM_H */
diff --git a/src/include/foreign/fdwapi.h b/src/include/foreign/fdwapi.h
index 854f177..d7181c7 100644
*** a/src/include/foreign/fdwapi.h
--- b/src/include/foreign/fdwapi.h
***************
*** 12,19 ****
--- 12,21 ----
#ifndef FDWAPI_H
#define FDWAPI_H
+ #include "foreign/foreign.h"
#include "nodes/execnodes.h"
#include "nodes/relation.h"
+ #include "utils/rel.h"
/* To avoid including explain.h here, reference ExplainState thus: */
struct ExplainState;
*************** typedef void (*ReScanForeignScan_functio
*** 50,55 ****
--- 52,60 ----
typedef void (*EndForeignScan_function) (ForeignScanState *node);
+ typedef void (*AnalyzeForeignTable_function) (Relation relation,
+ VacuumStmt *vacstmt,
+ int elevel);
/*
* FdwRoutine is the struct returned by a foreign-data wrapper's handler
*************** typedef struct FdwRoutine
*** 64,69 ****
--- 69,78 ----
{
NodeTag type;
+ /*
+ * These handlers are required to execute a scan on a foreign table. If
+ * any of them was NULL, scans on a foreign table managed by such FDW fail.
+ */
GetForeignRelSize_function GetForeignRelSize;
GetForeignPaths_function GetForeignPaths;
GetForeignPlan_function GetForeignPlan;
*************** typedef struct FdwRoutine
*** 72,77 ****
--- 81,92 ----
IterateForeignScan_function IterateForeignScan;
ReScanForeignScan_function ReScanForeignScan;
EndForeignScan_function EndForeignScan;
+
+ /*
+ * Handlers below are optional. You can set any of them to NULL to tell
+ * PostgreSQL backend that the FDW doesn't have the capability.
+ */
+ AnalyzeForeignTable_function AnalyzeForeignTable;
} FdwRoutine;
diff --git a/src/test/regress/expected/foreign_data.out b/src/test/regress/expected/foreign_data.out
index ba86883..f1379a6 100644
*** a/src/test/regress/expected/foreign_data.out
--- b/src/test/regress/expected/foreign_data.out
*************** CREATE FOREIGN TABLE ft1 (
*** 679,690 ****
COMMENT ON FOREIGN TABLE ft1 IS 'ft1';
COMMENT ON COLUMN ft1.c1 IS 'ft1.c1';
\d+ ft1
! Foreign table "public.ft1"
! Column | Type | Modifiers | FDW Options | Storage | Description
! --------+---------+-----------+--------------------------------+----------+-------------
! c1 | integer | not null | ("param 1" 'val1') | plain | ft1.c1
! c2 | text | | (param2 'val2', param3 'val3') | extended |
! c3 | date | | | plain |
Server: s0
FDW Options: (delimiter ',', quote '"', "be quoted" 'value')
Has OIDs: no
--- 679,690 ----
COMMENT ON FOREIGN TABLE ft1 IS 'ft1';
COMMENT ON COLUMN ft1.c1 IS 'ft1.c1';
\d+ ft1
! Foreign table "public.ft1"
! Column | Type | Modifiers | FDW Options | Storage | Stats target | Description
! --------+---------+-----------+--------------------------------+----------+--------------+-------------
! c1 | integer | not null | ("param 1" 'val1') | plain | | ft1.c1
! c2 | text | | (param2 'val2', param3 'val3') | extended | |
! c3 | date | | | plain | |
Server: s0
FDW Options: (delimiter ',', quote '"', "be quoted" 'value')
Has OIDs: no
*************** ERROR: cannot alter system column "xmin
*** 730,748 ****
ALTER FOREIGN TABLE ft1 ALTER COLUMN c7 OPTIONS (ADD p1 'v1', ADD p2 'v2'),
ALTER COLUMN c8 OPTIONS (ADD p1 'v1', ADD p2 'v2');
ALTER FOREIGN TABLE ft1 ALTER COLUMN c8 OPTIONS (SET p2 'V2', DROP p1);
\d+ ft1
! Foreign table "public.ft1"
! Column | Type | Modifiers | FDW Options | Storage | Description
! --------+---------+-----------+--------------------------------+----------+-------------
! c1 | integer | not null | ("param 1" 'val1') | plain |
! c2 | text | | (param2 'val2', param3 'val3') | extended |
! c3 | date | | | plain |
! c4 | integer | | | plain |
! c6 | integer | not null | | plain |
! c7 | integer | | (p1 'v1', p2 'v2') | plain |
! c8 | text | | (p2 'V2') | extended |
! c9 | integer | | | plain |
! c10 | integer | | (p1 'v1') | plain |
Server: s0
FDW Options: (delimiter ',', quote '"', "be quoted" 'value')
Has OIDs: no
--- 730,753 ----
ALTER FOREIGN TABLE ft1 ALTER COLUMN c7 OPTIONS (ADD p1 'v1', ADD p2 'v2'),
ALTER COLUMN c8 OPTIONS (ADD p1 'v1', ADD p2 'v2');
ALTER FOREIGN TABLE ft1 ALTER COLUMN c8 OPTIONS (SET p2 'V2', DROP p1);
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 SET STATISTICS 10000;
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 SET (n_distinct = 100);
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 SET (n_distinct_inherited = 100); -- ERROR
+ ERROR: cannot set "n_distinct_inherited" for foreign tables
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c8 SET STATISTICS -1;
\d+ ft1
! Foreign table "public.ft1"
! Column | Type | Modifiers | FDW Options | Storage | Stats target | Description
! --------+---------+-----------+--------------------------------+----------+--------------+-------------
! c1 | integer | not null | ("param 1" 'val1') | plain | 10000 |
! c2 | text | | (param2 'val2', param3 'val3') | extended | |
! c3 | date | | | plain | |
! c4 | integer | | | plain | |
! c6 | integer | not null | | plain | |
! c7 | integer | | (p1 'v1', p2 'v2') | plain | |
! c8 | text | | (p2 'V2') | extended | |
! c9 | integer | | | plain | |
! c10 | integer | | (p1 'v1') | plain | |
Server: s0
FDW Options: (delimiter ',', quote '"', "be quoted" 'value')
Has OIDs: no
diff --git a/src/test/regress/sql/foreign_data.sql b/src/test/regress/sql/foreign_data.sql
index 0c95672..03b5680 100644
*** a/src/test/regress/sql/foreign_data.sql
--- b/src/test/regress/sql/foreign_data.sql
*************** ALTER FOREIGN TABLE ft1 ALTER COLUMN xmi
*** 307,312 ****
--- 307,316 ----
ALTER FOREIGN TABLE ft1 ALTER COLUMN c7 OPTIONS (ADD p1 'v1', ADD p2 'v2'),
ALTER COLUMN c8 OPTIONS (ADD p1 'v1', ADD p2 'v2');
ALTER FOREIGN TABLE ft1 ALTER COLUMN c8 OPTIONS (SET p2 'V2', DROP p1);
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 SET STATISTICS 10000;
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 SET (n_distinct = 100);
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 SET (n_distinct_inherited = 100); -- ERROR
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c8 SET STATISTICS -1;
\d+ ft1
-- can't change the column type if it's used elsewhere
CREATE TABLE use_ft1_column_type (x ft1);
Shigeru HANADA wrote:
During a foreign scan, type input functions are used to convert
the text representation of values. If a foreign table is
misconfigured,
you can get error messages from these functions, like:
ERROR: invalid input syntax for type double precision: "etwas"
or
ERROR: value too long for type character varying(3)It might me nice for finding problems if the message were
something like:ERROR: cannot convert data in foreign scan of "tablename", column
"col"
in row 42
DETAIL: ERROR: value too long for type character varying(3)Agreed. How about showing context information with errcontext() in
addition to main error message? Of course, identifiers are quoted if
necessary. This way doesn't need additional PG_TRY block, so overhead
would be relatively cheap.postgres=# SELECT * FROM ft1 WHERE c1 = 1; -- ERROR
ERROR: invalid input syntax for integer: "1970-01-02 17:00:00+09"
CONTEXT: column c4 of foreign table ft1Showing index of the row seems overkill, because most cause of this
kind
of error is wrong configuration, as you say, and users would be able
to
address the issue without knowing which record caused the error.
Agreed. I think that is a better approach than what I suggested.
As stated previously, I don't think that using local stats on
foreign tables is a win. The other patches work fine for me, and
I'd be happy if that could go into 9.2.I have opposite opinion on this issue because we need to do some of
filtering on local side. We can leave cost/rows estimation to remote
side about WHERE expressions which are pushed down, but we need
selectivity of extra filtering done on local side. For such purpose,
having local stats of foreign data seems reasonable and useful.Of course, it has downside that we need to execute explicit ANALYZE
for
foreign tables which would cause full sequential scan on remote
tables,
in addition to ANALYZE for remote tables done on remote side as usual
maintenance work.
This approach is much better and does not suffer from the
limitations the original analyze patch had.
I think that the price of a remote table scan is something
we should be willing to pay for good local statistics.
And there is always the option not to analyze the foreign
table if you are not willing to pay that price.
Maybe the FDW API could be extended so that foreign data wrappers
can provide a random sample to avoid a full table scan.
Attached patch contains changes below:
pgsql_fdw_v19.patch
- show context of data conversion error
- move codes for fetch_count FDW option to option.c
(refactoring)
pgsql_fdw_pushdown_v12.patch
- make deparseExpr function static (refactoring)I also attached pgsql_fdw_analyze for only testing the effect of local
statistics. It contains both backend's ANALYZE command support and
pgsql_fdw's ANALYZE support.
I think the idea is promising.
I'll mark the patch as "ready for committer".
Yours,
Laurenz Albe
At 22:11 12/03/28 +0900, Shigeru HANADA wrote:
ANALYZE support for foreign tables is proposed by Fujita-san in current
CF, so I'd like to push it.
I updated the patch to the latest HEAD. Please find attached a patch.
Best regards,
Etsuro Fujita
Attachments:
postgresql-analyze-v7.patchapplication/octet-stream; name=postgresql-analyze-v7.patchDownload
diff --git a/contrib/file_fdw/file_fdw.c b/contrib/file_fdw/file_fdw.c
index e890770..5e3c06f 100644
--- a/contrib/file_fdw/file_fdw.c
+++ b/contrib/file_fdw/file_fdw.c
@@ -20,6 +20,7 @@
#include "commands/copy.h"
#include "commands/defrem.h"
#include "commands/explain.h"
+#include "commands/vacuum.h"
#include "foreign/fdwapi.h"
#include "foreign/foreign.h"
#include "miscadmin.h"
@@ -123,6 +124,9 @@ static void fileBeginForeignScan(ForeignScanState *node, int eflags);
static TupleTableSlot *fileIterateForeignScan(ForeignScanState *node);
static void fileReScanForeignScan(ForeignScanState *node);
static void fileEndForeignScan(ForeignScanState *node);
+static void fileAnalyzeForeignTable(Relation onerel,
+ VacuumStmt *vacstmt,
+ int elevel);
/*
* Helper functions
@@ -136,6 +140,9 @@ static void estimate_size(PlannerInfo *root, RelOptInfo *baserel,
static void estimate_costs(PlannerInfo *root, RelOptInfo *baserel,
FileFdwPlanState *fdw_private,
Cost *startup_cost, Cost *total_cost);
+static int acquire_sample_rows(Relation onerel, HeapTuple *rows, int targrows,
+ double *totalrows, double *totaldeadrows,
+ BlockNumber *totalpages, int elevel);
/*
@@ -155,6 +162,7 @@ file_fdw_handler(PG_FUNCTION_ARGS)
fdwroutine->IterateForeignScan = fileIterateForeignScan;
fdwroutine->ReScanForeignScan = fileReScanForeignScan;
fdwroutine->EndForeignScan = fileEndForeignScan;
+ fdwroutine->AnalyzeForeignTable = fileAnalyzeForeignTable;
PG_RETURN_POINTER(fdwroutine);
}
@@ -645,6 +653,18 @@ fileReScanForeignScan(ForeignScanState *node)
}
/*
+ * fileAnalyzeForeignTable
+ * Analyze foreign table
+ */
+static void
+fileAnalyzeForeignTable(Relation onerel, VacuumStmt *vacstmt, int elevel)
+{
+ do_analyze_rel(onerel, vacstmt,
+ elevel, false,
+ acquire_sample_rows);
+}
+
+/*
* Estimate size of a foreign table.
*
* The main result is returned in baserel->rows. We also set
@@ -657,7 +677,6 @@ estimate_size(PlannerInfo *root, RelOptInfo *baserel,
{
struct stat stat_buf;
BlockNumber pages;
- int tuple_width;
double ntuples;
double nrows;
@@ -677,16 +696,31 @@ estimate_size(PlannerInfo *root, RelOptInfo *baserel,
fdw_private->pages = pages;
- /*
- * Estimate the number of tuples in the file. We back into this estimate
- * using the planner's idea of the relation width; which is bogus if not
- * all columns are being read, not to mention that the text representation
- * of a row probably isn't the same size as its internal representation.
- * FIXME later.
- */
- tuple_width = MAXALIGN(baserel->width) + MAXALIGN(sizeof(HeapTupleHeaderData));
+ if (baserel->pages > 0)
+ {
+ double density;
- ntuples = clamp_row_est((double) stat_buf.st_size / (double) tuple_width);
+ density = baserel->tuples / (double) baserel->pages;
+
+ ntuples = clamp_row_est(density * (double) pages);
+ }
+ else
+ {
+ int tuple_width;
+
+ /*
+ * Estimate the number of tuples in the file. We back into this
+ * estimate using the planner's idea of the relation width; which is
+ * bogus if not all columns are being read, not to mention that the text
+ * representation of a row probably isn't the same size as its internal
+ * representation. FIXME later.
+ */
+ tuple_width = MAXALIGN(baserel->width) +
+ MAXALIGN(sizeof(HeapTupleHeaderData));
+
+ ntuples = clamp_row_est((double) stat_buf.st_size /
+ (double) tuple_width);
+ }
fdw_private->ntuples = ntuples;
@@ -736,3 +770,156 @@ estimate_costs(PlannerInfo *root, RelOptInfo *baserel,
run_cost += cpu_per_tuple * ntuples;
*total_cost = *startup_cost + run_cost;
}
+
+/*
+ * acquire_sample_rows -- acquire a random sample of rows from the table
+ *
+ * Selected rows are returned in the caller-allocated array rows[], which
+ * must have at least targrows entries.
+ * The actual number of rows selected is returned as the function result.
+ * We also count the total number of rows in the file, and return it into
+ * *totalrows. Note that *totaldeadrows is always set to 0.
+ *
+ * Note that the returned list of rows is not always in order by physical
+ * position in the file. Therefore, correlation estimates derived later
+ * may be meaningless, but it's OK because we don't use the estimates for
+ * now.
+ */
+static int
+acquire_sample_rows(Relation onerel, HeapTuple *rows, int targrows,
+ double *totalrows, double *totaldeadrows,
+ BlockNumber *totalpages, int elevel)
+{
+ int numrows = 0;
+ double rowstoskip = -1; /* -1 means not set yet */
+ double rstate;
+ HeapTuple tuple;
+ TupleDesc tupDesc;
+ Datum *values;
+ bool *nulls;
+ bool found;
+ char *filename;
+ struct stat stat_buf;
+ List *options;
+ CopyState cstate;
+ ErrorContextCallback errcontext;
+
+ Assert(onerel);
+ Assert(targrows > 0);
+
+ tupDesc = RelationGetDescr(onerel);
+ values = (Datum *) palloc(tupDesc->natts * sizeof(Datum));
+ nulls = (bool *) palloc(tupDesc->natts * sizeof(bool));
+
+ /* Fetch options of foreign table */
+ fileGetOptions(RelationGetRelid(onerel), &filename, &options);
+
+ /*
+ * Get size of the file.
+ */
+ if (stat(filename, &stat_buf) < 0)
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not stat file \"%s\": %m",
+ filename)));
+
+ /*
+ * Convert size to pages for use in I/O cost estimate.
+ */
+ *totalpages = (stat_buf.st_size + (BLCKSZ - 1)) / BLCKSZ;
+ if (*totalpages < 1)
+ *totalpages = 1;
+
+ /*
+ * Create CopyState from FDW options. We always acquire all columns, so
+ * as to match the expected ScanTupleSlot signature.
+ */
+ cstate = BeginCopyFrom(onerel, filename, NIL, options);
+
+ /* Prepare for sampling rows */
+ rstate = init_selection_state(targrows);
+
+ /* Set up callback to identify error line number. */
+ errcontext.callback = CopyFromErrorCallback;
+ errcontext.arg = (void *) cstate;
+ errcontext.previous = error_context_stack;
+ error_context_stack = &errcontext;
+
+ *totalrows = 0;
+ for (;;)
+ {
+ /*
+ * Check for user-requested abort.
+ */
+ CHECK_FOR_INTERRUPTS();
+
+ found = NextCopyFrom(cstate, NULL, values, nulls, NULL);
+ if (!found)
+ break;
+
+ tuple = heap_form_tuple(tupDesc, values, nulls);
+
+ /*
+ * The first targrows sample rows are simply copied into the reservoir.
+ * Then we start replacing tuples in the sample until we reach the end
+ * of the relation. This algorithm is from Jeff Vitter's paper (see
+ * full citation below). It works by repeatedly computing the number of
+ * tuples to skip before selecting a tuple, which replaces a randomly
+ * chosen element of the reservoir (current set of tuples).
+ * At all times the reservoir is a true random sample of the tuples
+ * we've passed over so far, so when we fall off the end of the relation
+ * we're done.
+ */
+ if (numrows < targrows)
+ rows[numrows++] = heap_copytuple(tuple);
+ else
+ {
+ /*
+ * t in Vitter's paper is the number of records already
+ * processed. If we need to compute a new S value, we
+ * must use the not-yet-incremented value of samplerows as
+ * t.
+ */
+ if (rowstoskip < 0)
+ rowstoskip = get_next_S(*totalrows, targrows, &rstate);
+
+ if (rowstoskip <= 0)
+ {
+ /*
+ * Found a suitable tuple, so save it, replacing one
+ * old tuple at random
+ */
+ int k = (int) (targrows * random_fract());
+
+ Assert(k >= 0 && k < targrows);
+ heap_freetuple(rows[k]);
+ rows[k] = heap_copytuple(tuple);
+ }
+
+ rowstoskip -= 1;
+ }
+
+ *totalrows += 1;
+ heap_freetuple(tuple);
+ }
+ *totaldeadrows = 0;
+
+ /* Remove error callback. */
+ error_context_stack = errcontext.previous;
+
+ EndCopyFrom(cstate);
+
+ pfree(values);
+ pfree(nulls);
+
+ /*
+ * Emit some interesting relation info
+ */
+ ereport(elevel,
+ (errmsg("\"%s\": scanned, "
+ "%d rows in sample, %d total rows",
+ RelationGetRelationName(onerel),
+ numrows, (int) *totalrows)));
+
+ return numrows;
+}
diff --git a/contrib/file_fdw/input/file_fdw.source b/contrib/file_fdw/input/file_fdw.source
index 8e3d553..21b6fb4 100644
--- a/contrib/file_fdw/input/file_fdw.source
+++ b/contrib/file_fdw/input/file_fdw.source
@@ -111,6 +111,11 @@ EXECUTE st(100);
EXECUTE st(100);
DEALLOCATE st;
+-- statistics collection tests
+ANALYZE agg_csv;
+SELECT relpages, reltuples FROM pg_class WHERE relname = 'agg_csv';
+SELECT * FROM pg_stats WHERE tablename = 'agg_csv';
+
-- tableoid
SELECT tableoid::regclass, b FROM agg_csv;
diff --git a/contrib/file_fdw/output/file_fdw.source b/contrib/file_fdw/output/file_fdw.source
index 84f0750..c2daafe 100644
--- a/contrib/file_fdw/output/file_fdw.source
+++ b/contrib/file_fdw/output/file_fdw.source
@@ -174,6 +174,21 @@ EXECUTE st(100);
(1 row)
DEALLOCATE st;
+-- statistics collection tests
+ANALYZE agg_csv;
+SELECT relpages, reltuples FROM pg_class WHERE relname = 'agg_csv';
+ relpages | reltuples
+----------+-----------
+ 1 | 3
+(1 row)
+
+SELECT * FROM pg_stats WHERE tablename = 'agg_csv';
+ schemaname | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram
+------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+-------------------------+-------------+-------------------+------------------------+----------------------
+ public | agg_csv | a | f | 0 | 2 | -1 | | | {0,42,100} | -0.5 | | |
+ public | agg_csv | b | f | 0 | 4 | -1 | | | {0.09561,99.097,324.78} | 0.5 | | |
+(2 rows)
+
-- tableoid
SELECT tableoid::regclass, b FROM agg_csv;
tableoid | b
diff --git a/doc/src/sgml/fdwhandler.sgml b/doc/src/sgml/fdwhandler.sgml
index f7bf3d8..0a70b47 100644
--- a/doc/src/sgml/fdwhandler.sgml
+++ b/doc/src/sgml/fdwhandler.sgml
@@ -299,6 +299,30 @@ EndForeignScan (ForeignScanState *node);
<para>
<programlisting>
+void
+AnalyzeForeignTable (Relation onerel,
+ VacuumStmt *vacstmt,
+ int elevel);
+</programlisting>
+
+ Collect statistics on a foreign table and store the results in the
+ pg_class and pg_statistics system catalogs.
+ This is optional, and if implemented, called when <command>ANALYZE
+ </> command is run.
+ The statistics are used by the query planner in order to make good
+ choices of query plans.
+ The function can be implemented by writing a sampling function that
+ acquires a random sample of rows from an external data source and
+ then by calling <function>do_analyze_rel</>, where you should pass
+ the sampling function as an argument.
+ Or the function can be directly implemented to get the statistics
+ from an external data source, transform it if neccesary, and store
+ it in the pg_class and pg_statistics system catalogs.
+ The function must be set to NULL if it isn't implemented.
+ </para>
+
+ <para>
+<programlisting>
ForeignDataWrapper *
GetForeignDataWrapper(Oid fdwid);
</programlisting>
diff --git a/doc/src/sgml/maintenance.sgml b/doc/src/sgml/maintenance.sgml
index 93c3ff5..54d0838 100644
--- a/doc/src/sgml/maintenance.sgml
+++ b/doc/src/sgml/maintenance.sgml
@@ -284,6 +284,10 @@
<command>ANALYZE</> strictly as a function of the number of rows
inserted or updated; it has no knowledge of whether that will lead
to meaningful statistical changes.
+ Note that the autovacuum daemon does not issue <command>ANALYZE</>
+ commands on foreign tables. It is recommended to run manually-managed
+ <command>ANALYZE</> commands as needed, which typically are executed
+ according to a schedule by cron or Task Scheduler scripts.
</para>
<para>
diff --git a/doc/src/sgml/ref/alter_foreign_table.sgml b/doc/src/sgml/ref/alter_foreign_table.sgml
index c4cdaa8..3819007 100644
--- a/doc/src/sgml/ref/alter_foreign_table.sgml
+++ b/doc/src/sgml/ref/alter_foreign_table.sgml
@@ -36,6 +36,9 @@ ALTER FOREIGN TABLE [ IF EXISTS ] <replaceable class="PARAMETER">name</replaceab
DROP [ COLUMN ] [ IF EXISTS ] <replaceable class="PARAMETER">column</replaceable> [ RESTRICT | CASCADE ]
ALTER [ COLUMN ] <replaceable class="PARAMETER">column</replaceable> [ SET DATA ] TYPE <replaceable class="PARAMETER">type</replaceable>
ALTER [ COLUMN ] <replaceable class="PARAMETER">column</replaceable> { SET | DROP } NOT NULL
+ ALTER [ COLUMN ] <replaceable class="PARAMETER">column</replaceable> SET STATISTICS <replaceable class="PARAMETER">integer</replaceable>
+ ALTER [ COLUMN ] <replaceable class="PARAMETER">column</replaceable> SET ( <replaceable class="PARAMETER">attribute_option</replaceable> = <replaceable class="PARAMETER">value</replaceable> [, ... ] )
+ ALTER [ COLUMN ] <replaceable class="PARAMETER">column</replaceable> RESET ( <replaceable class="PARAMETER">attribute_option</replaceable> [, ... ] )
ALTER [ COLUMN ] <replaceable class="PARAMETER">column</replaceable> OPTIONS ( [ ADD | SET | DROP ] <replaceable class="PARAMETER">option</replaceable> ['<replaceable class="PARAMETER">value</replaceable>'] [, ... ])
OWNER TO <replaceable class="PARAMETER">new_owner</replaceable>
OPTIONS ( [ ADD | SET | DROP ] <replaceable class="PARAMETER">option</replaceable> ['<replaceable class="PARAMETER">value</replaceable>'] [, ... ])
@@ -104,6 +107,45 @@ ALTER FOREIGN TABLE [ IF EXISTS ] <replaceable class="PARAMETER">name</replaceab
</varlistentry>
<varlistentry>
+ <term><literal>SET STATISTICS</literal></term>
+ <listitem>
+ <para>
+ This form sets the per-column statistics-gathering target for
+ subsequent <xref linkend="sql-analyze"> operations.
+ The target can be set in the range 0 to 10000; alternatively,
+ set it to -1 to revert to using the system default statistics
+ target (<xref linkend="guc-default-statistics-target">).
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>SET ( <replaceable class="PARAMETER">attribute_option</replaceable> = <replaceable class="PARAMETER">value</replaceable> [, ... ] )</literal></term>
+ <term><literal>RESET ( <replaceable class="PARAMETER">attribute_option</replaceable> [, ... ] )</literal></term>
+ <listitem>
+ <para>
+ This form sets or resets a per-attribute option. Currently, the only
+ defined per-attribute option is <literal>n_distinct</>, which overrides
+ the number-of-distinct-values estimates made by subsequent
+ <xref linkend="sql-analyze"> operations. When set to a positive value,
+ <command>ANALYZE</> will assume that the column contains exactly the
+ specified number of distinct nonnull values. When set to a negative
+ value, which must be greater than or equal to -1, <command>ANALYZE</>
+ will assume that the number of distinct nonnull values in the column is
+ linear in the size of the foreign table; the exact count is to be computed
+ by multiplying the estimated foreign table size by the absolute value of
+ the given number. For example, a value of -1 implies that all values in
+ the column are distinct, while a value of -0.5 implies that each value
+ appears twice on the average. This can be useful when the size of the
+ foreign table changes over time, since the multiplication by the number of
+ rows in the foreign table is not performed until query planning time.
+ Specify a value of 0 to revert to estimating the number of distinct values
+ normally.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
<term><literal>OWNER</literal></term>
<listitem>
<para>
diff --git a/doc/src/sgml/ref/analyze.sgml b/doc/src/sgml/ref/analyze.sgml
index 8c9057b..524a1c9 100644
--- a/doc/src/sgml/ref/analyze.sgml
+++ b/doc/src/sgml/ref/analyze.sgml
@@ -39,9 +39,11 @@ ANALYZE [ VERBOSE ] [ <replaceable class="PARAMETER">table</replaceable> [ ( <re
<para>
With no parameter, <command>ANALYZE</command> examines every table in the
- current database. With a parameter, <command>ANALYZE</command> examines
- only that table. It is further possible to give a list of column names,
- in which case only the statistics for those columns are collected.
+ current database except for foreign tables. With a parameter, <command>
+ ANALYZE</command> examines only that table. For a foreign table, it is
+ necessary to specify the name of that table. It is further possible to
+ give a list of column names, in which case only the statistics for those
+ columns are collected.
</para>
</refsect1>
@@ -63,7 +65,8 @@ ANALYZE [ VERBOSE ] [ <replaceable class="PARAMETER">table</replaceable> [ ( <re
<listitem>
<para>
The name (possibly schema-qualified) of a specific table to
- analyze. Defaults to all tables in the current database.
+ analyze. Defaults to all tables in the current database except
+ for foreign tables.
</para>
</listitem>
</varlistentry>
@@ -137,7 +140,9 @@ ANALYZE [ VERBOSE ] [ <replaceable class="PARAMETER">table</replaceable> [ ( <re
In rare situations, this non-determinism will cause the planner's
choices of query plans to change after <command>ANALYZE</command> is run.
To avoid this, raise the amount of statistics collected by
- <command>ANALYZE</command>, as described below.
+ <command>ANALYZE</command>, as described below. Note that the time
+ needed to analyze on foreign tables depends on the implementation of
+ the foreign data wrapper via which such tables are attached.
</para>
<para>
diff --git a/src/backend/commands/analyze.c b/src/backend/commands/analyze.c
index 9cd6e67..442ee2f 100644
--- a/src/backend/commands/analyze.c
+++ b/src/backend/commands/analyze.c
@@ -23,6 +23,7 @@
#include "access/xact.h"
#include "catalog/index.h"
#include "catalog/indexing.h"
+#include "catalog/pg_class.h"
#include "catalog/pg_collation.h"
#include "catalog/pg_inherits_fn.h"
#include "catalog/pg_namespace.h"
@@ -30,6 +31,8 @@
#include "commands/tablecmds.h"
#include "commands/vacuum.h"
#include "executor/executor.h"
+#include "foreign/foreign.h"
+#include "foreign/fdwapi.h"
#include "miscadmin.h"
#include "nodes/nodeFuncs.h"
#include "parser/parse_oper.h"
@@ -78,14 +81,11 @@ typedef struct AnlIndexData
int default_statistics_target = 100;
/* A few variables that don't seem worth passing around as parameters */
-static int elevel = -1;
-
static MemoryContext anl_context = NULL;
static BufferAccessStrategy vac_strategy;
-static void do_analyze_rel(Relation onerel, VacuumStmt *vacstmt, bool inh);
static void BlockSampler_Init(BlockSampler bs, BlockNumber nblocks,
int samplesize);
static bool BlockSampler_HasMore(BlockSampler bs);
@@ -96,15 +96,15 @@ static void compute_index_stats(Relation onerel, double totalrows,
MemoryContext col_context);
static VacAttrStats *examine_attribute(Relation onerel, int attnum,
Node *index_expr);
-static int acquire_sample_rows(Relation onerel, HeapTuple *rows,
- int targrows, double *totalrows, double *totaldeadrows);
-static double random_fract(void);
-static double init_selection_state(int n);
-static double get_next_S(double t, int n, double *stateptr);
-static int compare_rows(const void *a, const void *b);
+static int acquire_sample_rows(Relation onerel,
+ HeapTuple *rows, int targrows,
+ double *totalrows, double *totaldeadrows,
+ BlockNumber *totalpages, int elevel);
static int acquire_inherited_sample_rows(Relation onerel,
HeapTuple *rows, int targrows,
- double *totalrows, double *totaldeadrows);
+ double *totalrows, double *totaldeadrows,
+ BlockNumber *totalpages, int elevel);
+static int compare_rows(const void *a, const void *b);
static void update_attstats(Oid relid, bool inh,
int natts, VacAttrStats **vacattrstats);
static Datum std_fetch_func(VacAttrStatsP stats, int rownum, bool *isNull);
@@ -117,7 +117,9 @@ static Datum ind_fetch_func(VacAttrStatsP stats, int rownum, bool *isNull);
void
analyze_rel(Oid relid, VacuumStmt *vacstmt, BufferAccessStrategy bstrategy)
{
+ int elevel;
Relation onerel;
+ FdwRoutine *fdwroutine;
/* Set up static variables */
if (vacstmt->options & VACOPT_VERBOSE)
@@ -182,10 +184,12 @@ analyze_rel(Oid relid, VacuumStmt *vacstmt, BufferAccessStrategy bstrategy)
}
/*
- * Check that it's a plain table; we used to do this in get_rel_oids() but
- * seems safer to check after we've locked the relation.
+ * Check that it's a plain table or foreign table; we used to do this
+ * in get_rel_oids() but seems safer to check after we've locked the
+ * relation.
*/
- if (onerel->rd_rel->relkind != RELKIND_RELATION)
+ if (onerel->rd_rel->relkind != RELKIND_RELATION &&
+ onerel->rd_rel->relkind != RELKIND_FOREIGN_TABLE)
{
/* No need for a WARNING if we already complained during VACUUM */
if (!(vacstmt->options & VACOPT_VACUUM))
@@ -209,7 +213,9 @@ analyze_rel(Oid relid, VacuumStmt *vacstmt, BufferAccessStrategy bstrategy)
}
/*
- * We can ANALYZE any table except pg_statistic. See update_attstats
+ * We can ANALYZE any table except pg_statistic. See update_attstats.
+ * A foreign table can be ANALYZEed if the AnalyzeForeignTable callback
+ * routine is provided.
*/
if (RelationGetRelid(onerel) == StatisticRelationId)
{
@@ -217,6 +223,20 @@ analyze_rel(Oid relid, VacuumStmt *vacstmt, BufferAccessStrategy bstrategy)
return;
}
+ if (onerel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+ {
+ fdwroutine = GetFdwRoutineByRelId(RelationGetRelid(onerel));
+
+ if (fdwroutine->AnalyzeForeignTable == NULL)
+ {
+ ereport(WARNING,
+ (errmsg("skipping \"%s\" --- underlying foreign-data wrapper cannot analyze it",
+ RelationGetRelationName(onerel))));
+ relation_close(onerel, ShareUpdateExclusiveLock);
+ return;
+ }
+ }
+
/*
* OK, let's do it. First let other backends know I'm in ANALYZE.
*/
@@ -227,13 +247,32 @@ analyze_rel(Oid relid, VacuumStmt *vacstmt, BufferAccessStrategy bstrategy)
/*
* Do the normal non-recursive ANALYZE.
*/
- do_analyze_rel(onerel, vacstmt, false);
+ ereport(elevel,
+ (errmsg("analyzing \"%s.%s\"",
+ get_namespace_name(RelationGetNamespace(onerel)),
+ RelationGetRelationName(onerel))));
+
+ if (onerel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+ fdwroutine->AnalyzeForeignTable(onerel, vacstmt, elevel);
+ else
+ do_analyze_rel(onerel, vacstmt, elevel, false, acquire_sample_rows);
/*
* If there are child tables, do recursive ANALYZE.
*/
if (onerel->rd_rel->relhassubclass)
- do_analyze_rel(onerel, vacstmt, true);
+ {
+ Assert(onerel->rd_rel->relkind != RELKIND_FOREIGN_TABLE);
+
+ ereport(elevel,
+ (errmsg("analyzing \"%s.%s\" inheritance tree",
+ get_namespace_name(RelationGetNamespace(onerel)),
+ RelationGetRelationName(onerel))));
+
+ do_analyze_rel(onerel, vacstmt, elevel, true,
+ acquire_inherited_sample_rows);
+ }
+
/*
* Close source relation now, but keep lock so that no one deletes it
@@ -255,8 +294,9 @@ analyze_rel(Oid relid, VacuumStmt *vacstmt, BufferAccessStrategy bstrategy)
/*
* do_analyze_rel() -- analyze one relation, recursively or not
*/
-static void
-do_analyze_rel(Relation onerel, VacuumStmt *vacstmt, bool inh)
+void
+do_analyze_rel(Relation onerel, VacuumStmt *vacstmt, int elevel,
+ bool inh, AcquireSampleRowFunc acquirefunc)
{
int attr_cnt,
tcnt,
@@ -271,6 +311,7 @@ do_analyze_rel(Relation onerel, VacuumStmt *vacstmt, bool inh)
numrows;
double totalrows,
totaldeadrows;
+ BlockNumber totalpages;
HeapTuple *rows;
PGRUsage ru0;
TimestampTz starttime = 0;
@@ -279,17 +320,6 @@ do_analyze_rel(Relation onerel, VacuumStmt *vacstmt, bool inh)
int save_sec_context;
int save_nestlevel;
- if (inh)
- ereport(elevel,
- (errmsg("analyzing \"%s.%s\" inheritance tree",
- get_namespace_name(RelationGetNamespace(onerel)),
- RelationGetRelationName(onerel))));
- else
- ereport(elevel,
- (errmsg("analyzing \"%s.%s\"",
- get_namespace_name(RelationGetNamespace(onerel)),
- RelationGetRelationName(onerel))));
-
/*
* Set up a working context so that we can easily free whatever junk gets
* created.
@@ -447,11 +477,13 @@ do_analyze_rel(Relation onerel, VacuumStmt *vacstmt, bool inh)
*/
rows = (HeapTuple *) palloc(targrows * sizeof(HeapTuple));
if (inh)
- numrows = acquire_inherited_sample_rows(onerel, rows, targrows,
- &totalrows, &totaldeadrows);
+ numrows = acquirefunc(onerel, rows, targrows,
+ &totalrows, &totaldeadrows,
+ NULL, elevel);
else
- numrows = acquire_sample_rows(onerel, rows, targrows,
- &totalrows, &totaldeadrows);
+ numrows = acquirefunc(onerel, rows, targrows,
+ &totalrows, &totaldeadrows,
+ &totalpages, elevel);
/*
* Compute the statistics. Temporary results during the calculations for
@@ -532,7 +564,7 @@ do_analyze_rel(Relation onerel, VacuumStmt *vacstmt, bool inh)
*/
if (!inh)
vac_update_relstats(onerel,
- RelationGetNumberOfBlocks(onerel),
+ totalpages,
totalrows,
visibilitymap_count(onerel),
hasindex,
@@ -1015,7 +1047,8 @@ BlockSampler_Next(BlockSampler bs)
*/
static int
acquire_sample_rows(Relation onerel, HeapTuple *rows, int targrows,
- double *totalrows, double *totaldeadrows)
+ double *totalrows, double *totaldeadrows,
+ BlockNumber *totalpages, int elevel)
{
int numrows = 0; /* # rows now in reservoir */
double samplerows = 0; /* total # rows collected */
@@ -1030,6 +1063,8 @@ acquire_sample_rows(Relation onerel, HeapTuple *rows, int targrows,
Assert(targrows > 0);
totalblocks = RelationGetNumberOfBlocks(onerel);
+ if (totalpages)
+ *totalpages = totalblocks;
/* Need a cutoff xmin for HeapTupleSatisfiesVacuum */
OldestXmin = GetOldestXmin(onerel->rd_rel->relisshared, true);
@@ -1252,7 +1287,7 @@ acquire_sample_rows(Relation onerel, HeapTuple *rows, int targrows,
}
/* Select a random value R uniformly distributed in (0 - 1) */
-static double
+double
random_fract(void)
{
return ((double) random() + 1) / ((double) MAX_RANDOM_VALUE + 2);
@@ -1272,14 +1307,14 @@ random_fract(void)
* determines the number of records to skip before the next record is
* processed.
*/
-static double
+double
init_selection_state(int n)
{
/* Initial value of W (for use when Algorithm Z is first applied) */
return exp(-log(random_fract()) / n);
}
-static double
+double
get_next_S(double t, int n, double *stateptr)
{
double S;
@@ -1395,7 +1430,8 @@ compare_rows(const void *a, const void *b)
*/
static int
acquire_inherited_sample_rows(Relation onerel, HeapTuple *rows, int targrows,
- double *totalrows, double *totaldeadrows)
+ double *totalrows, double *totaldeadrows,
+ BlockNumber *totalpages, int elevel)
{
List *tableOIDs;
Relation *rels;
@@ -1458,6 +1494,8 @@ acquire_inherited_sample_rows(Relation onerel, HeapTuple *rows, int targrows,
totalblocks += relblocks[nrels];
nrels++;
}
+ if (totalpages)
+ *totalpages = totalblocks;
/*
* Now sample rows from each relation, proportionally to its fraction of
@@ -1491,7 +1529,9 @@ acquire_inherited_sample_rows(Relation onerel, HeapTuple *rows, int targrows,
rows + numrows,
childtargrows,
&trows,
- &tdrows);
+ &tdrows,
+ NULL,
+ elevel);
/* We may need to convert from child's rowtype to parent's */
if (childrows > 0 &&
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 9853686..3031496 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -320,6 +320,8 @@ static void ATPrepSetStatistics(Relation rel, const char *colName,
Node *newValue, LOCKMODE lockmode);
static void ATExecSetStatistics(Relation rel, const char *colName,
Node *newValue, LOCKMODE lockmode);
+static void ATPrepSetOptions(Relation rel, const char *colName,
+ Node *options, LOCKMODE lockmode);
static void ATExecSetOptions(Relation rel, const char *colName,
Node *options, bool isReset, LOCKMODE lockmode);
static void ATExecSetStorage(Relation rel, const char *colName,
@@ -3021,7 +3023,8 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
break;
case AT_SetOptions: /* ALTER COLUMN SET ( options ) */
case AT_ResetOptions: /* ALTER COLUMN RESET ( options ) */
- ATSimplePermissions(rel, ATT_TABLE | ATT_INDEX);
+ ATSimplePermissions(rel, ATT_TABLE | ATT_INDEX | ATT_FOREIGN_TABLE);
+ ATPrepSetOptions(rel, cmd->name, cmd->def, lockmode);
/* This command never recurses */
pass = AT_PASS_MISC;
break;
@@ -4999,10 +5002,11 @@ ATPrepSetStatistics(Relation rel, const char *colName, Node *newValue, LOCKMODE
* allowSystemTableMods to be turned on.
*/
if (rel->rd_rel->relkind != RELKIND_RELATION &&
- rel->rd_rel->relkind != RELKIND_INDEX)
+ rel->rd_rel->relkind != RELKIND_INDEX &&
+ rel->rd_rel->relkind != RELKIND_FOREIGN_TABLE)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
- errmsg("\"%s\" is not a table or index",
+ errmsg("\"%s\" is not a table, index or foreign table",
RelationGetRelationName(rel))));
/* Permissions checks */
@@ -5071,6 +5075,26 @@ ATExecSetStatistics(Relation rel, const char *colName, Node *newValue, LOCKMODE
}
static void
+ATPrepSetOptions(Relation rel, const char *colName, Node *options,
+ LOCKMODE lockmode)
+{
+ if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+ {
+ ListCell *cell;
+
+ foreach(cell, (List *) options)
+ {
+ DefElem *def = (DefElem *) lfirst(cell);
+
+ if (pg_strcasecmp(def->defname, "n_distinct_inherited") == 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("cannot set \"n_distinct_inherited\" for foreign tables")));
+ }
+ }
+}
+
+static void
ATExecSetOptions(Relation rel, const char *colName, Node *options,
bool isReset, LOCKMODE lockmode)
{
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index dc2248b..b3d2078 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -1104,7 +1104,7 @@ describeOneTableDetails(const char *schemaname,
bool printTableInitialized = false;
int i;
char *view_def = NULL;
- char *headers[6];
+ char *headers[7];
char **seq_values = NULL;
char **modifiers = NULL;
char **ptr;
@@ -1395,7 +1395,7 @@ describeOneTableDetails(const char *schemaname,
if (verbose)
{
headers[cols++] = gettext_noop("Storage");
- if (tableinfo.relkind == 'r')
+ if (tableinfo.relkind == 'r' || tableinfo.relkind == 'f')
headers[cols++] = gettext_noop("Stats target");
/* Column comments, if the relkind supports this feature. */
if (tableinfo.relkind == 'r' || tableinfo.relkind == 'v' ||
@@ -1498,7 +1498,7 @@ describeOneTableDetails(const char *schemaname,
false, false);
/* Statistics target, if the relkind supports this feature */
- if (tableinfo.relkind == 'r')
+ if (tableinfo.relkind == 'r' || tableinfo.relkind == 'f')
{
printTableAddCell(&cont, PQgetvalue(res, i, firstvcol + 1),
false, false);
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 975d655..d113adf 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -409,6 +409,21 @@ static const SchemaQuery Query_for_list_of_tsvf = {
NULL
};
+static const SchemaQuery Query_for_list_of_tf = {
+ /* catname */
+ "pg_catalog.pg_class c",
+ /* selcondition */
+ "c.relkind IN ('r', 'f')",
+ /* viscondition */
+ "pg_catalog.pg_table_is_visible(c.oid)",
+ /* namespace */
+ "c.relnamespace",
+ /* result */
+ "pg_catalog.quote_ident(c.relname)",
+ /* qualresult */
+ NULL
+};
+
static const SchemaQuery Query_for_list_of_views = {
/* catname */
"pg_catalog.pg_class c",
@@ -2833,7 +2848,7 @@ psql_completion(char *text, int start, int end)
/* ANALYZE */
/* If the previous word is ANALYZE, produce list of tables */
else if (pg_strcasecmp(prev_wd, "ANALYZE") == 0)
- COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables, NULL);
+ COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tf, NULL);
/* WHERE */
/* Simple case of the word before the where being the table name */
diff --git a/src/include/commands/vacuum.h b/src/include/commands/vacuum.h
index 3deee66..f6a3c59 100644
--- a/src/include/commands/vacuum.h
+++ b/src/include/commands/vacuum.h
@@ -58,6 +58,14 @@
*/
typedef struct VacAttrStats *VacAttrStatsP;
+typedef int (*AcquireSampleRowFunc) (Relation onerel,
+ HeapTuple *rows,
+ int targrows,
+ double *totalrows,
+ double *totaldeadrows,
+ BlockNumber *totalpages,
+ int elevel);
+
typedef Datum (*AnalyzeAttrFetchFunc) (VacAttrStatsP stats, int rownum,
bool *isNull);
@@ -139,6 +147,14 @@ extern int vacuum_freeze_table_age;
/* in commands/vacuum.c */
+extern void analyze_rel(Oid relid, VacuumStmt *vacstmt,
+ BufferAccessStrategy bstrategy);
+extern void do_analyze_rel(Relation onerel, VacuumStmt *vacstmt, int elevel,
+ bool inh, AcquireSampleRowFunc acquirefunc);
+extern double random_fract(void);
+extern double init_selection_state(int n);
+extern double get_next_S(double t, int n, double *stateptr);
+
extern void vacuum(VacuumStmt *vacstmt, Oid relid, bool do_toast,
BufferAccessStrategy bstrategy, bool for_wraparound, bool isTopLevel);
extern void vac_open_indexes(Relation relation, LOCKMODE lockmode,
diff --git a/src/include/foreign/fdwapi.h b/src/include/foreign/fdwapi.h
index 854f177..fbb5b0b 100644
--- a/src/include/foreign/fdwapi.h
+++ b/src/include/foreign/fdwapi.h
@@ -12,8 +12,10 @@
#ifndef FDWAPI_H
#define FDWAPI_H
+#include "foreign/foreign.h"
#include "nodes/execnodes.h"
#include "nodes/relation.h"
+#include "utils/rel.h"
/* To avoid including explain.h here, reference ExplainState thus: */
struct ExplainState;
@@ -50,6 +52,9 @@ typedef void (*ReScanForeignScan_function) (ForeignScanState *node);
typedef void (*EndForeignScan_function) (ForeignScanState *node);
+typedef void (*AnalyzeForeignTable_function) (Relation relation,
+ VacuumStmt *vacstmt,
+ int elevel);
/*
* FdwRoutine is the struct returned by a foreign-data wrapper's handler
@@ -64,6 +69,11 @@ typedef struct FdwRoutine
{
NodeTag type;
+ /*
+ * These handlers are required to execute simple scans on a foreign
+ * table. If any of them was set to NULL, scans on a foreign table
+ * managed by FDW would fail.
+ */
GetForeignRelSize_function GetForeignRelSize;
GetForeignPaths_function GetForeignPaths;
GetForeignPlan_function GetForeignPlan;
@@ -72,6 +82,12 @@ typedef struct FdwRoutine
IterateForeignScan_function IterateForeignScan;
ReScanForeignScan_function ReScanForeignScan;
EndForeignScan_function EndForeignScan;
+
+ /*
+ * Handlers below are optional. You can set any of them to NULL to
+ * tell PostgreSQL that FDW doesn't have the capability.
+ */
+ AnalyzeForeignTable_function AnalyzeForeignTable;
} FdwRoutine;
diff --git a/src/test/regress/expected/foreign_data.out b/src/test/regress/expected/foreign_data.out
index ba86883..f1379a6 100644
--- a/src/test/regress/expected/foreign_data.out
+++ b/src/test/regress/expected/foreign_data.out
@@ -679,12 +679,12 @@ CREATE FOREIGN TABLE ft1 (
COMMENT ON FOREIGN TABLE ft1 IS 'ft1';
COMMENT ON COLUMN ft1.c1 IS 'ft1.c1';
\d+ ft1
- Foreign table "public.ft1"
- Column | Type | Modifiers | FDW Options | Storage | Description
---------+---------+-----------+--------------------------------+----------+-------------
- c1 | integer | not null | ("param 1" 'val1') | plain | ft1.c1
- c2 | text | | (param2 'val2', param3 'val3') | extended |
- c3 | date | | | plain |
+ Foreign table "public.ft1"
+ Column | Type | Modifiers | FDW Options | Storage | Stats target | Description
+--------+---------+-----------+--------------------------------+----------+--------------+-------------
+ c1 | integer | not null | ("param 1" 'val1') | plain | | ft1.c1
+ c2 | text | | (param2 'val2', param3 'val3') | extended | |
+ c3 | date | | | plain | |
Server: s0
FDW Options: (delimiter ',', quote '"', "be quoted" 'value')
Has OIDs: no
@@ -730,19 +730,24 @@ ERROR: cannot alter system column "xmin"
ALTER FOREIGN TABLE ft1 ALTER COLUMN c7 OPTIONS (ADD p1 'v1', ADD p2 'v2'),
ALTER COLUMN c8 OPTIONS (ADD p1 'v1', ADD p2 'v2');
ALTER FOREIGN TABLE ft1 ALTER COLUMN c8 OPTIONS (SET p2 'V2', DROP p1);
+ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 SET STATISTICS 10000;
+ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 SET (n_distinct = 100);
+ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 SET (n_distinct_inherited = 100); -- ERROR
+ERROR: cannot set "n_distinct_inherited" for foreign tables
+ALTER FOREIGN TABLE ft1 ALTER COLUMN c8 SET STATISTICS -1;
\d+ ft1
- Foreign table "public.ft1"
- Column | Type | Modifiers | FDW Options | Storage | Description
---------+---------+-----------+--------------------------------+----------+-------------
- c1 | integer | not null | ("param 1" 'val1') | plain |
- c2 | text | | (param2 'val2', param3 'val3') | extended |
- c3 | date | | | plain |
- c4 | integer | | | plain |
- c6 | integer | not null | | plain |
- c7 | integer | | (p1 'v1', p2 'v2') | plain |
- c8 | text | | (p2 'V2') | extended |
- c9 | integer | | | plain |
- c10 | integer | | (p1 'v1') | plain |
+ Foreign table "public.ft1"
+ Column | Type | Modifiers | FDW Options | Storage | Stats target | Description
+--------+---------+-----------+--------------------------------+----------+--------------+-------------
+ c1 | integer | not null | ("param 1" 'val1') | plain | 10000 |
+ c2 | text | | (param2 'val2', param3 'val3') | extended | |
+ c3 | date | | | plain | |
+ c4 | integer | | | plain | |
+ c6 | integer | not null | | plain | |
+ c7 | integer | | (p1 'v1', p2 'v2') | plain | |
+ c8 | text | | (p2 'V2') | extended | |
+ c9 | integer | | | plain | |
+ c10 | integer | | (p1 'v1') | plain | |
Server: s0
FDW Options: (delimiter ',', quote '"', "be quoted" 'value')
Has OIDs: no
diff --git a/src/test/regress/sql/foreign_data.sql b/src/test/regress/sql/foreign_data.sql
index 0c95672..03b5680 100644
--- a/src/test/regress/sql/foreign_data.sql
+++ b/src/test/regress/sql/foreign_data.sql
@@ -307,6 +307,10 @@ ALTER FOREIGN TABLE ft1 ALTER COLUMN xmin OPTIONS (ADD p1 'v1'); -- ERROR
ALTER FOREIGN TABLE ft1 ALTER COLUMN c7 OPTIONS (ADD p1 'v1', ADD p2 'v2'),
ALTER COLUMN c8 OPTIONS (ADD p1 'v1', ADD p2 'v2');
ALTER FOREIGN TABLE ft1 ALTER COLUMN c8 OPTIONS (SET p2 'V2', DROP p1);
+ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 SET STATISTICS 10000;
+ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 SET (n_distinct = 100);
+ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 SET (n_distinct_inherited = 100); -- ERROR
+ALTER FOREIGN TABLE ft1 ALTER COLUMN c8 SET STATISTICS -1;
\d+ ft1
-- can't change the column type if it's used elsewhere
CREATE TABLE use_ft1_column_type (x ft1);
Sorry, I sent this email without noticing Hanada-san' earlier email. So,
please look at Hanada-san's post.
Best regards,
Etsuro Fujita
-----Original Message-----
From: pgsql-hackers-owner@postgresql.org
[mailto:pgsql-hackers-owner@postgresql.org] On Behalf Of Etsuro Fujita
Sent: Thursday, April 05, 2012 9:36 PM
To: Shigeru HANADA; Albe Laurenz
Cc: Tom Lane; Kevin Grittner; Robert Haas; PostgreSQL-development; Kohei
KaiGai; Martijn van Oosterhout; Hitoshi Harada
Subject: Re: [HACKERS] pgsql_fdw, FDW for PostgreSQL server
At 22:11 12/03/28 +0900, Shigeru HANADA wrote:
ANALYZE support for foreign tables is proposed by Fujita-san in current
CF, so I'd like to push it.
I updated the patch to the latest HEAD. Please find attached a patch.
Best regards,
Etsuro Fujita
"Albe Laurenz" <laurenz.albe@wien.gv.at> writes:
I think that the price of a remote table scan is something
we should be willing to pay for good local statistics.
And there is always the option not to analyze the foreign
table if you are not willing to pay that price.
Maybe the FDW API could be extended so that foreign data wrappers
can provide a random sample to avoid a full table scan.
The one thing that seems pretty clear from this discussion is that one
size doesn't fit all. I think we really ought to provide a hook so that
the FDW can determine whether ANALYZE applies to its foreign tables at
all, and how to obtain the sample rows if it does.
Since we've already whacked the FDW API around in incompatible ways for
9.2, now is probably a good time to add that. I'm inclined to say this
should happen whether or not we accept any of the currently proposed
patches for 9.2, because if the hook is there it will provide a way for
people to experiment with foreign-table ANALYZE operations outside of
core.
regards, tom lane
(2012/04/06 1:29), Tom Lane wrote:
"Albe Laurenz"<laurenz.albe@wien.gv.at> writes:
Maybe the FDW API could be extended so that foreign data wrappers
can provide a random sample to avoid a full table scan.The one thing that seems pretty clear from this discussion is that one
size doesn't fit all. I think we really ought to provide a hook so that
the FDW can determine whether ANALYZE applies to its foreign tables at
all, and how to obtain the sample rows if it does.Since we've already whacked the FDW API around in incompatible ways for
9.2, now is probably a good time to add that. I'm inclined to say this
should happen whether or not we accept any of the currently proposed
patches for 9.2, because if the hook is there it will provide a way for
people to experiment with foreign-table ANALYZE operations outside of
core.
To support foreign-table ANALYZE by adding a new hook, we would need a
mechanism (or at least documented guide lines) to manage the chain of
hook functions, because such hook might be used by multiple FDWs (or
other plug-ins) at the same time. A wrongly-written plug-in can easily
break the hook chain. We might need to provide register/unregister API
for this hook point, like RegisterResourceReleaseCallback, and call each
registered function until either of them processes the request. Is
there any other hook point which has similar issue?
Another concern is the place where we hook the process of ANALYZE. IOW,
how much portion of ANALYZE should be overridable? Replacing
analyze_rel or do_analyze_rel wholly requires plug-ins to copy most of
codes from original function in order to implement hook function. From
the perspective of FDW author, I think that row sampling
(acquire_sample_rows) function seems handy place to hook.
Regards,
--
Shigeru HANADA
(2012/04/04 15:43), Shigeru HANADA wrote:
Attached patch contains changes below:
pgsql_fdw_v19.patch
- show context of data conversion error
- move codes for fetch_count FDW option to option.c
(refactoring)
pgsql_fdw_pushdown_v12.patch
- make deparseExpr function static (refactoring)I also attached pgsql_fdw_analyze for only testing the effect of local
statistics. It contains both backend's ANALYZE command support and
pgsql_fdw's ANALYZE support.
Attached patch improves pgsql_fdw so that it uses new libpq row
processor API, and get rid of overhead of SQL-level cursor. This change
would speed up remote queries without extra memory.
This patch can be applied after pgsql_fdw_pushdown_v12.patch.
Regards,
--
Shigeru HANADA
Attachments:
pgsql_fdw_rowproc_v1.patchtext/plain; charset=Shift_JIS; name=pgsql_fdw_rowproc_v1.patchDownload
diff --git a/contrib/pgsql_fdw/connection.c b/contrib/pgsql_fdw/connection.c
index c971bd7..7cb448b 100644
*** a/contrib/pgsql_fdw/connection.c
--- b/contrib/pgsql_fdw/connection.c
*************** connect_pg_server(ForeignServer *server,
*** 293,299 ****
}
/*
! * Start transaction to use cursor to retrieve data separately.
*/
static void
begin_remote_tx(PGconn *conn)
--- 293,299 ----
}
/*
! * Start remote transaction with proper isolation level.
*/
static void
begin_remote_tx(PGconn *conn)
diff --git a/contrib/pgsql_fdw/deparse.c b/contrib/pgsql_fdw/deparse.c
index 6f8f753..0f94551 100644
*** a/contrib/pgsql_fdw/deparse.c
--- b/contrib/pgsql_fdw/deparse.c
*************** void
*** 82,89 ****
deparseSimpleSql(StringInfo buf,
Oid relid,
PlannerInfo *root,
! RelOptInfo *baserel,
! ForeignTable *table)
{
StringInfoData foreign_relname;
bool first;
--- 82,88 ----
deparseSimpleSql(StringInfo buf,
Oid relid,
PlannerInfo *root,
! RelOptInfo *baserel)
{
StringInfoData foreign_relname;
bool first;
*************** deparseSimpleSql(StringInfo buf,
*** 173,179 ****
* deparse FROM clause, including alias if any
*/
appendStringInfo(buf, "FROM ");
! deparseRelation(buf, table->relid, root, true);
elog(DEBUG3, "Remote SQL: %s", buf->data);
}
--- 172,178 ----
* deparse FROM clause, including alias if any
*/
appendStringInfo(buf, "FROM ");
! deparseRelation(buf, relid, root, true);
elog(DEBUG3, "Remote SQL: %s", buf->data);
}
diff --git a/contrib/pgsql_fdw/expected/pgsql_fdw.out b/contrib/pgsql_fdw/expected/pgsql_fdw.out
index 8e50614..c486094 100644
*** a/contrib/pgsql_fdw/expected/pgsql_fdw.out
--- b/contrib/pgsql_fdw/expected/pgsql_fdw.out
*************** ALTER SERVER loopback1 OPTIONS (
*** 113,120 ****
);
ALTER SERVER loopback1 OPTIONS (user 'value'); -- ERROR
ERROR: invalid option "user"
! HINT: Valid options in this context are: authtype, service, connect_timeout, dbname, host, hostaddr, port, tty, options, application_name, keepalives, keepalives_idle, keepalives_interval, keepalives_count, requiressl, sslcompression, sslmode, sslcert, sslkey, sslrootcert, sslcrl, requirepeer, krbsrvname, gsslib, fetch_count
! ALTER SERVER loopback2 OPTIONS (ADD fetch_count '2');
ALTER USER MAPPING FOR public SERVER loopback1
OPTIONS (DROP user, DROP password);
ALTER USER MAPPING FOR public SERVER loopback1
--- 113,119 ----
);
ALTER SERVER loopback1 OPTIONS (user 'value'); -- ERROR
ERROR: invalid option "user"
! HINT: Valid options in this context are: authtype, service, connect_timeout, dbname, host, hostaddr, port, tty, options, application_name, keepalives, keepalives_idle, keepalives_interval, keepalives_count, requiressl, sslcompression, sslmode, sslcert, sslkey, sslrootcert, sslcrl, requirepeer, krbsrvname, gsslib
ALTER USER MAPPING FOR public SERVER loopback1
OPTIONS (DROP user, DROP password);
ALTER USER MAPPING FOR public SERVER loopback1
*************** ALTER USER MAPPING FOR public SERVER loo
*** 122,137 ****
ERROR: invalid option "host"
HINT: Valid options in this context are: user, password
ALTER FOREIGN TABLE ft1 OPTIONS (nspname 'S 1', relname 'T 1');
! ALTER FOREIGN TABLE ft2 OPTIONS (nspname 'S 1', relname 'T 1', fetch_count '100');
ALTER FOREIGN TABLE ft1 OPTIONS (invalid 'value'); -- ERROR
ERROR: invalid option "invalid"
! HINT: Valid options in this context are: nspname, relname, fetch_count
! ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count 'a'); -- ERROR
! ERROR: invalid value for fetch_count: "a"
! ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count '0'); -- ERROR
! ERROR: invalid value for fetch_count: "0"
! ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count '-1'); -- ERROR
! ERROR: invalid value for fetch_count: "-1"
ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (invalid 'value'); -- ERROR
ERROR: invalid option "invalid"
HINT: Valid options in this context are: colname
--- 121,130 ----
ERROR: invalid option "host"
HINT: Valid options in this context are: user, password
ALTER FOREIGN TABLE ft1 OPTIONS (nspname 'S 1', relname 'T 1');
! ALTER FOREIGN TABLE ft2 OPTIONS (nspname 'S 1', relname 'T 1');
ALTER FOREIGN TABLE ft1 OPTIONS (invalid 'value'); -- ERROR
ERROR: invalid option "invalid"
! HINT: Valid options in this context are: nspname, relname
ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (invalid 'value'); -- ERROR
ERROR: invalid option "invalid"
HINT: Valid options in this context are: colname
*************** ALTER FOREIGN TABLE ft2 ALTER COLUMN c1
*** 149,155 ****
Name | Owner | Foreign-data wrapper | Access privileges | Type | Version | FDW Options | Description
-----------+----------------+----------------------+-------------------+------+---------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+-------------
loopback1 | pgsql_fdw_user | pgsql_fdw | | | | (authtype 'value', service 'value', connect_timeout 'value', dbname 'value', host 'value', hostaddr 'value', port 'value', tty 'value', options 'value', application_name 'value', keepalives 'value', keepalives_idle 'value', keepalives_interval 'value', sslcompression 'value', sslmode 'value', sslcert 'value', sslkey 'value', sslrootcert 'value', sslcrl 'value') |
! loopback2 | pgsql_fdw_user | pgsql_fdw | | | | (dbname 'contrib_regression', fetch_count '2') |
(2 rows)
\deu+
--- 142,148 ----
Name | Owner | Foreign-data wrapper | Access privileges | Type | Version | FDW Options | Description
-----------+----------------+----------------------+-------------------+------+---------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+-------------
loopback1 | pgsql_fdw_user | pgsql_fdw | | | | (authtype 'value', service 'value', connect_timeout 'value', dbname 'value', host 'value', hostaddr 'value', port 'value', tty 'value', options 'value', application_name 'value', keepalives 'value', keepalives_idle 'value', keepalives_interval 'value', sslcompression 'value', sslmode 'value', sslcert 'value', sslkey 'value', sslrootcert 'value', sslcrl 'value') |
! loopback2 | pgsql_fdw_user | pgsql_fdw | | | | (dbname 'contrib_regression') |
(2 rows)
\deu+
*************** ALTER FOREIGN TABLE ft2 ALTER COLUMN c1
*** 161,189 ****
(2 rows)
\det+
! List of foreign tables
! Schema | Table | Server | FDW Options | Description
! --------+-------+-----------+---------------------------------------------------+-------------
! public | ft1 | loopback2 | (nspname 'S 1', relname 'T 1') |
! public | ft2 | loopback2 | (nspname 'S 1', relname 'T 1', fetch_count '100') |
(2 rows)
-- ===================================================================
-- simple queries
-- ===================================================================
-- single table, with/without alias
! EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
! QUERY PLAN
! ------------------------------------------------------------------------------------------------------------------------------
Limit
- Output: c1, c2, c3, c4, c5, c6, c7
-> Sort
! Output: c1, c2, c3, c4, c5, c6, c7
! Sort Key: ft1.c3, ft1.c1
! -> Foreign Scan on public.ft1
! Output: c1, c2, c3, c4, c5, c6, c7
! Remote SQL: DECLARE pgsql_fdw_cursor_0 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
! (8 rows)
SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
c1 | c2 | c3 | c4 | c5 | c6 | c7
--- 154,179 ----
(2 rows)
\det+
! List of foreign tables
! Schema | Table | Server | FDW Options | Description
! --------+-------+-----------+--------------------------------+-------------
! public | ft1 | loopback2 | (nspname 'S 1', relname 'T 1') |
! public | ft2 | loopback2 | (nspname 'S 1', relname 'T 1') |
(2 rows)
-- ===================================================================
-- simple queries
-- ===================================================================
-- single table, with/without alias
! EXPLAIN (COSTS false) SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
! QUERY PLAN
! ---------------------------------------------------------------------------------
Limit
-> Sort
! Sort Key: c3, c1
! -> Foreign Scan on ft1
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
! (5 rows)
SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
c1 | c2 | c3 | c4 | c5 | c6 | c7
*************** SELECT * FROM ft1 ORDER BY c3, c1 OFFSET
*** 200,217 ****
110 | 0 | 00110 | Sun Jan 11 00:00:00 1970 PST | Sun Jan 11 00:00:00 1970 | 0 | 0
(10 rows)
! EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
! QUERY PLAN
! ------------------------------------------------------------------------------------------------------------------------------
Limit
- Output: c1, c2, c3, c4, c5, c6, c7
-> Sort
! Output: c1, c2, c3, c4, c5, c6, c7
! Sort Key: t1.c3, t1.c1
! -> Foreign Scan on public.ft1 t1
! Output: c1, c2, c3, c4, c5, c6, c7
! Remote SQL: DECLARE pgsql_fdw_cursor_2 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
! (8 rows)
SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
c1 | c2 | c3 | c4 | c5 | c6 | c7
--- 190,204 ----
110 | 0 | 00110 | Sun Jan 11 00:00:00 1970 PST | Sun Jan 11 00:00:00 1970 | 0 | 0
(10 rows)
! EXPLAIN (COSTS false) SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
! QUERY PLAN
! ---------------------------------------------------------------------------------
Limit
-> Sort
! Sort Key: c3, c1
! -> Foreign Scan on ft1 t1
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
! (5 rows)
SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
c1 | c2 | c3 | c4 | c5 | c6 | c7
*************** SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.
*** 229,242 ****
(10 rows)
-- with WHERE clause
! EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 >= '1';
! QUERY PLAN
! -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
! Foreign Scan on public.ft1 t1
! Output: c1, c2, c3, c4, c5, c6, c7
! Filter: (t1.c7 >= '1'::bpchar)
! Remote SQL: DECLARE pgsql_fdw_cursor_4 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.=) 101)) AND (((c6)::text OPERATOR(pg_catalog.=) '1'::text))
! (4 rows)
SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 >= '1';
c1 | c2 | c3 | c4 | c5 | c6 | c7
--- 216,228 ----
(10 rows)
-- with WHERE clause
! EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 >= '1';
! QUERY PLAN
! --------------------------------------------------------------------------------------------------------------------------------------------------------------------
! Foreign Scan on ft1 t1
! Filter: (c7 >= '1'::bpchar)
! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.=) 101)) AND (((c6)::text OPERATOR(pg_catalog.=) '1'::text))
! (3 rows)
SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 >= '1';
c1 | c2 | c3 | c4 | c5 | c6 | c7
diff --git a/contrib/pgsql_fdw/option.c b/contrib/pgsql_fdw/option.c
index 542ef01..0192fd2 100644
*** a/contrib/pgsql_fdw/option.c
--- b/contrib/pgsql_fdw/option.c
***************
*** 26,37 ****
#include "pgsql_fdw.h"
/*
- * Default fetch count for cursor. This can be overridden by fetch_count FDW
- * option.
- */
- #define DEFAULT_FETCH_COUNT 10000
-
- /*
* SQL functions
*/
extern Datum pgsql_fdw_validator(PG_FUNCTION_ARGS);
--- 26,31 ----
*************** static PgsqlFdwOption valid_options[] =
*** 105,117 ****
{"relname", ForeignTableRelationId, false},
{"colname", AttributeRelationId, false},
- /*
- * Options for cursor behavior.
- * These options can be overridden by finer-grained objects.
- */
- {"fetch_count", ForeignTableRelationId, false},
- {"fetch_count", ForeignServerRelationId, false},
-
/* Terminating entry --- MUST BE LAST */
{NULL, InvalidOid, false}
};
--- 99,104 ----
*************** pgsql_fdw_validator(PG_FUNCTION_ARGS)
*** 165,184 ****
errhint("Valid options in this context are: %s",
buf.data)));
}
-
- /* fetch_count be positive digit number. */
- if (strcmp(def->defname, "fetch_count") == 0)
- {
- long value;
- char *p = NULL;
-
- value = strtol(defGetString(def), &p, 10);
- if (*p != '\0' || value < 1)
- ereport(ERROR,
- (errcode(ERRCODE_FDW_INVALID_ATTRIBUTE_VALUE),
- errmsg("invalid value for %s: \"%s\"",
- def->defname, defGetString(def))));
- }
}
/*
--- 152,157 ----
*************** ExtractConnectionOptions(List *defelems,
*** 247,285 ****
return i;
}
- /*
- * Return fetch_count which should be used for the foreign table.
- */
- int
- GetFetchCountOption(ForeignTable *table, ForeignServer *server)
- {
- int fetch_count = DEFAULT_FETCH_COUNT;
- ListCell *lc;
- DefElem *def;
-
- /*
- * Use specified fetch_count instead of default value, if any. Foreign
- * table option overrides server option.
- */
- foreach(lc, table->options)
- {
- def = (DefElem *) lfirst(lc);
- if (strcmp(def->defname, "fetch_count") == 0)
- break;
-
- }
- if (lc == NULL)
- {
- foreach(lc, server->options)
- {
- def = (DefElem *) lfirst(lc);
- if (strcmp(def->defname, "fetch_count") == 0)
- break;
-
- }
- }
- if (lc != NULL)
- fetch_count = strtol(defGetString(def), NULL, 10);
-
- return fetch_count;
- }
--- 220,222 ----
diff --git a/contrib/pgsql_fdw/pgsql_fdw.c b/contrib/pgsql_fdw/pgsql_fdw.c
index 8975955..aede449 100644
*** a/contrib/pgsql_fdw/pgsql_fdw.c
--- b/contrib/pgsql_fdw/pgsql_fdw.c
*************** PG_MODULE_MAGIC;
*** 45,59 ****
*/
#define TRANSFER_COSTS_PER_BYTE 0.001
- /*
- * Cursors which are used together in a local query require different name, so
- * we use simple incremental name for that purpose. We don't care wrap around
- * of cursor_id because it's hard to imagine that 2^32 cursors are used in a
- * query.
- */
- #define CURSOR_NAME_FORMAT "pgsql_fdw_cursor_%u"
- static uint32 cursor_id = 0;
-
/* Convenient macros for accessing the first record of PGresult. */
#define PGRES_VAL0(col) (PQgetvalue(res, 0, (col)))
#define PGRES_NULL0(col) (PQgetisnull(res, 0, (col)))
--- 45,50 ----
*************** typedef struct PgsqlFdwPlanState {
*** 85,111 ****
* Index of FDW-private information stored in fdw_private list.
*
* We store various information in ForeignScan.fdw_private to pass them beyond
! * the boundary between planner and executor. Finally FdwPlan using cursor
! * would hold items below:
*
* 1) plain SELECT statement
- * 2) SQL statement used to declare cursor
- * 3) SQL statement used to fetch rows from cursor
- * 4) SQL statement used to reset cursor
- * 5) SQL statement used to close cursor
*
* These items are indexed with the enum FdwPrivateIndex, so an item
! * can be accessed directly via list_nth(). For example of FETCH
! * statement:
! * list_nth(fdw_private, FdwPrivateFetchSql)
*/
enum FdwPrivateIndex {
/* SQL statements */
FdwPrivateSelectSql,
- FdwPrivateDeclareSql,
- FdwPrivateFetchSql,
- FdwPrivateResetSql,
- FdwPrivateCloseSql,
/* # of elements stored in the list fdw_private */
FdwPrivateNum,
--- 76,93 ----
* Index of FDW-private information stored in fdw_private list.
*
* We store various information in ForeignScan.fdw_private to pass them beyond
! * the boundary between planner and executor. Finally FdwPlan holds items
! * below:
*
* 1) plain SELECT statement
*
* These items are indexed with the enum FdwPrivateIndex, so an item
! * can be accessed directly via list_nth(). For example of SELECT statement:
! * sql = list_nth(fdw_private, FdwPrivateSelectSql)
*/
enum FdwPrivateIndex {
/* SQL statements */
FdwPrivateSelectSql,
/* # of elements stored in the list fdw_private */
FdwPrivateNum,
*************** typedef struct PgsqlFdwExecutionState
*** 132,137 ****
--- 114,120 ----
/* for storing result tuples */
MemoryContext scan_cxt; /* context for per-scan lifespan data */
+ MemoryContext temp_cxt; /* context for per-tuple temporary data */
Tuplestorestate *tuples; /* result of the scan */
/* for error handling. */
*************** static void get_remote_estimate(const ch
*** 178,185 ****
static void adjust_costs(double rows, int width,
Cost *startup_cost, Cost *total_cost);
static void execute_query(ForeignScanState *node);
! static PGresult *fetch_result(ForeignScanState *node);
! static void store_result(ForeignScanState *node, PGresult *res);
static void pgsql_fdw_error_callback(void *arg);
/* Exported functions, but not written in pgsql_fdw.h. */
--- 161,168 ----
static void adjust_costs(double rows, int width,
Cost *startup_cost, Cost *total_cost);
static void execute_query(ForeignScanState *node);
! static int query_row_processor(PGresult *res, const PGdataValue *columns,
! const char **errmsgp, void *param);
static void pgsql_fdw_error_callback(void *arg);
/* Exported functions, but not written in pgsql_fdw.h. */
*************** pgsqlGetForeignRelSize(PlannerInfo *root
*** 288,294 ****
* appended later.
*/
sortConditions(root, baserel, &remote_conds, ¶m_conds, &local_conds);
! deparseSimpleSql(sql, foreigntableid, root, baserel, table);
if (list_length(remote_conds) > 0)
{
appendWhereClause(sql, fpstate->has_where, remote_conds, root);
--- 271,277 ----
* appended later.
*/
sortConditions(root, baserel, &remote_conds, ¶m_conds, &local_conds);
! deparseSimpleSql(sql, foreigntableid, root, baserel);
if (list_length(remote_conds) > 0)
{
appendWhereClause(sql, fpstate->has_where, remote_conds, root);
*************** pgsqlGetForeignPlan(PlannerInfo *root,
*** 400,409 ****
PgsqlFdwPlanState *fpstate = (PgsqlFdwPlanState *) baserel->fdw_private;
Index scan_relid = baserel->relid;
List *fdw_private = NIL;
- char name[128]; /* must be larger than format + 10 */
- StringInfoData cursor;
- int fetch_count;
- char *sql;
List *fdw_exprs = NIL;
List *local_exprs = NIL;
ListCell *lc;
--- 383,388 ----
*************** pgsqlGetForeignPlan(PlannerInfo *root,
*** 420,454 ****
foreach(lc, fpstate->local_conds)
local_exprs = lappend(local_exprs,
((RestrictInfo *) lfirst(lc))->clause);
- fetch_count = GetFetchCountOption(fpstate->table, fpstate->server);
- elog(DEBUG1, "relid=%u fetch_count=%d", foreigntableid, fetch_count);
-
- /* Construct cursor name from sequential value */
- sprintf(name, CURSOR_NAME_FORMAT, cursor_id++);
/*
! * Construct CURSOR statements from plain remote query, and make a list
! * contains all of them to pass them to executor with plan node for later
! * use.
*/
! sql = fpstate->sql.data;
! fdw_private = lappend(fdw_private, makeString(sql));
!
! initStringInfo(&cursor);
! appendStringInfo(&cursor, "DECLARE %s SCROLL CURSOR FOR %s", name, sql);
! fdw_private = lappend(fdw_private, makeString(cursor.data));
!
! initStringInfo(&cursor);
! appendStringInfo(&cursor, "FETCH %d FROM %s", fetch_count, name);
! fdw_private = lappend(fdw_private, makeString(cursor.data));
!
! initStringInfo(&cursor);
! appendStringInfo(&cursor, "MOVE ABSOLUTE 0 FROM %s", name);
! fdw_private = lappend(fdw_private, makeString(cursor.data));
!
! initStringInfo(&cursor);
! appendStringInfo(&cursor, "CLOSE %s", name);
! fdw_private = lappend(fdw_private, makeString(cursor.data));
/*
* Create the ForeignScan node from target list, local filtering
--- 399,410 ----
foreach(lc, fpstate->local_conds)
local_exprs = lappend(local_exprs,
((RestrictInfo *) lfirst(lc))->clause);
/*
! * Make a list contains SELECT statement to it to executor with plan node
! * for later use.
*/
! fdw_private = lappend(fdw_private, makeString(fpstate->sql.data));
/*
* Create the ForeignScan node from target list, local filtering
*************** pgsqlExplainForeignScan(ForeignScanState
*** 477,488 ****
List *fdw_private;
char *sql;
- /* CURSOR declaration is shown in only VERBOSE mode. */
fdw_private = ((ForeignScan *) node->ss.ps.plan)->fdw_private;
! if (es->verbose)
! sql = strVal(list_nth(fdw_private, FdwPrivateDeclareSql));
! else
! sql = strVal(list_nth(fdw_private, FdwPrivateSelectSql));
ExplainPropertyText("Remote SQL", sql, es);
}
--- 433,440 ----
List *fdw_private;
char *sql;
fdw_private = ((ForeignScan *) node->ss.ps.plan)->fdw_private;
! sql = strVal(list_nth(fdw_private, FdwPrivateSelectSql));
ExplainPropertyText("Remote SQL", sql, es);
}
*************** pgsqlBeginForeignScan(ForeignScanState *
*** 514,523 ****
festate->fdw_private = ((ForeignScan *) node->ss.ps.plan)->fdw_private;
/*
! * Create context for per-scan tuplestore under per-query context.
*/
festate->scan_cxt = AllocSetContextCreate(node->ss.ps.state->es_query_cxt,
! "pgsql_fdw",
ALLOCSET_DEFAULT_MINSIZE,
ALLOCSET_DEFAULT_INITSIZE,
ALLOCSET_DEFAULT_MAXSIZE);
--- 466,480 ----
festate->fdw_private = ((ForeignScan *) node->ss.ps.plan)->fdw_private;
/*
! * Create contexts for per-scan tuplestore under per-query context.
*/
festate->scan_cxt = AllocSetContextCreate(node->ss.ps.state->es_query_cxt,
! "pgsql_fdw per-scan data",
! ALLOCSET_DEFAULT_MINSIZE,
! ALLOCSET_DEFAULT_INITSIZE,
! ALLOCSET_DEFAULT_MAXSIZE);
! festate->temp_cxt = AllocSetContextCreate(node->ss.ps.state->es_query_cxt,
! "pgsql_fdw temporary data",
ALLOCSET_DEFAULT_MINSIZE,
ALLOCSET_DEFAULT_INITSIZE,
ALLOCSET_DEFAULT_MAXSIZE);
*************** pgsqlIterateForeignScan(ForeignScanState
*** 586,705 ****
{
PgsqlFdwExecutionState *festate;
TupleTableSlot *slot = node->ss.ss_ScanTupleSlot;
- PGresult *res = NULL;
MemoryContext oldcontext = CurrentMemoryContext;
festate = (PgsqlFdwExecutionState *) node->fdw_state;
/*
! * If this is the first call after Begin, we need to execute remote query.
! *
! * Since we needs cursor to prevent out of memory, we declare a cursor at
! * the first call and fetch from it in later calls.
*/
if (festate->tuples == NULL)
execute_query(node);
/*
! * If enough tuples are left in tuplestore, just return next tuple from it.
*
* It is necessary to switch to per-scan context to make returned tuple
* valid until next IterateForeignScan call, because it will be released
* with ExecClearTuple then. Otherwise, picked tuple is allocated in
* per-tuple context, and double-free of that tuple might happen.
*/
MemoryContextSwitchTo(festate->scan_cxt);
! if (tuplestore_gettupleslot(festate->tuples, true, false, slot))
! {
! MemoryContextSwitchTo(oldcontext);
! return slot;
! }
! MemoryContextSwitchTo(oldcontext);
!
! /*
! * Here we need to clear partial result and fetch next bunch of tuples from
! * from the cursor for the scan. If the fetch returns no tuple, the scan
! * has reached the end.
! *
! * PGresult must be released before leaving this function.
! */
! PG_TRY();
! {
! res = fetch_result(node);
! store_result(node, res);
! PQclear(res);
! res = NULL;
! }
! PG_CATCH();
! {
! PQclear(res);
! PG_RE_THROW();
! }
! PG_END_TRY();
!
! /*
! * If we got more tuples from the server cursor, return next tuple from
! * tuplestore.
! */
! MemoryContextSwitchTo(festate->scan_cxt);
! if (tuplestore_gettupleslot(festate->tuples, true, false, slot))
! {
! MemoryContextSwitchTo(oldcontext);
! return slot;
! }
MemoryContextSwitchTo(oldcontext);
- /* We don't have any result even in remote server cursor. */
- ExecClearTuple(slot);
return slot;
}
/*
* pgsqlReScanForeignScan
! * - Restart this scan by resetting fetch location.
*/
static void
pgsqlReScanForeignScan(ForeignScanState *node)
{
- List *fdw_private;
- char *sql;
- PGconn *conn;
- PGresult *res = NULL;
PgsqlFdwExecutionState *festate;
festate = (PgsqlFdwExecutionState *) node->fdw_state;
! /* If we have not opened cursor yet, nothing to do. */
if (festate->tuples == NULL)
return;
! /* Discard fetch results if any. */
! tuplestore_clear(festate->tuples);
!
! /* PGresult must be released before leaving this function. */
! PG_TRY();
! {
! /* Reset cursor */
! fdw_private = festate->fdw_private;
! conn = festate->conn;
! sql = strVal(list_nth(fdw_private, FdwPrivateResetSql));
! res = PQexec(conn, sql);
! if (PQresultStatus(res) != PGRES_COMMAND_OK)
! {
! ereport(ERROR,
! (errmsg("could not rewind cursor"),
! errdetail("%s", PQerrorMessage(conn)),
! errhint("%s", sql)));
! }
! PQclear(res);
! res = NULL;
! }
! PG_CATCH();
! {
! PQclear(res);
! PG_RE_THROW();
! }
! PG_END_TRY();
}
/*
--- 543,596 ----
{
PgsqlFdwExecutionState *festate;
TupleTableSlot *slot = node->ss.ss_ScanTupleSlot;
MemoryContext oldcontext = CurrentMemoryContext;
festate = (PgsqlFdwExecutionState *) node->fdw_state;
/*
! * If this is the first call after Begin or ReScan, we need to execute
! * remote query and get result set.
*/
if (festate->tuples == NULL)
execute_query(node);
/*
! * If tuples are still left in tuplestore, just return next tuple from it.
*
* It is necessary to switch to per-scan context to make returned tuple
* valid until next IterateForeignScan call, because it will be released
* with ExecClearTuple then. Otherwise, picked tuple is allocated in
* per-tuple context, and double-free of that tuple might happen.
+ *
+ * If we don't have any result in tuplestore, clear result slot to tell
+ * executor that this scan is over.
*/
MemoryContextSwitchTo(festate->scan_cxt);
! tuplestore_gettupleslot(festate->tuples, true, false, slot);
MemoryContextSwitchTo(oldcontext);
return slot;
}
/*
* pgsqlReScanForeignScan
! * - Restart this scan by clearing old results and set re-execute flag.
*/
static void
pgsqlReScanForeignScan(ForeignScanState *node)
{
PgsqlFdwExecutionState *festate;
festate = (PgsqlFdwExecutionState *) node->fdw_state;
! /* If we haven't have valid result yet, nothing to do. */
if (festate->tuples == NULL)
return;
! /*
! * Only rewind the current result set is enough.
! */
! tuplestore_rescan(festate->tuples);
}
/*
*************** pgsqlReScanForeignScan(ForeignScanState
*** 709,718 ****
static void
pgsqlEndForeignScan(ForeignScanState *node)
{
- List *fdw_private;
- char *sql;
- PGconn *conn;
- PGresult *res = NULL;
PgsqlFdwExecutionState *festate;
festate = (PgsqlFdwExecutionState *) node->fdw_state;
--- 600,605 ----
*************** pgsqlEndForeignScan(ForeignScanState *no
*** 721,763 ****
if (festate == NULL)
return;
! /* If we have not opened cursor yet, nothing to do. */
! if (festate->tuples == NULL)
! return;
/* Discard fetch results */
! tuplestore_end(festate->tuples);
! festate->tuples = NULL;
!
! /* PGresult must be released before leaving this function. */
! PG_TRY();
! {
! /* Close cursor */
! fdw_private = festate->fdw_private;
! conn = festate->conn;
! sql = strVal(list_nth(fdw_private, FdwPrivateCloseSql));
! res = PQexec(conn, sql);
! if (PQresultStatus(res) != PGRES_COMMAND_OK)
! {
! ereport(ERROR,
! (errmsg("could not close cursor"),
! errdetail("%s", PQerrorMessage(conn)),
! errhint("%s", sql)));
! }
! PQclear(res);
! res = NULL;
! }
! PG_CATCH();
{
! PQclear(res);
! PG_RE_THROW();
}
- PG_END_TRY();
-
- ReleaseConnection(festate->conn);
! MemoryContextDelete(festate->scan_cxt);
! festate->scan_cxt = NULL;
}
/*
--- 608,629 ----
if (festate == NULL)
return;
! /*
! * The connection which was used for this scan should be valid until the
! * end of the scan to make the lifespan of remote transaction same as the
! * local query.
! */
! ReleaseConnection(festate->conn);
! festate->conn = NULL;
/* Discard fetch results */
! if (festate->tuples != NULL)
{
! tuplestore_end(festate->tuples);
! festate->tuples = NULL;
}
! /* MemoryContext will be deleted automatically. */
}
/*
*************** adjust_costs(double rows, int width, Cos
*** 848,854 ****
static void
execute_query(ForeignScanState *node)
{
- List *fdw_private;
PgsqlFdwExecutionState *festate;
ParamListInfo params = node->ss.ps.state->es_param_list_info;
int numParams = params ? params->numParams : 0;
--- 714,719 ----
*************** execute_query(ForeignScanState *node)
*** 893,925 ****
PG_TRY();
{
/*
! * Execute remote query with parameters.
*/
conn = festate->conn;
! fdw_private = ((ForeignScan *) node->ss.ps.plan)->fdw_private;
! sql = strVal(list_nth(fdw_private, FdwPrivateDeclareSql));
res = PQexecParams(conn, sql, numParams, types, values, NULL, NULL, 0);
/*
* If the query has failed, reporting details is enough here.
* Connection(s) which are used by this query (at least used by
* pgsql_fdw) will be cleaned up by the foreign connection manager.
*/
! if (PQresultStatus(res) != PGRES_COMMAND_OK)
{
ereport(ERROR,
! (errmsg("could not declare cursor"),
errdetail("%s", PQerrorMessage(conn)),
errhint("%s", sql)));
}
! /* Discard result of DECLARE statement. */
! PQclear(res);
! res = NULL;
!
! /* Fetch first bunch of the result and store them into tuplestore. */
! res = fetch_result(node);
! store_result(node, res);
PQclear(res);
res = NULL;
}
--- 758,795 ----
PG_TRY();
{
/*
! * Execute remote query with parameters, and retrieve results with
! * custom row processor which stores results in tuplestore.
! *
! * We uninstall the custom row processor right after processing all
! * results.
*/
conn = festate->conn;
! sql = strVal(list_nth(festate->fdw_private, FdwPrivateSelectSql));
! PQsetRowProcessor(conn, query_row_processor, node);
res = PQexecParams(conn, sql, numParams, types, values, NULL, NULL, 0);
+ PQsetRowProcessor(conn, NULL, NULL);
+
+ /*
+ * We can't know whether the scan is over or not in custom row
+ * processor, so mark that the result is valid here.
+ */
+ tuplestore_donestoring(festate->tuples);
/*
* If the query has failed, reporting details is enough here.
* Connection(s) which are used by this query (at least used by
* pgsql_fdw) will be cleaned up by the foreign connection manager.
*/
! if (PQresultStatus(res) != PGRES_TUPLES_OK)
{
ereport(ERROR,
! (errmsg("could not execute remote query"),
errdetail("%s", PQerrorMessage(conn)),
errhint("%s", sql)));
}
! /* Discard result of SELECT statement. */
PQclear(res);
res = NULL;
}
*************** execute_query(ForeignScanState *node)
*** 932,1098 ****
}
/*
- * Fetch next partial result from remote server.
- *
- * Once this function has returned result records as PGresult, caller is
- * responsible to release it, so caller should put codes which might throw
- * exception in PG_TRY block. When an exception has been caught, release
- * PGresult and re-throw the exception in PG_CATCH block.
- */
- static PGresult *
- fetch_result(ForeignScanState *node)
- {
- PgsqlFdwExecutionState *festate;
- List *fdw_private;
- char *sql;
- PGconn *conn;
- PGresult *res;
-
- festate = (PgsqlFdwExecutionState *) node->fdw_state;
-
- /* Retrieve information for fetching result. */
- fdw_private = festate->fdw_private;
- sql = strVal(list_nth(fdw_private, FdwPrivateFetchSql));
- conn = festate->conn;
-
- /*
- * Fetch result from remote server. In error case, we must release
- * PGresult in this function to avoid memory leak because caller can't
- * get the reference.
- */
- res = PQexec(conn, sql);
- if (PQresultStatus(res) != PGRES_TUPLES_OK)
- {
- PQclear(res);
- ereport(ERROR,
- (errmsg("could not fetch rows from foreign server"),
- errdetail("%s", PQerrorMessage(conn)),
- errhint("%s", sql)));
- }
-
- return res;
- }
-
- /*
* Create tuples from PGresult and store them into tuplestore.
*
* Caller must use PG_TRY block to catch exception and release PGresult
* surely.
*/
! static void
! store_result(ForeignScanState *node, PGresult *res)
{
! int rows;
! int row;
! int i;
! int nfields;
! int attnum; /* number of non-dropped columns */
! Form_pg_attribute *attrs;
TupleTableSlot *slot = node->ss.ss_ScanTupleSlot;
TupleDesc tupdesc = slot->tts_tupleDescriptor;
! PgsqlFdwExecutionState *festate;
! AttInMetadata *attinmeta;
ErrorContextCallback errcontext;
! festate = (PgsqlFdwExecutionState *) node->fdw_state;
! rows = PQntuples(res);
! nfields = PQnfields(res);
! attrs = tupdesc->attrs;
! attinmeta = festate->attinmeta;
!
! /* First, ensure that the tuplestore is empty. */
! if (festate->tuples == NULL)
{
! MemoryContext oldcontext = CurrentMemoryContext;
! /*
! * Create tuplestore to store result of the query in per-query context.
! * Note that we use this memory context to avoid memory leak in error
! * cases.
! */
! MemoryContextSwitchTo(festate->scan_cxt);
! festate->tuples = tuplestore_begin_heap(false, false, work_mem);
! MemoryContextSwitchTo(oldcontext);
! }
! else
! {
! /* We already have tuplestore, just need to clear contents of it. */
! tuplestore_clear(festate->tuples);
! }
! /* 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)));
/*
! * Set up callback to identify error column. We don't set callback right
! * row because it should be set only during column value conversion.
*/
! errcontext.callback = pgsql_fdw_error_callback;
! errcontext.arg = (void *) festate;
! /* put a tuples into the slot */
! for (row = 0; row < rows; row++)
{
! int j;
! HeapTuple tuple;
! /* Install callback function for error. */
! errcontext.previous = error_context_stack;
! error_context_stack = &errcontext;
! for (i = 0, j = 0; i < tupdesc->natts; i++)
{
! /* skip dropped columns. */
! if (attrs[i]->attisdropped)
{
! festate->nulls[i] = true;
! continue;
}
/*
! * Set NULL indicator, and convert text representation to internal
! * representation if any.
*/
! if (PQgetisnull(res, row, j))
! festate->nulls[i] = true;
! else
! {
! Datum value;
! festate->cur_attno = i + 1; /* first attribute has index 1 */
! festate->nulls[i] = false;
! value = InputFunctionCall(&attinmeta->attinfuncs[i],
! PQgetvalue(res, row, j),
! attinmeta->attioparams[i],
! attinmeta->atttypmods[i]);
! festate->values[i] = value;
! }
! j++;
}
! /* Uninstall error callback function. */
! error_context_stack = errcontext.previous;
! /*
! * 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 = heap_form_tuple(tupdesc, festate->values, festate->nulls);
! tuplestore_puttuple(festate->tuples, tuple);
! }
! tuplestore_donestoring(festate->tuples);
}
/*
--- 802,954 ----
}
/*
* Create tuples from PGresult and store them into tuplestore.
*
* Caller must use PG_TRY block to catch exception and release PGresult
* surely.
*/
! static int
! query_row_processor(PGresult *res,
! const PGdataValue *columns,
! const char **errmsgp,
! void *param)
{
! ForeignScanState *node = (ForeignScanState *) param;
! int nfields = PQnfields(res);
! int i;
! int j;
! int attnum; /* number of non-dropped columns */
TupleTableSlot *slot = node->ss.ss_ScanTupleSlot;
TupleDesc tupdesc = slot->tts_tupleDescriptor;
! Form_pg_attribute *attrs = tupdesc->attrs;
! PgsqlFdwExecutionState *festate = (PgsqlFdwExecutionState *) node->fdw_state;
! AttInMetadata *attinmeta = festate->attinmeta;
! char *colbuf;
! int colbuflen;
! HeapTuple tuple;
ErrorContextCallback errcontext;
+ MemoryContext oldcontext;
! if (columns == NULL)
{
! /* 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)));
! /* First, ensure that the tuplestore is empty. */
! if (festate->tuples == NULL)
! {
! /*
! * Create tuplestore to store result of the query in per-query
! * context. Note that we use this memory context to avoid memory
! * leak in error cases.
! */
! oldcontext = MemoryContextSwitchTo(festate->scan_cxt);
! festate->tuples = tuplestore_begin_heap(false, false, work_mem);
! MemoryContextSwitchTo(oldcontext);
! }
! else
! {
! /* Clear old result just in case. */
! tuplestore_clear(festate->tuples);
! }
!
! return 1;
! }
/*
! * This function is called repeatedly until all result rows are processed,
! * so we should allow interrupt.
*/
! CHECK_FOR_INTERRUPTS();
! /*
! * Do the following work in a temp context that we reset after each tuple.
! * This cleans up not only the data we have direct access to, but any
! * cruft the I/O functions might leak.
! */
! oldcontext = MemoryContextSwitchTo(festate->temp_cxt);
!
! /* Initialize column value buffer. */
! colbuflen = 1024;
! colbuf = palloc(colbuflen);
!
! for (i = 0, j = 0; i < tupdesc->natts; i++)
{
! int len = columns[j].len;
! /* skip dropped columns. */
! if (attrs[i]->attisdropped)
! {
! festate->nulls[i] = true;
! continue;
! }
! /*
! * Set NULL indicator, and convert text representation to internal
! * representation if any.
! */
! if (len < 0)
! festate->nulls[i] = true;
! else
{
! Datum value;
!
! festate->nulls[i] = false;
!
! while (colbuflen < len + 1)
{
! colbuflen *= 2;
! colbuf = repalloc(colbuf, colbuflen);
}
+ memcpy(colbuf, columns[j].value, len);
+ colbuf[columns[j].len] = '\0';
/*
! * Set up and install callback to report where convertion error
! * occurs.
*/
! festate->cur_attno = i + 1;
! errcontext.callback = pgsql_fdw_error_callback;
! errcontext.arg = (void *) festate;
! errcontext.previous = error_context_stack;
! error_context_stack = &errcontext;
! value = InputFunctionCall(&attinmeta->attinfuncs[i],
! colbuf,
! attinmeta->attioparams[i],
! attinmeta->atttypmods[i]);
! festate->values[i] = value;
!
! /* Uninstall error context callback. */
! error_context_stack = errcontext.previous;
}
+ 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 = heap_form_tuple(tupdesc, festate->values, festate->nulls);
! tuplestore_puttuple(festate->tuples, tuple);
! /* Clean up */
! MemoryContextSwitchTo(oldcontext);
! MemoryContextReset(festate->temp_cxt);
! return 1;
}
/*
diff --git a/contrib/pgsql_fdw/pgsql_fdw.h b/contrib/pgsql_fdw/pgsql_fdw.h
index 5487d52..69b3b2d 100644
*** a/contrib/pgsql_fdw/pgsql_fdw.h
--- b/contrib/pgsql_fdw/pgsql_fdw.h
*************** int GetFetchCountOption(ForeignTable *ta
*** 29,36 ****
void deparseSimpleSql(StringInfo buf,
Oid relid,
PlannerInfo *root,
! RelOptInfo *baserel,
! ForeignTable *table);
void appendWhereClause(StringInfo buf,
bool has_where,
List *exprs,
--- 29,35 ----
void deparseSimpleSql(StringInfo buf,
Oid relid,
PlannerInfo *root,
! RelOptInfo *baserel);
void appendWhereClause(StringInfo buf,
bool has_where,
List *exprs,
diff --git a/contrib/pgsql_fdw/sql/pgsql_fdw.sql b/contrib/pgsql_fdw/sql/pgsql_fdw.sql
index 6692af7..6e43ea5 100644
*** a/contrib/pgsql_fdw/sql/pgsql_fdw.sql
--- b/contrib/pgsql_fdw/sql/pgsql_fdw.sql
*************** ALTER SERVER loopback1 OPTIONS (
*** 121,137 ****
--replication 'value'
);
ALTER SERVER loopback1 OPTIONS (user 'value'); -- ERROR
- ALTER SERVER loopback2 OPTIONS (ADD fetch_count '2');
ALTER USER MAPPING FOR public SERVER loopback1
OPTIONS (DROP user, DROP password);
ALTER USER MAPPING FOR public SERVER loopback1
OPTIONS (host 'value'); -- ERROR
ALTER FOREIGN TABLE ft1 OPTIONS (nspname 'S 1', relname 'T 1');
! ALTER FOREIGN TABLE ft2 OPTIONS (nspname 'S 1', relname 'T 1', fetch_count '100');
ALTER FOREIGN TABLE ft1 OPTIONS (invalid 'value'); -- ERROR
- ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count 'a'); -- ERROR
- ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count '0'); -- ERROR
- ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count '-1'); -- ERROR
ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (invalid 'value'); -- ERROR
ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (colname 'C 1');
ALTER FOREIGN TABLE ft2 ALTER COLUMN c1 OPTIONS (colname 'C 1');
--- 121,133 ----
--replication 'value'
);
ALTER SERVER loopback1 OPTIONS (user 'value'); -- ERROR
ALTER USER MAPPING FOR public SERVER loopback1
OPTIONS (DROP user, DROP password);
ALTER USER MAPPING FOR public SERVER loopback1
OPTIONS (host 'value'); -- ERROR
ALTER FOREIGN TABLE ft1 OPTIONS (nspname 'S 1', relname 'T 1');
! ALTER FOREIGN TABLE ft2 OPTIONS (nspname 'S 1', relname 'T 1');
ALTER FOREIGN TABLE ft1 OPTIONS (invalid 'value'); -- ERROR
ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (invalid 'value'); -- ERROR
ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (colname 'C 1');
ALTER FOREIGN TABLE ft2 ALTER COLUMN c1 OPTIONS (colname 'C 1');
*************** ALTER FOREIGN TABLE ft2 ALTER COLUMN c1
*** 144,155 ****
-- simple queries
-- ===================================================================
-- single table, with/without alias
! EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
! EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
-- with WHERE clause
! EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 >= '1';
SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 >= '1';
-- aggregate
SELECT COUNT(*) FROM ft1 t1;
--- 140,151 ----
-- simple queries
-- ===================================================================
-- single table, with/without alias
! EXPLAIN (COSTS false) SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
! EXPLAIN (COSTS false) SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
-- with WHERE clause
! EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 >= '1';
SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 >= '1';
-- aggregate
SELECT COUNT(*) FROM ft1 t1;
diff --git a/doc/src/sgml/pgsql-fdw.sgml b/doc/src/sgml/pgsql-fdw.sgml
index eb69f01..ee9c94a 100644
*** a/doc/src/sgml/pgsql-fdw.sgml
--- b/doc/src/sgml/pgsql-fdw.sgml
***************
*** 97,129 ****
</sect3>
- <sect3>
- <title>Cursor Options</title>
- <para>
- The <application>pgsql_fdw</application> always uses cursor to retrieve the
- result from external server. Users can control the behavior of cursor by
- setting cursor options to foreign table or foreign server. If an option
- is set to both objects, finer-grained setting is used. In other words,
- foreign table's setting overrides foreign server's setting.
- </para>
-
- <variablelist>
-
- <varlistentry>
- <term><literal>fetch_count</literal></term>
- <listitem>
- <para>
- This option specifies the number of rows to be fetched at a time.
- This option accepts only integer value larger than zero. The default
- setting is 10000.
- </para>
- </listitem>
- </varlistentry>
-
- </variablelist>
-
- </sect3>
-
</sect2>
<sect2>
--- 97,102 ----
*************** postgres=# EXPLAIN SELECT aid FROM pgben
*** 249,258 ****
Remote SQL: SELECT aid, NULL, abalance, NULL FROM public.pgbench_accounts
(3 rows)
</synopsis>
- <para>
- When you specify <literal>VERBOSE</> option, you can see actual DECLARE
- statement with cursor name which is used for the scan.
- </para>
</sect2>
<sect2>
--- 222,227 ----
Excuse me for cutting in,
2012/4/6 Shigeru HANADA <shigeru.hanada@gmail.com>:
To support foreign-table ANALYZE by adding a new hook, we would need a
mechanism (or at least documented guide lines) to manage the chain of
hook functions, because such hook might be used by multiple FDWs (or
other plug-ins) at the same time. A wrongly-written plug-in can easily
break the hook chain. We might need to provide register/unregister API
for this hook point, like RegisterResourceReleaseCallback, and call each
registered function until either of them processes the request. Is
there any other hook point which has similar issue?
+1
Plain hook mechanism in PostgreSQL is, I think, to hang a bunch
of faceless callbacks to be registered, unregistered and called
all together. And it does not fit to manage individual callbacks
which may be registered or unregistered in arbitrary order and
are preferred to be called separately.
Although we provide RegisterResourceReleaseCallback-like staff,
it seems far more complicated than the additional field in
FdwRoutine and some analyze_rel() modifications in core-side, and
confirmation of whether it's really the time for me should be a
reluctant work in plugin-side.
Of cource, I don't think there will be so many fdw-analyze
callbacks registered but two seems sufficient.
The current mods in analyze_rel() does not look definitive, but
it does not look so bad and seems more stable than simple hook
point which will be abandoned before long.
Another concern is the place where we hook the process of ANALYZE. IOW,
how much portion of ANALYZE should be overridable? Replacing
analyze_rel or do_analyze_rel wholly requires plug-ins to copy most of
codes from original function in order to implement hook function. From
the perspective of FDW author, I think that row sampling
(acquire_sample_rows) function seems handy place to hook.
regards,
--
Kyotaro Horiguchi
NTT Open Source Software Center
Shigeru HANADA <shigeru.hanada@gmail.com> writes:
(2012/04/06 1:29), Tom Lane wrote:
The one thing that seems pretty clear from this discussion is that one
size doesn't fit all. I think we really ought to provide a hook so that
the FDW can determine whether ANALYZE applies to its foreign tables at
all, and how to obtain the sample rows if it does.
To support foreign-table ANALYZE by adding a new hook, we would need a
mechanism (or at least documented guide lines) to manage the chain of
hook functions, because such hook might be used by multiple FDWs (or
other plug-ins) at the same time. A wrongly-written plug-in can easily
break the hook chain.
Sorry, I used the word "hook" loosely, not with the intention of meaning
a global function pointer. We should of course implement this with
another per-FDW method pointer, as in the postgresql-analyze patch
series.
Another concern is the place where we hook the process of ANALYZE. IOW,
how much portion of ANALYZE should be overridable?
Not much, IMO. The FDW should be able to decide whether or not to
analyze a particular table, and it should be in charge of implementing
its own version of acquire_sample_rows, but no more than that. In
particular I do not like the specific way it's done in the v7 patch
(I've not looked at v8 yet) because the interposed logic has a
hard-wired assumption that foreign tables do not have inheritance
children. I think that assumption has a life expectancy measured in
months at most, and I don't want to have to try to fix every FDW when
it changes. But I think we can easily revise the hook details to fix
that, and I'm hoping to get that done today.
regards, tom lane
On Fri, Apr 6, 2012 at 11:20 PM, Tom Lane <tgl@sss.pgh.pa.us> wrote:
Another concern is the place where we hook the process of ANALYZE. IOW,
how much portion of ANALYZE should be overridable?Not much, IMO. The FDW should be able to decide whether or not to
analyze a particular table, and it should be in charge of implementing
its own version of acquire_sample_rows, but no more than that.
ISTM that we have rough consensus about what FDW should do for an
ANALYZE request. FDW should choose either of:
a) get sample rows and return them to backend
b) tell backend that the FDW has nothing to do for the request
In
particular I do not like the specific way it's done in the v7 patch
(I've not looked at v8 yet) because the interposed logic has a
hard-wired assumption that foreign tables do not have inheritance
children. I think that assumption has a life expectancy measured in
months at most, and I don't want to have to try to fix every FDW when
it changes. But I think we can easily revise the hook details to fix
that, and I'm hoping to get that done today.
I'll try implementing the design you suggested.
Regards,
--
Shigeru Hanada
Shigeru Hanada <shigeru.hanada@gmail.com> writes:
On Fri, Apr 6, 2012 at 11:20 PM, Tom Lane <tgl@sss.pgh.pa.us> wrote:
In
particular I do not like the specific way it's done in the v7 patch
(I've not looked at v8 yet) because the interposed logic has a
hard-wired assumption that foreign tables do not have inheritance
children. I think that assumption has a life expectancy measured in
months at most, and I don't want to have to try to fix every FDW when
it changes. But I think we can easily revise the hook details to fix
that, and I'm hoping to get that done today.
I'll try implementing the design you suggested.
I've already got it fixed up ...
regards, tom lane
(2012/04/07 1:38), Tom Lane wrote:
Shigeru Hanada<shigeru.hanada@gmail.com> writes:
On Fri, Apr 6, 2012 at 11:20 PM, Tom Lane<tgl@sss.pgh.pa.us> wrote:
In
particular I do not like the specific way it's done in the v7 patch
(I've not looked at v8 yet) because the interposed logic has a
hard-wired assumption that foreign tables do not have inheritance
children. I think that assumption has a life expectancy measured in
months at most, and I don't want to have to try to fix every FDW when
it changes. But I think we can easily revise the hook details to fix
that, and I'm hoping to get that done today.I'll try implementing the design you suggested.
I've already got it fixed up ...
I've updated pgsql_fdw so that it can collect statistics from foreign
data with new FDW API.
Other changes from latest version are:
- Now pgsql_fdw uses libpq row processor API to get rid of SQL-level cursor.
- Introduced new struct ErrorPos to integrate error reporting in foreign
scan and analyze.
- Some refactoring, such as removing unnecessary parameter.
- Fix typos.
Regards,
--
Shigeru HANADA
Attachments:
pgsql_fdw_v20.patchtext/plain; charset=Shift_JIS; name=pgsql_fdw_v20.patchDownload
diff --git a/contrib/Makefile b/contrib/Makefile
new file mode 100644
index d230451..9bc35dd
*** a/contrib/Makefile
--- b/contrib/Makefile
*************** SUBDIRS = \
*** 42,47 ****
--- 42,48 ----
pgbench \
pgcrypto \
pgrowlocks \
+ pgsql_fdw \
pgstattuple \
seg \
spi \
diff --git a/contrib/README b/contrib/README
new file mode 100644
index a1d42a1..d3fa211
*** a/contrib/README
--- b/contrib/README
*************** pgrowlocks -
*** 158,163 ****
--- 158,167 ----
A function to return row locking information
by Tatsuo Ishii <ishii@sraoss.co.jp>
+ pgsql_fdw -
+ Foreign-data wrapper for external PostgreSQL servers.
+ by Shigeru Hanada <shigeru.hanada@gmail.com>
+
pgstattuple -
Functions to return statistics about "dead" tuples and free
space within a table
diff --git a/contrib/pgsql_fdw/.gitignore b/contrib/pgsql_fdw/.gitignore
new file mode 100644
index ...0854728
*** a/contrib/pgsql_fdw/.gitignore
--- b/contrib/pgsql_fdw/.gitignore
***************
*** 0 ****
--- 1,4 ----
+ # Generated subdirectories
+ /results/
+ *.o
+ *.so
diff --git a/contrib/pgsql_fdw/Makefile b/contrib/pgsql_fdw/Makefile
new file mode 100644
index ...6381365
*** a/contrib/pgsql_fdw/Makefile
--- b/contrib/pgsql_fdw/Makefile
***************
*** 0 ****
--- 1,22 ----
+ # contrib/pgsql_fdw/Makefile
+
+ MODULE_big = pgsql_fdw
+ OBJS = pgsql_fdw.o option.o deparse.o connection.o
+ PG_CPPFLAGS = -I$(libpq_srcdir)
+ SHLIB_LINK = $(libpq)
+
+ EXTENSION = pgsql_fdw
+ DATA = pgsql_fdw--1.0.sql
+
+ REGRESS = pgsql_fdw
+
+ ifdef USE_PGXS
+ PG_CONFIG = pg_config
+ PGXS := $(shell $(PG_CONFIG) --pgxs)
+ include $(PGXS)
+ else
+ subdir = contrib/pgsql_fdw
+ top_builddir = ../..
+ include $(top_builddir)/src/Makefile.global
+ include $(top_srcdir)/contrib/contrib-global.mk
+ endif
diff --git a/contrib/pgsql_fdw/connection.c b/contrib/pgsql_fdw/connection.c
new file mode 100644
index ...7cb448b
*** a/contrib/pgsql_fdw/connection.c
--- b/contrib/pgsql_fdw/connection.c
***************
*** 0 ****
--- 1,555 ----
+ /*-------------------------------------------------------------------------
+ *
+ * connection.c
+ * Connection management for pgsql_fdw
+ *
+ * Portions Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/connection.c
+ *
+ *-------------------------------------------------------------------------
+ */
+ #include "postgres.h"
+
+ #include "access/xact.h"
+ #include "catalog/pg_type.h"
+ #include "foreign/foreign.h"
+ #include "funcapi.h"
+ #include "libpq-fe.h"
+ #include "mb/pg_wchar.h"
+ #include "miscadmin.h"
+ #include "utils/array.h"
+ #include "utils/builtins.h"
+ #include "utils/hsearch.h"
+ #include "utils/memutils.h"
+ #include "utils/resowner.h"
+ #include "utils/tuplestore.h"
+
+ #include "pgsql_fdw.h"
+ #include "connection.h"
+
+ /* ============================================================================
+ * Connection management functions
+ * ==========================================================================*/
+
+ /*
+ * Connection cache entry managed with hash table.
+ */
+ typedef struct ConnCacheEntry
+ {
+ /* hash key must be first */
+ Oid serverid; /* oid of foreign server */
+ Oid userid; /* oid of local user */
+
+ bool use_tx; /* true when using remote transaction */
+ 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);
+ static void begin_remote_tx(PGconn *conn);
+ static void abort_remote_tx(PGconn *conn);
+
+ /*
+ * 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.
+ *
+ * When use_tx is true, remote transaction is started if caller is the only
+ * user of the connection. Isolation level of the remote transaction is same
+ * as local transaction, and remote transaction will be aborted when last
+ * user release.
+ *
+ * TODO: Note that caching connections requires a mechanism to detect change of
+ * FDW object to invalidate already established connections.
+ */
+ PGconn *
+ GetConnection(ForeignServer *server, UserMapping *user, bool use_tx)
+ {
+ bool found;
+ ConnCacheEntry *entry;
+ ConnCacheEntry key;
+
+ /* initialize connection cache if it isn't */
+ if (FSConnectionHash == NULL)
+ {
+ HASHCTL ctl;
+
+ /* hash key is a pair of oids: serverid and userid */
+ MemSet(&ctl, 0, sizeof(ctl));
+ ctl.keysize = sizeof(Oid) + sizeof(Oid);
+ ctl.entrysize = sizeof(ConnCacheEntry);
+ ctl.hash = tag_hash;
+ ctl.match = memcmp;
+ ctl.keycopy = memcpy;
+ /* allocate FSConnectionHash in the cache context */
+ ctl.hcxt = CacheMemoryContext;
+ FSConnectionHash = hash_create("Foreign Connections", 32,
+ &ctl,
+ HASH_ELEM | HASH_CONTEXT |
+ HASH_FUNCTION | HASH_COMPARE |
+ HASH_KEYCOPY);
+ }
+
+ /* Create key value for the entry. */
+ MemSet(&key, 0, sizeof(key));
+ key.serverid = server->serverid;
+ key.userid = GetOuterUserId();
+
+ /*
+ * Find cached entry for requested connection. If we couldn't find,
+ * callback function of ResourceOwner should be registered to clean the
+ * connection up on error including user interrupt.
+ */
+ entry = hash_search(FSConnectionHash, &key, HASH_ENTER, &found);
+ if (!found)
+ {
+ entry->refs = 0;
+ entry->use_tx = false;
+ entry->conn = NULL;
+ RegisterResourceReleaseCallback(cleanup_connection, entry);
+ }
+
+ /*
+ * We don't check the health of cached connection here, because it would
+ * require some overhead. Broken connection and its cache entry will be
+ * cleaned up when the connection is actually used.
+ */
+
+ /*
+ * If cache entry doesn't have connection, we have to establish new
+ * connection.
+ */
+ if (entry->conn == NULL)
+ {
+ /*
+ * Use PG_TRY block to ensure closing connection on error.
+ */
+ PG_TRY();
+ {
+ /*
+ * Connect to the foreign PostgreSQL server, and store it in cache
+ * entry to keep new connection.
+ * Note: key items of entry has already been initialized in
+ * hash_search(HASH_ENTER).
+ */
+ entry->conn = connect_pg_server(server, user);
+ }
+ PG_CATCH();
+ {
+ /* Clear connection cache entry on error case. */
+ PQfinish(entry->conn);
+ entry->refs = 0;
+ entry->use_tx = false;
+ entry->conn = NULL;
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+ }
+
+ /* Increase connection reference counter. */
+ entry->refs++;
+
+ /*
+ * If requester is the only referrer of this connection, start transaction
+ * with the same isolation level as the local transaction we are in.
+ * We need to remember whether this connection uses remote transaction to
+ * abort it when this connection is released completely.
+ */
+ if (use_tx && entry->refs == 1)
+ begin_remote_tx(entry->conn);
+ entry->use_tx = use_tx;
+
+
+ return entry->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 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. Those two object hold only libpq options.
+ * Extra 3 items are for:
+ * *) fallback_application_name
+ * *) client_encoding
+ * *) NULL termination (end marker)
+ *
+ * Note: We don't omit any parameters even target database might be older
+ * than local, because unexpected parameters are just ignored.
+ */
+ n = list_length(server->options) + list_length(user->options) + 3;
+ 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 += ExtractConnectionOptions(server->options,
+ all_keywords + n, all_values + n);
+ n += ExtractConnectionOptions(user->options,
+ all_keywords + n, all_values + n);
+ all_keywords[n] = all_values[n] = NULL;
+
+ for (i = 0, j = 0; all_keywords[i]; i++)
+ {
+ keywords[j] = all_keywords[i];
+ values[j] = all_values[i];
+ j++;
+ }
+
+ /* Use "pgsql_fdw" as fallback_application_name. */
+ keywords[j] = "fallback_application_name";
+ values[j++] = "pgsql_fdw";
+
+ /* Set client_encoding so that libpq can convert encoding properly. */
+ keywords[j] = "client_encoding";
+ values[j++] = GetDatabaseEncodingName();
+
+ keywords[j] = values[j] = NULL;
+ pfree(all_keywords);
+ pfree(all_values);
+
+ /* 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.")));
+ }
+
+ return conn;
+ }
+
+ /*
+ * Start remote transaction with proper isolation level.
+ */
+ static void
+ begin_remote_tx(PGconn *conn)
+ {
+ const char *sql = NULL; /* keep compiler quiet. */
+ PGresult *res;
+
+ switch (XactIsoLevel)
+ {
+ case XACT_READ_UNCOMMITTED:
+ case XACT_READ_COMMITTED:
+ case XACT_REPEATABLE_READ:
+ sql = "START TRANSACTION ISOLATION LEVEL REPEATABLE READ";
+ break;
+ case XACT_SERIALIZABLE:
+ sql = "START TRANSACTION ISOLATION LEVEL SERIALIZABLE";
+ break;
+ default:
+ elog(ERROR, "unexpected isolation level: %d", XactIsoLevel);
+ break;
+ }
+
+ elog(DEBUG3, "starting remote transaction with \"%s\"", sql);
+
+ res = PQexec(conn, sql);
+ if (PQresultStatus(res) != PGRES_COMMAND_OK)
+ {
+ PQclear(res);
+ elog(ERROR, "could not start transaction: %s", PQerrorMessage(conn));
+ }
+ PQclear(res);
+ }
+
+ static void
+ abort_remote_tx(PGconn *conn)
+ {
+ PGresult *res;
+
+ elog(DEBUG3, "aborting remote transaction");
+
+ res = PQexec(conn, "ABORT TRANSACTION");
+ if (PQresultStatus(res) != PGRES_COMMAND_OK)
+ {
+ PQclear(res);
+ elog(ERROR, "could not abort transaction: %s", PQerrorMessage(conn));
+ }
+ PQclear(res);
+ }
+
+ /*
+ * 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 sequentially 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)
+ {
+ hash_seq_term(&scan);
+ break;
+ }
+ }
+
+ /* If the released connection was an orphan, just close it. */
+ if (entry == NULL)
+ {
+ PQfinish(conn);
+ return;
+ }
+
+ /*
+ * If this connection uses remote transaction and caller is the last user,
+ * abort remote transaction and forget about it.
+ */
+ if (entry->use_tx && entry->refs == 1)
+ {
+ abort_remote_tx(conn);
+ entry->use_tx = false;
+ }
+
+ /*
+ * Decrease reference counter of this connection. Even if the caller was
+ * the last referrer, we don't unregister it from cache.
+ */
+ entry->refs--;
+ }
+
+ /*
+ * 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 need to clean up at end of subtransactions, because they might
+ * be recovered to consistent state with savepoints.
+ */
+ if (!isTopLevel)
+ return;
+
+ /*
+ * Here, it must be after abort of top level transaction. Disconnect and
+ * forget every referrer.
+ */
+ if (entry->conn != NULL)
+ {
+ PQfinish(entry->conn);
+ entry->refs = 0;
+ entry->conn = NULL;
+ }
+ }
+
+ /*
+ * Get list of connections currently active.
+ */
+ Datum pgsql_fdw_get_connections(PG_FUNCTION_ARGS);
+ PG_FUNCTION_INFO_V1(pgsql_fdw_get_connections);
+ Datum
+ pgsql_fdw_get_connections(PG_FUNCTION_ARGS)
+ {
+ ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+ HASH_SEQ_STATUS scan;
+ ConnCacheEntry *entry;
+ MemoryContext oldcontext = CurrentMemoryContext;
+ Tuplestorestate *tuplestore;
+ TupleDesc tupdesc;
+
+ /* We return list of connection with storing them in a Tuplestore. */
+ rsinfo->returnMode = SFRM_Materialize;
+ rsinfo->setResult = NULL;
+ rsinfo->setDesc = NULL;
+
+ /* Create tuplestore and copy of TupleDesc in per-query context. */
+ MemoryContextSwitchTo(rsinfo->econtext->ecxt_per_query_memory);
+
+ tupdesc = CreateTemplateTupleDesc(2, false);
+ TupleDescInitEntry(tupdesc, 1, "srvid", OIDOID, -1, 0);
+ TupleDescInitEntry(tupdesc, 2, "usesysid", OIDOID, -1, 0);
+ rsinfo->setDesc = tupdesc;
+
+ tuplestore = tuplestore_begin_heap(false, false, work_mem);
+ rsinfo->setResult = tuplestore;
+
+ MemoryContextSwitchTo(oldcontext);
+
+ /*
+ * We need to scan sequentially since we use the address to find
+ * appropriate PGconn from the hash table.
+ */
+ if (FSConnectionHash != NULL)
+ {
+ hash_seq_init(&scan, FSConnectionHash);
+ while ((entry = (ConnCacheEntry *) hash_seq_search(&scan)))
+ {
+ Datum values[2];
+ bool nulls[2];
+ HeapTuple tuple;
+
+ /* Ignore inactive connections */
+ if (PQstatus(entry->conn) != CONNECTION_OK)
+ continue;
+
+ /*
+ * Ignore other users' connections if current user isn't a
+ * superuser.
+ */
+ if (!superuser() && entry->userid != GetUserId())
+ continue;
+
+ values[0] = ObjectIdGetDatum(entry->serverid);
+ values[1] = ObjectIdGetDatum(entry->userid);
+ nulls[0] = false;
+ nulls[1] = false;
+
+ tuple = heap_formtuple(tupdesc, values, nulls);
+ tuplestore_puttuple(tuplestore, tuple);
+ }
+ }
+ tuplestore_donestoring(tuplestore);
+
+ PG_RETURN_VOID();
+ }
+
+ /*
+ * Discard persistent connection designated by given connection name.
+ */
+ Datum pgsql_fdw_disconnect(PG_FUNCTION_ARGS);
+ PG_FUNCTION_INFO_V1(pgsql_fdw_disconnect);
+ Datum
+ pgsql_fdw_disconnect(PG_FUNCTION_ARGS)
+ {
+ Oid serverid = PG_GETARG_OID(0);
+ Oid userid = PG_GETARG_OID(1);
+ ConnCacheEntry key;
+ ConnCacheEntry *entry = NULL;
+ bool found;
+
+ /* Non-superuser can't discard other users' connection. */
+ if (!superuser() && userid != GetOuterUserId())
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_RESOURCES),
+ errmsg("only superuser can discard other user's connection")));
+
+ /*
+ * If no connection has been established, or no such connections, just
+ * return "NG" to indicate nothing has done.
+ */
+ if (FSConnectionHash == NULL)
+ PG_RETURN_TEXT_P(cstring_to_text("NG"));
+
+ key.serverid = serverid;
+ key.userid = userid;
+ entry = hash_search(FSConnectionHash, &key, HASH_FIND, &found);
+ if (!found)
+ PG_RETURN_TEXT_P(cstring_to_text("NG"));
+
+ /* Discard cached connection, and clear reference counter. */
+ PQfinish(entry->conn);
+ entry->refs = 0;
+ entry->conn = NULL;
+
+ PG_RETURN_TEXT_P(cstring_to_text("OK"));
+ }
diff --git a/contrib/pgsql_fdw/connection.h b/contrib/pgsql_fdw/connection.h
new file mode 100644
index ...4427f56
*** a/contrib/pgsql_fdw/connection.h
--- b/contrib/pgsql_fdw/connection.h
***************
*** 0 ****
--- 1,25 ----
+ /*-------------------------------------------------------------------------
+ *
+ * connection.h
+ * Connection management for pgsql_fdw
+ *
+ * Portions Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_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, bool use_tx);
+ void ReleaseConnection(PGconn *conn);
+
+ #endif /* CONNECTION_H */
diff --git a/contrib/pgsql_fdw/deparse.c b/contrib/pgsql_fdw/deparse.c
new file mode 100644
index ...e1bbf6b
*** a/contrib/pgsql_fdw/deparse.c
--- b/contrib/pgsql_fdw/deparse.c
***************
*** 0 ****
--- 1,287 ----
+ /*-------------------------------------------------------------------------
+ *
+ * deparse.c
+ * query deparser for PostgreSQL
+ *
+ * Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/deparse.c
+ *
+ *-------------------------------------------------------------------------
+ */
+ #include "postgres.h"
+
+ #include "access/transam.h"
+ #include "catalog/pg_class.h"
+ #include "commands/defrem.h"
+ #include "foreign/foreign.h"
+ #include "lib/stringinfo.h"
+ #include "nodes/nodeFuncs.h"
+ #include "nodes/nodes.h"
+ #include "nodes/makefuncs.h"
+ #include "optimizer/clauses.h"
+ #include "optimizer/var.h"
+ #include "parser/parsetree.h"
+ #include "utils/builtins.h"
+ #include "utils/lsyscache.h"
+
+ #include "pgsql_fdw.h"
+
+ /*
+ * Context for walk-through the expression tree.
+ */
+ typedef struct foreign_executable_cxt
+ {
+ PlannerInfo *root;
+ RelOptInfo *foreignrel;
+ } foreign_executable_cxt;
+
+ /*
+ * Get string representation which can be used in SQL statement from a node.
+ */
+ static void deparseRelation(StringInfo buf, Oid relid, PlannerInfo *root,
+ bool need_prefix);
+ static void deparseVar(StringInfo buf, Var *node, PlannerInfo *root,
+ bool need_prefix);
+
+ /*
+ * Deparse query representation into SQL statement which suits for remote
+ * PostgreSQL server. This function basically creates simple query string
+ * which consists of only SELECT, FROM clauses.
+ */
+ void
+ deparseSimpleSql(StringInfo buf,
+ Oid relid,
+ PlannerInfo *root,
+ RelOptInfo *baserel)
+ {
+ StringInfoData foreign_relname;
+ bool first;
+ AttrNumber attr;
+ List *attr_used = NIL; /* List of AttNumber used in the query */
+
+ initStringInfo(buf);
+ initStringInfo(&foreign_relname);
+
+ /*
+ * First of all, determine which column should be retrieved for this scan.
+ *
+ * We do this before deparsing SELECT clause because attributes which are
+ * not used in neither reltargetlist nor baserel->baserestrictinfo, quals
+ * evaluated on local, can be replaced with literal "NULL" in the SELECT
+ * clause to reduce overhead of tuple handling tuple and data transfer.
+ */
+ if (baserel->baserestrictinfo != NIL)
+ {
+ ListCell *lc;
+
+ foreach (lc, baserel->baserestrictinfo)
+ {
+ RestrictInfo *ri = (RestrictInfo *) lfirst(lc);
+ List *attrs;
+
+ /*
+ * We need to know which attributes are used in qual evaluated
+ * on the local server, because they should be listed in the
+ * SELECT clause of remote query. We can ignore attributes
+ * which are referenced only in ORDER BY/GROUP BY clause because
+ * such attributes has already been kept in reltargetlist.
+ */
+ attrs = pull_var_clause((Node *) ri->clause,
+ PVC_RECURSE_AGGREGATES,
+ PVC_RECURSE_PLACEHOLDERS);
+ attr_used = list_union(attr_used, attrs);
+ }
+ }
+
+ /*
+ * deparse SELECT clause
+ *
+ * List attributes which are in either target list or local restriction.
+ * Unused attributes are replaced with a literal "NULL" for optimization.
+ *
+ * Note that nothing is added for dropped columns, though tuple constructor
+ * function requires entries for dropped columns. Such entries must be
+ * initialized with NULL before calling tuple constructor.
+ */
+ appendStringInfo(buf, "SELECT ");
+ attr_used = list_union(attr_used, baserel->reltargetlist);
+ first = true;
+ for (attr = 1; attr <= baserel->max_attr; attr++)
+ {
+ RangeTblEntry *rte = root->simple_rte_array[baserel->relid];
+ Var *var = NULL;
+ ListCell *lc;
+
+ /* Ignore dropped attributes. */
+ if (get_rte_attribute_is_dropped(rte, attr))
+ continue;
+
+ if (!first)
+ appendStringInfo(buf, ", ");
+ first = false;
+
+ /*
+ * We use linear search here, but it wouldn't be problem since
+ * attr_used seems to not become so large.
+ */
+ foreach (lc, attr_used)
+ {
+ var = lfirst(lc);
+ if (var->varattno == attr)
+ break;
+ var = NULL;
+ }
+ if (var != NULL)
+ deparseVar(buf, var, root, false);
+ else
+ appendStringInfo(buf, "NULL");
+ }
+ appendStringInfoChar(buf, ' ');
+
+ /*
+ * deparse FROM clause, including alias if any
+ */
+ appendStringInfo(buf, "FROM ");
+ deparseRelation(buf, relid, root, true);
+
+ elog(DEBUG3, "Remote SQL: %s", buf->data);
+ }
+
+ /*
+ * Deparse node into buf, with relation qualifier if need_prefix was true. If
+ * node is a column of a foreign table, use value of colname FDW option (if any)
+ * instead of attribute name.
+ */
+ static void
+ deparseVar(StringInfo buf,
+ Var *node,
+ PlannerInfo *root,
+ bool need_prefix)
+ {
+ RangeTblEntry *rte;
+ char *colname = NULL;
+ const char *q_colname = NULL;
+ List *options;
+ ListCell *lc;
+
+ /* node must not be any of OUTER_VAR,INNER_VAR and INDEX_VAR. */
+ Assert(node->varno >= 1 && node->varno <= root->simple_rel_array_size);
+
+ /* Get RangeTblEntry from array in PlannerInfo. */
+ rte = root->simple_rte_array[node->varno];
+
+ /*
+ * If the node is a column of a foreign table, and it has colname FDW
+ * option, use its value.
+ */
+ if (rte->relkind == RELKIND_FOREIGN_TABLE)
+ {
+ options = GetForeignColumnOptions(rte->relid, node->varattno);
+ foreach(lc, options)
+ {
+ DefElem *def = (DefElem *) lfirst(lc);
+
+ if (strcmp(def->defname, "colname") == 0)
+ {
+ colname = defGetString(def);
+ break;
+ }
+ }
+ }
+
+ /*
+ * If the node refers a column of a regular table or it doesn't have colname
+ * FDW option, use attribute name.
+ */
+ if (colname == NULL)
+ colname = get_attname(rte->relid, node->varattno);
+
+ if (need_prefix)
+ {
+ char *aliasname;
+ const char *q_aliasname;
+
+ if (rte->eref != NULL && rte->eref->aliasname != NULL)
+ aliasname = rte->eref->aliasname;
+ else if (rte->alias != NULL && rte->alias->aliasname != NULL)
+ aliasname = rte->alias->aliasname;
+
+ q_aliasname = quote_identifier(aliasname);
+ appendStringInfo(buf, "%s.", q_aliasname);
+ }
+
+ q_colname = quote_identifier(colname);
+ appendStringInfo(buf, "%s", q_colname);
+ }
+
+ /*
+ * Deparse table which has relid as oid into buf, with schema qualifier if
+ * need_prefix was true. If relid points a foreign table, use value of relname
+ * FDW option (if any) instead of relation's name. Similarly, nspname FDW
+ * option overrides schema name.
+ */
+ static void
+ deparseRelation(StringInfo buf,
+ Oid relid,
+ PlannerInfo *root,
+ bool need_prefix)
+ {
+ int i;
+ RangeTblEntry *rte = NULL;
+ ListCell *lc;
+ const char *nspname = NULL; /* plain namespace name */
+ const char *relname = NULL; /* plain relation name */
+ const char *q_nspname; /* quoted namespace name */
+ const char *q_relname; /* quoted relation name */
+
+ /* Find RangeTblEntry for the relation from PlannerInfo. */
+ for (i = 1; i < root->simple_rel_array_size; i++)
+ {
+ if (root->simple_rte_array[i]->relid == relid)
+ {
+ rte = root->simple_rte_array[i];
+ break;
+ }
+ }
+ if (rte == NULL)
+ elog(ERROR, "relation with OID %u is not used in the query", relid);
+
+ /* If target is a foreign table, obtain additional catalog information. */
+ if (rte->relkind == RELKIND_FOREIGN_TABLE)
+ {
+ ForeignTable *table = GetForeignTable(rte->relid);
+
+ /*
+ * Use value of FDW options if any, instead of the name of object
+ * itself.
+ */
+ foreach(lc, table->options)
+ {
+ DefElem *def = (DefElem *) lfirst(lc);
+
+ if (need_prefix && strcmp(def->defname, "nspname") == 0)
+ nspname = defGetString(def);
+ else if (strcmp(def->defname, "relname") == 0)
+ relname = defGetString(def);
+ }
+ }
+
+ /* Quote each identifier, if necessary. */
+ if (need_prefix)
+ {
+ if (nspname == NULL)
+ nspname = get_namespace_name(get_rel_namespace(relid));
+ q_nspname = quote_identifier(nspname);
+ }
+
+ if (relname == NULL)
+ relname = get_rel_name(relid);
+ q_relname = quote_identifier(relname);
+
+ /* Construct relation reference into the buffer. */
+ if (need_prefix)
+ appendStringInfo(buf, "%s.", q_nspname);
+ appendStringInfo(buf, "%s", q_relname);
+ }
diff --git a/contrib/pgsql_fdw/expected/pgsql_fdw.out b/contrib/pgsql_fdw/expected/pgsql_fdw.out
new file mode 100644
index ...1a20874
*** a/contrib/pgsql_fdw/expected/pgsql_fdw.out
--- b/contrib/pgsql_fdw/expected/pgsql_fdw.out
***************
*** 0 ****
--- 1,584 ----
+ -- ===================================================================
+ -- create FDW objects
+ -- ===================================================================
+ -- Clean up in case a prior regression run failed
+ -- Suppress NOTICE messages when roles don't exist
+ SET client_min_messages TO 'error';
+ DROP ROLE IF EXISTS pgsql_fdw_user;
+ RESET client_min_messages;
+ CREATE ROLE pgsql_fdw_user LOGIN SUPERUSER;
+ SET SESSION AUTHORIZATION 'pgsql_fdw_user';
+ CREATE EXTENSION pgsql_fdw;
+ CREATE SERVER loopback1 FOREIGN DATA WRAPPER pgsql_fdw;
+ CREATE SERVER loopback2 FOREIGN DATA WRAPPER pgsql_fdw
+ OPTIONS (dbname 'contrib_regression');
+ CREATE USER MAPPING FOR public SERVER loopback1
+ OPTIONS (user 'value', password 'value');
+ CREATE USER MAPPING FOR pgsql_fdw_user SERVER loopback2;
+ CREATE FOREIGN TABLE ft1 (
+ c0 int,
+ c1 int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10)
+ ) SERVER loopback2;
+ ALTER FOREIGN TABLE ft1 DROP COLUMN c0;
+ CREATE FOREIGN TABLE ft2 (
+ c0 int,
+ c1 int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10)
+ ) SERVER loopback2;
+ ALTER FOREIGN TABLE ft2 DROP COLUMN c0;
+ -- ===================================================================
+ -- create objects used through FDW
+ -- ===================================================================
+ CREATE SCHEMA "S 1";
+ CREATE TABLE "S 1"."T 1" (
+ "C 1" int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10),
+ CONSTRAINT t1_pkey PRIMARY KEY ("C 1")
+ );
+ NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "t1_pkey" for table "T 1"
+ CREATE TABLE "S 1"."T 2" (
+ c1 int NOT NULL,
+ c2 text,
+ CONSTRAINT t2_pkey PRIMARY KEY (c1)
+ );
+ NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "t2_pkey" for table "T 2"
+ BEGIN;
+ TRUNCATE "S 1"."T 1";
+ INSERT INTO "S 1"."T 1"
+ SELECT id,
+ id % 10,
+ to_char(id, 'FM00000'),
+ '1970-01-01'::timestamptz + ((id % 100) || ' days')::interval,
+ '1970-01-01'::timestamp + ((id % 100) || ' days')::interval,
+ id % 10,
+ id % 10
+ FROM generate_series(1, 1000) id;
+ TRUNCATE "S 1"."T 2";
+ INSERT INTO "S 1"."T 2"
+ SELECT id,
+ 'AAA' || to_char(id, 'FM000')
+ FROM generate_series(1, 100) id;
+ COMMIT;
+ -- ===================================================================
+ -- tests for pgsql_fdw_validator
+ -- ===================================================================
+ ALTER FOREIGN DATA WRAPPER pgsql_fdw OPTIONS (host 'value'); -- ERROR
+ ERROR: invalid option "host"
+ HINT: Valid options in this context are:
+ -- requiressl, krbsrvname and gsslib are omitted because they depend on
+ -- configure option
+ ALTER SERVER loopback1 OPTIONS (
+ authtype 'value',
+ service 'value',
+ connect_timeout 'value',
+ dbname 'value',
+ host 'value',
+ hostaddr 'value',
+ port 'value',
+ --client_encoding 'value',
+ tty 'value',
+ options 'value',
+ application_name 'value',
+ --fallback_application_name 'value',
+ keepalives 'value',
+ keepalives_idle 'value',
+ keepalives_interval 'value',
+ -- requiressl 'value',
+ sslcompression 'value',
+ sslmode 'value',
+ sslcert 'value',
+ sslkey 'value',
+ sslrootcert 'value',
+ sslcrl 'value'
+ --requirepeer 'value',
+ -- krbsrvname 'value',
+ -- gsslib 'value',
+ --replication 'value'
+ );
+ ALTER SERVER loopback1 OPTIONS (user 'value'); -- ERROR
+ ERROR: invalid option "user"
+ HINT: Valid options in this context are: authtype, service, connect_timeout, dbname, host, hostaddr, port, tty, options, application_name, keepalives, keepalives_idle, keepalives_interval, keepalives_count, requiressl, sslcompression, sslmode, sslcert, sslkey, sslrootcert, sslcrl, requirepeer, krbsrvname, gsslib
+ ALTER USER MAPPING FOR public SERVER loopback1
+ OPTIONS (DROP user, DROP password);
+ ALTER USER MAPPING FOR public SERVER loopback1
+ OPTIONS (host 'value'); -- ERROR
+ ERROR: invalid option "host"
+ HINT: Valid options in this context are: user, password
+ ALTER FOREIGN TABLE ft1 OPTIONS (nspname 'S 1', relname 'T 1');
+ ALTER FOREIGN TABLE ft2 OPTIONS (nspname 'S 1', relname 'T 1');
+ ALTER FOREIGN TABLE ft1 OPTIONS (invalid 'value'); -- ERROR
+ ERROR: invalid option "invalid"
+ HINT: Valid options in this context are: nspname, relname
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (invalid 'value'); -- ERROR
+ ERROR: invalid option "invalid"
+ HINT: Valid options in this context are: colname
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (colname 'C 1');
+ ALTER FOREIGN TABLE ft2 ALTER COLUMN c1 OPTIONS (colname 'C 1');
+ \dew+
+ List of foreign-data wrappers
+ Name | Owner | Handler | Validator | Access privileges | FDW Options | Description
+ -----------+----------------+-------------------+---------------------+-------------------+-------------+-------------
+ pgsql_fdw | pgsql_fdw_user | pgsql_fdw_handler | pgsql_fdw_validator | | |
+ (1 row)
+
+ \des+
+ List of foreign servers
+ Name | Owner | Foreign-data wrapper | Access privileges | Type | Version | FDW Options | Description
+ -----------+----------------+----------------------+-------------------+------+---------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+-------------
+ loopback1 | pgsql_fdw_user | pgsql_fdw | | | | (authtype 'value', service 'value', connect_timeout 'value', dbname 'value', host 'value', hostaddr 'value', port 'value', tty 'value', options 'value', application_name 'value', keepalives 'value', keepalives_idle 'value', keepalives_interval 'value', sslcompression 'value', sslmode 'value', sslcert 'value', sslkey 'value', sslrootcert 'value', sslcrl 'value') |
+ loopback2 | pgsql_fdw_user | pgsql_fdw | | | | (dbname 'contrib_regression') |
+ (2 rows)
+
+ \deu+
+ List of user mappings
+ Server | User name | FDW Options
+ -----------+----------------+-------------
+ loopback1 | public |
+ loopback2 | pgsql_fdw_user |
+ (2 rows)
+
+ \det+
+ List of foreign tables
+ Schema | Table | Server | FDW Options | Description
+ --------+-------+-----------+--------------------------------+-------------
+ public | ft1 | loopback2 | (nspname 'S 1', relname 'T 1') |
+ public | ft2 | loopback2 | (nspname 'S 1', relname 'T 1') |
+ (2 rows)
+
+ -- ===================================================================
+ -- simple queries
+ -- ===================================================================
+ -- single table, with/without alias
+ EXPLAIN (COSTS false) SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
+ QUERY PLAN
+ ---------------------------------------------------------------------------------
+ Limit
+ -> Sort
+ Sort Key: c3, c1
+ -> Foreign Scan on ft1
+ Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (5 rows)
+
+ SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ -----+----+-------+------------------------------+--------------------------+----+------------
+ 101 | 1 | 00101 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970 | 1 | 1
+ 102 | 2 | 00102 | Sat Jan 03 00:00:00 1970 PST | Sat Jan 03 00:00:00 1970 | 2 | 2
+ 103 | 3 | 00103 | Sun Jan 04 00:00:00 1970 PST | Sun Jan 04 00:00:00 1970 | 3 | 3
+ 104 | 4 | 00104 | Mon Jan 05 00:00:00 1970 PST | Mon Jan 05 00:00:00 1970 | 4 | 4
+ 105 | 5 | 00105 | Tue Jan 06 00:00:00 1970 PST | Tue Jan 06 00:00:00 1970 | 5 | 5
+ 106 | 6 | 00106 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6
+ 107 | 7 | 00107 | Thu Jan 08 00:00:00 1970 PST | Thu Jan 08 00:00:00 1970 | 7 | 7
+ 108 | 8 | 00108 | Fri Jan 09 00:00:00 1970 PST | Fri Jan 09 00:00:00 1970 | 8 | 8
+ 109 | 9 | 00109 | Sat Jan 10 00:00:00 1970 PST | Sat Jan 10 00:00:00 1970 | 9 | 9
+ 110 | 0 | 00110 | Sun Jan 11 00:00:00 1970 PST | Sun Jan 11 00:00:00 1970 | 0 | 0
+ (10 rows)
+
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+ QUERY PLAN
+ ---------------------------------------------------------------------------------
+ Limit
+ -> Sort
+ Sort Key: c3, c1
+ -> Foreign Scan on ft1 t1
+ Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (5 rows)
+
+ SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ -----+----+-------+------------------------------+--------------------------+----+------------
+ 101 | 1 | 00101 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970 | 1 | 1
+ 102 | 2 | 00102 | Sat Jan 03 00:00:00 1970 PST | Sat Jan 03 00:00:00 1970 | 2 | 2
+ 103 | 3 | 00103 | Sun Jan 04 00:00:00 1970 PST | Sun Jan 04 00:00:00 1970 | 3 | 3
+ 104 | 4 | 00104 | Mon Jan 05 00:00:00 1970 PST | Mon Jan 05 00:00:00 1970 | 4 | 4
+ 105 | 5 | 00105 | Tue Jan 06 00:00:00 1970 PST | Tue Jan 06 00:00:00 1970 | 5 | 5
+ 106 | 6 | 00106 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6
+ 107 | 7 | 00107 | Thu Jan 08 00:00:00 1970 PST | Thu Jan 08 00:00:00 1970 | 7 | 7
+ 108 | 8 | 00108 | Fri Jan 09 00:00:00 1970 PST | Fri Jan 09 00:00:00 1970 | 8 | 8
+ 109 | 9 | 00109 | Sat Jan 10 00:00:00 1970 PST | Sat Jan 10 00:00:00 1970 | 9 | 9
+ 110 | 0 | 00110 | Sun Jan 11 00:00:00 1970 PST | Sun Jan 11 00:00:00 1970 | 0 | 0
+ (10 rows)
+
+ -- with WHERE clause
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 >= '1';
+ QUERY PLAN
+ -----------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: ((c7 >= '1'::bpchar) AND (c1 = 101) AND ((c6)::text = '1'::text))
+ Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 >= '1';
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ -----+----+-------+------------------------------+--------------------------+----+------------
+ 101 | 1 | 00101 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970 | 1 | 1
+ (1 row)
+
+ -- aggregate
+ SELECT COUNT(*) FROM ft1 t1;
+ count
+ -------
+ 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
+ ----+----+-------+------------------------------+--------------------------+----+------------
+ 1 | 1 | 00001 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970 | 1 | 1
+ 2 | 2 | 00002 | Sat Jan 03 00:00:00 1970 PST | Sat Jan 03 00:00:00 1970 | 2 | 2
+ 3 | 3 | 00003 | Sun Jan 04 00:00:00 1970 PST | Sun Jan 04 00:00:00 1970 | 3 | 3
+ 4 | 4 | 00004 | Mon Jan 05 00:00:00 1970 PST | Mon Jan 05 00:00:00 1970 | 4 | 4
+ 5 | 5 | 00005 | Tue Jan 06 00:00:00 1970 PST | Tue Jan 06 00:00:00 1970 | 5 | 5
+ 6 | 6 | 00006 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6
+ 7 | 7 | 00007 | Thu Jan 08 00:00:00 1970 PST | Thu Jan 08 00:00:00 1970 | 7 | 7
+ 8 | 8 | 00008 | Fri Jan 09 00:00:00 1970 PST | Fri Jan 09 00:00:00 1970 | 8 | 8
+ 9 | 9 | 00009 | Sat Jan 10 00:00:00 1970 PST | Sat Jan 10 00:00:00 1970 | 9 | 9
+ 10 | 0 | 00010 | Sun Jan 11 00:00:00 1970 PST | Sun Jan 11 00:00:00 1970 | 0 | 0
+ (10 rows)
+
+ -- subquery+MAX
+ SELECT * FROM ft1 t1 WHERE t1.c3 = (SELECT MAX(c3) FROM ft2 t2) ORDER BY c1;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ ------+----+-------+------------------------------+--------------------------+----+------------
+ 1000 | 0 | 01000 | Thu Jan 01 00:00:00 1970 PST | Thu Jan 01 00:00:00 1970 | 0 | 0
+ (1 row)
+
+ -- used in CTE
+ WITH t1 AS (SELECT * FROM ft1 WHERE c1 <= 10) SELECT t2.c1, t2.c2, t2.c3, t2.c4 FROM t1, ft2 t2 WHERE t1.c1 = t2.c1 ORDER BY t1.c1;
+ c1 | c2 | c3 | c4
+ ----+----+-------+------------------------------
+ 1 | 1 | 00001 | Fri Jan 02 00:00:00 1970 PST
+ 2 | 2 | 00002 | Sat Jan 03 00:00:00 1970 PST
+ 3 | 3 | 00003 | Sun Jan 04 00:00:00 1970 PST
+ 4 | 4 | 00004 | Mon Jan 05 00:00:00 1970 PST
+ 5 | 5 | 00005 | Tue Jan 06 00:00:00 1970 PST
+ 6 | 6 | 00006 | Wed Jan 07 00:00:00 1970 PST
+ 7 | 7 | 00007 | Thu Jan 08 00:00:00 1970 PST
+ 8 | 8 | 00008 | Fri Jan 09 00:00:00 1970 PST
+ 9 | 9 | 00009 | Sat Jan 10 00:00:00 1970 PST
+ 10 | 0 | 00010 | Sun Jan 11 00:00:00 1970 PST
+ (10 rows)
+
+ -- fixed values
+ SELECT 'fixed', NULL FROM ft1 t1 WHERE c1 = 1;
+ ?column? | ?column?
+ ----------+----------
+ fixed |
+ (1 row)
+
+ -- user-defined operator/function
+ CREATE FUNCTION pgsql_fdw_abs(int) RETURNS int AS $$
+ BEGIN
+ RETURN abs($1);
+ END
+ $$ LANGUAGE plpgsql IMMUTABLE;
+ CREATE OPERATOR === (
+ LEFTARG = int,
+ RIGHTARG = int,
+ PROCEDURE = int4eq,
+ COMMUTATOR = ===,
+ NEGATOR = !==
+ );
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = pgsql_fdw_abs(t1.c2);
+ QUERY PLAN
+ ---------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = pgsql_fdw_abs(c2))
+ Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 === t1.c2;
+ QUERY PLAN
+ ---------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 === c2)
+ Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = abs(t1.c2);
+ QUERY PLAN
+ ---------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = abs(c2))
+ Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = t1.c2;
+ QUERY PLAN
+ ---------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = c2)
+ Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ -- ===================================================================
+ -- parameterized queries
+ -- ===================================================================
+ -- simple join
+ PREPARE st1(int, int) AS SELECT t1.c3, t2.c3 FROM ft1 t1, ft2 t2 WHERE t1.c1 = $1 AND t2.c1 = $2;
+ EXPLAIN (COSTS false) EXECUTE st1(1, 2);
+ QUERY PLAN
+ -------------------------------------------------------------------------------------------
+ Nested Loop
+ -> Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: SELECT "C 1", NULL, c3, NULL, NULL, NULL, NULL FROM "S 1"."T 1"
+ -> Materialize
+ -> Foreign Scan on ft2 t2
+ Filter: (c1 = 2)
+ Remote SQL: SELECT "C 1", NULL, c3, NULL, NULL, NULL, NULL FROM "S 1"."T 1"
+ (8 rows)
+
+ EXECUTE st1(1, 1);
+ c3 | c3
+ -------+-------
+ 00001 | 00001
+ (1 row)
+
+ EXECUTE st1(101, 101);
+ c3 | c3
+ -------+-------
+ 00101 | 00101
+ (1 row)
+
+ -- subquery using stable function (can't be pushed down)
+ PREPARE st2(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c4) = 6) ORDER BY c1;
+ EXPLAIN (COSTS false) EXECUTE st2(10, 20);
+ QUERY PLAN
+ ------------------------------------------------------------------------------------------------
+ Sort
+ Sort Key: t1.c1
+ -> Nested Loop Semi Join
+ Join Filter: (t1.c3 = t2.c3)
+ -> Foreign Scan on ft1 t1
+ Filter: (c1 < 20)
+ Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ -> Materialize
+ -> Foreign Scan on ft2 t2
+ Filter: ((c1 > 10) AND (date_part('dow'::text, c4) = 6::double precision))
+ Remote SQL: SELECT "C 1", NULL, c3, c4, NULL, NULL, NULL FROM "S 1"."T 1"
+ (11 rows)
+
+ EXECUTE st2(10, 20);
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ ----+----+-------+------------------------------+--------------------------+----+------------
+ 16 | 6 | 00016 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970 | 6 | 6
+ (1 row)
+
+ EXECUTE st1(101, 101);
+ c3 | c3
+ -------+-------
+ 00101 | 00101
+ (1 row)
+
+ -- subquery using immutable function (can be pushed down)
+ PREPARE st3(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c5) = 6) ORDER BY c1;
+ EXPLAIN (COSTS false) EXECUTE st3(10, 20);
+ QUERY PLAN
+ ------------------------------------------------------------------------------------------------
+ Sort
+ Sort Key: t1.c1
+ -> Nested Loop Semi Join
+ Join Filter: (t1.c3 = t2.c3)
+ -> Foreign Scan on ft1 t1
+ Filter: (c1 < 20)
+ Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ -> Materialize
+ -> Foreign Scan on ft2 t2
+ Filter: ((c1 > 10) AND (date_part('dow'::text, c5) = 6::double precision))
+ Remote SQL: SELECT "C 1", NULL, c3, NULL, c5, NULL, NULL FROM "S 1"."T 1"
+ (11 rows)
+
+ EXECUTE st3(10, 20);
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ ----+----+-------+------------------------------+--------------------------+----+------------
+ 16 | 6 | 00016 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970 | 6 | 6
+ (1 row)
+
+ EXECUTE st3(20, 30);
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ ----+----+-------+------------------------------+--------------------------+----+------------
+ 23 | 3 | 00023 | Sat Jan 24 00:00:00 1970 PST | Sat Jan 24 00:00:00 1970 | 3 | 3
+ (1 row)
+
+ -- custom plan should be chosen
+ PREPARE st4(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 = $1;
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ ---------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ ---------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ ---------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ ---------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ ---------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = 1)
+ Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ QUERY PLAN
+ ---------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Filter: (c1 = $1)
+ Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ (3 rows)
+
+ -- cleanup
+ DEALLOCATE st1;
+ DEALLOCATE st2;
+ DEALLOCATE st3;
+ DEALLOCATE st4;
+ -- ===================================================================
+ -- connection management
+ -- ===================================================================
+ SELECT srvname, usename FROM pgsql_fdw_connections;
+ srvname | usename
+ -----------+----------------
+ loopback2 | pgsql_fdw_user
+ (1 row)
+
+ SELECT pgsql_fdw_disconnect(srvid, usesysid) FROM pgsql_fdw_get_connections();
+ pgsql_fdw_disconnect
+ ----------------------
+ OK
+ (1 row)
+
+ SELECT srvname, usename FROM pgsql_fdw_connections;
+ srvname | usename
+ ---------+---------
+ (0 rows)
+
+ -- ===================================================================
+ -- conversion error
+ -- ===================================================================
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c5 TYPE int;
+ SELECT * FROM ft1 WHERE c1 = 1; -- ERROR
+ ERROR: invalid input syntax for integer: "1970-01-02 00:00:00"
+ CONTEXT: column c5 of foreign table ft1
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c5 TYPE timestamp;
+ -- ===================================================================
+ -- subtransaction
+ -- ===================================================================
+ BEGIN;
+ DECLARE c CURSOR FOR SELECT * FROM ft1 ORDER BY c1;
+ FETCH c;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ ----+----+-------+------------------------------+--------------------------+----+------------
+ 1 | 1 | 00001 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970 | 1 | 1
+ (1 row)
+
+ SAVEPOINT s;
+ ERROR OUT; -- ERROR
+ ERROR: syntax error at or near "ERROR"
+ LINE 1: ERROR OUT;
+ ^
+ ROLLBACK TO s;
+ SELECT srvname FROM pgsql_fdw_connections;
+ srvname
+ -----------
+ loopback2
+ (1 row)
+
+ FETCH c;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7
+ ----+----+-------+------------------------------+--------------------------+----+------------
+ 2 | 2 | 00002 | Sat Jan 03 00:00:00 1970 PST | Sat Jan 03 00:00:00 1970 | 2 | 2
+ (1 row)
+
+ COMMIT;
+ SELECT srvname FROM pgsql_fdw_connections;
+ srvname
+ -----------
+ loopback2
+ (1 row)
+
+ ERROR OUT; -- ERROR
+ ERROR: syntax error at or near "ERROR"
+ LINE 1: ERROR OUT;
+ ^
+ SELECT srvname FROM pgsql_fdw_connections;
+ srvname
+ ---------
+ (0 rows)
+
+ -- ===================================================================
+ -- cleanup
+ -- ===================================================================
+ DROP OPERATOR === (int, int) CASCADE;
+ DROP OPERATOR !== (int, int) CASCADE;
+ DROP FUNCTION pgsql_fdw_abs(int);
+ DROP SCHEMA "S 1" CASCADE;
+ NOTICE: drop cascades to 2 other objects
+ DETAIL: drop cascades to table "S 1"."T 1"
+ drop cascades to table "S 1"."T 2"
+ DROP EXTENSION pgsql_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 server loopback2
+ drop cascades to user mapping for pgsql_fdw_user
+ drop cascades to foreign table ft1
+ drop cascades to foreign table ft2
+ \c
+ DROP ROLE pgsql_fdw_user;
diff --git a/contrib/pgsql_fdw/option.c b/contrib/pgsql_fdw/option.c
new file mode 100644
index ...0192fd2
*** a/contrib/pgsql_fdw/option.c
--- b/contrib/pgsql_fdw/option.c
***************
*** 0 ****
--- 1,222 ----
+ /*-------------------------------------------------------------------------
+ *
+ * option.c
+ * FDW option handling
+ *
+ * Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/option.c
+ *
+ *-------------------------------------------------------------------------
+ */
+ #include "postgres.h"
+
+ #include "access/reloptions.h"
+ #include "catalog/pg_foreign_data_wrapper.h"
+ #include "catalog/pg_foreign_server.h"
+ #include "catalog/pg_foreign_table.h"
+ #include "catalog/pg_user_mapping.h"
+ #include "commands/defrem.h"
+ #include "fmgr.h"
+ #include "foreign/foreign.h"
+ #include "lib/stringinfo.h"
+ #include "miscadmin.h"
+
+ #include "pgsql_fdw.h"
+
+ /*
+ * SQL functions
+ */
+ extern Datum pgsql_fdw_validator(PG_FUNCTION_ARGS);
+ PG_FUNCTION_INFO_V1(pgsql_fdw_validator);
+
+ /*
+ * Describes the valid options for objects that use this wrapper.
+ */
+ typedef struct PgsqlFdwOption
+ {
+ const char *optname;
+ Oid optcontext; /* Oid of catalog in which options may appear */
+ bool is_libpq_opt; /* true if it's used in libpq */
+ } PgsqlFdwOption;
+
+ /*
+ * Valid options for pgsql_fdw.
+ */
+ static PgsqlFdwOption valid_options[] = {
+
+ /*
+ * Options for libpq connection.
+ * Note: This list should be updated along with PQconninfoOptions in
+ * interfaces/libpq/fe-connect.c, so the order is kept as is.
+ *
+ * Some useless libpq connection options are not accepted by pgsql_fdw:
+ * client_encoding: set to local database encoding automatically
+ * fallback_application_name: fixed to "pgsql_fdw"
+ * replication: pgsql_fdw never be replication client
+ */
+ {"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},
+ #ifdef NOT_USED
+ {"client_encoding", ForeignServerRelationId, true},
+ #endif
+ {"tty", ForeignServerRelationId, true},
+ {"options", ForeignServerRelationId, true},
+ {"application_name", ForeignServerRelationId, true},
+ #ifdef NOT_USED
+ {"fallback_application_name", ForeignServerRelationId, true},
+ #endif
+ {"keepalives", ForeignServerRelationId, true},
+ {"keepalives_idle", ForeignServerRelationId, true},
+ {"keepalives_interval", ForeignServerRelationId, true},
+ {"keepalives_count", ForeignServerRelationId, true},
+ {"requiressl", ForeignServerRelationId, true},
+ {"sslcompression", ForeignServerRelationId, true},
+ {"sslmode", ForeignServerRelationId, true},
+ {"sslcert", ForeignServerRelationId, true},
+ {"sslkey", ForeignServerRelationId, true},
+ {"sslrootcert", ForeignServerRelationId, true},
+ {"sslcrl", ForeignServerRelationId, true},
+ {"requirepeer", ForeignServerRelationId, true},
+ {"krbsrvname", ForeignServerRelationId, true},
+ {"gsslib", ForeignServerRelationId, true},
+ #ifdef NOT_USED
+ {"replication", ForeignServerRelationId, true},
+ #endif
+
+ /*
+ * Options for translation of object names.
+ */
+ {"nspname", ForeignTableRelationId, false},
+ {"relname", ForeignTableRelationId, false},
+ {"colname", AttributeRelationId, false},
+
+ /* Terminating entry --- MUST BE LAST */
+ {NULL, InvalidOid, false}
+ };
+
+ /*
+ * Helper functions
+ */
+ static bool is_valid_option(const char *optname, Oid context);
+
+ /*
+ * Validate the generic options given to a FOREIGN DATA WRAPPER, SERVER,
+ * USER MAPPING or FOREIGN TABLE that uses pgsql_fdw.
+ *
+ * Raise an ERROR if the option or its value is considered invalid.
+ */
+ Datum
+ pgsql_fdw_validator(PG_FUNCTION_ARGS)
+ {
+ List *options_list = untransformRelOptions(PG_GETARG_DATUM(0));
+ Oid catalog = PG_GETARG_OID(1);
+ ListCell *cell;
+
+ /*
+ * Check that only options supported by pgsql_fdw, and allowed for the
+ * current object type, are given.
+ */
+ foreach(cell, options_list)
+ {
+ DefElem *def = (DefElem *) lfirst(cell);
+
+ if (!is_valid_option(def->defname, catalog))
+ {
+ PgsqlFdwOption *opt;
+ StringInfoData buf;
+
+ /*
+ * Unknown option specified, complain about it. Provide a hint
+ * with list of valid options for the object.
+ */
+ initStringInfo(&buf);
+ for (opt = valid_options; opt->optname; opt++)
+ {
+ if (catalog == opt->optcontext)
+ appendStringInfo(&buf, "%s%s", (buf.len > 0) ? ", " : "",
+ opt->optname);
+ }
+
+ ereport(ERROR,
+ (errcode(ERRCODE_FDW_INVALID_OPTION_NAME),
+ errmsg("invalid option \"%s\"", def->defname),
+ errhint("Valid options in this context are: %s",
+ buf.data)));
+ }
+ }
+
+ /*
+ * We don't care option-specific limitation here; they will be validated at
+ * the execution time.
+ */
+
+ PG_RETURN_VOID();
+ }
+
+ /*
+ * Check whether the given option is one of the valid pgsql_fdw options.
+ * context is the Oid of the catalog holding the object the option is for.
+ */
+ static bool
+ is_valid_option(const char *optname, Oid context)
+ {
+ PgsqlFdwOption *opt;
+
+ for (opt = valid_options; opt->optname; opt++)
+ {
+ if (context == opt->optcontext && strcmp(opt->optname, optname) == 0)
+ return true;
+ }
+ return false;
+ }
+
+ /*
+ * Check whether the given option is one of the valid libpq options.
+ * context is the Oid of the catalog holding the object the option is for.
+ */
+ static bool
+ is_libpq_option(const char *optname)
+ {
+ PgsqlFdwOption *opt;
+
+ for (opt = valid_options; opt->optname; opt++)
+ {
+ if (strcmp(opt->optname, optname) == 0 && opt->is_libpq_opt)
+ return true;
+ }
+ return false;
+ }
+
+ /*
+ * Generate key-value arrays which includes only libpq options from the list
+ * which contains any kind of options.
+ */
+ int
+ ExtractConnectionOptions(List *defelems, const char **keywords, const char **values)
+ {
+ ListCell *lc;
+ int i;
+
+ i = 0;
+ foreach(lc, defelems)
+ {
+ DefElem *d = (DefElem *) lfirst(lc);
+ if (is_libpq_option(d->defname))
+ {
+ keywords[i] = d->defname;
+ values[i] = defGetString(d);
+ i++;
+ }
+ }
+ return i;
+ }
+
diff --git a/contrib/pgsql_fdw/pgsql_fdw--1.0.sql b/contrib/pgsql_fdw/pgsql_fdw--1.0.sql
new file mode 100644
index ...b0ea2b2
*** a/contrib/pgsql_fdw/pgsql_fdw--1.0.sql
--- b/contrib/pgsql_fdw/pgsql_fdw--1.0.sql
***************
*** 0 ****
--- 1,39 ----
+ /* contrib/pgsql_fdw/pgsql_fdw--1.0.sql */
+
+ -- complain if script is sourced in psql, rather than via CREATE EXTENSION
+ \echo Use "CREATE EXTENSION pgsql_fdw" to load this file. \quit
+
+ CREATE FUNCTION pgsql_fdw_handler()
+ RETURNS fdw_handler
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C STRICT;
+
+ CREATE FUNCTION pgsql_fdw_validator(text[], oid)
+ RETURNS void
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C STRICT;
+
+ CREATE FOREIGN DATA WRAPPER pgsql_fdw
+ HANDLER pgsql_fdw_handler
+ VALIDATOR pgsql_fdw_validator;
+
+ /* connection management functions and view */
+ CREATE FUNCTION pgsql_fdw_get_connections(out srvid oid, out usesysid oid)
+ RETURNS SETOF record
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C STRICT;
+
+ CREATE FUNCTION pgsql_fdw_disconnect(oid, oid)
+ RETURNS text
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C STRICT;
+
+ CREATE VIEW pgsql_fdw_connections AS
+ SELECT c.srvid srvid,
+ s.srvname srvname,
+ c.usesysid usesysid,
+ pg_get_userbyid(c.usesysid) usename
+ FROM pgsql_fdw_get_connections() c
+ JOIN pg_catalog.pg_foreign_server s ON (s.oid = c.srvid);
+ GRANT SELECT ON pgsql_fdw_connections TO public;
+
diff --git a/contrib/pgsql_fdw/pgsql_fdw.c b/contrib/pgsql_fdw/pgsql_fdw.c
new file mode 100644
index ...1b7db4d
*** a/contrib/pgsql_fdw/pgsql_fdw.c
--- b/contrib/pgsql_fdw/pgsql_fdw.c
***************
*** 0 ****
--- 1,934 ----
+ /*-------------------------------------------------------------------------
+ *
+ * pgsql_fdw.c
+ * foreign-data wrapper for remote PostgreSQL servers.
+ *
+ * Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/pgsql_fdw.c
+ *
+ *-------------------------------------------------------------------------
+ */
+ #include "postgres.h"
+ #include "fmgr.h"
+
+ #include "catalog/pg_foreign_server.h"
+ #include "catalog/pg_foreign_table.h"
+ #include "commands/defrem.h"
+ #include "commands/explain.h"
+ #include "foreign/fdwapi.h"
+ #include "funcapi.h"
+ #include "miscadmin.h"
+ #include "optimizer/cost.h"
+ #include "optimizer/pathnode.h"
+ #include "optimizer/planmain.h"
+ #include "optimizer/restrictinfo.h"
+ #include "utils/builtins.h"
+ #include "utils/lsyscache.h"
+ #include "utils/memutils.h"
+ #include "utils/rel.h"
+
+ #include "pgsql_fdw.h"
+ #include "connection.h"
+
+ PG_MODULE_MAGIC;
+
+ /*
+ * Cost to establish a connection.
+ * XXX: should be configurable per server?
+ */
+ #define CONNECTION_COSTS 100.0
+
+ /*
+ * Cost to transfer 1 byte from remote server.
+ * XXX: should be configurable per server?
+ */
+ #define TRANSFER_COSTS_PER_BYTE 0.001
+
+ /*
+ * The initial size of the buffer which is used to hold string representation
+ * of value of fetched column in libpq row processor.
+ */
+ #define INITIAL_COLBUF_SIZE 1024
+
+ /*
+ * FDW-specific information for RelOptInfo.fdw_private. This is used to pass
+ * information from pgsqlGetForeignRelSize to pgsqlGetForeignPaths.
+ */
+ typedef struct PgsqlFdwPlanState {
+ /*
+ * These are generated in GetForeignRelSize, and also used in subsequent
+ * GetForeignPaths.
+ */
+ StringInfoData sql;
+ Cost startup_cost;
+ Cost total_cost;
+
+ /* Cached catalog information. */
+ ForeignTable *table;
+ ForeignServer *server;
+ } PgsqlFdwPlanState;
+
+ /*
+ * Index of FDW-private information stored in fdw_private list.
+ *
+ * We store various information in ForeignScan.fdw_private to pass them beyond
+ * the boundary between planner and executor. Finally FdwPlan holds items
+ * below:
+ *
+ * 1) plain SELECT statement
+ *
+ * These items are indexed with the enum FdwPrivateIndex, so an item
+ * can be accessed directly via list_nth(). For example of SELECT statement:
+ * sql = list_nth(fdw_private, FdwPrivateSelectSql)
+ */
+ enum FdwPrivateIndex {
+ /* SQL statements */
+ FdwPrivateSelectSql,
+
+ /* # of elements stored in the list fdw_private */
+ FdwPrivateNum,
+ };
+
+ /*
+ * Describe the attribute where data conversion fails.
+ */
+ typedef struct ErrorPos {
+ Oid relid; /* oid of the foreign table */
+ AttrNumber cur_attno; /* attribute number under process */
+ } ErrorPos;
+
+ /*
+ * Describes an execution state of a foreign scan against a foreign table
+ * using pgsql_fdw.
+ */
+ typedef struct PgsqlFdwExecutionState
+ {
+ List *fdw_private; /* FDW-private information */
+
+ /* for remote query execution */
+ PGconn *conn; /* connection for the scan */
+ Oid *param_types; /* type array of external parameter */
+ const char **param_values; /* value array of external parameter */
+
+ /* for tuple generation. */
+ char *colbuf; /* column value buffer for row processor */
+ int colbuflen; /* column value buffer size for row processor */
+ AttrNumber attnum; /* # of non-dropped attribute */
+ Datum *values; /* column value buffer */
+ bool *nulls; /* column null indicator buffer */
+ AttInMetadata *attinmeta; /* attribute metadata */
+
+ /* for storing result tuples */
+ MemoryContext scan_cxt; /* context for per-scan lifespan data */
+ MemoryContext temp_cxt; /* context for per-tuple temporary data */
+ Tuplestorestate *tuples; /* result of the scan */
+
+ /* for error handling. */
+ ErrorPos errpos;
+ } PgsqlFdwExecutionState;
+
+ /*
+ * SQL functions
+ */
+ extern Datum pgsql_fdw_handler(PG_FUNCTION_ARGS);
+ PG_FUNCTION_INFO_V1(pgsql_fdw_handler);
+
+ /*
+ * FDW callback routines
+ */
+ static void pgsqlGetForeignRelSize(PlannerInfo *root,
+ RelOptInfo *baserel,
+ Oid foreigntableid);
+ static void pgsqlGetForeignPaths(PlannerInfo *root,
+ RelOptInfo *baserel,
+ Oid foreigntableid);
+ static ForeignScan *pgsqlGetForeignPlan(PlannerInfo *root,
+ RelOptInfo *baserel,
+ Oid foreigntableid,
+ ForeignPath *best_path,
+ List *tlist,
+ List *scan_clauses);
+ static void pgsqlExplainForeignScan(ForeignScanState *node, ExplainState *es);
+ static void pgsqlBeginForeignScan(ForeignScanState *node, int eflags);
+ static TupleTableSlot *pgsqlIterateForeignScan(ForeignScanState *node);
+ static void pgsqlReScanForeignScan(ForeignScanState *node);
+ static void pgsqlEndForeignScan(ForeignScanState *node);
+
+ /*
+ * Helper functions
+ */
+ static void get_remote_estimate(const char *sql,
+ PGconn *conn,
+ double *rows,
+ int *width,
+ Cost *startup_cost,
+ Cost *total_cost);
+ static void adjust_costs(double rows, int width,
+ Cost *startup_cost, Cost *total_cost);
+ static void execute_query(ForeignScanState *node);
+ static int query_row_processor(PGresult *res, const PGdataValue *columns,
+ const char **errmsgp, void *param);
+ static void pgsql_fdw_error_callback(void *arg);
+
+ /* Exported functions, but not written in pgsql_fdw.h. */
+ void _PG_init(void);
+ void _PG_fini(void);
+
+ /*
+ * Module-specific initialization.
+ */
+ void
+ _PG_init(void)
+ {
+ }
+
+ /*
+ * Module-specific clean up.
+ */
+ void
+ _PG_fini(void)
+ {
+ }
+
+ /*
+ * The content of FdwRoutine of pgsql_fdw is never changed, so we use one
+ * static object for all scan.
+ */
+ static FdwRoutine fdwroutine = {
+
+ /* Node type */
+ T_FdwRoutine,
+
+ /* Required handler functions. */
+ pgsqlGetForeignRelSize,
+ pgsqlGetForeignPaths,
+ pgsqlGetForeignPlan,
+ pgsqlExplainForeignScan,
+ pgsqlBeginForeignScan,
+ pgsqlIterateForeignScan,
+ pgsqlReScanForeignScan,
+ pgsqlEndForeignScan,
+
+ /* Optional handler functions. */
+ };
+
+ /*
+ * Foreign-data wrapper handler function: return a struct with pointers
+ * to my callback routines.
+ */
+ Datum
+ pgsql_fdw_handler(PG_FUNCTION_ARGS)
+ {
+ PG_RETURN_POINTER(&fdwroutine);
+ }
+
+ /*
+ * pgsqlGetForeignRelSize
+ * Estimate # of rows and width of the result of the scan
+ *
+ * Here we estimate number of rows returned by the scan in two steps. In the
+ * first step, we execute remote EXPLAIN command to obtain the number of rows
+ * returned from remote side. In the second step, we calculate the selectivity
+ * of the filtering done on local side, and modify first estimate.
+ *
+ * We have to get some catalog objects and generate remote query string here,
+ * so we store such expensive information in FDW private area of RelOptInfo and
+ * pass them to subsequent functions for reuse.
+ */
+ static void
+ pgsqlGetForeignRelSize(PlannerInfo *root,
+ RelOptInfo *baserel,
+ Oid foreigntableid)
+ {
+ PgsqlFdwPlanState *fpstate;
+ StringInfo sql;
+ ForeignTable *table;
+ ForeignServer *server;
+ UserMapping *user;
+ PGconn *conn;
+ double rows;
+ int width;
+ Cost startup_cost;
+ Cost total_cost;
+ Selectivity sel;
+
+ /*
+ * We use PgsqlFdwPlanState to pass various information to subsequent
+ * functions.
+ */
+ fpstate = palloc0(sizeof(PgsqlFdwPlanState));
+ initStringInfo(&fpstate->sql);
+ sql = &fpstate->sql;
+
+ /* Retrieve catalog objects which are necessary to estimate rows. */
+ table = GetForeignTable(foreigntableid);
+ server = GetForeignServer(table->serverid);
+ user = GetUserMapping(GetOuterUserId(), server->serverid);
+
+ /*
+ * Create plain SELECT statement with no WHERE clause for this scan in
+ * order to obtain meaningful rows estimation by executing EXPLAIN on
+ * remote server.
+ */
+ deparseSimpleSql(sql, foreigntableid, root, baserel);
+ conn = GetConnection(server, user, false);
+ get_remote_estimate(sql->data, conn, &rows, &width,
+ &startup_cost, &total_cost);
+ ReleaseConnection(conn);
+
+ /*
+ * Estimate selectivity of local filtering by calling
+ * clauselist_selectivity() against baserestrictinfo, and modify rows
+ * estimate with it.
+ */
+ sel = clauselist_selectivity(root, baserel->baserestrictinfo,
+ baserel->relid, JOIN_INNER, NULL);
+ baserel->rows = rows * sel;
+ baserel->width = width;
+
+ /*
+ * Pack obtained information into a object and store it in FDW-private area
+ * of RelOptInfo to pass them to subsequent functions.
+ */
+ fpstate->startup_cost = startup_cost;
+ fpstate->total_cost = total_cost;
+ fpstate->table = table;
+ fpstate->server = server;
+ baserel->fdw_private = (void *) fpstate;
+ }
+
+ /*
+ * pgsqlGetForeignPaths
+ * Create possible scan paths for a scan on the foreign table
+ */
+ static void
+ pgsqlGetForeignPaths(PlannerInfo *root,
+ RelOptInfo *baserel,
+ Oid foreigntableid)
+ {
+ PgsqlFdwPlanState *fpstate = (PgsqlFdwPlanState *) baserel->fdw_private;
+ ForeignPath *path;
+ Cost startup_cost;
+ Cost total_cost;
+ List *fdw_private;
+
+ /*
+ * We have cost values which are estimated on remote side, so use them to
+ * estimate better costs which respect various stuffs to complete the scan,
+ * such as sending query, transferring result, and local filtering.
+ *
+ * XXX We assume that remote cost factors are same as local, but it might
+ * be worth to make configurable.
+ */
+ startup_cost = fpstate->startup_cost;
+ total_cost = fpstate->total_cost;
+ adjust_costs(baserel->rows, baserel->width, &startup_cost, &total_cost);
+
+ /* Construct list of SQL statements and bind it with the path. */
+ fdw_private = lappend(NIL, makeString(fpstate->sql.data));
+
+ /*
+ * Create simplest ForeignScan path node and add it to baserel. This path
+ * corresponds to SeqScan path of regular tables.
+ */
+ path = create_foreignscan_path(root, baserel,
+ baserel->rows,
+ startup_cost,
+ total_cost,
+ NIL, /* no pathkeys */
+ NULL, /* no outer rel either */
+ NIL, /* no param clause */
+ fdw_private);
+ add_path(baserel, (Path *) path);
+
+ /*
+ * XXX We can consider sorted path or parameterized path here if we know
+ * that foreign table is indexed on remote end. For this purpose, we
+ * might have to support FOREIGN INDEX to represent possible sets of sort
+ * keys and/or filtering.
+ */
+ }
+
+ /*
+ * pgsqlGetForeignPlan
+ * Create ForeignScan plan node which implements selected best path
+ */
+ static ForeignScan *
+ pgsqlGetForeignPlan(PlannerInfo *root,
+ RelOptInfo *baserel,
+ Oid foreigntableid,
+ ForeignPath *best_path,
+ List *tlist,
+ List *scan_clauses)
+ {
+ PgsqlFdwPlanState *fpstate = (PgsqlFdwPlanState *) baserel->fdw_private;
+ Index scan_relid = baserel->relid;
+ List *fdw_private = NIL;
+
+ /*
+ * We have no native ability to evaluate restriction clauses, so we just
+ * put all the scan_clauses into the plan node's qual list for the
+ * executor to check. So all we have to do here is strip RestrictInfo
+ * nodes from the clauses and ignore pseudoconstants (which will be
+ * handled elsewhere).
+ */
+ scan_clauses = extract_actual_clauses(scan_clauses, false);
+
+ /*
+ * Make a list contains SELECT statement to it to executor with plan node
+ * for later use.
+ */
+ fdw_private = lappend(fdw_private, makeString(fpstate->sql.data));
+
+ /* Create the ForeignScan node with fdw_private of selected path. */
+ return make_foreignscan(tlist,
+ scan_clauses,
+ scan_relid,
+ NIL,
+ fdw_private);
+ }
+
+ /*
+ * pgsqlExplainForeignScan
+ * Produce extra output for EXPLAIN
+ */
+ static void
+ pgsqlExplainForeignScan(ForeignScanState *node, ExplainState *es)
+ {
+ List *fdw_private;
+ char *sql;
+
+ fdw_private = ((ForeignScan *) node->ss.ps.plan)->fdw_private;
+ sql = strVal(list_nth(fdw_private, FdwPrivateSelectSql));
+ ExplainPropertyText("Remote SQL", sql, es);
+ }
+
+ /*
+ * pgsqlBeginForeignScan
+ * Initiate access to a foreign PostgreSQL table.
+ */
+ static void
+ pgsqlBeginForeignScan(ForeignScanState *node, int eflags)
+ {
+ PgsqlFdwExecutionState *festate;
+ PGconn *conn;
+ Oid relid;
+ ForeignTable *table;
+ ForeignServer *server;
+ UserMapping *user;
+ TupleTableSlot *slot = node->ss.ss_ScanTupleSlot;
+
+ /*
+ * Do nothing in EXPLAIN (no ANALYZE) case. node->fdw_state stays NULL.
+ */
+ if (eflags & EXEC_FLAG_EXPLAIN_ONLY)
+ return;
+
+ /*
+ * Save state in node->fdw_state.
+ */
+ festate = (PgsqlFdwExecutionState *) palloc(sizeof(PgsqlFdwExecutionState));
+ festate->fdw_private = ((ForeignScan *) node->ss.ps.plan)->fdw_private;
+
+ /*
+ * Create contexts for per-scan tuplestore under per-query context.
+ */
+ festate->scan_cxt = AllocSetContextCreate(node->ss.ps.state->es_query_cxt,
+ "pgsql_fdw per-scan data",
+ ALLOCSET_DEFAULT_MINSIZE,
+ ALLOCSET_DEFAULT_INITSIZE,
+ ALLOCSET_DEFAULT_MAXSIZE);
+ festate->temp_cxt = AllocSetContextCreate(node->ss.ps.state->es_query_cxt,
+ "pgsql_fdw temporary data",
+ ALLOCSET_DEFAULT_MINSIZE,
+ ALLOCSET_DEFAULT_INITSIZE,
+ ALLOCSET_DEFAULT_MAXSIZE);
+
+ /*
+ * Get connection to the foreign server. Connection manager would
+ * establish new connection if necessary.
+ */
+ relid = RelationGetRelid(node->ss.ss_currentRelation);
+ table = GetForeignTable(relid);
+ server = GetForeignServer(table->serverid);
+ user = GetUserMapping(GetOuterUserId(), server->serverid);
+ conn = GetConnection(server, user, true);
+ festate->conn = conn;
+
+ /* Result will be filled in first Iterate call. */
+ festate->tuples = NULL;
+
+ /* Allocate buffers for column values. */
+ {
+ TupleDesc tupdesc = slot->tts_tupleDescriptor;
+ festate->values = palloc(sizeof(Datum) * tupdesc->natts);
+ festate->nulls = palloc(sizeof(bool) * tupdesc->natts);
+ festate->attinmeta = TupleDescGetAttInMetadata(tupdesc);
+ }
+
+ /* Allocate buffers for query parameters. */
+ {
+ ParamListInfo params = node->ss.ps.state->es_param_list_info;
+ int numParams = params ? params->numParams : 0;
+
+ if (numParams > 0)
+ {
+ festate->param_types = palloc0(sizeof(Oid) * numParams);
+ festate->param_values = palloc0(sizeof(char *) * numParams);
+ }
+ else
+ {
+ festate->param_types = NULL;
+ festate->param_values = NULL;
+ }
+ }
+
+ /* Remember which foreign table we are scanning. */
+ festate->errpos.relid = relid;
+
+ /* Store FDW-specific state into ForeignScanState */
+ node->fdw_state = (void *) festate;
+
+ return;
+ }
+
+ /*
+ * pgsqlIterateForeignScan
+ * Retrieve next row from the result set, or clear tuple slot to indicate
+ * EOF.
+ *
+ * Note that using per-query context when retrieving tuples from
+ * tuplestore to ensure that returned tuples can survive until next
+ * iteration because the tuple is released implicitly via ExecClearTuple.
+ * Retrieving a tuple from tuplestore in CurrentMemoryContext (it's a
+ * per-tuple context), ExecClearTuple will free dangling pointer.
+ */
+ static TupleTableSlot *
+ pgsqlIterateForeignScan(ForeignScanState *node)
+ {
+ PgsqlFdwExecutionState *festate;
+ TupleTableSlot *slot = node->ss.ss_ScanTupleSlot;
+ MemoryContext oldcontext = CurrentMemoryContext;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+
+ /*
+ * If this is the first call after Begin or ReScan, we need to execute
+ * remote query and get result set.
+ */
+ if (festate->tuples == NULL)
+ execute_query(node);
+
+ /*
+ * If tuples are still left in tuplestore, just return next tuple from it.
+ *
+ * It is necessary to switch to per-scan context to make returned tuple
+ * valid until next IterateForeignScan call, because it will be released
+ * with ExecClearTuple then. Otherwise, picked tuple is allocated in
+ * per-tuple context, and double-free of that tuple might happen.
+ *
+ * If we don't have any result in tuplestore, clear result slot to tell
+ * executor that this scan is over.
+ */
+ MemoryContextSwitchTo(festate->scan_cxt);
+ tuplestore_gettupleslot(festate->tuples, true, false, slot);
+ MemoryContextSwitchTo(oldcontext);
+
+ return slot;
+ }
+
+ /*
+ * pgsqlReScanForeignScan
+ * - Restart this scan by clearing old results and set re-execute flag.
+ */
+ static void
+ pgsqlReScanForeignScan(ForeignScanState *node)
+ {
+ PgsqlFdwExecutionState *festate;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+
+ /* If we haven't have valid result yet, nothing to do. */
+ if (festate->tuples == NULL)
+ return;
+
+ /*
+ * Only rewind the current result set is enough.
+ */
+ tuplestore_rescan(festate->tuples);
+ }
+
+ /*
+ * pgsqlEndForeignScan
+ * Finish scanning foreign table and dispose objects used for this scan
+ */
+ static void
+ pgsqlEndForeignScan(ForeignScanState *node)
+ {
+ PgsqlFdwExecutionState *festate;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+
+ /* if festate is NULL, we are in EXPLAIN; nothing to do */
+ if (festate == NULL)
+ return;
+
+ /*
+ * The connection which was used for this scan should be valid until the
+ * end of the scan to make the lifespan of remote transaction same as the
+ * local query.
+ */
+ ReleaseConnection(festate->conn);
+ festate->conn = NULL;
+
+ /* Discard fetch results */
+ if (festate->tuples != NULL)
+ {
+ tuplestore_end(festate->tuples);
+ festate->tuples = NULL;
+ }
+
+ /* MemoryContext will be deleted automatically. */
+ }
+
+ /*
+ * Estimate costs of executing given SQL statement.
+ */
+ static void
+ get_remote_estimate(const char *sql, PGconn *conn,
+ double *rows, int *width,
+ Cost *startup_cost, Cost *total_cost)
+ {
+ PGresult *res = NULL;
+ StringInfoData buf;
+ char *plan;
+ char *p;
+ int n;
+
+ /*
+ * Construct EXPLAIN statement with given SQL statement.
+ */
+ initStringInfo(&buf);
+ appendStringInfo(&buf, "EXPLAIN %s", sql);
+
+ /* PGresult must be released before leaving this function. */
+ PG_TRY();
+ {
+ res = PQexec(conn, buf.data);
+ if (PQresultStatus(res) != PGRES_TUPLES_OK || PQntuples(res) == 0)
+ {
+ char *msg;
+
+ msg = pstrdup(PQerrorMessage(conn));
+ ereport(ERROR,
+ (errmsg("could not execute EXPLAIN for cost estimation"),
+ errdetail("%s", msg),
+ errhint("%s", sql)));
+ }
+
+ /*
+ * Find estimation portion from top plan node. Here we search opening
+ * parentheses from the end of the line to avoid finding unexpected
+ * parentheses.
+ */
+ plan = PQgetvalue(res, 0, 0);
+ p = strrchr(plan, '(');
+ if (p == NULL)
+ elog(ERROR, "wrong EXPLAIN output: %s", plan);
+ n = sscanf(p,
+ "(cost=%lf..%lf rows=%lf width=%d)",
+ startup_cost, total_cost, rows, width);
+ if (n != 4)
+ elog(ERROR, "could not get estimation from EXPLAIN output");
+
+ PQclear(res);
+ res = NULL;
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+ }
+
+ /*
+ * Adjust costs estimated on remote end with some overheads such as connection
+ * and data transfer.
+ */
+ static void
+ adjust_costs(double rows, int width, Cost *startup_cost, Cost *total_cost)
+ {
+ /*
+ * TODO Selectivity of quals which are NOT pushed down should be also
+ * considered.
+ */
+
+ /* add cost to establish connection. */
+ *startup_cost += CONNECTION_COSTS;
+ *total_cost += CONNECTION_COSTS;
+
+ /* add cost to transfer result. */
+ *total_cost += TRANSFER_COSTS_PER_BYTE * width * rows;
+ *total_cost += cpu_tuple_cost * rows;
+ }
+
+ /*
+ * Execute remote query with current parameters.
+ */
+ static void
+ execute_query(ForeignScanState *node)
+ {
+ PgsqlFdwExecutionState *festate;
+ ParamListInfo params = node->ss.ps.state->es_param_list_info;
+ int numParams = params ? params->numParams : 0;
+ Oid *types = NULL;
+ const char **values = NULL;
+ char *sql;
+ PGconn *conn;
+ PGresult *res = NULL;
+
+ festate = (PgsqlFdwExecutionState *) node->fdw_state;
+ types = festate->param_types;
+ values = festate->param_values;
+
+ /*
+ * Construct parameter array in text format. We don't release memory for
+ * the arrays explicitly, because the memory usage would not be very large,
+ * and anyway they will be released in context cleanup.
+ */
+ if (numParams > 0)
+ {
+ int i;
+
+ 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;
+
+ getTypeOutputInfo(types[i], &out_func_oid, &isvarlena);
+ fmgr_info(out_func_oid, &func);
+ values[i] = OutputFunctionCall(&func, params->params[i].value);
+ }
+ }
+ }
+
+ /* PGresult must be released before leaving this function. */
+ PG_TRY();
+ {
+ /*
+ * Execute remote query with parameters, and retrieve results with
+ * custom row processor which stores results in tuplestore.
+ *
+ * We uninstall the custom row processor right after processing all
+ * results.
+ */
+ conn = festate->conn;
+ sql = strVal(list_nth(festate->fdw_private, FdwPrivateSelectSql));
+ PQsetRowProcessor(conn, query_row_processor, node);
+ res = PQexecParams(conn, sql, numParams, types, values, NULL, NULL, 0);
+ PQsetRowProcessor(conn, NULL, NULL);
+
+ /*
+ * We can't know whether the scan is over or not in custom row
+ * processor, so mark that the result is valid here.
+ */
+ tuplestore_donestoring(festate->tuples);
+
+ /*
+ * If the query has failed, reporting details is enough here.
+ * Connection(s) which are used by this query (at least used by
+ * pgsql_fdw) will be cleaned up by the foreign connection manager.
+ */
+ if (PQresultStatus(res) != PGRES_TUPLES_OK)
+ {
+ ereport(ERROR,
+ (errmsg("could not execute remote query"),
+ errdetail("%s", PQerrorMessage(conn)),
+ errhint("%s", sql)));
+ }
+
+ /* Discard result of SELECT statement. */
+ PQclear(res);
+ res = NULL;
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+ }
+
+ /*
+ * Create tuples from PGresult and store them into tuplestore.
+ *
+ * Caller must use PG_TRY block to catch exception and release PGresult
+ * surely.
+ */
+ static int
+ query_row_processor(PGresult *res,
+ const PGdataValue *columns,
+ const char **errmsgp,
+ void *param)
+ {
+ ForeignScanState *node = (ForeignScanState *) param;
+ int nfields = PQnfields(res);
+ int i;
+ int j;
+ int attnum; /* number of non-dropped columns */
+ TupleTableSlot *slot = node->ss.ss_ScanTupleSlot;
+ TupleDesc tupdesc = slot->tts_tupleDescriptor;
+ Form_pg_attribute *attrs = tupdesc->attrs;
+ PgsqlFdwExecutionState *festate = (PgsqlFdwExecutionState *) node->fdw_state;
+ AttInMetadata *attinmeta = festate->attinmeta;
+ HeapTuple tuple;
+ ErrorContextCallback errcontext;
+ MemoryContext oldcontext;
+
+ if (columns == NULL)
+ {
+ /* 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)));
+
+ /* First, ensure that the tuplestore is empty. */
+ if (festate->tuples == NULL)
+ {
+
+ /*
+ * Create tuplestore to store result of the query in per-query
+ * context. Note that we use this memory context to avoid memory
+ * leak in error cases.
+ */
+ oldcontext = MemoryContextSwitchTo(festate->scan_cxt);
+ festate->tuples = tuplestore_begin_heap(false, false, work_mem);
+ MemoryContextSwitchTo(oldcontext);
+ }
+ else
+ {
+ /* Clear old result just in case. */
+ tuplestore_clear(festate->tuples);
+ }
+
+ /* Initialize column value buffer. */
+ festate->colbuflen = INITIAL_COLBUF_SIZE;
+ festate->colbuf = palloc(festate->colbuflen);
+
+ return 1;
+ }
+
+ /*
+ * This function is called repeatedly until all result rows are processed,
+ * so we should allow interrupt.
+ */
+ CHECK_FOR_INTERRUPTS();
+
+ /*
+ * Do the following work in a temp context that we reset after each tuple.
+ * This cleans up not only the data we have direct access to, but any
+ * cruft the I/O functions might leak.
+ */
+ oldcontext = MemoryContextSwitchTo(festate->temp_cxt);
+
+ for (i = 0, j = 0; i < tupdesc->natts; i++)
+ {
+ int len = columns[j].len;
+
+ /* skip dropped columns. */
+ if (attrs[i]->attisdropped)
+ {
+ festate->nulls[i] = true;
+ continue;
+ }
+
+ /*
+ * Set NULL indicator, and convert text representation to internal
+ * representation if any.
+ */
+ if (len < 0)
+ festate->nulls[i] = true;
+ else
+ {
+ Datum value;
+
+ festate->nulls[i] = false;
+
+ while (festate->colbuflen < len + 1)
+ {
+ festate->colbuflen *= 2;
+ festate->colbuf = repalloc(festate->colbuf, festate->colbuflen);
+ }
+ memcpy(festate->colbuf, columns[j].value, len);
+ festate->colbuf[columns[j].len] = '\0';
+
+ /*
+ * Set up and install callback to report where conversion error
+ * occurs.
+ */
+ festate->errpos.cur_attno = i + 1;
+ errcontext.callback = pgsql_fdw_error_callback;
+ errcontext.arg = (void *) &festate->errpos;
+ errcontext.previous = error_context_stack;
+ error_context_stack = &errcontext;
+
+ value = InputFunctionCall(&attinmeta->attinfuncs[i],
+ festate->colbuf,
+ attinmeta->attioparams[i],
+ attinmeta->atttypmods[i]);
+ festate->values[i] = value;
+
+ /* Uninstall error context callback. */
+ error_context_stack = errcontext.previous;
+ }
+ 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 = heap_form_tuple(tupdesc, festate->values, festate->nulls);
+ tuplestore_puttuple(festate->tuples, tuple);
+
+ /* Clean up */
+ MemoryContextSwitchTo(oldcontext);
+ MemoryContextReset(festate->temp_cxt);
+
+ return 1;
+ }
+
+ /*
+ * Callback function which is called when error occurs during column value
+ * conversion. Print names of column and relation.
+ */
+ static void
+ pgsql_fdw_error_callback(void *arg)
+ {
+ ErrorPos *errpos = (ErrorPos *) arg;
+ const char *relname;
+ const char *colname;
+
+ relname = get_rel_name(errpos->relid);
+ colname = get_attname(errpos->relid, errpos->cur_attno);
+ errcontext("column %s of foreign table %s",
+ quote_identifier(colname), quote_identifier(relname));
+ }
diff --git a/contrib/pgsql_fdw/pgsql_fdw.control b/contrib/pgsql_fdw/pgsql_fdw.control
new file mode 100644
index ...0a9c8f4
*** a/contrib/pgsql_fdw/pgsql_fdw.control
--- b/contrib/pgsql_fdw/pgsql_fdw.control
***************
*** 0 ****
--- 1,5 ----
+ # pgsql_fdw extension
+ comment = 'foreign-data wrapper for remote PostgreSQL servers'
+ default_version = '1.0'
+ module_pathname = '$libdir/pgsql_fdw'
+ relocatable = true
diff --git a/contrib/pgsql_fdw/pgsql_fdw.h b/contrib/pgsql_fdw/pgsql_fdw.h
new file mode 100644
index ...4e7a8e1
*** a/contrib/pgsql_fdw/pgsql_fdw.h
--- b/contrib/pgsql_fdw/pgsql_fdw.h
***************
*** 0 ****
--- 1,33 ----
+ /*-------------------------------------------------------------------------
+ *
+ * pgsql_fdw.h
+ * foreign-data wrapper for remote PostgreSQL servers.
+ *
+ * Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pgsql_fdw/pgsql_fdw.h
+ *
+ *-------------------------------------------------------------------------
+ */
+
+ #ifndef PGSQL_FDW_H
+ #define PGSQL_FDW_H
+
+ #include "postgres.h"
+ #include "foreign/foreign.h"
+ #include "nodes/relation.h"
+
+ /* in option.c */
+ int ExtractConnectionOptions(List *defelems,
+ const char **keywords,
+ const char **values);
+ int GetFetchCountOption(ForeignTable *table, ForeignServer *server);
+
+ /* in deparse.c */
+ void deparseSimpleSql(StringInfo buf,
+ Oid relid,
+ PlannerInfo *root,
+ RelOptInfo *baserel);
+
+ #endif /* PGSQL_FDW_H */
diff --git a/contrib/pgsql_fdw/sql/pgsql_fdw.sql b/contrib/pgsql_fdw/sql/pgsql_fdw.sql
new file mode 100644
index ...da04568
*** a/contrib/pgsql_fdw/sql/pgsql_fdw.sql
--- b/contrib/pgsql_fdw/sql/pgsql_fdw.sql
***************
*** 0 ****
--- 1,251 ----
+ -- ===================================================================
+ -- create FDW objects
+ -- ===================================================================
+
+ -- Clean up in case a prior regression run failed
+
+ -- Suppress NOTICE messages when roles don't exist
+ SET client_min_messages TO 'error';
+
+ DROP ROLE IF EXISTS pgsql_fdw_user;
+
+ RESET client_min_messages;
+
+ CREATE ROLE pgsql_fdw_user LOGIN SUPERUSER;
+ SET SESSION AUTHORIZATION 'pgsql_fdw_user';
+
+ CREATE EXTENSION pgsql_fdw;
+
+ CREATE SERVER loopback1 FOREIGN DATA WRAPPER pgsql_fdw;
+ CREATE SERVER loopback2 FOREIGN DATA WRAPPER pgsql_fdw
+ OPTIONS (dbname 'contrib_regression');
+
+ CREATE USER MAPPING FOR public SERVER loopback1
+ OPTIONS (user 'value', password 'value');
+ CREATE USER MAPPING FOR pgsql_fdw_user SERVER loopback2;
+
+ CREATE FOREIGN TABLE ft1 (
+ c0 int,
+ c1 int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10)
+ ) SERVER loopback2;
+ ALTER FOREIGN TABLE ft1 DROP COLUMN c0;
+
+ CREATE FOREIGN TABLE ft2 (
+ c0 int,
+ c1 int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10)
+ ) SERVER loopback2;
+ ALTER FOREIGN TABLE ft2 DROP COLUMN c0;
+
+ -- ===================================================================
+ -- create objects used through FDW
+ -- ===================================================================
+ CREATE SCHEMA "S 1";
+ CREATE TABLE "S 1"."T 1" (
+ "C 1" int NOT NULL,
+ c2 int NOT NULL,
+ c3 text,
+ c4 timestamptz,
+ c5 timestamp,
+ c6 varchar(10),
+ c7 char(10),
+ CONSTRAINT t1_pkey PRIMARY KEY ("C 1")
+ );
+ CREATE TABLE "S 1"."T 2" (
+ c1 int NOT NULL,
+ c2 text,
+ CONSTRAINT t2_pkey PRIMARY KEY (c1)
+ );
+
+ BEGIN;
+ TRUNCATE "S 1"."T 1";
+ INSERT INTO "S 1"."T 1"
+ SELECT id,
+ id % 10,
+ to_char(id, 'FM00000'),
+ '1970-01-01'::timestamptz + ((id % 100) || ' days')::interval,
+ '1970-01-01'::timestamp + ((id % 100) || ' days')::interval,
+ id % 10,
+ id % 10
+ FROM generate_series(1, 1000) id;
+ TRUNCATE "S 1"."T 2";
+ INSERT INTO "S 1"."T 2"
+ SELECT id,
+ 'AAA' || to_char(id, 'FM000')
+ FROM generate_series(1, 100) id;
+ COMMIT;
+
+ -- ===================================================================
+ -- tests for pgsql_fdw_validator
+ -- ===================================================================
+ ALTER FOREIGN DATA WRAPPER pgsql_fdw OPTIONS (host 'value'); -- ERROR
+ -- requiressl, krbsrvname and gsslib are omitted because they depend on
+ -- configure option
+ ALTER SERVER loopback1 OPTIONS (
+ authtype 'value',
+ service 'value',
+ connect_timeout 'value',
+ dbname 'value',
+ host 'value',
+ hostaddr 'value',
+ port 'value',
+ --client_encoding 'value',
+ tty 'value',
+ options 'value',
+ application_name 'value',
+ --fallback_application_name 'value',
+ keepalives 'value',
+ keepalives_idle 'value',
+ keepalives_interval 'value',
+ -- requiressl 'value',
+ sslcompression 'value',
+ sslmode 'value',
+ sslcert 'value',
+ sslkey 'value',
+ sslrootcert 'value',
+ sslcrl 'value'
+ --requirepeer 'value',
+ -- krbsrvname 'value',
+ -- gsslib 'value',
+ --replication 'value'
+ );
+ ALTER SERVER loopback1 OPTIONS (user 'value'); -- ERROR
+ ALTER USER MAPPING FOR public SERVER loopback1
+ OPTIONS (DROP user, DROP password);
+ ALTER USER MAPPING FOR public SERVER loopback1
+ OPTIONS (host 'value'); -- ERROR
+ ALTER FOREIGN TABLE ft1 OPTIONS (nspname 'S 1', relname 'T 1');
+ ALTER FOREIGN TABLE ft2 OPTIONS (nspname 'S 1', relname 'T 1');
+ ALTER FOREIGN TABLE ft1 OPTIONS (invalid 'value'); -- ERROR
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (invalid 'value'); -- ERROR
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (colname 'C 1');
+ ALTER FOREIGN TABLE ft2 ALTER COLUMN c1 OPTIONS (colname 'C 1');
+ \dew+
+ \des+
+ \deu+
+ \det+
+
+ -- ===================================================================
+ -- simple queries
+ -- ===================================================================
+ -- single table, with/without alias
+ EXPLAIN (COSTS false) SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
+ SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+ SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+ -- with WHERE clause
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 >= '1';
+ SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 >= '1';
+ -- 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
+ SELECT * FROM ft1 t1 WHERE t1.c3 = (SELECT MAX(c3) FROM ft2 t2) ORDER BY c1;
+ -- used in CTE
+ WITH t1 AS (SELECT * FROM ft1 WHERE c1 <= 10) SELECT t2.c1, t2.c2, t2.c3, t2.c4 FROM t1, ft2 t2 WHERE t1.c1 = t2.c1 ORDER BY t1.c1;
+ -- fixed values
+ SELECT 'fixed', NULL FROM ft1 t1 WHERE c1 = 1;
+ -- user-defined operator/function
+ CREATE FUNCTION pgsql_fdw_abs(int) RETURNS int AS $$
+ BEGIN
+ RETURN abs($1);
+ END
+ $$ LANGUAGE plpgsql IMMUTABLE;
+ CREATE OPERATOR === (
+ LEFTARG = int,
+ RIGHTARG = int,
+ PROCEDURE = int4eq,
+ COMMUTATOR = ===,
+ NEGATOR = !==
+ );
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = pgsql_fdw_abs(t1.c2);
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 === t1.c2;
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = abs(t1.c2);
+ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = t1.c2;
+
+ -- ===================================================================
+ -- parameterized queries
+ -- ===================================================================
+ -- simple join
+ PREPARE st1(int, int) AS SELECT t1.c3, t2.c3 FROM ft1 t1, ft2 t2 WHERE t1.c1 = $1 AND t2.c1 = $2;
+ EXPLAIN (COSTS false) EXECUTE st1(1, 2);
+ EXECUTE st1(1, 1);
+ EXECUTE st1(101, 101);
+ -- subquery using stable function (can't be pushed down)
+ PREPARE st2(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c4) = 6) ORDER BY c1;
+ EXPLAIN (COSTS false) EXECUTE st2(10, 20);
+ EXECUTE st2(10, 20);
+ EXECUTE st1(101, 101);
+ -- subquery using immutable function (can be pushed down)
+ PREPARE st3(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c5) = 6) ORDER BY c1;
+ EXPLAIN (COSTS false) EXECUTE st3(10, 20);
+ EXECUTE st3(10, 20);
+ EXECUTE st3(20, 30);
+ -- custom plan should be chosen
+ PREPARE st4(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 = $1;
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ EXPLAIN (COSTS false) EXECUTE st4(1);
+ -- cleanup
+ DEALLOCATE st1;
+ DEALLOCATE st2;
+ DEALLOCATE st3;
+ DEALLOCATE st4;
+
+ -- ===================================================================
+ -- connection management
+ -- ===================================================================
+ SELECT srvname, usename FROM pgsql_fdw_connections;
+ SELECT pgsql_fdw_disconnect(srvid, usesysid) FROM pgsql_fdw_get_connections();
+ SELECT srvname, usename FROM pgsql_fdw_connections;
+
+ -- ===================================================================
+ -- conversion error
+ -- ===================================================================
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c5 TYPE int;
+ SELECT * FROM ft1 WHERE c1 = 1; -- ERROR
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c5 TYPE timestamp;
+
+ -- ===================================================================
+ -- subtransaction
+ -- ===================================================================
+ BEGIN;
+ DECLARE c CURSOR FOR SELECT * FROM ft1 ORDER BY c1;
+ FETCH c;
+ SAVEPOINT s;
+ ERROR OUT; -- ERROR
+ ROLLBACK TO s;
+ SELECT srvname FROM pgsql_fdw_connections;
+ FETCH c;
+ COMMIT;
+ SELECT srvname FROM pgsql_fdw_connections;
+ ERROR OUT; -- ERROR
+ SELECT srvname FROM pgsql_fdw_connections;
+
+ -- ===================================================================
+ -- cleanup
+ -- ===================================================================
+ DROP OPERATOR === (int, int) CASCADE;
+ DROP OPERATOR !== (int, int) CASCADE;
+ DROP FUNCTION pgsql_fdw_abs(int);
+ DROP SCHEMA "S 1" CASCADE;
+ DROP EXTENSION pgsql_fdw CASCADE;
+ \c
+ DROP ROLE pgsql_fdw_user;
diff --git a/doc/src/sgml/contrib.sgml b/doc/src/sgml/contrib.sgml
new file mode 100644
index 97031dd..c4dd9c3
*** a/doc/src/sgml/contrib.sgml
--- b/doc/src/sgml/contrib.sgml
*************** CREATE EXTENSION <replaceable>module_nam
*** 117,122 ****
--- 117,123 ----
&pgcrypto;
&pgfreespacemap;
&pgrowlocks;
+ &pgsql-fdw;
&pgstandby;
&pgstatstatements;
&pgstattuple;
diff --git a/doc/src/sgml/filelist.sgml b/doc/src/sgml/filelist.sgml
new file mode 100644
index 428a167..f3ff180
*** a/doc/src/sgml/filelist.sgml
--- b/doc/src/sgml/filelist.sgml
***************
*** 125,130 ****
--- 125,131 ----
<!ENTITY pgcrypto SYSTEM "pgcrypto.sgml">
<!ENTITY pgfreespacemap SYSTEM "pgfreespacemap.sgml">
<!ENTITY pgrowlocks SYSTEM "pgrowlocks.sgml">
+ <!ENTITY pgsql-fdw SYSTEM "pgsql-fdw.sgml">
<!ENTITY pgstandby SYSTEM "pgstandby.sgml">
<!ENTITY pgstatstatements SYSTEM "pgstatstatements.sgml">
<!ENTITY pgstattuple SYSTEM "pgstattuple.sgml">
diff --git a/doc/src/sgml/pgsql-fdw.sgml b/doc/src/sgml/pgsql-fdw.sgml
new file mode 100644
index ...ee9c94a
*** a/doc/src/sgml/pgsql-fdw.sgml
--- b/doc/src/sgml/pgsql-fdw.sgml
***************
*** 0 ****
--- 1,234 ----
+ <!-- doc/src/sgml/pgsql-fdw.sgml -->
+
+ <sect1 id="pgsql-fdw" xreflabel="pgsql_fdw">
+ <title>pgsql_fdw</title>
+
+ <indexterm zone="pgsql-fdw">
+ <primary>pgsql_fdw</primary>
+ </indexterm>
+
+ <para>
+ The <filename>pgsql_fdw</filename> module provides a foreign-data wrapper for
+ external <productname>PostgreSQL</productname> servers.
+ With this module, users can access data stored in external
+ <productname>PostgreSQL</productname> via plain SQL statements.
+ </para>
+
+ <para>
+ Note that default wrapper <literal>pgsql_fdw</literal> is created
+ automatically during <command>CREATE EXTENSION</command> command for
+ <application>pgsql_fdw</application>.
+ </para>
+
+ <sect2>
+ <title>FDW Options of pgsql_fdw</title>
+
+ <sect3>
+ <title>Connection Options</title>
+ <para>
+ A foreign server and user mapping created using this wrapper can have
+ <application>libpq</> connection options, expect below:
+
+ <itemizedlist>
+ <listitem><para>client_encoding</para></listitem>
+ <listitem><para>fallback_application_name</para></listitem>
+ <listitem><para>replication</para></listitem>
+ </itemizedlist>
+
+ For details of <application>libpq</> connection options, see
+ <xref linkend="libpq-connect">.
+ </para>
+
+ <para>
+ <literal>user</literal> and <literal>password</literal> can be
+ specified on user mappings, and others can be specified on foreign servers.
+ </para>
+ </sect3>
+
+ <sect3>
+ <title>Object Name Options</title>
+ <para>
+ Foreign tables which were created using this wrapper, or its columns can
+ have object name options. These options can be used to specify the names
+ used in SQL statement sent to remote <productname>PostgreSQL</productname>
+ server. These options are useful when a remote object has different name
+ from corresponding local one.
+ </para>
+
+ <variablelist>
+
+ <varlistentry>
+ <term><literal>nspname</literal></term>
+ <listitem>
+ <para>
+ This option, which can be specified on a foreign table, is used as a
+ namespace (schema) reference in the SQL statement. If this options is
+ omitted, <literal>pg_class.nspname</literal> of the foreign table is
+ used.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>relname</literal></term>
+ <listitem>
+ <para>
+ This option, which can be specified on a foreign table, is used as a
+ relation (table) reference in the SQL statement. If this options is
+ omitted, <literal>pg_class.relname</literal> of the foreign table is
+ used.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>colname</literal></term>
+ <listitem>
+ <para>
+ This option, which can be specified on a column of a foreign table, is
+ used as a column (attribute) reference in the SQL statement. If this
+ option is omitted, <literal>pg_attribute.attname</literal> of the column
+ of the foreign table is used.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ </variablelist>
+
+ </sect3>
+
+ </sect2>
+
+ <sect2>
+ <title>Connection Management</title>
+
+ <para>
+ The <application>pgsql_fdw</application> establishes a connection to a
+ foreign server in the beginning of the first query which uses a foreign
+ table associated to the foreign server, and reuses the connection following
+ queries and even in following foreign scans in same query.
+
+ You can see the list of active connections via
+ <structname>pgsql_fdw_connections</structname> view. It shows pair of oid
+ and name of server and local role for each active connections established by
+ <application>pgsql_fdw</application>. For security reason, only superuser
+ can see other role's connections.
+ </para>
+
+ <para>
+ Established connections are kept alive until local role changes or the
+ current transaction aborts or user requests so.
+ </para>
+
+ <para>
+ If role has been changed, active connections established as old local role
+ is kept alive but never be reused until local role has restored to original
+ role. This kind of situation happens with <command>SET ROLE</command> and
+ <command>SET SESSION AUTHORIZATION</command>.
+ </para>
+
+ <para>
+ If current transaction aborts by error or user request, all active
+ connections are disconnected automatically. This behavior avoids possible
+ connection leaks on error.
+ </para>
+
+ <para>
+ You can discard persistent connection at arbitrary timing with
+ <function>pgsql_fdw_disconnect()</function>. It takes server oid and
+ user oid as arguments. This function can handle only connections
+ established in current session; connections established by other backends
+ are not reachable.
+ </para>
+
+ <para>
+ You can discard all active and visible connections in current session with
+ using <structname>pgsql_fdw_connections</structname> and
+ <function>pgsql_fdw_disconnect()</function> together:
+ <synopsis>
+ postgres=# SELECT pgsql_fdw_disconnect(srvid, usesysid) FROM pgsql_fdw_connections;
+ pgsql_fdw_disconnect
+ ----------------------
+ OK
+ OK
+ (2 rows)
+ </synopsis>
+ </para>
+ </sect2>
+
+ <sect2>
+ <title>Transaction Management</title>
+ <para>
+ The <application>pgsql_fdw</application> begins remote transaction at the
+ beginning of a local query, and terminates it with <command>ABORT</command>
+ at the end of the local query. This means that all foreign scans on a
+ foreign server in a local query are executed in one transaction.
+ If isolation level of local transaction is <literal>SERIALIZABLE</literal>,
+ <literal>SERIALIZABLE</literal> is used for remote transaction. Otherwise,
+ if isolation level of local transaction is one of
+ <literal>READ UNCOMMITTED</literal>, <literal>READ COMMITTED</literal> or
+ <literal>REPEATABLE READ</literal>, then <literal>REPEATABLE READ</literal>
+ is used for remote transaction.
+ <literal>READ UNCOMMITTED</literal> and <literal>READ COMMITTED</literal>
+ are never used for remote transaction, because even
+ <literal>READ COMMITTED</literal> transaction might produce inconsistent
+ results, if remote data have been updated between two remote queries.
+ </para>
+ <para>
+ Note that even if the isolation level of local transaction was
+ <literal>SERIALIZABLE</literal> or <literal>REPEATABLE READ</literal>,
+ series of one query might produce different result, because foreign scans
+ in different local queries are executed in different remote transactions.
+ For instance, when client started a local transaction
+ explicitly with isolation level <literal>SERIALIZABLE</literal>, and
+ executed same local query which contains a foreign table which references
+ foreign data which is updated frequently, latter result would be different
+ from former result.
+ </para>
+ <para>
+ This restriction might be relaxed in future release.
+ </para>
+ </sect2>
+
+ <sect2>
+ <title>Estimation of Costs and Rows</title>
+ <para>
+ The <application>pgsql_fdw</application> estimates the costs of a foreign
+ scan by adding up some basic costs: connection costs, remote query costs
+ and data transfer costs.
+ To get remote query costs, <application>pgsql_fdw</application> executes
+ <command>EXPLAIN</command> command on remote server for each foreign scan.
+ </para>
+ <para>
+ On the other hand, estimated rows which was returned by
+ <command>EXPLAIN</command> is used for local estimation as-is.
+ </para>
+ </sect2>
+
+ <sect2>
+ <title>EXPLAIN Output</title>
+ <para>
+ For a foreign table using <literal>pgsql_fdw</>, <command>EXPLAIN</> shows
+ a remote SQL statement which is sent to remote
+ <productname>PostgreSQL</productname> server for a ForeignScan plan node.
+ For example:
+ </para>
+ <synopsis>
+ postgres=# EXPLAIN SELECT aid FROM pgbench_accounts WHERE abalance < 0;
+ QUERY PLAN
+ --------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on pgbench_accounts (cost=100.00..8105.13 rows=302613 width=8)
+ Filter: (abalance < 0)
+ Remote SQL: SELECT aid, NULL, abalance, NULL FROM public.pgbench_accounts
+ (3 rows)
+ </synopsis>
+ </sect2>
+
+ <sect2>
+ <title>Author</title>
+ <para>
+ Shigeru Hanada <email>shigeru.hanada@gmail.com</email>
+ </para>
+ </sect2>
+
+ </sect1>
pgsql_fdw_pushdown_v13.patchtext/plain; charset=Shift_JIS; name=pgsql_fdw_pushdown_v13.patchDownload
diff --git a/contrib/pgsql_fdw/deparse.c b/contrib/pgsql_fdw/deparse.c
index e1bbf6b..148d0b4 100644
--- a/contrib/pgsql_fdw/deparse.c
+++ b/contrib/pgsql_fdw/deparse.c
@@ -14,6 +14,8 @@
#include "access/transam.h"
#include "catalog/pg_class.h"
+#include "catalog/pg_operator.h"
+#include "catalog/pg_type.h"
#include "commands/defrem.h"
#include "foreign/foreign.h"
#include "lib/stringinfo.h"
@@ -22,9 +24,11 @@
#include "nodes/makefuncs.h"
#include "optimizer/clauses.h"
#include "optimizer/var.h"
+#include "parser/parser.h"
#include "parser/parsetree.h"
#include "utils/builtins.h"
#include "utils/lsyscache.h"
+#include "utils/syscache.h"
#include "pgsql_fdw.h"
@@ -35,15 +39,39 @@ typedef struct foreign_executable_cxt
{
PlannerInfo *root;
RelOptInfo *foreignrel;
+ bool has_param;
} foreign_executable_cxt;
/*
* Get string representation which can be used in SQL statement from a node.
*/
+static void deparseExpr(StringInfo buf, Expr *expr, PlannerInfo *root);
static void deparseRelation(StringInfo buf, Oid relid, PlannerInfo *root,
bool need_prefix);
static void deparseVar(StringInfo buf, Var *node, PlannerInfo *root,
bool need_prefix);
+static void deparseConst(StringInfo buf, Const *node, PlannerInfo *root);
+static void deparseBoolExpr(StringInfo buf, BoolExpr *node, PlannerInfo *root);
+static void deparseNullTest(StringInfo buf, NullTest *node, PlannerInfo *root);
+static void deparseDistinctExpr(StringInfo buf, DistinctExpr *node,
+ PlannerInfo *root);
+static void deparseRelabelType(StringInfo buf, RelabelType *node,
+ PlannerInfo *root);
+static void deparseFuncExpr(StringInfo buf, FuncExpr *node, PlannerInfo *root);
+static void deparseParam(StringInfo buf, Param *node, PlannerInfo *root);
+static void deparseScalarArrayOpExpr(StringInfo buf, ScalarArrayOpExpr *node,
+ PlannerInfo *root);
+static void deparseOpExpr(StringInfo buf, OpExpr *node, PlannerInfo *root);
+static void deparseArrayRef(StringInfo buf, ArrayRef *node, PlannerInfo *root);
+static void deparseArrayExpr(StringInfo buf, ArrayExpr *node, PlannerInfo *root);
+
+/*
+ * Determine whether an expression can be evaluated on remote side safely.
+ */
+static bool is_foreign_expr(PlannerInfo *root, RelOptInfo *baserel, Expr *expr,
+ bool *has_param);
+static bool foreign_expr_walker(Node *node, foreign_executable_cxt *context);
+static bool is_builtin(Oid procid);
/*
* Deparse query representation into SQL statement which suits for remote
@@ -150,6 +178,107 @@ deparseSimpleSql(StringInfo buf,
}
/*
+ * Examine each element in the list baserestrictinfo of baserel, and sort them
+ * into three groups: remote_conds contains conditions which can be evaluated
+ * - remote_conds is push-down safe, and don't contain any Param node
+ * - param_conds is push-down safe, but contain some Param node
+ * - local_conds is not push-down safe
+ *
+ * Only remote_conds can be used in remote EXPLAIN, and remote_conds and
+ * param_conds can be used in final remote query.
+ */
+void
+sortConditions(PlannerInfo *root,
+ RelOptInfo *baserel,
+ List **remote_conds,
+ List **param_conds,
+ List **local_conds)
+{
+ ListCell *lc;
+ bool has_param;
+
+ Assert(remote_conds);
+ Assert(param_conds);
+ Assert(local_conds);
+
+ foreach(lc, baserel->baserestrictinfo)
+ {
+ RestrictInfo *ri = (RestrictInfo *) lfirst(lc);
+
+ if (is_foreign_expr(root, baserel, ri->clause, &has_param))
+ {
+ if (has_param)
+ *param_conds = lappend(*param_conds, ri);
+ else
+ *remote_conds = lappend(*remote_conds, ri);
+ }
+ else
+ *local_conds = lappend(*local_conds, ri);
+ }
+}
+
+/*
+ * Deparse given expression into buf. Actual string operation is delegated to
+ * node-type-specific functions.
+ *
+ * Note that switch statement of this function MUST match the one in
+ * foreign_expr_walker to avoid unsupported error..
+ */
+static void
+deparseExpr(StringInfo buf, Expr *node, PlannerInfo *root)
+{
+ /*
+ * This part must be match foreign_expr_walker.
+ */
+ switch (nodeTag(node))
+ {
+ case T_Const:
+ deparseConst(buf, (Const *) node, root);
+ break;
+ case T_BoolExpr:
+ deparseBoolExpr(buf, (BoolExpr *) node, root);
+ break;
+ case T_NullTest:
+ deparseNullTest(buf, (NullTest *) node, root);
+ break;
+ case T_DistinctExpr:
+ deparseDistinctExpr(buf, (DistinctExpr *) node, root);
+ break;
+ case T_RelabelType:
+ deparseRelabelType(buf, (RelabelType *) node, root);
+ break;
+ case T_FuncExpr:
+ deparseFuncExpr(buf, (FuncExpr *) node, root);
+ break;
+ case T_Param:
+ deparseParam(buf, (Param *) node, root);
+ break;
+ case T_ScalarArrayOpExpr:
+ deparseScalarArrayOpExpr(buf, (ScalarArrayOpExpr *) node, root);
+ break;
+ case T_OpExpr:
+ deparseOpExpr(buf, (OpExpr *) node, root);
+ break;
+ case T_Var:
+ deparseVar(buf, (Var *) node, root, false);
+ break;
+ case T_ArrayRef:
+ deparseArrayRef(buf, (ArrayRef *) node, root);
+ break;
+ case T_ArrayExpr:
+ deparseArrayExpr(buf, (ArrayExpr *) node, root);
+ break;
+ default:
+ {
+ ereport(ERROR,
+ (errmsg("unsupported expression for deparse"),
+ errdetail("%s", nodeToString(node))));
+ }
+ break;
+ }
+}
+
+/*
* Deparse node into buf, with relation qualifier if need_prefix was true. If
* node is a column of a foreign table, use value of colname FDW option (if any)
* instead of attribute name.
@@ -285,3 +414,733 @@ deparseRelation(StringInfo buf,
appendStringInfo(buf, "%s.", q_nspname);
appendStringInfo(buf, "%s", q_relname);
}
+
+/*
+ * Deparse given constant value into buf. This function have to be kept in
+ * sync with get_const_expr.
+ */
+static void
+deparseConst(StringInfo buf,
+ Const *node,
+ PlannerInfo *root)
+{
+ Oid typoutput;
+ bool typIsVarlena;
+ char *extval;
+ bool isfloat = false;
+ bool needlabel;
+
+ if (node->constisnull)
+ {
+ appendStringInfo(buf, "NULL");
+ return;
+ }
+
+ getTypeOutputInfo(node->consttype,
+ &typoutput, &typIsVarlena);
+ extval = OidOutputFunctionCall(typoutput, node->constvalue);
+
+ switch (node->consttype)
+ {
+ case ANYARRAYOID:
+ case ANYNONARRAYOID:
+ elog(ERROR, "anyarray and anyenum are not supported");
+ break;
+ case INT2OID:
+ case INT4OID:
+ case INT8OID:
+ case OIDOID:
+ case FLOAT4OID:
+ case FLOAT8OID:
+ case NUMERICOID:
+ {
+ /*
+ * No need to quote unless they contain special values such as
+ * 'Nan'.
+ */
+ if (strspn(extval, "0123456789+-eE.") == strlen(extval))
+ {
+ if (extval[0] == '+' || extval[0] == '-')
+ appendStringInfo(buf, "(%s)", extval);
+ else
+ appendStringInfoString(buf, extval);
+ if (strcspn(extval, "eE.") != strlen(extval))
+ isfloat = true; /* it looks like a float */
+ }
+ else
+ appendStringInfo(buf, "'%s'", extval);
+ }
+ break;
+ case BITOID:
+ case VARBITOID:
+ appendStringInfo(buf, "B'%s'", extval);
+ break;
+ case BOOLOID:
+ if (strcmp(extval, "t") == 0)
+ appendStringInfoString(buf, "true");
+ else
+ appendStringInfoString(buf, "false");
+ break;
+
+ default:
+ {
+ const char *valptr;
+
+ appendStringInfoChar(buf, '\'');
+ for (valptr = extval; *valptr; valptr++)
+ {
+ char ch = *valptr;
+
+ /*
+ * standard_conforming_strings of remote session should be
+ * set to similar value as local session.
+ */
+ if (SQL_STR_DOUBLE(ch, !standard_conforming_strings))
+ appendStringInfoChar(buf, ch);
+ appendStringInfoChar(buf, ch);
+ }
+ appendStringInfoChar(buf, '\'');
+ }
+ break;
+ }
+
+ /*
+ * Append ::typename unless the constant will be implicitly typed as the
+ * right type when it is read in.
+ *
+ * XXX this code has to be kept in sync with the behavior of the parser,
+ * especially make_const.
+ */
+ switch (node->consttype)
+ {
+ case BOOLOID:
+ case INT4OID:
+ case UNKNOWNOID:
+ needlabel = false;
+ break;
+ case NUMERICOID:
+ needlabel = !isfloat || (node->consttypmod >= 0);
+ break;
+ default:
+ needlabel = true;
+ break;
+ }
+ if (needlabel)
+ {
+ appendStringInfo(buf, "::%s",
+ format_type_with_typemod(node->consttype,
+ node->consttypmod));
+ }
+}
+
+static void
+deparseBoolExpr(StringInfo buf,
+ BoolExpr *node,
+ PlannerInfo *root)
+{
+ ListCell *lc;
+ char *op;
+ bool first;
+
+ switch (node->boolop)
+ {
+ case AND_EXPR:
+ op = "AND";
+ break;
+ case OR_EXPR:
+ op = "OR";
+ break;
+ case NOT_EXPR:
+ appendStringInfo(buf, "(NOT ");
+ deparseExpr(buf, list_nth(node->args, 0), root);
+ appendStringInfo(buf, ")");
+ return;
+ }
+
+ first = true;
+ appendStringInfo(buf, "(");
+ foreach(lc, node->args)
+ {
+ if (!first)
+ appendStringInfo(buf, " %s ", op);
+ deparseExpr(buf, (Expr *) lfirst(lc), root);
+ first = false;
+ }
+ appendStringInfo(buf, ")");
+}
+
+/*
+ * Deparse given IS [NOT] NULL test expression into buf.
+ */
+static void
+deparseNullTest(StringInfo buf,
+ NullTest *node,
+ PlannerInfo *root)
+{
+ appendStringInfoChar(buf, '(');
+ deparseExpr(buf, node->arg, root);
+ if (node->nulltesttype == IS_NULL)
+ appendStringInfo(buf, " IS NULL)");
+ else
+ appendStringInfo(buf, " IS NOT NULL)");
+}
+
+static void
+deparseDistinctExpr(StringInfo buf,
+ DistinctExpr *node,
+ PlannerInfo *root)
+{
+ Assert(list_length(node->args) == 2);
+
+ deparseExpr(buf, linitial(node->args), root);
+ appendStringInfo(buf, " IS DISTINCT FROM ");
+ deparseExpr(buf, lsecond(node->args), root);
+}
+
+static void
+deparseRelabelType(StringInfo buf,
+ RelabelType *node,
+ PlannerInfo *root)
+{
+ char *typname;
+
+ Assert(node->arg);
+
+ /* We don't need to deparse cast when argument has same type as result. */
+ if (IsA(node->arg, Const) &&
+ ((Const *) node->arg)->consttype == node->resulttype &&
+ ((Const *) node->arg)->consttypmod == -1)
+ {
+ deparseExpr(buf, node->arg, root);
+ return;
+ }
+
+ typname = format_type_with_typemod(node->resulttype, node->resulttypmod);
+ appendStringInfoChar(buf, '(');
+ deparseExpr(buf, node->arg, root);
+ appendStringInfo(buf, ")::%s", typname);
+}
+
+/*
+ * Deparse given node which represents a function call into buf. We treat only
+ * explicit function call and explicit cast (coerce), because others are
+ * processed on remote side if necessary.
+ *
+ * Function name (and type name) is always qualified by schema name to avoid
+ * problems caused by different setting of search_path on remote side.
+ */
+static void
+deparseFuncExpr(StringInfo buf,
+ FuncExpr *node,
+ PlannerInfo *root)
+{
+ Oid pronamespace;
+ const char *schemaname;
+ const char *funcname;
+ ListCell *arg;
+ bool first;
+
+ pronamespace = get_func_namespace(node->funcid);
+ schemaname = quote_identifier(get_namespace_name(pronamespace));
+ funcname = quote_identifier(get_func_name(node->funcid));
+
+ if (node->funcformat == COERCE_EXPLICIT_CALL)
+ {
+ /* Function call, deparse all arguments recursively. */
+ appendStringInfo(buf, "%s.%s(", schemaname, funcname);
+ first = true;
+ foreach(arg, node->args)
+ {
+ if (!first)
+ appendStringInfo(buf, ", ");
+ deparseExpr(buf, lfirst(arg), root);
+ first = false;
+ }
+ appendStringInfoChar(buf, ')');
+ }
+ else if (node->funcformat == COERCE_EXPLICIT_CAST)
+ {
+ /* Explicit cast, deparse only first argument. */
+ appendStringInfoChar(buf, '(');
+ deparseExpr(buf, linitial(node->args), root);
+ appendStringInfo(buf, ")::%s", funcname);
+ }
+ else
+ {
+ /* Implicit cast, deparse only first argument. */
+ deparseExpr(buf, linitial(node->args), root);
+ }
+}
+
+/*
+ * Deparse given Param node into buf.
+ *
+ * We don't renumber parameter id, because skipping $1 is not cause problem
+ * as far as we pass through all arguments.
+ */
+static void
+deparseParam(StringInfo buf,
+ Param *node,
+ PlannerInfo *root)
+{
+ Assert(node->paramkind == PARAM_EXTERN);
+
+ appendStringInfo(buf, "$%d", node->paramid);
+}
+
+/*
+ * Deparse given ScalarArrayOpExpr expression into buf. To avoid problems
+ * around priority of operations, we always parenthesize the arguments. Also we
+ * use OPERATOR(schema.operator) notation to determine remote operator exactly.
+ */
+static void
+deparseScalarArrayOpExpr(StringInfo buf,
+ ScalarArrayOpExpr *node,
+ PlannerInfo *root)
+{
+ HeapTuple tuple;
+ Form_pg_operator form;
+ const char *opnspname;
+ char *opname;
+ Expr *arg1;
+ Expr *arg2;
+
+ /* Retrieve necessary information about the operator from system catalog. */
+ tuple = SearchSysCache1(OPEROID, ObjectIdGetDatum(node->opno));
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "cache lookup failed for operator %u", node->opno);
+ form = (Form_pg_operator) GETSTRUCT(tuple);
+ /* opname is not a SQL identifier, so we don't need to quote it. */
+ opname = NameStr(form->oprname);
+ opnspname = quote_identifier(get_namespace_name(form->oprnamespace));
+ ReleaseSysCache(tuple);
+
+ /* Sanity check. */
+ Assert(list_length(node->args) == 2);
+
+ /* Always parenthesize the expression. */
+ appendStringInfoChar(buf, '(');
+
+ /* Extract operands. */
+ arg1 = linitial(node->args);
+ arg2 = lsecond(node->args);
+
+ /* Deparse fully qualified operator name. */
+ deparseExpr(buf, arg1, root);
+ appendStringInfo(buf, " OPERATOR(%s.%s) %s (",
+ opnspname, opname, node->useOr ? "ANY" : "ALL");
+ deparseExpr(buf, arg2, root);
+ appendStringInfoChar(buf, ')');
+
+ /* Always parenthesize the expression. */
+ appendStringInfoChar(buf, ')');
+}
+
+/*
+ * Deparse given operator expression into buf. To avoid problems around
+ * priority of operations, we always parenthesize the arguments. Also we use
+ * OPERATOR(schema.operator) notation to determine remote operator exactly.
+ */
+static void
+deparseOpExpr(StringInfo buf,
+ OpExpr *node,
+ PlannerInfo *root)
+{
+ HeapTuple tuple;
+ Form_pg_operator form;
+ const char *opnspname;
+ char *opname;
+ char oprkind;
+ ListCell *arg;
+
+ /* Retrieve necessary information about the operator from system catalog. */
+ tuple = SearchSysCache1(OPEROID, ObjectIdGetDatum(node->opno));
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "cache lookup failed for operator %u", node->opno);
+ form = (Form_pg_operator) GETSTRUCT(tuple);
+ opnspname = quote_identifier(get_namespace_name(form->oprnamespace));
+ /* opname is not a SQL identifier, so we don't need to quote it. */
+ opname = NameStr(form->oprname);
+ oprkind = form->oprkind;
+ ReleaseSysCache(tuple);
+
+ /* Sanity check. */
+ Assert((oprkind == 'r' && list_length(node->args) == 1) ||
+ (oprkind == 'l' && list_length(node->args) == 1) ||
+ (oprkind == 'b' && list_length(node->args) == 2));
+
+ /* Always parenthesize the expression. */
+ appendStringInfoChar(buf, '(');
+
+ /* Deparse first operand. */
+ arg = list_head(node->args);
+ if (oprkind == 'r' || oprkind == 'b')
+ {
+ deparseExpr(buf, lfirst(arg), root);
+ appendStringInfoChar(buf, ' ');
+ }
+
+ /* Deparse fully qualified operator name. */
+ appendStringInfo(buf, "OPERATOR(%s.%s)", opnspname, opname);
+
+ /* Deparse last operand. */
+ arg = list_tail(node->args);
+ if (oprkind == 'l' || oprkind == 'b')
+ {
+ appendStringInfoChar(buf, ' ');
+ deparseExpr(buf, lfirst(arg), root);
+ }
+
+ appendStringInfoChar(buf, ')');
+}
+
+static void
+deparseArrayRef(StringInfo buf,
+ ArrayRef *node,
+ PlannerInfo *root)
+{
+ ListCell *lowlist_item;
+ ListCell *uplist_item;
+
+ /* Always parenthesize the expression. */
+ appendStringInfoChar(buf, '(');
+
+ /* Deparse referenced array expression first. */
+ appendStringInfoChar(buf, '(');
+ deparseExpr(buf, node->refexpr, root);
+ appendStringInfoChar(buf, ')');
+
+ /* Deparse subscripts expression. */
+ lowlist_item = list_head(node->reflowerindexpr); /* could be NULL */
+ foreach(uplist_item, node->refupperindexpr)
+ {
+ appendStringInfoChar(buf, '[');
+ if (lowlist_item)
+ {
+ deparseExpr(buf, lfirst(lowlist_item), root);
+ appendStringInfoChar(buf, ':');
+ lowlist_item = lnext(lowlist_item);
+ }
+ deparseExpr(buf, lfirst(uplist_item), root);
+ appendStringInfoChar(buf, ']');
+ }
+
+ appendStringInfoChar(buf, ')');
+}
+
+
+/*
+ * Deparse given array of something into buf.
+ */
+static void
+deparseArrayExpr(StringInfo buf,
+ ArrayExpr *node,
+ PlannerInfo *root)
+{
+ ListCell *lc;
+ bool first = true;
+
+ appendStringInfo(buf, "ARRAY[");
+ foreach(lc, node->elements)
+ {
+ if (!first)
+ appendStringInfo(buf, ", ");
+ deparseExpr(buf, lfirst(lc), root);
+
+ first = false;
+ }
+ appendStringInfoChar(buf, ']');
+
+ /* If the array is empty, we need explicit cast to the array type. */
+ if (node->elements == NIL)
+ {
+ char *typname;
+
+ typname = format_type_with_typemod(node->array_typeid, -1);
+ appendStringInfo(buf, "::%s", typname);
+ }
+}
+
+/*
+ * Returns true if given expr is safe to evaluate on the foreign server. If
+ * result is true, extra information has_param tells whether given expression
+ * contains any Param node. This is useful to determine whether the expression
+ * can be used in remote EXPLAIN.
+ */
+static bool
+is_foreign_expr(PlannerInfo *root,
+ RelOptInfo *baserel,
+ Expr *expr,
+ bool *has_param)
+{
+ foreign_executable_cxt context;
+ context.root = root;
+ context.foreignrel = baserel;
+ context.has_param = false;
+
+ /*
+ * An expression which includes any mutable function can't be pushed down
+ * because it's result is not stable. For example, pushing now() down to
+ * remote side would cause confusion from the clock offset.
+ * If we have routine mapping infrastructure in future release, we will be
+ * able to choose function to be pushed down in finer granularity.
+ */
+ if (contain_mutable_functions((Node *) expr))
+ return false;
+
+ /*
+ * Check that the expression consists of nodes which are known as safe to
+ * be pushed down.
+ */
+ if (foreign_expr_walker((Node *) expr, &context))
+ return false;
+
+ /*
+ * Tell caller whether the given expression contains any Param node, which
+ * can't be used in EXPLAIN statement before executor starts.
+ */
+ *has_param = context.has_param;
+
+ return true;
+}
+
+/*
+ * Return true if node includes any node which is not known as safe to be
+ * pushed down.
+ */
+static bool
+foreign_expr_walker(Node *node, foreign_executable_cxt *context)
+{
+ if (node == NULL)
+ return false;
+
+ /*
+ * Special case handling for List; expression_tree_walker handles List as
+ * well as other Expr nodes. For instance, List is used in RestrictInfo
+ * for args of FuncExpr node.
+ *
+ * Although the comments of expression_tree_walker mention that
+ * RangeTblRef, FromExpr, JoinExpr, and SetOperationStmt are handled as
+ * well, but we don't care them because they are not used in RestrictInfo.
+ * If one of them was passed into, default label catches it and give up
+ * traversing.
+ */
+ if (IsA(node, List))
+ {
+ ListCell *lc;
+
+ foreach(lc, (List *) node)
+ {
+ if (foreign_expr_walker(lfirst(lc), context))
+ return true;
+ }
+ return false;
+ }
+
+ /*
+ * If return type of given expression is not built-in, it can't be pushed
+ * down because it might has incompatible semantics on remote side.
+ */
+ if (!is_builtin(exprType(node)))
+ return true;
+
+ switch (nodeTag(node))
+ {
+ case T_Const:
+ /*
+ * Using anyarray and/or anyenum in remote query is not supported.
+ */
+ if (((Const *) node)->consttype == ANYARRAYOID ||
+ ((Const *) node)->consttype == ANYNONARRAYOID)
+ return true;
+ break;
+ case T_BoolExpr:
+ case T_NullTest:
+ case T_DistinctExpr:
+ case T_RelabelType:
+ /*
+ * 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 at the tail of this function.
+ */
+ break;
+ /*
+ * If function used by the expression is not built-in, it can't be
+ * pushed down because it might has incompatible semantics on remote
+ * side.
+ */
+ case T_FuncExpr:
+ {
+ FuncExpr *fe = (FuncExpr *) node;
+ if (!is_builtin(fe->funcid))
+ return true;
+ }
+ break;
+ case T_Param:
+ /*
+ * Only external parameters can be pushed down.:
+ */
+ {
+ if (((Param *) node)->paramkind != PARAM_EXTERN)
+ return true;
+
+ /* Mark that this expression contains Param node. */
+ context->has_param = true;
+ }
+ break;
+ case T_ScalarArrayOpExpr:
+ /*
+ * Only built-in operators can be pushed down. In addition,
+ * underlying function must be built-in and immutable, but we don't
+ * check volatility here; such check must be done already with
+ * contain_mutable_functions.
+ */
+ {
+ ScalarArrayOpExpr *oe = (ScalarArrayOpExpr *) node;
+
+ if (!is_builtin(oe->opno) || !is_builtin(oe->opfuncid))
+ return true;
+
+ /*
+ * If the operator takes collatable type as operands, we push
+ * down only "=" and "<>" which are not affected by collation.
+ * Other operators might be safe about collation, but these two
+ * seem enough to cover practical use cases.
+ */
+ if (exprInputCollation(node) != InvalidOid)
+ {
+ char *opname = get_opname(oe->opno);
+
+ if (strcmp(opname, "=") != 0 && strcmp(opname, "<>") != 0)
+ return true;
+ }
+
+ /* operands are checked later */
+ }
+ break;
+ case T_OpExpr:
+ /*
+ * Only built-in operators can be pushed down. In addition,
+ * underlying function must be built-in and immutable, but we don't
+ * check volatility here; such check must be done already with
+ * contain_mutable_functions.
+ */
+ {
+ OpExpr *oe = (OpExpr *) node;
+
+ if (!is_builtin(oe->opno) || !is_builtin(oe->opfuncid))
+ return true;
+
+ /*
+ * If the operator takes collatable type as operands, we push
+ * down only "=" and "<>" which are not affected by collation.
+ * Other operators might be safe about collation, but these two
+ * seem enough to cover practical use cases.
+ */
+ if (exprInputCollation(node) != InvalidOid)
+ {
+ char *opname = get_opname(oe->opno);
+
+ if (strcmp(opname, "=") != 0 && strcmp(opname, "<>") != 0)
+ return true;
+ }
+
+ /* operands are checked later */
+ }
+ 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;
+ foreign_executable_cxt *f_context;
+
+ f_context = (foreign_executable_cxt *) context;
+ if (var->varno != f_context->foreignrel->relid ||
+ var->varlevelsup != 0)
+ return true;
+ }
+ break;
+ case T_ArrayRef:
+ /*
+ * ArrayRef which holds non-built-in typed elements can't be pushed
+ * down.
+ */
+ {
+ ArrayRef *ar = (ArrayRef *) node;;
+
+ if (!is_builtin(ar->refelemtype))
+ return true;
+
+ /* Assignment should not be in restrictions. */
+ if (ar->refassgnexpr != NULL)
+ return true;
+ }
+ break;
+ case T_ArrayExpr:
+ /*
+ * ArrayExpr which holds non-built-in typed elements can't be pushed
+ * down.
+ */
+ {
+ if (!is_builtin(((ArrayExpr *) node)->element_typeid))
+ return true;
+ }
+ break;
+ default:
+ {
+ ereport(DEBUG3,
+ (errmsg("expression is too complex"),
+ errdetail("%s", nodeToString(node))));
+ return true;
+ }
+ break;
+ }
+
+ return expression_tree_walker(node, foreign_expr_walker, context);
+}
+
+/*
+ * Return true if given object is one of built-in objects.
+ */
+static bool
+is_builtin(Oid oid)
+{
+ return (oid < FirstNormalObjectId);
+}
+
+/*
+ * Deparse WHERE clause from given list of RestrictInfo and append them to buf.
+ * We assume that buf already holds a SQL statement which ends with valid WHERE
+ * clause.
+ *
+ * Only when calling the first time for a statement, is_first should be true.
+ */
+void
+appendWhereClause(StringInfo buf,
+ bool is_first,
+ List *exprs,
+ PlannerInfo *root)
+{
+ bool first = true;
+ ListCell *lc;
+
+ foreach(lc, exprs)
+ {
+ RestrictInfo *ri = (RestrictInfo *) lfirst(lc);
+
+ /* Connect expressions with "AND" and parenthesize whole condition. */
+ if (is_first && first)
+ appendStringInfo(buf, " WHERE ");
+ else
+ appendStringInfo(buf, " AND ");
+
+ appendStringInfoChar(buf, '(');
+ deparseExpr(buf, ri->clause, root);
+ appendStringInfoChar(buf, ')');
+
+ first = false;
+ }
+}
diff --git a/contrib/pgsql_fdw/expected/pgsql_fdw.out b/contrib/pgsql_fdw/expected/pgsql_fdw.out
index 1a20874..aea7b41 100644
--- a/contrib/pgsql_fdw/expected/pgsql_fdw.out
+++ b/contrib/pgsql_fdw/expected/pgsql_fdw.out
@@ -217,11 +217,11 @@ SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
-- with WHERE clause
EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 >= '1';
- QUERY PLAN
------------------------------------------------------------------------------
+ QUERY PLAN
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
- Filter: ((c7 >= '1'::bpchar) AND (c1 = 101) AND ((c6)::text = '1'::text))
- Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ Filter: (c7 >= '1'::bpchar)
+ Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.=) 101)) AND (((c6)::text OPERATOR(pg_catalog.=) '1'::text))
(3 rows)
SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 >= '1';
@@ -329,20 +329,91 @@ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 === t1.c2;
(3 rows)
EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = abs(t1.c2);
- QUERY PLAN
----------------------------------------------------------------------
+ QUERY PLAN
+-------------------------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
- Filter: (c1 = abs(c2))
- Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
-(3 rows)
+ Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.=) pg_catalog.abs(c2)))
+(2 rows)
EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = t1.c2;
- QUERY PLAN
----------------------------------------------------------------------
+ QUERY PLAN
+---------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
- Filter: (c1 = c2)
- Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
-(3 rows)
+ Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.=) c2))
+(2 rows)
+
+-- ===================================================================
+-- WHERE push down
+-- ===================================================================
+EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = 1; -- Var, OpExpr(b), Const
+ QUERY PLAN
+--------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.=) 1))
+(2 rows)
+
+EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = 100 AND t1.c2 = 0; -- BoolExpr
+ QUERY PLAN
+----------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.=) 100)) AND ((c2 OPERATOR(pg_catalog.=) 0))
+(2 rows)
+
+EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE c1 IS NULL; -- NullTest
+ QUERY PLAN
+---------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" IS NULL))
+(2 rows)
+
+EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE c1 IS NOT NULL; -- NullTest
+ QUERY PLAN
+-------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" IS NOT NULL))
+(2 rows)
+
+EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE round(abs(c1), 0) = 1; -- FuncExpr
+ QUERY PLAN
+------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE ((pg_catalog.round(pg_catalog.abs("C 1"), 0) OPERATOR(pg_catalog.=) 1::numeric))
+(2 rows)
+
+EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE c1 = -c1; -- OpExpr(l)
+ QUERY PLAN
+-------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.=) (OPERATOR(pg_catalog.-) "C 1")))
+(2 rows)
+
+EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE 1 = c1!; -- OpExpr(r)
+ QUERY PLAN
+------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE ((1::numeric OPERATOR(pg_catalog.=) ("C 1" OPERATOR(pg_catalog.!))))
+(2 rows)
+
+EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE (c1 IS NOT NULL) IS DISTINCT FROM (c1 IS NOT NULL); -- DistinctExpr
+ QUERY PLAN
+--------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" IS NOT NULL) IS DISTINCT FROM ("C 1" IS NOT NULL))
+(2 rows)
+
+EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE c1 = ANY(ARRAY[c2, 1, c1 + 0]); -- ScalarArrayOpExpr
+ QUERY PLAN
+-----------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 t1
+ Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.=) ANY (ARRAY[c2, 1, ("C 1" OPERATOR(pg_catalog.+) 0)])))
+(2 rows)
+
+EXPLAIN (COSTS false) SELECT * FROM ft1 ft WHERE c1 = (ARRAY[c1,c2,3])[1]; -- ArrayRef
+ QUERY PLAN
+---------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on ft1 ft
+ Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.=) ((ARRAY["C 1", c2, 3])[1])))
+(2 rows)
-- ===================================================================
-- parameterized queries
@@ -350,17 +421,14 @@ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = t1.c2;
-- simple join
PREPARE st1(int, int) AS SELECT t1.c3, t2.c3 FROM ft1 t1, ft2 t2 WHERE t1.c1 = $1 AND t2.c1 = $2;
EXPLAIN (COSTS false) EXECUTE st1(1, 2);
- QUERY PLAN
--------------------------------------------------------------------------------------------
+ QUERY PLAN
+------------------------------------------------------------------------------------------------------------------------------
Nested Loop
-> Foreign Scan on ft1 t1
- Filter: (c1 = 1)
- Remote SQL: SELECT "C 1", NULL, c3, NULL, NULL, NULL, NULL FROM "S 1"."T 1"
- -> Materialize
- -> Foreign Scan on ft2 t2
- Filter: (c1 = 2)
- Remote SQL: SELECT "C 1", NULL, c3, NULL, NULL, NULL, NULL FROM "S 1"."T 1"
-(8 rows)
+ Remote SQL: SELECT "C 1", NULL, c3, NULL, NULL, NULL, NULL FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.=) 1))
+ -> Foreign Scan on ft2 t2
+ Remote SQL: SELECT "C 1", NULL, c3, NULL, NULL, NULL, NULL FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.=) 2))
+(5 rows)
EXECUTE st1(1, 1);
c3 | c3
@@ -377,20 +445,19 @@ EXECUTE st1(101, 101);
-- subquery using stable function (can't be pushed down)
PREPARE st2(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c4) = 6) ORDER BY c1;
EXPLAIN (COSTS false) EXECUTE st2(10, 20);
- QUERY PLAN
-------------------------------------------------------------------------------------------------
+ QUERY PLAN
+-----------------------------------------------------------------------------------------------------------------------------------------
Sort
Sort Key: t1.c1
-> Nested Loop Semi Join
Join Filter: (t1.c3 = t2.c3)
-> Foreign Scan on ft1 t1
- Filter: (c1 < 20)
- Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.<) 20))
-> Materialize
-> Foreign Scan on ft2 t2
- Filter: ((c1 > 10) AND (date_part('dow'::text, c4) = 6::double precision))
- Remote SQL: SELECT "C 1", NULL, c3, c4, NULL, NULL, NULL FROM "S 1"."T 1"
-(11 rows)
+ Filter: (date_part('dow'::text, c4) = 6::double precision)
+ Remote SQL: SELECT "C 1", NULL, c3, c4, NULL, NULL, NULL FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.>) 10))
+(10 rows)
EXECUTE st2(10, 20);
c1 | c2 | c3 | c4 | c5 | c6 | c7
@@ -407,20 +474,18 @@ EXECUTE st1(101, 101);
-- subquery using immutable function (can be pushed down)
PREPARE st3(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c5) = 6) ORDER BY c1;
EXPLAIN (COSTS false) EXECUTE st3(10, 20);
- QUERY PLAN
-------------------------------------------------------------------------------------------------
+ QUERY PLAN
+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Sort
Sort Key: t1.c1
-> Nested Loop Semi Join
Join Filter: (t1.c3 = t2.c3)
-> Foreign Scan on ft1 t1
- Filter: (c1 < 20)
- Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
+ Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.<) 20))
-> Materialize
-> Foreign Scan on ft2 t2
- Filter: ((c1 > 10) AND (date_part('dow'::text, c5) = 6::double precision))
- Remote SQL: SELECT "C 1", NULL, c3, NULL, c5, NULL, NULL FROM "S 1"."T 1"
-(11 rows)
+ Remote SQL: SELECT "C 1", NULL, c3, NULL, c5, NULL, NULL FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.>) 10)) AND ((pg_catalog.date_part('dow'::text, c5) OPERATOR(pg_catalog.=) 6::double precision))
+(9 rows)
EXECUTE st3(10, 20);
c1 | c2 | c3 | c4 | c5 | c6 | c7
@@ -437,52 +502,46 @@ EXECUTE st3(20, 30);
-- custom plan should be chosen
PREPARE st4(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 = $1;
EXPLAIN (COSTS false) EXECUTE st4(1);
- QUERY PLAN
----------------------------------------------------------------------
+ QUERY PLAN
+--------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
- Filter: (c1 = 1)
- Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
-(3 rows)
+ Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.=) 1))
+(2 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
- QUERY PLAN
----------------------------------------------------------------------
+ QUERY PLAN
+--------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
- Filter: (c1 = 1)
- Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
-(3 rows)
+ Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.=) 1))
+(2 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
- QUERY PLAN
----------------------------------------------------------------------
+ QUERY PLAN
+--------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
- Filter: (c1 = 1)
- Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
-(3 rows)
+ Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.=) 1))
+(2 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
- QUERY PLAN
----------------------------------------------------------------------
+ QUERY PLAN
+--------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
- Filter: (c1 = 1)
- Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
-(3 rows)
+ Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.=) 1))
+(2 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
- QUERY PLAN
----------------------------------------------------------------------
+ QUERY PLAN
+--------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
- Filter: (c1 = 1)
- Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
-(3 rows)
+ Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.=) 1))
+(2 rows)
EXPLAIN (COSTS false) EXECUTE st4(1);
- QUERY PLAN
----------------------------------------------------------------------
+ QUERY PLAN
+---------------------------------------------------------------------------------------------------------------
Foreign Scan on ft1 t1
- Filter: (c1 = $1)
- Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1"
-(3 rows)
+ Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.=) $1))
+(2 rows)
-- cleanup
DEALLOCATE st1;
diff --git a/contrib/pgsql_fdw/pgsql_fdw.c b/contrib/pgsql_fdw/pgsql_fdw.c
index 1b7db4d..b7885a8 100644
--- a/contrib/pgsql_fdw/pgsql_fdw.c
+++ b/contrib/pgsql_fdw/pgsql_fdw.c
@@ -64,6 +64,9 @@ typedef struct PgsqlFdwPlanState {
StringInfoData sql;
Cost startup_cost;
Cost total_cost;
+ List *remote_conds;
+ List *param_conds;
+ List *local_conds;
/* Cached catalog information. */
ForeignTable *table;
@@ -252,6 +255,9 @@ pgsqlGetForeignRelSize(PlannerInfo *root,
int width;
Cost startup_cost;
Cost total_cost;
+ List *remote_conds = NIL;
+ List *param_conds = NIL;
+ List *local_conds = NIL;
Selectivity sel;
/*
@@ -268,23 +274,35 @@ pgsqlGetForeignRelSize(PlannerInfo *root,
user = GetUserMapping(GetOuterUserId(), server->serverid);
/*
- * Create plain SELECT statement with no WHERE clause for this scan in
- * order to obtain meaningful rows estimation by executing EXPLAIN on
- * remote server.
+ * Construct remote query which consists of SELECT, FROM, and WHERE
+ * clauses, but conditions contain any Param node are excluded because
+ * placeholder can't be used in EXPLAIN statement. Such conditions are
+ * appended later.
*/
+ sortConditions(root, baserel, &remote_conds, ¶m_conds, &local_conds);
deparseSimpleSql(sql, foreigntableid, root, baserel);
+ if (list_length(remote_conds) > 0)
+ appendWhereClause(sql, true, remote_conds, root);
conn = GetConnection(server, user, false);
get_remote_estimate(sql->data, conn, &rows, &width,
&startup_cost, &total_cost);
ReleaseConnection(conn);
+ if (list_length(param_conds) > 0)
+ appendWhereClause(sql, !(list_length(remote_conds) > 0), param_conds,
+ root);
/*
- * Estimate selectivity of local filtering by calling
- * clauselist_selectivity() against baserestrictinfo, and modify rows
- * estimate with it.
+ * Estimate selectivity of conditions which are not used in remote EXPLAIN
+ * by calling clauselist_selectivity(). The best we can do for
+ * parameterized condition is to estimate selectivity on the basis of local
+ * statistics. When we actually obtain result rows, such conditions are
+ * deparsed into remote query and reduce rows transferred.
*/
- sel = clauselist_selectivity(root, baserel->baserestrictinfo,
- baserel->relid, JOIN_INNER, NULL);
+ sel = 1.0;
+ sel *= clauselist_selectivity(root, param_conds,
+ baserel->relid, JOIN_INNER, NULL);
+ sel *= clauselist_selectivity(root, local_conds,
+ baserel->relid, JOIN_INNER, NULL);
baserel->rows = rows * sel;
baserel->width = width;
@@ -294,6 +312,9 @@ pgsqlGetForeignRelSize(PlannerInfo *root,
*/
fpstate->startup_cost = startup_cost;
fpstate->total_cost = total_cost;
+ fpstate->remote_conds = remote_conds;
+ fpstate->param_conds = param_conds;
+ fpstate->local_conds = local_conds;
fpstate->table = table;
fpstate->server = server;
baserel->fdw_private = (void *) fpstate;
@@ -366,15 +387,22 @@ pgsqlGetForeignPlan(PlannerInfo *root,
PgsqlFdwPlanState *fpstate = (PgsqlFdwPlanState *) baserel->fdw_private;
Index scan_relid = baserel->relid;
List *fdw_private = NIL;
+ List *fdw_exprs = NIL;
+ List *local_exprs = NIL;
+ ListCell *lc;
/*
- * We have no native ability to evaluate restriction clauses, so we just
- * put all the scan_clauses into the plan node's qual list for the
- * executor to check. So all we have to do here is strip RestrictInfo
- * nodes from the clauses and ignore pseudoconstants (which will be
- * handled elsewhere).
+ * We need lists of Expr other than the lists of RestrictInfo. Now we can
+ * merge remote_conds and param_conds into fdw_exprs, because they are
+ * evaluated on remote side for actual remote query.
*/
- scan_clauses = extract_actual_clauses(scan_clauses, false);
+ foreach(lc, fpstate->remote_conds)
+ fdw_exprs = lappend(fdw_exprs, ((RestrictInfo *) lfirst(lc))->clause);
+ foreach(lc, fpstate->param_conds)
+ fdw_exprs = lappend(fdw_exprs, ((RestrictInfo *) lfirst(lc))->clause);
+ foreach(lc, fpstate->local_conds)
+ local_exprs = lappend(local_exprs,
+ ((RestrictInfo *) lfirst(lc))->clause);
/*
* Make a list contains SELECT statement to it to executor with plan node
@@ -382,11 +410,20 @@ pgsqlGetForeignPlan(PlannerInfo *root,
*/
fdw_private = lappend(fdw_private, makeString(fpstate->sql.data));
- /* Create the ForeignScan node with fdw_private of selected path. */
+ /*
+ * Create the ForeignScan node from target list, local filtering
+ * expressions, remote filtering expressions, and FDW private information.
+ *
+ * We remove expressions which are evaluated on remote side from qual of
+ * the scan node to avoid redundant filtering. Such filter reduction
+ * can be done only here, done after choosing best path, because
+ * baserestrictinfo in RelOptInfo is shared by all possible paths until
+ * best path is chosen.
+ */
return make_foreignscan(tlist,
- scan_clauses,
+ local_exprs,
scan_relid,
- NIL,
+ fdw_exprs,
fdw_private);
}
diff --git a/contrib/pgsql_fdw/pgsql_fdw.h b/contrib/pgsql_fdw/pgsql_fdw.h
index 4e7a8e1..76c9f72 100644
--- a/contrib/pgsql_fdw/pgsql_fdw.h
+++ b/contrib/pgsql_fdw/pgsql_fdw.h
@@ -29,5 +29,14 @@ void deparseSimpleSql(StringInfo buf,
Oid relid,
PlannerInfo *root,
RelOptInfo *baserel);
+void appendWhereClause(StringInfo buf,
+ bool has_where,
+ List *exprs,
+ PlannerInfo *root);
+void sortConditions(PlannerInfo *root,
+ RelOptInfo *baserel,
+ List **remote_conds,
+ List **param_conds,
+ List **local_conds);
#endif /* PGSQL_FDW_H */
diff --git a/contrib/pgsql_fdw/sql/pgsql_fdw.sql b/contrib/pgsql_fdw/sql/pgsql_fdw.sql
index da04568..003e168 100644
--- a/contrib/pgsql_fdw/sql/pgsql_fdw.sql
+++ b/contrib/pgsql_fdw/sql/pgsql_fdw.sql
@@ -178,6 +178,20 @@ EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = abs(t1.c2);
EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = t1.c2;
-- ===================================================================
+-- WHERE push down
+-- ===================================================================
+EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = 1; -- Var, OpExpr(b), Const
+EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = 100 AND t1.c2 = 0; -- BoolExpr
+EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE c1 IS NULL; -- NullTest
+EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE c1 IS NOT NULL; -- NullTest
+EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE round(abs(c1), 0) = 1; -- FuncExpr
+EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE c1 = -c1; -- OpExpr(l)
+EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE 1 = c1!; -- OpExpr(r)
+EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE (c1 IS NOT NULL) IS DISTINCT FROM (c1 IS NOT NULL); -- DistinctExpr
+EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE c1 = ANY(ARRAY[c2, 1, c1 + 0]); -- ScalarArrayOpExpr
+EXPLAIN (COSTS false) SELECT * FROM ft1 ft WHERE c1 = (ARRAY[c1,c2,3])[1]; -- ArrayRef
+
+-- ===================================================================
-- parameterized queries
-- ===================================================================
-- simple join
pgsql_fdw_analyze_v2.patchtext/plain; charset=Shift_JIS; name=pgsql_fdw_analyze_v2.patchDownload
diff --git a/contrib/pgsql_fdw/deparse.c b/contrib/pgsql_fdw/deparse.c
new file mode 100644
index 148d0b4..9737a0c
*** a/contrib/pgsql_fdw/deparse.c
--- b/contrib/pgsql_fdw/deparse.c
***************
*** 28,33 ****
--- 28,34 ----
#include "parser/parsetree.h"
#include "utils/builtins.h"
#include "utils/lsyscache.h"
+ #include "utils/rel.h"
#include "utils/syscache.h"
#include "pgsql_fdw.h"
*************** sortConditions(PlannerInfo *root,
*** 218,223 ****
--- 219,289 ----
}
/*
+ * Deparse SELECT statement to acquire sample rows of given relation into buf.
+ */
+ void
+ deparseAnalyzeSql(StringInfo buf, Relation rel)
+ {
+ Oid relid = RelationGetRelid(rel);
+ TupleDesc tupdesc = RelationGetDescr(rel);
+ int i;
+ char *colname;
+ List *options;
+ ListCell *lc;
+ bool first = true;
+ char *nspname;
+ char *relname;
+ ForeignTable *table;
+
+ /* Deparse SELECT clause, use attribute name or colname option. */
+ appendStringInfo(buf, "SELECT ");
+ for (i = 0; i < tupdesc->natts; i++)
+ {
+ if (tupdesc->attrs[i]->attisdropped)
+ continue;
+
+ colname = NameStr(tupdesc->attrs[i]->attname);
+ options = GetForeignColumnOptions(relid, tupdesc->attrs[i]->attnum);
+
+ foreach(lc, options)
+ {
+ DefElem *def= (DefElem *) lfirst(lc);
+
+ if (strcmp(def->defname, "colname") == 0)
+ {
+ colname = defGetString(def);
+ break;
+ }
+ }
+
+ if (!first)
+ appendStringInfo(buf, ", ");
+ appendStringInfo(buf, "%s", quote_identifier(colname));
+ first = false;
+ }
+
+ /*
+ * Deparse FROM clause, use namespace and relation name, or use nspname and
+ * colname options respectively.
+ */
+ nspname = get_namespace_name(get_rel_namespace(relid));
+ relname = get_rel_name(relid);
+ table = GetForeignTable(relid);
+ foreach(lc, table->options)
+ {
+ DefElem *def= (DefElem *) lfirst(lc);
+
+ if (strcmp(def->defname, "nspname") == 0)
+ nspname = defGetString(def);
+ else if (strcmp(def->defname, "relname") == 0)
+ relname = defGetString(def);
+ }
+
+ appendStringInfo(buf, " FROM %s.%s", quote_identifier(nspname),
+ quote_identifier(relname));
+ }
+
+ /*
* Deparse given expression into buf. Actual string operation is delegated to
* node-type-specific functions.
*
diff --git a/contrib/pgsql_fdw/pgsql_fdw.c b/contrib/pgsql_fdw/pgsql_fdw.c
new file mode 100644
index b7885a8..63b2bba
*** a/contrib/pgsql_fdw/pgsql_fdw.c
--- b/contrib/pgsql_fdw/pgsql_fdw.c
***************
*** 17,22 ****
--- 17,23 ----
#include "catalog/pg_foreign_table.h"
#include "commands/defrem.h"
#include "commands/explain.h"
+ #include "commands/vacuum.h"
#include "foreign/fdwapi.h"
#include "funcapi.h"
#include "miscadmin.h"
*************** typedef struct PgsqlFdwExecutionState
*** 133,138 ****
--- 134,168 ----
} PgsqlFdwExecutionState;
/*
+ * Describes a state of analyze request for a foreign table.
+ */
+ typedef struct PgsqlAnalyzeState
+ {
+ /* for tuple generation. */
+ TupleDesc tupdesc;
+ AttInMetadata *attinmeta;
+ char *colbuf; /* column value buffer for row processor */
+ int colbuflen; /* column value buffer size for row processor */
+ Datum *values;
+ bool *nulls;
+
+ /* for random sampling */
+ HeapTuple *rows; /* result buffer */
+ int targrows; /* target # of sample rows */
+ int numrows; /* # of samples collected */
+ double samplerows; /* # of rows fetched */
+ double rowstoskip; /* # of rows skipped before next sample */
+ double rstate; /* random state */
+
+ /* for storing result tuples */
+ MemoryContext anl_cxt; /* context for per-analyze lifespan data */
+ MemoryContext temp_cxt; /* context for per-tuple temporary data */
+
+ /* for error handling. */
+ ErrorPos errpos;
+ } PgsqlAnalyzeState;
+
+ /*
* SQL functions
*/
extern Datum pgsql_fdw_handler(PG_FUNCTION_ARGS);
*************** static void pgsqlBeginForeignScan(Foreig
*** 158,163 ****
--- 188,196 ----
static TupleTableSlot *pgsqlIterateForeignScan(ForeignScanState *node);
static void pgsqlReScanForeignScan(ForeignScanState *node);
static void pgsqlEndForeignScan(ForeignScanState *node);
+ static bool pgsqlAnalyzeForeignTable(Relation relation,
+ AcquireSampleRowsFunc *func,
+ BlockNumber *totalpages);
/*
* Helper functions
*************** static void execute_query(ForeignScanSta
*** 174,179 ****
--- 207,218 ----
static int query_row_processor(PGresult *res, const PGdataValue *columns,
const char **errmsgp, void *param);
static void pgsql_fdw_error_callback(void *arg);
+ static int pgsqlAcquireSampleRowsFunc(Relation relation, int elevel,
+ HeapTuple *rows, int targrows,
+ double *totalrows,
+ double *totaldeadrows);
+ static int analyze_row_processor(PGresult *res, const PGdataValue *columns,
+ const char **errmsgp, void *param);
/* Exported functions, but not written in pgsql_fdw.h. */
void _PG_init(void);
*************** static FdwRoutine fdwroutine = {
*** 215,220 ****
--- 254,260 ----
pgsqlEndForeignScan,
/* Optional handler functions. */
+ pgsqlAnalyzeForeignTable,
};
/*
*************** query_row_processor(PGresult *res,
*** 908,918 ****
--- 948,960 ----
festate->nulls[i] = false;
+ MemoryContextSwitchTo(festate->scan_cxt);
while (festate->colbuflen < len + 1)
{
festate->colbuflen *= 2;
festate->colbuf = repalloc(festate->colbuf, festate->colbuflen);
}
+ MemoryContextSwitchTo(festate->temp_cxt);
memcpy(festate->colbuf, columns[j].value, len);
festate->colbuf[columns[j].len] = '\0';
*************** query_row_processor(PGresult *res,
*** 960,966 ****
static void
pgsql_fdw_error_callback(void *arg)
{
! ErrorPos *errpos = (ErrorPos *) arg;
const char *relname;
const char *colname;
--- 1002,1008 ----
static void
pgsql_fdw_error_callback(void *arg)
{
! ErrorPos *errpos = (ErrorPos *) arg;
const char *relname;
const char *colname;
*************** pgsql_fdw_error_callback(void *arg)
*** 969,971 ****
--- 1011,1311 ----
errcontext("column %s of foreign table %s",
quote_identifier(colname), quote_identifier(relname));
}
+
+ /*
+ * pgsqlAnalyzeForeignTable
+ * Test whether analyzing this foreign table is supported
+ */
+ static bool
+ pgsqlAnalyzeForeignTable(Relation relation,
+ AcquireSampleRowsFunc *func,
+ BlockNumber *totalpages)
+ {
+ *totalpages = 0;
+ *func = pgsqlAcquireSampleRowsFunc;
+
+ return true;
+ }
+
+ /*
+ * Acquire a random sample of rows from foreign table managed by pgsql_fdw.
+ *
+ * pgsql_fdw doesn't provide direct access to remote buffer, so we execute
+ * simple SELECT statement which retrieves whole rows from remote side, and
+ * pick some samples from them.
+ */
+ static int
+ pgsqlAcquireSampleRowsFunc(Relation relation, int elevel,
+ HeapTuple *rows, int targrows,
+ double *totalrows,
+ double *totaldeadrows)
+ {
+ PgsqlAnalyzeState astate;
+ StringInfoData sql;
+ ForeignTable *table;
+ ForeignServer *server;
+ UserMapping *user;
+ PGconn *conn = NULL;
+ PGresult *res = NULL;
+
+ /*
+ * Only few information are necessary as input to row processor. Other
+ * initialization will be done at the first row processor call.
+ */
+ astate.anl_cxt = CurrentMemoryContext;
+ astate.temp_cxt = AllocSetContextCreate(CurrentMemoryContext,
+ "pgsql_fdw analyze temporary data",
+ ALLOCSET_DEFAULT_MINSIZE,
+ ALLOCSET_DEFAULT_INITSIZE,
+ ALLOCSET_DEFAULT_MAXSIZE);
+ astate.rows = rows;
+ astate.targrows = targrows;
+ astate.tupdesc = relation->rd_att;
+ astate.errpos.relid = relation->rd_id;
+
+ /*
+ * Construct SELECT statement which retrieves whole rows from remote. We
+ * can't avoid running sequential scan on remote side to get practical
+ * statistics, so this seems reasonable compromise.
+ */
+ initStringInfo(&sql);
+ deparseAnalyzeSql(&sql, relation);
+
+ table = GetForeignTable(relation->rd_id);
+ server = GetForeignServer(table->serverid);
+ user = GetUserMapping(GetOuterUserId(), server->serverid);
+ conn = GetConnection(server, user, true);
+
+ /*
+ * Acquire sample rows from the result set.
+ */
+ PG_TRY();
+ {
+ /*
+ * Execute remote query and retrieve results with libpq row processor.
+ */
+ PQsetRowProcessor(conn, analyze_row_processor, &astate);
+ res = PQexec(conn, sql.data);
+ PQsetRowProcessor(conn, NULL, NULL);
+ if (PQresultStatus(res) != PGRES_TUPLES_OK)
+ ereport(ERROR,
+ (errmsg("could not execute remote query for analyze"),
+ errdetail("%s", PQerrorMessage(conn)),
+ errhint("%s", sql.data)));
+ PQclear(res);
+ res = NULL;
+ }
+ PG_CATCH();
+ {
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+
+ ReleaseConnection(conn);
+
+ /* We assume that we have no dead tuple. */
+ *totaldeadrows = 0.0;
+
+ /* We've retrieved all living tuples from foreign server. */
+ *totalrows = astate.samplerows;
+
+ /*
+ * We don't update pg_class.relpages because we don't care that in
+ * planning at all.
+ */
+
+ /*
+ * Emit some interesting relation info
+ */
+ ereport(elevel,
+ (errmsg("\"%s\": scanned with \"%s\", "
+ "containing %.0f live rows and %.0f dead rows; "
+ "%d rows in sample, %.0f estimated total rows",
+ RelationGetRelationName(relation), sql.data,
+ astate.samplerows, 0.0,
+ astate.numrows, astate.samplerows)));
+
+ return astate.numrows;
+ }
+
+ /*
+ * Custom row processor for acquire_sample_rows.
+ *
+ * Collect sample rows from the result of query.
+ * - Use all tuples as sample until target rows samples are collected.
+ * - Once reached the target, skip some tuples and replace already sampled
+ * tuple randomly.
+ */
+ static int
+ analyze_row_processor(PGresult *res, const PGdataValue *columns,
+ const char **errmsgp, void *param)
+ {
+ PgsqlAnalyzeState *astate = (PgsqlAnalyzeState *) param;
+ int targrows = astate->targrows;
+ TupleDesc tupdesc = astate->tupdesc;
+ AttInMetadata *attinmeta = astate->attinmeta;
+ int i;
+ int j;
+ int pos; /* position where next sample should be stored. */
+ HeapTuple tuple;
+ ErrorContextCallback errcontext;
+ MemoryContext callercontext;
+
+ if (columns == NULL)
+ {
+ /* Prepare for sampling rows */
+ astate->attinmeta = TupleDescGetAttInMetadata(tupdesc);
+ astate->colbuflen = INITIAL_COLBUF_SIZE;
+ astate->colbuf = palloc(astate->colbuflen);
+ astate->values = (Datum *) palloc(sizeof(Datum) * tupdesc->natts);
+ astate->nulls = (bool *) palloc(sizeof(bool) * tupdesc->natts);
+ astate->numrows = 0;
+ astate->samplerows = 0;
+ astate->rowstoskip = -1;
+ astate->numrows = 0;
+ astate->rstate = anl_init_selection_state(astate->targrows);
+
+ return 1;
+ }
+
+ /*
+ * ANALYZE against foreign tables are not done in processes of
+ * vacuum, so here we use CHECK_FOR_INTERRUPTS instead of
+ * vacuum_delay_point().
+ */
+ CHECK_FOR_INTERRUPTS();
+
+ /*
+ * Do the following work in a temp context that we reset after each tuple.
+ * This cleans up not only the data we have direct access to, but any
+ * cruft the I/O functions might leak.
+ */
+ callercontext = MemoryContextSwitchTo(astate->temp_cxt);
+
+ /*
+ * First targrows rows are once sampled always. If we have more source
+ * rows, pick up some of them by skipping and replace already sampled
+ * tuple randomly.
+ *
+ * Here we just determine the slot where next sample should be stored. Set
+ * pos to negative value to indicates the row should be skipped.
+ */
+ if (astate->numrows < targrows)
+ pos = astate->numrows++;
+ else
+ {
+ /*
+ * The first targrows sample rows are simply copied into
+ * the reservoir. Then we start replacing tuples in the
+ * sample until we reach the end of the relation. This
+ * algorithm is from Jeff Vitter's paper, similarly to
+ * acquire_sample_rows in analyze.c.
+ *
+ * We don't have block-wise accessibility, so every row in
+ * the PGresult is possible to be sample.
+ */
+ if (astate->rowstoskip < 0)
+ astate->rowstoskip = anl_get_next_S(astate->samplerows, targrows,
+ &astate->rstate);
+
+ if (astate->rowstoskip <= 0)
+ {
+ int k = (int) (targrows * anl_random_fract());
+
+ Assert(k >= 0 && k < targrows);
+
+ /*
+ * Create sample tuple from the result, and replace at
+ * random.
+ */
+ heap_freetuple(astate->rows[k]);
+ pos = k;
+ }
+ else
+ pos = -1;
+
+ astate->rowstoskip -= 1;
+ }
+
+ /* Always increment sample row counter. */
+ astate->samplerows += 1;
+
+ if (pos >= 0)
+ {
+ /*
+ * Create sample tuple from current result row, and store it into the
+ * position determined above. Note that i and j point entries in
+ * catalog and columns array respectively.
+ */
+ for (i = 0, j = 0; i < tupdesc->natts; i++)
+ {
+ int len = columns[j].len;
+
+ if (tupdesc->attrs[i]->attisdropped)
+ continue;
+
+ if (len < 0)
+ astate->nulls[i] = true;
+ else
+ {
+ Datum value;
+
+ astate->nulls[i] = false;
+
+ /*
+ * Expand column value string buffer twice as current until
+ * it can hold the value. We need to allocate the buffer in
+ * the context of analyze.
+ */
+ while (astate->colbuflen < len + 1)
+ {
+ astate->colbuflen *= 2;
+ MemoryContextSwitchTo(astate->anl_cxt);
+ astate->colbuf = repalloc(astate->colbuf,
+ astate->colbuflen);
+ MemoryContextSwitchTo(astate->temp_cxt);
+ }
+ memcpy(astate->colbuf, columns[j].value, len);
+ astate->colbuf[columns[j].len] = '\0';
+
+ /*
+ * Set up and install callback to report where conversion error
+ * occurs.
+ */
+ astate->errpos.cur_attno = i + 1;
+ errcontext.callback = pgsql_fdw_error_callback;
+ errcontext.arg = (void *) &astate->errpos;
+ errcontext.previous = error_context_stack;
+ error_context_stack = &errcontext;
+
+ value = InputFunctionCall(&attinmeta->attinfuncs[i],
+ astate->colbuf,
+ attinmeta->attioparams[i],
+ attinmeta->atttypmods[i]);
+ astate->values[i] = value;
+
+ /* Uninstall error callback function. */
+ error_context_stack = errcontext.previous;
+ }
+ j++;
+ }
+
+ /*
+ * Generate tuple from the result row data, and store it into the give
+ * buffer. Note that we need to allocate the tuple in the analyze
+ * context to make it valid even after temporary per-tuple context has
+ * been reset.
+ */
+ MemoryContextSwitchTo(astate->anl_cxt);
+ tuple = heap_form_tuple(tupdesc, astate->values, astate->nulls);
+ MemoryContextSwitchTo(astate->temp_cxt);
+ astate->rows[pos] = tuple;
+ }
+
+ /* Clean up */
+ MemoryContextSwitchTo(callercontext);
+ MemoryContextReset(astate->temp_cxt);
+
+ return 1;
+ }
diff --git a/contrib/pgsql_fdw/pgsql_fdw.h b/contrib/pgsql_fdw/pgsql_fdw.h
new file mode 100644
index 76c9f72..94e6f07
*** a/contrib/pgsql_fdw/pgsql_fdw.h
--- b/contrib/pgsql_fdw/pgsql_fdw.h
***************
*** 17,22 ****
--- 17,23 ----
#include "postgres.h"
#include "foreign/foreign.h"
#include "nodes/relation.h"
+ #include "utils/relcache.h"
/* in option.c */
int ExtractConnectionOptions(List *defelems,
*************** void sortConditions(PlannerInfo *root,
*** 38,42 ****
--- 39,44 ----
List **remote_conds,
List **param_conds,
List **local_conds);
+ void deparseAnalyzeSql(StringInfo buf, Relation rel);
#endif /* PGSQL_FDW_H */
2012/4/7 Shigeru HANADA <shigeru.hanada@gmail.com>:
I've updated pgsql_fdw so that it can collect statistics from foreign
data with new FDW API.
I notice that if you restart the remote server, the connection is
broken, but the client doesn't notice this until it goes to fire off
another command. Should there be an option to automatically
re-establish the connection upon noticing the connection has dropped,
and issue a NOTICE that it had done so?
Also I'm not particularly keen on the message provided to the user in
this event:
ERROR: could not execute EXPLAIN for cost estimation
DETAIL: FATAL: terminating connection due to administrator command
FATAL: terminating connection due to administrator command
There's no explanation what the "administrator" command was, and I
suspect this is really just a "I don't know what's happened here"
condition. I don't think we should reach that point.
--
Thom
(2012/04/08 5:19), Thom Brown wrote:
2012/4/7 Shigeru HANADA<shigeru.hanada@gmail.com>:
I've updated pgsql_fdw so that it can collect statistics from foreign
data with new FDW API.I notice that if you restart the remote server, the connection is
broken, but the client doesn't notice this until it goes to fire off
another command. Should there be an option to automatically
re-establish the connection upon noticing the connection has dropped,
and issue a NOTICE that it had done so?
Hm, I'd prefer reporting the connection failure and aborting the local
transaction, because reconnecting to the server would break consistency
between the results come from multiple foreign tables. Server shutdown
(or other troubles e.g. network failure) might happen at various timing
in the sequence of remote query (or sampling in ANALYZE). For example,
when we execute a local query which contains two foreign tables, foo and
bar, then the sequence of libpq activity would be like this.
1) connect to the server at the beginning of the local query
2) execute EXPLAIN for foreign table foo
3) execute EXPLAIN for foreign table bar
4) execute actual query for foreign table foo
5) execute actual query for foreign table bar
6) disconnect from the server at the end of the local query
If the connection has broken between 4) and 5), and immediate reconnect
succeeded, retrieved results for foo and bar might be inconsistent from
the viewpoint of transaction isolation.
In current implementation, next local query which contains foreign table
of failed foreign table tries to reconnect to the server.
Also I'm not particularly keen on the message provided to the user in
this event:ERROR: could not execute EXPLAIN for cost estimation
DETAIL: FATAL: terminating connection due to administrator command
FATAL: terminating connection due to administrator commandThere's no explanation what the "administrator" command was, and I
suspect this is really just a "I don't know what's happened here"
condition. I don't think we should reach that point.
That FATAL message is returned by remote backend's ProcessInterrupts()
during some administrator commands, such as immediate shutdown or
pg_terminate_backend(). If remote backend died of fast shutdown or
SIGKILL, no error message is available (see the sample below).
postgres=# select * From pgsql_branches ;
ERROR: could not execute EXPLAIN for cost estimation
DETAIL:
HINT: SELECT bid, bbalance, filler FROM public.pgbench_branches
I agree that the message is confusing. How about showing message like
"pgsql_fdw connection failure on <servername>" or something with remote
error message for such cases? It can be achieved by adding extra check
for connection status right after PQexec()/PQexecParams(). Although
some word polishing would be required :)
postgres=# select * from pgsql_branches ;
ERROR: pgsql_fdw connection failure on subaru_pgbench
DETAIL: FATAL: terminating connection due to administrator command
FATAL: terminating connection due to administrator command
This seems to impress users that remote side has some trouble.
Regards,
--
Shigeru HANADA
This is Gerald Devotta, Recruitment Specialist with Newt Global LLC, a
global staffing solutions provider that has been serving the
telecommunications and utility industries.
I am contacting you to let you know that your resume came to my attention
while I was conducting a job search for a project in Bloomington Illinois. I
have reviewed your resume with particular interest and am excited to let you
know that many of your qualifications match my client's needs
PostgreSQL database developer
Bloomington Illinois
2 +Years Contract
Attractive Compensation + Expenses paid
Required:
Solid experience with PgSQL programming on PostgreSQL ORBMS
Person can work 4 days at site and one day from home
If you are interested kindly send your update resume with expected
rate/salary
Regards,
Gerald Devotta
Recruitment Analyst
Newt Global Consulting LLC
Phone: 202.470.2492
Email: gdevotta@newtglobal.com <mailto:hr5@newtglobal.com> |
www.newtglobal.com
1300 W Walnut Hill Lane, Suite #230, Irving, TX 75038
From: Shigeru Hanada-2 [via PostgreSQL]
[mailto:ml-node+s1045698n5626807h34@n5.nabble.com]
Sent: Monday, April 09, 2012 12:39 AM
To: Gerald Devotta
Subject: Re: pgsql_fdw, FDW for PostgreSQL server
(2012/04/08 5:19), Thom Brown wrote:
2012/4/7 Shigeru HANADA<[hidden email]>:
I've updated pgsql_fdw so that it can collect statistics from foreign
data with new FDW API.I notice that if you restart the remote server, the connection is
broken, but the client doesn't notice this until it goes to fire off
another command. Should there be an option to automatically
re-establish the connection upon noticing the connection has dropped,
and issue a NOTICE that it had done so?
Hm, I'd prefer reporting the connection failure and aborting the local
transaction, because reconnecting to the server would break consistency
between the results come from multiple foreign tables. Server shutdown
(or other troubles e.g. network failure) might happen at various timing
in the sequence of remote query (or sampling in ANALYZE). For example,
when we execute a local query which contains two foreign tables, foo and
bar, then the sequence of libpq activity would be like this.
1) connect to the server at the beginning of the local query
2) execute EXPLAIN for foreign table foo
3) execute EXPLAIN for foreign table bar
4) execute actual query for foreign table foo
5) execute actual query for foreign table bar
6) disconnect from the server at the end of the local query
If the connection has broken between 4) and 5), and immediate reconnect
succeeded, retrieved results for foo and bar might be inconsistent from
the viewpoint of transaction isolation.
In current implementation, next local query which contains foreign table
of failed foreign table tries to reconnect to the server.
Also I'm not particularly keen on the message provided to the user in
this event:ERROR: could not execute EXPLAIN for cost estimation
DETAIL: FATAL: terminating connection due to administrator command
FATAL: terminating connection due to administrator commandThere's no explanation what the "administrator" command was, and I
suspect this is really just a "I don't know what's happened here"
condition. I don't think we should reach that point.
That FATAL message is returned by remote backend's ProcessInterrupts()
during some administrator commands, such as immediate shutdown or
pg_terminate_backend(). If remote backend died of fast shutdown or
SIGKILL, no error message is available (see the sample below).
postgres=# select * From pgsql_branches ;
ERROR: could not execute EXPLAIN for cost estimation
DETAIL:
HINT: SELECT bid, bbalance, filler FROM public.pgbench_branches
I agree that the message is confusing. How about showing message like
"pgsql_fdw connection failure on <servername>" or something with remote
error message for such cases? It can be achieved by adding extra check
for connection status right after PQexec()/PQexecParams(). Although
some word polishing would be required :)
postgres=# select * from pgsql_branches ;
ERROR: pgsql_fdw connection failure on subaru_pgbench
DETAIL: FATAL: terminating connection due to administrator command
FATAL: terminating connection due to administrator command
This seems to impress users that remote side has some trouble.
Regards,
--
Shigeru HANADA
--
Sent via pgsql-hackers mailing list ([hidden email])
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
_____
If you reply to this email, your message will be added to the discussion
below:
http://postgresql.1045698.n5.nabble.com/pgsql-fdw-FDW-for-PostgreSQL-server-
tp4935560p5626807.html
To unsubscribe from PostgreSQL, click
<http://postgresql.1045698.n5.nabble.com/template/NamlServlet.jtp?macro=unsu
bscribe_by_code&node=1843779&code=Z2Rldm90dGFAbmV3dGdsb2JhbC5jb218MTg0Mzc3OX
wtNTE4Mjg3MzQy> here.
<http://postgresql.1045698.n5.nabble.com/template/NamlServlet.jtp?macro=macr
o_viewer&id=instant_html%21nabble%3Aemail.naml&base=nabble.naml.namespaces.B
asicNamespace-nabble.view.web.template.NabbleNamespace-nabble.view.web.templ
ate.NodeNamespace&breadcrumbs=notify_subscribers%21nabble%3Aemail.naml-insta
nt_emails%21nabble%3Aemail.naml-send_instant_email%21nabble%3Aemail.naml>
NAML
-----
Regards,
Gerald Devotta
Recruitment Analyst
Newt Global Consulting LLC
Phone: 202.470.2492
Email: gdevotta@newtglobal.com | www.newtglobal.com
1300 W Walnut Hill Lane, Suite #230, Irving, TX 75038
--
View this message in context: http://postgresql.1045698.n5.nabble.com/pgsql-fdw-FDW-for-PostgreSQL-server-tp4935560p5626813.html
Sent from the PostgreSQL - hackers mailing list archive at Nabble.com.
2012/4/9 Shigeru HANADA <shigeru.hanada@gmail.com>:
1) connect to the server at the beginning of the local query
2) execute EXPLAIN for foreign table foo
3) execute EXPLAIN for foreign table bar
4) execute actual query for foreign table foo
5) execute actual query for foreign table bar
6) disconnect from the server at the end of the local queryIf the connection has broken between 4) and 5), and immediate reconnect
succeeded, retrieved results for foo and bar might be inconsistent from
the viewpoint of transaction isolation.In current implementation, next local query which contains foreign table
of failed foreign table tries to reconnect to the server.
How would this apply to the scenario where you haven't even begun a
transaction yet? There's no risk of inconsistency if the connection
is lost before the first command can execute, so why fail in such a
case? Isn't there a line in the sand we can draw where we say that if
we have passed it, we just die, otherwise we try to reconnect as
there's no risk of undesirable results?
Also I'm not particularly keen on the message provided to the user in
this event:ERROR: could not execute EXPLAIN for cost estimation
DETAIL: FATAL: terminating connection due to administrator command
FATAL: terminating connection due to administrator commandThere's no explanation what the "administrator" command was, and I
suspect this is really just a "I don't know what's happened here"
condition. I don't think we should reach that point.That FATAL message is returned by remote backend's ProcessInterrupts()
during some administrator commands, such as immediate shutdown or
pg_terminate_backend(). If remote backend died of fast shutdown or
SIGKILL, no error message is available (see the sample below).postgres=# select * From pgsql_branches ;
ERROR: could not execute EXPLAIN for cost estimation
DETAIL:
HINT: SELECT bid, bbalance, filler FROM public.pgbench_branchesI agree that the message is confusing. How about showing message like
"pgsql_fdw connection failure on <servername>" or something with remote
error message for such cases? It can be achieved by adding extra check
for connection status right after PQexec()/PQexecParams(). Although
some word polishing would be required :)postgres=# select * from pgsql_branches ;
ERROR: pgsql_fdw connection failure on subaru_pgbench
DETAIL: FATAL: terminating connection due to administrator command
FATAL: terminating connection due to administrator command
Yes, that would be an improvement.
--
Thom
Hi all,
(2012/03/07 3:39), Tom Lane wrote:
A bigger issue with postgresql_fdw_validator is that it supposes that
the core backend is authoritative as to what options libpq supports,
which is bad design on its face. It would be much more sensible for
dblink to be asking libpq what options libpq supports, say via
PQconndefaults().We might find that we have to leave postgresql_fdw_validator as-is
for backwards compatibility reasons (in particular, being able to load
existing FDW definitions) but I think we should migrate away from using
it.
In the discussion about pgsql_fdw which was proposed for 9.2, some
issues about postgresql_fdw_validator are pointed out.
* The name "postgresql_fdw_validator" conflicts with the name of the FDW
for PostgreSQL which
follows the naming habit of other FDWs.
* dblink depends on postgresql_fdw_validator.
* postgresql_fdw_validator assumes that libpq supports some particular
options.
An idea to resolve these is to add dblink's own validator which doesn't
assume much about libpq, and obsolete postgresql_fdw_validator.
* Add dblink_fdw_validator to contrib/dblink, which is similar to
postgresql_fdw_validator but it assumes less about libpq.
* Add dblink_fdw as default FDW of dblink, which uses
dblink_fdw_validator, and recommend to use it. This would prevent users
from using postgresql_fdw_validator with dblink.
* Mention that postgresql_fdw_validator might be obsolete in future
release in the document of CREATE FOREIGN DATA WRAPPER.
To make the behavior of dblink_fdw_validator similar to that of current
postgresql_fdw_validator, we need to assume that libpq supports "user"
option, and allow it and secret options in only USER MAPPING options,
and allow others in only SERVER options (and reject all debug options).
IMO this is not unreasonable assumption.
Is this proposal reasonable? Any comments and questions are welcome.
Regards,
--
Shigeru HANADA