[PATCH] Fix ALTER SYSTEM empty string bug for GUC_LIST_QUOTE parameters

Started by Andrei Klychkov5 months ago22 messages
#1Andrei Klychkov
andrew.a.klychkov@gmail.com
2 attachment(s)

Hi Hackers,

I'm submitting a patch to fix a bug where ALTER SYSTEM SET with empty
strings for
GUC_LIST_QUOTE parameters (like shared_preload_libraries) results in
malformed
configuration entries that cause server crashes on restart.

Please take a look,

Thanks
Andrew

Attachments:

README_fix.mdtext/markdown; charset=US-ASCII; name=README_fix.mdDownload
fix_alter_system_empty_string_bug.patchtext/x-patch; charset=US-ASCII; name=fix_alter_system_empty_string_bug.patchDownload
From 3a6a7db602036309fbc6a7f5c7c731a71038967b Mon Sep 17 00:00:00 2001
From: Andrew Klychkov <andrew.a.klychkov@gmail.com>
Date: Thu, 28 Aug 2025 10:33:42 +0200
Subject: [PATCH] Fix ALTER SYSTEM empty string bug for GUC_LIST_QUOTE
 parameters

When ALTER SYSTEM SET is used with an empty string for parameters with
GUC_LIST_QUOTE flag (like shared_preload_libraries), the empty string
was being quoted by quote_identifier(), resulting in '' being written
to postgresql.auto.conf. This caused server crashes on restart.

The fix prevents empty strings from being quoted when GUC_LIST_QUOTE is
set, treating them as 'no value' rather than literal empty strings.

Fixes bug where 'ALTER SYSTEM SET "shared_preload_libraries" TO '''
would write 'shared_preload_libraries = '""'' to postgresql.auto.conf.
---
 src/backend/utils/misc/guc_funcs.c | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/src/backend/utils/misc/guc_funcs.c b/src/backend/utils/misc/guc_funcs.c
index b9e26982ab..389841a4ec 100644
--- a/src/backend/utils/misc/guc_funcs.c
+++ b/src/backend/utils/misc/guc_funcs.c
@@ -288,8 +288,10 @@ flatten_set_variable_args(const char *name, List *args)
 					/*
 					 * Plain string literal or identifier.  For quote mode,
 					 * quote it if it's not a vanilla identifier.
+					 * However, empty strings should not be quoted as they
+					 * represent "no value" rather than a literal empty string.
 					 */
-					if (flags & GUC_LIST_QUOTE)
+					if ((flags & GUC_LIST_QUOTE) && val[0] != '\0')
 						appendStringInfoString(&buf, quote_identifier(val));
 					else
 						appendStringInfoString(&buf, val);
-- 
2.47.0

