From 19408d1d58f391dfbb13d76c3b46e553b0d4ec4d Mon Sep 17 00:00:00 2001
From: Mark Dilger <mark.dilger@enterprisedb.com>
Date: Thu, 6 Feb 2020 11:41:44 -0800
Subject: [PATCH v5] Migrating commandTag from string to enum.
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

The backend was using strings to represent command tags and doing string
comparisons in multiple places.  Fixing that by creating a new
CommandTag enum and using it instead.

Replacing numerous occurrences of char *completionTag with a
QueryCompletionData struct so that the code no longer stores information
about completed queries in a cstring.  Only at the last moment, in
EndCommand(), does this get converted to a string.

EventTriggerCacheItem no longer holds an array of palloc’d tag strings
in sorted order, but rather just a Bitmapset over the CommandTags.
---
 .../pg_stat_statements/pg_stat_statements.c   |  18 +-
 contrib/sepgsql/hooks.c                       |   6 +-
 src/backend/catalog/DataFile.pm               | 400 ++++++++
 src/backend/commands/createas.c               |  14 +-
 src/backend/commands/event_trigger.c          | 160 +--
 src/backend/commands/matview.c                |   2 +-
 src/backend/commands/portalcmds.c             |  16 +-
 src/backend/commands/prepare.c                |   4 +-
 src/backend/executor/execMain.c               |   4 +-
 src/backend/executor/functions.c              |   4 +-
 src/backend/executor/spi.c                    |  22 +-
 src/backend/replication/walsender.c           |  18 +-
 src/backend/tcop/dest.c                       |  38 +-
 src/backend/tcop/postgres.c                   |  24 +-
 src/backend/tcop/pquery.c                     | 111 +--
 src/backend/tcop/utility.c                    | 558 +++++------
 src/backend/utils/.gitignore                  |   3 +
 src/backend/utils/Makefile                    |  20 +-
 src/backend/utils/cache/evtcache.c            |  27 +-
 src/backend/utils/cache/plancache.c           |   4 +-
 src/backend/utils/commandtag.c                | 119 +++
 src/backend/utils/gencommandtag.pl            | 266 +++++
 src/backend/utils/mmgr/portalmem.c            |   6 +-
 src/include/Makefile                          |   4 +-
 src/include/commands/createas.h               |   3 +-
 src/include/commands/event_trigger.h          |   2 +-
 src/include/commands/matview.h                |   2 +-
 src/include/commands/portalcmds.h             |   3 +-
 src/include/commands/prepare.h                |   2 +-
 src/include/nodes/parsenodes.h                |   1 +
 src/include/tcop/dest.h                       |   7 +-
 src/include/tcop/pquery.h                     |   2 +-
 src/include/tcop/utility.h                    |  15 +-
 src/include/utils/.gitignore                  |   2 +
 src/include/utils/commandtag.dat              | 938 ++++++++++++++++++
 src/include/utils/commandtag.h                |  60 ++
 src/include/utils/evtcache.h                  |   3 +-
 src/include/utils/plancache.h                 |   7 +-
 src/include/utils/portal.h                    |   6 +-
 src/pl/plpgsql/src/pl_exec.c                  |  10 +-
 .../test_ddl_deparse/test_ddl_deparse.c       |   2 +-
 src/tools/msvc/Solution.pm                    |  12 +
 42 files changed, 2311 insertions(+), 614 deletions(-)
 create mode 100644 src/backend/catalog/DataFile.pm
 create mode 100644 src/backend/utils/commandtag.c
 create mode 100755 src/backend/utils/gencommandtag.pl
 create mode 100644 src/include/utils/commandtag.dat
 create mode 100644 src/include/utils/commandtag.h

diff --git a/contrib/pg_stat_statements/pg_stat_statements.c b/contrib/pg_stat_statements/pg_stat_statements.c
index e0dbeebde3..1f3e9c1041 100644
--- a/contrib/pg_stat_statements/pg_stat_statements.c
+++ b/contrib/pg_stat_statements/pg_stat_statements.c
@@ -307,7 +307,7 @@ static void pgss_ExecutorEnd(QueryDesc *queryDesc);
 static void pgss_ProcessUtility(PlannedStmt *pstmt, const char *queryString,
 								ProcessUtilityContext context, ParamListInfo params,
 								QueryEnvironment *queryEnv,
-								DestReceiver *dest, char *completionTag);
+								DestReceiver *dest, QueryCompletion *qc);
 static uint64 pgss_hash_string(const char *str, int len);
 static void pgss_store(const char *query, uint64 queryId,
 					   int query_location, int query_len,
@@ -960,7 +960,7 @@ static void
 pgss_ProcessUtility(PlannedStmt *pstmt, const char *queryString,
 					ProcessUtilityContext context,
 					ParamListInfo params, QueryEnvironment *queryEnv,
-					DestReceiver *dest, char *completionTag)
+					DestReceiver *dest, QueryCompletion *qc)
 {
 	Node	   *parsetree = pstmt->utilityStmt;
 
@@ -998,11 +998,11 @@ pgss_ProcessUtility(PlannedStmt *pstmt, const char *queryString,
 			if (prev_ProcessUtility)
 				prev_ProcessUtility(pstmt, queryString,
 									context, params, queryEnv,
-									dest, completionTag);
+									dest, qc);
 			else
 				standard_ProcessUtility(pstmt, queryString,
 										context, params, queryEnv,
-										dest, completionTag);
+										dest, qc);
 		}
 		PG_FINALLY();
 		{
@@ -1013,10 +1013,8 @@ pgss_ProcessUtility(PlannedStmt *pstmt, const char *queryString,
 		INSTR_TIME_SET_CURRENT(duration);
 		INSTR_TIME_SUBTRACT(duration, start);
 
-		/* parse command tag to retrieve the number of affected rows. */
-		if (completionTag &&
-			strncmp(completionTag, "COPY ", 5) == 0)
-			rows = pg_strtouint64(completionTag + 5, NULL, 10);
+		if (qc && qc->commandTag == COMMANDTAG_COPY)
+			rows = qc->nprocessed;
 		else
 			rows = 0;
 
@@ -1060,11 +1058,11 @@ pgss_ProcessUtility(PlannedStmt *pstmt, const char *queryString,
 		if (prev_ProcessUtility)
 			prev_ProcessUtility(pstmt, queryString,
 								context, params, queryEnv,
-								dest, completionTag);
+								dest, qc);
 		else
 			standard_ProcessUtility(pstmt, queryString,
 									context, params, queryEnv,
-									dest, completionTag);
+									dest, qc);
 	}
 }
 
diff --git a/contrib/sepgsql/hooks.c b/contrib/sepgsql/hooks.c
index 997a64c87e..853b5b04ab 100644
--- a/contrib/sepgsql/hooks.c
+++ b/contrib/sepgsql/hooks.c
@@ -317,7 +317,7 @@ sepgsql_utility_command(PlannedStmt *pstmt,
 						ParamListInfo params,
 						QueryEnvironment *queryEnv,
 						DestReceiver *dest,
-						char *completionTag)
+						QueryCompletion *qc)
 {
 	Node	   *parsetree = pstmt->utilityStmt;
 	sepgsql_context_info_t saved_context_info = sepgsql_context_info;
@@ -380,11 +380,11 @@ sepgsql_utility_command(PlannedStmt *pstmt,
 		if (next_ProcessUtility_hook)
 			(*next_ProcessUtility_hook) (pstmt, queryString,
 										 context, params, queryEnv,
-										 dest, completionTag);
+										 dest, qc);
 		else
 			standard_ProcessUtility(pstmt, queryString,
 									context, params, queryEnv,
-									dest, completionTag);
+									dest, qc);
 	}
 	PG_FINALLY();
 	{
diff --git a/src/backend/catalog/DataFile.pm b/src/backend/catalog/DataFile.pm
new file mode 100644
index 0000000000..f7bf7656fc
--- /dev/null
+++ b/src/backend/catalog/DataFile.pm
@@ -0,0 +1,400 @@
+#!/usr/bin/perl -w
+#----------------------------------------------------------------------
+# DataFile.pm
+#    Perl module that safely converts data files into perl data.
+#
+# Portions Copyright (c) 1996-2020, PostgreSQL Global Development Group
+# Portions Copyright (c) 1994, Regents of the University of California
+#
+# src/backend/catalog/DataFile.pm
+#
+#----------------------------------------------------------------------
+
+
+=pod
+
+=head1 NAME
+
+DataFile - module for safely parsing and vivifying perl data files
+
+=head1 SYNOPSIS
+
+  use DataFile;
+
+  my $array_reference = DataFile::ParseData("myfile.dat");
+
+=head1 DESCRIPTION
+
+DataFile contains a parsing function, ParseData, for parsing various
+data files.  By convention, these data files are stored as a single array
+of hashes of key/value pairs.
+
+=pod
+
+=head1 METHODS
+
+=cut
+
+package DataFile;
+
+use strict;
+use warnings;
+use Config;
+
+=pod
+
+=over
+
+=item slurp_file(filename)
+
+Return the full contents of the specified file.
+
+=cut
+
+sub slurp_file
+{
+	my ($filename) = @_;
+	local $/;
+	my $contents;
+	if ($Config{osname} ne 'MSWin32')
+	{
+		open(my $in, '<', $filename)
+		  or die "could not read \"$filename\": $!";
+		$contents = <$in>;
+		close $in;
+	}
+	else
+	{
+		my $fHandle = createFile($filename, "r", "rwd")
+		  or die "could not open \"$filename\": $^E";
+		OsFHandleOpen(my $fh = IO::Handle->new(), $fHandle, 'r')
+		  or die "could not read \"$filename\": $^E\n";
+		$contents = <$fh>;
+		CloseHandle($fHandle)
+		  or die "could not close \"$filename\": $^E\n";
+	}
+	$contents =~ s/\r//g if $Config{osname} eq 'msys';
+	return $contents;
+}
+
+=pod
+
+=item ParseData(filename)
+
+Parse a file which represents a valid perl array reference of hash references,
+vivify the perl array reference, and return it.
+
+This function is paranoid about evaluation or interpolation of embedded perl
+and will die with an error message if the file contains anything that looks
+like it will result in code being executed.
+
+Mistakes in the data file making it an invalid perl object will draw errors
+about the line number in the file where parsing failed and an attempt to
+indicate what parsing problem was encountered there.
+
+=cut
+
+our (%looking, $pos);
+sub update_search($$)
+{
+	my ($lookingfor, $atpos) = @_;
+	if ($atpos > $pos)
+	{
+		%looking = ($lookingfor => 1);
+		$pos = $atpos;
+	}
+	elsif ($atpos == $pos)
+	{
+		$looking{$lookingfor} = 1;
+	}
+}
+
+sub ParseData
+{
+	my ($inputfile) = @_;
+
+	# Vivify the input data, but be paranoid about eval'ing arbitrary code
+	my $data;
+	eval {
+		my $untrusted = slurp_file($inputfile);
+		our $noeval = undef;
+		our $noevalstr = undef;
+		our $noevalpos = -1;
+		%looking = ();
+		$pos = -1;
+
+		# A quotable string is one that does not embed an unescaped end-quote
+		# or end with a backslash which would escape the quote that otherwise
+		# terminates the quoted string.
+		my $singlequotablere = qr/
+								# Verify through look-behind that we're at the
+								# beginning of a single-quoted string
+								(?<=')
+								# Consume the single-quoted string contents
+								# without backtracking or givebacks.
+								(?>
+									# Any single character except single-quote
+									# or backslash
+									[^'\\]
+									|
+									# Any escaped character, including escaped
+									# single-quotes and escaped backslashes
+									\\.
+								)*+
+								# Verify through look-ahead that we're at the
+								# end of a single-quoted string
+								(?=')
+							/msx;
+
+		# Inside double-quoted strings, Perl interpolates substrings which look
+		# like variables with those variables' values.  This seems a bit unsafe
+		# to allow in this package, so we check double-quoted strings for
+		# anything that Perl might interpret this way at 'eval' time.  Such
+		# substrings still match our pattern, but we set a flag alerting us to
+		# not proceed to interpolate the substring.
+		my $doublequotablere = qr/
+								# Verify through look-behind that we're at the
+								# beginning of a double-quoted string
+								(?<=")
+								# Begin capturing.  If we encounter an illegal
+								# sequence, we can save the captured string for
+								# use later during error reporting
+								(
+								# Consume the double-quoted string contents
+								# without backtracking or givebacks.
+								(?>
+									# Any character except double-quote,
+									# backslash, or variable indicators [$%@]
+									[^"\\%@\$]
+									|
+									# Any escaped character
+									\\.
+									|
+									# A variable indicator, but we set the flag
+									# that we must not eval this string
+									[%@\$](?{ local $noeval = 1; })
+								)*+
+								# verify through look-ahead that we're at the
+								# end of the double-quoted string
+								(?=")
+								# End capture
+								)
+								# If we encountered a disallowed character
+								# during the capture, record the entire captured
+								# string for later
+								(?{ $noevalstr = $^N, $noevalpos = pos()
+										if ($noeval && !defined $noevalstr);
+								  })
+							/msx;
+
+		my $quotedstringre = qr/
+							(?:
+								'
+								(?{ update_search("end of single quoted string", pos()) })
+								$singlequotablere
+								'
+								|
+								"
+								(?{ update_search("end of double quoted string", pos()) })
+								$doublequotablere
+								"
+							)
+							/msx;
+
+		# A perl comment cannot occur inside a quoted string, but otherwise it can
+		# occur pretty much anywhere.  So long as we're not inside a quotation, we
+		# can match a # to the end of the line
+		my $commentre = qr/
+							(?:
+								(?{ update_search("comment", pos()) })
+								# Beginning of a comment
+								\#
+								# Anything other than end-of-line, no backtracking,
+								# and no give-backs
+								(?>[^\r\n])*+
+								# end-of-line
+								\r?\n
+							)
+						/msx;
+
+		# Comments and/or whitespace can occur just about anywhere.  It is convenient
+		# to have a single regex that matches them so we don't have to think about
+		# comments much from here onward.
+		my $spacere = qr/
+						(?:
+							# any number of comment lines, including none, including
+							# leading whitespace before the comment begins
+							(?:
+								(?{ update_search("whitespace", pos()) })
+								(?>\s)*+	# whitespace without backtracking
+								(?{ update_search("comment", pos()) })
+								$commentre
+							)*+
+							# whitespace following the zero or more comment lines,
+							# without backtracking
+							(?{ update_search("whitespace", pos()) })
+							(?>\s)*+
+						)
+						/msx;
+
+		# Keys must be either quoted strings, numbers, or barewords if followed
+		# by an arrow operator.  The arrow operator is not part of the key, however,
+		# so we check that with a look-ahead assertion
+		my $keyre = qr/
+						(?:
+							$quotedstringre
+							|
+							\d+
+							|
+							# bareword only allowed before arrow op, but there cannot
+							# be comments inbetween, only whitespace
+							\w+(?=\s*=>)
+						)
+					/msx;
+		# Values must be either numbers or quoted strings
+		my $valuere = qr/
+							(?:
+								$quotedstringre
+								|
+								\d+
+							)
+						/msx;
+
+		# Key/Value pairs can use arrow or comma separators.  We hid the complexity
+		# of arrow operator quoting in the keyre, above
+		my $keyvaluere = qr/
+							(?{ update_search("key value pair", pos()) })
+							(?:
+								$keyre
+								$spacere
+								(?:
+									=>
+									|
+									,
+								)
+								$spacere
+								$valuere
+							)
+							/msx;
+
+		# Key/Value pair lists
+		my $keyvaluelistre = qr/
+									# Optional key-value pair
+									(?:
+										$keyvaluere
+										$spacere
+
+										# Optionally followed by more
+										(?:
+											,		# Required
+											$spacere
+											$keyvaluere
+										)*
+										# Optionally ending with a comma
+										(?:
+											$spacere
+											,
+										)?
+									)?
+								/msx;
+
+		# Hash reference comprised of key/value pairs
+		my $hashrefre = qr/
+								(?:
+									(?{ update_search("beginning of hash reference", pos()) })
+									\{		# Open hash
+									$spacere
+									$keyvaluelistre
+									$spacere
+									(?{ update_search("ending of hash reference", pos()) })
+									\}		# Close hash
+								)
+							/msx;
+
+		# List of hash references
+		my $hashreflist = qr/
+								# Optional hash ref
+								(?:
+									$hashrefre
+									$spacere
+
+									# Optionally followed by more
+									(?:
+										,		# Required
+										$spacere
+										$hashrefre
+									)*+
+									# Optionally ending with a comma
+									(?:
+										$spacere
+										,
+									)?+
+								)?+
+							/msx;
+
+		# Reference to list of hash references
+		my $aryofhashrefre = qr/
+								(?:
+									(?{ update_search("beginning of array reference", pos()) })
+									\[		# Open array
+									$spacere
+									$hashreflist
+									$spacere
+									(?{ update_search("ending of array reference", pos()) })
+									\]		# Close array
+								)
+								/msx;
+
+
+		# After removing all comments, remaining structure must strictly
+		# match an array reference of hash references of key/value pairs
+		# with no code that perl's eval() could be tricked into running.
+		#
+		# This parser is stricter than necessary, but it should allow all
+		# data to be specified that we need it to allow, so instead of
+		# making this parser more complicated (or permissive), see if you
+		# can make the data file follow the expected format.
+		#
+		if ($untrusted =~ m/^
+								$spacere
+								$aryofhashrefre
+								$spacere
+							$/msx)
+		{
+			# If we encountered anything that might cause eval() to perform
+			# variable interpolation, complain and do not eval().  This may
+			# be overly paranoid....
+			if (defined $noevalstr)
+			{
+				my $lineno = scalar(split/\n/, substr($untrusted, 0, $noevalpos));
+				die "\nPossible attempt at variable interpolation in double " .
+					"quoted string in $inputfile, line $lineno: \"$noevalstr\"";
+			}
+
+			# We're treating the input file as a piece of Perl, so we
+			# need to use string eval here. Tell perlcritic we know what
+			# we're doing.
+			#
+			$data = eval $untrusted;	## no critic (ProhibitStringyEval)
+			die $@ if $@;
+		}
+		else
+		{
+			my $lineno = scalar(split/\n/, substr($untrusted, 0, $pos));
+			my $looking = join(" or ", keys %looking);
+			die("\n$inputfile does not match our expected format;\n" .
+				"At line $lineno, looking for $looking\n");
+		}
+	};
+	die $@ if $@;
+
+	return $data;
+}
+
+=pod
+
+=back
+
+=cut
+
+1;
diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c
index cc02cf824e..3bcd7a95af 100644
--- a/src/backend/commands/createas.c
+++ b/src/backend/commands/createas.c
@@ -10,7 +10,7 @@
  *
  * Formerly, CTAS was implemented as a variant of SELECT, which led
  * to assorted legacy behaviors that we still try to preserve, notably that
- * we must return a tuples-processed count in the completionTag.  (We no
+ * we must return a tuples-processed count in the QueryCompletion.  (We no
  * longer do that for CTAS ... WITH NO DATA, however.)
  *
  * Portions Copyright (c) 1996-2020, PostgreSQL Global Development Group
@@ -225,7 +225,7 @@ create_ctas_nodata(List *tlist, IntoClause *into)
 ObjectAddress
 ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt,
 				  ParamListInfo params, QueryEnvironment *queryEnv,
-				  char *completionTag)
+				  QueryCompletion *qc)
 {
 	Query	   *query = castNode(Query, stmt->query);
 	IntoClause *into = stmt->into;
@@ -270,7 +270,7 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt,
 		ExecuteStmt *estmt = castNode(ExecuteStmt, query->utilityStmt);
 
 		Assert(!is_matview);	/* excluded by syntax */
-		ExecuteQuery(pstate, estmt, into, params, dest, completionTag);
+		ExecuteQuery(pstate, estmt, into, params, dest, qc);
 
 		/* get object address that intorel_startup saved for us */
 		address = ((DR_intorel *) dest)->reladdr;
@@ -352,11 +352,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt,
 		/* run the plan to completion */
 		ExecutorRun(queryDesc, ForwardScanDirection, 0L, true);
 
-		/* save the rowcount if we're given a completionTag to fill */
-		if (completionTag)
-			snprintf(completionTag, COMPLETION_TAG_BUFSIZE,
-					 "SELECT " UINT64_FORMAT,
-					 queryDesc->estate->es_processed);
+		/* save the rowcount if we're given a qc to fill */
+		if (qc)
+			SetQueryCompletion(qc, COMMANDTAG_SELECT, queryDesc->estate->es_processed);
 
 		/* get object address that intorel_startup saved for us */
 		address = ((DR_intorel *) dest)->reladdr;
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index 71911d4067..7c8415475f 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -78,59 +78,6 @@ typedef struct
 	bool		supported;
 } event_trigger_support_data;
 
-typedef enum
-{
-	EVENT_TRIGGER_COMMAND_TAG_OK,
-	EVENT_TRIGGER_COMMAND_TAG_NOT_SUPPORTED,
-	EVENT_TRIGGER_COMMAND_TAG_NOT_RECOGNIZED
-} event_trigger_command_tag_check_result;
-
-/* XXX merge this with ObjectTypeMap? */
-static const event_trigger_support_data event_trigger_support[] = {
-	{"ACCESS METHOD", true},
-	{"AGGREGATE", true},
-	{"CAST", true},
-	{"CONSTRAINT", true},
-	{"COLLATION", true},
-	{"CONVERSION", true},
-	{"DATABASE", false},
-	{"DOMAIN", true},
-	{"EXTENSION", true},
-	{"EVENT TRIGGER", false},
-	{"FOREIGN DATA WRAPPER", true},
-	{"FOREIGN TABLE", true},
-	{"FUNCTION", true},
-	{"INDEX", true},
-	{"LANGUAGE", true},
-	{"MATERIALIZED VIEW", true},
-	{"OPERATOR", true},
-	{"OPERATOR CLASS", true},
-	{"OPERATOR FAMILY", true},
-	{"POLICY", true},
-	{"PROCEDURE", true},
-	{"PUBLICATION", true},
-	{"ROLE", false},
-	{"ROUTINE", true},
-	{"RULE", true},
-	{"SCHEMA", true},
-	{"SEQUENCE", true},
-	{"SERVER", true},
-	{"STATISTICS", true},
-	{"SUBSCRIPTION", true},
-	{"TABLE", true},
-	{"TABLESPACE", false},
-	{"TRANSFORM", true},
-	{"TRIGGER", true},
-	{"TEXT SEARCH CONFIGURATION", true},
-	{"TEXT SEARCH DICTIONARY", true},
-	{"TEXT SEARCH PARSER", true},
-	{"TEXT SEARCH TEMPLATE", true},
-	{"TYPE", true},
-	{"USER MAPPING", true},
-	{"VIEW", true},
-	{NULL, false}
-};
-
 /* Support for dropped objects */
 typedef struct SQLDropObject
 {
@@ -150,8 +97,6 @@ typedef struct SQLDropObject
 static void AlterEventTriggerOwner_internal(Relation rel,
 											HeapTuple tup,
 											Oid newOwnerId);
-static event_trigger_command_tag_check_result check_ddl_tag(const char *tag);
-static event_trigger_command_tag_check_result check_table_rewrite_ddl_tag(const char *tag);
 static void error_duplicate_filter_variable(const char *defname);
 static Datum filter_list_to_array(List *filterlist);
 static Oid	insert_event_trigger_tuple(const char *trigname, const char *eventname,
@@ -259,71 +204,23 @@ validate_ddl_tags(const char *filtervar, List *taglist)
 
 	foreach(lc, taglist)
 	{
-		const char *tag = strVal(lfirst(lc));
-		event_trigger_command_tag_check_result result;
+		const char *tagstr = strVal(lfirst(lc));
+		CommandTag	commandTag = GetCommandTagEnum(tagstr);
 
-		result = check_ddl_tag(tag);
-		if (result == EVENT_TRIGGER_COMMAND_TAG_NOT_RECOGNIZED)
+		if (commandTag == COMMANDTAG_UNKNOWN)
 			ereport(ERROR,
 					(errcode(ERRCODE_SYNTAX_ERROR),
 					 errmsg("filter value \"%s\" not recognized for filter variable \"%s\"",
-							tag, filtervar)));
-		if (result == EVENT_TRIGGER_COMMAND_TAG_NOT_SUPPORTED)
+							tagstr, filtervar)));
+		if ( ! command_tag_event_trigger_ok(commandTag))
 			ereport(ERROR,
 					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 			/* translator: %s represents an SQL statement name */
 					 errmsg("event triggers are not supported for %s",
-							tag)));
+							tagstr)));
 	}
 }
 
