 contrib/postgres_fdw/deparse.c                 |  34 ++++++
 contrib/postgres_fdw/expected/postgres_fdw.out | 144 +++++++++++++++++++++++++
 contrib/postgres_fdw/postgres_fdw.c            |  51 +++++++++
 contrib/postgres_fdw/postgres_fdw.h            |   2 +
 contrib/postgres_fdw/sql/postgres_fdw.sql      |  51 +++++++++
 doc/src/sgml/fdwhandler.sgml                   |  39 +++++++
 src/backend/commands/tablecmds.c               |  90 +++++++++++++++-
 src/include/foreign/fdwapi.h                   |   7 ++
 src/test/regress/expected/foreign_data.out     |   8 +-
 9 files changed, 418 insertions(+), 8 deletions(-)

diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index a3639e9604..05e95144f9 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -2098,6 +2098,40 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 	deparseRelation(buf, rel);
 }
 
+/*
+ * Construct a simple "TRUNCATE rel" statement
+ */
+void
+deparseTruncateSql(StringInfo buf, List *frels_list,
+				   DropBehavior behavior, bool restart_seqs)
+{
+	ListCell   *lc;
+
+	appendStringInfoString(buf, "TRUNCATE ");
+	foreach (lc, frels_list)
+	{
+		Relation	frel = lfirst(lc);
+
+		if (lc != list_head(frels_list))
+			appendStringInfoString(buf, ", ");
+		deparseRelation(buf, frel);
+	}
+	appendStringInfo(buf, " %s IDENTITY",
+					 restart_seqs ? "RESTART" : "CONTINUE");
+	switch (behavior)
+	{
+		case DROP_RESTRICT:
+			appendStringInfoString(buf, " RESTRICT");
+			break;
+		case DROP_CASCADE:
+			appendStringInfoString(buf, " CASCADE");
+			break;
+		default:
+			elog(ERROR, "Bug? unexpected DropBehavior (%d)", (int)behavior);
+			break;
+	}
+}
+
 /*
  * 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 84fd3ad2e0..ef10c8f709 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8168,6 +8168,150 @@ select * from rem3;
 
 drop foreign table rem3;
 drop table loc3;
+-- ===================================================================
+-- test for TRUNCATE
+-- ===================================================================
+CREATE TABLE tru_nt (id int, x text);
+CREATE FOREIGN TABLE tru_ft (id int, x text)
+       SERVER loopback OPTIONS (table_name 'tru_nt');
+INSERT INTO tru_nt (SELECT x,md5(x::text) FROM generate_series(1,10) x);
+CREATE TABLE tru_pt (id int, y text) PARTITION BY HASH(id);
+CREATE TABLE tru_pt__p0 PARTITION OF tru_pt
+                        FOR VALUES WITH (MODULUS 2, REMAINDER 0);
+CREATE TABLE __tru_pt__p1 (id int, y text);
+CREATE FOREIGN TABLE tru_pt__p1 PARTITION OF tru_pt
+                                FOR VALUES WITH (MODULUS 2, REMAINDER 1)
+               SERVER loopback OPTIONS (table_name '__tru_pt__p1');
+INSERT INTO tru_pt (SELECT x,md5(x::text) FROM generate_series(11,30) x);
+CREATE TABLE tru_pk (id int primary key, x text);
+INSERT INTO tru_pk (SELECT x,md5((x+1)::text) FROM generate_series(1,10) x);
+CREATE TABLE tru_fk (fkey int references tru_pk(id));
+INSERT INTO tru_fk (SELECT x % 10 + 1 FROM generate_series(1,30) x);
+CREATE FOREIGN TABLE tru_ft_ref (id int, x text)
+       SERVER loopback OPTIONS (table_name 'tru_pk');
+-- normal truncate
+SELECT * FROM tru_ft;
+ id |                x                 
+----+----------------------------------
+  1 | c4ca4238a0b923820dcc509a6f75849b
+  2 | c81e728d9d4c2f636f067f89cc14862c
+  3 | eccbc87e4b5ce2fe28308fd9f2a7baf3
+  4 | a87ff679a2f3e71d9181a67b7542122c
+  5 | e4da3b7fbbce2345d7772b0674a318d5
+  6 | 1679091c5a880faf6fb5e6087eb1b2dc
+  7 | 8f14e45fceea167a5a36dedd4bea2543
+  8 | c9f0f895fb98ab9159f51fd0297e236d
+  9 | 45c48cce2e2d7fbdea1afc51c7c6ad26
+ 10 | d3d9446802a44259755d38e6d163e820
+(10 rows)
+
+TRUNCATE tru_ft;
+SELECT * FROM tru_nt;
+ id | x 
+----+---
+(0 rows)
+
+SELECT * FROM tru_ft;
+ id | x 
+----+---
+(0 rows)
+
+-- partition table mixtured by table and foreign table
+SELECT * FROM tru_pt;
+ id |                y                 
+----+----------------------------------
+ 12 | c20ad4d76fe97759aa27a0c99bff6710
+ 13 | c51ce410c124a10e0db5e4b97fc2af39
+ 14 | aab3238922bcc25a6f606eb525ffdc56
+ 16 | c74d97b01eae257e44aa9d5bade97baf
+ 17 | 70efdf2ec9b086079795c442636b55fb
+ 18 | 6f4922f45568161a8cdf4ad2299f6d23
+ 23 | 37693cfc748049e45d87b8c7d8b9aacd
+ 25 | 8e296a067a37563370ded05f5a3bf3ec
+ 26 | 4e732ced3463d06de0ca9a15b6153677
+ 27 | 02e74f10e0327ad868d138f2b4fdd6f0
+ 28 | 33e75ff09dd601bbe69f351039152189
+ 30 | 34173cb38f07f89ddbebc2ac9128303f
+ 11 | 6512bd43d9caa6e02c990b0a82652dca
+ 15 | 9bf31c7ff062936a96d3c8bd1f8f2ff3
+ 19 | 1f0e3dad99908345f7439f8ffabdffc4
+ 20 | 98f13708210194c475687be6106a3b84
+ 21 | 3c59dc048e8850243be8079a5c74d079
+ 22 | b6d767d2f8ed5d21a44b0e5886680cb9
+ 24 | 1ff1de774005f8da13f42943881c655f
+ 29 | 6ea9ab1baa0efb9e19094440c317e21b
+(20 rows)
+
+TRUNCATE tru_pt;
+SELECT * FROM tru_pt;
+ id | y 
+----+---
+(0 rows)
+
+SELECT * FROM tru_pt__p0;
+ id | y 
+----+---
+(0 rows)
+
+SELECT * FROM tru_pt__p1;
+ id | y 
+----+---
+(0 rows)
+
+SELECT * FROM __tru_pt__p1;
+ id | y 
+----+---
+(0 rows)
+
+-- 'CASCADE' option
+SELECT * FROM tru_ft_ref;
+ id |                x                 
+----+----------------------------------
+  1 | c81e728d9d4c2f636f067f89cc14862c
+  2 | eccbc87e4b5ce2fe28308fd9f2a7baf3
+  3 | a87ff679a2f3e71d9181a67b7542122c
+  4 | e4da3b7fbbce2345d7772b0674a318d5
+  5 | 1679091c5a880faf6fb5e6087eb1b2dc
+  6 | 8f14e45fceea167a5a36dedd4bea2543
+  7 | c9f0f895fb98ab9159f51fd0297e236d
+  8 | 45c48cce2e2d7fbdea1afc51c7c6ad26
+  9 | d3d9446802a44259755d38e6d163e820
+ 10 | 6512bd43d9caa6e02c990b0a82652dca
+(10 rows)
+
+TRUNCATE tru_ft_ref;	-- failed
+ERROR:  cannot truncate a table referenced in a foreign key constraint
+DETAIL:  Table "tru_fk" references "tru_pk".
+HINT:  Truncate table "tru_fk" at the same time, or use TRUNCATE ... CASCADE.
+CONTEXT:  remote SQL command: TRUNCATE public.tru_pk CONTINUE IDENTITY RESTRICT
+TRUNCATE tru_ft_ref CASCADE;
+SELECT * FROM tru_ft_ref;
+ id | x 
+----+---
+(0 rows)
+
+-- truncate two tables at a command
+INSERT INTO tru_ft (SELECT x,md5((x+2)::text) FROM generate_series(1,8) x);
+INSERT INTO tru_ft_ref (SELECT x,md5((x+3)::text) FROM generate_series(1,8) x);
+SELECT * FROM tru_ft a, tru_ft_ref b WHERE a.id = b.id;
+ id |                x                 | id |                x                 
+----+----------------------------------+----+----------------------------------
+  1 | eccbc87e4b5ce2fe28308fd9f2a7baf3 |  1 | a87ff679a2f3e71d9181a67b7542122c
+  2 | a87ff679a2f3e71d9181a67b7542122c |  2 | e4da3b7fbbce2345d7772b0674a318d5
+  3 | e4da3b7fbbce2345d7772b0674a318d5 |  3 | 1679091c5a880faf6fb5e6087eb1b2dc
+  4 | 1679091c5a880faf6fb5e6087eb1b2dc |  4 | 8f14e45fceea167a5a36dedd4bea2543
+  5 | 8f14e45fceea167a5a36dedd4bea2543 |  5 | c9f0f895fb98ab9159f51fd0297e236d
+  6 | c9f0f895fb98ab9159f51fd0297e236d |  6 | 45c48cce2e2d7fbdea1afc51c7c6ad26
+  7 | 45c48cce2e2d7fbdea1afc51c7c6ad26 |  7 | d3d9446802a44259755d38e6d163e820
+  8 | d3d9446802a44259755d38e6d163e820 |  8 | 6512bd43d9caa6e02c990b0a82652dca
+(8 rows)
+
+TRUNCATE tru_ft, tru_ft_ref CASCADE;
+SELECT * FROM tru_ft a, tru_ft_ref b WHERE a.id = b.id;
+ id | x | id | x 
+----+---+----+---
+(0 rows)
+
 -- ===================================================================
 -- test IMPORT FOREIGN SCHEMA
 -- ===================================================================
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 2175dff824..ded54ab5e9 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -373,6 +373,9 @@ static void postgresExplainForeignModify(ModifyTableState *mtstate,
 										 ExplainState *es);
 static void postgresExplainDirectModify(ForeignScanState *node,
 										ExplainState *es);
+static void postgresExecForeignTruncate(List *frels_list,
+										DropBehavior behavior,
+										bool restart_seqs);
 static bool postgresAnalyzeForeignTable(Relation relation,
 										AcquireSampleRowsFunc *func,
 										BlockNumber *totalpages);
@@ -546,6 +549,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;
 
@@ -2656,6 +2662,51 @@ postgresExplainDirectModify(ForeignScanState *node, ExplainState *es)
 	}
 }
 
+/*
+ * postgresExecForeignTruncate
+ *		It propagates TRUNCATE command to the remote host inside of the
+ *		transaction block.
+ */
+static void
+postgresExecForeignTruncate(List *frels_list,
+							DropBehavior behavior,
+							bool restart_seqs)
+{
+	Oid				server_id = InvalidOid;
+	UserMapping	   *user = NULL;
+	PGconn		   *conn = NULL;
+	PGresult	   *res;
+	StringInfoData	sql;
+	ListCell	   *lc;
+
+	/* pick up remote connection, and sanity checks */
+	foreach (lc, frels_list)
+	{
+		Relation	frel = lfirst(lc);
+		Oid			frel_oid = RelationGetRelid(frel);
+
+		if (!OidIsValid(server_id))
+		{
+			server_id = GetForeignServerIdByRelId(frel_oid);
+			user = GetUserMapping(GetUserId(), server_id);
+			conn = GetConnection(user, false);
+		}
+		else if (server_id != GetForeignServerIdByRelId(frel_oid))
+			elog(ERROR, "Bug? inconsistent Server-IDs were supplied");
+	}
+	/* set up remote query */
+	initStringInfo(&sql);
+	deparseTruncateSql(&sql, frels_list, behavior, restart_seqs);
+	/* run remote query */
+	if (!PQsendQuery(conn, sql.data))
+		pgfdw_report_error(ERROR, NULL, conn, false, sql.data);
+	res = pgfdw_get_result(conn, sql.data);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		pgfdw_report_error(ERROR, res, conn, true, sql.data);
+	/* clean-up */
+	PQclear(res);
+	pfree(sql.data);
+}
 
 /*
  * estimate_path_cost_size
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index eef410db39..1d75bf363a 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -190,6 +190,8 @@ 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 *frels_list,
+							   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 acd7275c72..95c6e45975 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -2287,6 +2287,57 @@ select * from rem3;
 drop foreign table rem3;
 drop table loc3;
 
+-- ===================================================================
+-- test for TRUNCATE
+-- ===================================================================
+CREATE TABLE tru_nt (id int, x text);
+CREATE FOREIGN TABLE tru_ft (id int, x text)
+       SERVER loopback OPTIONS (table_name 'tru_nt');
+INSERT INTO tru_nt (SELECT x,md5(x::text) FROM generate_series(1,10) x);
+
+CREATE TABLE tru_pt (id int, y text) PARTITION BY HASH(id);
+CREATE TABLE tru_pt__p0 PARTITION OF tru_pt
+                        FOR VALUES WITH (MODULUS 2, REMAINDER 0);
+CREATE TABLE __tru_pt__p1 (id int, y text);
+CREATE FOREIGN TABLE tru_pt__p1 PARTITION OF tru_pt
+                                FOR VALUES WITH (MODULUS 2, REMAINDER 1)
+               SERVER loopback OPTIONS (table_name '__tru_pt__p1');
+INSERT INTO tru_pt (SELECT x,md5(x::text) FROM generate_series(11,30) x);
+
+CREATE TABLE tru_pk (id int primary key, x text);
+INSERT INTO tru_pk (SELECT x,md5((x+1)::text) FROM generate_series(1,10) x);
+CREATE TABLE tru_fk (fkey int references tru_pk(id));
+INSERT INTO tru_fk (SELECT x % 10 + 1 FROM generate_series(1,30) x);
+CREATE FOREIGN TABLE tru_ft_ref (id int, x text)
+       SERVER loopback OPTIONS (table_name 'tru_pk');
+
+-- normal truncate
+SELECT * FROM tru_ft;
+TRUNCATE tru_ft;
+SELECT * FROM tru_nt;
+SELECT * FROM tru_ft;
+
+-- partition table mixtured by table and foreign table
+SELECT * FROM tru_pt;
+TRUNCATE tru_pt;
+SELECT * FROM tru_pt;
+SELECT * FROM tru_pt__p0;
+SELECT * FROM tru_pt__p1;
+SELECT * FROM __tru_pt__p1;
+
+-- 'CASCADE' option
+SELECT * FROM tru_ft_ref;
+TRUNCATE tru_ft_ref;	-- failed
+TRUNCATE tru_ft_ref CASCADE;
+SELECT * FROM tru_ft_ref;
+
+-- truncate two tables at a command
+INSERT INTO tru_ft (SELECT x,md5((x+2)::text) FROM generate_series(1,8) x);
+INSERT INTO tru_ft_ref (SELECT x,md5((x+3)::text) FROM generate_series(1,8) x);
+SELECT * FROM tru_ft a, tru_ft_ref b WHERE a.id = b.id;
+TRUNCATE tru_ft, tru_ft_ref CASCADE;
+SELECT * FROM tru_ft a, tru_ft_ref b WHERE a.id = b.id;
+
 -- ===================================================================
 -- test IMPORT FOREIGN SCHEMA
 -- ===================================================================
diff --git a/doc/src/sgml/fdwhandler.sgml b/doc/src/sgml/fdwhandler.sgml
index 6587678af2..8a75d06047 100644
--- a/doc/src/sgml/fdwhandler.sgml
+++ b/doc/src/sgml/fdwhandler.sgml
@@ -968,6 +968,45 @@ EndDirectModify(ForeignScanState *node);
 
    </sect2>
 
+   <sect2 id="fdw-callbacks-truncate">
+    <title>FDW Routines for Truncate</title>
+<programlisting>
+void
+ExecForeignTruncate(List *frels_list,
+                    DropBehavior behavior, bool restart_seqs);
+</programlisting>
+    <para>
+     Executes remote truncate on the remote relation associated with this
+     foreign table. This optional function is called during execution of
+     <command>TRUNCATE</command>, for each foreign server being involved.
+     (Note that invocations are not per foreign table.)
+
+     If FDW does not provide this callback, PostgreSQL performs as if
+     <command>TRUNCATE</command> is not supported on the foreign table.
+    </para>
+    <para>
+     <literal>frels_list</literal> is a list of foreign tables that are
+     connected to a particular foreign server; thus, these foreign tables
+     should have identical foreign server ID.
+
+     <literal>behavior</literal> tells the FDW driver which options does
+     <command>TRUNCATE</command> command took; <literal>CASCADE</literal>
+     or <literal>RESTRICT</literal>.
+     <literal>restart_seqs</literal> also tells the FDW driver whether
+     <literal>RESTART IDENTITY</literal> option was supplied, or not.
+    </para>
+    <para>
+     Pay attention that <command>TRUNCATE</command> can be transaction-safe
+     in <productname>PostgreSQL</productname>.
+     We can revert the <command>TRUNCATE</command> operation unless current
+     transaction is not committed, however, it is not true on some other
+     database products.
+     If FDW author considers to support <command>TRUNCATE</command> on their
+     FDW driver, it may not be sufficient just to run a
+     <command>TRUNCATE</command> command on the remote side.
+    </para>
+   </sect2>
+
    <sect2 id="fdw-callbacks-row-locking">
     <title>FDW Routines for Row Locking</title>
 
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 30b72b6297..70a2e2d1bc 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -60,6 +60,7 @@
 #include "commands/user.h"
 #include "executor/executor.h"
 #include "foreign/foreign.h"
+#include "foreign/fdwapi.h"
 #include "miscadmin.h"
 #include "nodes/makefuncs.h"
 #include "nodes/nodeFuncs.h"
@@ -1628,6 +1629,16 @@ ExecuteTruncate(TruncateStmt *stmt)
 	}
 }
 
+/*
+ * ForeignTruncateInfo
+ */
+typedef struct
+{
+	Oid		server_id;
+	FdwRoutine *routine;
+	List   *frels_list;
+} ForeignTruncateInfo;
+
 /*
  * ExecuteTruncateGuts
  *
@@ -1647,6 +1658,7 @@ ExecuteTruncateGuts(List *explicit_rels, List *relids, List *relids_logged,
 {
 	List	   *rels;
 	List	   *seq_relids = NIL;
+	HTAB	   *ft_htab = NULL;
 	EState	   *estate;
 	ResultRelInfo *resultRelInfos;
 	ResultRelInfo *resultRelInfo;
@@ -1791,6 +1803,40 @@ ExecuteTruncateGuts(List *explicit_rels, List *relids, List *relids_logged,
 		/* Skip partitioned tables as there is nothing to do */
 		if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
 			continue;
