From 223d59267a584b89c270478453dac15d5d02a9f8 Mon Sep 17 00:00:00 2001
From: "okbob@github.com" <okbob@github.com>
Date: Thu, 6 Jul 2023 08:29:21 +0200
Subject: [PATCH 02/18] Storage for session variables and SQL interface

Session variables are stored in session memory in dedicated hash table. The
write access is done by LET command. The read access is by SELECT command.
The access rights should be checked.

The identifiers of session variables should be shadowed by possible column's
identifiers always. This is by design feature. We don't want to break an
application by creating some session variable that is badly named.

The limits of this patch (solved by other patches):

- session variables block parallel execution
- session variables blocks simple expression evaluation (in plpgsql)
- SQL functions with session variables are not inlined
- CALL statement is not supported (usage of direct access to express executor)
- memory used by dropped session variables is not released

Implementations of EXPLAIN LET and PREPARE LET statements
are in separate patches (for better readability)
---
 doc/src/sgml/catalogs.sgml                    |  13 +
 doc/src/sgml/ddl.sgml                         |  39 +
 doc/src/sgml/event-trigger.sgml               |  24 +
 doc/src/sgml/plpgsql.sgml                     |  12 +
 doc/src/sgml/ref/allfiles.sgml                |   1 +
 doc/src/sgml/ref/alter_variable.sgml          |   1 +
 doc/src/sgml/ref/create_variable.sgml         |   1 +
 doc/src/sgml/ref/drop_variable.sgml           |   1 +
 doc/src/sgml/ref/let.sgml                     |  97 ++
 doc/src/sgml/reference.sgml                   |   1 +
 src/backend/catalog/dependency.c              |   5 +
 src/backend/catalog/namespace.c               | 326 +++++++
 src/backend/catalog/pg_variable.c             |   2 +
 src/backend/commands/Makefile                 |   1 +
 src/backend/commands/meson.build              |   1 +
 src/backend/commands/session_variable.c       | 602 ++++++++++++
 src/backend/executor/Makefile                 |   1 +
 src/backend/executor/execExpr.c               |  42 +
 src/backend/executor/execMain.c               |  47 +
 src/backend/executor/meson.build              |   1 +
 src/backend/executor/svariableReceiver.c      | 201 ++++
 src/backend/nodes/nodeFuncs.c                 |  10 +
 src/backend/optimizer/plan/planner.c          |   8 +
 src/backend/optimizer/plan/setrefs.c          | 138 ++-
 src/backend/optimizer/prep/prepjointree.c     |   3 +
 src/backend/optimizer/util/clauses.c          |  35 +-
 src/backend/parser/analyze.c                  | 279 +++++-
 src/backend/parser/gram.y                     |  49 +-
 src/backend/parser/parse_agg.c                |   9 +
 src/backend/parser/parse_expr.c               | 223 ++++-
 src/backend/parser/parse_func.c               |   2 +
 src/backend/rewrite/rewriteHandler.c          |   1 +
 src/backend/tcop/dest.c                       |   7 +
 src/backend/tcop/pquery.c                     |   3 +
 src/backend/tcop/utility.c                    |  16 +
 src/backend/utils/adt/ruleutils.c             |  46 +
 src/backend/utils/cache/plancache.c           |  41 +-
 src/backend/utils/fmgr/fmgr.c                 |  10 +-
 src/bin/psql/tab-complete.c                   |  12 +-
 src/include/catalog/namespace.h               |   2 +
 src/include/catalog/pg_variable.h             |   7 +
 src/include/commands/session_variable.h       |  32 +
 src/include/executor/execdesc.h               |   4 +
 src/include/executor/svariableReceiver.h      |  22 +
 src/include/nodes/execnodes.h                 |  19 +
 src/include/nodes/parsenodes.h                |  17 +
 src/include/nodes/pathnodes.h                 |   5 +
 src/include/nodes/plannodes.h                 |   4 +-
 src/include/nodes/primnodes.h                 |  10 +-
 src/include/optimizer/planmain.h              |   2 +
 src/include/parser/kwlist.h                   |   1 +
 src/include/parser/parse_node.h               |   3 +
 src/include/tcop/cmdtaglist.h                 |   1 +
 src/include/tcop/dest.h                       |   1 +
 src/pl/plpgsql/src/pl_exec.c                  |   3 +-
 .../regress/expected/session_variables.out    | 887 ++++++++++++++++++
 src/test/regress/sql/session_variables.sql    | 604 ++++++++++++
 src/tools/pgindent/typedefs.list              |   5 +
 58 files changed, 3915 insertions(+), 25 deletions(-)
 create mode 100644 doc/src/sgml/ref/let.sgml
 create mode 100644 src/backend/commands/session_variable.c
 create mode 100644 src/backend/executor/svariableReceiver.c
 create mode 100644 src/include/commands/session_variable.h
 create mode 100644 src/include/executor/svariableReceiver.h

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 61891470c9..03d17e9745 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -9742,6 +9742,19 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>varcreate_lsn</structfield> <type>XLogRecPtr</type>
+      </para>
+      <para>
+       LSN of the transaction where variable was created. It is used
+       (in combination with <structfield>oid</structfield>) as everytime
+       unique identifier. Only <structfield>oid</structfield> cannot be
+       used for this purpose, because unused <structfield>oid</structfield>
+       can be reused.
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>varname</structfield> <type>name</type>
diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml
index cae53aeb45..55597d4bff 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -5283,6 +5283,45 @@ EXPLAIN SELECT count(*) FROM measurement WHERE logdate &gt;= DATE '2008-01-01';
     commands.  A session variable can be created by the <command>CREATE
     VARIABLE</command> command.
    </para>
+
+   <para>
+    The value of a session variable is set with the <command>LET</command> SQL
+    command.  While session variables share properties with tables, their value
+    cannot be updated with an <command>UPDATE</command> command. The value of a
+    session variable may be retrieved by the <command>SELECT</command> SQL
+    command.
+<programlisting>
+CREATE VARIABLE var1 AS date;
+LET var1 = current_date;
+SELECT var1;
+</programlisting>
+
+    or
+
+<programlisting>
+CREATE VARIABLE public.current_user_id AS integer;
+GRANT READ ON VARIABLE public.current_user_id TO PUBLIC;
+LET current_user_id = (SELECT id FROM users WHERE usename = session_user);
+SELECT current_user_id;
+</programlisting>
+   </para>
+
+   <para>
+    The value of a session variable is local to the current session. Retrieving
+    a variable's value returns either a <literal>NULL</literal> or a default
+    value, unless its value has been set to something else in the current
+    session using the <command>LET</command> command. The content of a variable
+    is not transactional. This is the same as regular variables in PL languages.
+    The session variables are persistent, but the content of session variables
+    is temporary and not shared (like the content of temporary tables).
+   </para>
+
+   <para>
+    The session variables can be shadowed by column references in a query. When
+    a query contains identifiers or qualified identifiers that could be used as
+    both a session variable identifiers and as column identifier, then the
+    column identifier is preferred every time.
+   </para>
   </sect1>
 
  <sect1 id="ddl-others">
diff --git a/doc/src/sgml/event-trigger.sgml b/doc/src/sgml/event-trigger.sgml
index f4a1d949ba..ef2a3579a0 100644
--- a/doc/src/sgml/event-trigger.sgml
+++ b/doc/src/sgml/event-trigger.sgml
@@ -432,6 +432,14 @@
         <entry align="center"><literal>-</literal></entry>
         <entry align="left"></entry>
        </row>
+       <row>
+        <entry align="left"><literal>ALTER VARIABLE</literal></entry>
+        <entry align="center"><literal>X</literal></entry>
+        <entry align="center"><literal>X</literal></entry>
+        <entry align="center"><literal>-</literal></entry>
+        <entry align="center"><literal>-</literal></entry>
+        <entry align="left"></entry>
+       </row>
        <row>
         <entry align="left"><literal>ALTER VIEW</literal></entry>
         <entry align="center"><literal>X</literal></entry>
@@ -728,6 +736,14 @@
         <entry align="center"><literal>-</literal></entry>
         <entry align="left"></entry>
        </row>
+       <row>
+        <entry align="left"><literal>CREATE VARIABLE</literal></entry>
+        <entry align="center"><literal>X</literal></entry>
+        <entry align="center"><literal>X</literal></entry>
+        <entry align="center"><literal>-</literal></entry>
+        <entry align="center"><literal>-</literal></entry>
+        <entry align="left"></entry>
+       </row>
        <row>
         <entry align="left"><literal>CREATE VIEW</literal></entry>
         <entry align="center"><literal>X</literal></entry>
@@ -1024,6 +1040,14 @@
         <entry align="center"><literal>-</literal></entry>
         <entry align="left"></entry>
        </row>
+       <row>
+        <entry align="left"><literal>DROP VARIABLE</literal></entry>
+        <entry align="center"><literal>X</literal></entry>
+        <entry align="center"><literal>X</literal></entry>
+        <entry align="center"><literal>X</literal></entry>
+        <entry align="center"><literal>-</literal></entry>
+        <entry align="left"></entry>
+       </row>
        <row>
         <entry align="left"><literal>DROP VIEW</literal></entry>
         <entry align="center"><literal>X</literal></entry>
diff --git a/doc/src/sgml/plpgsql.sgml b/doc/src/sgml/plpgsql.sgml
index c2b9c6adb0..87813dc0d4 100644
--- a/doc/src/sgml/plpgsql.sgml
+++ b/doc/src/sgml/plpgsql.sgml
@@ -5993,6 +5993,18 @@ $$ LANGUAGE plpgsql STRICT IMMUTABLE;
 </programlisting>
     </para>
    </sect3>
+
+   <sect3 id="plpgsql-session-variables">
+    <title><command>Session variables</command></title>
+
+    <para>
+     The <application>PL/pgSQL</application> language has no packages, and
+     therefore no package variables or package constants.
+     <productname>PostgreSQL</productname> has session variables and immutable
+     session variables. Session variables can be created by <command>CREATE
+     VARIABLE</command>, as described in <xref linkend="sql-createvariable"/>.
+    </para>
+   </sect3>
   </sect2>
 
   <sect2 id="plpgsql-porting-appendix">
diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml
index c9de6c8560..cf199a5b36 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 d87570e7d8..d2036351e5 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 04901c5510..faed05e22b 100644
--- a/doc/src/sgml/ref/create_variable.sgml
+++ b/doc/src/sgml/ref/create_variable.sgml
@@ -151,6 +151,7 @@ SELECT var1;
   <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 5bdb3560f0..67988b5fcd 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 0000000000..63968eddd9