-static event_trigger_command_tag_check_result
-check_ddl_tag(const char *tag)
-{
-	const char *obtypename;
-	const event_trigger_support_data *etsd;
-
-	/*
-	 * Handle some idiosyncratic special cases.
-	 */
-	if (pg_strcasecmp(tag, "CREATE TABLE AS") == 0 ||
-		pg_strcasecmp(tag, "SELECT INTO") == 0 ||
-		pg_strcasecmp(tag, "REFRESH MATERIALIZED VIEW") == 0 ||
-		pg_strcasecmp(tag, "ALTER DEFAULT PRIVILEGES") == 0 ||
-		pg_strcasecmp(tag, "ALTER LARGE OBJECT") == 0 ||
-		pg_strcasecmp(tag, "COMMENT") == 0 ||
-		pg_strcasecmp(tag, "GRANT") == 0 ||
-		pg_strcasecmp(tag, "REVOKE") == 0 ||
-		pg_strcasecmp(tag, "DROP OWNED") == 0 ||
-		pg_strcasecmp(tag, "IMPORT FOREIGN SCHEMA") == 0 ||
-		pg_strcasecmp(tag, "SECURITY LABEL") == 0)
-		return EVENT_TRIGGER_COMMAND_TAG_OK;
-
-	/*
-	 * Otherwise, command should be CREATE, ALTER, or DROP.
-	 */
-	if (pg_strncasecmp(tag, "CREATE ", 7) == 0)
-		obtypename = tag + 7;
-	else if (pg_strncasecmp(tag, "ALTER ", 6) == 0)
-		obtypename = tag + 6;
-	else if (pg_strncasecmp(tag, "DROP ", 5) == 0)
-		obtypename = tag + 5;
-	else
-		return EVENT_TRIGGER_COMMAND_TAG_NOT_RECOGNIZED;
-
-	/*
-	 * ...and the object type should be something recognizable.
-	 */
-	for (etsd = event_trigger_support; etsd->obtypename != NULL; etsd++)
-		if (pg_strcasecmp(etsd->obtypename, obtypename) == 0)
-			break;
-	if (etsd->obtypename == NULL)
-		return EVENT_TRIGGER_COMMAND_TAG_NOT_RECOGNIZED;
-	if (!etsd->supported)
-		return EVENT_TRIGGER_COMMAND_TAG_NOT_SUPPORTED;
-	return EVENT_TRIGGER_COMMAND_TAG_OK;
-}
-
 /*
  * Validate DDL command tags for event table_rewrite.
  */
@@ -334,29 +231,18 @@ validate_table_rewrite_tags(const char *filtervar, List *taglist)
 
 	foreach(lc, taglist)
 	{
-		const char *tag = strVal(lfirst(lc));
-		event_trigger_command_tag_check_result result;
+		const char *tagstr = strVal(lfirst(lc));
+		CommandTag	commandTag = GetCommandTagEnum(tagstr);
 
-		result = check_table_rewrite_ddl_tag(tag);
-		if (result == EVENT_TRIGGER_COMMAND_TAG_NOT_SUPPORTED)
+		if (! command_tag_table_rewrite_ok(commandTag))
 			ereport(ERROR,
 					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 			/* translator: %s represents an SQL statement name */
 					 errmsg("event triggers are not supported for %s",
-							tag)));
+							tagstr)));
 	}
 }
 
-static event_trigger_command_tag_check_result
-check_table_rewrite_ddl_tag(const char *tag)
-{
-	if (pg_strcasecmp(tag, "ALTER TABLE") == 0 ||
-		pg_strcasecmp(tag, "ALTER TYPE") == 0)
-		return EVENT_TRIGGER_COMMAND_TAG_OK;
-
-	return EVENT_TRIGGER_COMMAND_TAG_NOT_SUPPORTED;
-}
-
 /*
  * Complain about a duplicate filter variable.
  */
@@ -663,7 +549,7 @@ get_event_trigger_oid(const char *trigname, bool missing_ok)
  * tags matching.
  */
 static bool
-filter_event_trigger(const char **tag, EventTriggerCacheItem *item)
+filter_event_trigger(CommandTag tag, EventTriggerCacheItem *item)
 {
 	/*
 	 * Filter by session replication role, knowing that we never see disabled
@@ -681,9 +567,7 @@ filter_event_trigger(const char **tag, EventTriggerCacheItem *item)
 	}
 
 	/* Filter by tags, if any were specified. */
-	if (item->ntags != 0 && bsearch(tag, item->tag,
-									item->ntags, sizeof(char *),
-									pg_qsort_strcmp) == NULL)
+	if (!bms_is_empty(item->tagset) && !bms_is_member(tag, item->tagset))
 		return false;
 
 	/* if we reach that point, we're not filtering out this item */
@@ -700,7 +584,7 @@ EventTriggerCommonSetup(Node *parsetree,
 						EventTriggerEvent event, const char *eventstr,
 						EventTriggerData *trigdata)
 {
-	const char *tag;
+	CommandTag	tag;
 	List	   *cachelist;
 	ListCell   *lc;
 	List	   *runlist = NIL;
@@ -716,25 +600,25 @@ EventTriggerCommonSetup(Node *parsetree,
 	 *
 	 * If this cross-check fails for you, you probably need to either adjust
 	 * standard_ProcessUtility() not to invoke event triggers for the command
-	 * type in question, or you need to adjust check_ddl_tag to accept the
+	 * type in question, or you need to adjust event_trigger_ok to accept the
 	 * relevant command tag.
 	 */
 #ifdef USE_ASSERT_CHECKING
 	{
-		const char *dbgtag;
+		CommandTag	dbgtag;
 
 		dbgtag = CreateCommandTag(parsetree);
 		if (event == EVT_DDLCommandStart ||
 			event == EVT_DDLCommandEnd ||
 			event == EVT_SQLDrop)
 		{
-			if (check_ddl_tag(dbgtag) != EVENT_TRIGGER_COMMAND_TAG_OK)
-				elog(ERROR, "unexpected command tag \"%s\"", dbgtag);
+			if (! command_tag_event_trigger_ok(dbgtag))
+				elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(dbgtag));
 		}
 		else if (event == EVT_TableRewrite)
 		{
-			if (check_table_rewrite_ddl_tag(dbgtag) != EVENT_TRIGGER_COMMAND_TAG_OK)
-				elog(ERROR, "unexpected command tag \"%s\"", dbgtag);
+			if (! command_tag_table_rewrite_ok(dbgtag))
+				elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(dbgtag));
 		}
 	}
 #endif
@@ -758,7 +642,7 @@ EventTriggerCommonSetup(Node *parsetree,
 	{
 		EventTriggerCacheItem *item = lfirst(lc);
 
-		if (filter_event_trigger(&tag, item))
+		if (filter_event_trigger(tag, item))
 		{
 			/* We must plan to fire this trigger. */
 			runlist = lappend_oid(runlist, item->fnoid);
@@ -2136,7 +2020,7 @@ pg_event_trigger_ddl_commands(PG_FUNCTION_ARGS)
 					/* objsubid */
 					values[i++] = Int32GetDatum(addr.objectSubId);
 					/* command tag */
-					values[i++] = CStringGetTextDatum(CreateCommandTag(cmd->parsetree));
+					values[i++] = CStringGetTextDatum(CreateCommandName(cmd->parsetree));
 					/* object_type */
 					values[i++] = CStringGetTextDatum(type);
 					/* schema */
@@ -2161,7 +2045,7 @@ pg_event_trigger_ddl_commands(PG_FUNCTION_ARGS)
 				/* objsubid */
 				nulls[i++] = true;
 				/* command tag */
-				values[i++] = CStringGetTextDatum(CreateCommandTag(cmd->parsetree));
+				values[i++] = CStringGetTextDatum(CreateCommandName(cmd->parsetree));
 				/* object_type */
 				values[i++] = CStringGetTextDatum(stringify_adefprivs_objtype(cmd->d.defprivs.objtype));
 				/* schema */
diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index 1ee37c1aeb..c3954f3e24 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -136,7 +136,7 @@ SetMatViewPopulatedState(Relation relation, bool newstate)
  */
 ObjectAddress
 ExecRefreshMatView(RefreshMatViewStmt *stmt, const char *queryString,
-				   ParamListInfo params, char *completionTag)
+				   ParamListInfo params, QueryCompletion *qc)
 {
 	Oid			matviewOid;
 	Relation	matviewRel;
diff --git a/src/backend/commands/portalcmds.c b/src/backend/commands/portalcmds.c
index 7e5c805a1e..744d79a2e2 100644
--- a/src/backend/commands/portalcmds.c
+++ b/src/backend/commands/portalcmds.c
@@ -106,7 +106,8 @@ PerformCursorOpen(ParseState *pstate, DeclareCursorStmt *cstmt, ParamListInfo pa
 	PortalDefineQuery(portal,
 					  NULL,
 					  queryString,
-					  "SELECT", /* cursor's query is always a SELECT */
+					  COMMANDTAG_SELECT,	/* cursor's query is always a
+											 * SELECT */
 					  list_make1(plan),
 					  NULL);
 
@@ -160,15 +161,14 @@ PerformCursorOpen(ParseState *pstate, DeclareCursorStmt *cstmt, ParamListInfo pa
  *
  *	stmt: parsetree node for command
  *	dest: where to send results
- *	completionTag: points to a buffer of size COMPLETION_TAG_BUFSIZE
- *		in which to store a command completion status string.
+ *	qc: where to store a command completion status data.
  *
- * completionTag may be NULL if caller doesn't want a status string.
+ * qc may be NULL if caller doesn't want status data.
  */
 void
 PerformPortalFetch(FetchStmt *stmt,
 				   DestReceiver *dest,
-				   char *completionTag)
+				   QueryCompletion *qc)
 {
 	Portal		portal;
 	uint64		nprocessed;
@@ -203,10 +203,8 @@ PerformPortalFetch(FetchStmt *stmt,
 								dest);
 
 	/* Return command status if wanted */
-	if (completionTag)
-		snprintf(completionTag, COMPLETION_TAG_BUFSIZE, "%s " UINT64_FORMAT,
-				 stmt->ismove ? "MOVE" : "FETCH",
-				 nprocessed);
+	if (qc)
+		SetQueryCompletion(qc, stmt->ismove ? COMMANDTAG_MOVE : COMMANDTAG_FETCH, nprocessed);
 }
 
 /*
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index c4e4b6eaec..f917fc9c7a 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -187,7 +187,7 @@ void
 ExecuteQuery(ParseState *pstate,
 			 ExecuteStmt *stmt, IntoClause *intoClause,
 			 ParamListInfo params,
-			 DestReceiver *dest, char *completionTag)
+			 DestReceiver *dest, QueryCompletion *qc)
 {
 	PreparedStatement *entry;
 	CachedPlan *cplan;
@@ -288,7 +288,7 @@ ExecuteQuery(ParseState *pstate,
 	 */
 	PortalStart(portal, paramLI, eflags, GetActiveSnapshot());
 
-	(void) PortalRun(portal, count, false, true, dest, dest, completionTag);
+	(void) PortalRun(portal, count, false, true, dest, dest, qc);
 
 	PortalDrop(portal, false);
 
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index ee5c3a60ff..751b36b6bf 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -787,11 +787,11 @@ ExecCheckXactReadOnly(PlannedStmt *plannedstmt)
 		if (isTempNamespace(get_rel_namespace(rte->relid)))
 			continue;
 
-		PreventCommandIfReadOnly(CreateCommandTag((Node *) plannedstmt));
+		PreventCommandTagIfReadOnly(CreateCommandTag((Node *) plannedstmt));
 	}
 
 	if (plannedstmt->commandType != CMD_SELECT || plannedstmt->hasModifyingCTE)
-		PreventCommandIfParallelMode(CreateCommandTag((Node *) plannedstmt));
+		PreventCommandTagIfParallelMode(CreateCommandTag((Node *) plannedstmt));
 }
 
 
diff --git a/src/backend/executor/functions.c b/src/backend/executor/functions.c
index 5cff6c4321..9b45a8a9a0 100644
--- a/src/backend/executor/functions.c
+++ b/src/backend/executor/functions.c
@@ -530,7 +530,7 @@ init_execution_state(List *queryTree_list,
 							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 					/* translator: %s is a SQL statement name */
 							 errmsg("%s is not allowed in a SQL function",
-									CreateCommandTag(stmt->utilityStmt))));
+									CreateCommandName(stmt->utilityStmt))));
 			}
 
 			if (fcache->readonly_func && !CommandIsReadOnly(stmt))
@@ -538,7 +538,7 @@ init_execution_state(List *queryTree_list,
 						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				/* translator: %s is a SQL statement name */
 						 errmsg("%s is not allowed in a non-volatile function",
-								CreateCommandTag((Node *) stmt))));
+								CreateCommandName((Node *) stmt))));
 
 			/* OK, build the execution_state for this query */
 			newes = (execution_state *) palloc(sizeof(execution_state));
diff --git a/src/backend/executor/spi.c b/src/backend/executor/spi.c
index c46764bf42..d0a73778ed 100644
--- a/src/backend/executor/spi.c
+++ b/src/backend/executor/spi.c
@@ -1338,7 +1338,7 @@ SPI_cursor_open_internal(const char *name, SPIPlanPtr plan,
 				(errcode(ERRCODE_INVALID_CURSOR_DEFINITION),
 		/* translator: %s is name of a SQL command, eg INSERT */
 				 errmsg("cannot open %s query as cursor",
-						plansource->commandTag)));
+						GetCommandTagName(plansource->commandTag))));
 	}
 
 	Assert(list_length(plan->plancache_list) == 1);
@@ -1469,7 +1469,7 @@ 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((Node *) pstmt))));
+								CreateCommandName((Node *) pstmt))));
 		}
 	}
 
@@ -2255,7 +2255,7 @@ _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((Node *) stmt))));
+								CreateCommandName((Node *) stmt))));
 
 			/*
 			 * If not read-only mode, advance the command counter before each
@@ -2291,9 +2291,11 @@ _SPI_execute_plan(SPIPlanPtr plan, ParamListInfo paramLI,
 			}
 			else
 			{
-				char		completionTag[COMPLETION_TAG_BUFSIZE];
+				QueryCompletion	qc;
 				ProcessUtilityContext context;
 
+				InitializeQueryCompletion(&qc);
+
 				/*
 				 * If the SPI context is atomic, or we are asked to manage
 				 * snapshots, then we are in an atomic execution context.
@@ -2312,7 +2314,7 @@ _SPI_execute_plan(SPIPlanPtr plan, ParamListInfo paramLI,
 							   paramLI,
 							   _SPI_current->queryEnv,
 							   dest,
-							   completionTag);
+							   &qc);
 
 				/* Update "processed" if stmt returned tuples */
 				if (_SPI_current->tuptable)
