/*-------------------------------------------------------------------------
*
* explain.c
* Explain query execution plans
*
* Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group
* Portions Copyright (c) 1994-5, Regents of the University of California
*
* IDENTIFICATION
* src/backend/commands/explain.c
*
*-------------------------------------------------------------------------
*/
#include "postgres.h"
#include "access/xact.h"
#include "catalog/pg_collation.h"
#include "catalog/pg_type.h"
#include "commands/createas.h"
#include "commands/defrem.h"
#include "commands/prepare.h"
#include "executor/hashjoin.h"
#include "foreign/fdwapi.h"
#include "nodes/extensible.h"
#include "nodes/nodeFuncs.h"
#include "optimizer/clauses.h"
#include "optimizer/planmain.h"
#include "parser/parsetree.h"
#include "rewrite/rewriteHandler.h"
#include "storage/bufmgr.h"
#include "tcop/tcopprot.h"
#include "utils/builtins.h"
#include "utils/json.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
#include "utils/ruleutils.h"
#include "utils/snapmgr.h"
#include "utils/tuplesort.h"
#include "utils/typcache.h"
#include "utils/xml.h"
/* Hook for plugins to get control in ExplainOneQuery() */
ExplainOneQuery_hook_type ExplainOneQuery_hook = NULL;
/* Hook for plugins to get control in explain_get_index_name() */
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, int cursorOptions,
IntoClause *into, ExplainState *es,
const char *queryString, ParamListInfo params);
static void report_triggers(ResultRelInfo *rInfo, bool show_relname,
ExplainState *es);
static double elapsed_time(instr_time *starttime);
static bool ExplainPreScanNode(PlanState *planstate, Bitmapset **rels_used);
static void ExplainNode(PlanState *planstate, List *ancestors,
const char *relationship, const char *plan_name,
ExplainState *es);
static void show_plan_tlist(PlanState *planstate, List *ancestors,
ExplainState *es);
static void show_expression(Node *node, const char *qlabel,
PlanState *planstate, List *ancestors,
bool useprefix, ExplainState *es);
static void show_qual(List *qual, const char *qlabel,
PlanState *planstate, List *ancestors,
bool useprefix, ExplainState *es);
static void show_scan_qual(List *qual, const char *qlabel,
PlanState *planstate, List *ancestors,
ExplainState *es);
static void show_upper_qual(List *qual, const char *qlabel,
PlanState *planstate, List *ancestors,
ExplainState *es);
static void show_sort_keys(SortState *sortstate, List *ancestors,
ExplainState *es);
static void show_merge_append_keys(MergeAppendState *mstate, List *ancestors,
ExplainState *es);
static void show_agg_keys(AggState *astate, List *ancestors,
ExplainState *es);
static void show_grouping_sets(PlanState *planstate, Agg *agg,
List *ancestors, ExplainState *es);
static void show_grouping_set_keys(PlanState *planstate,
Agg *aggnode, Sort *sortnode,
List *context, bool useprefix,
List *ancestors, ExplainState *es);
static void show_group_keys(GroupState *gstate, List *ancestors,
ExplainState *es);
static void show_sort_group_keys(PlanState *planstate, const char *qlabel,
int nkeys, AttrNumber *keycols,
Oid *sortOperators, Oid *collations, bool *nullsFirst,
List *ancestors, ExplainState *es);
static void show_sortorder_options(StringInfo buf, Node *sortexpr,
Oid sortOperator, Oid collation, bool nullsFirst);
static void show_tablesample(TableSampleClause *tsc, PlanState *planstate,
List *ancestors, ExplainState *es);
static void show_sort_info(SortState *sortstate, ExplainState *es);
static void show_hash_info(HashState *hashstate, ExplainState *es);
static void show_tidbitmap_info(BitmapHeapScanState *planstate,
ExplainState *es);
static void show_instrumentation_count(const char *qlabel, int which,
PlanState *planstate, ExplainState *es);
static void show_foreignscan_info(ForeignScanState *fsstate, ExplainState *es);
static const char *explain_get_index_name(Oid indexId);
static void show_buffer_usage(ExplainState *es, const BufferUsage *usage);
static void ExplainIndexScanDetails(Oid indexid, ScanDirection indexorderdir,
ExplainState *es);
static void ExplainScanTarget(Scan *plan, ExplainState *es);
static void ExplainModifyTarget(ModifyTable *plan, ExplainState *es);
static void ExplainTargetRel(Plan *plan, Index rti, ExplainState *es);
static void show_modifytable_info(ModifyTableState *mtstate, List *ancestors,
ExplainState *es);
static void ExplainMemberNodes(List *plans, PlanState **planstates,
List *ancestors, ExplainState *es);
static void ExplainSubPlans(List *plans, List *ancestors,
const char *relationship, ExplainState *es);
static void ExplainCustomChildren(CustomScanState *css,
List *ancestors, ExplainState *es);
static void ExplainProperty(const char *qlabel, const char *value,
bool numeric, 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 ExplainXMLTag(const char *tagname, int flags, ExplainState *es);
static void ExplainJSONLineEnding(ExplainState *es);
static void ExplainYAMLLineStarting(ExplainState *es);
static void escape_yaml(StringInfo buf, const char *str);
/*
* ExplainQuery -
* execute an EXPLAIN command
*/
void
ExplainQuery(ParseState *pstate, ExplainStmt *stmt, const char *queryString,
ParamListInfo params, DestReceiver *dest)
{
ExplainState *es = NewExplainState();
TupOutputState *tstate;
List *rewritten;
ListCell *lc;
bool timing_set = false;
/* Parse options list. */
foreach(lc, stmt->options)
{
DefElem *opt = (DefElem *) lfirst(lc);
if (strcmp(opt->defname, "analyze") == 0)
es->analyze = defGetBoolean(opt);
else if (strcmp(opt->defname, "verbose") == 0)
es->verbose = defGetBoolean(opt);
else if (strcmp(opt->defname, "costs") == 0)
es->costs = defGetBoolean(opt);
else if (strcmp(opt->defname, "buffers") == 0)
es->buffers = defGetBoolean(opt);
else if (strcmp(opt->defname, "timing") == 0)
{
timing_set = true;
es->timing = 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 if (strcmp(p, "yaml") == 0)
es->format = EXPLAIN_FORMAT_YAML;
else
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("unrecognized value for EXPLAIN option \"%s\": \"%s\"",
opt->defname, p),
parser_errposition(pstate, opt->location)));
}
else
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("unrecognized EXPLAIN option \"%s\"",
opt->defname),
parser_errposition(pstate, opt->location)));
}
if (es->buffers && !es->analyze)
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("EXPLAIN option BUFFERS requires ANALYZE")));
/* if the timing was not set explicitly, set default value */
es->timing = (timing_set) ? es->timing : es->analyze;
/* check that timing is used with EXPLAIN ANALYZE */
if (es->timing && !es->analyze)
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("EXPLAIN option TIMING requires ANALYZE")));
/* currently, summary option is not exposed to users; just set it */
es->summary = es->analyze;
/*
* Parse analysis was done already, but we still have to run the rule
* rewriter. We do not do AcquireRewriteLocks: we assume the query either
* came straight from the parser, or suitable locks were acquired by
* plancache.c.
*
* Because the rewriter and planner tend to scribble on the input, we make
* a preliminary copy of the source querytree. This prevents problems in
* the case that the EXPLAIN is in a portal or plpgsql function and is
* executed repeatedly. (See also the same hack in DECLARE CURSOR and
* PREPARE.) XXX FIXME someday.
*/
Assert(IsA(stmt->query, Query));
rewritten = QueryRewrite((Query *) copyObject(stmt->query));
/* emit opening boilerplate */
ExplainBeginOutput(es);
if (rewritten == NIL)
{
/*
* In the case of an INSTEAD NOTHING, tell at least that. But in
* non-text format, the output is delimited, so this isn't necessary.
*/
if (es->format == EXPLAIN_FORMAT_TEXT)
appendStringInfoString(es->str, "Query rewrites to nothing\n");
}
else
{
ListCell *l;
/* Explain every plan */
foreach(l, rewritten)
{
ExplainOneQuery((Query *) lfirst(l),
CURSOR_OPT_PARALLEL_OK, NULL, es,
queryString, params);
/* Separate plans with an appropriate separator */
if (lnext(l) != NULL)
ExplainSeparatePlans(es);
}
}
/* emit closing boilerplate */
ExplainEndOutput(es);
Assert(es->indent == 0);
/* output tuples */
tstate = begin_tup_output_tupdesc(dest, ExplainResultDesc(stmt));
if (es->format == EXPLAIN_FORMAT_TEXT)
do_text_output_multiline(tstate, es->str->data);
else
do_text_output_oneline(tstate, es->str->data);
end_tup_output(tstate);
pfree(es->str->data);
}
/*
* Create a new ExplainState struct initialized with default options.
*/
ExplainState *
NewExplainState(void)
{
ExplainState *es = (ExplainState *) palloc0(sizeof(ExplainState));
/* Set default options (most fields can be left as zeroes). */
es->costs = true;
/* Prepare output buffer. */
es->str = makeStringInfo();
return es;
}
/*
* ExplainResultDesc -
* construct the result tupledesc for an EXPLAIN
*/
TupleDesc
ExplainResultDesc(ExplainStmt *stmt)
{
TupleDesc tupdesc;
ListCell *lc;
Oid result_type = TEXTOID;
/* Check for XML format option */
foreach(lc, stmt->options)
{
DefElem *opt = (DefElem *) lfirst(lc);
if (strcmp(opt->defname, "format") == 0)
{
char *p = defGetString(opt);
if (strcmp(p, "xml") == 0)
result_type = XMLOID;
else if (strcmp(p, "json") == 0)
result_type = JSONOID;
else
result_type = TEXTOID;
/* don't "break", as ExplainQuery will use the last value */
}
}
/* Need a tuple descriptor representing a single TEXT or XML column */
tupdesc = CreateTemplateTupleDesc(1, false);
TupleDescInitEntry(tupdesc, (AttrNumber) 1, "QUERY PLAN",
result_type, -1, 0);
return tupdesc;
}
/*
* ExplainOneQuery -
* print out the execution plan for one Query
*
* "into" is NULL unless we are explaining the contents of a CreateTableAsStmt.
*/
static void
ExplainOneQuery(Query *query, int cursorOptions,
IntoClause *into, ExplainState *es,
const char *queryString, ParamListInfo params)
{
/* planner will not cope with utility statements */
if (query->commandType == CMD_UTILITY)
{
ExplainOneUtility(query->utilityStmt, into, es, queryString, params);
return;
}
/* if an advisor plugin is present, let it manage things */
if (ExplainOneQuery_hook)
(*ExplainOneQuery_hook) (query, cursorOptions, into, es,
queryString, params);
else
{
PlannedStmt *plan;
instr_time planstart,
planduration;
INSTR_TIME_SET_CURRENT(planstart);
/* plan the query */
plan = pg_plan_query(query, cursorOptions, params);
INSTR_TIME_SET_CURRENT(planduration);
INSTR_TIME_SUBTRACT(planduration, planstart);
/* run it (if needed) and produce output */
ExplainOnePlan(plan, into, es, queryString, params, &planduration);
}
}
/*
* ExplainOneUtility -
* print out the execution plan for one utility statement
* (In general, utility statements don't have plans, but there are some
* we treat as special cases)
*
* "into" is NULL unless we are explaining the contents of a CreateTableAsStmt.
*
* This is exported because it's called back from prepare.c in the
* EXPLAIN EXECUTE case.
*/
void
ExplainOneUtility(Node *utilityStmt, IntoClause *into, ExplainState *es,
const char *queryString, ParamListInfo params)
{
if (utilityStmt == NULL)
return;
if (IsA(utilityStmt, CreateTableAsStmt))
{
/*
* We have to rewrite the contained SELECT and then pass it back to
* ExplainOneQuery. It's probably not really necessary to copy the
* contained parsetree another time, but let's be safe.
*
* Like ExecCreateTableAs, disallow parallelism in the plan.
*/
CreateTableAsStmt *ctas = (CreateTableAsStmt *) utilityStmt;
List *rewritten;
Assert(IsA(ctas->query, Query));
rewritten = QueryRewrite((Query *) copyObject(ctas->query));
Assert(list_length(rewritten) == 1);
ExplainOneQuery((Query *) linitial(rewritten),
0, ctas->into, es,
queryString, params);
}
else if (IsA(utilityStmt, DeclareCursorStmt))
{
/*
* Likewise for DECLARE CURSOR.
*
* Notice that if you say EXPLAIN ANALYZE DECLARE CURSOR then we'll
* actually run the query. This is different from pre-8.3 behavior
* but seems more useful than not running the query. No cursor will
* be created, however.
*/
DeclareCursorStmt *dcs = (DeclareCursorStmt *) utilityStmt;
List *rewritten;
Assert(IsA(dcs->query, Query));
rewritten = QueryRewrite((Query *) copyObject(dcs->query));
Assert(list_length(rewritten) == 1);
ExplainOneQuery((Query *) linitial(rewritten),
dcs->options, NULL, es,
queryString, params);
}
else if (IsA(utilityStmt, ExecuteStmt))
ExplainExecuteQuery((ExecuteStmt *) utilityStmt, into, es,
queryString, params);
else if (IsA(utilityStmt, NotifyStmt))
{
if (es->format == EXPLAIN_FORMAT_TEXT)
appendStringInfoString(es->str, "NOTIFY\n");
else
ExplainDummyGroup("Notify", NULL, es);
}
else
{
if (es->format == EXPLAIN_FORMAT_TEXT)
appendStringInfoString(es->str,
"Utility statements have no plan structure\n");
else
ExplainDummyGroup("Utility Statement", NULL, es);
}
}
/*
* ExplainOnePlan -
* given a planned query, execute it if needed, and then print
* EXPLAIN output
*
* "into" is NULL unless we are explaining the contents of a CreateTableAsStmt,
* in which case executing the query should result in creating that table.
*
* This is exported because it's called back from prepare.c in the
* EXPLAIN EXECUTE case, and because an index advisor plugin would need
* to call it.
*/
void
ExplainOnePlan(PlannedStmt *plannedstmt, IntoClause *into, ExplainState *es,
const char *queryString, ParamListInfo params,
const instr_time *planduration)
{
DestReceiver *dest;
QueryDesc *queryDesc;
instr_time starttime;
double totaltime = 0;
int eflags;
int instrument_option = 0;
Assert(plannedstmt->commandType != CMD_UTILITY);
if (es->analyze && es->timing)
instrument_option |= INSTRUMENT_TIMER;
else if (es->analyze)
instrument_option |= INSTRUMENT_ROWS;
if (es->buffers)
instrument_option |= INSTRUMENT_BUFFERS;
/*
* We always collect timing for the entire statement, even when node-level
* timing is off, so we don't look at es->timing here. (We could skip
* this if !es->summary, but it's hardly worth the complication.)
*/
INSTR_TIME_SET_CURRENT(starttime);
/*
* Use a snapshot with an updated command ID to ensure this query sees
* results of any previously executed queries.
*/
PushCopiedSnapshot(GetActiveSnapshot());
UpdateActiveSnapshotCommandId();
/*
* Normally we discard the query's output, but if explaining CREATE TABLE
* AS, we'd better use the appropriate tuple receiver.
*/
if (into)
dest = CreateIntoRelDestReceiver(into);
else
dest = None_Receiver;
/* Create a QueryDesc for the query */
queryDesc = CreateQueryDesc(plannedstmt, queryString,
GetActiveSnapshot(), InvalidSnapshot,
dest, params, instrument_option);
/* Select execution options */
if (es->analyze)
eflags = 0; /* default run-to-completion flags */
else
eflags = EXEC_FLAG_EXPLAIN_ONLY;
if (into)
eflags |= GetIntoRelEFlags(into);
/* call ExecutorStart to prepare the plan for execution */
ExecutorStart(queryDesc, eflags);
/* Execute the plan for statistics if asked for */
if (es->analyze)
{
ScanDirection dir;
/* EXPLAIN ANALYZE CREATE TABLE AS WITH NO DATA is weird */
if (into && into->skipData)
dir = NoMovementScanDirection;
else
dir = ForwardScanDirection;
/* run the plan */
ExecutorRun(queryDesc, dir, 0L);
/* run cleanup too */
ExecutorFinish(queryDesc);
/* We can't run ExecutorEnd 'till we're done printing the stats... */
totaltime += elapsed_time(&starttime);
}
ExplainOpenGroup("Query", NULL, true, es);
/* Create textual dump of plan tree */
ExplainPrintPlan(es, queryDesc);
if (es->summary && planduration)
{
double plantime = INSTR_TIME_GET_DOUBLE(*planduration);
if (es->format == EXPLAIN_FORMAT_TEXT)
appendStringInfo(es->str, "Planning time: %.3f ms\n",
1000.0 * plantime);
else
ExplainPropertyFloat("Planning Time", 1000.0 * plantime, 3, es);
}
/* Print info about runtime of triggers */
if (es->analyze)
ExplainPrintTriggers(es, queryDesc);
/*
* Close down the query and free resources. Include time for this in the
* total execution time (although it should be pretty minimal).
*/
INSTR_TIME_SET_CURRENT(starttime);
ExecutorEnd(queryDesc);
FreeQueryDesc(queryDesc);
PopActiveSnapshot();
/* We need a CCI just in case query expanded to multiple plans */
if (es->analyze)
CommandCounterIncrement();
totaltime += elapsed_time(&starttime);
if (es->summary)
{
if (es->format == EXPLAIN_FORMAT_TEXT)
appendStringInfo(es->str, "Execution time: %.3f ms\n",
1000.0 * totaltime);
else
ExplainPropertyFloat("Execution Time", 1000.0 * totaltime,
3, es);
}
ExplainCloseGroup("Query", NULL, true, es);
}
/*
* ExplainPrintPlan -
* convert a QueryDesc's plan tree to text and append it to es->str
*
* The caller should have set up the options fields of *es, as well as
* initializing the output buffer es->str. Also, output formatting state
* such as the indent level is assumed valid. Plan-tree-specific fields
* in *es are initialized here.
*
* NB: will not work on utility statements
*/
void
ExplainPrintPlan(ExplainState *es, QueryDesc *queryDesc)
{
Bitmapset *rels_used = NULL;
PlanState *ps;
/* Set up ExplainState fields associated with this plan tree */
Assert(queryDesc->plannedstmt != NULL);
es->pstmt = queryDesc->plannedstmt;
es->rtable = queryDesc->plannedstmt->rtable;
ExplainPreScanNode(queryDesc->planstate, &rels_used);
es->rtable_names = select_rtable_names_for_explain(es->rtable, rels_used);
es->deparse_cxt = deparse_context_for_plan_rtable(es->rtable,
es->rtable_names);
es->printed_subplans = NULL;
/*
* Sometimes we mark a Gather node as "invisible", which means that it's
* not displayed in EXPLAIN output. The purpose of this is to allow
* running regression tests with force_parallel_mode=regress to get the
* same results as running the same tests with force_parallel_mode=off.
*/
ps = queryDesc->planstate;
if (IsA(ps, GatherState) &&((Gather *) ps->plan)->invisible)
ps = outerPlanState(ps);
ExplainNode(ps, NIL, NULL, NULL, es);
}
/*
* ExplainPrintTriggers -
* convert a QueryDesc's trigger statistics to text and append it to
* es->str
*
* The caller should have set up the options fields of *es, as well as
* initializing the output buffer es->str. Other fields in *es are
* initialized here.
*/
void
ExplainPrintTriggers(ExplainState *es, QueryDesc *queryDesc)
{
ResultRelInfo *rInfo;
bool show_relname;
int numrels = queryDesc->estate->es_num_result_relations;
List *targrels = queryDesc->estate->es_trig_target_relations;
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);
foreach(l, targrels)
{
rInfo = (ResultRelInfo *) lfirst(l);
report_triggers(rInfo, show_relname, es);
}
ExplainCloseGroup("Triggers", "Triggers", false, es);
}
/*
* ExplainQueryText -
* add a "Query Text" node that contains the actual text of the query
*
* The caller should have set up the options fields of *es, as well as
* initializing the output buffer es->str.
*
*/
void
ExplainQueryText(ExplainState *es, QueryDesc *queryDesc)
{
if (queryDesc->sourceText)
ExplainPropertyText("Query Text", queryDesc->sourceText, es);
}
/*
* report_triggers -
* report execution stats for a single relation's triggers
*/
static void
report_triggers(ResultRelInfo *rInfo, bool show_relname, ExplainState *es)
{
int nt;
if (!rInfo->ri_TrigDesc || !rInfo->ri_TrigInstrument)
return;
for (nt = 0; nt < rInfo->ri_TrigDesc->numtriggers; nt++)
{
Trigger *trig = rInfo->ri_TrigDesc->triggers + nt;
Instrumentation *instr = rInfo->ri_TrigInstrument + nt;
char *relname;
char *conname = NULL;
/* Must clean up instrumentation state */
InstrEndLoop(instr);
/*
* We ignore triggers that were never invoked; they likely aren't
* relevant to the current query type.
*/
if (instr->ntuples == 0)
continue;
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)
{
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);
if (es->timing)
appendStringInfo(es->str, ": time=%.3f calls=%.0f\n",
1000.0 * instr->total, instr->ntuples);
else
appendStringInfo(es->str, ": calls=%.0f\n", instr->ntuples);
}
else
{
ExplainPropertyText("Trigger Name", trig->tgname, es);
if (conname)
ExplainPropertyText("Constraint Name", conname, es);
ExplainPropertyText("Relation", relname, es);
if (es->timing)
ExplainPropertyFloat("Time", 1000.0 * instr->total, 3, es);
ExplainPropertyFloat("Calls", instr->ntuples, 0, es);
}
if (conname)
pfree(conname);
ExplainCloseGroup("Trigger", NULL, true, es);
}
}
/* Compute elapsed time in seconds since given timestamp */
static double
elapsed_time(instr_time *starttime)
{
instr_time endtime;
INSTR_TIME_SET_CURRENT(endtime);
INSTR_TIME_SUBTRACT(endtime, *starttime);
return INSTR_TIME_GET_DOUBLE(endtime);
}
/*
* ExplainPreScanNode -
* Prescan the planstate tree to identify which RTEs are referenced
*
* Adds the relid of each referenced RTE to *rels_used. The result controls
* which RTEs are assigned aliases by select_rtable_names_for_explain.
* This ensures that we don't confusingly assign un-suffixed aliases to RTEs
* that never appear in the EXPLAIN output (such as inheritance parents).
*/
static bool
ExplainPreScanNode(PlanState *planstate, Bitmapset **rels_used)
{
Plan *plan = planstate->plan;
switch (nodeTag(plan))
{
case T_SeqScan:
case T_SampleScan:
case T_IndexScan:
case T_IndexOnlyScan:
case T_BitmapHeapScan:
case T_TidScan:
case T_SubqueryScan:
case T_FunctionScan:
case T_ValuesScan:
case T_CteScan:
case T_WorkTableScan:
*rels_used = bms_add_member(*rels_used,
((Scan *) plan)->scanrelid);
break;
case T_ForeignScan:
*rels_used = bms_add_members(*rels_used,
((ForeignScan *) plan)->fs_relids);
break;
case T_CustomScan:
*rels_used = bms_add_members(*rels_used,
((CustomScan *) plan)->custom_relids);
break;
case T_ModifyTable:
*rels_used = bms_add_member(*rels_used,
((ModifyTable *) plan)->nominalRelation);
if (((ModifyTable *) plan)->exclRelRTI)
*rels_used = bms_add_member(*rels_used,
((ModifyTable *) plan)->exclRelRTI);
break;
default:
break;
}
return planstate_tree_walker(planstate, ExplainPreScanNode, rels_used);
}
/*
* ExplainNode -
* Appends a description of a plan tree to es->str
*
* planstate points to the executor state node for the current plan node.
* We need to work from a PlanState node, not just a Plan node, in order to
* get at the instrumentation data (if any) as well as the list of subplans.
*
* ancestors is a list of parent PlanState nodes, most-closely-nested first.
* These are needed in order to interpret PARAM_EXEC Params.
*
* 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(PlanState *planstate, List *ancestors,
const char *relationship, const char *plan_name,
ExplainState *es)
{
Plan *plan = planstate->plan;
const char *pname; /* node type name for text output */
const char *sname; /* node type name for non-text output */
const char *strategy = NULL;
const char *partialmode = NULL;
const char *operation = NULL;
const char *custom_name = NULL;
int save_indent = es->indent;
bool haschildren;
switch (nodeTag(plan))
{
case T_Result:
pname = sname = "Result";
break;
case T_ProjectSet:
pname = sname = "ProjectSet";
break;
case T_ModifyTable:
sname = "ModifyTable";
switch (((ModifyTable *) plan)->operation)
{
case CMD_INSERT:
pname = operation = "Insert";
break;
case CMD_UPDATE:
pname = operation = "Update";
break;
case CMD_DELETE:
pname = operation = "Delete";
break;
default:
pname = "???";
break;
}
break;
case T_Append:
pname = sname = "Append";
break;
case T_MergeAppend:
pname = sname = "Merge Append";
break;
case T_RecursiveUnion:
pname = sname = "Recursive Union";
break;
case T_BitmapAnd:
pname = sname = "BitmapAnd";
break;
case T_BitmapOr:
pname = sname = "BitmapOr";
break;
case T_NestLoop:
pname = sname = "Nested Loop";
break;
case T_MergeJoin:
pname = "Merge"; /* "Join" gets added by jointype switch */
sname = "Merge Join";
break;
case T_HashJoin:
pname = "Hash"; /* "Join" gets added by jointype switch */
sname = "Hash Join";
break;
case T_SeqScan:
pname = sname = "Seq Scan";
break;
case T_SampleScan:
pname = sname = "Sample Scan";
break;
case T_Gather:
pname = sname = "Gather";
break;
case T_IndexScan:
pname = sname = "Index Scan";
break;
case T_IndexOnlyScan:
pname = sname = "Index Only Scan";
break;
case T_BitmapIndexScan:
pname = sname = "Bitmap Index Scan";
break;
case T_BitmapHeapScan:
pname = sname = "Bitmap Heap Scan";
break;
case T_TidScan:
pname = sname = "Tid Scan";
break;
case T_SubqueryScan:
pname = sname = "Subquery Scan";
break;
case T_FunctionScan:
pname = sname = "Function Scan";
break;
case T_ValuesScan:
pname = sname = "Values Scan";
break;
case T_CteScan:
pname = sname = "CTE Scan";
break;
case T_WorkTableScan:
pname = sname = "WorkTable Scan";
break;
case T_ForeignScan:
sname = "Foreign Scan";
switch (((ForeignScan *) plan)->operation)
{
case CMD_SELECT:
pname = "Foreign Scan";
operation = "Select";
break;
case CMD_INSERT:
pname = "Foreign Insert";
operation = "Insert";
break;
case CMD_UPDATE:
pname = "Foreign Update";
operation = "Update";
break;
case CMD_DELETE:
pname = "Foreign Delete";
operation = "Delete";
break;
default:
pname = "???";
break;
}
break;
case T_CustomScan:
sname = "Custom Scan";
custom_name = ((CustomScan *) plan)->methods->CustomName;
if (custom_name)
pname = psprintf("Custom Scan (%s)", custom_name);
else
pname = sname;
break;
case T_Material:
pname = sname = "Materialize";
break;
case T_Sort:
pname = sname = "Sort";
break;
case T_Group:
pname = sname = "Group";
break;
case T_Agg:
{
Agg *agg = (Agg *) plan;
sname = "Aggregate";
switch (agg->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;
}
if (DO_AGGSPLIT_SKIPFINAL(agg->aggsplit))
{
partialmode = "Partial";
pname = psprintf("%s %s", partialmode, pname);
}
else if (DO_AGGSPLIT_COMBINE(agg->aggsplit))
{
partialmode = "Finalize";
pname = psprintf("%s %s", partialmode, pname);
}
else
partialmode = "Simple";
}
break;
case T_WindowAgg:
pname = sname = "WindowAgg";
break;
case T_Unique:
pname = sname = "Unique";
break;
case T_SetOp:
sname = "SetOp";
switch (((SetOp *) plan)->strategy)
{
case SETOP_SORTED:
pname = "SetOp";
strategy = "Sorted";
break;
case SETOP_HASHED:
pname = "HashSetOp";
strategy = "Hashed";
break;
default:
pname = "SetOp ???";
strategy = "???";
break;
}
break;
case T_LockRows:
pname = sname = "LockRows";
break;
case T_Limit:
pname = sname = "Limit";
break;
case T_Hash:
pname = sname = "Hash";
break;
default:
pname = sname = "???";
break;
}
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;
}
if (plan->parallel_aware)
appendStringInfoString(es->str, "Parallel ");
appendStringInfoString(es->str, pname);
es->indent++;
}
else
{
ExplainPropertyText("Node Type", sname, es);
if (strategy)
ExplainPropertyText("Strategy", strategy, es);
if (partialmode)
ExplainPropertyText("Partial Mode", partialmode, es);
if (operation)
ExplainPropertyText("Operation", operation, es);
if (relationship)
ExplainPropertyText("Parent Relationship", relationship, es);
if (plan_name)
ExplainPropertyText("Subplan Name", plan_name, es);
if (custom_name)
ExplainPropertyText("Custom Plan Provider", custom_name, es);
ExplainPropertyBool("Parallel Aware", plan->parallel_aware, es);
}
switch (nodeTag(plan))
{
case T_SeqScan:
case T_SampleScan:
case T_BitmapHeapScan:
case T_TidScan:
case T_SubqueryScan:
case T_FunctionScan:
case T_ValuesScan:
case T_CteScan:
case T_WorkTableScan:
ExplainScanTarget((Scan *) plan, es);
break;
case T_ForeignScan:
case T_CustomScan:
if (((Scan *) plan)->scanrelid > 0)
ExplainScanTarget((Scan *) plan, es);
break;
case T_IndexScan:
{
IndexScan *indexscan = (IndexScan *) plan;
ExplainIndexScanDetails(indexscan->indexid,
indexscan->indexorderdir,
es);
ExplainScanTarget((Scan *) indexscan, es);
}
break;
case T_IndexOnlyScan:
{
IndexOnlyScan *indexonlyscan = (IndexOnlyScan *) plan;
ExplainIndexScanDetails(indexonlyscan->indexid,
indexonlyscan->indexorderdir,
es);
ExplainScanTarget((Scan *) indexonlyscan, es);
}
break;
case T_BitmapIndexScan:
{
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_ModifyTable:
ExplainModifyTarget((ModifyTable *) plan, 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))
appendStringInfoString(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)
{
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
* haven't done ExecutorEnd yet. This is pretty grotty ...
*
* Note: contrib/auto_explain could cause instrumentation to be set up
* even though we didn't ask for it here. Be careful not to print any
* instrumentation results the user didn't ask for. But we do the
* InstrEndLoop call anyway, if possible, to reduce the number of cases
* auto_explain has to contend with.
*/
if (planstate->instrument)
InstrEndLoop(planstate->instrument);
if (es->analyze &&
planstate->instrument && planstate->instrument->nloops > 0)
{
double nloops = planstate->instrument->nloops;
double startup_sec = 1000.0 * planstate->instrument->startup / nloops;
double total_sec = 1000.0 * planstate->instrument->total / nloops;
double rows = planstate->instrument->ntuples / nloops;
if (es->format == EXPLAIN_FORMAT_TEXT)
{
if (es->timing)
appendStringInfo(es->str,
" (actual time=%.3f..%.3f rows=%.0f loops=%.0f)",
startup_sec, total_sec, rows, nloops);
else
appendStringInfo(es->str,
" (actual rows=%.0f loops=%.0f)",
rows, nloops);
}
else
{
if (es->timing)
{
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)
{
if (es->format == EXPLAIN_FORMAT_TEXT)
appendStringInfoString(es->str, " (never executed)");
else
{
if (es->timing)
{
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(planstate, ancestors, es);
/* quals, sort keys, etc */
switch (nodeTag(plan))
{
case T_IndexScan:
show_scan_qual(((IndexScan *) plan)->indexqualorig,
"Index Cond", planstate, ancestors, es);
if (((IndexScan *) plan)->indexqualorig)
show_instrumentation_count("Rows Removed by Index Recheck", 2,
planstate, es);
show_scan_qual(((IndexScan *) plan)->indexorderbyorig,
"Order By", planstate, ancestors, es);
show_scan_qual(plan->qual, "Filter", planstate, ancestors, es);
if (plan->qual)
show_instrumentation_count("Rows Removed by Filter", 1,
planstate, es);
break;
case T_IndexOnlyScan:
show_scan_qual(((IndexOnlyScan *) plan)->indexqual,
"Index Cond", planstate, ancestors, es);
if (((IndexOnlyScan *) plan)->indexqual)
show_instrumentation_count("Rows Removed by Index Recheck", 2,
planstate, es);
show_scan_qual(((IndexOnlyScan *) plan)->indexorderby,
"Order By", planstate, ancestors, es);
show_scan_qual(plan->qual, "Filter", planstate, ancestors, es);
if (plan->qual)
show_instrumentation_count("Rows Removed by Filter", 1,
planstate, es);
if (es->analyze)
ExplainPropertyLong("Heap Fetches",
((IndexOnlyScanState *) planstate)->ioss_HeapFetches, es);
break;
case T_BitmapIndexScan:
show_scan_qual(((BitmapIndexScan *) plan)->indexqualorig,
"Index Cond", planstate, ancestors, es);
break;
case T_BitmapHeapScan:
show_scan_qual(((BitmapHeapScan *) plan)->bitmapqualorig,
"Recheck Cond", planstate, ancestors, es);
if (((BitmapHeapScan *) plan)->bitmapqualorig)
show_instrumentation_count("Rows Removed by Index Recheck", 2,
planstate, es);
show_scan_qual(plan->qual, "Filter", planstate, ancestors, es);
if (plan->qual)
show_instrumentation_count("Rows Removed by Filter", 1,
planstate, es);
if (es->analyze)
show_tidbitmap_info((BitmapHeapScanState *) planstate, es);
break;
case T_SampleScan:
show_tablesample(((SampleScan *) plan)->tablesample,
planstate, ancestors, es);
/* FALL THRU to print additional fields the same as SeqScan */
case T_SeqScan:
case T_ValuesScan:
case T_CteScan:
case T_WorkTableScan:
case T_SubqueryScan:
show_scan_qual(plan->qual, "Filter", planstate, ancestors, es);
if (plan->qual)
show_instrumentation_count("Rows Removed by Filter", 1,
planstate, es);
break;
case T_Gather:
{
Gather *gather = (Gather *) plan;
show_scan_qual(plan->qual, "Filter", planstate, ancestors, es);
if (plan->qual)
show_instrumentation_count("Rows Removed by Filter", 1,
planstate, es);
ExplainPropertyInteger("Workers Planned",
gather->num_workers, es);
if (es->analyze)
{
int nworkers;
nworkers = ((GatherState *) planstate)->nworkers_launched;
ExplainPropertyInteger("Workers Launched",
nworkers, es);
}
if (gather->single_copy || es->format != EXPLAIN_FORMAT_TEXT)
ExplainPropertyBool("Single Copy", gather->single_copy, es);
}
break;
case T_FunctionScan:
if (es->verbose)
{
List *fexprs = NIL;
ListCell *lc;
foreach(lc, ((FunctionScan *) plan)->functions)
{
RangeTblFunction *rtfunc = (RangeTblFunction *) lfirst(lc);
fexprs = lappend(fexprs, rtfunc->funcexpr);
}
/* We rely on show_expression to insert commas as needed */
show_expression((Node *) fexprs,
"Function Call", planstate, ancestors,
es->verbose, es);
}
show_scan_qual(plan->qual, "Filter", planstate, ancestors, es);
if (plan->qual)
show_instrumentation_count("Rows Removed by Filter", 1,
planstate, es);
break;
case T_TidScan:
{
/*
* The tidquals list has OR semantics, so be sure to show it
* as an OR condition.
*/
List *tidquals = ((TidScan *) plan)->tidquals;
if (list_length(tidquals) > 1)
tidquals = list_make1(make_orclause(tidquals));
show_scan_qual(tidquals, "TID Cond", planstate, ancestors, es);
show_scan_qual(plan->qual, "Filter", planstate, ancestors, es);
if (plan->qual)
show_instrumentation_count("Rows Removed by Filter", 1,
planstate, es);
}
break;
case T_ForeignScan:
show_scan_qual(plan->qual, "Filter", planstate, ancestors, es);
if (plan->qual)
show_instrumentation_count("Rows Removed by Filter", 1,
planstate, es);
show_foreignscan_info((ForeignScanState *) planstate, es);
break;
case T_CustomScan:
{
CustomScanState *css = (CustomScanState *) planstate;
show_scan_qual(plan->qual, "Filter", planstate, ancestors, es);
if (plan->qual)
show_instrumentation_count("Rows Removed by Filter", 1,
planstate, es);
if (css->methods->ExplainCustomScan)
css->methods->ExplainCustomScan(css, ancestors, es);
}
break;
case T_NestLoop:
show_upper_qual(((NestLoop *) plan)->join.joinqual,
"Join Filter", planstate, ancestors, es);
if (((NestLoop *) plan)->join.joinqual)
show_instrumentation_count("Rows Removed by Join Filter", 1,
planstate, es);
show_upper_qual(plan->qual, "Filter", planstate, ancestors, es);
if (plan->qual)
show_instrumentation_count("Rows Removed by Filter", 2,
planstate, es);
break;
case T_MergeJoin:
show_upper_qual(((MergeJoin *) plan)->mergeclauses,
"Merge Cond", planstate, ancestors, es);
show_upper_qual(((MergeJoin *) plan)->join.joinqual,
"Join Filter", planstate, ancestors, es);
if (((MergeJoin *) plan)->join.joinqual)
show_instrumentation_count("Rows Removed by Join Filter", 1,
planstate, es);
show_upper_qual(plan->qual, "Filter", planstate, ancestors, es);
if (plan->qual)
show_instrumentation_count("Rows Removed by Filter", 2,
planstate, es);
break;
case T_HashJoin:
show_upper_qual(((HashJoin *) plan)->hashclauses,
"Hash Cond", planstate, ancestors, es);
show_upper_qual(((HashJoin *) plan)->join.joinqual,
"Join Filter", planstate, ancestors, es);
if (((HashJoin *) plan)->join.joinqual)
show_instrumentation_count("Rows Removed by Join Filter", 1,
planstate, es);
show_upper_qual(plan->qual, "Filter", planstate, ancestors, es);
if (plan->qual)
show_instrumentation_count("Rows Removed by Filter", 2,
planstate, es);
break;
case T_Agg:
show_agg_keys((AggState *) planstate, ancestors, es);
show_upper_qual(plan->qual, "Filter", planstate, ancestors, es);
if (plan->qual)
show_instrumentation_count("Rows Removed by Filter", 1,
planstate, es);
break;
case T_Group:
show_group_keys((GroupState *) planstate, ancestors, es);
show_upper_qual(plan->qual, "Filter", planstate, ancestors, es);
if (plan->qual)
show_instrumentation_count("Rows Removed by Filter", 1,
planstate, es);
break;
case T_Sort:
show_sort_keys((SortState *) planstate, ancestors, es);
show_sort_info((SortState *) planstate, es);
break;
case T_MergeAppend:
show_merge_append_keys((MergeAppendState *) planstate,
ancestors, es);
break;
case T_Result:
show_upper_qual((List *) ((Result *) plan)->resconstantqual,
"One-Time Filter", planstate, ancestors, es);
show_upper_qual(plan->qual, "Filter", planstate, ancestors, es);
if (plan->qual)
show_instrumentation_count("Rows Removed by Filter", 1,
planstate, es);
break;
case T_ModifyTable:
show_modifytable_info((ModifyTableState *) planstate, ancestors,
es);
break;
case T_Hash:
show_hash_info((HashState *) planstate, es);
break;
default:
break;
}
/* Show buffer usage */
if (es->buffers && planstate->instrument)
show_buffer_usage(es, &planstate->instrument->bufusage);
/* Show worker detail */
if (es->analyze && es->verbose && planstate->worker_instrument)
{
WorkerInstrumentation *w = planstate->worker_instrument;
bool opened_group = false;
int n;
for (n = 0; n < w->num_workers; ++n)
{
Instrumentation *instrument = &w->instrument[n];
double nloops = instrument->nloops;
double startup_sec;
double total_sec;
double rows;
if (nloops <= 0)
continue;
startup_sec = 1000.0 * instrument->startup / nloops;
total_sec = 1000.0 * instrument->total / nloops;
rows = instrument->ntuples / nloops;
if (es->format == EXPLAIN_FORMAT_TEXT)
{
appendStringInfoSpaces(es->str, es->indent * 2);
appendStringInfo(es->str, "Worker %d: ", n);
if (es->timing)
appendStringInfo(es->str,
"actual time=%.3f..%.3f rows=%.0f loops=%.0f\n",
startup_sec, total_sec, rows, nloops);
else
appendStringInfo(es->str,
"actual rows=%.0f loops=%.0f\n",
rows, nloops);
es->indent++;
if (es->buffers)
show_buffer_usage(es, &instrument->bufusage);
es->indent--;
}
else
{
if (!opened_group)
{
ExplainOpenGroup("Workers", "Workers", false, es);
opened_group = true;
}
ExplainOpenGroup("Worker", NULL, true, es);
ExplainPropertyInteger("Worker Number", n, es);
if (es->timing)
{
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);
if (es->buffers)
show_buffer_usage(es, &instrument->bufusage);
ExplainCloseGroup("Worker", NULL, true, es);
}
}
if (opened_group)
ExplainCloseGroup("Workers", "Workers", false, es);
}
/* Get ready to display the child plans */
haschildren = planstate->initPlan ||
outerPlanState(planstate) ||
innerPlanState(planstate) ||
IsA(plan, ModifyTable) ||
IsA(plan, Append) ||
IsA(plan, MergeAppend) ||
IsA(plan, BitmapAnd) ||
IsA(plan, BitmapOr) ||
IsA(plan, SubqueryScan) ||
(IsA(planstate, CustomScanState) &&
((CustomScanState *) planstate)->custom_ps != NIL) ||
planstate->subPlan;
if (haschildren)
{
ExplainOpenGroup("Plans", "Plans", false, es);
/* Pass current PlanState as head of ancestors list for children */
ancestors = lcons(planstate, ancestors);
}
/* initPlan-s */
if (planstate->initPlan)
ExplainSubPlans(planstate->initPlan, ancestors, "InitPlan", es);
/* lefttree */
if (outerPlanState(planstate))
ExplainNode(outerPlanState(planstate), ancestors,
"Outer", NULL, es);
/* righttree */
if (innerPlanState(planstate))
ExplainNode(innerPlanState(planstate), ancestors,
"Inner", NULL, es);
/* special child plans */
switch (nodeTag(plan))
{
case T_ModifyTable:
ExplainMemberNodes(((ModifyTable *) plan)->plans,
((ModifyTableState *) planstate)->mt_plans,
ancestors, es);
break;
case T_Append:
ExplainMemberNodes(((Append *) plan)->appendplans,
((AppendState *) planstate)->appendplans,
ancestors, es);
break;
case T_MergeAppend:
ExplainMemberNodes(((MergeAppend *) plan)->mergeplans,
((MergeAppendState *) planstate)->mergeplans,
ancestors, es);
break;
case T_BitmapAnd:
ExplainMemberNodes(((BitmapAnd *) plan)->bitmapplans,
((BitmapAndState *) planstate)->bitmapplans,
ancestors, es);
break;
case T_BitmapOr:
ExplainMemberNodes(((BitmapOr *) plan)->bitmapplans,
((BitmapOrState *) planstate)->bitmapplans,
ancestors, es);
break;
case T_SubqueryScan:
ExplainNode(((SubqueryScanState *) planstate)->subplan, ancestors,
"Subquery", NULL, es);
break;
case T_CustomScan:
ExplainCustomChildren((CustomScanState *) planstate,
ancestors, es);
break;
default:
break;
}
/* subPlan-s */
if (planstate->subPlan)
ExplainSubPlans(planstate->subPlan, ancestors, "SubPlan", es);
/* end of child plans */
if (haschildren)
{
ancestors = list_delete_first(ancestors);
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(PlanState *planstate, List *ancestors, ExplainState *es)
{
Plan *plan = planstate->plan;
List *context;
List *result = NIL;
bool useprefix;
ListCell *lc;
/* No work if empty tlist (this occurs eg in bitmap indexscans) */
if (plan->targetlist == NIL)
return;
/* The tlist of an Append isn't real helpful, so suppress it */
if (IsA(plan, Append))
return;
/* Likewise for MergeAppend and RecursiveUnion */
if (IsA(plan, MergeAppend))
return;
if (IsA(plan, RecursiveUnion))
return;
/*
* Likewise for ForeignScan that executes a direct INSERT/UPDATE/DELETE
*
* Note: the tlist for a ForeignScan that executes a direct INSERT/UPDATE
* might contain subplan output expressions that are confusing in this
* context. The tlist for a ForeignScan that executes a direct UPDATE/
* DELETE always contains "junk" target columns to identify the exact row
* to update or delete, which would be confusing in this context. So, we
* suppress it in all the cases.
*/
if (IsA(plan, ForeignScan) &&
((ForeignScan *) plan)->operation != CMD_SELECT)
return;
/* Set up deparsing context */
context = set_deparse_context_planstate(es->deparse_cxt,
(Node *) planstate,
ancestors);
useprefix = list_length(es->rtable) > 1;
/* Deparse each result column (we now include resjunk ones) */
foreach(lc, plan->targetlist)
{
TargetEntry *tle = (TargetEntry *) lfirst(lc);
result = lappend(result,
deparse_expression((Node *) tle->expr, context,
useprefix, false));
}
/* Print results */
ExplainPropertyList("Output", result, es);
}
/*
* Show a generic expression
*/
static void
show_expression(Node *node, const char *qlabel,
PlanState *planstate, List *ancestors,
bool useprefix, ExplainState *es)
{
List *context;
char *exprstr;
/* Set up deparsing context */
context = set_deparse_context_planstate(es->deparse_cxt,
(Node *) planstate,
ancestors);
/* Deparse the expression */
exprstr = deparse_expression(node, context, useprefix, false);
/* And add to es->str */
ExplainPropertyText(qlabel, exprstr, es);
}
/*
* Show a qualifier expression (which is a List with implicit AND semantics)
*/
static void
show_qual(List *qual, const char *qlabel,
PlanState *planstate, List *ancestors,
bool useprefix, ExplainState *es)
{
Node *node;
/* No work if empty qual */
if (qual == NIL)
return;
/* Convert AND list to explicit AND */
node = (Node *) make_ands_explicit(qual);
/* And show it */
show_expression(node, qlabel, planstate, ancestors, useprefix, es);
}
/*
* Show a qualifier expression for a scan plan node
*/
static void
show_scan_qual(List *qual, const char *qlabel,
PlanState *planstate, List *ancestors,
ExplainState *es)
{
bool useprefix;
useprefix = (IsA(planstate->plan, SubqueryScan) ||es->verbose);
show_qual(qual, qlabel, planstate, ancestors, useprefix, es);
}
/*
* Show a qualifier expression for an upper-level plan node
*/
static void
show_upper_qual(List *qual, const char *qlabel,
PlanState *planstate, List *ancestors,
ExplainState *es)
{
bool useprefix;
useprefix = (list_length(es->rtable) > 1 || es->verbose);
show_qual(qual, qlabel, planstate, ancestors, useprefix, es);
}
/*
* Show the sort keys for a Sort node.
*/
static void
show_sort_keys(SortState *sortstate, List *ancestors, ExplainState *es)
{
Sort *plan = (Sort *) sortstate->ss.ps.plan;
show_sort_group_keys((PlanState *) sortstate, "Sort Key",
plan->numCols, plan->sortColIdx,
plan->sortOperators, plan->collations,
plan->nullsFirst,
ancestors, es);
}
/*
* Likewise, for a MergeAppend node.
*/
static void
show_merge_append_keys(MergeAppendState *mstate, List *ancestors,
ExplainState *es)
{
MergeAppend *plan = (MergeAppend *) mstate->ps.plan;
show_sort_group_keys((PlanState *) mstate, "Sort Key",
plan->numCols, plan->sortColIdx,
plan->sortOperators, plan->collations,
plan->nullsFirst,
ancestors, es);
}
/*
* Show the grouping keys for an Agg node.
*/
static void
show_agg_keys(AggState *astate, List *ancestors,
ExplainState *es)
{
Agg *plan = (Agg *) astate->ss.ps.plan;
if (plan->numCols > 0 || plan->groupingSets)
{
/* The key columns refer to the tlist of the child plan */
ancestors = lcons(astate, ancestors);
if (plan->groupingSets)
show_grouping_sets(outerPlanState(astate), plan, ancestors, es);
else
show_sort_group_keys(outerPlanState(astate), "Group Key",
plan->numCols, plan->grpColIdx,
NULL, NULL, NULL,
ancestors, es);
ancestors = list_delete_first(ancestors);
}
}
static void
show_grouping_sets(PlanState *planstate, Agg *agg,
List *ancestors, ExplainState *es)
{
List *context;
bool useprefix;
ListCell *lc;
/* Set up deparsing context */
context = set_deparse_context_planstate(es->deparse_cxt,
(Node *) planstate,
ancestors);
useprefix = (list_length(es->rtable) > 1 || es->verbose);
ExplainOpenGroup("Grouping Sets", "Grouping Sets", false, es);
show_grouping_set_keys(planstate, agg, NULL,
context, useprefix, ancestors, es);
foreach(lc, agg->chain)
{
Agg *aggnode = lfirst(lc);
Sort *sortnode = (Sort *) aggnode->plan.lefttree;
show_grouping_set_keys(planstate, aggnode, sortnode,
context, useprefix, ancestors, es);
}
ExplainCloseGroup("Grouping Sets", "Grouping Sets", false, es);
}
static void
show_grouping_set_keys(PlanState *planstate,
Agg *aggnode, Sort *sortnode,
List *context, bool useprefix,
List *ancestors, ExplainState *es)
{
Plan *plan = planstate->plan;
char *exprstr;
ListCell *lc;
List *gsets = aggnode->groupingSets;
AttrNumber *keycols = aggnode->grpColIdx;
ExplainOpenGroup("Grouping Set", NULL, true, es);
if (sortnode)
{
show_sort_group_keys(planstate, "Sort Key",
sortnode->numCols, sortnode->sortColIdx,
sortnode->sortOperators, sortnode->collations,
sortnode->nullsFirst,
ancestors, es);
if (es->format == EXPLAIN_FORMAT_TEXT)
es->indent++;
}
ExplainOpenGroup("Group Keys", "Group Keys", false, es);
foreach(lc, gsets)
{
List *result = NIL;
ListCell *lc2;
foreach(lc2, (List *) lfirst(lc))
{
Index i = lfirst_int(lc2);
AttrNumber keyresno = keycols[i];
TargetEntry *target = get_tle_by_resno(plan->targetlist,
keyresno);
if (!target)
elog(ERROR, "no tlist entry for key %d", keyresno);
/* Deparse the expression, showing any top-level cast */
exprstr = deparse_expression((Node *) target->expr, context,
useprefix, true);
result = lappend(result, exprstr);
}
if (!result && es->format == EXPLAIN_FORMAT_TEXT)
ExplainPropertyText("Group Key", "()", es);
else
ExplainPropertyListNested("Group Key", result, es);
}
ExplainCloseGroup("Group Keys", "Group Keys", false, es);
if (sortnode && es->format == EXPLAIN_FORMAT_TEXT)
es->indent--;
ExplainCloseGroup("Grouping Set", NULL, true, es);
}
/*
* Show the grouping keys for a Group node.
*/
static void
show_group_keys(GroupState *gstate, List *ancestors,
ExplainState *es)
{
Group *plan = (Group *) gstate->ss.ps.plan;
/* The key columns refer to the tlist of the child plan */
ancestors = lcons(gstate, ancestors);
show_sort_group_keys(outerPlanState(gstate), "Group Key",
plan->numCols, plan->grpColIdx,
NULL, NULL, NULL,
ancestors, es);
ancestors = list_delete_first(ancestors);
}
/*
* Common code to show sort/group keys, which are represented in plan nodes
* as arrays of targetlist indexes. If it's a sort key rather than a group
* key, also pass sort operators/collations/nullsFirst arrays.
*/
static void
show_sort_group_keys(PlanState *planstate, const char *qlabel,
int nkeys, AttrNumber *keycols,
Oid *sortOperators, Oid *collations, bool *nullsFirst,
List *ancestors, ExplainState *es)
{
Plan *plan = planstate->plan;
List *context;
List *result = NIL;
StringInfoData sortkeybuf;
bool useprefix;
int keyno;
if (nkeys <= 0)
return;
initStringInfo(&sortkeybuf);
/* Set up deparsing context */
context = set_deparse_context_planstate(es->deparse_cxt,
(Node *) planstate,
ancestors);
useprefix = (list_length(es->rtable) > 1 || es->verbose);
for (keyno = 0; keyno < nkeys; keyno++)
{
/* find key expression in tlist */
AttrNumber keyresno = keycols[keyno];
TargetEntry *target = get_tle_by_resno(plan->targetlist,
keyresno);
char *exprstr;
if (!target)
elog(ERROR, "no tlist entry for key %d", keyresno);
/* Deparse the expression, showing any top-level cast */
exprstr = deparse_expression((Node *) target->expr, context,
useprefix, true);
resetStringInfo(&sortkeybuf);
appendStringInfoString(&sortkeybuf, exprstr);
/* Append sort order information, if relevant */
if (sortOperators != NULL)
show_sortorder_options(&sortkeybuf,
(Node *) target->expr,
sortOperators[keyno],
collations[keyno],
nullsFirst[keyno]);
/* Emit one property-list item per sort key */
result = lappend(result, pstrdup(sortkeybuf.data));
}
ExplainPropertyList(qlabel, result, es);
}
/*
* Append nondefault characteristics of the sort ordering of a column to buf
* (collation, direction, NULLS FIRST/LAST)
*/
static void
show_sortorder_options(StringInfo buf, Node *sortexpr,
Oid sortOperator, Oid collation, bool nullsFirst)
{
Oid sortcoltype = exprType(sortexpr);
bool reverse = false;
TypeCacheEntry *typentry;
typentry = lookup_type_cache(sortcoltype,
TYPECACHE_LT_OPR | TYPECACHE_GT_OPR);
/*
* Print COLLATE if it's not default. There are some cases where this is
* redundant, eg if expression is a column whose declared collation is
* that collation, but it's hard to distinguish that here.
*/
if (OidIsValid(collation) && collation != DEFAULT_COLLATION_OID)
{
char *collname = get_collation_name(collation);
if (collname == NULL)
elog(ERROR, "cache lookup failed for collation %u", collation);
appendStringInfo(buf, " COLLATE %s", quote_identifier(collname));
}
/* Print direction if not ASC, or USING if non-default sort operator */
if (sortOperator == typentry->gt_opr)
{
appendStringInfoString(buf, " DESC");
reverse = true;
}
else if (sortOperator != typentry->lt_opr)
{
char *opname = get_opname(sortOperator);
if (opname == NULL)
elog(ERROR, "cache lookup failed for operator %u", sortOperator);
appendStringInfo(buf, " USING %s", opname);
/* Determine whether operator would be considered ASC or DESC */
(void) get_equality_op_for_ordering_op(sortOperator, &reverse);
}
/* Add NULLS FIRST/LAST only if it wouldn't be default */
if (nullsFirst && !reverse)
{
appendStringInfoString(buf, " NULLS FIRST");
}
else if (!nullsFirst && reverse)
{
appendStringInfoString(buf, " NULLS LAST");
}
}
/*
* Show TABLESAMPLE properties
*/
static void
show_tablesample(TableSampleClause *tsc, PlanState *planstate,
List *ancestors, ExplainState *es)
{
List *context;
bool useprefix;
char *method_name;
List *params = NIL;
char *repeatable;
ListCell *lc;
/* Set up deparsing context */
context = set_deparse_context_planstate(es->deparse_cxt,
(Node *) planstate,
ancestors);
useprefix = list_length(es->rtable) > 1;
/* Get the tablesample method name */
method_name = get_func_name(tsc->tsmhandler);
/* Deparse parameter expressions */
foreach(lc, tsc->args)
{
Node *arg = (Node *) lfirst(lc);
params = lappend(params,
deparse_expression(arg, context,
useprefix, false));
}
if (tsc->repeatable)
repeatable = deparse_expression((Node *) tsc->repeatable, context,
useprefix, false);
else
repeatable = NULL;
/* Print results */
if (es->format == EXPLAIN_FORMAT_TEXT)
{
bool first = true;
appendStringInfoSpaces(es->str, es->indent * 2);
appendStringInfo(es->str, "Sampling: %s (", method_name);
foreach(lc, params)
{
if (!first)
appendStringInfoString(es->str, ", ");
appendStringInfoString(es->str, (const char *) lfirst(lc));
first = false;
}
appendStringInfoChar(es->str, ')');
if (repeatable)
appendStringInfo(es->str, " REPEATABLE (%s)", repeatable);
appendStringInfoChar(es->str, '\n');
}
else
{
ExplainPropertyText("Sampling Method", method_name, es);
ExplainPropertyList("Sampling Parameters", params, es);
if (repeatable)
ExplainPropertyText("Repeatable Seed", repeatable, es);
}
}
/*
* If it's EXPLAIN ANALYZE, show tuplesort stats for a sort node
*/
static void
show_sort_info(SortState *sortstate, ExplainState *es)
{
Assert(IsA(sortstate, SortState));
if (es->analyze && sortstate->sort_Done &&
sortstate->tuplesortstate != NULL)
{
Tuplesortstate *state = (Tuplesortstate *) sortstate->tuplesortstate;
const char *sortMethod;
const char *spaceType;
long spaceUsed;
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);
}
}
}
/*
* Show information on hash buckets/batches.
*/
static void
show_hash_info(HashState *hashstate, ExplainState *es)
{
HashJoinTable hashtable;
Assert(IsA(hashstate, HashState));
hashtable = hashstate->hashtable;
if (hashtable)
{
long spacePeakKb = (hashtable->spacePeak + 1023) / 1024;
if (es->format != EXPLAIN_FORMAT_TEXT)
{
ExplainPropertyLong("Hash Buckets", hashtable->nbuckets, es);
ExplainPropertyLong("Original Hash Buckets",
hashtable->nbuckets_original, es);
ExplainPropertyLong("Hash Batches", hashtable->nbatch, es);
ExplainPropertyLong("Original Hash Batches",
hashtable->nbatch_original, es);
ExplainPropertyLong("Peak Memory Usage", spacePeakKb, es);
}
else if (hashtable->nbatch_original != hashtable->nbatch ||
hashtable->nbuckets_original != hashtable->nbuckets)
{
appendStringInfoSpaces(es->str, es->indent * 2);
appendStringInfo(es->str,
"Buckets: %d (originally %d) Batches: %d (originally %d) Memory Usage: %ldkB\n",
hashtable->nbuckets,
hashtable->nbuckets_original,
hashtable->nbatch,
hashtable->nbatch_original,
spacePeakKb);
}
else
{
appendStringInfoSpaces(es->str, es->indent * 2);
appendStringInfo(es->str,
"Buckets: %d Batches: %d Memory Usage: %ldkB\n",
hashtable->nbuckets, hashtable->nbatch,
spacePeakKb);
}
}
}
/*
* If it's EXPLAIN ANALYZE, show exact/lossy pages for a BitmapHeapScan node
*/
static void
show_tidbitmap_info(BitmapHeapScanState *planstate, ExplainState *es)
{
if (es->format != EXPLAIN_FORMAT_TEXT)
{
ExplainPropertyLong("Exact Heap Blocks", planstate->exact_pages, es);
ExplainPropertyLong("Lossy Heap Blocks", planstate->lossy_pages, es);
}
else
{
if (planstate->exact_pages > 0 || planstate->lossy_pages > 0)
{
appendStringInfoSpaces(es->str, es->indent * 2);
appendStringInfoString(es->str, "Heap Blocks:");
if (planstate->exact_pages > 0)
appendStringInfo(es->str, " exact=%ld", planstate->exact_pages);
if (planstate->lossy_pages > 0)
appendStringInfo(es->str, " lossy=%ld", planstate->lossy_pages);
appendStringInfoChar(es->str, '\n');
}
}
}
/*
* If it's EXPLAIN ANALYZE, show instrumentation information for a plan node
*
* "which" identifies which instrumentation counter to print
*/
static void
show_instrumentation_count(const char *qlabel, int which,
PlanState *planstate, ExplainState *es)
{
double nfiltered;
double nloops;
if (!es->analyze || !planstate->instrument)
return;
if (which == 2)
nfiltered = planstate->instrument->nfiltered2;
else
nfiltered = planstate->instrument->nfiltered1;
nloops = planstate->instrument->nloops;
/* In text mode, suppress zero counts; they're not interesting enough */
if (nfiltered > 0 || es->format != EXPLAIN_FORMAT_TEXT)
{
if (nloops > 0)
ExplainPropertyFloat(qlabel, nfiltered / nloops, 0, es);
else
ExplainPropertyFloat(qlabel, 0.0, 0, es);
}
}
/*
* Show extra information for a ForeignScan node.
*/
static void
show_foreignscan_info(ForeignScanState *fsstate, ExplainState *es)
{
FdwRoutine *fdwroutine = fsstate->fdwroutine;
/* Let the FDW emit whatever fields it wants */
if (((ForeignScan *) fsstate->ss.ps.plan)->operation != CMD_SELECT)
{
if (fdwroutine->ExplainDirectModify != NULL)
fdwroutine->ExplainDirectModify(fsstate, es);
}
else
{
if (fdwroutine->ExplainForeignScan != NULL)
fdwroutine->ExplainForeignScan(fsstate, es);
}
}
/*
* Fetch the name of an index in an EXPLAIN
*
* We allow plugins to get control here so that plans involving hypothetical
* indexes can be explained.
*/
static const char *
explain_get_index_name(Oid indexId)
{
const char *result;
if (explain_get_index_name_hook)
result = (*explain_get_index_name_hook) (indexId);
else
result = NULL;
if (result == NULL)
{
/* default behavior: look in the catalogs and quote it */
result = get_rel_name(indexId);
if (result == NULL)
elog(ERROR, "cache lookup failed for index %u", indexId);
result = quote_identifier(result);
}
return result;
}
/*
* Show buffer usage details.
*/
static void
show_buffer_usage(ExplainState *es, const BufferUsage *usage)
{
if (es->format == EXPLAIN_FORMAT_TEXT)
{
bool has_shared = (usage->shared_blks_hit > 0 ||
usage->shared_blks_read > 0 ||
usage->shared_blks_dirtied > 0 ||
usage->shared_blks_written > 0);
bool has_local = (usage->local_blks_hit > 0 ||
usage->local_blks_read > 0 ||
usage->local_blks_dirtied > 0 ||
usage->local_blks_written > 0);
bool has_temp = (usage->temp_blks_read > 0 ||
usage->temp_blks_written > 0);
bool has_timing = (!INSTR_TIME_IS_ZERO(usage->blk_read_time) ||
!INSTR_TIME_IS_ZERO(usage->blk_write_time));
/* Show only positive counter values. */
if (has_shared || has_local || has_temp)
{
appendStringInfoSpaces(es->str, es->indent * 2);
appendStringInfoString(es->str, "Buffers:");
if (has_shared)
{
appendStringInfoString(es->str, " shared");
if (usage->shared_blks_hit > 0)
appendStringInfo(es->str, " hit=%ld",
usage->shared_blks_hit);
if (usage->shared_blks_read > 0)
appendStringInfo(es->str, " read=%ld",
usage->shared_blks_read);
if (usage->shared_blks_dirtied > 0)
appendStringInfo(es->str, " dirtied=%ld",
usage->shared_blks_dirtied);
if (usage->shared_blks_written > 0)
appendStringInfo(es->str, " written=%ld",
usage->shared_blks_written);
if (has_local || has_temp)
appendStringInfoChar(es->str, ',');
}
if (has_local)
{
appendStringInfoString(es->str, " local");
if (usage->local_blks_hit > 0)
appendStringInfo(es->str, " hit=%ld",
usage->local_blks_hit);
if (usage->local_blks_read > 0)
appendStringInfo(es->str, " read=%ld",
usage->local_blks_read);
if (usage->local_blks_dirtied > 0)
appendStringInfo(es->str, " dirtied=%ld",
usage->local_blks_dirtied);
if (usage->local_blks_written > 0)
appendStringInfo(es->str, " written=%ld",
usage->local_blks_written);
if (has_temp)
appendStringInfoChar(es->str, ',');
}
if (has_temp)
{
appendStringInfoString(es->str, " temp");
if (usage->temp_blks_read > 0)
appendStringInfo(es->str, " read=%ld",
usage->temp_blks_read);
if (usage->temp_blks_written > 0)
appendStringInfo(es->str, " written=%ld",
usage->temp_blks_written);
}
appendStringInfoChar(es->str, '\n');
}
/* As above, show only positive counter values. */
if (has_timing)
{
appendStringInfoSpaces(es->str, es->indent * 2);
appendStringInfoString(es->str, "I/O Timings:");
if (!INSTR_TIME_IS_ZERO(usage->blk_read_time))
appendStringInfo(es->str, " read=%0.3f",
INSTR_TIME_GET_MILLISEC(usage->blk_read_time));
if (!INSTR_TIME_IS_ZERO(usage->blk_write_time))
appendStringInfo(es->str, " write=%0.3f",
INSTR_TIME_GET_MILLISEC(usage->blk_write_time));
appendStringInfoChar(es->str, '\n');
}
}
else
{
ExplainPropertyLong("Shared Hit Blocks", usage->shared_blks_hit, es);
ExplainPropertyLong("Shared Read Blocks", usage->shared_blks_read, es);
ExplainPropertyLong("Shared Dirtied Blocks", usage->shared_blks_dirtied, es);
ExplainPropertyLong("Shared Written Blocks", usage->shared_blks_written, es);
ExplainPropertyLong("Local Hit Blocks", usage->local_blks_hit, es);
ExplainPropertyLong("Local Read Blocks", usage->local_blks_read, es);
ExplainPropertyLong("Local Dirtied Blocks", usage->local_blks_dirtied, es);
ExplainPropertyLong("Local Written Blocks", usage->local_blks_written, es);
ExplainPropertyLong("Temp Read Blocks", usage->temp_blks_read, es);
ExplainPropertyLong("Temp Written Blocks", usage->temp_blks_written, es);
if (track_io_timing)
{
ExplainPropertyFloat("I/O Read Time", INSTR_TIME_GET_MILLISEC(usage->blk_read_time), 3, es);
ExplainPropertyFloat("I/O Write Time", INSTR_TIME_GET_MILLISEC(usage->blk_write_time), 3, es);
}
}
}
/*
* Add some additional details about an IndexScan or IndexOnlyScan
*/
static void
ExplainIndexScanDetails(Oid indexid, ScanDirection indexorderdir,
ExplainState *es)
{
const char *indexname = explain_get_index_name(indexid);
if (es->format == EXPLAIN_FORMAT_TEXT)
{
if (ScanDirectionIsBackward(indexorderdir))
appendStringInfoString(es->str, " Backward");
appendStringInfo(es->str, " using %s", indexname);
}
else
{
const char *scandir;
switch (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);
}
}
/*
* Show the target of a Scan node
*/
static void
ExplainScanTarget(Scan *plan, ExplainState *es)
{
ExplainTargetRel((Plan *) plan, plan->scanrelid, es);
}
/*
* Show the target of a ModifyTable node
*
* Here we show the nominal target (ie, the relation that was named in the
* original query). If the actual target(s) is/are different, we'll show them
* in show_modifytable_info().
*/
static void
ExplainModifyTarget(ModifyTable *plan, ExplainState *es)
{
ExplainTargetRel((Plan *) plan, plan->nominalRelation, es);
}
/*
* Show the target relation of a scan or modify node
*/
static void
ExplainTargetRel(Plan *plan, Index rti, ExplainState *es)
{
char *objectname = NULL;
char *namespace = NULL;
const char *objecttag = NULL;
RangeTblEntry *rte;
char *refname;
rte = rt_fetch(rti, es->rtable);
refname = (char *) list_nth(es->rtable_names, rti - 1);
if (refname == NULL)
refname = rte->eref->aliasname;
switch (nodeTag(plan))
{
case T_SeqScan:
case T_SampleScan:
case T_IndexScan:
case T_IndexOnlyScan:
case T_BitmapHeapScan:
case T_TidScan:
case T_ForeignScan:
case T_CustomScan:
case T_ModifyTable:
/* 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:
{
FunctionScan *fscan = (FunctionScan *) plan;
/* Assert it's on a RangeFunction */
Assert(rte->rtekind == RTE_FUNCTION);
/*
* If the expression is still a function call of a single
* function, we can get the real name of the function.
* Otherwise, punt. (Even if it was a single function call
* originally, the optimizer could have simplified it away.)
*/
if (list_length(fscan->functions) == 1)
{
RangeTblFunction *rtfunc = (RangeTblFunction *) linitial(fscan->functions);
if (IsA(rtfunc->funcexpr, FuncExpr))
{
FuncExpr *funcexpr = (FuncExpr *) rtfunc->funcexpr;
Oid funcid = 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_VALUES);
break;
case T_CteScan:
/* Assert it's on a non-self-reference CTE */
Assert(rte->rtekind == RTE_CTE);
Assert(!rte->self_reference);
objectname = rte->ctename;
objecttag = "CTE Name";
break;
case T_WorkTableScan:
/* Assert it's on a self-reference CTE */
Assert(rte->rtekind == RTE_CTE);
Assert(rte->self_reference);
objectname = rte->ctename;
objecttag = "CTE Name";
break;
default:
break;
}
if (es->format == EXPLAIN_FORMAT_TEXT)
{
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(refname, objectname) != 0)
appendStringInfo(es->str, " %s", quote_identifier(refname));
}
else
{
if (objecttag != NULL && objectname != NULL)
ExplainPropertyText(objecttag, objectname, es);
if (namespace != NULL)
ExplainPropertyText("Schema", namespace, es);
ExplainPropertyText("Alias", refname, es);
}
}
/*
* Show extra information for a ModifyTable node
*
* We have three objectives here. First, if there's more than one target
* table or it's different from the nominal target, identify the actual
* target(s). Second, give FDWs a chance to display extra info about foreign
* targets. Third, show information about ON CONFLICT.
*/
static void
show_modifytable_info(ModifyTableState *mtstate, List *ancestors,
ExplainState *es)
{
ModifyTable *node = (ModifyTable *) mtstate->ps.plan;
const char *operation;
const char *foperation;
bool labeltargets;
int j;
List *idxNames = NIL;
ListCell *lst;
switch (node->operation)
{
case CMD_INSERT:
operation = "Insert";
foperation = "Foreign Insert";
break;
case CMD_UPDATE:
operation = "Update";
foperation = "Foreign Update";
break;
case CMD_DELETE:
operation = "Delete";
foperation = "Foreign Delete";
break;
default:
operation = "???";
foperation = "Foreign ???";
break;
}
/* Should we explicitly label target relations? */
labeltargets = (mtstate->mt_nplans > 1 ||
(mtstate->mt_nplans == 1 &&
mtstate->resultRelInfo->ri_RangeTableIndex != node->nominalRelation));
if (labeltargets)
ExplainOpenGroup("Target Tables", "Target Tables", false, es);
for (j = 0; j < mtstate->mt_nplans; j++)
{
ResultRelInfo *resultRelInfo = mtstate->resultRelInfo + j;
FdwRoutine *fdwroutine = resultRelInfo->ri_FdwRoutine;
if (labeltargets)
{
/* Open a group for this target */
ExplainOpenGroup("Target Table", NULL, true, es);
/*
* In text mode, decorate each target with operation type, so that
* ExplainTargetRel's output of " on foo" will read nicely.
*/
if (es->format == EXPLAIN_FORMAT_TEXT)
{
appendStringInfoSpaces(es->str, es->indent * 2);
appendStringInfoString(es->str,
fdwroutine ? foperation : operation);
}
/* Identify target */
ExplainTargetRel((Plan *) node,
resultRelInfo->ri_RangeTableIndex,
es);
if (es->format == EXPLAIN_FORMAT_TEXT)
{
appendStringInfoChar(es->str, '\n');
es->indent++;
}
}
/* Give FDW a chance if needed */
if (!resultRelInfo->ri_usesFdwDirectModify &&
fdwroutine != NULL &&
fdwroutine->ExplainForeignModify != NULL)
{
List *fdw_private = (List *) list_nth(node->fdwPrivLists, j);
fdwroutine->ExplainForeignModify(mtstate,
resultRelInfo,
fdw_private,
j,
es);
}
if (labeltargets)
{
/* Undo the indentation we added in text format */
if (es->format == EXPLAIN_FORMAT_TEXT)
es->indent--;
/* Close the group */
ExplainCloseGroup("Target Table", NULL, true, es);
}
}
/* Gather names of ON CONFLICT arbiter indexes */
foreach(lst, node->arbiterIndexes)
{
char *indexname = get_rel_name(lfirst_oid(lst));
idxNames = lappend(idxNames, indexname);
}
if (node->onConflictAction != ONCONFLICT_NONE)
{
ExplainProperty("Conflict Resolution",
node->onConflictAction == ONCONFLICT_NOTHING ?
"NOTHING" : "UPDATE",
false, es);
/*
* Don't display arbiter indexes at all when DO NOTHING variant
* implicitly ignores all conflicts
*/
if (idxNames)
ExplainPropertyList("Conflict Arbiter Indexes", idxNames, es);
/* ON CONFLICT DO UPDATE WHERE qual is specially displayed */
if (node->onConflictWhere)
{
show_upper_qual((List *) node->onConflictWhere, "Conflict Filter",
&mtstate->ps, ancestors, es);
show_instrumentation_count("Rows Removed by Conflict Filter", 1, &mtstate->ps, es);
}
/* EXPLAIN ANALYZE display of actual outcome for each tuple proposed */
if (es->analyze && mtstate->ps.instrument)
{
double total;
double insert_path;
double other_path;
InstrEndLoop(mtstate->mt_plans[0]->instrument);
/* count the number of source rows */
total = mtstate->mt_plans[0]->instrument->ntuples;
other_path = mtstate->ps.instrument->nfiltered2;
insert_path = total - other_path;
ExplainPropertyFloat("Tuples Inserted", insert_path, 0, es);
ExplainPropertyFloat("Conflicting Tuples", other_path, 0, es);
}
}
if (labeltargets)
ExplainCloseGroup("Target Tables", "Target Tables", false, es);
}
/*
* Explain the constituent plans of a ModifyTable, Append, MergeAppend,
* BitmapAnd, or BitmapOr node.
*
* The ancestors list should already contain the immediate parent of these
* plans.
*
* Note: we don't actually need to examine the Plan list members, but
* we need the list in order to determine the length of the PlanState array.
*/
static void
ExplainMemberNodes(List *plans, PlanState **planstates,
List *ancestors, ExplainState *es)
{
int nplans = list_length(plans);
int j;
for (j = 0; j < nplans; j++)
ExplainNode(planstates[j], ancestors,
"Member", NULL, es);
}
/*
* Explain a list of SubPlans (or initPlans, which also use SubPlan nodes).
*
* The ancestors list should already contain the immediate parent of these
* SubPlanStates.
*/
static void
ExplainSubPlans(List *plans, List *ancestors,
const char *relationship, ExplainState *es)
{
ListCell *lst;
foreach(lst, plans)
{
SubPlanState *sps = (SubPlanState *) lfirst(lst);
SubPlan *sp = (SubPlan *) sps->xprstate.expr;
/*
* There can be multiple SubPlan nodes referencing the same physical
* subplan (same plan_id, which is its index in PlannedStmt.subplans).
* We should print a subplan only once, so track which ones we already
* printed. This state must be global across the plan tree, since the
* duplicate nodes could be in different plan nodes, eg both a bitmap
* indexscan's indexqual and its parent heapscan's recheck qual. (We
* do not worry too much about which plan node we show the subplan as
* attached to in such cases.)
*/
if (bms_is_member(sp->plan_id, es->printed_subplans))
continue;
es->printed_subplans = bms_add_member(es->printed_subplans,
sp->plan_id);
ExplainNode(sps->planstate, ancestors,
relationship, sp->plan_name, es);
}
}
/*
* Explain a list of children of a CustomScan.
*/
static void
ExplainCustomChildren(CustomScanState *css, List *ancestors, ExplainState *es)
{
ListCell *cell;
const char *label =
(list_length(css->custom_ps) != 1 ? "children" : "child");
foreach(cell, css->custom_ps)
ExplainNode((PlanState *) lfirst(cell), ancestors, label, NULL, 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.
*/
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, "- ");
str = escape_xml((const char *) lfirst(lc));
appendStringInfoString(es->str, str);
pfree(str);
appendStringInfoString(es->str, "
\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;
case EXPLAIN_FORMAT_YAML:
ExplainYAMLLineStarting(es);
appendStringInfo(es->str, "%s: ", qlabel);
foreach(lc, data)
{
appendStringInfoChar(es->str, '\n');
appendStringInfoSpaces(es->str, es->indent * 2 + 2);
appendStringInfoString(es->str, "- ");
escape_yaml(es->str, (const char *) lfirst(lc));
}
break;
}
}
/*
* Explain a property that takes the form of a list of unlabeled items within
* another list. "data" is a list of C strings.
*/
void
ExplainPropertyListNested(const char *qlabel, List *data, ExplainState *es)
{
ListCell *lc;
bool first = true;
switch (es->format)
{
case EXPLAIN_FORMAT_TEXT:
case EXPLAIN_FORMAT_XML:
ExplainPropertyList(qlabel, data, es);
return;
case EXPLAIN_FORMAT_JSON:
ExplainJSONLineEnding(es);
appendStringInfoSpaces(es->str, es->indent * 2);
appendStringInfoChar(es->str, '[');
foreach(lc, data)
{
if (!first)
appendStringInfoString(es->str, ", ");
escape_json(es->str, (const char *) lfirst(lc));
first = false;
}
appendStringInfoChar(es->str, ']');
break;
case EXPLAIN_FORMAT_YAML:
ExplainYAMLLineStarting(es);
appendStringInfoString(es->str, "- [");
foreach(lc, data)
{
if (!first)
appendStringInfoString(es->str, ", ");
escape_yaml(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;
case EXPLAIN_FORMAT_YAML:
ExplainYAMLLineStarting(es);
appendStringInfo(es->str, "%s: ", qlabel);
if (numeric)
appendStringInfoString(es->str, value);
else
escape_yaml(es->str, value);
break;
}
}
/*
* Explain a string-valued property.
*/
void
ExplainPropertyText(const char *qlabel, const char *value, ExplainState *es)
{
ExplainProperty(qlabel, value, false, es);
}
/*
* Explain an integer-valued property.
*/
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.
*/
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.
*/
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);
}
/*
* Explain a bool-valued property.
*/
void
ExplainPropertyBool(const char *qlabel, bool value, ExplainState *es)
{
ExplainProperty(qlabel, value ? "true" : "false", 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;
case EXPLAIN_FORMAT_YAML:
/*
* In YAML format, the grouping stack is an integer list. 0 means
* we've emitted nothing at this grouping level AND this grouping
* level is unlabelled and must be marked with "- ". See
* ExplainYAMLLineStarting().
*/
ExplainYAMLLineStarting(es);
if (labelname)
{
appendStringInfo(es->str, "%s: ", labelname);
es->grouping_stack = lcons_int(1, es->grouping_stack);
}
else
{
appendStringInfoString(es->str, "- ");
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;
case EXPLAIN_FORMAT_YAML:
es->indent--;
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;
case EXPLAIN_FORMAT_YAML:
ExplainYAMLLineStarting(es);
if (labelname)
{
escape_yaml(es->str, labelname);
appendStringInfoString(es->str, ": ");
}
else
{
appendStringInfoString(es->str, "- ");
}
escape_yaml(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.
*/
void
ExplainBeginOutput(ExplainState *es)
{
switch (es->format)
{
case EXPLAIN_FORMAT_TEXT:
/* nothing to do */
break;
case EXPLAIN_FORMAT_XML:
appendStringInfoString(es->str,
"\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;
case EXPLAIN_FORMAT_YAML:
es->grouping_stack = lcons_int(0, es->grouping_stack);
break;
}
}
/*
* Emit the end-of-output boilerplate.
*/
void
ExplainEndOutput(ExplainState *es)
{
switch (es->format)
{
case EXPLAIN_FORMAT_TEXT:
/* nothing to do */
break;
case EXPLAIN_FORMAT_XML:
es->indent--;
appendStringInfoString(es->str, "");
break;
case EXPLAIN_FORMAT_JSON:
es->indent--;
appendStringInfoString(es->str, "\n]");
es->grouping_stack = list_delete_first(es->grouping_stack);
break;
case EXPLAIN_FORMAT_YAML:
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:
case EXPLAIN_FORMAT_JSON:
case EXPLAIN_FORMAT_YAML:
/* nothing to do */
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 restricts tag names more than our other output formats, eg they can't
* contain white space or slashes. Replace invalid characters with dashes,
* so that for example "I/O Read Time" becomes "I-O-Read-Time".
*/
static void
ExplainXMLTag(const char *tagname, int flags, ExplainState *es)
{
const char *s;
const char *valid = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.";
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++)
appendStringInfoChar(es->str, strchr(valid, *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');
}
/*
* Indent a YAML line.
*
* YAML lines are ordinarily indented by two spaces per indentation level.
* The text emitted for each property begins just prior to the preceding
* line-break, except for the first property in an unlabelled group, for which
* it begins immediately after the "- " that introduces the group. The first
* property of the group appears on the same line as the opening "- ".
*/
static void
ExplainYAMLLineStarting(ExplainState *es)
{
Assert(es->format == EXPLAIN_FORMAT_YAML);
if (linitial_int(es->grouping_stack) == 0)
{
linitial_int(es->grouping_stack) = 1;
}
else
{
appendStringInfoChar(es->str, '\n');
appendStringInfoSpaces(es->str, es->indent * 2);
}
}
/*
* YAML is a superset of JSON; unfortuantely, the YAML quoting rules are
* ridiculously complicated -- as documented in sections 5.3 and 7.3.3 of
* http://yaml.org/spec/1.2/spec.html -- so we chose to just quote everything.
* Empty strings, strings with leading or trailing whitespace, and strings
* containing a variety of special characters must certainly be quoted or the
* output is invalid; and other seemingly harmless strings like "0xa" or
* "true" must be quoted, lest they be interpreted as a hexadecimal or Boolean
* constant rather than a string.
*/
static void
escape_yaml(StringInfo buf, const char *str)
{
escape_json(buf, str);
}