Extend EXPLAIN to support output in XML or JSON format.
authorTom Lane <tgl@sss.pgh.pa.us>
Mon, 10 Aug 2009 05:46:50 +0000 (05:46 +0000)
committerTom Lane <tgl@sss.pgh.pa.us>
Mon, 10 Aug 2009 05:46:50 +0000 (05:46 +0000)
There are probably still some adjustments to be made in the details
of the output, but this gets the basic structure in place.

Robert Haas

12 files changed:
contrib/auto_explain/auto_explain.c
doc/src/sgml/auto-explain.sgml
doc/src/sgml/ref/explain.sgml
src/backend/commands/explain.c
src/backend/commands/prepare.c
src/backend/utils/adt/xml.c
src/backend/utils/cache/lsyscache.c
src/backend/utils/sort/tuplesort.c
src/include/commands/explain.h
src/include/utils/lsyscache.h
src/include/utils/tuplesort.h
src/include/utils/xml.h

index eb2bc04aed9758b255a85f578e7ce4e1b3876564..6d3435be1f75f11a7a7b9c03622d0b706c03cf92 100644 (file)
@@ -6,7 +6,7 @@
  * Copyright (c) 2008-2009, PostgreSQL Global Development Group
  *
  * IDENTIFICATION
- *       $PostgreSQL: pgsql/contrib/auto_explain/auto_explain.c,v 1.6 2009/07/26 23:34:17 tgl Exp $
+ *       $PostgreSQL: pgsql/contrib/auto_explain/auto_explain.c,v 1.7 2009/08/10 05:46:49 tgl Exp $
  *
  *-------------------------------------------------------------------------
  */
@@ -22,8 +22,16 @@ PG_MODULE_MAGIC;
 static int     auto_explain_log_min_duration = -1; /* msec or -1 */
 static bool auto_explain_log_analyze = false;
 static bool auto_explain_log_verbose = false;
+static int     auto_explain_log_format = EXPLAIN_FORMAT_TEXT;
 static bool auto_explain_log_nested_statements = false;
 
+static const struct config_enum_entry format_options[] = {
+        {"text", EXPLAIN_FORMAT_TEXT, false},
+        {"xml", EXPLAIN_FORMAT_XML, false},
+        {"json", EXPLAIN_FORMAT_JSON, false},
+        {NULL, 0, false}
+};
+
 /* Current nesting depth of ExecutorRun calls */
 static int     nesting_level = 0;
 
@@ -84,6 +92,17 @@ _PG_init(void)
                                                         NULL,
                                                         NULL);
 
+       DefineCustomEnumVariable("auto_explain.log_format",
+                                                        "EXPLAIN format to be used for plan logging.",
+                                                        NULL,
+                                                        &auto_explain_log_format,
+                                                        EXPLAIN_FORMAT_TEXT,
+                                                        format_options,
+                                                        PGC_SUSET,
+                                                        0,
+                                                        NULL,
+                                                        NULL);
+
        DefineCustomBoolVariable("auto_explain.log_nested_statements",
                                                         "Log nested statements.",
                                                         NULL,
@@ -201,6 +220,7 @@ explain_ExecutorEnd(QueryDesc *queryDesc)
                        ExplainInitState(&es);
                        es.analyze = (queryDesc->doInstrument && auto_explain_log_analyze);
                        es.verbose = auto_explain_log_verbose;
+                       es.format = auto_explain_log_format;
 
                        ExplainPrintPlan(&es, queryDesc);
 
index c1e85af10e0f275152ad627e7d806704b8ad4298..72487f944ce6432168dbe2729d327b2b90ee6ad9 100644 (file)
@@ -1,4 +1,4 @@
-<!-- $PostgreSQL: pgsql/doc/src/sgml/auto-explain.sgml,v 1.3 2009/01/02 01:16:02 tgl Exp $ -->
+<!-- $PostgreSQL: pgsql/doc/src/sgml/auto-explain.sgml,v 1.4 2009/08/10 05:46:50 tgl Exp $ -->
 
 <sect1 id="auto-explain">
  <title>auto_explain</title>
@@ -102,6 +102,24 @@ LOAD 'auto_explain';
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+    <term>
+     <varname>auto_explain.log_format</varname> (<type>enum</type>)
+    </term>
+    <indexterm>
+     <primary><varname>auto_explain.log_format</> configuration parameter</primary>
+    </indexterm>
+    <listitem>
+     <para>
+      <varname>auto_explain.log_format</varname> selects the
+      <command>EXPLAIN</> output format to be used.
+      The allowed values are <literal>text</literal>, <literal>xml</literal>,
+      and <literal>json</literal>.  The default is text.
+      Only superusers can change this setting.
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry>
     <term>
      <varname>auto_explain.log_nested_statements</varname> (<type>boolean</type>)
index d3b5c979a29608dcba0f4e053869ea1a087b65d5..9670bd06f193a265fb3b53220e8daacf6e2306cf 100644 (file)
@@ -1,5 +1,5 @@
 <!--
-$PostgreSQL: pgsql/doc/src/sgml/ref/explain.sgml,v 1.45 2009/07/26 23:34:17 tgl Exp $
+$PostgreSQL: pgsql/doc/src/sgml/ref/explain.sgml,v 1.46 2009/08/10 05:46:50 tgl Exp $
 PostgreSQL documentation
 -->
 
@@ -31,7 +31,7 @@ PostgreSQL documentation
 
  <refsynopsisdiv>
 <synopsis>
-EXPLAIN [ ( { ANALYZE <replaceable class="parameter">boolean</replaceable> | VERBOSE <replaceable class="parameter">boolean</replaceable> | COSTS <replaceable class="parameter">boolean</replaceable> } [, ...] ) ] <replaceable class="parameter">statement</replaceable>
+EXPLAIN [ ( { ANALYZE <replaceable class="parameter">boolean</replaceable> | VERBOSE <replaceable class="parameter">boolean</replaceable> | COSTS <replaceable class="parameter">boolean</replaceable> | FORMAT { TEXT | XML | JSON } } [, ...] ) ] <replaceable class="parameter">statement</replaceable>
 EXPLAIN [ ANALYZE ] [ VERBOSE ] <replaceable class="parameter">statement</replaceable>
 </synopsis>
  </refsynopsisdiv>
@@ -109,7 +109,7 @@ ROLLBACK;
     <listitem>
      <para>
       Carry out the command and show the actual run times.  This
-      parameter defaults to <command>FALSE</command>.
+      parameter defaults to <literal>FALSE</literal>.
      </para>
     </listitem>
    </varlistentry>
@@ -118,8 +118,12 @@ ROLLBACK;
     <term><literal>VERBOSE</literal></term>
     <listitem>
      <para>
-      Include the output column list for each node in the plan tree.  This
-      parameter defaults to <command>FALSE</command>.
+      Display additional information regarding the plan.  Specifically, include
+      the output column list for each node in the plan tree, schema-qualify
+      table and function names, always label variables in expressions with
+      their range table alias, and always print the name of each trigger for
+      which statistics are displayed.  This parameter defaults to
+      <literal>FALSE</literal>.
      </para>
     </listitem>
    </varlistentry>
@@ -130,7 +134,19 @@ ROLLBACK;
      <para>
       Include information on the estimated startup and total cost of each
       plan node, as well as the estimated number of rows and the estimated
-      width of each row.  This parameter defaults to <command>TRUE</command>.
+      width of each row.  This parameter defaults to <literal>TRUE</literal>.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>FORMAT</literal></term>
+    <listitem>
+     <para>
+      Specify the output format, which can be TEXT, XML, or JSON.
+      XML or JSON output contains the same information as the text output
+      format, but is easier for programs to parse.  This parameter defaults to
+      <literal>TEXT</literal>.
      </para>
     </listitem>
    </varlistentry>
index 1388c8dd42fb8c5a31de0b567b63c79593af7ff0..d675d8d8171f028badacff4df86e19ad90e40659 100644 (file)
@@ -7,7 +7,7 @@
  * Portions Copyright (c) 1994-5, Regents of the University of California
  *
  * IDENTIFICATION
- *       $PostgreSQL: pgsql/src/backend/commands/explain.c,v 1.188 2009/07/26 23:34:17 tgl Exp $
+ *       $PostgreSQL: pgsql/src/backend/commands/explain.c,v 1.189 2009/08/10 05:46:50 tgl Exp $
  *
  *-------------------------------------------------------------------------
  */
@@ -32,6 +32,7 @@
 #include "utils/lsyscache.h"
 #include "utils/tuplesort.h"
 #include "utils/snapmgr.h"
+#include "utils/xml.h"
 
 
 /* Hook for plugins to get control in ExplainOneQuery() */
@@ -41,28 +42,60 @@ ExplainOneQuery_hook_type ExplainOneQuery_hook = NULL;
 explain_get_index_name_hook_type explain_get_index_name_hook = NULL;
 
 
+/* OR-able flags for ExplainXMLTag() */
+#define X_OPENING 0
+#define X_CLOSING 1
+#define X_CLOSE_IMMEDIATE 2
+#define X_NOWHITESPACE 4
+
 static void ExplainOneQuery(Query *query, ExplainState *es,
                                const char *queryString, ParamListInfo params);
 static void report_triggers(ResultRelInfo *rInfo, bool show_relname,
-                               StringInfo buf);
+                               ExplainState *es);
 static double elapsed_time(instr_time *starttime);
 static void ExplainNode(Plan *plan, PlanState *planstate,
-                               Plan *outer_plan, int indent, ExplainState *es);
-static void show_plan_tlist(Plan *plan, int indent, ExplainState *es);
+                               Plan *outer_plan,
+                               const char *relationship, const char *plan_name,
+                               ExplainState *es);
+static void show_plan_tlist(Plan *plan, ExplainState *es);
 static void show_qual(List *qual, const char *qlabel, Plan *plan,
-                 Plan *outer_plan, int indent, bool useprefix, ExplainState *es);
+                 Plan *outer_plan, bool useprefix, ExplainState *es);
 static void show_scan_qual(List *qual, const char *qlabel,
                           Plan *scan_plan, Plan *outer_plan,
-                          int indent, ExplainState *es);
+                          ExplainState *es);
 static void show_upper_qual(List *qual, const char *qlabel, Plan *plan,
-                               int indent, ExplainState *es);
-static void show_sort_keys(Plan *sortplan, int indent, ExplainState *es);
-static void show_sort_info(SortState *sortstate, int indent, ExplainState *es);
+                               ExplainState *es);
+static void show_sort_keys(Plan *sortplan, ExplainState *es);
+static void show_sort_info(SortState *sortstate, ExplainState *es);
 static const char *explain_get_index_name(Oid indexId);
 static void ExplainScanTarget(Scan *plan, ExplainState *es);
 static void ExplainMemberNodes(List *plans, PlanState **planstate,
-                                  Plan *outer_plan, int indent, ExplainState *es);
-static void ExplainSubPlans(List *plans, int indent, ExplainState *es);
+                                  Plan *outer_plan, ExplainState *es);
+static void ExplainSubPlans(List *plans, const char *relationship,
+                                                       ExplainState *es);
+static void ExplainPropertyList(const char *qlabel, List *data,
+                                                               ExplainState *es);
+static void ExplainProperty(const char *qlabel, const char *value,
+                                                       bool numeric, ExplainState *es);
+#define ExplainPropertyText(qlabel, value, es)  \
+       ExplainProperty(qlabel, value, false, es)
+static void ExplainPropertyInteger(const char *qlabel, int value,
+                                                                  ExplainState *es);
+static void ExplainPropertyLong(const char *qlabel, long value,
+                                                               ExplainState *es);
+static void ExplainPropertyFloat(const char *qlabel, double value, int ndigits,
+                                                                ExplainState *es);
+static void ExplainOpenGroup(const char *objtype, const char *labelname,
+                                bool labeled, ExplainState *es);
+static void ExplainCloseGroup(const char *objtype, const char *labelname,
+                                bool labeled, ExplainState *es);
+static void ExplainDummyGroup(const char *objtype, const char *labelname,
+                                                         ExplainState *es);
+static void ExplainBeginOutput(ExplainState *es);
+static void ExplainEndOutput(ExplainState *es);
+static void ExplainXMLTag(const char *tagname, int flags, ExplainState *es);
+static void ExplainJSONLineEnding(ExplainState *es);
+static void escape_json(StringInfo buf, const char *str);
 
 
 /*
@@ -94,6 +127,22 @@ ExplainQuery(ExplainStmt *stmt, const char *queryString,
                        es.verbose = defGetBoolean(opt);
                else if (strcmp(opt->defname, "costs") == 0)
                        es.costs = defGetBoolean(opt);
+               else if (strcmp(opt->defname, "format") == 0)
+               {
+                       char   *p = defGetString(opt);
+
+                       if (strcmp(p, "text") == 0)
+                               es.format = EXPLAIN_FORMAT_TEXT;
+                       else if (strcmp(p, "xml") == 0)
+                               es.format = EXPLAIN_FORMAT_XML;
+                       else if (strcmp(p, "json") == 0)
+                               es.format = EXPLAIN_FORMAT_JSON;
+                       else
+                               ereport(ERROR,
+                                       (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+                                        errmsg("unrecognized value for EXPLAIN option \"%s\": \"%s\"",
+                                                       opt->defname, p)));
+               }
                else
                        ereport(ERROR,
                                        (errcode(ERRCODE_SYNTAX_ERROR),
@@ -117,10 +166,17 @@ ExplainQuery(ExplainStmt *stmt, const char *queryString,
        rewritten = pg_analyze_and_rewrite((Node *) copyObject(stmt->query),
                                                                           queryString, param_types, num_params);
 
+       /* emit opening boilerplate */
+       ExplainBeginOutput(&es);
+
        if (rewritten == NIL)
        {
-               /* In the case of an INSTEAD NOTHING, tell at least that */
-               appendStringInfoString(es.str, "Query rewrites to nothing\n");
+               /*
+                * In the case of an INSTEAD NOTHING, tell at least that.  But in
+                * non-text format, the output is delimited, so this isn't necessary.
+                */
+               if (es.format == EXPLAIN_FORMAT_TEXT)
+                       appendStringInfoString(es.str, "Query rewrites to nothing\n");
        }
        else
        {
@@ -130,15 +186,23 @@ ExplainQuery(ExplainStmt *stmt, const char *queryString,
                foreach(l, rewritten)
                {
                        ExplainOneQuery((Query *) lfirst(l), &es, queryString, params);
-                       /* put a blank line between plans */
+
+                       /* Separate plans with an appropriate separator */
                        if (lnext(l) != NULL)
-                               appendStringInfoChar(es.str, '\n');
+                               ExplainSeparatePlans(&es);
                }
        }
 
+       /* emit closing boilerplate */
+       ExplainEndOutput(&es);
+       Assert(es.indent == 0);
+
        /* output tuples */
        tstate = begin_tup_output_tupdesc(dest, ExplainResultDesc(stmt));
-       do_text_output_multiline(tstate, es.str->data);
+       if (es.format == EXPLAIN_FORMAT_TEXT)
+               do_text_output_multiline(tstate, es.str->data);
+       else
+               do_text_output_oneline(tstate, es.str->data);
        end_tup_output(tstate);
 
        pfree(es.str->data);
@@ -165,11 +229,26 @@ TupleDesc
 ExplainResultDesc(ExplainStmt *stmt)
 {
        TupleDesc       tupdesc;
+       ListCell   *lc;
+       bool            xml = false;
 
-       /* need a tuple descriptor representing a single TEXT column */
+       /* Check for XML format option */
+       foreach(lc, stmt->options)
+       {
+               DefElem *opt = (DefElem *) lfirst(lc);
+
+               if (strcmp(opt->defname, "format") == 0)
+               {
+                       char   *p = defGetString(opt);
+
+                       xml = (strcmp(p, "xml") == 0);
+               }
+       }
+
+       /* Need a tuple descriptor representing a single TEXT or XML column */
        tupdesc = CreateTemplateTupleDesc(1, false);
        TupleDescInitEntry(tupdesc, (AttrNumber) 1, "QUERY PLAN",
-                                          TEXTOID, -1, 0);
+                                          xml ? XMLOID : TEXTOID, -1, 0);
        return tupdesc;
 }
 
