From a92b2fe12371ee41361e68e7b0f5afa14a6ac0f7 Mon Sep 17 00:00:00 2001
From: Jelte Fennema-Nio <jelte.fennema@microsoft.com>
Date: Fri, 5 Jan 2024 15:29:41 +0100
Subject: [PATCH v6 06/10] Add protocol message to change parameters

This commit adds the ParamaterSet protocol message. This is simply the
protocol equivalent of the SET command in SQL. Just like the Close
protocol message is the equivalent of DEALLOCATE command in SQL.

This new ParameterSet message thus provides a way of changing commands
that avoids the need to escape the parameter to SET using SQL escaping
rules. Escaping at the client side is generally frowned upon because it
can easily be forgotten, which can lead to SQL injection security
issues.

In addition it allows intermediary components, such as connection
poolers, to use this new message type to accept changes in configuration
for that intermediary component itself. All of this without that
component needing to parse SQL queries.

Finally, it paves the way for a future commit which introduces a new GUC
context that disallows changing a GUC with that context through SQL, but
will still accept changes using the ParameterSet protocol message.
---
 doc/src/sgml/libpq.sgml             | 48 +++++++++++++++
 doc/src/sgml/protocol.sgml          | 91 +++++++++++++++++++++++++++++
 src/backend/postmaster/postmaster.c |  1 +
 src/backend/tcop/postgres.c         | 24 ++++++++
 src/include/libpq/protocol.h        |  2 +
 src/interfaces/libpq/exports.txt    |  2 +
 src/interfaces/libpq/fe-exec.c      | 76 ++++++++++++++++++++++++
 src/interfaces/libpq/fe-protocol3.c | 22 +++++++
 src/interfaces/libpq/fe-trace.c     | 21 +++++++
 src/interfaces/libpq/libpq-fe.h     |  3 +
 src/interfaces/libpq/libpq-int.h    |  3 +-
 11 files changed, 292 insertions(+), 1 deletion(-)

diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 93be3dfbadd..9963af5ace5 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -3436,6 +3436,33 @@ PGresult *PQclosePortal(PGconn *conn, const char *portalName);
        </para>
       </listitem>
      </varlistentry>
+
+     <varlistentry id="libpq-PQparameterSet">
+      <term><function>PQparameterSet</function><indexterm><primary>PQparameterSet</primary></indexterm></term>
+
+      <listitem>
+       <para>
+        Submits a request to change a protocol parameter, and waits for completion.
+<synopsis>
+PGresult *PQparameterSet(PGconn *conn, const char *parameter, const char *value);
+</synopsis>
+       </para>
+
+       <para>
+        <xref linkend="libpq-PQparameterSet"/> allows a client to change
+        backend parameters on the current connection. This is identical to
+        sending a SET command, except that it is at the protocol level.
+       </para>
+
+       <para>
+        <parameter>parameter</parameter> is the name of the parameter to
+        change, and <parameter>value</parameter> is the value to which to
+        change it. On success, a <structname>PGresult</structname> with status
+        <literal>PGRES_COMMAND_OK</literal> is returned.
+       </para>
+      </listitem>
+     </varlistentry>
+
     </variablelist>
    </para>
 
@@ -5128,6 +5155,27 @@ int PQsendClosePortal(PGconn *conn, const char *portalName);
      </listitem>
     </varlistentry>
 
+    <varlistentry id="libpq-PQsendParameterSet">
+     <term><function>PQsendParameterSet</function><indexterm><primary>PQsendParameterSet</primary></indexterm></term>
+
+     <listitem>
+      <para>
+        Submits a request to change a protocol parameter, without waiting for
+        completion.
+<synopsis>
+int PQsendParameterSet(PGconn *conn, const char *portalName);
+</synopsis>
+
+       This is an asynchronous version of <xref linkend="libpq-PQparameterSet"/>:
+       it returns 1 if it was able to dispatch the request, and 0 if not.
+       After a successful call, call <xref linkend="libpq-PQgetResult"/> to
+       obtain the results.  The function's parameters are handled
+       identically to <xref linkend="libpq-PQparameterSet"/>.
+      </para>
+     </listitem>
+    </varlistentry>
+
+
     <varlistentry id="libpq-PQgetResult">
      <term><function>PQgetResult</function><indexterm><primary>PQgetResult</primary></indexterm></term>
 
diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml
index 6c3e8a631d7..72f43dd51f1 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -1030,6 +1030,29 @@ SELCT 1/0;<!-- this typo is intentional -->
      CloseComplete, or NoData messages.
     </para>
    </note>
+
+   <para>
+    Since protocol version 3.1, the ParameterSet message can be used to change
+    the value of a parameter. For most parameters this is equivalent to issuing
+    a SET command, but can be more convenient to parse for applications that
+    are inbetween a client and a server, such as a connection pooler. For
+    <xref linkend="runtime-config-protocol"/> this message type is the only way
+    of changing their value after the initial StartupMessage. When changing
+    such protocol extension parameters the server behavior is slightly
+    different than for normal backend parameters. The server will not allow
+    changing protocol extension parameters while a transaction is active, and
+    when run in a pipeline each ParameterSet message will implicitly commit.
+   </para>
+
+   <note>
+    <para>
+     It is still possible to change protocol extension parameters with
+     ParameterSet in a pipeline together with other commands but only if no
+     transaction is active. This means that the ParameterSet messages either
+     need to be the first messages in the pipeline or any previous transaction
+     needs to have been explicitely committed.
+    </para>
+   </note>
   </sect2>
 
   <sect2 id="protocol-flow-pipelining">
