diff --git a/contrib/postgres_fdw/Makefile b/contrib/postgres_fdw/Makefile
index d2b98e10f3a5e455886e6c605ae6e0bad5930799..354331247a82247ff7714b20c0ab87b8869850ec 100644
--- a/contrib/postgres_fdw/Makefile
+++ b/contrib/postgres_fdw/Makefile
@@ -1,7 +1,7 @@
 # contrib/postgres_fdw/Makefile
 
 MODULE_big = postgres_fdw
-OBJS = postgres_fdw.o option.o deparse.o connection.o $(WIN32RES)
+OBJS = postgres_fdw.o option.o deparse.o connection.o shippable.o $(WIN32RES)
 PGFILEDESC = "postgres_fdw - foreign data wrapper for PostgreSQL"
 
 PG_CPPFLAGS = -I$(libpq_srcdir)
diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index 3cb728fa693f6b2661bd3e46c2527893785c12a7..c8232f2c16f36866b015990d03542203233964ba 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -38,7 +38,6 @@
 #include "access/heapam.h"
 #include "access/htup_details.h"
 #include "access/sysattr.h"
-#include "access/transam.h"
 #include "catalog/pg_collation.h"
 #include "catalog/pg_namespace.h"
 #include "catalog/pg_operator.h"
@@ -102,7 +101,7 @@ typedef struct deparse_expr_cxt
 static bool foreign_expr_walker(Node *node,
 					foreign_glob_cxt *glob_cxt,
 					foreign_loc_cxt *outer_cxt);