--- /dev/null
+++ b/doc/src/sgml/ref/let.sgml
@@ -0,0 +1,97 @@
+<!--
+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><literal>session_variable</literal></term>
+    <listitem>
+     <para>
+      The name of the session variable.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>sql_expression</literal></term>
+    <listitem>
+     <para>
+      An SQL expression (can be subquery in parenthesis). The result must
+      be of castable to the same data type as the session variable (in
+      implicit or assignment context).
+     </para>
+    </listitem>
+   </varlistentry>
+
+  </variablelist>
+
+  <para>
+   Example:
+<programlisting>
+CREATE VARIABLE myvar AS integer;
+LET myvar = 10;
+LET myvar = (SELECT sum(val) FROM tab);
+</programlisting>
+  </para>
+ </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 f3217c8601..be9105ded5 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/catalog/dependency.c b/src/backend/catalog/dependency.c
index 1adcbc5c05..e3add7a0a2 100644
--- a/src/backend/catalog/dependency.c
+++ b/src/backend/catalog/dependency.c
@@ -1914,6 +1914,11 @@ find_expr_references_walker(Node *node,
 	{
 		Param	   *param = (Param *) node;
 
+		/* A variable parameter depends on the session variable */
+		if (param->paramkind == PARAM_VARIABLE)
+			add_object_address(OCLASS_VARIABLE, param->paramvarid, 0,
+							   context->addrs);
+
 		/* A parameter must depend on the parameter's datatype */
 		add_object_address(OCLASS_TYPE, param->paramtype, 0,
 						   context->addrs);
diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c
index d3b7f76f43..7c879c1694 100644
--- a/src/backend/catalog/namespace.c
+++ b/src/backend/catalog/namespace.c
@@ -3404,6 +3404,332 @@ LookupVariable(const char *nspname,
 	return varoid;
 }
 
+/*
+ * The input list contains names with indirection expressions used as the left
+ * part of LET statement. The following routine returns a new list with only
+ * initial strings (names) - without indirection expressions.
+ */
+List *
+NamesFromList(List *names)
+{
+	ListCell   *l;
+	List	   *result = NIL;
+
+	foreach(l, names)
+	{
+		Node	   *n = lfirst(l);
+
+		if (IsA(n, String))
+		{
+			result = lappend(result, n);
+		}
+		else
+			break;
+	}
+
+	return result;
+}
+
+/*
+ * IdentifyVariable - try to find variable identified by list of names.
+ *
+ * Before this call we don't know, how these fields should be mapped to
+ * schema name, variable name and attribute name. In this routine
+ * we try to apply passed names to all possible combinations of schema name,
+ * variable name and attribute name, and we count valid combinations.
+ *
+ * Returns oid of identified variable. When last field of names list is
+ * identified as an attribute, then the argument attrname contains the name of
+ * this field.
+ *
+ * When there is is no valid combinations, we can be sure the specified list of
+ * names cannot identify a session variable. In this case we return InvalidOid.
+ *
+ * We can find more valid combination than one.
+ * Example: users can have session variable x in schema y, and
+ * session variable y with attribute x inside some schema from
+ * search path. In this situation the meaning of expression "y"."x"
+ * is ambiguous. In this case this routine returns oid of variable
+ * x in schema y, and the output parameter "not_unique" is set to
+ * true. In this case this variable is locked.
+ *
+ * The AccessShareLock is created on related session variable. The lock
+ * will be kept for the whole transaction.
+ *
+ * Note: the out attrname should be used only when the session variable
+ * is identified. When the session variable is not identified, then this
+ * output variable can hold reference to some string, but isn't sure
+ * about its semantics.
+ *
+ * When we use this routine for identification of shadowed variable,
+ * we don't want to raise any error. Shadowing column reference is correct,
+ * and we don't want to break execution due shadowing check.
+ */
+Oid
+IdentifyVariable(List *names, char **attrname, bool *not_unique, bool noerror)
+{
+	Node	   *field1 = NULL;
+	Node	   *field2 = NULL;
+	Node	   *field3 = NULL;
+	Node	   *field4 = NULL;
+	char	   *a = NULL;
+	char	   *b = NULL;
+	char	   *c = NULL;
+	char	   *d = NULL;
+	Oid			varid = InvalidOid;
+	Oid			old_varid = InvalidOid;
+	uint64		inval_count;
+	bool		retry = false;
+
+	/*
+	 * DDL operations can change the results of a name lookup.  Since all such
+	 * operations will generate invalidation messages, we keep track of
+	 * whether any such messages show up while we're performing the operation,
+	 * and retry until either (1) no more invalidation messages show up or (2)
+	 * the answer doesn't change.
+	 */
+	for (;;)
+	{
+		Oid			varoid_without_attr = InvalidOid;
+		Oid			varoid_with_attr = InvalidOid;
+
+		*not_unique = false;
+		*attrname = NULL;
+		varid = InvalidOid;
+
+		inval_count = SharedInvalidMessageCounter;
+
+		switch (list_length(names))
+		{
+			case 1:
+				field1 = linitial(names);
+
+				Assert(IsA(field1, String));
+
+				varid = LookupVariable(NULL, strVal(field1), true);
+				break;
+
+			case 2:
+				field1 = linitial(names);
+				field2 = lsecond(names);
+
+				Assert(IsA(field1, String));
+				a = strVal(field1);
+
+				if (IsA(field2, String))
+				{
+					/* when both fields are of string type */
+					b = strVal(field2);
+
+					/*
+					 * a.b can mean "schema"."variable" or "variable"."field".
+					 * Check both variants, and returns InvalidOid with
+					 * not_unique flag, when both interpretations are
+					 * possible.
+					 */
+					varoid_without_attr = LookupVariable(a, b, true);
+					varoid_with_attr = LookupVariable(NULL, a, true);
+				}
+				else
+				{
+					/* The last field of list can be star too. */
+					Assert(IsA(field2, A_Star));
+
+					/*
+					 * In this case, the field1 should be variable name. But
+					 * direct unboxing of composite session variables is not
+					 * supported now, and then we don't need to try lookup
+					 * related variable.
+					 *
+					 * Unboxing is supported by syntax (var).*
+					 */
+					return InvalidOid;
+				}
+
+				if (OidIsValid(varoid_without_attr) && OidIsValid(varoid_with_attr))
+				{
+					*not_unique = true;
+					varid = varoid_without_attr;
+				}
+				else if (OidIsValid(varoid_without_attr))
+				{
+					varid = varoid_without_attr;
+				}
+				else if (OidIsValid(varoid_with_attr))
+				{
+					*attrname = b;
+					varid = varoid_with_attr;
+				}
+				break;
+
+			case 3:
+				{
+					bool		field1_is_catalog = false;
+
+					field1 = linitial(names);
+					field2 = lsecond(names);
+					field3 = lthird(names);
+
+					Assert(IsA(field1, String));
+					Assert(IsA(field2, String));
+
+					a = strVal(field1);
+					b = strVal(field2);
+
+					if (IsA(field3, String))
+					{
+						c = strVal(field3);
+
+						/*
+						 * a.b.c can mean catalog.schema.variable or
+						 * schema.variable.field.
+						 *
+						 * Check both variants, and set not_unique flag, when
+						 * both interpretations are possible.
+						 *
+						 * When third node is star, only possible
+						 * interpretation is schema.variable.*, but this
+						 * pattern is not supported now.
+						 */
+						varoid_with_attr = LookupVariable(a, b, true);
+
+						/*
+						 * check pattern catalog.schema.variable only when
+						 * there is possibility to success.
+						 */
+						if (strcmp(a, get_database_name(MyDatabaseId)) == 0)
+						{
+							field1_is_catalog = true;
+							varoid_without_attr = LookupVariable(b, c, true);
+						}
+					}
+					else
+					{
+						Assert(IsA(field3, A_Star));
+						return InvalidOid;
+					}
+
+					if (OidIsValid(varoid_without_attr) && OidIsValid(varoid_with_attr))
+					{
+						*not_unique = true;
+						varid = varoid_without_attr;
+					}
+					else if (OidIsValid(varoid_without_attr))
+					{
+						varid = varoid_without_attr;
+					}
+					else if (OidIsValid(varoid_with_attr))
+					{
+						*attrname = c;
+						varid = varoid_with_attr;
+					}
+
+					/*
+					 * When we didn't find variable, we can (when it is
+					 * allowed) raise cross-database reference error.
+					 */
+					if (!OidIsValid(varid) && !noerror && !field1_is_catalog)
+						ereport(ERROR,
+								(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+								 errmsg("cross-database references are not implemented: %s",
+										NameListToString(names))));
+				}
+				break;
+
+			case 4:
+				{
+					field1 = linitial(names);
+					field2 = lsecond(names);
+					field3 = lthird(names);
+					field4 = lfourth(names);
+
+					Assert(IsA(field1, String));
+					Assert(IsA(field2, String));
+					Assert(IsA(field3, String));
+
+					a = strVal(field1);
+					b = strVal(field2);
+					c = strVal(field3);
+
+					/*
+					 * In this case, "a" is used as catalog name - check it.
+					 */
+					if (strcmp(a, get_database_name(MyDatabaseId)) != 0)
+					{
+						if (!noerror)
+							ereport(ERROR,
+									(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+									 errmsg("cross-database references are not implemented: %s",
+											NameListToString(names))));
+					}
+					else
+					{
+						if (IsA(field4, String))
+						{
+							d = strVal(field4);
+						}
+						else
+						{
+							Assert(IsA(field4, A_Star));
+							return InvalidOid;
+						}
+
+						*attrname = d;
+						varid = LookupVariable(b, c, true);
+					}
+				}
+				break;
+
+			default:
+				if (!noerror)
+					ereport(ERROR,
+							(errcode(ERRCODE_SYNTAX_ERROR),
+							 errmsg("improper qualified name (too many dotted names): %s",
+									NameListToString(names))));
+				return InvalidOid;
+		}
+
+		/*
+		 * If, upon retry, we get back the same OID we did last time, then the
+		 * invalidation messages we processed did not change the final answer.
+		 * So we're done.
+		 *
+		 * If we got a different OID, we've locked the variable that used to
+		 * have this name rather than the one that does now.  So release the
+		 * lock.
+		 */
+		if (retry)
+		{
+			if (old_varid == varid)
+				break;
+
+			if (OidIsValid(old_varid))
+				UnlockDatabaseObject(VariableRelationId, old_varid, 0, AccessShareLock);
+		}
+
+		/*
+		 * Lock the variable.  This will also accept any pending invalidation
+		 * messages.  If we got back InvalidOid, indicating not found, then
+		 * there's nothing to lock, but we accept invalidation messages
+		 * anyway, to flush any negative catcache entries that may be
+		 * lingering.
+		 */
+		if (!OidIsValid(varid))
+			AcceptInvalidationMessages();
+		else if (OidIsValid(varid))
+			LockDatabaseObject(VariableRelationId, varid, 0, AccessShareLock);
+
+		if (inval_count == SharedInvalidMessageCounter)
+			break;
+
+		retry = true;
+		old_varid = varid;
+		varid = InvalidOid;
+	}
+
+	return varid;
+}
+
 /*
  * DeconstructQualifiedName
  *		Given a possibly-qualified name expressed as a list of String nodes,
diff --git a/src/backend/catalog/pg_variable.c b/src/backend/catalog/pg_variable.c
index 6b071ee8eb..13d44ee39d 100644
--- a/src/backend/catalog/pg_variable.c
+++ b/src/backend/catalog/pg_variable.c
@@ -26,6 +26,7 @@
 #include "parser/parse_type.h"
 #include "utils/builtins.h"
 #include "utils/lsyscache.h"
+#include "utils/pg_lsn.h"
 #include "utils/syscache.h"
 
 static ObjectAddress create_variable(const char *varName,
@@ -101,6 +102,7 @@ create_variable(const char *varName,
 	varid = GetNewOidWithIndex(rel, VariableOidIndexId, Anum_pg_variable_oid);
 
 	values[Anum_pg_variable_oid - 1] = ObjectIdGetDatum(varid);
+	values[Anum_pg_variable_varcreate_lsn - 1] = LSNGetDatum(GetXLogInsertRecPtr());
 	values[Anum_pg_variable_varname - 1] = NameGetDatum(&varname);
 	values[Anum_pg_variable_varnamespace - 1] = ObjectIdGetDatum(varNamespace);
 	values[Anum_pg_variable_vartype - 1] = ObjectIdGetDatum(varType);
diff --git a/src/backend/commands/Makefile b/src/backend/commands/Makefile
index 48f7348f91..1cfaeca51e 100644
--- a/src/backend/commands/Makefile
+++ b/src/backend/commands/Makefile
@@ -50,6 +50,7 @@ OBJS = \
 	schemacmds.o \
 	seclabel.o \
 	sequence.o \
+	session_variable.o \
 	statscmds.o \
 	subscriptioncmds.o \
 	tablecmds.o \
diff --git a/src/backend/commands/meson.build b/src/backend/commands/meson.build
index 6dd00a4abd..ca621be5ec 100644
--- a/src/backend/commands/meson.build
+++ b/src/backend/commands/meson.build
@@ -38,6 +38,7 @@ backend_sources += files(
   'schemacmds.c',
   'seclabel.c',
   'sequence.c',
+  'session_variable.c',
   'statscmds.c',
   'subscriptioncmds.c',
   'tablecmds.c',
diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c
new file mode 100644
index 0000000000..3d3fe35046
--- /dev/null
+++ b/src/backend/commands/session_variable.c
@@ -0,0 +1,602 @@
+/*-------------------------------------------------------------------------
+ *
+ * session_variable.c
+ *	  session variable creation/manipulation commands
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/commands/session_variable.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "catalog/pg_variable.h"
+#include "commands/session_variable.h"
+#include "executor/svariableReceiver.h"
+#include "funcapi.h"
+#include "miscadmin.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/snapmgr.h"
+#include "utils/syscache.h"
+
+/*
+ * Values of session variables are stored in the backend local memory
+ * inside sessionvars hash table in binary format inside a dedicated memory
+ * context SVariableMemoryContext.  The hash key is oid
+ * of related entry in pg_variable table. But long term unambiguity of oid is
+ * not guaranteed. As an example, for a value with one oid a session can be
+ * inactive for long time, while in the meantime the related session variable
+ * can be dropped in another session, assigned oid can be released, and
+ * theoreticaly this oid can be assigned to different session variable.
+ * At the end, the reading of value stored in old session should to fail,
+ * because related entry in pg_variable will not be consistent with
+ * stored value. This is reason why we do check consistency between stored
+ * value and catalog by create_lsn value.
+ *
+ * Before any usage (not only read in transaction) we need to check consistency
+ * with pg_variable entry. When there is not entry with stored oid, the related
+ * variable was dropped, and stored value is not consistent. When entry with
+ * known oid, but lsn number is different, entry of pg_variable was created
+ * for different variable and stored value is not consistent again.
+ */
+typedef struct SVariableData
+{
+	Oid			varid;			/* pg_variable OID of the variable (hash key) */
+	XLogRecPtr	create_lsn;
+
+	bool		isnull;
+	Datum		value;
+
+	Oid			typid;
+	int16		typlen;
+	bool		typbyval;
+
+	bool		is_domain;
+
+	/*
+	 * domain_check_extra holds an extra (cache) for domain check.
+	 * This extra is usually stored in fn_mcxt. We do not have same
+	 * memory context for session variables, but we can use
+	 * TopTransactionContext instead. Fresh extra is forced when
+	 * we detect we are in a different transaction (different
+	 * local transaction id domain_check_extra_lxid).
+	 */
+	void	   *domain_check_extra;
+	LocalTransactionId domain_check_extra_lxid;
+
+	/*
+	 * Stored value and type description can be outdated when we receive
+	 * sinval message. We have to check always if the stored data are
+	 * trustful.
+	 */
+	bool		is_valid;
+
+	uint32		hashvalue;		/* used for pairing sinval message */
+} SVariableData;
+
+typedef SVariableData *SVariable;
+
+static HTAB *sessionvars = NULL;	/* hash table for session variables */
+
+static MemoryContext SVariableMemoryContext = NULL;
+
+/*
+ * Callback function for session variable invalidation.
+ */
+static void
+pg_variable_cache_callback(Datum arg, int cacheid, uint32 hashvalue)
+{
+	HASH_SEQ_STATUS status;
+	SVariable	svar;
+
+	elog(DEBUG1, "pg_variable_cache_callback %u %u", cacheid, hashvalue);
+
+	Assert(sessionvars);
+
+	/*
+	 * When the hashvalue is not specified, then we have to recheck all
+	 * currently used session variables. Since we can't guarantee the exact
+	 * session variable from its hashValue, we also have to iterate over all
+	 * items of the sessionvars hash table.
+	 */
+	hash_seq_init(&status, sessionvars);
+
+	while ((svar = (SVariable) hash_seq_search(&status)) != NULL)
+	{
+		if (hashvalue == 0 || svar->hashvalue == hashvalue)
+		{
+			svar->is_valid = false;
+		}
+	}
+}
+
+/*
+ * Release stored value, free memory
+ */
+static void
+free_session_variable_value(SVariable svar)
+{
+	/* Clean current value */
+	if (!svar->isnull)
+	{
+		if (!svar->typbyval)
+			pfree(DatumGetPointer(svar->value));
+
+		svar->isnull = true;
+	}
+
+	svar->value = (Datum) 0;
+}
+
+/*
+ * Returns true when the entry in pg_variable is consistent with
+ * the given session variable.
+ */
+static bool
+is_session_variable_valid(SVariable svar)
+{
+	HeapTuple	tp;
+	bool		result = false;
+
+	Assert(OidIsValid(svar->varid));
+
+	tp = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(svar->varid));
+
+	if (HeapTupleIsValid(tp))
+	{
+		/*
+		 * In this case, the only oid cannot be used as unique identifier,
+		 * because the oid counter can wraparound, and the oid can be used for
+		 * new other session variable. We do a second check against 64bit
+		 * unique identifier.
+		 */
+		if (svar->create_lsn == ((Form_pg_variable) GETSTRUCT(tp))->varcreate_lsn)
+			result = true;
+
+		ReleaseSysCache(tp);
+	}
+
+	return result;
+}
+
+/*
+ * Update attributes cached in svar
+ */
+static void
+setup_session_variable(SVariable svar, Oid varid)
+{
+	HeapTuple	tup;
+	Form_pg_variable varform;
+
+	Assert(OidIsValid(varid));
+
+	tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid));
+
+	if (!HeapTupleIsValid(tup))
+		elog(ERROR, "cache lookup failed for session variable %u", varid);
+
+	varform = (Form_pg_variable) GETSTRUCT(tup);
+
+	svar->varid = varid;
+	svar->create_lsn = varform->varcreate_lsn;
+
+	svar->typid = varform->vartype;
+
+	get_typlenbyval(svar->typid, &svar->typlen, &svar->typbyval);
+
+	svar->is_domain = (get_typtype(varform->vartype) == TYPTYPE_DOMAIN);
+	svar->domain_check_extra = NULL;
+	svar->domain_check_extra_lxid = InvalidLocalTransactionId;
+
+	svar->isnull = true;
+	svar->value = (Datum) 0;
+
+	svar->is_valid = true;
+
+	svar->hashvalue = GetSysCacheHashValue1(VARIABLEOID,
+											ObjectIdGetDatum(varid));
+
+	ReleaseSysCache(tup);
+}
+
+/*
+ * Assign some content to the session variable. It's copied to
+ * SVariableMemoryContext if necessary.
+ *
+ * If any error happens, the existing value shouldn't be modified.
+ */
+static void
+set_session_variable(SVariable svar, Datum value, bool isnull)
+{
+	Datum		newval;
+	SVariableData locsvar,
+			   *_svar;
+
+	Assert(svar);
+	Assert(!isnull || value == (Datum) 0);
+
+	/*
+	 * Use typbyval, typbylen from session variable only when they are
+	 * trustable (the invalidation message was not accepted for this variable).
+	 * When the variable is possibly invalid, force setup.
+	 *
+	 * Do not do it against passed svar, it should be unchanged, when an
+	 * assignment is not successful (the datumCopy can fail).
+	 */
+	if (!svar->is_valid)
+	{
+		setup_session_variable(&locsvar, svar->varid);
+		_svar = &locsvar;
+	}
+	else
+		_svar = svar;
+
+	if (!isnull)
+	{
+		MemoryContext oldcxt = MemoryContextSwitchTo(SVariableMemoryContext);
+
+		newval = datumCopy(value, _svar->typbyval, _svar->typlen);
+
+		MemoryContextSwitchTo(oldcxt);
+	}
+	else
+		newval = value;
+
+	free_session_variable_value(svar);
+
+	/* We can overwrite old variable now. No error expected */
+	if (svar != _svar)
+		memcpy(svar, _svar, sizeof(SVariableData));
+
+	svar->value = newval;
+	svar->isnull = isnull;
+
+	/*
+	 * XXX While unlikely, an error here is possible. It wouldn't leak memory
+	 * as the allocated chunk has already been correctly assigned to the
+	 * session variable, but would contradict this function contract, which is
+	 * that this function should either succeed or leave the current value
+	 * untouched.
+	 */
+	elog(DEBUG1, "session variable \"%s.%s\" (oid:%u) has new value",
+		 get_namespace_name(get_session_variable_namespace(svar->varid)),
+		 get_session_variable_name(svar->varid),
+		 svar->varid);
+}
+
+/*
+ * Create the hash table for storing session variables.
+ */
+static void
+create_sessionvars_hashtables(void)
+{
+	HASHCTL		vars_ctl;
+
+	Assert(!sessionvars);
+
+	if (!SVariableMemoryContext)
+	{
+		/* Read sinval messages */
+		CacheRegisterSyscacheCallback(VARIABLEOID,
+									  pg_variable_cache_callback,
+									  (Datum) 0);
+
+		/* We need our own long lived memory context */
+		SVariableMemoryContext =
+			AllocSetContextCreate(TopMemoryContext,
+								  "session variables",
+								  ALLOCSET_START_SMALL_SIZES);
+	}
+
+	memset(&vars_ctl, 0, sizeof(vars_ctl));
+	vars_ctl.keysize = sizeof(Oid);
+	vars_ctl.entrysize = sizeof(SVariableData);
+	vars_ctl.hcxt = SVariableMemoryContext;
+
+	sessionvars = hash_create("Session variables", 64, &vars_ctl,
+							  HASH_ELEM | HASH_BLOBS | HASH_CONTEXT);
+}
+
+/*
+ * Search a seesion variable in the hash table given its oid. If it
+ * doesn't exist, then insert it there.
+ *
+ * Caller is responsible for doing permission checks.
+ *
+ * As side effect this function acquires AccessShareLock on
+ * related session variable until the end of the transaction.
+ */
+static SVariable
+get_session_variable(Oid varid)
+{
+	SVariable	svar;
+	bool		found;
+
+	/* Protect used session variable against drop until transaction end */
+	LockDatabaseObject(VariableRelationId, varid, 0, AccessShareLock);
+
+	if (!sessionvars)
+		create_sessionvars_hashtables();
+
+	svar = (SVariable) hash_search(sessionvars, &varid,
+								   HASH_ENTER, &found);
+
+	if (found)
+	{
+		if (!svar->is_valid)
+		{
+			/*
+			 * The variable can be flagged as invalid by processing invalidation
+			 * message, but can be validated by recheck against system catalog.
+			 */
+			if (is_session_variable_valid(svar))
+				svar->is_valid = true;
+			else
+				/*
+				 * When the value cannot be validated, we can safely throw it.
+				 * There is oid in catalog, but it is related to different
+				 * session variable (different create_lsn).
+				 */
+				free_session_variable_value(svar);
+		}
+	}
+	else
+		svar->is_valid = false;
+
+	/*
+	 * Force setup for not yet initialized variables or variables that cannot
+	 * be validated.
+	 */
+	if (!svar->is_valid)
+	{
+		setup_session_variable(svar, varid);
+
+		elog(DEBUG1, "session variable \"%s.%s\" (oid:%u) has assigned entry in memory (emitted by READ)",
+			 get_namespace_name(get_session_variable_namespace(varid)),
+			 get_session_variable_name(varid),
+			 varid);
+	}
+
+	/* Ensure so returned data is still correct domain */
+	if (svar->is_domain)
+	{
+		/*
+		 * Store domain_check extra in TopTransactionContext. When we are in
+		 * other transaction, the domain_check_extra cache is not valid
+		 * anymore.
+		 */
+		if (svar->domain_check_extra_lxid != MyProc->lxid)
+			svar->domain_check_extra = NULL;
+
+		domain_check(svar->value, svar->isnull,
+					 svar->typid, &svar->domain_check_extra,
+					 TopTransactionContext);
+
+		svar->domain_check_extra_lxid = MyProc->lxid;
+	}
+
+	return svar;
+}
+
+/*
+ * Store the given value in an SVariable, and cache it if not already present.
+ *
+ * Caller is responsible for doing permission checks.
+ *
+ * As side effect this function acquires AccessShareLock on
+ * related session variable until the end of the transaction.
+ */
+void
+SetSessionVariable(Oid varid, Datum value, bool isNull)
+{
+	SVariable	svar;
+	bool		found;
+
+	/* Protect used session variable against drop until transaction end */
+	LockDatabaseObject(VariableRelationId, varid, 0, AccessShareLock);
+
+	if (!sessionvars)
+		create_sessionvars_hashtables();
+
+	svar = (SVariable) hash_search(sessionvars, &varid,
+								   HASH_ENTER, &found);
+
+	if (!found)
+	{
+		setup_session_variable(svar, varid);
+
+		elog(DEBUG1, "session variable \"%s.%s\" (oid:%u) has assigned entry in memory (emitted by WRITE)",
+			 get_namespace_name(get_session_variable_namespace(svar->varid)),
+			 get_session_variable_name(svar->varid),
+			 varid);
+	}
+
+	/*
+	 * This should either succeed or fail without changing the currently
+	 * stored value.
+	 */
+	set_session_variable(svar, value, isNull);
+}
+
+/*
+ * Wrapper around SetSessionVariable after checking for correct permission.
+ */
+void
+SetSessionVariableWithSecurityCheck(Oid varid, Datum value, bool isNull)
+{
+	AclResult	aclresult;
+
+	/*
+	 * Is caller allowed to update 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));
+
+	SetSessionVariable(varid, value, isNull);
+}
+
+/*
+ * Returns copy of value stored in variable.
+ */
+static inline Datum
+copy_session_variable_value(SVariable svar, bool *isNull)
+{
+	Datum		value;
+
+	/* force copy of non NULL value */
+	if (!svar->isnull)
+	{
+		value = datumCopy(svar->value, svar->typbyval, svar->typlen);
+		*isNull = false;
+	}
+	else
+	{
+		value = (Datum) 0;
+		*isNull = true;
+	}
+
+	return value;
+}
+
+/*
+ * Returns a copy of the value of the session variable (in current memory
+ * context) specified by its oid. Caller is responsible for doing permission
+ * checks.
+ */
+Datum
+GetSessionVariable(Oid varid, bool *isNull, Oid *typid)
+{
+	SVariable	svar;
+
+	svar = get_session_variable(varid);
+
+	/*
+	 * Although svar is freshly validated in this point, the svar->is_valid can
+	 * be false, due possible accepting invalidation message inside domain
+	 * check. Now, the validation is done after lock, that can also accept
+	 * invalidation message, so validation should be trustful.
+	 *
+	 * For now, we don't need to repeat validation. Only svar should be valid
+	 * pointer.
+	 */
+	Assert(svar);
+
+	*typid = svar->typid;
+
+	return copy_session_variable_value(svar, isNull);
+}
+
+/*
+ * Returns a copy of the value of the session variable specified by its oid
+ * with a check of the expected type. Like previous GetSessionVariable, the
+ * caller is responsible for doing permission checks.
+ */
+Datum
+GetSessionVariableWithTypeCheck(Oid varid, bool *isNull, Oid expected_typid)
+{
+	SVariable	svar;
+
+	svar = get_session_variable(varid);
+
+	Assert(svar && svar->is_valid);
+
+	if (expected_typid != svar->typid)
+		elog(ERROR, "type of variable \"%s.%s\" is different than expected",
+			 get_namespace_name(get_session_variable_namespace(varid)),
+			 get_session_variable_name(varid));
+
+	return copy_session_variable_value(svar, isNull);
+}
+
+/*
+ * Assign result of evaluated expression to session variable
+ */
+void
+ExecuteLetStmt(ParseState *pstate,
+			   LetStmt *stmt,
+			   ParamListInfo params,
+			   QueryEnvironment *queryEnv,
+			   QueryCompletion *qc)
+{
+	Query	   *query = castNode(Query, stmt->query);
+	List	   *rewritten;
+	DestReceiver *dest;
+	AclResult	aclresult;
+	PlannedStmt *plan;
+	QueryDesc  *queryDesc;
+	Oid			varid = query->resultVariable;
+
+	Assert(OidIsValid(varid));
+
+	/*
+	 * Is it allowed to write to session variable?
+	 */
+	aclresult = object_aclcheck(VariableRelationId, varid, GetUserId(), ACL_UPDATE);
+	if (aclresult != ACLCHECK_OK)
+		aclcheck_error(aclresult, OBJECT_VARIABLE, get_session_variable_name(varid));
+
+	/* Create dest receiver for LET */
+	dest = CreateVariableDestReceiver(varid);
+
+	/* run rewriter - can be used for replacement of DEFAULT node */
+	query = copyObject(query);
+
+	rewritten = QueryRewrite(query);
+
+	Assert(list_length(rewritten) == 1);
+
+	query = linitial_node(Query, rewritten);
+	Assert(query->commandType == CMD_SELECT);
+
+	/* plan the query */
+	plan = pg_plan_query(query, pstate->p_sourcetext,
+						 CURSOR_OPT_PARALLEL_OK, params);
+
+	/*
+	 * Use a snapshot with an updated command ID to ensure this query sees
+	 * results of any previously executed queries.  (This could only matter if
+	 * the planner executed an allegedly-stable function that changed the
+	 * database contents, but let's do it anyway to be parallel to the EXPLAIN
+	 * code path.)
+	 */
+	PushCopiedSnapshot(GetActiveSnapshot());
+	UpdateActiveSnapshotCommandId();
+
+	/* Create a QueryDesc, redirecting output to our tuple receiver */
+	queryDesc = CreateQueryDesc(plan, pstate->p_sourcetext,
+								GetActiveSnapshot(), InvalidSnapshot,
+								dest, params, queryEnv, 0);
+
+	/* call ExecutorStart to prepare the plan for execution */
+	ExecutorStart(queryDesc, 0);
+
+	/*
+	 * Run the plan to completion. The result should be only one row. For an
+	 * check too_many_rows we need to read two rows.
+	 */
+	ExecutorRun(queryDesc, ForwardScanDirection, 2L, true);
+
+	/* save the rowcount if we're given a qc to fill */
+	if (qc)
+		SetQueryCompletion(qc, CMDTAG_LET, queryDesc->estate->es_processed);
+
+	/* and clean up */
+	ExecutorFinish(queryDesc);
+	ExecutorEnd(queryDesc);
+
+	FreeQueryDesc(queryDesc);
+
+	PopActiveSnapshot();
+}
diff --git a/src/backend/executor/Makefile b/src/backend/executor/Makefile
index 11118d0ce0..71248a34f2 100644
--- a/src/backend/executor/Makefile
+++ b/src/backend/executor/Makefile
@@ -76,6 +76,7 @@ OBJS = \
 	nodeWindowAgg.o \
 	nodeWorktablescan.o \
 	spi.o \
+	svariableReceiver.o \
 	tqueue.o \
 	tstoreReceiver.o
 
diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c
index 3181b1136a..501cddd3dc 100644
--- a/src/backend/executor/execExpr.c
+++ b/src/backend/executor/execExpr.c
@@ -34,6 +34,7 @@
 #include "catalog/objectaccess.h"
 #include "catalog/pg_proc.h"
 #include "catalog/pg_type.h"
+#include "catalog/pg_variable.h"
 #include "executor/execExpr.h"
 #include "executor/nodeSubplan.h"
 #include "funcapi.h"
@@ -981,6 +982,47 @@ ExecInitExprRec(Expr *node, ExprState *state,
 						scratch.d.param.paramtype = param->paramtype;
 						ExprEvalPushStep(state, &scratch);
 						break;
+
+					case PARAM_VARIABLE:
+						{
+							int			es_num_session_variables = 0;
+							SessionVariableValue *es_session_variables = NULL;
+							SessionVariableValue *var;
+
+							if (state->parent && state->parent->state)
+							{
+								es_session_variables = state->parent->state->es_session_variables;
+								es_num_session_variables = state->parent->state->es_num_session_variables;
+							}
+
+							Assert(es_session_variables);
+
+							/*
+							 * Use buffered session variables when the
+							 * buffer with copied values is avaiable
+							 * (standard query executor mode)
+							 */
+
+							/* Parameter sanity checks. */
+							if (param->paramid >= es_num_session_variables)
+								elog(ERROR, "paramid of PARAM_VARIABLE param is out of range");
+
+							var = &es_session_variables[param->paramid];
+
+							if (var->typid != param->paramtype)
+								elog(ERROR, "type of buffered value is different than PARAM_VARIABLE type");
+
+							/*
+							 * In this case, pass the value like a
+							 * constant.
+							 */
+							scratch.opcode = EEOP_CONST;
+							scratch.d.constval.value = var->value;
+							scratch.d.constval.isnull = var->isnull;
+							ExprEvalPushStep(state, &scratch);
+						}
+						break;
+
 					case PARAM_EXTERN:
 
 						/*
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 13a9b7da83..45cb2a0b48 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -46,7 +46,9 @@
 #include "catalog/namespace.h"
 #include "catalog/partition.h"
 #include "catalog/pg_publication.h"
+#include "catalog/pg_variable.h"
 #include "commands/matview.h"
+#include "commands/session_variable.h"
 #include "commands/trigger.h"
 #include "executor/execdebug.h"
 #include "executor/nodeSubplan.h"
@@ -201,6 +203,51 @@ standard_ExecutorStart(QueryDesc *queryDesc, int eflags)
 	Assert(queryDesc->sourceText != NULL);
 	estate->es_sourceText = queryDesc->sourceText;
 
+	/*
+	 * The executor doesn't work with session variables directly. Values of
+	 * related session variables are copied to dedicated array, and this array
+	 * is passed to executor.
+	 */
+	if (queryDesc->plannedstmt->sessionVariables)
+	{
+		ListCell   *lc;
+		int			nSessionVariables;
+		int			i = 0;
+
+		/*
+		 * In this case, the query uses session variables, but we have to
+		 * prepare the array with passed values (of used session variables)
+		 * first.
+		 */
+		Assert(!IsParallelWorker());
+		nSessionVariables = list_length(queryDesc->plannedstmt->sessionVariables);
+
+		/* Create the array used for passing values of used session variables */
+		estate->es_session_variables = (SessionVariableValue *)
+			palloc(nSessionVariables * sizeof(SessionVariableValue));
+
+		/* Fill the array */
+		foreach(lc, queryDesc->plannedstmt->sessionVariables)
+		{
+			AclResult	aclresult;
+			Oid			varid = lfirst_oid(lc);
+
+			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].varid = varid;
+			estate->es_session_variables[i].value = GetSessionVariable(varid,
+																	   &estate->es_session_variables[i].isnull,
+																	   &estate->es_session_variables[i].typid);
+
+			i++;
+		}
+
+		estate->es_num_session_variables = nSessionVariables;
+	}
+
 	/*
 	 * Fill in the query environment, if any, from queryDesc.
 	 */
diff --git a/src/backend/executor/meson.build b/src/backend/executor/meson.build
index b511a429ad..b6662c6e8f 100644
--- a/src/backend/executor/meson.build
+++ b/src/backend/executor/meson.build
@@ -64,6 +64,7 @@ backend_sources += files(
   'nodeWindowAgg.c',
   'nodeWorktablescan.c',
   'spi.c',
+  'svariableReceiver.c',
   'tqueue.c',
   'tstoreReceiver.c',
 )
diff --git a/src/backend/executor/svariableReceiver.c b/src/backend/executor/svariableReceiver.c
new file mode 100644
index 0000000000..d89dd23d76
--- /dev/null
+++ b/src/backend/executor/svariableReceiver.c
@@ -0,0 +1,201 @@
+/*-------------------------------------------------------------------------
+ *
+ * svariableReceiver.c
+ *	  An implementation of DestReceiver that stores the result value in
+ *	  a session variable.
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *	  src/backend/executor/svariableReceiver.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+#include "miscadmin.h"
+
+#include "access/detoast.h"
+#include "catalog/pg_variable.h"
+#include "commands/session_variable.h"
+#include "executor/svariableReceiver.h"
+#include "storage/lock.h"
+#include "utils/builtins.h"
+#include "utils/lsyscache.h"
+#include "utils/syscache.h"
+
+/*
+ * This DestReceiver is used by the LET command for storing the result to
+ * session variable.  The result has to have only one tuple with only one
+ * not deleted attribute.  The row counter (field "rows") is incremented
+ * after receiving of any row, and the error is raised when there are no rows
+ * or there are more than one received rows.  Because received tuple can have
+ * deleted attributes, we need to find wirst not deleted attribute
+ * (field "slot_offset"). The value is detoasted before storing to session
+ * variable.
+ */
+typedef struct
+{
+	DestReceiver pub;
+	Oid			varid;
+	bool		need_detoast;		/* we need detoast attr? */
+	int			slot_offset;		/* position of not deleted attr */
+	int			rows;				/* row counter */
+} SVariableState;
+
+/*
+ * Prepare to receive tuples from executor.
+ */
+static void
+svariableStartupReceiver(DestReceiver *self, int operation, TupleDesc typeinfo)
+{
+	SVariableState *myState = (SVariableState *) self;
+	int			natts = typeinfo->natts;
+	int			outcols = 0;
+	int			i;
+	LOCKTAG		locktag PG_USED_FOR_ASSERTS_ONLY;
+
+	Assert(myState->pub.mydest == DestVariable);
+	Assert(OidIsValid(myState->varid));
+	Assert(SearchSysCacheExists1(VARIABLEOID, myState->varid));
+
+#ifdef USE_ASSERT_CHECKING
+
+	SET_LOCKTAG_OBJECT(locktag,
+					   MyDatabaseId,
+					   VariableRelationId,
+					   myState->varid,
+					   0);
+
+	Assert(LockHeldByMe(&locktag, AccessShareLock));
+
+#endif
+
+	for (i = 0; i < natts; i++)
+	{
+		Form_pg_attribute attr = TupleDescAttr(typeinfo, i);
+		Oid			typid;
+		Oid			collid;
+		int32		typmod;
+
+		if (attr->attisdropped)
+			continue;
+
+		if (++outcols > 1)
+			continue;
+
+		get_session_variable_type_typmod_collid(myState->varid,
+												&typid,
+												&typmod,
+												&collid);
+
+		/*
+		 * double check - the type and typmod of target variable should be
+		 * same as type and typmod of assignment expression. It should be, the
+		 * expression is wrapped by cast to target type and typmod.
+		 */
+		if (attr->atttypid != typid ||
+			(attr->atttypmod >= 0 &&
+			 attr->atttypmod != typmod))
+			ereport(ERROR,
+					(errcode(ERRCODE_DATATYPE_MISMATCH),
+					 errmsg("target session variable is of type %s"
+							" but expression is of type %s",
+							format_type_with_typemod(typid, typmod),
+							format_type_with_typemod(attr->atttypid,
+													 attr->atttypmod))));
+
+		myState->need_detoast = attr->attlen == -1;
+		myState->slot_offset = i;
+	}
+
+	if (outcols != 1)
+		ereport(ERROR,
+				(errcode(ERRCODE_SYNTAX_ERROR),
+				 errmsg_plural("assignment expression returned %d column",
+							   "assignment expression returned %d columns",
+							   outcols,
+							   outcols)));
+
+	myState->rows = 0;
+}
+
+/*
+ * Receive a tuple from the executor and store it in session variable.
+ */
+static bool
+svariableReceiveSlot(TupleTableSlot *slot, DestReceiver *self)
+{
+	SVariableState *myState = (SVariableState *) self;
+	Datum		value;
+	bool		isnull;
+	bool		freeval = false;
+
+	/* Make sure the tuple is fully deconstructed */
+	slot_getallattrs(slot);
+
+	value = slot->tts_values[myState->slot_offset];
+	isnull = slot->tts_isnull[myState->slot_offset];
+
+	if (myState->need_detoast && !isnull && VARATT_IS_EXTERNAL(DatumGetPointer(value)))
+	{
+		value = PointerGetDatum(detoast_external_attr((struct varlena *)
+													  DatumGetPointer(value)));
+		freeval = true;
+	}
+
+	myState->rows += 1;
+
+	if (myState->rows > 1)
+		ereport(ERROR,
+				(errcode(ERRCODE_TOO_MANY_ROWS),
+				 errmsg("expression returned more than one row")));
+
+	SetSessionVariable(myState->varid, value, isnull);
+
+	if (freeval)
+		pfree(DatumGetPointer(value));
+
+	return true;
+}
+
+/*
+ * Clean up at end of an executor run
+ */
+static void
+svariableShutdownReceiver(DestReceiver *self)
+{
+	if (((SVariableState *) self)->rows == 0)
+		ereport(ERROR,
+				(errcode(ERRCODE_NO_DATA_FOUND),
+				 errmsg("expression returned no rows")));
+}
+
+/*
+ * Destroy receiver when done with it
+ */
+static void
+svariableDestroyReceiver(DestReceiver *self)
+{
+	pfree(self);
+}
+
+/*
+ * Initially create a DestReceiver object.
+ */
+DestReceiver *
+CreateVariableDestReceiver(Oid varid)
+{
+	SVariableState *self = (SVariableState *) palloc0(sizeof(SVariableState));
+
+	self->pub.receiveSlot = svariableReceiveSlot;
+	self->pub.rStartup = svariableStartupReceiver;
+	self->pub.rShutdown = svariableShutdownReceiver;
+	self->pub.rDestroy = svariableDestroyReceiver;
+	self->pub.mydest = DestVariable;
+
+	self->varid = varid;
+
+	return (DestReceiver *) self;
+}
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index e1a5bc7e95..d79a7509fa 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -4107,6 +4107,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 2e2458b128..a157823c4b 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -321,6 +321,7 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
 	glob->lastPlanNodeId = 0;
 	glob->transientPlan = false;
 	glob->dependsOnRole = false;
+	glob->sessionVariables = NIL;
 
 	/*
 	 * Assess whether it's feasible to use parallel mode for this query. We
@@ -556,6 +557,7 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
 	result->paramExecTypes = glob->paramExecTypes;
 	/* utilityStmt should be null, but we might as well copy it */
 	result->utilityStmt = parse->utilityStmt;
+	result->sessionVariables = glob->sessionVariables;
 	result->stmt_location = parse->stmt_location;
 	result->stmt_len = parse->stmt_len;
 
@@ -722,6 +724,12 @@ subquery_planner(PlannerGlobal *glob, Query *parse,
 	 */
 	pull_up_subqueries(root);
 
+	/*
+	 * Check if some subquery uses session variable. Flag hasSessionVariables
+	 * should be true if query or some subquery uses any session variable.
+	 */
+	pull_up_has_session_variables(root);
+
 	/*
 	 * If this is a simple UNION ALL query, flatten it into an appendrel. We
 	 * do this now because it requires applying pull_up_subqueries to the leaf
diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c
index 22a1fa29f3..2c7802b2cc 100644
--- a/src/backend/optimizer/plan/setrefs.c
+++ b/src/backend/optimizer/plan/setrefs.c
@@ -210,7 +210,9 @@ static List *set_returning_clause_references(PlannerInfo *root,
 static List *set_windowagg_runcondition_references(PlannerInfo *root,
 												   List *runcondition,
 												   Plan *plan);
-
+static bool pull_up_has_session_variables_walker(Node *node,
+												 PlannerInfo *root);
+static void record_plan_variable_dependency(PlannerInfo *root, Oid varid);
 
 /*****************************************************************************
  *
@@ -1293,6 +1295,50 @@ set_plan_refs(PlannerInfo *root, Plan *plan, int rtoffset)
 	return plan;
 }
 
+/*
+ * Search usage of session variables in subqueries
+ */
+void
+pull_up_has_session_variables(PlannerInfo *root)
+{
+	Query	   *query = root->parse;
+
+	if (query->hasSessionVariables)
+	{
+		root->hasSessionVariables = true;
+	}
+	else
+	{
+		(void) query_tree_walker(query,
+								 pull_up_has_session_variables_walker,
+								 (void *) root, 0);
+	}
+}
+
+static bool
+pull_up_has_session_variables_walker(Node *node, PlannerInfo *root)
+{
+	if (node == NULL)
+		return false;
+	if (IsA(node, Query))
+	{
+		Query	   *query = (Query *) node;
+
+		if (query->hasSessionVariables)
+		{
+			root->hasSessionVariables = true;
+			return false;
+		}
+
+		/* Recurse into subselects */
+		return query_tree_walker((Query *) node,
+								 pull_up_has_session_variables_walker,
+								 (void *) root, 0);
+	}
+	return expression_tree_walker(node, pull_up_has_session_variables_walker,
+								  (void *) root);
+}
+
 /*
  * set_indexonlyscan_references
  *		Do set_plan_references processing on an IndexOnlyScan
@@ -1941,8 +1987,9 @@ copyVar(Var *var)
  * This is code that is common to all variants of expression-fixing.
  * We must look up operator opcode info for OpExpr and related nodes,
  * add OIDs from regclass Const nodes into root->glob->relationOids, and
- * add PlanInvalItems for user-defined functions into root->glob->invalItems.
- * We also fill in column index lists for GROUPING() expressions.
+ * add PlanInvalItems for user-defined functions and session variables into
+ * root->glob->invalItems.  We also fill in column index lists for GROUPING()
+ * expressions.
  *
  * We assume it's okay to update opcode info in-place.  So this could possibly
  * scribble on the planner's input data structures, but it's OK.
@@ -2032,15 +2079,28 @@ fix_expr_common(PlannerInfo *root, Node *node)
 				g->cols = cols;
 		}
 	}
+	else if (IsA(node, Param))
+	{
+		Param	   *p = (Param *) node;
+
+		if (p->paramkind == PARAM_VARIABLE)
+			record_plan_variable_dependency(root, p->paramvarid);
+	}
 }
 
 /*
  * fix_param_node
  *		Do set_plan_references processing on a Param
+ *		Collect session variables list and replace variable oid by
+ *		index to collected list.
  *
  * If it's a PARAM_MULTIEXPR, replace it with the appropriate Param from
  * root->multiexpr_params; otherwise no change is needed.
  * Just for paranoia's sake, we make a copy of the node in either case.
+ *
+ * If it's a PARAM_VARIABLE, then we collect used session variables in
+ * list root->glob->sessionVariable. We should assign Param paramvarid
+ * too, and it is position of related session variable in mentioned list.
  */
 static Node *
 fix_param_node(PlannerInfo *root, Param *p)
@@ -2059,6 +2119,41 @@ fix_param_node(PlannerInfo *root, Param *p)
 			elog(ERROR, "unexpected PARAM_MULTIEXPR ID: %d", p->paramid);
 		return copyObject(list_nth(params, colno - 1));
 	}
+
+	if (p->paramkind == PARAM_VARIABLE)
+	{
+		ListCell   *lc;
+		int			n = 0;
+		bool		found = false;
+
+		/* We will modify object */
+		p = (Param *) copyObject(p);
+
+		/*
+		 * Now, we can actualize list of session variables, and we can
+		 * complete paramid parameter.
+		 */
+		foreach(lc, root->glob->sessionVariables)
+		{
+			if (lfirst_oid(lc) == p->paramvarid)
+			{
+				p->paramid = n;
+				found = true;
+				break;
+			}
+			n += 1;
+		}
+
+		if (!found)
+		{
+			root->glob->sessionVariables = lappend_oid(root->glob->sessionVariables,
+													   p->paramvarid);
+			p->paramid = n;
+		}
+
+		return (Node *) p;
+	}
+
 	return (Node *) copyObject(p);
 }
 
