From 7a0c4b8b0a39659f8918eaaa7fe6d811c9bb1f58 Mon Sep 17 00:00:00 2001
From: "okbob@github.com" <pavel.stehule@gmail.com>
Date: Mon, 24 Nov 2025 18:05:03 +0100
Subject: [PATCH 06/11] 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                         |  22 +++
 doc/src/sgml/ref/allfiles.sgml                |   1 +
 doc/src/sgml/ref/create_variable.sgml         |   3 +
 doc/src/sgml/ref/drop_variable.sgml           |   1 +
 doc/src/sgml/ref/let.sgml                     |  95 +++++++++++
 doc/src/sgml/reference.sgml                   |   1 +
 src/backend/commands/session_variable.c       |  87 ++++++++++
 src/backend/nodes/nodeFuncs.c                 |   8 +
 src/backend/optimizer/plan/planner.c          |   1 +
 src/backend/parser/analyze.c                  | 103 ++++++++++--
 src/backend/parser/gram.y                     |  38 ++++-
 src/backend/parser/parse_agg.c                |   6 +
 src/backend/parser/parse_expr.c               |   9 +
 src/backend/parser/parse_func.c               |   3 +
 src/backend/tcop/utility.c                    |  15 ++
 src/bin/psql/tab-complete.in.c                |   9 +-
 src/include/commands/session_variable.h       |   5 +
 src/include/nodes/parsenodes.h                |  15 ++
 src/include/nodes/pathnodes.h                 |   3 +
 src/include/parser/kwlist.h                   |   1 +
 src/include/parser/parse_node.h               |   1 +
 src/include/tcop/cmdtaglist.h                 |   1 +
 .../expected/session_variables_dml.out        | 156 ++++++++++++++++++
 .../regress/sql/session_variables_dml.sql     | 113 +++++++++++++
 src/tools/pgindent/typedefs.list              |   1 +
 25 files changed, 678 insertions(+), 20 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 0055eb84a78..91607a72188 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -5960,10 +5960,32 @@ SELECT ... FROM GRAPH_TABLE (myshop MATCH (IS person WHERE name = '...')-[]->...
     <literal>VARIABLE(varname)</literal> syntax. This avoids any risk of
     collision between variable names and column names.
    </para>
+
+   <para>
+    You set the value of a session variable with the <command>LET</command>
+    statement and retrieve it with <command>SELECT</command>:
+<programlisting>
+CREATE TEMP VARIABLE var1 AS date;
+LET var1 = current_date;
+SELECT VARIABLE(var1);
+</programlisting>
+
+    or
+
 <programlisting>
+CREATE TEMP VARIABLE current_user_id AS integer;
+LET current_user_id = (SELECT id FROM users WHERE usename = session_user);
 SELECT VARIABLE(current_user_id);
 </programlisting>
    </para>
+
+   <para>
+    By default, retrieving a session variable returns
+    <literal>NULL</literal> unless it has been set in the current session
+    using the <command>LET</command> command. Session variables are not
+    transactional: changes to their values persist even if the transaction
+    is rolled back, similar to variables in procedural languages
+   </para>
  </sect1>
 
  <sect1 id="ddl-others">
diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml
index 35d485f5bc4..a0a6150feab 100644
--- a/doc/src/sgml/ref/allfiles.sgml
+++ b/doc/src/sgml/ref/allfiles.sgml
@@ -160,6 +160,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/create_variable.sgml b/doc/src/sgml/ref/create_variable.sgml
index 4e8c1940252..1315b1248c7 100644
--- a/doc/src/sgml/ref/create_variable.sgml
+++ b/doc/src/sgml/ref/create_variable.sgml
@@ -108,6 +108,8 @@ CREATE { TEMP | TEMPORARY } VARIABLE <replaceable class="parameter">name</replac
    Create an date session variable <literal>var1</literal>:
 <programlisting>
 CREATE TEMPORARY VARIABLE var1 AS date;
+LET var1 = current_date;
+SELECT VARIABLE(var1);
 </programlisting>
   </para>
 
@@ -127,6 +129,7 @@ CREATE TEMPORARY VARIABLE var1 AS date;
 
   <simplelist type="inline">
    <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 e8517a78200..dede42e4ffb 100644
--- a/doc/src/sgml/ref/drop_variable.sgml
+++ b/doc/src/sgml/ref/drop_variable.sgml
@@ -78,6 +78,7 @@ DROP VARIABLE var1;
 
   <simplelist type="inline">
    <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..33ee42d3f20
--- /dev/null
+++ b/doc/src/sgml/ref/let.sgml
@@ -0,0 +1,95 @@
+<!--
+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 TEMPORARY 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-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 342a7afb517..4d5a88f753b 100644
--- a/doc/src/sgml/reference.sgml
+++ b/doc/src/sgml/reference.sgml
@@ -188,6 +188,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 cd171a68cbf..abcb0bb531a 100644
--- a/src/backend/commands/session_variable.c
+++ b/src/backend/commands/session_variable.c
@@ -17,13 +17,18 @@
 #include "catalog/pg_language.h"
 #include "catalog/pg_type.h"
 #include "commands/session_variable.h"
+#include "executor/executor.h"
+#include "executor/svariableReceiver.h"
 #include "miscadmin.h"
 #include "parser/parse_type.h"
+#include "rewrite/rewriteHandler.h"
 #include "storage/proc.h"
+#include "tcop/tcopprot.h"
 #include "utils/builtins.h"
 #include "utils/datum.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
+#include "utils/snapmgr.h"
 
 /*
  * The session variables are stored in the backend's private memory (data,
@@ -324,3 +329,85 @@ DropVariableByName(char *varname)
 					   NULL) == NULL)
 		elog(ERROR, "hash table corrupted");
 }
+
+/*
+ * 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;
+	PlannedStmt *plan;
+	QueryDesc  *queryDesc;
+	char	   *varname = query->resultVariable;
+	SVariable	svar;
+
+	svar = search_variable(varname);
+
+	/* only owner can set content of variable */
+	if (svar->varowner != GetUserId() && !superuser())
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("permission denied for session variable %s",
+						varname)));
+
+	/* create a dest receiver for LET */
+	dest = CreateVariableDestReceiver(varname);
+
+	/* 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);
+
+	dest->rDestroy(dest);
+	FreeQueryDesc(queryDesc);
+
+	PopActiveSnapshot();
+}
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index 0f6990f1b84..9efca476866 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -4449,6 +4449,14 @@ raw_expression_tree_walker_impl(Node *node,
 					return true;
 			}
 			break;
+		case T_LetStmt:
+			{
+				LetStmt    *stmt = (LetStmt *) node;
+
+				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 22f8008d238..5f04603d026 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -376,6 +376,7 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
 	glob->partition_directory = NULL;
 	glob->rel_notnullatts_hash = NULL;
 	glob->sessionVariables = NIL;
+	glob->resultVariable = parse->resultVariable;
 
 	/*
 	 * Assess whether it's feasible to use parallel mode for this query. We
diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
index a2b9ce66e12..409d3ee411b 100644
--- a/src/backend/parser/analyze.c
+++ b/src/backend/parser/analyze.c
@@ -59,15 +59,18 @@
 #include "utils/lsyscache.h"
 #include "utils/rangetypes.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 */