-static bool is_builtin(Oid procid);
+static char *deparse_type_name(Oid type_oid, int32 typemod);
 
 /*
  * Functions to construct string representation of a node tree.
@@ -220,11 +219,12 @@ is_foreign_expr(PlannerInfo *root,
  * In addition, *outer_cxt is updated with collation information.
  *
  * We must check that the expression contains only node types we can deparse,
- * that all types/functions/operators are safe to send (which we approximate
- * as being built-in), and that all collations used in the expression derive
- * from Vars of the foreign table.  Because of the latter, the logic is
- * pretty close to assign_collations_walker() in parse_collate.c, though we
- * can assume here that the given expression is valid.
+ * that all types/functions/operators are safe to send (they are "shippable"),
+ * and that all collations used in the expression derive from Vars of the
+ * foreign table.  Because of the latter, the logic is pretty close to
+ * assign_collations_walker() in parse_collate.c, though we can assume here
+ * that the given expression is valid.  Note function mutability is not
+ * currently considered here.
  */
 static bool
 foreign_expr_walker(Node *node,
@@ -232,6 +232,7 @@ foreign_expr_walker(Node *node,
 					foreign_loc_cxt *outer_cxt)
 {
 	bool		check_type = true;
+	PgFdwRelationInfo *fpinfo;
 	foreign_loc_cxt inner_cxt;
 	Oid			collation;
 	FDWCollateState state;
@@ -240,6 +241,9 @@ foreign_expr_walker(Node *node,
 	if (node == NULL)
 		return true;
 
+	/* May need server info from baserel's fdw_private struct */
+	fpinfo = (PgFdwRelationInfo *) (glob_cxt->foreignrel->fdw_private);
+
 	/* Set up inner_cxt for possible recursion to child nodes */
 	inner_cxt.collation = InvalidOid;
 	inner_cxt.state = FDW_COLLATE_NONE;
@@ -377,11 +381,11 @@ foreign_expr_walker(Node *node,
 				FuncExpr   *fe = (FuncExpr *) node;
 
 				/*
-				 * If function used by the expression is not built-in, it
+				 * If function used by the expression is not shippable, it
 				 * can't be sent to remote because it might have incompatible
 				 * semantics on remote side.
 				 */
-				if (!is_builtin(fe->funcid))
+				if (!is_shippable(fe->funcid, ProcedureRelationId, fpinfo))
 					return false;
 
 				/*
@@ -425,11 +429,11 @@ foreign_expr_walker(Node *node,
 				OpExpr	   *oe = (OpExpr *) node;
 
 				/*
-				 * Similarly, only built-in operators can be sent to remote.
-				 * (If the operator is, surely its underlying function is
-				 * too.)
+				 * Similarly, only shippable operators can be sent to remote.
+				 * (If the operator is shippable, we assume its underlying
+				 * function is too.)
 				 */
-				if (!is_builtin(oe->opno))
+				if (!is_shippable(oe->opno, OperatorRelationId, fpinfo))
 					return false;
 
 				/*
@@ -467,9 +471,9 @@ foreign_expr_walker(Node *node,
 				ScalarArrayOpExpr *oe = (ScalarArrayOpExpr *) node;
 
 				/*
-				 * Again, only built-in operators can be sent to remote.
+				 * Again, only shippable operators can be sent to remote.
 				 */
-				if (!is_builtin(oe->opno))
+				if (!is_shippable(oe->opno, OperatorRelationId, fpinfo))
 					return false;
 
 				/*
@@ -616,10 +620,10 @@ foreign_expr_walker(Node *node,
 	}
 
 	/*
-	 * If result type of given expression is not built-in, it can't be sent to
-	 * remote because it might have incompatible semantics on remote side.
+	 * If result type of given expression is not shippable, it can't be sent
+	 * to remote because it might have incompatible semantics on remote side.
 	 */
-	if (check_type && !is_builtin(exprType(node)))
+	if (check_type && !is_shippable(exprType(node), TypeRelationId, fpinfo))
 		return false;
 
 	/*
@@ -672,27 +676,23 @@ foreign_expr_walker(Node *node,
 }
 
 /*
- * Return true if given object is one of PostgreSQL's built-in objects.
- *
- * We use FirstBootstrapObjectId as the cutoff, so that we only consider
- * objects with hand-assigned OIDs to be "built in", not for instance any
- * function or type defined in the information_schema.
+ * Convert type OID + typmod info into a type name we can ship to the remote
+ * server.  Someplace else had better have verified that this type name is
+ * expected to be known on the remote end.
  *
- * Our constraints for dealing with types are tighter than they are for
- * functions or operators: we want to accept only types that are in pg_catalog,
- * else format_type might incorrectly fail to schema-qualify their names.
- * (This could be fixed with some changes to format_type, but for now there's
- * no need.)  Thus we must exclude information_schema types.
- *
- * XXX there is a problem with this, which is that the set of built-in
- * objects expands over time.  Something that is built-in to us might not
- * be known to the remote server, if it's of an older version.  But keeping
- * track of that would be a huge exercise.
+ * This is almost just format_type_with_typemod(), except that if left to its
+ * own devices, that function will make schema-qualification decisions based
+ * on the local search_path, which is wrong.  We must schema-qualify all
+ * type names that are not in pg_catalog.  We assume here that built-in types
+ * are all in pg_catalog and need not be qualified; otherwise, qualify.
  */
-static bool
-is_builtin(Oid oid)
+static char *
+deparse_type_name(Oid type_oid, int32 typemod)
 {
-	return (oid < FirstBootstrapObjectId);
+	if (is_builtin(type_oid))
+		return format_type_with_typemod(type_oid, typemod);
+	else
+		return format_type_with_typemod_qualified(type_oid, typemod);
 }
 
 
@@ -1358,8 +1358,8 @@ deparseConst(Const *node, deparse_expr_cxt *context)
 	{
 		appendStringInfoString(buf, "NULL");
 		appendStringInfo(buf, "::%s",
-						 format_type_with_typemod(node->consttype,
-												  node->consttypmod));
+						 deparse_type_name(node->consttype,
+										   node->consttypmod));
 		return;
 	}
 
@@ -1432,8 +1432,8 @@ deparseConst(Const *node, deparse_expr_cxt *context)
 	}
 	if (needlabel)
 		appendStringInfo(buf, "::%s",
-						 format_type_with_typemod(node->consttype,
-												  node->consttypmod));
+						 deparse_type_name(node->consttype,
+										   node->consttypmod));
 }
 
 /*
@@ -1558,7 +1558,7 @@ deparseFuncExpr(FuncExpr *node, deparse_expr_cxt *context)
 
 		deparseExpr((Expr *) linitial(node->args), context);
 		appendStringInfo(buf, "::%s",
-						 format_type_with_typemod(rettype, coercedTypmod));
+						 deparse_type_name(rettype, coercedTypmod));
 		return;
 	}
 
@@ -1753,8 +1753,8 @@ deparseRelabelType(RelabelType *node, deparse_expr_cxt *context)
 	deparseExpr(node->arg, context);
 	if (node->relabelformat != COERCE_IMPLICIT_CAST)
 		appendStringInfo(context->buf, "::%s",
-						 format_type_with_typemod(node->resulttype,
-												  node->resulttypmod));
+						 deparse_type_name(node->resulttype,
+										   node->resulttypmod));
 }
 
 /*
@@ -1834,7 +1834,7 @@ deparseArrayExpr(ArrayExpr *node, deparse_expr_cxt *context)
 	/* If the array is empty, we need an explicit cast to the array type. */
 	if (node->elements == NIL)
 		appendStringInfo(buf, "::%s",
-						 format_type_with_typemod(node->array_typeid, -1));
+						 deparse_type_name(node->array_typeid, -1));
 }
 
 /*
@@ -1850,7 +1850,7 @@ printRemoteParam(int paramindex, Oid paramtype, int32 paramtypmod,
 				 deparse_expr_cxt *context)
 {
 	StringInfo	buf = context->buf;
-	char	   *ptypename = format_type_with_typemod(paramtype, paramtypmod);
+	char	   *ptypename = deparse_type_name(paramtype, paramtypmod);
 
 	appendStringInfo(buf, "$%d::%s", paramindex, ptypename);
 }
@@ -1876,7 +1876,7 @@ printRemotePlaceholder(Oid paramtype, int32 paramtypmod,
 					   deparse_expr_cxt *context)
 {
 	StringInfo	buf = context->buf;
-	char	   *ptypename = format_type_with_typemod(paramtype, paramtypmod);
+	char	   *ptypename = deparse_type_name(paramtype, paramtypmod);
 
 	appendStringInfo(buf, "((SELECT null::%s)::%s)", ptypename, ptypename);
 }
@@ -1890,10 +1890,10 @@ void
 appendOrderByClause(StringInfo buf, PlannerInfo *root, RelOptInfo *baserel,
 					List *pathkeys)
 {
-	ListCell			*lcell;
-	deparse_expr_cxt	context;
-	int					nestlevel;
-	char				*delim = " ";
+	ListCell   *lcell;
+	deparse_expr_cxt context;
+	int			nestlevel;
+	char	   *delim = " ";
 
 	/* Set up context struct for recursion */
 	context.root = root;
