diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c index 322138dd0cd..2045774f24f 100644 --- a/contrib/postgres_fdw/deparse.c +++ b/contrib/postgres_fdw/deparse.c @@ -116,7 +116,6 @@ static void deparseReturningList(StringInfo buf, PlannerInfo *root, static void deparseColumnRef(StringInfo buf, int varno, int varattno, PlannerInfo *root); static void deparseRelation(StringInfo buf, Relation rel); -static void deparseStringLiteral(StringInfo buf, const char *val); static void deparseExpr(Expr *expr, deparse_expr_cxt *context); static void deparseVar(Var *node, deparse_expr_cxt *context); static void deparseConst(Const *node, deparse_expr_cxt *context); @@ -1160,7 +1159,7 @@ deparseRelation(StringInfo buf, Relation rel) /* * Append a SQL string literal representing "val" to buf. */ -static void +void deparseStringLiteral(StringInfo buf, const char *val) { const char *valptr; diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out index 2e49ee317a2..7eead58cffb 100644 --- a/contrib/postgres_fdw/expected/postgres_fdw.out +++ b/contrib/postgres_fdw/expected/postgres_fdw.out @@ -2834,3 +2834,233 @@ NOTICE: NEW: (13,"test triggered !") (0,27) (1 row) +-- =================================================================== +-- test IMPORT FOREIGN SCHEMA +-- =================================================================== +CREATE SCHEMA import_source; +CREATE TABLE import_source.t1 (c1 int, c2 varchar NOT NULL); +CREATE TABLE import_source.t2 (c1 int default 42, c2 varchar NULL, c3 text collate "POSIX"); +CREATE TYPE typ1 AS (m1 int, m2 varchar); +CREATE TABLE import_source.t3 (c1 timestamptz default now(), c2 typ1); +CREATE TABLE import_source."x 4" (c1 float8, "C 2" text, c3 varchar(42)); +CREATE TABLE import_source."x 5" (c1 float8); +ALTER TABLE import_source."x 5" DROP COLUMN c1; +CREATE SCHEMA import_dest1; +IMPORT FOREIGN SCHEMA import_source FROM SERVER loopback INTO import_dest1; +\det+ import_dest1 + List of foreign tables + Schema | Table | Server | FDW Options | Description +--------------+-------+----------+-------------------------------------------------+------------- + import_dest1 | t1 | loopback | (schema_name 'import_source', table_name 't1') | + import_dest1 | t2 | loopback | (schema_name 'import_source', table_name 't2') | + import_dest1 | t3 | loopback | (schema_name 'import_source', table_name 't3') | + import_dest1 | x 4 | loopback | (schema_name 'import_source', table_name 'x 4') | + import_dest1 | x 5 | loopback | (schema_name 'import_source', table_name 'x 5') | +(5 rows) + +\d import_dest1.* + Foreign table "import_dest1.t1" + Column | Type | Modifiers | FDW Options +--------+-------------------+-----------+-------------------- + c1 | integer | | (column_name 'c1') + c2 | character varying | not null | (column_name 'c2') +Server: loopback +FDW Options: (schema_name 'import_source', table_name 't1') + + Foreign table "import_dest1.t2" + Column | Type | Modifiers | FDW Options +--------+-------------------+---------------+-------------------- + c1 | integer | | (column_name 'c1') + c2 | character varying | | (column_name 'c2') + c3 | text | collate POSIX | (column_name 'c3') +Server: loopback +FDW Options: (schema_name 'import_source', table_name 't2') + + Foreign table "import_dest1.t3" + Column | Type | Modifiers | FDW Options +--------+--------------------------+-----------+-------------------- + c1 | timestamp with time zone | | (column_name 'c1') + c2 | typ1 | | (column_name 'c2') +Server: loopback +FDW Options: (schema_name 'import_source', table_name 't3') + + Foreign table "import_dest1.x 4" + Column | Type | Modifiers | FDW Options +--------+-----------------------+-----------+--------------------- + c1 | double precision | | (column_name 'c1') + C 2 | text | | (column_name 'C 2') + c3 | character varying(42) | | (column_name 'c3') +Server: loopback +FDW Options: (schema_name 'import_source', table_name 'x 4') + + Foreign table "import_dest1.x 5" + Column | Type | Modifiers | FDW Options +--------+------+-----------+------------- +Server: loopback +FDW Options: (schema_name 'import_source', table_name 'x 5') + +-- Options +CREATE SCHEMA import_dest2; +IMPORT FOREIGN SCHEMA import_source FROM SERVER loopback INTO import_dest2 + OPTIONS (import_default 'true'); +\det+ import_dest2 + List of foreign tables + Schema | Table | Server | FDW Options | Description +--------------+-------+----------+-------------------------------------------------+------------- + import_dest2 | t1 | loopback | (schema_name 'import_source', table_name 't1') | + import_dest2 | t2 | loopback | (schema_name 'import_source', table_name 't2') | + import_dest2 | t3 | loopback | (schema_name 'import_source', table_name 't3') | + import_dest2 | x 4 | loopback | (schema_name 'import_source', table_name 'x 4') | + import_dest2 | x 5 | loopback | (schema_name 'import_source', table_name 'x 5') | +(5 rows) + +\d import_dest2.* + Foreign table "import_dest2.t1" + Column | Type | Modifiers | FDW Options +--------+-------------------+-----------+-------------------- + c1 | integer | | (column_name 'c1') + c2 | character varying | not null | (column_name 'c2') +Server: loopback +FDW Options: (schema_name 'import_source', table_name 't1') + + Foreign table "import_dest2.t2" + Column | Type | Modifiers | FDW Options +--------+-------------------+---------------+-------------------- + c1 | integer | default 42 | (column_name 'c1') + c2 | character varying | | (column_name 'c2') + c3 | text | collate POSIX | (column_name 'c3') +Server: loopback +FDW Options: (schema_name 'import_source', table_name 't2') + + Foreign table "import_dest2.t3" + Column | Type | Modifiers | FDW Options +--------+--------------------------+---------------+-------------------- + c1 | timestamp with time zone | default now() | (column_name 'c1') + c2 | typ1 | | (column_name 'c2') +Server: loopback +FDW Options: (schema_name 'import_source', table_name 't3') + + Foreign table "import_dest2.x 4" + Column | Type | Modifiers | FDW Options +--------+-----------------------+-----------+--------------------- + c1 | double precision | | (column_name 'c1') + C 2 | text | | (column_name 'C 2') + c3 | character varying(42) | | (column_name 'c3') +Server: loopback +FDW Options: (schema_name 'import_source', table_name 'x 4') + + Foreign table "import_dest2.x 5" + Column | Type | Modifiers | FDW Options +--------+------+-----------+------------- +Server: loopback +FDW Options: (schema_name 'import_source', table_name 'x 5') + +CREATE SCHEMA import_dest3; +IMPORT FOREIGN SCHEMA import_source FROM SERVER loopback INTO import_dest3 + OPTIONS (import_collate 'false', import_not_null 'false'); +\det+ import_dest3 + List of foreign tables + Schema | Table | Server | FDW Options | Description +--------------+-------+----------+-------------------------------------------------+------------- + import_dest3 | t1 | loopback | (schema_name 'import_source', table_name 't1') | + import_dest3 | t2 | loopback | (schema_name 'import_source', table_name 't2') | + import_dest3 | t3 | loopback | (schema_name 'import_source', table_name 't3') | + import_dest3 | x 4 | loopback | (schema_name 'import_source', table_name 'x 4') | + import_dest3 | x 5 | loopback | (schema_name 'import_source', table_name 'x 5') | +(5 rows) + +\d import_dest3.* + Foreign table "import_dest3.t1" + Column | Type | Modifiers | FDW Options +--------+-------------------+-----------+-------------------- + c1 | integer | | (column_name 'c1') + c2 | character varying | | (column_name 'c2') +Server: loopback +FDW Options: (schema_name 'import_source', table_name 't1') + + Foreign table "import_dest3.t2" + Column | Type | Modifiers | FDW Options +--------+-------------------+-----------+-------------------- + c1 | integer | | (column_name 'c1') + c2 | character varying | | (column_name 'c2') + c3 | text | | (column_name 'c3') +Server: loopback +FDW Options: (schema_name 'import_source', table_name 't2') + + Foreign table "import_dest3.t3" + Column | Type | Modifiers | FDW Options +--------+--------------------------+-----------+-------------------- + c1 | timestamp with time zone | | (column_name 'c1') + c2 | typ1 | | (column_name 'c2') +Server: loopback +FDW Options: (schema_name 'import_source', table_name 't3') + + Foreign table "import_dest3.x 4" + Column | Type | Modifiers | FDW Options +--------+-----------------------+-----------+--------------------- + c1 | double precision | | (column_name 'c1') + C 2 | text | | (column_name 'C 2') + c3 | character varying(42) | | (column_name 'c3') +Server: loopback +FDW Options: (schema_name 'import_source', table_name 'x 4') + + Foreign table "import_dest3.x 5" + Column | Type | Modifiers | FDW Options +--------+------+-----------+------------- +Server: loopback +FDW Options: (schema_name 'import_source', table_name 'x 5') + +-- Check LIMIT TO and EXCEPT +CREATE SCHEMA import_dest4; +IMPORT FOREIGN SCHEMA import_source LIMIT TO (t1, nonesuch) + FROM SERVER loopback INTO import_dest4; +\det+ import_dest4 + List of foreign tables + Schema | Table | Server | FDW Options | Description +--------------+-------+----------+------------------------------------------------+------------- + import_dest4 | t1 | loopback | (schema_name 'import_source', table_name 't1') | +(1 row) + +IMPORT FOREIGN SCHEMA import_source EXCEPT (t1, "x 4", nonesuch) + FROM SERVER loopback INTO import_dest4; +\det+ import_dest4 + List of foreign tables + Schema | Table | Server | FDW Options | Description +--------------+-------+----------+-------------------------------------------------+------------- + import_dest4 | t1 | loopback | (schema_name 'import_source', table_name 't1') | + import_dest4 | t2 | loopback | (schema_name 'import_source', table_name 't2') | + import_dest4 | t3 | loopback | (schema_name 'import_source', table_name 't3') | + import_dest4 | x 5 | loopback | (schema_name 'import_source', table_name 'x 5') | +(4 rows) + +-- Assorted error cases +IMPORT FOREIGN SCHEMA import_source FROM SERVER loopback INTO import_dest4; +ERROR: relation "t1" already exists +CONTEXT: importing foreign table "t1" +IMPORT FOREIGN SCHEMA nonesuch FROM SERVER loopback INTO import_dest4; +ERROR: schema "nonesuch" is not present on foreign server "loopback" +IMPORT FOREIGN SCHEMA nonesuch FROM SERVER loopback INTO notthere; +ERROR: schema "notthere" does not exist +IMPORT FOREIGN SCHEMA nonesuch FROM SERVER nowhere INTO notthere; +ERROR: server "nowhere" does not exist +-- Check case of a type present only on the remote server. +-- We can fake this by dropping the type locally in our transaction. +CREATE TYPE "Colors" AS ENUM ('red', 'green', 'blue'); +CREATE TABLE import_source.t5 (c1 int, c2 text collate "C", "Col" "Colors"); +CREATE SCHEMA import_dest5; +BEGIN; +DROP TYPE "Colors" CASCADE; +NOTICE: drop cascades to table import_source.t5 column Col +IMPORT FOREIGN SCHEMA import_source LIMIT TO (t5) + FROM SERVER loopback INTO import_dest5; -- ERROR +ERROR: type "public.Colors" does not exist +LINE 4: "Col" public."Colors" OPTIONS (column_name 'Col') + ^ +QUERY: CREATE FOREIGN TABLE t5 ( + c1 integer OPTIONS (column_name 'c1'), + c2 text OPTIONS (column_name 'c2') COLLATE pg_catalog."C", + "Col" public."Colors" OPTIONS (column_name 'Col') +) SERVER loopback +OPTIONS (schema_name 'import_source', table_name 't5'); +CONTEXT: importing foreign table "t5" +ROLLBACK; diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c index 56374905f5b..19debfb5c9e 100644 --- a/contrib/postgres_fdw/postgres_fdw.c +++ b/contrib/postgres_fdw/postgres_fdw.c @@ -285,6 +285,8 @@ static void postgresExplainForeignModify(ModifyTableState *mtstate, static bool postgresAnalyzeForeignTable(Relation relation, AcquireSampleRowsFunc *func, BlockNumber *totalpages); +static List *postgresImportForeignSchema(ImportForeignSchemaStmt *stmt, + Oid serverOid); /* * Helper functions @@ -362,6 +364,9 @@ postgres_fdw_handler(PG_FUNCTION_ARGS) /* Support functions for ANALYZE */ routine->AnalyzeForeignTable = postgresAnalyzeForeignTable; + /* Support functions for IMPORT FOREIGN SCHEMA */ + routine->ImportForeignSchema = postgresImportForeignSchema; + PG_RETURN_POINTER(routine); } @@ -2563,6 +2568,270 @@ analyze_row_processor(PGresult *res, int row, PgFdwAnalyzeState *astate) } } +/* + * Import a foreign schema + */ +static List * +postgresImportForeignSchema(ImportForeignSchemaStmt *stmt, Oid serverOid) +{ + List *commands = NIL; + bool import_collate = true; + bool import_default = false; + bool import_not_null = true; + ForeignServer *server; + UserMapping *mapping; + PGconn *conn; + StringInfoData buf; + PGresult *volatile res = NULL; + int numrows, + i; + ListCell *lc; + + /* Parse statement options */ + foreach(lc, stmt->options) + { + DefElem *def = (DefElem *) lfirst(lc); + + if (strcmp(def->defname, "import_collate") == 0) + import_collate = defGetBoolean(def); + else if (strcmp(def->defname, "import_default") == 0) + import_default = defGetBoolean(def); + else if (strcmp(def->defname, "import_not_null") == 0) + import_not_null = defGetBoolean(def); + else + ereport(ERROR, + (errcode(ERRCODE_FDW_INVALID_OPTION_NAME), + errmsg("invalid option \"%s\"", def->defname))); + } + + /* + * Get connection to the foreign server. Connection manager will + * establish new connection if necessary. + */ + server = GetForeignServer(serverOid); + mapping = GetUserMapping(GetUserId(), server->serverid); + conn = GetConnection(server, mapping, false); + + /* Don't attempt to import collation if remote server hasn't got it */ + if (PQserverVersion(conn) < 90100) + import_collate = false; + + /* Create workspace for strings */ + initStringInfo(&buf); + + /* In what follows, do not risk leaking any PGresults. */ + PG_TRY(); + { + /* Check that the schema really exists */ + appendStringInfoString(&buf, "SELECT 1 FROM pg_catalog.pg_namespace WHERE nspname = "); + deparseStringLiteral(&buf, stmt->remote_schema); + + res = PQexec(conn, buf.data); + if (PQresultStatus(res) != PGRES_TUPLES_OK) + pgfdw_report_error(ERROR, res, conn, false, buf.data); + + if (PQntuples(res) != 1) + ereport(ERROR, + (errcode(ERRCODE_FDW_SCHEMA_NOT_FOUND), + errmsg("schema \"%s\" is not present on foreign server \"%s\"", + stmt->remote_schema, server->servername))); + + PQclear(res); + res = NULL; + resetStringInfo(&buf); + + /* + * Fetch all table data from this schema, possibly restricted by + * EXCEPT or LIMIT TO. + * + * Note: because we run the connection with search_path restricted to + * pg_catalog, the format_type() and pg_get_expr() outputs will always + * include a schema name for types/functions in other schemas, which + * is what we want. + */ + if (import_collate) + appendStringInfoString(&buf, + "SELECT relname, " + " attname, " + " format_type(atttypid, atttypmod), " + " attnotnull, " + " pg_get_expr(adbin, adrelid), " + " collname, " + " collnsp.nspname " + "FROM pg_class c " + " JOIN pg_namespace n ON " + " relnamespace = n.oid " + " LEFT JOIN pg_attribute a ON " + " attrelid = c.oid AND attnum > 0 " + " AND NOT attisdropped " + " LEFT JOIN pg_attrdef ad ON " + " adrelid = c.oid AND adnum = attnum " + " LEFT JOIN pg_collation coll ON " + " coll.oid = attcollation " + " LEFT JOIN pg_namespace collnsp ON " + " collnsp.oid = collnamespace "); + else + appendStringInfoString(&buf, + "SELECT relname, " + " attname, " + " format_type(atttypid, atttypmod), " + " attnotnull, " + " pg_get_expr(adbin, adrelid), " + " NULL, NULL " + "FROM pg_class c " + " JOIN pg_namespace n ON " + " relnamespace = n.oid " + " LEFT JOIN pg_attribute a ON " + " attrelid = c.oid AND attnum > 0 " + " AND NOT attisdropped " + " LEFT JOIN pg_attrdef ad ON " + " adrelid = c.oid AND adnum = attnum "); + + appendStringInfoString(&buf, + "WHERE c.relkind IN ('r', 'v', 'f', 'm') " + " AND n.nspname = "); + deparseStringLiteral(&buf, stmt->remote_schema); + + /* Apply restrictions for LIMIT TO and EXCEPT */ + if (stmt->list_type == FDW_IMPORT_SCHEMA_LIMIT_TO || + stmt->list_type == FDW_IMPORT_SCHEMA_EXCEPT) + { + bool first_item = true; + + appendStringInfoString(&buf, " AND c.relname "); + if (stmt->list_type == FDW_IMPORT_SCHEMA_EXCEPT) + appendStringInfoString(&buf, "NOT "); + appendStringInfoString(&buf, "IN ("); + + /* Append list of table names within IN clause */ + foreach(lc, stmt->table_list) + { + RangeVar *rv = (RangeVar *) lfirst(lc); + + if (first_item) + first_item = false; + else + appendStringInfoString(&buf, ", "); + deparseStringLiteral(&buf, rv->relname); + } + appendStringInfoString(&buf, ")"); + } + + /* Append ORDER BY at the end of query to ensure output ordering */ + appendStringInfo(&buf, " ORDER BY c.relname, a.attnum"); + + /* Fetch the data */ + res = PQexec(conn, buf.data); + if (PQresultStatus(res) != PGRES_TUPLES_OK) + pgfdw_report_error(ERROR, res, conn, false, buf.data); + + /* Process results */ + numrows = PQntuples(res); + /* note: incrementation of i happens in inner loop's while() test */ + for (i = 0; i < numrows;) + { + char *tablename = PQgetvalue(res, i, 0); + bool first_item = true; + + resetStringInfo(&buf); + appendStringInfo(&buf, "CREATE FOREIGN TABLE %s (\n", + quote_identifier(tablename)); + + /* Scan all rows for this table */ + do + { + char *attname; + char *typename; + char *attnotnull; + char *attdefault; + char *collname; + char *collnamespace; + + /* If table has no columns, we'll see nulls here */ + if (PQgetisnull(res, i, 1)) + continue; + + attname = PQgetvalue(res, i, 1); + typename = PQgetvalue(res, i, 2); + attnotnull = PQgetvalue(res, i, 3); + attdefault = PQgetisnull(res, i, 4) ? (char *) NULL : + PQgetvalue(res, i, 4); + collname = PQgetisnull(res, i, 5) ? (char *) NULL : + PQgetvalue(res, i, 5); + collnamespace = PQgetisnull(res, i, 6) ? (char *) NULL : + PQgetvalue(res, i, 6); + + if (first_item) + first_item = false; + else + appendStringInfoString(&buf, ",\n"); + + /* Print column name and type */ + appendStringInfo(&buf, " %s %s", + quote_identifier(attname), + typename); + + /* + * Add column_name option so that renaming the foreign table's + * column doesn't break the association to the underlying + * column. + */ + appendStringInfoString(&buf, " OPTIONS (column_name "); + deparseStringLiteral(&buf, attname); + appendStringInfoString(&buf, ")"); + + /* Add COLLATE if needed */ + if (import_collate && collname != NULL && collnamespace != NULL) + appendStringInfo(&buf, " COLLATE %s.%s", + quote_identifier(collnamespace), + quote_identifier(collname)); + + /* Add DEFAULT if needed */ + if (import_default && attdefault != NULL) + appendStringInfo(&buf, " DEFAULT %s", attdefault); + + /* Add NOT NULL if needed */ + if (import_not_null && attnotnull[0] == 't') + appendStringInfoString(&buf, " NOT NULL"); + } + while (++i < numrows && + strcmp(PQgetvalue(res, i, 0), tablename) == 0); + + /* + * Add server name and table-level options. We specify remote + * schema and table name as options (the latter to ensure that + * renaming the foreign table doesn't break the association). + */ + appendStringInfo(&buf, "\n) SERVER %s\nOPTIONS (", + quote_identifier(server->servername)); + + appendStringInfoString(&buf, "schema_name "); + deparseStringLiteral(&buf, stmt->remote_schema); + appendStringInfoString(&buf, ", table_name "); + deparseStringLiteral(&buf, tablename); + + appendStringInfoString(&buf, ");"); + + commands = lappend(commands, pstrdup(buf.data)); + } + + /* Clean up */ + PQclear(res); + res = NULL; + } + PG_CATCH(); + { + if (res) + PQclear(res); + PG_RE_THROW(); + } + PG_END_TRY(); + + ReleaseConnection(conn); + + return commands; +} + /* * Create a tuple from the specified row of the PGresult. * diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h index 8aa8f1a1b58..94eadae8916 100644 --- a/contrib/postgres_fdw/postgres_fdw.h +++ b/contrib/postgres_fdw/postgres_fdw.h @@ -73,5 +73,6 @@ extern void deparseDeleteSql(StringInfo buf, PlannerInfo *root, extern void deparseAnalyzeSizeSql(StringInfo buf, Relation rel); extern void deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs); +extern void deparseStringLiteral(StringInfo buf, const char *val); #endif /* POSTGRES_FDW_H */ diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql index 6187839453c..9f54359be58 100644 --- a/contrib/postgres_fdw/sql/postgres_fdw.sql +++ b/contrib/postgres_fdw/sql/postgres_fdw.sql @@ -609,3 +609,60 @@ UPDATE rem1 SET f2 = 'testo'; -- Test returning a system attribute INSERT INTO rem1(f2) VALUES ('test') RETURNING ctid; + +-- =================================================================== +-- test IMPORT FOREIGN SCHEMA +-- =================================================================== + +CREATE SCHEMA import_source; +CREATE TABLE import_source.t1 (c1 int, c2 varchar NOT NULL); +CREATE TABLE import_source.t2 (c1 int default 42, c2 varchar NULL, c3 text collate "POSIX"); +CREATE TYPE typ1 AS (m1 int, m2 varchar); +CREATE TABLE import_source.t3 (c1 timestamptz default now(), c2 typ1); +CREATE TABLE import_source."x 4" (c1 float8, "C 2" text, c3 varchar(42)); +CREATE TABLE import_source."x 5" (c1 float8); +ALTER TABLE import_source."x 5" DROP COLUMN c1; + +CREATE SCHEMA import_dest1; +IMPORT FOREIGN SCHEMA import_source FROM SERVER loopback INTO import_dest1; +\det+ import_dest1 +\d import_dest1.* + +-- Options +CREATE SCHEMA import_dest2; +IMPORT FOREIGN SCHEMA import_source FROM SERVER loopback INTO import_dest2 + OPTIONS (import_default 'true'); +\det+ import_dest2 +\d import_dest2.* +CREATE SCHEMA import_dest3; +IMPORT FOREIGN SCHEMA import_source FROM SERVER loopback INTO import_dest3 + OPTIONS (import_collate 'false', import_not_null 'false'); +\det+ import_dest3 +\d import_dest3.* + +-- Check LIMIT TO and EXCEPT +CREATE SCHEMA import_dest4; +IMPORT FOREIGN SCHEMA import_source LIMIT TO (t1, nonesuch) + FROM SERVER loopback INTO import_dest4; +\det+ import_dest4 +IMPORT FOREIGN SCHEMA import_source EXCEPT (t1, "x 4", nonesuch) + FROM SERVER loopback INTO import_dest4; +\det+ import_dest4 + +-- Assorted error cases +IMPORT FOREIGN SCHEMA import_source FROM SERVER loopback INTO import_dest4; +IMPORT FOREIGN SCHEMA nonesuch FROM SERVER loopback INTO import_dest4; +IMPORT FOREIGN SCHEMA nonesuch FROM SERVER loopback INTO notthere; +IMPORT FOREIGN SCHEMA nonesuch FROM SERVER nowhere INTO notthere; + +-- Check case of a type present only on the remote server. +-- We can fake this by dropping the type locally in our transaction. +CREATE TYPE "Colors" AS ENUM ('red', 'green', 'blue'); +CREATE TABLE import_source.t5 (c1 int, c2 text collate "C", "Col" "Colors"); + +CREATE SCHEMA import_dest5; +BEGIN; +DROP TYPE "Colors" CASCADE; +IMPORT FOREIGN SCHEMA import_source LIMIT TO (t5) + FROM SERVER loopback INTO import_dest5; -- ERROR +ROLLBACK; diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index 8ace8bd3a25..3b7fff4846c 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -3069,8 +3069,9 @@ ANALYZE measurement; For additional information, see , , - , and - . + , + , and + . diff --git a/doc/src/sgml/event-trigger.sgml b/doc/src/sgml/event-trigger.sgml index e5b9e661853..3db8ef1a132 100644 --- a/doc/src/sgml/event-trigger.sgml +++ b/doc/src/sgml/event-trigger.sgml @@ -603,6 +603,12 @@ X X + + IMPORT FOREIGN SCHEMA + X + X + - + SELECT INTO X diff --git a/doc/src/sgml/fdwhandler.sgml b/doc/src/sgml/fdwhandler.sgml index 6b5c8b7e97b..5fd8d6fbbe9 100644 --- a/doc/src/sgml/fdwhandler.sgml +++ b/doc/src/sgml/fdwhandler.sgml @@ -696,6 +696,66 @@ AcquireSampleRowsFunc (Relation relation, int elevel, + + FDW Routines For <command>IMPORT FOREIGN SCHEMA</> + + + +List * +ImportForeignSchema (ImportForeignSchemaStmt *stmt, Oid serverOid); + + + Obtain a list of foreign table creation commands. This function is + called when executing , and is + passed the parse tree for that statement, as well as the OID of the + foreign server to use. It should return a list of C strings, each of + which must contain a command. + These strings will be parsed and executed by the core server. + + + + Within the ImportForeignSchemaStmt struct, + remote_schema is the name of the remote schema from + which tables are to be imported. + list_type identifies how to filter table names: + FDW_IMPORT_SCHEMA_ALL means that all tables in the remote + schema should be imported (in this case table_list is + empty), FDW_IMPORT_SCHEMA_LIMIT_TO means to include only + tables listed in table_list, + and FDW_IMPORT_SCHEMA_EXCEPT means to exclude the tables + listed in table_list. + options is a list of options used for the import process. + The meanings of the options are up to the FDW. + For example, an FDW could use an option to define whether the + NOT NULL attributes of columns should be imported. + These options need not have anything to do with those supported by the + FDW as database object options. + + + + The FDW may ignore the local_schema field of + the ImportForeignSchemaStmt, because the core server + will automatically insert that name into the parsed CREATE + FOREIGN TABLE commands. + + + + The FDW does not have to concern itself with implementing the filtering + specified by list_type and table_list, + either, as the core server will automatically skip any returned commands + for tables excluded according to those options. However, it's often + useful to avoid the work of creating commands for excluded tables in the + first place. The function IsImportableForeignTable() may be + useful to test whether a given foreign-table name will pass the filter. + + + + If the FDW does not support importing table definitions, the + ImportForeignSchema pointer can be set to NULL. + + + + diff --git a/doc/src/sgml/postgres-fdw.sgml b/doc/src/sgml/postgres-fdw.sgml index e6f6e205815..43adb61455d 100644 --- a/doc/src/sgml/postgres-fdw.sgml +++ b/doc/src/sgml/postgres-fdw.sgml @@ -49,7 +49,8 @@ - Create a foreign table, using , + Create a foreign table, using + or , for each remote table you want to access. The columns of the foreign table must match the referenced remote table. You can, however, use table and/or column names different from the remote table's, if you @@ -99,7 +100,7 @@ user and password (specify these - for a user mapping, instead) + in a user mapping, instead) @@ -291,6 +292,72 @@ + + + Importing Options + + + postgres_fdw is able to import foreign table definitions + using . This command creates + foreign table definitions on the local server that match tables or + views present on the remote server. If the remote tables to be imported + have columns of user-defined data types, the local server must have types + of the same names. + + + + Importing behavior can be customized with the following options + (given in the IMPORT FOREIGN SCHEMA command): + + + + + import_collate + + + This option controls whether column COLLATE options + are included in the definitions of foreign tables imported + from a foreign server. The default is true. You might + need to turn this off if the remote server has a different set of + collation names than the local server does, which is likely to be the + case if it's running on a different operating system. + + + + + import_default + + + This option controls whether column DEFAULT expressions + are included in the definitions of foreign tables imported + from a foreign server. The default is false. If you + enable this option, be wary of defaults that might get computed + differently on the local server than they would be on the remote + server; nextval() is a common source of problems. + The IMPORT will fail altogether if an imported default + expression uses a function or operator that does not exist locally. + + + + + import_not_null + + + This option controls whether column NOT NULL + constraints are included in the definitions of foreign tables imported + from a foreign server. The default is true. + + + + + + + Note that constraints other than NOT NULL will never be + imported from the remote tables, since PostgreSQL + does not support any other type of constraint on a foreign table. + Checking other types of constraints is always left to the remote server. + + @@ -422,7 +489,7 @@ CREATE USER MAPPING FOR local_user CREATE FOREIGN TABLE foreign_table ( - id serial NOT NULL, + id integer NOT NULL, data text ) SERVER foreign_server @@ -434,6 +501,8 @@ CREATE FOREIGN TABLE foreign_table ( Column names must match as well, unless you attach column_name options to the individual columns to show how they are named in the remote table. + In many cases, use of is + preferable to constructing foreign table definitions manually. diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml index 1b0962c253d..b685e16a0fa 100644 --- a/doc/src/sgml/ref/allfiles.sgml +++ b/doc/src/sgml/ref/allfiles.sgml @@ -131,6 +131,7 @@ Complete list of usable sgml source files in this directory. + diff --git a/doc/src/sgml/ref/create_foreign_table.sgml b/doc/src/sgml/ref/create_foreign_table.sgml index 4a8cf3889c2..46a20eff14f 100644 --- a/doc/src/sgml/ref/create_foreign_table.sgml +++ b/doc/src/sgml/ref/create_foreign_table.sgml @@ -231,6 +231,7 @@ SERVER film_server; + diff --git a/doc/src/sgml/ref/import_foreign_schema.sgml b/doc/src/sgml/ref/import_foreign_schema.sgml new file mode 100644 index 00000000000..bdcc26558ac --- /dev/null +++ b/doc/src/sgml/ref/import_foreign_schema.sgml @@ -0,0 +1,168 @@ + + + + + IMPORT FOREIGN SCHEMA + + + + IMPORT FOREIGN SCHEMA + 7 + SQL - Language Statements + + + + IMPORT FOREIGN SCHEMA + import table definitions from a foreign server + + + + +IMPORT FOREIGN SCHEMA remote_schema +[ { LIMIT TO | EXCEPT } ( table_name [, ...] ) ] +FROM SERVER server_name +INTO local_schema +[ OPTIONS ( option 'value' [, ... ] ) ] + + + + + Description + + + IMPORT FOREIGN SCHEMA creates foreign tables that + represent tables existing on a foreign server. The new foreign tables + will be owned by the user issuing the command and are created with + the correct column definitions and options to match the remote tables. + + + + By default, all tables and views existing in a particular schema on the + foreign server are imported. Optionally, the list of tables can be limited + to a specified subset, or specific tables can be excluded. The new foreign + tables are all created in the target schema, which must already exist. + + + + To use IMPORT FOREIGN SCHEMA, the user must have + USAGE privilege on the foreign server, as well as + CREATE privilege on the target schema. + + + + + Parameters + + + + + remote_schema + + + The remote schema to import from. The specific meaning of a remote schema + depends on the foreign data wrapper in use. + + + + + + LIMIT TO ( table_name [, ...] ) + + + Import only foreign tables matching one of the given table names. + Other tables existing in the foreign schema will be ignored. + + + + + + EXCEPT ( table_name [, ...] ) + + + Exclude specified foreign tables from the import. All tables + existing in the foreign schema will be imported except the + ones listed here. + + + + + + server_name + + + The foreign server to import from. + + + + + + local_schema + + + The schema in which the imported foreign tables will be created. + + + + + + OPTIONS ( option 'value' [, ...] ) + + + Options to be used during the import. + The allowed option names and values are specific to each foreign + data wrapper. + + + + + + + + Examples + + + Import table definitions from a remote schema foreign_films + on server film_server, creating the foreign tables in + local schema films: + + +IMPORT FOREIGN SCHEMA foreign_films + FROM SERVER film_server INTO films; + + + + + As above, but import only the two tables actors and + directors (if they exist): + + +IMPORT FOREIGN SCHEMA foreign_films LIMIT TO (actors, directors) + FROM SERVER film_server INTO films; + + + + + + + Compatibility + + + The IMPORT FOREIGN SCHEMA command conforms to the + SQL standard, except that the OPTIONS + clause is a PostgreSQL extension. + + + + + + See Also + + + + + + + diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml index a6575f52ac0..6ec126381c3 100644 --- a/doc/src/sgml/reference.sgml +++ b/doc/src/sgml/reference.sgml @@ -159,6 +159,7 @@ &explain; &fetch; &grant; + &importForeignSchema; &insert; &listen; &load; diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c index 110fe004a46..754264eb3ee 100644 --- a/src/backend/commands/event_trigger.c +++ b/src/backend/commands/event_trigger.c @@ -250,7 +250,8 @@ check_ddl_tag(const char *tag) pg_strcasecmp(tag, "REFRESH MATERIALIZED VIEW") == 0 || pg_strcasecmp(tag, "ALTER DEFAULT PRIVILEGES") == 0 || pg_strcasecmp(tag, "ALTER LARGE OBJECT") == 0 || - pg_strcasecmp(tag, "DROP OWNED") == 0) + pg_strcasecmp(tag, "DROP OWNED") == 0 || + pg_strcasecmp(tag, "IMPORT FOREIGN SCHEMA") == 0) return EVENT_TRIGGER_COMMAND_TAG_OK; /* diff --git a/src/backend/commands/foreigncmds.c b/src/backend/commands/foreigncmds.c index 8ab9c439db2..ab4ed6c78ec 100644 --- a/src/backend/commands/foreigncmds.c +++ b/src/backend/commands/foreigncmds.c @@ -15,8 +15,8 @@ #include "access/heapam.h" #include "access/htup_details.h" -#include "access/xact.h" #include "access/reloptions.h" +#include "access/xact.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/objectaccess.h" @@ -27,9 +27,11 @@ #include "catalog/pg_type.h" #include "catalog/pg_user_mapping.h" #include "commands/defrem.h" +#include "foreign/fdwapi.h" #include "foreign/foreign.h" #include "miscadmin.h" #include "parser/parse_func.h" +#include "tcop/utility.h" #include "utils/acl.h" #include "utils/builtins.h" #include "utils/lsyscache.h" @@ -37,6 +39,16 @@ #include "utils/syscache.h" +typedef struct +{ + char *tablename; + char *cmd; +} import_error_callback_arg; + +/* Internal functions */ +static void import_error_callback(void *arg); + + /* * Convert a DefElem list to the text array format that is used in * pg_foreign_data_wrapper, pg_foreign_server, pg_user_mapping, and @@ -1427,3 +1439,133 @@ CreateForeignTable(CreateForeignTableStmt *stmt, Oid relid) heap_close(ftrel, RowExclusiveLock); } + +/* + * Import a foreign schema + */ +void +ImportForeignSchema(ImportForeignSchemaStmt *stmt) +{ + ForeignServer *server; + ForeignDataWrapper *fdw; + FdwRoutine *fdw_routine; + AclResult aclresult; + List *cmd_list; + ListCell *lc; + + /* Check that the foreign server exists and that we have USAGE on it */ + server = GetForeignServerByName(stmt->server_name, false); + aclresult = pg_foreign_server_aclcheck(server->serverid, GetUserId(), ACL_USAGE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, ACL_KIND_FOREIGN_SERVER, server->servername); + + /* Check that the schema exists and we have CREATE permissions on it */ + (void) LookupCreationNamespace(stmt->local_schema); + + /* Get the FDW and check it supports IMPORT */ + fdw = GetForeignDataWrapper(server->fdwid); + if (!OidIsValid(fdw->fdwhandler)) + ereport(ERROR, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("foreign-data wrapper \"%s\" has no handler", + fdw->fdwname))); + fdw_routine = GetFdwRoutine(fdw->fdwhandler); + if (fdw_routine->ImportForeignSchema == NULL) + ereport(ERROR, + (errcode(ERRCODE_FDW_NO_SCHEMAS), + errmsg("foreign-data wrapper \"%s\" does not support IMPORT FOREIGN SCHEMA", + fdw->fdwname))); + + /* Call FDW to get a list of commands */ + cmd_list = fdw_routine->ImportForeignSchema(stmt, server->serverid); + + /* Parse and execute each command */ + foreach(lc, cmd_list) + { + char *cmd = (char *) lfirst(lc); + import_error_callback_arg callback_arg; + ErrorContextCallback sqlerrcontext; + List *raw_parsetree_list; + ListCell *lc2; + + /* + * Setup error traceback support for ereport(). This is so that any + * error in the generated SQL will be displayed nicely. + */ + callback_arg.tablename = NULL; /* not known yet */ + callback_arg.cmd = cmd; + sqlerrcontext.callback = import_error_callback; + sqlerrcontext.arg = (void *) &callback_arg; + sqlerrcontext.previous = error_context_stack; + error_context_stack = &sqlerrcontext; + + /* + * Parse the SQL string into a list of raw parse trees. + */ + raw_parsetree_list = pg_parse_query(cmd); + + /* + * Process each parse tree (we allow the FDW to put more than one + * command per string, though this isn't really advised). + */ + foreach(lc2, raw_parsetree_list) + { + CreateForeignTableStmt *cstmt = lfirst(lc2); + + /* + * Because we only allow CreateForeignTableStmt, we can skip parse + * analysis, rewrite, and planning steps here. + */ + if (!IsA(cstmt, CreateForeignTableStmt)) + elog(ERROR, + "foreign-data wrapper \"%s\" returned incorrect statement type %d", + fdw->fdwname, (int) nodeTag(cstmt)); + + /* Ignore commands for tables excluded by filter options */ + if (!IsImportableForeignTable(cstmt->base.relation->relname, stmt)) + continue; + + /* Enable reporting of current table's name on error */ + callback_arg.tablename = cstmt->base.relation->relname; + + /* Ensure creation schema is the one given in IMPORT statement */ + cstmt->base.relation->schemaname = pstrdup(stmt->local_schema); + + /* Execute statement */ + ProcessUtility((Node *) cstmt, + cmd, + PROCESS_UTILITY_SUBCOMMAND, NULL, + None_Receiver, NULL); + + /* Be sure to advance the command counter between subcommands */ + CommandCounterIncrement(); + + callback_arg.tablename = NULL; + } + + error_context_stack = sqlerrcontext.previous; + } +} + +/* + * error context callback to let us supply the failing SQL statement's text + */ +static void +import_error_callback(void *arg) +{ + import_error_callback_arg *callback_arg = (import_error_callback_arg *) arg; + int syntaxerrposition; + + /* If it's a syntax error, convert to internal syntax error report */ + syntaxerrposition = geterrposition(); + if (syntaxerrposition > 0) + { + errposition(0); + internalerrposition(syntaxerrposition); + internalerrquery(callback_arg->cmd); + } + + if (callback_arg->tablename) + errcontext("importing foreign table \"%s\"", + callback_arg->tablename); +} diff --git a/src/backend/foreign/foreign.c b/src/backend/foreign/foreign.c index 6d548b7d08b..4f5f6ae362b 100644 --- a/src/backend/foreign/foreign.c +++ b/src/backend/foreign/foreign.c @@ -399,6 +399,47 @@ GetFdwRoutineForRelation(Relation relation, bool makecopy) } +/* + * IsImportableForeignTable - filter table names for IMPORT FOREIGN SCHEMA + * + * Returns TRUE if given table name should be imported according to the + * statement's import filter options. + */ +bool +IsImportableForeignTable(const char *tablename, + ImportForeignSchemaStmt *stmt) +{ + ListCell *lc; + + switch (stmt->list_type) + { + case FDW_IMPORT_SCHEMA_ALL: + return true; + + case FDW_IMPORT_SCHEMA_LIMIT_TO: + foreach(lc, stmt->table_list) + { + RangeVar *rv = (RangeVar *) lfirst(lc); + + if (strcmp(tablename, rv->relname) == 0) + return true; + } + return false; + + case FDW_IMPORT_SCHEMA_EXCEPT: + foreach(lc, stmt->table_list) + { + RangeVar *rv = (RangeVar *) lfirst(lc); + + if (strcmp(tablename, rv->relname) == 0) + return false; + } + return true; + } + return false; /* shouldn't get here */ +} + + /* * deflist_to_tuplestore - Helper function to convert DefElem list to * tuplestore usable in SRF. diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c index 8d3d5a7c734..30885789304 100644 --- a/src/backend/nodes/copyfuncs.c +++ b/src/backend/nodes/copyfuncs.c @@ -3567,6 +3567,21 @@ _copyCreateForeignTableStmt(const CreateForeignTableStmt *from) return newnode; } +static ImportForeignSchemaStmt * +_copyImportForeignSchemaStmt(const ImportForeignSchemaStmt *from) +{ + ImportForeignSchemaStmt *newnode = makeNode(ImportForeignSchemaStmt); + + COPY_STRING_FIELD(server_name); + COPY_STRING_FIELD(remote_schema); + COPY_STRING_FIELD(local_schema); + COPY_SCALAR_FIELD(list_type); + COPY_NODE_FIELD(table_list); + COPY_NODE_FIELD(options); + + return newnode; +} + static CreateTrigStmt * _copyCreateTrigStmt(const CreateTrigStmt *from) { @@ -4477,6 +4492,9 @@ copyObject(const void *from) case T_CreateForeignTableStmt: retval = _copyCreateForeignTableStmt(from); break; + case T_ImportForeignSchemaStmt: + retval = _copyImportForeignSchemaStmt(from); + break; case T_CreateTrigStmt: retval = _copyCreateTrigStmt(from); break; diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c index e7b49f680cf..1b07db69d73 100644 --- a/src/backend/nodes/equalfuncs.c +++ b/src/backend/nodes/equalfuncs.c @@ -1768,6 +1768,19 @@ _equalCreateForeignTableStmt(const CreateForeignTableStmt *a, const CreateForeig return true; } +static bool +_equalImportForeignSchemaStmt(const ImportForeignSchemaStmt *a, const ImportForeignSchemaStmt *b) +{ + COMPARE_STRING_FIELD(server_name); + COMPARE_STRING_FIELD(remote_schema); + COMPARE_STRING_FIELD(local_schema); + COMPARE_SCALAR_FIELD(list_type); + COMPARE_NODE_FIELD(table_list); + COMPARE_NODE_FIELD(options); + + return true; +} + static bool _equalCreateTrigStmt(const CreateTrigStmt *a, const CreateTrigStmt *b) { @@ -2943,6 +2956,9 @@ equal(const void *a, const void *b) case T_CreateForeignTableStmt: retval = _equalCreateForeignTableStmt(a, b); break; + case T_ImportForeignSchemaStmt: + retval = _equalImportForeignSchemaStmt(a, b); + break; case T_CreateTrigStmt: retval = _equalCreateTrigStmt(a, b); break; diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index c182212e62f..9573a9bb0e6 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -2011,6 +2011,19 @@ _outCreateForeignTableStmt(StringInfo str, const CreateForeignTableStmt *node) WRITE_NODE_FIELD(options); } +static void +_outImportForeignSchemaStmt(StringInfo str, const ImportForeignSchemaStmt *node) +{ + WRITE_NODE_TYPE("IMPORTFOREIGNSCHEMASTMT"); + + WRITE_STRING_FIELD(server_name); + WRITE_STRING_FIELD(remote_schema); + WRITE_STRING_FIELD(local_schema); + WRITE_ENUM_FIELD(list_type, ImportForeignSchemaType); + WRITE_NODE_FIELD(table_list); + WRITE_NODE_FIELD(options); +} + static void _outIndexStmt(StringInfo str, const IndexStmt *node) { @@ -3119,6 +3132,9 @@ _outNode(StringInfo str, const void *obj) case T_CreateForeignTableStmt: _outCreateForeignTableStmt(str, obj); break; + case T_ImportForeignSchemaStmt: + _outImportForeignSchemaStmt(str, obj); + break; case T_IndexStmt: _outIndexStmt(str, obj); break; diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index ba7d091dc79..a113809ca63 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -111,6 +111,13 @@ typedef struct PrivTarget List *objs; } PrivTarget; +/* Private struct for the result of import_qualification production */ +typedef struct ImportQual +{ + ImportForeignSchemaType type; + List *table_names; +} ImportQual; + /* ConstraintAttributeSpec yields an integer bitmask of these flags: */ #define CAS_NOT_DEFERRABLE 0x01 #define CAS_DEFERRABLE 0x02 @@ -212,6 +219,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); ResTarget *target; struct PrivTarget *privtarget; AccessPriv *accesspriv; + struct ImportQual *importqual; InsertStmt *istmt; VariableSetStmt *vsetstmt; } @@ -238,8 +246,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); DropAssertStmt DropTrigStmt DropRuleStmt DropCastStmt DropRoleStmt DropUserStmt DropdbStmt DropTableSpaceStmt DropFdwStmt DropForeignServerStmt DropUserMappingStmt ExplainStmt FetchStmt - GrantStmt GrantRoleStmt IndexStmt InsertStmt ListenStmt LoadStmt - LockStmt NotifyStmt ExplainableStmt PreparableStmt + GrantStmt GrantRoleStmt ImportForeignSchemaStmt IndexStmt InsertStmt + ListenStmt LoadStmt LockStmt NotifyStmt ExplainableStmt PreparableStmt CreateFunctionStmt AlterFunctionStmt ReindexStmt RemoveAggrStmt RemoveFuncStmt RemoveOperStmt RenameStmt RevokeStmt RevokeRoleStmt RuleActionStmt RuleActionStmtOrEmpty RuleStmt @@ -322,6 +330,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %type defacl_privilege_target %type DefACLOption %type DefACLOptionList +%type import_qualification_type +%type import_qualification %type stmtblock stmtmulti OptTableElementList TableElementList OptInherit definition @@ -556,7 +566,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); HANDLER HAVING HEADER_P HOLD HOUR_P - IDENTITY_P IF_P ILIKE IMMEDIATE IMMUTABLE IMPLICIT_P IN_P + IDENTITY_P IF_P ILIKE IMMEDIATE IMMUTABLE IMPLICIT_P IMPORT_P IN_P INCLUDING INCREMENT INDEX INDEXES INHERIT INHERITS INITIALLY INLINE_P INNER_P INOUT INPUT_P INSENSITIVE INSERT INSTEAD INT_P INTEGER INTERSECT INTERVAL INTO INVOKER IS ISNULL ISOLATION @@ -802,6 +812,7 @@ stmt : | FetchStmt | GrantStmt | GrantRoleStmt + | ImportForeignSchemaStmt | IndexStmt | InsertStmt | ListenStmt @@ -4272,6 +4283,52 @@ AlterForeignTableStmt: } ; +/***************************************************************************** + * + * QUERY: + * IMPORT FOREIGN SCHEMA remote_schema + * [ { LIMIT TO | EXCEPT } ( table_list ) ] + * FROM SERVER server_name INTO local_schema [ OPTIONS (...) ] + * + ****************************************************************************/ + +ImportForeignSchemaStmt: + IMPORT_P FOREIGN SCHEMA name import_qualification + FROM SERVER name INTO name create_generic_options + { + ImportForeignSchemaStmt *n = makeNode(ImportForeignSchemaStmt); + n->server_name = $8; + n->remote_schema = $4; + n->local_schema = $10; + n->list_type = $5->type; + n->table_list = $5->table_names; + n->options = $11; + $$ = (Node *) n; + } + ; + +import_qualification_type: + LIMIT TO { $$ = FDW_IMPORT_SCHEMA_LIMIT_TO; } + | EXCEPT { $$ = FDW_IMPORT_SCHEMA_EXCEPT; } + ; + +import_qualification: + import_qualification_type '(' relation_expr_list ')' + { + ImportQual *n = (ImportQual *) palloc(sizeof(ImportQual)); + n->type = $1; + n->table_names = $3; + $$ = n; + } + | /*EMPTY*/ + { + ImportQual *n = (ImportQual *) palloc(sizeof(ImportQual)); + n->type = FDW_IMPORT_SCHEMA_ALL; + n->table_names = NIL; + $$ = n; + } + ; + /***************************************************************************** * * QUERY: @@ -12909,6 +12966,7 @@ unreserved_keyword: | IMMEDIATE | IMMUTABLE | IMPLICIT_P + | IMPORT_P | INCLUDING | INCREMENT | INDEX diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 3423898c112..07e0b987212 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -202,6 +202,7 @@ check_xact_readonly(Node *parsetree) case T_AlterTableSpaceOptionsStmt: case T_AlterTableSpaceMoveStmt: case T_CreateForeignTableStmt: + case T_ImportForeignSchemaStmt: case T_SecLabelStmt: PreventCommandIfReadOnly(CreateCommandTag(parsetree)); break; @@ -1196,6 +1197,10 @@ ProcessUtilitySlow(Node *parsetree, RemoveUserMapping((DropUserMappingStmt *) parsetree); break; + case T_ImportForeignSchemaStmt: + ImportForeignSchema((ImportForeignSchemaStmt *) parsetree); + break; + case T_CompositeTypeStmt: /* CREATE TYPE (composite) */ { CompositeTypeStmt *stmt = (CompositeTypeStmt *) parsetree; @@ -1853,6 +1858,10 @@ CreateCommandTag(Node *parsetree) tag = "CREATE FOREIGN TABLE"; break; + case T_ImportForeignSchemaStmt: + tag = "IMPORT FOREIGN SCHEMA"; + break; + case T_DropStmt: switch (((DropStmt *) parsetree)->removeType) { @@ -2518,6 +2527,7 @@ GetCommandLogLevel(Node *parsetree) case T_CreateUserMappingStmt: case T_AlterUserMappingStmt: case T_DropUserMappingStmt: + case T_ImportForeignSchemaStmt: lev = LOGSTMT_DDL; break; diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c index bab03572352..6c2d431842c 100644 --- a/src/bin/psql/tab-complete.c +++ b/src/bin/psql/tab-complete.c @@ -876,8 +876,9 @@ psql_completion(const char *text, int start, int end) static const char *const sql_commands[] = { "ABORT", "ALTER", "ANALYZE", "BEGIN", "CHECKPOINT", "CLOSE", "CLUSTER", "COMMENT", "COMMIT", "COPY", "CREATE", "DEALLOCATE", "DECLARE", - "DELETE FROM", "DISCARD", "DO", "DROP", "END", "EXECUTE", "EXPLAIN", "FETCH", - "GRANT", "INSERT", "LISTEN", "LOAD", "LOCK", "MOVE", "NOTIFY", "PREPARE", + "DELETE FROM", "DISCARD", "DO", "DROP", "END", "EXECUTE", "EXPLAIN", + "FETCH", "GRANT", "IMPORT", "INSERT", "LISTEN", "LOAD", "LOCK", + "MOVE", "NOTIFY", "PREPARE", "REASSIGN", "REFRESH", "REINDEX", "RELEASE", "RESET", "REVOKE", "ROLLBACK", "SAVEPOINT", "SECURITY LABEL", "SELECT", "SET", "SHOW", "START", "TABLE", "TRUNCATE", "UNLISTEN", "UPDATE", "VACUUM", "VALUES", "WITH", @@ -2947,6 +2948,13 @@ psql_completion(const char *text, int start, int end) pg_strcasecmp(prev_wd, "GROUP") == 0) COMPLETE_WITH_CONST("BY"); +/* IMPORT FOREIGN SCHEMA */ + else if (pg_strcasecmp(prev_wd, "IMPORT") == 0) + COMPLETE_WITH_CONST("FOREIGN SCHEMA"); + else if (pg_strcasecmp(prev2_wd, "IMPORT") == 0 && + pg_strcasecmp(prev_wd, "FOREIGN") == 0) + COMPLETE_WITH_CONST("SCHEMA"); + /* INSERT */ /* Complete INSERT with "INTO" */ else if (pg_strcasecmp(prev_wd, "INSERT") == 0) diff --git a/src/include/commands/defrem.h b/src/include/commands/defrem.h index 5ec9374aad0..0ebdbc1c186 100644 --- a/src/include/commands/defrem.h +++ b/src/include/commands/defrem.h @@ -124,6 +124,7 @@ extern Oid AlterUserMapping(AlterUserMappingStmt *stmt); extern Oid RemoveUserMapping(DropUserMappingStmt *stmt); extern void RemoveUserMappingById(Oid umId); extern void CreateForeignTable(CreateForeignTableStmt *stmt, Oid relid); +extern void ImportForeignSchema(ImportForeignSchemaStmt *stmt); extern Datum transformGenericOptions(Oid catalogId, Datum oldOptions, List *options, diff --git a/src/include/foreign/fdwapi.h b/src/include/foreign/fdwapi.h index 1b735dacc6d..dc0a7fc7235 100644 --- a/src/include/foreign/fdwapi.h +++ b/src/include/foreign/fdwapi.h @@ -100,6 +100,9 @@ typedef bool (*AnalyzeForeignTable_function) (Relation relation, AcquireSampleRowsFunc *func, BlockNumber *totalpages); +typedef List *(*ImportForeignSchema_function) (ImportForeignSchemaStmt *stmt, + Oid serverOid); + /* * FdwRoutine is the struct returned by a foreign-data wrapper's handler * function. It provides pointers to the callback functions needed by the @@ -144,6 +147,9 @@ typedef struct FdwRoutine /* Support functions for ANALYZE */ AnalyzeForeignTable_function AnalyzeForeignTable; + + /* Support functions for IMPORT FOREIGN SCHEMA */ + ImportForeignSchema_function ImportForeignSchema; } FdwRoutine; @@ -151,5 +157,7 @@ typedef struct FdwRoutine extern FdwRoutine *GetFdwRoutine(Oid fdwhandler); extern FdwRoutine *GetFdwRoutineByRelId(Oid relid); extern FdwRoutine *GetFdwRoutineForRelation(Relation relation, bool makecopy); +extern bool IsImportableForeignTable(const char *tablename, + ImportForeignSchemaStmt *stmt); #endif /* FDWAPI_H */ diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h index 7b0088fdb50..067c7685f0d 100644 --- a/src/include/nodes/nodes.h +++ b/src/include/nodes/nodes.h @@ -357,6 +357,7 @@ typedef enum NodeTag T_AlterTableSpaceMoveStmt, T_SecLabelStmt, T_CreateForeignTableStmt, + T_ImportForeignSchemaStmt, T_CreateExtensionStmt, T_AlterExtensionStmt, T_AlterExtensionContentsStmt, diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index ff126ebca47..8364bef79f5 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1791,7 +1791,7 @@ typedef struct AlterForeignServerStmt } AlterForeignServerStmt; /* ---------------------- - * Create FOREIGN TABLE Statements + * Create FOREIGN TABLE Statement * ---------------------- */ @@ -1831,6 +1831,29 @@ typedef struct DropUserMappingStmt bool missing_ok; /* ignore missing mappings */ } DropUserMappingStmt; +/* ---------------------- + * Import Foreign Schema Statement + * ---------------------- + */ + +typedef enum ImportForeignSchemaType +{ + FDW_IMPORT_SCHEMA_ALL, /* all relations wanted */ + FDW_IMPORT_SCHEMA_LIMIT_TO, /* include only listed tables in import */ + FDW_IMPORT_SCHEMA_EXCEPT /* exclude listed tables from import */ +} ImportForeignSchemaType; + +typedef struct ImportForeignSchemaStmt +{ + NodeTag type; + char *server_name; /* FDW server name */ + char *remote_schema; /* remote schema name to query */ + char *local_schema; /* local schema to create objects in */ + ImportForeignSchemaType list_type; /* type of table list */ + List *table_list; /* List of RangeVar */ + List *options; /* list of options to pass to FDW */ +} ImportForeignSchemaStmt; + /* ---------------------- * Create TRIGGER Statement * ---------------------- diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index 04e98109635..b52e50757c8 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -184,6 +184,7 @@ PG_KEYWORD("ilike", ILIKE, TYPE_FUNC_NAME_KEYWORD) PG_KEYWORD("immediate", IMMEDIATE, UNRESERVED_KEYWORD) PG_KEYWORD("immutable", IMMUTABLE, UNRESERVED_KEYWORD) PG_KEYWORD("implicit", IMPLICIT_P, UNRESERVED_KEYWORD) +PG_KEYWORD("import", IMPORT_P, UNRESERVED_KEYWORD) PG_KEYWORD("in", IN_P, RESERVED_KEYWORD) PG_KEYWORD("including", INCLUDING, UNRESERVED_KEYWORD) PG_KEYWORD("increment", INCREMENT, UNRESERVED_KEYWORD) diff --git a/src/test/regress/expected/foreign_data.out b/src/test/regress/expected/foreign_data.out index ff203b201fd..e4dedb0c3ba 100644 --- a/src/test/regress/expected/foreign_data.out +++ b/src/test/regress/expected/foreign_data.out @@ -1193,6 +1193,16 @@ DROP TRIGGER trigtest_before_row ON foreign_schema.foreign_table_1; DROP TRIGGER trigtest_after_stmt ON foreign_schema.foreign_table_1; DROP TRIGGER trigtest_after_row ON foreign_schema.foreign_table_1; DROP FUNCTION dummy_trigger(); +-- IMPORT FOREIGN SCHEMA +IMPORT FOREIGN SCHEMA s1 FROM SERVER s9 INTO public; -- ERROR +ERROR: foreign-data wrapper "foo" has no handler +IMPORT FOREIGN SCHEMA s1 LIMIT TO (t1) FROM SERVER s9 INTO public; --ERROR +ERROR: foreign-data wrapper "foo" has no handler +IMPORT FOREIGN SCHEMA s1 EXCEPT (t1) FROM SERVER s9 INTO public; -- ERROR +ERROR: foreign-data wrapper "foo" has no handler +IMPORT FOREIGN SCHEMA s1 EXCEPT (t1, t2) FROM SERVER s9 INTO public +OPTIONS (option1 'value1', option2 'value2'); -- ERROR +ERROR: foreign-data wrapper "foo" has no handler -- DROP FOREIGN TABLE DROP FOREIGN TABLE no_table; -- ERROR ERROR: foreign table "no_table" does not exist diff --git a/src/test/regress/sql/foreign_data.sql b/src/test/regress/sql/foreign_data.sql index 0f0869ee268..de9dbc8f386 100644 --- a/src/test/regress/sql/foreign_data.sql +++ b/src/test/regress/sql/foreign_data.sql @@ -514,6 +514,13 @@ DROP TRIGGER trigtest_after_row ON foreign_schema.foreign_table_1; DROP FUNCTION dummy_trigger(); +-- IMPORT FOREIGN SCHEMA +IMPORT FOREIGN SCHEMA s1 FROM SERVER s9 INTO public; -- ERROR +IMPORT FOREIGN SCHEMA s1 LIMIT TO (t1) FROM SERVER s9 INTO public; --ERROR +IMPORT FOREIGN SCHEMA s1 EXCEPT (t1) FROM SERVER s9 INTO public; -- ERROR +IMPORT FOREIGN SCHEMA s1 EXCEPT (t1, t2) FROM SERVER s9 INTO public +OPTIONS (option1 'value1', option2 'value2'); -- ERROR + -- DROP FOREIGN TABLE DROP FOREIGN TABLE no_table; -- ERROR DROP FOREIGN TABLE IF EXISTS no_table;