From 181daa6dbe2c909c8ca6fc21818d000a1169599d Mon Sep 17 00:00:00 2001
From: "okbob@github.com" <pavel.stehule@gmail.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          |  25 ++
 src/backend/optimizer/plan/setrefs.c          |  34 ++-
 src/backend/parser/analyze.c                  | 238 ++++++++++++++++
 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, 1095 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 &gt;= 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.
+   </para>
+
+   <para>
+    The value of a session variable is set with the SQL statement
+    <command>LET</command>.  The value of a session variable can be retrieved
+    with the SQL statement <command>SELECT</command>.
 <programlisting>
+CREATE VARIABLE var1 AS date;
+LET var1 = current_date;
+SELECT VARIABLE(var1);
+</programlisting>
+
+    or
+
+<programlisting>
+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);
 </programlisting>
    </para>
+
+   <para>
+    The value of a session variable is local to the current session. Retrieving
+    a variable's value returns a <literal>NULL</literal>, unless its value has
+    been set to something else in the current session using the
+    <command>LET</command> 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).
+   </para>
   </sect1>
 
  <sect1 id="ddl-others">
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.
 <!ENTITY grant              SYSTEM "grant.sgml">
 <!ENTITY importForeignSchema SYSTEM "import_foreign_schema.sgml">
 <!ENTITY insert             SYSTEM "insert.sgml">
+<!ENTITY let                SYSTEM "let.sgml">
 <!ENTITY listen             SYSTEM "listen.sgml">
 <!ENTITY load               SYSTEM "load.sgml">
 <!ENTITY lock               SYSTEM "lock.sgml">
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;
   <simplelist type="inline">
    <member><xref linkend="sql-createvariable"/></member>
    <member><xref linkend="sql-dropvariable"/></member>
+   <member><xref linkend="sql-let"/></member>
   </simplelist>
  </refsect1>
 </refentry>
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 ] <replaceable class="parameter">name</replaceab
   <title>Examples</title>
 
   <para>
-   Create an date session variable <literal>var1</literal>:
+   Create a session variable <literal>var1</literal> of data type date:
 <programlisting>
 CREATE VARIABLE var1 AS date;
+LET var1 = current_date;
+SELECT VARIABLE(var1);
 </programlisting>
   </para>
 
@@ -143,6 +145,7 @@ CREATE VARIABLE var1 AS date;
   <simplelist type="inline">
    <member><xref linkend="sql-altervariable"/></member>
    <member><xref linkend="sql-dropvariable"/></member>
+   <member><xref linkend="sql-let"/></member>
   </simplelist>
  </refsect1>
 
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;
   <simplelist type="inline">
    <member><xref linkend="sql-altervariable"/></member>
    <member><xref linkend="sql-createvariable"/></member>
+   <member><xref linkend="sql-let"/></member>
   </simplelist>
  </refsect1>
 
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 @@
+<!--
+doc/src/sgml/ref/let.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-let">
+ <indexterm zone="sql-let">
+  <primary>LET</primary>
+ </indexterm>
+
+ <indexterm>
+  <primary>session variable</primary>
+  <secondary>changing</secondary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>LET</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>LET</refname>
+  <refpurpose>change a session variable's value</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+LET <replaceable class="parameter">session_variable</replaceable> = <replaceable class="parameter">sql_expression</replaceable>
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   The <command>LET</command> command assigns a value to the specified session
+   variable.
+  </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+  <variablelist>
+   <varlistentry>
+    <term><replaceable class="parameter">session_variable</replaceable></term>
+    <listitem>
+     <para>
+      The name of the session variable.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">sql_expression</replaceable></term>
+    <listitem>
+     <para>
+      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.
+     </para>
+    </listitem>
+   </varlistentry>
+
+  </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+<programlisting>
+CREATE VARIABLE myvar AS integer;
+LET myvar = 10;
+LET myvar = (SELECT sum(val) FROM tab);
+</programlisting>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   The <command>LET</command> is a <productname>PostgreSQL</productname>
+   extension.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-altervariable"/></member>
+   <member><xref linkend="sql-createvariable"/></member>
+   <member><xref linkend="sql-dropvariable"/></member>
+  </simplelist>
+ </refsect1>
+</refentry>
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 e14fcd23de9..0ea4a44f1b2 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"
 
 /*
@@ -512,3 +519,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 53d359c2468..c4474522bc5 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 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..7a229d79dad 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,17 @@ 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..09109e80b25 100644
--- a/src/backend/optimizer/plan/setrefs.c
+++ b/src/backend/optimizer/plan/setrefs.c
@@ -2224,6 +2224,28 @@ 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 +3731,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 +3833,9 @@ 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..3c3785b6d19 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,234 @@ 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, &not_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 <variable> 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..dee7e6915a3 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..423963ab42c 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 ac5b2f22f0f..0951cc1859e 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 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 svartest_dml_write_only_role;
+GRANT USAGE ON SCHEMA svartest_dml TO svartest_dml_write_only_role;
+GRANT UPDATE ON VARIABLE svartest_dml.sesvar45 TO svartest_dml_write_only_role;
+SET ROLE TO 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 svartest_dml_write_only_role;
+SET ROLE TO 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 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 88cd536162c..59bdffbc602 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 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 svartest_dml_write_only_role;
+GRANT USAGE ON SCHEMA svartest_dml TO svartest_dml_write_only_role;
+GRANT UPDATE ON VARIABLE svartest_dml.sesvar45 TO svartest_dml_write_only_role;
+
+SET ROLE TO 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 svartest_dml_write_only_role;
+
+SET ROLE TO 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 svartest_dml_write_only_role;
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 1e33dfd4589..815689e9401 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