@@ -2328,9 +2330,8 @@ _SPI_execute_plan(SPIPlanPtr plan, ParamListInfo paramLI,
 				{
 					CreateTableAsStmt *ctastmt = (CreateTableAsStmt *) stmt->utilityStmt;
 
-					if (strncmp(completionTag, "SELECT ", 7) == 0)
-						_SPI_current->processed =
-							pg_strtouint64(completionTag + 7, NULL, 10);
+					if (qc.commandTag == COMMANDTAG_SELECT)
+						_SPI_current->processed = qc.nprocessed;
 					else
 					{
 						/*
@@ -2351,9 +2352,8 @@ _SPI_execute_plan(SPIPlanPtr plan, ParamListInfo paramLI,
 				}
 				else if (IsA(stmt->utilityStmt, CopyStmt))
 				{
-					Assert(strncmp(completionTag, "COPY ", 5) == 0);
-					_SPI_current->processed = pg_strtouint64(completionTag + 5,
-															 NULL, 10);
+					Assert(qc.commandTag == COMMANDTAG_COPY);
+					_SPI_current->processed = qc.nprocessed;
 				}
 			}
 
diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c
index abb533b9d0..dc6c16101b 100644
--- a/src/backend/replication/walsender.c
+++ b/src/backend/replication/walsender.c
@@ -1074,8 +1074,11 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 static void
 DropReplicationSlot(DropReplicationSlotCmd *cmd)
 {
+	QueryCompletion	qc;
+
 	ReplicationSlotDrop(cmd->slotname, !cmd->wait);
-	EndCommand("DROP_REPLICATION_SLOT", DestRemote);
+	SetQueryCompletion(&qc, COMMANDTAG_DROP_REPLICATION_SLOT, 0);
+	EndCommand(&qc, DestRemote, false);
 }
 
 /*
@@ -1086,6 +1089,7 @@ static void
 StartLogicalReplication(StartReplicationCmd *cmd)
 {
 	StringInfoData buf;
+	QueryCompletion qc;
 
 	/* make sure that our requirements are still fulfilled */
 	CheckLogicalDecodingRequirements();
@@ -1160,7 +1164,8 @@ StartLogicalReplication(StartReplicationCmd *cmd)
 	WalSndSetState(WALSNDSTATE_STARTUP);
 
 	/* Get out of COPY mode (CommandComplete). */
-	EndCommand("COPY 0", DestRemote);
+	SetQueryCompletion(&qc, COMMANDTAG_COPY, 0);
+	EndCommand(&qc, DestRemote, false);
 }
 
 /*
@@ -1464,6 +1469,7 @@ exec_replication_command(const char *cmd_string)
 	Node	   *cmd_node;
 	MemoryContext cmd_context;
 	MemoryContext old_context;
+	QueryCompletion	qc;
 
 	/*
 	 * If WAL sender has been told that shutdown is getting close, switch its
@@ -1614,7 +1620,8 @@ exec_replication_command(const char *cmd_string)
 	MemoryContextDelete(cmd_context);
 
 	/* Send CommandComplete message */
-	EndCommand("SELECT", DestRemote);
+	SetQueryCompletion(&qc, COMMANDTAG_SELECT, 0);
+	EndCommand(&qc, DestRemote, true);
 
 	/* Report to pgstat that this process is now idle */
 	pgstat_report_activity(STATE_IDLE, NULL);
@@ -2867,8 +2874,11 @@ WalSndDone(WalSndSendDataCallback send_data)
 	if (WalSndCaughtUp && sentPtr == replicatedPtr &&
 		!pq_is_send_pending())
 	{
+		QueryCompletion	qc;
+
 		/* Inform the standby that XLOG streaming is done */
-		EndCommand("COPY 0", DestRemote);
+		SetQueryCompletion(&qc, COMMANDTAG_COPY, 0);
+		EndCommand(&qc, DestRemote, false);
 		pq_flush();
 
 		proc_exit(0);
diff --git a/src/backend/tcop/dest.c b/src/backend/tcop/dest.c
index 09c1dcbb53..5e6ab93f49 100644
--- a/src/backend/tcop/dest.c
+++ b/src/backend/tcop/dest.c
@@ -100,7 +100,7 @@ DestReceiver *None_Receiver = (DestReceiver *) &donothingDR;
  * ----------------
  */
 void
-BeginCommand(const char *commandTag, CommandDest dest)
+BeginCommand(CommandTag commandTag, CommandDest dest)
 {
 	/* Nothing to do at present */
 }
@@ -163,8 +163,12 @@ CreateDestReceiver(CommandDest dest)
  * ----------------
  */
 void
-EndCommand(const char *commandTag, CommandDest dest)
+EndCommand(const QueryCompletion *qc, CommandDest dest, bool force_undecorated_output)
 {
+	char		completionTag[COMPLETION_TAG_BUFSIZE];
+	CommandTag	tag;
+	const char *tagname;
+
 	switch (dest)
 	{
 		case DestRemote:
@@ -172,11 +176,33 @@ EndCommand(const char *commandTag, CommandDest dest)
 		case DestRemoteSimple:
 
 			/*
-			 * We assume the commandTag is plain ASCII and therefore requires
-			 * no encoding conversion.
+			 * We assume the tagname is plain ASCII and therefore requires no
+			 * encoding conversion.
+			 *
+			 * We no longer display LastOid, but to preserve the wire protocol,
+			 * we write InvalidOid where the LastOid used to be written.
+			 *
+			 * All cases where LastOid was written also write nprocessed count,
+			 * so just Assert that rather than having an extra test.
 			 */
-			pq_putmessage('C', commandTag, strlen(commandTag) + 1);
-			break;
+			tag = qc->commandTag;
+			tagname = GetCommandTagName(tag);
+
+			if (command_tag_display_last_oid(tag) && !force_undecorated_output)
+			{
+				/* We assume InvalidOid is zero in the snprintf format string */
+				Assert(InvalidOid == 0);
+				/* We assume rowcount will always be wanted when last_oid is wanted */
+				Assert(command_tag_display_rowcount(tag));
+				snprintf(completionTag, COMPLETION_TAG_BUFSIZE,
+						 "%s 0 " UINT64_FORMAT, tagname, qc->nprocessed);
+			}
+			else if (command_tag_display_rowcount(tag) && !force_undecorated_output)
+				snprintf(completionTag, COMPLETION_TAG_BUFSIZE,
+						 "%s " UINT64_FORMAT, tagname, qc->nprocessed);
+			else
+				snprintf(completionTag, COMPLETION_TAG_BUFSIZE, "%s", tagname);
+			pq_putmessage('C', completionTag, strlen(completionTag) + 1);
 
 		case DestNone:
 		case DestDebug:
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 0a6f80963b..62c181b0a5 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -1064,8 +1064,8 @@ exec_simple_query(const char *query_string)
 	{
 		RawStmt    *parsetree = lfirst_node(RawStmt, parsetree_item);
 		bool		snapshot_set = false;
-		const char *commandTag;
-		char		completionTag[COMPLETION_TAG_BUFSIZE];
+		CommandTag	commandTag;
+		QueryCompletion	qc;
 		MemoryContext per_parsetree_context = NULL;
 		List	   *querytree_list,
 				   *plantree_list;
@@ -1081,7 +1081,7 @@ exec_simple_query(const char *query_string)
 		 */
 		commandTag = CreateCommandTag(parsetree->stmt);
 
-		set_ps_display(commandTag, false);
+		set_ps_display(GetCommandTagName(commandTag), false);
 
 		BeginCommand(commandTag, dest);
 
@@ -1230,7 +1230,7 @@ exec_simple_query(const char *query_string)
 						 true,
 						 receiver,
 						 receiver,
-						 completionTag);
+						 &qc);
 
 		receiver->rDestroy(receiver);
 
@@ -1281,7 +1281,7 @@ exec_simple_query(const char *query_string)
 		 * command the client sent, regardless of rewriting. (But a command
 		 * aborted by error will not send an EndCommand report at all.)
 		 */
-		EndCommand(completionTag, dest);
+		EndCommand(&qc, dest, false);
 
 		/* Now we may drop the per-parsetree context, if one was created. */
 		if (per_parsetree_context)
@@ -1343,7 +1343,7 @@ exec_parse_message(const char *query_string,	/* string to execute */
 	MemoryContext oldcontext;
 	List	   *parsetree_list;
 	RawStmt    *raw_parse_tree;
-	const char *commandTag;
+	CommandTag	commandTag;
 	List	   *querytree_list;
 	CachedPlanSource *psrc;
 	bool		is_named;
@@ -1505,7 +1505,7 @@ exec_parse_message(const char *query_string,	/* string to execute */
 	{
 		/* Empty input string.  This is legal. */
 		raw_parse_tree = NULL;
-		commandTag = NULL;
+		commandTag = COMMANDTAG_UNKNOWN;
 		psrc = CreateCachedPlan(raw_parse_tree, query_string, commandTag);
 		querytree_list = NIL;
 	}
@@ -2022,7 +2022,7 @@ exec_execute_message(const char *portal_name, long max_rows)
 	DestReceiver *receiver;
 	Portal		portal;
 	bool		completed;
-	char		completionTag[COMPLETION_TAG_BUFSIZE];
+	QueryCompletion	qc;
 	const char *sourceText;
 	const char *prepStmtName;
 	ParamListInfo portalParams;
@@ -2049,7 +2049,7 @@ exec_execute_message(const char *portal_name, long max_rows)
 	 * If the original query was a null string, just return
 	 * EmptyQueryResponse.
 	 */
-	if (portal->commandTag == NULL)
+	if (portal->commandTag == COMMANDTAG_UNKNOWN)
 	{
 		Assert(portal->stmts == NIL);
 		NullCommand(dest);
@@ -2095,7 +2095,7 @@ exec_execute_message(const char *portal_name, long max_rows)
 
 	pgstat_report_activity(STATE_RUNNING, sourceText);
 
-	set_ps_display(portal->commandTag, false);
+	set_ps_display(GetCommandTagName(portal->commandTag), false);
 
 	if (save_log_statement_stats)
 		ResetUsage();
@@ -2176,7 +2176,7 @@ exec_execute_message(const char *portal_name, long max_rows)
 						  !execute_is_fetch && max_rows == FETCH_ALL,
 						  receiver,
 						  receiver,
-						  completionTag);
+						  &qc);
 
 	receiver->rDestroy(receiver);
 
@@ -2209,7 +2209,7 @@ exec_execute_message(const char *portal_name, long max_rows)
 		}
 
 		/* Send appropriate CommandComplete to client */
-		EndCommand(completionTag, dest);
+		EndCommand(&qc, dest, false);
 	}
 	else
 	{
diff --git a/src/backend/tcop/pquery.c b/src/backend/tcop/pquery.c
index 0f5801e046..f2089f5c34 100644
--- a/src/backend/tcop/pquery.c
+++ b/src/backend/tcop/pquery.c
@@ -40,7 +40,7 @@ static void ProcessQuery(PlannedStmt *plan,
 						 ParamListInfo params,
 						 QueryEnvironment *queryEnv,
 						 DestReceiver *dest,
-						 char *completionTag);
+						 QueryCompletion *qc);
 static void FillPortalStore(Portal portal, bool isTopLevel);
 static uint64 RunFromStore(Portal portal, ScanDirection direction, uint64 count,
 						   DestReceiver *dest);
@@ -48,11 +48,11 @@ static uint64 PortalRunSelect(Portal portal, bool forward, long count,
 							  DestReceiver *dest);
 static void PortalRunUtility(Portal portal, PlannedStmt *pstmt,
 							 bool isTopLevel, bool setHoldSnapshot,
-							 DestReceiver *dest, char *completionTag);
+							 DestReceiver *dest, QueryCompletion *qc);
 static void PortalRunMulti(Portal portal,
 						   bool isTopLevel, bool setHoldSnapshot,
 						   DestReceiver *dest, DestReceiver *altdest,
-						   char *completionTag);
+						   QueryCompletion *qc);
 static uint64 DoPortalRunFetch(Portal portal,
 							   FetchDirection fdirection,
 							   long count,
@@ -125,10 +125,9 @@ FreeQueryDesc(QueryDesc *qdesc)
  *	sourceText: the source text of the query
  *	params: any parameters needed
  *	dest: where to send results
- *	completionTag: points to a buffer of size COMPLETION_TAG_BUFSIZE
- *		in which to store a command completion status string.
+ *	qc: where to store the command completion status data.
  *
- * completionTag may be NULL if caller doesn't want a status string.
+ * qc may be NULL if caller doesn't want a status string.
  *
  * Must be called in a memory context that will be reset or deleted on
  * error; otherwise the executor's memory usage will be leaked.
@@ -139,7 +138,7 @@ ProcessQuery(PlannedStmt *plan,
 			 ParamListInfo params,
 			 QueryEnvironment *queryEnv,
 			 DestReceiver *dest,
-			 char *completionTag)
+			 QueryCompletion *qc)
 {
 	QueryDesc  *queryDesc;
 
@@ -161,38 +160,26 @@ ProcessQuery(PlannedStmt *plan,
 	ExecutorRun(queryDesc, ForwardScanDirection, 0L, true);
 
 	/*
-	 * Build command completion status string, if caller wants one.
+	 * Build command completion status data, if caller wants one.
 	 */
-	if (completionTag)
+	if (qc)
 	{
-		Oid			lastOid;
-
 		switch (queryDesc->operation)
 		{
 			case CMD_SELECT:
-				snprintf(completionTag, COMPLETION_TAG_BUFSIZE,
-						 "SELECT " UINT64_FORMAT,
-						 queryDesc->estate->es_processed);
+				SetQueryCompletion(qc, COMMANDTAG_SELECT, queryDesc->estate->es_processed);
 				break;
 			case CMD_INSERT:
-				/* lastoid doesn't exist anymore */
-				lastOid = InvalidOid;
-				snprintf(completionTag, COMPLETION_TAG_BUFSIZE,
-						 "INSERT %u " UINT64_FORMAT,
-						 lastOid, queryDesc->estate->es_processed);
+				SetQueryCompletion(qc, COMMANDTAG_INSERT, queryDesc->estate->es_processed);
 				break;
 			case CMD_UPDATE:
-				snprintf(completionTag, COMPLETION_TAG_BUFSIZE,
-						 "UPDATE " UINT64_FORMAT,
-						 queryDesc->estate->es_processed);
+				SetQueryCompletion(qc, COMMANDTAG_UPDATE, queryDesc->estate->es_processed);
 				break;
 			case CMD_DELETE:
-				snprintf(completionTag, COMPLETION_TAG_BUFSIZE,
-						 "DELETE " UINT64_FORMAT,
-						 queryDesc->estate->es_processed);
+				SetQueryCompletion(qc, COMMANDTAG_DELETE, queryDesc->estate->es_processed);
 				break;
 			default:
-				strcpy(completionTag, "???");
+				SetQueryCompletion(qc, COMMANDTAG_UNKNOWN, queryDesc->estate->es_processed);
 				break;
 		}
 	}
@@ -675,9 +662,8 @@ PortalSetResultFormat(Portal portal, int nFormats, int16 *formats)
  *
  * altdest: where to send output of non-primary queries
  *
- * completionTag: points to a buffer of size COMPLETION_TAG_BUFSIZE
- *		in which to store a command completion status string.
- *		May be NULL if caller doesn't want a status string.
+ * qc: where to store command completion status data.
+ *		May be NULL if caller doesn't want status data.
  *
  * Returns true if the portal's execution is complete, false if it was
  * suspended due to exhaustion of the count parameter.
@@ -685,7 +671,7 @@ PortalSetResultFormat(Portal portal, int nFormats, int16 *formats)
 bool
 PortalRun(Portal portal, long count, bool isTopLevel, bool run_once,
 		  DestReceiver *dest, DestReceiver *altdest,
-		  char *completionTag)
+		  QueryCompletion *qc)
 {
 	bool		result;
 	uint64		nprocessed;
@@ -700,9 +686,9 @@ PortalRun(Portal portal, long count, bool isTopLevel, bool run_once,
 
 	TRACE_POSTGRESQL_QUERY_EXECUTE_START();
 
-	/* Initialize completion tag to empty string */
-	if (completionTag)
-		completionTag[0] = '\0';
+	/* Initialize empty completion data */
+	if (qc)
+		InitializeQueryCompletion(qc);
 
 	if (log_executor_stats && portal->strategy != PORTAL_MULTI_QUERY)
 	{
@@ -771,16 +757,12 @@ PortalRun(Portal portal, long count, bool isTopLevel, bool run_once,
 
 				/*
 				 * If the portal result contains a command tag and the caller
-				 * gave us a pointer to store it, copy it. Patch the "SELECT"
-				 * tag to also provide the rowcount.
+				 * gave us a pointer to store it, copy it and update the rowcount.
 				 */
-				if (completionTag && portal->commandTag)
+				if (qc && portal->qc.commandTag != COMMANDTAG_UNKNOWN)
 				{
-					if (strcmp(portal->commandTag, "SELECT") == 0)
-						snprintf(completionTag, COMPLETION_TAG_BUFSIZE,
-								 "SELECT " UINT64_FORMAT, nprocessed);
-					else
-						strcpy(completionTag, portal->commandTag);
+					CopyQueryCompletion(qc, &portal->qc);
+					qc->nprocessed = nprocessed;
 				}
 
 				/* Mark portal not active */
@@ -794,7 +776,7 @@ PortalRun(Portal portal, long count, bool isTopLevel, bool run_once,
 
 			case PORTAL_MULTI_QUERY:
 				PortalRunMulti(portal, isTopLevel, false,
-							   dest, altdest, completionTag);
+							   dest, altdest, qc);
 
 				/* Prevent portal's commands from being re-executed */
 				MarkPortalDone(portal);
@@ -1005,8 +987,9 @@ static void
 FillPortalStore(Portal portal, bool isTopLevel)
 {
 	DestReceiver *treceiver;
-	char		completionTag[COMPLETION_TAG_BUFSIZE];
+	QueryCompletion	qc;
 
+	InitializeQueryCompletion(&qc);
 	PortalCreateHoldStore(portal);
 	treceiver = CreateDestReceiver(DestTuplestore);
 	SetTuplestoreDestReceiverParams(treceiver,
@@ -1014,8 +997,6 @@ FillPortalStore(Portal portal, bool isTopLevel)
 									portal->holdContext,
 									false);
 
-	completionTag[0] = '\0';
-
 	switch (portal->strategy)
 	{
 		case PORTAL_ONE_RETURNING:
@@ -1028,12 +1009,12 @@ FillPortalStore(Portal portal, bool isTopLevel)
 			 * portal's holdSnapshot to the snapshot used (or a copy of it).
 			 */
 			PortalRunMulti(portal, isTopLevel, true,
-						   treceiver, None_Receiver, completionTag);
+						   treceiver, None_Receiver, &qc);
 			break;
 
 		case PORTAL_UTIL_SELECT:
 			PortalRunUtility(portal, linitial_node(PlannedStmt, portal->stmts),
-							 isTopLevel, true, treceiver, completionTag);
+							 isTopLevel, true, treceiver, &qc);
 			break;
 
 		default:
@@ -1042,9 +1023,9 @@ FillPortalStore(Portal portal, bool isTopLevel)
 			break;
 	}
 
-	/* Override default completion tag with actual command result */
-	if (completionTag[0] != '\0')
-		portal->commandTag = pstrdup(completionTag);
+	/* Override portal completion data with actual command results */
+	if (qc.commandTag != COMMANDTAG_UNKNOWN)
+		CopyQueryCompletion(&portal->qc, &qc);
 
 	treceiver->rDestroy(treceiver);
 }
@@ -1130,7 +1111,7 @@ RunFromStore(Portal portal, ScanDirection direction, uint64 count,
 static void
 PortalRunUtility(Portal portal, PlannedStmt *pstmt,
 				 bool isTopLevel, bool setHoldSnapshot,
-				 DestReceiver *dest, char *completionTag)
+				 DestReceiver *dest, QueryCompletion *qc)
 {
 	Node	   *utilityStmt = pstmt->utilityStmt;
 	Snapshot	snapshot;
@@ -1178,7 +1159,7 @@ PortalRunUtility(Portal portal, PlannedStmt *pstmt,
 				   portal->portalParams,
 				   portal->queryEnv,
 				   dest,
-				   completionTag);
+				   qc);
 
 	/* Some utility statements may change context on us */
 	MemoryContextSwitchTo(portal->portalContext);
@@ -1202,7 +1183,7 @@ static void
 PortalRunMulti(Portal portal,
 			   bool isTopLevel, bool setHoldSnapshot,
 			   DestReceiver *dest, DestReceiver *altdest,
-			   char *completionTag)
+			   QueryCompletion *qc)
 {
 	bool		active_snapshot_set = false;
 	ListCell   *stmtlist_item;
@@ -1284,7 +1265,7 @@ PortalRunMulti(Portal portal,
 							 portal->sourceText,
 							 portal->portalParams,
 							 portal->queryEnv,
-							 dest, completionTag);
+							 dest, qc);
 			}
 			else
 			{
@@ -1319,7 +1300,7 @@ PortalRunMulti(Portal portal,
 				Assert(!active_snapshot_set);
 				/* statement can set tag string */
 				PortalRunUtility(portal, pstmt, isTopLevel, false,
-								 dest, completionTag);
+								 dest, qc);
 			}
 			else
 			{
@@ -1350,8 +1331,8 @@ PortalRunMulti(Portal portal,
 		PopActiveSnapshot();
 
 	/*
-	 * If a command completion tag was supplied, use it.  Otherwise use the
-	 * portal's commandTag as the default completion tag.
+	 * If a query completion data was supplied, use it.  Otherwise use the
+	 * portal's query completion data.
 	 *
 	 * Exception: Clients expect INSERT/UPDATE/DELETE tags to have counts, so
 	 * fake them with zeros.  This can happen with DO INSTEAD rules if there
@@ -1361,18 +1342,12 @@ PortalRunMulti(Portal portal,
 	 * e.g.  an INSERT that does an UPDATE instead should not print "0 1" if
 	 * one row was updated.  See QueryRewrite(), step 3, for details.
 	 */
-	if (completionTag && completionTag[0] == '\0')
+	if (qc && qc->commandTag == COMMANDTAG_UNKNOWN)
 	{
-		if (portal->commandTag)
-			strcpy(completionTag, portal->commandTag);
-		if (strcmp(completionTag, "SELECT") == 0)
-			sprintf(completionTag, "SELECT 0 0");
-		else if (strcmp(completionTag, "INSERT") == 0)
-			strcpy(completionTag, "INSERT 0 0");
-		else if (strcmp(completionTag, "UPDATE") == 0)
-			strcpy(completionTag, "UPDATE 0");
-		else if (strcmp(completionTag, "DELETE") == 0)
-			strcpy(completionTag, "DELETE 0");
+		if (portal->qc.commandTag != COMMANDTAG_UNKNOWN)
+			CopyQueryCompletion(qc, &portal->qc);
+		/* If the caller supplied a qc, we should have set it by now. */
+		Assert(qc->commandTag != COMMANDTAG_UNKNOWN);
 	}
 }
 
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index bb85b5e52a..b7b7c952a7 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -83,10 +83,9 @@ static void ProcessUtilitySlow(ParseState *pstate,
 							   ParamListInfo params,
 							   QueryEnvironment *queryEnv,
 							   DestReceiver *dest,
-							   char *completionTag);
+							   QueryCompletion *qc);
 static void ExecDropStmt(DropStmt *stmt, bool isTopLevel);
 
-
 /*
  * CommandIsReadOnly: is an executable query read-only?
  *
@@ -467,7 +466,6 @@ CheckRestrictedOperation(const char *cmdname)
 						cmdname)));
 }
 
-
 /*
  * ProcessUtility
  *		general utility function invoker
@@ -480,16 +478,13 @@ CheckRestrictedOperation(const char *cmdname)
  *	queryEnv: environment for parse through execution (e.g., ephemeral named
  *		tables like trigger transition tables).  May be NULL.
  *	dest: where to send results
- *	completionTag: points to a buffer of size COMPLETION_TAG_BUFSIZE
- *		in which to store a command completion status string.
+ *	qc: where to store command completion status data.
  *
  * Caller MUST supply a queryString; it is not allowed (anymore) to pass NULL.
  * If you really don't have source text, you can pass a constant string,
  * perhaps "(query not available)".
  *
- * completionTag is only set nonempty if we want to return a nondefault status.
- *
- * completionTag may be NULL if caller doesn't want a status string.
+ * qc may be NULL if caller doesn't want status data.
  *
  * Note for users of ProcessUtility_hook: the same queryString may be passed
  * to multiple invocations of ProcessUtility when processing a query string
@@ -507,7 +502,7 @@ ProcessUtility(PlannedStmt *pstmt,
 			   ParamListInfo params,
 			   QueryEnvironment *queryEnv,
 			   DestReceiver *dest,
-			   char *completionTag)
+			   QueryCompletion *qc)
 {
 	Assert(IsA(pstmt, PlannedStmt));
 	Assert(pstmt->commandType == CMD_UTILITY);
@@ -521,11 +516,11 @@ ProcessUtility(PlannedStmt *pstmt,
 	if (ProcessUtility_hook)
 		(*ProcessUtility_hook) (pstmt, queryString,
 								context, params, queryEnv,
-								dest, completionTag);
+								dest, qc);
 	else
 		standard_ProcessUtility(pstmt, queryString,
 								context, params, queryEnv,
-								dest, completionTag);
+								dest, qc);
 }
 
 /*
@@ -546,7 +541,7 @@ standard_ProcessUtility(PlannedStmt *pstmt,
 						ParamListInfo params,
 						QueryEnvironment *queryEnv,
 						DestReceiver *dest,
-						char *completionTag)
+						QueryCompletion *qc)
 {
 	Node	   *parsetree = pstmt->utilityStmt;
 	bool		isTopLevel = (context == PROCESS_UTILITY_TOPLEVEL);
@@ -562,18 +557,18 @@ standard_ProcessUtility(PlannedStmt *pstmt,
 	if (readonly_flags != COMMAND_IS_STRICTLY_READ_ONLY &&
 		(XactReadOnly || IsInParallelMode()))
 	{
-		const char *commandtag = CreateCommandTag(parsetree);
+		CommandTag	commandtag = CreateCommandTag(parsetree);
 
 		if ((readonly_flags & COMMAND_OK_IN_READ_ONLY_TXN) == 0)
-			PreventCommandIfReadOnly(commandtag);
+			PreventCommandTagIfReadOnly(commandtag);
 		if ((readonly_flags & COMMAND_OK_IN_PARALLEL_MODE) == 0)
-			PreventCommandIfParallelMode(commandtag);
+			PreventCommandTagIfParallelMode(commandtag);
 		if ((readonly_flags & COMMAND_OK_IN_RECOVERY) == 0)
-			PreventCommandDuringRecovery(commandtag);
+			PreventCommandTagDuringRecovery(commandtag);
 	}
 
-	if (completionTag)
-		completionTag[0] = '\0';
+	if (qc)
+		InitializeQueryCompletion(qc);
 
 	pstate = make_parsestate(NULL);
 	pstate->p_sourcetext = queryString;
@@ -623,18 +618,18 @@ standard_ProcessUtility(PlannedStmt *pstmt,
 					case TRANS_STMT_COMMIT:
 						if (!EndTransactionBlock(stmt->chain))
 						{
-							/* report unsuccessful commit in completionTag */
-							if (completionTag)
-								strcpy(completionTag, "ROLLBACK");
+							/* report unsuccessful commit in qc */
+							if (qc)
+								SetQueryCompletion(qc, COMMANDTAG_ROLLBACK, 0);
 						}
 						break;
 
 					case TRANS_STMT_PREPARE:
 						if (!PrepareTransactionBlock(stmt->gid))
 						{
-							/* report unsuccessful commit in completionTag */
-							if (completionTag)
-								strcpy(completionTag, "ROLLBACK");
+							/* report unsuccessful commit in qc */
+							if (qc)
+								SetQueryCompletion(qc, COMMANDTAG_ROLLBACK, 0);
 						}
 						break;
 
@@ -693,8 +688,7 @@ standard_ProcessUtility(PlannedStmt *pstmt,
 			break;
 
 		case T_FetchStmt:
-			PerformPortalFetch((FetchStmt *) parsetree, dest,
-							   completionTag);
+			PerformPortalFetch((FetchStmt *) parsetree, dest, qc);
 			break;
 
 		case T_DoStmt:
@@ -729,9 +723,8 @@ standard_ProcessUtility(PlannedStmt *pstmt,
 				DoCopy(pstate, (CopyStmt *) parsetree,
 					   pstmt->stmt_location, pstmt->stmt_len,
 					   &processed);
-				if (completionTag)
-					snprintf(completionTag, COMPLETION_TAG_BUFSIZE,
-							 "COPY " UINT64_FORMAT, processed);
+				if (qc)
+					SetQueryCompletion(qc, COMMANDTAG_COPY, processed);
 			}
 			break;
 
@@ -745,7 +738,7 @@ standard_ProcessUtility(PlannedStmt *pstmt,
 			ExecuteQuery(pstate,
 						 (ExecuteStmt *) parsetree, NULL,
 						 params,
-						 dest, completionTag);
+						 dest, qc);
 			break;
 
 		case T_DeallocateStmt:
@@ -974,7 +967,7 @@ standard_ProcessUtility(PlannedStmt *pstmt,
 				if (EventTriggerSupportsObjectType(stmt->objtype))
 					ProcessUtilitySlow(pstate, pstmt, queryString,
 									   context, params, queryEnv,
-									   dest, completionTag);
+									   dest, qc);
 				else
 					ExecuteGrantStmt(stmt);
 			}
@@ -987,7 +980,7 @@ standard_ProcessUtility(PlannedStmt *pstmt,
 				if (EventTriggerSupportsObjectType(stmt->removeType))
 					ProcessUtilitySlow(pstate, pstmt, queryString,
 									   context, params, queryEnv,
-									   dest, completionTag);
+									   dest, qc);
 				else
 					ExecDropStmt(stmt, isTopLevel);
 			}
