Re: [PATCH] Fix ALTER SYSTEM empty string bug for GUC_LIST_QUOTE parameters - Mailing list pgsql-hackers

From Tom Lane
Subject Re: [PATCH] Fix ALTER SYSTEM empty string bug for GUC_LIST_QUOTE parameters
Date
Msg-id 1528424.1757106377@sss.pgh.pa.us
Whole thread Raw
In response to Re: [PATCH] Fix ALTER SYSTEM empty string bug for GUC_LIST_QUOTE parameters  (Jim Jones <jim.jones@uni-muenster.de>)
Responses Re: [PATCH] Fix ALTER SYSTEM empty string bug for GUC_LIST_QUOTE parameters
List pgsql-hackers
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

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);

pgsql-hackers by date:

Previous
From: Mikhail Kot
Date:
Subject: Re: 回复: Fix segfault while accessing half-initialized hash table in pgstat_shmem.c
Next
From: Masahiko Sawada
Date:
Subject: Re: POC: enable logical decoding when wal_level = 'replica' without a server restart