Hi,

Attached patch add a new GUC parameter auto_explain.log_options that
accepts a comma-separated list of custom EXPLAIN options registered by
extensions. This allows auto_explain to pass extension specific options
(like from pg_plan_advice) when logging query plans. 

Based on pg_plan_advice and pg_overexplain use of custom explain options
I see two different cases:

1. pg_plan_advice check at planner_setup_hook() if custom explain option
is enabled or not to decide if the advice should be collected.

2. Since pg_overexplain don't collect any other data (just print more
planner information) it only check at explain_per_plan_hook() if the
custom explain option is enabled or not.

So it seems to me that we have two patterns here: 1. Custom extensions
that want to include more information during planning so in this case it
should use the planner_setup_hook() and 2. which are extensions that
don't need any extra planner information and can just hook
explain_per_plan_hook().

That being said, this patch creates a new planner_setup_hook for case
1 and changes explain_ExecutorEnd() to call explain_per_plan_hook()
for case 2. Note that even for case 1, we still need to call
explain_per_plan_hook() so the extra information from the custom
explain option is included in the explain output.

Regarding the explain_per_plan_hook() call in explain_ExecutorEnd():
normally this hook is called by ExplainOnePlan() during a regular
EXPLAIN command. However, auto_explain doesn't go through
ExplainOnePlan() - it builds its own ExplainState and calls the
individual explain functions (ExplainPrintPlan, ExplainPrintTriggers,
ExplainPrintJITSummary) directly. We can't use ExplainOnePlan()
because it expects to execute the query itself, whereas auto_explain
runs after execution is already complete (inside the ExecutorEnd hook)
and already has a QueryDesc with execution results.

Since there's no existing helper function that handles just the
"output explain for an already-executed query" portion while also
calling explain_per_plan_hook(), the only option currently is to call
explain_per_plan_hook() directly. I'm wondering if we should create
such a helper function, or if there's a better approach here?

--
Matheus Alcantara
EDB: https://www.enterprisedb.com
From d493f789d50637574d186802ebe5937e2659341a Mon Sep 17 00:00:00 2001
From: Matheus Alcantara <[email protected]>
Date: Thu, 26 Mar 2026 12:54:15 -0300
Subject: [PATCH v1] Add custom EXPLAIN options support to auto_explain

Add a new GUC parameter auto_explain.log_options that accepts a
comma-separated list of custom EXPLAIN options registered by extensions.
This allows auto_explain to pass extension specific options (like those
from pg_plan_advice) when logging query plans.

Extensions that register custom EXPLAIN options follow two patterns:

1. Some extensions (e.g., pg_plan_advice) check during planning whether
   their custom option is enabled, to decide if extra information should
   be collected. To support this, a new planner_setup_hook is added that
   creates an ExplainState with the configured options.

2. Other extensions (e.g., pg_overexplain) only check at output time
   whether their option is enabled, using explain_per_plan_hook.

Normally, explain_per_plan_hook is called by ExplainOnePlan() during a
regular EXPLAIN command. However, auto_explain generates explain output
manually at ExecutorEnd by calling ExplainPrintPlan and related
functions directly, since ExplainOnePlan expects to execute the query
itself. Therefore, this commit adds an explicit call to
explain_per_plan_hook in explain_ExecutorEnd to allow extensions to
include their output.
---
 contrib/auto_explain/auto_explain.c | 157 ++++++++++++++++++++++++++++
 doc/src/sgml/auto-explain.sgml      |  19 ++++
 2 files changed, 176 insertions(+)

diff --git a/contrib/auto_explain/auto_explain.c 
b/contrib/auto_explain/auto_explain.c
index e856cd35a6f..ff794394306 100644
--- a/contrib/auto_explain/auto_explain.c
+++ b/contrib/auto_explain/auto_explain.c
@@ -20,7 +20,11 @@
 #include "commands/explain_state.h"
 #include "common/pg_prng.h"
 #include "executor/instrument.h"
+#include "nodes/makefuncs.h"
+#include "optimizer/planner.h"
+#include "tcop/tcopprot.h"
 #include "utils/guc.h"