@@ -1907,8 +1907,8 @@ appendOrderByClause(StringInfo buf, PlannerInfo *root, RelOptInfo *baserel,
 	appendStringInfo(buf, " ORDER BY");
 	foreach(lcell, pathkeys)
 	{
-		PathKey				*pathkey = lfirst(lcell);
-		Expr				*em_expr;
+		PathKey    *pathkey = lfirst(lcell);
+		Expr	   *em_expr;
 
 		em_expr = find_em_expr_for_rel(pathkey->pk_eclass, baserel);
 		Assert(em_expr != NULL);
diff --git a/contrib/postgres_fdw/option.c b/contrib/postgres_fdw/option.c
index 7547ec28172e057f317a8cc782a6c795f09ad091..380ac80ab3a3c6b6323079ce05cd3279e5819137 100644
--- a/contrib/postgres_fdw/option.c
+++ b/contrib/postgres_fdw/option.c
@@ -19,6 +19,8 @@
 #include "catalog/pg_foreign_table.h"
 #include "catalog/pg_user_mapping.h"
 #include "commands/defrem.h"
+#include "commands/extension.h"
+#include "utils/builtins.h"
 
 
 /*
@@ -124,6 +126,11 @@ postgres_fdw_validator(PG_FUNCTION_ARGS)
 						 errmsg("%s requires a non-negative numeric value",
 								def->defname)));
 		}
+		else if (strcmp(def->defname, "extensions") == 0)
+		{
+			/* check list syntax, warn about uninstalled extensions */
+			(void) ExtractExtensionList(defGetString(def), true);
+		}
 	}
 
 	PG_RETURN_VOID();
