generic explain options v3

Started by Robert Haasover 16 years ago15 messages
#1Robert Haas
robertmhaas@gmail.com
1 attachment(s)

Here is an updated version of my "generic options for explain" patch.
Previous version here:

http://archives.postgresql.org/pgsql-hackers/2009-06/msg00866.php

This patch requires the "explain refactoring v4" patch, which you can
find here, to be applied first:

http://archives.postgresql.org/pgsql-hackers/2009-06/msg00865.php

In this version, I've taken the liberty of adding a "COSTS" option
which defaults to "ON", so that you can say: EXPLAIN (COSTS OFF) ...
to abolish display of the costs information, per my previous
suggestion. I was initially thinking of waiting to submit this as a
follow-on patch, but nobody seemed to object to the idea much, so I've
gone ahead and added it here. It remains to be seen whether someone
can develop a workable set of regression tests based on this
functionality, but it's pretty clear that it CAN'T be done without
this functionality, so this seems like a step in the right direction
at any rate.

The other major update in this patch is that it adds documentation. I
was not completely sure what the best way to document this was, so
it's very possible that what I've done here can be improved upon.

I will send updated versions of the "machine-readable explain output"
patches soon.

...Robert

Attachments:

explain_options-v3.patchtext/x-diff; charset=US-ASCII; name=explain_options-v3.patchDownload
*** a/contrib/auto_explain/auto_explain.c
--- b/contrib/auto_explain/auto_explain.c
***************
*** 14,19 ****
--- 14,20 ----
  
  #include "commands/explain.h"
  #include "executor/instrument.h"
+ #include "nodes/makefuncs.h"
  #include "utils/guc.h"
  
  PG_MODULE_MAGIC;
***************
*** 196,207 **** explain_ExecutorEnd(QueryDesc *queryDesc)
  		msec = queryDesc->totaltime->total * 1000.0;
  		if (msec >= auto_explain_log_min_duration)
  		{
  			StringInfoData buf;
  
  			initStringInfo(&buf);
! 			ExplainPrintPlan(&buf, queryDesc,
! 						 queryDesc->doInstrument && auto_explain_log_analyze,
! 							 auto_explain_log_verbose);
  
  			/* Remove last line break */
  			if (buf.len > 0 && buf.data[buf.len - 1] == '\n')
--- 197,210 ----
  		msec = queryDesc->totaltime->total * 1000.0;
  		if (msec >= auto_explain_log_min_duration)
  		{
+ 			ExplainStmt   *stmt = makeExplain(NIL, NULL);
  			StringInfoData buf;
  
  			initStringInfo(&buf);
! 			stmt->analyze =
! 				(queryDesc->doInstrument && auto_explain_log_analyze);
! 			stmt->verbose = auto_explain_log_verbose;
! 			ExplainPrintPlan(&buf, queryDesc, stmt);
  
  			/* Remove last line break */
  			if (buf.len > 0 && buf.data[buf.len - 1] == '\n')
*** a/doc/src/sgml/ref/explain.sgml
--- b/doc/src/sgml/ref/explain.sgml
***************
*** 31,36 **** PostgreSQL documentation
--- 31,37 ----
  
   <refsynopsisdiv>
  <synopsis>
+ EXPLAIN [ ( [ { ANALYZE | VERBOSE | COSTS } [ <replaceable class="parameter">boolean_value</replaceable> ] ] [, ...] ) ] <replaceable class="parameter">statement</replaceable>
  EXPLAIN [ ANALYZE ] [ VERBOSE ] <replaceable class="parameter">statement</replaceable>
  </synopsis>
   </refsynopsisdiv>
***************
*** 70,75 **** EXPLAIN [ ANALYZE ] [ VERBOSE ] <replaceable class="parameter">statement</replac
--- 71,86 ----
     are close to reality.
    </para>
  
+   <para>
+    Only the <literal>ANALYZE</literal> and <literal>VERBOSE</literal> options
+    can be specified, and only in the order, without surrounding the option list
+    in parentheses.  Prior to <productname>PostgreSQL</productname> 8.5, the
+    unparenthesized syntax was the only one supported.  It is expected that
+    all new options will be supported only when using the parenthesized syntax,
+    which also allows a value to be specified for each option
+    (e.g. <literal>TRUE</literal> or <literal>FALSE</literal>).
+   </para>
+ 
    <important>
     <para>
      Keep in mind that the statement is actually executed when
***************
*** 99,105 **** ROLLBACK;
      <term><literal>ANALYZE</literal></term>
      <listitem>
       <para>
!       Carry out the command and show the actual run times.
       </para>
      </listitem>
     </varlistentry>
--- 110,117 ----
      <term><literal>ANALYZE</literal></term>
      <listitem>
       <para>
!       Carry out the command and show the actual run times.  This
!       parameter defaults to <command>OFF</command>.
       </para>
      </listitem>
     </varlistentry>
***************
*** 108,114 **** ROLLBACK;
      <term><literal>VERBOSE</literal></term>
      <listitem>
       <para>
!       Include the output column list for each node in the plan tree.
       </para>
      </listitem>
     </varlistentry>
--- 120,152 ----
      <term><literal>VERBOSE</literal></term>
      <listitem>
       <para>
!       Include the output column list for each node in the plan tree.  This
!       parameter defaults to <command>OFF</command>.
!      </para>
!     </listitem>
!    </varlistentry>
! 
!    <varlistentry>
!     <term><literal>COSTS</literal></term>
!     <listitem>
!      <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>ON</command>.
!      </para>
!     </listitem>
!    </varlistentry>
! 
!    <varlistentry>
!     <term><replaceable class="parameter" />boolean_value</replaceable></term>
!     <listitem>
!      <para>
!       Specifies whether the named parameter should be turned on or off.  You
!       can use the values <literal>TRUE</literal>, <literal>ON</literal>,
!       <literal>YES</literal>, or <literal>1</literal> to request the stated
!       option, and <literal>FALSE</literal>, <literal>OFF</literal>,
!       <literal>NO</literal>, or <literal>0</literal>.  If the Boolean value
!       is omitted, it defaults to <literal>TRUE</literal>.
       </para>
      </listitem>
     </varlistentry>
***************
*** 202,207 **** EXPLAIN SELECT * FROM foo WHERE i = 4;
--- 240,259 ----
    </para>
  
    <para>
+    Here is the same plan with costs suppressed:
+ 
+ <programlisting>
+ EXPLAIN (COSTS OFF) SELECT * FROM foo WHERE i = 4;
+ 
+         QUERY PLAN
+ ----------------------------
+  Index Scan using fi on foo
+    Index Cond: (i = 4)
+ (2 rows)
+ </programlisting>
+   </para>
+ 
+   <para>
     Here is an example of a query plan for a query using an aggregate
     function:
  
*** a/src/backend/commands/explain.c
--- b/src/backend/commands/explain.c
***************
*** 46,51 **** typedef struct ExplainState
--- 46,52 ----
  	/* options */
  	bool		printTList;		/* print plan targetlists */
  	bool		printAnalyze;	/* print actual times */
+ 	bool		printCosts;		/* print costs */
  	/* other states */
  	PlannedStmt *pstmt;			/* top of plan */
  	List	   *rtable;			/* range table */
***************
*** 268,274 **** ExplainOnePlan(PlannedStmt *plannedstmt, ExplainStmt *stmt,
  
  	/* Create textual dump of plan tree */
  	initStringInfo(&buf);
! 	ExplainPrintPlan(&buf, queryDesc, stmt->analyze, stmt->verbose);
  
  	/*
  	 * If we ran the command, run any AFTER triggers it queued.  (Note this
--- 269,275 ----
  
  	/* Create textual dump of plan tree */
  	initStringInfo(&buf);
! 	ExplainPrintPlan(&buf, queryDesc, stmt);
  
  	/*
  	 * If we ran the command, run any AFTER triggers it queued.  (Note this
***************
*** 340,347 **** ExplainOnePlan(PlannedStmt *plannedstmt, ExplainStmt *stmt,
   * NB: will not work on utility statements
   */
  void
! ExplainPrintPlan(StringInfo str, QueryDesc *queryDesc,
! 				 bool analyze, bool verbose)
  {
  	ExplainState es;
  
--- 341,347 ----
   * NB: will not work on utility statements
   */
  void
! ExplainPrintPlan(StringInfo str, QueryDesc *queryDesc, ExplainStmt *stmt)
  {
  	ExplainState es;
  
***************
*** 349,356 **** ExplainPrintPlan(StringInfo str, QueryDesc *queryDesc,
  
  	memset(&es, 0, sizeof(es));
  	es.str = str;
! 	es.printTList = verbose;
! 	es.printAnalyze = analyze;
  	es.pstmt = queryDesc->plannedstmt;
  	es.rtable = queryDesc->plannedstmt->rtable;
  
--- 349,357 ----
  
  	memset(&es, 0, sizeof(es));
  	es.str = str;
! 	es.printTList = stmt->verbose;
! 	es.printAnalyze = stmt->analyze;
! 	es.printCosts = stmt->costs;
  	es.pstmt = queryDesc->plannedstmt;
  	es.rtable = queryDesc->plannedstmt->rtable;
  
***************
*** 688,696 **** ExplainNode(Plan *plan, PlanState *planstate, Plan *outer_plan,
  			break;
  	}
  
! 	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
--- 689,698 ----
  			break;
  	}
  
! 	if (es->printCosts)
! 		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
*** a/src/backend/nodes/copyfuncs.c
--- b/src/backend/nodes/copyfuncs.c
***************
*** 2876,2881 **** _copyExplainStmt(ExplainStmt *from)
--- 2876,2882 ----
  	COPY_NODE_FIELD(query);
  	COPY_SCALAR_FIELD(verbose);
  	COPY_SCALAR_FIELD(analyze);
+ 	COPY_SCALAR_FIELD(costs);
  
  	return newnode;
  }
*** a/src/backend/nodes/equalfuncs.c
--- b/src/backend/nodes/equalfuncs.c
***************
*** 1468,1473 **** _equalExplainStmt(ExplainStmt *a, ExplainStmt *b)
--- 1468,1474 ----
  	COMPARE_NODE_FIELD(query);
  	COMPARE_SCALAR_FIELD(verbose);
  	COMPARE_SCALAR_FIELD(analyze);
+ 	COMPARE_SCALAR_FIELD(costs);
  
  	return true;
  }
*** a/src/backend/nodes/makefuncs.c
--- b/src/backend/nodes/makefuncs.c
***************
*** 17,24 ****
--- 17,26 ----
  
  #include "catalog/pg_type.h"
  #include "nodes/makefuncs.h"
+ #include "utils/builtins.h"
  #include "utils/lsyscache.h"
  
+ static bool parseBooleanOption(DefElem *opt);
  
  /*
   * makeA_Expr -
***************
*** 385,387 **** makeDefElemExtended(char *namespace, char *name, Node *arg,
--- 387,460 ----
  
  	return res;
  }
+ 
+ /*
+  * makeExplain -
+  *  build an ExplainStmt node by parsing the generic options list
+  */
+ ExplainStmt *
+ makeExplain(List *options, Node *query)
+ {
+ 	ExplainStmt *n = makeNode(ExplainStmt);
+ 	ListCell *lc;
+ 
+ 	n->costs = true;
+ 	n->query = query;
+ 
+ 	foreach (lc, options)
+ 	{
+ 		DefElem *opt = lfirst(lc);
+ 		if (!strcmp(opt->defname, "analyze"))
+ 			n->analyze = parseBooleanOption(opt);
+ 		else if (!strcmp(opt->defname, "verbose"))
+ 			n->verbose = parseBooleanOption(opt);
+ 		else if (!strcmp(opt->defname, "costs"))
+ 			n->costs = parseBooleanOption(opt);
+ 		else
+ 			ereport(ERROR,
+ 				(errcode(ERRCODE_UNDEFINED_PARAMETER),
+ 				 errmsg("unknown EXPLAIN option: %s", opt->defname)));
+ 	}
+ 
+ 	return n;
+ }
+ 
+ /*
+  * parseBooleanOption -
+  * 	Interpret a DefElem option as a boolean.
+  */
+ static bool
+ parseBooleanOption(DefElem *opt)
+ {
+ 	bool res;
+ 
+ 	/*
+ 	 * We interpret an omitted boolean argument as equivalent to "true", so
+ 	 * that, for example, EXPLAIN (ANALYZE) means the same thing as
+ 	 * EXPLAIN (ANALYZE ON).
+ 	 */
+ 	if (!opt->arg)
+ 	{
+ 		return true;
+ 	}
+ 	else if (IsA(opt->arg, Integer))
+ 	{
+ 		if (intVal(opt->arg) == 0)
+ 			return false;
+ 		else if (intVal(opt->arg) == 1)
+ 			return true;
+ 	}
+ 	else if (IsA(opt->arg, String))
+ 	{
+ 		if (parse_bool(strVal(opt->arg), &res))
+ 			return res;
+ 	}
+ 
+ 	ereport(ERROR,
+ 		(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ 		 errmsg("parameter \"%s\" requires a Boolean value",
+ 			opt->defname)));
+ 
+ 	/* silence compiler warning */
+ 	return false;
+ }
*** a/src/backend/parser/gram.y
--- b/src/backend/parser/gram.y
***************
*** 366,371 **** static TypeName *TableFuncTypeName(List *columns);
--- 366,376 ----
  %type <defelt>	generic_option_elem alter_generic_option_elem
  %type <list>	generic_option_list alter_generic_option_list
  
+ %type <str>		explain_option_name
+ %type <node>	explain_option_arg
+ %type <defelt>	explain_option_elem
+ %type <list>	explain_option_list
+ 
  %type <typnam>	Typename SimpleTypename ConstTypename
  				GenericType Numeric opt_float
  				Character ConstCharacter
***************
*** 6441,6457 **** opt_name_list:
   *
   *		QUERY:
   *				EXPLAIN [ANALYZE] [VERBOSE] query
   *
   *****************************************************************************/
  
  ExplainStmt: EXPLAIN opt_analyze opt_verbose ExplainableStmt
  				{
! 					ExplainStmt *n = makeNode(ExplainStmt);
  					n->analyze = $2;
  					n->verbose = $3;
- 					n->query = $4;
  					$$ = (Node *)n;
  				}
  		;
  
  ExplainableStmt:
--- 6446,6466 ----
   *
   *		QUERY:
   *				EXPLAIN [ANALYZE] [VERBOSE] query
+  *				EXPLAIN ( options ) query
   *
   *****************************************************************************/
  
  ExplainStmt: EXPLAIN opt_analyze opt_verbose ExplainableStmt
  				{
! 					ExplainStmt *n = makeExplain(NIL, (Node *) $4);
  					n->analyze = $2;
  					n->verbose = $3;
  					$$ = (Node *)n;
  				}
+ 		|	EXPLAIN '(' explain_option_list ')' ExplainableStmt
+ 				{
+ 					$$ = (Node *) makeExplain((List *) $3, (Node *) $5);
+ 				}
  		;
  
  ExplainableStmt:
***************
*** 6464,6472 **** ExplainableStmt:
  			| ExecuteStmt					/* by default all are $$=$1 */
  		;
  
  opt_analyze:
  			analyze_keyword			{ $$ = TRUE; }
! 			| /* EMPTY */			{ $$ = FALSE; }
  		;
  
  /*****************************************************************************
--- 6473,6523 ----
  			| ExecuteStmt					/* by default all are $$=$1 */
  		;
  
+ /*
+  * The precedence declaration for the opt_analyze EMPTY case, below, is
+  * necessary to prevent a shift/reduce conflict in the second production for
+  * ExplainStmt, above.  Otherwise, when the parser encounters "EXPLAIN (", it
+  * can't tell whether the "(" is the beginning of a SelectStmt or the beginning
+  * of the options list.  The precedence declaration below forces the latter
+  * interpretation.
+  *
+  * It might seem that we could get away with simply changing the definition of
+  * ExplainableStmt to use select_without_parens rather than SelectStmt, but
+  * that does not work, because select_without_parens produces expressions such
+  * as "(SELECT NULL) ORDER BY 1" that we interpret as legal queries.
+  */
  opt_analyze:
  			analyze_keyword			{ $$ = TRUE; }
! 			| /* EMPTY */			%prec UMINUS { $$ = FALSE; }
! 		;
! 
! explain_option_list:
! 			explain_option_elem
! 				{
! 					$$ = list_make1($1);
! 				}
! 			| explain_option_list ',' explain_option_elem
! 				{
! 					$$ = lappend($1, $3);
! 				}
! 		;
! 
! explain_option_elem:
! 			explain_option_name explain_option_arg
! 				{
! 					$$ = makeDefElem($1, $2);
! 				}
! 		;
! 
! explain_option_name:
! 				ColLabel			{ $$ = $1; }
! 		;
! 
! explain_option_arg:
! 			  opt_boolean			{ $$ = (Node *) makeString($1); }
! 			| ColId_or_Sconst		{ $$ = (Node *) makeString($1); }
! 			| SignedIconst			{ $$ = (Node *) makeInteger($1); }
! 			| /* EMPTY */			{ $$ = NULL; }
  		;
  
  /*****************************************************************************
*** a/src/include/commands/explain.h
--- b/src/include/commands/explain.h
***************
*** 44,49 **** extern void ExplainOnePlan(PlannedStmt *plannedstmt, ExplainStmt *stmt,
  			   TupOutputState *tstate);
  
  extern void ExplainPrintPlan(StringInfo str, QueryDesc *queryDesc,
! 				 bool analyze, bool verbose);
  
  #endif   /* EXPLAIN_H */
--- 44,49 ----
  			   TupOutputState *tstate);
  
  extern void ExplainPrintPlan(StringInfo str, QueryDesc *queryDesc,
! 				 ExplainStmt *stmt);
  
  #endif   /* EXPLAIN_H */
*** a/src/include/nodes/makefuncs.h
--- b/src/include/nodes/makefuncs.h
***************
*** 69,72 **** extern DefElem *makeDefElem(char *name, Node *arg);
--- 69,74 ----
  extern DefElem *makeDefElemExtended(char *namespace, char *name, Node *arg,
  					DefElemAction defaction);
  
+ extern ExplainStmt *makeExplain(List *options, Node *query);
+ 
  #endif   /* MAKEFUNC_H */
*** a/src/include/nodes/parsenodes.h
--- b/src/include/nodes/parsenodes.h
***************
*** 2192,2198 **** typedef struct ExplainStmt
  	NodeTag		type;
  	Node	   *query;			/* the query (as a raw parse tree) */
  	bool		verbose;		/* print plan info */
! 	bool		analyze;		/* get statistics by executing plan */
  } ExplainStmt;
  
  /* ----------------------
--- 2192,2199 ----
  	NodeTag		type;
  	Node	   *query;			/* the query (as a raw parse tree) */
  	bool		verbose;		/* print plan info */
! 	bool		analyze;		/* actually execute plan */
! 	bool		costs;			/* print costs and times */
  } ExplainStmt;
  
  /* ----------------------
#2Andres Freund
andres@anarazel.de
In reply to: Robert Haas (#1)
1 attachment(s)
Re: generic explain options v3 - RR Review

Hi Robert, Hi All,

Patch applies with some offset changes, code changes look sensible, I
personally like the new syntax and the features it may allow in future. One,
possibly big, gripe remains though:
The formerly valid statement which cannot be written without the parentheses
and stay semantically equivalent:
EXPLAIN (SELECT 1 ORDER BY 1) UNION ALL (SELECT 2 ORDER BY 1);
is now not valid anymore (The added %prec UMINUS causes the first '(' to be
recognize as start of the option list as intended).
This currently can only be resolved by using an option list like:
EXPLAIN (VERBOSE OFF) ...
Its also currently impossible to use an empty set of parentheses to resolve
this - this could easily be changed though.

I have to admit I don't see a nice solution here except living with the
incompatibility... Perhaps somebody has a better idea?

Andres

PS: The 'offset corrected' version I tested with is attached

Attachments:

explain_options-v3_offset.patchtext/x-patch; charset=UTF-8; name=explain_options-v3_offset.patchDownload
diff --git a/contrib/auto_explain/auto_explain.c b/contrib/auto_explain/auto_explain.c
index b7c0ef9..956af94 100644
*** a/contrib/auto_explain/auto_explain.c
--- b/contrib/auto_explain/auto_explain.c
***************
*** 14,19 ****
--- 14,20 ----
  
  #include "commands/explain.h"
  #include "executor/instrument.h"
+ #include "nodes/makefuncs.h"
  #include "utils/guc.h"
  
  PG_MODULE_MAGIC;
*************** explain_ExecutorEnd(QueryDesc *queryDesc
*** 196,207 ****
  		msec = queryDesc->totaltime->total * 1000.0;
  		if (msec >= auto_explain_log_min_duration)
  		{
  			StringInfoData buf;
  
  			initStringInfo(&buf);
! 			ExplainPrintPlan(&buf, queryDesc,
! 						 queryDesc->doInstrument && auto_explain_log_analyze,
! 							 auto_explain_log_verbose);
  
  			/* Remove last line break */
  			if (buf.len > 0 && buf.data[buf.len - 1] == '\n')
