From ba021281fe7910fa197888b299281acbfda30c36 Mon Sep 17 00:00:00 2001
From: Jeff Davis <jeff@j-davis.com>
Date: Tue, 23 Jan 2024 11:11:21 -0800
Subject: [PATCH v9 1/3] Add SQL function pg_conninfo_from_server().

Retrieves valid Postgres connection string from a foreign server. Any
foreign server may be used, though it's expected to provide valid
libpq connection options. Invalid or unrecognized options will be
ignored.

Extends walreceiver API to return available libpq options.

In preparation for CREATE SUBSCRIPTION ... SERVER.

Discussion: https://postgr.es/m/2a89b14a2b1622bffb8b137ca1f9ab7866f2d2b9.camel@j-davis.com
---
 .../postgres_fdw/expected/postgres_fdw.out    |  14 +
 contrib/postgres_fdw/sql/postgres_fdw.sql     |   6 +
 doc/src/sgml/func.sgml                        |  19 ++
 src/backend/foreign/foreign.c                 | 255 +++++++++++++++++-
 .../libpqwalreceiver/libpqwalreceiver.c       |  48 ++++
 src/include/catalog/pg_proc.dat               |   8 +
 src/include/foreign/foreign.h                 |   2 +
 src/include/replication/walreceiver.h         |  20 ++
 src/test/regress/expected/foreign_data.out    |  46 ++++
 src/test/regress/sql/foreign_data.sql         |  40 +++
 10 files changed, 449 insertions(+), 9 deletions(-)

diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index b5a38aeb21..8a7a15cc51 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -24,6 +24,13 @@ CREATE USER MAPPING FOR public SERVER testserver1
 CREATE USER MAPPING FOR CURRENT_USER SERVER loopback;
 CREATE USER MAPPING FOR CURRENT_USER SERVER loopback2;
 CREATE USER MAPPING FOR public SERVER loopback3;
+-- test pg_conninfo_from_server()
+SELECT pg_conninfo_from_server('testserver1', CURRENT_USER, false);
+      pg_conninfo_from_server      
+-----------------------------------
+ user = 'value' password = 'value'
+(1 row)
+
 -- ===================================================================
 -- create objects used through FDW loopback server
 -- ===================================================================
@@ -196,6 +203,13 @@ ALTER USER MAPPING FOR public SERVER testserver1
 -- permitted to check validation.
 ALTER USER MAPPING FOR public SERVER testserver1
 	OPTIONS (ADD sslkey 'value', ADD sslcert 'value');
+-- check pg_conninfo_from_server() after ALTERs
+SELECT pg_conninfo_from_server('testserver1', CURRENT_USER, false);
+                                                                                                                                                                                                                                   pg_conninfo_from_server                                                                                                                                                                                                                                    
+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ service = 'value' connect_timeout = 'value' dbname = 'value' host = 'value' hostaddr = 'value' port = 'value' application_name = 'value' keepalives = 'value' keepalives_idle = 'value' keepalives_interval = 'value' tcp_user_timeout = 'value' sslcompression = 'value' sslmode = 'value' sslcert = 'value' sslkey = 'value' sslrootcert = 'value' sslcrl = 'value' krbsrvname = 'value' gsslib = 'value' gssdelegation = 'value' sslpassword = 'dummy' sslkey = 'value' sslcert = 'value'
+(1 row)
+
 ALTER FOREIGN TABLE ft1 OPTIONS (schema_name 'S 1', table_name 'T 1');
 ALTER FOREIGN TABLE ft2 OPTIONS (schema_name 'S 1', table_name 'T 1');
 ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (column_name 'C 1');
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index f410c3db4e..0d8478120d 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -28,6 +28,9 @@ CREATE USER MAPPING FOR CURRENT_USER SERVER loopback;
 CREATE USER MAPPING FOR CURRENT_USER SERVER loopback2;
 CREATE USER MAPPING FOR public SERVER loopback3;
 
+-- test pg_conninfo_from_server()
+SELECT pg_conninfo_from_server('testserver1', CURRENT_USER, false);
+
 -- ===================================================================
 -- create objects used through FDW loopback server
 -- ===================================================================
