From 4c509f2428f9c36ed9f6fd67bc8ce78165384038 Mon Sep 17 00:00:00 2001
From: Jelte Fennema-Nio <jelte.fennema@microsoft.com>
Date: Wed, 10 Jan 2024 12:10:19 +0100
Subject: [PATCH v6 10/10] Add _pq_.report_parameters protocol extension

Connection poolers use the ParameterStatus message to track changes in
session level parameters. Before assinging a server connection to a
client, the pooler will first restore the server parameters that the
client expects. This is done to emulate session level SET commands in
transaction pooling mode.

Previously this could only be done for a hard-coded set of backend
parameters, but with this change a connection pooler can choose for
which backend parameters it wants to receive status changes. It can do
this by setting the newly added `_pq_.report_parameters` protocol
extension parameter to a list of parameter names.
---
 doc/src/sgml/config.sgml                      |  33 ++++++
 doc/src/sgml/libpq.sgml                       |   4 +-
 doc/src/sgml/protocol.sgml                    |   8 +-
 src/backend/utils/misc/guc.c                  | 102 +++++++++++++++++-
 src/backend/utils/misc/guc_tables.c           |  34 +++---
 src/include/utils/guc.h                       |   1 +
 src/include/utils/guc_hooks.h                 |   2 +
 src/interfaces/libpq/fe-connect.c             |   4 +
 src/interfaces/libpq/fe-protocol3.c           |   3 +
 src/interfaces/libpq/libpq-int.h              |   1 +
 .../modules/libpq_pipeline/libpq_pipeline.c   |  73 +++++++++++++
 .../libpq_pipeline/t/001_libpq_pipeline.pl    |   2 +-
 .../traces/report_parameters.trace            |  31 ++++++
 13 files changed, 277 insertions(+), 21 deletions(-)
 create mode 100644 src/test/modules/libpq_pipeline/traces/report_parameters.trace

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 6add5c3966e..aac3cab43a6 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -11024,6 +11024,39 @@ dynamic_library_path = 'C:\tools\postgresql;H:\my_project\lib;$libdir'
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-pq-report-parameters" xreflabel="_pq_.protocol_managed_params">
+      <term><varname>_pq_.report_parameters</varname> (<type>string</type>)
+      <indexterm>
+       <primary><varname>_pq_.report_parameters</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        This parameter specifies for which backend parameters the server
+        uses the ParameterStatus message to report their value on connection
+        startup and whenever their value changes. Changing this parameter can
+        be useful to limit unnecessary network traffic for clients that don't
+        care about all of the the parameters that are reported by default. Or
+        it can be used to report changes for additional parameters that are not
+        reported by default. The parameters that are in this list by default
+        are
+        <varname>server_version</varname>,
+        <varname>server_encoding</varname>,
+        <varname>client_encoding</varname>,
+        <varname>application_name</varname>,
+        <varname>default_transaction_read_only</varname>,
+        <varname>in_hot_standby</varname>,
+        <varname>is_superuser</varname>,
+        <varname>session_authorization</varname>,
+        <varname>DateStyle</varname>,
+        <varname>IntervalStyle</varname>,
+        <varname>TimeZone</varname>,
+        <varname>integer_datetimes</varname>, and
+        <varname>standard_conforming_strings</varname>.
+       </para>
+      </listitem>
+     </varlistentry>
+
      </variablelist>
    </sect1>
 
diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 32abd220cef..c401bf64695 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -2519,7 +2519,9 @@ const char *PQparameterStatus(const PGconn *conn, const char *paramName);
       </para>
 
       <para>
-       Parameters reported as of the current release include
+       It's possible to choose for which parameter values are reported to the client
+       using <xref linkend="guc-pq-report-parameters"/> and
+       The parameters that are reported by default are
        <varname>server_version</varname>,
        <varname>server_encoding</varname>,
        <varname>client_encoding</varname>,
diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml
index 72f43dd51f1..33569cb6d8f 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -1336,8 +1336,9 @@ SELCT 1/0;<!-- this typo is intentional -->
    </para>
 
    <para>
-    At present there is a hard-wired set of parameters for which
-    ParameterStatus will be generated: they are
+    It's possible to choose for which parameter values are reported to the
+    client using <xref linkend="guc-pq-report-parameters"/>. The default
+    parameters for which ParameterStatus will be generated are
     <varname>server_version</varname>,
     <varname>server_encoding</varname>,
     <varname>client_encoding</varname>,
@@ -1366,7 +1367,8 @@ SELCT 1/0;<!-- this typo is intentional -->
     <varname>server_encoding</varname> and
     <varname>integer_datetimes</varname>
     are pseudo-parameters that cannot change after startup.
-    This set might change in the future, or even become configurable.
+    Since this set is configurable, a frontend can not assume that it only
+    gets these specific defaults.
     Accordingly, a frontend should simply ignore ParameterStatus for
     parameters that it does not understand or care about.
    </para>
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index b10f2295d13..42e2a908d4d 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -2602,8 +2602,16 @@ ReportChangedGUCOptions(void)
 		struct config_generic *conf = slist_container(struct config_generic,
 													  report_link, iter.cur);
 
-		Assert((conf->flags & GUC_REPORT) && (conf->status & GUC_NEEDS_REPORT));
-		ReportGUCOption(conf);
+		Assert(conf->status & GUC_NEEDS_REPORT);
+
+		/*
+		 * The GUC_REPORT is usually set if we reach here, but it's possible
+		 * that it was cleared just before due to a change in the value of
+		 * _pq_.report_parameter.
+		 */
+		if (conf->flags & GUC_REPORT)
+			ReportGUCOption(conf);
+
 		conf->status &= ~GUC_NEEDS_REPORT;
 		slist_delete_current(&iter);
 	}
@@ -7050,3 +7058,93 @@ assign_protocol_managed_params(const char *newval, void *extra)
 	pfree(protocol_params_str);
 	list_free(namelist);
 }
+
+
+/*
+ * GUC check_hook for report_parameters
+ */
+bool
+check_report_parameters(char **newval, void **extra, GucSource source)
+{
+	List	   *namelist;
+	char	   *protocol_params_str = pstrdup(*newval);
+
+	if (!SplitIdentifierString(protocol_params_str, ',', &namelist))
+	{
+		/* syntax error in name list */
+		GUC_check_errdetail("List syntax is invalid.");
+		pfree(protocol_params_str);
+		list_free(namelist);
+		return false;
+	}
+
+	/*
+	 * We explicitly allow unknown parameters here (but we still warn for
+	 * them). So that it is possible to add version specific parameters to the
+	 * report_parameters list in the StartupMessage without knowing the
+	 * current server version yet.
+	 */
+	foreach_ptr(char, pname, namelist)
+	{
+		find_option(pname, false, false, WARNING);
+	}
+
+	pfree(protocol_params_str);
+	list_free(namelist);
+	return true;
+}
+
+/*
+ * GUC check_hook for report_parameters
+ */
+void
+assign_report_parameters(const char *newval, void *extra)
+{
+	List	   *namelist;
+	char	   *old_protocol_params_str = pstrdup(report_parameters);
+	char	   *protocol_params_str = pstrdup(newval);
+
+	if (!SplitIdentifierString(old_protocol_params_str, ',', &namelist))
+	{
+		elog(ERROR, "List syntax is invalid and check hook should have checked.");
+	}
+
+	foreach_ptr(char, pname, namelist)
+	{
+		struct config_generic *config = find_option(pname, false, true, ERROR);
+
+		if (config)
+			config->flags &= ~GUC_REPORT;
+	}
+
+	list_free(namelist);
+
+	if (!SplitIdentifierString(protocol_params_str, ',', &namelist))
+	{
+		elog(ERROR, "List syntax is invalid and check hook should have checked.");
+	}
+
+	foreach_ptr(char, pname, namelist)
+	{
+		struct config_generic *config = find_option(pname, false, true, ERROR);
+
+		if (!config)
+			continue;
+
+		config->flags |= GUC_REPORT;
+
+		/* force a report of this GUC */
+		guc_free(config->last_reported);
+		config->last_reported = NULL;
+		if (!(config->status & GUC_NEEDS_REPORT) && IsNormalProcessingMode())
+		{
+			config->status |= GUC_NEEDS_REPORT;
+			slist_push_head(&guc_report_list, &config->report_link);
+		}
+
+	}
+
+	pfree(old_protocol_params_str);
+	pfree(protocol_params_str);
+	list_free(namelist);
+}
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index a4b3ed0c909..60f200bcbdb 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -493,6 +493,7 @@ extern const struct config_enum_entry dynamic_shared_memory_options[];
  * GUC option variables that are exported from this module
  */
 char	   *protocol_managed_params = "";