@@ -5271,6 +5294,74 @@ psql "dbname=postgres replication=database" -c "IDENTIFY_SYSTEM;"
     </listitem>
    </varlistentry>
 
+   <varlistentry id="protocol-message-formats-ParameterSet">
+    <term>ParameterSet (F)</term>
+    <listitem>
+     <variablelist>
+      <varlistentry>
+       <term>Byte1('U')</term>
+       <listitem>
+        <para>
+         Identifies the message as a run-time 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 run-time 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-ParameterSetComplete">
+    <term>ParameterSetComplete (B)</term>
+    <listitem>
+     <variablelist>
+      <varlistentry>
+       <term>Byte1('U')</term>
+       <listitem>
+        <para>
+         Identifies the message as a ParameterSet-complete indicator.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int32(4)</term>
+       <listitem>
+        <para>
+         Length of message contents in bytes, including self.
+        </para>
+       </listitem>
+      </varlistentry>
+     </variablelist>
+    </listitem>
+   </varlistentry>
+
    <varlistentry id="protocol-message-formats-Parse">
     <term>Parse (F)</term>
     <listitem>
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index ad31e1046bb..0c14b830c30 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -124,6 +124,7 @@
 #include "tcop/tcopprot.h"
 #include "utils/builtins.h"
 #include "utils/datetime.h"
+#include "utils/guc_tables.h"
 #include "utils/memutils.h"
 #include "utils/pidfile.h"
 #include "utils/ps_status.h"
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 1eaaf3c6c58..43471b7a877 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/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/ps_status.h"
@@ -427,6 +428,7 @@ SocketBackend(StringInfo inBuf)
 		case PqMsg_Describe:
 		case PqMsg_Execute:
 		case PqMsg_Flush:
+		case PqMsg_ParameterSet:
 			maxmsglen = PQ_SMALL_MESSAGE_LIMIT;
 			doing_extended_query_message = true;
 			break;
@@ -4851,6 +4853,28 @@ PostgresMain(const char *dbname, const char *username)
 				send_ready_for_query = true;
 				break;
 
