diff --git a/contrib/postgres_fdw/connection.c b/contrib/postgres_fdw/connection.c index 6a61d83862..82aa14a65d 100644 --- a/contrib/postgres_fdw/connection.c +++ b/contrib/postgres_fdw/connection.c @@ -92,7 +92,6 @@ static PGconn *connect_pg_server(ForeignServer *server, UserMapping *user); static void disconnect_pg_server(ConnCacheEntry *entry); static void check_conn_params(const char **keywords, const char **values, UserMapping *user); static void configure_remote_session(PGconn *conn); -static void do_sql_command(PGconn *conn, const char *sql); static void begin_remote_xact(ConnCacheEntry *entry); static void pgfdw_xact_callback(XactEvent event, void *arg); static void pgfdw_subxact_callback(SubXactEvent event, @@ -568,7 +567,7 @@ configure_remote_session(PGconn *conn) /* * Convenience subroutine to issue a non-data-returning SQL command to remote */ -static void +void do_sql_command(PGconn *conn, const char *sql) { PGresult *res; diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c index 5aa3455e30..bdc4c3620d 100644 --- a/contrib/postgres_fdw/deparse.c +++ b/contrib/postgres_fdw/deparse.c @@ -56,6 +56,7 @@ #include "utils/rel.h" #include "utils/syscache.h" #include "utils/typcache.h" +#include "commands/tablecmds.h" /* * Global context for foreign_expr_walker's search of an expression tree. @@ -2172,6 +2173,43 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs) deparseRelation(buf, rel); } +/* + * Construct a simple "TRUNCATE rel" statement + */ +void +deparseTruncateSql(StringInfo buf, + List *rels, + List *rels_extra, + DropBehavior behavior, + bool restart_seqs) +{ + ListCell *lc1, + *lc2; + + appendStringInfoString(buf, "TRUNCATE "); + + forboth(lc1, rels, lc2, rels_extra) + { + Relation rel = lfirst(lc1); + int extra = lfirst_int(lc2); + + if (lc1 != list_head(rels)) + appendStringInfoString(buf, ", "); + if (extra & TRUNCATE_REL_CONTEXT_ONLY) + appendStringInfoString(buf, "ONLY "); + + deparseRelation(buf, rel); + } + + appendStringInfo(buf, " %s IDENTITY", + restart_seqs ? "RESTART" : "CONTINUE"); + + if (behavior == DROP_RESTRICT) + appendStringInfoString(buf, " RESTRICT"); + else if (behavior == DROP_CASCADE) + appendStringInfoString(buf, " CASCADE"); +} + /* * Construct name to use for given column, and emit it into buf. * If it has a column_name FDW option, use that instead of attribute name. diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out index eeb6ae79d0..7f69fa0054 100644 --- a/contrib/postgres_fdw/expected/postgres_fdw.out +++ b/contrib/postgres_fdw/expected/postgres_fdw.out @@ -8215,6 +8215,205 @@ select * from rem3; drop foreign table rem3; drop table loc3; -- =================================================================== +-- test for TRUNCATE +-- =================================================================== +CREATE TABLE tru_rtable0 (id int primary key); +CREATE TABLE tru_rtable1 (id int primary key); +CREATE FOREIGN TABLE tru_ftable (id int) + SERVER loopback OPTIONS (table_name 'tru_rtable0'); +INSERT INTO tru_rtable0 (SELECT x FROM generate_series(1,10) x); +CREATE TABLE tru_ptable (id int) PARTITION BY HASH(id); +CREATE TABLE tru_ptable__p0 PARTITION OF tru_ptable + FOR VALUES WITH (MODULUS 2, REMAINDER 0); +CREATE FOREIGN TABLE tru_ftable__p1 PARTITION OF tru_ptable + FOR VALUES WITH (MODULUS 2, REMAINDER 1) + SERVER loopback OPTIONS (table_name 'tru_rtable1'); +INSERT INTO tru_ptable (SELECT x FROM generate_series(11,20) x); +CREATE TABLE tru_pk_table(id int primary key); +CREATE TABLE tru_fk_table(fkey int references tru_pk_table(id)); +INSERT INTO tru_pk_table (SELECT x FROM generate_series(1,10) x); +INSERT INTO tru_fk_table (SELECT x % 10 + 1 FROM generate_series(5,25) x); +CREATE FOREIGN TABLE tru_pk_ftable (id int) + SERVER loopback OPTIONS (table_name 'tru_pk_table'); +CREATE TABLE tru_rtable_parent (id int); +CREATE TABLE tru_rtable_child (id int); +CREATE FOREIGN TABLE tru_ftable_parent (id int) + SERVER loopback OPTIONS (table_name 'tru_rtable_parent'); +CREATE FOREIGN TABLE tru_ftable_child () INHERITS (tru_ftable_parent) + SERVER loopback OPTIONS (table_name 'tru_rtable_child'); +INSERT INTO tru_rtable_parent (SELECT x FROM generate_series(1,8) x); +INSERT INTO tru_rtable_child (SELECT x FROM generate_series(10, 18) x); +-- normal truncate +SELECT sum(id) FROM tru_ftable; -- 55 + sum +----- + 55 +(1 row) + +TRUNCATE tru_ftable; +SELECT count(*) FROM tru_rtable0; -- 0 + count +------- + 0 +(1 row) + +SELECT count(*) FROM tru_ftable; -- 0 + count +------- + 0 +(1 row) + +-- 'truncatable' option +ALTER SERVER loopback OPTIONS (ADD truncatable 'false'); +TRUNCATE tru_ftable; -- error +ERROR: foreign table "tru_ftable" does not allow truncates +ALTER FOREIGN TABLE tru_ftable OPTIONS (ADD truncatable 'true'); +TRUNCATE tru_ftable; -- accepted +ALTER FOREIGN TABLE tru_ftable OPTIONS (SET truncatable 'false'); +TRUNCATE tru_ftable; -- error +ERROR: foreign table "tru_ftable" does not allow truncates +ALTER SERVER loopback OPTIONS (DROP truncatable); +ALTER FOREIGN TABLE tru_ftable OPTIONS (SET truncatable 'false'); +TRUNCATE tru_ftable; -- error +ERROR: foreign table "tru_ftable" does not allow truncates +ALTER FOREIGN TABLE tru_ftable OPTIONS (SET truncatable 'true'); +TRUNCATE tru_ftable; -- accepted +-- partitioned table with both local and foreign tables as partitions +SELECT sum(id) FROM tru_ptable; -- 155 + sum +----- + 155 +(1 row) + +TRUNCATE tru_ptable; +SELECT count(*) FROM tru_ptable; -- 0 + count +------- + 0 +(1 row) + +SELECT count(*) FROM tru_ptable__p0; -- 0 + count +------- + 0 +(1 row) + +SELECT count(*) FROM tru_ftable__p1; -- 0 + count +------- + 0 +(1 row) + +SELECT count(*) FROM tru_rtable1; -- 0 + count +------- + 0 +(1 row) + +-- 'CASCADE' option +SELECT sum(id) FROM tru_pk_ftable; -- 55 + sum +----- + 55 +(1 row) + +TRUNCATE tru_pk_ftable; -- failed by FK reference +ERROR: cannot truncate a table referenced in a foreign key constraint +DETAIL: Table "tru_fk_table" references "tru_pk_table". +HINT: Truncate table "tru_fk_table" at the same time, or use TRUNCATE ... CASCADE. +CONTEXT: remote SQL command: TRUNCATE public.tru_pk_table CONTINUE IDENTITY RESTRICT +TRUNCATE tru_pk_ftable CASCADE; +SELECT count(*) FROM tru_pk_ftable; -- 0 + count +------- + 0 +(1 row) + +SELECT count(*) FROM tru_fk_table; -- also truncated,0 + count +------- + 0 +(1 row) + +-- truncate two tables at a command +INSERT INTO tru_ftable (SELECT x FROM generate_series(1,8) x); +INSERT INTO tru_pk_ftable (SELECT x FROM generate_series(3,10) x); +SELECT count(*) from tru_ftable; -- 8 + count +------- + 8 +(1 row) + +SELECT count(*) from tru_pk_ftable; -- 8 + count +------- + 8 +(1 row) + +TRUNCATE tru_ftable, tru_pk_ftable CASCADE; +SELECT count(*) from tru_ftable; -- 0 + count +------- + 0 +(1 row) + +SELECT count(*) from tru_pk_ftable; -- 0 + count +------- + 0 +(1 row) + +-- truncate with ONLY clause +TRUNCATE ONLY tru_ftable_parent; +SELECT sum(id) FROM tru_ftable_parent; -- 126 + sum +----- + 126 +(1 row) + +TRUNCATE tru_ftable_parent; +SELECT count(*) FROM tru_ftable_parent; -- 0 + count +------- + 0 +(1 row) + +-- in case when remote table has inherited children +CREATE TABLE tru_rtable0_child () INHERITS (tru_rtable0); +INSERT INTO tru_rtable0 (SELECT x FROM generate_series(5,9) x); +INSERT INTO tru_rtable0_child (SELECT x FROM generate_series(10,14) x); +SELECT sum(id) FROM tru_ftable; -- 95 + sum +----- + 95 +(1 row) + +TRUNCATE ONLY tru_ftable; -- truncate only parent portion +SELECT sum(id) FROM tru_ftable; -- 60 + sum +----- + 60 +(1 row) + +INSERT INTO tru_rtable0 (SELECT x FROM generate_series(21,25) x); +SELECT sum(id) FROM tru_ftable; -- 175 + sum +----- + 175 +(1 row) + +TRUNCATE tru_ftable; -- truncate both of parent and child +SELECT count(*) FROM tru_ftable; -- empty + count +------- + 0 +(1 row) + +-- cleanup +DROP FOREIGN TABLE tru_ftable_parent, tru_ftable_child, tru_pk_ftable,tru_ftable__p1,tru_ftable; +DROP TABLE tru_rtable0, tru_rtable1, tru_ptable, tru_ptable__p0, tru_pk_table, tru_fk_table, +tru_rtable_parent,tru_rtable_child, tru_rtable0_child; +-- =================================================================== -- test IMPORT FOREIGN SCHEMA -- =================================================================== CREATE SCHEMA import_source; @@ -8917,7 +9116,7 @@ DO $d$ END; $d$; ERROR: invalid option "password" -HINT: Valid options in this context are: service, passfile, channel_binding, connect_timeout, dbname, host, hostaddr, port, options, application_name, keepalives, keepalives_idle, keepalives_interval, keepalives_count, tcp_user_timeout, sslmode, sslcompression, sslcert, sslkey, sslrootcert, sslcrl, sslcrldir, sslsni, requirepeer, ssl_min_protocol_version, ssl_max_protocol_version, gssencmode, krbsrvname, gsslib, target_session_attrs, use_remote_estimate, fdw_startup_cost, fdw_tuple_cost, extensions, updatable, fetch_size, batch_size, async_capable, keep_connections +HINT: Valid options in this context are: service, passfile, channel_binding, connect_timeout, dbname, host, hostaddr, port, options, application_name, keepalives, keepalives_idle, keepalives_interval, keepalives_count, tcp_user_timeout, sslmode, sslcompression, sslcert, sslkey, sslrootcert, sslcrl, sslcrldir, sslsni, requirepeer, ssl_min_protocol_version, ssl_max_protocol_version, gssencmode, krbsrvname, gsslib, target_session_attrs, use_remote_estimate, fdw_startup_cost, fdw_tuple_cost, extensions, updatable, truncatable, fetch_size, batch_size, async_capable, keep_connections CONTEXT: SQL statement "ALTER SERVER loopback_nopw OPTIONS (ADD password 'dummypw')" PL/pgSQL function inline_code_block line 3 at EXECUTE -- If we add a password for our user mapping instead, we should get a different diff --git a/contrib/postgres_fdw/option.c b/contrib/postgres_fdw/option.c index f1d0c8bd41..672b55a808 100644 --- a/contrib/postgres_fdw/option.c +++ b/contrib/postgres_fdw/option.c @@ -108,6 +108,7 @@ postgres_fdw_validator(PG_FUNCTION_ARGS) */ if (strcmp(def->defname, "use_remote_estimate") == 0 || strcmp(def->defname, "updatable") == 0 || + strcmp(def->defname, "truncatable") == 0 || strcmp(def->defname, "async_capable") == 0 || strcmp(def->defname, "keep_connections") == 0) { @@ -213,6 +214,9 @@ InitPgFdwOptions(void) /* updatable is available on both server and table */ {"updatable", ForeignServerRelationId, false}, {"updatable", ForeignTableRelationId, false}, + /* truncatable is available on both server and table */ + {"truncatable", ForeignServerRelationId, false}, + {"truncatable", ForeignTableRelationId, false}, /* fetch_size is available on both server and table */ {"fetch_size", ForeignServerRelationId, false}, {"fetch_size", ForeignTableRelationId, false}, diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c index b6442070a3..c590f374c6 100644 --- a/contrib/postgres_fdw/postgres_fdw.c +++ b/contrib/postgres_fdw/postgres_fdw.c @@ -400,6 +400,10 @@ static void postgresExplainForeignModify(ModifyTableState *mtstate, ExplainState *es); static void postgresExplainDirectModify(ForeignScanState *node, ExplainState *es); +static void postgresExecForeignTruncate(List *rels, + List *rels_extra, + DropBehavior behavior, + bool restart_seqs); static bool postgresAnalyzeForeignTable(Relation relation, AcquireSampleRowsFunc *func, BlockNumber *totalpages); @@ -588,6 +592,9 @@ postgres_fdw_handler(PG_FUNCTION_ARGS) routine->ExplainForeignModify = postgresExplainForeignModify; routine->ExplainDirectModify = postgresExplainDirectModify; + /* Support function for TRUNCATE */ + routine->ExecForeignTruncate = postgresExecForeignTruncate; + /* Support functions for ANALYZE */ routine->AnalyzeForeignTable = postgresAnalyzeForeignTable; @@ -2868,6 +2875,102 @@ postgresExplainDirectModify(ForeignScanState *node, ExplainState *es) } } +/* + * postgresExecForeignTruncate + * Truncate one or more foreign tables + */ +static void +postgresExecForeignTruncate(List *rels, + List *rels_extra, + DropBehavior behavior, + bool restart_seqs) +{ + Oid serverid = InvalidOid; + UserMapping *user = NULL; + PGconn *conn = NULL; + StringInfoData sql; + ListCell *lc; + bool server_truncatable = true; + + /* + * By default, all postgres_fdw foreign tables are assumed truncatable. + * This can be overridden by a per-server setting, which in turn can be + * overridden by a per-table setting. + */ + foreach(lc, rels) + { + ForeignServer *server = NULL; + Relation rel = lfirst(lc); + ForeignTable *table = GetForeignTable(RelationGetRelid(rel)); + ListCell *cell; + bool truncatable; + + /* + * First time through, determine whether the foreign server allows + * truncates. Since all specified foreign tables are assumed to belong + * to the same foreign server, this result can be used for other + * foreign tables. + */ + if (!OidIsValid(serverid)) + { + serverid = table->serverid; + server = GetForeignServer(serverid); + + foreach(cell, server->options) + { + DefElem *defel = (DefElem *) lfirst(cell); + + if (strcmp(defel->defname, "truncatable") == 0) + { + server_truncatable = defGetBoolean(defel); + break; + } + } + } + + /* + * Confirm that all specified foreign tables belong to the same + * foreign server. + */ + Assert(table->serverid == serverid); + + /* Determine whether this foreign table allows truncations */ + truncatable = server_truncatable; + foreach(cell, table->options) + { + DefElem *defel = (DefElem *) lfirst(cell); + + if (strcmp(defel->defname, "truncatable") == 0) + { + truncatable = defGetBoolean(defel); + break; + } + } + + if (!truncatable) + ereport(ERROR, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("foreign table \"%s\" does not allow truncates", + RelationGetRelationName(rel)))); + } + Assert(OidIsValid(serverid)); + + /* + * Get connection to the foreign server. Connection manager will + * establish new connection if necessary. + */ + user = GetUserMapping(GetUserId(), serverid); + conn = GetConnection(user, false, NULL); + + /* Construct the TRUNCATE command string */ + initStringInfo(&sql); + deparseTruncateSql(&sql, rels, rels_extra, behavior, restart_seqs); + + /* Issue the TRUNCATE command to remote server */ + do_sql_command(conn, sql.data); + + pfree(sql.data); +} /* * estimate_path_cost_size diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h index 88d94da6f6..5d44b75314 100644 --- a/contrib/postgres_fdw/postgres_fdw.h +++ b/contrib/postgres_fdw/postgres_fdw.h @@ -145,6 +145,7 @@ extern PGconn *GetConnection(UserMapping *user, bool will_prep_stmt, extern void ReleaseConnection(PGconn *conn); extern unsigned int GetCursorNumber(PGconn *conn); extern unsigned int GetPrepStmtNumber(PGconn *conn); +extern void do_sql_command(PGconn *conn, const char *sql); extern PGresult *pgfdw_get_result(PGconn *conn, const char *query); extern PGresult *pgfdw_exec_query(PGconn *conn, const char *query, PgFdwConnState *state); @@ -206,6 +207,11 @@ extern void deparseDirectDeleteSql(StringInfo buf, PlannerInfo *root, extern void deparseAnalyzeSizeSql(StringInfo buf, Relation rel); extern void deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs); +extern void deparseTruncateSql(StringInfo buf, + List *rels, + List *rels_extra, + DropBehavior behavior, + bool restart_seqs); extern void deparseStringLiteral(StringInfo buf, const char *val); extern Expr *find_em_expr_for_rel(EquivalenceClass *ec, RelOptInfo *rel); extern Expr *find_em_expr_for_input_target(PlannerInfo *root, diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql index 3b4f90a99c..7487096eac 100644 --- a/contrib/postgres_fdw/sql/postgres_fdw.sql +++ b/contrib/postgres_fdw/sql/postgres_fdw.sql @@ -2351,6 +2351,107 @@ select * from rem3; drop foreign table rem3; drop table loc3; +-- =================================================================== +-- test for TRUNCATE +-- =================================================================== +CREATE TABLE tru_rtable0 (id int primary key); +CREATE TABLE tru_rtable1 (id int primary key); +CREATE FOREIGN TABLE tru_ftable (id int) + SERVER loopback OPTIONS (table_name 'tru_rtable0'); +INSERT INTO tru_rtable0 (SELECT x FROM generate_series(1,10) x); + +CREATE TABLE tru_ptable (id int) PARTITION BY HASH(id); +CREATE TABLE tru_ptable__p0 PARTITION OF tru_ptable + FOR VALUES WITH (MODULUS 2, REMAINDER 0); +CREATE FOREIGN TABLE tru_ftable__p1 PARTITION OF tru_ptable + FOR VALUES WITH (MODULUS 2, REMAINDER 1) + SERVER loopback OPTIONS (table_name 'tru_rtable1'); +INSERT INTO tru_ptable (SELECT x FROM generate_series(11,20) x); + +CREATE TABLE tru_pk_table(id int primary key); +CREATE TABLE tru_fk_table(fkey int references tru_pk_table(id)); +INSERT INTO tru_pk_table (SELECT x FROM generate_series(1,10) x); +INSERT INTO tru_fk_table (SELECT x % 10 + 1 FROM generate_series(5,25) x); +CREATE FOREIGN TABLE tru_pk_ftable (id int) + SERVER loopback OPTIONS (table_name 'tru_pk_table'); + +CREATE TABLE tru_rtable_parent (id int); +CREATE TABLE tru_rtable_child (id int); +CREATE FOREIGN TABLE tru_ftable_parent (id int) + SERVER loopback OPTIONS (table_name 'tru_rtable_parent'); +CREATE FOREIGN TABLE tru_ftable_child () INHERITS (tru_ftable_parent) + SERVER loopback OPTIONS (table_name 'tru_rtable_child'); +INSERT INTO tru_rtable_parent (SELECT x FROM generate_series(1,8) x); +INSERT INTO tru_rtable_child (SELECT x FROM generate_series(10, 18) x); + +-- normal truncate +SELECT sum(id) FROM tru_ftable; -- 55 +TRUNCATE tru_ftable; +SELECT count(*) FROM tru_rtable0; -- 0 +SELECT count(*) FROM tru_ftable; -- 0 + +-- 'truncatable' option +ALTER SERVER loopback OPTIONS (ADD truncatable 'false'); +TRUNCATE tru_ftable; -- error +ALTER FOREIGN TABLE tru_ftable OPTIONS (ADD truncatable 'true'); +TRUNCATE tru_ftable; -- accepted +ALTER FOREIGN TABLE tru_ftable OPTIONS (SET truncatable 'false'); +TRUNCATE tru_ftable; -- error +ALTER SERVER loopback OPTIONS (DROP truncatable); +ALTER FOREIGN TABLE tru_ftable OPTIONS (SET truncatable 'false'); +TRUNCATE tru_ftable; -- error +ALTER FOREIGN TABLE tru_ftable OPTIONS (SET truncatable 'true'); +TRUNCATE tru_ftable; -- accepted + +-- partitioned table with both local and foreign tables as partitions +SELECT sum(id) FROM tru_ptable; -- 155 +TRUNCATE tru_ptable; +SELECT count(*) FROM tru_ptable; -- 0 +SELECT count(*) FROM tru_ptable__p0; -- 0 +SELECT count(*) FROM tru_ftable__p1; -- 0 +SELECT count(*) FROM tru_rtable1; -- 0 + +-- 'CASCADE' option +SELECT sum(id) FROM tru_pk_ftable; -- 55 +TRUNCATE tru_pk_ftable; -- failed by FK reference +TRUNCATE tru_pk_ftable CASCADE; +SELECT count(*) FROM tru_pk_ftable; -- 0 +SELECT count(*) FROM tru_fk_table; -- also truncated,0 + +-- truncate two tables at a command +INSERT INTO tru_ftable (SELECT x FROM generate_series(1,8) x); +INSERT INTO tru_pk_ftable (SELECT x FROM generate_series(3,10) x); +SELECT count(*) from tru_ftable; -- 8 +SELECT count(*) from tru_pk_ftable; -- 8 +TRUNCATE tru_ftable, tru_pk_ftable CASCADE; +SELECT count(*) from tru_ftable; -- 0 +SELECT count(*) from tru_pk_ftable; -- 0 + +-- truncate with ONLY clause +TRUNCATE ONLY tru_ftable_parent; +SELECT sum(id) FROM tru_ftable_parent; -- 126 +TRUNCATE tru_ftable_parent; +SELECT count(*) FROM tru_ftable_parent; -- 0 + +-- in case when remote table has inherited children +CREATE TABLE tru_rtable0_child () INHERITS (tru_rtable0); +INSERT INTO tru_rtable0 (SELECT x FROM generate_series(5,9) x); +INSERT INTO tru_rtable0_child (SELECT x FROM generate_series(10,14) x); +SELECT sum(id) FROM tru_ftable; -- 95 + +TRUNCATE ONLY tru_ftable; -- truncate only parent portion +SELECT sum(id) FROM tru_ftable; -- 60 + +INSERT INTO tru_rtable0 (SELECT x FROM generate_series(21,25) x); +SELECT sum(id) FROM tru_ftable; -- 175 +TRUNCATE tru_ftable; -- truncate both of parent and child +SELECT count(*) FROM tru_ftable; -- empty + +-- cleanup +DROP FOREIGN TABLE tru_ftable_parent, tru_ftable_child, tru_pk_ftable,tru_ftable__p1,tru_ftable; +DROP TABLE tru_rtable0, tru_rtable1, tru_ptable, tru_ptable__p0, tru_pk_table, tru_fk_table, +tru_rtable_parent,tru_rtable_child, tru_rtable0_child; + -- =================================================================== -- test IMPORT FOREIGN SCHEMA -- =================================================================== diff --git a/doc/src/sgml/fdwhandler.sgml b/doc/src/sgml/fdwhandler.sgml index 0f2397df49..98882ddab8 100644 --- a/doc/src/sgml/fdwhandler.sgml +++ b/doc/src/sgml/fdwhandler.sgml @@ -1065,6 +1065,67 @@ EndDirectModify(ForeignScanState *node); + + FDW Routines for <command>TRUNCATE</command> + + + +void +ExecForeignTruncate(List *rels, List *rels_extra, + DropBehavior behavior, bool restart_seqs); + + + Truncate a set of foreign tables specified in rels. + This function is called when is executed + on foreign tables. rels is the list of + Relation data structure that indicates + a foreign table to truncate. rels_extra the list of + int value, which delivers extra information about + a foreign table to truncate. Possible values are + TRUNCATE_REL_CONTEXT_NORMAL, which means that + the foreign table is specified WITHOUT ONLY clause + in TRUNCATE, + TRUNCATE_REL_CONTEXT_ONLY, which means that + the foreign table is specified WITH ONLY clause, + and TRUNCATE_REL_CONTEXT_CASCADING, + which means that the foreign table is not specified explicitly, + but will be truncated due to dependency (for example, partition table). + There is one-to-one mapping between rels and + rels_extra. The number of entries is the same + between the two lists. + + + + behavior defines how foreign tables should + be truncated, using as possible values DROP_RESTRICT, + which means that RESTRICT option is specified, + and DROP_CASCADE, which means that + CASCADE option is specified, in + TRUNCATE command. + + + + restart_seqs is set to true + if RESTART IDENTITY option is specified in + TRUNCATE command. It is false + if CONTINUE IDENTITY option is specified. + + + + TRUNCATE invokes + ExecForeignTruncate once per foreign server + that foreign tables to truncate belong to. This means that all foreign + tables included in rels must belong to the same + server. + + + + If the ExecForeignTruncate pointer is set to + NULL, attempts to truncate foreign tables will + fail with an error message. + + + FDW Routines for Row Locking diff --git a/doc/src/sgml/postgres-fdw.sgml b/doc/src/sgml/postgres-fdw.sgml index b0792a13b1..fd34956936 100644 --- a/doc/src/sgml/postgres-fdw.sgml +++ b/doc/src/sgml/postgres-fdw.sgml @@ -63,9 +63,10 @@ Now you need only SELECT from a foreign table to access the data stored in its underlying remote table. You can also modify - the remote table using INSERT, UPDATE, or - DELETE. (Of course, the remote user you have specified - in your user mapping must have privileges to do these things.) + the remote table using INSERT, UPDATE, + DELETE, or TRUNCATE. + (Of course, the remote user you have specified in your user mapping must + have privileges to do these things.) @@ -436,6 +437,31 @@ OPTIONS (ADD password_required 'false'); + + Truncatability Options + + + By default all foreign tables using postgres_fdw are assumed + to be truncatable. This may be overridden using the following option: + + + + + + truncatable + + + This option controls whether postgres_fdw allows + foreign tables to be truncated using TRUNCATE + command. It can be specified for a foreign table or a foreign server. + A table-level option overrides a server-level option. + The default is true. + + + + + + Importing Options diff --git a/doc/src/sgml/ref/truncate.sgml b/doc/src/sgml/ref/truncate.sgml index 91cdac5562..acf3633be4 100644 --- a/doc/src/sgml/ref/truncate.sgml +++ b/doc/src/sgml/ref/truncate.sgml @@ -172,9 +172,9 @@ TRUNCATE [ TABLE ] [ ONLY ] name [ - TRUNCATE is not currently supported for foreign tables. - This implies that if a specified table has any descendant tables that are - foreign, the command will fail. + TRUNCATE can be used for foreign tables if + the foreign data wrapper supports, for instance, + see . diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 87f9bdaef0..1f19629a94 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -59,6 +59,7 @@ #include "commands/typecmds.h" #include "commands/user.h" #include "executor/executor.h" +#include "foreign/fdwapi.h" #include "foreign/foreign.h" #include "miscadmin.h" #include "nodes/makefuncs.h" @@ -310,6 +311,21 @@ struct DropRelationCallbackState #define ATT_FOREIGN_TABLE 0x0020 #define ATT_PARTITIONED_INDEX 0x0040 +/* + * ForeignTruncateInfo + * + * Information related to truncation of foreign tables. This is used for + * the elements in a hash table. It uses the server OID as lookup key, + * and includes a per-server list of all foreign tables involved in the + * truncation. + */ +typedef struct ForeignTruncateInfo +{ + Oid serverid; + List *rels; + List *rels_extra; +} ForeignTruncateInfo; + /* * Partition tables are expected to be dropped when the parent partitioned * table gets dropped. Hence for partitioning we use AUTO dependency. @@ -1589,7 +1605,10 @@ RangeVarCallbackForDropRelation(const RangeVar *rel, Oid relOid, Oid oldRelOid, * * This is a multi-relation truncate. We first open and grab exclusive * lock on all relations involved, checking permissions and otherwise - * verifying that the relation is OK for truncation. In CASCADE mode, + * verifying that the relation is OK for truncation. Note that if relations + * are foreign tables, at this stage, we have not yet checked that their + * foreign data in external data sources are OK for truncation. These are + * checked when foreign data are actually truncated later. In CASCADE mode, * relations having FK references to the targeted relations are automatically * added to the group; in RESTRICT mode, we check that all FK references are * internal to the group that's being truncated. Finally all the relations @@ -1600,6 +1619,7 @@ ExecuteTruncate(TruncateStmt *stmt) { List *rels = NIL; List *relids = NIL; + List *relids_extra = NIL; List *relids_logged = NIL; ListCell *cell; @@ -1636,6 +1656,9 @@ ExecuteTruncate(TruncateStmt *stmt) rels = lappend(rels, rel); relids = lappend_oid(relids, myrelid); + relids_extra = lappend_int(relids_extra, (recurse ? + TRUNCATE_REL_CONTEXT_NORMAL : + TRUNCATE_REL_CONTEXT_ONLY)); /* Log this relation only if needed for logical decoding */ if (RelationIsLogicallyLogged(rel)) relids_logged = lappend_oid(relids_logged, myrelid); @@ -1683,6 +1706,8 @@ ExecuteTruncate(TruncateStmt *stmt) rels = lappend(rels, rel); relids = lappend_oid(relids, childrelid); + relids_extra = lappend_int(relids_extra, + TRUNCATE_REL_CONTEXT_CASCADING); /* Log this relation only if needed for logical decoding */ if (RelationIsLogicallyLogged(rel)) relids_logged = lappend_oid(relids_logged, childrelid); @@ -1695,7 +1720,7 @@ ExecuteTruncate(TruncateStmt *stmt) errhint("Do not specify the ONLY keyword, or use TRUNCATE ONLY on the partitions directly."))); } - ExecuteTruncateGuts(rels, relids, relids_logged, + ExecuteTruncateGuts(rels, relids, relids_extra, relids_logged, stmt->behavior, stmt->restart_seqs); /* And close the rels */ @@ -1716,21 +1741,28 @@ ExecuteTruncate(TruncateStmt *stmt) * * explicit_rels is the list of Relations to truncate that the command * specified. relids is the list of Oids corresponding to explicit_rels. - * relids_logged is the list of Oids (a subset of relids) that require - * WAL-logging. This is all a bit redundant, but the existing callers have - * this information handy in this form. + * relids_extra is the list of integer values that deliver extra information + * about relations in explicit_rels. relids_logged is the list of Oids + * (a subset of relids) that require WAL-logging. This is all a bit redundant, + * but the existing callers have this information handy in this form. */ void -ExecuteTruncateGuts(List *explicit_rels, List *relids, List *relids_logged, +ExecuteTruncateGuts(List *explicit_rels, + List *relids, + List *relids_extra, + List *relids_logged, DropBehavior behavior, bool restart_seqs) { List *rels; List *seq_relids = NIL; + HTAB *ft_htab = NULL; EState *estate; ResultRelInfo *resultRelInfos; ResultRelInfo *resultRelInfo; SubTransactionId mySubid; ListCell *cell; + ListCell *lc1, + *lc2; Oid *logrelids; /* @@ -1768,6 +1800,8 @@ ExecuteTruncateGuts(List *explicit_rels, List *relids, List *relids_logged, truncate_check_activity(rel); rels = lappend(rels, rel); relids = lappend_oid(relids, relid); + relids_extra = lappend_int(relids_extra, + TRUNCATE_REL_CONTEXT_CASCADING); /* Log this relation only if needed for logical decoding */ if (RelationIsLogicallyLogged(rel)) relids_logged = lappend_oid(relids_logged, relid); @@ -1868,14 +1902,63 @@ ExecuteTruncateGuts(List *explicit_rels, List *relids, List *relids_logged, */ mySubid = GetCurrentSubTransactionId(); - foreach(cell, rels) + Assert(list_length(rels) == list_length(relids_extra)); + forboth(lc1, rels, lc2, relids_extra) { - Relation rel = (Relation) lfirst(cell); + Relation rel = (Relation) lfirst(lc1); + int extra = lfirst_int(lc2); /* Skip partitioned tables as there is nothing to do */ if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) continue; + /* + * Build the lists of foreign tables belonging to each foreign server + * and pass each list to the foreign data wrapper's callback function, + * so that each server can truncate its all foreign tables in bulk. + * Each list is saved as a single entry in a hash table that uses the + * server OID as lookup key. + */ + if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE) + { + Oid serverid = GetForeignServerIdByRelId(RelationGetRelid(rel)); + bool found; + ForeignTruncateInfo *ft_info; + + /* First time through, initialize hashtable for foreign tables */ + if (!ft_htab) + { + HASHCTL hctl; + + memset(&hctl, 0, sizeof(HASHCTL)); + hctl.keysize = sizeof(Oid); + hctl.entrysize = sizeof(ForeignTruncateInfo); + hctl.hcxt = CurrentMemoryContext; + + ft_htab = hash_create("TRUNCATE for Foreign Tables", + 32, /* start small and extend */ + &hctl, + HASH_ELEM | HASH_BLOBS | HASH_CONTEXT); + } + + /* Find or create cached entry for the foreign table */ + ft_info = hash_search(ft_htab, &serverid, HASH_ENTER, &found); + if (!found) + { + ft_info->serverid = serverid; + ft_info->rels = NIL; + ft_info->rels_extra = NIL; + } + + /* + * Save the foreign table in the entry of the server that the + * foreign table belongs to. + */ + ft_info->rels = lappend(ft_info->rels, rel); + ft_info->rels_extra = lappend_int(ft_info->rels_extra, extra); + continue; + } + /* * Normally, we need a transaction-safe truncation here. However, if * the table was either created in the current (sub)transaction or has @@ -1938,6 +2021,36 @@ ExecuteTruncateGuts(List *explicit_rels, List *relids, List *relids_logged, pgstat_count_truncate(rel); } + /* Now go through the hash table, and truncate foreign tables */ + if (ft_htab) + { + ForeignTruncateInfo *ft_info; + HASH_SEQ_STATUS seq; + + hash_seq_init(&seq, ft_htab); + + PG_TRY(); + { + while ((ft_info = hash_seq_search(&seq)) != NULL) + { + FdwRoutine *routine = GetFdwRoutineByServerId(ft_info->serverid); + + /* truncate_check_rel() has checked that already */ + Assert(routine->ExecForeignTruncate != NULL); + + routine->ExecForeignTruncate(ft_info->rels, + ft_info->rels_extra, + behavior, + restart_seqs); + } + } + PG_FINALLY(); + { + hash_destroy(ft_htab); + } + PG_END_TRY(); + } + /* * Restart owned sequences if we were asked to. */ @@ -2023,12 +2136,24 @@ truncate_check_rel(Oid relid, Form_pg_class reltuple) char *relname = NameStr(reltuple->relname); /* - * Only allow truncate on regular tables and partitioned tables (although, - * the latter are only being included here for the following checks; no - * physical truncation will occur in their case.) + * Only allow truncate on regular tables, foreign tables using foreign + * data wrappers supporting TRUNCATE and partitioned tables (although, the + * latter are only being included here for the following checks; no + * physical truncation will occur in their case.). */ - if (reltuple->relkind != RELKIND_RELATION && - reltuple->relkind != RELKIND_PARTITIONED_TABLE) + if (reltuple->relkind == RELKIND_FOREIGN_TABLE) + { + Oid serverid = GetForeignServerIdByRelId(relid); + FdwRoutine *fdwroutine = GetFdwRoutineByServerId(serverid); + + if (!fdwroutine->ExecForeignTruncate) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot truncate foreign table \"%s\"", + relname))); + } + else if (reltuple->relkind != RELKIND_RELATION && + reltuple->relkind != RELKIND_PARTITIONED_TABLE) ereport(ERROR, (errcode(ERRCODE_WRONG_OBJECT_TYPE), errmsg("\"%s\" is not a table", relname))); diff --git a/src/backend/replication/logical/worker.c b/src/backend/replication/logical/worker.c index 8da602d163..fb3ba5c415 100644 --- a/src/backend/replication/logical/worker.c +++ b/src/backend/replication/logical/worker.c @@ -1795,6 +1795,7 @@ apply_handle_truncate(StringInfo s) List *rels = NIL; List *part_rels = NIL; List *relids = NIL; + List *relids_extra = NIL; List *relids_logged = NIL; ListCell *lc; @@ -1824,6 +1825,7 @@ apply_handle_truncate(StringInfo s) remote_rels = lappend(remote_rels, rel); rels = lappend(rels, rel->localrel); relids = lappend_oid(relids, rel->localreloid); + relids_extra = lappend_int(relids_extra, TRUNCATE_REL_CONTEXT_NORMAL); if (RelationIsLogicallyLogged(rel->localrel)) relids_logged = lappend_oid(relids_logged, rel->localreloid); @@ -1862,6 +1864,7 @@ apply_handle_truncate(StringInfo s) rels = lappend(rels, childrel); part_rels = lappend(part_rels, childrel); relids = lappend_oid(relids, childrelid); + relids_extra = lappend_int(relids_extra, TRUNCATE_REL_CONTEXT_CASCADING); /* Log this relation only if needed for logical decoding */ if (RelationIsLogicallyLogged(childrel)) relids_logged = lappend_oid(relids_logged, childrelid); @@ -1874,8 +1877,12 @@ apply_handle_truncate(StringInfo s) * to replaying changes without further cascading. This might be later * changeable with a user specified option. */ - ExecuteTruncateGuts(rels, relids, relids_logged, DROP_RESTRICT, restart_seqs); - + ExecuteTruncateGuts(rels, + relids, + relids_extra, + relids_logged, + DROP_RESTRICT, + restart_seqs); foreach(lc, remote_rels) { LogicalRepRelMapEntry *rel = lfirst(lc); diff --git a/src/include/commands/tablecmds.h b/src/include/commands/tablecmds.h index b3d30acc35..b808a07e46 100644 --- a/src/include/commands/tablecmds.h +++ b/src/include/commands/tablecmds.h @@ -21,6 +21,16 @@ #include "storage/lock.h" #include "utils/relcache.h" +/* + * These values indicate how a relation was specified as the target to + * truncate in TRUNCATE command. + */ +#define TRUNCATE_REL_CONTEXT_NORMAL 1 /* specified without ONLY clause */ +#define TRUNCATE_REL_CONTEXT_ONLY 2 /* specified with ONLY clause */ +#define TRUNCATE_REL_CONTEXT_CASCADING 3 /* not specified but truncated + * due to dependency (e.g., + * partition table) */ + struct AlterTableUtilityContext; /* avoid including tcop/utility.h here */ @@ -56,8 +66,12 @@ extern void AlterRelationNamespaceInternal(Relation classRel, Oid relOid, extern void CheckTableNotInUse(Relation rel, const char *stmt); extern void ExecuteTruncate(TruncateStmt *stmt); -extern void ExecuteTruncateGuts(List *explicit_rels, List *relids, List *relids_logged, - DropBehavior behavior, bool restart_seqs); +extern void ExecuteTruncateGuts(List *explicit_rels, + List *relids, + List *relids_extra, + List *relids_logged, + DropBehavior behavior, + bool restart_seqs); extern void SetRelationHasSubclass(Oid relationId, bool relhassubclass); diff --git a/src/include/foreign/fdwapi.h b/src/include/foreign/fdwapi.h index 10d29ff292..4ebbca6de9 100644 --- a/src/include/foreign/fdwapi.h +++ b/src/include/foreign/fdwapi.h @@ -160,6 +160,11 @@ typedef bool (*AnalyzeForeignTable_function) (Relation relation, typedef List *(*ImportForeignSchema_function) (ImportForeignSchemaStmt *stmt, Oid serverOid); +typedef void (*ExecForeignTruncate_function) (List *rels, + List *rels_extra, + DropBehavior behavior, + bool restart_seqs); + typedef Size (*EstimateDSMForeignScan_function) (ForeignScanState *node, ParallelContext *pcxt); typedef void (*InitializeDSMForeignScan_function) (ForeignScanState *node, @@ -255,6 +260,9 @@ typedef struct FdwRoutine /* Support functions for IMPORT FOREIGN SCHEMA */ ImportForeignSchema_function ImportForeignSchema; + /* Support functions for TRUNCATE */ + ExecForeignTruncate_function ExecForeignTruncate; + /* Support functions for parallelism under Gather node */ IsForeignScanParallelSafe_function IsForeignScanParallelSafe; EstimateDSMForeignScan_function EstimateDSMForeignScan; diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h index 9a3a03e520..34e25eb597 100644 --- a/src/include/utils/rel.h +++ b/src/include/utils/rel.h @@ -634,7 +634,8 @@ typedef struct ViewOptions * WAL stream. * * We don't log information for unlogged tables (since they don't WAL log - * anyway) and for system tables (their content is hard to make sense of, and + * anyway), for foreign tables (since they don't WAL log, either), + * and for system tables (their content is hard to make sense of, and * it would complicate decoding slightly for little gain). Note that we *do* * log information for user defined catalog tables since they presumably are * interesting to the user... @@ -642,6 +643,7 @@ typedef struct ViewOptions #define RelationIsLogicallyLogged(relation) \ (XLogLogicalInfoActive() && \ RelationNeedsWAL(relation) && \ + (relation)->rd_rel->relkind != RELKIND_FOREIGN_TABLE && \ !IsCatalogRelation(relation)) /* routines in utils/cache/relcache.c */ diff --git a/src/test/regress/expected/foreign_data.out b/src/test/regress/expected/foreign_data.out index e4cdb780d0..5385f98a0f 100644 --- a/src/test/regress/expected/foreign_data.out +++ b/src/test/regress/expected/foreign_data.out @@ -1807,9 +1807,9 @@ Inherits: fd_pt1 -- TRUNCATE doesn't work on foreign tables, either directly or recursively TRUNCATE ft2; -- ERROR -ERROR: "ft2" is not a table +ERROR: foreign-data wrapper "dummy" has no handler TRUNCATE fd_pt1; -- ERROR -ERROR: "ft2" is not a table +ERROR: foreign-data wrapper "dummy" has no handler DROP TABLE fd_pt1 CASCADE; NOTICE: drop cascades to foreign table ft2 -- IMPORT FOREIGN SCHEMA @@ -2032,9 +2032,9 @@ ALTER FOREIGN TABLE fd_pt2_1 ADD CONSTRAINT fd_pt2chk1 CHECK (c1 > 0); ALTER TABLE fd_pt2 ATTACH PARTITION fd_pt2_1 FOR VALUES IN (1); -- TRUNCATE doesn't work on foreign tables, either directly or recursively TRUNCATE fd_pt2_1; -- ERROR -ERROR: "fd_pt2_1" is not a table +ERROR: foreign-data wrapper "dummy" has no handler TRUNCATE fd_pt2; -- ERROR -ERROR: "fd_pt2_1" is not a table +ERROR: foreign-data wrapper "dummy" has no handler DROP FOREIGN TABLE fd_pt2_1; DROP TABLE fd_pt2; -- foreign table cannot be part of partition tree made of temporary diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index efb9811198..fefa65705a 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -704,6 +704,7 @@ ForeignScanState ForeignServer ForeignServerInfo ForeignTable +ForeignTruncateInfo ForkNumber FormData_pg_aggregate FormData_pg_am