contrib/postgres_fdw/deparse.c | 9 ++ contrib/postgres_fdw/expected/postgres_fdw.out | 144 +++++++++++++++++++++++++ contrib/postgres_fdw/postgres_fdw.c | 57 ++++++++++ contrib/postgres_fdw/postgres_fdw.h | 1 + contrib/postgres_fdw/sql/postgres_fdw.sql | 51 +++++++++ doc/src/sgml/fdwhandler.sgml | 37 +++++++ src/backend/commands/tablecmds.c | 60 ++++++++++- src/include/foreign/fdwapi.h | 7 ++ 8 files changed, 362 insertions(+), 4 deletions(-) diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c index a3639e9604..aaa38cb482 100644 --- a/contrib/postgres_fdw/deparse.c +++ b/contrib/postgres_fdw/deparse.c @@ -2098,6 +2098,15 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs) deparseRelation(buf, rel); } +/* + * Construct a simple "TRUNCATE rel" statement + */ +void +deparseTruncateSql(StringInfo buf, Relation rel) +{ + deparseRelation(buf, rel); +} + /* * Construct name to use for given column, and emit it into buf. * If it has a column_name FDW option, use that instead of attribute name. diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out index 0912d6cd5e..0421f172f5 100644 --- a/contrib/postgres_fdw/expected/postgres_fdw.out +++ b/contrib/postgres_fdw/expected/postgres_fdw.out @@ -8168,6 +8168,150 @@ select * from rem3; drop foreign table rem3; drop table loc3; +-- =================================================================== +-- test for TRUNCATE +-- =================================================================== +CREATE TABLE tru_nt (id int, x text); +CREATE FOREIGN TABLE tru_ft (id int, x text) + SERVER loopback OPTIONS (table_name 'tru_nt'); +INSERT INTO tru_nt (SELECT x,md5(x::text) FROM generate_series(1,10) x); +CREATE TABLE tru_pt (id int, y text) PARTITION BY HASH(id); +CREATE TABLE tru_pt__p0 PARTITION OF tru_pt + FOR VALUES WITH (MODULUS 2, REMAINDER 0); +CREATE TABLE __tru_pt__p1 (id int, y text); +CREATE FOREIGN TABLE tru_pt__p1 PARTITION OF tru_pt + FOR VALUES WITH (MODULUS 2, REMAINDER 1) + SERVER loopback OPTIONS (table_name '__tru_pt__p1'); +INSERT INTO tru_pt (SELECT x,md5(x::text) FROM generate_series(11,30) x); +CREATE TABLE tru_pk (id int primary key, x text); +INSERT INTO tru_pk (SELECT x,md5((x+1)::text) FROM generate_series(1,10) x); +CREATE TABLE tru_fk (fkey int references tru_pk(id)); +INSERT INTO tru_fk (SELECT x % 10 + 1 FROM generate_series(1,30) x); +CREATE FOREIGN TABLE tru_ft_ref (id int, x text) + SERVER loopback OPTIONS (table_name 'tru_pk'); +-- normal truncate +SELECT * FROM tru_ft; + id | x +----+---------------------------------- + 1 | c4ca4238a0b923820dcc509a6f75849b + 2 | c81e728d9d4c2f636f067f89cc14862c + 3 | eccbc87e4b5ce2fe28308fd9f2a7baf3 + 4 | a87ff679a2f3e71d9181a67b7542122c + 5 | e4da3b7fbbce2345d7772b0674a318d5 + 6 | 1679091c5a880faf6fb5e6087eb1b2dc + 7 | 8f14e45fceea167a5a36dedd4bea2543 + 8 | c9f0f895fb98ab9159f51fd0297e236d + 9 | 45c48cce2e2d7fbdea1afc51c7c6ad26 + 10 | d3d9446802a44259755d38e6d163e820 +(10 rows) + +TRUNCATE tru_ft; +SELECT * FROM tru_nt; + id | x +----+--- +(0 rows) + +SELECT * FROM tru_ft; + id | x +----+--- +(0 rows) + +-- partition table mixtured by table and foreign table +SELECT * FROM tru_pt; + id | y +----+---------------------------------- + 12 | c20ad4d76fe97759aa27a0c99bff6710 + 13 | c51ce410c124a10e0db5e4b97fc2af39 + 14 | aab3238922bcc25a6f606eb525ffdc56 + 16 | c74d97b01eae257e44aa9d5bade97baf + 17 | 70efdf2ec9b086079795c442636b55fb + 18 | 6f4922f45568161a8cdf4ad2299f6d23 + 23 | 37693cfc748049e45d87b8c7d8b9aacd + 25 | 8e296a067a37563370ded05f5a3bf3ec + 26 | 4e732ced3463d06de0ca9a15b6153677 + 27 | 02e74f10e0327ad868d138f2b4fdd6f0 + 28 | 33e75ff09dd601bbe69f351039152189 + 30 | 34173cb38f07f89ddbebc2ac9128303f + 11 | 6512bd43d9caa6e02c990b0a82652dca + 15 | 9bf31c7ff062936a96d3c8bd1f8f2ff3 + 19 | 1f0e3dad99908345f7439f8ffabdffc4 + 20 | 98f13708210194c475687be6106a3b84 + 21 | 3c59dc048e8850243be8079a5c74d079 + 22 | b6d767d2f8ed5d21a44b0e5886680cb9 + 24 | 1ff1de774005f8da13f42943881c655f + 29 | 6ea9ab1baa0efb9e19094440c317e21b +(20 rows) + +TRUNCATE tru_pt; +SELECT * FROM tru_pt; + id | y +----+--- +(0 rows) + +SELECT * FROM tru_pt__p0; + id | y +----+--- +(0 rows) + +SELECT * FROM tru_pt__p1; + id | y +----+--- +(0 rows) + +SELECT * FROM __tru_pt__p1; + id | y +----+--- +(0 rows) + +-- 'CASCADE' option +SELECT * FROM tru_ft_ref; + id | x +----+---------------------------------- + 1 | c81e728d9d4c2f636f067f89cc14862c + 2 | eccbc87e4b5ce2fe28308fd9f2a7baf3 + 3 | a87ff679a2f3e71d9181a67b7542122c + 4 | e4da3b7fbbce2345d7772b0674a318d5 + 5 | 1679091c5a880faf6fb5e6087eb1b2dc + 6 | 8f14e45fceea167a5a36dedd4bea2543 + 7 | c9f0f895fb98ab9159f51fd0297e236d + 8 | 45c48cce2e2d7fbdea1afc51c7c6ad26 + 9 | d3d9446802a44259755d38e6d163e820 + 10 | 6512bd43d9caa6e02c990b0a82652dca +(10 rows) + +TRUNCATE tru_ft_ref; -- failed +ERROR: cannot truncate a table referenced in a foreign key constraint +DETAIL: Table "tru_fk" references "tru_pk". +HINT: Truncate table "tru_fk" at the same time, or use TRUNCATE ... CASCADE. +CONTEXT: remote SQL command: TRUNCATE public.tru_pk CONTINUE IDENTITY RESTRICT +TRUNCATE tru_ft_ref CASCADE; +SELECT * FROM tru_ft_ref; + id | x +----+--- +(0 rows) + +-- truncate two tables at a command +INSERT INTO tru_ft (SELECT x,md5((x+2)::text) FROM generate_series(1,8) x); +INSERT INTO tru_ft_ref (SELECT x,md5((x+3)::text) FROM generate_series(1,8) x); +SELECT * FROM tru_ft a, tru_ft_ref b WHERE a.id = b.id; + id | x | id | x +----+----------------------------------+----+---------------------------------- + 1 | eccbc87e4b5ce2fe28308fd9f2a7baf3 | 1 | a87ff679a2f3e71d9181a67b7542122c + 2 | a87ff679a2f3e71d9181a67b7542122c | 2 | e4da3b7fbbce2345d7772b0674a318d5 + 3 | e4da3b7fbbce2345d7772b0674a318d5 | 3 | 1679091c5a880faf6fb5e6087eb1b2dc + 4 | 1679091c5a880faf6fb5e6087eb1b2dc | 4 | 8f14e45fceea167a5a36dedd4bea2543 + 5 | 8f14e45fceea167a5a36dedd4bea2543 | 5 | c9f0f895fb98ab9159f51fd0297e236d + 6 | c9f0f895fb98ab9159f51fd0297e236d | 6 | 45c48cce2e2d7fbdea1afc51c7c6ad26 + 7 | 45c48cce2e2d7fbdea1afc51c7c6ad26 | 7 | d3d9446802a44259755d38e6d163e820 + 8 | d3d9446802a44259755d38e6d163e820 | 8 | 6512bd43d9caa6e02c990b0a82652dca +(8 rows) + +TRUNCATE tru_ft, tru_ft_ref CASCADE; +SELECT * FROM tru_ft a, tru_ft_ref b WHERE a.id = b.id; + id | x | id | x +----+---+----+--- +(0 rows) + -- =================================================================== -- test IMPORT FOREIGN SCHEMA -- =================================================================== diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c index 2175dff824..0cbbb58e89 100644 --- a/contrib/postgres_fdw/postgres_fdw.c +++ b/contrib/postgres_fdw/postgres_fdw.c @@ -373,6 +373,9 @@ static void postgresExplainForeignModify(ModifyTableState *mtstate, ExplainState *es); static void postgresExplainDirectModify(ForeignScanState *node, ExplainState *es); +static void postgresExecForeignTruncate(List *frels_list, + bool is_cascade, + bool restart_seqs); static bool postgresAnalyzeForeignTable(Relation relation, AcquireSampleRowsFunc *func, BlockNumber *totalpages); @@ -546,6 +549,9 @@ postgres_fdw_handler(PG_FUNCTION_ARGS) routine->ExplainForeignModify = postgresExplainForeignModify; routine->ExplainDirectModify = postgresExplainDirectModify; + /* Support function for TRUNCATE */ + routine->ExecForeignTruncate = postgresExecForeignTruncate; + /* Support functions for ANALYZE */ routine->AnalyzeForeignTable = postgresAnalyzeForeignTable; @@ -2656,6 +2662,57 @@ postgresExplainDirectModify(ForeignScanState *node, ExplainState *es) } } +/* + * postgresExecForeignTruncate + * It propagates TRUNCATE command to the remote host inside of the + * transaction block. + */ +static void +postgresExecForeignTruncate(List *frels_list, + bool is_cascade, + bool restart_seqs) +{ + Oid server_id = InvalidOid; + UserMapping *user = NULL; + PGconn *conn = NULL; + PGresult *res; + StringInfoData sql; + ListCell *lc; + + /* set up remote query */ + initStringInfo(&sql); + appendStringInfoString(&sql, "TRUNCATE "); + foreach (lc, frels_list) + { + Relation frel = lfirst(lc); + Oid frel_oid = RelationGetRelid(frel); + + if (!OidIsValid(server_id)) + { + server_id = GetForeignServerIdByRelId(frel_oid); + user = GetUserMapping(GetUserId(), server_id); + conn = GetConnection(user, false); + } + else + { + Assert(server_id == GetForeignServerIdByRelId(frel_oid)); + appendStringInfoString(&sql, ", "); + } + deparseTruncateSql(&sql, frel); + } + appendStringInfo(&sql, " %s IDENTITY %s", + restart_seqs ? "RESTART" : "CONTINUE", + is_cascade ? "CASCADE" : "RESTRICT"); + + if (!PQsendQuery(conn, sql.data)) + pgfdw_report_error(ERROR, NULL, conn, false, sql.data); + res = pgfdw_get_result(conn, sql.data); + if (PQresultStatus(res) != PGRES_COMMAND_OK) + pgfdw_report_error(ERROR, res, conn, true, sql.data); + /* clean-up */ + PQclear(res); + pfree(sql.data); +} /* * estimate_path_cost_size diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h index eef410db39..ab2eefb126 100644 --- a/contrib/postgres_fdw/postgres_fdw.h +++ b/contrib/postgres_fdw/postgres_fdw.h @@ -190,6 +190,7 @@ extern void deparseDirectDeleteSql(StringInfo buf, PlannerInfo *root, extern void deparseAnalyzeSizeSql(StringInfo buf, Relation rel); extern void deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs); +extern void deparseTruncateSql(StringInfo buf, Relation rel); extern void deparseStringLiteral(StringInfo buf, const char *val); extern Expr *find_em_expr_for_rel(EquivalenceClass *ec, RelOptInfo *rel); extern Expr *find_em_expr_for_input_target(PlannerInfo *root, diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql index f2a4089e9d..44857bf5a4 100644 --- a/contrib/postgres_fdw/sql/postgres_fdw.sql +++ b/contrib/postgres_fdw/sql/postgres_fdw.sql @@ -2287,6 +2287,57 @@ select * from rem3; drop foreign table rem3; drop table loc3; +-- =================================================================== +-- test for TRUNCATE +-- =================================================================== +CREATE TABLE tru_nt (id int, x text); +CREATE FOREIGN TABLE tru_ft (id int, x text) + SERVER loopback OPTIONS (table_name 'tru_nt'); +INSERT INTO tru_nt (SELECT x,md5(x::text) FROM generate_series(1,10) x); + +CREATE TABLE tru_pt (id int, y text) PARTITION BY HASH(id); +CREATE TABLE tru_pt__p0 PARTITION OF tru_pt + FOR VALUES WITH (MODULUS 2, REMAINDER 0); +CREATE TABLE __tru_pt__p1 (id int, y text); +CREATE FOREIGN TABLE tru_pt__p1 PARTITION OF tru_pt + FOR VALUES WITH (MODULUS 2, REMAINDER 1) + SERVER loopback OPTIONS (table_name '__tru_pt__p1'); +INSERT INTO tru_pt (SELECT x,md5(x::text) FROM generate_series(11,30) x); + +CREATE TABLE tru_pk (id int primary key, x text); +INSERT INTO tru_pk (SELECT x,md5((x+1)::text) FROM generate_series(1,10) x); +CREATE TABLE tru_fk (fkey int references tru_pk(id)); +INSERT INTO tru_fk (SELECT x % 10 + 1 FROM generate_series(1,30) x); +CREATE FOREIGN TABLE tru_ft_ref (id int, x text) + SERVER loopback OPTIONS (table_name 'tru_pk'); + +-- normal truncate +SELECT * FROM tru_ft; +TRUNCATE tru_ft; +SELECT * FROM tru_nt; +SELECT * FROM tru_ft; + +-- partition table mixtured by table and foreign table +SELECT * FROM tru_pt; +TRUNCATE tru_pt; +SELECT * FROM tru_pt; +SELECT * FROM tru_pt__p0; +SELECT * FROM tru_pt__p1; +SELECT * FROM __tru_pt__p1; + +-- 'CASCADE' option +SELECT * FROM tru_ft_ref; +TRUNCATE tru_ft_ref; -- failed +TRUNCATE tru_ft_ref CASCADE; +SELECT * FROM tru_ft_ref; + +-- truncate two tables at a command +INSERT INTO tru_ft (SELECT x,md5((x+2)::text) FROM generate_series(1,8) x); +INSERT INTO tru_ft_ref (SELECT x,md5((x+3)::text) FROM generate_series(1,8) x); +SELECT * FROM tru_ft a, tru_ft_ref b WHERE a.id = b.id; +TRUNCATE tru_ft, tru_ft_ref CASCADE; +SELECT * FROM tru_ft a, tru_ft_ref b WHERE a.id = b.id; + -- =================================================================== -- test IMPORT FOREIGN SCHEMA -- =================================================================== diff --git a/doc/src/sgml/fdwhandler.sgml b/doc/src/sgml/fdwhandler.sgml index 6587678af2..0c16f90d8d 100644 --- a/doc/src/sgml/fdwhandler.sgml +++ b/doc/src/sgml/fdwhandler.sgml @@ -968,6 +968,43 @@ EndDirectModify(ForeignScanState *node); + + FDW Routines for Truncate + +void +ExecForeignTruncate(List *frels_list, bool is_cascade, bool restart_seqs); + + + Executes remote truncate on the remote relation associated with this + foreign table. This optional function is called during execution of + TRUNCATE, for each foreign server being involved. + (Note that invocations are not per foreign table.) + + If FDW does not provide this callback, PostgreSQL considers + TRUNCATE is not supported on the foreign table. + + + frels_list is a list of foreign tables that are + connected to a particular foreign server; thus, these foreign tables + should have identical foreign server ID. + + is_cascade tells us whether CASCADE + option is supplied, and restart_seqs also tells us + whether RESTART IDENTITY option is supplied at the + TRUNCATE command. + + + Pay attention that TRUNCATE can be transaction-safe + in PostgreSQL. + We can revert the TRUNCATE operation unless current + transaction is not committed, however, it is not true on some other + database products. + If FDW author considers to support TRUNCATE on their + FDW driver, it may not be sufficient just to run a + TRUNCATE command on the remote side. + + + FDW Routines for Row Locking diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 421bc28727..2f80451060 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "commands/user.h" #include "executor/executor.h" #include "foreign/foreign.h" +#include "foreign/fdwapi.h" #include "miscadmin.h" #include "nodes/makefuncs.h" #include "nodes/nodeFuncs.h" @@ -1624,6 +1625,8 @@ ExecuteTruncateGuts(List *explicit_rels, List *relids, List *relids_logged, { List *rels; List *seq_relids = NIL; + List *servs_list = NIL; + List *frels_list = NIL; EState *estate; ResultRelInfo *resultRelInfos; ResultRelInfo *resultRelInfo; @@ -1768,6 +1771,16 @@ ExecuteTruncateGuts(List *explicit_rels, List *relids, List *relids_logged, /* Skip partitioned tables as there is nothing to do */ if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) continue; + /* Remember the server and foreign-tables to be truncated */ + if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE) + { + Oid frel_oid = RelationGetRelid(rel); + Oid server_id = GetForeignServerIdByRelId(frel_oid); + + servs_list = list_append_unique_oid(servs_list, server_id); + frels_list = lappend(frels_list, rel); + continue; + } /* * Normally, we need a transaction-safe truncation here. However, if @@ -1829,6 +1842,34 @@ ExecuteTruncateGuts(List *explicit_rels, List *relids, List *relids_logged, pgstat_count_truncate(rel); } + /* + * Invocation of FDW handler for each foreign server + */ + foreach (cell, servs_list) + { + Oid server_id = lfirst_oid(cell); + FdwRoutine *routine = GetFdwRoutineByServerId(server_id); + List *curr_frels = NIL; + ListCell *lc; + + foreach (lc, frels_list) + { + Relation frel = lfirst(lc); + Oid frel_oid = RelationGetRelid(frel); + + if (server_id == GetForeignServerIdByRelId(frel_oid)) + { + frels_list = foreach_delete_current(frels_list, lc); + curr_frels = lappend(curr_frels, frel); + } + } + Assert(curr_frels != NIL); + Assert(routine->ExecForeignTruncate != NULL); + routine->ExecForeignTruncate(curr_frels, + behavior == DROP_CASCADE, + restart_seqs); + } + /* * Restart owned sequences if we were asked to. */ @@ -1916,12 +1957,23 @@ truncate_check_rel(Oid relid, Form_pg_class reltuple) char *relname = NameStr(reltuple->relname); /* - * Only allow truncate on regular tables and partitioned tables (although, + * Only allow truncate on regular tables, partitioned tables (although, * the latter are only being included here for the following checks; no - * physical truncation will occur in their case.) + * physical truncation will occur in their case.) and foreign tables + * that support ExecForeignTruncate callback. */ - if (reltuple->relkind != RELKIND_RELATION && - reltuple->relkind != RELKIND_PARTITIONED_TABLE) + if (reltuple->relkind == RELKIND_FOREIGN_TABLE) + { + FdwRoutine *fdwroutine = GetFdwRoutineByRelId(relid); + + if (!fdwroutine->ExecForeignTruncate) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("\"%s\" is not a supported foreign table", + relname))); + } + else if (reltuple->relkind != RELKIND_RELATION && + reltuple->relkind != RELKIND_PARTITIONED_TABLE) ereport(ERROR, (errcode(ERRCODE_WRONG_OBJECT_TYPE), errmsg("\"%s\" is not a table", relname))); diff --git a/src/include/foreign/fdwapi.h b/src/include/foreign/fdwapi.h index 95556dfb15..c8864f2052 100644 --- a/src/include/foreign/fdwapi.h +++ b/src/include/foreign/fdwapi.h @@ -118,6 +118,10 @@ typedef TupleTableSlot *(*IterateDirectModify_function) (ForeignScanState *node) typedef void (*EndDirectModify_function) (ForeignScanState *node); +typedef void (*ExecForeignTruncate_function) (List *frels_list, + bool is_cascade, + bool restart_seqs); + typedef RowMarkType (*GetForeignRowMarkType_function) (RangeTblEntry *rte, LockClauseStrength strength); @@ -220,6 +224,9 @@ typedef struct FdwRoutine IterateDirectModify_function IterateDirectModify; EndDirectModify_function EndDirectModify; + /* Functions for truncating foreign tables */ + ExecForeignTruncate_function ExecForeignTruncate; + /* Functions for SELECT FOR UPDATE/SHARE row locking */ GetForeignRowMarkType_function GetForeignRowMarkType; RefetchForeignRow_function RefetchForeignRow;