From 01b1273d69399b26c06fb731bc048c2ce4e9b3e4 Mon Sep 17 00:00:00 2001
From: Jelte Fennema-Nio <jelte.fennema@microsoft.com>
Date: Fri, 5 Jan 2024 14:45:04 +0100
Subject: [PATCH v13 8/9] Add infrastructure for protocol parameters

Since the introduction of the NegotiateProtocolParameter message the
server has been able to say to the client that it doesn't support any of
the protocol parameters that the client requested.

While this is important for backwards compatibility, it doesn't give
much guidance for people wanting to add new protocol parameters. This
commit intends to change that by adding a generic infrastructure that
can be used to introduce new protocol parameters in a standardized way.

There are two key features that are necessary to actually be able to use
protocol parameters:

1. Negotiating the valid values of a protocol parameter (e.g. what
   compression methods are supported). This is needed because we want to
   support protocol parameters without adding an extra round-trip to the
   connection startup. So, a server needs to be able to accept the data
   in the StartupMessage, while also sharing with the client what it
   actually accepts in its response.
2. Changing a protocol parameter after connection startup. This is
   critical for connection poolers, otherwise they would need to
   separate connections with different values for the protocol
   parameters.

To support these two features this commit adds three new protocol
messages, including their code to handle these messages client and
server side:

1. NegotiateProtocolParameter (BE): Sent during connection startup when the
   server supports the protocol parameter. This tells the client if the
   server accepted the value that the client provided for the parameter.
   It also tells the client what other values it accepts.
2. SetProtocolParameter (FE): Can be used to change protocol parameters after
   the connection startup.
3. SetProtocolParameterComplete (BE): Response to SetProtocolParameter
   which tells the client if the new value was accepted or not.
---
 doc/src/sgml/protocol.sgml              | 203 +++++++++++++++++++++++-
 src/backend/libpq/Makefile              |   3 +-
 src/backend/libpq/meson.build           |   1 +
 src/backend/libpq/protocol-parameters.c | 127 +++++++++++++++
 src/backend/postmaster/postmaster.c     |   1 +
 src/backend/tcop/backend_startup.c      |  35 +++-
 src/backend/tcop/postgres.c             |  31 ++++
 src/backend/utils/init/postinit.c       |  15 ++
 src/include/libpq/libpq-be.h            |  55 +++++++
 src/include/libpq/protocol.h            |   3 +
 src/interfaces/libpq/fe-connect.c       |  21 ++-
 src/interfaces/libpq/fe-exec.c          |  76 +++++++++
 src/interfaces/libpq/fe-protocol3.c     | 171 ++++++++++++++++++++
 src/interfaces/libpq/fe-trace.c         |  44 ++++-
 src/interfaces/libpq/libpq-int.h        |   4 +-
 src/tools/pgindent/typedefs.list        |   1 +
 16 files changed, 777 insertions(+), 14 deletions(-)
 create mode 100644 src/backend/libpq/protocol-parameters.c

diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml
index 1b27d0a5479..8d33621a9fe 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -210,7 +210,7 @@
    appear in <xref linkend="protocol-message-formats"/>.)  There are
    several different sub-protocols depending on the state of the
    connection: start-up, query, function call,
-   <command>COPY</command>, and termination.  There are also special
+   <command>COPY</command>, protocol parameter, and termination.  There are also special
    provisions for asynchronous operations (including notification
    responses and command cancellation), which can occur at any time
    after the start-up phase.
@@ -405,8 +405,22 @@
         this message indicates the highest supported minor version.  This
         message will also be sent if the client requested unsupported protocol
         options (i.e., beginning with <literal>_pq_.</literal>) in the
-        startup packet.  This message will be followed by an ErrorResponse or
-        a message indicating the success or failure of authentication.
+        startup packet.  This message will be followed by an ErrorResponse, a
+        NegotiateProtocolParameter or a message indicating the success or
+        failure of authentication.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term>NegotiateProtocolParameter</term>
+      <listitem>
+       <para>
+        The server supports the requested protocol parameter. This message lets
+        the client know to which value the server has set the parameter, which
+        may be different than the value requested by the client. It also tells
+        the client what values it accepts for future SetProtocolParameter
+        messages involving this parameter.
        </para>
       </listitem>
      </varlistentry>
@@ -1669,6 +1683,49 @@ SELCT 1/0;<!-- this typo is intentional -->
     of authentication checking.
    </para>
   </sect2>