@@ -95,7 +98,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);
@@ -105,6 +108,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
@@ -352,6 +357,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:
@@ -431,6 +437,11 @@ transformStmt(ParseState *pstate, Node *parseTree)
 									   (CallStmt *) parseTree);
 			break;
 
+		case T_LetStmt:
+			result = transformLetStmt(pstate,
+									  (LetStmt *) parseTree);
+			break;
+
 		default:
 
 			/*
@@ -492,6 +503,7 @@ stmt_requires_parse_analysis(RawStmt *parseTree)
 		case T_ExplainStmt:
 		case T_CreateTableAsStmt:
 		case T_CallStmt:
+		case T_LetStmt:
 			result = true;
 			break;
 
@@ -557,6 +569,7 @@ query_requires_rewrite_plan(Query *query)
 			case T_ExplainStmt:
 			case T_CreateTableAsStmt:
 			case T_CallStmt:
+			case T_LetStmt:
 				result = true;
 				break;
 			default:
@@ -1729,7 +1742,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.
@@ -1782,8 +1795,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);
 
@@ -3221,9 +3234,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
@@ -3246,18 +3261,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;
@@ -3292,7 +3310,7 @@ transformPLAssignStmtTarget(ParseState *pstate, List *tlist,
 		tle->expr = (Expr *)
 			transformAssignmentIndirection(pstate,
 										   target,
-										   stmt->name,
+										   target_name,
 										   false,
 										   targettype,
 										   targettypmod,
@@ -3300,10 +3318,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)))
 	{
@@ -3326,7 +3344,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 */
@@ -3335,7 +3353,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."),
@@ -3703,6 +3721,59 @@ 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;
+	VariableFence *vf;
+	SelectStmtPassthrough passthru;
+	Param	   *paramvar;
+
+	/* gram allows only SELECT */
+	Assert(IsA(stmt->query, SelectStmt));
+
+	/* Use implicit VariableFence for forcing session variables */
+	vf = makeNode(VariableFence);
+	vf->varname = stmt->target;
+	vf->location = stmt->location;
+
+	target = transformExpr(pstate, (Node *) vf, EXPR_KIND_LET_TARGET);
+
+	paramvar = castNode(Param, target);
+
+	Assert(paramvar->paramkind == PARAM_VARIABLE);
+
+	/* Set up passthrough data for transformAssignTarget */
+	passthru.stmt = (Node *) stmt;
+	passthru.target = (Node *) paramvar;
+	passthru.target_name = paramvar->paramvarname;
+	passthru.indirection = NIL;
+	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->paramvarname;
+	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 f3a4aeef3fe..90821c62087 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -304,7 +304,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 RepackStmt ReturnStmt RevokeStmt RevokeRoleStmt
 		RuleActionStmt RuleActionStmtOrEmpty RuleStmt
