v20250613-0011-LET-command-assign-a-result-of-expression-to-the-ses.patch
text/x-patch
Filename: v20250613-0011-LET-command-assign-a-result-of-expression-to-the-ses.patch
Type: text/x-patch
Part: 4
Message:
Re: proposal: schema variables
Patch
Same data as JSON:
GET /api/v1/attachments/:id/patch
the parsed metadata as JSON — format, series position, per-file stats; never the diff bytes.
API reference →
Format: format-patch
Series: patch v20250613-0011
Subject: LET command - assign a result of expression to the session variable
| File | + | − |
|---|---|---|
| doc/src/sgml/ddl.sgml | 29 | 0 |
| doc/src/sgml/ref/allfiles.sgml | 1 | 0 |
| doc/src/sgml/ref/alter_variable.sgml | 1 | 0 |
| doc/src/sgml/ref/create_variable.sgml | 4 | 1 |
| doc/src/sgml/ref/drop_variable.sgml | 1 | 0 |
| doc/src/sgml/reference.sgml | 1 | 0 |
| doc/src/sgml/ref/let.sgml | 96 | 0 |
| src/backend/commands/session_variable.c | 86 | 0 |
| src/backend/executor/execMain.c | 17 | 6 |
| src/backend/nodes/nodeFuncs.c | 10 | 0 |
| src/backend/optimizer/plan/planner.c | 24 | 0 |
| src/backend/optimizer/plan/setrefs.c | 30 | 4 |
| src/backend/parser/analyze.c | 237 | 0 |
| src/backend/parser/gram.y | 37 | 2 |
| src/backend/tcop/utility.c | 15 | 0 |
| src/backend/utils/cache/plancache.c | 11 | 0 |
| src/bin/psql/tab-complete.in.c | 10 | 2 |
| src/include/commands/session_variable.h | 5 | 0 |
| src/include/nodes/parsenodes.h | 15 | 0 |
| src/include/nodes/pathnodes.h | 9 | 0 |
| src/include/nodes/plannodes.h | 7 | 0 |
| src/include/nodes/primnodes.h | 9 | 0 |
| src/include/parser/kwlist.h | 1 | 0 |
| src/include/tcop/cmdtaglist.h | 1 | 0 |
| src/test/regress/expected/session_variables_dml.out | 258 | 0 |
| src/test/regress/sql/session_variables_dml.sql | 177 | 0 |
| src/tools/pgindent/typedefs.list | 1 | 0 |
From 4ecb5e987a970195c4787ea9224d2a5b577ed2da 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 | 24 ++
src/backend/optimizer/plan/setrefs.c | 34 ++-
src/backend/parser/analyze.c | 237 ++++++++++++++++
src/backend/parser/gram.y | 39 ++-
src/backend/tcop/utility.c | 15 +
src/backend/utils/cache/plancache.c | 11 +
src/bin/psql/tab-complete.in.c | 12 +-
src/include/commands/session_variable.h | 5 +
src/include/nodes/parsenodes.h | 15 +
src/include/nodes/pathnodes.h | 9 +
src/include/nodes/plannodes.h | 7 +
src/include/nodes/primnodes.h | 9 +
src/include/parser/kwlist.h | 1 +
src/include/tcop/cmdtaglist.h | 1 +
.../expected/session_variables_dml.out | 258 ++++++++++++++++++
.../regress/sql/session_variables_dml.sql | 177 ++++++++++++
src/tools/pgindent/typedefs.list | 1 +
27 files changed, 1093 insertions(+), 15 deletions(-)
create mode 100644 doc/src/sgml/ref/let.sgml
diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml
index bcb62729771..df4eac5af8d 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -5393,10 +5393,39 @@ EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01';
session variable identifier, and can be used only for session variable
identifier. The special syntax for accessing session variables removes
risk of collisions between variable identifiers and column names.
+ </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 dbc054795bb..768163e2009 100644
--- a/src/backend/commands/session_variable.c
+++ b/src/backend/commands/session_variable.c
@@ -19,15 +19,22 @@
#include "catalog/namespace.h"
#include "catalog/pg_type.h"
#include "commands/session_variable.h"
+#include "executor/execdesc.h"
+#include "executor/executor.h"
+#include "executor/svariableReceiver.h"
#include "miscadmin.h"
+#include "nodes/plannodes.h"
#include "parser/parse_type.h"
+#include "rewrite/rewriteHandler.h"
#include "storage/lmgr.h"
#include "storage/proc.h"
+#include "tcop/tcopprot.h"
#include "utils/builtins.h"
#include "utils/datum.h"
#include "utils/inval.h"
#include "utils/lsyscache.h"
#include "utils/memutils.h"
+#include "utils/snapmgr.h"
#include "utils/syscache.h"
/*
@@ -514,3 +521,82 @@ CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt)
return variable;
}
+
+/*
+ * Assign the result of the evaluated expression to the session variable
+ */
+void
+ExecuteLetStmt(ParseState *pstate,
+ LetStmt *stmt,
+ ParamListInfo params,
+ QueryEnvironment *queryEnv,
+ QueryCompletion *qc)
+{
+ Query *query = castNode(Query, stmt->query);
+ List *rewritten;
+ DestReceiver *dest;
+ AclResult aclresult;
+ PlannedStmt *plan;
+ QueryDesc *queryDesc;
+ Oid varid = query->resultVariable;
+
+ Assert(OidIsValid(varid));
+
+ /* do we have permission to write to the session variable? */
+ aclresult = object_aclcheck(VariableRelationId, varid, GetUserId(), ACL_UPDATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_VARIABLE, get_session_variable_name(varid));
+
+ /* create a dest receiver for LET */
+ dest = CreateVariableDestReceiver(varid);
+
+ /* run the query rewriter */
+ query = copyObject(query);
+
+ rewritten = QueryRewrite(query);
+
+ Assert(list_length(rewritten) == 1);
+
+ query = linitial_node(Query, rewritten);
+ Assert(query->commandType == CMD_SELECT);
+
+ /* plan the query */
+ plan = pg_plan_query(query, pstate->p_sourcetext,
+ CURSOR_OPT_PARALLEL_OK, params);
+
+ /*
+ * Use a snapshot with an updated command ID to ensure this query sees the
+ * results of any previously executed queries. (This could only matter if
+ * the planner executed an allegedly-stable function that changed the
+ * database contents, but let's do it anyway to be parallel to the EXPLAIN
+ * code path.)
+ */
+ PushCopiedSnapshot(GetActiveSnapshot());
+ UpdateActiveSnapshotCommandId();
+
+ /* create a QueryDesc, redirecting output to our tuple receiver */
+ queryDesc = CreateQueryDesc(plan, pstate->p_sourcetext,
+ GetActiveSnapshot(), InvalidSnapshot,
+ dest, params, queryEnv, 0);
+
+ /* call ExecutorStart to prepare the plan for execution */
+ ExecutorStart(queryDesc, 0);
+
+ /*
+ * Run the plan to completion. The result should be only one row. To
+ * check if there are too many result rows, we try to fetch two.
+ */
+ ExecutorRun(queryDesc, ForwardScanDirection, 2L);
+
+ /* save the rowcount if we're given a QueryCompletion to fill */
+ if (qc)
+ SetQueryCompletion(qc, CMDTAG_LET, queryDesc->estate->es_processed);
+
+ /* and clean up */
+ ExecutorFinish(queryDesc);
+ ExecutorEnd(queryDesc);
+
+ FreeQueryDesc(queryDesc);
+
+ PopActiveSnapshot();
+}
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 223742b920f..685ba66fd34 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -235,13 +235,24 @@ standard_ExecutorStart(QueryDesc *queryDesc, int eflags)
/* fill the array */
foreach_oid(varid, queryDesc->plannedstmt->sessionVariables)
{
- AclResult aclresult;
+ /*
+ * Permission check should be executed on all explicitly used
+ * variables in the query. For implicitly used variable (like base
+ * node of assignment indirect) we cannot do permission check,
+ * because we need read the value (and user can have only UPDATE
+ * variable). In this case the permission check is executed in
+ * write time.
+ */
+ if (varid != queryDesc->plannedstmt->exclSelectPermCheckVarid)
+ {
+ AclResult aclresult;
- aclresult = object_aclcheck(VariableRelationId, varid,
- GetUserId(), ACL_SELECT);
- if (aclresult != ACLCHECK_OK)
- aclcheck_error(aclresult, OBJECT_VARIABLE,
- get_session_variable_name(varid));
+ aclresult = object_aclcheck(VariableRelationId, varid,
+ GetUserId(), ACL_SELECT);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_VARIABLE,
+ get_session_variable_name(varid));
+ }
estate->es_session_variables[i].value =
GetSessionVariable(varid,
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index cd609c6e479..575365eefcd 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -4378,6 +4378,16 @@ raw_expression_tree_walker_impl(Node *node,
return true;
}
break;
+ case T_LetStmt:
+ {
+ LetStmt *stmt = (LetStmt *) node;
+
+ if (WALK(stmt->target))
+ return true;
+ if (WALK(stmt->query))
+ return true;
+ }
+ break;
case T_PLAssignStmt:
{
PLAssignStmt *stmt = (PLAssignStmt *) node;
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 714f58bd3d7..7119ceecafb 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -344,6 +344,20 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
glob->partition_directory = NULL;
glob->sessionVariables = NIL;
+ /*
+ * The (session) result variable should be stored to global, because it is
+ * not set in subquery. When this variable is used other than in base
+ * node of assignment indirection, we need to check the access rights (and
+ * then we need to detect this situation). The variable used like base
+ * node cannot be different than target (result) variable. Because we know
+ * the result variable before planner invocation, we can simply search of
+ * usage just this variable, and we don't need to to wait until the end of
+ * planning when we know basenodeSessionVarid.
+ */
+ glob->resultVariable = parse->resultVariable;
+ glob->basenodeSessionVarid = InvalidOid;
+ glob->basenodeSessionVarSelectCheck = false;
+
/*
* Assess whether it's feasible to use parallel mode for this query. We
* can't do this in a standalone backend, or if the command will try to
@@ -583,6 +597,16 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
result->sessionVariables = glob->sessionVariables;
+ /*
+ * The session variable used (and only used) like base node for assignemnt
+ * indirection should be excluded from permission check.
+ */
+ if (OidIsValid(glob->basenodeSessionVarid) &&
+ (!glob->basenodeSessionVarSelectCheck))
+ result->exclSelectPermCheckVarid = glob->basenodeSessionVarid;
+ else
+ result->exclSelectPermCheckVarid = InvalidOid;
+
result->stmt_location = parse->stmt_location;
result->stmt_len = parse->stmt_len;
diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c
index 6c86ee1ad64..d47a726f2df 100644
--- a/src/backend/optimizer/plan/setrefs.c
+++ b/src/backend/optimizer/plan/setrefs.c
@@ -2224,6 +2224,27 @@ fix_param_node(PlannerInfo *root, Param *p)
p->paramid = n;
}
+ /*
+ * We do SELECT permission check of all variables used by the query
+ * excluding the variable that is used only as base node of assignment
+ * indirection. The variable id assigned to this param should be same
+ * like resultVariable id, and this param should be used only once in
+ * query. When the variable is referenced by any other param, we
+ * should to do SELECT permission check for this variable too.
+ */
+ if (p->parambasenode)
+ {
+ Assert(!OidIsValid(root->glob->basenodeSessionVarid));
+ Assert(root->glob->resultVariable == p->paramvarid);
+
+ root->glob->basenodeSessionVarid = p->paramvarid;
+ }
+ else
+ {
+ if (p->paramvarid == root->glob->resultVariable)
+ root->glob->basenodeSessionVarSelectCheck = true;
+ }
+
return (Node *) p;
}
@@ -3709,7 +3730,7 @@ record_plan_type_dependency(PlannerInfo *root, Oid typid)
/*
* Record dependency on a session variable. The variable can be used as a
- * session variable in an expression list.
+ * session variable in an expression list, or as the target of a LET statement.
*/
static void
record_plan_variable_dependency(PlannerInfo *root, Oid varid)
@@ -3811,9 +3832,10 @@ extract_query_dependencies_walker(Node *node, PlannerInfo *context)
}
/*
- * Ignore other utility statements, except those (such as EXPLAIN)
- * that contain a parsed-but-not-planned query. For those, we
- * just need to transfer our attention to the contained query.
+ * Ignore other utility statements, except those (such as EXPLAIN
+ * or LET) that contain a parsed-but-not-planned query. For
+ * those, we just need to transfer our attention to the contained
+ * query.
*/
query = UtilityContainsQuery(query->utilityStmt);
if (query == NULL)
@@ -3836,6 +3858,10 @@ extract_query_dependencies_walker(Node *node, PlannerInfo *context)
lappend_oid(context->glob->relationOids, rte->relid);
}
+ /* record dependency on the target variable of a LET command */
+ if (OidIsValid(query->resultVariable))
+ record_plan_variable_dependency(context, query->resultVariable);
+
/* And recurse into the query's subexpressions */
return query_tree_walker(query, extract_query_dependencies_walker,
context, 0);
diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
index 95bb0620f39..92715bf8b1f 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
@@ -330,6 +333,7 @@ transformStmt(ParseState *pstate, Node *parseTree)
case T_UpdateStmt:
case T_DeleteStmt:
case T_MergeStmt:
+ case T_LetStmt:
(void) test_raw_expression_coverage(parseTree, NULL);
break;
default:
@@ -409,6 +413,11 @@ transformStmt(ParseState *pstate, Node *parseTree)
(CallStmt *) parseTree);
break;
+ case T_LetStmt:
+ result = transformLetStmt(pstate,
+ (LetStmt *) parseTree);
+ break;
+
default:
/*
@@ -460,6 +469,7 @@ stmt_requires_parse_analysis(RawStmt *parseTree)
case T_SelectStmt:
case T_ReturnStmt:
case T_PLAssignStmt:
+ case T_LetStmt:
result = true;
break;
@@ -3362,6 +3372,233 @@ transformCallStmt(ParseState *pstate, CallStmt *stmt)
return result;
}
+/*
+ * transformLetStmt -
+ * transform an Let Statement
+ */
+static Query *
+transformLetStmt(ParseState *pstate, LetStmt *stmt)
+{
+ Query *query;
+ Query *result;
+ List *exprList = NIL;
+ List *exprListCoer = NIL;
+ ListCell *lc;
+ ListCell *indirection_head = NULL;
+ Query *selectQuery;
+ Oid varid;
+ char *attrname = NULL;
+ bool not_unique;
+ bool is_rowtype;
+ Oid typid;
+ int32 typmod;
+ Oid collid;
+ List *names = NULL;
+ int indirection_start;
+ int i = 0;
+
+ /* there can't be any outer WITH to worry about */
+ Assert(pstate->p_ctenamespace == NIL);
+
+ names = NamesFromList(stmt->target);
+
+ /* locks the variable with an AccessShareLock */
+ varid = IdentifyVariable(names, &attrname, ¬_unique, false);
+ if (not_unique)
+ ereport(ERROR,
+ (errcode(ERRCODE_AMBIGUOUS_PARAMETER),
+ errmsg("target \"%s\" of LET command is ambiguous",
+ NameListToString(names)),
+ parser_errposition(pstate, stmt->location)));
+
+ if (!OidIsValid(varid))
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("session variable \"%s\" doesn't exist",
+ NameListToString(names)),
+ parser_errposition(pstate, stmt->location)));
+
+ /*
+ * Calculate start of possible position of an indirection in list, and
+ * when it is inside the list, store pointer on first node of indirection.
+ */
+ indirection_start = list_length(names) - (attrname ? 1 : 0);
+ if (list_length(stmt->target) > indirection_start)
+ indirection_head = list_nth_cell(stmt->target, indirection_start);
+
+ get_session_variable_type_typmod_collid(varid, &typid, &typmod, &collid);
+
+ is_rowtype = type_is_rowtype(typid);
+
+ if (attrname && !is_rowtype)
+ ereport(ERROR,
+ (errcode(ERRCODE_DATATYPE_MISMATCH),
+ errmsg("cannot assign to field \"%s\" of session variable \"%s.%s\" because its type %s is not a composite type",
+ attrname,
+ get_namespace_name(get_session_variable_namespace(varid)),
+ get_session_variable_name(varid),
+ format_type_be(typid)),
+ parser_errposition(pstate, stmt->location)));
+
+ pstate->p_expr_kind = EXPR_KIND_UPDATE_TARGET;
+
+ /* we need to postpone conversion of "unknown" to text */
+ pstate->p_resolve_unknowns = false;
+
+ selectQuery = transformStmt(pstate, stmt->query);
+
+ /* the grammar should have produced a SELECT */
+ Assert(IsA(selectQuery, Query) && selectQuery->commandType == CMD_SELECT);
+
+ /*
+ * Generate an expression list for the LET that selects all the
+ * non-resjunk columns from the subquery.
+ */
+ exprList = NIL;
+ foreach_node(TargetEntry, tle, selectQuery->targetList)
+ {
+ if (tle->resjunk)
+ continue;
+
+ exprList = lappend(exprList, tle->expr);
+ }
+
+ /* don't allow multicolumn result */
+ if (list_length(exprList) != 1)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg_plural("assignment expression returned %d column",
+ "assignment expression returned %d columns",
+ list_length(exprList),
+ list_length(exprList)),
+ parser_errposition(pstate,
+ exprLocation((Node *) exprList))));
+
+ exprListCoer = NIL;
+
+ foreach(lc, exprList)
+ {
+ Expr *expr = (Expr *) lfirst(lc);
+ Expr *coerced_expr;
+ Oid exprtypid;
+
+ /* now we can read the type of the expression */
+ exprtypid = exprType((Node *) expr);
+
+ if (indirection_head)
+ {
+ bool targetIsArray;
+ char *targetName;
+ Param *param;
+
+ targetName = get_session_variable_name(varid);
+ targetIsArray = OidIsValid(get_element_type(typid));
+
+ pstate->p_hasSessionVariables = true;
+
+ param = makeNode(Param);
+ param->paramkind = PARAM_VARIABLE;
+ param->paramvarid = varid;
+ param->paramtype = typid;
+ param->paramtypmod = typmod;
+
+ /*
+ * The parameter used as basenode has to have special mark,
+ * because requires special access when we do SELECT access check.
+ */
+ param->parambasenode = true;
+
+ coerced_expr = (Expr *)
+ transformAssignmentIndirection(pstate,
+ (Node *) param,
+ targetName,
+ targetIsArray,
+ typid,
+ typmod,
+ InvalidOid,
+ stmt->target,
+ indirection_head,
+ (Node *) expr,
+ COERCION_ASSIGNMENT,
+ stmt->location);
+ }
+ else
+ coerced_expr = (Expr *)
+ coerce_to_target_type(pstate,
+ (Node *) expr,
+ exprtypid,
+ typid, typmod,
+ COERCION_ASSIGNMENT,
+ COERCE_IMPLICIT_CAST,
+ stmt->location);
+
+ if (coerced_expr == NULL)
+ ereport(ERROR,
+ (errcode(ERRCODE_DATATYPE_MISMATCH),
+ errmsg("variable \"%s.%s\" is of type %s, but expression is of type %s",
+ get_namespace_name(get_session_variable_namespace(varid)),
+ get_session_variable_name(varid),
+ format_type_be(typid),
+ format_type_be(exprtypid)),
+ errhint("You will need to rewrite or cast the expression."),
+ parser_errposition(pstate, exprLocation((Node *) expr))));
+
+ exprListCoer = lappend(exprListCoer, coerced_expr);
+ }
+
+ /* generate query's target list using the computed list of expressions */
+ query = makeNode(Query);
+ query->commandType = CMD_SELECT;
+
+ foreach(lc, exprListCoer)
+ {
+ Expr *expr = (Expr *) lfirst(lc);
+ TargetEntry *tle;
+
+ tle = makeTargetEntry(expr,
+ i + 1,
+ FigureColname((Node *) expr),
+ false);
+ query->targetList = lappend(query->targetList, tle);
+ }
+
+ /* done building the range table and jointree */
+ query->rtable = pstate->p_rtable;
+ query->jointree = makeFromExpr(pstate->p_joinlist, NULL);
+
+ query->hasTargetSRFs = pstate->p_hasTargetSRFs;
+ query->hasSubLinks = pstate->p_hasSubLinks;
+ query->hasSessionVariables = pstate->p_hasSessionVariables;
+
+ /* this is top-level query */
+ query->canSetTag = true;
+
+ /*
+ * Save target session variable ID. It is used later for acquiring an
+ * AccessShareLock on target variable, setting plan dependency and finally
+ * for creating VariableDestReceiver.
+ */
+ query->resultVariable = varid;
+
+ assign_query_collations(pstate, query);
+
+ /*
+ * The query is executed as utility command by nested executor call.
+ * Assigned queryId is required in this case.
+ */
+ if (IsQueryIdEnabled())
+ JumbleQuery(query);
+
+ stmt->query = (Node *) query;
+
+ /* represent the command as a utility Query */
+ result = makeNode(Query);
+ result->commandType = CMD_UTILITY;
+ result->utilityStmt = (Node *) stmt;
+
+ return result;
+}
+
/*
* Produce a string representation of a LockClauseStrength value.
* This should only be applied to valid values (not LCS_NONE).
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 6d7c8c56c90..f4ac5186348 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -296,7 +296,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
@@ -741,7 +741,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
@@ -1088,6 +1088,7 @@ stmt:
| ImportForeignSchemaStmt
| IndexStmt
| InsertStmt
+ | LetStmt
| ListenStmt
| RefreshMatViewStmt
| LoadStmt
@@ -12862,6 +12863,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:
@@ -17937,6 +17970,7 @@ unreserved_keyword:
| LARGE_P
| LAST_P
| LEAKPROOF
+ | LET
| LEVEL
| LISTEN
| LOAD
@@ -18551,6 +18585,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 78fc8ef6d45..c689a45918c 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",
@@ -4702,6 +4702,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 4654574c06c..1dd7f3ea00f 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 */
@@ -2168,6 +2171,18 @@ typedef struct MergeStmt
WithClause *withClause; /* WITH clause */
} MergeStmt;
+/* ----------------------
+ * Let Statement
+ * ----------------------
+ */
+typedef struct LetStmt
+{
+ NodeTag type;
+ List *target; /* target variable */
+ Node *query; /* source expression */
+ ParseLoc location;
+} LetStmt;
+
/* ----------------------
* Select Statement
*
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index ec5685df779..4b0c625619f 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -182,6 +182,15 @@ typedef struct PlannerGlobal
/* list of used session variables */
List *sessionVariables;
+
+ /* Oid of session variable used like target of LET command */
+ Oid resultVariable;
+
+ /* oid of session variable used like base node for assignment indirection */
+ Oid basenodeSessionVarid;
+
+ /* true, if we do SELECT permission check on basenodeSessionVarid */
+ bool basenodeSessionVarSelectCheck;
} PlannerGlobal;
/* macro for fetching the Plan associated with a SubPlan node */
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 3622699cfc9..10761cf9105 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -134,6 +134,13 @@ typedef struct PlannedStmt
/* OIDs for PARAM_VARIABLE Params */
List *sessionVariables;
+ /*
+ * The oid of session variable execluded from permission check. This
+ * session variable is used as base node of assignment indirection (and it
+ * is used only there).
+ */
+ int exclSelectPermCheckVarid;
+
/* statement location in source string (copied from Query) */
/* start location, or -1 if unknown */
ParseLoc stmt_location;
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index dc592602c65..0d1233f59e9 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -402,6 +402,15 @@ typedef struct Param
Oid paramcollid pg_node_attr(query_jumble_ignore);
/* OID of used session variable or InvalidOid if none */
Oid paramvarid pg_node_attr(query_jumble_ignore);
+
+ /*
+ * true if param is used as base node of assignment indirection (when
+ * target of LET statement is an array field or an record field). For this
+ * param we do not check SELECT access right, because this param is used
+ * just for execution of an modify operation.
+ */
+ bool parambasenode;
+
/* token location, or -1 if unknown */
ParseLoc location;
} Param;
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index 0ea0265de7c..8c0affba13b 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -257,6 +257,7 @@ PG_KEYWORD("leading", LEADING, RESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("leakproof", LEAKPROOF, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("least", LEAST, COL_NAME_KEYWORD, BARE_LABEL)
PG_KEYWORD("left", LEFT, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL)
+PG_KEYWORD("let", LET, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("level", LEVEL, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("like", LIKE, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL)
PG_KEYWORD("limit", LIMIT, RESERVED_KEYWORD, AS_LABEL)
diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h
index ea86954dded..22082c30008 100644
--- a/src/include/tcop/cmdtaglist.h
+++ b/src/include/tcop/cmdtaglist.h
@@ -186,6 +186,7 @@ PG_CMDTAG(CMDTAG_GRANT, "GRANT", true, false, false)
PG_CMDTAG(CMDTAG_GRANT_ROLE, "GRANT ROLE", false, false, false)
PG_CMDTAG(CMDTAG_IMPORT_FOREIGN_SCHEMA, "IMPORT FOREIGN SCHEMA", true, false, false)
PG_CMDTAG(CMDTAG_INSERT, "INSERT", false, false, true)
+PG_CMDTAG(CMDTAG_LET, "LET", false, false, false)
PG_CMDTAG(CMDTAG_LISTEN, "LISTEN", false, false, false)
PG_CMDTAG(CMDTAG_LOAD, "LOAD", false, false, false)
PG_CMDTAG(CMDTAG_LOCK_TABLE, "LOCK TABLE", false, false, false)
diff --git a/src/test/regress/expected/session_variables_dml.out b/src/test/regress/expected/session_variables_dml.out
index 3e21059acc2..0fdf0fdc68a 100644
--- a/src/test/regress/expected/session_variables_dml.out
+++ b/src/test/regress/expected/session_variables_dml.out
@@ -189,3 +189,261 @@ drop cascades to table svartest_dml.testtab
DROP ROLE regress_svartest_dml_read_role;
DROP VARIABLE sesvar40;
DROP TABLE svartest_dml;
+CREATE VARIABLE sesvar43 AS numeric;
+-- LET stmt is not allowed inside CTE
+WITH x AS (LET sesvar43 = 3.14) SELECT * FROM x;
+ERROR: syntax error at or near "LET"
+LINE 1: WITH x AS (LET sesvar43 = 3.14) SELECT * FROM x;
+ ^
+-- LET stmt requires result with exactly one row
+LET sesvar43 = generate_series(1,1);
+-- should fail
+LET sesvar43 = generate_series(1,2);
+ERROR: expression returned more than one row
+LET sesvar43 = generate_series(1,0);
+ERROR: expression returned no rows
+CREATE SCHEMA svartest_dml;
+CREATE VARIABLE svartest_dml.sesvar44 AS varchar;
+CREATE TYPE svartest_dml.composite_type AS (a int, b int, c int);
+CREATE VARIABLE svartest_dml.sesvar45 AS svartest_dml.composite_type;
+CREATE OR REPLACE FUNCTION svartest_dml.fx01(numeric)
+RETURNS void AS $$
+LET sesvar43 = $1;
+$$ LANGUAGE sql;
+CREATE OR REPLACE FUNCTION svartest_dml.fx02()
+RETURNS numeric AS $$
+SELECT VARIABLE(sesvar43);
+$$ LANGUAGE sql;
+SELECT svartest_dml.fx01(3.14);
+ fx01
+------
+
+(1 row)
+
+SELECT svartest_dml.fx02(), VARIABLE(sesvar43);
+ fx02 | sesvar43
+------+----------
+ 3.14 | 3.14
+(1 row)
+
+CREATE OR REPLACE FUNCTION svartest_dml.fx03(s varchar)
+RETURNS varchar AS $$
+BEGIN
+ LET svartest_dml.sesvar44 = s;
+ RETURN VARIABLE(svartest_dml.sesvar44);
+END
+$$ LANGUAGE plpgsql;
+SELECT svartest_dml.fx03('Hello');
+ fx03
+-------
+ Hello
+(1 row)
+
+CREATE OR REPLACE FUNCTION svartest_dml.fx04(s varchar)
+RETURNS varchar AS $$
+BEGIN
+ LET sesvar44 = s;
+ RETURN VARIABLE(sesvar44);
+END
+$$ LANGUAGE plpgsql
+SET SEARCH_PATH TO 'svartest_dml';
+SELECT svartest_dml.fx04('Hello');
+ fx04
+-------
+ Hello
+(1 row)
+
+CREATE OR REPLACE FUNCTION svartest_dml.fx05(a int, b int, c int)
+RETURNS svartest_dml.composite_type AS $$
+BEGIN
+ LET svartest_dml.sesvar45 = ROW(a, b, c);
+ RETURN VARIABLE(svartest_dml.sesvar45);
+END;
+$$ LANGUAGE plpgsql;
+SELECT row_to_json(svartest_dml.fx05(10, 20, 30));
+ row_to_json
+------------------------
+ {"a":10,"b":20,"c":30}
+(1 row)
+
+SELECT VARIABLE(svartest_dml.sesvar45);
+ sesvar45
+------------
+ (10,20,30)
+(1 row)
+
+SELECT VARIABLE(svartest_dml.sesvar45).*;
+ a | b | c
+----+----+----
+ 10 | 20 | 30
+(1 row)
+
+SELECT VARIABLE(svartest_dml.sesvar45.a);
+ a
+----
+ 10
+(1 row)
+
+SELECT VARIABLE(svartest_dml.sesvar45).a;
+ a
+----
+ 10
+(1 row)
+
+ALTER TYPE svartest_dml.composite_type ADD ATTRIBUTE d int;
+-- composite value should be still readable
+SELECT row_to_json(VARIABLE(svartest_dml.sesvar45));
+ row_to_json
+---------------------------------
+ {"a":10,"b":20,"c":30,"d":null}
+(1 row)
+
+LET svartest_dml.sesvar45 = ROW(100, 200, 300, NULL);
+SELECT row_to_json(VARIABLE(svartest_dml.sesvar45));
+ row_to_json
+------------------------------------
+ {"a":100,"b":200,"c":300,"d":null}
+(1 row)
+
+-- use variables inside view
+CREATE VIEW svartest_dml.view01 AS SELECT VARIABLE(svartest_dml.sesvar45).*;
+SELECT * FROM svartest_dml.view01;
+ a | b | c | d
+-----+-----+-----+---
+ 100 | 200 | 300 |
+(1 row)
+
+-- start new connection
+\c
+SELECT * FROM svartest_dml.view01;
+ a | b | c | d
+---+---+---+---
+ | | |
+(1 row)
+
+LET svartest_dml.sesvar45 = ROW(5, 6, 7, 8);
+SELECT * FROM svartest_dml.view01;
+ a | b | c | d
+---+---+---+---
+ 5 | 6 | 7 | 8
+(1 row)
+
+-- should fail (dependency)
+DROP VARIABLE svartest_dml.sesvar45;
+ERROR: cannot drop session variable svartest_dml.sesvar45 because other objects depend on it
+DETAIL: view svartest_dml.view01 depends on session variable svartest_dml.sesvar45
+HINT: Use DROP ... CASCADE to drop the dependent objects too.
+DROP VIEW svartest_dml.view01;
+-- test of access variables from generic plans
+CREATE OR REPLACE FUNCTION svartest_dml.fx06()
+RETURNS numeric AS $$
+BEGIN
+ RETURN VARIABLE(sesvar43);
+END;
+$$ LANGUAGE plpgsql;
+SET plan_cache_mode TO force_generic_plan;
+LET sesvar43 = 6.28;
+SELECT svartest_dml.fx06();
+ fx06
+------
+ 6.28
+(1 row)
+
+LET sesvar43 = VARIABLE(sesvar43) * 2;
+SELECT svartest_dml.fx06();
+ fx06
+-------
+ 12.56
+(1 row)
+
+-- plan cache invalidation test
+DROP VARIABLE sesvar43;
+-- should fail
+SELECT svartest_dml.fx06();
+ERROR: session variable "sesvar43" doesn't exist
+LINE 1: VARIABLE(sesvar43)
+ ^
+QUERY: VARIABLE(sesvar43)
+CONTEXT: PL/pgSQL function svartest_dml.fx06() line 3 at RETURN
+CREATE VARIABLE sesvar43 AS numeric;
+LET sesvar43 = 2.72;
+SELECT svartest_dml.fx06();
+ fx06
+------
+ 2.72
+(1 row)
+
+DROP VARIABLE sesvar43;
+CREATE DOMAIN svartest_dml.int_not_null AS int CHECK(value IS NOT NULL);
+CREATE VARIABLE svartest_dml.sesvar46 AS svartest_dml.int_not_null;
+-- should fail
+LET svartest_dml.sesvar46 = NULL;
+ERROR: value for domain svartest_dml.int_not_null violates check constraint "int_not_null_check"
+-- should be ok
+LET svartest_dml.sesvar46 = 100;
+LET svartest_dml.sesvar45 = ROW(1,2,3,4);
+LET svartest_dml.sesvar45.a = 100;
+SELECT row_to_json(VARIABLE(svartest_dml.sesvar45));
+ row_to_json
+-----------------------------
+ {"a":100,"b":2,"c":3,"d":4}
+(1 row)
+
+CREATE ROLE regress_svartest_dml_write_only_role;
+GRANT USAGE ON SCHEMA svartest_dml TO regress_svartest_dml_write_only_role;
+GRANT UPDATE ON VARIABLE svartest_dml.sesvar45 TO regress_svartest_dml_write_only_role;
+SET ROLE TO regress_svartest_dml_write_only_role;
+-- should fail
+SELECT VARIABLE(svartest_dml.sesvar45);
+ERROR: permission denied for session variable sesvar45
+-- should be ok
+LET svartest_dml.sesvar45.b = 200;
+SET ROLE TO DEFAULT;
+SELECT row_to_json(VARIABLE(svartest_dml.sesvar45));
+ row_to_json
+-------------------------------
+ {"a":100,"b":200,"c":3,"d":4}
+(1 row)
+
+CREATE VARIABLE svartest_dml.sesvar47 AS int[];
+LET svartest_dml.sesvar47 = ARRAY[1,2,3];
+GRANT UPDATE ON VARIABLE svartest_dml.sesvar47 TO regress_svartest_dml_write_only_role;
+SET ROLE TO regress_svartest_dml_write_only_role;
+-- should fail
+SELECT VARIABLE(svartest_dml.sesvar47);
+ERROR: permission denied for session variable sesvar47
+-- should be ok
+LET svartest_dml.sesvar47[1] = 200;
+SET ROLE TO DEFAULT;
+SELECT VARIABLE(svartest_dml.sesvar47);
+ sesvar47
+-----------
+ {200,2,3}
+(1 row)
+
+CREATE VARIABLE svartest_dml.sesvar48 AS int4multirange[];
+LET svartest_dml.sesvar48 = NULL;
+LET svartest_dml.sesvar48 = '{"{[2,8),[11,14)}","{[5,8),[12,14)}"}';
+LET svartest_dml.sesvar48[2] = '{[5,8),[12,100)}';
+SELECT VARIABLE(svartest_dml.sesvar48);
+ sesvar48
+----------------------------------------
+ {"{[2,8),[11,14)}","{[5,8),[12,100)}"}
+(1 row)
+
+DROP SCHEMA svartest_dml CASCADE;
+NOTICE: drop cascades to 13 other objects
+DETAIL: drop cascades to session variable svartest_dml.sesvar44
+drop cascades to type svartest_dml.composite_type
+drop cascades to session variable svartest_dml.sesvar45
+drop cascades to function svartest_dml.fx01(numeric)
+drop cascades to function svartest_dml.fx02()
+drop cascades to function svartest_dml.fx03(character varying)
+drop cascades to function svartest_dml.fx04(character varying)
+drop cascades to function svartest_dml.fx05(integer,integer,integer)
+drop cascades to function svartest_dml.fx06()
+drop cascades to type svartest_dml.int_not_null
+drop cascades to session variable svartest_dml.sesvar46
+drop cascades to session variable svartest_dml.sesvar47
+drop cascades to session variable svartest_dml.sesvar48
+DROP ROLE regress_svartest_dml_write_only_role;
diff --git a/src/test/regress/sql/session_variables_dml.sql b/src/test/regress/sql/session_variables_dml.sql
index b2870dde9e9..1250e7ef062 100644
--- a/src/test/regress/sql/session_variables_dml.sql
+++ b/src/test/regress/sql/session_variables_dml.sql
@@ -159,3 +159,180 @@ DROP ROLE regress_svartest_dml_read_role;
DROP VARIABLE sesvar40;
DROP TABLE svartest_dml;
+
+CREATE VARIABLE sesvar43 AS numeric;
+
+-- LET stmt is not allowed inside CTE
+WITH x AS (LET sesvar43 = 3.14) SELECT * FROM x;
+
+-- LET stmt requires result with exactly one row
+LET sesvar43 = generate_series(1,1);
+
+-- should fail
+LET sesvar43 = generate_series(1,2);
+LET sesvar43 = generate_series(1,0);
+
+CREATE SCHEMA svartest_dml;
+CREATE VARIABLE svartest_dml.sesvar44 AS varchar;
+CREATE TYPE svartest_dml.composite_type AS (a int, b int, c int);
+CREATE VARIABLE svartest_dml.sesvar45 AS svartest_dml.composite_type;
+
+CREATE OR REPLACE FUNCTION svartest_dml.fx01(numeric)
+RETURNS void AS $$
+LET sesvar43 = $1;
+$$ LANGUAGE sql;
+
+CREATE OR REPLACE FUNCTION svartest_dml.fx02()
+RETURNS numeric AS $$
+SELECT VARIABLE(sesvar43);
+$$ LANGUAGE sql;
+
+SELECT svartest_dml.fx01(3.14);
+SELECT svartest_dml.fx02(), VARIABLE(sesvar43);
+
+CREATE OR REPLACE FUNCTION svartest_dml.fx03(s varchar)
+RETURNS varchar AS $$
+BEGIN
+ LET svartest_dml.sesvar44 = s;
+ RETURN VARIABLE(svartest_dml.sesvar44);
+END
+$$ LANGUAGE plpgsql;
+
+SELECT svartest_dml.fx03('Hello');
+
+CREATE OR REPLACE FUNCTION svartest_dml.fx04(s varchar)
+RETURNS varchar AS $$
+BEGIN
+ LET sesvar44 = s;
+ RETURN VARIABLE(sesvar44);
+END
+$$ LANGUAGE plpgsql
+SET SEARCH_PATH TO 'svartest_dml';
+
+SELECT svartest_dml.fx04('Hello');
+
+CREATE OR REPLACE FUNCTION svartest_dml.fx05(a int, b int, c int)
+RETURNS svartest_dml.composite_type AS $$
+BEGIN
+ LET svartest_dml.sesvar45 = ROW(a, b, c);
+ RETURN VARIABLE(svartest_dml.sesvar45);
+END;
+$$ LANGUAGE plpgsql;
+
+SELECT row_to_json(svartest_dml.fx05(10, 20, 30));
+
+SELECT VARIABLE(svartest_dml.sesvar45);
+SELECT VARIABLE(svartest_dml.sesvar45).*;
+SELECT VARIABLE(svartest_dml.sesvar45.a);
+SELECT VARIABLE(svartest_dml.sesvar45).a;
+
+ALTER TYPE svartest_dml.composite_type ADD ATTRIBUTE d int;
+
+-- composite value should be still readable
+SELECT row_to_json(VARIABLE(svartest_dml.sesvar45));
+
+LET svartest_dml.sesvar45 = ROW(100, 200, 300, NULL);
+SELECT row_to_json(VARIABLE(svartest_dml.sesvar45));
+
+-- use variables inside view
+CREATE VIEW svartest_dml.view01 AS SELECT VARIABLE(svartest_dml.sesvar45).*;
+SELECT * FROM svartest_dml.view01;
+
+-- start new connection
+\c
+SELECT * FROM svartest_dml.view01;
+
+LET svartest_dml.sesvar45 = ROW(5, 6, 7, 8);
+
+SELECT * FROM svartest_dml.view01;
+
+-- should fail (dependency)
+DROP VARIABLE svartest_dml.sesvar45;
+
+DROP VIEW svartest_dml.view01;
+
+-- test of access variables from generic plans
+CREATE OR REPLACE FUNCTION svartest_dml.fx06()
+RETURNS numeric AS $$
+BEGIN
+ RETURN VARIABLE(sesvar43);
+END;
+$$ LANGUAGE plpgsql;
+
+SET plan_cache_mode TO force_generic_plan;
+
+LET sesvar43 = 6.28;
+
+SELECT svartest_dml.fx06();
+
+LET sesvar43 = VARIABLE(sesvar43) * 2;
+
+SELECT svartest_dml.fx06();
+
+-- plan cache invalidation test
+DROP VARIABLE sesvar43;
+
+-- should fail
+SELECT svartest_dml.fx06();
+
+CREATE VARIABLE sesvar43 AS numeric;
+
+LET sesvar43 = 2.72;
+
+SELECT svartest_dml.fx06();
+
+DROP VARIABLE sesvar43;
+
+CREATE DOMAIN svartest_dml.int_not_null AS int CHECK(value IS NOT NULL);
+CREATE VARIABLE svartest_dml.sesvar46 AS svartest_dml.int_not_null;
+
+-- should fail
+LET svartest_dml.sesvar46 = NULL;
+-- should be ok
+LET svartest_dml.sesvar46 = 100;
+
+LET svartest_dml.sesvar45 = ROW(1,2,3,4);
+LET svartest_dml.sesvar45.a = 100;
+SELECT row_to_json(VARIABLE(svartest_dml.sesvar45));
+
+CREATE ROLE regress_svartest_dml_write_only_role;
+GRANT USAGE ON SCHEMA svartest_dml TO regress_svartest_dml_write_only_role;
+GRANT UPDATE ON VARIABLE svartest_dml.sesvar45 TO regress_svartest_dml_write_only_role;
+
+SET ROLE TO regress_svartest_dml_write_only_role;
+
+-- should fail
+SELECT VARIABLE(svartest_dml.sesvar45);
+
+-- should be ok
+LET svartest_dml.sesvar45.b = 200;
+
+SET ROLE TO DEFAULT;
+
+SELECT row_to_json(VARIABLE(svartest_dml.sesvar45));
+
+CREATE VARIABLE svartest_dml.sesvar47 AS int[];
+LET svartest_dml.sesvar47 = ARRAY[1,2,3];
+
+GRANT UPDATE ON VARIABLE svartest_dml.sesvar47 TO regress_svartest_dml_write_only_role;
+
+SET ROLE TO regress_svartest_dml_write_only_role;
+
+-- should fail
+SELECT VARIABLE(svartest_dml.sesvar47);
+
+-- should be ok
+LET svartest_dml.sesvar47[1] = 200;
+
+SET ROLE TO DEFAULT;
+
+SELECT VARIABLE(svartest_dml.sesvar47);
+
+CREATE VARIABLE svartest_dml.sesvar48 AS int4multirange[];
+LET svartest_dml.sesvar48 = NULL;
+LET svartest_dml.sesvar48 = '{"{[2,8),[11,14)}","{[5,8),[12,14)}"}';
+LET svartest_dml.sesvar48[2] = '{[5,8),[12,100)}';
+SELECT VARIABLE(svartest_dml.sesvar48);
+
+DROP SCHEMA svartest_dml CASCADE;
+DROP ROLE regress_svartest_dml_write_only_role;
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 0c859cfe187..b08ead23a79 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1542,6 +1542,7 @@ LargeObjectDesc
Latch
LauncherLastStartTimesEntry
LerpFunc
+LetStmt
LexDescr
LexemeEntry
LexemeHashKey
--
2.49.0