TRUNCATE on foreign tables
Hello,
We right now don't support TRUNCATE on foreign tables.
It may be a strange missing piece and restriction of operations.
For example, if a partitioned table contains some foreign tables in its leaf,
user cannot use TRUNCATE command to clean up the partitioned table.
Probably, API design is not complicated. We add a new callback for truncate
on the FdwRoutine, and ExecuteTruncateGuts() calls it if relation is foreign-
table. In case of postgres_fdw, it also issues "TRUNCATE" command on the
remote side in the transaction block [*1].
[*1] But I hope oracle_fdw does not follow this implementation as is. :-)
How about your thought?
I noticed this restriction when I'm working on Arrow_Fdw enhancement for
"writable" capability. Because Apache Arrow [*2] is a columnar file format,
it is not designed for UPDATE/DELETE, but capable to bulk-INSERT.
It is straightforward idea to support only INSERT, and clear data by TRUNCATE.
[*2] Apache Arrow - https://arrow.apache.org/docs/format/Columnar.html
Best regards,
--
HeteroDB, Inc / The PG-Strom Project
KaiGai Kohei <kaigai@heterodb.com>
Hello,
The attached patch adds TRUNCATE support on foreign table.
This patch adds an optional callback ExecForeignTruncate(Relation rel)
to FdwRoutine.
It is invoked during ExecuteTruncateGuts, then FDW driver hands over
the jobs related
to complete "truncate on the foreign table".
Of course, it is not clear to define the concept of "truncate" on some
FDW drivers.
In this case, TRUNCATE command prohibits to apply these foreign tables.
2019 is not finished at everywhere on the earth yet, so I believe it
is Ok to add this patch
to CF-2020:Jan.
Best regards,
2020年1月1日(水) 11:46 Kohei KaiGai <kaigai@heterodb.com>:
Hello,
We right now don't support TRUNCATE on foreign tables.
It may be a strange missing piece and restriction of operations.
For example, if a partitioned table contains some foreign tables in its leaf,
user cannot use TRUNCATE command to clean up the partitioned table.Probably, API design is not complicated. We add a new callback for truncate
on the FdwRoutine, and ExecuteTruncateGuts() calls it if relation is foreign-
table. In case of postgres_fdw, it also issues "TRUNCATE" command on the
remote side in the transaction block [*1].[*1] But I hope oracle_fdw does not follow this implementation as is. :-)
How about your thought?
I noticed this restriction when I'm working on Arrow_Fdw enhancement for
"writable" capability. Because Apache Arrow [*2] is a columnar file format,
it is not designed for UPDATE/DELETE, but capable to bulk-INSERT.
It is straightforward idea to support only INSERT, and clear data by TRUNCATE.[*2] Apache Arrow - https://arrow.apache.org/docs/format/Columnar.html
Best regards,
--
HeteroDB, Inc / The PG-Strom Project
KaiGai Kohei <kaigai@heterodb.com>
--
HeteroDB, Inc / The PG-Strom Project
KaiGai Kohei <kaigai@heterodb.com>
Attachments:
pgsql13-truncate-on-foreign-table.v1.patchapplication/octet-stream; name=pgsql13-truncate-on-foreign-table.v1.patchDownload
contrib/postgres_fdw/deparse.c | 10 +++
contrib/postgres_fdw/expected/postgres_fdw.out | 89 ++++++++++++++++++++++++++
contrib/postgres_fdw/postgres_fdw.c | 31 +++++++++
contrib/postgres_fdw/postgres_fdw.h | 1 +
contrib/postgres_fdw/sql/postgres_fdw.sql | 31 +++++++++
doc/src/sgml/fdwhandler.sgml | 26 ++++++++
src/backend/commands/tablecmds.c | 28 ++++++--
src/include/foreign/fdwapi.h | 5 ++
8 files changed, 217 insertions(+), 4 deletions(-)
diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index e803997..99d6a05 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -2099,6 +2099,16 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
}
/*
+ * Construct a simple "TRUNCATE rel" statement
+ */
+void
+deparseTruncateSql(StringInfo buf, Relation rel)
+{
+ appendStringInfoString(buf, "TRUNCATE ");
+ 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 c915885..2a494f8 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8157,6 +8157,95 @@ 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);
+-- 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)
+
+-- ===================================================================
-- test IMPORT FOREIGN SCHEMA
-- ===================================================================
CREATE SCHEMA import_source;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index bdc21b3..9fd8bc7 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -373,6 +373,7 @@ static void postgresExplainForeignModify(ModifyTableState *mtstate,
ExplainState *es);
static void postgresExplainDirectModify(ForeignScanState *node,
ExplainState *es);
+static void postgresExecForeignTruncate(Relation relation);
static bool postgresAnalyzeForeignTable(Relation relation,
AcquireSampleRowsFunc *func,
BlockNumber *totalpages);
@@ -546,6 +547,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 +2660,33 @@ postgresExplainDirectModify(ForeignScanState *node, ExplainState *es)
}
}
+/*
+ * postgresExecForeignTruncate
+ * It propagates TRUNCATE command to the remote host inside of the
+ * transaction block.
+ */
+static void
+postgresExecForeignTruncate(Relation frel)
+{
+ ForeignTable *ft = GetForeignTable(RelationGetRelid(frel));
+ UserMapping *user = GetUserMapping(GetUserId(), ft->serverid);
+ PGconn *conn = GetConnection(user, false);
+ PGresult *res;
+ StringInfoData sql;
+
+ /* set up remote query */
+ initStringInfo(&sql);
+ deparseTruncateSql(&sql, frel);
+
+ 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 ea05287..a055ad7 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 4f29e7c..0b6a48d 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -2275,6 +2275,37 @@ 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);
+
+-- 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;
+
+-- ===================================================================
-- test IMPORT FOREIGN SCHEMA
-- ===================================================================
diff --git a/doc/src/sgml/fdwhandler.sgml b/doc/src/sgml/fdwhandler.sgml
index 6587678..ad91768 100644
--- a/doc/src/sgml/fdwhandler.sgml
+++ b/doc/src/sgml/fdwhandler.sgml
@@ -968,6 +968,32 @@ EndDirectModify(ForeignScanState *node);
</sect2>
+ <sect2 id="fdw-callbacks-truncate">
+ <title>FDW Routines for Truncate</title>
+<programlisting>
+void
+ExecForeignTruncate(Relation frel);
+</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>.
+
+ If FDW does not provide this callback, PostgreSQL considers
+ <command>TRUNCATE</command> is not supported on the foreign table.
+ </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 e19772a..1312b24 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"
@@ -1765,6 +1766,14 @@ 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;
+ /* FDW driver runs its "TRUNCATE" implementation */
+ if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+ {
+ FdwRoutine *fdwroutine = GetFdwRoutineForRelation(rel, false);
+
+ fdwroutine->ExecForeignTruncate(rel);
+ continue;
+ }
/*
* Normally, we need a transaction-safe truncation here. However, if
@@ -1913,12 +1922,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 8226860..20adfb5 100644
--- a/src/include/foreign/fdwapi.h
+++ b/src/include/foreign/fdwapi.h
@@ -118,6 +118,8 @@ typedef TupleTableSlot *(*IterateDirectModify_function) (ForeignScanState *node)
typedef void (*EndDirectModify_function) (ForeignScanState *node);
+typedef void (*ExecForeignTruncate_function) (Relation frel);
+
typedef RowMarkType (*GetForeignRowMarkType_function) (RangeTblEntry *rte,
LockClauseStrength strength);
@@ -220,6 +222,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;
On 2020-Jan-01, Kohei KaiGai wrote:
Hello,
The attached patch adds TRUNCATE support on foreign table.
This patch adds an optional callback ExecForeignTruncate(Relation rel)
to FdwRoutine.
It is invoked during ExecuteTruncateGuts, then FDW driver hands over
the jobs related to complete "truncate on the foreign table".
I think this would need to preserve the notion of multi-table truncates.
Otherwise it won't be possible to truncate tables linked by FKs. I
think this means the new entrypoint needs to receive a list of rels to
truncate, not just one. (Maybe an alternative is to make it "please
truncate rel X, and be aware that relations Y,Z are also being
truncated at the same time".)
Looking at apache arrow documentation, it doesn't appear that it has
anything like FK constraints.
--
�lvaro Herrera https://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services
2020年1月2日(木) 12:16 Alvaro Herrera <alvherre@2ndquadrant.com>:
On 2020-Jan-01, Kohei KaiGai wrote:
Hello,
The attached patch adds TRUNCATE support on foreign table.
This patch adds an optional callback ExecForeignTruncate(Relation rel)
to FdwRoutine.
It is invoked during ExecuteTruncateGuts, then FDW driver hands over
the jobs related to complete "truncate on the foreign table".I think this would need to preserve the notion of multi-table truncates.
Otherwise it won't be possible to truncate tables linked by FKs. I
think this means the new entrypoint needs to receive a list of rels to
truncate, not just one. (Maybe an alternative is to make it "please
truncate rel X, and be aware that relations Y,Z are also being
truncated at the same time".)
Please check at ExecuteTruncateGuts(). It makes a list of relations to be
truncated, including the relations that references the specified table by FK,
prior to invocation of the new FDW callback.
So, if multiple foreign tables are involved in a single TRUNCATE command,
this callback can be invoked multiple times.
Looking at apache arrow documentation, it doesn't appear that it has
anything like FK constraints.
Yes. It is just a bunch of columnar data.
In Apache Arrow, no constraint are defined except for "NOT NULL".
(In case when Field::nullable == false, all the values are considered
valid date.)
Thanks,
--
HeteroDB, Inc / The PG-Strom Project
KaiGai Kohei <kaigai@heterodb.com>
On 2020-Jan-02, Kohei KaiGai wrote:
2020年1月2日(木) 12:16 Alvaro Herrera <alvherre@2ndquadrant.com>:
I think this would need to preserve the notion of multi-table truncates.
Otherwise it won't be possible to truncate tables linked by FKs. I
think this means the new entrypoint needs to receive a list of rels to
truncate, not just one. (Maybe an alternative is to make it "please
truncate rel X, and be aware that relations Y,Z are also being
truncated at the same time".)Please check at ExecuteTruncateGuts(). It makes a list of relations to be
truncated, including the relations that references the specified table by FK,
prior to invocation of the new FDW callback.
So, if multiple foreign tables are involved in a single TRUNCATE command,
this callback can be invoked multiple times.
Yeah, that's my concern: if you have postgres_fdw tables linked by FKs
in the remote server, the truncate will fail because it'll try to
truncate them in separate commands instead of using a multi-table
truncate.
Looking at apache arrow documentation, it doesn't appear that it has
anything like FK constraints.Yes. It is just a bunch of columnar data.
In Apache Arrow, no constraint are defined except for "NOT NULL".
(In case when Field::nullable == false, all the values are considered
valid date.)
OK, I suppose that means there are no concerns such as what I mention
above.
--
Álvaro Herrera https://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services
2020年1月2日(木) 20:56 Alvaro Herrera <alvherre@2ndquadrant.com>:
On 2020-Jan-02, Kohei KaiGai wrote:
2020年1月2日(木) 12:16 Alvaro Herrera <alvherre@2ndquadrant.com>:
I think this would need to preserve the notion of multi-table truncates.
Otherwise it won't be possible to truncate tables linked by FKs. I
think this means the new entrypoint needs to receive a list of rels to
truncate, not just one. (Maybe an alternative is to make it "please
truncate rel X, and be aware that relations Y,Z are also being
truncated at the same time".)Please check at ExecuteTruncateGuts(). It makes a list of relations to be
truncated, including the relations that references the specified table by FK,
prior to invocation of the new FDW callback.
So, if multiple foreign tables are involved in a single TRUNCATE command,
this callback can be invoked multiple times.Yeah, that's my concern: if you have postgres_fdw tables linked by FKs
in the remote server, the truncate will fail because it'll try to
truncate them in separate commands instead of using a multi-table
truncate.
Ah, it makes sense.
Probably, backend can make sub-list of the foreign tables to be
truncated for each
pair of FDW and Server, then invoke the FDW callback only once with this list.
FDW driver can issue multi-tables truncate on all the foreign tables
supplied, with
nothing difficult to do.
Best regards,
--
HeteroDB, Inc / The PG-Strom Project
KaiGai Kohei <kaigai@heterodb.com>
Greetings,
* Kohei KaiGai (kaigai@heterodb.com) wrote:
2020年1月2日(木) 20:56 Alvaro Herrera <alvherre@2ndquadrant.com>:
On 2020-Jan-02, Kohei KaiGai wrote:
2020年1月2日(木) 12:16 Alvaro Herrera <alvherre@2ndquadrant.com>:
I think this would need to preserve the notion of multi-table truncates.
Otherwise it won't be possible to truncate tables linked by FKs. I
think this means the new entrypoint needs to receive a list of rels to
truncate, not just one. (Maybe an alternative is to make it "please
truncate rel X, and be aware that relations Y,Z are also being
truncated at the same time".)Please check at ExecuteTruncateGuts(). It makes a list of relations to be
truncated, including the relations that references the specified table by FK,
prior to invocation of the new FDW callback.
Erm, sure it does, but we don't support having FKs on foreign tables
today, so that doesn't really help with this issue, does it?
So, if multiple foreign tables are involved in a single TRUNCATE command,
this callback can be invoked multiple times.Yeah, that's my concern: if you have postgres_fdw tables linked by FKs
in the remote server, the truncate will fail because it'll try to
truncate them in separate commands instead of using a multi-table
truncate.
I agree that the FDW callback should support multiple tables in the
TRUNCATE, but I think it also should include CASCADE as an option and
have that be passed on to the FDW to handle.
Ah, it makes sense.
Probably, backend can make sub-list of the foreign tables to be
truncated for each
pair of FDW and Server, then invoke the FDW callback only once with this list.
FDW driver can issue multi-tables truncate on all the foreign tables
supplied, with
nothing difficult to do.
This doesn't really make sense as we don't track FK relationships in the
local server for foreign tables today- now, perhaps we should (and things
like primary keys too..), but I don't think that needs to be the job of
this particular patch. Instead, I'd suggest we have the core code build
up a list of tables to truncate, for each server, based just on the list
passed in by the user, and then also pass in if CASCADE was included or
not, and then let the FDW handle that in whatever way makes sense for
the foreign server (which, for a PG system, would probably be just
building up the TRUNCATE command and running it with or without the
CASCADE option, but it might be different on other systems).
Just to be clear- I don't mean to suggest that we should explicitly
avoid the logic in TruncateGuts that builds up the list when CASCADE is
used, just saying that it's not going to actually do anything when we're
talking about foreign tables- and that's *fine*. I don't think we need
to do more here until we're actually tracking remote FKs locally.
So, I think the patch just needs a bit of minor adjustment for that to
make it work for the case that Alvaro is concerned about. One thing
that isn't really clear to me is if we should also support the 'ONLY'
option to TRUNCATE when it comes to FDWs; a table can't be both foreign
and partitioned, so it's not an issue there, but a foreign table CAN
be a child table of another foreign table.
Of course, if that's the case, things get pretty odd looking pretty
quickly if both sides see the table as a child table because we
actually end up scanning the foreign parent (which will include rows
from the child on the remote side) and then scanning the foreign child
*again*, resulting in duplicate rows coming back, so I'm not really sure
how much effort we should be thinking about putting into dealing with
child foreign tables..
Thanks,
Stephen
On Thu, Jan 02, 2020 at 09:46:44AM -0500, Stephen Frost wrote:
I agree that the FDW callback should support multiple tables in the
TRUNCATE, but I think it also should include CASCADE as an option and
have that be passed on to the FDW to handle.
As much as RESTRICT, ONLY and the INDENTITY clauses, no? Just think
about postgres_fdw.
--
Michael
Greetings,
* Michael Paquier (michael@paquier.xyz) wrote:
On Thu, Jan 02, 2020 at 09:46:44AM -0500, Stephen Frost wrote:
I agree that the FDW callback should support multiple tables in the
TRUNCATE, but I think it also should include CASCADE as an option and
have that be passed on to the FDW to handle.As much as RESTRICT, ONLY and the INDENTITY clauses, no? Just think
about postgres_fdw.
RESTRICT, yes. I don't know about ONLY being sensible as we don't
really deal with inheritance and foreign tables very cleanly today, as I
said up-thread, so I'm not sure if we really want to put in the effort
that it'd require to figure out how to make ONLY make sense. The
question about how to handle IDENTITY is a good one. I suppose we could
just pass that down and let the FDW sort it out..?
Thanks,
Stephen
On Mon, Jan 06, 2020 at 04:32:39PM -0500, Stephen Frost wrote:
RESTRICT, yes. I don't know about ONLY being sensible as we don't
really deal with inheritance and foreign tables very cleanly today, as I
said up-thread, so I'm not sure if we really want to put in the effort
that it'd require to figure out how to make ONLY make sense.
True enough.
The question about how to handle IDENTITY is a good one. I suppose
we could just pass that down and let the FDW sort it out..?
Looking at the code, ExecuteTruncateGuts() passes down restart_seqs,
so it sounds sensible to me to just pass down that to the FDW
callback and the callback decide what to do.
--
Michael
2020年1月7日(火) 16:03 Michael Paquier <michael@paquier.xyz>:
On Mon, Jan 06, 2020 at 04:32:39PM -0500, Stephen Frost wrote:
RESTRICT, yes. I don't know about ONLY being sensible as we don't
really deal with inheritance and foreign tables very cleanly today, as I
said up-thread, so I'm not sure if we really want to put in the effort
that it'd require to figure out how to make ONLY make sense.True enough.
The question about how to handle IDENTITY is a good one. I suppose
we could just pass that down and let the FDW sort it out..?Looking at the code, ExecuteTruncateGuts() passes down restart_seqs,
so it sounds sensible to me to just pass down that to the FDW
callback and the callback decide what to do.
It looks to me the local sequences owned by a foreign table shall be restarted
by the core, regardless of relkind of the owner relation. So, even if FDW driver
is buggy, consistency of the local database is kept, right?
Indeed, "restart_seqs" flag is needed to propagate the behavior, however,
it shall be processed on the remote side via the secondary "TRUNCATE" command.
Is it so sensitive?
Best regards,
--
HeteroDB, Inc / The PG-Strom Project
KaiGai Kohei <kaigai@heterodb.com>
Hello,
The attached patch is the revised version of TRUNCATE on foreign tables.
Definition of the callback is revised as follows:
typedef void (*ExecForeignTruncate_function) (List *frels_list,
bool is_cascade,
bool restart_seqs);
The "frels_list" is a list of foreign tables that are connected to a particular
foreign server, thus, the server-id pulled out by foreign tables id should be
identical for all the relations in the list.
Due to the API design, this callback shall be invoked for each foreign server
involved in the TRUNCATE command, not per table basis.
The 2nd and 3rd arguments also informs FDW driver other options of the
command. If FDW has a concept of "cascaded truncate" or "restart sequence",
it can adjust its remote query. In postgres_fdw, it follows the manner of
usual TRUNCATE command.
Best regards,
2020年1月8日(水) 1:08 Kohei KaiGai <kaigai@heterodb.com>:
2020年1月7日(火) 16:03 Michael Paquier <michael@paquier.xyz>:
On Mon, Jan 06, 2020 at 04:32:39PM -0500, Stephen Frost wrote:
RESTRICT, yes. I don't know about ONLY being sensible as we don't
really deal with inheritance and foreign tables very cleanly today, as I
said up-thread, so I'm not sure if we really want to put in the effort
that it'd require to figure out how to make ONLY make sense.True enough.
The question about how to handle IDENTITY is a good one. I suppose
we could just pass that down and let the FDW sort it out..?Looking at the code, ExecuteTruncateGuts() passes down restart_seqs,
so it sounds sensible to me to just pass down that to the FDW
callback and the callback decide what to do.It looks to me the local sequences owned by a foreign table shall be restarted
by the core, regardless of relkind of the owner relation. So, even if FDW driver
is buggy, consistency of the local database is kept, right?
Indeed, "restart_seqs" flag is needed to propagate the behavior, however,
it shall be processed on the remote side via the secondary "TRUNCATE" command.
Is it so sensitive?Best regards,
--
HeteroDB, Inc / The PG-Strom Project
KaiGai Kohei <kaigai@heterodb.com>
--
HeteroDB, Inc / The PG-Strom Project
KaiGai Kohei <kaigai@heterodb.com>
Attachments:
pgsql13-truncate-on-foreign-table.v2.patchapplication/octet-stream; name=pgsql13-truncate-on-foreign-table.v2.patchDownload
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);
</sect2>
+ <sect2 id="fdw-callbacks-truncate">
+ <title>FDW Routines for Truncate</title>
+<programlisting>
+void
+ExecForeignTruncate(List *frels_list, bool is_cascade, 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 considers
+ <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>is_cascade</literal> tells us whether <literal>CASCADE</literal>
+ option is supplied, and <literal>restart_seqs</literal> also tells us
+ whether <literal>RESTART IDENTITY</literal> option is supplied at the
+ <command>TRUNCATE</command> command.
+ </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 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;
On Tue, Jan 14, 2020 at 06:16:17PM +0900, Kohei KaiGai wrote:
The "frels_list" is a list of foreign tables that are connected to a particular
foreign server, thus, the server-id pulled out by foreign tables id should be
identical for all the relations in the list.
Due to the API design, this callback shall be invoked for each foreign server
involved in the TRUNCATE command, not per table basis.The 2nd and 3rd arguments also informs FDW driver other options of the
command. If FDW has a concept of "cascaded truncate" or "restart sequence",
it can adjust its remote query. In postgres_fdw, it follows the manner of
usual TRUNCATE command.
I have done a quick read through the patch. You have modified the
patch to pass down to the callback a list of relation OIDs to execute
one command for all, and there are tests for FKs so that coverage
looks fine.
Regression tests are failing with this patch:
-- 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
You visibly just need to update the output because no handlers are
available for truncate in this case.
+void
+deparseTruncateSql(StringInfo buf, Relation rel)
+{
+ deparseRelation(buf, rel);
+}
Don't see much point in having this routine.
+ If FDW does not provide this callback, PostgreSQL considers
+ <command>TRUNCATE</command> is not supported on the foreign table.
+ </para>
This sentence is weird. Perhaps you meant "as not supported"?
+ <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
The list is built by the backend code, so that has to be true.
+ 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);
+ }
+ }
Wouldn't it be better to fill in a hash table for each server with a
list of relations?
+typedef void (*ExecForeignTruncate_function) (List *frels_list,
+ bool is_cascade,
+ bool restart_seqs);
I would recommend to pass down directly DropBehavior instead of a
boolean to the callback. That's more extensible.
--
Michael
2020年1月15日(水) 17:11 Michael Paquier <michael@paquier.xyz>:
On Tue, Jan 14, 2020 at 06:16:17PM +0900, Kohei KaiGai wrote:
The "frels_list" is a list of foreign tables that are connected to a particular
foreign server, thus, the server-id pulled out by foreign tables id should be
identical for all the relations in the list.
Due to the API design, this callback shall be invoked for each foreign server
involved in the TRUNCATE command, not per table basis.The 2nd and 3rd arguments also informs FDW driver other options of the
command. If FDW has a concept of "cascaded truncate" or "restart sequence",
it can adjust its remote query. In postgres_fdw, it follows the manner of
usual TRUNCATE command.I have done a quick read through the patch. You have modified the
patch to pass down to the callback a list of relation OIDs to execute
one command for all, and there are tests for FKs so that coverage
looks fine.Regression tests are failing with this patch: -- 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 You visibly just need to update the output because no handlers are available for truncate in this case.
What error message is better in this case? It does not print "ft2" anywhere,
so user may not notice that "ft2" is the source of the error.
How about 'foreign table "ft2" does not support truncate' ?
+void +deparseTruncateSql(StringInfo buf, Relation rel) +{ + deparseRelation(buf, rel); +} Don't see much point in having this routine.
deparseRelation() is a static function in postgres_fdw/deparse.c
On the other hand, it may be better to move entire logic to construct
remote TRUNCATE command in the deparse.c side like other commands.
+ If FDW does not provide this callback, PostgreSQL considers + <command>TRUNCATE</command> is not supported on the foreign table. + </para> This sentence is weird. Perhaps you meant "as not supported"?
Yes.
If FDW does not provide this callback, PostgreSQL performs as if TRUNCATE
is not supported on the foreign table.
+ <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 The list is built by the backend code, so that has to be true.+ 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); + } + } Wouldn't it be better to fill in a hash table for each server with a list of relations?
It's just a matter of preference. A temporary hash-table with server-id
and list of foreign-tables is an idea. Let me try to revise.
+typedef void (*ExecForeignTruncate_function) (List *frels_list, + bool is_cascade, + bool restart_seqs); I would recommend to pass down directly DropBehavior instead of a boolean to the callback. That's more extensible.
Ok,
Best regards,
--
HeteroDB, Inc / The PG-Strom Project
KaiGai Kohei <kaigai@heterodb.com>
On Wed, Jan 15, 2020 at 11:33:07PM +0900, Kohei KaiGai wrote:
2020年1月15日(水) 17:11 Michael Paquier <michael@paquier.xyz>:
I have done a quick read through the patch. You have modified the
patch to pass down to the callback a list of relation OIDs to execute
one command for all, and there are tests for FKs so that coverage
looks fine.Regression tests are failing with this patch: -- 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 You visibly just need to update the output because no handlers are available for truncate in this case.What error message is better in this case? It does not print "ft2" anywhere,
so user may not notice that "ft2" is the source of the error.
How about 'foreign table "ft2" does not support truncate' ?
It sounds to me that this message is kind of right. This FDW "dummy"
does not have any FDW handler at all, so it complains about it.
Having no support for TRUNCATE is something that may happen after
that. Actually, this error message from your patch used for a FDW
which has a handler but no TRUNCATE support could be reworked:
+ if (!fdwroutine->ExecForeignTruncate)
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("\"%s\" is not a supported foreign table",
+ relname)));
Something like "Foreign-data wrapper \"%s\" does not support
TRUNCATE"?
+ <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 The list is built by the backend code, so that has to be true.+ 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); + } + } Wouldn't it be better to fill in a hash table for each server with a list of relations?It's just a matter of preference. A temporary hash-table with server-id
and list of foreign-tables is an idea. Let me try to revise.
Thanks. It would not matter much for relations without inheritance
children, but if truncating a partition tree with many foreign tables
using various FDWs that could matter performance-wise.
--
Michael
Hi,
The v3 patch updated the points below:
- 2nd arg of ExecForeignTruncate was changed to DropBehavior, not bool
- ExecuteTruncateGuts() uses a local hash table to track a pair of server-id
and list of the foreign tables managed by the server.
- Error message on truncate_check_rel() was revised as follows:
"foreign data wrapper \"%s\" on behalf of the foreign table \"%s\"
does not support TRUNCATE"
- deparseTruncateSql() of postgres_fdw generates entire remote SQL as
like other commands.
- Document SGML was updated.
Best regards,
2020年1月16日(木) 14:40 Michael Paquier <michael@paquier.xyz>:
On Wed, Jan 15, 2020 at 11:33:07PM +0900, Kohei KaiGai wrote:
2020年1月15日(水) 17:11 Michael Paquier <michael@paquier.xyz>:
I have done a quick read through the patch. You have modified the
patch to pass down to the callback a list of relation OIDs to execute
one command for all, and there are tests for FKs so that coverage
looks fine.Regression tests are failing with this patch: -- 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 You visibly just need to update the output because no handlers are available for truncate in this case.What error message is better in this case? It does not print "ft2" anywhere,
so user may not notice that "ft2" is the source of the error.
How about 'foreign table "ft2" does not support truncate' ?It sounds to me that this message is kind of right. This FDW "dummy" does not have any FDW handler at all, so it complains about it. Having no support for TRUNCATE is something that may happen after that. Actually, this error message from your patch used for a FDW which has a handler but no TRUNCATE support could be reworked: + if (!fdwroutine->ExecForeignTruncate) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("\"%s\" is not a supported foreign table", + relname))); Something like "Foreign-data wrapper \"%s\" does not support TRUNCATE"?+ <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 The list is built by the backend code, so that has to be true.+ 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); + } + } Wouldn't it be better to fill in a hash table for each server with a list of relations?It's just a matter of preference. A temporary hash-table with server-id
and list of foreign-tables is an idea. Let me try to revise.Thanks. It would not matter much for relations without inheritance
children, but if truncating a partition tree with many foreign tables
using various FDWs that could matter performance-wise.
--
Michael
--
HeteroDB, Inc / The PG-Strom Project
KaiGai Kohei <kaigai@heterodb.com>
Hello.
At Fri, 17 Jan 2020 22:49:41 +0900, Kohei KaiGai <kaigai@heterodb.com> wrote in
The v3 patch updated the points below:
Did you attached it?
regards.
--
Kyotaro Horiguchi
NTT Open Source Software Center
2020年1月20日(月) 11:09 Kyotaro Horiguchi <horikyota.ntt@gmail.com>:
Hello.
At Fri, 17 Jan 2020 22:49:41 +0900, Kohei KaiGai <kaigai@heterodb.com> wrote in
The v3 patch updated the points below:
Did you attached it?
Sorry, it was a midnight job on Friday.
Please check the attached patch.
Best regards,
--
HeteroDB, Inc / The PG-Strom Project
KaiGai Kohei <kaigai@heterodb.com>
Attachments:
pgsql13-truncate-on-foreign-table.v3.patchapplication/octet-stream; name=pgsql13-truncate-on-foreign-table.v3.patchDownload
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
On Mon, Jan 20, 2020 at 11:30:34AM +0900, Kohei KaiGai wrote:
Sorry, it was a midnight job on Friday.
Should I be, err, worried about that? ;)
Please check the attached patch.
+ 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;
+ }
Here, you can actually remove the default clause. By doing so,
compilation would generate a warning if a new value is added to
DropBehavior if it is not listed. So anybody adding a new value to
the enum will need to think about this code path.
+ 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)));
I see two problems here:
- The error message is too complicated. I would just use "cannot
truncate foreign table \"%s\"".
- The error code should be ERRCODE_FEATURE_NOT_SUPPORTED.
The docs for the FDW description can be improved. I found that a
large portion of it used rather unclear English, and that things were
not clear enough regarding the use of a list of relations, when an
error is raised because ExecForeignTruncate is NULL, etc. I have also
cut the last paragraph which is actually implementation-specific
(think for example about callbacks at xact commit/abort time).
Documentation needs to be added to postgres_fdw about the truncation
support. Particularly, providing details about the possibility to do
truncates in our shot for a set of relations so as dependencies are
automatically handled is an advantage to mention.
There is no need to include the truncate routine in
ForeignTruncateInfo, as the server OID can be used to find it.
Another thing is that I would prefer splitting the patch into two
separate commits, so attached are two patches:
- 0001 for the addition of the in-core API
- 0002 for the addition of support in postgres_fdw.
I have spent a good amount of time polishing 0001, tweaking the docs,
comments, error messages and a bit its logic. I am getting
comfortable with it, but it still needs an extra lookup, an indent run
which has some noise and I lacked of time today. 0002 has some of its
issues fixed and I have not reviewed it fully yet. There are still
some places not adapted in it (why do you use "Bug?" in all your
elog() messages by the way?), so the postgres_fdw part needs more
attention. Could you think about some docs for it by the way?
--
Michael
Attachments:
0001-Add-FDW-callback-for-support-of-TRUNCATE.patchtext/x-diff; charset=us-asciiDownload
From ed63df0fdeac43443b8e15709c19fe6c1f38a1f8 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Mon, 20 Jan 2020 22:39:34 +0900
Subject: [PATCH 1/2] Add FDW callback for support of TRUNCATE
---
src/include/foreign/fdwapi.h | 7 ++
src/backend/commands/tablecmds.c | 110 ++++++++++++++++++++-
src/test/regress/expected/foreign_data.out | 8 +-
doc/src/sgml/fdwhandler.sgml | 36 +++++++
4 files changed, 153 insertions(+), 8 deletions(-)
diff --git a/src/include/foreign/fdwapi.h b/src/include/foreign/fdwapi.h
index 95556dfb15..0a9f36735e 100644
--- a/src/include/foreign/fdwapi.h
+++ b/src/include/foreign/fdwapi.h
@@ -151,6 +151,10 @@ typedef bool (*AnalyzeForeignTable_function) (Relation relation,
typedef List *(*ImportForeignSchema_function) (ImportForeignSchemaStmt *stmt,
Oid serverOid);
+typedef void (*ExecForeignTruncate_function) (List *frels_list,
+ DropBehavior behavior,
+ bool restart_seqs);
+
typedef Size (*EstimateDSMForeignScan_function) (ForeignScanState *node,
ParallelContext *pcxt);
typedef void (*InitializeDSMForeignScan_function) (ForeignScanState *node,
@@ -236,6 +240,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/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 30b72b6297..f83f88a82f 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"
@@ -295,6 +296,20 @@ struct DropRelationCallbackState
#define ATT_FOREIGN_TABLE 0x0020
#define ATT_PARTITIONED_INDEX 0x0040
+/*
+ * ForeignTruncateInfo
+ *
+ * Information related to truncation of foreign tables. This
+ * is a single entry part of a hash table using the server OID
+ * as key, with a list of all foreign tables involved.
+ */
+typedef struct
+{
+ Oid server_oid;
+ List *frels_list;
+} ForeignTruncateInfo;
+
+
/*
* Partition tables are expected to be dropped when the parent partitioned
* table gets dropped. Hence for partitioning we use AUTO dependency.
@@ -1647,6 +1662,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;
@@ -1792,6 +1808,57 @@ ExecuteTruncateGuts(List *explicit_rels, List *relids, List *relids_logged,
if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
continue;
+ /*
+ * If truncating a foreign table, the foreign data wrapper
+ * callback for TRUNCATE is called once for each server
+ * with a list of all the relations to process linked to this
+ * server. The list of relations for each server is saved as
+ * a single entry of a hash table that uses the server OID as
+ * lookup key. Once the full set of lists is built, all the
+ * entries of the hash table are scanned, and the list of relations
+ * associated to the server is passed down to the TRUNCATE callback
+ * of its foreign data wrapper.
+ */
+ if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+ {
+ Oid frel_oid = RelationGetRelid(rel);
+ Oid server_oid = GetForeignServerIdByRelId(frel_oid);
+ bool found;
+ ForeignTruncateInfo *ft_info;
+
+ /* if the hash table does not exist yet, initialize it */
+ 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);
+ }
+
+ /*
+ * Look after the entry of the server in the hash table,
+ * and initialize it if the entry does not exist yet.
+ */
+ ft_info = hash_search(ft_htab, &server_oid, HASH_ENTER, &found);
+ if (!found)
+ {
+ ft_info->server_oid = server_oid;
+ ft_info->frels_list = NIL;
+
+ }
+
+ /* save the relation in the list */
+ ft_info->frels_list = lappend(ft_info->frels_list, rel);
+ continue;
+ }
+
/*
* Normally, we need a transaction-safe truncation here. However, if
* the table was either created in the current (sub)transaction or has
@@ -1852,6 +1919,29 @@ ExecuteTruncateGuts(List *explicit_rels, List *relids, List *relids_logged,
pgstat_count_truncate(rel);
}
+ /*
+ * Now go through the hash table, and process each entry associated
+ * to the servers involved in the TRUNCATE.
+ */
+ 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 = GetFdwRoutineByServerId(ft_info->server_oid);
+
+ /* truncate_check_rel() has checked that already */
+ Assert(routine->ExecForeignTruncate != NULL);
+ routine->ExecForeignTruncate(ft_info->frels_list,
+ behavior,
+ restart_seqs);
+ }
+ }
+
/*
* Restart owned sequences if we were asked to.
*/
@@ -1939,12 +2029,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,
+ * 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.)
+ * physical truncation will occur in their case.).
*/
- 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)
+ 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/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
diff --git a/doc/src/sgml/fdwhandler.sgml b/doc/src/sgml/fdwhandler.sgml
index 6587678af2..f2416c9074 100644
--- a/doc/src/sgml/fdwhandler.sgml
+++ b/doc/src/sgml/fdwhandler.sgml
@@ -968,6 +968,42 @@ 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>
+ Truncate a set of foreign tables defined by
+ <literal>frels_list</literal> belonging to the same foreign server.
+ This optional function is called during execution of
+ <command>TRUNCATE</command> for each foreign server being involved
+ in one <command>TRUNCATE</command> command (note that invocations
+ are not per foreign table).
+ </para>
+
+ <para>
+ If the <function>ExecForeignTruncate</function> pointer is set to
+ <literal>NULL</literal>, attempts to truncate the foreign table will
+ fail with an error message.
+ </para>
+
+ <para>
+ <literal>behavior</literal> defines how foreign tables should
+ be truncated, using as possible values <literal>DROP_RESTRICT</literal>
+ and <literal>DROP_CASCADE</literal> (to map with the equivalents of
+ <command>TRUNCATE</command>).
+ </para>
+
+ <para>
+ <literal>restart_seqs</literal> is set to <literal>true</literal>
+ if <literal>RESTART IDENTITY</literal> was supplied in the
+ <command>TRUNCATE</command>.
+ </para>
+ </sect2>
+
<sect2 id="fdw-callbacks-row-locking">
<title>FDW Routines for Row Locking</title>
--
2.25.0
0002-Add-support-for-TRUNCATE-in-postgres_fdw.patchtext/x-diff; charset=us-asciiDownload
From 3918ddb188d62accaac7e1dbe5d8459f4fe72b54 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Mon, 20 Jan 2020 22:40:06 +0900
Subject: [PATCH 2/2] Add support for TRUNCATE in postgres_fdw
---
contrib/postgres_fdw/deparse.c | 31 ++++
.../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 +++++++
5 files changed, 279 insertions(+)
diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index a3639e9604..94e8f71d0c 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -2098,6 +2098,37 @@ 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;
+ }
+}
+
/*
* 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
-- ===================================================================
--
2.25.0
On Mon, Jan 20, 2020 at 10:50:21PM +0900, Michael Paquier wrote:
I have spent a good amount of time polishing 0001, tweaking the docs,
comments, error messages and a bit its logic. I am getting
comfortable with it, but it still needs an extra lookup, an indent run
which has some noise and I lacked of time today. 0002 has some of its
issues fixed and I have not reviewed it fully yet. There are still
some places not adapted in it (why do you use "Bug?" in all your
elog() messages by the way?), so the postgres_fdw part needs more
attention. Could you think about some docs for it by the way?
I have more comments about the postgres_fdw that need to be
addressed.
+ 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");
I agree here that an elog() looks more adapted than an assert.
However I would change the error message to be more like "incorrect
server OID supplied by the TRUNCATE callback" or something similar.
The server OID has to be valid anyway, so don't you just bypass any
errors if it is not set?
+-- truncate two tables at a command
What does this sentence mean? Isn't that "truncate two tables in one
single command"?
The table names in the tests are rather hard to parse. I think that
it would be better to avoid underscores at the beginning of the
relation names.
It would be nice to have some coverage with inheritance, and also
track down in the tests what we expect when ONLY is specified in that
case (with and without ONLY, both parent and child relations).
Anyway, attached is the polished version for 0001 that I would be fine
to commit, except for one point: are there objections if we do not
have extra handling for ONLY when it comes to foreign tables with
inheritance? As the patch stands, the list of relations is first
built, with an inheritance recursive lookup done depending on if ONLY
is used or not. Hence, if using "TRUNCATE ONLY foreign_tab, ONLY
foreign_tab2", then only those two tables would be passed down to the
FDW. If ONLY is removed, both tables as well as their children are
added to the lists of relations split by server OID. One problem is
that this could be confusing for some users I guess? For example,
with a 1:1 mapping in the schema of the local and remote servers, a
user asking for TRUNCATE ONLY foreign_tab would pass down to the
remote just the equivalent of "TRUNCATE foreign_tab" using
postgres_fdw, causing the full inheritance tree to be truncated on the
remote side. The concept of ONLY mixed with inherited foreign tables
is rather blurry (point raised by Stephen upthread).
--
Michael
Attachments:
0001-Add-FDW-callback-for-support-of-TRUNCATE.patchtext/x-diff; charset=us-asciiDownload
From dce70bca1242372c4d36c9d84973adffa90e5bdb Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Tue, 21 Jan 2020 15:32:41 +0900
Subject: [PATCH] Add FDW callback for support of TRUNCATE
---
src/include/foreign/fdwapi.h | 7 ++
src/backend/commands/tablecmds.c | 113 ++++++++++++++++++++-
src/test/regress/expected/foreign_data.out | 8 +-
doc/src/sgml/fdwhandler.sgml | 36 +++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 156 insertions(+), 9 deletions(-)
diff --git a/src/include/foreign/fdwapi.h b/src/include/foreign/fdwapi.h
index 95556dfb15..0a9f36735e 100644
--- a/src/include/foreign/fdwapi.h
+++ b/src/include/foreign/fdwapi.h
@@ -151,6 +151,10 @@ typedef bool (*AnalyzeForeignTable_function) (Relation relation,
typedef List *(*ImportForeignSchema_function) (ImportForeignSchemaStmt *stmt,
Oid serverOid);
+typedef void (*ExecForeignTruncate_function) (List *frels_list,
+ DropBehavior behavior,
+ bool restart_seqs);
+
typedef Size (*EstimateDSMForeignScan_function) (ForeignScanState *node,
ParallelContext *pcxt);
typedef void (*InitializeDSMForeignScan_function) (ForeignScanState *node,
@@ -236,6 +240,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/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 30b72b6297..2c575668c2 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"
@@ -295,6 +296,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 that uses the server OID as lookup key,
+ * and includes a per-server list of all foreign tables involved in the
+ * truncation.
+ */
+typedef struct
+{
+ Oid server_oid;
+ List *frels_list;
+} ForeignTruncateInfo;
+
+
/*
* Partition tables are expected to be dropped when the parent partitioned
* table gets dropped. Hence for partitioning we use AUTO dependency.
@@ -1647,6 +1663,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;
@@ -1792,6 +1809,56 @@ ExecuteTruncateGuts(List *explicit_rels, List *relids, List *relids_logged,
if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
continue;
+ /*
+ * If truncating a foreign table, the foreign data wrapper callback
+ * for TRUNCATE is called once for each server with a list of all the
+ * relations to process linked to this server. The list of relations
+ * for each server is saved as a single entry in a hash table that
+ * uses the server OID as lookup key. Once the full set of lists is
+ * built, all the entries of the hash table are scanned, and the list
+ * of relations associated to the server is passed down to the
+ * TRUNCATE callback of its foreign data wrapper.
+ */
+ if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+ {
+ Oid frel_oid = RelationGetRelid(rel);
+ Oid server_oid = GetForeignServerIdByRelId(frel_oid);
+ bool found;
+ ForeignTruncateInfo *ft_info;
+
+ /* if the hash table does not exist yet, initialize it */
+ 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);
+ }
+
+ /*
+ * Look after the entry of the server in the hash table, and
+ * initialize it if the entry does not exist yet.
+ */
+ ft_info = hash_search(ft_htab, &server_oid, HASH_ENTER, &found);
+ if (!found)
+ {
+ ft_info->server_oid = server_oid;
+ ft_info->frels_list = NIL;
+
+ }
+
+ /* save the relation in the list */
+ ft_info->frels_list = lappend(ft_info->frels_list, rel);
+ continue;
+ }
+
/*
* Normally, we need a transaction-safe truncation here. However, if
* the table was either created in the current (sub)transaction or has
@@ -1852,6 +1919,30 @@ ExecuteTruncateGuts(List *explicit_rels, List *relids, List *relids_logged,
pgstat_count_truncate(rel);
}
+ /*
+ * Now go through the hash table, and process each entry associated to the
+ * servers involved in the TRUNCATE.
+ */
+ 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 = GetFdwRoutineByServerId(ft_info->server_oid);
+
+ /* truncate_check_rel() has checked that already */
+ Assert(routine->ExecForeignTruncate != NULL);
+
+ routine->ExecForeignTruncate(ft_info->frels_list,
+ behavior,
+ restart_seqs);
+ }
+ }
+
/*
* Restart owned sequences if we were asked to.
*/
@@ -1939,12 +2030,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 server_id = GetForeignServerIdByRelId(relid);
+ FdwRoutine *fdwroutine = GetFdwRoutineByServerId(server_id);
+
+ 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/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
diff --git a/doc/src/sgml/fdwhandler.sgml b/doc/src/sgml/fdwhandler.sgml
index 6587678af2..f2416c9074 100644
--- a/doc/src/sgml/fdwhandler.sgml
+++ b/doc/src/sgml/fdwhandler.sgml
@@ -968,6 +968,42 @@ 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>
+ Truncate a set of foreign tables defined by
+ <literal>frels_list</literal> belonging to the same foreign server.
+ This optional function is called during execution of
+ <command>TRUNCATE</command> for each foreign server being involved
+ in one <command>TRUNCATE</command> command (note that invocations
+ are not per foreign table).
+ </para>
+
+ <para>
+ If the <function>ExecForeignTruncate</function> pointer is set to
+ <literal>NULL</literal>, attempts to truncate the foreign table will
+ fail with an error message.
+ </para>
+
+ <para>
+ <literal>behavior</literal> defines how foreign tables should
+ be truncated, using as possible values <literal>DROP_RESTRICT</literal>
+ and <literal>DROP_CASCADE</literal> (to map with the equivalents of
+ <command>TRUNCATE</command>).
+ </para>
+
+ <para>
+ <literal>restart_seqs</literal> is set to <literal>true</literal>
+ if <literal>RESTART IDENTITY</literal> was supplied in the
+ <command>TRUNCATE</command>.
+ </para>
+ </sect2>
+
<sect2 id="fdw-callbacks-row-locking">
<title>FDW Routines for Row Locking</title>
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index e216de9570..ef2bd90db1 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -686,6 +686,7 @@ ForeignScanState
ForeignServer
ForeignServerInfo
ForeignTable
+ForeignTruncateInfo
ForkNumber
FormData_pg_aggregate
FormData_pg_am
--
2.25.0
2020年1月21日(火) 15:38 Michael Paquier <michael@paquier.xyz>:
On Mon, Jan 20, 2020 at 10:50:21PM +0900, Michael Paquier wrote:
I have spent a good amount of time polishing 0001, tweaking the docs,
comments, error messages and a bit its logic. I am getting
comfortable with it, but it still needs an extra lookup, an indent run
which has some noise and I lacked of time today. 0002 has some of its
issues fixed and I have not reviewed it fully yet. There are still
some places not adapted in it (why do you use "Bug?" in all your
elog() messages by the way?), so the postgres_fdw part needs more
attention. Could you think about some docs for it by the way?I have more comments about the postgres_fdw that need to be
addressed.+ 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"); I agree here that an elog() looks more adapted than an assert. However I would change the error message to be more like "incorrect server OID supplied by the TRUNCATE callback" or something similar. The server OID has to be valid anyway, so don't you just bypass any errors if it is not set?+-- truncate two tables at a command
What does this sentence mean? Isn't that "truncate two tables in one
single command"?The table names in the tests are rather hard to parse. I think that
it would be better to avoid underscores at the beginning of the
relation names.It would be nice to have some coverage with inheritance, and also
track down in the tests what we expect when ONLY is specified in that
case (with and without ONLY, both parent and child relations).Anyway, attached is the polished version for 0001 that I would be fine
to commit, except for one point: are there objections if we do not
have extra handling for ONLY when it comes to foreign tables with
inheritance? As the patch stands, the list of relations is first
built, with an inheritance recursive lookup done depending on if ONLY
is used or not. Hence, if using "TRUNCATE ONLY foreign_tab, ONLY
foreign_tab2", then only those two tables would be passed down to the
FDW. If ONLY is removed, both tables as well as their children are
added to the lists of relations split by server OID. One problem is
that this could be confusing for some users I guess? For example,
with a 1:1 mapping in the schema of the local and remote servers, a
user asking for TRUNCATE ONLY foreign_tab would pass down to the
remote just the equivalent of "TRUNCATE foreign_tab" using
postgres_fdw, causing the full inheritance tree to be truncated on the
remote side. The concept of ONLY mixed with inherited foreign tables
is rather blurry (point raised by Stephen upthread).
Hmm. Do we need to deliver another list to inform why these relations are
trundated?
If callback is invoked with a foreign-relation that is specified by TRUNCATE
command with ONLY, it seems to me reasonable that remote TRUNCATE
command specifies the relation on behalf of the foreign table with ONLY.
Foreign-tables can be truncated because ...
1. it is specified by user with ONLY-clause.
2. it is specified by user without ONLY-clause.
3. it is inherited child of the relations specified at 2.
4. it depends on the relations picked up at 1-3.
So, if ExecForeignTruncate() has another list to inform the context for each
relation, postgres_fdw can build proper remote query that may specify the
remote tables with ONLY-clause.
Regarding to the other comments, it's all Ok for me. I'll update the patch.
And, I forgot "updatable" option at postgres_fdw. It should be checked on
the truncate also, right?
Best regards,
--
HeteroDB, Inc / The PG-Strom Project
KaiGai Kohei <kaigai@heterodb.com>
On Mon, Jan 27, 2020 at 11:08:36PM +0900, Kohei KaiGai wrote:
Hmm. Do we need to deliver another list to inform why these relations are
trundated?
I got to think more about this one, and being able to control ONLY on
a per-relation basis would be the least surprising approach for the
commands generated. But at least this avoids truncating a full
inheritance tree on a remote cluster even if ONLY is specified
locally. Note that I'd like to assume that most applications have a
1:1 mapping in their schemas between a local and remote cluster, but
that's most likely not always the case ;)
If callback is invoked with a foreign-relation that is specified by TRUNCATE
command with ONLY, it seems to me reasonable that remote TRUNCATE
command specifies the relation on behalf of the foreign table with ONLY.So, if ExecForeignTruncate() has another list to inform the context for each
relation, postgres_fdw can build proper remote query that may specify the
remote tables with ONLY-clause.
Yeah, TRUNCATE can specify ONLY on a per-table basis, so having a
second list makes sense. Then in the FDW, just make sure to
elog(ERROR) if the lengths do no match, and then use forboth() to loop
over them. One thing that you need to be careful about is that tables
which are added to the list because of inheritance should not be
marked with ONLY when generating the command to the remote.
Regarding to the other comments, it's all Ok for me. I'll update the patch.
And, I forgot "updatable" option at postgres_fdw. It should be checked on
the truncate also, right?
Hmm. Good point. Being able to filter that silently through a
configuration parameter is kind of interesting. Now I think that this
should be a separate option because updatable applies to DMLs. Like,
truncatable?
For now, as the patch needs more work for its implementation, docs and
tests, I am marking it as returned with feedback.
--
Michael
Hello,
The attached is revised version.
If callback is invoked with a foreign-relation that is specified by TRUNCATE
command with ONLY, it seems to me reasonable that remote TRUNCATE
command specifies the relation on behalf of the foreign table with ONLY.So, if ExecForeignTruncate() has another list to inform the context for each
relation, postgres_fdw can build proper remote query that may specify the
remote tables with ONLY-clause.Yeah, TRUNCATE can specify ONLY on a per-table basis, so having a
second list makes sense. Then in the FDW, just make sure to
elog(ERROR) if the lengths do no match, and then use forboth() to loop
over them. One thing that you need to be careful about is that tables
which are added to the list because of inheritance should not be
marked with ONLY when generating the command to the remote.
The v5 patch added separated list for the FDW callback, to inform the context
when relations are specified by TRUNCATE command. The frels_extra
argument is a list of integers. 0 means that relevant foreign-table is specified
without "ONLY" clause. and positive means specified with "ONLY" clause.
Negative value means that foreign-tables are not specified in the TRUNCATE
command, but truncated due to dependency (like partition's child leaf).
The remote SQL generates TRUNCATE command according to the above
"extra" information. So, "TRUNCATE ONLY ftable" generate a remote query
with "TRUNCATE ONLY mapped_remote_table".
On the other hand, it can make strange results, although it is a corner case.
The example below shows the result of TRUNCATE ONLY on a foreign-table
that mapps a remote table with an inherited children.
The rows id < 10 belongs to the parent table, thus TRUNCATE ONLY tru_ftable
eliminated the remote parent, however, it looks the tru_ftable still
contains rows
after TRUNCATE command.
I wonder whether it is tangible behavior for users. Of course, "ONLY" clause
controls local hierarchy of partitioned / inherited tables, however, I'm not
certain whether the concept shall be expanded to the structure of remote tables.
+SELECT * FROM tru_ftable;
+ id | x
+----+----------------------------------
+ 5 | e4da3b7fbbce2345d7772b0674a318d5
+ 6 | 1679091c5a880faf6fb5e6087eb1b2dc
+ 7 | 8f14e45fceea167a5a36dedd4bea2543
+ 8 | c9f0f895fb98ab9159f51fd0297e236d
+ 9 | 45c48cce2e2d7fbdea1afc51c7c6ad26
+ 10 | d3d9446802a44259755d38e6d163e820
+ 11 | 6512bd43d9caa6e02c990b0a82652dca
+ 12 | c20ad4d76fe97759aa27a0c99bff6710
+ 13 | c51ce410c124a10e0db5e4b97fc2af39
+ 14 | aab3238922bcc25a6f606eb525ffdc56
+(10 rows)
+
+TRUNCATE ONLY tru_ftable; -- truncate only parent portion
+SELECT * FROM tru_ftable;
+ id | x
+----+----------------------------------
+ 10 | d3d9446802a44259755d38e6d163e820
+ 11 | 6512bd43d9caa6e02c990b0a82652dca
+ 12 | c20ad4d76fe97759aa27a0c99bff6710
+ 13 | c51ce410c124a10e0db5e4b97fc2af39
+ 14 | aab3238922bcc25a6f606eb525ffdc56
+(5 rows)
Regarding to the other comments, it's all Ok for me. I'll update the patch.
And, I forgot "updatable" option at postgres_fdw. It should be checked on
the truncate also, right?Hmm. Good point. Being able to filter that silently through a
configuration parameter is kind of interesting. Now I think that this
should be a separate option because updatable applies to DMLs. Like,
truncatable?
Ok, "truncatable" option was added.
Please check the regression test and documentation updates.
Best regards,
--
HeteroDB, Inc / The PG-Strom Project
KaiGai Kohei <kaigai@heterodb.com>
Attachments:
pgsql13-truncate-on-foreign-table.v5.patchapplication/octet-stream; name=pgsql13-truncate-on-foreign-table.v5.patchDownload
contrib/postgres_fdw/deparse.c | 38 ++++
contrib/postgres_fdw/expected/postgres_fdw.out | 235 ++++++++++++++++++++++++-
contrib/postgres_fdw/option.c | 6 +-
contrib/postgres_fdw/postgres_fdw.c | 85 +++++++++
contrib/postgres_fdw/postgres_fdw.h | 5 +
contrib/postgres_fdw/sql/postgres_fdw.sql | 90 ++++++++++
doc/src/sgml/fdwhandler.sgml | 40 +++++
doc/src/sgml/postgres-fdw.sgml | 30 +++-
src/backend/commands/tablecmds.c | 134 +++++++++++++-
src/backend/replication/logical/worker.c | 10 +-
src/include/commands/tablecmds.h | 8 +-
src/include/foreign/fdwapi.h | 8 +
src/test/regress/expected/foreign_data.out | 8 +-
src/tools/pgindent/typedefs.list | 1 +
14 files changed, 676 insertions(+), 22 deletions(-)
diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index ad37a74221..1dc92e4df0 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -2114,6 +2114,44 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
deparseRelation(buf, rel);
}
+/*
+ * Construct a simple "TRUNCATE rel" statement
+ */
+void
+deparseTruncateSql(StringInfo buf,
+ List *frels_list,
+ List *frels_extra,
+ DropBehavior behavior,
+ bool restart_seqs)
+{
+ ListCell *lc1, *lc2;
+
+ appendStringInfoString(buf, "TRUNCATE ");
+ forboth (lc1, frels_list,
+ lc2, frels_extra)
+ {
+ Relation frel = lfirst(lc1);
+ int extra = lfirst_int(lc2);
+
+ if (lc1 != list_head(frels_list))
+ appendStringInfoString(buf, ", ");
+ if (extra != 0)
+ appendStringInfoString(buf, "ONLY ");
+ 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;
+ }
+}
+
/*
* 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 62c2697920..37004cd3b2 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8199,6 +8199,239 @@ select * from rem3;
drop foreign table rem3;
drop table loc3;
+-- ===================================================================
+-- test for TRUNCATE
+-- ===================================================================
+CREATE TABLE tru_rtable0 (id int primary key, x text);
+CREATE TABLE tru_rtable1 (id int primary key, y text);
+CREATE FOREIGN TABLE tru_ftable (id int, x text)
+ SERVER loopback OPTIONS (table_name 'tru_rtable0');
+INSERT INTO tru_rtable0 (SELECT x,md5(x::text) FROM generate_series(1,10) x);
+CREATE TABLE tru_ptable (id int, y text) 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,md5(x::text) FROM generate_series(11,20) x);
+CREATE TABLE tru_pk_table(id int primary key, z text);
+CREATE TABLE tru_fk_table(fkey int references tru_pk_table(id));
+INSERT INTO tru_pk_table (SELECT x,md5((x+1)::text) 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, z text)
+ SERVER loopback OPTIONS (table_name 'tru_pk_table');
+CREATE TABLE tru_rtable_parent (id int, a text);
+CREATE TABLE tru_rtable_child (id int, a text);
+CREATE FOREIGN TABLE tru_ftable_parent (id int, a text)
+ 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, md5(x::text) FROM generate_series(1,8) x);
+INSERT INTO tru_rtable_child (SELECT x, md5(x::text) FROM generate_series(10, 18) x);
+-- normal truncate
+SELECT * FROM tru_ftable;
+ 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_ftable;
+SELECT * FROM tru_rtable0; -- empty
+ id | x
+----+---
+(0 rows)
+
+SELECT * FROM tru_ftable; -- empty
+ id | x
+----+---
+(0 rows)
+
+-- 'truncatable' option
+ALTER SERVER loopback OPTIONS (ADD truncatable 'false');
+TRUNCATE tru_ftable; -- error
+ERROR: truncate on "tru_ftable" is prohibited
+ALTER SERVER loopback OPTIONS (DROP truncatable);
+ALTER FOREIGN TABLE tru_ftable OPTIONS (ADD truncatable 'false');
+TRUNCATE tru_ftable; -- error
+ERROR: truncate on "tru_ftable" is prohibited
+ALTER FOREIGN TABLE tru_ftable OPTIONS (SET truncatable 'true');
+TRUNCATE tru_ftable; -- accepted
+-- partition table mixtured by table and foreign table
+SELECT * FROM tru_ptable;
+ id | y
+----+----------------------------------
+ 12 | c20ad4d76fe97759aa27a0c99bff6710
+ 13 | c51ce410c124a10e0db5e4b97fc2af39
+ 14 | aab3238922bcc25a6f606eb525ffdc56
+ 16 | c74d97b01eae257e44aa9d5bade97baf
+ 17 | 70efdf2ec9b086079795c442636b55fb
+ 18 | 6f4922f45568161a8cdf4ad2299f6d23
+ 11 | 6512bd43d9caa6e02c990b0a82652dca
+ 15 | 9bf31c7ff062936a96d3c8bd1f8f2ff3
+ 19 | 1f0e3dad99908345f7439f8ffabdffc4
+ 20 | 98f13708210194c475687be6106a3b84
+(10 rows)
+
+TRUNCATE tru_ptable;
+SELECT * FROM tru_ptable; -- empty
+ id | y
+----+---
+(0 rows)
+
+SELECT * FROM tru_ptable__p0; -- empty
+ id | y
+----+---
+(0 rows)
+
+SELECT * FROM tru_ftable__p1; -- empty
+ id | y
+----+---
+(0 rows)
+
+SELECT * FROM tru_rtable1; -- empty
+ id | y
+----+---
+(0 rows)
+
+-- 'CASCADE' option
+SELECT * FROM tru_pk_ftable;
+ id | z
+----+----------------------------------
+ 1 | c81e728d9d4c2f636f067f89cc14862c
+ 2 | eccbc87e4b5ce2fe28308fd9f2a7baf3
+ 3 | a87ff679a2f3e71d9181a67b7542122c
+ 4 | e4da3b7fbbce2345d7772b0674a318d5
+ 5 | 1679091c5a880faf6fb5e6087eb1b2dc
+ 6 | 8f14e45fceea167a5a36dedd4bea2543
+ 7 | c9f0f895fb98ab9159f51fd0297e236d
+ 8 | 45c48cce2e2d7fbdea1afc51c7c6ad26
+ 9 | d3d9446802a44259755d38e6d163e820
+ 10 | 6512bd43d9caa6e02c990b0a82652dca
+(10 rows)
+
+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 * FROM tru_pk_ftable;
+ id | z
+----+---
+(0 rows)
+
+SELECT * FROM tru_fk_table; -- also truncated
+ fkey
+------
+(0 rows)
+
+-- truncate two tables at a command
+INSERT INTO tru_ftable (SELECT x,md5((x+2)::text) FROM generate_series(1,8) x);
+INSERT INTO tru_pk_ftable (SELECT x,md5((x+3)::text) FROM generate_series(3,10) x);
+SELECT * FROM tru_ftable a FULL OUTER JOIN tru_pk_ftable b ON a.id = b.id;
+ id | x | id | z
+----+----------------------------------+----+----------------------------------
+ 1 | eccbc87e4b5ce2fe28308fd9f2a7baf3 | |
+ 2 | a87ff679a2f3e71d9181a67b7542122c | |
+ 3 | e4da3b7fbbce2345d7772b0674a318d5 | 3 | 1679091c5a880faf6fb5e6087eb1b2dc
+ 4 | 1679091c5a880faf6fb5e6087eb1b2dc | 4 | 8f14e45fceea167a5a36dedd4bea2543
+ 5 | 8f14e45fceea167a5a36dedd4bea2543 | 5 | c9f0f895fb98ab9159f51fd0297e236d
+ 6 | c9f0f895fb98ab9159f51fd0297e236d | 6 | 45c48cce2e2d7fbdea1afc51c7c6ad26
+ 7 | 45c48cce2e2d7fbdea1afc51c7c6ad26 | 7 | d3d9446802a44259755d38e6d163e820
+ 8 | d3d9446802a44259755d38e6d163e820 | 8 | 6512bd43d9caa6e02c990b0a82652dca
+ | | 9 | c20ad4d76fe97759aa27a0c99bff6710
+ | | 10 | c51ce410c124a10e0db5e4b97fc2af39
+(10 rows)
+
+TRUNCATE tru_ftable, tru_pk_ftable CASCADE;
+SELECT * FROM tru_ftable a FULL OUTER JOIN tru_pk_ftable b ON a.id = b.id;
+ id | x | id | z
+----+---+----+---
+(0 rows)
+
+-- truncate with ONLY clause
+TRUNCATE ONLY tru_ftable_parent;
+SELECT * FROM tru_ftable_parent;
+ id | a
+----+----------------------------------
+ 10 | d3d9446802a44259755d38e6d163e820
+ 11 | 6512bd43d9caa6e02c990b0a82652dca
+ 12 | c20ad4d76fe97759aa27a0c99bff6710
+ 13 | c51ce410c124a10e0db5e4b97fc2af39
+ 14 | aab3238922bcc25a6f606eb525ffdc56
+ 15 | 9bf31c7ff062936a96d3c8bd1f8f2ff3
+ 16 | c74d97b01eae257e44aa9d5bade97baf
+ 17 | 70efdf2ec9b086079795c442636b55fb
+ 18 | 6f4922f45568161a8cdf4ad2299f6d23
+(9 rows)
+
+TRUNCATE tru_ftable_parent;
+SELECT * FROM tru_ftable_parent;
+ id | a
+----+---
+(0 rows)
+
+-- in case when remote table has inherited children
+CREATE TABLE tru_rtable0_child () INHERITS (tru_rtable0);
+INSERT INTO tru_rtable0 (SELECT x,md5(x::text) FROM generate_series(5,9) x);
+INSERT INTO tru_rtable0_child (SELECT x,md5(x::text) FROM generate_series(10,14) x);
+SELECT * FROM tru_ftable;
+ id | x
+----+----------------------------------
+ 5 | e4da3b7fbbce2345d7772b0674a318d5
+ 6 | 1679091c5a880faf6fb5e6087eb1b2dc
+ 7 | 8f14e45fceea167a5a36dedd4bea2543
+ 8 | c9f0f895fb98ab9159f51fd0297e236d
+ 9 | 45c48cce2e2d7fbdea1afc51c7c6ad26
+ 10 | d3d9446802a44259755d38e6d163e820
+ 11 | 6512bd43d9caa6e02c990b0a82652dca
+ 12 | c20ad4d76fe97759aa27a0c99bff6710
+ 13 | c51ce410c124a10e0db5e4b97fc2af39
+ 14 | aab3238922bcc25a6f606eb525ffdc56
+(10 rows)
+
+TRUNCATE ONLY tru_ftable; -- truncate only parent portion
+SELECT * FROM tru_ftable;
+ id | x
+----+----------------------------------
+ 10 | d3d9446802a44259755d38e6d163e820
+ 11 | 6512bd43d9caa6e02c990b0a82652dca
+ 12 | c20ad4d76fe97759aa27a0c99bff6710
+ 13 | c51ce410c124a10e0db5e4b97fc2af39
+ 14 | aab3238922bcc25a6f606eb525ffdc56
+(5 rows)
+
+INSERT INTO tru_rtable0 (SELECT x,md5(x::text) FROM generate_series(21,25) x);
+SELECT * FROM tru_ftable;
+ id | x
+----+----------------------------------
+ 21 | 3c59dc048e8850243be8079a5c74d079
+ 22 | b6d767d2f8ed5d21a44b0e5886680cb9
+ 23 | 37693cfc748049e45d87b8c7d8b9aacd
+ 24 | 1ff1de774005f8da13f42943881c655f
+ 25 | 8e296a067a37563370ded05f5a3bf3ec
+ 10 | d3d9446802a44259755d38e6d163e820
+ 11 | 6512bd43d9caa6e02c990b0a82652dca
+ 12 | c20ad4d76fe97759aa27a0c99bff6710
+ 13 | c51ce410c124a10e0db5e4b97fc2af39
+ 14 | aab3238922bcc25a6f606eb525ffdc56
+(10 rows)
+
+TRUNCATE tru_ftable; -- truncate both of parent and child
+SELECT * FROM tru_ftable;
+ id | x
+----+---
+(0 rows)
+
-- ===================================================================
-- test IMPORT FOREIGN SCHEMA
-- ===================================================================
@@ -8898,7 +9131,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, requirepeer, sslminprotocolversion, sslmaxprotocolversion, gssencmode, krbsrvname, gsslib, target_session_attrs, use_remote_estimate, fdw_startup_cost, fdw_tuple_cost, extensions, updatable, fetch_size
+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, requirepeer, sslminprotocolversion, sslmaxprotocolversion, gssencmode, krbsrvname, gsslib, target_session_attrs, use_remote_estimate, fdw_startup_cost, fdw_tuple_cost, extensions, updatable, truncatable, fetch_size
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 c442af5bb9..843c3e0e7f 100644
--- a/contrib/postgres_fdw/option.c
+++ b/contrib/postgres_fdw/option.c
@@ -107,7 +107,8 @@ postgres_fdw_validator(PG_FUNCTION_ARGS)
* Validate option value, when we can do so without any context.
*/
if (strcmp(def->defname, "use_remote_estimate") == 0 ||
- strcmp(def->defname, "updatable") == 0)
+ strcmp(def->defname, "updatable") == 0 ||
+ strcmp(def->defname, "truncatable") == 0)
{
/* these accept only boolean values */
(void) defGetBoolean(def);
@@ -200,6 +201,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 2175dff824..4d25093f33 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -373,6 +373,10 @@ static void postgresExplainForeignModify(ModifyTableState *mtstate,
ExplainState *es);
static void postgresExplainDirectModify(ForeignScanState *node,
ExplainState *es);
+static void postgresExecForeignTruncate(List *frels_list,
+ List *frels_extra,
+ DropBehavior behavior,
+ bool restart_seqs);
static bool postgresAnalyzeForeignTable(Relation relation,
AcquireSampleRowsFunc *func,
BlockNumber *totalpages);
@@ -546,6 +550,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 +2663,84 @@ 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,
+ List *frels_extra,
+ DropBehavior behavior,
+ bool restart_seqs)
+{
+ Oid server_id = InvalidOid;
+ ForeignServer *serv = NULL;
+ UserMapping *user = NULL;
+ PGconn *conn = NULL;
+ PGresult *res;
+ StringInfoData sql;
+ ListCell *lc;
+ bool server_truncatable = true;
+
+ /* pick up remote connection, and sanity checks */
+ foreach (lc, frels_list)
+ {
+ Relation frel = lfirst(lc);
+ Oid frel_oid = RelationGetRelid(frel);
+ ForeignTable *ft = GetForeignTable(frel_oid);
+ ListCell *cell;
+ bool truncatable;
+
+ if (!OidIsValid(server_id))
+ {
+ server_id = GetForeignServerIdByRelId(frel_oid);
+ serv = GetForeignServer(server_id);
+ user = GetUserMapping(GetUserId(), server_id);
+ conn = GetConnection(user, false);
+
+ foreach (cell, serv->options)
+ {
+ DefElem *defel = (DefElem *) lfirst(cell);
+
+ if (strcmp(defel->defname, "truncatable") == 0)
+ server_truncatable = defGetBoolean(defel);
+ }
+ }
+ else
+ {
+ /* ExecForeignTruncate() is invoked for each server */
+ Assert(server_id == GetForeignServerIdByRelId(frel_oid));
+ }
+
+ /* ensure the target foreign table is truncatable */
+ truncatable = server_truncatable;
+ foreach (cell, ft->options)
+ {
+ DefElem *defel = (DefElem *) lfirst(cell);
+
+ if (strcmp(defel->defname, "truncatable") == 0)
+ truncatable = defGetBoolean(defel);
+ }
+ if (!truncatable)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("truncate on \"%s\" is prohibited",
+ RelationGetRelationName(frel))));
+ }
+ /* set up remote query */
+ initStringInfo(&sql);
+ deparseTruncateSql(&sql, frels_list, frels_extra, 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..8d608c18db 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -190,6 +190,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 *frels_list,
+ List *frels_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 83971665e3..54bcab555f 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -2307,6 +2307,96 @@ select * from rem3;
drop foreign table rem3;
drop table loc3;
+-- ===================================================================
+-- test for TRUNCATE
+-- ===================================================================
+CREATE TABLE tru_rtable0 (id int primary key, x text);
+CREATE TABLE tru_rtable1 (id int primary key, y text);
+CREATE FOREIGN TABLE tru_ftable (id int, x text)
+ SERVER loopback OPTIONS (table_name 'tru_rtable0');
+INSERT INTO tru_rtable0 (SELECT x,md5(x::text) FROM generate_series(1,10) x);
+
+CREATE TABLE tru_ptable (id int, y text) 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,md5(x::text) FROM generate_series(11,20) x);
+
+CREATE TABLE tru_pk_table(id int primary key, z text);
+CREATE TABLE tru_fk_table(fkey int references tru_pk_table(id));
+INSERT INTO tru_pk_table (SELECT x,md5((x+1)::text) 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, z text)
+ SERVER loopback OPTIONS (table_name 'tru_pk_table');
+
+CREATE TABLE tru_rtable_parent (id int, a text);
+CREATE TABLE tru_rtable_child (id int, a text);
+CREATE FOREIGN TABLE tru_ftable_parent (id int, a text)
+ 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, md5(x::text) FROM generate_series(1,8) x);
+INSERT INTO tru_rtable_child (SELECT x, md5(x::text) FROM generate_series(10, 18) x);
+
+-- normal truncate
+SELECT * FROM tru_ftable;
+TRUNCATE tru_ftable;
+SELECT * FROM tru_rtable0; -- empty
+SELECT * FROM tru_ftable; -- empty
+
+-- 'truncatable' option
+ALTER SERVER loopback OPTIONS (ADD truncatable 'false');
+TRUNCATE tru_ftable; -- error
+ALTER SERVER loopback OPTIONS (DROP truncatable);
+ALTER FOREIGN TABLE tru_ftable OPTIONS (ADD truncatable 'false');
+TRUNCATE tru_ftable; -- error
+ALTER FOREIGN TABLE tru_ftable OPTIONS (SET truncatable 'true');
+TRUNCATE tru_ftable; -- accepted
+
+-- partition table mixtured by table and foreign table
+SELECT * FROM tru_ptable;
+TRUNCATE tru_ptable;
+SELECT * FROM tru_ptable; -- empty
+SELECT * FROM tru_ptable__p0; -- empty
+SELECT * FROM tru_ftable__p1; -- empty
+SELECT * FROM tru_rtable1; -- empty
+
+-- 'CASCADE' option
+SELECT * FROM tru_pk_ftable;
+TRUNCATE tru_pk_ftable; -- failed by FK reference
+TRUNCATE tru_pk_ftable CASCADE;
+SELECT * FROM tru_pk_ftable;
+SELECT * FROM tru_fk_table; -- also truncated
+
+-- truncate two tables at a command
+INSERT INTO tru_ftable (SELECT x,md5((x+2)::text) FROM generate_series(1,8) x);
+INSERT INTO tru_pk_ftable (SELECT x,md5((x+3)::text) FROM generate_series(3,10) x);
+SELECT * FROM tru_ftable a FULL OUTER JOIN tru_pk_ftable b ON a.id = b.id;
+TRUNCATE tru_ftable, tru_pk_ftable CASCADE;
+SELECT * FROM tru_ftable a FULL OUTER JOIN tru_pk_ftable b ON a.id = b.id;
+
+-- truncate with ONLY clause
+TRUNCATE ONLY tru_ftable_parent;
+SELECT * FROM tru_ftable_parent;
+TRUNCATE tru_ftable_parent;
+SELECT * FROM tru_ftable_parent;
+
+-- in case when remote table has inherited children
+CREATE TABLE tru_rtable0_child () INHERITS (tru_rtable0);
+INSERT INTO tru_rtable0 (SELECT x,md5(x::text) FROM generate_series(5,9) x);
+INSERT INTO tru_rtable0_child (SELECT x,md5(x::text) FROM generate_series(10,14) x);
+SELECT * FROM tru_ftable;
+
+TRUNCATE ONLY tru_ftable; -- truncate only parent portion
+SELECT * FROM tru_ftable;
+
+INSERT INTO tru_rtable0 (SELECT x,md5(x::text) FROM generate_series(21,25) x);
+SELECT * FROM tru_ftable;
+TRUNCATE tru_ftable; -- truncate both of parent and child
+SELECT * FROM tru_ftable;
+
-- ===================================================================
-- test IMPORT FOREIGN SCHEMA
-- ===================================================================
diff --git a/doc/src/sgml/fdwhandler.sgml b/doc/src/sgml/fdwhandler.sgml
index 6587678af2..92c058277a 100644
--- a/doc/src/sgml/fdwhandler.sgml
+++ b/doc/src/sgml/fdwhandler.sgml
@@ -968,6 +968,46 @@ EndDirectModify(ForeignScanState *node);
</sect2>
+ <sect2 id="fdw-callbacks-truncate">
+ <title>FDW Routines for Truncate</title>
+<programlisting>
+void
+ExecForeignTruncate(List *frels_list, List *frels_extra,
+ DropBehavior behavior, bool restart_seqs);
+</programlisting>
+ <para>
+ Truncate a set of foreign tables defined by
+ <literal>frels_list</literal> belonging to the same foreign server.
+ This optional function is called during execution of
+ <command>TRUNCATE</command> for each foreign server being involved
+ in one <command>TRUNCATE</command> command (note that invocations
+ are not per foreign table).
+
+ <literal>frels_extra</literal> is same length with
+ <literal>frels_list</literal>, that delivers extra information of
+ the context where the foreign-tables are truncated.
+ </para>
+
+ <para>
+ If the <function>ExecForeignTruncate</function> pointer is set to
+ <literal>NULL</literal>, attempts to truncate the foreign table will
+ fail with an error message.
+ </para>
+
+ <para>
+ <literal>behavior</literal> defines how foreign tables should
+ be truncated, using as possible values <literal>DROP_RESTRICT</literal>
+ and <literal>DROP_CASCADE</literal> (to map with the equivalents of
+ <command>TRUNCATE</command>).
+ </para>
+
+ <para>
+ <literal>restart_seqs</literal> is set to <literal>true</literal>
+ if <literal>RESTART IDENTITY</literal> was supplied in the
+ <command>TRUNCATE</command>.
+ </para>
+ </sect2>
+
<sect2 id="fdw-callbacks-row-locking">
<title>FDW Routines for Row Locking</title>
diff --git a/doc/src/sgml/postgres-fdw.sgml b/doc/src/sgml/postgres-fdw.sgml
index 94992be427..887045cbdd 100644
--- a/doc/src/sgml/postgres-fdw.sgml
+++ b/doc/src/sgml/postgres-fdw.sgml
@@ -63,9 +63,10 @@
<para>
Now you need only <command>SELECT</command> from a foreign table to access
the data stored in its underlying remote table. You can also modify
- the remote table using <command>INSERT</command>, <command>UPDATE</command>, or
- <command>DELETE</command>. (Of course, the remote user you have specified
- in your user mapping must have privileges to do these things.)
+ the remote table using <command>INSERT</command>, <command>UPDATE</command>,
+ <command>DELETE</command>, or <command>TRUNCATE</command>.
+ (Of course, the remote user you have specified in your user mapping must
+ have privileges to do these things.)
</para>
<para>
@@ -392,6 +393,29 @@
</listitem>
</varlistentry>
+ <varlistentry>
+ <term><literal>truncatable</literal></term>
+ <listitem>
+ <para>
+ This option controls whether <filename>postgres_fdw</filename> allows
+ foreign tables to be truncated using <command>TRUNCATE</command>
+ 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 <literal>true</literal>.
+ </para>
+ <para>
+ Pay attention for the case when a foreign table maps remote table
+ that has inherited children or partition leafs.
+ <command>TRUNCATE</command> specifies the foreign tables with
+ <literal>ONLY</literal> clause, remove queries over the
+ <filename>postgres_fdw</filename> also specify remote tables with
+ <literal>ONLY</literal> clause, that will truncate only parent
+ portion of the remote table. In the results, it looks like
+ <command>TRUNCATE</command> command partially eliminated contents
+ of the foreign tables.
+ </para>
+ </listitem>
+ </varlistentry>
</variablelist>
</sect3>
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 02a7c04fdb..9762577524 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"
@@ -295,6 +296,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 that uses the server OID as lookup key,
+ * and includes a per-server list of all foreign tables involved in the
+ * truncation.
+ */
+typedef struct
+{
+ Oid server_oid;
+ List *frels_list;
+ List *frels_extra;
+} ForeignTruncateInfo;
+
/*
* Partition tables are expected to be dropped when the parent partitioned
* table gets dropped. Hence for partitioning we use AUTO dependency.
@@ -1544,6 +1560,7 @@ ExecuteTruncate(TruncateStmt *stmt)
{
List *rels = NIL;
List *relids = NIL;
+ List *relids_extra = NIL;
List *relids_logged = NIL;
ListCell *cell;
@@ -1580,6 +1597,7 @@ ExecuteTruncate(TruncateStmt *stmt)
rels = lappend(rels, rel);
relids = lappend_oid(relids, myrelid);
+ relids_extra = lappend_int(relids_extra, (recurse ? 0 : 1));
/* Log this relation only if needed for logical decoding */
if (RelationIsLogicallyLogged(rel))
relids_logged = lappend_oid(relids_logged, myrelid);
@@ -1627,6 +1645,7 @@ ExecuteTruncate(TruncateStmt *stmt)
rels = lappend(rels, rel);
relids = lappend_oid(relids, childrelid);
+ relids_extra = lappend_int(relids_extra, -1);
/* Log this relation only if needed for logical decoding */
if (RelationIsLogicallyLogged(rel))
relids_logged = lappend_oid(relids_logged, childrelid);
@@ -1639,7 +1658,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 */
@@ -1665,16 +1684,21 @@ 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;
/*
@@ -1712,6 +1736,7 @@ 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, -1);
/* Log this relation only if needed for logical decoding */
if (RelationIsLogicallyLogged(rel))
relids_logged = lappend_oid(relids_logged, relid);
@@ -1808,14 +1833,68 @@ 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;
+ /*
+ * If truncating a foreign table, the foreign data wrapper callback
+ * for TRUNCATE is called once for each server with a list of all the
+ * relations to process linked to this server. The list of relations
+ * for each server is saved as a single entry in a hash table that
+ * uses the server OID as lookup key. Once the full set of lists is
+ * built, all the entries of the hash table are scanned, and the list
+ * of relations associated to the server is passed down to the
+ * TRUNCATE callback of its foreign data wrapper.
+ */
+ if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+ {
+ Oid frel_oid = RelationGetRelid(rel);
+ Oid server_oid = GetForeignServerIdByRelId(frel_oid);
+ bool found;
+ ForeignTruncateInfo *ft_info;
+
+ /* if the hash table does not exist yet, initialize it */
+ 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);
+ }
+
+ /*
+ * Look after the entry of the server in the hash table, and
+ * initialize it if the entry does not exist yet.
+ */
+ ft_info = hash_search(ft_htab, &server_oid, HASH_ENTER, &found);
+ if (!found)
+ {
+ ft_info->server_oid = server_oid;
+ ft_info->frels_list = NIL;
+ ft_info->frels_extra = NIL;
+ }
+
+ /* save the relation in the list */
+ ft_info->frels_list = lappend(ft_info->frels_list, rel);
+ ft_info->frels_extra = lappend_int(ft_info->frels_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
@@ -1876,6 +1955,31 @@ ExecuteTruncateGuts(List *explicit_rels, List *relids, List *relids_logged,
pgstat_count_truncate(rel);
}
+ /*
+ * Now go through the hash table, and process each entry associated to the
+ * servers involved in the TRUNCATE.
+ */
+ 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 = GetFdwRoutineByServerId(ft_info->server_oid);
+
+ /* truncate_check_rel() has checked that already */
+ Assert(routine->ExecForeignTruncate != NULL);
+
+ routine->ExecForeignTruncate(ft_info->frels_list,
+ ft_info->frels_extra,
+ behavior,
+ restart_seqs);
+ }
+ }
+
/*
* Restart owned sequences if we were asked to.
*/
@@ -1962,12 +2066,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 server_id = GetForeignServerIdByRelId(relid);
+ FdwRoutine *fdwroutine = GetFdwRoutineByServerId(server_id);
+
+ 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 ad4a732fd2..a27930c7c6 100644
--- a/src/backend/replication/logical/worker.c
+++ b/src/backend/replication/logical/worker.c
@@ -929,6 +929,7 @@ apply_handle_truncate(StringInfo s)
List *remote_rels = NIL;
List *rels = NIL;
List *relids = NIL;
+ List *relids_extra = NIL;
List *relids_logged = NIL;
ListCell *lc;
@@ -955,6 +956,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, 0);
if (RelationIsLogicallyLogged(rel->localrel))
relids_logged = lappend_oid(relids_logged, rel->localreloid);
}
@@ -964,8 +966,12 @@ apply_handle_truncate(StringInfo s)
* 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 c1581ad178..7c3ebe4353 100644
--- a/src/include/commands/tablecmds.h
+++ b/src/include/commands/tablecmds.h
@@ -56,8 +56,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 95556dfb15..674ed449bb 100644
--- a/src/include/foreign/fdwapi.h
+++ b/src/include/foreign/fdwapi.h
@@ -151,6 +151,11 @@ typedef bool (*AnalyzeForeignTable_function) (Relation relation,
typedef List *(*ImportForeignSchema_function) (ImportForeignSchemaStmt *stmt,
Oid serverOid);
+typedef void (*ExecForeignTruncate_function) (List *frels_list,
+ List *frels_extra,
+ DropBehavior behavior,
+ bool restart_seqs);
+
typedef Size (*EstimateDSMForeignScan_function) (ForeignScanState *node,
ParallelContext *pcxt);
typedef void (*InitializeDSMForeignScan_function) (ForeignScanState *node,
@@ -236,6 +241,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/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
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index e216de9570..ef2bd90db1 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -686,6 +686,7 @@ ForeignScanState
ForeignServer
ForeignServerInfo
ForeignTable
+ForeignTruncateInfo
ForkNumber
FormData_pg_aggregate
FormData_pg_am
On 1 Mar 2020, at 03:24, Kohei KaiGai <kaigai@heterodb.com> wrote:
The attached is revised version.
This version fails to apply to HEAD due to conflicts in postgres_fdw expected
test output. Can you please submit a rebased version. Marking the entry
Waiting on Author in the meantime.
cheers ./daniel
On 2 Jul 2020, at 16:40, Daniel Gustafsson <daniel@yesql.se> wrote:
On 1 Mar 2020, at 03:24, Kohei KaiGai <kaigai@heterodb.com> wrote:
The attached is revised version.
This version fails to apply to HEAD due to conflicts in postgres_fdw expected
test output. Can you please submit a rebased version. Marking the entry
Waiting on Author in the meantime.
As this has stalled, I've marked this patch Returned with Feedback. Feel free
to open a new entry if you return to this patch.
cheers ./daniel