+
+  <sect2 id="protocol-parameters">
+   <title>Protocol parameters</title>
+   <para>
+    The behaviour of the protocol can be modified by configuring protocol
+    parameters in the StartupMessage. A '<literal>_pq_.</literal>' prefix needs
+    to be added to indicate to the server that this is a protocol parameter,
+    and not a regular parameter. It's optional for a server to implement
+    support for these protocol parameters, so a client is expected to
+    gracefully fallback to not using the feature that these parameters might
+    enable when the server indicates non-support using NegotiateProtocolVersion
+    or NegotiateProtocolParameter.
+   </para>
+
+   <para>
+    Since protocol version 3.2, it is possible to change protocol parameters
+    after the StartupMessage by using the SetProtocolParameter. The server will
+    respond with a SetProtocolParameterComplete message of result type
+    '<literal>S</literal>', followed by a ReadyForQuery message. On most failures
+    (e.g. syntax errors in the value) the server will respond with an
+    SetProtocolParameterComplete message of result type '<literal>E</literal>',
+    followed by a ReadyForQuery message. The reason for these failures can be
+    found in the NoticeResponse message that precedes the
+    SetProtocolParameterComplete message. This is not using an ErrorResponse,
+    to avoid rolling back a possibly in progress transaction. However, in some
+    cases (e.g. out of memory, or unknown parameter) the server will still
+    respond with an ErrorResponse message, again followed by a ReadyForQuery
+    message.
+   </para>
+
+   <note>
+    <para>
+     The SetProtocolParameter message is not part of the
+     extended query protocol and thus cannot be sent as part of an already
+     started extended query pipeline. Though it is allowed to have multiple
+     SetProtocolParameter messages in flight at the same time.
+    </para>
+   </note>
+
+   <variablelist>
+
+   </variablelist>
+  </sect2>
  </sect1>
 
  <sect1 id="sasl-authentication">
@@ -5136,6 +5193,50 @@ psql "dbname=postgres replication=database" -c "IDENTIFY_SYSTEM;"
     </listitem>
    </varlistentry>
 
+   <varlistentry id="protocol-message-formats-NegotiateProtocolParameter">
+    <term>NegotiateProtocolParameter (B)</term>
+    <listitem>
+     <variablelist>
+      <varlistentry>
+       <term>Byte1('P')</term>
+       <listitem>
+        <para>
+         Identifies the message as a protocol parameter negotiation message.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int32</term>
+       <listitem>
+        <para>
+         Length of message contents in bytes, including self.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>String</term>
+       <listitem>
+        <para>
+         The name of the protocol parameter that the client attempted to set.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>String</term>
+       <listitem>
+        <para>
+         The new value of the protocol parameter.
+        </para>
+       </listitem>
+      </varlistentry>
+     </variablelist>
+    </listitem>
+   </varlistentry>
+
+
    <varlistentry id="protocol-message-formats-NoData">
     <term>NoData (B)</term>
     <listitem>
@@ -5363,6 +5464,102 @@ psql "dbname=postgres replication=database" -c "IDENTIFY_SYSTEM;"
     </listitem>
    </varlistentry>
 
+   <varlistentry id="protocol-message-formats-SetProtocolParameter">
+    <term>SetProtocolParameter (F)</term>
+    <listitem>
+     <variablelist>
+      <varlistentry>
+       <term>Byte1('O')</term>
+       <listitem>
+        <para>
+         Identifies the message as a protocol parameter change.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int32</term>
+       <listitem>
+        <para>
+         Length of message contents in bytes, including self.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>String</term>
+       <listitem>
+        <para>
+         The name of the protocol parameter to change.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>String</term>
+       <listitem>
+        <para>
+         The new value of the parameter.
+        </para>
+       </listitem>
+      </varlistentry>
+     </variablelist>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry id="protocol-message-formats-SetProtocolParameterComplete">
+    <term>SetProtocolParameterComplete (B)</term>
+    <listitem>
+     <variablelist>
+      <varlistentry>
+       <term>Byte1('O')</term>
+       <listitem>
+        <para>
+         Identifies the message as a SetProtocolParameter-complete indicator.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int32</term>
+       <listitem>
+        <para>
+         Length of message contents in bytes, including self.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>String</term>
+       <listitem>
+        <para>
+         The name of the protocol parameter that the client attempted to set.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>String</term>
+       <listitem>
+        <para>
+         The new value of the protocol parameter.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Byte1</term>
+       <listitem>
+        <para>
+         Result type of the request. The possible values '<literal>S</literal>' if the
+         value was changed successfully; '<literal>E</literal>' if the requested value could not be set;
+        </para>
+       </listitem>
+      </varlistentry>
+     </variablelist>
+    </listitem>
+   </varlistentry>
+
    <varlistentry id="protocol-message-formats-Parse">
     <term>Parse (F)</term>
     <listitem>
diff --git a/src/backend/libpq/Makefile b/src/backend/libpq/Makefile
index 6d385fd6a45..fb8457b7537 100644
--- a/src/backend/libpq/Makefile
+++ b/src/backend/libpq/Makefile
@@ -27,7 +27,8 @@ OBJS = \
 	pqcomm.o \
 	pqformat.o \
 	pqmq.o \
-	pqsignal.o
+	pqsignal.o \
+	protocol-parameters.o
 
 ifeq ($(with_ssl),openssl)
 OBJS += be-secure-openssl.o
diff --git a/src/backend/libpq/meson.build b/src/backend/libpq/meson.build
index 7c65314512c..99d7fd39dc2 100644
--- a/src/backend/libpq/meson.build
+++ b/src/backend/libpq/meson.build
@@ -14,6 +14,7 @@ backend_sources += files(
   'pqformat.c',
   'pqmq.c',
   'pqsignal.c',
+  'protocol-parameters.c',
 )
 
 if ssl.found()
