From 0e8ea911928c2051408c0551534468e8a094aa58 Mon Sep 17 00:00:00 2001 From: Nikolay Samokhvalov Date: Wed, 11 Mar 2026 17:41:21 +0000 Subject: [PATCH 1/3] feat: command_tag_format GUC with legacy/modern/verbose/fqn modes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a new enum GUC command_tag_format (PGC_USERSET) with four modes: - legacy: INSERT 0 N (default, backward compatible) - modern: INSERT N (drops legacy OID field) - verbose: INSERT tablename N (shows relation name) - fqn: INSERT schema.tablename N (fully qualified) Tested output: legacy: INSERT 0 1 modern: INSERT 1 verbose: INSERT users 1 fqn: INSERT myschema.users 1 Supports SET LOCAL for per-transaction control. libpq PQcmdTuples() handles all formats gracefully. Note: verbose/fqn currently only populated for INSERT (UPDATE/DELETE show count only — relname propagation for those pending). --- src/backend/tcop/cmdtag.c | 35 +++++++++++++++++++++-- src/backend/tcop/pquery.c | 24 ++++++++++++++++ src/backend/utils/misc/guc_parameters.dat | 10 +++++++ src/backend/utils/misc/guc_tables.c | 9 ++++++ src/include/tcop/cmdtag.h | 10 +++++++ src/interfaces/libpq/fe-exec.c | 18 ++++++++---- 6 files changed, 98 insertions(+), 8 deletions(-) diff --git a/src/backend/tcop/cmdtag.c b/src/backend/tcop/cmdtag.c index d38d5b390b9d1..9fe513150f9d2 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 d8fc75d0bb995..a1e812f9327a4 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/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat index a5a0edf2534aa..c86b5ab926ea6 100644 --- a/src/backend/utils/misc/guc_parameters.dat +++ b/src/backend/utils/misc/guc_parameters.dat @@ -426,6 +426,7 @@ options => 'client_message_level_options', }, + { name => 'cluster_name', type => 'string', context => 'PGC_POSTMASTER', group => 'PROCESS_TITLE', short_desc => 'Sets the name of the cluster, which is included in the process title.', flags => 'GUC_IS_NAME', @@ -435,6 +436,15 @@ }, # we have no microseconds designation, so can't supply units here + +{ name => 'command_tag_format', type => 'enum', context => 'PGC_USERSET', group => 'CLIENT_CONN_STATEMENT', + short_desc => 'Controls the format of INSERT command completion tags.', + long_desc => 'legacy: INSERT 0 N (default, backward compatible). modern: INSERT N (no OID). verbose: INSERT tablename N. fqn: INSERT schema.tablename N.', + variable => 'command_tag_format', + boot_val => 'COMMAND_TAG_FORMAT_LEGACY', + options => 'command_tag_format_options', + includes => 'tcop/cmdtag.h', +}, { 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.', variable => 'CommitDelay', diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c index 38aaf82f12094..11b2ce7e64f01 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 @@ -148,6 +149,14 @@ 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}, + {"modern", 1, false}, + {"verbose", 2, false}, + {"fqn", 3, false}, + {NULL, 0, false} +}; + const struct config_enum_entry server_message_level_options[] = { {"debug5", DEBUG5, false}, {"debug4", DEBUG4, false}, diff --git a/src/include/tcop/cmdtag.h b/src/include/tcop/cmdtag.h index cf2e87b98f314..42901db39bb84 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,14 @@ 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_MODERN 1 /* INSERT N (no OID) */ +#define COMMAND_TAG_FORMAT_VERBOSE 2 /* INSERT tablename N */ +#define COMMAND_TAG_FORMAT_FQN 3 /* INSERT schema.tablename N */ + +extern int command_tag_format; extern Size BuildQueryCompletionString(char *buff, const QueryCompletion *qc, bool nameonly); diff --git a/src/interfaces/libpq/fe-exec.c b/src/interfaces/libpq/fe-exec.c index 203d388bdbf2a..f072443e31902 100644 --- a/src/interfaces/libpq/fe-exec.c +++ b/src/interfaces/libpq/fe-exec.c @@ -3847,12 +3847,18 @@ PQcmdTuples(PGresult *res) if (strncmp(res->cmdStatus, "INSERT ", 7) == 0) { p = res->cmdStatus + 7; - /* INSERT: skip oid and space */ - while (*p && *p != ' ') - p++; - if (*p == 0) - goto interpret_error; /* no space? */ - p++; + /* INSERT: handle both "INSERT oid count" and "INSERT count" */ + { + char *q = p; + while (*q && *q != ' ') + q++; + if (*q == ' ') + { + /* Old format: "INSERT oid count" - skip oid and space */ + p = q + 1; + } + /* else: new format "INSERT count" - p already points to count */ + } } else if (strncmp(res->cmdStatus, "SELECT ", 7) == 0 || strncmp(res->cmdStatus, "DELETE ", 7) == 0 || From dca7a0137fa42461003151394320761c02d9ebf9 Mon Sep 17 00:00:00 2001 From: Nikolay Samokhvalov Date: Wed, 11 Mar 2026 17:49:30 +0000 Subject: [PATCH 2/3] feat: protocol negotiation via _pq_.command_tag_format + GUC_REPORT - Server handles _pq_.command_tag_format in startup parameters, mapping it to the command_tag_format GUC - Added GUC_REPORT flag so server auto-reports format changes to client - Client can negotiate format at connection time via: psql 'options=-ccommand_tag_format=modern' - Old client + new server: server uses legacy default (safe) - New client + old server: _pq_ option silently ignored (safe) - Mid-session SET also works and is reported to client Tested: three connections with different formats confirmed working. --- src/backend/tcop/backend_startup.c | 20 +++++++++++++++----- src/backend/utils/misc/guc_parameters.dat | 3 ++- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/backend/tcop/backend_startup.c b/src/backend/tcop/backend_startup.c index c517115927c96..68fc4d82732cf 100644 --- a/src/backend/tcop/backend_startup.c +++ b/src/backend/tcop/backend_startup.c @@ -778,12 +778,22 @@ 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) + { + /* Map protocol option to GUC */ + port->guc_options = lappend(port->guc_options, + pstrdup("command_tag_format")); + port->guc_options = lappend(port->guc_options, + pstrdup(valptr)); + } + else + { + unrecognized_protocol_options = + lappend(unrecognized_protocol_options, pstrdup(nameptr)); + } } else { diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat index c86b5ab926ea6..de23cc22e63ad 100644 --- a/src/backend/utils/misc/guc_parameters.dat +++ b/src/backend/utils/misc/guc_parameters.dat @@ -439,7 +439,8 @@ { name => 'command_tag_format', type => 'enum', context => 'PGC_USERSET', group => 'CLIENT_CONN_STATEMENT', short_desc => 'Controls the format of INSERT command completion tags.', - long_desc => 'legacy: INSERT 0 N (default, backward compatible). modern: INSERT N (no OID). verbose: INSERT tablename N. fqn: INSERT schema.tablename N.', + long_desc => 'legacy: INSERT 0 N (default, backward compatible). modern: INSERT N (no OID). 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', From 5163251168990d31375e8935ecdb404b026dda68 Mon Sep 17 00:00:00 2001 From: Nikolay Samokhvalov Date: Wed, 11 Mar 2026 18:20:00 +0000 Subject: [PATCH 3/3] feat: protocol-only command_tag_format (PGC_INTERNAL + _pq_ negotiation) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove 'modern' mode; keep legacy/verbose/fqn - Change GUC context from PGC_USERSET to PGC_INTERNAL: blocks SET, options=-c, ALTER SYSTEM, and postgresql.conf - Store _pq_.command_tag_format in Port->pq_command_tag_format (not guc_options) to isolate from regular GUC path - Apply deferred in process_startup_options() via PGC_S_OVERRIDE - Revert libpq fe-exec.c: old protocol completely untouched Safety guarantees: Old client + new server (no _pq_) → INSERT 0 N (always safe) New client + old server (_pq_ sent) → silently ignored SET command_tag_format → ERROR: cannot be changed options=-ccommand_tag_format=verbose → FATAL: cannot be changed _pq_.command_tag_format=verbose → INSERT tablename N (works) Tested with stock PG17 psql (old client) and raw _pq_ startup packet (Python socket test). --- src/backend/tcop/backend_startup.c | 12 +++++++----- src/backend/utils/init/postinit.c | 11 +++++++++++ src/backend/utils/misc/guc_parameters.dat | 7 +++---- src/backend/utils/misc/guc_tables.c | 5 ++--- src/include/libpq/libpq-be.h | 3 +++ src/include/tcop/cmdtag.h | 5 ++--- src/interfaces/libpq/fe-exec.c | 18 ++++++------------ 7 files changed, 34 insertions(+), 27 deletions(-) diff --git a/src/backend/tcop/backend_startup.c b/src/backend/tcop/backend_startup.c index 68fc4d82732cf..ff1d620cf3750 100644 --- a/src/backend/tcop/backend_startup.c +++ b/src/backend/tcop/backend_startup.c @@ -783,11 +783,13 @@ ProcessStartupPacket(Port *port, bool ssl_done, bool gss_done) */ if (strcmp(nameptr, "_pq_.command_tag_format") == 0) { - /* Map protocol option to GUC */ - port->guc_options = lappend(port->guc_options, - pstrdup("command_tag_format")); - port->guc_options = lappend(port->guc_options, - pstrdup(valptr)); + /* + * 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 { diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c index b59e08605cc79..6362640071ba3 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 de23cc22e63ad..9e839c8f454b9 100644 --- a/src/backend/utils/misc/guc_parameters.dat +++ b/src/backend/utils/misc/guc_parameters.dat @@ -426,7 +426,6 @@ options => 'client_message_level_options', }, - { name => 'cluster_name', type => 'string', context => 'PGC_POSTMASTER', group => 'PROCESS_TITLE', short_desc => 'Sets the name of the cluster, which is included in the process title.', flags => 'GUC_IS_NAME', @@ -435,17 +434,17 @@ check_hook => 'check_cluster_name', }, -# we have no microseconds designation, so can't supply units here -{ name => 'command_tag_format', type => 'enum', context => 'PGC_USERSET', group => 'CLIENT_CONN_STATEMENT', +{ 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). modern: INSERT N (no OID). verbose: INSERT tablename N. fqn: INSERT schema.tablename N. Can be set via _pq_.command_tag_format startup parameter for protocol-level negotiation.', + 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.', variable => 'CommitDelay', diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c index 11b2ce7e64f01..30e7b14cade9a 100644 --- a/src/backend/utils/misc/guc_tables.c +++ b/src/backend/utils/misc/guc_tables.c @@ -151,9 +151,8 @@ static const struct config_enum_entry client_message_level_options[] = { static const struct config_enum_entry command_tag_format_options[] = { {"legacy", 0, false}, - {"modern", 1, false}, - {"verbose", 2, false}, - {"fqn", 3, false}, + {"verbose", 1, false}, + {"fqn", 2, false}, {NULL, 0, false} }; diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h index 921b2daa4ff92..565bc49fcf311 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 42901db39bb84..eaf864f7a7544 100644 --- a/src/include/tcop/cmdtag.h +++ b/src/include/tcop/cmdtag.h @@ -61,9 +61,8 @@ 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_MODERN 1 /* INSERT N (no OID) */ -#define COMMAND_TAG_FORMAT_VERBOSE 2 /* INSERT tablename N */ -#define COMMAND_TAG_FORMAT_FQN 3 /* INSERT schema.tablename N */ +#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, diff --git a/src/interfaces/libpq/fe-exec.c b/src/interfaces/libpq/fe-exec.c index f072443e31902..203d388bdbf2a 100644 --- a/src/interfaces/libpq/fe-exec.c +++ b/src/interfaces/libpq/fe-exec.c @@ -3847,18 +3847,12 @@ PQcmdTuples(PGresult *res) if (strncmp(res->cmdStatus, "INSERT ", 7) == 0) { p = res->cmdStatus + 7; - /* INSERT: handle both "INSERT oid count" and "INSERT count" */ - { - char *q = p; - while (*q && *q != ' ') - q++; - if (*q == ' ') - { - /* Old format: "INSERT oid count" - skip oid and space */ - p = q + 1; - } - /* else: new format "INSERT count" - p already points to count */ - } + /* INSERT: skip oid and space */ + while (*p && *p != ' ') + p++; + if (*p == 0) + goto interpret_error; /* no space? */ + p++; } else if (strncmp(res->cmdStatus, "SELECT ", 7) == 0 || strncmp(res->cmdStatus, "DELETE ", 7) == 0 ||