From 9390f96a3e967099d55cd75ae03d29964f22f4d1 Mon Sep 17 00:00:00 2001
From: Jelte Fennema-Nio <jelte.fennema@microsoft.com>
Date: Wed, 13 Mar 2024 18:25:48 +0100
Subject: [PATCH v11 06/14] libpq: Add max_protocol_version connection option

All officially supported PostgreSQL servers send the
NegotiateProtocolVersion message when an unsupported minor protocol
version is requested by the client. But other applications implementing
the PosgreSQL protocol (connection poolers, or other databases) might
not, and the same is true for many unspported PostgreSQL server
versions. This patch adds an escape hatch for people wanting to connect
to such applications using libpq by introducing the max_protocol_version
connection option. This way people can manually specify what protocol
version to request from the server.
---
 doc/src/sgml/libpq.sgml           | 24 ++++++++++++++++++++
 src/interfaces/libpq/fe-connect.c | 37 +++++++++++++++++++++++++++++--
 src/interfaces/libpq/libpq-int.h  |  2 ++
 3 files changed, 61 insertions(+), 2 deletions(-)

diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 5f1d7d81200..61fcab6f897 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -2221,6 +2221,30 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
        </para>
       </listitem>
      </varlistentry>
+
+     <varlistentry id="libpq-max-protocol-version" xreflabel="load_balance_hosts">
+      <term><literal>max_protocol_version</literal></term>
+      <listitem>
+       <para>
+        Specifies the protocol version to request from the server. The argument
+        to this connection option is a single number: the result of multiplying
+        the major version number by 10000 and adding the minor version. For
+        example to request version 3.1 the provided argument should be 30001.
+        The default is to use the latest protocol version supported by libpq.
+        Normally there's no need to provide this argument, even when connecting
+        to PostgreSQL servers that don't support the latest protocol. The
+        reason for that is that all supported PostgreSQL server versions
+        automatically negotiate the highest minor protocol version supported by
+        both the client and server. But some servers might simply close
+        the connection when an unsupported protocol version is requested. This
+        problem only applies to unsupported <productname>PostgreSQL</productname>
+        versions, certain connection poolers and some other databases speaking
+        the <productname>PostgreSQL</productname> protocol. So this connection
+        option can be used as an escape hatch when connecting to such servers
+        with libpq.
+       </para>
+      </listitem>
+     </varlistentry>
     </variablelist>
    </para>
   </sect2>
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 881c01d842c..7267c0dd687 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -359,6 +359,11 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
 		"Load-Balance-Hosts", "", 8,	/* sizeof("disable") = 8 */
 	offsetof(struct pg_conn, load_balance_hosts)},
 
+	{"max_protocol_version", "PGMAXPROTOCOLVERSION",
+		NULL, NULL,
+		"Load-Balance-Hosts", "", 8,	/* sizeof("30001") = 8 */
+	offsetof(struct pg_conn, max_protocol_version)},
+
 	/* Terminating entry --- MUST BE LAST */
 	{NULL, NULL, NULL, NULL,
 	NULL, NULL, 0}
@@ -1774,6 +1779,34 @@ pqConnectOptions2(PGconn *conn)
 		}
 	}
 
+	if (conn->max_protocol_version)
+	{
+		char	   *end;
+		int			val;
+
+		val = strtol(conn->max_protocol_version, &end, 10);
+		if (*end)
+		{
+			conn->status = CONNECTION_BAD;
+			libpq_append_conn_error(conn, "invalid %s value: \"%s\"",
+									"max_protocol_version",
+									conn->max_protocol_version);
+			return false;
+		}
+		conn->max_pversion = PG_PROTOCOL(val / 10000, val % 10000);
+		if (conn->max_pversion > PG_PROTOCOL_LATEST ||
+			conn->max_pversion < PG_PROTOCOL_EARLIEST)
+		{
+			conn->status = CONNECTION_BAD;
+			libpq_append_conn_error(conn, "invalid %s value: \"%s\"",
+									"max_protocol_version",
+									conn->max_protocol_version);
+			return false;
+		}
+	}
+	else
+		conn->max_pversion = PG_PROTOCOL_LATEST;
+
 	/*
 	 * Resolve special "auto" client_encoding from the locale
 	 */
@@ -2789,7 +2822,7 @@ keep_going:						/* We will come back to here until there is
 		 * must persist across individual connection attempts, but we must
 		 * reset them when we start to consider a new server.
 		 */
-		conn->pversion = PG_PROTOCOL_LATEST;
+		conn->pversion = conn->max_pversion;
 		conn->send_appname = true;
 #ifdef USE_SSL
 		/* initialize these values based on SSL mode */
@@ -3827,7 +3860,7 @@ keep_going:						/* We will come back to here until there is
 					}
 
 					/* neither -- server shouldn't have sent it */
-					if (!(conn->pversion < PG_PROTOCOL_LATEST) && !conn->unsupported_pextension_params)
+					if (!(conn->pversion < conn->max_pversion) && !conn->unsupported_pextension_params)
 					{
 						libpq_append_conn_error(conn, "invalid %s message", "NegotiateProtocolVersion");
 						goto error_return;
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index b6dbcd64c5a..ccb58d2099d 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -408,6 +408,7 @@ struct pg_conn
 	char	   *target_session_attrs;	/* desired session properties */
 	char	   *require_auth;	/* name of the expected auth method */
 	char	   *load_balance_hosts; /* load balance over hosts */
+	char	   *max_protocol_version;	/* maximum used protocol version */
 
 	bool		cancelRequest;	/* true if this connection is used to send a
 								 * cancel request, instead of being a normal
@@ -495,6 +496,7 @@ struct pg_conn
 	AddrInfo   *addr;			/* the array of addresses for the currently
 								 * tried host */
 	bool		send_appname;	/* okay to send application_name? */
+	ProtocolVersion max_pversion;	/* protocol version to request */
 
 	/* Miscellaneous stuff */
 	int			be_pid;			/* PID of backend --- needed for cancels */
-- 
2.34.1