@@ -150,6 +157,8 @@ InitPgFdwOptions(void)
 		/* cost factors */
 		{"fdw_startup_cost", ForeignServerRelationId, false},
 		{"fdw_tuple_cost", ForeignServerRelationId, false},
+		/* shippable extensions */
+		{"extensions", ForeignServerRelationId, false},
 		/* updatable is available on both server and table */
 		{"updatable", ForeignServerRelationId, false},
 		{"updatable", ForeignTableRelationId, false},
@@ -293,3 +302,48 @@ ExtractConnectionOptions(List *defelems, const char **keywords,
 	}
 	return i;
 }
+
+/*
+ * Parse a comma-separated string and return a List of the OIDs of the
+ * extensions named in the string.  If any names in the list cannot be
+ * found, report a warning if warnOnMissing is true, else just silently
+ * ignore them.
+ */
+List *
+ExtractExtensionList(const char *extensionsString, bool warnOnMissing)
+{
+	List	   *extensionOids = NIL;
+	List	   *extlist;
+	ListCell   *lc;
+
+	/* SplitIdentifierString scribbles on its input, so pstrdup first */
+	if (!SplitIdentifierString(pstrdup(extensionsString), ',', &extlist))
+	{
+		/* syntax error in name list */
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("parameter \"%s\" must be a list of extension names",
+						"extensions")));
+	}
+
+	foreach(lc, extlist)
+	{
+		const char *extension_name = (const char *) lfirst(lc);
+		Oid			extension_oid = get_extension_oid(extension_name, true);
+
+		if (OidIsValid(extension_oid))
+		{
+			extensionOids = lappend_oid(extensionOids, extension_oid);
+		}
+		else if (warnOnMissing)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("extension \"%s\" is not installed",
+							extension_name)));
+		}
+	}
+
+	list_free(extlist);
+	return extensionOids;
+}
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 914e6704c26de4b715aae5b3adf4be516a1aad3d..cd4ed0c94dbc8e9ce3b2d1c3ea9f7c00b7fb1ce9 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -50,40 +50,6 @@ PG_MODULE_MAGIC;
 /* If no remote estimates, assume a sort costs 20% extra */
 #define DEFAULT_FDW_SORT_MULTIPLIER 1.2
 
