*** a/contrib/auto_explain/auto_explain.c
--- b/contrib/auto_explain/auto_explain.c
***************
*** 22,29 **** PG_MODULE_MAGIC;
--- 22,37 ----
  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,89 **** _PG_init(void)
--- 92,108 ----
  							 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,206 **** explain_ExecutorEnd(QueryDesc *queryDesc)
--- 220,226 ----
  			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);
  
*** a/doc/src/sgml/auto-explain.sgml
--- b/doc/src/sgml/auto-explain.sgml
***************
*** 104,109 **** LOAD 'auto_explain';
--- 104,126 ----
  
     <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 output format.
+       The legal 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>)
      </term>
      <indexterm>
*** a/doc/src/sgml/ref/explain.sgml
--- b/doc/src/sgml/ref/explain.sgml
***************
*** 31,37 **** 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 ] [ VERBOSE ] <replaceable class="parameter">statement</replaceable>
  </synopsis>
   </refsynopsisdiv>
--- 31,37 ----
  
   <refsynopsisdiv>
  <synopsis>
! EXPLAIN [ ( { ANALYZE <replaceable class="parameter">boolean</replaceable> | VERBOSE <replaceable class="parameter">boolean</replaceable> | COSTS <replaceable class="parameter">boolean</replaceable> | FORMAT { TEXT | JSON | XML }} [, ...] ) ] <replaceable class="parameter">statement</replaceable>
  EXPLAIN [ ANALYZE ] [ VERBOSE ] <replaceable class="parameter">statement</replaceable>
  </synopsis>
   </refsynopsisdiv>
***************
*** 118,125 **** 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>.
       </para>
      </listitem>
     </varlistentry>
--- 118,129 ----
      <term><literal>VERBOSE</literal></term>
      <listitem>
       <para>
!       Display additional information regarding the plan.  Specifically, include
!       the output column list for each node in the plan tree, schema-qualify
!       table and function scans, always label qualifier expressions with
!       the range table alias, and always print the name of each trigger for
!       which statistics are being displayed.  This parameter defaults to
!       <command>FALSE</command>.
       </para>
      </listitem>
     </varlistentry>
***************
*** 136,142 **** ROLLBACK;
     </varlistentry>
  
     <varlistentry>
!     <term><replaceable class="parameter">boolean</replaceable></term>
      <listitem>
       <para>
        Specifies whether the selected option should be turned on or off.
--- 140,158 ----
     </varlistentry>
  
     <varlistentry>
!     <term><literal>FORMAT</literal></term>
!     <listitem>
!      <para>
!       Specify the output format (TEXT, XML, or JSON) for the explain output.
!       XML or JSON output contains the same information as the text output
!       format, but is easier for programs to parse.  This parameter defaults to
!       <command>TEXT</command>.
!      </para>
!     </listitem>
!    </varlistentry>
! 
!    <varlistentry>
!     <term><replaceable class="parameter" />boolean_value</replaceable></term>
      <listitem>
       <para>
        Specifies whether the selected option should be turned on or off.
*** a/src/backend/commands/explain.c
--- b/src/backend/commands/explain.c
***************
*** 32,37 ****
--- 32,38 ----
  #include "utils/lsyscache.h"
  #include "utils/tuplesort.h"
  #include "utils/snapmgr.h"
+ #include "utils/xml.h"
  
  
  /* Hook for plugins to get control in ExplainOneQuery() */
***************
*** 44,69 **** explain_get_index_name_hook_type explain_get_index_name_hook = NULL;
  static void ExplainOneQuery(Query *query, ExplainState *es,
  				const char *queryString, ParamListInfo params);
  static void report_triggers(ResultRelInfo *rInfo, bool show_relname,
! 				StringInfo buf);
  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);
  static void show_qual(List *qual, const char *qlabel, Plan *plan,
! 		  Plan *outer_plan, int indent, bool useprefix, ExplainState *es);
  static void show_scan_qual(List *qual, const char *qlabel,
  			   Plan *scan_plan, Plan *outer_plan,
! 			   int indent, 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);
  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);
  
  
  /*
   * ExplainQuery -
--- 45,85 ----
  static void ExplainOneQuery(Query *query, ExplainState *es,
  				const char *queryString, ParamListInfo params);
  static void report_triggers(ResultRelInfo *rInfo, bool show_relname,
! 				ExplainState *es);
  static double elapsed_time(instr_time *starttime);
  static void ExplainNode(Plan *plan, PlanState *planstate,
! 				Plan *outer_plan, char *relationship, char *qlabel,
! 				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, bool useprefix, ExplainState *es);
  static void show_scan_qual(List *qual, const char *qlabel,
  			   Plan *scan_plan, Plan *outer_plan,
! 			   ExplainState *es);
  static void show_upper_qual(List *qual, const char *qlabel, Plan *plan,
! 				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, ExplainState *es);
! static void ExplainSubPlans(List *plans, char *relationship, ExplainState *es);
  
+ /* Flags for ExplainXMLTag() */
+ #define X_OPENING 0
+ #define X_CLOSING 1
+ #define X_NOWHITESPACE 2
+ 
+ static void ExplainPropertyList(char *qlabel, List *data, ExplainState *es);
+ static void ExplainPropertyText(const char *qlabel, const char *s,
+ 	int numeric, ExplainState *es);
+ static void ExplainOpenGroup(char *tagname, ExplainState *es);
+ static void ExplainCloseGroup(char *tagname, ExplainState *es);
+ static void ExplainJSONLineEnding(ExplainState *es);
+ static void ExplainXMLTag(const char *tagname, int flags, ExplainState *es);
+ 
+ static void escape_json(StringInfo buf, const char *str);
  
  /*
   * ExplainQuery -
***************
*** 94,99 **** ExplainQuery(ExplainStmt *stmt, const char *queryString,
--- 110,134 ----
  			es.verbose = defGetBoolean(opt);
  		else if (strcmp(opt->defname, "costs") == 0)
  			es.costs = defGetBoolean(opt);
+ 		else if (strcmp(opt->defname, "format") == 0)
+ 		{
+ 			int invalid = 0;
+ 			if (!opt->arg || !IsA(opt->arg, String))
+ 				invalid = 1;
+ 			else if (strcmp(strVal(opt->arg), "text") == 0)
+ 				es.format = EXPLAIN_FORMAT_TEXT;
+ 			else if (strcmp(strVal(opt->arg), "xml") == 0)
+ 				es.format = EXPLAIN_FORMAT_XML;
+ 			else if (strcmp(strVal(opt->arg), "json") == 0)
+ 				es.format = EXPLAIN_FORMAT_JSON;
+ 			else
+ 				invalid = 1;
+ 			if (invalid)
+ 				ereport(ERROR,
+ 					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ 					errmsg("invalid value for parameter \"%s\"",
+ 					opt->defname)));
+ 		}
  		else
  			ereport(ERROR,
  					(errcode(ERRCODE_SYNTAX_ERROR),
***************
*** 117,126 **** ExplainQuery(ExplainStmt *stmt, const char *queryString,
  	rewritten = pg_analyze_and_rewrite((Node *) copyObject(stmt->query),
  									   queryString, param_types, num_params);
  
  	if (rewritten == NIL)
  	{
! 		/* In the case of an INSTEAD NOTHING, tell at least that */
! 		appendStringInfoString(es.str, "Query rewrites to nothing\n");
  	}
  	else
  	{
--- 152,174 ----
  	rewritten = pg_analyze_and_rewrite((Node *) copyObject(stmt->query),
  									   queryString, param_types, num_params);
  
+ 	/* Opening boilerplate */
+ 	if (es.format == EXPLAIN_FORMAT_XML)
+ 		appendStringInfoString(es.str,
+ 			"<explain xmlns=\"http://www.postgresql.org/2009/explain\">\n");
+ 	else if (es.format == EXPLAIN_FORMAT_JSON)
+ 		appendStringInfoString(es.str, "[");
+ 	if (es.format != EXPLAIN_FORMAT_TEXT)
+ 		es.indent++;
+ 
  	if (rewritten == NIL)
  	{
! 		/*
! 		 * 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,144 **** ExplainQuery(ExplainStmt *stmt, const char *queryString,
  		foreach(l, rewritten)
  		{
  			ExplainOneQuery((Query *) lfirst(l), &es, queryString, params);
! 			/* put a blank line between plans */
! 			if (lnext(l) != NULL)
  				appendStringInfoChar(es.str, '\n');
  		}
  	}
  
  	/* output tuples */
  	tstate = begin_tup_output_tupdesc(dest, ExplainResultDesc(stmt));
