From: Robert Haas Date: Mon, 6 Apr 2026 19:09:24 +0000 (-0400) Subject: auto_explain: Add new GUC, auto_explain.log_extension_options. X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=e972dff6c30447ebcfa2f8601b67f926247463b6;p=thirdparty%2Fpostgresql.git auto_explain: Add new GUC, auto_explain.log_extension_options. 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 Reviewed-by: Lukas Fittl Discussion: http://postgr.es/m/CA+Tgmob-0W8306mvrJX5Urtqt1AAasu8pi4yLrZ1XfwZU-Uj1w@mail.gmail.com --- diff --git a/contrib/auto_explain/Makefile b/contrib/auto_explain/Makefile index 94ab28e7c06..1f608b1d733 100644 --- a/contrib/auto_explain/Makefile +++ b/contrib/auto_explain/Makefile @@ -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 diff --git a/contrib/auto_explain/auto_explain.c b/contrib/auto_explain/auto_explain.c index 39bf2543b70..6ceae1c69ce 100644 --- a/contrib/auto_explain/auto_explain.c +++ b/contrib/auto_explain/auto_explain.c @@ -15,12 +15,17 @@ #include #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 index 00000000000..b5a66772311 --- /dev/null +++ b/contrib/auto_explain/expected/extension_options.out @@ -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'; diff --git a/contrib/auto_explain/meson.build b/contrib/auto_explain/meson.build index 6f9d22bf5d8..d2b0650af1c 100644 --- a/contrib/auto_explain/meson.build +++ b/contrib/auto_explain/meson.build @@ -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 index 00000000000..98920e88c9f --- /dev/null +++ b/contrib/auto_explain/sql/extension_options.sql @@ -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'; diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl index 5f673bd14c1..b4e8e4b65a1 100644 --- a/contrib/auto_explain/t/001_auto_explain.pl +++ b/contrib/auto_explain/t/001_auto_explain.pl @@ -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 diff --git a/doc/src/sgml/auto-explain.sgml b/doc/src/sgml/auto-explain.sgml index 15c868021e6..ee85a67eb2e 100644 --- a/doc/src/sgml/auto-explain.sgml +++ b/doc/src/sgml/auto-explain.sgml @@ -245,6 +245,29 @@ LOAD 'auto_explain'; + + + auto_explain.log_extension_options (string) + + auto_explain.log_extension_options configuration parameter + + + + + Loadable modules can extend the EXPLAIN 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 + EXPLAIN option, such as + pg_plan_advice or + pg_overexplain, + should be loaded before this parameter is set. + Only superusers can change this setting. + + + + auto_explain.log_level (enum) diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index e2d8dbdb03b..7515682fe9f 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -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