-/*
- * FDW-specific planner information kept in RelOptInfo.fdw_private for a
- * foreign table.  This information is collected by postgresGetForeignRelSize.
- */
-typedef struct PgFdwRelationInfo
-{
-	/* baserestrictinfo clauses, broken down into safe and unsafe subsets. */
-	List	   *remote_conds;
-	List	   *local_conds;
-
-	/* Bitmap of attr numbers we need to fetch from the remote server. */
-	Bitmapset  *attrs_used;
-
-	/* Cost and selectivity of local_conds. */
-	QualCost	local_conds_cost;
-	Selectivity local_conds_sel;
-
-	/* Estimated size and cost for a scan with baserestrictinfo quals. */
-	double		rows;
-	int			width;
-	Cost		startup_cost;
-	Cost		total_cost;
-
-	/* Options extracted from catalogs. */
-	bool		use_remote_estimate;
-	Cost		fdw_startup_cost;
-	Cost		fdw_tuple_cost;
-
-	/* Cached catalog information. */
-	ForeignTable *table;
-	ForeignServer *server;
-	UserMapping *user;			/* only set in use_remote_estimate mode */
-} PgFdwRelationInfo;
-
 /*
  * Indexes of FDW-private information stored in fdw_private lists.
  *
@@ -409,6 +375,7 @@ postgresGetForeignRelSize(PlannerInfo *root,
 	fpinfo->use_remote_estimate = false;
 	fpinfo->fdw_startup_cost = DEFAULT_FDW_STARTUP_COST;
 	fpinfo->fdw_tuple_cost = DEFAULT_FDW_TUPLE_COST;
+	fpinfo->shippable_extensions = NIL;
 
 	foreach(lc, fpinfo->server->options)
 	{
@@ -420,6 +387,9 @@ postgresGetForeignRelSize(PlannerInfo *root,
 			fpinfo->fdw_startup_cost = strtod(defGetString(def), NULL);
 		else if (strcmp(def->defname, "fdw_tuple_cost") == 0)
 			fpinfo->fdw_tuple_cost = strtod(defGetString(def), NULL);
+		else if (strcmp(def->defname, "extensions") == 0)
+			fpinfo->shippable_extensions =
+				ExtractExtensionList(defGetString(def), false);
 	}
 	foreach(lc, fpinfo->table->options)
 	{
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index 8956cd2cf1c404020cc63b28a1534ce86ea3dab1..f243de8d6237822558422d76219c697490600547 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -20,6 +20,41 @@
 
 #include "libpq-fe.h"
 
+/*
+ * FDW-specific planner information kept in RelOptInfo.fdw_private for a
+ * foreign table.  This information is collected by postgresGetForeignRelSize.
+ */
+typedef struct PgFdwRelationInfo
+{
+	/* baserestrictinfo clauses, broken down into safe and unsafe subsets. */
+	List	   *remote_conds;
+	List	   *local_conds;
+
+	/* Bitmap of attr numbers we need to fetch from the remote server. */
+	Bitmapset  *attrs_used;
+
+	/* Cost and selectivity of local_conds. */
+	QualCost	local_conds_cost;
+	Selectivity local_conds_sel;
+
+	/* Estimated size and cost for a scan with baserestrictinfo quals. */
+	double		rows;
+	int			width;
+	Cost		startup_cost;
+	Cost		total_cost;
+
+	/* Options extracted from catalogs. */
+	bool		use_remote_estimate;
+	Cost		fdw_startup_cost;
+	Cost		fdw_tuple_cost;
+	List	   *shippable_extensions;	/* OIDs of whitelisted extensions */
+
+	/* Cached catalog information. */
+	ForeignTable *table;
+	ForeignServer *server;
+	UserMapping *user;			/* only set in use_remote_estimate mode */
+} PgFdwRelationInfo;
+
 /* in postgres_fdw.c */
 extern int	set_transmission_modes(void);
 extern void reset_transmission_modes(int nestlevel);
