From 37838f16be7549936457ce265ab290a76928fd72 Mon Sep 17 00:00:00 2001
From: Alexander Korotkov <akorotkov@postgresql.org>
Date: Sun, 20 Nov 2022 03:29:09 +0300
Subject: [PATCH] ALTER ROLE ... SET ... TO ... USER SET

---
 src/backend/catalog/pg_db_role_setting.c |  4 +-
 src/backend/commands/functioncmds.c      |  2 +-
 src/backend/parser/gram.y                | 20 ++++++
 src/backend/utils/misc/guc.c             | 90 +++++++++++++++++++-----
 src/bin/pg_dump/dumputils.c              | 11 ++-
 src/include/nodes/parsenodes.h           |  1 +
 src/include/utils/guc.h                  |  3 +-
 7 files changed, 108 insertions(+), 23 deletions(-)

diff --git a/src/backend/catalog/pg_db_role_setting.c b/src/backend/catalog/pg_db_role_setting.c
index 42387f4e304..4b9a39a953d 100644
--- a/src/backend/catalog/pg_db_role_setting.c
+++ b/src/backend/catalog/pg_db_role_setting.c
@@ -115,7 +115,7 @@ AlterSetting(Oid databaseid, Oid roleid, VariableSetStmt *setstmt)
 
 		/* Update (valuestr is NULL in RESET cases) */
 		if (valuestr)
-			a = GUCArrayAdd(a, setstmt->name, valuestr);
+			a = GUCArrayAdd(a, setstmt->name, valuestr, setstmt->user_set);
 		else
 			a = GUCArrayDelete(a, setstmt->name);
 
@@ -141,7 +141,7 @@ AlterSetting(Oid databaseid, Oid roleid, VariableSetStmt *setstmt)
 
 		memset(nulls, false, sizeof(nulls));
 
-		a = GUCArrayAdd(NULL, setstmt->name, valuestr);
+		a = GUCArrayAdd(NULL, setstmt->name, valuestr, setstmt->user_set);
 
 		values[Anum_pg_db_role_setting_setdatabase - 1] =
 			ObjectIdGetDatum(databaseid);
diff --git a/src/backend/commands/functioncmds.c b/src/backend/commands/functioncmds.c
index 1f820c93e96..950fd28badc 100644
--- a/src/backend/commands/functioncmds.c
+++ b/src/backend/commands/functioncmds.c
@@ -662,7 +662,7 @@ update_proconfig_value(ArrayType *a, List *set_items)
 			char	   *valuestr = ExtractSetVariableArgs(sstmt);
 
 			if (valuestr)
-				a = GUCArrayAdd(a, sstmt->name, valuestr);
+				a = GUCArrayAdd(a, sstmt->name, valuestr, sstmt->user_set);
 			else				/* RESET */
 				a = GUCArrayDelete(a, sstmt->name);
 		}
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 737bd2d06d5..c38398d8528 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -1622,6 +1622,26 @@ generic_set:
 					n->args = $3;
 					$$ = n;
 				}
+			| var_name TO var_list USER SET
+				{
+					VariableSetStmt *n = makeNode(VariableSetStmt);
+
+					n->kind = VAR_SET_VALUE;
+					n->name = $1;
+					n->args = $3;
+					n->user_set = true;
+					$$ = n;
+				}
+			| var_name '=' var_list USER SET
+				{
+					VariableSetStmt *n = makeNode(VariableSetStmt);
+
+					n->kind = VAR_SET_VALUE;
+					n->name = $1;
+					n->args = $3;
+					n->user_set = true;
+					$$ = n;
+				}
 			| var_name TO DEFAULT
 				{
 					VariableSetStmt *n = makeNode(VariableSetStmt);
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index 117a2d26a0e..c7af6305306 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -244,7 +244,7 @@ static void reapply_stacked_values(struct config_generic *variable,
 								   GucContext curscontext, GucSource cursource,
 								   Oid cursrole);
 static bool validate_option_array_item(const char *name, const char *value,
-									   bool skipIfNoPermissions);
+									   bool skipIfNoPermissions, bool user_set);
 static void write_auto_conf_file(int fd, const char *filename, ConfigVariable *head);
 static void replace_auto_config_value(ConfigVariable **head_p, ConfigVariable **tail_p,
 									  const char *name, const char *value);
@@ -6164,8 +6164,9 @@ RestoreGUCState(void *gucstate)
  * storage. Note that '-' is converted to '_' in the option name. If
  * there is no '=' in the input string then value will be NULL.
  */