@@ -223,10 +302,20 @@ ExplainOneUtility(Node *utilityStmt, ExplainState *es,
                ExplainExecuteQuery((ExecuteStmt *) utilityStmt, es,
                                                        queryString, params);
        else if (IsA(utilityStmt, NotifyStmt))
-               appendStringInfoString(es->str, "NOTIFY\n");
+       {
+               if (es->format == EXPLAIN_FORMAT_TEXT)
+                       appendStringInfoString(es->str, "NOTIFY\n");
+               else
+                       ExplainDummyGroup("Notify", NULL, es);
+       }
        else
-               appendStringInfoString(es->str,
+       {
+               if (es->format == EXPLAIN_FORMAT_TEXT)
+                       appendStringInfoString(es->str,
                                                           "Utility statements have no plan structure\n");
+               else
+                       ExplainDummyGroup("Utility Statement", NULL, es);
+       }
 }
 
 /*
@@ -288,6 +377,8 @@ ExplainOnePlan(PlannedStmt *plannedstmt, ExplainState *es,
                totaltime += elapsed_time(&starttime);
        }
 
+       ExplainOpenGroup("Query", NULL, true, es);
+
        /* Create textual dump of plan tree */
        ExplainPrintPlan(es, queryDesc);
 
@@ -313,16 +404,20 @@ ExplainOnePlan(PlannedStmt *plannedstmt, ExplainState *es,
                int                     nr;
                ListCell   *l;
 
+               ExplainOpenGroup("Triggers", "Triggers", false, es);
+
                show_relname = (numrels > 1 || targrels != NIL);
                rInfo = queryDesc->estate->es_result_relations;
                for (nr = 0; nr < numrels; rInfo++, nr++)
-                       report_triggers(rInfo, show_relname, es->str);
+                       report_triggers(rInfo, show_relname, es);
 
                foreach(l, targrels)
                {
                        rInfo = (ResultRelInfo *) lfirst(l);
-                       report_triggers(rInfo, show_relname, es->str);
+                       report_triggers(rInfo, show_relname, es);
                }
+
+               ExplainCloseGroup("Triggers", "Triggers", false, es);
        }
 
        /*
@@ -344,8 +439,16 @@ ExplainOnePlan(PlannedStmt *plannedstmt, ExplainState *es,
        totaltime += elapsed_time(&starttime);
 
        if (es->analyze)
-               appendStringInfo(es->str, "Total runtime: %.3f ms\n",
-                                                1000.0 * totaltime);
+       {
+               if (es->format == EXPLAIN_FORMAT_TEXT)
+                       appendStringInfo(es->str, "Total runtime: %.3f ms\n",
+                                                        1000.0 * totaltime);
+               else
+                       ExplainPropertyFloat("Total Runtime", 1000.0 * totaltime,
+                                                                3, es);
+       }
+
+       ExplainCloseGroup("Query", NULL, true, es);
 }
 
 /*
@@ -365,7 +468,7 @@ ExplainPrintPlan(ExplainState *es, QueryDesc *queryDesc)
        es->pstmt = queryDesc->plannedstmt;
        es->rtable = queryDesc->plannedstmt->rtable;
        ExplainNode(queryDesc->plannedstmt->planTree, queryDesc->planstate,
-                               NULL, 0, es);
+                               NULL, NULL, NULL, es);
 }
 
 /*
@@ -373,7 +476,7 @@ ExplainPrintPlan(ExplainState *es, QueryDesc *queryDesc)
  *             report execution stats for a single relation's triggers
  */
 static void
-report_triggers(ResultRelInfo *rInfo, bool show_relname, StringInfo buf)
+report_triggers(ResultRelInfo *rInfo, bool show_relname, ExplainState *es)
 {
        int                     nt;
 
@@ -383,7 +486,8 @@ report_triggers(ResultRelInfo *rInfo, bool show_relname, StringInfo buf)
        {
                Trigger    *trig = rInfo->ri_TrigDesc->triggers + nt;
                Instrumentation *instr = rInfo->ri_TrigInstrument + nt;
-               char       *conname;
+               char       *relname;
+               char       *conname = NULL;
 
                /* Must clean up instrumentation state */
                InstrEndLoop(instr);
@@ -395,21 +499,44 @@ report_triggers(ResultRelInfo *rInfo, bool show_relname, StringInfo buf)
                if (instr->ntuples == 0)
                        continue;
 
-               if (OidIsValid(trig->tgconstraint) &&
-                       (conname = get_constraint_name(trig->tgconstraint)) != NULL)
+               ExplainOpenGroup("Trigger", NULL, true, es);
+
+               relname = RelationGetRelationName(rInfo->ri_RelationDesc);
+               if (OidIsValid(trig->tgconstraint))
+                       conname = get_constraint_name(trig->tgconstraint);
+
+               /*
+                * In text format, we avoid printing both the trigger name and the
+                * constraint name unless VERBOSE is specified.  In non-text
+                * formats we just print everything.
+                */
+               if (es->format == EXPLAIN_FORMAT_TEXT)
                {
-                       appendStringInfo(buf, "Trigger for constraint %s", conname);
-                       pfree(conname);
+                       if (es->verbose || conname == NULL)
+                               appendStringInfo(es->str, "Trigger %s", trig->tgname);
+                       else
+                               appendStringInfoString(es->str, "Trigger");
+                       if (conname)
+                               appendStringInfo(es->str, " for constraint %s", conname);
+                       if (show_relname)
+                               appendStringInfo(es->str, " on %s", relname);
+                       appendStringInfo(es->str, ": time=%.3f calls=%.0f\n",
+                                                        1000.0 * instr->total, instr->ntuples);
                }
                else
-                       appendStringInfo(buf, "Trigger %s", trig->tgname);
+               {
+                       ExplainPropertyText("Trigger Name", trig->tgname, es);
+                       if (conname)
+                               ExplainPropertyText("Constraint Name", conname, es);
+                       ExplainPropertyText("Relation", relname, es);
+                       ExplainPropertyFloat("Time", 1000.0 * instr->total, 3, es);
+                       ExplainPropertyFloat("Calls", instr->ntuples, 0, es);
+               }
 
-               if (show_relname)
-                       appendStringInfo(buf, " on %s",
-                                                        RelationGetRelationName(rInfo->ri_RelationDesc));
+               if (conname)
+                       pfree(conname);
 
-               appendStringInfo(buf, ": time=%.3f calls=%.0f\n",
-                                                1000.0 * instr->total, instr->ntuples);
+               ExplainCloseGroup("Trigger", NULL, true, es);
        }
 }
 
@@ -426,7 +553,7 @@ elapsed_time(instr_time *starttime)
 
 /*
  * ExplainNode -
- *       converts a Plan node into ascii string and appends it to es->str
+ *       Appends a description of the Plan node to es->str
  *
  * planstate points to the executor state node corresponding to the plan node.
  * We need this to get at the instrumentation data (if any) as well as the
@@ -436,253 +563,222 @@ elapsed_time(instr_time *starttime)
  * side of a join with the current node.  This is only interesting for
  * deciphering runtime keys of an inner indexscan.
  *
- * If indent is positive, we indent the plan output accordingly and put "->"
- * in front of it.  This should only happen for child plan nodes.
+ * relationship describes the relationship of this plan node to its parent
+ * (eg, "Outer", "Inner"); it can be null at top level.  plan_name is an
+ * optional name to be attached to the node.
+ *
+ * In text format, es->indent is controlled in this function since we only
+ * want it to change at Plan-node boundaries.  In non-text formats, es->indent
+ * corresponds to the nesting depth of logical output groups, and therefore
+ * is controlled by ExplainOpenGroup/ExplainCloseGroup.
  */
 static void
 ExplainNode(Plan *plan, PlanState *planstate,
                        Plan *outer_plan,
-                       int indent, ExplainState *es)
+                       const char *relationship, const char *plan_name,
+                       ExplainState *es)
 {
-       const char *pname;
+       const char *pname;                      /* node type name for text output */
+       const char *sname;                      /* node type name for non-text output */
+       const char *strategy = NULL;
+       int                     save_indent = es->indent;
+       bool            haschildren;
 
-       if (indent)
-       {
-               Assert(indent >= 2);
-               appendStringInfoSpaces(es->str, 2 * indent - 4);
-               appendStringInfoString(es->str, "->  ");
-       }
-
-       if (plan == NULL)
-       {
-               appendStringInfoChar(es->str, '\n');
-               return;
-       }
+       Assert(plan);
 
        switch (nodeTag(plan))
        {
                case T_Result:
-                       pname = "Result";
+                       pname = sname = "Result";
                        break;
                case T_Append:
-                       pname = "Append";
+                       pname = sname = "Append";
                        break;
                case T_RecursiveUnion:
-                       pname = "Recursive Union";
+                       pname = sname = "Recursive Union";
                        break;
                case T_BitmapAnd:
-                       pname = "BitmapAnd";
+                       pname = sname = "BitmapAnd";
                        break;
                case T_BitmapOr:
-                       pname = "BitmapOr";
+                       pname = sname = "BitmapOr";
                        break;
                case T_NestLoop:
-                       switch (((NestLoop *) plan)->join.jointype)
-                       {
-                               case JOIN_INNER:
-                                       pname = "Nested Loop";
-                                       break;
-                               case JOIN_LEFT:
-                                       pname = "Nested Loop Left Join";
-                                       break;
-                               case JOIN_FULL:
-                                       pname = "Nested Loop Full Join";
-                                       break;
-                               case JOIN_RIGHT:
-                                       pname = "Nested Loop Right Join";
-                                       break;
-                               case JOIN_SEMI:
-                                       pname = "Nested Loop Semi Join";
-                                       break;
-                               case JOIN_ANTI:
-                                       pname = "Nested Loop Anti Join";
-                                       break;
-                               default:
-                                       pname = "Nested Loop ??? Join";
-                                       break;
-                       }
+                       pname = sname = "Nested Loop";
                        break;
                case T_MergeJoin:
-                       switch (((MergeJoin *) plan)->join.jointype)
-                       {
-                               case JOIN_INNER:
-                                       pname = "Merge Join";
-                                       break;
-                               case JOIN_LEFT:
-                                       pname = "Merge Left Join";
-                                       break;
-                               case JOIN_FULL:
-                                       pname = "Merge Full Join";
-                                       break;
-                               case JOIN_RIGHT:
-                                       pname = "Merge Right Join";
-                                       break;
-                               case JOIN_SEMI:
-                                       pname = "Merge Semi Join";
-                                       break;
-                               case JOIN_ANTI:
-                                       pname = "Merge Anti Join";
-                                       break;
-                               default:
-                                       pname = "Merge ??? Join";
-                                       break;
-                       }
+                       pname = "Merge";                /* "Join" gets added by jointype switch */
+                       sname = "Merge Join";
                        break;
                case T_HashJoin:
-                       switch (((HashJoin *) plan)->join.jointype)
-                       {
-                               case JOIN_INNER:
-                                       pname = "Hash Join";
-                                       break;
-                               case JOIN_LEFT:
-                                       pname = "Hash Left Join";
-                                       break;
-                               case JOIN_FULL:
-                                       pname = "Hash Full Join";
-                                       break;
-                               case JOIN_RIGHT:
-                                       pname = "Hash Right Join";
-                                       break;
-                               case JOIN_SEMI:
-                                       pname = "Hash Semi Join";
-                                       break;
-                               case JOIN_ANTI:
-                                       pname = "Hash Anti Join";
-                                       break;
-                               default:
-                                       pname = "Hash ??? Join";
-                                       break;
-                       }
+                       pname = "Hash";                 /* "Join" gets added by jointype switch */
+                       sname = "Hash Join";
                        break;
                case T_SeqScan:
-                       pname = "Seq Scan";
+                       pname = sname = "Seq Scan";
                        break;
                case T_IndexScan:
-                       pname = "Index Scan";
+                       pname = sname = "Index Scan";
                        break;
                case T_BitmapIndexScan:
-                       pname = "Bitmap Index Scan";
+                       pname = sname = "Bitmap Index Scan";
                        break;
                case T_BitmapHeapScan:
-                       pname = "Bitmap Heap Scan";
+                       pname = sname = "Bitmap Heap Scan";
                        break;
                case T_TidScan:
-                       pname = "Tid Scan";
+                       pname = sname = "Tid Scan";
                        break;
                case T_SubqueryScan:
-                       pname = "Subquery Scan";
+                       pname = sname = "Subquery Scan";
                        break;
                case T_FunctionScan:
-                       pname = "Function Scan";
+                       pname = sname = "Function Scan";
                        break;
                case T_ValuesScan:
-                       pname = "Values Scan";
+                       pname = sname = "Values Scan";
                        break;
                case T_CteScan:
-                       pname = "CTE Scan";
+                       pname = sname = "CTE Scan";
                        break;
                case T_WorkTableScan:
-                       pname = "WorkTable Scan";
+                       pname = sname = "WorkTable Scan";
                        break;
                case T_Material:
-                       pname = "Materialize";
+                       pname = sname = "Materialize";
                        break;
                case T_Sort:
-                       pname = "Sort";
+                       pname = sname = "Sort";
                        break;
                case T_Group:
-                       pname = "Group";
+                       pname = sname = "Group";
                        break;
                case T_Agg:
+                       sname = "Aggregate";
                        switch (((Agg *) plan)->aggstrategy)
                        {
                                case AGG_PLAIN:
                                        pname = "Aggregate";
+                                       strategy = "Plain";
                                        break;
                                case AGG_SORTED:
                                        pname = "GroupAggregate";
+                                       strategy = "Sorted";
                                        break;
                                case AGG_HASHED:
                                        pname = "HashAggregate";
+                                       strategy = "Hashed";
                                        break;
                                default:
                                        pname = "Aggregate ???";
+                                       strategy = "???";
                                        break;
                        }
                        break;
                case T_WindowAgg:
-                       pname = "WindowAgg";
+                       pname = sname = "WindowAgg";
                        break;
                case T_Unique:
-                       pname = "Unique";
+                       pname = sname = "Unique";
                        break;
                case T_SetOp:
+                       sname = "SetOp";
                        switch (((SetOp *) plan)->strategy)
                        {
                                case SETOP_SORTED:
-                                       switch (((SetOp *) plan)->cmd)
-                                       {
-                                               case SETOPCMD_INTERSECT:
-                                                       pname = "SetOp Intersect";
-                                                       break;
-                                               case SETOPCMD_INTERSECT_ALL:
-                                                       pname = "SetOp Intersect All";
-                                                       break;
-                                               case SETOPCMD_EXCEPT:
-                                                       pname = "SetOp Except";
-                                                       break;
-                                               case SETOPCMD_EXCEPT_ALL:
-                                                       pname = "SetOp Except All";
-                                                       break;
-                                               default:
-                                                       pname = "SetOp ???";
-                                                       break;
-                                       }
+                                       pname = "SetOp";
+                                       strategy = "Sorted";
                                        break;
                                case SETOP_HASHED:
-                                       switch (((SetOp *) plan)->cmd)
-                                       {
-                                               case SETOPCMD_INTERSECT:
-                                                       pname = "HashSetOp Intersect";
-                                                       break;
-                                               case SETOPCMD_INTERSECT_ALL:
-                                                       pname = "HashSetOp Intersect All";
-                                                       break;
-                                               case SETOPCMD_EXCEPT:
-                                                       pname = "HashSetOp Except";
-                                                       break;
-                                               case SETOPCMD_EXCEPT_ALL:
-                                                       pname = "HashSetOp Except All";
-                                                       break;
-                                               default:
-                                                       pname = "HashSetOp ???";
-                                                       break;
-                                       }
+                                       pname = "HashSetOp";
+                                       strategy = "Hashed";
                                        break;
                                default:
                                        pname = "SetOp ???";
+                                       strategy = "???";
                                        break;
                        }
                        break;
                case T_Limit:
-                       pname = "Limit";
+                       pname = sname = "Limit";
                        break;
                case T_Hash:
-                       pname = "Hash";
+                       pname = sname = "Hash";
                        break;
                default:
-                       pname = "???";
+                       pname = sname = "???";
                        break;
        }
 
-       appendStringInfoString(es->str, pname);
+       ExplainOpenGroup("Plan",
+                                        relationship ? NULL : "Plan",
+                                        true, es);
+
+       if (es->format == EXPLAIN_FORMAT_TEXT)
+       {
+               if (plan_name)
+               {
+                       appendStringInfoSpaces(es->str, es->indent * 2);
+                       appendStringInfo(es->str, "%s\n", plan_name);
+                       es->indent++;
+               }
+               if (es->indent)
+               {
+                       appendStringInfoSpaces(es->str, es->indent * 2);
+                       appendStringInfoString(es->str, "->  ");
+                       es->indent += 2;
+               }
+               appendStringInfoString(es->str, pname);
+               es->indent++;
+       }
+       else
+       {
+               ExplainPropertyText("Node Type", sname, es);
+               if (strategy)
+                       ExplainPropertyText("Strategy", strategy, es);
+               if (relationship)
+                       ExplainPropertyText("Parent Relationship", relationship, es);
+               if (plan_name)
+                       ExplainPropertyText("Subplan Name", plan_name, es);
+       }
+
        switch (nodeTag(plan))
        {
                case T_IndexScan:
-                       if (ScanDirectionIsBackward(((IndexScan *) plan)->indexorderdir))
-                               appendStringInfoString(es->str, " Backward");
-                       appendStringInfo(es->str, " using %s",
-                                         explain_get_index_name(((IndexScan *) plan)->indexid));
+                       {
+                               IndexScan *indexscan = (IndexScan *) plan;
+                               const char *indexname =
+                                       explain_get_index_name(indexscan->indexid);
+
+                               if (es->format == EXPLAIN_FORMAT_TEXT)
+                               {
+                                       if (ScanDirectionIsBackward(indexscan->indexorderdir))
+                                               appendStringInfoString(es->str, " Backward");
+                                       appendStringInfo(es->str, " using %s", indexname);
+                               }
+                               else
+                               {
+                                       const char *scandir;
+
+                                       switch (indexscan->indexorderdir)
+                                       {
+                                               case BackwardScanDirection:
+                                                       scandir = "Backward";
+                                                       break;
+                                               case NoMovementScanDirection:
+                                                       scandir = "NoMovement";
+                                                       break;
+                                               case ForwardScanDirection:
+                                                       scandir = "Forward";
+                                                       break;
+                                               default:
+                                                       scandir = "???";
+                                                       break;
+                                       }
+                                       ExplainPropertyText("Scan Direction", scandir, es);
+                                       ExplainPropertyText("Index Name", indexname, es);
+                               }
+                       }
                        /* FALL THRU */
                case T_SeqScan:
                case T_BitmapHeapScan:
@@ -695,17 +791,110 @@ ExplainNode(Plan *plan, PlanState *planstate,
                        ExplainScanTarget((Scan *) plan, es);
                        break;
                case T_BitmapIndexScan:
-                       appendStringInfo(es->str, " on %s",
-                               explain_get_index_name(((BitmapIndexScan *) plan)->indexid));
+                       {
+                               BitmapIndexScan *bitmapindexscan = (BitmapIndexScan *) plan;
+                               const char *indexname =
+                                       explain_get_index_name(bitmapindexscan->indexid);
+
+                               if (es->format == EXPLAIN_FORMAT_TEXT)
+                                       appendStringInfo(es->str, " on %s", indexname);
+                               else
+                                       ExplainPropertyText("Index Name", indexname, es);
+                       }
+                       break;
+               case T_NestLoop:
+               case T_MergeJoin:
+               case T_HashJoin:
+                       {
+                               const char *jointype;
+
+                               switch (((Join *) plan)->jointype)
+                               {
+                                       case JOIN_INNER:
+                                               jointype = "Inner";
+                                               break;
+                                       case JOIN_LEFT:
+                                               jointype = "Left";
+                                               break;
+                                       case JOIN_FULL:
+                                               jointype = "Full";
+                                               break;
+                                       case JOIN_RIGHT:
+                                               jointype = "Right";
+                                               break;
+                                       case JOIN_SEMI:
+                                               jointype = "Semi";
+                                               break;
+                                       case JOIN_ANTI:
+                                               jointype = "Anti";
+                                               break;
+                                       default:
+                                               jointype = "???";
+                                               break;
+                               }
+                               if (es->format == EXPLAIN_FORMAT_TEXT)
+                               {
+                                       /*
+                                        * For historical reasons, the join type is interpolated
+                                        * into the node type name...
+                                        */
+                                       if (((Join *) plan)->jointype != JOIN_INNER)
+                                               appendStringInfo(es->str, " %s Join", jointype);
+                                       else if (!IsA(plan, NestLoop))
+                                               appendStringInfo(es->str, " Join");
+                               }
+                               else
+                                       ExplainPropertyText("Join Type", jointype, es);
+                       }
+                       break;
+               case T_SetOp:
+                       {
+                               const char *setopcmd;
+
+                               switch (((SetOp *) plan)->cmd)
+                               {
+                                       case SETOPCMD_INTERSECT:
+                                               setopcmd = "Intersect";
+                                               break;
+                                       case SETOPCMD_INTERSECT_ALL:
+                                               setopcmd = "Intersect All";
+                                               break;
+                                       case SETOPCMD_EXCEPT:
+                                               setopcmd = "Except";
+                                               break;
+                                       case SETOPCMD_EXCEPT_ALL:
+                                               setopcmd = "Except All";
+                                               break;
+                                       default:
+                                               setopcmd = "???";
+                                               break;
+                               }
+                               if (es->format == EXPLAIN_FORMAT_TEXT)
+                                       appendStringInfo(es->str, " %s", setopcmd);
+                               else
+                                       ExplainPropertyText("Command", setopcmd, es);
+                       }
                        break;
                default:
                        break;
        }
 
        if (es->costs)
-               appendStringInfo(es->str, "  (cost=%.2f..%.2f rows=%.0f width=%d)",
-                                                plan->startup_cost, plan->total_cost,
-                                                plan->plan_rows, plan->plan_width);
+       {
+               if (es->format == EXPLAIN_FORMAT_TEXT)
+               {
+                       appendStringInfo(es->str, "  (cost=%.2f..%.2f rows=%.0f width=%d)",
+                                                        plan->startup_cost, plan->total_cost,
+                                                        plan->plan_rows, plan->plan_width);
+               }
+               else
+               {
+                       ExplainPropertyFloat("Startup Cost", plan->startup_cost, 2, es);
+                       ExplainPropertyFloat("Total Cost", plan->total_cost, 2, es);
+                       ExplainPropertyFloat("Plan Rows", plan->plan_rows, 0, es);
+                       ExplainPropertyInteger("Plan Width", plan->plan_width, es);
+               }
+       }
 
        /*
         * We have to forcibly clean up the instrumentation state because we
@@ -717,38 +906,60 @@ ExplainNode(Plan *plan, PlanState *planstate,
        if (planstate->instrument && planstate->instrument->nloops > 0)
        {
                double          nloops = planstate->instrument->nloops;
+               double          startup_sec = 1000.0 * planstate->instrument->startup / nloops;
+               double          total_sec = 1000.0 * planstate->instrument->total / nloops;
+               double          rows = planstate->instrument->ntuples / nloops;
 
-               appendStringInfo(es->str,
-                                                " (actual time=%.3f..%.3f rows=%.0f loops=%.0f)",
-                                                1000.0 * planstate->instrument->startup / nloops,
-                                                1000.0 * planstate->instrument->total / nloops,
-                                                planstate->instrument->ntuples / nloops,
-                                                planstate->instrument->nloops);
+               if (es->format == EXPLAIN_FORMAT_TEXT)
+               {
+                       appendStringInfo(es->str,
+                                                        " (actual time=%.3f..%.3f rows=%.0f loops=%.0f)",
+                                                        startup_sec, total_sec, rows, nloops);
+               }
+               else
+               {
+                       ExplainPropertyFloat("Actual Startup Time", startup_sec, 3, es);
+                       ExplainPropertyFloat("Actual Total Time", total_sec, 3, es);
+                       ExplainPropertyFloat("Actual Rows", rows, 0, es);
+                       ExplainPropertyFloat("Actual Loops", nloops, 0, es);
+               }
        }
        else if (es->analyze)
-               appendStringInfoString(es->str, " (never executed)");
-       appendStringInfoChar(es->str, '\n');
+       {
+               if (es->format == EXPLAIN_FORMAT_TEXT)
+                       appendStringInfo(es->str, " (never executed)");
+               else
+               {
+                       ExplainPropertyFloat("Actual Startup Time", 0.0, 3, es);
+                       ExplainPropertyFloat("Actual Total Time", 0.0, 3, es);
+                       ExplainPropertyFloat("Actual Rows", 0.0, 0, es);
+                       ExplainPropertyFloat("Actual Loops", 0.0, 0, es);
+               }
+       }
+
+       /* in text format, first line ends here */
+       if (es->format == EXPLAIN_FORMAT_TEXT)
+               appendStringInfoChar(es->str, '\n');
 
        /* target list */
        if (es->verbose)
-               show_plan_tlist(plan, indent, es);
+               show_plan_tlist(plan, es);
 
        /* quals, sort keys, etc */
        switch (nodeTag(plan))
        {
                case T_IndexScan:
                        show_scan_qual(((IndexScan *) plan)->indexqualorig,
-                                                  "Index Cond", plan, outer_plan, indent, es);
-                       show_scan_qual(plan->qual,
-                                                  "Filter", plan, outer_plan, indent, es);
+                                                  "Index Cond", plan, outer_plan, es);
+                       show_scan_qual(plan->qual, "Filter", plan, outer_plan, es);
                        break;
                case T_BitmapIndexScan:
                        show_scan_qual(((BitmapIndexScan *) plan)->indexqualorig,
-                                                  "Index Cond", plan, outer_plan, indent, es);
+                                                  "Index Cond", plan, outer_plan, es);
                        break;
                case T_BitmapHeapScan:
                        show_scan_qual(((BitmapHeapScan *) plan)->bitmapqualorig,
-                                                  "Recheck Cond", plan, outer_plan, indent, es);
+                                                  "Recheck Cond", plan, outer_plan, es);
                        /* FALL THRU */
                case T_SeqScan:
                case T_FunctionScan:
