From f46403f33189862933b545d4c4b1f9b0c68f2f78 Mon Sep 17 00:00:00 2001 From: Dinesh Salve Date: Sat, 20 Jun 2026 00:26:00 +0530 Subject: [PATCH] postgres_fdw: show remote EXPLAIN plans via REMOTE_PLANS option Add a REMOTE_PLANS option to EXPLAIN that, for every foreign scan or foreign modification, runs EXPLAIN on the remote server for the corresponding remote query and prints the result in a "Remote Plans" section keyed by the local plan node id. The remote query that postgres_fdw sends often contains $n parameter placeholders (parameterized foreign scans and joins, subquery outputs, and INSERT/UPDATE/DELETE), which a plain remote EXPLAIN cannot plan and fails with "there is no parameter $1". REMOTE_PLANS therefore forces GENERIC_PLAN on the remote EXPLAIN, which plans such statements without bound values. GENERIC_PLAN is only available from PostgreSQL 16, so REMOTE_PLANS errors out on older remote servers; the plans shown are consequently generic. REMOTE_PLANS cannot be combined with ANALYZE, so the ANALYZE-only EXPLAIN options are not forwarded to the remote. The remaining plan-shaping options the user requested are forwarded as-is; if the remote server does not recognize one of them it errors out with "unrecognized EXPLAIN option". The option is registered when postgres_fdw is loaded. --- .../postgres_fdw/expected/postgres_fdw.out | 626 ++++++++++++++++++ contrib/postgres_fdw/option.c | 71 ++ contrib/postgres_fdw/postgres_fdw.c | 279 +++++++- contrib/postgres_fdw/postgres_fdw.h | 26 + contrib/postgres_fdw/sql/postgres_fdw.sql | 50 ++ doc/src/sgml/postgres-fdw.sgml | 41 ++ src/include/commands/explain_state.h | 2 +- src/tools/pgindent/typedefs.list | 2 + 8 files changed, 1084 insertions(+), 13 deletions(-) diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out index e90289e..803ac6c 100644 --- a/contrib/postgres_fdw/expected/postgres_fdw.out +++ b/contrib/postgres_fdw/expected/postgres_fdw.out @@ -471,6 +471,173 @@ SELECT 'fixed', NULL FROM ft1 t1 WHERE c1 = 1; fixed | (1 row) +-- with WHERE clause and remote_plans with different formats +EXPLAIN (REMOTE_PLANS, FORMAT YAML, VERBOSE, COSTS OFF) SELECT * FROM ft1 t1 WHERE t1.c1 = 101; + QUERY PLAN +----------------------------------------------------------------------------------------------------------- + - Plan: + + Node Type: "Foreign Scan" + + Operation: "Select" + + Parallel Aware: false + + Async Capable: false + + Relation Name: "ft1" + + Schema: "public" + + Alias: "t1" + + Disabled: false + + 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 ((\"C 1\" = 101))"+ + Plan Node ID: 0 + + Remote Plans: + + Plan Node ID 0: + + - Plan: + + Node Type: "Index Scan" + + Parallel Aware: false + + Async Capable: false + + Scan Direction: "Forward" + + Index Name: "t1_pkey" + + Relation Name: "T 1" + + Schema: "S 1" + + Alias: "T 1" + + Disabled: false + + Output: + + - "\"C 1\"" + + - "c2" + + - "c3" + + - "c4" + + - "c5" + + - "c6" + + - "c7" + + - "c8" + + Index Cond: "(\"T 1\".\"C 1\" = 101)" +(1 row) + +EXPLAIN (REMOTE_PLANS TRUE, FORMAT XML, VERBOSE, COSTS OFF) SELECT * FROM ft1 t1 WHERE t1.c1 = 101; + QUERY PLAN +---------------------------------------------------------------------------------------------------------------- + + + + + + + Foreign Scan + + Select + + false + + false + + ft1 + + public + + t1 + + false + + + + c1 + + c2 + + c3 + + c4 + + c5 + + c6 + + c7 + + c8 + + + + SELECT "C 1", c2, c3, c4, c5, c6, c7, c8 FROM "S 1"."T 1" WHERE (("C 1" = 101))+ + 0 + + + + + + + + + + + + + + Index Scan + + false + + false + + Forward + + t1_pkey + + T 1 + + S 1 + + T 1 + + false + + + + "C 1" + + c2 + + c3 + + c4 + + c5 + + c6 + + c7 + + c8 + + + + ("T 1"."C 1" = 101) + + + + + + + + + + + + + + +(1 row) + +EXPLAIN (REMOTE_PLANS, FORMAT JSON, VERBOSE, COSTS OFF) SELECT * FROM ft1 t1 WHERE t1.c1 = 101; + QUERY PLAN +---------------------------------------------------------------------------------------------------------------- + [ + + { + + "Plan": { + + "Node Type": "Foreign Scan", + + "Operation": "Select", + + "Parallel Aware": false, + + "Async Capable": false, + + "Relation Name": "ft1", + + "Schema": "public", + + "Alias": "t1", + + "Disabled": false, + + "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 ((\"C 1\" = 101))",+ + "Plan Node ID": 0 + + }, + + "Remote Plans": { + + "Plan Node ID 0": [ + + [ + + { + + "Plan": { + + "Node Type": "Index Scan", + + "Parallel Aware": false, + + "Async Capable": false, + + "Scan Direction": "Forward", + + "Index Name": "t1_pkey", + + "Relation Name": "T 1", + + "Schema": "S 1", + + "Alias": "T 1", + + "Disabled": false, + + "Output": ["\"C 1\"", "c2", "c3", "c4", "c5", "c6", "c7", "c8"], + + "Index Cond": "(\"T 1\".\"C 1\" = 101)" + + } + + } + + ] + + ] + + } + + } + + ] +(1 row) + +EXPLAIN (REMOTE_PLANS TRUE, FORMAT TEXT, VERBOSE, COSTS OFF) SELECT * FROM ft1 t1 WHERE t1.c1 = 101; + 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 (("C 1" = 101)) + Plan Node ID: 0 + Remote Plans: + ------------- + Plan Node ID 0: + 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) +(10 rows) + -- Test forcing the remote server to produce sorted data for a merge join. SET enable_hashjoin TO false; SET enable_nestloop TO false; @@ -646,6 +813,25 @@ SELECT t1."C 1", t2.c1, t3.c1 FROM "S 1"."T 1" t1 full join ft1 t2 full join ft2 110 | 110 | 110 (10 rows) +-- Join push-down test +-- Ensure join conditions are pushed down to the foreign server +EXPLAIN (REMOTE_PLANS, VERBOSE, COSTS OFF) + SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) WHERE t1.c2 = 10; + QUERY PLAN +----------------------------------------------------------------------------------------------------------------------------------------- + Foreign Scan + Output: t1.c1, t2.c1 + Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2) + Remote SQL: SELECT r1."C 1", r2."C 1" FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r2."C 1" = r1."C 1")) AND ((r1.c2 = 10)))) + Plan Node ID: 0 + Remote Plans: + ------------- + Plan Node ID 0: + Seq Scan on "S 1"."T 1" r2 + Output: r2."C 1", r2."C 1" + Filter: (r2.c2 = 10) +(11 rows) + RESET enable_hashjoin; RESET enable_nestloop; -- Test executing assertion in estimate_path_cost_size() that makes sure that @@ -790,6 +976,32 @@ SELECT * FROM "S 1"."T 1" a, ft2 b WHERE a."C 1" = 47 AND b.c1 = a.c2; 47 | 7 | 00047 | Tue Feb 17 00:00:00 1970 PST | Tue Feb 17 00:00:00 1970 | 7 | 7 | foo | 7 | 7 | 00007 | Thu Jan 08 00:00:00 1970 PST | Thu Jan 08 00:00:00 1970 | 7 | 7 | foo (1 row) +-- REMOTE_PLANS over a parameterized foreign scan: the deparsed remote SQL +-- carries a $1 placeholder, which only plans on the remote side because +-- REMOTE_PLANS forces GENERIC_PLAN. +EXPLAIN (REMOTE_PLANS, VERBOSE, COSTS OFF) + SELECT * FROM "S 1"."T 1" a, ft2 b WHERE a."C 1" = 47 AND b.c1 = a.c2; + QUERY PLAN +------------------------------------------------------------------------------------------------------------- + Nested Loop + Output: a."C 1", a.c2, a.c3, a.c4, a.c5, a.c6, a.c7, a.c8, b.c1, b.c2, b.c3, b.c4, b.c5, b.c6, b.c7, b.c8 + Plan Node ID: 0 + -> Index Scan using t1_pkey on "S 1"."T 1" a + Output: a."C 1", a.c2, a.c3, a.c4, a.c5, a.c6, a.c7, a.c8 + Index Cond: (a."C 1" = 47) + Plan Node ID: 1 + -> Foreign Scan on public.ft2 b + Output: b.c1, b.c2, b.c3, b.c4, b.c5, b.c6, b.c7, b.c8 + Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8 FROM "S 1"."T 1" WHERE (("C 1" = $1::integer)) + Plan Node ID: 2 + Remote Plans: + ------------- + Plan Node ID 2: + 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" = $1) +(17 rows) + -- check both safe and unsafe join conditions EXPLAIN (VERBOSE, COSTS OFF) SELECT * FROM ft2 a, ft2 b @@ -5116,6 +5328,373 @@ SELECT ft1.c1 FROM ft1 JOIN ft2 on ft1.c1 = ft2.c1 WHERE Remote SQL: SELECT r5."C 1", r6.c1 FROM ("S 1"."T 1" r5 INNER JOIN "S 1"."T 3" r6 ON (((r5."C 1" = r6.c1)))) ORDER BY r5."C 1" ASC NULLS LAST (13 rows) +-- EXPLAIN remote_plans +EXPLAIN (remote_plans, format text, costs off, analyze) +SELECT ft1.c1 FROM ft1 JOIN ft2 on ft1.c1 = ft2.c1 WHERE + ft1.c1 IN ( + SELECT ft2.c1 FROM ft2 JOIN ft4 ON ft2.c1 = ft4.c1) + ORDER BY ft1.c1 LIMIT 5; +ERROR: EXPLAIN options REMOTE_PLANS and ANALYZE cannot be used together +EXPLAIN (remote_plans, format text, costs off) +SELECT ft1.c1 FROM ft1 JOIN ft2 on ft1.c1 = ft2.c1 WHERE + ft1.c1 IN ( + SELECT ft2.c1 FROM ft2 JOIN ft4 ON ft2.c1 = ft4.c1) + ORDER BY ft1.c1 LIMIT 5; + QUERY PLAN +------------------------------------------------------- + Limit + Plan Node ID: 0 + -> Merge Semi Join + Merge Cond: (ft1.c1 = ft2_1.c1) + Plan Node ID: 1 + -> Foreign Scan + Relations: (ft1) INNER JOIN (ft2) + Plan Node ID: 2 + -> Foreign Scan + Relations: (ft2 ft2_1) INNER JOIN (ft4) + Plan Node ID: 3 + Remote Plans: + ------------- + Plan Node ID 2: + Index Only Scan using t1_pkey on "T 1" r2 + Plan Node ID 3: + Merge Join + Merge Cond: (r5."C 1" = r6.c1) + -> Index Only Scan using t1_pkey on "T 1" r5 + -> Sort + Sort Key: r6.c1 + -> Seq Scan on "T 3" r6 +(22 rows) + +EXPLAIN (remote_plans, format xml, costs off) +SELECT ft1.c1 FROM ft1 JOIN ft2 on ft1.c1 = ft2.c1 WHERE + ft1.c1 IN ( + SELECT ft2.c1 FROM ft2 JOIN ft4 ON ft2.c1 = ft4.c1) + ORDER BY ft1.c1 LIMIT 5; + QUERY PLAN +------------------------------------------------------------------------ + + + + + + + Limit + + false + + false + + false + + 0 + + + + + + Merge Join + + Outer + + false + + false + + Semi + + false + + false + + (ft1.c1 = ft2_1.c1) + + 1 + + + + + + Foreign Scan + + Select + + Outer + + false + + false + + false + + (ft1) INNER JOIN (ft2) + + 2 + + + + + + Foreign Scan + + Select + + Inner + + false + + false + + false + + (ft2 ft2_1) INNER JOIN (ft4) + + 3 + + + + + + + + + + + + + + + + + + + + + + Index Only Scan + + false + + false + + Forward + + t1_pkey + + T 1 + + r2 + + false + + + + + + + + + + + + + + + + + + Merge Join + + false + + false + + Inner + + false + + true + + (r5."C 1" = r6.c1) + + + + + + Index Only Scan + + Outer + + false + + false + + Forward + + t1_pkey + + T 1 + + r5 + + false + + + + + + Sort + + Inner + + false + + false + + false + + + + r6.c1 + + + + + + + + Seq Scan + + Outer+ + false + + false + + T 3 + + r6 + + false + + + + + + + + + + + + + + + + + + + + + + +(1 row) + +EXPLAIN (remote_plans, format json, costs off) +SELECT ft1.c1 FROM ft1 JOIN ft2 on ft1.c1 = ft2.c1 WHERE + ft1.c1 IN ( + SELECT ft2.c1 FROM ft2 JOIN ft4 ON ft2.c1 = ft4.c1) + ORDER BY ft1.c1 LIMIT 5; + QUERY PLAN +------------------------------------------------------------ + [ + + { + + "Plan": { + + "Node Type": "Limit", + + "Parallel Aware": false, + + "Async Capable": false, + + "Disabled": false, + + "Plan Node ID": 0, + + "Plans": [ + + { + + "Node Type": "Merge Join", + + "Parent Relationship": "Outer", + + "Parallel Aware": false, + + "Async Capable": false, + + "Join Type": "Semi", + + "Disabled": false, + + "Inner Unique": false, + + "Merge Cond": "(ft1.c1 = ft2_1.c1)", + + "Plan Node ID": 1, + + "Plans": [ + + { + + "Node Type": "Foreign Scan", + + "Operation": "Select", + + "Parent Relationship": "Outer", + + "Parallel Aware": false, + + "Async Capable": false, + + "Disabled": false, + + "Relations": "(ft1) INNER JOIN (ft2)", + + "Plan Node ID": 2 + + }, + + { + + "Node Type": "Foreign Scan", + + "Operation": "Select", + + "Parent Relationship": "Inner", + + "Parallel Aware": false, + + "Async Capable": false, + + "Disabled": false, + + "Relations": "(ft2 ft2_1) INNER JOIN (ft4)",+ + "Plan Node ID": 3 + + } + + ] + + } + + ] + + }, + + "Remote Plans": { + + "Plan Node ID 2": [ + + [ + + { + + "Plan": { + + "Node Type": "Index Only Scan", + + "Parallel Aware": false, + + "Async Capable": false, + + "Scan Direction": "Forward", + + "Index Name": "t1_pkey", + + "Relation Name": "T 1", + + "Alias": "r2", + + "Disabled": false + + } + + } + + ] + + ], + + "Plan Node ID 3": [ + + [ + + { + + "Plan": { + + "Node Type": "Merge Join", + + "Parallel Aware": false, + + "Async Capable": false, + + "Join Type": "Inner", + + "Disabled": false, + + "Inner Unique": true, + + "Merge Cond": "(r5.\"C 1\" = r6.c1)", + + "Plans": [ + + { + + "Node Type": "Index Only Scan", + + "Parent Relationship": "Outer", + + "Parallel Aware": false, + + "Async Capable": false, + + "Scan Direction": "Forward", + + "Index Name": "t1_pkey", + + "Relation Name": "T 1", + + "Alias": "r5", + + "Disabled": false + + }, + + { + + "Node Type": "Sort", + + "Parent Relationship": "Inner", + + "Parallel Aware": false, + + "Async Capable": false, + + "Disabled": false, + + "Sort Key": ["r6.c1"], + + "Plans": [ + + { + + "Node Type": "Seq Scan", + + "Parent Relationship": "Outer", + + "Parallel Aware": false, + + "Async Capable": false, + + "Relation Name": "T 3", + + "Alias": "r6", + + "Disabled": false + + } + + ] + + } + + ] + + } + + } + + ] + + ] + + } + + } + + ] +(1 row) + +EXPLAIN (remote_plans, format yaml, costs off) +SELECT ft1.c1 FROM ft1 JOIN ft2 on ft1.c1 = ft2.c1 WHERE + ft1.c1 IN ( + SELECT ft2.c1 FROM ft2 JOIN ft4 ON ft2.c1 = ft4.c1) + ORDER BY ft1.c1 LIMIT 5; + QUERY PLAN +------------------------------------------------------- + - Plan: + + Node Type: "Limit" + + Parallel Aware: false + + Async Capable: false + + Disabled: false + + Plan Node ID: 0 + + Plans: + + - Node Type: "Merge Join" + + Parent Relationship: "Outer" + + Parallel Aware: false + + Async Capable: false + + Join Type: "Semi" + + Disabled: false + + Inner Unique: false + + Merge Cond: "(ft1.c1 = ft2_1.c1)" + + Plan Node ID: 1 + + Plans: + + - Node Type: "Foreign Scan" + + Operation: "Select" + + Parent Relationship: "Outer" + + Parallel Aware: false + + Async Capable: false + + Disabled: false + + Relations: "(ft1) INNER JOIN (ft2)" + + Plan Node ID: 2 + + - Node Type: "Foreign Scan" + + Operation: "Select" + + Parent Relationship: "Inner" + + Parallel Aware: false + + Async Capable: false + + Disabled: false + + Relations: "(ft2 ft2_1) INNER JOIN (ft4)"+ + Plan Node ID: 3 + + Remote Plans: + + Plan Node ID 2: + + - Plan: + + Node Type: "Index Only Scan" + + Parallel Aware: false + + Async Capable: false + + Scan Direction: "Forward" + + Index Name: "t1_pkey" + + Relation Name: "T 1" + + Alias: "r2" + + Disabled: false + + Plan Node ID 3: + + - Plan: + + Node Type: "Merge Join" + + Parallel Aware: false + + Async Capable: false + + Join Type: "Inner" + + Disabled: false + + Inner Unique: true + + Merge Cond: "(r5.\"C 1\" = r6.c1)" + + Plans: + + - Node Type: "Index Only Scan" + + Parent Relationship: "Outer" + + Parallel Aware: false + + Async Capable: false + + Scan Direction: "Forward" + + Index Name: "t1_pkey" + + Relation Name: "T 1" + + Alias: "r5" + + Disabled: false + + - Node Type: "Sort" + + Parent Relationship: "Inner" + + Parallel Aware: false + + Async Capable: false + + Disabled: false + + Sort Key: + + - "r6.c1" + + Plans: + + - Node Type: "Seq Scan" + + Parent Relationship: "Outer" + + Parallel Aware: false + + Async Capable: false + + Relation Name: "T 3" + + Alias: "r6" + + Disabled: false +(1 row) + -- =================================================================== -- test writable foreign table stuff -- =================================================================== @@ -6354,6 +6933,25 @@ SELECT * FROM ft8 WHERE c1 = '[2,3)' ORDER BY c1, c4; [2,3) | 3 | AAA002 | [01-01-2000,01-01-2020) (1 row) +-- test write on foreign tables with remote_plans +EXPLAIN (remote_plans, verbose, costs off) +UPDATE ft2 SET c2 = c2 + 300 WHERE c1 % 10 = 3; + QUERY PLAN +--------------------------------------------------------------------------------------- + Update on public.ft2 + Plan Node ID: 0 + -> Foreign Update on public.ft2 + Remote SQL: UPDATE "S 1"."T 1" SET c2 = (c2 + 300) WHERE ((("C 1" % 10) = 3)) + Plan Node ID: 1 + Remote Plans: + ------------- + Plan Node ID 1: + Update on "S 1"."T 1" + -> Seq Scan on "S 1"."T 1" + Output: (c2 + 300), ctid + Filter: (("T 1"."C 1" % 10) = 3) +(12 rows) + -- Test UPDATE/DELETE with RETURNING on a three-table join INSERT INTO ft2 (c1,c2,c3) SELECT id, id - 1200, to_char(id, 'FM00000') FROM generate_series(1201, 1300) id; @@ -12322,6 +12920,34 @@ SELECT * FROM insert_tbl ORDER BY a; 2505 | 505 | bar (2 rows) +EXPLAIN (REMOTE_PLANS, VERBOSE, COSTS OFF) +INSERT INTO insert_tbl (SELECT * FROM local_tbl UNION ALL SELECT * FROM remote_tbl); + QUERY PLAN +------------------------------------------------------------------------- + Insert on public.insert_tbl + Remote SQL: INSERT INTO public.base_tbl4(a, b, c) VALUES ($1, $2, $3) + Batch Size: 1 + Plan Node ID: 0 + -> Append + Plan Node ID: 1 + -> Seq Scan on public.local_tbl + Output: local_tbl.a, local_tbl.b, local_tbl.c + Plan Node ID: 2 + -> Async Foreign Scan on public.remote_tbl + Output: remote_tbl.a, remote_tbl.b, remote_tbl.c + Remote SQL: SELECT a, b, c FROM public.base_tbl3 + Plan Node ID: 3 + Remote Plans: + ------------- + Plan Node ID 0: + Insert on public.base_tbl4 + -> Result + Output: $1, $2, $3 + Plan Node ID 3: + Seq Scan on public.base_tbl3 + Output: a, b, c +(22 rows) + -- Check with direct modify EXPLAIN (VERBOSE, COSTS OFF) WITH t AS (UPDATE remote_tbl SET c = c || c RETURNING *) diff --git a/contrib/postgres_fdw/option.c b/contrib/postgres_fdw/option.c index 79b16c3..c39a387 100644 --- a/contrib/postgres_fdw/option.c +++ b/contrib/postgres_fdw/option.c @@ -17,6 +17,7 @@ #include "catalog/pg_foreign_table.h" #include "catalog/pg_user_mapping.h" #include "commands/defrem.h" +#include "commands/explain.h" #include "commands/extension.h" #include "libpq/libpq-be.h" #include "postgres_fdw.h" @@ -40,6 +41,13 @@ typedef struct PgFdwOption */ static PgFdwOption *postgres_fdw_options; +/* + * EXPLAIN hooks + */ +static explain_per_node_hook_type prev_explain_per_node_hook; +static explain_per_plan_hook_type prev_explain_per_plan_hook; +static explain_validate_options_hook_type prev_explain_validate_options_hook; + /* * GUC parameters */ @@ -566,6 +574,57 @@ process_pgfdw_appname(const char *appname) return buf.data; } +/* + * Get the PgFdwExplainState structure from an ExplainState; if there is + * none, create one, attach it to the ExplainState, and return it. + */ +static PgFdwExplainState * +pgfdw_ensure_options(ExplainState *es) +{ + PgFdwExplainState *pgfdw_explain_state; + + pgfdw_explain_state = GetExplainExtensionState(es, GetExplainExtensionId("postgres_fdw")); + + if (pgfdw_explain_state == NULL) + { + pgfdw_explain_state = palloc0(sizeof(PgFdwExplainState)); + SetExplainExtensionState(es, GetExplainExtensionId("postgres_fdw"), pgfdw_explain_state); + pgfdw_explain_state->all_remote_plans = NIL; + } + + return pgfdw_explain_state; +} + +/* + * Parse handler for EXPLAIN (REMOTE_PLANS). + */ +static void +pgfdw_remote_plans_apply(ExplainState *es, DefElem *opt, ParseState *pstate) +{ + PgFdwExplainState *options = pgfdw_ensure_options(es); + + options->remote_plans = defGetBoolean(opt); +} + +static void +postgresExplainValidateOptions(ExplainState *es, List *options, ParseState *pstate) +{ + ListCell *lc; + + foreach(lc, options) + { + DefElem *opt = (DefElem *) lfirst(lc); + + if (strcmp(opt->defname, "remote_plans") == 0) + { + if (defGetBoolean(opt) && es->analyze) + ereport(ERROR, + errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("EXPLAIN options REMOTE_PLANS and ANALYZE cannot be used together")); + } + } +} + /* * Module load callback */ @@ -592,4 +651,16 @@ _PG_init(void) NULL); MarkGUCPrefixReserved("postgres_fdw"); + + RegisterExtensionExplainOption("remote_plans", pgfdw_remote_plans_apply, + GUCCheckBooleanExplainOption); + + /* per node EXPLAIN hook */ + prev_explain_per_node_hook = explain_per_node_hook; + explain_per_node_hook = postgresExplainPerNode; + prev_explain_per_plan_hook = explain_per_plan_hook; + explain_per_plan_hook = postgresExplainPerPlan; + prev_explain_validate_options_hook = explain_validate_options_hook; + explain_validate_options_hook = postgresExplainValidateOptions; + } diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c index 6dbae58..8eeb1e6 100644 --- a/contrib/postgres_fdw/postgres_fdw.c +++ b/contrib/postgres_fdw/postgres_fdw.c @@ -137,6 +137,26 @@ enum FdwDirectModifyPrivateIndex FdwDirectModifyPrivateSetProcessed, }; +static const char *const explain_formats[] = { + [EXPLAIN_FORMAT_TEXT] = "TEXT", + [EXPLAIN_FORMAT_JSON] = "JSON", + [EXPLAIN_FORMAT_XML] = "XML", + [EXPLAIN_FORMAT_YAML] = "YAML", +}; + +/* + * Track the extension id in the backend. + */ +static int extension_id = -1; + +static int +get_extension_id(void) +{ + if (extension_id == -1) + extension_id = GetExplainExtensionId("postgres_fdw"); + return extension_id; +} + /* * Execution state of a foreign scan using postgres_fdw. */ @@ -3038,6 +3058,109 @@ postgresEndDirectModify(ForeignScanState *node) /* MemoryContext will be deleted automatically. */ } +static void +postgresExplainStatement(int plan_node_id, + ExplainState *es, + PgFdwExplainState * pgfdw_explain_state, + PGconn *conn, + char *sql) +{ + PGresult *volatile res = NULL; + StringInfoData explain_sql; + int remote_version = PQserverVersion(conn); + + /* + * REMOTE_PLANS relies on GENERIC_PLAN to explain the statements that + * postgres_fdw sends to the remote server: the deparsed SQL embeds $n + * parameter placeholders (for parameterized scans, and for INSERT/UPDATE/ + * DELETE), and a plain remote EXPLAIN would fail with "there is no + * parameter $1". GENERIC_PLAN plans such statements without bound values. + * It is only available on remote servers running v16 or later, so refuse + * to proceed against anything older rather than emit a confusing error. + */ + if (remote_version < 160000) + ereport(ERROR, + errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("EXPLAIN option REMOTE_PLANS requires a remote server version of 16 or later"), + errdetail("The remote server version is %d.", remote_version)); + + PG_TRY(); + { + int numrows, + i; + PgFdwExplainRemotePlans *explain = (PgFdwExplainRemotePlans *) palloc(sizeof(PgFdwExplainRemotePlans)); + + initStringInfo(&explain_sql); + initStringInfo(&explain->explain_plan); + + /* + * Build the remote EXPLAIN. GENERIC_PLAN is always forced on (see + * above). Of the remaining options we forward only the ones that + * describe the plan and that the user actually requested; the + * ANALYZE-only options (WAL, TIMING, SERIALIZE, IO, SUMMARY) are + * intentionally omitted because REMOTE_PLANS cannot be combined with + * ANALYZE. If the user requests an option that the remote server does + * not recognize, the remote EXPLAIN simply errors out with + * "unrecognized EXPLAIN option", which is a clear enough signal. + */ + appendStringInfoString(&explain_sql, "EXPLAIN (GENERIC_PLAN TRUE"); + appendStringInfo(&explain_sql, ", VERBOSE %s", es->verbose ? "TRUE" : "FALSE"); + appendStringInfo(&explain_sql, ", COSTS %s", es->costs ? "TRUE" : "FALSE"); + appendStringInfo(&explain_sql, ", SETTINGS %s", es->settings ? "TRUE" : "FALSE"); + appendStringInfo(&explain_sql, ", BUFFERS %s", es->buffers ? "TRUE" : "FALSE"); + appendStringInfo(&explain_sql, ", MEMORY %s", es->memory ? "TRUE" : "FALSE"); + + appendStringInfo(&explain_sql, ", FORMAT %s) %s", + explain_formats[es->format], sql); + + /* Run the query and collect the remote plan */ + res = pgfdw_exec_query(conn, explain_sql.data, NULL); + if (PQresultStatus(res) != PGRES_TUPLES_OK) + pgfdw_report_error(res, conn, explain_sql.data); + + numrows = PQntuples(res); + + for (i = 0; i < numrows; i++) + appendStringInfo(&explain->explain_plan, "%s\n", PQgetvalue(res, i, 0)); + + if (explain->explain_plan.len > 0 && explain->explain_plan.data[explain->explain_plan.len - 1] == '\n') + explain->explain_plan.data[--explain->explain_plan.len] = '\0'; + + explain->plan_node_id = plan_node_id; + pgfdw_explain_state->all_remote_plans = lappend(pgfdw_explain_state->all_remote_plans, explain); + } + PG_FINALLY(); + { + if (res) + PQclear(res); + + if (explain_sql.data) + pfree(explain_sql.data); + } + PG_END_TRY(); +} + +/* + * explain_remote_query + * Helper function to get connection and execute remote EXPLAIN + */ +static void +explain_remote_query(int plan_node_id, ExplainState *es, + PgFdwExplainState *pgfdw_explain_state, + Oid foreign_table_oid, char *sql) +{ + UserMapping *user; + PGconn *conn; + ForeignTable *table; + + table = GetForeignTable(foreign_table_oid); + user = GetUserMapping(GetUserId(), table->serverid); + conn = GetConnection(user, false, NULL); + + postgresExplainStatement(plan_node_id, es, pgfdw_explain_state, conn, sql); + ReleaseConnection(conn); +} + /* * postgresExplainForeignScan * Produce extra output for EXPLAIN of a ForeignScan on a foreign table @@ -3047,6 +3170,12 @@ postgresExplainForeignScan(ForeignScanState *node, ExplainState *es) { ForeignScan *plan = castNode(ForeignScan, node->ss.ps.plan); List *fdw_private = plan->fdw_private; + PgFdwExplainState *pgfdw_explain_state; + char *sql; + Oid foreign_table_oid = InvalidOid; + + if (node->ss.ss_currentRelation) + foreign_table_oid = RelationGetRelid(node->ss.ss_currentRelation); /* * Identify foreign scans that are really joins or upper relations. The @@ -3108,6 +3237,13 @@ postgresExplainForeignScan(ForeignScanState *node, ExplainState *es) Assert(rte->rtekind == RTE_RELATION); /* This logic should agree with explain.c's ExplainTargetRel */ relname = get_rel_name(rte->relid); + + /* + * Save first table OID for getting server connection + */ + if (!OidIsValid(foreign_table_oid)) + foreign_table_oid = rte->relid; + if (es->verbose) { char *namespace; @@ -3133,16 +3269,25 @@ postgresExplainForeignScan(ForeignScanState *node, ExplainState *es) ExplainPropertyText("Relations", relations.data, es); } + sql = strVal(list_nth(fdw_private, FdwScanPrivateSelectSql)); + /* * Add remote query, when VERBOSE option is specified. */ if (es->verbose) - { - char *sql; - - sql = strVal(list_nth(fdw_private, FdwScanPrivateSelectSql)); ExplainPropertyText("Remote SQL", sql, es); - } + + pgfdw_explain_state = GetExplainExtensionState(es, get_extension_id()); + + /* If we don't have a foreign table oid by now, something went wrong */ + Assert(foreign_table_oid); + + if (pgfdw_explain_state && pgfdw_explain_state->remote_plans) + explain_remote_query(node->ss.ps.plan->plan_node_id, + es, + pgfdw_explain_state, + foreign_table_oid, + sql); } /* @@ -3156,11 +3301,12 @@ postgresExplainForeignModify(ModifyTableState *mtstate, int subplan_index, ExplainState *es) { + char *sql = strVal(list_nth(fdw_private, + FdwModifyPrivateUpdateSql)); + PgFdwExplainState *pgfdw_explain_state; + if (es->verbose) { - char *sql = strVal(list_nth(fdw_private, - FdwModifyPrivateUpdateSql)); - ExplainPropertyText("Remote SQL", sql, es); /* @@ -3170,6 +3316,14 @@ postgresExplainForeignModify(ModifyTableState *mtstate, if (rinfo->ri_BatchSize > 0) ExplainPropertyInteger("Batch Size", NULL, rinfo->ri_BatchSize, es); } + + pgfdw_explain_state = GetExplainExtensionState(es, get_extension_id()); + if (pgfdw_explain_state && pgfdw_explain_state->remote_plans) + explain_remote_query(mtstate->ps.plan->plan_node_id, + es, + pgfdw_explain_state, + rinfo->ri_RelationDesc->rd_rel->oid, + sql); } /* @@ -3182,13 +3336,22 @@ postgresExplainDirectModify(ForeignScanState *node, ExplainState *es) { List *fdw_private; char *sql; + PgFdwExplainState *pgfdw_explain_state; + + fdw_private = ((ForeignScan *) node->ss.ps.plan)->fdw_private; + sql = strVal(list_nth(fdw_private, FdwDirectModifyPrivateUpdateSql)); if (es->verbose) - { - fdw_private = ((ForeignScan *) node->ss.ps.plan)->fdw_private; - sql = strVal(list_nth(fdw_private, FdwDirectModifyPrivateUpdateSql)); ExplainPropertyText("Remote SQL", sql, es); - } + + pgfdw_explain_state = GetExplainExtensionState(es, get_extension_id()); + + if (pgfdw_explain_state && pgfdw_explain_state->remote_plans) + explain_remote_query(node->ss.ps.plan->plan_node_id, + es, + pgfdw_explain_state, + RelationGetRelid(node->ss.ss_currentRelation), + sql); } /* @@ -8835,3 +8998,95 @@ get_batch_size_option(Relation rel) return batch_size; } + +void +postgresExplainPerNode(PlanState *planstate, List *ancestors, + const char *relationship, const char *plan_name, + ExplainState *es) +{ + PgFdwExplainState *pgfdw_explain_state; + + pgfdw_explain_state = GetExplainExtensionState(es, get_extension_id()); + + if (pgfdw_explain_state == NULL || + !pgfdw_explain_state->remote_plans) + return; + + if (pgfdw_explain_state && pgfdw_explain_state->remote_plans) + ExplainPropertyInteger("Plan Node ID", NULL, planstate->plan->plan_node_id, es); +} + +static void +pgfdwFormatRemotePlan(PgFdwExplainRemotePlans * explain, + ExplainState *es, + int plan_node_id) +{ + char *token; + StringInfoData remote_plan_name; + + initStringInfo(&remote_plan_name); + appendStringInfo(&remote_plan_name, "Plan Node ID %d", plan_node_id); + + ExplainOpenGroup(remote_plan_name.data, remote_plan_name.data, false, es); + + if (es->format == EXPLAIN_FORMAT_TEXT) + { + appendStringInfo(es->str, "Plan Node ID %d:", plan_node_id); + appendStringInfoString(es->str, "\n"); + } + + while ((token = strsep(&explain->explain_plan.data, "\n")) != NULL) + { + if (es->format == EXPLAIN_FORMAT_JSON || + es->format == EXPLAIN_FORMAT_YAML) + appendStringInfoString(es->str, "\n"); + + appendStringInfoSpaces(es->str, (es->indent == 0) ? 2 : es->indent * 2); + appendStringInfoString(es->str, token); + + if (es->format == EXPLAIN_FORMAT_XML || + es->format == EXPLAIN_FORMAT_TEXT) + appendStringInfoString(es->str, "\n"); + } + + ExplainCloseGroup(remote_plan_name.data, remote_plan_name.data, false, es); + pfree(remote_plan_name.data); +} + +void +postgresExplainPerPlan(PlannedStmt *plannedstmt, + IntoClause *into, + ExplainState *es, + const char *queryString, + ParamListInfo params, + QueryEnvironment *queryEnv) +{ + ListCell *lc; + PgFdwExplainState *pgfdw_explain_state; + + pgfdw_explain_state = GetExplainExtensionState(es, get_extension_id()); + + if (pgfdw_explain_state == NULL || + pgfdw_explain_state->all_remote_plans == NIL || + !pgfdw_explain_state->remote_plans) + return; + + ExplainOpenGroup("Remote Plans", "Remote Plans", true, es); + if (es->format == EXPLAIN_FORMAT_TEXT) + { + appendStringInfo(es->str, "Remote Plans:\n"); + appendStringInfo(es->str, "-------------\n"); + } + + /* Process every remote plan captured */ + foreach(lc, pgfdw_explain_state->all_remote_plans) + { + PgFdwExplainRemotePlans *explain = (PgFdwExplainRemotePlans *) lfirst(lc); + + pgfdwFormatRemotePlan(explain, + es, + explain->plan_node_id); + } + + ExplainCloseGroup("Remote Plans", "Remote Plans", true, es); +} diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h index a2bb1ff..cf74dba 100644 --- a/contrib/postgres_fdw/postgres_fdw.h +++ b/contrib/postgres_fdw/postgres_fdw.h @@ -13,6 +13,7 @@ #ifndef POSTGRES_FDW_H #define POSTGRES_FDW_H +#include "commands/explain_state.h" #include "foreign/foreign.h" #include "lib/stringinfo.h" #include "libpq/libpq-be-fe.h" @@ -151,6 +152,21 @@ typedef enum PgFdwSamplingMethod ANALYZE_SAMPLE_BERNOULLI, /* TABLESAMPLE bernoulli */ } PgFdwSamplingMethod; +typedef struct PgFdwExplainRemotePlans +{ + int plan_node_id; + StringInfoData explain_plan; + +} PgFdwExplainRemotePlans; + +typedef struct PgFdwExplainState +{ + List *all_remote_plans; + + /* EXPLAIN options */ + bool remote_plans; +} PgFdwExplainState; + /* in postgres_fdw.c */ extern int set_transmission_modes(void); extern void reset_transmission_modes(int nestlevel); @@ -178,6 +194,16 @@ extern int ExtractConnectionOptions(List *defelems, extern List *ExtractExtensionList(const char *extensionsString, bool warnOnMissing); extern char *process_pgfdw_appname(const char *appname); +extern void postgresExplainPerNode(PlanState *planstate, List *ancestors, + const char *relationship, + const char *plan_name, + ExplainState *es); +extern void postgresExplainPerPlan(PlannedStmt *plannedstmt, + IntoClause *into, + ExplainState *es, + const char *queryString, + ParamListInfo params, + QueryEnvironment *queryEnv); extern char *pgfdw_application_name; /* in deparse.c */ diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql index dfc58be..26a3f4f 100644 --- a/contrib/postgres_fdw/sql/postgres_fdw.sql +++ b/contrib/postgres_fdw/sql/postgres_fdw.sql @@ -311,6 +311,11 @@ 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; +-- with WHERE clause and remote_plans with different formats +EXPLAIN (REMOTE_PLANS, FORMAT YAML, VERBOSE, COSTS OFF) SELECT * FROM ft1 t1 WHERE t1.c1 = 101; +EXPLAIN (REMOTE_PLANS TRUE, FORMAT XML, VERBOSE, COSTS OFF) SELECT * FROM ft1 t1 WHERE t1.c1 = 101; +EXPLAIN (REMOTE_PLANS, FORMAT JSON, VERBOSE, COSTS OFF) SELECT * FROM ft1 t1 WHERE t1.c1 = 101; +EXPLAIN (REMOTE_PLANS TRUE, FORMAT TEXT, VERBOSE, COSTS OFF) SELECT * FROM ft1 t1 WHERE t1.c1 = 101; -- Test forcing the remote server to produce sorted data for a merge join. SET enable_hashjoin TO false; SET enable_nestloop TO false; @@ -338,6 +343,12 @@ SELECT t1."C 1", t2.c1, t3.c1 FROM "S 1"."T 1" t1 left join ft1 t2 full join ft2 EXPLAIN (VERBOSE, COSTS OFF) SELECT t1."C 1", t2.c1, t3.c1 FROM "S 1"."T 1" t1 full join ft1 t2 full join ft2 t3 on (t2.c1 = t3.c1) on (t3.c1 = t1."C 1") OFFSET 100 LIMIT 10; SELECT t1."C 1", t2.c1, t3.c1 FROM "S 1"."T 1" t1 full join ft1 t2 full join ft2 t3 on (t2.c1 = t3.c1) on (t3.c1 = t1."C 1") OFFSET 100 LIMIT 10; + +-- Join push-down test +-- Ensure join conditions are pushed down to the foreign server +EXPLAIN (REMOTE_PLANS, VERBOSE, COSTS OFF) + SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) WHERE t1.c2 = 10; + RESET enable_hashjoin; RESET enable_nestloop; @@ -379,6 +390,11 @@ EXPLAIN (VERBOSE, COSTS OFF) SELECT * FROM ft1 t1 WHERE c8 = 'foo'; -- can't be EXPLAIN (VERBOSE, COSTS OFF) SELECT * FROM "S 1"."T 1" a, ft2 b WHERE a."C 1" = 47 AND b.c1 = a.c2; SELECT * FROM "S 1"."T 1" a, ft2 b WHERE a."C 1" = 47 AND b.c1 = a.c2; +-- REMOTE_PLANS over a parameterized foreign scan: the deparsed remote SQL +-- carries a $1 placeholder, which only plans on the remote side because +-- REMOTE_PLANS forces GENERIC_PLAN. +EXPLAIN (REMOTE_PLANS, VERBOSE, COSTS OFF) + SELECT * FROM "S 1"."T 1" a, ft2 b WHERE a."C 1" = 47 AND b.c1 = a.c2; -- check both safe and unsafe join conditions EXPLAIN (VERBOSE, COSTS OFF) @@ -1527,6 +1543,33 @@ SELECT ft1.c1 FROM ft1 JOIN ft2 on ft1.c1 = ft2.c1 WHERE SELECT ft2.c1 FROM ft2 JOIN ft4 ON ft2.c1 = ft4.c1) ORDER BY ft1.c1 LIMIT 5; +-- EXPLAIN remote_plans +EXPLAIN (remote_plans, format text, costs off, analyze) +SELECT ft1.c1 FROM ft1 JOIN ft2 on ft1.c1 = ft2.c1 WHERE + ft1.c1 IN ( + SELECT ft2.c1 FROM ft2 JOIN ft4 ON ft2.c1 = ft4.c1) + ORDER BY ft1.c1 LIMIT 5; +EXPLAIN (remote_plans, format text, costs off) +SELECT ft1.c1 FROM ft1 JOIN ft2 on ft1.c1 = ft2.c1 WHERE + ft1.c1 IN ( + SELECT ft2.c1 FROM ft2 JOIN ft4 ON ft2.c1 = ft4.c1) + ORDER BY ft1.c1 LIMIT 5; +EXPLAIN (remote_plans, format xml, costs off) +SELECT ft1.c1 FROM ft1 JOIN ft2 on ft1.c1 = ft2.c1 WHERE + ft1.c1 IN ( + SELECT ft2.c1 FROM ft2 JOIN ft4 ON ft2.c1 = ft4.c1) + ORDER BY ft1.c1 LIMIT 5; +EXPLAIN (remote_plans, format json, costs off) +SELECT ft1.c1 FROM ft1 JOIN ft2 on ft1.c1 = ft2.c1 WHERE + ft1.c1 IN ( + SELECT ft2.c1 FROM ft2 JOIN ft4 ON ft2.c1 = ft4.c1) + ORDER BY ft1.c1 LIMIT 5; +EXPLAIN (remote_plans, format yaml, costs off) +SELECT ft1.c1 FROM ft1 JOIN ft2 on ft1.c1 = ft2.c1 WHERE + ft1.c1 IN ( + SELECT ft2.c1 FROM ft2 JOIN ft4 ON ft2.c1 = ft4.c1) + ORDER BY ft1.c1 LIMIT 5; + -- =================================================================== -- test writable foreign table stuff -- =================================================================== @@ -1587,6 +1630,10 @@ DELETE FROM ft8 FOR PORTION OF c4 FROM '2005-01-01' TO '2006-01-01' WHERE c1 = '[2,3)'; SELECT * FROM ft8 WHERE c1 = '[2,3)' ORDER BY c1, c4; +-- test write on foreign tables with remote_plans +EXPLAIN (remote_plans, verbose, costs off) +UPDATE ft2 SET c2 = c2 + 300 WHERE c1 % 10 = 3; + -- Test UPDATE/DELETE with RETURNING on a three-table join INSERT INTO ft2 (c1,c2,c3) SELECT id, id - 1200, to_char(id, 'FM00000') FROM generate_series(1201, 1300) id; @@ -4194,6 +4241,9 @@ INSERT INTO insert_tbl (SELECT * FROM local_tbl UNION ALL SELECT * FROM remote_t SELECT * FROM insert_tbl ORDER BY a; +EXPLAIN (REMOTE_PLANS, VERBOSE, COSTS OFF) +INSERT INTO insert_tbl (SELECT * FROM local_tbl UNION ALL SELECT * FROM remote_tbl); + -- Check with direct modify EXPLAIN (VERBOSE, COSTS OFF) WITH t AS (UPDATE remote_tbl SET c = c || c RETURNING *) diff --git a/doc/src/sgml/postgres-fdw.sgml b/doc/src/sgml/postgres-fdw.sgml index b9e1b04..d36a9f9 100644 --- a/doc/src/sgml/postgres-fdw.sgml +++ b/doc/src/sgml/postgres-fdw.sgml @@ -1197,6 +1197,47 @@ CREATE SUBSCRIPTION my_subscription SERVER subscription_server PUBLICATION testp The query that is actually sent to the remote server for execution can be examined using EXPLAIN VERBOSE. + + + In addition to the remote query itself, the plan that the remote server + chooses for that query can be examined with the + REMOTE_PLANS option of EXPLAIN, for + example EXPLAIN (VERBOSE, REMOTE_PLANS) SELECT .... For + every foreign scan or foreign modification in the local plan, + postgres_fdw runs EXPLAIN on the + remote server for the corresponding remote query and prints the result in a + Remote Plans section, keyed by the plan node id of the + local node. + + + + The REMOTE_PLANS option is registered when + postgres_fdw is loaded, so the module must be loaded + (for example with CREATE EXTENSION postgres_fdw) before + the option can be used. The following restrictions apply: + + + + It cannot be combined with ANALYZE; doing so raises an + error. The remote query is only planned, never executed, so the foreign + server is not affected and no remote rows are fetched. + + + + + The remote server must be running PostgreSQL + 16 or later. The remote query that postgres_fdw + sends may contain parameter placeholders (for parameterized foreign + scans, and for INSERT/UPDATE/DELETE), + so the option relies on the GENERIC_PLAN option of + EXPLAIN, which was introduced in that release. As a + consequence, the remote plans shown are generic plans that treat those + placeholders as unknown values rather than plans for specific parameter + values. + + + + diff --git a/src/include/commands/explain_state.h b/src/include/commands/explain_state.h index 97bc7ed..ba09c1d 100644 --- a/src/include/commands/explain_state.h +++ b/src/include/commands/explain_state.h @@ -45,7 +45,7 @@ typedef struct ExplainWorkersState typedef struct ExplainState { StringInfo str; /* output buffer */ - /* options */ + /* options. considering updating logic to create explain_sql in postgres_fdw extension if adding new option here.*/ bool verbose; /* be verbose */ bool analyze; /* print actual times */ bool costs; /* print estimated costs */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index f9eb23e..c7b1140 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -2304,6 +2304,8 @@ PgChecksumMode PgFdwAnalyzeState PgFdwConnState PgFdwDirectModifyState +PgFdwExplainRemotePlans +PgFdwExplainState PgFdwModifyState PgFdwOption PgFdwPathExtraData base-commit: 2963ddeef2be6d6e064cb9d382f67dcbf2c049a8 -- 2.50.1 (Apple Git-155)