! 	do_text_output_multiline(tstate, es.str->data);
  	end_tup_output(tstate);
  
  	pfree(es.str->data);
--- 178,203 ----
  		foreach(l, rewritten)
  		{
  			ExplainOneQuery((Query *) lfirst(l), &es, queryString, params);
! 			/* in text mode, put a blank line between plans */
! 			if (lnext(l) != NULL && es.format == EXPLAIN_FORMAT_TEXT)
  				appendStringInfoChar(es.str, '\n');
  		}
  	}
  
+ 	/* Closing boilerplate */
+ 	if (es.format != EXPLAIN_FORMAT_TEXT)
+ 		es.indent--;
+ 	if (es.format == EXPLAIN_FORMAT_XML)
+ 		appendStringInfoString(es.str, "</explain>");
+ 	else if (es.format == EXPLAIN_FORMAT_JSON)
+ 		appendStringInfoString(es.str, "\n]");
+ 
  	/* output tuples */
  	tstate = begin_tup_output_tupdesc(dest, ExplainResultDesc(stmt));
! 	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,175 **** TupleDesc
  ExplainResultDesc(ExplainStmt *stmt)
  {
  	TupleDesc	tupdesc;
  
  	/* need a tuple descriptor representing a single TEXT column */
  	tupdesc = CreateTemplateTupleDesc(1, false);
  	TupleDescInitEntry(tupdesc, (AttrNumber) 1, "QUERY PLAN",
! 					   TEXTOID, -1, 0);
  	return tupdesc;
  }
  
--- 224,250 ----
  ExplainResultDesc(ExplainStmt *stmt)
  {
  	TupleDesc	tupdesc;
+ 	ListCell   *lc;
+ 	bool		xml = false;
+ 
+ 	/* Check for XML format. */
+ 	foreach(lc, stmt->options)
+ 	{
+ 		DefElem *opt = (DefElem *) lfirst(lc);
+ 
+ 		if (strcmp(opt->defname, "format") == 0)
+ 		{
+ 			xml = opt->arg != NULL
+ 				&& IsA(opt->arg, String)
+ 				&& strcmp(strVal(opt->arg), "xml") == 0;
+ 			break;
+ 		}
+ 	}
  
  	/* need a tuple descriptor representing a single TEXT column */
  	tupdesc = CreateTemplateTupleDesc(1, false);
  	TupleDescInitEntry(tupdesc, (AttrNumber) 1, "QUERY PLAN",
! 					   xml ? XMLOID : TEXTOID, -1, 0);
  	return tupdesc;
  }
  
***************
*** 223,232 **** ExplainOneUtility(Node *utilityStmt, ExplainState *es,
  		ExplainExecuteQuery((ExecuteStmt *) utilityStmt, es,
  							queryString, params);
  	else if (IsA(utilityStmt, NotifyStmt))
! 		appendStringInfoString(es->str, "NOTIFY\n");
  	else
! 		appendStringInfoString(es->str,
  							   "Utility statements have no plan structure\n");
  }
  
  /*
--- 298,321 ----
  		ExplainExecuteQuery((ExecuteStmt *) utilityStmt, es,
  							queryString, params);
  	else if (IsA(utilityStmt, NotifyStmt))
! 	{
! 		if (es->format == EXPLAIN_FORMAT_TEXT)
! 			appendStringInfoString(es->str, "NOTIFY\n");
! 		else if (es->format == EXPLAIN_FORMAT_XML)
! 			appendStringInfoString(es->str, "  <Notify />\n");
! 		else if (es->format == EXPLAIN_FORMAT_JSON)
! 			appendStringInfoString(es->str, "\n  \"Notify\"");
! 	}
  	else
! 	{
! 		if (es->format == EXPLAIN_FORMAT_TEXT)
! 			appendStringInfoString(es->str,
  							   "Utility statements have no plan structure\n");
+ 		else if (es->format == EXPLAIN_FORMAT_XML)
+ 			appendStringInfoString(es->str, "  <Utility-Statement />\n");
+ 		else if (es->format == EXPLAIN_FORMAT_JSON)
+ 			appendStringInfoString(es->str, "\n  \"Utility-Statement\"");
+ 	}
  }
  
  /*
***************
*** 288,293 **** ExplainOnePlan(PlannedStmt *plannedstmt, ExplainState *es,
--- 377,385 ----
  		totaltime += elapsed_time(&starttime);
  	}
  
+ 	/* Opening boilerplate */
+ 	ExplainOpenGroup("Query", es);
+ 
  	/* Create textual dump of plan tree */
  	ExplainPrintPlan(es, queryDesc);
  
***************
*** 313,327 **** ExplainOnePlan(PlannedStmt *plannedstmt, ExplainState *es,
  		int			nr;
  		ListCell   *l;
  
  		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);
  
  		foreach(l, targrels)
  		{
  			rInfo = (ResultRelInfo *) lfirst(l);
! 			report_triggers(rInfo, show_relname, es->str);
  		}
  	}
  
--- 405,443 ----
  		int			nr;
  		ListCell   *l;
  
+ 		/* Opening boilerplate */
+ 		if (es->format == EXPLAIN_FORMAT_XML)
+ 			ExplainXMLTag("Triggers", X_OPENING, es);
+ 		else if (es->format == EXPLAIN_FORMAT_JSON)
+ 		{
+ 			ExplainJSONLineEnding(es);
+ 			appendStringInfoSpaces(es->str, 2 * es->indent);
+ 			appendStringInfoString(es->str, "\"Triggers\": [");
+ 			es->needs_separator = 0;
+ 		}
+ 		es->indent++;
+ 
  		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);
  
  		foreach(l, targrels)
  		{
  			rInfo = (ResultRelInfo *) lfirst(l);
! 			report_triggers(rInfo, show_relname, es);
! 		}
! 
! 		/* Closing boilerplate */
! 		es->indent--;
! 		if (es->format == EXPLAIN_FORMAT_XML)
! 			ExplainXMLTag("Triggers", X_CLOSING, es);
! 		else if (es->format == EXPLAIN_FORMAT_JSON)
! 		{
! 			appendStringInfoChar(es->str, '\n');
! 			appendStringInfoSpaces(es->str, 2 * es->indent);
! 			appendStringInfoChar(es->str, ']');
! 			es->needs_separator = 1;
  		}
  	}
  
***************
*** 344,351 **** ExplainOnePlan(PlannedStmt *plannedstmt, ExplainState *es,
  	totaltime += elapsed_time(&starttime);
  
  	if (es->analyze)
! 		appendStringInfo(es->str, "Total runtime: %.3f ms\n",
! 						 1000.0 * totaltime);
  }
  
  /*
--- 460,479 ----
  	totaltime += elapsed_time(&starttime);
  
  	if (es->analyze)
! 	{
! 		if (es->format == EXPLAIN_FORMAT_TEXT)
! 			appendStringInfo(es->str, "Total runtime: %.3f ms\n",
! 							 1000.0 * totaltime);
! 		else
! 		{
! 			char b[256];
! 			sprintf(b, "%.3f", 1000.0 * totaltime);
! 			ExplainPropertyText("Total Runtime", b, 1, es);
! 		}
! 	}
! 
! 	/* Closing boilerplate */
! 	ExplainCloseGroup("Query", es);
  }
  
  /*
***************
*** 362,371 **** void
  ExplainPrintPlan(ExplainState *es, QueryDesc *queryDesc)
  {
  	Assert(queryDesc->plannedstmt != NULL);
  	es->pstmt = queryDesc->plannedstmt;
  	es->rtable = queryDesc->plannedstmt->rtable;
  	ExplainNode(queryDesc->plannedstmt->planTree, queryDesc->planstate,
! 				NULL, 0, es);
  }
  
  /*
--- 490,502 ----
  ExplainPrintPlan(ExplainState *es, QueryDesc *queryDesc)
  {
  	Assert(queryDesc->plannedstmt != NULL);
+ 
  	es->pstmt = queryDesc->plannedstmt;
  	es->rtable = queryDesc->plannedstmt->rtable;
+ 	es->indent--;
  	ExplainNode(queryDesc->plannedstmt->planTree, queryDesc->planstate,
! 				NULL, NULL, NULL, es);
! 	es->indent++;
  }
  
  /*
***************
*** 373,379 **** 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)
  {
  	int			nt;
  
--- 504,510 ----
   *		report execution stats for a single relation's triggers
   */
  static void