@@ -2120,7 +2215,10 @@ fix_alternative_subplan(PlannerInfo *root, AlternativeSubPlan *asplan,
  * replacing Aggref nodes that should be replaced by initplan output Params,
  * choosing the best implementation for AlternativeSubPlans,
  * looking up operator opcode info for OpExpr and related nodes,
- * and adding OIDs from regclass Const nodes into root->glob->relationOids.
+ * adding OIDs from regclass Const nodes into root->glob->relationOids,
+ * and assigning paramvarid to PARAM_VARIABLE params, and collecting
+ * of OIDs of session variables in root->glob->sessionVariables list
+ * (paramvarid is an position of related session variable in this list).
  *
  * 'node': the expression to be modified
  * 'rtoffset': how much to increment varnos by
@@ -2142,7 +2240,8 @@ fix_scan_expr(PlannerInfo *root, Node *node, int rtoffset, double num_exec)
 		root->multiexpr_params != NIL ||
 		root->glob->lastPHId != 0 ||
 		root->minmax_aggs != NIL ||
-		root->hasAlternativeSubPlans)
+		root->hasAlternativeSubPlans ||
+		root->hasSessionVariables)
 	{
 		return fix_scan_expr_mutator(node, &context);
 	}
@@ -3505,6 +3604,25 @@ record_plan_type_dependency(PlannerInfo *root, Oid typid)
 	}
 }
 
+/*
+ * Record dependency on a session variable. The variable can be used as a
+ * session variable in expression list, or target of LET statement.
+ */
+static void
+record_plan_variable_dependency(PlannerInfo *root, Oid varid)
+{
+	PlanInvalItem *inval_item = makeNode(PlanInvalItem);
+
+	/* paramid is still session variable id */
+	inval_item->cacheId = VARIABLEOID;
+	inval_item->hashValue = GetSysCacheHashValue1(VARIABLEOID,
+												  ObjectIdGetDatum(varid));
+
+	/* Append this variable to global, register dependency */
+	root->glob->invalItems = lappend(root->glob->invalItems,
+									 inval_item);
+}
+
 /*
  * extract_query_dependencies
  *		Given a rewritten, but not yet planned, query or queries
@@ -3590,9 +3708,9 @@ extract_query_dependencies_walker(Node *node, PlannerInfo *context)
 			}
 
 			/*
-			 * Ignore other utility statements, except those (such as EXPLAIN)
-			 * that contain a parsed-but-not-planned query.  For those, we
-			 * just need to transfer our attention to the contained query.
+			 * Ignore other utility statements, except those (such as EXPLAIN
+			 * or LET) that contain a parsed-but-not-planned query.  For those,
+			 * we just need to transfer our attention to the contained query.
 			 */
 			query = UtilityContainsQuery(query->utilityStmt);
 			if (query == NULL)