+char	   *report_parameters = "client_encoding,DateStyle,server_encoding,server_version,session_authorization,TimeZone,application_name,IntervalStyle,is_superuser,default_transaction_read_only,integer_datetimes,standard_conforming_strings,in_hot_standby,scram_iterations";
 
 bool		log_duration = false;
 bool		Debug_print_plan = false;
@@ -1073,7 +1074,7 @@ struct config_bool ConfigureNamesBool[] =
 		{"is_superuser", PGC_INTERNAL, UNGROUPED,
 			gettext_noop("Shows whether the current user is a superuser."),
 			NULL,
-			GUC_REPORT | GUC_NO_SHOW_ALL | GUC_NO_RESET_ALL | GUC_NOT_IN_SAMPLE | GUC_DISALLOW_IN_FILE
+			GUC_NO_SHOW_ALL | GUC_NO_RESET_ALL | GUC_NOT_IN_SAMPLE | GUC_DISALLOW_IN_FILE
 		},
 		&current_role_is_superuser,
 		false,
@@ -1578,7 +1579,6 @@ struct config_bool ConfigureNamesBool[] =
 		{"default_transaction_read_only", PGC_USERSET, CLIENT_CONN_STATEMENT,
 			gettext_noop("Sets the default read-only status of new transactions."),
 			NULL,
-			GUC_REPORT
 		},
 		&DefaultXactReadOnly,
 		false,
@@ -1736,7 +1736,7 @@ struct config_bool ConfigureNamesBool[] =
 		{"integer_datetimes", PGC_INTERNAL, PRESET_OPTIONS,
 			gettext_noop("Shows whether datetimes are integer based."),
 			NULL,
-			GUC_REPORT | GUC_NOT_IN_SAMPLE | GUC_DISALLOW_IN_FILE
+			GUC_NOT_IN_SAMPLE | GUC_DISALLOW_IN_FILE
 		},
 		&integer_datetimes,
 		true,
@@ -1777,7 +1777,6 @@ struct config_bool ConfigureNamesBool[] =
 		{"standard_conforming_strings", PGC_USERSET, COMPAT_OPTIONS_PREVIOUS,
 			gettext_noop("Causes '...' strings to treat backslashes literally."),
 			NULL,
-			GUC_REPORT
 		},
 		&standard_conforming_strings,
 		true,
@@ -1838,7 +1837,7 @@ struct config_bool ConfigureNamesBool[] =
 		{"in_hot_standby", PGC_INTERNAL, PRESET_OPTIONS,
 			gettext_noop("Shows whether hot standby is currently active."),
 			NULL,
-			GUC_REPORT | GUC_NOT_IN_SAMPLE | GUC_DISALLOW_IN_FILE
+			GUC_NOT_IN_SAMPLE | GUC_DISALLOW_IN_FILE
 		},
 		&in_hot_standby_guc,
 		false,