! report_triggers(ResultRelInfo *rInfo, bool show_relname, ExplainState *es)
  {
  	int			nt;
  
***************
*** 383,389 **** report_triggers(ResultRelInfo *rInfo, bool show_relname, StringInfo buf)
  	{
  		Trigger    *trig = rInfo->ri_TrigDesc->triggers + nt;
  		Instrumentation *instr = rInfo->ri_TrigInstrument + nt;
! 		char	   *conname;
  
  		/* Must clean up instrumentation state */
  		InstrEndLoop(instr);
--- 514,521 ----
  	{
  		Trigger    *trig = rInfo->ri_TrigDesc->triggers + nt;
  		Instrumentation *instr = rInfo->ri_TrigInstrument + nt;
! 		char	   *conname = NULL;
! 		bool		previous_needs_separator;
  
  		/* Must clean up instrumentation state */
  		InstrEndLoop(instr);
***************
*** 395,415 **** 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)
  		{
! 			appendStringInfo(buf, "Trigger for constraint %s", conname);
! 			pfree(conname);
  		}
  		else
! 			appendStringInfo(buf, "Trigger %s", trig->tgname);
  
! 		if (show_relname)
! 			appendStringInfo(buf, " on %s",
  							 RelationGetRelationName(rInfo->ri_RelationDesc));
  
! 		appendStringInfo(buf, ": time=%.3f calls=%.0f\n",
! 						 1000.0 * instr->total, instr->ntuples);
  	}
  }
  
--- 527,591 ----
  		if (instr->ntuples == 0)
  			continue;
  
! 		/* Opening boilerplate for this trigger */
! 		if (es->format == EXPLAIN_FORMAT_XML)
! 			ExplainXMLTag("Trigger", X_OPENING, es);
! 		else if (es->format == EXPLAIN_FORMAT_JSON)
  		{
! 			ExplainJSONLineEnding(es);
! 			appendStringInfoSpaces(es->str, 2 * es->indent);
! 			appendStringInfoChar(es->str, '{');
  		}
+ 		es->indent++;
+ 		previous_needs_separator = es->needs_separator;
+ 		es->needs_separator = 0;
+ 
+ 		if (OidIsValid(trig->tgconstraint))
+ 			conname = get_constraint_name(trig->tgconstraint);
+ 
+ 		/*
+ 		 * In text mode, we avoid printing both the trigger name and the
+ 		 * constraint name unless VERBOSE is specified.  In XML or JSON
+ 		 * format we just print everything.
+ 		 */
+ 		if (es->format != EXPLAIN_FORMAT_TEXT)
+ 			ExplainPropertyText("Trigger Name", trig->tgname, 0, es);
+ 		else if (es->verbose || conname == NULL)
+ 			appendStringInfo(es->str, "Trigger %s", trig->tgname);
  		else
! 			appendStringInfoString(es->str, "Trigger");
! 
! 		if (conname != NULL)
! 		{
! 			if (es->format != EXPLAIN_FORMAT_TEXT)
! 				ExplainPropertyText("Constraint Name", conname, 0, es);
! 			else
! 				appendStringInfo(es->str, " for constraint %s", conname);
! 			pfree(conname);
! 		}
  
! 		if (es->format == EXPLAIN_FORMAT_TEXT)
! 		{
! 			if (show_relname)
! 				appendStringInfo(es->str, " on %s",
  							 RelationGetRelationName(rInfo->ri_RelationDesc));
+ 			appendStringInfo(es->str, ": time=%.3f calls=%.0f\n",
+ 							 1000.0 * instr->total, instr->ntuples);
+ 		}
+ 		else
+ 		{
+ 			char b[256];
+ 			ExplainPropertyText("Relation",
+ 					RelationGetRelationName(rInfo->ri_RelationDesc), 0, es);
+ 			sprintf(b, "%.3f", 1000.0 * instr->total);
+ 			ExplainPropertyText("Time", b, 1, es);
+ 			sprintf(b, "%.0f", instr->ntuples);
+ 			ExplainPropertyText("Calls", b, 1, es);
+ 		}
  
! 		/* Closing boilerplate for this trigger */
! 		ExplainCloseGroup("Trigger", es);
! 		es->needs_separator = previous_needs_separator;
  	}
  }
  
***************
*** 436,688 **** 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.
   */
  static void
  ExplainNode(Plan *plan, PlanState *planstate,
  			Plan *outer_plan,
! 			int indent, ExplainState *es)
  {
  	const char *pname;
  
! 	if (indent)
! 	{
! 		Assert(indent >= 2);
! 		appendStringInfoSpaces(es->str, 2 * indent - 4);
! 		appendStringInfoString(es->str, "->  ");
! 	}
  
! 	if (plan == NULL)
  	{
! 		appendStringInfoChar(es->str, '\n');
! 		return;
  	}
  
  	switch (nodeTag(plan))
  	{
  		case T_Result:
! 			pname = "Result";
  			break;
  		case T_Append:
! 			pname = "Append";
  			break;
  		case T_RecursiveUnion:
! 			pname = "Recursive Union";
  			break;
  		case T_BitmapAnd:
! 			pname = "BitmapAnd";
  			break;
  		case T_BitmapOr:
! 			pname = "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;
! 			}
  			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;
! 			}
  			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;
! 			}
  			break;
  		case T_SeqScan:
! 			pname = "Seq Scan";
  			break;
  		case T_IndexScan:
! 			pname = "Index Scan";
  			break;
  		case T_BitmapIndexScan:
! 			pname = "Bitmap Index Scan";
  			break;
  		case T_BitmapHeapScan:
! 			pname = "Bitmap Heap Scan";
  			break;
  		case T_TidScan:
! 			pname = "Tid Scan";
  			break;
  		case T_SubqueryScan:
! 			pname = "Subquery Scan";
  			break;
  		case T_FunctionScan:
! 			pname = "Function Scan";
  			break;
  		case T_ValuesScan:
! 			pname = "Values Scan";
  			break;
  		case T_CteScan:
! 			pname = "CTE Scan";
  			break;
  		case T_WorkTableScan:
! 			pname = "WorkTable Scan";
  			break;
  		case T_Material:
! 			pname = "Materialize";
  			break;
  		case T_Sort:
! 			pname = "Sort";
  			break;
  		case T_Group:
! 			pname = "Group";
  			break;
  		case T_Agg:
  			switch (((Agg *) plan)->aggstrategy)
  			{
  				case AGG_PLAIN:
  					pname = "Aggregate";
  					break;
  				case AGG_SORTED:
  					pname = "GroupAggregate";
  					break;
  				case AGG_HASHED:
  					pname = "HashAggregate";
  					break;
  				default:
  					pname = "Aggregate ???";
  					break;
  			}
  			break;
  		case T_WindowAgg:
! 			pname = "WindowAgg";
  			break;
  		case T_Unique:
! 			pname = "Unique";
  			break;
  		case T_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;
! 					}
  					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;
! 					}
  					break;
  				default:
  					pname = "SetOp ???";
  					break;
  			}
  			break;
  		case T_Limit:
! 			pname = "Limit";
  			break;
  		case T_Hash:
! 			pname = "Hash";
  			break;
  		default:
! 			pname = "???";
  			break;
  	}
  
! 	appendStringInfoString(es->str, pname);
  	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));
  			/* FALL THRU */
  		case T_SeqScan:
  		case T_BitmapHeapScan:
--- 612,842 ----
   * side of a join with the current node.  This is only interesting for
   * deciphering runtime keys of an inner indexscan.
   *