@@ -756,8 +967,7 @@ ExplainNode(Plan *plan, PlanState *planstate,
                case T_CteScan:
                case T_WorkTableScan:
                case T_SubqueryScan:
-                       show_scan_qual(plan->qual,
-                                                  "Filter", plan, outer_plan, indent, es);
+                       show_scan_qual(plan->qual, "Filter", plan, outer_plan, es);
                        break;
                case T_TidScan:
                        {
@@ -769,51 +979,61 @@ ExplainNode(Plan *plan, PlanState *planstate,
 
                                if (list_length(tidquals) > 1)
                                        tidquals = list_make1(make_orclause(tidquals));
-                               show_scan_qual(tidquals,
-                                                          "TID Cond", plan, outer_plan, indent, es);
-                               show_scan_qual(plan->qual,
-                                                          "Filter", plan, outer_plan, indent, es);
+                               show_scan_qual(tidquals, "TID Cond", plan, outer_plan, es);
+                               show_scan_qual(plan->qual, "Filter", plan, outer_plan, es);
                        }
                        break;
                case T_NestLoop:
                        show_upper_qual(((NestLoop *) plan)->join.joinqual,
-                                                       "Join Filter", plan, indent, es);
-                       show_upper_qual(plan->qual, "Filter", plan, indent, es);
+                                                       "Join Filter", plan, es);
+                       show_upper_qual(plan->qual, "Filter", plan, es);
                        break;
                case T_MergeJoin:
                        show_upper_qual(((MergeJoin *) plan)->mergeclauses,
-                                                       "Merge Cond", plan, indent, es);
+                                                       "Merge Cond", plan, es);
                        show_upper_qual(((MergeJoin *) plan)->join.joinqual,
-                                                       "Join Filter", plan, indent, es);
-                       show_upper_qual(plan->qual, "Filter", plan, indent, es);
+                                                       "Join Filter", plan, es);
+                       show_upper_qual(plan->qual, "Filter", plan, es);
                        break;
                case T_HashJoin:
                        show_upper_qual(((HashJoin *) plan)->hashclauses,
-                                                       "Hash Cond", plan, indent, es);
+                                                       "Hash Cond", plan, es);
                        show_upper_qual(((HashJoin *) plan)->join.joinqual,
-                                                       "Join Filter", plan, indent, es);
-                       show_upper_qual(plan->qual, "Filter", plan, indent, es);
+                                                       "Join Filter", plan, es);
+                       show_upper_qual(plan->qual, "Filter", plan, es);
                        break;
                case T_Agg:
                case T_Group:
-                       show_upper_qual(plan->qual, "Filter", plan, indent, es);
+                       show_upper_qual(plan->qual, "Filter", plan, es);
                        break;
                case T_Sort:
-                       show_sort_keys(plan, indent, es);
-                       show_sort_info((SortState *) planstate, indent, es);
+                       show_sort_keys(plan, es);
+                       show_sort_info((SortState *) planstate, es);
                        break;
                case T_Result:
                        show_upper_qual((List *) ((Result *) plan)->resconstantqual,
-                                                       "One-Time Filter", plan, indent, es);
-                       show_upper_qual(plan->qual, "Filter", plan, indent, es);
+                                                       "One-Time Filter", plan, es);
+                       show_upper_qual(plan->qual, "Filter", plan, es);
                        break;
                default:
                        break;
        }
 
+       /* Get ready to display the child plans */
+       haschildren = plan->initPlan ||
+               outerPlan(plan) ||
+               innerPlan(plan) ||
+               IsA(plan, Append) ||
+               IsA(plan, BitmapAnd) ||
+               IsA(plan, BitmapOr) ||
+               IsA(plan, SubqueryScan) ||
+               planstate->subPlan;
+       if (haschildren)
+               ExplainOpenGroup("Plans", "Plans", false, es);
+
        /* initPlan-s */
        if (plan->initPlan)
