diff --git a/contrib/pg_stat_statements/pg_stat_statements.c b/contrib/pg_stat_statements/pg_stat_statements.c
index 3d1b99f502d2da9bfd58cb90de7f2b30659a6ec4..7eec86fb289b4a3fbccb6b3bfbcedcb52d13d6d2 100644
--- a/contrib/pg_stat_statements/pg_stat_statements.c
+++ b/contrib/pg_stat_statements/pg_stat_statements.c
@@ -292,7 +292,7 @@ static void pgss_ExecutorRun(QueryDesc *queryDesc,
 				 uint64 count);
 static void pgss_ExecutorFinish(QueryDesc *queryDesc);
 static void pgss_ExecutorEnd(QueryDesc *queryDesc);
-static void pgss_ProcessUtility(Node *parsetree, const char *queryString,
+static void pgss_ProcessUtility(PlannedStmt *pstmt, const char *queryString,
 					ProcessUtilityContext context, ParamListInfo params,
 					DestReceiver *dest, char *completionTag);
 static uint32 pgss_hash_fn(const void *key, Size keysize);
@@ -942,10 +942,12 @@ pgss_ExecutorEnd(QueryDesc *queryDesc)
  * ProcessUtility hook
  */
 static void
-pgss_ProcessUtility(Node *parsetree, const char *queryString,
+pgss_ProcessUtility(PlannedStmt *pstmt, const char *queryString,
 					ProcessUtilityContext context, ParamListInfo params,
 					DestReceiver *dest, char *completionTag)
 {
+	Node	   *parsetree = pstmt->utilityStmt;
+
 	/*
 	 * If it's an EXECUTE statement, we don't track it and don't increment the
 	 * nesting level.  This allows the cycles to be charged to the underlying
@@ -979,11 +981,11 @@ pgss_ProcessUtility(Node *parsetree, const char *queryString,
 		PG_TRY();
 		{
 			if (prev_ProcessUtility)
-				prev_ProcessUtility(parsetree, queryString,
+				prev_ProcessUtility(pstmt, queryString,
 									context, params,
 									dest, completionTag);
 			else
-				standard_ProcessUtility(parsetree, queryString,
+				standard_ProcessUtility(pstmt, queryString,
 										context, params,
 										dest, completionTag);
 			nested_level--;
@@ -1044,11 +1046,11 @@ pgss_ProcessUtility(Node *parsetree, const char *queryString,
 	else
 	{
 		if (prev_ProcessUtility)
-			prev_ProcessUtility(parsetree, queryString,
+			prev_ProcessUtility(pstmt, queryString,
 								context, params,
 								dest, completionTag);
 		else
-			standard_ProcessUtility(parsetree, queryString,
+			standard_ProcessUtility(pstmt, queryString,
 									context, params,
 									dest, completionTag);
 	}
diff --git a/contrib/sepgsql/hooks.c b/contrib/sepgsql/hooks.c
index 15f40d83f11521001e2b9fcd960f09bf1efc32e3..93cc8debaa7e6ce6a3f9f354749e668819d2e67e 100644
--- a/contrib/sepgsql/hooks.c
+++ b/contrib/sepgsql/hooks.c
@@ -297,13 +297,14 @@ sepgsql_exec_check_perms(List *rangeTabls, bool abort)
  * break whole of the things if nefarious user would use.
  */
 static void
-sepgsql_utility_command(Node *parsetree,
+sepgsql_utility_command(PlannedStmt *pstmt,
 						const char *queryString,
 						ProcessUtilityContext context,
 						ParamListInfo params,
 						DestReceiver *dest,
 						char *completionTag)
 {
+	Node	   *parsetree = pstmt->utilityStmt;
 	sepgsql_context_info_t saved_context_info = sepgsql_context_info;
 	ListCell   *cell;
 
@@ -362,11 +363,11 @@ sepgsql_utility_command(Node *parsetree,
 		}
 
 		if (next_ProcessUtility_hook)
-			(*next_ProcessUtility_hook) (parsetree, queryString,
+			(*next_ProcessUtility_hook) (pstmt, queryString,
 										 context, params,
 										 dest, completionTag);
 		else
-			standard_ProcessUtility(parsetree, queryString,
+			standard_ProcessUtility(pstmt, queryString,
 									context, params,
 									dest, completionTag);
 	}
diff --git a/src/backend/catalog/pg_proc.c b/src/backend/catalog/pg_proc.c
index 6d8f17db2d14641618ac1c52e6374cf45c37d4b0..85cec0cb1cbeb1770cb4f120a6bc6bb188421aa6 100644
--- a/src/backend/catalog/pg_proc.c
+++ b/src/backend/catalog/pg_proc.c
@@ -934,7 +934,7 @@ fmgr_sql_validator(PG_FUNCTION_ARGS)
 			querytree_list = NIL;
 			foreach(lc, raw_parsetree_list)
 			{
-				Node	   *parsetree = (Node *) lfirst(lc);
+				RawStmt    *parsetree = (RawStmt *) lfirst(lc);
 				List	   *querytree_sublist;
 
 				querytree_sublist = pg_analyze_and_rewrite_params(parsetree,
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
index f56b2ac49b0c5c3fd4b7dccb7a0c4da33c2ec58c..1fd2162794832c76d8c2f0e275724d0cd7cf4b07 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -287,13 +287,13 @@ static const char BinarySignature[11] = "PGCOPY\n\377\r\n\0";
 
 
 /* non-export function prototypes */
-static CopyState BeginCopy(ParseState *pstate, bool is_from, Relation rel, Node *raw_query,
-		  const Oid queryRelId, List *attnamelist,
+static CopyState BeginCopy(ParseState *pstate, bool is_from, Relation rel,
+		  RawStmt *raw_query, Oid queryRelId, List *attnamelist,
 		  List *options);
 static void EndCopy(CopyState cstate);
 static void ClosePipeToProgram(CopyState cstate);
-static CopyState BeginCopyTo(ParseState *pstate, Relation rel, Node *query,
-			const Oid queryRelId, const char *filename, bool is_program,
+static CopyState BeginCopyTo(ParseState *pstate, Relation rel, RawStmt *query,
+			Oid queryRelId, const char *filename, bool is_program,
 			List *attnamelist, List *options);
 static void EndCopyTo(CopyState cstate);
 static uint64 DoCopyTo(CopyState cstate);
@@ -770,15 +770,17 @@ CopyLoadRawBuf(CopyState cstate)
  * Do not allow the copy if user doesn't have proper permission to access
  * the table or the specifically requested columns.
  */
-Oid
-DoCopy(ParseState *pstate, const CopyStmt *stmt, uint64 *processed)
+void
+DoCopy(ParseState *pstate, const CopyStmt *stmt,
+	   int stmt_location, int stmt_len,
+	   uint64 *processed)
 {
 	CopyState	cstate;
 	bool		is_from = stmt->is_from;
 	bool		pipe = (stmt->filename == NULL);
 	Relation	rel;
 	Oid			relid;
-	Node	   *query = NULL;
+	RawStmt    *query = NULL;
 	List	   *range_table = NIL;
 
 	/* Disallow COPY to/from file or program except to superusers. */
@@ -929,7 +931,10 @@ DoCopy(ParseState *pstate, const CopyStmt *stmt, uint64 *processed)
 			select->targetList = targetList;
 			select->fromClause = list_make1(from);
 
-			query = (Node *) select;
+			query = makeNode(RawStmt);
+			query->stmt = (Node *) select;
+			query->stmt_location = stmt_location;
+			query->stmt_len = stmt_len;
 
 			/*
 			 * Close the relation for now, but keep the lock on it to prevent
@@ -945,7 +950,11 @@ DoCopy(ParseState *pstate, const CopyStmt *stmt, uint64 *processed)
 	{
 		Assert(stmt->query);
 
-		query = stmt->query;
+		query = makeNode(RawStmt);
+		query->stmt = stmt->query;
+		query->stmt_location = stmt_location;
+		query->stmt_len = stmt_len;
+
 		relid = InvalidOid;
 		rel = NULL;
 	}
@@ -981,8 +990,6 @@ DoCopy(ParseState *pstate, const CopyStmt *stmt, uint64 *processed)
 	 */
 	if (rel != NULL)
 		heap_close(rel, (is_from ? NoLock : AccessShareLock));
-
-	return relid;
 }
 
 /*
@@ -1364,8 +1371,8 @@ static CopyState
 BeginCopy(ParseState *pstate,
 		  bool is_from,
 		  Relation rel,
-		  Node *raw_query,
-		  const Oid queryRelId,
+		  RawStmt *raw_query,
+		  Oid queryRelId,
 		  List *attnamelist,
 		  List *options)
 {
@@ -1456,7 +1463,7 @@ BeginCopy(ParseState *pstate,
 		 * function and is executed repeatedly.  (See also the same hack in
 		 * DECLARE CURSOR and PREPARE.)  XXX FIXME someday.
 		 */
-		rewritten = pg_analyze_and_rewrite((Node *) copyObject(raw_query),
+		rewritten = pg_analyze_and_rewrite((RawStmt *) copyObject(raw_query),
 										   pstate->p_sourcetext, NULL, 0);
 
 		/* check that we got back something we can work with */
@@ -1747,8 +1754,8 @@ EndCopy(CopyState cstate)
 static CopyState
 BeginCopyTo(ParseState *pstate,
 			Relation rel,
-			Node *query,
-			const Oid queryRelId,
+			RawStmt *query,
+			Oid queryRelId,
 			const char *filename,
 			bool is_program,
 			List *attnamelist,
diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c
index 57ef9817653a1e9bed718c2b3053a5ee4115201c..cee3b4d50b5d5a906c24c000eec005bcd7ed202c 100644
--- a/src/backend/commands/createas.c
+++ b/src/backend/commands/createas.c
@@ -326,7 +326,7 @@ ExecCreateTableAs(CreateTableAsStmt *stmt, const char *queryString,
 		query = (Query *) linitial(rewritten);
 		Assert(query->commandType == CMD_SELECT);
 
-		/* plan the query */
+		/* plan the query --- note we disallow parallelism */
 		plan = pg_plan_query(query, 0, params);
 
 		/*
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index c762fb07d4db0ee3c5f1984e20915e827813910e..ee7046c47b922e059b7d191dbaf6872ed82bea9a 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -53,7 +53,8 @@ explain_get_index_name_hook_type explain_get_index_name_hook = NULL;
 #define X_CLOSE_IMMEDIATE 2
 #define X_NOWHITESPACE 4
 
-static void ExplainOneQuery(Query *query, IntoClause *into, ExplainState *es,
+static void ExplainOneQuery(Query *query, int cursorOptions,
+				IntoClause *into, ExplainState *es,
 				const char *queryString, ParamListInfo params);
 static void report_triggers(ResultRelInfo *rInfo, bool show_relname,
 				ExplainState *es);
@@ -245,7 +246,8 @@ ExplainQuery(ParseState *pstate, ExplainStmt *stmt, const char *queryString,
 		/* Explain every plan */
 		foreach(l, rewritten)
 		{
-			ExplainOneQuery((Query *) lfirst(l), NULL, es,
+			ExplainOneQuery((Query *) lfirst(l),
+							CURSOR_OPT_PARALLEL_OK, NULL, es,
 							queryString, params);
 
 			/* Separate plans with an appropriate separator */
@@ -329,7 +331,8 @@ ExplainResultDesc(ExplainStmt *stmt)
  * "into" is NULL unless we are explaining the contents of a CreateTableAsStmt.
  */
 static void
-ExplainOneQuery(Query *query, IntoClause *into, ExplainState *es,
+ExplainOneQuery(Query *query, int cursorOptions,
+				IntoClause *into, ExplainState *es,
 				const char *queryString, ParamListInfo params)
 {
 	/* planner will not cope with utility statements */
@@ -341,7 +344,8 @@ ExplainOneQuery(Query *query, IntoClause *into, ExplainState *es,
 
 	/* if an advisor plugin is present, let it manage things */
 	if (ExplainOneQuery_hook)
-		(*ExplainOneQuery_hook) (query, into, es, queryString, params);
+		(*ExplainOneQuery_hook) (query, cursorOptions, into, es,
+								 queryString, params);
 	else
 	{
 		PlannedStmt *plan;
@@ -351,7 +355,7 @@ ExplainOneQuery(Query *query, IntoClause *into, ExplainState *es,
 		INSTR_TIME_SET_CURRENT(planstart);
 
 		/* plan the query */
-		plan = pg_plan_query(query, into ? 0 : CURSOR_OPT_PARALLEL_OK, params);
+		plan = pg_plan_query(query, cursorOptions, params);
 
 		INSTR_TIME_SET_CURRENT(planduration);
 		INSTR_TIME_SUBTRACT(planduration, planstart);
@@ -385,6 +389,8 @@ ExplainOneUtility(Node *utilityStmt, IntoClause *into, ExplainState *es,
 		 * We have to rewrite the contained SELECT and then pass it back to
 		 * ExplainOneQuery.  It's probably not really necessary to copy the
 		 * contained parsetree another time, but let's be safe.
+		 *
+		 * Like ExecCreateTableAs, disallow parallelism in the plan.
 		 */
 		CreateTableAsStmt *ctas = (CreateTableAsStmt *) utilityStmt;
 		List	   *rewritten;
@@ -392,7 +398,28 @@ ExplainOneUtility(Node *utilityStmt, IntoClause *into, ExplainState *es,
 		Assert(IsA(ctas->query, Query));
 		rewritten = QueryRewrite((Query *) copyObject(ctas->query));
 		Assert(list_length(rewritten) == 1);
-		ExplainOneQuery((Query *) linitial(rewritten), ctas->into, es,
+		ExplainOneQuery((Query *) linitial(rewritten),
+						0, ctas->into, es,
+						queryString, params);
+	}
+	else if (IsA(utilityStmt, DeclareCursorStmt))
+	{
+		/*
+		 * Likewise for DECLARE CURSOR.
+		 *
+		 * Notice that if you say EXPLAIN ANALYZE DECLARE CURSOR then we'll
+		 * actually run the query.  This is different from pre-8.3 behavior
+		 * but seems more useful than not running the query.  No cursor will
+		 * be created, however.
+		 */
+		DeclareCursorStmt *dcs = (DeclareCursorStmt *) utilityStmt;
+		List	   *rewritten;
+
+		Assert(IsA(dcs->query, Query));
+		rewritten = QueryRewrite((Query *) copyObject(dcs->query));
+		Assert(list_length(rewritten) == 1);
+		ExplainOneQuery((Query *) linitial(rewritten),
+						dcs->options, NULL, es,
 						queryString, params);
 	}
 	else if (IsA(utilityStmt, ExecuteStmt))
@@ -423,11 +450,6 @@ ExplainOneUtility(Node *utilityStmt, IntoClause *into, ExplainState *es,
  * "into" is NULL unless we are explaining the contents of a CreateTableAsStmt,
  * in which case executing the query should result in creating that table.
  *
- * 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.
@@ -444,6 +466,8 @@ ExplainOnePlan(PlannedStmt *plannedstmt, IntoClause *into, ExplainState *es,
 	int			eflags;
 	int			instrument_option = 0;
 
+	Assert(plannedstmt->commandType != CMD_UTILITY);
+
 	if (es->analyze && es->timing)
 		instrument_option |= INSTRUMENT_TIMER;
 	else if (es->analyze)
diff --git a/src/backend/commands/extension.c b/src/backend/commands/extension.c
index be521484d0895ee56ab610636cd0fe34da9b77a8..967b52a133fe3dfd4d91672ea043a35f8a0215d1 100644
--- a/src/backend/commands/extension.c
+++ b/src/backend/commands/extension.c
@@ -712,7 +712,7 @@ execute_sql_string(const char *sql, const char *filename)
 	 */
 	foreach(lc1, raw_parsetree_list)
 	{
-		Node	   *parsetree = (Node *) lfirst(lc1);
+		RawStmt    *parsetree = (RawStmt *) lfirst(lc1);
 		List	   *stmt_list;
 		ListCell   *lc2;
 
@@ -724,23 +724,17 @@ execute_sql_string(const char *sql, const char *filename)
 
 		foreach(lc2, stmt_list)
 		{
-			Node	   *stmt = (Node *) lfirst(lc2);
-
-			if (IsA(stmt, TransactionStmt))
-				ereport(ERROR,
-						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-						 errmsg("transaction control statements are not allowed within an extension script")));
+			PlannedStmt *stmt = (PlannedStmt *) lfirst(lc2);
 
 			CommandCounterIncrement();
 
 			PushActiveSnapshot(GetTransactionSnapshot());
 
-			if (IsA(stmt, PlannedStmt) &&
-				((PlannedStmt *) stmt)->utilityStmt == NULL)
+			if (stmt->utilityStmt == NULL)
 			{
 				QueryDesc  *qdesc;
 
-				qdesc = CreateQueryDesc((PlannedStmt *) stmt,
+				qdesc = CreateQueryDesc(stmt,
 										sql,
 										GetActiveSnapshot(), NULL,
 										dest, NULL, 0);
@@ -754,6 +748,11 @@ execute_sql_string(const char *sql, const char *filename)
 			}
 			else
 			{
+				if (IsA(stmt->utilityStmt, TransactionStmt))
+					ereport(ERROR,
+							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+							 errmsg("transaction control statements are not allowed within an extension script")));
+
 				ProcessUtility(stmt,
 							   sql,
 							   PROCESS_UTILITY_QUERY,
@@ -1434,7 +1433,8 @@ CreateExtensionInternal(char *extensionName,
 			csstmt->authrole = NULL;	/* will be created by current user */
 			csstmt->schemaElts = NIL;
 			csstmt->if_not_exists = false;
-			CreateSchemaCommand(csstmt, NULL);
+			CreateSchemaCommand(csstmt, "(generated CREATE SCHEMA command)",
+								-1, -1);
 
 			/*
 			 * CreateSchemaCommand includes CommandCounterIncrement, so new
diff --git a/src/backend/commands/foreigncmds.c b/src/backend/commands/foreigncmds.c
index 06b4bc3ba9a05d0f76fa8ed4ab9c659d906c5878..476a023ec54c91f4eec31a8b7c54692d4ff97f26 100644
--- a/src/backend/commands/foreigncmds.c
+++ b/src/backend/commands/foreigncmds.c
@@ -1572,7 +1572,9 @@ ImportForeignSchema(ImportForeignSchemaStmt *stmt)
 		 */
 		foreach(lc2, raw_parsetree_list)
 		{
-			CreateForeignTableStmt *cstmt = lfirst(lc2);
+			RawStmt    *rs = (RawStmt *) lfirst(lc2);
+			CreateForeignTableStmt *cstmt = (CreateForeignTableStmt *) rs->stmt;
+			PlannedStmt *pstmt;
 
 			/*
 			 * Because we only allow CreateForeignTableStmt, we can skip parse
@@ -1593,8 +1595,16 @@ ImportForeignSchema(ImportForeignSchemaStmt *stmt)
 			/* Ensure creation schema is the one given in IMPORT statement */
 			cstmt->base.relation->schemaname = pstrdup(stmt->local_schema);
 
+			/* No planning needed, just make a wrapper PlannedStmt */
+			pstmt = makeNode(PlannedStmt);
+			pstmt->commandType = CMD_UTILITY;
+			pstmt->canSetTag = false;
+			pstmt->utilityStmt = (Node *) cstmt;
+			pstmt->stmt_location = rs->stmt_location;
+			pstmt->stmt_len = rs->stmt_len;
+
 			/* Execute statement */
-			ProcessUtility((Node *) cstmt,
+			ProcessUtility(pstmt,
 						   cmd,
 						   PROCESS_UTILITY_SUBCOMMAND, NULL,
 						   None_Receiver, NULL);
diff --git a/src/backend/commands/portalcmds.c b/src/backend/commands/portalcmds.c
index 71640b7748f31908b4baf0f1992a8504ffa44f29..1d3e39299b9d405751a96286b6c22201e48a129d 100644
--- a/src/backend/commands/portalcmds.c
+++ b/src/backend/commands/portalcmds.c
@@ -27,7 +27,9 @@
 #include "commands/portalcmds.h"
 #include "executor/executor.h"
 #include "executor/tstoreReceiver.h"
+#include "rewrite/rewriteHandler.h"
 #include "tcop/pquery.h"
+#include "tcop/tcopprot.h"
 #include "utils/memutils.h"
 #include "utils/snapmgr.h"
 
@@ -35,21 +37,18 @@
 /*
  * PerformCursorOpen
  *		Execute SQL DECLARE CURSOR command.
- *
- * The query has already been through parse analysis, rewriting, and planning.
- * When it gets here, it looks like a SELECT PlannedStmt, except that the
- * utilityStmt field is set.
  */
 void
-PerformCursorOpen(PlannedStmt *stmt, ParamListInfo params,
+PerformCursorOpen(DeclareCursorStmt *cstmt, ParamListInfo params,
 				  const char *queryString, bool isTopLevel)
 {
-	DeclareCursorStmt *cstmt = (DeclareCursorStmt *) stmt->utilityStmt;
+	Query	   *query = (Query *) cstmt->query;
+	List	   *rewritten;
+	PlannedStmt *plan;
 	Portal		portal;
 	MemoryContext oldContext;
 
-	if (cstmt == NULL || !IsA(cstmt, DeclareCursorStmt))
-		elog(ERROR, "PerformCursorOpen called for non-cursor query");
+	Assert(IsA(query, Query));	/* else parse analysis wasn't done */
 
 	/*
 	 * Disallow empty-string cursor name (conflicts with protocol-level
@@ -68,6 +67,32 @@ PerformCursorOpen(PlannedStmt *stmt, ParamListInfo params,
 	if (!(cstmt->options & CURSOR_OPT_HOLD))
 		RequireTransactionChain(isTopLevel, "DECLARE CURSOR");
 
+	/*
+	 * Parse analysis was done already, but we still have to run the rule
+	 * rewriter.  We do not do AcquireRewriteLocks: we assume the query either
+	 * came straight from the parser, or suitable locks were acquired by
+	 * plancache.c.
+	 *
+	 * Because the rewriter and planner tend to scribble on the input, we make
+	 * a preliminary copy of the source querytree.  This prevents problems in
+	 * the case that the DECLARE CURSOR is in a portal or plpgsql function and
+	 * is executed repeatedly.  (See also the same hack in EXPLAIN and
+	 * PREPARE.)  XXX FIXME someday.
+	 */
+	rewritten = QueryRewrite((Query *) copyObject(query));
+
+	/* SELECT should never rewrite to more or less than one query */
+	if (list_length(rewritten) != 1)
+		elog(ERROR, "non-SELECT statement in DECLARE CURSOR");
+
+	query = (Query *) linitial(rewritten);
+
+	if (query->commandType != CMD_SELECT)
+		elog(ERROR, "non-SELECT statement in DECLARE CURSOR");
+
+	/* Plan the query, applying the specified options */
+	plan = pg_plan_query(query, cstmt->options, params);
+
 	/*
 	 * Create a portal and copy the plan and queryString into its memory.
 	 */
@@ -75,8 +100,7 @@ PerformCursorOpen(PlannedStmt *stmt, ParamListInfo params,
 
 	oldContext = MemoryContextSwitchTo(PortalGetHeapMemory(portal));
 
-	stmt = copyObject(stmt);
-	stmt->utilityStmt = NULL;	/* make it look like plain SELECT */
+	plan = copyObject(plan);
 
 	queryString = pstrdup(queryString);
 
@@ -84,7 +108,7 @@ PerformCursorOpen(PlannedStmt *stmt, ParamListInfo params,
 					  NULL,
 					  queryString,
 					  "SELECT", /* cursor's query is always a SELECT */
-					  list_make1(stmt),
+					  list_make1(plan),
 					  NULL);
 
 	/*----------
@@ -111,8 +135,8 @@ PerformCursorOpen(PlannedStmt *stmt, ParamListInfo params,
 	portal->cursorOptions = cstmt->options;
 	if (!(portal->cursorOptions & (CURSOR_OPT_SCROLL | CURSOR_OPT_NO_SCROLL)))
 	{
-		if (stmt->rowMarks == NIL &&
-			ExecSupportsBackwardScan(stmt->planTree))
+		if (plan->rowMarks == NIL &&
+			ExecSupportsBackwardScan(plan->planTree))
 			portal->cursorOptions |= CURSOR_OPT_SCROLL;
 		else
 			portal->cursorOptions |= CURSOR_OPT_NO_SCROLL;
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index d768cf8dda39a3418bba29094551eb15af449cba..1ff41661a551c586b4198e3dc14bf3deb317f384 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -52,8 +52,10 @@ static Datum build_regtype_array(Oid *param_types, int num_params);
  * Implements the 'PREPARE' utility statement.
  */
 void
-PrepareQuery(PrepareStmt *stmt, const char *queryString)
+PrepareQuery(PrepareStmt *stmt, const char *queryString,
+			 int stmt_location, int stmt_len)
 {
+	RawStmt    *rawstmt;
 	CachedPlanSource *plansource;
 	Oid		   *argtypes = NULL;
 	int			nargs;
@@ -70,11 +72,23 @@ PrepareQuery(PrepareStmt *stmt, const char *queryString)
 				(errcode(ERRCODE_INVALID_PSTATEMENT_DEFINITION),
 				 errmsg("invalid statement name: must not be empty")));
 
+	/*
+	 * Need to wrap the contained statement in a RawStmt node to pass it to
+	 * parse analysis.
+	 *
+	 * Because parse analysis scribbles on the raw querytree, we must make a
+	 * copy to ensure we don't modify the passed-in tree.  FIXME someday.
+	 */
+	rawstmt = makeNode(RawStmt);
+	rawstmt->stmt = (Node *) copyObject(stmt->query);
+	rawstmt->stmt_location = stmt_location;
+	rawstmt->stmt_len = stmt_len;
+
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(stmt->query, queryString,
+	plansource = CreateCachedPlan(rawstmt, queryString,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -108,12 +122,8 @@ PrepareQuery(PrepareStmt *stmt, const char *queryString)
 	 * Analyze the statement using these parameter types (any parameters
 	 * passed in from above us will not be visible to it), allowing
 	 * information about unknown parameters to be deduced from context.
-	 *
-	 * Because parse analysis scribbles on the raw querytree, we must make a
-	 * copy to ensure we don't modify the passed-in tree.  FIXME someday.
 	 */
-	query = parse_analyze_varparams((Node *) copyObject(stmt->query),
-									queryString,
+	query = parse_analyze_varparams(rawstmt, queryString,
 									&argtypes, &nargs);
 
 	/*
@@ -256,9 +266,8 @@ ExecuteQuery(ExecuteStmt *stmt, IntoClause *intoClause,
 					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
 					 errmsg("prepared statement is not a SELECT")));
 		pstmt = (PlannedStmt *) linitial(plan_list);
-		if (!IsA(pstmt, PlannedStmt) ||
-			pstmt->commandType != CMD_SELECT ||
-			pstmt->utilityStmt != NULL)
+		Assert(IsA(pstmt, PlannedStmt));
+		if (pstmt->commandType != CMD_SELECT)
 			ereport(ERROR,
 					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
 					 errmsg("prepared statement is not a SELECT")));
@@ -664,10 +673,11 @@ ExplainExecuteQuery(ExecuteStmt *execstmt, IntoClause *into, ExplainState *es,
 	{
 		PlannedStmt *pstmt = (PlannedStmt *) lfirst(p);
 
-		if (IsA(pstmt, PlannedStmt))
+		Assert(IsA(pstmt, PlannedStmt));
+		if (pstmt->commandType != CMD_UTILITY)
 			ExplainOnePlan(pstmt, into, es, query_string, paramLI, NULL);
 		else
-			ExplainOneUtility((Node *) pstmt, into, es, query_string, paramLI);
+			ExplainOneUtility(pstmt->utilityStmt, into, es, query_string, paramLI);
 
 		/* No need for CommandCounterIncrement, as ExplainOnePlan did it */
 
diff --git a/src/backend/commands/schemacmds.c b/src/backend/commands/schemacmds.c
index c475613b1c5cd1cfc6b311991798589e03a3ad06..c3b37b2625901f1a7756b4332c37d856d830b6fc 100644
--- a/src/backend/commands/schemacmds.c
+++ b/src/backend/commands/schemacmds.c
@@ -40,9 +40,16 @@ static void AlterSchemaOwner_internal(HeapTuple tup, Relation rel, Oid newOwnerI
 
 /*
  * CREATE SCHEMA
+ *
+ * Note: caller should pass in location information for the whole
+ * CREATE SCHEMA statement, which in turn we pass down as the location
+ * of the component commands.  This comports with our general plan of
+ * reporting location/len for the whole command even when executing
+ * a subquery.
  */
 Oid
-CreateSchemaCommand(CreateSchemaStmt *stmt, const char *queryString)
+CreateSchemaCommand(CreateSchemaStmt *stmt, const char *queryString,
+					int stmt_location, int stmt_len)
 {
 	const char *schemaName = stmt->schemaname;
 	Oid			namespaceId;
@@ -172,14 +179,24 @@ CreateSchemaCommand(CreateSchemaStmt *stmt, const char *queryString)
 	foreach(parsetree_item, parsetree_list)
 	{
 		Node	   *stmt = (Node *) lfirst(parsetree_item);
+		PlannedStmt *wrapper;
+
+		/* need to make a wrapper PlannedStmt */
+		wrapper = makeNode(PlannedStmt);
+		wrapper->commandType = CMD_UTILITY;
+		wrapper->canSetTag = false;
+		wrapper->utilityStmt = stmt;
+		wrapper->stmt_location = stmt_location;
+		wrapper->stmt_len = stmt_len;
 
 		/* do this step */
-		ProcessUtility(stmt,
+		ProcessUtility(wrapper,
 					   queryString,
 					   PROCESS_UTILITY_SUBCOMMAND,
 					   NULL,
 					   None_Receiver,
 					   NULL);
+
 		/* make sure later steps can see the object created here */
 		CommandCounterIncrement();
 	}
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index f913e87bc8b9d32d87e4b6ff28294fcbd2695568..e633a50dd2db4f08f3edb7b05273bd9648e4992b 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -9285,7 +9285,8 @@ ATPostAlterTypeParse(Oid oldId, Oid oldRelId, Oid refRelId, char *cmd,
 	querytree_list = NIL;
 	foreach(list_item, raw_parsetree_list)
 	{
-		Node	   *stmt = (Node *) lfirst(list_item);
+		RawStmt    *rs = (RawStmt *) lfirst(list_item);
+		Node	   *stmt = rs->stmt;
 
 		if (IsA(stmt, IndexStmt))
 			querytree_list = lappend(querytree_list,
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index 61666ad19238cc4bc7f8e5a2658f0355471229da..b404d1ea16ff99ef1198eb208aab28f913fa00cc 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -1078,6 +1078,7 @@ ConvertTriggerToFK(CreateTrigStmt *stmt, Oid funcoid)
 		AlterTableStmt *atstmt = makeNode(AlterTableStmt);
 		AlterTableCmd *atcmd = makeNode(AlterTableCmd);
 		Constraint *fkcon = makeNode(Constraint);
+		PlannedStmt *wrapper = makeNode(PlannedStmt);
 
 		ereport(NOTICE,
 				(errmsg("converting trigger group into constraint \"%s\" %s",
@@ -1167,8 +1168,15 @@ ConvertTriggerToFK(CreateTrigStmt *stmt, Oid funcoid)
 		fkcon->skip_validation = false;
 		fkcon->initially_valid = true;
 
+		/* finally, wrap it in a dummy PlannedStmt */
+		wrapper->commandType = CMD_UTILITY;
+		wrapper->canSetTag = false;
+		wrapper->utilityStmt = (Node *) atstmt;
+		wrapper->stmt_location = -1;
+		wrapper->stmt_len = -1;
+
 		/* ... and execute it */
-		ProcessUtility((Node *) atstmt,
+		ProcessUtility(wrapper,
 					   "(generated ALTER TABLE ADD FOREIGN KEY command)",
 					   PROCESS_UTILITY_SUBCOMMAND, NULL,
 					   None_Receiver, NULL);
diff --git a/src/backend/commands/view.c b/src/backend/commands/view.c
index e4be5c533205bfb2a080dd7401fd23a410ecf2ec..1f008b075666342c0cbae3486c3fbc4ea9080d3a 100644
--- a/src/backend/commands/view.c
+++ b/src/backend/commands/view.c
@@ -414,8 +414,10 @@ UpdateRangeTableOfViewParse(Oid viewOid, Query *viewParse)
  *		Execute a CREATE VIEW command.
  */
 ObjectAddress
-DefineView(ViewStmt *stmt, const char *queryString)
+DefineView(ViewStmt *stmt, const char *queryString,
+		   int stmt_location, int stmt_len)
 {
+	RawStmt    *rawstmt;
 	Query	   *viewParse;
 	RangeVar   *view;
 	ListCell   *cell;
@@ -429,8 +431,12 @@ DefineView(ViewStmt *stmt, const char *queryString)
 	 * Since parse analysis scribbles on its input, copy the raw parse tree;
 	 * this ensures we don't corrupt a prepared statement, for example.
 	 */
-	viewParse = parse_analyze((Node *) copyObject(stmt->query),
-							  queryString, NULL, 0);
+	rawstmt = makeNode(RawStmt);
+	rawstmt->stmt = (Node *) copyObject(stmt->query);
+	rawstmt->stmt_location = stmt_location;
+	rawstmt->stmt_len = stmt_len;
+
+	viewParse = parse_analyze(rawstmt, queryString, NULL, 0);
 
 	/*
 	 * The grammar should ensure that the result is a single SELECT Query.
@@ -443,8 +449,7 @@ DefineView(ViewStmt *stmt, const char *queryString)
 		ereport(ERROR,
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("views must not contain SELECT INTO")));
-	if (viewParse->commandType != CMD_SELECT ||
-		viewParse->utilityStmt != NULL)
+	if (viewParse->commandType != CMD_SELECT)
 		elog(ERROR, "unexpected parse analysis result");
 
 	/*
diff --git a/src/backend/executor/execParallel.c b/src/backend/executor/execParallel.c
index 6cf62daab8a58e384f88f53127de1fa5b3087ca6..e01fe6da96492f46d034c9522f88062646437d1c 100644
--- a/src/backend/executor/execParallel.c
+++ b/src/backend/executor/execParallel.c
@@ -156,13 +156,15 @@ ExecSerializePlan(Plan *plan, EState *estate)
 	pstmt->planTree = plan;
 	pstmt->rtable = estate->es_range_table;
 	pstmt->resultRelations = NIL;
-	pstmt->utilityStmt = NULL;
 	pstmt->subplans = NIL;
 	pstmt->rewindPlanIDs = NULL;
 	pstmt->rowMarks = NIL;
 	pstmt->relationOids = NIL;
 	pstmt->invalItems = NIL;	/* workers can't replan anyway... */
 	pstmt->nParamExec = estate->es_plannedstmt->nParamExec;
+	pstmt->utilityStmt = NULL;
+	pstmt->stmt_location = -1;
+	pstmt->stmt_len = -1;
 
 	/* Return serialized copy of our dummy PlannedStmt. */
 	return nodeToString(pstmt);
diff --git a/src/backend/executor/functions.c b/src/backend/executor/functions.c
index 039defa7b85f681f5398838b7946fd9941ce9402..e4a1da4dbbf4b2be0475fedfcfe64be498f13094 100644
--- a/src/backend/executor/functions.c
+++ b/src/backend/executor/functions.c
@@ -66,7 +66,7 @@ typedef struct execution_state
 	ExecStatus	status;
 	bool		setsResult;		/* true if this query produces func's result */
 	bool		lazyEval;		/* true if should fetch one row at a time */
-	Node	   *stmt;			/* PlannedStmt or utility statement */
+	PlannedStmt *stmt;			/* plan for this query */
 	QueryDesc  *qd;				/* null unless status == RUN */
 } execution_state;
 
@@ -487,45 +487,56 @@ init_execution_state(List *queryTree_list,
 		foreach(lc2, qtlist)
 		{
 			Query	   *queryTree = (Query *) lfirst(lc2);
-			Node	   *stmt;
+			PlannedStmt *stmt;
 			execution_state *newes;
 
 			Assert(IsA(queryTree, Query));
 
 			/* Plan the query if needed */
 			if (queryTree->commandType == CMD_UTILITY)
-				stmt = queryTree->utilityStmt;
+			{
+				/* Utility commands require no planning. */
+				stmt = makeNode(PlannedStmt);
+				stmt->commandType = CMD_UTILITY;
+				stmt->canSetTag = queryTree->canSetTag;
+				stmt->utilityStmt = queryTree->utilityStmt;
+				stmt->stmt_location = queryTree->stmt_location;
+				stmt->stmt_len = queryTree->stmt_len;
+			}
 			else
-				stmt = (Node *) pg_plan_query(queryTree,
+				stmt = pg_plan_query(queryTree,
 						  fcache->readonly_func ? CURSOR_OPT_PARALLEL_OK : 0,
-											  NULL);
+									 NULL);
 
 			/*
 			 * Precheck all commands for validity in a function.  This should
 			 * generally match the restrictions spi.c applies.
 			 */
-			if (IsA(stmt, CopyStmt) &&
-				((CopyStmt *) stmt)->filename == NULL)
-				ereport(ERROR,
-						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			if (stmt->commandType == CMD_UTILITY)
+			{
+				if (IsA(stmt->utilityStmt, CopyStmt) &&
+					((CopyStmt *) stmt->utilityStmt)->filename == NULL)
+					ereport(ERROR,
+							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 					errmsg("cannot COPY to/from client in a SQL function")));
 
-			if (IsA(stmt, TransactionStmt))
-				ereport(ERROR,
-						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-				/* translator: %s is a SQL statement name */
-						 errmsg("%s is not allowed in a SQL function",
-								CreateCommandTag(stmt))));
+				if (IsA(stmt->utilityStmt, TransactionStmt))
+					ereport(ERROR,
+							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					/* translator: %s is a SQL statement name */
+							 errmsg("%s is not allowed in a SQL function",
+									CreateCommandTag(stmt->utilityStmt))));
+			}
 
 			if (fcache->readonly_func && !CommandIsReadOnly(stmt))
 				ereport(ERROR,
 						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				/* translator: %s is a SQL statement name */
 					   errmsg("%s is not allowed in a non-volatile function",
-							  CreateCommandTag(stmt))));
+							  CreateCommandTag((Node *) stmt))));
 
 			if (IsInParallelMode() && !CommandIsReadOnly(stmt))
-				PreventCommandIfParallelMode(CreateCommandTag(stmt));
+				PreventCommandIfParallelMode(CreateCommandTag((Node *) stmt));
 
 			/* OK, build the execution_state for this query */
 			newes = (execution_state *) palloc(sizeof(execution_state));
@@ -569,15 +580,9 @@ init_execution_state(List *queryTree_list,
 	{
 		lasttages->setsResult = true;
 		if (lazyEvalOK &&
-			IsA(lasttages->stmt, PlannedStmt))
-		{
-			PlannedStmt *ps = (PlannedStmt *) lasttages->stmt;
-
-			if (ps->commandType == CMD_SELECT &&
-				ps->utilityStmt == NULL &&
-				!ps->hasModifyingCTE)
-				fcache->lazyEval = lasttages->lazyEval = true;
-		}
+			lasttages->stmt->commandType == CMD_SELECT &&
+			!lasttages->stmt->hasModifyingCTE)
+			fcache->lazyEval = lasttages->lazyEval = true;
 	}
 
 	return eslist;
@@ -704,7 +709,7 @@ init_sql_fcache(FmgrInfo *finfo, Oid collation, bool lazyEvalOK)
 	flat_query_list = NIL;
 	foreach(lc, raw_parsetree_list)
 	{
-		Node	   *parsetree = (Node *) lfirst(lc);
+		RawStmt    *parsetree = (RawStmt *) lfirst(lc);
 		List	   *queryTree_sublist;
 
 		queryTree_sublist = pg_analyze_and_rewrite_params(parsetree,
@@ -801,22 +806,15 @@ postquel_start(execution_state *es, SQLFunctionCachePtr fcache)
 	else
 		dest = None_Receiver;
 
-	if (IsA(es->stmt, PlannedStmt))
-		es->qd = CreateQueryDesc((PlannedStmt *) es->stmt,
-								 fcache->src,
-								 GetActiveSnapshot(),
-								 InvalidSnapshot,
-								 dest,
-								 fcache->paramLI, 0);
-	else
-		es->qd = CreateUtilityQueryDesc(es->stmt,
-										fcache->src,
-										GetActiveSnapshot(),
-										dest,
-										fcache->paramLI);
+	es->qd = CreateQueryDesc(es->stmt,
+							 fcache->src,
+							 GetActiveSnapshot(),
+							 InvalidSnapshot,
+							 dest,
+							 fcache->paramLI, 0);
 
 	/* Utility commands don't need Executor. */
-	if (es->qd->utilitystmt == NULL)
+	if (es->qd->operation != CMD_UTILITY)
 	{
 		/*
 		 * In lazyEval mode, do not let the executor set up an AfterTrigger
@@ -844,12 +842,9 @@ postquel_getnext(execution_state *es, SQLFunctionCachePtr fcache)
 {
 	bool		result;
 
-	if (es->qd->utilitystmt)
+	if (es->qd->operation == CMD_UTILITY)
 	{
-		/* ProcessUtility needs the PlannedStmt for DECLARE CURSOR */
-		ProcessUtility((es->qd->plannedstmt ?
-						(Node *) es->qd->plannedstmt :
-						es->qd->utilitystmt),
+		ProcessUtility(es->qd->plannedstmt,
 					   fcache->src,
 					   PROCESS_UTILITY_QUERY,
 					   es->qd->params,
@@ -882,7 +877,7 @@ postquel_end(execution_state *es)
 	es->status = F_EXEC_DONE;
 
 	/* Utility commands don't need Executor. */
-	if (es->qd->utilitystmt == NULL)
+	if (es->qd->operation != CMD_UTILITY)
 	{
 		ExecutorFinish(es->qd);
 		ExecutorEnd(es->qd);
@@ -1576,8 +1571,7 @@ check_sql_fn_retval(Oid func_id, Oid rettype, List *queryTreeList,
 	 * entities.
 	 */
 	if (parse &&
-		parse->commandType == CMD_SELECT &&
-		parse->utilityStmt == NULL)
+		parse->commandType == CMD_SELECT)
 	{
 		tlist_ptr = &parse->targetList;
 		tlist = parse->targetList;
diff --git a/src/backend/executor/spi.c b/src/backend/executor/spi.c
index ee7a7e2d78c2e4708ec507ec2bba1ef6c6dd5066..7bd37283b7f809560ed314078dabc4838520038c 100644
--- a/src/backend/executor/spi.c
+++ b/src/backend/executor/spi.c
@@ -1232,7 +1232,7 @@ SPI_cursor_open_internal(const char *name, SPIPlanPtr plan,
 	if (!(portal->cursorOptions & (CURSOR_OPT_SCROLL | CURSOR_OPT_NO_SCROLL)))
 	{
 		if (list_length(stmt_list) == 1 &&
-			IsA((Node *) linitial(stmt_list), PlannedStmt) &&
+		 ((PlannedStmt *) linitial(stmt_list))->commandType != CMD_UTILITY &&
 			((PlannedStmt *) linitial(stmt_list))->rowMarks == NIL &&
 			ExecSupportsBackwardScan(((PlannedStmt *) linitial(stmt_list))->planTree))
 			portal->cursorOptions |= CURSOR_OPT_SCROLL;
@@ -1248,7 +1248,7 @@ SPI_cursor_open_internal(const char *name, SPIPlanPtr plan,
 	if (portal->cursorOptions & CURSOR_OPT_SCROLL)
 	{
 		if (list_length(stmt_list) == 1 &&
-			IsA((Node *) linitial(stmt_list), PlannedStmt) &&
+		 ((PlannedStmt *) linitial(stmt_list))->commandType != CMD_UTILITY &&
 			((PlannedStmt *) linitial(stmt_list))->rowMarks != NIL)
 			ereport(ERROR,
 					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
@@ -1270,7 +1270,7 @@ SPI_cursor_open_internal(const char *name, SPIPlanPtr plan,
 
 		foreach(lc, stmt_list)
 		{
-			Node	   *pstmt = (Node *) lfirst(lc);
+			PlannedStmt *pstmt = (PlannedStmt *) lfirst(lc);
 
 			if (!CommandIsReadOnly(pstmt))
 			{
@@ -1279,9 +1279,9 @@ SPI_cursor_open_internal(const char *name, SPIPlanPtr plan,
 							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 					/* translator: %s is a SQL statement name */
 					   errmsg("%s is not allowed in a non-volatile function",
-							  CreateCommandTag(pstmt))));
+							  CreateCommandTag((Node *) pstmt))));
 				else
-					PreventCommandIfParallelMode(CreateCommandTag(pstmt));
+					PreventCommandIfParallelMode(CreateCommandTag((Node *) pstmt));
 			}
 		}
 	}
@@ -1757,7 +1757,7 @@ _SPI_prepare_plan(const char *src, SPIPlanPtr plan)
 
 	foreach(list_item, raw_parsetree_list)
 	{
-		Node	   *parsetree = (Node *) lfirst(list_item);
+		RawStmt    *parsetree = (RawStmt *) lfirst(list_item);
 		List	   *stmt_list;
 		CachedPlanSource *plansource;
 
@@ -1767,7 +1767,7 @@ _SPI_prepare_plan(const char *src, SPIPlanPtr plan)
 		 */
 		plansource = CreateCachedPlan(parsetree,
 									  src,
-									  CreateCommandTag(parsetree));
+									  CreateCommandTag(parsetree->stmt));
 
 		/*
 		 * Parameter datatypes are driven by parserSetup hook if provided,
@@ -1859,12 +1859,12 @@ _SPI_prepare_oneshot_plan(const char *src, SPIPlanPtr plan)
 
 	foreach(list_item, raw_parsetree_list)
 	{
-		Node	   *parsetree = (Node *) lfirst(list_item);
+		RawStmt    *parsetree = (RawStmt *) lfirst(list_item);
 		CachedPlanSource *plansource;
 
 		plansource = CreateOneShotCachedPlan(parsetree,
 											 src,
-											 CreateCommandTag(parsetree));
+										  CreateCommandTag(parsetree->stmt));
 
 		plancache_list = lappend(plancache_list, plansource);
 	}
@@ -1959,7 +1959,7 @@ _SPI_execute_plan(SPIPlanPtr plan, ParamListInfo paramLI,
 		 */
 		if (plan->oneshot)
 		{
-			Node	   *parsetree = plansource->raw_parse_tree;
+			RawStmt    *parsetree = plansource->raw_parse_tree;
 			const char *src = plansource->query_string;
 			List	   *stmt_list;
 
@@ -2018,26 +2018,19 @@ _SPI_execute_plan(SPIPlanPtr plan, ParamListInfo paramLI,
 
 		foreach(lc2, stmt_list)
 		{
-			Node	   *stmt = (Node *) lfirst(lc2);
-			bool		canSetTag;
+			PlannedStmt *stmt = (PlannedStmt *) lfirst(lc2);
+			bool		canSetTag = stmt->canSetTag;
 			DestReceiver *dest;
 
 			_SPI_current->processed = 0;
 			_SPI_current->lastoid = InvalidOid;
 			_SPI_current->tuptable = NULL;
 
-			if (IsA(stmt, PlannedStmt))
+			if (stmt->utilityStmt)
 			{
-				canSetTag = ((PlannedStmt *) stmt)->canSetTag;
-			}
-			else
-			{
-				/* utilities are canSetTag if only thing in list */
-				canSetTag = (list_length(stmt_list) == 1);
-
-				if (IsA(stmt, CopyStmt))
+				if (IsA(stmt->utilityStmt, CopyStmt))
 				{
-					CopyStmt   *cstmt = (CopyStmt *) stmt;
+					CopyStmt   *cstmt = (CopyStmt *) stmt->utilityStmt;
 
 					if (cstmt->filename == NULL)
 					{
@@ -2045,7 +2038,7 @@ _SPI_execute_plan(SPIPlanPtr plan, ParamListInfo paramLI,
 						goto fail;
 					}
 				}
-				else if (IsA(stmt, TransactionStmt))
+				else if (IsA(stmt->utilityStmt, TransactionStmt))
 				{
 					my_res = SPI_ERROR_TRANSACTION;
 					goto fail;
@@ -2057,10 +2050,10 @@ _SPI_execute_plan(SPIPlanPtr plan, ParamListInfo paramLI,
 						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				/* translator: %s is a SQL statement name */
 					   errmsg("%s is not allowed in a non-volatile function",
-							  CreateCommandTag(stmt))));
+							  CreateCommandTag((Node *) stmt))));
 
 			if (IsInParallelMode() && !CommandIsReadOnly(stmt))
-				PreventCommandIfParallelMode(CreateCommandTag(stmt));
+				PreventCommandIfParallelMode(CreateCommandTag((Node *) stmt));
 
 			/*
 			 * If not read-only mode, advance the command counter before each
@@ -2074,8 +2067,7 @@ _SPI_execute_plan(SPIPlanPtr plan, ParamListInfo paramLI,
 
 			dest = CreateDestReceiver(canSetTag ? DestSPI : DestNone);
 
-			if (IsA(stmt, PlannedStmt) &&
-				((PlannedStmt *) stmt)->utilityStmt == NULL)
+			if (stmt->utilityStmt == NULL)
 			{
 				QueryDesc  *qdesc;
 				Snapshot	snap;
@@ -2085,7 +2077,7 @@ _SPI_execute_plan(SPIPlanPtr plan, ParamListInfo paramLI,
 				else
 					snap = InvalidSnapshot;
 
-				qdesc = CreateQueryDesc((PlannedStmt *) stmt,
+				qdesc = CreateQueryDesc(stmt,
 										plansource->query_string,
 										snap, crosscheck_snapshot,
 										dest,
@@ -2116,9 +2108,9 @@ _SPI_execute_plan(SPIPlanPtr plan, ParamListInfo paramLI,
 				 * Some utility statements return a row count, even though the
 				 * tuples are not returned to the caller.
 				 */
-				if (IsA(stmt, CreateTableAsStmt))
+				if (IsA(stmt->utilityStmt, CreateTableAsStmt))
 				{
-					CreateTableAsStmt *ctastmt = (CreateTableAsStmt *) stmt;
+					CreateTableAsStmt *ctastmt = (CreateTableAsStmt *) stmt->utilityStmt;
 
 					if (strncmp(completionTag, "SELECT ", 7) == 0)
 						_SPI_current->processed =
@@ -2141,7 +2133,7 @@ _SPI_execute_plan(SPIPlanPtr plan, ParamListInfo paramLI,
 					if (ctastmt->is_select_into)
 						res = SPI_OK_SELINTO;
 				}
-				else if (IsA(stmt, CopyStmt))
+				else if (IsA(stmt->utilityStmt, CopyStmt))
 				{
 					Assert(strncmp(completionTag, "COPY ", 5) == 0);
 					_SPI_current->processed = pg_strtouint64(completionTag + 5,
@@ -2270,7 +2262,6 @@ _SPI_pquery(QueryDesc *queryDesc, bool fire_triggers, uint64 tcount)
 	switch (operation)
 	{
 		case CMD_SELECT:
-			Assert(queryDesc->plannedstmt->utilityStmt == NULL);
 			if (queryDesc->dest->mydest != DestSPI)
 			{
 				/* Don't return SPI_OK_SELECT if we're discarding result */
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index 930f2f1d5f40f6aaa7f40f1161dccb2022813c93..7107bbf16428feb860467ed05c91be97716f591e 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -90,13 +90,15 @@ _copyPlannedStmt(const PlannedStmt *from)
 	COPY_NODE_FIELD(planTree);
 	COPY_NODE_FIELD(rtable);
 	COPY_NODE_FIELD(resultRelations);
-	COPY_NODE_FIELD(utilityStmt);
 	COPY_NODE_FIELD(subplans);
 	COPY_BITMAPSET_FIELD(rewindPlanIDs);
 	COPY_NODE_FIELD(rowMarks);
 	COPY_NODE_FIELD(relationOids);
 	COPY_NODE_FIELD(invalItems);
 	COPY_SCALAR_FIELD(nParamExec);
+	COPY_NODE_FIELD(utilityStmt);
+	COPY_LOCATION_FIELD(stmt_location);
+	COPY_LOCATION_FIELD(stmt_len);
 
 	return newnode;
 }
@@ -2767,6 +2769,20 @@ _copyQuery(const Query *from)
 	COPY_NODE_FIELD(setOperations);
 	COPY_NODE_FIELD(constraintDeps);
 	COPY_NODE_FIELD(withCheckOptions);
+	COPY_LOCATION_FIELD(stmt_location);
+	COPY_LOCATION_FIELD(stmt_len);
+
+	return newnode;
+}
+
+static RawStmt *
+_copyRawStmt(const RawStmt *from)
+{
+	RawStmt    *newnode = makeNode(RawStmt);
+
+	COPY_NODE_FIELD(stmt);
+	COPY_LOCATION_FIELD(stmt_location);
+	COPY_LOCATION_FIELD(stmt_len);
 
 	return newnode;
 }
@@ -4728,6 +4744,9 @@ copyObject(const void *from)
 		case T_Query:
 			retval = _copyQuery(from);
 			break;
+		case T_RawStmt:
+			retval = _copyRawStmt(from);
+			break;
 		case T_InsertStmt:
 			retval = _copyInsertStmt(from);
 			break;
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index a27e5edf37233064b048c5441cc16a522bf1fe6e..ec4bbfc770b313bcbd5e539172472efe7fe751e6 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -946,6 +946,18 @@ _equalQuery(const Query *a, const Query *b)
 	COMPARE_NODE_FIELD(setOperations);
 	COMPARE_NODE_FIELD(constraintDeps);
 	COMPARE_NODE_FIELD(withCheckOptions);
+	COMPARE_LOCATION_FIELD(stmt_location);
+	COMPARE_LOCATION_FIELD(stmt_len);
+
+	return true;
+}
+
+static bool
+_equalRawStmt(const RawStmt *a, const RawStmt *b)
+{
+	COMPARE_NODE_FIELD(stmt);
+	COMPARE_LOCATION_FIELD(stmt_location);
+	COMPARE_LOCATION_FIELD(stmt_len);
 
 	return true;
 }
@@ -3015,6 +3027,9 @@ equal(const void *a, const void *b)
 		case T_Query:
 			retval = _equalQuery(a, b);
 			break;
+		case T_RawStmt:
+			retval = _equalRawStmt(a, b);
+			break;
 		case T_InsertStmt:
 			retval = _equalInsertStmt(a, b);
 			break;
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index 806d0a93aac639d4def0162be3d0f62842702e08..cf0a6059e91f280499946bfe51872423d09d5245 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -252,13 +252,15 @@ _outPlannedStmt(StringInfo str, const PlannedStmt *node)
 	WRITE_NODE_FIELD(planTree);
 	WRITE_NODE_FIELD(rtable);
 	WRITE_NODE_FIELD(resultRelations);
-	WRITE_NODE_FIELD(utilityStmt);
 	WRITE_NODE_FIELD(subplans);
 	WRITE_BITMAPSET_FIELD(rewindPlanIDs);
 	WRITE_NODE_FIELD(rowMarks);
 	WRITE_NODE_FIELD(relationOids);
 	WRITE_NODE_FIELD(invalItems);
 	WRITE_INT_FIELD(nParamExec);
+	WRITE_NODE_FIELD(utilityStmt);
+	WRITE_LOCATION_FIELD(stmt_location);
+	WRITE_LOCATION_FIELD(stmt_len);
 }
 
 /*
@@ -2705,6 +2707,9 @@ _outQuery(StringInfo str, const Query *node)
 	WRITE_NODE_FIELD(rowMarks);
 	WRITE_NODE_FIELD(setOperations);
 	WRITE_NODE_FIELD(constraintDeps);
+	/* withCheckOptions intentionally omitted, see comment in parsenodes.h */
+	WRITE_LOCATION_FIELD(stmt_location);
+	WRITE_LOCATION_FIELD(stmt_len);
 }
 
 static void
diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c
index dc40d0181e00dfcf47258dcdba0a2e1f930bb7a0..e02dd94f055b2a1aa8177e715c7a004bd76fac76 100644
--- a/src/backend/nodes/readfuncs.c
+++ b/src/backend/nodes/readfuncs.c
@@ -262,6 +262,9 @@ _readQuery(void)
 	READ_NODE_FIELD(rowMarks);
 	READ_NODE_FIELD(setOperations);
 	READ_NODE_FIELD(constraintDeps);
+	/* withCheckOptions intentionally omitted, see comment in parsenodes.h */
+	READ_LOCATION_FIELD(stmt_location);
+	READ_LOCATION_FIELD(stmt_len);
 
 	READ_DONE();
 }
@@ -1415,13 +1418,15 @@ _readPlannedStmt(void)
 	READ_NODE_FIELD(planTree);
 	READ_NODE_FIELD(rtable);
 	READ_NODE_FIELD(resultRelations);
-	READ_NODE_FIELD(utilityStmt);
 	READ_NODE_FIELD(subplans);
 	READ_BITMAPSET_FIELD(rewindPlanIDs);
 	READ_NODE_FIELD(rowMarks);
 	READ_NODE_FIELD(relationOids);
 	READ_NODE_FIELD(invalItems);
 	READ_INT_FIELD(nParamExec);
+	READ_NODE_FIELD(utilityStmt);
+	READ_LOCATION_FIELD(stmt_location);
+	READ_LOCATION_FIELD(stmt_len);
 
 	READ_DONE();
 }
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 207290fd2608e9d4a411f8ebea7386d90fb540f6..f936710171c7814dad813e64cc83b25b5b47e739 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -193,11 +193,6 @@ standard_planner(Query *parse, int cursorOptions, ParamListInfo boundParams)
 	ListCell   *lp,
 			   *lr;
 
-	/* Cursor options may come from caller or from DECLARE CURSOR stmt */
-	if (parse->utilityStmt &&
-		IsA(parse->utilityStmt, DeclareCursorStmt))
-		cursorOptions |= ((DeclareCursorStmt *) parse->utilityStmt)->options;
-
 	/*
 	 * Set up global state for this planner invocation.  This data is needed
 	 * across all levels of sub-Query that might exist in the given command,
@@ -246,7 +241,6 @@ standard_planner(Query *parse, int cursorOptions, ParamListInfo boundParams)
 		IsUnderPostmaster &&
 		dynamic_shared_memory_type != DSM_IMPL_NONE &&
 		parse->commandType == CMD_SELECT &&
-		parse->utilityStmt == NULL &&
 		!parse->hasModifyingCTE &&
 		max_parallel_workers_per_gather > 0 &&
 		!IsParallelWorker() &&
@@ -421,13 +415,16 @@ standard_planner(Query *parse, int cursorOptions, ParamListInfo boundParams)
 	result->planTree = top_plan;
 	result->rtable = glob->finalrtable;
 	result->resultRelations = glob->resultRelations;
-	result->utilityStmt = parse->utilityStmt;
 	result->subplans = glob->subplans;
 	result->rewindPlanIDs = glob->rewindPlanIDs;
 	result->rowMarks = glob->finalrowmarks;
 	result->relationOids = glob->relationOids;
 	result->invalItems = glob->invalItems;
 	result->nParamExec = glob->nParamExec;
+	/* utilityStmt should be null, but we might as well copy it */
+	result->utilityStmt = parse->utilityStmt;
+	result->stmt_location = parse->stmt_location;
+	result->stmt_len = parse->stmt_len;
 
 	return result;
 }
diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c
index be5d3d1104bfa69bd9ade06c84d7d7180be56c56..7cb1bc9a62c5ed05eddd6a32ecd31a153462af29 100644
--- a/src/backend/optimizer/prep/prepjointree.c
+++ b/src/backend/optimizer/prep/prepjointree.c
@@ -1409,8 +1409,7 @@ is_simple_subquery(Query *subquery, RangeTblEntry *rte,
 	 * Let's just make sure it's a valid subselect ...
 	 */
 	if (!IsA(subquery, Query) ||
-		subquery->commandType != CMD_SELECT ||
-		subquery->utilityStmt != NULL)
+		subquery->commandType != CMD_SELECT)
 		elog(ERROR, "subquery is bogus");
 
 	/*
@@ -1744,8 +1743,7 @@ is_simple_union_all(Query *subquery)
 
 	/* Let's just make sure it's a valid subselect ... */
 	if (!IsA(subquery, Query) ||
-		subquery->commandType != CMD_SELECT ||
-		subquery->utilityStmt != NULL)
+		subquery->commandType != CMD_SELECT)
 		elog(ERROR, "subquery is bogus");
 
 	/* Is it a set-operation query at all? */
diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c
index 10abdc3aff136ff5dc4c347dfbc6984971418456..59ccdf43d4999f1dd7fb3a219e59017809458a40 100644
--- a/src/backend/optimizer/util/clauses.c
+++ b/src/backend/optimizer/util/clauses.c
@@ -4479,7 +4479,6 @@ inline_function(Oid funcid, Oid result_type, Oid result_collid,
 	 */
 	if (!IsA(querytree, Query) ||
 		querytree->commandType != CMD_SELECT ||
-		querytree->utilityStmt ||
 		querytree->hasAggs ||
 		querytree->hasWindowFuncs ||
 		querytree->hasTargetSRFs ||
@@ -5006,8 +5005,7 @@ inline_set_returning_function(PlannerInfo *root, RangeTblEntry *rte)
 	 * The single command must be a plain SELECT.
 	 */
 	if (!IsA(querytree, Query) ||
-		querytree->commandType != CMD_SELECT ||
-		querytree->utilityStmt)
+		querytree->commandType != CMD_SELECT)
 		goto fail;
 
 	/*
diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
index 5116cbb71a27f7693113a601bc1db7d9d1dc5c8b..8278e742f8dcfd9b27d632d39230d808bd80a113 100644
--- a/src/backend/parser/analyze.c
+++ b/src/backend/parser/analyze.c
@@ -48,6 +48,7 @@
 /* Hook for plugins to get control at end of parse analysis */
 post_parse_analyze_hook_type post_parse_analyze_hook = NULL;
 
+static Query *transformOptionalSelectInto(ParseState *pstate, Node *parseTree);
 static Query *transformDeleteStmt(ParseState *pstate, DeleteStmt *stmt);
 static Query *transformInsertStmt(ParseState *pstate, InsertStmt *stmt);
 static List *transformInsertRow(ParseState *pstate, List *exprlist,
@@ -92,7 +93,7 @@ static bool test_raw_expression_coverage(Node *node, void *context);
  * a dummy CMD_UTILITY Query node.
  */
 Query *
-parse_analyze(Node *parseTree, const char *sourceText,
+parse_analyze(RawStmt *parseTree, const char *sourceText,
 			  Oid *paramTypes, int numParams)
 {
 	ParseState *pstate = make_parsestate(NULL);
@@ -123,7 +124,7 @@ parse_analyze(Node *parseTree, const char *sourceText,
  * be modified or enlarged (via repalloc).
  */
 Query *
-parse_analyze_varparams(Node *parseTree, const char *sourceText,
+parse_analyze_varparams(RawStmt *parseTree, const char *sourceText,
 						Oid **paramTypes, int *numParams)
 {
 	ParseState *pstate = make_parsestate(NULL);
@@ -174,14 +175,35 @@ parse_sub_analyze(Node *parseTree, ParseState *parentParseState,
  * transformTopLevelStmt -
  *	  transform a Parse tree into a Query tree.
  *
+ * This function is just responsible for transferring statement location data
+ * from the RawStmt into the finished Query.
+ */
+Query *
+transformTopLevelStmt(ParseState *pstate, RawStmt *parseTree)
+{
+	Query	   *result;
+
+	/* We're at top level, so allow SELECT INTO */
+	result = transformOptionalSelectInto(pstate, parseTree->stmt);
+
+	result->stmt_location = parseTree->stmt_location;
+	result->stmt_len = parseTree->stmt_len;
+
+	return result;
+}
+
+/*
+ * transformOptionalSelectInto -
+ *	  If SELECT has INTO, convert it to CREATE TABLE AS.
+ *
  * The only thing we do here that we don't do in transformStmt() is to
  * convert SELECT ... INTO into CREATE TABLE AS.  Since utility statements
  * aren't allowed within larger statements, this is only allowed at the top
  * of the parse tree, and so we only try it before entering the recursive
  * transformStmt() processing.
  */
-Query *
-transformTopLevelStmt(ParseState *pstate, Node *parseTree)
+static Query *
+transformOptionalSelectInto(ParseState *pstate, Node *parseTree)
 {
 	if (IsA(parseTree, SelectStmt))
 	{
@@ -318,11 +340,11 @@ transformStmt(ParseState *pstate, Node *parseTree)
  * Classification here should match transformStmt().
  */
 bool
-analyze_requires_snapshot(Node *parseTree)
+analyze_requires_snapshot(RawStmt *parseTree)
 {
 	bool		result;
 
-	switch (nodeTag(parseTree))
+	switch (nodeTag(parseTree->stmt))
 	{
 			/*
 			 * Optimizable statements
@@ -338,10 +360,6 @@ analyze_requires_snapshot(Node *parseTree)
 			 * Special cases
 			 */
 		case T_DeclareCursorStmt:
-			/* yes, because it's analyzed just like SELECT */
-			result = true;
-			break;
-
 		case T_ExplainStmt:
 		case T_CreateTableAsStmt:
 			/* yes, because we must analyze the contained statement */
@@ -563,8 +581,7 @@ transformInsertStmt(ParseState *pstate, InsertStmt *stmt)
 
 		/* The grammar should have produced a SELECT */
 		if (!IsA(selectQuery, Query) ||
-			selectQuery->commandType != CMD_SELECT ||
-			selectQuery->utilityStmt != NULL)
+			selectQuery->commandType != CMD_SELECT)
 			elog(ERROR, "unexpected non-SELECT command in INSERT ... SELECT");
 
 		/*
@@ -2344,17 +2361,17 @@ transformReturningList(ParseState *pstate, List *returningList)
  * transformDeclareCursorStmt -
  *	transform a DECLARE CURSOR Statement
  *
- * DECLARE CURSOR is a hybrid case: it's an optimizable statement (in fact not
- * significantly different from a SELECT) as far as parsing/rewriting/planning
- * are concerned, but it's not passed to the executor and so in that sense is
- * a utility statement.  We transform it into a Query exactly as if it were
- * a SELECT, then stick the original DeclareCursorStmt into the utilityStmt
- * field to carry the cursor name and options.
+ * DECLARE CURSOR is like other utility statements in that we emit it as a
+ * CMD_UTILITY Query node; however, we must first transform the contained
+ * query.  We used to postpone that until execution, but it's really necessary
+ * to do it during the normal parse analysis phase to ensure that side effects
+ * of parser hooks happen at the expected time.
  */
 static Query *
 transformDeclareCursorStmt(ParseState *pstate, DeclareCursorStmt *stmt)
 {
 	Query	   *result;
+	Query	   *query;
 
 	/*
 	 * Don't allow both SCROLL and NO SCROLL to be specified
@@ -2365,12 +2382,13 @@ transformDeclareCursorStmt(ParseState *pstate, DeclareCursorStmt *stmt)
 				(errcode(ERRCODE_INVALID_CURSOR_DEFINITION),
 				 errmsg("cannot specify both SCROLL and NO SCROLL")));
 
-	result = transformStmt(pstate, stmt->query);
+	/* Transform contained query, not allowing SELECT INTO */
+	query = transformStmt(pstate, stmt->query);
+	stmt->query = (Node *) query;
 
 	/* Grammar should not have allowed anything but SELECT */
-	if (!IsA(result, Query) ||
-		result->commandType != CMD_SELECT ||
-		result->utilityStmt != NULL)
+	if (!IsA(query, Query) ||
+		query->commandType != CMD_SELECT)
 		elog(ERROR, "unexpected non-SELECT command in DECLARE CURSOR");
 
 	/*
@@ -2378,47 +2396,47 @@ transformDeclareCursorStmt(ParseState *pstate, DeclareCursorStmt *stmt)
 	 * allowed, but the semantics of when the updates occur might be
 	 * surprising.)
 	 */
-	if (result->hasModifyingCTE)
+	if (query->hasModifyingCTE)
 		ereport(ERROR,
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("DECLARE CURSOR must not contain data-modifying statements in WITH")));
 
 	/* FOR UPDATE and WITH HOLD are not compatible */
-	if (result->rowMarks != NIL && (stmt->options & CURSOR_OPT_HOLD))
+	if (query->rowMarks != NIL && (stmt->options & CURSOR_OPT_HOLD))
 		ereport(ERROR,
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 		/*------
 		  translator: %s is a SQL row locking clause such as FOR UPDATE */
 				 errmsg("DECLARE CURSOR WITH HOLD ... %s is not supported",
 						LCS_asString(((RowMarkClause *)
-									  linitial(result->rowMarks))->strength)),
+									  linitial(query->rowMarks))->strength)),
 				 errdetail("Holdable cursors must be READ ONLY.")));
 
 	/* FOR UPDATE and SCROLL are not compatible */
-	if (result->rowMarks != NIL && (stmt->options & CURSOR_OPT_SCROLL))
+	if (query->rowMarks != NIL && (stmt->options & CURSOR_OPT_SCROLL))
 		ereport(ERROR,
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 		/*------
 		  translator: %s is a SQL row locking clause such as FOR UPDATE */
 				 errmsg("DECLARE SCROLL CURSOR ... %s is not supported",
 						LCS_asString(((RowMarkClause *)
-									  linitial(result->rowMarks))->strength)),
+									  linitial(query->rowMarks))->strength)),
 				 errdetail("Scrollable cursors must be READ ONLY.")));
 
 	/* FOR UPDATE and INSENSITIVE are not compatible */
-	if (result->rowMarks != NIL && (stmt->options & CURSOR_OPT_INSENSITIVE))
+	if (query->rowMarks != NIL && (stmt->options & CURSOR_OPT_INSENSITIVE))
 		ereport(ERROR,
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 		/*------
 		  translator: %s is a SQL row locking clause such as FOR UPDATE */
 				 errmsg("DECLARE INSENSITIVE CURSOR ... %s is not supported",
 						LCS_asString(((RowMarkClause *)
-									  linitial(result->rowMarks))->strength)),
+									  linitial(query->rowMarks))->strength)),
 				 errdetail("Insensitive cursors must be READ ONLY.")));
 
-	/* We won't need the raw querytree any more */
-	stmt->query = NULL;
-
+	/* represent the command as a utility Query */
+	result = makeNode(Query);
+	result->commandType = CMD_UTILITY;
 	result->utilityStmt = (Node *) stmt;
 
 	return result;
@@ -2441,7 +2459,7 @@ transformExplainStmt(ParseState *pstate, ExplainStmt *stmt)
 	Query	   *result;
 
 	/* transform contained query, allowing SELECT INTO */
-	stmt->query = (Node *) transformTopLevelStmt(pstate, stmt->query);
+	stmt->query = (Node *) transformOptionalSelectInto(pstate, stmt->query);
 
 	/* represent the command as a utility Query */
 	result = makeNode(Query);
@@ -2457,7 +2475,7 @@ transformExplainStmt(ParseState *pstate, ExplainStmt *stmt)
  *	transform a CREATE TABLE AS, SELECT ... INTO, or CREATE MATERIALIZED VIEW
  *	Statement
  *
- * As with EXPLAIN, transform the contained statement now.
+ * As with DECLARE CURSOR and EXPLAIN, transform the contained statement now.
  */
 static Query *
 transformCreateTableAsStmt(ParseState *pstate, CreateTableAsStmt *stmt)
@@ -2465,7 +2483,7 @@ transformCreateTableAsStmt(ParseState *pstate, CreateTableAsStmt *stmt)
 	Query	   *result;
 	Query	   *query;
 
-	/* transform contained query */
+	/* transform contained query, not allowing SELECT INTO */
 	query = transformStmt(pstate, stmt->query);
 	stmt->query = (Node *) query;
 
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 9eef550d2ab9c21747823e31e50ee7dd279ca7dc..e61ba06efe59088f358337fae9c5e85f3de58eef 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -80,7 +80,8 @@
 
 /*
  * The above macro assigns -1 (unknown) as the parse location of any
- * nonterminal that was reduced from an empty rule.  This is problematic
+ * nonterminal that was reduced from an empty rule, or whose leftmost
+ * component was reduced from an empty rule.  This is problematic
  * for nonterminals defined like
  *		OptFooList: / * EMPTY * / { ... } | OptFooList Foo { ... } ;
  * because we'll set -1 as the location during the first reduction and then
@@ -91,6 +92,12 @@
  * (Although we have many nonterminals that follow this pattern, we only
  * bother with fixing @$ like this when the nonterminal's parse location
  * is actually referenced in some rule.)
+ *
+ * A cleaner answer would be to make YYLLOC_DEFAULT scan all the Rhs
+ * locations until it's found one that's not -1.  Then we'd get a correct
+ * location for any nonterminal that isn't entirely empty.  But this way
+ * would add overhead to every rule reduction, and so far there's not been
+ * a compelling reason to pay that overhead.
  */
 
 /*
@@ -133,6 +140,8 @@ typedef struct ImportQual
 
 static void base_yyerror(YYLTYPE *yylloc, core_yyscan_t yyscanner,
 						 const char *msg);
+static RawStmt *makeRawStmt(Node *stmt, int stmt_location);
+static void updateRawStmtEnd(RawStmt *rs, int end_location);
 static Node *makeColumnRef(char *colname, List *indirection,
 						   int location, core_yyscan_t yyscanner);
 static Node *makeTypeCast(Node *arg, TypeName *typename, int location);
@@ -758,18 +767,32 @@ stmtblock:	stmtmulti
 			}
 		;
 
-/* the thrashing around here is to discard "empty" statements... */
+/*
+ * At top level, we wrap each stmt with a RawStmt node carrying start location
+ * and length of the stmt's text.  Notice that the start loc/len are driven
+ * entirely from semicolon locations (@2).  It would seem natural to use
+ * @1 or @3 to get the true start location of a stmt, but that doesn't work
+ * for statements that can start with empty nonterminals (opt_with_clause is
+ * the main offender here); as noted in the comments for YYLLOC_DEFAULT,
+ * we'd get -1 for the location in such cases.
+ * We also take care to discard empty statements entirely.
+ */
 stmtmulti:	stmtmulti ';' stmt
 				{
+					if ($1 != NIL)
+					{
+						/* update length of previous stmt */
+						updateRawStmtEnd((RawStmt *) llast($1), @2);
+					}
 					if ($3 != NULL)
-						$$ = lappend($1, $3);
+						$$ = lappend($1, makeRawStmt($3, @2 + 1));
 					else
 						$$ = $1;
 				}
 			| stmt
 				{
 					if ($1 != NULL)
-						$$ = list_make1($1);
+						$$ = list_make1(makeRawStmt($1, 0));
 					else
 						$$ = NIL;
 				}
@@ -14474,6 +14497,33 @@ base_yyerror(YYLTYPE *yylloc, core_yyscan_t yyscanner, const char *msg)
 	parser_yyerror(msg);
 }
 
+static RawStmt *
+makeRawStmt(Node *stmt, int stmt_location)
+{
+	RawStmt    *rs = makeNode(RawStmt);
+
+	rs->stmt = stmt;
+	rs->stmt_location = stmt_location;
+	rs->stmt_len = 0;			/* might get changed later */
+	return rs;
+}
+
+/* Adjust a RawStmt to reflect that it doesn't run to the end of the string */
+static void
+updateRawStmtEnd(RawStmt *rs, int end_location)
+{
+	/*
+	 * If we already set the length, don't change it.  This is for situations
+	 * like "select foo ;; select bar" where the same statement will be last
+	 * in the string for more than one semicolon.
+	 */
+	if (rs->stmt_len > 0)
+		return;
+
+	/* OK, update length of RawStmt */
+	rs->stmt_len = end_location - rs->stmt_location;
+}
+
 static Node *
 makeColumnRef(char *colname, List *indirection,
 			  int location, core_yyscan_t yyscanner)
diff --git a/src/backend/parser/parse_clause.c b/src/backend/parser/parse_clause.c
index d788ffd7999daef293f9499f0e478fc6564a8fe7..624ab41371f97c78de11490e285f620d9e6c378d 100644
--- a/src/backend/parser/parse_clause.c
+++ b/src/backend/parser/parse_clause.c
@@ -478,12 +478,11 @@ transformRangeSubselect(ParseState *pstate, RangeSubselect *r)
 	pstate->p_expr_kind = EXPR_KIND_NONE;
 
 	/*
-	 * Check that we got something reasonable.  Many of these conditions are
-	 * impossible given restrictions of the grammar, but check 'em anyway.
+	 * Check that we got a SELECT.  Anything else should be impossible given
+	 * restrictions of the grammar, but check anyway.
 	 */
 	if (!IsA(query, Query) ||
-		query->commandType != CMD_SELECT ||
-		query->utilityStmt != NULL)
+		query->commandType != CMD_SELECT)
 		elog(ERROR, "unexpected non-SELECT command in subquery in FROM");
 
 	/*
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index f62e45f8ac8fbdc24c1379ae421336fd10c2bed5..73521b93fcab498b3ffb3149183047bff7315bd6 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -1848,12 +1848,11 @@ transformSubLink(ParseState *pstate, SubLink *sublink)
 	qtree = parse_sub_analyze(sublink->subselect, pstate, NULL, false);
 
 	/*
-	 * Check that we got something reasonable.  Many of these conditions are
-	 * impossible given restrictions of the grammar, but check 'em anyway.
+	 * Check that we got a SELECT.  Anything else should be impossible given
+	 * restrictions of the grammar, but check anyway.
 	 */
 	if (!IsA(qtree, Query) ||
-		qtree->commandType != CMD_SELECT ||
-		qtree->utilityStmt != NULL)
+		qtree->commandType != CMD_SELECT)
 		elog(ERROR, "unexpected non-SELECT command in SubLink");
 
 	sublink->subselect = (Node *) qtree;
diff --git a/src/backend/parser/parse_type.c b/src/backend/parser/parse_type.c
index e2c884caae0dce9fe23e3e01b2ac9df8a59f461a..6660929dec748dc4b8698af985e93f776b827e86 100644
--- a/src/backend/parser/parse_type.c
+++ b/src/backend/parser/parse_type.c
@@ -720,7 +720,7 @@ typeStringToTypeName(const char *str)
 	 */
 	if (list_length(raw_parsetree_list) != 1)
 		goto fail;
-	stmt = (SelectStmt *) linitial(raw_parsetree_list);
+	stmt = (SelectStmt *) ((RawStmt *) linitial(raw_parsetree_list))->stmt;
 	if (stmt == NULL ||
 		!IsA(stmt, SelectStmt) ||
 		stmt->distinctClause != NIL ||
diff --git a/src/backend/parser/parser.c b/src/backend/parser/parser.c
index c870229817ccb8f06935df9812a0f77742abf0e9..245b4cda3b9b3a4ebe585cad639f5c1fe3b62d02 100644
--- a/src/backend/parser/parser.c
+++ b/src/backend/parser/parser.c
@@ -29,7 +29,8 @@
  * raw_parser
  *		Given a query in string form, do lexical and grammatical analysis.
  *
- * Returns a list of raw (un-analyzed) parse trees.
+ * Returns a list of raw (un-analyzed) parse trees.  The immediate elements
+ * of the list are always RawStmt nodes.
  */
 List *
 raw_parser(const char *str)
diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c
index 55900d4b0e180805ab641d9952accfb0f48a5509..864d45ff1289699e73ba37533caade16d80b87a6 100644
--- a/src/backend/rewrite/rewriteDefine.c
+++ b/src/backend/rewrite/rewriteDefine.c
@@ -330,8 +330,7 @@ DefineQueryRewrite(char *rulename,
 		 */
 		query = (Query *) linitial(action);
 		if (!is_instead ||
-			query->commandType != CMD_SELECT ||
-			query->utilityStmt != NULL)
+			query->commandType != CMD_SELECT)
 			ereport(ERROR,
 					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("rules on SELECT must have action INSTEAD SELECT")));
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 05b2e57b71d07ffe685df86bbede6799b32579fe..bb89cce8cb9ff17d7ab7c93eb673d3d3c343e529 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -183,8 +183,8 @@ static int	errdetail_recovery_conflict(void);
 static void start_xact_command(void);
 static void finish_xact_command(void);
 static bool IsTransactionExitStmt(Node *parsetree);
-static bool IsTransactionExitStmtList(List *parseTrees);
-static bool IsTransactionStmtList(List *parseTrees);
+static bool IsTransactionExitStmtList(List *pstmts);
+static bool IsTransactionStmtList(List *pstmts);
 static void drop_unnamed_stmt(void);
 static void SigHupHandler(SIGNAL_ARGS);
 static void log_disconnections(int code, Datum arg);
@@ -588,8 +588,8 @@ ProcessClientWriteInterrupt(bool blocked)
 /*
  * Do raw parsing (only).
  *
- * A list of parsetrees is returned, since there might be multiple
- * commands in the given string.
+ * A list of parsetrees (RawStmt nodes) is returned, since there might be
+ * multiple commands in the given string.
  *
  * NOTE: for interactive queries, it is important to keep this routine
  * separate from the analysis & rewrite stages.  Analysis and rewriting
@@ -641,7 +641,7 @@ pg_parse_query(const char *query_string)
  * NOTE: for reasons mentioned above, this must be separate from raw parsing.
  */
 List *
-pg_analyze_and_rewrite(Node *parsetree, const char *query_string,
+pg_analyze_and_rewrite(RawStmt *parsetree, const char *query_string,
 					   Oid *paramTypes, int numParams)
 {
 	Query	   *query;
@@ -676,7 +676,7 @@ pg_analyze_and_rewrite(Node *parsetree, const char *query_string,
  * hooks instead of a fixed list of parameter datatypes.
  */
 List *
-pg_analyze_and_rewrite_params(Node *parsetree,
+pg_analyze_and_rewrite_params(RawStmt *parsetree,
 							  const char *query_string,
 							  ParserSetupHook parserSetup,
 							  void *parserSetupArg)
@@ -833,8 +833,10 @@ pg_plan_query(Query *querytree, int cursorOptions, ParamListInfo boundParams)
 /*
  * Generate plans for a list of already-rewritten queries.
  *
- * Normal optimizable statements generate PlannedStmt entries in the result
- * list.  Utility statements are simply represented by their statement nodes.
+ * For normal optimizable statements, invoke the planner.  For utility
+ * statements, just make a wrapper PlannedStmt node.
+ *
+ * The result is a list of PlannedStmt nodes.
  */
 List *
 pg_plan_queries(List *querytrees, int cursorOptions, ParamListInfo boundParams)
@@ -845,16 +847,21 @@ pg_plan_queries(List *querytrees, int cursorOptions, ParamListInfo boundParams)
 	foreach(query_list, querytrees)
 	{
 		Query	   *query = (Query *) lfirst(query_list);
-		Node	   *stmt;
+		PlannedStmt *stmt;
 
 		if (query->commandType == CMD_UTILITY)
 		{
-			/* Utility commands have no plans. */
-			stmt = query->utilityStmt;
+			/* Utility commands require no planning. */
+			stmt = makeNode(PlannedStmt);
+			stmt->commandType = CMD_UTILITY;
+			stmt->canSetTag = query->canSetTag;
+			stmt->utilityStmt = query->utilityStmt;
+			stmt->stmt_location = query->stmt_location;
+			stmt->stmt_len = query->stmt_len;
 		}
 		else
 		{
-			stmt = (Node *) pg_plan_query(query, cursorOptions, boundParams);
+			stmt = pg_plan_query(query, cursorOptions, boundParams);
 		}
 
 		stmt_list = lappend(stmt_list, stmt);
@@ -955,7 +962,7 @@ exec_simple_query(const char *query_string)
 	 */
 	foreach(parsetree_item, parsetree_list)
 	{
-		Node	   *parsetree = (Node *) lfirst(parsetree_item);
+		RawStmt    *parsetree = (RawStmt *) lfirst(parsetree_item);
 		bool		snapshot_set = false;
 		const char *commandTag;
 		char		completionTag[COMPLETION_TAG_BUFSIZE];
@@ -971,7 +978,7 @@ exec_simple_query(const char *query_string)
 		 * do any special start-of-SQL-command processing needed by the
 		 * destination.
 		 */
-		commandTag = CreateCommandTag(parsetree);
+		commandTag = CreateCommandTag(parsetree->stmt);
 
 		set_ps_display(commandTag, false);
 
@@ -986,7 +993,7 @@ exec_simple_query(const char *query_string)
 		 * state, but not many...)
 		 */
 		if (IsAbortedTransactionBlockState() &&
-			!IsTransactionExitStmt(parsetree))
+			!IsTransactionExitStmt(parsetree->stmt))
 			ereport(ERROR,
 					(errcode(ERRCODE_IN_FAILED_SQL_TRANSACTION),
 					 errmsg("current transaction is aborted, "
@@ -1061,9 +1068,9 @@ exec_simple_query(const char *query_string)
 		 * backward compatibility...)
 		 */
 		format = 0;				/* TEXT is default */
-		if (IsA(parsetree, FetchStmt))
+		if (IsA(parsetree->stmt, FetchStmt))
 		{
-			FetchStmt  *stmt = (FetchStmt *) parsetree;
+			FetchStmt  *stmt = (FetchStmt *) parsetree->stmt;
 
 			if (!stmt->ismove)
 			{
@@ -1102,7 +1109,7 @@ exec_simple_query(const char *query_string)
 
 		PortalDrop(portal, false);
 
-		if (IsA(parsetree, TransactionStmt))
+		if (IsA(parsetree->stmt, TransactionStmt))
 		{
 			/*
 			 * If this was a transaction control statement, commit it. We will
@@ -1194,7 +1201,7 @@ exec_parse_message(const char *query_string,	/* string to execute */
 	MemoryContext unnamed_stmt_context = NULL;
 	MemoryContext oldcontext;
 	List	   *parsetree_list;
-	Node	   *raw_parse_tree;
+	RawStmt    *raw_parse_tree;
 	const char *commandTag;
 	List	   *querytree_list;
 	CachedPlanSource *psrc;
@@ -1279,12 +1286,12 @@ exec_parse_message(const char *query_string,	/* string to execute */
 		bool		snapshot_set = false;
 		int			i;
 
-		raw_parse_tree = (Node *) linitial(parsetree_list);
+		raw_parse_tree = (RawStmt *) linitial(parsetree_list);
 
 		/*
 		 * Get the command name for possible use in status display.
 		 */
-		commandTag = CreateCommandTag(raw_parse_tree);
+		commandTag = CreateCommandTag(raw_parse_tree->stmt);
 
 		/*
 		 * If we are in an aborted transaction, reject all commands except
@@ -1295,7 +1302,7 @@ exec_parse_message(const char *query_string,	/* string to execute */
 		 * state, but not many...)
 		 */
 		if (IsAbortedTransactionBlockState() &&
-			!IsTransactionExitStmt(raw_parse_tree))
+			!IsTransactionExitStmt(raw_parse_tree->stmt))
 			ereport(ERROR,
 					(errcode(ERRCODE_IN_FAILED_SQL_TRANSACTION),
 					 errmsg("current transaction is aborted, "
@@ -1552,7 +1559,7 @@ exec_bind_message(StringInfo input_message)
 	 * functions.
 	 */
 	if (IsAbortedTransactionBlockState() &&
-		(!IsTransactionExitStmt(psrc->raw_parse_tree) ||
+		(!IsTransactionExitStmt(psrc->raw_parse_tree->stmt) ||
 		 numParams != 0))
 		ereport(ERROR,
 				(errcode(ERRCODE_IN_FAILED_SQL_TRANSACTION),
@@ -2140,11 +2147,11 @@ errdetail_execute(List *raw_parsetree_list)
 
 	foreach(parsetree_item, raw_parsetree_list)
 	{
-		Node	   *parsetree = (Node *) lfirst(parsetree_item);
+		RawStmt    *parsetree = (RawStmt *) lfirst(parsetree_item);
 
-		if (IsA(parsetree, ExecuteStmt))
+		if (IsA(parsetree->stmt, ExecuteStmt))
 		{
-			ExecuteStmt *stmt = (ExecuteStmt *) parsetree;
+			ExecuteStmt *stmt = (ExecuteStmt *) parsetree->stmt;
 			PreparedStatement *pstmt;
 
 			pstmt = FetchPreparedStatement(stmt->name, false);
@@ -2488,45 +2495,33 @@ IsTransactionExitStmt(Node *parsetree)
 	return false;
 }
 
-/* Test a list that might contain Query nodes or bare parsetrees */
+/* Test a list that contains PlannedStmt nodes */
 static bool
-IsTransactionExitStmtList(List *parseTrees)
+IsTransactionExitStmtList(List *pstmts)
 {
-	if (list_length(parseTrees) == 1)
+	if (list_length(pstmts) == 1)
 	{
-		Node	   *stmt = (Node *) linitial(parseTrees);
-
-		if (IsA(stmt, Query))
-		{
-			Query	   *query = (Query *) stmt;
+		PlannedStmt *pstmt = (PlannedStmt *) linitial(pstmts);
 
-			if (query->commandType == CMD_UTILITY &&
-				IsTransactionExitStmt(query->utilityStmt))
-				return true;
-		}
-		else if (IsTransactionExitStmt(stmt))
+		Assert(IsA(pstmt, PlannedStmt));
+		if (pstmt->commandType == CMD_UTILITY &&
+			IsTransactionExitStmt(pstmt->utilityStmt))
 			return true;
 	}
 	return false;
 }
 
-/* Test a list that might contain Query nodes or bare parsetrees */
+/* Test a list that contains PlannedStmt nodes */
 static bool
-IsTransactionStmtList(List *parseTrees)
+IsTransactionStmtList(List *pstmts)
 {
-	if (list_length(parseTrees) == 1)
+	if (list_length(pstmts) == 1)
 	{
-		Node	   *stmt = (Node *) linitial(parseTrees);
+		PlannedStmt *pstmt = (PlannedStmt *) linitial(pstmts);
 
-		if (IsA(stmt, Query))
-		{
-			Query	   *query = (Query *) stmt;
-
-			if (query->commandType == CMD_UTILITY &&
-				IsA(query->utilityStmt, TransactionStmt))
-				return true;
-		}
-		else if (IsA(stmt, TransactionStmt))
+		Assert(IsA(pstmt, PlannedStmt));
+		if (pstmt->commandType == CMD_UTILITY &&
+			IsA(pstmt->utilityStmt, TransactionStmt))
 			return true;
 	}
 	return false;
diff --git a/src/backend/tcop/pquery.c b/src/backend/tcop/pquery.c
index 3b5da73051d323134d27b661234211d2f8321e58..704be399cf7c0b435ddbd4362c65155e056797e5 100644
--- a/src/backend/tcop/pquery.c
+++ b/src/backend/tcop/pquery.c
@@ -43,7 +43,7 @@ static uint64 RunFromStore(Portal portal, ScanDirection direction, uint64 count,
 			 DestReceiver *dest);
 static uint64 PortalRunSelect(Portal portal, bool forward, long count,
 				DestReceiver *dest);
-static void PortalRunUtility(Portal portal, Node *utilityStmt,
+static void PortalRunUtility(Portal portal, PlannedStmt *pstmt,
 				 bool isTopLevel, bool setHoldSnapshot,
 				 DestReceiver *dest, char *completionTag);
 static void PortalRunMulti(Portal portal,
@@ -73,7 +73,6 @@ CreateQueryDesc(PlannedStmt *plannedstmt,
 
 	qd->operation = plannedstmt->commandType;	/* operation */
 	qd->plannedstmt = plannedstmt;		/* plan */
-	qd->utilitystmt = plannedstmt->utilityStmt; /* in case DECLARE CURSOR */
 	qd->sourceText = sourceText;	/* query text */
 	qd->snapshot = RegisterSnapshot(snapshot);	/* snapshot */
 	/* RI check snapshot */
@@ -92,37 +91,6 @@ CreateQueryDesc(PlannedStmt *plannedstmt,
 	return qd;
 }
 
-/*
- * CreateUtilityQueryDesc
- */
-QueryDesc *
-CreateUtilityQueryDesc(Node *utilitystmt,
-					   const char *sourceText,
-					   Snapshot snapshot,
-					   DestReceiver *dest,
-					   ParamListInfo params)
-{
-	QueryDesc  *qd = (QueryDesc *) palloc(sizeof(QueryDesc));
-
-	qd->operation = CMD_UTILITY;	/* operation */
-	qd->plannedstmt = NULL;
-	qd->utilitystmt = utilitystmt;		/* utility command */
-	qd->sourceText = sourceText;	/* query text */
-	qd->snapshot = RegisterSnapshot(snapshot);	/* snapshot */
-	qd->crosscheck_snapshot = InvalidSnapshot;	/* RI check snapshot */
-	qd->dest = dest;			/* output dest */
-	qd->params = params;		/* parameter values passed into query */
-	qd->instrument_options = false;		/* uninteresting for utilities */
-
-	/* null these fields until set by ExecutorStart */
-	qd->tupDesc = NULL;
-	qd->estate = NULL;
-	qd->planstate = NULL;
-	qd->totaltime = NULL;
-
-	return qd;
-}
-
 /*
  * FreeQueryDesc
  */
@@ -236,7 +204,7 @@ ProcessQuery(PlannedStmt *plan,
  * ChoosePortalStrategy
  *		Select portal execution strategy given the intended statement list.
  *
- * The list elements can be Querys, PlannedStmts, or utility statements.
+ * The list elements can be Querys or PlannedStmts.
  * That's more general than portals need, but plancache.c uses this too.
  *
  * See the comments in portal.h.
@@ -263,16 +231,14 @@ ChoosePortalStrategy(List *stmts)
 
 			if (query->canSetTag)
 			{
-				if (query->commandType == CMD_SELECT &&
-					query->utilityStmt == NULL)
+				if (query->commandType == CMD_SELECT)
 				{
 					if (query->hasModifyingCTE)
 						return PORTAL_ONE_MOD_WITH;
 					else
 						return PORTAL_ONE_SELECT;
 				}
-				if (query->commandType == CMD_UTILITY &&
-					query->utilityStmt != NULL)
+				if (query->commandType == CMD_UTILITY)
 				{
 					if (UtilityReturnsTuples(query->utilityStmt))
 						return PORTAL_UTIL_SELECT;
@@ -287,24 +253,24 @@ ChoosePortalStrategy(List *stmts)
 
 			if (pstmt->canSetTag)
 			{
-				if (pstmt->commandType == CMD_SELECT &&
-					pstmt->utilityStmt == NULL)
+				if (pstmt->commandType == CMD_SELECT)
 				{
 					if (pstmt->hasModifyingCTE)
 						return PORTAL_ONE_MOD_WITH;
 					else
 						return PORTAL_ONE_SELECT;
 				}
+				if (pstmt->commandType == CMD_UTILITY)
+				{
+					if (UtilityReturnsTuples(pstmt->utilityStmt))
+						return PORTAL_UTIL_SELECT;
+					/* it can't be ONE_RETURNING, so give up */
+					return PORTAL_MULTI_QUERY;
+				}
 			}
 		}
 		else
-		{
-			/* must be a utility command; assume it's canSetTag */
-			if (UtilityReturnsTuples(stmt))
-				return PORTAL_UTIL_SELECT;
-			/* it can't be ONE_RETURNING, so give up */
-			return PORTAL_MULTI_QUERY;
-		}
+			elog(ERROR, "unrecognized node type: %d", (int) nodeTag(stmt));
 	}
 
 	/*
@@ -325,7 +291,8 @@ ChoosePortalStrategy(List *stmts)
 			{
 				if (++nSetTag > 1)
 					return PORTAL_MULTI_QUERY;	/* no need to look further */
-				if (query->returningList == NIL)
+				if (query->commandType == CMD_UTILITY ||
+					query->returningList == NIL)
 					return PORTAL_MULTI_QUERY;	/* no need to look further */
 			}
 		}
@@ -337,11 +304,13 @@ ChoosePortalStrategy(List *stmts)
 			{
 				if (++nSetTag > 1)
 					return PORTAL_MULTI_QUERY;	/* no need to look further */
-				if (!pstmt->hasReturning)
+				if (pstmt->commandType == CMD_UTILITY ||
+					!pstmt->hasReturning)
 					return PORTAL_MULTI_QUERY;	/* no need to look further */
 			}
 		}
-		/* otherwise, utility command, assumed not canSetTag */
+		else
+			elog(ERROR, "unrecognized node type: %d", (int) nodeTag(stmt));
 	}
 	if (nSetTag == 1)
 		return PORTAL_ONE_RETURNING;
@@ -364,7 +333,7 @@ FetchPortalTargetList(Portal portal)
 	if (portal->strategy == PORTAL_MULTI_QUERY)
 		return NIL;
 	/* get the primary statement and find out what it returns */
-	return FetchStatementTargetList(PortalGetPrimaryStmt(portal));
+	return FetchStatementTargetList((Node *) PortalGetPrimaryStmt(portal));
 }
 
 /*
@@ -372,7 +341,7 @@ FetchPortalTargetList(Portal portal)
  *		Given a statement that returns tuples, extract the query targetlist.
  *		Returns NIL if the statement doesn't have a determinable targetlist.
  *
- * This can be applied to a Query, a PlannedStmt, or a utility statement.
+ * This can be applied to a Query or a PlannedStmt.
  * That's more general than portals need, but plancache.c uses this too.
  *
  * Note: do not modify the result.
@@ -388,16 +357,14 @@ FetchStatementTargetList(Node *stmt)
 	{
 		Query	   *query = (Query *) stmt;
 
-		if (query->commandType == CMD_UTILITY &&
-			query->utilityStmt != NULL)
+		if (query->commandType == CMD_UTILITY)
 		{
 			/* transfer attention to utility statement */
 			stmt = query->utilityStmt;
 		}
 		else
 		{
-			if (query->commandType == CMD_SELECT &&
-				query->utilityStmt == NULL)
+			if (query->commandType == CMD_SELECT)
 				return query->targetList;
 			if (query->returningList)
 				return query->returningList;
@@ -408,12 +375,19 @@ FetchStatementTargetList(Node *stmt)
 	{
 		PlannedStmt *pstmt = (PlannedStmt *) stmt;
 
-		if (pstmt->commandType == CMD_SELECT &&
-			pstmt->utilityStmt == NULL)
-			return pstmt->planTree->targetlist;
-		if (pstmt->hasReturning)
-			return pstmt->planTree->targetlist;
-		return NIL;
+		if (pstmt->commandType == CMD_UTILITY)
+		{
+			/* transfer attention to utility statement */
+			stmt = pstmt->utilityStmt;
+		}
+		else
+		{
+			if (pstmt->commandType == CMD_SELECT)
+				return pstmt->planTree->targetlist;
+			if (pstmt->hasReturning)
+				return pstmt->planTree->targetlist;
+			return NIL;
+		}
 	}
 	if (IsA(stmt, FetchStmt))
 	{
@@ -566,8 +540,7 @@ PortalStart(Portal portal, ParamListInfo params,
 				{
 					PlannedStmt *pstmt;
 
-					pstmt = (PlannedStmt *) PortalGetPrimaryStmt(portal);
-					Assert(IsA(pstmt, PlannedStmt));
+					pstmt = PortalGetPrimaryStmt(portal);
 					portal->tupDesc =
 						ExecCleanTypeFromTL(pstmt->planTree->targetlist,
 											false);
@@ -588,10 +561,10 @@ PortalStart(Portal portal, ParamListInfo params,
 				 * take care of it if needed.
 				 */
 				{
-					Node	   *ustmt = PortalGetPrimaryStmt(portal);
+					PlannedStmt *pstmt = PortalGetPrimaryStmt(portal);
 
-					Assert(!IsA(ustmt, PlannedStmt));
-					portal->tupDesc = UtilityTupleDescriptor(ustmt);
+					Assert(pstmt->commandType == CMD_UTILITY);
+					portal->tupDesc = UtilityTupleDescriptor(pstmt->utilityStmt);
 				}
 
 				/*
@@ -1047,7 +1020,7 @@ FillPortalStore(Portal portal, bool isTopLevel)
 			break;
 
 		case PORTAL_UTIL_SELECT:
-			PortalRunUtility(portal, (Node *) linitial(portal->stmts),
+			PortalRunUtility(portal, (PlannedStmt *) linitial(portal->stmts),
 							 isTopLevel, true, treceiver, completionTag);
 			break;
 
@@ -1143,10 +1116,11 @@ RunFromStore(Portal portal, ScanDirection direction, uint64 count,
  *		Execute a utility statement inside a portal.
  */
 static void
-PortalRunUtility(Portal portal, Node *utilityStmt,
+PortalRunUtility(Portal portal, PlannedStmt *pstmt,
 				 bool isTopLevel, bool setHoldSnapshot,
 				 DestReceiver *dest, char *completionTag)
 {
+	Node	   *utilityStmt = pstmt->utilityStmt;
 	Snapshot	snapshot;
 
 	/*
@@ -1186,7 +1160,7 @@ PortalRunUtility(Portal portal, Node *utilityStmt,
 	else
 		snapshot = NULL;
 
-	ProcessUtility(utilityStmt,
+	ProcessUtility(pstmt,
 				   portal->sourceText,
 			   isTopLevel ? PROCESS_UTILITY_TOPLEVEL : PROCESS_UTILITY_QUERY,
 				   portal->portalParams,
@@ -1241,21 +1215,18 @@ PortalRunMulti(Portal portal,
 	 */
 	foreach(stmtlist_item, portal->stmts)
 	{
-		Node	   *stmt = (Node *) lfirst(stmtlist_item);
+		PlannedStmt *pstmt = (PlannedStmt *) lfirst(stmtlist_item);
 
 		/*
 		 * If we got a cancel signal in prior command, quit
 		 */
 		CHECK_FOR_INTERRUPTS();
 
-		if (IsA(stmt, PlannedStmt) &&
-			((PlannedStmt *) stmt)->utilityStmt == NULL)
+		if (pstmt->utilityStmt == NULL)
 		{
 			/*
 			 * process a plannable query.
 			 */
-			PlannedStmt *pstmt = (PlannedStmt *) stmt;
-
 			TRACE_POSTGRESQL_QUERY_EXECUTE_START();
 
 			if (log_executor_stats)
@@ -1320,9 +1291,6 @@ PortalRunMulti(Portal portal,
 			/*
 			 * process utility functions (create, destroy, etc..)
 			 *
-			 * These are assumed canSetTag if they're the only stmt in the
-			 * portal.
-			 *
 			 * We must not set a snapshot here for utility commands (if one is
 			 * needed, PortalRunUtility will do it).  If a utility command is
 			 * alone in a portal then everything's fine.  The only case where
@@ -1331,18 +1299,18 @@ PortalRunMulti(Portal portal,
 			 * whether it has a snapshot or not, so we just leave the current
 			 * snapshot alone if we have one.
 			 */
-			if (list_length(portal->stmts) == 1)
+			if (pstmt->canSetTag)
 			{
 				Assert(!active_snapshot_set);
 				/* statement can set tag string */
-				PortalRunUtility(portal, stmt, isTopLevel, false,
+				PortalRunUtility(portal, pstmt, isTopLevel, false,
 								 dest, completionTag);
 			}
 			else
 			{
-				Assert(IsA(stmt, NotifyStmt));
+				Assert(IsA(pstmt->utilityStmt, NotifyStmt));
 				/* stmt added by rewrite cannot set tag */
-				PortalRunUtility(portal, stmt, isTopLevel, false,
+				PortalRunUtility(portal, pstmt, isTopLevel, false,
 								 altdest, NULL);
 			}
 		}
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 127dc862e8d86b650d0036d82d6d4c8ac51522e9..149210133606443b3f3ba142f4df18836530bc4c 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -72,7 +72,7 @@ ProcessUtility_hook_type ProcessUtility_hook = NULL;
 
 /* local function declarations */
 static void ProcessUtilitySlow(ParseState *pstate,
-							   Node *parsetree,
+				   PlannedStmt *pstmt,
 				   const char *queryString,
 				   ProcessUtilityContext context,
 				   ParamListInfo params,
@@ -88,35 +88,33 @@ static void ExecDropStmt(DropStmt *stmt, bool isTopLevel);
  * the query must be *in truth* read-only, because the caller wishes
  * not to do CommandCounterIncrement for it.
  *
- * Note: currently no need to support Query nodes here
+ * Note: currently no need to support raw or analyzed queries here
  */
 bool
-CommandIsReadOnly(Node *parsetree)
+CommandIsReadOnly(PlannedStmt *pstmt)
 {
-	if (IsA(parsetree, PlannedStmt))
+	Assert(IsA(pstmt, PlannedStmt));
+	switch (pstmt->commandType)
 	{
-		PlannedStmt *stmt = (PlannedStmt *) parsetree;
-
-		switch (stmt->commandType)
-		{
-			case CMD_SELECT:
-				if (stmt->rowMarks != NIL)
-					return false;		/* SELECT FOR [KEY] UPDATE/SHARE */
-				else if (stmt->hasModifyingCTE)
-					return false;		/* data-modifying CTE */
-				else
-					return true;
-			case CMD_UPDATE:
-			case CMD_INSERT:
-			case CMD_DELETE:
-				return false;
-			default:
-				elog(WARNING, "unrecognized commandType: %d",
-					 (int) stmt->commandType);
-				break;
-		}
+		case CMD_SELECT:
+			if (pstmt->rowMarks != NIL)
+				return false;	/* SELECT FOR [KEY] UPDATE/SHARE */
+			else if (pstmt->hasModifyingCTE)
+				return false;	/* data-modifying CTE */
+			else
+				return true;
+		case CMD_UPDATE:
+		case CMD_INSERT:
+		case CMD_DELETE:
+			return false;
+		case CMD_UTILITY:
+			/* For now, treat all utility commands as read/write */
+			return false;
+		default:
+			elog(WARNING, "unrecognized commandType: %d",
+				 (int) pstmt->commandType);
+			break;
 	}
-	/* For now, treat all utility commands as read/write */
 	return false;
 }
 
@@ -297,7 +295,7 @@ CheckRestrictedOperation(const char *cmdname)
  * ProcessUtility
  *		general utility function invoker
  *
- *	parsetree: the parse tree for the utility statement
+ *	pstmt: PlannedStmt wrapper for the utility statement
  *	queryString: original source text of command
  *	context: identifies source of statement (toplevel client command,
  *		non-toplevel client command, subcommand of a larger utility command)
@@ -315,13 +313,15 @@ CheckRestrictedOperation(const char *cmdname)
  * completionTag may be NULL if caller doesn't want a status string.
  */
 void
-ProcessUtility(Node *parsetree,
+ProcessUtility(PlannedStmt *pstmt,
 			   const char *queryString,
 			   ProcessUtilityContext context,
 			   ParamListInfo params,
 			   DestReceiver *dest,
 			   char *completionTag)
 {
+	Assert(IsA(pstmt, PlannedStmt));
+	Assert(pstmt->commandType == CMD_UTILITY);
 	Assert(queryString != NULL);	/* required as of 8.4 */
 
 	/*
@@ -330,11 +330,11 @@ ProcessUtility(Node *parsetree,
 	 * call standard_ProcessUtility().
 	 */
 	if (ProcessUtility_hook)
-		(*ProcessUtility_hook) (parsetree, queryString,
+		(*ProcessUtility_hook) (pstmt, queryString,
 								context, params,
 								dest, completionTag);
 	else
-		standard_ProcessUtility(parsetree, queryString,
+		standard_ProcessUtility(pstmt, queryString,
 								context, params,
 								dest, completionTag);
 }
@@ -351,13 +351,14 @@ ProcessUtility(Node *parsetree,
  * which requires being in a valid transaction.
  */
 void
-standard_ProcessUtility(Node *parsetree,
+standard_ProcessUtility(PlannedStmt *pstmt,
 						const char *queryString,
 						ProcessUtilityContext context,
 						ParamListInfo params,
 						DestReceiver *dest,
 						char *completionTag)
 {
+	Node	   *parsetree = pstmt->utilityStmt;
 	bool		isTopLevel = (context == PROCESS_UTILITY_TOPLEVEL);
 	ParseState *pstate;
 
@@ -486,20 +487,10 @@ standard_ProcessUtility(Node *parsetree,
 
 			/*
 			 * Portal (cursor) manipulation
-			 *
-			 * Note: DECLARE CURSOR is processed mostly as a SELECT, and
-			 * therefore what we will get here is a PlannedStmt not a bare
-			 * DeclareCursorStmt.
 			 */
-		case T_PlannedStmt:
-			{
-				PlannedStmt *stmt = (PlannedStmt *) parsetree;
-
-				if (stmt->utilityStmt == NULL ||
-					!IsA(stmt->utilityStmt, DeclareCursorStmt))
-					elog(ERROR, "non-DECLARE CURSOR PlannedStmt passed to ProcessUtility");
-				PerformCursorOpen(stmt, params, queryString, isTopLevel);
-			}
+		case T_DeclareCursorStmt:
+			PerformCursorOpen((DeclareCursorStmt *) parsetree, params,
+							  queryString, isTopLevel);
 			break;
 
 		case T_ClosePortalStmt:
@@ -545,7 +536,9 @@ standard_ProcessUtility(Node *parsetree,
 			{
 				uint64		processed;
 
-				DoCopy(pstate, (CopyStmt *) parsetree, &processed);
+				DoCopy(pstate, (CopyStmt *) parsetree,
+					   pstmt->stmt_location, pstmt->stmt_len,
+					   &processed);
 				if (completionTag)
 					snprintf(completionTag, COMPLETION_TAG_BUFSIZE,
 							 "COPY " UINT64_FORMAT, processed);
@@ -554,7 +547,8 @@ standard_ProcessUtility(Node *parsetree,
 
 		case T_PrepareStmt:
 			CheckRestrictedOperation("PREPARE");
-			PrepareQuery((PrepareStmt *) parsetree, queryString);
+			PrepareQuery((PrepareStmt *) parsetree, queryString,
+						 pstmt->stmt_location, pstmt->stmt_len);
 			break;
 
 		case T_ExecuteStmt:
@@ -808,11 +802,11 @@ standard_ProcessUtility(Node *parsetree,
 				GrantStmt  *stmt = (GrantStmt *) parsetree;
 
 				if (EventTriggerSupportsGrantObjectType(stmt->objtype))
-					ProcessUtilitySlow(pstate, parsetree, queryString,
+					ProcessUtilitySlow(pstate, pstmt, queryString,
 									   context, params,
 									   dest, completionTag);
 				else
-					ExecuteGrantStmt((GrantStmt *) parsetree);
+					ExecuteGrantStmt(stmt);
 			}
 			break;
 
@@ -821,7 +815,7 @@ standard_ProcessUtility(Node *parsetree,
 				DropStmt   *stmt = (DropStmt *) parsetree;
 
 				if (EventTriggerSupportsObjectType(stmt->removeType))
-					ProcessUtilitySlow(pstate, parsetree, queryString,
+					ProcessUtilitySlow(pstate, pstmt, queryString,
 									   context, params,
 									   dest, completionTag);
 				else
@@ -834,7 +828,7 @@ standard_ProcessUtility(Node *parsetree,
 				RenameStmt *stmt = (RenameStmt *) parsetree;
 
 				if (EventTriggerSupportsObjectType(stmt->renameType))
-					ProcessUtilitySlow(pstate, parsetree, queryString,
+					ProcessUtilitySlow(pstate, pstmt, queryString,
 									   context, params,
 									   dest, completionTag);
 				else
@@ -847,7 +841,7 @@ standard_ProcessUtility(Node *parsetree,
 				AlterObjectDependsStmt *stmt = (AlterObjectDependsStmt *) parsetree;
 
 				if (EventTriggerSupportsObjectType(stmt->objectType))
-					ProcessUtilitySlow(pstate, parsetree, queryString,
+					ProcessUtilitySlow(pstate, pstmt, queryString,
 									   context, params,
 									   dest, completionTag);
 				else
@@ -860,7 +854,7 @@ standard_ProcessUtility(Node *parsetree,
 				AlterObjectSchemaStmt *stmt = (AlterObjectSchemaStmt *) parsetree;
 
 				if (EventTriggerSupportsObjectType(stmt->objectType))
-					ProcessUtilitySlow(pstate, parsetree, queryString,
+					ProcessUtilitySlow(pstate, pstmt, queryString,
 									   context, params,
 									   dest, completionTag);
 				else
@@ -873,7 +867,7 @@ standard_ProcessUtility(Node *parsetree,
 				AlterOwnerStmt *stmt = (AlterOwnerStmt *) parsetree;
 
 				if (EventTriggerSupportsObjectType(stmt->objectType))
-					ProcessUtilitySlow(pstate, parsetree, queryString,
+					ProcessUtilitySlow(pstate, pstmt, queryString,
 									   context, params,
 									   dest, completionTag);
 				else
@@ -886,11 +880,11 @@ standard_ProcessUtility(Node *parsetree,
 				CommentStmt *stmt = (CommentStmt *) parsetree;
 
 				if (EventTriggerSupportsObjectType(stmt->objtype))
-					ProcessUtilitySlow(pstate, parsetree, queryString,
+					ProcessUtilitySlow(pstate, pstmt, queryString,
 									   context, params,
 									   dest, completionTag);
 				else
-					CommentObject((CommentStmt *) parsetree);
+					CommentObject(stmt);
 				break;
 			}
 
@@ -899,7 +893,7 @@ standard_ProcessUtility(Node *parsetree,
 				SecLabelStmt *stmt = (SecLabelStmt *) parsetree;
 
 				if (EventTriggerSupportsObjectType(stmt->objtype))
-					ProcessUtilitySlow(pstate, parsetree, queryString,
+					ProcessUtilitySlow(pstate, pstmt, queryString,
 									   context, params,
 									   dest, completionTag);
 				else
@@ -909,7 +903,7 @@ standard_ProcessUtility(Node *parsetree,
 
 		default:
 			/* All other statement types have event trigger support */
-			ProcessUtilitySlow(pstate, parsetree, queryString,
+			ProcessUtilitySlow(pstate, pstmt, queryString,
 							   context, params,
 							   dest, completionTag);
 			break;
@@ -925,13 +919,14 @@ standard_ProcessUtility(Node *parsetree,
  */
 static void
 ProcessUtilitySlow(ParseState *pstate,
-				   Node *parsetree,
+				   PlannedStmt *pstmt,
 				   const char *queryString,
 				   ProcessUtilityContext context,
 				   ParamListInfo params,
 				   DestReceiver *dest,
 				   char *completionTag)
 {
+	Node	   *parsetree = pstmt->utilityStmt;
 	bool		isTopLevel = (context == PROCESS_UTILITY_TOPLEVEL);
 	bool		isCompleteQuery = (context <= PROCESS_UTILITY_QUERY);
 	bool		needCleanup;
@@ -955,7 +950,9 @@ ProcessUtilitySlow(ParseState *pstate,
 				 */
 			case T_CreateSchemaStmt:
 				CreateSchemaCommand((CreateSchemaStmt *) parsetree,
-									queryString);
+									queryString,
+									pstmt->stmt_location,
+									pstmt->stmt_len);
 
 				/*
 				 * EventTriggerCollectSimpleCommand called by
@@ -1036,7 +1033,16 @@ ProcessUtilitySlow(ParseState *pstate,
 							 * call will stash the objects so created into our
 							 * event trigger context.
 							 */
-							ProcessUtility(stmt,
+							PlannedStmt *wrapper;
+
+							wrapper = makeNode(PlannedStmt);
+							wrapper->commandType = CMD_UTILITY;
+							wrapper->canSetTag = false;
+							wrapper->utilityStmt = stmt;
+							wrapper->stmt_location = pstmt->stmt_location;
+							wrapper->stmt_len = pstmt->stmt_len;
+
+							ProcessUtility(wrapper,
 										   queryString,
 										   PROCESS_UTILITY_SUBCOMMAND,
 										   params,
@@ -1105,8 +1111,16 @@ ProcessUtilitySlow(ParseState *pstate,
 								 * queued commands is consistent with the way
 								 * they are executed here.
 								 */
+								PlannedStmt *wrapper;
+
 								EventTriggerAlterTableEnd();
-								ProcessUtility(stmt,
+								wrapper = makeNode(PlannedStmt);
+								wrapper->commandType = CMD_UTILITY;
+								wrapper->canSetTag = false;
+								wrapper->utilityStmt = stmt;
+								wrapper->stmt_location = pstmt->stmt_location;
+								wrapper->stmt_len = pstmt->stmt_len;
+								ProcessUtility(wrapper,
 											   queryString,
 											   PROCESS_UTILITY_SUBCOMMAND,
 											   params,
@@ -1376,7 +1390,8 @@ ProcessUtilitySlow(ParseState *pstate,
 
 			case T_ViewStmt:	/* CREATE VIEW */
 				EventTriggerAlterTableStart(parsetree);
-				address = DefineView((ViewStmt *) parsetree, queryString);
+				address = DefineView((ViewStmt *) parsetree, queryString,
+									 pstmt->stmt_location, pstmt->stmt_len);
 				EventTriggerCollectSimpleCommand(address, secondaryObject,
 												 parsetree);
 				/* stashed internally */
@@ -1480,6 +1495,7 @@ ProcessUtilitySlow(ParseState *pstate,
 
 			case T_AlterTSConfigurationStmt:
 				AlterTSConfiguration((AlterTSConfigurationStmt *) parsetree);
+
 				/*
 				 * Commands are stashed in MakeConfigurationMapping and
 				 * DropConfigurationMapping, which are called from
@@ -1736,10 +1752,8 @@ QueryReturnsTuples(Query *parsetree)
 	switch (parsetree->commandType)
 	{
 		case CMD_SELECT:
-			/* returns tuples ... unless it's DECLARE CURSOR */
-			if (parsetree->utilityStmt == NULL)
-				return true;
-			break;
+			/* returns tuples */
+			return true;
 		case CMD_INSERT:
 		case CMD_UPDATE:
 		case CMD_DELETE:
@@ -1780,6 +1794,13 @@ UtilityContainsQuery(Node *parsetree)
 
 	switch (nodeTag(parsetree))
 	{
+		case T_DeclareCursorStmt:
+			qry = (Query *) ((DeclareCursorStmt *) parsetree)->query;
+			Assert(IsA(qry, Query));
+			if (qry->commandType == CMD_UTILITY)
+				return UtilityContainsQuery(qry->utilityStmt);
+			return qry;
+
 		case T_ExplainStmt:
 			qry = (Query *) ((ExplainStmt *) parsetree)->query;
 			Assert(IsA(qry, Query));
@@ -1931,7 +1952,8 @@ AlterObjectTypeCommandTag(ObjectType objtype)
 /*
  * CreateCommandTag
  *		utility to get a string representation of the command operation,
- *		given either a raw (un-analyzed) parsetree or a planned query.
+ *		given either a raw (un-analyzed) parsetree, an analyzed Query,
+ *		or a PlannedStmt.
  *
  * This must handle all command types, but since the vast majority
  * of 'em are utility commands, it seems sensible to keep it here.
@@ -1946,6 +1968,11 @@ CreateCommandTag(Node *parsetree)
 
 	switch (nodeTag(parsetree))
 	{
+			/* recurse if we're given a RawStmt */
+		case T_RawStmt:
+			tag = CreateCommandTag(((RawStmt *) parsetree)->stmt);
+			break;
+
 			/* raw plannable queries */
 		case T_InsertStmt:
 			tag = "INSERT";
@@ -2608,12 +2635,7 @@ CreateCommandTag(Node *parsetree)
 						 * will be useful for complaints about read-only
 						 * statements
 						 */
-						if (stmt->utilityStmt != NULL)
-						{
-							Assert(IsA(stmt->utilityStmt, DeclareCursorStmt));
-							tag = "DECLARE CURSOR";
-						}
-						else if (stmt->rowMarks != NIL)
+						if (stmt->rowMarks != NIL)
 						{
 							/* not 100% but probably close enough */
 							switch (((PlanRowMark *) linitial(stmt->rowMarks))->strength)
@@ -2647,6 +2669,9 @@ CreateCommandTag(Node *parsetree)
 					case CMD_DELETE:
 						tag = "DELETE";
 						break;
+					case CMD_UTILITY:
+						tag = CreateCommandTag(stmt->utilityStmt);
+						break;
 					default:
 						elog(WARNING, "unrecognized commandType: %d",
 							 (int) stmt->commandType);
@@ -2670,12 +2695,7 @@ CreateCommandTag(Node *parsetree)
 						 * will be useful for complaints about read-only
 						 * statements
 						 */
-						if (stmt->utilityStmt != NULL)
-						{
-							Assert(IsA(stmt->utilityStmt, DeclareCursorStmt));
-							tag = "DECLARE CURSOR";
-						}
-						else if (stmt->rowMarks != NIL)
+						if (stmt->rowMarks != NIL)
 						{
 							/* not 100% but probably close enough */
 							switch (((RowMarkClause *) linitial(stmt->rowMarks))->strength)
@@ -2735,7 +2755,8 @@ CreateCommandTag(Node *parsetree)
 /*
  * GetCommandLogLevel
  *		utility to get the minimum log_statement level for a command,
- *		given either a raw (un-analyzed) parsetree or a planned query.
+ *		given either a raw (un-analyzed) parsetree, an analyzed Query,
+ *		or a PlannedStmt.
  *
  * This must handle all command types, but since the vast majority
  * of 'em are utility commands, it seems sensible to keep it here.
@@ -2747,6 +2768,11 @@ GetCommandLogLevel(Node *parsetree)
 
 	switch (nodeTag(parsetree))
 	{
+			/* recurse if we're given a RawStmt */
+		case T_RawStmt:
+			lev = GetCommandLogLevel(((RawStmt *) parsetree)->stmt);
+			break;
+
 			/* raw plannable queries */
 		case T_InsertStmt:
 		case T_DeleteStmt:
@@ -2850,7 +2876,7 @@ GetCommandLogLevel(Node *parsetree)
 				/* Look through an EXECUTE to the referenced stmt */
 				ps = FetchPreparedStatement(stmt->name, false);
 				if (ps && ps->plansource->raw_parse_tree)
-					lev = GetCommandLogLevel(ps->plansource->raw_parse_tree);
+					lev = GetCommandLogLevel(ps->plansource->raw_parse_tree->stmt);
 				else
 					lev = LOGSTMT_ALL;
 			}
@@ -3157,6 +3183,10 @@ GetCommandLogLevel(Node *parsetree)
 						lev = LOGSTMT_MOD;
 						break;
 
+					case CMD_UTILITY:
+						lev = GetCommandLogLevel(stmt->utilityStmt);
+						break;
+
 					default:
 						elog(WARNING, "unrecognized commandType: %d",
 							 (int) stmt->commandType);
diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c
index e48a878dc2a99eaa4b9e25d52f6f70de6d32b272..c31c603fbf5ad111e7bbd166fb1b64ac24ba6b53 100644
--- a/src/backend/utils/cache/plancache.c
+++ b/src/backend/utils/cache/plancache.c
@@ -77,7 +77,7 @@
  */
 #define IsTransactionStmtPlan(plansource)  \
 	((plansource)->raw_parse_tree && \
-	 IsA((plansource)->raw_parse_tree, TransactionStmt))
+	 IsA((plansource)->raw_parse_tree->stmt, TransactionStmt))
 
 /*
  * This is the head of the backend's list of "saved" CachedPlanSources (i.e.,
@@ -95,6 +95,7 @@ static CachedPlan *BuildCachedPlan(CachedPlanSource *plansource, List *qlist,
 static bool choose_custom_plan(CachedPlanSource *plansource,
 				   ParamListInfo boundParams);
 static double cached_plan_cost(CachedPlan *plan, bool include_planner);
+static Query *QueryListGetPrimaryStmt(List *stmts);
 static void AcquireExecutorLocks(List *stmt_list, bool acquire);
 static void AcquirePlannerLocks(List *stmt_list, bool acquire);
 static void ScanQueryForLocks(Query *parsetree, bool acquire);
@@ -147,7 +148,7 @@ InitPlanCache(void)
  * commandTag: compile-time-constant tag for query, or NULL if empty query
  */
 CachedPlanSource *
-CreateCachedPlan(Node *raw_parse_tree,
+CreateCachedPlan(RawStmt *raw_parse_tree,
 				 const char *query_string,
 				 const char *commandTag)
 {
@@ -230,7 +231,7 @@ CreateCachedPlan(Node *raw_parse_tree,
  * commandTag: compile-time-constant tag for query, or NULL if empty query
  */
 CachedPlanSource *
-CreateOneShotCachedPlan(Node *raw_parse_tree,
+CreateOneShotCachedPlan(RawStmt *raw_parse_tree,
 						const char *query_string,
 						const char *commandTag)
 {
@@ -555,7 +556,7 @@ static List *
 RevalidateCachedQuery(CachedPlanSource *plansource)
 {
 	bool		snapshot_set;
-	Node	   *rawtree;
+	RawStmt    *rawtree;
 	List	   *tlist;			/* transient query-tree list */
 	List	   *qlist;			/* permanent query-tree list */
 	TupleDesc	resultDesc;
@@ -976,7 +977,7 @@ BuildCachedPlan(CachedPlanSource *plansource, List *qlist,
 	{
 		PlannedStmt *plannedstmt = (PlannedStmt *) lfirst(lc);
 
-		if (!IsA(plannedstmt, PlannedStmt))
+		if (plannedstmt->commandType == CMD_UTILITY)
 			continue;			/* Ignore utility statements */
 
 		if (plannedstmt->transientPlan)
@@ -1071,7 +1072,7 @@ cached_plan_cost(CachedPlan *plan, bool include_planner)
 	{
 		PlannedStmt *plannedstmt = (PlannedStmt *) lfirst(lc);
 
-		if (!IsA(plannedstmt, PlannedStmt))
+		if (plannedstmt->commandType == CMD_UTILITY)
 			continue;			/* Ignore utility statements */
 
 		result += plannedstmt->planTree->total_cost;
@@ -1419,7 +1420,7 @@ CachedPlanIsValid(CachedPlanSource *plansource)
 List *
 CachedPlanGetTargetList(CachedPlanSource *plansource)
 {
-	Node	   *pstmt;
+	Query	   *pstmt;
 
 	/* Assert caller is doing things in a sane order */
 	Assert(plansource->magic == CACHEDPLANSOURCE_MAGIC);
@@ -1436,9 +1437,34 @@ CachedPlanGetTargetList(CachedPlanSource *plansource)
 	RevalidateCachedQuery(plansource);
 
 	/* Get the primary statement and find out what it returns */
-	pstmt = PortalListGetPrimaryStmt(plansource->query_list);
+	pstmt = QueryListGetPrimaryStmt(plansource->query_list);
 
-	return FetchStatementTargetList(pstmt);
+	return FetchStatementTargetList((Node *) pstmt);
+}
+
+/*
+ * QueryListGetPrimaryStmt
+ *		Get the "primary" stmt within a list, ie, the one marked canSetTag.
+ *
+ * Returns NULL if no such stmt.  If multiple queries within the list are
+ * marked canSetTag, returns the first one.  Neither of these cases should
+ * occur in present usages of this function.
+ */
+static Query *
+QueryListGetPrimaryStmt(List *stmts)
+{
+	ListCell   *lc;
+
+	foreach(lc, stmts)
+	{
+		Query	   *stmt = (Query *) lfirst(lc);
+
+		Assert(IsA(stmt, Query));
+
+		if (stmt->canSetTag)
+			return stmt;
+	}
+	return NULL;
 }
 
 /*
@@ -1456,8 +1482,9 @@ AcquireExecutorLocks(List *stmt_list, bool acquire)
 		int			rt_index;
 		ListCell   *lc2;
 
-		Assert(!IsA(plannedstmt, Query));
-		if (!IsA(plannedstmt, PlannedStmt))
+		Assert(IsA(plannedstmt, PlannedStmt));
+
+		if (plannedstmt->commandType == CMD_UTILITY)
 		{
 			/*
 			 * Ignore utility statements, except those (such as EXPLAIN) that
@@ -1466,7 +1493,7 @@ AcquireExecutorLocks(List *stmt_list, bool acquire)
 			 * rule rewriting, because rewriting doesn't change the query
 			 * representation.
 			 */
-			Query	   *query = UtilityContainsQuery((Node *) plannedstmt);
+			Query	   *query = UtilityContainsQuery(plannedstmt->utilityStmt);
 
 			if (query)
 				ScanQueryForLocks(query, acquire);
@@ -1654,8 +1681,7 @@ PlanCacheComputeResultDesc(List *stmt_list)
 			return ExecCleanTypeFromTL(query->targetList, false);
 
 		case PORTAL_ONE_RETURNING:
-			query = (Query *) PortalListGetPrimaryStmt(stmt_list);
-			Assert(IsA(query, Query));
+			query = QueryListGetPrimaryStmt(stmt_list);
 			Assert(query->returningList);
 			return ExecCleanTypeFromTL(query->returningList, false);
 
@@ -1720,8 +1746,7 @@ PlanCacheRelCallback(Datum arg, Oid relid)
 			{
 				PlannedStmt *plannedstmt = (PlannedStmt *) lfirst(lc);
 
-				Assert(!IsA(plannedstmt, Query));
-				if (!IsA(plannedstmt, PlannedStmt))
+				if (plannedstmt->commandType == CMD_UTILITY)
 					continue;	/* Ignore utility statements */
 				if ((relid == InvalidOid) ? plannedstmt->relationOids != NIL :
 					list_member_oid(plannedstmt->relationOids, relid))
@@ -1795,8 +1820,7 @@ PlanCacheFuncCallback(Datum arg, int cacheid, uint32 hashvalue)
 				PlannedStmt *plannedstmt = (PlannedStmt *) lfirst(lc);
 				ListCell   *lc3;
 
-				Assert(!IsA(plannedstmt, Query));
-				if (!IsA(plannedstmt, PlannedStmt))
+				if (plannedstmt->commandType == CMD_UTILITY)
 					continue;	/* Ignore utility statements */
 				foreach(lc3, plannedstmt->invalItems)
 				{
diff --git a/src/backend/utils/mmgr/portalmem.c b/src/backend/utils/mmgr/portalmem.c
index 5a15ed21d669eb6c326fcfa76d2500bd743dd34c..e8ebc4684caf32ff167d765c07c3c7de462cf46c 100644
--- a/src/backend/utils/mmgr/portalmem.c
+++ b/src/backend/utils/mmgr/portalmem.c
@@ -139,45 +139,26 @@ GetPortalByName(const char *name)
 }
 
 /*
- * PortalListGetPrimaryStmt
+ * PortalGetPrimaryStmt
  *		Get the "primary" stmt within a portal, ie, the one marked canSetTag.
  *
  * Returns NULL if no such stmt.  If multiple PlannedStmt structs within the
  * portal are marked canSetTag, returns the first one.  Neither of these
  * cases should occur in present usages of this function.
- *
- * Copes if given a list of Querys --- can't happen in a portal, but this
- * code also supports plancache.c, which needs both cases.
- *
- * Note: the reason this is just handed a List is so that plancache.c
- * can share the code.  For use with a portal, use PortalGetPrimaryStmt
- * rather than calling this directly.
  */
-Node *
-PortalListGetPrimaryStmt(List *stmts)
+PlannedStmt *
+PortalGetPrimaryStmt(Portal portal)
 {
 	ListCell   *lc;
 
-	foreach(lc, stmts)
+	foreach(lc, portal->stmts)
 	{
-		Node	   *stmt = (Node *) lfirst(lc);
+		PlannedStmt *stmt = (PlannedStmt *) lfirst(lc);
 
-		if (IsA(stmt, PlannedStmt))
-		{
-			if (((PlannedStmt *) stmt)->canSetTag)
-				return stmt;
-		}
-		else if (IsA(stmt, Query))
-		{
-			if (((Query *) stmt)->canSetTag)
-				return stmt;
-		}
-		else
-		{
-			/* Utility stmts are assumed canSetTag if they're the only stmt */
-			if (list_length(stmts) == 1)
-				return stmt;
-		}
+		Assert(IsA(stmt, PlannedStmt));
+
+		if (stmt->canSetTag)
+			return stmt;
 	}
 	return NULL;
 }
diff --git a/src/include/catalog/catversion.h b/src/include/catalog/catversion.h
index 95cfd53eeca9bd8e12da12205e60f0363acf27c0..29604b6f4edeaad50dc46a23fc911268ab70a220 100644
--- a/src/include/catalog/catversion.h
+++ b/src/include/catalog/catversion.h
@@ -53,6 +53,6 @@
  */
 
 /*							yyyymmddN */
-#define CATALOG_VERSION_NO	201612231
+#define CATALOG_VERSION_NO	201701141
 
 #endif
diff --git a/src/include/commands/copy.h b/src/include/commands/copy.h
index 4785631cef9cc2693443649d7fa54b7cf719b702..d63ca0f5e9966f4be93e8cbfe2acc707fea5a390 100644
--- a/src/include/commands/copy.h
+++ b/src/include/commands/copy.h
@@ -22,7 +22,8 @@
 /* CopyStateData is private in commands/copy.c */
 typedef struct CopyStateData *CopyState;
 
-extern Oid DoCopy(ParseState *state, const CopyStmt *stmt,
+extern void DoCopy(ParseState *state, const CopyStmt *stmt,
+	   int stmt_location, int stmt_len,
 	   uint64 *processed);
 
 extern void ProcessCopyOptions(ParseState *pstate, CopyState cstate, bool is_from, List *options);
diff --git a/src/include/commands/explain.h b/src/include/commands/explain.h
index 1820dd66bb255e83a5fcf54f40683692d79f5c0a..9191e186c15456bdc200d14f2beee298811d6b1a 100644
--- a/src/include/commands/explain.h
+++ b/src/include/commands/explain.h
@@ -49,6 +49,7 @@ typedef struct ExplainState
 
 /* Hook for plugins to get control in ExplainOneQuery() */
 typedef void (*ExplainOneQuery_hook_type) (Query *query,
+													   int cursorOptions,
 													   IntoClause *into,
 													   ExplainState *es,
 													 const char *queryString,
diff --git a/src/include/commands/portalcmds.h b/src/include/commands/portalcmds.h
index 0ae9b7213cdceaceaf191d21e291b9704c438a10..8f0e6c48f495aeaa58328e83ce06c436b586e8cd 100644
--- a/src/include/commands/portalcmds.h
+++ b/src/include/commands/portalcmds.h
@@ -18,7 +18,7 @@
 #include "utils/portal.h"
 
 
-extern void PerformCursorOpen(PlannedStmt *stmt, ParamListInfo params,
+extern void PerformCursorOpen(DeclareCursorStmt *cstmt, ParamListInfo params,
 				  const char *queryString, bool isTopLevel);
 
 extern void PerformPortalFetch(FetchStmt *stmt, DestReceiver *dest,
diff --git a/src/include/commands/prepare.h b/src/include/commands/prepare.h
index 0a3a8addca11d595f2e4c499e32a542c7af00c0d..d8d22edbbcd82545116be1a19722913780fc555d 100644
--- a/src/include/commands/prepare.h
+++ b/src/include/commands/prepare.h
@@ -35,7 +35,8 @@ typedef struct
 
 
 /* Utility statements PREPARE, EXECUTE, DEALLOCATE, EXPLAIN EXECUTE */
-extern void PrepareQuery(PrepareStmt *stmt, const char *queryString);
+extern void PrepareQuery(PrepareStmt *stmt, const char *queryString,
+			 int stmt_location, int stmt_len);
 extern void ExecuteQuery(ExecuteStmt *stmt, IntoClause *intoClause,
 			 const char *queryString, ParamListInfo params,
 			 DestReceiver *dest, char *completionTag);
diff --git a/src/include/commands/schemacmds.h b/src/include/commands/schemacmds.h
index 087878ddc33abdceee85ced502dcf82f66786a07..f07a389c7f6981f8a24dabe016f60db63c8e05dd 100644
--- a/src/include/commands/schemacmds.h
+++ b/src/include/commands/schemacmds.h
@@ -19,7 +19,8 @@
 #include "nodes/parsenodes.h"
 
 extern Oid CreateSchemaCommand(CreateSchemaStmt *parsetree,
-					const char *queryString);
+					const char *queryString,
+					int stmt_location, int stmt_len);
 
 extern void RemoveSchemaById(Oid schemaOid);
 
diff --git a/src/include/commands/view.h b/src/include/commands/view.h
index 648665ebe5d8e451c37ca38a73eb4632fc399f73..39763913c80590df8526a3810c60196a9f636e55 100644
--- a/src/include/commands/view.h
+++ b/src/include/commands/view.h
@@ -19,7 +19,8 @@
 
 extern void validateWithCheckOption(char *value);
 
-extern ObjectAddress DefineView(ViewStmt *stmt, const char *queryString);
+extern ObjectAddress DefineView(ViewStmt *stmt, const char *queryString,
+		   int stmt_location, int stmt_len);
 
 extern void StoreViewQuery(Oid viewOid, Query *viewParse, bool replace);
 
diff --git a/src/include/executor/execdesc.h b/src/include/executor/execdesc.h
index 2c221255dcbf510578c99efa84640cdbea302383..c99ea818158c9dafb9d8b8ac9d1d5044851b54d1 100644
--- a/src/include/executor/execdesc.h
+++ b/src/include/executor/execdesc.h
@@ -34,8 +34,7 @@ typedef struct QueryDesc
 {
 	/* These fields are provided by CreateQueryDesc */
 	CmdType		operation;		/* CMD_SELECT, CMD_UPDATE, etc. */
-	PlannedStmt *plannedstmt;	/* planner's output, or null if utility */
-	Node	   *utilitystmt;	/* utility statement, or null */
+	PlannedStmt *plannedstmt;	/* planner's output (could be utility, too) */
 	const char *sourceText;		/* source text of the query */
 	Snapshot	snapshot;		/* snapshot to use for query */
 	Snapshot	crosscheck_snapshot;	/* crosscheck for RI update/delete */
@@ -61,12 +60,6 @@ extern QueryDesc *CreateQueryDesc(PlannedStmt *plannedstmt,
 				ParamListInfo params,
 				int instrument_options);
 
-extern QueryDesc *CreateUtilityQueryDesc(Node *utilitystmt,
-					   const char *sourceText,
-					   Snapshot snapshot,
-					   DestReceiver *dest,
-					   ParamListInfo params);
-
 extern void FreeQueryDesc(QueryDesc *qdesc);
 
 #endif   /* EXECDESC_H  */
diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h
index a1bb0ac5cb89de61fd4e93b6ad1b63089eb3cf6d..4c4319bcabfb4ff26c4635d513a62155efaed4c8 100644
--- a/src/include/nodes/nodes.h
+++ b/src/include/nodes/nodes.h
@@ -301,6 +301,7 @@ typedef enum NodeTag
 	/*
 	 * TAGS FOR STATEMENT NODES (mostly in parsenodes.h)
 	 */
+	T_RawStmt,
 	T_Query,
 	T_PlannedStmt,
 	T_InsertStmt,
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 7ceaa22c33b2265fce291b7658c683a3fb1ecb5b..edb5cd21521141993a4eb63e37e75aa88ebe950f 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -7,7 +7,9 @@
  * This is a byte (not character) offset in the original source text, to be
  * used for positioning an error cursor when there is an error related to
  * the node.  Access to the original source text is needed to make use of
- * the location.
+ * the location.  At the topmost (statement) level, we also provide a
+ * statement length, likewise measured in bytes, for convenience in
+ * identifying statement boundaries in multi-statement source strings.
  *
  *
  * Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group
@@ -89,9 +91,7 @@ typedef uint32 AclMode;			/* a bitmask of privilege bits */
  *	  for further processing by the rewriter and planner.
  *
  *	  Utility statements (i.e. non-optimizable statements) have the
- *	  utilityStmt field set, and the Query itself is mostly dummy.
- *	  DECLARE CURSOR is a special case: it is represented like a SELECT,
- *	  but the original DeclareCursorStmt is stored in utilityStmt.
+ *	  utilityStmt field set, and the rest of the Query is mostly dummy.
  *
  *	  Planning converts a Query tree into a Plan tree headed by a PlannedStmt
  *	  node --- the Query structure is not used by the executor.
@@ -108,8 +108,7 @@ typedef struct Query
 
 	bool		canSetTag;		/* do I set the command result tag? */
 
-	Node	   *utilityStmt;	/* non-null if this is DECLARE CURSOR or a
-								 * non-optimizable statement */
+	Node	   *utilityStmt;	/* non-null if commandType == CMD_UTILITY */
 
 	int			resultRelation; /* rtable index of target relation for
 								 * INSERT/UPDATE/DELETE; 0 for SELECT */
@@ -162,6 +161,15 @@ typedef struct Query
 										 * are only added during rewrite and
 										 * therefore are not written out as
 										 * part of Query. */
+
+	/*
+	 * The following two fields identify the portion of the source text string
+	 * containing this query.  They are typically only populated in top-level
+	 * Queries, not in sub-queries.  When not set, they might both be zero, or
+	 * both be -1 meaning "unknown".
+	 */
+	int			stmt_location;	/* start location, or -1 if unknown */
+	int			stmt_len;		/* length in bytes; 0 means "rest of string" */
 } Query;
 
 
@@ -1307,6 +1315,30 @@ typedef struct TriggerTransition
 	bool		isTable;
 } TriggerTransition;
 
+/*****************************************************************************
+ *		Raw Grammar Output Statements
+ *****************************************************************************/
+
+/*
+ *		RawStmt --- container for any one statement's raw parse tree
+ *
+ * Parse analysis converts a raw parse tree headed by a RawStmt node into
+ * an analyzed statement headed by a Query node.  For optimizable statements,
+ * the conversion is complex.  For utility statements, the parser usually just
+ * transfers the raw parse tree (sans RawStmt) into the utilityStmt field of
+ * the Query node, and all the useful work happens at execution time.
+ *
+ * stmt_location/stmt_len identify the portion of the source text string
+ * containing this raw statement (useful for multi-statement strings).
+ */
+typedef struct RawStmt
+{
+	NodeTag		type;
+	Node	   *stmt;			/* raw parse tree */
+	int			stmt_location;	/* start location, or -1 if unknown */
+	int			stmt_len;		/* length in bytes; 0 means "rest of string" */
+} RawStmt;
+
 /*****************************************************************************
  *		Optimizable Statements
  *****************************************************************************/
@@ -1474,6 +1506,9 @@ typedef struct SetOperationStmt
  *		statements do need attention from parse analysis, and this is
  *		done by routines in parser/parse_utilcmd.c after ProcessUtility
  *		receives the command for execution.
+ *		DECLARE CURSOR, EXPLAIN, and CREATE TABLE AS are special cases:
+ *		they contain optimizable statements, which get processed normally
+ *		by parser/analyze.c.
  *****************************************************************************/
 
 /*
@@ -1794,7 +1829,7 @@ typedef struct CopyStmt
 	NodeTag		type;
 	RangeVar   *relation;		/* the relation to copy */
 	Node	   *query;			/* the query (SELECT or DML statement with
-								 * RETURNING) to copy */
+								 * RETURNING) to copy, as a raw parse tree */
 	List	   *attlist;		/* List of column names (as Strings), or NIL
 								 * for all columns */
 	bool		is_from;		/* TO or FROM */
@@ -2472,9 +2507,9 @@ typedef struct SecLabelStmt
 /* ----------------------
  *		Declare Cursor Statement
  *
- * Note: the "query" field of DeclareCursorStmt is only used in the raw grammar
- * output.  After parse analysis it's set to null, and the Query points to the
- * DeclareCursorStmt, not vice versa.
+ * The "query" field is initially a raw parse tree, and is converted to a
+ * Query node during parse analysis.  Note that rewriting and planning
+ * of the query are always postponed until execution.
  * ----------------------
  */
 #define CURSOR_OPT_BINARY		0x0001	/* BINARY */
@@ -2493,7 +2528,7 @@ typedef struct DeclareCursorStmt
 	NodeTag		type;
 	char	   *portalname;		/* name of the portal (cursor) */
 	int			options;		/* bitmask of options (see above) */
-	Node	   *query;			/* the raw SELECT query */
+	Node	   *query;			/* the query (see comments above) */
 } DeclareCursorStmt;
 
 /* ----------------------
@@ -2841,7 +2876,7 @@ typedef struct ViewStmt
 	NodeTag		type;
 	RangeVar   *view;			/* the view to be created */
 	List	   *aliases;		/* target column names */
-	Node	   *query;			/* the SELECT query */
+	Node	   *query;			/* the SELECT query (as a raw parse tree) */
 	bool		replace;		/* replace an existing view? */
 	List	   *options;		/* options from WITH clause */
 	ViewCheckOption withCheckOption;	/* WITH CHECK OPTION */
@@ -2950,9 +2985,9 @@ typedef struct VacuumStmt
 /* ----------------------
  *		Explain Statement
  *
- * The "query" field is either a raw parse tree (SelectStmt, InsertStmt, etc)
- * or a Query node if parse analysis has been done.  Note that rewriting and
- * planning of the query are always postponed until execution of EXPLAIN.
+ * The "query" field is initially a raw parse tree, and is converted to a
+ * Query node during parse analysis.  Note that rewriting and planning
+ * of the query are always postponed until execution.
  * ----------------------
  */
 typedef struct ExplainStmt
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 692a62698ed08316bb122a994bbe380b808bc63c..6810f8c0993dff974f91eb9647c773367ac12e7a 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -31,13 +31,18 @@
  *
  * The output of the planner is a Plan tree headed by a PlannedStmt node.
  * PlannedStmt holds the "one time" information needed by the executor.
+ *
+ * For simplicity in APIs, we also wrap utility statements in PlannedStmt
+ * nodes; in such cases, commandType == CMD_UTILITY, the statement itself
+ * is in the utilityStmt field, and the rest of the struct is mostly dummy.
+ * (We do use canSetTag, stmt_location, stmt_len, and possibly queryId.)
  * ----------------
  */
 typedef struct PlannedStmt
 {
 	NodeTag		type;
 
-	CmdType		commandType;	/* select|insert|update|delete */
+	CmdType		commandType;	/* select|insert|update|delete|utility */
 
 	uint32		queryId;		/* query identifier (copied from Query) */
 
@@ -60,8 +65,6 @@ typedef struct PlannedStmt
 	/* rtable indexes of target relations for INSERT/UPDATE/DELETE */
 	List	   *resultRelations;	/* integer list of RT indexes, or NIL */
 
-	Node	   *utilityStmt;	/* non-null if this is DECLARE CURSOR */
-
 	List	   *subplans;		/* Plan trees for SubPlan expressions */
 
 	Bitmapset  *rewindPlanIDs;	/* indices of subplans that require REWIND */
@@ -73,6 +76,12 @@ typedef struct PlannedStmt
 	List	   *invalItems;		/* other dependencies, as PlanInvalItems */
 
 	int			nParamExec;		/* number of PARAM_EXEC Params used */
+
+	Node	   *utilityStmt;	/* non-null if this is utility stmt */
+
+	/* statement location in source string (copied from Query) */
+	int			stmt_location;	/* start location, or -1 if unknown */
+	int			stmt_len;		/* length in bytes; 0 means "rest of string" */
 } PlannedStmt;
 
 /* macro for fetching the Plan associated with a SubPlan node */
diff --git a/src/include/parser/analyze.h b/src/include/parser/analyze.h
index 3bf4edd0bf2685376409a7b25b158289ffa08751..a7e5c55ab4f164f9ea4fe06df72fc621e71ca6a7 100644
--- a/src/include/parser/analyze.h
+++ b/src/include/parser/analyze.h
@@ -22,19 +22,19 @@ typedef void (*post_parse_analyze_hook_type) (ParseState *pstate,
 extern PGDLLIMPORT post_parse_analyze_hook_type post_parse_analyze_hook;
 
 
-extern Query *parse_analyze(Node *parseTree, const char *sourceText,
+extern Query *parse_analyze(RawStmt *parseTree, const char *sourceText,
 			  Oid *paramTypes, int numParams);
-extern Query *parse_analyze_varparams(Node *parseTree, const char *sourceText,
+extern Query *parse_analyze_varparams(RawStmt *parseTree, const char *sourceText,
 						Oid **paramTypes, int *numParams);
 
 extern Query *parse_sub_analyze(Node *parseTree, ParseState *parentParseState,
 				  CommonTableExpr *parentCTE,
 				  bool locked_from_parent);
 
-extern Query *transformTopLevelStmt(ParseState *pstate, Node *parseTree);
+extern Query *transformTopLevelStmt(ParseState *pstate, RawStmt *parseTree);
 extern Query *transformStmt(ParseState *pstate, Node *parseTree);
 
-extern bool analyze_requires_snapshot(Node *parseTree);
+extern bool analyze_requires_snapshot(RawStmt *parseTree);
 
 extern const char *LCS_asString(LockClauseStrength strength);
 extern void CheckSelectLocking(Query *qry, LockClauseStrength strength);
diff --git a/src/include/tcop/tcopprot.h b/src/include/tcop/tcopprot.h
index 54da6b65efb1f79878df5940a2d025d48dc96acd..1958be85b72019e985c2de5b93fca32a907a744b 100644
--- a/src/include/tcop/tcopprot.h
+++ b/src/include/tcop/tcopprot.h
@@ -47,9 +47,10 @@ typedef enum
 extern int	log_statement;
 
 extern List *pg_parse_query(const char *query_string);
-extern List *pg_analyze_and_rewrite(Node *parsetree, const char *query_string,
+extern List *pg_analyze_and_rewrite(RawStmt *parsetree,
+					   const char *query_string,
 					   Oid *paramTypes, int numParams);
-extern List *pg_analyze_and_rewrite_params(Node *parsetree,
+extern List *pg_analyze_and_rewrite_params(RawStmt *parsetree,
 							  const char *query_string,
 							  ParserSetupHook parserSetup,
 							  void *parserSetupArg);
diff --git a/src/include/tcop/utility.h b/src/include/tcop/utility.h
index 42c52a1c757dbf89d1d8cecd8594b321d30566cb..4f8d35390010b3c932288ccae480266bd61e4eb2 100644
--- a/src/include/tcop/utility.h
+++ b/src/include/tcop/utility.h
@@ -24,16 +24,16 @@ typedef enum
 } ProcessUtilityContext;
 
 /* Hook for plugins to get control in ProcessUtility() */
-typedef void (*ProcessUtility_hook_type) (Node *parsetree,
+typedef void (*ProcessUtility_hook_type) (PlannedStmt *pstmt,
 					  const char *queryString, ProcessUtilityContext context,
 													  ParamListInfo params,
 									DestReceiver *dest, char *completionTag);
 extern PGDLLIMPORT ProcessUtility_hook_type ProcessUtility_hook;
 
-extern void ProcessUtility(Node *parsetree, const char *queryString,
+extern void ProcessUtility(PlannedStmt *pstmt, const char *queryString,
 			   ProcessUtilityContext context, ParamListInfo params,
 			   DestReceiver *dest, char *completionTag);
-extern void standard_ProcessUtility(Node *parsetree, const char *queryString,
+extern void standard_ProcessUtility(PlannedStmt *pstmt, const char *queryString,
 						ProcessUtilityContext context, ParamListInfo params,
 						DestReceiver *dest, char *completionTag);
 
@@ -47,6 +47,6 @@ extern const char *CreateCommandTag(Node *parsetree);
 
 extern LogStmtLevel GetCommandLogLevel(Node *parsetree);
 
-extern bool CommandIsReadOnly(Node *parsetree);
+extern bool CommandIsReadOnly(PlannedStmt *pstmt);
 
 #endif   /* UTILITY_H */
diff --git a/src/include/utils/plancache.h b/src/include/utils/plancache.h
index 9ee0bc6e9f11143ab9dc1c2734ce86c307a6e835..84952d56e70d8bb182f0e88b668acbd48049f40b 100644
--- a/src/include/utils/plancache.h
+++ b/src/include/utils/plancache.h
@@ -18,6 +18,9 @@
 #include "access/tupdesc.h"
 #include "nodes/params.h"
 
+/* Forward declaration, to avoid including parsenodes.h here */
+struct RawStmt;
+
 #define CACHEDPLANSOURCE_MAGIC		195726186
 #define CACHEDPLAN_MAGIC			953717834
 
@@ -76,7 +79,7 @@
 typedef struct CachedPlanSource
 {
 	int			magic;			/* should equal CACHEDPLANSOURCE_MAGIC */
-	Node	   *raw_parse_tree; /* output of raw_parser(), or NULL */
+	struct RawStmt *raw_parse_tree;		/* output of raw_parser(), or NULL */
 	const char *query_string;	/* source text of query */
 	const char *commandTag;		/* command tag (a constant!), or NULL */
 	Oid		   *param_types;	/* array of parameter type OIDs, or NULL */
@@ -126,8 +129,7 @@ typedef struct CachedPlanSource
 typedef struct CachedPlan
 {
 	int			magic;			/* should equal CACHEDPLAN_MAGIC */
-	List	   *stmt_list;		/* list of statement nodes (PlannedStmts and
-								 * bare utility statements) */
+	List	   *stmt_list;		/* list of PlannedStmts */
 	bool		is_oneshot;		/* is it a "oneshot" plan? */
 	bool		is_saved;		/* is CachedPlan in a long-lived context? */
 	bool		is_valid;		/* is the stmt_list currently valid? */
@@ -144,10 +146,10 @@ typedef struct CachedPlan
 extern void InitPlanCache(void);
 extern void ResetPlanCache(void);
 
-extern CachedPlanSource *CreateCachedPlan(Node *raw_parse_tree,
+extern CachedPlanSource *CreateCachedPlan(struct RawStmt *raw_parse_tree,
 				 const char *query_string,
 				 const char *commandTag);
-extern CachedPlanSource *CreateOneShotCachedPlan(Node *raw_parse_tree,
+extern CachedPlanSource *CreateOneShotCachedPlan(struct RawStmt *raw_parse_tree,
 						const char *query_string,
 						const char *commandTag);
 extern void CompleteCachedPlan(CachedPlanSource *plansource,
diff --git a/src/include/utils/portal.h b/src/include/utils/portal.h
index 8359416b15ed5c72eb5b7c56736699f1d1d9a5df..dc76acd0a42d6f6b9e405e2024fae4ba1deae523 100644
--- a/src/include/utils/portal.h
+++ b/src/include/utils/portal.h
@@ -133,7 +133,7 @@ typedef struct PortalData
 	/* The query or queries the portal will execute */
 	const char *sourceText;		/* text of query (as of 8.4, never NULL) */
 	const char *commandTag;		/* command tag for original query */
-	List	   *stmts;			/* PlannedStmts and/or utility statements */
+	List	   *stmts;			/* list of PlannedStmts */
 	CachedPlan *cplan;			/* CachedPlan, if stmts are from one */
 
 	ParamListInfo portalParams; /* params to pass to query */
@@ -201,7 +201,6 @@ typedef struct PortalData
  */
 #define PortalGetQueryDesc(portal)	((portal)->queryDesc)
 #define PortalGetHeapMemory(portal) ((portal)->heap)
-#define PortalGetPrimaryStmt(portal) PortalListGetPrimaryStmt((portal)->stmts)
 
 
 /* Prototypes for functions in utils/mmgr/portalmem.c */
@@ -232,7 +231,7 @@ extern void PortalDefineQuery(Portal portal,
 				  const char *commandTag,
 				  List *stmts,
 				  CachedPlan *cplan);
-extern Node *PortalListGetPrimaryStmt(List *stmts);
+extern PlannedStmt *PortalGetPrimaryStmt(Portal portal);
 extern void PortalCreateHoldStore(Portal portal);
 extern void PortalHashTableDeleteAll(void);
 extern bool ThereAreNoReadyPortals(void);
diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
index 192cbcf98323eb9ceaa7bea2b263e8c902006075..bc7b00199efb0832db288260a154179573cb23fe 100644
--- a/src/pl/plpgsql/src/pl_exec.c
+++ b/src/pl/plpgsql/src/pl_exec.c
@@ -6827,12 +6827,11 @@ exec_simple_recheck_plan(PLpgSQL_expr *expr, CachedPlan *cplan)
 	if (list_length(cplan->stmt_list) != 1)
 		return;
 	stmt = (PlannedStmt *) linitial(cplan->stmt_list);
+	Assert(IsA(stmt, PlannedStmt));
 
 	/*
 	 * 2. It must be a RESULT plan --> no scan's required
 	 */
-	if (!IsA(stmt, PlannedStmt))
-		return;
 	if (stmt->commandType != CMD_SELECT)
 		return;
 	plan = stmt->planTree;