@@ -3561,7 +3560,6 @@ struct config_int ConfigureNamesInt[] =
 		{"scram_iterations", PGC_USERSET, CONN_AUTH_AUTH,
 			gettext_noop("Sets the iteration count for SCRAM secret generation."),
 			NULL,
-			GUC_REPORT
 		},
 		&scram_sha_256_iterations,
 		SCRAM_SHA_256_DEFAULT_ITERATIONS, 1, INT_MAX,
@@ -3868,6 +3866,16 @@ struct config_string ConfigureNamesString[] =
 		"",
 		check_protocol_managed_params, assign_protocol_managed_params, NULL
 	},
+	{
+		{"_pq_.report_parameters", PGC_PROTOCOL, PROTOCOL_EXTENSION,
+			gettext_noop("List of parameters for which changes should be reported using ParameterStatus."),
+			NULL,
+			GUC_LIST_INPUT | GUC_NO_SHOW_ALL | GUC_NOT_IN_SAMPLE
+		},
+		&report_parameters,
+		"client_encoding,DateStyle,server_encoding,server_version,session_authorization,TimeZone,application_name,IntervalStyle,is_superuser,default_transaction_read_only,integer_datetimes,standard_conforming_strings,in_hot_standby,scram_iterations",
+		check_report_parameters, assign_report_parameters, NULL
+	},
 	{
 		{"archive_command", PGC_SIGHUP, WAL_ARCHIVING,
 			gettext_noop("Sets the shell command that will be called to archive a WAL file."),
@@ -3999,7 +4007,7 @@ struct config_string ConfigureNamesString[] =
 		{"client_encoding", PGC_USERSET, CLIENT_CONN_LOCALE,
 			gettext_noop("Sets the client's character set encoding."),
 			NULL,
-			GUC_IS_NAME | GUC_REPORT
+			GUC_IS_NAME
 		},
 		&client_encoding_string,
 		"SQL_ASCII",
@@ -4031,7 +4039,7 @@ struct config_string ConfigureNamesString[] =
 			gettext_noop("Sets the display format for date and time values."),
 			gettext_noop("Also controls interpretation of ambiguous "
 						 "date inputs."),
-			GUC_LIST_INPUT | GUC_REPORT
+			GUC_LIST_INPUT
 		},
 		&datestyle_string,
 		"ISO, MDY",
@@ -4207,7 +4215,7 @@ struct config_string ConfigureNamesString[] =
 		{"server_encoding", PGC_INTERNAL, PRESET_OPTIONS,
 			gettext_noop("Shows the server (database) character set encoding."),
 			NULL,
-			GUC_IS_NAME | GUC_REPORT | GUC_NOT_IN_SAMPLE | GUC_DISALLOW_IN_FILE
+			GUC_IS_NAME | GUC_NOT_IN_SAMPLE | GUC_DISALLOW_IN_FILE
 		},
 		&server_encoding_string,
 		"SQL_ASCII",
@@ -4219,7 +4227,7 @@ struct config_string ConfigureNamesString[] =
 		{"server_version", PGC_INTERNAL, PRESET_OPTIONS,
 			gettext_noop("Shows the server version."),
 			NULL,
-			GUC_REPORT | GUC_NOT_IN_SAMPLE | GUC_DISALLOW_IN_FILE
+			GUC_NOT_IN_SAMPLE | GUC_DISALLOW_IN_FILE
 		},
 		&server_version_string,
 		PG_VERSION,
@@ -4243,7 +4251,7 @@ struct config_string ConfigureNamesString[] =
 		{"session_authorization", PGC_USERSET, UNGROUPED,
 			gettext_noop("Sets the session user name."),
 			NULL,
-			GUC_IS_NAME | GUC_REPORT | GUC_NO_SHOW_ALL | GUC_NO_RESET_ALL | GUC_NOT_IN_SAMPLE | GUC_DISALLOW_IN_FILE | GUC_NOT_WHILE_SEC_REST
+			GUC_IS_NAME | GUC_NO_SHOW_ALL | GUC_NO_RESET_ALL | GUC_NOT_IN_SAMPLE | GUC_DISALLOW_IN_FILE | GUC_NOT_WHILE_SEC_REST
 		},
 		&session_authorization_string,
 		NULL,
@@ -4310,7 +4318,6 @@ struct config_string ConfigureNamesString[] =
 		{"TimeZone", PGC_USERSET, CLIENT_CONN_LOCALE,
 			gettext_noop("Sets the time zone for displaying and interpreting time stamps."),
 			NULL,
-			GUC_REPORT
 		},
 		&timezone_string,
 		"GMT",
@@ -4560,7 +4567,7 @@ struct config_string ConfigureNamesString[] =
 		{"application_name", PGC_USERSET, LOGGING_WHAT,
 			gettext_noop("Sets the application name to be reported in statistics and logs."),
 			NULL,
-			GUC_IS_NAME | GUC_REPORT | GUC_NOT_IN_SAMPLE
+			GUC_IS_NAME | GUC_NOT_IN_SAMPLE
 		},
 		&application_name,
 		"",
@@ -4720,7 +4727,6 @@ struct config_enum ConfigureNamesEnum[] =
 		{"IntervalStyle", PGC_USERSET, CLIENT_CONN_LOCALE,
 			gettext_noop("Sets the display format for interval values."),
 			NULL,
-			GUC_REPORT
 		},
 		&IntervalStyle,
 		INTSTYLE_POSTGRES, intervalstyle_options,
diff --git a/src/include/utils/guc.h b/src/include/utils/guc.h
index 45479550d10..6a91de8a683 100644
--- a/src/include/utils/guc.h
+++ b/src/include/utils/guc.h
@@ -243,6 +243,7 @@ typedef enum
 
 /* GUC vars that are actually defined in guc_tables.c, rather than elsewhere */
 extern PGDLLIMPORT char *protocol_managed_params;
+extern PGDLLIMPORT char *report_parameters;
 
 extern PGDLLIMPORT bool Debug_print_plan;
 extern PGDLLIMPORT bool Debug_print_parse;
diff --git a/src/include/utils/guc_hooks.h b/src/include/utils/guc_hooks.h
index 04938b4d777..691aea557da 100644
--- a/src/include/utils/guc_hooks.h
+++ b/src/include/utils/guc_hooks.h
@@ -119,6 +119,8 @@ extern void assign_recovery_target_timeline(const char *newval, void *extra);
 extern bool check_recovery_target_xid(char **newval, void **extra,
 									  GucSource source);
 extern void assign_recovery_target_xid(const char *newval, void *extra);
+extern bool check_report_parameters(char **newval, void **extra, GucSource source);
+extern void assign_report_parameters(const char *newval, void *extra);
 extern bool check_role(char **newval, void **extra, GucSource source);
 extern void assign_role(const char *newval, void *extra);
 extern const char *show_role(void);
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index e28121a34a7..a05f4750216 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -363,6 +363,10 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
 		"Pq-Protocol-Managed-Params", "", 40,
 	offsetof(struct pg_conn, pq_protocol_managed_params)},
 
