diff --git a/contrib/auto_explain/auto_explain.c b/contrib/auto_explain/auto_explain.c
index eb2bc04aed9758b255a85f578e7ce4e1b3876564..6d3435be1f75f11a7a7b9c03622d0b706c03cf92 100644
--- a/contrib/auto_explain/auto_explain.c
+++ b/contrib/auto_explain/auto_explain.c
@@ -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);
 
diff --git a/doc/src/sgml/auto-explain.sgml b/doc/src/sgml/auto-explain.sgml
index c1e85af10e0f275152ad627e7d806704b8ad4298..72487f944ce6432168dbe2729d327b2b90ee6ad9 100644
--- a/doc/src/sgml/auto-explain.sgml
+++ b/doc/src/sgml/auto-explain.sgml
@@ -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>)
diff --git a/doc/src/sgml/ref/explain.sgml b/doc/src/sgml/ref/explain.sgml
index d3b5c979a29608dcba0f4e053869ea1a087b65d5..9670bd06f193a265fb3b53220e8daacf6e2306cf 100644
--- a/doc/src/sgml/ref/explain.sgml
+++ b/doc/src/sgml/ref/explain.sgml
@@ -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>
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 1388c8dd42fb8c5a31de0b567b63c79593af7ff0..d675d8d8171f028badacff4df86e19ad90e40659 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -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, '\"');
 }
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 4ee669691470e1eedd3e4bfa6eaa50646c9e37e3..56a16401f35ed37543be7db40953641e2e3b45cf 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -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)
diff --git a/src/backend/utils/adt/xml.c b/src/backend/utils/adt/xml.c
index b92d41b93ccc8eff83f31e0ed477f3c0d9746dcf..80e113329cef6dd7f99c27ccfa97a2cd8053b340 100644
--- a/src/backend/utils/adt/xml.c
+++ b/src/backend/utils/adt/xml.c
@@ -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;
 }
 
 
diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c
index 8247516bd14ba21ed9710bb99b2264ec0e0dad1f..0b50309bf3646991380c0c17209f5c0190def034 100644
--- a/src/backend/utils/cache/lsyscache.c
+++ b/src/backend/utils/cache/lsyscache.c
@@ -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.
diff --git a/src/backend/utils/sort/tuplesort.c b/src/backend/utils/sort/tuplesort.c
index cb89f65a9144c85f0fdea358d1cfd4d24cefb03b..8279a07ec8b6575d518014a146454d933041456a 100644
--- a/src/backend/utils/sort/tuplesort.c
+++ b/src/backend/utils/sort/tuplesort.c
@@ -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;
 }
 
 
diff --git a/src/include/commands/explain.h b/src/include/commands/explain.h
index a5cc40367f8ed1f2988e87f7579716d00ab1b76f..fa2c8aac668872767fd24c322c88929998b12ad8 100644
--- a/src/include/commands/explain.h
+++ b/src/include/commands/explain.h
@@ -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 $
  *
  *-------------------------------------------------------------------------
  */
@@ -15,16 +15,26 @@
 
 #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 */
diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h
index 4428b22d59260a4579c594f9aad8308a1b498194..28d768e7bf83eb4ccb2ee18d7948505db9ae74d4 100644
--- a/src/include/utils/lsyscache.h
+++ b/src/include/utils/lsyscache.h
@@ -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);
diff --git a/src/include/utils/tuplesort.h b/src/include/utils/tuplesort.h
index e351536d471895cd30e6ab2a82b4d17b92ca746b..8504e2e9b3a08aed28f4f2bb3051a9849c849d7f 100644
--- a/src/include/utils/tuplesort.h
+++ b/src/include/utils/tuplesort.h
@@ -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);
 
diff --git a/src/include/utils/xml.h b/src/include/utils/xml.h
index b7d631c031bcce59ce38bad11788e795c37109f7..b7829a84de0212c70145bd6c8de678be288ea29e 100644
--- a/src/include/utils/xml.h
+++ b/src/include/utils/xml.h
@@ -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);