2006 lines
50 KiB
C
2006 lines
50 KiB
C
/*-------------------------------------------------------------------------
|
|
*
|
|
* explain.c
|
|
* Explain query execution plans
|
|
*
|
|
* Portions Copyright (c) 1996-2009, PostgreSQL Global Development Group
|
|
* Portions Copyright (c) 1994-5, Regents of the University of California
|
|
*
|
|
* IDENTIFICATION
|
|
* $PostgreSQL: pgsql/src/backend/commands/explain.c,v 1.194 2009/12/11 01:33:35 adunstan Exp $
|
|
*
|
|
*-------------------------------------------------------------------------
|
|
*/
|
|
#include "postgres.h"
|
|
|
|
#include "access/xact.h"
|
|
#include "catalog/pg_constraint.h"
|
|
#include "catalog/pg_type.h"
|
|
#include "commands/defrem.h"
|
|
#include "commands/explain.h"
|
|
#include "commands/prepare.h"
|
|
#include "commands/trigger.h"
|
|
#include "executor/instrument.h"
|
|
#include "optimizer/clauses.h"
|
|
#include "optimizer/planner.h"
|
|
#include "optimizer/var.h"
|
|
#include "parser/parsetree.h"
|
|
#include "rewrite/rewriteHandler.h"
|
|
#include "tcop/tcopprot.h"
|
|
#include "utils/builtins.h"
|
|
#include "utils/guc.h"
|
|
#include "utils/lsyscache.h"
|
|
#include "utils/tuplesort.h"
|
|
#include "utils/snapmgr.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, 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 void ExplainNode(Plan *plan, PlanState *planstate,
|
|
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, bool useprefix, ExplainState *es);
|
|
static void show_scan_qual(List *qual, const char *qlabel,
|
|
Plan *scan_plan, Plan *outer_plan,
|
|
ExplainState *es);
|
|
static void show_upper_qual(List *qual, const char *qlabel, Plan *plan,
|
|
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, 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 ExplainYAMLLineStarting(ExplainState *es);
|
|
static void escape_json(StringInfo buf, const char *str);
|
|
static void escape_yaml(StringInfo buf, const char *str);
|
|
|
|
|
|
/*
|
|
* ExplainQuery -
|
|
* execute an EXPLAIN command
|
|
*/
|
|
void
|
|
ExplainQuery(ExplainStmt *stmt, const char *queryString,
|
|
ParamListInfo params, DestReceiver *dest)
|
|
{
|
|
ExplainState es;
|
|
TupOutputState *tstate;
|
|
List *rewritten;
|
|
ListCell *lc;
|
|
|
|
/* Initialize ExplainState. */
|
|
ExplainInitState(&es);
|
|
|
|
/* 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, "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)));
|
|
}
|
|
else
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_SYNTAX_ERROR),
|
|
errmsg("unrecognized EXPLAIN option \"%s\"",
|
|
opt->defname)));
|
|
}
|
|
|
|
/*
|
|
* Run parse analysis and rewrite. Note this also acquires sufficient
|
|
* locks on the source table(s).
|
|
*
|
|
* Because the parser and planner tend to scribble on their 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.
|
|
*/
|
|
rewritten = pg_analyze_and_rewrite_params((Node *) copyObject(stmt->query),
|
|
queryString,
|
|
(ParserSetupHook) setupParserWithParamList,
|
|
params);
|
|
|
|
/* 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), &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);
|
|
}
|
|
|
|
/*
|
|
* Initialize ExplainState.
|
|
*/
|
|
void
|
|
ExplainInitState(ExplainState *es)
|
|
{
|
|
/* Set default options. */
|
|
memset(es, 0, sizeof(ExplainState));
|
|
es->costs = true;
|
|
/* Prepare output buffer. */
|
|
es->str = makeStringInfo();
|
|
}
|
|
|
|
/*
|
|
* ExplainResultDesc -
|
|
* construct the result tupledesc for an EXPLAIN
|
|
*/
|
|
TupleDesc
|
|
ExplainResultDesc(ExplainStmt *stmt)
|
|
{
|
|
TupleDesc tupdesc;
|
|
ListCell *lc;
|
|
bool xml = false;
|
|
|
|
/* 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",
|
|
xml ? XMLOID : TEXTOID, -1, 0);
|
|
return tupdesc;
|
|
}
|
|
|
|
/*
|
|
* ExplainOneQuery -
|
|
* print out the execution plan for one Query
|
|
*/
|
|
static void
|
|
ExplainOneQuery(Query *query, ExplainState *es,
|
|
const char *queryString, ParamListInfo params)
|
|
{
|
|
/* planner will not cope with utility statements */
|
|
if (query->commandType == CMD_UTILITY)
|
|
{
|
|
ExplainOneUtility(query->utilityStmt, es, queryString, params);
|
|
return;
|
|
}
|
|
|
|
/* if an advisor plugin is present, let it manage things */
|
|
if (ExplainOneQuery_hook)
|
|
(*ExplainOneQuery_hook) (query, es, queryString, params);
|
|
else
|
|
{
|
|
PlannedStmt *plan;
|
|
|
|
/* plan the query */
|
|
plan = pg_plan_query(query, 0, params);
|
|
|
|
/* run it (if needed) and produce output */
|
|
ExplainOnePlan(plan, es, queryString, params);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* 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)
|
|
*
|
|
* This is exported because it's called back from prepare.c in the
|
|
* EXPLAIN EXECUTE case
|
|
*/
|
|
void
|
|
ExplainOneUtility(Node *utilityStmt, ExplainState *es,
|
|
const char *queryString, ParamListInfo params)
|
|
{
|
|
if (utilityStmt == NULL)
|
|
return;
|
|
|
|
if (IsA(utilityStmt, ExecuteStmt))
|
|
ExplainExecuteQuery((ExecuteStmt *) utilityStmt, es,
|
|
queryString, params);
|
|
else if (IsA(utilityStmt, NotifyStmt))
|
|
{
|
|
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
|
|
*
|
|
* Since we ignore any DeclareCursorStmt that might be attached to the query,
|
|
* 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.
|
|
*
|
|
* 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, ExplainState *es,
|
|
const char *queryString, ParamListInfo params)
|
|
{
|
|
QueryDesc *queryDesc;
|
|
instr_time starttime;
|
|
double totaltime = 0;
|
|
int eflags;
|
|
|
|
/*
|
|
* Use a snapshot with an updated command ID to ensure this query sees
|
|
* results of any previously executed queries.
|
|
*/
|
|
PushUpdatedSnapshot(GetActiveSnapshot());
|
|
|
|
/* Create a QueryDesc requesting no output */
|
|
queryDesc = CreateQueryDesc(plannedstmt, queryString,
|
|
GetActiveSnapshot(), InvalidSnapshot,
|
|
None_Receiver, params, es->analyze);
|
|
|
|
INSTR_TIME_SET_CURRENT(starttime);
|
|
|
|
/* If analyzing, we need to cope with queued triggers */
|
|
if (es->analyze)
|
|
AfterTriggerBeginQuery();
|
|
|
|
/* Select execution options */
|
|
if (es->analyze)
|
|
eflags = 0; /* default run-to-completion flags */
|
|
else
|
|
eflags = EXEC_FLAG_EXPLAIN_ONLY;
|
|
|
|
/* call ExecutorStart to prepare the plan for execution */
|
|
ExecutorStart(queryDesc, eflags);
|
|
|
|
/* Execute the plan for statistics if asked for */
|
|
if (es->analyze)
|
|
{
|
|
/* run the plan */
|
|
ExecutorRun(queryDesc, ForwardScanDirection, 0L);
|
|
|
|
/* We can't clean up '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 we ran the command, run any AFTER triggers it queued. (Note this
|
|
* will not include DEFERRED triggers; since those don't run until end of
|
|
* transaction, we can't measure them.) Include into total runtime.
|
|
*/
|
|
if (es->analyze)
|
|
{
|
|
INSTR_TIME_SET_CURRENT(starttime);
|
|
AfterTriggerEndQuery(queryDesc->estate);
|
|
totaltime += elapsed_time(&starttime);
|
|
}
|
|
|
|
/* Print info about runtime of triggers */
|
|
if (es->analyze)
|
|
{
|
|
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);
|
|
}
|
|
|
|
/*
|
|
* Close down the query and free resources. Include time for this in the
|
|
* total runtime (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->analyze)
|
|
{
|
|
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);
|
|
}
|
|
|
|
/*
|
|
* 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. Other fields in *es are
|
|
* initialized here.
|
|
*
|
|
* NB: will not work on utility statements
|
|
*/
|
|
void
|
|
ExplainPrintPlan(ExplainState *es, QueryDesc *queryDesc)
|
|
{
|
|
Assert(queryDesc->plannedstmt != NULL);
|
|
es->pstmt = queryDesc->plannedstmt;
|
|
es->rtable = queryDesc->plannedstmt->rtable;
|
|
ExplainNode(queryDesc->plannedstmt->planTree, queryDesc->planstate,
|
|
NULL, NULL, NULL, 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);
|
|
appendStringInfo(es->str, ": time=%.3f calls=%.0f\n",
|
|
1000.0 * instr->total, instr->ntuples);
|
|
}
|
|
else
|
|
{
|
|
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 (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);
|
|
}
|
|
|
|
/*
|
|
* ExplainNode -
|
|
* 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
|
|
* list of subplans.
|
|
*
|
|
* outer_plan, if not null, references another plan node that is the outer
|
|
* side of a join with the current node. This is only interesting for
|
|
* deciphering runtime keys of an inner indexscan.
|
|
*
|
|
* 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,
|
|
const char *relationship, const char *plan_name,
|
|
ExplainState *es)
|
|
{
|
|
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 *operation = NULL;
|
|
int save_indent = es->indent;
|
|
bool haschildren;
|
|
|
|
Assert(plan);
|
|
|
|
switch (nodeTag(plan))
|
|
{
|
|
case T_Result:
|
|
pname = sname = "Result";
|
|
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_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_IndexScan:
|
|
pname = sname = "Index 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_Material:
|
|
pname = sname = "Materialize";
|
|
break;
|
|
case T_Sort:
|
|
pname = sname = "Sort";
|
|
break;
|
|
case T_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 = 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;
|
|
}
|
|
appendStringInfoString(es->str, pname);
|
|
es->indent++;
|
|
}
|
|
else
|
|
{
|
|
ExplainPropertyText("Node Type", sname, es);
|
|
if (strategy)
|
|
ExplainPropertyText("Strategy", strategy, es);
|
|
if (operation)
|
|
ExplainPropertyText("Operation", operation, es);
|
|
if (relationship)
|
|
ExplainPropertyText("Parent Relationship", relationship, es);
|
|
if (plan_name)
|
|
ExplainPropertyText("Subplan Name", plan_name, es);
|
|
}
|
|
|
|
switch (nodeTag(plan))
|
|
{
|
|
case T_IndexScan:
|
|
{
|
|
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:
|
|
case T_TidScan:
|
|
case T_SubqueryScan:
|
|
case T_FunctionScan:
|
|
case T_ValuesScan:
|
|
case T_CteScan:
|
|
case T_WorkTableScan:
|
|
ExplainScanTarget((Scan *) plan, es);
|
|
break;
|
|
case T_BitmapIndexScan:
|
|
{
|
|
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)
|
|
{
|
|
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 ...
|
|
*/
|
|
if (planstate->instrument)
|
|
InstrEndLoop(planstate->instrument);
|
|
|
|
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;
|
|
|
|
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)
|
|
{
|
|
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, es);
|
|
|
|
/* quals, sort keys, etc */
|
|
switch (nodeTag(plan))
|
|
{
|
|
case T_IndexScan:
|
|
show_scan_qual(((IndexScan *) plan)->indexqualorig,
|
|
"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, es);
|
|
break;
|
|
case T_BitmapHeapScan:
|
|
show_scan_qual(((BitmapHeapScan *) plan)->bitmapqualorig,
|
|
"Recheck Cond", plan, outer_plan, es);
|
|
/* FALL THRU */
|
|
case T_SeqScan:
|
|
case T_FunctionScan:
|
|
case T_ValuesScan:
|
|
case T_CteScan:
|
|
case T_WorkTableScan:
|
|
case T_SubqueryScan:
|
|
show_scan_qual(plan->qual, "Filter", plan, outer_plan, 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", 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, es);
|
|
show_upper_qual(plan->qual, "Filter", plan, es);
|
|
break;
|
|
case T_MergeJoin:
|
|
show_upper_qual(((MergeJoin *) plan)->mergeclauses,
|
|
"Merge Cond", plan, es);
|
|
show_upper_qual(((MergeJoin *) plan)->join.joinqual,
|
|
"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, es);
|
|
show_upper_qual(((HashJoin *) plan)->join.joinqual,
|
|
"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, es);
|
|
break;
|
|
case T_Sort:
|
|
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, 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, ModifyTable) ||
|
|
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, "InitPlan", es);
|
|
|
|
/* lefttree */
|
|
if (outerPlan(plan))
|
|
{
|
|
/*
|
|
* Ordinarily we don't pass down our own outer_plan value to our child
|
|
* nodes, but in bitmap scan trees we must, since the bottom
|
|
* BitmapIndexScan nodes may have outer references.
|
|
*/
|
|
ExplainNode(outerPlan(plan), outerPlanState(planstate),
|
|
IsA(plan, BitmapHeapScan) ? outer_plan : NULL,
|
|
"Outer", NULL, es);
|
|
}
|
|
|
|
/* righttree */
|
|
if (innerPlan(plan))
|
|
{
|
|
ExplainNode(innerPlan(plan), innerPlanState(planstate),
|
|
outerPlan(plan),
|
|
"Inner", NULL, es);
|
|
}
|
|
|
|
/* special child plans */
|
|
switch (nodeTag(plan))
|
|
{
|
|
case T_ModifyTable:
|
|
ExplainMemberNodes(((ModifyTable *) plan)->plans,
|
|
((ModifyTableState *) planstate)->mt_plans,
|
|
outer_plan, es);
|
|
break;
|
|
case T_Append:
|
|
ExplainMemberNodes(((Append *) plan)->appendplans,
|
|
((AppendState *) planstate)->appendplans,
|
|
outer_plan, es);
|
|
break;
|
|
case T_BitmapAnd:
|
|
ExplainMemberNodes(((BitmapAnd *) plan)->bitmapplans,
|
|
((BitmapAndState *) planstate)->bitmapplans,
|
|
outer_plan, es);
|
|
break;
|
|
case T_BitmapOr:
|
|
ExplainMemberNodes(((BitmapOr *) plan)->bitmapplans,
|
|
((BitmapOrState *) planstate)->bitmapplans,
|
|
outer_plan, es);
|
|
break;
|
|
case T_SubqueryScan:
|
|
{
|
|
SubqueryScan *subqueryscan = (SubqueryScan *) plan;
|
|
SubqueryScanState *subquerystate = (SubqueryScanState *) planstate;
|
|
|
|
ExplainNode(subqueryscan->subplan, subquerystate->subplan,
|
|
NULL,
|
|
"Subquery", NULL, es);
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
/* subPlan-s */
|
|
if (planstate->subPlan)
|
|
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, ExplainState *es)
|
|
{
|
|
List *context;
|
|
List *result = NIL;
|
|
bool useprefix;
|
|
ListCell *lc;
|
|
int i;
|
|
|
|
/* 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 RecursiveUnion */
|
|
if (IsA(plan, RecursiveUnion))
|
|
return;
|
|
|
|
/* Set up deparsing context */
|
|
context = deparse_context_for_plan((Node *) plan,
|
|
NULL,
|
|
es->rtable,
|
|
es->pstmt->subplans);
|
|
useprefix = list_length(es->rtable) > 1;
|
|
|
|
/* Deparse each result column (we now include resjunk ones) */
|
|
i = 0;
|
|
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 qualifier expression
|
|
*
|
|
* Note: outer_plan is the referent for any OUTER vars in the scan qual;
|
|
* this would be the outer side of a nestloop plan. Pass NULL if none.
|
|
*/
|
|
static void
|
|
show_qual(List *qual, const char *qlabel, Plan *plan, Plan *outer_plan,
|
|
bool useprefix, ExplainState *es)
|
|
{
|
|
List *context;
|
|
Node *node;
|
|
char *exprstr;
|
|
|
|
/* No work if empty qual */
|
|
if (qual == NIL)
|
|
return;
|
|
|
|
/* Convert AND list to explicit AND */
|
|
node = (Node *) make_ands_explicit(qual);
|
|
|
|
/* Set up deparsing context */
|
|
context = deparse_context_for_plan((Node *) plan,
|
|
(Node *) outer_plan,
|
|
es->rtable,
|
|
es->pstmt->subplans);
|
|
|
|
/* Deparse the expression */
|
|
exprstr = deparse_expression(node, context, useprefix, false);
|
|
|
|
/* And add to es->str */
|
|
ExplainPropertyText(qlabel, exprstr, es);
|
|
}
|
|
|
|
/*
|
|
* Show a qualifier expression for a scan plan node
|
|
*/
|
|
static void
|
|
show_scan_qual(List *qual, const char *qlabel,
|
|
Plan *scan_plan, Plan *outer_plan,
|
|
ExplainState *es)
|
|
{
|
|
bool useprefix;
|
|
|
|
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, ExplainState *es)
|
|
{
|
|
bool useprefix;
|
|
|
|
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, ExplainState *es)
|
|
{
|
|
int nkeys = ((Sort *) sortplan)->numCols;
|
|
AttrNumber *keycols = ((Sort *) sortplan)->sortColIdx;
|
|
List *context;
|
|
List *result = NIL;
|
|
bool useprefix;
|
|
int keyno;
|
|
char *exprstr;
|
|
|
|
if (nkeys <= 0)
|
|
return;
|
|
|
|
/* Set up deparsing context */
|
|
context = deparse_context_for_plan((Node *) sortplan,
|
|
NULL,
|
|
es->rtable,
|
|
es->pstmt->subplans);
|
|
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(sortplan->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);
|
|
}
|
|
|
|
ExplainPropertyList("Sort Key", result, 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);
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* 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 the target of a Scan node
|
|
*/
|
|
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? */
|
|
return;
|
|
rte = rt_fetch(plan->scanrelid, es->rtable);
|
|
|
|
switch (nodeTag(plan))
|
|
{
|
|
case T_SeqScan:
|
|
case T_IndexScan:
|
|
case T_BitmapHeapScan:
|
|
case T_TidScan:
|
|
/* 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:
|
|
{
|
|
Node *funcexpr;
|
|
|
|
/* Assert it's on a RangeFunction */
|
|
Assert(rte->rtekind == RTE_FUNCTION);
|
|
|
|
/*
|
|
* If the expression is still a function call, we can get the
|
|
* real name of the function. Otherwise, punt (this can
|
|
* happen if the optimizer simplified away the function call,
|
|
* for example).
|
|
*/
|
|
funcexpr = ((FunctionScan *) plan)->funcexpr;
|
|
if (funcexpr && IsA(funcexpr, FuncExpr))
|
|
{
|
|
Oid funcid = ((FuncExpr *) funcexpr)->funcid;
|
|
|
|
objectname = get_func_name(funcid);
|
|
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(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);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Explain the constituent plans of a ModifyTable, Append, BitmapAnd,
|
|
* or BitmapOr node.
|
|
*
|
|
* Ordinarily we don't pass down outer_plan to our child nodes, but in these
|
|
* cases we must, since the node could be an "inner indexscan" in which case
|
|
* outer references can appear in the child nodes.
|
|
*/
|
|
static void
|
|
ExplainMemberNodes(List *plans, PlanState **planstate, Plan *outer_plan,
|
|
ExplainState *es)
|
|
{
|
|
ListCell *lst;
|
|
int j = 0;
|
|
|
|
foreach(lst, plans)
|
|
{
|
|
Plan *subnode = (Plan *) lfirst(lst);
|
|
|
|
ExplainNode(subnode, planstate[j],
|
|
outer_plan,
|
|
"Member", NULL,
|
|
es);
|
|
j++;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Explain a list of SubPlans (or initPlans, which also use SubPlan nodes).
|
|
*/
|
|
static void
|
|
ExplainSubPlans(List *plans, const char *relationship, ExplainState *es)
|
|
{
|
|
ListCell *lst;
|
|
|
|
foreach(lst, plans)
|
|
{
|
|
SubPlanState *sps = (SubPlanState *) lfirst(lst);
|
|
SubPlan *sp = (SubPlan *) sps->xprstate.expr;
|
|
|
|
ExplainNode(exec_subplan_get_plan(es->pstmt, sp),
|
|
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;
|
|
|
|
case EXPLAIN_FORMAT_YAML:
|
|
ExplainYAMLLineStarting(es);
|
|
escape_yaml(es->str, qlabel);
|
|
appendStringInfoChar(es->str, ':');
|
|
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 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 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;
|
|
|
|
case EXPLAIN_FORMAT_YAML:
|
|
ExplainYAMLLineStarting(es);
|
|
if (labelname)
|
|
{
|
|
appendStringInfo(es->str, "%s:", labelname);
|
|
es->grouping_stack = lcons_int(1, es->grouping_stack);
|
|
}
|
|
else
|
|
{
|
|
appendStringInfoChar(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)
|
|
appendStringInfo(es->str, "%s:", labelname);
|
|
appendStringInfoString(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;
|
|
|
|
case EXPLAIN_FORMAT_YAML:
|
|
es->grouping_stack = lcons_int(0, es->grouping_stack);
|
|
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;
|
|
|
|
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:
|
|
case EXPLAIN_FORMAT_YAML:
|
|
/* 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');
|
|
}
|
|
|
|
/*
|
|
* Indent a YAML line.
|
|
*/
|
|
static void
|
|
ExplainYAMLLineStarting(ExplainState *es)
|
|
{
|
|
Assert(es->format == EXPLAIN_FORMAT_YAML);
|
|
if (linitial_int(es->grouping_stack) == 0)
|
|
{
|
|
appendStringInfoChar(es->str, ' ');
|
|
linitial_int(es->grouping_stack) = 1;
|
|
}
|
|
else
|
|
{
|
|
appendStringInfoChar(es->str, '\n');
|
|
appendStringInfoSpaces(es->str, es->indent * 2);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* 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, '\"');
|
|
}
|
|
|
|
/*
|
|
* YAML is a superset of JSON: if we find quotable characters, we call escape_json
|
|
*/
|
|
static void
|
|
escape_yaml(StringInfo buf, const char *str)
|
|
{
|
|
const char *p;
|
|
|
|
for (p = str; *p; p++)
|
|
{
|
|
if ((unsigned char) *p < ' ' || strchr("\"\\\b\f\n\r\t", *p))
|
|
{
|
|
escape_json(buf, str);
|
|
return;
|
|
}
|
|
}
|
|
|
|
appendStringInfo(buf, "%s", str);
|
|
}
|