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..46ab2e6708 100644
--- a/doc/src/sgml/fdwhandler.sgml
+++ b/doc/src/sgml/fdwhandler.sgml
@@ -1065,6 +1065,50 @@ EndDirectModify(ForeignScanState *node);
+
+ FDW Routines for Truncate
+
+void
+ExecForeignTruncate(List *rels, List *rels_extra,
+ DropBehavior behavior, bool restart_seqs);
+
+
+ Truncate a set of foreign tables defined by
+ rels belonging to the same foreign server.
+ This optional function is called during execution of
+ TRUNCATE for each foreign server involved
+ in one TRUNCATE command (note that invocations
+ are not per foreign table).
+
+ rels_extra delivers extra information about
+ the context where the foreign tables are truncated. It is a list of integers and has same length with
+ rels. TRUNCATE_REL_CONTEXT_NORMAL means that
+ the foreign table is specified WITHOUT ONLY clause, and TRUNCATE_REL_CONTEXT_ONLY means
+ specified WITH ONLY clause. TRUNCATE_REL_CONTEXT_CASCADING
+ value means that foreign tables are not specified in the TRUNCATE,
+ but truncated due to dependency (for example, partition table).
+
+
+
+ If the ExecForeignTruncate pointer is set to
+ NULL, attempts to truncate the foreign table will
+ fail with an error message.
+
+
+
+ behavior defines how foreign tables should
+ be truncated, using as possible values DROP_RESTRICT
+ and DROP_CASCADE (to map with the equivalents of
+ TRUNCATE).
+
+
+
+ restart_seqs is set to true
+ if RESTART IDENTITY was supplied in the
+ TRUNCATE.
+
+
+
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..7899106ba1 100644
--- a/doc/src/sgml/ref/truncate.sgml
+++ b/doc/src/sgml/ref/truncate.sgml
@@ -172,9 +172,8 @@ 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..7bad33bafb 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 */
@@ -1721,16 +1746,22 @@ ExecuteTruncate(TruncateStmt *stmt)
* 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 +1799,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 +1901,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 +2020,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 +2135,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..57763ed734 100644
--- a/src/include/commands/tablecmds.h
+++ b/src/include/commands/tablecmds.h
@@ -55,9 +55,16 @@ extern void AlterRelationNamespaceInternal(Relation classRel, Oid relOid,
extern void CheckTableNotInUse(Relation rel, const char *stmt);
+#define TRUNCATE_REL_CONTEXT_NORMAL 0x01
+#define TRUNCATE_REL_CONTEXT_ONLY 0x02
+#define TRUNCATE_REL_CONTEXT_CASCADING 0x04
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