!  * In text mode, if es->indent is positive, we indent the plan output
!  * accordingly and put "->" in front of it.  This should only happen for child
!  * plan nodes.  In XML or JSON modes, es->indent will normally be > 0 at the
!  * top level.
   */
  static void
  ExplainNode(Plan *plan, PlanState *planstate,
  			Plan *outer_plan,
! 			char *relationship, char *qlabel, ExplainState *es)
  {
+ 	bool		moreplans;
  	const char *pname;
+ 	const char *sname;
+ 	const char *strategy = NULL;
+ 	int			previous_indent = es->indent;
+ 	int			previous_needs_separator = 0;
+ 	List	   *memberplans = NIL;
+ 	PlanState **memberplanstates = NULL;
  
! 	Assert(plan);
  
! 	if (es->format == EXPLAIN_FORMAT_TEXT)
  	{
! 		if (qlabel)
! 		{
! 			Assert(es->indent >= 3);
! 			++es->indent;
! 			appendStringInfoSpaces(es->str, es->indent * 2 - 6);
! 			appendStringInfo(es->str, "%s\n", qlabel);
! 		}
! 
! 		if (es->indent)
! 		{
! 			Assert(es->indent >= 2);
! 			appendStringInfoSpaces(es->str, 2 * es->indent - 4);
! 			appendStringInfoString(es->str, "->  ");
! 		}
  	}
  
  	switch (nodeTag(plan))
  	{
  		case T_Result:
! 			pname = sname = "Result";
  			break;
  		case T_Append:
! 			pname = sname = "Append";
  			break;
  		case T_RecursiveUnion:
! 			pname = sname = "Recursive Union";
  			break;
  		case T_BitmapAnd:
! 			pname = sname = "BitmapAnd";
  			break;
  		case T_BitmapOr:
! 			pname = sname = "BitmapOr";
  			break;
  		case T_NestLoop:
! 			sname = pname = "Nested Loop";
  			break;
  		case T_MergeJoin:
! 			sname = "Merge Join";
! 			pname = "Merge";		/* "Join" gets added by jointype switch */
  			break;
  		case T_HashJoin:
! 			sname = "Hash Join";
! 			pname = "Hash";			/* "Join" gets added by jointype switch */
  			break;
  		case T_SeqScan:
! 			pname = sname = "Seq Scan";
  			break;
  		case T_IndexScan:
! 			pname = sname = "Index Scan";
  			break;
  		case T_BitmapIndexScan:
! 			pname = sname = "Bitmap Index Scan";
  			break;
  		case T_BitmapHeapScan:
! 			pname = sname = "Bitmap Heap Scan";
  			break;
  		case T_TidScan:
! 			pname = sname = "Tid Scan";
  			break;
  		case T_SubqueryScan:
! 			pname = sname = "Subquery Scan";
  			break;
  		case T_FunctionScan:
! 			pname = sname = "Function Scan";
  			break;
  		case T_ValuesScan:
! 			pname = sname = "Values Scan";
  			break;
  		case T_CteScan:
! 			pname = sname = "CTE Scan";
  			break;
  		case T_WorkTableScan:
! 			pname = sname = "WorkTable Scan";
  			break;
  		case T_Material:
! 			pname = sname = "Materialize";
  			break;
  		case T_Sort:
! 			pname = sname = "Sort";
  			break;
  		case T_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 = sname = "WindowAgg";
  			break;
  		case T_Unique:
! 			pname = sname = "Unique";
  			break;
  		case T_SetOp:
+ 			sname = "SetOp";
  			switch (((SetOp *) plan)->strategy)
  			{
  				case SETOP_SORTED:
! 					pname = "SetOp";
! 					strategy = "Sorted";
  					break;
  				case SETOP_HASHED:
! 					pname = "HashSetOp";
! 					strategy = "Hashed";
  					break;
  				default:
  					pname = "SetOp ???";
+ 					strategy = "???";
  					break;
  			}
  			break;
  		case T_Limit:
! 			pname = sname = "Limit";
  			break;
  		case T_Hash:
! 			pname = sname = "Hash";
  			break;
  		default:
! 			pname = sname = "???";
  			break;
  	}
  
! 	if (es->format == EXPLAIN_FORMAT_TEXT)
! 		appendStringInfoString(es->str, pname);
! 	else
! 	{
! 		++es->indent;
! 		if (es->format == EXPLAIN_FORMAT_XML)
! 			ExplainXMLTag("Plan", X_OPENING, es);
! 		else if (es->format == EXPLAIN_FORMAT_JSON)
! 		{
! 			ExplainJSONLineEnding(es);
! 			previous_needs_separator = es->needs_separator;
! 			es->needs_separator = 0;
! 			appendStringInfoSpaces(es->str, 2 * es->indent);
! 			if (!relationship)
! 				appendStringInfoString(es->str, "\"Plan\": ");
! 			appendStringInfoChar(es->str, '{');
! 		}
! 		++es->indent;
! 		ExplainPropertyText("Node Type", sname, 0, es);
! 		if (relationship)
! 			ExplainPropertyText("Parent Relationship", relationship, 0, es);
! 		if (qlabel)
! 			ExplainPropertyText("Parent Label", qlabel, 0, es);
! 		if (strategy)
! 			ExplainPropertyText("Strategy", strategy, 0, es);
! 	}
! 
  	switch (nodeTag(plan))
  	{
  		case T_IndexScan:
! 			{
! 				IndexScan *indexscan = (IndexScan *) plan;
! 				const char *index =
! 					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", index);
! 				}
! 				else
! 				{
! 					char *scandir;
! 					switch (((IndexScan *) plan)->indexorderdir)
! 					{
! 						case BackwardScanDirection:
! 							scandir = "Backward";
! 							break;
! 						case NoMovementScanDirection:
! 							scandir = "NoMovement";
! 							break;
! 						case ForwardScanDirection:
! 							scandir = "Forward";
! 							break;
! 						default:
! 							scandir = "???";
! 							break;
! 					}
! 					ExplainPropertyText("Scan Direction", scandir, 0, es);
! 					ExplainPropertyText("Index Name", index, 0, es);
! 				}
! 			}
  			/* FALL THRU */
  		case T_SeqScan:
  		case T_BitmapHeapScan:
***************
*** 695,711 **** 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));
  			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);
  
  	/*
  	 * We have to forcibly clean up the instrumentation state because we
--- 849,955 ----
  			ExplainScanTarget((Scan *) plan, es);
  			break;
  		case T_BitmapIndexScan:
! 			{
! 				BitmapIndexScan *bitmapindexscan = (BitmapIndexScan *) plan;
! 				const char *index =
! 					explain_get_index_name(bitmapindexscan->indexid);
! 				if (es->format == EXPLAIN_FORMAT_TEXT)
! 					appendStringInfo(es->str, " on %s", index);
! 				else
! 					ExplainPropertyText("Index Name", index, 0, es);
! 				break;
! 			}
  			break;
+ 		case T_MergeJoin:
+ 		case T_HashJoin:
+ 		case T_NestLoop:
+ 			{
+ 				char *jointype = NULL;
+ 				switch (((Join *) plan)->jointype)
+ 				{
+ 					case JOIN_INNER:
+ 						if (es->format != EXPLAIN_FORMAT_TEXT)
+ 							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)
+ 					ExplainPropertyText("Join Type", jointype, 0, es);
+ 				else if (jointype)
+ 					appendStringInfo(es->str, " %s Join", jointype);
+ 				else if (!IsA(plan, NestLoop))
+ 					appendStringInfo(es->str, " Join");
+ 			}
+ 			break;
+ 		case T_SetOp:
+ 			{
+ 				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, 0, es);
+ 				break;
+ 			}
  		default:
  			break;
  	}
  
  	if (es->costs)
! 	{
! 		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
! 		{
! 			char b[256];
! 			sprintf(b, "%.2f", plan->startup_cost);
! 			ExplainPropertyText("Startup Cost", b, 1, es);
! 			sprintf(b, "%.2f", plan->total_cost);
! 			ExplainPropertyText("Total Cost", b, 1, es);
! 			sprintf(b, "%.0f", plan->plan_rows);
! 			ExplainPropertyText("Plan Rows", b, 1, es);
! 			sprintf(b, "%d", plan->plan_width);
! 			ExplainPropertyText("Plan Width", b, 1, es);
! 		}
! 	}
  
  	/*
  	 * We have to forcibly clean up the instrumentation state because we
***************
*** 717,754 **** ExplainNode(Plan *plan, PlanState *planstate,
  	if (planstate->instrument && planstate->instrument->nloops > 0)
  	{
  		double		nloops = planstate->instrument->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);
  	}
  	else if (es->analyze)
! 		appendStringInfoString(es->str, " (never executed)");
! 	appendStringInfoChar(es->str, '\n');
  
  	/* target list */
  	if (es->verbose)