--- 197,210 ----
  		msec = queryDesc->totaltime->total * 1000.0;
  		if (msec >= auto_explain_log_min_duration)
  		{
+ 			ExplainStmt   *stmt = makeExplain(NIL, NULL);
  			StringInfoData buf;
  
  			initStringInfo(&buf);
! 			stmt->analyze =
! 				(queryDesc->doInstrument && auto_explain_log_analyze);
! 			stmt->verbose = auto_explain_log_verbose;
! 			ExplainPrintPlan(&buf, queryDesc, stmt);
  
  			/* Remove last line break */
  			if (buf.len > 0 && buf.data[buf.len - 1] == '\n')
diff --git a/doc/src/sgml/ref/explain.sgml b/doc/src/sgml/ref/explain.sgml
index ea9b3a6..afcab08 100644
*** a/doc/src/sgml/ref/explain.sgml
--- b/doc/src/sgml/ref/explain.sgml
*************** PostgreSQL documentation
*** 31,36 ****
--- 31,37 ----
  
   <refsynopsisdiv>
  <synopsis>
+ EXPLAIN [ ( [ { ANALYZE | VERBOSE | COSTS } [ <replaceable class="parameter">boolean_value</replaceable> ] ] [, ...] ) ] <replaceable class="parameter">statement</replaceable>
  EXPLAIN [ ANALYZE ] [ VERBOSE ] <replaceable class="parameter">statement</replaceable>
  </synopsis>
   </refsynopsisdiv>
*************** EXPLAIN [ ANALYZE ] [ VERBOSE ] <replace
*** 70,75 ****
--- 71,86 ----
     are close to reality.
    </para>
  
+   <para>
+    Only the <literal>ANALYZE</literal> and <literal>VERBOSE</literal> options
+    can be specified, and only in the order, without surrounding the option list
+    in parentheses.  Prior to <productname>PostgreSQL</productname> 8.5, the
+    unparenthesized syntax was the only one supported.  It is expected that
+    all new options will be supported only when using the parenthesized syntax,
+    which also allows a value to be specified for each option
+    (e.g. <literal>TRUE</literal> or <literal>FALSE</literal>).
+   </para>
+ 
    <important>
     <para>
      Keep in mind that the statement is actually executed when
*************** ROLLBACK;
*** 99,105 ****
      <term><literal>ANALYZE</literal></term>
      <listitem>
       <para>
!       Carry out the command and show the actual run times.
       </para>
      </listitem>
     </varlistentry>
--- 110,117 ----
      <term><literal>ANALYZE</literal></term>
      <listitem>
       <para>
!       Carry out the command and show the actual run times.  This
!       parameter defaults to <command>OFF</command>.
       </para>
      </listitem>
     </varlistentry>
*************** ROLLBACK;
*** 108,114 ****
      <term><literal>VERBOSE</literal></term>
      <listitem>
       <para>
!       Include the output column list for each node in the plan tree.
       </para>
      </listitem>
     </varlistentry>
--- 120,152 ----
      <term><literal>VERBOSE</literal></term>
      <listitem>
       <para>
!       Include the output column list for each node in the plan tree.  This
!       parameter defaults to <command>OFF</command>.
!      </para>
!     </listitem>
!    </varlistentry>
! 
!    <varlistentry>
!     <term><literal>COSTS</literal></term>
!     <listitem>
!      <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>ON</command>.
!      </para>
!     </listitem>
!    </varlistentry>
! 
!    <varlistentry>
!     <term><replaceable class="parameter" />boolean_value</replaceable></term>
!     <listitem>
!      <para>
!       Specifies whether the named parameter should be turned on or off.  You
!       can use the values <literal>TRUE</literal>, <literal>ON</literal>,
!       <literal>YES</literal>, or <literal>1</literal> to request the stated
!       option, and <literal>FALSE</literal>, <literal>OFF</literal>,
!       <literal>NO</literal>, or <literal>0</literal>.  If the Boolean value
!       is omitted, it defaults to <literal>TRUE</literal>.
       </para>
      </listitem>
     </varlistentry>
*************** EXPLAIN SELECT * FROM foo WHERE i = 4;
*** 202,207 ****
--- 240,259 ----
    </para>
  
    <para>
+    Here is the same plan with costs suppressed:
+ 
+ <programlisting>
+ EXPLAIN (COSTS OFF) SELECT * FROM foo WHERE i = 4;
+ 
+         QUERY PLAN
+ ----------------------------
+  Index Scan using fi on foo
+    Index Cond: (i = 4)
+ (2 rows)
+ </programlisting>
+   </para>
+ 
+   <para>
     Here is an example of a query plan for a query using an aggregate
     function:
  
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 950a2f1..efe436f 100644
*** a/src/backend/commands/explain.c
--- b/src/backend/commands/explain.c
*************** explain_get_index_name_hook_type explain
*** 42,50 ****
--- 42,52 ----
  
  typedef struct ExplainState
  {
+ 	StringInfo	str;			/* output buffer */
  	/* options */
  	bool		printTList;		/* print plan targetlists */
  	bool		printAnalyze;	/* print actual times */
+ 	bool		printCosts;		/* print costs */
  	/* other states */
  	PlannedStmt *pstmt;			/* top of plan */
  	List	   *rtable;			/* range table */
*************** static void ExplainOneQuery(Query *query
*** 56,78 ****
  static void report_triggers(ResultRelInfo *rInfo, bool show_relname,
  				StringInfo buf);
  static double elapsed_time(instr_time *starttime);
! static void explain_outNode(StringInfo str,
! 				Plan *plan, PlanState *planstate,
! 				Plan *outer_plan,
! 				int indent, ExplainState *es);
! static void show_plan_tlist(Plan *plan,
! 				StringInfo str, int indent, ExplainState *es);
  static void show_scan_qual(List *qual, const char *qlabel,
! 			   int scanrelid, Plan *scan_plan, Plan *outer_plan,
! 			   StringInfo str, int indent, ExplainState *es);
  static void show_upper_qual(List *qual, const char *qlabel, Plan *plan,
! 				StringInfo str, int indent, ExplainState *es);
! static void show_sort_keys(Plan *sortplan, int nkeys, AttrNumber *keycols,
! 			   const char *qlabel,
! 			   StringInfo str, int indent, ExplainState *es);
! static void show_sort_info(SortState *sortstate,
! 			   StringInfo str, int indent, ExplainState *es);
  static const char *explain_get_index_name(Oid indexId);
  
  
  /*
--- 58,80 ----
  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 ExplainSubNodes(List *plans, int indent, ExplainState *es);
  
  
  /*
*************** ExplainOnePlan(PlannedStmt *plannedstmt,
*** 267,273 ****
  
  	/* Create textual dump of plan tree */
  	initStringInfo(&buf);
! 	ExplainPrintPlan(&buf, queryDesc, stmt->analyze, stmt->verbose);
  
  	/*
  	 * If we ran the command, run any AFTER triggers it queued.  (Note this
--- 269,275 ----
  
  	/* Create textual dump of plan tree */
  	initStringInfo(&buf);
! 	ExplainPrintPlan(&buf, queryDesc, stmt);
  
  	/*
  	 * If we ran the command, run any AFTER triggers it queued.  (Note this
*************** ExplainOnePlan(PlannedStmt *plannedstmt,
*** 339,360 ****
   * NB: will not work on utility statements
   */
  void
! ExplainPrintPlan(StringInfo str, QueryDesc *queryDesc,
! 				 bool analyze, bool verbose)
  {
  	ExplainState es;
  
  	Assert(queryDesc->plannedstmt != NULL);
  
  	memset(&es, 0, sizeof(es));
! 	es.printTList = verbose;
! 	es.printAnalyze = analyze;
  	es.pstmt = queryDesc->plannedstmt;
  	es.rtable = queryDesc->plannedstmt->rtable;
  
! 	explain_outNode(str,
! 					queryDesc->plannedstmt->planTree, queryDesc->planstate,
! 					NULL, 0, &es);
  }
  
  /*
--- 341,362 ----
   * NB: will not work on utility statements
   */
  void
! ExplainPrintPlan(StringInfo str, QueryDesc *queryDesc, ExplainStmt *stmt)
  {
  	ExplainState es;
  
  	Assert(queryDesc->plannedstmt != NULL);
  
  	memset(&es, 0, sizeof(es));
! 	es.str = str;
! 	es.printTList = stmt->verbose;
! 	es.printAnalyze = stmt->analyze;
! 	es.printCosts = stmt->costs;
  	es.pstmt = queryDesc->plannedstmt;
  	es.rtable = queryDesc->plannedstmt->rtable;
  
! 	ExplainNode(queryDesc->plannedstmt->planTree, queryDesc->planstate,
! 				NULL, 0, &es);
  }
  
  /*
*************** elapsed_time(instr_time *starttime)
*** 414,420 ****
  }
  
  /*
!  * explain_outNode -
   *	  converts a Plan node into ascii string and appends it to 'str'
   *
   * planstate points to the executor state node corresponding to the plan node.
--- 416,422 ----
  }
  
  /*
!  * ExplainNode -
   *	  converts a Plan node into ascii string and appends it to 'str'
   *
   * planstate points to the executor state node corresponding to the plan node.
*************** elapsed_time(instr_time *starttime)
*** 426,442 ****
   * deciphering runtime keys of an inner indexscan.
   */
  static void
! explain_outNode(StringInfo str,
! 				Plan *plan, PlanState *planstate,
! 				Plan *outer_plan,
! 				int indent, ExplainState *es)
  {
  	const char *pname;
! 	int			i;
  
  	if (plan == NULL)
  	{
! 		appendStringInfoChar(str, '\n');
  		return;
  	}
  
--- 428,448 ----
   * deciphering runtime keys of an inner indexscan.
   */
  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;
  	}
  
*************** explain_outNode(StringInfo str,
*** 656,798 ****
  			break;
  	}
  
! 	appendStringInfoString(str, pname);
  	switch (nodeTag(plan))
  	{
  		case T_IndexScan:
  			if (ScanDirectionIsBackward(((IndexScan *) plan)->indexorderdir))
! 				appendStringInfoString(str, " Backward");
! 			appendStringInfo(str, " using %s",
  					  explain_get_index_name(((IndexScan *) plan)->indexid));
  			/* FALL THRU */
  		case T_SeqScan:
  		case T_BitmapHeapScan:
  		case T_TidScan:
- 			if (((Scan *) plan)->scanrelid > 0)
- 			{
- 				RangeTblEntry *rte = rt_fetch(((Scan *) plan)->scanrelid,
- 											  es->rtable);
- 				char	   *relname;
- 
- 				/* Assume it's on a real relation */
- 				Assert(rte->rtekind == RTE_RELATION);
- 
- 				/* We only show the rel name, not schema name */
- 				relname = get_rel_name(rte->relid);
- 
- 				appendStringInfo(str, " on %s",
- 								 quote_identifier(relname));
- 				if (strcmp(rte->eref->aliasname, relname) != 0)
- 					appendStringInfo(str, " %s",
- 									 quote_identifier(rte->eref->aliasname));
- 			}
- 			break;
- 		case T_BitmapIndexScan:
- 			appendStringInfo(str, " on %s",
- 				explain_get_index_name(((BitmapIndexScan *) plan)->indexid));
- 			break;
  		case T_SubqueryScan:
- 			if (((Scan *) plan)->scanrelid > 0)
- 			{
- 				RangeTblEntry *rte = rt_fetch(((Scan *) plan)->scanrelid,
- 											  es->rtable);
- 
- 				appendStringInfo(str, " %s",
- 								 quote_identifier(rte->eref->aliasname));
- 			}
- 			break;
  		case T_FunctionScan:
- 			if (((Scan *) plan)->scanrelid > 0)
- 			{
- 				RangeTblEntry *rte = rt_fetch(((Scan *) plan)->scanrelid,
- 											  es->rtable);
- 				Node	   *funcexpr;
- 				char	   *proname;
- 
- 				/* Assert it's on a RangeFunction */
- 				Assert(rte->rtekind == RTE_FUNCTION);
- 
- 				/*
- 				 * If the expression is still a function call, we can get the
- 				 * real name of the function.  Otherwise, punt (this can
- 				 * happen if the optimizer simplified away the function call,
- 				 * for example).
- 				 */
- 				funcexpr = ((FunctionScan *) plan)->funcexpr;
- 				if (funcexpr && IsA(funcexpr, FuncExpr))
- 				{
- 					Oid			funcid = ((FuncExpr *) funcexpr)->funcid;
- 
- 					/* We only show the func name, not schema name */
- 					proname = get_func_name(funcid);
- 				}
- 				else
- 					proname = rte->eref->aliasname;
- 
- 				appendStringInfo(str, " on %s",
- 								 quote_identifier(proname));
- 				if (strcmp(rte->eref->aliasname, proname) != 0)
- 					appendStringInfo(str, " %s",
- 									 quote_identifier(rte->eref->aliasname));
- 			}
- 			break;
  		case T_ValuesScan:
- 			if (((Scan *) plan)->scanrelid > 0)
- 			{
- 				RangeTblEntry *rte = rt_fetch(((Scan *) plan)->scanrelid,
- 											  es->rtable);
- 				char	   *valsname;
- 
- 				/* Assert it's on a values rte */
- 				Assert(rte->rtekind == RTE_VALUES);
- 
- 				valsname = rte->eref->aliasname;
- 
- 				appendStringInfo(str, " on %s",
- 								 quote_identifier(valsname));
- 			}
- 			break;
  		case T_CteScan:
- 			if (((Scan *) plan)->scanrelid > 0)
- 			{
- 				RangeTblEntry *rte = rt_fetch(((Scan *) plan)->scanrelid,
- 											  es->rtable);
- 
- 				/* Assert it's on a non-self-reference CTE */
- 				Assert(rte->rtekind == RTE_CTE);
- 				Assert(!rte->self_reference);
- 
- 				appendStringInfo(str, " on %s",
- 								 quote_identifier(rte->ctename));
- 				if (strcmp(rte->eref->aliasname, rte->ctename) != 0)
- 					appendStringInfo(str, " %s",
- 									 quote_identifier(rte->eref->aliasname));
- 			}
- 			break;
  		case T_WorkTableScan:
! 			if (((Scan *) plan)->scanrelid > 0)
! 			{
! 				RangeTblEntry *rte = rt_fetch(((Scan *) plan)->scanrelid,
! 											  es->rtable);
! 
! 				/* Assert it's on a self-reference CTE */
! 				Assert(rte->rtekind == RTE_CTE);
! 				Assert(rte->self_reference);
! 
! 				appendStringInfo(str, " on %s",
! 								 quote_identifier(rte->ctename));
! 				if (strcmp(rte->eref->aliasname, rte->ctename) != 0)
! 					appendStringInfo(str, " %s",
! 									 quote_identifier(rte->eref->aliasname));
! 			}
  			break;
  		default:
  			break;
  	}
  
! 	appendStringInfo(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
--- 662,698 ----
  			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:
  		case T_TidScan:
  		case T_SubqueryScan:
  		case T_FunctionScan:
  		case T_ValuesScan:
  		case T_CteScan:
  		case T_WorkTableScan:
! 			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->printCosts)
! 		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
*************** explain_outNode(StringInfo str,
*** 805,871 ****
  	{
  		double		nloops = planstate->instrument->nloops;
  
! 		appendStringInfo(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->printAnalyze)
! 		appendStringInfo(str, " (never executed)");
! 	appendStringInfoChar(str, '\n');
  
  	/* target list */
  	if (es->printTList)
! 		show_plan_tlist(plan, str, indent, es);
  
  	/* quals, sort keys, etc */
  	switch (nodeTag(plan))
  	{
  		case T_IndexScan:
  			show_scan_qual(((IndexScan *) plan)->indexqualorig,
! 						   "Index Cond",
! 						   ((Scan *) plan)->scanrelid,
! 						   plan, outer_plan,
! 						   str, indent, es);
  			show_scan_qual(plan->qual,
! 						   "Filter",
! 						   ((Scan *) plan)->scanrelid,
! 						   plan, outer_plan,
! 						   str, indent, es);
  			break;
  		case T_BitmapIndexScan:
  			show_scan_qual(((BitmapIndexScan *) plan)->indexqualorig,
! 						   "Index Cond",
! 						   ((Scan *) plan)->scanrelid,
! 						   plan, outer_plan,
! 						   str, indent, es);
  			break;
  		case T_BitmapHeapScan:
  			/* XXX do we want to show this in production? */
  			show_scan_qual(((BitmapHeapScan *) plan)->bitmapqualorig,
! 						   "Recheck Cond",
! 						   ((Scan *) plan)->scanrelid,
! 						   plan, outer_plan,
! 						   str, indent, es);
  			/* FALL THRU */
  		case T_SeqScan:
  		case T_FunctionScan:
  		case T_ValuesScan:
  		case T_CteScan:
  		case T_WorkTableScan:
- 			show_scan_qual(plan->qual,
- 						   "Filter",
- 						   ((Scan *) plan)->scanrelid,
- 						   plan, outer_plan,
- 						   str, indent, es);
- 			break;
  		case T_SubqueryScan:
  			show_scan_qual(plan->qual,
! 						   "Filter",
! 						   ((Scan *) plan)->scanrelid,
! 						   plan, outer_plan,
! 						   str, indent, es);
  			break;
  		case T_TidScan:
  			{
--- 705,751 ----
  	{
  		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->printAnalyze)
! 		appendStringInfo(es->str, " (never executed)");
! 	appendStringInfoChar(es->str, '\n');
  
  	/* target list */
  	if (es->printTList)
! 		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:
  			/* XXX do we want to show this in production? */
  			show_scan_qual(((BitmapHeapScan *) plan)->bitmapqualorig,
! 						   "Recheck Cond", plan, outer_plan, indent, es);
  			/* FALL THRU */
  		case T_SeqScan:
  		case T_FunctionScan:
  		case T_ValuesScan:
  		case T_CteScan:
  		case T_WorkTableScan:
  		case T_SubqueryScan:
  			show_scan_qual(plan->qual,
! 						   "Filter", plan, outer_plan, indent, es);
  			break;
  		case T_TidScan:
  			{
*************** explain_outNode(StringInfo str,
*** 878,946 ****
  				if (list_length(tidquals) > 1)
  					tidquals = list_make1(make_orclause(tidquals));
  				show_scan_qual(tidquals,
! 							   "TID Cond",
! 							   ((Scan *) plan)->scanrelid,
! 							   plan, outer_plan,
! 							   str, indent, es);
  				show_scan_qual(plan->qual,
! 							   "Filter",
! 							   ((Scan *) plan)->scanrelid,
! 							   plan, outer_plan,
! 							   str, indent, es);
  			}
  			break;
  		case T_NestLoop:
  			show_upper_qual(((NestLoop *) plan)->join.joinqual,
! 							"Join Filter", plan,
! 							str, indent, es);
! 			show_upper_qual(plan->qual,
! 							"Filter", plan,
! 							str, indent, es);
  			break;
  		case T_MergeJoin:
  			show_upper_qual(((MergeJoin *) plan)->mergeclauses,
! 							"Merge Cond", plan,
! 							str, indent, es);
  			show_upper_qual(((MergeJoin *) plan)->join.joinqual,
! 							"Join Filter", plan,
! 							str, indent, es);
! 			show_upper_qual(plan->qual,
! 							"Filter", plan,
! 							str, indent, es);
  			break;
  		case T_HashJoin:
  			show_upper_qual(((HashJoin *) plan)->hashclauses,
! 							"Hash Cond", plan,
! 							str, indent, es);
  			show_upper_qual(((HashJoin *) plan)->join.joinqual,
! 							"Join Filter", plan,
! 							str, indent, es);
! 			show_upper_qual(plan->qual,
! 							"Filter", plan,
! 							str, indent, es);
  			break;
  		case T_Agg:
  		case T_Group:
! 			show_upper_qual(plan->qual,
! 							"Filter", plan,
! 							str, indent, es);
  			break;
  		case T_Sort:
! 			show_sort_keys(plan,
! 						   ((Sort *) plan)->numCols,
! 						   ((Sort *) plan)->sortColIdx,
! 						   "Sort Key",
! 						   str, indent, es);
! 			show_sort_info((SortState *) planstate,
! 						   str, indent, es);
  			break;
  		case T_Result:
  			show_upper_qual((List *) ((Result *) plan)->resconstantqual,
! 							"One-Time Filter", plan,
! 							str, indent, es);
! 			show_upper_qual(plan->qual,
! 							"Filter", plan,
! 							str, indent, es);
  			break;
  		default:
  			break;
--- 758,799 ----
  				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;
*************** explain_outNode(StringInfo str,
*** 948,1081 ****
  
  	/* initPlan-s */
  	if (plan->initPlan)
! 	{
! 		ListCell   *lst;
! 
! 		foreach(lst, planstate->initPlan)
! 		{
! 			SubPlanState *sps = (SubPlanState *) lfirst(lst);
! 			SubPlan    *sp = (SubPlan *) sps->xprstate.expr;
! 
! 			for (i = 0; i < indent; i++)
! 				appendStringInfo(str, "  ");
! 			appendStringInfo(str, "  %s\n", sp->plan_name);
! 			for (i = 0; i < indent; i++)
! 				appendStringInfo(str, "  ");
! 			appendStringInfo(str, "    ->  ");
! 			explain_outNode(str,
! 							exec_subplan_get_plan(es->pstmt, sp),
! 							sps->planstate,
! 							NULL,
! 							indent + 4, es);
! 		}
! 	}
  
  	/* lefttree */
  	if (outerPlan(plan))
  	{
- 		for (i = 0; i < indent; i++)
- 			appendStringInfo(str, "  ");
- 		appendStringInfo(str, "  ->  ");
- 
  		/*
  		 * Ordinarily we don't pass down our own outer_plan value to our child
  		 * nodes, but in bitmap scan trees we must, since the bottom
  		 * BitmapIndexScan nodes may have outer references.
  		 */
! 		explain_outNode(str, outerPlan(plan),
! 						outerPlanState(planstate),
! 						IsA(plan, BitmapHeapScan) ? outer_plan : NULL,
! 						indent + 3, es);
  	}
  
  	/* righttree */
  	if (innerPlan(plan))
  	{
! 		for (i = 0; i < indent; i++)
! 			appendStringInfo(str, "  ");
! 		appendStringInfo(str, "  ->  ");
! 		explain_outNode(str, innerPlan(plan),
! 						innerPlanState(planstate),
! 						outerPlan(plan),
! 						indent + 3, es);
! 	}
! 
! 	if (IsA(plan, Append))
! 	{
! 		Append	   *appendplan = (Append *) plan;
! 		AppendState *appendstate = (AppendState *) planstate;
! 		ListCell   *lst;
! 		int			j;
! 
! 		j = 0;
! 		foreach(lst, appendplan->appendplans)
! 		{
! 			Plan	   *subnode = (Plan *) lfirst(lst);
! 
! 			for (i = 0; i < indent; i++)
! 				appendStringInfo(str, "  ");
! 			appendStringInfo(str, "  ->  ");
! 
! 			/*
! 			 * Ordinarily we don't pass down our own outer_plan value to our
! 			 * child nodes, but in an Append we must, since we might be
! 			 * looking at an appendrel indexscan with outer references from
! 			 * the member scans.
! 			 */
! 			explain_outNode(str, subnode,
! 							appendstate->appendplans[j],
! 							outer_plan,
! 							indent + 3, es);
! 			j++;
! 		}
! 	}
! 
! 	if (IsA(plan, BitmapAnd))
! 	{
! 		BitmapAnd  *bitmapandplan = (BitmapAnd *) plan;
! 		BitmapAndState *bitmapandstate = (BitmapAndState *) planstate;
! 		ListCell   *lst;
! 		int			j;
! 
! 		j = 0;
! 		foreach(lst, bitmapandplan->bitmapplans)
! 		{
! 			Plan	   *subnode = (Plan *) lfirst(lst);
! 
! 			for (i = 0; i < indent; i++)
! 				appendStringInfo(str, "  ");
! 			appendStringInfo(str, "  ->  ");
! 
! 			explain_outNode(str, subnode,
! 							bitmapandstate->bitmapplans[j],
! 							outer_plan, /* pass down same outer plan */
! 							indent + 3, es);
! 			j++;
! 		}
  	}
  
! 	if (IsA(plan, BitmapOr))
! 	{
! 		BitmapOr   *bitmaporplan = (BitmapOr *) plan;
! 		BitmapOrState *bitmaporstate = (BitmapOrState *) planstate;
! 		ListCell   *lst;
! 		int			j;
! 
! 		j = 0;
! 		foreach(lst, bitmaporplan->bitmapplans)
! 		{
! 			Plan	   *subnode = (Plan *) lfirst(lst);
! 
! 			for (i = 0; i < indent; i++)
! 				appendStringInfo(str, "  ");
! 			appendStringInfo(str, "  ->  ");
! 
! 			explain_outNode(str, subnode,
! 							bitmaporstate->bitmapplans[j],
! 							outer_plan, /* pass down same outer plan */
! 							indent + 3, es);
! 			j++;
! 		}
  	}
  
  	if (IsA(plan, SubqueryScan))
--- 801,846 ----
  
  	/* initPlan-s */
  	if (plan->initPlan)
! 		ExplainSubNodes(planstate->initPlan, indent, es);
  
  	/* lefttree */
  	if (outerPlan(plan))
  	{
  		/*
  		 * Ordinarily we don't pass down our own outer_plan value to our child
  		 * nodes, but in bitmap scan trees we must, since the bottom
  		 * BitmapIndexScan nodes may have outer references.
  		 */
! 		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);
  	}
  
! 	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;
! 		default:
! 			break;
  	}
  
  	if (IsA(plan, SubqueryScan))
*************** explain_outNode(StringInfo str,
*** 1084,1130 ****
  		SubqueryScanState *subquerystate = (SubqueryScanState *) planstate;
  		Plan	   *subnode = subqueryscan->subplan;
  
! 		for (i = 0; i < indent; i++)
! 			appendStringInfo(str, "  ");
! 		appendStringInfo(str, "  ->  ");
! 
! 		explain_outNode(str, subnode,
! 						subquerystate->subplan,
! 						NULL,
! 						indent + 3, es);
  	}
  
  	/* subPlan-s */
  	if (planstate->subPlan)
! 	{
! 		ListCell   *lst;
! 
! 		foreach(lst, planstate->subPlan)
! 		{
! 			SubPlanState *sps = (SubPlanState *) lfirst(lst);
! 			SubPlan    *sp = (SubPlan *) sps->xprstate.expr;
! 
! 			for (i = 0; i < indent; i++)
! 				appendStringInfo(str, "  ");
! 			appendStringInfo(str, "  %s\n", sp->plan_name);
! 			for (i = 0; i < indent; i++)
! 				appendStringInfo(str, "  ");
! 			appendStringInfo(str, "    ->  ");
! 			explain_outNode(str,
! 							exec_subplan_get_plan(es->pstmt, sp),
! 							sps->planstate,
! 							NULL,
! 							indent + 4, es);
! 		}
! 	}
  }
  
  /*
   * Show the targetlist of a plan node
   */
  static void
! show_plan_tlist(Plan *plan,
! 				StringInfo str, int indent, ExplainState *es)
  {
  	List	   *context;
  	bool		useprefix;
--- 849,867 ----
  		SubqueryScanState *subquerystate = (SubqueryScanState *) planstate;
  		Plan	   *subnode = subqueryscan->subplan;
  
! 		ExplainNode(subnode, subquerystate->subplan, NULL, indent + 3, es);
  	}
  
  	/* subPlan-s */
  	if (planstate->subPlan)
! 		ExplainSubNodes(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;
*************** show_plan_tlist(Plan *plan,
*** 1149,1157 ****
  	useprefix = list_length(es->rtable) > 1;
  
  	/* Emit line prefix */
! 	for (i = 0; i < indent; i++)
! 		appendStringInfo(str, "  ");
! 	appendStringInfo(str, "  Output: ");
  
  	/* Deparse each non-junk result column */
  	i = 0;
--- 886,893 ----
  	useprefix = list_length(es->rtable) > 1;
  
  	/* Emit line prefix */
! 	appendStringInfoSpaces(es->str, indent * 2);
! 	appendStringInfo(es->str, "  Output: ");
  
  	/* Deparse each non-junk result column */
  	i = 0;
*************** show_plan_tlist(Plan *plan,
*** 1162,1192 ****
  		if (tle->resjunk)
  			continue;
  		if (i++ > 0)
! 			appendStringInfo(str, ", ");
! 		appendStringInfoString(str,
  							   deparse_expression((Node *) tle->expr, context,
  												  useprefix, false));
  	}
  
! 	appendStringInfoChar(str, '\n');
  }
  
  /*
!  * Show a qualifier expression for a scan plan node
   *
   * Note: outer_plan is the referent for any OUTER vars in the scan qual;
   * this would be the outer side of a nestloop plan.  Pass NULL if none.
   */
  static void
! show_scan_qual(List *qual, const char *qlabel,
! 			   int scanrelid, Plan *scan_plan, Plan *outer_plan,
! 			   StringInfo str, int indent, ExplainState *es)
  {
  	List	   *context;
- 	bool		useprefix;
  	Node	   *node;
  	char	   *exprstr;
- 	int			i;
  
  	/* No work if empty qual */
  	if (qual == NIL)
--- 898,925 ----
  		if (tle->resjunk)
  			continue;
  		if (i++ > 0)
! 			appendStringInfo(es->str, ", ");
! 		appendStringInfoString(es->str,
  							   deparse_expression((Node *) tle->expr, context,
  												  useprefix, false));
  	}
  
! 	appendStringInfoChar(es->str, '\n');
  }
  
  /*
!  * Show a qualifier expression
   *
   * Note: outer_plan is the referent for any OUTER vars in the scan qual;
   * this would be the outer side of a nestloop plan.  Pass NULL if none.
   */
  static void
! show_qual(List *qual, const char *qlabel, Plan *plan, Plan *outer_plan,
! 		  int indent, bool useprefix, ExplainState *es)
  {
  	List	   *context;
  	Node	   *node;
  	char	   *exprstr;
  
  	/* No work if empty qual */
  	if (qual == NIL)
*************** show_scan_qual(List *qual, const char *q
*** 1196,1214 ****
  	node = (Node *) make_ands_explicit(qual);
  
  	/* Set up deparsing context */
! 	context = deparse_context_for_plan((Node *) scan_plan,
  									   (Node *) outer_plan,
  									   es->rtable,
  									   es->pstmt->subplans);
- 	useprefix = (outer_plan != NULL || IsA(scan_plan, SubqueryScan));
  
  	/* Deparse the expression */
  	exprstr = deparse_expression(node, context, useprefix, false);
  
  	/* And add to str */
! 	for (i = 0; i < indent; i++)
! 		appendStringInfo(str, "  ");
! 	appendStringInfo(str, "  %s: %s\n", qlabel, exprstr);
  }
  
  /*
--- 929,958 ----
  	node = (Node *) make_ands_explicit(qual);
  
  	/* Set up deparsing context */
! 	context = deparse_context_for_plan((Node *) plan,
  									   (Node *) outer_plan,
  									   es->rtable,
  									   es->pstmt->subplans);
  
  	/* Deparse the expression */
  	exprstr = deparse_expression(node, context, useprefix, false);
  
  	/* And add to str */
! 	appendStringInfoSpaces(es->str, indent * 2);
! 	appendStringInfo(es->str, "  %s: %s\n", qlabel, exprstr);
! }
! 
! /*
!  * Show a qualifier expression for a scan plan node
!  */
! static void
! show_scan_qual(List *qual, const char *qlabel,
! 			   Plan *scan_plan, Plan *outer_plan,
! 			   int indent, ExplainState *es)
! {
! 	bool		useprefix =
! 		(outer_plan != NULL || IsA(scan_plan, SubqueryScan));
! 	show_qual(qual, qlabel, scan_plan, outer_plan, indent, useprefix, es);
  }
  
  /*
*************** show_scan_qual(List *qual, const char *q
*** 1216,1270 ****
   */
  static void
  show_upper_qual(List *qual, const char *qlabel, Plan *plan,
! 				StringInfo str, int indent, ExplainState *es)
  {
! 	List	   *context;
! 	bool		useprefix;
! 	Node	   *node;
! 	char	   *exprstr;
! 	int			i;
! 
! 	/* No work if empty qual */
! 	if (qual == NIL)
! 		return;
! 
! 	/* Set up deparsing context */
! 	context = deparse_context_for_plan((Node *) plan,
! 									   NULL,
! 									   es->rtable,
! 									   es->pstmt->subplans);
! 	useprefix = list_length(es->rtable) > 1;
! 
! 	/* Deparse the expression */
! 	node = (Node *) make_ands_explicit(qual);
! 	exprstr = deparse_expression(node, context, useprefix, false);
  
! 	/* And add to str */
! 	for (i = 0; i < indent; i++)
! 		appendStringInfo(str, "  ");
! 	appendStringInfo(str, "  %s: %s\n", qlabel, exprstr);
  }
  
  /*
   * Show the sort keys for a Sort node.
   */
  static void
! show_sort_keys(Plan *sortplan, int nkeys, AttrNumber *keycols,
! 			   const char *qlabel,
! 			   StringInfo str, int indent, ExplainState *es)
  {
  	List	   *context;
  	bool		useprefix;
  	int			keyno;
  	char	   *exprstr;
! 	int			i;
  
  	if (nkeys <= 0)
  		return;
  
! 	for (i = 0; i < indent; i++)
! 		appendStringInfo(str, "  ");
! 	appendStringInfo(str, "  %s: ", qlabel);
  
  	/* Set up deparsing context */
  	context = deparse_context_for_plan((Node *) sortplan,
--- 960,990 ----
   */
  static void
  show_upper_qual(List *qual, const char *qlabel, Plan *plan,
! 				int indent, ExplainState *es)
  {
! 	bool		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)
  {
  	List	   *context;
  	bool		useprefix;
  	int			keyno;
  	char	   *exprstr;
! 	int			nkeys = ((Sort *) sortplan)->numCols;
! 	AttrNumber *keycols = ((Sort *) sortplan)->sortColIdx;
  
  	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,
*************** show_sort_keys(Plan *sortplan, int nkeys
*** 1286,1316 ****
  									 useprefix, true);
  		/* And add to str */
  		if (keyno > 0)
! 			appendStringInfo(str, ", ");
! 		appendStringInfoString(str, exprstr);
  	}
  
! 	appendStringInfo(str, "\n");
  }
  
  /*
   * If it's EXPLAIN ANALYZE, show tuplesort explain info for a sort node
   */
  static void
! show_sort_info(SortState *sortstate,
! 			   StringInfo str, int indent, ExplainState *es)
  {
  	Assert(IsA(sortstate, SortState));
  	if (es->printAnalyze && sortstate->sort_Done &&
  		sortstate->tuplesortstate != NULL)
  	{
  		char	   *sortinfo;
- 		int			i;
  
  		sortinfo = tuplesort_explain((Tuplesortstate *) sortstate->tuplesortstate);
! 		for (i = 0; i < indent; i++)
! 			appendStringInfo(str, "  ");
! 		appendStringInfo(str, "  %s\n", sortinfo);
  		pfree(sortinfo);
  	}
  }
--- 1006,1033 ----
  									 useprefix, true);
  		/* And add to str */
  		if (keyno > 0)
! 			appendStringInfo(es->str, ", ");
! 		appendStringInfoString(es->str, exprstr);
  	}
  
! 	appendStringInfo(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->printAnalyze && 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);
  	}
  }
*************** explain_get_index_name(Oid indexId)
*** 1340,1342 ****
--- 1057,1168 ----
  	}
  	return result;
  }
+ 
+ /*
+  * Explain details for Scan nodes.
+  */
+ static void
+ ExplainScanTarget(Scan *plan, ExplainState *es)
+ {
+ 	char *objectname = NULL;
+ 	Node *funcexpr;
+ 	RangeTblEntry *rte;
+ 
+ 	if (plan->scanrelid <= 0)
+ 		return;
+ 	rte = rt_fetch(plan->scanrelid, es->rtable);
+ 
+ 	switch (nodeTag(plan))
+ 	{
+ 		case T_IndexScan:
+ 		case T_SeqScan:
+ 		case T_BitmapHeapScan:
+ 		case T_TidScan:
+ 			/* Assert it's on a real relation */
+ 			Assert(rte->rtekind == RTE_RELATION);
+ 			objectname = get_rel_name(rte->relid);
+ 			break;
+ 		case T_FunctionScan:
+ 			/* Assert it's on a RangeFunction */
+ 			Assert(rte->rtekind == RTE_FUNCTION);
+ 
+ 			/*
+ 			 * If the expression is still a function call, we can get the
+ 			 * real name of the function.  Otherwise, punt (this can
+ 			 * happen if the optimizer simplified away the function call,
+ 			 * for example).
+ 			 */
+ 			funcexpr = ((FunctionScan *) plan)->funcexpr;
+ 			if (funcexpr && IsA(funcexpr, FuncExpr))
+ 			{
+ 				Oid			funcid = ((FuncExpr *) funcexpr)->funcid;
+ 				objectname = get_func_name(funcid);
+ 			}
+ 			break;
+ 		case T_ValuesScan:
+ 			Assert(rte->rtekind == RTE_VALUES);
+ 			break;
+ 		case T_CteScan:
+ 			/* Assert it's on a non-self-reference CTE */
+ 			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",
+ 						 quote_identifier(rte->eref->aliasname));
+ }
+ 
+ /*
+  * Explain details for Append, BitmapAnd, or BitmapOr constutent plans.
+  * Ordinarily we don't pass down outer_plan value to our child nodes, but in
+  * an Append, BitmapAnd, or BitmapOr we must, since these nodes can have outer
+  * references from the member scans.
+  */
+ static void
+ ExplainMemberNodes(List *plans, PlanState **planstate, Plan *outer_plan,
+ 		           int indent, ExplainState *es)
+ {
+ 	ListCell   *lst;
+ 	int			j = 0;
+ 
+ 	foreach(lst, plans)
+ 	{
+ 		ExplainNode((Plan *) lfirst(lst), planstate[j], outer_plan,
+ 					indent + 3, es);
+ 		++j;
+ 	}
+ }
+ 
+ /*
+  * Explain a list of Subplans (or initPlans, which use SubPlan nodes).
+  */
+ static void
+ ExplainSubNodes(List *plans, int indent, ExplainState *es)
+ {
+ 	ListCell   *lst;
+ 
+ 	foreach(lst, plans)
+ 	{
+ 		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);
+ 	}
+ }
diff --git a/src/backend/lib/stringinfo.c b/src/backend/lib/stringinfo.c
index b09f206..5ad884e 100644
*** a/src/backend/lib/stringinfo.c
--- b/src/backend/lib/stringinfo.c
*************** appendStringInfoChar(StringInfo str, cha
*** 187,192 ****
--- 187,209 ----
  }
  
  /*
+  * appendStringInfoSpaces
+  *
+  * Append spaces to a buffer.
+  */
+ void
+ appendStringInfoSpaces(StringInfo str, int count)
+ {
+ 	/* Make more room if needed */
+ 	enlargeStringInfo(str, count);
+ 
+ 	/* OK, append the spaces */
+ 	while (--count >= 0)
+ 		str->data[str->len++] = ' ';
+ 	str->data[str->len] = '\0';
+ }
+ 
+ /*
   * appendBinaryStringInfo
   *
   * Append arbitrary binary data to a StringInfo, allocating more space
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index 1976648..6da1c50 100644
*** a/src/backend/nodes/copyfuncs.c
--- b/src/backend/nodes/copyfuncs.c
*************** _copyExplainStmt(ExplainStmt *from)
*** 2876,2881 ****
--- 2876,2882 ----
  	COPY_NODE_FIELD(query);
  	COPY_SCALAR_FIELD(verbose);
  	COPY_SCALAR_FIELD(analyze);
+ 	COPY_SCALAR_FIELD(costs);
  
  	return newnode;
  }
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index 8b466f4..61f2679 100644
*** a/src/backend/nodes/equalfuncs.c
--- b/src/backend/nodes/equalfuncs.c
*************** _equalExplainStmt(ExplainStmt *a, Explai
*** 1468,1473 ****
--- 1468,1474 ----
  	COMPARE_NODE_FIELD(query);
  	COMPARE_SCALAR_FIELD(verbose);
  	COMPARE_SCALAR_FIELD(analyze);
+ 	COMPARE_SCALAR_FIELD(costs);
  
  	return true;
  }
diff --git a/src/backend/nodes/makefuncs.c b/src/backend/nodes/makefuncs.c
index 9ed9018..83d4248 100644
*** a/src/backend/nodes/makefuncs.c
--- b/src/backend/nodes/makefuncs.c
***************
*** 17,24 ****
--- 17,26 ----
  
  #include "catalog/pg_type.h"
  #include "nodes/makefuncs.h"
+ #include "utils/builtins.h"
  #include "utils/lsyscache.h"
  
+ static bool parseBooleanOption(DefElem *opt);
  
  /*
   * makeA_Expr -
*************** makeDefElemExtended(char *nameSpace, cha
*** 385,387 ****
--- 387,460 ----
  
  	return res;
  }
+ 
+ /*
+  * makeExplain -
+  *  build an ExplainStmt node by parsing the generic options list
+  */
+ ExplainStmt *
+ makeExplain(List *options, Node *query)
+ {
+ 	ExplainStmt *n = makeNode(ExplainStmt);
+ 	ListCell *lc;
+ 
+ 	n->costs = true;
+ 	n->query = query;
+ 
+ 	foreach (lc, options)
+ 	{
+ 		DefElem *opt = lfirst(lc);
+ 		if (!strcmp(opt->defname, "analyze"))
+ 			n->analyze = parseBooleanOption(opt);
+ 		else if (!strcmp(opt->defname, "verbose"))
+ 			n->verbose = parseBooleanOption(opt);
+ 		else if (!strcmp(opt->defname, "costs"))
+ 			n->costs = parseBooleanOption(opt);
+ 		else
+ 			ereport(ERROR,
+ 				(errcode(ERRCODE_UNDEFINED_PARAMETER),
+ 				 errmsg("unknown EXPLAIN option: %s", opt->defname)));
+ 	}
+ 
+ 	return n;
+ }
+ 
+ /*
+  * parseBooleanOption -
+  * 	Interpret a DefElem option as a boolean.
+  */
+ static bool
+ parseBooleanOption(DefElem *opt)
+ {
+ 	bool res;
+ 
+ 	/*
+ 	 * We interpret an omitted boolean argument as equivalent to "true", so
+ 	 * that, for example, EXPLAIN (ANALYZE) means the same thing as
+ 	 * EXPLAIN (ANALYZE ON).
+ 	 */
+ 	if (!opt->arg)
+ 	{
+ 		return true;
+ 	}
+ 	else if (IsA(opt->arg, Integer))
+ 	{
+ 		if (intVal(opt->arg) == 0)
+ 			return false;
+ 		else if (intVal(opt->arg) == 1)
+ 			return true;
+ 	}
+ 	else if (IsA(opt->arg, String))
+ 	{
+ 		if (parse_bool(strVal(opt->arg), &res))
+ 			return res;
+ 	}
+ 
+ 	ereport(ERROR,
+ 		(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ 		 errmsg("parameter \"%s\" requires a Boolean value",
+ 			opt->defname)));
+ 
+ 	/* silence compiler warning */
+ 	return false;
+ }
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 858e16c..1d51032 100644
*** a/src/backend/parser/gram.y
--- b/src/backend/parser/gram.y
*************** static TypeName *TableFuncTypeName(List 
*** 369,374 ****
--- 369,379 ----
  %type <defelt>	generic_option_elem alter_generic_option_elem
  %type <list>	generic_option_list alter_generic_option_list
  
+ %type <str>		explain_option_name
+ %type <node>	explain_option_arg
+ %type <defelt>	explain_option_elem
+ %type <list>	explain_option_list
+ 
  %type <typnam>	Typename SimpleTypename ConstTypename
  				GenericType Numeric opt_float
  				Character ConstCharacter
*************** opt_name_list:
*** 6447,6463 ****
   *
   *		QUERY:
   *				EXPLAIN [ANALYZE] [VERBOSE] query
   *
   *****************************************************************************/
  
  ExplainStmt: EXPLAIN opt_analyze opt_verbose ExplainableStmt
  				{
! 					ExplainStmt *n = makeNode(ExplainStmt);
  					n->analyze = $2;
  					n->verbose = $3;
- 					n->query = $4;
  					$$ = (Node *)n;
  				}
  		;
  
  ExplainableStmt:
--- 6452,6472 ----
   *
   *		QUERY:
   *				EXPLAIN [ANALYZE] [VERBOSE] query
+  *				EXPLAIN ( options ) query
   *
   *****************************************************************************/
  
  ExplainStmt: EXPLAIN opt_analyze opt_verbose ExplainableStmt
  				{
! 					ExplainStmt *n = makeExplain(NIL, (Node *) $4);
  					n->analyze = $2;
  					n->verbose = $3;
  					$$ = (Node *)n;
  				}
+ 		|	EXPLAIN '(' explain_option_list ')' ExplainableStmt
+ 				{
+ 					$$ = (Node *) makeExplain((List *) $3, (Node *) $5);
+ 				}
  		;
  
  ExplainableStmt:
*************** ExplainableStmt:
*** 6470,6478 ****
  			| ExecuteStmt					/* by default all are $$=$1 */
  		;
  
  opt_analyze:
  			analyze_keyword			{ $$ = TRUE; }
! 			| /* EMPTY */			{ $$ = FALSE; }
  		;
  
  /*****************************************************************************
--- 6479,6529 ----
  			| ExecuteStmt					/* by default all are $$=$1 */
  		;
  
+ /*
+  * The precedence declaration for the opt_analyze EMPTY case, below, is
+  * necessary to prevent a shift/reduce conflict in the second production for
+  * ExplainStmt, above.  Otherwise, when the parser encounters "EXPLAIN (", it
+  * can't tell whether the "(" is the beginning of a SelectStmt or the beginning
+  * of the options list.  The precedence declaration below forces the latter
+  * interpretation.
+  *
+  * It might seem that we could get away with simply changing the definition of
+  * ExplainableStmt to use select_without_parens rather than SelectStmt, but
+  * that does not work, because select_without_parens produces expressions such
+  * as "(SELECT NULL) ORDER BY 1" that we interpret as legal queries.
+  */
  opt_analyze:
  			analyze_keyword			{ $$ = TRUE; }
! 			| /* EMPTY */			%prec UMINUS { $$ = FALSE; }
! 		;
! 
! explain_option_list:
! 			explain_option_elem
! 				{
! 					$$ = list_make1($1);
! 				}
! 			| explain_option_list ',' explain_option_elem
! 				{
! 					$$ = lappend($1, $3);
! 				}
! 		;
! 
! explain_option_elem:
! 			explain_option_name explain_option_arg
! 				{
! 					$$ = makeDefElem($1, $2);
! 				}
! 		;
! 
! explain_option_name:
! 				ColLabel			{ $$ = $1; }
! 		;
! 
! explain_option_arg:
! 			  opt_boolean			{ $$ = (Node *) makeString($1); }
! 			| ColId_or_Sconst		{ $$ = (Node *) makeString($1); }
! 			| SignedIconst			{ $$ = (Node *) makeInteger($1); }
! 			| /* EMPTY */			{ $$ = NULL; }
  		;
  
  /*****************************************************************************
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 333cb25..9005209 100644
*** a/src/backend/utils/adt/ruleutils.c
--- b/src/backend/utils/adt/ruleutils.c
*************** static RangeTblEntry *find_rte_by_refnam
*** 187,193 ****
  					deparse_context *context);
  static const char *get_simple_binary_op_name(OpExpr *expr);
  static bool isSimpleNode(Node *node, Node *parentNode, int prettyFlags);
- static void appendStringInfoSpaces(StringInfo buf, int count);
  static void appendContextKeyword(deparse_context *context, const char *str,
  					 int indentBefore, int indentAfter, int indentPlus);
  static void get_rule_expr(Node *node, deparse_context *context,
--- 187,192 ----
*************** isSimpleNode(Node *node, Node *parentNod
*** 4173,4188 ****
  
  
  /*
-  * appendStringInfoSpaces - append spaces to buffer
-  */
- static void
- appendStringInfoSpaces(StringInfo buf, int count)
- {
- 	while (count-- > 0)
- 		appendStringInfoChar(buf, ' ');
- }
- 
- /*
   * appendContextKeyword - append a keyword to buffer
   *
   * If prettyPrint is enabled, perform a line break, and adjust indentation.
--- 4172,4177 ----
diff --git a/src/include/commands/explain.h b/src/include/commands/explain.h
index aa3b643..238b654 100644
*** a/src/include/commands/explain.h
--- b/src/include/commands/explain.h
*************** extern void ExplainOnePlan(PlannedStmt *
*** 44,49 ****
  			   TupOutputState *tstate);
  
  extern void ExplainPrintPlan(StringInfo str, QueryDesc *queryDesc,
! 				 bool analyze, bool verbose);
  
  #endif   /* EXPLAIN_H */
--- 44,49 ----
  			   TupOutputState *tstate);
  
  extern void ExplainPrintPlan(StringInfo str, QueryDesc *queryDesc,
! 				 ExplainStmt *stmt);
  
  #endif   /* EXPLAIN_H */