@@ -786,7 +786,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
@@ -1138,6 +1138,7 @@ stmt:
 			| ImportForeignSchemaStmt
 			| IndexStmt
 			| InsertStmt
+			| LetStmt
 			| ListenStmt
 			| RefreshMatViewStmt
 			| LoadStmt
@@ -13621,6 +13622,37 @@ opt_hold: /* EMPTY */						{ $$ = 0; }
 			| WITHOUT HOLD					{ $$ = 0; }
 		;
 
+/*****************************************************************************
+ *
+ *		QUERY:
+ *				LET STATEMENT
+ *
+ *****************************************************************************/
+LetStmt:	LET ColId '=' 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->location = @2;
+					$$ = (Node *) n;
+				}
+		;
+
 /*****************************************************************************
  *
  *		QUERY:
@@ -19021,6 +19053,7 @@ unreserved_keyword:
 			| LARGE_P
 			| LAST_P
 			| LEAKPROOF
+			| LET
 			| LEVEL
 			| LISTEN
 			| LOAD
@@ -19651,6 +19684,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 acb933392de..92fb1ef43a1 100644
--- a/src/backend/parser/parse_agg.c
+++ b/src/backend/parser/parse_agg.c
@@ -597,7 +597,10 @@ check_agglevels_and_constraints(ParseState *pstate, Node *expr)
 				err = _("aggregate functions are not allowed in property definition expressions");
 			else
 				err = _("grouping operations are not allowed in property definition expressions");
+			break;
 
+		case EXPR_KIND_LET_TARGET:
+			errkind = true;
 			break;
 
 			/*
@@ -1045,6 +1048,9 @@ transformWindowFuncCall(ParseState *pstate, WindowFunc *wfunc,
 		case EXPR_KIND_FOR_PORTION:
 			err = _("window functions are not allowed in FOR PORTION OF expressions");
 			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 20d4b7643fd..e29dfbadf6a 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -597,6 +597,9 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
 		case EXPR_KIND_FOR_PORTION:
 			err = _("cannot use column reference in FOR PORTION OF 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
@@ -978,6 +981,7 @@ expr_kind_allows_session_variables(ParseExprKind p_expr_kind)
 		case EXPR_KIND_VALUES:
 		case EXPR_KIND_VALUES_SINGLE:
 		case EXPR_KIND_PROPGRAPH_PROPERTY:
+		case EXPR_KIND_LET_TARGET:
 			result = true;
 			break;
 
@@ -2012,6 +2016,9 @@ transformSubLink(ParseState *pstate, SubLink *sublink)
 		case EXPR_KIND_FOR_PORTION:
 			err = _("cannot use subquery in FOR PORTION OF 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
@@ -3375,6 +3382,8 @@ ParseExprKindName(ParseExprKind exprKind)
 			return "property definition expression";
 		case EXPR_KIND_FOR_PORTION:
 			return "FOR PORTION OF";
+		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 35ff6427147..8ddb5a4e219 100644
--- a/src/backend/parser/parse_func.c
+++ b/src/backend/parser/parse_func.c
@@ -2789,6 +2789,9 @@ check_srf_call_placement(ParseState *pstate, Node *last_srf, int location)
 		case EXPR_KIND_FOR_PORTION:
 			err = _("set-returning functions are not allowed in FOR PORTION OF expressions");
 			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 269983987eb..fda4e019b13 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -240,6 +240,7 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree)
 
 		case T_CallStmt:
 		case T_DoStmt:
+		case T_LetStmt:
 			{
 				/*
 				 * Commands inside the DO block or the called procedure might
@@ -1078,6 +1079,11 @@ standard_ProcessUtility(PlannedStmt *pstmt,
 			DropVariableByName(((DropSessionVarStmt *) parsetree)->name);
 			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,
@@ -2232,6 +2238,10 @@ UtilityContainsQuery(Node *parsetree)
 				return UtilityContainsQuery(qry->utilityStmt);
 			return qry;
 
+		case T_LetStmt:
+			qry = castNode(Query, ((LetStmt *) parsetree)->query);
+			return qry;
+
 		default:
 			return NULL;
 	}
@@ -2430,6 +2440,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:
 			{
@@ -3334,6 +3348,7 @@ GetCommandLogLevel(Node *parsetree)
 			break;
 
 		case T_PLAssignStmt:
+		case T_LetStmt:
 			lev = LOGSTMT_ALL;
 			break;
 
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index fe012829ac0..69f348c129e 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -1273,8 +1273,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", "REPACK",
 	"RESET", "REVOKE", "ROLLBACK",
 	"SAVEPOINT", "SECURITY LABEL", "SELECT", "SET", "SHOW", "START",
@@ -4953,6 +4953,11 @@ match_previous_words(int pattern_id,
 	else if (TailMatches("VALUES") && !TailMatches("DEFAULT", "VALUES"))
 		COMPLETE_WITH("(");
 
+/* LET */
+	/* 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 610b757899e..c4b4d9e6832 100644
--- a/src/include/commands/session_variable.h
+++ b/src/include/commands/session_variable.h
@@ -16,8 +16,10 @@
 #define SESSIONVARIABLE_H
 
 #include "catalog/objectaddress.h"
+#include "nodes/params.h"
 #include "parser/parse_node.h"
 #include "nodes/parsenodes.h"
+#include "tcop/cmdtag.h"
 
 extern void CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt);
 extern void DropVariableByName(char *varname);
@@ -32,4 +34,7 @@ extern void get_session_variable_type_typmod_collid(char *varname,
 													int32 *typmod,
 													Oid *collid);
 
+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 97f63a75939..7d77a368b06 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -150,6 +150,9 @@ typedef struct Query
 	/* FOR PORTION OF clause for UPDATE/DELETE */
 	ForPortionOfExpr *forPortionOf;
 