! 		show_plan_tlist(plan, indent, 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);
  			break;
  		case T_BitmapIndexScan:
  			show_scan_qual(((BitmapIndexScan *) plan)->indexqualorig,
! 						   "Index Cond", plan, outer_plan, indent, es);
  			break;
  		case T_BitmapHeapScan:
  			show_scan_qual(((BitmapHeapScan *) plan)->bitmapqualorig,
! 						   "Recheck Cond", plan, outer_plan, indent, es);
  			/* FALL THRU */
  		case T_SeqScan:
  		case T_FunctionScan:
--- 961,1021 ----
  	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;
  
! 		if (es->format == EXPLAIN_FORMAT_TEXT)
! 		{
! 			appendStringInfo(es->str,
! 							 " (actual time=%.3f..%.3f rows=%.0f loops=%.0f)",
! 							startup_sec, total_sec, rows, nloops);
! 		}
! 		else
! 		{
! 			char b[256];
! 			sprintf(b, "%.3f", startup_sec);
! 			ExplainPropertyText("Actual Startup Time", b, 1, es);
! 			sprintf(b, "%.3f", total_sec);
! 			ExplainPropertyText("Actual Total Time", b, 1, es);
! 			sprintf(b, "%.0f", rows);
! 			ExplainPropertyText("Actual Rows", b, 1, es);
! 			sprintf(b, "%.0f", nloops);
! 			ExplainPropertyText("Actual Loops", b, 1, es);
! 		}
  	}
  	else if (es->analyze)
! 	{
! 		if (es->format == EXPLAIN_FORMAT_TEXT)
! 			appendStringInfo(es->str, " (never executed)");
! 		else
! 			ExplainPropertyText("Actual Loops", "0", 1, 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, es);
  
  	/* quals, sort keys, etc */
  	switch (nodeTag(plan))
  	{
  		case T_IndexScan:
  			show_scan_qual(((IndexScan *) plan)->indexqualorig,
! 						   "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, es);
  			break;
  		case T_BitmapHeapScan:
  			show_scan_qual(((BitmapHeapScan *) plan)->bitmapqualorig,
! 						   "Recheck Cond", plan, outer_plan, es);
  			/* FALL THRU */
  		case T_SeqScan:
  		case T_FunctionScan:
***************
*** 756,763 **** 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);
  			break;
  		case T_TidScan:
  			{
--- 1023,1029 ----
  		case T_CteScan:
  		case T_WorkTableScan:
  		case T_SubqueryScan:
! 			show_scan_qual(plan->qual, "Filter", plan, outer_plan, es);
  			break;
  		case T_TidScan:
  			{
***************
*** 769,819 **** 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);
  			}
  			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);
  			break;
  		case T_MergeJoin:
  			show_upper_qual(((MergeJoin *) plan)->mergeclauses,
! 							"Merge Cond", plan, indent, es);
  			show_upper_qual(((MergeJoin *) plan)->join.joinqual,
! 							"Join Filter", plan, indent, es);
! 			show_upper_qual(plan->qual, "Filter", plan, indent, es);
  			break;
  		case T_HashJoin:
  			show_upper_qual(((HashJoin *) plan)->hashclauses,
! 							"Hash Cond", plan, indent, es);
  			show_upper_qual(((HashJoin *) plan)->join.joinqual,
! 							"Join Filter", plan, indent, es);
! 			show_upper_qual(plan->qual, "Filter", plan, indent, es);
  			break;
  		case T_Agg:
  		case T_Group:
! 			show_upper_qual(plan->qual, "Filter", plan, indent, es);
  			break;
  		case T_Sort:
! 			show_sort_keys(plan, indent, es);
! 			show_sort_info((SortState *) planstate, indent, 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);
  			break;
  		default:
  			break;
  	}
  
  	/* initPlan-s */
  	if (plan->initPlan)
! 		ExplainSubPlans(planstate->initPlan, indent, es);
  
  	/* lefttree */
  	if (outerPlan(plan))
--- 1035,1115 ----
  
  				if (list_length(tidquals) > 1)
  					tidquals = list_make1(make_orclause(tidquals));
! 				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, es);
! 			show_upper_qual(plan->qual, "Filter", plan, es);
  			break;
  		case T_MergeJoin:
  			show_upper_qual(((MergeJoin *) plan)->mergeclauses,
! 							"Merge Cond", plan, es);
  			show_upper_qual(((MergeJoin *) plan)->join.joinqual,
! 							"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, es);
  			show_upper_qual(((HashJoin *) plan)->join.joinqual,
! 							"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, es);
  			break;
  		case T_Sort:
! 			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, es);
! 			show_upper_qual(plan->qual, "Filter", plan, es);
! 			break;
! 		case T_Append:
! 			memberplans = ((Append *) plan)->appendplans;
! 			memberplanstates = ((AppendState *) planstate)->appendplans;
! 			break;
! 		case T_BitmapAnd:
! 			memberplans = ((BitmapAnd *) plan)->bitmapplans;
! 			memberplanstates = ((BitmapAndState *) planstate)->bitmapplans;
! 			break;
! 		case T_BitmapOr:
! 			memberplans = ((BitmapOr *) plan)->bitmapplans;
! 			memberplanstates = ((BitmapOrState *) planstate)->bitmapplans;
  			break;
  		default:
  			break;
  	}
  
+ 	/* Get ready to display the child plans. */
+ 	moreplans = plan->initPlan || outerPlan(plan) || innerPlan(plan)
+ 				|| IsA(plan, SubqueryScan) || memberplans
+ 				|| planstate->subPlan;
+ 	if (es->format == EXPLAIN_FORMAT_TEXT)
+ 		es->indent += 3;
+ 	else if (moreplans)
+ 	{
+ 		if (es->format == EXPLAIN_FORMAT_XML)
+ 			ExplainXMLTag("Plans", X_OPENING, es);
+ 		else if (es->format == EXPLAIN_FORMAT_JSON)
+ 		{
+ 			ExplainJSONLineEnding(es);
+ 			appendStringInfoSpaces(es->str, 2 * es->indent);
+ 			escape_json(es->str, "Plans");
+ 			appendStringInfoString(es->str, ": [");
+ 			es->needs_separator = 0;
+ 		}
+ 	}
+ 
  	/* initPlan-s */
  	if (plan->initPlan)
! 		ExplainSubPlans(planstate->initPlan, "InitPlan", es);
  
  	/* lefttree */
  	if (outerPlan(plan))
***************
*** 825,883 **** ExplainNode(Plan *plan, PlanState *planstate,
  		 */
  		ExplainNode(outerPlan(plan), outerPlanState(planstate),
  					IsA(plan, BitmapHeapScan) ? outer_plan : NULL,
! 					indent + 3, es);
  	}
  
  	/* righttree */
  	if (innerPlan(plan))
  	{
  		ExplainNode(innerPlan(plan), innerPlanState(planstate),
! 					outerPlan(plan), indent + 3, es);
  	}
  
  	/* special child plans */
! 	switch (nodeTag(plan))
  	{
! 		case T_Append:
! 			ExplainMemberNodes(((Append *) plan)->appendplans,
! 							   ((AppendState *) planstate)->appendplans,
! 							   outer_plan, indent, es);
! 			break;
! 		case T_BitmapAnd:
! 			ExplainMemberNodes(((BitmapAnd *) plan)->bitmapplans,
! 							   ((BitmapAndState *) planstate)->bitmapplans,
! 							   outer_plan, indent, es);
! 			break;
! 		case T_BitmapOr:
! 			ExplainMemberNodes(((BitmapOr *) plan)->bitmapplans,
! 							   ((BitmapOrState *) planstate)->bitmapplans,
! 							   outer_plan, indent, es);
! 			break;
! 		case T_SubqueryScan:
! 			{
! 				SubqueryScan *subqueryscan = (SubqueryScan *) plan;
! 				SubqueryScanState *subquerystate = (SubqueryScanState *) planstate;
  
! 				ExplainNode(subqueryscan->subplan, subquerystate->subplan,
! 							NULL, indent + 3, es);
! 			}
! 			break;
! 		default:
! 			break;
  	}
  
  	/* subPlan-s */
  	if (planstate->subPlan)