@@ -213,6 +216,9 @@ ALTER USER MAPPING FOR public SERVER testserver1
 ALTER USER MAPPING FOR public SERVER testserver1
 	OPTIONS (ADD sslkey 'value', ADD sslcert 'value');
 
+-- check pg_conninfo_from_server() after ALTERs
+SELECT pg_conninfo_from_server('testserver1', CURRENT_USER, false);
+
 ALTER FOREIGN TABLE ft1 OPTIONS (schema_name 'S 1', table_name 'T 1');
 ALTER FOREIGN TABLE ft2 OPTIONS (schema_name 'S 1', table_name 'T 1');
 ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (column_name 'C 1');
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 210c7c0b02..79e1792eae 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -27985,6 +27985,25 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_conninfo_from_server</primary>
+        </indexterm>
+        <function>pg_conninfo_from_server</function> ( <parameter>servername</parameter> <type>text</type>, <parameter>username</parameter> <type>text</type>, <parameter>append_overrides</parameter> <type>boolean</type> )
+        <returnvalue>text</returnvalue>
+       </para>
+       <para>
+        Returns connection string generated from the foreign server and user
+        mapping associated with the given
+        <replaceable>servername</replaceable> and
+        <replaceable>username</replaceable>. If
+        <replaceable>append_overrides</replaceable> is
+        <literal>true</literal>, it appends override parameters necessary for
+        making outbound connections.
+       </para></entry>
+      </row>
+
       <row>
        <entry id="pg-logical-emit-message" role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/src/backend/foreign/foreign.c b/src/backend/foreign/foreign.c
index 02e1898131..b4635d6eba 100644
--- a/src/backend/foreign/foreign.c
+++ b/src/backend/foreign/foreign.c
@@ -18,11 +18,15 @@
 #include "catalog/pg_foreign_server.h"
 #include "catalog/pg_foreign_table.h"
 #include "catalog/pg_user_mapping.h"
+#include "commands/defrem.h"
 #include "foreign/fdwapi.h"
 #include "foreign/foreign.h"
 #include "funcapi.h"
 #include "lib/stringinfo.h"
+#include "mb/pg_wchar.h"
 #include "miscadmin.h"
+#include "replication/walreceiver.h"
+#include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/memutils.h"
 #include "utils/rel.h"
@@ -190,6 +194,146 @@ GetForeignServerByName(const char *srvname, bool missing_ok)
 }
 
 