@@ -3615,6 +3733,10 @@ extract_query_dependencies_walker(Node *node, PlannerInfo *context)
 					lappend_oid(context->glob->relationOids, rte->relid);
 		}
 
+		/* Record dependency on targer variable of 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,
 								 (void *) context, 0);
diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c
index aa83dd3636..1a08775636 100644
--- a/src/backend/optimizer/prep/prepjointree.c
+++ b/src/backend/optimizer/prep/prepjointree.c
@@ -1265,6 +1265,9 @@ pull_up_simple_subquery(PlannerInfo *root, Node *jtnode, RangeTblEntry *rte,
 	/* If subquery had any RLS conditions, now main query does too */
 	parse->hasRowSecurity |= subquery->hasRowSecurity;
 
+	/* If subquery had session variables, now main query does too */
+	parse->hasSessionVariables |= subquery->hasSessionVariables;
+
 	/*
 	 * subquery won't be pulled up if it hasAggs, hasWindowFuncs, or
 	 * hasTargetSRFs, so no work needed on those flags
diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c
index 94eb56a1e7..2f5b408877 100644
--- a/src/backend/optimizer/util/clauses.c
+++ b/src/backend/optimizer/util/clauses.c
@@ -26,6 +26,7 @@
 #include "catalog/pg_operator.h"
 #include "catalog/pg_proc.h"
 #include "catalog/pg_type.h"
+#include "commands/session_variable.h"
 #include "executor/executor.h"
 #include "executor/functions.h"
 #include "funcapi.h"
@@ -918,6 +919,13 @@ max_parallel_hazard_walker(Node *node, max_parallel_hazard_context *context)
 		if (param->paramkind == PARAM_EXTERN)
 			return false;
 
+		/* We doesn't support passing session variables to workers */
+		if (param->paramkind == PARAM_VARIABLE)
+		{
+			if (max_parallel_hazard_test(PROPARALLEL_RESTRICTED, context))
+				return true;
+		}
+
 		if (param->paramkind != PARAM_EXEC ||
 			!list_member_int(context->safe_param_ids, param->paramid))
 		{
@@ -2372,6 +2380,7 @@ convert_saop_to_hashed_saop_walker(Node *node, void *context)
  *	  value of the Param.
  * 2. Fold stable, as well as immutable, functions to constants.
  * 3. Reduce PlaceHolderVar nodes to their contained expressions.
+ * 4. Current value of session variable can be used for estimation too.
  *--------------------
  */
 Node *
@@ -2494,6 +2503,29 @@ eval_const_expressions_mutator(Node *node,
 						}
 					}
 				}
+				else if (param->paramkind == PARAM_VARIABLE &&
+						 context->estimate)
+				{
+					int16		typLen;
+					bool		typByVal;
+					Datum		pval;
+					bool		isnull;
+
+					get_typlenbyval(param->paramtype,
+									&typLen, &typByVal);
+
+					pval = GetSessionVariableWithTypeCheck(param->paramvarid,
+														   &isnull,
+														   param->paramtype);
+
+					return (Node *) makeConst(param->paramtype,
+											  param->paramtypmod,
+											  param->paramcollid,
+											  (int) typLen,
+											  pval,
+											  isnull,
+											  typByVal);
+				}
 
 				/*
 				 * Not replaceable, so just copy the Param (no need to
@@ -4685,7 +4717,8 @@ inline_function(Oid funcid, Oid result_type, Oid result_collid,
 		querytree->limitOffset ||
 		querytree->limitCount ||
 		querytree->setOperations ||
-		list_length(querytree->targetList) != 1)
+		(list_length(querytree->targetList) != 1) ||
+		querytree->hasSessionVariables)
 		goto fail;
 
 	/* If the function result is composite, resolve it */
diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
index dbdf6bf896..57aee4e27e 100644
--- a/src/backend/parser/analyze.c
+++ b/src/backend/parser/analyze.c
@@ -25,9 +25,12 @@
 #include "postgres.h"
 
 #include "access/sysattr.h"
+#include "catalog/namespace.h"
 #include "catalog/pg_proc.h"
 #include "catalog/pg_type.h"
+#include "catalog/pg_variable.h"
 #include "commands/defrem.h"
+#include "commands/session_variable.h"
 #include "miscadmin.h"
 #include "nodes/makefuncs.h"
 #include "nodes/nodeFuncs.h"
@@ -52,6 +55,7 @@
 #include "utils/backend_status.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
+#include "utils/lsyscache.h"
 #include "utils/rel.h"
 #include "utils/syscache.h"
 
@@ -85,6 +89,8 @@ static Query *transformCreateTableAsStmt(ParseState *pstate,
 										 CreateTableAsStmt *stmt);
 static Query *transformCallStmt(ParseState *pstate,
 								CallStmt *stmt);
+static Query *transformLetStmt(ParseState *pstate,
+							   LetStmt *stmt);
 static void transformLockingClause(ParseState *pstate, Query *qry,
 								   LockingClause *lc, bool pushedDown);
 #ifdef RAW_EXPRESSION_COVERAGE_TEST
@@ -328,6 +334,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:
@@ -406,6 +413,11 @@ transformStmt(ParseState *pstate, Node *parseTree)
 									   (CallStmt *) parseTree);
 			break;
 
+		case T_LetStmt:
+			result = transformLetStmt(pstate,
+									  (LetStmt *) parseTree);
+			break;
+
 		default:
 
 			/*
@@ -457,6 +469,7 @@ stmt_requires_parse_analysis(RawStmt *parseTree)
 		case T_SelectStmt:
 		case T_ReturnStmt:
 		case T_PLAssignStmt:
+		case T_LetStmt:
 			result = true;
 			break;
 
@@ -564,6 +577,7 @@ transformDeleteStmt(ParseState *pstate, DeleteStmt *stmt)
 	qry->hasWindowFuncs = pstate->p_hasWindowFuncs;
 	qry->hasTargetSRFs = pstate->p_hasTargetSRFs;
 	qry->hasAggs = pstate->p_hasAggs;
+	qry->hasSessionVariables = pstate->p_hasSessionVariables;
 
 	assign_query_collations(pstate, qry);
 
@@ -989,6 +1003,7 @@ transformInsertStmt(ParseState *pstate, InsertStmt *stmt)
 
 	qry->hasTargetSRFs = pstate->p_hasTargetSRFs;
 	qry->hasSubLinks = pstate->p_hasSubLinks;
+	qry->hasSessionVariables = pstate->p_hasSessionVariables;
 
 	assign_query_collations(pstate, qry);
 
@@ -1443,6 +1458,7 @@ transformSelectStmt(ParseState *pstate, SelectStmt *stmt)
 	qry->hasWindowFuncs = pstate->p_hasWindowFuncs;
 	qry->hasTargetSRFs = pstate->p_hasTargetSRFs;
 	qry->hasAggs = pstate->p_hasAggs;
+	qry->hasSessionVariables = pstate->p_hasSessionVariables;
 
 	foreach(l, stmt->lockingClause)
 	{
@@ -1669,12 +1685,250 @@ transformValuesClause(ParseState *pstate, SelectStmt *stmt)
 	qry->jointree = makeFromExpr(pstate->p_joinlist, NULL);
 
 	qry->hasSubLinks = pstate->p_hasSubLinks;
+	qry->hasSessionVariables = pstate->p_hasSessionVariables;
 
 	assign_query_collations(pstate, qry);
 
 	return qry;
 }
 
+/*
+ * transformLetStmt -
+ *	  transform an Let Statement
+ */
+static Query *
+transformLetStmt(ParseState *pstate, LetStmt *stmt)
+{
+	Query	   *query;
+	Query	   *result;
+	List	   *exprList = NIL;
+	List	   *exprListCoer = NIL;
+	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;
+	AclResult	aclresult;
+	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);
+
+	/*
+	 * The AccessShareLock is created on related session variable. The lock
+	 * will be kept for the whole transaction.
+	 */
+	varid = IdentifyVariable(names, &attrname, &not_unique, false);
+	if (not_unique)
+		ereport(ERROR,
+				(errcode(ERRCODE_AMBIGUOUS_PARAMETER),
+				 errmsg("target \"%s\" of LET command is ambiguous",
+						NameListToString(names)),
+				 parser_errposition(pstate, stmt->location)));
+
+	if (!OidIsValid(varid))
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("session variable \"%s\" doesn't exist",
+						NameListToString(names)),
+				 parser_errposition(pstate, stmt->location)));
+
+	/*
+	 * Calculate start of possible position of an indirection in list, and
+	 * when it is inside the list, store pointer on first node of indirection.
+	 */
+	indirection_start = list_length(names) - (attrname ? 1 : 0);
+	if (list_length(stmt->target) > indirection_start)
+		indirection_head = list_nth_cell(stmt->target, indirection_start);
+
+	get_session_variable_type_typmod_collid(varid, &typid, &typmod, &collid);
+
+	is_rowtype = type_is_rowtype(typid);
+
+	if (attrname && !is_rowtype)
+		ereport(ERROR,
+				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+				 errmsg("type \"%s\" of target session variable \"%s.%s\" is not a composite type",
+						format_type_be(typid),
+						get_namespace_name(get_session_variable_namespace(varid)),
+						get_session_variable_name(varid)),
+				 parser_errposition(pstate, stmt->location)));
+
+	aclresult = object_aclcheck(VariableRelationId, varid, GetUserId(), ACL_UPDATE);
+	if (aclresult != ACLCHECK_OK)
+		aclcheck_error(aclresult, OBJECT_VARIABLE, NameListToString(names));
+
+	/*
+	 * The behavior of EXPR_KIND_LET_TARGET is almost same like
+	 * EXPR_KIND_UPDATE_TARGET. EXPR_KIND_LET_TARGET was introduced to be
+	 * possible to raise more correct errors, where the command LET is
+	 * mentioned (instead UPDATE command).
+	 */
+	pstate->p_expr_kind = EXPR_KIND_LET_TARGET;
+
+	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(lc, selectQuery->targetList)
+	{
+		TargetEntry *tle = (TargetEntry *) lfirst(lc);
+
+		if (tle->resjunk)
+			continue;
+
+		exprList = lappend(exprList, tle->expr);
+	}
+
+	/* don't allow multicolumn result */
+	if (list_length(exprList) != 1)
+		ereport(ERROR,
+				(errcode(ERRCODE_SYNTAX_ERROR),
+				 errmsg_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;
+		Param	   *param;
+		Oid			exprtypid;
+
+		if (IsA(expr, Const) && ((Const *) expr)->constisnull)
+		{
+			/* use known type for NULL value */
+			expr = (Expr *) makeNullConst(typid, typmod, collid);
+		}
+
+		/* now we can read type of expression */
+		exprtypid = exprType((Node *) expr);
+
+		param = makeNode(Param);
+		param->paramkind = PARAM_VARIABLE;
+		param->paramvarid = varid;
+		param->paramtype = typid;
+		param->paramtypmod = typmod;
+
+		if (indirection_head)
+		{
+			bool		targetIsArray;
+			char	   *targetName;
+
+			targetName = get_session_variable_name(varid);
+			targetIsArray = OidIsValid(get_element_type(typid));
+
+			pstate->p_hasSessionVariables = true;
+
+			coerced_expr = (Expr *)
+				transformAssignmentIndirection(pstate,
+											   (Node *) param,
+											   targetName,
+											   targetIsArray,
+											   typid,
+											   typmod,
+											   InvalidOid,
+											   stmt->target,
+											   indirection_head,
+											   (Node *) expr,
+											   COERCION_PLPGSQL,
+											   stmt->location);
+		}
+		else
+			coerced_expr = (Expr *)
+				coerce_to_target_type(pstate,
+									  (Node *) expr,
+									  exprtypid,
+									  typid, typmod,
+									  COERCION_ASSIGNMENT,
+									  COERCE_IMPLICIT_CAST,
+									  stmt->location);
+
+		if (coerced_expr == NULL)
+			ereport(ERROR,
+					(errcode(ERRCODE_DATATYPE_MISMATCH),
+					 errmsg("variable \"%s.%s\" is of type %s,"
+							" but expression is of type %s",
+							get_namespace_name(get_session_variable_namespace(varid)),
+							get_session_variable_name(varid),
+							format_type_be(typid),
+							format_type_be(exprtypid)),
+					 errhint("You will need to rewrite or cast the expression."),
+					 parser_errposition(pstate, exprLocation((Node *) expr))));
+
+		exprListCoer = lappend(exprListCoer, coerced_expr);
+	}
+
+	/*
+	 * Generate query's target list using the computed list of expressions.
+	 */
+	query = makeNode(Query);
+	query->commandType = CMD_SELECT;
+
+	foreach(lc, exprListCoer)
+	{
+		Expr	   *expr = (Expr *) lfirst(lc);
+		TargetEntry *tle;
+
+		tle = makeTargetEntry(expr,
+							  i + 1,
+							  FigureColname((Node *) expr),
+							  false);
+		query->targetList = lappend(query->targetList, tle);
+	}
+
+	/* done building the range table and jointree */
+	query->rtable = pstate->p_rtable;
+	query->jointree = makeFromExpr(pstate->p_joinlist, NULL);
+
+	query->hasTargetSRFs = pstate->p_hasTargetSRFs;
+	query->hasSubLinks = pstate->p_hasSubLinks;
+	query->hasSessionVariables = pstate->p_hasSessionVariables;
+
+	/* This is top query */
+	query->canSetTag = true;
+
+	/*
+	 * Save target session variable id. This value is used multiple times: by
+	 * query rewriter (for getting related defexpr), by planner (for acquiring
+	 * AccessShareLock on target variable), and by executor (we need to know
+	 * target variable id).
+	 */
+	query->resultVariable = varid;
+
+	assign_query_collations(pstate, query);
+
+	stmt->query = (Node *) query;
+
+	/* represent the command as a utility Query */
+	result = makeNode(Query);
+	result->commandType = CMD_UTILITY;
+	result->utilityStmt = (Node *) stmt;
+
+	return result;
+}
+
 /*
  * transformSetOperationStmt -
  *	  transforms a set-operations tree
@@ -1919,6 +2173,7 @@ transformSetOperationStmt(ParseState *pstate, SelectStmt *stmt)
 	qry->hasWindowFuncs = pstate->p_hasWindowFuncs;
 	qry->hasTargetSRFs = pstate->p_hasTargetSRFs;
 	qry->hasAggs = pstate->p_hasAggs;
+	qry->hasSessionVariables = pstate->p_hasSessionVariables;
 
 	foreach(l, lockingClause)
 	{
@@ -2393,6 +2648,7 @@ transformReturnStmt(ParseState *pstate, ReturnStmt *stmt)
 	qry->hasWindowFuncs = pstate->p_hasWindowFuncs;
 	qry->hasTargetSRFs = pstate->p_hasTargetSRFs;
 	qry->hasAggs = pstate->p_hasAggs;
+	qry->hasSessionVariables = pstate->p_hasSessionVariables;
 
 	assign_query_collations(pstate, qry);
 
@@ -2459,6 +2715,7 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
 
 	qry->hasTargetSRFs = pstate->p_hasTargetSRFs;
 	qry->hasSubLinks = pstate->p_hasSubLinks;
+	qry->hasSessionVariables = pstate->p_hasSessionVariables;
 
 	assign_query_collations(pstate, qry);
 
@@ -2645,9 +2902,15 @@ transformPLAssignStmt(ParseState *pstate, PLAssignStmt *stmt)
 	/*
 	 * Transform the target reference.  Typically we will get back a Param
 	 * node, but there's no reason to be too picky about its type.
+	 *
+	 * The session variables should not be used as target of PL/pgSQL assign
+	 * statement. So we should to use dedicated expression kind, and disallow
+	 * session variables there. The dedicated context allows to eliminate
+	 * false alarm warnings related to possibility of target PL/pgSQL variable
+	 * shadows a session variable.
 	 */
 	target = transformExpr(pstate, (Node *) cref,
-						   EXPR_KIND_UPDATE_TARGET);
+						   EXPR_KIND_ASSIGN_TARGET);
 	targettype = exprType(target);
 	targettypmod = exprTypmod(target);
 	targetcollation = exprCollation(target);
@@ -2689,6 +2952,10 @@ transformPLAssignStmt(ParseState *pstate, PLAssignStmt *stmt)
 	 */
 	type_id = exprType((Node *) tle->expr);
 
+	/*
+	 * For indirection processing and additional casts we can use expr_kind
+	 * EXPR_KIND_UPDATE_TARGET
+	 */
 	pstate->p_expr_kind = EXPR_KIND_UPDATE_TARGET;
 
 	if (indirection)
@@ -2831,6 +3098,8 @@ transformPLAssignStmt(ParseState *pstate, PLAssignStmt *stmt)
 							   (LockingClause *) lfirst(l), false);
 	}
 
+	qry->hasSessionVariables = pstate->p_hasSessionVariables;
+
 	assign_query_collations(pstate, qry);
 
 	/* this must be done after collations, for reliable comparison of exprs */
@@ -3104,6 +3373,14 @@ transformCallStmt(ParseState *pstate, CallStmt *stmt)
 							 true,
 							 stmt->funccall->location);
 
+	/*
+	 * The arguments of CALL statement are evaluated by direct
+	 * expression executor call. This path is unsupported yet,
+	 * so block it. It will be enabled by some other patch.
+	 */
+	if (pstate->p_hasSessionVariables)
+		elog(ERROR, "session variable cannot be used as an argument");
+
 	assign_expr_collations(pstate, node);
 
 	fexpr = castNode(FuncExpr, node);
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 7f3e024b8c..7a26c2c9a5 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -306,7 +306,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
@@ -455,6 +455,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 				TriggerTransitions TriggerReferencing
 				vacuum_relation_list opt_vacuum_relation_list
 				drop_option_list pub_obj_list
+				let_target
 
 %type <node>	opt_routine_body
 %type <groupclause> group_clause
@@ -730,7 +731,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	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 METHOD
@@ -1078,6 +1079,7 @@ stmt:
 			| ImportForeignSchemaStmt
 			| IndexStmt
 			| InsertStmt
+			| LetStmt
 			| ListenStmt
 			| RefreshMatViewStmt
 			| LoadStmt
@@ -12643,6 +12645,47 @@ opt_hold: /* EMPTY */						{ $$ = 0; }
 			| WITHOUT HOLD					{ $$ = 0; }
 		;
 
+/*****************************************************************************
+ *
+ *		QUERY:
+ *				LET STATEMENTS
+ *
+ *****************************************************************************/
+LetStmt:	LET let_target '=' a_expr
+				{
+					LetStmt	   *n = makeNode(LetStmt);
+					SelectStmt *select;
+					ResTarget  *res;
+
+					n->target = $2;
+
+					select = makeNode(SelectStmt);
+					res = makeNode(ResTarget);
+
+					/* Create target list for implicit query */
+					res->name = NULL;
+					res->indirection = NIL;
+					res->val = (Node *) $4;
+					res->location = @4;
+
+					select->targetList = list_make1(res);
+					n->query = (Node *) select;
+
+					n->location = @2;
+					$$ = (Node *) n;
+				}
+		;
+
+let_target:
+			ColId opt_indirection
+				{
+					$$ = list_make1(makeString($1));
+					if ($2)
+						  $$ = list_concat($$,
+										   check_indirection($2, yyscanner));
+				}
+		;
+
 /*****************************************************************************
  *
  *		QUERY:
@@ -17364,6 +17407,7 @@ unreserved_keyword:
 			| LARGE_P
 			| LAST_P
 			| LEAKPROOF
+			| LET
 			| LEVEL
 			| LISTEN
 			| LOAD
@@ -17953,6 +17997,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 7b211a7743..02b6c45682 100644
--- a/src/backend/parser/parse_agg.c
+++ b/src/backend/parser/parse_agg.c
@@ -575,6 +575,11 @@ check_agglevels_and_constraints(ParseState *pstate, Node *expr)
 			errkind = true;
 			break;
 
+		case EXPR_KIND_ASSIGN_TARGET:
+		case EXPR_KIND_LET_TARGET:
+			errkind = true;
+			break;
+
 			/*
 			 * There is intentionally no default: case here, so that the
 			 * compiler will warn if we add a new ParseExprKind without
@@ -964,6 +969,10 @@ transformWindowFuncCall(ParseState *pstate, WindowFunc *wfunc,
 		case EXPR_KIND_CYCLE_MARK:
 			errkind = true;
 			break;
+		case EXPR_KIND_ASSIGN_TARGET:
+		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 9300c7b9ab..c1d2b7bc46 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -18,6 +18,7 @@
 #include "catalog/pg_aggregate.h"
 #include "catalog/pg_proc.h"
 #include "catalog/pg_type.h"
+#include "catalog/pg_variable.h"
 #include "commands/dbcommands.h"
 #include "miscadmin.h"
 #include "nodes/makefuncs.h"
@@ -34,17 +35,18 @@
 #include "parser/parse_relation.h"
 #include "parser/parse_target.h"
 #include "parser/parse_type.h"
+#include "storage/lmgr.h"
 #include "utils/builtins.h"
 #include "utils/date.h"
 #include "utils/fmgroids.h"
 #include "utils/lsyscache.h"
 #include "utils/timestamp.h"
+#include "utils/typcache.h"
 #include "utils/xml.h"
 
 /* GUC parameters */
 bool		Transform_null_equals = false;
 