! 		ExplainSubPlans(planstate->subPlan, indent, es);
  }
  
  /*
   * Show the targetlist of a plan node
   */
  static void
! show_plan_tlist(Plan *plan, int indent, ExplainState *es)
  {
  	List	   *context;
  	bool		useprefix;
  	ListCell   *lc;
  	int			i;
--- 1121,1187 ----
  		 */
  		ExplainNode(outerPlan(plan), outerPlanState(planstate),
  					IsA(plan, BitmapHeapScan) ? outer_plan : NULL,
! 					"Outer", NULL, es);
  	}
  
  	/* righttree */
  	if (innerPlan(plan))
  	{
  		ExplainNode(innerPlan(plan), innerPlanState(planstate),
! 					outerPlan(plan), "Inner", NULL, es);
  	}
  
+ 	/* member plans */
+ 	if (memberplans)
+ 		ExplainMemberNodes(memberplans, memberplanstates, outer_plan, es);
+ 
  	/* special child plans */
! 	if (IsA(plan, SubqueryScan))
  	{
! 		SubqueryScan *subqueryscan = (SubqueryScan *) plan;
! 		SubqueryScanState *subquerystate = (SubqueryScanState *) planstate;
  
! 		ExplainNode(subqueryscan->subplan, subquerystate->subplan,
! 					NULL, "Subquery", NULL, es);
  	}
  
  	/* subPlan-s */
  	if (planstate->subPlan)
! 		ExplainSubPlans(planstate->subPlan, "SubPlan", es);
! 
! 	/* close out output for this plan node */
! 	if (es->format != EXPLAIN_FORMAT_TEXT)
! 	{
! 		/* end of init, outer, inner, subquery, and subplans */
! 		if (moreplans)
! 		{
! 			if (es->format == EXPLAIN_FORMAT_XML)
! 				ExplainXMLTag("Plans", X_CLOSING, es);
! 			else if (es->format == EXPLAIN_FORMAT_JSON)
! 			{
! 				appendStringInfoChar(es->str, '\n');
! 				appendStringInfoSpaces(es->str, 2 * es->indent);
! 				appendStringInfoString(es->str, "]");
! 			}
! 		}
! 
! 		/* end of plan */
! 		ExplainCloseGroup("Plan", es);
! 	}
! 
! 	/* restore previous indent and separator state */
! 	es->indent = previous_indent;
! 	es->needs_separator = previous_needs_separator;
  }
  
  /*
   * Show the targetlist of a plan node
   */
  static void
! show_plan_tlist(Plan *plan, ExplainState *es)
  {
  	List	   *context;
+ 	List	   *result = NIL;
  	bool		useprefix;
  	ListCell   *lc;
  	int			i;
***************
*** 899,908 **** 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)
--- 1203,1208 ----
***************
*** 911,924 **** 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,
  												  useprefix, false));
  	}
  
! 	appendStringInfoChar(es->str, '\n');
  }
  
  /*
--- 1211,1223 ----
  
  		if (tle->resjunk)
  			continue;
! 		result = lappend(result,
! 					     deparse_expression((Node *) tle->expr, context,
  												  useprefix, false));
  	}
  
! 	/* Print results. */
! 	ExplainPropertyList("Output", result, es);
  }
  
  /*
***************
*** 929,935 **** 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)
  {
  	List	   *context;
  	Node	   *node;
--- 1228,1234 ----
   */
  static void
  show_qual(List *qual, const char *qlabel, Plan *plan, Plan *outer_plan,
! 		  bool useprefix, ExplainState *es)
  {
  	List	   *context;
  	Node	   *node;
***************
*** 952,959 **** 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);
  }
  
  /*
--- 1251,1257 ----
  	exprstr = deparse_expression(node, context, useprefix, false);
  
  	/* And add to es->str */
! 	ExplainPropertyText(qlabel, exprstr, 0, es);
  }
  
  /*
***************
*** 961,997 **** 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)
  {
  	bool		useprefix;
  
! 	useprefix = (outer_plan != NULL || IsA(scan_plan, SubqueryScan));
! 	show_qual(qual, qlabel, scan_plan, outer_plan, indent, 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)
  {
  	bool		useprefix;
  
! 	useprefix = (list_length(es->rtable) > 1);
! 	show_qual(qual, qlabel, plan, NULL, indent, useprefix, es);
  }
  
  /*
   * Show the sort keys for a Sort node.
   */
  static void
! show_sort_keys(Plan *sortplan, int indent, ExplainState *es)
  {
  	int			nkeys = ((Sort *) sortplan)->numCols;
  	AttrNumber *keycols = ((Sort *) sortplan)->sortColIdx;
  	List	   *context;
  	bool		useprefix;
  	int			keyno;
  	char	   *exprstr;
--- 1259,1295 ----
   */
  static void
  show_scan_qual(List *qual, const char *qlabel,
! 			   Plan *scan_plan, Plan *outer_plan, ExplainState *es)
  {
  	bool		useprefix;
  
! 	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, ExplainState *es)
  {
  	bool		useprefix;
  
! 	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, ExplainState *es)
  {
  	int			nkeys = ((Sort *) sortplan)->numCols;
  	AttrNumber *keycols = ((Sort *) sortplan)->sortColIdx;
  	List	   *context;
+ 	List	   *result = NIL;
  	bool		useprefix;
  	int			keyno;
  	char	   *exprstr;
***************
*** 999,1007 **** 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,
--- 1297,1302 ----
***************
*** 1020,1050 **** 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);
  	}
  
! 	appendStringInfoChar(es->str, '\n');
  }
  
  /*
   * If it's EXPLAIN ANALYZE, show tuplesort explain info for a sort node
   */
  static void
! show_sort_info(SortState *sortstate, int indent, ExplainState *es)
  {
  	Assert(IsA(sortstate, SortState));
  	if (es->analyze && sortstate->sort_Done &&
  		sortstate->tuplesortstate != NULL)
  	{
! 		char	   *sortinfo;
  
! 		sortinfo = tuplesort_explain((Tuplesortstate *) sortstate->tuplesortstate);
! 		appendStringInfoSpaces(es->str, indent * 2);
! 		appendStringInfo(es->str, "  %s\n", sortinfo);
! 		pfree(sortinfo);
  	}
  }
  
--- 1315,1358 ----
  		/* Deparse the expression, showing any top-level cast */
  		exprstr = deparse_expression((Node *) target->expr, context,
  									 useprefix, true);
! 		result = lappend(result, exprstr);
  	}
  
! 	ExplainPropertyList(es->format == EXPLAIN_FORMAT_TEXT
! 						&& list_length(result) == 1 ? "Sort Key" : "Sort Keys",
! 						result, es);
  }
  
  /*
   * If it's EXPLAIN ANALYZE, show tuplesort explain info for a sort node
   */
  static void
! show_sort_info(SortState *sortstate, ExplainState *es)
  {
  	Assert(IsA(sortstate, SortState));
  	if (es->analyze && sortstate->sort_Done &&
  		sortstate->tuplesortstate != NULL)
  	{
! 		Tuplesortstate	*state = (Tuplesortstate *) sortstate->tuplesortstate;
! 		char	   *method;
! 		char	   *type;
! 		long		spaceUsed;
! 
! 		method = tuplesort_explain(state, &type, &spaceUsed);
  
! 		if (es->format == EXPLAIN_FORMAT_TEXT)
! 		{
! 			appendStringInfoSpaces(es->str, es->indent * 2);
! 			appendStringInfo(es->str, "  Sort Method:  %s  %s: %ldkB\n",
! 							 method, type, spaceUsed);
! 		}
! 		else
! 		{
! 			char spaceUsedStr[128];
! 			sprintf(spaceUsedStr, "%ld", spaceUsed);
! 			ExplainPropertyText("Sort Method", method, 0, es);
! 			ExplainPropertyText("Sort Space Used", spaceUsedStr, 1, es);
! 		}
  	}
  }
  