+/*
+ * Values in connection strings must be enclosed in single quotes. Single
+ * quotes and backslashes must be escaped with backslash. NB: these rules are
+ * different from the rules for escaping a SQL literal.
+ */
+static void
+appendEscapedValue(StringInfo str, const char *val)
+{
+	appendStringInfoChar(str, '\'');
+	for (int i = 0; val[i] != '\0'; i++)
+	{
+		if (val[i] == '\\' || val[i] == '\'')
+			appendStringInfoChar(str, '\\');
+		appendStringInfoChar(str, val[i]);
+	}
+	appendStringInfoChar(str, '\'');
+}
+
+
+/*
+ * Check if the provided option is one of libpq conninfo options.
+ * context is the Oid of the catalog the option came from, or 0 if we
+ * don't care.
+ */
+static bool
+is_libpq_conninfo_option(const char *option, Oid context)
+{
+	const ConnectionOption *opt;
+
+	/* skip options that must be overridden */
+	if (strcmp(option, "client_encoding") == 0)
+		return false;
+
+	for (opt = walrcv_conninfo_options(); opt->optname; opt++)
+	{
+		if (strcmp(opt->optname, option) == 0)
+		{
+			if (opt->isdebug)
+				return false;
+
+			if (opt->issecret || strcmp(opt->optname, "user") == 0)
+				return (context == UserMappingRelationId);
+
+			return (context == ForeignServerRelationId);
+		}
+	}
+	return false;
+}
+
+
+/*
+ * Helper for ForeignServerConnectionString().
+ *
+ * Transform a List of DefElem into a connection string.
+ */
+static char *
+options_to_conninfo(List *options, bool append_overrides)
+{
+	StringInfoData	 str;
+	ListCell		*lc;
+	char			*sep = "";
+
+	initStringInfo(&str);
+	foreach(lc, options)
+	{
+		DefElem *d = (DefElem *) lfirst(lc);
+		char *name = d->defname;
+		char *value;
+
+		/* ignore unknown options */
+		if (!is_libpq_conninfo_option(name, ForeignServerRelationId) &&
+			!is_libpq_conninfo_option(name, UserMappingRelationId))
+			continue;
+
+		value = defGetString(d);
+
+		appendStringInfo(&str, "%s%s = ", sep, name);
+		appendEscapedValue(&str, value);
+		sep = " ";
+	}
+
+	/* override client_encoding */
+	if (append_overrides)
+	{
+		appendStringInfo(&str, "%sclient_encoding = ", sep);
+		appendEscapedValue(&str, GetDatabaseEncodingName());
+		sep = " ";
+	}
+
+	return str.data;
+}
+
+
+/*
+ * Given a user ID and server ID, return a postgres connection string suitable
+ * to pass to libpq.
+ */
+char *
+ForeignServerConnectionString(Oid userid, Oid serverid, bool append_overrides)
+{
+	static MemoryContext	 tmpcontext = NULL;
+	ForeignServer			*server;
+	UserMapping				*um;
+	List					*options;
+	char					*conninfo;
+	MemoryContext			 oldcontext;
+
+	/* Load the library providing us libpq calls. */
+	load_file("libpqwalreceiver", false);
+
+	/*
+	 * Use a temporary context rather than trying to track individual
+	 * allocations in GetForeignServer() and GetUserMapping().
+	 */
+	if (tmpcontext == NULL)
+		tmpcontext = AllocSetContextCreate(TopMemoryContext,
+										   "temp context for building connection string",
+										   ALLOCSET_DEFAULT_SIZES);
+
+	oldcontext = MemoryContextSwitchTo(tmpcontext);
+
+	server = GetForeignServer(serverid);
+	um = GetUserMapping(userid, serverid);
+
+	/* user mapping options override server options */
+	options = list_concat(server->options, um->options);
+
+	conninfo = options_to_conninfo(options, append_overrides);
+
+	MemoryContextSwitchTo(oldcontext);
+
+	/* copy only conninfo into the current context */
+	conninfo = pstrdup(conninfo);
+
+	MemoryContextReset(tmpcontext);
+
+	return conninfo;
+}
+
+
 /*
  * GetUserMapping - look up the user mapping.
  *
@@ -549,10 +693,103 @@ pg_options_to_table(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * pg_conninfo_from_server
+ *
+ * Extract connection string from the given foreign server.
+ */
+Datum
+pg_conninfo_from_server(PG_FUNCTION_ARGS)
+{
+	char *server_name = text_to_cstring(PG_GETARG_TEXT_P(0));
+	char *user_name = text_to_cstring(PG_GETARG_TEXT_P(1));
+	bool  append_overrides = PG_GETARG_BOOL(2);
+	Oid serverid = get_foreign_server_oid(server_name, false);
+	Oid userid = get_role_oid_or_public(user_name);
+	AclResult aclresult;
+	char *conninfo;
+
+	/* if the specified userid is not PUBLIC, check SET ROLE privileges */
+	if (userid != ACL_ID_PUBLIC)
+		check_can_set_role(GetUserId(), userid);
+
+	/* ACL check on foreign server */
+	aclresult = object_aclcheck(ForeignServerRelationId, serverid,
+								GetUserId(), ACL_USAGE);
+	if (aclresult != ACLCHECK_OK)
+		aclcheck_error(aclresult, OBJECT_FOREIGN_SERVER, server_name);
+
+	conninfo = ForeignServerConnectionString(userid, serverid,
+											 append_overrides);
+
+	PG_RETURN_TEXT_P(cstring_to_text(conninfo));
+}
+
+
+/*
+ * Validate the generic option given to SERVER or USER MAPPING.
+ * Raise an ERROR if the option or its value is considered invalid.
+ *
+ * Valid server options are all libpq conninfo options except
+ * user and password -- these may only appear in USER MAPPING options.
+ */
+Datum
+pg_connection_validator(PG_FUNCTION_ARGS)
+{
+	List	   *options_list = untransformRelOptions(PG_GETARG_DATUM(0));
+	Oid			catalog = PG_GETARG_OID(1);
+
+	ListCell   *cell;
+
+	/* Load the library providing us libpq calls. */
+	load_file("libpqwalreceiver", false);
+
+	foreach(cell, options_list)
+	{
+		DefElem    *def = lfirst(cell);
+
+		if (!is_libpq_conninfo_option(def->defname, catalog))
+		{
+			const ConnectionOption *opt;
+			const char *closest_match;
+			ClosestMatchState match_state;
+			bool		has_valid_options = false;
+
+			/*
+			 * Unknown option specified, complain about it. Provide a hint
+			 * with a valid option that looks similar, if there is one.
+			 */
+			initClosestMatch(&match_state, def->defname, 4);
+			for (opt = walrcv_conninfo_options(); opt->optname; opt++)
+			{
+				if (is_libpq_conninfo_option(opt->optname, catalog))
+				{
+					has_valid_options = true;
+					updateClosestMatch(&match_state, opt->optname);
+				}
+			}
+
+			closest_match = getClosestMatch(&match_state);
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("invalid option \"%s\"", def->defname),
+					 has_valid_options ? closest_match ?
+					 errhint("Perhaps you meant the option \"%s\".",
+							 closest_match) : 0 :
+					 errhint("There are no valid options in this context.")));
+
+			PG_RETURN_BOOL(false);
+		}
+	}
+
+	PG_RETURN_BOOL(true);
+}
+
+
 /*
  * Describes the valid options for postgresql FDW, server, and user mapping.
  */