diff --git a/src/backend/libpq/protocol-parameters.c b/src/backend/libpq/protocol-parameters.c
new file mode 100644
index 00000000000..4a41407d11b
--- /dev/null
+++ b/src/backend/libpq/protocol-parameters.c
@@ -0,0 +1,127 @@
+/*-------------------------------------------------------------------------
+ *
+ * protocol-parameters.c
+ *	  Routines to handle parsing and changing of protocol parameters.
+ *
+ * Portions Copyright (c) 2024, PostgreSQL Global Development Group
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/libpq/protocol-parameters.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "libpq/libpq-be.h"
+#include "libpq/pqformat.h"
+#include "tcop/tcopprot.h"
+#include "utils/memutils.h"
+
+static void SendSetProtocolParameterComplete(ProtocolParameter *param, bool error);
+
+
+static MemoryContext ProtocolParameterMemoryContext;
+
+struct ProtocolParameter SupportedProtocolParameters[] = {
+};
+
+ProtocolParameter *
+find_protocol_parameter(const char *name)
+{
+	for (ProtocolParameter *param = SupportedProtocolParameters; param->name; param++)
+	{
+		if (strcmp(param->name, name) == 0)
+		{
+			return param;
+		}
+	}
+	return NULL;
+}
+
+void
+init_protocol_parameter(ProtocolParameter *param, const char *value)
+{
+	const char *new_value = param->handler(param, value);
+
+	/* If the handler returns NULL, use the default */
+	if (!new_value)
+		new_value = param->value;
+
+	if (!ProtocolParameterMemoryContext)
+		ProtocolParameterMemoryContext = AllocSetContextCreate(TopMemoryContext,
+															   "ProtocolParameterMemoryContext",
+															   ALLOCSET_DEFAULT_SIZES);
+
+	param->value = MemoryContextStrdup(ProtocolParameterMemoryContext, new_value);
+
+	param->requested = true;
+}
+
+
+void
+set_protocol_parameter(const char *name, const char *value)
+{
+	ProtocolParameter *param = find_protocol_parameter(name);
+	const char *new_value;
+
+	if (!param)
+	{
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("unrecognized protocol parameter \"%s\"", name)));
+	}
+	if (!param->requested)
+	{
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("protocol parameter \"%s\" was not requested during connection startup", name)));
+	}
+	new_value = param->handler(param, value);
+
+	if (new_value)
+	{
+		char	   *copy = MemoryContextStrdup(ProtocolParameterMemoryContext, new_value);
+
+		pfree(param->value);
+		param->value = copy;
+	}
+
+	if (whereToSendOutput == DestRemote)
+		SendSetProtocolParameterComplete(param, !new_value);
+}
+
+/*
+ * Send a NegotiateProtocolParameter message to the client. This lets the
+ * client know what values are accepted when changing the given parameter in
+ * the future, as well as the parameter its current value.
+ */
+void
+SendNegotiateProtocolParameter(ProtocolParameter *param)
+{
+	StringInfoData buf;
+
+	pq_beginmessage(&buf, PqMsg_NegotiateProtocolParameter);
+	pq_sendstring(&buf, param->name);
+	pq_sendstring(&buf, param->value);
+	if (param->supported_string)
+		pq_sendstring(&buf, param->supported_string);
+	else
+		pq_sendstring(&buf, param->supported_handler());
+	pq_endmessage(&buf);
+
+	/* no need to flush, some other message will follow */
+}
+
+static void
+SendSetProtocolParameterComplete(ProtocolParameter *param, bool error)
+{
+	StringInfoData buf;
+
+	pq_beginmessage(&buf, PqMsg_SetProtocolParameterComplete);
+	pq_sendstring(&buf, param->name);
+	pq_sendstring(&buf, param->value);
+	pq_sendbyte(&buf, error ? 'E' : 'S');
+	pq_endmessage(&buf);
+}
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index bf0241aed0c..59f48fe312e 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -118,6 +118,7 @@
 #include "tcop/backend_startup.h"
 #include "tcop/tcopprot.h"
 #include "utils/datetime.h"
+#include "utils/guc_tables.h"
 #include "utils/memutils.h"
 #include "utils/pidfile.h"
 #include "utils/timestamp.h"
diff --git a/src/backend/tcop/backend_startup.c b/src/backend/tcop/backend_startup.c
index cbb3342a821..29ef0c1d72e 100644
--- a/src/backend/tcop/backend_startup.c
+++ b/src/backend/tcop/backend_startup.c
@@ -33,6 +33,7 @@
 #include "tcop/backend_startup.h"
 #include "tcop/tcopprot.h"
 #include "utils/builtins.h"
+#include "utils/guc_tables.h"
 #include "utils/memutils.h"
 #include "utils/ps_status.h"
 #include "utils/timeout.h"