#2Jim Jones
jim.jones@uni-muenster.de
In reply to: Andrei Klychkov (#1)
Re: [PATCH] Fix ALTER SYSTEM empty string bug for GUC_LIST_QUOTE parameters

Hi Andrew

On 28.08.25 11:29, Andrei Klychkov wrote:

I'm submitting a patch to fix a bug where ALTER SYSTEM SET with empty
strings for
GUC_LIST_QUOTE parameters (like shared_preload_libraries) results in
malformed
configuration entries that cause server crashes on restart.

I tested the patch and it does what you described

$ psql postgres -c "ALTER SYSTEM SET shared_preload_libraries TO '';"
ALTER SYSTEM
$ cat /usr/local/postgres-dev/testdb/postgresql.auto.conf
# Do not edit this file manually!
# It will be overwritten by the ALTER SYSTEM command.
shared_preload_libraries = ''

However, it breaks one of the rules.sql regression tests

@@ -3552,21 +3552,7 @@
     SET local_preload_libraries TO "Mixed/Case", 'c:/''a"/path', '',
'0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789'
     IMMUTABLE STRICT;
 SELECT pg_get_functiondef('func_with_set_params()'::regprocedure);
-                                                                           
pg_get_functiondef                                                                          
 
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------
- CREATE OR REPLACE FUNCTION
public.func_with_set_params()                                                                                                               
+
-  RETURNS
integer                                                                                                                                                       
+
-  LANGUAGE
sql                                                                                                                                                          
+
-  IMMUTABLE
STRICT                                                                                                                                                      
+
-  SET search_path TO
'pg_catalog'                                                                                                                                       
+
-  SET extra_float_digits TO
'2'                                                                                                                                         
+
-  SET work_mem TO
'4MB'                                                                                                                                                 
+
-  SET "DateStyle" TO 'iso,
mdy'                                                                                                                                         
+
-  SET local_preload_libraries TO 'Mixed/Case', 'c:/''a"/path', '',
'0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789'+
- AS $function$select
1;$function$                                                                                                                                       
+
-
-(1 row)
-
+ERROR:  invalid list syntax in proconfig item

Best, Jim

#3Andrei Klychkov
andrew.a.klychkov@gmail.com
In reply to: Jim Jones (#2)
2 attachment(s)
Re: [PATCH] Fix ALTER SYSTEM empty string bug for GUC_LIST_QUOTE parameters

Hi Jim,

Thanks a lot for reviewing! Nice catch, TIL!
Version 2 of the patch is attached, please check it out.
In a nutshell, the issue actually wasn't in the flatten_set_variable_args()
function as initially suspected, but rather in the configuration file
writing logic in the write_auto_conf_file(): more details in v2_README.md

Looking forward to your feedback, thanks!
Regards
Andrew

On Tue, Sep 2, 2025 at 2:16 PM Jim Jones <jim.jones@uni-muenster.de> wrote:

Show quoted text

Hi Andrew

On 28.08.25 11:29, Andrei Klychkov wrote:

I'm submitting a patch to fix a bug where ALTER SYSTEM SET with empty
strings for
GUC_LIST_QUOTE parameters (like shared_preload_libraries) results in
malformed
configuration entries that cause server crashes on restart.

I tested the patch and it does what you described

$ psql postgres -c "ALTER SYSTEM SET shared_preload_libraries TO '';"
ALTER SYSTEM
$ cat /usr/local/postgres-dev/testdb/postgresql.auto.conf
# Do not edit this file manually!
# It will be overwritten by the ALTER SYSTEM command.
shared_preload_libraries = ''

However, it breaks one of the rules.sql regression tests

@@ -3552,21 +3552,7 @@
SET local_preload_libraries TO "Mixed/Case", 'c:/''a"/path', '',

'0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789'
IMMUTABLE STRICT;
SELECT pg_get_functiondef('func_with_set_params()'::regprocedure);

-

pg_get_functiondef

---------------------------------------------------------------------------------------------------------------------------------------------------------------------------
- CREATE OR REPLACE FUNCTION

public.func_with_set_params()
+
-  RETURNS
integer
+
-  LANGUAGE
sql
+
-  IMMUTABLE
STRICT
+
-  SET search_path TO
'pg_catalog'
+
-  SET extra_float_digits TO
'2'
+
-  SET work_mem TO
'4MB'
+
-  SET "DateStyle" TO 'iso,
mdy'
+
-  SET local_preload_libraries TO 'Mixed/Case', 'c:/''a"/path', '',

'0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789'+
- AS $function$select

1;$function$
+
-
-(1 row)
-
+ERROR:  invalid list syntax in proconfig item

Best, Jim

Attachments:

v2_README_fix.mdtext/markdown; charset=US-ASCII; name=v2_README_fix.mdDownload
v2_fix_alter_system_empty_string_bug.patchtext/x-patch; charset=US-ASCII; name=v2_fix_alter_system_empty_string_bug.patchDownload
From 4642e4e0b0a63d9da6ac4479963dec6c7b71ac68 Mon Sep 17 00:00:00 2001
From: Andrew Klychkov <andrew.a.klychkov@gmail.com>
Date: Wed, 3 Sep 2025 10:48:49 +0200
Subject: [PATCH] Fix ALTER SYSTEM empty string bug for GUC_LIST_QUOTE
 parameters

When ALTER SYSTEM SET is used with an empty string for parameters with
GUC_LIST_QUOTE flag (like shared_preload_libraries), the empty string
was being double-quoted, resulting in '""' being written to
postgresql.auto.conf. This caused server crashes on restart due to
malformed configuration syntax.

The fix detects when a GUC_LIST_QUOTE parameter has the value '""' and
writes it as '' instead, preventing the double-quoting issue while
maintaining proper configuration file syntax.

Fixes bug where 'ALTER SYSTEM SET "shared_preload_libraries" TO '''
would write 'shared_preload_libraries = '""'' to postgresql.auto.conf.
---
 src/backend/utils/misc/guc.c | 39 ++++++++++++++++++++++++++++++++++++
 1 file changed, 39 insertions(+)

diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index 46fdefebe3..7494cbd56b 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -4497,6 +4497,45 @@ write_auto_conf_file(int fd, const char *filename, ConfigVariable *head)
 	for (item = head; item != NULL; item = item->next)
 	{
 		char	   *escaped;
+		bool		is_guc_list_quote = false;
+
+		/*
+		 * Check if this is a GUC_LIST_QUOTE parameter to handle empty strings
+		 * specially and prevent double-quoting issues.
+		 */
+		if (item->value[0] == '\0' || strcmp(item->value, "\"\"") == 0)
+		{
+			struct config_generic *record = find_option(item->name, false, true, WARNING);
+			if (record && (record->flags & GUC_LIST_QUOTE))
+				is_guc_list_quote = true;
+		}
+
+		/*
+		 * Special handling for GUC_LIST_QUOTE parameters with empty strings.
+		 * When ALTER SYSTEM SET is used with an empty string for such parameters,
+		 * the value comes through as '""' (quoted empty string). We want to write
+		 * this as an empty string rather than the quoted version to avoid the
+		 * double-quoting issue.
+		 */
+		if (is_guc_list_quote && (item->value[0] == '\0' || strcmp(item->value, "\"\"") == 0))
+		{
+			/* For GUC_LIST_QUOTE parameters, write empty string as '' */
+			resetStringInfo(&buf);
+			appendStringInfoString(&buf, item->name);
+			appendStringInfoString(&buf, " = ''\n");
+
+			errno = 0;
+			if (write(fd, buf.data, buf.len) != buf.len)
+			{
+				/* if write didn't set errno, assume problem is no disk space */
+				if (errno == 0)
+					errno = ENOSPC;
+				ereport(ERROR,
+						(errcode_for_file_access(),
+						 errmsg("could not write to file \"%s\": %m", filename)));
+			}
+			continue;
+		}
 
 		resetStringInfo(&buf);
 
-- 
2.47.0

#4Fujii Masao
masao.fujii@gmail.com
In reply to: Andrei Klychkov (#3)
Re: [PATCH] Fix ALTER SYSTEM empty string bug for GUC_LIST_QUOTE parameters

On Wed, Sep 3, 2025 at 6:59 PM Andrei Klychkov
<andrew.a.klychkov@gmail.com> wrote:

Hi Jim,

Thanks a lot for reviewing! Nice catch, TIL!
Version 2 of the patch is attached, please check it out.
In a nutshell, the issue actually wasn't in the flatten_set_variable_args() function as initially suspected, but rather in the configuration file writing logic in the write_auto_conf_file(): more details in v2_README.md

Even with this patch, an empty string set via SET is still quoted. For example:

=# SET local_preload_libraries TO '';
SET
=# SHOW local_preload_libraries ;
local_preload_libraries
-------------------------
""
(1 row)

Is this behavior acceptable? I was thinking that an empty string should not
be quoted, regardless of whether it's set by ALTER SYSTEM SET or SET.
Thought?

If we agree it should be handled in both cases, flatten_set_variable_args()
seems to need to be updated. For example:

@@ -289,7 +289,8 @@ flatten_set_variable_args(const char *name, List *args)
                                         * Plain string literal or
identifier.  For quote mode,
                                         * quote it if it's not a
vanilla identifier.
                                         */
-                                       if (flags & GUC_LIST_QUOTE)
+                                       if (flags & GUC_LIST_QUOTE &&
+                                               !(val[0] == '\0' &&
list_length(args) == 1))

appendStringInfoString(&buf, quote_identifier(val));
else

appendStringInfoString(&buf, val);

Regards,

--
Fujii Masao

#5Andrei Klychkov
andrew.a.klychkov@gmail.com
In reply to: Fujii Masao (#4)
Re: [PATCH] Fix ALTER SYSTEM empty string bug for GUC_LIST_QUOTE parameters

Even with this patch, an empty string set via SET is still quoted. For

example:

=# SET local_preload_libraries TO '';
SET
=# SHOW local_preload_libraries ;
local_preload_libraries
-------------------------
""
(1 row)

Is this behavior acceptable? I was thinking that an empty string should

not

be quoted, regardless of whether it's set by ALTER SYSTEM SET or SET.
Thought?

If we agree it should be handled in both cases,

flatten_set_variable_args()

seems to need to be updated.

Hello Fujii,
Thanks for your review!

I'm personally not sure because this is my first patch and I'm trying to
solve a specific issue of the postgresql.auto.conf-related server crashes.
If what your *broader-impact* suggestion makes sense to more experienced
devs in this area, I'd be happy to update the patch as you put it, test it
(as much as I can), and re-submit v3.
Otherwise, I'd be happy with the v2 implementation that seemingly solves my
specific issue.

Thanks
Regards
Andrew

On Wed, Sep 3, 2025 at 4:48 PM Fujii Masao <masao.fujii@gmail.com> wrote:

Show quoted text

On Wed, Sep 3, 2025 at 6:59 PM Andrei Klychkov
<andrew.a.klychkov@gmail.com> wrote:

Hi Jim,

Thanks a lot for reviewing! Nice catch, TIL!
Version 2 of the patch is attached, please check it out.
In a nutshell, the issue actually wasn't in the

flatten_set_variable_args() function as initially suspected, but rather in
the configuration file writing logic in the write_auto_conf_file(): more
details in v2_README.md

Even with this patch, an empty string set via SET is still quoted. For
example:

=# SET local_preload_libraries TO '';
SET
=# SHOW local_preload_libraries ;
local_preload_libraries
-------------------------
""
(1 row)

Is this behavior acceptable? I was thinking that an empty string should not
be quoted, regardless of whether it's set by ALTER SYSTEM SET or SET.
Thought?

If we agree it should be handled in both cases, flatten_set_variable_args()
seems to need to be updated. For example:

@@ -289,7 +289,8 @@ flatten_set_variable_args(const char *name, List *args)
* Plain string literal or
identifier.  For quote mode,
* quote it if it's not a
vanilla identifier.
*/
-                                       if (flags & GUC_LIST_QUOTE)
+                                       if (flags & GUC_LIST_QUOTE &&
+                                               !(val[0] == '\0' &&
list_length(args) == 1))

appendStringInfoString(&buf, quote_identifier(val));
else

appendStringInfoString(&buf, val);

Regards,

--
Fujii Masao

#6Jim Jones
jim.jones@uni-muenster.de
In reply to: Andrei Klychkov (#5)
Re: [PATCH] Fix ALTER SYSTEM empty string bug for GUC_LIST_QUOTE parameters

On 04.09.25 09:41, Andrei Klychkov wrote:

Even with this patch, an empty string set via SET is still quoted. For
example:

    =# SET local_preload_libraries TO '';
    SET
    =# SHOW local_preload_libraries ;
     local_preload_libraries
    -------------------------
     ""
    (1 row)

Is this behavior acceptable? I was thinking that an empty string
should not
be quoted, regardless of whether it's set by ALTER SYSTEM SET or SET.
Thought?

If we agree it should be handled in both cases,
flatten_set_variable_args()
seems to need to be updated. For example:

@@ -289,7 +289,8 @@ flatten_set_variable_args(const char *name, List
*args)
                                         * Plain string literal or
identifier.  For quote mode,
                                         * quote it if it's not a
vanilla identifier.
                                         */
-                                       if (flags & GUC_LIST_QUOTE)
+                                       if (flags & GUC_LIST_QUOTE &&
+                                               !(val[0] == '\0' &&
list_length(args) == 1))

appendStringInfoString(&buf, quote_identifier(val));
                                        else

appendStringInfoString(&buf, val);

Yeah, I also think that SET and ALTER SYSTEM SET should be consistent. I
tested your proposed changes in flatten_set_variable_args ..

/*
 * Plain string literal or identifier.  For quote mode,
 * quote it if it's not a vanilla identifier. However, if the value
 * is an empty string (val[0] == '\0') and it is the only element
 * in the list (list_length(args) == 1), display it as an empty string
 * without quotes for clarity and consistency.
 */
if (flags & GUC_LIST_QUOTE &&
    !(val[0] == '\0' && list_length(args) == 1))
    appendStringInfoString(&buf, quote_identifier(val));
else
    appendStringInfoString(&buf, val);

... and it seems to work:

$ psql postgres -c "SET local_preload_libraries TO ''; SHOW
local_preload_libraries;"
SET
 local_preload_libraries
-------------------------
 
(1 row)

$ psql postgres -c "ALTER SYSTEM SET local_preload_libraries TO '';"
ALTER SYSTEM

(restart ..)

$ cat /usr/local/postgres-dev/testdb/postgresql.auto.conf
# Do not edit this file manually!
# It will be overwritten by the ALTER SYSTEM command.
local_preload_libraries = ''

$ psql postgres -c "SHOW local_preload_libraries;"
 local_preload_libraries
-------------------------
 
(1 row)

I'm wondering if we should add some tests, e.g. in guc.sql:

SET local_preload_libraries TO '';
SHOW local_preload_libraries;

and also it's equivalents for ALTER SYSTEM SET (still not sure where :)).

Best regards, Jim

#7Fujii Masao
masao.fujii@gmail.com
In reply to: Andrei Klychkov (#5)
Re: [PATCH] Fix ALTER SYSTEM empty string bug for GUC_LIST_QUOTE parameters

On Thu, Sep 4, 2025 at 4:42 PM Andrei Klychkov
<andrew.a.klychkov@gmail.com> wrote:

Even with this patch, an empty string set via SET is still quoted. For example:

=# SET local_preload_libraries TO '';
SET
=# SHOW local_preload_libraries ;
local_preload_libraries
-------------------------
""
(1 row)

Is this behavior acceptable? I was thinking that an empty string should not
be quoted, regardless of whether it's set by ALTER SYSTEM SET or SET.
Thought?

If we agree it should be handled in both cases, flatten_set_variable_args()
seems to need to be updated.

Hello Fujii,
Thanks for your review!

I'm personally not sure because this is my first patch and I'm trying to solve a specific issue of the postgresql.auto.conf-related server crashes.
If what your *broader-impact* suggestion makes sense to more experienced devs in this area, I'd be happy to update the patch as you put it, test it (as much as I can), and re-submit v3.
Otherwise, I'd be happy with the v2 implementation that seemingly solves my specific issue.

Yeah, I think my suggestion makes sense.

BTW, regarding the behavior change, I believe that users likely expect
the parameter to be reset when specifying an empty string, rather than
being set to "". So the proposed change seems reasonable. However,
the current behavior has existed for a long time, and I haven’t seen
any complaints about it. Some users may rely on the existing behavior
(I think that’s unlikely, though). So I'm not completely sure yet if this change
should be applied.

Regards,

--
Fujii Masao

#8Fujii Masao
masao.fujii@gmail.com
In reply to: Jim Jones (#6)
Re: [PATCH] Fix ALTER SYSTEM empty string bug for GUC_LIST_QUOTE parameters

On Thu, Sep 4, 2025 at 11:58 PM Jim Jones <jim.jones@uni-muenster.de> wrote:

Yeah, I also think that SET and ALTER SYSTEM SET should be consistent. I
tested your proposed changes in flatten_set_variable_args ..

Thanks for the test!

I'm wondering if we should add some tests, e.g. in guc.sql:

+1 to add regression tests.

Regards,

--
Fujii Masao

#9Tom Lane
tgl@sss.pgh.pa.us
In reply to: Fujii Masao (#7)
Re: [PATCH] Fix ALTER SYSTEM empty string bug for GUC_LIST_QUOTE parameters

Fujii Masao <masao.fujii@gmail.com> writes:

Even with this patch, an empty string set via SET is still quoted. For example:

=# SET local_preload_libraries TO '';
SET
=# SHOW local_preload_libraries ;
local_preload_libraries
-------------------------
""
(1 row)

Is this behavior acceptable? I was thinking that an empty string should not
be quoted, regardless of whether it's set by ALTER SYSTEM SET or SET.

BTW, regarding the behavior change, I believe that users likely expect
the parameter to be reset when specifying an empty string, rather than
being set to "". So the proposed change seems reasonable. However,
the current behavior has existed for a long time, and I haven’t seen
any complaints about it.

I think this is largely based on confusion. In the above example,
local_preload_libraries is being set to a list containing a single
entry that is an empty string, and the output of SHOW is a fully
accurate depiction of that state. It is *not* being set to an
empty list --- we actually don't have any syntax that would permit
doing so in SET. For comparison, there is a big difference between

SET local_preload_libraries = a, b;
SET local_preload_libraries = 'a, b';

In the latter case you get a single list entry containing the
string "a, b". We do not try to parse that into multiple entries,
and by the same token parsing an empty string into an empty list
would be the Wrong Thing.

We might want to start resolving this by inventing a syntax for
setting a list GUC to an empty list. I'm not very sure what that
should look like, except that it mustn't be SET ... TO ''.

I'm not certain whether config-file parsing or ALTER SYSTEM
would need any code changes once we resolve the ambiguity in SET.
The config-file syntax is different and doesn't have this problem
of not being able to represent an empty list.

(Also, "let's unify the list-GUC syntax between config file and SET"
seems like a non-starter. It'd be better no doubt if they hadn't
diverged, but at this point we'd break far more than we fix if
we change either one.)

regards, tom lane

#10Tom Lane
tgl@sss.pgh.pa.us
In reply to: Tom Lane (#9)
Re: [PATCH] Fix ALTER SYSTEM empty string bug for GUC_LIST_QUOTE parameters

I wrote:

We might want to start resolving this by inventing a syntax for
setting a list GUC to an empty list. I'm not very sure what that
should look like, except that it mustn't be SET ... TO ''.

I experimented with the idea of allowing

SET var TO ;

and got a bunch of shift-reduce conflicts from bison, which don't
look easy to avoid. It's probably a bit too surprising anyway.

This works from a grammar standpoint:

SET var TO NULL;

Because NULL is fully reserved, this isn't usurping any cases that
weren't syntax errors before. It might still be too surprising.

Another idea is that we could redefine a single '' as meaning an empty
list if we were to forbid empty strings as members of GUC_LIST_QUOTE
variables. This doesn't look like it'd be a big issue for the current
set of such variables:

local_preload_libraries
search_path
session_preload_libraries
shared_preload_libraries
temp_tablespaces
unix_socket_directories

We might break some applications that're relying on the current
behavior that an empty item would be effectively ignored.
But that seems a bit remote, and anyway they could just switch
to some other nonexistent name.

Actually, looking at the small number of GUCs that are marked
GUC_LIST_INPUT but not GUC_LIST_QUOTE, I wonder if we shouldn't
prohibit empty strings across-the-board for GUC_LIST_INPUT GUCs.
I don't see any where it'd be useful to allow them. Then we
could allow '' to mean empty list for all of them.

regards, tom lane

#11Tom Lane
tgl@sss.pgh.pa.us
In reply to: Tom Lane (#10)
1 attachment(s)
Re: [PATCH] Fix ALTER SYSTEM empty string bug for GUC_LIST_QUOTE parameters

I wrote:

Another idea is that we could redefine a single '' as meaning an empty
list if we were to forbid empty strings as members of GUC_LIST_QUOTE
variables.

I tried to work through what this'd imply, and arrived at the attached
patch. I might've missed some places, and I did not think about what
documentation updates would be appropriate.

Note that the patch includes changing SplitIdentifierString and its
clones to forbid zero-length quoted elements, which were formerly
allowed. Without this, we'd accept values from config files that
could not be represented in SET, which is exactly the situation we
are trying to fix.

I'm not entirely sure if this is the way to go, or if we want to
adopt some other solution that doesn't involve forbidding empty
list elements. I suspect that anything else we come up with would
be less intuitive than letting SET list_var = '' do the job;
but maybe I just lack imagination today.

regards, tom lane

Attachments:

v3-allow-SET-to-an-empty-list.patchtext/x-diff; charset=us-ascii; name=v3-allow-SET-to-an-empty-list.patchDownload
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 3d6e6bdbfd2..27ba1920e68 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -3086,9 +3086,10 @@ pg_get_functiondef(PG_FUNCTION_ARGS)
 				 * rules used there aren't exactly like SQL's, we have to
 				 * break the list value apart and then quote the elements as
 				 * string literals.  (The elements may be double-quoted as-is,
-				 * but we can't just feed them to the SQL parser; it would do
-				 * the wrong thing with elements that are zero-length or
-				 * longer than NAMEDATALEN.)
+				 * but we can't just feed them to the SQL parser that way; it
+				 * would truncate elements that are longer than NAMEDATALEN,
+				 * which would be wrong if they're paths.)  Also, we need a
+				 * special case for empty lists.
 				 *
 				 * Variables that are not so marked should just be emitted as
 				 * simple string literals.  If the variable is not known to
@@ -3106,6 +3107,9 @@ pg_get_functiondef(PG_FUNCTION_ARGS)
 						/* this shouldn't fail really */
 						elog(ERROR, "invalid list syntax in proconfig item");
 					}
+					/* Special case: represent an empty list as '' */
+					if (namelist == NIL)
+						appendStringInfoString(&buf, "''");
 					foreach(lc, namelist)
 					{
 						char	   *curname = (char *) lfirst(lc);
diff --git a/src/backend/utils/adt/varlena.c b/src/backend/utils/adt/varlena.c
index 2c398cd9e5c..d5ef492ee41 100644
--- a/src/backend/utils/adt/varlena.c
+++ b/src/backend/utils/adt/varlena.c
@@ -2753,7 +2753,7 @@ SplitIdentifierString(char *rawstring, char separator,
 		nextp++;				/* skip leading whitespace */
 
 	if (*nextp == '\0')
-		return true;			/* allow empty string */
+		return true;			/* empty string represents empty list */
 
 	/* At the top of the loop, we are at start of a new identifier. */
 	do
@@ -2777,6 +2777,8 @@ SplitIdentifierString(char *rawstring, char separator,
 				nextp = endp;
 			}
 			/* endp now points at the terminating quote */
+			if (curname == endp)
+				return false;	/* empty quoted name not allowed */
 			nextp = endp + 1;
 		}
 		else
@@ -2880,7 +2882,7 @@ SplitDirectoriesString(char *rawstring, char separator,
 		nextp++;				/* skip leading whitespace */
 
 	if (*nextp == '\0')
-		return true;			/* allow empty string */
+		return true;			/* empty string represents empty list */
 
 	/* At the top of the loop, we are at start of a new directory. */
 	do
@@ -2904,6 +2906,8 @@ SplitDirectoriesString(char *rawstring, char separator,
 				nextp = endp;
 			}
 			/* endp now points at the terminating quote */
+			if (curname == endp)
+				return false;	/* empty quoted name not allowed */
 			nextp = endp + 1;
 		}
 		else
@@ -3001,7 +3005,7 @@ SplitGUCList(char *rawstring, char separator,
 		nextp++;				/* skip leading whitespace */
 
 	if (*nextp == '\0')
-		return true;			/* allow empty string */
+		return true;			/* empty string represents empty list */
 
 	/* At the top of the loop, we are at start of a new identifier. */
 	do
@@ -3025,6 +3029,8 @@ SplitGUCList(char *rawstring, char separator,
 				nextp = endp;
 			}
 			/* endp now points at the terminating quote */
+			if (curname == endp)
+				return false;	/* empty quoted name not allowed */
 			nextp = endp + 1;
 		}
 		else
diff --git a/src/backend/utils/misc/guc_funcs.c b/src/backend/utils/misc/guc_funcs.c
index b9e26982abd..a0557d164d8 100644
--- a/src/backend/utils/misc/guc_funcs.c
+++ b/src/backend/utils/misc/guc_funcs.c
@@ -210,12 +210,30 @@ flatten_set_variable_args(const char *name, List *args)
 	else
 		flags = 0;
 
-	/* Complain if list input and non-list variable */
-	if ((flags & GUC_LIST_INPUT) == 0 &&
-		list_length(args) != 1)
-		ereport(ERROR,
-				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-				 errmsg("SET %s takes only one argument", name)));
+	/*
+	 * Handle special cases for list input.
+	 */
+	if (flags & GUC_LIST_INPUT)
+	{
+		/* A single empty-string item is treated as an empty list. */
+		if (list_length(args) == 1)
+		{
+			Node	   *arg = (Node *) linitial(args);
+
+			if (IsA(arg, A_Const) &&
+				nodeTag(&((A_Const *) arg)->val) == T_String &&
+				*strVal(&((A_Const *) arg)->val) == '\0')
+				return pstrdup("");
+		}
+	}
+	else
+	{
+		/* Complain if list input and non-list variable. */
+		if (list_length(args) != 1)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("SET %s takes only one argument", name)));
+	}
 
 	initStringInfo(&buf);
 
@@ -269,6 +287,9 @@ flatten_set_variable_args(const char *name, List *args)
 					Datum		interval;
 					char	   *intervalout;
 
+					/* gram.y ensures this is only reachable for TIME ZONE */
+					Assert(!(flags & GUC_LIST_QUOTE));
+
 					typenameTypeIdAndMod(NULL, typeName, &typoid, &typmod);
 					Assert(typoid == INTERVALOID);
 
@@ -286,9 +307,17 @@ flatten_set_variable_args(const char *name, List *args)
 				else
 				{
 					/*
-					 * Plain string literal or identifier.  For quote mode,
+					 * Plain string literal or identifier.  In a list GUC,
+					 * disallow empty-string elements (so that the preceding
+					 * hack for empty lists is unambiguous).  For quote mode,
 					 * quote it if it's not a vanilla identifier.
 					 */
+					if ((flags & GUC_LIST_INPUT) && *val == '\0')
+						ereport(ERROR,
+								(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+								 errmsg("SET %s does not accept empty-string elements",
+										name)));
+
 					if (flags & GUC_LIST_QUOTE)
 						appendStringInfoString(&buf, quote_identifier(val));
 					else
diff --git a/src/bin/pg_dump/dumputils.c b/src/bin/pg_dump/dumputils.c
index 05b84c0d6e7..00a369c8861 100644
--- a/src/bin/pg_dump/dumputils.c
+++ b/src/bin/pg_dump/dumputils.c
@@ -781,7 +781,7 @@ SplitGUCList(char *rawstring, char separator,
 		nextp++;				/* skip leading whitespace */
 
 	if (*nextp == '\0')
-		return true;			/* allow empty string */
+		return true;			/* empty string represents empty list */
 
 	/* At the top of the loop, we are at start of a new identifier. */
 	do
@@ -805,6 +805,8 @@ SplitGUCList(char *rawstring, char separator,
 				nextp = endp;
 			}
 			/* endp now points at the terminating quote */
+			if (curname == endp)
+				return false;	/* empty quoted name not allowed */
 			nextp = endp + 1;
 		}
 		else
@@ -891,8 +893,9 @@ makeAlterConfigCommand(PGconn *conn, const char *configitem,
 	 * array.  However, because the quoting rules used there aren't exactly
 	 * like SQL's, we have to break the list value apart and then quote the
 	 * elements as string literals.  (The elements may be double-quoted as-is,
-	 * but we can't just feed them to the SQL parser; it would do the wrong
-	 * thing with elements that are zero-length or longer than NAMEDATALEN.)
+	 * but we can't just feed them to the SQL parser that way; it would
+	 * truncate elements that are longer than NAMEDATALEN, which would be
+	 * wrong if they're paths.)  Also, we need a special case for empty lists.
 	 *
 	 * Variables that are not so marked should just be emitted as simple
 	 * string literals.  If the variable is not known to
@@ -908,6 +911,9 @@ makeAlterConfigCommand(PGconn *conn, const char *configitem,
 		/* this shouldn't fail really */
 		if (SplitGUCList(pos, ',', &namelist))
 		{
+			/* Special case: represent an empty list as '' */
+			if (*namelist == NULL)
+				appendPQExpBufferStr(buf, "''");
 			for (nameptr = namelist; *nameptr; nameptr++)
 			{
 				if (nameptr != namelist)
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index bea793456f9..9d36a6a5aaf 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -13699,8 +13699,9 @@ dumpFunc(Archive *fout, const FuncInfo *finfo)
 		 * aren't exactly like SQL's, we have to break the list value apart
 		 * and then quote the elements as string literals.  (The elements may
 		 * be double-quoted as-is, but we can't just feed them to the SQL
-		 * parser; it would do the wrong thing with elements that are
-		 * zero-length or longer than NAMEDATALEN.)
+		 * parser that way; it would truncate elements that are longer than
+		 * NAMEDATALEN, which would be wrong if they're paths.)  Also, we need
+		 * a special case for empty lists.
 		 *
 		 * Variables that are not so marked should just be emitted as simple
 		 * string literals.  If the variable is not known to
@@ -13716,6 +13717,9 @@ dumpFunc(Archive *fout, const FuncInfo *finfo)
 			/* this shouldn't fail really */
 			if (SplitGUCList(pos, ',', &namelist))
 			{
+				/* Special case: represent an empty list as '' */
+				if (*namelist == NULL)
+					appendPQExpBufferStr(q, "''");
 				for (nameptr = namelist; *nameptr; nameptr++)
 				{
 					if (nameptr != namelist)
diff --git a/src/test/regress/expected/guc.out b/src/test/regress/expected/guc.out
index 7f9e29c765c..e49e609415b 100644
--- a/src/test/regress/expected/guc.out
+++ b/src/test/regress/expected/guc.out
@@ -31,6 +31,24 @@ SELECT '2006-08-13 12:34:56'::timestamptz;
  2006-08-13 12:34:56-07
 (1 row)
 
+-- Check handling of list GUCs
+SET search_path = 'pg_catalog', Foo, 'Bar';
+SHOW search_path;
+      search_path       
+------------------------
+ pg_catalog, foo, "Bar"
+(1 row)
+
+SET search_path = '';  -- means empty list
+SHOW search_path;
+ search_path 
+-------------
+ 
+(1 row)
+
+SET search_path = '', 'foo';  -- error, empty list elements not OK
+ERROR:  SET search_path does not accept empty-string elements
+RESET search_path;
 -- SET LOCAL has no effect outside of a transaction
 SET LOCAL vacuum_cost_delay TO 50;
 WARNING:  SET LOCAL can only be used in transaction blocks
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 35e8aad7701..43d5cf10266 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -3549,21 +3549,23 @@ CREATE FUNCTION func_with_set_params() RETURNS integer
     SET extra_float_digits TO 2
     SET work_mem TO '4MB'
     SET datestyle to iso, mdy
-    SET local_preload_libraries TO "Mixed/Case", 'c:/''a"/path', '', '0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789'
+    SET temp_tablespaces to ''
+    SET local_preload_libraries TO "Mixed/Case", 'c:/''a"/path', '0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789'
     IMMUTABLE STRICT;
 SELECT pg_get_functiondef('func_with_set_params()'::regprocedure);
-                                                                            pg_get_functiondef                                                                            
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------
- CREATE OR REPLACE FUNCTION public.func_with_set_params()                                                                                                                +
-  RETURNS integer                                                                                                                                                        +
-  LANGUAGE sql                                                                                                                                                           +
-  IMMUTABLE STRICT                                                                                                                                                       +
-  SET search_path TO 'pg_catalog'                                                                                                                                        +
-  SET extra_float_digits TO '2'                                                                                                                                          +
-  SET work_mem TO '4MB'                                                                                                                                                  +
-  SET "DateStyle" TO 'iso, mdy'                                                                                                                                          +
-  SET local_preload_libraries TO 'Mixed/Case', 'c:/''a"/path', '', '0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789'+
- AS $function$select 1;$function$                                                                                                                                        +
+                                                                          pg_get_functiondef                                                                          
+----------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE OR REPLACE FUNCTION public.func_with_set_params()                                                                                                            +
+  RETURNS integer                                                                                                                                                    +
+  LANGUAGE sql                                                                                                                                                       +
+  IMMUTABLE STRICT                                                                                                                                                   +
+  SET search_path TO 'pg_catalog'                                                                                                                                    +
+  SET extra_float_digits TO '2'                                                                                                                                      +
+  SET work_mem TO '4MB'                                                                                                                                              +
+  SET "DateStyle" TO 'iso, mdy'                                                                                                                                      +
+  SET temp_tablespaces TO ''                                                                                                                                         +
+  SET local_preload_libraries TO 'Mixed/Case', 'c:/''a"/path', '0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789'+
+ AS $function$select 1;$function$                                                                                                                                    +
  
 (1 row)
 
diff --git a/src/test/regress/sql/guc.sql b/src/test/regress/sql/guc.sql
index f65f84a2632..65630135f18 100644
--- a/src/test/regress/sql/guc.sql
+++ b/src/test/regress/sql/guc.sql
@@ -12,6 +12,14 @@ SHOW vacuum_cost_delay;
 SHOW datestyle;
 SELECT '2006-08-13 12:34:56'::timestamptz;
 
+-- Check handling of list GUCs
+SET search_path = 'pg_catalog', Foo, 'Bar';
+SHOW search_path;
+SET search_path = '';  -- means empty list
+SHOW search_path;
+SET search_path = '', 'foo';  -- error, empty list elements not OK
+RESET search_path;
+
 -- SET LOCAL has no effect outside of a transaction
 SET LOCAL vacuum_cost_delay TO 50;
 SHOW vacuum_cost_delay;
diff --git a/src/test/regress/sql/rules.sql b/src/test/regress/sql/rules.sql
index fdd3ff1d161..5c5ff5e9cca 100644
--- a/src/test/regress/sql/rules.sql
+++ b/src/test/regress/sql/rules.sql
@@ -1217,7 +1217,8 @@ CREATE FUNCTION func_with_set_params() RETURNS integer
     SET extra_float_digits TO 2
     SET work_mem TO '4MB'
     SET datestyle to iso, mdy
-    SET local_preload_libraries TO "Mixed/Case", 'c:/''a"/path', '', '0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789'
+    SET temp_tablespaces to ''
+    SET local_preload_libraries TO "Mixed/Case", 'c:/''a"/path', '0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789'
     IMMUTABLE STRICT;
 SELECT pg_get_functiondef('func_with_set_params()'::regprocedure);
 
#12Jim Jones
jim.jones@uni-muenster.de
In reply to: Tom Lane (#11)
Re: [PATCH] Fix ALTER SYSTEM empty string bug for GUC_LIST_QUOTE parameters

On 04.09.25 23:52, Tom Lane wrote:

Note that the patch includes changing SplitIdentifierString and its
clones to forbid zero-length quoted elements, which were formerly
allowed. Without this, we'd accept values from config files that
could not be represented in SET, which is exactly the situation we
are trying to fix.

I'm not entirely sure if this is the way to go, or if we want to
adopt some other solution that doesn't involve forbidding empty
list elements. I suspect that anything else we come up with would
be less intuitive than letting SET list_var = '' do the job;
but maybe I just lack imagination today.

This approach LGTM. It solves the initial issue:

$ psql postgres -c "ALTER SYSTEM SET local_preload_libraries TO '';"
ALTER SYSTEM

$ cat /usr/local/postgres-dev/testdb/postgresql.auto.conf
# Do not edit this file manually!
# It will be overwritten by the ALTER SYSTEM command.
local_preload_libraries = ''

... making a clear distinction between empty elements and empty lists:

postgres=# SET local_preload_libraries TO '','foo';
ERROR:  SET local_preload_libraries does not accept empty-string elements

postgres=# SET local_preload_libraries TO '';
SET
postgres=# SHOW local_preload_libraries;
 local_preload_libraries
-------------------------
 
(1 row)

The ambiguity between an empty list and an empty element has always
existed in list-valued GUCs. This patch resolves the issue by
disallowing empty elements, thereby making '' an unambiguous
representation of an empty list. Personally, I find SET var TO NULL (or
perhaps a keyword like EMPTY or NONE) a more palatable syntax for
expressing empty lists in this case. However, I’m not sure the
additional complexity and compatibility implications would justify such
a change.

Best regards, Jim

#13Andrei Klychkov
andrew.a.klychkov@gmail.com
In reply to: Jim Jones (#12)
1 attachment(s)
Re: [PATCH] Fix ALTER SYSTEM empty string bug for GUC_LIST_QUOTE parameters

As it solves the initial issue, it SGTM too.
I applied v3, updated the docs and added some tests in attached v4.
Hopefully it's OK.
Please take a look

Thanks
Regards

On Fri, Sep 5, 2025 at 9:33 AM Jim Jones <jim.jones@uni-muenster.de> wrote:

Show quoted text

On 04.09.25 23:52, Tom Lane wrote:

Note that the patch includes changing SplitIdentifierString and its
clones to forbid zero-length quoted elements, which were formerly
allowed. Without this, we'd accept values from config files that
could not be represented in SET, which is exactly the situation we
are trying to fix.

I'm not entirely sure if this is the way to go, or if we want to
adopt some other solution that doesn't involve forbidding empty
list elements. I suspect that anything else we come up with would
be less intuitive than letting SET list_var = '' do the job;
but maybe I just lack imagination today.

This approach LGTM. It solves the initial issue:

$ psql postgres -c "ALTER SYSTEM SET local_preload_libraries TO '';"
ALTER SYSTEM

$ cat /usr/local/postgres-dev/testdb/postgresql.auto.conf
# Do not edit this file manually!
# It will be overwritten by the ALTER SYSTEM command.
local_preload_libraries = ''

... making a clear distinction between empty elements and empty lists:

postgres=# SET local_preload_libraries TO '','foo';
ERROR: SET local_preload_libraries does not accept empty-string elements

postgres=# SET local_preload_libraries TO '';
SET
postgres=# SHOW local_preload_libraries;
local_preload_libraries
-------------------------

(1 row)

The ambiguity between an empty list and an empty element has always
existed in list-valued GUCs. This patch resolves the issue by
disallowing empty elements, thereby making '' an unambiguous
representation of an empty list. Personally, I find SET var TO NULL (or
perhaps a keyword like EMPTY or NONE) a more palatable syntax for
expressing empty lists in this case. However, I’m not sure the
additional complexity and compatibility implications would justify such
a change.

Best regards, Jim

Attachments:

v4-allow-SET-to-an-empty-list.patchtext/x-patch; charset=US-ASCII; name=v4-allow-SET-to-an-empty-list.patchDownload
From dd33d106fc67086dde59056d5ad39c61621b4dde Mon Sep 17 00:00:00 2001
From: Andrew Klychkov <andrew.a.klychkov@gmail.com>
Date: Fri, 5 Sep 2025 10:59:08 +0200
Subject: [PATCH] Allow SET to an empty list

---
 doc/src/sgml/config.sgml                      |  2 +
 doc/src/sgml/ref/set.sgml                     |  6 +++
 src/backend/utils/adt/ruleutils.c             | 10 +++--
 src/backend/utils/adt/varlena.c               | 12 ++++--
 src/backend/utils/misc/guc_funcs.c            | 43 ++++++++++++++++---
 src/bin/pg_dump/dumputils.c                   | 12 ++++--
 src/bin/pg_dump/pg_dump.c                     |  8 +++-
 src/test/modules/test_misc/meson.build        |  1 +
 .../modules/test_misc/t/009_alter_system.pl   | 31 +++++++++++++
 src/test/regress/expected/guc.out             | 18 ++++++++
 src/test/regress/expected/rules.out           | 28 ++++++------
 src/test/regress/sql/guc.sql                  |  8 ++++
 src/test/regress/sql/rules.sql                |  3 +-
 13 files changed, 150 insertions(+), 32 deletions(-)
 create mode 100644 src/test/modules/test_misc/t/009_alter_system.pl

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 0a4b3e55ba..8d2fba2a5e 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -52,6 +52,8 @@
        quotes within the value.  Quotes can usually be omitted if the value
        is a simple number or identifier, however.
        (Values that match an SQL keyword require quoting in some contexts.)
+       For parameters that take a list of strings, do not use an empty
+       string as an element, as that is not allowed.
       </para>
      </listitem>
 
diff --git a/doc/src/sgml/ref/set.sgml b/doc/src/sgml/ref/set.sgml
index 2218f54682..ce81993911 100644
--- a/doc/src/sgml/ref/set.sgml
+++ b/doc/src/sgml/ref/set.sgml
@@ -141,6 +141,12 @@ SET [ SESSION | LOCAL ] TIME ZONE { <replaceable class="parameter">value</replac
       value it would have had if no <command>SET</command> had been executed
       in the current session).
      </para>
+     <para>
+      For a parameter that takes a list of strings, a single empty string
+      (<literal>''</literal>) may be specified to set the parameter to an
+      empty list.  This is a special case; empty strings are not otherwise
+      allowed as elements of such lists.
+     </para>
     </listitem>
    </varlistentry>
   </variablelist>
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 3d6e6bdbfd..27ba1920e6 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -3086,9 +3086,10 @@ pg_get_functiondef(PG_FUNCTION_ARGS)
 				 * rules used there aren't exactly like SQL's, we have to
 				 * break the list value apart and then quote the elements as
 				 * string literals.  (The elements may be double-quoted as-is,
-				 * but we can't just feed them to the SQL parser; it would do
-				 * the wrong thing with elements that are zero-length or
-				 * longer than NAMEDATALEN.)
+				 * but we can't just feed them to the SQL parser that way; it
+				 * would truncate elements that are longer than NAMEDATALEN,
+				 * which would be wrong if they're paths.)  Also, we need a
+				 * special case for empty lists.
 				 *
 				 * Variables that are not so marked should just be emitted as
 				 * simple string literals.  If the variable is not known to
@@ -3106,6 +3107,9 @@ pg_get_functiondef(PG_FUNCTION_ARGS)
 						/* this shouldn't fail really */
 						elog(ERROR, "invalid list syntax in proconfig item");
 					}
+					/* Special case: represent an empty list as '' */
+					if (namelist == NIL)
+						appendStringInfoString(&buf, "''");
 					foreach(lc, namelist)
 					{
 						char	   *curname = (char *) lfirst(lc);
diff --git a/src/backend/utils/adt/varlena.c b/src/backend/utils/adt/varlena.c
index 2c398cd9e5..d5ef492ee4 100644
--- a/src/backend/utils/adt/varlena.c
+++ b/src/backend/utils/adt/varlena.c
@@ -2753,7 +2753,7 @@ SplitIdentifierString(char *rawstring, char separator,
 		nextp++;				/* skip leading whitespace */
 
 	if (*nextp == '\0')
-		return true;			/* allow empty string */
+		return true;			/* empty string represents empty list */
 
 	/* At the top of the loop, we are at start of a new identifier. */
 	do
@@ -2777,6 +2777,8 @@ SplitIdentifierString(char *rawstring, char separator,
 				nextp = endp;
 			}
 			/* endp now points at the terminating quote */
+			if (curname == endp)
+				return false;	/* empty quoted name not allowed */
 			nextp = endp + 1;
 		}
 		else
@@ -2880,7 +2882,7 @@ SplitDirectoriesString(char *rawstring, char separator,
 		nextp++;				/* skip leading whitespace */
 
 	if (*nextp == '\0')
-		return true;			/* allow empty string */
+		return true;			/* empty string represents empty list */
 
 	/* At the top of the loop, we are at start of a new directory. */
 	do
@@ -2904,6 +2906,8 @@ SplitDirectoriesString(char *rawstring, char separator,
 				nextp = endp;
 			}
 			/* endp now points at the terminating quote */
+			if (curname == endp)
+				return false;	/* empty quoted name not allowed */
 			nextp = endp + 1;
 		}
 		else
@@ -3001,7 +3005,7 @@ SplitGUCList(char *rawstring, char separator,
 		nextp++;				/* skip leading whitespace */
 
 	if (*nextp == '\0')
-		return true;			/* allow empty string */
+		return true;			/* empty string represents empty list */
 
 	/* At the top of the loop, we are at start of a new identifier. */
 	do
@@ -3025,6 +3029,8 @@ SplitGUCList(char *rawstring, char separator,
 				nextp = endp;
 			}
 			/* endp now points at the terminating quote */
+			if (curname == endp)
+				return false;	/* empty quoted name not allowed */
 			nextp = endp + 1;
 		}
 		else
diff --git a/src/backend/utils/misc/guc_funcs.c b/src/backend/utils/misc/guc_funcs.c
index b9e26982ab..a0557d164d 100644
--- a/src/backend/utils/misc/guc_funcs.c
+++ b/src/backend/utils/misc/guc_funcs.c
@@ -210,12 +210,30 @@ flatten_set_variable_args(const char *name, List *args)
 	else
 		flags = 0;
 
-	/* Complain if list input and non-list variable */
-	if ((flags & GUC_LIST_INPUT) == 0 &&
-		list_length(args) != 1)
-		ereport(ERROR,
-				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-				 errmsg("SET %s takes only one argument", name)));
+	/*
+	 * Handle special cases for list input.
+	 */
+	if (flags & GUC_LIST_INPUT)
+	{
+		/* A single empty-string item is treated as an empty list. */
+		if (list_length(args) == 1)
+		{
+			Node	   *arg = (Node *) linitial(args);
+
+			if (IsA(arg, A_Const) &&
+				nodeTag(&((A_Const *) arg)->val) == T_String &&
+				*strVal(&((A_Const *) arg)->val) == '\0')
+				return pstrdup("");
+		}
+	}
+	else
+	{
+		/* Complain if list input and non-list variable. */
+		if (list_length(args) != 1)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("SET %s takes only one argument", name)));
+	}
 
 	initStringInfo(&buf);
 
@@ -269,6 +287,9 @@ flatten_set_variable_args(const char *name, List *args)
 					Datum		interval;
 					char	   *intervalout;
 
+					/* gram.y ensures this is only reachable for TIME ZONE */
+					Assert(!(flags & GUC_LIST_QUOTE));
+
 					typenameTypeIdAndMod(NULL, typeName, &typoid, &typmod);
 					Assert(typoid == INTERVALOID);
 
@@ -286,9 +307,17 @@ flatten_set_variable_args(const char *name, List *args)
 				else
 				{
 					/*
-					 * Plain string literal or identifier.  For quote mode,
+					 * Plain string literal or identifier.  In a list GUC,
+					 * disallow empty-string elements (so that the preceding
+					 * hack for empty lists is unambiguous).  For quote mode,
 					 * quote it if it's not a vanilla identifier.
 					 */
+					if ((flags & GUC_LIST_INPUT) && *val == '\0')
+						ereport(ERROR,
+								(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+								 errmsg("SET %s does not accept empty-string elements",
+										name)));
+
 					if (flags & GUC_LIST_QUOTE)
 						appendStringInfoString(&buf, quote_identifier(val));
 					else
diff --git a/src/bin/pg_dump/dumputils.c b/src/bin/pg_dump/dumputils.c
index 05b84c0d6e..00a369c886 100644
--- a/src/bin/pg_dump/dumputils.c
+++ b/src/bin/pg_dump/dumputils.c
@@ -781,7 +781,7 @@ SplitGUCList(char *rawstring, char separator,
 		nextp++;				/* skip leading whitespace */
 
 	if (*nextp == '\0')
-		return true;			/* allow empty string */
+		return true;			/* empty string represents empty list */
 
 	/* At the top of the loop, we are at start of a new identifier. */
 	do
@@ -805,6 +805,8 @@ SplitGUCList(char *rawstring, char separator,
 				nextp = endp;
 			}
 			/* endp now points at the terminating quote */
+			if (curname == endp)
+				return false;	/* empty quoted name not allowed */
 			nextp = endp + 1;
 		}
 		else
@@ -891,8 +893,9 @@ makeAlterConfigCommand(PGconn *conn, const char *configitem,
 	 * array.  However, because the quoting rules used there aren't exactly
 	 * like SQL's, we have to break the list value apart and then quote the
 	 * elements as string literals.  (The elements may be double-quoted as-is,
-	 * but we can't just feed them to the SQL parser; it would do the wrong
-	 * thing with elements that are zero-length or longer than NAMEDATALEN.)
+	 * but we can't just feed them to the SQL parser that way; it would
+	 * truncate elements that are longer than NAMEDATALEN, which would be
+	 * wrong if they're paths.)  Also, we need a special case for empty lists.
 	 *
 	 * Variables that are not so marked should just be emitted as simple
 	 * string literals.  If the variable is not known to
@@ -908,6 +911,9 @@ makeAlterConfigCommand(PGconn *conn, const char *configitem,
 		/* this shouldn't fail really */
 		if (SplitGUCList(pos, ',', &namelist))
 		{
+			/* Special case: represent an empty list as '' */
+			if (*namelist == NULL)
+				appendPQExpBufferStr(buf, "''");
 			for (nameptr = namelist; *nameptr; nameptr++)
 			{
 				if (nameptr != namelist)
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index bea793456f..9d36a6a5aa 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -13699,8 +13699,9 @@ dumpFunc(Archive *fout, const FuncInfo *finfo)
 		 * aren't exactly like SQL's, we have to break the list value apart
 		 * and then quote the elements as string literals.  (The elements may
 		 * be double-quoted as-is, but we can't just feed them to the SQL
-		 * parser; it would do the wrong thing with elements that are
-		 * zero-length or longer than NAMEDATALEN.)
+		 * parser that way; it would truncate elements that are longer than
+		 * NAMEDATALEN, which would be wrong if they're paths.)  Also, we need
+		 * a special case for empty lists.
 		 *
 		 * Variables that are not so marked should just be emitted as simple
 		 * string literals.  If the variable is not known to
@@ -13716,6 +13717,9 @@ dumpFunc(Archive *fout, const FuncInfo *finfo)
 			/* this shouldn't fail really */
 			if (SplitGUCList(pos, ',', &namelist))
 			{
+				/* Special case: represent an empty list as '' */
+				if (*namelist == NULL)
+					appendPQExpBufferStr(q, "''");
 				for (nameptr = namelist; *nameptr; nameptr++)
 				{
 					if (nameptr != namelist)
diff --git a/src/test/modules/test_misc/meson.build b/src/test/modules/test_misc/meson.build
index 6b1e730bf4..40d50e245d 100644
--- a/src/test/modules/test_misc/meson.build
+++ b/src/test/modules/test_misc/meson.build
@@ -17,6 +17,7 @@ tests += {
       't/006_signal_autovacuum.pl',
       't/007_catcache_inval.pl',
       't/008_replslot_single_user.pl',
+      't/009_alter_system.pl.pl',
     ],
   },
 }
diff --git a/src/test/modules/test_misc/t/009_alter_system.pl b/src/test/modules/test_misc/t/009_alter_system.pl
new file mode 100644
index 0000000000..1f2d1e28e6
--- /dev/null
+++ b/src/test/modules/test_misc/t/009_alter_system.pl
@@ -0,0 +1,31 @@
+# Copyright (c) 2025, PostgreSQL Global Development Group
+#
+# Test ALTER SYSTEM for list-based GUCs
+use strict;
+use warnings FATAL => 'all';
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Initialize a new node and start it
+my $node = PostgreSQL::Test::Cluster->new('main');
+$node->init;
+$node->start;
+
+# Test setting a list GUC to an empty list
+$node->safe_psql('postgres', "ALTER SYSTEM SET search_path TO ''");
+$node->reload;
+
+my ($ret, $stdout, $stderr) = $node->psql('postgres', "SHOW search_path");
+is($stdout, '', 'ALTER SYSTEM SET search_path TO \'\' results in an empty list');
+
+# Test that ALTER SYSTEM rejects a list with an empty element
+($ret, $stdout, $stderr) = $node->psql('postgres', "ALTER SYSTEM SET search_path TO 'foo', ''");
+isnt($ret, 0, 'ALTER SYSTEM rejects list with empty element');
+like($stderr, qr/does not accept empty-string elements/, 'Correct error message for empty element in list');
+
+# Clean up GUC settings
+$node->safe_psql('postgres', "ALTER SYSTEM RESET search_path");
+$node->reload;
+
+done_testing(); 
diff --git a/src/test/regress/expected/guc.out b/src/test/regress/expected/guc.out
index 7f9e29c765..e49e609415 100644
--- a/src/test/regress/expected/guc.out
+++ b/src/test/regress/expected/guc.out
@@ -31,6 +31,24 @@ SELECT '2006-08-13 12:34:56'::timestamptz;
  2006-08-13 12:34:56-07
 (1 row)
 
+-- Check handling of list GUCs
+SET search_path = 'pg_catalog', Foo, 'Bar';
+SHOW search_path;
+      search_path       
+------------------------
+ pg_catalog, foo, "Bar"
+(1 row)
+
+SET search_path = '';  -- means empty list
+SHOW search_path;
+ search_path 
+-------------
+ 
+(1 row)
+
+SET search_path = '', 'foo';  -- error, empty list elements not OK
+ERROR:  SET search_path does not accept empty-string elements
+RESET search_path;
 -- SET LOCAL has no effect outside of a transaction
 SET LOCAL vacuum_cost_delay TO 50;
 WARNING:  SET LOCAL can only be used in transaction blocks
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 35e8aad770..43d5cf1026 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -3549,21 +3549,23 @@ CREATE FUNCTION func_with_set_params() RETURNS integer
     SET extra_float_digits TO 2
     SET work_mem TO '4MB'
     SET datestyle to iso, mdy
-    SET local_preload_libraries TO "Mixed/Case", 'c:/''a"/path', '', '0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789'
+    SET temp_tablespaces to ''
+    SET local_preload_libraries TO "Mixed/Case", 'c:/''a"/path', '0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789'
     IMMUTABLE STRICT;
 SELECT pg_get_functiondef('func_with_set_params()'::regprocedure);
-                                                                            pg_get_functiondef                                                                            
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------
- CREATE OR REPLACE FUNCTION public.func_with_set_params()                                                                                                                +
-  RETURNS integer                                                                                                                                                        +
-  LANGUAGE sql                                                                                                                                                           +
-  IMMUTABLE STRICT                                                                                                                                                       +
-  SET search_path TO 'pg_catalog'                                                                                                                                        +
-  SET extra_float_digits TO '2'                                                                                                                                          +
-  SET work_mem TO '4MB'                                                                                                                                                  +
-  SET "DateStyle" TO 'iso, mdy'                                                                                                                                          +
-  SET local_preload_libraries TO 'Mixed/Case', 'c:/''a"/path', '', '0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789'+
- AS $function$select 1;$function$                                                                                                                                        +
+                                                                          pg_get_functiondef                                                                          
+----------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE OR REPLACE FUNCTION public.func_with_set_params()                                                                                                            +
+  RETURNS integer                                                                                                                                                    +
+  LANGUAGE sql                                                                                                                                                       +
+  IMMUTABLE STRICT                                                                                                                                                   +
+  SET search_path TO 'pg_catalog'                                                                                                                                    +
+  SET extra_float_digits TO '2'                                                                                                                                      +
+  SET work_mem TO '4MB'                                                                                                                                              +
+  SET "DateStyle" TO 'iso, mdy'                                                                                                                                      +
+  SET temp_tablespaces TO ''                                                                                                                                         +
+  SET local_preload_libraries TO 'Mixed/Case', 'c:/''a"/path', '0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789'+
+ AS $function$select 1;$function$                                                                                                                                    +
  
 (1 row)
 
diff --git a/src/test/regress/sql/guc.sql b/src/test/regress/sql/guc.sql
index f65f84a263..65630135f1 100644
--- a/src/test/regress/sql/guc.sql
+++ b/src/test/regress/sql/guc.sql
@@ -12,6 +12,14 @@ SHOW vacuum_cost_delay;
 SHOW datestyle;
 SELECT '2006-08-13 12:34:56'::timestamptz;
 
+-- Check handling of list GUCs
+SET search_path = 'pg_catalog', Foo, 'Bar';
+SHOW search_path;
+SET search_path = '';  -- means empty list
+SHOW search_path;
+SET search_path = '', 'foo';  -- error, empty list elements not OK
+RESET search_path;
+
 -- SET LOCAL has no effect outside of a transaction
 SET LOCAL vacuum_cost_delay TO 50;
 SHOW vacuum_cost_delay;
diff --git a/src/test/regress/sql/rules.sql b/src/test/regress/sql/rules.sql
index fdd3ff1d16..5c5ff5e9cc 100644
--- a/src/test/regress/sql/rules.sql
+++ b/src/test/regress/sql/rules.sql
@@ -1217,7 +1217,8 @@ CREATE FUNCTION func_with_set_params() RETURNS integer
     SET extra_float_digits TO 2
     SET work_mem TO '4MB'
     SET datestyle to iso, mdy
-    SET local_preload_libraries TO "Mixed/Case", 'c:/''a"/path', '', '0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789'
+    SET temp_tablespaces to ''
+    SET local_preload_libraries TO "Mixed/Case", 'c:/''a"/path', '0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789'
     IMMUTABLE STRICT;
 SELECT pg_get_functiondef('func_with_set_params()'::regprocedure);
 
-- 
2.47.0

#14Tom Lane
tgl@sss.pgh.pa.us
In reply to: Jim Jones (#12)
1 attachment(s)
Re: [PATCH] Fix ALTER SYSTEM empty string bug for GUC_LIST_QUOTE parameters

Jim Jones <jim.jones@uni-muenster.de> writes:

On 04.09.25 23:52, Tom Lane wrote:

I'm not entirely sure if this is the way to go, or if we want to
adopt some other solution that doesn't involve forbidding empty
list elements. I suspect that anything else we come up with would
be less intuitive than letting SET list_var = '' do the job;
but maybe I just lack imagination today.

The ambiguity between an empty list and an empty element has always
existed in list-valued GUCs. This patch resolves the issue by
disallowing empty elements, thereby making '' an unambiguous
representation of an empty list. Personally, I find SET var TO NULL (or
perhaps a keyword like EMPTY or NONE) a more palatable syntax for
expressing empty lists in this case. However, I’m not sure the
additional complexity and compatibility implications would justify such
a change.

Since you expressed interest, I made a draft patch that does it like
that. Unsurprisingly, it has to touch mostly the same places that
the v3 patch did, plus the grammar. Still ends up a bit shorter
though.

I remain unsure which way I like better. The NULL approach has the
advantage of not foreclosing use of empty-string list elements, which
we might want someday even if there's no obvious value today. (And
for the same reason, it's less of a behavioral change.) But it still
feels a bit less intuitive to me. It might flow better with some
other keyword --- but we have to use a fully-reserved keyword, and we
are surely not going to make a new one of those just for this purpose,
and NULL is the only existing one that's even slightly on-point.

regards, tom lane

Attachments:

v5-allow-SET-to-an-empty-list.patchtext/x-diff; charset=us-ascii; name=v5-allow-SET-to-an-empty-list.patchDownload
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 9fd48acb1f8..ff31653b6ac 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -1709,6 +1709,26 @@ generic_set:
 					n->location = @3;
 					$$ = n;
 				}
+			| var_name TO NULL_P
+				{
+					VariableSetStmt *n = makeNode(VariableSetStmt);
+
+					n->kind = VAR_SET_VALUE;
+					n->name = $1;
+					n->args = list_make1(makeNullAConst(@3));
+					n->location = @3;
+					$$ = n;
+				}
+			| var_name '=' NULL_P
+				{
+					VariableSetStmt *n = makeNode(VariableSetStmt);
+
+					n->kind = VAR_SET_VALUE;
+					n->name = $1;
+					n->args = list_make1(makeNullAConst(@3));
+					n->location = @3;
+					$$ = n;
+				}
 			| var_name TO DEFAULT
 				{
 					VariableSetStmt *n = makeNode(VariableSetStmt);
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 3d6e6bdbfd2..f81cfd17fd0 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -3088,7 +3088,8 @@ pg_get_functiondef(PG_FUNCTION_ARGS)
 				 * string literals.  (The elements may be double-quoted as-is,
 				 * but we can't just feed them to the SQL parser; it would do
 				 * the wrong thing with elements that are zero-length or
-				 * longer than NAMEDATALEN.)
+				 * longer than NAMEDATALEN.)  Also, we need a special case for
+				 * empty lists.
 				 *
 				 * Variables that are not so marked should just be emitted as
 				 * simple string literals.  If the variable is not known to
@@ -3106,6 +3107,9 @@ pg_get_functiondef(PG_FUNCTION_ARGS)
 						/* this shouldn't fail really */
 						elog(ERROR, "invalid list syntax in proconfig item");
 					}
+					/* Special case: represent an empty list as NULL */
+					if (namelist == NIL)
+						appendStringInfoString(&buf, "NULL");
 					foreach(lc, namelist)
 					{
 						char	   *curname = (char *) lfirst(lc);
diff --git a/src/backend/utils/adt/varlena.c b/src/backend/utils/adt/varlena.c
index 2c398cd9e5c..1e1f69f7528 100644
--- a/src/backend/utils/adt/varlena.c
+++ b/src/backend/utils/adt/varlena.c
@@ -2753,7 +2753,7 @@ SplitIdentifierString(char *rawstring, char separator,
 		nextp++;				/* skip leading whitespace */
 
 	if (*nextp == '\0')
-		return true;			/* allow empty string */
+		return true;			/* empty string represents empty list */
 
 	/* At the top of the loop, we are at start of a new identifier. */
 	do
@@ -2880,7 +2880,7 @@ SplitDirectoriesString(char *rawstring, char separator,
 		nextp++;				/* skip leading whitespace */
 
 	if (*nextp == '\0')
-		return true;			/* allow empty string */
+		return true;			/* empty string represents empty list */
 
 	/* At the top of the loop, we are at start of a new directory. */
 	do
@@ -3001,7 +3001,7 @@ SplitGUCList(char *rawstring, char separator,
 		nextp++;				/* skip leading whitespace */
 
 	if (*nextp == '\0')
-		return true;			/* allow empty string */
+		return true;			/* empty string represents empty list */
 
 	/* At the top of the loop, we are at start of a new identifier. */
 	do
diff --git a/src/backend/utils/misc/guc_funcs.c b/src/backend/utils/misc/guc_funcs.c
index b9e26982abd..d0f4e396acc 100644
--- a/src/backend/utils/misc/guc_funcs.c
+++ b/src/backend/utils/misc/guc_funcs.c
@@ -210,12 +210,29 @@ flatten_set_variable_args(const char *name, List *args)
 	else
 		flags = 0;
 
-	/* Complain if list input and non-list variable */
-	if ((flags & GUC_LIST_INPUT) == 0 &&
-		list_length(args) != 1)
-		ereport(ERROR,
-				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-				 errmsg("SET %s takes only one argument", name)));
+	/*
+	 * Handle special cases for list input.
+	 */
+	if (flags & GUC_LIST_INPUT)
+	{
+		/* NULL represents an empty list. */
+		if (list_length(args) == 1)
+		{
+			Node	   *arg = (Node *) linitial(args);
+
+			if (IsA(arg, A_Const) &&
+				((A_Const *) arg)->isnull)
+				return pstrdup("");
+		}
+	}
+	else
+	{
+		/* Complain if list input and non-list variable. */
+		if (list_length(args) != 1)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("SET %s takes only one argument", name)));
+	}
 
 	initStringInfo(&buf);
 
@@ -246,6 +263,12 @@ flatten_set_variable_args(const char *name, List *args)
 			elog(ERROR, "unrecognized node type: %d", (int) nodeTag(arg));
 		con = (A_Const *) arg;
 
+		/* Complain if NULL is used with a non-list variable. */
+		if (con->isnull)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("NULL is an invalid value for %s", name)));
+
 		switch (nodeTag(&con->val))
 		{
 			case T_Integer:
@@ -269,6 +292,9 @@ flatten_set_variable_args(const char *name, List *args)
 					Datum		interval;
 					char	   *intervalout;
 
+					/* gram.y ensures this is only reachable for TIME ZONE */
+					Assert(!(flags & GUC_LIST_QUOTE));
+
 					typenameTypeIdAndMod(NULL, typeName, &typoid, &typmod);
 					Assert(typoid == INTERVALOID);
 
diff --git a/src/bin/pg_dump/dumputils.c b/src/bin/pg_dump/dumputils.c
index 05b84c0d6e7..2d22723aa91 100644
--- a/src/bin/pg_dump/dumputils.c
+++ b/src/bin/pg_dump/dumputils.c
@@ -781,7 +781,7 @@ SplitGUCList(char *rawstring, char separator,
 		nextp++;				/* skip leading whitespace */
 
 	if (*nextp == '\0')
-		return true;			/* allow empty string */
+		return true;			/* empty string represents empty list */
 
 	/* At the top of the loop, we are at start of a new identifier. */
 	do
@@ -893,6 +893,7 @@ makeAlterConfigCommand(PGconn *conn, const char *configitem,
 	 * elements as string literals.  (The elements may be double-quoted as-is,
 	 * but we can't just feed them to the SQL parser; it would do the wrong
 	 * thing with elements that are zero-length or longer than NAMEDATALEN.)
+	 * Also, we need a special case for empty lists.
 	 *
 	 * Variables that are not so marked should just be emitted as simple
 	 * string literals.  If the variable is not known to
@@ -908,6 +909,9 @@ makeAlterConfigCommand(PGconn *conn, const char *configitem,
 		/* this shouldn't fail really */
 		if (SplitGUCList(pos, ',', &namelist))
 		{
+			/* Special case: represent an empty list as NULL */
+			if (*namelist == NULL)
+				appendPQExpBufferStr(buf, "NULL");
 			for (nameptr = namelist; *nameptr; nameptr++)
 			{
 				if (nameptr != namelist)
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index bea793456f9..08fcfcfbdfe 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -13700,7 +13700,8 @@ dumpFunc(Archive *fout, const FuncInfo *finfo)
 		 * and then quote the elements as string literals.  (The elements may
 		 * be double-quoted as-is, but we can't just feed them to the SQL
 		 * parser; it would do the wrong thing with elements that are
-		 * zero-length or longer than NAMEDATALEN.)
+		 * zero-length or longer than NAMEDATALEN.)  Also, we need a special
+		 * case for empty lists.
 		 *
 		 * Variables that are not so marked should just be emitted as simple
 		 * string literals.  If the variable is not known to
@@ -13716,6 +13717,9 @@ dumpFunc(Archive *fout, const FuncInfo *finfo)
 			/* this shouldn't fail really */
 			if (SplitGUCList(pos, ',', &namelist))
 			{
+				/* Special case: represent an empty list as NULL */
+				if (*namelist == NULL)
+					appendPQExpBufferStr(q, "NULL");
 				for (nameptr = namelist; *nameptr; nameptr++)
 				{
 					if (nameptr != namelist)
diff --git a/src/test/regress/expected/guc.out b/src/test/regress/expected/guc.out
index 7f9e29c765c..d6fb879f500 100644
--- a/src/test/regress/expected/guc.out
+++ b/src/test/regress/expected/guc.out
@@ -31,6 +31,28 @@ SELECT '2006-08-13 12:34:56'::timestamptz;
  2006-08-13 12:34:56-07
 (1 row)
 
+-- Check handling of list GUCs
+SET search_path = 'pg_catalog', Foo, 'Bar', '';
+SHOW search_path;
+        search_path         
+----------------------------
+ pg_catalog, foo, "Bar", ""
+(1 row)
+
+SET search_path = null;  -- means empty list
+SHOW search_path;
+ search_path 
+-------------
+ 
+(1 row)
+
+SET search_path = null, null;  -- syntax error
+ERROR:  syntax error at or near ","
+LINE 1: SET search_path = null, null;
+                              ^
+SET enable_seqscan = null;  -- error
+ERROR:  NULL is an invalid value for enable_seqscan
+RESET search_path;
 -- SET LOCAL has no effect outside of a transaction
 SET LOCAL vacuum_cost_delay TO 50;
 WARNING:  SET LOCAL can only be used in transaction blocks
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 35e8aad7701..3cc5f8a77b2 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -3549,6 +3549,7 @@ CREATE FUNCTION func_with_set_params() RETURNS integer
     SET extra_float_digits TO 2
     SET work_mem TO '4MB'
     SET datestyle to iso, mdy
+    SET temp_tablespaces to NULL
     SET local_preload_libraries TO "Mixed/Case", 'c:/''a"/path', '', '0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789'
     IMMUTABLE STRICT;
 SELECT pg_get_functiondef('func_with_set_params()'::regprocedure);
@@ -3562,6 +3563,7 @@ SELECT pg_get_functiondef('func_with_set_params()'::regprocedure);
   SET extra_float_digits TO '2'                                                                                                                                          +
   SET work_mem TO '4MB'                                                                                                                                                  +
   SET "DateStyle" TO 'iso, mdy'                                                                                                                                          +
+  SET temp_tablespaces TO NULL                                                                                                                                           +
   SET local_preload_libraries TO 'Mixed/Case', 'c:/''a"/path', '', '0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789'+
  AS $function$select 1;$function$                                                                                                                                        +
  
diff --git a/src/test/regress/sql/guc.sql b/src/test/regress/sql/guc.sql
index f65f84a2632..bafaf067e82 100644
--- a/src/test/regress/sql/guc.sql
+++ b/src/test/regress/sql/guc.sql
@@ -12,6 +12,15 @@ SHOW vacuum_cost_delay;
 SHOW datestyle;
 SELECT '2006-08-13 12:34:56'::timestamptz;
 
+-- Check handling of list GUCs
+SET search_path = 'pg_catalog', Foo, 'Bar', '';
+SHOW search_path;
+SET search_path = null;  -- means empty list
+SHOW search_path;
+SET search_path = null, null;  -- syntax error
+SET enable_seqscan = null;  -- error
+RESET search_path;
+
 -- SET LOCAL has no effect outside of a transaction
 SET LOCAL vacuum_cost_delay TO 50;
 SHOW vacuum_cost_delay;
diff --git a/src/test/regress/sql/rules.sql b/src/test/regress/sql/rules.sql
index fdd3ff1d161..3f240bec7b0 100644
--- a/src/test/regress/sql/rules.sql
+++ b/src/test/regress/sql/rules.sql
@@ -1217,6 +1217,7 @@ CREATE FUNCTION func_with_set_params() RETURNS integer
     SET extra_float_digits TO 2
     SET work_mem TO '4MB'
     SET datestyle to iso, mdy
+    SET temp_tablespaces to NULL
     SET local_preload_libraries TO "Mixed/Case", 'c:/''a"/path', '', '0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789'
     IMMUTABLE STRICT;
 SELECT pg_get_functiondef('func_with_set_params()'::regprocedure);
#15Jim Jones
jim.jones@uni-muenster.de
In reply to: Tom Lane (#14)
Re: [PATCH] Fix ALTER SYSTEM empty string bug for GUC_LIST_QUOTE parameters

Hi Tom

On 05.09.25 23:06, Tom Lane wrote:

I remain unsure which way I like better. The NULL approach has the
advantage of not foreclosing use of empty-string list elements, which
we might want someday even if there's no obvious value today. (And
for the same reason, it's less of a behavioral change.) But it still
feels a bit less intuitive to me.

I think this is a nice addition. The way I see it is: it provides an
unambiguous way to "clear" the variable, which, as you pointed out,
might carry different semantics in the future than an empty string. More
generally, I understand that using NULL (unknown/undefined) to represent
an empty list could be seen as a semantic stretch, but in this case it
doesn’t feel unintuitive to me. Although I prefer this new syntax, I can
definitely live without it :)

Here some tests:

== ALTER SYSTEM SET var TO ''

$ psql postgres -c "ALTER SYSTEM SET local_preload_libraries TO '';"
ALTER SYSTEM

$ cat /usr/local/postgres-dev/testdb/postgresql.auto.conf
# Do not edit this file manually!
# It will be overwritten by the ALTER SYSTEM command.
local_preload_libraries = '""'

$ pg_ctl -D /usr/local/postgres-dev/testdb -l
/usr/local/postgres-dev/logfile restart
waiting for server to shut down.... done
server stopped
waiting for server to start.... done
server started

$ psql postgres
psql: error: connection to server on socket "/tmp/.s.PGSQL.5432" failed:
FATAL:  could not access file "$libdir/plugins/": No such file or directory

The error itself is expected, but the message does not make it
immediately clear that the problem comes from a misconfigured GUC. But
this seems to be more related to the specific variable than to the scope
of this patch.

== ALTER SYSTEM SET var TO NULL
Using the new syntax it works just fine:

$ psql postgres -c "ALTER SYSTEM SET local_preload_libraries TO NULL;"
ALTER SYSTEM

$ cat /usr/local/postgres-dev/testdb/postgresql.auto.conf
# Do not edit this file manually!
# It will be overwritten by the ALTER SYSTEM command.
local_preload_libraries = ''

$ pg_ctl -D /usr/local/postgres-dev/testdb -l
/usr/local/postgres-dev/logfile restart
waiting for server to shut down.... done
server stopped
waiting for server to start.... done
server started

$ psql postgres -c "SHOW local_preload_libraries;"
 local_preload_libraries
-------------------------
 
(1 row)

== SET var TO ''

$ psql postgres -c "SET local_preload_libraries TO ''; SHOW
local_preload_libraries;"
SET
 local_preload_libraries
-------------------------
 ""
(1 row)

== SET var TO NULL

$ psql postgres -c "SET local_preload_libraries TO NULL; SHOW
local_preload_libraries;"
SET
 local_preload_libraries
-------------------------
 
(1 row)

== SET var TO list containing empty element

$ psql postgres -c "SET local_preload_libraries TO 'foo',''; SHOW
local_preload_libraries;"
SET
 local_preload_libraries
-------------------------
 foo, ""
(1 row)

== SET var TO list containing NULL element

$ psql postgres -c "SET local_preload_libraries TO NULL,''; SHOW
local_preload_libraries;"
ERROR:  syntax error at or near ","
LINE 1: SET local_preload_libraries TO NULL,''; SHOW local_preload_l...

== SET var TO list containing multiple empty elements

$ /usr/local/postgres-dev/bin/psql postgres -c "SET
local_preload_libraries TO '',''; SHOW local_preload_libraries;"
SET
 local_preload_libraries
-------------------------
 "", ""
(1 row)

Best regards, Jim

#16Andrei Klychkov
andrew.a.klychkov@gmail.com
In reply to: Jim Jones (#15)
Re: [PATCH] Fix ALTER SYSTEM empty string bug for GUC_LIST_QUOTE parameters

Hello,

On 05.09.25 23:06, Tom Lane wrote:

I remain unsure which way I like better. The NULL approach has the
advantage of not foreclosing use of empty-string list elements, which
we might want someday even if there's no obvious value today. (And
for the same reason, it's less of a behavioral change.) But it still
feels a bit less intuitive to me.

Looks like the patch v5 resolves a long-standing limitation where there was
no SQL syntax to set a list-based GUC to an empty list. I like this
approach. Also the changes seem non-breaking. Thanks

1. Would be great to have some explanations in docs about this new behavior.

2. It doesn't look to me that v5 solves the original issue of a user
running ALTER SYSTEM SET <setting like shared_preload_libraries> = ''; ,
then restarting the server and not getting it back online.

Regards
Andrew

On Sat, Sep 6, 2025 at 4:44 PM Jim Jones <jim.jones@uni-muenster.de> wrote:

Show quoted text

Hi Tom

On 05.09.25 23:06, Tom Lane wrote:

I remain unsure which way I like better. The NULL approach has the
advantage of not foreclosing use of empty-string list elements, which
we might want someday even if there's no obvious value today. (And
for the same reason, it's less of a behavioral change.) But it still
feels a bit less intuitive to me.

I think this is a nice addition. The way I see it is: it provides an
unambiguous way to "clear" the variable, which, as you pointed out,
might carry different semantics in the future than an empty string. More
generally, I understand that using NULL (unknown/undefined) to represent
an empty list could be seen as a semantic stretch, but in this case it
doesn’t feel unintuitive to me. Although I prefer this new syntax, I can
definitely live without it :)

Here some tests:

== ALTER SYSTEM SET var TO ''

$ psql postgres -c "ALTER SYSTEM SET local_preload_libraries TO '';"
ALTER SYSTEM

$ cat /usr/local/postgres-dev/testdb/postgresql.auto.conf
# Do not edit this file manually!
# It will be overwritten by the ALTER SYSTEM command.
local_preload_libraries = '""'

$ pg_ctl -D /usr/local/postgres-dev/testdb -l
/usr/local/postgres-dev/logfile restart
waiting for server to shut down.... done
server stopped
waiting for server to start.... done
server started

$ psql postgres
psql: error: connection to server on socket "/tmp/.s.PGSQL.5432" failed:
FATAL: could not access file "$libdir/plugins/": No such file or directory

The error itself is expected, but the message does not make it
immediately clear that the problem comes from a misconfigured GUC. But
this seems to be more related to the specific variable than to the scope
of this patch.

== ALTER SYSTEM SET var TO NULL
Using the new syntax it works just fine:

$ psql postgres -c "ALTER SYSTEM SET local_preload_libraries TO NULL;"
ALTER SYSTEM

$ cat /usr/local/postgres-dev/testdb/postgresql.auto.conf
# Do not edit this file manually!
# It will be overwritten by the ALTER SYSTEM command.
local_preload_libraries = ''

$ pg_ctl -D /usr/local/postgres-dev/testdb -l
/usr/local/postgres-dev/logfile restart
waiting for server to shut down.... done
server stopped
waiting for server to start.... done
server started

$ psql postgres -c "SHOW local_preload_libraries;"
local_preload_libraries
-------------------------

(1 row)

== SET var TO ''

$ psql postgres -c "SET local_preload_libraries TO ''; SHOW
local_preload_libraries;"
SET
local_preload_libraries
-------------------------
""
(1 row)

== SET var TO NULL

$ psql postgres -c "SET local_preload_libraries TO NULL; SHOW
local_preload_libraries;"
SET
local_preload_libraries
-------------------------

(1 row)

== SET var TO list containing empty element

$ psql postgres -c "SET local_preload_libraries TO 'foo',''; SHOW
local_preload_libraries;"
SET
local_preload_libraries
-------------------------
foo, ""
(1 row)

== SET var TO list containing NULL element

$ psql postgres -c "SET local_preload_libraries TO NULL,''; SHOW
local_preload_libraries;"
ERROR: syntax error at or near ","
LINE 1: SET local_preload_libraries TO NULL,''; SHOW local_preload_l...

== SET var TO list containing multiple empty elements

$ /usr/local/postgres-dev/bin/psql postgres -c "SET
local_preload_libraries TO '',''; SHOW local_preload_libraries;"
SET
local_preload_libraries
-------------------------
"", ""
(1 row)

Best regards, Jim

#17Tom Lane
tgl@sss.pgh.pa.us
In reply to: Andrei Klychkov (#16)
1 attachment(s)
Re: [PATCH] Fix ALTER SYSTEM empty string bug for GUC_LIST_QUOTE parameters

Andrei Klychkov <andrew.a.klychkov@gmail.com> writes:

Looks like the patch v5 resolves a long-standing limitation where there was
no SQL syntax to set a list-based GUC to an empty list. I like this
approach. Also the changes seem non-breaking. Thanks

1. Would be great to have some explanations in docs about this new behavior.

Yeah, I hadn't bothered with docs, pending decisions about which way
we were going to implement this. But it seems like we're leaning
towards using the NULL syntax, so I went ahead and did some docs work.

2. It doesn't look to me that v5 solves the original issue of a user
running ALTER SYSTEM SET <setting like shared_preload_libraries> = ''; ,
then restarting the server and not getting it back online.

[ shrug... ] It's not supposed to "solve" that. That command is
erroneous, and if you didn't test the setting before restarting the
server, you shouldn't be too surprised if restart fails. What this
patch is meant to do is provide a valid way to accomplish what you
wanted, namely

ALTER SYSTEM SET shared_preload_libraries = NULL;

Anyway, the attached v6 is the same as v5, except now with proposed
doc changes and a draft commit message. I spent some effort on
getting the ALTER SYSTEM and SET ref pages back into sync; it seemed
like more care has been taken with the ALTER SYSTEM synopsis and
other details.

I'm not sure if we want to change anything about this in config.sgml.
There are enough GUC_LIST_INPUT GUCs that I can't see mentioning it
for each one.

regards, tom lane

Attachments:

v6-0001-Allow-SET-list_guc-TO-NULL-to-specify-setting-the.patchtext/x-diff; charset=us-ascii; name*0=v6-0001-Allow-SET-list_guc-TO-NULL-to-specify-setting-the.p; name*1=atchDownload
From 78044b38eda13ee6abe49ac7cd4c689ad7f58b9c Mon Sep 17 00:00:00 2001
From: Tom Lane <tgl@sss.pgh.pa.us>
Date: Sat, 1 Nov 2025 15:59:00 -0400
Subject: [PATCH v6] Allow "SET list_guc TO NULL" to specify setting the GUC to
 empty.

We have never had a SET syntax that allows setting a GUC_LIST_INPUT
parameter to be an empty list.  A locution such as
	SET search_path = '';
doesn't mean that; it means setting the GUC to contain a single item
that is an empty string.  (For search_path the net effect is much the
same, because search_path ignores invalid schema names and '' must be
invalid.)  This is confusing, not least because configuration-file
entries and the set_config() function can easily produce empty-list
values.

We debated making the above syntax work, but that would foreclose
ever allowing empty-string items to be valid in list GUCs.  While
there isn't any obvious use-case for that today, it feels like the
kind of restriction that might hurt someday.  Instead, let's accept
the forbidden-up-to-now value NULL and treat that as meaning an
empty list.  (An objection to this could be "what if we someday want
to allow NULL as a GUC value?".  That seems unlikely though, and even
if we did allow it for scalar GUCs, we could continue to treat it as
meaning an empty list for list GUCs.)
---
 doc/src/sgml/ref/alter_system.sgml  |  2 ++
 doc/src/sgml/ref/set.sgml           | 24 +++++++++++++++---
 src/backend/parser/gram.y           | 20 +++++++++++++++
 src/backend/utils/adt/ruleutils.c   |  6 ++++-
 src/backend/utils/adt/varlena.c     |  6 ++---
 src/backend/utils/misc/guc_funcs.c  | 38 ++++++++++++++++++++++++-----
 src/bin/pg_dump/dumputils.c         |  6 ++++-
 src/bin/pg_dump/pg_dump.c           |  6 ++++-
 src/test/regress/expected/guc.out   | 22 +++++++++++++++++
 src/test/regress/expected/rules.out |  2 ++
 src/test/regress/sql/guc.sql        |  9 +++++++
 src/test/regress/sql/rules.sql      |  1 +
 12 files changed, 126 insertions(+), 16 deletions(-)

diff --git a/doc/src/sgml/ref/alter_system.sgml b/doc/src/sgml/ref/alter_system.sgml
index 1bde66d6ad2..65676e5c1c5 100644
--- a/doc/src/sgml/ref/alter_system.sgml
+++ b/doc/src/sgml/ref/alter_system.sgml
@@ -84,6 +84,8 @@ ALTER SYSTEM RESET ALL
       constants, identifiers, numbers, or comma-separated lists of
       these, as appropriate for the particular parameter.
       Values that are neither numbers nor valid identifiers must be quoted.
+      If the parameter accepts a list of values, <literal>NULL</literal>
+      can be written to specify an empty list.
       <literal>DEFAULT</literal> can be written to specify removing the
       parameter and its value from <filename>postgresql.auto.conf</filename>.
      </para>
diff --git a/doc/src/sgml/ref/set.sgml b/doc/src/sgml/ref/set.sgml
index 2218f54682e..16c7e9a7b26 100644
--- a/doc/src/sgml/ref/set.sgml
+++ b/doc/src/sgml/ref/set.sgml
@@ -21,8 +21,8 @@ PostgreSQL documentation
 
  <refsynopsisdiv>
 <synopsis>
-SET [ SESSION | LOCAL ] <replaceable class="parameter">configuration_parameter</replaceable> { TO | = } { <replaceable class="parameter">value</replaceable> | '<replaceable class="parameter">value</replaceable>' | DEFAULT }
-SET [ SESSION | LOCAL ] TIME ZONE { <replaceable class="parameter">value</replaceable> | '<replaceable class="parameter">value</replaceable>' | LOCAL | DEFAULT }
+SET [ SESSION | LOCAL ] <replaceable class="parameter">configuration_parameter</replaceable> { TO | = } { <replaceable class="parameter">value</replaceable> [, ...] | DEFAULT }
+SET [ SESSION | LOCAL ] TIME ZONE { <replaceable class="parameter">value</replaceable> | LOCAL | DEFAULT }
 </synopsis>
  </refsynopsisdiv>
 
@@ -123,7 +123,7 @@ SET [ SESSION | LOCAL ] TIME ZONE { <replaceable class="parameter">value</replac
     <term><replaceable class="parameter">configuration_parameter</replaceable></term>
     <listitem>
      <para>
-      Name of a settable run-time parameter.  Available parameters are
+      Name of a settable configuration parameter.  Available parameters are
       documented in <xref linkend="runtime-config"/> and below.
      </para>
     </listitem>
@@ -133,9 +133,12 @@ SET [ SESSION | LOCAL ] TIME ZONE { <replaceable class="parameter">value</replac
     <term><replaceable class="parameter">value</replaceable></term>
     <listitem>
      <para>
-      New value of parameter.  Values can be specified as string
+      New value of the parameter.  Values can be specified as string
       constants, identifiers, numbers, or comma-separated lists of
       these, as appropriate for the particular parameter.
+      Values that are neither numbers nor valid identifiers must be quoted.
+      If the parameter accepts a list of values, <literal>NULL</literal>
+      can be written to specify an empty list.
       <literal>DEFAULT</literal> can be written to specify
       resetting the parameter to its default value (that is, whatever
       value it would have had if no <command>SET</command> had been executed
@@ -283,6 +286,19 @@ SELECT setseed(<replaceable>value</replaceable>);
    Set the schema search path:
 <programlisting>
 SET search_path TO my_schema, public;
+</programlisting>
+   Note that this is not the same as
+<programlisting>
+SET search_path TO 'my_schema, public';
+</programlisting>
+   which would have the effect of setting <varname>search_path</varname>
+   to contain a single, probably-nonexistent schema name.
+  </para>
+
+  <para>
+   Set the list of temporary tablespace names to be empty:
+<programlisting>
+SET temp_tablespaces TO NULL;
 </programlisting>
   </para>
 
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index a4b29c822e8..50828a27053 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -1716,6 +1716,26 @@ generic_set:
 					n->location = @3;
 					$$ = n;
 				}
+			| var_name TO NULL_P
+				{
+					VariableSetStmt *n = makeNode(VariableSetStmt);
+
+					n->kind = VAR_SET_VALUE;
+					n->name = $1;
+					n->args = list_make1(makeNullAConst(@3));
+					n->location = @3;
+					$$ = n;
+				}
+			| var_name '=' NULL_P
+				{
+					VariableSetStmt *n = makeNode(VariableSetStmt);
+
+					n->kind = VAR_SET_VALUE;
+					n->name = $1;
+					n->args = list_make1(makeNullAConst(@3));
+					n->location = @3;
+					$$ = n;
+				}
 			| var_name TO DEFAULT
 				{
 					VariableSetStmt *n = makeNode(VariableSetStmt);
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 79ec136231b..5398679cce2 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -3087,7 +3087,8 @@ pg_get_functiondef(PG_FUNCTION_ARGS)
 				 * string literals.  (The elements may be double-quoted as-is,
 				 * but we can't just feed them to the SQL parser; it would do
 				 * the wrong thing with elements that are zero-length or
-				 * longer than NAMEDATALEN.)
+				 * longer than NAMEDATALEN.)  Also, we need a special case for
+				 * empty lists.
 				 *
 				 * Variables that are not so marked should just be emitted as
 				 * simple string literals.  If the variable is not known to
@@ -3105,6 +3106,9 @@ pg_get_functiondef(PG_FUNCTION_ARGS)
 						/* this shouldn't fail really */
 						elog(ERROR, "invalid list syntax in proconfig item");
 					}
+					/* Special case: represent an empty list as NULL */
+					if (namelist == NIL)
+						appendStringInfoString(&buf, "NULL");
 					foreach(lc, namelist)
 					{
 						char	   *curname = (char *) lfirst(lc);
diff --git a/src/backend/utils/adt/varlena.c b/src/backend/utils/adt/varlena.c
index 8d735786e51..3894457ab40 100644
--- a/src/backend/utils/adt/varlena.c
+++ b/src/backend/utils/adt/varlena.c
@@ -2753,7 +2753,7 @@ SplitIdentifierString(char *rawstring, char separator,
 		nextp++;				/* skip leading whitespace */
 
 	if (*nextp == '\0')
-		return true;			/* allow empty string */
+		return true;			/* empty string represents empty list */
 
 	/* At the top of the loop, we are at start of a new identifier. */
 	do
@@ -2880,7 +2880,7 @@ SplitDirectoriesString(char *rawstring, char separator,
 		nextp++;				/* skip leading whitespace */
 
 	if (*nextp == '\0')
-		return true;			/* allow empty string */
+		return true;			/* empty string represents empty list */
 
 	/* At the top of the loop, we are at start of a new directory. */
 	do
@@ -3001,7 +3001,7 @@ SplitGUCList(char *rawstring, char separator,
 		nextp++;				/* skip leading whitespace */
 
 	if (*nextp == '\0')
-		return true;			/* allow empty string */
+		return true;			/* empty string represents empty list */
 
 	/* At the top of the loop, we are at start of a new identifier. */
 	do
diff --git a/src/backend/utils/misc/guc_funcs.c b/src/backend/utils/misc/guc_funcs.c
index 4f58fa3d4e0..9dbc5d3aeb9 100644
--- a/src/backend/utils/misc/guc_funcs.c
+++ b/src/backend/utils/misc/guc_funcs.c
@@ -210,12 +210,29 @@ flatten_set_variable_args(const char *name, List *args)
 	else
 		flags = 0;
 
-	/* Complain if list input and non-list variable */
-	if ((flags & GUC_LIST_INPUT) == 0 &&
-		list_length(args) != 1)
-		ereport(ERROR,
-				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-				 errmsg("SET %s takes only one argument", name)));
+	/*
+	 * Handle special cases for list input.
+	 */
+	if (flags & GUC_LIST_INPUT)
+	{
+		/* NULL represents an empty list. */
+		if (list_length(args) == 1)
+		{
+			Node	   *arg = (Node *) linitial(args);
+
+			if (IsA(arg, A_Const) &&
+				((A_Const *) arg)->isnull)
+				return pstrdup("");
+		}
+	}
+	else
+	{
+		/* Complain if list input and non-list variable. */
+		if (list_length(args) != 1)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("SET %s takes only one argument", name)));
+	}
 
 	initStringInfo(&buf);
 
@@ -246,6 +263,12 @@ flatten_set_variable_args(const char *name, List *args)
 			elog(ERROR, "unrecognized node type: %d", (int) nodeTag(arg));
 		con = (A_Const *) arg;
 
+		/* Complain if NULL is used with a non-list variable. */
+		if (con->isnull)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("NULL is an invalid value for %s", name)));
+
 		switch (nodeTag(&con->val))
 		{
 			case T_Integer:
@@ -269,6 +292,9 @@ flatten_set_variable_args(const char *name, List *args)
 					Datum		interval;
 					char	   *intervalout;
 
+					/* gram.y ensures this is only reachable for TIME ZONE */
+					Assert(!(flags & GUC_LIST_QUOTE));
+
 					typenameTypeIdAndMod(NULL, typeName, &typoid, &typmod);
 					Assert(typoid == INTERVALOID);
 
diff --git a/src/bin/pg_dump/dumputils.c b/src/bin/pg_dump/dumputils.c
index 05b84c0d6e7..2d22723aa91 100644
--- a/src/bin/pg_dump/dumputils.c
+++ b/src/bin/pg_dump/dumputils.c
@@ -781,7 +781,7 @@ SplitGUCList(char *rawstring, char separator,
 		nextp++;				/* skip leading whitespace */
 
 	if (*nextp == '\0')
-		return true;			/* allow empty string */
+		return true;			/* empty string represents empty list */
 
 	/* At the top of the loop, we are at start of a new identifier. */
 	do
@@ -893,6 +893,7 @@ makeAlterConfigCommand(PGconn *conn, const char *configitem,
 	 * elements as string literals.  (The elements may be double-quoted as-is,
 	 * but we can't just feed them to the SQL parser; it would do the wrong
 	 * thing with elements that are zero-length or longer than NAMEDATALEN.)
+	 * Also, we need a special case for empty lists.
 	 *
 	 * Variables that are not so marked should just be emitted as simple
 	 * string literals.  If the variable is not known to
@@ -908,6 +909,9 @@ makeAlterConfigCommand(PGconn *conn, const char *configitem,
 		/* this shouldn't fail really */
 		if (SplitGUCList(pos, ',', &namelist))
 		{
+			/* Special case: represent an empty list as NULL */
+			if (*namelist == NULL)
+				appendPQExpBufferStr(buf, "NULL");
 			for (nameptr = namelist; *nameptr; nameptr++)
 			{
 				if (nameptr != namelist)
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 47913178a93..a00918bacb4 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -13764,7 +13764,8 @@ dumpFunc(Archive *fout, const FuncInfo *finfo)
 		 * and then quote the elements as string literals.  (The elements may
 		 * be double-quoted as-is, but we can't just feed them to the SQL
 		 * parser; it would do the wrong thing with elements that are
-		 * zero-length or longer than NAMEDATALEN.)
+		 * zero-length or longer than NAMEDATALEN.)  Also, we need a special
+		 * case for empty lists.
 		 *
 		 * Variables that are not so marked should just be emitted as simple
 		 * string literals.  If the variable is not known to
@@ -13780,6 +13781,9 @@ dumpFunc(Archive *fout, const FuncInfo *finfo)
 			/* this shouldn't fail really */
 			if (SplitGUCList(pos, ',', &namelist))
 			{
+				/* Special case: represent an empty list as NULL */
+				if (*namelist == NULL)
+					appendPQExpBufferStr(q, "NULL");
 				for (nameptr = namelist; *nameptr; nameptr++)
 				{
 					if (nameptr != namelist)
diff --git a/src/test/regress/expected/guc.out b/src/test/regress/expected/guc.out
index 7f9e29c765c..d6fb879f500 100644
--- a/src/test/regress/expected/guc.out
+++ b/src/test/regress/expected/guc.out
@@ -31,6 +31,28 @@ SELECT '2006-08-13 12:34:56'::timestamptz;
  2006-08-13 12:34:56-07
 (1 row)
 
+-- Check handling of list GUCs
+SET search_path = 'pg_catalog', Foo, 'Bar', '';
+SHOW search_path;
+        search_path         
+----------------------------
+ pg_catalog, foo, "Bar", ""
+(1 row)
+
+SET search_path = null;  -- means empty list
+SHOW search_path;
+ search_path 
+-------------
+ 
+(1 row)
+
+SET search_path = null, null;  -- syntax error
+ERROR:  syntax error at or near ","
+LINE 1: SET search_path = null, null;
+                              ^
+SET enable_seqscan = null;  -- error
+ERROR:  NULL is an invalid value for enable_seqscan
+RESET search_path;
 -- SET LOCAL has no effect outside of a transaction
 SET LOCAL vacuum_cost_delay TO 50;
 WARNING:  SET LOCAL can only be used in transaction blocks
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 77e25ca029e..2bf968ae3d3 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -3572,6 +3572,7 @@ CREATE FUNCTION func_with_set_params() RETURNS integer
     SET extra_float_digits TO 2
     SET work_mem TO '4MB'
     SET datestyle to iso, mdy
+    SET temp_tablespaces to NULL
     SET local_preload_libraries TO "Mixed/Case", 'c:/''a"/path', '', '0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789'
     IMMUTABLE STRICT;
 SELECT pg_get_functiondef('func_with_set_params()'::regprocedure);
@@ -3585,6 +3586,7 @@ SELECT pg_get_functiondef('func_with_set_params()'::regprocedure);
   SET extra_float_digits TO '2'                                                                                                                                          +
   SET work_mem TO '4MB'                                                                                                                                                  +
   SET "DateStyle" TO 'iso, mdy'                                                                                                                                          +
+  SET temp_tablespaces TO NULL                                                                                                                                           +
   SET local_preload_libraries TO 'Mixed/Case', 'c:/''a"/path', '', '0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789'+
  AS $function$select 1;$function$                                                                                                                                        +
  
diff --git a/src/test/regress/sql/guc.sql b/src/test/regress/sql/guc.sql
index f65f84a2632..bafaf067e82 100644
--- a/src/test/regress/sql/guc.sql
+++ b/src/test/regress/sql/guc.sql
@@ -12,6 +12,15 @@ SHOW vacuum_cost_delay;
 SHOW datestyle;
 SELECT '2006-08-13 12:34:56'::timestamptz;
 
+-- Check handling of list GUCs
+SET search_path = 'pg_catalog', Foo, 'Bar', '';
+SHOW search_path;
+SET search_path = null;  -- means empty list
+SHOW search_path;
+SET search_path = null, null;  -- syntax error
+SET enable_seqscan = null;  -- error
+RESET search_path;
+
 -- SET LOCAL has no effect outside of a transaction
 SET LOCAL vacuum_cost_delay TO 50;
 SHOW vacuum_cost_delay;
diff --git a/src/test/regress/sql/rules.sql b/src/test/regress/sql/rules.sql
index fdd3ff1d161..3f240bec7b0 100644
--- a/src/test/regress/sql/rules.sql
+++ b/src/test/regress/sql/rules.sql
@@ -1217,6 +1217,7 @@ CREATE FUNCTION func_with_set_params() RETURNS integer
     SET extra_float_digits TO 2
     SET work_mem TO '4MB'
     SET datestyle to iso, mdy
+    SET temp_tablespaces to NULL
     SET local_preload_libraries TO "Mixed/Case", 'c:/''a"/path', '', '0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789'
     IMMUTABLE STRICT;
 SELECT pg_get_functiondef('func_with_set_params()'::regprocedure);
-- 
2.43.7

#18Tom Lane
tgl@sss.pgh.pa.us
In reply to: Tom Lane (#17)
Re: [PATCH] Fix ALTER SYSTEM empty string bug for GUC_LIST_QUOTE parameters

I wrote:

Andrei Klychkov <andrew.a.klychkov@gmail.com> writes:

2. It doesn't look to me that v5 solves the original issue of a user
running ALTER SYSTEM SET <setting like shared_preload_libraries> = ''; ,
then restarting the server and not getting it back online.

[ shrug... ] It's not supposed to "solve" that. That command is
erroneous, and if you didn't test the setting before restarting the
server, you shouldn't be too surprised if restart fails.

If you are feeling excited about that specific case, I think the
correct solution would be to install a GUC check_hook for
shared_preload_libraries (and probably its siblings too). It couldn't
go so far as to actually dlopen() the list items, but it could check
that each one resolves as an accessible file.

A potential objection is that this could result in unwanted failures
in some scenarios, say where you're restoring a dump and haven't
yet installed all the relevant extensions. I'm not quite sure if
there are realistic scenarios where that's actually a problem.
If it is, perhaps we could adjust the check_hook so it issues
WARNINGs not hard errors.

I'm not sure it's worth the trouble though. A quick look at dfmgr.c
suggests that it'd take quite a lot of refactoring (or else code
duplication, not good) to be able to apply the library lookup process
without actually doing dlopen. In any case that would be a totally
different patch from what we are discussing here.

Anyway, the attached v6 is the same as v5, except now with proposed
doc changes and a draft commit message. I spent some effort on
getting the ALTER SYSTEM and SET ref pages back into sync; it seemed
like more care has been taken with the ALTER SYSTEM synopsis and
other details.

Hearing no further comments, I'm going to go ahead with v6.

regards, tom lane

#19Andreas Karlsson
andreas@proxel.se
In reply to: Tom Lane (#18)
Re: [PATCH] Fix ALTER SYSTEM empty string bug for GUC_LIST_QUOTE parameters

On 11/4/25 5:58 PM, Tom Lane wrote:

Hearing no further comments, I'm going to go ahead with v6.

Honestly none of the alternatives is very appealing and v6 is probably
the least bad.

When I ran into this issue I thought about adding and EMPTY keyword (we
do not want more keywords) or adding support for specifying array
literals like this [] or [a, b], but that would be confusing since
postgresql.conf does not allow any such syntax.

Andreas

#20Tom Lane
tgl@sss.pgh.pa.us
In reply to: Andreas Karlsson (#19)
Re: [PATCH] Fix ALTER SYSTEM empty string bug for GUC_LIST_QUOTE parameters

Andreas Karlsson <andreas@proxel.se> writes:

Honestly none of the alternatives is very appealing and v6 is probably
the least bad.

Yeah, that's about where I'm at with this.

When I ran into this issue I thought about adding and EMPTY keyword (we
do not want more keywords) or adding support for specifying array
literals like this [] or [a, b], but that would be confusing since
postgresql.conf does not allow any such syntax.

I would have preferred a different keyword. But AFAICS it'd have to
be a fully-reserved word, and creating a new one of those has to
clear a pretty high bar. NULL is not so far off...

regards, tom lane

#21Maciek Sakrejda
maciek@pganalyze.com
In reply to: Tom Lane (#18)
Re: [PATCH] Fix ALTER SYSTEM empty string bug for GUC_LIST_QUOTE parameters

On Tue, Nov 4, 2025 at 8:58 AM Tom Lane <tgl@sss.pgh.pa.us> wrote:

I wrote:

Andrei Klychkov <andrew.a.klychkov@gmail.com> writes:

2. It doesn't look to me that v5 solves the original issue of a user
running ALTER SYSTEM SET <setting like shared_preload_libraries> = ''; ,
then restarting the server and not getting it back online.

[ shrug... ] It's not supposed to "solve" that. That command is
erroneous, and if you didn't test the setting before restarting the
server, you shouldn't be too surprised if restart fails.

If you are feeling excited about that specific case, I think the
correct solution would be to install a GUC check_hook for
shared_preload_libraries (and probably its siblings too). It couldn't
go so far as to actually dlopen() the list items, but it could check
that each one resolves as an accessible file.

A potential objection is that this could result in unwanted failures
in some scenarios, say where you're restoring a dump and haven't
yet installed all the relevant extensions. I'm not quite sure if
there are realistic scenarios where that's actually a problem.
If it is, perhaps we could adjust the check_hook so it issues
WARNINGs not hard errors.

I'm not sure it's worth the trouble though. A quick look at dfmgr.c
suggests that it'd take quite a lot of refactoring (or else code
duplication, not good) to be able to apply the library lookup process
without actually doing dlopen. In any case that would be a totally
different patch from what we are discussing here.

For what it's worth, there was a patch that took a stab at this a
while ago, but it ended up RWF:
/messages/by-id/Z1kfMUoZkl9P0egB@paquier.xyz

I was a reviewer and I still think something like that would be useful
and prevent a lot of mistakes.

Thanks,
Maciek

#22Tom Lane
tgl@sss.pgh.pa.us
In reply to: Maciek Sakrejda (#21)
Re: [PATCH] Fix ALTER SYSTEM empty string bug for GUC_LIST_QUOTE parameters

Maciek Sakrejda <maciek@pganalyze.com> writes:

On Tue, Nov 4, 2025 at 8:58 AM Tom Lane <tgl@sss.pgh.pa.us> wrote:

If you are feeling excited about that specific case, I think the
correct solution would be to install a GUC check_hook for
shared_preload_libraries (and probably its siblings too).

For what it's worth, there was a patch that took a stab at this a
while ago, but it ended up RWF:
/messages/by-id/Z1kfMUoZkl9P0egB@paquier.xyz
I was a reviewer and I still think something like that would be useful
and prevent a lot of mistakes.

Hah, yeah, I thought that idea seemed familiar.

Re-reading that thread, it seems like a whole lot of the difficulties
arose precisely from not wanting to make the check_hook's complaints
be hard errors. Maybe we should abandon the idea that we need to
permit setting the GUC to a value that we know will not work. It was
argued that there were use-cases for that, but the argument seems
rather thin and not worth tying the behavior in knots for.

Another idea, considering our experience with search_path and
temp_tablespaces, is maybe it shouldn't be a hard error if the
GUC contains references to nonexistent libraries, only if the
syntax is bad. As long as an empty-string item is bad syntax,
the check_hook could still prevent the problem we started this
thread with.

regards, tom lane