+	{"_pq_.report_parameters", NULL, NULL, NULL,
+		"Pq-Protocol-Managed-Params", NULL, 40,
+	offsetof(struct pg_conn, pq_report_parameters)},
+
 	/* Terminating entry --- MUST BE LAST */
 	{NULL, NULL, NULL, NULL,
 	NULL, NULL, 0}
diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c
index 13e88be0d8f..d486a575084 100644
--- a/src/interfaces/libpq/fe-protocol3.c
+++ b/src/interfaces/libpq/fe-protocol3.c
@@ -2322,6 +2322,9 @@ build_startup_packet(const PGconn *conn, char *packet,
 	if (conn->pq_protocol_managed_params && conn->pq_protocol_managed_params[0])
 		ADD_STARTUP_OPTION("_pq_.protocol_managed_params", conn->pq_protocol_managed_params);
 
+	if (conn->pq_report_parameters)
+		ADD_STARTUP_OPTION("_pq_.report_parameters", conn->pq_report_parameters);
+
 	/* Add trailing terminator */
 	if (packet)
 		packet[packet_len] = '\0';
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index 58c34fe2c21..f6331a6f51b 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -410,6 +410,7 @@ struct pg_conn
 	char	   *require_auth;	/* name of the expected auth method */
 	char	   *load_balance_hosts; /* load balance over hosts */
 	char	   *pq_protocol_managed_params; /* _pq_.protocol_managed_params */
+	char	   *pq_report_parameters;	/* _pq_.report_parameters */
 
 	/* Optional file to write trace info to */
 	FILE	   *Pfdebug;
diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c
index 6abaab45e93..57e5a4bc624 100644
--- a/src/test/modules/libpq_pipeline/libpq_pipeline.c
+++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c
@@ -1531,6 +1531,76 @@ test_pipeline_idle(PGconn *conn)
 	fprintf(stderr, "ok - 2\n");
 }
 
