From b7f262cf98be76215a9b9968c8800831874cf1d7 Mon Sep 17 00:00:00 2001 From: Abhijit Menon-Sen Date: Sun, 27 Sep 2020 06:52:30 +0530 Subject: Accept "SET xyz += pqr" to add pqr to the current setting of xyz A new function called by ExtractSetVariableArgs() modifies the current value of a configuration setting represented as a comma-separated list of strings (e.g., search_path) by adding or removing each of the given arguments, based on new stmt->kind values of VAR_{ADD,SUBTRACT}_VALUE. Using += x will add x if it is not already present and do nothing otherwise, and -= x will remove x if it is present and do nothing otherwise. The implementation extends to ALTER SYSTEM SET and similar commands, so this can be used by extension creation scripts to add individual entries to shared_preload_libraries. Examples: SET search_path += my_schema, other_schema; SET search_path -= public; ALTER SYSTEM SET shared_preload_libraries += auto_explain; --- doc/src/sgml/ref/set.sgml | 18 ++++ src/backend/parser/gram.y | 22 +++++ src/backend/parser/scan.l | 23 ++++- src/backend/tcop/utility.c | 2 + src/backend/utils/misc/guc.c | 168 +++++++++++++++++++++++++++++++++ src/include/nodes/parsenodes.h | 4 +- src/include/parser/scanner.h | 1 + 7 files changed, 233 insertions(+), 5 deletions(-) diff --git a/doc/src/sgml/ref/set.sgml b/doc/src/sgml/ref/set.sgml index 63f312e812..e30e9b42f0 100644 --- a/doc/src/sgml/ref/set.sgml +++ b/doc/src/sgml/ref/set.sgml @@ -22,6 +22,7 @@ PostgreSQL documentation SET [ SESSION | LOCAL ] configuration_parameter { TO | = } { value | 'value' | DEFAULT } +SET [ SESSION | LOCAL ] configuration_parameter { += | -= } { value | 'value' } SET [ SESSION | LOCAL ] TIME ZONE { timezone | LOCAL | DEFAULT } @@ -40,6 +41,14 @@ SET [ SESSION | LOCAL ] TIME ZONE { timezone + + For configuration parameters that accept a list of values, such as + search_path, you can modify the existing setting by adding + or removing individual elements with the += and + -= syntax. The former will add a value if it is not + already present, while the latter will remove an existing value. + + If SET (or equivalently SET SESSION) is issued within a transaction that is later aborted, the effects of the @@ -284,6 +293,15 @@ SET search_path TO my_schema, public; + + Modify the contents of the existing search path: + +SET search_path += some_schema; +SET search_path += other_schema, yetanother_schema; +SET search_path -= some_schema, my_schema; + + + Set the style of date to traditional POSTGRES with day before month diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 17653ef3a7..455b29131f 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -600,6 +600,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %type PartitionBoundSpec %type hash_partbound %type hash_partbound_elem +%type set_operation /* * Non-keyword token types. These are hard-wired into the "flex" lexer. @@ -617,6 +618,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %token ICONST PARAM %token TYPECAST DOT_DOT COLON_EQUALS EQUALS_GREATER %token LESS_EQUALS GREATER_EQUALS NOT_EQUALS +%token PLUS_EQUALS MINUS_EQUALS /* * If you want to make any keyword changes, update the keyword table in @@ -741,6 +743,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %right NOT %nonassoc IS ISNULL NOTNULL /* IS sets precedence for IS NULL, etc */ %nonassoc '<' '>' '=' LESS_EQUALS GREATER_EQUALS NOT_EQUALS +%nonassoc PLUS_EQUALS MINUS_EQUALS %nonassoc BETWEEN IN_P LIKE ILIKE SIMILAR NOT_LA %nonassoc ESCAPE /* ESCAPE must be just above LIKE/ILIKE/SIMILAR */ /* @@ -1450,6 +1453,17 @@ set_rest: | set_rest_more ; +set_operation: + PLUS_EQUALS + { + $$ = VAR_ADD_VALUE; + } + | MINUS_EQUALS + { + $$ = VAR_SUBTRACT_VALUE; + } + ; + generic_set: var_name TO var_list { @@ -1467,6 +1481,14 @@ generic_set: n->args = $3; $$ = n; } + | var_name set_operation var_list + { + VariableSetStmt *n = makeNode(VariableSetStmt); + n->name = $1; + n->kind = $2; + n->args = $3; + $$ = n; + } | var_name TO DEFAULT { VariableSetStmt *n = makeNode(VariableSetStmt); diff --git a/src/backend/parser/scan.l b/src/backend/parser/scan.l index 4eab2980c9..8d5efaa91c 100644 --- a/src/backend/parser/scan.l +++ b/src/backend/parser/scan.l @@ -364,6 +364,8 @@ less_equals "<=" greater_equals ">=" less_greater "<>" not_equals "!=" +plus_equals "+=" +minus_equals "-=" /* * "self" is the set of chars that should be returned as single-character @@ -853,6 +855,16 @@ other . return NOT_EQUALS; } +{plus_equals} { + SET_YYLLOC(); + return PLUS_EQUALS; + } + +{minus_equals} { + SET_YYLLOC(); + return MINUS_EQUALS; + } + {self} { SET_YYLLOC(); return yytext[0]; @@ -933,10 +945,9 @@ other . strchr(",()[].;:+-*/%^<>=", yytext[0])) return yytext[0]; /* - * Likewise, if what we have left is two chars, and - * those match the tokens ">=", "<=", "=>", "<>" or - * "!=", then we must return the appropriate token - * rather than the generic Op. + * Likewise, if what we have left is two chars, + * there may be a more specific matching token + * to return. */ if (nchars == 2) { @@ -950,6 +961,10 @@ other . return NOT_EQUALS; if (yytext[0] == '!' && yytext[1] == '=') return NOT_EQUALS; + if (yytext[0] == '+' && yytext[1] == '=') + return PLUS_EQUALS; + if (yytext[0] == '-' && yytext[1] == '=') + return MINUS_EQUALS; } } diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 9a35147b26..075b43e989 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -2830,6 +2830,8 @@ CreateCommandTag(Node *parsetree) case VAR_SET_CURRENT: case VAR_SET_DEFAULT: case VAR_SET_MULTI: + case VAR_ADD_VALUE: + case VAR_SUBTRACT_VALUE: tag = CMDTAG_SET; break; case VAR_RESET: diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c index 596bcb7b84..9e56f64fec 100644 --- a/src/backend/utils/misc/guc.c +++ b/src/backend/utils/misc/guc.c @@ -7986,6 +7986,167 @@ flatten_set_variable_args(const char *name, List *args) return buf.data; } +/* + * alter_set_variable_args + * Given a parsenode List as emitted by the grammar for SET, + * convert to the flat string representation used by GUC, with the + * args added to or removed from the current value of the setting, + * depending on the desired operation + * + * The result is a palloc'd string. + */ +static char * +alter_set_variable_args(const char *name, VariableSetKind operation, List *args) +{ + StringInfoData value; + struct config_generic *record; + struct config_string *conf; + char *rawstring; + List *elemlist; + ListCell *l; + char **argstrings; + bool *argswanted; + int cur = 0; + int max; + + record = find_option(name, false, ERROR); + if (record == NULL) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("unrecognized configuration parameter \"%s\"", name))); + + /* + * At present, this function can operate only on a list represented + * as a comma-separated string. + */ + if (record->vartype != PGC_STRING || (record->flags & GUC_LIST_INPUT) == 0) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("SET %s cannot perform list operations", name))); + + /* + * To determine whether to add or remove each argument, we build an + * array of strings from the List of args, and an array of booleans + * to indicate whether the argument should or should not be present + * in the final return value. + */ + max = 8; + argstrings = guc_malloc(ERROR, max * sizeof(char *)); + argswanted = guc_malloc(ERROR, max * sizeof(bool)); + foreach(l, args) + { + Node *arg = (Node *) lfirst(l); + A_Const *con; + char *val; + int i; + + if (!IsA(arg, A_Const)) + elog(ERROR, "unrecognized node type: %d", (int) nodeTag(arg)); + + con = (A_Const *) arg; + if (nodeTag(&con->val) != T_String) + elog(ERROR, "unrecognized node type: %d", (int) nodeTag(&con->val)); + + val = strVal(&con->val); + + for (i = 0; i < cur; i++) + if (pg_strcasecmp(argstrings[i], val) == 0) + break; + + if (i < cur) + continue; + + argstrings[cur] = val; + argswanted[cur] = operation == VAR_ADD_VALUE; + if (++cur == max) + { + max *= 2; + argstrings = guc_realloc(ERROR, argstrings, max * sizeof(char *)); + argswanted = guc_realloc(ERROR, argswanted, max * sizeof(bool)); + } + } + + /* + * Split the current value of the GUC setting into a list, for + * comparison with the argstrings extracted above. + */ + conf = (struct config_string *) record; + rawstring = pstrdup(*conf->variable && **conf->variable ? *conf->variable : ""); + if (!SplitIdentifierString(rawstring, ',', &elemlist)) + { + list_free(elemlist); + pfree(rawstring); + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("SET %s cannot operate on an already-invalid list", name))); + } + + initStringInfo(&value); + + /* + * Iterate over the elements in the current value of the setting and + * either suppress them (if operation is SUBTRACT and the element is + * in argstrings) or include them in the final value. + */ + foreach(l, elemlist) + { + char *tok = (char *) lfirst(l); + int i = 0; + + /* + * Check if tok is in argstrings. If so, we don't need to add it + * later; and if we want to remove it, we must skip this entry. + */ + for (i = 0; i < cur; i++) + if (pg_strcasecmp(argstrings[i], tok) == 0) + break; + + if (i < cur) + { + if (operation == VAR_ADD_VALUE) + argswanted[i] = false; + else + continue; + } + + /* Retain this element of the current setting */ + + if (value.len > 0) + appendStringInfoString(&value, ", "); + + if (record->flags & GUC_LIST_QUOTE) + appendStringInfoString(&value, quote_identifier(tok)); + else + appendStringInfoString(&value, tok); + } + + pfree(rawstring); + list_free(elemlist); + + /* + * Finally, if operation is ADD, we iterate over argstrings and add + * any elements to the output that are still wanted. + */ + for (int i = 0; i < cur; i++) + { + if (!argswanted[i]) + continue; + + if (value.len > 0) + appendStringInfoString(&value, ", "); + + if (record->flags & GUC_LIST_QUOTE) + appendStringInfoString(&value, quote_identifier(argstrings[i])); + else + appendStringInfoString(&value, argstrings[i]); + } + + free(argstrings); + free(argswanted); + + return value.data; +} + /* * Write updated configuration parameter values into a temporary file. * This function traverses the list of parameters and quotes the string @@ -8154,6 +8315,8 @@ AlterSystemSetConfigFile(AlterSystemStmt *altersysstmt) switch (altersysstmt->setstmt->kind) { case VAR_SET_VALUE: + case VAR_ADD_VALUE: + case VAR_SUBTRACT_VALUE: value = ExtractSetVariableArgs(altersysstmt->setstmt); break; @@ -8361,6 +8524,8 @@ ExecSetVariableStmt(VariableSetStmt *stmt, bool isTopLevel) { case VAR_SET_VALUE: case VAR_SET_CURRENT: + case VAR_ADD_VALUE: + case VAR_SUBTRACT_VALUE: if (stmt->is_local) WarnNoTransactionBlock(isTopLevel, "SET LOCAL"); (void) set_config_option(stmt->name, @@ -8474,6 +8639,9 @@ ExtractSetVariableArgs(VariableSetStmt *stmt) { case VAR_SET_VALUE: return flatten_set_variable_args(stmt->name, stmt->args); + case VAR_ADD_VALUE: + case VAR_SUBTRACT_VALUE: + return alter_set_variable_args(stmt->name, stmt->kind, stmt->args); case VAR_SET_CURRENT: return GetConfigOptionByName(stmt->name, NULL, false); default: diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 60c2f45466..c9b24a1778 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -2035,7 +2035,9 @@ typedef enum VAR_SET_CURRENT, /* SET var FROM CURRENT */ VAR_SET_MULTI, /* special case for SET TRANSACTION ... */ VAR_RESET, /* RESET var */ - VAR_RESET_ALL /* RESET ALL */ + VAR_RESET_ALL, /* RESET ALL */ + VAR_ADD_VALUE, /* SET var += value */ + VAR_SUBTRACT_VALUE /* SET var -= value */ } VariableSetKind; typedef struct VariableSetStmt diff --git a/src/include/parser/scanner.h b/src/include/parser/scanner.h index a27352afc1..57f073b6ff 100644 --- a/src/include/parser/scanner.h +++ b/src/include/parser/scanner.h @@ -52,6 +52,7 @@ typedef union core_YYSTYPE * %token ICONST PARAM * %token TYPECAST DOT_DOT COLON_EQUALS EQUALS_GREATER * %token LESS_EQUALS GREATER_EQUALS NOT_EQUALS + * %token PLUS_EQUALS MINUS_EQUALS * The above token definitions *must* be the first ones declared in any * bison parser built atop this scanner, so that they will have consistent * numbers assigned to them (specifically, IDENT = 258 and so on). -- 2.27.0