+	/* target variable of LET statement */
+	char	   *resultVariable;
+
 	/* has aggregates in tlist or havingQual */
 	bool		hasAggs pg_node_attr(query_jumble_ignore);
 	/* has window functions in tlist */
@@ -2276,6 +2279,18 @@ typedef struct MergeStmt
 	WithClause *withClause;		/* WITH clause */
 } MergeStmt;
 
+/* ----------------------
+ *		Let Statement
+ * ----------------------
+ */
+typedef struct LetStmt
+{
+	NodeTag		type;
+	char	   *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 32c98bc7c1e..4ba6f709cf2 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -274,6 +274,9 @@ typedef struct PlannerGlobal
 
 	/* list of used session variables */
 	List	   *sessionVariables;
+
+	/* name of session variable used like target of LET command */
+	char	   *resultVariable;
 } PlannerGlobal;
 
 /* macro for fetching the Plan associated with a SubPlan node */
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index 491347970a2..3bc10aab33f 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -262,6 +262,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 00d77a1abc4..7f0d5fbb1b0 100644
--- a/src/include/parser/parse_node.h
+++ b/src/include/parser/parse_node.h
@@ -84,6 +84,7 @@ typedef enum ParseExprKind
 	EXPR_KIND_GENERATED_COLUMN, /* generation expression for a column */
 	EXPR_KIND_CYCLE_MARK,		/* cycle mark value */
 	EXPR_KIND_PROPGRAPH_PROPERTY,	/* derived property expression */