@@ -1000,7 +993,7 @@ standard_ProcessUtility(PlannedStmt *pstmt,
 				if (EventTriggerSupportsObjectType(stmt->renameType))
 					ProcessUtilitySlow(pstate, pstmt, queryString,
 									   context, params, queryEnv,
-									   dest, completionTag);
+									   dest, qc);
 				else
 					ExecRenameStmt(stmt);
 			}
@@ -1013,7 +1006,7 @@ standard_ProcessUtility(PlannedStmt *pstmt,
 				if (EventTriggerSupportsObjectType(stmt->objectType))
 					ProcessUtilitySlow(pstate, pstmt, queryString,
 									   context, params, queryEnv,
-									   dest, completionTag);
+									   dest, qc);
 				else
 					ExecAlterObjectDependsStmt(stmt, NULL);
 			}
@@ -1026,7 +1019,7 @@ standard_ProcessUtility(PlannedStmt *pstmt,
 				if (EventTriggerSupportsObjectType(stmt->objectType))
 					ProcessUtilitySlow(pstate, pstmt, queryString,
 									   context, params, queryEnv,
-									   dest, completionTag);
+									   dest, qc);
 				else
 					ExecAlterObjectSchemaStmt(stmt, NULL);
 			}
@@ -1039,7 +1032,7 @@ standard_ProcessUtility(PlannedStmt *pstmt,
 				if (EventTriggerSupportsObjectType(stmt->objectType))
 					ProcessUtilitySlow(pstate, pstmt, queryString,
 									   context, params, queryEnv,
-									   dest, completionTag);
+									   dest, qc);
 				else
 					ExecAlterOwnerStmt(stmt);
 			}
@@ -1052,7 +1045,7 @@ standard_ProcessUtility(PlannedStmt *pstmt,
 				if (EventTriggerSupportsObjectType(stmt->objtype))
 					ProcessUtilitySlow(pstate, pstmt, queryString,
 									   context, params, queryEnv,
-									   dest, completionTag);
+									   dest, qc);
 				else
 					CommentObject(stmt);
 				break;
@@ -1065,7 +1058,7 @@ standard_ProcessUtility(PlannedStmt *pstmt,
 				if (EventTriggerSupportsObjectType(stmt->objtype))
 					ProcessUtilitySlow(pstate, pstmt, queryString,
 									   context, params, queryEnv,
-									   dest, completionTag);
+									   dest, qc);
 				else
 					ExecSecLabelStmt(stmt);
 				break;
@@ -1075,7 +1068,7 @@ standard_ProcessUtility(PlannedStmt *pstmt,
 			/* All other statement types have event trigger support */
 			ProcessUtilitySlow(pstate, pstmt, queryString,
 							   context, params, queryEnv,
-							   dest, completionTag);
+							   dest, qc);
 			break;
 	}
 