-struct ConnectionOption
+struct TestConnectionOption
 {
 	const char *optname;
 	Oid			optcontext;		/* Oid of catalog in which option may appear */
@@ -563,7 +800,7 @@ struct ConnectionOption
  *
  * The list is small - don't bother with bsearch if it stays so.
  */
-static const struct ConnectionOption libpq_conninfo_options[] = {
+static const struct TestConnectionOption test_conninfo_options[] = {
 	{"authtype", ForeignServerRelationId},
 	{"service", ForeignServerRelationId},
 	{"user", UserMappingRelationId},
@@ -584,16 +821,16 @@ static const struct ConnectionOption libpq_conninfo_options[] = {
 
 
 /*
- * Check if the provided option is one of libpq conninfo options.
+ * Check if the provided option is one of the test conninfo options.
  * context is the Oid of the catalog the option came from, or 0 if we
  * don't care.
  */
 static bool
-is_conninfo_option(const char *option, Oid context)
+is_test_conninfo_option(const char *option, Oid context)
 {
-	const struct ConnectionOption *opt;
+	const struct TestConnectionOption *opt;
 
-	for (opt = libpq_conninfo_options; opt->optname; opt++)
+	for (opt = test_conninfo_options; opt->optname; opt++)
 		if (context == opt->optcontext && strcmp(opt->optname, option) == 0)
 			return true;
 	return false;
@@ -624,9 +861,9 @@ postgresql_fdw_validator(PG_FUNCTION_ARGS)
 	{
 		DefElem    *def = lfirst(cell);
 
-		if (!is_conninfo_option(def->defname, catalog))
+		if (!is_test_conninfo_option(def->defname, catalog))
 		{
-			const struct ConnectionOption *opt;
+			const struct TestConnectionOption *opt;
 			const char *closest_match;
 			ClosestMatchState match_state;
 			bool		has_valid_options = false;
@@ -636,7 +873,7 @@ postgresql_fdw_validator(PG_FUNCTION_ARGS)
 			 * with a valid option that looks similar, if there is one.
 			 */
 			initClosestMatch(&match_state, def->defname, 4);
-			for (opt = libpq_conninfo_options; opt->optname; opt++)
+			for (opt = test_conninfo_options; opt->optname; opt++)
 			{
 				if (catalog == opt->optcontext)
 				{
diff --git a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
index 77669074e8..a1845e6dfa 100644
--- a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
+++ b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
@@ -52,6 +52,7 @@ static WalReceiverConn *libpqrcv_connect(const char *conninfo,
 										 const char *appname, char **err);
 static void libpqrcv_check_conninfo(const char *conninfo,
 									bool must_use_password);
+static const ConnectionOption *libpqrcv_conninfo_options(void);
 static char *libpqrcv_get_conninfo(WalReceiverConn *conn);
 static void libpqrcv_get_senderinfo(WalReceiverConn *conn,
 									char **sender_host, int *sender_port);
@@ -85,6 +86,7 @@ static void libpqrcv_disconnect(WalReceiverConn *conn);
 static WalReceiverFunctionsType PQWalReceiverFunctions = {
 	.walrcv_connect = libpqrcv_connect,
 	.walrcv_check_conninfo = libpqrcv_check_conninfo,
+	.walrcv_conninfo_options = libpqrcv_conninfo_options,
 	.walrcv_get_conninfo = libpqrcv_get_conninfo,
 	.walrcv_get_senderinfo = libpqrcv_get_senderinfo,
 	.walrcv_identify_system = libpqrcv_identify_system,
@@ -337,6 +339,52 @@ libpqrcv_check_conninfo(const char *conninfo, bool must_use_password)
 	PQconninfoFree(opts);
 }
 
+static const ConnectionOption *
+libpqrcv_conninfo_options(void)
+{
+	static ConnectionOption	*connection_options = NULL;
+
+	if (connection_options == NULL)
+	{
+		PQconninfoOption	*conndefaults	= PQconndefaults();
+		PQconninfoOption	*lopt;
+		ConnectionOption	*tmp_options	= NULL;
+		ConnectionOption	*popt;
+		size_t				 options_size	= 0;
+		int					 num_libpq_opts	= 0;
+
+		for (lopt = conndefaults; lopt->keyword; lopt++)
+			num_libpq_opts++;
+
+		/* leave room for all-zero entry at the end */
+		options_size = sizeof(ConnectionOption) * (num_libpq_opts + 1);
+		tmp_options = MemoryContextAllocZero(TopMemoryContext, options_size);
+
+		popt = tmp_options;
+		for (lopt = conndefaults; lopt->keyword; lopt++)
+		{
+			if (strchr(lopt->dispchar, '*'))
+				popt->issecret = true;
+			else if (strchr(lopt->dispchar, 'D'))
+				popt->isdebug = true;
+
+			popt->optname = MemoryContextStrdup(TopMemoryContext,
+												lopt->keyword);
+			popt++;
+		}
+
+		/* last entry is all zero */
+		Assert(popt->optname == NULL);
+
+		PQconninfoFree(conndefaults);
+
+		/* if everything succeeded, set static variable */
+		connection_options = tmp_options;
+	}
+
+	return connection_options;
+}
+
 /*
  * Return a user-displayable conninfo string.  Any security-sensitive fields
  * are obfuscated.
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index ad74e07dbb..5890d22dd9 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7516,6 +7516,14 @@
   proname => 'postgresql_fdw_validator', prorettype => 'bool',
   proargtypes => '_text oid', prosrc => 'postgresql_fdw_validator' },
 
+{ oid => '6015', descr => '(internal)',
+  proname => 'pg_connection_validator', prorettype => 'bool',
+  proargtypes => '_text oid', prosrc => 'pg_connection_validator' },
+
+{ oid => '6123', descr => 'extract connection string from the given foreign server',
+  proname => 'pg_conninfo_from_server', prorettype => 'text',
+  proargtypes => 'text text bool', prosrc => 'pg_conninfo_from_server' },
+
 { oid => '2290', descr => 'I/O',
   proname => 'record_in', provolatile => 's', prorettype => 'record',
   proargtypes => 'cstring oid int4', prosrc => 'record_in' },
diff --git a/src/include/foreign/foreign.h b/src/include/foreign/foreign.h
index 82b8153100..b5b9b97f4d 100644
--- a/src/include/foreign/foreign.h
+++ b/src/include/foreign/foreign.h
@@ -69,6 +69,8 @@ extern ForeignServer *GetForeignServerExtended(Oid serverid,
 											   bits16 flags);
 extern ForeignServer *GetForeignServerByName(const char *srvname,
 											 bool missing_ok);
+extern char *ForeignServerConnectionString(Oid userid, Oid serverid,
+										   bool append_overrides);
 extern UserMapping *GetUserMapping(Oid userid, Oid serverid);
 extern ForeignDataWrapper *GetForeignDataWrapper(Oid fdwid);
 extern ForeignDataWrapper *GetForeignDataWrapperExtended(Oid fdwid,
diff --git a/src/include/replication/walreceiver.h b/src/include/replication/walreceiver.h
index 0899891cdb..a2ecbf825a 100644
--- a/src/include/replication/walreceiver.h
+++ b/src/include/replication/walreceiver.h
@@ -223,6 +223,16 @@ typedef struct WalRcvExecResult
 	TupleDesc	tupledesc;
 } WalRcvExecResult;
 
+/*
+ * Describes the valid options for postgresql FDW, server, and user mapping.
+ */
+typedef struct ConnectionOption
+{
+	const char *optname;
+	bool		issecret;		/* is option for a password? */
+	bool		isdebug;		/* is option a debug option? */
+} ConnectionOption;
+
 /* WAL receiver - libpqwalreceiver hooks */
 
 /*
@@ -250,6 +260,13 @@ typedef WalReceiverConn *(*walrcv_connect_fn) (const char *conninfo,
 typedef void (*walrcv_check_conninfo_fn) (const char *conninfo,
 										  bool must_use_password);
 
+/*
+ * walrcv_conninfo_options_fn
+ *
+ * Return a pointer to a static array of the available options from libpq.
+ */
+typedef const struct ConnectionOption *(*walrcv_conninfo_options_fn) (void);
+
 /*
  * walrcv_get_conninfo_fn
  *
@@ -389,6 +406,7 @@ typedef struct WalReceiverFunctionsType
 {
 	walrcv_connect_fn walrcv_connect;
 	walrcv_check_conninfo_fn walrcv_check_conninfo;
+	walrcv_conninfo_options_fn walrcv_conninfo_options;
 	walrcv_get_conninfo_fn walrcv_get_conninfo;
 	walrcv_get_senderinfo_fn walrcv_get_senderinfo;
 	walrcv_identify_system_fn walrcv_identify_system;
@@ -410,6 +428,8 @@ extern PGDLLIMPORT WalReceiverFunctionsType *WalReceiverFunctions;
 	WalReceiverFunctions->walrcv_connect(conninfo, logical, must_use_password, appname, err)
 #define walrcv_check_conninfo(conninfo, must_use_password) \
 	WalReceiverFunctions->walrcv_check_conninfo(conninfo, must_use_password)
+#define walrcv_conninfo_options() \
+	WalReceiverFunctions->walrcv_conninfo_options()
 #define walrcv_get_conninfo(conn) \
 	WalReceiverFunctions->walrcv_get_conninfo(conn)
 #define walrcv_get_senderinfo(conn, sender_host, sender_port) \
diff --git a/src/test/regress/expected/foreign_data.out b/src/test/regress/expected/foreign_data.out
index 1dfe23cc1e..0211531f32 100644
--- a/src/test/regress/expected/foreign_data.out
+++ b/src/test/regress/expected/foreign_data.out
@@ -688,6 +688,52 @@ DROP SERVER s7;
  t1     | regress_test_role
 (8 rows)
 
+--
+-- test pg_conninfo_from_server().
+--
+-- use test validator function (not all libpq options supported)
+CREATE FOREIGN DATA WRAPPER regress_connection_fdw
+  VALIDATOR pg_connection_validator;
+\set VERBOSITY terse
+CREATE SERVER connection_server FOREIGN DATA WRAPPER regress_connection_fdw
+  OPTIONS (host 'thehost', client_encoding 'LATIN1'); -- fail
+ERROR:  invalid option "client_encoding"
+CREATE SERVER connection_server FOREIGN DATA WRAPPER regress_connection_fdw
+  OPTIONS (host 'thehost', nonsense 'asdf'); -- fail
+ERROR:  invalid option "nonsense"
+CREATE SERVER connection_server FOREIGN DATA WRAPPER regress_connection_fdw
+  OPTIONS (host 'thehost', password 'secret'); -- fail
+ERROR:  invalid option "password"
+\set VERBOSITY default
+CREATE SERVER connection_server FOREIGN DATA WRAPPER regress_connection_fdw
+  OPTIONS (hsot 'thehost'); -- fail - misspelling
+ERROR:  invalid option "hsot"
+HINT:  Perhaps you meant the option "host".
+CREATE SERVER connection_server FOREIGN DATA WRAPPER regress_connection_fdw
+  OPTIONS (host 'thehost', port '5432');
+CREATE USER MAPPING FOR regress_test_role SERVER connection_server
+  OPTIONS (user 'role', password 'secret', host 'otherhost'); -- fail
+ERROR:  invalid option "host"
+CREATE USER MAPPING FOR regress_test_role SERVER connection_server
+  OPTIONS (user 'role', password 'secret');
+CREATE USER MAPPING FOR PUBLIC SERVER connection_server
+  OPTIONS (user 'publicuser', password $pwd$'\"$# secret'$pwd$);
+SELECT pg_conninfo_from_server('connection_server', 'regress_test_role', false);
+                     pg_conninfo_from_server                      
+------------------------------------------------------------------
+ host = 'thehost' port = '5432' user = 'role' password = 'secret'
+(1 row)
+
+SELECT pg_conninfo_from_server('connection_server', 'regress_test_role2', false);
+                             pg_conninfo_from_server                              
+----------------------------------------------------------------------------------
+ host = 'thehost' port = '5432' user = 'publicuser' password = '\'\\"$# secret\''
+(1 row)
+
+DROP USER MAPPING FOR regress_test_role SERVER connection_server;
+DROP USER MAPPING FOR PUBLIC SERVER connection_server;
+DROP SERVER connection_server;
+DROP FOREIGN DATA WRAPPER regress_connection_fdw;
 -- CREATE FOREIGN TABLE
 CREATE SCHEMA foreign_schema;
 CREATE SERVER s0 FOREIGN DATA WRAPPER dummy;
diff --git a/src/test/regress/sql/foreign_data.sql b/src/test/regress/sql/foreign_data.sql
index eefb860adc..a8e2edfeee 100644
--- a/src/test/regress/sql/foreign_data.sql
+++ b/src/test/regress/sql/foreign_data.sql
@@ -291,6 +291,46 @@ RESET ROLE;
 DROP SERVER s7;
 \deu
 
+--
+-- test pg_conninfo_from_server().
+--
+
+-- use test validator function (not all libpq options supported)
+CREATE FOREIGN DATA WRAPPER regress_connection_fdw
+  VALIDATOR pg_connection_validator;
+
+\set VERBOSITY terse
+CREATE SERVER connection_server FOREIGN DATA WRAPPER regress_connection_fdw
+  OPTIONS (host 'thehost', client_encoding 'LATIN1'); -- fail
+CREATE SERVER connection_server FOREIGN DATA WRAPPER regress_connection_fdw
+  OPTIONS (host 'thehost', nonsense 'asdf'); -- fail
+CREATE SERVER connection_server FOREIGN DATA WRAPPER regress_connection_fdw
+  OPTIONS (host 'thehost', password 'secret'); -- fail
+\set VERBOSITY default
+
+CREATE SERVER connection_server FOREIGN DATA WRAPPER regress_connection_fdw
+  OPTIONS (hsot 'thehost'); -- fail - misspelling
+
+CREATE SERVER connection_server FOREIGN DATA WRAPPER regress_connection_fdw
+  OPTIONS (host 'thehost', port '5432');
+
+CREATE USER MAPPING FOR regress_test_role SERVER connection_server
+  OPTIONS (user 'role', password 'secret', host 'otherhost'); -- fail
+
+CREATE USER MAPPING FOR regress_test_role SERVER connection_server
+  OPTIONS (user 'role', password 'secret');
+CREATE USER MAPPING FOR PUBLIC SERVER connection_server
+  OPTIONS (user 'publicuser', password $pwd$'\"$# secret'$pwd$);
+
+SELECT pg_conninfo_from_server('connection_server', 'regress_test_role', false);
+
+SELECT pg_conninfo_from_server('connection_server', 'regress_test_role2', false);
+
+DROP USER MAPPING FOR regress_test_role SERVER connection_server;
+DROP USER MAPPING FOR PUBLIC SERVER connection_server;
+DROP SERVER connection_server;
+DROP FOREIGN DATA WRAPPER regress_connection_fdw;
+
 -- CREATE FOREIGN TABLE
 CREATE SCHEMA foreign_schema;
 CREATE SERVER s0 FOREIGN DATA WRAPPER dummy;
-- 
2.34.1