-void
-ParseLongOption(const char *string, char **name, char **value)
+static void
+ParseLongOptionInternal(const char *string, char **name, char **value,
+						bool *user_set)
 {
 	size_t		equal_pos;
 	char	   *cp;
@@ -6178,8 +6179,20 @@ ParseLongOption(const char *string, char **name, char **value)
 
 	if (string[equal_pos] == '=')
 	{
-		*name = palloc(equal_pos + 1);
-		strlcpy(*name, string, equal_pos + 1);
+		if (equal_pos >= 3 && string[equal_pos - 1] == ')')
+		{
+			*name = palloc(equal_pos - 2);
+			strlcpy(*name, string, equal_pos - 2);
+			if (user_set)
+				*user_set = true;
+		}
+		else
+		{
+			*name = palloc(equal_pos + 1);
+			strlcpy(*name, string, equal_pos + 1);
+			if (user_set)
+				*user_set = false;
+		}
 
 		*value = pstrdup(&string[equal_pos + 1]);
 	}
@@ -6188,6 +6201,9 @@ ParseLongOption(const char *string, char **name, char **value)
 		/* no equal sign in string */
 		*name = pstrdup(string);
 		*value = NULL;
+
+		if (user_set)
+			*user_set = false;
 	}
 
 	for (cp = *name; *cp; cp++)
@@ -6195,6 +6211,19 @@ ParseLongOption(const char *string, char **name, char **value)
 			*cp = '_';
 }
 
+/*
+ * A little "long argument" simulation, although not quite GNU
+ * compliant. Takes a string of the form "some-option=some value" and
+ * returns name = "some_option" and value = "some value" in palloc'ed
+ * storage. Note that '-' is converted to '_' in the option name. If
+ * there is no '=' in the input string then value will be NULL.
+ */
+void
+ParseLongOption(const char *string, char **name, char **value)
+{
+	ParseLongOptionInternal(string, name, value, NULL);
+}
+
 
 /*
  * Handle options fetched from pg_db_role_setting.setconfig,
@@ -6220,6 +6249,7 @@ ProcessGUCArray(ArrayType *array,
 		char	   *s;
 		char	   *name;
 		char	   *value;
+		bool		user_set;
 
 		d = array_ref(array, 1, &i,
 					  -1 /* varlenarray */ ,
