diff --git a/contrib/postgres_fdw/Makefile b/contrib/postgres_fdw/Makefile
index adfbd2ef758e..2346d2947cab 100644
--- a/contrib/postgres_fdw/Makefile
+++ b/contrib/postgres_fdw/Makefile
@@ -7,7 +7,8 @@ OBJS = \
deparse.o \
option.o \
postgres_fdw.o \
- shippable.o
+ shippable.o \
+ postgres_fdw_auto_explain.o
PGFILEDESC = "postgres_fdw - foreign data wrapper for PostgreSQL"
PG_CPPFLAGS = -I$(libpq_srcdir)
diff --git a/contrib/postgres_fdw/connection.c b/contrib/postgres_fdw/connection.c
index 8a8d3b4481f3..d72fb38163ac 100644
--- a/contrib/postgres_fdw/connection.c
+++ b/contrib/postgres_fdw/connection.c
@@ -775,6 +775,11 @@ configure_remote_session(PGconn *conn)
do_sql_command(conn, "SET extra_float_digits = 3");
else
do_sql_command(conn, "SET extra_float_digits = 2");
+
+ if (get_postgres_fdw_show_remote_explain_enabled())
+ {
+ do_sql_command(conn, "LOAD 'postgres_fdw'");
+ }
}
/*
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index daa3b1d7a6d9..e58481016bb6 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -442,6 +442,98 @@ SELECT 'fixed', NULL FROM ft1 t1 WHERE c1 = 1;
fixed |
(1 row)
+-- run same queries with postgres_fdw.show_remote_explain_plans set on
+SET postgres_fdw.show_remote_explain_plans = on;
+-- single table without alias
+EXPLAIN (COSTS OFF) SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
+ QUERY PLAN
+-----------------------------------
+ Foreign Scan on ft1
+ Remote Plan
+ Limit
+ -> Sort
+ Sort Key: c3, "C 1"
+ -> Seq Scan on "T 1"
+(6 rows)
+
+-- single table with alias - also test that tableoid sort is not pushed to remote side
+EXPLAIN (VERBOSE, COSTS OFF) SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1, t1.tableoid OFFSET 100 LIMIT 10;
+ QUERY PLAN
+-------------------------------------------------------------------------------------
+ Limit
+ Output: c1, c2, c3, c4, c5, c6, c7, c8, tableoid
+ -> Sort
+ Output: c1, c2, c3, c4, c5, c6, c7, c8, tableoid
+ Sort Key: t1.c3, t1.c1, t1.tableoid
+ -> Foreign Scan on public.ft1 t1
+ Output: c1, c2, c3, c4, c5, c6, c7, c8, tableoid
+ Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8 FROM "S 1"."T 1"
+ Remote Plan
+ Seq Scan on "S 1"."T 1"
+ Output: "C 1", c2, c3, c4, c5, c6, c7, c8
+(11 rows)
+
+-- whole-row reference
+EXPLAIN (VERBOSE, COSTS OFF) SELECT t1 FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+ QUERY PLAN
+--------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on public.ft1 t1
+ Output: t1.*, c3, c1
+ Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8 FROM "S 1"."T 1" ORDER BY c3 ASC NULLS LAST, "C 1" ASC NULLS LAST LIMIT 10::bigint OFFSET 100::bigint
+ Remote Plan
+ Limit
+ Output: "C 1", c2, c3, c4, c5, c6, c7, c8
+ -> Sort
+ Output: "C 1", c2, c3, c4, c5, c6, c7, c8
+ Sort Key: "T 1".c3, "T 1"."C 1"
+ -> Seq Scan on "S 1"."T 1"
+ Output: "C 1", c2, c3, c4, c5, c6, c7, c8
+(11 rows)
+
+-- with WHERE clause
+EXPLAIN (VERBOSE, COSTS OFF) SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 >= '1';
+ QUERY PLAN
+----------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on public.ft1 t1
+ Output: c1, c2, c3, c4, c5, c6, c7, c8
+ Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8 FROM "S 1"."T 1" WHERE ((c7 >= '1')) AND (("C 1" = 101)) AND ((c6 = '1'))
+ Remote Plan
+ Index Scan using t1_pkey on "S 1"."T 1"
+ Output: "C 1", c2, c3, c4, c5, c6, c7, c8
+ Index Cond: ("T 1"."C 1" = 101)
+ Filter: (("T 1".c7 >= '1'::bpchar) AND (("T 1".c6)::text = '1'::text))
+(8 rows)
+
+-- with FOR UPDATE/SHARE
+EXPLAIN (VERBOSE, COSTS OFF) SELECT * FROM ft1 t1 WHERE c1 = 101 FOR UPDATE;
+ QUERY PLAN
+----------------------------------------------------------------------------------------------------------
+ Foreign Scan on public.ft1 t1
+ Output: c1, c2, c3, c4, c5, c6, c7, c8, t1.*
+ Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8 FROM "S 1"."T 1" WHERE (("C 1" = 101)) FOR UPDATE
+ Remote Plan
+ LockRows
+ Output: "C 1", c2, c3, c4, c5, c6, c7, c8, ctid
+ -> Index Scan using t1_pkey on "S 1"."T 1"
+ Output: "C 1", c2, c3, c4, c5, c6, c7, c8, ctid
+ Index Cond: ("T 1"."C 1" = 101)
+(9 rows)
+
+EXPLAIN (VERBOSE, COSTS OFF) SELECT * FROM ft1 t1 WHERE c1 = 102 FOR SHARE;
+ QUERY PLAN
+---------------------------------------------------------------------------------------------------------
+ Foreign Scan on public.ft1 t1
+ Output: c1, c2, c3, c4, c5, c6, c7, c8, t1.*
+ Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8 FROM "S 1"."T 1" WHERE (("C 1" = 102)) FOR SHARE
+ Remote Plan
+ LockRows
+ Output: "C 1", c2, c3, c4, c5, c6, c7, c8, ctid
+ -> Index Scan using t1_pkey on "S 1"."T 1"
+ Output: "C 1", c2, c3, c4, c5, c6, c7, c8, ctid
+ Index Cond: ("T 1"."C 1" = 102)
+(9 rows)
+
+SET postgres_fdw.show_remote_explain_plans = off;
-- Test forcing the remote server to produce sorted data for a merge join.
SET enable_hashjoin TO false;
SET enable_nestloop TO false;
diff --git a/contrib/postgres_fdw/option.c b/contrib/postgres_fdw/option.c
index d0766f007d2f..15870912e422 100644
--- a/contrib/postgres_fdw/option.c
+++ b/contrib/postgres_fdw/option.c
@@ -589,4 +589,8 @@ _PG_init(void)
NULL);
MarkGUCPrefixReserved("postgres_fdw");
+
+ /* Setup hooks and gucs. */
+ setup_postgres_fdw_gucs();
+ setup_postgres_fdw_hooks();
}
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index de43727a2a05..470f1cb8a93d 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -59,6 +59,9 @@ PG_MODULE_MAGIC;
/* If no remote estimates, assume a sort costs 20% extra */
#define DEFAULT_FDW_SORT_MULTIPLIER 1.2
+/* This is set implementation of post_parse_analyze_hook so that we know EXPLAIN command options. */
+extern ExplainState *pg_fdw_parsed_explain_state;
+
/*
* Indexes of FDW-private information stored in fdw_private lists.
*
@@ -170,6 +173,7 @@ typedef struct PgFdwScanState
MemoryContext temp_cxt; /* context for per-tuple temporary data */
int fetch_size; /* number of tuples per fetch */
+ StringInfo remote_explain_plan; /* EXPLAIN plan received from foreign server */
} PgFdwScanState;
/*
@@ -541,7 +545,14 @@ static void merge_fdw_options(PgFdwRelationInfo *fpinfo,
const PgFdwRelationInfo *fpinfo_o,
const PgFdwRelationInfo *fpinfo_i);
static int get_batch_size_option(Relation rel);
-
+static void set_guc_boolean(PGconn *conn, char* guc, bool value);
+static void set_guc_string(PGconn *conn, char* guc, char* value);
+static void subscribe_postgres_fdw_notices(PGconn *conn, StringInfo explain_plans);
+static void postgres_fdw_explain_notice_processor(void *arg, const char *notice);
+bool static is_explain_query(const char *sql);
+static void enrich_foreign_plans(char *sql, ExplainState *es, UserMapping *inputUser);
+static UserMapping* get_remote_user(ForeignScanState *node);
+static void append_foreign_explain_plan(ExplainState *es, StringInfo explainPlan);
/*
* Foreign-data wrapper handler function: return a struct with pointers
@@ -1516,20 +1527,7 @@ postgresBeginForeignScan(ForeignScanState *node, int eflags)
fsstate = (PgFdwScanState *) palloc0(sizeof(PgFdwScanState));
node->fdw_state = fsstate;
- /*
- * Identify which user to do the remote access as. This should match what
- * ExecCheckPermissions() does.
- */
- userid = OidIsValid(fsplan->checkAsUser) ? fsplan->checkAsUser : GetUserId();
- if (fsplan->scan.scanrelid > 0)
- rtindex = fsplan->scan.scanrelid;
- else
- rtindex = bms_next_member(fsplan->fs_base_relids, -1);
- rte = exec_rt_fetch(rtindex, estate);
-
- /* Get info about foreign table. */
- table = GetForeignTable(rte->relid);
- user = GetUserMapping(userid, table->serverid);
+ user = get_remote_user(node);
/*
* Get connection to the foreign server. Connection manager will
@@ -1537,6 +1535,37 @@ postgresBeginForeignScan(ForeignScanState *node, int eflags)
*/
fsstate->conn = GetConnection(user, false, &fsstate->conn_state);
+ /*
+ * If this is EXPLAIN command, set guc on remote connection to provide options to foreign server for plan generation.
+ */
+ if (is_explain_query(estate->es_sourceText) && get_postgres_fdw_show_remote_explain_enabled()) {
+ set_guc_boolean(fsstate->conn, "postgres_fdw.auto_explain_enabled", true);
+ set_guc_boolean(fsstate->conn, "postgres_fdw.analyze_enabled", pg_fdw_parsed_explain_state->analyze);
+
+ char *expected_explain_format;
+ switch (pg_fdw_parsed_explain_state->format)
+ {
+ case EXPLAIN_FORMAT_TEXT:
+ expected_explain_format = "text";
+ break;
+ case EXPLAIN_FORMAT_JSON:
+ expected_explain_format = "json";
+ break;
+ case EXPLAIN_FORMAT_YAML:
+ expected_explain_format = "yaml";
+ break;
+ case EXPLAIN_FORMAT_XML:
+ expected_explain_format = "xml";
+ break;
+ }
+ set_guc_string(fsstate->conn, "postgres_fdw.format_enabled", expected_explain_format);
+ set_guc_boolean(fsstate->conn, "postgres_fdw.settings_enabled", pg_fdw_parsed_explain_state->settings);
+ set_guc_boolean(fsstate->conn, "postgres_fdw.verbose_enabled", pg_fdw_parsed_explain_state->verbose);
+ set_guc_boolean(fsstate->conn, "postgres_fdw.buffers_enabled", pg_fdw_parsed_explain_state->buffers);
+ set_guc_boolean(fsstate->conn, "postgres_fdw.wal_enabled", pg_fdw_parsed_explain_state->wal);
+ set_guc_boolean(fsstate->conn, "postgres_fdw.timing_enabled", pg_fdw_parsed_explain_state->timing);
+ }
+
/* Assign a unique ID for my cursor */
fsstate->cursor_number = GetCursorNumber(fsstate->conn);
fsstate->cursor_exists = false;
@@ -1574,6 +1603,14 @@ postgresBeginForeignScan(ForeignScanState *node, int eflags)
fsstate->attinmeta = TupleDescGetAttInMetadata(fsstate->tupdesc);
+ /*
+ * Subscribe to NOTICES received from foreign server.
+ */
+ if (get_postgres_fdw_show_remote_explain_enabled() && is_explain_query(estate->es_sourceText)) {
+ fsstate->remote_explain_plan = makeStringInfo();
+ subscribe_postgres_fdw_notices(fsstate->conn, fsstate->remote_explain_plan);
+ }
+
/*
* Prepare for processing of parameters used in remote query, if any.
*/
@@ -2822,6 +2859,114 @@ postgresEndDirectModify(ForeignScanState *node)
/* MemoryContext will be deleted automatically. */
}
+/*
+ * Append plan received from foreign server to ExplainState.
+ */
+static void
+append_foreign_explain_plan(ExplainState *es, StringInfo explainPlan)
+{
+ if (explainPlan->len)
+ {
+ /* Append "Remote Plan" header */
+ switch (es->format)
+ {
+ case EXPLAIN_FORMAT_TEXT:
+ appendStringInfoSpaces(es->str, es->indent * 2);
+ appendStringInfo(es->str, "Remote Plan\n");
+ break;
+ case EXPLAIN_FORMAT_JSON:
+ appendStringInfo(es->str, ",\n");
+ appendStringInfoSpaces(es->str, es->indent * 2);
+ appendStringInfo(es->str, "\"Remote Plan\": ");
+ break;
+ case EXPLAIN_FORMAT_XML:
+ appendStringInfoSpaces(es->str, es->indent * 2);
+ appendStringInfo(es->str, "\n");
+ break;
+ case EXPLAIN_FORMAT_YAML:
+ appendStringInfo(es->str, "\n");
+ appendStringInfoSpaces(es->str, es->indent * 2);
+ appendStringInfo(es->str, "Remote-Plan:\n");
+ break;
+ }
+
+ /* Append fetched remote plan */
+ char *explainPlans = explainPlan->data;
+ if (explainPlans) {
+ // remove extra newlines at the end
+ char *last = explainPlans + explainPlan->len;
+ while (last > explainPlans && (*last == '\n' || *last == '\0'))
+ {
+ *last = '\0';
+ last--;
+ }
+ }
+
+ /* Fetch explain plan from { */
+ // why formatting changes are required
+ if (explainPlans && es->format == EXPLAIN_FORMAT_JSON)
+ {
+ // explainPlans = strchr(explainPlans, '{');
+ }
+ else if (es->format == EXPLAIN_FORMAT_YAML) {
+ explainPlans = strchr(explainPlans, 'P');
+ }
+ bool firstLine = true;
+ while (explainPlans && strlen(explainPlans) > 0)
+ {
+ // this is last line in JSON format
+ if (es->format == EXPLAIN_FORMAT_JSON && strcmp(explainPlans, "]") == 0) {
+ // appendStringInfoSpaces(es->str, es->indent * 2);
+ // appendStringInfoString(es->str,"}");
+ appendStringInfoSpaces(es->str, es->indent * 2);
+ appendStringInfoString(es->str,"]");
+ break;
+ }
+
+ /* add extra indent if not first line in json */
+ if (es->format == EXPLAIN_FORMAT_JSON && firstLine) {
+ firstLine = false;
+ }
+ else if (es->format == EXPLAIN_FORMAT_JSON) {
+ appendStringInfoSpaces(es->str, es->indent * 2);
+ }
+ else if (es->format == EXPLAIN_FORMAT_YAML) {
+ appendStringInfoSpaces(es->str, es->indent * 2 + 4);
+ }
+ else if (es->format == EXPLAIN_FORMAT_XML) {
+ appendStringInfoSpaces(es->str, es->indent * 2);
+ }
+ else {
+ appendStringInfoSpaces(es->str, es->indent * 2 + 2);
+ }
+
+ /* Temporarily replace earliest '\n' with '\0' to get current line */
+ char *curLine = strchr(explainPlans, '\n');
+ if (curLine)
+ *curLine = '\0';
+
+ if (curLine) /* if newline in curline, add '\n' at end */
+ appendStringInfo(es->str,"%s\n", explainPlans);
+ else
+ appendStringInfo(es->str,"%s", explainPlans);
+
+ /* Restore '\n' */
+ if (curLine)
+ *curLine = '\n';
+
+ explainPlans = curLine ? (curLine+1) : NULL;
+ }
+
+ /* Append remote plan footer */
+ switch (es->format)
+ {
+ case EXPLAIN_FORMAT_XML:
+ appendStringInfo(es->str, "\n");
+ break;
+ }
+ }
+}
+
/*
* postgresExplainForeignScan
* Produce extra output for EXPLAIN of a ForeignScan on a foreign table
@@ -2831,6 +2976,7 @@ postgresExplainForeignScan(ForeignScanState *node, ExplainState *es)
{
ForeignScan *plan = castNode(ForeignScan, node->ss.ps.plan);
List *fdw_private = plan->fdw_private;
+ EState *estate = node->ss.ps.state;
/*
* Identify foreign scans that are really joins or upper relations. The
@@ -2927,6 +3073,28 @@ postgresExplainForeignScan(ForeignScanState *node, ExplainState *es)
sql = strVal(list_nth(fdw_private, FdwScanPrivateSelectSql));
ExplainPropertyText("Remote SQL", sql, es);
}
+
+ if (get_postgres_fdw_show_remote_explain_enabled())
+ {
+ /* For analyze = false, we explicitly fetch query plans by executing EXPLAIN on remote shard. */
+ if (!es->analyze)
+ {
+ int rtindex;
+ char *sql;
+ sql = strVal(list_nth(fdw_private, FdwScanPrivateSelectSql));
+ UserMapping *user = get_remote_user(node);
+ enrich_foreign_plans(sql, es, user);
+ }
+ else if (is_explain_query(node->ss.ps.state->es_sourceText)) {
+ PgFdwScanState *fsstate = (PgFdwScanState *) node->fdw_state;
+ if (fsstate->remote_explain_plan->len > 0) {
+ append_foreign_explain_plan(es, fsstate->remote_explain_plan);
+ if (es->format == EXPLAIN_FORMAT_TEXT) {
+ appendStringInfo(es->str, "\n");
+ }
+ }
+ }
+ }
}
/*
@@ -3804,8 +3972,15 @@ static void
fetch_more_data(ForeignScanState *node)
{
PgFdwScanState *fsstate = (PgFdwScanState *) node->fdw_state;
+ ForeignScan *fsplan = (ForeignScan *) node->ss.ps.plan;
+ EState *estate = node->ss.ps.state;
PGresult *volatile res = NULL;
MemoryContext oldcontext;
+ RangeTblEntry *rte;
+ Oid userid;
+ ForeignTable *table;
+ UserMapping *user;
+ int rtindex;
/*
* We'll store the tuples in the batch_cxt. First, flush the previous
@@ -7963,3 +8138,154 @@ get_batch_size_option(Relation rel)
return batch_size;
}
+
+/*
+ * Identify which user to do the remote access as. This should match what
+ * ExecCheckPermissions() does.
+ */
+static UserMapping*
+get_remote_user(ForeignScanState *node)
+{
+ ForeignScan *fsplan = (ForeignScan *) node->ss.ps.plan;
+ EState *estate = node->ss.ps.state;
+ Oid userid;
+ int rtindex;
+ RangeTblEntry *rte;
+ ForeignTable *table;
+
+ userid = OidIsValid(fsplan->checkAsUser) ? fsplan->checkAsUser : GetUserId();
+ if (fsplan->scan.scanrelid > 0)
+ rtindex = fsplan->scan.scanrelid;
+ else
+ rtindex = bms_next_member(fsplan->fs_base_relids, -1);
+ rte = exec_rt_fetch(rtindex, estate);
+
+ /* Get info about foreign table. */
+ table = GetForeignTable(rte->relid);
+ return GetUserMapping(userid, table->serverid);
+}
+
+/*
+ * Connect to remote shards and retreive the explain plans for the given sql.
+ */
+static void
+enrich_foreign_plans(char *sql, ExplainState *es, UserMapping *inputUser) {
+ PGresult *volatile res = NULL;
+ PGconn *conn;
+ StringInfoData sqlE, multiLineplans;
+
+ /* Prepare EXPLAIN command to be sent to foreign server. */
+ initStringInfo(&sqlE);
+ appendStringInfoString(&sqlE, "EXPLAIN (");
+ appendStringInfo(&sqlE, "ANALYZE %s", es->analyze? "true" : "false");
+ appendStringInfo(&sqlE, ", VERBOSE %s", es->verbose? "true" : "false");
+ appendStringInfo(&sqlE, ", COSTS %s", es->costs? "true" : "false");
+ appendStringInfo(&sqlE, ", SETTINGS %s", es->settings? "true" : "false");
+ appendStringInfo(&sqlE, ", BUFFERS %s", es->buffers? "true" : "false");
+
+ if (es->serialize == EXPLAIN_SERIALIZE_NONE) {
+ appendStringInfoString(&sqlE, ", SERIALIZE OFF");
+ }
+ else if (es->serialize == EXPLAIN_SERIALIZE_TEXT) {
+ appendStringInfoString(&sqlE, ", SERIALIZE TEXT");
+ }
+ else if (es->serialize == EXPLAIN_SERIALIZE_BINARY) {
+ appendStringInfoString(&sqlE, ", SERIALIZE BINARY");
+ }
+
+ appendStringInfo(&sqlE, ", WAL %s", es->wal? "true" : "false");
+ appendStringInfo(&sqlE, ", TIMING %s", es->timing? "true" : "false");
+ appendStringInfo(&sqlE, ", SUMMARY %s", es->summary? "true" : "false");
+ appendStringInfo(&sqlE, ", MEMORY %s", es->memory? "true" : "false");
+
+ if (es->format == EXPLAIN_FORMAT_TEXT)
+ {
+ appendStringInfoString(&sqlE, ", FORMAT TEXT");
+ }
+ else if (es->format == EXPLAIN_FORMAT_JSON)
+ {
+ appendStringInfoString(&sqlE, ", FORMAT JSON");
+ }
+ else if (es->format == EXPLAIN_FORMAT_XML)
+ {
+ appendStringInfoString(&sqlE, ", FORMAT XML");
+ }
+ else if (es->format == EXPLAIN_FORMAT_YAML)
+ {
+ appendStringInfoString(&sqlE, ", FORMAT YAML");
+ }
+ appendStringInfoString(&sqlE, ")");
+ appendStringInfoString(&sqlE, sql);
+
+ conn = GetConnection(inputUser, false, NULL);
+
+ PG_TRY();
+ {
+ // Run the query and collect the remote plan
+ res = pgfdw_exec_query(conn, sqlE.data, NULL);
+ if (PQresultStatus(res) != PGRES_TUPLES_OK)
+ pgfdw_report_error(ERROR, res, conn, false, sql);
+ int numrows = PQntuples(res);
+
+ initStringInfo(&multiLineplans);
+ for (int i = 0; i < numrows; i++) {
+ appendStringInfoString(&multiLineplans, PQgetvalue(res, i, 0));
+ if (i != numrows - 1)
+ appendStringInfoString(&multiLineplans, "\n");
+ }
+ append_foreign_explain_plan(es, &multiLineplans);
+ }
+ PG_FINALLY();
+ {
+ if (res)
+ PQclear(res);
+ }
+ PG_END_TRY();
+
+ ReleaseConnection(conn);
+}
+
+static void set_guc_boolean(PGconn *conn, char* guc, bool value)
+{
+ int guc_sql_len = ((strlen("SET LOCAL = ") + strlen(guc)) * sizeof(char)) + sizeof(int);
+ char* guc_sql = palloc0(guc_sql_len + 1);
+ snprintf(guc_sql, guc_sql_len, "SET LOCAL %s = %d", guc, value);
+ do_sql_command(conn, guc_sql);
+}
+
+static void set_guc_string(PGconn *conn, char* guc, char* value)
+{
+ int guc_sql_len = ((strlen("SET LOCAL = ") + strlen(guc) + strlen(value)) * sizeof(char)) + sizeof(int);
+ char* guc_sql = palloc0(guc_sql_len + 1);
+ snprintf(guc_sql, guc_sql_len, "SET LOCAL %s = %s", guc, value);
+ do_sql_command(conn, guc_sql);
+}
+
+/*
+ * This is callback function for NOTICEs from remote shards for EXPLAIN ANALYZE queries.
+ */
+static void postgres_fdw_explain_notice_processor(void *arg, const char *notice) {
+ StringInfo explain_plans = (char **) arg;
+ // We might receive plans per batch of cursor, but we only need to store one.
+ // do we really need to handle len==0. report warn if we still recived. have test around this warn.
+ if (strstr(notice, "postgres_fdw_explain_plan") && explain_plans->len == 0) {
+ char *explain_plan_str = strchr(strchr(notice, ':') + 1, ':') + 1;
+ appendStringInfoString(explain_plans, explain_plan_str);
+ }
+}
+
+/*
+ * Remote shards sends the EXPLAIN PLANS as a NOTICE to the host shard when a guc is set for EXPLAIN ANALYZE queries.
+ * To listen to those NOTICEs, here we subscribe to NOTICEs on this connection and register callback so that
+ * callback function is called instead of defaultNoticeProcessor.
+ */
+static void subscribe_postgres_fdw_notices(PGconn *conn, StringInfo explain_plans) {
+ PQsetNoticeProcessor(conn, postgres_fdw_explain_notice_processor, explain_plans);
+}
+
+/*
+ * Return true if this is EXPLAIN query.
+ */
+bool static is_explain_query(const char *sql) {
+ return pg_strncasecmp("EXPLAIN", sql, 7) == 0;
+}
\ No newline at end of file
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index 81358f3bde7d..e33313b23041 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -257,4 +257,7 @@ extern const char *get_jointype_name(JoinType jointype);
extern bool is_builtin(Oid objectId);
extern bool is_shippable(Oid objectId, Oid classId, PgFdwRelationInfo *fpinfo);
+extern void setup_postgres_fdw_hooks(void);
+extern void setup_postgres_fdw_gucs(void);
+
#endif /* POSTGRES_FDW_H */
diff --git a/contrib/postgres_fdw/postgres_fdw_auto_explain.c b/contrib/postgres_fdw/postgres_fdw_auto_explain.c
new file mode 100644
index 000000000000..699792e03135
--- /dev/null
+++ b/contrib/postgres_fdw/postgres_fdw_auto_explain.c
@@ -0,0 +1,383 @@
+/*-------------------------------------------------------------------------
+ *
+ * postgres_fdw_auto_explain.c
+ *
+ *
+ * Copyright (c) 2008-2024, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/postgres_fdw/postgres_fdw_auto_explain.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include
+
+#include "access/parallel.h"
+#include "commands/explain.h"
+#include "common/pg_prng.h"
+#include "executor/instrument.h"
+#include "utils/guc.h"
+#include "parser/analyze.h"
+
+/* GUC variables */
+static bool postgres_fdw_show_remote_explain_plans = false;
+static bool postgres_fdw_auto_explain_enabled = false;
+static bool postgres_fdw_analyze = false;
+static bool postgres_fdw_verbose = false;
+static bool postgres_fdw_buffers = false;
+static bool postgres_fdw_wal = false;
+static bool postgres_fdw_triggers = false;
+static bool postgres_fdw_timing = true;
+static bool postgres_fdw_settings = false;
+static int postgres_fdw_format = EXPLAIN_FORMAT_TEXT;
+
+
+static const struct config_enum_entry format_options[] = {
+ {"text", EXPLAIN_FORMAT_TEXT, false},
+ {"xml", EXPLAIN_FORMAT_XML, false},
+ {"json", EXPLAIN_FORMAT_JSON, false},
+ {"yaml", EXPLAIN_FORMAT_YAML, false},
+ {NULL, 0, false}
+};
+
+/* Current nesting depth of ExecutorRun calls */
+static int nesting_level = 0;
+
+#define auto_explain_enabled() \
+ (postgres_fdw_auto_explain_enabled && nesting_level == 1)
+
+
+/* Saved hook values in case of unload */
+static ExecutorStart_hook_type prev_ExecutorStart = NULL;
+static ExecutorRun_hook_type prev_ExecutorRun = NULL;
+static ExecutorFinish_hook_type prev_ExecutorFinish = NULL;
+static ExecutorEnd_hook_type prev_ExecutorEnd = NULL;
+static post_parse_analyze_hook_type prev_post_parse_analyze_hook = NULL;
+
+ExplainState *pg_fdw_parsed_explain_state;
+static bool pg_fdw_load_initialized = false;
+
+static void explain_ExecutorStart(QueryDesc *queryDesc, int eflags);
+static void explain_ExecutorRun(QueryDesc *queryDesc,
+ ScanDirection direction,
+ uint64 count, bool execute_once);
+static void explain_ExecutorFinish(QueryDesc *queryDesc);
+static void explain_ExecutorEnd(QueryDesc *queryDesc);
+static void postgres_fdw_post_parse_analyze(ParseState *pstate, Query *query, JumbleState *jstate);
+bool get_postgres_fdw_show_remote_explain_enabled();
+
+void setup_postgres_fdw_hooks()
+{
+ prev_ExecutorStart = ExecutorStart_hook;
+ ExecutorStart_hook = explain_ExecutorStart;
+ prev_ExecutorRun = ExecutorRun_hook;
+ ExecutorRun_hook = explain_ExecutorRun;
+ prev_ExecutorFinish = ExecutorFinish_hook;
+ ExecutorFinish_hook = explain_ExecutorFinish;
+ prev_ExecutorEnd = ExecutorEnd_hook;
+ ExecutorEnd_hook = explain_ExecutorEnd;
+
+ prev_post_parse_analyze_hook = post_parse_analyze_hook;
+ post_parse_analyze_hook = postgres_fdw_post_parse_analyze;
+ pg_fdw_load_initialized = true;
+}
+
+void setup_postgres_fdw_gucs()
+{
+ DefineCustomBoolVariable("postgres_fdw.show_remote_explain_plans",
+ "If enabled, plan from foreign server is embeded in EXPLAIN command output",
+ NULL,
+ &postgres_fdw_show_remote_explain_plans,
+ false,
+ PGC_SIGHUP | PGC_USERSET,
+ 0,
+ NULL,
+ NULL,
+ NULL);
+
+ DefineCustomBoolVariable("postgres_fdw.auto_explain_enabled",
+ "If enabled, explain plan is sent as a NOTICE",
+ NULL,
+ &postgres_fdw_auto_explain_enabled,
+ false,
+ PGC_USERSET,
+ 0,
+ NULL,
+ NULL,
+ NULL);
+
+ DefineCustomBoolVariable("postgres_fdw.analyze_enabled",
+ "Use EXPLAIN ANALYZE for plan logging.",
+ NULL,
+ &postgres_fdw_analyze,
+ false,
+ PGC_USERSET,
+ 0,
+ NULL,
+ NULL,
+ NULL);
+
+ DefineCustomBoolVariable("postgres_fdw.settings_enabled",
+ "Log modified configuration parameters affecting query planning.",
+ NULL,
+ &postgres_fdw_settings,
+ false,
+ PGC_USERSET,
+ 0,
+ NULL,
+ NULL,
+ NULL);
+
+ DefineCustomBoolVariable("postgres_fdw.verbose_enabled",
+ "Use EXPLAIN VERBOSE for plan logging.",
+ NULL,
+ &postgres_fdw_verbose,
+ false,
+ PGC_SUSET,
+ 0,
+ NULL,
+ NULL,
+ NULL);
+
+ DefineCustomBoolVariable("postgres_fdw.buffers_enabled",
+ "Log buffers usage.",
+ NULL,
+ &postgres_fdw_buffers,
+ false,
+ PGC_SUSET,
+ 0,
+ NULL,
+ NULL,
+ NULL);
+
+ DefineCustomBoolVariable("postgres_fdw.wal_enabled",
+ "Log WAL usage.",
+ NULL,
+ &postgres_fdw_wal,
+ false,
+ PGC_SUSET,
+ 0,
+ NULL,
+ NULL,
+ NULL);
+
+ DefineCustomBoolVariable("postgres_fdw.triggers_enabled",
+ "Include trigger statistics in plans.",
+ "This has no effect unless analyze is also set.",
+ &postgres_fdw_triggers,
+ false,
+ PGC_SUSET,
+ 0,
+ NULL,
+ NULL,
+ NULL);
+
+ DefineCustomEnumVariable("postgres_fdw.format_enabled",
+ "EXPLAIN format to be used for plan logging.",
+ NULL,
+ &postgres_fdw_format,
+ EXPLAIN_FORMAT_TEXT,
+ format_options,
+ PGC_SUSET,
+ 0,
+ NULL,
+ NULL,
+ NULL);
+
+ DefineCustomBoolVariable("postgres_fdw.timing_enabled",
+ "Collect timing data, not just row counts.",
+ NULL,
+ &postgres_fdw_timing,
+ true,
+ PGC_SUSET,
+ 0,
+ NULL,
+ NULL,
+ NULL);
+}
+
+static void
+postgres_fdw_post_parse_analyze(ParseState *pstate, Query *query, JumbleState *jstate)
+{
+ if (prev_post_parse_analyze_hook)
+ prev_post_parse_analyze_hook(pstate, query, jstate);
+
+ if (query->utilityStmt != NULL && IsA(query->utilityStmt, ExplainStmt)) {
+ pg_fdw_parsed_explain_state = NULL;
+ ExplainStmt *stmt = (ExplainStmt *) query->utilityStmt;
+ pg_fdw_parsed_explain_state = ParseExplainStmtOptions(pstate, stmt->options);
+ }
+}
+
+/*
+ * ExecutorStart hook: start up logging if needed
+ */
+static void
+explain_ExecutorStart(QueryDesc *queryDesc, int eflags)
+{
+ if (auto_explain_enabled())
+ {
+ /* Enable per-node instrumentation iff analyze is required. */
+ if (postgres_fdw_analyze && (eflags & EXEC_FLAG_EXPLAIN_ONLY) == 0)
+ {
+ if (postgres_fdw_timing)
+ queryDesc->instrument_options |= INSTRUMENT_TIMER;
+ else
+ queryDesc->instrument_options |= INSTRUMENT_ROWS;
+ if (postgres_fdw_buffers)
+ queryDesc->instrument_options |= INSTRUMENT_BUFFERS;
+ if (postgres_fdw_wal)
+ queryDesc->instrument_options |= INSTRUMENT_WAL;
+ }
+ }
+
+ if (prev_ExecutorStart)
+ prev_ExecutorStart(queryDesc, eflags);
+ else
+ standard_ExecutorStart(queryDesc, eflags);
+
+ if (auto_explain_enabled())
+ {
+ /*
+ * Set up to track total elapsed time in ExecutorRun. Make sure the
+ * space is allocated in the per-query context so it will go away at
+ * ExecutorEnd.
+ */
+ if (queryDesc->totaltime == NULL)
+ {
+ MemoryContext oldcxt;
+
+ oldcxt = MemoryContextSwitchTo(queryDesc->estate->es_query_cxt);
+ queryDesc->totaltime = InstrAlloc(1, INSTRUMENT_ALL, false);
+ MemoryContextSwitchTo(oldcxt);
+ }
+ }
+}
+
+/*
+ * ExecutorRun hook: all we need do is track nesting depth
+ */
+static void
+explain_ExecutorRun(QueryDesc *queryDesc, ScanDirection direction,
+ uint64 count, bool execute_once)
+{
+ nesting_level++;
+ PG_TRY();
+ {
+ if (prev_ExecutorRun)
+ prev_ExecutorRun(queryDesc, direction, count);
+ else
+ standard_ExecutorRun(queryDesc, direction, count);
+
+ if (auto_explain_enabled()) {
+ // send explain plans
+ ExplainState *es = NewExplainState();
+
+ es->analyze = (queryDesc->instrument_options && postgres_fdw_analyze);
+ es->verbose = postgres_fdw_verbose;
+ es->buffers = (es->analyze && postgres_fdw_buffers);
+ es->wal = (es->analyze && postgres_fdw_wal);
+ es->timing = (es->analyze && postgres_fdw_timing);
+ es->summary = es->analyze;
+ /* No support for MEMORY */
+ /* es->memory = false; */
+ es->format = postgres_fdw_format;
+ es->settings = postgres_fdw_settings;
+
+ ExplainBeginOutput(es);
+ ExplainQueryParameters(es, queryDesc->params, log_parameter_max_length);
+ ExplainPrintPlan(es, queryDesc);
+ if (es->analyze && postgres_fdw_triggers)
+ ExplainPrintTriggers(es, queryDesc);
+ if (es->costs)
+ ExplainPrintJITSummary(es, queryDesc);
+ ExplainEndOutput(es);
+
+ /* Remove last line break */
+ if (es->str->len > 0 && es->str->data[es->str->len - 1] == '\n')
+ es->str->data[--es->str->len] = '\0';
+
+ /* Fix JSON to output an object */
+ if (postgres_fdw_format == EXPLAIN_FORMAT_JSON)
+ {
+ es->str->data[0] = '{';
+ es->str->data[es->str->len - 1] = '}';
+ }
+
+ /*
+ * Note: we rely on the existing logging of context or
+ * debug_query_string to identify just which statement is being
+ * reported. This isn't ideal but trying to do it here would
+ * often result in duplication.
+ */
+ ereport(LOG,
+ (errmsg("postgres_fdw_auto_explain_enabled:%d", postgres_fdw_auto_explain_enabled),
+ errhidestmt(true)));
+ ereport(LOG,
+ (errmsg("sending explain plan to caller:%s", es->str->data),
+ errhidestmt(true)));
+ // receive token from source server and add that in response so that no other code can impact this.
+ ereport(NOTICE,
+ (errmsg("postgres_fdw_explain_plan:%s", es->str->data),
+ errhidestmt(true)));
+ }
+ }
+ PG_FINALLY();
+ {
+ nesting_level--;
+ }
+ PG_END_TRY();
+}
+
+/*
+ * ExecutorFinish hook: all we need do is track nesting depth
+ */
+static void
+explain_ExecutorFinish(QueryDesc *queryDesc)
+{
+ nesting_level++;
+ PG_TRY();
+ {
+ if (prev_ExecutorFinish)
+ prev_ExecutorFinish(queryDesc);
+ else
+ standard_ExecutorFinish(queryDesc);
+ }
+ PG_FINALLY();
+ {
+ nesting_level--;
+ }
+ PG_END_TRY();
+}
+
+/*
+ * ExecutorEnd hook: log results if needed
+ */
+static void
+explain_ExecutorEnd(QueryDesc *queryDesc)
+{
+ if (prev_ExecutorEnd)
+ prev_ExecutorEnd(queryDesc);
+ else
+ standard_ExecutorEnd(queryDesc);
+}
+
+bool get_postgres_fdw_show_remote_explain_enabled()
+{
+ if (postgres_fdw_show_remote_explain_plans) {
+ /*
+ * Check if extension is loaded and we have required hooks initialized. This happens when postgres_fdw wasn't
+ * already loaded and this is first SQL who need to access foreign server.
+ */
+ if (!pg_fdw_load_initialized) {
+ ereport(ERROR,
+ (errmsg("postgres_fdw.show_remote_explain_plans is set but extension postgres_fdw is not loaded yet."),
+ errhidestmt(true)));
+ return false;
+ }
+ return true;
+ }
+ return false;
+}
\ No newline at end of file
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 1598d9e0862d..72e3fc4954f2 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -285,6 +285,22 @@ SELECT * FROM ft1 t1 WHERE t1.c3 = (SELECT MAX(c3) FROM ft2 t2) ORDER BY c1;
WITH t1 AS (SELECT * FROM ft1 WHERE c1 <= 10) SELECT t2.c1, t2.c2, t2.c3, t2.c4 FROM t1, ft2 t2 WHERE t1.c1 = t2.c1 ORDER BY t1.c1;
-- fixed values
SELECT 'fixed', NULL FROM ft1 t1 WHERE c1 = 1;
+
+-- run same queries with postgres_fdw.show_remote_explain_plans set on
+SET postgres_fdw.show_remote_explain_plans = on;
+-- single table without alias
+EXPLAIN (COSTS OFF) SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10;
+-- single table with alias - also test that tableoid sort is not pushed to remote side
+EXPLAIN (VERBOSE, COSTS OFF) SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1, t1.tableoid OFFSET 100 LIMIT 10;
+-- whole-row reference
+EXPLAIN (VERBOSE, COSTS OFF) SELECT t1 FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+-- with WHERE clause
+EXPLAIN (VERBOSE, COSTS OFF) SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 >= '1';
+-- with FOR UPDATE/SHARE
+EXPLAIN (VERBOSE, COSTS OFF) SELECT * FROM ft1 t1 WHERE c1 = 101 FOR UPDATE;
+EXPLAIN (VERBOSE, COSTS OFF) SELECT * FROM ft1 t1 WHERE c1 = 102 FOR SHARE;
+SET postgres_fdw.show_remote_explain_plans = off;
+
-- Test forcing the remote server to produce sorted data for a merge join.
SET enable_hashjoin TO false;
SET enable_nestloop TO false;
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index c0d614866a9a..b2f9c38c7bc4 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -181,28 +181,20 @@ static void ExplainYAMLLineStarting(ExplainState *es);
static void escape_yaml(StringInfo buf, const char *str);
static SerializeMetrics GetSerializationMetrics(DestReceiver *dest);
-
-
-/*
- * ExplainQuery -
- * execute an EXPLAIN command
- */
-void
-ExplainQuery(ParseState *pstate, ExplainStmt *stmt,
- ParamListInfo params, DestReceiver *dest)
+ExplainState *
+ParseExplainStmtOptions(ParseState *pstate, List *options)
{
- ExplainState *es = NewExplainState();
- TupOutputState *tstate;
- JumbleState *jstate = NULL;
- Query *query;
- List *rewritten;
ListCell *lc;
bool timing_set = false;
bool buffers_set = false;
bool summary_set = false;
+ ExplainState *es = NewExplainState();
+
+ if (!options)
+ return es;
/* Parse options list. */
- foreach(lc, stmt->options)
+ foreach(lc, options)
{
DefElem *opt = (DefElem *) lfirst(lc);
@@ -287,18 +279,37 @@ ExplainQuery(ParseState *pstate, ExplainStmt *stmt,
parser_errposition(pstate, opt->location)));
}
+ /* if the timing was not set explicitly, set default value */
+ es->timing = (timing_set) ? es->timing : es->analyze;
+
+ /* if the summary was not set explicitly, set default value */
+ es->summary = (summary_set) ? es->summary : es->analyze;
+
+ return es;
+}
+
+/*
+ * ExplainQuery -
+ * execute an EXPLAIN command
+ */
+void
+ExplainQuery(ParseState *pstate, ExplainStmt *stmt,
+ ParamListInfo params, DestReceiver *dest)
+{
+ ExplainState *es;
+ TupOutputState *tstate;
+ JumbleState *jstate = NULL;
+ Query *query;
+ List *rewritten;
+
+ es = ParseExplainStmtOptions(pstate, stmt->options);
+
/* check that WAL is used with EXPLAIN ANALYZE */
if (es->wal && !es->analyze)
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("EXPLAIN option %s requires ANALYZE", "WAL")));
- /* if the timing was not set explicitly, set default value */
- es->timing = (timing_set) ? es->timing : es->analyze;
-
- /* if the buffers was not set explicitly, set default value */
- es->buffers = (buffers_set) ? es->buffers : es->analyze;
-
/* check that timing is used with EXPLAIN ANALYZE */
if (es->timing && !es->analyze)
ereport(ERROR,
@@ -317,9 +328,6 @@ ExplainQuery(ParseState *pstate, ExplainStmt *stmt,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("EXPLAIN options ANALYZE and GENERIC_PLAN cannot be used together")));
- /* if the summary was not set explicitly, set default value */
- es->summary = (summary_set) ? es->summary : es->analyze;
-
query = castNode(Query, stmt->query);
if (IsQueryIdEnabled())
jstate = JumbleQuery(query);
diff --git a/src/include/commands/explain.h b/src/include/commands/explain.h
index 570e7cad1fa3..10083eb3da5c 100644
--- a/src/include/commands/explain.h
+++ b/src/include/commands/explain.h
@@ -146,4 +146,6 @@ extern void ExplainCloseGroup(const char *objtype, const char *labelname,
extern DestReceiver *CreateExplainSerializeDestReceiver(ExplainState *es);
+extern ExplainState *ParseExplainStmtOptions(ParseState *pstate, List *options);
+
#endif /* EXPLAIN_H */
diff --git a/src/include/pg_config_ext.h b/src/include/pg_config_ext.h
new file mode 100644
index 000000000000..b4c07dd85724
--- /dev/null
+++ b/src/include/pg_config_ext.h
@@ -0,0 +1,8 @@
+/* src/include/pg_config_ext.h. Generated from pg_config_ext.h.in by configure. */
+/*
+ * src/include/pg_config_ext.h.in. This is generated manually, not by
+ * autoheader, since we want to limit which symbols get defined here.
+ */
+
+/* Define to the name of a signed 64-bit integer type. */
+#define PG_INT64_TYPE long int
diff --git a/src/include/stamp-ext-h b/src/include/stamp-ext-h
new file mode 100644
index 000000000000..8b137891791f
--- /dev/null
+++ b/src/include/stamp-ext-h
@@ -0,0 +1 @@
+