diff --git a/src/include/lib/stringinfo.h b/src/include/lib/stringinfo.h
index 17990c4..3932107 100644
*** a/src/include/lib/stringinfo.h
--- b/src/include/lib/stringinfo.h
*************** extern void appendStringInfoChar(StringI
*** 132,137 ****
--- 132,143 ----
  	 (void)((str)->data[(str)->len] = (ch), (str)->data[++(str)->len] = '\0'))
  
  /*------------------------
+  * appendStringInfoSpaces
+  * Append a given number of spaces to str.
+  */
+ extern void appendStringInfoSpaces(StringInfo str, int count);
+ 
+ /*------------------------
   * appendBinaryStringInfo
   * Append arbitrary binary data to a StringInfo, allocating more space
   * if necessary.
diff --git a/src/include/nodes/makefuncs.h b/src/include/nodes/makefuncs.h
index 7d24346..4ee826a 100644
*** a/src/include/nodes/makefuncs.h
--- b/src/include/nodes/makefuncs.h
*************** extern DefElem *makeDefElem(char *name, 
*** 69,72 ****
--- 69,74 ----
  extern DefElem *makeDefElemExtended(char *nameSpace, char *name, Node *arg,
  					DefElemAction defaction);
  
+ extern ExplainStmt *makeExplain(List *options, Node *query);
+ 
  #endif   /* MAKEFUNC_H */
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 9d53ab9..b7319c1 100644
*** a/src/include/nodes/parsenodes.h
--- b/src/include/nodes/parsenodes.h
*************** typedef struct ExplainStmt
*** 2193,2199 ****
  	NodeTag		type;
  	Node	   *query;			/* the query (as a raw parse tree) */
  	bool		verbose;		/* print plan info */