-
 static Node *transformExprRecurse(ParseState *pstate, Node *expr);
 static Node *transformParamRef(ParseState *pstate, ParamRef *pref);
 static Node *transformAExprOp(ParseState *pstate, A_Expr *a);
@@ -98,6 +100,9 @@ static Expr *make_distinct_op(ParseState *pstate, List *opname,
 							  Node *ltree, Node *rtree, int location);
 static Node *make_nulltest_from_distinct(ParseState *pstate,
 										 A_Expr *distincta, Node *arg);
+static Node *makeParamSessionVariable(ParseState *pstate,
+									  Oid varid, Oid typid, int32 typmod, Oid collid,
+									  char *attrname, int location);
 
 
 /*
@@ -482,6 +487,88 @@ transformIndirection(ParseState *pstate, A_Indirection *ind)
 	return result;
 }
 
+/*
+ * Returns true if the given expression kind is valid for session variables
+ * Session variables can be used everywhere where external parameters can be
+ * used. Session variables are not allowed in DDL commands or in constraints.
+ *
+ * An identifier can be parsed as a session variable only for expression kinds
+ * where session variables are allowed. This is the primary usage of this
+ * function.
+ *
+ * Second usage of this function is to decide whether "column does not exist" or
+ * "column or variable does not exist" error message should be printed.
+ * When we are in an expression where session variables cannot be used, we raise
+ * the first form of error message.
+ */
+static bool
+expr_kind_allows_session_variables(ParseExprKind p_expr_kind)
+{
+	bool		result = false;
+
+	switch (p_expr_kind)
+	{
+		case EXPR_KIND_NONE:
+			Assert(false);		/* can't happen */
+			return false;
+
+			/* allow */
+		case EXPR_KIND_OTHER:
+		case EXPR_KIND_JOIN_ON:
+		case EXPR_KIND_FROM_SUBSELECT:
+		case EXPR_KIND_FROM_FUNCTION:
+		case EXPR_KIND_WHERE:
+		case EXPR_KIND_HAVING:
+		case EXPR_KIND_FILTER:
+		case EXPR_KIND_WINDOW_PARTITION:
+		case EXPR_KIND_WINDOW_ORDER:
+		case EXPR_KIND_WINDOW_FRAME_RANGE:
+		case EXPR_KIND_WINDOW_FRAME_ROWS:
+		case EXPR_KIND_WINDOW_FRAME_GROUPS:
+		case EXPR_KIND_SELECT_TARGET:
+		case EXPR_KIND_INSERT_TARGET:
+		case EXPR_KIND_UPDATE_SOURCE:
+		case EXPR_KIND_UPDATE_TARGET:
+		case EXPR_KIND_MERGE_WHEN:
+		case EXPR_KIND_GROUP_BY:
+		case EXPR_KIND_ORDER_BY:
+		case EXPR_KIND_DISTINCT_ON:
+		case EXPR_KIND_LIMIT:
+		case EXPR_KIND_OFFSET:
+		case EXPR_KIND_RETURNING:
+		case EXPR_KIND_VALUES:
+		case EXPR_KIND_VALUES_SINGLE:
+		case EXPR_KIND_ALTER_COL_TRANSFORM:
+		case EXPR_KIND_EXECUTE_PARAMETER:
+		case EXPR_KIND_POLICY:
+		case EXPR_KIND_CALL_ARGUMENT:
+		case EXPR_KIND_COPY_WHERE:
+		case EXPR_KIND_LET_TARGET:
+			result = true;
+			break;
+
+			/* not allow */
+		case EXPR_KIND_CHECK_CONSTRAINT:
+		case EXPR_KIND_DOMAIN_CHECK:
+		case EXPR_KIND_COLUMN_DEFAULT:
+		case EXPR_KIND_FUNCTION_DEFAULT:
+		case EXPR_KIND_INDEX_EXPRESSION:
+		case EXPR_KIND_INDEX_PREDICATE:
+		case EXPR_KIND_STATS_EXPRESSION:
+		case EXPR_KIND_TRIGGER_WHEN:
+		case EXPR_KIND_PARTITION_BOUND:
+		case EXPR_KIND_PARTITION_EXPRESSION:
+		case EXPR_KIND_GENERATED_COLUMN:
+		case EXPR_KIND_JOIN_USING:
+		case EXPR_KIND_CYCLE_MARK:
+		case EXPR_KIND_ASSIGN_TARGET:
+			result = false;
+			break;
+	}
+
+	return result;
+}
+
 /*
  * Transform a ColumnRef.
  *
@@ -557,6 +644,8 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
 		case EXPR_KIND_COPY_WHERE:
 		case EXPR_KIND_GENERATED_COLUMN:
 		case EXPR_KIND_CYCLE_MARK:
+		case EXPR_KIND_ASSIGN_TARGET:
+		case EXPR_KIND_LET_TARGET:
 			/* okay */
 			break;
 
@@ -828,6 +917,64 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
 					 parser_errposition(pstate, cref->location)));
 	}
 
+	/*
+	 * There are contexts where session's variables are not allowed. The
+	 * question is if we want to identify session's variables in these
+	 * contexts? The code can be more simple, when we don't do it, but then we
+	 * cannot to raise maybe useful message like "you cannot to use session
+	 * variables here".
+	 */
+	if (expr_kind_allows_session_variables(pstate->p_expr_kind))
+	{
+		Oid			varid = InvalidOid;
+		char	   *attrname = NULL;
+		bool		not_unique;
+
+		/*
+		 * Session variables are shadowed by columns, routine's variables or
+		 * routine's arguments. We don't want to use session variable
+		 * when it is not exactly shadowed, but RTE like this is valid:
+		 *
+		 * CREATE TYPE T AS (c int); CREATE VARIABLE foo AS T; CREATE TABLE
+		 * foo(a int, b int);
+		 *
+		 * SELECT foo.a, foo.b, foo.c FROM foo;
+		 *
+		 * This case can be messy and then we disallow it. When we know that a
+		 * possible variable will be shadowed, we don't try to identify
+		 * variable.
+		 */
+		if (!node && !(relname && crerr == CRERR_NO_COLUMN))
+		{
+			/*
+			 * The AccessShareLock is created on related session variable. The
+			 * lock will be kept for the whole transaction.
+			 */
+			varid = IdentifyVariable(cref->fields, &attrname, &not_unique, false);
+
+			if (OidIsValid(varid))
+			{
+				Oid			typid;
+				int32		typmod;
+				Oid			collid;
+
+				if (not_unique)
+					ereport(ERROR,
+							(errcode(ERRCODE_AMBIGUOUS_PARAMETER),
+							 errmsg("session variable reference \"%s\" is ambiguous",
+									NameListToString(cref->fields)),
+							 parser_errposition(pstate, cref->location)));
+
+				get_session_variable_type_typmod_collid(varid, &typid, &typmod,
+														&collid);
+
+				node = makeParamSessionVariable(pstate,
+												varid, typid, typmod, collid,
+												attrname, cref->location);
+			}
+		}
+	}
+
 	/*
 	 * Throw error if no translation found.
 	 */
@@ -862,6 +1009,72 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
 	return node;
 }
 
+/*
+ * Generate param variable for reference to session variable
+ */
+static Node *
+makeParamSessionVariable(ParseState *pstate,
+						 Oid varid, Oid typid, int32 typmod, Oid collid,
+						 char *attrname, int location)
+{
+	Param	   *param;
+
+	param = makeNode(Param);
+
+	param->paramkind = PARAM_VARIABLE;
+	param->paramvarid = varid;
+	param->paramtype = typid;
+	param->paramtypmod = typmod;
+	param->paramcollid = collid;
+
+	pstate->p_hasSessionVariables = true;
+
+	if (attrname != NULL)
+	{
+		TupleDesc	tupdesc;
+		int			i;
+
+		tupdesc = lookup_rowtype_tupdesc_noerror(typid, typmod, true);
+		if (!tupdesc)
+			ereport(ERROR,
+					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+					 errmsg("variable \"%s.%s\" is of type \"%s\", which is not a composite type",
+							get_namespace_name(get_session_variable_namespace(varid)),
+							get_session_variable_name(varid),
+							format_type_be(typid)),
+					 parser_errposition(pstate, location)));
+
+		for (i = 0; i < tupdesc->natts; i++)
+		{
+			Form_pg_attribute att = TupleDescAttr(tupdesc, i);
+
+			if (strcmp(attrname, NameStr(att->attname)) == 0 &&
+				!att->attisdropped)
+			{
+				/* Success, so generate a FieldSelect expression */
+				FieldSelect *fselect = makeNode(FieldSelect);
+
+				fselect->arg = (Expr *) param;
+				fselect->fieldnum = i + 1;
+				fselect->resulttype = att->atttypid;
+				fselect->resulttypmod = att->atttypmod;
+				/* save attribute's collation for parse_collate.c */
+				fselect->resultcollid = att->attcollation;
+
+				ReleaseTupleDesc(tupdesc);
+				return (Node *) fselect;
+			}
+		}
+
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_COLUMN),
+				 errmsg("could not identify column \"%s\" in variable", attrname),
+				 parser_errposition(pstate, location)));
+	}
+
+	return (Node *) param;
+}
+
 static Node *
 transformParamRef(ParseState *pstate, ParamRef *pref)
 {
@@ -1770,6 +1983,7 @@ transformSubLink(ParseState *pstate, SubLink *sublink)
 		case EXPR_KIND_VALUES:
 		case EXPR_KIND_VALUES_SINGLE:
 		case EXPR_KIND_CYCLE_MARK:
+		case EXPR_KIND_LET_TARGET:
 			/* okay */
 			break;
 		case EXPR_KIND_CHECK_CONSTRAINT:
@@ -1813,6 +2027,9 @@ transformSubLink(ParseState *pstate, SubLink *sublink)
 		case EXPR_KIND_GENERATED_COLUMN:
 			err = _("cannot use subquery in column generation expression");
 			break;
+		case EXPR_KIND_ASSIGN_TARGET:
+			err = _("cannot use subquery as target of assign statement");
+			break;
 
 			/*
 			 * There is intentionally no default: case here, so that the
@@ -3149,6 +3366,10 @@ ParseExprKindName(ParseExprKind exprKind)
 			return "GENERATED AS";
 		case EXPR_KIND_CYCLE_MARK:
 			return "CYCLE";
+		case EXPR_KIND_ASSIGN_TARGET:
+			return "ASSIGN";
+		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 fdb3e6df33..8d1c382a06 100644
--- a/src/backend/parser/parse_func.c
+++ b/src/backend/parser/parse_func.c
@@ -2654,6 +2654,8 @@ check_srf_call_placement(ParseState *pstate, Node *last_srf, int location)
 			err = _("set-returning functions are not allowed in column generation expressions");
 			break;
 		case EXPR_KIND_CYCLE_MARK:
+		case EXPR_KIND_ASSIGN_TARGET:
+		case EXPR_KIND_LET_TARGET:
 			errkind = true;
 			break;
 
diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
index f60b34deb6..4ff57c8821 100644
--- a/src/backend/rewrite/rewriteHandler.c
+++ b/src/backend/rewrite/rewriteHandler.c
@@ -26,6 +26,7 @@
 #include "catalog/dependency.h"
 #include "catalog/partition.h"
 #include "catalog/pg_type.h"
+#include "catalog/pg_variable.h"
 #include "commands/trigger.h"
 #include "executor/executor.h"
 #include "foreign/fdwapi.h"
diff --git a/src/backend/tcop/dest.c b/src/backend/tcop/dest.c
index 810b25f418..8c8bc137dd 100644
--- a/src/backend/tcop/dest.c
+++ b/src/backend/tcop/dest.c
@@ -37,6 +37,7 @@
 #include "executor/functions.h"
 #include "executor/tqueue.h"
 #include "executor/tstoreReceiver.h"
+#include "executor/svariableReceiver.h"
 #include "libpq/libpq.h"
 #include "libpq/pqformat.h"
 #include "utils/portal.h"
@@ -152,6 +153,9 @@ CreateDestReceiver(CommandDest dest)
 
 		case DestTupleQueue:
 			return CreateTupleQueueDestReceiver(NULL);
+
+		case DestVariable:
+			return CreateVariableDestReceiver(InvalidOid);
 	}
 
 	/* should never get here */
@@ -187,6 +191,7 @@ EndCommand(const QueryCompletion *qc, CommandDest dest, bool force_undecorated_o
 		case DestSQLFunction:
 		case DestTransientRel:
 		case DestTupleQueue:
+		case DestVariable:
 			break;
 	}
 }
@@ -232,6 +237,7 @@ NullCommand(CommandDest dest)
 		case DestSQLFunction:
 		case DestTransientRel:
 		case DestTupleQueue:
+		case DestVariable:
 			break;
 	}
 }
@@ -275,6 +281,7 @@ ReadyForQuery(CommandDest dest)
 		case DestSQLFunction:
 		case DestTransientRel:
 		case DestTupleQueue:
+		case DestVariable:
 			break;
 	}
 }
diff --git a/src/backend/tcop/pquery.c b/src/backend/tcop/pquery.c
index 0c45fcf318..85b809cb07 100644
--- a/src/backend/tcop/pquery.c
+++ b/src/backend/tcop/pquery.c
@@ -86,6 +86,9 @@ CreateQueryDesc(PlannedStmt *plannedstmt,
 	qd->queryEnv = queryEnv;
 	qd->instrument_options = instrument_options;	/* instrumentation wanted? */
 
+	qd->num_session_variables = 0;
+	qd->session_variables = NULL;
+
 	/* null these fields until set by ExecutorStart */
 	qd->tupDesc = NULL;
 	qd->estate = NULL;
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 91fb74104f..8b8a995d65 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -52,6 +52,7 @@
 #include "commands/schemacmds.h"
 #include "commands/seclabel.h"
 #include "commands/sequence.h"
+#include "commands/session_variable.h"
 #include "commands/subscriptioncmds.h"
 #include "commands/tablecmds.h"
 #include "commands/tablespace.h"
@@ -242,6 +243,7 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree)
 
 		case T_CallStmt:
 		case T_DoStmt:
+		case T_LetStmt:
 			{
 				/*
 				 * Commands inside the DO block or the called procedure might
@@ -1071,6 +1073,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,
@@ -2211,6 +2218,10 @@ UtilityContainsQuery(Node *parsetree)
 				return UtilityContainsQuery(qry->utilityStmt);
 			return qry;
 
+		case T_LetStmt:
+			qry = castNode(Query, ((LetStmt *) parsetree)->query);
+			return qry;
+
 		default:
 			return NULL;
 	}
@@ -2409,6 +2420,10 @@ CreateCommandTag(Node *parsetree)
 			tag = CMDTAG_SELECT;
 			break;
 
+		case T_LetStmt:
+			tag = CMDTAG_LET;
+			break;
+
 			/* utility statements --- same whether raw or cooked */
 		case T_TransactionStmt:
 			{
@@ -3294,6 +3309,7 @@ GetCommandLogLevel(Node *parsetree)
 			break;
 
 		case T_PLAssignStmt:
+		case T_LetStmt:
 			lev = LOGSTMT_ALL;
 			break;
 
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index b625f471a8..29bf08bb81 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -38,6 +38,7 @@
 #include "catalog/pg_statistic_ext.h"
 #include "catalog/pg_trigger.h"
 #include "catalog/pg_type.h"
+#include "catalog/pg_variable.h"
 #include "commands/defrem.h"
 #include "commands/tablespace.h"
 #include "common/keywords.h"
@@ -515,6 +516,7 @@ static char *generate_function_name(Oid funcid, int nargs,
 static char *generate_operator_name(Oid operid, Oid arg1, Oid arg2);
 static void add_cast_to(StringInfo buf, Oid typid);
 static char *generate_qualified_type_name(Oid typid);
+static char *generate_session_variable_name(Oid varid);
 static text *string_to_text(char *str);
 static char *flatten_reloptions(Oid relid);
 static void get_reloptions(StringInfo buf, Datum reloptions);
@@ -8187,6 +8189,14 @@ get_parameter(Param *param, deparse_context *context)
 		return;
 	}
 
+	/* translate paramvarid to session variable name */
+	if (param->paramkind == PARAM_VARIABLE)
+	{
+		appendStringInfo(context->buf, "%s",
+						 generate_session_variable_name(param->paramvarid));
+		return;
+	}
+
 	/*
 	 * If it's an external parameter, see if the outermost namespace provides
 	 * function argument names.
@@ -12512,6 +12522,42 @@ generate_collation_name(Oid collid)
 	return result;
 }
 
+/*
+ * generate_session_variable_name
+ *		Compute the name to display for a session variable specified by OID
+ *
+ * The result includes all necessary quoting and schema-prefixing.
+ */
+static char *
+generate_session_variable_name(Oid varid)
+{
+	HeapTuple	tup;
+	Form_pg_variable varform;
+	char	   *varname;
+	char	   *nspname;
+	char	   *result;
+
+	tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid));
+
+	if (!HeapTupleIsValid(tup))
+		elog(ERROR, "cache lookup failed for variable %u", varid);
+
+	varform = (Form_pg_variable) GETSTRUCT(tup);
+
+	varname = NameStr(varform->varname);
+
+	if (!VariableIsVisible(varid))
+		nspname = get_namespace_name_or_temp(varform->varnamespace);
+	else
+		nspname = NULL;
+
+	result = quote_qualified_identifier(nspname, varname);
+
+	ReleaseSysCache(tup);
+
+	return result;
+}
+
 /*
  * Given a C string, produce a TEXT datum.
  *
diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c
index 5194cbf2cc..54a7a376e8 100644
--- a/src/backend/utils/cache/plancache.c
+++ b/src/backend/utils/cache/plancache.c
@@ -58,6 +58,7 @@
 
 #include "access/transam.h"
 #include "catalog/namespace.h"
+#include "catalog/pg_variable.h"
 #include "executor/executor.h"
 #include "miscadmin.h"
 #include "nodes/nodeFuncs.h"
@@ -163,6 +164,7 @@ InitPlanCache(void)
 	CacheRegisterSyscacheCallback(AMOPOPID, PlanCacheSysCallback, (Datum) 0);
 	CacheRegisterSyscacheCallback(FOREIGNSERVEROID, PlanCacheSysCallback, (Datum) 0);
 	CacheRegisterSyscacheCallback(FOREIGNDATAWRAPPEROID, PlanCacheSysCallback, (Datum) 0);
+	CacheRegisterSyscacheCallback(VARIABLEOID, PlanCacheObjectCallback, (Datum) 0);
 }
 
 /*
@@ -1905,18 +1907,33 @@ ScanQueryForLocks(Query *parsetree, bool acquire)
 
 	/*
 	 * Recurse into sublink subqueries, too.  But we already did the ones in
-	 * the rtable and cteList.
+	 * the rtable and cteList. We need to force recursive call for session
+	 * variables too, to find and lock variables used in query (see
+	 * ScanQueryWalker).
 	 */
-	if (parsetree->hasSubLinks)
+	if (parsetree->hasSubLinks ||
+		parsetree->hasSessionVariables)
 	{
 		query_tree_walker(parsetree, ScanQueryWalker,
 						  (void *) &acquire,
 						  QTW_IGNORE_RC_SUBQUERIES);
 	}
+
+	/* Process session variables */
+	if (OidIsValid(parsetree->resultVariable))
+	{
+		if (acquire)
+			LockDatabaseObject(VariableRelationId, parsetree->resultVariable,
+							   0, AccessShareLock);
+		else
+			UnlockDatabaseObject(VariableRelationId, parsetree->resultVariable,
+								 0, AccessShareLock);
+	}
 }
 
 /*
- * Walker to find sublink subqueries for ScanQueryForLocks
+ * Walker to find sublink subqueries or referenced session variables
+ * for ScanQueryForLocks
  */
 static bool
 ScanQueryWalker(Node *node, bool *acquire)
