* Portions Copyright (c) 1994-5, Regents of the University of California
*
* IDENTIFICATION
- * $PostgreSQL: pgsql/src/backend/commands/explain.c,v 1.188 2009/07/26 23:34:17 tgl Exp $
+ * $PostgreSQL: pgsql/src/backend/commands/explain.c,v 1.189 2009/08/10 05:46:50 tgl Exp $
*
*-------------------------------------------------------------------------
*/
#include "utils/lsyscache.h"
#include "utils/tuplesort.h"
#include "utils/snapmgr.h"
+#include "utils/xml.h"
/* Hook for plugins to get control in ExplainOneQuery() */
explain_get_index_name_hook_type explain_get_index_name_hook = NULL;
+/* OR-able flags for ExplainXMLTag() */
+#define X_OPENING 0
+#define X_CLOSING 1
+#define X_CLOSE_IMMEDIATE 2
+#define X_NOWHITESPACE 4
+
static void ExplainOneQuery(Query *query, ExplainState *es,
const char *queryString, ParamListInfo params);
static void report_triggers(ResultRelInfo *rInfo, bool show_relname,
- StringInfo buf);
+ ExplainState *es);
static double elapsed_time(instr_time *starttime);
static void ExplainNode(Plan *plan, PlanState *planstate,
- Plan *outer_plan, int indent, ExplainState *es);
-static void show_plan_tlist(Plan *plan, int indent, ExplainState *es);
+ Plan *outer_plan,
+ const char *relationship, const char *plan_name,
+ ExplainState *es);
+static void show_plan_tlist(Plan *plan, ExplainState *es);
static void show_qual(List *qual, const char *qlabel, Plan *plan,
- Plan *outer_plan, int indent, bool useprefix, ExplainState *es);
+ Plan *outer_plan, bool useprefix, ExplainState *es);
static void show_scan_qual(List *qual, const char *qlabel,
Plan *scan_plan, Plan *outer_plan,
- int indent, ExplainState *es);
+ ExplainState *es);
static void show_upper_qual(List *qual, const char *qlabel, Plan *plan,
- int indent, ExplainState *es);
-static void show_sort_keys(Plan *sortplan, int indent, ExplainState *es);
-static void show_sort_info(SortState *sortstate, int indent, ExplainState *es);
+ ExplainState *es);
+static void show_sort_keys(Plan *sortplan, ExplainState *es);
+static void show_sort_info(SortState *sortstate, ExplainState *es);
static const char *explain_get_index_name(Oid indexId);
static void ExplainScanTarget(Scan *plan, ExplainState *es);
static void ExplainMemberNodes(List *plans, PlanState **planstate,
- Plan *outer_plan, int indent, ExplainState *es);
-static void ExplainSubPlans(List *plans, int indent, ExplainState *es);
+ Plan *outer_plan, ExplainState *es);
+static void ExplainSubPlans(List *plans, const char *relationship,
+ ExplainState *es);
+static void ExplainPropertyList(const char *qlabel, List *data,
+ ExplainState *es);
+static void ExplainProperty(const char *qlabel, const char *value,
+ bool numeric, ExplainState *es);
+#define ExplainPropertyText(qlabel, value, es) \
+ ExplainProperty(qlabel, value, false, es)
+static void ExplainPropertyInteger(const char *qlabel, int value,
+ ExplainState *es);
+static void ExplainPropertyLong(const char *qlabel, long value,
+ ExplainState *es);
+static void ExplainPropertyFloat(const char *qlabel, double value, int ndigits,
+ ExplainState *es);
+static void ExplainOpenGroup(const char *objtype, const char *labelname,
+ bool labeled, ExplainState *es);
+static void ExplainCloseGroup(const char *objtype, const char *labelname,
+ bool labeled, ExplainState *es);
+static void ExplainDummyGroup(const char *objtype, const char *labelname,
+ ExplainState *es);
+static void ExplainBeginOutput(ExplainState *es);
+static void ExplainEndOutput(ExplainState *es);
+static void ExplainXMLTag(const char *tagname, int flags, ExplainState *es);
+static void ExplainJSONLineEnding(ExplainState *es);
+static void escape_json(StringInfo buf, const char *str);
/*
es.verbose = defGetBoolean(opt);
else if (strcmp(opt->defname, "costs") == 0)
es.costs = defGetBoolean(opt);
+ else if (strcmp(opt->defname, "format") == 0)
+ {
+ char *p = defGetString(opt);
+
+ if (strcmp(p, "text") == 0)
+ es.format = EXPLAIN_FORMAT_TEXT;
+ else if (strcmp(p, "xml") == 0)
+ es.format = EXPLAIN_FORMAT_XML;
+ else if (strcmp(p, "json") == 0)
+ es.format = EXPLAIN_FORMAT_JSON;
+ else
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("unrecognized value for EXPLAIN option \"%s\": \"%s\"",
+ opt->defname, p)));
+ }
else
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
rewritten = pg_analyze_and_rewrite((Node *) copyObject(stmt->query),
queryString, param_types, num_params);
+ /* emit opening boilerplate */
+ ExplainBeginOutput(&es);
+
if (rewritten == NIL)
{
- /* In the case of an INSTEAD NOTHING, tell at least that */
- appendStringInfoString(es.str, "Query rewrites to nothing\n");
+ /*
+ * In the case of an INSTEAD NOTHING, tell at least that. But in
+ * non-text format, the output is delimited, so this isn't necessary.
+ */
+ if (es.format == EXPLAIN_FORMAT_TEXT)
+ appendStringInfoString(es.str, "Query rewrites to nothing\n");
}
else
{
foreach(l, rewritten)
{
ExplainOneQuery((Query *) lfirst(l), &es, queryString, params);
- /* put a blank line between plans */
+
+ /* Separate plans with an appropriate separator */
if (lnext(l) != NULL)
- appendStringInfoChar(es.str, '\n');
+ ExplainSeparatePlans(&es);
}
}
+ /* emit closing boilerplate */
+ ExplainEndOutput(&es);
+ Assert(es.indent == 0);
+
/* output tuples */
tstate = begin_tup_output_tupdesc(dest, ExplainResultDesc(stmt));
- do_text_output_multiline(tstate, es.str->data);
+ if (es.format == EXPLAIN_FORMAT_TEXT)
+ do_text_output_multiline(tstate, es.str->data);
+ else
+ do_text_output_oneline(tstate, es.str->data);
end_tup_output(tstate);
pfree(es.str->data);
ExplainResultDesc(ExplainStmt *stmt)
{
TupleDesc tupdesc;
+ ListCell *lc;
+ bool xml = false;
- /* need a tuple descriptor representing a single TEXT column */
+ /* Check for XML format option */
+ foreach(lc, stmt->options)
+ {
+ DefElem *opt = (DefElem *) lfirst(lc);
+
+ if (strcmp(opt->defname, "format") == 0)
+ {
+ char *p = defGetString(opt);
+
+ xml = (strcmp(p, "xml") == 0);
+ }
+ }
+
+ /* Need a tuple descriptor representing a single TEXT or XML column */
tupdesc = CreateTemplateTupleDesc(1, false);
TupleDescInitEntry(tupdesc, (AttrNumber) 1, "QUERY PLAN",
- TEXTOID, -1, 0);
+ xml ? XMLOID : TEXTOID, -1, 0);
return tupdesc;
}
ExplainExecuteQuery((ExecuteStmt *) utilityStmt, es,
queryString, params);
else if (IsA(utilityStmt, NotifyStmt))
- appendStringInfoString(es->str, "NOTIFY\n");
+ {
+ if (es->format == EXPLAIN_FORMAT_TEXT)
+ appendStringInfoString(es->str, "NOTIFY\n");
+ else
+ ExplainDummyGroup("Notify", NULL, es);
+ }
else
- appendStringInfoString(es->str,
+ {
+ if (es->format == EXPLAIN_FORMAT_TEXT)
+ appendStringInfoString(es->str,
"Utility statements have no plan structure\n");
+ else
+ ExplainDummyGroup("Utility Statement", NULL, es);
+ }
}
/*
totaltime += elapsed_time(&starttime);
}
+ ExplainOpenGroup("Query", NULL, true, es);
+
/* Create textual dump of plan tree */
ExplainPrintPlan(es, queryDesc);
int nr;
ListCell *l;
+ ExplainOpenGroup("Triggers", "Triggers", false, es);
+
show_relname = (numrels > 1 || targrels != NIL);
rInfo = queryDesc->estate->es_result_relations;
for (nr = 0; nr < numrels; rInfo++, nr++)
- report_triggers(rInfo, show_relname, es->str);
+ report_triggers(rInfo, show_relname, es);
foreach(l, targrels)
{
rInfo = (ResultRelInfo *) lfirst(l);
- report_triggers(rInfo, show_relname, es->str);
+ report_triggers(rInfo, show_relname, es);
}
+
+ ExplainCloseGroup("Triggers", "Triggers", false, es);
}
/*
totaltime += elapsed_time(&starttime);
if (es->analyze)
- appendStringInfo(es->str, "Total runtime: %.3f ms\n",
- 1000.0 * totaltime);
+ {
+ if (es->format == EXPLAIN_FORMAT_TEXT)
+ appendStringInfo(es->str, "Total runtime: %.3f ms\n",
+ 1000.0 * totaltime);
+ else
+ ExplainPropertyFloat("Total Runtime", 1000.0 * totaltime,
+ 3, es);
+ }
+
+ ExplainCloseGroup("Query", NULL, true, es);
}
/*
es->pstmt = queryDesc->plannedstmt;
es->rtable = queryDesc->plannedstmt->rtable;
ExplainNode(queryDesc->plannedstmt->planTree, queryDesc->planstate,
- NULL, 0, es);
+ NULL, NULL, NULL, es);
}
/*
* report execution stats for a single relation's triggers
*/
static void
-report_triggers(ResultRelInfo *rInfo, bool show_relname, StringInfo buf)
+report_triggers(ResultRelInfo *rInfo, bool show_relname, ExplainState *es)
{
int nt;
{
Trigger *trig = rInfo->ri_TrigDesc->triggers + nt;
Instrumentation *instr = rInfo->ri_TrigInstrument + nt;
- char *conname;
+ char *relname;
+ char *conname = NULL;
/* Must clean up instrumentation state */
InstrEndLoop(instr);
if (instr->ntuples == 0)
continue;
- if (OidIsValid(trig->tgconstraint) &&
- (conname = get_constraint_name(trig->tgconstraint)) != NULL)
+ ExplainOpenGroup("Trigger", NULL, true, es);
+
+ relname = RelationGetRelationName(rInfo->ri_RelationDesc);
+ if (OidIsValid(trig->tgconstraint))
+ conname = get_constraint_name(trig->tgconstraint);
+
+ /*
+ * In text format, we avoid printing both the trigger name and the
+ * constraint name unless VERBOSE is specified. In non-text
+ * formats we just print everything.
+ */
+ if (es->format == EXPLAIN_FORMAT_TEXT)
{
- appendStringInfo(buf, "Trigger for constraint %s", conname);
- pfree(conname);
+ if (es->verbose || conname == NULL)
+ appendStringInfo(es->str, "Trigger %s", trig->tgname);
+ else
+ appendStringInfoString(es->str, "Trigger");
+ if (conname)
+ appendStringInfo(es->str, " for constraint %s", conname);
+ if (show_relname)
+ appendStringInfo(es->str, " on %s", relname);
+ appendStringInfo(es->str, ": time=%.3f calls=%.0f\n",
+ 1000.0 * instr->total, instr->ntuples);
}
else
- appendStringInfo(buf, "Trigger %s", trig->tgname);
+ {
+ ExplainPropertyText("Trigger Name", trig->tgname, es);
+ if (conname)
+ ExplainPropertyText("Constraint Name", conname, es);
+ ExplainPropertyText("Relation", relname, es);
+ ExplainPropertyFloat("Time", 1000.0 * instr->total, 3, es);
+ ExplainPropertyFloat("Calls", instr->ntuples, 0, es);
+ }
- if (show_relname)
- appendStringInfo(buf, " on %s",
- RelationGetRelationName(rInfo->ri_RelationDesc));
+ if (conname)
+ pfree(conname);
- appendStringInfo(buf, ": time=%.3f calls=%.0f\n",
- 1000.0 * instr->total, instr->ntuples);
+ ExplainCloseGroup("Trigger", NULL, true, es);
}
}
/*
* ExplainNode -
- * converts a Plan node into ascii string and appends it to es->str
+ * Appends a description of the Plan node to es->str
*
* planstate points to the executor state node corresponding to the plan node.
* We need this to get at the instrumentation data (if any) as well as the
* side of a join with the current node. This is only interesting for
* deciphering runtime keys of an inner indexscan.
*
- * If indent is positive, we indent the plan output accordingly and put "->"
- * in front of it. This should only happen for child plan nodes.
+ * relationship describes the relationship of this plan node to its parent
+ * (eg, "Outer", "Inner"); it can be null at top level. plan_name is an
+ * optional name to be attached to the node.
+ *
+ * In text format, es->indent is controlled in this function since we only
+ * want it to change at Plan-node boundaries. In non-text formats, es->indent
+ * corresponds to the nesting depth of logical output groups, and therefore
+ * is controlled by ExplainOpenGroup/ExplainCloseGroup.
*/
static void
ExplainNode(Plan *plan, PlanState *planstate,
Plan *outer_plan,
- int indent, ExplainState *es)
+ const char *relationship, const char *plan_name,
+ ExplainState *es)
{
- const char *pname;
+ const char *pname; /* node type name for text output */
+ const char *sname; /* node type name for non-text output */
+ const char *strategy = NULL;
+ int save_indent = es->indent;
+ bool haschildren;
- if (indent)
- {
- Assert(indent >= 2);
- appendStringInfoSpaces(es->str, 2 * indent - 4);
- appendStringInfoString(es->str, "-> ");
- }
-
- if (plan == NULL)
- {
- appendStringInfoChar(es->str, '\n');
- return;
- }
+ Assert(plan);
switch (nodeTag(plan))
{
case T_Result:
- pname = "Result";
+ pname = sname = "Result";
break;
case T_Append:
- pname = "Append";
+ pname = sname = "Append";
break;
case T_RecursiveUnion:
- pname = "Recursive Union";
+ pname = sname = "Recursive Union";
break;
case T_BitmapAnd:
- pname = "BitmapAnd";
+ pname = sname = "BitmapAnd";
break;
case T_BitmapOr:
- pname = "BitmapOr";
+ pname = sname = "BitmapOr";
break;
case T_NestLoop:
- switch (((NestLoop *) plan)->join.jointype)
- {
- case JOIN_INNER:
- pname = "Nested Loop";
- break;
- case JOIN_LEFT:
- pname = "Nested Loop Left Join";
- break;
- case JOIN_FULL:
- pname = "Nested Loop Full Join";
- break;
- case JOIN_RIGHT:
- pname = "Nested Loop Right Join";
- break;
- case JOIN_SEMI:
- pname = "Nested Loop Semi Join";
- break;
- case JOIN_ANTI:
- pname = "Nested Loop Anti Join";
- break;
- default:
- pname = "Nested Loop ??? Join";
- break;
- }
+ pname = sname = "Nested Loop";
break;
case T_MergeJoin:
- switch (((MergeJoin *) plan)->join.jointype)
- {
- case JOIN_INNER:
- pname = "Merge Join";
- break;
- case JOIN_LEFT:
- pname = "Merge Left Join";
- break;
- case JOIN_FULL:
- pname = "Merge Full Join";
- break;
- case JOIN_RIGHT:
- pname = "Merge Right Join";
- break;
- case JOIN_SEMI:
- pname = "Merge Semi Join";
- break;
- case JOIN_ANTI:
- pname = "Merge Anti Join";
- break;
- default:
- pname = "Merge ??? Join";
- break;
- }
+ pname = "Merge"; /* "Join" gets added by jointype switch */
+ sname = "Merge Join";
break;
case T_HashJoin:
- switch (((HashJoin *) plan)->join.jointype)
- {
- case JOIN_INNER:
- pname = "Hash Join";
- break;
- case JOIN_LEFT:
- pname = "Hash Left Join";
- break;
- case JOIN_FULL:
- pname = "Hash Full Join";
- break;
- case JOIN_RIGHT:
- pname = "Hash Right Join";
- break;
- case JOIN_SEMI:
- pname = "Hash Semi Join";
- break;
- case JOIN_ANTI:
- pname = "Hash Anti Join";
- break;
- default:
- pname = "Hash ??? Join";
- break;
- }
+ pname = "Hash"; /* "Join" gets added by jointype switch */
+ sname = "Hash Join";
break;
case T_SeqScan:
- pname = "Seq Scan";
+ pname = sname = "Seq Scan";
break;
case T_IndexScan:
- pname = "Index Scan";
+ pname = sname = "Index Scan";
break;
case T_BitmapIndexScan:
- pname = "Bitmap Index Scan";
+ pname = sname = "Bitmap Index Scan";
break;
case T_BitmapHeapScan:
- pname = "Bitmap Heap Scan";
+ pname = sname = "Bitmap Heap Scan";
break;
case T_TidScan:
- pname = "Tid Scan";
+ pname = sname = "Tid Scan";
break;
case T_SubqueryScan:
- pname = "Subquery Scan";
+ pname = sname = "Subquery Scan";
break;
case T_FunctionScan:
- pname = "Function Scan";
+ pname = sname = "Function Scan";
break;
case T_ValuesScan:
- pname = "Values Scan";
+ pname = sname = "Values Scan";
break;
case T_CteScan:
- pname = "CTE Scan";
+ pname = sname = "CTE Scan";
break;
case T_WorkTableScan:
- pname = "WorkTable Scan";
+ pname = sname = "WorkTable Scan";
break;
case T_Material:
- pname = "Materialize";
+ pname = sname = "Materialize";
break;
case T_Sort:
- pname = "Sort";
+ pname = sname = "Sort";
break;
case T_Group:
- pname = "Group";
+ pname = sname = "Group";
break;
case T_Agg:
+ sname = "Aggregate";
switch (((Agg *) plan)->aggstrategy)
{
case AGG_PLAIN:
pname = "Aggregate";
+ strategy = "Plain";
break;
case AGG_SORTED:
pname = "GroupAggregate";
+ strategy = "Sorted";
break;
case AGG_HASHED:
pname = "HashAggregate";
+ strategy = "Hashed";
break;
default:
pname = "Aggregate ???";
+ strategy = "???";
break;
}
break;
case T_WindowAgg:
- pname = "WindowAgg";
+ pname = sname = "WindowAgg";
break;
case T_Unique:
- pname = "Unique";
+ pname = sname = "Unique";
break;
case T_SetOp:
+ sname = "SetOp";
switch (((SetOp *) plan)->strategy)
{
case SETOP_SORTED:
- switch (((SetOp *) plan)->cmd)
- {
- case SETOPCMD_INTERSECT:
- pname = "SetOp Intersect";
- break;
- case SETOPCMD_INTERSECT_ALL:
- pname = "SetOp Intersect All";
- break;
- case SETOPCMD_EXCEPT:
- pname = "SetOp Except";
- break;
- case SETOPCMD_EXCEPT_ALL:
- pname = "SetOp Except All";
- break;
- default:
- pname = "SetOp ???";
- break;
- }
+ pname = "SetOp";
+ strategy = "Sorted";
break;
case SETOP_HASHED:
- switch (((SetOp *) plan)->cmd)
- {
- case SETOPCMD_INTERSECT:
- pname = "HashSetOp Intersect";
- break;
- case SETOPCMD_INTERSECT_ALL:
- pname = "HashSetOp Intersect All";
- break;
- case SETOPCMD_EXCEPT:
- pname = "HashSetOp Except";
- break;
- case SETOPCMD_EXCEPT_ALL:
- pname = "HashSetOp Except All";
- break;
- default:
- pname = "HashSetOp ???";
- break;
- }
+ pname = "HashSetOp";
+ strategy = "Hashed";
break;
default:
pname = "SetOp ???";
+ strategy = "???";
break;
}
break;
case T_Limit:
- pname = "Limit";
+ pname = sname = "Limit";
break;
case T_Hash:
- pname = "Hash";
+ pname = sname = "Hash";
break;
default:
- pname = "???";
+ pname = sname = "???";
break;
}
- appendStringInfoString(es->str, pname);
+ ExplainOpenGroup("Plan",
+ relationship ? NULL : "Plan",
+ true, es);
+
+ if (es->format == EXPLAIN_FORMAT_TEXT)
+ {
+ if (plan_name)
+ {
+ appendStringInfoSpaces(es->str, es->indent * 2);
+ appendStringInfo(es->str, "%s\n", plan_name);
+ es->indent++;
+ }
+ if (es->indent)
+ {
+ appendStringInfoSpaces(es->str, es->indent * 2);
+ appendStringInfoString(es->str, "-> ");
+ es->indent += 2;
+ }
+ appendStringInfoString(es->str, pname);
+ es->indent++;
+ }
+ else
+ {
+ ExplainPropertyText("Node Type", sname, es);
+ if (strategy)
+ ExplainPropertyText("Strategy", strategy, es);
+ if (relationship)
+ ExplainPropertyText("Parent Relationship", relationship, es);
+ if (plan_name)
+ ExplainPropertyText("Subplan Name", plan_name, es);
+ }
+
switch (nodeTag(plan))
{
case T_IndexScan:
- if (ScanDirectionIsBackward(((IndexScan *) plan)->indexorderdir))
- appendStringInfoString(es->str, " Backward");
- appendStringInfo(es->str, " using %s",
- explain_get_index_name(((IndexScan *) plan)->indexid));
+ {
+ IndexScan *indexscan = (IndexScan *) plan;
+ const char *indexname =
+ explain_get_index_name(indexscan->indexid);
+
+ if (es->format == EXPLAIN_FORMAT_TEXT)
+ {
+ if (ScanDirectionIsBackward(indexscan->indexorderdir))
+ appendStringInfoString(es->str, " Backward");
+ appendStringInfo(es->str, " using %s", indexname);
+ }
+ else
+ {
+ const char *scandir;
+
+ switch (indexscan->indexorderdir)
+ {
+ case BackwardScanDirection:
+ scandir = "Backward";
+ break;
+ case NoMovementScanDirection:
+ scandir = "NoMovement";
+ break;
+ case ForwardScanDirection:
+ scandir = "Forward";
+ break;
+ default:
+ scandir = "???";
+ break;
+ }
+ ExplainPropertyText("Scan Direction", scandir, es);
+ ExplainPropertyText("Index Name", indexname, es);
+ }
+ }
/* FALL THRU */
case T_SeqScan:
case T_BitmapHeapScan:
ExplainScanTarget((Scan *) plan, es);
break;
case T_BitmapIndexScan:
- appendStringInfo(es->str, " on %s",
- explain_get_index_name(((BitmapIndexScan *) plan)->indexid));
+ {
+ BitmapIndexScan *bitmapindexscan = (BitmapIndexScan *) plan;
+ const char *indexname =
+ explain_get_index_name(bitmapindexscan->indexid);
+
+ if (es->format == EXPLAIN_FORMAT_TEXT)
+ appendStringInfo(es->str, " on %s", indexname);
+ else
+ ExplainPropertyText("Index Name", indexname, es);
+ }
+ break;
+ case T_NestLoop:
+ case T_MergeJoin:
+ case T_HashJoin:
+ {
+ const char *jointype;
+
+ switch (((Join *) plan)->jointype)
+ {
+ case JOIN_INNER:
+ jointype = "Inner";
+ break;
+ case JOIN_LEFT:
+ jointype = "Left";
+ break;
+ case JOIN_FULL:
+ jointype = "Full";
+ break;
+ case JOIN_RIGHT:
+ jointype = "Right";
+ break;
+ case JOIN_SEMI:
+ jointype = "Semi";
+ break;
+ case JOIN_ANTI:
+ jointype = "Anti";
+ break;
+ default:
+ jointype = "???";
+ break;
+ }
+ if (es->format == EXPLAIN_FORMAT_TEXT)
+ {
+ /*
+ * For historical reasons, the join type is interpolated
+ * into the node type name...
+ */
+ if (((Join *) plan)->jointype != JOIN_INNER)
+ appendStringInfo(es->str, " %s Join", jointype);
+ else if (!IsA(plan, NestLoop))
+ appendStringInfo(es->str, " Join");
+ }
+ else
+ ExplainPropertyText("Join Type", jointype, es);
+ }
+ break;
+ case T_SetOp:
+ {
+ const char *setopcmd;
+
+ switch (((SetOp *) plan)->cmd)
+ {
+ case SETOPCMD_INTERSECT:
+ setopcmd = "Intersect";
+ break;
+ case SETOPCMD_INTERSECT_ALL:
+ setopcmd = "Intersect All";
+ break;
+ case SETOPCMD_EXCEPT:
+ setopcmd = "Except";
+ break;
+ case SETOPCMD_EXCEPT_ALL:
+ setopcmd = "Except All";
+ break;
+ default:
+ setopcmd = "???";
+ break;
+ }
+ if (es->format == EXPLAIN_FORMAT_TEXT)
+ appendStringInfo(es->str, " %s", setopcmd);
+ else
+ ExplainPropertyText("Command", setopcmd, es);
+ }
break;
default:
break;
}
if (es->costs)
- appendStringInfo(es->str, " (cost=%.2f..%.2f rows=%.0f width=%d)",
- plan->startup_cost, plan->total_cost,
- plan->plan_rows, plan->plan_width);
+ {
+ if (es->format == EXPLAIN_FORMAT_TEXT)
+ {
+ appendStringInfo(es->str, " (cost=%.2f..%.2f rows=%.0f width=%d)",
+ plan->startup_cost, plan->total_cost,
+ plan->plan_rows, plan->plan_width);
+ }
+ else
+ {
+ ExplainPropertyFloat("Startup Cost", plan->startup_cost, 2, es);
+ ExplainPropertyFloat("Total Cost", plan->total_cost, 2, es);
+ ExplainPropertyFloat("Plan Rows", plan->plan_rows, 0, es);
+ ExplainPropertyInteger("Plan Width", plan->plan_width, es);
+ }
+ }
/*
* We have to forcibly clean up the instrumentation state because we
if (planstate->instrument && planstate->instrument->nloops > 0)
{
double nloops = planstate->instrument->nloops;
+ double startup_sec = 1000.0 * planstate->instrument->startup / nloops;
+ double total_sec = 1000.0 * planstate->instrument->total / nloops;
+ double rows = planstate->instrument->ntuples / nloops;
- appendStringInfo(es->str,
- " (actual time=%.3f..%.3f rows=%.0f loops=%.0f)",
- 1000.0 * planstate->instrument->startup / nloops,
- 1000.0 * planstate->instrument->total / nloops,
- planstate->instrument->ntuples / nloops,
- planstate->instrument->nloops);
+ if (es->format == EXPLAIN_FORMAT_TEXT)
+ {
+ appendStringInfo(es->str,
+ " (actual time=%.3f..%.3f rows=%.0f loops=%.0f)",
+ startup_sec, total_sec, rows, nloops);
+ }
+ else
+ {
+ ExplainPropertyFloat("Actual Startup Time", startup_sec, 3, es);
+ ExplainPropertyFloat("Actual Total Time", total_sec, 3, es);
+ ExplainPropertyFloat("Actual Rows", rows, 0, es);
+ ExplainPropertyFloat("Actual Loops", nloops, 0, es);
+ }
}
else if (es->analyze)
- appendStringInfoString(es->str, " (never executed)");
- appendStringInfoChar(es->str, '\n');
+ {
+ if (es->format == EXPLAIN_FORMAT_TEXT)
+ appendStringInfo(es->str, " (never executed)");
+ else
+ {
+ ExplainPropertyFloat("Actual Startup Time", 0.0, 3, es);
+ ExplainPropertyFloat("Actual Total Time", 0.0, 3, es);
+ ExplainPropertyFloat("Actual Rows", 0.0, 0, es);
+ ExplainPropertyFloat("Actual Loops", 0.0, 0, es);
+ }
+ }
+
+ /* in text format, first line ends here */
+ if (es->format == EXPLAIN_FORMAT_TEXT)
+ appendStringInfoChar(es->str, '\n');
/* target list */
if (es->verbose)
- show_plan_tlist(plan, indent, es);
+ show_plan_tlist(plan, es);
/* quals, sort keys, etc */
switch (nodeTag(plan))
{
case T_IndexScan:
show_scan_qual(((IndexScan *) plan)->indexqualorig,
- "Index Cond", plan, outer_plan, indent, es);
- show_scan_qual(plan->qual,
- "Filter", plan, outer_plan, indent, es);
+ "Index Cond", plan, outer_plan, es);
+ show_scan_qual(plan->qual, "Filter", plan, outer_plan, es);
break;
case T_BitmapIndexScan:
show_scan_qual(((BitmapIndexScan *) plan)->indexqualorig,
- "Index Cond", plan, outer_plan, indent, es);
+ "Index Cond", plan, outer_plan, es);
break;
case T_BitmapHeapScan:
show_scan_qual(((BitmapHeapScan *) plan)->bitmapqualorig,
- "Recheck Cond", plan, outer_plan, indent, es);
+ "Recheck Cond", plan, outer_plan, es);
/* FALL THRU */
case T_SeqScan:
case T_FunctionScan:
case T_CteScan:
case T_WorkTableScan:
case T_SubqueryScan:
- show_scan_qual(plan->qual,
- "Filter", plan, outer_plan, indent, es);
+ show_scan_qual(plan->qual, "Filter", plan, outer_plan, es);
break;
case T_TidScan:
{
if (list_length(tidquals) > 1)
tidquals = list_make1(make_orclause(tidquals));
- show_scan_qual(tidquals,
- "TID Cond", plan, outer_plan, indent, es);
- show_scan_qual(plan->qual,
- "Filter", plan, outer_plan, indent, es);
+ show_scan_qual(tidquals, "TID Cond", plan, outer_plan, es);
+ show_scan_qual(plan->qual, "Filter", plan, outer_plan, es);
}
break;
case T_NestLoop:
show_upper_qual(((NestLoop *) plan)->join.joinqual,
- "Join Filter", plan, indent, es);
- show_upper_qual(plan->qual, "Filter", plan, indent, es);
+ "Join Filter", plan, es);
+ show_upper_qual(plan->qual, "Filter", plan, es);
break;
case T_MergeJoin:
show_upper_qual(((MergeJoin *) plan)->mergeclauses,
- "Merge Cond", plan, indent, es);
+ "Merge Cond", plan, es);
show_upper_qual(((MergeJoin *) plan)->join.joinqual,
- "Join Filter", plan, indent, es);
- show_upper_qual(plan->qual, "Filter", plan, indent, es);
+ "Join Filter", plan, es);
+ show_upper_qual(plan->qual, "Filter", plan, es);
break;
case T_HashJoin:
show_upper_qual(((HashJoin *) plan)->hashclauses,
- "Hash Cond", plan, indent, es);
+ "Hash Cond", plan, es);
show_upper_qual(((HashJoin *) plan)->join.joinqual,
- "Join Filter", plan, indent, es);
- show_upper_qual(plan->qual, "Filter", plan, indent, es);
+ "Join Filter", plan, es);
+ show_upper_qual(plan->qual, "Filter", plan, es);
break;
case T_Agg:
case T_Group:
- show_upper_qual(plan->qual, "Filter", plan, indent, es);
+ show_upper_qual(plan->qual, "Filter", plan, es);
break;
case T_Sort:
- show_sort_keys(plan, indent, es);
- show_sort_info((SortState *) planstate, indent, es);
+ show_sort_keys(plan, es);
+ show_sort_info((SortState *) planstate, es);
break;
case T_Result:
show_upper_qual((List *) ((Result *) plan)->resconstantqual,
- "One-Time Filter", plan, indent, es);
- show_upper_qual(plan->qual, "Filter", plan, indent, es);
+ "One-Time Filter", plan, es);
+ show_upper_qual(plan->qual, "Filter", plan, es);
break;
default:
break;
}
+ /* Get ready to display the child plans */
+ haschildren = plan->initPlan ||
+ outerPlan(plan) ||
+ innerPlan(plan) ||
+ IsA(plan, Append) ||
+ IsA(plan, BitmapAnd) ||
+ IsA(plan, BitmapOr) ||
+ IsA(plan, SubqueryScan) ||
+ planstate->subPlan;
+ if (haschildren)
+ ExplainOpenGroup("Plans", "Plans", false, es);
+
/* initPlan-s */
if (plan->initPlan)
- ExplainSubPlans(planstate->initPlan, indent, es);
+ ExplainSubPlans(planstate->initPlan, "InitPlan", es);
/* lefttree */
if (outerPlan(plan))
*/
ExplainNode(outerPlan(plan), outerPlanState(planstate),
IsA(plan, BitmapHeapScan) ? outer_plan : NULL,
- indent + 3, es);
+ "Outer", NULL, es);
}
/* righttree */
if (innerPlan(plan))
{
ExplainNode(innerPlan(plan), innerPlanState(planstate),
- outerPlan(plan), indent + 3, es);
+ outerPlan(plan),
+ "Inner", NULL, es);
}
/* special child plans */
case T_Append:
ExplainMemberNodes(((Append *) plan)->appendplans,
((AppendState *) planstate)->appendplans,
- outer_plan, indent, es);
+ outer_plan, es);
break;
case T_BitmapAnd:
ExplainMemberNodes(((BitmapAnd *) plan)->bitmapplans,
((BitmapAndState *) planstate)->bitmapplans,
- outer_plan, indent, es);
+ outer_plan, es);
break;
case T_BitmapOr:
ExplainMemberNodes(((BitmapOr *) plan)->bitmapplans,
((BitmapOrState *) planstate)->bitmapplans,
- outer_plan, indent, es);
+ outer_plan, es);
break;
case T_SubqueryScan:
{
SubqueryScanState *subquerystate = (SubqueryScanState *) planstate;
ExplainNode(subqueryscan->subplan, subquerystate->subplan,
- NULL, indent + 3, es);
+ NULL,
+ "Subquery", NULL, es);
}
break;
default:
/* subPlan-s */
if (planstate->subPlan)
- ExplainSubPlans(planstate->subPlan, indent, es);
+ ExplainSubPlans(planstate->subPlan, "SubPlan", es);
+
+ /* end of child plans */
+ if (haschildren)
+ ExplainCloseGroup("Plans", "Plans", false, es);
+
+ /* in text format, undo whatever indentation we added */
+ if (es->format == EXPLAIN_FORMAT_TEXT)
+ es->indent = save_indent;
+
+ ExplainCloseGroup("Plan",
+ relationship ? NULL : "Plan",
+ true, es);
}
/*
* Show the targetlist of a plan node
*/
static void
-show_plan_tlist(Plan *plan, int indent, ExplainState *es)
+show_plan_tlist(Plan *plan, ExplainState *es)
{
List *context;
+ List *result = NIL;
bool useprefix;
ListCell *lc;
int i;
es->pstmt->subplans);
useprefix = list_length(es->rtable) > 1;
- /* Emit line prefix */
- appendStringInfoSpaces(es->str, indent * 2);
- appendStringInfoString(es->str, " Output: ");
-
/* Deparse each non-junk result column */
i = 0;
foreach(lc, plan->targetlist)
if (tle->resjunk)
continue;
- if (i++ > 0)
- appendStringInfoString(es->str, ", ");
- appendStringInfoString(es->str,
- deparse_expression((Node *) tle->expr, context,
+ result = lappend(result,
+ deparse_expression((Node *) tle->expr, context,
useprefix, false));
}
- appendStringInfoChar(es->str, '\n');
+ /* Print results */
+ ExplainPropertyList("Output", result, es);
}
/*
*/
static void
show_qual(List *qual, const char *qlabel, Plan *plan, Plan *outer_plan,
- int indent, bool useprefix, ExplainState *es)
+ bool useprefix, ExplainState *es)
{
List *context;
Node *node;
exprstr = deparse_expression(node, context, useprefix, false);
/* And add to es->str */
- appendStringInfoSpaces(es->str, indent * 2);
- appendStringInfo(es->str, " %s: %s\n", qlabel, exprstr);
+ ExplainPropertyText(qlabel, exprstr, es);
}
/*
static void
show_scan_qual(List *qual, const char *qlabel,
Plan *scan_plan, Plan *outer_plan,
- int indent, ExplainState *es)
+ ExplainState *es)
{
bool useprefix;
- useprefix = (outer_plan != NULL || IsA(scan_plan, SubqueryScan));
- show_qual(qual, qlabel, scan_plan, outer_plan, indent, useprefix, es);
+ useprefix = (outer_plan != NULL || IsA(scan_plan, SubqueryScan) ||
+ es->verbose);
+ show_qual(qual, qlabel, scan_plan, outer_plan, useprefix, es);
}
/*
* Show a qualifier expression for an upper-level plan node
*/
static void
-show_upper_qual(List *qual, const char *qlabel, Plan *plan,
- int indent, ExplainState *es)
+show_upper_qual(List *qual, const char *qlabel, Plan *plan, ExplainState *es)
{
bool useprefix;
- useprefix = (list_length(es->rtable) > 1);
- show_qual(qual, qlabel, plan, NULL, indent, useprefix, es);
+ useprefix = (list_length(es->rtable) > 1 || es->verbose);
+ show_qual(qual, qlabel, plan, NULL, useprefix, es);
}
/*
* Show the sort keys for a Sort node.
*/
static void
-show_sort_keys(Plan *sortplan, int indent, ExplainState *es)
+show_sort_keys(Plan *sortplan, ExplainState *es)
{
int nkeys = ((Sort *) sortplan)->numCols;
AttrNumber *keycols = ((Sort *) sortplan)->sortColIdx;
List *context;
+ List *result = NIL;
bool useprefix;
int keyno;
char *exprstr;
if (nkeys <= 0)
return;
- appendStringInfoSpaces(es->str, indent * 2);
- appendStringInfoString(es->str, " Sort Key: ");
-
/* Set up deparsing context */
context = deparse_context_for_plan((Node *) sortplan,
NULL,
es->rtable,
es->pstmt->subplans);
- useprefix = list_length(es->rtable) > 1;
+ useprefix = (list_length(es->rtable) > 1 || es->verbose);
for (keyno = 0; keyno < nkeys; keyno++)
{
/* Deparse the expression, showing any top-level cast */
exprstr = deparse_expression((Node *) target->expr, context,
useprefix, true);
- /* And add to es->str */
- if (keyno > 0)
- appendStringInfoString(es->str, ", ");
- appendStringInfoString(es->str, exprstr);
+ result = lappend(result, exprstr);
}
- appendStringInfoChar(es->str, '\n');
+ ExplainPropertyList("Sort Key", result, es);
}
/*
- * If it's EXPLAIN ANALYZE, show tuplesort explain info for a sort node
+ * If it's EXPLAIN ANALYZE, show tuplesort stats for a sort node
*/
static void
-show_sort_info(SortState *sortstate, int indent, ExplainState *es)
+show_sort_info(SortState *sortstate, ExplainState *es)
{
Assert(IsA(sortstate, SortState));
if (es->analyze && sortstate->sort_Done &&
sortstate->tuplesortstate != NULL)
{
- char *sortinfo;
+ Tuplesortstate *state = (Tuplesortstate *) sortstate->tuplesortstate;
+ const char *sortMethod;
+ const char *spaceType;
+ long spaceUsed;
- sortinfo = tuplesort_explain((Tuplesortstate *) sortstate->tuplesortstate);
- appendStringInfoSpaces(es->str, indent * 2);
- appendStringInfo(es->str, " %s\n", sortinfo);
- pfree(sortinfo);
+ tuplesort_get_stats(state, &sortMethod, &spaceType, &spaceUsed);
+
+ if (es->format == EXPLAIN_FORMAT_TEXT)
+ {
+ appendStringInfoSpaces(es->str, es->indent * 2);
+ appendStringInfo(es->str, "Sort Method: %s %s: %ldkB\n",
+ sortMethod, spaceType, spaceUsed);
+ }
+ else
+ {
+ ExplainPropertyText("Sort Method", sortMethod, es);
+ ExplainPropertyLong("Sort Space Used", spaceUsed, es);
+ ExplainPropertyText("Sort Space Type", spaceType, es);
+ }
}
}
ExplainScanTarget(Scan *plan, ExplainState *es)
{
char *objectname = NULL;
+ char *namespace = NULL;
+ const char *objecttag = NULL;
RangeTblEntry *rte;
if (plan->scanrelid <= 0) /* Is this still possible? */
/* Assert it's on a real relation */
Assert(rte->rtekind == RTE_RELATION);
objectname = get_rel_name(rte->relid);
+ if (es->verbose)
+ namespace = get_namespace_name(get_rel_namespace(rte->relid));
+ objecttag = "Relation Name";
break;
case T_FunctionScan:
{
Oid funcid = ((FuncExpr *) funcexpr)->funcid;
objectname = get_func_name(funcid);
+ if (es->verbose)
+ namespace =
+ get_namespace_name(get_func_namespace(funcid));
}
+ objecttag = "Function Name";
}
break;
case T_ValuesScan:
Assert(rte->rtekind == RTE_CTE);
Assert(!rte->self_reference);
objectname = rte->ctename;
+ objecttag = "CTE Name";
break;
case T_WorkTableScan:
/* Assert it's on a self-reference CTE */
Assert(rte->rtekind == RTE_CTE);
Assert(rte->self_reference);
objectname = rte->ctename;
+ objecttag = "CTE Name";
break;
default:
break;
}
- appendStringInfoString(es->str, " on");
- if (objectname != NULL)
- appendStringInfo(es->str, " %s", quote_identifier(objectname));
- if (objectname == NULL || strcmp(rte->eref->aliasname, objectname) != 0)
- appendStringInfo(es->str, " %s",
- quote_identifier(rte->eref->aliasname));
+ if (es->format == EXPLAIN_FORMAT_TEXT)
+ {
+ appendStringInfoString(es->str, " on");
+ if (namespace != NULL)
+ appendStringInfo(es->str, " %s.%s", quote_identifier(namespace),
+ quote_identifier(objectname));
+ else if (objectname != NULL)
+ appendStringInfo(es->str, " %s", quote_identifier(objectname));
+ if (objectname == NULL ||
+ strcmp(rte->eref->aliasname, objectname) != 0)
+ appendStringInfo(es->str, " %s",
+ quote_identifier(rte->eref->aliasname));
+ }
+ else
+ {
+ if (objecttag != NULL && objectname != NULL)
+ ExplainPropertyText(objecttag, objectname, es);
+ if (namespace != NULL)
+ ExplainPropertyText("Schema", namespace, es);
+ ExplainPropertyText("Alias", rte->eref->aliasname, es);
+ }
}
/*
*/
static void
ExplainMemberNodes(List *plans, PlanState **planstate, Plan *outer_plan,
- int indent, ExplainState *es)
+ ExplainState *es)
{
ListCell *lst;
int j = 0;
Plan *subnode = (Plan *) lfirst(lst);
ExplainNode(subnode, planstate[j],
- outer_plan, indent + 3, es);
+ outer_plan,
+ "Member", NULL,
+ es);
j++;
}
}
* Explain a list of SubPlans (or initPlans, which also use SubPlan nodes).
*/
static void
-ExplainSubPlans(List *plans, int indent, ExplainState *es)
+ExplainSubPlans(List *plans, const char *relationship, ExplainState *es)
{
ListCell *lst;
SubPlanState *sps = (SubPlanState *) lfirst(lst);
SubPlan *sp = (SubPlan *) sps->xprstate.expr;
- appendStringInfoSpaces(es->str, indent * 2);
- appendStringInfo(es->str, " %s\n", sp->plan_name);
ExplainNode(exec_subplan_get_plan(es->pstmt, sp),
- sps->planstate, NULL, indent + 4, es);
+ sps->planstate,
+ NULL,
+ relationship, sp->plan_name,
+ es);
+ }
+}
+
+/*
+ * Explain a property, such as sort keys or targets, that takes the form of
+ * a list of unlabeled items. "data" is a list of C strings.
+ */
+static void
+ExplainPropertyList(const char *qlabel, List *data, ExplainState *es)
+{
+ ListCell *lc;
+ bool first = true;
+
+ switch (es->format)
+ {
+ case EXPLAIN_FORMAT_TEXT:
+ appendStringInfoSpaces(es->str, es->indent * 2);
+ appendStringInfo(es->str, "%s: ", qlabel);
+ foreach(lc, data)
+ {
+ if (!first)
+ appendStringInfoString(es->str, ", ");
+ appendStringInfoString(es->str, (const char *) lfirst(lc));
+ first = false;
+ }
+ appendStringInfoChar(es->str, '\n');
+ break;
+
+ case EXPLAIN_FORMAT_XML:
+ ExplainXMLTag(qlabel, X_OPENING, es);
+ foreach(lc, data)
+ {
+ char *str;
+
+ appendStringInfoSpaces(es->str, es->indent * 2 + 2);
+ appendStringInfoString(es->str, "<Item>");
+ str = escape_xml((const char *) lfirst(lc));
+ appendStringInfoString(es->str, str);
+ pfree(str);
+ appendStringInfoString(es->str, "</Item>\n");
+ }
+ ExplainXMLTag(qlabel, X_CLOSING, es);
+ break;
+
+ case EXPLAIN_FORMAT_JSON:
+ ExplainJSONLineEnding(es);
+ appendStringInfoSpaces(es->str, es->indent * 2);
+ escape_json(es->str, qlabel);
+ appendStringInfoString(es->str, ": [");
+ foreach(lc, data)
+ {
+ if (!first)
+ appendStringInfoString(es->str, ", ");
+ escape_json(es->str, (const char *) lfirst(lc));
+ first = false;
+ }
+ appendStringInfoChar(es->str, ']');
+ break;
+ }
+}
+
+/*
+ * Explain a simple property.
+ *
+ * If "numeric" is true, the value is a number (or other value that
+ * doesn't need quoting in JSON).
+ *
+ * This usually should not be invoked directly, but via one of the datatype
+ * specific routines ExplainPropertyText, ExplainPropertyInteger, etc.
+ */
+static void
+ExplainProperty(const char *qlabel, const char *value, bool numeric,
+ ExplainState *es)
+{
+ switch (es->format)
+ {
+ case EXPLAIN_FORMAT_TEXT:
+ appendStringInfoSpaces(es->str, es->indent * 2);
+ appendStringInfo(es->str, "%s: %s\n", qlabel, value);
+ break;
+
+ case EXPLAIN_FORMAT_XML:
+ {
+ char *str;
+
+ appendStringInfoSpaces(es->str, es->indent * 2);
+ ExplainXMLTag(qlabel, X_OPENING | X_NOWHITESPACE, es);
+ str = escape_xml(value);
+ appendStringInfoString(es->str, str);
+ pfree(str);
+ ExplainXMLTag(qlabel, X_CLOSING | X_NOWHITESPACE, es);
+ appendStringInfoChar(es->str, '\n');
+ }
+ break;
+
+ case EXPLAIN_FORMAT_JSON:
+ ExplainJSONLineEnding(es);
+ appendStringInfoSpaces(es->str, es->indent * 2);
+ escape_json(es->str, qlabel);
+ appendStringInfoString(es->str, ": ");
+ if (numeric)
+ appendStringInfoString(es->str, value);
+ else
+ escape_json(es->str, value);
+ break;
+ }
+}
+
+/*
+ * Explain an integer-valued property.
+ */
+static void
+ExplainPropertyInteger(const char *qlabel, int value, ExplainState *es)
+{
+ char buf[32];
+
+ snprintf(buf, sizeof(buf), "%d", value);
+ ExplainProperty(qlabel, buf, true, es);
+}
+
+/*
+ * Explain a long-integer-valued property.
+ */
+static void
+ExplainPropertyLong(const char *qlabel, long value, ExplainState *es)
+{
+ char buf[32];
+
+ snprintf(buf, sizeof(buf), "%ld", value);
+ ExplainProperty(qlabel, buf, true, es);
+}
+
+/*
+ * Explain a float-valued property, using the specified number of
+ * fractional digits.
+ */
+static void
+ExplainPropertyFloat(const char *qlabel, double value, int ndigits,
+ ExplainState *es)
+{
+ char buf[256];
+
+ snprintf(buf, sizeof(buf), "%.*f", ndigits, value);
+ ExplainProperty(qlabel, buf, true, es);
+}
+
+/*
+ * Open a group of related objects.
+ *
+ * objtype is the type of the group object, labelname is its label within
+ * a containing object (if any).
+ *
+ * If labeled is true, the group members will be labeled properties,
+ * while if it's false, they'll be unlabeled objects.
+ */
+static void
+ExplainOpenGroup(const char *objtype, const char *labelname,
+ bool labeled, ExplainState *es)
+{
+ switch (es->format)
+ {
+ case EXPLAIN_FORMAT_TEXT:
+ /* nothing to do */
+ break;
+
+ case EXPLAIN_FORMAT_XML:
+ ExplainXMLTag(objtype, X_OPENING, es);
+ es->indent++;
+ break;
+
+ case EXPLAIN_FORMAT_JSON:
+ ExplainJSONLineEnding(es);
+ appendStringInfoSpaces(es->str, 2 * es->indent);
+ if (labelname)
+ {
+ escape_json(es->str, labelname);
+ appendStringInfoString(es->str, ": ");
+ }
+ appendStringInfoChar(es->str, labeled ? '{' : '[');
+
+ /*
+ * In JSON format, the grouping_stack is an integer list. 0 means
+ * we've emitted nothing at this grouping level, 1 means we've
+ * emitted something (and so the next item needs a comma).
+ * See ExplainJSONLineEnding().
+ */
+ es->grouping_stack = lcons_int(0, es->grouping_stack);
+ es->indent++;
+ break;
+ }
+}
+
+/*
+ * Close a group of related objects.
+ * Parameters must match the corresponding ExplainOpenGroup call.
+ */
+static void
+ExplainCloseGroup(const char *objtype, const char *labelname,
+ bool labeled, ExplainState *es)
+{
+ switch (es->format)
+ {
+ case EXPLAIN_FORMAT_TEXT:
+ /* nothing to do */
+ break;
+
+ case EXPLAIN_FORMAT_XML:
+ es->indent--;
+ ExplainXMLTag(objtype, X_CLOSING, es);
+ break;
+
+ case EXPLAIN_FORMAT_JSON:
+ es->indent--;
+ appendStringInfoChar(es->str, '\n');
+ appendStringInfoSpaces(es->str, 2 * es->indent);
+ appendStringInfoChar(es->str, labeled ? '}' : ']');
+ es->grouping_stack = list_delete_first(es->grouping_stack);
+ break;
+ }
+}
+
+/*
+ * Emit a "dummy" group that never has any members.
+ *
+ * objtype is the type of the group object, labelname is its label within
+ * a containing object (if any).
+ */
+static void
+ExplainDummyGroup(const char *objtype, const char *labelname, ExplainState *es)
+{
+ switch (es->format)
+ {
+ case EXPLAIN_FORMAT_TEXT:
+ /* nothing to do */
+ break;
+
+ case EXPLAIN_FORMAT_XML:
+ ExplainXMLTag(objtype, X_CLOSE_IMMEDIATE, es);
+ break;
+
+ case EXPLAIN_FORMAT_JSON:
+ ExplainJSONLineEnding(es);
+ appendStringInfoSpaces(es->str, 2 * es->indent);
+ if (labelname)
+ {
+ escape_json(es->str, labelname);
+ appendStringInfoString(es->str, ": ");
+ }
+ escape_json(es->str, objtype);
+ break;
+ }
+}
+
+/*
+ * Emit the start-of-output boilerplate.
+ *
+ * This is just enough different from processing a subgroup that we need
+ * a separate pair of subroutines.
+ */
+static void
+ExplainBeginOutput(ExplainState *es)
+{
+ switch (es->format)
+ {
+ case EXPLAIN_FORMAT_TEXT:
+ /* nothing to do */
+ break;
+
+ case EXPLAIN_FORMAT_XML:
+ appendStringInfoString(es->str,
+ "<explain xmlns=\"http://www.postgresql.org/2009/explain\">\n");
+ es->indent++;
+ break;
+
+ case EXPLAIN_FORMAT_JSON:
+ /* top-level structure is an array of plans */
+ appendStringInfoChar(es->str, '[');
+ es->grouping_stack = lcons_int(0, es->grouping_stack);
+ es->indent++;
+ break;
+ }
+}
+
+/*
+ * Emit the end-of-output boilerplate.
+ */
+static void
+ExplainEndOutput(ExplainState *es)
+{
+ switch (es->format)
+ {
+ case EXPLAIN_FORMAT_TEXT:
+ /* nothing to do */
+ break;
+
+ case EXPLAIN_FORMAT_XML:
+ es->indent--;
+ appendStringInfoString(es->str, "</explain>");
+ break;
+
+ case EXPLAIN_FORMAT_JSON:
+ es->indent--;
+ appendStringInfoString(es->str, "\n]");
+ es->grouping_stack = list_delete_first(es->grouping_stack);
+ break;
+ }
+}
+
+/*
+ * Put an appropriate separator between multiple plans
+ */
+void
+ExplainSeparatePlans(ExplainState *es)
+{
+ switch (es->format)
+ {
+ case EXPLAIN_FORMAT_TEXT:
+ /* add a blank line */
+ appendStringInfoChar(es->str, '\n');
+ break;
+
+ case EXPLAIN_FORMAT_XML:
+ /* nothing to do */
+ break;
+
+ case EXPLAIN_FORMAT_JSON:
+ /* must have a comma between array elements */
+ appendStringInfoChar(es->str, ',');
+ break;
+ }
+}
+
+/*
+ * Emit opening or closing XML tag.
+ *
+ * "flags" must contain X_OPENING, X_CLOSING, or X_CLOSE_IMMEDIATE.
+ * Optionally, OR in X_NOWHITESPACE to suppress the whitespace we'd normally
+ * add.
+ *
+ * XML tag names can't contain white space, so we replace any spaces in
+ * "tagname" with dashes.
+ */
+static void
+ExplainXMLTag(const char *tagname, int flags, ExplainState *es)
+{
+ const char *s;
+
+ if ((flags & X_NOWHITESPACE) == 0)
+ appendStringInfoSpaces(es->str, 2 * es->indent);
+ appendStringInfoCharMacro(es->str, '<');
+ if ((flags & X_CLOSING) != 0)
+ appendStringInfoCharMacro(es->str, '/');
+ for (s = tagname; *s; s++)
+ appendStringInfoCharMacro(es->str, (*s == ' ') ? '-' : *s);
+ if ((flags & X_CLOSE_IMMEDIATE) != 0)
+ appendStringInfoString(es->str, " /");
+ appendStringInfoCharMacro(es->str, '>');
+ if ((flags & X_NOWHITESPACE) == 0)
+ appendStringInfoCharMacro(es->str, '\n');
+}
+
+/*
+ * Emit a JSON line ending.
+ *
+ * JSON requires a comma after each property but the last. To facilitate this,
+ * in JSON format, the text emitted for each property begins just prior to the
+ * preceding line-break (and comma, if applicable).
+ */
+static void
+ExplainJSONLineEnding(ExplainState *es)
+{
+ Assert(es->format == EXPLAIN_FORMAT_JSON);
+ if (linitial_int(es->grouping_stack) != 0)
+ appendStringInfoChar(es->str, ',');
+ else
+ linitial_int(es->grouping_stack) = 1;
+ appendStringInfoChar(es->str, '\n');
+}
+
+/*
+ * Produce a JSON string literal, properly escaping characters in the text.
+ */
+static void
+escape_json(StringInfo buf, const char *str)
+{
+ const char *p;
+
+ appendStringInfoCharMacro(buf, '\"');
+ for (p = str; *p; p++)
+ {
+ switch (*p)
+ {
+ case '\b':
+ appendStringInfoString(buf, "\\b");
+ break;
+ case '\f':
+ appendStringInfoString(buf, "\\f");
+ break;
+ case '\n':
+ appendStringInfoString(buf, "\\n");
+ break;
+ case '\r':
+ appendStringInfoString(buf, "\\r");
+ break;
+ case '\t':
+ appendStringInfoString(buf, "\\t");
+ break;
+ case '"':
+ appendStringInfoString(buf, "\\\"");
+ break;
+ case '\\':
+ appendStringInfoString(buf, "\\\\");
+ break;
+ default:
+ if ((unsigned char) *p < ' ')
+ appendStringInfo(buf, "\\u%04x", (int) *p);
+ else
+ appendStringInfoCharMacro(buf, *p);
+ break;
+ }
}
+ appendStringInfoCharMacro(buf, '\"');
}