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