+		/* Remember the server and foreign-tables to be truncated */
+		if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+		{
+			Oid		frel_oid = RelationGetRelid(rel);
+			Oid		server_id = GetForeignServerIdByRelId(frel_oid);
+			bool	found;
+			ForeignTruncateInfo *ft_info;
+
+			if (!ft_htab)
+			{
+				HASHCTL		hctl;
+
+				memset(&hctl, 0, sizeof(HASHCTL));
+				hctl.keysize = sizeof(Oid);
+				hctl.entrysize = sizeof(ForeignTruncateInfo);
+				/* temporary usage, to be released soon */
+				hctl.hcxt = CurrentMemoryContext;
+				ft_htab = hash_create("TRUNCATE Foreign Tables",
+									  128,
+									  &hctl,
+									  HASH_ELEM | HASH_BLOBS | HASH_CONTEXT);
+			}
+			ft_info = hash_search(ft_htab, &server_id, HASH_ENTER, &found);
+			if (!found)
+			{
+				FdwRoutine *routine = GetFdwRoutineByServerId(server_id);
+				ft_info->server_id = server_id;
+				ft_info->routine = routine;
+				ft_info->frels_list = NIL;
+				Assert(routine->ExecForeignTruncate != NULL);
+			}
+			ft_info->frels_list = lappend(ft_info->frels_list, rel);
+			continue;
+		}
 
 		/*
 		 * Normally, we need a transaction-safe truncation here.  However, if
@@ -1852,6 +1898,25 @@ ExecuteTruncateGuts(List *explicit_rels, List *relids, List *relids_logged,
 		pgstat_count_truncate(rel);
 	}
 
+	/*
+	 * Invocation of FDW handler for each foreign server
+	 */
+	if (ft_htab)
+	{
+		ForeignTruncateInfo *ft_info;
+		HASH_SEQ_STATUS	seq;
+
+		hash_seq_init(&seq, ft_htab);
+		while ((ft_info = hash_seq_search(&seq)) != NULL)
+		{
+			FdwRoutine *routine = ft_info->routine;
+
+			routine->ExecForeignTruncate(ft_info->frels_list,
+										 behavior,
+										 restart_seqs);
+		}
+	}
+
 	/*
 	 * Restart owned sequences if we were asked to.
 	 */
