From a767a27209a5dae0fdb0ab59d528b188d27ad813 Mon Sep 17 00:00:00 2001
From: Nikolay Samokhvalov <nik@postgres.ai>
Date: Wed, 11 Mar 2026 20:57:24 +0000
Subject: [PATCH] Add command_tag_format protocol-level negotiation

Add a new protocol-level option _pq_.command_tag_format that allows
clients to negotiate richer command completion tags at connection time.

The INSERT command completion tag has carried a vestigial OID field
since PostgreSQL 12 dropped support for table OIDs: INSERT always
returns 'INSERT 0 N' where the '0' is meaningless.  Changing this
in the default wire format would break existing clients, since
libpq's PQcmdTuples() hardcodes the 'INSERT oid count' parse pattern.

This patch adds protocol-level negotiation via _pq_.command_tag_format
in the startup packet, with three modes:

  legacy  - INSERT 0 N  (default, fully backward compatible)
  verbose - INSERT tablename N
  fqn     - INSERT schema.tablename N

The verbose and fqn modes also populate relation names for UPDATE,
DELETE, and MERGE command tags.

The GUC is registered as PGC_INTERNAL with GUC_REPORT, ensuring:

  - Old clients that don't send _pq_.command_tag_format always get
    the legacy format (zero breakage)
  - New clients connecting to old servers have _pq_ silently ignored
  - SET, options=-c, ALTER SYSTEM, and postgresql.conf cannot change
    the format (only the _pq_ startup packet path works)
  - The server auto-reports the negotiated format to the client

Implementation details:

  - The _pq_ handler in ProcessStartupPacket() stores the requested
    format in a new Port field (pq_command_tag_format), separate from
    guc_options, to prevent the regular GUC path from setting it
  - The value is applied in process_startup_options() after normal
    GUC initialization via SetConfigOption(PGC_INTERNAL, PGC_S_OVERRIDE)
  - QueryCompletion is extended with relname/nspname fields, populated
    from the executor's ResultRelInfo after query completion
  - BuildQueryCompletionString() formats the tag based on the
    negotiated mode
  - No changes to libpq (fe-exec.c) -- old protocol is untouched

Discussion: (none yet)
---
 src/backend/tcop/backend_startup.c        | 22 ++++++++++----
 src/backend/tcop/cmdtag.c                 | 35 +++++++++++++++++++++--
 src/backend/tcop/pquery.c                 | 24 ++++++++++++++++
 src/backend/utils/init/postinit.c         | 11 +++++++
 src/backend/utils/misc/guc_parameters.dat | 10 +++++++
 src/backend/utils/misc/guc_tables.c       |  8 ++++++
 src/include/libpq/libpq-be.h              |  3 ++
 src/include/tcop/cmdtag.h                 |  9 ++++++
 8 files changed, 115 insertions(+), 7 deletions(-)

