From 9bd27b7c9e998087f390774bd0f43916813a2847 Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Mon, 10 Aug 2009 05:46:50 +0000 Subject: [PATCH] Extend EXPLAIN to support output in XML or JSON format. There are probably still some adjustments to be made in the details of the output, but this gets the basic structure in place. Robert Haas --- contrib/auto_explain/auto_explain.c | 22 +- doc/src/sgml/auto-explain.sgml | 20 +- doc/src/sgml/ref/explain.sgml | 28 +- src/backend/commands/explain.c | 1257 +++++++++++++++++++++------ src/backend/commands/prepare.c | 11 +- src/backend/utils/adt/xml.c | 67 +- src/backend/utils/cache/lsyscache.c | 28 +- src/backend/utils/sort/tuplesort.c | 49 +- src/include/commands/explain.h | 16 +- src/include/utils/lsyscache.h | 3 +- src/include/utils/tuplesort.h | 7 +- src/include/utils/xml.h | 3 +- 12 files changed, 1150 insertions(+), 361 deletions(-) diff --git a/contrib/auto_explain/auto_explain.c b/contrib/auto_explain/auto_explain.c index eb2bc04aed975..6d3435be1f75f 100644 --- a/contrib/auto_explain/auto_explain.c +++ b/contrib/auto_explain/auto_explain.c @@ -6,7 +6,7 @@ * Copyright (c) 2008-2009, PostgreSQL Global Development Group * * IDENTIFICATION - * $PostgreSQL: pgsql/contrib/auto_explain/auto_explain.c,v 1.6 2009/07/26 23:34:17 tgl Exp $ + * $PostgreSQL: pgsql/contrib/auto_explain/auto_explain.c,v 1.7 2009/08/10 05:46:49 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -22,8 +22,16 @@ PG_MODULE_MAGIC; static int auto_explain_log_min_duration = -1; /* msec or -1 */ static bool auto_explain_log_analyze = false; static bool auto_explain_log_verbose = false; +static int auto_explain_log_format = EXPLAIN_FORMAT_TEXT; static bool auto_explain_log_nested_statements = false; +static const struct config_enum_entry format_options[] = { + {"text", EXPLAIN_FORMAT_TEXT, false}, + {"xml", EXPLAIN_FORMAT_XML, false}, + {"json", EXPLAIN_FORMAT_JSON, false}, + {NULL, 0, false} +}; + /* Current nesting depth of ExecutorRun calls */ static int nesting_level = 0; @@ -84,6 +92,17 @@ _PG_init(void) NULL, NULL); + DefineCustomEnumVariable("auto_explain.log_format", + "EXPLAIN format to be used for plan logging.", + NULL, + &auto_explain_log_format, + EXPLAIN_FORMAT_TEXT, + format_options, + PGC_SUSET, + 0, + NULL, + NULL); + DefineCustomBoolVariable("auto_explain.log_nested_statements", "Log nested statements.", NULL, @@ -201,6 +220,7 @@ explain_ExecutorEnd(QueryDesc *queryDesc) ExplainInitState(&es); es.analyze = (queryDesc->doInstrument && auto_explain_log_analyze); es.verbose = auto_explain_log_verbose; + es.format = auto_explain_log_format; ExplainPrintPlan(&es, queryDesc); diff --git a/doc/src/sgml/auto-explain.sgml b/doc/src/sgml/auto-explain.sgml index c1e85af10e0f2..72487f944ce64 100644 --- a/doc/src/sgml/auto-explain.sgml +++ b/doc/src/sgml/auto-explain.sgml @@ -1,4 +1,4 @@ - + auto_explain @@ -102,6 +102,24 @@ LOAD 'auto_explain'; + + + auto_explain.log_format (enum) + + + auto_explain.log_format configuration parameter + + + + auto_explain.log_format selects the + EXPLAIN output format to be used. + The allowed values are text, xml, + and json. The default is text. + Only superusers can change this setting. + + + + auto_explain.log_nested_statements (boolean) diff --git a/doc/src/sgml/ref/explain.sgml b/doc/src/sgml/ref/explain.sgml index d3b5c979a2960..9670bd06f193a 100644 --- a/doc/src/sgml/ref/explain.sgml +++ b/doc/src/sgml/ref/explain.sgml @@ -1,5 +1,5 @@ @@ -31,7 +31,7 @@ PostgreSQL documentation -EXPLAIN [ ( { ANALYZE boolean | VERBOSE boolean | COSTS boolean } [, ...] ) ] statement +EXPLAIN [ ( { ANALYZE boolean | VERBOSE boolean | COSTS boolean | FORMAT { TEXT | XML | JSON } } [, ...] ) ] statement EXPLAIN [ ANALYZE ] [ VERBOSE ] statement @@ -109,7 +109,7 @@ ROLLBACK; Carry out the command and show the actual run times. This - parameter defaults to FALSE. + parameter defaults to FALSE. @@ -118,8 +118,12 @@ ROLLBACK; VERBOSE - Include the output column list for each node in the plan tree. This - parameter defaults to FALSE. + Display additional information regarding the plan. Specifically, include + the output column list for each node in the plan tree, schema-qualify + table and function names, always label variables in expressions with + their range table alias, and always print the name of each trigger for + which statistics are displayed. This parameter defaults to + FALSE. @@ -130,7 +134,19 @@ ROLLBACK; Include information on the estimated startup and total cost of each plan node, as well as the estimated number of rows and the estimated - width of each row. This parameter defaults to TRUE. + width of each row. This parameter defaults to TRUE. + + + + + + FORMAT + + + Specify the output format, which can be TEXT, XML, or JSON. + XML or JSON output contains the same information as the text output + format, but is easier for programs to parse. This parameter defaults to + TEXT. diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c index 1388c8dd42fb8..d675d8d8171f0 100644 --- a/src/backend/commands/explain.c +++ b/src/backend/commands/explain.c @@ -7,7 +7,7 @@ * Portions Copyright (c) 1994-5, Regents of the University of California * * IDENTIFICATION - * $PostgreSQL: pgsql/src/backend/commands/explain.c,v 1.188 2009/07/26 23:34:17 tgl Exp $ + * $PostgreSQL: pgsql/src/backend/commands/explain.c,v 1.189 2009/08/10 05:46:50 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -32,6 +32,7 @@ #include "utils/lsyscache.h" #include "utils/tuplesort.h" #include "utils/snapmgr.h" +#include "utils/xml.h" /* Hook for plugins to get control in ExplainOneQuery() */ @@ -41,28 +42,60 @@ ExplainOneQuery_hook_type ExplainOneQuery_hook = NULL; explain_get_index_name_hook_type explain_get_index_name_hook = NULL; +/* OR-able flags for ExplainXMLTag() */ +#define X_OPENING 0 +#define X_CLOSING 1 +#define X_CLOSE_IMMEDIATE 2 +#define X_NOWHITESPACE 4 + static void ExplainOneQuery(Query *query, ExplainState *es, const char *queryString, ParamListInfo params); static void report_triggers(ResultRelInfo *rInfo, bool show_relname, - StringInfo buf); + ExplainState *es); static double elapsed_time(instr_time *starttime); static void ExplainNode(Plan *plan, PlanState *planstate, - Plan *outer_plan, int indent, ExplainState *es); -static void show_plan_tlist(Plan *plan, int indent, ExplainState *es); + Plan *outer_plan, + const char *relationship, const char *plan_name, + ExplainState *es); +static void show_plan_tlist(Plan *plan, ExplainState *es); static void show_qual(List *qual, const char *qlabel, Plan *plan, - Plan *outer_plan, int indent, bool useprefix, ExplainState *es); + Plan *outer_plan, bool useprefix, ExplainState *es); static void show_scan_qual(List *qual, const char *qlabel, Plan *scan_plan, Plan *outer_plan, - int indent, ExplainState *es); + ExplainState *es); static void show_upper_qual(List *qual, const char *qlabel, Plan *plan, - int indent, ExplainState *es); -static void show_sort_keys(Plan *sortplan, int indent, ExplainState *es); -static void show_sort_info(SortState *sortstate, int indent, ExplainState *es); + ExplainState *es); +static void show_sort_keys(Plan *sortplan, ExplainState *es); +static void show_sort_info(SortState *sortstate, ExplainState *es); static const char *explain_get_index_name(Oid indexId); static void ExplainScanTarget(Scan *plan, ExplainState *es); static void ExplainMemberNodes(List *plans, PlanState **planstate, - Plan *outer_plan, int indent, ExplainState *es); -static void ExplainSubPlans(List *plans, int indent, ExplainState *es); + Plan *outer_plan, ExplainState *es); +static void ExplainSubPlans(List *plans, const char *relationship, + ExplainState *es); +static void ExplainPropertyList(const char *qlabel, List *data, + ExplainState *es); +static void ExplainProperty(const char *qlabel, const char *value, + bool numeric, ExplainState *es); +#define ExplainPropertyText(qlabel, value, es) \ + ExplainProperty(qlabel, value, false, es) +static void ExplainPropertyInteger(const char *qlabel, int value, + ExplainState *es); +static void ExplainPropertyLong(const char *qlabel, long value, + ExplainState *es); +static void ExplainPropertyFloat(const char *qlabel, double value, int ndigits, + ExplainState *es); +static void ExplainOpenGroup(const char *objtype, const char *labelname, + bool labeled, ExplainState *es); +static void ExplainCloseGroup(const char *objtype, const char *labelname, + bool labeled, ExplainState *es); +static void ExplainDummyGroup(const char *objtype, const char *labelname, + ExplainState *es); +static void ExplainBeginOutput(ExplainState *es); +static void ExplainEndOutput(ExplainState *es); +static void ExplainXMLTag(const char *tagname, int flags, ExplainState *es); +static void ExplainJSONLineEnding(ExplainState *es); +static void escape_json(StringInfo buf, const char *str); /* @@ -94,6 +127,22 @@ ExplainQuery(ExplainStmt *stmt, const char *queryString, es.verbose = defGetBoolean(opt); else if (strcmp(opt->defname, "costs") == 0) es.costs = defGetBoolean(opt); + else if (strcmp(opt->defname, "format") == 0) + { + char *p = defGetString(opt); + + if (strcmp(p, "text") == 0) + es.format = EXPLAIN_FORMAT_TEXT; + else if (strcmp(p, "xml") == 0) + es.format = EXPLAIN_FORMAT_XML; + else if (strcmp(p, "json") == 0) + es.format = EXPLAIN_FORMAT_JSON; + else + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("unrecognized value for EXPLAIN option \"%s\": \"%s\"", + opt->defname, p))); + } else ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), @@ -117,10 +166,17 @@ ExplainQuery(ExplainStmt *stmt, const char *queryString, rewritten = pg_analyze_and_rewrite((Node *) copyObject(stmt->query), queryString, param_types, num_params); + /* emit opening boilerplate */ + ExplainBeginOutput(&es); + if (rewritten == NIL) { - /* In the case of an INSTEAD NOTHING, tell at least that */ - appendStringInfoString(es.str, "Query rewrites to nothing\n"); + /* + * In the case of an INSTEAD NOTHING, tell at least that. But in + * non-text format, the output is delimited, so this isn't necessary. + */ + if (es.format == EXPLAIN_FORMAT_TEXT) + appendStringInfoString(es.str, "Query rewrites to nothing\n"); } else { @@ -130,15 +186,23 @@ ExplainQuery(ExplainStmt *stmt, const char *queryString, foreach(l, rewritten) { ExplainOneQuery((Query *) lfirst(l), &es, queryString, params); - /* put a blank line between plans */ + + /* Separate plans with an appropriate separator */ if (lnext(l) != NULL) - appendStringInfoChar(es.str, '\n'); + ExplainSeparatePlans(&es); } } + /* emit closing boilerplate */ + ExplainEndOutput(&es); + Assert(es.indent == 0); + /* output tuples */ tstate = begin_tup_output_tupdesc(dest, ExplainResultDesc(stmt)); - do_text_output_multiline(tstate, es.str->data); + if (es.format == EXPLAIN_FORMAT_TEXT) + do_text_output_multiline(tstate, es.str->data); + else + do_text_output_oneline(tstate, es.str->data); end_tup_output(tstate); pfree(es.str->data); @@ -165,11 +229,26 @@ TupleDesc ExplainResultDesc(ExplainStmt *stmt) { TupleDesc tupdesc; + ListCell *lc; + bool xml = false; - /* need a tuple descriptor representing a single TEXT column */ + /* Check for XML format option */ + foreach(lc, stmt->options) + { + DefElem *opt = (DefElem *) lfirst(lc); + + if (strcmp(opt->defname, "format") == 0) + { + char *p = defGetString(opt); + + xml = (strcmp(p, "xml") == 0); + } + } + + /* Need a tuple descriptor representing a single TEXT or XML column */ tupdesc = CreateTemplateTupleDesc(1, false); TupleDescInitEntry(tupdesc, (AttrNumber) 1, "QUERY PLAN", - TEXTOID, -1, 0); + xml ? XMLOID : TEXTOID, -1, 0); return tupdesc; } @@ -223,10 +302,20 @@ ExplainOneUtility(Node *utilityStmt, ExplainState *es, ExplainExecuteQuery((ExecuteStmt *) utilityStmt, es, queryString, params); else if (IsA(utilityStmt, NotifyStmt)) - appendStringInfoString(es->str, "NOTIFY\n"); + { + if (es->format == EXPLAIN_FORMAT_TEXT) + appendStringInfoString(es->str, "NOTIFY\n"); + else + ExplainDummyGroup("Notify", NULL, es); + } else - appendStringInfoString(es->str, + { + if (es->format == EXPLAIN_FORMAT_TEXT) + appendStringInfoString(es->str, "Utility statements have no plan structure\n"); + else + ExplainDummyGroup("Utility Statement", NULL, es); + } } /* @@ -288,6 +377,8 @@ ExplainOnePlan(PlannedStmt *plannedstmt, ExplainState *es, totaltime += elapsed_time(&starttime); } + ExplainOpenGroup("Query", NULL, true, es); + /* Create textual dump of plan tree */ ExplainPrintPlan(es, queryDesc); @@ -313,16 +404,20 @@ ExplainOnePlan(PlannedStmt *plannedstmt, ExplainState *es, int nr; ListCell *l; + ExplainOpenGroup("Triggers", "Triggers", false, es); + show_relname = (numrels > 1 || targrels != NIL); rInfo = queryDesc->estate->es_result_relations; for (nr = 0; nr < numrels; rInfo++, nr++) - report_triggers(rInfo, show_relname, es->str); + report_triggers(rInfo, show_relname, es); foreach(l, targrels) { rInfo = (ResultRelInfo *) lfirst(l); - report_triggers(rInfo, show_relname, es->str); + report_triggers(rInfo, show_relname, es); } + + ExplainCloseGroup("Triggers", "Triggers", false, es); } /* @@ -344,8 +439,16 @@ ExplainOnePlan(PlannedStmt *plannedstmt, ExplainState *es, totaltime += elapsed_time(&starttime); if (es->analyze) - appendStringInfo(es->str, "Total runtime: %.3f ms\n", - 1000.0 * totaltime); + { + if (es->format == EXPLAIN_FORMAT_TEXT) + appendStringInfo(es->str, "Total runtime: %.3f ms\n", + 1000.0 * totaltime); + else + ExplainPropertyFloat("Total Runtime", 1000.0 * totaltime, + 3, es); + } + + ExplainCloseGroup("Query", NULL, true, es); } /* @@ -365,7 +468,7 @@ ExplainPrintPlan(ExplainState *es, QueryDesc *queryDesc) es->pstmt = queryDesc->plannedstmt; es->rtable = queryDesc->plannedstmt->rtable; ExplainNode(queryDesc->plannedstmt->planTree, queryDesc->planstate, - NULL, 0, es); + NULL, NULL, NULL, es); } /* @@ -373,7 +476,7 @@ ExplainPrintPlan(ExplainState *es, QueryDesc *queryDesc) * report execution stats for a single relation's triggers */ static void -report_triggers(ResultRelInfo *rInfo, bool show_relname, StringInfo buf) +report_triggers(ResultRelInfo *rInfo, bool show_relname, ExplainState *es) { int nt; @@ -383,7 +486,8 @@ report_triggers(ResultRelInfo *rInfo, bool show_relname, StringInfo buf) { Trigger *trig = rInfo->ri_TrigDesc->triggers + nt; Instrumentation *instr = rInfo->ri_TrigInstrument + nt; - char *conname; + char *relname; + char *conname = NULL; /* Must clean up instrumentation state */ InstrEndLoop(instr); @@ -395,21 +499,44 @@ report_triggers(ResultRelInfo *rInfo, bool show_relname, StringInfo buf) if (instr->ntuples == 0) continue; - if (OidIsValid(trig->tgconstraint) && - (conname = get_constraint_name(trig->tgconstraint)) != NULL) + ExplainOpenGroup("Trigger", NULL, true, es); + + relname = RelationGetRelationName(rInfo->ri_RelationDesc); + if (OidIsValid(trig->tgconstraint)) + conname = get_constraint_name(trig->tgconstraint); + + /* + * In text format, we avoid printing both the trigger name and the + * constraint name unless VERBOSE is specified. In non-text + * formats we just print everything. + */ + if (es->format == EXPLAIN_FORMAT_TEXT) { - appendStringInfo(buf, "Trigger for constraint %s", conname); - pfree(conname); + if (es->verbose || conname == NULL) + appendStringInfo(es->str, "Trigger %s", trig->tgname); + else + appendStringInfoString(es->str, "Trigger"); + if (conname) + appendStringInfo(es->str, " for constraint %s", conname); + if (show_relname) + appendStringInfo(es->str, " on %s", relname); + appendStringInfo(es->str, ": time=%.3f calls=%.0f\n", + 1000.0 * instr->total, instr->ntuples); } else - appendStringInfo(buf, "Trigger %s", trig->tgname); + { + ExplainPropertyText("Trigger Name", trig->tgname, es); + if (conname) + ExplainPropertyText("Constraint Name", conname, es); + ExplainPropertyText("Relation", relname, es); + ExplainPropertyFloat("Time", 1000.0 * instr->total, 3, es); + ExplainPropertyFloat("Calls", instr->ntuples, 0, es); + } - if (show_relname) - appendStringInfo(buf, " on %s", - RelationGetRelationName(rInfo->ri_RelationDesc)); + if (conname) + pfree(conname); - appendStringInfo(buf, ": time=%.3f calls=%.0f\n", - 1000.0 * instr->total, instr->ntuples); + ExplainCloseGroup("Trigger", NULL, true, es); } } @@ -426,7 +553,7 @@ elapsed_time(instr_time *starttime) /* * ExplainNode - - * converts a Plan node into ascii string and appends it to es->str + * Appends a description of the Plan node to es->str * * planstate points to the executor state node corresponding to the plan node. * We need this to get at the instrumentation data (if any) as well as the @@ -436,253 +563,222 @@ elapsed_time(instr_time *starttime) * side of a join with the current node. This is only interesting for * deciphering runtime keys of an inner indexscan. * - * If indent is positive, we indent the plan output accordingly and put "->" - * in front of it. This should only happen for child plan nodes. + * relationship describes the relationship of this plan node to its parent + * (eg, "Outer", "Inner"); it can be null at top level. plan_name is an + * optional name to be attached to the node. + * + * In text format, es->indent is controlled in this function since we only + * want it to change at Plan-node boundaries. In non-text formats, es->indent + * corresponds to the nesting depth of logical output groups, and therefore + * is controlled by ExplainOpenGroup/ExplainCloseGroup. */ static void ExplainNode(Plan *plan, PlanState *planstate, Plan *outer_plan, - int indent, ExplainState *es) + const char *relationship, const char *plan_name, + ExplainState *es) { - const char *pname; + const char *pname; /* node type name for text output */ + const char *sname; /* node type name for non-text output */ + const char *strategy = NULL; + int save_indent = es->indent; + bool haschildren; - if (indent) - { - Assert(indent >= 2); - appendStringInfoSpaces(es->str, 2 * indent - 4); - appendStringInfoString(es->str, "-> "); - } - - if (plan == NULL) - { - appendStringInfoChar(es->str, '\n'); - return; - } + Assert(plan); switch (nodeTag(plan)) { case T_Result: - pname = "Result"; + pname = sname = "Result"; break; case T_Append: - pname = "Append"; + pname = sname = "Append"; break; case T_RecursiveUnion: - pname = "Recursive Union"; + pname = sname = "Recursive Union"; break; case T_BitmapAnd: - pname = "BitmapAnd"; + pname = sname = "BitmapAnd"; break; case T_BitmapOr: - pname = "BitmapOr"; + pname = sname = "BitmapOr"; break; case T_NestLoop: - switch (((NestLoop *) plan)->join.jointype) - { - case JOIN_INNER: - pname = "Nested Loop"; - break; - case JOIN_LEFT: - pname = "Nested Loop Left Join"; - break; - case JOIN_FULL: - pname = "Nested Loop Full Join"; - break; - case JOIN_RIGHT: - pname = "Nested Loop Right Join"; - break; - case JOIN_SEMI: - pname = "Nested Loop Semi Join"; - break; - case JOIN_ANTI: - pname = "Nested Loop Anti Join"; - break; - default: - pname = "Nested Loop ??? Join"; - break; - } + pname = sname = "Nested Loop"; break; case T_MergeJoin: - switch (((MergeJoin *) plan)->join.jointype) - { - case JOIN_INNER: - pname = "Merge Join"; - break; - case JOIN_LEFT: - pname = "Merge Left Join"; - break; - case JOIN_FULL: - pname = "Merge Full Join"; - break; - case JOIN_RIGHT: - pname = "Merge Right Join"; - break; - case JOIN_SEMI: - pname = "Merge Semi Join"; - break; - case JOIN_ANTI: - pname = "Merge Anti Join"; - break; - default: - pname = "Merge ??? Join"; - break; - } + pname = "Merge"; /* "Join" gets added by jointype switch */ + sname = "Merge Join"; break; case T_HashJoin: - switch (((HashJoin *) plan)->join.jointype) - { - case JOIN_INNER: - pname = "Hash Join"; - break; - case JOIN_LEFT: - pname = "Hash Left Join"; - break; - case JOIN_FULL: - pname = "Hash Full Join"; - break; - case JOIN_RIGHT: - pname = "Hash Right Join"; - break; - case JOIN_SEMI: - pname = "Hash Semi Join"; - break; - case JOIN_ANTI: - pname = "Hash Anti Join"; - break; - default: - pname = "Hash ??? Join"; - break; - } + pname = "Hash"; /* "Join" gets added by jointype switch */ + sname = "Hash Join"; break; case T_SeqScan: - pname = "Seq Scan"; + pname = sname = "Seq Scan"; break; case T_IndexScan: - pname = "Index Scan"; + pname = sname = "Index Scan"; break; case T_BitmapIndexScan: - pname = "Bitmap Index Scan"; + pname = sname = "Bitmap Index Scan"; break; case T_BitmapHeapScan: - pname = "Bitmap Heap Scan"; + pname = sname = "Bitmap Heap Scan"; break; case T_TidScan: - pname = "Tid Scan"; + pname = sname = "Tid Scan"; break; case T_SubqueryScan: - pname = "Subquery Scan"; + pname = sname = "Subquery Scan"; break; case T_FunctionScan: - pname = "Function Scan"; + pname = sname = "Function Scan"; break; case T_ValuesScan: - pname = "Values Scan"; + pname = sname = "Values Scan"; break; case T_CteScan: - pname = "CTE Scan"; + pname = sname = "CTE Scan"; break; case T_WorkTableScan: - pname = "WorkTable Scan"; + pname = sname = "WorkTable Scan"; break; case T_Material: - pname = "Materialize"; + pname = sname = "Materialize"; break; case T_Sort: - pname = "Sort"; + pname = sname = "Sort"; break; case T_Group: - pname = "Group"; + pname = sname = "Group"; break; case T_Agg: + sname = "Aggregate"; switch (((Agg *) plan)->aggstrategy) { case AGG_PLAIN: pname = "Aggregate"; + strategy = "Plain"; break; case AGG_SORTED: pname = "GroupAggregate"; + strategy = "Sorted"; break; case AGG_HASHED: pname = "HashAggregate"; + strategy = "Hashed"; break; default: pname = "Aggregate ???"; + strategy = "???"; break; } break; case T_WindowAgg: - pname = "WindowAgg"; + pname = sname = "WindowAgg"; break; case T_Unique: - pname = "Unique"; + pname = sname = "Unique"; break; case T_SetOp: + sname = "SetOp"; switch (((SetOp *) plan)->strategy) { case SETOP_SORTED: - switch (((SetOp *) plan)->cmd) - { - case SETOPCMD_INTERSECT: - pname = "SetOp Intersect"; - break; - case SETOPCMD_INTERSECT_ALL: - pname = "SetOp Intersect All"; - break; - case SETOPCMD_EXCEPT: - pname = "SetOp Except"; - break; - case SETOPCMD_EXCEPT_ALL: - pname = "SetOp Except All"; - break; - default: - pname = "SetOp ???"; - break; - } + pname = "SetOp"; + strategy = "Sorted"; break; case SETOP_HASHED: - switch (((SetOp *) plan)->cmd) - { - case SETOPCMD_INTERSECT: - pname = "HashSetOp Intersect"; - break; - case SETOPCMD_INTERSECT_ALL: - pname = "HashSetOp Intersect All"; - break; - case SETOPCMD_EXCEPT: - pname = "HashSetOp Except"; - break; - case SETOPCMD_EXCEPT_ALL: - pname = "HashSetOp Except All"; - break; - default: - pname = "HashSetOp ???"; - break; - } + pname = "HashSetOp"; + strategy = "Hashed"; break; default: pname = "SetOp ???"; + strategy = "???"; break; } break; case T_Limit: - pname = "Limit"; + pname = sname = "Limit"; break; case T_Hash: - pname = "Hash"; + pname = sname = "Hash"; break; default: - pname = "???"; + pname = sname = "???"; break; } - appendStringInfoString(es->str, pname); + ExplainOpenGroup("Plan", + relationship ? NULL : "Plan", + true, es); + + if (es->format == EXPLAIN_FORMAT_TEXT) + { + if (plan_name) + { + appendStringInfoSpaces(es->str, es->indent * 2); + appendStringInfo(es->str, "%s\n", plan_name); + es->indent++; + } + if (es->indent) + { + appendStringInfoSpaces(es->str, es->indent * 2); + appendStringInfoString(es->str, "-> "); + es->indent += 2; + } + appendStringInfoString(es->str, pname); + es->indent++; + } + else + { + ExplainPropertyText("Node Type", sname, es); + if (strategy) + ExplainPropertyText("Strategy", strategy, es); + if (relationship) + ExplainPropertyText("Parent Relationship", relationship, es); + if (plan_name) + ExplainPropertyText("Subplan Name", plan_name, es); + } + switch (nodeTag(plan)) { case T_IndexScan: - if (ScanDirectionIsBackward(((IndexScan *) plan)->indexorderdir)) - appendStringInfoString(es->str, " Backward"); - appendStringInfo(es->str, " using %s", - explain_get_index_name(((IndexScan *) plan)->indexid)); + { + IndexScan *indexscan = (IndexScan *) plan; + const char *indexname = + explain_get_index_name(indexscan->indexid); + + if (es->format == EXPLAIN_FORMAT_TEXT) + { + if (ScanDirectionIsBackward(indexscan->indexorderdir)) + appendStringInfoString(es->str, " Backward"); + appendStringInfo(es->str, " using %s", indexname); + } + else + { + const char *scandir; + + switch (indexscan->indexorderdir) + { + case BackwardScanDirection: + scandir = "Backward"; + break; + case NoMovementScanDirection: + scandir = "NoMovement"; + break; + case ForwardScanDirection: + scandir = "Forward"; + break; + default: + scandir = "???"; + break; + } + ExplainPropertyText("Scan Direction", scandir, es); + ExplainPropertyText("Index Name", indexname, es); + } + } /* FALL THRU */ case T_SeqScan: case T_BitmapHeapScan: @@ -695,17 +791,110 @@ ExplainNode(Plan *plan, PlanState *planstate, ExplainScanTarget((Scan *) plan, es); break; case T_BitmapIndexScan: - appendStringInfo(es->str, " on %s", - explain_get_index_name(((BitmapIndexScan *) plan)->indexid)); + { + BitmapIndexScan *bitmapindexscan = (BitmapIndexScan *) plan; + const char *indexname = + explain_get_index_name(bitmapindexscan->indexid); + + if (es->format == EXPLAIN_FORMAT_TEXT) + appendStringInfo(es->str, " on %s", indexname); + else + ExplainPropertyText("Index Name", indexname, es); + } + break; + case T_NestLoop: + case T_MergeJoin: + case T_HashJoin: + { + const char *jointype; + + switch (((Join *) plan)->jointype) + { + case JOIN_INNER: + jointype = "Inner"; + break; + case JOIN_LEFT: + jointype = "Left"; + break; + case JOIN_FULL: + jointype = "Full"; + break; + case JOIN_RIGHT: + jointype = "Right"; + break; + case JOIN_SEMI: + jointype = "Semi"; + break; + case JOIN_ANTI: + jointype = "Anti"; + break; + default: + jointype = "???"; + break; + } + if (es->format == EXPLAIN_FORMAT_TEXT) + { + /* + * For historical reasons, the join type is interpolated + * into the node type name... + */ + if (((Join *) plan)->jointype != JOIN_INNER) + appendStringInfo(es->str, " %s Join", jointype); + else if (!IsA(plan, NestLoop)) + appendStringInfo(es->str, " Join"); + } + else + ExplainPropertyText("Join Type", jointype, es); + } + break; + case T_SetOp: + { + const char *setopcmd; + + switch (((SetOp *) plan)->cmd) + { + case SETOPCMD_INTERSECT: + setopcmd = "Intersect"; + break; + case SETOPCMD_INTERSECT_ALL: + setopcmd = "Intersect All"; + break; + case SETOPCMD_EXCEPT: + setopcmd = "Except"; + break; + case SETOPCMD_EXCEPT_ALL: + setopcmd = "Except All"; + break; + default: + setopcmd = "???"; + break; + } + if (es->format == EXPLAIN_FORMAT_TEXT) + appendStringInfo(es->str, " %s", setopcmd); + else + ExplainPropertyText("Command", setopcmd, es); + } break; default: break; } if (es->costs) - appendStringInfo(es->str, " (cost=%.2f..%.2f rows=%.0f width=%d)", - plan->startup_cost, plan->total_cost, - plan->plan_rows, plan->plan_width); + { + if (es->format == EXPLAIN_FORMAT_TEXT) + { + appendStringInfo(es->str, " (cost=%.2f..%.2f rows=%.0f width=%d)", + plan->startup_cost, plan->total_cost, + plan->plan_rows, plan->plan_width); + } + else + { + ExplainPropertyFloat("Startup Cost", plan->startup_cost, 2, es); + ExplainPropertyFloat("Total Cost", plan->total_cost, 2, es); + ExplainPropertyFloat("Plan Rows", plan->plan_rows, 0, es); + ExplainPropertyInteger("Plan Width", plan->plan_width, es); + } + } /* * We have to forcibly clean up the instrumentation state because we @@ -717,38 +906,60 @@ ExplainNode(Plan *plan, PlanState *planstate, if (planstate->instrument && planstate->instrument->nloops > 0) { double nloops = planstate->instrument->nloops; + double startup_sec = 1000.0 * planstate->instrument->startup / nloops; + double total_sec = 1000.0 * planstate->instrument->total / nloops; + double rows = planstate->instrument->ntuples / nloops; - appendStringInfo(es->str, - " (actual time=%.3f..%.3f rows=%.0f loops=%.0f)", - 1000.0 * planstate->instrument->startup / nloops, - 1000.0 * planstate->instrument->total / nloops, - planstate->instrument->ntuples / nloops, - planstate->instrument->nloops); + if (es->format == EXPLAIN_FORMAT_TEXT) + { + appendStringInfo(es->str, + " (actual time=%.3f..%.3f rows=%.0f loops=%.0f)", + startup_sec, total_sec, rows, nloops); + } + else + { + ExplainPropertyFloat("Actual Startup Time", startup_sec, 3, es); + ExplainPropertyFloat("Actual Total Time", total_sec, 3, es); + ExplainPropertyFloat("Actual Rows", rows, 0, es); + ExplainPropertyFloat("Actual Loops", nloops, 0, es); + } } else if (es->analyze) - appendStringInfoString(es->str, " (never executed)"); - appendStringInfoChar(es->str, '\n'); + { + if (es->format == EXPLAIN_FORMAT_TEXT) + appendStringInfo(es->str, " (never executed)"); + else + { + ExplainPropertyFloat("Actual Startup Time", 0.0, 3, es); + ExplainPropertyFloat("Actual Total Time", 0.0, 3, es); + ExplainPropertyFloat("Actual Rows", 0.0, 0, es); + ExplainPropertyFloat("Actual Loops", 0.0, 0, es); + } + } + + /* in text format, first line ends here */ + if (es->format == EXPLAIN_FORMAT_TEXT) + appendStringInfoChar(es->str, '\n'); /* target list */ if (es->verbose) - show_plan_tlist(plan, indent, es); + show_plan_tlist(plan, es); /* quals, sort keys, etc */ switch (nodeTag(plan)) { case T_IndexScan: show_scan_qual(((IndexScan *) plan)->indexqualorig, - "Index Cond", plan, outer_plan, indent, es); - show_scan_qual(plan->qual, - "Filter", plan, outer_plan, indent, es); + "Index Cond", plan, outer_plan, es); + show_scan_qual(plan->qual, "Filter", plan, outer_plan, es); break; case T_BitmapIndexScan: show_scan_qual(((BitmapIndexScan *) plan)->indexqualorig, - "Index Cond", plan, outer_plan, indent, es); + "Index Cond", plan, outer_plan, es); break; case T_BitmapHeapScan: show_scan_qual(((BitmapHeapScan *) plan)->bitmapqualorig, - "Recheck Cond", plan, outer_plan, indent, es); + "Recheck Cond", plan, outer_plan, es); /* FALL THRU */ case T_SeqScan: case T_FunctionScan: @@ -756,8 +967,7 @@ ExplainNode(Plan *plan, PlanState *planstate, case T_CteScan: case T_WorkTableScan: case T_SubqueryScan: - show_scan_qual(plan->qual, - "Filter", plan, outer_plan, indent, es); + show_scan_qual(plan->qual, "Filter", plan, outer_plan, es); break; case T_TidScan: { @@ -769,51 +979,61 @@ ExplainNode(Plan *plan, PlanState *planstate, if (list_length(tidquals) > 1) tidquals = list_make1(make_orclause(tidquals)); - show_scan_qual(tidquals, - "TID Cond", plan, outer_plan, indent, es); - show_scan_qual(plan->qual, - "Filter", plan, outer_plan, indent, es); + show_scan_qual(tidquals, "TID Cond", plan, outer_plan, es); + show_scan_qual(plan->qual, "Filter", plan, outer_plan, es); } break; case T_NestLoop: show_upper_qual(((NestLoop *) plan)->join.joinqual, - "Join Filter", plan, indent, es); - show_upper_qual(plan->qual, "Filter", plan, indent, es); + "Join Filter", plan, es); + show_upper_qual(plan->qual, "Filter", plan, es); break; case T_MergeJoin: show_upper_qual(((MergeJoin *) plan)->mergeclauses, - "Merge Cond", plan, indent, es); + "Merge Cond", plan, es); show_upper_qual(((MergeJoin *) plan)->join.joinqual, - "Join Filter", plan, indent, es); - show_upper_qual(plan->qual, "Filter", plan, indent, es); + "Join Filter", plan, es); + show_upper_qual(plan->qual, "Filter", plan, es); break; case T_HashJoin: show_upper_qual(((HashJoin *) plan)->hashclauses, - "Hash Cond", plan, indent, es); + "Hash Cond", plan, es); show_upper_qual(((HashJoin *) plan)->join.joinqual, - "Join Filter", plan, indent, es); - show_upper_qual(plan->qual, "Filter", plan, indent, es); + "Join Filter", plan, es); + show_upper_qual(plan->qual, "Filter", plan, es); break; case T_Agg: case T_Group: - show_upper_qual(plan->qual, "Filter", plan, indent, es); + show_upper_qual(plan->qual, "Filter", plan, es); break; case T_Sort: - show_sort_keys(plan, indent, es); - show_sort_info((SortState *) planstate, indent, es); + show_sort_keys(plan, es); + show_sort_info((SortState *) planstate, es); break; case T_Result: show_upper_qual((List *) ((Result *) plan)->resconstantqual, - "One-Time Filter", plan, indent, es); - show_upper_qual(plan->qual, "Filter", plan, indent, es); + "One-Time Filter", plan, es); + show_upper_qual(plan->qual, "Filter", plan, es); break; default: break; } + /* Get ready to display the child plans */ + haschildren = plan->initPlan || + outerPlan(plan) || + innerPlan(plan) || + IsA(plan, Append) || + IsA(plan, BitmapAnd) || + IsA(plan, BitmapOr) || + IsA(plan, SubqueryScan) || + planstate->subPlan; + if (haschildren) + ExplainOpenGroup("Plans", "Plans", false, es); + /* initPlan-s */ if (plan->initPlan) - ExplainSubPlans(planstate->initPlan, indent, es); + ExplainSubPlans(planstate->initPlan, "InitPlan", es); /* lefttree */ if (outerPlan(plan)) @@ -825,14 +1045,15 @@ ExplainNode(Plan *plan, PlanState *planstate, */ ExplainNode(outerPlan(plan), outerPlanState(planstate), IsA(plan, BitmapHeapScan) ? outer_plan : NULL, - indent + 3, es); + "Outer", NULL, es); } /* righttree */ if (innerPlan(plan)) { ExplainNode(innerPlan(plan), innerPlanState(planstate), - outerPlan(plan), indent + 3, es); + outerPlan(plan), + "Inner", NULL, es); } /* special child plans */ @@ -841,17 +1062,17 @@ ExplainNode(Plan *plan, PlanState *planstate, case T_Append: ExplainMemberNodes(((Append *) plan)->appendplans, ((AppendState *) planstate)->appendplans, - outer_plan, indent, es); + outer_plan, es); break; case T_BitmapAnd: ExplainMemberNodes(((BitmapAnd *) plan)->bitmapplans, ((BitmapAndState *) planstate)->bitmapplans, - outer_plan, indent, es); + outer_plan, es); break; case T_BitmapOr: ExplainMemberNodes(((BitmapOr *) plan)->bitmapplans, ((BitmapOrState *) planstate)->bitmapplans, - outer_plan, indent, es); + outer_plan, es); break; case T_SubqueryScan: { @@ -859,7 +1080,8 @@ ExplainNode(Plan *plan, PlanState *planstate, SubqueryScanState *subquerystate = (SubqueryScanState *) planstate; ExplainNode(subqueryscan->subplan, subquerystate->subplan, - NULL, indent + 3, es); + NULL, + "Subquery", NULL, es); } break; default: @@ -868,16 +1090,29 @@ ExplainNode(Plan *plan, PlanState *planstate, /* subPlan-s */ if (planstate->subPlan) - ExplainSubPlans(planstate->subPlan, indent, es); + ExplainSubPlans(planstate->subPlan, "SubPlan", es); + + /* end of child plans */ + if (haschildren) + ExplainCloseGroup("Plans", "Plans", false, es); + + /* in text format, undo whatever indentation we added */ + if (es->format == EXPLAIN_FORMAT_TEXT) + es->indent = save_indent; + + ExplainCloseGroup("Plan", + relationship ? NULL : "Plan", + true, es); } /* * Show the targetlist of a plan node */ static void -show_plan_tlist(Plan *plan, int indent, ExplainState *es) +show_plan_tlist(Plan *plan, ExplainState *es) { List *context; + List *result = NIL; bool useprefix; ListCell *lc; int i; @@ -899,10 +1134,6 @@ show_plan_tlist(Plan *plan, int indent, ExplainState *es) es->pstmt->subplans); useprefix = list_length(es->rtable) > 1; - /* Emit line prefix */ - appendStringInfoSpaces(es->str, indent * 2); - appendStringInfoString(es->str, " Output: "); - /* Deparse each non-junk result column */ i = 0; foreach(lc, plan->targetlist) @@ -911,14 +1142,13 @@ show_plan_tlist(Plan *plan, int indent, ExplainState *es) if (tle->resjunk) continue; - if (i++ > 0) - appendStringInfoString(es->str, ", "); - appendStringInfoString(es->str, - deparse_expression((Node *) tle->expr, context, + result = lappend(result, + deparse_expression((Node *) tle->expr, context, useprefix, false)); } - appendStringInfoChar(es->str, '\n'); + /* Print results */ + ExplainPropertyList("Output", result, es); } /* @@ -929,7 +1159,7 @@ show_plan_tlist(Plan *plan, int indent, ExplainState *es) */ static void show_qual(List *qual, const char *qlabel, Plan *plan, Plan *outer_plan, - int indent, bool useprefix, ExplainState *es) + bool useprefix, ExplainState *es) { List *context; Node *node; @@ -952,8 +1182,7 @@ show_qual(List *qual, const char *qlabel, Plan *plan, Plan *outer_plan, exprstr = deparse_expression(node, context, useprefix, false); /* And add to es->str */ - appendStringInfoSpaces(es->str, indent * 2); - appendStringInfo(es->str, " %s: %s\n", qlabel, exprstr); + ExplainPropertyText(qlabel, exprstr, es); } /* @@ -962,36 +1191,37 @@ show_qual(List *qual, const char *qlabel, Plan *plan, Plan *outer_plan, static void show_scan_qual(List *qual, const char *qlabel, Plan *scan_plan, Plan *outer_plan, - int indent, ExplainState *es) + ExplainState *es) { bool useprefix; - useprefix = (outer_plan != NULL || IsA(scan_plan, SubqueryScan)); - show_qual(qual, qlabel, scan_plan, outer_plan, indent, useprefix, es); + useprefix = (outer_plan != NULL || IsA(scan_plan, SubqueryScan) || + es->verbose); + show_qual(qual, qlabel, scan_plan, outer_plan, useprefix, es); } /* * Show a qualifier expression for an upper-level plan node */ static void -show_upper_qual(List *qual, const char *qlabel, Plan *plan, - int indent, ExplainState *es) +show_upper_qual(List *qual, const char *qlabel, Plan *plan, ExplainState *es) { bool useprefix; - useprefix = (list_length(es->rtable) > 1); - show_qual(qual, qlabel, plan, NULL, indent, useprefix, es); + useprefix = (list_length(es->rtable) > 1 || es->verbose); + show_qual(qual, qlabel, plan, NULL, useprefix, es); } /* * Show the sort keys for a Sort node. */ static void -show_sort_keys(Plan *sortplan, int indent, ExplainState *es) +show_sort_keys(Plan *sortplan, ExplainState *es) { int nkeys = ((Sort *) sortplan)->numCols; AttrNumber *keycols = ((Sort *) sortplan)->sortColIdx; List *context; + List *result = NIL; bool useprefix; int keyno; char *exprstr; @@ -999,15 +1229,12 @@ show_sort_keys(Plan *sortplan, int indent, ExplainState *es) if (nkeys <= 0) return; - appendStringInfoSpaces(es->str, indent * 2); - appendStringInfoString(es->str, " Sort Key: "); - /* Set up deparsing context */ context = deparse_context_for_plan((Node *) sortplan, NULL, es->rtable, es->pstmt->subplans); - useprefix = list_length(es->rtable) > 1; + useprefix = (list_length(es->rtable) > 1 || es->verbose); for (keyno = 0; keyno < nkeys; keyno++) { @@ -1020,31 +1247,41 @@ show_sort_keys(Plan *sortplan, int indent, ExplainState *es) /* Deparse the expression, showing any top-level cast */ exprstr = deparse_expression((Node *) target->expr, context, useprefix, true); - /* And add to es->str */ - if (keyno > 0) - appendStringInfoString(es->str, ", "); - appendStringInfoString(es->str, exprstr); + result = lappend(result, exprstr); } - appendStringInfoChar(es->str, '\n'); + ExplainPropertyList("Sort Key", result, es); } /* - * If it's EXPLAIN ANALYZE, show tuplesort explain info for a sort node + * If it's EXPLAIN ANALYZE, show tuplesort stats for a sort node */ static void -show_sort_info(SortState *sortstate, int indent, ExplainState *es) +show_sort_info(SortState *sortstate, ExplainState *es) { Assert(IsA(sortstate, SortState)); if (es->analyze && sortstate->sort_Done && sortstate->tuplesortstate != NULL) { - char *sortinfo; + Tuplesortstate *state = (Tuplesortstate *) sortstate->tuplesortstate; + const char *sortMethod; + const char *spaceType; + long spaceUsed; - sortinfo = tuplesort_explain((Tuplesortstate *) sortstate->tuplesortstate); - appendStringInfoSpaces(es->str, indent * 2); - appendStringInfo(es->str, " %s\n", sortinfo); - pfree(sortinfo); + tuplesort_get_stats(state, &sortMethod, &spaceType, &spaceUsed); + + if (es->format == EXPLAIN_FORMAT_TEXT) + { + appendStringInfoSpaces(es->str, es->indent * 2); + appendStringInfo(es->str, "Sort Method: %s %s: %ldkB\n", + sortMethod, spaceType, spaceUsed); + } + else + { + ExplainPropertyText("Sort Method", sortMethod, es); + ExplainPropertyLong("Sort Space Used", spaceUsed, es); + ExplainPropertyText("Sort Space Type", spaceType, es); + } } } @@ -1081,6 +1318,8 @@ static void ExplainScanTarget(Scan *plan, ExplainState *es) { char *objectname = NULL; + char *namespace = NULL; + const char *objecttag = NULL; RangeTblEntry *rte; if (plan->scanrelid <= 0) /* Is this still possible? */ @@ -1096,6 +1335,9 @@ ExplainScanTarget(Scan *plan, ExplainState *es) /* Assert it's on a real relation */ Assert(rte->rtekind == RTE_RELATION); objectname = get_rel_name(rte->relid); + if (es->verbose) + namespace = get_namespace_name(get_rel_namespace(rte->relid)); + objecttag = "Relation Name"; break; case T_FunctionScan: { @@ -1116,7 +1358,11 @@ ExplainScanTarget(Scan *plan, ExplainState *es) Oid funcid = ((FuncExpr *) funcexpr)->funcid; objectname = get_func_name(funcid); + if (es->verbose) + namespace = + get_namespace_name(get_func_namespace(funcid)); } + objecttag = "Function Name"; } break; case T_ValuesScan: @@ -1127,23 +1373,40 @@ ExplainScanTarget(Scan *plan, ExplainState *es) Assert(rte->rtekind == RTE_CTE); Assert(!rte->self_reference); objectname = rte->ctename; + objecttag = "CTE Name"; break; case T_WorkTableScan: /* Assert it's on a self-reference CTE */ Assert(rte->rtekind == RTE_CTE); Assert(rte->self_reference); objectname = rte->ctename; + objecttag = "CTE Name"; break; default: break; } - appendStringInfoString(es->str, " on"); - if (objectname != NULL) - appendStringInfo(es->str, " %s", quote_identifier(objectname)); - if (objectname == NULL || strcmp(rte->eref->aliasname, objectname) != 0) - appendStringInfo(es->str, " %s", - quote_identifier(rte->eref->aliasname)); + if (es->format == EXPLAIN_FORMAT_TEXT) + { + appendStringInfoString(es->str, " on"); + if (namespace != NULL) + appendStringInfo(es->str, " %s.%s", quote_identifier(namespace), + quote_identifier(objectname)); + else if (objectname != NULL) + appendStringInfo(es->str, " %s", quote_identifier(objectname)); + if (objectname == NULL || + strcmp(rte->eref->aliasname, objectname) != 0) + appendStringInfo(es->str, " %s", + quote_identifier(rte->eref->aliasname)); + } + else + { + if (objecttag != NULL && objectname != NULL) + ExplainPropertyText(objecttag, objectname, es); + if (namespace != NULL) + ExplainPropertyText("Schema", namespace, es); + ExplainPropertyText("Alias", rte->eref->aliasname, es); + } } /* @@ -1155,7 +1418,7 @@ ExplainScanTarget(Scan *plan, ExplainState *es) */ static void ExplainMemberNodes(List *plans, PlanState **planstate, Plan *outer_plan, - int indent, ExplainState *es) + ExplainState *es) { ListCell *lst; int j = 0; @@ -1165,7 +1428,9 @@ ExplainMemberNodes(List *plans, PlanState **planstate, Plan *outer_plan, Plan *subnode = (Plan *) lfirst(lst); ExplainNode(subnode, planstate[j], - outer_plan, indent + 3, es); + outer_plan, + "Member", NULL, + es); j++; } } @@ -1174,7 +1439,7 @@ ExplainMemberNodes(List *plans, PlanState **planstate, Plan *outer_plan, * Explain a list of SubPlans (or initPlans, which also use SubPlan nodes). */ static void -ExplainSubPlans(List *plans, int indent, ExplainState *es) +ExplainSubPlans(List *plans, const char *relationship, ExplainState *es) { ListCell *lst; @@ -1183,9 +1448,431 @@ ExplainSubPlans(List *plans, int indent, ExplainState *es) SubPlanState *sps = (SubPlanState *) lfirst(lst); SubPlan *sp = (SubPlan *) sps->xprstate.expr; - appendStringInfoSpaces(es->str, indent * 2); - appendStringInfo(es->str, " %s\n", sp->plan_name); ExplainNode(exec_subplan_get_plan(es->pstmt, sp), - sps->planstate, NULL, indent + 4, es); + sps->planstate, + NULL, + relationship, sp->plan_name, + es); + } +} + +/* + * Explain a property, such as sort keys or targets, that takes the form of + * a list of unlabeled items. "data" is a list of C strings. + */ +static void +ExplainPropertyList(const char *qlabel, List *data, ExplainState *es) +{ + ListCell *lc; + bool first = true; + + switch (es->format) + { + case EXPLAIN_FORMAT_TEXT: + appendStringInfoSpaces(es->str, es->indent * 2); + appendStringInfo(es->str, "%s: ", qlabel); + foreach(lc, data) + { + if (!first) + appendStringInfoString(es->str, ", "); + appendStringInfoString(es->str, (const char *) lfirst(lc)); + first = false; + } + appendStringInfoChar(es->str, '\n'); + break; + + case EXPLAIN_FORMAT_XML: + ExplainXMLTag(qlabel, X_OPENING, es); + foreach(lc, data) + { + char *str; + + appendStringInfoSpaces(es->str, es->indent * 2 + 2); + appendStringInfoString(es->str, ""); + 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; + } +} + +/* + * Explain a simple property. + * + * If "numeric" is true, the value is a number (or other value that + * doesn't need quoting in JSON). + * + * This usually should not be invoked directly, but via one of the datatype + * specific routines ExplainPropertyText, ExplainPropertyInteger, etc. + */ +static void +ExplainProperty(const char *qlabel, const char *value, bool numeric, + ExplainState *es) +{ + switch (es->format) + { + case EXPLAIN_FORMAT_TEXT: + appendStringInfoSpaces(es->str, es->indent * 2); + appendStringInfo(es->str, "%s: %s\n", qlabel, value); + break; + + case EXPLAIN_FORMAT_XML: + { + char *str; + + appendStringInfoSpaces(es->str, es->indent * 2); + ExplainXMLTag(qlabel, X_OPENING | X_NOWHITESPACE, es); + str = escape_xml(value); + appendStringInfoString(es->str, str); + pfree(str); + ExplainXMLTag(qlabel, X_CLOSING | X_NOWHITESPACE, es); + appendStringInfoChar(es->str, '\n'); + } + break; + + case EXPLAIN_FORMAT_JSON: + ExplainJSONLineEnding(es); + appendStringInfoSpaces(es->str, es->indent * 2); + escape_json(es->str, qlabel); + appendStringInfoString(es->str, ": "); + if (numeric) + appendStringInfoString(es->str, value); + else + escape_json(es->str, value); + break; + } +} + +/* + * Explain an integer-valued property. + */ +static void +ExplainPropertyInteger(const char *qlabel, int value, ExplainState *es) +{ + char buf[32]; + + snprintf(buf, sizeof(buf), "%d", value); + ExplainProperty(qlabel, buf, true, es); +} + +/* + * Explain a long-integer-valued property. + */ +static void +ExplainPropertyLong(const char *qlabel, long value, ExplainState *es) +{ + char buf[32]; + + snprintf(buf, sizeof(buf), "%ld", value); + ExplainProperty(qlabel, buf, true, es); +} + +/* + * Explain a float-valued property, using the specified number of + * fractional digits. + */ +static void +ExplainPropertyFloat(const char *qlabel, double value, int ndigits, + ExplainState *es) +{ + char buf[256]; + + snprintf(buf, sizeof(buf), "%.*f", ndigits, value); + ExplainProperty(qlabel, buf, true, es); +} + +/* + * Open a group of related objects. + * + * objtype is the type of the group object, labelname is its label within + * a containing object (if any). + * + * If labeled is true, the group members will be labeled properties, + * while if it's false, they'll be unlabeled objects. + */ +static void +ExplainOpenGroup(const char *objtype, const char *labelname, + bool labeled, ExplainState *es) +{ + switch (es->format) + { + case EXPLAIN_FORMAT_TEXT: + /* nothing to do */ + break; + + case EXPLAIN_FORMAT_XML: + ExplainXMLTag(objtype, X_OPENING, es); + es->indent++; + break; + + case EXPLAIN_FORMAT_JSON: + ExplainJSONLineEnding(es); + appendStringInfoSpaces(es->str, 2 * es->indent); + if (labelname) + { + escape_json(es->str, labelname); + appendStringInfoString(es->str, ": "); + } + appendStringInfoChar(es->str, labeled ? '{' : '['); + + /* + * In JSON format, the grouping_stack is an integer list. 0 means + * we've emitted nothing at this grouping level, 1 means we've + * emitted something (and so the next item needs a comma). + * See ExplainJSONLineEnding(). + */ + es->grouping_stack = lcons_int(0, es->grouping_stack); + es->indent++; + break; + } +} + +/* + * Close a group of related objects. + * Parameters must match the corresponding ExplainOpenGroup call. + */ +static void +ExplainCloseGroup(const char *objtype, const char *labelname, + bool labeled, ExplainState *es) +{ + switch (es->format) + { + case EXPLAIN_FORMAT_TEXT: + /* nothing to do */ + break; + + case EXPLAIN_FORMAT_XML: + es->indent--; + ExplainXMLTag(objtype, X_CLOSING, es); + break; + + case EXPLAIN_FORMAT_JSON: + es->indent--; + appendStringInfoChar(es->str, '\n'); + appendStringInfoSpaces(es->str, 2 * es->indent); + appendStringInfoChar(es->str, labeled ? '}' : ']'); + es->grouping_stack = list_delete_first(es->grouping_stack); + break; + } +} + +/* + * Emit a "dummy" group that never has any members. + * + * objtype is the type of the group object, labelname is its label within + * a containing object (if any). + */ +static void +ExplainDummyGroup(const char *objtype, const char *labelname, ExplainState *es) +{ + switch (es->format) + { + case EXPLAIN_FORMAT_TEXT: + /* nothing to do */ + break; + + case EXPLAIN_FORMAT_XML: + ExplainXMLTag(objtype, X_CLOSE_IMMEDIATE, es); + break; + + case EXPLAIN_FORMAT_JSON: + ExplainJSONLineEnding(es); + appendStringInfoSpaces(es->str, 2 * es->indent); + if (labelname) + { + escape_json(es->str, labelname); + appendStringInfoString(es->str, ": "); + } + escape_json(es->str, objtype); + break; + } +} + +/* + * Emit the start-of-output boilerplate. + * + * This is just enough different from processing a subgroup that we need + * a separate pair of subroutines. + */ +static void +ExplainBeginOutput(ExplainState *es) +{ + switch (es->format) + { + case EXPLAIN_FORMAT_TEXT: + /* nothing to do */ + break; + + case EXPLAIN_FORMAT_XML: + appendStringInfoString(es->str, + "\n"); + es->indent++; + break; + + case EXPLAIN_FORMAT_JSON: + /* top-level structure is an array of plans */ + appendStringInfoChar(es->str, '['); + es->grouping_stack = lcons_int(0, es->grouping_stack); + es->indent++; + break; + } +} + +/* + * Emit the end-of-output boilerplate. + */ +static void +ExplainEndOutput(ExplainState *es) +{ + switch (es->format) + { + case EXPLAIN_FORMAT_TEXT: + /* nothing to do */ + break; + + case EXPLAIN_FORMAT_XML: + es->indent--; + appendStringInfoString(es->str, ""); + break; + + case EXPLAIN_FORMAT_JSON: + es->indent--; + appendStringInfoString(es->str, "\n]"); + es->grouping_stack = list_delete_first(es->grouping_stack); + break; + } +} + +/* + * Put an appropriate separator between multiple plans + */ +void +ExplainSeparatePlans(ExplainState *es) +{ + switch (es->format) + { + case EXPLAIN_FORMAT_TEXT: + /* add a blank line */ + appendStringInfoChar(es->str, '\n'); + break; + + case EXPLAIN_FORMAT_XML: + /* nothing to do */ + break; + + case EXPLAIN_FORMAT_JSON: + /* must have a comma between array elements */ + appendStringInfoChar(es->str, ','); + break; + } +} + +/* + * Emit opening or closing XML tag. + * + * "flags" must contain X_OPENING, X_CLOSING, or X_CLOSE_IMMEDIATE. + * Optionally, OR in X_NOWHITESPACE to suppress the whitespace we'd normally + * add. + * + * XML tag names can't contain white space, so we replace any spaces in + * "tagname" with dashes. + */ +static void +ExplainXMLTag(const char *tagname, int flags, ExplainState *es) +{ + const char *s; + + if ((flags & X_NOWHITESPACE) == 0) + appendStringInfoSpaces(es->str, 2 * es->indent); + appendStringInfoCharMacro(es->str, '<'); + if ((flags & X_CLOSING) != 0) + appendStringInfoCharMacro(es->str, '/'); + for (s = tagname; *s; s++) + appendStringInfoCharMacro(es->str, (*s == ' ') ? '-' : *s); + if ((flags & X_CLOSE_IMMEDIATE) != 0) + appendStringInfoString(es->str, " /"); + appendStringInfoCharMacro(es->str, '>'); + if ((flags & X_NOWHITESPACE) == 0) + appendStringInfoCharMacro(es->str, '\n'); +} + +/* + * Emit a JSON line ending. + * + * JSON requires a comma after each property but the last. To facilitate this, + * in JSON format, the text emitted for each property begins just prior to the + * preceding line-break (and comma, if applicable). + */ +static void +ExplainJSONLineEnding(ExplainState *es) +{ + Assert(es->format == EXPLAIN_FORMAT_JSON); + if (linitial_int(es->grouping_stack) != 0) + appendStringInfoChar(es->str, ','); + else + linitial_int(es->grouping_stack) = 1; + appendStringInfoChar(es->str, '\n'); +} + +/* + * Produce a JSON string literal, properly escaping characters in the text. + */ +static void +escape_json(StringInfo buf, const char *str) +{ + const char *p; + + appendStringInfoCharMacro(buf, '\"'); + for (p = str; *p; p++) + { + switch (*p) + { + case '\b': + appendStringInfoString(buf, "\\b"); + break; + case '\f': + appendStringInfoString(buf, "\\f"); + break; + case '\n': + appendStringInfoString(buf, "\\n"); + break; + case '\r': + appendStringInfoString(buf, "\\r"); + break; + case '\t': + appendStringInfoString(buf, "\\t"); + break; + case '"': + appendStringInfoString(buf, "\\\""); + break; + case '\\': + appendStringInfoString(buf, "\\\\"); + break; + default: + if ((unsigned char) *p < ' ') + appendStringInfo(buf, "\\u%04x", (int) *p); + else + appendStringInfoCharMacro(buf, *p); + break; + } } + appendStringInfoCharMacro(buf, '\"'); } diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 4ee669691470e..56a16401f35ed 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -10,7 +10,7 @@ * Copyright (c) 2002-2009, PostgreSQL Global Development Group * * IDENTIFICATION - * $PostgreSQL: pgsql/src/backend/commands/prepare.c,v 1.98 2009/07/26 23:34:17 tgl Exp $ + * $PostgreSQL: pgsql/src/backend/commands/prepare.c,v 1.99 2009/08/10 05:46:50 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -685,9 +685,6 @@ ExplainExecuteQuery(ExecuteStmt *execstmt, ExplainState *es, foreach(p, plan_list) { PlannedStmt *pstmt = (PlannedStmt *) lfirst(p); - bool is_last_query; - - is_last_query = (lnext(p) == NULL); if (IsA(pstmt, PlannedStmt)) { @@ -714,9 +711,9 @@ ExplainExecuteQuery(ExecuteStmt *execstmt, ExplainState *es, /* No need for CommandCounterIncrement, as ExplainOnePlan did it */ - /* put a blank line between plans */ - if (!is_last_query) - appendStringInfoChar(es->str, '\n'); + /* Separate plans with an appropriate separator */ + if (lnext(p) != NULL) + ExplainSeparatePlans(es); } if (estate) diff --git a/src/backend/utils/adt/xml.c b/src/backend/utils/adt/xml.c index b92d41b93ccc8..80e113329cef6 100644 --- a/src/backend/utils/adt/xml.c +++ b/src/backend/utils/adt/xml.c @@ -7,7 +7,7 @@ * Portions Copyright (c) 1996-2009, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * - * $PostgreSQL: pgsql/src/backend/utils/adt/xml.c,v 1.92 2009/06/11 14:49:04 momjian Exp $ + * $PostgreSQL: pgsql/src/backend/utils/adt/xml.c,v 1.93 2009/08/10 05:46:50 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -1593,8 +1593,6 @@ map_xml_name_to_sql_identifier(char *name) char * map_sql_value_to_xml_value(Datum value, Oid type, bool xml_escape_strings) { - StringInfoData buf; - if (type_is_array(type)) { ArrayType *array; @@ -1605,6 +1603,7 @@ map_sql_value_to_xml_value(Datum value, Oid type, bool xml_escape_strings) int num_elems; Datum *elem_values; bool *elem_nulls; + StringInfoData buf; int i; array = DatumGetArrayTypeP(value); @@ -1638,8 +1637,7 @@ map_sql_value_to_xml_value(Datum value, Oid type, bool xml_escape_strings) { Oid typeOut; bool isvarlena; - char *p, - *str; + char *str; /* * Special XSD formatting for some data types @@ -1788,32 +1786,47 @@ map_sql_value_to_xml_value(Datum value, Oid type, bool xml_escape_strings) return str; /* otherwise, translate special characters as needed */ - initStringInfo(&buf); + return escape_xml(str); + } +} - for (p = str; *p; p++) + +/* + * Escape characters in text that have special meanings in XML. + * + * Returns a palloc'd string. + * + * NB: this is intentionally not dependent on libxml. + */ +char * +escape_xml(const char *str) +{ + StringInfoData buf; + const char *p; + + initStringInfo(&buf); + for (p = str; *p; p++) + { + switch (*p) { - switch (*p) - { - case '&': - appendStringInfoString(&buf, "&"); - break; - case '<': - appendStringInfoString(&buf, "<"); - break; - case '>': - appendStringInfoString(&buf, ">"); - break; - case '\r': - appendStringInfoString(&buf, " "); - break; - default: - appendStringInfoCharMacro(&buf, *p); - break; - } + case '&': + appendStringInfoString(&buf, "&"); + break; + case '<': + appendStringInfoString(&buf, "<"); + break; + case '>': + appendStringInfoString(&buf, ">"); + break; + case '\r': + appendStringInfoString(&buf, " "); + break; + default: + appendStringInfoCharMacro(&buf, *p); + break; } - - return buf.data; } + return buf.data; } diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c index 8247516bd14ba..0b50309bf3646 100644 --- a/src/backend/utils/cache/lsyscache.c +++ b/src/backend/utils/cache/lsyscache.c @@ -7,7 +7,7 @@ * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION - * $PostgreSQL: pgsql/src/backend/utils/cache/lsyscache.c,v 1.162 2009/06/11 14:49:05 momjian Exp $ + * $PostgreSQL: pgsql/src/backend/utils/cache/lsyscache.c,v 1.163 2009/08/10 05:46:50 tgl Exp $ * * NOTES * Eventually, the index information should go through here, too. @@ -1298,6 +1298,32 @@ get_func_name(Oid funcid) return NULL; } +/* + * get_func_namespace + * + * Returns the pg_namespace OID associated with a given function. + */ +Oid +get_func_namespace(Oid funcid) +{ + HeapTuple tp; + + tp = SearchSysCache(PROCOID, + ObjectIdGetDatum(funcid), + 0, 0, 0); + if (HeapTupleIsValid(tp)) + { + Form_pg_proc functup = (Form_pg_proc) GETSTRUCT(tp); + Oid result; + + result = functup->pronamespace; + ReleaseSysCache(tp); + return result; + } + else + return InvalidOid; +} + /* * get_func_rettype * Given procedure id, return the function's result type. diff --git a/src/backend/utils/sort/tuplesort.c b/src/backend/utils/sort/tuplesort.c index cb89f65a9144c..8279a07ec8b65 100644 --- a/src/backend/utils/sort/tuplesort.c +++ b/src/backend/utils/sort/tuplesort.c @@ -91,7 +91,7 @@ * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION - * $PostgreSQL: pgsql/src/backend/utils/sort/tuplesort.c,v 1.92 2009/08/01 20:59:17 tgl Exp $ + * $PostgreSQL: pgsql/src/backend/utils/sort/tuplesort.c,v 1.93 2009/08/10 05:46:50 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -2200,21 +2200,20 @@ tuplesort_restorepos(Tuplesortstate *state) } /* - * tuplesort_explain - produce a line of information for EXPLAIN ANALYZE + * tuplesort_get_stats - extract summary statistics * * This can be called after tuplesort_performsort() finishes to obtain * printable summary information about how the sort was performed. - * - * The result is a palloc'd string. + * spaceUsed is measured in kilobytes. */ -char * -tuplesort_explain(Tuplesortstate *state) +void +tuplesort_get_stats(Tuplesortstate *state, + const char **sortMethod, + const char **spaceType, + long *spaceUsed) { - char *result = (char *) palloc(100); - long spaceUsed; - /* - * Note: it might seem we should print both memory and disk usage for a + * Note: it might seem we should provide both memory and disk usage for a * disk-based sort. However, the current code doesn't track memory space * accurately once we have begun to return tuples to the caller (since we * don't account for pfree's the caller is expected to do), so we cannot @@ -2223,38 +2222,34 @@ tuplesort_explain(Tuplesortstate *state) * tell us how much is actually used in sortcontext? */ if (state->tapeset) - spaceUsed = LogicalTapeSetBlocks(state->tapeset) * (BLCKSZ / 1024); + { + *spaceType = "Disk"; + *spaceUsed = LogicalTapeSetBlocks(state->tapeset) * (BLCKSZ / 1024); + } else - spaceUsed = (state->allowedMem - state->availMem + 1023) / 1024; + { + *spaceType = "Memory"; + *spaceUsed = (state->allowedMem - state->availMem + 1023) / 1024; + } switch (state->status) { case TSS_SORTEDINMEM: if (state->boundUsed) - snprintf(result, 100, - "Sort Method: top-N heapsort Memory: %ldkB", - spaceUsed); + *sortMethod = "top-N heapsort"; else - snprintf(result, 100, - "Sort Method: quicksort Memory: %ldkB", - spaceUsed); + *sortMethod = "quicksort"; break; case TSS_SORTEDONTAPE: - snprintf(result, 100, - "Sort Method: external sort Disk: %ldkB", - spaceUsed); + *sortMethod = "external sort"; break; case TSS_FINALMERGE: - snprintf(result, 100, - "Sort Method: external merge Disk: %ldkB", - spaceUsed); + *sortMethod = "external merge"; break; default: - snprintf(result, 100, "sort still in progress"); + *sortMethod = "still in progress"; break; } - - return result; } diff --git a/src/include/commands/explain.h b/src/include/commands/explain.h index a5cc40367f8ed..fa2c8aac66887 100644 --- a/src/include/commands/explain.h +++ b/src/include/commands/explain.h @@ -6,7 +6,7 @@ * Portions Copyright (c) 1996-2009, PostgreSQL Global Development Group * Portions Copyright (c) 1994-5, Regents of the University of California * - * $PostgreSQL: pgsql/src/include/commands/explain.h,v 1.40 2009/07/26 23:34:18 tgl Exp $ + * $PostgreSQL: pgsql/src/include/commands/explain.h,v 1.41 2009/08/10 05:46:50 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -15,16 +15,26 @@ #include "executor/executor.h" +typedef enum ExplainFormat +{ + EXPLAIN_FORMAT_TEXT, + EXPLAIN_FORMAT_XML, + EXPLAIN_FORMAT_JSON +} ExplainFormat; + typedef struct ExplainState { StringInfo str; /* output buffer */ /* options */ - bool verbose; /* print plan targetlists */ + bool verbose; /* be verbose */ bool analyze; /* print actual times */ bool costs; /* print costs */ + ExplainFormat format; /* output format */ /* other states */ PlannedStmt *pstmt; /* top of plan */ List *rtable; /* range table */ + int indent; /* current indentation level */ + List *grouping_stack; /* format-specific grouping state */ } ExplainState; /* Hook for plugins to get control in ExplainOneQuery() */ @@ -54,4 +64,6 @@ extern void ExplainOnePlan(PlannedStmt *plannedstmt, ExplainState *es, extern void ExplainPrintPlan(ExplainState *es, QueryDesc *queryDesc); +extern void ExplainSeparatePlans(ExplainState *es); + #endif /* EXPLAIN_H */ diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h index 4428b22d59260..28d768e7bf83e 100644 --- a/src/include/utils/lsyscache.h +++ b/src/include/utils/lsyscache.h @@ -6,7 +6,7 @@ * Portions Copyright (c) 1996-2009, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * - * $PostgreSQL: pgsql/src/include/utils/lsyscache.h,v 1.128 2009/06/11 14:49:13 momjian Exp $ + * $PostgreSQL: pgsql/src/include/utils/lsyscache.h,v 1.129 2009/08/10 05:46:50 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -76,6 +76,7 @@ extern Oid get_negator(Oid opno); extern RegProcedure get_oprrest(Oid opno); extern RegProcedure get_oprjoin(Oid opno); extern char *get_func_name(Oid funcid); +extern Oid get_func_namespace(Oid funcid); extern Oid get_func_rettype(Oid funcid); extern int get_func_nargs(Oid funcid); extern Oid get_func_signature(Oid funcid, Oid **argtypes, int *nargs); diff --git a/src/include/utils/tuplesort.h b/src/include/utils/tuplesort.h index e351536d47189..8504e2e9b3a08 100644 --- a/src/include/utils/tuplesort.h +++ b/src/include/utils/tuplesort.h @@ -13,7 +13,7 @@ * Portions Copyright (c) 1996-2009, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * - * $PostgreSQL: pgsql/src/include/utils/tuplesort.h,v 1.33 2009/06/11 14:49:13 momjian Exp $ + * $PostgreSQL: pgsql/src/include/utils/tuplesort.h,v 1.34 2009/08/10 05:46:50 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -84,7 +84,10 @@ extern bool tuplesort_getdatum(Tuplesortstate *state, bool forward, extern void tuplesort_end(Tuplesortstate *state); -extern char *tuplesort_explain(Tuplesortstate *state); +extern void tuplesort_get_stats(Tuplesortstate *state, + const char **sortMethod, + const char **spaceType, + long *spaceUsed); extern int tuplesort_merge_order(long allowedMem); diff --git a/src/include/utils/xml.h b/src/include/utils/xml.h index b7d631c031bcc..b7829a84de021 100644 --- a/src/include/utils/xml.h +++ b/src/include/utils/xml.h @@ -7,7 +7,7 @@ * Portions Copyright (c) 1996-2009, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * - * $PostgreSQL: pgsql/src/include/utils/xml.h,v 1.28 2009/06/11 14:49:13 momjian Exp $ + * $PostgreSQL: pgsql/src/include/utils/xml.h,v 1.29 2009/08/10 05:46:50 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -70,6 +70,7 @@ extern xmltype *xmlpi(char *target, text *arg, bool arg_is_null, bool *result_is extern xmltype *xmlroot(xmltype *data, text *version, int standalone); extern bool xml_is_document(xmltype *arg); extern text *xmltotext_with_xmloption(xmltype *data, XmlOptionType xmloption_arg); +extern char *escape_xml(const char *str); extern char *map_sql_identifier_to_xml_name(char *ident, bool fully_escaped, bool escape_period); extern char *map_xml_name_to_sql_identifier(char *name);