PGSERVICEFILE as part of a normal connection string

Started by Torsten Förtschabout 1 year ago35 messages
#1Torsten Förtsch
tfoertsch123@gmail.com
1 attachment(s)

Hi,

I like to bundle all my database connections in a .pg_service.conf. Over
time I collected a bunch of such service files. A while back I discovered
that the service file can only be specified as an environment variable. It
cannot be given as part of the connection string like

psql "service=$MY_SERVICE servicefile=MY_SERVICE_FILE"

The attached patch allows that.

Regards.
--
Torsten

Attachments:

v1-0001-PGSERVICEFILE-as-part-of-the-connection-string.patchtext/x-patch; charset=US-ASCII; name=v1-0001-PGSERVICEFILE-as-part-of-the-connection-string.patchDownload
From f419584d2fc7766c143d304ba2f2fad98501d9ea Mon Sep 17 00:00:00 2001
From: Torsten Foertsch <tfoertsch123@gmail.com>
Date: Sat, 16 Nov 2024 20:17:20 +0100
Subject: [PATCH v1] PGSERVICEFILE as part of the connection string

Libpq interprets the PGSERVICEFILE environment variable. However,
the servicefile cannot be specified as part of the connection
string itself.

This change implements that.
---
 doc/src/sgml/libpq.sgml               | 16 ++++-
 src/interfaces/libpq/fe-connect.c     | 18 ++++-
 src/interfaces/libpq/t/006_service.pl | 97 +++++++++++++++++++++++++++
 3 files changed, 129 insertions(+), 2 deletions(-)
 create mode 100644 src/interfaces/libpq/t/006_service.pl

diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index bfefb1289e..de8830bf51 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -2202,6 +2202,19 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
       </listitem>
      </varlistentry>
 
+     <varlistentry id="libpq-connect-servicefile" xreflabel="servicefile">
+      <term><literal>servicefile</literal></term>
+      <listitem>
+       <para>
+        This option specifies the name of the per-user connection service file
+        (see <xref linkend="libpq-pgservice"/>).
+        Defaults to <filename>~/.pg_service.conf</filename>, or
+        <filename>%APPDATA%\postgresql\.pg_service.conf</filename> on
+        Microsoft Windows.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="libpq-connect-target-session-attrs" xreflabel="target_session_attrs">
       <term><literal>target_session_attrs</literal></term>
       <listitem>