! 	bool		analyze;		/* get statistics by executing plan */
  } ExplainStmt;
  
  /* ----------------------
--- 2193,2200 ----
  	NodeTag		type;
  	Node	   *query;			/* the query (as a raw parse tree) */
  	bool		verbose;		/* print plan info */
! 	bool		analyze;		/* actually execute plan */
! 	bool		costs;			/* print costs and times */
  } ExplainStmt;
  
  /* ----------------------
#3Martijn van Oosterhout
kleptog@svana.org
In reply to: Andres Freund (#2)
Re: generic explain options v3 - RR Review

On Sun, Jul 19, 2009 at 03:15:38AM +0200, Andres Freund wrote:

Hi Robert, Hi All,

Patch applies with some offset changes, code changes look sensible, I
personally like the new syntax and the features it may allow in future. One,
possibly big, gripe remains though:
The formerly valid statement which cannot be written without the parentheses
and stay semantically equivalent:
EXPLAIN (SELECT 1 ORDER BY 1) UNION ALL (SELECT 2 ORDER BY 1);
is now not valid anymore (The added %prec UMINUS causes the first '(' to be
recognize as start of the option list as intended).
This currently can only be resolved by using an option list like:
EXPLAIN (VERBOSE OFF) ...
Its also currently impossible to use an empty set of parentheses to resolve
this - this could easily be changed though.

I have to admit I don't see a nice solution here except living with the
incompatibility... Perhaps somebody has a better idea?

I think another possibility might be to allow the syntax:

EXPLAIN VERBOSE ANALYSE (options) SELECt ...;

Sure, it's a bit ugly, but in the grammer you could then do:

ExplainStmt: EXPLAIN opt_analyze opt_verbose ExplainableStmt
| EXPLAIN opt_analyze opt_verbose '(' explain_option_list ')' ExplainableStmt

Which means that (I think) bison can use the token *after* the '(' to
disambiguate, and since SELECT is a reserved word I think the problem
may be solved.

(The point being that then Bison can reduce the opt_analyze for both
cases).

Hope this helps,
--
Martijn van Oosterhout <kleptog@svana.org> http://svana.org/kleptog/

Show quoted text

Please line up in a tree and maintain the heap invariant while
boarding. Thank you for flying nlogn airlines.

#4Andres Freund
andres@anarazel.de
In reply to: Martijn van Oosterhout (#3)
1 attachment(s)
Re: generic explain options v3 - RR Review

On Sunday 19 July 2009 14:39:33 Martijn van Oosterhout wrote:

On Sun, Jul 19, 2009 at 03:15:38AM +0200, Andres Freund wrote:

Hi Robert, Hi All,

Patch applies with some offset changes, code changes look sensible, I
personally like the new syntax and the features it may allow in future.
One, possibly big, gripe remains though:
The formerly valid statement which cannot be written without the
parentheses and stay semantically equivalent:
EXPLAIN (SELECT 1 ORDER BY 1) UNION ALL (SELECT 2 ORDER BY 1);
is now not valid anymore (The added %prec UMINUS causes the first '(' to
be recognize as start of the option list as intended).
This currently can only be resolved by using an option list like:
EXPLAIN (VERBOSE OFF) ...
Its also currently impossible to use an empty set of parentheses to
resolve this - this could easily be changed though.

I have to admit I don't see a nice solution here except living with the
incompatibility... Perhaps somebody has a better idea?

I think another possibility might be to allow the syntax:

EXPLAIN VERBOSE ANALYSE (options) SELECt ...;

Sure, it's a bit ugly, but in the grammer you could then do:

ExplainStmt: EXPLAIN opt_analyze opt_verbose ExplainableStmt

| EXPLAIN opt_analyze opt_verbose '(' explain_option_list ')'
| ExplainableStmt

Which means that (I think) bison can use the token *after* the '(' to
disambiguate, and since SELECT is a reserved word I think the problem
may be solved.

I think that does not work since explain_option_name has to include keywords
to be able to use ANALYZE and VERBOSE.

Its solvable by not allowing all keywords there but only ANALYZE and VERBOSE.
Involves some duplication though...

Patch attached.

Andres

Attachments:

resolve_parentheses_conflict.patchtext/x-patch; charset=UTF-8; name=resolve_parentheses_conflict.patchDownload
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 1d51032..175929c 100644
*** a/src/backend/parser/gram.y
--- b/src/backend/parser/gram.y
*************** static TypeName *TableFuncTypeName(List 
*** 321,327 ****
  %type <list>	opt_interval interval_second
  %type <node>	overlay_placing substr_from substr_for
  
! %type <boolean> opt_instead opt_analyze
  %type <boolean> index_opt_unique opt_verbose opt_full
  %type <boolean> opt_freeze opt_default opt_recheck
  %type <defelt>	opt_binary opt_oids copy_delimiter
--- 321,327 ----
  %type <list>	opt_interval interval_second
  %type <node>	overlay_placing substr_from substr_for
  
! %type <boolean> opt_instead
  %type <boolean> index_opt_unique opt_verbose opt_full
  %type <boolean> opt_freeze opt_default opt_recheck
  %type <defelt>	opt_binary opt_oids copy_delimiter
*************** opt_name_list:
*** 6456,6468 ****
   *
   *****************************************************************************/
  