+static void
+test_report_parameters(PGconn *conn)
+{
+	PGresult   *res = NULL;
+	const char *val = NULL;
+
+	res = PQexec(conn, "SET application_name = 'test1'");
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		pg_fatal("failed to set parameter: %s", PQerrorMessage(conn));
+
+	val = PQparameterStatus(conn, "application_name");
+	if (strcmp(val, "test1") != 0)
+		pg_fatal("expected application_name to tracked as 'test1', but was '%s'", val);
+
+	res = PQparameterSet(conn, "application_name", "test2");
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		pg_fatal("failed to set parameter: %s", PQerrorMessage(conn));
+
+	val = PQparameterStatus(conn, "application_name");
+	if (strcmp(val, "test2") != 0)
+		pg_fatal("expected application_name to tracked as 'test2', but was '%s'", val);
+
+	res = PQparameterSet(conn, "_pq_.report_parameters", "lock_timeout");
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		pg_fatal("failed to set parameter: %s", PQerrorMessage(conn));
+
+	/* Should have automatically received initial value */
+	val = PQparameterStatus(conn, "lock_timeout");
+	if (strcmp(val, "0") != 0)
+		pg_fatal("expected application_name to tracked as '123000', but was '%s'", val);
+
+	/*
+	 * Add some more parameters to track, including _pq_.report_parameters
+	 * itself.
+	 */
+	res = PQparameterSet(conn, "_pq_.report_parameters", "lock_timeout,_pq_.report_parameters,work_mem");
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		pg_fatal("failed to set parameter: %s", PQerrorMessage(conn));
+
+	val = PQparameterStatus(conn, "lock_timeout");
+	if (strcmp(val, "0") != 0)
+		pg_fatal("expected application_name to tracked as '123000', but was '%s'", val);
+
+	val = PQparameterStatus(conn, "_pq_.report_parameters");
+	if (strcmp(val, "lock_timeout,_pq_.report_parameters,work_mem") != 0)
+		pg_fatal("_pq_.report_parameters was an unexpected value '%s'", val);
+
+	val = PQparameterStatus(conn, "work_mem");
+	if (strcmp(val, "4096") != 0)
+		pg_fatal("expected work_mem to be tracked as '4096', but was '%s'", val);
+
+	/* changes to application_name should not be tracked anymore */
+	res = PQparameterSet(conn, "application_name", "test3333");
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		pg_fatal("failed to set parameter: %s", PQerrorMessage(conn));
+
+	val = PQparameterStatus(conn, "application_name");
+	if (strcmp(val, "test2") != 0)
+		pg_fatal("expected application_name to tracked as 'test2', but was '%s'", val);
+
+	/* changes to and work_mem lock_timeout should be tracked */
+	res = PQparameterSet(conn, "lock_timeout", "123s");
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		pg_fatal("failed to set parameter: %s", PQerrorMessage(conn));
+
+	val = PQparameterStatus(conn, "lock_timeout");
+	if (strcmp(val, "123000") != 0)
+		pg_fatal("expected application_name to tracked as '123000', but was '%s'", val);
+}
+
 static void
 test_simple_pipeline(PGconn *conn)
 {
@@ -2161,6 +2231,7 @@ print_test_list(void)
 	printf("pipeline_idle\n");
 	printf("pipelined_insert\n");
 	printf("prepared\n");
+	printf("report_parameters\n");
 	printf("simple_pipeline\n");
 	printf("singlerow\n");
 	printf("transaction\n");
@@ -2271,6 +2342,8 @@ main(int argc, char **argv)
 		test_pipelined_insert(conn, numrows);
 	else if (strcmp(testname, "prepared") == 0)
 		test_prepared(conn);
+	else if (strcmp(testname, "report_parameters") == 0)
+		test_report_parameters(conn);
 	else if (strcmp(testname, "simple_pipeline") == 0)
 		test_simple_pipeline(conn);
 	else if (strcmp(testname, "singlerow") == 0)
diff --git a/src/test/modules/libpq_pipeline/t/001_libpq_pipeline.pl b/src/test/modules/libpq_pipeline/t/001_libpq_pipeline.pl
index eec5d954bf0..c08b3c5f6bc 100644
--- a/src/test/modules/libpq_pipeline/t/001_libpq_pipeline.pl
+++ b/src/test/modules/libpq_pipeline/t/001_libpq_pipeline.pl
@@ -39,7 +39,7 @@ for my $testname (@tests)
 	my $cmptrace = grep(/^$testname$/,
 		qw(simple_pipeline nosync multi_pipelines parameter_set prepared singlerow
 		  pipeline_abort pipeline_idle transaction
-		  disallowed_in_pipeline)) > 0;
+		  disallowed_in_pipeline report_paramters)) > 0;
 
 	# For a bunch of tests, generate a libpq trace file too.
 	my $traceout =
