]> git.ipfire.org Git - thirdparty/postgresql.git/commitdiff
auto_explain: Add new GUC, auto_explain.log_extension_options.
authorRobert Haas <rhaas@postgresql.org>
Mon, 6 Apr 2026 19:09:24 +0000 (15:09 -0400)
committerRobert Haas <rhaas@postgresql.org>
Mon, 6 Apr 2026 19:19:42 +0000 (15:19 -0400)
The associated value should look like something that could be
part of an EXPLAIN options list, but restricted to EXPLAIN options
added by extensions.

For example, if pg_overexplain is loaded, you could set
auto_explain.log_extension_options = 'DEBUG, RANGE_TABLE'.
You can also specify arguments to these options in the same manner
as normal e.g. 'DEBUG 1, RANGE_TABLE false'.

Reviewed-by: Matheus Alcantara <matheusssilv97@gmail.com>
Reviewed-by: Lukas Fittl <lukas@fittl.com>
Discussion: http://postgr.es/m/CA+Tgmob-0W8306mvrJX5Urtqt1AAasu8pi4yLrZ1XfwZU-Uj1w@mail.gmail.com

contrib/auto_explain/Makefile
contrib/auto_explain/auto_explain.c
contrib/auto_explain/expected/extension_options.out [new file with mode: 0644]
contrib/auto_explain/meson.build
contrib/auto_explain/sql/extension_options.sql [new file with mode: 0644]
contrib/auto_explain/t/001_auto_explain.pl
doc/src/sgml/auto-explain.sgml
src/tools/pgindent/typedefs.list

index 94ab28e7c06b91f06ce295fb1dbc96a84ab592f8..1f608b1d733623db32f9bf12a015502dd5e46926 100644 (file)
@@ -6,7 +6,8 @@ OBJS = \
        auto_explain.o
 PGFILEDESC = "auto_explain - logging facility for execution plans"
 
-REGRESS = alter_reset
+EXTRA_INSTALL = contrib/pg_overexplain
+REGRESS = alter_reset extension_options
 
 TAP_TESTS = 1
 
index 39bf2543b701dc7efd094f63ffbb9d3a9f79d36a..6ceae1c69ce01d2ae37b8a189735c1b4979a7db1 100644 (file)
 #include <limits.h>
 
 #include "access/parallel.h"
+#include "commands/defrem.h"
 #include "commands/explain.h"
 #include "commands/explain_format.h"
 #include "commands/explain_state.h"
 #include "common/pg_prng.h"
 #include "executor/instrument.h"
+#include "nodes/makefuncs.h"
+#include "nodes/value.h"
+#include "parser/scansup.h"
 #include "utils/guc.h"