-               ExplainSubPlans(planstate->initPlan, indent, es);
+               ExplainSubPlans(planstate->initPlan, "InitPlan", es);
 
        /* lefttree */
        if (outerPlan(plan))
@@ -825,14 +1045,15 @@ ExplainNode(Plan *plan, PlanState *planstate,
                 */
                ExplainNode(outerPlan(plan), outerPlanState(planstate),
                                        IsA(plan, BitmapHeapScan) ? outer_plan : NULL,
-                                       indent + 3, es);
+                                       "Outer", NULL, es);
        }
 
        /* righttree */
        if (innerPlan(plan))
        {
                ExplainNode(innerPlan(plan), innerPlanState(planstate),
-                                       outerPlan(plan), indent + 3, es);
+                                       outerPlan(plan),
+                                       "Inner", NULL, es);
        }
 
        /* special child plans */
@@ -841,17 +1062,17 @@ ExplainNode(Plan *plan, PlanState *planstate,
                case T_Append:
                        ExplainMemberNodes(((Append *) plan)->appendplans,
                                                           ((AppendState *) planstate)->appendplans,
-                                                          outer_plan, indent, es);
+                                                          outer_plan, es);
                        break;
                case T_BitmapAnd:
                        ExplainMemberNodes(((BitmapAnd *) plan)->bitmapplans,
                                                           ((BitmapAndState *) planstate)->bitmapplans,
-                                                          outer_plan, indent, es);
+                                                          outer_plan, es);
                        break;
                case T_BitmapOr:
                        ExplainMemberNodes(((BitmapOr *) plan)->bitmapplans,
                                                           ((BitmapOrState *) planstate)->bitmapplans,
-                                                          outer_plan, indent, es);
+                                                          outer_plan, es);
                        break;
                case T_SubqueryScan:
                        {
@@ -859,7 +1080,8 @@ ExplainNode(Plan *plan, PlanState *planstate,
                                SubqueryScanState *subquerystate = (SubqueryScanState *) planstate;
 
                                ExplainNode(subqueryscan->subplan, subquerystate->subplan,
-                                                       NULL, indent + 3, es);
+                                                       NULL,
+                                                       "Subquery", NULL, es);
                        }
                        break;
                default:
@@ -868,16 +1090,29 @@ ExplainNode(Plan *plan, PlanState *planstate,
 
        /* subPlan-s */
        if (planstate->subPlan)
-               ExplainSubPlans(planstate->subPlan, indent, es);
+               ExplainSubPlans(planstate->subPlan, "SubPlan", es);
+
+       /* end of child plans */
+       if (haschildren)
+               ExplainCloseGroup("Plans", "Plans", false, es);
+
+       /* in text format, undo whatever indentation we added */
+       if (es->format == EXPLAIN_FORMAT_TEXT)
+               es->indent = save_indent;
+
+       ExplainCloseGroup("Plan",
+                                         relationship ? NULL : "Plan",
+                                         true, es);
 }
 
 /*
  * Show the targetlist of a plan node
  */
 static void
-show_plan_tlist(Plan *plan, int indent, ExplainState *es)
+show_plan_tlist(Plan *plan, ExplainState *es)
 {
        List       *context;
+       List       *result = NIL;
        bool            useprefix;
        ListCell   *lc;
        int                     i;
@@ -899,10 +1134,6 @@ show_plan_tlist(Plan *plan, int indent, ExplainState *es)
                                                                           es->pstmt->subplans);
        useprefix = list_length(es->rtable) > 1;
 
-       /* Emit line prefix */
-       appendStringInfoSpaces(es->str, indent * 2);
-       appendStringInfoString(es->str, "  Output: ");
-
        /* Deparse each non-junk result column */
        i = 0;
        foreach(lc, plan->targetlist)
@@ -911,14 +1142,13 @@ show_plan_tlist(Plan *plan, int indent, ExplainState *es)
 
                if (tle->resjunk)
                        continue;