@@ -37,6 +72,8 @@ extern void pgfdw_report_error(int elevel, PGresult *res, PGconn *conn,
 extern int ExtractConnectionOptions(List *defelems,
 						 const char **keywords,
 						 const char **values);
+extern List *ExtractExtensionList(const char *extensionsString,
+					 bool warnOnMissing);
 
 /* in deparse.c */
 extern void classifyConditions(PlannerInfo *root,
@@ -76,6 +113,10 @@ extern void deparseAnalyzeSql(StringInfo buf, Relation rel,
 extern void deparseStringLiteral(StringInfo buf, const char *val);
 extern Expr *find_em_expr_for_rel(EquivalenceClass *ec, RelOptInfo *rel);
 extern void appendOrderByClause(StringInfo buf, PlannerInfo *root,
-								RelOptInfo *baserel, List *pathkeys);
+					RelOptInfo *baserel, List *pathkeys);
+
+/* in shippable.c */
+extern bool is_builtin(Oid objectId);
+extern bool is_shippable(Oid objectId, Oid classId, PgFdwRelationInfo *fpinfo);
 
 #endif   /* POSTGRES_FDW_H */
diff --git a/contrib/postgres_fdw/shippable.c b/contrib/postgres_fdw/shippable.c
new file mode 100644
index 0000000000000000000000000000000000000000..ebb1f5e31770ccf99b6baf3b3b6fa42cef42c612
--- /dev/null
+++ b/contrib/postgres_fdw/shippable.c
@@ -0,0 +1,214 @@
+/*-------------------------------------------------------------------------
+ *
+ * shippable.c
+ *	  Determine which database objects are shippable to a remote server.
+ *
+ * We need to determine whether particular functions, operators, and indeed
+ * data types are shippable to a remote server for execution --- that is,
+ * do they exist and have the same behavior remotely as they do locally?
+ * Built-in objects are generally considered shippable.  Other objects can
+ * be shipped if they are white-listed by the user.
+ *
+ * Note: there are additional filter rules that prevent shipping mutable
+ * functions or functions using nonportable collations.  Those considerations
+ * need not be accounted for here.
+ *
+ * Portions Copyright (c) 1996-2015, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *	  contrib/postgres_fdw/shippable.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "postgres_fdw.h"
+
+#include "access/transam.h"
+#include "catalog/dependency.h"
+#include "utils/hsearch.h"
+#include "utils/inval.h"
+#include "utils/syscache.h"
+
+
+/* Hash table for caching the results of shippability lookups */
+static HTAB *ShippableCacheHash = NULL;
+
+/*
+ * Hash key for shippability lookups.  We include the FDW server OID because
+ * decisions may differ per-server.  Otherwise, objects are identified by
+ * their (local!) OID and catalog OID.
+ */
+typedef struct
+{
+	/* XXX we assume this struct contains no padding bytes */
+	Oid			objid;			/* function/operator/type OID */
+	Oid			classid;		/* OID of its catalog (pg_proc, etc) */
+	Oid			serverid;		/* FDW server we are concerned with */
+} ShippableCacheKey;
+
+typedef struct
+{
+	ShippableCacheKey key;		/* hash key - must be first */
+	bool		shippable;
+} ShippableCacheEntry;
+
+
+/*
+ * Flush cache entries when pg_foreign_server is updated.
+ *
+ * We do this because of the possibility of ALTER SERVER being used to change
+ * a server's extensions option.  We do not currently bother to check whether
+ * objects' extension membership changes once a shippability decision has been
+ * made for them, however.
+ */
+static void
+InvalidateShippableCacheCallback(Datum arg, int cacheid, uint32 hashvalue)
+{
+	HASH_SEQ_STATUS status;
+	ShippableCacheEntry *entry;
+
+	/*
+	 * In principle we could flush only cache entries relating to the
+	 * pg_foreign_server entry being outdated; but that would be more
+	 * complicated, and it's probably not worth the trouble.  So for now, just
+	 * flush all entries.
+	 */
+	hash_seq_init(&status, ShippableCacheHash);
+	while ((entry = (ShippableCacheEntry *) hash_seq_search(&status)) != NULL)
+	{
+		if (hash_search(ShippableCacheHash,
+						(void *) &entry->key,
+						HASH_REMOVE,
+						NULL) == NULL)
+			elog(ERROR, "hash table corrupted");
+	}
+}
+
+/*
+ * Initialize the backend-lifespan cache of shippability decisions.
+ */
+static void
+InitializeShippableCache(void)
+{
+	HASHCTL		ctl;
+
+	/* Create the hash table. */
+	MemSet(&ctl, 0, sizeof(ctl));
+	ctl.keysize = sizeof(ShippableCacheKey);
+	ctl.entrysize = sizeof(ShippableCacheEntry);
+	ShippableCacheHash =
+		hash_create("Shippability cache", 256, &ctl, HASH_ELEM | HASH_BLOBS);
+
+	/* Set up invalidation callback on pg_foreign_server. */
+	CacheRegisterSyscacheCallback(FOREIGNSERVEROID,
+								  InvalidateShippableCacheCallback,
+								  (Datum) 0);
+}
+
+/*
+ * Returns true if given object (operator/function/type) is shippable
+ * according to the server options.
+ *
+ * Right now "shippability" is exclusively a function of whether the object
+ * belongs to an extension declared by the user.  In the future we could
+ * additionally have a whitelist of functions/operators declared one at a time.
+ */
+static bool
+lookup_shippable(Oid objectId, Oid classId, PgFdwRelationInfo *fpinfo)
+{
+	Oid			extensionOid;
+
+	/*
+	 * Is object a member of some extension?  (Note: this is a fairly
+	 * expensive lookup, which is why we try to cache the results.)
+	 */
+	extensionOid = getExtensionOfObject(classId, objectId);
+
+	/* If so, is that extension in fpinfo->shippable_extensions? */
+	if (OidIsValid(extensionOid) &&
+		list_member_oid(fpinfo->shippable_extensions, extensionOid))
+		return true;
+
+	return false;
+}
+
+/*
+ * Return true if given object is one of PostgreSQL's built-in objects.
+ *
+ * We use FirstBootstrapObjectId as the cutoff, so that we only consider
+ * objects with hand-assigned OIDs to be "built in", not for instance any
+ * function or type defined in the information_schema.
+ *
+ * Our constraints for dealing with types are tighter than they are for
+ * functions or operators: we want to accept only types that are in pg_catalog,
+ * else deparse_type_name might incorrectly fail to schema-qualify their names.
+ * Thus we must exclude information_schema types.
+ *
+ * XXX there is a problem with this, which is that the set of built-in
+ * objects expands over time.  Something that is built-in to us might not
+ * be known to the remote server, if it's of an older version.  But keeping
+ * track of that would be a huge exercise.
+ */
+bool
+is_builtin(Oid objectId)
+{
+	return (objectId < FirstBootstrapObjectId);
+}
+
+/*
+ * is_shippable
+ *	   Is this object (function/operator/type) shippable to foreign server?
+ */
+bool
+is_shippable(Oid objectId, Oid classId, PgFdwRelationInfo *fpinfo)
+{
+	ShippableCacheKey key;
+	ShippableCacheEntry *entry;
+
+	/* Built-in objects are presumed shippable. */
+	if (is_builtin(objectId))
+		return true;
+
+	/* Otherwise, give up if user hasn't specified any shippable extensions. */
+	if (fpinfo->shippable_extensions == NIL)
+		return false;
+
+	/* Initialize cache if first time through. */
+	if (!ShippableCacheHash)
+		InitializeShippableCache();
+
+	/* Set up cache hash key */
+	key.objid = objectId;
+	key.classid = classId;
+	key.serverid = fpinfo->server->serverid;
+
+	/* See if we already cached the result. */
+	entry = (ShippableCacheEntry *)
+		hash_search(ShippableCacheHash,
+					(void *) &key,
+					HASH_FIND,
+					NULL);
+
+	if (!entry)
+	{
+		/* Not found in cache, so perform shippability lookup. */
+		bool		shippable = lookup_shippable(objectId, classId, fpinfo);
+
+		/*
+		 * Don't create a new hash entry until *after* we have the shippable
+		 * result in hand, as the underlying catalog lookups might trigger a
+		 * cache invalidation.
+		 */
+		entry = (ShippableCacheEntry *)
+			hash_search(ShippableCacheHash,
+						(void *) &key,
+						HASH_ENTER,
+						NULL);
+
+		entry->shippable = shippable;
+	}
+
+	return entry->shippable;
+}
diff --git a/doc/src/sgml/postgres-fdw.sgml b/doc/src/sgml/postgres-fdw.sgml
index 7c922821e988ffde8711e8323ff310c8166187d4..5a829d537a69816a31c645442866aa33303ee1d6 100644
--- a/doc/src/sgml/postgres-fdw.sgml
+++ b/doc/src/sgml/postgres-fdw.sgml
@@ -264,6 +264,46 @@
 
   </sect3>
 
+  <sect3>
+   <title>Remote Execution Options</title>
+
+   <para>
+    By default, only <literal>WHERE</> clauses using built-in operators and
+    functions will be considered for execution on the remote server.  Clauses
+    involving non-built-in functions are checked locally after rows are
+    fetched.  If such functions are available on the remote server and can be
+    relied on to produce the same results as they do locally, performance can
+    be improved by sending such <literal>WHERE</> clauses for remote
+    execution.  This behavior can be controlled using the following option:
+   </para>
+
+   <variablelist>
+
+    <varlistentry>
+     <term><literal>extensions</literal></term>
+     <listitem>
+      <para>
+       This option is a comma-separated list of names
+       of <productname>PostgreSQL</> extensions that are installed, in
+       compatible versions, on both the local and remote servers.  Functions
+       and operators that are immutable and belong to a listed extension will
+       be considered shippable to the remote server.
+       This option can only be specified for foreign servers, not per-table.
+      </para>
+     </listitem>
+    </varlistentry>
+
+   </variablelist>
+
+   <para>
+    When using the <literal>extensions</literal> option, <emphasis>it is the
+    user's responsibility</> that the listed extensions exist and behave
+    identically on both the local and remote servers.  Otherwise, remote
+    queries may fail or behave unexpectedly.
+   </para>
+
+  </sect3>
+
   <sect3>
    <title>Updatability Options</title>
 
@@ -427,8 +467,10 @@
    execution, and by not retrieving table columns that are not needed for
    the current query.  To reduce the risk of misexecution of queries,
    <literal>WHERE</> clauses are not sent to the remote server unless they use
-   only built-in data types, operators, and functions.  Operators and
-   functions in the clauses must be <literal>IMMUTABLE</> as well.
+   only data types, operators, and functions that are built-in or belong to an
+   extension that's listed in the foreign server's <literal>extensions</>
+   option.  Operators and functions in such clauses must
+   be <literal>IMMUTABLE</> as well.
   </para>
 
   <para>
diff --git a/src/backend/utils/adt/format_type.c b/src/backend/utils/adt/format_type.c
index a8519835cc66215aebd193ead8f682438c1d0d12..e046f05f28c659c0c988e429f3100fdecedb31ff 100644
--- a/src/backend/utils/adt/format_type.c
+++ b/src/backend/utils/adt/format_type.c
@@ -97,7 +97,8 @@ format_type_be(Oid type_oid)
 }
 
 /*
- * This version returns a name which is always qualified.
+ * This version returns a name that is always qualified (unless it's one
+ * of the SQL-keyword type names, such as TIMESTAMP WITH TIME ZONE).
  */
 char *
 format_type_be_qualified(Oid type_oid)
@@ -114,6 +115,19 @@ format_type_with_typemod(Oid type_oid, int32 typemod)
 	return format_type_internal(type_oid, typemod, true, false, false);
 }
 