@@ -1102,7 +1095,7 @@ ProcessUtilitySlow(ParseState *pstate,
 				   ParamListInfo params,
 				   QueryEnvironment *queryEnv,
 				   DestReceiver *dest,
-				   char *completionTag)
+				   QueryCompletion *qc)
 {
 	Node	   *parsetree = pstmt->utilityStmt;
 	bool		isTopLevel = (context == PROCESS_UTILITY_TOPLEVEL);
@@ -1605,7 +1598,7 @@ ProcessUtilitySlow(ParseState *pstate,
 
 			case T_CreateTableAsStmt:
 				address = ExecCreateTableAs(pstate, (CreateTableAsStmt *) parsetree,
-											params, queryEnv, completionTag);
+											params, queryEnv, qc);
 				break;
 
 			case T_RefreshMatViewStmt:
@@ -1620,7 +1613,7 @@ ProcessUtilitySlow(ParseState *pstate,
 				PG_TRY();
 				{
 					address = ExecRefreshMatView((RefreshMatViewStmt *) parsetree,
-												 queryString, params, completionTag);
+												 queryString, params, qc);
 				}
 				PG_FINALLY();
 				{
@@ -2099,137 +2092,137 @@ UtilityContainsQuery(Node *parsetree)
  *
  * This covers most cases where ALTER is used with an ObjectType enum.
  */
-static const char *
+static CommandTag
 AlterObjectTypeCommandTag(ObjectType objtype)
 {
-	const char *tag;
+	CommandTag	tag;
 
 	switch (objtype)
 	{
 		case OBJECT_AGGREGATE:
-			tag = "ALTER AGGREGATE";
+			tag = COMMANDTAG_ALTER_AGGREGATE;
 			break;
 		case OBJECT_ATTRIBUTE:
-			tag = "ALTER TYPE";
+			tag = COMMANDTAG_ALTER_TYPE;
 			break;
 		case OBJECT_CAST:
-			tag = "ALTER CAST";
+			tag = COMMANDTAG_ALTER_CAST;
 			break;
 		case OBJECT_COLLATION:
-			tag = "ALTER COLLATION";
+			tag = COMMANDTAG_ALTER_COLLATION;
 			break;
 		case OBJECT_COLUMN:
-			tag = "ALTER TABLE";
+			tag = COMMANDTAG_ALTER_TABLE;
 			break;
 		case OBJECT_CONVERSION:
-			tag = "ALTER CONVERSION";
+			tag = COMMANDTAG_ALTER_CONVERSION;
 			break;
 		case OBJECT_DATABASE:
-			tag = "ALTER DATABASE";
+			tag = COMMANDTAG_ALTER_DATABASE;
 			break;
 		case OBJECT_DOMAIN:
 		case OBJECT_DOMCONSTRAINT:
-			tag = "ALTER DOMAIN";
+			tag = COMMANDTAG_ALTER_DOMAIN;
 			break;
 		case OBJECT_EXTENSION:
-			tag = "ALTER EXTENSION";
+			tag = COMMANDTAG_ALTER_EXTENSION;
 			break;
 		case OBJECT_FDW:
-			tag = "ALTER FOREIGN DATA WRAPPER";
+			tag = COMMANDTAG_ALTER_FOREIGN_DATA_WRAPPER;
 			break;
 		case OBJECT_FOREIGN_SERVER:
-			tag = "ALTER SERVER";
+			tag = COMMANDTAG_ALTER_SERVER;
 			break;
 		case OBJECT_FOREIGN_TABLE:
-			tag = "ALTER FOREIGN TABLE";
+			tag = COMMANDTAG_ALTER_FOREIGN_TABLE;
 			break;
 		case OBJECT_FUNCTION:
-			tag = "ALTER FUNCTION";
+			tag = COMMANDTAG_ALTER_FUNCTION;
 			break;
 		case OBJECT_INDEX:
-			tag = "ALTER INDEX";
+			tag = COMMANDTAG_ALTER_INDEX;
 			break;
 		case OBJECT_LANGUAGE:
-			tag = "ALTER LANGUAGE";
+			tag = COMMANDTAG_ALTER_LANGUAGE;
 			break;
 		case OBJECT_LARGEOBJECT:
-			tag = "ALTER LARGE OBJECT";
+			tag = COMMANDTAG_ALTER_LARGE_OBJECT;
 			break;
 		case OBJECT_OPCLASS:
-			tag = "ALTER OPERATOR CLASS";
+			tag = COMMANDTAG_ALTER_OPERATOR_CLASS;
 			break;
 		case OBJECT_OPERATOR:
-			tag = "ALTER OPERATOR";
+			tag = COMMANDTAG_ALTER_OPERATOR;
 			break;
 		case OBJECT_OPFAMILY:
-			tag = "ALTER OPERATOR FAMILY";
+			tag = COMMANDTAG_ALTER_OPERATOR_FAMILY;
 			break;
 		case OBJECT_POLICY:
-			tag = "ALTER POLICY";
+			tag = COMMANDTAG_ALTER_POLICY;
 			break;
 		case OBJECT_PROCEDURE:
-			tag = "ALTER PROCEDURE";
+			tag = COMMANDTAG_ALTER_PROCEDURE;
 			break;
 		case OBJECT_ROLE:
-			tag = "ALTER ROLE";
+			tag = COMMANDTAG_ALTER_ROLE;
 			break;
 		case OBJECT_ROUTINE:
-			tag = "ALTER ROUTINE";
+			tag = COMMANDTAG_ALTER_ROUTINE;
 			break;
 		case OBJECT_RULE:
-			tag = "ALTER RULE";
+			tag = COMMANDTAG_ALTER_RULE;
 			break;
 		case OBJECT_SCHEMA:
-			tag = "ALTER SCHEMA";
+			tag = COMMANDTAG_ALTER_SCHEMA;
 			break;
 		case OBJECT_SEQUENCE:
-			tag = "ALTER SEQUENCE";
+			tag = COMMANDTAG_ALTER_SEQUENCE;
 			break;
 		case OBJECT_TABLE:
 		case OBJECT_TABCONSTRAINT:
-			tag = "ALTER TABLE";
+			tag = COMMANDTAG_ALTER_TABLE;
 			break;
 		case OBJECT_TABLESPACE:
-			tag = "ALTER TABLESPACE";
+			tag = COMMANDTAG_ALTER_TABLESPACE;
 			break;
 		case OBJECT_TRIGGER:
-			tag = "ALTER TRIGGER";
+			tag = COMMANDTAG_ALTER_TRIGGER;
 			break;
 		case OBJECT_EVENT_TRIGGER:
-			tag = "ALTER EVENT TRIGGER";
+			tag = COMMANDTAG_ALTER_EVENT_TRIGGER;
 			break;
 		case OBJECT_TSCONFIGURATION:
-			tag = "ALTER TEXT SEARCH CONFIGURATION";
+			tag = COMMANDTAG_ALTER_TEXT_SEARCH_CONFIGURATION;
 			break;
 		case OBJECT_TSDICTIONARY:
-			tag = "ALTER TEXT SEARCH DICTIONARY";
+			tag = COMMANDTAG_ALTER_TEXT_SEARCH_DICTIONARY;
 			break;
 		case OBJECT_TSPARSER:
-			tag = "ALTER TEXT SEARCH PARSER";
+			tag = COMMANDTAG_ALTER_TEXT_SEARCH_PARSER;
 			break;
 		case OBJECT_TSTEMPLATE:
-			tag = "ALTER TEXT SEARCH TEMPLATE";
+			tag = COMMANDTAG_ALTER_TEXT_SEARCH_TEMPLATE;
 			break;
 		case OBJECT_TYPE:
-			tag = "ALTER TYPE";
+			tag = COMMANDTAG_ALTER_TYPE;
 			break;
 		case OBJECT_VIEW:
-			tag = "ALTER VIEW";
+			tag = COMMANDTAG_ALTER_VIEW;
 			break;
 		case OBJECT_MATVIEW:
-			tag = "ALTER MATERIALIZED VIEW";
+			tag = COMMANDTAG_ALTER_MATERIALIZED_VIEW;
 			break;
 		case OBJECT_PUBLICATION:
-			tag = "ALTER PUBLICATION";
+			tag = COMMANDTAG_ALTER_PUBLICATION;
 			break;
 		case OBJECT_SUBSCRIPTION:
-			tag = "ALTER SUBSCRIPTION";
+			tag = COMMANDTAG_ALTER_SUBSCRIPTION;
 			break;
 		case OBJECT_STATISTIC_EXT:
-			tag = "ALTER STATISTICS";
+			tag = COMMANDTAG_ALTER_STATISTICS;
 			break;
 		default:
-			tag = "???";
+			tag = COMMANDTAG_UNKNOWN;
 			break;
 	}
 
@@ -2238,20 +2231,17 @@ AlterObjectTypeCommandTag(ObjectType objtype)
 
 /*
  * CreateCommandTag
- *		utility to get a string representation of the command operation,
+ *		utility to get a CommandTag for the command operation,
  *		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.
- *
- * NB: all result strings must be shorter than COMPLETION_TAG_BUFSIZE.
- * Also, the result must point at a true constant (permanent storage).
  */
-const char *
+CommandTag
 CreateCommandTag(Node *parsetree)
 {
-	const char *tag;
+	CommandTag	tag;
 
 	switch (nodeTag(parsetree))
 	{
@@ -2262,19 +2252,19 @@ CreateCommandTag(Node *parsetree)
 
 			/* raw plannable queries */
 		case T_InsertStmt:
-			tag = "INSERT";
+			tag = COMMANDTAG_INSERT;
 			break;
 
 		case T_DeleteStmt:
-			tag = "DELETE";
+			tag = COMMANDTAG_DELETE;
 			break;
 
 		case T_UpdateStmt:
-			tag = "UPDATE";
+			tag = COMMANDTAG_UPDATE;
 			break;
 
 		case T_SelectStmt:
-			tag = "SELECT";
+			tag = COMMANDTAG_SELECT;
 			break;
 
 			/* utility statements --- same whether raw or cooked */
@@ -2285,51 +2275,51 @@ CreateCommandTag(Node *parsetree)
 				switch (stmt->kind)
 				{
 					case TRANS_STMT_BEGIN:
-						tag = "BEGIN";
+						tag = COMMANDTAG_BEGIN;
 						break;
 
 					case TRANS_STMT_START:
-						tag = "START TRANSACTION";
+						tag = COMMANDTAG_START_TRANSACTION;
 						break;
 
 					case TRANS_STMT_COMMIT:
-						tag = "COMMIT";
+						tag = COMMANDTAG_COMMIT;
 						break;
 
 					case TRANS_STMT_ROLLBACK:
 					case TRANS_STMT_ROLLBACK_TO:
-						tag = "ROLLBACK";
+						tag = COMMANDTAG_ROLLBACK;
 						break;
 
 					case TRANS_STMT_SAVEPOINT:
-						tag = "SAVEPOINT";
+						tag = COMMANDTAG_SAVEPOINT;
 						break;
 
 					case TRANS_STMT_RELEASE:
-						tag = "RELEASE";
+						tag = COMMANDTAG_RELEASE;
 						break;
 
 					case TRANS_STMT_PREPARE:
-						tag = "PREPARE TRANSACTION";
+						tag = COMMANDTAG_PREPARE_TRANSACTION;
 						break;
 
 					case TRANS_STMT_COMMIT_PREPARED:
-						tag = "COMMIT PREPARED";
+						tag = COMMANDTAG_COMMIT_PREPARED;
 						break;
 
 					case TRANS_STMT_ROLLBACK_PREPARED:
-						tag = "ROLLBACK PREPARED";
+						tag = COMMANDTAG_ROLLBACK_PREPARED;
 						break;
 
 					default:
-						tag = "???";
+						tag = COMMANDTAG_UNKNOWN;
 						break;
 				}
 			}
 			break;
 
 		case T_DeclareCursorStmt:
-			tag = "DECLARE CURSOR";
+			tag = COMMANDTAG_DECLARE_CURSOR;
 			break;
 
 		case T_ClosePortalStmt:
@@ -2337,9 +2327,9 @@ CreateCommandTag(Node *parsetree)
 				ClosePortalStmt *stmt = (ClosePortalStmt *) parsetree;
 
 				if (stmt->portalname == NULL)
-					tag = "CLOSE CURSOR ALL";
+					tag = COMMANDTAG_CLOSE_CURSOR_ALL;
 				else
-					tag = "CLOSE CURSOR";
+					tag = COMMANDTAG_CLOSE_CURSOR;
 			}
 			break;
 
@@ -2347,209 +2337,209 @@ CreateCommandTag(Node *parsetree)
 			{
 				FetchStmt  *stmt = (FetchStmt *) parsetree;
 
-				tag = (stmt->ismove) ? "MOVE" : "FETCH";
+				tag = (stmt->ismove) ? COMMANDTAG_MOVE : COMMANDTAG_FETCH;
 			}
 			break;
 
 		case T_CreateDomainStmt:
-			tag = "CREATE DOMAIN";
+			tag = COMMANDTAG_CREATE_DOMAIN;
 			break;
 
 		case T_CreateSchemaStmt:
-			tag = "CREATE SCHEMA";
+			tag = COMMANDTAG_CREATE_SCHEMA;
 			break;
 
 		case T_CreateStmt:
-			tag = "CREATE TABLE";
+			tag = COMMANDTAG_CREATE_TABLE;
 			break;
 
 		case T_CreateTableSpaceStmt:
-			tag = "CREATE TABLESPACE";
+			tag = COMMANDTAG_CREATE_TABLESPACE;
 			break;
 
 		case T_DropTableSpaceStmt:
-			tag = "DROP TABLESPACE";
+			tag = COMMANDTAG_DROP_TABLESPACE;
 			break;
 
 		case T_AlterTableSpaceOptionsStmt:
-			tag = "ALTER TABLESPACE";
+			tag = COMMANDTAG_ALTER_TABLESPACE;
 			break;
 
 		case T_CreateExtensionStmt:
-			tag = "CREATE EXTENSION";
+			tag = COMMANDTAG_CREATE_EXTENSION;
 			break;
 
 		case T_AlterExtensionStmt:
-			tag = "ALTER EXTENSION";
+			tag = COMMANDTAG_ALTER_EXTENSION;
 			break;
 
 		case T_AlterExtensionContentsStmt:
-			tag = "ALTER EXTENSION";
+			tag = COMMANDTAG_ALTER_EXTENSION;
 			break;
 
 		case T_CreateFdwStmt:
-			tag = "CREATE FOREIGN DATA WRAPPER";
+			tag = COMMANDTAG_CREATE_FOREIGN_DATA_WRAPPER;
 			break;
 
 		case T_AlterFdwStmt:
-			tag = "ALTER FOREIGN DATA WRAPPER";
+			tag = COMMANDTAG_ALTER_FOREIGN_DATA_WRAPPER;
 			break;
 
 		case T_CreateForeignServerStmt:
-			tag = "CREATE SERVER";
+			tag = COMMANDTAG_CREATE_SERVER;
 			break;
 
 		case T_AlterForeignServerStmt:
-			tag = "ALTER SERVER";
+			tag = COMMANDTAG_ALTER_SERVER;
 			break;
 
 		case T_CreateUserMappingStmt:
-			tag = "CREATE USER MAPPING";
+			tag = COMMANDTAG_CREATE_USER_MAPPING;
 			break;
 
 		case T_AlterUserMappingStmt:
-			tag = "ALTER USER MAPPING";
+			tag = COMMANDTAG_ALTER_USER_MAPPING;
 			break;
 
 		case T_DropUserMappingStmt:
-			tag = "DROP USER MAPPING";
+			tag = COMMANDTAG_DROP_USER_MAPPING;
 			break;
 
 		case T_CreateForeignTableStmt:
-			tag = "CREATE FOREIGN TABLE";
+			tag = COMMANDTAG_CREATE_FOREIGN_TABLE;
 			break;
 
 		case T_ImportForeignSchemaStmt:
-			tag = "IMPORT FOREIGN SCHEMA";
+			tag = COMMANDTAG_IMPORT_FOREIGN_SCHEMA;
 			break;
 
 		case T_DropStmt:
 			switch (((DropStmt *) parsetree)->removeType)
 			{
 				case OBJECT_TABLE:
-					tag = "DROP TABLE";
+					tag = COMMANDTAG_DROP_TABLE;
 					break;
 				case OBJECT_SEQUENCE:
-					tag = "DROP SEQUENCE";
+					tag = COMMANDTAG_DROP_SEQUENCE;
 					break;
 				case OBJECT_VIEW:
-					tag = "DROP VIEW";
+					tag = COMMANDTAG_DROP_VIEW;
 					break;
 				case OBJECT_MATVIEW:
-					tag = "DROP MATERIALIZED VIEW";
+					tag = COMMANDTAG_DROP_MATERIALIZED_VIEW;
 					break;
 				case OBJECT_INDEX:
-					tag = "DROP INDEX";
+					tag = COMMANDTAG_DROP_INDEX;
 					break;
 				case OBJECT_TYPE:
-					tag = "DROP TYPE";
+					tag = COMMANDTAG_DROP_TYPE;
 					break;
 				case OBJECT_DOMAIN:
-					tag = "DROP DOMAIN";
+					tag = COMMANDTAG_DROP_DOMAIN;
 					break;
 				case OBJECT_COLLATION:
-					tag = "DROP COLLATION";
+					tag = COMMANDTAG_DROP_COLLATION;
 					break;
 				case OBJECT_CONVERSION:
-					tag = "DROP CONVERSION";
+					tag = COMMANDTAG_DROP_CONVERSION;
 					break;
 				case OBJECT_SCHEMA:
-					tag = "DROP SCHEMA";
+					tag = COMMANDTAG_DROP_SCHEMA;
 					break;
 				case OBJECT_TSPARSER:
-					tag = "DROP TEXT SEARCH PARSER";
+					tag = COMMANDTAG_DROP_TEXT_SEARCH_PARSER;
 					break;
 				case OBJECT_TSDICTIONARY:
-					tag = "DROP TEXT SEARCH DICTIONARY";
+					tag = COMMANDTAG_DROP_TEXT_SEARCH_DICTIONARY;
 					break;
 				case OBJECT_TSTEMPLATE:
-					tag = "DROP TEXT SEARCH TEMPLATE";
+					tag = COMMANDTAG_DROP_TEXT_SEARCH_TEMPLATE;
 					break;
 				case OBJECT_TSCONFIGURATION:
-					tag = "DROP TEXT SEARCH CONFIGURATION";
+					tag = COMMANDTAG_DROP_TEXT_SEARCH_CONFIGURATION;
 					break;
 				case OBJECT_FOREIGN_TABLE:
-					tag = "DROP FOREIGN TABLE";
+					tag = COMMANDTAG_DROP_FOREIGN_TABLE;
 					break;
 				case OBJECT_EXTENSION:
-					tag = "DROP EXTENSION";
+					tag = COMMANDTAG_DROP_EXTENSION;
 					break;
 				case OBJECT_FUNCTION:
-					tag = "DROP FUNCTION";
+					tag = COMMANDTAG_DROP_FUNCTION;
 					break;
 				case OBJECT_PROCEDURE:
-					tag = "DROP PROCEDURE";
+					tag = COMMANDTAG_DROP_PROCEDURE;
 					break;
 				case OBJECT_ROUTINE:
-					tag = "DROP ROUTINE";
+					tag = COMMANDTAG_DROP_ROUTINE;
 					break;
 				case OBJECT_AGGREGATE:
-					tag = "DROP AGGREGATE";
+					tag = COMMANDTAG_DROP_AGGREGATE;
 					break;
 				case OBJECT_OPERATOR:
-					tag = "DROP OPERATOR";
+					tag = COMMANDTAG_DROP_OPERATOR;
 					break;
 				case OBJECT_LANGUAGE:
-					tag = "DROP LANGUAGE";
+					tag = COMMANDTAG_DROP_LANGUAGE;
 					break;
 				case OBJECT_CAST:
-					tag = "DROP CAST";
+					tag = COMMANDTAG_DROP_CAST;
 					break;
 				case OBJECT_TRIGGER:
-					tag = "DROP TRIGGER";
+					tag = COMMANDTAG_DROP_TRIGGER;
 					break;
 				case OBJECT_EVENT_TRIGGER:
-					tag = "DROP EVENT TRIGGER";
+					tag = COMMANDTAG_DROP_EVENT_TRIGGER;
 					break;
 				case OBJECT_RULE:
-					tag = "DROP RULE";
+					tag = COMMANDTAG_DROP_RULE;
 					break;
 				case OBJECT_FDW:
-					tag = "DROP FOREIGN DATA WRAPPER";
+					tag = COMMANDTAG_DROP_FOREIGN_DATA_WRAPPER;
 					break;
 				case OBJECT_FOREIGN_SERVER:
-					tag = "DROP SERVER";
+					tag = COMMANDTAG_DROP_SERVER;
 					break;
 				case OBJECT_OPCLASS:
-					tag = "DROP OPERATOR CLASS";
+					tag = COMMANDTAG_DROP_OPERATOR_CLASS;
 					break;
 				case OBJECT_OPFAMILY:
-					tag = "DROP OPERATOR FAMILY";
+					tag = COMMANDTAG_DROP_OPERATOR_FAMILY;
 					break;
 				case OBJECT_POLICY:
-					tag = "DROP POLICY";
+					tag = COMMANDTAG_DROP_POLICY;
 					break;
 				case OBJECT_TRANSFORM:
-					tag = "DROP TRANSFORM";
+					tag = COMMANDTAG_DROP_TRANSFORM;
 					break;
 				case OBJECT_ACCESS_METHOD:
-					tag = "DROP ACCESS METHOD";
+					tag = COMMANDTAG_DROP_ACCESS_METHOD;
 					break;
 				case OBJECT_PUBLICATION:
-					tag = "DROP PUBLICATION";
+					tag = COMMANDTAG_DROP_PUBLICATION;
 					break;
 				case OBJECT_STATISTIC_EXT:
-					tag = "DROP STATISTICS";
+					tag = COMMANDTAG_DROP_STATISTICS;
 					break;
 				default:
-					tag = "???";
+					tag = COMMANDTAG_UNKNOWN;
 			}
 			break;
 
 		case T_TruncateStmt:
-			tag = "TRUNCATE TABLE";
+			tag = COMMANDTAG_TRUNCATE_TABLE;
 			break;
 
 		case T_CommentStmt:
-			tag = "COMMENT";
+			tag = COMMANDTAG_COMMENT;
 			break;
 
 		case T_SecLabelStmt:
-			tag = "SECURITY LABEL";
+			tag = COMMANDTAG_SECURITY_LABEL;
 			break;
 
 		case T_CopyStmt:
-			tag = "COPY";
+			tag = COMMANDTAG_COPY;
 			break;
 
 		case T_RenameStmt:
@@ -2584,23 +2574,23 @@ CreateCommandTag(Node *parsetree)
 			break;
 
 		case T_AlterDomainStmt:
-			tag = "ALTER DOMAIN";
+			tag = COMMANDTAG_ALTER_DOMAIN;
 			break;
 
 		case T_AlterFunctionStmt:
 			switch (((AlterFunctionStmt *) parsetree)->objtype)
 			{
 				case OBJECT_FUNCTION:
-					tag = "ALTER FUNCTION";
+					tag = COMMANDTAG_ALTER_FUNCTION;
 					break;
 				case OBJECT_PROCEDURE:
-					tag = "ALTER PROCEDURE";
+					tag = COMMANDTAG_ALTER_PROCEDURE;
 					break;
 				case OBJECT_ROUTINE:
-					tag = "ALTER ROUTINE";
+					tag = COMMANDTAG_ALTER_ROUTINE;
 					break;
 				default:
-					tag = "???";
+					tag = COMMANDTAG_UNKNOWN;
 			}
 			break;
 
@@ -2608,7 +2598,7 @@ CreateCommandTag(Node *parsetree)
 			{
 				GrantStmt  *stmt = (GrantStmt *) parsetree;
 
-				tag = (stmt->is_grant) ? "GRANT" : "REVOKE";
+				tag = (stmt->is_grant) ? COMMANDTAG_GRANT : COMMANDTAG_REVOKE;
 			}
 			break;
 
@@ -2616,145 +2606,145 @@ CreateCommandTag(Node *parsetree)
 			{
 				GrantRoleStmt *stmt = (GrantRoleStmt *) parsetree;
 
-				tag = (stmt->is_grant) ? "GRANT ROLE" : "REVOKE ROLE";
+				tag = (stmt->is_grant) ? COMMANDTAG_GRANT_ROLE : COMMANDTAG_REVOKE_ROLE;
 			}
 			break;
 
 		case T_AlterDefaultPrivilegesStmt:
-			tag = "ALTER DEFAULT PRIVILEGES";
+			tag = COMMANDTAG_ALTER_DEFAULT_PRIVILEGES;
 			break;
 
 		case T_DefineStmt:
 			switch (((DefineStmt *) parsetree)->kind)
 			{
 				case OBJECT_AGGREGATE:
-					tag = "CREATE AGGREGATE";
+					tag = COMMANDTAG_CREATE_AGGREGATE;
 					break;
 				case OBJECT_OPERATOR:
-					tag = "CREATE OPERATOR";
+					tag = COMMANDTAG_CREATE_OPERATOR;
 					break;
 				case OBJECT_TYPE:
-					tag = "CREATE TYPE";
+					tag = COMMANDTAG_CREATE_TYPE;
 					break;
 				case OBJECT_TSPARSER:
-					tag = "CREATE TEXT SEARCH PARSER";
+					tag = COMMANDTAG_CREATE_TEXT_SEARCH_PARSER;
 					break;
 				case OBJECT_TSDICTIONARY:
-					tag = "CREATE TEXT SEARCH DICTIONARY";
+					tag = COMMANDTAG_CREATE_TEXT_SEARCH_DICTIONARY;
 					break;
 				case OBJECT_TSTEMPLATE:
-					tag = "CREATE TEXT SEARCH TEMPLATE";
+					tag = COMMANDTAG_CREATE_TEXT_SEARCH_TEMPLATE;
 					break;
 				case OBJECT_TSCONFIGURATION:
-					tag = "CREATE TEXT SEARCH CONFIGURATION";
+					tag = COMMANDTAG_CREATE_TEXT_SEARCH_CONFIGURATION;
 					break;
 				case OBJECT_COLLATION:
-					tag = "CREATE COLLATION";
+					tag = COMMANDTAG_CREATE_COLLATION;
 					break;
 				case OBJECT_ACCESS_METHOD:
-					tag = "CREATE ACCESS METHOD";
+					tag = COMMANDTAG_CREATE_ACCESS_METHOD;
 					break;
 				default:
-					tag = "???";
+					tag = COMMANDTAG_UNKNOWN;
 			}
 			break;
 
 		case T_CompositeTypeStmt:
-			tag = "CREATE TYPE";
+			tag = COMMANDTAG_CREATE_TYPE;
 			break;
 
 		case T_CreateEnumStmt:
-			tag = "CREATE TYPE";
+			tag = COMMANDTAG_CREATE_TYPE;
 			break;
 
 		case T_CreateRangeStmt:
-			tag = "CREATE TYPE";
+			tag = COMMANDTAG_CREATE_TYPE;
 			break;
 
 		case T_AlterEnumStmt:
-			tag = "ALTER TYPE";
+			tag = COMMANDTAG_ALTER_TYPE;
 			break;
 
 		case T_ViewStmt:
-			tag = "CREATE VIEW";
+			tag = COMMANDTAG_CREATE_VIEW;
 			break;
 
 		case T_CreateFunctionStmt:
 			if (((CreateFunctionStmt *) parsetree)->is_procedure)
-				tag = "CREATE PROCEDURE";
+				tag = COMMANDTAG_CREATE_PROCEDURE;
 			else
-				tag = "CREATE FUNCTION";
+				tag = COMMANDTAG_CREATE_FUNCTION;
 			break;
 
 		case T_IndexStmt:
-			tag = "CREATE INDEX";
+			tag = COMMANDTAG_CREATE_INDEX;
 			break;
 
 		case T_RuleStmt:
-			tag = "CREATE RULE";
+			tag = COMMANDTAG_CREATE_RULE;
 			break;
 
 		case T_CreateSeqStmt:
-			tag = "CREATE SEQUENCE";
+			tag = COMMANDTAG_CREATE_SEQUENCE;
 			break;
 
 		case T_AlterSeqStmt:
-			tag = "ALTER SEQUENCE";
+			tag = COMMANDTAG_ALTER_SEQUENCE;
 			break;
 
 		case T_DoStmt:
-			tag = "DO";
+			tag = COMMANDTAG_DO;
 			break;
 
 		case T_CreatedbStmt:
-			tag = "CREATE DATABASE";
+			tag = COMMANDTAG_CREATE_DATABASE;
 			break;
 
 		case T_AlterDatabaseStmt:
-			tag = "ALTER DATABASE";
+			tag = COMMANDTAG_ALTER_DATABASE;
 			break;
 
 		case T_AlterDatabaseSetStmt:
-			tag = "ALTER DATABASE";
+			tag = COMMANDTAG_ALTER_DATABASE;
 			break;
 
 		case T_DropdbStmt:
-			tag = "DROP DATABASE";
+			tag = COMMANDTAG_DROP_DATABASE;
 			break;
 
 		case T_NotifyStmt:
-			tag = "NOTIFY";
+			tag = COMMANDTAG_NOTIFY;
 			break;
 
 		case T_ListenStmt:
-			tag = "LISTEN";
+			tag = COMMANDTAG_LISTEN;
 			break;
 
 		case T_UnlistenStmt:
-			tag = "UNLISTEN";
+			tag = COMMANDTAG_UNLISTEN;
 			break;
 
 		case T_LoadStmt:
-			tag = "LOAD";
+			tag = COMMANDTAG_LOAD;
 			break;
 
 		case T_CallStmt:
-			tag = "CALL";
+			tag = COMMANDTAG_CALL;
 			break;
 
 		case T_ClusterStmt:
-			tag = "CLUSTER";
+			tag = COMMANDTAG_CLUSTER;
 			break;
 
 		case T_VacuumStmt:
 			if (((VacuumStmt *) parsetree)->is_vacuumcmd)
-				tag = "VACUUM";
+				tag = COMMANDTAG_VACUUM;
 			else
-				tag = "ANALYZE";
+				tag = COMMANDTAG_ANALYZE;
 			break;
 
 		case T_ExplainStmt:
-			tag = "EXPLAIN";
+			tag = COMMANDTAG_EXPLAIN;
 			break;
 
 		case T_CreateTableAsStmt:
@@ -2762,24 +2752,24 @@ CreateCommandTag(Node *parsetree)
 			{
 				case OBJECT_TABLE:
 					if (((CreateTableAsStmt *) parsetree)->is_select_into)
-						tag = "SELECT INTO";
+						tag = COMMANDTAG_SELECT_INTO;
 					else
-						tag = "CREATE TABLE AS";
+						tag = COMMANDTAG_CREATE_TABLE_AS;
 					break;
 				case OBJECT_MATVIEW:
-					tag = "CREATE MATERIALIZED VIEW";
+					tag = COMMANDTAG_CREATE_MATERIALIZED_VIEW;
 					break;
 				default:
-					tag = "???";
+					tag = COMMANDTAG_UNKNOWN;
 			}
 			break;
 
 		case T_RefreshMatViewStmt:
-			tag = "REFRESH MATERIALIZED VIEW";
+			tag = COMMANDTAG_REFRESH_MATERIALIZED_VIEW;
 			break;
 
 		case T_AlterSystemStmt:
-			tag = "ALTER SYSTEM";
+			tag = COMMANDTAG_ALTER_SYSTEM;
 			break;
 
 		case T_VariableSetStmt:
@@ -2789,183 +2779,183 @@ CreateCommandTag(Node *parsetree)
 				case VAR_SET_CURRENT:
 				case VAR_SET_DEFAULT:
 				case VAR_SET_MULTI:
-					tag = "SET";
+					tag = COMMANDTAG_SET;
 					break;
 				case VAR_RESET:
 				case VAR_RESET_ALL:
-					tag = "RESET";
+					tag = COMMANDTAG_RESET;
 					break;
 				default:
-					tag = "???";
+					tag = COMMANDTAG_UNKNOWN;
 			}
 			break;
 
 		case T_VariableShowStmt:
-			tag = "SHOW";
+			tag = COMMANDTAG_SHOW;
 			break;
 
 		case T_DiscardStmt:
 			switch (((DiscardStmt *) parsetree)->target)
 			{
 				case DISCARD_ALL:
-					tag = "DISCARD ALL";
+					tag = COMMANDTAG_DISCARD_ALL;
 					break;
 				case DISCARD_PLANS:
-					tag = "DISCARD PLANS";
+					tag = COMMANDTAG_DISCARD_PLANS;
 					break;
 				case DISCARD_TEMP:
-					tag = "DISCARD TEMP";
+					tag = COMMANDTAG_DISCARD_TEMP;
 					break;
 				case DISCARD_SEQUENCES:
-					tag = "DISCARD SEQUENCES";
+					tag = COMMANDTAG_DISCARD_SEQUENCES;
 					break;
 				default:
-					tag = "???";
+					tag = COMMANDTAG_UNKNOWN;
 			}
 			break;
 
 		case T_CreateTransformStmt:
-			tag = "CREATE TRANSFORM";
+			tag = COMMANDTAG_CREATE_TRANSFORM;
 			break;
 
 		case T_CreateTrigStmt:
-			tag = "CREATE TRIGGER";
+			tag = COMMANDTAG_CREATE_TRIGGER;
 			break;
 
 		case T_CreateEventTrigStmt:
-			tag = "CREATE EVENT TRIGGER";
+			tag = COMMANDTAG_CREATE_EVENT_TRIGGER;
 			break;
 
 		case T_AlterEventTrigStmt:
-			tag = "ALTER EVENT TRIGGER";
+			tag = COMMANDTAG_ALTER_EVENT_TRIGGER;
 			break;
 
 		case T_CreatePLangStmt:
-			tag = "CREATE LANGUAGE";
+			tag = COMMANDTAG_CREATE_LANGUAGE;
 			break;
 
 		case T_CreateRoleStmt:
-			tag = "CREATE ROLE";
+			tag = COMMANDTAG_CREATE_ROLE;
 			break;
 
 		case T_AlterRoleStmt:
-			tag = "ALTER ROLE";
+			tag = COMMANDTAG_ALTER_ROLE;
 			break;
 
 		case T_AlterRoleSetStmt:
-			tag = "ALTER ROLE";
+			tag = COMMANDTAG_ALTER_ROLE;
 			break;
 
 		case T_DropRoleStmt:
-			tag = "DROP ROLE";
+			tag = COMMANDTAG_DROP_ROLE;
 			break;
 
 		case T_DropOwnedStmt:
-			tag = "DROP OWNED";
+			tag = COMMANDTAG_DROP_OWNED;
 			break;
 
 		case T_ReassignOwnedStmt:
-			tag = "REASSIGN OWNED";
+			tag = COMMANDTAG_REASSIGN_OWNED;
 			break;
 
 		case T_LockStmt:
-			tag = "LOCK TABLE";
+			tag = COMMANDTAG_LOCK_TABLE;
 			break;
 
 		case T_ConstraintsSetStmt:
-			tag = "SET CONSTRAINTS";
+			tag = COMMANDTAG_SET_CONSTRAINTS;
 			break;
 
 		case T_CheckPointStmt:
-			tag = "CHECKPOINT";
+			tag = COMMANDTAG_CHECKPOINT;
 			break;
 
 		case T_ReindexStmt:
-			tag = "REINDEX";
+			tag = COMMANDTAG_REINDEX;
 			break;
 
 		case T_CreateConversionStmt:
-			tag = "CREATE CONVERSION";
+			tag = COMMANDTAG_CREATE_CONVERSION;
 			break;
 
 		case T_CreateCastStmt:
-			tag = "CREATE CAST";
+			tag = COMMANDTAG_CREATE_CAST;
 			break;
 
 		case T_CreateOpClassStmt:
-			tag = "CREATE OPERATOR CLASS";
+			tag = COMMANDTAG_CREATE_OPERATOR_CLASS;
 			break;
 
 		case T_CreateOpFamilyStmt:
-			tag = "CREATE OPERATOR FAMILY";
+			tag = COMMANDTAG_CREATE_OPERATOR_FAMILY;
 			break;
 
 		case T_AlterOpFamilyStmt:
-			tag = "ALTER OPERATOR FAMILY";
+			tag = COMMANDTAG_ALTER_OPERATOR_FAMILY;
 			break;
 
 		case T_AlterOperatorStmt:
-			tag = "ALTER OPERATOR";
+			tag = COMMANDTAG_ALTER_OPERATOR;
 			break;
 
 		case T_AlterTSDictionaryStmt:
-			tag = "ALTER TEXT SEARCH DICTIONARY";
+			tag = COMMANDTAG_ALTER_TEXT_SEARCH_DICTIONARY;
 			break;
 
 		case T_AlterTSConfigurationStmt:
-			tag = "ALTER TEXT SEARCH CONFIGURATION";
+			tag = COMMANDTAG_ALTER_TEXT_SEARCH_CONFIGURATION;
 			break;
 
 		case T_CreatePolicyStmt:
-			tag = "CREATE POLICY";
+			tag = COMMANDTAG_CREATE_POLICY;
 			break;
 
 		case T_AlterPolicyStmt:
-			tag = "ALTER POLICY";
+			tag = COMMANDTAG_ALTER_POLICY;
 			break;
 
 		case T_CreateAmStmt:
-			tag = "CREATE ACCESS METHOD";
+			tag = COMMANDTAG_CREATE_ACCESS_METHOD;
 			break;
 
 		case T_CreatePublicationStmt:
-			tag = "CREATE PUBLICATION";
+			tag = COMMANDTAG_CREATE_PUBLICATION;
 			break;
 
 		case T_AlterPublicationStmt:
-			tag = "ALTER PUBLICATION";
+			tag = COMMANDTAG_ALTER_PUBLICATION;
 			break;
 
 		case T_CreateSubscriptionStmt:
-			tag = "CREATE SUBSCRIPTION";
+			tag = COMMANDTAG_CREATE_SUBSCRIPTION;
 			break;
 
 		case T_AlterSubscriptionStmt:
-			tag = "ALTER SUBSCRIPTION";
+			tag = COMMANDTAG_ALTER_SUBSCRIPTION;
 			break;
 
 		case T_DropSubscriptionStmt:
-			tag = "DROP SUBSCRIPTION";
+			tag = COMMANDTAG_DROP_SUBSCRIPTION;
 			break;
 
 		case T_AlterCollationStmt:
-			tag = "ALTER COLLATION";
+			tag = COMMANDTAG_ALTER_COLLATION;
 			break;
 
 		case T_PrepareStmt:
-			tag = "PREPARE";
+			tag = COMMANDTAG_PREPARE;
 			break;
 
 		case T_ExecuteStmt:
-			tag = "EXECUTE";
+			tag = COMMANDTAG_EXECUTE;
 			break;
 
 		case T_CreateStatsStmt:
-			tag = "CREATE STATISTICS";
+			tag = COMMANDTAG_CREATE_STATISTICS;
 			break;
 
 		case T_AlterStatsStmt:
-			tag = "ALTER STATISTICS";
+			tag = COMMANDTAG_ALTER_STATISTICS;
 			break;
 
 		case T_DeallocateStmt:
@@ -2973,9 +2963,9 @@ CreateCommandTag(Node *parsetree)
 				DeallocateStmt *stmt = (DeallocateStmt *) parsetree;
 
 				if (stmt->name == NULL)
-					tag = "DEALLOCATE ALL";
+					tag = COMMANDTAG_DEALLOCATE_ALL;
 				else
-					tag = "DEALLOCATE";
+					tag = COMMANDTAG_DEALLOCATE;
 			}
 			break;
 
@@ -2999,33 +2989,33 @@ CreateCommandTag(Node *parsetree)
 							switch (((PlanRowMark *) linitial(stmt->rowMarks))->strength)
 							{
 								case LCS_FORKEYSHARE:
-									tag = "SELECT FOR KEY SHARE";
+									tag = COMMANDTAG_SELECT_FOR_KEY_SHARE;
 									break;
 								case LCS_FORSHARE:
-									tag = "SELECT FOR SHARE";
+									tag = COMMANDTAG_SELECT_FOR_SHARE;
 									break;
 								case LCS_FORNOKEYUPDATE:
-									tag = "SELECT FOR NO KEY UPDATE";
+									tag = COMMANDTAG_SELECT_FOR_NO_KEY_UPDATE;
 									break;
 								case LCS_FORUPDATE:
-									tag = "SELECT FOR UPDATE";
+									tag = COMMANDTAG_SELECT_FOR_UPDATE;
 									break;
 								default:
-									tag = "SELECT";
+									tag = COMMANDTAG_SELECT;
 									break;
 							}
 						}
 						else