-               if (i++ > 0)
-                       appendStringInfoString(es->str, ", ");
-               appendStringInfoString(es->str,
-                                                          deparse_expression((Node *) tle->expr, context,
+               result = lappend(result,
+                                            deparse_expression((Node *) tle->expr, context,
                                                                                                  useprefix, false));
        }
 
-       appendStringInfoChar(es->str, '\n');
+       /* Print results */
+       ExplainPropertyList("Output", result, es);
 }
 
 /*
@@ -929,7 +1159,7 @@ show_plan_tlist(Plan *plan, int indent, ExplainState *es)
  */
 static void
 show_qual(List *qual, const char *qlabel, Plan *plan, Plan *outer_plan,
-                 int indent, bool useprefix, ExplainState *es)
+                 bool useprefix, ExplainState *es)
 {
        List       *context;
        Node       *node;
@@ -952,8 +1182,7 @@ show_qual(List *qual, const char *qlabel, Plan *plan, Plan *outer_plan,
        exprstr = deparse_expression(node, context, useprefix, false);
 
        /* And add to es->str */
-       appendStringInfoSpaces(es->str, indent * 2);
-       appendStringInfo(es->str, "  %s: %s\n", qlabel, exprstr);
+       ExplainPropertyText(qlabel, exprstr, es);
 }
 
 /*
@@ -962,36 +1191,37 @@ show_qual(List *qual, const char *qlabel, Plan *plan, Plan *outer_plan,
 static void
 show_scan_qual(List *qual, const char *qlabel,
                           Plan *scan_plan, Plan *outer_plan,
-                          int indent, ExplainState *es)
+                          ExplainState *es)
 {
        bool            useprefix;
 
-       useprefix = (outer_plan != NULL || IsA(scan_plan, SubqueryScan));
-       show_qual(qual, qlabel, scan_plan, outer_plan, indent, useprefix, es);
+       useprefix = (outer_plan != NULL || IsA(scan_plan, SubqueryScan) ||
+                                es->verbose);
+       show_qual(qual, qlabel, scan_plan, outer_plan, useprefix, es);
 }
 
 /*
  * Show a qualifier expression for an upper-level plan node
  */
 static void
-show_upper_qual(List *qual, const char *qlabel, Plan *plan,
-                               int indent, ExplainState *es)
+show_upper_qual(List *qual, const char *qlabel, Plan *plan, ExplainState *es)
 {
        bool            useprefix;
 
-       useprefix = (list_length(es->rtable) > 1);
-       show_qual(qual, qlabel, plan, NULL, indent, useprefix, es);
+       useprefix = (list_length(es->rtable) > 1 || es->verbose);
+       show_qual(qual, qlabel, plan, NULL, useprefix, es);
 }
 
 /*
  * Show the sort keys for a Sort node.
  */
 static void
-show_sort_keys(Plan *sortplan, int indent, ExplainState *es)
+show_sort_keys(Plan *sortplan, ExplainState *es)
 {
        int                     nkeys = ((Sort *) sortplan)->numCols;
        AttrNumber *keycols = ((Sort *) sortplan)->sortColIdx;
        List       *context;
+       List       *result = NIL;
        bool            useprefix;
        int                     keyno;
        char       *exprstr;
@@ -999,15 +1229,12 @@ show_sort_keys(Plan *sortplan, int indent, ExplainState *es)
        if (nkeys <= 0)
                return;
 
-       appendStringInfoSpaces(es->str, indent * 2);
-       appendStringInfoString(es->str, "  Sort Key: ");
-
        /* Set up deparsing context */
        context = deparse_context_for_plan((Node *) sortplan,
                                                                           NULL,
                                                                           es->rtable,
                                                                           es->pstmt->subplans);
-       useprefix = list_length(es->rtable) > 1;
+       useprefix = (list_length(es->rtable) > 1 || es->verbose);
 
        for (keyno = 0; keyno < nkeys; keyno++)
        {
@@ -1020,31 +1247,41 @@ show_sort_keys(Plan *sortplan, int indent, ExplainState *es)
                /* Deparse the expression, showing any top-level cast */
                exprstr = deparse_expression((Node *) target->expr, context,
                                                                         useprefix, true);
-               /* And add to es->str */
-               if (keyno > 0)
-                       appendStringInfoString(es->str, ", ");
-               appendStringInfoString(es->str, exprstr);
+               result = lappend(result, exprstr);
        }
 
-       appendStringInfoChar(es->str, '\n');
+       ExplainPropertyList("Sort Key", result, es);
 }
 
 /*
- * If it's EXPLAIN ANALYZE, show tuplesort explain info for a sort node
+ * If it's EXPLAIN ANALYZE, show tuplesort stats for a sort node
  */
 static void
-show_sort_info(SortState *sortstate, int indent, ExplainState *es)
+show_sort_info(SortState *sortstate, ExplainState *es)
 {
        Assert(IsA(sortstate, SortState));
        if (es->analyze && sortstate->sort_Done &&
                sortstate->tuplesortstate != NULL)
        {
-               char       *sortinfo;
+               Tuplesortstate  *state = (Tuplesortstate *) sortstate->tuplesortstate;
+               const char *sortMethod;
+               const char *spaceType;
+               long            spaceUsed;
 
-               sortinfo = tuplesort_explain((Tuplesortstate *) sortstate->tuplesortstate);
-               appendStringInfoSpaces(es->str, indent * 2);
-               appendStringInfo(es->str, "  %s\n", sortinfo);
-               pfree(sortinfo);
+               tuplesort_get_stats(state, &sortMethod, &spaceType, &spaceUsed);
+
+               if (es->format == EXPLAIN_FORMAT_TEXT)
+               {
+                       appendStringInfoSpaces(es->str, es->indent * 2);
+                       appendStringInfo(es->str, "Sort Method:  %s  %s: %ldkB\n",
+                                                        sortMethod, spaceType, spaceUsed);
+               }
+               else
+               {
+                       ExplainPropertyText("Sort Method", sortMethod, es);
+                       ExplainPropertyLong("Sort Space Used", spaceUsed, es);
+                       ExplainPropertyText("Sort Space Type", spaceType, es);
+               }
        }
 }
 
@@ -1081,6 +1318,8 @@ static void
 ExplainScanTarget(Scan *plan, ExplainState *es)
 {
        char       *objectname = NULL;
+       char       *namespace = NULL;
+       const char *objecttag = NULL;
        RangeTblEntry *rte;
 
        if (plan->scanrelid <= 0)       /* Is this still possible? */
@@ -1096,6 +1335,9 @@ ExplainScanTarget(Scan *plan, ExplainState *es)
                        /* Assert it's on a real relation */
                        Assert(rte->rtekind == RTE_RELATION);
                        objectname = get_rel_name(rte->relid);
+                       if (es->verbose)
+                               namespace = get_namespace_name(get_rel_namespace(rte->relid));
+                       objecttag = "Relation Name";
                        break;
                case T_FunctionScan:
                        {
@@ -1116,7 +1358,11 @@ ExplainScanTarget(Scan *plan, ExplainState *es)
                                        Oid                     funcid = ((FuncExpr *) funcexpr)->funcid;
 
                                        objectname = get_func_name(funcid);
+                                       if (es->verbose)
+                                               namespace =
+                                                       get_namespace_name(get_func_namespace(funcid));
                                }
+                               objecttag = "Function Name";
                        }
                        break;
                case T_ValuesScan:
@@ -1127,23 +1373,40 @@ ExplainScanTarget(Scan *plan, ExplainState *es)
                        Assert(rte->rtekind == RTE_CTE);
                        Assert(!rte->self_reference);
                        objectname = rte->ctename;
+                       objecttag = "CTE Name";
                        break;
                case T_WorkTableScan:
                        /* Assert it's on a self-reference CTE */
                        Assert(rte->rtekind == RTE_CTE);
                        Assert(rte->self_reference);
                        objectname = rte->ctename;
+                       objecttag = "CTE Name";
                        break;
                default:
                        break;
        }
 
-       appendStringInfoString(es->str, " on");
-       if (objectname != NULL)
-               appendStringInfo(es->str, " %s", quote_identifier(objectname));
-       if (objectname == NULL || strcmp(rte->eref->aliasname, objectname) != 0)
-               appendStringInfo(es->str, " %s",
-                                                quote_identifier(rte->eref->aliasname));
+       if (es->format == EXPLAIN_FORMAT_TEXT)
+       {
+               appendStringInfoString(es->str, " on");
+               if (namespace != NULL)
+                       appendStringInfo(es->str, " %s.%s", quote_identifier(namespace),
+                                                        quote_identifier(objectname));
+               else if (objectname != NULL)
+                       appendStringInfo(es->str, " %s", quote_identifier(objectname));
+               if (objectname == NULL ||
+                       strcmp(rte->eref->aliasname, objectname) != 0)
+                       appendStringInfo(es->str, " %s",
+                                                        quote_identifier(rte->eref->aliasname));
+       }
+       else
+       {
+               if (objecttag != NULL && objectname != NULL)
+                       ExplainPropertyText(objecttag, objectname, es);
+               if (namespace != NULL)
+                       ExplainPropertyText("Schema", namespace, es);
+               ExplainPropertyText("Alias", rte->eref->aliasname, es);
+       }
 }
 
 /*
@@ -1155,7 +1418,7 @@ ExplainScanTarget(Scan *plan, ExplainState *es)
  */
 static void
 ExplainMemberNodes(List *plans, PlanState **planstate, Plan *outer_plan,
-                          int indent, ExplainState *es)
+                          ExplainState *es)
 {
        ListCell   *lst;
        int                     j = 0;
@@ -1165,7 +1428,9 @@ ExplainMemberNodes(List *plans, PlanState **planstate, Plan *outer_plan,
                Plan       *subnode = (Plan *) lfirst(lst);
 
                ExplainNode(subnode, planstate[j],
-                                       outer_plan, indent + 3, es);
+                                       outer_plan,
+                                       "Member", NULL,
+                                       es);
                j++;
        }
 }