@@ -1939,12 +2004,29 @@ truncate_check_rel(Oid relid, Form_pg_class reltuple)
 	char	   *relname = NameStr(reltuple->relname);
 
 	/*
-	 * Only allow truncate on regular tables and partitioned tables (although,
+	 * Only allow truncate on regular tables, partitioned tables (although,
 	 * the latter are only being included here for the following checks; no
-	 * physical truncation will occur in their case.)
+	 * physical truncation will occur in their case.) and foreign tables
+	 * that support ExecForeignTruncate callback.
 	 */
-	if (reltuple->relkind != RELKIND_RELATION &&
-		reltuple->relkind != RELKIND_PARTITIONED_TABLE)
+	if (reltuple->relkind == RELKIND_FOREIGN_TABLE)
+	{
+		Oid			server_id = GetForeignServerIdByRelId(relid);
+		FdwRoutine *fdwroutine = GetFdwRoutineByServerId(server_id);
+
+		if (!fdwroutine->ExecForeignTruncate)
+		{
+			ForeignServer  *server = GetForeignServer(server_id);
+			ForeignDataWrapper *fdw = GetForeignDataWrapper(server->fdwid);
+
+			ereport(ERROR,
+					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+					 errmsg("foreign data wrapper \"%s\" on behalf of the foreign table \"%s\" does not support TRUNCATE",
+							fdw->fdwname, 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/include/foreign/fdwapi.h b/src/include/foreign/fdwapi.h
index 95556dfb15..91105939b2 100644
--- a/src/include/foreign/fdwapi.h
+++ b/src/include/foreign/fdwapi.h
@@ -118,6 +118,10 @@ typedef TupleTableSlot *(*IterateDirectModify_function) (ForeignScanState *node)
 
 typedef void (*EndDirectModify_function) (ForeignScanState *node);
 
+typedef void (*ExecForeignTruncate_function) (List *frels_list,
+											  DropBehavior behavior,
+											  bool restart_seqs);
+
 typedef RowMarkType (*GetForeignRowMarkType_function) (RangeTblEntry *rte,
 													   LockClauseStrength strength);
 
@@ -220,6 +224,9 @@ typedef struct FdwRoutine
 	IterateDirectModify_function IterateDirectModify;
 	EndDirectModify_function EndDirectModify;
 
+	/* Functions for truncating foreign tables */
+	ExecForeignTruncate_function ExecForeignTruncate;
+
 	/* Functions for SELECT FOR UPDATE/SHARE row locking */
 	GetForeignRowMarkType_function GetForeignRowMarkType;
 	RefetchForeignRow_function RefetchForeignRow;
diff --git a/src/test/regress/expected/foreign_data.out b/src/test/regress/expected/foreign_data.out
index b9e25820bc..e2c0bcea51 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