+	EXPR_KIND_LET_TARGET,		/* only session variables */
 } ParseExprKind;
 
 
diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h
index 35229a16add..5ff586e14ef 100644
--- a/src/include/tcop/cmdtaglist.h
+++ b/src/include/tcop/cmdtaglist.h
@@ -188,6 +188,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 1519bf723e0..599751ec5c8 100644
--- a/src/test/regress/expected/session_variables_dml.out
+++ b/src/test/regress/expected/session_variables_dml.out
@@ -133,3 +133,159 @@ RESET min_parallel_table_scan_size;
 RESET max_parallel_workers_per_gather;
 DROP TABLE testvar_testtab;
 DROP VARIABLE temp_var02;
+CREATE TEMP VARIABLE temp_var03 AS numeric;
+-- LET stmt is not allowed inside CTE
+WITH x AS (LET  temp_var03 = 3.14) SELECT * FROM x;
+ERROR:  syntax error at or near "LET"
+LINE 1: WITH x AS (LET  temp_var03 = 3.14) SELECT * FROM x;
+                   ^
+-- LET stmt requires result with exactly one row
+LET temp_var03 = generate_series(1,1);
+SELECT VARIABLE(temp_var03);
+ temp_var03 
+------------
+          1
+(1 row)
+
+-- should fail
+LET temp_var03 = generate_series(1,2);
+ERROR:  expression returned more than one row
+LET temp_var03 = generate_series(1,0);
+ERROR:  expression returned no rows
+CREATE OR REPLACE FUNCTION testvar_sql01(numeric)
+RETURNS void AS $$
+LET temp_var03 = $1;
+$$ LANGUAGE sql;
+CREATE OR REPLACE FUNCTION testvar_sql02()
+RETURNS numeric AS $$
+SELECT VARIABLE(temp_var03);
+$$ LANGUAGE sql;
+SELECT testvar_sql01(3.14);
+ testvar_sql01 
+---------------
+ 
+(1 row)
+
+SELECT testvar_sql02(), VARIABLE(temp_var03);
+ testvar_sql02 | temp_var03 
+---------------+------------
+          3.14 |       3.14
+(1 row)
+
+CREATE OR REPLACE FUNCTION testvar_pl(varchar)
+RETURNS varchar AS $$
+BEGIN
+  LET temp_var03 = $1::numeric;
+  RETURN VARIABLE(temp_var03);
+END
+$$ LANGUAGE plpgsql SECURITY DEFINER;
+SELECT testvar_pl('3.14');
+ testvar_pl 
+------------
+ 3.14
+(1 row)
+
+DROP VARIABLE temp_var03;
+SET plan_cache_mode to force_generic_plan;
+-- should not crash
+SELECT testvar_sql01(3.14);
+ERROR:  session variable "temp_var03" doesn't exist
+CONTEXT:  SQL function "testvar_sql01" during inlining
+SELECT testvar_sql02(), VARIABLE(temp_var03);
+ERROR:  session variable "temp_var03" doesn't exist
+SELECT testvar_pl('3.141592');
+ERROR:  session variable "temp_var03" doesn't exist
+CONTEXT:  SQL statement "LET temp_var03 = $1::numeric"
+PL/pgSQL function testvar_pl(character varying) line 3 at SQL statement
+-- can work again if we create variable
+CREATE TEMP VARIABLE temp_var03 AS numeric;
+SELECT testvar_sql01(3.14);
+ testvar_sql01 
+---------------
+ 
+(1 row)
+
+SELECT testvar_sql02(), VARIABLE(temp_var03);
+ testvar_sql02 | temp_var03 
+---------------+------------
+          3.14 |       3.14
+(1 row)
+
+SELECT testvar_pl('3.141592');
+ testvar_pl 
+------------
+ 3.141592
+(1 row)
+
+CREATE ROLE regress_session_variable_test_role_04;
+SET ROLE regress_session_variable_test_role_04;
+-- should fail
+SELECT testvar_sql01(3.14);
+ERROR:  permission denied for session variable temp_var03
+CONTEXT:  SQL function "testvar_sql01" statement 1
+-- should be ok (security definer)
+SELECT testvar_pl('3.141592');
+ testvar_pl 
+------------
+ 3.141592
+(1 row)
+
+SET ROLE TO DEFAULT;
+DROP FUNCTION testvar_sql01(numeric);
+DROP FUNCTION testvar_sql02();
+DROP FUNCTION testvar_pl(varchar);
+DROP ROLE regress_session_variable_test_role_04;
+DROP VARIABLE temp_var03;
+SET plan_cache_mode TO DEFAULT;
+-- test extended query protocol
+CREATE TEMP VARIABLE temp_var04 AS int;
+LET temp_var04 = $1 \bind 10 \g
+SELECT VARIABLE(temp_var04);
+ temp_var04 
+------------
+         10
+(1 row)
+
+LET temp_var04 = $1 \parse letps
+\bind_named letps 100 \g
+SELECT VARIABLE(temp_var04);
+ temp_var04 
+------------
+        100
+(1 row)
+
+\close_prepared letps
+DROP VARIABLE temp_var04;
+-- original value should not be changed when LET fails
+CREATE TEMP VARIABLE temp_var04 AS numeric;
+LET temp_var04 = 42;
+LET temp_var04 = generate_series(1,2); -- ERROR: too many row
+ERROR:  expression returned more than one row
+SELECT VARIABLE(temp_var04); -- expected 42
+ temp_var04 
+------------
+         42
+(1 row)
+
+DROP VARIABLE temp_var04;
+CREATE TEMP VARIABLE temp_var04 AS int;
+LET temp_var04 = 42;
+LET temp_var04 = generate_series(1,2); -- ERROR: too many row
+ERROR:  expression returned more than one row
+SELECT VARIABLE(temp_var04); -- expected 42
+ temp_var04 
+------------
+         42
+(1 row)
+
+DROP VARIABLE temp_var04;
+CREATE TEMP VARIABLE temp_var04 AS int;
+LET temp_var04 = generate_series(1,2); -- ERROR: too many row
+ERROR:  expression returned more than one row
+SELECT VARIABLE(temp_var04); -- expected NULL
+ temp_var04 
+------------
+           
+(1 row)
+
+DROP VARIABLE temp_var04;
diff --git a/src/test/regress/sql/session_variables_dml.sql b/src/test/regress/sql/session_variables_dml.sql
index bf56b19467b..fcb429f6bbf 100644
--- a/src/test/regress/sql/session_variables_dml.sql
+++ b/src/test/regress/sql/session_variables_dml.sql
@@ -118,3 +118,116 @@ RESET max_parallel_workers_per_gather;
 
 DROP TABLE testvar_testtab;
 DROP VARIABLE temp_var02;