! ExplainStmt: EXPLAIN opt_analyze opt_verbose ExplainableStmt
  				{
  					ExplainStmt *n = makeExplain(NIL, (Node *) $4);
! 					n->analyze = $2;
  					n->verbose = $3;
  					$$ = (Node *)n;
  				}
  		|	EXPLAIN '(' explain_option_list ')' ExplainableStmt
  				{
  					$$ = (Node *) makeExplain((List *) $3, (Node *) $5);
--- 6456,6480 ----
   *
   *****************************************************************************/
  
! ExplainStmt:
! 		EXPLAIN ExplainableStmt
! 				{
! 					ExplainStmt *n = makeExplain(NIL, (Node *) $2);
! 					$$ = (Node *)n;
! 				}
! 		| EXPLAIN ANALYZE opt_verbose ExplainableStmt
  				{
  					ExplainStmt *n = makeExplain(NIL, (Node *) $4);
! 					n->analyze = TRUE;
  					n->verbose = $3;
  					$$ = (Node *)n;
  				}
+ 		| EXPLAIN VERBOSE ExplainableStmt
+ 				{
+ 					ExplainStmt *n = makeExplain(NIL, (Node *) $3);
+ 					n->verbose = TRUE;
+ 					$$ = (Node *)n;
+ 				}
  		|	EXPLAIN '(' explain_option_list ')' ExplainableStmt
  				{
  					$$ = (Node *) makeExplain((List *) $3, (Node *) $5);
*************** ExplainableStmt:
*** 6479,6502 ****
  			| ExecuteStmt					/* by default all are $$=$1 */
  		;
  
- /*
-  * The precedence declaration for the opt_analyze EMPTY case, below, is
-  * necessary to prevent a shift/reduce conflict in the second production for
-  * ExplainStmt, above.  Otherwise, when the parser encounters "EXPLAIN (", it
-  * can't tell whether the "(" is the beginning of a SelectStmt or the beginning
-  * of the options list.  The precedence declaration below forces the latter
-  * interpretation.
-  *
-  * It might seem that we could get away with simply changing the definition of
-  * ExplainableStmt to use select_without_parens rather than SelectStmt, but
-  * that does not work, because select_without_parens produces expressions such
-  * as "(SELECT NULL) ORDER BY 1" that we interpret as legal queries.
-  */
- opt_analyze:
- 			analyze_keyword			{ $$ = TRUE; }
- 			| /* EMPTY */			%prec UMINUS { $$ = FALSE; }
- 		;
- 
  explain_option_list:
  			explain_option_elem
  				{
--- 6491,6496 ----
*************** explain_option_elem:
*** 6516,6522 ****
  		;
  
  explain_option_name:
! 				ColLabel			{ $$ = $1; }
  		;
  
  explain_option_arg:
--- 6510,6518 ----
  		;
  
  explain_option_name:
! 				ColId			{ $$ = $1; }
! 			| VERBOSE			{ $$ = "verbose"; }
! 			| analyze_keyword	{ $$ = "analyze"; }
  		;
  
  explain_option_arg:
#5Robert Haas
robertmhaas@gmail.com
In reply to: Andres Freund (#4)
Re: generic explain options v3 - RR Review

On Sun, Jul 19, 2009 at 9:47 AM, Andres Freund<andres@anarazel.de> wrote:

On Sunday 19 July 2009 14:39:33 Martijn van Oosterhout wrote:

On Sun, Jul 19, 2009 at 03:15:38AM +0200, Andres Freund wrote:

Hi Robert, Hi All,

Patch applies with some offset changes, code changes look sensible, I
personally like the new syntax and the features it may allow in future.
One, possibly big, gripe remains though:
The formerly valid statement which cannot be written without the
parentheses and stay semantically equivalent:
EXPLAIN (SELECT 1 ORDER BY 1) UNION ALL (SELECT 2 ORDER BY 1);
is now not valid anymore (The added %prec UMINUS causes the first '(' to
be recognize as start of the option list as intended).
This currently can only be resolved by using an option list like:
EXPLAIN (VERBOSE OFF) ...
Its also currently impossible to use an empty set of parentheses to
resolve this - this could easily be changed though.

I have to admit I don't see a nice solution here except living with the
incompatibility... Perhaps somebody has a better idea?

I think another possibility might be to allow the syntax:

EXPLAIN VERBOSE ANALYSE (options) SELECt ...;

Sure, it's a bit ugly, but in the grammer you could then do:

  ExplainStmt: EXPLAIN opt_analyze opt_verbose ExplainableStmt

            |       EXPLAIN opt_analyze opt_verbose '(' explain_option_list ')'
            | ExplainableStmt

Which means that (I think) bison can use the token *after* the '(' to
disambiguate, and since SELECT is a reserved word I think the problem
may be solved.

I think that does not work since explain_option_name has to include keywords
to be able to use ANALYZE and VERBOSE.

Its solvable by not allowing all keywords there but only ANALYZE and VERBOSE.
Involves some duplication though...

Patch attached.

Hmm, good idea. I will update and resubmit.

...Robert

#6Tom Lane
tgl@sss.pgh.pa.us
In reply to: Robert Haas (#1)
Re: generic explain options v3

Robert Haas <robertmhaas@gmail.com> writes:

Here is an updated version of my "generic options for explain" patch.

What is the rationale for essentially duplicating defGetBoolean()?

Also, I'd suggest changing the ExplainStmt struct to have a list of
DefElem options instead of hard-wiring the option set at that level.

regards, tom lane

#7Robert Haas
robertmhaas@gmail.com
In reply to: Tom Lane (#6)
1 attachment(s)
Re: generic explain options v3

On Tue, Jul 21, 2009 at 7:47 PM, Tom Lane<tgl@sss.pgh.pa.us> wrote:

Robert Haas <robertmhaas@gmail.com> writes:

Here is an updated version of my "generic options for explain" patch.

What is the rationale for essentially duplicating defGetBoolean()?

I just didn't realize we already had something along those lines.
Updated patch attached, to which I've also applied Andres Freund's
parser changes, suggested here:

http://archives.postgresql.org/pgsql-hackers/2009-07/msg01213.php

Also, I'd suggest changing the ExplainStmt struct to have a list of
DefElem options instead of hard-wiring the option set at that level.

What is the advantage of that?

...Robert

Attachments:

explain_options-v5.patchtext/x-diff; charset=US-ASCII; name=explain_options-v5.patchDownload
*** a/contrib/auto_explain/auto_explain.c
--- b/contrib/auto_explain/auto_explain.c
***************
*** 14,19 ****
--- 14,20 ----
  
  #include "commands/explain.h"
  #include "executor/instrument.h"
+ #include "nodes/makefuncs.h"
  #include "utils/guc.h"
  
  PG_MODULE_MAGIC;
***************
*** 196,207 **** explain_ExecutorEnd(QueryDesc *queryDesc)
  		msec = queryDesc->totaltime->total * 1000.0;
  		if (msec >= auto_explain_log_min_duration)
  		{
  			StringInfoData buf;
  
  			initStringInfo(&buf);
! 			ExplainPrintPlan(&buf, queryDesc,
! 						 queryDesc->doInstrument && auto_explain_log_analyze,
! 							 auto_explain_log_verbose);
  
  			/* Remove last line break */
  			if (buf.len > 0 && buf.data[buf.len - 1] == '\n')
--- 197,210 ----
  		msec = queryDesc->totaltime->total * 1000.0;
  		if (msec >= auto_explain_log_min_duration)
  		{
+ 			ExplainStmt   *stmt = makeExplain(NIL, NULL);
  			StringInfoData buf;
  
  			initStringInfo(&buf);
! 			stmt->analyze =
! 				(queryDesc->doInstrument && auto_explain_log_analyze);
! 			stmt->verbose = auto_explain_log_verbose;
! 			ExplainPrintPlan(&buf, queryDesc, stmt);
  
  			/* Remove last line break */
  			if (buf.len > 0 && buf.data[buf.len - 1] == '\n')
*** a/doc/src/sgml/ref/explain.sgml
--- b/doc/src/sgml/ref/explain.sgml
***************
*** 31,36 **** PostgreSQL documentation
--- 31,37 ----
  
   <refsynopsisdiv>
  <synopsis>
+ EXPLAIN [ ( [ { ANALYZE | VERBOSE | COSTS } [ <replaceable class="parameter">boolean_value</replaceable> ] ] [, ...] ) ] <replaceable class="parameter">statement</replaceable>
  EXPLAIN [ ANALYZE ] [ VERBOSE ] <replaceable class="parameter">statement</replaceable>
  </synopsis>
   </refsynopsisdiv>
***************
*** 70,75 **** EXPLAIN [ ANALYZE ] [ VERBOSE ] <replaceable class="parameter">statement</replac
--- 71,86 ----
     are close to reality.
    </para>
  
+   <para>
+    Only the <literal>ANALYZE</literal> and <literal>VERBOSE</literal> options
+    can be specified, and only in the order, without surrounding the option list
+    in parentheses.  Prior to <productname>PostgreSQL</productname> 8.5, the
+    unparenthesized syntax was the only one supported.  It is expected that
+    all new options will be supported only when using the parenthesized syntax,
+    which also allows a value to be specified for each option
+    (e.g. <literal>TRUE</literal> or <literal>FALSE</literal>).
+   </para>
+ 
    <important>
     <para>
      Keep in mind that the statement is actually executed when
***************
*** 99,105 **** ROLLBACK;
      <term><literal>ANALYZE</literal></term>
      <listitem>
       <para>
!       Carry out the command and show the actual run times.
       </para>
      </listitem>
     </varlistentry>
--- 110,117 ----
      <term><literal>ANALYZE</literal></term>
      <listitem>
       <para>
!       Carry out the command and show the actual run times.  This
!       parameter defaults to <command>FALSE</command>.
       </para>
      </listitem>
     </varlistentry>
***************
*** 108,114 **** ROLLBACK;
      <term><literal>VERBOSE</literal></term>
      <listitem>
       <para>
!       Include the output column list for each node in the plan tree.
       </para>
      </listitem>
     </varlistentry>
--- 120,151 ----
      <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>
! 
!    <varlistentry>
!     <term><literal>COSTS</literal></term>
!     <listitem>
!      <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>.
!      </para>
!     </listitem>
!    </varlistentry>
! 
!    <varlistentry>
!     <term><replaceable class="parameter" />boolean_value</replaceable></term>
!     <listitem>
!      <para>
!       Specifies whether the named parameter should be turned on or off.  You
!       can use the values <literal>TRUE</literal> or <literal>1</literal> to
!       request the stated option, and <literal>FALSE</literal>
!       or <literal>0</literal>.  If the Boolean value is omitted, it defaults
!       to <literal>TRUE</literal>.
       </para>
      </listitem>
     </varlistentry>
***************
*** 202,207 **** EXPLAIN SELECT * FROM foo WHERE i = 4;
--- 239,258 ----
    </para>
  
    <para>
+    Here is the same plan with costs suppressed:
+ 
+ <programlisting>
+ EXPLAIN (COSTS FALSE) SELECT * FROM foo WHERE i = 4;
+ 
+         QUERY PLAN
+ ----------------------------
+  Index Scan using fi on foo
+    Index Cond: (i = 4)
+ (2 rows)
+ </programlisting>
+   </para>
+ 
+   <para>
     Here is an example of a query plan for a query using an aggregate
     function:
  
*** a/src/backend/commands/explain.c
--- b/src/backend/commands/explain.c
***************
*** 46,51 **** typedef struct ExplainState
--- 46,52 ----
  	/* options */
  	bool		printTList;		/* print plan targetlists */
  	bool		printAnalyze;	/* print actual times */
+ 	bool		printCosts;		/* print costs */
  	/* other states */
  	PlannedStmt *pstmt;			/* top of plan */
  	List	   *rtable;			/* range table */
***************
*** 268,274 **** ExplainOnePlan(PlannedStmt *plannedstmt, ExplainStmt *stmt,
  
  	/* Create textual dump of plan tree */
  	initStringInfo(&buf);
! 	ExplainPrintPlan(&buf, queryDesc, stmt->analyze, stmt->verbose);
  
  	/*
  	 * If we ran the command, run any AFTER triggers it queued.  (Note this
--- 269,275 ----
  
  	/* Create textual dump of plan tree */
  	initStringInfo(&buf);
! 	ExplainPrintPlan(&buf, queryDesc, stmt);
  
  	/*
  	 * If we ran the command, run any AFTER triggers it queued.  (Note this
***************
*** 340,347 **** ExplainOnePlan(PlannedStmt *plannedstmt, ExplainStmt *stmt,
   * NB: will not work on utility statements
   */
  void
! ExplainPrintPlan(StringInfo str, QueryDesc *queryDesc,
! 				 bool analyze, bool verbose)
  {
  	ExplainState es;
  
--- 341,347 ----
   * NB: will not work on utility statements
   */
  void
! ExplainPrintPlan(StringInfo str, QueryDesc *queryDesc, ExplainStmt *stmt)
  {
  	ExplainState es;
  
***************
*** 349,356 **** ExplainPrintPlan(StringInfo str, QueryDesc *queryDesc,
  
  	memset(&es, 0, sizeof(es));
  	es.str = str;
! 	es.printTList = verbose;
! 	es.printAnalyze = analyze;
  	es.pstmt = queryDesc->plannedstmt;
  	es.rtable = queryDesc->plannedstmt->rtable;
  
--- 349,357 ----
  
  	memset(&es, 0, sizeof(es));
  	es.str = str;
! 	es.printTList = stmt->verbose;
! 	es.printAnalyze = stmt->analyze;
! 	es.printCosts = stmt->costs;
  	es.pstmt = queryDesc->plannedstmt;
  	es.rtable = queryDesc->plannedstmt->rtable;
  
***************
*** 688,696 **** ExplainNode(Plan *plan, PlanState *planstate, Plan *outer_plan,
  			break;
  	}
  
! 	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
--- 689,698 ----
  			break;
  	}
  
! 	if (es->printCosts)
! 		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
*** a/src/backend/nodes/copyfuncs.c
--- b/src/backend/nodes/copyfuncs.c
***************
*** 2877,2882 **** _copyExplainStmt(ExplainStmt *from)
--- 2877,2883 ----
  	COPY_NODE_FIELD(query);
  	COPY_SCALAR_FIELD(verbose);
  	COPY_SCALAR_FIELD(analyze);
+ 	COPY_SCALAR_FIELD(costs);
  
  	return newnode;
  }
*** a/src/backend/nodes/equalfuncs.c
--- b/src/backend/nodes/equalfuncs.c
***************
*** 1469,1474 **** _equalExplainStmt(ExplainStmt *a, ExplainStmt *b)
--- 1469,1475 ----
  	COMPARE_NODE_FIELD(query);
  	COMPARE_SCALAR_FIELD(verbose);
  	COMPARE_SCALAR_FIELD(analyze);
+ 	COMPARE_SCALAR_FIELD(costs);
  
  	return true;
  }
*** a/src/backend/nodes/makefuncs.c
--- b/src/backend/nodes/makefuncs.c
***************
*** 17,25 ****
  
  #include "catalog/pg_type.h"
  #include "nodes/makefuncs.h"
  #include "utils/lsyscache.h"
  
- 
  /*
   * makeA_Expr -
   *		makes an A_Expr node
--- 17,25 ----
  
  #include "catalog/pg_type.h"
  #include "nodes/makefuncs.h"
+ #include "utils/builtins.h"
  #include "utils/lsyscache.h"
  
  /*
   * makeA_Expr -
   *		makes an A_Expr node
***************
*** 385,387 **** makeDefElemExtended(char *nameSpace, char *name, Node *arg,
--- 385,418 ----
  
  	return res;
  }
+ 
+ /*
+  * makeExplain -
+  *  build an ExplainStmt node by parsing the generic options list
+  */
+ ExplainStmt *
+ makeExplain(List *options, Node *query)
+ {
+ 	ExplainStmt *n = makeNode(ExplainStmt);
+ 	ListCell *lc;
+ 
+ 	n->costs = true;
+ 	n->query = query;
+ 
+ 	foreach (lc, options)
+ 	{
+ 		DefElem *opt = lfirst(lc);
+ 		if (!strcmp(opt->defname, "analyze"))
+ 			n->analyze = defGetBoolean(opt);
+ 		else if (!strcmp(opt->defname, "verbose"))
+ 			n->verbose = defGetBoolean(opt);
+ 		else if (!strcmp(opt->defname, "costs"))
+ 			n->costs = defGetBoolean(opt);
+ 		else
+ 			ereport(ERROR,
+ 				(errcode(ERRCODE_UNDEFINED_PARAMETER),
+ 				 errmsg("unknown EXPLAIN option: %s", opt->defname)));
+ 	}
+ 
+ 	return n;
+ }
*** a/src/backend/parser/gram.y
--- b/src/backend/parser/gram.y
***************
*** 321,327 **** static TypeName *TableFuncTypeName(List *columns);
  %type <list>	opt_interval interval_second
  %type <node>	overlay_placing substr_from substr_for
  
! %type <boolean> opt_instead opt_analyze
  %type <boolean> index_opt_unique opt_verbose opt_full
  %type <boolean> opt_freeze opt_default opt_recheck
  %type <defelt>	opt_binary opt_oids copy_delimiter
--- 321,327 ----
  %type <list>	opt_interval interval_second
  %type <node>	overlay_placing substr_from substr_for
  
! %type <boolean> opt_instead
  %type <boolean> index_opt_unique opt_verbose opt_full
  %type <boolean> opt_freeze opt_default opt_recheck
  %type <defelt>	opt_binary opt_oids copy_delimiter
***************
*** 369,374 **** static TypeName *TableFuncTypeName(List *columns);
--- 369,379 ----
  %type <defelt>	generic_option_elem alter_generic_option_elem
  %type <list>	generic_option_list alter_generic_option_list
  
+ %type <str>		explain_option_name
+ %type <node>	explain_option_arg
+ %type <defelt>	explain_option_elem
+ %type <list>	explain_option_list
+ 
  %type <typnam>	Typename SimpleTypename ConstTypename
  				GenericType Numeric opt_float
  				Character ConstCharacter
***************
*** 6469,6485 **** opt_name_list:
   *
   *		QUERY:
   *				EXPLAIN [ANALYZE] [VERBOSE] query
   *
   *****************************************************************************/
  
! ExplainStmt: EXPLAIN opt_analyze opt_verbose ExplainableStmt
  				{
! 					ExplainStmt *n = makeNode(ExplainStmt);
! 					n->analyze = $2;
  					n->verbose = $3;
- 					n->query = $4;
  					$$ = (Node *)n;
  				}
  		;
  
  ExplainableStmt:
--- 6474,6506 ----
   *
   *		QUERY:
   *				EXPLAIN [ANALYZE] [VERBOSE] query
+  *				EXPLAIN ( options ) query
   *
   *****************************************************************************/
  
! ExplainStmt:
! 		EXPLAIN ExplainableStmt
! 				{
! 					ExplainStmt *n = makeExplain(NIL, (Node *) $2);
! 					$$ = (Node *)n;
! 				}
! 		| EXPLAIN ANALYZE opt_verbose ExplainableStmt
  				{
! 					ExplainStmt *n = makeExplain(NIL, (Node *) $4);
! 					n->analyze = TRUE;
  					n->verbose = $3;
  					$$ = (Node *)n;
  				}
+ 		| EXPLAIN VERBOSE ExplainableStmt
+ 				{
+ 					ExplainStmt *n = makeExplain(NIL, (Node *) $3);
+ 					n->verbose = TRUE;
+ 					$$ = (Node *)n;
+ 				}
+ 		|	EXPLAIN '(' explain_option_list ')' ExplainableStmt
+ 				{
+ 					$$ = (Node *) makeExplain((List *) $3, (Node *) $5);
+ 				}
  		;
  
  ExplainableStmt:
***************
*** 6492,6500 **** ExplainableStmt:
  			| ExecuteStmt					/* by default all are $$=$1 */
  		;
  
! opt_analyze:
! 			analyze_keyword			{ $$ = TRUE; }
! 			| /* EMPTY */			{ $$ = FALSE; }
  		;
  
  /*****************************************************************************
--- 6513,6547 ----
  			| ExecuteStmt					/* by default all are $$=$1 */
  		;
  
! explain_option_list:
! 			explain_option_elem
! 				{
! 					$$ = list_make1($1);
! 				}
! 			| explain_option_list ',' explain_option_elem
! 				{
! 					$$ = lappend($1, $3);
! 				}
! 		;
! 
! explain_option_elem:
! 			explain_option_name explain_option_arg
! 				{
! 					$$ = makeDefElem($1, $2);
! 				}
! 		;
! 
! explain_option_name:
! 				ColId			{ $$ = $1; }
! 			| VERBOSE			{ $$ = "verbose"; }
! 			| analyze_keyword	{ $$ = "analyze"; }
! 		;
! 
! explain_option_arg:
! 			  opt_boolean			{ $$ = (Node *) makeString($1); }
! 			| ColId_or_Sconst		{ $$ = (Node *) makeString($1); }
! 			| SignedIconst			{ $$ = (Node *) makeInteger($1); }
! 			| /* EMPTY */			{ $$ = NULL; }
  		;
  
  /*****************************************************************************
*** a/src/include/commands/explain.h
--- b/src/include/commands/explain.h
***************
*** 44,49 **** extern void ExplainOnePlan(PlannedStmt *plannedstmt, ExplainStmt *stmt,
  			   TupOutputState *tstate);
  
  extern void ExplainPrintPlan(StringInfo str, QueryDesc *queryDesc,
! 				 bool analyze, bool verbose);
  
  #endif   /* EXPLAIN_H */
--- 44,49 ----
  			   TupOutputState *tstate);
  
  extern void ExplainPrintPlan(StringInfo str, QueryDesc *queryDesc,
! 				 ExplainStmt *stmt);
  
  #endif   /* EXPLAIN_H */
*** a/src/include/nodes/makefuncs.h
--- b/src/include/nodes/makefuncs.h
***************
*** 69,72 **** extern DefElem *makeDefElem(char *name, Node *arg);
--- 69,74 ----
  extern DefElem *makeDefElemExtended(char *nameSpace, char *name, Node *arg,
  					DefElemAction defaction);
  
+ extern ExplainStmt *makeExplain(List *options, Node *query);
+ 
  #endif   /* MAKEFUNC_H */
*** a/src/include/nodes/parsenodes.h
--- b/src/include/nodes/parsenodes.h
***************
*** 2194,2200 **** typedef struct ExplainStmt
  	NodeTag		type;
  	Node	   *query;			/* the query (as a raw parse tree) */
  	bool		verbose;		/* print plan info */
! 	bool		analyze;		/* get statistics by executing plan */
  } ExplainStmt;
  
  /* ----------------------
--- 2194,2201 ----
  	NodeTag		type;
  	Node	   *query;			/* the query (as a raw parse tree) */
  	bool		verbose;		/* print plan info */
! 	bool		analyze;		/* actually execute plan */
! 	bool		costs;			/* print costs and times */
  } ExplainStmt;
  
  /* ----------------------
#8Tom Lane
tgl@sss.pgh.pa.us
In reply to: Robert Haas (#7)
Re: generic explain options v3

Robert Haas <robertmhaas@gmail.com> writes:

On Tue, Jul 21, 2009 at 7:47 PM, Tom Lane<tgl@sss.pgh.pa.us> wrote:

Also, I'd suggest changing the ExplainStmt struct to have a list of
DefElem options instead of hard-wiring the option set at that level.

What is the advantage of that?

Fewer places to change when you add a new option; in particular, not
copyfuncs or equalfuncs. Also, the way you are doing it is gratuitously
unlike every other command that has similar issues to deal with.
Everybody else parses their DefElem list at execution time. I think
you should have the legacy ANALYZE and VERBOSE syntax elements generate
DefElem list members that get examined at execution.

BTW, I see that your "explain refactoring" patch is marked ready
for committer, but is it actually sane to apply it before the other
two?

regards, tom lane

#9Robert Haas
robertmhaas@gmail.com
In reply to: Tom Lane (#8)
Re: generic explain options v3

On Tue, Jul 21, 2009 at 10:05 PM, Tom Lane<tgl@sss.pgh.pa.us> wrote:

Robert Haas <robertmhaas@gmail.com> writes:

On Tue, Jul 21, 2009 at 7:47 PM, Tom Lane<tgl@sss.pgh.pa.us> wrote:

Also, I'd suggest changing the ExplainStmt struct to have a list of
DefElem options instead of hard-wiring the option set at that level.

What is the advantage of that?

Fewer places to change when you add a new option; in particular, not
copyfuncs or equalfuncs.  Also, the way you are doing it is gratuitously
unlike every other command that has similar issues to deal with.
Everybody else parses their DefElem list at execution time.  I think
you should have the legacy ANALYZE and VERBOSE syntax elements generate
DefElem list members that get examined at execution.

Not having to touch copyfuncs or equalfuncs for future options is a
definite plus, so I'll rework along these lines.

BTW, I see that your "explain refactoring" patch is marked ready
for committer, but is it actually sane to apply it before the other
two?

I think so. It's all code cleanup, with no behavioral changes, and is
intended to contain only the stuff that seemed to me as thought it
would still be worth doing even if the rest of the patch set were
rejected.

...Robert

#10Robert Haas
robertmhaas@gmail.com
In reply to: Robert Haas (#9)
Re: generic explain options v3

On Tue, Jul 21, 2009 at 10:29 PM, Robert Haas<robertmhaas@gmail.com> wrote:

On Tue, Jul 21, 2009 at 10:05 PM, Tom Lane<tgl@sss.pgh.pa.us> wrote:

Robert Haas <robertmhaas@gmail.com> writes:

On Tue, Jul 21, 2009 at 7:47 PM, Tom Lane<tgl@sss.pgh.pa.us> wrote:

Also, I'd suggest changing the ExplainStmt struct to have a list of
DefElem options instead of hard-wiring the option set at that level.

What is the advantage of that?

Fewer places to change when you add a new option; in particular, not
copyfuncs or equalfuncs.  Also, the way you are doing it is gratuitously
unlike every other command that has similar issues to deal with.
Everybody else parses their DefElem list at execution time.  I think
you should have the legacy ANALYZE and VERBOSE syntax elements generate
DefElem list members that get examined at execution.

Not having to touch copyfuncs or equalfuncs for future options is a
definite plus, so I'll rework along these lines.

Ugh. I took a look at this and it turns out that there are some
tentacles. It doesn't seem very sane to actually do anything with a
list of DefElem nodes, so we really need to parse that list and
convert it to a more sensible format right away (this also seems
important for proper error checking).

The natural place to do this would be in ExplainPrintPlan(), which is
already copying the relevant fields from the ExplainStmt over into an
ExplainState, but that's too far down the call tree, which (for a
non-utility statement when ExplainOneQuery_hook is null) looks like
this:

ExplainQuery -> ExplainOneQuery -> ExplainOnePlan -> ExplainPrintPlan

The obvious solution to that is to create the ExplainState sooner,
back up at the ExplainQuery level. If we do that, though, then
ExplainState will need to become a public API, because
contrib/auto_explain calls ExplainPrintPlan(). And if we do that,
then probably we should declare it in include/nodes/execnodes.h and
make it a node type... and if we do that then we'll be back to a
copyfuncs/equalfuncs change every time we add a flag.

Now that's not to say there's no advantage in the proposed refactoring
- it's still more consistent with the way things are done elsewhere.
But since it's going to be a fair amount of work and fail to achieve
one of the two goals you set forth for it, I'd like to get
confirmation before proceeding if possible, and any suggestions you
may have for how to make it as clean as possible.

Thanks,

...Robert

#11Tom Lane
tgl@sss.pgh.pa.us
In reply to: Robert Haas (#10)
Re: generic explain options v3

Robert Haas <robertmhaas@gmail.com> writes:

Ugh. I took a look at this and it turns out that there are some
tentacles. It doesn't seem very sane to actually do anything with a
list of DefElem nodes, so we really need to parse that list and
convert it to a more sensible format right away (this also seems
important for proper error checking).

Yeah, the standard approach is to convert it into a group of values
at the start of execution of the utility command.

The obvious solution to that is to create the ExplainState sooner,
back up at the ExplainQuery level. If we do that, though, then
ExplainState will need to become a public API, because
contrib/auto_explain calls ExplainPrintPlan().

Well, if we add any more options to EXPLAIN then auto_explain may well
be interested in them, so I'm not sure this is bad. The alternative
is to keep adding retail parameters to the public functions.

And if we do that,
then probably we should declare it in include/nodes/execnodes.h and
make it a node type...

No, just a struct declared in commands/explain.h. There's no reason
for it to be part of the Node system.

regards, tom lane

#12Robert Haas
robertmhaas@gmail.com
In reply to: Tom Lane (#11)
Re: generic explain options v3

On Thu, Jul 23, 2009 at 12:08 PM, Tom Lane<tgl@sss.pgh.pa.us> wrote:

Robert Haas <robertmhaas@gmail.com> writes:

Ugh.  I took a look at this and it turns out that there are some
tentacles.  It doesn't seem very sane to actually do anything with a
list of DefElem nodes, so we really need to parse that list and
convert it to a more sensible format right away (this also seems
important for proper error checking).

Yeah, the standard approach is to convert it into a group of values
at the start of execution of the utility command.

The obvious solution to that is to create the ExplainState sooner,
back up at the ExplainQuery level.  If we do that, though, then
ExplainState will need to become a public API, because
contrib/auto_explain calls ExplainPrintPlan().

Well, if we add any more options to EXPLAIN then auto_explain may well
be interested in them, so I'm not sure this is bad.  The alternative
is to keep adding retail parameters to the public functions.

And if we do that,
then probably we should declare it in include/nodes/execnodes.h and
make it a node type...

No, just a struct declared in commands/explain.h.  There's no reason
for it to be part of the Node system.

Oh, OK. That will work. Thanks.

...Robert

#13Robert Haas
robertmhaas@gmail.com
In reply to: Robert Haas (#12)
1 attachment(s)
Re: generic explain options v3

On Thu, Jul 23, 2009 at 2:23 PM, Robert Haas<robertmhaas@gmail.com> wrote:

On Thu, Jul 23, 2009 at 12:08 PM, Tom Lane<tgl@sss.pgh.pa.us> wrote:

Robert Haas <robertmhaas@gmail.com> writes:

Ugh.  I took a look at this and it turns out that there are some
tentacles.  It doesn't seem very sane to actually do anything with a
list of DefElem nodes, so we really need to parse that list and
convert it to a more sensible format right away (this also seems
important for proper error checking).

Yeah, the standard approach is to convert it into a group of values
at the start of execution of the utility command.

The obvious solution to that is to create the ExplainState sooner,
back up at the ExplainQuery level.  If we do that, though, then
ExplainState will need to become a public API, because
contrib/auto_explain calls ExplainPrintPlan().

Well, if we add any more options to EXPLAIN then auto_explain may well
be interested in them, so I'm not sure this is bad.  The alternative
is to keep adding retail parameters to the public functions.

And if we do that,
then probably we should declare it in include/nodes/execnodes.h and
make it a node type...

No, just a struct declared in commands/explain.h.  There's no reason
for it to be part of the Node system.

Oh, OK.  That will work.  Thanks.

Here's the update. There are a few things that I'm not entirely happy
with here, but not quite sure what to do about either.

- ExplainPrintPlan() is now almost trivial. It seems like there
should be some way to get rid of this altogether, but I'm not quite
sure how. I thought about ripping pstmt and rtable out of
ExplainState and just storying queryDesc there. But that involves
changing a lot of code, and while it makes some things simpler, it
makes other parts more complex. I'm not sure whether it's a win or
not; I'm also not sure how much brainpower it's worth spending on
this.

- It's becoming increasingly evident to me that the explain stuff in
prepare.c has no business being there and should be moved to
explain.c. I haven't done that here, but it's worth thinking about.
We could turn several functions that are currently public into statics
if we did that.

- The hack needed in ExplainLogLevel is just that.

Help!

...Robert

Attachments:

explain_options-v6.patchtext/x-diff; charset=US-ASCII; name=explain_options-v6.patchDownload
*** a/contrib/auto_explain/auto_explain.c
--- b/contrib/auto_explain/auto_explain.c
***************
*** 196,211 **** explain_ExecutorEnd(QueryDesc *queryDesc)
  		msec = queryDesc->totaltime->total * 1000.0;
  		if (msec >= auto_explain_log_min_duration)
  		{
! 			StringInfoData buf;
  
! 			initStringInfo(&buf);
! 			ExplainPrintPlan(&buf, queryDesc,
! 						 queryDesc->doInstrument && auto_explain_log_analyze,
! 							 auto_explain_log_verbose);
  
  			/* Remove last line break */
! 			if (buf.len > 0 && buf.data[buf.len - 1] == '\n')
! 				buf.data[--buf.len] = '\0';
  
  			/*
  			 * Note: we rely on the existing logging of context or
--- 196,212 ----
  		msec = queryDesc->totaltime->total * 1000.0;
  		if (msec >= auto_explain_log_min_duration)
  		{
! 			ExplainState	es;
  
! 			ExplainInitState(&es);
! 			es.analyze = (queryDesc->doInstrument && auto_explain_log_analyze);
! 			es.verbose = auto_explain_log_verbose;
! 			es.pstmt = queryDesc->plannedstmt;
! 			ExplainPrintPlan(&es, queryDesc);
  
  			/* Remove last line break */
! 			if (es.str.len > 0 && es.str.data[es.str.len - 1] == '\n')
! 				es.str.data[--es.str.len] = '\0';
  
  			/*
  			 * Note: we rely on the existing logging of context or
***************
*** 215,223 **** explain_ExecutorEnd(QueryDesc *queryDesc)
  			 */
  			ereport(LOG,
  					(errmsg("duration: %.3f ms  plan:\n%s",
! 							msec, buf.data)));
  
! 			pfree(buf.data);
  		}
  	}
  
--- 216,224 ----
  			 */
  			ereport(LOG,
  					(errmsg("duration: %.3f ms  plan:\n%s",
! 							msec, es.str.data)));
  
! 			pfree(es.str.data);
  		}
  	}
  
*** a/doc/src/sgml/ref/explain.sgml
--- b/doc/src/sgml/ref/explain.sgml
***************
*** 31,36 **** PostgreSQL documentation
--- 31,37 ----
  
   <refsynopsisdiv>
  <synopsis>
+ EXPLAIN [ ( [ { ANALYZE | VERBOSE | COSTS } [ <replaceable class="parameter">boolean_value</replaceable> ] ] [, ...] ) ] <replaceable class="parameter">statement</replaceable>
  EXPLAIN [ ANALYZE ] [ VERBOSE ] <replaceable class="parameter">statement</replaceable>
  </synopsis>
   </refsynopsisdiv>
***************
*** 70,75 **** EXPLAIN [ ANALYZE ] [ VERBOSE ] <replaceable class="parameter">statement</replac
--- 71,86 ----
     are close to reality.
    </para>
  
+   <para>
+    Only the <literal>ANALYZE</literal> and <literal>VERBOSE</literal> options
+    can be specified, and only in the order, without surrounding the option list
+    in parentheses.  Prior to <productname>PostgreSQL</productname> 8.5, the
+    unparenthesized syntax was the only one supported.  It is expected that
+    all new options will be supported only when using the parenthesized syntax,
+    which also allows a value to be specified for each option
+    (e.g. <literal>TRUE</literal> or <literal>FALSE</literal>).
+   </para>
+ 
    <important>
     <para>
      Keep in mind that the statement is actually executed when
***************
*** 99,105 **** ROLLBACK;
      <term><literal>ANALYZE</literal></term>
      <listitem>
       <para>
!       Carry out the command and show the actual run times.
       </para>
      </listitem>
     </varlistentry>
--- 110,117 ----
      <term><literal>ANALYZE</literal></term>
      <listitem>
       <para>
!       Carry out the command and show the actual run times.  This
!       parameter defaults to <command>FALSE</command>.
       </para>
      </listitem>
     </varlistentry>
***************
*** 108,114 **** ROLLBACK;
      <term><literal>VERBOSE</literal></term>
      <listitem>
       <para>
!       Include the output column list for each node in the plan tree.
       </para>
      </listitem>
     </varlistentry>
--- 120,151 ----
      <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>
! 
!    <varlistentry>
!     <term><literal>COSTS</literal></term>
!     <listitem>
!      <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>.
!      </para>
!     </listitem>
!    </varlistentry>
! 
!    <varlistentry>
!     <term><replaceable class="parameter" />boolean_value</replaceable></term>
!     <listitem>
!      <para>
!       Specifies whether the named parameter should be turned on or off.  You
!       can use the values <literal>TRUE</literal> or <literal>1</literal> to
!       request the stated option, and <literal>FALSE</literal>
!       or <literal>0</literal>.  If the Boolean value is omitted, it defaults
!       to <literal>TRUE</literal>.
       </para>
      </listitem>
     </varlistentry>
***************
*** 202,207 **** EXPLAIN SELECT * FROM foo WHERE i = 4;
--- 239,258 ----
    </para>
  
    <para>
+    Here is the same plan with costs suppressed:
+ 
+ <programlisting>
+ EXPLAIN (COSTS FALSE) SELECT * FROM foo WHERE i = 4;
+ 
+         QUERY PLAN
+ ----------------------------
+  Index Scan using fi on foo
+    Index Cond: (i = 4)
+ (2 rows)
+ </programlisting>
+   </para>
+ 
+   <para>
     Here is an example of a query plan for a query using an aggregate
     function:
  
*** a/src/backend/commands/explain.c
--- b/src/backend/commands/explain.c
***************
*** 16,21 ****
--- 16,22 ----
  #include "access/xact.h"
  #include "catalog/pg_constraint.h"
  #include "catalog/pg_type.h"
+ #include "commands/defrem.h"
  #include "commands/explain.h"
  #include "commands/prepare.h"
  #include "commands/trigger.h"
***************
*** 40,59 **** ExplainOneQuery_hook_type ExplainOneQuery_hook = NULL;
  explain_get_index_name_hook_type explain_get_index_name_hook = NULL;
  
  
! typedef struct ExplainState
! {
! 	StringInfo	str;			/* output buffer */
! 	/* options */
! 	bool		printTList;		/* print plan targetlists */
! 	bool		printAnalyze;	/* print actual times */
! 	/* other states */
! 	PlannedStmt *pstmt;			/* top of plan */
! 	List	   *rtable;			/* range table */
! } ExplainState;
! 
! static void ExplainOneQuery(Query *query, ExplainStmt *stmt,
  				const char *queryString,
! 				ParamListInfo params, TupOutputState *tstate);
  static void report_triggers(ResultRelInfo *rInfo, bool show_relname,
  				StringInfo buf);
  static double elapsed_time(instr_time *starttime);
--- 41,49 ----
  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);
***************
*** 88,94 **** ExplainQuery(ExplainStmt *stmt, const char *queryString,
  	int			num_params;
  	TupOutputState *tstate;
  	List	   *rewritten;
! 	ListCell   *l;
  
  	/* Convert parameter type data to the form parser wants */
  	getParamListTypes(params, &param_types, &num_params);
--- 78,103 ----
  	int			num_params;
  	TupOutputState *tstate;
  	List	   *rewritten;
! 	ListCell   *lc;
! 	ExplainState es;
! 
! 	/* Initialize ExplainState. */
! 	ExplainInitState(&es);
! 
! 	/* Parse options list. */
! 	foreach (lc, stmt->options)
! 	{
! 		DefElem *opt = lfirst(lc);
! 		if (!strcmp(opt->defname, "analyze"))
! 			es.analyze = defGetBoolean(opt);
! 		else if (!strcmp(opt->defname, "verbose"))
! 			es.verbose = defGetBoolean(opt);
! 		else if (!strcmp(opt->defname, "costs"))
! 			es.costs = defGetBoolean(opt);
! 		else
! 			ereport(ERROR, (errcode(ERRCODE_UNDEFINED_PARAMETER),
! 				errmsg("unknown EXPLAIN option: %s", opt->defname)));
! 	}
  
  	/* Convert parameter type data to the form parser wants */
  	getParamListTypes(params, &param_types, &num_params);
***************
*** 106,136 **** ExplainQuery(ExplainStmt *stmt, const char *queryString,
  	rewritten = pg_analyze_and_rewrite((Node *) copyObject(stmt->query),
  									   queryString, param_types, num_params);
  
- 	/* prepare for projection of tuples */
- 	tstate = begin_tup_output_tupdesc(dest, ExplainResultDesc(stmt));
- 
  	if (rewritten == NIL)
  	{
  		/* In the case of an INSTEAD NOTHING, tell at least that */
! 		do_text_output_oneline(tstate, "Query rewrites to nothing");
  	}
  	else
  	{
  		/* Explain every plan */
  		foreach(l, rewritten)
  		{
! 			ExplainOneQuery((Query *) lfirst(l), stmt,
! 							queryString, params, tstate);
  			/* put a blank line between plans */
  			if (lnext(l) != NULL)
! 				do_text_output_oneline(tstate, "");
  		}
  	}
  
  	end_tup_output(tstate);
  }
  
  /*
   * ExplainResultDesc -
   *	  construct the result tupledesc for an EXPLAIN
   */
--- 115,157 ----
  	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
  	{
+ 		ListCell   *l;
+ 
  		/* Explain every plan */
  		foreach(l, rewritten)
  		{
! 			ExplainOneQuery((Query *) lfirst(l), &es, queryString, params);
  			/* put a blank line between plans */
  			if (lnext(l) != NULL)
! 				appendStringInfoString(&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);
  }
  
  /*
+  * Initialize ExplainState.
+  */
+ void
+ ExplainInitState(ExplainState *es)
+ {
+ 	memset(es, 0, sizeof(ExplainState));
+ 	es->costs = 1;
+ 	initStringInfo(&es->str);
+ }
+ 
+ /*
   * ExplainResultDesc -
   *	  construct the result tupledesc for an EXPLAIN
   */
***************
*** 151,179 **** ExplainResultDesc(ExplainStmt *stmt)
   *	  print out the execution plan for one Query
   */
  static void
! ExplainOneQuery(Query *query, ExplainStmt *stmt, const char *queryString,
! 				ParamListInfo params, TupOutputState *tstate)
  {
  	/* planner will not cope with utility statements */
  	if (query->commandType == CMD_UTILITY)
  	{
! 		ExplainOneUtility(query->utilityStmt, stmt,
! 						  queryString, params, tstate);
  		return;
  	}
  
  	/* if an advisor plugin is present, let it manage things */
  	if (ExplainOneQuery_hook)
! 		(*ExplainOneQuery_hook) (query, stmt, queryString, params, tstate);
  	else
  	{
- 		PlannedStmt *plan;
- 
  		/* plan the query */
! 		plan = pg_plan_query(query, 0, params);
  
  		/* run it (if needed) and produce output */
! 		ExplainOnePlan(plan, stmt, queryString, params, tstate);
  	}
  }
  
--- 172,197 ----
   *	  print out the execution plan for one Query
   */
  static void
! ExplainOneQuery(Query *query, ExplainState *es, const char *queryString,
! 				ParamListInfo params)
  {
  	/* planner will not cope with utility statements */
  	if (query->commandType == CMD_UTILITY)
  	{
! 		ExplainOneUtility(query->utilityStmt, es, queryString, params);
  		return;
  	}
  
  	/* if an advisor plugin is present, let it manage things */
  	if (ExplainOneQuery_hook)
! 		(*ExplainOneQuery_hook) (query, es, queryString, params);
  	else
  	{
  		/* plan the query */
! 		es->pstmt = pg_plan_query(query, 0, params);
  
  		/* run it (if needed) and produce output */
! 		ExplainOnePlan(es, queryString, params);
  	}
  }
  
***************
*** 187,207 **** ExplainOneQuery(Query *query, ExplainStmt *stmt, const char *queryString,
   * EXPLAIN EXECUTE case
   */
  void
! ExplainOneUtility(Node *utilityStmt, ExplainStmt *stmt,
! 				  const char *queryString, ParamListInfo params,
! 				  TupOutputState *tstate)
  {
  	if (utilityStmt == NULL)
  		return;
  
  	if (IsA(utilityStmt, ExecuteStmt))
! 		ExplainExecuteQuery((ExecuteStmt *) utilityStmt, stmt,
! 							queryString, params, tstate);
  	else if (IsA(utilityStmt, NotifyStmt))
! 		do_text_output_oneline(tstate, "NOTIFY");
  	else
! 		do_text_output_oneline(tstate,
! 							   "Utility statements have no plan structure");
  }
  
  /*
--- 205,224 ----
   * EXPLAIN EXECUTE case
   */
  void
! ExplainOneUtility(Node *utilityStmt, ExplainState *es,
! 				  const char *queryString, ParamListInfo params)
  {
  	if (utilityStmt == NULL)
  		return;
  
  	if (IsA(utilityStmt, ExecuteStmt))
! 		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");
  }
  
  /*
***************
*** 219,232 **** ExplainOneUtility(Node *utilityStmt, ExplainStmt *stmt,
   * to call it.
   */
  void
! ExplainOnePlan(PlannedStmt *plannedstmt, ExplainStmt *stmt,
! 			   const char *queryString, ParamListInfo params,
! 			   TupOutputState *tstate)
  {
  	QueryDesc  *queryDesc;
  	instr_time	starttime;
  	double		totaltime = 0;
- 	StringInfoData buf;
  	int			eflags;
  
  	/*
--- 236,246 ----
   * to call it.
   */
  void
! ExplainOnePlan(ExplainState *es, const char *queryString, ParamListInfo params)
  {
  	QueryDesc  *queryDesc;
  	instr_time	starttime;
  	double		totaltime = 0;
  	int			eflags;
  
  	/*
***************
*** 236,254 **** ExplainOnePlan(PlannedStmt *plannedstmt, ExplainStmt *stmt,
  	PushUpdatedSnapshot(GetActiveSnapshot());
  
  	/* Create a QueryDesc requesting no output */
! 	queryDesc = CreateQueryDesc(plannedstmt, queryString,
  								GetActiveSnapshot(), InvalidSnapshot,
! 								None_Receiver, params,
! 								stmt->analyze);
  
  	INSTR_TIME_SET_CURRENT(starttime);
  
  	/* If analyzing, we need to cope with queued triggers */
! 	if (stmt->analyze)
  		AfterTriggerBeginQuery();
  
  	/* Select execution options */
! 	if (stmt->analyze)
  		eflags = 0;				/* default run-to-completion flags */
  	else
  		eflags = EXEC_FLAG_EXPLAIN_ONLY;
--- 250,267 ----
  	PushUpdatedSnapshot(GetActiveSnapshot());
  
  	/* Create a QueryDesc requesting no output */
! 	queryDesc = CreateQueryDesc(es->pstmt, queryString,
  								GetActiveSnapshot(), InvalidSnapshot,
! 								None_Receiver, params, es->analyze);
  
  	INSTR_TIME_SET_CURRENT(starttime);
  
  	/* If analyzing, we need to cope with queued triggers */
! 	if (es->analyze)
  		AfterTriggerBeginQuery();
  
  	/* Select execution options */
! 	if (es->analyze)
  		eflags = 0;				/* default run-to-completion flags */
  	else
  		eflags = EXEC_FLAG_EXPLAIN_ONLY;
***************
*** 257,263 **** ExplainOnePlan(PlannedStmt *plannedstmt, ExplainStmt *stmt,
  	ExecutorStart(queryDesc, eflags);
  
  	/* Execute the plan for statistics if asked for */
! 	if (stmt->analyze)
  	{
  		/* run the plan */
  		ExecutorRun(queryDesc, ForwardScanDirection, 0L);
--- 270,276 ----
  	ExecutorStart(queryDesc, eflags);
  
  	/* Execute the plan for statistics if asked for */
! 	if (es->analyze)
  	{
  		/* run the plan */
  		ExecutorRun(queryDesc, ForwardScanDirection, 0L);
***************
*** 267,281 **** ExplainOnePlan(PlannedStmt *plannedstmt, ExplainStmt *stmt,
  	}
  
  	/* Create textual dump of plan tree */
! 	initStringInfo(&buf);
! 	ExplainPrintPlan(&buf, queryDesc, stmt->analyze, stmt->verbose);
  
  	/*
  	 * If we ran the command, run any AFTER triggers it queued.  (Note this
  	 * will not include DEFERRED triggers; since those don't run until end of
  	 * transaction, we can't measure them.)  Include into total runtime.
  	 */
! 	if (stmt->analyze)
  	{
  		INSTR_TIME_SET_CURRENT(starttime);
  		AfterTriggerEndQuery(queryDesc->estate);
--- 280,293 ----
  	}
  
  	/* Create textual dump of plan tree */
! 	ExplainPrintPlan(es, queryDesc);
  
  	/*
  	 * If we ran the command, run any AFTER triggers it queued.  (Note this
  	 * will not include DEFERRED triggers; since those don't run until end of
  	 * transaction, we can't measure them.)  Include into total runtime.
  	 */
! 	if (es->analyze)
  	{
  		INSTR_TIME_SET_CURRENT(starttime);
  		AfterTriggerEndQuery(queryDesc->estate);
***************
*** 283,289 **** ExplainOnePlan(PlannedStmt *plannedstmt, ExplainStmt *stmt,
  	}
  
  	/* Print info about runtime of triggers */
! 	if (stmt->analyze)
  	{
  		ResultRelInfo *rInfo;
  		bool		show_relname;
--- 295,301 ----
  	}
  
  	/* Print info about runtime of triggers */
! 	if (es->analyze)
  	{
  		ResultRelInfo *rInfo;
  		bool		show_relname;
***************
*** 295,306 **** ExplainOnePlan(PlannedStmt *plannedstmt, ExplainStmt *stmt,
  		show_relname = (numrels > 1 || targrels != NIL);
  		rInfo = queryDesc->estate->es_result_relations;
  		for (nr = 0; nr < numrels; rInfo++, nr++)
! 			report_triggers(rInfo, show_relname, &buf);
  
  		foreach(l, targrels)
  		{
  			rInfo = (ResultRelInfo *) lfirst(l);
! 			report_triggers(rInfo, show_relname, &buf);
  		}
  	}
  
--- 307,318 ----
  		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);
  		}
  	}
  
***************
*** 317,361 **** ExplainOnePlan(PlannedStmt *plannedstmt, ExplainStmt *stmt,
  	PopActiveSnapshot();
  
  	/* We need a CCI just in case query expanded to multiple plans */
! 	if (stmt->analyze)
  		CommandCounterIncrement();
  
  	totaltime += elapsed_time(&starttime);
  
! 	if (stmt->analyze)
! 		appendStringInfo(&buf, "Total runtime: %.3f ms\n",
  						 1000.0 * totaltime);
- 	do_text_output_multiline(tstate, buf.data);
- 
- 	pfree(buf.data);
  }
  
  /*
   * ExplainPrintPlan -
   *	  convert a QueryDesc's plan tree to text and append it to 'str'
   *
-  * 'analyze' means to include runtime instrumentation results
-  * 'verbose' means a verbose printout (currently, it shows targetlists)
-  *
   * NB: will not work on utility statements
   */
  void
! ExplainPrintPlan(StringInfo str, QueryDesc *queryDesc,
! 				 bool analyze, bool verbose)
  {
! 	ExplainState es;
! 
! 	Assert(queryDesc->plannedstmt != NULL);
! 
! 	memset(&es, 0, sizeof(es));
! 	es.str = str;
! 	es.printTList = verbose;
! 	es.printAnalyze = analyze;
! 	es.pstmt = queryDesc->plannedstmt;
! 	es.rtable = queryDesc->plannedstmt->rtable;
  
  	ExplainNode(queryDesc->plannedstmt->planTree, queryDesc->planstate,
! 				NULL, 0, &es);
  }
  
  /*
--- 329,358 ----
  	PopActiveSnapshot();
  
  	/* We need a CCI just in case query expanded to multiple plans */
! 	if (es->analyze)
  		CommandCounterIncrement();
  
  	totaltime += elapsed_time(&starttime);
  
! 	if (es->analyze)
! 		appendStringInfo(&es->str, "Total runtime: %.3f ms\n",
  						 1000.0 * totaltime);
  }
  
  /*
   * ExplainPrintPlan -
   *	  convert a QueryDesc's plan tree to text and append it to 'str'
   *
   * NB: will not work on utility statements
   */
  void
! ExplainPrintPlan(ExplainState *es, QueryDesc *queryDesc)
  {
! 	Assert(queryDesc->plannedstmt == es->pstmt);
  
+ 	es->rtable = queryDesc->plannedstmt->rtable;
  	ExplainNode(queryDesc->plannedstmt->planTree, queryDesc->planstate,
! 				NULL, 0, es);
  }
  
  /*
***************
*** 439,451 **** ExplainNode(Plan *plan, PlanState *planstate,
  	if (indent)
  	{
  		Assert(indent >= 2);
! 		appendStringInfoSpaces(es->str, 2 * indent - 4);
! 		appendStringInfoString(es->str, "->  ");
  	}
  
  	if (plan == NULL)
  	{
! 		appendStringInfoChar(es->str, '\n');
  		return;
  	}
  
--- 436,448 ----
  	if (indent)
  	{
  		Assert(indent >= 2);
! 		appendStringInfoSpaces(&es->str, 2 * indent - 4);
! 		appendStringInfoString(&es->str, "->  ");
  	}
  
  	if (plan == NULL)
  	{
! 		appendStringInfoChar(&es->str, '\n');
  		return;
  	}
  
***************
*** 665,677 **** ExplainNode(Plan *plan, PlanState *planstate,
  			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:
--- 662,674 ----
  			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:
***************
*** 685,700 **** 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;
  	}
  
! 	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
--- 682,698 ----
  			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
***************
*** 707,725 **** ExplainNode(Plan *plan, PlanState *planstate,
  	{
  		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->printAnalyze)
! 		appendStringInfoString(es->str, " (never executed)");
! 	appendStringInfoChar(es->str, '\n');
  
  	/* target list */
! 	if (es->printTList)
  		show_plan_tlist(plan, indent, es);
  
  	/* quals, sort keys, etc */
--- 705,723 ----
  	{
  		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)
! 		appendStringInfo(&es->str, " (never executed)");
! 	appendStringInfoChar(&es->str, '\n');
  
  	/* target list */
! 	if (es->verbose)
  		show_plan_tlist(plan, indent, es);
  
  	/* quals, sort keys, etc */
***************
*** 889,896 **** show_plan_tlist(Plan *plan, int indent, ExplainState *es)
  	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;
--- 887,894 ----
  	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;
***************
*** 901,913 **** 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');
  }
  
  /*
--- 899,911 ----
  		if (tle->resjunk)
  			continue;
  		if (i++ > 0)
! 			appendStringInfo(&es->str, ", ");
! 		appendStringInfoString(&es->str,
  							   deparse_expression((Node *) tle->expr, context,
  												  useprefix, false));
  	}
  
! 	appendStringInfoChar(&es->str, '\n');
  }
  
  /*
***************
*** 940,948 **** show_qual(List *qual, const char *qlabel, Plan *plan, Plan *outer_plan,
  	/* Deparse the expression */
  	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);
  }
  
  /*
--- 938,946 ----
  	/* Deparse the expression */
  	exprstr = deparse_expression(node, context, useprefix, false);
  
! 	/* And add to str */
! 	appendStringInfoSpaces(&es->str, indent * 2);
! 	appendStringInfo(&es->str, "  %s: %s\n", qlabel, exprstr);
  }
  
  /*
***************
*** 988,995 **** 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,
--- 986,993 ----
  	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,
***************
*** 1011,1021 **** show_sort_keys(Plan *sortplan, int indent, ExplainState *es)
  									 useprefix, true);
  		/* And add to es->str */
  		if (keyno > 0)
! 			appendStringInfoString(es->str, ", ");
! 		appendStringInfoString(es->str, exprstr);
  	}
  
! 	appendStringInfoChar(es->str, '\n');
  }
  
  /*
--- 1009,1019 ----
  									 useprefix, true);
  		/* And add to es->str */
  		if (keyno > 0)
! 			appendStringInfo(&es->str, ", ");
! 		appendStringInfoString(&es->str, exprstr);
  	}
  