+#include "utils/varlena.h"
 
 PG_MODULE_MAGIC_EXT(
                                        .name = "auto_explain",
@@ -41,6 +46,31 @@ 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_extension_options = NULL;
+
+/*
+ * Parsed form of one option from auto_explain.log_extension_options.
+ */
+typedef struct auto_explain_option
+{
+       char       *name;
+       char       *value;
+       NodeTag         type;
+} auto_explain_option;
+
+/*
+ * Parsed form of the entirety of auto_explain.log_extension_options, stored
+ * as GUC extra. The options[] array will have pointers into the string
+ * following the array.
+ */
+typedef struct auto_explain_extension_options
+{
+       int                     noptions;
+       auto_explain_option options[FLEXIBLE_ARRAY_MEMBER];
+       /* a null-terminated copy of the GUC string follows the array */
+} auto_explain_extension_options;
+
+static auto_explain_extension_options *extension_options = NULL;
 
 static const struct config_enum_entry format_options[] = {
        {"text", EXPLAIN_FORMAT_TEXT, false},
@@ -88,6 +118,15 @@ static void explain_ExecutorRun(QueryDesc *queryDesc,
 static void explain_ExecutorFinish(QueryDesc *queryDesc);
 static void explain_ExecutorEnd(QueryDesc *queryDesc);
 
+static bool check_log_extension_options(char **newval, void **extra,
+                                                                               GucSource source);
+static void assign_log_extension_options(const char *newval, void *extra);
+static void apply_extension_options(ExplainState *es,
+                                                                       auto_explain_extension_options *ext);
+static char *auto_explain_scan_literal(char **endp, char **nextp);
+static int     auto_explain_split_options(char *rawstring,
+                                                                          auto_explain_option *options,
+                                                                          int maxoptions, char **errmsg);
 
 /*
  * Module load callback
@@ -232,6 +271,17 @@ _PG_init(void)
                                                         NULL,
                                                         NULL);
 
+       DefineCustomStringVariable("auto_explain.log_extension_options",
+                                                          "Extension EXPLAIN options to be added.",
+                                                          NULL,
+                                                          &auto_explain_log_extension_options,
+                                                          NULL,
+                                                          PGC_SUSET,
+                                                          0,
+                                                          check_log_extension_options,
+                                                          assign_log_extension_options,
+                                                          NULL);
+
        DefineCustomRealVariable("auto_explain.sample_rate",
                                                         "Fraction of queries to process.",
                                                         NULL,
@@ -398,6 +448,8 @@ explain_ExecutorEnd(QueryDesc *queryDesc)
                        es->format = auto_explain_log_format;
                        es->settings = auto_explain_log_settings;
 
+                       apply_extension_options(es, extension_options);
+
                        ExplainBeginOutput(es);
                        ExplainQueryText(es, queryDesc);
                        ExplainQueryParameters(es, queryDesc->params, auto_explain_log_parameter_max_length);
@@ -406,6 +458,12 @@ explain_ExecutorEnd(QueryDesc *queryDesc)
                                ExplainPrintTriggers(es, queryDesc);
                        if (es->costs)
                                ExplainPrintJITSummary(es, queryDesc);
+                       if (explain_per_plan_hook)
+                               (*explain_per_plan_hook) (queryDesc->plannedstmt,
+                                                                                 NULL, es,
+                                                                                 queryDesc->sourceText,
+                                                                                 queryDesc->params,
+                                                                                 queryDesc->estate->es_queryEnv);
                        ExplainEndOutput(es);
 
                        /* Remove last line break */
@@ -439,3 +497,332 @@ explain_ExecutorEnd(QueryDesc *queryDesc)
        else
                standard_ExecutorEnd(queryDesc);
 }
+
+/*
+ * GUC check hook for auto_explain.log_extension_options.
+ */
+static bool
+check_log_extension_options(char **newval, void **extra, GucSource source)
+{
+       char       *rawstring;
+       auto_explain_extension_options *result;
+       auto_explain_option *options;
+       int                     maxoptions = 8;
+       Size            rawstring_len;
+       Size            allocsize;
+       char       *errmsg;
+
+       /* NULL or empty string means no options. */
+       if (*newval == NULL || (*newval)[0] == '\0')
+       {
+               *extra = NULL;
+               return true;
+       }
+
+       rawstring_len = strlen(*newval) + 1;
+
+retry:
+       /* Try to allocate an auto_explain_extension_options object. */
+       allocsize = offsetof(auto_explain_extension_options, options) +
+               sizeof(auto_explain_option) * maxoptions +
+               rawstring_len;
+       result = (auto_explain_extension_options *) guc_malloc(LOG, allocsize);
+       if (result == NULL)
+               return false;
+
+       /* Copy the string after the options array. */
+       rawstring = (char *) &result->options[maxoptions];
+       memcpy(rawstring, *newval, rawstring_len);
+
+       /* Parse. */
+       options = result->options;
+       result->noptions = auto_explain_split_options(rawstring, options,
+                                                                                                 maxoptions, &errmsg);
+       if (result->noptions < 0)
+       {
+               GUC_check_errdetail("%s", errmsg);
+               guc_free(result);
+               return false;
+       }
+
+       /*
+        * Retry with a larger array if needed.
+        *
+        * It should be impossible for this to loop more than once, because
+        * auto_explain_split_options tells us how many entries are needed.
+        */
+       if (result->noptions > maxoptions)
+       {
+               maxoptions = result->noptions;
+               guc_free(result);
+               goto retry;
+       }
+
+       /* Validate each option against its registered check handler. */
+       for (int i = 0; i < result->noptions; i++)
+       {
+               if (!GUCCheckExplainExtensionOption(options[i].name, options[i].value,
+                                                                                       options[i].type))
+               {
+                       guc_free(result);
+                       return false;
+               }
+       }
+
+       *extra = result;
+       return true;
+}
+
+/*
+ * GUC assign hook for auto_explain.log_extension_options.
+ */
+static void
+assign_log_extension_options(const char *newval, void *extra)
+{
+       extension_options = (auto_explain_extension_options *) extra;
+}
+
+/*
+ * Apply parsed extension options to an ExplainState.
+ */
+static void
+apply_extension_options(ExplainState *es, auto_explain_extension_options *ext)
+{
+       if (ext == NULL)
+               return;
+
+       for (int i = 0; i < ext->noptions; i++)
+       {
+               auto_explain_option *opt = &ext->options[i];
+               DefElem    *def;
+               Node       *arg;
+
+               if (opt->value == NULL)
+                       arg = NULL;
+               else if (opt->type == T_Integer)
+                       arg = (Node *) makeInteger(strtol(opt->value, NULL, 0));
+               else if (opt->type == T_Float)
+                       arg = (Node *) makeFloat(opt->value);
+               else
+                       arg = (Node *) makeString(opt->value);
+
+               def = makeDefElem(opt->name, arg, -1);
+               ApplyExtensionExplainOption(es, def, NULL);
+       }
+}
+
+/*
+ * auto_explain_scan_literal - In-place scanner for single-quoted string
+ * literals.
+ *
+ * This is the single-quote analog of scan_quoted_identifier from varlena.c.
+ */
+static char *
+auto_explain_scan_literal(char **endp, char **nextp)
+{
+       char       *token = *nextp + 1;
+
+       for (;;)
+       {
+               *endp = strchr(*nextp + 1, '\'');
+               if (*endp == NULL)
+                       return NULL;            /* mismatched quotes */
+               if ((*endp)[1] != '\'')
+                       break;                          /* found end of literal */
+               /* Collapse adjacent quotes into one quote, and look again */
+               memmove(*endp, *endp + 1, strlen(*endp));
+               *nextp = *endp;
+       }
+       /* *endp now points at the terminating quote */
+       *nextp = *endp + 1;
+
+       return token;
+}
+
+/*
+ * auto_explain_split_options - Parse an option string into an array of
+ * auto_explain_option structs.
+ *
+ * Much of this logic is similar to SplitIdentifierString and friends, but our
+ * needs are different enough that we roll our own parsing logic. The goal here
+ * is to accept the same syntax that the main parser would accept inside of
+ * an EXPLAIN option list. While we can't do that perfectly without adding a
+ * lot more code, the goal of this implementation is to be close enough that
+ * users don't really notice the differences.
+ *
+ * The input string is modified in place (null-terminated, downcased, quotes
+ * collapsed).  All name and value pointers in the output array refer into
+ * this string, so the caller must ensure the string outlives the array.
+ *
+ * Returns the full number of options in the input string, but stores no
+ * more than maxoptions into the caller-provided array. If a syntax error
+ * occurs, returns -1 and sets *errmsg.
+ */
+static int
+auto_explain_split_options(char *rawstring, auto_explain_option *options,
+                                                  int maxoptions, char **errmsg)
+{
+       char       *nextp = rawstring;
+       int                     noptions = 0;
+       bool            done = false;
+
+       *errmsg = NULL;
+
+       while (scanner_isspace(*nextp))
+               nextp++;                                /* skip leading whitespace */
+
+       if (*nextp == '\0')
+               return 0;                               /* empty string is fine */
+
+       while (!done)
+       {
+               char       *name;
+               char       *name_endp;
+               char       *value = NULL;
+               char       *value_endp = NULL;
+               NodeTag         type = T_Invalid;
+
+               /* Parse the option name. */
+               name = scan_identifier(&name_endp, &nextp, ',', true);
+               if (name == NULL || name_endp == name)
+               {
+                       *errmsg = "option name missing or empty";
+                       return -1;
+               }
+
+               /* Skip whitespace after the option name. */
+               while (scanner_isspace(*nextp))
+                       nextp++;
+
+               /*
+                * Determine whether we have an option value.  A comma or end of
+                * string means no value; otherwise we have one.
+                */
+               if (*nextp != '\0' && *nextp != ',')
+               {
+                       if (*nextp == '\'')
+                       {
+                               /* Single-quoted string literal. */
+                               type = T_String;
+                               value = auto_explain_scan_literal(&value_endp, &nextp);
+                               if (value == NULL)
+                               {
+                                       *errmsg = "unterminated single-quoted string";
+                                       return -1;
+                               }
+                       }
+                       else if (isdigit((unsigned char) *nextp) ||
+                                        ((*nextp == '+' || *nextp == '-') &&
+                                         isdigit((unsigned char) nextp[1])))
+                       {
+                               char       *endptr;
+                               long            intval;
+                               char            saved;
+
+                               /* Remember the start of the next token, and find the end. */
+                               value = nextp;
+                               while (*nextp && *nextp != ',' && !scanner_isspace(*nextp))
+                                       nextp++;
+                               value_endp = nextp;
+
+                               /* Temporarily '\0'-terminate so we can use strtol/strtod. */
+                               saved = *value_endp;
+                               *value_endp = '\0';
+
+                               /*
+                                * Integer, float, or neither?
+                                *
+                                * NB: Since we use strtol and strtod here rather than
+                                * pg_strtoint64_safe, some syntax that would be accepted by
+                                * the main parser is not accepted here, e.g. 100_000. On the
+                                * plus side, strtol and strtod won't allocate, and
+                                * pg_strtoint64_safe might. For now, it seems better to keep
+                                * things simple here.
+                                */
+                               errno = 0;
+                               intval = strtol(value, &endptr, 0);
+                               if (errno == 0 && *endptr == '\0' && endptr != value &&
+                                       intval == (int) intval)
+                                       type = T_Integer;
+                               else
+                               {
+                                       type = T_Float;
+                                       (void) strtod(value, &endptr);
+                                       if (*endptr != '\0')
+                                       {
+                                               *value_endp = saved;
+                                               *errmsg = "invalid numeric value";
+                                               return -1;
+                                       }
+                               }
+
+                               /* Remove temporary terminator. */
+                               *value_endp = saved;
+                       }
+                       else
+                       {
+                               /* Identifier, possibly double-quoted. */
+                               type = T_String;
+                               value = scan_identifier(&value_endp, &nextp, ',', true);
+                               if (value == NULL)
+                               {
+                                       /*
+                                        * scan_identifier will return NULL if it finds an
+                                        * unterminated double-quoted identifier or it finds no
+                                        * identifier at all because the next character is
+                                        * whitespace or the separator character, here a comma.
+                                        * But the latter case is impossible here because the code
+                                        * above has skipped whitespace and checked for commas.
+                                        */
+                                       *errmsg = "unterminated double-quoted string";
+                                       return -1;
+                               }
+                       }
+               }
+
+               /* Skip trailing whitespace. */
+               while (scanner_isspace(*nextp))
+                       nextp++;
+
+               /* Expect comma or end of string. */
+               if (*nextp == ',')
+               {
+                       nextp++;
+                       while (scanner_isspace(*nextp))
+                               nextp++;
+                       if (*nextp == '\0')
+                       {
+                               *errmsg = "trailing comma in option list";
+                               return -1;
+                       }
+               }
+               else if (*nextp == '\0')
+                       done = true;
+               else
+               {
+                       *errmsg = "expected comma or end of option list";
+                       return -1;
+               }
+
+               /*
+                * Now safe to null-terminate the name and value.  We couldn't do this
+                * earlier because in the unquoted case, the null terminator position
+                * may coincide with a character that the scanning logic above still
+                * needed to read.
+                */
+               *name_endp = '\0';
+               if (value_endp != NULL)
+                       *value_endp = '\0';
+
+               /* Always count this option, and store the details if there is room. */
+               if (noptions < maxoptions)
+               {
+                       options[noptions].name = name;
+                       options[noptions].type = type;
+                       options[noptions].value = value;
+               }
+               noptions++;
+       }
+
+       return noptions;
+}
diff --git a/contrib/auto_explain/expected/extension_options.out b/contrib/auto_explain/expected/extension_options.out
new file mode 100644 (file)
index 0000000..b5a6677
--- /dev/null
@@ -0,0 +1,49 @@
+--
+-- Tests for auto_explain.log_extension_options.
+--
+LOAD 'auto_explain';
+LOAD 'pg_overexplain';
+-- Various legal values with assorted quoting and whitespace choices.
+SET auto_explain.log_extension_options = '';
+SET auto_explain.log_extension_options = 'debug, RANGE_TABLE';
+SET auto_explain.log_extension_options = 'debug TRUE  ';
+SET auto_explain.log_extension_options = '   debug 1,RAnge_table "off"';
+SET auto_explain.log_extension_options = $$"debug" tRuE, range_table 'false'$$;
+-- Syntax errors.
+SET auto_explain.log_extension_options = ',';
+ERROR:  invalid value for parameter "auto_explain.log_extension_options": ","
+DETAIL:  option name missing or empty
+SET auto_explain.log_extension_options = ', range_table';
+ERROR:  invalid value for parameter "auto_explain.log_extension_options": ", range_table"
+DETAIL:  option name missing or empty
+SET auto_explain.log_extension_options = 'range_table, ';
+ERROR:  invalid value for parameter "auto_explain.log_extension_options": "range_table, "
+DETAIL:  trailing comma in option list
+SET auto_explain.log_extension_options = 'range_table true false';
+ERROR:  invalid value for parameter "auto_explain.log_extension_options": "range_table true false"
+DETAIL:  expected comma or end of option list
+SET auto_explain.log_extension_options = '"range_table';
+ERROR:  invalid value for parameter "auto_explain.log_extension_options": ""range_table"
+DETAIL:  option name missing or empty
+SET auto_explain.log_extension_options = 'range_table 3.1415nine';
+ERROR:  invalid value for parameter "auto_explain.log_extension_options": "range_table 3.1415nine"
+DETAIL:  invalid numeric value
+SET auto_explain.log_extension_options = 'range_table "true';
+ERROR:  invalid value for parameter "auto_explain.log_extension_options": "range_table "true"
+DETAIL:  unterminated double-quoted string
+SET auto_explain.log_extension_options = $$range_table 'true$$;
+ERROR:  invalid value for parameter "auto_explain.log_extension_options": "range_table 'true"
+DETAIL:  unterminated single-quoted string
+SET auto_explain.log_extension_options = $$'$$;
+ERROR:  unrecognized EXPLAIN option "'"
+-- Unacceptable option values.
+SET auto_explain.log_extension_options = 'range_table maybe';
+ERROR:  EXPLAIN option "range_table" requires a Boolean value
+SET auto_explain.log_extension_options = 'range_table 2';
+ERROR:  EXPLAIN option "range_table" requires a Boolean value
+SET auto_explain.log_extension_options = 'range_table "0"';
+ERROR:  EXPLAIN option "range_table" requires a Boolean value
+SET auto_explain.log_extension_options = 'range_table 3.14159';
+ERROR:  EXPLAIN option "range_table" requires a Boolean value
+-- Supply enough options to force the option array to be reallocated.
+SET auto_explain.log_extension_options = 'debug, debug, debug, debug, debug, debug, debug, debug, debug, debug false';
index 6f9d22bf5d87925a52be4ea591ca329a5d6e7eac..d2b0650af1cbff6c7bb06849a4a63d83351b9398 100644 (file)
@@ -23,6 +23,7 @@ tests += {
   'regress': {
     'sql': [
       'alter_reset',
+      'extension_options',
     ],
   },
   'tap': {
diff --git a/contrib/auto_explain/sql/extension_options.sql b/contrib/auto_explain/sql/extension_options.sql
new file mode 100644 (file)
index 0000000..98920e8
--- /dev/null
@@ -0,0 +1,33 @@
+--
+-- Tests for auto_explain.log_extension_options.
+--
+
+LOAD 'auto_explain';
+LOAD 'pg_overexplain';
+
+-- Various legal values with assorted quoting and whitespace choices.
+SET auto_explain.log_extension_options = '';
+SET auto_explain.log_extension_options = 'debug, RANGE_TABLE';
+SET auto_explain.log_extension_options = 'debug TRUE  ';
+SET auto_explain.log_extension_options = '   debug 1,RAnge_table "off"';
+SET auto_explain.log_extension_options = $$"debug" tRuE, range_table 'false'$$;
+
+-- Syntax errors.
+SET auto_explain.log_extension_options = ',';
+SET auto_explain.log_extension_options = ', range_table';
+SET auto_explain.log_extension_options = 'range_table, ';
+SET auto_explain.log_extension_options = 'range_table true false';
+SET auto_explain.log_extension_options = '"range_table';
+SET auto_explain.log_extension_options = 'range_table 3.1415nine';
+SET auto_explain.log_extension_options = 'range_table "true';
+SET auto_explain.log_extension_options = $$range_table 'true$$;
+SET auto_explain.log_extension_options = $$'$$;
+
+-- Unacceptable option values.
+SET auto_explain.log_extension_options = 'range_table maybe';
+SET auto_explain.log_extension_options = 'range_table 2';
+SET auto_explain.log_extension_options = 'range_table "0"';
+SET auto_explain.log_extension_options = 'range_table 3.14159';
+
+-- Supply enough options to force the option array to be reallocated.
+SET auto_explain.log_extension_options = 'debug, debug, debug, debug, debug, debug, debug, debug, debug, debug false';
index 5f673bd14c11c5104d731c7c58d049d615481a34..b4e8e4b65a1dc9c21da4700028209d89b956dc56 100644 (file)
@@ -30,7 +30,7 @@ sub query_log
 my $node = PostgreSQL::Test::Cluster->new('main');
 $node->init(auth_extra => [ '--create-role' => 'regress_user1' ]);
 $node->append_conf('postgresql.conf',
-       "session_preload_libraries = 'auto_explain'");
+       "session_preload_libraries = 'pg_overexplain,auto_explain'");
 $node->append_conf('postgresql.conf', "auto_explain.log_min_duration = 0");
 $node->append_conf('postgresql.conf', "auto_explain.log_analyze = on");
 $node->start;
@@ -172,6 +172,22 @@ like(
        qr/"Node Type": "Index Scan"[^}]*"Index Name": "pg_class_relname_nsp_index"/s,
        "index scan logged, json mode");
 
+# Extension options.
+$log_contents = query_log(
+       $node,
+       "SELECT 1;",
+       { "auto_explain.log_extension_options" => "debug" });
+
+like(
+       $log_contents,
+       qr/Parallel Safe:/,
+       "extension option produces per-node output");
+
+like(
+       $log_contents,
+       qr/Command Type: select/,
+       "extension option produces per-plan output");
+
 # Check that PGC_SUSET parameters can be set by non-superuser if granted,
 # otherwise not
 
index 15c868021e6736ad205c7073d4afb89f26451ba5..ee85a67eb2e70d8431e6ae5006da8c9d6ed8c42a 100644 (file)
@@ -245,6 +245,29 @@ LOAD 'auto_explain';
     </listitem>
    </varlistentry>
 
+   <varlistentry id="auto-explain-configuration-parameters-log-extension-options">
+    <term>
+     <varname>auto_explain.log_extension_options</varname> (<type>string</type>)
+     <indexterm>
+      <primary><varname>auto_explain.log_extension_options</varname> configuration parameter</primary>
+     </indexterm>
+    </term>
+    <listitem>
+     <para>
+      Loadable modules can extend the <literal>EXPLAIN</literal> command with
+      additional options that affect the output format. Such options can also
+      be specified here. The value of this parameter is a comma-separated
+      list of options, each of which is an option name followed optionally by
+      an associated value. The module that provides the
+      <literal>EXPLAIN</literal> option, such as
+      <link linkend="pgplanadvice"><literal>pg_plan_advice</literal></link> or
+      <link linkend="pgoverexplain"><literal>pg_overexplain</literal></link>,
+      should be loaded before this parameter is set.
+      Only superusers can change this setting.
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry id="auto-explain-configuration-parameters-log-level">
     <term>
      <varname>auto_explain.log_level</varname> (<type>enum</type>)
index e2d8dbdb03b00fe1b05a24b2e21b5ecfcc1e672c..7515682fe9f9dca6a0bd1211e437d2440d50ce20 100644 (file)
@@ -3585,6 +3585,8 @@ astreamer_verify
 astreamer_waldump
 astreamer_zstd_frame
 auth_password_hook_typ
+auto_explain_extension_options
+auto_explain_option
 autovac_table
 av_relation
 avc_cache