diff --git a/contrib/postgres_fdw/Makefile b/contrib/postgres_fdw/Makefile index d2b98e10f3a5e..dc164ee6e5735 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) @@ -10,7 +10,10 @@ SHLIB_LINK = $(libpq) EXTENSION = postgres_fdw DATA = postgres_fdw--1.0.sql -REGRESS = postgres_fdw +# Note: shippable tests depend on postgres_fdw tests setup +REGRESS = postgres_fdw shippable +# Note: shippable tests require cube and seg +EXTRA_INSTALL = contrib/cube contrib/seg ifdef USE_PGXS PG_CONFIG = pg_config diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c index 697de60dfe51b..aa575b39ed078 100644 --- a/contrib/postgres_fdw/deparse.c +++ b/contrib/postgres_fdw/deparse.c @@ -233,6 +233,9 @@ foreign_expr_walker(Node *node, Oid collation; FDWCollateState state; + /* Access extension metadata from fpinfo on baserel */ + PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *)(glob_cxt->foreignrel->fdw_private); + /* Need do nothing for empty subexpressions */ if (node == NULL) return true; @@ -378,7 +381,8 @@ foreign_expr_walker(Node *node, * can't be sent to remote because it might have incompatible * semantics on remote side. */ - if (!is_builtin(fe->funcid)) + if (!is_builtin(fe->funcid) && + !is_shippable(fe->funcid, ProcedureRelationId, fpinfo->server, fpinfo->extensions)) return false; /* @@ -426,7 +430,8 @@ foreign_expr_walker(Node *node, * (If the operator is, surely its underlying function is * too.) */ - if (!is_builtin(oe->opno)) + if (!is_builtin(oe->opno) && + !is_shippable(oe->opno, OperatorRelationId, fpinfo->server, fpinfo->extensions)) return false; /* @@ -466,7 +471,8 @@ foreign_expr_walker(Node *node, /* * Again, only built-in operators can be sent to remote. */ - if (!is_builtin(oe->opno)) + if (!is_builtin(oe->opno) && + !is_shippable(oe->opno, OperatorRelationId, fpinfo->server, fpinfo->extensions)) return false; /* @@ -616,7 +622,9 @@ 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 (check_type && !is_builtin(exprType(node))) + if (check_type && + !is_builtin(exprType(node)) && + !is_shippable(exprType(node), TypeRelationId, fpinfo->server, fpinfo->extensions)) return false; /* @@ -1351,6 +1359,9 @@ deparseConst(Const *node, deparse_expr_cxt *context) bool isfloat = false; bool needlabel; + /* Access extension metadata from fpinfo on baserel */ + PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *)(context->foreignrel->fdw_private); + if (node->constisnull) { appendStringInfoString(buf, "NULL"); @@ -1428,9 +1439,21 @@ deparseConst(Const *node, deparse_expr_cxt *context) break; } if (needlabel) - appendStringInfo(buf, "::%s", - format_type_with_typemod(node->consttype, - node->consttypmod)); + { + /* + * References to extension types need to be fully qualified, + * but references to built-in types shouldn't be. + */ + if (!is_builtin(node->consttype) && + is_shippable(node->consttype, TypeRelationId, fpinfo->server, fpinfo->extensions)) + { + appendStringInfo(buf, "::%s", format_type_be_qualified(node->consttype)); + } + else + { + appendStringInfo(buf, "::%s", format_type_with_typemod(node->consttype, node->consttypmod)); + } + } } /* diff --git a/contrib/postgres_fdw/expected/shippable.out b/contrib/postgres_fdw/expected/shippable.out new file mode 100644 index 0000000000000..abf924efe4ae4 --- /dev/null +++ b/contrib/postgres_fdw/expected/shippable.out @@ -0,0 +1,225 @@ +-- =================================================================== +-- create FDW objects +-- =================================================================== +-- Error, extension isn't installed yet +ALTER SERVER loopback OPTIONS (ADD extensions 'cube'); +ERROR: required extension "cube" is not installed +HINT: Extension must be installed locally before it can be used on a remote server. +-- Try again +CREATE EXTENSION cube; +ALTER SERVER loopback OPTIONS (ADD extensions 'cube'); +ALTER SERVER loopback OPTIONS (DROP extensions); +-- =================================================================== +-- create objects used through FDW loopback server +-- =================================================================== +CREATE SCHEMA "SH 1"; +CREATE TABLE "SH 1"."TBL 1" ( + "C 1" int NOT NULL, + c2 int NOT NULL, + c3 cube, + c4 timestamptz +); +INSERT INTO "SH 1"."TBL 1" + SELECT id, + 2 * id, + cube(id,2*id), + '1970-01-01'::timestamptz + ((id % 100) || ' days')::interval + FROM generate_series(1, 1000) id; +ANALYZE "SH 1"."TBL 1"; +-- =================================================================== +-- create foreign table +-- =================================================================== +CREATE FOREIGN TABLE shft1 ( + "C 1" int NOT NULL, + c2 int NOT NULL, + c3 cube, + c4 timestamptz +) SERVER loopback +OPTIONS (schema_name 'SH 1', table_name 'TBL 1'); +-- =================================================================== +-- simple queries +-- =================================================================== +-- without operator shipping +EXPLAIN (COSTS false) SELECT * FROM shft1 LIMIT 1; + QUERY PLAN +----------------------------- + Limit + -> Foreign Scan on shft1 +(2 rows) + +EXPLAIN VERBOSE SELECT c2 FROM shft1 WHERE c3 && cube(1.5,2.5); + QUERY PLAN +--------------------------------------------------------------------- + Foreign Scan on public.shft1 (cost=100.00..205.06 rows=15 width=4) + Output: c2 + Filter: (shft1.c3 && '(1.5),(2.5)'::cube) + Remote SQL: SELECT c2, c3 FROM "SH 1"."TBL 1" +(4 rows) + +SELECT c2 FROM shft1 WHERE c3 && cube(1.5,2.5); + c2 +---- + 2 + 4 +(2 rows) + +EXPLAIN VERBOSE SELECT c2 FROM shft1 WHERE c3 && '(1.5),(2.5)'::cube; + QUERY PLAN +--------------------------------------------------------------------- + Foreign Scan on public.shft1 (cost=100.00..205.06 rows=15 width=4) + Output: c2 + Filter: (shft1.c3 && '(1.5),(2.5)'::cube) + Remote SQL: SELECT c2, c3 FROM "SH 1"."TBL 1" +(4 rows) + +-- with operator shipping +ALTER SERVER loopback OPTIONS (ADD extensions 'cube'); +EXPLAIN VERBOSE SELECT c2 FROM shft1 WHERE c3 && cube(1.5,2.5); + QUERY PLAN +--------------------------------------------------------------------------------------------------------- + Foreign Scan on public.shft1 (cost=100.00..146.86 rows=15 width=4) + Output: c2 + Remote SQL: SELECT c2 FROM "SH 1"."TBL 1" WHERE ((c3 OPERATOR(public.&&) '(1.5),(2.5)'::public.cube)) +(3 rows) + +SELECT c2 FROM shft1 WHERE c3 && cube(1.5,2.5); + c2 +---- + 2 + 4 +(2 rows) + +EXPLAIN VERBOSE SELECT c2 FROM shft1 WHERE c3 && '(1.5),(2.5)'::cube; + QUERY PLAN +--------------------------------------------------------------------------------------------------------- + Foreign Scan on public.shft1 (cost=100.00..146.86 rows=15 width=4) + Output: c2 + Remote SQL: SELECT c2 FROM "SH 1"."TBL 1" WHERE ((c3 OPERATOR(public.&&) '(1.5),(2.5)'::public.cube)) +(3 rows) + +EXPLAIN VERBOSE SELECT cube_dim(c3) FROM shft1 WHERE c3 && '(1.5),(2.5)'::cube; + QUERY PLAN +--------------------------------------------------------------------------------------------------------- + Foreign Scan on public.shft1 (cost=100.00..128.43 rows=7 width=32) + Output: cube_dim(c3) + Remote SQL: SELECT c3 FROM "SH 1"."TBL 1" WHERE ((c3 OPERATOR(public.&&) '(1.5),(2.5)'::public.cube)) +(3 rows) + +SELECT cube_dim(c3) FROM shft1 WHERE c3 && '(1.5),(2.5)'::cube; + cube_dim +---------- + 1 + 1 +(2 rows) + +EXPLAIN VERBOSE SELECT c2 FROM shft1 WHERE cube_dim(c3) = 1 LIMIT 2; + QUERY PLAN +------------------------------------------------------------------------------------- + Limit (cost=100.00..107.22 rows=2 width=4) + Output: c2 + -> Foreign Scan on public.shft1 (cost=100.00..154.18 rows=15 width=4) + Output: c2 + Remote SQL: SELECT c2 FROM "SH 1"."TBL 1" WHERE ((public.cube_dim(c3) = 1)) +(5 rows) + +SELECT c2 FROM shft1 WHERE cube_dim(c3) = 1 LIMIT 2; + c2 +---- + 2 + 4 +(2 rows) + +-- =================================================================== +-- add a second server with different extension shipping +-- =================================================================== +DO $d$ + BEGIN + EXECUTE $$CREATE SERVER loopback_two FOREIGN DATA WRAPPER postgres_fdw + OPTIONS (dbname '$$||current_database()||$$', + port '$$||current_setting('port')||$$' + )$$; + END; +$d$; +CREATE USER MAPPING FOR CURRENT_USER SERVER loopback_two; +CREATE EXTENSION seg; +CREATE TABLE seg_local ( + id integer, + s seg, + n text +); +INSERT INTO seg_local (id, s, n) VALUES (1, '1.0 .. 2.0', 'foo'); +INSERT INTO seg_local (id, s, n) VALUES (2, '3.0 .. 4.0', 'bar'); +INSERT INTO seg_local (id, s, n) VALUES (3, '5.0 .. 6.0', 'baz'); +ANALYZE seg_local; +CREATE FOREIGN TABLE seg_remote_two ( + id integer, + s seg, + n text +) SERVER loopback_two +OPTIONS (table_name 'seg_local'); +SELECT id FROM seg_local WHERE s && '5.8 .. 6.2'::seg AND n = 'baz'; + id +---- + 3 +(1 row) + +EXPLAIN VERBOSE SELECT id FROM seg_remote_two WHERE s && '5.8 .. 6.2'::seg AND n = 'baz'; + QUERY PLAN +----------------------------------------------------------------------------- + Foreign Scan on public.seg_remote_two (cost=100.00..157.88 rows=1 width=4) + Output: id + Filter: (seg_remote_two.s && '5.8 .. 6.2'::seg) + Remote SQL: SELECT id, s FROM public.seg_local WHERE ((n = 'baz'::text)) +(4 rows) + +ALTER SERVER loopback_two OPTIONS (ADD extensions 'seg'); +EXPLAIN VERBOSE SELECT id FROM seg_remote_two WHERE s && '5.8 .. 6.2'::seg AND n = 'baz'; + QUERY PLAN +-------------------------------------------------------------------------------------------------------------------------------- + Foreign Scan on public.seg_remote_two (cost=100.00..153.89 rows=1 width=4) + Output: id + Remote SQL: SELECT id FROM public.seg_local WHERE ((s OPERATOR(public.&&) '5.8 .. 6.2'::public.seg)) AND ((n = 'baz'::text)) +(3 rows) + +CREATE FOREIGN TABLE seg_remote_one ( + id integer, + s seg, + n text +) SERVER loopback +OPTIONS (table_name 'seg_local'); +SELECT id FROM seg_remote_one WHERE s && '5.8 .. 6.2'::seg AND n = 'baz'; + id +---- + 3 +(1 row) + +EXPLAIN VERBOSE SELECT id FROM seg_remote_one WHERE s && '5.8 .. 6.2'::seg AND n = 'baz'; + QUERY PLAN +----------------------------------------------------------------------------- + Foreign Scan on public.seg_remote_one (cost=100.00..157.88 rows=1 width=4) + Output: id + Filter: (seg_remote_one.s && '5.8 .. 6.2'::seg) + Remote SQL: SELECT id, s FROM public.seg_local WHERE ((n = 'baz'::text)) +(4 rows) + +EXPLAIN VERBOSE SELECT id FROM seg_remote_two WHERE s && '5.8 .. 6.2'::seg AND n = 'baz'; + QUERY PLAN +-------------------------------------------------------------------------------------------------------------------------------- + Foreign Scan on public.seg_remote_two (cost=100.00..153.89 rows=1 width=4) + Output: id + Remote SQL: SELECT id FROM public.seg_local WHERE ((s OPERATOR(public.&&) '5.8 .. 6.2'::public.seg)) AND ((n = 'baz'::text)) +(3 rows) + +-- =================================================================== +-- clean up +-- =================================================================== +DROP FOREIGN TABLE seg_remote_one, seg_remote_two; +DROP USER MAPPING FOR CURRENT_USER SERVER loopback_two; +DROP SERVER loopback_two; +DROP TABLE seg_local; +DROP FOREIGN TABLE shft1; +DROP TABLE "SH 1"."TBL 1"; +DROP SCHEMA "SH 1"; +DROP EXTENSION cube; +DROP EXTENSION seg; +ALTER SERVER loopback OPTIONS (DROP extensions); diff --git a/contrib/postgres_fdw/option.c b/contrib/postgres_fdw/option.c index 7547ec28172e0..a20f626f62dfa 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 that the requested extensions are actually installed */ + (void) ExtractExtensionList(defGetString(def), false); + } } PG_RETURN_VOID(); @@ -153,6 +160,8 @@ InitPgFdwOptions(void) /* updatable is available on both server and table */ {"updatable", ForeignServerRelationId, false}, {"updatable", ForeignTableRelationId, false}, + /* "extensions" option is available on server */ + {"extensions", ForeignServerRelationId, false}, {NULL, InvalidOid, false} }; @@ -293,3 +302,49 @@ ExtractConnectionOptions(List *defelems, const char **keywords, } return i; } + +/* + * Parse a comma-separated string and return a List of the Oids of the + * extensions in the string. If an extension provided cannot be looked + * up in the catalog (it hasn't been installed or doesn't exist) then + * raise an error. + */ +List * +ExtractExtensionList(char *extensionString, bool populateList) +{ + List *extlist; + List *extensionOids = NIL; + ListCell *l; + + if (!SplitIdentifierString(extensionString, ',', &extlist)) + { + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("invalid extension list syntax"))); + } + + foreach(l, extlist) + { + const char *extension_name = (const char *) lfirst(l); + Oid extension_oid = get_extension_oid(extension_name, true); + + if (!OidIsValid(extension_oid)) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("required extension \"%s\" is not installed", + extension_name), + errhint("Extension must be installed locally before it can be used on a remote server."))); + else if (populateList) + { + /* + * Only add this extension OID to the list if it is not already + * in included. + */ + if (!list_member_oid(extensionOids, extension_oid)) + extensionOids = lappend_oid(extensionOids, extension_oid); + } + } + + list_free(extlist); + return extensionOids; +} diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c index e4d799cecd541..26147771199d7 100644 --- a/contrib/postgres_fdw/postgres_fdw.c +++ b/contrib/postgres_fdw/postgres_fdw.c @@ -47,39 +47,6 @@ PG_MODULE_MAGIC; /* Default CPU cost to process 1 row (above and beyond cpu_tuple_cost). */ #define DEFAULT_FDW_TUPLE_COST 0.01 -/* - * FDW-specific planner information kept in RelOptInfo.fdw_private for a - * foreign table. This information is collected by postgresGetForeignRelSize. - */ -typedef struct PgFdwRelationInfo -{ - /* baserestrictinfo clauses, broken down into safe and unsafe subsets. */ - List *remote_conds; - List *local_conds; - - /* Bitmap of attr numbers we need to fetch from the remote server. */ - Bitmapset *attrs_used; - - /* Cost and selectivity of local_conds. */ - QualCost local_conds_cost; - Selectivity local_conds_sel; - - /* Estimated size and cost for a scan with baserestrictinfo quals. */ - double rows; - int width; - Cost startup_cost; - Cost total_cost; - - /* Options extracted from catalogs. */ - bool use_remote_estimate; - Cost fdw_startup_cost; - Cost fdw_tuple_cost; - - /* Cached catalog information. */ - ForeignTable *table; - ForeignServer *server; - UserMapping *user; /* only set in use_remote_estimate mode */ -} PgFdwRelationInfo; /* * Indexes of FDW-private information stored in fdw_private lists. @@ -405,6 +372,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->extensions = NIL; foreach(lc, fpinfo->server->options) { @@ -416,6 +384,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->extensions = + ExtractExtensionList(defGetString(def), true); } foreach(lc, fpinfo->table->options) { diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h index 3835ddb79ac61..f30d9d153f95d 100644 --- a/contrib/postgres_fdw/postgres_fdw.h +++ b/contrib/postgres_fdw/postgres_fdw.h @@ -20,6 +20,43 @@ #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; + + /* Optional extensions to support (list of Oids). */ + List *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 +74,11 @@ 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(char *extensionString, + bool populateList); + +/* in shippable.c */ +extern bool is_shippable(Oid objnumber, Oid classnumber, ForeignServer *server, List *extension_list); /* in deparse.c */ extern void classifyConditions(PlannerInfo *root, diff --git a/contrib/postgres_fdw/shippable.c b/contrib/postgres_fdw/shippable.c new file mode 100644 index 0000000000000..960a7eccc51e0 --- /dev/null +++ b/contrib/postgres_fdw/shippable.c @@ -0,0 +1,211 @@ +/*------------------------------------------------------------------------- + * + * shippable.c + * Facility to track database objects shippable to a foreign server. + * + * Determine if functions and operators for non-built-in types/functions/ops + * are shippable to the remote server. + * + * Portions Copyright (c) 1996-2015, PostgreSQL Global Development Group + * + * IDENTIFICATION + * contrib/postgres_fdw/shippable.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "postgres_fdw.h" + +#include "access/genam.h" +#include "access/heapam.h" +#include "access/htup_details.h" +#include "catalog/dependency.h" +#include "catalog/indexing.h" +#include "catalog/pg_depend.h" +#include "utils/fmgroids.h" +#include "utils/hsearch.h" +#include "utils/inval.h" +#include "utils/rel.h" +#include "utils/snapmgr.h" +#include "utils/syscache.h" + +/* Hash table for informations about remote objects we'll call */ +static HTAB *ShippableCacheHash = NULL; + +/* objid is the lookup key, must appear first */ +typedef struct +{ + /* extension the object appears within, or InvalidOid if none */ + Oid serverid; + Oid classid; + Oid objid; +} ShippableCacheKey; + +typedef struct +{ + /* lookup key - must be first */ + ShippableCacheKey key; + bool shippable; +} ShippableCacheEntry; + +/* + * Flush all cache entries when pg_foreign_server is updated. + */ +static void +InvalidateShippableCacheCallback(Datum arg, int cacheid, uint32 hashvalue) +{ + HASH_SEQ_STATUS status; + ShippableCacheEntry *entry; + + 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 cache of functions we can ship to remote server. + */ +static void +InitializeShippableCache(void) +{ + HASHCTL ctl; + + /* Initialize the hash table. */ + MemSet(&ctl, 0, sizeof(ctl)); + ctl.keysize = sizeof(ShippableCacheKey); + ctl.entrysize = sizeof(ShippableCacheEntry); + ShippableCacheHash = + hash_create("Shippable cache", 256, &ctl, HASH_ELEM); + + CacheRegisterSyscacheCallback(FOREIGNSERVEROID, + InvalidateShippableCacheCallback, + (Datum) 0); +} + +/* + * Returns true if given operator/function is part of an extension listed in + * the server options. + */ +static bool +lookup_shippable(Oid objnumber, Oid classnumber, List *extension_list) +{ + static int nkeys = 2; + ScanKeyData key[nkeys]; + HeapTuple tup; + Relation depRel; + SysScanDesc scan; + bool is_shippable = false; + + /* Always return false if the user hasn't set the "extensions" option */ + if (extension_list == NIL) + return false; + + depRel = heap_open(DependRelationId, RowExclusiveLock); + + /* + * Scan the system dependency table for all entries this object + * depends on, then iterate through and see if one of them + * is an extension declared by the user in the options + */ + ScanKeyInit(&key[0], + Anum_pg_depend_classid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(classnumber)); + + ScanKeyInit(&key[1], + Anum_pg_depend_objid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(objnumber)); + + scan = systable_beginscan(depRel, DependDependerIndexId, true, + GetCatalogSnapshot(depRel->rd_id), nkeys, key); + + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_depend foundDep = (Form_pg_depend) GETSTRUCT(tup); + + if (foundDep->deptype == DEPENDENCY_EXTENSION && + list_member_oid(extension_list, foundDep->refobjid)) + { + is_shippable = true; + break; + } + } + + systable_endscan(scan); + relation_close(depRel, RowExclusiveLock); + + return is_shippable; +} + +/* + * is_shippable + * Is this object (proc/op/type) shippable to foreign server? + * Check cache first, then look-up whether (proc/op/type) is + * part of a declared extension if it is not cached. + */ +bool +is_shippable(Oid objnumber, Oid classnumber, ForeignServer *server, List *extension_list) +{ + ShippableCacheKey key; + ShippableCacheEntry *entry; + + /* Always return false if the user hasn't set the "extensions" option */ + if (extension_list == NIL) + return false; + + /* Find existing cache, if any. */ + if (!ShippableCacheHash) + InitializeShippableCache(); + + /* Zero out the key */ + MemSet(&key, 0, sizeof(key)); + + key.objid = objnumber; + key.classid = classnumber; + key.serverid = server->serverid; + + entry = (ShippableCacheEntry *) + hash_search(ShippableCacheHash, + (void *) &key, + HASH_FIND, + NULL); + + /* Not found in ShippableCacheHash cache. Construct new entry. */ + if (!entry) + { + /* + * Right now "shippability" is exclusively a function of whether + * the obj (proc/op/type) is in an extension declared by the user. + * In the future we could additionally have a whitelist of functions + * declared one at a time. + */ + bool shippable = lookup_shippable(objnumber, classnumber, extension_list); + + /* + * Don't create a new hash entry until *after* we have the shippable + * result in hand, as the shippable lookup might trigger a cache + * invalidation. + */ + entry = (ShippableCacheEntry *) + hash_search(ShippableCacheHash, + (void *) &key, + HASH_ENTER, + NULL); + + entry->shippable = shippable; + } + + if (!entry) + return false; + else + return entry->shippable; +} diff --git a/contrib/postgres_fdw/sql/shippable.sql b/contrib/postgres_fdw/sql/shippable.sql new file mode 100644 index 0000000000000..d3e03c219bcda --- /dev/null +++ b/contrib/postgres_fdw/sql/shippable.sql @@ -0,0 +1,133 @@ +-- =================================================================== +-- create FDW objects +-- =================================================================== + +-- Error, extension isn't installed yet +ALTER SERVER loopback OPTIONS (ADD extensions 'cube'); + +-- Try again +CREATE EXTENSION cube; +ALTER SERVER loopback OPTIONS (ADD extensions 'cube'); +ALTER SERVER loopback OPTIONS (DROP extensions); + + +-- =================================================================== +-- create objects used through FDW loopback server +-- =================================================================== + +CREATE SCHEMA "SH 1"; +CREATE TABLE "SH 1"."TBL 1" ( + "C 1" int NOT NULL, + c2 int NOT NULL, + c3 cube, + c4 timestamptz +); + +INSERT INTO "SH 1"."TBL 1" + SELECT id, + 2 * id, + cube(id,2*id), + '1970-01-01'::timestamptz + ((id % 100) || ' days')::interval + FROM generate_series(1, 1000) id; + +ANALYZE "SH 1"."TBL 1"; + +-- =================================================================== +-- create foreign table +-- =================================================================== + +CREATE FOREIGN TABLE shft1 ( + "C 1" int NOT NULL, + c2 int NOT NULL, + c3 cube, + c4 timestamptz +) SERVER loopback +OPTIONS (schema_name 'SH 1', table_name 'TBL 1'); + +-- =================================================================== +-- simple queries +-- =================================================================== + +-- without operator shipping +EXPLAIN (COSTS false) SELECT * FROM shft1 LIMIT 1; +EXPLAIN VERBOSE SELECT c2 FROM shft1 WHERE c3 && cube(1.5,2.5); +SELECT c2 FROM shft1 WHERE c3 && cube(1.5,2.5); +EXPLAIN VERBOSE SELECT c2 FROM shft1 WHERE c3 && '(1.5),(2.5)'::cube; + +-- with operator shipping +ALTER SERVER loopback OPTIONS (ADD extensions 'cube'); +EXPLAIN VERBOSE SELECT c2 FROM shft1 WHERE c3 && cube(1.5,2.5); +SELECT c2 FROM shft1 WHERE c3 && cube(1.5,2.5); +EXPLAIN VERBOSE SELECT c2 FROM shft1 WHERE c3 && '(1.5),(2.5)'::cube; +EXPLAIN VERBOSE SELECT cube_dim(c3) FROM shft1 WHERE c3 && '(1.5),(2.5)'::cube; +SELECT cube_dim(c3) FROM shft1 WHERE c3 && '(1.5),(2.5)'::cube; + +EXPLAIN VERBOSE SELECT c2 FROM shft1 WHERE cube_dim(c3) = 1 LIMIT 2; +SELECT c2 FROM shft1 WHERE cube_dim(c3) = 1 LIMIT 2; + +-- =================================================================== +-- add a second server with different extension shipping +-- =================================================================== + +DO $d$ + BEGIN + EXECUTE $$CREATE SERVER loopback_two FOREIGN DATA WRAPPER postgres_fdw + OPTIONS (dbname '$$||current_database()||$$', + port '$$||current_setting('port')||$$' + )$$; + END; +$d$; + +CREATE USER MAPPING FOR CURRENT_USER SERVER loopback_two; + +CREATE EXTENSION seg; + +CREATE TABLE seg_local ( + id integer, + s seg, + n text +); + +INSERT INTO seg_local (id, s, n) VALUES (1, '1.0 .. 2.0', 'foo'); +INSERT INTO seg_local (id, s, n) VALUES (2, '3.0 .. 4.0', 'bar'); +INSERT INTO seg_local (id, s, n) VALUES (3, '5.0 .. 6.0', 'baz'); + +ANALYZE seg_local; + +CREATE FOREIGN TABLE seg_remote_two ( + id integer, + s seg, + n text +) SERVER loopback_two +OPTIONS (table_name 'seg_local'); + +SELECT id FROM seg_local WHERE s && '5.8 .. 6.2'::seg AND n = 'baz'; +EXPLAIN VERBOSE SELECT id FROM seg_remote_two WHERE s && '5.8 .. 6.2'::seg AND n = 'baz'; +ALTER SERVER loopback_two OPTIONS (ADD extensions 'seg'); +EXPLAIN VERBOSE SELECT id FROM seg_remote_two WHERE s && '5.8 .. 6.2'::seg AND n = 'baz'; + +CREATE FOREIGN TABLE seg_remote_one ( + id integer, + s seg, + n text +) SERVER loopback +OPTIONS (table_name 'seg_local'); + +SELECT id FROM seg_remote_one WHERE s && '5.8 .. 6.2'::seg AND n = 'baz'; +EXPLAIN VERBOSE SELECT id FROM seg_remote_one WHERE s && '5.8 .. 6.2'::seg AND n = 'baz'; +EXPLAIN VERBOSE SELECT id FROM seg_remote_two WHERE s && '5.8 .. 6.2'::seg AND n = 'baz'; + + +-- =================================================================== +-- clean up +-- =================================================================== +DROP FOREIGN TABLE seg_remote_one, seg_remote_two; +DROP USER MAPPING FOR CURRENT_USER SERVER loopback_two; +DROP SERVER loopback_two; +DROP TABLE seg_local; +DROP FOREIGN TABLE shft1; +DROP TABLE "SH 1"."TBL 1"; +DROP SCHEMA "SH 1"; +DROP EXTENSION cube; +DROP EXTENSION seg; +ALTER SERVER loopback OPTIONS (DROP extensions); diff --git a/doc/src/sgml/postgres-fdw.sgml b/doc/src/sgml/postgres-fdw.sgml index 7c922821e988f..1e7ec08c9b9db 100644 --- a/doc/src/sgml/postgres-fdw.sgml +++ b/doc/src/sgml/postgres-fdw.sgml @@ -373,6 +373,38 @@ foreign tables, see . + + + Extension Options + + + By default only built-in operators and functions will be sent from the + local to the foreign server. This may be overridden using the following + option: + + + + + + extensions + + + This option controls the list of extensions that are expected to be + installed on the foreign server, using a comma-separated list of + extension names. Those extensions are also expected to be installed + on the local server too. This option is available for servers. + + +CREATE SERVER foreign_server + FOREIGN DATA WRAPPER postgres_fdw + OPTIONS (host '127.0.0.1', port '5432', dbname 'my_db', extensions 'cube, seg'); + + + + + + +