-							tag = "SELECT";
+							tag = COMMANDTAG_SELECT;
 						break;
 					case CMD_UPDATE:
-						tag = "UPDATE";
+						tag = COMMANDTAG_UPDATE;
 						break;
 					case CMD_INSERT:
-						tag = "INSERT";
+						tag = COMMANDTAG_INSERT;
 						break;
 					case CMD_DELETE:
-						tag = "DELETE";
+						tag = COMMANDTAG_DELETE;
 						break;
 					case CMD_UTILITY:
 						tag = CreateCommandTag(stmt->utilityStmt);
@@ -3033,7 +3023,7 @@ CreateCommandTag(Node *parsetree)
 					default:
 						elog(WARNING, "unrecognized commandType: %d",
 							 (int) stmt->commandType);
-						tag = "???";
+						tag = COMMANDTAG_UNKNOWN;
 						break;
 				}
 			}
@@ -3059,33 +3049,33 @@ CreateCommandTag(Node *parsetree)
 							switch (((RowMarkClause *) linitial(stmt->rowMarks))->strength)
 							{
 								case LCS_FORKEYSHARE:
-									tag = "SELECT FOR KEY SHARE";
+									tag = COMMANDTAG_SELECT_FOR_KEY_SHARE;
 									break;
 								case LCS_FORSHARE:
-									tag = "SELECT FOR SHARE";
+									tag = COMMANDTAG_SELECT_FOR_SHARE;
 									break;
 								case LCS_FORNOKEYUPDATE:
-									tag = "SELECT FOR NO KEY UPDATE";
+									tag = COMMANDTAG_SELECT_FOR_NO_KEY_UPDATE;
 									break;
 								case LCS_FORUPDATE:
-									tag = "SELECT FOR UPDATE";
+									tag = COMMANDTAG_SELECT_FOR_UPDATE;
 									break;
 								default:
-									tag = "???";
+									tag = COMMANDTAG_UNKNOWN;
 									break;
 							}
 						}
 						else
-							tag = "SELECT";
+							tag = COMMANDTAG_SELECT;
 						break;
 					case CMD_UPDATE:
-						tag = "UPDATE";
+						tag = COMMANDTAG_UPDATE;
 						break;
 					case CMD_INSERT:
-						tag = "INSERT";
+						tag = COMMANDTAG_INSERT;
 						break;
 					case CMD_DELETE:
-						tag = "DELETE";
+						tag = COMMANDTAG_DELETE;
 						break;
 					case CMD_UTILITY:
 						tag = CreateCommandTag(stmt->utilityStmt);
@@ -3093,7 +3083,7 @@ CreateCommandTag(Node *parsetree)
 					default:
 						elog(WARNING, "unrecognized commandType: %d",
 							 (int) stmt->commandType);
-						tag = "???";
+						tag = COMMANDTAG_UNKNOWN;
 						break;
 				}
 			}
@@ -3102,7 +3092,7 @@ CreateCommandTag(Node *parsetree)
 		default:
 			elog(WARNING, "unrecognized node type: %d",
 				 (int) nodeTag(parsetree));
-			tag = "???";
+			tag = COMMANDTAG_UNKNOWN;
 			break;
 	}
 
diff --git a/src/backend/utils/.gitignore b/src/backend/utils/.gitignore
index 0685556959..7d6dfa45a5 100644
--- a/src/backend/utils/.gitignore
+++ b/src/backend/utils/.gitignore
@@ -1,6 +1,9 @@
+/commandtag_behavior.h
+/commandtag_enum.h
 /fmgrtab.c
 /fmgroids.h
 /fmgrprotos.h
 /fmgr-stamp
+/commandtag-stamp
 /probes.h
 /errcodes.h
diff --git a/src/backend/utils/Makefile b/src/backend/utils/Makefile
index b91028ddfd..3c433cf249 100644
--- a/src/backend/utils/Makefile
+++ b/src/backend/utils/Makefile
@@ -13,7 +13,7 @@ subdir = src/backend/utils
 top_builddir = ../../..
 include $(top_builddir)/src/Makefile.global
 
-OBJS        = fmgrtab.o
+OBJS        = commandtag.o fmgrtab.o
 SUBDIRS     = adt cache error fmgr hash init mb misc mmgr resowner sort time
 
 # location of Catalog.pm
@@ -23,13 +23,13 @@ include $(top_srcdir)/src/backend/common.mk
 
 all: distprep probes.h generated-header-symlinks
 
-distprep: fmgr-stamp errcodes.h
+distprep: commandtag-stamp fmgr-stamp errcodes.h
 
 .PHONY: generated-header-symlinks
 
 generated-header-symlinks: $(top_builddir)/src/include/utils/header-stamp $(top_builddir)/src/include/utils/probes.h
 
-$(SUBDIRS:%=%-recursive): fmgr-stamp errcodes.h
+$(SUBDIRS:%=%-recursive): commandtag-stamp fmgr-stamp errcodes.h
 
 # fmgr-stamp records the last time we ran Gen_fmgrtab.pl.  We don't rely on
 # the timestamps of the individual output files, because the Perl script
@@ -38,6 +38,10 @@ fmgr-stamp: Gen_fmgrtab.pl $(catalogdir)/Catalog.pm $(top_srcdir)/src/include/ca
 	$(PERL) -I $(catalogdir) $< --include-path=$(top_srcdir)/src/include/ $(top_srcdir)/src/include/catalog/pg_proc.dat
 	touch $@
 
+commandtag-stamp: gencommandtag.pl $(top_srcdir)/src/include/utils/commandtag.dat
+	$(PERL) $<
+	touch $@
+
 errcodes.h: $(top_srcdir)/src/backend/utils/errcodes.txt generate-errcodes.pl
 	$(PERL) $(srcdir)/generate-errcodes.pl $< > $@
 
@@ -62,10 +66,10 @@ endif
 # These generated headers must be symlinked into builddir/src/include/,
 # using absolute links for the reasons explained in src/backend/Makefile.
 # We use header-stamp to record that we've done this because the symlinks
-# themselves may appear older than fmgr-stamp.
-$(top_builddir)/src/include/utils/header-stamp: fmgr-stamp errcodes.h
+# themselves may appear older than commandtag-stamp and fmgr-stamp.
+$(top_builddir)/src/include/utils/header-stamp: commandtag-stamp fmgr-stamp errcodes.h
 	prereqdir=`cd '$(dir $<)' >/dev/null && pwd` && \
-	cd '$(dir $@)' && for file in fmgroids.h fmgrprotos.h errcodes.h; do \
+	cd '$(dir $@)' && for file in commandtag_enum.h commandtag_behavior.h fmgroids.h fmgrprotos.h errcodes.h; do \
 	  rm -f $$file && $(LN_S) "$$prereqdir/$$file" . ; \
 	done
 	touch $@
@@ -87,10 +91,10 @@ installdirs:
 uninstall-data:
 	rm -f $(addprefix '$(DESTDIR)$(datadir)'/, errcodes.txt)
 
-# fmgroids.h, fmgrprotos.h, fmgrtab.c, fmgr-stamp, and errcodes.h are in the
+# fmgroids.h, fmgrprotos.h, fmgrtab.c, commandtag-stamp, fmgr-stamp, and errcodes.h are in the
 # distribution tarball, so they are not cleaned here.
 clean:
 	rm -f probes.h
 
 maintainer-clean: clean
-	rm -f fmgroids.h fmgrprotos.h fmgrtab.c fmgr-stamp errcodes.h
+	rm -f commandtag_enum.h commandtag_behavior.h commandtag-stamp fmgroids.h fmgrprotos.h fmgrtab.c fmgr-stamp errcodes.h
diff --git a/src/backend/utils/cache/evtcache.c b/src/backend/utils/cache/evtcache.c
index 1b63048a77..a960d7ca80 100644
--- a/src/backend/utils/cache/evtcache.c
+++ b/src/backend/utils/cache/evtcache.c
@@ -51,7 +51,7 @@ static EventTriggerCacheStateType EventTriggerCacheState = ETCS_NEEDS_REBUILD;
 static void BuildEventTriggerCache(void);
 static void InvalidateEventCacheCallback(Datum arg,
 										 int cacheid, uint32 hashvalue);
-static int	DecodeTextArrayToCString(Datum array, char ***cstringp);
+static void	DecodeTextArrayToBitmapset(Datum array, Bitmapset **bms);
 
 /*
  * Search the event cache by trigger event.
@@ -180,10 +180,7 @@ BuildEventTriggerCache(void)
 		evttags = heap_getattr(tup, Anum_pg_event_trigger_evttags,
 							   RelationGetDescr(rel), &evttags_isnull);
 		if (!evttags_isnull)
-		{
-			item->ntags = DecodeTextArrayToCString(evttags, &item->tag);
-			qsort(item->tag, item->ntags, sizeof(char *), pg_qsort_strcmp);
-		}
+			DecodeTextArrayToBitmapset(evttags, &item->tagset);
 
 		/* Add to cache entry. */
 		entry = hash_search(cache, &event, HASH_ENTER, &found);
@@ -215,18 +212,18 @@ BuildEventTriggerCache(void)
 }
 
 /*
- * Decode text[] to an array of C strings.
+ * Decode text[] to a Bitmapset of CommandTags.
  *
  * We could avoid a bit of overhead here if we were willing to duplicate some
  * of the logic from deconstruct_array, but it doesn't seem worth the code
  * complexity.
  */