@@ -697,6 +698,7 @@ ProcessStartupPacket(Port *port, bool ssl_done, bool gss_done)
 	{
 		int32		offset = sizeof(ProtocolVersion);
 		List	   *unrecognized_protocol_options = NIL;
+		List	   *preauth_protocol_parameters = NIL;
 
 		/*
 		 * Scan packet body for name/option pairs.  We can assume any string
@@ -748,13 +750,27 @@ 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.
-				 */
-				unrecognized_protocol_options =
-					lappend(unrecognized_protocol_options, pstrdup(nameptr));
+				ProtocolParameter *param = find_protocol_parameter(&nameptr[5]);
+
+				if (!param)
+				{
+					/*
+					 * We report unknown protocol extensions using the
+					 * NegotiateProtocolVersion message instead of erroring
+					 */
+					unrecognized_protocol_options =
+						lappend(unrecognized_protocol_options, pstrdup(nameptr));
+				}
+				else if (param->preauth)
+				{
+					init_protocol_parameter(param, valptr);
+					preauth_protocol_parameters = lappend(preauth_protocol_parameters, param);
+				}
+				else
+				{
+					port->protocol_parameter = lappend(port->protocol_parameter, param);
+					port->protocol_parameter_values = lappend(port->protocol_parameter_values, pstrdup(valptr));
+				}
 			}
 			else
 			{
@@ -795,6 +811,11 @@ ProcessStartupPacket(Port *port, bool ssl_done, bool gss_done)
 		 */
 		if (proto > PG_PROTOCOL_LATEST || unrecognized_protocol_options != NIL)
 			SendNegotiateProtocolVersion(unrecognized_protocol_options);
+
+		foreach_ptr(ProtocolParameter, param, preauth_protocol_parameters)
+		{
+			SendNegotiateProtocolParameter(param);
+		}
 	}
 
 	/* Check a user name was given. */
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 45a3794b8e3..93aafa5792a 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -72,6 +72,7 @@
 #include "tcop/tcopprot.h"
 #include "tcop/utility.h"
 #include "utils/guc_hooks.h"
+#include "utils/guc_tables.h"
 #include "utils/injection_point.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
@@ -418,6 +419,20 @@ SocketBackend(StringInfo inBuf)
 			ignore_till_sync = false;
 			break;
 
+		case PqMsg_SetProtocolParameter:
+			maxmsglen = PQ_LARGE_MESSAGE_LIMIT;
+			if (FrontendProtocol < PG_PROTOCOL(3, 2))
+				ereport(FATAL,
+						(errcode(ERRCODE_PROTOCOL_VIOLATION),
+						 errmsg("SetProtocolParameter requires protocol version 3.2 or later, but the connection uses %d.%d",
+								FrontendProtocol >> 16, FrontendProtocol & 0xFFFF)));
+			if (doing_extended_query_message)
+				ereport(FATAL,
+						(errcode(ERRCODE_PROTOCOL_VIOLATION),
+						 errmsg("unexpected SetProtocolParameter message during extended query protocol")));
+			doing_extended_query_message = false;
+			break;
+
 		case PqMsg_Bind:
 		case PqMsg_Parse:
 			maxmsglen = PQ_LARGE_MESSAGE_LIMIT;
@@ -4878,6 +4893,22 @@ PostgresMain(const char *dbname, const char *username)
 				send_ready_for_query = true;
 				break;
 
+			case PqMsg_SetProtocolParameter:
+				{
+					const char *parameter_name;
+					const char *parameter_value;
+
+					forbidden_in_wal_sender(firstchar);
+
+					parameter_name = pq_getmsgstring(&input_message);
+					parameter_value = pq_getmsgstring(&input_message);
+
+					set_protocol_parameter(parameter_name, parameter_value);
+					valgrind_report_error_query("SetProtocolParameter message");
+					send_ready_for_query = true;
+				}
+				break;
+
 				/*
 				 * 'X' means that the frontend is closing down the socket. EOF
 				 * means unexpected loss of frontend connection. Either way,
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 0805398e24d..ea881df1df1 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -1262,6 +1262,8 @@ process_startup_options(Port *port, bool am_superuser)
 {
 	GucContext	gucctx;
 	ListCell   *gucopts;
+	ListCell   *protocol_parameter;
+	ListCell   *protocol_parameter_value;
 
 	gucctx = am_superuser ? PGC_SU_BACKEND : PGC_BACKEND;
 
@@ -1314,8 +1316,21 @@ process_startup_options(Port *port, bool am_superuser)
 
 		SetConfigOption(name, value, gucctx, PGC_S_CLIENT);
 	}
+
+	/*
+	 * Process any protocol parameters.
+	 */
+	forboth(protocol_parameter, port->protocol_parameter, protocol_parameter_value, port->protocol_parameter_values)
+	{
+		ProtocolParameter *param = lfirst(protocol_parameter);
+		char	   *value = lfirst(protocol_parameter_value);
+
+		init_protocol_parameter(param, value);
+		SendNegotiateProtocolParameter(param);
+	}
 }
 
