From: Robert Haas Date: Thu, 27 Feb 2025 17:37:10 +0000 (-0500) Subject: Create explain_format.c and move relevant code there. X-Git-Tag: REL_18_BETA1~731 X-Git-Url: http://git.postgresql.org/gitweb/?a=commitdiff_plain;h=9173e8b604636633a8e3aca54bb56a437bffa718;p=postgresql.git Create explain_format.c and move relevant code there. explain.c has grown rather large, so move various functions that are principally concerned with output generation to a new source file, explain_format.c, instead of lumping them in with everything else that is part of explain.c Reviewed-by: Peter Geoghegan Discussion: http://postgr.es/m/CA+TgmoYutMw1Jgo8BWUmB3TqnOhsEAJiYO=rOQufF4gPLWmkLQ@mail.gmail.com --- diff --git a/contrib/auto_explain/auto_explain.c b/contrib/auto_explain/auto_explain.c index 82c17c0a28a..8d665f1e621 100644 --- a/contrib/auto_explain/auto_explain.c +++ b/contrib/auto_explain/auto_explain.c @@ -16,6 +16,7 @@ #include "access/parallel.h" #include "commands/explain.h" +#include "commands/explain_format.h" #include "common/pg_prng.h" #include "executor/instrument.h" #include "utils/guc.h" diff --git a/contrib/file_fdw/file_fdw.c b/contrib/file_fdw/file_fdw.c index 0655bf532a0..bf707c812ed 100644 --- a/contrib/file_fdw/file_fdw.c +++ b/contrib/file_fdw/file_fdw.c @@ -25,6 +25,7 @@ #include "commands/copyfrom_internal.h" #include "commands/defrem.h" #include "commands/explain.h" +#include "commands/explain_format.h" #include "commands/vacuum.h" #include "foreign/fdwapi.h" #include "foreign/foreign.h" diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c index de43727a2a0..1131a8bf77e 100644 --- a/contrib/postgres_fdw/postgres_fdw.c +++ b/contrib/postgres_fdw/postgres_fdw.c @@ -20,6 +20,7 @@ #include "catalog/pg_opfamily.h" #include "commands/defrem.h" #include "commands/explain.h" +#include "commands/explain_format.h" #include "executor/execAsync.h" #include "foreign/fdwapi.h" #include "funcapi.h" diff --git a/src/backend/commands/Makefile b/src/backend/commands/Makefile index 48f7348f91c..04e406fb7cf 100644 --- a/src/backend/commands/Makefile +++ b/src/backend/commands/Makefile @@ -34,6 +34,7 @@ OBJS = \ dropcmds.o \ event_trigger.o \ explain.o \ + explain_format.o \ extension.o \ foreigncmds.o \ functioncmds.o \ diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c index 4271dd48e4e..38430a2e314 100644 --- a/src/backend/commands/explain.c +++ b/src/backend/commands/explain.c @@ -17,6 +17,7 @@ #include "catalog/pg_type.h" #include "commands/createas.h" #include "commands/defrem.h" +#include "commands/explain_format.h" #include "commands/prepare.h" #include "foreign/fdwapi.h" #include "jit/jit.h" @@ -57,12 +58,6 @@ typedef struct SerializeMetrics BufferUsage bufferUsage; /* buffers accessed during serialization */ } SerializeMetrics; -/* OR-able flags for ExplainXMLTag() */ -#define X_OPENING 0 -#define X_CLOSING 1 -#define X_CLOSE_IMMEDIATE 2 -#define X_NOWHITESPACE 4 - /* * Various places within need to convert bytes to kilobytes. Round these up * to the next whole kilobyte. @@ -166,19 +161,6 @@ static ExplainWorkersState *ExplainCreateWorkersState(int num_workers); static void ExplainOpenWorker(int n, ExplainState *es); static void ExplainCloseWorker(int n, ExplainState *es); static void ExplainFlushWorkersState(ExplainState *es); -static void ExplainProperty(const char *qlabel, const char *unit, - const char *value, bool numeric, ExplainState *es); -static void ExplainOpenSetAsideGroup(const char *objtype, const char *labelname, - bool labeled, int depth, ExplainState *es); -static void ExplainSaveGroup(ExplainState *es, int depth, int *state_save); -static void ExplainRestoreGroup(ExplainState *es, int depth, int *state_save); -static void ExplainDummyGroup(const char *objtype, const char *labelname, - ExplainState *es); -static void ExplainXMLTag(const char *tagname, int flags, ExplainState *es); -static void ExplainIndentText(ExplainState *es); -static void ExplainJSONLineEnding(ExplainState *es); -static void ExplainYAMLLineStarting(ExplainState *es); -static void escape_yaml(StringInfo buf, const char *str); static SerializeMetrics GetSerializationMetrics(DestReceiver *dest); @@ -4958,690 +4940,6 @@ ExplainFlushWorkersState(ExplainState *es) pfree(wstate); } -/* - * Explain a property, such as sort keys or targets, that takes the form of - * a list of unlabeled items. "data" is a list of C strings. - */ -void -ExplainPropertyList(const char *qlabel, List *data, ExplainState *es) -{ - ListCell *lc; - bool first = true; - - switch (es->format) - { - case EXPLAIN_FORMAT_TEXT: - ExplainIndentText(es); - appendStringInfo(es->str, "%s: ", qlabel); - foreach(lc, data) - { - if (!first) - appendStringInfoString(es->str, ", "); - appendStringInfoString(es->str, (const char *) lfirst(lc)); - first = false; - } - appendStringInfoChar(es->str, '\n'); - break; - - case EXPLAIN_FORMAT_XML: - ExplainXMLTag(qlabel, X_OPENING, es); - foreach(lc, data) - { - char *str; - - appendStringInfoSpaces(es->str, es->indent * 2 + 2); - appendStringInfoString(es->str, ""); - str = escape_xml((const char *) lfirst(lc)); - appendStringInfoString(es->str, str); - pfree(str); - appendStringInfoString(es->str, "\n"); - } - ExplainXMLTag(qlabel, X_CLOSING, es); - break; - - case EXPLAIN_FORMAT_JSON: - ExplainJSONLineEnding(es); - appendStringInfoSpaces(es->str, es->indent * 2); - escape_json(es->str, qlabel); - appendStringInfoString(es->str, ": ["); - foreach(lc, data) - { - if (!first) - appendStringInfoString(es->str, ", "); - escape_json(es->str, (const char *) lfirst(lc)); - first = false; - } - appendStringInfoChar(es->str, ']'); - break; - - case EXPLAIN_FORMAT_YAML: - ExplainYAMLLineStarting(es); - appendStringInfo(es->str, "%s: ", qlabel); - foreach(lc, data) - { - appendStringInfoChar(es->str, '\n'); - appendStringInfoSpaces(es->str, es->indent * 2 + 2); - appendStringInfoString(es->str, "- "); - escape_yaml(es->str, (const char *) lfirst(lc)); - } - break; - } -} - -/* - * Explain a property that takes the form of a list of unlabeled items within - * another list. "data" is a list of C strings. - */ -void -ExplainPropertyListNested(const char *qlabel, List *data, ExplainState *es) -{ - ListCell *lc; - bool first = true; - - switch (es->format) - { - case EXPLAIN_FORMAT_TEXT: - case EXPLAIN_FORMAT_XML: - ExplainPropertyList(qlabel, data, es); - return; - - case EXPLAIN_FORMAT_JSON: - ExplainJSONLineEnding(es); - appendStringInfoSpaces(es->str, es->indent * 2); - appendStringInfoChar(es->str, '['); - foreach(lc, data) - { - if (!first) - appendStringInfoString(es->str, ", "); - escape_json(es->str, (const char *) lfirst(lc)); - first = false; - } - appendStringInfoChar(es->str, ']'); - break; - - case EXPLAIN_FORMAT_YAML: - ExplainYAMLLineStarting(es); - appendStringInfoString(es->str, "- ["); - foreach(lc, data) - { - if (!first) - appendStringInfoString(es->str, ", "); - escape_yaml(es->str, (const char *) lfirst(lc)); - first = false; - } - appendStringInfoChar(es->str, ']'); - break; - } -} - -/* - * Explain a simple property. - * - * If "numeric" is true, the value is a number (or other value that - * doesn't need quoting in JSON). - * - * If unit is non-NULL the text format will display it after the value. - * - * This usually should not be invoked directly, but via one of the datatype - * specific routines ExplainPropertyText, ExplainPropertyInteger, etc. - */ -static void -ExplainProperty(const char *qlabel, const char *unit, const char *value, - bool numeric, ExplainState *es) -{ - switch (es->format) - { - case EXPLAIN_FORMAT_TEXT: - ExplainIndentText(es); - if (unit) - appendStringInfo(es->str, "%s: %s %s\n", qlabel, value, unit); - else - appendStringInfo(es->str, "%s: %s\n", qlabel, value); - break; - - case EXPLAIN_FORMAT_XML: - { - char *str; - - appendStringInfoSpaces(es->str, es->indent * 2); - ExplainXMLTag(qlabel, X_OPENING | X_NOWHITESPACE, es); - str = escape_xml(value); - appendStringInfoString(es->str, str); - pfree(str); - ExplainXMLTag(qlabel, X_CLOSING | X_NOWHITESPACE, es); - appendStringInfoChar(es->str, '\n'); - } - break; - - case EXPLAIN_FORMAT_JSON: - ExplainJSONLineEnding(es); - appendStringInfoSpaces(es->str, es->indent * 2); - escape_json(es->str, qlabel); - appendStringInfoString(es->str, ": "); - if (numeric) - appendStringInfoString(es->str, value); - else - escape_json(es->str, value); - break; - - case EXPLAIN_FORMAT_YAML: - ExplainYAMLLineStarting(es); - appendStringInfo(es->str, "%s: ", qlabel); - if (numeric) - appendStringInfoString(es->str, value); - else - escape_yaml(es->str, value); - break; - } -} - -/* - * Explain a string-valued property. - */ -void -ExplainPropertyText(const char *qlabel, const char *value, ExplainState *es) -{ - ExplainProperty(qlabel, NULL, value, false, es); -} - -/* - * Explain an integer-valued property. - */ -void -ExplainPropertyInteger(const char *qlabel, const char *unit, int64 value, - ExplainState *es) -{ - char buf[32]; - - snprintf(buf, sizeof(buf), INT64_FORMAT, value); - ExplainProperty(qlabel, unit, buf, true, es); -} - -/* - * Explain an unsigned integer-valued property. - */ -void -ExplainPropertyUInteger(const char *qlabel, const char *unit, uint64 value, - ExplainState *es) -{ - char buf[32]; - - snprintf(buf, sizeof(buf), UINT64_FORMAT, value); - ExplainProperty(qlabel, unit, buf, true, es); -} - -/* - * Explain a float-valued property, using the specified number of - * fractional digits. - */ -void -ExplainPropertyFloat(const char *qlabel, const char *unit, double value, - int ndigits, ExplainState *es) -{ - char *buf; - - buf = psprintf("%.*f", ndigits, value); - ExplainProperty(qlabel, unit, buf, true, es); - pfree(buf); -} - -/* - * Explain a bool-valued property. - */ -void -ExplainPropertyBool(const char *qlabel, bool value, ExplainState *es) -{ - ExplainProperty(qlabel, NULL, value ? "true" : "false", true, es); -} - -/* - * Open a group of related objects. - * - * objtype is the type of the group object, labelname is its label within - * a containing object (if any). - * - * If labeled is true, the group members will be labeled properties, - * while if it's false, they'll be unlabeled objects. - */ -void -ExplainOpenGroup(const char *objtype, const char *labelname, - bool labeled, ExplainState *es) -{ - switch (es->format) - { - case EXPLAIN_FORMAT_TEXT: - /* nothing to do */ - break; - - case EXPLAIN_FORMAT_XML: - ExplainXMLTag(objtype, X_OPENING, es); - es->indent++; - break; - - case EXPLAIN_FORMAT_JSON: - ExplainJSONLineEnding(es); - appendStringInfoSpaces(es->str, 2 * es->indent); - if (labelname) - { - escape_json(es->str, labelname); - appendStringInfoString(es->str, ": "); - } - appendStringInfoChar(es->str, labeled ? '{' : '['); - - /* - * In JSON format, the grouping_stack is an integer list. 0 means - * we've emitted nothing at this grouping level, 1 means we've - * emitted something (and so the next item needs a comma). See - * ExplainJSONLineEnding(). - */ - es->grouping_stack = lcons_int(0, es->grouping_stack); - es->indent++; - break; - - case EXPLAIN_FORMAT_YAML: - - /* - * In YAML format, the grouping stack is an integer list. 0 means - * we've emitted nothing at this grouping level AND this grouping - * level is unlabeled and must be marked with "- ". See - * ExplainYAMLLineStarting(). - */ - ExplainYAMLLineStarting(es); - if (labelname) - { - appendStringInfo(es->str, "%s: ", labelname); - es->grouping_stack = lcons_int(1, es->grouping_stack); - } - else - { - appendStringInfoString(es->str, "- "); - es->grouping_stack = lcons_int(0, es->grouping_stack); - } - es->indent++; - break; - } -} - -/* - * Close a group of related objects. - * Parameters must match the corresponding ExplainOpenGroup call. - */ -void -ExplainCloseGroup(const char *objtype, const char *labelname, - bool labeled, ExplainState *es) -{ - switch (es->format) - { - case EXPLAIN_FORMAT_TEXT: - /* nothing to do */ - break; - - case EXPLAIN_FORMAT_XML: - es->indent--; - ExplainXMLTag(objtype, X_CLOSING, es); - break; - - case EXPLAIN_FORMAT_JSON: - es->indent--; - appendStringInfoChar(es->str, '\n'); - appendStringInfoSpaces(es->str, 2 * es->indent); - appendStringInfoChar(es->str, labeled ? '}' : ']'); - es->grouping_stack = list_delete_first(es->grouping_stack); - break; - - case EXPLAIN_FORMAT_YAML: - es->indent--; - es->grouping_stack = list_delete_first(es->grouping_stack); - break; - } -} - -/* - * Open a group of related objects, without emitting actual data. - * - * Prepare the formatting state as though we were beginning a group with - * the identified properties, but don't actually emit anything. Output - * subsequent to this call can be redirected into a separate output buffer, - * and then eventually appended to the main output buffer after doing a - * regular ExplainOpenGroup call (with the same parameters). - * - * The extra "depth" parameter is the new group's depth compared to current. - * It could be more than one, in case the eventual output will be enclosed - * in additional nesting group levels. We assume we don't need to track - * formatting state for those levels while preparing this group's output. - * - * There is no ExplainCloseSetAsideGroup --- in current usage, we always - * pop this state with ExplainSaveGroup. - */ -static void -ExplainOpenSetAsideGroup(const char *objtype, const char *labelname, - bool labeled, int depth, ExplainState *es) -{ - switch (es->format) - { - case EXPLAIN_FORMAT_TEXT: - /* nothing to do */ - break; - - case EXPLAIN_FORMAT_XML: - es->indent += depth; - break; - - case EXPLAIN_FORMAT_JSON: - es->grouping_stack = lcons_int(0, es->grouping_stack); - es->indent += depth; - break; - - case EXPLAIN_FORMAT_YAML: - if (labelname) - es->grouping_stack = lcons_int(1, es->grouping_stack); - else - es->grouping_stack = lcons_int(0, es->grouping_stack); - es->indent += depth; - break; - } -} - -/* - * Pop one level of grouping state, allowing for a re-push later. - * - * This is typically used after ExplainOpenSetAsideGroup; pass the - * same "depth" used for that. - * - * This should not emit any output. If state needs to be saved, - * save it at *state_save. Currently, an integer save area is sufficient - * for all formats, but we might need to revisit that someday. - */ -static void -ExplainSaveGroup(ExplainState *es, int depth, int *state_save) -{ - switch (es->format) - { - case EXPLAIN_FORMAT_TEXT: - /* nothing to do */ - break; - - case EXPLAIN_FORMAT_XML: - es->indent -= depth; - break; - - case EXPLAIN_FORMAT_JSON: - es->indent -= depth; - *state_save = linitial_int(es->grouping_stack); - es->grouping_stack = list_delete_first(es->grouping_stack); - break; - - case EXPLAIN_FORMAT_YAML: - es->indent -= depth; - *state_save = linitial_int(es->grouping_stack); - es->grouping_stack = list_delete_first(es->grouping_stack); - break; - } -} - -/* - * Re-push one level of grouping state, undoing the effects of ExplainSaveGroup. - */ -static void -ExplainRestoreGroup(ExplainState *es, int depth, int *state_save) -{ - switch (es->format) - { - case EXPLAIN_FORMAT_TEXT: - /* nothing to do */ - break; - - case EXPLAIN_FORMAT_XML: - es->indent += depth; - break; - - case EXPLAIN_FORMAT_JSON: - es->grouping_stack = lcons_int(*state_save, es->grouping_stack); - es->indent += depth; - break; - - case EXPLAIN_FORMAT_YAML: - es->grouping_stack = lcons_int(*state_save, es->grouping_stack); - es->indent += depth; - break; - } -} - -/* - * Emit a "dummy" group that never has any members. - * - * objtype is the type of the group object, labelname is its label within - * a containing object (if any). - */ -static void -ExplainDummyGroup(const char *objtype, const char *labelname, ExplainState *es) -{ - switch (es->format) - { - case EXPLAIN_FORMAT_TEXT: - /* nothing to do */ - break; - - case EXPLAIN_FORMAT_XML: - ExplainXMLTag(objtype, X_CLOSE_IMMEDIATE, es); - break; - - case EXPLAIN_FORMAT_JSON: - ExplainJSONLineEnding(es); - appendStringInfoSpaces(es->str, 2 * es->indent); - if (labelname) - { - escape_json(es->str, labelname); - appendStringInfoString(es->str, ": "); - } - escape_json(es->str, objtype); - break; - - case EXPLAIN_FORMAT_YAML: - ExplainYAMLLineStarting(es); - if (labelname) - { - escape_yaml(es->str, labelname); - appendStringInfoString(es->str, ": "); - } - else - { - appendStringInfoString(es->str, "- "); - } - escape_yaml(es->str, objtype); - break; - } -} - -/* - * Emit the start-of-output boilerplate. - * - * This is just enough different from processing a subgroup that we need - * a separate pair of subroutines. - */ -void -ExplainBeginOutput(ExplainState *es) -{ - switch (es->format) - { - case EXPLAIN_FORMAT_TEXT: - /* nothing to do */ - break; - - case EXPLAIN_FORMAT_XML: - appendStringInfoString(es->str, - "\n"); - es->indent++; - break; - - case EXPLAIN_FORMAT_JSON: - /* top-level structure is an array of plans */ - appendStringInfoChar(es->str, '['); - es->grouping_stack = lcons_int(0, es->grouping_stack); - es->indent++; - break; - - case EXPLAIN_FORMAT_YAML: - es->grouping_stack = lcons_int(0, es->grouping_stack); - break; - } -} - -/* - * Emit the end-of-output boilerplate. - */ -void -ExplainEndOutput(ExplainState *es) -{ - switch (es->format) - { - case EXPLAIN_FORMAT_TEXT: - /* nothing to do */ - break; - - case EXPLAIN_FORMAT_XML: - es->indent--; - appendStringInfoString(es->str, ""); - break; - - case EXPLAIN_FORMAT_JSON: - es->indent--; - appendStringInfoString(es->str, "\n]"); - es->grouping_stack = list_delete_first(es->grouping_stack); - break; - - case EXPLAIN_FORMAT_YAML: - es->grouping_stack = list_delete_first(es->grouping_stack); - break; - } -} - -/* - * Put an appropriate separator between multiple plans - */ -void -ExplainSeparatePlans(ExplainState *es) -{ - switch (es->format) - { - case EXPLAIN_FORMAT_TEXT: - /* add a blank line */ - appendStringInfoChar(es->str, '\n'); - break; - - case EXPLAIN_FORMAT_XML: - case EXPLAIN_FORMAT_JSON: - case EXPLAIN_FORMAT_YAML: - /* nothing to do */ - break; - } -} - -/* - * Emit opening or closing XML tag. - * - * "flags" must contain X_OPENING, X_CLOSING, or X_CLOSE_IMMEDIATE. - * Optionally, OR in X_NOWHITESPACE to suppress the whitespace we'd normally - * add. - * - * XML restricts tag names more than our other output formats, eg they can't - * contain white space or slashes. Replace invalid characters with dashes, - * so that for example "I/O Read Time" becomes "I-O-Read-Time". - */ -static void -ExplainXMLTag(const char *tagname, int flags, ExplainState *es) -{ - const char *s; - const char *valid = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_."; - - if ((flags & X_NOWHITESPACE) == 0) - appendStringInfoSpaces(es->str, 2 * es->indent); - appendStringInfoCharMacro(es->str, '<'); - if ((flags & X_CLOSING) != 0) - appendStringInfoCharMacro(es->str, '/'); - for (s = tagname; *s; s++) - appendStringInfoChar(es->str, strchr(valid, *s) ? *s : '-'); - if ((flags & X_CLOSE_IMMEDIATE) != 0) - appendStringInfoString(es->str, " /"); - appendStringInfoCharMacro(es->str, '>'); - if ((flags & X_NOWHITESPACE) == 0) - appendStringInfoCharMacro(es->str, '\n'); -} - -/* - * Indent a text-format line. - * - * We indent by two spaces per indentation level. However, when emitting - * data for a parallel worker there might already be data on the current line - * (cf. ExplainOpenWorker); in that case, don't indent any more. - */ -static void -ExplainIndentText(ExplainState *es) -{ - Assert(es->format == EXPLAIN_FORMAT_TEXT); - if (es->str->len == 0 || es->str->data[es->str->len - 1] == '\n') - appendStringInfoSpaces(es->str, es->indent * 2); -} - -/* - * Emit a JSON line ending. - * - * JSON requires a comma after each property but the last. To facilitate this, - * in JSON format, the text emitted for each property begins just prior to the - * preceding line-break (and comma, if applicable). - */ -static void -ExplainJSONLineEnding(ExplainState *es) -{ - Assert(es->format == EXPLAIN_FORMAT_JSON); - if (linitial_int(es->grouping_stack) != 0) - appendStringInfoChar(es->str, ','); - else - linitial_int(es->grouping_stack) = 1; - appendStringInfoChar(es->str, '\n'); -} - -/* - * Indent a YAML line. - * - * YAML lines are ordinarily indented by two spaces per indentation level. - * The text emitted for each property begins just prior to the preceding - * line-break, except for the first property in an unlabeled group, for which - * it begins immediately after the "- " that introduces the group. The first - * property of the group appears on the same line as the opening "- ". - */ -static void -ExplainYAMLLineStarting(ExplainState *es) -{ - Assert(es->format == EXPLAIN_FORMAT_YAML); - if (linitial_int(es->grouping_stack) == 0) - { - linitial_int(es->grouping_stack) = 1; - } - else - { - appendStringInfoChar(es->str, '\n'); - appendStringInfoSpaces(es->str, es->indent * 2); - } -} - -/* - * YAML is a superset of JSON; unfortunately, the YAML quoting rules are - * ridiculously complicated -- as documented in sections 5.3 and 7.3.3 of - * http://yaml.org/spec/1.2/spec.html -- so we chose to just quote everything. - * Empty strings, strings with leading or trailing whitespace, and strings - * containing a variety of special characters must certainly be quoted or the - * output is invalid; and other seemingly harmless strings like "0xa" or - * "true" must be quoted, lest they be interpreted as a hexadecimal or Boolean - * constant rather than a string. - */ -static void -escape_yaml(StringInfo buf, const char *str) -{ - escape_json(buf, str); -} - - /* * DestReceiver functions for SERIALIZE option * diff --git a/src/backend/commands/explain_format.c b/src/backend/commands/explain_format.c new file mode 100644 index 00000000000..bccdd76a874 --- /dev/null +++ b/src/backend/commands/explain_format.c @@ -0,0 +1,713 @@ +/*------------------------------------------------------------------------- + * + * explain_format.c + * Format routines for explaining query execution plans + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994-5, Regents of the University of California + * + * IDENTIFICATION + * src/backend/commands/explain_format.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "commands/explain.h" +#include "commands/explain_format.h" +#include "utils/json.h" +#include "utils/xml.h" + +/* OR-able flags for ExplainXMLTag() */ +#define X_OPENING 0 +#define X_CLOSING 1 +#define X_CLOSE_IMMEDIATE 2 +#define X_NOWHITESPACE 4 + +static void ExplainJSONLineEnding(ExplainState *es); +static void ExplainXMLTag(const char *tagname, int flags, ExplainState *es); +static void ExplainYAMLLineStarting(ExplainState *es); +static void escape_yaml(StringInfo buf, const char *str); + +/* + * Explain a property, such as sort keys or targets, that takes the form of + * a list of unlabeled items. "data" is a list of C strings. + */ +void +ExplainPropertyList(const char *qlabel, List *data, ExplainState *es) +{ + ListCell *lc; + bool first = true; + + switch (es->format) + { + case EXPLAIN_FORMAT_TEXT: + ExplainIndentText(es); + appendStringInfo(es->str, "%s: ", qlabel); + foreach(lc, data) + { + if (!first) + appendStringInfoString(es->str, ", "); + appendStringInfoString(es->str, (const char *) lfirst(lc)); + first = false; + } + appendStringInfoChar(es->str, '\n'); + break; + + case EXPLAIN_FORMAT_XML: + ExplainXMLTag(qlabel, X_OPENING, es); + foreach(lc, data) + { + char *str; + + appendStringInfoSpaces(es->str, es->indent * 2 + 2); + appendStringInfoString(es->str, ""); + str = escape_xml((const char *) lfirst(lc)); + appendStringInfoString(es->str, str); + pfree(str); + appendStringInfoString(es->str, "\n"); + } + ExplainXMLTag(qlabel, X_CLOSING, es); + break; + + case EXPLAIN_FORMAT_JSON: + ExplainJSONLineEnding(es); + appendStringInfoSpaces(es->str, es->indent * 2); + escape_json(es->str, qlabel); + appendStringInfoString(es->str, ": ["); + foreach(lc, data) + { + if (!first) + appendStringInfoString(es->str, ", "); + escape_json(es->str, (const char *) lfirst(lc)); + first = false; + } + appendStringInfoChar(es->str, ']'); + break; + + case EXPLAIN_FORMAT_YAML: + ExplainYAMLLineStarting(es); + appendStringInfo(es->str, "%s: ", qlabel); + foreach(lc, data) + { + appendStringInfoChar(es->str, '\n'); + appendStringInfoSpaces(es->str, es->indent * 2 + 2); + appendStringInfoString(es->str, "- "); + escape_yaml(es->str, (const char *) lfirst(lc)); + } + break; + } +} + +/* + * Explain a property that takes the form of a list of unlabeled items within + * another list. "data" is a list of C strings. + */ +void +ExplainPropertyListNested(const char *qlabel, List *data, ExplainState *es) +{ + ListCell *lc; + bool first = true; + + switch (es->format) + { + case EXPLAIN_FORMAT_TEXT: + case EXPLAIN_FORMAT_XML: + ExplainPropertyList(qlabel, data, es); + return; + + case EXPLAIN_FORMAT_JSON: + ExplainJSONLineEnding(es); + appendStringInfoSpaces(es->str, es->indent * 2); + appendStringInfoChar(es->str, '['); + foreach(lc, data) + { + if (!first) + appendStringInfoString(es->str, ", "); + escape_json(es->str, (const char *) lfirst(lc)); + first = false; + } + appendStringInfoChar(es->str, ']'); + break; + + case EXPLAIN_FORMAT_YAML: + ExplainYAMLLineStarting(es); + appendStringInfoString(es->str, "- ["); + foreach(lc, data) + { + if (!first) + appendStringInfoString(es->str, ", "); + escape_yaml(es->str, (const char *) lfirst(lc)); + first = false; + } + appendStringInfoChar(es->str, ']'); + break; + } +} + +/* + * Explain a simple property. + * + * If "numeric" is true, the value is a number (or other value that + * doesn't need quoting in JSON). + * + * If unit is non-NULL the text format will display it after the value. + * + * This usually should not be invoked directly, but via one of the datatype + * specific routines ExplainPropertyText, ExplainPropertyInteger, etc. + */ +static void +ExplainProperty(const char *qlabel, const char *unit, const char *value, + bool numeric, ExplainState *es) +{ + switch (es->format) + { + case EXPLAIN_FORMAT_TEXT: + ExplainIndentText(es); + if (unit) + appendStringInfo(es->str, "%s: %s %s\n", qlabel, value, unit); + else + appendStringInfo(es->str, "%s: %s\n", qlabel, value); + break; + + case EXPLAIN_FORMAT_XML: + { + char *str; + + appendStringInfoSpaces(es->str, es->indent * 2); + ExplainXMLTag(qlabel, X_OPENING | X_NOWHITESPACE, es); + str = escape_xml(value); + appendStringInfoString(es->str, str); + pfree(str); + ExplainXMLTag(qlabel, X_CLOSING | X_NOWHITESPACE, es); + appendStringInfoChar(es->str, '\n'); + } + break; + + case EXPLAIN_FORMAT_JSON: + ExplainJSONLineEnding(es); + appendStringInfoSpaces(es->str, es->indent * 2); + escape_json(es->str, qlabel); + appendStringInfoString(es->str, ": "); + if (numeric) + appendStringInfoString(es->str, value); + else + escape_json(es->str, value); + break; + + case EXPLAIN_FORMAT_YAML: + ExplainYAMLLineStarting(es); + appendStringInfo(es->str, "%s: ", qlabel); + if (numeric) + appendStringInfoString(es->str, value); + else + escape_yaml(es->str, value); + break; + } +} + +/* + * Explain a string-valued property. + */ +void +ExplainPropertyText(const char *qlabel, const char *value, ExplainState *es) +{ + ExplainProperty(qlabel, NULL, value, false, es); +} + +/* + * Explain an integer-valued property. + */ +void +ExplainPropertyInteger(const char *qlabel, const char *unit, int64 value, + ExplainState *es) +{ + char buf[32]; + + snprintf(buf, sizeof(buf), INT64_FORMAT, value); + ExplainProperty(qlabel, unit, buf, true, es); +} + +/* + * Explain an unsigned integer-valued property. + */ +void +ExplainPropertyUInteger(const char *qlabel, const char *unit, uint64 value, + ExplainState *es) +{ + char buf[32]; + + snprintf(buf, sizeof(buf), UINT64_FORMAT, value); + ExplainProperty(qlabel, unit, buf, true, es); +} + +/* + * Explain a float-valued property, using the specified number of + * fractional digits. + */ +void +ExplainPropertyFloat(const char *qlabel, const char *unit, double value, + int ndigits, ExplainState *es) +{ + char *buf; + + buf = psprintf("%.*f", ndigits, value); + ExplainProperty(qlabel, unit, buf, true, es); + pfree(buf); +} + +/* + * Explain a bool-valued property. + */ +void +ExplainPropertyBool(const char *qlabel, bool value, ExplainState *es) +{ + ExplainProperty(qlabel, NULL, value ? "true" : "false", true, es); +} + +/* + * Open a group of related objects. + * + * objtype is the type of the group object, labelname is its label within + * a containing object (if any). + * + * If labeled is true, the group members will be labeled properties, + * while if it's false, they'll be unlabeled objects. + */ +void +ExplainOpenGroup(const char *objtype, const char *labelname, + bool labeled, ExplainState *es) +{ + switch (es->format) + { + case EXPLAIN_FORMAT_TEXT: + /* nothing to do */ + break; + + case EXPLAIN_FORMAT_XML: + ExplainXMLTag(objtype, X_OPENING, es); + es->indent++; + break; + + case EXPLAIN_FORMAT_JSON: + ExplainJSONLineEnding(es); + appendStringInfoSpaces(es->str, 2 * es->indent); + if (labelname) + { + escape_json(es->str, labelname); + appendStringInfoString(es->str, ": "); + } + appendStringInfoChar(es->str, labeled ? '{' : '['); + + /* + * In JSON format, the grouping_stack is an integer list. 0 means + * we've emitted nothing at this grouping level, 1 means we've + * emitted something (and so the next item needs a comma). See + * ExplainJSONLineEnding(). + */ + es->grouping_stack = lcons_int(0, es->grouping_stack); + es->indent++; + break; + + case EXPLAIN_FORMAT_YAML: + + /* + * In YAML format, the grouping stack is an integer list. 0 means + * we've emitted nothing at this grouping level AND this grouping + * level is unlabeled and must be marked with "- ". See + * ExplainYAMLLineStarting(). + */ + ExplainYAMLLineStarting(es); + if (labelname) + { + appendStringInfo(es->str, "%s: ", labelname); + es->grouping_stack = lcons_int(1, es->grouping_stack); + } + else + { + appendStringInfoString(es->str, "- "); + es->grouping_stack = lcons_int(0, es->grouping_stack); + } + es->indent++; + break; + } +} + +/* + * Close a group of related objects. + * Parameters must match the corresponding ExplainOpenGroup call. + */ +void +ExplainCloseGroup(const char *objtype, const char *labelname, + bool labeled, ExplainState *es) +{ + switch (es->format) + { + case EXPLAIN_FORMAT_TEXT: + /* nothing to do */ + break; + + case EXPLAIN_FORMAT_XML: + es->indent--; + ExplainXMLTag(objtype, X_CLOSING, es); + break; + + case EXPLAIN_FORMAT_JSON: + es->indent--; + appendStringInfoChar(es->str, '\n'); + appendStringInfoSpaces(es->str, 2 * es->indent); + appendStringInfoChar(es->str, labeled ? '}' : ']'); + es->grouping_stack = list_delete_first(es->grouping_stack); + break; + + case EXPLAIN_FORMAT_YAML: + es->indent--; + es->grouping_stack = list_delete_first(es->grouping_stack); + break; + } +} + +/* + * Open a group of related objects, without emitting actual data. + * + * Prepare the formatting state as though we were beginning a group with + * the identified properties, but don't actually emit anything. Output + * subsequent to this call can be redirected into a separate output buffer, + * and then eventually appended to the main output buffer after doing a + * regular ExplainOpenGroup call (with the same parameters). + * + * The extra "depth" parameter is the new group's depth compared to current. + * It could be more than one, in case the eventual output will be enclosed + * in additional nesting group levels. We assume we don't need to track + * formatting state for those levels while preparing this group's output. + * + * There is no ExplainCloseSetAsideGroup --- in current usage, we always + * pop this state with ExplainSaveGroup. + */ +void +ExplainOpenSetAsideGroup(const char *objtype, const char *labelname, + bool labeled, int depth, ExplainState *es) +{ + switch (es->format) + { + case EXPLAIN_FORMAT_TEXT: + /* nothing to do */ + break; + + case EXPLAIN_FORMAT_XML: + es->indent += depth; + break; + + case EXPLAIN_FORMAT_JSON: + es->grouping_stack = lcons_int(0, es->grouping_stack); + es->indent += depth; + break; + + case EXPLAIN_FORMAT_YAML: + if (labelname) + es->grouping_stack = lcons_int(1, es->grouping_stack); + else + es->grouping_stack = lcons_int(0, es->grouping_stack); + es->indent += depth; + break; + } +} + +/* + * Pop one level of grouping state, allowing for a re-push later. + * + * This is typically used after ExplainOpenSetAsideGroup; pass the + * same "depth" used for that. + * + * This should not emit any output. If state needs to be saved, + * save it at *state_save. Currently, an integer save area is sufficient + * for all formats, but we might need to revisit that someday. + */ +void +ExplainSaveGroup(ExplainState *es, int depth, int *state_save) +{ + switch (es->format) + { + case EXPLAIN_FORMAT_TEXT: + /* nothing to do */ + break; + + case EXPLAIN_FORMAT_XML: + es->indent -= depth; + break; + + case EXPLAIN_FORMAT_JSON: + es->indent -= depth; + *state_save = linitial_int(es->grouping_stack); + es->grouping_stack = list_delete_first(es->grouping_stack); + break; + + case EXPLAIN_FORMAT_YAML: + es->indent -= depth; + *state_save = linitial_int(es->grouping_stack); + es->grouping_stack = list_delete_first(es->grouping_stack); + break; + } +} + +/* + * Re-push one level of grouping state, undoing the effects of ExplainSaveGroup. + */ +void +ExplainRestoreGroup(ExplainState *es, int depth, int *state_save) +{ + switch (es->format) + { + case EXPLAIN_FORMAT_TEXT: + /* nothing to do */ + break; + + case EXPLAIN_FORMAT_XML: + es->indent += depth; + break; + + case EXPLAIN_FORMAT_JSON: + es->grouping_stack = lcons_int(*state_save, es->grouping_stack); + es->indent += depth; + break; + + case EXPLAIN_FORMAT_YAML: + es->grouping_stack = lcons_int(*state_save, es->grouping_stack); + es->indent += depth; + break; + } +} + +/* + * Emit a "dummy" group that never has any members. + * + * objtype is the type of the group object, labelname is its label within + * a containing object (if any). + */ +void +ExplainDummyGroup(const char *objtype, const char *labelname, ExplainState *es) +{ + switch (es->format) + { + case EXPLAIN_FORMAT_TEXT: + /* nothing to do */ + break; + + case EXPLAIN_FORMAT_XML: + ExplainXMLTag(objtype, X_CLOSE_IMMEDIATE, es); + break; + + case EXPLAIN_FORMAT_JSON: + ExplainJSONLineEnding(es); + appendStringInfoSpaces(es->str, 2 * es->indent); + if (labelname) + { + escape_json(es->str, labelname); + appendStringInfoString(es->str, ": "); + } + escape_json(es->str, objtype); + break; + + case EXPLAIN_FORMAT_YAML: + ExplainYAMLLineStarting(es); + if (labelname) + { + escape_yaml(es->str, labelname); + appendStringInfoString(es->str, ": "); + } + else + { + appendStringInfoString(es->str, "- "); + } + escape_yaml(es->str, objtype); + break; + } +} + +/* + * Emit the start-of-output boilerplate. + * + * This is just enough different from processing a subgroup that we need + * a separate pair of subroutines. + */ +void +ExplainBeginOutput(ExplainState *es) +{ + switch (es->format) + { + case EXPLAIN_FORMAT_TEXT: + /* nothing to do */ + break; + + case EXPLAIN_FORMAT_XML: + appendStringInfoString(es->str, + "\n"); + es->indent++; + break; + + case EXPLAIN_FORMAT_JSON: + /* top-level structure is an array of plans */ + appendStringInfoChar(es->str, '['); + es->grouping_stack = lcons_int(0, es->grouping_stack); + es->indent++; + break; + + case EXPLAIN_FORMAT_YAML: + es->grouping_stack = lcons_int(0, es->grouping_stack); + break; + } +} + +/* + * Emit the end-of-output boilerplate. + */ +void +ExplainEndOutput(ExplainState *es) +{ + switch (es->format) + { + case EXPLAIN_FORMAT_TEXT: + /* nothing to do */ + break; + + case EXPLAIN_FORMAT_XML: + es->indent--; + appendStringInfoString(es->str, ""); + break; + + case EXPLAIN_FORMAT_JSON: + es->indent--; + appendStringInfoString(es->str, "\n]"); + es->grouping_stack = list_delete_first(es->grouping_stack); + break; + + case EXPLAIN_FORMAT_YAML: + es->grouping_stack = list_delete_first(es->grouping_stack); + break; + } +} + +/* + * Put an appropriate separator between multiple plans + */ +void +ExplainSeparatePlans(ExplainState *es) +{ + switch (es->format) + { + case EXPLAIN_FORMAT_TEXT: + /* add a blank line */ + appendStringInfoChar(es->str, '\n'); + break; + + case EXPLAIN_FORMAT_XML: + case EXPLAIN_FORMAT_JSON: + case EXPLAIN_FORMAT_YAML: + /* nothing to do */ + break; + } +} + +/* + * Emit opening or closing XML tag. + * + * "flags" must contain X_OPENING, X_CLOSING, or X_CLOSE_IMMEDIATE. + * Optionally, OR in X_NOWHITESPACE to suppress the whitespace we'd normally + * add. + * + * XML restricts tag names more than our other output formats, eg they can't + * contain white space or slashes. Replace invalid characters with dashes, + * so that for example "I/O Read Time" becomes "I-O-Read-Time". + */ +static void +ExplainXMLTag(const char *tagname, int flags, ExplainState *es) +{ + const char *s; + const char *valid = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_."; + + if ((flags & X_NOWHITESPACE) == 0) + appendStringInfoSpaces(es->str, 2 * es->indent); + appendStringInfoCharMacro(es->str, '<'); + if ((flags & X_CLOSING) != 0) + appendStringInfoCharMacro(es->str, '/'); + for (s = tagname; *s; s++) + appendStringInfoChar(es->str, strchr(valid, *s) ? *s : '-'); + if ((flags & X_CLOSE_IMMEDIATE) != 0) + appendStringInfoString(es->str, " /"); + appendStringInfoCharMacro(es->str, '>'); + if ((flags & X_NOWHITESPACE) == 0) + appendStringInfoCharMacro(es->str, '\n'); +} + +/* + * Indent a text-format line. + * + * We indent by two spaces per indentation level. However, when emitting + * data for a parallel worker there might already be data on the current line + * (cf. ExplainOpenWorker); in that case, don't indent any more. + */ +void +ExplainIndentText(ExplainState *es) +{ + Assert(es->format == EXPLAIN_FORMAT_TEXT); + if (es->str->len == 0 || es->str->data[es->str->len - 1] == '\n') + appendStringInfoSpaces(es->str, es->indent * 2); +} + +/* + * Emit a JSON line ending. + * + * JSON requires a comma after each property but the last. To facilitate this, + * in JSON format, the text emitted for each property begins just prior to the + * preceding line-break (and comma, if applicable). + */ +static void +ExplainJSONLineEnding(ExplainState *es) +{ + Assert(es->format == EXPLAIN_FORMAT_JSON); + if (linitial_int(es->grouping_stack) != 0) + appendStringInfoChar(es->str, ','); + else + linitial_int(es->grouping_stack) = 1; + appendStringInfoChar(es->str, '\n'); +} + +/* + * Indent a YAML line. + * + * YAML lines are ordinarily indented by two spaces per indentation level. + * The text emitted for each property begins just prior to the preceding + * line-break, except for the first property in an unlabeled group, for which + * it begins immediately after the "- " that introduces the group. The first + * property of the group appears on the same line as the opening "- ". + */ +static void +ExplainYAMLLineStarting(ExplainState *es) +{ + Assert(es->format == EXPLAIN_FORMAT_YAML); + if (linitial_int(es->grouping_stack) == 0) + { + linitial_int(es->grouping_stack) = 1; + } + else + { + appendStringInfoChar(es->str, '\n'); + appendStringInfoSpaces(es->str, es->indent * 2); + } +} + +/* + * YAML is a superset of JSON; unfortunately, the YAML quoting rules are + * ridiculously complicated -- as documented in sections 5.3 and 7.3.3 of + * http://yaml.org/spec/1.2/spec.html -- so we chose to just quote everything. + * Empty strings, strings with leading or trailing whitespace, and strings + * containing a variety of special characters must certainly be quoted or the + * output is invalid; and other seemingly harmless strings like "0xa" or + * "true" must be quoted, lest they be interpreted as a hexadecimal or Boolean + * constant rather than a string. + */ +static void +escape_yaml(StringInfo buf, const char *str) +{ + escape_json(buf, str); +} diff --git a/src/backend/commands/meson.build b/src/backend/commands/meson.build index ef0d407a383..0d0106ec096 100644 --- a/src/backend/commands/meson.build +++ b/src/backend/commands/meson.build @@ -22,6 +22,7 @@ backend_sources += files( 'dropcmds.c', 'event_trigger.c', 'explain.c', + 'explain_format.c', 'extension.c', 'foreigncmds.c', 'functioncmds.c', diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index c025b1f9f8c..4d68d4d25c7 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "catalog/pg_type.h" #include "commands/createas.h" +#include "commands/explain_format.h" #include "commands/prepare.h" #include "funcapi.h" #include "nodes/nodeFuncs.h" diff --git a/src/include/commands/explain.h b/src/include/commands/explain.h index 570e7cad1fa..8dd87c47eec 100644 --- a/src/include/commands/explain.h +++ b/src/include/commands/explain.h @@ -120,30 +120,6 @@ extern void ExplainPrintJITSummary(ExplainState *es, QueryDesc *queryDesc); extern void ExplainQueryText(ExplainState *es, QueryDesc *queryDesc); extern void ExplainQueryParameters(ExplainState *es, ParamListInfo params, int maxlen); -extern void ExplainBeginOutput(ExplainState *es); -extern void ExplainEndOutput(ExplainState *es); -extern void ExplainSeparatePlans(ExplainState *es); - -extern void ExplainPropertyList(const char *qlabel, List *data, - ExplainState *es); -extern void ExplainPropertyListNested(const char *qlabel, List *data, - ExplainState *es); -extern void ExplainPropertyText(const char *qlabel, const char *value, - ExplainState *es); -extern void ExplainPropertyInteger(const char *qlabel, const char *unit, - int64 value, ExplainState *es); -extern void ExplainPropertyUInteger(const char *qlabel, const char *unit, - uint64 value, ExplainState *es); -extern void ExplainPropertyFloat(const char *qlabel, const char *unit, - double value, int ndigits, ExplainState *es); -extern void ExplainPropertyBool(const char *qlabel, bool value, - ExplainState *es); - -extern void ExplainOpenGroup(const char *objtype, const char *labelname, - bool labeled, ExplainState *es); -extern void ExplainCloseGroup(const char *objtype, const char *labelname, - bool labeled, ExplainState *es); - extern DestReceiver *CreateExplainSerializeDestReceiver(ExplainState *es); #endif /* EXPLAIN_H */ diff --git a/src/include/commands/explain_format.h b/src/include/commands/explain_format.h new file mode 100644 index 00000000000..0460f0fd2af --- /dev/null +++ b/src/include/commands/explain_format.h @@ -0,0 +1,52 @@ +/*------------------------------------------------------------------------- + * + * explain_format.h + * prototypes for explain_format.c + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994-5, Regents of the University of California + * + * src/include/commands/explain_format.h + * + *------------------------------------------------------------------------- + */ +#ifndef EXPLAIN_FORMAT_H +#define EXPLAIN_FORMAT_H + +#include "commands/explain.h" + +extern void ExplainPropertyList(const char *qlabel, List *data, + ExplainState *es); +extern void ExplainPropertyListNested(const char *qlabel, List *data, + ExplainState *es); +extern void ExplainPropertyText(const char *qlabel, const char *value, + ExplainState *es); +extern void ExplainPropertyInteger(const char *qlabel, const char *unit, + int64 value, ExplainState *es); +extern void ExplainPropertyUInteger(const char *qlabel, const char *unit, + uint64 value, ExplainState *es); +extern void ExplainPropertyFloat(const char *qlabel, const char *unit, + double value, int ndigits, ExplainState *es); +extern void ExplainPropertyBool(const char *qlabel, bool value, + ExplainState *es); + +extern void ExplainOpenGroup(const char *objtype, const char *labelname, + bool labeled, ExplainState *es); +extern void ExplainCloseGroup(const char *objtype, const char *labelname, + bool labeled, ExplainState *es); + +extern void ExplainOpenSetAsideGroup(const char *objtype, const char *labelname, + bool labeled, int depth, ExplainState *es); +extern void ExplainSaveGroup(ExplainState *es, int depth, int *state_save); +extern void ExplainRestoreGroup(ExplainState *es, int depth, int *state_save); + +extern void ExplainDummyGroup(const char *objtype, const char *labelname, + ExplainState *es); + +extern void ExplainBeginOutput(ExplainState *es); +extern void ExplainEndOutput(ExplainState *es); +extern void ExplainSeparatePlans(ExplainState *es); + +extern void ExplainIndentText(ExplainState *es); + +#endif