@@ -1931,6 +1948,20 @@ ScanQueryWalker(Node *node, bool *acquire)
 		ScanQueryForLocks(castNode(Query, sub->subselect), *acquire);
 		/* Fall through to process lefthand args of SubLink */
 	}
+	else if (IsA(node, Param))
+	{
+		Param	   *p = (Param *) node;
+
+		if (p->paramkind == PARAM_VARIABLE)
+		{
+			if (acquire)
+				LockDatabaseObject(VariableRelationId, p->paramvarid,
+								   0, AccessShareLock);
+			else
+				UnlockDatabaseObject(VariableRelationId, p->paramvarid,
+									 0, AccessShareLock);
+		}
+	}
 
 	/*
 	 * Do NOT recurse into Query nodes, because ScanQueryForLocks already
@@ -2062,7 +2093,9 @@ PlanCacheRelCallback(Datum arg, Oid relid)
 
 /*
  * PlanCacheObjectCallback
- *		Syscache inval callback function for PROCOID and TYPEOID caches
+ *		Syscache inval callback function for TYPEOID, PROCOID, NAMESPACEOID,
+ * OPEROID, AMOPOPID, FOREIGNSERVEROID, FOREIGNDATAWRAPPEROID and VARIABLEOID
+ * caches.
  *
  * Invalidate all plans mentioning the object with the specified hash value,
  * or all plans mentioning any member of this cache if hashvalue == 0.
diff --git a/src/backend/utils/fmgr/fmgr.c b/src/backend/utils/fmgr/fmgr.c
index e48a86be54..eaae0c5a93 100644
--- a/src/backend/utils/fmgr/fmgr.c
+++ b/src/backend/utils/fmgr/fmgr.c
@@ -2026,9 +2026,13 @@ get_call_expr_arg_stable(Node *expr, int argnum)
 	 */
 	if (IsA(arg, Const))
 		return true;
-	if (IsA(arg, Param) &&
-		((Param *) arg)->paramkind == PARAM_EXTERN)
-		return true;
+	if (IsA(arg, Param))
+	{
+		Param	   *p = (Param *) arg;
+
+		if (p->paramkind == PARAM_EXTERN || p->paramkind == PARAM_VARIABLE)
+			return true;
+	}
 
 	return false;
 }
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index f9e8858fb8..2cf14c7a25 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1708,8 +1708,8 @@ psql_completion(const char *text, int start, int end)
 		"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",
@@ -4250,6 +4250,14 @@ psql_completion(const char *text, int start, int end)
 	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/catalog/namespace.h b/src/include/catalog/namespace.h
index ca29867077..723e646a9b 100644
--- a/src/include/catalog/namespace.h
+++ b/src/include/catalog/namespace.h
@@ -170,7 +170,9 @@ extern SearchPathMatcher *GetSearchPathMatcher(MemoryContext context);
 extern SearchPathMatcher *CopySearchPathMatcher(SearchPathMatcher *path);
 extern bool SearchPathMatchesCurrentEnvironment(SearchPathMatcher *path);
 
+extern List *NamesFromList(List *names);
 extern Oid	LookupVariable(const char *nspname, const char *varname, bool missing_ok);
+extern Oid	IdentifyVariable(List *names, char **attrname, bool *not_unique, bool noerror);
 
 extern Oid	get_collation_oid(List *collname, bool missing_ok);
 extern Oid	get_conversion_oid(List *conname, bool missing_ok);
diff --git a/src/include/catalog/pg_variable.h b/src/include/catalog/pg_variable.h
index 6163ca34dd..f9e61c2209 100644
--- a/src/include/catalog/pg_variable.h
+++ b/src/include/catalog/pg_variable.h
@@ -26,6 +26,10 @@
 /* ----------------
  *		pg_variable definition.  cpp turns this into
  *		typedef struct FormData_pg_variable
+ *
+ * The column varcreate_lsn of XlogRecPtr type (8-byte) should be on position
+ * divisible by 8 unconditionally and before varname column of NameData type.
+ * see sanity_check:check_columns
  * ----------------
  */
 CATALOG(pg_variable,9222,VariableRelationId)
@@ -35,6 +39,9 @@ CATALOG(pg_variable,9222,VariableRelationId)
 	/* OID of entry in pg_type for variable's type */
 	Oid			vartype BKI_LOOKUP(pg_type);
 
+	/* used for identity check [oid, create_lsn] */
+	XLogRecPtr	varcreate_lsn;
+
 	/* variable name */
 	NameData	varname;
 
diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h
new file mode 100644
index 0000000000..b3f03c6582
--- /dev/null
+++ b/src/include/commands/session_variable.h
@@ -0,0 +1,32 @@
+/*-------------------------------------------------------------------------
+ *
+ * sessionvariable.h
+ *	  prototypes for sessionvariable.c.
+ *
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/commands/session_variable.h
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef SESSIONVARIABLE_H
+#define SESSIONVARIABLE_H
+
+#include "nodes/params.h"
+#include "nodes/parsenodes.h"
+#include "parser/parse_node.h"
+#include "tcop/cmdtag.h"
+#include "utils/queryenvironment.h"
+
+extern void SetSessionVariable(Oid varid, Datum value, bool isNull);
+extern void SetSessionVariableWithSecurityCheck(Oid varid, Datum value, bool isNull);
+extern Datum GetSessionVariable(Oid varid, bool *isNull, Oid *typid);
+extern Datum GetSessionVariableWithTypeCheck(Oid varid, bool *isNull, Oid expected_typid);
+
+extern void ExecuteLetStmt(ParseState *pstate, LetStmt *stmt, ParamListInfo params,
+						   QueryEnvironment *queryEnv, QueryCompletion *qc);
+
+#endif
diff --git a/src/include/executor/execdesc.h b/src/include/executor/execdesc.h
index 0a7274e26c..d1b2f59e0c 100644
--- a/src/include/executor/execdesc.h
+++ b/src/include/executor/execdesc.h
@@ -48,6 +48,10 @@ typedef struct QueryDesc
 	EState	   *estate;			/* executor's query-wide state */
 	PlanState  *planstate;		/* tree of per-plan-node state */
 
+	/* reference to session variables buffer */
+	int			num_session_variables;
+	SessionVariableValue *session_variables;
+
 	/* This field is set by ExecutorRun */
 	bool		already_executed;	/* true if previously executed */
 
diff --git a/src/include/executor/svariableReceiver.h b/src/include/executor/svariableReceiver.h
new file mode 100644
index 0000000000..db44d8b94c
--- /dev/null
+++ b/src/include/executor/svariableReceiver.h
@@ -0,0 +1,22 @@
+/*-------------------------------------------------------------------------
+ *
+ * svariableReceiver.h
+ *	  prototypes for svariableReceiver.c
+ *
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/executor/svariableReceiver.h
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef SVARIABLE_RECEIVER_H
+#define SVARIABLE_RECEIVER_H
+
+#include "tcop/dest.h"
+
+extern DestReceiver *CreateVariableDestReceiver(Oid varid);
+
+#endif							/* SVARIABLE_RECEIVER_H */
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 444a5f0fd5..8286ca2b64 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -607,6 +607,18 @@ typedef struct AsyncRequest
 								 * tuples) */
 } AsyncRequest;
 
+/* ----------------
+ * SessionVariableValue
+ * ----------------
+ */
+typedef struct SessionVariableValue
+{
+	Oid			varid;
+	Oid			typid;
+	bool		isnull;
+	Datum		value;
+} SessionVariableValue;
+
 /* ----------------
  *	  EState information
  *
@@ -659,6 +671,13 @@ typedef struct EState
 	ParamListInfo es_param_list_info;	/* values of external params */
 	ParamExecData *es_param_exec_vals;	/* values of internal params */
 
+	/* Variables info: */
+	/* number of used session variables */
+	int			es_num_session_variables;
+
+	/* array of copied values of session variables */
+	SessionVariableValue *es_session_variables;
+
 	QueryEnvironment *es_queryEnv;	/* query environment */
 
 	/* Other working state: */
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 9cbdda7311..130b64fa07 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -141,6 +141,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 */
@@ -159,6 +162,8 @@ typedef struct Query
 	bool		hasForUpdate pg_node_attr(query_jumble_ignore);
 	/* rewriter has applied some RLS policy */
 	bool		hasRowSecurity pg_node_attr(query_jumble_ignore);
+	/* uses session variables */
+	bool		hasSessionVariables pg_node_attr(query_jumble_ignore);
 	/* is a RETURN statement */
 	bool		isReturn pg_node_attr(query_jumble_ignore);
 
@@ -1930,6 +1935,18 @@ typedef struct MergeStmt
 	WithClause *withClause;		/* WITH clause */
 } MergeStmt;
 
+/* ----------------------
+ *		Let Statement
+ * ----------------------
+ */
+typedef struct LetStmt
+{
+	NodeTag		type;
+	List	   *target;			/* target variable */
+	Node	   *query;			/* source expression */
+	int			location;
+} LetStmt;
+
 /* ----------------------
  *		Select Statement
  *
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index 534692bee1..a081fd21c6 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -160,6 +160,9 @@ typedef struct PlannerGlobal
 
 	/* partition descriptors */
 	PartitionDirectory partition_directory pg_node_attr(read_write_ignore);
+
+	/* list of used session variables */
+	List	   *sessionVariables;
 } PlannerGlobal;
 
 /* macro for fetching the Plan associated with a SubPlan node */
@@ -499,6 +502,8 @@ struct PlannerInfo
 	bool		placeholdersFrozen;
 	/* true if planning a recursive WITH item */
 	bool		hasRecursion;
+	/* true if session variables were used */
+	bool		hasSessionVariables;
 
 	/*
 	 * Information about aggregates. Filled by preprocess_aggrefs().
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index b4ef6bc44c..fab0923c1a 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -49,7 +49,7 @@ typedef struct PlannedStmt
 
 	NodeTag		type;
 
-	CmdType		commandType;	/* select|insert|update|delete|merge|utility */
+	CmdType		commandType;	/* select|let|insert|update|delete|merge|utility */
 
 	uint64		queryId;		/* query identifier (copied from Query) */
 
@@ -94,6 +94,8 @@ typedef struct PlannedStmt
 
 	Node	   *utilityStmt;	/* non-null if this is utility stmt */
 
+	List	   *sessionVariables;	/* OIDs for PARAM_VARIABLE Params */
+
 	/* statement location in source string (copied from Query) */
 	int			stmt_location;	/* start location, or -1 if unknown */
 	int			stmt_len;		/* length in bytes; 0 means "rest of string" */
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index 4a154606d2..788e3cb93f 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -51,7 +51,9 @@ typedef struct Alias
 	List	   *colnames;		/* optional list of column aliases */
 } Alias;
 