***************
*** 1081,1086 **** static void
--- 1389,1396 ----
  ExplainScanTarget(Scan *plan, ExplainState *es)
  {
  	char	   *objectname = NULL;
+ 	char	   *objecttag = NULL;
+ 	char	   *namespace = NULL;
  	RangeTblEntry *rte;
  
  	if (plan->scanrelid <= 0)	/* Is this still possible? */
***************
*** 1096,1101 **** ExplainScanTarget(Scan *plan, ExplainState *es)
--- 1406,1414 ----
  			/* 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,1122 **** ExplainScanTarget(Scan *plan, ExplainState *es)
--- 1429,1439 ----
  					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,1145 **** ExplainScanTarget(Scan *plan, ExplainState *es)
  			Assert(rte->rtekind == RTE_CTE);
  			Assert(!rte->self_reference);
  			objectname = rte->ctename;
  			break;
  		case T_WorkTableScan:
  			/* Assert it's on a self-reference CTE */
  			Assert(rte->rtekind == RTE_CTE);
  			Assert(rte->self_reference);
  			objectname = rte->ctename;
  			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",
--- 1444,1477 ----
  			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;
  	}
  
+ 	if (es->format != EXPLAIN_FORMAT_TEXT)
+ 	{
+ 		if (objecttag != NULL)
+ 			ExplainPropertyText(objecttag, objectname, 0, es);
+ 		if (namespace != NULL)
+ 			ExplainPropertyText("Schema", namespace, 0, es);
+ 		ExplainPropertyText("Alias", rte->eref->aliasname, 0, es);
+ 		return;
+ 	}
+ 
  	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",
***************
*** 1155,1161 **** ExplainScanTarget(Scan *plan, ExplainState *es)
   */
  static void
  ExplainMemberNodes(List *plans, PlanState **planstate, Plan *outer_plan,
! 		           int indent, ExplainState *es)
  {
  	ListCell   *lst;
  	int			j = 0;
--- 1487,1493 ----
   */
  static void
  ExplainMemberNodes(List *plans, PlanState **planstate, Plan *outer_plan,
! 		           ExplainState *es)
  {
  	ListCell   *lst;
  	int			j = 0;
***************
*** 1163,1171 **** ExplainMemberNodes(List *plans, PlanState **planstate, Plan *outer_plan,
  	foreach(lst, plans)
  	{
  		Plan	   *subnode = (Plan *) lfirst(lst);
- 
  		ExplainNode(subnode, planstate[j],
! 					outer_plan, indent + 3, es);
  		j++;
  	}
  }
--- 1495,1502 ----
  	foreach(lst, plans)
  	{
  		Plan	   *subnode = (Plan *) lfirst(lst);
  		ExplainNode(subnode, planstate[j],
! 					outer_plan, "Member", NULL, es);
  		j++;
  	}
  }
***************
*** 1174,1180 **** 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)
  {
  	ListCell   *lst;
  
--- 1505,1511 ----
   * Explain a list of SubPlans (or initPlans, which also use SubPlan nodes).
   */
  static void
! ExplainSubPlans(List *plans, char *relationship, ExplainState *es)
  {
  	ListCell   *lst;
  
***************
*** 1183,1191 **** 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);
  	}
  }
--- 1514,1726 ----
  		SubPlanState *sps = (SubPlanState *) lfirst(lst);
  		SubPlan    *sp = (SubPlan *) sps->xprstate.expr;
  
  		ExplainNode(exec_subplan_get_plan(es->pstmt, sp),
! 					sps->planstate, NULL, relationship, sp->plan_name, es);
! 	}
! }
! 
! /*
!  * Explain a property, such as sort keys or targets, that takes the form of
!  * a list.
!  */
! static void
! ExplainPropertyList(char *qlabel, List *data, ExplainState *es)
! {
! 	ListCell *lc;
! 
! 	if (es->format == EXPLAIN_FORMAT_TEXT)
! 	{
! 		int first = 1;
! 		appendStringInfoSpaces(es->str, es->indent * 2);
! 		appendStringInfo(es->str, "  %s: ", qlabel);
! 		foreach (lc, data)
! 		{
! 			if (!first)
! 				appendStringInfoString(es->str, ", ");
! 			appendStringInfoString(es->str, lfirst(lc));
! 			first = 0;
! 		}
! 		appendStringInfoChar(es->str, '\n');
! 	}
! 	else if (es->format == EXPLAIN_FORMAT_XML)
! 	{
! 		ExplainXMLTag(qlabel, X_OPENING, es);
! 		foreach (lc, data)
! 		{
! 			appendStringInfoSpaces(es->str, es->indent * 2 + 2);
! 			appendStringInfoString(es->str, "<Item>");
! 			escape_xml(es->str, (const char *) lfirst(lc));
! 			appendStringInfoString(es->str, "</Item>\n");
! 		}
! 		ExplainXMLTag(qlabel, X_CLOSING, es);
! 	}
! 	else if (es->format == EXPLAIN_FORMAT_JSON)
! 	{
! 		int first = 1;
! 		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 = 0;
! 		}
! 		appendStringInfoChar(es->str, ']');
! 	}
! }
! 
! /*
!  * Explain text property.
!  */
! static void
! ExplainPropertyText(const char *qlabel, const char *s, int numeric,
! 	ExplainState *es)
! {
! 	if (es->format == EXPLAIN_FORMAT_TEXT)
! 	{
! 		appendStringInfoSpaces(es->str, es->indent * 2);
! 		appendStringInfo(es->str, "  %s: %s\n", qlabel, s);
! 	}
! 	else if (es->format == EXPLAIN_FORMAT_XML)
! 	{
! 		appendStringInfoSpaces(es->str, es->indent * 2);
! 		ExplainXMLTag(qlabel, X_OPENING | X_NOWHITESPACE, es);
! 		escape_xml(es->str, s);
! 		ExplainXMLTag(qlabel, X_CLOSING | X_NOWHITESPACE, es);
! 		appendStringInfoChar(es->str, '\n');
! 	}
! 	else if (es->format == EXPLAIN_FORMAT_JSON)
! 	{
! 		ExplainJSONLineEnding(es);
! 		appendStringInfoSpaces(es->str, es->indent * 2);
! 		escape_json(es->str, qlabel);
! 		appendStringInfoString(es->str, ": ");
! 		if (numeric)
! 			appendStringInfoString(es->str, s);
! 		else
! 			escape_json(es->str, s);
! 	}
! }
! 
! /*
!  * Open a group of related properties.
!  */
! static void
! ExplainOpenGroup(char *tagname, ExplainState *es)
! {
! 	if (es->format == EXPLAIN_FORMAT_XML)
! 		ExplainXMLTag(tagname, X_OPENING, es);
! 	else if (es->format == EXPLAIN_FORMAT_JSON)
! 	{
! 		ExplainJSONLineEnding(es);
! 		appendStringInfoSpaces(es->str, 2 * es->indent);
! 		appendStringInfoChar(es->str, '{');
! 		es->needs_separator = 0;
! 	}
! 	es->indent++;
! }
! 
! /*
!  * Close a group of related properties.
!  */
! static void
! ExplainCloseGroup(char *tagname, ExplainState *es)
! {
! 	es->indent--;
! 	if (es->format == EXPLAIN_FORMAT_XML)
! 		ExplainXMLTag(tagname, X_CLOSING, es);
! 	else if (es->format == EXPLAIN_FORMAT_JSON)
! 	{
! 		appendStringInfoChar(es->str, '\n');
! 		appendStringInfoSpaces(es->str, 2 * es->indent);
! 		appendStringInfoChar(es->str, '}');
! 		es->needs_separator = 1;
! 	}
! }
! 
! /*
!  * Emit a JSON line ending.
!  *
!  * JSON requires a comma after each property but the last.  To facilitate this,
!  * in JSON mode, the text emitted for each property begins just prior to the
!  * preceding line-break (and comma, if applicable).  es->needs_separator is
!  * true for each property but the first.
!  */
! static void
! ExplainJSONLineEnding(ExplainState *es)
! {
! 	Assert(es->format == EXPLAIN_FORMAT_JSON);
! 	if (es->needs_separator)
! 		appendStringInfoChar(es->str, ',');
! 	appendStringInfoChar(es->str, '\n');
! 	es->needs_separator = 1;
! }
! 
! /*
!  * Emit opening or closing XML tag.  XML tag names can't contain white space,
!  * so we replace any spaces we encounter 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);
! 	appendStringInfoCharMacro(es->str, '>');
! 	if ((flags & X_NOWHITESPACE) == 0)
! 		appendStringInfoCharMacro(es->str, '\n');
! }
! 
! /*
!  * Escape characters in text that have special meanings in JSON.
!  */
! 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 (*p < ' ')
! 					appendStringInfo(buf, "\\u%04x", (int) *p);
! 				else
! 					appendStringInfoCharMacro(buf, *p);
! 				break;
! 		}
  	}
