From 4c4c9377da10d7df0b33d13958a064b23ce0ad91 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 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The value is assigned to session variables usually by SET command. Unfortunately there are two reasons why SET should not be used for this purpose in Postgres. 1. Using a_expr inside generic_set ram rule produces reduce conflicts, so it needs    total reimplementation of related gram rules. 2. SET is no plan command - so it doesn't support usage of parameters. 3. Excepting implementation issues, there is fact, so if we use SET command    for assigning values to session variables, then there can be collisions    between session variables and GUC, and then we need some concepts, how    these collisions should be solved, or how to protect self against these    collisions. With the dedicated command, the collisions between GUC and session    variables are not possible. The command LET is executed as usual query execution. The result is stored to the target session variable (resultVariable) by using VariableDestReceiver. 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 | 136 ++++++++- src/backend/parser/gram.y | 39 ++- src/backend/parser/parse_agg.c | 7 + src/backend/parser/parse_expr.c | 26 +- src/backend/parser/parse_func.c | 3 + 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/parser/parse_node.h | 1 + src/include/tcop/cmdtaglist.h | 1 + .../expected/session_variables_dml.out | 277 ++++++++++++++++++ .../regress/sql/session_variables_dml.sql | 189 ++++++++++++ src/tools/pgindent/typedefs.list | 1 + 31 files changed, 1043 insertions(+), 32 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 910989a7aae..6cba31aaefc 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -5695,10 +5695,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 61db22a4c25..e392dc8d084 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 0fe33a7efcb..5b3be89f732 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..9bdc92bfd30 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, NULL); + + /* + * 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 4c5b101c8ee..76f478ff580 100644 --- a/src/backend/executor/execMain.c +++ b/src/backend/executor/execMain.c @@ -234,13 +234,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 c761fde6acb..0d5461cbd6b 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -4374,6 +4374,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 cc03b0311a4..8dccd50737a 100644 --- a/src/backend/optimizer/plan/planner.c +++ b/src/backend/optimizer/plan/planner.c @@ -376,6 +376,20 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions, glob->rel_notnullatts_hash = 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 @@ -621,6 +635,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 a12a3c6094c..0cbe09834c5 100644 --- a/src/backend/optimizer/plan/setrefs.c +++ b/src/backend/optimizer/plan/setrefs.c @@ -2249,6 +2249,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; } @@ -3734,7 +3755,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) @@ -3836,9 +3857,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) @@ -3861,6 +3883,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 c3897aafd21..4038876497d 100644 --- a/src/backend/parser/analyze.c +++ b/src/backend/parser/analyze.c @@ -52,15 +52,18 @@ #include "utils/builtins.h" #include "utils/guc.h" #include "utils/rel.h" +#include "utils/lsyscache.h" #include "utils/syscache.h" -/* Passthrough data for transformPLAssignStmtTarget */ +/* Passthrough data for transformAssignTarget */ typedef struct SelectStmtPassthrough { - PLAssignStmt *stmt; /* the assignment statement */ + Node *stmt; /* the assignment statement */ Node *target; /* node representing the target variable */ + char *target_name; /* the name used by err */ List *indirection; /* indirection yet to be applied to target */ + CoercionContext ccontext; /* context indicators to control coercions */ } SelectStmtPassthrough; /* Hook for plugins to get control at end of parse analysis */ @@ -84,7 +87,7 @@ static Query *transformReturnStmt(ParseState *pstate, ReturnStmt *stmt); static Query *transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt); static Query *transformPLAssignStmt(ParseState *pstate, PLAssignStmt *stmt); -static List *transformPLAssignStmtTarget(ParseState *pstate, List *tlist, +static List *transformAssignTarget(ParseState *pstate, List *tlist, SelectStmtPassthrough *passthru); static Query *transformDeclareCursorStmt(ParseState *pstate, DeclareCursorStmt *stmt); @@ -94,6 +97,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 @@ -341,6 +346,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: @@ -420,6 +426,11 @@ transformStmt(ParseState *pstate, Node *parseTree) (CallStmt *) parseTree); break; + case T_LetStmt: + result = transformLetStmt(pstate, + (LetStmt *) parseTree); + break; + default: /* @@ -481,6 +492,7 @@ stmt_requires_parse_analysis(RawStmt *parseTree) case T_ExplainStmt: case T_CreateTableAsStmt: case T_CallStmt: + case T_LetStmt: result = true; break; @@ -546,6 +558,7 @@ query_requires_rewrite_plan(Query *query) case T_ExplainStmt: case T_CreateTableAsStmt: case T_CallStmt: + case T_LetStmt: result = true; break; default: @@ -1389,7 +1402,7 @@ count_rowexpr_columns(ParseState *pstate, Node *expr) * * This function is also used to transform the source expression of a * PLAssignStmt. In that usage, passthru is non-NULL and we need to - * call transformPLAssignStmtTarget after the initial transformation of the + * call transformAssignTarget after the initial transformation of the * SELECT's targetlist. (We could generalize this into an arbitrary callback * function, but for now that would just be more notation with no benefit.) * All the rest is the same as a regular SelectStmt. @@ -1442,8 +1455,8 @@ transformSelectStmt(ParseState *pstate, SelectStmt *stmt, * Otherwise, mark column origins (which are useless in a PLAssignStmt). */ if (passthru) - qry->targetList = transformPLAssignStmtTarget(pstate, qry->targetList, - passthru); + qry->targetList = transformAssignTarget(pstate, qry->targetList, + passthru); else markTargetListOrigins(pstate, qry->targetList); @@ -2841,9 +2854,11 @@ transformPLAssignStmt(ParseState *pstate, PLAssignStmt *stmt) EXPR_KIND_UPDATE_TARGET); /* Set up passthrough data for transformPLAssignStmtTarget */ - passthru.stmt = stmt; + passthru.stmt = (Node *) stmt; passthru.target = target; + passthru.target_name = stmt->name; passthru.indirection = indirection; + passthru.ccontext = COERCION_PLPGSQL; /* * To avoid duplicating a lot of code, we use transformSelectStmt to do @@ -2866,18 +2881,21 @@ transformPLAssignStmt(ParseState *pstate, PLAssignStmt *stmt) /* * Callback function to adjust a SELECT's tlist to make the output suitable - * for assignment to a PLAssignStmt's target variable. + * for assignment to a PLAssignStmt's target variable pr LET's target + * session variable. * * Note: we actually modify the tle->expr in-place, but the function's API * is set up to not presume that. */ static List * -transformPLAssignStmtTarget(ParseState *pstate, List *tlist, - SelectStmtPassthrough *passthru) +transformAssignTarget(ParseState *pstate, List *tlist, + SelectStmtPassthrough *passthru) { - PLAssignStmt *stmt = passthru->stmt; + Node *stmt = passthru->stmt; Node *target = passthru->target; + char *target_name = passthru->target_name; List *indirection = passthru->indirection; + CoercionContext ccontext = passthru->ccontext; Oid targettype; int32 targettypmod; Oid targetcollation; @@ -2912,7 +2930,7 @@ transformPLAssignStmtTarget(ParseState *pstate, List *tlist, tle->expr = (Expr *) transformAssignmentIndirection(pstate, target, - stmt->name, + target_name, false, targettype, targettypmod, @@ -2920,10 +2938,10 @@ transformPLAssignStmtTarget(ParseState *pstate, List *tlist, indirection, list_head(indirection), (Node *) tle->expr, - COERCION_PLPGSQL, + ccontext, exprLocation(target)); } - else if (targettype != type_id && + else if (IsA(stmt, PLAssignStmt) && targettype != type_id && (targettype == RECORDOID || ISCOMPLEX(targettype)) && (type_id == RECORDOID || ISCOMPLEX(type_id))) { @@ -2946,7 +2964,7 @@ transformPLAssignStmtTarget(ParseState *pstate, List *tlist, coerce_to_target_type(pstate, orig_expr, type_id, targettype, targettypmod, - COERCION_PLPGSQL, + ccontext, COERCE_IMPLICIT_CAST, -1); /* With COERCION_PLPGSQL, this error is probably unreachable */ @@ -2955,7 +2973,7 @@ transformPLAssignStmtTarget(ParseState *pstate, List *tlist, (errcode(ERRCODE_DATATYPE_MISMATCH), errmsg("variable \"%s\" is of type %s" " but expression is of type %s", - stmt->name, + target_name, format_type_be(targettype), format_type_be(type_id)), errhint("You will need to rewrite or cast the expression."), @@ -3319,6 +3337,92 @@ transformCallStmt(ParseState *pstate, CallStmt *stmt) return result; } +/* + * transformLetStmt - + * transform an Let Statement + */ +static Query * +transformLetStmt(ParseState *pstate, LetStmt *stmt) +{ + Query *qry; + Query *result; + Node *target; + List *names = NULL; + int nnames; + List *indirection; + VariableFence *vf; + SelectStmtPassthrough passthru; + Param *paramvar; + + /* gram allows only SELECT */ + Assert(IsA(stmt->query, SelectStmt)); + + names = NamesFromList(stmt->target); + nnames = list_length(names); + + /* Use implicit VariableFence for forcing session variables */ + vf = makeNode(VariableFence); + vf->varname = names; + vf->location = stmt->location; + + target = transformExpr(pstate, (Node *) vf, EXPR_KIND_LET_TARGET); + + /* + * The FieldStore is returned when field name is specified. + * In this case decrease nnames number (number of dotted names), + * and get target from FieldSelect node. Field name will be part + * of indirection. + */ + if (IsA(target, FieldStore)) + { + nnames -= 1; + target = (Node *)((FieldStore *) target)->arg; + } + + paramvar = castNode(Param, target); + + Assert(paramvar->paramkind == PARAM_VARIABLE); + + if (list_length(stmt->target) > nnames) + indirection = list_copy_tail(stmt->target, nnames); + else + indirection = NIL; + + /* + * The parameter used as basenode has to have special mark, + * because requires special access when we do SELECT access check. + */ + if (indirection) + { + paramvar->parambasenode = true; + pstate->p_hasSessionVariables = true; + } + + /* Set up passthrough data for transformAssignTarget */ + passthru.stmt = (Node *) stmt; + passthru.target = (Node *) paramvar; + passthru.target_name = get_session_variable_name(paramvar->paramvarid); + passthru.indirection = indirection; + passthru.ccontext = COERCION_ASSIGNMENT; + + /* we need to postpone conversion of "unknown" to text */ + pstate->p_resolve_unknowns = false; + + qry = transformSelectStmt(pstate, (SelectStmt *) stmt->query, &passthru); + + qry->resultVariable = paramvar->paramvarid; + qry->canSetTag = true; + + stmt->query = (Node *) qry; + + /* 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 f5c2f151752..738d1391684 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -302,7 +302,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 @@ -748,7 +748,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 LET LEFT LEVEL LIKE LIMIT LISTEN LOAD LOCAL LOCALTIME LOCALTIMESTAMP LOCATION LOCK_P LOCKED LOGGED LSN_P MAPPING MATCH MATCHED MATERIALIZED MAXVALUE MERGE MERGE_ACTION METHOD @@ -1095,6 +1095,7 @@ stmt: | ImportForeignSchemaStmt | IndexStmt | InsertStmt + | LetStmt | ListenStmt | RefreshMatViewStmt | LoadStmt @@ -12983,6 +12984,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: @@ -18099,6 +18132,7 @@ unreserved_keyword: | LARGE_P | LAST_P | LEAKPROOF + | LET | LEVEL | LISTEN | LOAD @@ -18716,6 +18750,7 @@ bare_label_keyword: | LEAKPROOF | LEAST | LEFT + | LET | LEVEL | LIKE | LISTEN diff --git a/src/backend/parser/parse_agg.c b/src/backend/parser/parse_agg.c index 3254c83cc6c..ddf7905654c 100644 --- a/src/backend/parser/parse_agg.c +++ b/src/backend/parser/parse_agg.c @@ -580,6 +580,10 @@ check_agglevels_and_constraints(ParseState *pstate, Node *expr) errkind = true; break; + case EXPR_KIND_LET_TARGET: + errkind = true; + break; + /* * There is intentionally no default: case here, so that the * compiler will warn if we add a new ParseExprKind without @@ -996,6 +1000,9 @@ transformWindowFuncCall(ParseState *pstate, WindowFunc *wfunc, case EXPR_KIND_CYCLE_MARK: errkind = true; break; + case EXPR_KIND_LET_TARGET: + errkind = true; + break; /* * There is intentionally no default: case here, so that the diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index 8b32f74de23..ff16a3bc3cc 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -593,6 +593,9 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref) case EXPR_KIND_PARTITION_BOUND: err = _("cannot use column reference in partition bound expression"); break; + case EXPR_KIND_LET_TARGET: + err = _("cannot use column reference as target of LET command"); + break; /* * There is intentionally no default: case here, so that the @@ -963,6 +966,7 @@ expr_kind_allows_session_variables(ParseExprKind p_expr_kind) case EXPR_KIND_RETURNING: case EXPR_KIND_VALUES: case EXPR_KIND_VALUES_SINGLE: + case EXPR_KIND_LET_TARGET: result = true; break; @@ -1998,6 +2002,9 @@ transformSubLink(ParseState *pstate, SubLink *sublink) case EXPR_KIND_GENERATED_COLUMN: err = _("cannot use subquery in column generation expression"); break; + case EXPR_KIND_LET_TARGET: + err = _("cannot use subquery as a target of LET command"); + break; /* * There is intentionally no default: case here, so that the @@ -3261,7 +3268,8 @@ make_nulltest_from_distinct(ParseState *pstate, A_Expr *distincta, Node *arg) } /* - * Generate param variable for reference to session variable + * Generate param variable for reference to session variable. It can be + * wrapped by FieldSelect (Read) or FieldStore (Write) nodes. */ static Node * makeParamSessionVariable(ParseState *pstate, @@ -3285,6 +3293,20 @@ makeParamSessionVariable(ParseState *pstate, TupleDesc tupdesc; int i; + if (pstate->p_expr_kind == EXPR_KIND_LET_TARGET) + { + FieldStore *fstore = makeNode(FieldStore); + + /* + * make aux FieldStore wrapper - it is used just for signalization, + * so session variable has an attribute. All necessary transformations + * and checks will be done in transformAssignmentIndirection. + */ + fstore->arg = (Expr *) param; + + return (Node *) fstore; + } + tupdesc = lookup_rowtype_tupdesc_noerror(typid, typmod, true); if (!tupdesc) ereport(ERROR, @@ -3426,6 +3448,8 @@ ParseExprKindName(ParseExprKind exprKind) return "GENERATED AS"; case EXPR_KIND_CYCLE_MARK: return "CYCLE"; + case EXPR_KIND_LET_TARGET: + return "LET"; /* * There is intentionally no default: case here, so that the diff --git a/src/backend/parser/parse_func.c b/src/backend/parser/parse_func.c index 778d69c6f3c..13616c9b3c2 100644 --- a/src/backend/parser/parse_func.c +++ b/src/backend/parser/parse_func.c @@ -2783,6 +2783,9 @@ check_srf_call_placement(ParseState *pstate, Node *last_srf, int location) case EXPR_KIND_CYCLE_MARK: errkind = true; break; + case EXPR_KIND_LET_TARGET: + errkind = true; + break; /* * There is intentionally no default: case here, so that the diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 760d0faaad0..0ea7a166977 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -236,6 +236,7 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree) case T_CallStmt: case T_DoStmt: + case T_LetStmt: { /* * Commands inside the DO block or the called procedure might @@ -1065,6 +1066,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, @@ -2212,6 +2218,10 @@ UtilityContainsQuery(Node *parsetree) return UtilityContainsQuery(qry->utilityStmt); return qry; + case T_LetStmt: + qry = castNode(Query, ((LetStmt *) parsetree)->query); + return qry; + default: return NULL; } @@ -2410,6 +2420,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: { @@ -3299,6 +3313,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 1d20f3382b0..c8e91be1482 100644 --- a/src/backend/utils/cache/plancache.c +++ b/src/backend/utils/cache/plancache.c @@ -2053,6 +2053,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 094e65db849..fe020a9b381 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -1272,8 +1272,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", @@ -4820,6 +4820,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 a4a9e281463..0fcce621c34 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 */ @@ -2170,6 +2173,18 @@ typedef struct MergeStmt WithClause *withClause; /* WITH clause */ } 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 75a863dad93..50d4a20be77 100644 --- a/src/include/nodes/pathnodes.h +++ b/src/include/nodes/pathnodes.h @@ -192,6 +192,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 0aa52cbc4c5..cf863c43d8c 100644 --- a/src/include/nodes/plannodes.h +++ b/src/include/nodes/plannodes.h @@ -161,6 +161,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 5f3a1a2d1be..9107891fa9d 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -404,6 +404,15 @@ typedef struct Param Oid paramcollid; /* 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 d1ad6861805..badd1618af8 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -258,6 +258,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/parser/parse_node.h b/src/include/parser/parse_node.h index 84e886940d8..026743b7337 100644 --- a/src/include/parser/parse_node.h +++ b/src/include/parser/parse_node.h @@ -82,6 +82,7 @@ typedef enum ParseExprKind EXPR_KIND_COPY_WHERE, /* WHERE condition in COPY FROM */ EXPR_KIND_GENERATED_COLUMN, /* generation expression for a column */ EXPR_KIND_CYCLE_MARK, /* cycle mark value */ + EXPR_KIND_LET_TARGET, /* only session variables */ } ParseExprKind; diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h index cbb09fcf511..be14059cc79 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..67f78a548dc 100644 --- a/src/test/regress/expected/session_variables_dml.out +++ b/src/test/regress/expected/session_variables_dml.out @@ -189,3 +189,280 @@ 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) + +-- test extended query protocol +CREATE VARIABLE svartest_dml.sesvar49 AS int; +LET svartest_dml.sesvar49 = $1 \bind 10 \g +SELECT VARIABLE(svartest_dml.sesvar49); + sesvar49 +---------- + 10 +(1 row) + +LET svartest_dml.sesvar49 = $1 \parse letps +\bind_named letps 100 \g +SELECT VARIABLE(svartest_dml.sesvar49); + sesvar49 +---------- + 100 +(1 row) + +\close_prepared letps +DROP SCHEMA svartest_dml CASCADE; +NOTICE: drop cascades to 14 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 cascades to session variable svartest_dml.sesvar49 +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..2c8d6c3a497 100644 --- a/src/test/regress/sql/session_variables_dml.sql +++ b/src/test/regress/sql/session_variables_dml.sql @@ -159,3 +159,192 @@ 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); + +-- test extended query protocol +CREATE VARIABLE svartest_dml.sesvar49 AS int; + +LET svartest_dml.sesvar49 = $1 \bind 10 \g +SELECT VARIABLE(svartest_dml.sesvar49); + +LET svartest_dml.sesvar49 = $1 \parse letps +\bind_named letps 100 \g +SELECT VARIABLE(svartest_dml.sesvar49); + +\close_prepared letps + +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 c022959ecca..d3a9433b432 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1549,6 +1549,7 @@ LargeObjectDesc Latch LauncherLastStartTimesEntry LerpFunc +LetStmt LexDescr LexemeEntry LexemeHashKey -- 2.51.1