diff --git a/src/test/modules/libpq_pipeline/traces/report_parameters.trace b/src/test/modules/libpq_pipeline/traces/report_parameters.trace
new file mode 100644
index 00000000000..a9f6b5039ea
--- /dev/null
+++ b/src/test/modules/libpq_pipeline/traces/report_parameters.trace
@@ -0,0 +1,31 @@
+F	35	Query	 "SET application_name = 'test1'"
+B	8	CommandComplete	 "SET"
+B	27	ParameterStatus	 "application_name" "test1"
+B	5	ReadyForQuery	 I
+F	27	ParameterSet	 "application_name" "test2"
+F	4	Sync
+B	4	ParameterSetComplete
+B	27	ParameterStatus	 "application_name" "test2"
+B	5	ReadyForQuery	 I
+F	40	ParameterSet	 "_pq_.report_parameters" "lock_timeout"
+F	4	Sync
+B	4	ParameterSetComplete
+B	19	ParameterStatus	 "lock_timeout" "0"
+B	5	ReadyForQuery	 I
+F	72	ParameterSet	 "_pq_.report_parameters" "lock_timeout,_pq_.report_parameters,work_mem"
+F	4	Sync
+B	4	ParameterSetComplete
+B	18	ParameterStatus	 "work_mem" "4096"
+B	72	ParameterStatus	 "_pq_.report_parameters" "lock_timeout,_pq_.report_parameters,work_mem"
+B	19	ParameterStatus	 "lock_timeout" "0"
+B	5	ReadyForQuery	 I
+F	30	ParameterSet	 "application_name" "test3333"
+F	4	Sync
+B	4	ParameterSetComplete
+B	5	ReadyForQuery	 I
+F	22	ParameterSet	 "lock_timeout" "123s"
+F	4	Sync
+B	4	ParameterSetComplete
+B	24	ParameterStatus	 "lock_timeout" "123000"
+B	5	ReadyForQuery	 I
+F	4	Terminate
-- 
2.34.1

