From 56eb709074a74edd067ac5bca73cb40cb46d571a Mon Sep 17 00:00:00 2001 From: "okbob@github.com" Date: Tue, 5 Apr 2022 00:54:29 +0200 Subject: [PATCH 03/11] LET command Set session variable value to result of query or result of default expression --- src/backend/commands/explain.c | 16 ++ src/backend/commands/session_variable.c | 81 ++++++- src/backend/executor/Makefile | 1 + src/backend/executor/execMain.c | 1 + src/backend/executor/svariableReceiver.c | 154 ++++++++++++ src/backend/nodes/nodeFuncs.c | 10 + src/backend/parser/analyze.c | 283 +++++++++++++++++++++++ src/backend/parser/gram.y | 53 ++++- src/backend/parser/parse_agg.c | 4 + src/backend/parser/parse_expr.c | 4 + src/backend/parser/parse_func.c | 3 + src/backend/parser/parse_target.c | 4 +- src/backend/rewrite/rewriteHandler.c | 34 +++ src/backend/rewrite/rowsecurity.c | 8 +- src/backend/tcop/dest.c | 7 + src/backend/tcop/utility.c | 15 ++ src/backend/utils/cache/plancache.c | 15 +- src/include/commands/session_variable.h | 3 + src/include/executor/svariableReceiver.h | 25 ++ src/include/nodes/parsenodes.h | 15 ++ src/include/nodes/plannodes.h | 2 +- src/include/parser/kwlist.h | 1 + src/include/parser/parse_node.h | 3 +- src/include/tcop/cmdtaglist.h | 1 + src/include/tcop/dest.h | 3 +- 25 files changed, 734 insertions(+), 12 deletions(-) create mode 100644 src/backend/executor/svariableReceiver.c create mode 100644 src/include/executor/svariableReceiver.h diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c index 053d2ca5ae..1e93cb1523 100644 --- a/src/backend/commands/explain.c +++ b/src/backend/commands/explain.c @@ -492,6 +492,22 @@ ExplainOneUtility(Node *utilityStmt, IntoClause *into, ExplainState *es, else ExplainDummyGroup("Notify", NULL, es); } + else if (IsA(utilityStmt, LetStmt)) + { + LetStmt *letstmt = (LetStmt *) utilityStmt; + List *rewritten; + + if (es->format == EXPLAIN_FORMAT_TEXT) + appendStringInfoString(es->str, "SET SESSION VARIABLE\n"); + else + ExplainDummyGroup("Set Session Variable", NULL, es); + + rewritten = QueryRewrite(castNode(Query, copyObject(letstmt->query))); + Assert(list_length(rewritten) == 1); + ExplainOneQuery(linitial_node(Query, rewritten), + CURSOR_OPT_PARALLEL_OK, NULL, es, + queryString, params, queryEnv); + } else { if (es->format == EXPLAIN_FORMAT_TEXT) diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index 8378921a75..09610b14bf 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -786,7 +786,7 @@ SetSessionVariableWithSecurityCheck(Oid varid, Datum value, bool isNull) /* * Is possible to write to session variable? */ - aclresult = pg_variable_aclcheck(varid, GetUserId(), ACL_WRITE); + aclresult = pg_variable_aclcheck(varid, GetUserId(), ACL_UPDATE); if (aclresult != ACLCHECK_OK) aclcheck_error(aclresult, OBJECT_VARIABLE, get_session_variable_name(varid)); @@ -1122,6 +1122,85 @@ ResetSessionVariables(void) xact_recheck_varids = NIL; } +/* + * Assign result of evaluated expression to 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)); + + /* + * Is it allowed to write to session variable? + */ + aclresult = pg_variable_aclcheck(varid, GetUserId(), ACL_UPDATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_VARIABLE, get_session_variable_name(varid)); + + /* Create dest receiver for LET */ + dest = CreateDestReceiver(DestVariable); + SetVariableDestReceiverParams(dest, varid); + + /* run rewriter - can be used for replacement of DEFAULT node */ + 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 + * 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 */ + ExecutorRun(queryDesc, ForwardScanDirection, 2L, true); + + /* save the rowcount if we're given a qc to fill */ + if (qc) + SetQueryCompletion(qc, CMDTAG_LET, queryDesc->estate->es_processed); + + /* and clean up */ + ExecutorFinish(queryDesc); + ExecutorEnd(queryDesc); + + FreeQueryDesc(queryDesc); + + PopActiveSnapshot(); +} + /* * Registration of actions to be executed on session variables at transaction * end time. We want to drop temporary session variables with clause ON COMMIT diff --git a/src/backend/executor/Makefile b/src/backend/executor/Makefile index 11118d0ce0..71248a34f2 100644 --- a/src/backend/executor/Makefile +++ b/src/backend/executor/Makefile @@ -76,6 +76,7 @@ OBJS = \ nodeWindowAgg.o \ nodeWorktablescan.o \ spi.o \ + svariableReceiver.o \ tqueue.o \ tstoreReceiver.o diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c index 68e748eb73..00836a809a 100644 --- a/src/backend/executor/execMain.c +++ b/src/backend/executor/execMain.c @@ -51,6 +51,7 @@ #include "commands/session_variable.h" #include "executor/execdebug.h" #include "executor/nodeSubplan.h" +#include "executor/svariableReceiver.h" #include "foreign/fdwapi.h" #include "jit/jit.h" #include "mb/pg_wchar.h" diff --git a/src/backend/executor/svariableReceiver.c b/src/backend/executor/svariableReceiver.c new file mode 100644 index 0000000000..2cd5e67aa6 --- /dev/null +++ b/src/backend/executor/svariableReceiver.c @@ -0,0 +1,154 @@ +/*------------------------------------------------------------------------- + * + * svariableReceiver.c + * An implementation of DestReceiver that stores the result value in + * a session variable. + * + * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/backend/executor/svariableReceiver.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "access/detoast.h" +#include "executor/svariableReceiver.h" +#include "commands/session_variable.h" + +typedef struct +{ + DestReceiver pub; + Oid varid; + Oid typid; + int32 typmod; + int typlen; + int slot_offset; + int rows; +} svariableState; + + +/* + * Prepare to receive tuples from executor. + */ +static void +svariableStartupReceiver(DestReceiver *self, int operation, TupleDesc typeinfo) +{ + svariableState *myState = (svariableState *) self; + int natts = typeinfo->natts; + int outcols = 0; + int i; + + /* Receiver should be initialized by SetVariableDestReceiverParams */ + Assert(OidIsValid(myState->varid)); + + for (i = 0; i < natts; i++) + { + Form_pg_attribute attr = TupleDescAttr(typeinfo, i); + + if (attr->attisdropped) + continue; + + if (++outcols > 1) + elog(ERROR, "svariable DestReceiver can take only one attribute"); + + myState->typid = attr->atttypid; + myState->typmod = attr->atttypmod; + myState->typlen = attr->attlen; + myState->slot_offset = i; + } + + if (outcols == 0) + elog(ERROR, "svariable DestReceiver requires one attribute"); + + myState->rows = 0; +} + +/* + * Receive a tuple from the executor and store it in session variable. + */ +static bool +svariableReceiveSlot(TupleTableSlot *slot, DestReceiver *self) +{ + svariableState *myState = (svariableState *) self; + Datum value; + bool isnull; + bool freeval = false; + + /* Make sure the tuple is fully deconstructed */ + slot_getallattrs(slot); + + value = slot->tts_values[myState->slot_offset]; + isnull = slot->tts_isnull[myState->slot_offset]; + + if (myState->typlen == -1 && !isnull && VARATT_IS_EXTERNAL(DatumGetPointer(value))) + { + value = PointerGetDatum(detoast_external_attr((struct varlena *) + DatumGetPointer(value))); + freeval = true; + } + + SetSessionVariable(myState->varid, value, isnull); + + if (freeval) + pfree(DatumGetPointer(value)); + + return true; +} + +/* + * Clean up at end of an executor run + */ +static void +svariableShutdownReceiver(DestReceiver *self) +{ + /* Nothing to do. */ +} + +/* + * Destroy receiver when done with it + */ +static void +svariableDestroyReceiver(DestReceiver *self) +{ + pfree(self); +} + +/* + * Initially create a DestReceiver object. + */ +DestReceiver * +CreateVariableDestReceiver(void) +{ + svariableState *self = (svariableState *) palloc0(sizeof(svariableState)); + + self->pub.receiveSlot = svariableReceiveSlot; + self->pub.rStartup = svariableStartupReceiver; + self->pub.rShutdown = svariableShutdownReceiver; + self->pub.rDestroy = svariableDestroyReceiver; + self->pub.mydest = DestVariable; + + /* + * Private fields will be set by SetVariableDestReceiverParams and + * svariableStartupReceiver. + */ + return (DestReceiver *) self; +} + +/* + * Set parameters for a VariableDestReceiver. + * Should be called right after creating the DestReceiver. + */ +void +SetVariableDestReceiverParams(DestReceiver *self, Oid varid) +{ + svariableState *myState = (svariableState *) self; + + Assert(myState->pub.mydest == DestVariable); + Assert(OidIsValid(varid)); + + myState->varid = varid; +} diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index 3bac350bf5..1b5dfa174a 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -3822,6 +3822,16 @@ raw_expression_tree_walker(Node *node, return true; } break; + case T_LetStmt: + { + LetStmt *stmt = (LetStmt *) node; + + if (walker(stmt->target, context)) + return true; + if (walker(stmt->query, context)) + return true; + } + break; case T_PLAssignStmt: { PLAssignStmt *stmt = (PLAssignStmt *) node; diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c index a013031d22..7eb30a101a 100644 --- a/src/backend/parser/analyze.c +++ b/src/backend/parser/analyze.c @@ -25,8 +25,11 @@ #include "postgres.h" #include "access/sysattr.h" +#include "catalog/namespace.h" #include "catalog/pg_proc.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" +#include "commands/session_variable.h" #include "miscadmin.h" #include "nodes/makefuncs.h" #include "nodes/nodeFuncs.h" @@ -51,6 +54,7 @@ #include "utils/builtins.h" #include "utils/guc.h" #include "utils/queryjumble.h" +#include "utils/lsyscache.h" #include "utils/rel.h" #include "utils/syscache.h" @@ -84,6 +88,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 RAW_EXPRESSION_COVERAGE_TEST @@ -327,6 +333,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: @@ -400,6 +407,11 @@ transformStmt(ParseState *pstate, Node *parseTree) (CallStmt *) parseTree); break; + case T_LetStmt: + result = transformLetStmt(pstate, + (LetStmt *) parseTree); + break; + default: /* @@ -442,6 +454,7 @@ analyze_requires_snapshot(RawStmt *parseTree) case T_MergeStmt: case T_SelectStmt: case T_PLAssignStmt: + case T_LetStmt: result = true; break; @@ -1628,12 +1641,282 @@ transformValuesClause(ParseState *pstate, SelectStmt *stmt) qry->jointree = makeFromExpr(pstate->p_joinlist, NULL); qry->hasSubLinks = pstate->p_hasSubLinks; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); return qry; } +/* + * transformLetStmt - + * transform an Let Statement + */ +static Query * +transformLetStmt(ParseState *pstate, LetStmt *stmt) +{ + Query *query; + Query *result; + List *exprList = NIL; + List *exprListCoer = NIL; + List *indirection = NIL; + ListCell *lc; + Query *selectQuery; + int i = 0; + Oid varid; + char *attrname = NULL; + bool not_unique; + bool is_rowtype; + Oid typid; + int32 typmod; + Oid collid; + AclResult aclresult; + List *names = NULL; + int indirection_start; + + /* There can't be any outer WITH to worry about */ + Assert(pstate->p_ctenamespace == NIL); + + names = NamesFromList(stmt->target); + + varid = IdentifyVariable(names, &attrname, ¬_unique); + 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))); + + indirection_start = list_length(names) - (attrname ? 1 : 0); + indirection = list_copy_tail(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_WRONG_OBJECT_TYPE), + errmsg("type \"%s\" of target session variable \"%s.%s\" is not a composite type", + format_type_be(typid), + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)), + parser_errposition(pstate, stmt->location))); + + if (stmt->set_default && attrname != NULL) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("only session variable (without attribute specification) can be set to default"), + parser_errposition(pstate, stmt->location))); + + aclresult = pg_variable_aclcheck(varid, GetUserId(), ACL_UPDATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_VARIABLE, NameListToString(names)); + + pstate->p_expr_kind = EXPR_KIND_LET_TARGET; + + /* + * The LET statements suppports two syntaxes: LET var = expr and + * LET var = DEFAULT. In first case, the expression is of SelectStmt + * node and it is transformated to query (SELECT) by usual way. + * Second syntax should be transformed differently. It is more cleaner + * do this transformation here (like special case), because we don't + * need to enhance SelectStmt about fields necessary for this + * transformation somwehere in SelectStmt transformation. Isn't to + * necessary to uglify the SelectStmt transformation. This is special + * case of LET statement, not SelectStmt. + */ + if (stmt->set_default) + { + selectQuery = makeNode(Query); + selectQuery->commandType = CMD_SELECT; + + /* + * ResTarget(SetToDefault) -> TargetEntry(expr(SetToDefault)) + * The SetToDefault is replaced by defexpr by rewriter in + * RewriteQuery function. + */ + selectQuery->targetList = transformTargetList(pstate, + ((SelectStmt *) stmt->query)->targetList, + EXPR_KIND_LET_TARGET); + } + else + selectQuery = transformStmt(pstate, stmt->query); + + /* The grammar should have produced a SELECT */ + if (!IsA(selectQuery, Query) || + selectQuery->commandType != CMD_SELECT) + elog(ERROR, "unexpected non-SELECT command in LET command"); + + /*---------- + * Generate an expression list for the LET that selects all the + * non-resjunk columns from the subquery. + *---------- + */ + exprList = NIL; + foreach(lc, selectQuery->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + + 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("expression is not scalar value"), + parser_errposition(pstate, + exprLocation((Node *) exprList)))); + + exprListCoer = NIL; + + foreach(lc, exprList) + { + Expr *expr = (Expr *) lfirst(lc); + Expr *coerced_expr; + Param *param; + Oid exprtypid; + + if (IsA(expr, SetToDefault)) + { + SetToDefault *def = (SetToDefault *) expr; + + def->typeId = typid; + def->typeMod = typmod; + def->collation = collid; + } + else if (IsA(expr, Const) && ((Const *) expr)->constisnull) + { + /* use known type for NULL value */ + expr = (Expr *) makeNullConst(typid, typmod, collid); + } + + /* now we can read type of expression */ + exprtypid = exprType((Node *) expr); + + param = makeNode(Param); + param->paramkind = PARAM_VARIABLE; + param->paramvarid = varid; + param->paramtype = typid; + param->paramtypmod = typmod; + + if (indirection != NULL) + { + bool targetIsArray; + char *targetName; + + targetName = get_session_variable_name(varid); + targetIsArray = OidIsValid(get_element_type(typid)); + + pstate->p_hasSessionVariables = true; + + coerced_expr = (Expr *) + transformAssignmentIndirection(pstate, + (Node *) param, + targetName, + targetIsArray, + typid, + typmod, + InvalidOid, + indirection, + list_head(indirection), + (Node *) expr, + COERCION_PLPGSQL, + 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 query */ + query->canSetTag = true; + + /* + * Save target session variable id. This value is used + * multiple times: by query rewriter (for getting related + * defexpr), by planner (for acquiring AccessShareLock on + * target variable), and by executor (we need to know + * target variable id). + */ + query->resultVariable = varid; + + assign_query_collations(pstate, query); + + stmt->query = (Node *) query; + + /* + * When statement is executed as a PlpgSQL LET statement, then we should + * return the query because we don't want to use a utilityStmt wrapper node. + */ + if (stmt->plpgsql_mode) + return query; + + /* represent the command as a utility Query */ + result = makeNode(Query); + result->commandType = CMD_UTILITY; + result->utilityStmt = (Node *) stmt; + + return result; +} + /* * transformSetOperationStmt - * transforms a set-operations tree diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 1b8b874164..bd3a81bf16 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -309,7 +309,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 @@ -457,6 +457,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); TriggerTransitions TriggerReferencing vacuum_relation_list opt_vacuum_relation_list drop_option_list pub_obj_list + let_target %type opt_routine_body %type group_clause @@ -720,7 +721,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); KEY 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 METHOD @@ -1046,6 +1047,7 @@ stmt: | ImportForeignSchemaStmt | IndexStmt | InsertStmt + | LetStmt | ListenStmt | RefreshMatViewStmt | LoadStmt @@ -11921,6 +11923,7 @@ ExplainableStmt: | CreateMatViewStmt | RefreshMatViewStmt | ExecuteStmt /* by default all are $$=$1 */ + | LetStmt ; /***************************************************************************** @@ -11951,6 +11954,7 @@ PreparableStmt: | UpdateStmt | DeleteStmt | MergeStmt /* by default all are $$=$1 */ + | LetStmt ; /***************************************************************************** @@ -12538,6 +12542,49 @@ opt_hold: /* EMPTY */ { $$ = 0; } | WITHOUT HOLD { $$ = 0; } ; +/***************************************************************************** + * + * QUERY: + * LET STATEMENTS + * + *****************************************************************************/ +LetStmt: LET let_target '=' a_expr + { + LetStmt *n = makeNode(LetStmt); + SelectStmt *select; + ResTarget *res; + + n->target = $2; + + select = makeNode(SelectStmt); + res = makeNode(ResTarget); + + /* Create target list for implicit query */ + res->name = NULL; + res->indirection = NIL; + res->val = (Node *) $4; + res->location = @4; + + select->targetList = list_make1(res); + n->query = (Node *) select; + + n->set_default = IsA($4, SetToDefault); + + n->location = @2; + $$ = (Node *) n; + } + ; + +let_target: + ColId opt_indirection + { + $$ = list_make1(makeString($1)); + if ($2) + $$ = list_concat($$, + check_indirection($2, yyscanner)); + } + ; + /***************************************************************************** * * QUERY: @@ -16908,6 +16955,7 @@ unreserved_keyword: | LARGE_P | LAST_P | LEAKPROOF + | LET | LEVEL | LISTEN | LOAD @@ -17477,6 +17525,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 dbe630a51b..4365692735 100644 --- a/src/backend/parser/parse_agg.c +++ b/src/backend/parser/parse_agg.c @@ -348,6 +348,7 @@ check_agglevels_and_constraints(ParseState *pstate, Node *expr) Assert(false); /* can't happen */ break; case EXPR_KIND_OTHER: + case EXPR_KIND_LET_TARGET: /* * Accept aggregate/grouping here; caller must throw error if @@ -955,6 +956,9 @@ transformWindowFuncCall(ParseState *pstate, WindowFunc *wfunc, case EXPR_KIND_CYCLE_MARK: errkind = true; break; + case EXPR_KIND_LET_TARGET: + err = _("window functions are not allowed in LET statement"); + 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 d8287ac134..d7dbbc8fe5 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -537,6 +537,7 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref) case EXPR_KIND_GENERATED_COLUMN: case EXPR_KIND_CYCLE_MARK: case EXPR_KIND_VARIABLE_DEFAULT: + case EXPR_KIND_LET_TARGET: /* okay */ break; @@ -1953,6 +1954,7 @@ transformSubLink(ParseState *pstate, SubLink *sublink) case EXPR_KIND_VALUES: case EXPR_KIND_VALUES_SINGLE: case EXPR_KIND_CYCLE_MARK: + case EXPR_KIND_LET_TARGET: /* okay */ break; case EXPR_KIND_CHECK_CONSTRAINT: @@ -3328,6 +3330,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 005bc0400f..21fbf939f2 100644 --- a/src/backend/parser/parse_func.c +++ b/src/backend/parser/parse_func.c @@ -2660,6 +2660,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: + err = _("set-returning functions are not allowed in CALL arguments"); + break; /* * There is intentionally no default: case here, so that the diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c index 4e1593d900..a0fef7e6d4 100644 --- a/src/backend/parser/parse_target.c +++ b/src/backend/parser/parse_target.c @@ -89,7 +89,9 @@ transformTargetEntry(ParseState *pstate, * through unmodified. (transformExpr will throw the appropriate * error if we're disallowing it.) */ - if (exprKind == EXPR_KIND_UPDATE_SOURCE && IsA(node, SetToDefault)) + if ((exprKind == EXPR_KIND_UPDATE_SOURCE || + exprKind == EXPR_KIND_LET_TARGET) + && IsA(node, SetToDefault)) expr = node; else expr = transformExpr(pstate, node, exprKind); diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c index 29ae27e5e3..08ed6ed7f9 100644 --- a/src/backend/rewrite/rewriteHandler.c +++ b/src/backend/rewrite/rewriteHandler.c @@ -25,6 +25,7 @@ #include "access/table.h" #include "catalog/dependency.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/trigger.h" #include "foreign/fdwapi.h" #include "miscadmin.h" @@ -3674,6 +3675,39 @@ RewriteQuery(Query *parsetree, List *rewrite_events) } } + /* + * Rewrite SetToDefault by default expression of Let statement. + */ + if (event == CMD_SELECT && OidIsValid(parsetree->resultVariable)) + { + Oid resultVariable = parsetree->resultVariable; + + if (list_length(parsetree->targetList) == 1 && + IsA(((TargetEntry *) linitial(parsetree->targetList))->expr, SetToDefault)) + { + Variable var; + TargetEntry *tle; + TargetEntry *newtle; + Expr *defexpr; + + /* Read session variable metadata with defexpr */ + initVariable(&var, resultVariable, false); + + if (var.has_defexpr) + defexpr = (Expr *) var.defexpr; + else + defexpr = (Expr *) makeNullConst(var.typid, var.typmod, var.collation); + + tle = (TargetEntry *) linitial(parsetree->targetList); + newtle = makeTargetEntry(defexpr, + tle->resno, + pstrdup(tle->resname), + false); + + parsetree->targetList = list_make1(newtle); + } + } + /* * If the statement is an insert, update, delete, or merge, adjust its * targetlist as needed, and then fire INSERT/UPDATE/DELETE rules on it. diff --git a/src/backend/rewrite/rowsecurity.c b/src/backend/rewrite/rowsecurity.c index b2a7237430..36c6b2d63a 100644 --- a/src/backend/rewrite/rowsecurity.c +++ b/src/backend/rewrite/rowsecurity.c @@ -213,10 +213,10 @@ get_row_security_policies(Query *root, RangeTblEntry *rte, int rt_index, } /* - * For SELECT, UPDATE and DELETE, add security quals to enforce the USING - * policies. These security quals control access to existing table rows. - * Restrictive policies are combined together using AND, and permissive - * policies are combined together using OR. + * For SELECT, LET, UPDATE and DELETE, add security quals to enforce the + * USING policies. These security quals control access to existing table + * rows. Restrictive policies are combined together using AND, and + * permissive policies are combined together using OR. */ get_policies_for_relation(rel, commandType, user_id, &permissive_policies, diff --git a/src/backend/tcop/dest.c b/src/backend/tcop/dest.c index c952cbea8b..9c5339804b 100644 --- a/src/backend/tcop/dest.c +++ b/src/backend/tcop/dest.c @@ -37,6 +37,7 @@ #include "executor/functions.h" #include "executor/tqueue.h" #include "executor/tstoreReceiver.h" +#include "executor/svariableReceiver.h" #include "libpq/libpq.h" #include "libpq/pqformat.h" #include "utils/portal.h" @@ -152,6 +153,9 @@ CreateDestReceiver(CommandDest dest) case DestTupleQueue: return CreateTupleQueueDestReceiver(NULL); + + case DestVariable: + return CreateVariableDestReceiver(); } /* should never get here */ @@ -207,6 +211,7 @@ EndCommand(const QueryCompletion *qc, CommandDest dest, bool force_undecorated_o case DestSQLFunction: case DestTransientRel: case DestTupleQueue: + case DestVariable: break; } } @@ -252,6 +257,7 @@ NullCommand(CommandDest dest) case DestSQLFunction: case DestTransientRel: case DestTupleQueue: + case DestVariable: break; } } @@ -295,6 +301,7 @@ ReadyForQuery(CommandDest dest) case DestSQLFunction: case DestTransientRel: case DestTupleQueue: + case DestVariable: break; } } diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 58f5c2b69c..724b74acb5 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -242,6 +242,7 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree) case T_CallStmt: case T_DoStmt: + case T_LetStmt: { /* * Commands inside the DO block or the called procedure might @@ -1071,6 +1072,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, @@ -2197,6 +2203,10 @@ UtilityContainsQuery(Node *parsetree) return UtilityContainsQuery(qry->utilityStmt); return qry; + case T_LetStmt: + qry = castNode(Query, ((LetStmt *) parsetree)->query); + return qry; + default: return NULL; } @@ -2395,6 +2405,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: { @@ -3280,6 +3294,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 74daf13a3f..7a3f678f06 100644 --- a/src/backend/utils/cache/plancache.c +++ b/src/backend/utils/cache/plancache.c @@ -135,6 +135,7 @@ InitPlanCache(void) CacheRegisterSyscacheCallback(AMOPOPID, PlanCacheSysCallback, (Datum) 0); CacheRegisterSyscacheCallback(FOREIGNSERVEROID, PlanCacheSysCallback, (Datum) 0); CacheRegisterSyscacheCallback(FOREIGNDATAWRAPPEROID, PlanCacheSysCallback, (Datum) 0); + CacheRegisterSyscacheCallback(VARIABLEOID, PlanCacheObjectCallback, (Datum) 0); } /* @@ -1868,12 +1869,24 @@ ScanQueryForLocks(Query *parsetree, bool acquire) * Recurse into sublink subqueries, too. But we already did the ones in * the rtable and cteList. */ - if (parsetree->hasSubLinks) + if (parsetree->hasSubLinks || + parsetree->hasSessionVariables) { query_tree_walker(parsetree, ScanQueryWalker, (void *) &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/include/commands/session_variable.h b/src/include/commands/session_variable.h index 8d64853831..6b95f66a3a 100644 --- a/src/include/commands/session_variable.h +++ b/src/include/commands/session_variable.h @@ -38,4 +38,7 @@ extern void AtPreEOXact_SessionVariable_on_xact_actions(bool isCommit); extern void AtEOSubXact_SessionVariable_on_xact_actions(bool isCommit, SubTransactionId mySubid, SubTransactionId parentSubid); +extern void ExecuteLetStmt(ParseState *pstate, LetStmt *stmt, ParamListInfo params, + QueryEnvironment *queryEnv, QueryCompletion *qc); + #endif diff --git a/src/include/executor/svariableReceiver.h b/src/include/executor/svariableReceiver.h new file mode 100644 index 0000000000..1d823a42a8 --- /dev/null +++ b/src/include/executor/svariableReceiver.h @@ -0,0 +1,25 @@ +/*------------------------------------------------------------------------- + * + * svariableReceiver.h + * prototypes for svariableReceiver.c + * + * + * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/executor/svariableReceiver.h + * + *------------------------------------------------------------------------- + */ + +#ifndef SVARIABLE_RECEIVER_H +#define SVARIABLE_RECEIVER_H + +#include "tcop/dest.h" + + +extern DestReceiver *CreateVariableDestReceiver(void); + +extern void SetVariableDestReceiverParams(DestReceiver *self, Oid varid); + +#endif /* SVARIABLE_RECEIVER_H */ diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 8e15d40963..52b2a037f6 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1699,6 +1699,21 @@ typedef struct MergeStmt WithClause *withClause; /* WITH clause */ } MergeStmt; +/* ---------------------- + * Let Statement + * ---------------------- + */ +typedef struct LetStmt +{ + NodeTag type; + List *target; /* target variable */ + Node *query; /* source expression */ + bool set_default; /* true, when set to DEFAULt is wanted */ + bool plpgsql_mode; /* true, when command will be executed (parsed) + * by plpgsql runtime */ + int location; +} LetStmt; + /* ---------------------- * Select Statement * diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h index e03727e19f..7108203d2b 100644 --- a/src/include/nodes/plannodes.h +++ b/src/include/nodes/plannodes.h @@ -49,7 +49,7 @@ typedef struct PlannedStmt NodeTag type; - CmdType commandType; /* select|insert|update|delete|merge|utility */ + CmdType commandType; /* select|let|insert|update|delete|merge|utility */ uint64 queryId; /* query identifier (copied from Query) */ diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index dfe3cfc437..84266519b3 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -237,6 +237,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 d07f5b0584..43fd9ca887 100644 --- a/src/include/parser/parse_node.h +++ b/src/include/parser/parse_node.h @@ -81,7 +81,8 @@ 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_VARIABLE_DEFAULT /* default value for session variable */ + EXPR_KIND_VARIABLE_DEFAULT, /* default value for session variable */ + EXPR_KIND_LET_TARGET /* LET assignment (should be same like UPDATE) */ } ParseExprKind; diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h index 6c1be5e9d4..357989b52f 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/include/tcop/dest.h b/src/include/tcop/dest.h index 3c3eabae67..7f18e07915 100644 --- a/src/include/tcop/dest.h +++ b/src/include/tcop/dest.h @@ -97,7 +97,8 @@ typedef enum DestCopyOut, /* results sent to COPY TO code */ DestSQLFunction, /* results sent to SQL-language func mgr */ DestTransientRel, /* results sent to transient relation */ - DestTupleQueue /* results sent to tuple queue */ + DestTupleQueue, /* results sent to tuple queue */ + DestVariable /* results sents to session variable */ } CommandDest; /* ---------------- -- 2.37.2