! 	appendStringInfoChar(&es->str, '\n');
  }
  
  /*
***************
*** 1025,1038 **** static void
  show_sort_info(SortState *sortstate, int indent, ExplainState *es)
  {
  	Assert(IsA(sortstate, SortState));
! 	if (es->printAnalyze && 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);
  	}
  }
--- 1023,1036 ----
  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);
  	}
  }
***************
*** 1127,1137 **** ExplainScanTarget(Scan *plan, ExplainState *es)
  			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));
  }
  
--- 1125,1135 ----
  			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));
  }
  
***************
*** 1172,1179 **** 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);
  	}
--- 1170,1177 ----
  		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);
  	}
*** a/src/backend/commands/prepare.c
--- b/src/backend/commands/prepare.c
***************
*** 641,649 **** DropAllPreparedStatements(void)
   * not the original PREPARE; we get the latter string from the plancache.
   */
  void
! ExplainExecuteQuery(ExecuteStmt *execstmt, ExplainStmt *stmt,
! 					const char *queryString,
! 					ParamListInfo params, TupOutputState *tstate)
  {
  	PreparedStatement *entry;
  	const char *query_string;
--- 641,648 ----
   * not the original PREPARE; we get the latter string from the plancache.
   */
  void
! ExplainExecuteQuery(ExecuteStmt *execstmt, ExplainState *es,
! 					const char *queryString, ParamListInfo params)
  {
  	PreparedStatement *entry;
  	const char *query_string;
***************
*** 707,726 **** ExplainExecuteQuery(ExecuteStmt *execstmt, ExplainStmt *stmt,
  				pstmt->intoClause = execstmt->into;
  			}
  
! 			ExplainOnePlan(pstmt, stmt, query_string,
! 						   paramLI, tstate);
  		}
  		else
  		{
! 			ExplainOneUtility((Node *) pstmt, stmt, query_string,
! 							  params, tstate);
  		}
  
  		/* No need for CommandCounterIncrement, as ExplainOnePlan did it */
  
  		/* put a blank line between plans */
  		if (!is_last_query)
! 			do_text_output_oneline(tstate, "");
  	}
  
  	if (estate)
