From 0d76476cd9305d95ae1b871e0f6ed0b97737f6e8 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 v12 14/14] 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 4e06b6ea446..9653a5dd008 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -11318,6 +11318,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 29799ef86a9..77d0f873201 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -2590,7 +2590,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"/>.
+       The parameters that are reported by default as of the current release include:
        <simplelist type="vert" columns="2">
         <member><varname>application_name</varname></member>
         <member><varname>client_encoding</varname></member>
diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml
index fdab4e8e287..b83f2b806a3 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:
     <simplelist type="vert" columns="2">
      <member><varname>application_name</varname></member>
      <member><varname>client_encoding</varname></member>
@@ -1363,7 +1364,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 263dc7ab7ae..b3ba08d05d5 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -2621,8 +2621,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);
 	}
@@ -7074,3 +7082,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 8028b8d1ecb..10d5e09eb85 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -496,6 +496,7 @@ extern const struct config_enum_entry dynamic_shared_memory_options[];
  */
 bool		AllowAlterSystem = true;
 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;
@@ -1039,7 +1040,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,
@@ -1560,7 +1561,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,
@@ -1718,7 +1718,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,
@@ -1759,7 +1759,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,
@@ -1820,7 +1819,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,
@@ -3654,7 +3653,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,
@@ -3961,6 +3959,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."),
@@ -4092,7 +4100,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",
@@ -4124,7 +4132,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",
@@ -4300,7 +4308,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",
@@ -4312,7 +4320,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,
@@ -4336,7 +4344,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,
@@ -4403,7 +4411,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",
@@ -4653,7 +4660,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,
 		"",
@@ -4827,7 +4834,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 3fc92d4cced..1ea5640e15f 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 ebe048b5561..3ada4d18ed6 100644
--- a/src/include/utils/guc_hooks.h
+++ b/src/include/utils/guc_hooks.h
@@ -126,6 +126,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 b0bc1efe4dc..28b3f95859d 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -368,6 +368,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-Report-Parameters", "", 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 ee9205494d7..f8277e6c890 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 3fad5f09ac7..d8750c56c66 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -411,6 +411,7 @@ struct pg_conn
 	char	   *load_balance_hosts; /* load balance over hosts */
 	char	   *max_protocol_version;	/* maximum used protocol version */
 	char	   *pq_protocol_managed_params; /* _pq_.protocol_managed_params */
+	char	   *pq_report_parameters;	/* _pq_.report_parameters */
 
 	bool		cancelRequest;	/* true if this connection is used to send a
 								 * cancel request, instead of being a normal
diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c
index 9d76a2db297..8949d28a358 100644
--- a/src/test/modules/libpq_pipeline/libpq_pipeline.c
+++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c
@@ -1893,6 +1893,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)
 {
@@ -2524,6 +2594,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");
@@ -2636,6 +2707,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