diff --git a/src/backend/tcop/backend_startup.c b/src/backend/tcop/backend_startup.c
index c517115927c..ff1d620cf37 100644
--- a/src/backend/tcop/backend_startup.c
+++ b/src/backend/tcop/backend_startup.c
@@ -778,12 +778,24 @@ ProcessStartupPacket(Port *port, bool ssl_done, bool gss_done)
 			else if (strncmp(nameptr, "_pq_.", 5) == 0)
 			{
 				/*
-				 * Any option beginning with _pq_. is reserved for use as a
-				 * protocol-level option, but at present no such options are
-				 * defined.
+				 * Options beginning with _pq_. are protocol-level options.
+				 * Recognized options are mapped to their corresponding GUCs.
 				 */
-				unrecognized_protocol_options =
-					lappend(unrecognized_protocol_options, pstrdup(nameptr));
+				if (strcmp(nameptr, "_pq_.command_tag_format") == 0)
+				{
+					/*
+					 * Protocol-level option: store for deferred application
+					 * in process_startup_options() after GUC init.  This
+					 * is NOT added to guc_options so that old-style
+					 * options=-c cannot set it (GUC is PGC_INTERNAL).
+					 */
+					port->pq_command_tag_format = pstrdup(valptr);
+				}
+				else
+				{
+					unrecognized_protocol_options =
+						lappend(unrecognized_protocol_options, pstrdup(nameptr));
+				}
 			}
 			else
 			{
diff --git a/src/backend/tcop/cmdtag.c b/src/backend/tcop/cmdtag.c
index d38d5b390b9..9fe513150f9 100644
--- a/src/backend/tcop/cmdtag.c
+++ b/src/backend/tcop/cmdtag.c
@@ -14,6 +14,7 @@
 #include "postgres.h"
 
 #include "tcop/cmdtag.h"
+#include "utils/guc.h"
 #include "utils/builtins.h"
 
 
@@ -36,11 +37,16 @@ static const CommandTagBehavior tag_behavior[] = {
 
 #undef PG_CMDTAG
 
+/* GUC variable: command tag format style */
+int		command_tag_format = COMMAND_TAG_FORMAT_LEGACY;
+
 void
 InitializeQueryCompletion(QueryCompletion *qc)
 {
 	qc->commandTag = CMDTAG_UNKNOWN;
 	qc->nprocessed = 0;
+	qc->relname = NULL;
+	qc->nspname = NULL;
 }
 
 const char *
@@ -147,8 +153,33 @@ BuildQueryCompletionString(char *buff, const QueryCompletion *qc,
 	{
 		if (tag == CMDTAG_INSERT)
 		{
-			*bufp++ = ' ';
-			*bufp++ = '0';
+			if (command_tag_format == COMMAND_TAG_FORMAT_LEGACY)
+			{
+				/* Legacy: INSERT 0 N */
+				*bufp++ = ' ';
+				*bufp++ = '0';
+			}
+			else if ((command_tag_format == COMMAND_TAG_FORMAT_VERBOSE ||
+					  command_tag_format == COMMAND_TAG_FORMAT_FQN) &&
+					 qc->relname != NULL)
+			{
+				/* Verbose/FQN: INSERT [schema.]table N */
+				*bufp++ = ' ';
+				if (command_tag_format == COMMAND_TAG_FORMAT_FQN &&
+					qc->nspname != NULL)
+				{
+					Size nsplen = strlen(qc->nspname);
+					memcpy(bufp, qc->nspname, nsplen);
+					bufp += nsplen;
+					*bufp++ = '.';
+				}
+				{
+					Size rellen = strlen(qc->relname);
+					memcpy(bufp, qc->relname, rellen);
+					bufp += rellen;
+				}
+			}
+			/* Modern: INSERT N (nothing extra before count) */
 		}
 		*bufp++ = ' ';
 		bufp += pg_ulltoa_n(qc->nprocessed, bufp);
diff --git a/src/backend/tcop/pquery.c b/src/backend/tcop/pquery.c
index d8fc75d0bb9..a1e812f9327 100644
--- a/src/backend/tcop/pquery.c
+++ b/src/backend/tcop/pquery.c
@@ -26,6 +26,9 @@
 #include "tcop/pquery.h"
 #include "tcop/utility.h"
 #include "utils/memutils.h"
+#include "catalog/namespace.h"
+#include "utils/rel.h"
+#include "utils/lsyscache.h"
 #include "utils/snapmgr.h"
 
 
@@ -181,6 +184,27 @@ ProcessQuery(PlannedStmt *plan,
 			tag = CMDTAG_UNKNOWN;
 
 		SetQueryCompletion(qc, tag, queryDesc->estate->es_processed);
+
+		/* For verbose/FQN command tags, attach relation info for DML */
+		if (command_tag_format >= COMMAND_TAG_FORMAT_VERBOSE &&
+			(tag == CMDTAG_INSERT || tag == CMDTAG_UPDATE ||
+			 tag == CMDTAG_DELETE || tag == CMDTAG_MERGE) &&
+			queryDesc->plannedstmt != NULL &&
+			queryDesc->plannedstmt->resultRelations != NIL &&
+			queryDesc->estate->es_result_relations != NULL)
+		{
+			int ri_index = linitial_int(queryDesc->plannedstmt->resultRelations) - 1;
+			if (ri_index >= 0 &&
+				ri_index < (int) queryDesc->estate->es_range_table_size &&
+				queryDesc->estate->es_result_relations[ri_index] != NULL &&
+				queryDesc->estate->es_result_relations[ri_index]->ri_RelationDesc != NULL)
+			{
+				ResultRelInfo *rri = queryDesc->estate->es_result_relations[ri_index];
+				qc->relname = RelationGetRelationName(rri->ri_RelationDesc);
+				qc->nspname = get_namespace_name(
+					RelationGetNamespace(rri->ri_RelationDesc));
+			}
+		}
 	}
 
 	/*
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index b59e08605cc..6362640071b 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -1309,6 +1309,17 @@ process_startup_options(Port *port, bool am_superuser)
 
 		SetConfigOption(name, value, gucctx, PGC_S_CLIENT);
 	}
+
+	/*
+	 * Apply protocol-negotiated options.  These use PGC_INTERNAL context
+	 * with PGC_S_OVERRIDE source, so they bypass the normal GUC access
+	 * controls.  This ensures only the _pq_ protocol path can set them;
+	 * SET and options=-c are blocked by PGC_INTERNAL.
+	 */
+	if (port->pq_command_tag_format != NULL)
+		SetConfigOption("command_tag_format",
+						port->pq_command_tag_format,
+						PGC_INTERNAL, PGC_S_OVERRIDE);
 }
 
 /*
diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat
index a5a0edf2534..9e839c8f454 100644
--- a/src/backend/utils/misc/guc_parameters.dat
+++ b/src/backend/utils/misc/guc_parameters.dat
@@ -434,6 +434,16 @@
   check_hook => 'check_cluster_name',
 },
 
+
+{ name => 'command_tag_format', type => 'enum', context => 'PGC_INTERNAL', group => 'CLIENT_CONN_STATEMENT',
+  short_desc => 'Controls the format of INSERT command completion tags.',
+  long_desc => 'legacy: INSERT 0 N (default, backward compatible). verbose: INSERT tablename N. fqn: INSERT schema.tablename N. Can be set via _pq_.command_tag_format startup parameter for protocol-level negotiation.',
+  flags => 'GUC_REPORT',
+  variable => 'command_tag_format',
+  boot_val => 'COMMAND_TAG_FORMAT_LEGACY',
+  options => 'command_tag_format_options',
+  includes => 'tcop/cmdtag.h',
+},
 # we have no microseconds designation, so can't supply units here
 { name => 'commit_delay', type => 'int', context => 'PGC_SUSET', group => 'WAL_SETTINGS',
   short_desc => 'Sets the delay in microseconds between transaction commit and flushing WAL to disk.',
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 38aaf82f120..30e7b14cade 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -19,6 +19,7 @@
  *--------------------------------------------------------------------
  */
 #include "postgres.h"
+#include "tcop/cmdtag.h"
 
 #ifdef HAVE_COPYFILE_H
 #include <copyfile.h>
@@ -148,6 +149,13 @@ static const struct config_enum_entry client_message_level_options[] = {
 	{NULL, 0, false}
 };
 
+static const struct config_enum_entry command_tag_format_options[] = {
+	{"legacy", 0, false},
+	{"verbose", 1, false},
+	{"fqn", 2, false},
+	{NULL, 0, false}
+};
+
 const struct config_enum_entry server_message_level_options[] = {
 	{"debug5", DEBUG5, false},
 	{"debug4", DEBUG4, false},
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 921b2daa4ff..565bc49fcf3 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -152,6 +152,9 @@ typedef struct Port
 	char	   *cmdline_options;
 	List	   *guc_options;
 
+	/* Protocol-negotiated command tag format (from _pq_.command_tag_format) */
+	char	   *pq_command_tag_format;
+
 	/*
 	 * The startup packet application name, only used here for the "connection
 	 * authorized" log message. We shouldn't use this post-startup, instead
diff --git a/src/include/tcop/cmdtag.h b/src/include/tcop/cmdtag.h
index cf2e87b98f3..eaf864f7a75 100644
--- a/src/include/tcop/cmdtag.h
+++ b/src/include/tcop/cmdtag.h
@@ -30,6 +30,8 @@ typedef struct QueryCompletion
 {
 	CommandTag	commandTag;
 	uint64		nprocessed;
+	const char *relname;		/* relation name for verbose command tags */
+	const char *nspname;		/* schema name for FQN command tags */
 } QueryCompletion;
 
 
@@ -56,6 +58,13 @@ 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 *commandname);
+
+/* GUC: command tag format style */
+#define COMMAND_TAG_FORMAT_LEGACY   0   /* INSERT 0 N (default, backward compat) */
+#define COMMAND_TAG_FORMAT_VERBOSE  1   /* INSERT tablename N */
+#define COMMAND_TAG_FORMAT_FQN      2   /* INSERT schema.tablename N */
+
+extern int command_tag_format;
 extern Size BuildQueryCompletionString(char *buff, const QueryCompletion *qc,
 									   bool nameonly);
 
-- 
2.43.0