+			case PqMsg_ParameterSet:
+				{
+					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);
+
+					start_xact_command();
+
+					SetConfigOption(
+									parameter_name,
+									parameter_value,
+									(superuser() ? PGC_SUSET : PGC_USERSET),
+									PGC_S_SESSION);
+					if (whereToSendOutput == DestRemote)
+						pq_putemptymessage(PqMsg_ParameterSetComplete);
+				}
+				break;
+
 				/*
 				 * 'X' means that the frontend is closing down the socket. EOF
 				 * means unexpected loss of frontend connection. Either way,
diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h
index 4b8d4403656..c4040f99a66 100644
--- a/src/include/libpq/protocol.h
+++ b/src/include/libpq/protocol.h
@@ -25,6 +25,7 @@
 #define PqMsg_Parse					'P'
 #define PqMsg_Query					'Q'
 #define PqMsg_Sync					'S'
+#define PqMsg_ParameterSet			'U'
 #define PqMsg_Terminate				'X'
 #define PqMsg_CopyFail				'f'
 #define PqMsg_GSSResponse			'p'
@@ -52,6 +53,7 @@
 #define PqMsg_RowDescription		'T'
 #define PqMsg_FunctionCallResponse	'V'
 #define PqMsg_CopyBothResponse		'W'
+#define PqMsg_ParameterSetComplete	'U'
 #define PqMsg_ReadyForQuery			'Z'
 #define PqMsg_NoData				'n'
 #define PqMsg_PortalSuspended		's'
diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt
index ca9744801a8..7d43bb79f52 100644
--- a/src/interfaces/libpq/exports.txt
+++ b/src/interfaces/libpq/exports.txt
@@ -193,3 +193,5 @@ PQsendClosePrepared       190
 PQsendClosePortal         191
 PQchangePassword          192
 PQunsupportedProtocolExtensions 193
+PQparameterSet            194
+PQsendParameterSet        195
diff --git a/src/interfaces/libpq/fe-exec.c b/src/interfaces/libpq/fe-exec.c
index 106d14e6eed..1f7e9cc5614 100644
--- a/src/interfaces/libpq/fe-exec.c
+++ b/src/interfaces/libpq/fe-exec.c
@@ -3346,6 +3346,82 @@ PQsendFlushRequest(PGconn *conn)
 	return 1;
 }
 
+/*
+ * PQparameterSet
+ *	  Send a request for the server to change a run-time parameter setting.
+ *
+ * If the query was not even sent, return NULL; conn->errorMessage is set to
+ * a relevant message.
+ * If the query 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.
+ */
+PGresult *
+PQparameterSet(PGconn *conn, const char *parameter, const char *value)
+{
+	if (!PQexecStart(conn))
+		return NULL;
+	if (!PQsendParameterSet(conn, parameter, value))
+		return NULL;
+	return PQexecFinish(conn);
+}
+
+/*
+ * PQsendParameterSet
+ *	 Send a request for the server to change a run-time parameter setting.
+ *
+ * Returns 1 on success and 0 on failure.
+ */
+int
+PQsendParameterSet(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 Close message */
+	if (pqPutMsgStart(PqMsg_ParameterSet, conn) < 0 ||
+		pqPuts(parameter, conn) < 0 ||
+		pqPuts(value, conn) < 0 ||
+		pqPutMsgEnd(conn) < 0)
+		goto sendFailed;
+
+	/* construct the Sync message */
+	if (conn->pipelineStatus == PQ_PIPELINE_OFF)
+	{
+		if (pqPutMsgStart(PqMsg_Sync, conn) < 0 ||
+			pqPutMsgEnd(conn) < 0)
+			goto sendFailed;
+	}
+
+	entry->queryclass = PGQUERY_PARAMETER_SET;
+
+	/*
+	 * Give the data a push (in pipeline mode, only if we're past the size
+	 * threshold).  In nonblock mode, don't complain if we're unable to send
+	 * it all; PQgetResult() will do any additional flushing needed.
+	 */
+	if (pqPipelineFlush(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 75a0ee96785..d8c7b541b97 100644
--- a/src/interfaces/libpq/fe-protocol3.c
+++ b/src/interfaces/libpq/fe-protocol3.c
@@ -297,6 +297,28 @@ pqParseInput3(PGconn *conn)
 						conn->asyncStatus = PGASYNC_READY;
 					}
 					break;
+				case PqMsg_ParameterSetComplete:
+
+					/*
+					 * If we're doing PQsendParameterSet, we're done; else
+					 * ignore
+					 */
+					if (conn->cmd_queue_head &&
+						conn->cmd_queue_head->queryclass == PGQUERY_PARAMETER_SET)
+					{
+						if (!pgHavePendingResult(conn))
+						{
+							conn->result = PQmakeEmptyPGresult(conn,
+															   PGRES_COMMAND_OK);
+							if (!conn->result)
+							{
+								libpq_append_conn_error(conn, "out of memory");
+								pqSaveErrorResult(conn);
+							}
+						}
+						conn->asyncStatus = PGASYNC_READY;
+					}
+					break;
 				case PqMsg_ParameterStatus:
 					if (getParameterStatus(conn))
 						return;
diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c
index c9932fc8a6b..b69ecbd597d 100644
--- a/src/interfaces/libpq/fe-trace.c
+++ b/src/interfaces/libpq/fe-trace.c
@@ -514,6 +514,23 @@ pqTraceOutputW(FILE *f, const char *message, int *cursor, int length)
 		pqTraceOutputInt16(f, message, cursor);
 }
 
+/* ParameterSet(F) or ParameterSetComplete(B) */
+static void
+pqTraceOutputU(FILE *f, bool toServer, const char *message, int *cursor)
+{
+	if (toServer)
+	{
+		fprintf(f, "ParameterSet\t");
+		pqTraceOutputString(f, message, cursor, false);
+		pqTraceOutputString(f, message, cursor, false);
+	}
+	else
+	{
+		fprintf(f, "ParameterSetComplete");
+		/* No message content */
+	}
+}
+
 /* ReadyForQuery */
 static void
 pqTraceOutputZ(FILE *f, const char *message, int *cursor)
@@ -589,6 +606,10 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer)
 			Assert(PqMsg_Close == PqMsg_CommandComplete);
 			pqTraceOutputC(conn->Pfdebug, toServer, message, &logCursor);
 			break;
+		case PqMsg_ParameterSet:
+			Assert(PqMsg_ParameterSet == PqMsg_ParameterSetComplete);
+			pqTraceOutputU(conn->Pfdebug, toServer, message, &logCursor);
+			break;
 		case PqMsg_CopyData:
 			/* Drop COPY data to reduce the overhead of logging. */
 			break;
diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h
index 809204d2eb4..097a5513ea5 100644
--- a/src/interfaces/libpq/libpq-fe.h
+++ b/src/interfaces/libpq/libpq-fe.h
@@ -555,6 +555,9 @@ extern PGresult *PQclosePortal(PGconn *conn, const char *portal);
 extern int	PQsendClosePrepared(PGconn *conn, const char *stmt);
 extern int	PQsendClosePortal(PGconn *conn, const char *portal);
 
+extern PGresult *PQparameterSet(PGconn *conn, const char *parameter, const char *value);
+extern int	PQsendParameterSet(PGconn *conn, const char *parameter, const char *value);
+
 /* Delete a PGresult */
 extern void PQclear(PGresult *res);
 
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index d302a95ceaa..8f152c51252 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -322,7 +322,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_PARAMETER_SET,		/* Set a server parameter */
 } PGQueryClass;
 
 /*
-- 
2.34.1