@@ -1174,7 +1439,7 @@ ExplainMemberNodes(List *plans, PlanState **planstate, Plan *outer_plan,
  * Explain a list of SubPlans (or initPlans, which also use SubPlan nodes).
  */
 static void
-ExplainSubPlans(List *plans, int indent, ExplainState *es)
+ExplainSubPlans(List *plans, const char *relationship, ExplainState *es)
 {
        ListCell   *lst;
 
@@ -1183,9 +1448,431 @@ ExplainSubPlans(List *plans, int indent, ExplainState *es)
                SubPlanState *sps = (SubPlanState *) lfirst(lst);
                SubPlan    *sp = (SubPlan *) sps->xprstate.expr;
 
-               appendStringInfoSpaces(es->str, indent * 2);
-               appendStringInfo(es->str, "  %s\n", sp->plan_name);
                ExplainNode(exec_subplan_get_plan(es->pstmt, sp),
-                                       sps->planstate, NULL, indent + 4, es);
+                                       sps->planstate,
+                                       NULL,
+                                       relationship, sp->plan_name,
+                                       es);
+       }
+}
+
+/*
+ * 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.
+ */
+static void
+ExplainPropertyList(const char *qlabel, List *data, ExplainState *es)
+{
+       ListCell   *lc;
+       bool            first = true;
+
+       switch (es->format)
+       {
+               case EXPLAIN_FORMAT_TEXT:
+                       appendStringInfoSpaces(es->str, es->indent * 2);
+                       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, "<Item>");
+                               str = escape_xml((const char *) lfirst(lc));
+                               appendStringInfoString(es->str, str);
+                               pfree(str);
+                               appendStringInfoString(es->str, "</Item>\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;
+       }
+}
+
+/*
+ * Explain a simple property.
+ *
+ * If "numeric" is true, the value is a number (or other value that
+ * doesn't need quoting in JSON).
+ *
+ * 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 *value, bool numeric,
+                               ExplainState *es)
+{
+       switch (es->format)
+       {
+               case EXPLAIN_FORMAT_TEXT:
+                       appendStringInfoSpaces(es->str, es->indent * 2);
+                       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;
+       }
+}
+
+/*
+ * Explain an integer-valued property.
+ */
+static void
+ExplainPropertyInteger(const char *qlabel, int value, ExplainState *es)
+{
+       char    buf[32];
+
+       snprintf(buf, sizeof(buf), "%d", value);
+       ExplainProperty(qlabel, buf, true, es);
+}
+
+/*
+ * Explain a long-integer-valued property.
+ */
+static void
+ExplainPropertyLong(const char *qlabel, long value, ExplainState *es)
+{
+       char    buf[32];
+
+       snprintf(buf, sizeof(buf), "%ld", value);
+       ExplainProperty(qlabel, buf, true, es);
+}
+
+/*
+ * Explain a float-valued property, using the specified number of
+ * fractional digits.
+ */
+static void
+ExplainPropertyFloat(const char *qlabel, double value, int ndigits,
+                                        ExplainState *es)
+{
+       char    buf[256];
+
+       snprintf(buf, sizeof(buf), "%.*f", ndigits, value);
+       ExplainProperty(qlabel, buf, 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.
+ */
+static 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;
+       }
+}
+
+/*
+ * Close a group of related objects.
+ * Parameters must match the corresponding ExplainOpenGroup call.
+ */
+static 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;
+       }
+}
+
+/*
+ * 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;
+       }
+}
+
+/*
+ * Emit the start-of-output boilerplate.
+ *
+ * This is just enough different from processing a subgroup that we need
+ * a separate pair of subroutines.
+ */
+static void
+ExplainBeginOutput(ExplainState *es)
+{
+       switch (es->format)
+       {
+               case EXPLAIN_FORMAT_TEXT:
+                       /* nothing to do */
+                       break;
+
+               case EXPLAIN_FORMAT_XML:
+                       appendStringInfoString(es->str,
+                                                                  "<explain xmlns=\"http://www.postgresql.org/2009/explain\">\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;
+       }
+}
+
+/*
+ * Emit the end-of-output boilerplate.
+ */
+static void
+ExplainEndOutput(ExplainState *es)
+{
+       switch (es->format)
+       {
+               case EXPLAIN_FORMAT_TEXT:
+                       /* nothing to do */
+                       break;
+
+               case EXPLAIN_FORMAT_XML:
+                       es->indent--;
+                       appendStringInfoString(es->str, "</explain>");
+                       break;
+
+               case EXPLAIN_FORMAT_JSON:
+                       es->indent--;
+                       appendStringInfoString(es->str, "\n]");
+                       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:
+                       /* nothing to do */
+                       break;
+
+               case EXPLAIN_FORMAT_JSON:
+                       /* must have a comma between array elements */
+                       appendStringInfoChar(es->str, ',');
+                       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 tag names can't contain white space, so we replace any spaces in
+ * "tagname" with dashes.
+ */
+static void
+ExplainXMLTag(const char *tagname, int flags, ExplainState *es)
+{
+       const char *s;
+
+       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++)
+               appendStringInfoCharMacro(es->str, (*s == ' ') ? '-' : *s);
+       if ((flags & X_CLOSE_IMMEDIATE) != 0)
+               appendStringInfoString(es->str, " /");
+       appendStringInfoCharMacro(es->str, '>');
+       if ((flags & X_NOWHITESPACE) == 0)
+               appendStringInfoCharMacro(es->str, '\n');
+}
+
+/*
+ * 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');
+}
+
+/*
+ * Produce a JSON string literal, properly escaping characters in the text.
+ */
+static void
+escape_json(StringInfo buf, const char *str)
+{
+       const char *p;
+
+       appendStringInfoCharMacro(buf, '\"');
+       for (p = str; *p; p++)
+       {
+               switch (*p)
+               {
+                       case '\b':
+                               appendStringInfoString(buf, "\\b");
+                               break;
+                       case '\f':
+                               appendStringInfoString(buf, "\\f");
+                               break;
+                       case '\n':
+                               appendStringInfoString(buf, "\\n");
+                               break;
+                       case '\r':
+                               appendStringInfoString(buf, "\\r");
+                               break;
+                       case '\t':
+                               appendStringInfoString(buf, "\\t");
+                               break;
+                       case '"':
+                               appendStringInfoString(buf, "\\\"");
+                               break;
+                       case '\\':
+                               appendStringInfoString(buf, "\\\\");
+                               break;
+                       default:
+                               if ((unsigned char) *p < ' ')
+                                       appendStringInfo(buf, "\\u%04x", (int) *p);
+                               else
+                                       appendStringInfoCharMacro(buf, *p);
+                               break;
+               }
        }
+       appendStringInfoCharMacro(buf, '\"');
 }
index 4ee669691470e1eedd3e4bfa6eaa50646c9e37e3..56a16401f35ed37543be7db40953641e2e3b45cf 100644 (file)
@@ -10,7 +10,7 @@
  * Copyright (c) 2002-2009, PostgreSQL Global Development Group
  *
  * IDENTIFICATION
- *       $PostgreSQL: pgsql/src/backend/commands/prepare.c,v 1.98 2009/07/26 23:34:17 tgl Exp $
+ *       $PostgreSQL: pgsql/src/backend/commands/prepare.c,v 1.99 2009/08/10 05:46:50 tgl Exp $
  *
  *-------------------------------------------------------------------------
  */
@@ -685,9 +685,6 @@ ExplainExecuteQuery(ExecuteStmt *execstmt, ExplainState *es,
        foreach(p, plan_list)
        {
                PlannedStmt *pstmt = (PlannedStmt *) lfirst(p);
-               bool            is_last_query;
-
-               is_last_query = (lnext(p) == NULL);
 
                if (IsA(pstmt, PlannedStmt))
                {
@@ -714,9 +711,9 @@ ExplainExecuteQuery(ExecuteStmt *execstmt, ExplainState *es,
 
                /* No need for CommandCounterIncrement, as ExplainOnePlan did it */
 
-               /* put a blank line between plans */
-               if (!is_last_query)
-                       appendStringInfoChar(es->str, '\n');
+               /* Separate plans with an appropriate separator */
+               if (lnext(p) != NULL)
+                       ExplainSeparatePlans(es);
        }
 
        if (estate)
index b92d41b93ccc8eff83f31e0ed477f3c0d9746dcf..80e113329cef6dd7f99c27ccfa97a2cd8053b340 100644 (file)
@@ -7,7 +7,7 @@
  * Portions Copyright (c) 1996-2009, PostgreSQL Global Development Group
  * Portions Copyright (c) 1994, Regents of the University of California
  *
- * $PostgreSQL: pgsql/src/backend/utils/adt/xml.c,v 1.92 2009/06/11 14:49:04 momjian Exp $
+ * $PostgreSQL: pgsql/src/backend/utils/adt/xml.c,v 1.93 2009/08/10 05:46:50 tgl Exp $
  *
  *-------------------------------------------------------------------------
  */
@@ -1593,8 +1593,6 @@ map_xml_name_to_sql_identifier(char *name)
 char *
 map_sql_value_to_xml_value(Datum value, Oid type, bool xml_escape_strings)
 {
-       StringInfoData buf;
-
        if (type_is_array(type))
        {
                ArrayType  *array;
@@ -1605,6 +1603,7 @@ map_sql_value_to_xml_value(Datum value, Oid type, bool xml_escape_strings)
                int                     num_elems;
                Datum      *elem_values;
                bool       *elem_nulls;
+               StringInfoData buf;
                int                     i;
 
                array = DatumGetArrayTypeP(value);
@@ -1638,8 +1637,7 @@ map_sql_value_to_xml_value(Datum value, Oid type, bool xml_escape_strings)
        {
                Oid                     typeOut;
                bool            isvarlena;
-               char       *p,
-                                  *str;
+               char       *str;
 
                /*
                 * Special XSD formatting for some data types
@@ -1788,32 +1786,47 @@ map_sql_value_to_xml_value(Datum value, Oid type, bool xml_escape_strings)
                        return str;
 
                /* otherwise, translate special characters as needed */
-               initStringInfo(&buf);
+               return escape_xml(str);
+       }
+}
 
-               for (p = str; *p; p++)
+
+/*
+ * Escape characters in text that have special meanings in XML.
+ *
+ * Returns a palloc'd string.
+ *
+ * NB: this is intentionally not dependent on libxml.
+ */
+char *
+escape_xml(const char *str)
+{
+       StringInfoData buf;
+       const char *p;
+
+       initStringInfo(&buf);
+       for (p = str; *p; p++)
+       {
+               switch (*p)
                {
-                       switch (*p)
-                       {
-                               case '&':
-                                       appendStringInfoString(&buf, "&amp;");
-                                       break;
-                               case '<':
-                                       appendStringInfoString(&buf, "&lt;");
-                                       break;
-                               case '>':
-                                       appendStringInfoString(&buf, "&gt;");
-                                       break;
-                               case '\r':
-                                       appendStringInfoString(&buf, "&#x0d;");
-                                       break;
-                               default:
-                                       appendStringInfoCharMacro(&buf, *p);
-                                       break;
-                       }
+                       case '&':
+                               appendStringInfoString(&buf, "&amp;");
+                               break;
+                       case '<':
+                               appendStringInfoString(&buf, "&lt;");
+                               break;
+                       case '>':
+                               appendStringInfoString(&buf, "&gt;");
+                               break;
+                       case '\r':
+                               appendStringInfoString(&buf, "&#x0d;");
+                               break;
+                       default:
+                               appendStringInfoCharMacro(&buf, *p);
+                               break;
                }
-
-               return buf.data;
        }
+       return buf.data;
 }
 
 
index 8247516bd14ba21ed9710bb99b2264ec0e0dad1f..0b50309bf3646991380c0c17209f5c0190def034 100644 (file)
@@ -7,7 +7,7 @@
  * Portions Copyright (c) 1994, Regents of the University of California
  *
  * IDENTIFICATION
- *       $PostgreSQL: pgsql/src/backend/utils/cache/lsyscache.c,v 1.162 2009/06/11 14:49:05 momjian Exp $
+ *       $PostgreSQL: pgsql/src/backend/utils/cache/lsyscache.c,v 1.163 2009/08/10 05:46:50 tgl Exp $
  *
  * NOTES
  *       Eventually, the index information should go through here, too.
@@ -1298,6 +1298,32 @@ get_func_name(Oid funcid)
                return NULL;
 }
 
+/*
+ * get_func_namespace
+ *
+ *             Returns the pg_namespace OID associated with a given function.
+ */
+Oid
+get_func_namespace(Oid funcid)
+{
+       HeapTuple       tp;
+
+       tp = SearchSysCache(PROCOID,
+                                               ObjectIdGetDatum(funcid),
+                                               0, 0, 0);
+       if (HeapTupleIsValid(tp))
+       {
+               Form_pg_proc functup = (Form_pg_proc) GETSTRUCT(tp);
+               Oid                     result;
+
+               result = functup->pronamespace;
+               ReleaseSysCache(tp);
+               return result;
+       }
+       else
+               return InvalidOid;
+}
+
 /*
  * get_func_rettype
  *             Given procedure id, return the function's result type.
index cb89f65a9144c85f0fdea358d1cfd4d24cefb03b..8279a07ec8b6575d518014a146454d933041456a 100644 (file)
@@ -91,7 +91,7 @@
  * Portions Copyright (c) 1994, Regents of the University of California
  *
  * IDENTIFICATION
- *       $PostgreSQL: pgsql/src/backend/utils/sort/tuplesort.c,v 1.92 2009/08/01 20:59:17 tgl Exp $
+ *       $PostgreSQL: pgsql/src/backend/utils/sort/tuplesort.c,v 1.93 2009/08/10 05:46:50 tgl Exp $
  *
  *-------------------------------------------------------------------------
  */
@@ -2200,21 +2200,20 @@ tuplesort_restorepos(Tuplesortstate *state)
 }
 
 /*
- * tuplesort_explain - produce a line of information for EXPLAIN ANALYZE
+ * tuplesort_get_stats - extract summary statistics
  *
  * This can be called after tuplesort_performsort() finishes to obtain
  * printable summary information about how the sort was performed.
- *
- * The result is a palloc'd string.
+ * spaceUsed is measured in kilobytes.
  */
-char *
-tuplesort_explain(Tuplesortstate *state)
+void
+tuplesort_get_stats(Tuplesortstate *state,
+                                       const char **sortMethod,
+                                       const char **spaceType,
+                                       long *spaceUsed)
 {
-       char       *result = (char *) palloc(100);
-       long            spaceUsed;
-
        /*
-        * Note: it might seem we should print both memory and disk usage for a
+        * Note: it might seem we should provide both memory and disk usage for a
         * disk-based sort.  However, the current code doesn't track memory space
         * accurately once we have begun to return tuples to the caller (since we
         * don't account for pfree's the caller is expected to do), so we cannot
@@ -2223,38 +2222,34 @@ tuplesort_explain(Tuplesortstate *state)
         * tell us how much is actually used in sortcontext?
         */
        if (state->tapeset)
-               spaceUsed = LogicalTapeSetBlocks(state->tapeset) * (BLCKSZ / 1024);
+       {
+               *spaceType = "Disk";
+               *spaceUsed = LogicalTapeSetBlocks(state->tapeset) * (BLCKSZ / 1024);
+       }
        else
-               spaceUsed = (state->allowedMem - state->availMem + 1023) / 1024;
+       {
+               *spaceType = "Memory";
+               *spaceUsed = (state->allowedMem - state->availMem + 1023) / 1024;
+       }
 
        switch (state->status)
        {
                case TSS_SORTEDINMEM:
                        if (state->boundUsed)
-                               snprintf(result, 100,
-                                                "Sort Method:  top-N heapsort  Memory: %ldkB",
-                                                spaceUsed);
+                               *sortMethod = "top-N heapsort";
                        else
-                               snprintf(result, 100,
-                                                "Sort Method:  quicksort  Memory: %ldkB",
-                                                spaceUsed);
+                               *sortMethod = "quicksort";
                        break;
                case TSS_SORTEDONTAPE:
-                       snprintf(result, 100,
-                                        "Sort Method:  external sort  Disk: %ldkB",
-                                        spaceUsed);
+                       *sortMethod = "external sort";
                        break;
                case TSS_FINALMERGE:
-                       snprintf(result, 100,
-                                        "Sort Method:  external merge  Disk: %ldkB",
-                                        spaceUsed);
+                       *sortMethod = "external merge";
                        break;
                default:
-                       snprintf(result, 100, "sort still in progress");
+                       *sortMethod = "still in progress";
                        break;
        }
-
-       return result;
 }
 
 
index a5cc40367f8ed1f2988e87f7579716d00ab1b76f..fa2c8aac668872767fd24c322c88929998b12ad8 100644 (file)
@@ -6,7 +6,7 @@
  * Portions Copyright (c) 1996-2009, PostgreSQL Global Development Group
  * Portions Copyright (c) 1994-5, Regents of the University of California
  *
- * $PostgreSQL: pgsql/src/include/commands/explain.h,v 1.40 2009/07/26 23:34:18 tgl Exp $
+ * $PostgreSQL: pgsql/src/include/commands/explain.h,v 1.41 2009/08/10 05:46:50 tgl Exp $
  *
  *-------------------------------------------------------------------------
  */
 
 #include "executor/executor.h"
 
+typedef enum ExplainFormat
+{
+       EXPLAIN_FORMAT_TEXT,
+       EXPLAIN_FORMAT_XML,
+       EXPLAIN_FORMAT_JSON
+} ExplainFormat;
+
 typedef struct ExplainState
 {
        StringInfo      str;                    /* output buffer */
        /* options */
-       bool            verbose;                /* print plan targetlists */
+       bool            verbose;                /* be verbose */
        bool            analyze;                /* print actual times */
        bool            costs;                  /* print costs */
+       ExplainFormat format;           /* output format */
        /* other states */
        PlannedStmt *pstmt;                     /* top of plan */
        List       *rtable;                     /* range table */
+       int                     indent;                 /* current indentation level */
+       List       *grouping_stack;     /* format-specific grouping state */
 } ExplainState;
 
 /* Hook for plugins to get control in ExplainOneQuery() */
@@ -54,4 +64,6 @@ extern void ExplainOnePlan(PlannedStmt *plannedstmt, ExplainState *es,
 
 extern void ExplainPrintPlan(ExplainState *es, QueryDesc *queryDesc);
 
+extern void ExplainSeparatePlans(ExplainState *es);
+
 #endif   /* EXPLAIN_H */
index 4428b22d59260a4579c594f9aad8308a1b498194..28d768e7bf83eb4ccb2ee18d7948505db9ae74d4 100644 (file)
@@ -6,7 +6,7 @@
  * Portions Copyright (c) 1996-2009, PostgreSQL Global Development Group
  * Portions Copyright (c) 1994, Regents of the University of California
  *
- * $PostgreSQL: pgsql/src/include/utils/lsyscache.h,v 1.128 2009/06/11 14:49:13 momjian Exp $
+ * $PostgreSQL: pgsql/src/include/utils/lsyscache.h,v 1.129 2009/08/10 05:46:50 tgl Exp $
  *
  *-------------------------------------------------------------------------
  */
@@ -76,6 +76,7 @@ extern Oid    get_negator(Oid opno);
 extern RegProcedure get_oprrest(Oid opno);
 extern RegProcedure get_oprjoin(Oid opno);
 extern char *get_func_name(Oid funcid);
+extern Oid     get_func_namespace(Oid funcid);
 extern Oid     get_func_rettype(Oid funcid);
 extern int     get_func_nargs(Oid funcid);
 extern Oid     get_func_signature(Oid funcid, Oid **argtypes, int *nargs);
index e351536d471895cd30e6ab2a82b4d17b92ca746b..8504e2e9b3a08aed28f4f2bb3051a9849c849d7f 100644 (file)
@@ -13,7 +13,7 @@
  * Portions Copyright (c) 1996-2009, PostgreSQL Global Development Group
  * Portions Copyright (c) 1994, Regents of the University of California
  *
- * $PostgreSQL: pgsql/src/include/utils/tuplesort.h,v 1.33 2009/06/11 14:49:13 momjian Exp $
+ * $PostgreSQL: pgsql/src/include/utils/tuplesort.h,v 1.34 2009/08/10 05:46:50 tgl Exp $
  *
  *-------------------------------------------------------------------------
  */
@@ -84,7 +84,10 @@ extern bool tuplesort_getdatum(Tuplesortstate *state, bool forward,
 
 extern void tuplesort_end(Tuplesortstate *state);
 
-extern char *tuplesort_explain(Tuplesortstate *state);
+extern void tuplesort_get_stats(Tuplesortstate *state,
+                                                               const char **sortMethod,
+                                                               const char **spaceType,
+                                                               long *spaceUsed);
 
 extern int     tuplesort_merge_order(long allowedMem);
 
index b7d631c031bcce59ce38bad11788e795c37109f7..b7829a84de0212c70145bd6c8de678be288ea29e 100644 (file)
@@ -7,7 +7,7 @@
  * Portions Copyright (c) 1996-2009, PostgreSQL Global Development Group
  * Portions Copyright (c) 1994, Regents of the University of California
  *
- * $PostgreSQL: pgsql/src/include/utils/xml.h,v 1.28 2009/06/11 14:49:13 momjian Exp $
+ * $PostgreSQL: pgsql/src/include/utils/xml.h,v 1.29 2009/08/10 05:46:50 tgl Exp $
  *
  *-------------------------------------------------------------------------
  */
@@ -70,6 +70,7 @@ extern xmltype *xmlpi(char *target, text *arg, bool arg_is_null, bool *result_is
 extern xmltype *xmlroot(xmltype *data, text *version, int standalone);
 extern bool xml_is_document(xmltype *arg);
 extern text *xmltotext_with_xmloption(xmltype *data, XmlOptionType xmloption_arg);
+extern char *escape_xml(const char *str);
 
 extern char *map_sql_identifier_to_xml_name(char *ident, bool fully_escaped, bool escape_period);
 extern char *map_xml_name_to_sql_identifier(char *name);