-static int
-DecodeTextArrayToCString(Datum array, char ***cstringp)
+static void
+DecodeTextArrayToBitmapset(Datum array, Bitmapset **tagset)
 {
 	ArrayType  *arr = DatumGetArrayTypeP(array);
 	Datum	   *elems;
-	char	  **cstring;
+	Bitmapset  *bms;
 	int			i;
 	int			nelems;
 
@@ -234,13 +231,15 @@ DecodeTextArrayToCString(Datum array, char ***cstringp)
 		elog(ERROR, "expected 1-D text array");
 	deconstruct_array(arr, TEXTOID, -1, false, 'i', &elems, NULL, &nelems);
 
-	cstring = palloc(nelems * sizeof(char *));
-	for (i = 0; i < nelems; ++i)
-		cstring[i] = TextDatumGetCString(elems[i]);
+	for (bms = NULL, i = 0; i < nelems; ++i)
+	{
+		char *str = TextDatumGetCString(elems[i]);
+		bms = bms_add_member(bms, GetCommandTagEnum(str));
+		pfree(str);
+	}
 
 	pfree(elems);
-	*cstringp = cstring;
-	return nelems;
+	*tagset = bms;
 }
 
 /*
diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c
index c47be0ba4c..53401bd4e9 100644
--- a/src/backend/utils/cache/plancache.c
+++ b/src/backend/utils/cache/plancache.c
@@ -163,7 +163,7 @@ InitPlanCache(void)
 CachedPlanSource *
 CreateCachedPlan(RawStmt *raw_parse_tree,
 				 const char *query_string,
-				 const char *commandTag)
+				 CommandTag commandTag)
 {
 	CachedPlanSource *plansource;
 	MemoryContext source_context;
@@ -246,7 +246,7 @@ CreateCachedPlan(RawStmt *raw_parse_tree,
 CachedPlanSource *
 CreateOneShotCachedPlan(RawStmt *raw_parse_tree,
 						const char *query_string,
-						const char *commandTag)
+						CommandTag commandTag)
 {
 	CachedPlanSource *plansource;
 
diff --git a/src/backend/utils/commandtag.c b/src/backend/utils/commandtag.c
new file mode 100644
index 0000000000..c629eca608
--- /dev/null
+++ b/src/backend/utils/commandtag.c
@@ -0,0 +1,119 @@
+/*-------------------------------------------------------------------------
+ *
+ * commandtag.c
+ *	  Data and routines for commandtag names and enumeration.
+ *
+ * Portions Copyright (c) 1996-2020, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *	  src/backend/utils/commandtag.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "c.h"
+#include "miscadmin.h"
+#include "utils/commandtag.h"
+#include "utils/commandtag_enum.h"
+#include "utils/commandtag_behavior.h"
+
+static const unsigned int tag_behavior_length = lengthof(tag_behavior);
+
+static inline bool
+PG_VALID_COMMANDTAG(CommandTag commandTag)
+{
+	return (commandTag >= FIRST_COMMANDTAG &&
+			commandTag <= LAST_COMMANDTAG);
+}
+
+void
+InitializeQueryCompletion(QueryCompletion *qc)
+{
+	qc->commandTag = COMMANDTAG_UNKNOWN;
+	qc->nprocessed = 0;
+}
+
+const char *
+GetCommandTagName(CommandTag commandTag)
+{
+	Assert(PG_VALID_COMMANDTAG(commandTag));
+	return tag_behavior[commandTag].name;
+}
+
+bool
+command_tag_display_last_oid(CommandTag commandTag)
+{
+	Assert(PG_VALID_COMMANDTAG(commandTag));
+	return tag_behavior[commandTag].display_last_oid;
+}
+
+bool
+command_tag_display_rowcount(CommandTag commandTag)
+{
+	Assert(PG_VALID_COMMANDTAG(commandTag));
+	return tag_behavior[commandTag].display_rowcount;
+}
+
+bool
+command_tag_event_trigger_ok(CommandTag commandTag)
+{
+	Assert(PG_VALID_COMMANDTAG(commandTag));
+	return tag_behavior[commandTag].event_trigger_ok;
+}
+
+bool
+command_tag_table_rewrite_ok(CommandTag commandTag)
+{
+	Assert(PG_VALID_COMMANDTAG(commandTag));
+	return tag_behavior[commandTag].table_rewrite_ok;
+}
+
+/*
+ * Search CommandTag by name
+ *
+ * Returns CommandTag, or COMMANDTAG_UNKNOWN if not recognized
+ */
+CommandTag
+GetCommandTagEnum(const char *commandname)
+{
+	const CommandTagBehavior *base, *last, *position;
+	int		 result;
+
+	if (commandname == NULL || *commandname == '\0')
+		return COMMANDTAG_UNKNOWN;
+
+	base = tag_behavior;
+	last = tag_behavior + tag_behavior_length - 1;
+	while (last >= base)
+	{
+		position = base + ((last - base) >> 1);
+		result = pg_strcasecmp(commandname, position->name);
+		if (result == 0)
+			return (CommandTag) (position - tag_behavior);
+		else if (result < 0)
+			last = position - 1;
+		else
+			base = position + 1;
+	}
+	return COMMANDTAG_UNKNOWN;
+}
+
+void
+PreventCommandTagIfReadOnly(CommandTag commandTag)
+{
+	PreventCommandIfReadOnly(GetCommandTagName(commandTag));
+}
+
+void
+PreventCommandTagIfParallelMode(CommandTag commandTag)
+{
+	PreventCommandIfParallelMode(GetCommandTagName(commandTag));
+}
+
+void
+PreventCommandTagDuringRecovery(CommandTag commandTag)
+{
+	PreventCommandDuringRecovery(GetCommandTagName(commandTag));
+}
diff --git a/src/backend/utils/gencommandtag.pl b/src/backend/utils/gencommandtag.pl
new file mode 100755
index 0000000000..434f165305
--- /dev/null
+++ b/src/backend/utils/gencommandtag.pl
@@ -0,0 +1,266 @@
+#!/usr/bin/perl -w
+#----------------------------------------------------------------------
+#
+# gencommandtag.pl
+#    Perl script that generates the commandtag data headers from the
+#    specially formatted commandtag.dat data file.
+#
+# Portions Copyright (c) 1996-2020, PostgreSQL Global Development Group
+# Portions Copyright (c) 1994, Regents of the University of California
+#
+# src/backend/utils/gencommandtag.pl
+#
+#----------------------------------------------------------------------
+
+use strict;
+use warnings;
+use Getopt::Long;
+
+use File::Basename;
+use File::Spec;
+BEGIN { use lib File::Spec->rel2abs(dirname(__FILE__)); }
+
+use FindBin;
+use lib "$FindBin::RealBin/../catalog/";
+use DataFile;
+
+my $outputdir = '.';
+my $inputfile = '../../include/utils/commandtag.dat';
+
+GetOptions(
+	'inputfile:s'		=> \$inputfile,
+	'outputdir:s'		=> \$outputdir) || usage();
+
+# Sanity check arguments.
+die "input file $inputfile does not exist" unless(-f $inputfile);
+die "output directory $outputdir does not exist" unless(-d $outputdir);
+
+# Make sure paths end with a slash.
+$outputdir .= '/' if (substr($outputdir, -1) ne '/');
+
+my $enumfile = join('', $outputdir, "commandtag_enum.h");
+my $arrayfile = join('', $outputdir, "commandtag_behavior.h");
+
+# Vivify the input data, but be paranoid about eval'ing arbitrary code
+my $data = DataFile::ParseData($inputfile);
+sanity_check_data($data);
+
+# Sort the commandtags based on their stringified representation,
+# not their labels.
+my @sorted = sort { $a->{tagname} cmp $b->{tagname} } @$data;
+
+# Generate all output internally before outputting anything, to avoid
+# partially overwriting generated files under error conditions
+my (@enum_chunks, @behavior_chunks);
+push(@enum_chunks,
+	 boilerplate_header("commandtag_enum.h",
+						"Enumeration of all PostgreSQL command tags.",
+						"src/include/utils/commandtag_enum.h",
+						"COMMANDTAG_ENUM_H"));
+
+push (@enum_chunks, qq(
+typedef enum CommandTag
+{
+#define FIRST_COMMANDTAG COMMANDTAG_$sorted[0]->{taglabel})
+);
+
+push(@behavior_chunks,
+	 boilerplate_header("commandtag_behavior.h",
+						"Array of records defining the behavior of each command tag.",
+						"src/include/utils/commandtag_behavior.h",
+						"COMMANDTAG_BEHAVIOR_H"));
+push (@behavior_chunks, q(
+const CommandTagBehavior tag_behavior[] = {)
+);
+
+for(my $i = 0; $i < scalar(@sorted); $i++)
+{
+	my $rec = $sorted[$i];
+	my $comma = ($i < $#sorted) ? "," : "";
+	my $enumrow = sprintf("\tCOMMANDTAG_%s%s", $rec->{taglabel}, $comma);
+	push(@enum_chunks, $enumrow);
+
+	my $behaviorrow = sprintf("\t[COMMANDTAG_%s] = {", $rec->{taglabel});
+	$behaviorrow .= sprintf("\n\t\t.name = \"%s\"", $rec->{tagname});
+	$behaviorrow .= ",\n\t\t.event_trigger_ok = true" if ($rec->{event_trigger_ok});
+	$behaviorrow .= ",\n\t\t.table_rewrite_ok = true" if ($rec->{table_rewrite_ok});
+	$behaviorrow .= ",\n\t\t.display_rowcount = true" if ($rec->{display_rowcount});
+	$behaviorrow .= ",\n\t\t.display_last_oid = true" if ($rec->{display_last_oid});
+	$behaviorrow .= "\n\t}$comma";
+	push (@behavior_chunks, $behaviorrow);
+}
+
+push(@enum_chunks,
+qq(#define LAST_COMMANDTAG COMMANDTAG_$sorted[$#sorted]->{taglabel}
+}			CommandTag;),
+	boilerplate_footer("COMMANDTAG_ENUM_H")
+	);
+
+push(@behavior_chunks,
+q(};
+StaticAssertDecl(FIRST_COMMANDTAG == (CommandTag)0, "first command tag");
+StaticAssertDecl(lengthof(tag_behavior) == (LAST_COMMANDTAG + 1),
+				 "array length mismatch");),
+	boilerplate_footer("COMMANDTAG_BEHAVIOR_H")
+	);
+
+my $enum_out = IO::File->new(">$enumfile")
+	or die "Cannot write $enumfile: $!";
+$enum_out->print(join("\n", @enum_chunks));
+$enum_out->close();
+
+my $array_out = IO::File->new(">$arrayfile")
+	or die "Cannot write $arrayfile: $!";
+$array_out->print(join("\n", @behavior_chunks));
+$array_out->close();
+
+# Check that the records from the data file look reasonable.
+#
+# Some of the checks here cannot fail, because they check things that were
+# already checked before eval'ing the inputfile data.  We check anyway, for
+# future-proofing and because the prior checks were against the stringified
+# form of the data and these checks are against the perl data structures that
+# we revivified, and the possibility of subtle corner cases that entails.
+#
+sub sanity_check_data
+{
+	my ($data) = @_;
+
+	# Verify that at the outermost layer the data file is an array reference
+	dataerror("Malformed input data: expected an array reference",
+			  "The input file should start with a '[' and end with a ']'")
+		unless defined $data and ref($data) and ref($data) =~ m/ARRAY/;
+
+	# Verify that the data array contains only hash references
+	dataerror("Malformed input data: expected an array of hash references",
+			  "The input file array should contain only records in perl hash " .
+			  "reference '{...}' format")
+		if (grep { !ref($_) || ref($_) !~ m/HASH/ } @$data);
+
+	my @required_keys = qw(tagname taglabel);
+	my @flag_keys = qw(event_trigger_ok table_rewrite_ok display_rowcount
+					   display_last_oid);
+	my @expected_keys = (@required_keys, @flag_keys);
+	my %expected_keys = map { $_ => 1 } @expected_keys;
+
+	# Check that each record in the data file contains the required
+	# fields.  If we have only one required field (name or label) but
+	# not the other, we can use the one we have in the error message
+	# to help direct the user to the record which needs correcting.
+	foreach my $href (@$data)
+	{
+		my $preamble = "Malformed input data:";
+		if (defined $href->{taglabel} and length $href->{taglabel})
+		{
+			$preamble .= " Record for commandtag label '$href->{taglabel}':";
+		}
+		elsif (defined $href->{tagname} and length $href->{tagname})
+		{
+			$preamble .= " Record for commandtag name '$href->{tagname}':";
+		}
+
+		foreach my $key (@required_keys)
+		{
+			dataerror("$preamble missing '$key'",
+					  "Each record in the data file should fields for @required_keys")
+				unless(exists $href->{$key});
+			dataerror("$preamble trivial value for '$key'")
+				unless(defined $href->{$key} and length $href->{$key});
+			dataerror("$preamble value of '$key' not uppercase: $href->{$key}")
+				unless($href->{$key} eq uc($href->{$key}));
+		}
+
+		foreach my $key (keys %$href)
+		{
+			dataerror("$preamble non-printable value for '$key'",
+					  "Do not embed non-printable characters such as tabs, form-feeds or newlines")
+				if (grep { ord($_) < 0x20 } split(//, $href->{$key}));
+			dataerror("$preamble non-ASCII value for '$key'",
+					  "Only printable 7-bit ASCII characters are allowed as commandtag data")
+				if (grep { ord($_) > 0x7F } split(//, $href->{$key}));
+			dataerror("$preamble unrecognized field '$key'",
+					  "Recognized keys are @expected_keys")
+				unless $expected_keys{$key};
+		}
+
+		foreach my $flag (grep { exists $href->{$_} } @flag_keys)
+		{
+			my $val = $href->{$flag};
+			dataerror("$preamble empty or undefined value for '$flag'",
+					  "Fields with no value should simply be omitted")
+				unless defined $val and length $val;
+			dataerror("$preamble false value need not be specified for '$flag'",
+					  "Fields with false values should simply be omitted")
+				if ($val =~ m/^(?:false|f|0|off)$/i);
+			dataerror("$preamble unrecognized value for '$flag': '$val'")
+				unless ($val =~ m/^(?:true|t|1|on)$/i);
+		}
+	}
+}
+
+sub boilerplate_header
+{
+	my ($filename, $filedesc, $repo_path, $header_guard) = @_;
+	my $year = (localtime(time))[5] + 1900;
+	return
+"/*-------------------------------------------------------------------------
+ *
+ * $filename
+ *	  $filedesc
+ *
+ * Portions Copyright (c) 1996-$year, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * NOTES
+ *  ******************************
+ *  *** DO NOT EDIT THIS FILE! ***
+ *  ******************************
+ *
+ * It has been generated by src/backend/utils/gencommandtag.pl
+ *
+ * IDENTIFICATION
+ *	  $repo_path
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef $header_guard
+#define $header_guard";
+}
+
+sub boilerplate_footer
+{
+	my ($header_guard) = @_;
+	return "#endif				/* $header_guard */";
+}
+
+sub dataerror
+{
+	my ($errmsg, $errhint) = @_;
+	print STDERR "Error encountered while verifying $inputfile:\n";
+	print STDERR $errmsg, "\n";
+	if (defined $errhint)
+	{
+		print STDERR "HINT: ", $errhint, "\n";
+	}
+	print STDERR "Please fix $inputfile\n";
+	exit 1;
+}
+
+sub usage
+{
+	die <<EOM;
+Usage: perl gencommandtag.pl [--inputfile <file.dat>] [--outputdir <path>]
+
+Options:
+    --inputfile         Optional: Input commandtag data file
+                        (defaults to ../../include/utils/commandtag.dat)
+    --outputdir         Optional: Output directory for generated headers
+                        (defaults to current working directory)
+
+gencommandtag.pl generates the commandtag_enum.h and commandtag_behavior.h
+files from the specially formatted commandtag.dat data file.
+
+Report bugs to <pgsql-bugs\@lists.postgresql.org>.
+EOM
+}
diff --git a/src/backend/utils/mmgr/portalmem.c b/src/backend/utils/mmgr/portalmem.c
index b675575c31..00e8065342 100644
--- a/src/backend/utils/mmgr/portalmem.c
+++ b/src/backend/utils/mmgr/portalmem.c
@@ -281,7 +281,7 @@ void
 PortalDefineQuery(Portal portal,
 				  const char *prepStmtName,
 				  const char *sourceText,
-				  const char *commandTag,
+				  CommandTag commandTag,
 				  List *stmts,
 				  CachedPlan *cplan)
 {
@@ -289,10 +289,12 @@ PortalDefineQuery(Portal portal,
 	AssertState(portal->status == PORTAL_NEW);
 
 	AssertArg(sourceText != NULL);
-	AssertArg(commandTag != NULL || stmts == NIL);
+	AssertArg(commandTag != COMMANDTAG_UNKNOWN || stmts == NIL);
 
 	portal->prepStmtName = prepStmtName;
 	portal->sourceText = sourceText;
+	portal->qc.commandTag = commandTag;
+	portal->qc.nprocessed = 0;
 	portal->commandTag = commandTag;
 	portal->stmts = stmts;
 	portal->cplan = cplan;
diff --git a/src/include/Makefile b/src/include/Makefile
index c557375ae3..fadbd737c9 100644
--- a/src/include/Makefile
+++ b/src/include/Makefile
@@ -44,6 +44,8 @@ install: all installdirs
 	$(INSTALL_DATA) pg_config.h     '$(DESTDIR)$(includedir_server)'
 	$(INSTALL_DATA) pg_config_ext.h '$(DESTDIR)$(includedir_server)'
 	$(INSTALL_DATA) pg_config_os.h  '$(DESTDIR)$(includedir_server)'
+	$(INSTALL_DATA) utils/commandtag_enum.h '$(DESTDIR)$(includedir_server)/utils'
+	$(INSTALL_DATA) utils/commandtag_behavior.h '$(DESTDIR)$(includedir_server)/utils'
 	$(INSTALL_DATA) utils/errcodes.h '$(DESTDIR)$(includedir_server)/utils'
 	$(INSTALL_DATA) utils/fmgroids.h '$(DESTDIR)$(includedir_server)/utils'
 	$(INSTALL_DATA) utils/fmgrprotos.h '$(DESTDIR)$(includedir_server)/utils'
@@ -77,7 +79,7 @@ uninstall:
 
 
 clean:
-	rm -f utils/fmgroids.h utils/fmgrprotos.h utils/errcodes.h utils/header-stamp
+	rm -f utils/commandtag_enum.h utils/commandtag_behavior.h utils/fmgroids.h utils/fmgrprotos.h utils/errcodes.h utils/header-stamp
 	rm -f parser/gram.h storage/lwlocknames.h utils/probes.h
 	rm -f catalog/schemapg.h catalog/pg_*_d.h catalog/header-stamp
 
diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h
index 7743851a38..5615b5ecac 100644
--- a/src/include/commands/createas.h
+++ b/src/include/commands/createas.h
@@ -22,7 +22,8 @@
 
 
 extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt,
-									   ParamListInfo params, QueryEnvironment *queryEnv, char *completionTag);
+									   ParamListInfo params, QueryEnvironment *queryEnv,
+									   QueryCompletion *qc);
 
 extern int	GetIntoRelEFlags(IntoClause *intoClause);
 
diff --git a/src/include/commands/event_trigger.h b/src/include/commands/event_trigger.h
index faa2958b89..5123c4b850 100644
--- a/src/include/commands/event_trigger.h
+++ b/src/include/commands/event_trigger.h
@@ -25,7 +25,7 @@ typedef struct EventTriggerData
 	NodeTag		type;
 	const char *event;			/* event name */
 	Node	   *parsetree;		/* parse tree */
-	const char *tag;			/* command tag */
+	CommandTag	tag;
 } EventTriggerData;
 
 #define AT_REWRITE_ALTER_PERSISTENCE	0x01
diff --git a/src/include/commands/matview.h b/src/include/commands/matview.h
index 6bdb7ca258..3ea4f5c80b 100644
--- a/src/include/commands/matview.h
+++ b/src/include/commands/matview.h
@@ -24,7 +24,7 @@
 extern void SetMatViewPopulatedState(Relation relation, bool newstate);
 
 extern ObjectAddress ExecRefreshMatView(RefreshMatViewStmt *stmt, const char *queryString,
-										ParamListInfo params, char *completionTag);
+										ParamListInfo params, QueryCompletion *qc);
 
 extern DestReceiver *CreateTransientRelDestReceiver(Oid oid);
 
diff --git a/src/include/commands/portalcmds.h b/src/include/commands/portalcmds.h
index 4ecc1a2ecd..41bb7c395b 100644
--- a/src/include/commands/portalcmds.h
+++ b/src/include/commands/portalcmds.h
@@ -22,8 +22,7 @@
 extern void PerformCursorOpen(ParseState *pstate, DeclareCursorStmt *cstmt, ParamListInfo params,
 							  bool isTopLevel);
 
-extern void PerformPortalFetch(FetchStmt *stmt, DestReceiver *dest,
-							   char *completionTag);
+extern void PerformPortalFetch(FetchStmt *stmt, DestReceiver *dest, QueryCompletion *qc);
 
 extern void PerformPortalClose(const char *name);
 
diff --git a/src/include/commands/prepare.h b/src/include/commands/prepare.h
index a0509e1f33..4fcf2406c1 100644
--- a/src/include/commands/prepare.h
+++ b/src/include/commands/prepare.h
@@ -40,7 +40,7 @@ extern void PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 extern void ExecuteQuery(ParseState *pstate,
 						 ExecuteStmt *stmt, IntoClause *intoClause,
 						 ParamListInfo params,
-						 DestReceiver *dest, char *completionTag);
+						 DestReceiver *dest, QueryCompletion *qc);
 extern void DeallocateQuery(DeallocateStmt *stmt);
 extern void ExplainExecuteQuery(ExecuteStmt *execstmt, IntoClause *into,
 								ExplainState *es, const char *queryString,
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index da0706add5..2c3ae6b603 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -27,6 +27,7 @@
 #include "nodes/primnodes.h"
 #include "nodes/value.h"
 #include "partitioning/partdefs.h"
+#include "utils/commandtag.h"
 
 
 typedef enum OverridingKind
diff --git a/src/include/tcop/dest.h b/src/include/tcop/dest.h
index 35bce731a1..dd699b0993 100644
--- a/src/include/tcop/dest.h
+++ b/src/include/tcop/dest.h
@@ -68,7 +68,7 @@
 #define DEST_H
 
 #include "executor/tuptable.h"
-
+#include "utils/commandtag.h"
 
 /* buffer size to use for command completion tags */
 #define COMPLETION_TAG_BUFSIZE	64
@@ -134,9 +134,10 @@ extern PGDLLIMPORT DestReceiver *None_Receiver; /* permanent receiver for
 
 /* The primary destination management functions */
 
-extern void BeginCommand(const char *commandTag, CommandDest dest);
+extern void BeginCommand(CommandTag commandTag, CommandDest dest);
 extern DestReceiver *CreateDestReceiver(CommandDest dest);
-extern void EndCommand(const char *commandTag, CommandDest dest);
+extern void EndCommand(const QueryCompletion *qc, CommandDest dest,
+					   bool force_undecorated_output);
 
 /* Additional functions that go with destination management, more or less. */
 
diff --git a/src/include/tcop/pquery.h b/src/include/tcop/pquery.h
index 4ad6324e2d..437642cc72 100644
--- a/src/include/tcop/pquery.h
+++ b/src/include/tcop/pquery.h
@@ -35,7 +35,7 @@ extern void PortalSetResultFormat(Portal portal, int nFormats,
 
 extern bool PortalRun(Portal portal, long count, bool isTopLevel,
 					  bool run_once, DestReceiver *dest, DestReceiver *altdest,
-					  char *completionTag);
+					  QueryCompletion *qc);
 
 extern uint64 PortalRunFetch(Portal portal,
 							 FetchDirection fdirection,
diff --git a/src/include/tcop/utility.h b/src/include/tcop/utility.h
index a551e08cb8..8781ac7d38 100644
--- a/src/include/tcop/utility.h
+++ b/src/include/tcop/utility.h
@@ -15,6 +15,7 @@
 #define UTILITY_H
 
 #include "tcop/tcopprot.h"
+#include "utils/commandtag.h"
 
 typedef enum
 {
@@ -71,17 +72,17 @@ typedef void (*ProcessUtility_hook_type) (PlannedStmt *pstmt,
 										  const char *queryString, ProcessUtilityContext context,
 										  ParamListInfo params,
 										  QueryEnvironment *queryEnv,
-										  DestReceiver *dest, char *completionTag);
+										  DestReceiver *dest, QueryCompletion *qc);
 extern PGDLLIMPORT ProcessUtility_hook_type ProcessUtility_hook;
 
 extern void ProcessUtility(PlannedStmt *pstmt, const char *queryString,
 						   ProcessUtilityContext context, ParamListInfo params,
 						   QueryEnvironment *queryEnv,
-						   DestReceiver *dest, char *completionTag);
+						   DestReceiver *dest, QueryCompletion *qc);
 extern void standard_ProcessUtility(PlannedStmt *pstmt, const char *queryString,
 									ProcessUtilityContext context, ParamListInfo params,
 									QueryEnvironment *queryEnv,
-									DestReceiver *dest, char *completionTag);
+									DestReceiver *dest, QueryCompletion *qc);
 
 extern void ProcessUtilityForAlterTable(Node *stmt,
 										AlterTableUtilityContext *context);
@@ -92,7 +93,13 @@ extern TupleDesc UtilityTupleDescriptor(Node *parsetree);
 
 extern Query *UtilityContainsQuery(Node *parsetree);
 
-extern const char *CreateCommandTag(Node *parsetree);
+extern CommandTag CreateCommandTag(Node *parsetree);
+
+static inline const char *
+CreateCommandName(Node *parsetree)
+{
+	return GetCommandTagName(CreateCommandTag(parsetree));
+}
 
 extern LogStmtLevel GetCommandLogLevel(Node *parsetree);
 
diff --git a/src/include/utils/.gitignore b/src/include/utils/.gitignore
index 05cfa7a8d6..9bc600aacc 100644
--- a/src/include/utils/.gitignore
+++ b/src/include/utils/.gitignore
@@ -1,3 +1,5 @@
+/commandtag_behavior.h
+/commandtag_enum.h
 /fmgroids.h
 /fmgrprotos.h
 /probes.h
diff --git a/src/include/utils/commandtag.dat b/src/include/utils/commandtag.dat
new file mode 100644
index 0000000000..a79ee50e00
--- /dev/null
+++ b/src/include/utils/commandtag.dat
@@ -0,0 +1,938 @@
+#----------------------------------------------------------------------
+#
+# commandtag.dat
+#    Data file from which commandtag_enum.h and commandtag_behavior.h
+#    are generated
+#
+# Portions Copyright (c) 1996-2020, PostgreSQL Global Development Group
+# Portions Copyright (c) 1994, Regents of the University of California
+#
+# src/backend/utils/commandtag.dat
+#
+#----------------------------------------------------------------------
+
+# gencommandtag.pl will sort these records, so there is no requirement
+# that they be correctly sorted here, but by convention, we keep these
+# in sorted order by tagname.
+#
+# The 'taglabel' field is typically just a repeat of the tagname but
+# with spaces converted to underscores.  It serves as the label within
+# the generated CommandTag enum.  For weirder names, like ???, the
+# label does not match, for obvious reasons.
+#
+# The following boolean fields specify whether event trigger support
+# exists for the given command tag:
+#
+#	event_trigger_ok
+#	table_rewrite_ok
+#
+# And these two boolean fields specify the formatting that EndCommand
+# should send back to the client for the command tag:
+#
+#	display_rowcount
+#	display_last_oid
+#
+# For these four boolean fields, the default is false.  Only specify
+# a value for records where they are true.
+[
+{
+  taglabel => 'UNKNOWN',
+  tagname => '???',
+},
+{
+  taglabel => 'ALTER_ACCESS_METHOD',
+  tagname => 'ALTER ACCESS METHOD',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'ALTER_AGGREGATE',
+  tagname => 'ALTER AGGREGATE',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'ALTER_CAST',
+  tagname => 'ALTER CAST',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'ALTER_COLLATION',
+  tagname => 'ALTER COLLATION',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'ALTER_CONSTRAINT',
+  tagname => 'ALTER CONSTRAINT',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'ALTER_CONVERSION',
+  tagname => 'ALTER CONVERSION',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'ALTER_DATABASE',
+  tagname => 'ALTER DATABASE',
+},
+{
+  taglabel => 'ALTER_DEFAULT_PRIVILEGES',
+  tagname => 'ALTER DEFAULT PRIVILEGES',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'ALTER_DOMAIN',
+  tagname => 'ALTER DOMAIN',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'ALTER_EVENT_TRIGGER',
+  tagname => 'ALTER EVENT TRIGGER',
+},
+{
+  taglabel => 'ALTER_EXTENSION',
+  tagname => 'ALTER EXTENSION',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'ALTER_FOREIGN_DATA_WRAPPER',
+  tagname => 'ALTER FOREIGN DATA WRAPPER',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'ALTER_FOREIGN_TABLE',
+  tagname => 'ALTER FOREIGN TABLE',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'ALTER_FUNCTION',
+  tagname => 'ALTER FUNCTION',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'ALTER_INDEX',
+  tagname => 'ALTER INDEX',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'ALTER_LANGUAGE',
+  tagname => 'ALTER LANGUAGE',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'ALTER_LARGE_OBJECT',
+  tagname => 'ALTER LARGE OBJECT',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'ALTER_MATERIALIZED_VIEW',
+  tagname => 'ALTER MATERIALIZED VIEW',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'ALTER_OPERATOR',
+  tagname => 'ALTER OPERATOR',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'ALTER_OPERATOR_CLASS',
+  tagname => 'ALTER OPERATOR CLASS',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'ALTER_OPERATOR_FAMILY',
+  tagname => 'ALTER OPERATOR FAMILY',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'ALTER_POLICY',
+  tagname => 'ALTER POLICY',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'ALTER_PROCEDURE',
+  tagname => 'ALTER PROCEDURE',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'ALTER_PUBLICATION',
+  tagname => 'ALTER PUBLICATION',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'ALTER_ROLE',
+  tagname => 'ALTER ROLE',
+},
+{
+  taglabel => 'ALTER_ROUTINE',
+  tagname => 'ALTER ROUTINE',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'ALTER_RULE',
+  tagname => 'ALTER RULE',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'ALTER_SCHEMA',
+  tagname => 'ALTER SCHEMA',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'ALTER_SEQUENCE',
+  tagname => 'ALTER SEQUENCE',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'ALTER_SERVER',
+  tagname => 'ALTER SERVER',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'ALTER_STATISTICS',
+  tagname => 'ALTER STATISTICS',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'ALTER_SUBSCRIPTION',
+  tagname => 'ALTER SUBSCRIPTION',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'ALTER_SYSTEM',
+  tagname => 'ALTER SYSTEM',
+},
+{
+  taglabel => 'ALTER_TABLE',
+  tagname => 'ALTER TABLE',
+  event_trigger_ok => 'true',
+  table_rewrite_ok => 'true',
+},
+{
+  taglabel => 'ALTER_TABLESPACE',
+  tagname => 'ALTER TABLESPACE',
+},
+{
+  taglabel => 'ALTER_TEXT_SEARCH_CONFIGURATION',
+  tagname => 'ALTER TEXT SEARCH CONFIGURATION',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'ALTER_TEXT_SEARCH_DICTIONARY',
+  tagname => 'ALTER TEXT SEARCH DICTIONARY',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'ALTER_TEXT_SEARCH_PARSER',
+  tagname => 'ALTER TEXT SEARCH PARSER',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'ALTER_TEXT_SEARCH_TEMPLATE',
+  tagname => 'ALTER TEXT SEARCH TEMPLATE',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'ALTER_TRANSFORM',
+  tagname => 'ALTER TRANSFORM',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'ALTER_TRIGGER',
+  tagname => 'ALTER TRIGGER',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'ALTER_TYPE',
+  tagname => 'ALTER TYPE',
+  event_trigger_ok => 'true',
+  table_rewrite_ok => 'true',
+},
+{
+  taglabel => 'ALTER_USER_MAPPING',
+  tagname => 'ALTER USER MAPPING',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'ALTER_VIEW',
+  tagname => 'ALTER VIEW',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'ANALYZE',
+  tagname => 'ANALYZE',
+},
+{
+  taglabel => 'BEGIN',
+  tagname => 'BEGIN',
+},
+{
+  taglabel => 'CALL',
+  tagname => 'CALL',
+},
+{
+  taglabel => 'CHECKPOINT',
+  tagname => 'CHECKPOINT',
+},
+{
+  taglabel => 'CLOSE',
+  tagname => 'CLOSE',
+},
+{
+  taglabel => 'CLOSE_CURSOR',
+  tagname => 'CLOSE CURSOR',
+},
+{
+  taglabel => 'CLOSE_CURSOR_ALL',
+  tagname => 'CLOSE CURSOR ALL',
+},
+{
+  taglabel => 'CLUSTER',
+  tagname => 'CLUSTER',
+},
+{
+  taglabel => 'COMMENT',
+  tagname => 'COMMENT',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'COMMIT',
+  tagname => 'COMMIT',
+},
+{
+  taglabel => 'COMMIT_PREPARED',
+  tagname => 'COMMIT PREPARED',
+},
+{
+  taglabel => 'COPY',
+  tagname => 'COPY',
+  display_rowcount => 'true',
+},
+{
+  taglabel => 'COPY_FROM',
+  tagname => 'COPY FROM',
+},
+{
+  taglabel => 'CREATE_ACCESS_METHOD',
+  tagname => 'CREATE ACCESS METHOD',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'CREATE_AGGREGATE',
+  tagname => 'CREATE AGGREGATE',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'CREATE_CAST',
+  tagname => 'CREATE CAST',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'CREATE_COLLATION',
+  tagname => 'CREATE COLLATION',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'CREATE_CONSTRAINT',
+  tagname => 'CREATE CONSTRAINT',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'CREATE_CONVERSION',
+  tagname => 'CREATE CONVERSION',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'CREATE_DATABASE',
+  tagname => 'CREATE DATABASE',
+},
+{
+  taglabel => 'CREATE_DOMAIN',
+  tagname => 'CREATE DOMAIN',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'CREATE_EVENT_TRIGGER',
+  tagname => 'CREATE EVENT TRIGGER',
+},
+{
+  taglabel => 'CREATE_EXTENSION',
+  tagname => 'CREATE EXTENSION',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'CREATE_FOREIGN_DATA_WRAPPER',
+  tagname => 'CREATE FOREIGN DATA WRAPPER',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'CREATE_FOREIGN_TABLE',
+  tagname => 'CREATE FOREIGN TABLE',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'CREATE_FUNCTION',
+  tagname => 'CREATE FUNCTION',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'CREATE_INDEX',
+  tagname => 'CREATE INDEX',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'CREATE_LANGUAGE',
+  tagname => 'CREATE LANGUAGE',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'CREATE_MATERIALIZED_VIEW',
+  tagname => 'CREATE MATERIALIZED VIEW',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'CREATE_OPERATOR',
+  tagname => 'CREATE OPERATOR',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'CREATE_OPERATOR_CLASS',
+  tagname => 'CREATE OPERATOR CLASS',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'CREATE_OPERATOR_FAMILY',
+  tagname => 'CREATE OPERATOR FAMILY',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'CREATE_POLICY',
+  tagname => 'CREATE POLICY',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'CREATE_PROCEDURE',
+  tagname => 'CREATE PROCEDURE',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'CREATE_PUBLICATION',
+  tagname => 'CREATE PUBLICATION',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'CREATE_ROLE',
+  tagname => 'CREATE ROLE',
+},
+{
+  taglabel => 'CREATE_ROUTINE',
+  tagname => 'CREATE ROUTINE',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'CREATE_RULE',
+  tagname => 'CREATE RULE',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'CREATE_SCHEMA',
+  tagname => 'CREATE SCHEMA',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'CREATE_SEQUENCE',
+  tagname => 'CREATE SEQUENCE',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'CREATE_SERVER',
+  tagname => 'CREATE SERVER',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'CREATE_STATISTICS',
+  tagname => 'CREATE STATISTICS',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'CREATE_SUBSCRIPTION',
+  tagname => 'CREATE SUBSCRIPTION',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'CREATE_TABLE',
+  tagname => 'CREATE TABLE',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'CREATE_TABLE_AS',
+  tagname => 'CREATE TABLE AS',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'CREATE_TABLESPACE',
+  tagname => 'CREATE TABLESPACE',
+},
+{
+  taglabel => 'CREATE_TEXT_SEARCH_CONFIGURATION',
+  tagname => 'CREATE TEXT SEARCH CONFIGURATION',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'CREATE_TEXT_SEARCH_DICTIONARY',
+  tagname => 'CREATE TEXT SEARCH DICTIONARY',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'CREATE_TEXT_SEARCH_PARSER',
+  tagname => 'CREATE TEXT SEARCH PARSER',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'CREATE_TEXT_SEARCH_TEMPLATE',
+  tagname => 'CREATE TEXT SEARCH TEMPLATE',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'CREATE_TRANSFORM',
+  tagname => 'CREATE TRANSFORM',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'CREATE_TRIGGER',
+  tagname => 'CREATE TRIGGER',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'CREATE_TYPE',
+  tagname => 'CREATE TYPE',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'CREATE_USER_MAPPING',
+  tagname => 'CREATE USER MAPPING',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'CREATE_VIEW',
+  tagname => 'CREATE VIEW',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'DEALLOCATE',
+  tagname => 'DEALLOCATE',
+},
+{
+  taglabel => 'DEALLOCATE_ALL',
+  tagname => 'DEALLOCATE ALL',
+},
+{
+  taglabel => 'DECLARE_CURSOR',
+  tagname => 'DECLARE CURSOR',
+},
+{
+  taglabel => 'DELETE',
+  tagname => 'DELETE',
+  display_rowcount => 'true',
+},
+{
+  taglabel => 'DISCARD',
+  tagname => 'DISCARD',
+},
+{
+  taglabel => 'DISCARD_ALL',
+  tagname => 'DISCARD ALL',
+},
+{
+  taglabel => 'DISCARD_PLANS',
+  tagname => 'DISCARD PLANS',
+},
+{
+  taglabel => 'DISCARD_SEQUENCES',
+  tagname => 'DISCARD SEQUENCES',
+},
+{
+  taglabel => 'DISCARD_TEMP',
+  tagname => 'DISCARD TEMP',
+},
+{
+  taglabel => 'DO',
+  tagname => 'DO',
+},
+{
+  taglabel => 'DROP_ACCESS_METHOD',
+  tagname => 'DROP ACCESS METHOD',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'DROP_AGGREGATE',
+  tagname => 'DROP AGGREGATE',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'DROP_CAST',
+  tagname => 'DROP CAST',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'DROP_COLLATION',
+  tagname => 'DROP COLLATION',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'DROP_CONSTRAINT',
+  tagname => 'DROP CONSTRAINT',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'DROP_CONVERSION',
+  tagname => 'DROP CONVERSION',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'DROP_DATABASE',
+  tagname => 'DROP DATABASE',
+},
+{
+  taglabel => 'DROP_DOMAIN',
+  tagname => 'DROP DOMAIN',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'DROP_EVENT_TRIGGER',
+  tagname => 'DROP EVENT TRIGGER',
+},
+{
+  taglabel => 'DROP_EXTENSION',
+  tagname => 'DROP EXTENSION',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'DROP_FOREIGN_DATA_WRAPPER',
+  tagname => 'DROP FOREIGN DATA WRAPPER',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'DROP_FOREIGN_TABLE',
+  tagname => 'DROP FOREIGN TABLE',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'DROP_FUNCTION',
+  tagname => 'DROP FUNCTION',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'DROP_INDEX',
+  tagname => 'DROP INDEX',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'DROP_LANGUAGE',
+  tagname => 'DROP LANGUAGE',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'DROP_MATERIALIZED_VIEW',
+  tagname => 'DROP MATERIALIZED VIEW',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'DROP_OPERATOR',
+  tagname => 'DROP OPERATOR',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'DROP_OPERATOR_CLASS',
+  tagname => 'DROP OPERATOR CLASS',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'DROP_OPERATOR_FAMILY',
+  tagname => 'DROP OPERATOR FAMILY',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'DROP_OWNED',
+  tagname => 'DROP OWNED',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'DROP_POLICY',
+  tagname => 'DROP POLICY',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'DROP_PROCEDURE',
+  tagname => 'DROP PROCEDURE',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'DROP_PUBLICATION',
+  tagname => 'DROP PUBLICATION',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'DROP_REPLICATION_SLOT',
+  tagname => 'DROP REPLICATION SLOT',
+},
+{
+  taglabel => 'DROP_ROLE',
+  tagname => 'DROP ROLE',
+},
+{
+  taglabel => 'DROP_ROUTINE',
+  tagname => 'DROP ROUTINE',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'DROP_RULE',
+  tagname => 'DROP RULE',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'DROP_SCHEMA',
+  tagname => 'DROP SCHEMA',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'DROP_SEQUENCE',
+  tagname => 'DROP SEQUENCE',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'DROP_SERVER',
+  tagname => 'DROP SERVER',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'DROP_STATISTICS',
+  tagname => 'DROP STATISTICS',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'DROP_SUBSCRIPTION',
+  tagname => 'DROP SUBSCRIPTION',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'DROP_TABLE',
+  tagname => 'DROP TABLE',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'DROP_TABLESPACE',
+  tagname => 'DROP TABLESPACE',
+},
+{
+  taglabel => 'DROP_TEXT_SEARCH_CONFIGURATION',
+  tagname => 'DROP TEXT SEARCH CONFIGURATION',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'DROP_TEXT_SEARCH_DICTIONARY',
+  tagname => 'DROP TEXT SEARCH DICTIONARY',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'DROP_TEXT_SEARCH_PARSER',
+  tagname => 'DROP TEXT SEARCH PARSER',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'DROP_TEXT_SEARCH_TEMPLATE',
+  tagname => 'DROP TEXT SEARCH TEMPLATE',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'DROP_TRANSFORM',
+  tagname => 'DROP TRANSFORM',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'DROP_TRIGGER',
+  tagname => 'DROP TRIGGER',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'DROP_TYPE',
+  tagname => 'DROP TYPE',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'DROP_USER_MAPPING',
+  tagname => 'DROP USER MAPPING',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'DROP_VIEW',
+  tagname => 'DROP VIEW',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'EXECUTE',
+  tagname => 'EXECUTE',
+},
+{
+  taglabel => 'EXPLAIN',
+  tagname => 'EXPLAIN',
+},
+{
+  taglabel => 'FETCH',
+  tagname => 'FETCH',
+  display_rowcount => 'true',
+},
+{
+  taglabel => 'GRANT',
+  tagname => 'GRANT',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'GRANT_ROLE',
+  tagname => 'GRANT ROLE',
+},
+{
+  taglabel => 'IMPORT_FOREIGN_SCHEMA',
+  tagname => 'IMPORT FOREIGN SCHEMA',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'INSERT',
+  tagname => 'INSERT',
+  display_last_oid => 'true',
+  display_rowcount => 'true',
+},
+{
+  taglabel => 'LISTEN',
+  tagname => 'LISTEN',
+},
+{
+  taglabel => 'LOAD',
+  tagname => 'LOAD',
+},
+{
+  taglabel => 'LOCK_TABLE',
+  tagname => 'LOCK TABLE',
+},
+{
+  taglabel => 'MOVE',
+  tagname => 'MOVE',
+  display_rowcount => 'true',
+},
+{
+  taglabel => 'NOTIFY',
+  tagname => 'NOTIFY',
+},
+{
+  taglabel => 'PREPARE',
+  tagname => 'PREPARE',
+},
+{
+  taglabel => 'PREPARE_TRANSACTION',
+  tagname => 'PREPARE TRANSACTION',
+},
+{
+  taglabel => 'REASSIGN_OWNED',
+  tagname => 'REASSIGN OWNED',
+},
+{
+  taglabel => 'REFRESH_MATERIALIZED_VIEW',
+  tagname => 'REFRESH MATERIALIZED VIEW',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'REINDEX',
+  tagname => 'REINDEX',
+},
+{
+  taglabel => 'RELEASE',
+  tagname => 'RELEASE',
+},
+{
+  taglabel => 'RESET',
+  tagname => 'RESET',
+},
+{
+  taglabel => 'REVOKE',
+  tagname => 'REVOKE',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'REVOKE_ROLE',
+  tagname => 'REVOKE ROLE',
+},
+{
+  taglabel => 'ROLLBACK',
+  tagname => 'ROLLBACK',
+},
+{
+  taglabel => 'ROLLBACK_PREPARED',
+  tagname => 'ROLLBACK PREPARED',
+},
+{
+  taglabel => 'SAVEPOINT',
+  tagname => 'SAVEPOINT',
+},
+{
+  taglabel => 'SECURITY_LABEL',
+  tagname => 'SECURITY LABEL',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'SELECT',
+  tagname => 'SELECT',
+  display_rowcount => 'true',
+},
+{
+  taglabel => 'SELECT_FOR_KEY_SHARE',
+  tagname => 'SELECT FOR KEY SHARE',
+},
+{
+  taglabel => 'SELECT_FOR_NO_KEY_UPDATE',
+  tagname => 'SELECT FOR NO KEY UPDATE',
+},
+{
+  taglabel => 'SELECT_FOR_SHARE',
+  tagname => 'SELECT FOR SHARE',
+},
+{
+  taglabel => 'SELECT_FOR_UPDATE',
+  tagname => 'SELECT FOR UPDATE',
+},
+{
+  taglabel => 'SELECT_INTO',
+  tagname => 'SELECT INTO',
+  event_trigger_ok => 'true',
+},
+{
+  taglabel => 'SET',
+  tagname => 'SET',
+},
+{
+  taglabel => 'SET_CONSTRAINTS',
+  tagname => 'SET CONSTRAINTS',
+},
+{
+  taglabel => 'SHOW',
+  tagname => 'SHOW',
+},
+{
+  taglabel => 'START_TRANSACTION',
+  tagname => 'START TRANSACTION',
+},
+{
+  taglabel => 'TRUNCATE_TABLE',
+  tagname => 'TRUNCATE TABLE',
+},
+{
+  taglabel => 'UNLISTEN',
+  tagname => 'UNLISTEN',
+},
+{
+  taglabel => 'UPDATE',
+  tagname => 'UPDATE',
+  display_rowcount => 'true',
+},
+{
+  taglabel => 'VACUUM',
+  tagname => 'VACUUM',
+},
+]
diff --git a/src/include/utils/commandtag.h b/src/include/utils/commandtag.h
new file mode 100644
index 0000000000..ea9b2c682a
--- /dev/null
+++ b/src/include/utils/commandtag.h
@@ -0,0 +1,60 @@
+/*-------------------------------------------------------------------------
+ *
+ * commandtag.h
+ *	  Declarations for commandtag names and enumeration.
+ *
+ * Portions Copyright (c) 1996-2020, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/utils/commandtag.h
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef COMMANDTAG_H
+#define COMMANDTAG_H
+
+#include "utils/commandtag_enum.h"
+
+typedef struct CommandTagBehavior {
+	const char *name;
+	const bool display_last_oid;
+	const bool display_rowcount;
+	const bool event_trigger_ok;
+	const bool table_rewrite_ok;
+} CommandTagBehavior;
+
+extern const CommandTagBehavior tag_behavior[];
+
+typedef struct QueryCompletion
+{
+	CommandTag	commandTag;
+	uint64		nprocessed;
+} QueryCompletion;
+
+static inline void
+SetQueryCompletion(QueryCompletion *qc, CommandTag commandTag, uint64 nprocessed)
+{
+	qc->commandTag = commandTag;
+	qc->nprocessed = nprocessed;
+}
+
+static inline void
+CopyQueryCompletion(QueryCompletion *dst, const QueryCompletion *src)
+{
+	dst->commandTag = src->commandTag;
+	dst->nprocessed = src->nprocessed;
+}
+
+extern void InitializeQueryCompletion(QueryCompletion *qc);
+extern const char *GetCommandTagName(CommandTag commandTag);
+extern bool command_tag_display_last_oid(CommandTag commandTag);
+extern bool command_tag_display_rowcount(CommandTag commandTag);
+extern bool command_tag_event_trigger_ok(CommandTag commandTag);
+extern bool command_tag_table_rewrite_ok(CommandTag commandTag);
+extern CommandTag GetCommandTagEnum(const char *tagname);
+extern void PreventCommandTagIfReadOnly(CommandTag commandTag);
+extern void PreventCommandTagIfParallelMode(CommandTag commandTag);
+extern void PreventCommandTagDuringRecovery(CommandTag commandTag);
+
+#endif							/* COMMANDTAG_H */
diff --git a/src/include/utils/evtcache.h b/src/include/utils/evtcache.h
index 6c3ff81ba3..bc8ce48061 100644
--- a/src/include/utils/evtcache.h
+++ b/src/include/utils/evtcache.h
@@ -28,8 +28,7 @@ typedef struct
 {
 	Oid			fnoid;			/* function to be called */
 	char		enabled;		/* as SESSION_REPLICATION_ROLE_* */
-	int			ntags;			/* number of command tags */
-	char	  **tag;			/* command tags in SORTED order */
+	Bitmapset  *tagset;			/* command tags, or NULL if empty */
 } EventTriggerCacheItem;
 
 extern List *EventCacheLookup(EventTriggerEvent event);
diff --git a/src/include/utils/plancache.h b/src/include/utils/plancache.h
index e48661ebec..b2900307ad 100644
--- a/src/include/utils/plancache.h
+++ b/src/include/utils/plancache.h
@@ -18,6 +18,7 @@
 #include "access/tupdesc.h"
 #include "lib/ilist.h"
 #include "nodes/params.h"
+#include "utils/commandtag.h"
 #include "utils/queryenvironment.h"
 
 /* Forward declaration, to avoid including parsenodes.h here */
@@ -95,7 +96,7 @@ typedef struct CachedPlanSource
 	int			magic;			/* should equal CACHEDPLANSOURCE_MAGIC */
 	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 */
+	CommandTag	commandTag;
 	Oid		   *param_types;	/* array of parameter type OIDs, or NULL */
 	int			num_params;		/* length of param_types array */
 	ParserSetupHook parserSetup;	/* alternative parameter spec method */
@@ -186,10 +187,10 @@ extern void ResetPlanCache(void);
 
 extern CachedPlanSource *CreateCachedPlan(struct RawStmt *raw_parse_tree,
 										  const char *query_string,
-										  const char *commandTag);
+										  CommandTag commandTag);
 extern CachedPlanSource *CreateOneShotCachedPlan(struct RawStmt *raw_parse_tree,
 												 const char *query_string,
-												 const char *commandTag);
+												 CommandTag commandTag);
 extern void CompleteCachedPlan(CachedPlanSource *plansource,
 							   List *querytree_list,
 							   MemoryContext querytree_context,
diff --git a/src/include/utils/portal.h b/src/include/utils/portal.h
index 0b69433722..b398ae45a3 100644
--- a/src/include/utils/portal.h
+++ b/src/include/utils/portal.h
@@ -48,6 +48,7 @@
 
 #include "datatype/timestamp.h"
 #include "executor/execdesc.h"
+#include "utils/commandtag.h"
 #include "utils/plancache.h"
 #include "utils/resowner.h"
 
@@ -132,7 +133,8 @@ 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 */
+	CommandTag	commandTag;		/* command tag for original query */
+	QueryCompletion	qc;			/* command completion data for executed query */
 	List	   *stmts;			/* list of PlannedStmts */
 	CachedPlan *cplan;			/* CachedPlan, if stmts are from one */
 
@@ -227,7 +229,7 @@ extern Portal GetPortalByName(const char *name);
 extern void PortalDefineQuery(Portal portal,
 							  const char *prepStmtName,
 							  const char *sourceText,
-							  const char *commandTag,
+							  CommandTag commandTag,
 							  List *stmts,
 							  CachedPlan *cplan);
 extern PlannedStmt *PortalGetPrimaryStmt(Portal portal);
diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
index 5acf604f63..e165790d66 100644
--- a/src/pl/plpgsql/src/pl_exec.c
+++ b/src/pl/plpgsql/src/pl_exec.c
@@ -41,6 +41,7 @@
 #include "tcop/utility.h"
 #include "utils/array.h"
 #include "utils/builtins.h"
+#include "utils/commandtag.h"
 #include "utils/datum.h"
 #include "utils/fmgroids.h"
 #include "utils/lsyscache.h"
@@ -1473,7 +1474,7 @@ plpgsql_fulfill_promise(PLpgSQL_execstate *estate,
 		case PLPGSQL_PROMISE_TG_TAG:
 			if (estate->evtrigdata == NULL)
 				elog(ERROR, "event trigger promise is not in an event trigger function");
-			assign_text_var(estate, var, estate->evtrigdata->tag);
+			assign_text_var(estate, var, GetCommandTagName(estate->evtrigdata->tag));
 			break;
 
 		default:
@@ -4115,10 +4116,9 @@ exec_stmt_execsql(PLpgSQL_execstate *estate,
 			 * tree(s), since those are the result of rewriting and could have
 			 * been transmogrified into something else entirely.
 			 */
-			if (plansource->commandTag &&
-				(strcmp(plansource->commandTag, "INSERT") == 0 ||
-				 strcmp(plansource->commandTag, "UPDATE") == 0 ||
-				 strcmp(plansource->commandTag, "DELETE") == 0))
+			if (plansource->commandTag == COMMANDTAG_INSERT ||
+				plansource->commandTag == COMMANDTAG_UPDATE ||
+				plansource->commandTag == COMMANDTAG_DELETE)
 			{
 				stmt->mod_stmt = true;
 				break;
diff --git a/src/test/modules/test_ddl_deparse/test_ddl_deparse.c b/src/test/modules/test_ddl_deparse/test_ddl_deparse.c
index e1629ec618..b7bdb88ce7 100644
--- a/src/test/modules/test_ddl_deparse/test_ddl_deparse.c
+++ b/src/test/modules/test_ddl_deparse/test_ddl_deparse.c
@@ -74,7 +74,7 @@ get_command_tag(PG_FUNCTION_ARGS)
 	if (!cmd->parsetree)
 		PG_RETURN_NULL();
 
-	PG_RETURN_TEXT_P(cstring_to_text(CreateCommandTag(cmd->parsetree)));
+	PG_RETURN_TEXT_P(cstring_to_text(CreateCommandName(cmd->parsetree)));
 }
 
 /*
diff --git a/src/tools/msvc/Solution.pm b/src/tools/msvc/Solution.pm
index 8412ef298e..5afc83498f 100644
--- a/src/tools/msvc/Solution.pm
+++ b/src/tools/msvc/Solution.pm
@@ -558,6 +558,7 @@ sub GenerateFiles
 
 	chdir('src/backend/utils');
 	my $pg_proc_dat = '../../../src/include/catalog/pg_proc.dat';
+	my $commandtag_dat = '../../../src/include/utils/commandtag.dat';
 	if (   IsNewer('fmgr-stamp', 'Gen_fmgrtab.pl')
 		|| IsNewer('fmgr-stamp', '../catalog/Catalog.pm')
 		|| IsNewer('fmgr-stamp', $pg_proc_dat)
@@ -570,6 +571,17 @@ sub GenerateFiles
 		  || confess "Could not touch fmgr-stamp";
 		close($f);
 	}
+	if (   IsNewer('commandtag-stamp', 'gencommandtag.pl')
+		|| IsNewer('commandtag-stamp', 'DataFile.pm')
+		|| IsNewer('commandtag-stamp', $commandtag_dat))
+	{
+		system(
+			"perl -I ../../../src/include/utils gencommandtag.pl --headerdir=../../../src/include/utils --sourcedir=. --inputfile=$commandtag_dat"
+		);
+		open(my $f, '>', 'commandtag-stamp')
+		  || confess "Could not touch commandtag-stamp";
+		close($f);
+	}
 	chdir('../../..');
 
 	if (IsNewer(
-- 
2.21.1 (Apple Git-122.3)