+ 	appendStringInfoCharMacro(buf, '\"');
  }
*** a/src/backend/commands/prepare.c
--- b/src/backend/commands/prepare.c
***************
*** 716,722 **** ExplainExecuteQuery(ExecuteStmt *execstmt, ExplainState *es,
  
  		/* put a blank line between plans */
  		if (!is_last_query)
! 			appendStringInfoChar(es->str, '\n');
  	}
  
  	if (estate)
--- 716,727 ----
  
  		/* put a blank line between plans */
  		if (!is_last_query)
! 		{
! 			if (es->format == EXPLAIN_FORMAT_TEXT)
! 				appendStringInfoChar(es->str, '\n');
! 			else if (es->format == EXPLAIN_FORMAT_JSON)
! 				appendStringInfoChar(es->str, ',');
! 		}
  	}
  
  	if (estate)
*** a/src/backend/utils/adt/xml.c
--- b/src/backend/utils/adt/xml.c
***************
*** 1638,1645 **** map_sql_value_to_xml_value(Datum value, Oid type, bool xml_escape_strings)
  	{
  		Oid			typeOut;
  		bool		isvarlena;
! 		char	   *p,
! 				   *str;
  
  		/*
  		 * Special XSD formatting for some data types
--- 1638,1644 ----
  	{
  		Oid			typeOut;
  		bool		isvarlena;
! 		char	   *str;
  
  		/*
  		 * Special XSD formatting for some data types
***************
*** 1789,1822 **** map_sql_value_to_xml_value(Datum value, Oid type, bool xml_escape_strings)
  
  		/* otherwise, translate special characters as needed */
  		initStringInfo(&buf);
  
! 		for (p = str; *p; 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;
! 			}
  		}
- 
- 		return buf.data;
  	}
  }
  
- 
  static char *
  _SPI_strdup(const char *s)
  {
--- 1788,1829 ----
  
  		/* otherwise, translate special characters as needed */
  		initStringInfo(&buf);
+ 		escape_xml(&buf, str);
+ 		return buf.data;
+ 	}
+ }
  
! /*
!  * Escape characters in text that have special meanings in XML.
!  */
! void
! escape_xml(StringInfo buf, const char *str)
! {
! 	const char *p;
! 
! 	for (p = str; *p; 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;
  		}
  	}
  }
  
  static char *
  _SPI_strdup(const char *s)
  {
*** a/src/backend/utils/cache/lsyscache.c
--- b/src/backend/utils/cache/lsyscache.c
***************
*** 1299,1304 **** get_func_name(Oid funcid)
--- 1299,1330 ----
  }
  
  /*
+  * get_func_namespace
+  *
+  *		Returns the pg_namespace OID associated with a given funcation.
+  */
+ 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.
   */
*** a/src/backend/utils/sort/tuplesort.c
--- b/src/backend/utils/sort/tuplesort.c
***************
*** 2200,2218 **** tuplesort_restorepos(Tuplesortstate *state)
  }
  
  /*
!  * tuplesort_explain - produce a line of information for EXPLAIN ANALYZE
   *
   * 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.
   */
  char *
! tuplesort_explain(Tuplesortstate *state)
  {
- 	char	   *result = (char *) palloc(100);
- 	long		spaceUsed;
- 
  	/*
  	 * Note: it might seem we should print both memory and disk usage for a
  	 * disk-based sort.  However, the current code doesn't track memory space
--- 2200,2216 ----
  }
  
  /*
!  * tuplesort_explain - produce information for EXPLAIN ANALYZE
   *
   * This can be called after tuplesort_performsort() finishes to obtain
   * printable summary information about how the sort was performed.
   *
!  * The result is the sort method; the type (memory vs. disk) and amount
!  * of space used are returned using separate parameters.
   */
  char *
! tuplesort_explain(Tuplesortstate *state, char **type, long *spaceUsed)
  {
  	/*
  	 * Note: it might seem we should print both memory and disk usage for a
  	 * disk-based sort.  However, the current code doesn't track memory space
***************
*** 2223,2260 **** tuplesort_explain(Tuplesortstate *state)
  	 * tell us how much is actually used in sortcontext?
  	 */
  	if (state->tapeset)
! 		spaceUsed = LogicalTapeSetBlocks(state->tapeset) * (BLCKSZ / 1024);
  	else
! 		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);
  			else
! 				snprintf(result, 100,
! 						 "Sort Method:  quicksort  Memory: %ldkB",
! 						 spaceUsed);
  			break;
  		case TSS_SORTEDONTAPE:
! 			snprintf(result, 100,
! 					 "Sort Method:  external sort  Disk: %ldkB",
! 					 spaceUsed);
  			break;
  		case TSS_FINALMERGE:
! 			snprintf(result, 100,
! 					 "Sort Method:  external merge  Disk: %ldkB",
! 					 spaceUsed);
  			break;
  		default:
! 			snprintf(result, 100, "sort still in progress");
  			break;
  	}
- 
- 	return result;
  }
  
  
--- 2221,2252 ----
  	 * tell us how much is actually used in sortcontext?
  	 */
  	if (state->tapeset)
! 		*spaceUsed = LogicalTapeSetBlocks(state->tapeset) * (BLCKSZ / 1024);
  	else
! 		*spaceUsed = (state->allowedMem - state->availMem + 1023) / 1024;
  
  	switch (state->status)
  	{
  		case TSS_SORTEDINMEM:
+ 			*type = "Memory";
  			if (state->boundUsed)
! 				return "top-N heapsort";
  			else
! 				return "quicksort";
  			break;
  		case TSS_SORTEDONTAPE:
! 			*type = "Disk";
! 			return "external sort";
  			break;
  		case TSS_FINALMERGE:
! 			*type = "Disk";
! 			return "external merge";
  			break;
  		default:
! 			*type = "Storage";
! 			return "still in progress";
  			break;
  	}
  }
  
  
*** a/src/include/commands/explain.h
--- b/src/include/commands/explain.h
***************
*** 15,30 ****
  
  #include "executor/executor.h"
  
  typedef struct ExplainState
  {
  	StringInfo	str;			/* output buffer */
  	/* options */
! 	bool		verbose;		/* print plan targetlists */
  	bool		analyze;		/* print actual times */
  	bool		costs;			/* print costs */
  	/* other states */
  	PlannedStmt *pstmt;			/* top of plan */
  	List	   *rtable;			/* range table */
  } ExplainState;
  
  /* Hook for plugins to get control in ExplainOneQuery() */
--- 15,40 ----
  
  #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;		/* 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 indent level */
+ 	bool		needs_separator; /* separator needed before next item? */
  } ExplainState;
  
  /* Hook for plugins to get control in ExplainOneQuery() */
*** a/src/include/utils/lsyscache.h
--- b/src/include/utils/lsyscache.h
***************
*** 76,81 **** extern Oid	get_negator(Oid opno);
--- 76,82 ----
  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 relid);
  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);
*** a/src/include/utils/tuplesort.h
--- b/src/include/utils/tuplesort.h
***************
*** 84,90 **** extern bool tuplesort_getdatum(Tuplesortstate *state, bool forward,
  
  extern void tuplesort_end(Tuplesortstate *state);
  
! extern char *tuplesort_explain(Tuplesortstate *state);
  
  extern int	tuplesort_merge_order(long allowedMem);
  
--- 84,91 ----
  
  extern void tuplesort_end(Tuplesortstate *state);
  
! extern char *tuplesort_explain(Tuplesortstate *state, char **type,
! 					long *spaceUsed);
  
  extern int	tuplesort_merge_order(long allowedMem);
  
*** a/src/include/utils/xml.h
--- b/src/include/utils/xml.h
***************
*** 16,21 ****
--- 16,22 ----
  #define XML_H
  
  #include "fmgr.h"
+ #include "lib/stringinfo.h"
  #include "nodes/execnodes.h"
  #include "nodes/primnodes.h"
  
***************
*** 70,75 **** extern xmltype *xmlpi(char *target, text *arg, bool arg_is_null, bool *result_is
--- 71,77 ----
  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 void escape_xml(StringInfo buf, 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);