--- 706,724 ----
  				pstmt->intoClause = execstmt->into;
  			}
  
! 			es->pstmt = pstmt;
! 			ExplainOnePlan(es, query_string, paramLI);
  		}
  		else
  		{
! 			ExplainOneUtility((Node *) pstmt, es, query_string, params);
  		}
  
  		/* No need for CommandCounterIncrement, as ExplainOnePlan did it */
  
  		/* put a blank line between plans */
  		if (!is_last_query)
! 			appendStringInfo(&es->str, "\n");
  	}
  
  	if (estate)
*** a/src/backend/nodes/copyfuncs.c
--- b/src/backend/nodes/copyfuncs.c
***************
*** 2875,2882 **** _copyExplainStmt(ExplainStmt *from)
  	ExplainStmt *newnode = makeNode(ExplainStmt);
  
  	COPY_NODE_FIELD(query);
! 	COPY_SCALAR_FIELD(verbose);
! 	COPY_SCALAR_FIELD(analyze);
  
  	return newnode;
  }
--- 2875,2881 ----
  	ExplainStmt *newnode = makeNode(ExplainStmt);
  
  	COPY_NODE_FIELD(query);
! 	COPY_NODE_FIELD(options);
  
  	return newnode;
  }
*** a/src/backend/nodes/equalfuncs.c
--- b/src/backend/nodes/equalfuncs.c
***************
*** 1467,1474 **** static bool
  _equalExplainStmt(ExplainStmt *a, ExplainStmt *b)
  {
  	COMPARE_NODE_FIELD(query);
! 	COMPARE_SCALAR_FIELD(verbose);
! 	COMPARE_SCALAR_FIELD(analyze);
  
  	return true;
  }