@@ -6233,7 +6263,7 @@ ProcessGUCArray(ArrayType *array,
 
 		s = TextDatumGetCString(d);
 
-		ParseLongOption(s, &name, &value);
+		ParseLongOptionInternal(s, &name, &value, &user_set);
 		if (!value)
 		{
 			ereport(WARNING,
@@ -6245,7 +6275,7 @@ ProcessGUCArray(ArrayType *array,
 		}
 
 		(void) set_config_option(name, value,
-								 context, source,
+								 user_set ? PGC_USERSET : context, source,
 								 action, true, 0, false);
 
 		pfree(name);
@@ -6260,7 +6290,8 @@ ProcessGUCArray(ArrayType *array,
  * to indicate the current table entry is NULL.
  */
 ArrayType *
-GUCArrayAdd(ArrayType *array, const char *name, const char *value)
+GUCArrayAdd(ArrayType *array, const char *name, const char *value,
+			bool user_set)
 {
 	struct config_generic *record;
 	Datum		datum;
@@ -6271,7 +6302,7 @@ GUCArrayAdd(ArrayType *array, const char *name, const char *value)
 	Assert(value);
 
 	/* test if the option is valid and we're allowed to set it */
-	(void) validate_option_array_item(name, value, false);
+	(void) validate_option_array_item(name, value, false, user_set);
 
 	/* normalize name (converts obsolete GUC names to modern spellings) */
 	record = find_option(name, false, true, WARNING);
@@ -6279,7 +6310,10 @@ GUCArrayAdd(ArrayType *array, const char *name, const char *value)
 		name = record->name;
 
 	/* build new item for array */
-	newval = psprintf("%s=%s", name, value);
+	if (user_set)
+		newval = psprintf("%s(u)=%s", name, value);
+	else
+		newval = psprintf("%s=%s", name, value);
 	datum = CStringGetTextDatum(newval);
 
 	if (array)
@@ -6310,8 +6344,15 @@ GUCArrayAdd(ArrayType *array, const char *name, const char *value)
 			current = TextDatumGetCString(d);
 
 			/* check for match up through and including '=' */
-			if (strncmp(current, newval, strlen(name) + 1) == 0)
+			if (strncmp(current, newval, strlen(name)) == 0 &&
+				(current[strlen(name)] == '=' || current[strlen(name)] == '('))
 			{
+				/*
+				 * recheck permissons if there is not USER SET option, while
+				 * we were looking for USER SET
+				 */
+				if (current[strlen(name)] == '=' && user_set)
+					(void) validate_option_array_item(name, value, false, false);
 				index = i;
 				break;
 			}
@@ -6347,9 +6388,6 @@ GUCArrayDelete(ArrayType *array, const char *name)
 
 	Assert(name);
 
-	/* test if the option is valid and we're allowed to set it */
-	(void) validate_option_array_item(name, NULL, false);
-
 	/* normalize name (converts obsolete GUC names to modern spellings) */
 	record = find_option(name, false, true, WARNING);
 	if (record)
@@ -6380,8 +6418,13 @@ GUCArrayDelete(ArrayType *array, const char *name)
 
 		/* ignore entry if it's what we want to delete */
 		if (strncmp(val, name, strlen(name)) == 0
-			&& val[strlen(name)] == '=')
+			&& (val[strlen(name)] == '=' || val[strlen(name)] == '('))
+		{
+			/* test if the option is valid and we're allowed to set it */
+			(void) validate_option_array_item(name, NULL, false,
+											  val[strlen(name)] == '(');
 			continue;
+		}
 
 		/* else add it to the output array */
 		if (newarray)
@@ -6431,6 +6474,7 @@ GUCArrayReset(ArrayType *array)
 		char	   *val;
 		char	   *eqsgn;
 		bool		isnull;
+		bool		user_set = false;
 
 		d = array_ref(array, 1, &i,
 					  -1 /* varlenarray */ ,
@@ -6443,10 +6487,18 @@ GUCArrayReset(ArrayType *array)
 		val = TextDatumGetCString(d);
 
 		eqsgn = strchr(val, '=');
-		*eqsgn = '\0';
+		if (eqsgn - val >= 3 && eqsgn[-1] == ')')
+		{
+			eqsgn[-3] = '\0';
+			user_set = true;
+		}
+		else
+		{
+			*eqsgn = '\0';
+		}
 
 		/* skip if we have permission to delete it */
-		if (validate_option_array_item(val, NULL, true))
+		if (validate_option_array_item(val, NULL, true, user_set))
 			continue;
 
 		/* else add it to the output array */
@@ -6481,7 +6533,7 @@ GUCArrayReset(ArrayType *array)
  */
 static bool
 validate_option_array_item(const char *name, const char *value,
-						   bool skipIfNoPermissions)
+						   bool skipIfNoPermissions, bool user_set)
 
 {
 	struct config_generic *gconf;
@@ -6518,6 +6570,8 @@ validate_option_array_item(const char *name, const char *value,
 		 * We cannot do any meaningful check on the value, so only permissions
 		 * are useful to check.
 		 */
+		if (user_set)
+			return true;
 		if (superuser() ||
 			pg_parameter_aclcheck(name, GetUserId(), ACL_SET) == ACLCHECK_OK)
 			return true;
diff --git a/src/bin/pg_dump/dumputils.c b/src/bin/pg_dump/dumputils.c
index 6e501a54138..3aea75296dc 100644
--- a/src/bin/pg_dump/dumputils.c
+++ b/src/bin/pg_dump/dumputils.c
@@ -820,6 +820,7 @@ makeAlterConfigCommand(PGconn *conn, const char *configitem,
 {
 	char	   *mine;
 	char	   *pos;
+	bool		user_set = false;
 
 	/* Parse the configitem.  If we can't find an "=", silently do nothing. */
 	mine = pg_strdup(configitem);
@@ -829,7 +830,13 @@ makeAlterConfigCommand(PGconn *conn, const char *configitem,
 		pg_free(mine);
 		return;
 	}
-	*pos++ = '\0';
+	if (pos - mine >= 3 && pos[-1] == ')')
+	{
+		user_set = true;
+		pos[-3] = '\0';
+	}
+	else
+		*pos++ = '\0';
 
 	/* Build the command, with suitable quoting for everything. */
 	appendPQExpBuffer(buf, "ALTER %s %s ", type, fmtId(name));
@@ -872,6 +879,8 @@ makeAlterConfigCommand(PGconn *conn, const char *configitem,
 	else
 		appendStringLiteralConn(buf, pos, conn);
 
+	if (user_set)
+		appendPQExpBufferStr(buf, "USER SET;\n");
 	appendPQExpBufferStr(buf, ";\n");
 
 	pg_free(mine);
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 7e7ad3f7e47..d48acda7c7b 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2228,6 +2228,7 @@ typedef struct VariableSetStmt
 	char	   *name;			/* variable to be set */
 	List	   *args;			/* List of A_Const nodes */
 	bool		is_local;		/* SET LOCAL? */
+	bool		user_set;
 } VariableSetStmt;
 
 /* ----------------------
diff --git a/src/include/utils/guc.h b/src/include/utils/guc.h
index b3aaff9665b..95b605f9b1e 100644
--- a/src/include/utils/guc.h
+++ b/src/include/utils/guc.h
@@ -393,7 +393,8 @@ extern char *GetConfigOptionByName(const char *name, const char **varname,
 
 extern void ProcessGUCArray(ArrayType *array,
 							GucContext context, GucSource source, GucAction action);
-extern ArrayType *GUCArrayAdd(ArrayType *array, const char *name, const char *value);
+extern ArrayType *GUCArrayAdd(ArrayType *array, const char *name,
+							  const char *value, bool user_set);
 extern ArrayType *GUCArrayDelete(ArrayType *array, const char *name);
 extern ArrayType *GUCArrayReset(ArrayType *array);
 
-- 
2.24.3 (Apple Git-128)