+#include "utils/varlena.h"
 
 PG_MODULE_MAGIC_EXT(
                                        .name = "auto_explain",
@@ -41,6 +45,7 @@ static int    auto_explain_log_format = EXPLAIN_FORMAT_TEXT;
 static int     auto_explain_log_level = LOG;
 static bool auto_explain_log_nested_statements = false;
 static double auto_explain_sample_rate = 1;
+static char *auto_explain_log_options = NULL;
 
 static const struct config_enum_entry format_options[] = {
        {"text", EXPLAIN_FORMAT_TEXT, false},
@@ -76,11 +81,15 @@ static bool current_query_sampled = false;
         current_query_sampled)
 
 /* Saved hook values */
+static planner_setup_hook_type prev_planner_setup = NULL;
 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 void explain_planner_setup(PlannerGlobal *glob, Query *parse,
+                                                                 const char 
*query_string, int cursorOptions,
+                                                                 double 
*tuple_fraction, ExplainState *es);
 static void explain_ExecutorStart(QueryDesc *queryDesc, int eflags);
 static void explain_ExecutorRun(QueryDesc *queryDesc,
                                                                ScanDirection 
direction,
@@ -88,6 +97,125 @@ static void explain_ExecutorRun(QueryDesc *queryDesc,
 static void explain_ExecutorFinish(QueryDesc *queryDesc);
 static void explain_ExecutorEnd(QueryDesc *queryDesc);
 
+/*
+ * Apply custom EXPLAIN options from auto_explain.log_options to the
+ * ExplainState.
+ *
+ * This parses the comma-separated option list and calls
+ * ApplyExtensionExplainOption for each one.
+ */
+static void
+apply_custom_options(ExplainState *es)
+{
+       char       *options;
+       List       *elemlist;
+       ListCell   *lc;
+
+       if (auto_explain_log_options == NULL || auto_explain_log_options[0] == 
'\0')
+               return;
+
+       options = pstrdup(auto_explain_log_options);
+
+       if (!SplitIdentifierString(options, ',', &elemlist))
+       {
+               /* Shouldn't happen since check_explain_options validated this 
*/
+               Assert(false);
+               pfree(options);
+               return;
+       }
+
+       foreach(lc, elemlist)
+       {
+               const char *option = (const char *) lfirst(lc);
+               DefElem    *def;
+
+               /*
+                * Create a DefElem for this option. Pass NULL as the argument, 
which
+                * for boolean options means "true".
+                */
+               def = makeDefElem(pstrdup(option), NULL, -1);
+
+               /*
+                * Apply the option. If the extension that registered this 
option is
+                * not loaded, ApplyExtensionExplainOption will return false, 
which we
+                * silently ignore. This allows the GUC to be set even if the
+                * extension providing the option isn't currently loaded.
+                *
+                * XXX: ParseState is not used by pg_plan_advice and 
pg_overexplain.
+                * Using NULL is a problem for future extensions?
+                */
+               ApplyExtensionExplainOption(es, def, NULL);
+       }
+
+       pfree(options);
+       list_free(elemlist);
+}
+
+/* GUC check hook for auto_explain.log_options. */
+static bool
+check_explain_options(char **newval, void **extra, GucSource source)
+{
+       char       *options;
+       List       *elemlist;
+
+       if (*newval == NULL || (*newval)[0] == '\0')
+               return true;
+
+       /* Make a modifiable copy */
+       options = pstrdup(*newval);
+
+       if (!SplitIdentifierString(options, ',', &elemlist))
+       {
+               GUC_check_errdetail("Invalid syntax in option list.");
+               pfree(options);
+               list_free(elemlist);
+               return false;
+       }
+
+       pfree(options);
+       list_free(elemlist);
+       return true;
+}
+
+/*
+ * Planner setup hook: pass custom EXPLAIN options to planner extensions.
+ *
+ * Extensions like pg_plan_advice need to know about custom EXPLAIN options
+ * during planning so they can generate data that will be displayed later.
+ * When auto_explain.log_options is configured and auto_explain is potentially
+ * active, we create an ExplainState with those options and pass it to the
+ * planner hook chain.
+ */
+static void
+explain_planner_setup(PlannerGlobal *glob, Query *parse,
+                                         const char *query_string, int 
cursorOptions,
+                                         double *tuple_fraction, ExplainState 
*es)
+{
+       /*
+        * If auto_explain is potentially active (log_min_duration >= 0) and we
+        * have custom options configured, create an ExplainState with those
+        * options applied. This signals to extensions like pg_plan_advice that
+        * they should generate data for these options.
+        *
+        * We do this even if the caller already provided an ExplainState (i.e.,
+        * we're inside an EXPLAIN command), because our options might differ.
+        * However, if an ExplainState is already provided, extensions will see
+        * that one first, so we only create ours if es is NULL.
+        */
+       if (auto_explain_log_min_duration >= 0 &&
+               auto_explain_log_options != NULL &&
+               auto_explain_log_options[0] != '\0')
+       {
+               if (es == NULL)
+                       es = NewExplainState();
+
+               apply_custom_options(es);
+       }
+
+       if (prev_planner_setup)
+               prev_planner_setup(glob, parse, query_string, cursorOptions,
+                                                  tuple_fraction, es);
+}
 
 /*
  * Module load callback
@@ -245,9 +373,22 @@ _PG_init(void)
                                                         NULL,
                                                         NULL);
 
+       DefineCustomStringVariable("auto_explain.log_options",
+                                                          "Custom EXPLAIN 
options to include in plan logging.",
+                                                          "Comma-separated 
list of custom EXPLAIN options registered by extensions. ",
+                                                          
&auto_explain_log_options,
+                                                          NULL,
+                                                          PGC_SUSET,
+                                                          GUC_LIST_INPUT,
+                                                          
check_explain_options,
+                                                          NULL,
+                                                          NULL);
+
        MarkGUCPrefixReserved("auto_explain");
 
        /* Install hooks. */
+       prev_planner_setup = planner_setup_hook;
+       planner_setup_hook = explain_planner_setup;
        prev_ExecutorStart = ExecutorStart_hook;
        ExecutorStart_hook = explain_ExecutorStart;
        prev_ExecutorRun = ExecutorRun_hook;
@@ -404,6 +545,9 @@ explain_ExecutorEnd(QueryDesc *queryDesc)
                        es->format = auto_explain_log_format;
                        es->settings = auto_explain_log_settings;
 
+                       /* Apply any custom EXPLAIN options */
+                       apply_custom_options(es);
+
                        ExplainBeginOutput(es);
                        ExplainQueryText(es, queryDesc);
                        ExplainQueryParameters(es, queryDesc->params, 
auto_explain_log_parameter_max_length);
@@ -412,6 +556,19 @@ explain_ExecutorEnd(QueryDesc *queryDesc)
                                ExplainPrintTriggers(es, queryDesc);
                        if (es->costs)
                                ExplainPrintJITSummary(es, queryDesc);
+
+                       /*
+                        * Allow plugins to print additional EXPLAIN 
information. This
+                        * mirrors what ExplainOnePlan does, allowing 
extensions that use
+                        * explain_per_plan_hook to add their output.
+                        */
+                       if (explain_per_plan_hook)
+                               (*explain_per_plan_hook) 
(queryDesc->plannedstmt,
+                                                                               
  NULL, /* into */
+                                                                               
  es,
+                                                                               
  debug_query_string,
+                                                                               
  queryDesc->params,
+                                                                               
  queryDesc->estate->es_queryEnv);
                        ExplainEndOutput(es);
 
                        /* Remove last line break */
diff --git a/doc/src/sgml/auto-explain.sgml b/doc/src/sgml/auto-explain.sgml
index 15c868021e6..91908161e09 100644
--- a/doc/src/sgml/auto-explain.sgml
+++ b/doc/src/sgml/auto-explain.sgml
@@ -299,6 +299,25 @@ LOAD 'auto_explain';
      </para>
     </listitem>
    </varlistentry>
+
+   <varlistentry id="auto-explain-configuration-parameters-log-options">
+    <term>
+     <varname>auto_explain.log_options</varname> (<type>string</type>)
+     <indexterm>
+      <primary><varname>auto_explain.log_options</varname> configuration 
parameter</primary>
+     </indexterm>
+    </term>
+    <listitem>
+     <para>
+      <varname>auto_explain.log_options</varname> specifies a comma-separated 
list
+      of custom <command>EXPLAIN</command> options registered by extensions.
+      This allows <filename>auto_explain</filename> to include output from
+      extension-provided <command>EXPLAIN</command> options when logging query
+      plans.  The default is an empty string, meaning no custom options are
+      enabled. Only superusers can change this setting.
+     </para>
+    </listitem>
+   </varlistentry>
   </variablelist>
 
   <para>
-- 
2.52.0

Reply via email to