-/* What to do at commit time for temporary relations */
+/*
+ * What to do at commit time for temporary relations or session variables.
+ */
 typedef enum OnCommitAction
 {
 	ONCOMMIT_NOOP,				/* No ON COMMIT clause (do nothing) */
@@ -347,6 +349,9 @@ typedef struct Const
  *				of the `paramid' field contain the SubLink's subLinkId, and
  *				the low-order 16 bits contain the column number.  (This type
  *				of Param is also converted to PARAM_EXEC during planning.)
+ *
+ *		PARAM_VARIABLE:  The parameter is an access to session variable
+ *				paramid holds varid.
  */
 typedef enum ParamKind
 {
@@ -354,6 +359,7 @@ typedef enum ParamKind
 	PARAM_EXEC,
 	PARAM_SUBLINK,
 	PARAM_MULTIEXPR,
+	PARAM_VARIABLE,
 } ParamKind;
 
 typedef struct Param
@@ -366,6 +372,8 @@ typedef struct Param
 	int32		paramtypmod pg_node_attr(query_jumble_ignore);
 	/* OID of collation, or InvalidOid if none */
 	Oid			paramcollid pg_node_attr(query_jumble_ignore);
+	/* OID of session variable if it is used */
+	Oid			paramvarid;
 	/* token location, or -1 if unknown */
 	int			location;
 } Param;
diff --git a/src/include/optimizer/planmain.h b/src/include/optimizer/planmain.h
index f2e3fa4c2e..690ee67306 100644
--- a/src/include/optimizer/planmain.h
+++ b/src/include/optimizer/planmain.h
@@ -126,4 +126,6 @@ extern void record_plan_function_dependency(PlannerInfo *root, Oid funcid);
 extern void record_plan_type_dependency(PlannerInfo *root, Oid typid);
 extern bool extract_query_dependencies_walker(Node *node, PlannerInfo *context);
 
+extern void pull_up_has_session_variables(PlannerInfo *root);
+
 #endif							/* PLANMAIN_H */
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index 304cb999b3..fd846cf0f9 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -248,6 +248,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 99d6515736..4df2ee9771 100644
--- a/src/include/parser/parse_node.h
+++ b/src/include/parser/parse_node.h
@@ -81,6 +81,8 @@ typedef enum ParseExprKind
 	EXPR_KIND_COPY_WHERE,		/* WHERE condition in COPY FROM */
 	EXPR_KIND_GENERATED_COLUMN, /* generation expression for a column */
 	EXPR_KIND_CYCLE_MARK,		/* cycle mark value */
+	EXPR_KIND_ASSIGN_TARGET,	/* PL/pgSQL assignment target */
+	EXPR_KIND_LET_TARGET,		/* LET target */
 } ParseExprKind;
 
 
@@ -224,6 +226,7 @@ struct ParseState
 	bool		p_hasTargetSRFs;
 	bool		p_hasSubLinks;
 	bool		p_hasModifyingCTE;
+	bool		p_hasSessionVariables;
 
 	Node	   *p_last_srf;		/* most recent set-returning func/op found */
 
diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h
index 9465df7b2f..a921af2486 100644
--- a/src/include/tcop/cmdtaglist.h
+++ b/src/include/tcop/cmdtaglist.h
@@ -186,6 +186,7 @@ PG_CMDTAG(CMDTAG_GRANT, "GRANT", true, false, false)
 PG_CMDTAG(CMDTAG_GRANT_ROLE, "GRANT ROLE", false, false, false)
 PG_CMDTAG(CMDTAG_IMPORT_FOREIGN_SCHEMA, "IMPORT FOREIGN SCHEMA", true, false, false)
 PG_CMDTAG(CMDTAG_INSERT, "INSERT", false, false, true)
+PG_CMDTAG(CMDTAG_LET, "LET", false, false, false)
 PG_CMDTAG(CMDTAG_LISTEN, "LISTEN", false, false, false)
 PG_CMDTAG(CMDTAG_LOAD, "LOAD", false, false, false)
 PG_CMDTAG(CMDTAG_LOCK_TABLE, "LOCK TABLE", false, false, false)
diff --git a/src/include/tcop/dest.h b/src/include/tcop/dest.h
index 7e613bd7fc..99395f53dc 100644
--- a/src/include/tcop/dest.h
+++ b/src/include/tcop/dest.h
@@ -96,6 +96,7 @@ typedef enum
 	DestSQLFunction,			/* results sent to SQL-language func mgr */
 	DestTransientRel,			/* results sent to transient relation */
 	DestTupleQueue,				/* results sent to tuple queue */
+	DestVariable,				/* results sents to session variable */
 } CommandDest;
 
 /* ----------------
diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
index 6d1691340c..ca57ae0ae8 100644
--- a/src/pl/plpgsql/src/pl_exec.c
+++ b/src/pl/plpgsql/src/pl_exec.c
@@ -8035,7 +8035,8 @@ exec_simple_check_plan(PLpgSQL_execstate *estate, PLpgSQL_expr *expr)
 		query->sortClause ||
 		query->limitOffset ||
 		query->limitCount ||
-		query->setOperations)
+		query->setOperations ||
+		query->hasSessionVariables)
 		return;
 
 	/*
diff --git a/src/test/regress/expected/session_variables.out b/src/test/regress/expected/session_variables.out
index aca766042d..e0d5ac12ab 100644
--- a/src/test/regress/expected/session_variables.out
+++ b/src/test/regress/expected/session_variables.out
@@ -55,3 +55,890 @@ SET ROLE TO DEFAULT;
 DROP VARIABLE svartest.var1;
 DROP SCHEMA svartest;
 DROP ROLE regress_variable_owner;
+-- check access rights
+CREATE ROLE regress_noowner;
+CREATE VARIABLE var1 AS int;
+CREATE OR REPLACE FUNCTION sqlfx(int)
+RETURNS int AS $$ SELECT $1 + var1 $$ LANGUAGE sql;
+CREATE OR REPLACE FUNCTION sqlfx_sd(int)
+RETURNS int AS $$ SELECT $1 + var1 $$ LANGUAGE sql SECURITY DEFINER;
+CREATE OR REPLACE FUNCTION plpgsqlfx(int)
+RETURNS int AS $$ BEGIN RETURN $1 + var1; END $$ LANGUAGE plpgsql;
+CREATE OR REPLACE FUNCTION plpgsqlfx_sd(int)
+RETURNS int AS $$ BEGIN RETURN $1 + var1; END $$ LANGUAGE plpgsql SECURITY DEFINER;
+LET var1 = 10;
+-- should be ok
+SELECT var1;
+ var1 
+------
+   10
+(1 row)
+
+SELECT sqlfx(20);
+ sqlfx 
+-------
+    30
+(1 row)
+
+SELECT sqlfx_sd(20);
+ sqlfx_sd 
+----------
+       30
+(1 row)
+
+SELECT plpgsqlfx(20);
+ plpgsqlfx 
+-----------
+        30
+(1 row)
+
+SELECT plpgsqlfx_sd(20);
+ plpgsqlfx_sd 
+--------------
+           30
+(1 row)
+
+-- should to fail
+SET ROLE TO regress_noowner;
+SELECT var1;
+ERROR:  permission denied for session variable var1
+SELECT sqlfx(20);
+ERROR:  permission denied for session variable var1
+CONTEXT:  SQL function "sqlfx" statement 1
+SELECT plpgsqlfx(20);
+ERROR:  permission denied for session variable var1
+CONTEXT:  SQL expression "$1 + var1"
+PL/pgSQL function plpgsqlfx(integer) line 1 at RETURN
+-- should be ok
+SELECT sqlfx_sd(20);
+ sqlfx_sd 
+----------
+       30
+(1 row)
+
+SELECT plpgsqlfx_sd(20);
+ plpgsqlfx_sd 
+--------------
+           30
+(1 row)
+
+SET ROLE TO DEFAULT;
+GRANT SELECT ON VARIABLE var1 TO regress_noowner;
+-- should be ok
+SET ROLE TO regress_noowner;
+SELECT var1;
+ var1 
+------
+   10
+(1 row)
+
+SELECT sqlfx(20);
+ sqlfx 
+-------
+    30
+(1 row)
+
+SELECT plpgsqlfx(20);
+ plpgsqlfx 
+-----------
+        30
+(1 row)
+
+SET ROLE TO DEFAULT;
+DROP VARIABLE var1;
+DROP FUNCTION sqlfx(int);
+DROP FUNCTION plpgsqlfx(int);
+DROP FUNCTION sqlfx_sd(int);
+DROP FUNCTION plpgsqlfx_sd(int);
+DROP ROLE regress_noowner;
+-- use variables inside views
+CREATE VARIABLE var1 AS numeric;
+-- use variables in views
+CREATE VIEW test_view AS SELECT COALESCE(var1 + v, 0) AS result FROM generate_series(1,2) g(v);
+SELECT * FROM test_view;
+ result 
+--------
+      0
+      0
+(2 rows)
+
+LET var1 = 3.14;
+SELECT * FROM test_view;
+ result 
+--------
+   4.14
+   5.14
+(2 rows)
+
+-- start a new session
+\c
+SELECT * FROM test_view;
+ result 
+--------
+      0
+      0
+(2 rows)
+
+LET var1 = 3.14;
+SELECT * FROM test_view;
+ result 
+--------
+   4.14
+   5.14
+(2 rows)
+
+-- should fail, dependency
+DROP VARIABLE var1;
+ERROR:  cannot drop session variable var1 because other objects depend on it
+DETAIL:  view test_view depends on session variable var1
+HINT:  Use DROP ... CASCADE to drop the dependent objects too.
+-- should be ok
+DROP VARIABLE var1 CASCADE;
+NOTICE:  drop cascades to view test_view
+CREATE VARIABLE var1 text;
+CREATE VARIABLE var2 text;
+-- use variables in SQL functions
+CREATE OR REPLACE FUNCTION sqlfx1(varchar)
+RETURNS varchar AS $$ SELECT var1 || ', ' || $1 $$ LANGUAGE sql;
+CREATE OR REPLACE FUNCTION sqlfx2( varchar)
+RETURNS varchar AS $$ SELECT var2 || ', ' || $1 $$ LANGUAGE sql;
+LET var1 = 'str1';
+LET var2 = 'str2';
+SELECT sqlfx1(sqlfx2('Hello'));
+      sqlfx1       
+-------------------
+ str1, str2, Hello
+(1 row)
+
+-- inlining is blocked
+EXPLAIN (COSTS OFF, VERBOSE) SELECT sqlfx1(sqlfx2('Hello'));
+                      QUERY PLAN                      
+------------------------------------------------------
+ Result
+   Output: sqlfx1(sqlfx2('Hello'::character varying))
+(2 rows)
+
+DROP FUNCTION sqlfx1(varchar);
+DROP FUNCTION sqlfx2(varchar);
+DROP VARIABLE var1;
+DROP VARIABLE var2;
+-- access from cached plans should to work
+CREATE VARIABLE var1 AS numeric;
+CREATE OR REPLACE FUNCTION plpgsqlfx()
+RETURNS numeric AS $$ BEGIN RETURN var1; END $$ LANGUAGE plpgsql;
+set plan_cache_mode TO force_generic_plan;
+LET var1 = 3.14;
+SELECT plpgsqlfx();
+ plpgsqlfx 
+-----------
+      3.14
+(1 row)
+
+LET var1 = 3.14 * 2;
+SELECT plpgsqlfx();
+ plpgsqlfx 
+-----------
+      6.28
+(1 row)
+
+DROP VARIABLE var1;
+-- dependency (plan invalidation) should to work
+CREATE VARIABLE var1 AS numeric;
+LET var1 = 3.14 * 3;
+SELECT plpgsqlfx();
+ plpgsqlfx 
+-----------
+      9.42
+(1 row)
+
+LET var1 = 3.14 * 4;
+SELECT plpgsqlfx();
+ plpgsqlfx 
+-----------
+     12.56
+(1 row)
+
+DROP VARIABLE var1;
+DROP FUNCTION plpgsqlfx();
+set plan_cache_mode TO DEFAULT;
+-- usage LET statement in plpgsql should to work
+CREATE VARIABLE var1 int;
+CREATE VARIABLE var2 numeric[];
+DO $$
+BEGIN
+  LET var2 = '{}'::int[];
+  FOR i IN 1..10
+  LOOP
+    LET var1 = i;
+    LET var2[var1] = i;
+  END LOOP;
+  RAISE NOTICE 'result array: %', var2;
+END;
+$$;
+NOTICE:  result array: {1,2,3,4,5,6,7,8,9,10}
+DROP VARIABLE var1;
+DROP VARIABLE var2;
+-- CALL statement is not supported yet
+-- requires direct access to session variable from expression executor
+CREATE VARIABLE v int;
+CREATE PROCEDURE p(arg int) AS $$ BEGIN RAISE NOTICE '%', arg; END $$ LANGUAGE plpgsql;
+-- should not crash (but is not supported yet)
+CALL p(v);
+ERROR:  session variable cannot be used as an argument
+DO $$ BEGIN CALL p(v); END $$;
+ERROR:  session variable cannot be used as an argument
+CONTEXT:  SQL statement "CALL p(v)"
+PL/pgSQL function inline_code_block line 1 at CALL
+DROP PROCEDURE p(int);
+DROP VARIABLE v;
+-- test search path
+CREATE SCHEMA svartest;
+CREATE VARIABLE svartest.var1 AS numeric;
+-- should to fail
+LET var1 = pi();
+ERROR:  session variable "var1" doesn't exist
+LINE 1: LET var1 = pi();
+            ^
+SELECT var1;
+ERROR:  column "var1" does not exist
+LINE 1: SELECT var1;
+               ^
+-- should be ok
+LET svartest.var1 = pi();
+SELECT svartest.var1;
+       var1       
+------------------
+ 3.14159265358979
+(1 row)
+
+SET search_path TO svartest;
+-- should be ok
+LET var1 = pi() + 10;
+SELECT var1;
+       var1       
+------------------
+ 13.1415926535898
+(1 row)
+
+RESET search_path;
+DROP SCHEMA svartest CASCADE;
+NOTICE:  drop cascades to session variable svartest.var1
+CREATE VARIABLE var1 AS text;
+-- variables can be updated under RO transaction
+BEGIN;
+SET TRANSACTION READ ONLY;
+LET var1 = 'hello';
+COMMIT;
+SELECT var1;
+ var1  
+-------
+ hello
+(1 row)
+
+DROP VARIABLE var1;
+-- test of domains
+CREATE DOMAIN int_domain AS int NOT NULL CHECK (VALUE > 100);
+CREATE VARIABLE var1 AS int_domain;
+-- should fail
+SELECT var1;
+ERROR:  domain int_domain does not allow null values
+-- should be ok
+LET var1 = 1000;
+SELECT var1;
+ var1 
+------
+ 1000
+(1 row)
+
+-- should fail
+LET var1 = 10;
+ERROR:  value for domain int_domain violates check constraint "int_domain_check"
+-- should fail
+LET var1 = NULL;
+-- note - domain defaults are not supported yet (like PLpgSQL)
+DROP VARIABLE var1;
+DROP DOMAIN int_domain;
+CREATE SCHEMA svartest CREATE VARIABLE var1 AS int CREATE TABLE foo(a int);
+LET svartest.var1 = 100;
+SELECT svartest.var1;
+ var1 
+------
+  100
+(1 row)
+
+SET search_path to public, svartest;
+SELECT var1;
+ var1 
+------
+  100
+(1 row)
+
+DROP SCHEMA svartest CASCADE;
+NOTICE:  drop cascades to 2 other objects
+DETAIL:  drop cascades to table foo
+drop cascades to session variable var1
+CREATE VARIABLE var1 AS int;
+CREATE VARIABLE var2 AS int[];
+LET var1 = 2;
+LET var2 = '{}'::int[];
+LET var2[var1] = 0;
+SELECT var2;
+   var2    
+-----------
+ [2:2]={0}
+(1 row)
+
+DROP VARIABLE var1, var2;
+CREATE VARIABLE var1 AS int;
+CREATE VARIABLE var2 AS int[];
+LET var1 = 2;
+LET var2 = '{}'::int[];
+SELECT var2;
+ var2 
+------
+ {}
+(1 row)
+
+DROP VARIABLE var1, var2;
+-- the LET statement should be disallowed in CTE
+CREATE VARIABLE var1 AS int;
+WITH x AS (LET var1 = 100) SELECT * FROM x;
+ERROR:  syntax error at or near "LET"
+LINE 1: WITH x AS (LET var1 = 100) SELECT * FROM x;
+                   ^
+-- should be ok
+LET var1 = generate_series(1, 1);
+-- should fail
+LET var1 = generate_series(1, 2);
+ERROR:  expression returned more than one row
+LET var1 = generate_series(1, 0);
+ERROR:  expression returned no rows
+DROP VARIABLE var1;
+-- composite variables
+CREATE TYPE sv_xyz AS (x int, y int, z numeric(10,2));
+CREATE VARIABLE v1 AS sv_xyz;
+CREATE VARIABLE v2 AS sv_xyz;
+LET v1 = (1, 2, 3.14);
+LET v2 = (10, 20, 3.14 * 10);
+-- should work too - there are prepared casts
+LET v1 = (1, 2, 3);
+SELECT v1;
+     v1     
+------------
+ (1,2,3.00)
+(1 row)
+
+SELECT v2;
+      v2       
+---------------
+ (10,20,31.40)
+(1 row)
+
+SELECT (v1).*;
+ x | y |  z   
+---+---+------
+ 1 | 2 | 3.00
+(1 row)
+
+SELECT (v2).*;
+ x  | y  |   z   
+----+----+-------
+ 10 | 20 | 31.40
+(1 row)
+
+SELECT v1.x + v1.z;
+ ?column? 
+----------
+     4.00
+(1 row)
+
+SELECT v2.x + v2.z;
+ ?column? 
+----------
+    41.40
+(1 row)
+
+-- access to composite fields should be safe too
+CREATE ROLE regress_var_test_role;
+SET ROLE TO regress_var_test_role;
+-- should fail
+SELECT v2.x;
+ERROR:  permission denied for session variable v2
+SET ROLE TO DEFAULT;
+DROP VARIABLE v1;
+DROP VARIABLE v2;
+DROP ROLE regress_var_test_role;
+CREATE TYPE t1 AS (a int, b numeric, c text);
+CREATE VARIABLE v1 AS t1;
+LET v1 = (1, pi(), 'hello');
+SELECT v1;
+             v1             
+----------------------------
+ (1,3.14159265358979,hello)
+(1 row)
+
+LET v1.b = 10.2222;
+SELECT v1;
+        v1         
+-------------------
+ (1,10.2222,hello)
+(1 row)
+
+-- should fail, attribute doesn't exist
+LET v1.x = 10;
+ERROR:  cannot assign to field "x" of column "v1" because there is no such column in data type t1
+LINE 1: LET v1.x = 10;
+            ^
+-- should fail, don't allow multi column query
+LET v1 = (NULL::t1).*;
+ERROR:  assignment expression returned 3 columns
+LINE 1: LET v1 = (NULL::t1).*;
+                  ^
+-- allow DROP or ADD ATTRIBUTE on composite types
+-- should be ok
+ALTER TYPE t1 DROP ATTRIBUTE c;
+SELECT v1;
+     v1      
+-------------
+ (1,10.2222)
+(1 row)
+
+-- should be ok
+ALTER TYPE t1 ADD ATTRIBUTE c int;
+SELECT v1;
+      v1      
+--------------
+ (1,10.2222,)
+(1 row)
+
+LET v1 = (10, 10.3, 20);
+SELECT v1;
+      v1      
+--------------
+ (10,10.3,20)
+(1 row)
+
+-- should be ok
+ALTER TYPE t1 DROP ATTRIBUTE b;
+SELECT v1;
+   v1    
+---------
+ (10,20)
+(1 row)
+
+-- should fail, disallow data type change
+ALTER TYPE t1 ALTER ATTRIBUTE c TYPE int;
+ERROR:  cannot alter type "t1" because session variable "public.v1" uses it
+DROP VARIABLE v1;
+DROP TYPE t1;
+-- the table type can be used as composite type too
+CREATE TABLE svar_test(a int, b numeric, c date);
+CREATE VARIABLE var1 AS svar_test;
+LET var1 = (10, pi(), '2023-05-26');
+SELECT var1;
+               var1               
+----------------------------------
+ (10,3.14159265358979,05-26-2023)
+(1 row)
+
+-- should fail due dependency
+ALTER TABLE svar_test ALTER COLUMN a TYPE text;
+ERROR:  cannot alter table "svar_test" because session variable "public.var1" uses it
+-- should fail
+DROP TABLE svar_test;
+ERROR:  cannot drop table svar_test because other objects depend on it
+DETAIL:  session variable var1 depends on type svar_test
+HINT:  Use DROP ... CASCADE to drop the dependent objects too.
+DROP VARIABLE var1;
+DROP TABLE svar_test;
+-- arrays are supported
+CREATE VARIABLE var1 AS numeric[];
+LET var1 = ARRAY[1.1,2.1];
+LET var1[1] = 10.1;
+SELECT var1;
+    var1    
+------------
+ {10.1,2.1}
+(1 row)
+
+-- LET target doesn't allow srf, should fail
+LET var1[generate_series(1,3)] = 100;
+ERROR:  set-returning functions are not allowed in LET
+LINE 1: LET var1[generate_series(1,3)] = 100;
+                 ^
+DROP VARIABLE var1;
+-- arrays inside composite
+CREATE TYPE t1 AS (a numeric, b numeric[]);
+CREATE VARIABLE var1 AS t1;
+LET var1 = (10.1, ARRAY[0.0, 0.0]);
+LET var1.a = 10.2;
+SELECT var1;
+        var1        
+--------------------
+ (10.2,"{0.0,0.0}")
+(1 row)
+
+LET var1.b[1] = 10.3;
+SELECT var1;
+        var1         
+---------------------
+ (10.2,"{10.3,0.0}")
+(1 row)
+
+DROP VARIABLE var1;
+DROP TYPE t1;
+-- Encourage use of parallel plans
+SET parallel_setup_cost = 0;
+SET parallel_tuple_cost = 0;
+SET min_parallel_table_scan_size = 0;
+SET max_parallel_workers_per_gather = 2;
+-- test on query with workers
+CREATE TABLE svar_test(a int);
+INSERT INTO svar_test SELECT * FROM generate_series(1,1000);
+ANALYZE svar_test;
+CREATE VARIABLE zero int;
+LET zero = 0;
+-- result should be 100
+SELECT count(*) FROM svar_test WHERE a%10 = zero;
+ count 
+-------
+   100
+(1 row)
+
+-- parallel execution is not supported yet
+EXPLAIN (COSTS OFF) SELECT count(*) FROM svar_test WHERE a%10 = zero;
+            QUERY PLAN             
+-----------------------------------
+ Aggregate
+   ->  Seq Scan on svar_test
+         Filter: ((a % 10) = zero)
+(3 rows)
+
+LET zero = (SELECT count(*) FROM svar_test);
+-- result should be 1000
+SELECT zero;
+ zero 
+------
+ 1000
+(1 row)
+
+DROP VARIABLE zero;
+DROP TABLE svar_test;
+RESET parallel_setup_cost;
+RESET parallel_tuple_cost;
+RESET min_parallel_table_scan_size;
+RESET max_parallel_workers_per_gather;
+-- the result of view should be same in parallel mode too
+CREATE VARIABLE var1 AS int;
+LET var1 = 10;
+CREATE VIEW var1view AS SELECT COALESCE(var1, 0) AS result;
+SELECT * FROM var1view;
+ result 
+--------
+     10
+(1 row)
+
+SET debug_parallel_query TO on;
+SELECT * FROM var1view;
+ result 
+--------
+     10
+(1 row)
+
+SET debug_parallel_query TO off;
+DROP VIEW var1view;
+DROP VARIABLE var1;
+CREATE VARIABLE varid int;
+CREATE TABLE svar_test(id int, v int);
+LET varid = 1;
+INSERT INTO svar_test VALUES(varid, 100);
+SELECT * FROM svar_test;
+ id |  v  
+----+-----
+  1 | 100
+(1 row)
+
+UPDATE svar_test SET v = 200 WHERE id = varid;
+SELECT * FROM svar_test;
+ id |  v  
+----+-----
+  1 | 200
+(1 row)
+
+DELETE FROM svar_test WHERE id = varid;
+SELECT * FROM svar_test;
+ id | v 
+----+---
+(0 rows)
+
+DROP TABLE svar_test;
+DROP VARIABLE varid;
+-- visibility check
+-- variables should be shadowed always
+CREATE VARIABLE var1 AS text;
+SELECT var1.relname FROM pg_class var1 WHERE var1.relname = 'pg_class';
+ relname  
+----------
+ pg_class
+(1 row)
+
+DROP VARIABLE var1;
+CREATE TABLE xxtab(avar int);
+INSERT INTO xxtab VALUES(333);
+CREATE TYPE xxtype AS (avar int);
+CREATE VARIABLE xxtab AS xxtype;
+INSERT INTO xxtab VALUES(10);
+-- it is ambiguous, but columns are preferred
+SELECT xxtab.avar FROM xxtab;
+ avar 
+------
+  333
+   10
+(2 rows)
+
+-- should be ok
+SELECT avar FROM xxtab;
+ avar 
+------
+  333
+   10
+(2 rows)
+
+CREATE VARIABLE public.avar AS int;
+-- should be ok, see the table
+SELECT avar FROM xxtab;
+ avar 
+------
+  333
+   10
+(2 rows)
+
+-- should be ok
+SELECT public.avar FROM xxtab;
+ avar 
+------
+     
+     
+(2 rows)
+
+DROP VARIABLE xxtab;
+SELECT xxtab.avar FROM xxtab;
+ avar 
+------
+  333
+   10
+(2 rows)
+
+DROP VARIABLE public.avar;
+DROP TYPE xxtype;
+DROP TABLE xxtab;
+-- The variable can be shadowed by table or by alias
+CREATE TYPE public.svar_type AS (a int, b int, c int);
+CREATE VARIABLE public.svar AS public.svar_type;
+CREATE TABLE public.svar(a int, b int);
+INSERT INTO public.svar VALUES(10, 20);
+LET public.svar = (100, 200, 300);
+-- should be ok
+-- show table
+SELECT * FROM public.svar;
+ a  | b  
+----+----
+ 10 | 20
+(1 row)
+
+SELECT svar.a FROM public.svar;
+ a  
+----
+ 10
+(1 row)
+
+SELECT svar.* FROM public.svar;
+ a  | b  
+----+----
+ 10 | 20
+(1 row)
+
+-- show variable
+SELECT public.svar;
+     svar      
+---------------
+ (100,200,300)
+(1 row)
+
+SELECT public.svar.c;
+  c  
+-----
+ 300
+(1 row)
+
+SELECT (public.svar).*;
+  a  |  b  |  c  
+-----+-----+-----
+ 100 | 200 | 300
+(1 row)
+
+-- the variable is shadowed, raise error
+SELECT public.svar.c FROM public.svar;
+ERROR:  column svar.c does not exist
+LINE 1: SELECT public.svar.c FROM public.svar;
+               ^
+-- can be fixed by alias
+SELECT public.svar.c FROM public.svar x;
+  c  
+-----
+ 300
+(1 row)
+
+SELECT svar.a FROM public.svar;
+ a  
+----
+ 10
+(1 row)
+
+SELECT svar.* FROM public.svar;
+ a  | b  
+----+----
+ 10 | 20
+(1 row)
+
+-- show variable
+SELECT public.svar;
+     svar      
+---------------
+ (100,200,300)
+(1 row)
+
+SELECT public.svar.c;
+  c  
+-----
+ 300
+(1 row)
+
+SELECT (public.svar).*;
+  a  |  b  |  c  
+-----+-----+-----
+ 100 | 200 | 300
+(1 row)
+
+-- the variable is shadowed, raise error
+SELECT public.svar.c FROM public.svar;
+ERROR:  column svar.c does not exist
+LINE 1: SELECT public.svar.c FROM public.svar;
+               ^
+-- can be fixed by alias
+SELECT public.svar.c FROM public.svar x;
+  c  
+-----
+ 300
+(1 row)
+
+DROP VARIABLE public.svar;
+DROP TABLE public.svar;
+DROP TYPE public.svar_type;
+CREATE TYPE ab AS (a integer, b integer);
+CREATE VARIABLE v_ab AS ab;
+CREATE TABLE v_ab (a integer, b integer);
+INSERT INTO v_ab VALUES(10,20);
+-- we should to see table
+SELECT v_ab.a FROM v_ab;
+ a  
+----
+ 10
+(1 row)
+
+CREATE SCHEMA v_ab;
+CREATE VARIABLE v_ab.a AS integer;
+-- we should to see table
+SELECT v_ab.a FROM v_ab;
+ a  
+----
+ 10
+(1 row)
+
+DROP VARIABLE v_ab;
+DROP TABLE v_ab;
+DROP TYPE ab;
+CREATE TYPE t_am_type AS (b int);
+CREATE SCHEMA xxx_am;
+SET search_path TO public;
+CREATE VARIABLE xxx_am AS t_am_type;
+LET xxx_am = ROW(10);
+-- should be ok
+SELECT xxx_am;
+ xxx_am 
+--------
+ (10)
+(1 row)
+
+CREATE VARIABLE xxx_am.b AS int;
+LET :"DBNAME".xxx_am.b = 20;
+-- should be still ok
+SELECT xxx_am;
+ xxx_am 
+--------
+ (10)
+(1 row)
+
+-- should fail, the reference should be ambiguous
+SELECT xxx_am.b;
+ERROR:  session variable reference "xxx_am.b" is ambiguous
+LINE 1: SELECT xxx_am.b;
+               ^
+-- enhanced references should be ok
+SELECT public.xxx_am.b;
+ b  
+----
+ 10
+(1 row)
+
+SELECT :"DBNAME".xxx_am.b;
+ b  
+----
+ 20
+(1 row)
+
+CREATE TABLE xxx_am(b  int);
+INSERT INTO xxx_am VALUES(10);
+-- we should to see table
+SELECT xxx_am.b FROM xxx_am;
+ b  
+----
+ 10
+(1 row)
+
+SELECT x.b FROM xxx_am x;
+ b  
+----
+ 10
+(1 row)
+
+DROP TABLE xxx_am;
+DROP VARIABLE public.xxx_am;
+DROP VARIABLE xxx_am.b;
+DROP SCHEMA xxx_am;
+CREATE SCHEMA :"DBNAME";
+CREATE VARIABLE :"DBNAME".:"DBNAME".:"DBNAME" AS t_am_type;
+CREATE VARIABLE :"DBNAME".:"DBNAME".b AS int;
+SET search_path TO :"DBNAME";
+-- should be ambiguous
+SELECT :"DBNAME".b;
+ERROR:  session variable reference "regression.b" is ambiguous
+LINE 1: SELECT "regression".b;
+               ^
+-- should be ambiguous too
+SELECT :"DBNAME".:"DBNAME".b;
+ERROR:  session variable reference "regression.regression.b" is ambiguous
+LINE 1: SELECT "regression"."regression".b;
+               ^
+CREATE TABLE :"DBNAME"(b int);
+-- should be ok
+SELECT :"DBNAME".b FROM :"DBNAME";
+ b 
+---
+(0 rows)
+
+DROP TABLE :"DBNAME";
+DROP VARIABLE :"DBNAME".:"DBNAME".b;
+DROP VARIABLE :"DBNAME".:"DBNAME".:"DBNAME";
+DROP SCHEMA :"DBNAME";
+RESET search_path;
diff --git a/src/test/regress/sql/session_variables.sql b/src/test/regress/sql/session_variables.sql
index 42dfb95a02..1f72b405a2 100644
--- a/src/test/regress/sql/session_variables.sql
+++ b/src/test/regress/sql/session_variables.sql
@@ -59,3 +59,607 @@ DROP VARIABLE svartest.var1;
 DROP SCHEMA svartest;
 
 DROP ROLE regress_variable_owner;
+
+-- check access rights
+CREATE ROLE regress_noowner;
+
+CREATE VARIABLE var1 AS int;
+
+CREATE OR REPLACE FUNCTION sqlfx(int)
+RETURNS int AS $$ SELECT $1 + var1 $$ LANGUAGE sql;
+
+CREATE OR REPLACE FUNCTION sqlfx_sd(int)
+RETURNS int AS $$ SELECT $1 + var1 $$ LANGUAGE sql SECURITY DEFINER;
+
+CREATE OR REPLACE FUNCTION plpgsqlfx(int)
+RETURNS int AS $$ BEGIN RETURN $1 + var1; END $$ LANGUAGE plpgsql;
+
+CREATE OR REPLACE FUNCTION plpgsqlfx_sd(int)
+RETURNS int AS $$ BEGIN RETURN $1 + var1; END $$ LANGUAGE plpgsql SECURITY DEFINER;
+
+LET var1 = 10;
+-- should be ok
+SELECT var1;
+SELECT sqlfx(20);
+SELECT sqlfx_sd(20);
+SELECT plpgsqlfx(20);
+SELECT plpgsqlfx_sd(20);
+
+-- should to fail
+SET ROLE TO regress_noowner;
+
+SELECT var1;
+SELECT sqlfx(20);
+SELECT plpgsqlfx(20);
+
+-- should be ok
+SELECT sqlfx_sd(20);
+SELECT plpgsqlfx_sd(20);
+
+SET ROLE TO DEFAULT;
+GRANT SELECT ON VARIABLE var1 TO regress_noowner;
+
+-- should be ok
+SET ROLE TO regress_noowner;
+
+SELECT var1;
+SELECT sqlfx(20);
+SELECT plpgsqlfx(20);
+
+SET ROLE TO DEFAULT;
+DROP VARIABLE var1;
+DROP FUNCTION sqlfx(int);
+DROP FUNCTION plpgsqlfx(int);
+DROP FUNCTION sqlfx_sd(int);
+DROP FUNCTION plpgsqlfx_sd(int);
+
+DROP ROLE regress_noowner;
+
+-- use variables inside views
+CREATE VARIABLE var1 AS numeric;
+
+-- use variables in views
+CREATE VIEW test_view AS SELECT COALESCE(var1 + v, 0) AS result FROM generate_series(1,2) g(v);
+SELECT * FROM test_view;
+LET var1 = 3.14;
+SELECT * FROM test_view;
+
+-- start a new session
+\c
+
+SELECT * FROM test_view;
+LET var1 = 3.14;
+SELECT * FROM test_view;
+
+-- should fail, dependency
+DROP VARIABLE var1;
+
+-- should be ok
+DROP VARIABLE var1 CASCADE;
+
+CREATE VARIABLE var1 text;
+CREATE VARIABLE var2 text;
+
+-- use variables in SQL functions
+CREATE OR REPLACE FUNCTION sqlfx1(varchar)
+RETURNS varchar AS $$ SELECT var1 || ', ' || $1 $$ LANGUAGE sql;
+
+CREATE OR REPLACE FUNCTION sqlfx2( varchar)
+RETURNS varchar AS $$ SELECT var2 || ', ' || $1 $$ LANGUAGE sql;
+
+LET var1 = 'str1';
+LET var2 = 'str2';
+
+SELECT sqlfx1(sqlfx2('Hello'));
+
+-- inlining is blocked
+EXPLAIN (COSTS OFF, VERBOSE) SELECT sqlfx1(sqlfx2('Hello'));
+
+DROP FUNCTION sqlfx1(varchar);
+DROP FUNCTION sqlfx2(varchar);
+DROP VARIABLE var1;
+DROP VARIABLE var2;
+
+-- access from cached plans should to work
+CREATE VARIABLE var1 AS numeric;
+
+CREATE OR REPLACE FUNCTION plpgsqlfx()
+RETURNS numeric AS $$ BEGIN RETURN var1; END $$ LANGUAGE plpgsql;
+
+set plan_cache_mode TO force_generic_plan;
+
+LET var1 = 3.14;
+SELECT plpgsqlfx();
+LET var1 = 3.14 * 2;
+SELECT plpgsqlfx();
+
+DROP VARIABLE var1;
+
+-- dependency (plan invalidation) should to work
+CREATE VARIABLE var1 AS numeric;
+
+LET var1 = 3.14 * 3;
+SELECT plpgsqlfx();
+LET var1 = 3.14 * 4;
+SELECT plpgsqlfx();
+
+DROP VARIABLE var1;
+DROP FUNCTION plpgsqlfx();
+
+set plan_cache_mode TO DEFAULT;
+
+-- usage LET statement in plpgsql should to work
+CREATE VARIABLE var1 int;
+CREATE VARIABLE var2 numeric[];
+
+DO $$
+BEGIN
+  LET var2 = '{}'::int[];
+  FOR i IN 1..10
+  LOOP
+    LET var1 = i;
+    LET var2[var1] = i;
+  END LOOP;
+  RAISE NOTICE 'result array: %', var2;
+END;
+$$;
+
+DROP VARIABLE var1;
+DROP VARIABLE var2;
+
+-- CALL statement is not supported yet
+-- requires direct access to session variable from expression executor
+CREATE VARIABLE v int;
+
+CREATE PROCEDURE p(arg int) AS $$ BEGIN RAISE NOTICE '%', arg; END $$ LANGUAGE plpgsql;
+
+-- should not crash (but is not supported yet)
+CALL p(v);
+
+DO $$ BEGIN CALL p(v); END $$;
+
+DROP PROCEDURE p(int);
+DROP VARIABLE v;
+
+-- test search path
+CREATE SCHEMA svartest;
+CREATE VARIABLE svartest.var1 AS numeric;
+
+-- should to fail
+LET var1 = pi();
+SELECT var1;
+
+-- should be ok
+LET svartest.var1 = pi();
+SELECT svartest.var1;
+
+SET search_path TO svartest;
+
+-- should be ok
+LET var1 = pi() + 10;
+SELECT var1;
+
+RESET search_path;
+DROP SCHEMA svartest CASCADE;
+
+CREATE VARIABLE var1 AS text;
+
+-- variables can be updated under RO transaction
+BEGIN;
+SET TRANSACTION READ ONLY;
+LET var1 = 'hello';
+COMMIT;
+
+SELECT var1;
+
+DROP VARIABLE var1;
+
+-- test of domains
+CREATE DOMAIN int_domain AS int NOT NULL CHECK (VALUE > 100);
+CREATE VARIABLE var1 AS int_domain;
+
+-- should fail
+SELECT var1;
+
+-- should be ok
+LET var1 = 1000;
+SELECT var1;
+
+-- should fail
+LET var1 = 10;
+
+-- should fail
+LET var1 = NULL;
+
+-- note - domain defaults are not supported yet (like PLpgSQL)
+
+DROP VARIABLE var1;
+DROP DOMAIN int_domain;
+
+CREATE SCHEMA svartest CREATE VARIABLE var1 AS int CREATE TABLE foo(a int);
+LET svartest.var1 = 100;
+SELECT svartest.var1;
+
+SET search_path to public, svartest;
+
+SELECT var1;
+
+DROP SCHEMA svartest CASCADE;
+
+CREATE VARIABLE var1 AS int;
+CREATE VARIABLE var2 AS int[];
+
+LET var1 = 2;
+LET var2 = '{}'::int[];
+
+LET var2[var1] = 0;
+
+SELECT var2;
+
+DROP VARIABLE var1, var2;
+
+CREATE VARIABLE var1 AS int;
+CREATE VARIABLE var2 AS int[];
+
+LET var1 = 2;
+LET var2 = '{}'::int[];
+
+SELECT var2;
+
+DROP VARIABLE var1, var2;
+
+-- the LET statement should be disallowed in CTE
+CREATE VARIABLE var1 AS int;
+WITH x AS (LET var1 = 100) SELECT * FROM x;
+
+-- should be ok
+LET var1 = generate_series(1, 1);
+
+-- should fail
+LET var1 = generate_series(1, 2);
+LET var1 = generate_series(1, 0);
+
+DROP VARIABLE var1;
+
+-- composite variables
+CREATE TYPE sv_xyz AS (x int, y int, z numeric(10,2));
+
+CREATE VARIABLE v1 AS sv_xyz;
+CREATE VARIABLE v2 AS sv_xyz;
+
+LET v1 = (1, 2, 3.14);
+LET v2 = (10, 20, 3.14 * 10);
+
+-- should work too - there are prepared casts
+LET v1 = (1, 2, 3);
+
+SELECT v1;
+SELECT v2;
+SELECT (v1).*;
+SELECT (v2).*;
+
+SELECT v1.x + v1.z;
+SELECT v2.x + v2.z;
+
+-- access to composite fields should be safe too
+CREATE ROLE regress_var_test_role;
+
+SET ROLE TO regress_var_test_role;
+
+-- should fail
+SELECT v2.x;
+
+SET ROLE TO DEFAULT;
+
+DROP VARIABLE v1;
+DROP VARIABLE v2;
+
+DROP ROLE regress_var_test_role;
+
+CREATE TYPE t1 AS (a int, b numeric, c text);
+
+CREATE VARIABLE v1 AS t1;
+LET v1 = (1, pi(), 'hello');
+SELECT v1;
+LET v1.b = 10.2222;
+SELECT v1;
+
+-- should fail, attribute doesn't exist
+LET v1.x = 10;
+
+-- should fail, don't allow multi column query
+LET v1 = (NULL::t1).*;
+
+-- allow DROP or ADD ATTRIBUTE on composite types
+-- should be ok
+ALTER TYPE t1 DROP ATTRIBUTE c;
+SELECT v1;
+
+-- should be ok
+ALTER TYPE t1 ADD ATTRIBUTE c int;
+SELECT v1;
+
+LET v1 = (10, 10.3, 20);
+SELECT v1;
+
+-- should be ok
+ALTER TYPE t1 DROP ATTRIBUTE b;
+SELECT v1;
+
+-- should fail, disallow data type change
+ALTER TYPE t1 ALTER ATTRIBUTE c TYPE int;
+
+DROP VARIABLE v1;
+DROP TYPE t1;
+
+-- the table type can be used as composite type too
+CREATE TABLE svar_test(a int, b numeric, c date);
+CREATE VARIABLE var1 AS svar_test;
+
+LET var1 = (10, pi(), '2023-05-26');
+SELECT var1;
+
+-- should fail due dependency
+ALTER TABLE svar_test ALTER COLUMN a TYPE text;
+
+-- should fail
+DROP TABLE svar_test;
+
+DROP VARIABLE var1;
+DROP TABLE svar_test;
+
+-- arrays are supported
+CREATE VARIABLE var1 AS numeric[];
+LET var1 = ARRAY[1.1,2.1];
+LET var1[1] = 10.1;
+SELECT var1;
+
+-- LET target doesn't allow srf, should fail
+LET var1[generate_series(1,3)] = 100;
+
+DROP VARIABLE var1;
+
+-- arrays inside composite
+CREATE TYPE t1 AS (a numeric, b numeric[]);
+CREATE VARIABLE var1 AS t1;
+LET var1 = (10.1, ARRAY[0.0, 0.0]);
+LET var1.a = 10.2;
+SELECT var1;
+LET var1.b[1] = 10.3;
+SELECT var1;
+
+DROP VARIABLE var1;
+DROP TYPE t1;
+
+-- Encourage use of parallel plans
+SET parallel_setup_cost = 0;
+SET parallel_tuple_cost = 0;
+SET min_parallel_table_scan_size = 0;
+SET max_parallel_workers_per_gather = 2;
+
+-- test on query with workers
+CREATE TABLE svar_test(a int);
+INSERT INTO svar_test SELECT * FROM generate_series(1,1000);
+ANALYZE svar_test;
+CREATE VARIABLE zero int;
+LET zero = 0;
+
+-- result should be 100
+SELECT count(*) FROM svar_test WHERE a%10 = zero;
+
+-- parallel execution is not supported yet
+EXPLAIN (COSTS OFF) SELECT count(*) FROM svar_test WHERE a%10 = zero;
+
+LET zero = (SELECT count(*) FROM svar_test);
+
+-- result should be 1000
+SELECT zero;
+
+DROP VARIABLE zero;
+DROP TABLE svar_test;
+
+RESET parallel_setup_cost;
+RESET parallel_tuple_cost;
+RESET min_parallel_table_scan_size;
+RESET max_parallel_workers_per_gather;
+
+-- the result of view should be same in parallel mode too
+CREATE VARIABLE var1 AS int;
+LET var1 = 10;
+
+CREATE VIEW var1view AS SELECT COALESCE(var1, 0) AS result;
+
+SELECT * FROM var1view;
+
+SET debug_parallel_query TO on;
+
+SELECT * FROM var1view;
+
+SET debug_parallel_query TO off;
+
+DROP VIEW var1view;
+DROP VARIABLE var1;
+
+CREATE VARIABLE varid int;
+CREATE TABLE svar_test(id int, v int);
+
+LET varid = 1;
+INSERT INTO svar_test VALUES(varid, 100);
+SELECT * FROM svar_test;
+UPDATE svar_test SET v = 200 WHERE id = varid;
+SELECT * FROM svar_test;
+DELETE FROM svar_test WHERE id = varid;
+SELECT * FROM svar_test;
+
+DROP TABLE svar_test;
+DROP VARIABLE varid;
+
+
+-- visibility check
+-- variables should be shadowed always
+CREATE VARIABLE var1 AS text;
+SELECT var1.relname FROM pg_class var1 WHERE var1.relname = 'pg_class';
+
+DROP VARIABLE var1;
+
+CREATE TABLE xxtab(avar int);
+
+INSERT INTO xxtab VALUES(333);
+
+CREATE TYPE xxtype AS (avar int);
+
+CREATE VARIABLE xxtab AS xxtype;
+
+INSERT INTO xxtab VALUES(10);
+
+-- it is ambiguous, but columns are preferred
+SELECT xxtab.avar FROM xxtab;
+
+-- should be ok
+SELECT avar FROM xxtab;
+
+CREATE VARIABLE public.avar AS int;
+
+-- should be ok, see the table
+SELECT avar FROM xxtab;
+
+-- should be ok
+SELECT public.avar FROM xxtab;
+
+DROP VARIABLE xxtab;
+
+SELECT xxtab.avar FROM xxtab;
+
+DROP VARIABLE public.avar;
+
+DROP TYPE xxtype;
+
+DROP TABLE xxtab;
+
+-- The variable can be shadowed by table or by alias
+CREATE TYPE public.svar_type AS (a int, b int, c int);
+CREATE VARIABLE public.svar AS public.svar_type;
+
+CREATE TABLE public.svar(a int, b int);
+
+INSERT INTO public.svar VALUES(10, 20);
+
+LET public.svar = (100, 200, 300);
+
+-- should be ok
+-- show table
+SELECT * FROM public.svar;
+SELECT svar.a FROM public.svar;
+SELECT svar.* FROM public.svar;
+
+-- show variable
+SELECT public.svar;
+SELECT public.svar.c;
+SELECT (public.svar).*;
+
+-- the variable is shadowed, raise error
+SELECT public.svar.c FROM public.svar;
+
+-- can be fixed by alias
+SELECT public.svar.c FROM public.svar x;
+
+SELECT svar.a FROM public.svar;
+SELECT svar.* FROM public.svar;
+
+-- show variable
+SELECT public.svar;
+SELECT public.svar.c;
+SELECT (public.svar).*;
+
+-- the variable is shadowed, raise error
+SELECT public.svar.c FROM public.svar;
+
+-- can be fixed by alias
+SELECT public.svar.c FROM public.svar x;
+
+DROP VARIABLE public.svar;
+DROP TABLE public.svar;
+DROP TYPE public.svar_type;
+
+CREATE TYPE ab AS (a integer, b integer);
+
+CREATE VARIABLE v_ab AS ab;
+
+CREATE TABLE v_ab (a integer, b integer);
+INSERT INTO v_ab VALUES(10,20);
+
+-- we should to see table
+SELECT v_ab.a FROM v_ab;
+
+CREATE SCHEMA v_ab;
+
+CREATE VARIABLE v_ab.a AS integer;
+
+-- we should to see table
+SELECT v_ab.a FROM v_ab;
+
+DROP VARIABLE v_ab;
+DROP TABLE v_ab;
+DROP TYPE ab;
+
+CREATE TYPE t_am_type AS (b int);
+CREATE SCHEMA xxx_am;
+
+SET search_path TO public;
+
+CREATE VARIABLE xxx_am AS t_am_type;
+LET xxx_am = ROW(10);
+
+-- should be ok
+SELECT xxx_am;
+
+CREATE VARIABLE xxx_am.b AS int;
+LET :"DBNAME".xxx_am.b = 20;
+
+-- should be still ok
+SELECT xxx_am;
+
+-- should fail, the reference should be ambiguous
+SELECT xxx_am.b;
+
+-- enhanced references should be ok
+SELECT public.xxx_am.b;
+SELECT :"DBNAME".xxx_am.b;
+
+CREATE TABLE xxx_am(b  int);
+INSERT INTO xxx_am VALUES(10);
+
+-- we should to see table
+SELECT xxx_am.b FROM xxx_am;
+SELECT x.b FROM xxx_am x;
+
+DROP TABLE xxx_am;
+DROP VARIABLE public.xxx_am;
+DROP VARIABLE xxx_am.b;
+DROP SCHEMA xxx_am;
+
+CREATE SCHEMA :"DBNAME";
+
+CREATE VARIABLE :"DBNAME".:"DBNAME".:"DBNAME" AS t_am_type;
+CREATE VARIABLE :"DBNAME".:"DBNAME".b AS int;
+
+SET search_path TO :"DBNAME";
+
+-- should be ambiguous
+SELECT :"DBNAME".b;
+
+-- should be ambiguous too
+SELECT :"DBNAME".:"DBNAME".b;
+
+CREATE TABLE :"DBNAME"(b int);
+
+-- should be ok
+SELECT :"DBNAME".b FROM :"DBNAME";
+
+DROP TABLE :"DBNAME";
+
+DROP VARIABLE :"DBNAME".:"DBNAME".b;
+DROP VARIABLE :"DBNAME".:"DBNAME".:"DBNAME";
+DROP SCHEMA :"DBNAME";
+
+RESET search_path;
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f0a51799f..375f659604 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1431,6 +1431,7 @@ LargeObjectDesc
 Latch
 LauncherLastStartTimesEntry
 LerpFunc
+LetStmt
 LexDescr
 LexemeEntry
 LexemeHashKey
@@ -2518,6 +2519,7 @@ SerializedTransactionState
 Session
 SessionBackupState
 SessionEndType
+SessionVariableValue
 SetConstraintState
 SetConstraintStateData
 SetConstraintTriggerData
@@ -2705,6 +2707,9 @@ SupportRequestRows
 SupportRequestSelectivity
 SupportRequestSimplify
 SupportRequestWFuncMonotonic
+SVariable
+SVariableData
+SVariableState
 Syn
 SyncOps
 SyncRepConfigData
-- 
2.43.0