+
 /*
  * Load GUC settings from pg_db_role_setting.
  *
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 05cb1874c58..f17e74385c3 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -152,6 +152,8 @@ typedef struct Port
 	char	   *user_name;
 	char	   *cmdline_options;
 	List	   *guc_options;
+	List	   *protocol_parameter;
+	List	   *protocol_parameter_values;
 
 	/*
 	 * The startup packet application name, only used here for the "connection
@@ -239,6 +241,59 @@ typedef struct ClientSocket
 	SockAddr	raddr;			/* remote addr (client) */
 } ClientSocket;
 
+struct ProtocolParameter;
+
+typedef const char *(*ProtocolParameterHandler) (struct ProtocolParameter *param, const char *new_value);
+typedef const char *(*ProtocolParameterSupportedHandler) ();
+
+/*
+ *
+ */
+typedef struct ProtocolParameter
+{
+	/* Name of the protocol parameter without the _pq_. prefix */
+	const char *name;
+	/* Current value encoded as string, should be set to default */
+	char	   *value;
+
+	/*
+	 * Validates and parses the given string value. Returns the new
+	 * string_value if successful. This handler should be careful to not throw
+	 * an ERROR in case of invalid input, instead it should ignore the invalid
+	 * parts and return a new string containing only the valid parts. If the
+	 * string cannot be parsed at all NULL should be returned to indicate a
+	 * parsing error.
+	 *
+	 * The main reason for the handler to throw an ERROR should be for
+	 * out-of-memory errors.
+	 */
+	ProtocolParameterHandler handler;
+
+	/*
+	 * A hardcoded string, that can be used by the client to find out what
+	 * values are supported. The format of the string is parameter specific.
+	 */
+	const char *supported_string;
+
+	/*
+	 * A function that returns the supported values if a hardcoded string is
+	 * not possible.
+	 */
+	ProtocolParameterSupportedHandler supported_handler;
+
+	/* If the protocol parameter should be set before or after authentication */
+	bool		preauth;
+
+	/* If this parameter was requested by the client on connection startup */
+	bool		requested;
+
+} ProtocolParameter;
+
+extern ProtocolParameter *find_protocol_parameter(const char *name);
+extern void init_protocol_parameter(ProtocolParameter *param, const char *value);
+extern void set_protocol_parameter(const char *name, const char *value);
+extern void SendNegotiateProtocolParameter(ProtocolParameter *param);
+
 #ifdef USE_SSL
 /*
  *	Hardcoded DH parameters, used in ephemeral DH keying.  (See also
diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h
index 4b8d4403656..5adb362c9cf 100644
--- a/src/include/libpq/protocol.h
+++ b/src/include/libpq/protocol.h
@@ -31,6 +31,7 @@
 #define PqMsg_PasswordMessage		'p'
 #define PqMsg_SASLInitialResponse	'p'
 #define PqMsg_SASLResponse			'p'
+#define PqMsg_SetProtocolParameter	'O'
 
 
 /* These are the response codes sent by the backend. */
@@ -57,6 +58,8 @@
 #define PqMsg_PortalSuspended		's'
 #define PqMsg_ParameterDescription	't'
 #define PqMsg_NegotiateProtocolVersion 'v'
+#define PqMsg_NegotiateProtocolParameter 'P'
+#define PqMsg_SetProtocolParameterComplete 'O'
 
 
 /* These are the codes sent by both the frontend and backend. */
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index cc144479d62..c4d8516079e 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -3800,7 +3800,8 @@ keep_going:						/* We will come back to here until there is
 				 */
 				if (beresp != PqMsg_AuthenticationRequest &&
 					beresp != PqMsg_ErrorResponse &&
-					beresp != PqMsg_NegotiateProtocolVersion)
+					beresp != PqMsg_NegotiateProtocolVersion &&
+					beresp != PqMsg_NegotiateProtocolParameter)
 				{
 					libpq_append_conn_error(conn, "expected authentication request from server, but received %c",
 											beresp);
@@ -3957,6 +3958,24 @@ keep_going:						/* We will come back to here until there is
 					conn->inStart = conn->inCursor;
 					goto keep_going;
 				}
+				else if (beresp == PqMsg_NegotiateProtocolParameter)
+				{
+					if (pqGetNegotiateProtocolParameter(conn))
+					{
+						libpq_append_conn_error(conn, "received invalid NegotiateProtocolParameter message");
+						goto error_return;
+					}
+
+					if (conn->error_result)
+						goto error_return;
+
+					if (conn->Pfdebug)
+						pqTraceOutputMessage(conn, conn->inBuffer + conn->inStart, false);
+
+					/* OK, we read the message; mark data consumed */
+					conn->inStart = conn->inCursor;
+					goto keep_going;
+				}
 
 				/* It is an authentication request. */
 				conn->auth_req_received = true;
diff --git a/src/interfaces/libpq/fe-exec.c b/src/interfaces/libpq/fe-exec.c
index 7bdfc4c21aa..da42dd5a626 100644
--- a/src/interfaces/libpq/fe-exec.c
+++ b/src/interfaces/libpq/fe-exec.c
@@ -80,6 +80,8 @@ static bool PQexecStart(PGconn *conn);
 static PGresult *PQexecFinish(PGconn *conn);
 static int	PQsendTypedCommand(PGconn *conn, char command, char type,
 							   const char *target);
