From e98c52816028de792ee3953eb83d9ad6fa54651e Mon Sep 17 00:00:00 2001 From: "okbob@github.com" Date: Mon, 2 Jun 2025 08:29:37 +0200 Subject: [PATCH 11/15] LET command - assign a result of expression to the session variable Implementations of EXPLAIN LET and PREPARE LET statements are not supported now. Postponed to next step due reducing patch size. --- doc/src/sgml/ddl.sgml | 29 ++ doc/src/sgml/ref/allfiles.sgml | 1 + doc/src/sgml/ref/alter_variable.sgml | 1 + doc/src/sgml/ref/create_variable.sgml | 5 +- doc/src/sgml/ref/drop_variable.sgml | 1 + doc/src/sgml/ref/let.sgml | 96 +++++++ doc/src/sgml/reference.sgml | 1 + src/backend/commands/session_variable.c | 86 ++++++ src/backend/executor/execMain.c | 23 +- src/backend/nodes/nodeFuncs.c | 10 + src/backend/optimizer/plan/planner.c | 24 ++ src/backend/optimizer/plan/setrefs.c | 34 ++- src/backend/parser/analyze.c | 237 ++++++++++++++++ src/backend/parser/gram.y | 39 ++- src/backend/tcop/utility.c | 15 + src/backend/utils/cache/plancache.c | 11 + src/bin/psql/tab-complete.in.c | 12 +- src/include/commands/session_variable.h | 5 + src/include/nodes/parsenodes.h | 15 + src/include/nodes/pathnodes.h | 9 + src/include/nodes/plannodes.h | 7 + src/include/nodes/primnodes.h | 9 + src/include/parser/kwlist.h | 1 + src/include/tcop/cmdtaglist.h | 1 + .../expected/session_variables_dml.out | 258 ++++++++++++++++++ .../regress/sql/session_variables_dml.sql | 177 ++++++++++++ src/tools/pgindent/typedefs.list | 1 + 27 files changed, 1093 insertions(+), 15 deletions(-) create mode 100644 doc/src/sgml/ref/let.sgml diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index 613608e620d..5d09ad09b80 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -5392,10 +5392,39 @@ EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01'; session variable identifier, and can be used only for session variable identifier. The special syntax for accessing session variables removes risk of collisions between variable identifiers and column names. + + + + The value of a session variable is set with the SQL statement + LET. The value of a session variable can be retrieved + with the SQL statement SELECT. +CREATE VARIABLE var1 AS date; +LET var1 = current_date; +SELECT VARIABLE(var1); + + + or + + +CREATE VARIABLE public.current_user_id AS integer; +GRANT SELECT ON VARIABLE public.current_user_id TO PUBLIC; +LET current_user_id = (SELECT id FROM users WHERE usename = session_user); SELECT VARIABLE(current_user_id); + + + The value of a session variable is local to the current session. Retrieving + a variable's value returns a NULL, unless its value has + been set to something else in the current session using the + LET command. Session variables are not transactional: + any changes made to the value of a session variable in a transaction won't + be undone if the transaction is rolled back (just like variables in + procedural languages). Session variables themselves are persistent, but + their values are neither persistent nor shared (like the content of + temporary tables). + diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml index 2f67de3e21b..cc3bd5ab540 100644 --- a/doc/src/sgml/ref/allfiles.sgml +++ b/doc/src/sgml/ref/allfiles.sgml @@ -158,6 +158,7 @@ Complete list of usable sgml source files in this directory. + diff --git a/doc/src/sgml/ref/alter_variable.sgml b/doc/src/sgml/ref/alter_variable.sgml index 96d2586423e..221a699469b 100644 --- a/doc/src/sgml/ref/alter_variable.sgml +++ b/doc/src/sgml/ref/alter_variable.sgml @@ -173,6 +173,7 @@ ALTER VARIABLE boo SET SCHEMA private; + diff --git a/doc/src/sgml/ref/create_variable.sgml b/doc/src/sgml/ref/create_variable.sgml index 6e988f2e472..43000ce004d 100644 --- a/doc/src/sgml/ref/create_variable.sgml +++ b/doc/src/sgml/ref/create_variable.sgml @@ -120,9 +120,11 @@ CREATE VARIABLE [ IF NOT EXISTS ] nameExamples - Create an date session variable var1: + Create a session variable var1 of data type date: CREATE VARIABLE var1 AS date; +LET var1 = current_date; +SELECT VARIABLE(var1); @@ -143,6 +145,7 @@ CREATE VARIABLE var1 AS date; + diff --git a/doc/src/sgml/ref/drop_variable.sgml b/doc/src/sgml/ref/drop_variable.sgml index 5bdb3560f0b..67988b5fcd8 100644 --- a/doc/src/sgml/ref/drop_variable.sgml +++ b/doc/src/sgml/ref/drop_variable.sgml @@ -111,6 +111,7 @@ DROP VARIABLE var1; + diff --git a/doc/src/sgml/ref/let.sgml b/doc/src/sgml/ref/let.sgml new file mode 100644 index 00000000000..00f9bea91fe --- /dev/null +++ b/doc/src/sgml/ref/let.sgml @@ -0,0 +1,96 @@ + + + + + LET + + + + session variable + changing + + + + LET + 7 + SQL - Language Statements + + + + LET + change a session variable's value + + + + +LET session_variable = sql_expression + + + + + Description + + + The LET command assigns a value to the specified session + variable. + + + + + + Parameters + + + + session_variable + + + The name of the session variable. + + + + + + sql_expression + + + An arbitrary SQL expression. The result must be of a data type that can + be cast to the type of the session variable in an assignment. + + + + + + + + + Examples + +CREATE VARIABLE myvar AS integer; +LET myvar = 10; +LET myvar = (SELECT sum(val) FROM tab); + + + + + Compatibility + + + The LET is a PostgreSQL + extension. + + + + + See Also + + + + + + + + diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml index 25578f3946c..13e4adc5df3 100644 --- a/doc/src/sgml/reference.sgml +++ b/doc/src/sgml/reference.sgml @@ -186,6 +186,7 @@ &grant; &importForeignSchema; &insert; + &let; &listen; &load; &lock; diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index dbc054795bb..768163e2009 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -19,15 +19,22 @@ #include "catalog/namespace.h" #include "catalog/pg_type.h" #include "commands/session_variable.h" +#include "executor/execdesc.h" +#include "executor/executor.h" +#include "executor/svariableReceiver.h" #include "miscadmin.h" +#include "nodes/plannodes.h" #include "parser/parse_type.h" +#include "rewrite/rewriteHandler.h" #include "storage/lmgr.h" #include "storage/proc.h" +#include "tcop/tcopprot.h" #include "utils/builtins.h" #include "utils/datum.h" #include "utils/inval.h" #include "utils/lsyscache.h" #include "utils/memutils.h" +#include "utils/snapmgr.h" #include "utils/syscache.h" /* @@ -514,3 +521,82 @@ CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt) return variable; } + +/* + * Assign the result of the evaluated expression to the session variable + */ +void +ExecuteLetStmt(ParseState *pstate, + LetStmt *stmt, + ParamListInfo params, + QueryEnvironment *queryEnv, + QueryCompletion *qc) +{ + Query *query = castNode(Query, stmt->query); + List *rewritten; + DestReceiver *dest; + AclResult aclresult; + PlannedStmt *plan; + QueryDesc *queryDesc; + Oid varid = query->resultVariable; + + Assert(OidIsValid(varid)); + + /* do we have permission to write to the session variable? */ + aclresult = object_aclcheck(VariableRelationId, varid, GetUserId(), ACL_UPDATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_VARIABLE, get_session_variable_name(varid)); + + /* create a dest receiver for LET */ + dest = CreateVariableDestReceiver(varid); + + /* run the query rewriter */ + query = copyObject(query); + + rewritten = QueryRewrite(query); + + Assert(list_length(rewritten) == 1); + + query = linitial_node(Query, rewritten); + Assert(query->commandType == CMD_SELECT); + + /* plan the query */ + plan = pg_plan_query(query, pstate->p_sourcetext, + CURSOR_OPT_PARALLEL_OK, params); + + /* + * Use a snapshot with an updated command ID to ensure this query sees the + * results of any previously executed queries. (This could only matter if + * the planner executed an allegedly-stable function that changed the + * database contents, but let's do it anyway to be parallel to the EXPLAIN + * code path.) + */ + PushCopiedSnapshot(GetActiveSnapshot()); + UpdateActiveSnapshotCommandId(); + + /* create a QueryDesc, redirecting output to our tuple receiver */ + queryDesc = CreateQueryDesc(plan, pstate->p_sourcetext, + GetActiveSnapshot(), InvalidSnapshot, + dest, params, queryEnv, 0); + + /* call ExecutorStart to prepare the plan for execution */ + ExecutorStart(queryDesc, 0); + + /* + * Run the plan to completion. The result should be only one row. To + * check if there are too many result rows, we try to fetch two. + */ + ExecutorRun(queryDesc, ForwardScanDirection, 2L); + + /* save the rowcount if we're given a QueryCompletion to fill */ + if (qc) + SetQueryCompletion(qc, CMDTAG_LET, queryDesc->estate->es_processed); + + /* and clean up */ + ExecutorFinish(queryDesc); + ExecutorEnd(queryDesc); + + FreeQueryDesc(queryDesc); + + PopActiveSnapshot(); +} diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c index 223742b920f..685ba66fd34 100644 --- a/src/backend/executor/execMain.c +++ b/src/backend/executor/execMain.c @@ -235,13 +235,24 @@ standard_ExecutorStart(QueryDesc *queryDesc, int eflags) /* fill the array */ foreach_oid(varid, queryDesc->plannedstmt->sessionVariables) { - AclResult aclresult; + /* + * Permission check should be executed on all explicitly used + * variables in the query. For implicitly used variable (like base + * node of assignment indirect) we cannot do permission check, + * because we need read the value (and user can have only UPDATE + * variable). In this case the permission check is executed in + * write time. + */ + if (varid != queryDesc->plannedstmt->exclSelectPermCheckVarid) + { + AclResult aclresult; - aclresult = object_aclcheck(VariableRelationId, varid, - GetUserId(), ACL_SELECT); - if (aclresult != ACLCHECK_OK) - aclcheck_error(aclresult, OBJECT_VARIABLE, - get_session_variable_name(varid)); + aclresult = object_aclcheck(VariableRelationId, varid, + GetUserId(), ACL_SELECT); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_VARIABLE, + get_session_variable_name(varid)); + } estate->es_session_variables[i].value = GetSessionVariable(varid, diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index cd609c6e479..575365eefcd 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -4378,6 +4378,16 @@ raw_expression_tree_walker_impl(Node *node, return true; } break; + case T_LetStmt: + { + LetStmt *stmt = (LetStmt *) node; + + if (WALK(stmt->target)) + return true; + if (WALK(stmt->query)) + return true; + } + break; case T_PLAssignStmt: { PLAssignStmt *stmt = (PLAssignStmt *) node; diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c index 714f58bd3d7..7119ceecafb 100644 --- a/src/backend/optimizer/plan/planner.c +++ b/src/backend/optimizer/plan/planner.c @@ -344,6 +344,20 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions, glob->partition_directory = NULL; glob->sessionVariables = NIL; + /* + * The (session) result variable should be stored to global, because it is + * not set in subquery. When this variable is used other than in base + * node of assignment indirection, we need to check the access rights (and + * then we need to detect this situation). The variable used like base + * node cannot be different than target (result) variable. Because we know + * the result variable before planner invocation, we can simply search of + * usage just this variable, and we don't need to to wait until the end of + * planning when we know basenodeSessionVarid. + */ + glob->resultVariable = parse->resultVariable; + glob->basenodeSessionVarid = InvalidOid; + glob->basenodeSessionVarSelectCheck = false; + /* * Assess whether it's feasible to use parallel mode for this query. We * can't do this in a standalone backend, or if the command will try to @@ -583,6 +597,16 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions, result->sessionVariables = glob->sessionVariables; + /* + * The session variable used (and only used) like base node for assignemnt + * indirection should be excluded from permission check. + */ + if (OidIsValid(glob->basenodeSessionVarid) && + (!glob->basenodeSessionVarSelectCheck)) + result->exclSelectPermCheckVarid = glob->basenodeSessionVarid; + else + result->exclSelectPermCheckVarid = InvalidOid; + result->stmt_location = parse->stmt_location; result->stmt_len = parse->stmt_len; diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c index 6c86ee1ad64..d47a726f2df 100644 --- a/src/backend/optimizer/plan/setrefs.c +++ b/src/backend/optimizer/plan/setrefs.c @@ -2224,6 +2224,27 @@ fix_param_node(PlannerInfo *root, Param *p) p->paramid = n; } + /* + * We do SELECT permission check of all variables used by the query + * excluding the variable that is used only as base node of assignment + * indirection. The variable id assigned to this param should be same + * like resultVariable id, and this param should be used only once in + * query. When the variable is referenced by any other param, we + * should to do SELECT permission check for this variable too. + */ + if (p->parambasenode) + { + Assert(!OidIsValid(root->glob->basenodeSessionVarid)); + Assert(root->glob->resultVariable == p->paramvarid); + + root->glob->basenodeSessionVarid = p->paramvarid; + } + else + { + if (p->paramvarid == root->glob->resultVariable) + root->glob->basenodeSessionVarSelectCheck = true; + } + return (Node *) p; } @@ -3709,7 +3730,7 @@ record_plan_type_dependency(PlannerInfo *root, Oid typid) /* * Record dependency on a session variable. The variable can be used as a - * session variable in an expression list. + * session variable in an expression list, or as the target of a LET statement. */ static void record_plan_variable_dependency(PlannerInfo *root, Oid varid) @@ -3811,9 +3832,10 @@ extract_query_dependencies_walker(Node *node, PlannerInfo *context) } /* - * Ignore other utility statements, except those (such as EXPLAIN) - * that contain a parsed-but-not-planned query. For those, we - * just need to transfer our attention to the contained query. + * Ignore other utility statements, except those (such as EXPLAIN + * or LET) that contain a parsed-but-not-planned query. For + * those, we just need to transfer our attention to the contained + * query. */ query = UtilityContainsQuery(query->utilityStmt); if (query == NULL) @@ -3836,6 +3858,10 @@ extract_query_dependencies_walker(Node *node, PlannerInfo *context) lappend_oid(context->glob->relationOids, rte->relid); } + /* record dependency on the target variable of a LET command */ + if (OidIsValid(query->resultVariable)) + record_plan_variable_dependency(context, query->resultVariable); + /* And recurse into the query's subexpressions */ return query_tree_walker(query, extract_query_dependencies_walker, context, 0); diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c index 0d5efea5ca9..2a5ea22917f 100644 --- a/src/backend/parser/analyze.c +++ b/src/backend/parser/analyze.c @@ -52,6 +52,7 @@ #include "utils/builtins.h" #include "utils/guc.h" #include "utils/rel.h" +#include "utils/lsyscache.h" #include "utils/syscache.h" @@ -83,6 +84,8 @@ static Query *transformCreateTableAsStmt(ParseState *pstate, CreateTableAsStmt *stmt); static Query *transformCallStmt(ParseState *pstate, CallStmt *stmt); +static Query *transformLetStmt(ParseState *pstate, + LetStmt *stmt); static void transformLockingClause(ParseState *pstate, Query *qry, LockingClause *lc, bool pushedDown); #ifdef DEBUG_NODE_TESTS_ENABLED @@ -409,6 +412,7 @@ transformStmt(ParseState *pstate, Node *parseTree) case T_UpdateStmt: case T_DeleteStmt: case T_MergeStmt: + case T_LetStmt: (void) test_raw_expression_coverage(parseTree, NULL); break; default: @@ -488,6 +492,11 @@ transformStmt(ParseState *pstate, Node *parseTree) (CallStmt *) parseTree); break; + case T_LetStmt: + result = transformLetStmt(pstate, + (LetStmt *) parseTree); + break; + default: /* @@ -540,6 +549,7 @@ stmt_requires_parse_analysis(RawStmt *parseTree) case T_SelectStmt: case T_ReturnStmt: case T_PLAssignStmt: + case T_LetStmt: result = true; break; @@ -3442,6 +3452,233 @@ transformCallStmt(ParseState *pstate, CallStmt *stmt) return result; } +/* + * transformLetStmt - + * transform an Let Statement + */ +static Query * +transformLetStmt(ParseState *pstate, LetStmt *stmt) +{ + Query *query; + Query *result; + List *exprList = NIL; + List *exprListCoer = NIL; + ListCell *lc; + ListCell *indirection_head = NULL; + Query *selectQuery; + Oid varid; + char *attrname = NULL; + bool not_unique; + bool is_rowtype; + Oid typid; + int32 typmod; + Oid collid; + List *names = NULL; + int indirection_start; + int i = 0; + + /* there can't be any outer WITH to worry about */ + Assert(pstate->p_ctenamespace == NIL); + + names = NamesFromList(stmt->target); + + /* locks the variable with an AccessShareLock */ + varid = IdentifyVariable(names, &attrname, ¬_unique, false); + if (not_unique) + ereport(ERROR, + (errcode(ERRCODE_AMBIGUOUS_PARAMETER), + errmsg("target \"%s\" of LET command is ambiguous", + NameListToString(names)), + parser_errposition(pstate, stmt->location))); + + if (!OidIsValid(varid)) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("session variable \"%s\" doesn't exist", + NameListToString(names)), + parser_errposition(pstate, stmt->location))); + + /* + * Calculate start of possible position of an indirection in list, and + * when it is inside the list, store pointer on first node of indirection. + */ + indirection_start = list_length(names) - (attrname ? 1 : 0); + if (list_length(stmt->target) > indirection_start) + indirection_head = list_nth_cell(stmt->target, indirection_start); + + get_session_variable_type_typmod_collid(varid, &typid, &typmod, &collid); + + is_rowtype = type_is_rowtype(typid); + + if (attrname && !is_rowtype) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("cannot assign to field \"%s\" of session variable \"%s.%s\" because its type %s is not a composite type", + attrname, + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid), + format_type_be(typid)), + parser_errposition(pstate, stmt->location))); + + pstate->p_expr_kind = EXPR_KIND_UPDATE_TARGET; + + /* we need to postpone conversion of "unknown" to text */ + pstate->p_resolve_unknowns = false; + + selectQuery = transformStmt(pstate, stmt->query); + + /* the grammar should have produced a SELECT */ + Assert(IsA(selectQuery, Query) && selectQuery->commandType == CMD_SELECT); + + /* + * Generate an expression list for the LET that selects all the + * non-resjunk columns from the subquery. + */ + exprList = NIL; + foreach_node(TargetEntry, tle, selectQuery->targetList) + { + if (tle->resjunk) + continue; + + exprList = lappend(exprList, tle->expr); + } + + /* don't allow multicolumn result */ + if (list_length(exprList) != 1) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg_plural("assignment expression returned %d column", + "assignment expression returned %d columns", + list_length(exprList), + list_length(exprList)), + parser_errposition(pstate, + exprLocation((Node *) exprList)))); + + exprListCoer = NIL; + + foreach(lc, exprList) + { + Expr *expr = (Expr *) lfirst(lc); + Expr *coerced_expr; + Oid exprtypid; + + /* now we can read the type of the expression */ + exprtypid = exprType((Node *) expr); + + if (indirection_head) + { + bool targetIsArray; + char *targetName; + Param *param; + + targetName = get_session_variable_name(varid); + targetIsArray = OidIsValid(get_element_type(typid)); + + pstate->p_hasSessionVariables = true; + + param = makeNode(Param); + param->paramkind = PARAM_VARIABLE; + param->paramvarid = varid; + param->paramtype = typid; + param->paramtypmod = typmod; + + /* + * The parameter used as basenode has to have special mark, + * because requires special access when we do SELECT access check. + */ + param->parambasenode = true; + + coerced_expr = (Expr *) + transformAssignmentIndirection(pstate, + (Node *) param, + targetName, + targetIsArray, + typid, + typmod, + InvalidOid, + stmt->target, + indirection_head, + (Node *) expr, + COERCION_ASSIGNMENT, + stmt->location); + } + else + coerced_expr = (Expr *) + coerce_to_target_type(pstate, + (Node *) expr, + exprtypid, + typid, typmod, + COERCION_ASSIGNMENT, + COERCE_IMPLICIT_CAST, + stmt->location); + + if (coerced_expr == NULL) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("variable \"%s.%s\" is of type %s, but expression is of type %s", + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid), + format_type_be(typid), + format_type_be(exprtypid)), + errhint("You will need to rewrite or cast the expression."), + parser_errposition(pstate, exprLocation((Node *) expr)))); + + exprListCoer = lappend(exprListCoer, coerced_expr); + } + + /* generate query's target list using the computed list of expressions */ + query = makeNode(Query); + query->commandType = CMD_SELECT; + + foreach(lc, exprListCoer) + { + Expr *expr = (Expr *) lfirst(lc); + TargetEntry *tle; + + tle = makeTargetEntry(expr, + i + 1, + FigureColname((Node *) expr), + false); + query->targetList = lappend(query->targetList, tle); + } + + /* done building the range table and jointree */ + query->rtable = pstate->p_rtable; + query->jointree = makeFromExpr(pstate->p_joinlist, NULL); + + query->hasTargetSRFs = pstate->p_hasTargetSRFs; + query->hasSubLinks = pstate->p_hasSubLinks; + query->hasSessionVariables = pstate->p_hasSessionVariables; + + /* this is top-level query */ + query->canSetTag = true; + + /* + * Save target session variable ID. It is used later for acquiring an + * AccessShareLock on target variable, setting plan dependency and finally + * for creating VariableDestReceiver. + */ + query->resultVariable = varid; + + assign_query_collations(pstate, query); + + /* + * The query is executed as utility command by nested executor call. + * Assigned queryId is required in this case. + */ + if (IsQueryIdEnabled()) + JumbleQuery(query); + + stmt->query = (Node *) query; + + /* represent the command as a utility Query */ + result = makeNode(Query); + result->commandType = CMD_UTILITY; + result->utilityStmt = (Node *) stmt; + + return result; +} + /* * Produce a string representation of a LockClauseStrength value. * This should only be applied to valid values (not LCS_NONE). diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 28bd26cc922..750cf5171d8 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -297,7 +297,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); DropTransformStmt DropUserMappingStmt ExplainStmt FetchStmt GrantStmt GrantRoleStmt ImportForeignSchemaStmt IndexStmt InsertStmt - ListenStmt LoadStmt LockStmt MergeStmt NotifyStmt ExplainableStmt PreparableStmt + LetStmt ListenStmt LoadStmt LockStmt MergeStmt NotifyStmt ExplainableStmt PreparableStmt CreateFunctionStmt AlterFunctionStmt ReindexStmt RemoveAggrStmt RemoveFuncStmt RemoveOperStmt RenameStmt ReturnStmt RevokeStmt RevokeRoleStmt RuleActionStmt RuleActionStmtOrEmpty RuleStmt @@ -742,7 +742,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); KEEP KEY KEYS LABEL LANGUAGE LARGE_P LAST_P LATERAL_P - LEADING LEAKPROOF LEAST LEFT LEVEL LIKE LIMIT LISTEN LOAD LOCAL + LEADING LEAKPROOF LEAST LEFT LET LEVEL LIKE LIMIT LISTEN LOAD LOCAL LOCALTIME LOCALTIMESTAMP LOCATION LOCK_P LOCKED LOGGED MAPPING MATCH MATCHED MATERIALIZED MAXVALUE MERGE MERGE_ACTION METHOD @@ -1089,6 +1089,7 @@ stmt: | ImportForeignSchemaStmt | IndexStmt | InsertStmt + | LetStmt | ListenStmt | RefreshMatViewStmt | LoadStmt @@ -12868,6 +12869,38 @@ opt_hold: /* EMPTY */ { $$ = 0; } | WITHOUT HOLD { $$ = 0; } ; +/***************************************************************************** + * + * QUERY: + * LET STATEMENT + * + *****************************************************************************/ +LetStmt: LET ColId opt_indirection '=' a_expr + { + LetStmt *n = makeNode(LetStmt); + SelectStmt *select; + ResTarget *res; + + n->target = lcons(makeString($2), + check_indirection($3, yyscanner)); + + select = makeNode(SelectStmt); + res = makeNode(ResTarget); + + /* create target list for implicit query */ + res->name = NULL; + res->indirection = NIL; + res->val = (Node *) $5; + res->location = @5; + + select->targetList = list_make1(res); + n->query = (Node *) select; + + n->location = @2; + $$ = (Node *) n; + } + ; + /***************************************************************************** * * QUERY: @@ -17970,6 +18003,7 @@ unreserved_keyword: | LARGE_P | LAST_P | LEAKPROOF + | LET | LEVEL | LISTEN | LOAD @@ -18584,6 +18618,7 @@ bare_label_keyword: | LEAKPROOF | LEAST | LEFT + | LET | LEVEL | LIKE | LISTEN diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 782b022da9c..20e4d43576b 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -235,6 +235,7 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree) case T_CallStmt: case T_DoStmt: + case T_LetStmt: { /* * Commands inside the DO block or the called procedure might @@ -1067,6 +1068,11 @@ standard_ProcessUtility(PlannedStmt *pstmt, break; } + case T_LetStmt: + ExecuteLetStmt(pstate, (LetStmt *) parsetree, params, + queryEnv, qc); + break; + default: /* All other statement types have event trigger support */ ProcessUtilitySlow(pstate, pstmt, queryString, @@ -2206,6 +2212,10 @@ UtilityContainsQuery(Node *parsetree) return UtilityContainsQuery(qry->utilityStmt); return qry; + case T_LetStmt: + qry = castNode(Query, ((LetStmt *) parsetree)->query); + return qry; + default: return NULL; } @@ -2404,6 +2414,10 @@ CreateCommandTag(Node *parsetree) tag = CMDTAG_SELECT; break; + case T_LetStmt: + tag = CMDTAG_LET; + break; + /* utility statements --- same whether raw or cooked */ case T_TransactionStmt: { @@ -3289,6 +3303,7 @@ GetCommandLogLevel(Node *parsetree) break; case T_PLAssignStmt: + case T_LetStmt: lev = LOGSTMT_ALL; break; diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c index ca92be9a6ae..59469c52abe 100644 --- a/src/backend/utils/cache/plancache.c +++ b/src/backend/utils/cache/plancache.c @@ -2030,6 +2030,17 @@ ScanQueryForLocks(Query *parsetree, bool acquire) query_tree_walker(parsetree, ScanQueryWalker, &acquire, QTW_IGNORE_RC_SUBQUERIES); } + + /* process session variables */ + if (OidIsValid(parsetree->resultVariable)) + { + if (acquire) + LockDatabaseObject(VariableRelationId, parsetree->resultVariable, + 0, AccessShareLock); + else + UnlockDatabaseObject(VariableRelationId, parsetree->resultVariable, + 0, AccessShareLock); + } } /* diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index 7fe19a568f1..39d1b6aa610 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -1228,8 +1228,8 @@ static const char *const sql_commands[] = { "ABORT", "ALTER", "ANALYZE", "BEGIN", "CALL", "CHECKPOINT", "CLOSE", "CLUSTER", "COMMENT", "COMMIT", "COPY", "CREATE", "DEALLOCATE", "DECLARE", "DELETE FROM", "DISCARD", "DO", "DROP", "END", "EXECUTE", "EXPLAIN", - "FETCH", "GRANT", "IMPORT FOREIGN SCHEMA", "INSERT INTO", "LISTEN", "LOAD", "LOCK", - "MERGE INTO", "MOVE", "NOTIFY", "PREPARE", + "FETCH", "GRANT", "IMPORT FOREIGN SCHEMA", "INSERT INTO", "LET", + "LISTEN", "LOAD", "LOCK", "MERGE INTO", "MOVE", "NOTIFY", "PREPARE", "REASSIGN", "REFRESH MATERIALIZED VIEW", "REINDEX", "RELEASE", "RESET", "REVOKE", "ROLLBACK", "SAVEPOINT", "SECURITY LABEL", "SELECT", "SET", "SHOW", "START", @@ -4701,6 +4701,14 @@ match_previous_words(int pattern_id, else if (TailMatches("VALUES") && !TailMatches("DEFAULT", "VALUES")) COMPLETE_WITH("("); +/* LET */ + /* If prev. word is LET suggest a list of variables */ + else if (Matches("LET")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables); + /* Complete LET with "=" */ + else if (TailMatches("LET", MatchAny)) + COMPLETE_WITH("="); + /* LOCK */ /* Complete LOCK [TABLE] [ONLY] with a list of tables */ else if (Matches("LOCK")) diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h index 9f5c6e30fbd..2ebe8477789 100644 --- a/src/include/commands/session_variable.h +++ b/src/include/commands/session_variable.h @@ -17,11 +17,16 @@ #include "catalog/objectaddress.h" #include "parser/parse_node.h" +#include "nodes/params.h" #include "nodes/parsenodes.h" +#include "tcop/cmdtag.h" extern void SetSessionVariable(Oid varid, Datum value, bool isNull); extern Datum GetSessionVariable(Oid varid, bool *isNull); extern ObjectAddress CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt); +extern void ExecuteLetStmt(ParseState *pstate, LetStmt *stmt, ParamListInfo params, + QueryEnvironment *queryEnv, QueryCompletion *qc); + #endif diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 76cde246f63..8c508b851da 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -147,6 +147,9 @@ typedef struct Query */ int resultRelation pg_node_attr(query_jumble_ignore); + /* target variable of LET statement */ + Oid resultVariable; + /* has aggregates in tlist or havingQual */ bool hasAggs pg_node_attr(query_jumble_ignore); /* has window functions in tlist */ @@ -2166,6 +2169,18 @@ typedef struct MergeStmt ParseLoc stmt_len; /* length in bytes; 0 means "rest of string" */ } MergeStmt; +/* ---------------------- + * Let Statement + * ---------------------- + */ +typedef struct LetStmt +{ + NodeTag type; + List *target; /* target variable */ + Node *query; /* source expression */ + ParseLoc location; +} LetStmt; + /* ---------------------- * Select Statement * diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h index ec5685df779..4b0c625619f 100644 --- a/src/include/nodes/pathnodes.h +++ b/src/include/nodes/pathnodes.h @@ -182,6 +182,15 @@ typedef struct PlannerGlobal /* list of used session variables */ List *sessionVariables; + + /* Oid of session variable used like target of LET command */ + Oid resultVariable; + + /* oid of session variable used like base node for assignment indirection */ + Oid basenodeSessionVarid; + + /* true, if we do SELECT permission check on basenodeSessionVarid */ + bool basenodeSessionVarSelectCheck; } PlannerGlobal; /* macro for fetching the Plan associated with a SubPlan node */ diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h index 3622699cfc9..10761cf9105 100644 --- a/src/include/nodes/plannodes.h +++ b/src/include/nodes/plannodes.h @@ -134,6 +134,13 @@ typedef struct PlannedStmt /* OIDs for PARAM_VARIABLE Params */ List *sessionVariables; + /* + * The oid of session variable execluded from permission check. This + * session variable is used as base node of assignment indirection (and it + * is used only there). + */ + int exclSelectPermCheckVarid; + /* statement location in source string (copied from Query) */ /* start location, or -1 if unknown */ ParseLoc stmt_location; diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 2b1b3ac8a33..fa33f4f1bab 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -402,6 +402,15 @@ typedef struct Param Oid paramcollid pg_node_attr(query_jumble_ignore); /* OID of used session variable or InvalidOid if none */ Oid paramvarid pg_node_attr(query_jumble_ignore); + + /* + * true if param is used as base node of assignment indirection (when + * target of LET statement is an array field or an record field). For this + * param we do not check SELECT access right, because this param is used + * just for execution of an modify operation. + */ + bool parambasenode; + /* token location, or -1 if unknown */ ParseLoc location; } Param; diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index 0ea0265de7c..8c0affba13b 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -257,6 +257,7 @@ PG_KEYWORD("leading", LEADING, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("leakproof", LEAKPROOF, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("least", LEAST, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("left", LEFT, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("let", LET, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("level", LEVEL, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("like", LIKE, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("limit", LIMIT, RESERVED_KEYWORD, AS_LABEL) diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h index ea86954dded..22082c30008 100644 --- a/src/include/tcop/cmdtaglist.h +++ b/src/include/tcop/cmdtaglist.h @@ -186,6 +186,7 @@ PG_CMDTAG(CMDTAG_GRANT, "GRANT", true, false, false) PG_CMDTAG(CMDTAG_GRANT_ROLE, "GRANT ROLE", false, false, false) PG_CMDTAG(CMDTAG_IMPORT_FOREIGN_SCHEMA, "IMPORT FOREIGN SCHEMA", true, false, false) PG_CMDTAG(CMDTAG_INSERT, "INSERT", false, false, true) +PG_CMDTAG(CMDTAG_LET, "LET", false, false, false) PG_CMDTAG(CMDTAG_LISTEN, "LISTEN", false, false, false) PG_CMDTAG(CMDTAG_LOAD, "LOAD", false, false, false) PG_CMDTAG(CMDTAG_LOCK_TABLE, "LOCK TABLE", false, false, false) diff --git a/src/test/regress/expected/session_variables_dml.out b/src/test/regress/expected/session_variables_dml.out index 3e21059acc2..0fdf0fdc68a 100644 --- a/src/test/regress/expected/session_variables_dml.out +++ b/src/test/regress/expected/session_variables_dml.out @@ -189,3 +189,261 @@ drop cascades to table svartest_dml.testtab DROP ROLE regress_svartest_dml_read_role; DROP VARIABLE sesvar40; DROP TABLE svartest_dml; +CREATE VARIABLE sesvar43 AS numeric; +-- LET stmt is not allowed inside CTE +WITH x AS (LET sesvar43 = 3.14) SELECT * FROM x; +ERROR: syntax error at or near "LET" +LINE 1: WITH x AS (LET sesvar43 = 3.14) SELECT * FROM x; + ^ +-- LET stmt requires result with exactly one row +LET sesvar43 = generate_series(1,1); +-- should fail +LET sesvar43 = generate_series(1,2); +ERROR: expression returned more than one row +LET sesvar43 = generate_series(1,0); +ERROR: expression returned no rows +CREATE SCHEMA svartest_dml; +CREATE VARIABLE svartest_dml.sesvar44 AS varchar; +CREATE TYPE svartest_dml.composite_type AS (a int, b int, c int); +CREATE VARIABLE svartest_dml.sesvar45 AS svartest_dml.composite_type; +CREATE OR REPLACE FUNCTION svartest_dml.fx01(numeric) +RETURNS void AS $$ +LET sesvar43 = $1; +$$ LANGUAGE sql; +CREATE OR REPLACE FUNCTION svartest_dml.fx02() +RETURNS numeric AS $$ +SELECT VARIABLE(sesvar43); +$$ LANGUAGE sql; +SELECT svartest_dml.fx01(3.14); + fx01 +------ + +(1 row) + +SELECT svartest_dml.fx02(), VARIABLE(sesvar43); + fx02 | sesvar43 +------+---------- + 3.14 | 3.14 +(1 row) + +CREATE OR REPLACE FUNCTION svartest_dml.fx03(s varchar) +RETURNS varchar AS $$ +BEGIN + LET svartest_dml.sesvar44 = s; + RETURN VARIABLE(svartest_dml.sesvar44); +END +$$ LANGUAGE plpgsql; +SELECT svartest_dml.fx03('Hello'); + fx03 +------- + Hello +(1 row) + +CREATE OR REPLACE FUNCTION svartest_dml.fx04(s varchar) +RETURNS varchar AS $$ +BEGIN + LET sesvar44 = s; + RETURN VARIABLE(sesvar44); +END +$$ LANGUAGE plpgsql +SET SEARCH_PATH TO 'svartest_dml'; +SELECT svartest_dml.fx04('Hello'); + fx04 +------- + Hello +(1 row) + +CREATE OR REPLACE FUNCTION svartest_dml.fx05(a int, b int, c int) +RETURNS svartest_dml.composite_type AS $$ +BEGIN + LET svartest_dml.sesvar45 = ROW(a, b, c); + RETURN VARIABLE(svartest_dml.sesvar45); +END; +$$ LANGUAGE plpgsql; +SELECT row_to_json(svartest_dml.fx05(10, 20, 30)); + row_to_json +------------------------ + {"a":10,"b":20,"c":30} +(1 row) + +SELECT VARIABLE(svartest_dml.sesvar45); + sesvar45 +------------ + (10,20,30) +(1 row) + +SELECT VARIABLE(svartest_dml.sesvar45).*; + a | b | c +----+----+---- + 10 | 20 | 30 +(1 row) + +SELECT VARIABLE(svartest_dml.sesvar45.a); + a +---- + 10 +(1 row) + +SELECT VARIABLE(svartest_dml.sesvar45).a; + a +---- + 10 +(1 row) + +ALTER TYPE svartest_dml.composite_type ADD ATTRIBUTE d int; +-- composite value should be still readable +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + row_to_json +--------------------------------- + {"a":10,"b":20,"c":30,"d":null} +(1 row) + +LET svartest_dml.sesvar45 = ROW(100, 200, 300, NULL); +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + row_to_json +------------------------------------ + {"a":100,"b":200,"c":300,"d":null} +(1 row) + +-- use variables inside view +CREATE VIEW svartest_dml.view01 AS SELECT VARIABLE(svartest_dml.sesvar45).*; +SELECT * FROM svartest_dml.view01; + a | b | c | d +-----+-----+-----+--- + 100 | 200 | 300 | +(1 row) + +-- start new connection +\c +SELECT * FROM svartest_dml.view01; + a | b | c | d +---+---+---+--- + | | | +(1 row) + +LET svartest_dml.sesvar45 = ROW(5, 6, 7, 8); +SELECT * FROM svartest_dml.view01; + a | b | c | d +---+---+---+--- + 5 | 6 | 7 | 8 +(1 row) + +-- should fail (dependency) +DROP VARIABLE svartest_dml.sesvar45; +ERROR: cannot drop session variable svartest_dml.sesvar45 because other objects depend on it +DETAIL: view svartest_dml.view01 depends on session variable svartest_dml.sesvar45 +HINT: Use DROP ... CASCADE to drop the dependent objects too. +DROP VIEW svartest_dml.view01; +-- test of access variables from generic plans +CREATE OR REPLACE FUNCTION svartest_dml.fx06() +RETURNS numeric AS $$ +BEGIN + RETURN VARIABLE(sesvar43); +END; +$$ LANGUAGE plpgsql; +SET plan_cache_mode TO force_generic_plan; +LET sesvar43 = 6.28; +SELECT svartest_dml.fx06(); + fx06 +------ + 6.28 +(1 row) + +LET sesvar43 = VARIABLE(sesvar43) * 2; +SELECT svartest_dml.fx06(); + fx06 +------- + 12.56 +(1 row) + +-- plan cache invalidation test +DROP VARIABLE sesvar43; +-- should fail +SELECT svartest_dml.fx06(); +ERROR: session variable "sesvar43" doesn't exist +LINE 1: VARIABLE(sesvar43) + ^ +QUERY: VARIABLE(sesvar43) +CONTEXT: PL/pgSQL function svartest_dml.fx06() line 3 at RETURN +CREATE VARIABLE sesvar43 AS numeric; +LET sesvar43 = 2.72; +SELECT svartest_dml.fx06(); + fx06 +------ + 2.72 +(1 row) + +DROP VARIABLE sesvar43; +CREATE DOMAIN svartest_dml.int_not_null AS int CHECK(value IS NOT NULL); +CREATE VARIABLE svartest_dml.sesvar46 AS svartest_dml.int_not_null; +-- should fail +LET svartest_dml.sesvar46 = NULL; +ERROR: value for domain svartest_dml.int_not_null violates check constraint "int_not_null_check" +-- should be ok +LET svartest_dml.sesvar46 = 100; +LET svartest_dml.sesvar45 = ROW(1,2,3,4); +LET svartest_dml.sesvar45.a = 100; +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + row_to_json +----------------------------- + {"a":100,"b":2,"c":3,"d":4} +(1 row) + +CREATE ROLE regress_svartest_dml_write_only_role; +GRANT USAGE ON SCHEMA svartest_dml TO regress_svartest_dml_write_only_role; +GRANT UPDATE ON VARIABLE svartest_dml.sesvar45 TO regress_svartest_dml_write_only_role; +SET ROLE TO regress_svartest_dml_write_only_role; +-- should fail +SELECT VARIABLE(svartest_dml.sesvar45); +ERROR: permission denied for session variable sesvar45 +-- should be ok +LET svartest_dml.sesvar45.b = 200; +SET ROLE TO DEFAULT; +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + row_to_json +------------------------------- + {"a":100,"b":200,"c":3,"d":4} +(1 row) + +CREATE VARIABLE svartest_dml.sesvar47 AS int[]; +LET svartest_dml.sesvar47 = ARRAY[1,2,3]; +GRANT UPDATE ON VARIABLE svartest_dml.sesvar47 TO regress_svartest_dml_write_only_role; +SET ROLE TO regress_svartest_dml_write_only_role; +-- should fail +SELECT VARIABLE(svartest_dml.sesvar47); +ERROR: permission denied for session variable sesvar47 +-- should be ok +LET svartest_dml.sesvar47[1] = 200; +SET ROLE TO DEFAULT; +SELECT VARIABLE(svartest_dml.sesvar47); + sesvar47 +----------- + {200,2,3} +(1 row) + +CREATE VARIABLE svartest_dml.sesvar48 AS int4multirange[]; +LET svartest_dml.sesvar48 = NULL; +LET svartest_dml.sesvar48 = '{"{[2,8),[11,14)}","{[5,8),[12,14)}"}'; +LET svartest_dml.sesvar48[2] = '{[5,8),[12,100)}'; +SELECT VARIABLE(svartest_dml.sesvar48); + sesvar48 +---------------------------------------- + {"{[2,8),[11,14)}","{[5,8),[12,100)}"} +(1 row) + +DROP SCHEMA svartest_dml CASCADE; +NOTICE: drop cascades to 13 other objects +DETAIL: drop cascades to session variable svartest_dml.sesvar44 +drop cascades to type svartest_dml.composite_type +drop cascades to session variable svartest_dml.sesvar45 +drop cascades to function svartest_dml.fx01(numeric) +drop cascades to function svartest_dml.fx02() +drop cascades to function svartest_dml.fx03(character varying) +drop cascades to function svartest_dml.fx04(character varying) +drop cascades to function svartest_dml.fx05(integer,integer,integer) +drop cascades to function svartest_dml.fx06() +drop cascades to type svartest_dml.int_not_null +drop cascades to session variable svartest_dml.sesvar46 +drop cascades to session variable svartest_dml.sesvar47 +drop cascades to session variable svartest_dml.sesvar48 +DROP ROLE regress_svartest_dml_write_only_role; diff --git a/src/test/regress/sql/session_variables_dml.sql b/src/test/regress/sql/session_variables_dml.sql index b2870dde9e9..1250e7ef062 100644 --- a/src/test/regress/sql/session_variables_dml.sql +++ b/src/test/regress/sql/session_variables_dml.sql @@ -159,3 +159,180 @@ DROP ROLE regress_svartest_dml_read_role; DROP VARIABLE sesvar40; DROP TABLE svartest_dml; + +CREATE VARIABLE sesvar43 AS numeric; + +-- LET stmt is not allowed inside CTE +WITH x AS (LET sesvar43 = 3.14) SELECT * FROM x; + +-- LET stmt requires result with exactly one row +LET sesvar43 = generate_series(1,1); + +-- should fail +LET sesvar43 = generate_series(1,2); +LET sesvar43 = generate_series(1,0); + +CREATE SCHEMA svartest_dml; +CREATE VARIABLE svartest_dml.sesvar44 AS varchar; +CREATE TYPE svartest_dml.composite_type AS (a int, b int, c int); +CREATE VARIABLE svartest_dml.sesvar45 AS svartest_dml.composite_type; + +CREATE OR REPLACE FUNCTION svartest_dml.fx01(numeric) +RETURNS void AS $$ +LET sesvar43 = $1; +$$ LANGUAGE sql; + +CREATE OR REPLACE FUNCTION svartest_dml.fx02() +RETURNS numeric AS $$ +SELECT VARIABLE(sesvar43); +$$ LANGUAGE sql; + +SELECT svartest_dml.fx01(3.14); +SELECT svartest_dml.fx02(), VARIABLE(sesvar43); + +CREATE OR REPLACE FUNCTION svartest_dml.fx03(s varchar) +RETURNS varchar AS $$ +BEGIN + LET svartest_dml.sesvar44 = s; + RETURN VARIABLE(svartest_dml.sesvar44); +END +$$ LANGUAGE plpgsql; + +SELECT svartest_dml.fx03('Hello'); + +CREATE OR REPLACE FUNCTION svartest_dml.fx04(s varchar) +RETURNS varchar AS $$ +BEGIN + LET sesvar44 = s; + RETURN VARIABLE(sesvar44); +END +$$ LANGUAGE plpgsql +SET SEARCH_PATH TO 'svartest_dml'; + +SELECT svartest_dml.fx04('Hello'); + +CREATE OR REPLACE FUNCTION svartest_dml.fx05(a int, b int, c int) +RETURNS svartest_dml.composite_type AS $$ +BEGIN + LET svartest_dml.sesvar45 = ROW(a, b, c); + RETURN VARIABLE(svartest_dml.sesvar45); +END; +$$ LANGUAGE plpgsql; + +SELECT row_to_json(svartest_dml.fx05(10, 20, 30)); + +SELECT VARIABLE(svartest_dml.sesvar45); +SELECT VARIABLE(svartest_dml.sesvar45).*; +SELECT VARIABLE(svartest_dml.sesvar45.a); +SELECT VARIABLE(svartest_dml.sesvar45).a; + +ALTER TYPE svartest_dml.composite_type ADD ATTRIBUTE d int; + +-- composite value should be still readable +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + +LET svartest_dml.sesvar45 = ROW(100, 200, 300, NULL); +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + +-- use variables inside view +CREATE VIEW svartest_dml.view01 AS SELECT VARIABLE(svartest_dml.sesvar45).*; +SELECT * FROM svartest_dml.view01; + +-- start new connection +\c +SELECT * FROM svartest_dml.view01; + +LET svartest_dml.sesvar45 = ROW(5, 6, 7, 8); + +SELECT * FROM svartest_dml.view01; + +-- should fail (dependency) +DROP VARIABLE svartest_dml.sesvar45; + +DROP VIEW svartest_dml.view01; + +-- test of access variables from generic plans +CREATE OR REPLACE FUNCTION svartest_dml.fx06() +RETURNS numeric AS $$ +BEGIN + RETURN VARIABLE(sesvar43); +END; +$$ LANGUAGE plpgsql; + +SET plan_cache_mode TO force_generic_plan; + +LET sesvar43 = 6.28; + +SELECT svartest_dml.fx06(); + +LET sesvar43 = VARIABLE(sesvar43) * 2; + +SELECT svartest_dml.fx06(); + +-- plan cache invalidation test +DROP VARIABLE sesvar43; + +-- should fail +SELECT svartest_dml.fx06(); + +CREATE VARIABLE sesvar43 AS numeric; + +LET sesvar43 = 2.72; + +SELECT svartest_dml.fx06(); + +DROP VARIABLE sesvar43; + +CREATE DOMAIN svartest_dml.int_not_null AS int CHECK(value IS NOT NULL); +CREATE VARIABLE svartest_dml.sesvar46 AS svartest_dml.int_not_null; + +-- should fail +LET svartest_dml.sesvar46 = NULL; +-- should be ok +LET svartest_dml.sesvar46 = 100; + +LET svartest_dml.sesvar45 = ROW(1,2,3,4); +LET svartest_dml.sesvar45.a = 100; +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + +CREATE ROLE regress_svartest_dml_write_only_role; +GRANT USAGE ON SCHEMA svartest_dml TO regress_svartest_dml_write_only_role; +GRANT UPDATE ON VARIABLE svartest_dml.sesvar45 TO regress_svartest_dml_write_only_role; + +SET ROLE TO regress_svartest_dml_write_only_role; + +-- should fail +SELECT VARIABLE(svartest_dml.sesvar45); + +-- should be ok +LET svartest_dml.sesvar45.b = 200; + +SET ROLE TO DEFAULT; + +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + +CREATE VARIABLE svartest_dml.sesvar47 AS int[]; +LET svartest_dml.sesvar47 = ARRAY[1,2,3]; + +GRANT UPDATE ON VARIABLE svartest_dml.sesvar47 TO regress_svartest_dml_write_only_role; + +SET ROLE TO regress_svartest_dml_write_only_role; + +-- should fail +SELECT VARIABLE(svartest_dml.sesvar47); + +-- should be ok +LET svartest_dml.sesvar47[1] = 200; + +SET ROLE TO DEFAULT; + +SELECT VARIABLE(svartest_dml.sesvar47); + +CREATE VARIABLE svartest_dml.sesvar48 AS int4multirange[]; +LET svartest_dml.sesvar48 = NULL; +LET svartest_dml.sesvar48 = '{"{[2,8),[11,14)}","{[5,8),[12,14)}"}'; +LET svartest_dml.sesvar48[2] = '{[5,8),[12,100)}'; +SELECT VARIABLE(svartest_dml.sesvar48); + +DROP SCHEMA svartest_dml CASCADE; +DROP ROLE regress_svartest_dml_write_only_role; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 0c859cfe187..b08ead23a79 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1542,6 +1542,7 @@ LargeObjectDesc Latch LauncherLastStartTimesEntry LerpFunc +LetStmt LexDescr LexemeEntry LexemeHashKey -- 2.49.0