contrib/postgres_fdw/deparse.c | 9 ++
contrib/postgres_fdw/expected/postgres_fdw.out | 144 +++++++++++++++++++++++++
contrib/postgres_fdw/postgres_fdw.c | 57 ++++++++++
contrib/postgres_fdw/postgres_fdw.h | 1 +
contrib/postgres_fdw/sql/postgres_fdw.sql | 51 +++++++++
doc/src/sgml/fdwhandler.sgml | 37 +++++++
src/backend/commands/tablecmds.c | 60 ++++++++++-
src/include/foreign/fdwapi.h | 7 ++
8 files changed, 362 insertions(+), 4 deletions(-)
diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index a3639e9604..aaa38cb482 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -2098,6 +2098,15 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
deparseRelation(buf, rel);
}
+/*
+ * Construct a simple "TRUNCATE rel" statement
+ */
+void
+deparseTruncateSql(StringInfo buf, Relation rel)
+{
+ deparseRelation(buf, rel);
+}
+
/*
* 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 0912d6cd5e..0421f172f5 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..0cbbb58e89 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,
+ bool is_cascade,
+ 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,57 @@ 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,
+ bool is_cascade,
+ bool restart_seqs)
+{
+ Oid server_id = InvalidOid;
+ UserMapping *user = NULL;
+ PGconn *conn = NULL;
+ PGresult *res;
+ StringInfoData sql;
+ ListCell *lc;
+
+ /* set up remote query */
+ initStringInfo(&sql);
+ appendStringInfoString(&sql, "TRUNCATE ");
+ 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
+ {
+ Assert(server_id == GetForeignServerIdByRelId(frel_oid));
+ appendStringInfoString(&sql, ", ");
+ }
+ deparseTruncateSql(&sql, frel);
+ }
+ appendStringInfo(&sql, " %s IDENTITY %s",
+ restart_seqs ? "RESTART" : "CONTINUE",
+ is_cascade ? "CASCADE" : "RESTRICT");
+
+ 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..ab2eefb126 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -190,6 +190,7 @@ 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, Relation rel);
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 f2a4089e9d..44857bf5a4 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..0c16f90d8d 100644
--- a/doc/src/sgml/fdwhandler.sgml
+++ b/doc/src/sgml/fdwhandler.sgml
@@ -968,6 +968,43 @@ EndDirectModify(ForeignScanState *node);
+
+ FDW Routines for Truncate
+
+void
+ExecForeignTruncate(List *frels_list, bool is_cascade, bool restart_seqs);
+
+
+ Executes remote truncate on the remote relation associated with this
+ foreign table. This optional function is called during execution of
+ TRUNCATE, for each foreign server being involved.
+ (Note that invocations are not per foreign table.)
+
+ If FDW does not provide this callback, PostgreSQL considers
+ TRUNCATE is not supported on the foreign table.
+
+
+ frels_list is a list of foreign tables that are
+ connected to a particular foreign server; thus, these foreign tables
+ should have identical foreign server ID.
+
+ is_cascade tells us whether CASCADE
+ option is supplied, and restart_seqs also tells us
+ whether RESTART IDENTITY option is supplied at the
+ TRUNCATE command.
+
+
+ Pay attention that TRUNCATE can be transaction-safe
+ in PostgreSQL.
+ We can revert the TRUNCATE operation unless current
+ transaction is not committed, however, it is not true on some other
+ database products.
+ If FDW author considers to support TRUNCATE on their
+ FDW driver, it may not be sufficient just to run a
+ TRUNCATE command on the remote side.
+
+
+
FDW Routines for Row Locking
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 421bc28727..2f80451060 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"
@@ -1624,6 +1625,8 @@ ExecuteTruncateGuts(List *explicit_rels, List *relids, List *relids_logged,
{
List *rels;
List *seq_relids = NIL;
+ List *servs_list = NIL;
+ List *frels_list = NIL;
EState *estate;
ResultRelInfo *resultRelInfos;
ResultRelInfo *resultRelInfo;
@@ -1768,6 +1771,16 @@ 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);
+
+ servs_list = list_append_unique_oid(servs_list, server_id);
+ frels_list = lappend(frels_list, rel);
+ continue;
+ }
/*
* Normally, we need a transaction-safe truncation here. However, if
@@ -1829,6 +1842,34 @@ ExecuteTruncateGuts(List *explicit_rels, List *relids, List *relids_logged,
pgstat_count_truncate(rel);
}
+ /*
+ * Invocation of FDW handler for each foreign server
+ */
+ foreach (cell, servs_list)
+ {
+ Oid server_id = lfirst_oid(cell);
+ FdwRoutine *routine = GetFdwRoutineByServerId(server_id);
+ List *curr_frels = NIL;
+ ListCell *lc;
+
+ foreach (lc, frels_list)
+ {
+ Relation frel = lfirst(lc);
+ Oid frel_oid = RelationGetRelid(frel);
+
+ if (server_id == GetForeignServerIdByRelId(frel_oid))
+ {
+ frels_list = foreach_delete_current(frels_list, lc);
+ curr_frels = lappend(curr_frels, frel);
+ }
+ }
+ Assert(curr_frels != NIL);
+ Assert(routine->ExecForeignTruncate != NULL);
+ routine->ExecForeignTruncate(curr_frels,
+ behavior == DROP_CASCADE,
+ restart_seqs);
+ }
+
/*
* Restart owned sequences if we were asked to.
*/
@@ -1916,12 +1957,23 @@ 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)
+ {
+ FdwRoutine *fdwroutine = GetFdwRoutineByRelId(relid);
+
+ if (!fdwroutine->ExecForeignTruncate)
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("\"%s\" is not a supported foreign table",
+ 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..c8864f2052 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,
+ bool is_cascade,
+ 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;