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);