+/*
+ * This version allows a nondefault typemod to be specified, and forces
+ * qualification of normal type names.
+ */
+char *
+format_type_with_typemod_qualified(Oid type_oid, int32 typemod)
+{
+	return format_type_internal(type_oid, typemod, true, false, true);
+}
+
+/*
+ * Common workhorse.
+ */
 static char *
 format_type_internal(Oid type_oid, int32 typemod,
 					 bool typemod_given, bool allow_invalid,
diff --git a/src/include/utils/builtins.h b/src/include/utils/builtins.h
index fc1679ed462bbe602010f412a952463524e983d2..c193e4425ed9864cf2db82fec1ed90f8939a79bc 100644
--- a/src/include/utils/builtins.h
+++ b/src/include/utils/builtins.h
@@ -1105,6 +1105,7 @@ extern Datum format_type(PG_FUNCTION_ARGS);
 extern char *format_type_be(Oid type_oid);
 extern char *format_type_be_qualified(Oid type_oid);
 extern char *format_type_with_typemod(Oid type_oid, int32 typemod);
+extern char *format_type_with_typemod_qualified(Oid type_oid, int32 typemod);
 extern Datum oidvectortypes(PG_FUNCTION_ARGS);
 extern int32 type_maximum_size(Oid type_oid, int32 typemod);