--- 1467,1473 ----
  _equalExplainStmt(ExplainStmt *a, ExplainStmt *b)
  {
  	COMPARE_NODE_FIELD(query);
! 	COMPARE_NODE_FIELD(options);
  
  	return true;
  }
*** a/src/backend/nodes/makefuncs.c
--- b/src/backend/nodes/makefuncs.c
***************
*** 17,25 ****
  
  #include "catalog/pg_type.h"
  #include "nodes/makefuncs.h"
  #include "utils/lsyscache.h"
  
- 
  /*
   * makeA_Expr -
   *		makes an A_Expr node
--- 17,25 ----
  
  #include "catalog/pg_type.h"
  #include "nodes/makefuncs.h"
+ #include "utils/builtins.h"
  #include "utils/lsyscache.h"
  
  /*
   * makeA_Expr -
   *		makes an A_Expr node
***************
*** 385,387 **** makeDefElemExtended(char *nameSpace, char *name, Node *arg,
--- 385,402 ----
  
  	return res;
  }
+ 
+ /*
+  * makeExplain -
+  *  build an ExplainStmt node
+  */
+ ExplainStmt *
+ makeExplain(Node *query, List *options)
+ {
+ 	ExplainStmt *n = makeNode(ExplainStmt);
+ 
+ 	n->query = query;
+ 	n->options = options;
+ 
+ 	return n;
+ }
*** a/src/backend/parser/gram.y
--- b/src/backend/parser/gram.y
***************
*** 321,327 **** static TypeName *TableFuncTypeName(List *columns);
  %type <list>	opt_interval interval_second
  %type <node>	overlay_placing substr_from substr_for
  
! %type <boolean> opt_instead opt_analyze
  %type <boolean> index_opt_unique opt_verbose opt_full
  %type <boolean> opt_freeze opt_default opt_recheck
  %type <defelt>	opt_binary opt_oids copy_delimiter
--- 321,327 ----
  %type <list>	opt_interval interval_second
  %type <node>	overlay_placing substr_from substr_for
  
! %type <boolean> opt_instead
  %type <boolean> index_opt_unique opt_verbose opt_full
  %type <boolean> opt_freeze opt_default opt_recheck
  %type <defelt>	opt_binary opt_oids copy_delimiter
***************
*** 369,374 **** static TypeName *TableFuncTypeName(List *columns);
--- 369,379 ----
  %type <defelt>	generic_option_elem alter_generic_option_elem
  %type <list>	generic_option_list alter_generic_option_list
  
+ %type <str>		explain_option_name
+ %type <node>	explain_option_arg
+ %type <defelt>	explain_option_elem
+ %type <list>	explain_option_list
+ 
  %type <typnam>	Typename SimpleTypename ConstTypename
  				GenericType Numeric opt_float
  				Character ConstCharacter
***************
*** 6469,6484 **** opt_name_list:
   *
   *		QUERY:
   *				EXPLAIN [ANALYZE] [VERBOSE] query
   *
   *****************************************************************************/
  
! ExplainStmt: EXPLAIN opt_analyze opt_verbose ExplainableStmt
  				{
! 					ExplainStmt *n = makeNode(ExplainStmt);
! 					n->analyze = $2;
! 					n->verbose = $3;
! 					n->query = $4;
! 					$$ = (Node *)n;
  				}
  		;
  
--- 6474,6503 ----
   *
   *		QUERY:
   *				EXPLAIN [ANALYZE] [VERBOSE] query
+  *				EXPLAIN ( options ) query
   *
   *****************************************************************************/
  
! ExplainStmt:
! 		EXPLAIN ExplainableStmt
  				{
! 					$$ = (Node *) makeExplain((Node *) $2, NIL);
! 				}
! 		| EXPLAIN ANALYZE opt_verbose ExplainableStmt
! 				{
! 					List *l = list_make1(makeDefElem("analyze", NULL));
! 					if ($3)
! 						lappend(l, makeDefElem("verbose", NULL));
! 					$$ = (Node *) makeExplain((Node *) $4, l);
! 				}
! 		| EXPLAIN VERBOSE ExplainableStmt
! 				{
! 					List *l = list_make1(makeDefElem("verbose", NULL));
! 					$$ = (Node *) makeExplain((Node *) $3, l);
! 				}
! 		|	EXPLAIN '(' explain_option_list ')' ExplainableStmt
! 				{
! 					$$ = (Node *) makeExplain((Node *) $5, (List *) $3);
  				}
  		;
  
***************
*** 6492,6500 **** ExplainableStmt:
  			| ExecuteStmt					/* by default all are $$=$1 */
  		;
  
! opt_analyze:
! 			analyze_keyword			{ $$ = TRUE; }
! 			| /* EMPTY */			{ $$ = FALSE; }
  		;
  
  /*****************************************************************************
--- 6511,6545 ----
  			| ExecuteStmt					/* by default all are $$=$1 */
  		;
  
! explain_option_list:
! 			explain_option_elem
! 				{
! 					$$ = list_make1($1);
! 				}
! 			| explain_option_list ',' explain_option_elem
! 				{
! 					$$ = lappend($1, $3);
! 				}
! 		;
! 
! explain_option_elem:
! 			explain_option_name explain_option_arg
! 				{
! 					$$ = makeDefElem($1, $2);
! 				}
! 		;
! 
! explain_option_name:
! 				ColId			{ $$ = $1; }
! 			| VERBOSE			{ $$ = "verbose"; }
! 			| analyze_keyword	{ $$ = "analyze"; }
! 		;
! 
! explain_option_arg:
! 			  opt_boolean			{ $$ = (Node *) makeString($1); }
! 			| ColId_or_Sconst		{ $$ = (Node *) makeString($1); }
! 			| SignedIconst			{ $$ = (Node *) makeInteger($1); }
! 			| /* EMPTY */			{ $$ = NULL; }
  		;
  
  /*****************************************************************************
*** a/src/backend/tcop/utility.c
--- b/src/backend/tcop/utility.c
***************
*** 2316,2325 **** GetCommandLogLevel(Node *parsetree)
  		case T_ExplainStmt:
  			{
  				ExplainStmt *stmt = (ExplainStmt *) parsetree;
  
  				/* Look through an EXPLAIN ANALYZE to the contained stmt */
! 				if (stmt->analyze)
  					return GetCommandLogLevel(stmt->query);
  				/* Plain EXPLAIN isn't so interesting */
  				lev = LOGSTMT_ALL;
  			}
--- 2316,2334 ----
  		case T_ExplainStmt:
  			{
  				ExplainStmt *stmt = (ExplainStmt *) parsetree;
+ 				ListCell	*lc;
+ 				int			 analyze = 0;
  
  				/* Look through an EXPLAIN ANALYZE to the contained stmt */
! 				foreach (lc, stmt->options)
! 				{
! 					DefElem *opt = lfirst(lc);
! 					if (!strcmp(opt->defname, "analyze"))
! 							analyze = defGetBoolean(opt);
! 				}
! 				if (analyze)
  					return GetCommandLogLevel(stmt->query);
+ 
  				/* Plain EXPLAIN isn't so interesting */
  				lev = LOGSTMT_ALL;
  			}
*** a/src/include/commands/explain.h
--- b/src/include/commands/explain.h
***************
*** 15,26 ****
  
  #include "executor/executor.h"
  
  /* Hook for plugins to get control in ExplainOneQuery() */
  typedef void (*ExplainOneQuery_hook_type) (Query *query,
! 													   ExplainStmt *stmt,
  													 const char *queryString,
! 													   ParamListInfo params,
! 													 TupOutputState *tstate);
  extern PGDLLIMPORT ExplainOneQuery_hook_type ExplainOneQuery_hook;
  
  /* Hook for plugins to get control in explain_get_index_name() */
--- 15,37 ----
  
  #include "executor/executor.h"
  
+ typedef struct ExplainState
+ {
+ 	StringInfoData	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() */
  typedef void (*ExplainOneQuery_hook_type) (Query *query,
! 													   ExplainState *es,
  													 const char *queryString,
! 													   ParamListInfo params);
  extern PGDLLIMPORT ExplainOneQuery_hook_type ExplainOneQuery_hook;
  
  /* Hook for plugins to get control in explain_get_index_name() */
***************
*** 28,49 **** typedef const char *(*explain_get_index_name_hook_type) (Oid indexId);
  extern PGDLLIMPORT explain_get_index_name_hook_type explain_get_index_name_hook;
  
  
! extern void ExplainQuery(ExplainStmt *stmt, const char *queryString,
  			 ParamListInfo params, DestReceiver *dest);
  
  extern TupleDesc ExplainResultDesc(ExplainStmt *stmt);
  
! extern void ExplainOneUtility(Node *utilityStmt, ExplainStmt *stmt,
  				  const char *queryString,
! 				  ParamListInfo params,
! 				  TupOutputState *tstate);
  
! extern void ExplainOnePlan(PlannedStmt *plannedstmt, ExplainStmt *stmt,
! 			   const char *queryString,
! 			   ParamListInfo params,
! 			   TupOutputState *tstate);
  
! extern void ExplainPrintPlan(StringInfo str, QueryDesc *queryDesc,
! 				 bool analyze, bool verbose);
  
  #endif   /* EXPLAIN_H */
--- 39,58 ----
  extern PGDLLIMPORT explain_get_index_name_hook_type explain_get_index_name_hook;
  
  
! extern void ExplainQuery(ExplainStmt *es, const char *queryString,
  			 ParamListInfo params, DestReceiver *dest);
  
+ extern void ExplainInitState(ExplainState *es);
+ 
  extern TupleDesc ExplainResultDesc(ExplainStmt *stmt);
  
! extern void ExplainOneUtility(Node *utilityStmt, ExplainState *es,
  				  const char *queryString,
! 				  ParamListInfo params);
  
! extern void ExplainOnePlan(ExplainState *es, const char *queryString,
! 			   ParamListInfo params);
  
! extern void ExplainPrintPlan(ExplainState *stmt, QueryDesc *queryDesc);
  
  #endif   /* EXPLAIN_H */
*** a/src/include/commands/prepare.h
--- b/src/include/commands/prepare.h
***************
*** 13,18 ****
--- 13,19 ----
  #ifndef PREPARE_H
  #define PREPARE_H
  
+ #include "commands/explain.h"
  #include "executor/executor.h"
  #include "utils/plancache.h"
  #include "utils/timestamp.h"
***************
*** 40,48 **** extern void ExecuteQuery(ExecuteStmt *stmt, const char *queryString,
  			 ParamListInfo params,
  			 DestReceiver *dest, char *completionTag);
  extern void DeallocateQuery(DeallocateStmt *stmt);
! extern void ExplainExecuteQuery(ExecuteStmt *execstmt, ExplainStmt *stmt,
  					const char *queryString,
! 					ParamListInfo params, TupOutputState *tstate);
  
  /* Low-level access to stored prepared statements */
  extern void StorePreparedStatement(const char *stmt_name,
--- 41,49 ----
  			 ParamListInfo params,
  			 DestReceiver *dest, char *completionTag);
  extern void DeallocateQuery(DeallocateStmt *stmt);
! extern void ExplainExecuteQuery(ExecuteStmt *execstmt, ExplainState *es,
  					const char *queryString,
! 					ParamListInfo params);
  
  /* Low-level access to stored prepared statements */
  extern void StorePreparedStatement(const char *stmt_name,
*** a/src/include/nodes/makefuncs.h
--- b/src/include/nodes/makefuncs.h
***************
*** 69,72 **** extern DefElem *makeDefElem(char *name, Node *arg);
--- 69,74 ----
  extern DefElem *makeDefElemExtended(char *nameSpace, char *name, Node *arg,
  					DefElemAction defaction);
  
+ extern ExplainStmt *makeExplain(Node *query, List *options);
+ 
  #endif   /* MAKEFUNC_H */
*** a/src/include/nodes/parsenodes.h
--- b/src/include/nodes/parsenodes.h
***************
*** 2193,2200 **** typedef struct ExplainStmt
  {
  	NodeTag		type;
  	Node	   *query;			/* the query (as a raw parse tree) */
! 	bool		verbose;		/* print plan info */
! 	bool		analyze;		/* get statistics by executing plan */
  } ExplainStmt;
  
  /* ----------------------
--- 2193,2199 ----
  {
  	NodeTag		type;
  	Node	   *query;			/* the query (as a raw parse tree) */
! 	List	   *options;		/* List of DefElem nodes */
  } ExplainStmt;
  
  /* ----------------------
#14Tom Lane
tgl@sss.pgh.pa.us
In reply to: Robert Haas (#13)
Re: generic explain options v3

Robert Haas <robertmhaas@gmail.com> writes:

Here's the update. There are a few things that I'm not entirely happy
with here, but not quite sure what to do about either.

Committed with a few editorializations.

- ExplainPrintPlan() is now almost trivial. It seems like there
should be some way to get rid of this altogether, but I'm not quite
sure how. I thought about ripping pstmt and rtable out of
ExplainState and just storying queryDesc there. But that involves
changing a lot of code, and while it makes some things simpler, it
makes other parts more complex. I'm not sure whether it's a win or
not; I'm also not sure how much brainpower it's worth spending on
this.

I think the problem here is that you chose to treat ExplainState.pstmt
as a parameter, when it's better considered as an internal field.
I changed it to the latter approach.

- It's becoming increasingly evident to me that the explain stuff in
prepare.c has no business being there and should be moved to
explain.c. I haven't done that here, but it's worth thinking about.

I'm unconvinced. The reason that code is that way is that the
alternative would require explain.c to know quite a lot about prepared
plans, which does not seem like an improvement.

- The hack needed in ExplainLogLevel is just that.

Yeah, I thought that was okay. We could alternatively refactor the
code so that the parameter analysis code is a separate function that
utility.c could call, but it's unclear that it's worth the trouble.

regards, tom lane

#15Robert Haas
robertmhaas@gmail.com
In reply to: Tom Lane (#14)
Re: generic explain options v3

On Sun, Jul 26, 2009 at 7:40 PM, Tom Lane<tgl@sss.pgh.pa.us> wrote:

Robert Haas <robertmhaas@gmail.com> writes:

Here's the update.  There are a few things that I'm not entirely happy
with here, but not quite sure what to do about either.

Committed with a few editorializations.

Thanks.

- ExplainPrintPlan() is now almost trivial.  It seems like there
should be some way to get rid of this altogether, but I'm not quite
sure how.  I thought about ripping pstmt and rtable out of
ExplainState and just storying queryDesc there.  But that involves
changing a lot of code, and while it makes some things simpler, it
makes other parts more complex.  I'm not sure whether it's a win or
not; I'm also not sure how much brainpower it's worth spending on
this.

I think the problem here is that you chose to treat ExplainState.pstmt
as a parameter, when it's better considered as an internal field.
I changed it to the latter approach.

Sounds fine.

- It's becoming increasingly evident to me that the explain stuff in
prepare.c has no business being there and should be moved to
explain.c.  I haven't done that here, but it's worth thinking about.

I'm unconvinced.  The reason that code is that way is that the
alternative would require explain.c to know quite a lot about prepared
plans, which does not seem like an improvement.

I didn't consider that. As it is, prepare.c has to know quite a lot
about explaining, so it may be six of one, half a dozen of the other.

- The hack needed in ExplainLogLevel is just that.

Yeah, I thought that was okay.  We could alternatively refactor the
code so that the parameter analysis code is a separate function that
utility.c could call, but it's unclear that it's worth the trouble.

OK.

It seems I have quite a bit of work in front of me unbreaking the
machine-readable explain patch. I started grinding through it, but
it's not pretty. I'll post an updated version when I have it.

...Robert