+
+CREATE TEMP VARIABLE temp_var03 AS numeric;
+
+-- LET stmt is not allowed inside CTE
+WITH x AS (LET  temp_var03 = 3.14) SELECT * FROM x;
+
+-- LET stmt requires result with exactly one row
+LET temp_var03 = generate_series(1,1);
+SELECT VARIABLE(temp_var03);
+
+-- should fail
+LET temp_var03 = generate_series(1,2);
+LET temp_var03 = generate_series(1,0);
+
+CREATE OR REPLACE FUNCTION testvar_sql01(numeric)
+RETURNS void AS $$
+LET temp_var03 = $1;
+$$ LANGUAGE sql;
+
+CREATE OR REPLACE FUNCTION testvar_sql02()
+RETURNS numeric AS $$
+SELECT VARIABLE(temp_var03);
+$$ LANGUAGE sql;
+
+SELECT testvar_sql01(3.14);
+SELECT testvar_sql02(), VARIABLE(temp_var03);
+
+CREATE OR REPLACE FUNCTION testvar_pl(varchar)
+RETURNS varchar AS $$
+BEGIN
+  LET temp_var03 = $1::numeric;
+  RETURN VARIABLE(temp_var03);
+END
+$$ LANGUAGE plpgsql SECURITY DEFINER;
+
+SELECT testvar_pl('3.14');
+
+DROP VARIABLE temp_var03;
+
+SET plan_cache_mode to force_generic_plan;
+
+-- should not crash
+SELECT testvar_sql01(3.14);
+SELECT testvar_sql02(), VARIABLE(temp_var03);
+SELECT testvar_pl('3.141592');
+
+-- can work again if we create variable
+CREATE TEMP VARIABLE temp_var03 AS numeric;
+SELECT testvar_sql01(3.14);
+SELECT testvar_sql02(), VARIABLE(temp_var03);
+SELECT testvar_pl('3.141592');
+
+CREATE ROLE regress_session_variable_test_role_04;
+
+SET ROLE regress_session_variable_test_role_04;
+
+-- should fail
+SELECT testvar_sql01(3.14);
+
+-- should be ok (security definer)
+SELECT testvar_pl('3.141592');
+
+SET ROLE TO DEFAULT;
+
+DROP FUNCTION testvar_sql01(numeric);
+DROP FUNCTION testvar_sql02();
+DROP FUNCTION testvar_pl(varchar);
+
+DROP ROLE regress_session_variable_test_role_04;
+
+DROP VARIABLE temp_var03;
+
+SET plan_cache_mode TO DEFAULT;
+
+-- test extended query protocol
+CREATE TEMP VARIABLE temp_var04 AS int;
+
+LET temp_var04 = $1 \bind 10 \g
+SELECT VARIABLE(temp_var04);
+
+LET temp_var04 = $1 \parse letps
+\bind_named letps 100 \g
+SELECT VARIABLE(temp_var04);
+
+\close_prepared letps
+
+DROP VARIABLE temp_var04;
+
+-- original value should not be changed when LET fails
+CREATE TEMP VARIABLE temp_var04 AS numeric;
+
+LET temp_var04 = 42;
+
+LET temp_var04 = generate_series(1,2); -- ERROR: too many row
+SELECT VARIABLE(temp_var04); -- expected 42
+
+DROP VARIABLE temp_var04;
+
+CREATE TEMP VARIABLE temp_var04 AS int;
+
+LET temp_var04 = 42;
+
+LET temp_var04 = generate_series(1,2); -- ERROR: too many row
+SELECT VARIABLE(temp_var04); -- expected 42
+
+DROP VARIABLE temp_var04;
+
+CREATE TEMP VARIABLE temp_var04 AS int;
+
+LET temp_var04 = generate_series(1,2); -- ERROR: too many row
+SELECT VARIABLE(temp_var04); -- expected NULL
+
+DROP VARIABLE temp_var04;
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 22198dfafef..85bd4262f3c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1598,6 +1598,7 @@ LargeObjectDesc
 Latch
 LauncherLastStartTimesEntry
 LerpFunc
+LetStmt
 LexDescr
 LexemeEntry
 LexemeHashKey
-- 
2.53.0