+static PGresult *PQsetProtocolParameter(PGconn *conn, const char *parameter, const char *value);
+static int	PQsendSetProtocolParameter(PGconn *conn, const char *parameter, const char *value);
 static int	check_field_number(const PGresult *res, int field_num);
 static void pqPipelineProcessQueue(PGconn *conn);
 static int	pqPipelineSyncInternal(PGconn *conn, bool immediate_flush);
@@ -3153,6 +3155,13 @@ pqCommandQueueAdvance(PGconn *conn, bool isReadyForQuery, bool gotSync)
 	if (conn->cmd_queue_head->queryclass == PGQUERY_SIMPLE && !isReadyForQuery)
 		return;
 
+	/*
+	 * If processing a set protocol parameter, we only advance the queue when
+	 * we receive the ReadyForQuery message for it.
+	 */
+	if (conn->cmd_queue_head->queryclass == PGQUERY_SET_PROTOCOL_PARAMETER && !isReadyForQuery)
+		return;
+
 	/*
 	 * If we're waiting for a SYNC, don't advance the queue until we get one.
 	 */
@@ -3405,6 +3414,73 @@ PQsendFlushRequest(PGconn *conn)
 	return 1;
 }
 
+/*
+ * PQsetProtocolParameter
+ *		Send a request for the server to change a protocol parameter.
+ *
+ * If the request was not even sent, return NULL; conn->errorMessage is set
+ * to a relevant message.
+ * If the request was sent, a new PGresult is returned (which could indicate
+ * either success or failure).  On success, the PGresult contains status
+ * PGRES_COMMAND_OK. The user is responsible for freeing the PGresult via
+ * PQclear() when done with it.
+ */
+static PGresult *
+PQsetProtocolParameter(PGconn *conn, const char *parameter, const char *value)
+{
+	if (!PQexecStart(conn))
+		return NULL;
+	if (!PQsendSetProtocolParameter(conn, parameter, value))
+		return NULL;
+	return PQexecFinish(conn);
+}
+
+/*
+ * PQsendSetProtocolParameter
+ *	 Send a request for the server to change a run-time parameter setting.
+ *
+ * Returns 1 on success and 0 on failure.
+ */
+static int
+PQsendSetProtocolParameter(PGconn *conn, const char *parameter, const char *value)
+{
+	PGcmdQueueEntry *entry = NULL;
+
+	if (!PQsendQueryStart(conn, true))
+		return 0;
+
+	entry = pqAllocCmdQueueEntry(conn);
+	if (entry == NULL)
+		return 0;				/* error msg already set */
+
+	/* construct the SetProtocolParameter message */
+	if (pqPutMsgStart(PqMsg_SetProtocolParameter, conn) < 0 ||
+		pqPuts(parameter, conn) < 0 ||
+		pqPuts(value, conn) < 0 ||
+		pqPutMsgEnd(conn) < 0)
+		goto sendFailed;
+
+	entry->queryclass = PGQUERY_SET_PROTOCOL_PARAMETER;
+
+	/*
+	 * Give the data a push.  In nonblock mode, don't complain if we're unable
+	 * to send it all; PQgetResult() will do any additional flushing needed.
+	 */
+	if (pqFlush(conn) < 0)
+		goto sendFailed;
+
+	/* OK, it's launched! */
+	pqAppendCmdQueueEntry(conn, entry);
+
+	return 1;
+
+sendFailed:
+	pqRecycleCmdQueueEntry(conn, entry);
+	/* error message should be set up already */
+	return 0;
+}
+
+
 /* ====== accessor funcs for PGresult ======== */
 
 ExecStatusType
diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c
index 5e200519190..be199621d4d 100644
--- a/src/interfaces/libpq/fe-protocol3.c
+++ b/src/interfaces/libpq/fe-protocol3.c
@@ -47,6 +47,7 @@ static void handleSyncLoss(PGconn *conn, char id, int msgLength);
 static int	getRowDescriptions(PGconn *conn, int msgLength);
 static int	getParamDescriptions(PGconn *conn, int msgLength);
 static int	getAnotherTuple(PGconn *conn, int msgLength);
+static int	getSetProtocolParameterComplete(PGconn *conn);
 static int	getParameterStatus(PGconn *conn);
 static int	getNotify(PGconn *conn);
 static int	getCopyStart(PGconn *conn, ExecStatusType copytype);
@@ -299,6 +300,12 @@ pqParseInput3(PGconn *conn)
 						}
 						conn->asyncStatus = PGASYNC_READY;
 					}
+					break;
+				case PqMsg_SetProtocolParameter:
+					if (getSetProtocolParameterComplete(conn))
+						return;
+					conn->asyncStatus = PGASYNC_READY;
+
 					break;
 				case PqMsg_ParameterStatus:
 					if (getParameterStatus(conn))
@@ -316,6 +323,16 @@ pqParseInput3(PGconn *conn)
 					if (pqGetInt(&(conn->be_key), 4, conn))
 						return;
 					break;