@@ -9337,7 +9350,8 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough)
    On Microsoft Windows, it is named
    <filename>%APPDATA%\postgresql\.pg_service.conf</filename> (where
    <filename>%APPDATA%</filename> refers to the Application Data subdirectory
-   in the user's profile).  A different file name can be specified by
+   in the user's profile).  A different file name can be specified using the
+   <literal>servicefile</literal> key word in a libpq connection string or by
    setting the environment variable <envar>PGSERVICEFILE</envar>.
    The system-wide file is named <filename>pg_service.conf</filename>.
    By default it is sought in the <filename>etc</filename> directory
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 51083dcfd8..117588c0e8 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -192,6 +192,9 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
 	{"service", "PGSERVICE", NULL, NULL,
 	"Database-Service", "", 20, -1},
 
+	{"servicefile", "PGSERVICEFILE", NULL, NULL,
+	"Database-Service-File", "", 64, -1},
+
 	{"user", "PGUSER", NULL, NULL,
 		"Database-User", "", 20,
 	offsetof(struct pg_conn, pguser)},
@@ -5493,6 +5496,7 @@ static int
 parseServiceInfo(PQconninfoOption *options, PQExpBuffer errorMessage)
 {
 	const char *service = conninfo_getval(options, "service");
+	const char *service_fname = conninfo_getval(options, "servicefile");
 	char		serviceFile[MAXPGPATH];
 	char	   *env;
 	bool		group_found = false;
@@ -5515,7 +5519,9 @@ parseServiceInfo(PQconninfoOption *options, PQExpBuffer errorMessage)
 	 * Try PGSERVICEFILE if specified, else try ~/.pg_service.conf (if that
 	 * exists).
 	 */
-	if ((env = getenv("PGSERVICEFILE")) != NULL)
+	if (service_fname != NULL)
+		strlcpy(serviceFile, service_fname, sizeof(serviceFile));
+	else if ((env = getenv("PGSERVICEFILE")) != NULL)
 		strlcpy(serviceFile, env, sizeof(serviceFile));
 	else
 	{
@@ -5678,6 +5684,16 @@ parseServiceFile(const char *serviceFile,
 					goto exit;
 				}
 
+				if (strcmp(key, "servicefile") == 0)
+				{
+					libpq_append_error(errorMessage,
+									   "nested servicefile specifications not supported in service file \"%s\", line %d",
+									   serviceFile,
+									   linenr);
+					result = 3;
+					goto exit;
+				}
+
 				/*
 				 * Set the parameter --- but don't override any previous
 				 * explicit setting.
diff --git a/src/interfaces/libpq/t/006_service.pl b/src/interfaces/libpq/t/006_service.pl
new file mode 100644
index 0000000000..5de84b78af
--- /dev/null
+++ b/src/interfaces/libpq/t/006_service.pl
@@ -0,0 +1,97 @@
+# Copyright (c) 2023-2024, PostgreSQL Global Development Group
+use strict;
+use warnings FATAL => 'all';
+use Config;
+use PostgreSQL::Test::Utils;
+use PostgreSQL::Test::Cluster;
+use Test::More;
+
+# This tests "service" and "servicefile"
+
+# Cluster setup which is shared for testing both load balancing methods
+my $node = PostgreSQL::Test::Cluster->new('node');
+
+# Create a data directory with initdb
+$node->init();
+
+# Start the PostgreSQL server
+$node->start();
+
+my $td = PostgreSQL::Test::Utils::tempdir;
+my $srvfile = "$td/pgsrv.conf";
+
+open my $fh, '>', $srvfile or die $!;
+print $fh "[my_srv]\n";
+print $fh +($node->connstr =~ s/ /\n/gr), "\n";
+close $fh;
+
+{
+	local $ENV{PGSERVICEFILE} = $srvfile;
+	$node->connect_ok(
+		'service=my_srv',
+		'service=my_srv',
+		sql => "SELECT 'connect1'",
+		expected_stdout => qr/connect1/);
+
+	$node->connect_ok(
+		'postgres://?service=my_srv',
+		'postgres://?service=my_srv',
+		sql => "SELECT 'connect2'",
+		expected_stdout => qr/connect2/);
+
+	local $ENV{PGSERVICE} = 'my_srv';
+	$node->connect_ok(
+		'',
+		'envvar: PGSERVICE=my_srv',
+		sql => "SELECT 'connect3'",
+		expected_stdout => qr/connect3/);
+
+	$node->connect_fails(
+		'service=non-existent-service',
+		'service=non-existent-service',
+		expected_stderr => qr/definition of service "non-existent-service" not found/);
+}
+
+{
+	$node->connect_ok(
+		q{service=my_srv servicefile='}.$srvfile.q{'},
+		'service=my_srv servicefile=...',
+		sql => "SELECT 'connect4'",
+		expected_stdout => qr/connect4/);
+
+	$node->connect_ok(
+		'postgresql:///?service=my_srv&servicefile='.($srvfile =~ s!/!%2F!gr),
+		'postgresql:///?service=my_srv&servicefile=...',
+		sql => "SELECT 'connect5'",
+		expected_stdout => qr/connect5/);
+
+	local $ENV{PGSERVICE} = 'my_srv';
+	$node->connect_ok(
+		q{servicefile='}.$srvfile.q{'},
+		'envvar: PGSERVICE=my_srv + servicefile=...',
+		sql => "SELECT 'connect6'",
+		expected_stdout => qr/connect6/);
+
+	$node->connect_ok(
+		'postgresql://?servicefile='.($srvfile =~ s!/!%2F!gr),
+		'envvar: PGSERVICE=my_srv + postgresql://?servicefile=...',
+		sql => "SELECT 'connect6'",
+		expected_stdout => qr/connect6/);
+}
+
+{
+	local $ENV{PGSERVICEFILE} = 'non-existent-file.conf';
+
+	$node->connect_fails(
+		'service=my_srv',
+		'service=... fails with wrong PGSERVICEFILE',
+		expected_stderr => qr/service file "non-existent-file\.conf" not found/);
+
+	$node->connect_ok(
+		q{service=my_srv servicefile='}.$srvfile.q{'},
+		'servicefile= takes precedence over PGSERVICEFILE',
+		sql => "SELECT 'connect7'",
+		expected_stdout => qr/connect7/);
+}
+
+done_testing();
-- 
2.34.1

#2Laurenz Albe
laurenz.albe@cybertec.at
In reply to: Torsten Förtsch (#1)
Re: PGSERVICEFILE as part of a normal connection string

On Mon, 2024-11-18 at 21:21 +0100, Torsten Förtsch wrote:

I like to bundle all my database connections in a .pg_service.conf. Over time I
collected a bunch of such service files. A while back I discovered that the
service file can only be specified as an environment variable. It cannot be
given as part of the connection string like

psql "service=$MY_SERVICE servicefile=MY_SERVICE_FILE"

The attached patch allows that.

+1 for the idea (I didn't test the patch).

Yours,
Laurenz Albe

#3Michael Paquier
michael@paquier.xyz
In reply to: Torsten Förtsch (#1)
Re: PGSERVICEFILE as part of a normal connection string

On Mon, Nov 18, 2024 at 09:21:56PM +0100, Torsten Förtsch wrote:

I like to bundle all my database connections in a .pg_service.conf. Over
time I collected a bunch of such service files. A while back I discovered
that the service file can only be specified as an environment variable. It
cannot be given as part of the connection string like

-    if ((env = getenv("PGSERVICEFILE")) != NULL)
+    if (service_fname != NULL)
+        strlcpy(serviceFile, service_fname, sizeof(serviceFile));
+    else if ((env = getenv("PGSERVICEFILE")) != NULL)
         strlcpy(serviceFile, env, sizeof(serviceFile));

That should be right, the connection parameter takes priority over the
environment variable. The comment at the top of this code block
becomes incorrect.

else
{
@@ -5678,6 +5684,16 @@ parseServiceFile(const char *serviceFile,
goto exit;
}

+                if (strcmp(key, "servicefile") == 0)
+                {
+                    libpq_append_error(errorMessage,
+                                       "nested servicefile specifications not supported in service file \"%s\", line %d",
+                                       serviceFile,
+                                       linenr);
+                    result = 3;
+                    goto exit;
+                }

Interesting. We've never had tests for that even for "service".
Perhaps it would be the time to add some tests for the existing case
and the one you are adding? Your test suite should make that easy to
add.

+# This tests "service" and "servicefile"

You are introducing tests for the existing "service", as well as tests
for the new "servicefile". Could it be possible to split that into
two patches for clarity? You'd want one to provide coverage for the
existing features (PGSERVICEFILE, PGSERVICE and connection parameter
"service"), then add tests for the new feature "servicename" with its
libpq implementation. That would make your main patch simpler, as
well.

+open my $fh, '>', $srvfile or die $!;
+print $fh "[my_srv]\n";
+print $fh +($node->connstr =~ s/ /\n/gr), "\n";
+close $fh;

Sure that's OK on Windows where we have CRLFs, not just LFs?
--
Michael

#4Corey Huinker
corey.huinker@gmail.com
In reply to: Michael Paquier (#3)
Re: PGSERVICEFILE as part of a normal connection string

Interesting. We've never had tests for that even for "service".
Perhaps it would be the time to add some tests for the existing case
and the one you are adding? Your test suite should make that easy to
add.

Currently, a lot of our utility scripts (anything that uses
connectDatabase) don't support service=name params or PGSERVICE=name env
vars, which is really too bad. I previously thought that this was because
of a lack of interest, but perhaps people do want it?

#5Michael Paquier
michael@paquier.xyz
In reply to: Corey Huinker (#4)
Re: PGSERVICEFILE as part of a normal connection string

On Wed, Nov 20, 2024 at 02:58:43AM -0500, Corey Huinker wrote:

Currently, a lot of our utility scripts (anything that uses
connectDatabase) don't support service=name params or PGSERVICE=name env
vars, which is really too bad. I previously thought that this was because
of a lack of interest, but perhaps people do want it?

I'm all for more test coverage, FWIW.

Torsten, the patch has been waiting on input from you based on my
latest review for some time, so I have marked it as returned with
feedback in the CP app. Feel free to resubmit a new version if you
are planning to work on that.

Thanks.
--
Michael

#6Ryo Kanbayashi
kanbayashi.dev@gmail.com
In reply to: Michael Paquier (#5)
Re: PGSERVICEFILE as part of a normal connection string

On Mon, Jan 27, 2025 at 2:01 PM Michael Paquier <michael@paquier.xyz> wrote:

On Wed, Nov 20, 2024 at 02:58:43AM -0500, Corey Huinker wrote:

Currently, a lot of our utility scripts (anything that uses
connectDatabase) don't support service=name params or PGSERVICE=name env
vars, which is really too bad. I previously thought that this was because
of a lack of interest, but perhaps people do want it?

I'm all for more test coverage, FWIW.

Torsten, the patch has been waiting on input from you based on my
latest review for some time, so I have marked it as returned with
feedback in the CP app. Feel free to resubmit a new version if you
are planning to work on that.

TO: Torsten,
CC: Micael and other hackers

If you can't work for ther patch for a while because you are busy or
other some reason,
I can become additinal reviewer and apply review comments from Micael
to the patch instead of you.

If you don't want my action, please reply and notice me that. If
possible, within a week :)

Just to let you know, my action is not intended to steal your
contribution but to prevent your good idea from being lost.

TO: Mecael and other hackers,

There are any problem in light of community customs?

---
Great regards,
Ryo Kanbayashi

#7Michael Paquier
michael@paquier.xyz
In reply to: Ryo Kanbayashi (#6)
Re: PGSERVICEFILE as part of a normal connection string

On Thu, Mar 13, 2025 at 08:53:49AM +0900, Ryo Kanbayashi wrote:

If you can't work for ther patch for a while because you are busy or
other some reason,
I can become additinal reviewer and apply review comments from Micael
to the patch instead of you.

If you don't want my action, please reply and notice me that. If
possible, within a week :)

Putting a bit of context here. Most of the Postgres hackers based in
Japan had a meeting last Friday, and Kanbayashi-san has asked me about
patches that introduce to simpler code paths in the tree that could be
worked on for this release. I've mentioned this thread to him.

Just to let you know, my action is not intended to steal your
contribution but to prevent your good idea from being lost.

Authors and reviewers get busy because of life and work matters, and
contributions are listed in the commit logs for everybody who
participates. If you can help move this patch forward, thanks a lot
for the help! IMO, that would be great. The patch set still needs
more reorganization and adjustments, but I think that we can get it
there.
--
Michael

#8Laurenz Albe
laurenz.albe@cybertec.at
In reply to: Ryo Kanbayashi (#6)
Re: PGSERVICEFILE as part of a normal connection string

On Thu, 2025-03-13 at 08:53 +0900, Ryo Kanbayashi wrote:

Just to let you know, my action is not intended to steal your
contribution but to prevent your good idea from being lost.

TO: Mecael and other hackers,

There are any problem in light of community customs?

Anything submitted to the mailing list is no longer private
intellectual property. You are free and welcome to start working
on any patch that you are interested in and that seems neglected
by the author. There is no problem with listing more than one
author.

Yours,
Laurenz Albe

#9Ryo Kanbayashi
kanbayashi.dev@gmail.com
In reply to: Laurenz Albe (#8)
Re: PGSERVICEFILE as part of a normal connection string

On Thu, Mar 13, 2025 at 9:42 AM Michael Paquier <michael@paquier.xyz> wrote:

On Thu, Mar 13, 2025 at 08:53:49AM +0900, Ryo Kanbayashi wrote:

If you can't work for ther patch for a while because you are busy or
other some reason,
I can become additinal reviewer and apply review comments from Micael
to the patch instead of you.

If you don't want my action, please reply and notice me that. If
possible, within a week :)

Putting a bit of context here. Most of the Postgres hackers based in
Japan had a meeting last Friday, and Kanbayashi-san has asked me about
patches that introduce to simpler code paths in the tree that could be
worked on for this release. I've mentioned this thread to him.

Just to let you know, my action is not intended to steal your
contribution but to prevent your good idea from being lost.

Authors and reviewers get busy because of life and work matters, and
contributions are listed in the commit logs for everybody who
participates. If you can help move this patch forward, thanks a lot
for the help! IMO, that would be great. The patch set still needs
more reorganization and adjustments, but I think that we can get it
there.

On Thu, Mar 13, 2025 at 3:07 PM Laurenz Albe <laurenz.albe@cybertec.at> wrote:

On Thu, 2025-03-13 at 08:53 +0900, Ryo Kanbayashi wrote:

Just to let you know, my action is not intended to steal your
contribution but to prevent your good idea from being lost.

TO: Mecael and other hackers,

There are any problem in light of community customs?

Anything submitted to the mailing list is no longer private
intellectual property. You are free and welcome to start working
on any patch that you are interested in and that seems neglected
by the author. There is no problem with listing more than one
author.

Michael and Laurenz,

Thank you for context description and comments to my action :)

I start coding to complete the patch :)

---
Great regards,
Ryo Kanbayashi

#10Ryo Kanbayashi
kanbayashi.dev@gmail.com
In reply to: Michael Paquier (#7)
2 attachment(s)
Re: PGSERVICEFILE as part of a normal connection string

On Mon, Jan 27, 2025 at 2:01 PM Michael Paquier <michael@paquier.xyz> wrote:

On Thu, Mar 13, 2025 at 08:53:49AM +0900, Ryo Kanbayashi wrote:

Putting a bit of context here. Most of the Postgres hackers based in
Japan had a meeting last Friday, and Kanbayashi-san has asked me about
patches that introduce to simpler code paths in the tree that could be
worked on for this release. I've mentioned this thread to him.

Just to let you know, my action is not intended to steal your
contribution but to prevent your good idea from being lost.

Authors and reviewers get busy because of life and work matters, and
contributions are listed in the commit logs for everybody who
participates. If you can help move this patch forward, thanks a lot
for the help! IMO, that would be great. The patch set still needs
more reorganization and adjustments, but I think that we can get it
there

Michael,
CC: Torsten

I reviewed the patch and add some modification described below.

part of /messages/by-id/Zz2AE7NKKLIZTtEh@paquier.xyz

+# This tests "service" and "servicefile"

You are introducing tests for the existing "service", as well as tests
for the new "servicefile". Could it be possible to split that into
two patches for clarity? You'd want one to provide coverage for the
existing features (PGSERVICEFILE, PGSERVICE and connection parameter
"service"), then add tests for the new feature "servicename" with its
libpq implementation. That would make your main patch simpler, as
well.

+open my $fh, '>', $srvfile or die $!;
+print $fh "[my_srv]\n";
+print $fh +($node->connstr =~ s/ /\n/gr), "\n";
+close $fh;

Sure that's OK on Windows where we have CRLFs, not just LFs?

I did...
* Split the patch to two patches
1) regression test of existing features.
2) adding servicefile option feature, its regression test and etc
* Add codes which care new line code of Windows
* Add comments and apply formatter :)

---
Great Regards,
Ryo Kanbayashi

Attachments:

v1-0001-add-regression-test-of-service-option.patchapplication/octet-stream; name=v1-0001-add-regression-test-of-service-option.patchDownload
diff --git a/src/interfaces/libpq/meson.build b/src/interfaces/libpq/meson.build
index 19f4a52a97a..292fecf3320 100644
--- a/src/interfaces/libpq/meson.build
+++ b/src/interfaces/libpq/meson.build
@@ -122,6 +122,7 @@ tests += {
       't/003_load_balance_host_list.pl',
       't/004_load_balance_dns.pl',
       't/005_negotiate_encryption.pl',
+      't/006_service.pl',
     ],
     'env': {
       'with_ssl': ssl_library,
diff --git a/src/interfaces/libpq/t/006_service.pl b/src/interfaces/libpq/t/006_service.pl
new file mode 100644
index 00000000000..24c9a915a42
--- /dev/null
+++ b/src/interfaces/libpq/t/006_service.pl
@@ -0,0 +1,82 @@
+# Copyright (c) 2023-2024, PostgreSQL Global Development Group
+use strict;
+use warnings FATAL => 'all';
+use Config;
+use PostgreSQL::Test::Utils;
+use PostgreSQL::Test::Cluster;
+use Test::More;
+
+# This tests "service"
+
+# Cluster setup which is shared for testing both load balancing methods
+my $node = PostgreSQL::Test::Cluster->new('node');
+
+# Create a data directory with initdb
+$node->init();
+
+# Start the PostgreSQL server
+$node->start();
+
+my $td      = PostgreSQL::Test::Utils::tempdir;
+my $srvfile = "$td/pgsrv.conf";
+
+# Create a service file
+open my $fh, '>', $srvfile or die $!;
+if ($windows_os) {
+
+    # Windows: use CRLF
+    print $fh "[my_srv]",                                   "\r\n";
+    print $fh join( "\r\n", split( ' ', $node->connstr ) ), "\r\n";
+}
+else {
+    # Non-Windows: use LF
+    print $fh "[my_srv]",                                 "\n";
+    print $fh join( "\n", split( ' ', $node->connstr ) ), "\n";
+}
+close $fh;
+
+# Check that service option works as expected
+{
+    local $ENV{PGSERVICEFILE} = $srvfile;
+    $node->connect_ok(
+        'service=my_srv',
+        'service=my_srv',
+        sql             => "SELECT 'connect1'",
+        expected_stdout => qr/connect1/
+    );
+
+    $node->connect_ok(
+        'postgres://?service=my_srv',
+        'postgres://?service=my_srv',
+        sql             => "SELECT 'connect2'",
+        expected_stdout => qr/connect2/
+    );
+
+    local $ENV{PGSERVICE} = 'my_srv';
+    $node->connect_ok(
+        '',
+        'envvar: PGSERVICE=my_srv',
+        sql             => "SELECT 'connect3'",
+        expected_stdout => qr/connect3/
+    );
+}
+
+# Check that not existing service fails
+{
+    local $ENV{PGSERVICE} = 'non-existent-service';
+    $node->connect_fails(
+        '',
+        'envvar: PGSERVICE=non-existent-service',
+        expected_stdout =>
+          qr/definition of service "non-existent-service" not found/
+    );
+
+    $node->connect_fails(
+        'service=non-existent-service',
+        'service=non-existent-service',
+        expected_stderr =>
+          qr/definition of service "non-existent-service" not found/
+    );
+}
+
+done_testing();
v2-0001-PGSERVICEFILE-as-part-of-the-connection-string.patchapplication/octet-stream; name=v2-0001-PGSERVICEFILE-as-part-of-the-connection-string.patchDownload
diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 8fa0515c6a0..1050db5c983 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -2248,6 +2248,19 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
       </listitem>
      </varlistentry>
 
+     <varlistentry id="libpq-connect-servicefile" xreflabel="servicefile">
+      <term><literal>servicefile</literal></term>
+      <listitem>
+       <para>
+        This option specifies the name of the per-user connection service file
+        (see <xref linkend="libpq-pgservice"/>).
+        Defaults to <filename>~/.pg_service.conf</filename>, or
+        <filename>%APPDATA%\postgresql\.pg_service.conf</filename> on
+        Microsoft Windows.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="libpq-connect-target-session-attrs" xreflabel="target_session_attrs">
       <term><literal>target_session_attrs</literal></term>
       <listitem>
@@ -9504,7 +9517,8 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough)
    On Microsoft Windows, it is named
    <filename>%APPDATA%\postgresql\.pg_service.conf</filename> (where
    <filename>%APPDATA%</filename> refers to the Application Data subdirectory
-   in the user's profile).  A different file name can be specified by
+   in the user's profile).  A different file name can be specified using the
+   <literal>servicefile</literal> key word in a libpq connection string or by
    setting the environment variable <envar>PGSERVICEFILE</envar>.
    The system-wide file is named <filename>pg_service.conf</filename>.
    By default it is sought in the <filename>etc</filename> directory
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index d5051f5e820..f16468be7d1 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -195,6 +195,9 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
 		"Database-Service", "", 20,
 	offsetof(struct pg_conn, pgservice)},
 
+	{"servicefile", "PGSERVICEFILE", NULL, NULL,
+	"Database-Service-File", "", 64, -1},
+
 	{"user", "PGUSER", NULL, NULL,
 		"Database-User", "", 20,
 	offsetof(struct pg_conn, pguser)},
@@ -5838,6 +5841,7 @@ static int
 parseServiceInfo(PQconninfoOption *options, PQExpBuffer errorMessage)
 {
 	const char *service = conninfo_getval(options, "service");
+	const char *service_fname = conninfo_getval(options, "servicefile");
 	char		serviceFile[MAXPGPATH];
 	char	   *env;
 	bool		group_found = false;
@@ -5857,11 +5861,18 @@ parseServiceInfo(PQconninfoOption *options, PQExpBuffer errorMessage)
 		return 0;
 
 	/*
-	 * Try PGSERVICEFILE if specified, else try ~/.pg_service.conf (if that
-	 * exists).
+	 * First, check servicefile option on connection string. Second, check
+	 * PGSERVICEFILE environment variable. Finally, check ~/.pg_service.conf
+	 * (if that exists).
 	 */
-	if ((env = getenv("PGSERVICEFILE")) != NULL)
+	if (service_fname != NULL)
+	{
+		strlcpy(serviceFile, service_fname, sizeof(serviceFile));
+	}
+	else if ((env = getenv("PGSERVICEFILE")) != NULL)
+	{
 		strlcpy(serviceFile, env, sizeof(serviceFile));
+	}
 	else
 	{
 		char		homedir[MAXPGPATH];
@@ -6023,6 +6034,16 @@ parseServiceFile(const char *serviceFile,
 					goto exit;
 				}
 
+				if (strcmp(key, "servicefile") == 0)
+				{
+					libpq_append_error(errorMessage,
+									   "nested servicefile specifications not supported in service file \"%s\", line %d",
+									   serviceFile,
+									   linenr);
+					result = 3;
+					goto exit;
+				}
+
 				/*
 				 * Set the parameter --- but don't override any previous
 				 * explicit setting.
diff --git a/src/interfaces/libpq/meson.build b/src/interfaces/libpq/meson.build
index 292fecf3320..4714c253792 100644
--- a/src/interfaces/libpq/meson.build
+++ b/src/interfaces/libpq/meson.build
@@ -123,6 +123,7 @@ tests += {
       't/004_load_balance_dns.pl',
       't/005_negotiate_encryption.pl',
       't/006_service.pl',
+      't/007_servicefile_opt.pl',
     ],
     'env': {
       'with_ssl': ssl_library,
diff --git a/src/interfaces/libpq/t/007_servicefile_opt.pl b/src/interfaces/libpq/t/007_servicefile_opt.pl
new file mode 100644
index 00000000000..7d33bd18563
--- /dev/null
+++ b/src/interfaces/libpq/t/007_servicefile_opt.pl
@@ -0,0 +1,100 @@
+# Copyright (c) 2023-2024, PostgreSQL Global Development Group
+use strict;
+use warnings FATAL => 'all';
+use Config;
+use PostgreSQL::Test::Utils;
+use PostgreSQL::Test::Cluster;
+use Test::More;
+
+# This tests "servicefile option" on connection string
+
+# Cluster setup which is shared for testing both load balancing methods
+my $node = PostgreSQL::Test::Cluster->new('node');
+
+# Create a data directory with initdb
+$node->init();
+
+# Start the PostgreSQL server
+$node->start();
+
+my $td      = PostgreSQL::Test::Utils::tempdir;
+my $srvfile = "$td/pgsrv.conf";
+
+# Create a service file
+open my $fh, '>', $srvfile or die $!;
+if ($windows_os) {
+
+    # Windows: use CRLF
+    print $fh "[my_srv]",                                   "\r\n";
+    print $fh join( "\r\n", split( ' ', $node->connstr ) ), "\r\n";
+
+    # Escape backslashes for use in connection string later
+    $srvfile =~ s/\\/\\\\/g;
+}
+else {
+    # Non-Windows: use LF
+    print $fh "[my_srv]",                                 "\n";
+    print $fh join( "\n", split( ' ', $node->connstr ) ), "\n";
+}
+close $fh;
+
+# Check that servicefile option works as expected
+{
+    $node->connect_ok(
+        q{service=my_srv servicefile='} . $srvfile . q{'},
+        'service=my_srv servicefile=...',
+        sql             => "SELECT 'connect1'",
+        expected_stdout => qr/connect1/
+    );
+
+    # Escape slashes in servicefile path for use in connection string
+    # Consider that the servicefile path may contain backslashes on Windows
+    my $encoded_srvfile = $srvfile =~ s{([\\/])}{
+        $1 eq '/' ? '%2F' : '%5C'
+    }ger;
+
+    # Escape a colon in servicefile path of Windows
+    $encoded_srvfile =~ s/:/%3A/g;
+
+    $node->connect_ok(
+        'postgresql:///?service=my_srv&servicefile=' . $encoded_srvfile,
+        'postgresql:///?service=my_srv&servicefile=...',
+        sql             => "SELECT 'connect2'",
+        expected_stdout => qr/connect2/
+    );
+
+    local $ENV{PGSERVICE} = 'my_srv';
+    $node->connect_ok(
+        q{servicefile='} . $srvfile . q{'},
+        'envvar: PGSERVICE=my_srv + servicefile=...',
+        sql             => "SELECT 'connect3'",
+        expected_stdout => qr/connect3/
+    );
+
+    $node->connect_ok(
+        'postgresql://?servicefile=' . $encoded_srvfile,
+        'envvar: PGSERVICE=my_srv + postgresql://?servicefile=...',
+        sql             => "SELECT 'connect4'",
+        expected_stdout => qr/connect4/
+    );
+}
+
+# Check that servicefile option takes precedence over PGSERVICEFILE environment variable
+{
+    local $ENV{PGSERVICEFILE} = 'non-existent-file.conf';
+
+    $node->connect_fails(
+        'service=my_srv',
+        'service=... fails with wrong PGSERVICEFILE',
+        expected_stderr => qr/service file "non-existent-file\.conf" not found/
+    );
+
+    $node->connect_ok(
+        q{service=my_srv servicefile='} . $srvfile . q{'},
+        'servicefile= takes precedence over PGSERVICEFILE',
+        sql             => "SELECT 'connect5'",
+        expected_stdout => qr/connect5/
+    );
+}
+
+done_testing();
#11Ryo Kanbayashi
kanbayashi.dev@gmail.com
In reply to: Ryo Kanbayashi (#10)
1 attachment(s)
[PATCH] PGSERVICEFILE as part of a normal connection string

On Thu, Mar 20, 2025 at 5:39 PM Ryo Kanbayashi <kanbayashi.dev@gmail.com> wrote:

On Mon, Jan 27, 2025 at 2:01 PM Michael Paquier <michael@paquier.xyz> wrote:

On Thu, Mar 13, 2025 at 08:53:49AM +0900, Ryo Kanbayashi wrote:

Putting a bit of context here. Most of the Postgres hackers based in
Japan had a meeting last Friday, and Kanbayashi-san has asked me about
patches that introduce to simpler code paths in the tree that could be
worked on for this release. I've mentioned this thread to him.

Just to let you know, my action is not intended to steal your
contribution but to prevent your good idea from being lost.

Authors and reviewers get busy because of life and work matters, and
contributions are listed in the commit logs for everybody who
participates. If you can help move this patch forward, thanks a lot
for the help! IMO, that would be great. The patch set still needs
more reorganization and adjustments, but I think that we can get it
there

Michael,
CC: Torsten

I reviewed the patch and add some modification described below.

part of /messages/by-id/Zz2AE7NKKLIZTtEh@paquier.xyz

+# This tests "service" and "servicefile"

You are introducing tests for the existing "service", as well as tests
for the new "servicefile". Could it be possible to split that into
two patches for clarity? You'd want one to provide coverage for the
existing features (PGSERVICEFILE, PGSERVICE and connection parameter
"service"), then add tests for the new feature "servicename" with its
libpq implementation. That would make your main patch simpler, as
well.

+open my $fh, '>', $srvfile or die $!;
+print $fh "[my_srv]\n";
+print $fh +($node->connstr =~ s/ /\n/gr), "\n";
+close $fh;

Sure that's OK on Windows where we have CRLFs, not just LFs?

I did...
* Split the patch to two patches
1) regression test of existing features.
2) adding servicefile option feature, its regression test and etc
* Add codes which care new line code of Windows
* Add comments and apply formatter :)

Sorry, I found a miss on 006_service.pl.
Fixed patch is attached...

---
Great Regards,
Ryo Kanbayashi

Attachments:

v2-0001-add-regression-test-of-service-option.patchapplication/octet-stream; name=v2-0001-add-regression-test-of-service-option.patchDownload
diff --git a/src/interfaces/libpq/meson.build b/src/interfaces/libpq/meson.build
index 19f4a52a97a..292fecf3320 100644
--- a/src/interfaces/libpq/meson.build
+++ b/src/interfaces/libpq/meson.build
@@ -122,6 +122,7 @@ tests += {
       't/003_load_balance_host_list.pl',
       't/004_load_balance_dns.pl',
       't/005_negotiate_encryption.pl',
+      't/006_service.pl',
     ],
     'env': {
       'with_ssl': ssl_library,
diff --git a/src/interfaces/libpq/t/006_service.pl b/src/interfaces/libpq/t/006_service.pl
new file mode 100644
index 00000000000..24c9a915a42
--- /dev/null
+++ b/src/interfaces/libpq/t/006_service.pl
@@ -0,0 +1,82 @@
+# Copyright (c) 2023-2024, PostgreSQL Global Development Group
+use strict;
+use warnings FATAL => 'all';
+use Config;
+use PostgreSQL::Test::Utils;
+use PostgreSQL::Test::Cluster;
+use Test::More;
+
+# This tests "service"
+
+# Cluster setup which is shared for testing both load balancing methods
+my $node = PostgreSQL::Test::Cluster->new('node');
+
+# Create a data directory with initdb
+$node->init();
+
+# Start the PostgreSQL server
+$node->start();
+
+my $td      = PostgreSQL::Test::Utils::tempdir;
+my $srvfile = "$td/pgsrv.conf";
+
+# Create a service file
+open my $fh, '>', $srvfile or die $!;
+if ($windows_os) {
+
+    # Windows: use CRLF
+    print $fh "[my_srv]",                                   "\r\n";
+    print $fh join( "\r\n", split( ' ', $node->connstr ) ), "\r\n";
+}
+else {
+    # Non-Windows: use LF
+    print $fh "[my_srv]",                                 "\n";
+    print $fh join( "\n", split( ' ', $node->connstr ) ), "\n";
+}
+close $fh;
+
+# Check that service option works as expected
+{
+    local $ENV{PGSERVICEFILE} = $srvfile;
+    $node->connect_ok(
+        'service=my_srv',
+        'service=my_srv',
+        sql             => "SELECT 'connect1'",
+        expected_stdout => qr/connect1/
+    );
+
+    $node->connect_ok(
+        'postgres://?service=my_srv',
+        'postgres://?service=my_srv',
+        sql             => "SELECT 'connect2'",
+        expected_stdout => qr/connect2/
+    );
+
+    local $ENV{PGSERVICE} = 'my_srv';
+    $node->connect_ok(
+        '',
+        'envvar: PGSERVICE=my_srv',
+        sql             => "SELECT 'connect3'",
+        expected_stdout => qr/connect3/
+    );
+}
+
+# Check that not existing service fails
+{
+    local $ENV{PGSERVICEFILE} = $srvfile;
+    local $ENV{PGSERVICE} = 'non-existent-service';
+    $node->connect_fails(
+        '',
+        'envvar: PGSERVICE=non-existent-service',
+        expected_stdout =>
+          qr/definition of service "non-existent-service" not found/
+    );
+
+    $node->connect_fails(
+        'service=non-existent-service',
+        'service=non-existent-service',
+        expected_stderr =>
+          qr/definition of service "non-existent-service" not found/
+    );
+}
+
+done_testing();
#12Michael Paquier
michael@paquier.xyz
In reply to: Ryo Kanbayashi (#11)
Re: [PATCH] PGSERVICEFILE as part of a normal connection string

On Thu, Mar 20, 2025 at 06:16:44PM +0900, Ryo Kanbayashi wrote:

Sorry, I found a miss on 006_service.pl.
Fixed patch is attached...

Please note that the commit fest app needs all the patches of a a set
to be posted in the same message. In this case, v2-0001 is not going
to get automatic test coverage.

Your patch naming policy is also a bit confusing. I would suggest to
use `git format-patch -vN -2`, where N is your version number. 0001
would be the new tests for service files, and 0002 the new feature,
with its own tests.

+if ($windows_os) {
+
+    # Windows: use CRLF
+    print $fh "[my_srv]",                                   "\r\n";
+    print $fh join( "\r\n", split( ' ', $node->connstr ) ), "\r\n";
+}
+else {
+    # Non-Windows: use LF
+    print $fh "[my_srv]",                                 "\n";
+    print $fh join( "\n", split( ' ', $node->connstr ) ), "\n";
+}
+close $fh;

That's duplicated. Let's perhaps use a $newline variable and print
into the file using the $newline?

Question: you are doing things this way in the test because fgets() is
what is used by libpq to retrieve the lines of the service file, is
that right?

Please note that the CI is failing. It seems to me that you are
missing a done_testing() at the end of the script. If you have a
github account, I'd suggest to set up a CI in your own fork of
Postgres, this is really helpful to double-check the correctness of a
patch before posting it to the lists, and saves in round trips between
author and reviewer. Please see src/tools/ci/README in the code tree
for details.

+# Copyright (c) 2023-2024, PostgreSQL Global Development Group

These dates are incorrect. Should be 2025, as it's a new file.

+++ b/src/interfaces/libpq/t/007_servicefile_opt.pl
@@ -0,0 +1,100 @@
+# Copyright (c) 2023-2024, PostgreSQL Global Development Group

Incorrect date again in the second path with the new feature. I'd
suggest to merge all the tests in a single script, with only one node
initialized and started.
--
Michael

#13Ryo Kanbayashi
kanbayashi.dev@gmail.com
In reply to: Michael Paquier (#12)
2 attachment(s)
Re: [PATCH] PGSERVICEFILE as part of a normal connection string

On Sat, Mar 22, 2025 at 4:46 PM Michael Paquier <michael@paquier.xyz> wrote:

On Thu, Mar 20, 2025 at 06:16:44PM +0900, Ryo Kanbayashi wrote:

Sorry, I found a miss on 006_service.pl.
Fixed patch is attached...

Please note that the commit fest app needs all the patches of a a set
to be posted in the same message. In this case, v2-0001 is not going
to get automatic test coverage.

Your patch naming policy is also a bit confusing. I would suggest to
use `git format-patch -vN -2`, where N is your version number. 0001
would be the new tests for service files, and 0002 the new feature,
with its own tests.

All right.
I attached patches generated with your suggested command :)

+if ($windows_os) {
+
+    # Windows: use CRLF
+    print $fh "[my_srv]",                                   "\r\n";
+    print $fh join( "\r\n", split( ' ', $node->connstr ) ), "\r\n";
+}
+else {
+    # Non-Windows: use LF
+    print $fh "[my_srv]",                                 "\n";
+    print $fh join( "\n", split( ' ', $node->connstr ) ), "\n";
+}
+close $fh;

That's duplicated. Let's perhaps use a $newline variable and print
into the file using the $newline?

OK.
I reflected above comment.

Question: you are doing things this way in the test because fgets() is
what is used by libpq to retrieve the lines of the service file, is
that right?

No. I'm doing above way simply because line ending code of service file
wrote by users may become CRLF in Windows platform.

Please note that the CI is failing. It seems to me that you are
missing a done_testing() at the end of the script. If you have a
github account, I'd suggest to set up a CI in your own fork of
Postgres, this is really helpful to double-check the correctness of a
patch before posting it to the lists, and saves in round trips between
author and reviewer. Please see src/tools/ci/README in the code tree
for details.

Sorry.
I'm using Cirrus CI with GitHub and I checked passing the CI.
But there were misses when I created patch files...

+# Copyright (c) 2023-2024, PostgreSQL Global Development Group

These dates are incorrect. Should be 2025, as it's a new file.

OK.

+++ b/src/interfaces/libpq/t/007_servicefile_opt.pl
@@ -0,0 +1,100 @@
+# Copyright (c) 2023-2024, PostgreSQL Global Development Group

Incorrect date again in the second path with the new feature. I'd
suggest to merge all the tests in a single script, with only one node
initialized and started.

OK.
Additional test scripts have been merged to a single script ^^ b

---
Great regards,
Ryo Kanbayashi

Attachments:

v3-0001-add-regression-test-of-service-connection-option.patchapplication/x-patch; name=v3-0001-add-regression-test-of-service-connection-option.patchDownload
From 69c4f4eb8e0ed40c434867fbb740a68383623da9 Mon Sep 17 00:00:00 2001
From: Ryo Kanbayashi <ryo.contact@gmail.com>
Date: Sun, 23 Mar 2025 11:41:06 +0900
Subject: [PATCH v3 1/2] add regression test of service connection option.

---
 src/interfaces/libpq/meson.build      |  1 +
 src/interfaces/libpq/t/006_service.pl | 79 +++++++++++++++++++++++++++
 2 files changed, 80 insertions(+)
 create mode 100644 src/interfaces/libpq/t/006_service.pl

diff --git a/src/interfaces/libpq/meson.build b/src/interfaces/libpq/meson.build
index 19f4a52a97a..292fecf3320 100644
--- a/src/interfaces/libpq/meson.build
+++ b/src/interfaces/libpq/meson.build
@@ -122,6 +122,7 @@ tests += {
       't/003_load_balance_host_list.pl',
       't/004_load_balance_dns.pl',
       't/005_negotiate_encryption.pl',
+      't/006_service.pl',
     ],
     'env': {
       'with_ssl': ssl_library,
diff --git a/src/interfaces/libpq/t/006_service.pl b/src/interfaces/libpq/t/006_service.pl
new file mode 100644
index 00000000000..045e25a05d3
--- /dev/null
+++ b/src/interfaces/libpq/t/006_service.pl
@@ -0,0 +1,79 @@
+# Copyright (c) 2025, PostgreSQL Global Development Group
+use strict;
+use warnings FATAL => 'all';
+use Config;
+use PostgreSQL::Test::Utils;
+use PostgreSQL::Test::Cluster;
+use Test::More;
+
+# This tests "service" connection options.
+
+# Cluster setup which is shared for testing both load balancing methods
+my $node = PostgreSQL::Test::Cluster->new('node');
+
+# Create a data directory with initdb
+$node->init();
+
+# Start the PostgreSQL server
+$node->start();
+
+my $td      = PostgreSQL::Test::Utils::tempdir;
+my $srvfile = "$td/pgsrv.conf";
+
+# Windows: use CRLF
+# Non-Windows: use LF
+my $newline = $windows_os ? "\r\n" : "\n";
+
+# Create a service file
+open my $fh, '>', $srvfile or die $!;
+print $fh "[my_srv]",                                     $newline;
+print $fh join( $newline, split( ' ', $node->connstr ) ), $newline;
+
+close $fh;
+
+# Check that service option works as expected
+{
+    local $ENV{PGSERVICEFILE} = $srvfile;
+    $node->connect_ok(
+        'service=my_srv',
+        'service=my_srv',
+        sql             => "SELECT 'connect1'",
+        expected_stdout => qr/connect1/
+    );
+
+    $node->connect_ok(
+        'postgres://?service=my_srv',
+        'postgres://?service=my_srv',
+        sql             => "SELECT 'connect2'",
+        expected_stdout => qr/connect2/
+    );
+
+    local $ENV{PGSERVICE} = 'my_srv';
+    $node->connect_ok(
+        '',
+        'envvar: PGSERVICE=my_srv',
+        sql             => "SELECT 'connect3'",
+        expected_stdout => qr/connect3/
+    );
+}
+
+# Check that not existing service fails
+{
+    local $ENV{PGSERVICEFILE} = $srvfile;
+    local $ENV{PGSERVICE} = 'non-existent-service';
+    $node->connect_fails(
+        '',
+        'envvar: PGSERVICE=non-existent-service',
+        expected_stdout =>
+          qr/definition of service "non-existent-service" not found/
+    );
+
+    $node->connect_fails(
+        'service=non-existent-service',
+        'service=non-existent-service',
+        expected_stderr =>
+          qr/definition of service "non-existent-service" not found/
+    );
+}
+
+done_testing();
\ No newline at end of file
-- 
2.25.1

v3-0002-add-servicefile-connection-option-feature.patchapplication/x-patch; name=v3-0002-add-servicefile-connection-option-feature.patchDownload
From df4ce542960afdba9fd4c619c4e40671ed4c09fd Mon Sep 17 00:00:00 2001
From: Ryo Kanbayashi <ryo.contact@gmail.com>
Date: Sun, 23 Mar 2025 11:44:15 +0900
Subject: [PATCH v3 2/2] add servicefile connection option feature

---
 doc/src/sgml/libpq.sgml               | 16 ++++++-
 src/interfaces/libpq/fe-connect.c     | 27 +++++++++--
 src/interfaces/libpq/t/006_service.pl | 65 ++++++++++++++++++++++++++-
 3 files changed, 103 insertions(+), 5 deletions(-)

diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 8fa0515c6a0..1050db5c983 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -2248,6 +2248,19 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
       </listitem>
      </varlistentry>
 
+     <varlistentry id="libpq-connect-servicefile" xreflabel="servicefile">
+      <term><literal>servicefile</literal></term>
+      <listitem>
+       <para>
+        This option specifies the name of the per-user connection service file
+        (see <xref linkend="libpq-pgservice"/>).
+        Defaults to <filename>~/.pg_service.conf</filename>, or
+        <filename>%APPDATA%\postgresql\.pg_service.conf</filename> on
+        Microsoft Windows.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="libpq-connect-target-session-attrs" xreflabel="target_session_attrs">
       <term><literal>target_session_attrs</literal></term>
       <listitem>
@@ -9504,7 +9517,8 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough)
    On Microsoft Windows, it is named
    <filename>%APPDATA%\postgresql\.pg_service.conf</filename> (where
    <filename>%APPDATA%</filename> refers to the Application Data subdirectory
-   in the user's profile).  A different file name can be specified by
+   in the user's profile).  A different file name can be specified using the
+   <literal>servicefile</literal> key word in a libpq connection string or by
    setting the environment variable <envar>PGSERVICEFILE</envar>.
    The system-wide file is named <filename>pg_service.conf</filename>.
    By default it is sought in the <filename>etc</filename> directory
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index d5051f5e820..f16468be7d1 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -195,6 +195,9 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
 		"Database-Service", "", 20,
 	offsetof(struct pg_conn, pgservice)},
 
+	{"servicefile", "PGSERVICEFILE", NULL, NULL,
+	"Database-Service-File", "", 64, -1},
+
 	{"user", "PGUSER", NULL, NULL,
 		"Database-User", "", 20,
 	offsetof(struct pg_conn, pguser)},
@@ -5838,6 +5841,7 @@ static int
 parseServiceInfo(PQconninfoOption *options, PQExpBuffer errorMessage)
 {
 	const char *service = conninfo_getval(options, "service");
+	const char *service_fname = conninfo_getval(options, "servicefile");
 	char		serviceFile[MAXPGPATH];
 	char	   *env;
 	bool		group_found = false;
@@ -5857,11 +5861,18 @@ parseServiceInfo(PQconninfoOption *options, PQExpBuffer errorMessage)
 		return 0;
 
 	/*
-	 * Try PGSERVICEFILE if specified, else try ~/.pg_service.conf (if that
-	 * exists).
+	 * First, check servicefile option on connection string. Second, check
+	 * PGSERVICEFILE environment variable. Finally, check ~/.pg_service.conf
+	 * (if that exists).
 	 */
-	if ((env = getenv("PGSERVICEFILE")) != NULL)
+	if (service_fname != NULL)
+	{
+		strlcpy(serviceFile, service_fname, sizeof(serviceFile));
+	}
+	else if ((env = getenv("PGSERVICEFILE")) != NULL)
+	{
 		strlcpy(serviceFile, env, sizeof(serviceFile));
+	}
 	else
 	{
 		char		homedir[MAXPGPATH];
@@ -6023,6 +6034,16 @@ parseServiceFile(const char *serviceFile,
 					goto exit;
 				}
 
+				if (strcmp(key, "servicefile") == 0)
+				{
+					libpq_append_error(errorMessage,
+									   "nested servicefile specifications not supported in service file \"%s\", line %d",
+									   serviceFile,
+									   linenr);
+					result = 3;
+					goto exit;
+				}
+
 				/*
 				 * Set the parameter --- but don't override any previous
 				 * explicit setting.
diff --git a/src/interfaces/libpq/t/006_service.pl b/src/interfaces/libpq/t/006_service.pl
index 045e25a05d3..ff0493954dd 100644
--- a/src/interfaces/libpq/t/006_service.pl
+++ b/src/interfaces/libpq/t/006_service.pl
@@ -6,7 +6,7 @@ use PostgreSQL::Test::Utils;
 use PostgreSQL::Test::Cluster;
 use Test::More;
 
-# This tests "service" connection options.
+# This tests "service" and "servicefile" connection options.
 
 # Cluster setup which is shared for testing both load balancing methods
 my $node = PostgreSQL::Test::Cluster->new('node');
@@ -76,4 +76,67 @@ close $fh;
     );
 }
 
+# Backslashes escaped path string for getting collect result at concatenation
+# for Windows environment
+my $srvfile_win_cared = $srvfile;
+$srvfile_win_cared =~ s/\\/\\\\/g;
+
+# Check that servicefile option works as expected
+{
+    $node->connect_ok(
+        q{service=my_srv servicefile='} . $srvfile_win_cared . q{'},
+        'service=my_srv servicefile=...',
+        sql             => "SELECT 'connect4'",
+        expected_stdout => qr/connect4/
+    );
+
+    # Encode slashes and backslash
+    my $encoded_srvfile = $srvfile =~ s{([\\/])}{
+        $1 eq '/' ? '%2F' : '%5C'
+    }ger;
+
+    # Additionaly encode a colon in servicefile path of Windows
+    $encoded_srvfile =~ s/:/%3A/g;
+
+    $node->connect_ok(
+        'postgresql:///?service=my_srv&servicefile=' . $encoded_srvfile,
+        'postgresql:///?service=my_srv&servicefile=...',
+        sql             => "SELECT 'connect5'",
+        expected_stdout => qr/connect5/
+    );
+
+    local $ENV{PGSERVICE} = 'my_srv';
+    $node->connect_ok(
+        q{servicefile='} . $srvfile_win_cared . q{'},
+        'envvar: PGSERVICE=my_srv + servicefile=...',
+        sql             => "SELECT 'connect6'",
+        expected_stdout => qr/connect6/
+    );
+
+    $node->connect_ok(
+        'postgresql://?servicefile=' . $encoded_srvfile,
+        'envvar: PGSERVICE=my_srv + postgresql://?servicefile=...',
+        sql             => "SELECT 'connect7'",
+        expected_stdout => qr/connect7/
+    );
+}
+
+# Check that servicefile option takes precedence over PGSERVICEFILE environment variable
+{
+    local $ENV{PGSERVICEFILE} = 'non-existent-file.conf';
+
+    $node->connect_fails(
+        'service=my_srv',
+        'service=... fails with wrong PGSERVICEFILE',
+        expected_stderr => qr/service file "non-existent-file\.conf" not found/
+    );
+
+    $node->connect_ok(
+        q{service=my_srv servicefile='} . $srvfile_win_cared . q{'},
+        'servicefile= takes precedence over PGSERVICEFILE',
+        sql             => "SELECT 'connect8'",
+        expected_stdout => qr/connect8/
+    );
+}
+
 done_testing();
\ No newline at end of file
-- 
2.25.1

#14Michael Paquier
michael@paquier.xyz
In reply to: Ryo Kanbayashi (#13)
Re: [PATCH] PGSERVICEFILE as part of a normal connection string

On Sun, Mar 23, 2025 at 12:32:03PM +0900, Ryo Kanbayashi wrote:

Additional test scripts have been merged to a single script ^^ b

I have spent quite a bit of time on the review 0001 with the new
tests to get something in for this release, and there was quite a bit
going on there:
- The script should set PGSYSCONFDIR, or it could grab data that
depend on the host. This can use the temporary folder created in the
test.
- On the same ground, we need a similar tweak for PGSERVICEFILE or we
would go into pqGetHomeDirectory() and look at a HOME folder (WIN32
and non-WIN32).

With that addressed, there could be much more tests, like for cases
where PGSERVICEFILE is set but points to a file that does not exist,
more combinations between URIs, connection parameters and PGSERVICE,
for success and failure cases, empty service file, etc.

Another thing that I've noticed to be useful to cover is the case
based on the hardcoded service file name pg_service.conf in
PGSYSCONFDIR, which is used as a fallback in the code if the service
name cannot be found in the initial PGSERVICEFILE, acting as a
fallback option. As long as PGSYSCONFDIR is set, we could test one in
isolation using the temporary folder created by the test.

With all that in mind and more documentation added to the test, I've
applied 0001, so let's see what the buildfarm has to say. The CI was
stable, so it's a start.

I am not sure that I'll have the time to look at 0002 for this release
cycle, could it be possible to get a rebase for it?
--
Michael

#15Michael Paquier
michael@paquier.xyz
In reply to: Michael Paquier (#14)
1 attachment(s)
Re: [PATCH] PGSERVICEFILE as part of a normal connection string

On Thu, Mar 27, 2025 at 06:31:14PM +0900, Michael Paquier wrote:

I am not sure that I'll have the time to look at 0002 for this release
cycle, could it be possible to get a rebase for it?

Here is a simple rebase that I have been able to assemble this
morning. I won't have the space to review it for this release cycle
unfortunately, but at least it works in the CI.

I am moving this patch entry to the next CF for v19, as a result of
that.
--
Michael

Attachments:

v4-0001-add-servicefile-connection-option-feature.patchtext/x-diff; charset=us-asciiDownload
From 83818caa5f5d5847ca99210446cc37fa8f8a1caf Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Fri, 28 Mar 2025 08:26:01 +0900
Subject: [PATCH v4] add servicefile connection option feature

---
 src/interfaces/libpq/fe-connect.c     | 27 +++++++++++--
 src/interfaces/libpq/t/006_service.pl | 58 +++++++++++++++++++++++++++
 doc/src/sgml/libpq.sgml               | 16 +++++++-
 3 files changed, 97 insertions(+), 4 deletions(-)

diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index d5051f5e820f..f16468be7d14 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -195,6 +195,9 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
 		"Database-Service", "", 20,
 	offsetof(struct pg_conn, pgservice)},
 
+	{"servicefile", "PGSERVICEFILE", NULL, NULL,
+	"Database-Service-File", "", 64, -1},
+
 	{"user", "PGUSER", NULL, NULL,
 		"Database-User", "", 20,
 	offsetof(struct pg_conn, pguser)},
@@ -5838,6 +5841,7 @@ static int
 parseServiceInfo(PQconninfoOption *options, PQExpBuffer errorMessage)
 {
 	const char *service = conninfo_getval(options, "service");
+	const char *service_fname = conninfo_getval(options, "servicefile");
 	char		serviceFile[MAXPGPATH];
 	char	   *env;
 	bool		group_found = false;
@@ -5857,11 +5861,18 @@ parseServiceInfo(PQconninfoOption *options, PQExpBuffer errorMessage)
 		return 0;
 
 	/*
-	 * Try PGSERVICEFILE if specified, else try ~/.pg_service.conf (if that
-	 * exists).
+	 * First, check servicefile option on connection string. Second, check
+	 * PGSERVICEFILE environment variable. Finally, check ~/.pg_service.conf
+	 * (if that exists).
 	 */
-	if ((env = getenv("PGSERVICEFILE")) != NULL)
+	if (service_fname != NULL)
+	{
+		strlcpy(serviceFile, service_fname, sizeof(serviceFile));
+	}
+	else if ((env = getenv("PGSERVICEFILE")) != NULL)
+	{
 		strlcpy(serviceFile, env, sizeof(serviceFile));
+	}
 	else
 	{
 		char		homedir[MAXPGPATH];
@@ -6023,6 +6034,16 @@ parseServiceFile(const char *serviceFile,
 					goto exit;
 				}
 
+				if (strcmp(key, "servicefile") == 0)
+				{
+					libpq_append_error(errorMessage,
+									   "nested servicefile specifications not supported in service file \"%s\", line %d",
+									   serviceFile,
+									   linenr);
+					result = 3;
+					goto exit;
+				}
+
 				/*
 				 * Set the parameter --- but don't override any previous
 				 * explicit setting.
diff --git a/src/interfaces/libpq/t/006_service.pl b/src/interfaces/libpq/t/006_service.pl
index d3ecfa6b6fc8..752973d96534 100644
--- a/src/interfaces/libpq/t/006_service.pl
+++ b/src/interfaces/libpq/t/006_service.pl
@@ -136,6 +136,64 @@ local $ENV{PGSERVICEFILE} = "$srvfile_empty";
 	unlink($srvfile_default);
 }
 
+# Backslashes escaped path string for getting collect result at concatenation
+# for Windows environment
+my $srvfile_win_cared = $srvfile_valid;
+$srvfile_win_cared =~ s/\\/\\\\/g;
+
+# Check that servicefile option works as expected
+{
+	$node->connect_ok(
+		q{service=my_srv servicefile='} . $srvfile_win_cared . q{'},
+		'service=my_srv servicefile=...',
+		sql => "SELECT 'connect4'",
+		expected_stdout => qr/connect4/);
+
+	# Encode slashes and backslash
+	my $encoded_srvfile = $srvfile_valid =~ s{([\\/])}{
+        $1 eq '/' ? '%2F' : '%5C'
+    }ger;
+
+	# Additionaly encode a colon in servicefile path of Windows
+	$encoded_srvfile =~ s/:/%3A/g;
+
+	$node->connect_ok(
+		'postgresql:///?service=my_srv&servicefile=' . $encoded_srvfile,
+		'postgresql:///?service=my_srv&servicefile=...',
+		sql => "SELECT 'connect5'",
+		expected_stdout => qr/connect5/);
+
+	local $ENV{PGSERVICE} = 'my_srv';
+	$node->connect_ok(
+		q{servicefile='} . $srvfile_win_cared . q{'},
+		'envvar: PGSERVICE=my_srv + servicefile=...',
+		sql => "SELECT 'connect6'",
+		expected_stdout => qr/connect6/);
+
+	$node->connect_ok(
+		'postgresql://?servicefile=' . $encoded_srvfile,
+		'envvar: PGSERVICE=my_srv + postgresql://?servicefile=...',
+		sql => "SELECT 'connect7'",
+		expected_stdout => qr/connect7/);
+}
+
+# Check that servicefile option takes precedence over PGSERVICEFILE environment variable
+{
+	local $ENV{PGSERVICEFILE} = 'non-existent-file.conf';
+
+	$node->connect_fails(
+		'service=my_srv',
+		'service=... fails with wrong PGSERVICEFILE',
+		expected_stderr =>
+		  qr/service file "non-existent-file\.conf" not found/);
+
+	$node->connect_ok(
+		q{service=my_srv servicefile='} . $srvfile_win_cared . q{'},
+		'servicefile= takes precedence over PGSERVICEFILE',
+		sql => "SELECT 'connect8'",
+		expected_stdout => qr/connect8/);
+}
+
 $node->teardown_node;
 
 done_testing();
diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index b359fbff295b..f387f8910319 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -2248,6 +2248,19 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
       </listitem>
      </varlistentry>
 
+     <varlistentry id="libpq-connect-servicefile" xreflabel="servicefile">
+      <term><literal>servicefile</literal></term>
+      <listitem>
+       <para>
+        This option specifies the name of the per-user connection service file
+        (see <xref linkend="libpq-pgservice"/>).
+        Defaults to <filename>~/.pg_service.conf</filename>, or
+        <filename>%APPDATA%\postgresql\.pg_service.conf</filename> on
+        Microsoft Windows.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="libpq-connect-target-session-attrs" xreflabel="target_session_attrs">
       <term><literal>target_session_attrs</literal></term>
       <listitem>
@@ -9504,7 +9517,8 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough)
    On Microsoft Windows, it is named
    <filename>%APPDATA%\postgresql\.pg_service.conf</filename> (where
    <filename>%APPDATA%</filename> refers to the Application Data subdirectory
-   in the user's profile).  A different file name can be specified by
+   in the user's profile).  A different file name can be specified using the
+   <literal>servicefile</literal> key word in a libpq connection string or by
    setting the environment variable <envar>PGSERVICEFILE</envar>.
    The system-wide file is named <filename>pg_service.conf</filename>.
    By default it is sought in the <filename>etc</filename> directory
-- 
2.49.0

#16Michael Paquier
michael@paquier.xyz
In reply to: Michael Paquier (#14)
Re: [PATCH] PGSERVICEFILE as part of a normal connection string

On Thu, Mar 27, 2025 at 06:31:14PM +0900, Michael Paquier wrote:

With all that in mind and more documentation added to the test, I've
applied 0001, so let's see what the buildfarm has to say. The CI was
stable, so it's a start.

The buildfarm (particularly the Windows members that worried me), have
reported back and I am not seeing any failures, so we should be good
with 72c2f36d5727.
--
Michael

#17Ryo Kanbayashi
kanbayashi.dev@gmail.com
In reply to: Michael Paquier (#16)
Re: [PATCH] PGSERVICEFILE as part of a normal connection string

On Fri, Mar 28, 2025 at 10:44 AM Michael Paquier <michael@paquier.xyz> wrote:

On Thu, Mar 27, 2025 at 06:31:14PM +0900, Michael Paquier wrote:

With all that in mind and more documentation added to the test, I've
applied 0001, so let's see what the buildfarm has to say. The CI was
stable, so it's a start.

The buildfarm (particularly the Windows members that worried me), have
reported back and I am not seeing any failures, so we should be good
with 72c2f36d5727.

Thank you for review and additional modification to the patch.
I'm glad the patch was made in time for this release, even if it was
just a partial one.

On Fri, Mar 28, 2025 at 8:57 AM Michael Paquier <michael@paquier.xyz> wrote:

I am not sure that I'll have the time to look at 0002 for this release
cycle, could it be possible to get a rebase for it?

Here is a simple rebase that I have been able to assemble this
morning. I won't have the space to review it for this release cycle
unfortunately, but at least it works in the CI.

I'm sorry I couldn't respond to your request :(

I am moving this patch entry to the next CF for v19, as a result of
that.

OK
Thanks :)

On Thu, Mar 27, 2025 at 6:31 PM Michael Paquier <michael@paquier.xyz> wrote:

I have spent quite a bit of time on the review 0001 with the new
tests to get something in for this release, and there was quite a bit
going on there:
- The script should set PGSYSCONFDIR, or it could grab data that
depend on the host. This can use the temporary folder created in the
test.
- On the same ground, we need a similar tweak for PGSERVICEFILE or we
would go into pqGetHomeDirectory() and look at a HOME folder (WIN32
and non-WIN32).

With that addressed, there could be much more tests, like for cases
where PGSERVICEFILE is set but points to a file that does not exist,
more combinations between URIs, connection parameters and PGSERVICE,
for success and failure cases, empty service file, etc.

Another thing that I've noticed to be useful to cover is the case
based on the hardcoded service file name pg_service.conf in
PGSYSCONFDIR, which is used as a fallback in the code if the service
name cannot be found in the initial PGSERVICEFILE, acting as a
fallback option. As long as PGSYSCONFDIR is set, we could test one in
isolation using the temporary folder created by the test.

I check and modify 0002 patch (adding servicefile option and its
regression tests)
in light of the above and committed 0001 patch (regression test of
existing features)
toward next release :)

---
Great Regards,
Ryo Kanbayashi

#18Ryo Kanbayashi
kanbayashi.dev@gmail.com
In reply to: Ryo Kanbayashi (#17)
1 attachment(s)
Re: [PATCH] PGSERVICEFILE as part of a normal connection string

On Sat, Mar 29, 2025 at 3:35 PM Ryo Kanbayashi <kanbayashi.dev@gmail.com> wrote:

On Fri, Mar 28, 2025 at 8:57 AM Michael Paquier <michael@paquier.xyz> wrote:

I am not sure that I'll have the time to look at 0002 for this release
cycle, could it be possible to get a rebase for it?

Here is a simple rebase that I have been able to assemble this
morning. I won't have the space to review it for this release cycle
unfortunately, but at least it works in the CI.

I'm sorry I couldn't respond to your request :(

I am moving this patch entry to the next CF for v19, as a result of
that.

OK
Thanks :)

On Thu, Mar 27, 2025 at 6:31 PM Michael Paquier <michael@paquier.xyz> wrote:

I have spent quite a bit of time on the review 0001 with the new
tests to get something in for this release, and there was quite a bit
going on there:
- The script should set PGSYSCONFDIR, or it could grab data that
depend on the host. This can use the temporary folder created in the
test.
- On the same ground, we need a similar tweak for PGSERVICEFILE or we
would go into pqGetHomeDirectory() and look at a HOME folder (WIN32
and non-WIN32).

With that addressed, there could be much more tests, like for cases
where PGSERVICEFILE is set but points to a file that does not exist,
more combinations between URIs, connection parameters and PGSERVICE,
for success and failure cases, empty service file, etc.

Another thing that I've noticed to be useful to cover is the case
based on the hardcoded service file name pg_service.conf in
PGSYSCONFDIR, which is used as a fallback in the code if the service
name cannot be found in the initial PGSERVICEFILE, acting as a
fallback option. As long as PGSYSCONFDIR is set, we could test one in
isolation using the temporary folder created by the test.

I check and modify 0002 patch (adding servicefile option and its
regression tests)
in light of the above and committed 0001 patch (regression test of
existing features)
toward next release :)

Although it probably won't be ready in time for this release, I've
created new 0001 patch (former 0002) which is reflected your review
comments.

I checked That the patch passes CI of my GitHub repository.

Best of luck :)

---
Great regards,
Ryo Kanbayashi

Attachments:

v5-0001-add-servicefile-connection-option-feature.patchapplication/octet-stream; name=v5-0001-add-servicefile-connection-option-feature.patchDownload
From 65cdc2b918c0ca17a199c57f0d160a7b4b42d626 Mon Sep 17 00:00:00 2001
From: Ryo Kanbayashi <ryo.contact@gmail.com>
Date: Sat, 29 Mar 2025 20:53:33 +0900
Subject: [PATCH v5] add servicefile connection option feature

---
 doc/src/sgml/libpq.sgml               | 16 ++++-
 src/interfaces/libpq/fe-connect.c     | 27 +++++++-
 src/interfaces/libpq/t/006_service.pl | 96 +++++++++++++++++++++++++++
 3 files changed, 135 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index b359fbff29..f387f89103 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -2248,6 +2248,19 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
       </listitem>
      </varlistentry>
 
+     <varlistentry id="libpq-connect-servicefile" xreflabel="servicefile">
+      <term><literal>servicefile</literal></term>
+      <listitem>
+       <para>
+        This option specifies the name of the per-user connection service file
+        (see <xref linkend="libpq-pgservice"/>).
+        Defaults to <filename>~/.pg_service.conf</filename>, or
+        <filename>%APPDATA%\postgresql\.pg_service.conf</filename> on
+        Microsoft Windows.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="libpq-connect-target-session-attrs" xreflabel="target_session_attrs">
       <term><literal>target_session_attrs</literal></term>
       <listitem>
@@ -9504,7 +9517,8 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough)
    On Microsoft Windows, it is named
    <filename>%APPDATA%\postgresql\.pg_service.conf</filename> (where
    <filename>%APPDATA%</filename> refers to the Application Data subdirectory
-   in the user's profile).  A different file name can be specified by
+   in the user's profile).  A different file name can be specified using the
+   <literal>servicefile</literal> key word in a libpq connection string or by
    setting the environment variable <envar>PGSERVICEFILE</envar>.
    The system-wide file is named <filename>pg_service.conf</filename>.
    By default it is sought in the <filename>etc</filename> directory
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index d5051f5e82..f16468be7d 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -195,6 +195,9 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
 		"Database-Service", "", 20,
 	offsetof(struct pg_conn, pgservice)},
 
+	{"servicefile", "PGSERVICEFILE", NULL, NULL,
+	"Database-Service-File", "", 64, -1},
+
 	{"user", "PGUSER", NULL, NULL,
 		"Database-User", "", 20,
 	offsetof(struct pg_conn, pguser)},
@@ -5838,6 +5841,7 @@ static int
 parseServiceInfo(PQconninfoOption *options, PQExpBuffer errorMessage)
 {
 	const char *service = conninfo_getval(options, "service");
+	const char *service_fname = conninfo_getval(options, "servicefile");
 	char		serviceFile[MAXPGPATH];
 	char	   *env;
 	bool		group_found = false;
@@ -5857,11 +5861,18 @@ parseServiceInfo(PQconninfoOption *options, PQExpBuffer errorMessage)
 		return 0;
 
 	/*
-	 * Try PGSERVICEFILE if specified, else try ~/.pg_service.conf (if that
-	 * exists).
+	 * First, check servicefile option on connection string. Second, check
+	 * PGSERVICEFILE environment variable. Finally, check ~/.pg_service.conf
+	 * (if that exists).
 	 */
-	if ((env = getenv("PGSERVICEFILE")) != NULL)
+	if (service_fname != NULL)
+	{
+		strlcpy(serviceFile, service_fname, sizeof(serviceFile));
+	}
+	else if ((env = getenv("PGSERVICEFILE")) != NULL)
+	{
 		strlcpy(serviceFile, env, sizeof(serviceFile));
+	}
 	else
 	{
 		char		homedir[MAXPGPATH];
@@ -6023,6 +6034,16 @@ parseServiceFile(const char *serviceFile,
 					goto exit;
 				}
 
+				if (strcmp(key, "servicefile") == 0)
+				{
+					libpq_append_error(errorMessage,
+									   "nested servicefile specifications not supported in service file \"%s\", line %d",
+									   serviceFile,
+									   linenr);
+					result = 3;
+					goto exit;
+				}
+
 				/*
 				 * Set the parameter --- but don't override any previous
 				 * explicit setting.
diff --git a/src/interfaces/libpq/t/006_service.pl b/src/interfaces/libpq/t/006_service.pl
index d3ecfa6b6f..3320f1b513 100644
--- a/src/interfaces/libpq/t/006_service.pl
+++ b/src/interfaces/libpq/t/006_service.pl
@@ -136,6 +136,102 @@ local $ENV{PGSERVICEFILE} = "$srvfile_empty";
 	unlink($srvfile_default);
 }
 
+# Backslashes escaped path string for getting collect result at concatenation
+# for Windows environment
+my $srvfile_win_cared = $srvfile_valid;
+$srvfile_win_cared =~ s/\\/\\\\/g;
+
+# Checks combinations of service name and valid "servicefile" string.
+{
+	$node->connect_ok(
+		q{service=my_srv servicefile='} . $srvfile_win_cared . q{'},
+		'connection with correct "service" string and correct "servicefile" string',
+		sql             => "SELECT 'connect3_1'",
+		expected_stdout => qr/connect3_1/);
+
+	local $ENV{PGSERVICE} = 'my_srv';
+	$node->connect_ok(
+		q{servicefile='} . $srvfile_win_cared . q{'},
+		'connection with correct PGSERVICE and collect "servicefile" string',
+		sql             => "SELECT 'connect3_2'",
+		expected_stdout => qr/connect3_2/);
+
+	$node->connect_fails(
+		q{service=undefined-service servicefile='} . $srvfile_win_cared . q{'},
+		'connection with incorrect "service" string and collect "servicefile" string',
+		expected_stderr =>
+			qr/definition of service "undefined-service" not found/);
+
+	local $ENV{PGSERVICE} = 'undefined-service';
+	$node->connect_fails(
+		q{servicefile='} . $srvfile_win_cared . q{'},
+		'connection with incorrect PGSERVICE and collect "servicefile"',
+		expected_stderr =>
+			qr/definition of service "undefined-service" not found/);
+}
+
+# Checks combinations of service name and a valid "servicefile" string in URI format.
+{
+	# Encode slashes and backslash
+	my $encoded_srvfile = $srvfile_valid =~ s{([\\/])}{
+        $1 eq '/' ? '%2F' : '%5C'
+    }ger;
+
+	# Additionaly encode a colon in servicefile path of Windows
+	$encoded_srvfile =~ s/:/%3A/g;
+
+	$node->connect_ok(
+		'postgresql:///?service=my_srv&servicefile=' . $encoded_srvfile,
+		'connection with correct "service" string and correct "servicefile" in URI format',
+		sql => "SELECT 'connect4_1'",
+		expected_stdout => qr/connect4_1/);
+
+	local $ENV{PGSERVICE} = 'my_srv';
+	$node->connect_ok(
+		'postgresql://?servicefile=' . $encoded_srvfile,
+		'connection with correct PGSERVICE and collect "servicefile" in URI format',
+		sql => "SELECT 'connect4_2'",
+		expected_stdout => qr/connect4_2/);
+
+	$node->connect_fails(
+		'postgresql:///?service=undefined-service&servicefile=' . $encoded_srvfile,
+		'connection with incorrect "service" string and collect "servicefile" in URI format',
+		expected_stderr =>
+		  qr/definition of service "undefined-service" not found/);
+}
+
+# Checks case of incorrect "servicefile" string.
+{
+	# Backslashes escaped path string for getting collect result at concatenation
+	# for Windows environment
+	my $srvfile_missing_win_cared = $srvfile_missing;
+	$srvfile_missing_win_cared =~ s/\\/\\\\/g;
+
+	$node->connect_fails(
+		q{service=my_srv servicefile='} . $srvfile_missing_win_cared . q{'},
+		'connection with correct "service" string and incorrect "servicefile" string',
+		sql => "SELECT 'connect5_1'",
+		expected_stderr =>
+			qr/service file ".*pg_service_missing.conf" not found/);
+}
+
+# Check that "servicefile" string takes precedence over PGSERVICEFILE environment variable
+{
+	local $ENV{PGSERVICEFILE} = $srvfile_missing;
+
+	$node->connect_fails(
+		'service=my_srv',
+		'connecttion with correct "service" string and incorrect PGSERVICEFILE',
+		expected_stderr =>
+		  qr/service file ".*pg_service_missing.conf" not found/);
+
+	$node->connect_ok(
+		q{service=my_srv servicefile='} . $srvfile_win_cared . q{'},
+		'connectin with correct "service" string, incorrect PGSERVICEFILE and correct "servicefile" string',
+		sql => "SELECT 'connect6_1'",
+		expected_stdout => qr/connect6_1/);
+}
+
 $node->teardown_node;
 
 done_testing();
-- 
2.25.1

#19Andrew Jackson
andrewjackson947@gmail.com
In reply to: Ryo Kanbayashi (#13)
1 attachment(s)
Re: [PATCH] PGSERVICEFILE as part of a normal connection string

Hi,

I am working on a feature adjacent to the connection service functionality
and noticed some issues with the tests introduced in this thread. Basically
they incorrectly invoke the append perl function by passing multiple
strings to append when the function only takes one string to append. This
caused the generated service files to not actually contain any connection
parameters. The tests were only passing because the connect_ok perl
function set the connection parameters as environment variables which
covered up the misformed connection service file.

The attached patch is much more strict in that it creates a dummy database
that is not started and passes all queries though that and tests that the
connection service file correctly overrides the environment variables set
by the dummy databases' query functions

Thanks,
Andrew Jackson

On Mon, Mar 31, 2025, 4:01 PM Ryo Kanbayashi <kanbayashi.dev@gmail.com>
wrote:

Show quoted text

On Sat, Mar 22, 2025 at 4:46 PM Michael Paquier <michael@paquier.xyz>
wrote:

On Thu, Mar 20, 2025 at 06:16:44PM +0900, Ryo Kanbayashi wrote:

Sorry, I found a miss on 006_service.pl.
Fixed patch is attached...

Please note that the commit fest app needs all the patches of a a set
to be posted in the same message. In this case, v2-0001 is not going
to get automatic test coverage.

Your patch naming policy is also a bit confusing. I would suggest to
use `git format-patch -vN -2`, where N is your version number. 0001
would be the new tests for service files, and 0002 the new feature,
with its own tests.

All right.
I attached patches generated with your suggested command :)

+if ($windows_os) {
+
+    # Windows: use CRLF
+    print $fh "[my_srv]",                                   "\r\n";
+    print $fh join( "\r\n", split( ' ', $node->connstr ) ), "\r\n";
+}
+else {
+    # Non-Windows: use LF
+    print $fh "[my_srv]",                                 "\n";
+    print $fh join( "\n", split( ' ', $node->connstr ) ), "\n";
+}
+close $fh;

That's duplicated. Let's perhaps use a $newline variable and print
into the file using the $newline?

OK.
I reflected above comment.

Question: you are doing things this way in the test because fgets() is
what is used by libpq to retrieve the lines of the service file, is
that right?

No. I'm doing above way simply because line ending code of service file
wrote by users may become CRLF in Windows platform.

Please note that the CI is failing. It seems to me that you are
missing a done_testing() at the end of the script. If you have a
github account, I'd suggest to set up a CI in your own fork of
Postgres, this is really helpful to double-check the correctness of a
patch before posting it to the lists, and saves in round trips between
author and reviewer. Please see src/tools/ci/README in the code tree
for details.

Sorry.
I'm using Cirrus CI with GitHub and I checked passing the CI.
But there were misses when I created patch files...

+# Copyright (c) 2023-2024, PostgreSQL Global Development Group

These dates are incorrect. Should be 2025, as it's a new file.

OK.

+++ b/src/interfaces/libpq/t/007_servicefile_opt.pl
@@ -0,0 +1,100 @@
+# Copyright (c) 2023-2024, PostgreSQL Global Development Group

Incorrect date again in the second path with the new feature. I'd
suggest to merge all the tests in a single script, with only one node
initialized and started.

OK.
Additional test scripts have been merged to a single script ^^ b

---
Great regards,
Ryo Kanbayashi

Attachments:

v1-0001-libpq-Fix-TAP-tests-for-service-files-and-names.patchapplication/x-patch; name=v1-0001-libpq-Fix-TAP-tests-for-service-files-and-names.patchDownload
From 0d732ee8edbb16132b95f35775388528fa9003d8 Mon Sep 17 00:00:00 2001
From: AndrewJackson <AndrewJackson947@gmail.com>
Date: Mon, 31 Mar 2025 15:18:48 -0500
Subject: [PATCH] libpq: Fix TAP tests for service files and names

This commit builds on the tests that were added in a prior commit
that tests the connection service file functionality. The tests are
fixed to correctly invoke the append_to_file subroutine which only
takes one argument and not multiple. The test also runs the statements
through a dummy database to ensure that the options from the service
file are actually being picked up and just passing because they are
defaulting to the connection environment variables that are set in
the connect_ok function.

Author: Andrew Jackson <AndrewJackson947@gmail.com>
---
 src/interfaces/libpq/t/006_service.pl | 38 ++++++++++++++++++---------
 1 file changed, 25 insertions(+), 13 deletions(-)

diff --git a/src/interfaces/libpq/t/006_service.pl b/src/interfaces/libpq/t/006_service.pl
index d3ecfa6b6fc..e0d18d72359 100644
--- a/src/interfaces/libpq/t/006_service.pl
+++ b/src/interfaces/libpq/t/006_service.pl
@@ -9,6 +9,9 @@ use Test::More;
 # This tests scenarios related to the service name and the service file,
 # for the connection options and their environment variables.

+my $dummy_node = PostgreSQL::Test::Cluster->new('dummy_node');
+$dummy_node->init;
+
 my $node = PostgreSQL::Test::Cluster->new('node');
 $node->init;
 $node->start;
@@ -23,8 +26,16 @@ my $newline = $windows_os ? "\r\n" : "\n";
 # File that includes a valid service name, that uses a decomposed connection
 # string for its contents, split on spaces.
 my $srvfile_valid = "$td/pg_service_valid.conf";
-append_to_file($srvfile_valid, "[my_srv]", $newline);
-append_to_file($srvfile_valid, split(/\s+/, $node->connstr) . $newline);
+append_to_file($srvfile_valid, "[my_srv]");
+append_to_file($srvfile_valid, $newline);
+
+append_to_file($srvfile_valid, "host=");
+append_to_file($srvfile_valid, $node->host);
+append_to_file($srvfile_valid, $newline);
+
+append_to_file($srvfile_valid, "port=");
+append_to_file($srvfile_valid, $node->port);
+append_to_file($srvfile_valid, $newline);

 # File defined with no contents, used as default value for PGSERVICEFILE,
 # so as no lookup is attempted in the user's home directory.
@@ -51,33 +62,33 @@ local $ENV{PGSERVICEFILE} = "$srvfile_empty";
 # Checks combinations of service name and a valid service file.
 {
 	local $ENV{PGSERVICEFILE} = $srvfile_valid;
-	$node->connect_ok(
+	$dummy_node->connect_ok(
 		'service=my_srv',
 		'connection with correct "service" string and PGSERVICEFILE',
 		sql => "SELECT 'connect1_1'",
 		expected_stdout => qr/connect1_1/);

-	$node->connect_ok(
+	$dummy_node->connect_ok(
 		'postgres://?service=my_srv',
 		'connection with correct "service" URI and PGSERVICEFILE',
 		sql => "SELECT 'connect1_2'",
 		expected_stdout => qr/connect1_2/);

-	$node->connect_fails(
+	$dummy_node->connect_fails(
 		'service=undefined-service',
 		'connection with incorrect "service" string and PGSERVICEFILE',
 		expected_stderr =>
 		  qr/definition of service "undefined-service" not found/);

 	local $ENV{PGSERVICE} = 'my_srv';
-	$node->connect_ok(
+	$dummy_node->connect_ok(
 		'',
 		'connection with correct PGSERVICE and PGSERVICEFILE',
 		sql => "SELECT 'connect1_3'",
 		expected_stdout => qr/connect1_3/);

 	local $ENV{PGSERVICE} = 'undefined-service';
-	$node->connect_fails(
+	$dummy_node->connect_fails(
 		'',
 		'connection with incorrect PGSERVICE and PGSERVICEFILE',
 		expected_stdout =>
@@ -87,7 +98,7 @@ local $ENV{PGSERVICEFILE} = "$srvfile_empty";
 # Checks case of incorrect service file.
 {
 	local $ENV{PGSERVICEFILE} = $srvfile_missing;
-	$node->connect_fails(
+	$dummy_node->connect_fails(
 		'service=my_srv',
 		'connection with correct "service" string and incorrect PGSERVICEFILE',
 		expected_stderr =>
@@ -100,33 +111,33 @@ local $ENV{PGSERVICEFILE} = "$srvfile_empty";
 	my $srvfile_default = "$td/pg_service.conf";
 	copy($srvfile_valid, $srvfile_default);

-	$node->connect_ok(
+	$dummy_node->connect_ok(
 		'service=my_srv',
 		'connection with correct "service" string and pg_service.conf',
 		sql => "SELECT 'connect2_1'",
 		expected_stdout => qr/connect2_1/);

-	$node->connect_ok(
+	$dummy_node->connect_ok(
 		'postgres://?service=my_srv',
 		'connection with correct "service" URI and default pg_service.conf',
 		sql => "SELECT 'connect2_2'",
 		expected_stdout => qr/connect2_2/);

-	$node->connect_fails(
+	$dummy_node->connect_fails(
 		'service=undefined-service',
 		'connection with incorrect "service" string and default pg_service.conf',
 		expected_stderr =>
 		  qr/definition of service "undefined-service" not found/);

 	local $ENV{PGSERVICE} = 'my_srv';
-	$node->connect_ok(
+	$dummy_node->connect_ok(
 		'',
 		'connection with correct PGSERVICE and default pg_service.conf',
 		sql => "SELECT 'connect2_3'",
 		expected_stdout => qr/connect2_3/);

 	local $ENV{PGSERVICE} = 'undefined-service';
-	$node->connect_fails(
+	$dummy_node->connect_fails(
 		'',
 		'connection with incorrect PGSERVICE and default pg_service.conf',
 		expected_stdout =>
@@ -137,5 +148,6 @@ local $ENV{PGSERVICEFILE} = "$srvfile_empty";
 }

 $node->teardown_node;
+$dummy_node->teardown_node;

 done_testing();
--
2.43.5

#20Ryo Kanbayashi
kanbayashi.dev@gmail.com
In reply to: Andrew Jackson (#19)
Re: [PATCH] PGSERVICEFILE as part of a normal connection string

On Tue, Apr 1, 2025 at 6:26 AM Andrew Jackson
<andrewjackson947@gmail.com> wrote:

Hi,

I am working on a feature adjacent to the connection service functionality and noticed some issues with the tests introduced in this thread. Basically they incorrectly invoke the append perl function by passing multiple strings to append when the function only takes one string to append. This caused the generated service files to not actually contain any connection parameters. The tests were only passing because the connect_ok perl function set the connection parameters as environment variables which covered up the misformed connection service file.
The attached patch is much more strict in that it creates a dummy database that is not started and passes all queries though that and tests that the connection service file correctly overrides the environment variables set by the dummy databases' query functions

Andrew,
CC: Michael, Torsten

Thank you to find issues the tests.

I confirmed points you noticed and validity of your proposed
modifications with local execution and internal impl of connect_ok
func.

- Current usage of append_to_file func is wrong and not appropriate
service file is generated
- connect_ok perl func set the connection parameters as environment
variables which covered up the misformed connection service file
- https://github.com/postgres/postgres/blob/ea3f9b6da34a1a4dc2c0c118789587c2a85c78d7/src/test/perl/PostgreSQL/Test/Cluster.pm#L2576
- https://github.com/postgres/postgres/blob/ea3f9b6da34a1a4dc2c0c118789587c2a85c78d7/src/test/perl/PostgreSQL/Test/Cluster.pm#L2120
- https://github.com/postgres/postgres/blob/ea3f9b6da34a1a4dc2c0c118789587c2a85c78d7/src/test/perl/PostgreSQL/Test/Cluster.pm#L1718
- Your dummy node object introduced code works without problem and the
code is more strict than current code

I'll reflect your notice and suggestion to the patch current I'm working on :)

---
Great Regards,
Ryo Kanbayashi

#21Michael Paquier
michael@paquier.xyz
In reply to: Andrew Jackson (#19)
Re: [PATCH] PGSERVICEFILE as part of a normal connection string

On Mon, Mar 31, 2025 at 04:26:27PM -0500, Andrew Jackson wrote:

I am working on a feature adjacent to the connection service functionality
and noticed some issues with the tests introduced in this thread. Basically
they incorrectly invoke the append perl function by passing multiple
strings to append when the function only takes one string to append. This
caused the generated service files to not actually contain any connection
parameters. The tests were only passing because the connect_ok perl
function set the connection parameters as environment variables which
covered up the misformed connection service file.

Yep, you are right on this one. I didn't really like the hardcoding
of the host and port parts, and we should still be OK to rely on a
connstr from the valid node split on spaces. At least that's a bit
simpler.

The attached patch is much more strict in that it creates a dummy database
that is not started and passes all queries though that and tests that the
connection service file correctly overrides the environment variables set
by the dummy databases' query functions

Interesting trick, I like that. The point of not starting the node is
important, while we also make sure to load an environment related to
the node where the valid connection should happen. Breaking the
contents of the valid service file on purpose breaks the connection
attempts, making the tests fail.

So applied as you have proposed, mostly, and I have added more
documentation explaining the idea behind the dummy node.
--
Michael

#22Michael Paquier
michael@paquier.xyz
In reply to: Ryo Kanbayashi (#20)
Re: [PATCH] PGSERVICEFILE as part of a normal connection string

On Thu, Apr 03, 2025 at 12:36:59AM +0900, Ryo Kanbayashi wrote:

I'll reflect your notice and suggestion to the patch current I'm
working on :)

Thanks for that.

And I have forgotten to add you as a reviewer of what has been
committed as 2c7bd2ba507e. Sorry for that :/
--
Michael

#23Ryo Kanbayashi
kanbayashi.dev@gmail.com
In reply to: Michael Paquier (#22)
1 attachment(s)
Re: [PATCH] PGSERVICEFILE as part of a normal connection string

On Mon, Apr 7, 2025 at 1:10 PM Michael Paquier <michael@paquier.xyz> wrote:

On Thu, Apr 03, 2025 at 12:36:59AM +0900, Ryo Kanbayashi wrote:

I'll reflect your notice and suggestion to the patch current I'm
working on :)

Thanks for that.

And I have forgotten to add you as a reviewer of what has been
committed as 2c7bd2ba507e. Sorry for that :/

No problem :)

I rebased our patch according to 2c7bd2ba507e.
https://commitfest.postgresql.org/patch/5387/

---
Great regards,
Ryo Kanbayashi

Attachments:

v7-0001-add-servicefile-connection-option-feature.patchapplication/octet-stream; name=v7-0001-add-servicefile-connection-option-feature.patchDownload
From 29faf3149035ab4766c7aa4a51f1c77b71bb5aa2 Mon Sep 17 00:00:00 2001
From: Ryo Kanbayashi <ryo.contact@gmail.com>
Date: Sun, 13 Apr 2025 18:24:36 +0900
Subject: [PATCH v7] add servicefile connection option feature

---
 doc/src/sgml/libpq.sgml               | 16 ++++++-
 src/interfaces/libpq/fe-connect.c     | 27 +++++++++--
 src/interfaces/libpq/t/006_service.pl | 65 ++++++++++++++++++++++++++-
 3 files changed, 103 insertions(+), 5 deletions(-)

diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 37102c235b..cf6c0320e6 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -2320,6 +2320,19 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
       </listitem>
      </varlistentry>
 
+     <varlistentry id="libpq-connect-servicefile" xreflabel="servicefile">
+      <term><literal>servicefile</literal></term>
+      <listitem>
+       <para>
+        This option specifies the name of the per-user connection service file
+        (see <xref linkend="libpq-pgservice"/>).
+        Defaults to <filename>~/.pg_service.conf</filename>, or
+        <filename>%APPDATA%\postgresql\.pg_service.conf</filename> on
+        Microsoft Windows.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="libpq-connect-target-session-attrs" xreflabel="target_session_attrs">
       <term><literal>target_session_attrs</literal></term>
       <listitem>
@@ -9596,7 +9609,8 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough)
    On Microsoft Windows, it is named
    <filename>%APPDATA%\postgresql\.pg_service.conf</filename> (where
    <filename>%APPDATA%</filename> refers to the Application Data subdirectory
-   in the user's profile).  A different file name can be specified by
+   in the user's profile).  A different file name can be specified using the
+   <literal>servicefile</literal> key word in a libpq connection string or by
    setting the environment variable <envar>PGSERVICEFILE</envar>.
    The system-wide file is named <filename>pg_service.conf</filename>.
    By default it is sought in the <filename>etc</filename> directory
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 0258d9ace3..b6350cb62f 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -195,6 +195,9 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
 		"Database-Service", "", 20,
 	offsetof(struct pg_conn, pgservice)},
 
+	{"servicefile", "PGSERVICEFILE", NULL, NULL,
+	"Database-Service-File", "", 64, -1},
+
 	{"user", "PGUSER", NULL, NULL,
 		"Database-User", "", 20,
 	offsetof(struct pg_conn, pguser)},
@@ -5904,6 +5907,7 @@ static int
 parseServiceInfo(PQconninfoOption *options, PQExpBuffer errorMessage)
 {
 	const char *service = conninfo_getval(options, "service");
+	const char *service_fname = conninfo_getval(options, "servicefile");
 	char		serviceFile[MAXPGPATH];
 	char	   *env;
 	bool		group_found = false;
@@ -5923,11 +5927,18 @@ parseServiceInfo(PQconninfoOption *options, PQExpBuffer errorMessage)
 		return 0;
 
 	/*
-	 * Try PGSERVICEFILE if specified, else try ~/.pg_service.conf (if that
-	 * exists).
+	 * First, check servicefile option on connection string. Second, check
+	 * PGSERVICEFILE environment variable. Finally, check ~/.pg_service.conf
+	 * (if that exists).
 	 */
-	if ((env = getenv("PGSERVICEFILE")) != NULL)
+	if (service_fname != NULL)
+	{
+		strlcpy(serviceFile, service_fname, sizeof(serviceFile));
+	}
+	else if ((env = getenv("PGSERVICEFILE")) != NULL)
+	{
 		strlcpy(serviceFile, env, sizeof(serviceFile));
+	}
 	else
 	{
 		char		homedir[MAXPGPATH];
@@ -6089,6 +6100,16 @@ parseServiceFile(const char *serviceFile,
 					goto exit;
 				}
 
+				if (strcmp(key, "servicefile") == 0)
+				{
+					libpq_append_error(errorMessage,
+									   "nested servicefile specifications not supported in service file \"%s\", line %d",
+									   serviceFile,
+									   linenr);
+					result = 3;
+					goto exit;
+				}
+
 				/*
 				 * Set the parameter --- but don't override any previous
 				 * explicit setting.
diff --git a/src/interfaces/libpq/t/006_service.pl b/src/interfaces/libpq/t/006_service.pl
index 4fe5adc5c2..a5c02d9895 100644
--- a/src/interfaces/libpq/t/006_service.pl
+++ b/src/interfaces/libpq/t/006_service.pl
@@ -7,7 +7,7 @@ use PostgreSQL::Test::Cluster;
 use Test::More;
 
 # This tests scenarios related to the service name and the service file,
-# for the connection options and their environment variables.
+# for the connection options, servicefile options and their environment variables.
 
 my $node = PostgreSQL::Test::Cluster->new('node');
 $node->init;
@@ -146,6 +146,69 @@ local $ENV{PGSERVICEFILE} = "$srvfile_empty";
 	unlink($srvfile_default);
 }
 
+# Backslashes escaped path string for getting collect result at concatenation
+# for Windows environment
+my $srvfile_win_cared = $srvfile_valid;
+$srvfile_win_cared =~ s/\\/\\\\/g;
+
+# Check that servicefile option works as expected
+{
+	$dummy_node->connect_ok(
+		q{service=my_srv servicefile='} . $srvfile_win_cared . q{'},
+		'service=my_srv servicefile=...',
+		sql             => "SELECT 'connect3'",
+		expected_stdout => qr/connect3/
+	);
+
+	# Encode slashes and backslash
+	my $encoded_srvfile = $srvfile_valid =~ s{([\\/])}{
+		$1 eq '/' ? '%2F' : '%5C'
+	}ger;
+
+	# Additionaly encode a colon in servicefile path of Windows
+	$encoded_srvfile =~ s/:/%3A/g;
+
+	$dummy_node->connect_ok(
+		'postgresql:///?service=my_srv&servicefile=' . $encoded_srvfile,
+		'postgresql:///?service=my_srv&servicefile=...',
+		sql             => "SELECT 'connect4'",
+		expected_stdout => qr/connect4/
+	);
+
+	local $ENV{PGSERVICE} = 'my_srv';
+	$dummy_node->connect_ok(
+		q{servicefile='} . $srvfile_win_cared . q{'},
+		'envvar: PGSERVICE=my_srv + servicefile=...',
+		sql             => "SELECT 'connect5'",
+		expected_stdout => qr/connect5/
+	);
+
+	$dummy_node->connect_ok(
+		'postgresql://?servicefile=' . $encoded_srvfile,
+		'envvar: PGSERVICE=my_srv + postgresql://?servicefile=...',
+		sql             => "SELECT 'connect6'",
+		expected_stdout => qr/connect6/
+	);
+}
+
+# Check that servicefile option takes precedence over PGSERVICEFILE environment variable
+{
+	local $ENV{PGSERVICEFILE} = 'non-existent-file.conf';
+
+	$dummy_node->connect_fails(
+		'service=my_srv',
+		'service=... fails with wrong PGSERVICEFILE',
+		expected_stderr => qr/service file "non-existent-file\.conf" not found/
+	);
+
+	$dummy_node->connect_ok(
+		q{service=my_srv servicefile='} . $srvfile_win_cared . q{'},
+		'servicefile= takes precedence over PGSERVICEFILE',
+		sql             => "SELECT 'connect7'",
+		expected_stdout => qr/connect7/
+	);
+}
+
 $node->teardown_node;
 
 done_testing();
-- 
2.34.1

#24Michael Paquier
michael@paquier.xyz
In reply to: Ryo Kanbayashi (#23)
Re: [PATCH] PGSERVICEFILE as part of a normal connection string

On Sun, Apr 13, 2025 at 07:06:06PM +0900, Ryo Kanbayashi wrote:

I rebased our patch according to 2c7bd2ba507e.
https://commitfest.postgresql.org/patch/5387/

Thanks for the new version.

-# for the connection options and their environment variables.
+# for the connection options, servicefile options and their environment variables.

It seems to me that this comment does not need to be changed.

+	{"servicefile", "PGSERVICEFILE", NULL, NULL,
+	"Database-Service-File", "", 64, -1},

Could it be better to have a new field in pg_conn? This would also
require a free() in freePGconn() and new PQserviceFile() routine.

+                if (strcmp(key, "servicefile") == 0)
+                {
+                    libpq_append_error(errorMessage,
+                                       "nested servicefile specifications not supported in service file \"%s\", line %d",
+                                       serviceFile,
+                                       linenr);
+                    result = 3;
+                    goto exit;
+                }

Perhaps we should add a test for that? The same is true with
"service", as I am looking at these code paths now. I'd suggest to
apply double quotes to the parameter name "servicefile" in this error
message, to make clear what this is.

+ # Additionaly encode a colon in servicefile path of Windows

Typo: Additionally.

+# Backslashes escaped path string for getting collect result at concatenation
+# for Windows environment

Comment is unclear. But what you mean here is that the conversion is
required to allow the test to work when giving the path to the
connection option, right?
--
Michael

#25Ryo Kanbayashi
kanbayashi.dev@gmail.com
In reply to: Michael Paquier (#24)
1 attachment(s)
Re: [PATCH] PGSERVICEFILE as part of a normal connection string

On Wed, May 28, 2025 at 3:48 PM Michael Paquier <michael@paquier.xyz> wrote:

On Sun, Apr 13, 2025 at 07:06:06PM +0900, Ryo Kanbayashi wrote:

I rebased our patch according to 2c7bd2ba507e.
https://commitfest.postgresql.org/patch/5387/

Thanks for the new version.

Thanks for review :)

-# for the connection options and their environment variables.
+# for the connection options, servicefile options and their environment variables.

It seems to me that this comment does not need to be changed.

OK.

+       {"servicefile", "PGSERVICEFILE", NULL, NULL,
+       "Database-Service-File", "", 64, -1},

Could it be better to have a new field in pg_conn? This would also
require a free() in freePGconn() and new PQserviceFile() routine.

OK.

+                if (strcmp(key, "servicefile") == 0)
+                {
+                    libpq_append_error(errorMessage,
+                                       "nested servicefile specifications not supported in service file \"%s\", line %d",
+                                       serviceFile,
+                                       linenr);
+                    result = 3;
+                    goto exit;
+                }

Perhaps we should add a test for that? The same is true with
"service", as I am looking at these code paths now. I'd suggest to
apply double quotes to the parameter name "servicefile" in this error
message, to make clear what this is.

I added test cases for nested situations.and double-quoted the parameter names.

+ # Additionaly encode a colon in servicefile path of Windows

Typo: Additionally.

OK.

+# Backslashes escaped path string for getting collect result at concatenation
+# for Windows environment

Comment is unclear. But what you mean here is that the conversion is
required to allow the test to work when giving the path to the
connection option, right?

Strictly speaking, in a Windows environment, a path containing a
backslash is stored in the $td variable, so the value of the
$srvfile_valid variable, which contains that path, also needs to be
converted.
If conversion is not performed, this test will not work correctly in a
Windows environment.
And just because a path string is included does not necessarily mean
that conversion is necessary.

But it's difficult to describe this succinctly...

---
Great regards,
Ryo Kanbayashi

Attachments:

v8-0001-add-servicefile-option-usage-on-connection-string.patchapplication/octet-stream; name=v8-0001-add-servicefile-option-usage-on-connection-string.patchDownload
From 00bc6e14ea8415fce616a245349dd2aa10dd54af Mon Sep 17 00:00:00 2001
From: Ryo Kanbayashi <ryo.contact@gmail.com>
Date: Sun, 1 Jun 2025 21:28:34 +0900
Subject: [PATCH v8] add servicefile option usage on connection string feature
 and its tests.

---
 doc/src/sgml/libpq.sgml               | 16 ++++-
 src/interfaces/libpq/fe-connect.c     | 39 +++++++++--
 src/interfaces/libpq/libpq-fe.h       |  1 +
 src/interfaces/libpq/libpq-int.h      |  1 +
 src/interfaces/libpq/t/006_service.pl | 97 +++++++++++++++++++++++++++
 5 files changed, 149 insertions(+), 5 deletions(-)

diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 695fe958c3e..8e5807311ff 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -2320,6 +2320,19 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
       </listitem>
      </varlistentry>
 
+     <varlistentry id="libpq-connect-servicefile" xreflabel="servicefile">
+      <term><literal>servicefile</literal></term>
+      <listitem>
+       <para>
+        This option specifies the name of the per-user connection service file
+        (see <xref linkend="libpq-pgservice"/>).
+        Defaults to <filename>~/.pg_service.conf</filename>, or
+        <filename>%APPDATA%\postgresql\.pg_service.conf</filename> on
+        Microsoft Windows.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="libpq-connect-target-session-attrs" xreflabel="target_session_attrs">
       <term><literal>target_session_attrs</literal></term>
       <listitem>
@@ -9596,7 +9609,8 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough)
    On Microsoft Windows, it is named
    <filename>%APPDATA%\postgresql\.pg_service.conf</filename> (where
    <filename>%APPDATA%</filename> refers to the Application Data subdirectory
-   in the user's profile).  A different file name can be specified by
+   in the user's profile).  A different file name can be specified using the
+   <literal>servicefile</literal> key word in a libpq connection string or by
    setting the environment variable <envar>PGSERVICEFILE</envar>.
    The system-wide file is named <filename>pg_service.conf</filename>.
    By default it is sought in the <filename>etc</filename> directory
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index ccb01aad361..2ac783e5e00 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -201,6 +201,10 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
 		"Database-Service", "", 20,
 	offsetof(struct pg_conn, pgservice)},
 
+	{"servicefile", "PGSERVICEFILE", NULL, NULL,
+	"Database-Service-File", "", 64,
+	offsetof(struct pg_conn, pgservicefile)},
+
 	{"user", "PGUSER", NULL, NULL,
 		"Database-User", "", 20,
 	offsetof(struct pg_conn, pguser)},
@@ -5062,6 +5066,7 @@ freePGconn(PGconn *conn)
 	free(conn->dbName);
 	free(conn->replication);
 	free(conn->pgservice);
+	free(conn->pgservicefile);
 	free(conn->pguser);
 	if (conn->pgpass)
 	{
@@ -5914,6 +5919,7 @@ static int
 parseServiceInfo(PQconninfoOption *options, PQExpBuffer errorMessage)
 {
 	const char *service = conninfo_getval(options, "service");
+	const char *service_fname = conninfo_getval(options, "servicefile");
 	char		serviceFile[MAXPGPATH];
 	char	   *env;
 	bool		group_found = false;
@@ -5933,11 +5939,18 @@ parseServiceInfo(PQconninfoOption *options, PQExpBuffer errorMessage)
 		return 0;
 
 	/*
-	 * Try PGSERVICEFILE if specified, else try ~/.pg_service.conf (if that
-	 * exists).
+	 * First, check servicefile option on connection string. Second, check
+	 * PGSERVICEFILE environment variable. Finally, check ~/.pg_service.conf
+	 * (if that exists).
 	 */
-	if ((env = getenv("PGSERVICEFILE")) != NULL)
+	if (service_fname != NULL)
+	{
+		strlcpy(serviceFile, service_fname, sizeof(serviceFile));
+	}
+	else if ((env = getenv("PGSERVICEFILE")) != NULL)
+	{
 		strlcpy(serviceFile, env, sizeof(serviceFile));
+	}
 	else
 	{
 		char		homedir[MAXPGPATH];
@@ -6092,7 +6105,17 @@ parseServiceFile(const char *serviceFile,
 				if (strcmp(key, "service") == 0)
 				{
 					libpq_append_error(errorMessage,
-									   "nested service specifications not supported in service file \"%s\", line %d",
+									   "nested \"service\" specifications not supported in service file \"%s\", line %d",
+									   serviceFile,
+									   linenr);
+					result = 3;
+					goto exit;
+				}
+
+				if (strcmp(key, "servicefile") == 0)
+				{
+					libpq_append_error(errorMessage,
+									   "nested \"servicefile\" specifications not supported in service file \"%s\", line %d",
 									   serviceFile,
 									   linenr);
 					result = 3;
@@ -7469,6 +7492,14 @@ PQservice(const PGconn *conn)
 	return conn->pgservice;
 }
 
+char *
+PQserviceFile(const PGconn *conn)
+{
+	if (!conn)
+		return NULL;
+	return conn->pgservicefile;
+}
+
 char *
 PQuser(const PGconn *conn)
 {
diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h
index 7d3a9df6fd5..c7c443531c2 100644
--- a/src/interfaces/libpq/libpq-fe.h
+++ b/src/interfaces/libpq/libpq-fe.h
@@ -401,6 +401,7 @@ extern int	PQrequestCancel(PGconn *conn);
 /* Accessor functions for PGconn objects */
 extern char *PQdb(const PGconn *conn);
 extern char *PQservice(const PGconn *conn);
+extern char *PQserviceFile(const PGconn *conn);
 extern char *PQuser(const PGconn *conn);
 extern char *PQpass(const PGconn *conn);
 extern char *PQhost(const PGconn *conn);
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index a6cfd7f5c9d..5ae4e88f0b7 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -389,6 +389,7 @@ struct pg_conn
 	char	   *dbName;			/* database name */
 	char	   *replication;	/* connect as the replication standby? */
 	char	   *pgservice;		/* Postgres service, if any */
+	char	   *pgservicefile;	/* path to a service file containing service(s) */
 	char	   *pguser;			/* Postgres username and password, if any */
 	char	   *pgpass;
 	char	   *pgpassfile;		/* path to a file containing password(s) */
diff --git a/src/interfaces/libpq/t/006_service.pl b/src/interfaces/libpq/t/006_service.pl
index 4fe5adc5c2a..c40d3f7fa78 100644
--- a/src/interfaces/libpq/t/006_service.pl
+++ b/src/interfaces/libpq/t/006_service.pl
@@ -47,6 +47,21 @@ my $srvfile_default = "$td/pg_service.conf";
 # Missing service file.
 my $srvfile_missing = "$td/pg_service_missing.conf";
 
+# "service" param included service file (invalid) 
+# including contents of pg_service_valid.conf and a nested service option
+my $srvfile_service_nested = "$td/pg_service_service_nested.conf";
+copy($srvfile_valid, $srvfile_service_nested) or 
+	die "Could not copy $srvfile_valid to $srvfile_service_nested: $!";
+append_to_file($srvfile_service_nested, 'service=tmp_srv' . $newline);
+
+# "servicefile" param included service file (invalid)
+# including contents of pg_service_valid.conf and a nested servicefile option
+my $srvfile_servicefile_nested = "$td/pg_service_servicefile_nested.conf";
+copy($srvfile_valid, $srvfile_servicefile_nested) or 
+	die "Could not copy $srvfile_valid to $srvfile_servicefile_nested: $!";
+append_to_file($srvfile_servicefile_nested, 'servicefile=' . $srvfile_default . $newline);
+
+
 # Set the fallback directory lookup of the service file to the temporary
 # directory of this test.  PGSYSCONFDIR is used if the service file
 # defined in PGSERVICEFILE cannot be found, or when a service file is
@@ -146,6 +161,88 @@ local $ENV{PGSERVICEFILE} = "$srvfile_empty";
 	unlink($srvfile_default);
 }
 
+# Backslashes escaped path string for getting collect result at concatenation
+# for Windows environment
+my $srvfile_win_cared = $srvfile_valid;
+$srvfile_win_cared =~ s/\\/\\\\/g;
+
+# Check that servicefile option works as expected
+{
+	$dummy_node->connect_ok(
+		q{service=my_srv servicefile='} . $srvfile_win_cared . q{'},
+		'service=my_srv servicefile=...',
+		sql             => "SELECT 'connect3'",
+		expected_stdout => qr/connect3/
+	);
+
+	# Encode slashes and backslash
+	my $encoded_srvfile = $srvfile_valid =~ s{([\\/])}{
+		$1 eq '/' ? '%2F' : '%5C'
+	}ger;
+
+	# Additionally encode a colon in servicefile path of Windows
+	$encoded_srvfile =~ s/:/%3A/g;
+
+	$dummy_node->connect_ok(
+		'postgresql:///?service=my_srv&servicefile=' . $encoded_srvfile,
+		'postgresql:///?service=my_srv&servicefile=...',
+		sql             => "SELECT 'connect4'",
+		expected_stdout => qr/connect4/
+	);
+
+	local $ENV{PGSERVICE} = 'my_srv';
+	$dummy_node->connect_ok(
+		q{servicefile='} . $srvfile_win_cared . q{'},
+		'envvar: PGSERVICE=my_srv + servicefile=...',
+		sql             => "SELECT 'connect5'",
+		expected_stdout => qr/connect5/
+	);
+
+	$dummy_node->connect_ok(
+		'postgresql://?servicefile=' . $encoded_srvfile,
+		'envvar: PGSERVICE=my_srv + postgresql://?servicefile=...',
+		sql             => "SELECT 'connect6'",
+		expected_stdout => qr/connect6/
+	);
+}
+
+# Check that servicefile option takes precedence over PGSERVICEFILE environment variable
+{
+	local $ENV{PGSERVICEFILE} = 'non-existent-file.conf';
+
+	$dummy_node->connect_fails(
+		'service=my_srv',
+		'service=... fails with wrong PGSERVICEFILE',
+		expected_stderr => qr/service file "non-existent-file\.conf" not found/
+	);
+
+	$dummy_node->connect_ok(
+		q{service=my_srv servicefile='} . $srvfile_win_cared . q{'},
+		'servicefile= takes precedence over PGSERVICEFILE',
+		sql             => "SELECT 'connect7'",
+		expected_stdout => qr/connect7/
+	);
+}
+
+# Check that service file which contains nested service and servicefile options fails
+{
+	local $ENV{PGSERVICEFILE} = $srvfile_service_nested;
+
+	$dummy_node->connect_fails(
+		'service=my_srv',
+		'service=... fails with nested service option in service file',
+		expected_stderr => qr/nested "service" specifications not supported in service file/
+	);
+
+	local $ENV{PGSERVICEFILE} = $srvfile_servicefile_nested;
+
+	$dummy_node->connect_fails(
+		'service=my_srv',
+		'servicefile=... fails with nested service option in service file',
+		expected_stderr => qr/nested "servicefile" specifications not supported in service file/
+	);
+}
+
 $node->teardown_node;
 
 done_testing();
-- 
2.45.1.windows.1

#26Michael Paquier
michael@paquier.xyz
In reply to: Ryo Kanbayashi (#25)
Re: [PATCH] PGSERVICEFILE as part of a normal connection string

On Sun, Jun 01, 2025 at 09:36:08PM +0900, Ryo Kanbayashi wrote:

Strictly speaking, in a Windows environment, a path containing a
backslash is stored in the $td variable, so the value of the
$srvfile_valid variable, which contains that path, also needs to be
converted.
If conversion is not performed, this test will not work correctly in a
Windows environment.
And just because a path string is included does not necessarily mean
that conversion is necessary.

But it's difficult to describe this succinctly...

+# Backslashes escaped path string for getting collect result at concatenation
+# for Windows environment

I could suggest a simpler sentence here:
"Use correct escaped path for Windows."

Worth noting a few things reported by a `git diff --check`.

+char *
+PQserviceFile(const PGconn *conn)

All these APIs are public and need to be documented.

Another thing that could be added on top of the rest is a psql
variable called SERVICEFILE, and we would be rather feature complete,
with:
- Support in SyncVariables(), as in psql/command.c.
- Some documentation as well for the new psql variable, mapping with
the existing SERVICE.
- Perhaps a shortcut for PROMPT?
- Use of your new libpq API PQserviceFile().
This could be a patch built on top of the introduction of the core API
for the service file.
--
Michael

#27Ryo Kanbayashi
kanbayashi.dev@gmail.com
In reply to: Michael Paquier (#26)
1 attachment(s)
Re: [PATCH] PGSERVICEFILE as part of a normal connection string

On Wed, Jun 4, 2025 at 1:42 PM Michael Paquier <michael@paquier.xyz> wrote:

On Sun, Jun 01, 2025 at 09:36:08PM +0900, Ryo Kanbayashi wrote:

Strictly speaking, in a Windows environment, a path containing a
backslash is stored in the $td variable, so the value of the
$srvfile_valid variable, which contains that path, also needs to be
converted.
If conversion is not performed, this test will not work correctly in a
Windows environment.
And just because a path string is included does not necessarily mean
that conversion is necessary.

But it's difficult to describe this succinctly...

+# Backslashes escaped path string for getting collect result at concatenation
+# for Windows environment

I could suggest a simpler sentence here:
"Use correct escaped path for Windows."

OK.
Thanks for suggestion!

Worth noting a few things reported by a `git diff --check`.

OK.

+char *
+PQserviceFile(const PGconn *conn)

All these APIs are public and need to be documented.

OK.

Another thing that could be added on top of the rest is a psql
variable called SERVICEFILE, and we would be rather feature complete,
with:
- Support in SyncVariables(), as in psql/command.c.

OK.

- Some documentation as well for the new psql variable, mapping with
the existing SERVICE.

OK.

- Perhaps a shortcut for PROMPT?

I will kindly take a rain check on this one :)

- Use of your new libpq API PQserviceFile().

OK.

This could be a patch built on top of the introduction of the core API
for the service file.

:)

---
Great regards,
Ryo Kanbayashi

Attachments:

v9-0001-servicefile-option-usage-on-connection-string-fea.patchapplication/octet-stream; name=v9-0001-servicefile-option-usage-on-connection-string-fea.patchDownload
From 3f60cf47e50884aec2e77ac9a65b44cd84f63298 Mon Sep 17 00:00:00 2001
From: Ryo Kanbayashi <ryo.contact@gmail.com>
Date: Mon, 9 Jun 2025 22:20:04 +0900
Subject: [PATCH v9] servicefile option usage on connection string feature and
 its tests.

---
 doc/src/sgml/libpq.sgml               | 37 ++++++++++-
 doc/src/sgml/ref/psql-ref.sgml        |  9 +++
 src/bin/psql/command.c                |  2 +
 src/interfaces/libpq/exports.txt      | 11 +--
 src/interfaces/libpq/fe-connect.c     | 39 +++++++++--
 src/interfaces/libpq/libpq-fe.h       |  1 +
 src/interfaces/libpq/libpq-int.h      |  1 +
 src/interfaces/libpq/t/006_service.pl | 96 +++++++++++++++++++++++++++
 8 files changed, 186 insertions(+), 10 deletions(-)

diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 695fe958c3e..5e410ae8004 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -2320,6 +2320,19 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
       </listitem>
      </varlistentry>
 
+     <varlistentry id="libpq-connect-servicefile" xreflabel="servicefile">
+      <term><literal>servicefile</literal></term>
+      <listitem>
+       <para>
+        This option specifies the name of the per-user connection service file
+        (see <xref linkend="libpq-pgservice"/>).
+        Defaults to <filename>~/.pg_service.conf</filename>, or
+        <filename>%APPDATA%\postgresql\.pg_service.conf</filename> on
+        Microsoft Windows.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="libpq-connect-target-session-attrs" xreflabel="target_session_attrs">
       <term><literal>target_session_attrs</literal></term>
       <listitem>
@@ -2760,6 +2773,25 @@ char *PQservice(const PGconn *conn);
      </listitem>
     </varlistentry>
 
+    <varlistentry id="libpq-PQserviceFile">
+     <term><function>PQserviceFile</function></term>
+
+     <listitem>
+      <para>
+       Returns the service file name of the active connection.
+<synopsis>
+char *PQserviceFile(const PGconn *conn);
+</synopsis>
+      </para>
+
+      <para>
+       <xref linkend="libpq-PQserviceFile"/> returns <symbol>NULL</symbol> if the
+       <parameter>conn</parameter> argument is <symbol>NULL</symbol>.
+       Otherwise, if there was no service file provided, it returns an empty string.
+      </para>
+     </listitem>
+    </varlistentry>
+
     <varlistentry id="libpq-PQtty">
      <term><function>PQtty</function><indexterm><primary>PQtty</primary></indexterm></term>
 
@@ -9166,6 +9198,8 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough)
       Defaults to <filename>~/.pg_service.conf</filename>, or
       <filename>%APPDATA%\postgresql\.pg_service.conf</filename> on
       Microsoft Windows.
+      <envar>This environment variable</envar> behaves the same as the <xref
+      linkend="libpq-connect-servicefile"/> connection parameter.
      </para>
     </listitem>
 
@@ -9596,7 +9630,8 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough)
    On Microsoft Windows, it is named
    <filename>%APPDATA%\postgresql\.pg_service.conf</filename> (where
    <filename>%APPDATA%</filename> refers to the Application Data subdirectory
-   in the user's profile).  A different file name can be specified by
+   in the user's profile).  A different file name can be specified using the
+   <literal>servicefile</literal> key word in a libpq connection string or by
    setting the environment variable <envar>PGSERVICEFILE</envar>.
    The system-wide file is named <filename>pg_service.conf</filename>.
    By default it is sought in the <filename>etc</filename> directory
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 8f7d8758ca0..e55f144f9a0 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -4610,6 +4610,15 @@ bar
         </listitem>
       </varlistentry>
 
+      <varlistentry id="app-psql-variables-servicefile">
+        <term><varname>SERVICEFILE</varname></term>
+        <listitem>
+        <para>
+        The service file name, if applicable.
+        </para>
+        </listitem>
+      </varlistentry>
+
       <varlistentry id="app-psql-variables-shell-error">
        <term><varname>SHELL_ERROR</varname></term>
        <listitem>
diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index 81a5ba844ba..4305e398b34 100644
--- a/src/bin/psql/command.c
+++ b/src/bin/psql/command.c
@@ -4487,6 +4487,7 @@ SyncVariables(void)
 
 	SetVariable(pset.vars, "DBNAME", PQdb(pset.db));
 	SetVariable(pset.vars, "SERVICE", PQservice(pset.db));
+	SetVariable(pset.vars, "SERVICEFILE", PQserviceFile(pset.db));
 	SetVariable(pset.vars, "USER", PQuser(pset.db));
 	SetVariable(pset.vars, "HOST", PQhost(pset.db));
 	SetVariable(pset.vars, "PORT", PQport(pset.db));
@@ -4521,6 +4522,7 @@ UnsyncVariables(void)
 {
 	SetVariable(pset.vars, "DBNAME", NULL);
 	SetVariable(pset.vars, "SERVICE", NULL);
+	SetVariable(pset.vars, "SERVICEFILE", NULL);
 	SetVariable(pset.vars, "USER", NULL);
 	SetVariable(pset.vars, "HOST", NULL);
 	SetVariable(pset.vars, "PORT", NULL);
diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt
index 0625cf39e9a..72b6584e795 100644
--- a/src/interfaces/libpq/exports.txt
+++ b/src/interfaces/libpq/exports.txt
@@ -206,8 +206,9 @@ PQsocketPoll              203
 PQsetChunkedRowsMode      204
 PQgetCurrentTimeUSec      205
 PQservice                 206
-PQsetAuthDataHook         207
-PQgetAuthDataHook         208
-PQdefaultAuthDataHook     209
-PQfullProtocolVersion     210
-appendPQExpBufferVA       211
+PQserviceFile             207
+PQsetAuthDataHook         208
+PQgetAuthDataHook         209
+PQdefaultAuthDataHook     210
+PQfullProtocolVersion     211
+appendPQExpBufferVA       212
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index ccb01aad361..2ac783e5e00 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -201,6 +201,10 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
 		"Database-Service", "", 20,
 	offsetof(struct pg_conn, pgservice)},
 
+	{"servicefile", "PGSERVICEFILE", NULL, NULL,
+	"Database-Service-File", "", 64,
+	offsetof(struct pg_conn, pgservicefile)},
+
 	{"user", "PGUSER", NULL, NULL,
 		"Database-User", "", 20,
 	offsetof(struct pg_conn, pguser)},
@@ -5062,6 +5066,7 @@ freePGconn(PGconn *conn)
 	free(conn->dbName);
 	free(conn->replication);
 	free(conn->pgservice);
+	free(conn->pgservicefile);
 	free(conn->pguser);
 	if (conn->pgpass)
 	{
@@ -5914,6 +5919,7 @@ static int
 parseServiceInfo(PQconninfoOption *options, PQExpBuffer errorMessage)
 {
 	const char *service = conninfo_getval(options, "service");
+	const char *service_fname = conninfo_getval(options, "servicefile");
 	char		serviceFile[MAXPGPATH];
 	char	   *env;
 	bool		group_found = false;
@@ -5933,11 +5939,18 @@ parseServiceInfo(PQconninfoOption *options, PQExpBuffer errorMessage)
 		return 0;
 
 	/*
-	 * Try PGSERVICEFILE if specified, else try ~/.pg_service.conf (if that
-	 * exists).
+	 * First, check servicefile option on connection string. Second, check
+	 * PGSERVICEFILE environment variable. Finally, check ~/.pg_service.conf
+	 * (if that exists).
 	 */
-	if ((env = getenv("PGSERVICEFILE")) != NULL)
+	if (service_fname != NULL)
+	{
+		strlcpy(serviceFile, service_fname, sizeof(serviceFile));
+	}
+	else if ((env = getenv("PGSERVICEFILE")) != NULL)
+	{
 		strlcpy(serviceFile, env, sizeof(serviceFile));
+	}
 	else
 	{
 		char		homedir[MAXPGPATH];
@@ -6092,7 +6105,17 @@ parseServiceFile(const char *serviceFile,
 				if (strcmp(key, "service") == 0)
 				{
 					libpq_append_error(errorMessage,
-									   "nested service specifications not supported in service file \"%s\", line %d",
+									   "nested \"service\" specifications not supported in service file \"%s\", line %d",
+									   serviceFile,
+									   linenr);
+					result = 3;
+					goto exit;
+				}
+
+				if (strcmp(key, "servicefile") == 0)
+				{
+					libpq_append_error(errorMessage,
+									   "nested \"servicefile\" specifications not supported in service file \"%s\", line %d",
 									   serviceFile,
 									   linenr);
 					result = 3;
@@ -7469,6 +7492,14 @@ PQservice(const PGconn *conn)
 	return conn->pgservice;
 }
 
+char *
+PQserviceFile(const PGconn *conn)
+{
+	if (!conn)
+		return NULL;
+	return conn->pgservicefile;
+}
+
 char *
 PQuser(const PGconn *conn)
 {
diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h
index 7d3a9df6fd5..c7c443531c2 100644
--- a/src/interfaces/libpq/libpq-fe.h
+++ b/src/interfaces/libpq/libpq-fe.h
@@ -401,6 +401,7 @@ extern int	PQrequestCancel(PGconn *conn);
 /* Accessor functions for PGconn objects */
 extern char *PQdb(const PGconn *conn);
 extern char *PQservice(const PGconn *conn);
+extern char *PQserviceFile(const PGconn *conn);
 extern char *PQuser(const PGconn *conn);
 extern char *PQpass(const PGconn *conn);
 extern char *PQhost(const PGconn *conn);
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index a6cfd7f5c9d..5ae4e88f0b7 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -389,6 +389,7 @@ struct pg_conn
 	char	   *dbName;			/* database name */
 	char	   *replication;	/* connect as the replication standby? */
 	char	   *pgservice;		/* Postgres service, if any */
+	char	   *pgservicefile;	/* path to a service file containing service(s) */
 	char	   *pguser;			/* Postgres username and password, if any */
 	char	   *pgpass;
 	char	   *pgpassfile;		/* path to a file containing password(s) */
diff --git a/src/interfaces/libpq/t/006_service.pl b/src/interfaces/libpq/t/006_service.pl
index 4fe5adc5c2a..b27b3c17d13 100644
--- a/src/interfaces/libpq/t/006_service.pl
+++ b/src/interfaces/libpq/t/006_service.pl
@@ -47,6 +47,21 @@ my $srvfile_default = "$td/pg_service.conf";
 # Missing service file.
 my $srvfile_missing = "$td/pg_service_missing.conf";
 
+# "service" param included service file (invalid)
+# including contents of pg_service_valid.conf and a nested service option
+my $srvfile_service_nested = "$td/pg_service_service_nested.conf";
+copy($srvfile_valid, $srvfile_service_nested) or
+	die "Could not copy $srvfile_valid to $srvfile_service_nested: $!";
+append_to_file($srvfile_service_nested, 'service=tmp_srv' . $newline);
+
+# "servicefile" param included service file (invalid)
+# including contents of pg_service_valid.conf and a nested servicefile option
+my $srvfile_servicefile_nested = "$td/pg_service_servicefile_nested.conf";
+copy($srvfile_valid, $srvfile_servicefile_nested) or
+	die "Could not copy $srvfile_valid to $srvfile_servicefile_nested: $!";
+append_to_file($srvfile_servicefile_nested, 'servicefile=' . $srvfile_default . $newline);
+
+
 # Set the fallback directory lookup of the service file to the temporary
 # directory of this test.  PGSYSCONFDIR is used if the service file
 # defined in PGSERVICEFILE cannot be found, or when a service file is
@@ -146,6 +161,87 @@ local $ENV{PGSERVICEFILE} = "$srvfile_empty";
 	unlink($srvfile_default);
 }
 
+# Use correct escaped path for Windows.
+my $srvfile_win_cared = $srvfile_valid;
+$srvfile_win_cared =~ s/\\/\\\\/g;
+
+# Check that servicefile option works as expected
+{
+	$dummy_node->connect_ok(
+		q{service=my_srv servicefile='} . $srvfile_win_cared . q{'},
+		'service=my_srv servicefile=...',
+		sql             => "SELECT 'connect3'",
+		expected_stdout => qr/connect3/
+	);
+
+	# Encode slashes and backslash
+	my $encoded_srvfile = $srvfile_valid =~ s{([\\/])}{
+		$1 eq '/' ? '%2F' : '%5C'
+	}ger;
+
+	# Additionally encode a colon in servicefile path of Windows
+	$encoded_srvfile =~ s/:/%3A/g;
+
+	$dummy_node->connect_ok(
+		'postgresql:///?service=my_srv&servicefile=' . $encoded_srvfile,
+		'postgresql:///?service=my_srv&servicefile=...',
+		sql             => "SELECT 'connect4'",
+		expected_stdout => qr/connect4/
+	);
+
+	local $ENV{PGSERVICE} = 'my_srv';
+	$dummy_node->connect_ok(
+		q{servicefile='} . $srvfile_win_cared . q{'},
+		'envvar: PGSERVICE=my_srv + servicefile=...',
+		sql             => "SELECT 'connect5'",
+		expected_stdout => qr/connect5/
+	);
+
+	$dummy_node->connect_ok(
+		'postgresql://?servicefile=' . $encoded_srvfile,
+		'envvar: PGSERVICE=my_srv + postgresql://?servicefile=...',
+		sql             => "SELECT 'connect6'",
+		expected_stdout => qr/connect6/
+	);
+}
+
+# Check that servicefile option takes precedence over PGSERVICEFILE environment variable
+{
+	local $ENV{PGSERVICEFILE} = 'non-existent-file.conf';
+
+	$dummy_node->connect_fails(
+		'service=my_srv',
+		'service=... fails with wrong PGSERVICEFILE',
+		expected_stderr => qr/service file "non-existent-file\.conf" not found/
+	);
+
+	$dummy_node->connect_ok(
+		q{service=my_srv servicefile='} . $srvfile_win_cared . q{'},
+		'servicefile= takes precedence over PGSERVICEFILE',
+		sql             => "SELECT 'connect7'",
+		expected_stdout => qr/connect7/
+	);
+}
+
+# Check that service file which contains nested service and servicefile options fails
+{
+	local $ENV{PGSERVICEFILE} = $srvfile_service_nested;
+
+	$dummy_node->connect_fails(
+		'service=my_srv',
+		'service=... fails with nested service option in service file',
+		expected_stderr => qr/nested "service" specifications not supported in service file/
+	);
+
+	local $ENV{PGSERVICEFILE} = $srvfile_servicefile_nested;
+
+	$dummy_node->connect_fails(
+		'service=my_srv',
+		'servicefile=... fails with nested service option in service file',
+		expected_stderr => qr/nested "servicefile" specifications not supported in service file/
+	);
+}
+
 $node->teardown_node;
 
 done_testing();
-- 
2.45.1.windows.1

#28Michael Paquier
michael@paquier.xyz
In reply to: Ryo Kanbayashi (#27)
Re: [PATCH] PGSERVICEFILE as part of a normal connection string

On Mon, Jun 09, 2025 at 10:25:26PM +0900, Ryo Kanbayashi wrote:

This could be a patch built on top of the introduction of the core API
for the service file.

:)

- Perhaps a shortcut for PROMPT?

I will kindly take a rain check on this one :)

I am not sure to understand what you mean here, but let's discard this
idea as it is also possible to use %:name: in a psql's prompt with the
new variable you are introducing.

+        Defaults to <filename>~/.pg_service.conf</filename>, or
+        <filename>%APPDATA%\postgresql\.pg_service.conf</filename> on
+        Microsoft Windows.

I don't think that this sentence is true. The parameter does not
default to these values. The connection logic would fall back to
these files if the parameter is not defined, and the parameter knows
nothing about them.

--- a/src/interfaces/libpq/exports.txt
+++ b/src/interfaces/libpq/exports.txt
@@ -206,8 +206,9 @@ PQsocketPoll              203
 PQsetChunkedRowsMode      204
 PQgetCurrentTimeUSec      205
 PQservice                 206
-PQsetAuthDataHook         207
-PQgetAuthDataHook         208
-PQdefaultAuthDataHook     209
-PQfullProtocolVersion     210
-appendPQExpBufferVA       211
+PQserviceFile             207
+PQsetAuthDataHook         208
+PQgetAuthDataHook         209
+PQdefaultAuthDataHook     210
+PQfullProtocolVersion     211
+appendPQExpBufferVA       212

The new one goes to the bottom AFAIK.

The patch can be split into multiple pieces:
- Core libpq changes with API and tests.
- psql changes.

+# "service" param included service file (invalid)
+# including contents of pg_service_valid.conf and a nested service option

"Service file with service defined (invalid)."

+# "servicefile" param included service file (invalid)
+# including contents of pg_service_valid.conf and a nested servicefile option

"Service file with servicefile defined (invalid)."
--
Michael

#29Ryo Kanbayashi
kanbayashi.dev@gmail.com
In reply to: Michael Paquier (#28)
2 attachment(s)
Re: [PATCH] PGSERVICEFILE as part of a normal connection string

On Tue, Jun 10, 2025 at 4:30 PM Michael Paquier <michael@paquier.xyz> wrote:

On Mon, Jun 09, 2025 at 10:25:26PM +0900, Ryo Kanbayashi wrote:

This could be a patch built on top of the introduction of the core API
for the service file.

:)

- Perhaps a shortcut for PROMPT?

I will kindly take a rain check on this one :)

I am not sure to understand what you mean here, but let's discard this
idea as it is also possible to use %:name: in a psql's prompt with the
new variable you are introducing.

OK ^^;

+        Defaults to <filename>~/.pg_service.conf</filename>, or
+        <filename>%APPDATA%\postgresql\.pg_service.conf</filename> on
+        Microsoft Windows.

I don't think that this sentence is true. The parameter does not
default to these values. The connection logic would fall back to
these files if the parameter is not defined, and the parameter knows
nothing about them.

OK.
I removed the sentence.

--- a/src/interfaces/libpq/exports.txt
+++ b/src/interfaces/libpq/exports.txt
@@ -206,8 +206,9 @@ PQsocketPoll              203
PQsetChunkedRowsMode      204
PQgetCurrentTimeUSec      205
PQservice                 206
-PQsetAuthDataHook         207
-PQgetAuthDataHook         208
-PQdefaultAuthDataHook     209
-PQfullProtocolVersion     210
-appendPQExpBufferVA       211
+PQserviceFile             207
+PQsetAuthDataHook         208
+PQgetAuthDataHook         209
+PQdefaultAuthDataHook     210
+PQfullProtocolVersion     211
+appendPQExpBufferVA       212

The new one goes to the bottom AFAIK.

OK.

The patch can be split into multiple pieces:
- Core libpq changes with API and tests.
- psql changes.

OK.
I splited our patch to above 2 pieces.

+# "service" param included service file (invalid)
+# including contents of pg_service_valid.conf and a nested service option

"Service file with service defined (invalid)."

+# "servicefile" param included service file (invalid)
+# including contents of pg_service_valid.conf and a nested servicefile option

"Service file with servicefile defined (invalid)."

OK

Thanks for review :)

---
Great Regards,
Ryo Kanbayashi

Attachments:

v10-0001-servicefile-option-usage-on-connection-string-fe.patchapplication/octet-stream; name=v10-0001-servicefile-option-usage-on-connection-string-fe.patchDownload
From 3eb9cfc91c95132acc48d8cc862d9972d5be2db9 Mon Sep 17 00:00:00 2001
From: Ryo Kanbayashi <ryo.contact@gmail.com>
Date: Sun, 15 Jun 2025 20:34:02 +0900
Subject: [PATCH v10 1/2] servicefile option usage on connection string feature
 and its tests.

---
 doc/src/sgml/libpq.sgml               | 34 +++++++++-
 src/interfaces/libpq/exports.txt      |  1 +
 src/interfaces/libpq/fe-connect.c     | 39 +++++++++--
 src/interfaces/libpq/libpq-fe.h       |  1 +
 src/interfaces/libpq/libpq-int.h      |  1 +
 src/interfaces/libpq/t/006_service.pl | 94 +++++++++++++++++++++++++++
 6 files changed, 165 insertions(+), 5 deletions(-)

diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 695fe958c3e..cb1fd28229a 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -2320,6 +2320,16 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
       </listitem>
      </varlistentry>
 
+     <varlistentry id="libpq-connect-servicefile" xreflabel="servicefile">
+      <term><literal>servicefile</literal></term>
+      <listitem>
+       <para>
+        This option specifies the location of connection service file
+        (see <xref linkend="libpq-pgservice"/>).
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="libpq-connect-target-session-attrs" xreflabel="target_session_attrs">
       <term><literal>target_session_attrs</literal></term>
       <listitem>
@@ -2760,6 +2770,25 @@ char *PQservice(const PGconn *conn);
      </listitem>
     </varlistentry>
 
+    <varlistentry id="libpq-PQserviceFile">
+     <term><function>PQserviceFile</function></term>
+
+     <listitem>
+      <para>
+       Returns the service file name of the active connection.
+<synopsis>
+char *PQserviceFile(const PGconn *conn);
+</synopsis>
+      </para>
+
+      <para>
+       <xref linkend="libpq-PQserviceFile"/> returns <symbol>NULL</symbol> if the
+       <parameter>conn</parameter> argument is <symbol>NULL</symbol>.
+       Otherwise, if there was no service file provided, it returns an empty string.
+      </para>
+     </listitem>
+    </varlistentry>
+
     <varlistentry id="libpq-PQtty">
      <term><function>PQtty</function><indexterm><primary>PQtty</primary></indexterm></term>
 
@@ -9166,6 +9195,8 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough)
       Defaults to <filename>~/.pg_service.conf</filename>, or
       <filename>%APPDATA%\postgresql\.pg_service.conf</filename> on
       Microsoft Windows.
+      <envar>This environment variable</envar> behaves the same as the <xref
+      linkend="libpq-connect-servicefile"/> connection parameter.
      </para>
     </listitem>
 
@@ -9596,7 +9627,8 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough)
    On Microsoft Windows, it is named
    <filename>%APPDATA%\postgresql\.pg_service.conf</filename> (where
    <filename>%APPDATA%</filename> refers to the Application Data subdirectory
-   in the user's profile).  A different file name can be specified by
+   in the user's profile).  A different file name can be specified using the
+   <literal>servicefile</literal> key word in a libpq connection string or by
    setting the environment variable <envar>PGSERVICEFILE</envar>.
    The system-wide file is named <filename>pg_service.conf</filename>.
    By default it is sought in the <filename>etc</filename> directory
diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt
index 0625cf39e9a..366f6553858 100644
--- a/src/interfaces/libpq/exports.txt
+++ b/src/interfaces/libpq/exports.txt
@@ -211,3 +211,4 @@ PQgetAuthDataHook         208
 PQdefaultAuthDataHook     209
 PQfullProtocolVersion     210
 appendPQExpBufferVA       211
+PQserviceFile             212
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index ccb01aad361..2ac783e5e00 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -201,6 +201,10 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
 		"Database-Service", "", 20,
 	offsetof(struct pg_conn, pgservice)},
 
+	{"servicefile", "PGSERVICEFILE", NULL, NULL,
+	"Database-Service-File", "", 64,
+	offsetof(struct pg_conn, pgservicefile)},
+
 	{"user", "PGUSER", NULL, NULL,
 		"Database-User", "", 20,
 	offsetof(struct pg_conn, pguser)},
@@ -5062,6 +5066,7 @@ freePGconn(PGconn *conn)
 	free(conn->dbName);
 	free(conn->replication);
 	free(conn->pgservice);
+	free(conn->pgservicefile);
 	free(conn->pguser);
 	if (conn->pgpass)
 	{
@@ -5914,6 +5919,7 @@ static int
 parseServiceInfo(PQconninfoOption *options, PQExpBuffer errorMessage)
 {
 	const char *service = conninfo_getval(options, "service");
+	const char *service_fname = conninfo_getval(options, "servicefile");
 	char		serviceFile[MAXPGPATH];
 	char	   *env;
 	bool		group_found = false;
@@ -5933,11 +5939,18 @@ parseServiceInfo(PQconninfoOption *options, PQExpBuffer errorMessage)
 		return 0;
 
 	/*
-	 * Try PGSERVICEFILE if specified, else try ~/.pg_service.conf (if that
-	 * exists).
+	 * First, check servicefile option on connection string. Second, check
+	 * PGSERVICEFILE environment variable. Finally, check ~/.pg_service.conf
+	 * (if that exists).
 	 */
-	if ((env = getenv("PGSERVICEFILE")) != NULL)
+	if (service_fname != NULL)
+	{
+		strlcpy(serviceFile, service_fname, sizeof(serviceFile));
+	}
+	else if ((env = getenv("PGSERVICEFILE")) != NULL)
+	{
 		strlcpy(serviceFile, env, sizeof(serviceFile));
+	}
 	else
 	{
 		char		homedir[MAXPGPATH];
@@ -6092,7 +6105,17 @@ parseServiceFile(const char *serviceFile,
 				if (strcmp(key, "service") == 0)
 				{
 					libpq_append_error(errorMessage,
-									   "nested service specifications not supported in service file \"%s\", line %d",
+									   "nested \"service\" specifications not supported in service file \"%s\", line %d",
+									   serviceFile,
+									   linenr);
+					result = 3;
+					goto exit;
+				}
+
+				if (strcmp(key, "servicefile") == 0)
+				{
+					libpq_append_error(errorMessage,
+									   "nested \"servicefile\" specifications not supported in service file \"%s\", line %d",
 									   serviceFile,
 									   linenr);
 					result = 3;
@@ -7469,6 +7492,14 @@ PQservice(const PGconn *conn)
 	return conn->pgservice;
 }
 
+char *
+PQserviceFile(const PGconn *conn)
+{
+	if (!conn)
+		return NULL;
+	return conn->pgservicefile;
+}
+
 char *
 PQuser(const PGconn *conn)
 {
diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h
index 7d3a9df6fd5..c7c443531c2 100644
--- a/src/interfaces/libpq/libpq-fe.h
+++ b/src/interfaces/libpq/libpq-fe.h
@@ -401,6 +401,7 @@ extern int	PQrequestCancel(PGconn *conn);
 /* Accessor functions for PGconn objects */
 extern char *PQdb(const PGconn *conn);
 extern char *PQservice(const PGconn *conn);
+extern char *PQserviceFile(const PGconn *conn);
 extern char *PQuser(const PGconn *conn);
 extern char *PQpass(const PGconn *conn);
 extern char *PQhost(const PGconn *conn);
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index a6cfd7f5c9d..5ae4e88f0b7 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -389,6 +389,7 @@ struct pg_conn
 	char	   *dbName;			/* database name */
 	char	   *replication;	/* connect as the replication standby? */
 	char	   *pgservice;		/* Postgres service, if any */
+	char	   *pgservicefile;	/* path to a service file containing service(s) */
 	char	   *pguser;			/* Postgres username and password, if any */
 	char	   *pgpass;
 	char	   *pgpassfile;		/* path to a file containing password(s) */
diff --git a/src/interfaces/libpq/t/006_service.pl b/src/interfaces/libpq/t/006_service.pl
index 4fe5adc5c2a..f4d41fcb309 100644
--- a/src/interfaces/libpq/t/006_service.pl
+++ b/src/interfaces/libpq/t/006_service.pl
@@ -47,6 +47,19 @@ my $srvfile_default = "$td/pg_service.conf";
 # Missing service file.
 my $srvfile_missing = "$td/pg_service_missing.conf";
 
+# Service file with service defined (invalid).
+my $srvfile_service_nested = "$td/pg_service_service_nested.conf";
+copy($srvfile_valid, $srvfile_service_nested) or
+	die "Could not copy $srvfile_valid to $srvfile_service_nested: $!";
+append_to_file($srvfile_service_nested, 'service=tmp_srv' . $newline);
+
+# Service file with servicefile defined (invalid).
+my $srvfile_servicefile_nested = "$td/pg_service_servicefile_nested.conf";
+copy($srvfile_valid, $srvfile_servicefile_nested) or
+	die "Could not copy $srvfile_valid to $srvfile_servicefile_nested: $!";
+append_to_file($srvfile_servicefile_nested, 'servicefile=' . $srvfile_default . $newline);
+
+
 # Set the fallback directory lookup of the service file to the temporary
 # directory of this test.  PGSYSCONFDIR is used if the service file
 # defined in PGSERVICEFILE cannot be found, or when a service file is
@@ -146,6 +159,87 @@ local $ENV{PGSERVICEFILE} = "$srvfile_empty";
 	unlink($srvfile_default);
 }
 
+# Use correct escaped path for Windows.
+my $srvfile_win_cared = $srvfile_valid;
+$srvfile_win_cared =~ s/\\/\\\\/g;
+
+# Check that servicefile option works as expected
+{
+	$dummy_node->connect_ok(
+		q{service=my_srv servicefile='} . $srvfile_win_cared . q{'},
+		'service=my_srv servicefile=...',
+		sql             => "SELECT 'connect3'",
+		expected_stdout => qr/connect3/
+	);
+
+	# Encode slashes and backslash
+	my $encoded_srvfile = $srvfile_valid =~ s{([\\/])}{
+		$1 eq '/' ? '%2F' : '%5C'
+	}ger;
+
+	# Additionally encode a colon in servicefile path of Windows
+	$encoded_srvfile =~ s/:/%3A/g;
+
+	$dummy_node->connect_ok(
+		'postgresql:///?service=my_srv&servicefile=' . $encoded_srvfile,
+		'postgresql:///?service=my_srv&servicefile=...',
+		sql             => "SELECT 'connect4'",
+		expected_stdout => qr/connect4/
+	);
+
+	local $ENV{PGSERVICE} = 'my_srv';
+	$dummy_node->connect_ok(
+		q{servicefile='} . $srvfile_win_cared . q{'},
+		'envvar: PGSERVICE=my_srv + servicefile=...',
+		sql             => "SELECT 'connect5'",
+		expected_stdout => qr/connect5/
+	);
+
+	$dummy_node->connect_ok(
+		'postgresql://?servicefile=' . $encoded_srvfile,
+		'envvar: PGSERVICE=my_srv + postgresql://?servicefile=...',
+		sql             => "SELECT 'connect6'",
+		expected_stdout => qr/connect6/
+	);
+}
+
+# Check that servicefile option takes precedence over PGSERVICEFILE environment variable
+{
+	local $ENV{PGSERVICEFILE} = 'non-existent-file.conf';
+
+	$dummy_node->connect_fails(
+		'service=my_srv',
+		'service=... fails with wrong PGSERVICEFILE',
+		expected_stderr => qr/service file "non-existent-file\.conf" not found/
+	);
+
+	$dummy_node->connect_ok(
+		q{service=my_srv servicefile='} . $srvfile_win_cared . q{'},
+		'servicefile= takes precedence over PGSERVICEFILE',
+		sql             => "SELECT 'connect7'",
+		expected_stdout => qr/connect7/
+	);
+}
+
+# Check that service file which contains nested service and servicefile options fails
+{
+	local $ENV{PGSERVICEFILE} = $srvfile_service_nested;
+
+	$dummy_node->connect_fails(
+		'service=my_srv',
+		'service=... fails with nested service option in service file',
+		expected_stderr => qr/nested "service" specifications not supported in service file/
+	);
+
+	local $ENV{PGSERVICEFILE} = $srvfile_servicefile_nested;
+
+	$dummy_node->connect_fails(
+		'service=my_srv',
+		'servicefile=... fails with nested service option in service file',
+		expected_stderr => qr/nested "servicefile" specifications not supported in service file/
+	);
+}
+
 $node->teardown_node;
 
 done_testing();
-- 
2.45.1.windows.1

v10-0002-psql-enhancement-related-servicefile-option-on-c.patchapplication/octet-stream; name=v10-0002-psql-enhancement-related-servicefile-option-on-c.patchDownload
From da903f066758d4af03fadba5d8096b78476adc51 Mon Sep 17 00:00:00 2001
From: Ryo Kanbayashi <ryo.contact@gmail.com>
Date: Sun, 15 Jun 2025 20:36:58 +0900
Subject: [PATCH v10 2/2] psql enhancement related servicefile option on
 connection string

---
 doc/src/sgml/ref/psql-ref.sgml | 9 +++++++++
 src/bin/psql/command.c         | 2 ++
 2 files changed, 11 insertions(+)

diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 570ef21d1fc..903dfa91b25 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -4623,6 +4623,15 @@ bar
         </listitem>
       </varlistentry>
 
+      <varlistentry id="app-psql-variables-servicefile">
+        <term><varname>SERVICEFILE</varname></term>
+        <listitem>
+        <para>
+        The service file name, if applicable.
+        </para>
+        </listitem>
+      </varlistentry>
+
       <varlistentry id="app-psql-variables-shell-error">
        <term><varname>SHELL_ERROR</varname></term>
        <listitem>
diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index e26c010d044..6f702ec4ca3 100644
--- a/src/bin/psql/command.c
+++ b/src/bin/psql/command.c
@@ -4490,6 +4490,7 @@ SyncVariables(void)
 
 	SetVariable(pset.vars, "DBNAME", PQdb(pset.db));
 	SetVariable(pset.vars, "SERVICE", PQservice(pset.db));
+	SetVariable(pset.vars, "SERVICEFILE", PQserviceFile(pset.db));
 	SetVariable(pset.vars, "USER", PQuser(pset.db));
 	SetVariable(pset.vars, "HOST", PQhost(pset.db));
 	SetVariable(pset.vars, "PORT", PQport(pset.db));
@@ -4524,6 +4525,7 @@ UnsyncVariables(void)
 {
 	SetVariable(pset.vars, "DBNAME", NULL);
 	SetVariable(pset.vars, "SERVICE", NULL);
+	SetVariable(pset.vars, "SERVICEFILE", NULL);
 	SetVariable(pset.vars, "USER", NULL);
 	SetVariable(pset.vars, "HOST", NULL);
 	SetVariable(pset.vars, "PORT", NULL);
-- 
2.45.1.windows.1

#30Michael Paquier
michael@paquier.xyz
In reply to: Ryo Kanbayashi (#29)
Re: [PATCH] PGSERVICEFILE as part of a normal connection string

On Sun, Jun 15, 2025 at 09:02:31PM +0900, Ryo Kanbayashi wrote:

Thanks for review :)

Thanks for the new patch.

While testing the patch, I've bumped into this scenario which feels
incomplete:
- Rely on a default location of the service file, like
$HOME/.pg_service.conf.
- Define a service, with PGSERVICE or a connection parameter.
In this case, :SERVICE shows up some information, not :SERVICEFILE
because it remains empty when building a connection file path if we
don't provide PGSERVICEFILE or servicefile as connection option. It
seems to me that we had better force pg_conn->pgservicefile into a
value in this case, pointing to the value libpq thinks is the default
at the time of resolving the HOME location in pqGetHomeDirectory()?
It seems to me that you should be able to do that at the end of
parseServiceFile(), at least, if we know that the status is a success
(free value if any, assign the new one, and invent an error code path
for the OOM on strdup()).

Defining PGSERVICEFILE or servicefile in a connection string reports
correctly "pgservicefile" in the libpq connection, of course. That's
just for the default location paths.

By the way, could you split the test case for the nested "service"
value in a service file into its own file? This is an existing error
case, and there is no need for the new feature to add this test.
--
Michael

#31Ryo Kanbayashi
kanbayashi.dev@gmail.com
In reply to: Michael Paquier (#30)
3 attachment(s)
Re: [PATCH] PGSERVICEFILE as part of a normal connection string

On Wed, Jun 18, 2025 at 12:23 PM Michael Paquier <michael@paquier.xyz> wrote:

While testing the patch, I've bumped into this scenario which feels
incomplete:
- Rely on a default location of the service file, like
$HOME/.pg_service.conf.
- Define a service, with PGSERVICE or a connection parameter.
In this case, :SERVICE shows up some information, not :SERVICEFILE
because it remains empty when building a connection file path if we
don't provide PGSERVICEFILE or servicefile as connection option. It
seems to me that we had better force pg_conn->pgservicefile into a
value in this case, pointing to the value libpq thinks is the default
at the time of resolving the HOME location in pqGetHomeDirectory()?
It seems to me that you should be able to do that at the end of
parseServiceFile(), at least, if we know that the status is a success
(free value if any, assign the new one, and invent an error code path
for the OOM on strdup()).

Defining PGSERVICEFILE or servicefile in a connection string reports
correctly "pgservicefile" in the libpq connection, of course. That's
just for the default location paths.

OK.
I think I was able to fix it as per your request :)

By the way, could you split the test case for the nested "service"
value in a service file into its own file? This is an existing error
case, and there is no need for the new feature to add this test.

No problem.
I've attached modified and splited patch files to this mail.

---
Great regards,
Ryo Kanbayashi

Attachments:

v11-0001-add-test-for-nested-options-in-servie-file.patchapplication/octet-stream; name=v11-0001-add-test-for-nested-options-in-servie-file.patchDownload
From 34bc97a197b926abf86f80576e9f3bf2bc4cce69 Mon Sep 17 00:00:00 2001
From: Ryo Kanbayashi <ryo.contact@gmail.com>
Date: Fri, 27 Jun 2025 20:46:38 +0900
Subject: [PATCH v11 1/3] add test for nested options in servie file

---
 src/interfaces/libpq/t/006_service.pl | 31 +++++++++++++++++++++++++++
 1 file changed, 31 insertions(+)

diff --git a/src/interfaces/libpq/t/006_service.pl b/src/interfaces/libpq/t/006_service.pl
index 4fe5adc5c2a..65abbf8df89 100644
--- a/src/interfaces/libpq/t/006_service.pl
+++ b/src/interfaces/libpq/t/006_service.pl
@@ -47,6 +47,18 @@ my $srvfile_default = "$td/pg_service.conf";
 # Missing service file.
 my $srvfile_missing = "$td/pg_service_missing.conf";
 
+# Service file with service defined (invalid).
+my $srvfile_service_nested = "$td/pg_service_service_nested.conf";
+copy($srvfile_valid, $srvfile_service_nested) or
+	die "Could not copy $srvfile_valid to $srvfile_service_nested: $!";
+append_to_file($srvfile_service_nested, 'service=tmp_srv' . $newline);
+
+# Service file with servicefile defined (invalid).
+my $srvfile_servicefile_nested = "$td/pg_service_servicefile_nested.conf";
+copy($srvfile_valid, $srvfile_servicefile_nested) or
+	die "Could not copy $srvfile_valid to $srvfile_servicefile_nested: $!";
+append_to_file($srvfile_servicefile_nested, 'servicefile=' . $srvfile_default . $newline);
+
 # Set the fallback directory lookup of the service file to the temporary
 # directory of this test.  PGSYSCONFDIR is used if the service file
 # defined in PGSERVICEFILE cannot be found, or when a service file is
@@ -146,6 +158,25 @@ local $ENV{PGSERVICEFILE} = "$srvfile_empty";
 	unlink($srvfile_default);
 }
 
+# Check that service file which contains nested service and servicefile options fails
+{
+	local $ENV{PGSERVICEFILE} = $srvfile_service_nested;
+
+	$dummy_node->connect_fails(
+		'service=my_srv',
+		'service=... fails with nested service option in service file',
+		expected_stderr => qr/nested "service" specifications not supported in service file/
+	);
+
+	local $ENV{PGSERVICEFILE} = $srvfile_servicefile_nested;
+
+	$dummy_node->connect_fails(
+		'service=my_srv',
+		'servicefile=... fails with nested service option in service file',
+		expected_stderr => qr/nested "servicefile" specifications not supported in service file/
+	);
+}
+
 $node->teardown_node;
 
 done_testing();
-- 
2.34.1

v11-0002-servicefile-option-usage-on-connection-string-fe.patchapplication/octet-stream; name=v11-0002-servicefile-option-usage-on-connection-string-fe.patchDownload
From fe1677b0023f9adc20633cb74474c0ce1e60ff3f Mon Sep 17 00:00:00 2001
From: Ryo Kanbayashi <ryo.contact@gmail.com>
Date: Sun, 15 Jun 2025 20:34:02 +0900
Subject: [PATCH v11 2/3] servicefile option usage on connection string feature
 and its tests.

---
 doc/src/sgml/libpq.sgml               | 34 ++++++++++++++-
 src/interfaces/libpq/exports.txt      |  1 +
 src/interfaces/libpq/fe-connect.c     | 39 +++++++++++++++--
 src/interfaces/libpq/libpq-fe.h       |  1 +
 src/interfaces/libpq/libpq-int.h      |  1 +
 src/interfaces/libpq/t/006_service.pl | 62 +++++++++++++++++++++++++++
 6 files changed, 133 insertions(+), 5 deletions(-)

diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 298c4b38ef9..5e30200f8f9 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -2320,6 +2320,16 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
       </listitem>
      </varlistentry>
 
+     <varlistentry id="libpq-connect-servicefile" xreflabel="servicefile">
+      <term><literal>servicefile</literal></term>
+      <listitem>
+       <para>
+        This option specifies the location of connection service file
+        (see <xref linkend="libpq-pgservice"/>).
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="libpq-connect-target-session-attrs" xreflabel="target_session_attrs">
       <term><literal>target_session_attrs</literal></term>
       <listitem>
@@ -2760,6 +2770,25 @@ char *PQservice(const PGconn *conn);
      </listitem>
     </varlistentry>
 
+    <varlistentry id="libpq-PQserviceFile">
+     <term><function>PQserviceFile</function></term>
+
+     <listitem>
+      <para>
+       Returns the service file name of the active connection.
+<synopsis>
+char *PQserviceFile(const PGconn *conn);
+</synopsis>
+      </para>
+
+      <para>
+       <xref linkend="libpq-PQserviceFile"/> returns <symbol>NULL</symbol> if the
+       <parameter>conn</parameter> argument is <symbol>NULL</symbol>.
+       Otherwise, if there was no service file provided, it returns an empty string.
+      </para>
+     </listitem>
+    </varlistentry>
+
     <varlistentry id="libpq-PQtty">
      <term><function>PQtty</function><indexterm><primary>PQtty</primary></indexterm></term>
 
@@ -9166,6 +9195,8 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough)
       Defaults to <filename>~/.pg_service.conf</filename>, or
       <filename>%APPDATA%\postgresql\.pg_service.conf</filename> on
       Microsoft Windows.
+      <envar>This environment variable</envar> behaves the same as the <xref
+      linkend="libpq-connect-servicefile"/> connection parameter.
      </para>
     </listitem>
 
@@ -9596,7 +9627,8 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough)
    On Microsoft Windows, it is named
    <filename>%APPDATA%\postgresql\.pg_service.conf</filename> (where
    <filename>%APPDATA%</filename> refers to the Application Data subdirectory
-   in the user's profile).  A different file name can be specified by
+   in the user's profile).  A different file name can be specified using the
+   <literal>servicefile</literal> key word in a libpq connection string or by
    setting the environment variable <envar>PGSERVICEFILE</envar>.
    The system-wide file is named <filename>pg_service.conf</filename>.
    By default it is sought in the <filename>etc</filename> directory
diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt
index 0625cf39e9a..366f6553858 100644
--- a/src/interfaces/libpq/exports.txt
+++ b/src/interfaces/libpq/exports.txt
@@ -211,3 +211,4 @@ PQgetAuthDataHook         208
 PQdefaultAuthDataHook     209
 PQfullProtocolVersion     210
 appendPQExpBufferVA       211
+PQserviceFile             212
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 51a9c416584..b94c6ce94bc 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -201,6 +201,10 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
 		"Database-Service", "", 20,
 	offsetof(struct pg_conn, pgservice)},
 
+	{"servicefile", "PGSERVICEFILE", NULL, NULL,
+	"Database-Service-File", "", 64,
+	offsetof(struct pg_conn, pgservicefile)},
+
 	{"user", "PGUSER", NULL, NULL,
 		"Database-User", "", 20,
 	offsetof(struct pg_conn, pguser)},
@@ -5062,6 +5066,7 @@ freePGconn(PGconn *conn)
 	free(conn->dbName);
 	free(conn->replication);
 	free(conn->pgservice);
+	free(conn->pgservicefile);
 	free(conn->pguser);
 	if (conn->pgpass)
 	{
@@ -5914,6 +5919,7 @@ static int
 parseServiceInfo(PQconninfoOption *options, PQExpBuffer errorMessage)
 {
 	const char *service = conninfo_getval(options, "service");
+	const char *service_fname = conninfo_getval(options, "servicefile");
 	char		serviceFile[MAXPGPATH];
 	char	   *env;
 	bool		group_found = false;
@@ -5933,11 +5939,18 @@ parseServiceInfo(PQconninfoOption *options, PQExpBuffer errorMessage)
 		return 0;
 
 	/*
-	 * Try PGSERVICEFILE if specified, else try ~/.pg_service.conf (if that
-	 * exists).
+	 * First, check servicefile option on connection string. Second, check
+	 * PGSERVICEFILE environment variable. Finally, check ~/.pg_service.conf
+	 * (if that exists).
 	 */
-	if ((env = getenv("PGSERVICEFILE")) != NULL)
+	if (service_fname != NULL)
+	{
+		strlcpy(serviceFile, service_fname, sizeof(serviceFile));
+	}
+	else if ((env = getenv("PGSERVICEFILE")) != NULL)
+	{
 		strlcpy(serviceFile, env, sizeof(serviceFile));
+	}
 	else
 	{
 		char		homedir[MAXPGPATH];
@@ -6092,7 +6105,17 @@ parseServiceFile(const char *serviceFile,
 				if (strcmp(key, "service") == 0)
 				{
 					libpq_append_error(errorMessage,
-									   "nested service specifications not supported in service file \"%s\", line %d",
+									   "nested \"service\" specifications not supported in service file \"%s\", line %d",
+									   serviceFile,
+									   linenr);
+					result = 3;
+					goto exit;
+				}
+
+				if (strcmp(key, "servicefile") == 0)
+				{
+					libpq_append_error(errorMessage,
+									   "nested \"servicefile\" specifications not supported in service file \"%s\", line %d",
 									   serviceFile,
 									   linenr);
 					result = 3;
@@ -7469,6 +7492,14 @@ PQservice(const PGconn *conn)
 	return conn->pgservice;
 }
 
+char *
+PQserviceFile(const PGconn *conn)
+{
+	if (!conn)
+		return NULL;
+	return conn->pgservicefile;
+}
+
 char *
 PQuser(const PGconn *conn)
 {
diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h
index 7d3a9df6fd5..c7c443531c2 100644
--- a/src/interfaces/libpq/libpq-fe.h
+++ b/src/interfaces/libpq/libpq-fe.h
@@ -401,6 +401,7 @@ extern int	PQrequestCancel(PGconn *conn);
 /* Accessor functions for PGconn objects */
 extern char *PQdb(const PGconn *conn);
 extern char *PQservice(const PGconn *conn);
+extern char *PQserviceFile(const PGconn *conn);
 extern char *PQuser(const PGconn *conn);
 extern char *PQpass(const PGconn *conn);
 extern char *PQhost(const PGconn *conn);
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index a6cfd7f5c9d..5ae4e88f0b7 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -389,6 +389,7 @@ struct pg_conn
 	char	   *dbName;			/* database name */
 	char	   *replication;	/* connect as the replication standby? */
 	char	   *pgservice;		/* Postgres service, if any */
+	char	   *pgservicefile;	/* path to a service file containing service(s) */
 	char	   *pguser;			/* Postgres username and password, if any */
 	char	   *pgpass;
 	char	   *pgpassfile;		/* path to a file containing password(s) */
diff --git a/src/interfaces/libpq/t/006_service.pl b/src/interfaces/libpq/t/006_service.pl
index 65abbf8df89..7aad9ad904d 100644
--- a/src/interfaces/libpq/t/006_service.pl
+++ b/src/interfaces/libpq/t/006_service.pl
@@ -158,6 +158,68 @@ local $ENV{PGSERVICEFILE} = "$srvfile_empty";
 	unlink($srvfile_default);
 }
 
+# Use correct escaped path for Windows.
+my $srvfile_win_cared = $srvfile_valid;
+$srvfile_win_cared =~ s/\\/\\\\/g;
+
+# Check that servicefile option works as expected
+{
+	$dummy_node->connect_ok(
+		q{service=my_srv servicefile='} . $srvfile_win_cared . q{'},
+		'service=my_srv servicefile=...',
+		sql             => "SELECT 'connect3'",
+		expected_stdout => qr/connect3/
+	);
+
+	# Encode slashes and backslash
+	my $encoded_srvfile = $srvfile_valid =~ s{([\\/])}{
+		$1 eq '/' ? '%2F' : '%5C'
+	}ger;
+
+	# Additionally encode a colon in servicefile path of Windows
+	$encoded_srvfile =~ s/:/%3A/g;
+
+	$dummy_node->connect_ok(
+		'postgresql:///?service=my_srv&servicefile=' . $encoded_srvfile,
+		'postgresql:///?service=my_srv&servicefile=...',
+		sql             => "SELECT 'connect4'",
+		expected_stdout => qr/connect4/
+	);
+
+	local $ENV{PGSERVICE} = 'my_srv';
+	$dummy_node->connect_ok(
+		q{servicefile='} . $srvfile_win_cared . q{'},
+		'envvar: PGSERVICE=my_srv + servicefile=...',
+		sql             => "SELECT 'connect5'",
+		expected_stdout => qr/connect5/
+	);
+
+	$dummy_node->connect_ok(
+		'postgresql://?servicefile=' . $encoded_srvfile,
+		'envvar: PGSERVICE=my_srv + postgresql://?servicefile=...',
+		sql             => "SELECT 'connect6'",
+		expected_stdout => qr/connect6/
+	);
+}
+
+# Check that servicefile option takes precedence over PGSERVICEFILE environment variable
+{
+	local $ENV{PGSERVICEFILE} = 'non-existent-file.conf';
+
+	$dummy_node->connect_fails(
+		'service=my_srv',
+		'service=... fails with wrong PGSERVICEFILE',
+		expected_stderr => qr/service file "non-existent-file\.conf" not found/
+	);
+
+	$dummy_node->connect_ok(
+		q{service=my_srv servicefile='} . $srvfile_win_cared . q{'},
+		'servicefile= takes precedence over PGSERVICEFILE',
+		sql             => "SELECT 'connect7'",
+		expected_stdout => qr/connect7/
+	);
+}
+
 # Check that service file which contains nested service and servicefile options fails
 {
 	local $ENV{PGSERVICEFILE} = $srvfile_service_nested;
-- 
2.34.1

v11-0003-psql-enhancement-related-servicefile-option-on-c.patchapplication/octet-stream; name=v11-0003-psql-enhancement-related-servicefile-option-on-c.patchDownload
From 6b14ef841bf0e15e0fffbf1381bae186e1913258 Mon Sep 17 00:00:00 2001
From: Ryo Kanbayashi <ryo.contact@gmail.com>
Date: Fri, 27 Jun 2025 21:16:02 +0900
Subject: [PATCH v11 3/3] psql enhancement related servicefile option on
 connection string

---
 doc/src/sgml/ref/psql-ref.sgml    |  9 +++++++++
 src/bin/psql/command.c            |  2 ++
 src/interfaces/libpq/fe-connect.c | 22 ++++++++++++++++++++++
 3 files changed, 33 insertions(+)

diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 95f4cac2467..4f7b11175c6 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -4623,6 +4623,15 @@ bar
         </listitem>
       </varlistentry>
 
+      <varlistentry id="app-psql-variables-servicefile">
+        <term><varname>SERVICEFILE</varname></term>
+        <listitem>
+        <para>
+        The service file name, if applicable.
+        </para>
+        </listitem>
+      </varlistentry>
+
       <varlistentry id="app-psql-variables-shell-error">
        <term><varname>SHELL_ERROR</varname></term>
        <listitem>
diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index 9fcd2db8326..b65c5633e5a 100644
--- a/src/bin/psql/command.c
+++ b/src/bin/psql/command.c
@@ -4490,6 +4490,7 @@ SyncVariables(void)
 
 	SetVariable(pset.vars, "DBNAME", PQdb(pset.db));
 	SetVariable(pset.vars, "SERVICE", PQservice(pset.db));
+	SetVariable(pset.vars, "SERVICEFILE", PQserviceFile(pset.db));
 	SetVariable(pset.vars, "USER", PQuser(pset.db));
 	SetVariable(pset.vars, "HOST", PQhost(pset.db));
 	SetVariable(pset.vars, "PORT", PQport(pset.db));
@@ -4524,6 +4525,7 @@ UnsyncVariables(void)
 {
 	SetVariable(pset.vars, "DBNAME", NULL);
 	SetVariable(pset.vars, "SERVICE", NULL);
+	SetVariable(pset.vars, "SERVICEFILE", NULL);
 	SetVariable(pset.vars, "USER", NULL);
 	SetVariable(pset.vars, "HOST", NULL);
 	SetVariable(pset.vars, "PORT", NULL);
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index b94c6ce94bc..7ea3c673696 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -6158,6 +6158,28 @@ parseServiceFile(const char *serviceFile,
 	}
 
 exit:
+
+	/* If service was found successfully, set servicefile option if not already set */
+	if (*group_found && result == 0)
+	{
+		for (i = 0; options[i].keyword; i++)
+		{
+			if (strcmp(options[i].keyword, "servicefile") == 0)
+			{
+				if (options[i].val == NULL)
+				{
+					options[i].val = strdup(serviceFile);
+					if (!options[i].val)
+					{
+						libpq_append_error(errorMessage, "out of memory");
+						return 3;
+					}
+				}
+				break;
+			}
+		}
+	}
+
 	fclose(f);
 
 	return result;
-- 
2.34.1

#32Michael Paquier
michael@paquier.xyz
In reply to: Ryo Kanbayashi (#31)
2 attachment(s)
Re: [PATCH] PGSERVICEFILE as part of a normal connection string

On Fri, Jun 27, 2025 at 09:25:47PM +0900, Ryo Kanbayashi wrote:

I've attached modified and splited patch files to this mail.

Taken in isolation, 0001 was incorrect because it still contained the
case of "servicefile" nested to a service file, but this code path is
only introduced in 0002. I have extracted the relevant part of the
patch that works on HEAD, and applied it.

Attached is a rebased version of the rest, with the recent stanza
related to fef6da9e9c87 taken into account. 0002 still has a change
that should be in 0001: I have not really touched the structure of the
two remaining patches yet.
--
Michael

Attachments:

v12-0001-servicefile-option-usage-on-connection-string-fe.patchtext/x-diff; charset=us-asciiDownload
From 19d59646e1842ffd73aa0d79fc80c89370878f60 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Wed, 9 Jul 2025 16:17:36 +0900
Subject: [PATCH v12 1/2] servicefile option usage on connection string feature
 and its tests.

---
 src/interfaces/libpq/fe-connect.c     | 31 ++++++++--
 src/interfaces/libpq/libpq-int.h      |  1 +
 src/interfaces/libpq/t/006_service.pl | 81 ++++++++++++++++++++++++++-
 doc/src/sgml/libpq.sgml               | 15 ++++-
 4 files changed, 121 insertions(+), 7 deletions(-)

diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 09eb79812ac6..f9d626d9991c 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -201,6 +201,10 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
 		"Database-Service", "", 20,
 	offsetof(struct pg_conn, pgservice)},
 
+	{"servicefile", "PGSERVICEFILE", NULL, NULL,
+	"Database-Service-File", "", 64,
+	offsetof(struct pg_conn, pgservicefile)},
+
 	{"user", "PGUSER", NULL, NULL,
 		"Database-User", "", 20,
 	offsetof(struct pg_conn, pguser)},
@@ -5062,6 +5066,7 @@ freePGconn(PGconn *conn)
 	free(conn->dbName);
 	free(conn->replication);
 	free(conn->pgservice);
+	free(conn->pgservicefile);
 	free(conn->pguser);
 	if (conn->pgpass)
 	{
@@ -5914,6 +5919,7 @@ static int
 parseServiceInfo(PQconninfoOption *options, PQExpBuffer errorMessage)
 {
 	const char *service = conninfo_getval(options, "service");
+	const char *service_fname = conninfo_getval(options, "servicefile");
 	char		serviceFile[MAXPGPATH];
 	char	   *env;
 	bool		group_found = false;
@@ -5933,11 +5939,18 @@ parseServiceInfo(PQconninfoOption *options, PQExpBuffer errorMessage)
 		return 0;
 
 	/*
-	 * Try PGSERVICEFILE if specified, else try ~/.pg_service.conf (if that
-	 * exists).
+	 * First, check servicefile option on connection string. Second, check
+	 * PGSERVICEFILE environment variable. Finally, check ~/.pg_service.conf
+	 * (if that exists).
 	 */
-	if ((env = getenv("PGSERVICEFILE")) != NULL)
+	if (service_fname != NULL)
+	{
+		strlcpy(serviceFile, service_fname, sizeof(serviceFile));
+	}
+	else if ((env = getenv("PGSERVICEFILE")) != NULL)
+	{
 		strlcpy(serviceFile, env, sizeof(serviceFile));
+	}
 	else
 	{
 		char		homedir[MAXPGPATH];
@@ -6092,7 +6105,17 @@ parseServiceFile(const char *serviceFile,
 				if (strcmp(key, "service") == 0)
 				{
 					libpq_append_error(errorMessage,
-									   "nested service specifications not supported in service file \"%s\", line %d",
+									   "nested \"service\" specifications not supported in service file \"%s\", line %d",
+									   serviceFile,
+									   linenr);
+					result = 3;
+					goto exit;
+				}
+
+				if (strcmp(key, "servicefile") == 0)
+				{
+					libpq_append_error(errorMessage,
+									   "nested \"servicefile\" specifications not supported in service file \"%s\", line %d",
 									   serviceFile,
 									   linenr);
 					result = 3;
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index a6cfd7f5c9d8..5ae4e88f0b75 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -389,6 +389,7 @@ struct pg_conn
 	char	   *dbName;			/* database name */
 	char	   *replication;	/* connect as the replication standby? */
 	char	   *pgservice;		/* Postgres service, if any */
+	char	   *pgservicefile;	/* path to a service file containing service(s) */
 	char	   *pguser;			/* Postgres username and password, if any */
 	char	   *pgpass;
 	char	   *pgpassfile;		/* path to a file containing password(s) */
diff --git a/src/interfaces/libpq/t/006_service.pl b/src/interfaces/libpq/t/006_service.pl
index d896558a6cc2..d10cc206c468 100644
--- a/src/interfaces/libpq/t/006_service.pl
+++ b/src/interfaces/libpq/t/006_service.pl
@@ -53,6 +53,12 @@ copy($srvfile_valid, $srvfile_nested)
   or die "Could not copy $srvfile_valid to $srvfile_nested: $!";
 append_to_file($srvfile_nested, 'service=invalid_srv' . $newline);
 
+# Service file with nested "servicefile" defined.
+my $srvfile_nested_2 = "$td/pg_service_nested_2.conf";
+copy($srvfile_valid, $srvfile_nested_2) or
+   die "Could not copy $srvfile_valid to $srvfile_nested_2: $!";
+append_to_file($srvfile_nested_2, 'servicefile=' . $srvfile_default . $newline);
+
 # Set the fallback directory lookup of the service file to the temporary
 # directory of this test.  PGSYSCONFDIR is used if the service file
 # defined in PGSERVICEFILE cannot be found, or when a service file is
@@ -158,9 +164,80 @@ local $ENV{PGSERVICEFILE} = "$srvfile_empty";
 
 	$dummy_node->connect_fails(
 		'service=my_srv',
-		'connection with nested service file',
+		'connection with "service" in nested service file',
 		expected_stderr =>
-		  qr/nested service specifications not supported in service file/);
+		  qr/nested "service" specifications not supported in service file/);
+
+	local $ENV{PGSERVICEFILE} = $srvfile_nested_2;
+
+	$dummy_node->connect_fails(
+		'service=my_srv',
+		'connection with "servicefile" in nested service file',
+		expected_stderr => qr/nested "servicefile" specifications not supported in service file/
+	);
+}
+
+# Use correct escaped path for Windows.
+my $srvfile_win_cared = $srvfile_valid;
+$srvfile_win_cared =~ s/\\/\\\\/g;
+
+# Check that "servicefile" option works as expected
+{
+	$dummy_node->connect_ok(
+		q{service=my_srv servicefile='} . $srvfile_win_cared . q{'},
+		'service=my_srv servicefile=...',
+		sql             => "SELECT 'connect3_1'",
+		expected_stdout => qr/connect3_1/
+	);
+
+	# Encode slashes and backslash
+	my $encoded_srvfile = $srvfile_valid =~ s{([\\/])}{
+		$1 eq '/' ? '%2F' : '%5C'
+	}ger;
+
+	# Additionally encode a colon in servicefile path of Windows
+	$encoded_srvfile =~ s/:/%3A/g;
+
+	$dummy_node->connect_ok(
+		'postgresql:///?service=my_srv&servicefile=' . $encoded_srvfile,
+		'postgresql:///?service=my_srv&servicefile=...',
+		sql             => "SELECT 'connect3_2'",
+		expected_stdout => qr/connect3_2/
+	);
+
+	local $ENV{PGSERVICE} = 'my_srv';
+	$dummy_node->connect_ok(
+		q{servicefile='} . $srvfile_win_cared . q{'},
+		'envvar: PGSERVICE=my_srv + servicefile=...',
+		sql             => "SELECT 'connect3_3'",
+		expected_stdout => qr/connect3_3/
+	);
+
+	$dummy_node->connect_ok(
+		'postgresql://?servicefile=' . $encoded_srvfile,
+		'envvar: PGSERVICE=my_srv + postgresql://?servicefile=...',
+		sql             => "SELECT 'connect3_4'",
+		expected_stdout => qr/connect3_4/
+	);
+}
+
+# Check that "servicefile" option takes precedence over PGSERVICEFILE
+# environment variable
+{
+	local $ENV{PGSERVICEFILE} = 'non-existent-file.conf';
+
+	$dummy_node->connect_fails(
+		'service=my_srv',
+		'service=... fails with wrong PGSERVICEFILE',
+		expected_stderr => qr/service file "non-existent-file\.conf" not found/
+	);
+
+	$dummy_node->connect_ok(
+		q{service=my_srv servicefile='} . $srvfile_win_cared . q{'},
+		'servicefile= takes precedence over PGSERVICEFILE',
+		sql             => "SELECT 'connect4_1'",
+		expected_stdout => qr/connect4_1/
+	);
 }
 
 $node->teardown_node;
diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index b2c2cf9eac83..c4c4bb35ac29 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -2320,6 +2320,16 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
       </listitem>
      </varlistentry>
 
+     <varlistentry id="libpq-connect-servicefile" xreflabel="servicefile">
+      <term><literal>servicefile</literal></term>
+      <listitem>
+       <para>
+        This option specifies the location of connection service file
+        (see <xref linkend="libpq-pgservice"/>).
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="libpq-connect-target-session-attrs" xreflabel="target_session_attrs">
       <term><literal>target_session_attrs</literal></term>
       <listitem>
@@ -9146,6 +9156,8 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough)
       Defaults to <filename>~/.pg_service.conf</filename>, or
       <filename>%APPDATA%\postgresql\.pg_service.conf</filename> on
       Microsoft Windows.
+      <envar>This environment variable</envar> behaves the same as the <xref
+      linkend="libpq-connect-servicefile"/> connection parameter.
      </para>
     </listitem>
 
@@ -9576,7 +9588,8 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough)
    On Microsoft Windows, it is named
    <filename>%APPDATA%\postgresql\.pg_service.conf</filename> (where
    <filename>%APPDATA%</filename> refers to the Application Data subdirectory
-   in the user's profile).  A different file name can be specified by
+   in the user's profile).  A different file name can be specified using the
+   <literal>servicefile</literal> key word in a libpq connection string or by
    setting the environment variable <envar>PGSERVICEFILE</envar>.
    The system-wide file is named <filename>pg_service.conf</filename>.
    By default it is sought in the <filename>etc</filename> directory
-- 
2.50.0

v12-0002-psql-enhancement-related-servicefile-option-on-c.patchtext/x-diff; charset=us-asciiDownload
From ba1651054d8d1f1a05664f3fe7b29354f861e0ee Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Wed, 9 Jul 2025 16:27:56 +0900
Subject: [PATCH v12 2/2] psql enhancement related servicefile option on
 connection string

---
 src/bin/psql/command.c            |  7 +++++++
 src/interfaces/libpq/fe-connect.c | 22 ++++++++++++++++++++++
 doc/src/sgml/ref/psql-ref.sgml    |  9 +++++++++
 3 files changed, 38 insertions(+)

diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index 0a55901b14e1..0e00d73487c3 100644
--- a/src/bin/psql/command.c
+++ b/src/bin/psql/command.c
@@ -4481,6 +4481,7 @@ SyncVariables(void)
 	char		vbuf[32];
 	const char *server_version;
 	char	   *service_name;
+	char	   *service_file;
 
 	/* get stuff from connection */
 	pset.encoding = PQclientEncoding(pset.db);
@@ -4500,6 +4501,11 @@ SyncVariables(void)
 	if (service_name)
 		pg_free(service_name);
 
+	service_file = get_conninfo_value("servicefile");
+	SetVariable(pset.vars, "SERVICEFILE", service_file);
+	if (service_file)
+		pg_free(service_file);
+
 	/* this bit should match connection_warnings(): */
 	/* Try to get full text form of version, might include "devel" etc */
 	server_version = PQparameterStatus(pset.db, "server_version");
@@ -4529,6 +4535,7 @@ UnsyncVariables(void)
 {
 	SetVariable(pset.vars, "DBNAME", NULL);
 	SetVariable(pset.vars, "SERVICE", NULL);
+	SetVariable(pset.vars, "SERVICEFILE", NULL);
 	SetVariable(pset.vars, "USER", NULL);
 	SetVariable(pset.vars, "HOST", NULL);
 	SetVariable(pset.vars, "PORT", NULL);
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index f9d626d9991c..17262fcf939a 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -6158,6 +6158,28 @@ parseServiceFile(const char *serviceFile,
 	}
 
 exit:
+
+	/* If service was found successfully, set servicefile option if not already set */
+	if (*group_found && result == 0)
+	{
+		for (i = 0; options[i].keyword; i++)
+		{
+			if (strcmp(options[i].keyword, "servicefile") != 0)
+				continue;
+
+			if (options[i].val != NULL)
+				break;
+
+			options[i].val = strdup(serviceFile);
+			if (options[i].val == NULL)
+			{
+				libpq_append_error(errorMessage, "out of memory");
+				return 3;
+			}
+			break;
+		}
+	}
+
 	fclose(f);
 
 	return result;
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 95f4cac2467e..4f7b11175c67 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -4623,6 +4623,15 @@ bar
         </listitem>
       </varlistentry>
 
+      <varlistentry id="app-psql-variables-servicefile">
+        <term><varname>SERVICEFILE</varname></term>
+        <listitem>
+        <para>
+        The service file name, if applicable.
+        </para>
+        </listitem>
+      </varlistentry>
+
       <varlistentry id="app-psql-variables-shell-error">
        <term><varname>SHELL_ERROR</varname></term>
        <listitem>
-- 
2.50.0

#33Michael Paquier
michael@paquier.xyz
In reply to: Michael Paquier (#32)
2 attachment(s)
Re: [PATCH] PGSERVICEFILE as part of a normal connection string

On Wed, Jul 09, 2025 at 04:31:31PM +0900, Michael Paquier wrote:

Attached is a rebased version of the rest, with the recent stanza
related to fef6da9e9c87 taken into account. 0002 still has a change
that should be in 0001: I have not really touched the structure of the
two remaining patches yet.

+    /* If service was found successfully, set servicefile option if not already set */
+    if (*group_found && result == 0)
+    {
+        for (i = 0; options[i].keyword; i++)
+        {
+            if (strcmp(options[i].keyword, "servicefile") != 0)
+                continue;
+
+            if (options[i].val != NULL)
+                break;
+
+            options[i].val = strdup(serviceFile);
+            if (options[i].val == NULL)
+            {
+                libpq_append_error(errorMessage, "out of memory");
+                return 3;
+            }
+            break;

There was a bug here: if the new value cannot be strdup'd, we would
miss the fclose() of the exit path, so this cannot return directly.
It is possible to set the status to a new value instead, then break.

After that, I have applied a few cosmetic tweaks here and there, and
attached is what I have staged for commit, minus proper commit
messages. The new TAP tests have some WIN32-specific things, and I
won't be able to look at the buildfarm if I were to apply things
today, so this will have to wait until the beginning of next week.
The CI is happy with it, so at least we are one checkbox down.
--
Michael

Attachments:

v13-0001-libpq-Add-servicefile-connection-option.patchtext/x-diff; charset=us-asciiDownload
From 6f916278ca1d637851485820a8edbc79ff469656 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Thu, 10 Jul 2025 14:12:53 +0900
Subject: [PATCH v13 1/2] libpq: Add "servicefile" connection option

---
 src/interfaces/libpq/fe-connect.c     | 55 +++++++++++++++++--
 src/interfaces/libpq/libpq-int.h      |  2 +
 src/interfaces/libpq/t/006_service.pl | 79 ++++++++++++++++++++++++++-
 doc/src/sgml/libpq.sgml               | 15 ++++-
 4 files changed, 144 insertions(+), 7 deletions(-)

diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 09eb79812ac6..cd2e99a3213e 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -201,6 +201,10 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
 		"Database-Service", "", 20,
 	offsetof(struct pg_conn, pgservice)},
 
+	{"servicefile", "PGSERVICEFILE", NULL, NULL,
+		"Database-Service-File", "", 64,
+	offsetof(struct pg_conn, pgservicefile)},
+
 	{"user", "PGUSER", NULL, NULL,
 		"Database-User", "", 20,
 	offsetof(struct pg_conn, pguser)},
@@ -5062,6 +5066,7 @@ freePGconn(PGconn *conn)
 	free(conn->dbName);
 	free(conn->replication);
 	free(conn->pgservice);
+	free(conn->pgservicefile);
 	free(conn->pguser);
 	if (conn->pgpass)
 	{
@@ -5914,6 +5919,7 @@ static int
 parseServiceInfo(PQconninfoOption *options, PQExpBuffer errorMessage)
 {
 	const char *service = conninfo_getval(options, "service");
+	const char *service_fname = conninfo_getval(options, "servicefile");
 	char		serviceFile[MAXPGPATH];
 	char	   *env;
 	bool		group_found = false;
@@ -5933,10 +5939,13 @@ parseServiceInfo(PQconninfoOption *options, PQExpBuffer errorMessage)
 		return 0;
 
 	/*
-	 * Try PGSERVICEFILE if specified, else try ~/.pg_service.conf (if that
-	 * exists).
+	 * First, try the "servicefile" option in connection string.  Then, try
+	 * the PGSERVICEFILE environment variable.  Finally, check
+	 * ~/.pg_service.conf (if that exists).
 	 */
-	if ((env = getenv("PGSERVICEFILE")) != NULL)
+	if (service_fname != NULL)
+		strlcpy(serviceFile, service_fname, sizeof(serviceFile));
+	else if ((env = getenv("PGSERVICEFILE")) != NULL)
 		strlcpy(serviceFile, env, sizeof(serviceFile));
 	else
 	{
@@ -6092,7 +6101,17 @@ parseServiceFile(const char *serviceFile,
 				if (strcmp(key, "service") == 0)
 				{
 					libpq_append_error(errorMessage,
-									   "nested service specifications not supported in service file \"%s\", line %d",
+									   "nested \"service\" specifications not supported in service file \"%s\", line %d",
+									   serviceFile,
+									   linenr);
+					result = 3;
+					goto exit;
+				}
+
+				if (strcmp(key, "servicefile") == 0)
+				{
+					libpq_append_error(errorMessage,
+									   "nested \"servicefile\" specifications not supported in service file \"%s\", line %d",
 									   serviceFile,
 									   linenr);
 					result = 3;
@@ -6135,6 +6154,34 @@ parseServiceFile(const char *serviceFile,
 	}
 
 exit:
+
+	/*
+	 * If a service has been successfully found, set the "servicefile" option
+	 * if not already set.  This matters for the cases where we use a default
+	 * service file or a service file set with PGSERVICEFILE, where we want to
+	 * be able track the value used.
+	 */
+	if (*group_found && result == 0)
+	{
+		for (i = 0; options[i].keyword; i++)
+		{
+			if (strcmp(options[i].keyword, "servicefile") != 0)
+				continue;
+
+			/* If value is already set, nothing to do */
+			if (options[i].val != NULL)
+				break;
+
+			options[i].val = strdup(serviceFile);
+			if (options[i].val == NULL)
+			{
+				libpq_append_error(errorMessage, "out of memory");
+				result = 3;
+			}
+			break;
+		}
+	}
+
 	fclose(f);
 
 	return result;
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index a6cfd7f5c9d8..70c28f2ffca0 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -389,6 +389,8 @@ struct pg_conn
 	char	   *dbName;			/* database name */
 	char	   *replication;	/* connect as the replication standby? */
 	char	   *pgservice;		/* Postgres service, if any */
+	char	   *pgservicefile;	/* path to a service file containing
+								 * service(s) */
 	char	   *pguser;			/* Postgres username and password, if any */
 	char	   *pgpass;
 	char	   *pgpassfile;		/* path to a file containing password(s) */
diff --git a/src/interfaces/libpq/t/006_service.pl b/src/interfaces/libpq/t/006_service.pl
index d896558a6cc2..797e6232b8fc 100644
--- a/src/interfaces/libpq/t/006_service.pl
+++ b/src/interfaces/libpq/t/006_service.pl
@@ -53,6 +53,13 @@ copy($srvfile_valid, $srvfile_nested)
   or die "Could not copy $srvfile_valid to $srvfile_nested: $!";
 append_to_file($srvfile_nested, 'service=invalid_srv' . $newline);
 
+# Service file with nested "servicefile" defined.
+my $srvfile_nested_2 = "$td/pg_service_nested_2.conf";
+copy($srvfile_valid, $srvfile_nested_2)
+  or die "Could not copy $srvfile_valid to $srvfile_nested_2: $!";
+append_to_file($srvfile_nested_2,
+	'servicefile=' . $srvfile_default . $newline);
+
 # Set the fallback directory lookup of the service file to the temporary
 # directory of this test.  PGSYSCONFDIR is used if the service file
 # defined in PGSERVICEFILE cannot be found, or when a service file is
@@ -158,9 +165,77 @@ local $ENV{PGSERVICEFILE} = "$srvfile_empty";
 
 	$dummy_node->connect_fails(
 		'service=my_srv',
-		'connection with nested service file',
+		'connection with "service" in nested service file',
 		expected_stderr =>
-		  qr/nested service specifications not supported in service file/);
+		  qr/nested "service" specifications not supported in service file/);
+
+	local $ENV{PGSERVICEFILE} = $srvfile_nested_2;
+
+	$dummy_node->connect_fails(
+		'service=my_srv',
+		'connection with "servicefile" in nested service file',
+		expected_stderr =>
+		  qr/nested "servicefile" specifications not supported in service file/
+	);
+}
+
+# Properly escape backslashes in the path, to ensure the generation of
+# correct connection strings.
+my $srvfile_win_cared = $srvfile_valid;
+$srvfile_win_cared =~ s/\\/\\\\/g;
+
+# Checks that the "servicefile" option works as expected
+{
+	$dummy_node->connect_ok(
+		q{service=my_srv servicefile='} . $srvfile_win_cared . q{'},
+		'connection with valid servicefile in connection string',
+		sql => "SELECT 'connect3_1'",
+		expected_stdout => qr/connect3_1/);
+
+	# Encode slashes and backslash
+	my $encoded_srvfile = $srvfile_valid =~ s{([\\/])}{
+		$1 eq '/' ? '%2F' : '%5C'
+	}ger;
+
+	# Additionally encode a colon in servicefile path of Windows
+	$encoded_srvfile =~ s/:/%3A/g;
+
+	$dummy_node->connect_ok(
+		'postgresql:///?service=my_srv&servicefile=' . $encoded_srvfile,
+		'connection with valid servicefile in URI',
+		sql => "SELECT 'connect3_2'",
+		expected_stdout => qr/connect3_2/);
+
+	local $ENV{PGSERVICE} = 'my_srv';
+	$dummy_node->connect_ok(
+		q{servicefile='} . $srvfile_win_cared . q{'},
+		'connection with PGSERVICE and servicefile in connection string',
+		sql => "SELECT 'connect3_3'",
+		expected_stdout => qr/connect3_3/);
+
+	$dummy_node->connect_ok(
+		'postgresql://?servicefile=' . $encoded_srvfile,
+		'connection with PGSERVICE and servicefile in URI',
+		sql => "SELECT 'connect3_4'",
+		expected_stdout => qr/connect3_4/);
+}
+
+# Check that the "servicefile" option takes priority over the PGSERVICEFILE
+# environment variable.
+{
+	local $ENV{PGSERVICEFILE} = 'non-existent-file.conf';
+
+	$dummy_node->connect_fails(
+		'service=my_srv',
+		'connection with invalid PGSERVICEFILE',
+		expected_stderr =>
+		  qr/service file "non-existent-file\.conf" not found/);
+
+	$dummy_node->connect_ok(
+		q{service=my_srv servicefile='} . $srvfile_win_cared . q{'},
+		'connection with both servicefile and PGSERVICEFILE',
+		sql => "SELECT 'connect4_1'",
+		expected_stdout => qr/connect4_1/);
 }
 
 $node->teardown_node;
diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index b2c2cf9eac83..c4c4bb35ac29 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -2320,6 +2320,16 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
       </listitem>
      </varlistentry>
 
+     <varlistentry id="libpq-connect-servicefile" xreflabel="servicefile">
+      <term><literal>servicefile</literal></term>
+      <listitem>
+       <para>
+        This option specifies the location of connection service file
+        (see <xref linkend="libpq-pgservice"/>).
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="libpq-connect-target-session-attrs" xreflabel="target_session_attrs">
       <term><literal>target_session_attrs</literal></term>
       <listitem>
@@ -9146,6 +9156,8 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough)
       Defaults to <filename>~/.pg_service.conf</filename>, or
       <filename>%APPDATA%\postgresql\.pg_service.conf</filename> on
       Microsoft Windows.
+      <envar>This environment variable</envar> behaves the same as the <xref
+      linkend="libpq-connect-servicefile"/> connection parameter.
      </para>
     </listitem>
 
@@ -9576,7 +9588,8 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough)
    On Microsoft Windows, it is named
    <filename>%APPDATA%\postgresql\.pg_service.conf</filename> (where
    <filename>%APPDATA%</filename> refers to the Application Data subdirectory
-   in the user's profile).  A different file name can be specified by
+   in the user's profile).  A different file name can be specified using the
+   <literal>servicefile</literal> key word in a libpq connection string or by
    setting the environment variable <envar>PGSERVICEFILE</envar>.
    The system-wide file is named <filename>pg_service.conf</filename>.
    By default it is sought in the <filename>etc</filename> directory
-- 
2.50.0

v13-0002-psql-Add-SERVICEFILE-variable.patchtext/x-diff; charset=us-asciiDownload
From f4a589919986268a3e177e0828798ec6bc1ce250 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Thu, 10 Jul 2025 14:13:09 +0900
Subject: [PATCH v13 2/2] psql: Add SERVICEFILE variable

---
 src/bin/psql/command.c         | 7 +++++++
 doc/src/sgml/ref/psql-ref.sgml | 9 +++++++++
 2 files changed, 16 insertions(+)

diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index 0a55901b14e1..0e00d73487c3 100644
--- a/src/bin/psql/command.c
+++ b/src/bin/psql/command.c
@@ -4481,6 +4481,7 @@ SyncVariables(void)
 	char		vbuf[32];
 	const char *server_version;
 	char	   *service_name;
+	char	   *service_file;
 
 	/* get stuff from connection */
 	pset.encoding = PQclientEncoding(pset.db);
@@ -4500,6 +4501,11 @@ SyncVariables(void)
 	if (service_name)
 		pg_free(service_name);
 
+	service_file = get_conninfo_value("servicefile");
+	SetVariable(pset.vars, "SERVICEFILE", service_file);
+	if (service_file)
+		pg_free(service_file);
+
 	/* this bit should match connection_warnings(): */
 	/* Try to get full text form of version, might include "devel" etc */
 	server_version = PQparameterStatus(pset.db, "server_version");
@@ -4529,6 +4535,7 @@ UnsyncVariables(void)
 {
 	SetVariable(pset.vars, "DBNAME", NULL);
 	SetVariable(pset.vars, "SERVICE", NULL);
+	SetVariable(pset.vars, "SERVICEFILE", NULL);
 	SetVariable(pset.vars, "USER", NULL);
 	SetVariable(pset.vars, "HOST", NULL);
 	SetVariable(pset.vars, "PORT", NULL);
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 95f4cac2467e..4f7b11175c67 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -4623,6 +4623,15 @@ bar
         </listitem>
       </varlistentry>
 
+      <varlistentry id="app-psql-variables-servicefile">
+        <term><varname>SERVICEFILE</varname></term>
+        <listitem>
+        <para>
+        The service file name, if applicable.
+        </para>
+        </listitem>
+      </varlistentry>
+
       <varlistentry id="app-psql-variables-shell-error">
        <term><varname>SHELL_ERROR</varname></term>
        <listitem>
-- 
2.50.0

#34Michael Paquier
michael@paquier.xyz
In reply to: Michael Paquier (#33)
Re: [PATCH] PGSERVICEFILE as part of a normal connection string

On Thu, Jul 10, 2025 at 02:21:38PM +0900, Michael Paquier wrote:

After that, I have applied a few cosmetic tweaks here and there, and
attached is what I have staged for commit, minus proper commit
messages. The new TAP tests have some WIN32-specific things, and I
won't be able to look at the buildfarm if I were to apply things
today, so this will have to wait until the beginning of next week.
The CI is happy with it, so at least we are one checkbox down.

Both are now applied as of 6b1c4d326b06 and 092f3c63efc6.

The scary part was the WIN32 buildfarm members, which I've confirmed
have accepted the new tests. So we should be good here.
--
Michael

#35Ryo Kanbayashi
kanbayashi.dev@gmail.com
In reply to: Michael Paquier (#34)
Re: [PATCH] PGSERVICEFILE as part of a normal connection string

On Mon, Jul 14, 2025 at 9:10 AM Michael Paquier <michael@paquier.xyz> wrote:

On Thu, Jul 10, 2025 at 02:21:38PM +0900, Michael Paquier wrote:

After that, I have applied a few cosmetic tweaks here and there, and
attached is what I have staged for commit, minus proper commit
messages. The new TAP tests have some WIN32-specific things, and I
won't be able to look at the buildfarm if I were to apply things
today, so this will have to wait until the beginning of next week.
The CI is happy with it, so at least we are one checkbox down.

Both are now applied as of 6b1c4d326b06 and 092f3c63efc6.

The scary part was the WIN32 buildfarm members, which I've confirmed
have accepted the new tests. So we should be good here.

Thank you for your long-time review responses and comments :)

I will do my best for next patch as well :)

---
Great regards,
NTT Open Source Software Center
Ryo Kanbayashi