+				case PqMsg_NegotiateProtocolParameter:
+
+					/*
+					 * This is expected only during backend startup, but it's
+					 * just as easy to handle it as part of the main loop.
+					 * Save the data and continue processing.
+					 */
+					if (pqGetNegotiateProtocolParameter(conn))
+						return;
+					break;
 				case PqMsg_RowDescription:
 					if (conn->error_result ||
 						(conn->result != NULL &&
@@ -1485,6 +1502,160 @@ failure:
 	return 0;
 }
 
+/*
+ * Attempt to read a NegotiateProtocolParameter message.
+ * Entry: 'p' message type and length have already been consumed.
+ * Exit: returns 0 if successfully consumed message.
+ *		 returns EOF if not enough data.
+ */
+int
+pqGetNegotiateProtocolParameter(PGconn *conn)
+{
+	PQExpBufferData valueBuf;
+	PQExpBufferData supportedBuf;
+	bool		found = false;
+
+	initPQExpBuffer(&valueBuf);
+	initPQExpBuffer(&supportedBuf);
+
+	/* Get the parameter name */
+	if (pqGets(&conn->workBuffer, conn))
+		goto eof;
+	/* Get the parameter value (could be large) */
+	if (pqGets(&valueBuf, conn))
+		goto eof;
+	/* Get the supported parameter format */
+	if (pqGets(&supportedBuf, conn))
+		goto eof;
+
+	for (const pg_protocol_parameter *param = KnownProtocolParameters; param->name; param++)
+	{
+		if (strcmp(param->name, conn->workBuffer.data) == 0)
+		{
+			char	  **server_value = (char **) ((char *) conn + param->conn_server_value_offset);
+			char	   *value_copy = strdup(valueBuf.data);
+
+			if (!value_copy)
+			{
+				libpq_append_conn_error(conn, "out of memory");
+				goto failure;
+			}
+			*server_value = value_copy;
+			found = true;
+		}
+	}
+	if (!found)
+	{
+		libpq_append_conn_error(conn, "received NegotiateProtocolParameter for unknown parameter %s", valueBuf.data);
+		goto failure;
+	}
+
+	termPQExpBuffer(&valueBuf);
+	termPQExpBuffer(&supportedBuf);
+	return 0;
+
+failure:
+	conn->asyncStatus = PGASYNC_READY;
+	pqSaveErrorResult(conn);
+	termPQExpBuffer(&valueBuf);
+	termPQExpBuffer(&supportedBuf);
+	return 0;
+
+eof:
+	termPQExpBuffer(&valueBuf);
+	termPQExpBuffer(&supportedBuf);
+	return EOF;
+}
+
+/*
+ * Attempt to read a SetProtocolParameterComplete message.
+ * Entry: 'S' message type and length have already been consumed.
+ * Exit: returns 0 if successfully consumed message.
+ *		 returns EOF if not enough data.
+ */
+static int
+getSetProtocolParameterComplete(PGconn *conn)
+{
+	PQExpBufferData valueBuf;
+	char		result_code;
+	bool		found = false;
+
+	initPQExpBuffer(&valueBuf);
+
+	/* Get the parameter name */
+	if (pqGets(&conn->workBuffer, conn))
+	{
+		goto eof;
+	}
+	if (pqGets(&valueBuf, conn))
+	{
+		goto eof;
+	}
+	if (pqGetc(&result_code, conn))
+	{
+		goto eof;
+	}
+
+	for (const pg_protocol_parameter *param = KnownProtocolParameters; param->name; param++)
+	{
+		if (strcmp(param->name, conn->workBuffer.data) == 0)
+		{
+			char	  **server_value = (char **) ((char *) conn + param->conn_server_value_offset);
+
+			char	   *value_copy = strdup(valueBuf.data);
+
+			if (!value_copy)
+			{
+				libpq_append_conn_error(conn, "out of memory");
+				pqSaveErrorResult(conn);
+				goto failure;
+			}
+			free(*server_value);
+			*server_value = value_copy;
+			found = true;
+
+			break;
+		}
+	}
+
+	if (!found)
+	{
+		libpq_append_conn_error(conn, "received SetProtocolParameterComplete for unknown parameter");
+		pqSaveErrorResult(conn);
+		goto failure;
+	}
+
+	if (result_code == 'S')
+	{
+		conn->result = PQmakeEmptyPGresult(conn,
+										   PGRES_COMMAND_OK);
+	}
+	else if (result_code == 'E')
+	{
+		conn->result = PQmakeEmptyPGresult(conn,
+										   PGRES_NONFATAL_ERROR);
+	}
+	else
+	{
+		libpq_append_conn_error(conn, "received SetProtocolParameterComplete with unknown result code");
+		pqSaveErrorResult(conn);
+		goto failure;
+	}
+
+	termPQExpBuffer(&valueBuf);
+	return 0;
+
+failure:
+	conn->asyncStatus = PGASYNC_READY;
+	pqSaveErrorResult(conn);
+	termPQExpBuffer(&valueBuf);
+	return 0;
+
+eof:
+	termPQExpBuffer(&valueBuf);
+	return EOF;
+}
+
 
 /*
  * Attempt to read a ParameterStatus message.
diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c
index d976b50d702..940051303f7 100644
--- a/src/interfaces/libpq/fe-trace.c
+++ b/src/interfaces/libpq/fe-trace.c
@@ -485,6 +485,15 @@ pqTraceOutput_NegotiateProtocolVersion(FILE *f, const char *message, int *cursor
 	}
 }
 
+static void
+pqTraceOutput_NegotiateProtocolParameter(FILE *f, const char *message, int *cursor)
+{
+	fprintf(f, "NegotiateProtocolParameter\t");
+	pqTraceOutputString(f, message, cursor, false);
+	pqTraceOutputString(f, message, cursor, false);
+	pqTraceOutputString(f, message, cursor, false);
+}
+
 static void
 pqTraceOutput_FunctionCallResponse(FILE *f, const char *message, int *cursor)
 {
@@ -506,6 +515,23 @@ pqTraceOutput_CopyBothResponse(FILE *f, const char *message, int *cursor, int le
 		pqTraceOutputInt16(f, message, cursor);
 }
 
+static void
+pqTraceOutput_SetProtocolParameter(FILE *f, bool toServer, const char *message, int *cursor)
+{
+	fprintf(f, "SetProtocolParameter\t");
+	pqTraceOutputString(f, message, cursor, false);
+	pqTraceOutputString(f, message, cursor, false);
+}
+
+static void
+pqTraceOutput_SetProtocolParameterComplete(FILE *f, bool toServer, const char *message, int *cursor)
+{
+	fprintf(f, "SetProtocolParameterComplete");
+	pqTraceOutputString(f, message, cursor, false);
+	pqTraceOutputString(f, message, cursor, false);
+	pqTraceOutputByte1(f, message, cursor);
+}
+
 static void
 pqTraceOutput_ReadyForQuery(FILE *f, const char *message, int *cursor)
 {
@@ -583,6 +609,18 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer)
 			else
 				pqTraceOutput_CommandComplete(conn->Pfdebug, message, &logCursor);
 			break;
+		case PqMsg_SetProtocolParameter:
+
+			/*
+			 * SetProtocolParameter(F) and SetProtocolParameterComplete(B) use
+			 * the same identifier.
+			 */
+			Assert(PqMsg_SetProtocolParameter == PqMsg_SetProtocolParameterComplete);
+			if (toServer)
+				pqTraceOutput_SetProtocolParameter(conn->Pfdebug, toServer, message, &logCursor);
+			else
+				pqTraceOutput_SetProtocolParameterComplete(conn->Pfdebug, toServer, message, &logCursor);
+			break;
 		case PqMsg_CopyData:
 			/* Drop COPY data to reduce the overhead of logging. */
 			break;
@@ -634,7 +672,11 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer)
 			pqTraceOutput_NoticeResponse(conn->Pfdebug, message, &logCursor, regress);
 			break;
 		case PqMsg_Parse:
-			pqTraceOutput_Parse(conn->Pfdebug, message, &logCursor, regress);
+			Assert(PqMsg_Parse == PqMsg_NegotiateProtocolParameter);
+			if (toServer)
+				pqTraceOutput_Parse(conn->Pfdebug, message, &logCursor, regress);
+			else
+				pqTraceOutput_NegotiateProtocolParameter(conn->Pfdebug, message, &logCursor);
 			break;
 		case PqMsg_Query:
 			pqTraceOutput_Query(conn->Pfdebug, message, &logCursor);
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index 8b8560268f8..ebcf8279b27 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -328,7 +328,8 @@ typedef enum
 	PGQUERY_PREPARE,			/* Parse only (PQprepare) */
 	PGQUERY_DESCRIBE,			/* Describe Statement or Portal */
 	PGQUERY_SYNC,				/* Sync (at end of a pipeline) */
-	PGQUERY_CLOSE				/* Close Statement or Portal */
+	PGQUERY_CLOSE,				/* Close Statement or Portal */
+	PGQUERY_SET_PROTOCOL_PARAMETER, /* Set a protocol parameter */
 } PGQueryClass;
 
 /*
@@ -741,6 +742,7 @@ extern int	pqGetErrorNotice3(PGconn *conn, bool isError);
 extern void pqBuildErrorMessage3(PQExpBuffer msg, const PGresult *res,
 								 PGVerbosity verbosity, PGContextVisibility show_context);
 extern int	pqGetNegotiateProtocolVersion3(PGconn *conn);
+extern int	pqGetNegotiateProtocolParameter(PGconn *conn);
 extern int	pqGetCopyData3(PGconn *conn, char **buffer, int async);
 extern int	pqGetline3(PGconn *conn, char *s, int maxlen);
 extern int	pqGetlineAsync3(PGconn *conn, char *buffer, int bufsize);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 97b4e5f1e02..925905aa06f 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2230,6 +2230,7 @@ ProjectSetState
 ProjectionInfo
 ProjectionPath
 PromptInterruptContext
+ProtocolParameter
 ProtocolVersion
 PrsStorage
 PruneFreezeResult
-- 
2.34.1

