Subject: Schema variables - new implementation for Postgres 15

From: Pavel Stehule <[email-masked]>
Date: Thu, 15 Apr 2021 10:42:42 +0200

Hi,

I am returning back to implementation of schema variables. The schema
variables can be used as an alternative to package variables (Oracle's
PL/SQL or ADA). The schema variables can be used as fast and safe storage
of session information for RLS too.

The previous implementation had not cleanly implemented execution of the
LET statement. It was something between query and utility, and although it
was working - it was out of Postgres concept (with different implementation
of queries and utilities).

I totally rewrote the implementation of the LET statement. I prepared two
variants:

First variant is based on the introduction of the new command type CMD_LET
and new very small executor node SetVariable (this is a very very reduced
analogy of ModifyTable node). The code is consistent and what is important
- the LET statement can be prepared. The execution is relatively fast from
PLpgSQL too. Without any special support the execution has the same speed
like non simple queries. The statement reuses  an execution plan, but
simple execution is not supported.

Second variant is implemented like a classic utility command. There is not
any surprise. It is shorter, simple, but the LET statement cannot be
prepared (this is the limit of all utility statements). Without special
support in PLpgSQL the execution is about 10x slower than the execution of
the first variant. But there is a new possibility of using the main parser
from PLpgSQL (implemented by Tom for new implementation of assign statement
in pg 14), and then this support in plpgsql requires only a few lines).
When the statement LET is explicitly supported by PLpgSQL, then execution
is very fast (the speed is comparable with the speed of the assign
statement) - it is about 10x faster than the first variant.

I tested code

do $$
declare x int ;
begin
  for i in 1..1000000
  loop
    let ooo = i;
  end loop;
end;
$$;

variant 1 .. 1500 ms
variant 2 with PLpgSQL support .. 140 ms
variant 2 without PLpgSQL support 9000 ms

The slower speed of the first variant from PLpgSQL can be fixed. But for
this moment, the speed is good enough. This is the worst case, because in
the first variant LET statement cannot use optimization for simple query
evaluation (now).

Now I think so implementation is significantly cleaner, and I hope so it
will be more acceptable for committers.

I am starting a new thread, because this is a new implementation, and
because I am sending two alternative implementations of one functionality.

Comments, notes, objections?

Regards

Pavel
diff --git a/doc/src/sgml/advanced.sgml b/doc/src/sgml/advanced.sgml
index 2d4ab85d45..5fbc6f73ab 100644
--- a/doc/src/sgml/advanced.sgml
+++ b/doc/src/sgml/advanced.sgml
@@ -700,6 +700,56 @@ SELECT name, elevation
   </sect1>

+  <sect1 id="tutorial-schema-variables">
+   <title>Schema Variables</title>
+
+   <indexterm zone="tutorial-schema-variables">
+    <primary>Schema variables</primary>
+   </indexterm>
+
+   <indexterm>
+    <primary>schema variable</primary>
+    <secondary>introduction</secondary>
+   </indexterm>
+
+   <para>
+    Schema variables are database objects that can hold a value.
+    Schema variables, like relations, exist within a schema and their access is
+    controlled via <command>GRANT</command> and <command>REVOKE</command> commands.
+    The schema variable can be created by the <command>CREATE VARIABLE</command> command.
+   </para>
+
+   <para>
+    The value of a schema variable is local to the current session. Retrieving
+    a variable's value returns either a NULL or a default value, unless its value
+    is set to something else in the current session with a LET command. The content
+    of a variable is not transactional. This is the same as in regular variables
+    in PL languages.
+   </para>
+
+   <para>
+    Schema variables are retrieved by the <command>SELECT</command> SQL command.
+    Their value is set with the <command>LET</command> SQL command.
+    While schema variables share properties with tables, their value cannot be updated
+    with an <command>UPDATE</command> 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>
+  </sect1>
+
+
   <sect1 id="tutorial-conclusion">
    <title>Conclusion</title>
 
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 2656786d1e..0edd6ba91c 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -359,6 +359,11 @@
       <entry><link linkend="catalog-pg-user-mapping"><structname>pg_user_mapping</structname></link></entry>
       <entry>mappings of users to foreign servers</entry>
      </row>
+
+     <row>
+      <entry><link linkend="catalog-pg-variable"><structname>pg_variable</structname></link></entry>
+      <entry>schema variables</entry>
+     </row>
     </tbody>
    </tgroup>
   </table>
@@ -13886,4 +13891,143 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
 
  </sect1>
 
+ <sect1 id="catalog-pg-variable">
+  <title><structname>pg_variable</structname></title>
+
+  <indexterm zone="catalog-pg-variable">
+   <primary>pg_variable</primary>
+  </indexterm>
+
+  <para>
+   The table <structname>pg_variable</structname> holds metadata
+   of schema variables.
+  </para>
+
+  <table>
+   <title><structname>pg_variable</structname> Columns</title>
+
+   <tgroup cols="4">
+    <thead>
+     <row>
+      <entry>Name</entry>
+      <entry>Type</entry>
+      <entry>References</entry>
+      <entry>Description</entry>
+     </row>
+    </thead>
+    <tbody>
+     <row>
+      <entry><structfield>oid</structfield></entry>
+      <entry><type>oid</type></entry>
+      <entry></entry>
+      <entry>Row identifier</entry>
+     </row>
+
+     <row>
+      <entry><structfield>varname</structfield></entry>
+      <entry><type>name</type></entry>
+      <entry></entry>
+      <entry>Name of the schema variable</entry>
+     </row>
+
+     <row>
+      <entry><structfield>varnamespace</structfield></entry>
+      <entry><type>oid</type></entry>
+      <entry><literal><link linkend="catalog-pg-namespace"><structname>pg_namespace</structname></link>.oid</literal></entry>
+      <entry>
+       The OID of the namespace that contains this variable
+      </entry>
+     </row>
+
+     <row>
+      <entry><structfield>vartype</structfield></entry>
+      <entry><type>oid</type></entry>
+      <entry><literal><link linkend="catalog-pg-type"><structname>pg_type</structname></link>.oid</literal></entry>
+      <entry>
+       The OID of the variable's data type.
+      </entry>
+     </row>
+
+     <row>
+      <entry><structfield>vartypmod</structfield></entry>
+      <entry><type>int4</type></entry>
+      <entry></entry>
+      <entry>
+       <structfield>vartypmod</structfield> records type-specific data
+       supplied at variable creation time (for example, the maximum
+       length of a <type>varchar</type> column).  It is passed to
+       type-specific input 
+       functions and length coercion functions.
+       The value will generally be -1 for types that do not need <structfield>vartypmod</structfield>.
+      </entry>
+     </row>
+
+     <row>
+      <entry><structfield>varowner</structfield></entry>
+      <entry><type>oid</type></entry>
+      <entry><literal><link linkend="catalog-pg-authid"><structname>pg_authid</structname></link>.oid</literal></entry>
+      <entry>Owner of the variable</entry>
+     </row>
+
+     <row>
+      <entry><structfield>varcollation</structfield></entry>
+      <entry><type>oid</type></entry>
+      <entry><literal><link linkend="catalog-pg-collation"><structname>pg_collation</structname></link>.oid</literal></entry>
+      <entry>
+       The defined collation of the variable, or zero if the variable is
+       not of a collatable data type.
+      </entry>
+     </row>
+
+     <row>
+      <entry><structfield>varisnotnull</structfield></entry>
+      <entry><type>boolean</type></entry>
+      <entry></entry>
+      <entry>
+       True if the schema variable doesn't allow null value. The default value is false.
+      </entry>
+     </row>
+
+     <row>
+      <entry><structfield>varisimmutable</structfield></entry>
+      <entry><type>boolean</type></entry>
+      <entry></entry>
+      <entry>
+       True if the variable is immutable (cannot be modified). The default value is false.
+      </entry>
+     </row>
+
+     <row>
+      <entry><structfield>vareoxaction</structfield></entry>
+      <entry><type>char</type></entry>
+      <entry></entry>
+      <entry>
+       <literal>n</literal> = no action, <literal>d</literal> = drop the variable,
+       <literal>r</literal> = reset the variable to its default value.
+      </entry>
+     </row>
+
+     <row>
+      <entry><structfield>vardefexpr</structfield></entry>
+      <entry><type>pg_node_tree</type></entry>
+      <entry></entry>
+      <entry>The internal representation of the variable default value.</entry>
+     </row>
+
+     <row>
+      <entry><structfield>varacl</structfield></entry>
+      <entry><type>aclitem[]</type></entry>
+      <entry></entry>
+      <entry>
+       Access privileges; see
+       <xref linkend="sql-grant"/> and
+       <xref linkend="sql-revoke"/>
+       for details
+      </entry>
+     </row>
+    </tbody>
+   </tgroup>
+  </table>
+ </sect1>
+
 </chapter>
diff --git a/doc/src/sgml/plpgsql.sgml b/doc/src/sgml/plpgsql.sgml
index 52f60c827c..ac72f3c71d 100644
--- a/doc/src/sgml/plpgsql.sgml
+++ b/doc/src/sgml/plpgsql.sgml
@@ -5926,6 +5926,19 @@ $$ LANGUAGE plpgsql STRICT IMMUTABLE;
 </programlisting>
     </para>
    </sect3>
+
+   <sect3>
+    <title><command>Global variables and constants</command></title>
+
+    <para>
+     The <application>PL/pgSQL</application> language has no packages,
+     and therefore no package variables or package constants.
+     <productname>PostgreSQL</productname> has schema variables and
+     immutable schema variables. Schema variables can be created
+     by <command>CREATE VARIABLE</command> 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 d67270ccc3..8458cad752 100644
--- a/doc/src/sgml/ref/allfiles.sgml
+++ b/doc/src/sgml/ref/allfiles.sgml
@@ -47,6 +47,7 @@ Complete list of usable sgml source files in this directory.
 <!ENTITY alterType          SYSTEM "alter_type.sgml">
 <!ENTITY alterUser          SYSTEM "alter_user.sgml">
 <!ENTITY alterUserMapping   SYSTEM "alter_user_mapping.sgml">
+<!ENTITY alterVariable      SYSTEM "alter_variable.sgml">
 <!ENTITY alterView          SYSTEM "alter_view.sgml">
 <!ENTITY analyze            SYSTEM "analyze.sgml">
 <!ENTITY begin              SYSTEM "begin.sgml">
@@ -99,6 +100,7 @@ Complete list of usable sgml source files in this directory.
 <!ENTITY createType         SYSTEM "create_type.sgml">
 <!ENTITY createUser         SYSTEM "create_user.sgml">
 <!ENTITY createUserMapping  SYSTEM "create_user_mapping.sgml">
+<!ENTITY createVariable     SYSTEM "create_variable.sgml">
 <!ENTITY createView         SYSTEM "create_view.sgml">
 <!ENTITY deallocate         SYSTEM "deallocate.sgml">
 <!ENTITY declare            SYSTEM "declare.sgml">
@@ -148,6 +150,7 @@ Complete list of usable sgml source files in this directory.
 <!ENTITY dropUser           SYSTEM "drop_user.sgml">
 <!ENTITY dropUserMapping    SYSTEM "drop_user_mapping.sgml">
 <!ENTITY dropView           SYSTEM "drop_view.sgml">
+<!ENTITY dropVariable       SYSTEM "drop_variable.sgml">
 <!ENTITY end                SYSTEM "end.sgml">
 <!ENTITY execute            SYSTEM "execute.sgml">
 <!ENTITY explain            SYSTEM "explain.sgml">
@@ -155,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_default_privileges.sgml b/doc/src/sgml/ref/alter_default_privileges.sgml
index f1d54f5aa3..95e8352167 100644
--- a/doc/src/sgml/ref/alter_default_privileges.sgml
+++ b/doc/src/sgml/ref/alter_default_privileges.sgml
@@ -50,6 +50,10 @@ GRANT { USAGE | CREATE | ALL [ PRIVILEGES ] }
     ON SCHEMAS
     TO { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...] [ WITH GRANT OPTION ]
 
+GRANT { READ | WRITE | ALL [ PRIVILEGES ] }
+    ON VARIABLES
+    TO { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...] [ WITH GRANT OPTION ]
+
 REVOKE [ GRANT OPTION FOR ]
     { { SELECT | INSERT | UPDATE | DELETE | TRUNCATE | REFERENCES | TRIGGER }
     [, ...] | ALL [ PRIVILEGES ] }
@@ -81,6 +85,12 @@ REVOKE [ GRANT OPTION FOR ]
     ON SCHEMAS
     FROM { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...]
     [ CASCADE | RESTRICT ]
+
+REVOKE [ GRANT OPTION FOR ]
+    { { READ | WRITE } [, ...] | ALL [ PRIVILEGES ] }
+    ON VARIABLES
+    FROM { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...]
+    [ CASCADE | RESTRICT ]
 </synopsis>
  </refsynopsisdiv>
 
@@ -92,8 +102,8 @@ REVOKE [ GRANT OPTION FOR ]
    that will be applied to objects created in the future.  (It does not
    affect privileges assigned to already-existing objects.)  Currently,
    only the privileges for schemas, tables (including views and foreign
-   tables), sequences, functions, and types (including domains) can be
-   altered.  For this command, functions include aggregates and procedures.
+   tables), sequences, functions, types (including domains) and schema variables
+   can be altered.  For this command, functions include aggregates and procedures.
    The words <literal>FUNCTIONS</literal> and <literal>ROUTINES</literal> are
    equivalent in this command.  (<literal>ROUTINES</literal> is preferred
    going forward as the standard term for functions and procedures taken
diff --git a/doc/src/sgml/ref/alter_variable.sgml b/doc/src/sgml/ref/alter_variable.sgml
new file mode 100644
index 0000000000..85a38fb9d5
--- /dev/null
+++ b/doc/src/sgml/ref/alter_variable.sgml
@@ -0,0 +1,179 @@
+<!--
+doc/src/sgml/ref/alter_variable.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-altervariable">
+ <indexterm zone="sql-altervariable">
+  <primary>ALTER VARIABLE</primary>
+ </indexterm>
+
+ <indexterm>
+  <primary>schema variable</primary>
+  <secondary>altering</secondary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>ALTER VARIABLE</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>ALTER VARIABLE</refname>
+  <refpurpose>
+   change the definition of a variable
+  </refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+ALTER VARIABLE <replaceable class="parameter">name</replaceable> OWNER TO { <replaceable class="parameter">new_owner</replaceable> | CURRENT_USER | SESSION_USER }
+ALTER VARIABLE <replaceable class="parameter">name</replaceable> RENAME TO <replaceable class="parameter">new_name</replaceable>
+ALTER VARIABLE <replaceable class="parameter">name</replaceable> SET SCHEMA <replaceable class="parameter">new_schema</replaceable>
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   The <command>ALTER VARIABLE</command> command changes the definition of an existing
+   variable. There are several subforms:
+
+  <variablelist>
+   <varlistentry>
+    <term><literal>OWNER</literal></term>
+    <listitem>
+     <para>
+      This form changes the owner of the variable.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>RENAME</literal></term>
+    <listitem>
+     <para>
+      This form changes the name of the variable.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>SET SCHEMA</literal></term>
+    <listitem>
+     <para>
+      This form moves the variable into another schema.
+     </para>
+    </listitem>
+   </varlistentry>
+
+  </variablelist>
+  </para>
+
+  <para>
+   Only the owner or a superuser is allowed to alter a schema variable.
+   In order to move a schema variable from one schema to another, the user must
+   also have the <literal>CREATE</literal> privilege on the new schema (or be
+   a superuser).
+
+   In order to move the schema variable ownership from one role to another,
+   the user must also be a direct or indirect member of the new
+   owning role, and that role must have the <literal>CREATE</literal> privilege
+   on the variable's schema (or be a superuser). These restrictions enforce that
+   altering the owner doesn't do anything you couldn't do by dropping and
+   recreating the variable.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+   <para>
+    <variablelist>
+     <varlistentry>
+      <term><replaceable class="parameter">name</replaceable></term>
+      <listitem>
+       <para>
+        The name (possibly schema-qualified) of the existing variable to
+        alter.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><replaceable class="parameter">new_name</replaceable></term>
+      <listitem>
+       <para>
+        The new name for the variable.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><replaceable class="parameter">new_owner</replaceable></term>
+      <listitem>
+       <para>
+        The user name of the new owner of the variable.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><replaceable class="parameter">new_schema</replaceable></term>
+      <listitem>
+       <para>
+        The new schema for the variable.
+       </para>
+      </listitem>
+     </varlistentry>
+    </variablelist>
+   </para>
+  </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   To rename a variable:
+<programlisting>
+ALTER VARIABLE foo RENAME TO boo;
+</programlisting>
+  </para>
+
+  <para>
+   To change the owner of the variable <literal>boo</literal>
+   to <literal>joe</literal>:
+<programlisting>
+ALTER VARIABLE boo OWNER TO joe;
+</programlisting>
+  </para>
+
+  <para>
+   To change the schema of the variable <literal>boo</literal>
+   to <literal>private</literal>:
+<programlisting>
+ALTER VARIABLE boo SET SCHEMA private;
+</programlisting>
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   Schema variables and this command in particular are a PostgreSQL extension.
+  </para>
+ </refsect1>
+
+ <refsect1 id="sql-altervariable-see-also">
+  <title>See Also</title>
+
+  <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
new file mode 100644
index 0000000000..859c0bc2f1
--- /dev/null
+++ b/doc/src/sgml/ref/create_variable.sgml
@@ -0,0 +1,190 @@
+<!--
+doc/src/sgml/ref/create_variable.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-createvariable">
+ <indexterm zone="sql-createvariable">
+  <primary>CREATE VARIABLE</primary>
+ </indexterm>
+
+ <indexterm>
+  <primary>schema variable</primary>
+  <secondary>defining</secondary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>CREATE VARIABLE</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>CREATE VARIABLE</refname>
+  <refpurpose>define a schema variable</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+CREATE [ { TEMPORARY | TEMP } ] [ IMMUTABLE ] VARIABLE [ IF NOT EXISTS ] <replaceable class="parameter">name</replaceable> [ AS ] <replaceable class="parameter">data_type</replaceable> ] [ COLLATE <replaceable class="parameter">collation</replaceable> ]
+    [ NOT NULL ] [ DEFAULT <replaceable class="parameter">default_expr</replaceable> ] [ { ON COMMIT DROP | ON TRANSACTION END RESET } ]
+</synopsis>
+ </refsynopsisdiv>
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   The <command>CREATE VARIABLE</command> command creates a schema variable.
+   Schema variables, like relations, exist within a schema and their access is
+   controlled via <command>GRANT</command> and <command>REVOKE</command> commands.
+   Changing a schema variable is non-transactional.
+  </para>
+
+  <para>
+   The value of a schema variable is local to the current session. Retrieving
+   a variable's value returns either a NULL or a default value, unless its value
+   is set to something else in the current session with a LET command. The content
+   of a variable is not transactional. This is the same as in regular variables in PL languages.
+  </para>
+
+  <para>
+   Schema variables are retrieved by the <command>SELECT</command> SQL command.
+   Their value is set with the <command>LET</command> SQL command.
+   While schema variables share properties with tables, their value cannot be updated
+   with an <command>UPDATE</command> command.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+  <variablelist>
+   <varlistentry>
+    <term><literal>IMMUTABLE</literal></term>
+    <listitem>
+     <para>
+      The value of the variable cannot be changed.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>IF NOT EXISTS</literal></term>
+    <listitem>
+     <para>
+      Do not throw an error if the name already exists. A notice is issued in this case.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">name</replaceable></term>
+    <listitem>
+     <para>
+      The name, optionally schema-qualified, of the variable.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">data_type</replaceable></term>
+    <listitem>
+     <para>
+      The name, optionally schema-qualified, of the data type of the variable.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>COLLATE <replaceable>collation</replaceable></literal></term>
+    <listitem>
+     <para>
+      The <literal>COLLATE</literal> clause assigns a collation to
+      the variable (which must be of a collatable data type).
+      If not specified, the variable data type's default collation is used.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>NOT NULL</literal></term>
+    <listitem>
+     <para>
+      The <literal>NOT NULL</literal> clause forbids to set the variable to
+      a null value. A variable created as NOT NULL and without an explicitly
+      declared default value cannot be read until it is initialized by a LET
+      command. This obliges the user to explicitly initialize the variable
+      content before reading it.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>DEFAULT <replaceable>default_expr</replaceable></literal></term>
+    <listitem>
+     <para>
+      The <literal>DEFAULT</literal> clause can be used to assign a default value to
+      a schema variable.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>ON COMMIT DROP</literal>, <literal>ON TRANSACTION END RESET</literal></term>
+    <listitem>
+     <para>
+      The <literal>ON COMMIT DROP</literal> clause specifies the behaviour
+      of a temporary schema variable at transaction commit. With this clause the
+      variable is dropped at commit time. The clause is only allowed
+      for temporary variables. The <literal>ON TRANSACTION END RESET</literal>
+      clause enforces the variable to be reset to its default value when
+      the transaction is committed or rolled back.
+     </para>
+    </listitem>
+   </varlistentry>
+
+  </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Notes</title>
+
+  <para>
+   Use the <command>DROP VARIABLE</command> command to remove a variable.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   Create an integer variable <literal>var1</literal>:
+<programlisting>
+CREATE VARIABLE var1 AS date;
+LET var1 = current_date;
+SELECT var1;
+</programlisting>
+  </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   The <command>CREATE VARIABLE</command> command is a PostgreSQL extension.
+   <!-- The choice of wording here seems to be left to personal preference... -->
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-altervariable"/></member>
+   <member><xref linkend="sql-dropvariable"/></member>
+   <member><xref linkend="sql-let"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/ref/discard.sgml b/doc/src/sgml/ref/discard.sgml
index bf44c523ca..698b2c07fd 100644
--- a/doc/src/sgml/ref/discard.sgml
+++ b/doc/src/sgml/ref/discard.sgml
@@ -21,7 +21,7 @@ PostgreSQL documentation
 
  <refsynopsisdiv>
 <synopsis>
-DISCARD { ALL | PLANS | SEQUENCES | TEMPORARY | TEMP }
+DISCARD { ALL | PLANS | SEQUENCES | TEMPORARY | TEMP | VARIABLES }
 </synopsis>
  </refsynopsisdiv>
 
@@ -75,6 +75,17 @@ DISCARD { ALL | PLANS | SEQUENCES | TEMPORARY | TEMP }
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+    <term><literal>VARIABLES</literal></term>
+    <listitem>
+     <para>
+      Resets the value of all schema variables. If a variable
+      is later reused, it is re-initialized to either
+      NULL or its default value.
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry>
     <term><literal>ALL</literal></term>
     <listitem>
@@ -93,6 +104,7 @@ SELECT pg_advisory_unlock_all();
 DISCARD PLANS;
 DISCARD TEMP;
 DISCARD SEQUENCES;
+DISCARD VARIABLES;
 </programlisting></para>
     </listitem>
    </varlistentry>
diff --git a/doc/src/sgml/ref/drop_variable.sgml b/doc/src/sgml/ref/drop_variable.sgml
new file mode 100644
index 0000000000..da3632e780
--- /dev/null
+++ b/doc/src/sgml/ref/drop_variable.sgml
@@ -0,0 +1,120 @@
+<!--
+doc/src/sgml/ref/drop_variable.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-dropvariable">
+ <indexterm zone="sql-dropvariable">
+  <primary>DROP VARIABLE</primary>
+ </indexterm>
+
+ <indexterm>
+  <primary>schema variable</primary>
+  <secondary>removing</secondary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>DROP VARIABLE</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>DROP VARIABLE</refname>
+  <refpurpose>remove a schema variable</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+DROP VARIABLE [ IF EXISTS ] <replaceable class="parameter">name</replaceable> [, ...] [ CASCADE | RESTRICT ]
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>DROP VARIABLE</command> removes a schema variable.
+   A variable can only be dropped by its owner or a superuser.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+  <variablelist>
+   <varlistentry>
+    <term><literal>IF EXISTS</literal></term>
+    <listitem>
+     <para>
+      Do not throw an error if the variable does not exist. A notice is issued
+      in this case.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">name</replaceable></term>
+    <listitem>
+     <para>
+      The name, optionally schema-qualified, of a schema variable.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>CASCADE</literal></term>
+    <listitem>
+     <para>
+      Automatically drop objects that depend on the variable (such as
+      views),
+      and in turn all objects that depend on those objects
+      (see <xref linkend="ddl-depend"/>).
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>RESTRICT</literal></term>
+    <listitem>
+     <para>
+      Refuse to drop the variable if any objects depend on it.  This is
+      the default.
+     </para>
+    </listitem>
+   </varlistentry>
+  </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   To remove the schema variable <literal>var1</literal>:
+
+<programlisting>
+DROP VARIABLE var1;
+</programlisting></para>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   The <command>DROP VARIABLE</command> command is a PostgreSQL extension.
+   <!-- create variable is a "PostgreSQL feature",
+        this is a "proprietary PostgreSQL command" ... -->
+  </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-let"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/ref/grant.sgml b/doc/src/sgml/ref/grant.sgml
index a897712de2..6586e412c1 100644
--- a/doc/src/sgml/ref/grant.sgml
+++ b/doc/src/sgml/ref/grant.sgml
@@ -103,6 +103,11 @@ GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replace
   | CURRENT_ROLE
   | CURRENT_USER
   | SESSION_USER
+
+GRANT { READ | WRITE | ALL [ PRIVILEGES ] }
+    ON VARIABLE <replaceable>variable_name</replaceable> [, ...]
+    TO <replaceable class="parameter">role_specification</replaceable> [, ...] [ WITH GRANT OPTION ]
+
 </synopsis>
  </refsynopsisdiv>
 
@@ -201,6 +206,24 @@ GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replace
      </listitem>
     </varlistentry>
 
+    <varlistentry>
+     <term><literal>READ</literal></term>
+     <listitem>
+      <para>
+       Allows to read a schema variable.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>WRITE</literal></term>
+     <listitem>
+      <para>
+       Allows to set a schema variable's content.
+      </para>
+     </listitem>
+    </varlistentry>
+
     <varlistentry>
      <term><literal>ALL PRIVILEGES</literal></term>
      <listitem>
diff --git a/doc/src/sgml/ref/let.sgml b/doc/src/sgml/ref/let.sgml
new file mode 100644
index 0000000000..d4396cad3c
--- /dev/null
+++ b/doc/src/sgml/ref/let.sgml
@@ -0,0 +1,107 @@
+<!--
+doc/src/sgml/ref/let.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-let">
+ <indexterm zone="sql-let">
+  <primary>LET</primary>
+ </indexterm>
+
+ <indexterm>
+  <primary>schema 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 schema variable's value</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+LET <replaceable class="parameter">schema_variable</replaceable> = <replaceable class="parameter">sql_expression</replaceable>
+LET <replaceable class="parameter">schema_variable</replaceable> = DEFAULT
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   The <command>LET</command> command updates the specified schema variable value.
+  </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+  <variablelist>
+   <varlistentry>
+    <term><literal>schema_variable</literal></term>
+    <listitem>
+     <para>
+      The name of the schema variable.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>sql_expression</literal></term>
+    <listitem>
+     <para>
+      An SQL expression. The result is cast into the schema variable's type.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>DEFAULT</literal></term>
+    <listitem>
+     <para>
+      Reset the schema variable to its default value, if that is defined.
+      If no explicit default value has been assigned, the schema variable
+      is set to NULL.
+     </para>
+    </listitem>
+   </varlistentry>
+  </variablelist>
+
+  <para>
+   Example:
+<programlisting>
+CREATE VARIABLE myvar AS integer;
+LET myvar = 10;
+LET myvar = (SELECT sum(val) FROM tab);
+LET myvar = DEFAULT;
+</programlisting>
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   <literal>LET</literal> extends the syntax defined in the SQL
+   standard. The <literal>SET</literal> command from the SQL standard
+   is used for different purposes in PostgreSQL.
+  </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/ref/pg_restore.sgml b/doc/src/sgml/ref/pg_restore.sgml
index 93ea937ac8..0d86239eb4 100644
--- a/doc/src/sgml/ref/pg_restore.sgml
+++ b/doc/src/sgml/ref/pg_restore.sgml
@@ -106,6 +106,17 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>-A <replaceable class="parameter">schema_variable</replaceable></option></term>
+      <term><option>--variable=<replaceable class="parameter">schema_variable</replaceable></option></term>
+      <listitem>
+       <para>
+        Restore a named schema variable only.  Multiple schema variables may be specified with
+        multiple <option>-A</option> switches.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>-c</option></term>
       <term><option>--clean</option></term>
diff --git a/doc/src/sgml/ref/revoke.sgml b/doc/src/sgml/ref/revoke.sgml
index 3014c864ea..3e9f9804be 100644
--- a/doc/src/sgml/ref/revoke.sgml
+++ b/doc/src/sgml/ref/revoke.sgml
@@ -130,6 +130,12 @@ REVOKE [ ADMIN OPTION FOR ]
   | CURRENT_ROLE
   | CURRENT_USER
   | SESSION_USER
+
+REVOKE [ GRANT OPTION FOR ]
+    { { READ | WRITE } [, ...] | ALL [ PRIVILEGES ] }
+    ON VARIABLE <replaceable>variable_name</replaceable> [, ...]
+    FROM { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...]
+    [ CASCADE | RESTRICT ]
 </synopsis>
  </refsynopsisdiv>
 
diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml
index da421ff24e..f9e42443b6 100644
--- a/doc/src/sgml/reference.sgml
+++ b/doc/src/sgml/reference.sgml
@@ -75,6 +75,7 @@
    &alterType;
    &alterUser;
    &alterUserMapping;
+   &alterVariable;
    &alterView;
    &analyze;
    &begin;
@@ -127,6 +128,7 @@
    &createType;
    &createUser;
    &createUserMapping;
+   &createVariable;
    &createView;
    &deallocate;
    &declare;
@@ -175,6 +177,7 @@
    &dropType;
    &dropUser;
    &dropUserMapping;
+   &dropVariable;
    &dropView;
    &end;
    &execute;
@@ -183,6 +186,7 @@
    &grant;
    &importForeignSchema;
    &insert;
+   &let;
    &listen;
    &load;
    &lock;
diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c
index 441445927e..a702a9760f 100644
--- a/src/backend/access/transam/xact.c
+++ b/src/backend/access/transam/xact.c
@@ -35,6 +35,7 @@
 #include "catalog/pg_enum.h"
 #include "catalog/storage.h"
 #include "commands/async.h"
+#include "commands/schema_variable.h"
 #include "commands/tablecmds.h"
 #include "commands/trigger.h"
 #include "executor/spi.h"
@@ -2145,6 +2146,9 @@ CommitTransaction(void)
 	 */
 	smgrDoPendingSyncs(true, is_parallel_worker);
 
+	/* Let ON COMMIT DROP or ON TRANSACTION END */
+	AtPreEOXact_SchemaVariable_on_commit_actions(true);
+
 	/* close large objects before lower-level cleanup */
 	AtEOXact_LargeObject(true);
 
@@ -2274,6 +2278,7 @@ CommitTransaction(void)
 	AtEOXact_SPI(true);
 	AtEOXact_Enum();
 	AtEOXact_on_commit_actions(true);
+	AtEOXact_SchemaVariable_on_commit_actions(true);
 	AtEOXact_Namespace(true, is_parallel_worker);
 	AtEOXact_SMgr();
 	AtEOXact_Files(true);
@@ -2702,6 +2707,9 @@ AbortTransaction(void)
 	AtAbort_Portals();
 	smgrDoPendingSyncs(false, is_parallel_worker);
 	AtEOXact_LargeObject(false);
+
+	/* 'false' means it's abort */
+	AtPreEOXact_SchemaVariable_on_commit_actions(false);
 	AtAbort_Notify();
 	AtEOXact_RelationMap(false, is_parallel_worker);
 	AtAbort_Twophase();
@@ -2766,6 +2774,7 @@ AbortTransaction(void)
 		AtEOXact_SPI(false);
 		AtEOXact_Enum();
 		AtEOXact_on_commit_actions(false);
+		AtEOXact_SchemaVariable_on_commit_actions(false);
 		AtEOXact_Namespace(false, is_parallel_worker);
 		AtEOXact_SMgr();
 		AtEOXact_Files(false);
diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile
index e36a9602c1..cc52bf7cfe 100644
--- a/src/backend/catalog/Makefile
+++ b/src/backend/catalog/Makefile
@@ -42,6 +42,7 @@ OBJS = \
 	pg_shdepend.o \
 	pg_subscription.o \
 	pg_type.o \
+	pg_variable.o \
 	storage.o \
 	toasting.o
 
@@ -68,7 +69,7 @@ CATALOG_HEADERS := \
 	pg_default_acl.h pg_init_privs.h pg_seclabel.h pg_shseclabel.h \
 	pg_collation.h pg_partitioned_table.h pg_range.h pg_transform.h \
 	pg_sequence.h pg_publication.h pg_publication_rel.h pg_subscription.h \
-	pg_subscription_rel.h
+	pg_subscription_rel.h pg_variable.h
 
 GENERATED_HEADERS := $(CATALOG_HEADERS:%.h=%_d.h) schemapg.h system_fk_info.h
 
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index e1573eb398..c7f5796bcc 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -58,6 +58,7 @@
 #include "catalog/pg_ts_parser.h"
 #include "catalog/pg_ts_template.h"
 #include "catalog/pg_type.h"
+#include "catalog/pg_variable.h"
 #include "commands/dbcommands.h"
 #include "commands/event_trigger.h"
 #include "commands/extension.h"
@@ -111,6 +112,7 @@ static void ExecGrant_Largeobject(InternalGrant *grantStmt);
 static void ExecGrant_Namespace(InternalGrant *grantStmt);
 static void ExecGrant_Tablespace(InternalGrant *grantStmt);
 static void ExecGrant_Type(InternalGrant *grantStmt);
+static void ExecGrant_Variable(InternalGrant *grantStmt);
 
 static void SetDefaultACLsInSchemas(InternalDefaultACL *iacls, List *nspnames);
 static void SetDefaultACL(InternalDefaultACL *iacls);
@@ -258,6 +260,9 @@ restrict_and_check_grant(bool is_grant, AclMode avail_goptions, bool all_privs,
 		case OBJECT_TYPE:
 			whole_mask = ACL_ALL_RIGHTS_TYPE;
 			break;
+		case OBJECT_VARIABLE:
+			whole_mask = ACL_ALL_RIGHTS_VARIABLE;
+			break;
 		default:
 			elog(ERROR, "unrecognized object type: %d", objtype);
 			/* not reached, but keep compiler quiet */
@@ -497,6 +502,10 @@ ExecuteGrantStmt(GrantStmt *stmt)
 			all_privileges = ACL_ALL_RIGHTS_FOREIGN_SERVER;
 			errormsg = gettext_noop("invalid privilege type %s for foreign server");
 			break;
+		case OBJECT_VARIABLE:
+			all_privileges = ACL_ALL_RIGHTS_VARIABLE;
+			errormsg = gettext_noop("invalid privilege type %s for schema variable");
+			break;
 		default:
 			elog(ERROR, "unrecognized GrantStmt.objtype: %d",
 				 (int) stmt->objtype);
@@ -599,6 +608,9 @@ ExecGrantStmt_oids(InternalGrant *istmt)
 		case OBJECT_TABLESPACE:
 			ExecGrant_Tablespace(istmt);
 			break;
+		case OBJECT_VARIABLE:
+			ExecGrant_Variable(istmt);
+			break;
 		default:
 			elog(ERROR, "unrecognized GrantStmt.objtype: %d",
 				 (int) istmt->objtype);
@@ -758,6 +770,16 @@ objectNamesToOids(ObjectType objtype, List *objnames)
 				objects = lappend_oid(objects, srvid);
 			}
 			break;
+		case OBJECT_VARIABLE:
+			foreach(cell, objnames)
+			{
+				RangeVar   *varvar = (RangeVar *) lfirst(cell);
+				Oid			relOid;
+
+				relOid = LookupVariable(varvar->schemaname, varvar->relname, false);
+				objects = lappend_oid(objects, relOid);
+			}
+			break;
 		default:
 			elog(ERROR, "unrecognized GrantStmt.objtype: %d",
 				 (int) objtype);
@@ -847,6 +869,33 @@ objectsInSchemaToOids(ObjectType objtype, List *nspnames)
 					table_close(rel, AccessShareLock);
 				}
 				break;
+			case OBJECT_VARIABLE:
+				{
+					ScanKeyData key;
+					Relation	rel;
+					TableScanDesc scan;
+					HeapTuple	tuple;
+
+					ScanKeyInit(&key,
+								Anum_pg_variable_varnamespace,
+								BTEqualStrategyNumber, F_OIDEQ,
+								ObjectIdGetDatum(namespaceId));
+
+					rel = table_open(VariableRelationId, AccessShareLock);
+					scan = table_beginscan_catalog(rel, 1, &key);
+
+					while ((tuple = heap_getnext(scan, ForwardScanDirection)) != NULL)
+					{
+						Oid			oid = ((Form_pg_proc) GETSTRUCT(tuple))->oid;
+
+						objects = lappend_oid(objects, oid);
+					}
+
+					table_endscan(scan);
+					table_close(rel, AccessShareLock);
+				}
+				break;
+
 			default:
 				/* should not happen */
 				elog(ERROR, "unrecognized GrantStmt.objtype: %d",
@@ -1012,6 +1061,10 @@ ExecAlterDefaultPrivilegesStmt(ParseState *pstate, AlterDefaultPrivilegesStmt *s
 			all_privileges = ACL_ALL_RIGHTS_SCHEMA;
 			errormsg = gettext_noop("invalid privilege type %s for schema");
 			break;
+		case OBJECT_VARIABLE:
+			all_privileges = ACL_ALL_RIGHTS_VARIABLE;
+			errormsg = gettext_noop("invalid privilege type %s for schema variable");
+			break;
 		default:
 			elog(ERROR, "unrecognized GrantStmt.objtype: %d",
 				 (int) action->objtype);
@@ -1209,6 +1262,12 @@ SetDefaultACL(InternalDefaultACL *iacls)
 				this_privileges = ACL_ALL_RIGHTS_SCHEMA;
 			break;
 
+		case OBJECT_VARIABLE:
+			objtype = DEFACLOBJ_VARIABLE;
+			if (iacls->all_privs && this_privileges == ACL_NO_RIGHTS)
+				this_privileges = ACL_ALL_RIGHTS_VARIABLE;
+			break;
+
 		default:
 			elog(ERROR, "unrecognized objtype: %d",
 				 (int) iacls->objtype);
@@ -1442,6 +1501,9 @@ RemoveRoleFromObjectACL(Oid roleid, Oid classid, Oid objid)
 			case DEFACLOBJ_NAMESPACE:
 				iacls.objtype = OBJECT_SCHEMA;
 				break;
+			case DEFACLOBJ_VARIABLE:
+				iacls.objtype = OBJECT_VARIABLE;
+				break;
 			default:
 				/* Shouldn't get here */
 				elog(ERROR, "unexpected default ACL type: %d",
@@ -1499,6 +1561,9 @@ RemoveRoleFromObjectACL(Oid roleid, Oid classid, Oid objid)
 			case ForeignDataWrapperRelationId:
 				istmt.objtype = OBJECT_FDW;
 				break;
+			case VariableRelationId:
+				istmt.objtype = OBJECT_VARIABLE;
+				break;
 			default:
 				elog(ERROR, "unexpected object class %u", classid);
 				break;
@@ -3230,6 +3295,129 @@ ExecGrant_Type(InternalGrant *istmt)
 	table_close(relation, RowExclusiveLock);
 }
 
+static void
+ExecGrant_Variable(InternalGrant *istmt)
+{
+	Relation	relation;
+	ListCell   *cell;
+
+	if (istmt->all_privs && istmt->privileges == ACL_NO_RIGHTS)
+		istmt->privileges = ACL_ALL_RIGHTS_VARIABLE;
+
+	relation = table_open(VariableRelationId, RowExclusiveLock);
+
+	foreach(cell, istmt->objects)
+	{
+		Oid			varId = lfirst_oid(cell);
+		Form_pg_variable pg_variable_tuple;
+		Datum		aclDatum;
+		bool		isNull;
+		AclMode		avail_goptions;
+		AclMode		this_privileges;
+		Acl		   *old_acl;
+		Acl		   *new_acl;
+		Oid			grantorId;
+		Oid			ownerId;
+		HeapTuple	tuple;
+		HeapTuple	newtuple;
+		Datum		values[Natts_pg_variable];
+		bool		nulls[Natts_pg_variable];
+		bool		replaces[Natts_pg_variable];
+		int			noldmembers;
+		int			nnewmembers;
+		Oid		   *oldmembers;
+		Oid		   *newmembers;
+
+		tuple = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varId));
+		if (!HeapTupleIsValid(tuple))
+			elog(ERROR, "cache lookup failed for schema variables %u", varId);
+
+		pg_variable_tuple = (Form_pg_variable) GETSTRUCT(tuple);
+
+		/*
+		 * Get owner ID and working copy of existing ACL. If there's no ACL,
+		 * substitute the proper default.
+		 */
+		ownerId = pg_variable_tuple->varowner;
+		aclDatum = SysCacheGetAttr(VARIABLEOID, tuple, Anum_pg_variable_varacl,
+								   &isNull);
+		if (isNull)
+		{
+			old_acl = acldefault(OBJECT_VARIABLE, ownerId);
+			/* There are no old member roles according to the catalogs */
+			noldmembers = 0;
+			oldmembers = NULL;
+		}
+		else
+		{
+			old_acl = DatumGetAclPCopy(aclDatum);
+			/* Get the roles mentioned in the existing ACL */
+			noldmembers = aclmembers(old_acl, &oldmembers);
+		}
+
+		/* Determine ID to do the grant as, and available grant options */
+		select_best_grantor(GetUserId(), istmt->privileges,
+							old_acl, ownerId,
+							&grantorId, &avail_goptions);
+
+		/*
+		 * Restrict the privileges to what we can actually grant, and emit the
+		 * standards-mandated warning and error messages.
+		 */
+		this_privileges =
+			restrict_and_check_grant(istmt->is_grant, avail_goptions,
+									 istmt->all_privs, istmt->privileges,
+									 varId, grantorId, OBJECT_VARIABLE,
+									 NameStr(pg_variable_tuple->varname),
+									 0, NULL);
+
+		/*
+		 * Generate new ACL.
+		 */
+		new_acl = merge_acl_with_grant(old_acl, istmt->is_grant,
+									   istmt->grant_option, istmt->behavior,
+									   istmt->grantees, this_privileges,
+									   grantorId, ownerId);
+
+		/*
+		 * We need the members of both old and new ACLs so we can correct the
+		 * shared dependency information.
+		 */
+		nnewmembers = aclmembers(new_acl, &newmembers);
+
+		/* finished building new ACL value, now insert it */
+		MemSet(values, 0, sizeof(values));
+		MemSet(nulls, false, sizeof(nulls));
+		MemSet(replaces, false, sizeof(replaces));
+
+		replaces[Anum_pg_variable_varacl - 1] = true;
+		values[Anum_pg_variable_varacl - 1] = PointerGetDatum(new_acl);
+
+		newtuple = heap_modify_tuple(tuple, RelationGetDescr(relation), values,
+									 nulls, replaces);
+
+		CatalogTupleUpdate(relation, &newtuple->t_self, newtuple);
+
+		/* Update initial privileges for extensions */
+		recordExtensionInitPriv(varId, VariableRelationId, 0, new_acl);
+
+		/* Update the shared dependency ACL info */
+		updateAclDependencies(VariableRelationId, varId, 0,
+							  ownerId,
+							  noldmembers, oldmembers,
+							  nnewmembers, newmembers);
+
+		ReleaseSysCache(tuple);
+
+		pfree(new_acl);
+
+		/* prevent error when processing duplicate objects */
+		CommandCounterIncrement();
+	}
+
+	table_close(relation, RowExclusiveLock);
+}
+
 
 static AclMode
 string_to_privilege(const char *privname)
@@ -3262,6 +3450,10 @@ string_to_privilege(const char *privname)
 		return ACL_CONNECT;
 	if (strcmp(privname, "rule") == 0)
 		return 0;				/* ignore old RULE privileges */
+	if (strcmp(privname, "read") == 0)
+		return ACL_READ;
+	if (strcmp(privname, "write") == 0)
+		return ACL_WRITE;
 	ereport(ERROR,
 			(errcode(ERRCODE_SYNTAX_ERROR),
 			 errmsg("unrecognized privilege type \"%s\"", privname)));
@@ -3297,6 +3489,10 @@ privilege_to_string(AclMode privilege)
 			return "TEMP";
 		case ACL_CONNECT:
 			return "CONNECT";
+		case ACL_READ:
+			return "READ";
+		case ACL_WRITE:
+			return "WRITE";
 		default:
 			elog(ERROR, "unrecognized privilege: %d", (int) privilege);
 	}
@@ -3420,6 +3616,9 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
 					case OBJECT_TYPE:
 						msg = gettext_noop("permission denied for type %s");
 						break;
+					case OBJECT_VARIABLE:
+						msg = gettext_noop("permission denied for schema variable %s");
+						break;
 					case OBJECT_VIEW:
 						msg = gettext_noop("permission denied for view %s");
 						break;
@@ -3530,6 +3729,9 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
 					case OBJECT_TYPE:
 						msg = gettext_noop("must be owner of type %s");
 						break;
+					case OBJECT_VARIABLE:
+						msg = gettext_noop("must be owner of schema variable %s");
+						break;
 					case OBJECT_VIEW:
 						msg = gettext_noop("must be owner of view %s");
 						break;
@@ -3674,6 +3876,8 @@ pg_aclmask(ObjectType objtype, Oid table_oid, AttrNumber attnum, Oid roleid,
 			return ACL_NO_RIGHTS;
 		case OBJECT_TYPE:
 			return pg_type_aclmask(table_oid, roleid, mask, how);
+		case OBJECT_VARIABLE:
+			return pg_variable_aclmask(table_oid, roleid, mask, how);
 		default:
 			elog(ERROR, "unrecognized objtype: %d",
 				 (int) objtype);
@@ -4542,6 +4746,66 @@ pg_type_aclmask(Oid type_oid, Oid roleid, AclMode mask, AclMaskHow how)
 	return result;
 }
 
+/*
+ * Exported routine for examining a user's privileges for a variable.
+ */
+AclMode
+pg_variable_aclmask(Oid var_oid, Oid roleid, AclMode mask, AclMaskHow how)
+{
+	AclMode		result;
+	HeapTuple	tuple;
+	Datum		aclDatum;
+	bool		isNull;
+	Acl		   *acl;
+	Oid			ownerId;
+
+	Form_pg_variable varForm;
+
+	/* Bypass permission checks for superusers */
+	if (superuser_arg(roleid))
+		return mask;
+
+	/*
+	 * Must get the type's tuple from pg_type
+	 */
+	tuple = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(var_oid));
+	if (!HeapTupleIsValid(tuple))
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("variable with OID %u does not exist",
+						var_oid)));
+	varForm = (Form_pg_variable) GETSTRUCT(tuple);
+
+	/*
+	 * Now get the type's owner and ACL from the tuple
+	 */
+	ownerId = varForm->varowner;
+
+	aclDatum = SysCacheGetAttr(VARIABLEOID, tuple,
+							   Anum_pg_variable_varacl, &isNull);
+	if (isNull)
+	{
+		/* No ACL, so build default ACL */
+		acl = acldefault(OBJECT_VARIABLE, ownerId);
+		aclDatum = (Datum) 0;
+	}
+	else
+	{
+		/* detoast rel's ACL if necessary */
+		acl = DatumGetAclP(aclDatum);
+	}
+
+	result = aclmask(acl, roleid, ownerId, mask, how);
+
+	/* if we have a detoasted copy, free it */
+	if (acl && (Pointer) acl != DatumGetPointer(aclDatum))
+		pfree(acl);
+
+	ReleaseSysCache(tuple);
+
+	return result;
+}
+
 /*
  * Exported routine for checking a user's access privileges to a column
  *
@@ -4816,6 +5080,18 @@ pg_type_aclcheck(Oid type_oid, Oid roleid, AclMode mode)
 		return ACLCHECK_NO_PRIV;
 }
 
+/*
+ * Exported routine for checking a user's access privileges to a variable
+ */
+AclResult
+pg_variable_aclcheck(Oid type_oid, Oid roleid, AclMode mode)
+{
+	if (pg_variable_aclmask(type_oid, roleid, mode, ACLMASK_ANY) != 0)
+		return ACLCHECK_OK;
+	else
+		return ACLCHECK_NO_PRIV;
+}
+
 /*
  * Ownership check for a relation (specified by OID).
  */
@@ -5433,6 +5709,33 @@ pg_statistics_object_ownercheck(Oid stat_oid, Oid roleid)
 	return has_privs_of_role(roleid, ownerId);
 }
 
+/*
+ * Ownership check for a schema variables (specified by OID).
+ */
+bool
+pg_variable_ownercheck(Oid db_oid, Oid roleid)
+{
+	HeapTuple	tuple;
+	Oid			ownerId;
+
+	/* Superusers bypass all permission checking. */
+	if (superuser_arg(roleid))
+		return true;
+
+	tuple = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(db_oid));
+	if (!HeapTupleIsValid(tuple))
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_DATABASE),
+				 errmsg("variable with OID %u does not exist", db_oid)));
+
+	ownerId = ((Form_pg_variable) GETSTRUCT(tuple))->varowner;
+
+	ReleaseSysCache(tuple);
+
+	return has_privs_of_role(roleid, ownerId);
+}
+
+
 /*
  * Check whether specified role has CREATEROLE privilege (or is a superuser)
  *
@@ -5561,6 +5864,10 @@ get_user_default_acl(ObjectType objtype, Oid ownerId, Oid nsp_oid)
 			defaclobjtype = DEFACLOBJ_NAMESPACE;
 			break;
 
+		case OBJECT_VARIABLE:
+			defaclobjtype = DEFACLOBJ_VARIABLE;
+			break;
+
 		default:
 			return NULL;
 	}
diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c
index 8d8e926c21..7ce7c38532 100644
--- a/src/backend/catalog/dependency.c
+++ b/src/backend/catalog/dependency.c
@@ -61,12 +61,15 @@
 #include "catalog/pg_ts_template.h"
 #include "catalog/pg_type.h"
 #include "catalog/pg_user_mapping.h"
+#include "catalog/pg_variable.h"
 #include "commands/comment.h"
 #include "commands/defrem.h"
 #include "commands/event_trigger.h"
 #include "commands/extension.h"
 #include "commands/policy.h"
 #include "commands/publicationcmds.h"
+#include "commands/schemacmds.h"
+#include "commands/schema_variable.h"
 #include "commands/seclabel.h"
 #include "commands/sequence.h"
 #include "commands/trigger.h"
@@ -181,7 +184,8 @@ static const Oid object_classes[] = {
 	PublicationRelationId,		/* OCLASS_PUBLICATION */
 	PublicationRelRelationId,	/* OCLASS_PUBLICATION_REL */
 	SubscriptionRelationId,		/* OCLASS_SUBSCRIPTION */
-	TransformRelationId			/* OCLASS_TRANSFORM */
+	TransformRelationId,		/* OCLASS_TRANSFORM */
+	VariableRelationId			/* OCLASS_VARIABLE */
 };

@@ -1572,6 +1576,10 @@ doDeletion(const ObjectAddress *object, int flags)
 			DropObjectById(object);
 			break;
 
+		case OCLASS_VARIABLE:
+			RemoveSchemaVariable(object->objectId);
+			break;
+
 			/*
 			 * These global object types are not supported here.
 			 */
@@ -2010,6 +2018,11 @@ find_expr_references_walker(Node *node,
 	{
 		Param	   *param = (Param *) node;
 
+		if (param->paramkind == PARAM_VARIABLE)
+			/* A variable parameter depends on the schema 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);
@@ -3002,6 +3015,9 @@ getObjectClass(const ObjectAddress *object)
 
 		case TransformRelationId:
 			return OCLASS_TRANSFORM;
+
+		case VariableRelationId:
+			return OCLASS_VARIABLE;
 	}
 
 	/* shouldn't get here */
diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c
index 005e029c38..1f2b207687 100644
--- a/src/backend/catalog/namespace.c
+++ b/src/backend/catalog/namespace.c
@@ -39,6 +39,7 @@
 #include "catalog/pg_ts_parser.h"
 #include "catalog/pg_ts_template.h"
 #include "catalog/pg_type.h"
+#include "catalog/pg_variable.h"
 #include "commands/dbcommands.h"
 #include "funcapi.h"
 #include "mb/pg_wchar.h"
@@ -762,6 +763,70 @@ RelationIsVisible(Oid relid)
 	return visible;
 }
 
+/*
+ * VariableIsVisible
+ *		Determine whether a variable (identified by OID) is visible in the
+ *		current search path.  Visible means "would be found by searching
+ *		for the unqualified variable name".
+ */
+bool
+VariableIsVisible(Oid varid)
+{
+	HeapTuple	vartup;
+	Form_pg_variable varform;
+	Oid			varnamespace;
+	bool		visible;
+
+	vartup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid));
+	if (!HeapTupleIsValid(vartup))
+		elog(ERROR, "cache lookup failed for schema variable %u", varid);
+	varform = (Form_pg_variable) GETSTRUCT(vartup);
+
+	recomputeNamespacePath();
+
+	/*
+	 * Quick check: if it ain't in the path at all, it ain't visible. Items in
+	 * the system namespace are surely in the path and so we needn't even do
+	 * list_member_oid() for them.
+	 */
+	varnamespace = varform->varnamespace;
+	if (varnamespace != PG_CATALOG_NAMESPACE &&
+		!list_member_oid(activeSearchPath, varnamespace))
+		visible = false;
+	else
+	{
+		/*
+		 * If it is in the path, it might still not be visible; it could be
+		 * hidden by another relation of the same name earlier in the path. So
+		 * we must do a slow check for conflicting relations.
+		 */
+		char	   *varname = NameStr(varform->varname);
+		ListCell   *l;
+
+		visible = false;
+		foreach(l, activeSearchPath)
+		{
+			Oid			namespaceId = lfirst_oid(l);
+
+			if (namespaceId == varnamespace)
+			{
+				/* Found it first in path */
+				visible = true;
+				break;
+			}
+			if (OidIsValid(get_varname_varid(varname, namespaceId)))
+			{
+				/* Found something else first in path */
+				break;
+			}
+		}
+	}
+
+	ReleaseSysCache(vartup);
+
+	return visible;
+}
+
 
 /*
  * TypenameGetTypid
@@ -2796,6 +2861,209 @@ TSConfigIsVisible(Oid cfgid)
 	return visible;
 }
 
+/*
+ * When we know a variable name, then we can find variable simply
+ */
+Oid
+LookupVariable(const char *nspname, const char *varname, bool missing_ok)
+{
+	Oid			namespaceId;
+	Oid			varoid = InvalidOid;
+	ListCell   *l;
+
+	if (nspname)
+	{
+		namespaceId = LookupExplicitNamespace(nspname, missing_ok);
+		if (!OidIsValid(namespaceId))
+			return InvalidOid;
+
+		varoid = GetSysCacheOid2(VARIABLENAMENSP, Anum_pg_variable_oid,
+								 PointerGetDatum(varname),
+								 ObjectIdGetDatum(namespaceId));
+	}
+	else
+	{
+		/* search for it in search path */
+		recomputeNamespacePath();
+
+		foreach(l, activeSearchPath)
+		{
+			namespaceId = lfirst_oid(l);
+
+			varoid = GetSysCacheOid2(VARIABLENAMENSP, Anum_pg_variable_oid,
+									 PointerGetDatum(varname),
+									 ObjectIdGetDatum(namespaceId));
+
+			if (OidIsValid(varoid))
+				break;
+		}
+	}
+
+	if (!OidIsValid(varoid) && !missing_ok)
+	{
+		if (nspname)
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("variable \"%s\".\"%s\" does not exist",
+							nspname, varname)));
+		else
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("variable \"%s\" does not exist",
+							varname)));
+	}
+
+	return varoid;
+}
+
+/*
+ * The source list holds names with indirection expressions used
+ * as left part of LET statement. Following routine makes 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
+ *
+ * Returns oid of not ambigonuous variable specified by qualified path
+ * or InvalidOid. When the path is ambigonuous, then not_uniq flag is
+ * is true.
+ */
+Oid
+IdentifyVariable(List *names, char **attrname, bool *not_uniq)
+{
+	char	   *a = NULL;
+	char	   *b = NULL;
+	char	   *c = NULL;
+	char	   *d = NULL;
+	Oid			varoid_without_attr;
+	Oid			varoid_with_attr;
+
+	*not_uniq = false;
+
+	switch (list_length(names))
+	{
+		case 1:
+			a = strVal(linitial(names));
+			return LookupVariable(NULL, a, true);
+
+		case 2:
+			a = strVal(linitial(names));
+			b = strVal(lsecond(names));
+
+			/*
+			 * a.b can mean "schema"."variable" or "variable"."field", Check
+			 * both variants, and returns InvalidOid with not_uniq flag, when
+			 * both interpretations are possible.
+			 */
+			varoid_without_attr = LookupVariable(a, b, true);
+			varoid_with_attr = LookupVariable(NULL, a, true);
+
+			if (OidIsValid(varoid_without_attr) && OidIsValid(varoid_with_attr))
+			{
+				*not_uniq = true;
+				return InvalidOid;
+			}
+			else if (OidIsValid(varoid_without_attr))
+			{
+				*attrname = NULL;
+				return varoid_without_attr;
+			}
+			else
+			{
+				*attrname = b;
+				return varoid_with_attr;
+			}
+			break;
+
+		case 3:
+			a = strVal(linitial(names));
+			b = strVal(lsecond(names));
+			c = strVal(lthird(names));
+
+			/*
+			 * a.b.c can mean "catalog"."schema"."variable" or
+			 * "schema"."variable"."field", Check both variants, and returns
+			 * InvalidOid with not_uniq flag, when both interpretations are
+			 * possible.
+			 */
+			varoid_without_attr = LookupVariable(b, c, true);
+			varoid_with_attr = LookupVariable(a, b, true);
+
+			if (OidIsValid(varoid_without_attr) && OidIsValid(varoid_with_attr))
+			{
+				*not_uniq = true;
+				return InvalidOid;
+			}
+			else if (OidIsValid(varoid_without_attr))
+			{
+				*attrname = NULL;
+
+				/*
+				 * We in this case a "a" is used as catalog name, check it.
+				 */
+				if (strcmp(a, get_database_name(MyDatabaseId)) != 0)
+					ereport(ERROR,
+							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+							 errmsg("cross-database references are not implemented: %s",
+									NameListToString(names))));
+
+				return varoid_without_attr;
+			}
+			else
+			{
+				*attrname = c;
+				return varoid_with_attr;
+			}
+			break;
+
+		case 4:
+			a = strVal(linitial(names));
+			b = strVal(lsecond(names));
+			c = strVal(lthird(names));
+			d = strVal(lfourth(names));
+
+			/*
+			 * We in this case a "a" is used as catalog name, check it.
+			 */
+			if (strcmp(a, get_database_name(MyDatabaseId)) != 0)
+				ereport(ERROR,
+						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+						 errmsg("cross-database references are not implemented: %s",
+								NameListToString(names))));
+
+			*attrname = d;
+			return LookupVariable(b, c, true);
+
+		default:
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("improper qualified name (too many dotted names): %s",
+							NameListToString(names))));
+			break;
+	}
+}
 
 /*
  * DeconstructQualifiedName
@@ -4611,3 +4879,14 @@ pg_is_other_temp_schema(PG_FUNCTION_ARGS)
 
 	PG_RETURN_BOOL(isOtherTempNamespace(oid));
 }
+
+Datum
+pg_variable_is_visible(PG_FUNCTION_ARGS)
+{
+	Oid			oid = PG_GETARG_OID(0);
+
+	if (!SearchSysCacheExists1(VARIABLEOID, ObjectIdGetDatum(oid)))
+		PG_RETURN_NULL();
+
+	PG_RETURN_BOOL(VariableIsVisible(oid));
+}
diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c
index 6d88b690d8..e4df1834dc 100644
--- a/src/backend/catalog/objectaddress.c
+++ b/src/backend/catalog/objectaddress.c
@@ -61,6 +61,7 @@
 #include "catalog/pg_ts_template.h"
 #include "catalog/pg_type.h"
 #include "catalog/pg_user_mapping.h"
+#include "catalog/pg_variable.h"
 #include "commands/dbcommands.h"
 #include "commands/defrem.h"
 #include "commands/event_trigger.h"
@@ -615,6 +616,20 @@ static const ObjectPropertyType ObjectProperty[] =
 		OBJECT_USER_MAPPING,
 		false
 	},
+	{
+		"schema variable",
+		VariableRelationId,
+		VariableObjectIndexId,
+		VARIABLEOID,
+		VARIABLENAMENSP,
+		Anum_pg_variable_oid,
+		Anum_pg_variable_varname,
+		Anum_pg_variable_varnamespace,
+		Anum_pg_variable_varowner,
+		Anum_pg_variable_varacl,
+		OBJECT_VARIABLE,
+		true
+	}
 };
 
 /*
@@ -839,6 +854,10 @@ static const struct object_type_map
 	/* OCLASS_STATISTIC_EXT */
 	{
 		"statistics object", OBJECT_STATISTIC_EXT
+	},
+	/* OCLASS_VARIABLE */
+	{
+		"schema variable", OBJECT_VARIABLE
 	}
 };
 
@@ -864,6 +883,7 @@ static ObjectAddress get_object_address_attrdef(ObjectType objtype,
 												bool missing_ok);
 static ObjectAddress get_object_address_type(ObjectType objtype,
 											 TypeName *typename, bool missing_ok);
+static ObjectAddress get_object_address_variable(List *object, bool missing_ok);
 static ObjectAddress get_object_address_opcf(ObjectType objtype, List *object,
 											 bool missing_ok);
 static ObjectAddress get_object_address_opf_member(ObjectType objtype,
@@ -1127,6 +1147,9 @@ get_object_address(ObjectType objtype, Node *object,
 															 missing_ok);
 				address.objectSubId = 0;
 				break;
+			case OBJECT_VARIABLE:
+				address = get_object_address_variable(castNode(List, object), missing_ok);
+				break;
 			default:
 				elog(ERROR, "unrecognized objtype: %d", (int) objtype);
 				/* placate compiler, in case it thinks elog might return */
@@ -1983,16 +2006,20 @@ get_object_address_defacl(List *object, bool missing_ok)
 		case DEFACLOBJ_NAMESPACE:
 			objtype_str = "schemas";
 			break;
+		case DEFACLOBJ_VARIABLE:
+			objtype_str = "variables";
+			break;
 		default:
 			ereport(ERROR,
 					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 					 errmsg("unrecognized default ACL object type \"%c\"", objtype),
-					 errhint("Valid object types are \"%c\", \"%c\", \"%c\", \"%c\", \"%c\".",
+					 errhint("Valid object types are \"%c\", \"%c\", \"%c\", \"%c\", \"%c\", \"%c\".",
 							 DEFACLOBJ_RELATION,
 							 DEFACLOBJ_SEQUENCE,
 							 DEFACLOBJ_FUNCTION,
 							 DEFACLOBJ_TYPE,
-							 DEFACLOBJ_NAMESPACE)));
+							 DEFACLOBJ_NAMESPACE,
+							 DEFACLOBJ_VARIABLE)));
 	}
 
 	/*
@@ -2077,6 +2104,24 @@ textarray_to_strvaluelist(ArrayType *arr)
 	return list;
 }
 
+/*
+ * Find the ObjectAddress for a schema variable
+ */
+static ObjectAddress
+get_object_address_variable(List *object, bool missing_ok)
+{
+	ObjectAddress address;
+	char	   *nspname = NULL;
+	char	   *varname = NULL;
+
+	ObjectAddressSet(address, VariableRelationId, InvalidOid);
+
+	DeconstructQualifiedName(object, &nspname, &varname);
+	address.objectId = LookupVariable(nspname, varname, missing_ok);
+
+	return address;
+}
+
 /*
  * SQL-callable version of get_object_address
  */
@@ -2266,6 +2311,7 @@ pg_get_object_address(PG_FUNCTION_ARGS)
 		case OBJECT_TABCONSTRAINT:
 		case OBJECT_OPCLASS:
 		case OBJECT_OPFAMILY:
+		case OBJECT_VARIABLE:
 			objnode = (Node *) name;
 			break;
 		case OBJECT_ACCESS_METHOD:
@@ -2572,6 +2618,11 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address,
 			if (!pg_statistics_object_ownercheck(address.objectId, roleid))
 				aclcheck_error_type(ACLCHECK_NOT_OWNER, address.objectId);
 			break;
+		case OBJECT_VARIABLE:
+			if (!pg_variable_ownercheck(address.objectId, roleid))
+				aclcheck_error(ACLCHECK_NOT_OWNER, objtype,
+							   NameListToString(castNode(List, object)));
+			break;
 		default:
 			elog(ERROR, "unrecognized object type: %d",
 				 (int) objtype);
@@ -3448,6 +3499,32 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok)
 				break;
 			}
 
+		case OCLASS_VARIABLE:
+			{
+				char	   *nspname;
+				HeapTuple	tup;
+				Form_pg_variable varform;
+
+				tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(object->objectId));
+				if (!HeapTupleIsValid(tup))
+					elog(ERROR, "cache lookup failed for schema variable %u",
+						 object->objectId);
+
+				varform = (Form_pg_variable) GETSTRUCT(tup);
+
+				if (VariableIsVisible(object->objectId))
+					nspname = NULL;
+				else
+					nspname = get_namespace_name(varform->varnamespace);
+
+				appendStringInfo(&buffer, _("schema variable %s"),
+								 quote_qualified_identifier(nspname,
+															NameStr(varform->varname)));
+
+				ReleaseSysCache(tup);
+				break;
+			}
+
 		case OCLASS_TSPARSER:
 			{
 				HeapTuple	tup;
@@ -3758,6 +3835,16 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok)
 										 _("default privileges on new schemas belonging to role %s"),
 										 rolename);
 						break;
+					case DEFACLOBJ_VARIABLE:
+						if (nspname)
+							appendStringInfo(&buffer,
+											 _("default privileges on new variables belonging to role %s in schema %s"),
+											 rolename, nspname);
+						else
+							appendStringInfo(&buffer,
+											 _("default privileges on new variables belonging to role %s"),
+											 rolename);
+						break;
 					default:
 						/* shouldn't get here */
 						if (nspname)
@@ -4478,6 +4565,10 @@ getObjectTypeDescription(const ObjectAddress *object, bool missing_ok)
 			appendStringInfoString(&buffer, "transform");
 			break;
 
+		case OCLASS_VARIABLE:
+			appendStringInfoString(&buffer, "schema variable");
+			break;
+
 			/*
 			 * There's intentionally no default: case here; we want the
 			 * compiler to warn if a new OCLASS hasn't been handled above.
@@ -5570,6 +5661,10 @@ getObjectIdentityParts(const ObjectAddress *object,
 						appendStringInfoString(&buffer,
 											   " on schemas");
 						break;
+					case DEFACLOBJ_VARIABLE:
+						appendStringInfoString(&buffer,
+											   " on schema variables");
+						break;
 				}
 
 				if (objname)
@@ -5762,6 +5857,33 @@ getObjectIdentityParts(const ObjectAddress *object,
 			}
 			break;
 
+		case OCLASS_VARIABLE:
+			{
+				char	   *schema;
+				char	   *varname;
+				HeapTuple	tup;
+				Form_pg_variable varform;
+
+				tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(object->objectId));
+				if (!HeapTupleIsValid(tup))
+					elog(ERROR, "cache lookup failed for schema variable %u",
+						 object->objectId);
+
+				varform = (Form_pg_variable) GETSTRUCT(tup);
+
+				schema = get_namespace_name_or_temp(varform->varnamespace);
+				varname = NameStr(varform->varname);
+
+				appendStringInfo(&buffer, "%s",
+								 quote_qualified_identifier(schema, varname));
+
+				if (objname)
+					*objname = list_make2(schema, varname);
+
+				ReleaseSysCache(tup);
+				break;
+			}
+
 			/*
 			 * There's intentionally no default: case here; we want the
 			 * compiler to warn if a new OCLASS hasn't been handled above.
diff --git a/src/backend/catalog/pg_variable.c b/src/backend/catalog/pg_variable.c
new file mode 100644
index 0000000000..451b57a0b7
--- /dev/null
+++ b/src/backend/catalog/pg_variable.c
@@ -0,0 +1,388 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_variable.c
+ *		schema variables
+ *
+ * Portions Copyright (c) 1996-2020, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *		src/backend/catalog/pg_variable.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/heapam.h"
+#include "access/htup_details.h"
+
+#include "catalog/catalog.h"
+#include "catalog/dependency.h"
+#include "catalog/indexing.h"
+#include "catalog/namespace.h"
+#include "catalog/objectaccess.h"
+#include "catalog/pg_namespace.h"
+#include "catalog/pg_type.h"
+#include "catalog/pg_variable.h"
+
+#include "commands/schema_variable.h"
+
+#include "storage/lmgr.h"
+
+#include "utils/builtins.h"
+#include "utils/lsyscache.h"
+#include "utils/syscache.h"
+
+static VariableEOXActionCodes
+to_eoxaction_code(VariableEOXAction action)
+{
+	switch (action)
+	{
+		case VARIABLE_EOX_NOOP:
+			return VARIABLE_EOX_CODE_NOOP;
+
+		case VARIABLE_EOX_DROP:
+			return VARIABLE_EOX_CODE_DROP;
+
+		case VARIABLE_EOX_RESET:
+			return VARIABLE_EOX_CODE_RESET;
+
+		default:
+			elog(ERROR, "unexpected action");
+	}
+
+}
+
+static VariableEOXAction
+to_eoxaction(VariableEOXActionCodes code)
+{
+	switch (code)
+	{
+		case VARIABLE_EOX_CODE_NOOP:
+			return VARIABLE_EOX_NOOP;
+
+		case VARIABLE_EOX_CODE_DROP:
+			return VARIABLE_EOX_DROP;
+
+		case VARIABLE_EOX_CODE_RESET:
+			return VARIABLE_EOX_RESET;
+
+		default:
+			elog(ERROR, "unexpected code");
+	}
+}
+
+/*
+ * Returns name of schema variable. When variable is not on path,
+ * then the name is qualified.
+ */
+char *
+schema_variable_get_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 = NULL;
+	else
+		nspname = get_namespace_name(varform->varnamespace);
+
+	result = quote_qualified_identifier(nspname, varname);
+
+	ReleaseSysCache(tup);
+
+	return result;
+}
+
+/*
+ * Returns varname field of pg_variable
+ */
+char *
+get_schema_variable_name(Oid varid)
+{
+	HeapTuple	tup;
+	Form_pg_variable varform;
+	char	   *varname;
+
+	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);
+
+	ReleaseSysCache(tup);
+
+	return varname;
+}
+
+/*
+ * Returns type, typmod of schema variable
+ */
+void
+get_schema_variable_type_typmod_collid(Oid varid, Oid *typid, int32 *typmod, Oid *collid)
+{
+	HeapTuple	tup;
+	Form_pg_variable varform;
+
+	tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid));
+
+	if (!HeapTupleIsValid(tup))
+		elog(ERROR, "cache lookup failed for variable %u", varid);
+
+	varform = (Form_pg_variable) GETSTRUCT(tup);
+
+	*typid = varform->vartype;
+	*typmod = varform->vartypmod;
+	*collid = varform->varcollation;
+
+	ReleaseSysCache(tup);
+
+	return;
+}
+
+/*
+ * Fetch all fields of schema variable from the syscache.
+ */
+void
+initVariable(Variable *var, Oid varid, bool missing_ok, bool fast_only)
+{
+	HeapTuple	tup;
+	Form_pg_variable varform;
+	Datum		defexpr_datum;
+	bool		defexpr_isnull;
+
+	tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid));
+
+	if (!HeapTupleIsValid(tup))
+	{
+		if (missing_ok)
+		{
+			var->oid = InvalidOid;
+			return;
+		}
+
+		elog(ERROR, "cache lookup failed for variable %u", varid);
+	}
+
+	varform = (Form_pg_variable) GETSTRUCT(tup);
+
+	var->oid = varid;
+	var->name = pstrdup(NameStr(varform->varname));
+	var->namespace = varform->varnamespace;
+	var->typid = varform->vartype;
+	var->typmod = varform->vartypmod;
+	var->owner = varform->varowner;
+	var->collation = varform->varcollation;
+	var->eoxaction = to_eoxaction(varform->vareoxaction);
+	var->is_not_null = varform->varisnotnull;
+	var->is_immutable = varform->varisimmutable;
+
+	/* Get defexpr */
+	defexpr_datum = SysCacheGetAttr(VARIABLEOID,
+									tup,
+									Anum_pg_variable_vardefexpr,
+									&defexpr_isnull);
+
+	var->has_defexpr = !defexpr_isnull;
+
+	/*
+	 * Sometimes we don't need deserialized defexpr, but we need info about
+	 * existence of defexpr.
+	 */
+
+	if (!fast_only)
+	{
+		Datum		aclDatum;
+		bool		isnull;
+
+		/* name */
+		var->name = pstrdup(NameStr(varform->varname));
+
+		if (!defexpr_isnull)
+			var->defexpr = stringToNode(TextDatumGetCString(defexpr_datum));
+		else
+			var->defexpr = NULL;
+
+		/* Get varacl */
+		aclDatum = SysCacheGetAttr(VARIABLEOID,
+								   tup,
+								   Anum_pg_variable_varacl,
+								   &isnull);
+		if (!isnull)
+			var->acl = DatumGetAclPCopy(aclDatum);
+		else
+			var->acl = NULL;
+	}
+	else
+	{
+		var->name = NULL;
+		var->defexpr = NULL;
+		var->acl = NULL;
+	}
+
+	ReleaseSysCache(tup);
+
+	return;
+}
+
+/*
+ * Create entry in pg_variable table
+ */
+ObjectAddress
+VariableCreate(const char *varName,
+			   Oid varNamespace,
+			   Oid varType,
+			   int32 varTypmod,
+			   Oid varOwner,
+			   Oid varCollation,
+			   Node *varDefexpr,
+			   VariableEOXAction eoxaction,
+			   bool is_not_null,
+			   bool if_not_exists,
+			   bool is_immutable)
+{
+	Acl		   *varacl;
+	NameData	varname;
+	bool		nulls[Natts_pg_variable];
+	Datum		values[Natts_pg_variable];
+	Relation	rel;
+	HeapTuple	tup,
+				oldtup;
+	TupleDesc	tupdesc;
+	ObjectAddress myself,
+				referenced;
+	Oid			varid;
+	int			i;
+
+	for (i = 0; i < Natts_pg_variable; i++)
+	{
+		nulls[i] = false;
+		values[i] = (Datum) 0;
+	}
+
+	namestrcpy(&varname, varName);
+
+	rel = table_open(VariableRelationId, RowExclusiveLock);
+
+	varid = GetNewOidWithIndex(rel, VariableObjectIndexId, Anum_pg_variable_oid);
+	values[Anum_pg_variable_oid - 1] = ObjectIdGetDatum(varid);
+	values[Anum_pg_variable_varname - 1] = NameGetDatum(&varname);
+	values[Anum_pg_variable_varnamespace - 1] = ObjectIdGetDatum(varNamespace);
+	values[Anum_pg_variable_vartype - 1] = ObjectIdGetDatum(varType);
+	values[Anum_pg_variable_vartypmod - 1] = Int32GetDatum(varTypmod);
+	values[Anum_pg_variable_varowner - 1] = ObjectIdGetDatum(varOwner);
+	values[Anum_pg_variable_varcollation - 1] = ObjectIdGetDatum(varCollation);
+	values[Anum_pg_variable_vareoxaction - 1] = CharGetDatum((char) to_eoxaction_code(eoxaction));
+	values[Anum_pg_variable_varisnotnull - 1] = BoolGetDatum(is_not_null);
+	values[Anum_pg_variable_varisimmutable - 1] = BoolGetDatum(is_immutable);
+	/* proacl will be determined later */
+
+	if (varDefexpr)
+		values[Anum_pg_variable_vardefexpr - 1] = CStringGetTextDatum(nodeToString(varDefexpr));
+	else
+		nulls[Anum_pg_variable_vardefexpr - 1] = true;
+
+	tupdesc = RelationGetDescr(rel);
+
+	oldtup = SearchSysCache2(VARIABLENAMENSP,
+							 PointerGetDatum(varName),
+							 ObjectIdGetDatum(varNamespace));
+
+	if (HeapTupleIsValid(oldtup))
+	{
+		if (if_not_exists)
+			ereport(NOTICE,
+					(errcode(ERRCODE_DUPLICATE_OBJECT),
+					 errmsg("schema variable \"%s\" already exists, skipping",
+							varName)));
+		else
+			ereport(ERROR,
+					(errcode(ERRCODE_DUPLICATE_OBJECT),
+					 errmsg("schema variable \"%s\" already exists",
+							varName)));
+
+		ReleaseSysCache(oldtup);
+		table_close(rel, RowExclusiveLock);
+
+		return InvalidObjectAddress;
+	}
+
+	varacl = get_user_default_acl(OBJECT_VARIABLE, varOwner,
+								  varNamespace);
+
+	if (varacl != NULL)
+		values[Anum_pg_variable_varacl - 1] = PointerGetDatum(varacl);
+	else
+		nulls[Anum_pg_variable_varacl - 1] = true;
+
+	tup = heap_form_tuple(tupdesc, values, nulls);
+	CatalogTupleInsert(rel, tup);
+
+	myself.classId = VariableRelationId;
+	myself.objectId = varid;
+	myself.objectSubId = 0;
+
+	/* dependency on namespace */
+	referenced.classId = NamespaceRelationId;
+	referenced.objectId = varNamespace;
+	referenced.objectSubId = 0;
+	recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL);
+
+	/* dependency on used type */
+	referenced.classId = TypeRelationId;
+	referenced.objectId = varType;
+	referenced.objectSubId = 0;
+	recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL);
+
+	/* dependency on default expr */
+	if (varDefexpr)
+		recordDependencyOnExpr(&myself, (Node *) varDefexpr,
+							   NIL, DEPENDENCY_NORMAL);
+
+	/* dependency on any roles mentioned in ACL */
+	if (varacl != NULL)
+	{
+		int			nnewmembers;
+		Oid		   *newmembers;
+
+		nnewmembers = aclmembers(varacl, &newmembers);
+		updateAclDependencies(VariableRelationId, varid, 0,
+							  varOwner,
+							  0, NULL,
+							  nnewmembers, newmembers);
+	}
+
+	/* dependency on extension */
+	recordDependencyOnCurrentExtension(&myself, false);
+
+	/*
+	 * register on commit action if it is necessary. This moment, the variable
+	 * has not any value, so we don't need to solve content transactionality.
+	 */
+	register_variable_on_commit_action(myself.objectId, eoxaction);
+
+	heap_freetuple(tup);
+
+	/* Post creation hook for new function */
+	InvokeObjectPostCreateHook(VariableRelationId, varid, 0);
+
+	table_close(rel, RowExclusiveLock);
+
+	return myself;
+}
diff --git a/src/backend/commands/Makefile b/src/backend/commands/Makefile
index e8504f0ae4..0b785be2ce 100644
--- a/src/backend/commands/Makefile
+++ b/src/backend/commands/Makefile
@@ -48,6 +48,7 @@ OBJS = \
 	proclang.o \
 	publicationcmds.o \
 	schemacmds.o \
+	schemavariable.o \
 	seclabel.o \
 	sequence.o \
 	statscmds.o \
diff --git a/src/backend/commands/alter.c b/src/backend/commands/alter.c
index 29249498a9..225b971d90 100644
--- a/src/backend/commands/alter.c
+++ b/src/backend/commands/alter.c
@@ -40,6 +40,7 @@
 #include "catalog/pg_ts_dict.h"
 #include "catalog/pg_ts_parser.h"
 #include "catalog/pg_ts_template.h"
+#include "catalog/pg_variable.h"
 #include "commands/alter.h"
 #include "commands/collationcmds.h"
 #include "commands/conversioncmds.h"
@@ -141,6 +142,10 @@ report_namespace_conflict(Oid classId, const char *name, Oid nspOid)
 			Assert(OidIsValid(nspOid));
 			msgfmt = gettext_noop("text search configuration \"%s\" already exists in schema \"%s\"");
 			break;
+		case VariableRelationId:
+			Assert(OidIsValid(nspOid));
+			msgfmt = gettext_noop("schema variable \"%s\" already exists in schema \"%s\"");
+			break;
 		default:
 			elog(ERROR, "unsupported object class %u", classId);
 			break;
@@ -393,6 +398,7 @@ ExecRenameStmt(RenameStmt *stmt)
 		case OBJECT_TSTEMPLATE:
 		case OBJECT_PUBLICATION:
 		case OBJECT_SUBSCRIPTION:
+		case OBJECT_VARIABLE:
 			{
 				ObjectAddress address;
 				Relation	catalog;
@@ -536,6 +542,7 @@ ExecAlterObjectSchemaStmt(AlterObjectSchemaStmt *stmt,
 		case OBJECT_TSDICTIONARY:
 		case OBJECT_TSPARSER:
 		case OBJECT_TSTEMPLATE:
+		case OBJECT_VARIABLE:
 			{
 				Relation	catalog;
 				Relation	relation;
@@ -626,6 +633,7 @@ AlterObjectNamespace_oid(Oid classId, Oid objid, Oid nspOid,
 		case OCLASS_TSDICT:
 		case OCLASS_TSTEMPLATE:
 		case OCLASS_TSCONFIG:
+		case OCLASS_VARIABLE:
 			{
 				Relation	catalog;
 
@@ -883,6 +891,7 @@ ExecAlterOwnerStmt(AlterOwnerStmt *stmt)
 		case OBJECT_TABLESPACE:
 		case OBJECT_TSDICTIONARY:
 		case OBJECT_TSCONFIGURATION:
+		case OBJECT_VARIABLE:
 			{
 				Relation	catalog;
 				Relation	relation;
diff --git a/src/backend/commands/discard.c b/src/backend/commands/discard.c
index 57d3d7dd9b..a712bf44b6 100644
--- a/src/backend/commands/discard.c
+++ b/src/backend/commands/discard.c
@@ -19,6 +19,7 @@
 #include "commands/discard.h"
 #include "commands/prepare.h"
 #include "commands/sequence.h"
+#include "commands/schema_variable.h"
 #include "utils/guc.h"
 #include "utils/portal.h"
 
@@ -48,6 +49,10 @@ DiscardCommand(DiscardStmt *stmt, bool isTopLevel)
 			ResetTempTableNamespace();
 			break;
 
+		case DISCARD_VARIABLES:
+			ResetSchemaVariableCache();
+			break;
+
 		default:
 			elog(ERROR, "unrecognized DISCARD target: %d", stmt->target);
 	}
@@ -75,4 +80,5 @@ DiscardAll(bool isTopLevel)
 	ResetPlanCache();
 	ResetTempTableNamespace();
 	ResetSequenceCaches();
+	ResetSchemaVariableCache();
 }
diff --git a/src/backend/commands/dropcmds.c b/src/backend/commands/dropcmds.c
index 97e5e9a765..b925e0dbdc 100644
--- a/src/backend/commands/dropcmds.c
+++ b/src/backend/commands/dropcmds.c
@@ -481,6 +481,10 @@ does_not_exist_skipping(ObjectType objtype, Node *object)
 			msg = gettext_noop("publication \"%s\" does not exist, skipping");
 			name = strVal((Value *) object);
 			break;
+		case OBJECT_VARIABLE:
+			msg = gettext_noop("schema variable \"%s\" does not exist, skipping");
+			name = NameListToString(castNode(List, object));
+			break;
 		default:
 			elog(ERROR, "unrecognized object type: %d", (int) objtype);
 			break;
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index 5bde507c75..aa330b20b8 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -990,6 +990,7 @@ EventTriggerSupportsObjectType(ObjectType obtype)
 		case OBJECT_TSTEMPLATE:
 		case OBJECT_TYPE:
 		case OBJECT_USER_MAPPING:
+		case OBJECT_VARIABLE:
 		case OBJECT_VIEW:
 			return true;
 
@@ -1053,6 +1054,7 @@ EventTriggerSupportsObjectClass(ObjectClass objclass)
 		case OCLASS_PUBLICATION_REL:
 		case OCLASS_SUBSCRIPTION:
 		case OCLASS_TRANSFORM:
+		case OCLASS_VARIABLE:
 			return true;
 
 			/*
@@ -2097,6 +2099,8 @@ stringify_grant_objtype(ObjectType objtype)
 			return "TABLESPACE";
 		case OBJECT_TYPE:
 			return "TYPE";
+		case OBJECT_VARIABLE:
+			return "VARIABLE";
 			/* these currently aren't used */
 		case OBJECT_ACCESS_METHOD:
 		case OBJECT_AGGREGATE:
@@ -2179,6 +2183,8 @@ stringify_adefprivs_objtype(ObjectType objtype)
 			return "TABLESPACES";
 		case OBJECT_TYPE:
 			return "TYPES";
+		case OBJECT_VARIABLE:
+			return "VARIABLES";
 			/* these currently aren't used */
 		case OBJECT_ACCESS_METHOD:
 		case OBJECT_AGGREGATE:
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index b62a76e7e5..1cbe810435 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -1191,6 +1191,9 @@ ExplainNode(PlanState *planstate, List *ancestors,
 					break;
 			}
 			break;
+		case T_SetVariable:
+			pname = sname = "Set Variable";
+			break;
 		case T_Append:
 			pname = sname = "Append";
 			break;
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index f767751c71..e7277c9e08 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -144,6 +144,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 		case CMD_INSERT:
 		case CMD_UPDATE:
 		case CMD_DELETE:
+		case CMD_LET:
 			/* OK */
 			break;
 		default:
diff --git a/src/backend/commands/schemavariable.c b/src/backend/commands/schemavariable.c
new file mode 100644
index 0000000000..9f50fe62c9
--- /dev/null
+++ b/src/backend/commands/schemavariable.c
@@ -0,0 +1,775 @@
+/*-------------------------------------------------------------------------
+ *
+ * schemavariable.c
+ *	  schema variable creation/manipulation commands
+ *
+ * Portions Copyright (c) 1996-2020, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/commands/schemavariable.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+#include "miscadmin.h"
+#include "access/heapam.h"
+#include "access/htup_details.h"
+#include "access/xact.h"
+#include "catalog/dependency.h"
+#include "catalog/indexing.h"
+#include "catalog/namespace.h"
+#include "catalog/pg_variable.h"
+#include "commands/schema_variable.h"
+#include "executor/executor.h"
+#include "executor/svariableReceiver.h"
+#include "optimizer/optimizer.h"
+#include "parser/parse_coerce.h"
+#include "parser/parse_collate.h"
+#include "parser/parse_expr.h"
+#include "parser/parse_type.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"
+
+/*
+ * ON COMMIT action list
+ */
+typedef struct OnCommitItem
+{
+	Oid			varid;			/* relid of relation */
+	VariableEOXAction eoxaction;	/* what to do at end of xact */
+
+	/*
+	 * If this entry was created during the current transaction,
+	 * creating_subid is the ID of the creating subxact; if created in a prior
+	 * transaction, creating_subid is zero.  If deleted during the current
+	 * transaction, deleting_subid is the ID of the deleting subxact; if no
+	 * deletion request is pending, deleting_subid is zero.
+	 */
+	SubTransactionId creating_subid;
+	SubTransactionId deleting_subid;
+} OnCommitItem;
+
+static List *on_commits = NIL;
+
+/*
+ * The content of variables is not transactional. Due this fact the
+ * implementation of DROP can be simple, because although DROP VARIABLE
+ * can be reverted, the content of variable can be lost. In this example,
+ * DROP VARIABLE is same like reset variable.
+ */
+
+typedef struct SchemaVariableData
+{
+	Oid			varid;			/* pg_variable OID of this sequence (hash key) */
+	Oid			typid;			/* OID of the data type */
+	int16		typlen;
+	bool		typbyval;
+	bool		isnull;
+	bool		freeval;
+	Datum		value;
+	bool		is_rowtype;		/* true when variable is composite */
+	bool		is_valid;		/* true when variable was successfuly
+								 * initialized */
+	bool		reset_auto;		/* next read fix invalidate value by self */
+
+	bool		is_not_null;	/* don't allow null values */
+	bool		has_defexpr;	/* true when there are default value */
+	bool		is_immutable;	/* true when variable is immutable */
+
+	SubTransactionId creating_subid;
+}			SchemaVariableData;
+
+typedef SchemaVariableData * SchemaVariable;
+
+static HTAB *schemavarhashtab = NULL;	/* hash table for session variables */
+static MemoryContext SchemaVariableMemoryContext = NULL;
+
+static bool first_time = true;
+static bool clean_cache_req = false;
+
+static void create_schema_variable_hashtable(void);
+static void clean_cache_callback(XactEvent event, void *arg);
+static void free_schema_variable(SchemaVariable svar, bool force);
+static void set_schema_variable(SchemaVariable svar, Datum value, bool isnull, Oid typid);
+static void init_schema_variable(SchemaVariable svar, Variable *var);
+static SchemaVariable prepare_variable_for_reading(Oid varid, bool reset);
+static void remove_variable_on_commit_actions(Oid varid, VariableEOXAction eoxaction);
+static void clean_cache_variable(Oid varid);
+
+/*
+ * Save info about necessity to clean hash table, because some
+ * schema variable was dropped. Don't do here more, recheck
+ * needs to be in transaction state.
+ */
+static void
+InvalidateSchemaVariableCacheCallback(Datum arg, int cacheid, uint32 hashvalue)
+{
+	if (cacheid != VARIABLEOID)
+		return;
+
+	clean_cache_req = true;
+}
+
+/*
+ * Recheck values of all schema variables stored in local memory.
+ * Remove the value from local memory, when related schema variable
+ * was dropped (and removed from system catalogue).
+ */
+static void
+clean_cache_callback(XactEvent event, void *arg)
+{
+	/*
+	 * should continue only in transaction time, when syscache is available.
+	 */
+	if (clean_cache_req && IsTransactionState())
+	{
+		HASH_SEQ_STATUS status;
+		SchemaVariable svar;
+
+		if (!schemavarhashtab)
+			return;
+
+		hash_seq_init(&status, schemavarhashtab);
+
+		while ((svar = (SchemaVariable) hash_seq_search(&status)) != NULL)
+		{
+			HeapTuple	tp;
+
+			tp = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(svar->varid));
+			if (!HeapTupleIsValid(tp))
+			{
+				elog(DEBUG1, "variable %d is removed from cache", svar->varid);
+
+				free_schema_variable(svar, true);
+
+				remove_variable_on_commit_actions(svar->varid, VARIABLE_EOX_DROP);
+
+				(void) hash_search(schemavarhashtab, (void *) &svar->varid,
+								   HASH_REMOVE, NULL);
+			}
+			else
+				ReleaseSysCache(tp);
+		}
+
+		clean_cache_req = false;
+	}
+}
+
+/*
+ * Create the hash table for storing schema variables
+ */
+static void
+create_schema_variable_hashtable(void)
+{
+	HASHCTL		ctl;
+
+	/* set callbacks */
+	if (first_time)
+	{
+		CacheRegisterSyscacheCallback(VARIABLEOID,
+									  InvalidateSchemaVariableCacheCallback,
+									  (Datum) 0);
+
+		RegisterXactCallback(clean_cache_callback, NULL);
+
+		first_time = false;
+	}
+
+	/* needs own long life memory context */
+	if (SchemaVariableMemoryContext == NULL)
+	{
+		SchemaVariableMemoryContext =
+			AllocSetContextCreate(TopMemoryContext,
+								  "schema variables",
+								  ALLOCSET_START_SMALL_SIZES);
+	}
+
+	memset(&ctl, 0, sizeof(ctl));
+	ctl.keysize = sizeof(Oid);
+	ctl.entrysize = sizeof(SchemaVariableData);
+	ctl.hcxt = SchemaVariableMemoryContext;
+
+	schemavarhashtab = hash_create("Schema variables", 64, &ctl,
+								   HASH_ELEM | HASH_BLOBS | HASH_CONTEXT);
+}
+
+/*
+ * Fast drop complete content of schema variables
+ */
+void
+ResetSchemaVariableCache(void)
+{
+	/* Remove temporal schema variables */
+	AtPreEOXact_SchemaVariable_on_commit_actions(true);
+
+	/* Destroy hash table and reset related memory context */
+	if (schemavarhashtab)
+	{
+		hash_destroy(schemavarhashtab);
+		schemavarhashtab = NULL;
+	}
+
+	/* Release memory allocated by schema variables */
+	if (SchemaVariableMemoryContext != NULL)
+		MemoryContextReset(SchemaVariableMemoryContext);
+}
+
+/*
+ * Release data stored inside svar. When a variable is transactional,
+ * and was not modified already in this transaction, then it archives
+ * current value for possible future usage on rollback.
+ *
+ * When force is true, then release current and possibly archived value.
+ */
+static void
+free_schema_variable(SchemaVariable svar, bool force)
+{
+	if (svar->freeval)
+		pfree(DatumGetPointer(svar->value));
+
+	/* Clean current value, and mark it as invalid */
+	svar->value = (Datum) 0;
+	svar->isnull = true;
+	svar->freeval = false;
+
+	svar->is_valid = false;
+}
+
+/*
+ * Assign some content to the schema variable. It does copy to
+ * SchemaVariableMemoryContext if it is necessary.
+ */
+static void
+set_schema_variable(SchemaVariable svar, Datum value, bool isnull, Oid typid)
+{
+	MemoryContext oldcxt;
+	Datum		newval = value;
+
+	/* Don't allow assign null to NOT NULL variable */
+	if (isnull && svar->is_not_null)
+		ereport(ERROR,
+				(errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
+				 errmsg("null value is not allowed for NOT NULL schema variable \"%s\"",
+						schema_variable_get_name(svar->varid))));
+
+	if (!isnull && svar->typid != typid)
+		ereport(ERROR,
+				(errcode(ERRCODE_DATATYPE_MISMATCH),
+				 errmsg("type \"%s\" of assigned value is different than type \"%s\" of schema variable \"%s\"",
+						format_type_be(typid),
+						format_type_be(svar->typid),
+						schema_variable_get_name(svar->varid))));
+
+	/* copy value to session persistent context */
+	oldcxt = MemoryContextSwitchTo(SchemaVariableMemoryContext);
+	if (!isnull)
+		newval = datumCopy(value, svar->typbyval, svar->typlen);
+	MemoryContextSwitchTo(oldcxt);
+
+	free_schema_variable(svar, false);
+
+	svar->value = newval;
+	svar->isnull = isnull;
+	svar->freeval = newval != value;
+	svar->is_valid = true;
+	svar->reset_auto = false;
+}
+
+/*
+ * Initialize svar from var
+ * svar - SchemaVariable - holds data
+ * var  - Variable - holds metadata
+ */
+static void
+init_schema_variable(SchemaVariable svar, Variable *var)
+{
+	Assert(OidIsValid(var->oid));
+
+	svar->varid = var->oid;
+	svar->typid = var->typid;
+
+	get_typlenbyval(var->typid,
+					&svar->typlen,
+					&svar->typbyval);
+
+	svar->isnull = true;
+	svar->freeval = false;
+	svar->value = (Datum) 0;
+
+	svar->is_rowtype = type_is_rowtype(var->typid);
+	svar->is_not_null = var->is_not_null;
+	svar->is_immutable = var->is_immutable;
+	svar->has_defexpr = var->has_defexpr;
+
+	/* the variable initialization was not complete here */
+	svar->is_valid = false;
+	svar->reset_auto = false;
+
+	svar->creating_subid = InvalidSubTransactionId;
+}
+
+/*
+ * Try to search value in hash table. If doesn't
+ * exists insert it (and calculate defexpr if exists.
+ * When reset is true, we would to enforce calculate
+ * defexpr. When some is wrong, then this function try
+ * don't break previous value.
+ */
+static SchemaVariable
+prepare_variable_for_reading(Oid varid, bool reset)
+{
+	SchemaVariable svar;
+	Variable	var;
+	bool		found;
+
+	var.oid = InvalidOid;
+
+	if (schemavarhashtab == NULL)
+		create_schema_variable_hashtable();
+
+	svar = (SchemaVariable) hash_search(schemavarhashtab, &varid,
+										HASH_ENTER, &found);
+
+	/* Return content if it is available, and reset is not required */
+	if (found && !reset && !svar->reset_auto)
+	{
+		/* raise exception when content is not valid */
+		if (!svar->is_valid)
+			ereport(ERROR,
+					(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+					 errmsg("variable \"%s\" has not valid content",
+							schema_variable_get_name(varid)),
+					 errhint("Overwrite the content of variable or assign DEFAULT again.")));
+
+		return svar;
+	}
+
+	/* We need to load defexpr. */
+	initVariable(&var, varid, false, false);
+
+	if (!found)
+		init_schema_variable(svar, &var);
+
+	/* Raise a error when we cannot to initialize variable correctly */
+	if (var.is_not_null && !var.defexpr)
+		ereport(ERROR,
+				(errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
+				 errmsg("null value is not allowed for NOT NULL schema variable \"%s\"",
+						schema_variable_get_name(varid)),
+				 errdetail("The schema variable was not initialized yet.")));
+
+	if (!found)
+		register_variable_on_commit_action(varid, var.eoxaction);
+
+	if (svar->has_defexpr)
+	{
+		Datum		value = (Datum) 0;
+		bool		isnull;
+		EState	   *estate = NULL;
+		Expr	   *defexpr;
+		ExprState  *defexprs;
+		MemoryContext oldcxt;
+
+		/* Prepare default expr */
+		estate = CreateExecutorState();
+
+		oldcxt = MemoryContextSwitchTo(estate->es_query_cxt);
+
+		defexpr = expression_planner((Expr *) var.defexpr);
+		defexprs = ExecInitExpr(defexpr, NULL);
+		value = ExecEvalExprSwitchContext(defexprs,
+										  GetPerTupleExprContext(estate),
+										  &isnull);
+
+
+		/* Store result before releasing Executor memory */
+		set_schema_variable(svar, value, isnull, svar->typid);
+
+		MemoryContextSwitchTo(oldcxt);
+
+		FreeExecutorState(estate);
+	}
+	else
+		set_schema_variable(svar, (Datum) 0, true, svar->typid);
+
+	return svar;
+}
+
+/*
+ * Write value to variable. We expect secured access in this moment.
+ * We try to don't break previous value, if some is wrong.
+ */
+void
+SetSchemaVariable(Oid varid, Datum value, bool isNull, Oid typid)
+{
+	SchemaVariable svar;
+	bool		found;
+
+	if (schemavarhashtab == NULL)
+		create_schema_variable_hashtable();
+
+	svar = (SchemaVariable) hash_search(schemavarhashtab, &varid,
+										HASH_ENTER, &found);
+
+	/* Initialize svar when was not initialized or when stored value is null */
+	if (!found)
+	{
+		Variable	var;
+
+		/* don't need defexpr and acl here */
+		initVariable(&var, varid, false, true);
+		init_schema_variable(svar, &var);
+		register_variable_on_commit_action(varid, var.eoxaction);
+	}
+
+	/* don't allow a update on immutable variable */
+	if (svar->is_immutable)
+		ereport(ERROR,
+				(errcode(ERRCODE_ERROR_IN_ASSIGNMENT),
+				 errmsg("schema variable \"%s\" is declared IMMUTABLE",
+						schema_variable_get_name(svar->varid))));
+
+	set_schema_variable(svar, value, isNull, typid);
+}
+
+/*
+ * Returns copy of value of schema variable spcified by varid
+ */
+Datum
+CopySchemaVariable(Oid varid, bool *isNull, Oid *typid)
+{
+	SchemaVariable svar;
+
+	svar = prepare_variable_for_reading(varid, false);
+	Assert(svar != NULL && svar->is_valid);
+
+	*isNull = svar->isnull;
+	*typid = svar->typid;
+
+	if (!svar->isnull)
+		return datumCopy(svar->value, svar->typbyval, svar->typlen);
+
+	return (Datum) 0;
+}
+
+/*
+ * Returns value of schema variable specified by varid. Check correct
+ * result type. Optionaly the result can be copy.
+ */
+Datum
+GetSchemaVariable(Oid varid, bool *isNull, Oid expected_typid, bool copy)
+{
+	SchemaVariable svar;
+	Datum		value;
+	bool		isnull;
+
+	svar = prepare_variable_for_reading(varid, false);
+	Assert(svar != NULL);
+
+	if (expected_typid != svar->typid)
+		elog(ERROR, "type of variable \"%s\" is different than expected",
+			 schema_variable_get_name(varid));
+
+	value = svar->value;
+	isnull = svar->isnull;
+
+	*isNull = isnull;
+
+	if (!isnull && copy)
+		return datumCopy(value, svar->typbyval, svar->typlen);
+
+	return value;
+}
+
+/*
+ * Clean variable defined by varid
+ */
+static void
+clean_cache_variable(Oid varid)
+{
+	SchemaVariable svar;
+	bool		found;
+
+	if (schemavarhashtab == NULL)
+		return;
+
+	svar = (SchemaVariable) hash_search(schemavarhashtab, &varid,
+										HASH_FIND, &found);
+	if (found)
+	{
+		/* clean content, if it is necessary */
+		free_schema_variable(svar, true);
+
+		if (hash_search(schemavarhashtab,
+						(void *) &svar->varid,
+						HASH_REMOVE,
+						NULL) == NULL)
+			elog(DEBUG1, "hash table corrupted");
+	}
+}
+
+/*
+ * Drop variable by OID
+ */
+void
+RemoveSchemaVariable(Oid varid)
+{
+	Relation	rel;
+	HeapTuple	tup;
+
+	rel = table_open(VariableRelationId, RowExclusiveLock);
+
+	tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid));
+
+	if (!HeapTupleIsValid(tup))
+		elog(ERROR, "cache lookup failed for variable %u", varid);
+
+	CatalogTupleDelete(rel, &tup->t_self);
+
+	ReleaseSysCache(tup);
+
+	table_close(rel, RowExclusiveLock);
+
+	clean_cache_variable(varid);
+
+	/* remove variable from on_commits list */
+	remove_variable_on_commit_actions(varid, VARIABLE_EOX_DROP);
+}
+
+/*
+ * Creates new variable - entry in pg_catalog.pg_variable table
+ *
+ * Used by CREATE VARIABLE command
+ */
+ObjectAddress
+DefineSchemaVariable(ParseState *pstate, CreateSchemaVarStmt * stmt)
+{
+	Oid			namespaceid;
+	AclResult	aclresult;
+	Oid			typid;
+	int32		typmod;
+	Oid			varowner = GetUserId();
+	Oid			collation;
+	Oid			typcollation;
+	ObjectAddress variable;
+
+	Node	   *cooked_default = NULL;
+
+	/*
+	 * Check consistency of arguments
+	 */
+	if (stmt->eoxaction == VARIABLE_EOX_DROP
+		&& stmt->variable->relpersistence != RELPERSISTENCE_TEMP)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+				 errmsg("ON COMMIT DROP can only be used on temporary variables")));
+
+	namespaceid =
+		RangeVarGetAndCheckCreationNamespace(stmt->variable, NoLock, NULL);
+
+	typenameTypeIdAndMod(pstate, stmt->typeName, &typid, &typmod);
+	typcollation = get_typcollation(typid);
+
+	aclresult = pg_type_aclcheck(typid, GetUserId(), ACL_USAGE);
+	if (aclresult != ACLCHECK_OK)
+		aclcheck_error_type(aclresult, typid);
+
+	if (stmt->collClause)
+		collation = LookupCollation(pstate,
+									stmt->collClause->collname,
+									stmt->collClause->location);
+	else
+		collation = typcollation;;
+
+	/* Complain if COLLATE is applied to an uncollatable type */
+	if (OidIsValid(collation) && !OidIsValid(typcollation))
+		ereport(ERROR,
+				(errcode(ERRCODE_DATATYPE_MISMATCH),
+				 errmsg("collations are not supported by type %s",
+						format_type_be(typid)),
+				 parser_errposition(pstate, stmt->collClause->location)));
+
+	if (stmt->defexpr)
+	{
+		cooked_default = transformExpr(pstate, stmt->defexpr,
+									   EXPR_KIND_VARIABLE_DEFAULT);
+
+		cooked_default = coerce_to_specific_type(pstate,
+												 cooked_default, typid, "DEFAULT");
+		assign_expr_collations(pstate, cooked_default);
+	}
+
+	variable = VariableCreate(stmt->variable->relname,
+							  namespaceid,
+							  typid,
+							  typmod,
+							  varowner,
+							  collation,
+							  cooked_default,
+							  stmt->eoxaction,
+							  stmt->is_not_null,
+							  stmt->if_not_exists,
+							  stmt->is_immutable);
+
+	/*
+	 * We must bump the command counter to make the newly-created variable
+	 * tuple visible for any other operations.
+	 */
+	CommandCounterIncrement();
+
+	return variable;
+}
+
+/*
+ * Register a newly-created variable ON COMMIT action.
+ */
+void
+register_variable_on_commit_action(Oid varid,
+								   VariableEOXAction action)
+{
+	OnCommitItem *oc;
+	MemoryContext oldcxt;
+
+	/*
+	 * We needn't bother registering the relation unless there is an ON COMMIT
+	 * action we need to take.
+	 */
+	if (action == VARIABLE_EOX_NOOP)
+		return;
+
+	oldcxt = MemoryContextSwitchTo(CacheMemoryContext);
+
+	oc = (OnCommitItem *) palloc(sizeof(OnCommitItem));
+	oc->varid = varid;
+	oc->eoxaction = action;
+
+	oc->creating_subid = GetCurrentSubTransactionId();
+	oc->deleting_subid = InvalidSubTransactionId;
+
+	on_commits = lcons(oc, on_commits);
+
+	MemoryContextSwitchTo(oldcxt);
+}
+
+/*
+ * Remove variable from on_commits action
+ */
+static void
+remove_variable_on_commit_actions(Oid varid,
+								  VariableEOXAction eoxaction)
+{
+	ListCell   *l;
+
+	foreach(l, on_commits)
+	{
+		OnCommitItem *oc = (OnCommitItem *) lfirst(l);
+
+		if (oc->varid == varid)
+		{
+			if (eoxaction == VARIABLE_EOX_DROP ||
+				(oc->eoxaction == VARIABLE_EOX_RESET &&
+				 eoxaction == VARIABLE_EOX_RESET))
+				oc->deleting_subid = GetCurrentSubTransactionId();
+		}
+	}
+}
+
+/*
+ * Perform ON TRANSACTION END RESET, ON COMMIT DROP
+ * and COMMIT/ROLLBACK of transaction schema variables.
+ */
+void
+AtPreEOXact_SchemaVariable_on_commit_actions(bool isCommit)
+{
+	ListCell   *l;
+
+	foreach(l, on_commits)
+	{
+		OnCommitItem *oc = (OnCommitItem *) lfirst(l);
+
+		/* Ignore entry if already dropped in this xact */
+		if (oc->deleting_subid != InvalidSubTransactionId)
+			continue;
+
+		switch (oc->eoxaction)
+		{
+			case VARIABLE_EOX_NOOP:
+				/* Do nothing */
+				break;
+			case VARIABLE_EOX_RESET:
+				clean_cache_variable(oc->varid);
+				remove_variable_on_commit_actions(oc->varid,
+												  VARIABLE_EOX_RESET);
+				break;
+			case VARIABLE_EOX_DROP:
+				{
+					/*
+					 * ON COMMIT DROP is allowed only for temp schema
+					 * variables. So we should explicit delete only when
+					 * current transaction was committed. When is rollback,
+					 * then schema variable is removed automatically.
+					 */
+					if (isCommit)
+					{
+						ObjectAddress object;
+
+						object.classId = VariableRelationId;
+						object.objectId = oc->varid;
+						object.objectSubId = 0;
+
+						/*
+						 * Since this is an automatic drop, rather than one
+						 * directly initiated by the user, we pass the
+						 * PERFORM_DELETION_INTERNAL flag.
+						 */
+						performDeletion(&object,
+										DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+					}
+				}
+				break;
+		}
+	}
+}
+
+/*
+ * Post-commit or post-abort cleanup for ON COMMIT management.
+ *
+ * All we do here is remove no-longer-needed OnCommitItem entries.
+ *
+ */
+void
+AtEOXact_SchemaVariable_on_commit_actions(bool isCommit)
+{
+	ListCell   *cur_item;
+
+	foreach(cur_item, on_commits)
+	{
+		OnCommitItem *oc = (OnCommitItem *) lfirst(cur_item);
+
+		/*
+		 * During commit, remove entries that were deleted during this
+		 * transaction; during abort, remove those created during this
+		 * transaction.
+		 *
+		 * The transact variable event is removed every time.
+		 */
+
+		if ((isCommit ? oc->deleting_subid != InvalidSubTransactionId :
+			 oc->creating_subid != InvalidSubTransactionId))
+		{
+			on_commits = foreach_delete_current(on_commits, cur_item);
+			pfree(oc);
+		}
+		else
+		{
+			oc->creating_subid = InvalidSubTransactionId;
+			oc->deleting_subid = InvalidSubTransactionId;
+		}
+	}
+}
diff --git a/src/backend/commands/seclabel.c b/src/backend/commands/seclabel.c
index 6906714298..0f2df23116 100644
--- a/src/backend/commands/seclabel.c
+++ b/src/backend/commands/seclabel.c
@@ -90,6 +90,7 @@ SecLabelSupportsObjectType(ObjectType objtype)
 		case OBJECT_TSPARSER:
 		case OBJECT_TSTEMPLATE:
 		case OBJECT_USER_MAPPING:
+		case OBJECT_VARIABLE:
 			return false;
 
 			/*
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 096a6f2891..0d4f2d5e2c 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -12067,6 +12067,7 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel,
 			case OCLASS_PUBLICATION_REL:
 			case OCLASS_SUBSCRIPTION:
 			case OCLASS_TRANSFORM:
+			case OCLASS_VARIABLE:
 
 				/*
 				 * We don't expect any of these sorts of objects to depend on
diff --git a/src/backend/executor/Makefile b/src/backend/executor/Makefile
index f08b282a5e..aa4037f9eb 100644
--- a/src/backend/executor/Makefile
+++ b/src/backend/executor/Makefile
@@ -65,6 +65,7 @@ OBJS = \
 	nodeSamplescan.o \
 	nodeSeqscan.o \
 	nodeSetOp.o \
+	nodeSetVariable.o \
 	nodeSort.o \
 	nodeSubplan.o \
 	nodeSubqueryscan.o \
@@ -76,6 +77,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 77c9d785d9..58232e5168 100644
--- a/src/backend/executor/execExpr.c
+++ b/src/backend/executor/execExpr.c
@@ -33,6 +33,7 @@
 #include "access/nbtree.h"
 #include "catalog/objectaccess.h"
 #include "catalog/pg_type.h"
+#include "commands/schema_variable.h"
 #include "executor/execExpr.h"
 #include "executor/nodeSubplan.h"
 #include "funcapi.h"
@@ -940,6 +941,60 @@ ExecInitExprRec(Expr *node, ExprState *state,
 						scratch.d.param.paramtype = param->paramtype;
 						ExprEvalPushStep(state, &scratch);
 						break;
+
+					case PARAM_VARIABLE:
+						{
+							int			es_num_schema_variables = 0;
+							SchemaVariableValue *es_schema_variables = NULL;
+
+							if (state->parent && state->parent->state)
+							{
+								es_schema_variables = state->parent->state->es_schema_variables;
+								es_num_schema_variables = state->parent->state->es_num_schema_variables;
+							}
+
+							/*
+							 * We should use schema variable buffer, when
+							 * it is available.
+							 */
+							if (es_schema_variables)
+							{
+								SchemaVariableValue *var;
+
+								/* check params, unexpected */
+								if (param->paramid >= es_num_schema_variables)
+									elog(ERROR, "paramid of PARAM_VARIABLE param is out of range");
+
+								var = &es_schema_variables[param->paramid];
+
+								/* unexpected */
+								if (var->typid != param->paramtype)
+									elog(ERROR, "type of buffered value is different than PARAM_VARIABLE type");
+
+								/*
+								 * In this case, the parameter is like a
+								 * constant
+								 */
+								scratch.opcode = EEOP_CONST;
+								scratch.d.constval.value = var->value;
+								scratch.d.constval.isnull = var->isnull;
+								ExprEvalPushStep(state, &scratch);
+							}
+							else
+							{
+								/*
+								 * When we have not a full PlanState (plpgsql
+								 * simple expr evaluation, then we should to
+								 * use direct access.
+								 */
+								scratch.opcode = EEOP_PARAM_VARIABLE;
+								scratch.d.vparam.varid = param->paramvarid;
+								scratch.d.vparam.vartype = param->paramtype;
+								ExprEvalPushStep(state, &scratch);
+							}
+						}
+						break;
+
 					case PARAM_EXTERN:
 
 						/*
diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c
index 094e22d392..5eae836a98 100644
--- a/src/backend/executor/execExprInterp.c
+++ b/src/backend/executor/execExprInterp.c
@@ -59,6 +59,7 @@
 #include "access/heaptoast.h"
 #include "catalog/pg_type.h"
 #include "commands/sequence.h"
+#include "commands/schema_variable.h"
 #include "executor/execExpr.h"
 #include "executor/nodeSubplan.h"
 #include "funcapi.h"
@@ -444,6 +445,7 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull)
 		&&CASE_EEOP_PARAM_EXEC,
 		&&CASE_EEOP_PARAM_EXTERN,
 		&&CASE_EEOP_PARAM_CALLBACK,
+		&&CASE_EEOP_PARAM_VARIABLE,
 		&&CASE_EEOP_CASE_TESTVAL,
 		&&CASE_EEOP_MAKE_READONLY,
 		&&CASE_EEOP_IOCOERCE,
@@ -1073,6 +1075,16 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull)
 			EEO_NEXT();
 		}
 
+		EEO_CASE(EEOP_PARAM_VARIABLE)
+		{
+			/* direct access to schema variable (without buffering) */
+			*op->resvalue = GetSchemaVariable(op->d.vparam.varid,
+											  op->resnull,
+											  op->d.vparam.vartype,
+											  true);
+			EEO_NEXT();
+		}
+
 		EEO_CASE(EEOP_CASE_TESTVAL)
 		{
 			/*
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index b2e2df8773..46cda5fd2a 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -45,8 +45,10 @@
 #include "access/xact.h"
 #include "catalog/namespace.h"
 #include "catalog/pg_publication.h"
+#include "catalog/pg_variable.h"
 #include "commands/matview.h"
 #include "commands/trigger.h"
+#include "commands/schema_variable.h"
 #include "executor/execdebug.h"
 #include "executor/nodeSubplan.h"
 #include "foreign/fdwapi.h"
@@ -197,6 +199,52 @@ standard_ExecutorStart(QueryDesc *queryDesc, int eflags)
 
 	estate->es_sourceText = queryDesc->sourceText;
 
+	/*
+	 * Prepare schema variables, if are not prepared in queryDesc
+	 */
+	if (queryDesc->num_schema_variables > 0)
+	{
+		/*
+		 * link shared memory with working copy of schema variable's values
+		 * used in this query. This access is used by parallel query executor's
+		 * workers.
+		 */
+		estate->es_schema_variables = queryDesc->schema_variables;
+		estate->es_num_schema_variables = queryDesc->num_schema_variables;
+	}
+	else if (queryDesc->plannedstmt->schemaVariables)
+	{
+		ListCell   *lc;
+		int			nSchemaVariables;
+		int			i = 0;
+
+		nSchemaVariables = list_length(queryDesc->plannedstmt->schemaVariables);
+
+		/* Create buffer for used schema variables */
+		estate->es_schema_variables = (SchemaVariableValue *)
+			palloc(nSchemaVariables * sizeof(SchemaVariableValue));
+
+		foreach(lc, queryDesc->plannedstmt->schemaVariables)
+		{
+			AclResult	aclresult;
+			Oid			varid = lfirst_oid(lc);
+
+			aclresult = pg_variable_aclcheck(varid, GetUserId(), ACL_READ);
+			if (aclresult != ACLCHECK_OK)
+				aclcheck_error(aclresult, OBJECT_VARIABLE,
+							   schema_variable_get_name(varid));
+
+			estate->es_schema_variables[i].varid = varid;
+			estate->es_schema_variables[i].value = CopySchemaVariable(varid,
+																	  &estate->es_schema_variables[i].isnull,
+																	  &estate->es_schema_variables[i].typid);
+
+			i++;
+		}
+
+		estate->es_num_schema_variables = nSchemaVariables;
+	}
+
 	/*
 	 * Fill in the query environment, if any, from queryDesc.
 	 */
@@ -208,6 +256,7 @@ standard_ExecutorStart(QueryDesc *queryDesc, int eflags)
 	switch (queryDesc->operation)
 	{
 		case CMD_SELECT:
+		case CMD_LET:
 
 			/*
 			 * SELECT FOR [KEY] UPDATE/SHARE and modifying CTEs need to mark
diff --git a/src/backend/executor/execParallel.c b/src/backend/executor/execParallel.c
index 4fca8782b2..1ec11a9ea3 100644
--- a/src/backend/executor/execParallel.c
+++ b/src/backend/executor/execParallel.c
@@ -12,8 +12,9 @@
  * workers and ensuring that their state generally matches that of the
  * leader; see src/backend/access/transam/README.parallel for details.
  * However, we must save and restore relevant executor state, such as
- * any ParamListInfo associated with the query, buffer/WAL usage info, and
- * the actual plan to be passed down to the worker.
+ * any ParamListInfo associated with the query, buffer/WAL usage info,
+ * used schema variables buffer, and the actual plan to be passed down
+ * to the worker.
  *
  * IDENTIFICATION
  *	  src/backend/executor/execParallel.c
@@ -66,6 +67,7 @@
 #define PARALLEL_KEY_QUERY_TEXT		UINT64CONST(0xE000000000000008)
 #define PARALLEL_KEY_JIT_INSTRUMENTATION UINT64CONST(0xE000000000000009)
 #define PARALLEL_KEY_WAL_USAGE			UINT64CONST(0xE00000000000000A)
+#define PARALLEL_KEY_SCHEMA_VARIABLES	UINT64CONST(0xE00000000000000B)
 
 #define PARALLEL_TUPLE_QUEUE_SIZE		65536
 
@@ -140,6 +142,12 @@ static bool ExecParallelRetrieveInstrumentation(PlanState *planstate,
 /* Helper function that runs in the parallel worker. */
 static DestReceiver *ExecParallelGetReceiver(dsm_segment *seg, shm_toc *toc);
 
+/* Helper functions that can pass values of used schema variables */
+static Size EstimateSchemaVariables(EState *estate);
+static void SerializeSchemaVariables(EState *estate, char **start_address);
+static SchemaVariableValue * RestoreSchemaVariables(char **start_address,
+													int *num_schema_variables);
+
 /*
  * Create a serialized representation of the plan to be sent to each worker.
  */
@@ -597,6 +605,7 @@ ExecInitParallelPlan(PlanState *planstate, EState *estate,
 	char	   *pstmt_data;
 	char	   *pstmt_space;
 	char	   *paramlistinfo_space;
+	char	   *schema_variables_space;
 	BufferUsage *bufusage_space;
 	WalUsage   *walusage_space;
 	SharedExecutorInstrumentation *instrumentation = NULL;
@@ -606,6 +615,7 @@ ExecInitParallelPlan(PlanState *planstate, EState *estate,
 	int			instrumentation_len = 0;
 	int			jit_instrumentation_len = 0;
 	int			instrument_offset = 0;
+	int			schema_variables_len = 0;
 	Size		dsa_minsize = dsa_minimum_size();
 	char	   *query_string;
 	int			query_len;
@@ -661,6 +671,11 @@ ExecInitParallelPlan(PlanState *planstate, EState *estate,
 	shm_toc_estimate_chunk(&pcxt->estimator, paramlistinfo_len);
 	shm_toc_estimate_keys(&pcxt->estimator, 1);
 
+	/* Estimate space for serialized schema variables. */
+	schema_variables_len = EstimateSchemaVariables(estate);
+	shm_toc_estimate_chunk(&pcxt->estimator, schema_variables_len);
+	shm_toc_estimate_keys(&pcxt->estimator, 1);
+
 	/*
 	 * Estimate space for BufferUsage.
 	 *
@@ -758,6 +773,11 @@ ExecInitParallelPlan(PlanState *planstate, EState *estate,
 	shm_toc_insert(pcxt->toc, PARALLEL_KEY_PARAMLISTINFO, paramlistinfo_space);
 	SerializeParamList(estate->es_param_list_info, &paramlistinfo_space);
 
+	/* Store serialized schema variables. */
+	schema_variables_space = shm_toc_allocate(pcxt->toc, schema_variables_len);
+	shm_toc_insert(pcxt->toc, PARALLEL_KEY_SCHEMA_VARIABLES, schema_variables_space);
+	SerializeSchemaVariables(estate, &schema_variables_space);
+
 	/* Allocate space for each worker's BufferUsage; no need to initialize. */
 	bufusage_space = shm_toc_allocate(pcxt->toc,
 									  mul_size(sizeof(BufferUsage), pcxt->nworkers));
@@ -1406,6 +1426,7 @@ ParallelQueryMain(dsm_segment *seg, shm_toc *toc)
 	SharedJitInstrumentation *jit_instrumentation;
 	int			instrument_options = 0;
 	void	   *area_space;
+	char	   *schemavariable_space;
 	dsa_area   *area;
 	ParallelWorkerContext pwcxt;
 
@@ -1431,6 +1452,14 @@ ParallelQueryMain(dsm_segment *seg, shm_toc *toc)
 	area_space = shm_toc_lookup(toc, PARALLEL_KEY_DSA, false);
 	area = dsa_attach_in_place(area_space, seg);
 
+	/* Reconstruct schema variables. */
+	schemavariable_space = shm_toc_lookup(toc,
+										  PARALLEL_KEY_SCHEMA_VARIABLES,
+										  false);
+	queryDesc->schema_variables =
+		RestoreSchemaVariables(&schemavariable_space,
+							   &queryDesc->num_schema_variables);
+
 	/* Start up the executor */
 	queryDesc->plannedstmt->jitFlags = fpes->jit_flags;
 	ExecutorStart(queryDesc, fpes->eflags);
@@ -1500,3 +1529,118 @@ ParallelQueryMain(dsm_segment *seg, shm_toc *toc)
 	FreeQueryDesc(queryDesc);
 	receiver->rDestroy(receiver);
 }
+
+/*
+ * Estimate the amount of space required to serialize a used
+ * schema variables.
+ */
+static Size
+EstimateSchemaVariables(EState *estate)
+{
+	int			i;
+	Size		sz = sizeof(int);
+
+	if (estate->es_schema_variables == NULL)
+		return sz;
+
+	for (i = 0; i < estate->es_num_schema_variables; i++)
+	{
+		SchemaVariableValue *svarval;
+		Oid			typeOid;
+		int16		typLen;
+		bool		typByVal;
+
+		svarval = &estate->es_schema_variables[i];
+
+		typeOid = svarval->typid;
+
+		sz = add_size(sz, sizeof(Oid)); /* space for type OID */
+
+		/* space for datum/isnull */
+		Assert(OidIsValid(typeOid));
+		get_typlenbyval(typeOid, &typLen, &typByVal);
+
+		sz = add_size(sz,
+					  datumEstimateSpace(svarval->value, svarval->isnull, typByVal, typLen));
+	}
+
+	return sz;
+}
+
+/*
+ * Serialize a schema variables buffer into caller-provided storage.
+ *
+ * We write the number of parameters first, as a 4-byte integer, and then
+ * write details for each parameter in turn.  The details for each parameter
+ * consist of a 4-byte type OID, and then the datum as serialized by
+ * datumSerialize().  The caller is responsible for ensuring that there is
+ * enough storage to store the number of bytes that will be written; use
+ * EstimateSchemaVariables to find out how many will be needed.
+ * *start_address is updated to point to the byte immediately following those
+ * written.
+ *
+ * RestoreSchemaVariables can be used to recreate a schema variable buffer
+ * based on the serialized representation;
+ */
+static void
+SerializeSchemaVariables(EState *estate, char **start_address)
+{
+	int			nparams;
+	int			i;
+
+	/* Write number of parameters. */
+	nparams = estate->es_num_schema_variables;
+	memcpy(*start_address, &nparams, sizeof(int));
+	*start_address += sizeof(int);
+
+	/* Write each parameter in turn. */
+	for (i = 0; i < nparams; i++)
+	{
+		SchemaVariableValue *svarval;
+		Oid			typeOid;
+		int16		typLen;
+		bool		typByVal;
+
+		svarval = &estate->es_schema_variables[i];
+		typeOid = svarval->typid;
+
+		/* Write type OID. */
+		memcpy(*start_address, &typeOid, sizeof(Oid));
+		*start_address += sizeof(Oid);
+
+		Assert(OidIsValid(typeOid));
+		get_typlenbyval(typeOid, &typLen, &typByVal);
+
+		datumSerialize(svarval->value, svarval->isnull, typByVal, typLen,
+					   start_address);
+	}
+}
+
+static SchemaVariableValue *
+RestoreSchemaVariables(char **start_address, int *num_schema_variables)
+{
+	SchemaVariableValue *schema_variables;
+	int			i;
+	int			nparams;
+
+	memcpy(&nparams, *start_address, sizeof(int));
+	*start_address += sizeof(int);
+
+	*num_schema_variables = nparams;
+	schema_variables = (SchemaVariableValue *)
+		palloc(nparams * sizeof(SchemaVariableValue));
+
+	for (i = 0; i < nparams; i++)
+	{
+		SchemaVariableValue *svarval = &schema_variables[i];
+
+		/* Read type OID. */
+		memcpy(&svarval->typid, *start_address, sizeof(Oid));
+		*start_address += sizeof(Oid);
+
+		/* Read datum/isnull. */
+		svarval->value = datumRestore(start_address, &svarval->isnull);
+	}
+
+	return schema_variables;
+}
diff --git a/src/backend/executor/execProcnode.c b/src/backend/executor/execProcnode.c
index 9f8c7582e0..d42cbe4099 100644
--- a/src/backend/executor/execProcnode.c
+++ b/src/backend/executor/execProcnode.c
@@ -106,6 +106,7 @@
 #include "executor/nodeSamplescan.h"
 #include "executor/nodeSeqscan.h"
 #include "executor/nodeSetOp.h"
+#include "executor/nodeSetVariable.h"
 #include "executor/nodeSort.h"
 #include "executor/nodeSubplan.h"
 #include "executor/nodeSubqueryscan.h"
@@ -177,6 +178,11 @@ ExecInitNode(Plan *node, EState *estate, int eflags)
 													   estate, eflags);
 			break;
 
+		case T_SetVariable:
+			result = (PlanState *) ExecInitSetVariable((SetVariable *) node,
+														   estate, eflags);
+			break;
+
 		case T_Append:
 			result = (PlanState *) ExecInitAppend((Append *) node,
 												  estate, eflags);
@@ -755,6 +761,10 @@ ExecEndNode(PlanState *node)
 			ExecEndLimit((LimitState *) node);
 			break;
 
+		case T_SetVariableState:
+			ExecEndSetVariable((SetVariableState *) node);
+			break;
+
 		default:
 			elog(ERROR, "unrecognized node type: %d", (int) nodeTag(node));
 			break;
diff --git a/src/backend/executor/nodeSetVariable.c b/src/backend/executor/nodeSetVariable.c
new file mode 100644
index 0000000000..7e992c0550
--- /dev/null
+++ b/src/backend/executor/nodeSetVariable.c
@@ -0,0 +1,142 @@
+/*-------------------------------------------------------------------------
+ *
+ * nodeSetVariable.c
+ *	  routines to handle SetVariable node
+ *
+ * Portions Copyright (c) 1996-2021, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/executor/nodeModifyTable.c
+ *
+ *-------------------------------------------------------------------------
+ */
+/* INTERFACE ROUTINES
+ *		ExecInitnodeSetVariable - initialize the SetVariable node
+ *		ExecnodeSetVariable		- execute SetVariable node
+ *		ExecEndnodeSetVariable	- shut down the SetVariable node
+ *		ExecReScannodeSetVariable - rescan the SetVariable node
+ *
+ *	 NOTES
+ *		The nodeSetVariable node receives input from its outerPlan and
+ *		store this value in target schema variable.
+ */
+
+#include "postgres.h"
+
+#include "commands/schema_variable.h"
+#include "executor/executor.h"
+#include "executor/nodeSetVariable.h"
+#include "executor/svariableReceiver.h"
+#include "miscadmin.h"
+#include "nodes/nodeFuncs.h"
+
+/* ----------------------------------------------------------------
+ *		ExecnodeSetVariable
+ *
+ *		Execute an plan - and set an result to schema variable
+ *
+ *		It returns NULL every time, but we can implement RETURNING
+ *		clase in future.
+ * ----------------------------------------------------------------
+ */
+static TupleTableSlot *		/* return NULL allways */
+ExecSetVariable(PlanState *pstate)
+{
+	SetVariableState *mstate = castNode(SetVariableState, pstate);
+	TupleTableSlot *slot;
+	PlanState  *outerPlan = outerPlanState(mstate);
+	EState	   *estate = mstate->ps.state;
+
+	CHECK_FOR_INTERRUPTS();
+
+	/*
+	 * outerPlan should to produce just one tuple. If there are
+	 * more tuples, then receiver raises an exception.
+	 */
+	for (;;)
+	{
+		slot = ExecProcNode(outerPlan);
+
+		if (TupIsNull(slot))
+		{
+			return NULL;
+		}
+
+		(void) mstate->dest->receiveSlot(slot, mstate->dest);
+		estate->es_processed++;
+	}
+
+	return NULL;
+}
+
+/* ----------------------------------------------------------------
+ *		ExecInitnodeSetVariable
+ *
+ *		initialize the node - initialize outer plan and schema
+ *		variable receiver
+ * ----------------------------------------------------------------
+ */
+SetVariableState *
+ExecInitSetVariable(SetVariable *node, EState *estate, int eflags)
+{
+	SetVariableState *mstate;
+	Plan	   *outerPlan;
+	PlanState  *outerNode;
+	TupleDesc	tupdesc;
+	AclResult	aclresult;
+
+	/* check for unsupported flags */
+	Assert(!(eflags & EXEC_FLAG_MARK));
+
+	/*
+	 * Is possible to write to schema variable?
+	 */
+	aclresult = pg_variable_aclcheck(node->varid, GetUserId(), ACL_WRITE);
+	if (aclresult != ACLCHECK_OK)
+		aclcheck_error(aclresult, OBJECT_VARIABLE,
+					   schema_variable_get_name(node->varid));
+
+	mstate = makeNode(SetVariableState);
+	mstate->ps.plan = (Plan *) node;
+	mstate->ps.state = estate;
+	mstate->ps.ExecProcNode = ExecSetVariable;
+
+	outerPlan = outerPlan(node);
+	outerNode = ExecInitNode(outerPlan, estate, eflags);
+	outerPlanState(mstate) = outerNode;
+
+	tupdesc = ExecGetResultType(outerNode);
+	mstate->dest = CreateVariableDestReceiver();
+	SetVariableDestReceiverParams(mstate->dest, node->varid);
+
+	mstate->dest->rStartup(mstate->dest, 0, tupdesc);
+
+	return mstate;
+}
+
+/* ----------------------------------------------------------------
+ *		ExecEndnodeSetVariable
+ *
+ *		Shuts down the plan.
+ *
+ *		Returns nothing of interest.
+ * ----------------------------------------------------------------
+ */
+void
+ExecEndSetVariable(SetVariableState *node)
+{
+	node->dest->rShutdown(node->dest);
+	ExecEndNode(outerPlanState(node));
+}
+
+void
+ExecReScanSetVariable(SetVariableState *node)
+{
+	/*
+	 * Currently, we don't need to support rescan on SetVariable nodes. The
+	 * semantics of that would be a bit debatable anyway.
+	 */
+	elog(ERROR, "ExecReScanSetVariable is not implemented");
+}
diff --git a/src/backend/executor/spi.c b/src/backend/executor/spi.c
index 00aa78ea53..80cc393c16 100644
--- a/src/backend/executor/spi.c
+++ b/src/backend/executor/spi.c
@@ -1909,6 +1909,8 @@ SPI_result_code_string(int code)
 			return "SPI_OK_UPDATE";
 		case SPI_OK_CURSOR:
 			return "SPI_OK_CURSOR";
+		case SPI_OK_LET:
+			return "SPI_OK_LET";
 		case SPI_OK_INSERT_RETURNING:
 			return "SPI_OK_INSERT_RETURNING";
 		case SPI_OK_DELETE_RETURNING:
@@ -2709,6 +2711,10 @@ _SPI_pquery(QueryDesc *queryDesc, bool fire_triggers, uint64 tcount)
 			else
 				res = SPI_OK_UPDATE;
 			break;
+		case CMD_LET:
+			res = SPI_OK_LET;
+			break;
+
 		default:
 			return SPI_ERROR_OPUNKNOWN;
 	}
diff --git a/src/backend/executor/svariableReceiver.c b/src/backend/executor/svariableReceiver.c
new file mode 100644
index 0000000000..298330c1cf
--- /dev/null
+++ b/src/backend/executor/svariableReceiver.c
@@ -0,0 +1,145 @@
+/*-------------------------------------------------------------------------
+ *
+ * svariableReceiver.c
+ *	  An implementation of DestReceiver that stores the result value in
+ *	  a schema variable.
+ *
+ * Portions Copyright (c) 1996-2020, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *	  src/backend/executor/svariableReceiver.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/detoast.h"
+#include "executor/svariableReceiver.h"
+#include "commands/schema_variable.h"
+
+typedef struct
+{
+	DestReceiver pub;
+	Oid			varid;
+	Oid			typid;
+	int32		typmod;
+	int			typlen;
+	int			slot_offset;
+	int			rows;
+}			svariableState;
+
+
+/*
+ * Prepare to receive tuples from executor.
+ */
+static void
+svariableStartupReceiver(DestReceiver *self, int operation, TupleDesc typeinfo)
+{
+	svariableState *myState = (svariableState *) self;
+	int			natts = typeinfo->natts;
+	int			outcols = 0;
+	int			i;
+
+	for (i = 0; i < natts; i++)
+	{
+		Form_pg_attribute attr = TupleDescAttr(typeinfo, i);
+
+		if (attr->attisdropped)
+			continue;
+
+		if (++outcols > 1)
+			elog(ERROR, "svariable DestReceiver can take only one attribute");
+
+		myState->typid = attr->atttypid;
+		myState->typmod = attr->atttypmod;
+		myState->typlen = attr->attlen;
+		myState->slot_offset = i;
+	}
+
+	myState->rows = 0;
+}
+
+/*
+ * Receive a tuple from the executor and store it in schema variable.
+ */
+static bool
+svariableReceiveSlot(TupleTableSlot *slot, DestReceiver *self)
+{
+	svariableState *myState = (svariableState *) self;
+	Datum		value;
+	bool		isnull;
+	bool		freeval = false;
+
+	/* Make sure the tuple is fully deconstructed */
+	slot_getallattrs(slot);
+
+	value = slot->tts_values[myState->slot_offset];
+	isnull = slot->tts_isnull[myState->slot_offset];
+
+	if (myState->typlen == -1 && !isnull && VARATT_IS_EXTERNAL(DatumGetPointer(value)))
+	{
+		value = PointerGetDatum(detoast_external_attr((struct varlena *)
+													  DatumGetPointer(value)));
+		freeval = true;
+	}
+
+	SetSchemaVariable(myState->varid, value, isnull, myState->typid);
+
+	if (freeval)
+		pfree(DatumGetPointer(value));
+
+	return true;
+}
+
+/*
+ * Clean up at end of an executor run
+ */
+static void
+svariableShutdownReceiver(DestReceiver *self)
+{
+	/* Do nothing */
+}
+
+/*
+ * Destroy receiver when done with it
+ */
+static void
+svariableDestroyReceiver(DestReceiver *self)
+{
+	pfree(self);
+}
+
+/*
+ * Initially create a DestReceiver object.
+ */
+DestReceiver *
+CreateVariableDestReceiver(void)
+{
+	svariableState *self = (svariableState *) palloc0(sizeof(svariableState));
+
+	self->pub.receiveSlot = svariableReceiveSlot;
+	self->pub.rStartup = svariableStartupReceiver;
+	self->pub.rShutdown = svariableShutdownReceiver;
+	self->pub.rDestroy = svariableDestroyReceiver;
+	self->pub.mydest = DestVariable;
+
+	/* private fields will be set by SetVariableDestReceiverParams */
+
+	return (DestReceiver *) self;
+}
+
+/*
+ * Set parameters for a VariableDestReceiver
+ */
+void
+SetVariableDestReceiverParams(DestReceiver *self, Oid varid)
+{
+	svariableState *myState = (svariableState *) self;
+
+	Assert(myState->pub.mydest == DestVariable);
+	Assert(OidIsValid(varid));
+
+	myState->varid = varid;
+}
diff --git a/src/backend/jit/llvm/llvmjit_expr.c b/src/backend/jit/llvm/llvmjit_expr.c
index 0f9cc790c7..3c1e30a15b 100644
--- a/src/backend/jit/llvm/llvmjit_expr.c
+++ b/src/backend/jit/llvm/llvmjit_expr.c
@@ -1073,6 +1073,12 @@ llvm_compile_expr(ExprState *state)
 				LLVMBuildBr(b, opblocks[opno + 1]);
 				break;
 
+			case EEOP_PARAM_VARIABLE:
+				build_EvalXFunc(b, mod, "ExecEvalParamVariable",
+								v_state, op, v_econtext);
+				LLVMBuildBr(b, opblocks[opno + 1]);
+				break;
+
 			case EEOP_PARAM_CALLBACK:
 				{
 					LLVMTypeRef v_functype;
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index 632cc31a04..b5c2067aab 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -99,6 +99,7 @@ _copyPlannedStmt(const PlannedStmt *from)
 	COPY_NODE_FIELD(invalItems);
 	COPY_NODE_FIELD(paramExecTypes);
 	COPY_NODE_FIELD(utilityStmt);
+	COPY_NODE_FIELD(schemaVariables);
 	COPY_LOCATION_FIELD(stmt_location);
 	COPY_SCALAR_FIELD(stmt_len);
 
@@ -224,6 +225,27 @@ _copyModifyTable(const ModifyTable *from)
 	return newnode;
 }
 
+/*
+ * _copySetVariable
+ */
+static SetVariable *
+_copySetVariable(const SetVariable *from)
+{
+	SetVariable *newnode = makeNode(SetVariable);
+
+	/*
+	 * copy node superclass fields
+	 */
+	CopyPlanFields((const Plan *) from, (Plan *) newnode);
+
+	/*
+	 * copy remainder of node
+	 */
+	COPY_SCALAR_FIELD(varid);
+
+	return newnode;
+}
+
 /*
  * _copyAppend
  */
@@ -1513,6 +1535,7 @@ _copyParam(const Param *from)
 	COPY_SCALAR_FIELD(paramtype);
 	COPY_SCALAR_FIELD(paramtypmod);
 	COPY_SCALAR_FIELD(paramcollid);
+	COPY_SCALAR_FIELD(paramvarid);
 	COPY_LOCATION_FIELD(location);
 
 	return newnode;
@@ -3163,6 +3186,7 @@ _copyQuery(const Query *from)
 	COPY_SCALAR_FIELD(canSetTag);
 	COPY_NODE_FIELD(utilityStmt);
 	COPY_SCALAR_FIELD(resultRelation);
+	COPY_SCALAR_FIELD(resultVariable);
 	COPY_SCALAR_FIELD(hasAggs);
 	COPY_SCALAR_FIELD(hasWindowFuncs);
 	COPY_SCALAR_FIELD(hasTargetSRFs);
@@ -3173,6 +3197,7 @@ _copyQuery(const Query *from)
 	COPY_SCALAR_FIELD(hasForUpdate);
 	COPY_SCALAR_FIELD(hasRowSecurity);
 	COPY_SCALAR_FIELD(isReturn);
+	COPY_SCALAR_FIELD(hasSchemaVariables);
 	COPY_NODE_FIELD(cteList);
 	COPY_NODE_FIELD(rtable);
 	COPY_NODE_FIELD(jointree);
@@ -3286,6 +3311,18 @@ _copySelectStmt(const SelectStmt *from)
 	return newnode;
 }
 
+static LetStmt *
+_copyLetStmt(const LetStmt * from)
+{
+	LetStmt    *newnode = makeNode(LetStmt);
+
+	COPY_NODE_FIELD(target);
+	COPY_NODE_FIELD(selectStmt);
+	COPY_LOCATION_FIELD(location);
+
+	return newnode;
+}
+
 static SetOperationStmt *
 _copySetOperationStmt(const SetOperationStmt *from)
 {
@@ -4865,6 +4902,23 @@ _copyDropSubscriptionStmt(const DropSubscriptionStmt *from)
 	return newnode;
 }
 
+static CreateSchemaVarStmt *
+_copyCreateSchemaVarStmt(const CreateSchemaVarStmt *from)
+{
+	CreateSchemaVarStmt *newnode = makeNode(CreateSchemaVarStmt);
+
+	COPY_NODE_FIELD(variable);
+	COPY_NODE_FIELD(typeName);
+	COPY_NODE_FIELD(collClause);
+	COPY_NODE_FIELD(defexpr);
+	COPY_SCALAR_FIELD(eoxaction);
+	COPY_SCALAR_FIELD(if_not_exists);
+	COPY_SCALAR_FIELD(is_not_null);
+	COPY_SCALAR_FIELD(is_immutable);
+
+	return newnode;
+}
+
 /* ****************************************************************
  *					extensible.h copy functions
  * ****************************************************************
@@ -4975,6 +5029,9 @@ copyObjectImpl(const void *from)
 		case T_ModifyTable:
 			retval = _copyModifyTable(from);
 			break;
+		case T_SetVariable:
+			retval = _copySetVariable(from);
+			break;
 		case T_Append:
 			retval = _copyAppend(from);
 			break;
@@ -5354,6 +5411,9 @@ copyObjectImpl(const void *from)
 		case T_SelectStmt:
 			retval = _copySelectStmt(from);
 			break;
+		case T_LetStmt:
+			retval = _copyLetStmt(from);
+			break;
 		case T_SetOperationStmt:
 			retval = _copySetOperationStmt(from);
 			break;
@@ -5696,6 +5756,9 @@ copyObjectImpl(const void *from)
 		case T_DropSubscriptionStmt:
 			retval = _copyDropSubscriptionStmt(from);
 			break;
+		case  T_CreateSchemaVarStmt:
+			retval = _copyCreateSchemaVarStmt(from);
+			break;
 		case T_A_Expr:
 			retval = _copyAExpr(from);
 			break;
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index a410a29a17..28bf78befe 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -208,6 +208,7 @@ _equalParam(const Param *a, const Param *b)
 	COMPARE_SCALAR_FIELD(paramtype);
 	COMPARE_SCALAR_FIELD(paramtypmod);
 	COMPARE_SCALAR_FIELD(paramcollid);
+	COMPARE_SCALAR_FIELD(paramvarid);
 	COMPARE_LOCATION_FIELD(location);
 
 	return true;
@@ -967,6 +968,7 @@ _equalQuery(const Query *a, const Query *b)
 	COMPARE_SCALAR_FIELD(canSetTag);
 	COMPARE_NODE_FIELD(utilityStmt);
 	COMPARE_SCALAR_FIELD(resultRelation);
+	COMPARE_SCALAR_FIELD(resultVariable);
 	COMPARE_SCALAR_FIELD(hasAggs);
 	COMPARE_SCALAR_FIELD(hasWindowFuncs);
 	COMPARE_SCALAR_FIELD(hasTargetSRFs);
@@ -977,6 +979,7 @@ _equalQuery(const Query *a, const Query *b)
 	COMPARE_SCALAR_FIELD(hasForUpdate);
 	COMPARE_SCALAR_FIELD(hasRowSecurity);
 	COMPARE_SCALAR_FIELD(isReturn);
+	COMPARE_SCALAR_FIELD(hasSchemaVariables);
 	COMPARE_NODE_FIELD(cteList);
 	COMPARE_NODE_FIELD(rtable);
 	COMPARE_NODE_FIELD(jointree);
@@ -1080,6 +1083,15 @@ _equalSelectStmt(const SelectStmt *a, const SelectStmt *b)
 	return true;
 }
 
+static bool
+_equalLetStmt(const LetStmt * a, const LetStmt * b)
+{
+	COMPARE_NODE_FIELD(target);
+	COMPARE_NODE_FIELD(selectStmt);
+
+	return true;
+}
+
 static bool
 _equalSetOperationStmt(const SetOperationStmt *a, const SetOperationStmt *b)
 {
@@ -2333,6 +2345,22 @@ _equalDropSubscriptionStmt(const DropSubscriptionStmt *a,
 	return true;
 }
 
+static bool
+_equalCreateSchemaVarStmt(const CreateSchemaVarStmt *a,
+						  const CreateSchemaVarStmt *b)
+{
+	COMPARE_NODE_FIELD(variable);
+	COMPARE_NODE_FIELD(typeName);
+	COMPARE_NODE_FIELD(collClause);
+	COMPARE_NODE_FIELD(defexpr);
+	COMPARE_SCALAR_FIELD(eoxaction);
+	COMPARE_SCALAR_FIELD(if_not_exists);
+	COMPARE_SCALAR_FIELD(is_not_null);
+	COMPARE_SCALAR_FIELD(is_immutable);
+
+	return true;
+}
+
 static bool
 _equalCreatePolicyStmt(const CreatePolicyStmt *a, const CreatePolicyStmt *b)
 {
@@ -3347,6 +3375,9 @@ equal(const void *a, const void *b)
 		case T_SelectStmt:
 			retval = _equalSelectStmt(a, b);
 			break;
+		case T_LetStmt:
+			retval = _equalLetStmt(a, b);
+			break;
 		case T_SetOperationStmt:
 			retval = _equalSetOperationStmt(a, b);
 			break;
@@ -3689,6 +3720,9 @@ equal(const void *a, const void *b)
 		case T_DropSubscriptionStmt:
 			retval = _equalDropSubscriptionStmt(a, b);
 			break;
+		case T_CreateSchemaVarStmt:
+			retval = _equalCreateSchemaVarStmt(a, b);
+			break;
 		case T_A_Expr:
 			retval = _equalAExpr(a, b);
 			break;
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index c723f6d635..7c9668d0ea 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -317,6 +317,7 @@ _outPlannedStmt(StringInfo str, const PlannedStmt *node)
 	WRITE_NODE_FIELD(invalItems);
 	WRITE_NODE_FIELD(paramExecTypes);
 	WRITE_NODE_FIELD(utilityStmt);
+	WRITE_NODE_FIELD(schemaVariables);
 	WRITE_LOCATION_FIELD(stmt_location);
 	WRITE_INT_FIELD(stmt_len);
 }
@@ -423,6 +424,16 @@ _outModifyTable(StringInfo str, const ModifyTable *node)
 	WRITE_NODE_FIELD(exclRelTlist);
 }
 
+static void
+_outSetVariable(StringInfo str, const SetVariable *node)
+{
+	WRITE_NODE_TYPE("SETVARIABLE");
+
+	_outPlanInfo(str, (const Plan *) node);
+
+	WRITE_OID_FIELD(varid);
+}
+
 static void
 _outAppend(StringInfo str, const Append *node)
 {
@@ -1156,6 +1167,7 @@ _outParam(StringInfo str, const Param *node)
 	WRITE_OID_FIELD(paramtype);
 	WRITE_INT_FIELD(paramtypmod);
 	WRITE_OID_FIELD(paramcollid);
+	WRITE_OID_FIELD(paramvarid);
 	WRITE_LOCATION_FIELD(location);
 }
 
@@ -2184,6 +2196,17 @@ _outModifyTablePath(StringInfo str, const ModifyTablePath *node)
 	WRITE_INT_FIELD(epqParam);
 }
 
+static void
+_outSetVariablePath(StringInfo str, const SetVariablePath *node)
+{
+	WRITE_NODE_TYPE("SetVariablePATH");
+
+	_outPathInfo(str, (const Path *) node);
+
+	WRITE_NODE_FIELD(subpath);
+	WRITE_OID_FIELD(varid);
+}
+
 static void
 _outLimitPath(StringInfo str, const LimitPath *node)
 {
@@ -2257,6 +2280,7 @@ _outPlannerGlobal(StringInfo str, const PlannerGlobal *node)
 	WRITE_NODE_FIELD(relationOids);
 	WRITE_NODE_FIELD(invalItems);
 	WRITE_NODE_FIELD(paramExecTypes);
+	WRITE_NODE_FIELD(schemaVariables);
 	WRITE_UINT_FIELD(lastPHId);
 	WRITE_UINT_FIELD(lastRowMarkId);
 	WRITE_INT_FIELD(lastPlanNodeId);
@@ -2317,6 +2341,7 @@ _outPlannerInfo(StringInfo str, const PlannerInfo *node)
 	WRITE_BOOL_FIELD(hasPseudoConstantQuals);
 	WRITE_BOOL_FIELD(hasAlternativeSubPlans);
 	WRITE_BOOL_FIELD(hasRecursion);
+	WRITE_BOOL_FIELD(hasSchemaVariables);
 	WRITE_INT_FIELD(wt_param_id);
 	WRITE_BITMAPSET_FIELD(curOuterRels);
 	WRITE_NODE_FIELD(curOuterParams);
@@ -2856,6 +2881,16 @@ _outPLAssignStmt(StringInfo str, const PLAssignStmt *node)
 	WRITE_LOCATION_FIELD(location);
 }
 
+static void
+_outLetStmt(StringInfo str, const LetStmt * node)
+{
+	WRITE_NODE_TYPE("LET");
+
+	WRITE_NODE_FIELD(target);
+	WRITE_NODE_FIELD(selectStmt);
+	WRITE_LOCATION_FIELD(location);
+}
+
 static void
 _outFuncCall(StringInfo str, const FuncCall *node)
 {
@@ -3047,6 +3082,7 @@ _outQuery(StringInfo str, const Query *node)
 		appendStringInfoString(str, " :utilityStmt <>");
 
 	WRITE_INT_FIELD(resultRelation);
+	WRITE_INT_FIELD(resultVariable);
 	WRITE_BOOL_FIELD(hasAggs);
 	WRITE_BOOL_FIELD(hasWindowFuncs);
 	WRITE_BOOL_FIELD(hasTargetSRFs);
@@ -3057,6 +3093,7 @@ _outQuery(StringInfo str, const Query *node)
 	WRITE_BOOL_FIELD(hasForUpdate);
 	WRITE_BOOL_FIELD(hasRowSecurity);
 	WRITE_BOOL_FIELD(isReturn);
+	WRITE_BOOL_FIELD(hasSchemaVariables);
 	WRITE_NODE_FIELD(cteList);
 	WRITE_NODE_FIELD(rtable);
 	WRITE_NODE_FIELD(jointree);
@@ -3849,6 +3886,9 @@ outNode(StringInfo str, const void *obj)
 			case T_ModifyTable:
 				_outModifyTable(str, obj);
 				break;
+			case T_SetVariable:
+				_outSetVariable(str, obj);
+				break;
 			case T_Append:
 				_outAppend(str, obj);
 				break;
@@ -4236,6 +4276,9 @@ outNode(StringInfo str, const void *obj)
 			case T_ModifyTablePath:
 				_outModifyTablePath(str, obj);
 				break;
+			case T_SetVariablePath:
+				_outSetVariablePath(str, obj);
+				break;
 			case T_LimitPath:
 				_outLimitPath(str, obj);
 				break;
@@ -4353,6 +4396,9 @@ outNode(StringInfo str, const void *obj)
 			case T_PLAssignStmt:
 				_outPLAssignStmt(str, obj);
 				break;
+			case T_LetStmt:
+				_outLetStmt(str, obj);
+				break;
 			case T_ColumnDef:
 				_outColumnDef(str, obj);
 				break;
diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c
index 3746668f52..6cb9e732ae 100644
--- a/src/backend/nodes/readfuncs.c
+++ b/src/backend/nodes/readfuncs.c
@@ -254,6 +254,7 @@ _readQuery(void)
 	READ_BOOL_FIELD(canSetTag);
 	READ_NODE_FIELD(utilityStmt);
 	READ_INT_FIELD(resultRelation);
+	READ_INT_FIELD(resultVariable);
 	READ_BOOL_FIELD(hasAggs);
 	READ_BOOL_FIELD(hasWindowFuncs);
 	READ_BOOL_FIELD(hasTargetSRFs);
@@ -264,6 +265,7 @@ _readQuery(void)
 	READ_BOOL_FIELD(hasForUpdate);
 	READ_BOOL_FIELD(hasRowSecurity);
 	READ_BOOL_FIELD(isReturn);
+	READ_BOOL_FIELD(hasSchemaVariables);
 	READ_NODE_FIELD(cteList);
 	READ_NODE_FIELD(rtable);
 	READ_NODE_FIELD(jointree);
@@ -628,6 +630,7 @@ _readParam(void)
 	READ_OID_FIELD(paramtype);
 	READ_INT_FIELD(paramtypmod);
 	READ_OID_FIELD(paramcollid);
+	READ_OID_FIELD(paramvarid);
 	READ_LOCATION_FIELD(location);
 
 	READ_DONE();
@@ -1598,6 +1601,7 @@ _readPlannedStmt(void)
 	READ_NODE_FIELD(invalItems);
 	READ_NODE_FIELD(paramExecTypes);
 	READ_NODE_FIELD(utilityStmt);
+	READ_NODE_FIELD(schemaVariables);
 	READ_LOCATION_FIELD(stmt_location);
 	READ_INT_FIELD(stmt_len);
 
@@ -1704,6 +1708,20 @@ _readModifyTable(void)
 	READ_DONE();
 }
 
+/*
+ * _readSetVariable
+ */
+static SetVariable *
+_readSetVariable(void)
+{
+	READ_LOCALS(SetVariable);
+
+	ReadCommonPlan(&local_node->plan);
+	READ_OID_FIELD(varid);
+
+	READ_DONE();
+}
+
 /*
  * _readAppend
  */
@@ -2865,6 +2883,8 @@ parseNodeString(void)
 		return_value = _readProjectSet();
 	else if (MATCH("MODIFYTABLE", 11))
 		return_value = _readModifyTable();
+	else if (MATCH("SETVARIABLE", 11))
+		return_value = _readSetVariable();
 	else if (MATCH("APPEND", 6))
 		return_value = _readAppend();
 	else if (MATCH("MERGEAPPEND", 11))
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index 22f10fa339..fba2169a9a 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -117,6 +117,8 @@ static RecursiveUnion *create_recursiveunion_plan(PlannerInfo *root, RecursiveUn
 static LockRows *create_lockrows_plan(PlannerInfo *root, LockRowsPath *best_path,
 									  int flags);
 static ModifyTable *create_modifytable_plan(PlannerInfo *root, ModifyTablePath *best_path);
+static SetVariable *create_SetVariable_plan(PlannerInfo *root,
+													SetVariablePath *best_path);
 static Limit *create_limit_plan(PlannerInfo *root, LimitPath *best_path,
 								int flags);
 static SeqScan *create_seqscan_plan(PlannerInfo *root, Path *best_path,
@@ -314,6 +316,7 @@ static ModifyTable *make_modifytable(PlannerInfo *root, Plan *subplan,
 									 List *updateColnosLists,
 									 List *withCheckOptionLists, List *returningLists,
 									 List *rowMarks, OnConflictExpr *onconflict, int epqParam);
+static SetVariable *make_SetVariable(Plan *lefttree, Oid varid);
 static GatherMerge *create_gather_merge_plan(PlannerInfo *root,
 											 GatherMergePath *best_path);
 
@@ -532,6 +535,10 @@ create_plan_recurse(PlannerInfo *root, Path *best_path, int flags)
 			plan = (Plan *) create_modifytable_plan(root,
 													(ModifyTablePath *) best_path);
 			break;
+		case T_SetVariable:
+			plan = (Plan *) create_SetVariable_plan(root,
+													(SetVariablePath *) best_path);
+			break;
 		case T_Limit:
 			plan = (Plan *) create_limit_plan(root,
 											  (LimitPath *) best_path,
@@ -2764,6 +2771,31 @@ create_modifytable_plan(PlannerInfo *root, ModifyTablePath *best_path)
 	return plan;
 }
 
+/*
+ * create_SetVariable_plan
+ *
+ *	  Returns a Plan node.
+ */
+static SetVariable *
+create_SetVariable_plan(PlannerInfo *root, SetVariablePath *best_path)
+{
+	SetVariable *plan;
+	Path	   *subpath = best_path->subpath;
+	Plan	   *subplan;
+
+	/* Subplan must produce exactly the specified tlist */
+	subplan = create_plan_recurse(root, subpath, CP_EXACT_TLIST);
+
+	/* Transfer resname/resjunk labeling, too, to keep executor happy */
+	apply_tlist_labeling(subplan->targetlist, root->processed_tlist);
+
+	plan = make_SetVariable(subplan, best_path->varid);
+
+	copy_generic_path_info(&plan->plan, (Path *) best_path);
+
+	return plan;
+}
+
 /*
  * create_limit_plan
  *
@@ -7111,6 +7143,25 @@ make_modifytable(PlannerInfo *root, Plan *subplan,
 	return node;
 }
 
+/*
+ * make_SetVariable
+ */
+static SetVariable *
+make_SetVariable(Plan *lefttree, Oid varid)
+{
+	SetVariable *node = makeNode(SetVariable);
+	Plan	   *plan = &node->plan;
+
+	plan->targetlist = lefttree->targetlist;
+	plan->qual = NIL;
+	plan->lefttree = lefttree;
+	plan->righttree = NULL;
+
+	node->varid = varid;
+
+	return node;
+}
+
 /*
  * is_projection_capable_path
  *		Check whether a given Path node is able to do projection.
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 1868c4eff4..52099f5082 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -310,6 +310,7 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
 	glob->lastPlanNodeId = 0;
 	glob->transientPlan = false;
 	glob->dependsOnRole = false;
+	glob->schemaVariables = NIL;
 
 	/*
 	 * Assess whether it's feasible to use parallel mode for this query. We
@@ -523,6 +524,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->schemaVariables = glob->schemaVariables;
 	result->stmt_location = parse->stmt_location;
 	result->stmt_len = parse->stmt_len;
 
@@ -672,6 +674,12 @@ subquery_planner(PlannerGlobal *glob, Query *parse,
 	 */
 	pull_up_subqueries(root);
 
+	/*
+	 * Check if some subquery uses schema variable. Flag hasSchemaVariables
+	 * should be true if query or some subquery uses any schema variable.
+	 */
+	pull_up_has_schema_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
@@ -1705,10 +1713,20 @@ grouping_planner(PlannerInfo *root, double tuple_fraction)
 											  offset_est, count_est);
 		}
 
+		/*
+		 * If this is LET statement, add SetVariable node.
+		 */
+		if (parse->commandType == CMD_LET)
+		{
+			path = (Path *)
+				create_setvariable_path(root, final_rel, path,
+											parse->resultVariable);
+		}
+
 		/*
 		 * If this is an INSERT/UPDATE/DELETE, add the ModifyTable node.
 		 */
-		if (parse->commandType != CMD_SELECT)
+		else if (parse->commandType != CMD_SELECT)
 		{
 			Index		rootRelation;
 			List	   *resultRelations = NIL;
diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c
index 9f40ed77e6..115fb9d1a5 100644
--- a/src/backend/optimizer/plan/setrefs.c
+++ b/src/backend/optimizer/plan/setrefs.c
@@ -172,7 +172,8 @@ static List *set_returning_clause_references(PlannerInfo *root,
 											 Plan *topplan,
 											 Index resultRelation,
 											 int rtoffset);
-
+static bool pull_up_has_schema_variables_walker(Node *node,
+											   PlannerInfo *root);
 
 /*****************************************************************************
  *
@@ -767,6 +768,7 @@ set_plan_refs(PlannerInfo *root, Plan *plan, int rtoffset)
 		case T_IncrementalSort:
 		case T_Unique:
 		case T_SetOp:
+		case T_SetVariable:
 
 			/*
 			 * These plan types don't actually bother to evaluate their
@@ -1094,6 +1096,50 @@ set_plan_refs(PlannerInfo *root, Plan *plan, int rtoffset)
 	return plan;
 }
 
+/*
+ * Search usage of schema variables in subqueries
+ */
+void
+pull_up_has_schema_variables(PlannerInfo *root)
+{
+	Query	   *query = root->parse;
+
+	if (query->hasSchemaVariables)
+	{
+		root->hasSchemaVariables = true;
+	}
+	else
+	{
+		(void) query_tree_walker(query,
+								 pull_up_has_schema_variables_walker,
+								 (void *) root, 0);
+	}
+}
+
+static bool
+pull_up_has_schema_variables_walker(Node *node, PlannerInfo *root)
+{
+	if (node == NULL)
+		return false;
+	if (IsA(node, Query))
+	{
+		Query	   *query = (Query *) node;
+
+		if (query->hasSchemaVariables)
+		{
+			root->hasSchemaVariables = true;
+			return false;
+		}
+
+		/* Recurse into subselects */
+		return query_tree_walker((Query *) node,
+								 pull_up_has_schema_variables_walker,
+								 (void *) root, 0);
+	}
+	return expression_tree_walker(node, pull_up_has_schema_variables_walker,
+								  (void *) root);
+}
+
 /*
  * set_indexonlyscan_references
  *		Do set_plan_references processing on an IndexOnlyScan
@@ -1722,10 +1768,14 @@ fix_expr_common(PlannerInfo *root, Node *node)
 /*
  * fix_param_node
  *		Do set_plan_references processing on a Param
+ *		Collect schema 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 should to calculate paramid.
  */
 static Node *
 fix_param_node(PlannerInfo *root, Param *p)
@@ -1744,6 +1794,52 @@ 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 schema variables, and we can complete
+		 * paramid parameter.
+		 */
+		foreach(lc, root->glob->schemaVariables)
+		{
+			if (lfirst_oid(lc) == p->paramvarid)
+			{
+				p->paramid = n;
+				found = true;
+				break;
+			}
+			n += 1;
+		}
+
+		if (!found)
+		{
+			PlanInvalItem *inval_item = makeNode(PlanInvalItem);
+
+			/* paramid is still schema variable id */
+			inval_item->cacheId = VARIABLEOID;
+			inval_item->hashValue = GetSysCacheHashValue1(VARIABLEOID,
+														  ObjectIdGetDatum(p->paramvarid));
+
+			/* Append this variable to global, register dependency */
+			root->glob->invalItems = lappend(root->glob->invalItems,
+											 inval_item);
+			root->glob->schemaVariables = lappend_oid(root->glob->schemaVariables,
+													  p->paramvarid);
+
+			p->paramid = n;
+		}
+
+		return (Node *) p;
+	}
+
 	return (Node *) copyObject(p);
 }
 
@@ -1819,7 +1915,9 @@ 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.
+ * and adding OIDs from regclass Const nodes into root->glob->relationOids,
+ * and replacing PARAM_VARIABLE paramid, that is oid of schema variable
+ * to offset to array of by query used schema variables.
  *
  * 'node': the expression to be modified
  * 'rtoffset': how much to increment varnos by
@@ -1841,7 +1939,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->hasSchemaVariables)
 	{
 		return fix_scan_expr_mutator(node, &context);
 	}
diff --git a/src/backend/optimizer/plan/subselect.c b/src/backend/optimizer/plan/subselect.c
index 0881a208ac..4ba9757c33 100644
--- a/src/backend/optimizer/plan/subselect.c
+++ b/src/backend/optimizer/plan/subselect.c
@@ -2758,6 +2758,7 @@ finalize_plan(PlannerInfo *root, Plan *plan,
 		case T_Unique:
 		case T_SetOp:
 		case T_Group:
+		case T_SetVariable:
 			/* no node-type-specific fields need fixing */
 			break;
 
diff --git a/src/backend/optimizer/prep/preptlist.c b/src/backend/optimizer/prep/preptlist.c
index 363132185d..59e583bffa 100644
--- a/src/backend/optimizer/prep/preptlist.c
+++ b/src/backend/optimizer/prep/preptlist.c
@@ -94,7 +94,8 @@ preprocess_targetlist(PlannerInfo *root)
 		target_relation = table_open(target_rte->relid, NoLock);
 	}
 	else
-		Assert(command_type == CMD_SELECT);
+		Assert(command_type == CMD_SELECT ||
+			   command_type == CMD_LET);
 
 	/*
 	 * In an INSERT, the executor expects the targetlist to match the exact
diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c
index 526997327c..c008343c37 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/schema_variable.h"
 #include "executor/executor.h"
 #include "executor/functions.h"
 #include "funcapi.h"
@@ -814,7 +815,8 @@ max_parallel_hazard_walker(Node *node, max_parallel_hazard_context *context)
 	{
 		Param	   *param = (Param *) node;
 
-		if (param->paramkind == PARAM_EXTERN)
+		if (param->paramkind == PARAM_EXTERN ||
+			param->paramkind == PARAM_VARIABLE)
 			return false;
 
 		if (param->paramkind != PARAM_EXEC ||
@@ -2180,6 +2182,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 schema variable can be used for estimation too.
  *--------------------
  */
 Node *
@@ -2299,6 +2302,30 @@ 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 = GetSchemaVariable(param->paramvarid,
+											 &isnull,
+											 param->paramtype,
+											 true);
+
+					return (Node *) makeConst(param->paramtype,
+											  param->paramtypmod,
+											  param->paramcollid,
+											  (int) typLen,
+											  pval,
+											  isnull,
+											  typByVal);
+				}
 
 				/*
 				 * Not replaceable, so just copy the Param (no need to
@@ -4628,7 +4655,7 @@ substitute_actual_parameters_mutator(Node *node,
 {
 	if (node == NULL)
 		return NULL;
-	if (IsA(node, Param))
+	if (IsA(node, Param) &&((Param *) node)->paramkind != PARAM_VARIABLE)
 	{
 		Param	   *param = (Param *) node;
 
diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.c
index b248b038e0..e539287da2 100644
--- a/src/backend/optimizer/util/pathnode.c
+++ b/src/backend/optimizer/util/pathnode.c
@@ -3686,6 +3686,41 @@ create_modifytable_path(PlannerInfo *root, RelOptInfo *rel,
 	return pathnode;
 }
 
+/*
+ * create_SetVariable_path
+ *	  Create a pathnode that represents LET statement
+ *
+ * 'rel' is the parent relation associated with the result
+ * 'subpath' is a Path producing source data
+ * 'varid' is an oid of target schema variable
+ */
+SetVariablePath *
+create_setvariable_path(PlannerInfo *root, RelOptInfo *rel,
+							Path *subpath,
+							Oid varid)
+{
+	SetVariablePath *pathnode = makeNode(SetVariablePath);
+
+	pathnode->path.pathtype = T_SetVariable;
+	pathnode->path.parent = rel;
+	/* Limit doesn't project, so use source path's pathtarget */
+	pathnode->path.pathtarget = subpath->pathtarget;
+	/* For now, assume we are above any joins, so no parameterization */
+	pathnode->path.param_info = NULL;
+	pathnode->path.parallel_aware = false;
+	pathnode->path.parallel_safe = false;
+	pathnode->path.parallel_workers = 0;
+	pathnode->path.rows = 1;
+	pathnode->path.startup_cost = subpath->startup_cost;
+	pathnode->path.total_cost = subpath->total_cost;
+	pathnode->path.pathkeys = NIL;
+	pathnode->subpath = subpath;
+	pathnode->varid = varid;
+
+	return pathnode;
+}
+
+
 /*
  * create_limit_path
  *	  Creates a pathnode that represents performing LIMIT/OFFSET
diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
index 862f18a92f..04eb99dee7 100644
--- a/src/backend/parser/analyze.c
+++ b/src/backend/parser/analyze.c
@@ -25,7 +25,10 @@
 #include "postgres.h"
 
 #include "access/sysattr.h"
+#include "catalog/namespace.h"
 #include "catalog/pg_type.h"
+#include "catalog/pg_variable.h"
+#include "commands/schema_variable.h"
 #include "miscadmin.h"
 #include "nodes/makefuncs.h"
 #include "nodes/nodeFuncs.h"
@@ -49,6 +52,7 @@
 #include "utils/builtins.h"
 #include "utils/guc.h"
 #include "utils/queryjumble.h"
+#include "utils/lsyscache.h"
 #include "utils/rel.h"

@@ -86,6 +90,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
@@ -287,6 +293,7 @@ transformStmt(ParseState *pstate, Node *parseTree)
 		case T_InsertStmt:
 		case T_UpdateStmt:
 		case T_DeleteStmt:
+		case T_LetStmt:
 			(void) test_raw_expression_coverage(parseTree, NULL);
 			break;
 		default:
@@ -356,6 +363,11 @@ transformStmt(ParseState *pstate, Node *parseTree)
 									   (CallStmt *) parseTree);
 			break;
 
+		case T_LetStmt:
+			result = transformLetStmt(pstate,
+									  (LetStmt *) parseTree);
+			break;
+
 		default:
 
 			/*
@@ -397,6 +409,7 @@ analyze_requires_snapshot(RawStmt *parseTree)
 		case T_UpdateStmt:
 		case T_SelectStmt:
 		case T_PLAssignStmt:
+		case T_LetStmt:
 			result = true;
 			break;
 
@@ -480,6 +493,8 @@ transformDeleteStmt(ParseState *pstate, DeleteStmt *stmt)
 	qry->hasTargetSRFs = pstate->p_hasTargetSRFs;
 	qry->hasAggs = pstate->p_hasAggs;
 
+	qry->hasSchemaVariables = pstate->p_hasSchemaVariables;
+
 	assign_query_collations(pstate, qry);
 
 	/* this must be done after collations, for reliable comparison of exprs */
@@ -897,6 +912,7 @@ transformInsertStmt(ParseState *pstate, InsertStmt *stmt)
 
 	qry->hasTargetSRFs = pstate->p_hasTargetSRFs;
 	qry->hasSubLinks = pstate->p_hasSubLinks;
+	qry->hasSchemaVariables = pstate->p_hasSchemaVariables;
 
 	assign_query_collations(pstate, qry);
 
@@ -1239,6 +1255,7 @@ transformSelectStmt(ParseState *pstate, SelectStmt *stmt)
 	Query	   *qry = makeNode(Query);
 	Node	   *qual;
 	ListCell   *l;
+	ParseExprKind		target_exprkind;
 
 	qry->commandType = CMD_SELECT;
 
@@ -1267,9 +1284,19 @@ transformSelectStmt(ParseState *pstate, SelectStmt *stmt)
 	/* process the FROM clause */
 	transformFromClause(pstate, stmt->fromClause);
 
-	/* transform targetlist */
-	qry->targetList = transformTargetList(pstate, stmt->targetList,
-										  EXPR_KIND_SELECT_TARGET);
+	/*
+	 * Transform targetlist. Second usage of this transformation
+	 * is for Let statement. In this case we would to allow DEFAULT
+	 * keyword - we specify EXPR_KIND_LET_TARGET.
+	 */
+	target_exprkind =
+		(pstate->p_expr_kind != EXPR_KIND_LET_TARGET ||
+		 pstate->parentParseState != NULL) ?
+					EXPR_KIND_SELECT_TARGET : EXPR_KIND_LET_TARGET;
+
+	qry->targetList = transformTargetList(pstate,
+										  stmt->targetList,
+										  target_exprkind);
 
 	/* mark column origins */
 	markTargetListOrigins(pstate, qry->targetList);
@@ -1359,6 +1386,8 @@ transformSelectStmt(ParseState *pstate, SelectStmt *stmt)
 							   (LockingClause *) lfirst(l), false);
 	}
 
+	qry->hasSchemaVariables = pstate->p_hasSchemaVariables;
+
 	assign_query_collations(pstate, qry);
 
 	/* this must be done after collations, for reliable comparison of exprs */
@@ -1583,12 +1612,243 @@ transformValuesClause(ParseState *pstate, SelectStmt *stmt)
 	qry->jointree = makeFromExpr(pstate->p_joinlist, NULL);
 
 	qry->hasSubLinks = pstate->p_hasSubLinks;
+	qry->hasSchemaVariables = pstate->p_hasSchemaVariables;
 
 	assign_query_collations(pstate, qry);
 
 	return qry;
 }
 
+/*
+ * transformLetStmt -
+ *	  transform an Let Statement
+ */
+static Query *
+transformLetStmt(ParseState *pstate, LetStmt * stmt)
+{
+	Query	   *qry = makeNode(Query);
+	List	   *exprList = NIL;
+	List	   *exprListCoer = NIL;
+	List	   *indirection = NIL;
+	ListCell   *lc;
+	Query	   *selectQuery;
+	int			i = 0;
+	Oid			varid;
+	ParseExprKind sv_expr_kind;
+	char	   *attrname = NULL;
+	bool		not_unique;
+	bool		is_rowtype;
+	bool		to_default = false;
+	Oid			typid;
+	int32		typmod;
+	Oid			collid;
+	AclResult	aclresult;
+	List	   *names = NULL;
+	int			indirection_start;
+
+	qry->commandType = CMD_LET;
+
+	/* utilityStmt should be defined, let is executed like utility stmts */
+	qry->utilityStmt = NULL; //(Node *) stmt;
+
+	sv_expr_kind = pstate->p_expr_kind;
+	pstate->p_expr_kind = EXPR_KIND_LET_TARGET;
+
+	/* There can't be any outer WITH to worry about */
+	Assert(pstate->p_ctenamespace == NIL);
+
+	names = NamesFromList(stmt->target);
+
+	varid = IdentifyVariable(names, &attrname, &not_unique);
+	if (not_unique)
+		ereport(ERROR,
+				(errcode(ERRCODE_AMBIGUOUS_PARAMETER),
+				 errmsg("target \"%s\" of LET command is ambiguous",
+						NameListToString(names)),
+				 parser_errposition(pstate, stmt->location)));
+
+	if (!OidIsValid(varid))
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("schema variable \"%s\" doesn't exists",
+						NameListToString(names)),
+				 parser_errposition(pstate, stmt->location)));
+
+	qry->resultVariable = varid;
+
+	indirection_start = list_length(names) - (attrname ? 1 : 0);
+	indirection = list_copy_tail(stmt->target, indirection_start);
+
+	get_schema_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("target variable \"%s\" is not row type",
+						schema_variable_get_name(varid)),
+				 parser_errposition(pstate, stmt->location)));
+
+	aclresult = pg_variable_aclcheck(varid, GetUserId(), ACL_WRITE);
+	if (aclresult != ACLCHECK_OK)
+		aclcheck_error(aclresult, OBJECT_VARIABLE, NameListToString(names));
+
+	selectQuery = transformStmt(pstate, stmt->selectStmt);
+
+	/* The grammar should have produced a SELECT */
+	if (!IsA(selectQuery, Query) ||
+		selectQuery->commandType != CMD_SELECT)
+		elog(ERROR, "unexpected non-SELECT command in LET command");
+
+	/*----------
+	 * Generate an expression list for the LET that selects all the
+	 * non-resjunk columns from the subquery.
+	 *----------
+	 */
+	exprList = NIL;
+	foreach(lc, selectQuery->targetList)
+	{
+		TargetEntry *tle = (TargetEntry *) lfirst(lc);
+
+		if (tle->resjunk)
+			continue;
+
+		if (IsA(tle->expr, SetToDefault))
+			to_default = true;
+
+		exprList = lappend(exprList, tle->expr);
+	}
+
+	/*
+	 * Because doesn't support pattern matching, don't allow multicolumn
+	 * result
+	 */
+	if (list_length(exprList) != 1)
+		ereport(ERROR,
+				(errcode(ERRCODE_SYNTAX_ERROR),
+				 errmsg("expression is not scalar value"),
+				 parser_errposition(pstate,
+									exprLocation((Node *) exprList))));
+
+	if (to_default && attrname != NULL)
+		ereport(ERROR,
+				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+				 errmsg("only complete variable can be set to default"),
+				 parser_errposition(pstate, stmt->location)));
+
+	exprListCoer = NIL;
+
+	foreach(lc, exprList)
+	{
+		Expr	   *expr = (Expr *) lfirst(lc);
+		Expr	   *coerced_expr;
+		Param	   *param;
+		Oid			exprtypid;
+
+		if (IsA(expr, SetToDefault))
+		{
+			SetToDefault *def = (SetToDefault *) expr;
+
+			def->typeId = typid;
+			def->typeMod = typmod;
+			def->collation = collid;
+		}
+		else if (IsA(expr, Const) && ((Const *) expr)->constisnull)
+		{
+			/* use known type for NULL value */
+			expr = (Expr *) makeNullConst(typid, typmod, collid);
+		}
+
+		/* now we can read type of expression */
+		exprtypid = exprType((Node *) expr);
+
+		param = makeNode(Param);
+		param->paramkind = PARAM_VARIABLE;
+		param->paramvarid = varid;
+		param->paramtype = typid;
+		param->paramtypmod = typmod;
+
+		if (indirection != NULL)
+		{
+			bool		targetIsArray;
+			char	   *targetName;
+
+			targetName = attrname != NULL ? attrname : get_schema_variable_name(varid);
+			targetIsArray = OidIsValid(get_element_type(typid));
+
+			pstate->p_hasSchemaVariables = true;
+
+			coerced_expr = (Expr *)
+				transformAssignmentIndirection(pstate,
+											   (Node *) param,
+											   targetName,
+											   targetIsArray,
+											   typid,
+											   typmod,
+											   InvalidOid,
+											   indirection,
+											   list_head(indirection),
+											   (Node *) expr,
+											   COERCION_PLPGSQL,
+											   stmt->location);
+		}
+		else
+			coerced_expr = (Expr *)
+				coerce_to_target_type(pstate,
+									  (Node *) expr,
+									  exprtypid,
+									  typid, typmod,
+									  COERCION_ASSIGNMENT,
+									  COERCE_IMPLICIT_CAST,
+									  stmt->location);
+
+		if (coerced_expr == NULL)
+			ereport(ERROR,
+					(errcode(ERRCODE_DATATYPE_MISMATCH),
+					 errmsg("variable \"%s\" is of type %s,"
+							" but expression is of type %s",
+							schema_variable_get_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.
+	 * Also, mark all the target columns as needing insert permissions.
+	 */
+	qry->targetList = NIL;
+	foreach(lc, exprListCoer)
+	{
+		Expr	   *expr = (Expr *) lfirst(lc);
+		TargetEntry *tle;
+
+		tle = makeTargetEntry(expr,
+							  i + 1,
+							  FigureColname((Node *) expr),
+							  false);
+		qry->targetList = lappend(qry->targetList, tle);
+	}
+
+	/* done building the range table and jointree */
+	qry->rtable = pstate->p_rtable;
+	qry->jointree = makeFromExpr(pstate->p_joinlist, NULL);
+
+	qry->hasTargetSRFs = pstate->p_hasTargetSRFs;
+	qry->hasSubLinks = pstate->p_hasSubLinks;
+	qry->hasSchemaVariables = pstate->p_hasSchemaVariables;
+
+	assign_query_collations(pstate, qry);
+
+	pstate->p_expr_kind = sv_expr_kind;
+
+	return qry;
+}
+
 /*
  * transformSetOperationStmt -
  *	  transforms a set-operations tree
@@ -1839,6 +2099,8 @@ transformSetOperationStmt(ParseState *pstate, SelectStmt *stmt)
 							   (LockingClause *) lfirst(l), false);
 	}
 
+	qry->hasSchemaVariables = pstate->p_hasSchemaVariables;
+
 	assign_query_collations(pstate, qry);
 
 	/* this must be done after collations, for reliable comparison of exprs */
@@ -2354,6 +2616,7 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
 
 	qry->hasTargetSRFs = pstate->p_hasTargetSRFs;
 	qry->hasSubLinks = pstate->p_hasSubLinks;
+	qry->hasSchemaVariables = pstate->p_hasSchemaVariables;
 
 	assign_query_collations(pstate, qry);
 
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 73494002ad..8b89edf00a 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -226,6 +226,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	JoinType			jtype;
 	DropBehavior		dbehavior;
 	OnCommitAction		oncommit;
+	VariableEOXAction	oneoxaction;
 	List				*list;
 	Node				*node;
 	Value				*value;
@@ -276,8 +277,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 		ConstraintsSetStmt CopyStmt CreateAsStmt CreateCastStmt
 		CreateDomainStmt CreateExtensionStmt CreateGroupStmt CreateOpClassStmt
 		CreateOpFamilyStmt AlterOpFamilyStmt CreatePLangStmt
-		CreateSchemaStmt CreateSeqStmt CreateStmt CreateStatsStmt CreateTableSpaceStmt
-		CreateFdwStmt CreateForeignServerStmt CreateForeignTableStmt
+		CreateSchemaStmt CreateSchemaVarStmt CreateSeqStmt CreateStmt CreateStatsStmt
+		CreateTableSpaceStmt CreateFdwStmt CreateForeignServerStmt CreateForeignTableStmt
 		CreateAssertionStmt CreateTransformStmt CreateTrigStmt CreateEventTrigStmt
 		CreateUserStmt CreateUserMappingStmt CreateRoleStmt CreatePolicyStmt
 		CreatedbStmt DeclareCursorStmt DefineStmt DeleteStmt DiscardStmt DoStmt
@@ -287,7 +288,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 		DropTransformStmt
 		DropUserMappingStmt ExplainStmt FetchStmt
 		GrantStmt GrantRoleStmt ImportForeignSchemaStmt IndexStmt InsertStmt
-		ListenStmt LoadStmt LockStmt NotifyStmt ExplainableStmt PreparableStmt
+		LetStmt ListenStmt LoadStmt LockStmt NotifyStmt ExplainableStmt PreparableStmt
 		CreateFunctionStmt AlterFunctionStmt ReindexStmt RemoveAggrStmt
 		RemoveFuncStmt RemoveOperStmt RenameStmt ReturnStmt RevokeStmt RevokeRoleStmt
 		RuleActionStmt RuleActionStmtOrEmpty RuleStmt
@@ -427,6 +428,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 				TriggerTransitions TriggerReferencing
 				vacuum_relation_list opt_vacuum_relation_list
 				drop_option_list
+				let_target
 
 %type <node>	opt_routine_body
 %type <groupclause> group_clause
@@ -450,6 +452,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %type <ival>	 OptTemp
 %type <ival>	 OptNoLog
 %type <oncommit> OnCommitOption
+%type <oneoxaction> OnEOXActionOption
 
 %type <ival>	for_locking_strength
 %type <node>	for_locking_item
@@ -608,6 +611,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %type <partboundspec> PartitionBoundSpec
 %type <list>		hash_partbound
 %type <defelt>		hash_partbound_elem
+%type <node>		OptSchemaVarDefExpr
+%type <boolean>		OptNotNull OptImmutable
 
 %type <str>	optColumnCompression
 
@@ -678,7 +683,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	KEY
 
 	LABEL LANGUAGE LARGE_P LAST_P LATERAL_P
-	LEADING LEAKPROOF LEAST LEFT LEVEL LIKE LIMIT LISTEN LOAD LOCAL
+	LEADING LEAKPROOF LEAST LEFT LET LEVEL LIKE LIMIT LISTEN LOAD LOCAL
 	LOCALTIME LOCALTIMESTAMP LOCATION LOCK_P LOCKED LOGGED
 
 	MAPPING MATCH MATERIALIZED MAXVALUE METHOD MINUTE_P MINVALUE MODE MONTH_P MOVE
@@ -717,8 +722,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	UESCAPE UNBOUNDED UNCOMMITTED UNENCRYPTED UNION UNIQUE UNKNOWN
 	UNLISTEN UNLOGGED UNTIL UPDATE USER USING
 
-	VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIADIC VARYING
-	VERBOSE VERSION_P VIEW VIEWS VOLATILE
+	VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIABLE VARIABLES
+	VARIADIC VARYING VERBOSE VERSION_P VIEW VIEWS VOLATILE
 
 	WHEN WHERE WHITESPACE_P WINDOW WITH WITHIN WITHOUT WORK WRAPPER WRITE
 
@@ -960,6 +965,7 @@ stmt:
 			| CreatePolicyStmt
 			| CreatePLangStmt
 			| CreateSchemaStmt
+			| CreateSchemaVarStmt
 			| CreateSeqStmt
 			| CreateStmt
 			| CreateSubscriptionStmt
@@ -997,6 +1003,7 @@ stmt:
 			| ImportForeignSchemaStmt
 			| IndexStmt
 			| InsertStmt
+			| LetStmt
 			| ListenStmt
 			| RefreshMatViewStmt
 			| LoadStmt
@@ -1889,7 +1896,12 @@ DiscardStmt:
 					n->target = DISCARD_SEQUENCES;
 					$$ = (Node *) n;
 				}
-
+			| DISCARD VARIABLES
+				{
+					DiscardStmt *n = makeNode(DiscardStmt);
+					n->target = DISCARD_VARIABLES;
+					$$ = (Node *) n;
+				}
 		;

@@ -4645,6 +4657,61 @@ create_extension_opt_item:
 				}
 		;
 
+/*****************************************************************************
+ *
+ *		QUERY :
+ *				CREATE VARIABLE varname [AS] type
+ *
+ *****************************************************************************/
+
+CreateSchemaVarStmt:
+			CREATE OptTemp OptImmutable VARIABLE qualified_name opt_as Typename opt_collate_clause OptNotNull OptSchemaVarDefExpr OnEOXActionOption
+				{
+					CreateSchemaVarStmt *n = makeNode(CreateSchemaVarStmt);
+					$5->relpersistence = $2;
+					n->is_immutable = $3;
+					n->variable = $5;
+					n->typeName = $7;
+					n->collClause = (CollateClause *) $8;
+					n->is_not_null = $9;
+					n->defexpr = $10;
+					n->eoxaction = $11;
+					n->if_not_exists = false;
+					$$ = (Node *) n;
+				}
+			| CREATE OptTemp OptImmutable VARIABLE IF_P NOT EXISTS qualified_name opt_as Typename opt_collate_clause OptNotNull OptSchemaVarDefExpr OnEOXActionOption
+				{
+					CreateSchemaVarStmt *n = makeNode(CreateSchemaVarStmt);
+					$8->relpersistence = $2;
+					n->is_immutable = $3;
+					n->variable = $8;
+					n->typeName = $10;
+					n->collClause = (CollateClause *) $11;
+					n->is_not_null = $12;
+					n->defexpr = $13;
+					n->eoxaction = $14;
+					n->if_not_exists = true;
+					$$ = (Node *) n;
+				}
+		;
+
+OptSchemaVarDefExpr: DEFAULT b_expr					{ $$ = $2; }
+			| /* EMPTY */							{ $$ = NULL; }
+		;
+
+OnEOXActionOption:  ON COMMIT DROP					{ $$ = VARIABLE_EOX_DROP; }
+			| ON TRANSACTION END_P RESET			{ $$ = VARIABLE_EOX_RESET; }
+			| /*EMPTY*/								{ $$ = VARIABLE_EOX_NOOP; }
+		;
+
+OptNotNull: NOT NULL_P								{ $$ = true; }
+			| /* EMPTY */							{ $$ = false; }
+		;
+
+OptImmutable: IMMUTABLE								{ $$ = true; }
+			| /* EMPTY */							{ $$ = false; }
+		;
+
 /*****************************************************************************
  *
  * ALTER EXTENSION name UPDATE [ TO version ]
@@ -6332,6 +6399,7 @@ object_type_any_name:
 			| TEXT_P SEARCH DICTIONARY				{ $$ = OBJECT_TSDICTIONARY; }
 			| TEXT_P SEARCH TEMPLATE				{ $$ = OBJECT_TSTEMPLATE; }
 			| TEXT_P SEARCH CONFIGURATION			{ $$ = OBJECT_TSCONFIGURATION; }
+			| VARIABLE								{ $$ = OBJECT_VARIABLE; }
 		;
 
 /*
@@ -7100,6 +7168,14 @@ privilege_target:
 					n->objs = $2;
 					$$ = n;
 				}
+			| VARIABLE qualified_name_list
+				{
+					PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget));
+					n->targtype = ACL_TARGET_OBJECT;
+					n->objtype = OBJECT_VARIABLE;
+					n->objs = $2;
+					$$ = n;
+				}
 			| ALL TABLES IN_P SCHEMA name_list
 				{
 					PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget));
@@ -7140,6 +7216,14 @@ privilege_target:
 					n->objs = $5;
 					$$ = n;
 				}
+			| ALL VARIABLES IN_P SCHEMA name_list
+				{
+					PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget));
+					n->targtype = ACL_TARGET_ALL_IN_SCHEMA;
+					n->objtype = OBJECT_VARIABLE;
+					n->objs = $5;
+					$$ = n;
+				}
 		;

@@ -7300,6 +7384,7 @@ defacl_privilege_target:
 			| SEQUENCES		{ $$ = OBJECT_SEQUENCE; }
 			| TYPES_P		{ $$ = OBJECT_TYPE; }
 			| SCHEMAS		{ $$ = OBJECT_SCHEMA; }
+			| VARIABLES		{ $$ = OBJECT_VARIABLE; }
 		;

@@ -9026,6 +9111,25 @@ RenameStmt: ALTER AGGREGATE aggregate_with_argtypes RENAME TO name
 					n->missing_ok = false;
 					$$ = (Node *)n;
 				}
+			| ALTER VARIABLE any_name RENAME TO name
+				{
+					RenameStmt *n = makeNode(RenameStmt);
+					n->renameType = OBJECT_VARIABLE;
+					n->object = (Node *) $3;
+					n->newname = $6;
+					n->missing_ok = false;
+					$$ = (Node *)n;
+				}
+			| ALTER VARIABLE IF_P EXISTS any_name RENAME TO name
+				{
+					RenameStmt *n = makeNode(RenameStmt);
+					n->renameType = OBJECT_VARIABLE;
+					n->object = (Node *) $5;
+					n->newname = $8;
+					n->missing_ok = true;
+					$$ = (Node *)n;
+				}
+
 		;
 
 opt_column: COLUMN
@@ -9354,6 +9458,25 @@ AlterObjectSchemaStmt:
 					n->missing_ok = false;
 					$$ = (Node *)n;
 				}
+			| ALTER VARIABLE any_name SET SCHEMA name
+				{
+					AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt);
+					n->objectType = OBJECT_VARIABLE;
+					n->object = (Node *) $3;
+					n->newschema = $6;
+					n->missing_ok = false;
+					$$ = (Node *)n;
+				}
+			| ALTER VARIABLE IF_P EXISTS any_name SET SCHEMA name
+				{
+					AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt);
+					n->objectType = OBJECT_VARIABLE;
+					n->object = (Node *) $5;
+					n->newschema = $8;
+					n->missing_ok = true;
+					$$ = (Node *)n;
+				}
+
 		;
 
 /*****************************************************************************
@@ -9607,6 +9730,14 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec
 					n->newowner = $6;
 					$$ = (Node *)n;
 				}
+			| ALTER VARIABLE any_name OWNER TO RoleSpec
+				{
+					AlterOwnerStmt *n = makeNode(AlterOwnerStmt);
+					n->objectType = OBJECT_VARIABLE;
+					n->object = (Node *) $3;
+					n->newowner = $6;
+					$$ = (Node *)n;
+				}
 		;

@@ -10848,6 +10979,7 @@ ExplainableStmt:
 			| CreateMatViewStmt
 			| RefreshMatViewStmt
 			| ExecuteStmt					/* by default all are $$=$1 */
+			| LetStmt
 		;
 
 /*****************************************************************************
@@ -10876,6 +11008,7 @@ PreparableStmt:
 			| InsertStmt
 			| UpdateStmt
 			| DeleteStmt					/* by default all are $$=$1 */
+			| LetStmt
 		;
 
 /*****************************************************************************
@@ -11293,6 +11426,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->selectStmt = (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:
@@ -15615,6 +15789,7 @@ unreserved_keyword:
 			| LARGE_P
 			| LAST_P
 			| LEAKPROOF
+			| LET
 			| LEVEL
 			| LISTEN
 			| LOAD
@@ -15772,6 +15947,8 @@ unreserved_keyword:
 			| VALIDATE
 			| VALIDATOR
 			| VALUE_P
+			| VARIABLE
+			| VARIABLES
 			| VARYING
 			| VERSION_P
 			| VIEW
@@ -16179,6 +16356,7 @@ bare_label_keyword:
 			| LEAKPROOF
 			| LEAST
 			| LEFT
+			| LET
 			| LEVEL
 			| LIKE
 			| LISTEN
@@ -16378,6 +16556,8 @@ bare_label_keyword:
 			| VALUE_P
 			| VALUES
 			| VARCHAR
+			| VARIABLE
+			| VARIABLES
 			| VARIADIC
 			| VERBOSE
 			| VERSION_P
diff --git a/src/backend/parser/parse_agg.c b/src/backend/parser/parse_agg.c
index ceb0bf597d..2c48f3a08b 100644
--- a/src/backend/parser/parse_agg.c
+++ b/src/backend/parser/parse_agg.c
@@ -348,6 +348,7 @@ check_agglevels_and_constraints(ParseState *pstate, Node *expr)
 			Assert(false);		/* can't happen */
 			break;
 		case EXPR_KIND_OTHER:
+		case EXPR_KIND_LET_TARGET:
 
 			/*
 			 * Accept aggregate/grouping here; caller must throw error if
@@ -464,6 +465,7 @@ check_agglevels_and_constraints(ParseState *pstate, Node *expr)
 			break;
 		case EXPR_KIND_COLUMN_DEFAULT:
 		case EXPR_KIND_FUNCTION_DEFAULT:
+		case EXPR_KIND_VARIABLE_DEFAULT:
 
 			if (isAgg)
 				err = _("aggregate functions are not allowed in DEFAULT expressions");
@@ -912,6 +914,7 @@ transformWindowFuncCall(ParseState *pstate, WindowFunc *wfunc,
 			break;
 		case EXPR_KIND_COLUMN_DEFAULT:
 		case EXPR_KIND_FUNCTION_DEFAULT:
+		case EXPR_KIND_VARIABLE_DEFAULT:
 			err = _("window functions are not allowed in DEFAULT expressions");
 			break;
 		case EXPR_KIND_INDEX_EXPRESSION:
@@ -950,6 +953,9 @@ transformWindowFuncCall(ParseState *pstate, WindowFunc *wfunc,
 		case EXPR_KIND_CYCLE_MARK:
 			errkind = true;
 			break;
+		case EXPR_KIND_LET_TARGET:
+			err = _("window functions are not allowed in LET statement");
+			break;
 
 			/*
 			 * There is intentionally no default: case here, so that the
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index f928c32311..e662b335b9 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -16,6 +16,7 @@
 #include "postgres.h"
 
 #include "catalog/pg_type.h"
+#include "catalog/pg_variable.h"
 #include "commands/dbcommands.h"
 #include "miscadmin.h"
 #include "nodes/makefuncs.h"
@@ -36,6 +37,7 @@
 #include "utils/date.h"
 #include "utils/lsyscache.h"
 #include "utils/timestamp.h"
+#include "utils/typcache.h"
 #include "utils/xml.h"
 
 /* GUC parameters */
@@ -82,7 +84,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 *makeParamSchemaVariable(ParseState *pstate,
+									 Oid varid, Oid typid, int32 typmod, Oid collid,
+									 char *attrname, int location);
 
 /*
  * transformExpr -
@@ -282,6 +286,7 @@ transformExprRecurse(ParseState *pstate, Node *expr)
 			 * processed it rather than passing it to transformExpr().
 			 */
 		case T_SetToDefault:
+			Assert(false);
 			ereport(ERROR,
 					(errcode(ERRCODE_SYNTAX_ERROR),
 					 errmsg("DEFAULT is not allowed in this context"),
@@ -448,6 +453,9 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
 	char	   *relname = NULL;
 	char	   *colname = NULL;
 	ParseNamespaceItem *nsitem;
+	Oid			varid = InvalidOid;
+	char	   *attrname = NULL;
+	bool		not_unique;
 	int			levels_up;
 	enum
 	{
@@ -509,6 +517,9 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
 		case EXPR_KIND_COPY_WHERE:
 		case EXPR_KIND_GENERATED_COLUMN:
 		case EXPR_KIND_CYCLE_MARK:
+		case EXPR_KIND_VARIABLE_DEFAULT:
+		case EXPR_KIND_LET_TARGET:
+
 			/* okay */
 			break;
 
@@ -766,6 +777,15 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
 			break;
 	}
 
+	varid = IdentifyVariable(cref->fields, &attrname, &not_unique);
+
+	if (not_unique)
+		ereport(ERROR,
+				(errcode(ERRCODE_AMBIGUOUS_PARAMETER),
+				 errmsg("schema variable reference \"%s\" is ambiguous",
+						NameListToString(cref->fields)),
+				 parser_errposition(pstate, cref->location)));
+
 	/*
 	 * Now give the PostParseColumnRefHook, if any, a chance.  We pass the
 	 * translation-so-far so that it can throw an error if it wishes in the
@@ -790,6 +810,78 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
 					 parser_errposition(pstate, cref->location)));
 	}
 
+	if (OidIsValid(varid))
+	{
+		Oid			typid;
+		int32		typmod;
+		Oid			collid;
+
+		get_schema_variable_type_typmod_collid(varid, &typid, &typmod, &collid);
+
+		if (node != NULL)
+		{
+			/*
+			 * Some cases with ambiguous references can be solved without
+			 * raising an error. When there is an collision between column
+			 * name (or label) and some schema variable name, and when we know
+			 * attribute name, then we can ignore the collision when:
+			 *
+			 *   a) variable is of scalar type (then indirection cannot be
+			 *      applied on this schema variable.
+			 *
+			 *   b) when related variable has no field of attrname, then
+			 *      indirection cannot be applied on this schema variable.
+			 */
+			if (attrname)
+			{
+				if (type_is_rowtype(typid))
+				{
+					TupleDesc	tupdesc;
+					bool		found = false;
+					int			i;
+
+					/* slow part, I hope it will not be to often */
+					tupdesc = lookup_rowtype_tupdesc(typid, typmod);
+					for (i = 0; i < tupdesc->natts; i++)
+					{
+						if (namestrcmp(&(TupleDescAttr(tupdesc, i)->attname), attrname) == 0 &&
+							!TupleDescAttr(tupdesc, i)->attisdropped)
+						{
+							found = true;
+							break;
+						}
+					}
+
+					FreeTupleDesc(tupdesc);
+
+					/* there are not composite variable with this field */
+					if (!found)
+						varid = InvalidOid;
+				}
+				else
+					/* there are not composite variable with this name */
+					varid = InvalidOid;
+			}
+
+			/*
+			 * Raise error if varid is still valid. It should be really
+			 * amigonuous
+			 */
+			if (OidIsValid(varid))
+				ereport(ERROR,
+						(errcode(ERRCODE_AMBIGUOUS_COLUMN),
+						 errmsg("column reference \"%s\" is ambiguous",
+								NameListToString(cref->fields)),
+						 errdetail("The qualified identifier can be column reference or schema variable reference"),
+						 parser_errposition(pstate, cref->location)));
+		}
+
+		if (OidIsValid(varid))
+			node = makeParamSchemaVariable(pstate,
+										   varid, typid, typmod, collid,
+										   attrname, cref->location);
+	}
+
 	/*
 	 * Throw error if no translation found.
 	 */
@@ -824,6 +916,74 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
 	return node;
 }
 
+/*
+ * Generate param variable for reference to schema variable
+ */
+static Node *
+makeParamSchemaVariable(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;
+
+	/*
+	 * There are two access to schema variables - direct, used by simple
+	 * plpgsql expressions, where there are not necessary to emulate
+	 * stability. Buffered access is used elsewhere. We should to ensure
+	 * stable values, and because schema variables are global, then we should
+	 * to work with copied values instead direct access to variables. For
+	 * direct access the varid is best for access. For buffered access we need
+	 * to assign index to buffer - later, when we will know what variables are
+	 * used. Now, we just remember, so we use schema variables.
+	 */
+	pstate->p_hasSchemaVariables = true;
+
+	if (attrname != NULL)
+	{
+		TupleDesc	tupdesc;
+		int			i;
+
+		tupdesc = lookup_rowtype_tupdesc(typid, typmod);
+
+		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)
 {
@@ -1726,6 +1886,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:
@@ -1734,6 +1895,7 @@ transformSubLink(ParseState *pstate, SubLink *sublink)
 			break;
 		case EXPR_KIND_COLUMN_DEFAULT:
 		case EXPR_KIND_FUNCTION_DEFAULT:
+		case EXPR_KIND_VARIABLE_DEFAULT:
 			err = _("cannot use subquery in DEFAULT expression");
 			break;
 		case EXPR_KIND_INDEX_EXPRESSION:
@@ -3064,6 +3226,7 @@ ParseExprKindName(ParseExprKind exprKind)
 			return "CHECK";
 		case EXPR_KIND_COLUMN_DEFAULT:
 		case EXPR_KIND_FUNCTION_DEFAULT:
+		case EXPR_KIND_VARIABLE_DEFAULT:
 			return "DEFAULT";
 		case EXPR_KIND_INDEX_EXPRESSION:
 			return "index expression";
@@ -3089,6 +3252,8 @@ ParseExprKindName(ParseExprKind exprKind)
 			return "GENERATED AS";
 		case EXPR_KIND_CYCLE_MARK:
 			return "CYCLE";
+		case EXPR_KIND_LET_TARGET:
+			return "LET";
 
 			/*
 			 * There is intentionally no default: case here, so that the
diff --git a/src/backend/parser/parse_func.c b/src/backend/parser/parse_func.c
index baac089d68..99f7993ea7 100644
--- a/src/backend/parser/parse_func.c
+++ b/src/backend/parser/parse_func.c
@@ -2501,6 +2501,7 @@ check_srf_call_placement(ParseState *pstate, Node *last_srf, int location)
 			break;
 		case EXPR_KIND_COLUMN_DEFAULT:
 		case EXPR_KIND_FUNCTION_DEFAULT:
+		case EXPR_KIND_VARIABLE_DEFAULT:
 			err = _("set-returning functions are not allowed in DEFAULT expressions");
 			break;
 		case EXPR_KIND_INDEX_EXPRESSION:
@@ -2539,6 +2540,9 @@ check_srf_call_placement(ParseState *pstate, Node *last_srf, int location)
 		case EXPR_KIND_CYCLE_MARK:
 			errkind = true;
 			break;
+		case EXPR_KIND_LET_TARGET:
+			err = _("set-returning functions are not allowed in CALL arguments");
+			break;
 
 			/*
 			 * There is intentionally no default: case here, so that the
diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c
index 6e8fbc4780..26acb72a53 100644
--- a/src/backend/parser/parse_target.c
+++ b/src/backend/parser/parse_target.c
@@ -89,7 +89,9 @@ transformTargetEntry(ParseState *pstate,
 		 * through unmodified.  (transformExpr will throw the appropriate
 		 * error if we're disallowing it.)
 		 */
-		if (exprKind == EXPR_KIND_UPDATE_SOURCE && IsA(node, SetToDefault))
+		if ((exprKind == EXPR_KIND_UPDATE_SOURCE ||
+			 exprKind == EXPR_KIND_LET_TARGET)
+			&& IsA(node, SetToDefault))
 			expr = node;
 		else
 			expr = transformExpr(pstate, node, exprKind);
diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
index da78f02775..8f9cabc30a 100644
--- a/src/backend/rewrite/rewriteHandler.c
+++ b/src/backend/rewrite/rewriteHandler.c
@@ -25,6 +25,7 @@
 #include "access/table.h"
 #include "catalog/dependency.h"
 #include "catalog/pg_type.h"
+#include "catalog/pg_variable.h"
 #include "commands/trigger.h"
 #include "foreign/fdwapi.h"
 #include "miscadmin.h"
@@ -3665,6 +3666,41 @@ RewriteQuery(Query *parsetree, List *rewrite_events)
 		}
 	}
 
+	/*
+	 * Rewrite SetToDefault by default expression of Let statement.
+	 */
+	if (event == CMD_LET)
+	{
+		Oid			resultVariable = parsetree->resultVariable;
+
+		Assert(OidIsValid(resultVariable));
+
+		if (list_length(parsetree->targetList) == 1 &&
+			IsA(((TargetEntry *) linitial(parsetree->targetList))->expr, SetToDefault))
+		{
+			Variable		var;
+			TargetEntry	   *tle;
+			TargetEntry	   *newtle;
+			Expr		   *defexpr;
+
+			/* Read schema variable metadata with defexpr */
+			initVariable(&var, resultVariable, false, false);
+
+			if (var.has_defexpr)
+				defexpr = (Expr *) var.defexpr;
+			else
+				defexpr = (Expr *) makeNullConst(var.typid, var.typmod, var.collation);
+
+			tle = (TargetEntry *) linitial(parsetree->targetList);
+			newtle = makeTargetEntry(defexpr,
+									  tle->resno,
+									  pstrdup(tle->resname),
+									  false);
+
+			parsetree->targetList = list_make1(newtle);
+		}
+	}
+
 	/*
 	 * If the statement is an insert, update, or delete, adjust its targetlist
 	 * as needed, and then fire INSERT/UPDATE/DELETE rules on it.
@@ -3673,7 +3709,7 @@ RewriteQuery(Query *parsetree, List *rewrite_events)
 	 * get executed.  Also, utilities aren't rewritten at all (do we still
 	 * need that check?)
 	 */
-	if (event != CMD_SELECT && event != CMD_UTILITY)
+	else if (event != CMD_SELECT && event != CMD_UTILITY)
 	{
 		int			result_relation;
 		RangeTblEntry *rt_entry;
diff --git a/src/backend/rewrite/rowsecurity.c b/src/backend/rewrite/rowsecurity.c
index fc26cb23a2..df9b0e39ee 100644
--- a/src/backend/rewrite/rowsecurity.c
+++ b/src/backend/rewrite/rowsecurity.c
@@ -213,10 +213,10 @@ get_row_security_policies(Query *root, RangeTblEntry *rte, int rt_index,
 	}
 
 	/*
-	 * For SELECT, UPDATE and DELETE, add security quals to enforce the USING
-	 * policies.  These security quals control access to existing table rows.
-	 * Restrictive policies are combined together using AND, and permissive
-	 * policies are combined together using OR.
+	 * For SELECT, LET, UPDATE and DELETE, add security quals to enforce the
+	 * USING policies.  These security quals control access to existing table
+	 * rows. Restrictive policies are combined together using AND, and
+	 * permissive policies are combined together using OR.
 	 */
 
 	get_policies_for_relation(rel, commandType, user_id, &permissive_policies,
diff --git a/src/backend/tcop/dest.c b/src/backend/tcop/dest.c
index 1dfadfa8e1..c676cb19fc 100644
--- a/src/backend/tcop/dest.c
+++ b/src/backend/tcop/dest.c
@@ -37,6 +37,7 @@
 #include "executor/functions.h"
 #include "executor/tqueue.h"
 #include "executor/tstoreReceiver.h"
+#include "executor/svariableReceiver.h"
 #include "libpq/libpq.h"
 #include "libpq/pqformat.h"
 #include "utils/portal.h"
@@ -152,6 +153,9 @@ CreateDestReceiver(CommandDest dest)
 
 		case DestTupleQueue:
 			return CreateTupleQueueDestReceiver(NULL);
+
+		case DestVariable:
+			return CreateVariableDestReceiver();
 	}
 
 	/* should never get here */
@@ -207,6 +211,7 @@ EndCommand(const QueryCompletion *qc, CommandDest dest, bool force_undecorated_o
 		case DestSQLFunction:
 		case DestTransientRel:
 		case DestTupleQueue:
+		case DestVariable:
 			break;
 	}
 }
@@ -252,6 +257,7 @@ NullCommand(CommandDest dest)
 		case DestSQLFunction:
 		case DestTransientRel:
 		case DestTupleQueue:
+		case DestVariable:
 			break;
 	}
 }
@@ -295,6 +301,7 @@ ReadyForQuery(CommandDest dest)
 		case DestSQLFunction:
 		case DestTransientRel:
 		case DestTupleQueue:
+		case DestVariable:
 			break;
 	}
 }
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 825fd55107..78ba782b91 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -2604,7 +2604,6 @@ exec_describe_statement_message(const char *stmt_name)
 	}
 	else
 		pq_putemptymessage('n');	/* NoData */
-
 }
 
 /*
diff --git a/src/backend/tcop/pquery.c b/src/backend/tcop/pquery.c
index 44f5fe8fc9..5b70fc782a 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_schema_variables = 0;
+	qd->schema_variables = NULL;
+
 	/* null these fields until set by ExecutorStart */
 	qd->tupDesc = NULL;
 	qd->estate = NULL;
@@ -178,6 +181,9 @@ ProcessQuery(PlannedStmt *plan,
 			case CMD_DELETE:
 				SetQueryCompletion(qc, CMDTAG_DELETE, queryDesc->estate->es_processed);
 				break;
+			case CMD_LET:
+				SetQueryCompletion(qc, CMDTAG_LET, queryDesc->estate->es_processed);
+				break;
 			default:
 				SetQueryCompletion(qc, CMDTAG_UNKNOWN, queryDesc->estate->es_processed);
 				break;
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 16c6f17e23..9440933486 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -48,6 +48,7 @@
 #include "commands/proclang.h"
 #include "commands/publicationcmds.h"
 #include "commands/schemacmds.h"
+#include "commands/schema_variable.h"
 #include "commands/seclabel.h"
 #include "commands/sequence.h"
 #include "commands/subscriptioncmds.h"
@@ -186,6 +187,7 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree)
 		case T_CreateRangeStmt:
 		case T_CreateRoleStmt:
 		case T_CreateSchemaStmt:
+		case T_CreateSchemaVarStmt:
 		case T_CreateSeqStmt:
 		case T_CreateStatsStmt:
 		case T_CreateStmt:
@@ -1352,6 +1354,10 @@ ProcessUtilitySlow(ParseState *pstate,
 				}
 				break;
 
+			case T_CreateSchemaVarStmt:
+				address = DefineSchemaVariable(pstate, (CreateSchemaVarStmt *) parsetree);
+				break;
+
 				/*
 				 * ************* object creation / destruction **************
 				 */
@@ -2086,6 +2092,8 @@ QueryReturnsTuples(Query *parsetree)
 			break;
 		case CMD_UTILITY:
 			return UtilityReturnsTuples(parsetree->utilityStmt);
+		case CMD_LET:
+			break;
 		case CMD_UNKNOWN:
 		case CMD_NOTHING:
 			/* probably shouldn't get here */
@@ -2276,6 +2284,9 @@ AlterObjectTypeCommandTag(ObjectType objtype)
 		case OBJECT_STATISTIC_EXT:
 			tag = CMDTAG_ALTER_STATISTICS;
 			break;
+		case OBJECT_VARIABLE:
+			tag = CMDTAG_ALTER_VARIABLE;
+			break;
 		default:
 			tag = CMDTAG_UNKNOWN;
 			break;
@@ -2326,6 +2337,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:
 			{
@@ -2580,6 +2595,9 @@ CreateCommandTag(Node *parsetree)
 				case OBJECT_STATISTIC_EXT:
 					tag = CMDTAG_DROP_STATISTICS;
 					break;
+				case OBJECT_VARIABLE:
+					tag = CMDTAG_DROP_VARIABLE;
+					break;
 				default:
 					tag = CMDTAG_UNKNOWN;
 			}
@@ -2868,6 +2886,9 @@ CreateCommandTag(Node *parsetree)
 				case DISCARD_SEQUENCES:
 					tag = CMDTAG_DISCARD_SEQUENCES;
 					break;
+				case DISCARD_VARIABLES:
+					tag = CMDTAG_DISCARD_VARIABLES;
+					break;
 				default:
 					tag = CMDTAG_UNKNOWN;
 			}
@@ -3076,6 +3097,9 @@ CreateCommandTag(Node *parsetree)
 					case CMD_DELETE:
 						tag = CMDTAG_DELETE;
 						break;
+					case CMD_LET:
+						tag = CMDTAG_LET;
+						break;
 					case CMD_UTILITY:
 						tag = CreateCommandTag(stmt->utilityStmt);
 						break;
@@ -3136,6 +3160,9 @@ CreateCommandTag(Node *parsetree)
 					case CMD_DELETE:
 						tag = CMDTAG_DELETE;
 						break;
+					case CMD_LET:
+						tag = CMDTAG_LET;
+						break;
 					case CMD_UTILITY:
 						tag = CreateCommandTag(stmt->utilityStmt);
 						break;
@@ -3148,6 +3175,10 @@ CreateCommandTag(Node *parsetree)
 			}
 			break;
 
+		case T_CreateSchemaVarStmt:
+			tag = CMDTAG_CREATE_VARIABLE;
+			break;
+
 		default:
 			elog(WARNING, "unrecognized node type: %d",
 				 (int) nodeTag(parsetree));
@@ -3195,6 +3226,7 @@ GetCommandLogLevel(Node *parsetree)
 			break;
 
 		case T_PLAssignStmt:
+		case T_LetStmt:
 			lev = LOGSTMT_ALL;
 			break;
 
@@ -3631,6 +3663,7 @@ GetCommandLogLevel(Node *parsetree)
 					case CMD_UPDATE:
 					case CMD_INSERT:
 					case CMD_DELETE:
+					case CMD_LET:
 						lev = LOGSTMT_MOD;
 						break;
 
@@ -3661,6 +3694,7 @@ GetCommandLogLevel(Node *parsetree)
 					case CMD_UPDATE:
 					case CMD_INSERT:
 					case CMD_DELETE:
+					case CMD_LET:
 						lev = LOGSTMT_MOD;
 						break;
 
diff --git a/src/backend/utils/adt/acl.c b/src/backend/utils/adt/acl.c
index 7861a0a613..a1152a4fa7 100644
--- a/src/backend/utils/adt/acl.c
+++ b/src/backend/utils/adt/acl.c
@@ -306,6 +306,12 @@ aclparse(const char *s, AclItem *aip)
 			case ACL_CONNECT_CHR:
 				read = ACL_CONNECT;
 				break;
+			case ACL_READ_CHR:
+				read = ACL_READ;
+				break;
+			case ACL_WRITE_CHR:
+				read = ACL_WRITE;
+				break;
 			case 'R':			/* ignore old RULE privileges */
 				read = 0;
 				break;
@@ -794,6 +800,10 @@ acldefault(ObjectType objtype, Oid ownerId)
 			world_default = ACL_USAGE;
 			owner_default = ACL_ALL_RIGHTS_TYPE;
 			break;
+		case OBJECT_VARIABLE:
+			world_default = ACL_NO_RIGHTS;
+			owner_default = ACL_ALL_RIGHTS_VARIABLE;
+			break;
 		default:
 			elog(ERROR, "unrecognized objtype: %d", (int) objtype);
 			world_default = ACL_NO_RIGHTS;	/* keep compiler quiet */
@@ -888,6 +898,9 @@ acldefault_sql(PG_FUNCTION_ARGS)
 		case 'T':
 			objtype = OBJECT_TYPE;
 			break;
+		case 'V':
+			objtype = OBJECT_VARIABLE;
+			break;
 		default:
 			elog(ERROR, "unrecognized objtype abbreviation: %c", objtypec);
 	}
@@ -1604,6 +1617,10 @@ convert_priv_string(text *priv_type_text)
 		return ACL_CONNECT;
 	if (pg_strcasecmp(priv_type, "RULE") == 0)
 		return 0;				/* ignore old RULE privileges */
+	if (pg_strcasecmp(priv_type, "READ") == 0)
+		return ACL_READ;
+	if (pg_strcasecmp(priv_type, "WRITE") == 0)
+		return ACL_WRITE;
 
 	ereport(ERROR,
 			(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
@@ -1698,6 +1715,10 @@ convert_aclright_to_string(int aclright)
 			return "TEMPORARY";
 		case ACL_CONNECT:
 			return "CONNECT";
+		case ACL_READ:
+			return "READ";
+		case ACL_WRITE:
+			return "WRITE";
 		default:
 			elog(ERROR, "unrecognized aclright: %d", aclright);
 			return NULL;
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 0a4fa93d01..60150d9af8 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"
@@ -7864,6 +7865,14 @@ get_parameter(Param *param, deparse_context *context)
 		return;
 	}
 
+	/* translate paramvarid to schema variable name */
+	if (param->paramkind == PARAM_VARIABLE)
+	{
+		appendStringInfo(context->buf, "%s",
+						 schema_variable_get_name(param->paramvarid));
+		return;
+	}
+
 	/*
 	 * If it's an external parameter, see if the outermost namespace provides
 	 * function argument names.
diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c
index 6bba5f8ec4..d8c8ad7ff4 100644
--- a/src/backend/utils/cache/lsyscache.c
+++ b/src/backend/utils/cache/lsyscache.c
@@ -35,6 +35,7 @@
 #include "catalog/pg_statistic.h"
 #include "catalog/pg_transform.h"
 #include "catalog/pg_type.h"
+#include "catalog/pg_variable.h"
 #include "miscadmin.h"
 #include "nodes/makefuncs.h"
 #include "utils/array.h"
@@ -1860,6 +1861,18 @@ get_relname_relid(const char *relname, Oid relnamespace)
 						   ObjectIdGetDatum(relnamespace));
 }
 
+/*
+ * get_varname_varid
+ *		Given name and namespace of variable, look up the OID.
+ */
+Oid
+get_varname_varid(const char *varname, Oid varnamespace)
+{
+	return GetSysCacheOid2(VARIABLENAMENSP, Anum_pg_variable_oid,
+						   PointerGetDatum(varname),
+						   ObjectIdGetDatum(varnamespace));
+}
+
 #ifdef NOT_USED
 /*
  * get_relnatts
diff --git a/src/backend/utils/cache/syscache.c b/src/backend/utils/cache/syscache.c
index e4dc4ee34e..2cb9a74ca8 100644
--- a/src/backend/utils/cache/syscache.c
+++ b/src/backend/utils/cache/syscache.c
@@ -73,6 +73,7 @@
 #include "catalog/pg_ts_template.h"
 #include "catalog/pg_type.h"
 #include "catalog/pg_user_mapping.h"
+#include "catalog/pg_variable.h"
 #include "lib/qunique.h"
 #include "utils/catcache.h"
 #include "utils/rel.h"
@@ -991,6 +992,28 @@ static const struct cachedesc cacheinfo[] = {
 			0
 		},
 		2
+	},
+	{VariableRelationId,		/* VARIABLENAMENSP */
+		VariableNameNspIndexId,
+		2,
+		{
+			Anum_pg_variable_varname,
+			Anum_pg_variable_varnamespace,
+			0,
+			0
+		},
+		8
+	},
+	{VariableRelationId,		/* VARIABLEOID */
+		VariableObjectIndexId,
+		1,
+		{
+			Anum_pg_variable_oid,
+			0,
+			0,
+			0
+		},
+		8
 	}
 };
 
diff --git a/src/backend/utils/fmgr/fmgr.c b/src/backend/utils/fmgr/fmgr.c
index b6835c2c4c..e8f3f709f4 100644
--- a/src/backend/utils/fmgr/fmgr.c
+++ b/src/backend/utils/fmgr/fmgr.c
@@ -1913,15 +1913,19 @@ get_call_expr_arg_stable(Node *expr, int argnum)
 	arg = (Node *) list_nth(args, argnum);
 
 	/*
-	 * Either a true Const or an external Param will have a value that doesn't
-	 * change during the execution of the query.  In future we might want to
-	 * consider other cases too, e.g. now().
+	 * Either a true Const or an external Param or variable will have a value
+	 * that doesn't change during the execution of the query.  In future we
+	 * might want to consider other cases too, e.g. now().
 	 */
 	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/pg_dump/common.c b/src/bin/pg_dump/common.c
index 1a261a5545..191b606077 100644
--- a/src/bin/pg_dump/common.c
+++ b/src/bin/pg_dump/common.c
@@ -260,6 +260,9 @@ getSchemaData(Archive *fout, int *numTablesPtr)
 	pg_log_info("reading subscriptions");
 	getSubscriptions(fout);
 
+	pg_log_info("reading variables");
+	getVariables(fout);
+
 	*numTablesPtr = numTables;
 	return tblinfo;
 }
diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index 3bc86635f7..6ec33c185e 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -113,12 +113,14 @@ typedef struct _restoreOptions
 	int			selFunction;
 	int			selTrigger;
 	int			selTable;
+	int			selVariable;
 	SimpleStringList indexNames;
 	SimpleStringList functionNames;
 	SimpleStringList schemaNames;
 	SimpleStringList schemaExcludeNames;
 	SimpleStringList triggerNames;
 	SimpleStringList tableNames;
+	SimpleStringList variableNames;
 
 	int			useDB;
 	ConnParams	cparams;		/* parameters to use if useDB */
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 86de26a4bf..a72e04978f 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -2948,6 +2948,14 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 					!simple_string_list_member(&ropt->triggerNames, te->tag))
 					return 0;
 			}
+			else if (strcmp(te->desc, "VARIABLE") == 0)
+			{
+				if (!ropt->selVariable)
+					return 0;
+				if (ropt->variableNames.head != NULL &&
+					!simple_string_list_member(&ropt->variableNames, te->tag))
+					return 0;
+			}
 			else
 				return 0;
 		}
@@ -3434,6 +3442,7 @@ _getObjectDescription(PQExpBuffer buf, TocEntry *te)
 		strcmp(type, "TEXT SEARCH DICTIONARY") == 0 ||
 		strcmp(type, "TEXT SEARCH CONFIGURATION") == 0 ||
 		strcmp(type, "STATISTICS") == 0 ||
+		strcmp(type, "VARIABLE") == 0 ||
 	/* non-schema-specified objects */
 		strcmp(type, "DATABASE") == 0 ||
 		strcmp(type, "PROCEDURAL LANGUAGE") == 0 ||
@@ -3619,7 +3628,8 @@ _printTocEntry(ArchiveHandle *AH, TocEntry *te, bool isData)
 			strcmp(te->desc, "SERVER") == 0 ||
 			strcmp(te->desc, "STATISTICS") == 0 ||
 			strcmp(te->desc, "PUBLICATION") == 0 ||
-			strcmp(te->desc, "SUBSCRIPTION") == 0)
+			strcmp(te->desc, "SUBSCRIPTION") == 0 ||
+			strcmp(te->desc, "VARIABLE") == 0)
 		{
 			PQExpBuffer temp = createPQExpBuffer();
 
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index d0ea489614..2f1a0cd75d 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -271,6 +271,7 @@ static void dumpPolicy(Archive *fout, const PolicyInfo *polinfo);
 static void dumpPublication(Archive *fout, const PublicationInfo *pubinfo);
 static void dumpPublicationTable(Archive *fout, const PublicationRelInfo *pubrinfo);
 static void dumpSubscription(Archive *fout, const SubscriptionInfo *subinfo);
+static void dumpVariable(Archive *fout, const VariableInfo * varinfo);
 static void dumpDatabase(Archive *AH);
 static void dumpDatabaseConfig(Archive *AH, PQExpBuffer outbuf,
 							   const char *dbname, Oid dboid);
@@ -4639,6 +4640,232 @@ get_next_possible_free_pg_type_oid(Archive *fout, PQExpBuffer upgrade_query)
 	return next_possible_free_oid;
 }
 
+/*
+ * getVariables
+ *	  get information about variables
+ */
+void
+getVariables(Archive *fout)
+{
+	DumpOptions *dopt = fout->dopt;
+	PQExpBuffer query;
+	PQExpBuffer acl_subquery = createPQExpBuffer();
+	PQExpBuffer racl_subquery = createPQExpBuffer();
+	PQExpBuffer init_acl_subquery = createPQExpBuffer();
+	PQExpBuffer init_racl_subquery = createPQExpBuffer();
+	PGresult   *res;
+	VariableInfo *varinfo;
+	int			i_tableoid;
+	int			i_oid;
+	int			i_varname;
+	int			i_varnamespace;
+	int			i_vartype;
+	int			i_vartypname;
+	int			i_vardefexpr;
+	int			i_rolname;
+	int			i_varacl;
+	int			i_rvaracl;
+	int			i_initvaracl;
+	int			i_initrvaracl;
+	int			i_vareoxaction;
+	int			i_varisnotnull;
+	int			i_varisimmutable;
+	int			i,
+				ntups;
+
+	if (fout->remoteVersion < 130000)
+		return;
+
+	acl_subquery = createPQExpBuffer();
+	racl_subquery = createPQExpBuffer();
+	init_acl_subquery = createPQExpBuffer();
+	init_racl_subquery = createPQExpBuffer();
+
+	buildACLQueries(acl_subquery, racl_subquery, init_acl_subquery,
+					init_racl_subquery, "v.varacl", "v.varowner", "'V'",
+					dopt->binary_upgrade);
+
+	query = createPQExpBuffer();
+
+	resetPQExpBuffer(query);
+
+	/* Get the variables in current database. */
+	appendPQExpBuffer(query,
+					  "SELECT v.tableoid, v.oid, v.varname, "
+					  "v.vareoxaction, "
+					  "v.varnamespace, "
+					  "(%s varowner) AS rolname, "
+					  "%s as varacl, "
+					  "%s as rvaracl, "
+					  "%s as initvaracl, "
+					  "%s as initrvaracl, "
+					  "v.vartype, "
+					  "pg_catalog.format_type(v.vartype, v.vartypmod) as vartypname, "
+					  "v.varisnotnull, "
+					  "v.varisimmutable, "
+					  "pg_catalog.pg_get_expr(v.vardefexpr,0) as vardefexpr "
+					  "FROM pg_variable v "
+					  "LEFT JOIN pg_init_privs pip "
+					  "ON (v.oid = pip.objoid "
+					  "AND pip.classoid = 'pg_variable'::regclass "
+					  "AND pip.objsubid = 0)",
+					  username_subquery,
+					  acl_subquery->data,
+					  racl_subquery->data,
+					  init_acl_subquery->data,
+					  init_racl_subquery->data);
+
+	destroyPQExpBuffer(acl_subquery);
+	destroyPQExpBuffer(racl_subquery);
+	destroyPQExpBuffer(init_acl_subquery);
+	destroyPQExpBuffer(init_racl_subquery);
+
+	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+
+	ntups = PQntuples(res);
+
+	i_tableoid = PQfnumber(res, "tableoid");
+	i_oid = PQfnumber(res, "oid");
+	i_varname = PQfnumber(res, "varname");
+	i_varnamespace = PQfnumber(res, "varnamespace");
+	i_rolname = PQfnumber(res, "rolname");
+	i_vartype = PQfnumber(res, "vartype");
+	i_vartypname = PQfnumber(res, "vartypname");
+	i_vareoxaction = PQfnumber(res, "vareoxaction");
+	i_vardefexpr = PQfnumber(res, "vardefexpr");
+	i_varacl = PQfnumber(res, "varacl");
+	i_rvaracl = PQfnumber(res, "rvaracl");
+	i_initvaracl = PQfnumber(res, "initvaracl");
+	i_initrvaracl = PQfnumber(res, "initrvaracl");
+	i_varisnotnull = PQfnumber(res, "varisnotnull");
+	i_varisimmutable = PQfnumber(res, "varisimmutable");
+
+	varinfo = pg_malloc(ntups * sizeof(VariableInfo));
+
+	for (i = 0; i < ntups; i++)
+	{
+		TypeInfo   *vtype;
+
+		varinfo[i].dobj.objType = DO_VARIABLE;
+		varinfo[i].dobj.catId.tableoid =
+			atooid(PQgetvalue(res, i, i_tableoid));
+		varinfo[i].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
+		AssignDumpId(&varinfo[i].dobj);
+		varinfo[i].dobj.name = pg_strdup(PQgetvalue(res, i, i_varname));
+		varinfo[i].dobj.namespace =
+			findNamespace(atooid(PQgetvalue(res, i, i_varnamespace)));
+
+		varinfo[i].rolname = pg_strdup(PQgetvalue(res, i, i_rolname));
+		varinfo[i].vartype = atooid(PQgetvalue(res, i, i_vartype));
+		varinfo[i].vartypname = pg_strdup(PQgetvalue(res, i, i_vartypname));
+
+		varinfo[i].vareoxaction = pg_strdup(PQgetvalue(res, i, i_vareoxaction));
+
+		varinfo[i].varacl = pg_strdup(PQgetvalue(res, i, i_varacl));
+		varinfo[i].rvaracl = pg_strdup(PQgetvalue(res, i, i_rvaracl));
+		varinfo[i].initvaracl = pg_strdup(PQgetvalue(res, i, i_initvaracl));
+		varinfo[i].initrvaracl = pg_strdup(PQgetvalue(res, i, i_initrvaracl));
+
+		varinfo[i].varisnotnull = *(PQgetvalue(res, i, i_varisnotnull)) == 't';
+		varinfo[i].varisimmutable = *(PQgetvalue(res, i, i_varisimmutable)) == 't';
+
+		/* Decide whether we want to dump it */
+		selectDumpableObject(&(varinfo[i].dobj), fout);
+
+		/* Do not try to dump ACL if no ACL exists. */
+		if (PQgetisnull(res, i, i_varacl) && PQgetisnull(res, i, i_rvaracl) &&
+			PQgetisnull(res, i, i_initvaracl) &&
+			PQgetisnull(res, i, i_initrvaracl))
+			varinfo[i].dobj.dump &= ~DUMP_COMPONENT_ACL;
+
+		if (PQgetisnull(res, i, i_vardefexpr))
+			varinfo[i].vardefexpr = NULL;
+		else
+			varinfo[i].vardefexpr = pg_strdup(PQgetvalue(res, i, i_vardefexpr));
+
+		if (strlen(varinfo[i].rolname) == 0)
+			pg_log_warning("owner of variable \"%s\" appears to be invalid",
+						   varinfo[i].dobj.name);
+
+		/* Decide whether we want to dump it */
+		selectDumpableObject(&(varinfo[i].dobj), fout);
+
+		vtype = findTypeByOid(varinfo[i].vartype);
+		addObjectDependency(&varinfo[i].dobj, vtype->dobj.dumpId);
+	}
+	PQclear(res);
+
+	destroyPQExpBuffer(query);
+}
+
+/*
+ * dumpVariable
+ *	  dump the definition of the given variables
+ */
+static void
+dumpVariable(Archive *fout, const VariableInfo * varinfo)
+{
+	DumpOptions *dopt = fout->dopt;
+
+	PQExpBuffer delq;
+	PQExpBuffer query;
+	const char *varname;
+	const char *vartypname;
+	const char *vardefexpr;
+	const char *vareoxaction;
+	const char *varisnotnull;
+	const char *varisimmutable;
+
+	/* Skip if not to be dumped */
+	if (!varinfo->dobj.dump || dopt->dataOnly)
+		return;
+
+	delq = createPQExpBuffer();
+	query = createPQExpBuffer();
+
+	varname = fmtQualifiedDumpable(varinfo);
+	vartypname = varinfo->vartypname;
+	vardefexpr = varinfo->vardefexpr;
+	vareoxaction = varinfo->vareoxaction;
+	varisnotnull = varinfo->varisnotnull ? " NOT NULL" : "";
+	varisimmutable = varinfo->varisimmutable ? "IMMUTABLE " : "";
+
+	appendPQExpBuffer(delq, "DROP VARIABLE %s;\n",
+					  varname);
+
+	appendPQExpBuffer(query, "CREATE %sVARIABLE %s AS %s%s",
+					  varisimmutable, varname, vartypname, varisnotnull);
+
+	if (vardefexpr)
+		appendPQExpBuffer(query, " DEFAULT %s",
+						  vardefexpr);
+
+	if (strcmp(vareoxaction, "d") == 0)
+		appendPQExpBuffer(query, " ON COMMIT DROP");
+	else if (strcmp(vareoxaction, "r") == 0)
+		appendPQExpBuffer(query, " ON TRANSACTION END RESET");
+
+	appendPQExpBuffer(query, ";\n");
+
+	if (varinfo->dobj.dump & DUMP_COMPONENT_DEFINITION)
+		ArchiveEntry(fout, varinfo->dobj.catId, varinfo->dobj.dumpId,
+					 ARCHIVE_OPTS(.tag = varinfo->dobj.name,
+								  .namespace = varinfo->dobj.namespace->dobj.name,
+								  .owner = varinfo->rolname,
+								  .description = "VARIABLE",
+								  .section = SECTION_PRE_DATA,
+								  .createStmt = query->data,
+								  .dropStmt = delq->data));
+
+	if (varinfo->dobj.dump & DUMP_COMPONENT_COMMENT)
+		dumpComment(fout, "VARIABLE", varname,
+					NULL, varinfo->rolname,
+					varinfo->dobj.catId, 0, varinfo->dobj.dumpId);
+
+	destroyPQExpBuffer(delq);
+	destroyPQExpBuffer(query);
+}
+
 static void
 binary_upgrade_set_type_oids_by_type_oid(Archive *fout,
 										 PQExpBuffer upgrade_buffer,
@@ -10472,6 +10699,9 @@ dumpDumpableObject(Archive *fout, const DumpableObject *dobj)
 		case DO_SUBSCRIPTION:
 			dumpSubscription(fout, (const SubscriptionInfo *) dobj);
 			break;
+		case DO_VARIABLE:
+			dumpVariable(fout, (VariableInfo *) dobj);
+			break;
 		case DO_PRE_DATA_BOUNDARY:
 		case DO_POST_DATA_BOUNDARY:
 			/* never dumped, nothing to do */
@@ -18625,6 +18855,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 			case DO_CONVERSION:
 			case DO_TABLE:
 			case DO_TABLE_ATTACH:
+			case DO_VARIABLE:
 			case DO_ATTRDEF:
 			case DO_PROCLANG:
 			case DO_CAST:
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 5340843081..9eced17344 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -52,6 +52,7 @@ typedef enum
 	DO_TABLE,
 	DO_TABLE_ATTACH,
 	DO_ATTRDEF,
+	DO_VARIABLE,
 	DO_INDEX,
 	DO_INDEX_ATTACH,
 	DO_STATSEXT,
@@ -81,7 +82,7 @@ typedef enum
 	DO_POLICY,
 	DO_PUBLICATION,
 	DO_PUBLICATION_REL,
-	DO_SUBSCRIPTION
+	DO_SUBSCRIPTION,
 } DumpableObjectType;
 
 /* component types of an object which can be selected for dumping */
@@ -643,6 +644,25 @@ typedef struct _SubscriptionInfo
 	char	   *subpublications;
 } SubscriptionInfo;
 
+/*
+ * The VariableInfo struct is used to represent schema variables
+ */
+typedef struct _VariableInfo
+{
+	DumpableObject dobj;
+	Oid			vartype;
+	char	   *vartypname;
+	char	   *rolname;		/* name of owner, or empty string */
+	char	   *vareoxaction;
+	char	   *vardefexpr;
+	char	   *varacl;
+	char	   *rvaracl;
+	char	   *initvaracl;
+	char	   *initrvaracl;
+	bool		varisnotnull;
+	bool		varisimmutable;
+}			VariableInfo;
+
 /*
  * We build an array of these with an entry for each object that is an
  * extension member according to pg_depend.
@@ -734,5 +754,6 @@ extern PublicationInfo *getPublications(Archive *fout,
 extern void getPublicationTables(Archive *fout, TableInfo tblinfo[],
 								 int numTables);
 extern void getSubscriptions(Archive *fout);
+extern void getVariables(Archive *fout);
 
 #endif							/* PG_DUMP_H */
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index 46461fb6a1..cb76d70f2d 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -66,6 +66,7 @@ enum dbObjectTypePriorities
 	PRIO_TABLE_ATTACH,
 	PRIO_DUMMY_TYPE,
 	PRIO_ATTRDEF,
+	PRIO_VARIABLE,
 	PRIO_BLOB,
 	PRIO_PRE_DATA_BOUNDARY,		/* boundary! */
 	PRIO_TABLE_DATA,
@@ -106,6 +107,7 @@ static const int dbObjectTypePriority[] =
 	PRIO_TABLE,					/* DO_TABLE */
 	PRIO_TABLE_ATTACH,			/* DO_TABLE_ATTACH */
 	PRIO_ATTRDEF,				/* DO_ATTRDEF */
+	PRIO_VARIABLE,				/* DO_VARIABLE */
 	PRIO_INDEX,					/* DO_INDEX */
 	PRIO_INDEX_ATTACH,			/* DO_INDEX_ATTACH */
 	PRIO_STATSEXT,				/* DO_STATSEXT */
@@ -1492,6 +1494,10 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
 					 "POST-DATA BOUNDARY  (ID %d)",
 					 obj->dumpId);
 			return;
+		case DO_VARIABLE:
+			snprintf(buf, bufsize,
+					 "VARIABLE %s  (ID %d OID %u)",
+					 obj->name, obj->dumpId, obj->catId.oid);
 	}
 	/* shouldn't get here */
 	snprintf(buf, bufsize,
diff --git a/src/bin/pg_dump/pg_restore.c b/src/bin/pg_dump/pg_restore.c
index 589b4aed53..b844aceb5c 100644
--- a/src/bin/pg_dump/pg_restore.c
+++ b/src/bin/pg_dump/pg_restore.c
@@ -101,6 +101,7 @@ main(int argc, char **argv)
 		{"trigger", 1, NULL, 'T'},
 		{"use-list", 1, NULL, 'L'},
 		{"username", 1, NULL, 'U'},
+		{"variable", 1, NULL, 'A'},
 		{"verbose", 0, NULL, 'v'},
 		{"single-transaction", 0, NULL, '1'},
 
@@ -148,7 +149,7 @@ main(int argc, char **argv)
 		}
 	}
 
-	while ((c = getopt_long(argc, argv, "acCd:ef:F:h:I:j:lL:n:N:Op:P:RsS:t:T:U:vwWx1",
+	while ((c = getopt_long(argc, argv, "A:acCd:ef:F:h:I:j:lL:n:N:Op:P:RsS:t:T:U:vwWx1",
 							cmdopts, NULL)) != -1)
 	{
 		switch (c)
@@ -156,6 +157,11 @@ main(int argc, char **argv)
 			case 'a':			/* Dump data only */
 				opts->dataOnly = 1;
 				break;
+			case 'A':			/* vAriable */
+				opts->selTypes = 1;
+				opts->selVariable = 1;
+				simple_string_list_append(&opts->variableNames, optarg);
+				break;
 			case 'c':			/* clean (i.e., drop) schema prior to create */
 				opts->dropSchema = 1;
 				break;
@@ -473,6 +479,7 @@ usage(const char *progname)
 
 	printf(_("\nOptions controlling the restore:\n"));
 	printf(_("  -a, --data-only              restore only the data, no schema\n"));
+	printf(_("  -A, --variable=NAME          restore named schema variable\n"));
 	printf(_("  -c, --clean                  clean (drop) database objects before recreating\n"));
 	printf(_("  -C, --create                 create the target database\n"));
 	printf(_("  -e, --exit-on-error          exit on error, default is to continue\n"));
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index 86113df29c..fa4d7efc2b 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -2774,6 +2774,30 @@ my %tests = (
 		},
 	},
 
+	'CREATE VARIABLE test_variable' => {
+		all_runs     => 1,
+		catch_all    => 'CREATE ... commands',
+		create_order => 61,
+		create_sql   => 'CREATE VARIABLE dump_test.variable1 AS integer DEFAULT 0;',
+		regexp => qr/^
+			\QCREATE VARIABLE dump_test.variable1 AS integer DEFAULT 0;\E/xm,
+		like =>
+		  { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, },
+	},
+
+	'CREATE IMMUTABLE VARIABLE test_variable' => {
+		all_runs     => 1,
+		catch_all    => 'CREATE ... commands',
+		create_order => 61,
+		create_sql   => 'CREATE IMMUTABLE VARIABLE dump_test.variable2 AS integer DEFAULT 0;',
+		regexp => qr/^
+			\QCREATE IMMUTABLE VARIABLE dump_test.variable2 AS integer DEFAULT 0;\E/xm,
+		like =>
+		  { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, },
+	},
+
 	'CREATE VIEW test_view' => {
 		create_order => 61,
 		create_sql   => 'CREATE VIEW dump_test.test_view
diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index 543401c6d6..6662f298dd 100644
--- a/src/bin/psql/command.c
+++ b/src/bin/psql/command.c
@@ -928,6 +928,9 @@ exec_command_d(PsqlScanState scan_state, bool active_branch, const char *cmd)
 						break;
 				}
 				break;
+			case 'V':			/* Variables */
+				success = listVariables(pattern, show_verbose);
+				break;
 			case 'x':			/* Extensions */
 				if (show_verbose)
 					success = listExtensionContents(pattern);
diff --git a/src/bin/psql/common.c b/src/bin/psql/common.c
index 028a357991..e9c8123d20 100644
--- a/src/bin/psql/common.c
+++ b/src/bin/psql/common.c
@@ -361,6 +361,7 @@ AcceptResult(const PGresult *result, bool show_error)
 	if (!result)
 		OK = false;
 	else
+	{
 		switch (PQresultStatus(result))
 		{
 			case PGRES_COMMAND_OK:
@@ -384,6 +385,7 @@ AcceptResult(const PGresult *result, bool show_error)
 							 PQresultStatus(result));
 				break;
 		}
+	}
 
 	if (!OK && show_error)
 	{
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index fdc2a89085..41140a8b24 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -5067,6 +5067,88 @@ listSchemas(const char *pattern, bool verbose, bool showSystem)
 	return true;
 }
 
+/*
+ * \dV
+ *
+ * listVariables()
+ */
+bool
+listVariables(const char *pattern, bool verbose)
+{
+	PQExpBufferData buf;
+	PGresult   *res;
+	printQueryOpt myopt = pset.popt;
+	static const bool translate_columns[] = {false, false, false, false, false, false, false, false};
+
+	initPQExpBuffer(&buf);
+
+	printfPQExpBuffer(&buf,
+					  "SELECT n.nspname as \"%s\",\n"
+					  "  v.varname as \"%s\",\n"
+					  "  pg_catalog.format_type(v.vartype, v.vartypmod) as \"%s\",\n"
+					  "  NOT v.varisnotnull as \"%s\",\n"
+					  "  NOT v.varisimmutable as \"%s\",\n"
+					  "  pg_catalog.pg_get_expr(v.vardefexpr, 0) as \"%s\",\n"
+					  "  pg_catalog.pg_get_userbyid(v.varowner) as \"%s\",\n"
+					  "  CASE v.vareoxaction\n"
+					  "    WHEN 'd' THEN 'ON COMMIT DROP'\n"
+					  "    WHEN 'r' THEN 'ON TRANSACTION END RESET' END as \"%s\"\n",
+					  gettext_noop("Schema"),
+					  gettext_noop("Name"),
+					  gettext_noop("Type"),
+					  gettext_noop("Is nullable"),
+					  gettext_noop("Is mutable"),
+					  gettext_noop("Default"),
+					  gettext_noop("Owner"),
+					  gettext_noop("Transactional end action"));
+
+	appendPQExpBufferStr(&buf,
+						 "\nFROM pg_catalog.pg_variable v"
+						 "\n     LEFT JOIN pg_catalog.pg_namespace n ON n.oid = v.varnamespace");
+
+	appendPQExpBufferStr(&buf, "\nWHERE true\n");
+	if (!pattern)
+		appendPQExpBufferStr(&buf, "      AND n.nspname <> 'pg_catalog'\n"
+							 "      AND n.nspname <> 'information_schema'\n");
+
+	processSQLNamePattern(pset.db, &buf, pattern, true, false,
+						  "n.nspname", "v.varname", NULL,
+						  "pg_catalog.pg_variable_is_visible(v.oid)");
+
+	appendPQExpBufferStr(&buf, "ORDER BY 1,2;");
+
+	res = PSQLexec(buf.data);
+	termPQExpBuffer(&buf);
+	if (!res)
+		return false;
+
+	/*
+	 * Most functions in this file are content to print an empty table when
+	 * there are no matching objects.  We intentionally deviate from that
+	 * here, but only in !quiet mode, for historical reasons.
+	 */
+	if (PQntuples(res) == 0 && !pset.quiet)
+	{
+		if (pattern)
+			pg_log_error("Did not find any schema variable named \"%s\".",
+						 pattern);
+		else
+			pg_log_error("Did not find any schema variables.");
+	}
+	else
+	{
+		myopt.nullPrint = NULL;
+		myopt.title = _("List of variables");
+		myopt.translate_header = true;
+		myopt.translate_columns = translate_columns;
+		myopt.n_translate_columns = lengthof(translate_columns);
+
+		printQuery(res, &myopt, pset.queryFout, false, pset.logfile);
+	}
+
+	PQclear(res);
+	return true;
+}
 
 /*
  * \dFp
diff --git a/src/bin/psql/describe.h b/src/bin/psql/describe.h
index 71b320f1fc..a86fccc0d7 100644
--- a/src/bin/psql/describe.h
+++ b/src/bin/psql/describe.h
@@ -139,5 +139,7 @@ extern bool listOpFamilyOperators(const char *accessMethod_pattern,
 extern bool listOpFamilyFunctions(const char *access_method_pattern,
 								  const char *family_pattern, bool verbose);
 
+/* \dV */
+extern bool listVariables(const char *pattern, bool varbose);
 
 #endif							/* DESCRIBE_H */
diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c
index 36501d5e2b..8812a58333 100644
--- a/src/bin/psql/help.c
+++ b/src/bin/psql/help.c
@@ -264,6 +264,7 @@ slashUsage(unsigned short int pager)
 	fprintf(output, _("  \\dT[S+] [PATTERN]      list data types\n"));
 	fprintf(output, _("  \\du[S+] [PATTERN]      list roles\n"));
 	fprintf(output, _("  \\dv[S+] [PATTERN]      list views\n"));
+	fprintf(output, _("  \\dV     [PATTERN]      list variables\n"));
 	fprintf(output, _("  \\dx[+]  [PATTERN]      list extensions\n"));
 	fprintf(output, _("  \\dX     [PATTERN]      list extended statistics\n"));
 	fprintf(output, _("  \\dy     [PATTERN]      list event triggers\n"));
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index d34271e3b8..a624944086 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -629,6 +629,13 @@ static const SchemaQuery Query_for_list_of_collations = {
 	.result = "pg_catalog.quote_ident(c.collname)",
 };
 
+static const SchemaQuery Query_for_list_of_variables = {
+	.min_server_version = 120000,
+	.catname = "pg_catalog.pg_variable v",
+	.viscondition = "pg_catalog.pg_variable_is_visible(v.oid)",
+	.namespace = "v.varnamespace",
+	.result = "pg_catalog.quote_ident(v.varname)",
+};
 
 /*
  * Queries to get lists of names of various kinds of things, possibly
@@ -1107,6 +1114,7 @@ static const pgsql_thing_t words_after_create[] = {
 																	 * TABLE ... */
 	{"USER", Query_for_list_of_roles " UNION SELECT 'MAPPING FOR'"},
 	{"USER MAPPING FOR", NULL, NULL, NULL},
+	{"VARIABLE", NULL, NULL, &Query_for_list_of_variables},
 	{"VIEW", NULL, NULL, &Query_for_list_of_views},
 	{NULL}						/* end of list */
 };
@@ -1505,8 +1513,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", "LISTEN", "LOAD", "LOCK",
-		"MOVE", "NOTIFY", "PREPARE",
+		"FETCH", "GRANT", "IMPORT FOREIGN SCHEMA", "INSERT", "LET", "LISTEN", "LOAD",
+		"LOCK", "MOVE", "NOTIFY", "PREPARE",
 		"REASSIGN", "REFRESH MATERIALIZED VIEW", "REINDEX", "RELEASE",
 		"RESET", "REVOKE", "ROLLBACK",
 		"SAVEPOINT", "SECURITY LABEL", "SELECT", "SET", "SHOW", "START",
@@ -1525,7 +1533,7 @@ psql_completion(const char *text, int start, int end)
 		"\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL",
 		"\\dm", "\\dn", "\\do", "\\dO", "\\dp", "\\dP", "\\dPi", "\\dPt",
 		"\\drds", "\\dRs", "\\dRp", "\\ds", "\\dS",
-		"\\dt", "\\dT", "\\dv", "\\du", "\\dx", "\\dX", "\\dy",
+		"\\dt", "\\dT", "\\dv", "\\du", "\\dx", "\\dX", "\\dy", "\\dV",
 		"\\e", "\\echo", "\\ef", "\\elif", "\\else", "\\encoding",
 		"\\endif", "\\errverbose", "\\ev",
 		"\\f",
@@ -1930,6 +1938,9 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH_QUERY(Query_for_list_of_alter_system_set_vars);
 	else if (Matches("ALTER", "SYSTEM", "SET", MatchAny))
 		COMPLETE_WITH("TO");
+	/* ALTER VARIABLE <name> */
+	else if (Matches("ALTER", "VARIABLE", MatchAny))
+		COMPLETE_WITH("OWNER TO", "RENAME TO", "SET SCHEMA");
 	/* ALTER VIEW <name> */
 	else if (Matches("ALTER", "VIEW", MatchAny))
 		COMPLETE_WITH("ALTER COLUMN", "OWNER TO", "RENAME",
@@ -2709,7 +2720,7 @@ psql_completion(const char *text, int start, int end)
 /* CREATE TABLE --- is allowed inside CREATE SCHEMA, so use TailMatches */
 	/* Complete "CREATE TEMP/TEMPORARY" with the possible temp objects */
 	else if (TailMatches("CREATE", "TEMP|TEMPORARY"))
-		COMPLETE_WITH("SEQUENCE", "TABLE", "VIEW");
+		COMPLETE_WITH("SEQUENCE", "TABLE", "VIEW", "VARIABLE");
 	/* Complete "CREATE UNLOGGED" with TABLE or MATVIEW */
 	else if (TailMatches("CREATE", "UNLOGGED"))
 		COMPLETE_WITH("TABLE", "MATERIALIZED VIEW");
@@ -2997,6 +3008,14 @@ psql_completion(const char *text, int start, int end)
 		else if (TailMatches("=", MatchAnyExcept("*)")))
 			COMPLETE_WITH(",", ")");
 	}
+/* CREATE VARIABLE --- is allowed inside CREATE SCHEMA, so use TailMatches */
+	/* Complete CREATE VARIABLE <name> with AS */
+	else if (TailMatches("CREATE", "VARIABLE", MatchAny) ||
+			 TailMatches("TEMP|TEMPORARY", "VARIABLE", MatchAny))
+			COMPLETE_WITH("AS");
+	else if (TailMatches("VARIABLE", MatchAny, "AS"))
+			/* Complete CREATE VARIABLE <name> with AS types */
+			COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes, NULL);
 
 /* CREATE VIEW --- is allowed inside CREATE SCHEMA, so use TailMatches */
 	/* Complete CREATE [ OR REPLACE ] VIEW <name> with AS */
@@ -3111,7 +3130,7 @@ psql_completion(const char *text, int start, int end)
 
 /* DISCARD */
 	else if (Matches("DISCARD"))
-		COMPLETE_WITH("ALL", "PLANS", "SEQUENCES", "TEMP");
+		COMPLETE_WITH("ALL", "PLANS", "SEQUENCES", "TEMP", "VARIABLES");
 
 /* DO */
 	else if (Matches("DO"))
@@ -3217,6 +3236,12 @@ psql_completion(const char *text, int start, int end)
 	else if (Matches("DROP", "RULE", MatchAny, "ON", MatchAny))
 		COMPLETE_WITH("CASCADE", "RESTRICT");
 
+	/* DROP VARIABLE */
+	else if (Matches("DROP", "VARIABLE"))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables, NULL);
+	else if (Matches("DROP", "VARIABLE", MatchAny))
+		COMPLETE_WITH("CASCADE", "RESTRICT");
+
 /* EXECUTE */
 	else if (Matches("EXECUTE"))
 		COMPLETE_WITH_QUERY(Query_for_list_of_prepared_statements);
@@ -3388,6 +3413,7 @@ psql_completion(const char *text, int start, int end)
 									   " UNION SELECT 'ALL ROUTINES IN SCHEMA'"
 									   " UNION SELECT 'ALL SEQUENCES IN SCHEMA'"
 									   " UNION SELECT 'ALL TABLES IN SCHEMA'"
+									   " UNION SELECT 'ALL VARIABLES IN SCHEMA'"
 									   " UNION SELECT 'DATABASE'"
 									   " UNION SELECT 'DOMAIN'"
 									   " UNION SELECT 'FOREIGN DATA WRAPPER'"
@@ -3401,14 +3427,16 @@ psql_completion(const char *text, int start, int end)
 									   " UNION SELECT 'SEQUENCE'"
 									   " UNION SELECT 'TABLE'"
 									   " UNION SELECT 'TABLESPACE'"
-									   " UNION SELECT 'TYPE'");
+									   " UNION SELECT 'TYPE'"
+									   " UNION SELECT 'VARIABLE'");
 	}
 	else if (TailMatches("GRANT|REVOKE", MatchAny, "ON", "ALL"))
 		COMPLETE_WITH("FUNCTIONS IN SCHEMA",
 					  "PROCEDURES IN SCHEMA",
 					  "ROUTINES IN SCHEMA",
 					  "SEQUENCES IN SCHEMA",
-					  "TABLES IN SCHEMA");
+					  "TABLES IN SCHEMA",
+					  "VARIABLES IN SCHEMA");
 	else if (TailMatches("GRANT|REVOKE", MatchAny, "ON", "FOREIGN"))
 		COMPLETE_WITH("DATA WRAPPER", "SERVER");
 
@@ -3442,6 +3470,8 @@ psql_completion(const char *text, int start, int end)
 			COMPLETE_WITH_QUERY(Query_for_list_of_tablespaces);
 		else if (TailMatches("TYPE"))
 			COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes, NULL);
+		else if (TailMatches("VARIABLE"))
+			COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables, NULL);
 		else if (TailMatches("GRANT", MatchAny, MatchAny, MatchAny))
 			COMPLETE_WITH("TO");
 		else
@@ -3605,7 +3635,7 @@ psql_completion(const char *text, int start, int end)
 
 /* PREPARE xx AS */
 	else if (Matches("PREPARE", MatchAny, "AS"))
-		COMPLETE_WITH("SELECT", "UPDATE", "INSERT", "DELETE FROM");
+		COMPLETE_WITH("SELECT", "UPDATE", "INSERT", "DELETE FROM", "LET");
 
 /*
  * PREPARE TRANSACTION is missing on purpose. It's intended for transaction
@@ -3879,6 +3909,14 @@ psql_completion(const char *text, int start, int end)
 	else if (TailMatches("UPDATE", MatchAny, "SET", MatchAnyExcept("*=")))
 		COMPLETE_WITH("=");
 
+/* LET --- can be inside EXPLAIN, PREPARE etc */
+	/* If prev. word is LET suggest a list of variables */
+	else if (TailMatches("LET"))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables, NULL);
+	/* Complete LET <variable> with "=" */
+	else if (TailMatches("LET", MatchAny))
+		COMPLETE_WITH("=");
+
 /* USER MAPPING */
 	else if (Matches("ALTER|CREATE|DROP", "USER", "MAPPING"))
 		COMPLETE_WITH("FOR");
@@ -4040,6 +4078,8 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH_QUERY(Query_for_list_of_roles);
 	else if (TailMatchesCS("\\dv*"))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_views, NULL);
+	else if (TailMatchesCS("\\dV*"))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables, NULL);
 	else if (TailMatchesCS("\\dx*"))
 		COMPLETE_WITH_QUERY(Query_for_list_of_extensions);
 	else if (TailMatchesCS("\\dX*"))
diff --git a/src/include/catalog/dependency.h b/src/include/catalog/dependency.h
index f272e2c99f..e7ce899f0e 100644
--- a/src/include/catalog/dependency.h
+++ b/src/include/catalog/dependency.h
@@ -132,10 +132,11 @@ typedef enum ObjectClass
 	OCLASS_PUBLICATION,			/* pg_publication */
 	OCLASS_PUBLICATION_REL,		/* pg_publication_rel */
 	OCLASS_SUBSCRIPTION,		/* pg_subscription */
-	OCLASS_TRANSFORM			/* pg_transform */
+	OCLASS_TRANSFORM,			/* pg_transform */
+	OCLASS_VARIABLE				/* pg_variable */
 } ObjectClass;
 
-#define LAST_OCLASS		OCLASS_TRANSFORM
+#define LAST_OCLASS		OCLASS_VARIABLE
 
 /* flag bits for performDeletion/performMultipleDeletions: */
 #define PERFORM_DELETION_INTERNAL			0x0001	/* internal action */
diff --git a/src/include/catalog/namespace.h b/src/include/catalog/namespace.h
index aa2774e2d4..e3b22a1537 100644
--- a/src/include/catalog/namespace.h
+++ b/src/include/catalog/namespace.h
@@ -95,6 +95,8 @@ extern Oid	TypenameGetTypid(const char *typname);
 extern Oid	TypenameGetTypidExtended(const char *typname, bool temp_ok);
 extern bool TypeIsVisible(Oid typid);
 
+extern bool VariableIsVisible(Oid varid);
+
 extern FuncCandidateList FuncnameGetCandidates(List *names,
 											   int nargs, List *argnames,
 											   bool expand_variadic,
@@ -162,6 +164,10 @@ extern void SetTempNamespaceState(Oid tempNamespaceId,
 								  Oid tempToastNamespaceId);
 extern void ResetTempTableNamespace(void);
 
+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_uniq);
+
 extern OverrideSearchPath *GetOverrideSearchPath(MemoryContext context);
 extern OverrideSearchPath *CopyOverrideSearchPath(OverrideSearchPath *path);
 extern bool OverrideSearchPathMatchesCurrent(OverrideSearchPath *path);
diff --git a/src/include/catalog/pg_default_acl.h b/src/include/catalog/pg_default_acl.h
index 156fc0712d..22839aa356 100644
--- a/src/include/catalog/pg_default_acl.h
+++ b/src/include/catalog/pg_default_acl.h
@@ -68,6 +68,7 @@ DECLARE_UNIQUE_INDEX_PKEY(pg_default_acl_oid_index, 828, on pg_default_acl using
 #define DEFACLOBJ_FUNCTION		'f' /* function */
 #define DEFACLOBJ_TYPE			'T' /* type */
 #define DEFACLOBJ_NAMESPACE		'n' /* namespace */
+#define DEFACLOBJ_VARIABLE		'V' /* variable */
 
 #endif							/* EXPOSE_TO_CLIENT_CODE */
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f4957653ae..b556c7e99e 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6178,6 +6178,9 @@
   proname => 'pg_collation_is_visible', procost => '10', provolatile => 's',
   prorettype => 'bool', proargtypes => 'oid',
   prosrc => 'pg_collation_is_visible' },
+{ oid => '9221', descr => 'is schema variable visible in search path?',
+  proname => 'pg_variable_is_visible', procost => '10', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'oid', prosrc => 'pg_variable_is_visible' },
 
 { oid => '2854', descr => 'get OID of current session\'s temp schema, if any',
   proname => 'pg_my_temp_schema', provolatile => 's', proparallel => 'r',
diff --git a/src/include/catalog/pg_variable.h b/src/include/catalog/pg_variable.h
new file mode 100644
index 0000000000..30f84f6b6d
--- /dev/null
+++ b/src/include/catalog/pg_variable.h
@@ -0,0 +1,117 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_variable.h
+ *	  definition of schema variables system catalog (pg_variables)
+ *
+ *
+ * Portions Copyright (c) 1996-2020, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/pg_variable.h
+ *
+ * NOTES
+ *	  The Catalog.pm module reads this file and derives schema
+ *	  information.
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PG_VARIABLE_H
+#define PG_VARIABLE_H
+
+#include "catalog/genbki.h"
+#include "catalog/objectaddress.h"
+#include "catalog/pg_variable_d.h"
+#include "utils/acl.h"
+
+/* ----------------
+ *		pg_variable definition.  cpp turns this into
+ *		typedef struct FormData_pg_variable
+ * ----------------
+ */
+CATALOG(pg_variable,9222,VariableRelationId)
+{
+	Oid			oid;			/* oid */
+	NameData	varname;		/* variable name */
+	Oid			varnamespace;	/* OID of namespace containing variable class */
+	Oid			vartype;		/* OID of entry in pg_type for variable's type */
+	int32		vartypmod;		/* typmode for variable's type */
+	Oid			varowner;		/* class owner */
+	Oid			varcollation;	/* variable collation */
+	bool		varisnotnull;	/* Don't allow NULL */
+	bool		varisimmutable;	/* Don't allow changes */
+	char		vareoxaction;	/* action on transaction end */
+
+#ifdef CATALOG_VARLEN			/* variable-length fields start here */
+
+	/* list of expression trees for variable default (NULL if none) */
+	pg_node_tree vardefexpr BKI_DEFAULT(_null_);
+
+	aclitem		varacl[1] BKI_DEFAULT(_null_);	/* access permissions */
+
+#endif
+} FormData_pg_variable;
+
+typedef enum VariableEOXActionCodes
+{
+	VARIABLE_EOX_CODE_NOOP = 'n',	/* NOOP */
+	VARIABLE_EOX_CODE_DROP = 'd',	/* ON COMMIT DROP */
+	VARIABLE_EOX_CODE_RESET = 'r',	/* ON COMMIT RESET */
+}			VariableEOXActionCodes;
+
+/* ----------------
+ *		Form_pg_variable corresponds to a pointer to a tuple with
+ *		the format of pg_variable relation.
+ * ----------------
+ */
+typedef FormData_pg_variable * Form_pg_variable;
+
+DECLARE_UNIQUE_INDEX_PKEY(pg_variable_oid_index, 9223, on pg_variable using btree(oid oid_ops));
+#define VariableObjectIndexId 9223
+
+DECLARE_UNIQUE_INDEX(pg_variable_varname_nsp_index, 9224, on pg_variable using btree(varname name_ops, varnamespace oid_ops));
+#define VariableNameNspIndexId  9224
+
+typedef struct Variable
+{
+	Oid			oid;
+	char	   *name;
+	Oid			namespace;
+	Oid			typid;
+	int32		typmod;
+	Oid			owner;
+	Oid			collation;
+	bool		is_not_null;
+	bool		is_immutable;
+	VariableEOXAction eoxaction;
+	bool		has_defexpr;
+	Node	   *defexpr;
+	Acl		   *acl;
+} Variable;
+
+/* returns fields from pg_variable table */
+extern char *get_schema_variable_name(Oid varid);
+extern void get_schema_variable_type_typmod_collid(Oid varid,
+												   Oid *typid,
+												   int32 *typmod,
+												   Oid *collid);
+
+/* returns name of variable based on current search path */
+extern char *schema_variable_get_name(Oid varid);
+
+extern void initVariable(Variable *var,
+						 Oid varid,
+						 bool missing_ok,
+						 bool fast_only);
+extern ObjectAddress VariableCreate(const char *varName,
+									Oid varNamespace,
+									Oid varType,
+									int32 varTypmod,
+									Oid varOwner,
+									Oid varCollation,
+									Node *varDefexpr,
+									VariableEOXAction eoxaction,
+									bool is_not_null,
+									bool if_not_exists,
+									bool is_immutable);
+
+#endif							/* PG_VARIABLE_H */
diff --git a/src/include/commands/schema_variable.h b/src/include/commands/schema_variable.h
new file mode 100644
index 0000000000..0398b9d00a
--- /dev/null
+++ b/src/include/commands/schema_variable.h
@@ -0,0 +1,39 @@
+/*-------------------------------------------------------------------------
+ *
+ * schemavariable.h
+ *	  prototypes for schemavariable.c.
+ *
+ *
+ * Portions Copyright (c) 1996-2020, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/commands/schemavariable.h
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef SCHEMAVARIABLE_H
+#define SCHEMAVARIABLE_H
+
+#include "catalog/objectaddress.h"
+#include "catalog/pg_variable.h"
+#include "nodes/params.h"
+#include "nodes/parsenodes.h"
+#include "nodes/plannodes.h"
+#include "utils/queryenvironment.h"
+
+extern void ResetSchemaVariableCache(void);
+
+extern void RemoveSchemaVariable(Oid varid);
+extern ObjectAddress DefineSchemaVariable(ParseState *pstate, CreateSchemaVarStmt * stmt);
+
+extern Datum GetSchemaVariable(Oid varid, bool *isNull, Oid expected_typid, bool copy);
+extern Datum CopySchemaVariable(Oid varid, bool *isNull, Oid *typid);
+extern void SetSchemaVariable(Oid varid, Datum value, bool isNull, Oid typid);
+
+extern void register_variable_on_commit_action(Oid varid, VariableEOXAction action);
+
+extern void AtPreEOXact_SchemaVariable_on_commit_actions(bool isCommit);
+extern void AtEOXact_SchemaVariable_on_commit_actions(bool isCommit);
+
+#endif
diff --git a/src/include/executor/execExpr.h b/src/include/executor/execExpr.h
index 785600d04d..13b7902633 100644
--- a/src/include/executor/execExpr.h
+++ b/src/include/executor/execExpr.h
@@ -158,6 +158,7 @@ typedef enum ExprEvalOp
 	EEOP_PARAM_EXEC,
 	EEOP_PARAM_EXTERN,
 	EEOP_PARAM_CALLBACK,
+	EEOP_PARAM_VARIABLE,
 
 	/* return CaseTestExpr value */
 	EEOP_CASE_TESTVAL,
@@ -380,6 +381,13 @@ typedef struct ExprEvalStep
 			Oid			paramtype;	/* OID of parameter's datatype */
 		}			param;
 
+		/* for EEOP_PARAM_VARIABLE */
+		struct
+		{
+			Oid			varid;	/* OID of assigned variable */
+			Oid			vartype;	/* OID of parameter's datatype */
+		}			vparam;
+
 		/* for EEOP_PARAM_CALLBACK */
 		struct
 		{
@@ -735,6 +743,8 @@ extern void ExecEvalParamExec(ExprState *state, ExprEvalStep *op,
 							  ExprContext *econtext);
 extern void ExecEvalParamExtern(ExprState *state, ExprEvalStep *op,
 								ExprContext *econtext);
+extern void ExecEvalParamVariable(ExprState *state, ExprEvalStep *op,
+								  ExprContext *econtext);
 extern void ExecEvalSQLValueFunction(ExprState *state, ExprEvalStep *op);
 extern void ExecEvalCurrentOfExpr(ExprState *state, ExprEvalStep *op);
 extern void ExecEvalNextValueExpr(ExprState *state, ExprEvalStep *op);
diff --git a/src/include/executor/execdesc.h b/src/include/executor/execdesc.h
index 017ad87117..c85e7ee89d 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 schema variables buffer */
+	int			num_schema_variables;
+	SchemaVariableValue *schema_variables;
+
 	/* This field is set by ExecutorRun */
 	bool		already_executed;	/* true if previously executed */
 
diff --git a/src/include/executor/nodeSetVariable.h b/src/include/executor/nodeSetVariable.h
new file mode 100644
index 0000000000..0d2140fb2c
--- /dev/null
+++ b/src/include/executor/nodeSetVariable.h
@@ -0,0 +1,23 @@
+/*-------------------------------------------------------------------------
+ *
+ * nodeSetVariable.h
+ *
+ *
+ * Portions Copyright (c) 1996-2021, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/executor/nodeSetVariable.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef NODESETVARIABLE_H
+#define NODESETVARIABLE_H
+
+#include "nodes/execnodes.h"
+
+extern SetVariableState *ExecInitSetVariable(SetVariable *node,
+													 EState *estate, int eflags);
+extern void ExecEndSetVariable(SetVariableState *node);
+extern void ExecReScanSetVariable(SetVariableState *node);
+
+#endif							/* NODESETVARIABLE_H */
diff --git a/src/include/executor/spi.h b/src/include/executor/spi.h
index 6455d100f5..0fa13b8da6 100644
--- a/src/include/executor/spi.h
+++ b/src/include/executor/spi.h
@@ -95,6 +95,7 @@ typedef struct _SPI_plan *SPIPlanPtr;
 #define SPI_OK_REL_REGISTER		15
 #define SPI_OK_REL_UNREGISTER	16
 #define SPI_OK_TD_REGISTER		17
+#define SPI_OK_LET				18
 
 #define SPI_OPT_NONATOMIC		(1 << 0)
 
diff --git a/src/include/executor/svariableReceiver.h b/src/include/executor/svariableReceiver.h
new file mode 100644
index 0000000000..5305936547
--- /dev/null
+++ b/src/include/executor/svariableReceiver.h
@@ -0,0 +1,25 @@
+/*-------------------------------------------------------------------------
+ *
+ * svariableReceiver.h
+ *	  prototypes for svariableReceiver.c
+ *
+ *
+ * Portions Copyright (c) 1996-2020, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/executor/svariableReceiver.h
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef SVARIABLE_RECEIVER_H
+#define SVARIABLE_RECEIVER_H
+
+#include "tcop/dest.h"
+
+
+extern DestReceiver *CreateVariableDestReceiver(void);
+
+extern void SetVariableDestReceiverParams(DestReceiver *self, Oid varid);
+
+#endif							/* SVARIABLE_RECEIVER_H */
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index e7ae21c023..b886c5ae40 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -24,6 +24,7 @@
 #include "nodes/tidbitmap.h"
 #include "partitioning/partdefs.h"
 #include "storage/condition_variable.h"
+#include "tcop/dest.h"
 #include "utils/hsearch.h"
 #include "utils/queryenvironment.h"
 #include "utils/reltrigger.h"
@@ -541,6 +542,18 @@ typedef struct AsyncRequest
 	TupleTableSlot *result;		/* Result (NULL if no more tuples) */
 } AsyncRequest;
 
+/* ----------------
+ * SchemaVariableValue
+ * ----------------
+ */
+typedef struct SchemaVariableValue
+{
+	Oid			varid;
+	Oid			typid;
+	bool		isnull;
+	Datum		value;
+}			SchemaVariableValue;
+
 /* ----------------
  *	  EState information
  *
@@ -592,6 +605,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 schema variables */
+	int			es_num_schema_variables;
+
+	/* array of copied values of schema variables */
+	SchemaVariableValue *es_schema_variables;
+
 	QueryEnvironment *es_queryEnv;	/* query environment */
 
 	/* Other working state: */
@@ -1225,6 +1245,17 @@ typedef struct ModifyTableState
 	struct TransitionCaptureState *mt_oc_transition_capture;
 } ModifyTableState;
 
+/* ----------------
+ *	 SetVariableState information
+ * ----------------
+ */
+typedef struct SetVariableState
+{
+	PlanState	ps;
+	DestReceiver *dest;
+	Oid			varid;
+} SetVariableState;
+
 /* ----------------
  *	 AppendState information
  *
diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h
index d9e417bcd7..1ab5e0f4fb 100644
--- a/src/include/nodes/nodes.h
+++ b/src/include/nodes/nodes.h
@@ -46,6 +46,7 @@ typedef enum NodeTag
 	T_Result,
 	T_ProjectSet,
 	T_ModifyTable,
+	T_SetVariable,
 	T_Append,
 	T_MergeAppend,
 	T_RecursiveUnion,
@@ -105,6 +106,7 @@ typedef enum NodeTag
 	T_ResultState,
 	T_ProjectSetState,
 	T_ModifyTableState,
+	T_SetVariableState,
 	T_AppendState,
 	T_MergeAppendState,
 	T_RecursiveUnionState,
@@ -262,6 +264,7 @@ typedef enum NodeTag
 	T_RecursiveUnionPath,
 	T_LockRowsPath,
 	T_ModifyTablePath,
+	T_SetVariablePath,
 	T_LimitPath,
 	/* these aren't subclasses of Path: */
 	T_EquivalenceClass,
@@ -359,6 +362,7 @@ typedef enum NodeTag
 	T_CreateTableAsStmt,
 	T_CreateSeqStmt,
 	T_AlterSeqStmt,
+	T_CreateSchemaVarStmt,
 	T_VariableSetStmt,
 	T_VariableShowStmt,
 	T_DiscardStmt,
@@ -432,6 +436,7 @@ typedef enum NodeTag
 	T_AlterCollationStmt,
 	T_CallStmt,
 	T_AlterStatsStmt,
+	T_LetStmt,
 
 	/*
 	 * TAGS FOR PARSE TREE NODES (parsenodes.h)
@@ -685,7 +690,8 @@ typedef enum CmdType
 	CMD_SELECT,					/* select stmt */
 	CMD_UPDATE,					/* update stmt */
 	CMD_INSERT,					/* insert stmt */
-	CMD_DELETE,
+	CMD_DELETE,					/* delete stmt */
+	CMD_LET,					/* let stmt */
 	CMD_UTILITY,				/* cmds like create, destroy, copy, vacuum,
 								 * etc. */
 	CMD_NOTHING					/* dummy command for instead nothing rules
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 7a44bccdd3..bd3a1bef88 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -92,7 +92,9 @@ typedef uint32 AclMode;			/* a bitmask of privilege bits */
 #define ACL_CREATE		(1<<9)	/* for namespaces and databases */
 #define ACL_CREATE_TEMP (1<<10) /* for databases */
 #define ACL_CONNECT		(1<<11) /* for databases */
-#define N_ACL_RIGHTS	12		/* 1 plus the last 1<<x */
+#define ACL_READ		(1<<12) /* for variables */
+#define ACL_WRITE		(1<<13) /* for variables */
+#define N_ACL_RIGHTS	14		/* 1 plus the last 1<<x */
 #define ACL_NO_RIGHTS	0
 /* Currently, SELECT ... FOR [KEY] UPDATE/SHARE requires UPDATE privileges */
 #define ACL_SELECT_FOR_UPDATE	ACL_UPDATE
@@ -129,6 +131,7 @@ typedef struct Query
 
 	int			resultRelation; /* rtable index of target relation for
 								 * INSERT/UPDATE/DELETE; 0 for SELECT */
+	int			resultVariable; /* Oid of target variable or 0 */
 
 	bool		hasAggs;		/* has aggregates in tlist or havingQual */
 	bool		hasWindowFuncs; /* has window functions in tlist */
@@ -139,6 +142,7 @@ typedef struct Query
 	bool		hasModifyingCTE;	/* has INSERT/UPDATE/DELETE in WITH */
 	bool		hasForUpdate;	/* FOR [KEY] UPDATE/SHARE was specified */
 	bool		hasRowSecurity; /* rewriter has applied some RLS policy */
+	bool		hasSchemaVariables;	/* uses schema variables */
 
 	bool		isReturn;		/* is a RETURN statement */
 
@@ -1615,6 +1619,18 @@ typedef struct UpdateStmt
 	WithClause *withClause;		/* WITH clause */
 } UpdateStmt;
 
+/* ----------------------
+ *		Let Statement
+ * ----------------------
+ */
+typedef struct LetStmt
+{
+	NodeTag		type;
+	List	   *target;			/* target variable */
+	Node	   *selectStmt;		/* source expression */
+	int			location;
+}			LetStmt;
+
 /* ----------------------
  *		Select Statement
  *
@@ -1823,6 +1839,7 @@ typedef enum ObjectType
 	OBJECT_TSTEMPLATE,
 	OBJECT_TYPE,
 	OBJECT_USER_MAPPING,
+	OBJECT_VARIABLE,
 	OBJECT_VIEW
 } ObjectType;
 
@@ -2637,6 +2654,23 @@ typedef struct AlterSeqStmt
 	bool		missing_ok;		/* skip error if a role is missing? */
 } AlterSeqStmt;
 
+/* ----------------------
+ *		{Create|Alter} VARIABLE Statement
+ * ----------------------
+ */
+typedef struct CreateSchemaVarStmt
+{
+	NodeTag		type;
+	RangeVar   *variable;		/* the variable to create */
+	TypeName   *typeName;		/* the type of variable */
+	CollateClause *collClause;
+	Node	   *defexpr;		/* default expression */
+	VariableEOXAction eoxaction;	/* on commit action */
+	bool		if_not_exists;	/* do nothing if it already exists */
+	bool		is_not_null;	/* Disallow nulls */
+	bool		is_immutable;	/* Don't allow changes */
+}			CreateSchemaVarStmt;
+
 /* ----------------------
  *		Create {Aggregate|Operator|Type} Statement
  * ----------------------
@@ -3402,7 +3436,8 @@ typedef enum DiscardMode
 	DISCARD_ALL,
 	DISCARD_PLANS,
 	DISCARD_SEQUENCES,
-	DISCARD_TEMP
+	DISCARD_TEMP,
+	DISCARD_VARIABLES
 } DiscardMode;
 
 typedef struct DiscardStmt
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index a65bda7e3c..1b15c206b9 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -129,6 +129,8 @@ typedef struct PlannerGlobal
 	char		maxParallelHazard;	/* worst PROPARALLEL hazard level */
 
 	PartitionDirectory partition_directory; /* partition descriptors */
+
+	List	   *schemaVariables;	/* list of used schema variables */
 } PlannerGlobal;
 
 /* macro for fetching the Plan associated with a SubPlan node */
@@ -349,6 +351,7 @@ struct PlannerInfo
 										 * pseudoconstant = true */
 	bool		hasAlternativeSubPlans; /* true if we've made any of those */
 	bool		hasRecursion;	/* true if planning a recursive WITH item */
+	bool		hasSchemaVariables;	/* true if schema variables was used */
 
 	/*
 	 * Information about aggregates. Filled by preprocess_aggrefs().
@@ -1897,6 +1900,17 @@ typedef struct LimitPath
 	LimitOption limitOption;	/* FETCH FIRST with ties or exact number */
 } LimitPath;
 
+/*
+ * SetVariablePath represents setting evaluated value
+ * to schema variable. It is root node of plan of LET
+ * statement.
+ */
+typedef struct SetVariablePath
+{
+	Path		path;
+	Path	   *subpath;		/* path representing input source */
+	Oid			varid;			/* oid of target schema variable */
+} SetVariablePath;
 
 /*
  * Restriction clause info.
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index d671328dfd..7cce2e4d73 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -43,7 +43,7 @@ typedef struct PlannedStmt
 {
 	NodeTag		type;
 
-	CmdType		commandType;	/* select|insert|update|delete|utility */
+	CmdType		commandType;	/* select|let|insert|update|delete|utility */
 
 	uint64		queryId;		/* query identifier (copied from Query) */
 
@@ -85,6 +85,8 @@ typedef struct PlannedStmt
 
 	Node	   *utilityStmt;	/* non-null if this is utility stmt */
 
+	List	   *schemaVariables;	/* list of 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" */
@@ -238,6 +240,16 @@ typedef struct ModifyTable
 	List	   *exclRelTlist;	/* tlist of the EXCLUDED pseudo relation */
 } ModifyTable;
 
+/* ----------------
+ * SetVariable node
+ * ----------------
+ */
+typedef struct SetVariable
+{
+	Plan		plan;
+	Oid			varid;
+} SetVariable;
+
 struct PartitionPruneInfo;		/* forward reference to struct below */
 
 /* ----------------
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index 9ae851d847..3370f5c550 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -43,7 +43,10 @@ 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
+ * persistent/temporary variable.
+ */
 typedef enum OnCommitAction
 {
 	ONCOMMIT_NOOP,				/* No ON COMMIT clause (do nothing) */
@@ -52,6 +55,13 @@ typedef enum OnCommitAction
 	ONCOMMIT_DROP				/* ON COMMIT DROP */
 } OnCommitAction;
 
+typedef enum VariableEOXAction
+{
+	VARIABLE_EOX_NOOP,			/* Do nothing */
+	VARIABLE_EOX_DROP,			/* ON TRANSACTION END DROP */
+	VARIABLE_EOX_RESET			/* ON TRANSACTION END RESET */
+}			VariableEOXAction;
+
 /*
  * RangeVar - range variable, used in FROM clauses
  *
@@ -252,13 +262,17 @@ 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 a access to schema variable
+ *				paramid holds varid.
  */
 typedef enum ParamKind
 {
 	PARAM_EXTERN,
 	PARAM_EXEC,
 	PARAM_SUBLINK,
-	PARAM_MULTIEXPR
+	PARAM_MULTIEXPR,
+	PARAM_VARIABLE
 } ParamKind;
 
 typedef struct Param
@@ -269,6 +283,7 @@ typedef struct Param
 	Oid			paramtype;		/* pg_type OID of parameter's datatype */
 	int32		paramtypmod;	/* typmod value, if known */
 	Oid			paramcollid;	/* OID of collation, or InvalidOid if none */
+	Oid			paramvarid;		/* OID of schema variable if it is used */
 	int			location;		/* token location, or -1 if unknown */
 } Param;
 
diff --git a/src/include/optimizer/pathnode.h b/src/include/optimizer/pathnode.h
index 53261ee91f..1a95939338 100644
--- a/src/include/optimizer/pathnode.h
+++ b/src/include/optimizer/pathnode.h
@@ -276,6 +276,8 @@ extern ModifyTablePath *create_modifytable_path(PlannerInfo *root,
 												List *withCheckOptionLists, List *returningLists,
 												List *rowMarks, OnConflictExpr *onconflict,
 												int epqParam);
+extern SetVariablePath *create_setvariable_path(PlannerInfo *root, RelOptInfo *rel,
+														Path *subpath, Oid varid);
 extern LimitPath *create_limit_path(PlannerInfo *root, RelOptInfo *rel,
 									Path *subpath,
 									Node *limitOffset, Node *limitCount,
diff --git a/src/include/optimizer/planmain.h b/src/include/optimizer/planmain.h
index bf1adfc52a..4cbcbeba10 100644
--- a/src/include/optimizer/planmain.h
+++ b/src/include/optimizer/planmain.h
@@ -116,4 +116,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 *root);
 
+extern void pull_up_has_schema_variables(PlannerInfo *root);
+
 #endif							/* PLANMAIN_H */
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index f836acf876..82f3bd2eb0 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -237,6 +237,7 @@ PG_KEYWORD("leading", LEADING, RESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("leakproof", LEAKPROOF, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("least", LEAST, COL_NAME_KEYWORD, BARE_LABEL)
 PG_KEYWORD("left", LEFT, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL)
+PG_KEYWORD("let", LET, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("level", LEVEL, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("like", LIKE, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL)
 PG_KEYWORD("limit", LIMIT, RESERVED_KEYWORD, AS_LABEL)
@@ -450,6 +451,8 @@ PG_KEYWORD("validator", VALIDATOR, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("value", VALUE_P, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("values", VALUES, COL_NAME_KEYWORD, BARE_LABEL)
 PG_KEYWORD("varchar", VARCHAR, COL_NAME_KEYWORD, BARE_LABEL)
+PG_KEYWORD("variable", VARIABLE, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("variables", VARIABLES, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("variadic", VARIADIC, RESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("varying", VARYING, UNRESERVED_KEYWORD, AS_LABEL)
 PG_KEYWORD("verbose", VERBOSE, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL)
diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h
index 1500de2dd0..2b7676eb08 100644
--- a/src/include/parser/parse_node.h
+++ b/src/include/parser/parse_node.h
@@ -80,6 +80,8 @@ typedef enum ParseExprKind
 	EXPR_KIND_COPY_WHERE,		/* WHERE condition in COPY FROM */
 	EXPR_KIND_GENERATED_COLUMN, /* generation expression for a column */
 	EXPR_KIND_CYCLE_MARK,		/* cycle mark value */
+	EXPR_KIND_VARIABLE_DEFAULT, /* default value for schema variable */
+	EXPR_KIND_LET_TARGET		/* LET assignment (should be same like UPDATE) */
 } ParseExprKind;

@@ -210,6 +212,7 @@ struct ParseState
 	bool		p_hasTargetSRFs;
 	bool		p_hasSubLinks;
 	bool		p_hasModifyingCTE;
+	bool		p_hasSchemaVariables;
 
 	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 9ba24d4ca9..8a81e62849 100644
--- a/src/include/tcop/cmdtaglist.h
+++ b/src/include/tcop/cmdtaglist.h
@@ -68,6 +68,7 @@ PG_CMDTAG(CMDTAG_ALTER_TRANSFORM, "ALTER TRANSFORM", true, false, false)
 PG_CMDTAG(CMDTAG_ALTER_TRIGGER, "ALTER TRIGGER", true, false, false)
 PG_CMDTAG(CMDTAG_ALTER_TYPE, "ALTER TYPE", true, true, false)
 PG_CMDTAG(CMDTAG_ALTER_USER_MAPPING, "ALTER USER MAPPING", true, false, false)
+PG_CMDTAG(CMDTAG_ALTER_VARIABLE, "ALTER VARIABLE", true, false, false)
 PG_CMDTAG(CMDTAG_ALTER_VIEW, "ALTER VIEW", true, false, false)
 PG_CMDTAG(CMDTAG_ANALYZE, "ANALYZE", false, false, false)
 PG_CMDTAG(CMDTAG_BEGIN, "BEGIN", false, false, false)
@@ -123,6 +124,7 @@ PG_CMDTAG(CMDTAG_CREATE_TRANSFORM, "CREATE TRANSFORM", true, false, false)
 PG_CMDTAG(CMDTAG_CREATE_TRIGGER, "CREATE TRIGGER", true, false, false)
 PG_CMDTAG(CMDTAG_CREATE_TYPE, "CREATE TYPE", true, false, false)
 PG_CMDTAG(CMDTAG_CREATE_USER_MAPPING, "CREATE USER MAPPING", true, false, false)
+PG_CMDTAG(CMDTAG_CREATE_VARIABLE, "CREATE VARIABLE", true, false, false)
 PG_CMDTAG(CMDTAG_CREATE_VIEW, "CREATE VIEW", true, false, false)
 PG_CMDTAG(CMDTAG_DEALLOCATE, "DEALLOCATE", false, false, false)
 PG_CMDTAG(CMDTAG_DEALLOCATE_ALL, "DEALLOCATE ALL", false, false, false)
@@ -133,6 +135,7 @@ PG_CMDTAG(CMDTAG_DISCARD_ALL, "DISCARD ALL", false, false, false)
 PG_CMDTAG(CMDTAG_DISCARD_PLANS, "DISCARD PLANS", false, false, false)
 PG_CMDTAG(CMDTAG_DISCARD_SEQUENCES, "DISCARD SEQUENCES", false, false, false)
 PG_CMDTAG(CMDTAG_DISCARD_TEMP, "DISCARD TEMP", false, false, false)
+PG_CMDTAG(CMDTAG_DISCARD_VARIABLES, "DISCARD VARIABLES", false, false, false)
 PG_CMDTAG(CMDTAG_DO, "DO", false, false, false)
 PG_CMDTAG(CMDTAG_DROP_ACCESS_METHOD, "DROP ACCESS METHOD", true, false, false)
 PG_CMDTAG(CMDTAG_DROP_AGGREGATE, "DROP AGGREGATE", true, false, false)
@@ -175,6 +178,7 @@ PG_CMDTAG(CMDTAG_DROP_TRANSFORM, "DROP TRANSFORM", true, false, false)
 PG_CMDTAG(CMDTAG_DROP_TRIGGER, "DROP TRIGGER", true, false, false)
 PG_CMDTAG(CMDTAG_DROP_TYPE, "DROP TYPE", true, false, false)
 PG_CMDTAG(CMDTAG_DROP_USER_MAPPING, "DROP USER MAPPING", true, false, false)
+PG_CMDTAG(CMDTAG_DROP_VARIABLE, "DROP VARIABLE", true, false, false)
 PG_CMDTAG(CMDTAG_DROP_VIEW, "DROP VIEW", true, false, false)
 PG_CMDTAG(CMDTAG_EXECUTE, "EXECUTE", false, false, false)
 PG_CMDTAG(CMDTAG_EXPLAIN, "EXPLAIN", false, false, false)
@@ -183,6 +187,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 88a7e59de5..e2f348eb33 100644
--- a/src/include/tcop/dest.h
+++ b/src/include/tcop/dest.h
@@ -97,7 +97,8 @@ typedef enum
 	DestCopyOut,				/* results sent to COPY TO code */
 	DestSQLFunction,			/* results sent to SQL-language func mgr */
 	DestTransientRel,			/* results sent to transient relation */
-	DestTupleQueue				/* results sent to tuple queue */
+	DestTupleQueue,				/* results sent to tuple queue */
+	DestVariable				/* results sents to schema variable */
 } CommandDest;
 
 /* ----------------
diff --git a/src/include/utils/acl.h b/src/include/utils/acl.h
index af771c901d..84f670d7f4 100644
--- a/src/include/utils/acl.h
+++ b/src/include/utils/acl.h
@@ -146,9 +146,11 @@ typedef struct ArrayType Acl;
 #define ACL_CREATE_CHR			'C'
 #define ACL_CREATE_TEMP_CHR		'T'
 #define ACL_CONNECT_CHR			'c'
+#define ACL_READ_CHR			'S' /* 'R' is occupated by old RULE priv */
+#define ACL_WRITE_CHR			'W'
 
 /* string holding all privilege code chars, in order by bitmask position */
-#define ACL_ALL_RIGHTS_STR	"arwdDxtXUCTc"
+#define ACL_ALL_RIGHTS_STR	"arwdDxtXUCTcSW"
 
 /*
  * Bitmasks defining "all rights" for each supported object type
@@ -165,6 +167,7 @@ typedef struct ArrayType Acl;
 #define ACL_ALL_RIGHTS_SCHEMA		(ACL_USAGE|ACL_CREATE)
 #define ACL_ALL_RIGHTS_TABLESPACE	(ACL_CREATE)
 #define ACL_ALL_RIGHTS_TYPE			(ACL_USAGE)
+#define ACL_ALL_RIGHTS_VARIABLE		(ACL_READ|ACL_WRITE)
 
 /* operation codes for pg_*_aclmask */
 typedef enum
@@ -259,7 +262,8 @@ extern AclMode pg_foreign_server_aclmask(Oid srv_oid, Oid roleid,
 										 AclMode mask, AclMaskHow how);
 extern AclMode pg_type_aclmask(Oid type_oid, Oid roleid,
 							   AclMode mask, AclMaskHow how);
-
+extern AclMode pg_variable_aclmask(Oid var_oid, Oid roleid,
+								   AclMode mask, AclMaskHow how);
 extern AclResult pg_attribute_aclcheck(Oid table_oid, AttrNumber attnum,
 									   Oid roleid, AclMode mode);
 extern AclResult pg_attribute_aclcheck_ext(Oid table_oid, AttrNumber attnum,
@@ -280,6 +284,7 @@ extern AclResult pg_tablespace_aclcheck(Oid spc_oid, Oid roleid, AclMode mode);
 extern AclResult pg_foreign_data_wrapper_aclcheck(Oid fdw_oid, Oid roleid, AclMode mode);
 extern AclResult pg_foreign_server_aclcheck(Oid srv_oid, Oid roleid, AclMode mode);
 extern AclResult pg_type_aclcheck(Oid type_oid, Oid roleid, AclMode mode);
+extern AclResult pg_variable_aclcheck(Oid type_oid, Oid roleid, AclMode mode);
 
 extern void aclcheck_error(AclResult aclerr, ObjectType objtype,
 						   const char *objectname);
@@ -316,6 +321,7 @@ extern bool pg_extension_ownercheck(Oid ext_oid, Oid roleid);
 extern bool pg_publication_ownercheck(Oid pub_oid, Oid roleid);
 extern bool pg_subscription_ownercheck(Oid sub_oid, Oid roleid);
 extern bool pg_statistics_object_ownercheck(Oid stat_oid, Oid roleid);
+extern bool pg_variable_ownercheck(Oid stat_oid, Oid roleid);
 extern bool has_createrole_privilege(Oid roleid);
 extern bool has_bypassrls_privilege(Oid roleid);
 
diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h
index 77871aaefc..af645a6818 100644
--- a/src/include/utils/lsyscache.h
+++ b/src/include/utils/lsyscache.h
@@ -132,6 +132,7 @@ extern char get_func_prokind(Oid funcid);
 extern bool get_func_leakproof(Oid funcid);
 extern RegProcedure get_func_support(Oid funcid);
 extern Oid	get_relname_relid(const char *relname, Oid relnamespace);
+extern Oid	get_varname_varid(const char *varname, Oid varnamespace);
 extern char *get_rel_name(Oid relid);
 extern Oid	get_rel_namespace(Oid relid);
 extern Oid	get_rel_type_id(Oid relid);
diff --git a/src/include/utils/syscache.h b/src/include/utils/syscache.h
index d74a348600..41544f0d41 100644
--- a/src/include/utils/syscache.h
+++ b/src/include/utils/syscache.h
@@ -109,9 +109,11 @@ enum SysCacheIdentifier
 	TYPENAMENSP,
 	TYPEOID,
 	USERMAPPINGOID,
-	USERMAPPINGUSERSERVER
+	USERMAPPINGUSERSERVER,
+	VARIABLENAMENSP,
+	VARIABLEOID
 
-#define SysCacheSize (USERMAPPINGUSERSERVER + 1)
+#define SysCacheSize (VARIABLEOID + 1)
 };
 
 extern void InitCatalogCache(void);
diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
index b4c70aaa7f..e2ecfbf8d8 100644
--- a/src/pl/plpgsql/src/pl_exec.c
+++ b/src/pl/plpgsql/src/pl_exec.c
@@ -4254,6 +4254,7 @@ exec_stmt_execsql(PLpgSQL_execstate *estate,
 	switch (rc)
 	{
 		case SPI_OK_SELECT:
+		case SPI_OK_LET:
 			Assert(!stmt->mod_stmt);
 			exec_set_found(estate, (SPI_processed != 0));
 			break;
diff --git a/src/pl/tcl/pltcl.c b/src/pl/tcl/pltcl.c
index e11837559d..24a5e6e598 100644
--- a/src/pl/tcl/pltcl.c
+++ b/src/pl/tcl/pltcl.c
@@ -2436,6 +2436,7 @@ pltcl_process_SPI_result(Tcl_Interp *interp,
 		case SPI_OK_INSERT:
 		case SPI_OK_DELETE:
 		case SPI_OK_UPDATE:
+		case SPI_OK_LET:
 			Tcl_SetObjResult(interp, Tcl_NewWideIntObj(ntuples));
 			break;
 
diff --git a/src/test/regress/expected/misc_sanity.out b/src/test/regress/expected/misc_sanity.out
index 9ebe28a78d..dc8a14ffc6 100644
--- a/src/test/regress/expected/misc_sanity.out
+++ b/src/test/regress/expected/misc_sanity.out
@@ -109,7 +109,9 @@ ORDER BY 1, 2;
  pg_index                | indpred       | pg_node_tree
  pg_largeobject          | data          | bytea
  pg_largeobject_metadata | lomacl        | aclitem[]
-(11 rows)
+ pg_variable             | varacl        | aclitem[]
+ pg_variable             | vardefexpr    | pg_node_tree
+(13 rows)
 
 -- system catalogs without primary keys
 --
diff --git a/src/test/regress/expected/sanity_check.out b/src/test/regress/expected/sanity_check.out
index d9ce961be2..947851f3eb 100644
--- a/src/test/regress/expected/sanity_check.out
+++ b/src/test/regress/expected/sanity_check.out
@@ -163,6 +163,7 @@ pg_ts_parser|t
 pg_ts_template|t
 pg_type|t
 pg_user_mapping|t
+pg_variable|t
 point_tbl|t
 polygon_tbl|t
 quad_box_tbl|t
diff --git a/src/test/regress/expected/schema_variables.out b/src/test/regress/expected/schema_variables.out
new file mode 100644
index 0000000000..c81f6014da
--- /dev/null
+++ b/src/test/regress/expected/schema_variables.out
@@ -0,0 +1,634 @@
+CREATE SCHEMA svartest;
+SET search_path = svartest;
+CREATE VARIABLE var1 AS integer;
+CREATE TEMP VARIABLE var2 AS text;
+DROP VARIABLE var1, var2;
+-- functional interface
+CREATE VARIABLE var1 AS numeric;
+CREATE ROLE var_test_role;
+GRANT USAGE ON SCHEMA svartest TO var_test_role;
+SET ROLE TO var_test_role;
+-- should fail
+SELECT var1;
+ERROR:  permission denied for schema variable var1
+SET ROLE TO DEFAULT;
+GRANT READ ON VARIABLE var1 TO var_test_role;
+SET ROLE TO var_test_role;
+-- should fail
+LET var1 = 10;
+ERROR:  permission denied for schema variable var1
+-- should work
+SELECT var1;
+ var1 
+------
+     
+(1 row)
+
+SET ROLE TO DEFAULT;
+GRANT WRITE ON VARIABLE var1 TO var_test_role;
+SET ROLE TO var_test_role;
+-- should work
+LET var1 = 333;
+SET ROLE TO DEFAULT;
+REVOKE ALL ON VARIABLE var1 FROM var_test_role;
+CREATE OR REPLACE FUNCTION secure_var()
+RETURNS int AS $$
+  SELECT svartest.var1::int;
+$$ LANGUAGE sql SECURITY DEFINER;
+SELECT secure_var();
+ secure_var 
+------------
+        333
+(1 row)
+
+SET ROLE TO var_test_role;
+-- should fail
+SELECT svartest.var1;
+ERROR:  permission denied for schema variable var1
+-- should work;
+SELECT secure_var();
+ secure_var 
+------------
+        333
+(1 row)
+
+SET ROLE TO DEFAULT;
+EXPLAIN (VERBOSE, COSTS OFF) SELECT * FROM generate_series(1,100) g(v) WHERE v = var1;
+                  QUERY PLAN                   
+-----------------------------------------------
+ Function Scan on pg_catalog.generate_series g
+   Output: v
+   Function Call: generate_series(1, 100)
+   Filter: ((g.v)::numeric = var1)
+(4 rows)
+
+CREATE VIEW schema_var_view AS SELECT var1;
+SELECT * FROM schema_var_view;
+ var1 
+------
+  333
+(1 row)
+
+\c -
+SET search_path = svartest;
+-- should work still, but var will be empty
+SELECT * FROM schema_var_view;
+ var1 
+------
+     
+(1 row)
+
+LET var1 = pi();
+SELECT var1;
+       var1       
+------------------
+ 3.14159265358979
+(1 row)
+
+-- we can see execution plan of LET statement
+EXPLAIN (VERBOSE, COSTS OFF) LET var1 = pi();
+            QUERY PLAN            
+----------------------------------
+ Set Variable
+   Output: 3.14159265358979
+   ->  Result
+         Output: 3.14159265358979
+(4 rows)
+
+-- LET can be prepared
+PREPARE var_pp(int, numeric) AS LET var1 = $1 + $2;
+EXECUTE var_pp(100, 1.23456);
+SELECT var1;
+   var1    
+-----------
+ 101.23456
+(1 row)
+
+CREATE VARIABLE var3 AS int;
+CREATE OR REPLACE FUNCTION inc(int)
+RETURNS int AS $$
+BEGIN
+  LET svartest.var3 = COALESCE(svartest.var3 + $1, $1);
+  RETURN var3;
+END;
+$$ LANGUAGE plpgsql;
+SELECT inc(1);
+ inc 
+-----
+   1
+(1 row)
+
+SELECT inc(1);
+ inc 
+-----
+   2
+(1 row)
+
+SELECT inc(1);
+ inc 
+-----
+   3
+(1 row)
+
+SELECT inc(1) FROM generate_series(1,10);
+ inc 
+-----
+   4
+   5
+   6
+   7
+   8
+   9
+  10
+  11
+  12
+  13
+(10 rows)
+
+SET ROLE TO var_test_role;
+-- should fail
+LET var3 = 0;
+ERROR:  permission denied for schema variable var3
+SET ROLE TO DEFAULT;
+DROP VIEW schema_var_view;
+DROP VARIABLE var1 CASCADE;
+DROP VARIABLE var3 CASCADE;
+-- 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;
+\d v1
+\d v2
+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.14);
+SELECT v1;
+     v1     
+------------
+ (1,2,3.14)
+(1 row)
+
+SELECT v2;
+      v2       
+---------------
+ (10,20,31.40)
+(1 row)
+
+SELECT (v1).*;
+ x | y |  z   
+---+---+------
+ 1 | 2 | 3.14
+(1 row)
+
+SELECT (v2).*;
+ x  | y  |   z   
+----+----+-------
+ 10 | 20 | 31.40
+(1 row)
+
+SELECT v1.x + v1.z;
+ ?column? 
+----------
+     4.14
+(1 row)
+
+SELECT v2.x + v2.z;
+ ?column? 
+----------
+    41.40
+(1 row)
+
+-- access to composite fields should be safe too
+-- should fail
+SET ROLE TO var_test_role;
+SELECT v2.x;
+ERROR:  permission denied for schema variable v2
+SET ROLE TO DEFAULT;
+DROP VARIABLE v1;
+DROP VARIABLE v2;
+REVOKE USAGE ON SCHEMA svartest FROM var_test_role;
+DROP ROLE var_test_role;
+-- scalar variables should not be in conflict with qualified column
+CREATE VARIABLE varx AS text;
+SELECT varx.relname FROM pg_class varx WHERE varx.relname = 'pg_class';
+ relname  
+----------
+ pg_class
+(1 row)
+
+-- should fail
+SELECT varx.xxx;
+ERROR:  type text is not composite
+-- variables can be updated under RO transaction
+BEGIN;
+SET TRANSACTION READ ONLY;
+LET varx = 'hello';
+COMMIT;
+SELECT varx;
+ varx  
+-------
+ hello
+(1 row)
+
+DROP VARIABLE varx;
+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
+LET v1.x = 10;
+ERROR:  cannot assign to field "x" of column "x" because there is no such column in data type t1
+LINE 1: LET v1.x = 10;
+            ^
+DROP VARIABLE v1;
+DROP TYPE t1;
+-- arrays are supported
+CREATE VARIABLE va1 AS numeric[];
+LET va1 = ARRAY[1.1,2.1];
+LET va1[1] = 10.1;
+SELECT va1;
+    va1     
+------------
+ {10.1,2.1}
+(1 row)
+
+CREATE TYPE ta2 AS (a numeric, b numeric[]);
+CREATE VARIABLE va2 AS ta2;
+LET va2 = (10.1, ARRAY[0.0, 0.0]);
+LET va2.a = 10.2;
+SELECT va2;
+        va2         
+--------------------
+ (10.2,"{0.0,0.0}")
+(1 row)
+
+LET va2.b[1] = 10.3;
+SELECT va2;
+         va2         
+---------------------
+ (10.2,"{10.3,0.0}")
+(1 row)
+
+DROP VARIABLE va1;
+DROP VARIABLE va2;
+DROP TYPE ta2;
+-- default values
+CREATE VARIABLE v1 AS numeric DEFAULT pi();
+LET v1 = v1 * 2;
+SELECT v1;
+        v1        
+------------------
+ 6.28318530717958
+(1 row)
+
+CREATE TYPE t2 AS (a numeric, b text);
+CREATE VARIABLE v2 AS t2 DEFAULT (NULL, 'Hello');
+LET svartest.v2.a = pi();
+SELECT v2;
+            v2            
+--------------------------
+ (3.14159265358979,Hello)
+(1 row)
+
+-- shoudl fail due dependency
+DROP TYPE t2;
+ERROR:  cannot drop type t2 because other objects depend on it
+DETAIL:  schema variable v2 depends on type t2
+HINT:  Use DROP ... CASCADE to drop the dependent objects too.
+-- should be ok
+DROP VARIABLE v1;
+DROP VARIABLE v2;
+-- tests of alters
+CREATE SCHEMA var_schema1;
+CREATE SCHEMA var_schema2;
+CREATE VARIABLE var_schema1.var1 AS integer;
+LET var_schema1.var1 = 1000;
+SELECT var_schema1.var1;
+ var1 
+------
+ 1000
+(1 row)
+
+ALTER VARIABLE var_schema1.var1 SET SCHEMA var_schema2;
+SELECT var_schema2.var1;
+ var1 
+------
+ 1000
+(1 row)
+
+CREATE ROLE var_test_role;
+ALTER VARIABLE var_schema2.var1 OWNER TO var_test_role;
+SET ROLE TO var_test_role;
+-- should fail, no access to schema var_schema2.var
+SELECT var_schema2.var1;
+ERROR:  permission denied for schema var_schema2
+DROP VARIABLE var_schema2.var1;
+ERROR:  permission denied for schema var_schema2
+SET ROLE TO DEFAULT;
+ALTER VARIABLE var_schema2.var1 SET SCHEMA public;
+SET ROLE TO var_test_role;
+SELECT public.var1;
+ var1 
+------
+ 1000
+(1 row)
+
+ALTER VARIABLE public.var1 RENAME TO var1_renamed;
+SELECT public.var1_renamed;
+ var1_renamed 
+--------------
+         1000
+(1 row)
+
+DROP VARIABLE public.var1_renamed;
+SET ROLE TO DEFAULt;
+DROP ROLE var_test_role;
+CREATE VARIABLE xx AS text DEFAULT 'hello';
+SELECT xx, upper(xx);
+  xx   | upper 
+-------+-------
+ hello | HELLO
+(1 row)
+
+LET xx = 'Hi';
+SELECT xx;
+ xx 
+----
+ Hi
+(1 row)
+
+DROP VARIABLE xx;
+-- ON TRANSACTION END RESET tests
+CREATE VARIABLE t1 AS int DEFAULT -1 ON TRANSACTION END RESET;
+BEGIN;
+  SELECT t1;
+ t1 
+----
+ -1
+(1 row)
+
+  LET t1 = 100;
+  SELECT t1;
+ t1  
+-----
+ 100
+(1 row)
+
+COMMIT;
+SELECT t1;
+ t1 
+----
+ -1
+(1 row)
+
+BEGIN;
+  SELECT t1;
+ t1 
+----
+ -1
+(1 row)
+
+  LET t1 = 100;
+  SELECT t1;
+ t1  
+-----
+ 100
+(1 row)
+
+ROLLBACK;
+SELECT t1;
+ t1 
+----
+ -1
+(1 row)
+
+DROP VARIABLE t1;
+CREATE VARIABLE v1 AS int DEFAULT 0;
+CREATE VARIABLE v2 AS text DEFAULT 'none';
+LET v1 = 100;
+LET v2 = 'Hello';
+SELECT v1, v2;
+ v1  |  v2   
+-----+-------
+ 100 | Hello
+(1 row)
+
+LET v1 = DEFAULT;
+LET v2 = DEFAULT;
+SELECT v1, v2;
+ v1 |  v2  
+----+------
+  0 | none
+(1 row)
+
+DROP VARIABLE v1;
+DROP VARIABLE v2;
+-- ON COMMIT DROP tests
+-- should be 0 always
+SELECT count(*) FROM pg_variable;
+ count 
+-------
+     0
+(1 row)
+
+CREATE TEMP VARIABLE g AS int ON COMMIT DROP;
+SELECT count(*) FROM pg_variable;
+ count 
+-------
+     0
+(1 row)
+
+BEGIN;
+  CREATE TEMP VARIABLE g AS int ON COMMIT DROP;
+COMMIT;
+SELECT count(*) FROM pg_variable;
+ count 
+-------
+     0
+(1 row)
+
+BEGIN;
+  CREATE TEMP VARIABLE g AS int ON COMMIT DROP;
+ROLLBACK;
+SELECT count(*) FROM pg_variable;
+ count 
+-------
+     0
+(1 row)
+
+-- test on query with workers
+CREATE TABLE svar_test(a int);
+INSERT INTO svar_test SELECT * FROM generate_series(1,1000000);
+ANALYZE svar_test;
+CREATE VARIABLE zero int;
+LET zero = 0;
+-- parallel workers should be used
+EXPLAIN (costs off) SELECT count(*) FROM svar_test WHERE a%10 = zero;
+                    QUERY PLAN                    
+--------------------------------------------------
+ Finalize Aggregate
+   ->  Gather
+         Workers Planned: 2
+         ->  Partial Aggregate
+               ->  Parallel Seq Scan on svar_test
+                     Filter: ((a % 10) = zero)
+(6 rows)
+
+-- result should be 100000
+SELECT count(*) FROM svar_test WHERE a%10 = zero;
+ count  
+--------
+ 100000
+(1 row)
+
+LET zero = (SELECT count(*) FROM svar_test);
+-- result should be 1000000
+SELECT zero;
+  zero   
+---------
+ 1000000
+(1 row)
+
+-- parallel workers should be used (!!! DOESNT WORK YET !!!)
+EXPLAIN (costs off) LET zero = (SELECT count(*) FROM svar_test);
+             QUERY PLAN              
+-------------------------------------
+ Set Variable
+   InitPlan 1 (returns $0)
+     ->  Aggregate
+           ->  Seq Scan on svar_test
+   ->  Result
+(5 rows)
+
+DROP TABLE svar_test;
+DROP VARIABLE zero;
+-- use variables in prepared statements
+CREATE VARIABLE v AS numeric;
+LET v = 3.14;
+PREPARE pv1(int) AS LET v = v + $1;
+PREPARE pv2 AS SELECT v;
+EXECUTE pv1(1000);
+EXECUTE pv2;
+    v    
+---------
+ 1003.14
+(1 row)
+
+-- use variables in views
+CREATE VIEW vv AS SELECT COALESCE(v, 0) + 1000 AS result;
+SELECT * FROM vv;
+ result  
+---------
+ 2003.14
+(1 row)
+
+-- start a new session
+\c
+SET search_path to svartest;
+SELECT * FROM vv;
+ result 
+--------
+   1000
+(1 row)
+
+LET v = 3.14;
+SELECT * FROM vv;
+ result  
+---------
+ 1003.14
+(1 row)
+
+-- should fail, dependency
+DROP VARIABLE v;
+ERROR:  cannot drop schema variable v because other objects depend on it
+DETAIL:  view vv depends on schema variable v
+HINT:  Use DROP ... CASCADE to drop the dependent objects too.
+-- should be ok
+DROP VARIABLE v CASCADE;
+NOTICE:  drop cascades to view vv
+-- other features
+CREATE VARIABLE dt AS integer DEFAULT 0;
+LET dt = 100;
+SELECT dt;
+ dt  
+-----
+ 100
+(1 row)
+
+DISCARD VARIABLES;
+SELECT dt;
+ dt 
+----
+  0
+(1 row)
+
+DROP VARIABLE dt;
+-- NOT NULL
+CREATE VARIABLE v1 AS int NOT NULL;
+CREATE VARIABLE v2 AS int NOT NULL DEFAULT NULL;
+-- should fail
+SELECT v1;
+ERROR:  null value is not allowed for NOT NULL schema variable "v1"
+DETAIL:  The schema variable was not initialized yet.
+SELECT v2;
+ERROR:  null value is not allowed for NOT NULL schema variable "v2"
+LET v1 = NULL;
+ERROR:  null value is not allowed for NOT NULL schema variable "v1"
+LET v2 = NULL;
+ERROR:  null value is not allowed for NOT NULL schema variable "v2"
+LET v1 = DEFAULT;
+ERROR:  null value is not allowed for NOT NULL schema variable "v1"
+LET v2 = DEFAULT;
+ERROR:  null value is not allowed for NOT NULL schema variable "v2"
+-- should be ok
+LET v1 = 100;
+LET v2 = 1000;
+SELECT v1, v2;
+ v1  |  v2  
+-----+------
+ 100 | 1000
+(1 row)
+
+DROP VARIABLE v1;
+DROP VARIABLE v2;
+CREATE VARIABLE tv AS int;
+CREATE VARIABLE IF NOT EXISTS tv AS int;
+NOTICE:  schema variable "tv" already exists, skipping
+DROP VARIABLE tv;
+CREATE IMMUTABLE VARIABLE iv AS int DEFAULT 100;
+SELECT iv;
+ iv  
+-----
+ 100
+(1 row)
+
+-- should fail;
+LET iv = 10000;
+ERROR:  schema variable "iv" is declared IMMUTABLE
+DROP VARIABLE iv;
+-- create variable inside plpgsql block
+DO $$
+BEGIN
+  CREATE VARIABLE do_test_svar AS date DEFAULT '2000-01-01';
+END;
+$$;
+SELECT do_test_svar;
+ do_test_svar 
+--------------
+ 01-01-2000
+(1 row)
+
+DROP VARIABLE do_test_svar;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index a091300857..059e698635 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -114,7 +114,7 @@ test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath
 # NB: temp.sql does a reconnect which transiently uses 2 connections,
 # so keep this parallel group to at most 19 tests
 # ----------
-test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml
+test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml schema_variables
 
 # ----------
 # Another group of parallel tests
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 5644847601..67f6e31e90 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -209,3 +209,4 @@ test: event_trigger
 test: oidjoins
 test: fast_default
 test: stats
+test: schema_variables
diff --git a/src/test/regress/sql/schema_variables.sql b/src/test/regress/sql/schema_variables.sql
new file mode 100644
index 0000000000..5f638302c7
--- /dev/null
+++ b/src/test/regress/sql/schema_variables.sql
@@ -0,0 +1,427 @@
+CREATE SCHEMA svartest;
+
+SET search_path = svartest;
+
+CREATE VARIABLE var1 AS integer;
+CREATE TEMP VARIABLE var2 AS text;
+
+DROP VARIABLE var1, var2;
+
+-- functional interface
+CREATE VARIABLE var1 AS numeric;
+
+CREATE ROLE var_test_role;
+GRANT USAGE ON SCHEMA svartest TO var_test_role;
+
+SET ROLE TO var_test_role;
+
+-- should fail
+SELECT var1;
+
+SET ROLE TO DEFAULT;
+
+GRANT READ ON VARIABLE var1 TO var_test_role;
+
+SET ROLE TO var_test_role;
+-- should fail
+LET var1 = 10;
+-- should work
+SELECT var1;
+
+SET ROLE TO DEFAULT;
+
+GRANT WRITE ON VARIABLE var1 TO var_test_role;
+
+SET ROLE TO var_test_role;
+
+-- should work
+LET var1 = 333;
+
+SET ROLE TO DEFAULT;
+
+REVOKE ALL ON VARIABLE var1 FROM var_test_role;
+
+CREATE OR REPLACE FUNCTION secure_var()
+RETURNS int AS $$
+  SELECT svartest.var1::int;
+$$ LANGUAGE sql SECURITY DEFINER;
+
+SELECT secure_var();
+
+SET ROLE TO var_test_role;
+
+-- should fail
+SELECT svartest.var1;
+
+-- should work;
+SELECT secure_var();
+
+SET ROLE TO DEFAULT;
+
+EXPLAIN (VERBOSE, COSTS OFF) SELECT * FROM generate_series(1,100) g(v) WHERE v = var1;
+
+CREATE VIEW schema_var_view AS SELECT var1;
+
+SELECT * FROM schema_var_view;
+
+\c -
+
+SET search_path = svartest;
+
+-- should work still, but var will be empty
+SELECT * FROM schema_var_view;
+
+LET var1 = pi();
+
+SELECT var1;
+
+-- we can see execution plan of LET statement
+EXPLAIN (VERBOSE, COSTS OFF) LET var1 = pi();
+
+-- LET can be prepared
+PREPARE var_pp(int, numeric) AS LET var1 = $1 + $2;
+
+EXECUTE var_pp(100, 1.23456);
+
+SELECT var1;
+
+CREATE VARIABLE var3 AS int;
+
+CREATE OR REPLACE FUNCTION inc(int)
+RETURNS int AS $$
+BEGIN
+  LET svartest.var3 = COALESCE(svartest.var3 + $1, $1);
+  RETURN var3;
+END;
+$$ LANGUAGE plpgsql;
+
+SELECT inc(1);
+SELECT inc(1);
+SELECT inc(1);
+
+SELECT inc(1) FROM generate_series(1,10);
+
+SET ROLE TO var_test_role;
+
+-- should fail
+LET var3 = 0;
+
+SET ROLE TO DEFAULT;
+
+DROP VIEW schema_var_view;
+
+DROP VARIABLE var1 CASCADE;
+DROP VARIABLE var3 CASCADE;
+
+-- 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;
+
+\d v1
+\d v2
+
+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.14);
+
+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
+-- should fail
+SET ROLE TO var_test_role;
+
+SELECT v2.x;
+
+SET ROLE TO DEFAULT;
+
+DROP VARIABLE v1;
+DROP VARIABLE v2;
+
+REVOKE USAGE ON SCHEMA svartest FROM var_test_role;
+DROP ROLE var_test_role;
+
+-- scalar variables should not be in conflict with qualified column
+CREATE VARIABLE varx AS text;
+SELECT varx.relname FROM pg_class varx WHERE varx.relname = 'pg_class';
+
+-- should fail
+SELECT varx.xxx;
+
+-- variables can be updated under RO transaction
+
+BEGIN;
+SET TRANSACTION READ ONLY;
+LET varx = 'hello';
+COMMIT;
+
+SELECT varx;
+
+DROP VARIABLE varx;
+
+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
+LET v1.x = 10;
+
+DROP VARIABLE v1;
+DROP TYPE t1;
+
+-- arrays are supported
+CREATE VARIABLE va1 AS numeric[];
+LET va1 = ARRAY[1.1,2.1];
+LET va1[1] = 10.1;
+SELECT va1;
+
+CREATE TYPE ta2 AS (a numeric, b numeric[]);
+CREATE VARIABLE va2 AS ta2;
+LET va2 = (10.1, ARRAY[0.0, 0.0]);
+LET va2.a = 10.2;
+SELECT va2;
+LET va2.b[1] = 10.3;
+SELECT va2;
+
+DROP VARIABLE va1;
+DROP VARIABLE va2;
+DROP TYPE ta2;
+
+-- default values
+CREATE VARIABLE v1 AS numeric DEFAULT pi();
+LET v1 = v1 * 2;
+SELECT v1;
+
+CREATE TYPE t2 AS (a numeric, b text);
+CREATE VARIABLE v2 AS t2 DEFAULT (NULL, 'Hello');
+LET svartest.v2.a = pi();
+SELECT v2;
+
+-- shoudl fail due dependency
+DROP TYPE t2;
+
+-- should be ok
+DROP VARIABLE v1;
+DROP VARIABLE v2;
+
+-- tests of alters
+CREATE SCHEMA var_schema1;
+CREATE SCHEMA var_schema2;
+
+CREATE VARIABLE var_schema1.var1 AS integer;
+LET var_schema1.var1 = 1000;
+SELECT var_schema1.var1;
+ALTER VARIABLE var_schema1.var1 SET SCHEMA var_schema2;
+SELECT var_schema2.var1;
+
+CREATE ROLE var_test_role;
+
+ALTER VARIABLE var_schema2.var1 OWNER TO var_test_role;
+SET ROLE TO var_test_role;
+
+-- should fail, no access to schema var_schema2.var
+SELECT var_schema2.var1;
+DROP VARIABLE var_schema2.var1;
+
+SET ROLE TO DEFAULT;
+
+ALTER VARIABLE var_schema2.var1 SET SCHEMA public;
+
+SET ROLE TO var_test_role;
+SELECT public.var1;
+
+ALTER VARIABLE public.var1 RENAME TO var1_renamed;
+
+SELECT public.var1_renamed;
+
+DROP VARIABLE public.var1_renamed;
+
+SET ROLE TO DEFAULt;
+
+DROP ROLE var_test_role;
+
+CREATE VARIABLE xx AS text DEFAULT 'hello';
+
+SELECT xx, upper(xx);
+
+LET xx = 'Hi';
+
+SELECT xx;
+
+DROP VARIABLE xx;
+
+-- ON TRANSACTION END RESET tests
+CREATE VARIABLE t1 AS int DEFAULT -1 ON TRANSACTION END RESET;
+
+BEGIN;
+  SELECT t1;
+  LET t1 = 100;
+  SELECT t1;
+COMMIT;
+
+SELECT t1;
+
+BEGIN;
+  SELECT t1;
+  LET t1 = 100;
+  SELECT t1;
+ROLLBACK;
+
+SELECT t1;
+
+DROP VARIABLE t1;
+
+CREATE VARIABLE v1 AS int DEFAULT 0;
+CREATE VARIABLE v2 AS text DEFAULT 'none';
+
+LET v1 = 100;
+LET v2 = 'Hello';
+SELECT v1, v2;
+LET v1 = DEFAULT;
+LET v2 = DEFAULT;
+SELECT v1, v2;
+
+DROP VARIABLE v1;
+DROP VARIABLE v2;
+
+-- ON COMMIT DROP tests
+-- should be 0 always
+SELECT count(*) FROM pg_variable;
+
+CREATE TEMP VARIABLE g AS int ON COMMIT DROP;
+
+SELECT count(*) FROM pg_variable;
+
+BEGIN;
+  CREATE TEMP VARIABLE g AS int ON COMMIT DROP;
+COMMIT;
+
+SELECT count(*) FROM pg_variable;
+
+BEGIN;
+  CREATE TEMP VARIABLE g AS int ON COMMIT DROP;
+ROLLBACK;
+
+SELECT count(*) FROM pg_variable;
+
+-- test on query with workers
+CREATE TABLE svar_test(a int);
+INSERT INTO svar_test SELECT * FROM generate_series(1,1000000);
+ANALYZE svar_test;
+CREATE VARIABLE zero int;
+LET zero = 0;
+
+-- parallel workers should be used
+EXPLAIN (costs off) SELECT count(*) FROM svar_test WHERE a%10 = zero;
+
+-- result should be 100000
+SELECT count(*) FROM svar_test WHERE a%10 = zero;
+
+LET zero = (SELECT count(*) FROM svar_test);
+
+-- result should be 1000000
+SELECT zero;
+
+-- parallel workers should be used (!!! DOESNT WORK YET !!!)
+EXPLAIN (costs off) LET zero = (SELECT count(*) FROM svar_test);
+
+DROP TABLE svar_test;
+DROP VARIABLE zero;
+
+-- use variables in prepared statements
+CREATE VARIABLE v AS numeric;
+LET v = 3.14;
+
+PREPARE pv1(int) AS LET v = v + $1;
+PREPARE pv2 AS SELECT v;
+
+EXECUTE pv1(1000);
+EXECUTE pv2;
+
+-- use variables in views
+CREATE VIEW vv AS SELECT COALESCE(v, 0) + 1000 AS result;
+SELECT * FROM vv;
+
+-- start a new session
+\c
+
+SET search_path to svartest;
+
+SELECT * FROM vv;
+LET v = 3.14;
+SELECT * FROM vv;
+
+-- should fail, dependency
+DROP VARIABLE v;
+
+-- should be ok
+DROP VARIABLE v CASCADE;
+
+-- other features
+CREATE VARIABLE dt AS integer DEFAULT 0;
+
+LET dt = 100;
+SELECT dt;
+
+DISCARD VARIABLES;
+
+SELECT dt;
+
+DROP VARIABLE dt;
+
+-- NOT NULL
+CREATE VARIABLE v1 AS int NOT NULL;
+CREATE VARIABLE v2 AS int NOT NULL DEFAULT NULL;
+
+-- should fail
+SELECT v1;
+SELECT v2;
+LET v1 = NULL;
+LET v2 = NULL;
+LET v1 = DEFAULT;
+LET v2 = DEFAULT;
+
+-- should be ok
+LET v1 = 100;
+LET v2 = 1000;
+SELECT v1, v2;
+
+DROP VARIABLE v1;
+DROP VARIABLE v2;
+
+CREATE VARIABLE tv AS int;
+CREATE VARIABLE IF NOT EXISTS tv AS int;
+DROP VARIABLE tv;
+
+CREATE IMMUTABLE VARIABLE iv AS int DEFAULT 100;
+SELECT iv;
+
+-- should fail;
+LET iv = 10000;
+
+DROP VARIABLE iv;
+
+-- create variable inside plpgsql block
+DO $$
+BEGIN
+  CREATE VARIABLE do_test_svar AS date DEFAULT '2000-01-01';
+END;
+$$;
+
+SELECT do_test_svar;
+
+DROP VARIABLE do_test_svar;
diff --git a/doc/src/sgml/advanced.sgml b/doc/src/sgml/advanced.sgml
index 2d4ab85d45..5fbc6f73ab 100644
--- a/doc/src/sgml/advanced.sgml
+++ b/doc/src/sgml/advanced.sgml
@@ -700,6 +700,56 @@ SELECT name, elevation
   </sect1>

+  <sect1 id="tutorial-schema-variables">
+   <title>Schema Variables</title>
+
+   <indexterm zone="tutorial-schema-variables">
+    <primary>Schema variables</primary>
+   </indexterm>
+
+   <indexterm>
+    <primary>schema variable</primary>
+    <secondary>introduction</secondary>
+   </indexterm>
+
+   <para>
+    Schema variables are database objects that can hold a value.
+    Schema variables, like relations, exist within a schema and their access is
+    controlled via <command>GRANT</command> and <command>REVOKE</command> commands.
+    The schema variable can be created by the <command>CREATE VARIABLE</command> command.
+   </para>
+
+   <para>
+    The value of a schema variable is local to the current session. Retrieving
+    a variable's value returns either a NULL or a default value, unless its value
+    is set to something else in the current session with a LET command. The content
+    of a variable is not transactional. This is the same as in regular variables
+    in PL languages.
+   </para>
+
+   <para>
+    Schema variables are retrieved by the <command>SELECT</command> SQL command.
+    Their value is set with the <command>LET</command> SQL command.
+    While schema variables share properties with tables, their value cannot be updated
+    with an <command>UPDATE</command> 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>
+  </sect1>
+
+
   <sect1 id="tutorial-conclusion">
    <title>Conclusion</title>
 
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 2656786d1e..0edd6ba91c 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -359,6 +359,11 @@
       <entry><link linkend="catalog-pg-user-mapping"><structname>pg_user_mapping</structname></link></entry>
       <entry>mappings of users to foreign servers</entry>
      </row>
+
+     <row>
+      <entry><link linkend="catalog-pg-variable"><structname>pg_variable</structname></link></entry>
+      <entry>schema variables</entry>
+     </row>
     </tbody>
    </tgroup>
   </table>
@@ -13886,4 +13891,143 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
 
  </sect1>
 
+ <sect1 id="catalog-pg-variable">
+  <title><structname>pg_variable</structname></title>
+
+  <indexterm zone="catalog-pg-variable">
+   <primary>pg_variable</primary>
+  </indexterm>
+
+  <para>
+   The table <structname>pg_variable</structname> holds metadata
+   of schema variables.
+  </para>
+
+  <table>
+   <title><structname>pg_variable</structname> Columns</title>
+
+   <tgroup cols="4">
+    <thead>
+     <row>
+      <entry>Name</entry>
+      <entry>Type</entry>
+      <entry>References</entry>
+      <entry>Description</entry>
+     </row>
+    </thead>
+    <tbody>
+     <row>
+      <entry><structfield>oid</structfield></entry>
+      <entry><type>oid</type></entry>
+      <entry></entry>
+      <entry>Row identifier</entry>
+     </row>
+
+     <row>
+      <entry><structfield>varname</structfield></entry>
+      <entry><type>name</type></entry>
+      <entry></entry>
+      <entry>Name of the schema variable</entry>
+     </row>
+
+     <row>
+      <entry><structfield>varnamespace</structfield></entry>
+      <entry><type>oid</type></entry>
+      <entry><literal><link linkend="catalog-pg-namespace"><structname>pg_namespace</structname></link>.oid</literal></entry>
+      <entry>
+       The OID of the namespace that contains this variable
+      </entry>
+     </row>
+
+     <row>
+      <entry><structfield>vartype</structfield></entry>
+      <entry><type>oid</type></entry>
+      <entry><literal><link linkend="catalog-pg-type"><structname>pg_type</structname></link>.oid</literal></entry>
+      <entry>
+       The OID of the variable's data type.
+      </entry>
+     </row>
+
+     <row>
+      <entry><structfield>vartypmod</structfield></entry>
+      <entry><type>int4</type></entry>
+      <entry></entry>
+      <entry>
+       <structfield>vartypmod</structfield> records type-specific data
+       supplied at variable creation time (for example, the maximum
+       length of a <type>varchar</type> column).  It is passed to
+       type-specific input 
+       functions and length coercion functions.
+       The value will generally be -1 for types that do not need <structfield>vartypmod</structfield>.
+      </entry>
+     </row>
+
+     <row>
+      <entry><structfield>varowner</structfield></entry>
+      <entry><type>oid</type></entry>
+      <entry><literal><link linkend="catalog-pg-authid"><structname>pg_authid</structname></link>.oid</literal></entry>
+      <entry>Owner of the variable</entry>
+     </row>
+
+     <row>
+      <entry><structfield>varcollation</structfield></entry>
+      <entry><type>oid</type></entry>
+      <entry><literal><link linkend="catalog-pg-collation"><structname>pg_collation</structname></link>.oid</literal></entry>
+      <entry>
+       The defined collation of the variable, or zero if the variable is
+       not of a collatable data type.
+      </entry>
+     </row>
+
+     <row>
+      <entry><structfield>varisnotnull</structfield></entry>
+      <entry><type>boolean</type></entry>
+      <entry></entry>
+      <entry>
+       True if the schema variable doesn't allow null value. The default value is false.
+      </entry>
+     </row>
+
+     <row>
+      <entry><structfield>varisimmutable</structfield></entry>
+      <entry><type>boolean</type></entry>
+      <entry></entry>
+      <entry>
+       True if the variable is immutable (cannot be modified). The default value is false.
+      </entry>
+     </row>
+
+     <row>
+      <entry><structfield>vareoxaction</structfield></entry>
+      <entry><type>char</type></entry>
+      <entry></entry>
+      <entry>
+       <literal>n</literal> = no action, <literal>d</literal> = drop the variable,
+       <literal>r</literal> = reset the variable to its default value.
+      </entry>
+     </row>
+
+     <row>
+      <entry><structfield>vardefexpr</structfield></entry>
+      <entry><type>pg_node_tree</type></entry>
+      <entry></entry>
+      <entry>The internal representation of the variable default value.</entry>
+     </row>
+
+     <row>
+      <entry><structfield>varacl</structfield></entry>
+      <entry><type>aclitem[]</type></entry>
+      <entry></entry>
+      <entry>
+       Access privileges; see
+       <xref linkend="sql-grant"/> and
+       <xref linkend="sql-revoke"/>
+       for details
+      </entry>
+     </row>
+    </tbody>
+   </tgroup>
+  </table>
+ </sect1>
+
 </chapter>
diff --git a/doc/src/sgml/plpgsql.sgml b/doc/src/sgml/plpgsql.sgml
index 52f60c827c..ac72f3c71d 100644
--- a/doc/src/sgml/plpgsql.sgml
+++ b/doc/src/sgml/plpgsql.sgml
@@ -5926,6 +5926,19 @@ $$ LANGUAGE plpgsql STRICT IMMUTABLE;
 </programlisting>
     </para>
    </sect3>
+
+   <sect3>
+    <title><command>Global variables and constants</command></title>
+
+    <para>
+     The <application>PL/pgSQL</application> language has no packages,
+     and therefore no package variables or package constants.
+     <productname>PostgreSQL</productname> has schema variables and
+     immutable schema variables. Schema variables can be created
+     by <command>CREATE VARIABLE</command> 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 d67270ccc3..8458cad752 100644
--- a/doc/src/sgml/ref/allfiles.sgml
+++ b/doc/src/sgml/ref/allfiles.sgml
@@ -47,6 +47,7 @@ Complete list of usable sgml source files in this directory.
 <!ENTITY alterType          SYSTEM "alter_type.sgml">
 <!ENTITY alterUser          SYSTEM "alter_user.sgml">
 <!ENTITY alterUserMapping   SYSTEM "alter_user_mapping.sgml">
+<!ENTITY alterVariable      SYSTEM "alter_variable.sgml">
 <!ENTITY alterView          SYSTEM "alter_view.sgml">
 <!ENTITY analyze            SYSTEM "analyze.sgml">
 <!ENTITY begin              SYSTEM "begin.sgml">
@@ -99,6 +100,7 @@ Complete list of usable sgml source files in this directory.
 <!ENTITY createType         SYSTEM "create_type.sgml">
 <!ENTITY createUser         SYSTEM "create_user.sgml">
 <!ENTITY createUserMapping  SYSTEM "create_user_mapping.sgml">
+<!ENTITY createVariable     SYSTEM "create_variable.sgml">
 <!ENTITY createView         SYSTEM "create_view.sgml">
 <!ENTITY deallocate         SYSTEM "deallocate.sgml">
 <!ENTITY declare            SYSTEM "declare.sgml">
@@ -148,6 +150,7 @@ Complete list of usable sgml source files in this directory.
 <!ENTITY dropUser           SYSTEM "drop_user.sgml">
 <!ENTITY dropUserMapping    SYSTEM "drop_user_mapping.sgml">
 <!ENTITY dropView           SYSTEM "drop_view.sgml">
+<!ENTITY dropVariable       SYSTEM "drop_variable.sgml">
 <!ENTITY end                SYSTEM "end.sgml">
 <!ENTITY execute            SYSTEM "execute.sgml">
 <!ENTITY explain            SYSTEM "explain.sgml">
@@ -155,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_default_privileges.sgml b/doc/src/sgml/ref/alter_default_privileges.sgml
index f1d54f5aa3..95e8352167 100644
--- a/doc/src/sgml/ref/alter_default_privileges.sgml
+++ b/doc/src/sgml/ref/alter_default_privileges.sgml
@@ -50,6 +50,10 @@ GRANT { USAGE | CREATE | ALL [ PRIVILEGES ] }
     ON SCHEMAS
     TO { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...] [ WITH GRANT OPTION ]
 
+GRANT { READ | WRITE | ALL [ PRIVILEGES ] }
+    ON VARIABLES
+    TO { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...] [ WITH GRANT OPTION ]
+
 REVOKE [ GRANT OPTION FOR ]
     { { SELECT | INSERT | UPDATE | DELETE | TRUNCATE | REFERENCES | TRIGGER }
     [, ...] | ALL [ PRIVILEGES ] }
@@ -81,6 +85,12 @@ REVOKE [ GRANT OPTION FOR ]
     ON SCHEMAS
     FROM { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...]
     [ CASCADE | RESTRICT ]
+
+REVOKE [ GRANT OPTION FOR ]
+    { { READ | WRITE } [, ...] | ALL [ PRIVILEGES ] }
+    ON VARIABLES
+    FROM { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...]
+    [ CASCADE | RESTRICT ]
 </synopsis>
  </refsynopsisdiv>
 
@@ -92,8 +102,8 @@ REVOKE [ GRANT OPTION FOR ]
    that will be applied to objects created in the future.  (It does not
    affect privileges assigned to already-existing objects.)  Currently,
    only the privileges for schemas, tables (including views and foreign
-   tables), sequences, functions, and types (including domains) can be
-   altered.  For this command, functions include aggregates and procedures.
+   tables), sequences, functions, types (including domains) and schema variables
+   can be altered.  For this command, functions include aggregates and procedures.
    The words <literal>FUNCTIONS</literal> and <literal>ROUTINES</literal> are
    equivalent in this command.  (<literal>ROUTINES</literal> is preferred
    going forward as the standard term for functions and procedures taken
diff --git a/doc/src/sgml/ref/alter_variable.sgml b/doc/src/sgml/ref/alter_variable.sgml
new file mode 100644
index 0000000000..85a38fb9d5
--- /dev/null
+++ b/doc/src/sgml/ref/alter_variable.sgml
@@ -0,0 +1,179 @@
+<!--
+doc/src/sgml/ref/alter_variable.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-altervariable">
+ <indexterm zone="sql-altervariable">
+  <primary>ALTER VARIABLE</primary>
+ </indexterm>
+
+ <indexterm>
+  <primary>schema variable</primary>
+  <secondary>altering</secondary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>ALTER VARIABLE</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>ALTER VARIABLE</refname>
+  <refpurpose>
+   change the definition of a variable
+  </refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+ALTER VARIABLE <replaceable class="parameter">name</replaceable> OWNER TO { <replaceable class="parameter">new_owner</replaceable> | CURRENT_USER | SESSION_USER }
+ALTER VARIABLE <replaceable class="parameter">name</replaceable> RENAME TO <replaceable class="parameter">new_name</replaceable>
+ALTER VARIABLE <replaceable class="parameter">name</replaceable> SET SCHEMA <replaceable class="parameter">new_schema</replaceable>
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   The <command>ALTER VARIABLE</command> command changes the definition of an existing
+   variable. There are several subforms:
+
+  <variablelist>
+   <varlistentry>
+    <term><literal>OWNER</literal></term>
+    <listitem>
+     <para>
+      This form changes the owner of the variable.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>RENAME</literal></term>
+    <listitem>
+     <para>
+      This form changes the name of the variable.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>SET SCHEMA</literal></term>
+    <listitem>
+     <para>
+      This form moves the variable into another schema.
+     </para>
+    </listitem>
+   </varlistentry>
+
+  </variablelist>
+  </para>
+
+  <para>
+   Only the owner or a superuser is allowed to alter a schema variable.
+   In order to move a schema variable from one schema to another, the user must
+   also have the <literal>CREATE</literal> privilege on the new schema (or be
+   a superuser).
+
+   In order to move the schema variable ownership from one role to another,
+   the user must also be a direct or indirect member of the new
+   owning role, and that role must have the <literal>CREATE</literal> privilege
+   on the variable's schema (or be a superuser). These restrictions enforce that
+   altering the owner doesn't do anything you couldn't do by dropping and
+   recreating the variable.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+   <para>
+    <variablelist>
+     <varlistentry>
+      <term><replaceable class="parameter">name</replaceable></term>
+      <listitem>
+       <para>
+        The name (possibly schema-qualified) of the existing variable to
+        alter.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><replaceable class="parameter">new_name</replaceable></term>
+      <listitem>
+       <para>
+        The new name for the variable.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><replaceable class="parameter">new_owner</replaceable></term>
+      <listitem>
+       <para>
+        The user name of the new owner of the variable.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><replaceable class="parameter">new_schema</replaceable></term>
+      <listitem>
+       <para>
+        The new schema for the variable.
+       </para>
+      </listitem>
+     </varlistentry>
+    </variablelist>
+   </para>
+  </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   To rename a variable:
+<programlisting>
+ALTER VARIABLE foo RENAME TO boo;
+</programlisting>
+  </para>
+
+  <para>
+   To change the owner of the variable <literal>boo</literal>
+   to <literal>joe</literal>:
+<programlisting>
+ALTER VARIABLE boo OWNER TO joe;
+</programlisting>
+  </para>
+
+  <para>
+   To change the schema of the variable <literal>boo</literal>
+   to <literal>private</literal>:
+<programlisting>
+ALTER VARIABLE boo SET SCHEMA private;
+</programlisting>
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   Schema variables and this command in particular are a PostgreSQL extension.
+  </para>
+ </refsect1>
+
+ <refsect1 id="sql-altervariable-see-also">
+  <title>See Also</title>
+
+  <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
new file mode 100644
index 0000000000..859c0bc2f1
--- /dev/null
+++ b/doc/src/sgml/ref/create_variable.sgml
@@ -0,0 +1,190 @@
+<!--
+doc/src/sgml/ref/create_variable.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-createvariable">
+ <indexterm zone="sql-createvariable">
+  <primary>CREATE VARIABLE</primary>
+ </indexterm>
+
+ <indexterm>
+  <primary>schema variable</primary>
+  <secondary>defining</secondary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>CREATE VARIABLE</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>CREATE VARIABLE</refname>
+  <refpurpose>define a schema variable</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+CREATE [ { TEMPORARY | TEMP } ] [ IMMUTABLE ] VARIABLE [ IF NOT EXISTS ] <replaceable class="parameter">name</replaceable> [ AS ] <replaceable class="parameter">data_type</replaceable> ] [ COLLATE <replaceable class="parameter">collation</replaceable> ]
+    [ NOT NULL ] [ DEFAULT <replaceable class="parameter">default_expr</replaceable> ] [ { ON COMMIT DROP | ON TRANSACTION END RESET } ]
+</synopsis>
+ </refsynopsisdiv>
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   The <command>CREATE VARIABLE</command> command creates a schema variable.
+   Schema variables, like relations, exist within a schema and their access is
+   controlled via <command>GRANT</command> and <command>REVOKE</command> commands.
+   Changing a schema variable is non-transactional.
+  </para>
+
+  <para>
+   The value of a schema variable is local to the current session. Retrieving
+   a variable's value returns either a NULL or a default value, unless its value
+   is set to something else in the current session with a LET command. The content
+   of a variable is not transactional. This is the same as in regular variables in PL languages.
+  </para>
+
+  <para>
+   Schema variables are retrieved by the <command>SELECT</command> SQL command.
+   Their value is set with the <command>LET</command> SQL command.
+   While schema variables share properties with tables, their value cannot be updated
+   with an <command>UPDATE</command> command.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+  <variablelist>
+   <varlistentry>
+    <term><literal>IMMUTABLE</literal></term>
+    <listitem>
+     <para>
+      The value of the variable cannot be changed.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>IF NOT EXISTS</literal></term>
+    <listitem>
+     <para>
+      Do not throw an error if the name already exists. A notice is issued in this case.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">name</replaceable></term>
+    <listitem>
+     <para>
+      The name, optionally schema-qualified, of the variable.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">data_type</replaceable></term>
+    <listitem>
+     <para>
+      The name, optionally schema-qualified, of the data type of the variable.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>COLLATE <replaceable>collation</replaceable></literal></term>
+    <listitem>
+     <para>
+      The <literal>COLLATE</literal> clause assigns a collation to
+      the variable (which must be of a collatable data type).
+      If not specified, the variable data type's default collation is used.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>NOT NULL</literal></term>
+    <listitem>
+     <para>
+      The <literal>NOT NULL</literal> clause forbids to set the variable to
+      a null value. A variable created as NOT NULL and without an explicitly
+      declared default value cannot be read until it is initialized by a LET
+      command. This obliges the user to explicitly initialize the variable
+      content before reading it.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>DEFAULT <replaceable>default_expr</replaceable></literal></term>
+    <listitem>
+     <para>
+      The <literal>DEFAULT</literal> clause can be used to assign a default value to
+      a schema variable.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>ON COMMIT DROP</literal>, <literal>ON TRANSACTION END RESET</literal></term>
+    <listitem>
+     <para>
+      The <literal>ON COMMIT DROP</literal> clause specifies the behaviour
+      of a temporary schema variable at transaction commit. With this clause the
+      variable is dropped at commit time. The clause is only allowed
+      for temporary variables. The <literal>ON TRANSACTION END RESET</literal>
+      clause enforces the variable to be reset to its default value when
+      the transaction is committed or rolled back.
+     </para>
+    </listitem>
+   </varlistentry>
+
+  </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Notes</title>
+
+  <para>
+   Use the <command>DROP VARIABLE</command> command to remove a variable.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   Create an integer variable <literal>var1</literal>:
+<programlisting>
+CREATE VARIABLE var1 AS date;
+LET var1 = current_date;
+SELECT var1;
+</programlisting>
+  </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   The <command>CREATE VARIABLE</command> command is a PostgreSQL extension.
+   <!-- The choice of wording here seems to be left to personal preference... -->
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-altervariable"/></member>
+   <member><xref linkend="sql-dropvariable"/></member>
+   <member><xref linkend="sql-let"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/ref/discard.sgml b/doc/src/sgml/ref/discard.sgml
index bf44c523ca..698b2c07fd 100644
--- a/doc/src/sgml/ref/discard.sgml
+++ b/doc/src/sgml/ref/discard.sgml
@@ -21,7 +21,7 @@ PostgreSQL documentation
 
  <refsynopsisdiv>
 <synopsis>
-DISCARD { ALL | PLANS | SEQUENCES | TEMPORARY | TEMP }
+DISCARD { ALL | PLANS | SEQUENCES | TEMPORARY | TEMP | VARIABLES }
 </synopsis>
  </refsynopsisdiv>
 
@@ -75,6 +75,17 @@ DISCARD { ALL | PLANS | SEQUENCES | TEMPORARY | TEMP }
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+    <term><literal>VARIABLES</literal></term>
+    <listitem>
+     <para>
+      Resets the value of all schema variables. If a variable
+      is later reused, it is re-initialized to either
+      NULL or its default value.
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry>
     <term><literal>ALL</literal></term>
     <listitem>
@@ -93,6 +104,7 @@ SELECT pg_advisory_unlock_all();
 DISCARD PLANS;
 DISCARD TEMP;
 DISCARD SEQUENCES;
+DISCARD VARIABLES;
 </programlisting></para>
     </listitem>
    </varlistentry>
diff --git a/doc/src/sgml/ref/drop_variable.sgml b/doc/src/sgml/ref/drop_variable.sgml
new file mode 100644
index 0000000000..da3632e780
--- /dev/null
+++ b/doc/src/sgml/ref/drop_variable.sgml
@@ -0,0 +1,120 @@
+<!--
+doc/src/sgml/ref/drop_variable.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-dropvariable">
+ <indexterm zone="sql-dropvariable">
+  <primary>DROP VARIABLE</primary>
+ </indexterm>
+
+ <indexterm>
+  <primary>schema variable</primary>
+  <secondary>removing</secondary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>DROP VARIABLE</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>DROP VARIABLE</refname>
+  <refpurpose>remove a schema variable</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+DROP VARIABLE [ IF EXISTS ] <replaceable class="parameter">name</replaceable> [, ...] [ CASCADE | RESTRICT ]
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>DROP VARIABLE</command> removes a schema variable.
+   A variable can only be dropped by its owner or a superuser.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+  <variablelist>
+   <varlistentry>
+    <term><literal>IF EXISTS</literal></term>
+    <listitem>
+     <para>
+      Do not throw an error if the variable does not exist. A notice is issued
+      in this case.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">name</replaceable></term>
+    <listitem>
+     <para>
+      The name, optionally schema-qualified, of a schema variable.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>CASCADE</literal></term>
+    <listitem>
+     <para>
+      Automatically drop objects that depend on the variable (such as
+      views),
+      and in turn all objects that depend on those objects
+      (see <xref linkend="ddl-depend"/>).
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>RESTRICT</literal></term>
+    <listitem>
+     <para>
+      Refuse to drop the variable if any objects depend on it.  This is
+      the default.
+     </para>
+    </listitem>
+   </varlistentry>
+  </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   To remove the schema variable <literal>var1</literal>:
+
+<programlisting>
+DROP VARIABLE var1;
+</programlisting></para>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   The <command>DROP VARIABLE</command> command is a PostgreSQL extension.
+   <!-- create variable is a "PostgreSQL feature",
+        this is a "proprietary PostgreSQL command" ... -->
+  </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-let"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/ref/grant.sgml b/doc/src/sgml/ref/grant.sgml
index a897712de2..6586e412c1 100644
--- a/doc/src/sgml/ref/grant.sgml
+++ b/doc/src/sgml/ref/grant.sgml
@@ -103,6 +103,11 @@ GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replace
   | CURRENT_ROLE
   | CURRENT_USER
   | SESSION_USER
+
+GRANT { READ | WRITE | ALL [ PRIVILEGES ] }
+    ON VARIABLE <replaceable>variable_name</replaceable> [, ...]
+    TO <replaceable class="parameter">role_specification</replaceable> [, ...] [ WITH GRANT OPTION ]
+
 </synopsis>
  </refsynopsisdiv>
 
@@ -201,6 +206,24 @@ GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replace
      </listitem>
     </varlistentry>
 
+    <varlistentry>
+     <term><literal>READ</literal></term>
+     <listitem>
+      <para>
+       Allows to read a schema variable.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>WRITE</literal></term>
+     <listitem>
+      <para>
+       Allows to set a schema variable's content.
+      </para>
+     </listitem>
+    </varlistentry>
+
     <varlistentry>
      <term><literal>ALL PRIVILEGES</literal></term>
      <listitem>
diff --git a/doc/src/sgml/ref/let.sgml b/doc/src/sgml/ref/let.sgml
new file mode 100644
index 0000000000..d4396cad3c
--- /dev/null
+++ b/doc/src/sgml/ref/let.sgml
@@ -0,0 +1,107 @@
+<!--
+doc/src/sgml/ref/let.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-let">
+ <indexterm zone="sql-let">
+  <primary>LET</primary>
+ </indexterm>
+
+ <indexterm>
+  <primary>schema 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 schema variable's value</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+LET <replaceable class="parameter">schema_variable</replaceable> = <replaceable class="parameter">sql_expression</replaceable>
+LET <replaceable class="parameter">schema_variable</replaceable> = DEFAULT
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   The <command>LET</command> command updates the specified schema variable value.
+  </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+  <variablelist>
+   <varlistentry>
+    <term><literal>schema_variable</literal></term>
+    <listitem>
+     <para>
+      The name of the schema variable.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>sql_expression</literal></term>
+    <listitem>
+     <para>
+      An SQL expression. The result is cast into the schema variable's type.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>DEFAULT</literal></term>
+    <listitem>
+     <para>
+      Reset the schema variable to its default value, if that is defined.
+      If no explicit default value has been assigned, the schema variable
+      is set to NULL.
+     </para>
+    </listitem>
+   </varlistentry>
+  </variablelist>
+
+  <para>
+   Example:
+<programlisting>
+CREATE VARIABLE myvar AS integer;
+LET myvar = 10;
+LET myvar = (SELECT sum(val) FROM tab);
+LET myvar = DEFAULT;
+</programlisting>
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   <literal>LET</literal> extends the syntax defined in the SQL
+   standard. The <literal>SET</literal> command from the SQL standard
+   is used for different purposes in PostgreSQL.
+  </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/ref/pg_restore.sgml b/doc/src/sgml/ref/pg_restore.sgml
index 93ea937ac8..0d86239eb4 100644
--- a/doc/src/sgml/ref/pg_restore.sgml
+++ b/doc/src/sgml/ref/pg_restore.sgml
@@ -106,6 +106,17 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>-A <replaceable class="parameter">schema_variable</replaceable></option></term>
+      <term><option>--variable=<replaceable class="parameter">schema_variable</replaceable></option></term>
+      <listitem>
+       <para>
+        Restore a named schema variable only.  Multiple schema variables may be specified with
+        multiple <option>-A</option> switches.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>-c</option></term>
       <term><option>--clean</option></term>
diff --git a/doc/src/sgml/ref/revoke.sgml b/doc/src/sgml/ref/revoke.sgml
index 3014c864ea..3e9f9804be 100644
--- a/doc/src/sgml/ref/revoke.sgml
+++ b/doc/src/sgml/ref/revoke.sgml
@@ -130,6 +130,12 @@ REVOKE [ ADMIN OPTION FOR ]
   | CURRENT_ROLE
   | CURRENT_USER
   | SESSION_USER
+
+REVOKE [ GRANT OPTION FOR ]
+    { { READ | WRITE } [, ...] | ALL [ PRIVILEGES ] }
+    ON VARIABLE <replaceable>variable_name</replaceable> [, ...]
+    FROM { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...]
+    [ CASCADE | RESTRICT ]
 </synopsis>
  </refsynopsisdiv>
 
diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml
index da421ff24e..f9e42443b6 100644
--- a/doc/src/sgml/reference.sgml
+++ b/doc/src/sgml/reference.sgml
@@ -75,6 +75,7 @@
    &alterType;
    &alterUser;
    &alterUserMapping;
+   &alterVariable;
    &alterView;
    &analyze;
    &begin;
@@ -127,6 +128,7 @@
    &createType;
    &createUser;
    &createUserMapping;
+   &createVariable;
    &createView;
    &deallocate;
    &declare;
@@ -175,6 +177,7 @@
    &dropType;
    &dropUser;
    &dropUserMapping;
+   &dropVariable;
    &dropView;
    &end;
    &execute;
@@ -183,6 +186,7 @@
    &grant;
    &importForeignSchema;
    &insert;
+   &let;
    &listen;
    &load;
    &lock;
diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c
index 441445927e..a702a9760f 100644
--- a/src/backend/access/transam/xact.c
+++ b/src/backend/access/transam/xact.c
@@ -35,6 +35,7 @@
 #include "catalog/pg_enum.h"
 #include "catalog/storage.h"
 #include "commands/async.h"
+#include "commands/schema_variable.h"
 #include "commands/tablecmds.h"
 #include "commands/trigger.h"
 #include "executor/spi.h"
@@ -2145,6 +2146,9 @@ CommitTransaction(void)
 	 */
 	smgrDoPendingSyncs(true, is_parallel_worker);
 
+	/* Let ON COMMIT DROP or ON TRANSACTION END */
+	AtPreEOXact_SchemaVariable_on_commit_actions(true);
+
 	/* close large objects before lower-level cleanup */
 	AtEOXact_LargeObject(true);
 
@@ -2274,6 +2278,7 @@ CommitTransaction(void)
 	AtEOXact_SPI(true);
 	AtEOXact_Enum();
 	AtEOXact_on_commit_actions(true);
+	AtEOXact_SchemaVariable_on_commit_actions(true);
 	AtEOXact_Namespace(true, is_parallel_worker);
 	AtEOXact_SMgr();
 	AtEOXact_Files(true);
@@ -2702,6 +2707,9 @@ AbortTransaction(void)
 	AtAbort_Portals();
 	smgrDoPendingSyncs(false, is_parallel_worker);
 	AtEOXact_LargeObject(false);
+
+	/* 'false' means it's abort */
+	AtPreEOXact_SchemaVariable_on_commit_actions(false);
 	AtAbort_Notify();
 	AtEOXact_RelationMap(false, is_parallel_worker);
 	AtAbort_Twophase();
@@ -2766,6 +2774,7 @@ AbortTransaction(void)
 		AtEOXact_SPI(false);
 		AtEOXact_Enum();
 		AtEOXact_on_commit_actions(false);
+		AtEOXact_SchemaVariable_on_commit_actions(false);
 		AtEOXact_Namespace(false, is_parallel_worker);
 		AtEOXact_SMgr();
 		AtEOXact_Files(false);
diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile
index e36a9602c1..cc52bf7cfe 100644
--- a/src/backend/catalog/Makefile
+++ b/src/backend/catalog/Makefile
@@ -42,6 +42,7 @@ OBJS = \
 	pg_shdepend.o \
 	pg_subscription.o \
 	pg_type.o \
+	pg_variable.o \
 	storage.o \
 	toasting.o
 
@@ -68,7 +69,7 @@ CATALOG_HEADERS := \
 	pg_default_acl.h pg_init_privs.h pg_seclabel.h pg_shseclabel.h \
 	pg_collation.h pg_partitioned_table.h pg_range.h pg_transform.h \
 	pg_sequence.h pg_publication.h pg_publication_rel.h pg_subscription.h \
-	pg_subscription_rel.h
+	pg_subscription_rel.h pg_variable.h
 
 GENERATED_HEADERS := $(CATALOG_HEADERS:%.h=%_d.h) schemapg.h system_fk_info.h
 
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index e1573eb398..c7f5796bcc 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -58,6 +58,7 @@
 #include "catalog/pg_ts_parser.h"
 #include "catalog/pg_ts_template.h"
 #include "catalog/pg_type.h"
+#include "catalog/pg_variable.h"
 #include "commands/dbcommands.h"
 #include "commands/event_trigger.h"
 #include "commands/extension.h"
@@ -111,6 +112,7 @@ static void ExecGrant_Largeobject(InternalGrant *grantStmt);
 static void ExecGrant_Namespace(InternalGrant *grantStmt);
 static void ExecGrant_Tablespace(InternalGrant *grantStmt);
 static void ExecGrant_Type(InternalGrant *grantStmt);
+static void ExecGrant_Variable(InternalGrant *grantStmt);
 
 static void SetDefaultACLsInSchemas(InternalDefaultACL *iacls, List *nspnames);
 static void SetDefaultACL(InternalDefaultACL *iacls);
@@ -258,6 +260,9 @@ restrict_and_check_grant(bool is_grant, AclMode avail_goptions, bool all_privs,
 		case OBJECT_TYPE:
 			whole_mask = ACL_ALL_RIGHTS_TYPE;
 			break;
+		case OBJECT_VARIABLE:
+			whole_mask = ACL_ALL_RIGHTS_VARIABLE;
+			break;
 		default:
 			elog(ERROR, "unrecognized object type: %d", objtype);
 			/* not reached, but keep compiler quiet */
@@ -497,6 +502,10 @@ ExecuteGrantStmt(GrantStmt *stmt)
 			all_privileges = ACL_ALL_RIGHTS_FOREIGN_SERVER;
 			errormsg = gettext_noop("invalid privilege type %s for foreign server");
 			break;
+		case OBJECT_VARIABLE:
+			all_privileges = ACL_ALL_RIGHTS_VARIABLE;
+			errormsg = gettext_noop("invalid privilege type %s for schema variable");
+			break;
 		default:
 			elog(ERROR, "unrecognized GrantStmt.objtype: %d",
 				 (int) stmt->objtype);
@@ -599,6 +608,9 @@ ExecGrantStmt_oids(InternalGrant *istmt)
 		case OBJECT_TABLESPACE:
 			ExecGrant_Tablespace(istmt);
 			break;
+		case OBJECT_VARIABLE:
+			ExecGrant_Variable(istmt);
+			break;
 		default:
 			elog(ERROR, "unrecognized GrantStmt.objtype: %d",
 				 (int) istmt->objtype);
@@ -758,6 +770,16 @@ objectNamesToOids(ObjectType objtype, List *objnames)
 				objects = lappend_oid(objects, srvid);
 			}
 			break;
+		case OBJECT_VARIABLE:
+			foreach(cell, objnames)
+			{
+				RangeVar   *varvar = (RangeVar *) lfirst(cell);
+				Oid			relOid;
+
+				relOid = LookupVariable(varvar->schemaname, varvar->relname, false);
+				objects = lappend_oid(objects, relOid);
+			}
+			break;
 		default:
 			elog(ERROR, "unrecognized GrantStmt.objtype: %d",
 				 (int) objtype);
@@ -847,6 +869,33 @@ objectsInSchemaToOids(ObjectType objtype, List *nspnames)
 					table_close(rel, AccessShareLock);
 				}
 				break;
+			case OBJECT_VARIABLE:
+				{
+					ScanKeyData key;
+					Relation	rel;
+					TableScanDesc scan;
+					HeapTuple	tuple;
+
+					ScanKeyInit(&key,
+								Anum_pg_variable_varnamespace,
+								BTEqualStrategyNumber, F_OIDEQ,
+								ObjectIdGetDatum(namespaceId));
+
+					rel = table_open(VariableRelationId, AccessShareLock);
+					scan = table_beginscan_catalog(rel, 1, &key);
+
+					while ((tuple = heap_getnext(scan, ForwardScanDirection)) != NULL)
+					{
+						Oid			oid = ((Form_pg_proc) GETSTRUCT(tuple))->oid;
+
+						objects = lappend_oid(objects, oid);
+					}
+
+					table_endscan(scan);
+					table_close(rel, AccessShareLock);
+				}
+				break;
+
 			default:
 				/* should not happen */
 				elog(ERROR, "unrecognized GrantStmt.objtype: %d",
@@ -1012,6 +1061,10 @@ ExecAlterDefaultPrivilegesStmt(ParseState *pstate, AlterDefaultPrivilegesStmt *s
 			all_privileges = ACL_ALL_RIGHTS_SCHEMA;
 			errormsg = gettext_noop("invalid privilege type %s for schema");
 			break;
+		case OBJECT_VARIABLE:
+			all_privileges = ACL_ALL_RIGHTS_VARIABLE;
+			errormsg = gettext_noop("invalid privilege type %s for schema variable");
+			break;
 		default:
 			elog(ERROR, "unrecognized GrantStmt.objtype: %d",
 				 (int) action->objtype);
@@ -1209,6 +1262,12 @@ SetDefaultACL(InternalDefaultACL *iacls)
 				this_privileges = ACL_ALL_RIGHTS_SCHEMA;
 			break;
 
+		case OBJECT_VARIABLE:
+			objtype = DEFACLOBJ_VARIABLE;
+			if (iacls->all_privs && this_privileges == ACL_NO_RIGHTS)
+				this_privileges = ACL_ALL_RIGHTS_VARIABLE;
+			break;
+
 		default:
 			elog(ERROR, "unrecognized objtype: %d",
 				 (int) iacls->objtype);
@@ -1442,6 +1501,9 @@ RemoveRoleFromObjectACL(Oid roleid, Oid classid, Oid objid)
 			case DEFACLOBJ_NAMESPACE:
 				iacls.objtype = OBJECT_SCHEMA;
 				break;
+			case DEFACLOBJ_VARIABLE:
+				iacls.objtype = OBJECT_VARIABLE;
+				break;
 			default:
 				/* Shouldn't get here */
 				elog(ERROR, "unexpected default ACL type: %d",
@@ -1499,6 +1561,9 @@ RemoveRoleFromObjectACL(Oid roleid, Oid classid, Oid objid)
 			case ForeignDataWrapperRelationId:
 				istmt.objtype = OBJECT_FDW;
 				break;
+			case VariableRelationId:
+				istmt.objtype = OBJECT_VARIABLE;
+				break;
 			default:
 				elog(ERROR, "unexpected object class %u", classid);
 				break;
@@ -3230,6 +3295,129 @@ ExecGrant_Type(InternalGrant *istmt)
 	table_close(relation, RowExclusiveLock);
 }
 
+static void
+ExecGrant_Variable(InternalGrant *istmt)
+{
+	Relation	relation;
+	ListCell   *cell;
+
+	if (istmt->all_privs && istmt->privileges == ACL_NO_RIGHTS)
+		istmt->privileges = ACL_ALL_RIGHTS_VARIABLE;
+
+	relation = table_open(VariableRelationId, RowExclusiveLock);
+
+	foreach(cell, istmt->objects)
+	{
+		Oid			varId = lfirst_oid(cell);
+		Form_pg_variable pg_variable_tuple;
+		Datum		aclDatum;
+		bool		isNull;
+		AclMode		avail_goptions;
+		AclMode		this_privileges;
+		Acl		   *old_acl;
+		Acl		   *new_acl;
+		Oid			grantorId;
+		Oid			ownerId;
+		HeapTuple	tuple;
+		HeapTuple	newtuple;
+		Datum		values[Natts_pg_variable];
+		bool		nulls[Natts_pg_variable];
+		bool		replaces[Natts_pg_variable];
+		int			noldmembers;
+		int			nnewmembers;
+		Oid		   *oldmembers;
+		Oid		   *newmembers;
+
+		tuple = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varId));
+		if (!HeapTupleIsValid(tuple))
+			elog(ERROR, "cache lookup failed for schema variables %u", varId);
+
+		pg_variable_tuple = (Form_pg_variable) GETSTRUCT(tuple);
+
+		/*
+		 * Get owner ID and working copy of existing ACL. If there's no ACL,
+		 * substitute the proper default.
+		 */
+		ownerId = pg_variable_tuple->varowner;
+		aclDatum = SysCacheGetAttr(VARIABLEOID, tuple, Anum_pg_variable_varacl,
+								   &isNull);
+		if (isNull)
+		{
+			old_acl = acldefault(OBJECT_VARIABLE, ownerId);
+			/* There are no old member roles according to the catalogs */
+			noldmembers = 0;
+			oldmembers = NULL;
+		}
+		else
+		{
+			old_acl = DatumGetAclPCopy(aclDatum);
+			/* Get the roles mentioned in the existing ACL */
+			noldmembers = aclmembers(old_acl, &oldmembers);
+		}
+
+		/* Determine ID to do the grant as, and available grant options */
+		select_best_grantor(GetUserId(), istmt->privileges,
+							old_acl, ownerId,
+							&grantorId, &avail_goptions);
+
+		/*
+		 * Restrict the privileges to what we can actually grant, and emit the
+		 * standards-mandated warning and error messages.
+		 */
+		this_privileges =
+			restrict_and_check_grant(istmt->is_grant, avail_goptions,
+									 istmt->all_privs, istmt->privileges,
+									 varId, grantorId, OBJECT_VARIABLE,
+									 NameStr(pg_variable_tuple->varname),
+									 0, NULL);
+
+		/*
+		 * Generate new ACL.
+		 */
+		new_acl = merge_acl_with_grant(old_acl, istmt->is_grant,
+									   istmt->grant_option, istmt->behavior,
+									   istmt->grantees, this_privileges,
+									   grantorId, ownerId);
+
+		/*
+		 * We need the members of both old and new ACLs so we can correct the
+		 * shared dependency information.
+		 */
+		nnewmembers = aclmembers(new_acl, &newmembers);
+
+		/* finished building new ACL value, now insert it */
+		MemSet(values, 0, sizeof(values));
+		MemSet(nulls, false, sizeof(nulls));
+		MemSet(replaces, false, sizeof(replaces));
+
+		replaces[Anum_pg_variable_varacl - 1] = true;
+		values[Anum_pg_variable_varacl - 1] = PointerGetDatum(new_acl);
+
+		newtuple = heap_modify_tuple(tuple, RelationGetDescr(relation), values,
+									 nulls, replaces);
+
+		CatalogTupleUpdate(relation, &newtuple->t_self, newtuple);
+
+		/* Update initial privileges for extensions */
+		recordExtensionInitPriv(varId, VariableRelationId, 0, new_acl);
+
+		/* Update the shared dependency ACL info */
+		updateAclDependencies(VariableRelationId, varId, 0,
+							  ownerId,
+							  noldmembers, oldmembers,
+							  nnewmembers, newmembers);
+
+		ReleaseSysCache(tuple);
+
+		pfree(new_acl);
+
+		/* prevent error when processing duplicate objects */
+		CommandCounterIncrement();
+	}
+
+	table_close(relation, RowExclusiveLock);
+}
+
 
 static AclMode
 string_to_privilege(const char *privname)
@@ -3262,6 +3450,10 @@ string_to_privilege(const char *privname)
 		return ACL_CONNECT;
 	if (strcmp(privname, "rule") == 0)
 		return 0;				/* ignore old RULE privileges */
+	if (strcmp(privname, "read") == 0)
+		return ACL_READ;
+	if (strcmp(privname, "write") == 0)
+		return ACL_WRITE;
 	ereport(ERROR,
 			(errcode(ERRCODE_SYNTAX_ERROR),
 			 errmsg("unrecognized privilege type \"%s\"", privname)));
@@ -3297,6 +3489,10 @@ privilege_to_string(AclMode privilege)
 			return "TEMP";
 		case ACL_CONNECT:
 			return "CONNECT";
+		case ACL_READ:
+			return "READ";
+		case ACL_WRITE:
+			return "WRITE";
 		default:
 			elog(ERROR, "unrecognized privilege: %d", (int) privilege);
 	}
@@ -3420,6 +3616,9 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
 					case OBJECT_TYPE:
 						msg = gettext_noop("permission denied for type %s");
 						break;
+					case OBJECT_VARIABLE:
+						msg = gettext_noop("permission denied for schema variable %s");
+						break;
 					case OBJECT_VIEW:
 						msg = gettext_noop("permission denied for view %s");
 						break;
@@ -3530,6 +3729,9 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
 					case OBJECT_TYPE:
 						msg = gettext_noop("must be owner of type %s");
 						break;
+					case OBJECT_VARIABLE:
+						msg = gettext_noop("must be owner of schema variable %s");
+						break;
 					case OBJECT_VIEW:
 						msg = gettext_noop("must be owner of view %s");
 						break;
@@ -3674,6 +3876,8 @@ pg_aclmask(ObjectType objtype, Oid table_oid, AttrNumber attnum, Oid roleid,
 			return ACL_NO_RIGHTS;
 		case OBJECT_TYPE:
 			return pg_type_aclmask(table_oid, roleid, mask, how);
+		case OBJECT_VARIABLE:
+			return pg_variable_aclmask(table_oid, roleid, mask, how);
 		default:
 			elog(ERROR, "unrecognized objtype: %d",
 				 (int) objtype);
@@ -4542,6 +4746,66 @@ pg_type_aclmask(Oid type_oid, Oid roleid, AclMode mask, AclMaskHow how)
 	return result;
 }
 
+/*
+ * Exported routine for examining a user's privileges for a variable.
+ */
+AclMode
+pg_variable_aclmask(Oid var_oid, Oid roleid, AclMode mask, AclMaskHow how)
+{
+	AclMode		result;
+	HeapTuple	tuple;
+	Datum		aclDatum;
+	bool		isNull;
+	Acl		   *acl;
+	Oid			ownerId;
+
+	Form_pg_variable varForm;
+
+	/* Bypass permission checks for superusers */
+	if (superuser_arg(roleid))
+		return mask;
+
+	/*
+	 * Must get the type's tuple from pg_type
+	 */
+	tuple = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(var_oid));
+	if (!HeapTupleIsValid(tuple))
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("variable with OID %u does not exist",
+						var_oid)));
+	varForm = (Form_pg_variable) GETSTRUCT(tuple);
+
+	/*
+	 * Now get the type's owner and ACL from the tuple
+	 */
+	ownerId = varForm->varowner;
+
+	aclDatum = SysCacheGetAttr(VARIABLEOID, tuple,
+							   Anum_pg_variable_varacl, &isNull);
+	if (isNull)
+	{
+		/* No ACL, so build default ACL */
+		acl = acldefault(OBJECT_VARIABLE, ownerId);
+		aclDatum = (Datum) 0;
+	}
+	else
+	{
+		/* detoast rel's ACL if necessary */
+		acl = DatumGetAclP(aclDatum);
+	}
+
+	result = aclmask(acl, roleid, ownerId, mask, how);
+
+	/* if we have a detoasted copy, free it */
+	if (acl && (Pointer) acl != DatumGetPointer(aclDatum))
+		pfree(acl);
+
+	ReleaseSysCache(tuple);
+
+	return result;
+}
+
 /*
  * Exported routine for checking a user's access privileges to a column
  *
@@ -4816,6 +5080,18 @@ pg_type_aclcheck(Oid type_oid, Oid roleid, AclMode mode)
 		return ACLCHECK_NO_PRIV;
 }
 
+/*
+ * Exported routine for checking a user's access privileges to a variable
+ */
+AclResult
+pg_variable_aclcheck(Oid type_oid, Oid roleid, AclMode mode)
+{
+	if (pg_variable_aclmask(type_oid, roleid, mode, ACLMASK_ANY) != 0)
+		return ACLCHECK_OK;
+	else
+		return ACLCHECK_NO_PRIV;
+}
+
 /*
  * Ownership check for a relation (specified by OID).
  */
@@ -5433,6 +5709,33 @@ pg_statistics_object_ownercheck(Oid stat_oid, Oid roleid)
 	return has_privs_of_role(roleid, ownerId);
 }
 
+/*
+ * Ownership check for a schema variables (specified by OID).
+ */
+bool
+pg_variable_ownercheck(Oid db_oid, Oid roleid)
+{
+	HeapTuple	tuple;
+	Oid			ownerId;
+
+	/* Superusers bypass all permission checking. */
+	if (superuser_arg(roleid))
+		return true;
+
+	tuple = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(db_oid));
+	if (!HeapTupleIsValid(tuple))
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_DATABASE),
+				 errmsg("variable with OID %u does not exist", db_oid)));
+
+	ownerId = ((Form_pg_variable) GETSTRUCT(tuple))->varowner;
+
+	ReleaseSysCache(tuple);
+
+	return has_privs_of_role(roleid, ownerId);
+}
+
+
 /*
  * Check whether specified role has CREATEROLE privilege (or is a superuser)
  *
@@ -5561,6 +5864,10 @@ get_user_default_acl(ObjectType objtype, Oid ownerId, Oid nsp_oid)
 			defaclobjtype = DEFACLOBJ_NAMESPACE;
 			break;
 
+		case OBJECT_VARIABLE:
+			defaclobjtype = DEFACLOBJ_VARIABLE;
+			break;
+
 		default:
 			return NULL;
 	}
diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c
index 8d8e926c21..7ce7c38532 100644
--- a/src/backend/catalog/dependency.c
+++ b/src/backend/catalog/dependency.c
@@ -61,12 +61,15 @@
 #include "catalog/pg_ts_template.h"
 #include "catalog/pg_type.h"
 #include "catalog/pg_user_mapping.h"
+#include "catalog/pg_variable.h"
 #include "commands/comment.h"
 #include "commands/defrem.h"
 #include "commands/event_trigger.h"
 #include "commands/extension.h"
 #include "commands/policy.h"
 #include "commands/publicationcmds.h"
+#include "commands/schemacmds.h"
+#include "commands/schema_variable.h"
 #include "commands/seclabel.h"
 #include "commands/sequence.h"
 #include "commands/trigger.h"
@@ -181,7 +184,8 @@ static const Oid object_classes[] = {
 	PublicationRelationId,		/* OCLASS_PUBLICATION */
 	PublicationRelRelationId,	/* OCLASS_PUBLICATION_REL */
 	SubscriptionRelationId,		/* OCLASS_SUBSCRIPTION */
-	TransformRelationId			/* OCLASS_TRANSFORM */
+	TransformRelationId,		/* OCLASS_TRANSFORM */
+	VariableRelationId			/* OCLASS_VARIABLE */
 };

@@ -1572,6 +1576,10 @@ doDeletion(const ObjectAddress *object, int flags)
 			DropObjectById(object);
 			break;
 
+		case OCLASS_VARIABLE:
+			RemoveSchemaVariable(object->objectId);
+			break;
+
 			/*
 			 * These global object types are not supported here.
 			 */
@@ -2010,6 +2018,11 @@ find_expr_references_walker(Node *node,
 	{
 		Param	   *param = (Param *) node;
 
+		if (param->paramkind == PARAM_VARIABLE)
+			/* A variable parameter depends on the schema 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);
@@ -3002,6 +3015,9 @@ getObjectClass(const ObjectAddress *object)
 
 		case TransformRelationId:
 			return OCLASS_TRANSFORM;
+
+		case VariableRelationId:
+			return OCLASS_VARIABLE;
 	}
 
 	/* shouldn't get here */
diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c
index 005e029c38..1f2b207687 100644
--- a/src/backend/catalog/namespace.c
+++ b/src/backend/catalog/namespace.c
@@ -39,6 +39,7 @@
 #include "catalog/pg_ts_parser.h"
 #include "catalog/pg_ts_template.h"
 #include "catalog/pg_type.h"
+#include "catalog/pg_variable.h"
 #include "commands/dbcommands.h"
 #include "funcapi.h"
 #include "mb/pg_wchar.h"
@@ -762,6 +763,70 @@ RelationIsVisible(Oid relid)
 	return visible;
 }
 
+/*
+ * VariableIsVisible
+ *		Determine whether a variable (identified by OID) is visible in the
+ *		current search path.  Visible means "would be found by searching
+ *		for the unqualified variable name".
+ */
+bool
+VariableIsVisible(Oid varid)
+{
+	HeapTuple	vartup;
+	Form_pg_variable varform;
+	Oid			varnamespace;
+	bool		visible;
+
+	vartup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid));
+	if (!HeapTupleIsValid(vartup))
+		elog(ERROR, "cache lookup failed for schema variable %u", varid);
+	varform = (Form_pg_variable) GETSTRUCT(vartup);
+
+	recomputeNamespacePath();
+
+	/*
+	 * Quick check: if it ain't in the path at all, it ain't visible. Items in
+	 * the system namespace are surely in the path and so we needn't even do
+	 * list_member_oid() for them.
+	 */
+	varnamespace = varform->varnamespace;
+	if (varnamespace != PG_CATALOG_NAMESPACE &&
+		!list_member_oid(activeSearchPath, varnamespace))
+		visible = false;
+	else
+	{
+		/*
+		 * If it is in the path, it might still not be visible; it could be
+		 * hidden by another relation of the same name earlier in the path. So
+		 * we must do a slow check for conflicting relations.
+		 */
+		char	   *varname = NameStr(varform->varname);
+		ListCell   *l;
+
+		visible = false;
+		foreach(l, activeSearchPath)
+		{
+			Oid			namespaceId = lfirst_oid(l);
+
+			if (namespaceId == varnamespace)
+			{
+				/* Found it first in path */
+				visible = true;
+				break;
+			}
+			if (OidIsValid(get_varname_varid(varname, namespaceId)))
+			{
+				/* Found something else first in path */
+				break;
+			}
+		}
+	}
+
+	ReleaseSysCache(vartup);
+
+	return visible;
+}
+
 
 /*
  * TypenameGetTypid
@@ -2796,6 +2861,209 @@ TSConfigIsVisible(Oid cfgid)
 	return visible;
 }
 
+/*
+ * When we know a variable name, then we can find variable simply
+ */
+Oid
+LookupVariable(const char *nspname, const char *varname, bool missing_ok)
+{
+	Oid			namespaceId;
+	Oid			varoid = InvalidOid;
+	ListCell   *l;
+
+	if (nspname)
+	{
+		namespaceId = LookupExplicitNamespace(nspname, missing_ok);
+		if (!OidIsValid(namespaceId))
+			return InvalidOid;
+
+		varoid = GetSysCacheOid2(VARIABLENAMENSP, Anum_pg_variable_oid,
+								 PointerGetDatum(varname),
+								 ObjectIdGetDatum(namespaceId));
+	}
+	else
+	{
+		/* search for it in search path */
+		recomputeNamespacePath();
+
+		foreach(l, activeSearchPath)
+		{
+			namespaceId = lfirst_oid(l);
+
+			varoid = GetSysCacheOid2(VARIABLENAMENSP, Anum_pg_variable_oid,
+									 PointerGetDatum(varname),
+									 ObjectIdGetDatum(namespaceId));
+
+			if (OidIsValid(varoid))
+				break;
+		}
+	}
+
+	if (!OidIsValid(varoid) && !missing_ok)
+	{
+		if (nspname)
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("variable \"%s\".\"%s\" does not exist",
+							nspname, varname)));
+		else
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("variable \"%s\" does not exist",
+							varname)));
+	}
+
+	return varoid;
+}
+
+/*
+ * The source list holds names with indirection expressions used
+ * as left part of LET statement. Following routine makes 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
+ *
+ * Returns oid of not ambigonuous variable specified by qualified path
+ * or InvalidOid. When the path is ambigonuous, then not_uniq flag is
+ * is true.
+ */
+Oid
+IdentifyVariable(List *names, char **attrname, bool *not_uniq)
+{
+	char	   *a = NULL;
+	char	   *b = NULL;
+	char	   *c = NULL;
+	char	   *d = NULL;
+	Oid			varoid_without_attr;
+	Oid			varoid_with_attr;
+
+	*not_uniq = false;
+
+	switch (list_length(names))
+	{
+		case 1:
+			a = strVal(linitial(names));
+			return LookupVariable(NULL, a, true);
+
+		case 2:
+			a = strVal(linitial(names));
+			b = strVal(lsecond(names));
+
+			/*
+			 * a.b can mean "schema"."variable" or "variable"."field", Check
+			 * both variants, and returns InvalidOid with not_uniq flag, when
+			 * both interpretations are possible.
+			 */
+			varoid_without_attr = LookupVariable(a, b, true);
+			varoid_with_attr = LookupVariable(NULL, a, true);
+
+			if (OidIsValid(varoid_without_attr) && OidIsValid(varoid_with_attr))
+			{
+				*not_uniq = true;
+				return InvalidOid;
+			}
+			else if (OidIsValid(varoid_without_attr))
+			{
+				*attrname = NULL;
+				return varoid_without_attr;
+			}
+			else
+			{
+				*attrname = b;
+				return varoid_with_attr;
+			}
+			break;
+
+		case 3:
+			a = strVal(linitial(names));
+			b = strVal(lsecond(names));
+			c = strVal(lthird(names));
+
+			/*
+			 * a.b.c can mean "catalog"."schema"."variable" or
+			 * "schema"."variable"."field", Check both variants, and returns
+			 * InvalidOid with not_uniq flag, when both interpretations are
+			 * possible.
+			 */
+			varoid_without_attr = LookupVariable(b, c, true);
+			varoid_with_attr = LookupVariable(a, b, true);
+
+			if (OidIsValid(varoid_without_attr) && OidIsValid(varoid_with_attr))
+			{
+				*not_uniq = true;
+				return InvalidOid;
+			}
+			else if (OidIsValid(varoid_without_attr))
+			{
+				*attrname = NULL;
+
+				/*
+				 * We in this case a "a" is used as catalog name, check it.
+				 */
+				if (strcmp(a, get_database_name(MyDatabaseId)) != 0)
+					ereport(ERROR,
+							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+							 errmsg("cross-database references are not implemented: %s",
+									NameListToString(names))));
+
+				return varoid_without_attr;
+			}
+			else
+			{
+				*attrname = c;
+				return varoid_with_attr;
+			}
+			break;
+
+		case 4:
+			a = strVal(linitial(names));
+			b = strVal(lsecond(names));
+			c = strVal(lthird(names));
+			d = strVal(lfourth(names));
+
+			/*
+			 * We in this case a "a" is used as catalog name, check it.
+			 */
+			if (strcmp(a, get_database_name(MyDatabaseId)) != 0)
+				ereport(ERROR,
+						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+						 errmsg("cross-database references are not implemented: %s",
+								NameListToString(names))));
+
+			*attrname = d;
+			return LookupVariable(b, c, true);
+
+		default:
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("improper qualified name (too many dotted names): %s",
+							NameListToString(names))));
+			break;
+	}
+}
 
 /*
  * DeconstructQualifiedName
@@ -4611,3 +4879,14 @@ pg_is_other_temp_schema(PG_FUNCTION_ARGS)
 
 	PG_RETURN_BOOL(isOtherTempNamespace(oid));
 }
+
+Datum
+pg_variable_is_visible(PG_FUNCTION_ARGS)
+{
+	Oid			oid = PG_GETARG_OID(0);
+
+	if (!SearchSysCacheExists1(VARIABLEOID, ObjectIdGetDatum(oid)))
+		PG_RETURN_NULL();
+
+	PG_RETURN_BOOL(VariableIsVisible(oid));
+}
diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c
index 6d88b690d8..e4df1834dc 100644
--- a/src/backend/catalog/objectaddress.c
+++ b/src/backend/catalog/objectaddress.c
@@ -61,6 +61,7 @@
 #include "catalog/pg_ts_template.h"
 #include "catalog/pg_type.h"
 #include "catalog/pg_user_mapping.h"
+#include "catalog/pg_variable.h"
 #include "commands/dbcommands.h"
 #include "commands/defrem.h"
 #include "commands/event_trigger.h"
@@ -615,6 +616,20 @@ static const ObjectPropertyType ObjectProperty[] =
 		OBJECT_USER_MAPPING,
 		false
 	},
+	{
+		"schema variable",
+		VariableRelationId,
+		VariableObjectIndexId,
+		VARIABLEOID,
+		VARIABLENAMENSP,
+		Anum_pg_variable_oid,
+		Anum_pg_variable_varname,
+		Anum_pg_variable_varnamespace,
+		Anum_pg_variable_varowner,
+		Anum_pg_variable_varacl,
+		OBJECT_VARIABLE,
+		true
+	}
 };
 
 /*
@@ -839,6 +854,10 @@ static const struct object_type_map
 	/* OCLASS_STATISTIC_EXT */
 	{
 		"statistics object", OBJECT_STATISTIC_EXT
+	},
+	/* OCLASS_VARIABLE */
+	{
+		"schema variable", OBJECT_VARIABLE
 	}
 };
 
@@ -864,6 +883,7 @@ static ObjectAddress get_object_address_attrdef(ObjectType objtype,
 												bool missing_ok);
 static ObjectAddress get_object_address_type(ObjectType objtype,
 											 TypeName *typename, bool missing_ok);
+static ObjectAddress get_object_address_variable(List *object, bool missing_ok);
 static ObjectAddress get_object_address_opcf(ObjectType objtype, List *object,
 											 bool missing_ok);
 static ObjectAddress get_object_address_opf_member(ObjectType objtype,
@@ -1127,6 +1147,9 @@ get_object_address(ObjectType objtype, Node *object,
 															 missing_ok);
 				address.objectSubId = 0;
 				break;
+			case OBJECT_VARIABLE:
+				address = get_object_address_variable(castNode(List, object), missing_ok);
+				break;
 			default:
 				elog(ERROR, "unrecognized objtype: %d", (int) objtype);
 				/* placate compiler, in case it thinks elog might return */
@@ -1983,16 +2006,20 @@ get_object_address_defacl(List *object, bool missing_ok)
 		case DEFACLOBJ_NAMESPACE:
 			objtype_str = "schemas";
 			break;
+		case DEFACLOBJ_VARIABLE:
+			objtype_str = "variables";
+			break;
 		default:
 			ereport(ERROR,
 					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 					 errmsg("unrecognized default ACL object type \"%c\"", objtype),
-					 errhint("Valid object types are \"%c\", \"%c\", \"%c\", \"%c\", \"%c\".",
+					 errhint("Valid object types are \"%c\", \"%c\", \"%c\", \"%c\", \"%c\", \"%c\".",
 							 DEFACLOBJ_RELATION,
 							 DEFACLOBJ_SEQUENCE,
 							 DEFACLOBJ_FUNCTION,
 							 DEFACLOBJ_TYPE,
-							 DEFACLOBJ_NAMESPACE)));
+							 DEFACLOBJ_NAMESPACE,
+							 DEFACLOBJ_VARIABLE)));
 	}
 
 	/*
@@ -2077,6 +2104,24 @@ textarray_to_strvaluelist(ArrayType *arr)
 	return list;
 }
 
+/*
+ * Find the ObjectAddress for a schema variable
+ */
+static ObjectAddress
+get_object_address_variable(List *object, bool missing_ok)
+{
+	ObjectAddress address;
+	char	   *nspname = NULL;
+	char	   *varname = NULL;
+
+	ObjectAddressSet(address, VariableRelationId, InvalidOid);
+
+	DeconstructQualifiedName(object, &nspname, &varname);
+	address.objectId = LookupVariable(nspname, varname, missing_ok);
+
+	return address;
+}
+
 /*
  * SQL-callable version of get_object_address
  */
@@ -2266,6 +2311,7 @@ pg_get_object_address(PG_FUNCTION_ARGS)
 		case OBJECT_TABCONSTRAINT:
 		case OBJECT_OPCLASS:
 		case OBJECT_OPFAMILY:
+		case OBJECT_VARIABLE:
 			objnode = (Node *) name;
 			break;
 		case OBJECT_ACCESS_METHOD:
@@ -2572,6 +2618,11 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address,
 			if (!pg_statistics_object_ownercheck(address.objectId, roleid))
 				aclcheck_error_type(ACLCHECK_NOT_OWNER, address.objectId);
 			break;
+		case OBJECT_VARIABLE:
+			if (!pg_variable_ownercheck(address.objectId, roleid))
+				aclcheck_error(ACLCHECK_NOT_OWNER, objtype,
+							   NameListToString(castNode(List, object)));
+			break;
 		default:
 			elog(ERROR, "unrecognized object type: %d",
 				 (int) objtype);
@@ -3448,6 +3499,32 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok)
 				break;
 			}
 
+		case OCLASS_VARIABLE:
+			{
+				char	   *nspname;
+				HeapTuple	tup;
+				Form_pg_variable varform;
+
+				tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(object->objectId));
+				if (!HeapTupleIsValid(tup))
+					elog(ERROR, "cache lookup failed for schema variable %u",
+						 object->objectId);
+
+				varform = (Form_pg_variable) GETSTRUCT(tup);
+
+				if (VariableIsVisible(object->objectId))
+					nspname = NULL;
+				else
+					nspname = get_namespace_name(varform->varnamespace);
+
+				appendStringInfo(&buffer, _("schema variable %s"),
+								 quote_qualified_identifier(nspname,
+															NameStr(varform->varname)));
+
+				ReleaseSysCache(tup);
+				break;
+			}
+
 		case OCLASS_TSPARSER:
 			{
 				HeapTuple	tup;
@@ -3758,6 +3835,16 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok)
 										 _("default privileges on new schemas belonging to role %s"),
 										 rolename);
 						break;
+					case DEFACLOBJ_VARIABLE:
+						if (nspname)
+							appendStringInfo(&buffer,
+											 _("default privileges on new variables belonging to role %s in schema %s"),
+											 rolename, nspname);
+						else
+							appendStringInfo(&buffer,
+											 _("default privileges on new variables belonging to role %s"),
+											 rolename);
+						break;
 					default:
 						/* shouldn't get here */
 						if (nspname)
@@ -4478,6 +4565,10 @@ getObjectTypeDescription(const ObjectAddress *object, bool missing_ok)
 			appendStringInfoString(&buffer, "transform");
 			break;
 
+		case OCLASS_VARIABLE:
+			appendStringInfoString(&buffer, "schema variable");
+			break;
+
 			/*
 			 * There's intentionally no default: case here; we want the
 			 * compiler to warn if a new OCLASS hasn't been handled above.
@@ -5570,6 +5661,10 @@ getObjectIdentityParts(const ObjectAddress *object,
 						appendStringInfoString(&buffer,
 											   " on schemas");
 						break;
+					case DEFACLOBJ_VARIABLE:
+						appendStringInfoString(&buffer,
+											   " on schema variables");
+						break;
 				}
 
 				if (objname)
@@ -5762,6 +5857,33 @@ getObjectIdentityParts(const ObjectAddress *object,
 			}
 			break;
 
+		case OCLASS_VARIABLE:
+			{
+				char	   *schema;
+				char	   *varname;
+				HeapTuple	tup;
+				Form_pg_variable varform;
+
+				tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(object->objectId));
+				if (!HeapTupleIsValid(tup))
+					elog(ERROR, "cache lookup failed for schema variable %u",
+						 object->objectId);
+
+				varform = (Form_pg_variable) GETSTRUCT(tup);
+
+				schema = get_namespace_name_or_temp(varform->varnamespace);
+				varname = NameStr(varform->varname);
+
+				appendStringInfo(&buffer, "%s",
+								 quote_qualified_identifier(schema, varname));
+
+				if (objname)
+					*objname = list_make2(schema, varname);
+
+				ReleaseSysCache(tup);
+				break;
+			}
+
 			/*
 			 * There's intentionally no default: case here; we want the
 			 * compiler to warn if a new OCLASS hasn't been handled above.
diff --git a/src/backend/catalog/pg_variable.c b/src/backend/catalog/pg_variable.c
new file mode 100644
index 0000000000..451b57a0b7
--- /dev/null
+++ b/src/backend/catalog/pg_variable.c
@@ -0,0 +1,388 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_variable.c
+ *		schema variables
+ *
+ * Portions Copyright (c) 1996-2020, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *		src/backend/catalog/pg_variable.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/heapam.h"
+#include "access/htup_details.h"
+
+#include "catalog/catalog.h"
+#include "catalog/dependency.h"
+#include "catalog/indexing.h"
+#include "catalog/namespace.h"
+#include "catalog/objectaccess.h"
+#include "catalog/pg_namespace.h"
+#include "catalog/pg_type.h"
+#include "catalog/pg_variable.h"
+
+#include "commands/schema_variable.h"
+
+#include "storage/lmgr.h"
+
+#include "utils/builtins.h"
+#include "utils/lsyscache.h"
+#include "utils/syscache.h"
+
+static VariableEOXActionCodes
+to_eoxaction_code(VariableEOXAction action)
+{
+	switch (action)
+	{
+		case VARIABLE_EOX_NOOP:
+			return VARIABLE_EOX_CODE_NOOP;
+
+		case VARIABLE_EOX_DROP:
+			return VARIABLE_EOX_CODE_DROP;
+
+		case VARIABLE_EOX_RESET:
+			return VARIABLE_EOX_CODE_RESET;
+
+		default:
+			elog(ERROR, "unexpected action");
+	}
+
+}
+
+static VariableEOXAction
+to_eoxaction(VariableEOXActionCodes code)
+{
+	switch (code)
+	{
+		case VARIABLE_EOX_CODE_NOOP:
+			return VARIABLE_EOX_NOOP;
+
+		case VARIABLE_EOX_CODE_DROP:
+			return VARIABLE_EOX_DROP;
+
+		case VARIABLE_EOX_CODE_RESET:
+			return VARIABLE_EOX_RESET;
+
+		default:
+			elog(ERROR, "unexpected code");
+	}
+}
+
+/*
+ * Returns name of schema variable. When variable is not on path,
+ * then the name is qualified.
+ */
+char *
+schema_variable_get_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 = NULL;
+	else
+		nspname = get_namespace_name(varform->varnamespace);
+
+	result = quote_qualified_identifier(nspname, varname);
+
+	ReleaseSysCache(tup);
+
+	return result;
+}
+
+/*
+ * Returns varname field of pg_variable
+ */
+char *
+get_schema_variable_name(Oid varid)
+{
+	HeapTuple	tup;
+	Form_pg_variable varform;
+	char	   *varname;
+
+	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);
+
+	ReleaseSysCache(tup);
+
+	return varname;
+}
+
+/*
+ * Returns type, typmod of schema variable
+ */
+void
+get_schema_variable_type_typmod_collid(Oid varid, Oid *typid, int32 *typmod, Oid *collid)
+{
+	HeapTuple	tup;
+	Form_pg_variable varform;
+
+	tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid));
+
+	if (!HeapTupleIsValid(tup))
+		elog(ERROR, "cache lookup failed for variable %u", varid);
+
+	varform = (Form_pg_variable) GETSTRUCT(tup);
+
+	*typid = varform->vartype;
+	*typmod = varform->vartypmod;
+	*collid = varform->varcollation;
+
+	ReleaseSysCache(tup);
+
+	return;
+}
+
+/*
+ * Fetch all fields of schema variable from the syscache.
+ */
+void
+initVariable(Variable *var, Oid varid, bool missing_ok, bool fast_only)
+{
+	HeapTuple	tup;
+	Form_pg_variable varform;
+	Datum		defexpr_datum;
+	bool		defexpr_isnull;
+
+	tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid));
+
+	if (!HeapTupleIsValid(tup))
+	{
+		if (missing_ok)
+		{
+			var->oid = InvalidOid;
+			return;
+		}
+
+		elog(ERROR, "cache lookup failed for variable %u", varid);
+	}
+
+	varform = (Form_pg_variable) GETSTRUCT(tup);
+
+	var->oid = varid;
+	var->name = pstrdup(NameStr(varform->varname));
+	var->namespace = varform->varnamespace;
+	var->typid = varform->vartype;
+	var->typmod = varform->vartypmod;
+	var->owner = varform->varowner;
+	var->collation = varform->varcollation;
+	var->eoxaction = to_eoxaction(varform->vareoxaction);
+	var->is_not_null = varform->varisnotnull;
+	var->is_immutable = varform->varisimmutable;
+
+	/* Get defexpr */
+	defexpr_datum = SysCacheGetAttr(VARIABLEOID,
+									tup,
+									Anum_pg_variable_vardefexpr,
+									&defexpr_isnull);
+
+	var->has_defexpr = !defexpr_isnull;
+
+	/*
+	 * Sometimes we don't need deserialized defexpr, but we need info about
+	 * existence of defexpr.
+	 */
+
+	if (!fast_only)
+	{
+		Datum		aclDatum;
+		bool		isnull;
+
+		/* name */
+		var->name = pstrdup(NameStr(varform->varname));
+
+		if (!defexpr_isnull)
+			var->defexpr = stringToNode(TextDatumGetCString(defexpr_datum));
+		else
+			var->defexpr = NULL;
+
+		/* Get varacl */
+		aclDatum = SysCacheGetAttr(VARIABLEOID,
+								   tup,
+								   Anum_pg_variable_varacl,
+								   &isnull);
+		if (!isnull)
+			var->acl = DatumGetAclPCopy(aclDatum);
+		else
+			var->acl = NULL;
+	}
+	else
+	{
+		var->name = NULL;
+		var->defexpr = NULL;
+		var->acl = NULL;
+	}
+
+	ReleaseSysCache(tup);
+
+	return;
+}
+
+/*
+ * Create entry in pg_variable table
+ */
+ObjectAddress
+VariableCreate(const char *varName,
+			   Oid varNamespace,
+			   Oid varType,
+			   int32 varTypmod,
+			   Oid varOwner,
+			   Oid varCollation,
+			   Node *varDefexpr,
+			   VariableEOXAction eoxaction,
+			   bool is_not_null,
+			   bool if_not_exists,
+			   bool is_immutable)
+{
+	Acl		   *varacl;
+	NameData	varname;
+	bool		nulls[Natts_pg_variable];
+	Datum		values[Natts_pg_variable];
+	Relation	rel;
+	HeapTuple	tup,
+				oldtup;
+	TupleDesc	tupdesc;
+	ObjectAddress myself,
+				referenced;
+	Oid			varid;
+	int			i;
+
+	for (i = 0; i < Natts_pg_variable; i++)
+	{
+		nulls[i] = false;
+		values[i] = (Datum) 0;
+	}
+
+	namestrcpy(&varname, varName);
+
+	rel = table_open(VariableRelationId, RowExclusiveLock);
+
+	varid = GetNewOidWithIndex(rel, VariableObjectIndexId, Anum_pg_variable_oid);
+	values[Anum_pg_variable_oid - 1] = ObjectIdGetDatum(varid);
+	values[Anum_pg_variable_varname - 1] = NameGetDatum(&varname);
+	values[Anum_pg_variable_varnamespace - 1] = ObjectIdGetDatum(varNamespace);
+	values[Anum_pg_variable_vartype - 1] = ObjectIdGetDatum(varType);
+	values[Anum_pg_variable_vartypmod - 1] = Int32GetDatum(varTypmod);
+	values[Anum_pg_variable_varowner - 1] = ObjectIdGetDatum(varOwner);
+	values[Anum_pg_variable_varcollation - 1] = ObjectIdGetDatum(varCollation);
+	values[Anum_pg_variable_vareoxaction - 1] = CharGetDatum((char) to_eoxaction_code(eoxaction));
+	values[Anum_pg_variable_varisnotnull - 1] = BoolGetDatum(is_not_null);
+	values[Anum_pg_variable_varisimmutable - 1] = BoolGetDatum(is_immutable);
+	/* proacl will be determined later */
+
+	if (varDefexpr)
+		values[Anum_pg_variable_vardefexpr - 1] = CStringGetTextDatum(nodeToString(varDefexpr));
+	else
+		nulls[Anum_pg_variable_vardefexpr - 1] = true;
+
+	tupdesc = RelationGetDescr(rel);
+
+	oldtup = SearchSysCache2(VARIABLENAMENSP,
+							 PointerGetDatum(varName),
+							 ObjectIdGetDatum(varNamespace));
+
+	if (HeapTupleIsValid(oldtup))
+	{
+		if (if_not_exists)
+			ereport(NOTICE,
+					(errcode(ERRCODE_DUPLICATE_OBJECT),
+					 errmsg("schema variable \"%s\" already exists, skipping",
+							varName)));
+		else
+			ereport(ERROR,
+					(errcode(ERRCODE_DUPLICATE_OBJECT),
+					 errmsg("schema variable \"%s\" already exists",
+							varName)));
+
+		ReleaseSysCache(oldtup);
+		table_close(rel, RowExclusiveLock);
+
+		return InvalidObjectAddress;
+	}
+
+	varacl = get_user_default_acl(OBJECT_VARIABLE, varOwner,
+								  varNamespace);
+
+	if (varacl != NULL)
+		values[Anum_pg_variable_varacl - 1] = PointerGetDatum(varacl);
+	else
+		nulls[Anum_pg_variable_varacl - 1] = true;
+
+	tup = heap_form_tuple(tupdesc, values, nulls);
+	CatalogTupleInsert(rel, tup);
+
+	myself.classId = VariableRelationId;
+	myself.objectId = varid;
+	myself.objectSubId = 0;
+
+	/* dependency on namespace */
+	referenced.classId = NamespaceRelationId;
+	referenced.objectId = varNamespace;
+	referenced.objectSubId = 0;
+	recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL);
+
+	/* dependency on used type */
+	referenced.classId = TypeRelationId;
+	referenced.objectId = varType;
+	referenced.objectSubId = 0;
+	recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL);
+
+	/* dependency on default expr */
+	if (varDefexpr)
+		recordDependencyOnExpr(&myself, (Node *) varDefexpr,
+							   NIL, DEPENDENCY_NORMAL);
+
+	/* dependency on any roles mentioned in ACL */
+	if (varacl != NULL)
+	{
+		int			nnewmembers;
+		Oid		   *newmembers;
+
+		nnewmembers = aclmembers(varacl, &newmembers);
+		updateAclDependencies(VariableRelationId, varid, 0,
+							  varOwner,
+							  0, NULL,
+							  nnewmembers, newmembers);
+	}
+
+	/* dependency on extension */
+	recordDependencyOnCurrentExtension(&myself, false);
+
+	/*
+	 * register on commit action if it is necessary. This moment, the variable
+	 * has not any value, so we don't need to solve content transactionality.
+	 */
+	register_variable_on_commit_action(myself.objectId, eoxaction);
+
+	heap_freetuple(tup);
+
+	/* Post creation hook for new function */
+	InvokeObjectPostCreateHook(VariableRelationId, varid, 0);
+
+	table_close(rel, RowExclusiveLock);
+
+	return myself;
+}
diff --git a/src/backend/commands/Makefile b/src/backend/commands/Makefile
index e8504f0ae4..0b785be2ce 100644
--- a/src/backend/commands/Makefile
+++ b/src/backend/commands/Makefile
@@ -48,6 +48,7 @@ OBJS = \
 	proclang.o \
 	publicationcmds.o \
 	schemacmds.o \
+	schemavariable.o \
 	seclabel.o \
 	sequence.o \
 	statscmds.o \
diff --git a/src/backend/commands/alter.c b/src/backend/commands/alter.c
index 29249498a9..225b971d90 100644
--- a/src/backend/commands/alter.c
+++ b/src/backend/commands/alter.c
@@ -40,6 +40,7 @@
 #include "catalog/pg_ts_dict.h"
 #include "catalog/pg_ts_parser.h"
 #include "catalog/pg_ts_template.h"
+#include "catalog/pg_variable.h"
 #include "commands/alter.h"
 #include "commands/collationcmds.h"
 #include "commands/conversioncmds.h"
@@ -141,6 +142,10 @@ report_namespace_conflict(Oid classId, const char *name, Oid nspOid)
 			Assert(OidIsValid(nspOid));
 			msgfmt = gettext_noop("text search configuration \"%s\" already exists in schema \"%s\"");
 			break;
+		case VariableRelationId:
+			Assert(OidIsValid(nspOid));
+			msgfmt = gettext_noop("schema variable \"%s\" already exists in schema \"%s\"");
+			break;
 		default:
 			elog(ERROR, "unsupported object class %u", classId);
 			break;
@@ -393,6 +398,7 @@ ExecRenameStmt(RenameStmt *stmt)
 		case OBJECT_TSTEMPLATE:
 		case OBJECT_PUBLICATION:
 		case OBJECT_SUBSCRIPTION:
+		case OBJECT_VARIABLE:
 			{
 				ObjectAddress address;
 				Relation	catalog;
@@ -536,6 +542,7 @@ ExecAlterObjectSchemaStmt(AlterObjectSchemaStmt *stmt,
 		case OBJECT_TSDICTIONARY:
 		case OBJECT_TSPARSER:
 		case OBJECT_TSTEMPLATE:
+		case OBJECT_VARIABLE:
 			{
 				Relation	catalog;
 				Relation	relation;
@@ -626,6 +633,7 @@ AlterObjectNamespace_oid(Oid classId, Oid objid, Oid nspOid,
 		case OCLASS_TSDICT:
 		case OCLASS_TSTEMPLATE:
 		case OCLASS_TSCONFIG:
+		case OCLASS_VARIABLE:
 			{
 				Relation	catalog;
 
@@ -883,6 +891,7 @@ ExecAlterOwnerStmt(AlterOwnerStmt *stmt)
 		case OBJECT_TABLESPACE:
 		case OBJECT_TSDICTIONARY:
 		case OBJECT_TSCONFIGURATION:
+		case OBJECT_VARIABLE:
 			{
 				Relation	catalog;
 				Relation	relation;
diff --git a/src/backend/commands/discard.c b/src/backend/commands/discard.c
index 57d3d7dd9b..a712bf44b6 100644
--- a/src/backend/commands/discard.c
+++ b/src/backend/commands/discard.c
@@ -19,6 +19,7 @@
 #include "commands/discard.h"
 #include "commands/prepare.h"
 #include "commands/sequence.h"
+#include "commands/schema_variable.h"
 #include "utils/guc.h"
 #include "utils/portal.h"
 
@@ -48,6 +49,10 @@ DiscardCommand(DiscardStmt *stmt, bool isTopLevel)
 			ResetTempTableNamespace();
 			break;
 
+		case DISCARD_VARIABLES:
+			ResetSchemaVariableCache();
+			break;
+
 		default:
 			elog(ERROR, "unrecognized DISCARD target: %d", stmt->target);
 	}
@@ -75,4 +80,5 @@ DiscardAll(bool isTopLevel)
 	ResetPlanCache();
 	ResetTempTableNamespace();
 	ResetSequenceCaches();
+	ResetSchemaVariableCache();
 }
diff --git a/src/backend/commands/dropcmds.c b/src/backend/commands/dropcmds.c
index 97e5e9a765..b925e0dbdc 100644
--- a/src/backend/commands/dropcmds.c
+++ b/src/backend/commands/dropcmds.c
@@ -481,6 +481,10 @@ does_not_exist_skipping(ObjectType objtype, Node *object)
 			msg = gettext_noop("publication \"%s\" does not exist, skipping");
 			name = strVal((Value *) object);
 			break;
+		case OBJECT_VARIABLE:
+			msg = gettext_noop("schema variable \"%s\" does not exist, skipping");
+			name = NameListToString(castNode(List, object));
+			break;
 		default:
 			elog(ERROR, "unrecognized object type: %d", (int) objtype);
 			break;
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index 5bde507c75..aa330b20b8 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -990,6 +990,7 @@ EventTriggerSupportsObjectType(ObjectType obtype)
 		case OBJECT_TSTEMPLATE:
 		case OBJECT_TYPE:
 		case OBJECT_USER_MAPPING:
+		case OBJECT_VARIABLE:
 		case OBJECT_VIEW:
 			return true;
 
@@ -1053,6 +1054,7 @@ EventTriggerSupportsObjectClass(ObjectClass objclass)
 		case OCLASS_PUBLICATION_REL:
 		case OCLASS_SUBSCRIPTION:
 		case OCLASS_TRANSFORM:
+		case OCLASS_VARIABLE:
 			return true;
 
 			/*
@@ -2097,6 +2099,8 @@ stringify_grant_objtype(ObjectType objtype)
 			return "TABLESPACE";
 		case OBJECT_TYPE:
 			return "TYPE";
+		case OBJECT_VARIABLE:
+			return "VARIABLE";
 			/* these currently aren't used */
 		case OBJECT_ACCESS_METHOD:
 		case OBJECT_AGGREGATE:
@@ -2179,6 +2183,8 @@ stringify_adefprivs_objtype(ObjectType objtype)
 			return "TABLESPACES";
 		case OBJECT_TYPE:
 			return "TYPES";
+		case OBJECT_VARIABLE:
+			return "VARIABLES";
 			/* these currently aren't used */
 		case OBJECT_ACCESS_METHOD:
 		case OBJECT_AGGREGATE:
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index b62a76e7e5..28bbb0f323 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -498,6 +498,22 @@ ExplainOneUtility(Node *utilityStmt, IntoClause *into, ExplainState *es,
 		else
 			ExplainDummyGroup("Notify", NULL, es);
 	}
+	else if (IsA(utilityStmt, LetStmt))
+	{
+		LetStmt	   *letstmt = (LetStmt *) utilityStmt;
+		List	   *rewritten;
+
+		if (es->format == EXPLAIN_FORMAT_TEXT)
+			appendStringInfoString(es->str, "SET SCHEMA VARIABLE\n");
+		else
+			ExplainDummyGroup("Set Schema Variable", NULL, es);
+
+		rewritten = QueryRewrite(castNode(Query, copyObject(letstmt->query)));
+		Assert(list_length(rewritten) == 1);
+		ExplainOneQuery(linitial_node(Query, rewritten),
+						CURSOR_OPT_PARALLEL_OK, NULL, es,
+						queryString, params, queryEnv);
+	}
 	else
 	{
 		if (es->format == EXPLAIN_FORMAT_TEXT)
diff --git a/src/backend/commands/schemavariable.c b/src/backend/commands/schemavariable.c
new file mode 100644
index 0000000000..5582d7748d
--- /dev/null
+++ b/src/backend/commands/schemavariable.c
@@ -0,0 +1,876 @@
+/*-------------------------------------------------------------------------
+ *
+ * schemavariable.c
+ *	  schema variable creation/manipulation commands
+ *
+ * Portions Copyright (c) 1996-2020, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/commands/schemavariable.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+#include "miscadmin.h"
+#include "access/heapam.h"
+#include "access/htup_details.h"
+#include "access/xact.h"
+#include "catalog/dependency.h"
+#include "catalog/indexing.h"
+#include "catalog/namespace.h"
+#include "catalog/pg_variable.h"
+#include "commands/schema_variable.h"
+#include "executor/executor.h"
+#include "executor/svariableReceiver.h"
+#include "optimizer/optimizer.h"
+#include "parser/parse_coerce.h"
+#include "parser/parse_collate.h"
+#include "parser/parse_expr.h"
+#include "parser/parse_type.h"
+#include "rewrite/rewriteHandler.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"
+
+/*
+ * ON COMMIT action list
+ */
+typedef struct OnCommitItem
+{
+	Oid			varid;			/* relid of relation */
+	VariableEOXAction eoxaction;	/* what to do at end of xact */
+
+	/*
+	 * If this entry was created during the current transaction,
+	 * creating_subid is the ID of the creating subxact; if created in a prior
+	 * transaction, creating_subid is zero.  If deleted during the current
+	 * transaction, deleting_subid is the ID of the deleting subxact; if no
+	 * deletion request is pending, deleting_subid is zero.
+	 */
+	SubTransactionId creating_subid;
+	SubTransactionId deleting_subid;
+} OnCommitItem;
+
+static List *on_commits = NIL;
+
+/*
+ * The content of variables is not transactional. Due this fact the
+ * implementation of DROP can be simple, because although DROP VARIABLE
+ * can be reverted, the content of variable can be lost. In this example,
+ * DROP VARIABLE is same like reset variable.
+ */
+
+typedef struct SchemaVariableData
+{
+	Oid			varid;			/* pg_variable OID of this sequence (hash key) */
+	Oid			typid;			/* OID of the data type */
+	int16		typlen;
+	bool		typbyval;
+	bool		isnull;
+	bool		freeval;
+	Datum		value;
+	bool		is_rowtype;		/* true when variable is composite */
+	bool		is_valid;		/* true when variable was successfuly
+								 * initialized */
+	bool		reset_auto;		/* next read fix invalidate value by self */
+
+	bool		is_not_null;	/* don't allow null values */
+	bool		has_defexpr;	/* true when there are default value */
+	bool		is_immutable;	/* true when variable is immutable */
+
+	SubTransactionId creating_subid;
+}			SchemaVariableData;
+
+typedef SchemaVariableData * SchemaVariable;
+
+static HTAB *schemavarhashtab = NULL;	/* hash table for session variables */
+static MemoryContext SchemaVariableMemoryContext = NULL;
+
+static bool first_time = true;
+static bool clean_cache_req = false;
+
+static void create_schema_variable_hashtable(void);
+static void clean_cache_callback(XactEvent event, void *arg);
+static void free_schema_variable(SchemaVariable svar, bool force);
+static void set_schema_variable(SchemaVariable svar, Datum value, bool isnull, Oid typid);
+static void init_schema_variable(SchemaVariable svar, Variable *var);
+static SchemaVariable prepare_variable_for_reading(Oid varid, bool reset);
+static void remove_variable_on_commit_actions(Oid varid, VariableEOXAction eoxaction);
+static void clean_cache_variable(Oid varid);
+
+/*
+ * Save info about necessity to clean hash table, because some
+ * schema variable was dropped. Don't do here more, recheck
+ * needs to be in transaction state.
+ */
+static void
+InvalidateSchemaVariableCacheCallback(Datum arg, int cacheid, uint32 hashvalue)
+{
+	if (cacheid != VARIABLEOID)
+		return;
+
+	clean_cache_req = true;
+}
+
+/*
+ * Recheck values of all schema variables stored in local memory.
+ * Remove the value from local memory, when related schema variable
+ * was dropped (and removed from system catalogue).
+ */
+static void
+clean_cache_callback(XactEvent event, void *arg)
+{
+	/*
+	 * should continue only in transaction time, when syscache is available.
+	 */
+	if (clean_cache_req && IsTransactionState())
+	{
+		HASH_SEQ_STATUS status;
+		SchemaVariable svar;
+
+		if (!schemavarhashtab)
+			return;
+
+		hash_seq_init(&status, schemavarhashtab);
+
+		while ((svar = (SchemaVariable) hash_seq_search(&status)) != NULL)
+		{
+			HeapTuple	tp;
+
+			tp = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(svar->varid));
+			if (!HeapTupleIsValid(tp))
+			{
+				elog(DEBUG1, "variable %d is removed from cache", svar->varid);
+
+				free_schema_variable(svar, true);
+
+				remove_variable_on_commit_actions(svar->varid, VARIABLE_EOX_DROP);
+
+				(void) hash_search(schemavarhashtab, (void *) &svar->varid,
+								   HASH_REMOVE, NULL);
+			}
+			else
+				ReleaseSysCache(tp);
+		}
+
+		clean_cache_req = false;
+	}
+}
+
+/*
+ * Create the hash table for storing schema variables
+ */
+static void
+create_schema_variable_hashtable(void)
+{
+	HASHCTL		ctl;
+
+	/* set callbacks */
+	if (first_time)
+	{
+		CacheRegisterSyscacheCallback(VARIABLEOID,
+									  InvalidateSchemaVariableCacheCallback,
+									  (Datum) 0);
+
+		RegisterXactCallback(clean_cache_callback, NULL);
+
+		first_time = false;
+	}
+
+	/* needs own long life memory context */
+	if (SchemaVariableMemoryContext == NULL)
+	{
+		SchemaVariableMemoryContext =
+			AllocSetContextCreate(TopMemoryContext,
+								  "schema variables",
+								  ALLOCSET_START_SMALL_SIZES);
+	}
+
+	memset(&ctl, 0, sizeof(ctl));
+	ctl.keysize = sizeof(Oid);
+	ctl.entrysize = sizeof(SchemaVariableData);
+	ctl.hcxt = SchemaVariableMemoryContext;
+
+	schemavarhashtab = hash_create("Schema variables", 64, &ctl,
+								   HASH_ELEM | HASH_BLOBS | HASH_CONTEXT);
+}
+
+/*
+ * Fast drop complete content of schema variables
+ */
+void
+ResetSchemaVariableCache(void)
+{
+	/* Remove temporal schema variables */
+	AtPreEOXact_SchemaVariable_on_commit_actions(true);
+
+	/* Destroy hash table and reset related memory context */
+	if (schemavarhashtab)
+	{
+		hash_destroy(schemavarhashtab);
+		schemavarhashtab = NULL;
+	}
+
+	/* Release memory allocated by schema variables */
+	if (SchemaVariableMemoryContext != NULL)
+		MemoryContextReset(SchemaVariableMemoryContext);
+}
+
+/*
+ * Release data stored inside svar. When a variable is transactional,
+ * and was not modified already in this transaction, then it archives
+ * current value for possible future usage on rollback.
+ *
+ * When force is true, then release current and possibly archived value.
+ */
+static void
+free_schema_variable(SchemaVariable svar, bool force)
+{
+	if (svar->freeval)
+		pfree(DatumGetPointer(svar->value));
+
+	/* Clean current value, and mark it as invalid */
+	svar->value = (Datum) 0;
+	svar->isnull = true;
+	svar->freeval = false;
+
+	svar->is_valid = false;
+}
+
+/*
+ * Assign some content to the schema variable. It does copy to
+ * SchemaVariableMemoryContext if it is necessary.
+ */
+static void
+set_schema_variable(SchemaVariable svar, Datum value, bool isnull, Oid typid)
+{
+	MemoryContext oldcxt;
+	Datum		newval = value;
+
+	/* Don't allow assign null to NOT NULL variable */
+	if (isnull && svar->is_not_null)
+		ereport(ERROR,
+				(errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
+				 errmsg("null value is not allowed for NOT NULL schema variable \"%s\"",
+						schema_variable_get_name(svar->varid))));
+
+	if (!isnull && svar->typid != typid)
+		ereport(ERROR,
+				(errcode(ERRCODE_DATATYPE_MISMATCH),
+				 errmsg("type \"%s\" of assigned value is different than type \"%s\" of schema variable \"%s\"",
+						format_type_be(typid),
+						format_type_be(svar->typid),
+						schema_variable_get_name(svar->varid))));
+
+	/* copy value to session persistent context */
+	oldcxt = MemoryContextSwitchTo(SchemaVariableMemoryContext);
+	if (!isnull)
+		newval = datumCopy(value, svar->typbyval, svar->typlen);
+	MemoryContextSwitchTo(oldcxt);
+
+	free_schema_variable(svar, false);
+
+	svar->value = newval;
+	svar->isnull = isnull;
+	svar->freeval = newval != value;
+	svar->is_valid = true;
+	svar->reset_auto = false;
+}
+
+/*
+ * Initialize svar from var
+ * svar - SchemaVariable - holds data
+ * var  - Variable - holds metadata
+ */
+static void
+init_schema_variable(SchemaVariable svar, Variable *var)
+{
+	Assert(OidIsValid(var->oid));
+
+	svar->varid = var->oid;
+	svar->typid = var->typid;
+
+	get_typlenbyval(var->typid,
+					&svar->typlen,
+					&svar->typbyval);
+
+	svar->isnull = true;
+	svar->freeval = false;
+	svar->value = (Datum) 0;
+
+	svar->is_rowtype = type_is_rowtype(var->typid);
+	svar->is_not_null = var->is_not_null;
+	svar->is_immutable = var->is_immutable;
+	svar->has_defexpr = var->has_defexpr;
+
+	/* the variable initialization was not complete here */
+	svar->is_valid = false;
+	svar->reset_auto = false;
+
+	svar->creating_subid = InvalidSubTransactionId;
+}
+
+/*
+ * Try to search value in hash table. If doesn't
+ * exists insert it (and calculate defexpr if exists.
+ * When reset is true, we would to enforce calculate
+ * defexpr. When some is wrong, then this function try
+ * don't break previous value.
+ */
+static SchemaVariable
+prepare_variable_for_reading(Oid varid, bool reset)
+{
+	SchemaVariable svar;
+	Variable	var;
+	bool		found;
+
+	var.oid = InvalidOid;
+
+	if (schemavarhashtab == NULL)
+		create_schema_variable_hashtable();
+
+	svar = (SchemaVariable) hash_search(schemavarhashtab, &varid,
+										HASH_ENTER, &found);
+
+	/* Return content if it is available, and reset is not required */
+	if (found && !reset && !svar->reset_auto)
+	{
+		/* raise exception when content is not valid */
+		if (!svar->is_valid)
+			ereport(ERROR,
+					(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+					 errmsg("variable \"%s\" has not valid content",
+							schema_variable_get_name(varid)),
+					 errhint("Overwrite the content of variable or assign DEFAULT again.")));
+
+		return svar;
+	}
+
+	/* We need to load defexpr. */
+	initVariable(&var, varid, false, false);
+
+	if (!found)
+		init_schema_variable(svar, &var);
+
+	/* Raise a error when we cannot to initialize variable correctly */
+	if (var.is_not_null && !var.defexpr)
+		ereport(ERROR,
+				(errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
+				 errmsg("null value is not allowed for NOT NULL schema variable \"%s\"",
+						schema_variable_get_name(varid)),
+				 errdetail("The schema variable was not initialized yet.")));
+
+	if (!found)
+		register_variable_on_commit_action(varid, var.eoxaction);
+
+	if (svar->has_defexpr)
+	{
+		Datum		value = (Datum) 0;
+		bool		isnull;
+		EState	   *estate = NULL;
+		Expr	   *defexpr;
+		ExprState  *defexprs;
+		MemoryContext oldcxt;
+
+		/* Prepare default expr */
+		estate = CreateExecutorState();
+
+		oldcxt = MemoryContextSwitchTo(estate->es_query_cxt);
+
+		defexpr = expression_planner((Expr *) var.defexpr);
+		defexprs = ExecInitExpr(defexpr, NULL);
+		value = ExecEvalExprSwitchContext(defexprs,
+										  GetPerTupleExprContext(estate),
+										  &isnull);
+
+
+		/* Store result before releasing Executor memory */
+		set_schema_variable(svar, value, isnull, svar->typid);
+
+		MemoryContextSwitchTo(oldcxt);
+
+		FreeExecutorState(estate);
+	}
+	else
+		set_schema_variable(svar, (Datum) 0, true, svar->typid);
+
+	return svar;
+}
+
+/*
+ * Write value to variable. We expect secured access in this moment.
+ * We try to don't break previous value, if some is wrong.
+ */
+void
+SetSchemaVariable(Oid varid, Datum value, bool isNull, Oid typid)
+{
+	SchemaVariable svar;
+	bool		found;
+
+	if (schemavarhashtab == NULL)
+		create_schema_variable_hashtable();
+
+	svar = (SchemaVariable) hash_search(schemavarhashtab, &varid,
+										HASH_ENTER, &found);
+
+	/* Initialize svar when was not initialized or when stored value is null */
+	if (!found)
+	{
+		Variable	var;
+
+		/* don't need defexpr and acl here */
+		initVariable(&var, varid, false, true);
+		init_schema_variable(svar, &var);
+		register_variable_on_commit_action(varid, var.eoxaction);
+	}
+
+	/* don't allow a update on immutable variable */
+	if (svar->is_immutable)
+		ereport(ERROR,
+				(errcode(ERRCODE_ERROR_IN_ASSIGNMENT),
+				 errmsg("schema variable \"%s\" is declared IMMUTABLE",
+						schema_variable_get_name(svar->varid))));
+
+	set_schema_variable(svar, value, isNull, typid);
+}
+
+
+/*
+ * Write value to variable with security check.
+ * We try to don't break previous value, if some is wrong.
+ */
+void
+SetSchemaVariableWithSecurityCheck(Oid varid, Datum value, bool isNull, Oid typid)
+{
+	AclResult	aclresult;
+
+	/*
+	 * Is possible to write to schema variable?
+	 */
+	aclresult = pg_variable_aclcheck(varid, GetUserId(), ACL_WRITE);
+	if (aclresult != ACLCHECK_OK)
+		aclcheck_error(aclresult, OBJECT_VARIABLE, schema_variable_get_name(varid));
+
+	SetSchemaVariable(varid, value, isNull, typid);
+}
+
+/*
+ * Returns copy of value of schema variable spcified by varid
+ */
+Datum
+CopySchemaVariable(Oid varid, bool *isNull, Oid *typid)
+{
+	SchemaVariable svar;
+
+	svar = prepare_variable_for_reading(varid, false);
+	Assert(svar != NULL && svar->is_valid);
+
+	*isNull = svar->isnull;
+	*typid = svar->typid;
+
+	if (!svar->isnull)
+		return datumCopy(svar->value, svar->typbyval, svar->typlen);
+
+	return (Datum) 0;
+}
+
+/*
+ * Returns value of schema variable specified by varid. Check correct
+ * result type. Optionaly the result can be copy.
+ */
+Datum
+GetSchemaVariable(Oid varid, bool *isNull, Oid expected_typid, bool copy)
+{
+	SchemaVariable svar;
+	Datum		value;
+	bool		isnull;
+
+	svar = prepare_variable_for_reading(varid, false);
+	Assert(svar != NULL);
+
+	if (expected_typid != svar->typid)
+		elog(ERROR, "type of variable \"%s\" is different than expected",
+			 schema_variable_get_name(varid));
+
+	value = svar->value;
+	isnull = svar->isnull;
+
+	*isNull = isnull;
+
+	if (!isnull && copy)
+		return datumCopy(value, svar->typbyval, svar->typlen);
+
+	return value;
+}
+
+/*
+ * Clean variable defined by varid
+ */
+static void
+clean_cache_variable(Oid varid)
+{
+	SchemaVariable svar;
+	bool		found;
+
+	if (schemavarhashtab == NULL)
+		return;
+
+	svar = (SchemaVariable) hash_search(schemavarhashtab, &varid,
+										HASH_FIND, &found);
+	if (found)
+	{
+		/* clean content, if it is necessary */
+		free_schema_variable(svar, true);
+
+		if (hash_search(schemavarhashtab,
+						(void *) &svar->varid,
+						HASH_REMOVE,
+						NULL) == NULL)
+			elog(DEBUG1, "hash table corrupted");
+	}
+}
+
+/*
+ * Drop variable by OID
+ */
+void
+RemoveSchemaVariable(Oid varid)
+{
+	Relation	rel;
+	HeapTuple	tup;
+
+	rel = table_open(VariableRelationId, RowExclusiveLock);
+
+	tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid));
+
+	if (!HeapTupleIsValid(tup))
+		elog(ERROR, "cache lookup failed for variable %u", varid);
+
+	CatalogTupleDelete(rel, &tup->t_self);
+
+	ReleaseSysCache(tup);
+
+	table_close(rel, RowExclusiveLock);
+
+	clean_cache_variable(varid);
+
+	/* remove variable from on_commits list */
+	remove_variable_on_commit_actions(varid, VARIABLE_EOX_DROP);
+}
+
+/*
+ * Assign result of evaluated expression to schema 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 possible to write to schema variable?
+	 */
+	aclresult = pg_variable_aclcheck(varid, GetUserId(), ACL_WRITE);
+	if (aclresult != ACLCHECK_OK)
+		aclcheck_error(aclresult, OBJECT_VARIABLE, schema_variable_get_name(varid));
+
+	/* Create dest receiver for LET */
+	dest = CreateDestReceiver(DestVariable);
+	SetVariableDestReceiverParams(dest, varid);
+
+	/* run rewriter - there can be used for replacement of DEFAULT node */
+	query = copyObject(query);
+
+	rewritten = QueryRewrite(query);
+
+	Assert(list_length(rewritten) == 1);
+
+	query = linitial_node(Query, rewritten);
+	Assert(query->commandType == CMD_SELECT);
+
+	/* plan the query */
+	plan = pg_plan_query(query, pstate->p_sourcetext,
+						 CURSOR_OPT_PARALLEL_OK, params);
+
+	/*
+	 * Use a snapshot with an updated command ID to ensure this query sees
+	 * results of any previously executed queries.  (This could only
+	 * matter if the planner executed an allegedly-stable function that
+	 * changed the database contents, but let's do it anyway to be
+	 * parallel to the EXPLAIN code path.)
+	 */
+	PushCopiedSnapshot(GetActiveSnapshot());
+	UpdateActiveSnapshotCommandId();
+
+	/* Create a QueryDesc, redirecting output to our tuple receiver */
+	queryDesc = CreateQueryDesc(plan, pstate->p_sourcetext,
+								GetActiveSnapshot(), InvalidSnapshot,
+								dest, params, queryEnv, 0);
+
+	/* call ExecutorStart to prepare the plan for execution */
+	ExecutorStart(queryDesc, 0);
+
+	/* run the plan to completion */
+	ExecutorRun(queryDesc, ForwardScanDirection, 2L, true);
+
+	/* save the rowcount if we're given a qc to fill */
+	if (qc)
+		SetQueryCompletion(qc, CMDTAG_LET, queryDesc->estate->es_processed);
+
+	/* and clean up */
+	ExecutorFinish(queryDesc);
+	ExecutorEnd(queryDesc);
+
+	FreeQueryDesc(queryDesc);
+
+	PopActiveSnapshot();
+}
+
+/*
+ * Creates new variable - entry in pg_catalog.pg_variable table
+ *
+ * Used by CREATE VARIABLE command
+ */
+ObjectAddress
+DefineSchemaVariable(ParseState *pstate, CreateSchemaVarStmt * stmt)
+{
+	Oid			namespaceid;
+	AclResult	aclresult;
+	Oid			typid;
+	int32		typmod;
+	Oid			varowner = GetUserId();
+	Oid			collation;
+	Oid			typcollation;
+	ObjectAddress variable;
+
+	Node	   *cooked_default = NULL;
+
+	/*
+	 * Check consistency of arguments
+	 */
+	if (stmt->eoxaction == VARIABLE_EOX_DROP
+		&& stmt->variable->relpersistence != RELPERSISTENCE_TEMP)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+				 errmsg("ON COMMIT DROP can only be used on temporary variables")));
+
+	namespaceid =
+		RangeVarGetAndCheckCreationNamespace(stmt->variable, NoLock, NULL);
+
+	typenameTypeIdAndMod(pstate, stmt->typeName, &typid, &typmod);
+	typcollation = get_typcollation(typid);
+
+	aclresult = pg_type_aclcheck(typid, GetUserId(), ACL_USAGE);
+	if (aclresult != ACLCHECK_OK)
+		aclcheck_error_type(aclresult, typid);
+
+	if (stmt->collClause)
+		collation = LookupCollation(pstate,
+									stmt->collClause->collname,
+									stmt->collClause->location);
+	else
+		collation = typcollation;;
+
+	/* Complain if COLLATE is applied to an uncollatable type */
+	if (OidIsValid(collation) && !OidIsValid(typcollation))
+		ereport(ERROR,
+				(errcode(ERRCODE_DATATYPE_MISMATCH),
+				 errmsg("collations are not supported by type %s",
+						format_type_be(typid)),
+				 parser_errposition(pstate, stmt->collClause->location)));
+
+	if (stmt->defexpr)
+	{
+		cooked_default = transformExpr(pstate, stmt->defexpr,
+									   EXPR_KIND_VARIABLE_DEFAULT);
+
+		cooked_default = coerce_to_specific_type(pstate,
+												 cooked_default, typid, "DEFAULT");
+		assign_expr_collations(pstate, cooked_default);
+	}
+
+	variable = VariableCreate(stmt->variable->relname,
+							  namespaceid,
+							  typid,
+							  typmod,
+							  varowner,
+							  collation,
+							  cooked_default,
+							  stmt->eoxaction,
+							  stmt->is_not_null,
+							  stmt->if_not_exists,
+							  stmt->is_immutable);
+
+	/*
+	 * We must bump the command counter to make the newly-created variable
+	 * tuple visible for any other operations.
+	 */
+	CommandCounterIncrement();
+
+	return variable;
+}
+
+/*
+ * Register a newly-created variable ON COMMIT action.
+ */
+void
+register_variable_on_commit_action(Oid varid,
+								   VariableEOXAction action)
+{
+	OnCommitItem *oc;
+	MemoryContext oldcxt;
+
+	/*
+	 * We needn't bother registering the relation unless there is an ON COMMIT
+	 * action we need to take.
+	 */
+	if (action == VARIABLE_EOX_NOOP)
+		return;
+
+	oldcxt = MemoryContextSwitchTo(CacheMemoryContext);
+
+	oc = (OnCommitItem *) palloc(sizeof(OnCommitItem));
+	oc->varid = varid;
+	oc->eoxaction = action;
+
+	oc->creating_subid = GetCurrentSubTransactionId();
+	oc->deleting_subid = InvalidSubTransactionId;
+
+	on_commits = lcons(oc, on_commits);
+
+	MemoryContextSwitchTo(oldcxt);
+}
+
+/*
+ * Remove variable from on_commits action
+ */
+static void
+remove_variable_on_commit_actions(Oid varid,
+								  VariableEOXAction eoxaction)
+{
+	ListCell   *l;
+
+	foreach(l, on_commits)
+	{
+		OnCommitItem *oc = (OnCommitItem *) lfirst(l);
+
+		if (oc->varid == varid)
+		{
+			if (eoxaction == VARIABLE_EOX_DROP ||
+				(oc->eoxaction == VARIABLE_EOX_RESET &&
+				 eoxaction == VARIABLE_EOX_RESET))
+				oc->deleting_subid = GetCurrentSubTransactionId();
+		}
+	}
+}
+
+/*
+ * Perform ON TRANSACTION END RESET, ON COMMIT DROP
+ * and COMMIT/ROLLBACK of transaction schema variables.
+ */
+void
+AtPreEOXact_SchemaVariable_on_commit_actions(bool isCommit)
+{
+	ListCell   *l;
+
+	foreach(l, on_commits)
+	{
+		OnCommitItem *oc = (OnCommitItem *) lfirst(l);
+
+		/* Ignore entry if already dropped in this xact */
+		if (oc->deleting_subid != InvalidSubTransactionId)
+			continue;
+
+		switch (oc->eoxaction)
+		{
+			case VARIABLE_EOX_NOOP:
+				/* Do nothing */
+				break;
+			case VARIABLE_EOX_RESET:
+				clean_cache_variable(oc->varid);
+				remove_variable_on_commit_actions(oc->varid,
+												  VARIABLE_EOX_RESET);
+				break;
+			case VARIABLE_EOX_DROP:
+				{
+					/*
+					 * ON COMMIT DROP is allowed only for temp schema
+					 * variables. So we should explicit delete only when
+					 * current transaction was committed. When is rollback,
+					 * then schema variable is removed automatically.
+					 */
+					if (isCommit)
+					{
+						ObjectAddress object;
+
+						object.classId = VariableRelationId;
+						object.objectId = oc->varid;
+						object.objectSubId = 0;
+
+						/*
+						 * Since this is an automatic drop, rather than one
+						 * directly initiated by the user, we pass the
+						 * PERFORM_DELETION_INTERNAL flag.
+						 */
+						performDeletion(&object,
+										DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+					}
+				}
+				break;
+		}
+	}
+}
+
+/*
+ * Post-commit or post-abort cleanup for ON COMMIT management.
+ *
+ * All we do here is remove no-longer-needed OnCommitItem entries.
+ *
+ */
+void
+AtEOXact_SchemaVariable_on_commit_actions(bool isCommit)
+{
+	ListCell   *cur_item;
+
+	foreach(cur_item, on_commits)
+	{
+		OnCommitItem *oc = (OnCommitItem *) lfirst(cur_item);
+
+		/*
+		 * During commit, remove entries that were deleted during this
+		 * transaction; during abort, remove those created during this
+		 * transaction.
+		 *
+		 * The transact variable event is removed every time.
+		 */
+
+		if ((isCommit ? oc->deleting_subid != InvalidSubTransactionId :
+			 oc->creating_subid != InvalidSubTransactionId))
+		{
+			on_commits = foreach_delete_current(on_commits, cur_item);
+			pfree(oc);
+		}
+		else
+		{
+			oc->creating_subid = InvalidSubTransactionId;
+			oc->deleting_subid = InvalidSubTransactionId;
+		}
+	}
+}
diff --git a/src/backend/commands/seclabel.c b/src/backend/commands/seclabel.c
index 6906714298..0f2df23116 100644
--- a/src/backend/commands/seclabel.c
+++ b/src/backend/commands/seclabel.c
@@ -90,6 +90,7 @@ SecLabelSupportsObjectType(ObjectType objtype)
 		case OBJECT_TSPARSER:
 		case OBJECT_TSTEMPLATE:
 		case OBJECT_USER_MAPPING:
+		case OBJECT_VARIABLE:
 			return false;
 
 			/*
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 096a6f2891..0d4f2d5e2c 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -12067,6 +12067,7 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel,
 			case OCLASS_PUBLICATION_REL:
 			case OCLASS_SUBSCRIPTION:
 			case OCLASS_TRANSFORM:
+			case OCLASS_VARIABLE:
 
 				/*
 				 * We don't expect any of these sorts of objects to depend on
diff --git a/src/backend/executor/Makefile b/src/backend/executor/Makefile
index f08b282a5e..5ad3bb6caf 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 77c9d785d9..58232e5168 100644
--- a/src/backend/executor/execExpr.c
+++ b/src/backend/executor/execExpr.c
@@ -33,6 +33,7 @@
 #include "access/nbtree.h"
 #include "catalog/objectaccess.h"
 #include "catalog/pg_type.h"
+#include "commands/schema_variable.h"
 #include "executor/execExpr.h"
 #include "executor/nodeSubplan.h"
 #include "funcapi.h"
@@ -940,6 +941,60 @@ ExecInitExprRec(Expr *node, ExprState *state,
 						scratch.d.param.paramtype = param->paramtype;
 						ExprEvalPushStep(state, &scratch);
 						break;
+
+					case PARAM_VARIABLE:
+						{
+							int			es_num_schema_variables = 0;
+							SchemaVariableValue *es_schema_variables = NULL;
+
+							if (state->parent && state->parent->state)
+							{
+								es_schema_variables = state->parent->state->es_schema_variables;
+								es_num_schema_variables = state->parent->state->es_num_schema_variables;
+							}
+
+							/*
+							 * We should use schema variable buffer, when
+							 * it is available.
+							 */
+							if (es_schema_variables)
+							{
+								SchemaVariableValue *var;
+
+								/* check params, unexpected */
+								if (param->paramid >= es_num_schema_variables)
+									elog(ERROR, "paramid of PARAM_VARIABLE param is out of range");
+
+								var = &es_schema_variables[param->paramid];
+
+								/* unexpected */
+								if (var->typid != param->paramtype)
+									elog(ERROR, "type of buffered value is different than PARAM_VARIABLE type");
+
+								/*
+								 * In this case, the parameter is like a
+								 * constant
+								 */
+								scratch.opcode = EEOP_CONST;
+								scratch.d.constval.value = var->value;
+								scratch.d.constval.isnull = var->isnull;
+								ExprEvalPushStep(state, &scratch);
+							}
+							else
+							{
+								/*
+								 * When we have not a full PlanState (plpgsql
+								 * simple expr evaluation, then we should to
+								 * use direct access.
+								 */
+								scratch.opcode = EEOP_PARAM_VARIABLE;
+								scratch.d.vparam.varid = param->paramvarid;
+								scratch.d.vparam.vartype = param->paramtype;
+								ExprEvalPushStep(state, &scratch);
+							}
+						}
+						break;
+
 					case PARAM_EXTERN:
 
 						/*
diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c
index 094e22d392..5eae836a98 100644
--- a/src/backend/executor/execExprInterp.c
+++ b/src/backend/executor/execExprInterp.c
@@ -59,6 +59,7 @@
 #include "access/heaptoast.h"
 #include "catalog/pg_type.h"
 #include "commands/sequence.h"
+#include "commands/schema_variable.h"
 #include "executor/execExpr.h"
 #include "executor/nodeSubplan.h"
 #include "funcapi.h"
@@ -444,6 +445,7 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull)
 		&&CASE_EEOP_PARAM_EXEC,
 		&&CASE_EEOP_PARAM_EXTERN,
 		&&CASE_EEOP_PARAM_CALLBACK,
+		&&CASE_EEOP_PARAM_VARIABLE,
 		&&CASE_EEOP_CASE_TESTVAL,
 		&&CASE_EEOP_MAKE_READONLY,
 		&&CASE_EEOP_IOCOERCE,
@@ -1073,6 +1075,16 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull)
 			EEO_NEXT();
 		}
 
+		EEO_CASE(EEOP_PARAM_VARIABLE)
+		{
+			/* direct access to schema variable (without buffering) */
+			*op->resvalue = GetSchemaVariable(op->d.vparam.varid,
+											  op->resnull,
+											  op->d.vparam.vartype,
+											  true);
+			EEO_NEXT();
+		}
+
 		EEO_CASE(EEOP_CASE_TESTVAL)
 		{
 			/*
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index b2e2df8773..eba5daa81a 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -45,10 +45,13 @@
 #include "access/xact.h"
 #include "catalog/namespace.h"
 #include "catalog/pg_publication.h"
+#include "catalog/pg_variable.h"
 #include "commands/matview.h"
 #include "commands/trigger.h"
+#include "commands/schema_variable.h"
 #include "executor/execdebug.h"
 #include "executor/nodeSubplan.h"
+#include "executor/svariableReceiver.h"
 #include "foreign/fdwapi.h"
 #include "jit/jit.h"
 #include "mb/pg_wchar.h"
@@ -197,6 +200,52 @@ standard_ExecutorStart(QueryDesc *queryDesc, int eflags)
 
 	estate->es_sourceText = queryDesc->sourceText;
 
+	/*
+	 * Prepare schema variables, if are not prepared in queryDesc
+	 */
+	if (queryDesc->num_schema_variables > 0)
+	{
+		/*
+		 * link shared memory with working copy of schema variable's values
+		 * used in this query. This access is used by parallel query executor's
+		 * workers.
+		 */
+		estate->es_schema_variables = queryDesc->schema_variables;
+		estate->es_num_schema_variables = queryDesc->num_schema_variables;
+	}
+	else if (queryDesc->plannedstmt->schemaVariables)
+	{
+		ListCell   *lc;
+		int			nSchemaVariables;
+		int			i = 0;
+
+		nSchemaVariables = list_length(queryDesc->plannedstmt->schemaVariables);
+
+		/* Create buffer for used schema variables */
+		estate->es_schema_variables = (SchemaVariableValue *)
+			palloc(nSchemaVariables * sizeof(SchemaVariableValue));
+
+		foreach(lc, queryDesc->plannedstmt->schemaVariables)
+		{
+			AclResult	aclresult;
+			Oid			varid = lfirst_oid(lc);
+
+			aclresult = pg_variable_aclcheck(varid, GetUserId(), ACL_READ);
+			if (aclresult != ACLCHECK_OK)
+				aclcheck_error(aclresult, OBJECT_VARIABLE,
+							   schema_variable_get_name(varid));
+
+			estate->es_schema_variables[i].varid = varid;
+			estate->es_schema_variables[i].value = CopySchemaVariable(varid,
+																	  &estate->es_schema_variables[i].isnull,
+																	  &estate->es_schema_variables[i].typid);
+
+			i++;
+		}
+
+		estate->es_num_schema_variables = nSchemaVariables;
+	}
+
 	/*
 	 * Fill in the query environment, if any, from queryDesc.
 	 */
diff --git a/src/backend/executor/execParallel.c b/src/backend/executor/execParallel.c
index 4fca8782b2..1ec11a9ea3 100644
--- a/src/backend/executor/execParallel.c
+++ b/src/backend/executor/execParallel.c
@@ -12,8 +12,9 @@
  * workers and ensuring that their state generally matches that of the
  * leader; see src/backend/access/transam/README.parallel for details.
  * However, we must save and restore relevant executor state, such as
- * any ParamListInfo associated with the query, buffer/WAL usage info, and
- * the actual plan to be passed down to the worker.
+ * any ParamListInfo associated with the query, buffer/WAL usage info,
+ * used schema variables buffer, and the actual plan to be passed down
+ * to the worker.
  *
  * IDENTIFICATION
  *	  src/backend/executor/execParallel.c
@@ -66,6 +67,7 @@
 #define PARALLEL_KEY_QUERY_TEXT		UINT64CONST(0xE000000000000008)
 #define PARALLEL_KEY_JIT_INSTRUMENTATION UINT64CONST(0xE000000000000009)
 #define PARALLEL_KEY_WAL_USAGE			UINT64CONST(0xE00000000000000A)
+#define PARALLEL_KEY_SCHEMA_VARIABLES	UINT64CONST(0xE00000000000000B)
 
 #define PARALLEL_TUPLE_QUEUE_SIZE		65536
 
@@ -140,6 +142,12 @@ static bool ExecParallelRetrieveInstrumentation(PlanState *planstate,
 /* Helper function that runs in the parallel worker. */
 static DestReceiver *ExecParallelGetReceiver(dsm_segment *seg, shm_toc *toc);
 
+/* Helper functions that can pass values of used schema variables */
+static Size EstimateSchemaVariables(EState *estate);
+static void SerializeSchemaVariables(EState *estate, char **start_address);
+static SchemaVariableValue * RestoreSchemaVariables(char **start_address,
+													int *num_schema_variables);
+
 /*
  * Create a serialized representation of the plan to be sent to each worker.
  */
@@ -597,6 +605,7 @@ ExecInitParallelPlan(PlanState *planstate, EState *estate,
 	char	   *pstmt_data;
 	char	   *pstmt_space;
 	char	   *paramlistinfo_space;
+	char	   *schema_variables_space;
 	BufferUsage *bufusage_space;
 	WalUsage   *walusage_space;
 	SharedExecutorInstrumentation *instrumentation = NULL;
@@ -606,6 +615,7 @@ ExecInitParallelPlan(PlanState *planstate, EState *estate,
 	int			instrumentation_len = 0;
 	int			jit_instrumentation_len = 0;
 	int			instrument_offset = 0;
+	int			schema_variables_len = 0;
 	Size		dsa_minsize = dsa_minimum_size();
 	char	   *query_string;
 	int			query_len;
@@ -661,6 +671,11 @@ ExecInitParallelPlan(PlanState *planstate, EState *estate,
 	shm_toc_estimate_chunk(&pcxt->estimator, paramlistinfo_len);
 	shm_toc_estimate_keys(&pcxt->estimator, 1);
 
+	/* Estimate space for serialized schema variables. */
+	schema_variables_len = EstimateSchemaVariables(estate);
+	shm_toc_estimate_chunk(&pcxt->estimator, schema_variables_len);
+	shm_toc_estimate_keys(&pcxt->estimator, 1);
+
 	/*
 	 * Estimate space for BufferUsage.
 	 *
@@ -758,6 +773,11 @@ ExecInitParallelPlan(PlanState *planstate, EState *estate,
 	shm_toc_insert(pcxt->toc, PARALLEL_KEY_PARAMLISTINFO, paramlistinfo_space);
 	SerializeParamList(estate->es_param_list_info, &paramlistinfo_space);
 
+	/* Store serialized schema variables. */
+	schema_variables_space = shm_toc_allocate(pcxt->toc, schema_variables_len);
+	shm_toc_insert(pcxt->toc, PARALLEL_KEY_SCHEMA_VARIABLES, schema_variables_space);
+	SerializeSchemaVariables(estate, &schema_variables_space);
+
 	/* Allocate space for each worker's BufferUsage; no need to initialize. */
 	bufusage_space = shm_toc_allocate(pcxt->toc,
 									  mul_size(sizeof(BufferUsage), pcxt->nworkers));
@@ -1406,6 +1426,7 @@ ParallelQueryMain(dsm_segment *seg, shm_toc *toc)
 	SharedJitInstrumentation *jit_instrumentation;
 	int			instrument_options = 0;
 	void	   *area_space;
+	char	   *schemavariable_space;
 	dsa_area   *area;
 	ParallelWorkerContext pwcxt;
 
@@ -1431,6 +1452,14 @@ ParallelQueryMain(dsm_segment *seg, shm_toc *toc)
 	area_space = shm_toc_lookup(toc, PARALLEL_KEY_DSA, false);
 	area = dsa_attach_in_place(area_space, seg);
 
+	/* Reconstruct schema variables. */
+	schemavariable_space = shm_toc_lookup(toc,
+										  PARALLEL_KEY_SCHEMA_VARIABLES,
+										  false);
+	queryDesc->schema_variables =
+		RestoreSchemaVariables(&schemavariable_space,
+							   &queryDesc->num_schema_variables);
+
 	/* Start up the executor */
 	queryDesc->plannedstmt->jitFlags = fpes->jit_flags;
 	ExecutorStart(queryDesc, fpes->eflags);
@@ -1500,3 +1529,118 @@ ParallelQueryMain(dsm_segment *seg, shm_toc *toc)
 	FreeQueryDesc(queryDesc);
 	receiver->rDestroy(receiver);
 }
+
+/*
+ * Estimate the amount of space required to serialize a used
+ * schema variables.
+ */
+static Size
+EstimateSchemaVariables(EState *estate)
+{
+	int			i;
+	Size		sz = sizeof(int);
+
+	if (estate->es_schema_variables == NULL)
+		return sz;
+
+	for (i = 0; i < estate->es_num_schema_variables; i++)
+	{
+		SchemaVariableValue *svarval;
+		Oid			typeOid;
+		int16		typLen;
+		bool		typByVal;
+
+		svarval = &estate->es_schema_variables[i];
+
+		typeOid = svarval->typid;
+
+		sz = add_size(sz, sizeof(Oid)); /* space for type OID */
+
+		/* space for datum/isnull */
+		Assert(OidIsValid(typeOid));
+		get_typlenbyval(typeOid, &typLen, &typByVal);
+
+		sz = add_size(sz,
+					  datumEstimateSpace(svarval->value, svarval->isnull, typByVal, typLen));
+	}
+
+	return sz;
+}
+
+/*
+ * Serialize a schema variables buffer into caller-provided storage.
+ *
+ * We write the number of parameters first, as a 4-byte integer, and then
+ * write details for each parameter in turn.  The details for each parameter
+ * consist of a 4-byte type OID, and then the datum as serialized by
+ * datumSerialize().  The caller is responsible for ensuring that there is
+ * enough storage to store the number of bytes that will be written; use
+ * EstimateSchemaVariables to find out how many will be needed.
+ * *start_address is updated to point to the byte immediately following those
+ * written.
+ *
+ * RestoreSchemaVariables can be used to recreate a schema variable buffer
+ * based on the serialized representation;
+ */
+static void
+SerializeSchemaVariables(EState *estate, char **start_address)
+{
+	int			nparams;
+	int			i;
+
+	/* Write number of parameters. */
+	nparams = estate->es_num_schema_variables;
+	memcpy(*start_address, &nparams, sizeof(int));
+	*start_address += sizeof(int);
+
+	/* Write each parameter in turn. */
+	for (i = 0; i < nparams; i++)
+	{
+		SchemaVariableValue *svarval;
+		Oid			typeOid;
+		int16		typLen;
+		bool		typByVal;
+
+		svarval = &estate->es_schema_variables[i];
+		typeOid = svarval->typid;
+
+		/* Write type OID. */
+		memcpy(*start_address, &typeOid, sizeof(Oid));
+		*start_address += sizeof(Oid);
+
+		Assert(OidIsValid(typeOid));
+		get_typlenbyval(typeOid, &typLen, &typByVal);
+
+		datumSerialize(svarval->value, svarval->isnull, typByVal, typLen,
+					   start_address);
+	}
+}
+
+static SchemaVariableValue *
+RestoreSchemaVariables(char **start_address, int *num_schema_variables)
+{
+	SchemaVariableValue *schema_variables;
+	int			i;
+	int			nparams;
+
+	memcpy(&nparams, *start_address, sizeof(int));
+	*start_address += sizeof(int);
+
+	*num_schema_variables = nparams;
+	schema_variables = (SchemaVariableValue *)
+		palloc(nparams * sizeof(SchemaVariableValue));
+
+	for (i = 0; i < nparams; i++)
+	{
+		SchemaVariableValue *svarval = &schema_variables[i];
+
+		/* Read type OID. */
+		memcpy(&svarval->typid, *start_address, sizeof(Oid));
+		*start_address += sizeof(Oid);
+
+		/* Read datum/isnull. */
+		svarval->value = datumRestore(start_address, &svarval->isnull);
+	}
+
+	return schema_variables;
+}
diff --git a/src/backend/executor/spi.c b/src/backend/executor/spi.c
index 00aa78ea53..87c4de696a 100644
--- a/src/backend/executor/spi.c
+++ b/src/backend/executor/spi.c
@@ -2788,6 +2788,9 @@ _SPI_error_callback(void *arg)
 			case RAW_PARSE_PLPGSQL_ASSIGN3:
 				errcontext("PL/pgSQL assignment \"%s\"", query);
 				break;
+			case RAW_PARSE_PLPGSQL_LET:
+				errcontext("LET statement \"%s\"", query);
+				break;
 			default:
 				errcontext("SQL statement \"%s\"", query);
 				break;
diff --git a/src/backend/executor/svariableReceiver.c b/src/backend/executor/svariableReceiver.c
new file mode 100644
index 0000000000..298330c1cf
--- /dev/null
+++ b/src/backend/executor/svariableReceiver.c
@@ -0,0 +1,145 @@
+/*-------------------------------------------------------------------------
+ *
+ * svariableReceiver.c
+ *	  An implementation of DestReceiver that stores the result value in
+ *	  a schema variable.
+ *
+ * Portions Copyright (c) 1996-2020, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *	  src/backend/executor/svariableReceiver.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/detoast.h"
+#include "executor/svariableReceiver.h"
+#include "commands/schema_variable.h"
+
+typedef struct
+{
+	DestReceiver pub;
+	Oid			varid;
+	Oid			typid;
+	int32		typmod;
+	int			typlen;
+	int			slot_offset;
+	int			rows;
+}			svariableState;
+
+
+/*
+ * Prepare to receive tuples from executor.
+ */
+static void
+svariableStartupReceiver(DestReceiver *self, int operation, TupleDesc typeinfo)
+{
+	svariableState *myState = (svariableState *) self;
+	int			natts = typeinfo->natts;
+	int			outcols = 0;
+	int			i;
+
+	for (i = 0; i < natts; i++)
+	{
+		Form_pg_attribute attr = TupleDescAttr(typeinfo, i);
+
+		if (attr->attisdropped)
+			continue;
+
+		if (++outcols > 1)
+			elog(ERROR, "svariable DestReceiver can take only one attribute");
+
+		myState->typid = attr->atttypid;
+		myState->typmod = attr->atttypmod;
+		myState->typlen = attr->attlen;
+		myState->slot_offset = i;
+	}
+
+	myState->rows = 0;
+}
+
+/*
+ * Receive a tuple from the executor and store it in schema variable.
+ */
+static bool
+svariableReceiveSlot(TupleTableSlot *slot, DestReceiver *self)
+{
+	svariableState *myState = (svariableState *) self;
+	Datum		value;
+	bool		isnull;
+	bool		freeval = false;
+
+	/* Make sure the tuple is fully deconstructed */
+	slot_getallattrs(slot);
+
+	value = slot->tts_values[myState->slot_offset];
+	isnull = slot->tts_isnull[myState->slot_offset];
+
+	if (myState->typlen == -1 && !isnull && VARATT_IS_EXTERNAL(DatumGetPointer(value)))
+	{
+		value = PointerGetDatum(detoast_external_attr((struct varlena *)
+													  DatumGetPointer(value)));
+		freeval = true;
+	}
+
+	SetSchemaVariable(myState->varid, value, isnull, myState->typid);
+
+	if (freeval)
+		pfree(DatumGetPointer(value));
+
+	return true;
+}
+
+/*
+ * Clean up at end of an executor run
+ */
+static void
+svariableShutdownReceiver(DestReceiver *self)
+{
+	/* Do nothing */
+}
+
+/*
+ * Destroy receiver when done with it
+ */
+static void
+svariableDestroyReceiver(DestReceiver *self)
+{
+	pfree(self);
+}
+
+/*
+ * Initially create a DestReceiver object.
+ */
+DestReceiver *
+CreateVariableDestReceiver(void)
+{
+	svariableState *self = (svariableState *) palloc0(sizeof(svariableState));
+
+	self->pub.receiveSlot = svariableReceiveSlot;
+	self->pub.rStartup = svariableStartupReceiver;
+	self->pub.rShutdown = svariableShutdownReceiver;
+	self->pub.rDestroy = svariableDestroyReceiver;
+	self->pub.mydest = DestVariable;
+
+	/* private fields will be set by SetVariableDestReceiverParams */
+
+	return (DestReceiver *) self;
+}
+
+/*
+ * Set parameters for a VariableDestReceiver
+ */
+void
+SetVariableDestReceiverParams(DestReceiver *self, Oid varid)
+{
+	svariableState *myState = (svariableState *) self;
+
+	Assert(myState->pub.mydest == DestVariable);
+	Assert(OidIsValid(varid));
+
+	myState->varid = varid;
+}
diff --git a/src/backend/jit/llvm/llvmjit_expr.c b/src/backend/jit/llvm/llvmjit_expr.c
index 0f9cc790c7..3c1e30a15b 100644
--- a/src/backend/jit/llvm/llvmjit_expr.c
+++ b/src/backend/jit/llvm/llvmjit_expr.c
@@ -1073,6 +1073,12 @@ llvm_compile_expr(ExprState *state)
 				LLVMBuildBr(b, opblocks[opno + 1]);
 				break;
 
+			case EEOP_PARAM_VARIABLE:
+				build_EvalXFunc(b, mod, "ExecEvalParamVariable",
+								v_state, op, v_econtext);
+				LLVMBuildBr(b, opblocks[opno + 1]);
+				break;
+
 			case EEOP_PARAM_CALLBACK:
 				{
 					LLVMTypeRef v_functype;
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index 632cc31a04..1393aec3fa 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -99,6 +99,7 @@ _copyPlannedStmt(const PlannedStmt *from)
 	COPY_NODE_FIELD(invalItems);
 	COPY_NODE_FIELD(paramExecTypes);
 	COPY_NODE_FIELD(utilityStmt);
+	COPY_NODE_FIELD(schemaVariables);
 	COPY_LOCATION_FIELD(stmt_location);
 	COPY_SCALAR_FIELD(stmt_len);
 
@@ -1513,6 +1514,7 @@ _copyParam(const Param *from)
 	COPY_SCALAR_FIELD(paramtype);
 	COPY_SCALAR_FIELD(paramtypmod);
 	COPY_SCALAR_FIELD(paramcollid);
+	COPY_SCALAR_FIELD(paramvarid);
 	COPY_LOCATION_FIELD(location);
 
 	return newnode;
@@ -3163,6 +3165,7 @@ _copyQuery(const Query *from)
 	COPY_SCALAR_FIELD(canSetTag);
 	COPY_NODE_FIELD(utilityStmt);
 	COPY_SCALAR_FIELD(resultRelation);
+	COPY_SCALAR_FIELD(resultVariable);
 	COPY_SCALAR_FIELD(hasAggs);
 	COPY_SCALAR_FIELD(hasWindowFuncs);
 	COPY_SCALAR_FIELD(hasTargetSRFs);
@@ -3173,6 +3176,7 @@ _copyQuery(const Query *from)
 	COPY_SCALAR_FIELD(hasForUpdate);
 	COPY_SCALAR_FIELD(hasRowSecurity);
 	COPY_SCALAR_FIELD(isReturn);
+	COPY_SCALAR_FIELD(hasSchemaVariables);
 	COPY_NODE_FIELD(cteList);
 	COPY_NODE_FIELD(rtable);
 	COPY_NODE_FIELD(jointree);
@@ -3286,6 +3290,19 @@ _copySelectStmt(const SelectStmt *from)
 	return newnode;
 }
 
+static LetStmt *
+_copyLetStmt(const LetStmt * from)
+{
+	LetStmt    *newnode = makeNode(LetStmt);
+
+	COPY_NODE_FIELD(target);
+	COPY_NODE_FIELD(query);
+	COPY_SCALAR_FIELD(plpgsql_mode);
+	COPY_LOCATION_FIELD(location);
+
+	return newnode;
+}
+
 static SetOperationStmt *
 _copySetOperationStmt(const SetOperationStmt *from)
 {
@@ -4865,6 +4882,23 @@ _copyDropSubscriptionStmt(const DropSubscriptionStmt *from)
 	return newnode;
 }
 
+static CreateSchemaVarStmt *
+_copyCreateSchemaVarStmt(const CreateSchemaVarStmt *from)
+{
+	CreateSchemaVarStmt *newnode = makeNode(CreateSchemaVarStmt);
+
+	COPY_NODE_FIELD(variable);
+	COPY_NODE_FIELD(typeName);
+	COPY_NODE_FIELD(collClause);
+	COPY_NODE_FIELD(defexpr);
+	COPY_SCALAR_FIELD(eoxaction);
+	COPY_SCALAR_FIELD(if_not_exists);
+	COPY_SCALAR_FIELD(is_not_null);
+	COPY_SCALAR_FIELD(is_immutable);
+
+	return newnode;
+}
+
 /* ****************************************************************
  *					extensible.h copy functions
  * ****************************************************************
@@ -5354,6 +5388,9 @@ copyObjectImpl(const void *from)
 		case T_SelectStmt:
 			retval = _copySelectStmt(from);
 			break;
+		case T_LetStmt:
+			retval = _copyLetStmt(from);
+			break;
 		case T_SetOperationStmt:
 			retval = _copySetOperationStmt(from);
 			break;
@@ -5696,6 +5733,9 @@ copyObjectImpl(const void *from)
 		case T_DropSubscriptionStmt:
 			retval = _copyDropSubscriptionStmt(from);
 			break;
+		case  T_CreateSchemaVarStmt:
+			retval = _copyCreateSchemaVarStmt(from);
+			break;
 		case T_A_Expr:
 			retval = _copyAExpr(from);
 			break;
@@ -5858,7 +5898,7 @@ copyObjectImpl(const void *from)
 			break;
 
 		default:
-			elog(ERROR, "unrecognized node type: %d", (int) nodeTag(from));
+			elog(ERROR, "urecognized node type: %d", (int) nodeTag(from));
 			retval = 0;			/* keep compiler quiet */
 			break;
 	}
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index a410a29a17..46cbff2b91 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -208,6 +208,7 @@ _equalParam(const Param *a, const Param *b)
 	COMPARE_SCALAR_FIELD(paramtype);
 	COMPARE_SCALAR_FIELD(paramtypmod);
 	COMPARE_SCALAR_FIELD(paramcollid);
+	COMPARE_SCALAR_FIELD(paramvarid);
 	COMPARE_LOCATION_FIELD(location);
 
 	return true;
@@ -967,6 +968,7 @@ _equalQuery(const Query *a, const Query *b)
 	COMPARE_SCALAR_FIELD(canSetTag);
 	COMPARE_NODE_FIELD(utilityStmt);
 	COMPARE_SCALAR_FIELD(resultRelation);
+	COMPARE_SCALAR_FIELD(resultVariable);
 	COMPARE_SCALAR_FIELD(hasAggs);
 	COMPARE_SCALAR_FIELD(hasWindowFuncs);
 	COMPARE_SCALAR_FIELD(hasTargetSRFs);
@@ -977,6 +979,7 @@ _equalQuery(const Query *a, const Query *b)
 	COMPARE_SCALAR_FIELD(hasForUpdate);
 	COMPARE_SCALAR_FIELD(hasRowSecurity);
 	COMPARE_SCALAR_FIELD(isReturn);
+	COMPARE_SCALAR_FIELD(hasSchemaVariables);
 	COMPARE_NODE_FIELD(cteList);
 	COMPARE_NODE_FIELD(rtable);
 	COMPARE_NODE_FIELD(jointree);
@@ -1080,6 +1083,16 @@ _equalSelectStmt(const SelectStmt *a, const SelectStmt *b)
 	return true;
 }
 
+static bool
+_equalLetStmt(const LetStmt * a, const LetStmt * b)
+{
+	COMPARE_NODE_FIELD(target);
+	COMPARE_NODE_FIELD(query);
+	COMPARE_SCALAR_FIELD(plpgsql_mode);
+
+	return true;
+}
+
 static bool
 _equalSetOperationStmt(const SetOperationStmt *a, const SetOperationStmt *b)
 {
@@ -2333,6 +2346,22 @@ _equalDropSubscriptionStmt(const DropSubscriptionStmt *a,
 	return true;
 }
 
+static bool
+_equalCreateSchemaVarStmt(const CreateSchemaVarStmt *a,
+						  const CreateSchemaVarStmt *b)
+{
+	COMPARE_NODE_FIELD(variable);
+	COMPARE_NODE_FIELD(typeName);
+	COMPARE_NODE_FIELD(collClause);
+	COMPARE_NODE_FIELD(defexpr);
+	COMPARE_SCALAR_FIELD(eoxaction);
+	COMPARE_SCALAR_FIELD(if_not_exists);
+	COMPARE_SCALAR_FIELD(is_not_null);
+	COMPARE_SCALAR_FIELD(is_immutable);
+
+	return true;
+}
+
 static bool
 _equalCreatePolicyStmt(const CreatePolicyStmt *a, const CreatePolicyStmt *b)
 {
@@ -3347,6 +3376,9 @@ equal(const void *a, const void *b)
 		case T_SelectStmt:
 			retval = _equalSelectStmt(a, b);
 			break;
+		case T_LetStmt:
+			retval = _equalLetStmt(a, b);
+			break;
 		case T_SetOperationStmt:
 			retval = _equalSetOperationStmt(a, b);
 			break;
@@ -3689,6 +3721,9 @@ equal(const void *a, const void *b)
 		case T_DropSubscriptionStmt:
 			retval = _equalDropSubscriptionStmt(a, b);
 			break;
+		case T_CreateSchemaVarStmt:
+			retval = _equalCreateSchemaVarStmt(a, b);
+			break;
 		case T_A_Expr:
 			retval = _equalAExpr(a, b);
 			break;
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index c723f6d635..1a8b078020 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -317,6 +317,7 @@ _outPlannedStmt(StringInfo str, const PlannedStmt *node)
 	WRITE_NODE_FIELD(invalItems);
 	WRITE_NODE_FIELD(paramExecTypes);
 	WRITE_NODE_FIELD(utilityStmt);
+	WRITE_NODE_FIELD(schemaVariables);
 	WRITE_LOCATION_FIELD(stmt_location);
 	WRITE_INT_FIELD(stmt_len);
 }
@@ -1156,6 +1157,7 @@ _outParam(StringInfo str, const Param *node)
 	WRITE_OID_FIELD(paramtype);
 	WRITE_INT_FIELD(paramtypmod);
 	WRITE_OID_FIELD(paramcollid);
+	WRITE_OID_FIELD(paramvarid);
 	WRITE_LOCATION_FIELD(location);
 }
 
@@ -2257,6 +2259,7 @@ _outPlannerGlobal(StringInfo str, const PlannerGlobal *node)
 	WRITE_NODE_FIELD(relationOids);
 	WRITE_NODE_FIELD(invalItems);
 	WRITE_NODE_FIELD(paramExecTypes);
+	WRITE_NODE_FIELD(schemaVariables);
 	WRITE_UINT_FIELD(lastPHId);
 	WRITE_UINT_FIELD(lastRowMarkId);
 	WRITE_INT_FIELD(lastPlanNodeId);
@@ -2317,6 +2320,7 @@ _outPlannerInfo(StringInfo str, const PlannerInfo *node)
 	WRITE_BOOL_FIELD(hasPseudoConstantQuals);
 	WRITE_BOOL_FIELD(hasAlternativeSubPlans);
 	WRITE_BOOL_FIELD(hasRecursion);
+	WRITE_BOOL_FIELD(hasSchemaVariables);
 	WRITE_INT_FIELD(wt_param_id);
 	WRITE_BITMAPSET_FIELD(curOuterRels);
 	WRITE_NODE_FIELD(curOuterParams);
@@ -2856,6 +2860,17 @@ _outPLAssignStmt(StringInfo str, const PLAssignStmt *node)
 	WRITE_LOCATION_FIELD(location);
 }
 
+static void
+_outLetStmt(StringInfo str, const LetStmt * node)
+{
+	WRITE_NODE_TYPE("LET");
+
+	WRITE_NODE_FIELD(target);
+	WRITE_NODE_FIELD(query);
+	WRITE_BOOL_FIELD(plpgsql_mode);
+	WRITE_LOCATION_FIELD(location);
+}
+
 static void
 _outFuncCall(StringInfo str, const FuncCall *node)
 {
@@ -3036,6 +3051,7 @@ _outQuery(StringInfo str, const Query *node)
 			case T_IndexStmt:
 			case T_NotifyStmt:
 			case T_DeclareCursorStmt:
+			case T_LetStmt:
 				WRITE_NODE_FIELD(utilityStmt);
 				break;
 			default:
@@ -3047,6 +3063,7 @@ _outQuery(StringInfo str, const Query *node)
 		appendStringInfoString(str, " :utilityStmt <>");
 
 	WRITE_INT_FIELD(resultRelation);
+	WRITE_OID_FIELD(resultVariable);
 	WRITE_BOOL_FIELD(hasAggs);
 	WRITE_BOOL_FIELD(hasWindowFuncs);
 	WRITE_BOOL_FIELD(hasTargetSRFs);
@@ -3057,6 +3074,7 @@ _outQuery(StringInfo str, const Query *node)
 	WRITE_BOOL_FIELD(hasForUpdate);
 	WRITE_BOOL_FIELD(hasRowSecurity);
 	WRITE_BOOL_FIELD(isReturn);
+	WRITE_BOOL_FIELD(hasSchemaVariables);
 	WRITE_NODE_FIELD(cteList);
 	WRITE_NODE_FIELD(rtable);
 	WRITE_NODE_FIELD(jointree);
@@ -4353,6 +4371,9 @@ outNode(StringInfo str, const void *obj)
 			case T_PLAssignStmt:
 				_outPLAssignStmt(str, obj);
 				break;
+			case T_LetStmt:
+				_outLetStmt(str, obj);
+				break;
 			case T_ColumnDef:
 				_outColumnDef(str, obj);
 				break;
diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c
index 3746668f52..31c16842e2 100644
--- a/src/backend/nodes/readfuncs.c
+++ b/src/backend/nodes/readfuncs.c
@@ -254,6 +254,7 @@ _readQuery(void)
 	READ_BOOL_FIELD(canSetTag);
 	READ_NODE_FIELD(utilityStmt);
 	READ_INT_FIELD(resultRelation);
+	READ_OID_FIELD(resultVariable);
 	READ_BOOL_FIELD(hasAggs);
 	READ_BOOL_FIELD(hasWindowFuncs);
 	READ_BOOL_FIELD(hasTargetSRFs);
@@ -264,6 +265,7 @@ _readQuery(void)
 	READ_BOOL_FIELD(hasForUpdate);
 	READ_BOOL_FIELD(hasRowSecurity);
 	READ_BOOL_FIELD(isReturn);
+	READ_BOOL_FIELD(hasSchemaVariables);
 	READ_NODE_FIELD(cteList);
 	READ_NODE_FIELD(rtable);
 	READ_NODE_FIELD(jointree);
@@ -628,6 +630,7 @@ _readParam(void)
 	READ_OID_FIELD(paramtype);
 	READ_INT_FIELD(paramtypmod);
 	READ_OID_FIELD(paramcollid);
+	READ_OID_FIELD(paramvarid);
 	READ_LOCATION_FIELD(location);
 
 	READ_DONE();
@@ -1598,6 +1601,7 @@ _readPlannedStmt(void)
 	READ_NODE_FIELD(invalItems);
 	READ_NODE_FIELD(paramExecTypes);
 	READ_NODE_FIELD(utilityStmt);
+	READ_NODE_FIELD(schemaVariables);
 	READ_LOCATION_FIELD(stmt_location);
 	READ_INT_FIELD(stmt_len);
 
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 1868c4eff4..66bf1f2b37 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -310,6 +310,7 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
 	glob->lastPlanNodeId = 0;
 	glob->transientPlan = false;
 	glob->dependsOnRole = false;
+	glob->schemaVariables = NIL;
 
 	/*
 	 * Assess whether it's feasible to use parallel mode for this query. We
@@ -523,6 +524,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->schemaVariables = glob->schemaVariables;
 	result->stmt_location = parse->stmt_location;
 	result->stmt_len = parse->stmt_len;
 
@@ -672,6 +674,12 @@ subquery_planner(PlannerGlobal *glob, Query *parse,
 	 */
 	pull_up_subqueries(root);
 
+	/*
+	 * Check if some subquery uses schema variable. Flag hasSchemaVariables
+	 * should be true if query or some subquery uses any schema variable.
+	 */
+	pull_up_has_schema_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 9f40ed77e6..1fc739ff97 100644
--- a/src/backend/optimizer/plan/setrefs.c
+++ b/src/backend/optimizer/plan/setrefs.c
@@ -172,7 +172,8 @@ static List *set_returning_clause_references(PlannerInfo *root,
 											 Plan *topplan,
 											 Index resultRelation,
 											 int rtoffset);
-
+static bool pull_up_has_schema_variables_walker(Node *node,
+											   PlannerInfo *root);
 
 /*****************************************************************************
  *
@@ -1094,6 +1095,50 @@ set_plan_refs(PlannerInfo *root, Plan *plan, int rtoffset)
 	return plan;
 }
 
+/*
+ * Search usage of schema variables in subqueries
+ */
+void
+pull_up_has_schema_variables(PlannerInfo *root)
+{
+	Query	   *query = root->parse;
+
+	if (query->hasSchemaVariables)
+	{
+		root->hasSchemaVariables = true;
+	}
+	else
+	{
+		(void) query_tree_walker(query,
+								 pull_up_has_schema_variables_walker,
+								 (void *) root, 0);
+	}
+}
+
+static bool
+pull_up_has_schema_variables_walker(Node *node, PlannerInfo *root)
+{
+	if (node == NULL)
+		return false;
+	if (IsA(node, Query))
+	{
+		Query	   *query = (Query *) node;
+
+		if (query->hasSchemaVariables)
+		{
+			root->hasSchemaVariables = true;
+			return false;
+		}
+
+		/* Recurse into subselects */
+		return query_tree_walker((Query *) node,
+								 pull_up_has_schema_variables_walker,
+								 (void *) root, 0);
+	}
+	return expression_tree_walker(node, pull_up_has_schema_variables_walker,
+								  (void *) root);
+}
+
 /*
  * set_indexonlyscan_references
  *		Do set_plan_references processing on an IndexOnlyScan
@@ -1722,10 +1767,14 @@ fix_expr_common(PlannerInfo *root, Node *node)
 /*
  * fix_param_node
  *		Do set_plan_references processing on a Param
+ *		Collect schema 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 should to calculate paramid.
  */
 static Node *
 fix_param_node(PlannerInfo *root, Param *p)
@@ -1744,6 +1793,52 @@ 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 schema variables, and we can complete
+		 * paramid parameter.
+		 */
+		foreach(lc, root->glob->schemaVariables)
+		{
+			if (lfirst_oid(lc) == p->paramvarid)
+			{
+				p->paramid = n;
+				found = true;
+				break;
+			}
+			n += 1;
+		}
+
+		if (!found)
+		{
+			PlanInvalItem *inval_item = makeNode(PlanInvalItem);
+
+			/* paramid is still schema variable id */
+			inval_item->cacheId = VARIABLEOID;
+			inval_item->hashValue = GetSysCacheHashValue1(VARIABLEOID,
+														  ObjectIdGetDatum(p->paramvarid));
+
+			/* Append this variable to global, register dependency */
+			root->glob->invalItems = lappend(root->glob->invalItems,
+											 inval_item);
+			root->glob->schemaVariables = lappend_oid(root->glob->schemaVariables,
+													  p->paramvarid);
+
+			p->paramid = n;
+		}
+
+		return (Node *) p;
+	}
+
 	return (Node *) copyObject(p);
 }
 
@@ -1819,7 +1914,9 @@ 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.
+ * and adding OIDs from regclass Const nodes into root->glob->relationOids,
+ * and replacing PARAM_VARIABLE paramid, that is oid of schema variable
+ * to offset to array of by query used schema variables.
  *
  * 'node': the expression to be modified
  * 'rtoffset': how much to increment varnos by
@@ -1841,7 +1938,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->hasSchemaVariables)
 	{
 		return fix_scan_expr_mutator(node, &context);
 	}
diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c
index 526997327c..c008343c37 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/schema_variable.h"
 #include "executor/executor.h"
 #include "executor/functions.h"
 #include "funcapi.h"
@@ -814,7 +815,8 @@ max_parallel_hazard_walker(Node *node, max_parallel_hazard_context *context)
 	{
 		Param	   *param = (Param *) node;
 
-		if (param->paramkind == PARAM_EXTERN)
+		if (param->paramkind == PARAM_EXTERN ||
+			param->paramkind == PARAM_VARIABLE)
 			return false;
 
 		if (param->paramkind != PARAM_EXEC ||
@@ -2180,6 +2182,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 schema variable can be used for estimation too.
  *--------------------
  */
 Node *
@@ -2299,6 +2302,30 @@ 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 = GetSchemaVariable(param->paramvarid,
+											 &isnull,
+											 param->paramtype,
+											 true);
+
+					return (Node *) makeConst(param->paramtype,
+											  param->paramtypmod,
+											  param->paramcollid,
+											  (int) typLen,
+											  pval,
+											  isnull,
+											  typByVal);
+				}
 
 				/*
 				 * Not replaceable, so just copy the Param (no need to
@@ -4628,7 +4655,7 @@ substitute_actual_parameters_mutator(Node *node,
 {
 	if (node == NULL)
 		return NULL;
-	if (IsA(node, Param))
+	if (IsA(node, Param) &&((Param *) node)->paramkind != PARAM_VARIABLE)
 	{
 		Param	   *param = (Param *) node;
 
diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
index 862f18a92f..ce20fd0f0a 100644
--- a/src/backend/parser/analyze.c
+++ b/src/backend/parser/analyze.c
@@ -25,7 +25,10 @@
 #include "postgres.h"
 
 #include "access/sysattr.h"
+#include "catalog/namespace.h"
 #include "catalog/pg_type.h"
+#include "catalog/pg_variable.h"
+#include "commands/schema_variable.h"
 #include "miscadmin.h"
 #include "nodes/makefuncs.h"
 #include "nodes/nodeFuncs.h"
@@ -49,6 +52,7 @@
 #include "utils/builtins.h"
 #include "utils/guc.h"
 #include "utils/queryjumble.h"
+#include "utils/lsyscache.h"
 #include "utils/rel.h"

@@ -86,6 +90,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
@@ -287,6 +293,7 @@ transformStmt(ParseState *pstate, Node *parseTree)
 		case T_InsertStmt:
 		case T_UpdateStmt:
 		case T_DeleteStmt:
+		case T_LetStmt:
 			(void) test_raw_expression_coverage(parseTree, NULL);
 			break;
 		default:
@@ -356,6 +363,11 @@ transformStmt(ParseState *pstate, Node *parseTree)
 									   (CallStmt *) parseTree);
 			break;
 
+		case T_LetStmt:
+			result = transformLetStmt(pstate,
+									  (LetStmt *) parseTree);
+			break;
+
 		default:
 
 			/*
@@ -397,6 +409,7 @@ analyze_requires_snapshot(RawStmt *parseTree)
 		case T_UpdateStmt:
 		case T_SelectStmt:
 		case T_PLAssignStmt:
+		case T_LetStmt:
 			result = true;
 			break;
 
@@ -480,6 +493,8 @@ transformDeleteStmt(ParseState *pstate, DeleteStmt *stmt)
 	qry->hasTargetSRFs = pstate->p_hasTargetSRFs;
 	qry->hasAggs = pstate->p_hasAggs;
 
+	qry->hasSchemaVariables = pstate->p_hasSchemaVariables;
+
 	assign_query_collations(pstate, qry);
 
 	/* this must be done after collations, for reliable comparison of exprs */
@@ -897,6 +912,7 @@ transformInsertStmt(ParseState *pstate, InsertStmt *stmt)
 
 	qry->hasTargetSRFs = pstate->p_hasTargetSRFs;
 	qry->hasSubLinks = pstate->p_hasSubLinks;
+	qry->hasSchemaVariables = pstate->p_hasSchemaVariables;
 
 	assign_query_collations(pstate, qry);
 
@@ -1239,6 +1255,7 @@ transformSelectStmt(ParseState *pstate, SelectStmt *stmt)
 	Query	   *qry = makeNode(Query);
 	Node	   *qual;
 	ListCell   *l;
+	ParseExprKind		target_exprkind;
 
 	qry->commandType = CMD_SELECT;
 
@@ -1267,9 +1284,19 @@ transformSelectStmt(ParseState *pstate, SelectStmt *stmt)
 	/* process the FROM clause */
 	transformFromClause(pstate, stmt->fromClause);
 
-	/* transform targetlist */
-	qry->targetList = transformTargetList(pstate, stmt->targetList,
-										  EXPR_KIND_SELECT_TARGET);
+	/*
+	 * Transform targetlist. Second usage of this transformation
+	 * is for Let statement. In this case we would to allow DEFAULT
+	 * keyword - we specify EXPR_KIND_LET_TARGET.
+	 */
+	target_exprkind =
+		(pstate->p_expr_kind != EXPR_KIND_LET_TARGET ||
+		 pstate->parentParseState != NULL) ?
+					EXPR_KIND_SELECT_TARGET : EXPR_KIND_LET_TARGET;
+
+	qry->targetList = transformTargetList(pstate,
+										  stmt->targetList,
+										  target_exprkind);
 
 	/* mark column origins */
 	markTargetListOrigins(pstate, qry->targetList);
@@ -1359,6 +1386,8 @@ transformSelectStmt(ParseState *pstate, SelectStmt *stmt)
 							   (LockingClause *) lfirst(l), false);
 	}
 
+	qry->hasSchemaVariables = pstate->p_hasSchemaVariables;
+
 	assign_query_collations(pstate, qry);
 
 	/* this must be done after collations, for reliable comparison of exprs */
@@ -1583,12 +1612,261 @@ transformValuesClause(ParseState *pstate, SelectStmt *stmt)
 	qry->jointree = makeFromExpr(pstate->p_joinlist, NULL);
 
 	qry->hasSubLinks = pstate->p_hasSubLinks;
+	qry->hasSchemaVariables = pstate->p_hasSchemaVariables;
 
 	assign_query_collations(pstate, qry);
 
 	return qry;
 }
 
+/*
+ * transformLetStmt -
+ *	  transform an Let Statement
+ */
+static Query *
+transformLetStmt(ParseState *pstate, LetStmt * stmt)
+{
+	Query	   *query;
+	Query	   *result;
+	List	   *exprList = NIL;
+	List	   *exprListCoer = NIL;
+	List	   *indirection = NIL;
+	ListCell   *lc;
+	Query	   *selectQuery;
+	int			i = 0;
+	Oid			varid;
+	ParseExprKind sv_expr_kind;
+	char	   *attrname = NULL;
+	bool		not_unique;
+	bool		is_rowtype;
+	bool		to_default = false;
+	Oid			typid;
+	int32		typmod;
+	Oid			collid;
+	AclResult	aclresult;
+	List	   *names = NULL;
+	int			indirection_start;
+
+	sv_expr_kind = pstate->p_expr_kind;
+	pstate->p_expr_kind = EXPR_KIND_LET_TARGET;
+
+	/* There can't be any outer WITH to worry about */
+	Assert(pstate->p_ctenamespace == NIL);
+
+	names = NamesFromList(stmt->target);
+
+	varid = IdentifyVariable(names, &attrname, &not_unique);
+	if (not_unique)
+		ereport(ERROR,
+				(errcode(ERRCODE_AMBIGUOUS_PARAMETER),
+				 errmsg("target \"%s\" of LET command is ambiguous",
+						NameListToString(names)),
+				 parser_errposition(pstate, stmt->location)));
+
+	if (!OidIsValid(varid))
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("schema variable \"%s\" doesn't exists",
+						NameListToString(names)),
+				 parser_errposition(pstate, stmt->location)));
+
+	indirection_start = list_length(names) - (attrname ? 1 : 0);
+	indirection = list_copy_tail(stmt->target, indirection_start);
+
+	get_schema_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("target variable \"%s\" is not row type",
+						schema_variable_get_name(varid)),
+				 parser_errposition(pstate, stmt->location)));
+
+	aclresult = pg_variable_aclcheck(varid, GetUserId(), ACL_WRITE);
+	if (aclresult != ACLCHECK_OK)
+		aclcheck_error(aclresult, OBJECT_VARIABLE, NameListToString(names));
+
+	selectQuery = transformStmt(pstate, stmt->query);
+
+	/* The grammar should have produced a SELECT */
+	if (!IsA(selectQuery, Query) ||
+		selectQuery->commandType != CMD_SELECT)
+		elog(ERROR, "unexpected non-SELECT command in LET command");
+
+	/*----------
+	 * Generate an expression list for the LET that selects all the
+	 * non-resjunk columns from the subquery.
+	 *----------
+	 */
+	exprList = NIL;
+	foreach(lc, selectQuery->targetList)
+	{
+		TargetEntry *tle = (TargetEntry *) lfirst(lc);
+
+		if (tle->resjunk)
+			continue;
+
+		if (IsA(tle->expr, SetToDefault))
+			to_default = true;
+
+		exprList = lappend(exprList, tle->expr);
+	}
+
+	/*
+	 * Because doesn't support pattern matching, don't allow multicolumn
+	 * result
+	 */
+	if (list_length(exprList) != 1)
+		ereport(ERROR,
+				(errcode(ERRCODE_SYNTAX_ERROR),
+				 errmsg("expression is not scalar value"),
+				 parser_errposition(pstate,
+									exprLocation((Node *) exprList))));
+
+	if (to_default && attrname != NULL)
+		ereport(ERROR,
+				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+				 errmsg("only complete variable can be set to default"),
+				 parser_errposition(pstate, stmt->location)));
+
+	exprListCoer = NIL;
+
+	foreach(lc, exprList)
+	{
+		Expr	   *expr = (Expr *) lfirst(lc);
+		Expr	   *coerced_expr;
+		Param	   *param;
+		Oid			exprtypid;
+
+		if (IsA(expr, SetToDefault))
+		{
+			SetToDefault *def = (SetToDefault *) expr;
+
+			def->typeId = typid;
+			def->typeMod = typmod;
+			def->collation = collid;
+		}
+		else if (IsA(expr, Const) && ((Const *) expr)->constisnull)
+		{
+			/* use known type for NULL value */
+			expr = (Expr *) makeNullConst(typid, typmod, collid);
+		}
+
+		/* now we can read type of expression */
+		exprtypid = exprType((Node *) expr);
+
+		param = makeNode(Param);
+		param->paramkind = PARAM_VARIABLE;
+		param->paramvarid = varid;
+		param->paramtype = typid;
+		param->paramtypmod = typmod;
+
+		if (indirection != NULL)
+		{
+			bool		targetIsArray;
+			char	   *targetName;
+
+			targetName = attrname != NULL ? attrname : get_schema_variable_name(varid);
+			targetIsArray = OidIsValid(get_element_type(typid));
+
+			pstate->p_hasSchemaVariables = true;
+
+			coerced_expr = (Expr *)
+				transformAssignmentIndirection(pstate,
+											   (Node *) param,
+											   targetName,
+											   targetIsArray,
+											   typid,
+											   typmod,
+											   InvalidOid,
+											   indirection,
+											   list_head(indirection),
+											   (Node *) expr,
+											   COERCION_PLPGSQL,
+											   stmt->location);
+		}
+		else
+			coerced_expr = (Expr *)
+				coerce_to_target_type(pstate,
+									  (Node *) expr,
+									  exprtypid,
+									  typid, typmod,
+									  COERCION_ASSIGNMENT,
+									  COERCE_IMPLICIT_CAST,
+									  stmt->location);
+
+		if (coerced_expr == NULL)
+			ereport(ERROR,
+					(errcode(ERRCODE_DATATYPE_MISMATCH),
+					 errmsg("variable \"%s\" is of type %s,"
+							" but expression is of type %s",
+							schema_variable_get_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->hasSchemaVariables = pstate->p_hasSchemaVariables;
+
+	/* This is top query */
+	query->canSetTag = true;
+
+	/*
+	 * rewrite SetToDefaults needs varid in Query structure
+	 */
+	query->resultVariable = varid;
+
+	assign_query_collations(pstate, query);
+
+	pstate->p_expr_kind = sv_expr_kind;
+
+	stmt->query = (Node *) query;
+
+	/*
+	 * When statement is executed as an expression as PLpgSQL LET
+	 * statement, then we should to return query. We should not
+	 * to use an utility wrapper node.
+	 */
+	if (stmt->plpgsql_mode)
+		return query;
+
+	/* represent the command as a utility Query */
+	result = makeNode(Query);
+	result->commandType = CMD_UTILITY;
+	result->utilityStmt = (Node *) stmt;
+
+	return result;
+}
+
 /*
  * transformSetOperationStmt -
  *	  transforms a set-operations tree
@@ -1839,6 +2117,8 @@ transformSetOperationStmt(ParseState *pstate, SelectStmt *stmt)
 							   (LockingClause *) lfirst(l), false);
 	}
 
+	qry->hasSchemaVariables = pstate->p_hasSchemaVariables;
+
 	assign_query_collations(pstate, qry);
 
 	/* this must be done after collations, for reliable comparison of exprs */
@@ -2354,6 +2634,7 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
 
 	qry->hasTargetSRFs = pstate->p_hasTargetSRFs;
 	qry->hasSubLinks = pstate->p_hasSubLinks;
+	qry->hasSchemaVariables = pstate->p_hasSchemaVariables;
 
 	assign_query_collations(pstate, qry);
 
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 73494002ad..8b27e079ce 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -226,6 +226,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	JoinType			jtype;
 	DropBehavior		dbehavior;
 	OnCommitAction		oncommit;
+	VariableEOXAction	oneoxaction;
 	List				*list;
 	Node				*node;
 	Value				*value;
@@ -276,8 +277,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 		ConstraintsSetStmt CopyStmt CreateAsStmt CreateCastStmt
 		CreateDomainStmt CreateExtensionStmt CreateGroupStmt CreateOpClassStmt
 		CreateOpFamilyStmt AlterOpFamilyStmt CreatePLangStmt
-		CreateSchemaStmt CreateSeqStmt CreateStmt CreateStatsStmt CreateTableSpaceStmt
-		CreateFdwStmt CreateForeignServerStmt CreateForeignTableStmt
+		CreateSchemaStmt CreateSchemaVarStmt CreateSeqStmt CreateStmt CreateStatsStmt
+		CreateTableSpaceStmt CreateFdwStmt CreateForeignServerStmt CreateForeignTableStmt
 		CreateAssertionStmt CreateTransformStmt CreateTrigStmt CreateEventTrigStmt
 		CreateUserStmt CreateUserMappingStmt CreateRoleStmt CreatePolicyStmt
 		CreatedbStmt DeclareCursorStmt DefineStmt DeleteStmt DiscardStmt DoStmt
@@ -287,7 +288,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 		DropTransformStmt
 		DropUserMappingStmt ExplainStmt FetchStmt
 		GrantStmt GrantRoleStmt ImportForeignSchemaStmt IndexStmt InsertStmt
-		ListenStmt LoadStmt LockStmt NotifyStmt ExplainableStmt PreparableStmt
+		LetStmt ListenStmt LoadStmt LockStmt NotifyStmt ExplainableStmt PreparableStmt
 		CreateFunctionStmt AlterFunctionStmt ReindexStmt RemoveAggrStmt
 		RemoveFuncStmt RemoveOperStmt RenameStmt ReturnStmt RevokeStmt RevokeRoleStmt
 		RuleActionStmt RuleActionStmtOrEmpty RuleStmt
@@ -427,6 +428,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 				TriggerTransitions TriggerReferencing
 				vacuum_relation_list opt_vacuum_relation_list
 				drop_option_list
+				let_target
 
 %type <node>	opt_routine_body
 %type <groupclause> group_clause
@@ -450,6 +452,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %type <ival>	 OptTemp
 %type <ival>	 OptNoLog
 %type <oncommit> OnCommitOption
+%type <oneoxaction> OnEOXActionOption
 
 %type <ival>	for_locking_strength
 %type <node>	for_locking_item
@@ -608,6 +611,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %type <partboundspec> PartitionBoundSpec
 %type <list>		hash_partbound
 %type <defelt>		hash_partbound_elem
+%type <node>		OptSchemaVarDefExpr
+%type <boolean>		OptNotNull OptImmutable
 
 %type <str>	optColumnCompression
 
@@ -678,7 +683,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	KEY
 
 	LABEL LANGUAGE LARGE_P LAST_P LATERAL_P
-	LEADING LEAKPROOF LEAST LEFT LEVEL LIKE LIMIT LISTEN LOAD LOCAL
+	LEADING LEAKPROOF LEAST LEFT LET LEVEL LIKE LIMIT LISTEN LOAD LOCAL
 	LOCALTIME LOCALTIMESTAMP LOCATION LOCK_P LOCKED LOGGED
 
 	MAPPING MATCH MATERIALIZED MAXVALUE METHOD MINUTE_P MINVALUE MODE MONTH_P MOVE
@@ -717,8 +722,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	UESCAPE UNBOUNDED UNCOMMITTED UNENCRYPTED UNION UNIQUE UNKNOWN
 	UNLISTEN UNLOGGED UNTIL UPDATE USER USING
 
-	VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIADIC VARYING
-	VERBOSE VERSION_P VIEW VIEWS VOLATILE
+	VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIABLE VARIABLES
+	VARIADIC VARYING VERBOSE VERSION_P VIEW VIEWS VOLATILE
 
 	WHEN WHERE WHITESPACE_P WINDOW WITH WITHIN WITHOUT WORK WRAPPER WRITE
 
@@ -753,6 +758,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %token		MODE_PLPGSQL_ASSIGN1
 %token		MODE_PLPGSQL_ASSIGN2
 %token		MODE_PLPGSQL_ASSIGN3
+%token		MODE_PLPGSQL_LET

 /* Precedence: lowest to highest */
@@ -858,6 +864,13 @@ parse_toplevel:
 				pg_yyget_extra(yyscanner)->parsetree =
 					list_make1(makeRawStmt((Node *) n, 0));
 			}
+			| MODE_PLPGSQL_LET LetStmt
+			{
+				LetStmt *n = (LetStmt *) $2;
+				n->plpgsql_mode = true;
+				pg_yyget_extra(yyscanner)->parsetree =
+					list_make1(makeRawStmt((Node *) n, 0));
+			}
 		;
 
 /*
@@ -960,6 +973,7 @@ stmt:
 			| CreatePolicyStmt
 			| CreatePLangStmt
 			| CreateSchemaStmt
+			| CreateSchemaVarStmt
 			| CreateSeqStmt
 			| CreateStmt
 			| CreateSubscriptionStmt
@@ -997,6 +1011,7 @@ stmt:
 			| ImportForeignSchemaStmt
 			| IndexStmt
 			| InsertStmt
+			| LetStmt
 			| ListenStmt
 			| RefreshMatViewStmt
 			| LoadStmt
@@ -1889,7 +1904,12 @@ DiscardStmt:
 					n->target = DISCARD_SEQUENCES;
 					$$ = (Node *) n;
 				}
-
+			| DISCARD VARIABLES
+				{
+					DiscardStmt *n = makeNode(DiscardStmt);
+					n->target = DISCARD_VARIABLES;
+					$$ = (Node *) n;
+				}
 		;

@@ -4645,6 +4665,61 @@ create_extension_opt_item:
 				}
 		;
 
+/*****************************************************************************
+ *
+ *		QUERY :
+ *				CREATE VARIABLE varname [AS] type
+ *
+ *****************************************************************************/
+
+CreateSchemaVarStmt:
+			CREATE OptTemp OptImmutable VARIABLE qualified_name opt_as Typename opt_collate_clause OptNotNull OptSchemaVarDefExpr OnEOXActionOption
+				{
+					CreateSchemaVarStmt *n = makeNode(CreateSchemaVarStmt);
+					$5->relpersistence = $2;
+					n->is_immutable = $3;
+					n->variable = $5;
+					n->typeName = $7;
+					n->collClause = (CollateClause *) $8;
+					n->is_not_null = $9;
+					n->defexpr = $10;
+					n->eoxaction = $11;
+					n->if_not_exists = false;
+					$$ = (Node *) n;
+				}
+			| CREATE OptTemp OptImmutable VARIABLE IF_P NOT EXISTS qualified_name opt_as Typename opt_collate_clause OptNotNull OptSchemaVarDefExpr OnEOXActionOption
+				{
+					CreateSchemaVarStmt *n = makeNode(CreateSchemaVarStmt);
+					$8->relpersistence = $2;
+					n->is_immutable = $3;
+					n->variable = $8;
+					n->typeName = $10;
+					n->collClause = (CollateClause *) $11;
+					n->is_not_null = $12;
+					n->defexpr = $13;
+					n->eoxaction = $14;
+					n->if_not_exists = true;
+					$$ = (Node *) n;
+				}
+		;
+
+OptSchemaVarDefExpr: DEFAULT b_expr					{ $$ = $2; }
+			| /* EMPTY */							{ $$ = NULL; }
+		;
+
+OnEOXActionOption:  ON COMMIT DROP					{ $$ = VARIABLE_EOX_DROP; }
+			| ON TRANSACTION END_P RESET			{ $$ = VARIABLE_EOX_RESET; }
+			| /*EMPTY*/								{ $$ = VARIABLE_EOX_NOOP; }
+		;
+
+OptNotNull: NOT NULL_P								{ $$ = true; }
+			| /* EMPTY */							{ $$ = false; }
+		;
+
+OptImmutable: IMMUTABLE								{ $$ = true; }
+			| /* EMPTY */							{ $$ = false; }
+		;
+
 /*****************************************************************************
  *
  * ALTER EXTENSION name UPDATE [ TO version ]
@@ -6332,6 +6407,7 @@ object_type_any_name:
 			| TEXT_P SEARCH DICTIONARY				{ $$ = OBJECT_TSDICTIONARY; }
 			| TEXT_P SEARCH TEMPLATE				{ $$ = OBJECT_TSTEMPLATE; }
 			| TEXT_P SEARCH CONFIGURATION			{ $$ = OBJECT_TSCONFIGURATION; }
+			| VARIABLE								{ $$ = OBJECT_VARIABLE; }
 		;
 
 /*
@@ -7100,6 +7176,14 @@ privilege_target:
 					n->objs = $2;
 					$$ = n;
 				}
+			| VARIABLE qualified_name_list
+				{
+					PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget));
+					n->targtype = ACL_TARGET_OBJECT;
+					n->objtype = OBJECT_VARIABLE;
+					n->objs = $2;
+					$$ = n;
+				}
 			| ALL TABLES IN_P SCHEMA name_list
 				{
 					PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget));
@@ -7140,6 +7224,14 @@ privilege_target:
 					n->objs = $5;
 					$$ = n;
 				}
+			| ALL VARIABLES IN_P SCHEMA name_list
+				{
+					PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget));
+					n->targtype = ACL_TARGET_ALL_IN_SCHEMA;
+					n->objtype = OBJECT_VARIABLE;
+					n->objs = $5;
+					$$ = n;
+				}
 		;

@@ -7300,6 +7392,7 @@ defacl_privilege_target:
 			| SEQUENCES		{ $$ = OBJECT_SEQUENCE; }
 			| TYPES_P		{ $$ = OBJECT_TYPE; }
 			| SCHEMAS		{ $$ = OBJECT_SCHEMA; }
+			| VARIABLES		{ $$ = OBJECT_VARIABLE; }
 		;

@@ -9026,6 +9119,25 @@ RenameStmt: ALTER AGGREGATE aggregate_with_argtypes RENAME TO name
 					n->missing_ok = false;
 					$$ = (Node *)n;
 				}
+			| ALTER VARIABLE any_name RENAME TO name
+				{
+					RenameStmt *n = makeNode(RenameStmt);
+					n->renameType = OBJECT_VARIABLE;
+					n->object = (Node *) $3;
+					n->newname = $6;
+					n->missing_ok = false;
+					$$ = (Node *)n;
+				}
+			| ALTER VARIABLE IF_P EXISTS any_name RENAME TO name
+				{
+					RenameStmt *n = makeNode(RenameStmt);
+					n->renameType = OBJECT_VARIABLE;
+					n->object = (Node *) $5;
+					n->newname = $8;
+					n->missing_ok = true;
+					$$ = (Node *)n;
+				}
+
 		;
 
 opt_column: COLUMN
@@ -9354,6 +9466,25 @@ AlterObjectSchemaStmt:
 					n->missing_ok = false;
 					$$ = (Node *)n;
 				}
+			| ALTER VARIABLE any_name SET SCHEMA name
+				{
+					AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt);
+					n->objectType = OBJECT_VARIABLE;
+					n->object = (Node *) $3;
+					n->newschema = $6;
+					n->missing_ok = false;
+					$$ = (Node *)n;
+				}
+			| ALTER VARIABLE IF_P EXISTS any_name SET SCHEMA name
+				{
+					AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt);
+					n->objectType = OBJECT_VARIABLE;
+					n->object = (Node *) $5;
+					n->newschema = $8;
+					n->missing_ok = true;
+					$$ = (Node *)n;
+				}
+
 		;
 
 /*****************************************************************************
@@ -9607,6 +9738,14 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec
 					n->newowner = $6;
 					$$ = (Node *)n;
 				}
+			| ALTER VARIABLE any_name OWNER TO RoleSpec
+				{
+					AlterOwnerStmt *n = makeNode(AlterOwnerStmt);
+					n->objectType = OBJECT_VARIABLE;
+					n->object = (Node *) $3;
+					n->newowner = $6;
+					$$ = (Node *)n;
+				}
 		;

@@ -10848,6 +10987,7 @@ ExplainableStmt:
 			| CreateMatViewStmt
 			| RefreshMatViewStmt
 			| ExecuteStmt					/* by default all are $$=$1 */
+			| LetStmt
 		;
 
 /*****************************************************************************
@@ -10876,6 +11016,7 @@ PreparableStmt:
 			| InsertStmt
 			| UpdateStmt
 			| DeleteStmt					/* by default all are $$=$1 */
+			| LetStmt
 		;
 
 /*****************************************************************************
@@ -11293,6 +11434,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:
@@ -15615,6 +15797,7 @@ unreserved_keyword:
 			| LARGE_P
 			| LAST_P
 			| LEAKPROOF
+			| LET
 			| LEVEL
 			| LISTEN
 			| LOAD
@@ -15772,6 +15955,8 @@ unreserved_keyword:
 			| VALIDATE
 			| VALIDATOR
 			| VALUE_P
+			| VARIABLE
+			| VARIABLES
 			| VARYING
 			| VERSION_P
 			| VIEW
@@ -16179,6 +16364,7 @@ bare_label_keyword:
 			| LEAKPROOF
 			| LEAST
 			| LEFT
+			| LET
 			| LEVEL
 			| LIKE
 			| LISTEN
@@ -16378,6 +16564,8 @@ bare_label_keyword:
 			| VALUE_P
 			| VALUES
 			| VARCHAR
+			| VARIABLE
+			| VARIABLES
 			| VARIADIC
 			| VERBOSE
 			| VERSION_P
diff --git a/src/backend/parser/parse_agg.c b/src/backend/parser/parse_agg.c
index ceb0bf597d..2c48f3a08b 100644
--- a/src/backend/parser/parse_agg.c
+++ b/src/backend/parser/parse_agg.c
@@ -348,6 +348,7 @@ check_agglevels_and_constraints(ParseState *pstate, Node *expr)
 			Assert(false);		/* can't happen */
 			break;
 		case EXPR_KIND_OTHER:
+		case EXPR_KIND_LET_TARGET:
 
 			/*
 			 * Accept aggregate/grouping here; caller must throw error if
@@ -464,6 +465,7 @@ check_agglevels_and_constraints(ParseState *pstate, Node *expr)
 			break;
 		case EXPR_KIND_COLUMN_DEFAULT:
 		case EXPR_KIND_FUNCTION_DEFAULT:
+		case EXPR_KIND_VARIABLE_DEFAULT:
 
 			if (isAgg)
 				err = _("aggregate functions are not allowed in DEFAULT expressions");
@@ -912,6 +914,7 @@ transformWindowFuncCall(ParseState *pstate, WindowFunc *wfunc,
 			break;
 		case EXPR_KIND_COLUMN_DEFAULT:
 		case EXPR_KIND_FUNCTION_DEFAULT:
+		case EXPR_KIND_VARIABLE_DEFAULT:
 			err = _("window functions are not allowed in DEFAULT expressions");
 			break;
 		case EXPR_KIND_INDEX_EXPRESSION:
@@ -950,6 +953,9 @@ transformWindowFuncCall(ParseState *pstate, WindowFunc *wfunc,
 		case EXPR_KIND_CYCLE_MARK:
 			errkind = true;
 			break;
+		case EXPR_KIND_LET_TARGET:
+			err = _("window functions are not allowed in LET statement");
+			break;
 
 			/*
 			 * There is intentionally no default: case here, so that the
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index f928c32311..e662b335b9 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -16,6 +16,7 @@
 #include "postgres.h"
 
 #include "catalog/pg_type.h"
+#include "catalog/pg_variable.h"
 #include "commands/dbcommands.h"
 #include "miscadmin.h"
 #include "nodes/makefuncs.h"
@@ -36,6 +37,7 @@
 #include "utils/date.h"
 #include "utils/lsyscache.h"
 #include "utils/timestamp.h"
+#include "utils/typcache.h"
 #include "utils/xml.h"
 
 /* GUC parameters */
@@ -82,7 +84,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 *makeParamSchemaVariable(ParseState *pstate,
+									 Oid varid, Oid typid, int32 typmod, Oid collid,
+									 char *attrname, int location);
 
 /*
  * transformExpr -
@@ -282,6 +286,7 @@ transformExprRecurse(ParseState *pstate, Node *expr)
 			 * processed it rather than passing it to transformExpr().
 			 */
 		case T_SetToDefault:
+			Assert(false);
 			ereport(ERROR,
 					(errcode(ERRCODE_SYNTAX_ERROR),
 					 errmsg("DEFAULT is not allowed in this context"),
@@ -448,6 +453,9 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
 	char	   *relname = NULL;
 	char	   *colname = NULL;
 	ParseNamespaceItem *nsitem;
+	Oid			varid = InvalidOid;
+	char	   *attrname = NULL;
+	bool		not_unique;
 	int			levels_up;
 	enum
 	{
@@ -509,6 +517,9 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
 		case EXPR_KIND_COPY_WHERE:
 		case EXPR_KIND_GENERATED_COLUMN:
 		case EXPR_KIND_CYCLE_MARK:
+		case EXPR_KIND_VARIABLE_DEFAULT:
+		case EXPR_KIND_LET_TARGET:
+
 			/* okay */
 			break;
 
@@ -766,6 +777,15 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
 			break;
 	}
 
+	varid = IdentifyVariable(cref->fields, &attrname, &not_unique);
+
+	if (not_unique)
+		ereport(ERROR,
+				(errcode(ERRCODE_AMBIGUOUS_PARAMETER),
+				 errmsg("schema variable reference \"%s\" is ambiguous",
+						NameListToString(cref->fields)),
+				 parser_errposition(pstate, cref->location)));
+
 	/*
 	 * Now give the PostParseColumnRefHook, if any, a chance.  We pass the
 	 * translation-so-far so that it can throw an error if it wishes in the
@@ -790,6 +810,78 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
 					 parser_errposition(pstate, cref->location)));
 	}
 
+	if (OidIsValid(varid))
+	{
+		Oid			typid;
+		int32		typmod;
+		Oid			collid;
+
+		get_schema_variable_type_typmod_collid(varid, &typid, &typmod, &collid);
+
+		if (node != NULL)
+		{
+			/*
+			 * Some cases with ambiguous references can be solved without
+			 * raising an error. When there is an collision between column
+			 * name (or label) and some schema variable name, and when we know
+			 * attribute name, then we can ignore the collision when:
+			 *
+			 *   a) variable is of scalar type (then indirection cannot be
+			 *      applied on this schema variable.
+			 *
+			 *   b) when related variable has no field of attrname, then
+			 *      indirection cannot be applied on this schema variable.
+			 */
+			if (attrname)
+			{
+				if (type_is_rowtype(typid))
+				{
+					TupleDesc	tupdesc;
+					bool		found = false;
+					int			i;
+
+					/* slow part, I hope it will not be to often */
+					tupdesc = lookup_rowtype_tupdesc(typid, typmod);
+					for (i = 0; i < tupdesc->natts; i++)
+					{
+						if (namestrcmp(&(TupleDescAttr(tupdesc, i)->attname), attrname) == 0 &&
+							!TupleDescAttr(tupdesc, i)->attisdropped)
+						{
+							found = true;
+							break;
+						}
+					}
+
+					FreeTupleDesc(tupdesc);
+
+					/* there are not composite variable with this field */
+					if (!found)
+						varid = InvalidOid;
+				}
+				else
+					/* there are not composite variable with this name */
+					varid = InvalidOid;
+			}
+
+			/*
+			 * Raise error if varid is still valid. It should be really
+			 * amigonuous
+			 */
+			if (OidIsValid(varid))
+				ereport(ERROR,
+						(errcode(ERRCODE_AMBIGUOUS_COLUMN),
+						 errmsg("column reference \"%s\" is ambiguous",
+								NameListToString(cref->fields)),
+						 errdetail("The qualified identifier can be column reference or schema variable reference"),
+						 parser_errposition(pstate, cref->location)));
+		}
+
+		if (OidIsValid(varid))
+			node = makeParamSchemaVariable(pstate,
+										   varid, typid, typmod, collid,
+										   attrname, cref->location);
+	}
+
 	/*
 	 * Throw error if no translation found.
 	 */
@@ -824,6 +916,74 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
 	return node;
 }
 
+/*
+ * Generate param variable for reference to schema variable
+ */
+static Node *
+makeParamSchemaVariable(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;
+
+	/*
+	 * There are two access to schema variables - direct, used by simple
+	 * plpgsql expressions, where there are not necessary to emulate
+	 * stability. Buffered access is used elsewhere. We should to ensure
+	 * stable values, and because schema variables are global, then we should
+	 * to work with copied values instead direct access to variables. For
+	 * direct access the varid is best for access. For buffered access we need
+	 * to assign index to buffer - later, when we will know what variables are
+	 * used. Now, we just remember, so we use schema variables.
+	 */
+	pstate->p_hasSchemaVariables = true;
+
+	if (attrname != NULL)
+	{
+		TupleDesc	tupdesc;
+		int			i;
+
+		tupdesc = lookup_rowtype_tupdesc(typid, typmod);
+
+		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)
 {
@@ -1726,6 +1886,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:
@@ -1734,6 +1895,7 @@ transformSubLink(ParseState *pstate, SubLink *sublink)
 			break;
 		case EXPR_KIND_COLUMN_DEFAULT:
 		case EXPR_KIND_FUNCTION_DEFAULT:
+		case EXPR_KIND_VARIABLE_DEFAULT:
 			err = _("cannot use subquery in DEFAULT expression");
 			break;
 		case EXPR_KIND_INDEX_EXPRESSION:
@@ -3064,6 +3226,7 @@ ParseExprKindName(ParseExprKind exprKind)
 			return "CHECK";
 		case EXPR_KIND_COLUMN_DEFAULT:
 		case EXPR_KIND_FUNCTION_DEFAULT:
+		case EXPR_KIND_VARIABLE_DEFAULT:
 			return "DEFAULT";
 		case EXPR_KIND_INDEX_EXPRESSION:
 			return "index expression";
@@ -3089,6 +3252,8 @@ ParseExprKindName(ParseExprKind exprKind)
 			return "GENERATED AS";
 		case EXPR_KIND_CYCLE_MARK:
 			return "CYCLE";
+		case EXPR_KIND_LET_TARGET:
+			return "LET";
 
 			/*
 			 * There is intentionally no default: case here, so that the
diff --git a/src/backend/parser/parse_func.c b/src/backend/parser/parse_func.c
index baac089d68..99f7993ea7 100644
--- a/src/backend/parser/parse_func.c
+++ b/src/backend/parser/parse_func.c
@@ -2501,6 +2501,7 @@ check_srf_call_placement(ParseState *pstate, Node *last_srf, int location)
 			break;
 		case EXPR_KIND_COLUMN_DEFAULT:
 		case EXPR_KIND_FUNCTION_DEFAULT:
+		case EXPR_KIND_VARIABLE_DEFAULT:
 			err = _("set-returning functions are not allowed in DEFAULT expressions");
 			break;
 		case EXPR_KIND_INDEX_EXPRESSION:
@@ -2539,6 +2540,9 @@ check_srf_call_placement(ParseState *pstate, Node *last_srf, int location)
 		case EXPR_KIND_CYCLE_MARK:
 			errkind = true;
 			break;
+		case EXPR_KIND_LET_TARGET:
+			err = _("set-returning functions are not allowed in CALL arguments");
+			break;
 
 			/*
 			 * There is intentionally no default: case here, so that the
diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c
index 6e8fbc4780..26acb72a53 100644
--- a/src/backend/parser/parse_target.c
+++ b/src/backend/parser/parse_target.c
@@ -89,7 +89,9 @@ transformTargetEntry(ParseState *pstate,
 		 * through unmodified.  (transformExpr will throw the appropriate
 		 * error if we're disallowing it.)
 		 */
-		if (exprKind == EXPR_KIND_UPDATE_SOURCE && IsA(node, SetToDefault))
+		if ((exprKind == EXPR_KIND_UPDATE_SOURCE ||
+			 exprKind == EXPR_KIND_LET_TARGET)
+			&& IsA(node, SetToDefault))
 			expr = node;
 		else
 			expr = transformExpr(pstate, node, exprKind);
diff --git a/src/backend/parser/parser.c b/src/backend/parser/parser.c
index 875de7ba28..0f499712d8 100644
--- a/src/backend/parser/parser.c
+++ b/src/backend/parser/parser.c
@@ -61,7 +61,8 @@ raw_parser(const char *str, RawParseMode mode)
 			MODE_PLPGSQL_EXPR,	/* RAW_PARSE_PLPGSQL_EXPR */
 			MODE_PLPGSQL_ASSIGN1,	/* RAW_PARSE_PLPGSQL_ASSIGN1 */
 			MODE_PLPGSQL_ASSIGN2,	/* RAW_PARSE_PLPGSQL_ASSIGN2 */
-			MODE_PLPGSQL_ASSIGN3	/* RAW_PARSE_PLPGSQL_ASSIGN3 */
+			MODE_PLPGSQL_ASSIGN3,	/* RAW_PARSE_PLPGSQL_ASSIGN3 */
+			MODE_PLPGSQL_LET	/* RAW_PARSE_PLPGSQL_LET */
 		};
 
 		yyextra.have_lookahead = true;
diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
index da78f02775..702c01d907 100644
--- a/src/backend/rewrite/rewriteHandler.c
+++ b/src/backend/rewrite/rewriteHandler.c
@@ -25,6 +25,7 @@
 #include "access/table.h"
 #include "catalog/dependency.h"
 #include "catalog/pg_type.h"
+#include "catalog/pg_variable.h"
 #include "commands/trigger.h"
 #include "foreign/fdwapi.h"
 #include "miscadmin.h"
@@ -3665,6 +3666,39 @@ RewriteQuery(Query *parsetree, List *rewrite_events)
 		}
 	}
 
+	/*
+	 * Rewrite SetToDefault by default expression of Let statement.
+	 */
+	if (event == CMD_SELECT && OidIsValid(parsetree->resultVariable))
+	{
+		Oid			resultVariable = parsetree->resultVariable;
+
+		if (list_length(parsetree->targetList) == 1 &&
+			IsA(((TargetEntry *) linitial(parsetree->targetList))->expr, SetToDefault))
+		{
+			Variable		var;
+			TargetEntry	   *tle;
+			TargetEntry	   *newtle;
+			Expr		   *defexpr;
+
+			/* Read schema variable metadata with defexpr */
+			initVariable(&var, resultVariable, false, false);
+
+			if (var.has_defexpr)
+				defexpr = (Expr *) var.defexpr;
+			else
+				defexpr = (Expr *) makeNullConst(var.typid, var.typmod, var.collation);
+
+			tle = (TargetEntry *) linitial(parsetree->targetList);
+			newtle = makeTargetEntry(defexpr,
+									  tle->resno,
+									  pstrdup(tle->resname),
+									  false);
+
+			parsetree->targetList = list_make1(newtle);
+		}
+	}
+
 	/*
 	 * If the statement is an insert, update, or delete, adjust its targetlist
 	 * as needed, and then fire INSERT/UPDATE/DELETE rules on it.
diff --git a/src/backend/rewrite/rowsecurity.c b/src/backend/rewrite/rowsecurity.c
index fc26cb23a2..df9b0e39ee 100644
--- a/src/backend/rewrite/rowsecurity.c
+++ b/src/backend/rewrite/rowsecurity.c
@@ -213,10 +213,10 @@ get_row_security_policies(Query *root, RangeTblEntry *rte, int rt_index,
 	}
 
 	/*
-	 * For SELECT, UPDATE and DELETE, add security quals to enforce the USING
-	 * policies.  These security quals control access to existing table rows.
-	 * Restrictive policies are combined together using AND, and permissive
-	 * policies are combined together using OR.
+	 * For SELECT, LET, UPDATE and DELETE, add security quals to enforce the
+	 * USING policies.  These security quals control access to existing table
+	 * rows. Restrictive policies are combined together using AND, and
+	 * permissive policies are combined together using OR.
 	 */
 
 	get_policies_for_relation(rel, commandType, user_id, &permissive_policies,
diff --git a/src/backend/tcop/dest.c b/src/backend/tcop/dest.c
index 1dfadfa8e1..c676cb19fc 100644
--- a/src/backend/tcop/dest.c
+++ b/src/backend/tcop/dest.c
@@ -37,6 +37,7 @@
 #include "executor/functions.h"
 #include "executor/tqueue.h"
 #include "executor/tstoreReceiver.h"
+#include "executor/svariableReceiver.h"
 #include "libpq/libpq.h"
 #include "libpq/pqformat.h"
 #include "utils/portal.h"
@@ -152,6 +153,9 @@ CreateDestReceiver(CommandDest dest)
 
 		case DestTupleQueue:
 			return CreateTupleQueueDestReceiver(NULL);
+
+		case DestVariable:
+			return CreateVariableDestReceiver();
 	}
 
 	/* should never get here */
@@ -207,6 +211,7 @@ EndCommand(const QueryCompletion *qc, CommandDest dest, bool force_undecorated_o
 		case DestSQLFunction:
 		case DestTransientRel:
 		case DestTupleQueue:
+		case DestVariable:
 			break;
 	}
 }
@@ -252,6 +257,7 @@ NullCommand(CommandDest dest)
 		case DestSQLFunction:
 		case DestTransientRel:
 		case DestTupleQueue:
+		case DestVariable:
 			break;
 	}
 }
@@ -295,6 +301,7 @@ ReadyForQuery(CommandDest dest)
 		case DestSQLFunction:
 		case DestTransientRel:
 		case DestTupleQueue:
+		case DestVariable:
 			break;
 	}
 }
diff --git a/src/backend/tcop/pquery.c b/src/backend/tcop/pquery.c
index 44f5fe8fc9..26d084382e 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_schema_variables = 0;
+	qd->schema_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 16c6f17e23..58d7c7f705 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -48,6 +48,7 @@
 #include "commands/proclang.h"
 #include "commands/publicationcmds.h"
 #include "commands/schemacmds.h"
+#include "commands/schema_variable.h"
 #include "commands/seclabel.h"
 #include "commands/sequence.h"
 #include "commands/subscriptioncmds.h"
@@ -186,6 +187,7 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree)
 		case T_CreateRangeStmt:
 		case T_CreateRoleStmt:
 		case T_CreateSchemaStmt:
+		case T_CreateSchemaVarStmt:
 		case T_CreateSeqStmt:
 		case T_CreateStatsStmt:
 		case T_CreateStmt:
@@ -237,6 +239,7 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree)
 
 		case T_CallStmt:
 		case T_DoStmt:
+		case T_LetStmt:
 			{
 				/*
 				 * Commands inside the DO block or the called procedure might
@@ -1029,6 +1032,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,
@@ -1352,6 +1360,10 @@ ProcessUtilitySlow(ParseState *pstate,
 				}
 				break;
 
+			case T_CreateSchemaVarStmt:
+				address = DefineSchemaVariable(pstate, (CreateSchemaVarStmt *) parsetree);
+				break;
+
 				/*
 				 * ************* object creation / destruction **************
 				 */
@@ -2135,6 +2147,10 @@ UtilityContainsQuery(Node *parsetree)
 				return UtilityContainsQuery(qry->utilityStmt);
 			return qry;
 
+		case T_LetStmt:
+			qry = castNode(Query, ((LetStmt *) parsetree)->query);
+			return qry;
+
 		default:
 			return NULL;
 	}
@@ -2276,6 +2292,9 @@ AlterObjectTypeCommandTag(ObjectType objtype)
 		case OBJECT_STATISTIC_EXT:
 			tag = CMDTAG_ALTER_STATISTICS;
 			break;
+		case OBJECT_VARIABLE:
+			tag = CMDTAG_ALTER_VARIABLE;
+			break;
 		default:
 			tag = CMDTAG_UNKNOWN;
 			break;
@@ -2326,6 +2345,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:
 			{
@@ -2580,6 +2603,9 @@ CreateCommandTag(Node *parsetree)
 				case OBJECT_STATISTIC_EXT:
 					tag = CMDTAG_DROP_STATISTICS;
 					break;
+				case OBJECT_VARIABLE:
+					tag = CMDTAG_DROP_VARIABLE;
+					break;
 				default:
 					tag = CMDTAG_UNKNOWN;
 			}
@@ -2868,6 +2894,9 @@ CreateCommandTag(Node *parsetree)
 				case DISCARD_SEQUENCES:
 					tag = CMDTAG_DISCARD_SEQUENCES;
 					break;
+				case DISCARD_VARIABLES:
+					tag = CMDTAG_DISCARD_VARIABLES;
+					break;
 				default:
 					tag = CMDTAG_UNKNOWN;
 			}
@@ -3148,6 +3177,10 @@ CreateCommandTag(Node *parsetree)
 			}
 			break;
 
+		case T_CreateSchemaVarStmt:
+			tag = CMDTAG_CREATE_VARIABLE;
+			break;
+
 		default:
 			elog(WARNING, "unrecognized node type: %d",
 				 (int) nodeTag(parsetree));
@@ -3195,6 +3228,7 @@ GetCommandLogLevel(Node *parsetree)
 			break;
 
 		case T_PLAssignStmt:
+		case T_LetStmt:
 			lev = LOGSTMT_ALL;
 			break;
 
diff --git a/src/backend/utils/adt/acl.c b/src/backend/utils/adt/acl.c
index 7861a0a613..a1152a4fa7 100644
--- a/src/backend/utils/adt/acl.c
+++ b/src/backend/utils/adt/acl.c
@@ -306,6 +306,12 @@ aclparse(const char *s, AclItem *aip)
 			case ACL_CONNECT_CHR:
 				read = ACL_CONNECT;
 				break;
+			case ACL_READ_CHR:
+				read = ACL_READ;
+				break;
+			case ACL_WRITE_CHR:
+				read = ACL_WRITE;
+				break;
 			case 'R':			/* ignore old RULE privileges */
 				read = 0;
 				break;
@@ -794,6 +800,10 @@ acldefault(ObjectType objtype, Oid ownerId)
 			world_default = ACL_USAGE;
 			owner_default = ACL_ALL_RIGHTS_TYPE;
 			break;
+		case OBJECT_VARIABLE:
+			world_default = ACL_NO_RIGHTS;
+			owner_default = ACL_ALL_RIGHTS_VARIABLE;
+			break;
 		default:
 			elog(ERROR, "unrecognized objtype: %d", (int) objtype);
 			world_default = ACL_NO_RIGHTS;	/* keep compiler quiet */
@@ -888,6 +898,9 @@ acldefault_sql(PG_FUNCTION_ARGS)
 		case 'T':
 			objtype = OBJECT_TYPE;
 			break;
+		case 'V':
+			objtype = OBJECT_VARIABLE;
+			break;
 		default:
 			elog(ERROR, "unrecognized objtype abbreviation: %c", objtypec);
 	}
@@ -1604,6 +1617,10 @@ convert_priv_string(text *priv_type_text)
 		return ACL_CONNECT;
 	if (pg_strcasecmp(priv_type, "RULE") == 0)
 		return 0;				/* ignore old RULE privileges */
+	if (pg_strcasecmp(priv_type, "READ") == 0)
+		return ACL_READ;
+	if (pg_strcasecmp(priv_type, "WRITE") == 0)
+		return ACL_WRITE;
 
 	ereport(ERROR,
 			(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
@@ -1698,6 +1715,10 @@ convert_aclright_to_string(int aclright)
 			return "TEMPORARY";
 		case ACL_CONNECT:
 			return "CONNECT";
+		case ACL_READ:
+			return "READ";
+		case ACL_WRITE:
+			return "WRITE";
 		default:
 			elog(ERROR, "unrecognized aclright: %d", aclright);
 			return NULL;
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 0a4fa93d01..60150d9af8 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"
@@ -7864,6 +7865,14 @@ get_parameter(Param *param, deparse_context *context)
 		return;
 	}
 
+	/* translate paramvarid to schema variable name */
+	if (param->paramkind == PARAM_VARIABLE)
+	{
+		appendStringInfo(context->buf, "%s",
+						 schema_variable_get_name(param->paramvarid));
+		return;
+	}
+
 	/*
 	 * If it's an external parameter, see if the outermost namespace provides
 	 * function argument names.
diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c
index 6bba5f8ec4..d8c8ad7ff4 100644
--- a/src/backend/utils/cache/lsyscache.c
+++ b/src/backend/utils/cache/lsyscache.c
@@ -35,6 +35,7 @@
 #include "catalog/pg_statistic.h"
 #include "catalog/pg_transform.h"
 #include "catalog/pg_type.h"
+#include "catalog/pg_variable.h"
 #include "miscadmin.h"
 #include "nodes/makefuncs.h"
 #include "utils/array.h"
@@ -1860,6 +1861,18 @@ get_relname_relid(const char *relname, Oid relnamespace)
 						   ObjectIdGetDatum(relnamespace));
 }
 
+/*
+ * get_varname_varid
+ *		Given name and namespace of variable, look up the OID.
+ */
+Oid
+get_varname_varid(const char *varname, Oid varnamespace)
+{
+	return GetSysCacheOid2(VARIABLENAMENSP, Anum_pg_variable_oid,
+						   PointerGetDatum(varname),
+						   ObjectIdGetDatum(varnamespace));
+}
+
 #ifdef NOT_USED
 /*
  * get_relnatts
diff --git a/src/backend/utils/cache/syscache.c b/src/backend/utils/cache/syscache.c
index e4dc4ee34e..2cb9a74ca8 100644
--- a/src/backend/utils/cache/syscache.c
+++ b/src/backend/utils/cache/syscache.c
@@ -73,6 +73,7 @@
 #include "catalog/pg_ts_template.h"
 #include "catalog/pg_type.h"
 #include "catalog/pg_user_mapping.h"
+#include "catalog/pg_variable.h"
 #include "lib/qunique.h"
 #include "utils/catcache.h"
 #include "utils/rel.h"
@@ -991,6 +992,28 @@ static const struct cachedesc cacheinfo[] = {
 			0
 		},
 		2
+	},
+	{VariableRelationId,		/* VARIABLENAMENSP */
+		VariableNameNspIndexId,
+		2,
+		{
+			Anum_pg_variable_varname,
+			Anum_pg_variable_varnamespace,
+			0,
+			0
+		},
+		8
+	},
+	{VariableRelationId,		/* VARIABLEOID */
+		VariableObjectIndexId,
+		1,
+		{
+			Anum_pg_variable_oid,
+			0,
+			0,
+			0
+		},
+		8
 	}
 };
 
diff --git a/src/backend/utils/fmgr/fmgr.c b/src/backend/utils/fmgr/fmgr.c
index b6835c2c4c..e8f3f709f4 100644
--- a/src/backend/utils/fmgr/fmgr.c
+++ b/src/backend/utils/fmgr/fmgr.c
@@ -1913,15 +1913,19 @@ get_call_expr_arg_stable(Node *expr, int argnum)
 	arg = (Node *) list_nth(args, argnum);
 
 	/*
-	 * Either a true Const or an external Param will have a value that doesn't
-	 * change during the execution of the query.  In future we might want to
-	 * consider other cases too, e.g. now().
+	 * Either a true Const or an external Param or variable will have a value
+	 * that doesn't change during the execution of the query.  In future we
+	 * might want to consider other cases too, e.g. now().
 	 */
 	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/pg_dump/common.c b/src/bin/pg_dump/common.c
index 1a261a5545..191b606077 100644
--- a/src/bin/pg_dump/common.c
+++ b/src/bin/pg_dump/common.c
@@ -260,6 +260,9 @@ getSchemaData(Archive *fout, int *numTablesPtr)
 	pg_log_info("reading subscriptions");
 	getSubscriptions(fout);
 
+	pg_log_info("reading variables");
+	getVariables(fout);
+
 	*numTablesPtr = numTables;
 	return tblinfo;
 }
diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index 3bc86635f7..6ec33c185e 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -113,12 +113,14 @@ typedef struct _restoreOptions
 	int			selFunction;
 	int			selTrigger;
 	int			selTable;
+	int			selVariable;
 	SimpleStringList indexNames;
 	SimpleStringList functionNames;
 	SimpleStringList schemaNames;
 	SimpleStringList schemaExcludeNames;
 	SimpleStringList triggerNames;
 	SimpleStringList tableNames;
+	SimpleStringList variableNames;
 
 	int			useDB;
 	ConnParams	cparams;		/* parameters to use if useDB */
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 86de26a4bf..a72e04978f 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -2948,6 +2948,14 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 					!simple_string_list_member(&ropt->triggerNames, te->tag))
 					return 0;
 			}
+			else if (strcmp(te->desc, "VARIABLE") == 0)
+			{
+				if (!ropt->selVariable)
+					return 0;
+				if (ropt->variableNames.head != NULL &&
+					!simple_string_list_member(&ropt->variableNames, te->tag))
+					return 0;
+			}
 			else
 				return 0;
 		}
@@ -3434,6 +3442,7 @@ _getObjectDescription(PQExpBuffer buf, TocEntry *te)
 		strcmp(type, "TEXT SEARCH DICTIONARY") == 0 ||
 		strcmp(type, "TEXT SEARCH CONFIGURATION") == 0 ||
 		strcmp(type, "STATISTICS") == 0 ||
+		strcmp(type, "VARIABLE") == 0 ||
 	/* non-schema-specified objects */
 		strcmp(type, "DATABASE") == 0 ||
 		strcmp(type, "PROCEDURAL LANGUAGE") == 0 ||
@@ -3619,7 +3628,8 @@ _printTocEntry(ArchiveHandle *AH, TocEntry *te, bool isData)
 			strcmp(te->desc, "SERVER") == 0 ||
 			strcmp(te->desc, "STATISTICS") == 0 ||
 			strcmp(te->desc, "PUBLICATION") == 0 ||
-			strcmp(te->desc, "SUBSCRIPTION") == 0)
+			strcmp(te->desc, "SUBSCRIPTION") == 0 ||
+			strcmp(te->desc, "VARIABLE") == 0)
 		{
 			PQExpBuffer temp = createPQExpBuffer();
 
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index d0ea489614..2f1a0cd75d 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -271,6 +271,7 @@ static void dumpPolicy(Archive *fout, const PolicyInfo *polinfo);
 static void dumpPublication(Archive *fout, const PublicationInfo *pubinfo);
 static void dumpPublicationTable(Archive *fout, const PublicationRelInfo *pubrinfo);
 static void dumpSubscription(Archive *fout, const SubscriptionInfo *subinfo);
+static void dumpVariable(Archive *fout, const VariableInfo * varinfo);
 static void dumpDatabase(Archive *AH);
 static void dumpDatabaseConfig(Archive *AH, PQExpBuffer outbuf,
 							   const char *dbname, Oid dboid);
@@ -4639,6 +4640,232 @@ get_next_possible_free_pg_type_oid(Archive *fout, PQExpBuffer upgrade_query)
 	return next_possible_free_oid;
 }
 
+/*
+ * getVariables
+ *	  get information about variables
+ */
+void
+getVariables(Archive *fout)
+{
+	DumpOptions *dopt = fout->dopt;
+	PQExpBuffer query;
+	PQExpBuffer acl_subquery = createPQExpBuffer();
+	PQExpBuffer racl_subquery = createPQExpBuffer();
+	PQExpBuffer init_acl_subquery = createPQExpBuffer();
+	PQExpBuffer init_racl_subquery = createPQExpBuffer();
+	PGresult   *res;
+	VariableInfo *varinfo;
+	int			i_tableoid;
+	int			i_oid;
+	int			i_varname;
+	int			i_varnamespace;
+	int			i_vartype;
+	int			i_vartypname;
+	int			i_vardefexpr;
+	int			i_rolname;
+	int			i_varacl;
+	int			i_rvaracl;
+	int			i_initvaracl;
+	int			i_initrvaracl;
+	int			i_vareoxaction;
+	int			i_varisnotnull;
+	int			i_varisimmutable;
+	int			i,
+				ntups;
+
+	if (fout->remoteVersion < 130000)
+		return;
+
+	acl_subquery = createPQExpBuffer();
+	racl_subquery = createPQExpBuffer();
+	init_acl_subquery = createPQExpBuffer();
+	init_racl_subquery = createPQExpBuffer();
+
+	buildACLQueries(acl_subquery, racl_subquery, init_acl_subquery,
+					init_racl_subquery, "v.varacl", "v.varowner", "'V'",
+					dopt->binary_upgrade);
+
+	query = createPQExpBuffer();
+
+	resetPQExpBuffer(query);
+
+	/* Get the variables in current database. */
+	appendPQExpBuffer(query,
+					  "SELECT v.tableoid, v.oid, v.varname, "
+					  "v.vareoxaction, "
+					  "v.varnamespace, "
+					  "(%s varowner) AS rolname, "
+					  "%s as varacl, "
+					  "%s as rvaracl, "
+					  "%s as initvaracl, "
+					  "%s as initrvaracl, "
+					  "v.vartype, "
+					  "pg_catalog.format_type(v.vartype, v.vartypmod) as vartypname, "
+					  "v.varisnotnull, "
+					  "v.varisimmutable, "
+					  "pg_catalog.pg_get_expr(v.vardefexpr,0) as vardefexpr "
+					  "FROM pg_variable v "
+					  "LEFT JOIN pg_init_privs pip "
+					  "ON (v.oid = pip.objoid "
+					  "AND pip.classoid = 'pg_variable'::regclass "
+					  "AND pip.objsubid = 0)",
+					  username_subquery,
+					  acl_subquery->data,
+					  racl_subquery->data,
+					  init_acl_subquery->data,
+					  init_racl_subquery->data);
+
+	destroyPQExpBuffer(acl_subquery);
+	destroyPQExpBuffer(racl_subquery);
+	destroyPQExpBuffer(init_acl_subquery);
+	destroyPQExpBuffer(init_racl_subquery);
+
+	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+
+	ntups = PQntuples(res);
+
+	i_tableoid = PQfnumber(res, "tableoid");
+	i_oid = PQfnumber(res, "oid");
+	i_varname = PQfnumber(res, "varname");
+	i_varnamespace = PQfnumber(res, "varnamespace");
+	i_rolname = PQfnumber(res, "rolname");
+	i_vartype = PQfnumber(res, "vartype");
+	i_vartypname = PQfnumber(res, "vartypname");
+	i_vareoxaction = PQfnumber(res, "vareoxaction");
+	i_vardefexpr = PQfnumber(res, "vardefexpr");
+	i_varacl = PQfnumber(res, "varacl");
+	i_rvaracl = PQfnumber(res, "rvaracl");
+	i_initvaracl = PQfnumber(res, "initvaracl");
+	i_initrvaracl = PQfnumber(res, "initrvaracl");
+	i_varisnotnull = PQfnumber(res, "varisnotnull");
+	i_varisimmutable = PQfnumber(res, "varisimmutable");
+
+	varinfo = pg_malloc(ntups * sizeof(VariableInfo));
+
+	for (i = 0; i < ntups; i++)
+	{
+		TypeInfo   *vtype;
+
+		varinfo[i].dobj.objType = DO_VARIABLE;
+		varinfo[i].dobj.catId.tableoid =
+			atooid(PQgetvalue(res, i, i_tableoid));
+		varinfo[i].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
+		AssignDumpId(&varinfo[i].dobj);
+		varinfo[i].dobj.name = pg_strdup(PQgetvalue(res, i, i_varname));
+		varinfo[i].dobj.namespace =
+			findNamespace(atooid(PQgetvalue(res, i, i_varnamespace)));
+
+		varinfo[i].rolname = pg_strdup(PQgetvalue(res, i, i_rolname));
+		varinfo[i].vartype = atooid(PQgetvalue(res, i, i_vartype));
+		varinfo[i].vartypname = pg_strdup(PQgetvalue(res, i, i_vartypname));
+
+		varinfo[i].vareoxaction = pg_strdup(PQgetvalue(res, i, i_vareoxaction));
+
+		varinfo[i].varacl = pg_strdup(PQgetvalue(res, i, i_varacl));
+		varinfo[i].rvaracl = pg_strdup(PQgetvalue(res, i, i_rvaracl));
+		varinfo[i].initvaracl = pg_strdup(PQgetvalue(res, i, i_initvaracl));
+		varinfo[i].initrvaracl = pg_strdup(PQgetvalue(res, i, i_initrvaracl));
+
+		varinfo[i].varisnotnull = *(PQgetvalue(res, i, i_varisnotnull)) == 't';
+		varinfo[i].varisimmutable = *(PQgetvalue(res, i, i_varisimmutable)) == 't';
+
+		/* Decide whether we want to dump it */
+		selectDumpableObject(&(varinfo[i].dobj), fout);
+
+		/* Do not try to dump ACL if no ACL exists. */
+		if (PQgetisnull(res, i, i_varacl) && PQgetisnull(res, i, i_rvaracl) &&
+			PQgetisnull(res, i, i_initvaracl) &&
+			PQgetisnull(res, i, i_initrvaracl))
+			varinfo[i].dobj.dump &= ~DUMP_COMPONENT_ACL;
+
+		if (PQgetisnull(res, i, i_vardefexpr))
+			varinfo[i].vardefexpr = NULL;
+		else
+			varinfo[i].vardefexpr = pg_strdup(PQgetvalue(res, i, i_vardefexpr));
+
+		if (strlen(varinfo[i].rolname) == 0)
+			pg_log_warning("owner of variable \"%s\" appears to be invalid",
+						   varinfo[i].dobj.name);
+
+		/* Decide whether we want to dump it */
+		selectDumpableObject(&(varinfo[i].dobj), fout);
+
+		vtype = findTypeByOid(varinfo[i].vartype);
+		addObjectDependency(&varinfo[i].dobj, vtype->dobj.dumpId);
+	}
+	PQclear(res);
+
+	destroyPQExpBuffer(query);
+}
+
+/*
+ * dumpVariable
+ *	  dump the definition of the given variables
+ */
+static void
+dumpVariable(Archive *fout, const VariableInfo * varinfo)
+{
+	DumpOptions *dopt = fout->dopt;
+
+	PQExpBuffer delq;
+	PQExpBuffer query;
+	const char *varname;
+	const char *vartypname;
+	const char *vardefexpr;
+	const char *vareoxaction;
+	const char *varisnotnull;
+	const char *varisimmutable;
+
+	/* Skip if not to be dumped */
+	if (!varinfo->dobj.dump || dopt->dataOnly)
+		return;
+
+	delq = createPQExpBuffer();
+	query = createPQExpBuffer();
+
+	varname = fmtQualifiedDumpable(varinfo);
+	vartypname = varinfo->vartypname;
+	vardefexpr = varinfo->vardefexpr;
+	vareoxaction = varinfo->vareoxaction;
+	varisnotnull = varinfo->varisnotnull ? " NOT NULL" : "";
+	varisimmutable = varinfo->varisimmutable ? "IMMUTABLE " : "";
+
+	appendPQExpBuffer(delq, "DROP VARIABLE %s;\n",
+					  varname);
+
+	appendPQExpBuffer(query, "CREATE %sVARIABLE %s AS %s%s",
+					  varisimmutable, varname, vartypname, varisnotnull);
+
+	if (vardefexpr)
+		appendPQExpBuffer(query, " DEFAULT %s",
+						  vardefexpr);
+
+	if (strcmp(vareoxaction, "d") == 0)
+		appendPQExpBuffer(query, " ON COMMIT DROP");
+	else if (strcmp(vareoxaction, "r") == 0)
+		appendPQExpBuffer(query, " ON TRANSACTION END RESET");
+
+	appendPQExpBuffer(query, ";\n");
+
+	if (varinfo->dobj.dump & DUMP_COMPONENT_DEFINITION)
+		ArchiveEntry(fout, varinfo->dobj.catId, varinfo->dobj.dumpId,
+					 ARCHIVE_OPTS(.tag = varinfo->dobj.name,
+								  .namespace = varinfo->dobj.namespace->dobj.name,
+								  .owner = varinfo->rolname,
+								  .description = "VARIABLE",
+								  .section = SECTION_PRE_DATA,
+								  .createStmt = query->data,
+								  .dropStmt = delq->data));
+
+	if (varinfo->dobj.dump & DUMP_COMPONENT_COMMENT)
+		dumpComment(fout, "VARIABLE", varname,
+					NULL, varinfo->rolname,
+					varinfo->dobj.catId, 0, varinfo->dobj.dumpId);
+
+	destroyPQExpBuffer(delq);
+	destroyPQExpBuffer(query);
+}
+
 static void
 binary_upgrade_set_type_oids_by_type_oid(Archive *fout,
 										 PQExpBuffer upgrade_buffer,
@@ -10472,6 +10699,9 @@ dumpDumpableObject(Archive *fout, const DumpableObject *dobj)
 		case DO_SUBSCRIPTION:
 			dumpSubscription(fout, (const SubscriptionInfo *) dobj);
 			break;
+		case DO_VARIABLE:
+			dumpVariable(fout, (VariableInfo *) dobj);
+			break;
 		case DO_PRE_DATA_BOUNDARY:
 		case DO_POST_DATA_BOUNDARY:
 			/* never dumped, nothing to do */
@@ -18625,6 +18855,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 			case DO_CONVERSION:
 			case DO_TABLE:
 			case DO_TABLE_ATTACH:
+			case DO_VARIABLE:
 			case DO_ATTRDEF:
 			case DO_PROCLANG:
 			case DO_CAST:
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 5340843081..9eced17344 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -52,6 +52,7 @@ typedef enum
 	DO_TABLE,
 	DO_TABLE_ATTACH,
 	DO_ATTRDEF,
+	DO_VARIABLE,
 	DO_INDEX,
 	DO_INDEX_ATTACH,
 	DO_STATSEXT,
@@ -81,7 +82,7 @@ typedef enum
 	DO_POLICY,
 	DO_PUBLICATION,
 	DO_PUBLICATION_REL,
-	DO_SUBSCRIPTION
+	DO_SUBSCRIPTION,
 } DumpableObjectType;
 
 /* component types of an object which can be selected for dumping */
@@ -643,6 +644,25 @@ typedef struct _SubscriptionInfo
 	char	   *subpublications;
 } SubscriptionInfo;
 
+/*
+ * The VariableInfo struct is used to represent schema variables
+ */
+typedef struct _VariableInfo
+{
+	DumpableObject dobj;
+	Oid			vartype;
+	char	   *vartypname;
+	char	   *rolname;		/* name of owner, or empty string */
+	char	   *vareoxaction;
+	char	   *vardefexpr;
+	char	   *varacl;
+	char	   *rvaracl;
+	char	   *initvaracl;
+	char	   *initrvaracl;
+	bool		varisnotnull;
+	bool		varisimmutable;
+}			VariableInfo;
+
 /*
  * We build an array of these with an entry for each object that is an
  * extension member according to pg_depend.
@@ -734,5 +754,6 @@ extern PublicationInfo *getPublications(Archive *fout,
 extern void getPublicationTables(Archive *fout, TableInfo tblinfo[],
 								 int numTables);
 extern void getSubscriptions(Archive *fout);
+extern void getVariables(Archive *fout);
 
 #endif							/* PG_DUMP_H */
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index 46461fb6a1..cb76d70f2d 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -66,6 +66,7 @@ enum dbObjectTypePriorities
 	PRIO_TABLE_ATTACH,
 	PRIO_DUMMY_TYPE,
 	PRIO_ATTRDEF,
+	PRIO_VARIABLE,
 	PRIO_BLOB,
 	PRIO_PRE_DATA_BOUNDARY,		/* boundary! */
 	PRIO_TABLE_DATA,
@@ -106,6 +107,7 @@ static const int dbObjectTypePriority[] =
 	PRIO_TABLE,					/* DO_TABLE */
 	PRIO_TABLE_ATTACH,			/* DO_TABLE_ATTACH */
 	PRIO_ATTRDEF,				/* DO_ATTRDEF */
+	PRIO_VARIABLE,				/* DO_VARIABLE */
 	PRIO_INDEX,					/* DO_INDEX */
 	PRIO_INDEX_ATTACH,			/* DO_INDEX_ATTACH */
 	PRIO_STATSEXT,				/* DO_STATSEXT */
@@ -1492,6 +1494,10 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
 					 "POST-DATA BOUNDARY  (ID %d)",
 					 obj->dumpId);
 			return;
+		case DO_VARIABLE:
+			snprintf(buf, bufsize,
+					 "VARIABLE %s  (ID %d OID %u)",
+					 obj->name, obj->dumpId, obj->catId.oid);
 	}
 	/* shouldn't get here */
 	snprintf(buf, bufsize,
diff --git a/src/bin/pg_dump/pg_restore.c b/src/bin/pg_dump/pg_restore.c
index 589b4aed53..b844aceb5c 100644
--- a/src/bin/pg_dump/pg_restore.c
+++ b/src/bin/pg_dump/pg_restore.c
@@ -101,6 +101,7 @@ main(int argc, char **argv)
 		{"trigger", 1, NULL, 'T'},
 		{"use-list", 1, NULL, 'L'},
 		{"username", 1, NULL, 'U'},
+		{"variable", 1, NULL, 'A'},
 		{"verbose", 0, NULL, 'v'},
 		{"single-transaction", 0, NULL, '1'},
 
@@ -148,7 +149,7 @@ main(int argc, char **argv)
 		}
 	}
 
-	while ((c = getopt_long(argc, argv, "acCd:ef:F:h:I:j:lL:n:N:Op:P:RsS:t:T:U:vwWx1",
+	while ((c = getopt_long(argc, argv, "A:acCd:ef:F:h:I:j:lL:n:N:Op:P:RsS:t:T:U:vwWx1",
 							cmdopts, NULL)) != -1)
 	{
 		switch (c)
@@ -156,6 +157,11 @@ main(int argc, char **argv)
 			case 'a':			/* Dump data only */
 				opts->dataOnly = 1;
 				break;
+			case 'A':			/* vAriable */
+				opts->selTypes = 1;
+				opts->selVariable = 1;
+				simple_string_list_append(&opts->variableNames, optarg);
+				break;
 			case 'c':			/* clean (i.e., drop) schema prior to create */
 				opts->dropSchema = 1;
 				break;
@@ -473,6 +479,7 @@ usage(const char *progname)
 
 	printf(_("\nOptions controlling the restore:\n"));
 	printf(_("  -a, --data-only              restore only the data, no schema\n"));
+	printf(_("  -A, --variable=NAME          restore named schema variable\n"));
 	printf(_("  -c, --clean                  clean (drop) database objects before recreating\n"));
 	printf(_("  -C, --create                 create the target database\n"));
 	printf(_("  -e, --exit-on-error          exit on error, default is to continue\n"));
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index 86113df29c..fa4d7efc2b 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -2774,6 +2774,30 @@ my %tests = (
 		},
 	},
 
+	'CREATE VARIABLE test_variable' => {
+		all_runs     => 1,
+		catch_all    => 'CREATE ... commands',
+		create_order => 61,
+		create_sql   => 'CREATE VARIABLE dump_test.variable1 AS integer DEFAULT 0;',
+		regexp => qr/^
+			\QCREATE VARIABLE dump_test.variable1 AS integer DEFAULT 0;\E/xm,
+		like =>
+		  { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, },
+	},
+
+	'CREATE IMMUTABLE VARIABLE test_variable' => {
+		all_runs     => 1,
+		catch_all    => 'CREATE ... commands',
+		create_order => 61,
+		create_sql   => 'CREATE IMMUTABLE VARIABLE dump_test.variable2 AS integer DEFAULT 0;',
+		regexp => qr/^
+			\QCREATE IMMUTABLE VARIABLE dump_test.variable2 AS integer DEFAULT 0;\E/xm,
+		like =>
+		  { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, },
+	},
+
 	'CREATE VIEW test_view' => {
 		create_order => 61,
 		create_sql   => 'CREATE VIEW dump_test.test_view
diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index 543401c6d6..6662f298dd 100644
--- a/src/bin/psql/command.c
+++ b/src/bin/psql/command.c
@@ -928,6 +928,9 @@ exec_command_d(PsqlScanState scan_state, bool active_branch, const char *cmd)
 						break;
 				}
 				break;
+			case 'V':			/* Variables */
+				success = listVariables(pattern, show_verbose);
+				break;
 			case 'x':			/* Extensions */
 				if (show_verbose)
 					success = listExtensionContents(pattern);
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index fdc2a89085..41140a8b24 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -5067,6 +5067,88 @@ listSchemas(const char *pattern, bool verbose, bool showSystem)
 	return true;
 }
 
+/*
+ * \dV
+ *
+ * listVariables()
+ */
+bool
+listVariables(const char *pattern, bool verbose)
+{
+	PQExpBufferData buf;
+	PGresult   *res;
+	printQueryOpt myopt = pset.popt;
+	static const bool translate_columns[] = {false, false, false, false, false, false, false, false};
+
+	initPQExpBuffer(&buf);
+
+	printfPQExpBuffer(&buf,
+					  "SELECT n.nspname as \"%s\",\n"
+					  "  v.varname as \"%s\",\n"
+					  "  pg_catalog.format_type(v.vartype, v.vartypmod) as \"%s\",\n"
+					  "  NOT v.varisnotnull as \"%s\",\n"
+					  "  NOT v.varisimmutable as \"%s\",\n"
+					  "  pg_catalog.pg_get_expr(v.vardefexpr, 0) as \"%s\",\n"
+					  "  pg_catalog.pg_get_userbyid(v.varowner) as \"%s\",\n"
+					  "  CASE v.vareoxaction\n"
+					  "    WHEN 'd' THEN 'ON COMMIT DROP'\n"
+					  "    WHEN 'r' THEN 'ON TRANSACTION END RESET' END as \"%s\"\n",
+					  gettext_noop("Schema"),
+					  gettext_noop("Name"),
+					  gettext_noop("Type"),
+					  gettext_noop("Is nullable"),
+					  gettext_noop("Is mutable"),
+					  gettext_noop("Default"),
+					  gettext_noop("Owner"),
+					  gettext_noop("Transactional end action"));
+
+	appendPQExpBufferStr(&buf,
+						 "\nFROM pg_catalog.pg_variable v"
+						 "\n     LEFT JOIN pg_catalog.pg_namespace n ON n.oid = v.varnamespace");
+
+	appendPQExpBufferStr(&buf, "\nWHERE true\n");
+	if (!pattern)
+		appendPQExpBufferStr(&buf, "      AND n.nspname <> 'pg_catalog'\n"
+							 "      AND n.nspname <> 'information_schema'\n");
+
+	processSQLNamePattern(pset.db, &buf, pattern, true, false,
+						  "n.nspname", "v.varname", NULL,
+						  "pg_catalog.pg_variable_is_visible(v.oid)");
+
+	appendPQExpBufferStr(&buf, "ORDER BY 1,2;");
+
+	res = PSQLexec(buf.data);
+	termPQExpBuffer(&buf);
+	if (!res)
+		return false;
+
+	/*
+	 * Most functions in this file are content to print an empty table when
+	 * there are no matching objects.  We intentionally deviate from that
+	 * here, but only in !quiet mode, for historical reasons.
+	 */
+	if (PQntuples(res) == 0 && !pset.quiet)
+	{
+		if (pattern)
+			pg_log_error("Did not find any schema variable named \"%s\".",
+						 pattern);
+		else
+			pg_log_error("Did not find any schema variables.");
+	}
+	else
+	{
+		myopt.nullPrint = NULL;
+		myopt.title = _("List of variables");
+		myopt.translate_header = true;
+		myopt.translate_columns = translate_columns;
+		myopt.n_translate_columns = lengthof(translate_columns);
+
+		printQuery(res, &myopt, pset.queryFout, false, pset.logfile);
+	}
+
+	PQclear(res);
+	return true;
+}
 
 /*
  * \dFp
diff --git a/src/bin/psql/describe.h b/src/bin/psql/describe.h
index 71b320f1fc..a86fccc0d7 100644
--- a/src/bin/psql/describe.h
+++ b/src/bin/psql/describe.h
@@ -139,5 +139,7 @@ extern bool listOpFamilyOperators(const char *accessMethod_pattern,
 extern bool listOpFamilyFunctions(const char *access_method_pattern,
 								  const char *family_pattern, bool verbose);
 
+/* \dV */
+extern bool listVariables(const char *pattern, bool varbose);
 
 #endif							/* DESCRIBE_H */
diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c
index 36501d5e2b..8812a58333 100644
--- a/src/bin/psql/help.c
+++ b/src/bin/psql/help.c
@@ -264,6 +264,7 @@ slashUsage(unsigned short int pager)
 	fprintf(output, _("  \\dT[S+] [PATTERN]      list data types\n"));
 	fprintf(output, _("  \\du[S+] [PATTERN]      list roles\n"));
 	fprintf(output, _("  \\dv[S+] [PATTERN]      list views\n"));
+	fprintf(output, _("  \\dV     [PATTERN]      list variables\n"));
 	fprintf(output, _("  \\dx[+]  [PATTERN]      list extensions\n"));
 	fprintf(output, _("  \\dX     [PATTERN]      list extended statistics\n"));
 	fprintf(output, _("  \\dy     [PATTERN]      list event triggers\n"));
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index d34271e3b8..a624944086 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -629,6 +629,13 @@ static const SchemaQuery Query_for_list_of_collations = {
 	.result = "pg_catalog.quote_ident(c.collname)",
 };
 
+static const SchemaQuery Query_for_list_of_variables = {
+	.min_server_version = 120000,
+	.catname = "pg_catalog.pg_variable v",
+	.viscondition = "pg_catalog.pg_variable_is_visible(v.oid)",
+	.namespace = "v.varnamespace",
+	.result = "pg_catalog.quote_ident(v.varname)",
+};
 
 /*
  * Queries to get lists of names of various kinds of things, possibly
@@ -1107,6 +1114,7 @@ static const pgsql_thing_t words_after_create[] = {
 																	 * TABLE ... */
 	{"USER", Query_for_list_of_roles " UNION SELECT 'MAPPING FOR'"},
 	{"USER MAPPING FOR", NULL, NULL, NULL},
+	{"VARIABLE", NULL, NULL, &Query_for_list_of_variables},
 	{"VIEW", NULL, NULL, &Query_for_list_of_views},
 	{NULL}						/* end of list */
 };
@@ -1505,8 +1513,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", "LISTEN", "LOAD", "LOCK",
-		"MOVE", "NOTIFY", "PREPARE",
+		"FETCH", "GRANT", "IMPORT FOREIGN SCHEMA", "INSERT", "LET", "LISTEN", "LOAD",
+		"LOCK", "MOVE", "NOTIFY", "PREPARE",
 		"REASSIGN", "REFRESH MATERIALIZED VIEW", "REINDEX", "RELEASE",
 		"RESET", "REVOKE", "ROLLBACK",
 		"SAVEPOINT", "SECURITY LABEL", "SELECT", "SET", "SHOW", "START",
@@ -1525,7 +1533,7 @@ psql_completion(const char *text, int start, int end)
 		"\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL",
 		"\\dm", "\\dn", "\\do", "\\dO", "\\dp", "\\dP", "\\dPi", "\\dPt",
 		"\\drds", "\\dRs", "\\dRp", "\\ds", "\\dS",
-		"\\dt", "\\dT", "\\dv", "\\du", "\\dx", "\\dX", "\\dy",
+		"\\dt", "\\dT", "\\dv", "\\du", "\\dx", "\\dX", "\\dy", "\\dV",
 		"\\e", "\\echo", "\\ef", "\\elif", "\\else", "\\encoding",
 		"\\endif", "\\errverbose", "\\ev",
 		"\\f",
@@ -1930,6 +1938,9 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH_QUERY(Query_for_list_of_alter_system_set_vars);
 	else if (Matches("ALTER", "SYSTEM", "SET", MatchAny))
 		COMPLETE_WITH("TO");
+	/* ALTER VARIABLE <name> */
+	else if (Matches("ALTER", "VARIABLE", MatchAny))
+		COMPLETE_WITH("OWNER TO", "RENAME TO", "SET SCHEMA");
 	/* ALTER VIEW <name> */
 	else if (Matches("ALTER", "VIEW", MatchAny))
 		COMPLETE_WITH("ALTER COLUMN", "OWNER TO", "RENAME",
@@ -2709,7 +2720,7 @@ psql_completion(const char *text, int start, int end)
 /* CREATE TABLE --- is allowed inside CREATE SCHEMA, so use TailMatches */
 	/* Complete "CREATE TEMP/TEMPORARY" with the possible temp objects */
 	else if (TailMatches("CREATE", "TEMP|TEMPORARY"))
-		COMPLETE_WITH("SEQUENCE", "TABLE", "VIEW");
+		COMPLETE_WITH("SEQUENCE", "TABLE", "VIEW", "VARIABLE");
 	/* Complete "CREATE UNLOGGED" with TABLE or MATVIEW */
 	else if (TailMatches("CREATE", "UNLOGGED"))
 		COMPLETE_WITH("TABLE", "MATERIALIZED VIEW");
@@ -2997,6 +3008,14 @@ psql_completion(const char *text, int start, int end)
 		else if (TailMatches("=", MatchAnyExcept("*)")))
 			COMPLETE_WITH(",", ")");
 	}
+/* CREATE VARIABLE --- is allowed inside CREATE SCHEMA, so use TailMatches */
+	/* Complete CREATE VARIABLE <name> with AS */
+	else if (TailMatches("CREATE", "VARIABLE", MatchAny) ||
+			 TailMatches("TEMP|TEMPORARY", "VARIABLE", MatchAny))
+			COMPLETE_WITH("AS");
+	else if (TailMatches("VARIABLE", MatchAny, "AS"))
+			/* Complete CREATE VARIABLE <name> with AS types */
+			COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes, NULL);
 
 /* CREATE VIEW --- is allowed inside CREATE SCHEMA, so use TailMatches */
 	/* Complete CREATE [ OR REPLACE ] VIEW <name> with AS */
@@ -3111,7 +3130,7 @@ psql_completion(const char *text, int start, int end)
 
 /* DISCARD */
 	else if (Matches("DISCARD"))
-		COMPLETE_WITH("ALL", "PLANS", "SEQUENCES", "TEMP");
+		COMPLETE_WITH("ALL", "PLANS", "SEQUENCES", "TEMP", "VARIABLES");
 
 /* DO */
 	else if (Matches("DO"))
@@ -3217,6 +3236,12 @@ psql_completion(const char *text, int start, int end)
 	else if (Matches("DROP", "RULE", MatchAny, "ON", MatchAny))
 		COMPLETE_WITH("CASCADE", "RESTRICT");
 
+	/* DROP VARIABLE */
+	else if (Matches("DROP", "VARIABLE"))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables, NULL);
+	else if (Matches("DROP", "VARIABLE", MatchAny))
+		COMPLETE_WITH("CASCADE", "RESTRICT");
+
 /* EXECUTE */
 	else if (Matches("EXECUTE"))
 		COMPLETE_WITH_QUERY(Query_for_list_of_prepared_statements);
@@ -3388,6 +3413,7 @@ psql_completion(const char *text, int start, int end)
 									   " UNION SELECT 'ALL ROUTINES IN SCHEMA'"
 									   " UNION SELECT 'ALL SEQUENCES IN SCHEMA'"
 									   " UNION SELECT 'ALL TABLES IN SCHEMA'"
+									   " UNION SELECT 'ALL VARIABLES IN SCHEMA'"
 									   " UNION SELECT 'DATABASE'"
 									   " UNION SELECT 'DOMAIN'"
 									   " UNION SELECT 'FOREIGN DATA WRAPPER'"
@@ -3401,14 +3427,16 @@ psql_completion(const char *text, int start, int end)
 									   " UNION SELECT 'SEQUENCE'"
 									   " UNION SELECT 'TABLE'"
 									   " UNION SELECT 'TABLESPACE'"
-									   " UNION SELECT 'TYPE'");
+									   " UNION SELECT 'TYPE'"
+									   " UNION SELECT 'VARIABLE'");
 	}
 	else if (TailMatches("GRANT|REVOKE", MatchAny, "ON", "ALL"))
 		COMPLETE_WITH("FUNCTIONS IN SCHEMA",
 					  "PROCEDURES IN SCHEMA",
 					  "ROUTINES IN SCHEMA",
 					  "SEQUENCES IN SCHEMA",
-					  "TABLES IN SCHEMA");
+					  "TABLES IN SCHEMA",
+					  "VARIABLES IN SCHEMA");
 	else if (TailMatches("GRANT|REVOKE", MatchAny, "ON", "FOREIGN"))
 		COMPLETE_WITH("DATA WRAPPER", "SERVER");
 
@@ -3442,6 +3470,8 @@ psql_completion(const char *text, int start, int end)
 			COMPLETE_WITH_QUERY(Query_for_list_of_tablespaces);
 		else if (TailMatches("TYPE"))
 			COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes, NULL);
+		else if (TailMatches("VARIABLE"))
+			COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables, NULL);
 		else if (TailMatches("GRANT", MatchAny, MatchAny, MatchAny))
 			COMPLETE_WITH("TO");
 		else
@@ -3605,7 +3635,7 @@ psql_completion(const char *text, int start, int end)
 
 /* PREPARE xx AS */
 	else if (Matches("PREPARE", MatchAny, "AS"))
-		COMPLETE_WITH("SELECT", "UPDATE", "INSERT", "DELETE FROM");
+		COMPLETE_WITH("SELECT", "UPDATE", "INSERT", "DELETE FROM", "LET");
 
 /*
  * PREPARE TRANSACTION is missing on purpose. It's intended for transaction
@@ -3879,6 +3909,14 @@ psql_completion(const char *text, int start, int end)
 	else if (TailMatches("UPDATE", MatchAny, "SET", MatchAnyExcept("*=")))
 		COMPLETE_WITH("=");
 
+/* LET --- can be inside EXPLAIN, PREPARE etc */
+	/* If prev. word is LET suggest a list of variables */
+	else if (TailMatches("LET"))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables, NULL);
+	/* Complete LET <variable> with "=" */
+	else if (TailMatches("LET", MatchAny))
+		COMPLETE_WITH("=");
+
 /* USER MAPPING */
 	else if (Matches("ALTER|CREATE|DROP", "USER", "MAPPING"))
 		COMPLETE_WITH("FOR");
@@ -4040,6 +4078,8 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH_QUERY(Query_for_list_of_roles);
 	else if (TailMatchesCS("\\dv*"))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_views, NULL);
+	else if (TailMatchesCS("\\dV*"))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables, NULL);
 	else if (TailMatchesCS("\\dx*"))
 		COMPLETE_WITH_QUERY(Query_for_list_of_extensions);
 	else if (TailMatchesCS("\\dX*"))
diff --git a/src/include/catalog/dependency.h b/src/include/catalog/dependency.h
index f272e2c99f..e7ce899f0e 100644
--- a/src/include/catalog/dependency.h
+++ b/src/include/catalog/dependency.h
@@ -132,10 +132,11 @@ typedef enum ObjectClass
 	OCLASS_PUBLICATION,			/* pg_publication */
 	OCLASS_PUBLICATION_REL,		/* pg_publication_rel */
 	OCLASS_SUBSCRIPTION,		/* pg_subscription */
-	OCLASS_TRANSFORM			/* pg_transform */
+	OCLASS_TRANSFORM,			/* pg_transform */
+	OCLASS_VARIABLE				/* pg_variable */
 } ObjectClass;
 
-#define LAST_OCLASS		OCLASS_TRANSFORM
+#define LAST_OCLASS		OCLASS_VARIABLE
 
 /* flag bits for performDeletion/performMultipleDeletions: */
 #define PERFORM_DELETION_INTERNAL			0x0001	/* internal action */
diff --git a/src/include/catalog/namespace.h b/src/include/catalog/namespace.h
index aa2774e2d4..e3b22a1537 100644
--- a/src/include/catalog/namespace.h
+++ b/src/include/catalog/namespace.h
@@ -95,6 +95,8 @@ extern Oid	TypenameGetTypid(const char *typname);
 extern Oid	TypenameGetTypidExtended(const char *typname, bool temp_ok);
 extern bool TypeIsVisible(Oid typid);
 
+extern bool VariableIsVisible(Oid varid);
+
 extern FuncCandidateList FuncnameGetCandidates(List *names,
 											   int nargs, List *argnames,
 											   bool expand_variadic,
@@ -162,6 +164,10 @@ extern void SetTempNamespaceState(Oid tempNamespaceId,
 								  Oid tempToastNamespaceId);
 extern void ResetTempTableNamespace(void);
 
+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_uniq);
+
 extern OverrideSearchPath *GetOverrideSearchPath(MemoryContext context);
 extern OverrideSearchPath *CopyOverrideSearchPath(OverrideSearchPath *path);
 extern bool OverrideSearchPathMatchesCurrent(OverrideSearchPath *path);
diff --git a/src/include/catalog/pg_default_acl.h b/src/include/catalog/pg_default_acl.h
index 156fc0712d..22839aa356 100644
--- a/src/include/catalog/pg_default_acl.h
+++ b/src/include/catalog/pg_default_acl.h
@@ -68,6 +68,7 @@ DECLARE_UNIQUE_INDEX_PKEY(pg_default_acl_oid_index, 828, on pg_default_acl using
 #define DEFACLOBJ_FUNCTION		'f' /* function */
 #define DEFACLOBJ_TYPE			'T' /* type */
 #define DEFACLOBJ_NAMESPACE		'n' /* namespace */
+#define DEFACLOBJ_VARIABLE		'V' /* variable */
 
 #endif							/* EXPOSE_TO_CLIENT_CODE */
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f4957653ae..b556c7e99e 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6178,6 +6178,9 @@
   proname => 'pg_collation_is_visible', procost => '10', provolatile => 's',
   prorettype => 'bool', proargtypes => 'oid',
   prosrc => 'pg_collation_is_visible' },
+{ oid => '9221', descr => 'is schema variable visible in search path?',
+  proname => 'pg_variable_is_visible', procost => '10', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'oid', prosrc => 'pg_variable_is_visible' },
 
 { oid => '2854', descr => 'get OID of current session\'s temp schema, if any',
   proname => 'pg_my_temp_schema', provolatile => 's', proparallel => 'r',
diff --git a/src/include/catalog/pg_variable.h b/src/include/catalog/pg_variable.h
new file mode 100644
index 0000000000..30f84f6b6d
--- /dev/null
+++ b/src/include/catalog/pg_variable.h
@@ -0,0 +1,117 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_variable.h
+ *	  definition of schema variables system catalog (pg_variables)
+ *
+ *
+ * Portions Copyright (c) 1996-2020, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/pg_variable.h
+ *
+ * NOTES
+ *	  The Catalog.pm module reads this file and derives schema
+ *	  information.
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PG_VARIABLE_H
+#define PG_VARIABLE_H
+
+#include "catalog/genbki.h"
+#include "catalog/objectaddress.h"
+#include "catalog/pg_variable_d.h"
+#include "utils/acl.h"
+
+/* ----------------
+ *		pg_variable definition.  cpp turns this into
+ *		typedef struct FormData_pg_variable
+ * ----------------
+ */
+CATALOG(pg_variable,9222,VariableRelationId)
+{
+	Oid			oid;			/* oid */
+	NameData	varname;		/* variable name */
+	Oid			varnamespace;	/* OID of namespace containing variable class */
+	Oid			vartype;		/* OID of entry in pg_type for variable's type */
+	int32		vartypmod;		/* typmode for variable's type */
+	Oid			varowner;		/* class owner */
+	Oid			varcollation;	/* variable collation */
+	bool		varisnotnull;	/* Don't allow NULL */
+	bool		varisimmutable;	/* Don't allow changes */
+	char		vareoxaction;	/* action on transaction end */
+
+#ifdef CATALOG_VARLEN			/* variable-length fields start here */
+
+	/* list of expression trees for variable default (NULL if none) */
+	pg_node_tree vardefexpr BKI_DEFAULT(_null_);
+
+	aclitem		varacl[1] BKI_DEFAULT(_null_);	/* access permissions */
+
+#endif
+} FormData_pg_variable;
+
+typedef enum VariableEOXActionCodes
+{
+	VARIABLE_EOX_CODE_NOOP = 'n',	/* NOOP */
+	VARIABLE_EOX_CODE_DROP = 'd',	/* ON COMMIT DROP */
+	VARIABLE_EOX_CODE_RESET = 'r',	/* ON COMMIT RESET */
+}			VariableEOXActionCodes;
+
+/* ----------------
+ *		Form_pg_variable corresponds to a pointer to a tuple with
+ *		the format of pg_variable relation.
+ * ----------------
+ */
+typedef FormData_pg_variable * Form_pg_variable;
+
+DECLARE_UNIQUE_INDEX_PKEY(pg_variable_oid_index, 9223, on pg_variable using btree(oid oid_ops));
+#define VariableObjectIndexId 9223
+
+DECLARE_UNIQUE_INDEX(pg_variable_varname_nsp_index, 9224, on pg_variable using btree(varname name_ops, varnamespace oid_ops));
+#define VariableNameNspIndexId  9224
+
+typedef struct Variable
+{
+	Oid			oid;
+	char	   *name;
+	Oid			namespace;
+	Oid			typid;
+	int32		typmod;
+	Oid			owner;
+	Oid			collation;
+	bool		is_not_null;
+	bool		is_immutable;
+	VariableEOXAction eoxaction;
+	bool		has_defexpr;
+	Node	   *defexpr;
+	Acl		   *acl;
+} Variable;
+
+/* returns fields from pg_variable table */
+extern char *get_schema_variable_name(Oid varid);
+extern void get_schema_variable_type_typmod_collid(Oid varid,
+												   Oid *typid,
+												   int32 *typmod,
+												   Oid *collid);
+
+/* returns name of variable based on current search path */
+extern char *schema_variable_get_name(Oid varid);
+
+extern void initVariable(Variable *var,
+						 Oid varid,
+						 bool missing_ok,
+						 bool fast_only);
+extern ObjectAddress VariableCreate(const char *varName,
+									Oid varNamespace,
+									Oid varType,
+									int32 varTypmod,
+									Oid varOwner,
+									Oid varCollation,
+									Node *varDefexpr,
+									VariableEOXAction eoxaction,
+									bool is_not_null,
+									bool if_not_exists,
+									bool is_immutable);
+
+#endif							/* PG_VARIABLE_H */
diff --git a/src/include/commands/schema_variable.h b/src/include/commands/schema_variable.h
new file mode 100644
index 0000000000..5350829669
--- /dev/null
+++ b/src/include/commands/schema_variable.h
@@ -0,0 +1,44 @@
+/*-------------------------------------------------------------------------
+ *
+ * schemavariable.h
+ *	  prototypes for schemavariable.c.
+ *
+ *
+ * Portions Copyright (c) 1996-2020, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/commands/schemavariable.h
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef SCHEMAVARIABLE_H
+#define SCHEMAVARIABLE_H
+
+#include "catalog/objectaddress.h"
+#include "catalog/pg_variable.h"
+#include "nodes/params.h"
+#include "nodes/parsenodes.h"
+#include "nodes/plannodes.h"
+#include "tcop/cmdtag.h"
+#include "utils/queryenvironment.h"
+
+extern void ResetSchemaVariableCache(void);
+
+extern void RemoveSchemaVariable(Oid varid);
+extern ObjectAddress DefineSchemaVariable(ParseState *pstate, CreateSchemaVarStmt * stmt);
+
+extern Datum GetSchemaVariable(Oid varid, bool *isNull, Oid expected_typid, bool copy);
+extern Datum CopySchemaVariable(Oid varid, bool *isNull, Oid *typid);
+extern void SetSchemaVariable(Oid varid, Datum value, bool isNull, Oid typid);
+extern void SetSchemaVariableWithSecurityCheck(Oid varid, Datum value, bool isNull, Oid typid);
+
+extern void ExecuteLetStmt(ParseState *pstate, LetStmt *stmt, ParamListInfo params,
+						   QueryEnvironment *queryEnv, QueryCompletion *qc);
+
+extern void register_variable_on_commit_action(Oid varid, VariableEOXAction action);
+
+extern void AtPreEOXact_SchemaVariable_on_commit_actions(bool isCommit);
+extern void AtEOXact_SchemaVariable_on_commit_actions(bool isCommit);
+
+#endif
diff --git a/src/include/executor/execExpr.h b/src/include/executor/execExpr.h
index 785600d04d..13b7902633 100644
--- a/src/include/executor/execExpr.h
+++ b/src/include/executor/execExpr.h
@@ -158,6 +158,7 @@ typedef enum ExprEvalOp
 	EEOP_PARAM_EXEC,
 	EEOP_PARAM_EXTERN,
 	EEOP_PARAM_CALLBACK,
+	EEOP_PARAM_VARIABLE,
 
 	/* return CaseTestExpr value */
 	EEOP_CASE_TESTVAL,
@@ -380,6 +381,13 @@ typedef struct ExprEvalStep
 			Oid			paramtype;	/* OID of parameter's datatype */
 		}			param;
 
+		/* for EEOP_PARAM_VARIABLE */
+		struct
+		{
+			Oid			varid;	/* OID of assigned variable */
+			Oid			vartype;	/* OID of parameter's datatype */
+		}			vparam;
+
 		/* for EEOP_PARAM_CALLBACK */
 		struct
 		{
@@ -735,6 +743,8 @@ extern void ExecEvalParamExec(ExprState *state, ExprEvalStep *op,
 							  ExprContext *econtext);
 extern void ExecEvalParamExtern(ExprState *state, ExprEvalStep *op,
 								ExprContext *econtext);
+extern void ExecEvalParamVariable(ExprState *state, ExprEvalStep *op,
+								  ExprContext *econtext);
 extern void ExecEvalSQLValueFunction(ExprState *state, ExprEvalStep *op);
 extern void ExecEvalCurrentOfExpr(ExprState *state, ExprEvalStep *op);
 extern void ExecEvalNextValueExpr(ExprState *state, ExprEvalStep *op);
diff --git a/src/include/executor/execdesc.h b/src/include/executor/execdesc.h
index 017ad87117..c85e7ee89d 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 schema variables buffer */
+	int			num_schema_variables;
+	SchemaVariableValue *schema_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..5305936547
--- /dev/null
+++ b/src/include/executor/svariableReceiver.h
@@ -0,0 +1,25 @@
+/*-------------------------------------------------------------------------
+ *
+ * svariableReceiver.h
+ *	  prototypes for svariableReceiver.c
+ *
+ *
+ * Portions Copyright (c) 1996-2020, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/executor/svariableReceiver.h
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef SVARIABLE_RECEIVER_H
+#define SVARIABLE_RECEIVER_H
+
+#include "tcop/dest.h"
+
+
+extern DestReceiver *CreateVariableDestReceiver(void);
+
+extern void SetVariableDestReceiverParams(DestReceiver *self, Oid varid);
+
+#endif							/* SVARIABLE_RECEIVER_H */
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index e7ae21c023..71ddc97d85 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -541,6 +541,18 @@ typedef struct AsyncRequest
 	TupleTableSlot *result;		/* Result (NULL if no more tuples) */
 } AsyncRequest;
 
+/* ----------------
+ * SchemaVariableValue
+ * ----------------
+ */
+typedef struct SchemaVariableValue
+{
+	Oid			varid;
+	Oid			typid;
+	bool		isnull;
+	Datum		value;
+}			SchemaVariableValue;
+
 /* ----------------
  *	  EState information
  *
@@ -592,6 +604,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 schema variables */
+	int			es_num_schema_variables;
+
+	/* array of copied values of schema variables */
+	SchemaVariableValue *es_schema_variables;
+
 	QueryEnvironment *es_queryEnv;	/* query environment */
 
 	/* Other working state: */
diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h
index d9e417bcd7..209bab6a72 100644
--- a/src/include/nodes/nodes.h
+++ b/src/include/nodes/nodes.h
@@ -359,6 +359,7 @@ typedef enum NodeTag
 	T_CreateTableAsStmt,
 	T_CreateSeqStmt,
 	T_AlterSeqStmt,
+	T_CreateSchemaVarStmt,
 	T_VariableSetStmt,
 	T_VariableShowStmt,
 	T_DiscardStmt,
@@ -432,6 +433,7 @@ typedef enum NodeTag
 	T_AlterCollationStmt,
 	T_CallStmt,
 	T_AlterStatsStmt,
+	T_LetStmt,
 
 	/*
 	 * TAGS FOR PARSE TREE NODES (parsenodes.h)
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 7a44bccdd3..c622c0efbd 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -92,7 +92,9 @@ typedef uint32 AclMode;			/* a bitmask of privilege bits */
 #define ACL_CREATE		(1<<9)	/* for namespaces and databases */
 #define ACL_CREATE_TEMP (1<<10) /* for databases */
 #define ACL_CONNECT		(1<<11) /* for databases */
-#define N_ACL_RIGHTS	12		/* 1 plus the last 1<<x */
+#define ACL_READ		(1<<12) /* for variables */
+#define ACL_WRITE		(1<<13) /* for variables */
+#define N_ACL_RIGHTS	14		/* 1 plus the last 1<<x */
 #define ACL_NO_RIGHTS	0
 /* Currently, SELECT ... FOR [KEY] UPDATE/SHARE requires UPDATE privileges */
 #define ACL_SELECT_FOR_UPDATE	ACL_UPDATE
@@ -129,7 +131,7 @@ typedef struct Query
 
 	int			resultRelation; /* rtable index of target relation for
 								 * INSERT/UPDATE/DELETE; 0 for SELECT */
-
+	Oid			resultVariable;	/* target variable of LET statement */
 	bool		hasAggs;		/* has aggregates in tlist or havingQual */
 	bool		hasWindowFuncs; /* has window functions in tlist */
 	bool		hasTargetSRFs;	/* has set-returning functions in tlist */
@@ -139,6 +141,7 @@ typedef struct Query
 	bool		hasModifyingCTE;	/* has INSERT/UPDATE/DELETE in WITH */
 	bool		hasForUpdate;	/* FOR [KEY] UPDATE/SHARE was specified */
 	bool		hasRowSecurity; /* rewriter has applied some RLS policy */
+	bool		hasSchemaVariables;	/* uses schema variables */
 
 	bool		isReturn;		/* is a RETURN statement */
 
@@ -1615,6 +1618,20 @@ typedef struct UpdateStmt
 	WithClause *withClause;		/* WITH clause */
 } UpdateStmt;
 
+/* ----------------------
+ *		Let Statement
+ * ----------------------
+ */
+typedef struct LetStmt
+{
+	NodeTag		type;
+	List	   *target;			/* target variable */
+	Node	   *query;			/* source expression */
+	bool		plpgsql_mode;	/* true, when command will be executed (parsed)
+								 * by plpgsql runtime */
+	int			location;
+}			LetStmt;
+
 /* ----------------------
  *		Select Statement
  *
@@ -1823,6 +1840,7 @@ typedef enum ObjectType
 	OBJECT_TSTEMPLATE,
 	OBJECT_TYPE,
 	OBJECT_USER_MAPPING,
+	OBJECT_VARIABLE,
 	OBJECT_VIEW
 } ObjectType;
 
@@ -2637,6 +2655,23 @@ typedef struct AlterSeqStmt
 	bool		missing_ok;		/* skip error if a role is missing? */
 } AlterSeqStmt;
 
+/* ----------------------
+ *		{Create|Alter} VARIABLE Statement
+ * ----------------------
+ */
+typedef struct CreateSchemaVarStmt
+{
+	NodeTag		type;
+	RangeVar   *variable;		/* the variable to create */
+	TypeName   *typeName;		/* the type of variable */
+	CollateClause *collClause;
+	Node	   *defexpr;		/* default expression */
+	VariableEOXAction eoxaction;	/* on commit action */
+	bool		if_not_exists;	/* do nothing if it already exists */
+	bool		is_not_null;	/* Disallow nulls */
+	bool		is_immutable;	/* Don't allow changes */
+}			CreateSchemaVarStmt;
+
 /* ----------------------
  *		Create {Aggregate|Operator|Type} Statement
  * ----------------------
@@ -3402,7 +3437,8 @@ typedef enum DiscardMode
 	DISCARD_ALL,
 	DISCARD_PLANS,
 	DISCARD_SEQUENCES,
-	DISCARD_TEMP
+	DISCARD_TEMP,
+	DISCARD_VARIABLES
 } DiscardMode;
 
 typedef struct DiscardStmt
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index a65bda7e3c..12e4fe45ad 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -129,6 +129,8 @@ typedef struct PlannerGlobal
 	char		maxParallelHazard;	/* worst PROPARALLEL hazard level */
 
 	PartitionDirectory partition_directory; /* partition descriptors */
+
+	List	   *schemaVariables;	/* list of used schema variables */
 } PlannerGlobal;
 
 /* macro for fetching the Plan associated with a SubPlan node */
@@ -349,6 +351,7 @@ struct PlannerInfo
 										 * pseudoconstant = true */
 	bool		hasAlternativeSubPlans; /* true if we've made any of those */
 	bool		hasRecursion;	/* true if planning a recursive WITH item */
+	bool		hasSchemaVariables;	/* true if schema variables was used */
 
 	/*
 	 * Information about aggregates. Filled by preprocess_aggrefs().
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index d671328dfd..63503fa9dd 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -43,7 +43,7 @@ typedef struct PlannedStmt
 {
 	NodeTag		type;
 
-	CmdType		commandType;	/* select|insert|update|delete|utility */
+	CmdType		commandType;	/* select|let|insert|update|delete|utility */
 
 	uint64		queryId;		/* query identifier (copied from Query) */
 
@@ -85,6 +85,8 @@ typedef struct PlannedStmt
 
 	Node	   *utilityStmt;	/* non-null if this is utility stmt */
 
+	List	   *schemaVariables;	/* list of 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 9ae851d847..3370f5c550 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -43,7 +43,10 @@ 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
+ * persistent/temporary variable.
+ */
 typedef enum OnCommitAction
 {
 	ONCOMMIT_NOOP,				/* No ON COMMIT clause (do nothing) */
@@ -52,6 +55,13 @@ typedef enum OnCommitAction
 	ONCOMMIT_DROP				/* ON COMMIT DROP */
 } OnCommitAction;
 
+typedef enum VariableEOXAction
+{
+	VARIABLE_EOX_NOOP,			/* Do nothing */
+	VARIABLE_EOX_DROP,			/* ON TRANSACTION END DROP */
+	VARIABLE_EOX_RESET			/* ON TRANSACTION END RESET */
+}			VariableEOXAction;
+
 /*
  * RangeVar - range variable, used in FROM clauses
  *
@@ -252,13 +262,17 @@ 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 a access to schema variable
+ *				paramid holds varid.
  */
 typedef enum ParamKind
 {
 	PARAM_EXTERN,
 	PARAM_EXEC,
 	PARAM_SUBLINK,
-	PARAM_MULTIEXPR
+	PARAM_MULTIEXPR,
+	PARAM_VARIABLE
 } ParamKind;
 
 typedef struct Param
@@ -269,6 +283,7 @@ typedef struct Param
 	Oid			paramtype;		/* pg_type OID of parameter's datatype */
 	int32		paramtypmod;	/* typmod value, if known */
 	Oid			paramcollid;	/* OID of collation, or InvalidOid if none */
+	Oid			paramvarid;		/* OID of schema variable if it is used */
 	int			location;		/* token location, or -1 if unknown */
 } Param;
 
diff --git a/src/include/optimizer/planmain.h b/src/include/optimizer/planmain.h
index bf1adfc52a..4cbcbeba10 100644
--- a/src/include/optimizer/planmain.h
+++ b/src/include/optimizer/planmain.h
@@ -116,4 +116,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 *root);
 
+extern void pull_up_has_schema_variables(PlannerInfo *root);
+
 #endif							/* PLANMAIN_H */
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index f836acf876..82f3bd2eb0 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -237,6 +237,7 @@ PG_KEYWORD("leading", LEADING, RESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("leakproof", LEAKPROOF, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("least", LEAST, COL_NAME_KEYWORD, BARE_LABEL)
 PG_KEYWORD("left", LEFT, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL)
+PG_KEYWORD("let", LET, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("level", LEVEL, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("like", LIKE, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL)
 PG_KEYWORD("limit", LIMIT, RESERVED_KEYWORD, AS_LABEL)
@@ -450,6 +451,8 @@ PG_KEYWORD("validator", VALIDATOR, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("value", VALUE_P, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("values", VALUES, COL_NAME_KEYWORD, BARE_LABEL)
 PG_KEYWORD("varchar", VARCHAR, COL_NAME_KEYWORD, BARE_LABEL)
+PG_KEYWORD("variable", VARIABLE, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("variables", VARIABLES, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("variadic", VARIADIC, RESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("varying", VARYING, UNRESERVED_KEYWORD, AS_LABEL)
 PG_KEYWORD("verbose", VERBOSE, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL)
diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h
index 1500de2dd0..2b7676eb08 100644
--- a/src/include/parser/parse_node.h
+++ b/src/include/parser/parse_node.h
@@ -80,6 +80,8 @@ typedef enum ParseExprKind
 	EXPR_KIND_COPY_WHERE,		/* WHERE condition in COPY FROM */
 	EXPR_KIND_GENERATED_COLUMN, /* generation expression for a column */
 	EXPR_KIND_CYCLE_MARK,		/* cycle mark value */
+	EXPR_KIND_VARIABLE_DEFAULT, /* default value for schema variable */
+	EXPR_KIND_LET_TARGET		/* LET assignment (should be same like UPDATE) */
 } ParseExprKind;

@@ -210,6 +212,7 @@ struct ParseState
 	bool		p_hasTargetSRFs;
 	bool		p_hasSubLinks;
 	bool		p_hasModifyingCTE;
+	bool		p_hasSchemaVariables;
 
 	Node	   *p_last_srf;		/* most recent set-returning func/op found */
 
diff --git a/src/include/parser/parser.h b/src/include/parser/parser.h
index 853b0f1606..587112e526 100644
--- a/src/include/parser/parser.h
+++ b/src/include/parser/parser.h
@@ -33,6 +33,9 @@
  * RAW_PARSE_PLPGSQL_ASSIGNn: parse a PL/pgSQL assignment statement,
  * and return a one-element List containing a RawStmt node.  "n"
  * gives the number of dotted names comprising the target ColumnRef.
+ *
+ * RAW_PARSE_PLPGSQL_LET: parse a LET statement, and return a
+ * one-element List containing a RawStmt node.
  */
 typedef enum
 {
@@ -41,7 +44,8 @@ typedef enum
 	RAW_PARSE_PLPGSQL_EXPR,
 	RAW_PARSE_PLPGSQL_ASSIGN1,
 	RAW_PARSE_PLPGSQL_ASSIGN2,
-	RAW_PARSE_PLPGSQL_ASSIGN3
+	RAW_PARSE_PLPGSQL_ASSIGN3,
+	RAW_PARSE_PLPGSQL_LET
 } RawParseMode;
 
 /* Values for the backslash_quote GUC */
diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h
index 9ba24d4ca9..8a81e62849 100644
--- a/src/include/tcop/cmdtaglist.h
+++ b/src/include/tcop/cmdtaglist.h
@@ -68,6 +68,7 @@ PG_CMDTAG(CMDTAG_ALTER_TRANSFORM, "ALTER TRANSFORM", true, false, false)
 PG_CMDTAG(CMDTAG_ALTER_TRIGGER, "ALTER TRIGGER", true, false, false)
 PG_CMDTAG(CMDTAG_ALTER_TYPE, "ALTER TYPE", true, true, false)
 PG_CMDTAG(CMDTAG_ALTER_USER_MAPPING, "ALTER USER MAPPING", true, false, false)
+PG_CMDTAG(CMDTAG_ALTER_VARIABLE, "ALTER VARIABLE", true, false, false)
 PG_CMDTAG(CMDTAG_ALTER_VIEW, "ALTER VIEW", true, false, false)
 PG_CMDTAG(CMDTAG_ANALYZE, "ANALYZE", false, false, false)
 PG_CMDTAG(CMDTAG_BEGIN, "BEGIN", false, false, false)
@@ -123,6 +124,7 @@ PG_CMDTAG(CMDTAG_CREATE_TRANSFORM, "CREATE TRANSFORM", true, false, false)
 PG_CMDTAG(CMDTAG_CREATE_TRIGGER, "CREATE TRIGGER", true, false, false)
 PG_CMDTAG(CMDTAG_CREATE_TYPE, "CREATE TYPE", true, false, false)
 PG_CMDTAG(CMDTAG_CREATE_USER_MAPPING, "CREATE USER MAPPING", true, false, false)
+PG_CMDTAG(CMDTAG_CREATE_VARIABLE, "CREATE VARIABLE", true, false, false)
 PG_CMDTAG(CMDTAG_CREATE_VIEW, "CREATE VIEW", true, false, false)
 PG_CMDTAG(CMDTAG_DEALLOCATE, "DEALLOCATE", false, false, false)
 PG_CMDTAG(CMDTAG_DEALLOCATE_ALL, "DEALLOCATE ALL", false, false, false)
@@ -133,6 +135,7 @@ PG_CMDTAG(CMDTAG_DISCARD_ALL, "DISCARD ALL", false, false, false)
 PG_CMDTAG(CMDTAG_DISCARD_PLANS, "DISCARD PLANS", false, false, false)
 PG_CMDTAG(CMDTAG_DISCARD_SEQUENCES, "DISCARD SEQUENCES", false, false, false)
 PG_CMDTAG(CMDTAG_DISCARD_TEMP, "DISCARD TEMP", false, false, false)
+PG_CMDTAG(CMDTAG_DISCARD_VARIABLES, "DISCARD VARIABLES", false, false, false)
 PG_CMDTAG(CMDTAG_DO, "DO", false, false, false)
 PG_CMDTAG(CMDTAG_DROP_ACCESS_METHOD, "DROP ACCESS METHOD", true, false, false)
 PG_CMDTAG(CMDTAG_DROP_AGGREGATE, "DROP AGGREGATE", true, false, false)
@@ -175,6 +178,7 @@ PG_CMDTAG(CMDTAG_DROP_TRANSFORM, "DROP TRANSFORM", true, false, false)
 PG_CMDTAG(CMDTAG_DROP_TRIGGER, "DROP TRIGGER", true, false, false)
 PG_CMDTAG(CMDTAG_DROP_TYPE, "DROP TYPE", true, false, false)
 PG_CMDTAG(CMDTAG_DROP_USER_MAPPING, "DROP USER MAPPING", true, false, false)
+PG_CMDTAG(CMDTAG_DROP_VARIABLE, "DROP VARIABLE", true, false, false)
 PG_CMDTAG(CMDTAG_DROP_VIEW, "DROP VIEW", true, false, false)
 PG_CMDTAG(CMDTAG_EXECUTE, "EXECUTE", false, false, false)
 PG_CMDTAG(CMDTAG_EXPLAIN, "EXPLAIN", false, false, false)
@@ -183,6 +187,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 88a7e59de5..e2f348eb33 100644
--- a/src/include/tcop/dest.h
+++ b/src/include/tcop/dest.h
@@ -97,7 +97,8 @@ typedef enum
 	DestCopyOut,				/* results sent to COPY TO code */
 	DestSQLFunction,			/* results sent to SQL-language func mgr */
 	DestTransientRel,			/* results sent to transient relation */
-	DestTupleQueue				/* results sent to tuple queue */
+	DestTupleQueue,				/* results sent to tuple queue */
+	DestVariable				/* results sents to schema variable */
 } CommandDest;
 
 /* ----------------
diff --git a/src/include/utils/acl.h b/src/include/utils/acl.h
index af771c901d..84f670d7f4 100644
--- a/src/include/utils/acl.h
+++ b/src/include/utils/acl.h
@@ -146,9 +146,11 @@ typedef struct ArrayType Acl;
 #define ACL_CREATE_CHR			'C'
 #define ACL_CREATE_TEMP_CHR		'T'
 #define ACL_CONNECT_CHR			'c'
+#define ACL_READ_CHR			'S' /* 'R' is occupated by old RULE priv */
+#define ACL_WRITE_CHR			'W'
 
 /* string holding all privilege code chars, in order by bitmask position */
-#define ACL_ALL_RIGHTS_STR	"arwdDxtXUCTc"
+#define ACL_ALL_RIGHTS_STR	"arwdDxtXUCTcSW"
 
 /*
  * Bitmasks defining "all rights" for each supported object type
@@ -165,6 +167,7 @@ typedef struct ArrayType Acl;
 #define ACL_ALL_RIGHTS_SCHEMA		(ACL_USAGE|ACL_CREATE)
 #define ACL_ALL_RIGHTS_TABLESPACE	(ACL_CREATE)
 #define ACL_ALL_RIGHTS_TYPE			(ACL_USAGE)
+#define ACL_ALL_RIGHTS_VARIABLE		(ACL_READ|ACL_WRITE)
 
 /* operation codes for pg_*_aclmask */
 typedef enum
@@ -259,7 +262,8 @@ extern AclMode pg_foreign_server_aclmask(Oid srv_oid, Oid roleid,
 										 AclMode mask, AclMaskHow how);
 extern AclMode pg_type_aclmask(Oid type_oid, Oid roleid,
 							   AclMode mask, AclMaskHow how);
-
+extern AclMode pg_variable_aclmask(Oid var_oid, Oid roleid,
+								   AclMode mask, AclMaskHow how);
 extern AclResult pg_attribute_aclcheck(Oid table_oid, AttrNumber attnum,
 									   Oid roleid, AclMode mode);
 extern AclResult pg_attribute_aclcheck_ext(Oid table_oid, AttrNumber attnum,
@@ -280,6 +284,7 @@ extern AclResult pg_tablespace_aclcheck(Oid spc_oid, Oid roleid, AclMode mode);
 extern AclResult pg_foreign_data_wrapper_aclcheck(Oid fdw_oid, Oid roleid, AclMode mode);
 extern AclResult pg_foreign_server_aclcheck(Oid srv_oid, Oid roleid, AclMode mode);
 extern AclResult pg_type_aclcheck(Oid type_oid, Oid roleid, AclMode mode);
+extern AclResult pg_variable_aclcheck(Oid type_oid, Oid roleid, AclMode mode);
 
 extern void aclcheck_error(AclResult aclerr, ObjectType objtype,
 						   const char *objectname);
@@ -316,6 +321,7 @@ extern bool pg_extension_ownercheck(Oid ext_oid, Oid roleid);
 extern bool pg_publication_ownercheck(Oid pub_oid, Oid roleid);
 extern bool pg_subscription_ownercheck(Oid sub_oid, Oid roleid);
 extern bool pg_statistics_object_ownercheck(Oid stat_oid, Oid roleid);
+extern bool pg_variable_ownercheck(Oid stat_oid, Oid roleid);
 extern bool has_createrole_privilege(Oid roleid);
 extern bool has_bypassrls_privilege(Oid roleid);
 
diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h
index 77871aaefc..af645a6818 100644
--- a/src/include/utils/lsyscache.h
+++ b/src/include/utils/lsyscache.h
@@ -132,6 +132,7 @@ extern char get_func_prokind(Oid funcid);
 extern bool get_func_leakproof(Oid funcid);
 extern RegProcedure get_func_support(Oid funcid);
 extern Oid	get_relname_relid(const char *relname, Oid relnamespace);
+extern Oid	get_varname_varid(const char *varname, Oid varnamespace);
 extern char *get_rel_name(Oid relid);
 extern Oid	get_rel_namespace(Oid relid);
 extern Oid	get_rel_type_id(Oid relid);
diff --git a/src/include/utils/syscache.h b/src/include/utils/syscache.h
index d74a348600..41544f0d41 100644
--- a/src/include/utils/syscache.h
+++ b/src/include/utils/syscache.h
@@ -109,9 +109,11 @@ enum SysCacheIdentifier
 	TYPENAMENSP,
 	TYPEOID,
 	USERMAPPINGOID,
-	USERMAPPINGUSERSERVER
+	USERMAPPINGUSERSERVER,
+	VARIABLENAMENSP,
+	VARIABLEOID
 
-#define SysCacheSize (USERMAPPINGUSERSERVER + 1)
+#define SysCacheSize (VARIABLEOID + 1)
 };
 
 extern void InitCatalogCache(void);
diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
index b4c70aaa7f..8785f04ea6 100644
--- a/src/pl/plpgsql/src/pl_exec.c
+++ b/src/pl/plpgsql/src/pl_exec.c
@@ -24,6 +24,7 @@
 #include "catalog/pg_proc.h"
 #include "catalog/pg_type.h"
 #include "commands/defrem.h"
+#include "commands/schema_variable.h"
 #include "executor/execExpr.h"
 #include "executor/spi.h"
 #include "executor/tstoreReceiver.h"
@@ -319,6 +320,8 @@ static int	exec_stmt_rollback(PLpgSQL_execstate *estate,
 							   PLpgSQL_stmt_rollback *stmt);
 static int	exec_stmt_set(PLpgSQL_execstate *estate,
 						  PLpgSQL_stmt_set *stmt);
+static int	exec_stmt_let(PLpgSQL_execstate *estate,
+						  PLpgSQL_stmt_let *let);
 
 static void plpgsql_estate_setup(PLpgSQL_execstate *estate,
 								 PLpgSQL_function *func,
@@ -2092,6 +2095,10 @@ exec_stmts(PLpgSQL_execstate *estate, List *stmts)
 				rc = exec_stmt_set(estate, (PLpgSQL_stmt_set *) stmt);
 				break;
 
+			case PLPGSQL_STMT_LET:
+				rc = exec_stmt_let(estate, (PLpgSQL_stmt_let *) stmt);
+				break;
+
 			default:
 				/* point err_stmt to parent, since this one seems corrupt */
 				estate->err_stmt = save_estmt;
@@ -4978,6 +4985,54 @@ exec_stmt_set(PLpgSQL_execstate *estate, PLpgSQL_stmt_set *stmt)
 	return PLPGSQL_RC_OK;
 }
 
+/* ----------
+ * exec_stmt_let			Evaluate an expression and
+ *					put the result into a schema variable.
+ * ----------
+ */
+static int
+exec_stmt_let(PLpgSQL_execstate *estate, PLpgSQL_stmt_let *stmt)
+{
+	bool		isNull;
+	Oid			valtype;
+	int32		valtypmod;
+	Datum		value;
+	Oid			varid;
+
+	List	   *plansources;
+	CachedPlanSource *plansource;
+
+	value = exec_eval_expr(estate,
+						   stmt->expr,
+						   &isNull,
+						   &valtype,
+						   &valtypmod);
+
+	/*
+	 * Oid of target schema variable is stored in Query structure.
+	 * It is safer to read this value directly from plan, then to
+	 * hold this value in plpgsql context, because I don't need to
+	 * solve invalidation of this cached value. Next operations
+	 * are read only without any allocations, so we can expect so
+	 * taking varid from Query should be fast.
+	 */
+	plansources = SPI_plan_get_plan_sources(stmt->expr->plan);
+	if (list_length(plansources) != 1)
+		elog(ERROR, "unexpected length of plansources of query for LET statement");
+
+	plansource = (CachedPlanSource *) linitial(plansources);
+	if (list_length(plansource->query_list) != 1)
+		elog(ERROR, "unexpected length of plansource of query for LET statement");
+
+	varid = linitial_node(Query, plansource->query_list)->resultVariable;
+	if (!OidIsValid(varid))
+		elog(ERROR, "oid of target schema variable is not valid");
+
+	SetSchemaVariableWithSecurityCheck(varid, value, isNull, valtype);
+
+	return PLPGSQL_RC_OK;
+}
+
 /* ----------
  * exec_assign_expr			Put an expression's result into a variable.
  * ----------
diff --git a/src/pl/plpgsql/src/pl_funcs.c b/src/pl/plpgsql/src/pl_funcs.c
index 919b968826..4cd40afa76 100644
--- a/src/pl/plpgsql/src/pl_funcs.c
+++ b/src/pl/plpgsql/src/pl_funcs.c
@@ -290,6 +290,8 @@ plpgsql_stmt_typename(PLpgSQL_stmt *stmt)
 			return "ROLLBACK";
 		case PLPGSQL_STMT_SET:
 			return "SET";
+		case PLPGSQL_STMT_LET:
+			return "LET";
 	}
 
 	return "unknown";
@@ -371,6 +373,7 @@ static void free_call(PLpgSQL_stmt_call *stmt);
 static void free_commit(PLpgSQL_stmt_commit *stmt);
 static void free_rollback(PLpgSQL_stmt_rollback *stmt);
 static void free_set(PLpgSQL_stmt_set *stmt);
+static void free_let(PLpgSQL_stmt_let *stmt);
 static void free_expr(PLpgSQL_expr *expr);

@@ -463,6 +466,9 @@ free_stmt(PLpgSQL_stmt *stmt)
 		case PLPGSQL_STMT_SET:
 			free_set((PLpgSQL_stmt_set *) stmt);
 			break;
+		case PLPGSQL_STMT_LET:
+			free_let((PLpgSQL_stmt_let *) stmt);
+			break;
 		default:
 			elog(ERROR, "unrecognized cmd_type: %d", stmt->cmd_type);
 			break;
@@ -723,6 +729,12 @@ free_getdiag(PLpgSQL_stmt_getdiag *stmt)
 {
 }
 
+static void
+free_let(PLpgSQL_stmt_let *stmt)
+{
+	free_expr(stmt->expr);
+}
+
 static void
 free_expr(PLpgSQL_expr *expr)
 {
@@ -826,6 +838,7 @@ static void dump_call(PLpgSQL_stmt_call *stmt);
 static void dump_commit(PLpgSQL_stmt_commit *stmt);
 static void dump_rollback(PLpgSQL_stmt_rollback *stmt);
 static void dump_set(PLpgSQL_stmt_set *stmt);
+static void dump_let(PLpgSQL_stmt_let *stmt);
 static void dump_expr(PLpgSQL_expr *expr);

@@ -928,6 +941,9 @@ dump_stmt(PLpgSQL_stmt *stmt)
 		case PLPGSQL_STMT_SET:
 			dump_set((PLpgSQL_stmt_set *) stmt);
 			break;
+		case PLPGSQL_STMT_LET:
+			dump_let((PLpgSQL_stmt_let *) stmt);
+			break;
 		default:
 			elog(ERROR, "unrecognized cmd_type: %d", stmt->cmd_type);
 			break;
@@ -1611,6 +1627,14 @@ dump_getdiag(PLpgSQL_stmt_getdiag *stmt)
 	printf("\n");
 }
 
+static void
+dump_let(PLpgSQL_stmt_let *stmt)
+{
+	dump_ind();
+	dump_expr(stmt->expr);
+	printf("\n");
+}
+
 static void
 dump_expr(PLpgSQL_expr *expr)
 {
diff --git a/src/pl/plpgsql/src/pl_gram.y b/src/pl/plpgsql/src/pl_gram.y
index 34e0520719..a406082875 100644
--- a/src/pl/plpgsql/src/pl_gram.y
+++ b/src/pl/plpgsql/src/pl_gram.y
@@ -197,7 +197,7 @@ static	void			check_raise_parameters(PLpgSQL_stmt_raise *stmt);
 %type <stmt>	stmt_return stmt_raise stmt_assert stmt_execsql
 %type <stmt>	stmt_dynexecute stmt_for stmt_perform stmt_call stmt_getdiag
 %type <stmt>	stmt_open stmt_fetch stmt_move stmt_close stmt_null
-%type <stmt>	stmt_commit stmt_rollback stmt_set
+%type <stmt>	stmt_commit stmt_rollback stmt_set stmt_let
 %type <stmt>	stmt_case stmt_foreach_a
 
 %type <list>	proc_exceptions
@@ -304,6 +304,7 @@ static	void			check_raise_parameters(PLpgSQL_stmt_raise *stmt);
 %token <keyword>	K_INTO
 %token <keyword>	K_IS
 %token <keyword>	K_LAST
+%token <keyword>	K_LET
 %token <keyword>	K_LOG
 %token <keyword>	K_LOOP
 %token <keyword>	K_MESSAGE
@@ -901,6 +902,8 @@ proc_stmt		: pl_block ';'
 						{ $$ = $1; }
 				| stmt_set
 						{ $$ = $1; }
+				| stmt_let
+						{ $$ = $1; }
 				;
 
 stmt_perform	: K_PERFORM
@@ -1017,6 +1020,29 @@ stmt_assign		: T_DATUM
 					}
 				;
 
+stmt_let		: K_LET
+					{
+						PLpgSQL_stmt_let *new;
+						RawParseMode pmode;
+
+						pmode = RAW_PARSE_PLPGSQL_LET;
+
+						new = palloc0(sizeof(PLpgSQL_stmt_let));
+						new->cmd_type = PLPGSQL_STMT_LET;
+						new->lineno   = plpgsql_location_to_lineno(@1);
+						new->stmtid = ++plpgsql_curr_compile->nstatements;
+
+						/* Push back the head name to include it in the stmt */
+						plpgsql_push_back_token(K_LET);
+						new->expr = read_sql_construct(';', 0, 0, ";",
+													   pmode,
+													   false, true, true,
+													   NULL, NULL);
+
+						$$ = (PLpgSQL_stmt *)new;
+					}
+				;
+
 stmt_getdiag	: K_GET getdiag_area_opt K_DIAGNOSTICS getdiag_list ';'
 					{
 						PLpgSQL_stmt_getdiag	 *new;
diff --git a/src/pl/plpgsql/src/pl_reserved_kwlist.h b/src/pl/plpgsql/src/pl_reserved_kwlist.h
index daf835e683..5e64984ff4 100644
--- a/src/pl/plpgsql/src/pl_reserved_kwlist.h
+++ b/src/pl/plpgsql/src/pl_reserved_kwlist.h
@@ -40,6 +40,7 @@ PG_KEYWORD("from", K_FROM)
 PG_KEYWORD("if", K_IF)
 PG_KEYWORD("in", K_IN)
 PG_KEYWORD("into", K_INTO)
+PG_KEYWORD("let", K_LET)
 PG_KEYWORD("loop", K_LOOP)
 PG_KEYWORD("not", K_NOT)
 PG_KEYWORD("null", K_NULL)
diff --git a/src/pl/plpgsql/src/plpgsql.h b/src/pl/plpgsql/src/plpgsql.h
index d5010862a5..c995ce43fb 100644
--- a/src/pl/plpgsql/src/plpgsql.h
+++ b/src/pl/plpgsql/src/plpgsql.h
@@ -128,7 +128,8 @@ typedef enum PLpgSQL_stmt_type
 	PLPGSQL_STMT_CALL,
 	PLPGSQL_STMT_COMMIT,
 	PLPGSQL_STMT_ROLLBACK,
-	PLPGSQL_STMT_SET
+	PLPGSQL_STMT_SET,
+	PLPGSQL_STMT_LET
 } PLpgSQL_stmt_type;
 
 /*
@@ -520,6 +521,17 @@ typedef struct PLpgSQL_stmt_assign
 	PLpgSQL_expr *expr;
 } PLpgSQL_stmt_assign;
 
+/*
+ * Let statement
+ */
+typedef struct PLpgSQL_stmt_let
+{
+	PLpgSQL_stmt_type cmd_type;
+	int			lineno;
+	unsigned int stmtid;
+	PLpgSQL_expr *expr;
+} PLpgSQL_stmt_let;
+
 /*
  * PERFORM statement
  */
diff --git a/src/test/regress/expected/misc_sanity.out b/src/test/regress/expected/misc_sanity.out
index 9ebe28a78d..dc8a14ffc6 100644
--- a/src/test/regress/expected/misc_sanity.out
+++ b/src/test/regress/expected/misc_sanity.out
@@ -109,7 +109,9 @@ ORDER BY 1, 2;
  pg_index                | indpred       | pg_node_tree
  pg_largeobject          | data          | bytea
  pg_largeobject_metadata | lomacl        | aclitem[]
-(11 rows)
+ pg_variable             | varacl        | aclitem[]
+ pg_variable             | vardefexpr    | pg_node_tree
+(13 rows)
 
 -- system catalogs without primary keys
 --
diff --git a/src/test/regress/expected/sanity_check.out b/src/test/regress/expected/sanity_check.out
index d9ce961be2..947851f3eb 100644
--- a/src/test/regress/expected/sanity_check.out
+++ b/src/test/regress/expected/sanity_check.out
@@ -163,6 +163,7 @@ pg_ts_parser|t
 pg_ts_template|t
 pg_type|t
 pg_user_mapping|t
+pg_variable|t
 point_tbl|t
 polygon_tbl|t
 quad_box_tbl|t
diff --git a/src/test/regress/expected/schema_variables.out b/src/test/regress/expected/schema_variables.out
new file mode 100644
index 0000000000..b04de1fb38
--- /dev/null
+++ b/src/test/regress/expected/schema_variables.out
@@ -0,0 +1,624 @@
+CREATE SCHEMA svartest;
+SET search_path = svartest;
+CREATE VARIABLE var1 AS integer;
+CREATE TEMP VARIABLE var2 AS text;
+DROP VARIABLE var1, var2;
+-- functional interface
+CREATE VARIABLE var1 AS numeric;
+CREATE ROLE var_test_role;
+GRANT USAGE ON SCHEMA svartest TO var_test_role;
+SET ROLE TO var_test_role;
+-- should fail
+SELECT var1;
+ERROR:  permission denied for schema variable var1
+SET ROLE TO DEFAULT;
+GRANT READ ON VARIABLE var1 TO var_test_role;
+SET ROLE TO var_test_role;
+-- should fail
+LET var1 = 10;
+ERROR:  permission denied for schema variable var1
+-- should work
+SELECT var1;
+ var1 
+------
+     
+(1 row)
+
+SET ROLE TO DEFAULT;
+GRANT WRITE ON VARIABLE var1 TO var_test_role;
+SET ROLE TO var_test_role;
+-- should work
+LET var1 = 333;
+SET ROLE TO DEFAULT;
+REVOKE ALL ON VARIABLE var1 FROM var_test_role;
+CREATE OR REPLACE FUNCTION secure_var()
+RETURNS int AS $$
+  SELECT svartest.var1::int;
+$$ LANGUAGE sql SECURITY DEFINER;
+SELECT secure_var();
+ secure_var 
+------------
+        333
+(1 row)
+
+SET ROLE TO var_test_role;
+-- should fail
+SELECT svartest.var1;
+ERROR:  permission denied for schema variable var1
+-- should work;
+SELECT secure_var();
+ secure_var 
+------------
+        333
+(1 row)
+
+SET ROLE TO DEFAULT;
+EXPLAIN (VERBOSE, COSTS OFF) SELECT * FROM generate_series(1,100) g(v) WHERE v = var1;
+                  QUERY PLAN                   
+-----------------------------------------------
+ Function Scan on pg_catalog.generate_series g
+   Output: v
+   Function Call: generate_series(1, 100)
+   Filter: ((g.v)::numeric = var1)
+(4 rows)
+
+CREATE VIEW schema_var_view AS SELECT var1;
+SELECT * FROM schema_var_view;
+ var1 
+------
+  333
+(1 row)
+
+\c -
+SET search_path = svartest;
+-- should work still, but var will be empty
+SELECT * FROM schema_var_view;
+ var1 
+------
+     
+(1 row)
+
+LET var1 = pi();
+SELECT var1;
+       var1       
+------------------
+ 3.14159265358979
+(1 row)
+
+-- we can see execution plan of LET statement
+EXPLAIN (VERBOSE, COSTS OFF) LET var1 = pi();
+         QUERY PLAN         
+----------------------------
+ SET SCHEMA VARIABLE
+ Result
+   Output: 3.14159265358979
+(3 rows)
+
+SELECT var1;
+       var1       
+------------------
+ 3.14159265358979
+(1 row)
+
+CREATE VARIABLE var3 AS int;
+CREATE OR REPLACE FUNCTION inc(int)
+RETURNS int AS $$
+BEGIN
+  LET svartest.var3 = COALESCE(svartest.var3 + $1, $1);
+  RETURN var3;
+END;
+$$ LANGUAGE plpgsql;
+SELECT inc(1);
+ inc 
+-----
+   1
+(1 row)
+
+SELECT inc(1);
+ inc 
+-----
+   2
+(1 row)
+
+SELECT inc(1);
+ inc 
+-----
+   3
+(1 row)
+
+SELECT inc(1) FROM generate_series(1,10);
+ inc 
+-----
+   4
+   5
+   6
+   7
+   8
+   9
+  10
+  11
+  12
+  13
+(10 rows)
+
+SET ROLE TO var_test_role;
+-- should fail
+LET var3 = 0;
+ERROR:  permission denied for schema variable var3
+SET ROLE TO DEFAULT;
+DROP VIEW schema_var_view;
+DROP VARIABLE var1 CASCADE;
+DROP VARIABLE var3 CASCADE;
+-- 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;
+\d v1
+\d v2
+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.14);
+SELECT v1;
+     v1     
+------------
+ (1,2,3.14)
+(1 row)
+
+SELECT v2;
+      v2       
+---------------
+ (10,20,31.40)
+(1 row)
+
+SELECT (v1).*;
+ x | y |  z   
+---+---+------
+ 1 | 2 | 3.14
+(1 row)
+
+SELECT (v2).*;
+ x  | y  |   z   
+----+----+-------
+ 10 | 20 | 31.40
+(1 row)
+
+SELECT v1.x + v1.z;
+ ?column? 
+----------
+     4.14
+(1 row)
+
+SELECT v2.x + v2.z;
+ ?column? 
+----------
+    41.40
+(1 row)
+
+-- access to composite fields should be safe too
+-- should fail
+SET ROLE TO var_test_role;
+SELECT v2.x;
+ERROR:  permission denied for schema variable v2
+SET ROLE TO DEFAULT;
+DROP VARIABLE v1;
+DROP VARIABLE v2;
+REVOKE USAGE ON SCHEMA svartest FROM var_test_role;
+DROP ROLE var_test_role;
+-- scalar variables should not be in conflict with qualified column
+CREATE VARIABLE varx AS text;
+SELECT varx.relname FROM pg_class varx WHERE varx.relname = 'pg_class';
+ relname  
+----------
+ pg_class
+(1 row)
+
+-- should fail
+SELECT varx.xxx;
+ERROR:  type text is not composite
+-- variables can be updated under RO transaction
+BEGIN;
+SET TRANSACTION READ ONLY;
+LET varx = 'hello';
+COMMIT;
+SELECT varx;
+ varx  
+-------
+ hello
+(1 row)
+
+DROP VARIABLE varx;
+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
+LET v1.x = 10;
+ERROR:  cannot assign to field "x" of column "x" because there is no such column in data type t1
+LINE 1: LET v1.x = 10;
+            ^
+DROP VARIABLE v1;
+DROP TYPE t1;
+-- arrays are supported
+CREATE VARIABLE va1 AS numeric[];
+LET va1 = ARRAY[1.1,2.1];
+LET va1[1] = 10.1;
+SELECT va1;
+    va1     
+------------
+ {10.1,2.1}
+(1 row)
+
+CREATE TYPE ta2 AS (a numeric, b numeric[]);
+CREATE VARIABLE va2 AS ta2;
+LET va2 = (10.1, ARRAY[0.0, 0.0]);
+LET va2.a = 10.2;
+SELECT va2;
+        va2         
+--------------------
+ (10.2,"{0.0,0.0}")
+(1 row)
+
+LET va2.b[1] = 10.3;
+SELECT va2;
+         va2         
+---------------------
+ (10.2,"{10.3,0.0}")
+(1 row)
+
+DROP VARIABLE va1;
+DROP VARIABLE va2;
+DROP TYPE ta2;
+-- default values
+CREATE VARIABLE v1 AS numeric DEFAULT pi();
+LET v1 = v1 * 2;
+SELECT v1;
+        v1        
+------------------
+ 6.28318530717958
+(1 row)
+
+CREATE TYPE t2 AS (a numeric, b text);
+CREATE VARIABLE v2 AS t2 DEFAULT (NULL, 'Hello');
+LET svartest.v2.a = pi();
+SELECT v2;
+            v2            
+--------------------------
+ (3.14159265358979,Hello)
+(1 row)
+
+-- shoudl fail due dependency
+DROP TYPE t2;
+ERROR:  cannot drop type t2 because other objects depend on it
+DETAIL:  schema variable v2 depends on type t2
+HINT:  Use DROP ... CASCADE to drop the dependent objects too.
+-- should be ok
+DROP VARIABLE v1;
+DROP VARIABLE v2;
+-- tests of alters
+CREATE SCHEMA var_schema1;
+CREATE SCHEMA var_schema2;
+CREATE VARIABLE var_schema1.var1 AS integer;
+LET var_schema1.var1 = 1000;
+SELECT var_schema1.var1;
+ var1 
+------
+ 1000
+(1 row)
+
+ALTER VARIABLE var_schema1.var1 SET SCHEMA var_schema2;
+SELECT var_schema2.var1;
+ var1 
+------
+ 1000
+(1 row)
+
+CREATE ROLE var_test_role;
+ALTER VARIABLE var_schema2.var1 OWNER TO var_test_role;
+SET ROLE TO var_test_role;
+-- should fail, no access to schema var_schema2.var
+SELECT var_schema2.var1;
+ERROR:  permission denied for schema var_schema2
+DROP VARIABLE var_schema2.var1;
+ERROR:  permission denied for schema var_schema2
+SET ROLE TO DEFAULT;
+ALTER VARIABLE var_schema2.var1 SET SCHEMA public;
+SET ROLE TO var_test_role;
+SELECT public.var1;
+ var1 
+------
+ 1000
+(1 row)
+
+ALTER VARIABLE public.var1 RENAME TO var1_renamed;
+SELECT public.var1_renamed;
+ var1_renamed 
+--------------
+         1000
+(1 row)
+
+DROP VARIABLE public.var1_renamed;
+SET ROLE TO DEFAULt;
+DROP ROLE var_test_role;
+CREATE VARIABLE xx AS text DEFAULT 'hello';
+SELECT xx, upper(xx);
+  xx   | upper 
+-------+-------
+ hello | HELLO
+(1 row)
+
+LET xx = 'Hi';
+SELECT xx;
+ xx 
+----
+ Hi
+(1 row)
+
+DROP VARIABLE xx;
+-- ON TRANSACTION END RESET tests
+CREATE VARIABLE t1 AS int DEFAULT -1 ON TRANSACTION END RESET;
+BEGIN;
+  SELECT t1;
+ t1 
+----
+ -1
+(1 row)
+
+  LET t1 = 100;
+  SELECT t1;
+ t1  
+-----
+ 100
+(1 row)
+
+COMMIT;
+SELECT t1;
+ t1 
+----
+ -1
+(1 row)
+
+BEGIN;
+  SELECT t1;
+ t1 
+----
+ -1
+(1 row)
+
+  LET t1 = 100;
+  SELECT t1;
+ t1  
+-----
+ 100
+(1 row)
+
+ROLLBACK;
+SELECT t1;
+ t1 
+----
+ -1
+(1 row)
+
+DROP VARIABLE t1;
+CREATE VARIABLE v1 AS int DEFAULT 0;
+CREATE VARIABLE v2 AS text DEFAULT 'none';
+LET v1 = 100;
+LET v2 = 'Hello';
+SELECT v1, v2;
+ v1  |  v2   
+-----+-------
+ 100 | Hello
+(1 row)
+
+LET v1 = DEFAULT;
+LET v2 = DEFAULT;
+SELECT v1, v2;
+ v1 |  v2  
+----+------
+  0 | none
+(1 row)
+
+DROP VARIABLE v1;
+DROP VARIABLE v2;
+-- ON COMMIT DROP tests
+-- should be 0 always
+SELECT count(*) FROM pg_variable;
+ count 
+-------
+     0
+(1 row)
+
+CREATE TEMP VARIABLE g AS int ON COMMIT DROP;
+SELECT count(*) FROM pg_variable;
+ count 
+-------
+     0
+(1 row)
+
+BEGIN;
+  CREATE TEMP VARIABLE g AS int ON COMMIT DROP;
+COMMIT;
+SELECT count(*) FROM pg_variable;
+ count 
+-------
+     0
+(1 row)
+
+BEGIN;
+  CREATE TEMP VARIABLE g AS int ON COMMIT DROP;
+ROLLBACK;
+SELECT count(*) FROM pg_variable;
+ count 
+-------
+     0
+(1 row)
+
+-- test on query with workers
+CREATE TABLE svar_test(a int);
+INSERT INTO svar_test SELECT * FROM generate_series(1,1000000);
+ANALYZE svar_test;
+CREATE VARIABLE zero int;
+LET zero = 0;
+-- parallel workers should be used
+EXPLAIN (costs off) SELECT count(*) FROM svar_test WHERE a%10 = zero;
+                    QUERY PLAN                    
+--------------------------------------------------
+ Finalize Aggregate
+   ->  Gather
+         Workers Planned: 2
+         ->  Partial Aggregate
+               ->  Parallel Seq Scan on svar_test
+                     Filter: ((a % 10) = zero)
+(6 rows)
+
+-- result should be 100000
+SELECT count(*) FROM svar_test WHERE a%10 = zero;
+ count  
+--------
+ 100000
+(1 row)
+
+LET zero = (SELECT count(*) FROM svar_test);
+-- result should be 1000000
+SELECT zero;
+  zero   
+---------
+ 1000000
+(1 row)
+
+-- parallel workers should be used
+EXPLAIN (costs off) LET zero = (SELECT count(*) FROM svar_test);
+                        QUERY PLAN                        
+----------------------------------------------------------
+ SET SCHEMA VARIABLE
+ Result
+   InitPlan 1 (returns $1)
+     ->  Finalize Aggregate
+           ->  Gather
+                 Workers Planned: 2
+                 ->  Partial Aggregate
+                       ->  Parallel Seq Scan on svar_test
+(8 rows)
+
+DROP TABLE svar_test;
+DROP VARIABLE zero;
+-- use variables in prepared statements
+CREATE VARIABLE v AS numeric;
+LET v = 3.14;
+-- use variables in views
+CREATE VIEW vv AS SELECT COALESCE(v, 0) + 1000 AS result;
+SELECT * FROM vv;
+ result  
+---------
+ 1003.14
+(1 row)
+
+-- start a new session
+\c
+SET search_path to svartest;
+SELECT * FROM vv;
+ result 
+--------
+   1000
+(1 row)
+
+LET v = 3.14;
+SELECT * FROM vv;
+ result  
+---------
+ 1003.14
+(1 row)
+
+-- should fail, dependency
+DROP VARIABLE v;
+ERROR:  cannot drop schema variable v because other objects depend on it
+DETAIL:  view vv depends on schema variable v
+HINT:  Use DROP ... CASCADE to drop the dependent objects too.
+-- should be ok
+DROP VARIABLE v CASCADE;
+NOTICE:  drop cascades to view vv
+-- other features
+CREATE VARIABLE dt AS integer DEFAULT 0;
+LET dt = 100;
+SELECT dt;
+ dt  
+-----
+ 100
+(1 row)
+
+DISCARD VARIABLES;
+SELECT dt;
+ dt 
+----
+  0
+(1 row)
+
+DROP VARIABLE dt;
+-- NOT NULL
+CREATE VARIABLE v1 AS int NOT NULL;
+CREATE VARIABLE v2 AS int NOT NULL DEFAULT NULL;
+-- should fail
+SELECT v1;
+ERROR:  null value is not allowed for NOT NULL schema variable "v1"
+DETAIL:  The schema variable was not initialized yet.
+SELECT v2;
+ERROR:  null value is not allowed for NOT NULL schema variable "v2"
+LET v1 = NULL;
+ERROR:  null value is not allowed for NOT NULL schema variable "v1"
+LET v2 = NULL;
+ERROR:  null value is not allowed for NOT NULL schema variable "v2"
+LET v1 = DEFAULT;
+ERROR:  null value is not allowed for NOT NULL schema variable "v1"
+LET v2 = DEFAULT;
+ERROR:  null value is not allowed for NOT NULL schema variable "v2"
+-- should be ok
+LET v1 = 100;
+LET v2 = 1000;
+SELECT v1, v2;
+ v1  |  v2  
+-----+------
+ 100 | 1000
+(1 row)
+
+DROP VARIABLE v1;
+DROP VARIABLE v2;
+CREATE VARIABLE tv AS int;
+CREATE VARIABLE IF NOT EXISTS tv AS int;
+NOTICE:  schema variable "tv" already exists, skipping
+DROP VARIABLE tv;
+CREATE IMMUTABLE VARIABLE iv AS int DEFAULT 100;
+SELECT iv;
+ iv  
+-----
+ 100
+(1 row)
+
+-- should fail;
+LET iv = 10000;
+ERROR:  schema variable "iv" is declared IMMUTABLE
+DROP VARIABLE iv;
+-- create variable inside plpgsql block
+DO $$
+BEGIN
+  CREATE VARIABLE do_test_svar AS date DEFAULT '2000-01-01';
+END;
+$$;
+SELECT do_test_svar;
+ do_test_svar 
+--------------
+ 01-01-2000
+(1 row)
+
+DROP VARIABLE do_test_svar;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index a091300857..059e698635 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -114,7 +114,7 @@ test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath
 # NB: temp.sql does a reconnect which transiently uses 2 connections,
 # so keep this parallel group to at most 19 tests
 # ----------
-test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml
+test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml schema_variables
 
 # ----------
 # Another group of parallel tests
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 5644847601..67f6e31e90 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -209,3 +209,4 @@ test: event_trigger
 test: oidjoins
 test: fast_default
 test: stats
+test: schema_variables
diff --git a/src/test/regress/sql/schema_variables.sql b/src/test/regress/sql/schema_variables.sql
new file mode 100644
index 0000000000..381a422c80
--- /dev/null
+++ b/src/test/regress/sql/schema_variables.sql
@@ -0,0 +1,416 @@
+CREATE SCHEMA svartest;
+
+SET search_path = svartest;
+
+CREATE VARIABLE var1 AS integer;
+CREATE TEMP VARIABLE var2 AS text;
+
+DROP VARIABLE var1, var2;
+
+-- functional interface
+CREATE VARIABLE var1 AS numeric;
+
+CREATE ROLE var_test_role;
+GRANT USAGE ON SCHEMA svartest TO var_test_role;
+
+SET ROLE TO var_test_role;
+
+-- should fail
+SELECT var1;
+
+SET ROLE TO DEFAULT;
+
+GRANT READ ON VARIABLE var1 TO var_test_role;
+
+SET ROLE TO var_test_role;
+-- should fail
+LET var1 = 10;
+-- should work
+SELECT var1;
+
+SET ROLE TO DEFAULT;
+
+GRANT WRITE ON VARIABLE var1 TO var_test_role;
+
+SET ROLE TO var_test_role;
+
+-- should work
+LET var1 = 333;
+
+SET ROLE TO DEFAULT;
+
+REVOKE ALL ON VARIABLE var1 FROM var_test_role;
+
+CREATE OR REPLACE FUNCTION secure_var()
+RETURNS int AS $$
+  SELECT svartest.var1::int;
+$$ LANGUAGE sql SECURITY DEFINER;
+
+SELECT secure_var();
+
+SET ROLE TO var_test_role;
+
+-- should fail
+SELECT svartest.var1;
+
+-- should work;
+SELECT secure_var();
+
+SET ROLE TO DEFAULT;
+
+EXPLAIN (VERBOSE, COSTS OFF) SELECT * FROM generate_series(1,100) g(v) WHERE v = var1;
+
+CREATE VIEW schema_var_view AS SELECT var1;
+
+SELECT * FROM schema_var_view;
+
+\c -
+
+SET search_path = svartest;
+
+-- should work still, but var will be empty
+SELECT * FROM schema_var_view;
+
+LET var1 = pi();
+
+SELECT var1;
+
+-- we can see execution plan of LET statement
+EXPLAIN (VERBOSE, COSTS OFF) LET var1 = pi();
+
+SELECT var1;
+
+CREATE VARIABLE var3 AS int;
+
+CREATE OR REPLACE FUNCTION inc(int)
+RETURNS int AS $$
+BEGIN
+  LET svartest.var3 = COALESCE(svartest.var3 + $1, $1);
+  RETURN var3;
+END;
+$$ LANGUAGE plpgsql;
+
+SELECT inc(1);
+SELECT inc(1);
+SELECT inc(1);
+
+SELECT inc(1) FROM generate_series(1,10);
+
+SET ROLE TO var_test_role;
+
+-- should fail
+LET var3 = 0;
+
+SET ROLE TO DEFAULT;
+
+DROP VIEW schema_var_view;
+
+DROP VARIABLE var1 CASCADE;
+DROP VARIABLE var3 CASCADE;
+
+-- 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;
+
+\d v1
+\d v2
+
+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.14);
+
+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
+-- should fail
+SET ROLE TO var_test_role;
+
+SELECT v2.x;
+
+SET ROLE TO DEFAULT;
+
+DROP VARIABLE v1;
+DROP VARIABLE v2;
+
+REVOKE USAGE ON SCHEMA svartest FROM var_test_role;
+DROP ROLE var_test_role;
+
+-- scalar variables should not be in conflict with qualified column
+CREATE VARIABLE varx AS text;
+SELECT varx.relname FROM pg_class varx WHERE varx.relname = 'pg_class';
+
+-- should fail
+SELECT varx.xxx;
+
+-- variables can be updated under RO transaction
+
+BEGIN;
+SET TRANSACTION READ ONLY;
+LET varx = 'hello';
+COMMIT;
+
+SELECT varx;
+
+DROP VARIABLE varx;
+
+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
+LET v1.x = 10;
+
+DROP VARIABLE v1;
+DROP TYPE t1;
+
+-- arrays are supported
+CREATE VARIABLE va1 AS numeric[];
+LET va1 = ARRAY[1.1,2.1];
+LET va1[1] = 10.1;
+SELECT va1;
+
+CREATE TYPE ta2 AS (a numeric, b numeric[]);
+CREATE VARIABLE va2 AS ta2;
+LET va2 = (10.1, ARRAY[0.0, 0.0]);
+LET va2.a = 10.2;
+SELECT va2;
+LET va2.b[1] = 10.3;
+SELECT va2;
+
+DROP VARIABLE va1;
+DROP VARIABLE va2;
+DROP TYPE ta2;
+
+-- default values
+CREATE VARIABLE v1 AS numeric DEFAULT pi();
+LET v1 = v1 * 2;
+SELECT v1;
+
+CREATE TYPE t2 AS (a numeric, b text);
+CREATE VARIABLE v2 AS t2 DEFAULT (NULL, 'Hello');
+LET svartest.v2.a = pi();
+SELECT v2;
+
+-- shoudl fail due dependency
+DROP TYPE t2;
+
+-- should be ok
+DROP VARIABLE v1;
+DROP VARIABLE v2;
+
+-- tests of alters
+CREATE SCHEMA var_schema1;
+CREATE SCHEMA var_schema2;
+
+CREATE VARIABLE var_schema1.var1 AS integer;
+LET var_schema1.var1 = 1000;
+SELECT var_schema1.var1;
+ALTER VARIABLE var_schema1.var1 SET SCHEMA var_schema2;
+SELECT var_schema2.var1;
+
+CREATE ROLE var_test_role;
+
+ALTER VARIABLE var_schema2.var1 OWNER TO var_test_role;
+SET ROLE TO var_test_role;
+
+-- should fail, no access to schema var_schema2.var
+SELECT var_schema2.var1;
+DROP VARIABLE var_schema2.var1;
+
+SET ROLE TO DEFAULT;
+
+ALTER VARIABLE var_schema2.var1 SET SCHEMA public;
+
+SET ROLE TO var_test_role;
+SELECT public.var1;
+
+ALTER VARIABLE public.var1 RENAME TO var1_renamed;
+
+SELECT public.var1_renamed;
+
+DROP VARIABLE public.var1_renamed;
+
+SET ROLE TO DEFAULt;
+
+DROP ROLE var_test_role;
+
+CREATE VARIABLE xx AS text DEFAULT 'hello';
+
+SELECT xx, upper(xx);
+
+LET xx = 'Hi';
+
+SELECT xx;
+
+DROP VARIABLE xx;
+
+-- ON TRANSACTION END RESET tests
+CREATE VARIABLE t1 AS int DEFAULT -1 ON TRANSACTION END RESET;
+
+BEGIN;
+  SELECT t1;
+  LET t1 = 100;
+  SELECT t1;
+COMMIT;
+
+SELECT t1;
+
+BEGIN;
+  SELECT t1;
+  LET t1 = 100;
+  SELECT t1;
+ROLLBACK;
+
+SELECT t1;
+
+DROP VARIABLE t1;
+
+CREATE VARIABLE v1 AS int DEFAULT 0;
+CREATE VARIABLE v2 AS text DEFAULT 'none';
+
+LET v1 = 100;
+LET v2 = 'Hello';
+SELECT v1, v2;
+LET v1 = DEFAULT;
+LET v2 = DEFAULT;
+SELECT v1, v2;
+
+DROP VARIABLE v1;
+DROP VARIABLE v2;
+
+-- ON COMMIT DROP tests
+-- should be 0 always
+SELECT count(*) FROM pg_variable;
+
+CREATE TEMP VARIABLE g AS int ON COMMIT DROP;
+
+SELECT count(*) FROM pg_variable;
+
+BEGIN;
+  CREATE TEMP VARIABLE g AS int ON COMMIT DROP;
+COMMIT;
+
+SELECT count(*) FROM pg_variable;
+
+BEGIN;
+  CREATE TEMP VARIABLE g AS int ON COMMIT DROP;
+ROLLBACK;
+
+SELECT count(*) FROM pg_variable;
+
+-- test on query with workers
+CREATE TABLE svar_test(a int);
+INSERT INTO svar_test SELECT * FROM generate_series(1,1000000);
+ANALYZE svar_test;
+CREATE VARIABLE zero int;
+LET zero = 0;
+
+-- parallel workers should be used
+EXPLAIN (costs off) SELECT count(*) FROM svar_test WHERE a%10 = zero;
+
+-- result should be 100000
+SELECT count(*) FROM svar_test WHERE a%10 = zero;
+
+LET zero = (SELECT count(*) FROM svar_test);
+
+-- result should be 1000000
+SELECT zero;
+
+-- parallel workers should be used
+EXPLAIN (costs off) LET zero = (SELECT count(*) FROM svar_test);
+
+DROP TABLE svar_test;
+DROP VARIABLE zero;
+
+-- use variables in prepared statements
+CREATE VARIABLE v AS numeric;
+LET v = 3.14;
+
+-- use variables in views
+CREATE VIEW vv AS SELECT COALESCE(v, 0) + 1000 AS result;
+SELECT * FROM vv;
+
+-- start a new session
+\c
+
+SET search_path to svartest;
+
+SELECT * FROM vv;
+LET v = 3.14;
+SELECT * FROM vv;
+
+-- should fail, dependency
+DROP VARIABLE v;
+
+-- should be ok
+DROP VARIABLE v CASCADE;
+
+-- other features
+CREATE VARIABLE dt AS integer DEFAULT 0;
+
+LET dt = 100;
+SELECT dt;
+
+DISCARD VARIABLES;
+
+SELECT dt;
+
+DROP VARIABLE dt;
+
+-- NOT NULL
+CREATE VARIABLE v1 AS int NOT NULL;
+CREATE VARIABLE v2 AS int NOT NULL DEFAULT NULL;
+
+-- should fail
+SELECT v1;
+SELECT v2;
+LET v1 = NULL;
+LET v2 = NULL;
+LET v1 = DEFAULT;
+LET v2 = DEFAULT;
+
+-- should be ok
+LET v1 = 100;
+LET v2 = 1000;
+SELECT v1, v2;
+
+DROP VARIABLE v1;
+DROP VARIABLE v2;
+
+CREATE VARIABLE tv AS int;
+CREATE VARIABLE IF NOT EXISTS tv AS int;
+DROP VARIABLE tv;
+
+CREATE IMMUTABLE VARIABLE iv AS int DEFAULT 100;
+SELECT iv;
+
+-- should fail;
+LET iv = 10000;
+
+DROP VARIABLE iv;
+
+-- create variable inside plpgsql block
+DO $$
+BEGIN
+  CREATE VARIABLE do_test_svar AS date DEFAULT '2000-01-01';
+END;
+$$;
+
+SELECT do_test_svar;
+
+DROP VARIABLE do_test_svar;

---

From: "[email-masked]" <[email-masked]>
Date: Thu, 15 Apr 2021 16:02:45 +0000

From: Pavel Stehule <[email-masked]>
--------------------------------------------------
do $$
declare x int ;
begin
  for i in 1..1000000
  loop
    let ooo = i;
  end loop;
end;
$$;

variant 1 .. 1500 ms
variant 2 with PLpgSQL support .. 140 ms
variant 2 without PLpgSQL support 9000 ms
--------------------------------------------------

That's impressive!  But 1 million times of variable assignment took only 140 ms?  It's that one assignment took only 140 nanosecond, which is near one DRAM access?  Can PL/pgSQL processing be really so fast?

Regards
Takayuki Tsunakawa

---

From: Pavel Stehule <[email-masked]>
Date: Thu, 15 Apr 2021 18:11:40 +0200

čt 15. 4. 2021 v 18:02 odesílatel [email-masked] <
[email-masked]> napsal:

> That's impressive!  But 1 million times of variable assignment took only
> 140 ms?  It's that one assignment took only 140 nanosecond, which is near
> one DRAM access?  Can PL/pgSQL processing be really so fast?
>

In this case the PLpgSQL can be very fast - and after changes in pg 13, the
PLpgSQL is not significantly slower than Lua or than PHP.

Every body can repeat these tests - I did it on my Lenovo T520 notebook

Pavel

---

From: Pavel Stehule <[email-masked]>
Date: Fri, 16 Apr 2021 05:32:26 +0200

> Comments, notes, objections?
I am sending only one patch and I assign this thread to commitfest
application

Regards

Pavel

 
 
  </sect1>

 

  <refsynopsisdiv>
 <synopsis>

+   <varlistentry>

+    <varlistentry>
 
+     <varlistentry>

 

 

+		case OBJECT_VARIABLE:
 
+static void
 
 static AclMode
 
+/*
 
+/*
 
+/*
 
+		case OBJECT_VARIABLE:

+		case OCLASS_VARIABLE:

 
 	/* shouldn't get here */
 
+/*
+ * VariableIsVisible
 
 /*
  * TypenameGetTypid
 
+/*
 
 /*

 /*

 	/*
 
+/*
 
+		case OCLASS_VARIABLE:
+			{
+				char	   *nspname;
+				HeapTuple	tup;
 
+		case OCLASS_VARIABLE:
 
 				if (objname)
 
+		case OCLASS_VARIABLE:
+			{
+				char	   *schema;
+				char	   *varname;
+				HeapTuple	tup;

 

 			/*
 
 			/*
 
 				/*

 						/*

 
+	/*

 

 

 
 
 	return newnode;
 
+static LetStmt *

 		default:
 
 	return true;
 
+static bool
 
+static bool

+static void

 	READ_DONE();

 	/*

+	/*

+/*

 
 
 				/*

 
+		case T_LetStmt:
 
 			/*

 

 

 

 

 
 	return qry;
 }
 
+/*
+ * transformLetStmt -

 

 

 

 

 
 /*

 
 /*

 
 opt_column: COLUMN

 

 
 			/*
 
 			if (isAgg)
 
 			/*

 /* GUC parameters */
 
 /*
  * transformExpr -

 
+	if (OidIsValid(varid))
+	{
+		Oid			typid;
+		int32		typmod;
+		Oid			collid;
+
 
+/*
 
 			/*
 
 			/*

+	/*
 
 	/*

 		case DestTupleQueue:

 
 		case T_CallStmt:
 		case T_DoStmt:
+		case T_LetStmt:
 			{
 				/*
 
+		case T_LetStmt:

+		case T_LetStmt:
 
+		case T_LetStmt:
+			tag = CMDTAG_LET;
+			break;
+

 		case T_PLAssignStmt:
+		case T_LetStmt:
 			lev = LOGSTMT_ALL;
 			break;

 	ereport(ERROR,

+/*
+ * get_varname_varid

 	/*
 
 	return false;
 }

 	int			useDB;

+/*
+ * getVariables

+/*
 
 #endif							/* PG_DUMP_H */

 

+/*
+ * \dV
+ *
+ * listVariables()
+ */
+bool
 
 /*
  * \dFp
 
+/* \dV */
 
 #endif							/* DESCRIBE_H */

 /*

 /* DISCARD */
 
 /* DO */
 	else if (Matches("DO"))
 
+	/* DROP VARIABLE */

 /* PREPARE xx AS */
 
 /*

 

 

 

 
 
+/* ----------------
+ * SchemaVariableValue
+ * ----------------
+ */
 
+	/* Variables info: */

 	/*

 

 

 
 
 	/*

 

 

 typedef struct Param

 

 
 /* ----------------

 /*

 

 
 
+			case PLPGSQL_STMT_LET:
 
+/* ----------
 
 	return "unknown";

+static void

+static void

 stmt_perform	: K_PERFORM
 
+stmt_let		: K_LET
+					{
+						PLpgSQL_stmt_let *new;
+						RawParseMode pmode;
+
 
 /*
 
+/*
+ * Let statement
+ */

 # ----------

---

From: "Joel Jacobson" <[email-masked]>
Date: Fri, 16 Apr 2021 08:06:48 +0200

On Thu, Apr 15, 2021, at 10:42, Pavel Stehule wrote:
> *Attachments:*
>  * schema-variables-v-execnode-2021-01.patch
>  * schema-variables-v-utility-2021-01.patch

Applications are currently know to be misusing set_config()+current_setting() to pass information in a session or transaction.

Such users might be interested in using Schema variables as a better replacement.

However, since set_config() is transactional, it can't be used as a drop-in replacement:

+   <para>

I think the "The content of a variable is not transactional." part is therefore a bad idea.

Another pattern is to use TEMP TABLEs to pass around information in a session or transaction.
If the LET command would be transactional, it could be used as a drop-in replacement for such use-cases as well.

Furthermore, I think a non-transactional LET command would be insidious,
since it looks like any other SQL command, all of which are transactional.
(The ones that aren't such as REINDEX CONCURRENTLY will properly throw an error if run inside a transaction block.)

A non-transactional LET command IMO would be non-SQL-idiomatic and non-intuitive.

/Joel

---

From: Pavel Stehule <[email-masked]>
Date: Fri, 16 Apr 2021 08:40:42 +0200

pá 16. 4. 2021 v 8:07 odesílatel Joel Jacobson <[email-masked]> napsal:

>    - schema-variables-v-execnode-2021-01.patch
>    - schema-variables-v-utility-2021-01.patch
>
>
> Applications are currently know to be misusing
> set_config()+current_setting() to pass information in a session or
> transaction.
>
> (The ones that aren't such as REINDEX CONCURRENTLY will properly throw an
> error if run inside a transaction block.)
>

I am sorry, but in this topic we have different opinions. The variables in
PLpgSQL are not transactional too (same is true in Perl, Python, ...).
Session variables in Oracle, MS SQL, DB2, MySQL are not transactional too.
My primary focus is PLpgSQL - and I would like to use schema variables as
global plpgsql variables (from PLpgSQL perspective) - that means in
Postgres's perspective session variables. But in Postgres, I have to write
features that will work with others PL too - PLPython, PLPerl, ...
Statement SET in ANSI/SQL standard (SQL/PSM) doesn't expect transactional
behaviour for variables too. Unfortunately SET keyword is used in Postgres
for GUC, and isn't possible to reuse without a compatibility break.

The PostgreSQL configuration is transactional, but it is a different
feature designed for different purposes. Using GUC like session variables
is just a workaround. It can be useful for some cases, sure. But it is not
usual behaviour. And for other cases the transactional behaviour is not
practical. Schema variables are not replacement of GUC, schema variables
are not replacement of temporal tables. There is a prepared patch for
global temp tables. I hope so this patch can be committed to Postgres 15.
Global temp tables fixes almost all disadvantages of temporary tables in
Postgres. So the schema variable is not a one row table. It is a different
creature - designed to support the server's side procedural features.

I have prepared a patch that allows non default transactional behaviour
(but this behaviour should not be default - I didn't design schema
variables as temp tables replacement). This patch increases the length of
the current patch about 1/4, and I have enough work with rebasing with the
current patch, so I didn't send it to commitfest now. If schema variables
will be inside core, this day I'll send the patch that allows transactional
behaviour for schema variables - I promise.

Regards

Pavel

---

From: "[email-masked]" <[email-masked]>
Date: Fri, 16 Apr 2021 07:00:39 +0000

From: Pavel Stehule <[email-masked]>
--------------------------------------------------
I am sorry, but in this topic we have different opinions. The variables in PLpgSQL are not transactional too (same is true in Perl, Python, ...). Session variables in Oracle, MS SQL, DB2, MySQL are not transactional too. My primary focus is PLpgSQL - and I would like to use schema variables as global plpgsql variables (from PLpgSQL perspective) - that means in Postgres's perspective session variables. But in Postgres, I have to write features that will work with others PL too - PLPython, PLPerl, ... Statement SET in ANSI/SQL standard (SQL/PSM) doesn't expect transactional behaviour for variables too. Unfortunately SET keyword is used in Postgres for GUC, and isn't possible to reuse without a compatibility break.

The PostgreSQL configuration is transactional, but it is a different feature designed for different purposes. Using GUC like session variables is just a workaround. It can be useful for some cases, sure. But it is not usual behaviour. And for other cases the transactional behaviour is not practical. Schema variables are not replacement of GUC, schema variables are not replacement of temporal tables. There is a prepared patch for global temp tables. I hope so this patch can be committed to Postgres 15. Global temp tables fixes almost all disadvantages of temporary tables in Postgres. So the schema variable is not a one row table. It is a different creature - designed to support the server's side procedural features.
--------------------------------------------------

+1
I understand (and wish) this feature is intended to ease migration from Oracle PL/SQL, which will further increase the popularity of Postgres.  So, the transactional behavior is not necessary unless Oracle has such a feature.

Furthermore, Postgres already has some non-transactonal SQL commands.  So, I don't think we need to reject non-transactional LET.

* Sequence operation: SELECT nextval/setval
* SET [SESSION]
* SET ROLE
* SET SESSION AUTHORIZATION

--------------------------------------------------
I have prepared a patch that allows non default transactional behaviour (but this behaviour should not be default - I didn't design schema variables as temp tables replacement). This patch increases the length of the current patch about 1/4, and I have enough work with rebasing with the current patch, so I didn't send it to commitfest now. If schema variables will be inside core, this day I'll send the patch that allows transactional behaviour for schema variables - I promise.
--------------------------------------------------

I prefer the simpler, targeted one without transactional behavior initially, because added complexity might prevent this feature from being committed in PG 15.

Regards
Takayuki Tsunakawa

---

From: Pavel Stehule <[email-masked]>
Date: Wed, 12 May 2021 06:17:03 +0200

Hi

only rebase

Regards

Pavel

---

From: Pavel Stehule <[email-masked]>
Date: Wed, 12 May 2021 07:37:14 +0200

> only rebase

second try - rebase after serial_scheduler remove

Regards

Pavel

---

From: Pavel Stehule <[email-masked]>
Date: Mon, 17 May 2021 11:04:59 +0200

only rebase

Regards

Pavel

---

From: Pavel Stehule <[email-masked]>
Date: Sat, 12 Jun 2021 08:00:05 +0200

Hi

rebase

Regards

Pavel

---

From: Pavel Stehule <[email-masked]>
Date: Fri, 2 Jul 2021 13:29:27 +0200

> rebase
rebase only

Regards

Pavel

Regards

---

From: Gilles Darold <[email-masked]>
Date: Sat, 28 Aug 2021 11:57:28 +0200

Hi,

Review resume:

This patch implements Schema Variables that are database objects that
can hold a single or composite value following the data type used at
variable declaration. Schema variables, like relations, exist within a
schema and their access is controlled via GRANT and REVOKE commands. The
schema variable can be created by the CREATE VARIABLE command, altered
using ALTER VARIABLE and removed using DROP VARIABLE.

The value of a schema variable is local to the current session.
Retrieving a variable's value returns either a NULL or a default value,
unless its value is set to something else in the current session with a
LET command. The content of a variable is not transactional. This is the
same as in regular variables in PL languages.

Schema variables are retrieved by the SELECT SQL command. Their value is
set with the LET SQL command. While schema variables share properties
with tables, their value cannot be updated with an UPDATE command.

The patch apply with the patch command without problem and compilation
reports no warning or errors. Regression tests pass successfully using
make check or make installcheck
It also includes all documentation and regression tests.

Performances are near the set of plpgsql variable settings which is
impressive:

do $$
declare var1 int ; i int;
begin
  for i in 1..1000000
  loop
    var1 := i;
  end loop;
end;
$$;
DO
Time: 71,515 ms

CREATE VARIABLE var1 AS integer;
do $$
declare i int ;
begin
  for i in 1..1000000
  loop
    let var1 = i;
  end loop;
end;
$$;
DO
Time: 94,658 ms

There is just one thing that puzzles me.We can use :

    CREATE VARIABLE var1 AS date NOT NULL;
    postgres=# SELECT var1;
    ERROR:  null value is not allowed for NOT NULL schema variable "var1"

which I understand and is the right behavior. But if we use:

    CREATE IMMUTABLE VARIABLE var1 AS date NOT NULL;
    postgres=# SELECT var1;
    ERROR:  null value is not allowed for NOT NULL schema variable "var1"
    DETAIL:  The schema variable was not initialized yet.
    postgres=# LET var1=current_date;
    ERROR:  schema variable "var1" is declared IMMUTABLE

It should probably be better to not allow NOT NULL when IMMUTABLE is
used because the variable can not be used at all.  Also probably
IMMUTABLE without a DEFAULT value should also be restricted as it makes
no sens. If the user wants the variable to be NULL he must use DEFAULT
NULL. This is just a though, the above error messages are explicit and
the user can understand what wrong declaration he have done.

Except that I think this patch is ready for committers, so if there is
no other opinion in favor of restricting the use of IMMUTABLE with NOT
NULL and DEFAULT I will change the status to ready for committers.

-- 
Gilles Darold
http://www.darold.net/

---

From: Pavel Stehule <[email-masked]>
Date: Sun, 29 Aug 2021 22:46:47 +0200

so 28. 8. 2021 v 11:57 odesílatel Gilles Darold <[email-masked]> napsal:

> Hi,
>
> Review resume:
>
>
> This patch implements Schema Variables that are database objects that can
> hold a single or composite value following the data type used at variable
> declaration. Schema variables, like relations, exist within a schema and
> their access is controlled via GRANT and REVOKE commands. The schema
> variable can be created by the CREATE VARIABLE command, altered using ALTER
> VARIABLE and removed using DROP VARIABLE.
>
> The value of a schema variable is local to the current session. Retrieving
> a variable's value returns either a NULL or a default value, unless its
> value is set to something else in the current session with a LET command.
> The content of a variable is not transactional. This is the same as in
> regular variables in PL languages.
>
> set with the LET SQL command. While schema variables share properties with
> tables, their value cannot be updated with an UPDATE command.
>
> reports no warning or errors. Regression tests pass successfully using make
> check or make installcheck
> There is just one thing that puzzles me. We can use :
>
> It should probably be better to not allow NOT NULL when IMMUTABLE is used
> because the variable can not be used at all.  Also probably IMMUTABLE
> without a DEFAULT value should also be restricted as it makes no sens. If
> the user wants the variable to be NULL he must use DEFAULT NULL. This is
> just a though, the above error messages are explicit and the user can
> understand what wrong declaration he have done.
>

I thought about this case, and I have one scenario, where this behaviour
can be useful. When the variable is declared as IMMUTABLE NOT NULL without
not null default, then any access to the content of the variable has to
fail. I think it can be used for detection, where and when the variable is
first used. So this behavior is allowed just because I think, so this
feature can be interesting for debugging. If this idea is too strange, I
have no problem to disable this case.

Regards

Pavel

>
> Except that I think this patch is ready for committers, so if there is no
> other opinion in favor of restricting the use of IMMUTABLE with NOT NULL
> and DEFAULT I will change the status to ready for committers.
>
> --
> Gilles Daroldhttp://www.darold.net/
>
>

---

From: Pavel Stehule <[email-masked]>
Date: Wed, 8 Sep 2021 14:41:27 +0200

Hi

> understand what wrong declaration he have done.

I wrote a check that disables this case.  Please, see the attached patch. I
agree, so this case is confusing, and it is better to disable it.

Regards

Pavel

---

From: Gilles Darold <[email-masked]>
Date: Wed, 8 Sep 2021 17:59:16 +0100

Le 08/09/2021 à 13:41, Pavel Stehule a écrit :
> Hi
>
> so 28. 8. 2021 v 11:57 odesílatel Gilles Darold <[email-masked] 
> <mailto:[email-masked]>> napsal:
>
>     Hi,
>
>     Review resume:
>
>
>     This patch implements Schema Variables that are database objects
>     that can hold a single or composite value following the data type
>     used at variable declaration. Schema variables, like relations,
>     exist within a schema and their access is controlled via GRANT and
>     REVOKE commands. The schema variable can be created by the CREATE
>     VARIABLE command, altered using ALTER VARIABLE and removed using
>     DROP VARIABLE.
>
>     Retrieving a variable's value returns either a NULL or a default
>     value, unless its value is set to something else in the current
>     session with a LET command. The content of a variable is not
>     transactional. This is the same as in regular variables in PL
>     languages.
>
>     Schema variables are retrieved by the SELECT SQL command. Their
>     value is set with the LET SQL command. While schema variables
>     share properties with tables, their value cannot be updated with
>     an UPDATE command.
>
>
>     The patch apply with the patch command without problem and
>     compilation reports no warning or errors. Regression tests pass
>     successfully using make check or make installcheck
>     Performances are near the set of plpgsql variable settings which
>     is impressive:
>
>     do $$
>     It should probably be better to not allow NOT NULL when IMMUTABLE
>     is used because the variable can not be used at all.  Also
>     probably IMMUTABLE without a DEFAULT value should also be
>     restricted as it makes no sens. If the user wants the variable to
>     be NULL he must use DEFAULT NULL. This is just a though, the above
>     error messages are explicit and the user can understand what wrong
>     declaration he have done.
>
>
> I wrote a check that disables this case.  Please, see the attached 
> patch. I agree, so this case is confusing, and it is better to disable it.
>

Great, I also think that this is better to not confuse the user.

     postgres=# CREATE IMMUTABLE VARIABLE var1 AS date NOT NULL;
     ERROR:  IMMUTABLE NOT NULL variable requires default expression

Working as expected. I have moved the patch to "Ready for committers". 
Thanks for this feature.

-- 
Gilles Darold
http://www.darold.net/

---

From: Pavel Stehule <[email-masked]>
Date: Wed, 8 Sep 2021 21:23:41 +0200

Hi

> Working as expected. I have moved the patch to "Ready for committers".

Thank you very much

Pavel

---

From: Pavel Stehule <[email-masked]>
Date: Thu, 9 Sep 2021 06:59:31 +0200

Hi

fresh rebase

Regards

Pavel

---

From: Erik Rijkers <[email-masked]>
Date: Thu, 9 Sep 2021 12:21:19 +0200

> [schema-variables-20210909.patch]

Hi Pavel,

The patch applies and compiles fine but 'make check' for the 
assert-enabled fails on 131 out of 210 tests.

(while compiling HEAD checks run without errors for both assert-disabled 
and assert-enabled)

Erik Rijkers

test tablespace                   ... ok          303 ms
parallel group (20 tests):  oid char pg_lsn int2 varchar txid int4 
regproc uuid float4 text name money boolean bit float8 int8 enum numeric 
rangetypes
      boolean                      ... ok          112 ms
      char                         ... ok           57 ms
      name                         ... ok          106 ms
      varchar                      ... ok           74 ms
      text                         ... ok          106 ms
      int2                         ... ok           73 ms
      int4                         ... ok           92 ms
      int8                         ... ok          130 ms
      oid                          ... ok           55 ms
      float4                       ... ok          102 ms
      float8                       ... ok          126 ms
      bit                          ... ok          124 ms
      numeric                      ... ok          362 ms
      txid                         ... ok           87 ms
      uuid                         ... ok          100 ms
      enum                         ... ok          142 ms
      money                        ... ok          109 ms
      rangetypes                   ... ok          433 ms
      pg_lsn                       ... ok           64 ms
      regproc                      ... ok           91 ms
parallel group (20 tests):  lseg path circle time macaddr 
create_function_0 timetz line macaddr8 numerology point interval inet 
date strings polygon box multirangetypes timestamp timestamptz
      strings                      ... ok          166 ms
      numerology                   ... ok           89 ms
      point                        ... ok           96 ms
      lseg                         ... ok           35 ms
      line                         ... ok           70 ms
      box                          ... ok          255 ms
      path                         ... ok           50 ms
      polygon                      ... ok          237 ms
      circle                       ... ok           53 ms
      date                         ... ok          127 ms
      time                         ... ok           60 ms
      timetz                       ... ok           67 ms
      timestamp                    ... ok          379 ms
      timestamptz                  ... ok          413 ms
      interval                     ... ok           97 ms
      inet                         ... ok          118 ms
      macaddr                      ... ok           60 ms
      macaddr8                     ... ok           80 ms
      multirangetypes              ... ok          307 ms
      create_function_0            ... ok           63 ms
parallel group (12 tests):  comments unicode misc_sanity tstypes xid 
expressions horology geometry mvcc type_sanity regex opr_sanity
      geometry                     ... ok          140 ms
      horology                     ... ok          120 ms
      tstypes                      ... ok           53 ms
      regex                        ... ok          335 ms
      type_sanity                  ... ok          155 ms
      opr_sanity                   ... ok          355 ms
      misc_sanity                  ... ok           43 ms
      comments                     ... ok           20 ms
      expressions                  ... ok          100 ms
      unicode                      ... ok           25 ms
      xid                          ... ok           56 ms
      mvcc                         ... ok          146 ms
test create_function_1            ... ok           10 ms
test create_type                  ... ok           30 ms
test create_table                 ... ok          333 ms
test create_function_2            ... ok           11 ms
parallel group (5 tests):  copydml copyselect insert_conflict insert copy
      copy                         ... ok          336 ms
      copyselect                   ... ok           34 ms
      copydml                      ... ok           28 ms
      insert                       ... ok          291 ms
      insert_conflict              ... FAILED (test process exited with 
exit code 2)      239 ms
parallel group (3 tests):  create_operator create_procedure create_misc
      create_misc                  ... ok          131 ms
      create_operator              ... ok           29 ms
      create_procedure             ... ok           52 ms
parallel group (5 tests):  create_view create_index_spgist 
index_including create_index index_including_gist
      create_index                 ... FAILED (test process exited with 
exit code 2)     3801 ms
      create_index_spgist          ... ok          523 ms
      create_view                  ... FAILED (test process exited with 
exit code 2)      339 ms
      index_including              ... FAILED (test process exited with 
exit code 2)     3801 ms
      index_including_gist         ... FAILED (test process exited with 
exit code 2)     3801 ms
parallel group (16 tests):  create_aggregate create_cast typed_table 
drop_if_exists roleattributes create_am hash_func updatable_views errors 
infinite_recurse create_function_3 triggers constraints select inherit 
vacuum
      create_aggregate             ... FAILED (test process exited with 
exit code 2)      164 ms
      create_function_3            ... FAILED (test process exited with 
exit code 2)      164 ms
      create_cast                  ... FAILED (test process exited with 
exit code 2)      164 ms
      constraints                  ... FAILED (test process exited with 
exit code 2)      181 ms
      triggers                     ... FAILED (test process exited with 
exit code 2)      181 ms
      select                       ... FAILED (test process exited with 
exit code 2)      181 ms
      inherit                      ... FAILED (test process exited with 
exit code 2)      181 ms
      typed_table                  ... FAILED (test process exited with 
exit code 2)      163 ms
      vacuum                       ... FAILED (test process exited with 
exit code 2)      180 ms
      drop_if_exists               ... FAILED (test process exited with 
exit code 2)      163 ms
      updatable_views              ... FAILED (test process exited with 
exit code 2)      163 ms
      roleattributes               ... FAILED (test process exited with 
exit code 2)      163 ms
      create_am                    ... FAILED (test process exited with 
exit code 2)      163 ms
      hash_func                    ... FAILED (test process exited with 
exit code 2)      162 ms
      errors                       ... FAILED (test process exited with 
exit code 2)      162 ms
      infinite_recurse             ... FAILED (test process exited with 
exit code 2)      162 ms
test sanity_check                 ... FAILED (test process exited with 
exit code 2)       26 ms
parallel group (20 tests):  select_into subselect select_distinct arrays 
join namespace hash_index select_having portals transactions aggregates 
random update delete union btree_index select_implicit 
select_distinct_on prepared_xacts case
      select_into                  ... FAILED (test process exited with 
exit code 2)       20 ms
      select_distinct              ... FAILED (test process exited with 
exit code 2)       21 ms
      select_distinct_on           ... FAILED (test process exited with 
exit code 2)       26 ms
      select_implicit              ... FAILED (test process exited with 
exit code 2)       26 ms
      select_having                ... FAILED (test process exited with 
exit code 2)       23 ms
      subselect                    ... FAILED (test process exited with 
exit code 2)       20 ms
      union                        ... FAILED (test process exited with 
exit code 2)       25 ms
      case                         ... FAILED (test process exited with 
exit code 2)       27 ms
      join                         ... FAILED (test process exited with 
exit code 2)       22 ms
      aggregates                   ... FAILED (test process exited with 
exit code 2)       24 ms
      transactions                 ... FAILED (test process exited with 
exit code 2)       24 ms
      random                       ... failed (ignored) (test process 
exited with exit code 2)       24 ms
      portals                      ... FAILED (test process exited with 
exit code 2)       23 ms
      arrays                       ... FAILED (test process exited with 
exit code 2)       20 ms
      btree_index                  ... FAILED (test process exited with 
exit code 2)       25 ms
      hash_index                   ... FAILED (test process exited with 
exit code 2)       22 ms
      update                       ... FAILED (test process exited with 
exit code 2)       23 ms
      delete                       ... FAILED (test process exited with 
exit code 2)       24 ms
      namespace                    ... FAILED (test process exited with 
exit code 2)       21 ms
      prepared_xacts               ... FAILED (test process exited with 
exit code 2)       25 ms
parallel group (20 tests):  gist brin identity generated password 
tablesample lock matview replica_identity rowsecurity security_label 
object_address drop_operator groupingsets join_hash privileges collate 
init_privs spgist gin
      brin                         ... FAILED (test process exited with 
exit code 2)       15 ms
      gin                          ... FAILED (test process exited with 
exit code 2)       22 ms
      gist                         ... FAILED (test process exited with 
exit code 2)       13 ms
      spgist                       ... FAILED (test process exited with 
exit code 2)       22 ms
      privileges                   ... FAILED (test process exited with 
exit code 2)       19 ms
      init_privs                   ... FAILED (test process exited with 
exit code 2)       21 ms
      security_label               ... FAILED (test process exited with 
exit code 2)       17 ms
      collate                      ... FAILED (test process exited with 
exit code 2)       20 ms
      matview                      ... FAILED (test process exited with 
exit code 2)       17 ms
      lock                         ... FAILED (test process exited with 
exit code 2)       15 ms
      replica_identity             ... FAILED (test process exited with 
exit code 2)       17 ms
      rowsecurity                  ... FAILED (test process exited with 
exit code 2)       17 ms
      object_address               ... FAILED (test process exited with 
exit code 2)       17 ms
      tablesample                  ... FAILED (test process exited with 
exit code 2)       15 ms
      groupingsets                 ... FAILED (test process exited with 
exit code 2)       17 ms
      drop_operator                ... FAILED (test process exited with 
exit code 2)       17 ms
      password                     ... FAILED (test process exited with 
exit code 2)       14 ms
      identity                     ... FAILED (test process exited with 
exit code 2)       13 ms
      generated                    ... FAILED (test process exited with 
exit code 2)       14 ms
      join_hash                    ... FAILED (test process exited with 
exit code 2)       18 ms
parallel group (2 tests):  brin_multi brin_bloom
      brin_bloom                   ... FAILED (test process exited with 
exit code 2)        4 ms
      brin_multi                   ... FAILED (test process exited with 
exit code 2)        4 ms
parallel group (14 tests):  async create_table_like collate.icu.utf8 
misc sysviews alter_operator tidscan tidrangescan alter_generic tsrf 
incremental_sort misc_functions tid dbsize
      create_table_like            ... FAILED (test process exited with 
exit code 2)       10 ms
      alter_generic                ... FAILED (test process exited with 
exit code 2)       15 ms
      alter_operator               ... FAILED (test process exited with 
exit code 2)       13 ms
      misc                         ... FAILED (test process exited with 
exit code 2)       11 ms
      async                        ... FAILED (test process exited with 
exit code 2)        9 ms
      dbsize                       ... FAILED (test process exited with 
exit code 2)       17 ms
      misc_functions               ... FAILED (test process exited with 
exit code 2)       14 ms
      sysviews                     ... FAILED (test process exited with 
exit code 2)       12 ms
      tsrf                         ... FAILED (test process exited with 
exit code 2)       14 ms
      tid                          ... FAILED (test process exited with 
exit code 2)       16 ms
      tidscan                      ... FAILED (test process exited with 
exit code 2)       13 ms
      tidrangescan                 ... FAILED (test process exited with 
exit code 2)       13 ms
      collate.icu.utf8             ... FAILED (test process exited with 
exit code 2)        9 ms
      incremental_sort             ... FAILED (test process exited with 
exit code 2)       13 ms
parallel group (6 tests):  amutils psql_crosstab collate.linux.utf8 psql 
stats_ext rules
      rules                        ... FAILED (test process exited with 
exit code 2)        8 ms
      psql                         ... FAILED (test process exited with 
exit code 2)        7 ms
      psql_crosstab                ... FAILED (test process exited with 
exit code 2)        7 ms
      amutils                      ... FAILED (test process exited with 
exit code 2)        7 ms
      stats_ext                    ... FAILED (test process exited with 
exit code 2)        8 ms
      collate.linux.utf8           ... FAILED (test process exited with 
exit code 2)        7 ms
test select_parallel              ... FAILED (test process exited with 
exit code 2)        3 ms
test write_parallel               ... FAILED (test process exited with 
exit code 2)        3 ms
parallel group (2 tests):  publication subscription
      publication                  ... FAILED (test process exited with 
exit code 2)        4 ms
      subscription                 ... FAILED (test process exited with 
exit code 2)        4 ms
parallel group (17 tests):  select_views foreign_key xmlmap window 
functional_deps tsearch cluster combocid bitmapops tsdicts equivclass 
guc indirect_toast advisory_lock foreign_data dependency portals_p2
      select_views                 ... FAILED (test process exited with 
exit code 2)       11 ms
      portals_p2                   ... FAILED (test process exited with 
exit code 2)       20 ms
      foreign_key                  ... FAILED (test process exited with 
exit code 2)       11 ms
      cluster                      ... FAILED (test process exited with 
exit code 2)       15 ms
      dependency                   ... FAILED (test process exited with 
exit code 2)       19 ms
      guc                          ... FAILED (test process exited with 
exit code 2)       16 ms
      bitmapops                    ... FAILED (test process exited with 
exit code 2)       16 ms
      combocid                     ... FAILED (test process exited with 
exit code 2)       15 ms
      tsearch                      ... FAILED (test process exited with 
exit code 2)       14 ms
      tsdicts                      ... FAILED (test process exited with 
exit code 2)       16 ms
      foreign_data                 ... FAILED (test process exited with 
exit code 2)       17 ms
      window                       ... FAILED (test process exited with 
exit code 2)       12 ms
      xmlmap                       ... FAILED (test process exited with 
exit code 2)       12 ms
      functional_deps              ... FAILED (test process exited with 
exit code 2)       13 ms
      advisory_lock                ... FAILED (test process exited with 
exit code 2)       16 ms
      indirect_toast               ... FAILED (test process exited with 
exit code 2)       16 ms
      equivclass                   ... FAILED (test process exited with 
exit code 2)       15 ms
parallel group (6 tests):  json json_encoding jsonb jsonpath_encoding 
jsonb_jsonpath jsonpath
      json                         ... FAILED (test process exited with 
exit code 2)        4 ms
      jsonb                        ... FAILED (test process exited with 
exit code 2)        6 ms
      json_encoding                ... FAILED (test process exited with 
exit code 2)        5 ms
      jsonpath                     ... FAILED (test process exited with 
exit code 2)       10 ms
      jsonpath_encoding            ... FAILED (test process exited with 
exit code 2)        6 ms
      jsonb_jsonpath               ... FAILED (test process exited with 
exit code 2)        7 ms
parallel group (19 tests):  plpgsql limit rowtypes sequence largeobject 
returning domain polymorphism plancache prepare alter_table truncate 
temp rangefuncs with copy2 conversion schema_variables xml
      plancache                    ... FAILED (test process exited with 
exit code 2)       16 ms
      limit                        ... FAILED (test process exited with 
exit code 2)       10 ms
      plpgsql                      ... FAILED (test process exited with 
exit code 2)        7 ms
      copy2                        ... FAILED (test process exited with 
exit code 2)       25 ms
      temp                         ... FAILED (test process exited with 
exit code 2)       21 ms
      domain                       ... FAILED (test process exited with 
exit code 2)       13 ms
      rangefuncs                   ... FAILED (test process exited with 
exit code 2)       22 ms
      prepare                      ... FAILED (test process exited with 
exit code 2)       19 ms
      conversion                   ... FAILED (test process exited with 
exit code 2)       24 ms
      truncate                     ... FAILED (test process exited with 
exit code 2)       19 ms
      alter_table                  ... FAILED (test process exited with 
exit code 2)       18 ms
      sequence                     ... FAILED (test process exited with 
exit code 2)       11 ms
      polymorphism                 ... FAILED (test process exited with 
exit code 2)       13 ms
      rowtypes                     ... FAILED (test process exited with 
exit code 2)       10 ms
      returning                    ... FAILED (test process exited with 
exit code 2)       11 ms
      largeobject                  ... FAILED (test process exited with 
exit code 2)       10 ms
      with                         ... FAILED (test process exited with 
exit code 2)       22 ms
      xml                          ... FAILED (test process exited with 
exit code 2)       23 ms
      schema_variables             ... FAILED (test process exited with 
exit code 2)       23 ms
parallel group (11 tests):  explain hash_part partition_info reloptions 
memoize compression partition_aggregate partition_join indexing 
partition_prune tuplesort
      partition_join               ... FAILED      902 ms
      partition_prune              ... ok         1006 ms
      reloptions                   ... ok          106 ms
      hash_part                    ... ok           99 ms
      indexing                     ... ok          929 ms
      partition_aggregate          ... ok          791 ms
      partition_info               ... ok          104 ms
      tuplesort                    ... ok         1099 ms
      explain                      ... ok           90 ms
      compression                  ... ok          214 ms
      memoize                      ... ok          109 ms
parallel group (2 tests):  event_trigger oidjoins
      event_trigger                ... ok          107 ms
      oidjoins                     ... ok          157 ms
test fast_default                 ... ok          138 ms
test stats                        ... ok          617 ms

---

From: Pavel Stehule <[email-masked]>
Date: Thu, 9 Sep 2021 12:40:23 +0200

Hi

> assert-enabled fails on 131 out of 210 tests.

This morning I tested it. I'll recheck it.

Pavel

---

From: Gilles Darold <[email-masked]>
Date: Thu, 9 Sep 2021 12:17:31 +0100

Le 09/09/2021 à 11:40, Pavel Stehule a écrit :
> Hi
>
> čt 9. 9. 2021 v 12:21 odesílatel Erik Rijkers <[email-masked] 
> <mailto:[email-masked]>> napsal:
>
>      > [schema-variables-20210909.patch]
>
>     Hi Pavel,
>

I had not this problem yesterday.

-- 
Gilles Darold
http://www.darold.net/

---

From: Pavel Stehule <[email-masked]>
Date: Thu, 9 Sep 2021 18:11:41 +0200

> I had not this problem yesterday.

I am able to reproduce it. Looks like some current changes of Nodes don't
work with this patch. I have to investigate it.

Regards

Pavel

---

From: Pavel Stehule <[email-masked]>
Date: Fri, 10 Sep 2021 10:06:04 +0200

Hi

> (while compiling HEAD checks run without errors for both assert-disabled

Please, check, attached patch. I fixed a routine for processing a list of
identifiers - now it works with the identifier's node more sensitive.
Previous implementation of strVal was more tolerant.

Regards

Pavel

---

From: Erik Rijkers <[email-masked]>
Date: Fri, 10 Sep 2021 10:32:24 +0200

> [schema-variables-20210910.patch]

Apply, compile, make, & check(-world), and my small testsuite OK.

So all's well again - Ready for committer!

Thanks,

Erik Rijkers

---

From: Pavel Stehule <[email-masked]>
Date: Fri, 10 Sep 2021 10:51:57 +0200

> So all's well again - Ready for committer!

Thank you for check and for report

Regards

Pavel

---

From: Jaime Casanova <[email-masked]>
Date: Sat, 11 Sep 2021 21:13:38 -0500

Hi Pavel,

Just noted that there is no support for REASSIGN OWNED BY:

"""
regression=# create variable random_number numeric;
CREATE VARIABLE
regression=# alter variable random_number owner to jcm;
ALTER VARIABLE
regression=# reassign owned by jcm to jaime;
ERROR:  unexpected classid 9222
"""

TEMP variables are not schema variables? at least not attached to the
schema one expects:

"""
regression=# create temp variable random_number numeric ;
CREATE VARIABLE
regression=# \dV
                                               List of variables
  Schema   |     Name      |  Type   | Is nullable | Is mutable | Default |  Owner   | Transaction
al end action
-----------+---------------+---------+-------------+------------+---------+----------+------------
--------------
 pg_temp_4 | random_number | numeric | t           | t          |         | jcasanov |
(1 row)

regression=# select public.random_number;
ERROR:  missing FROM-clause entry for table "public"
LINE 1: select public.random_number;
               ^
"""

There was a comment that TEMP variables should be DECLAREd instead of
CREATEd, i guess that is because those have similar behaviour. At least,
I would like to see similar messages when using the ON COMMIT DROP
option in a TEMP variable:

"""
regression=# create temp variable random_number numeric on commit drop;
CREATE VARIABLE
regression=# \dV
Did not find any schema variables.
regression=# declare q cursor  for select 1;
ERROR:  DECLARE CURSOR can only be used in transaction blocks
"""

About that, why are you not using syntax ON COMMIT RESET instead on
inventing ON TRANSACTION END RESET? seems better because you already use
ON COMMIT DROP.

I will test more this patch tomorrow. Great work, very complete.

-- 
Jaime Casanova
Director de Servicios Profesionales
SystemGuards - Consultores de PostgreSQL

---

From: Pavel Stehule <[email-masked]>
Date: Sun, 12 Sep 2021 17:38:42 +0200

Hi

> ERROR:  unexpected classid 9222
should be fixed by the attached patch, please check.

> TEMP variables are not schema variables? at least not attached to the

temp variables are schema variables like any other. But they are created in
temp schema - like temp tables.
I designed it in consistency with temporary tables.

>   Schema   |     Name      |  Type   | Is nullable | Is mutable | Default

I don't remember this comment. When I talked about similarity with the
DECLARE statement, I thought about semantic similarity with T-SQL
(Microsoft SQL) DECLARE command. Unfortunately, DECLARE command is pretty
messy - it exists in SQL, it exists in SQL/PSM and it exists in T-SQL - and
every time has similar syntax, but partially different semantics. For me -
CREATE TEMP VARIABLE creates session's life limited variable (by default),
similarly like DECLARE @localvariable command from T-SQL.

> ERROR:  DECLARE CURSOR can only be used in transaction blocks

I have different result

postgres=# create temp variable random_number numeric on commit drop;
CREATE VARIABLE
postgres=# \dV
                                             List of variables
┌────────┬───────────────┬─────────┬─────────────┬────────────┬─────────┬───────┬──────────────────────────┐
│ Schema │     Name      │  Type   │ Is nullable │ Is mutable │ Default │
Owner │ Transactional end action │
╞════════╪═══════════════╪═════════╪═════════════╪════════════╪═════════╪═══════╪══════════════════════════╡
│ public │ random_number │ numeric │ t           │ t          │         │
tom2  │                          │
└────────┴───────────────┴─────────┴─────────────┴────────────┴─────────┴───────┴──────────────────────────┘
(1 row)

> inventing ON TRANSACTION END RESET? seems better because you already use

I thought about this question for a very long time, and I think the
introduction of a new clause is better, and I will try to explain why.

One part of this patch are DDL statements - and all DDL statements are
consistent with other DDL statements in Postgres. Schema variables DDL
commands are transactional and for TEMP variables we can specify a scope -
session or transaction, and then clause ON COMMIT DROP is used. You should
not need to specify ON ROLLBACK action, because in this case an removing
from system catalogue is only one possible action.

Second part of this patch is holding some value in schema variables or
initialization with default expression. The default behaviour is not
transactional, and the value is stored all session's time by default. But I
think it can be very useful to enforce initialization in some specific
times - now only the end of the transaction is possible to specify. In the
future there can be transaction end, transaction start, rollback, commit,
top query start, top query end, ... This logic is different from the logic
of DDL commands.  For DDL commands I need to specify behaviour just for the
COMMIT end. But for reset of non-transactional schema variables I need to
specify any possible end of transaction - COMMIT, ROLLBACK or COMMIT or
ROLLBACK. In this initial version I implemented "ON COMMIT OR ROLLBACK
RESET", and although it is clean I think it is more readable is the clause
that I invented "ON TRANSACTION END". "ON COMMIT RESET" is not exact. "ON
COMMIT OR ROLLBACK RESET" sounds a little bit strange for me, but we use
something similar in trigger definition "ON INSERT OR UPDATE OR DELETE ..."
My opinion is not too strong if "ON TRANSACTION END  RESET"  or "ON COMMIT
OR ROLLBACK RESET" is better, and I can change it if people will have
different preferences, but I am sure so "ON COMMIT RESET" is not correct in
implemented case. And from the perspective of a PLpgSQL developer, I would
have initialized the variable on any transaction start, so I need to reset
it on any end.

Regards

Pavel

---

From: Pavel Stehule <[email-masked]>
Date: Sun, 12 Sep 2021 17:45:25 +0200

ne 12. 9. 2021 v 17:38 odesílatel Pavel Stehule <[email-masked]>
napsal:

> temp variables are schema variables like any other. But they are created
> in temp schema - like temp tables.

any value of a schema variable has a session (or transaction) life cycle.
But the schema variable itself is persistent.  temp schema variable is an
exception. It is limited by session (and the value stored in the variable
is limited to session too).

---

From: Jaime Casanova <[email-masked]>
Date: Sun, 12 Sep 2021 11:26:00 -0500

Hi, 

Thanks, will test rebased version.
BTW, that is not the temp variable. You can note it because of the
schema or the lack of a "Transaction end action". That is a normal
non-temp variable that has been created before. A TEMP variable with an
ON COMMIT DROP created outside an explicit transaction will disappear
immediatly like cursor does in the same situation.

---

From: Pavel Stehule <[email-masked]>
Date: Sun, 12 Sep 2021 19:15:05 +0200

Hi,
> immediatly like cursor does in the same situation.

Unfortunately, I don't see it - or I don't understand to your example from
morning mail well

"""
regression=# create temp variable random_number numeric ;
CREATE VARIABLE
regression=# \dV
                                               List of variables
  Schema   |     Name      |  Type   | Is nullable | Is mutable | Default
|  Owner   | Transaction
al end action
-----------+---------------+---------+-------------+------------+---------+----------+------------
--------------
 pg_temp_4 | random_number | numeric | t           | t          |         |
jcasanov |
(1 row)

---

From: Pavel Stehule <[email-masked]>
Date: Thu, 16 Sep 2021 07:15:31 +0200

Hi

just rebase of patch

Regards

Pavel
index 71ae423f63..2d6555497c 100644

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 2f0def9b19..942508faa6 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -359,6 +359,11 @@
       <entry><link linkend="catalog-pg-user-mapping"><structname>pg_user_mapping</structname></link></entry>
       <entry>mappings of users to foreign servers</entry>
      </row>
+
+     <row>
+      <entry><link linkend="catalog-pg-variable"><structname>pg_variable</structname></link></entry>
+      <entry>schema variables</entry>
+     </row>
     </tbody>
    </tgroup>
   </table>
@@ -13889,4 +13894,143 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
 
  </sect1>
 
index 4cd4bcba80..86e6cd3f27 100644
@@ -5942,6 +5942,19 @@ $$ LANGUAGE plpgsql STRICT IMMUTABLE;

 
 
  <refsynopsisdiv>
 <synopsis>

+   <varlistentry>

+    <varlistentry>
 
+     <varlistentry>
 
index 6597ec45a9..b364791dfd 100644

@@ -2284,6 +2288,7 @@ CommitTransaction(void)
 	AtEOXact_SPI(true);
 	AtEOXact_Enum();
 	AtEOXact_on_commit_actions(true);
+	AtEOXact_SchemaVariable_on_commit_actions(true);
 	AtEOXact_Namespace(true, is_parallel_worker);
 	AtEOXact_SMgr();
 	AtEOXact_Files(true);
@@ -2712,6 +2717,9 @@ AbortTransaction(void)
 	AtAbort_Portals();
 	smgrDoPendingSyncs(false, is_parallel_worker);
 	AtEOXact_LargeObject(false);
+
+	/* 'false' means it's abort */
+	AtPreEOXact_SchemaVariable_on_commit_actions(false);
 	AtAbort_Notify();
 	AtEOXact_RelationMap(false, is_parallel_worker);
 	AtAbort_Twophase();
@@ -2776,6 +2784,7 @@ AbortTransaction(void)
 		AtEOXact_SPI(false);
 		AtEOXact_Enum();
 		AtEOXact_on_commit_actions(false);
+		AtEOXact_SchemaVariable_on_commit_actions(false);
 		AtEOXact_Namespace(false, is_parallel_worker);
 		AtEOXact_SMgr();
 		AtEOXact_Files(false);
diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile
index d297e77361..22616e7d5c 100644
--- a/src/backend/catalog/Makefile
+++ b/src/backend/catalog/Makefile
@@ -43,6 +43,7 @@ OBJS = \
 	pg_shdepend.o \
 	pg_subscription.o \
 	pg_type.o \
+	pg_variable.o \
 	storage.o \
 	toasting.o
 
@@ -69,7 +70,7 @@ CATALOG_HEADERS := \

index 89792b154e..316e897813 100644
@@ -112,6 +113,7 @@ static void ExecGrant_Largeobject(InternalGrant *grantStmt);
 
@@ -259,6 +261,9 @@ restrict_and_check_grant(bool is_grant, AclMode avail_goptions, bool all_privs,
@@ -498,6 +503,10 @@ ExecuteGrantStmt(GrantStmt *stmt)
@@ -600,6 +609,9 @@ ExecGrantStmt_oids(InternalGrant *istmt)
@@ -759,6 +771,16 @@ objectNamesToOids(ObjectType objtype, List *objnames)
@@ -848,6 +870,33 @@ objectsInSchemaToOids(ObjectType objtype, List *nspnames)
@@ -1007,6 +1056,10 @@ ExecAlterDefaultPrivilegesStmt(ParseState *pstate, AlterDefaultPrivilegesStmt *s
@@ -1204,6 +1257,12 @@ SetDefaultACL(InternalDefaultACL *iacls)
 
+		case OBJECT_VARIABLE:
@@ -1437,6 +1496,9 @@ RemoveRoleFromObjectACL(Oid roleid, Oid classid, Oid objid)
@@ -1494,6 +1556,9 @@ RemoveRoleFromObjectACL(Oid roleid, Oid classid, Oid objid)
@@ -3225,6 +3290,129 @@ ExecGrant_Type(InternalGrant *istmt)
 
+static void
 
 static AclMode
 string_to_privilege(const char *privname)
@@ -3257,6 +3445,10 @@ string_to_privilege(const char *privname)
 		return ACL_CONNECT;
 	if (strcmp(privname, "rule") == 0)
 		return 0;				/* ignore old RULE privileges */
+	if (strcmp(privname, "read") == 0)
+		return ACL_READ;
+	if (strcmp(privname, "write") == 0)
+		return ACL_WRITE;
 	ereport(ERROR,
 			(errcode(ERRCODE_SYNTAX_ERROR),
 			 errmsg("unrecognized privilege type \"%s\"", privname)));
@@ -3292,6 +3484,10 @@ privilege_to_string(AclMode privilege)
 			return "TEMP";
 		case ACL_CONNECT:
 			return "CONNECT";
+		case ACL_READ:
+			return "READ";
+		case ACL_WRITE:
+			return "WRITE";
 		default:
 			elog(ERROR, "unrecognized privilege: %d", (int) privilege);
 	}
@@ -3415,6 +3611,9 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
 					case OBJECT_TYPE:
 						msg = gettext_noop("permission denied for type %s");
 						break;
+					case OBJECT_VARIABLE:
+						msg = gettext_noop("permission denied for schema variable %s");
+						break;
 					case OBJECT_VIEW:
 						msg = gettext_noop("permission denied for view %s");
 						break;
@@ -3525,6 +3724,9 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
 					case OBJECT_TYPE:
 						msg = gettext_noop("must be owner of type %s");
 						break;
+					case OBJECT_VARIABLE:
+						msg = gettext_noop("must be owner of schema variable %s");
+						break;
 					case OBJECT_VIEW:
 						msg = gettext_noop("must be owner of view %s");
 						break;
@@ -3669,6 +3871,8 @@ pg_aclmask(ObjectType objtype, Oid table_oid, AttrNumber attnum, Oid roleid,
 			return ACL_NO_RIGHTS;
 		case OBJECT_TYPE:
 			return pg_type_aclmask(table_oid, roleid, mask, how);
+		case OBJECT_VARIABLE:
+			return pg_variable_aclmask(table_oid, roleid, mask, how);
 		default:
 			elog(ERROR, "unrecognized objtype: %d",
 				 (int) objtype);
@@ -4537,6 +4741,66 @@ pg_type_aclmask(Oid type_oid, Oid roleid, AclMode mask, AclMaskHow how)
 	return result;
 }
 
+/*
@@ -4811,6 +5075,18 @@ pg_type_aclcheck(Oid type_oid, Oid roleid, AclMode mode)
 		return ACLCHECK_NO_PRIV;
 }
 
+/*
@@ -5428,6 +5704,33 @@ pg_statistics_object_ownercheck(Oid stat_oid, Oid roleid)
 
+/*
@@ -5556,6 +5859,10 @@ get_user_default_acl(ObjectType objtype, Oid ownerId, Oid nsp_oid)
 
+		case OBJECT_VARIABLE:
index 91c3e976e0..03b3b9fcf2 100644
@@ -62,12 +62,15 @@

@@ -1486,6 +1490,10 @@ doDeletion(const ObjectAddress *object, int flags)
 			DropObjectById(object);
 			break;
 
+		case OCLASS_VARIABLE:
+			RemoveSchemaVariable(object->objectId);
+			break;
+
 			/*
 			 * These global object types are not supported here.
 			 */
@@ -1864,6 +1872,11 @@ find_expr_references_walker(Node *node,
 	{
 		Param	   *param = (Param *) node;
 
@@ -2861,6 +2874,9 @@ getObjectClass(const ObjectAddress *object)

 	/* shouldn't get here */
index 4de8400fd0..02c5de7f73 100644
@@ -763,6 +764,70 @@ RelationIsVisible(Oid relid)
 	return visible;
 }
 
+/*
+ * VariableIsVisible
 
 /*
  * TypenameGetTypid
@@ -2842,6 +2907,271 @@ TSConfigIsVisible(Oid cfgid)
 	return visible;
 }
 
+/*
+ * When we know a variable name, then we can find variable simply
+ */
+Oid
+LookupVariable(const char *nspname, const char *varname, bool missing_ok)
+{
+	Oid			namespaceId;
+	Oid			varoid = InvalidOid;
+	ListCell   *l;
+
+	if (nspname)
+	{
+		namespaceId = LookupExplicitNamespace(nspname, missing_ok);
+		if (!OidIsValid(namespaceId))
+			return InvalidOid;
+
+		varoid = GetSysCacheOid2(VARIABLENAMENSP, Anum_pg_variable_oid,
+								 PointerGetDatum(varname),
+								 ObjectIdGetDatum(namespaceId));
+	}
+	else
+	{
+		/* search for it in search path */
+		recomputeNamespacePath();
+
+		foreach(l, activeSearchPath)
+		{
+			namespaceId = lfirst_oid(l);
+
+			varoid = GetSysCacheOid2(VARIABLENAMENSP, Anum_pg_variable_oid,
+									 PointerGetDatum(varname),
+									 ObjectIdGetDatum(namespaceId));
+
+			if (OidIsValid(varoid))
+				break;
+		}
+	}
+
+	if (!OidIsValid(varoid) && !missing_ok)
+	{
+		if (nspname)
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("variable \"%s\".\"%s\" does not exist",
+							nspname, varname)));
+		else
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("variable \"%s\" does not exist",
+							varname)));
+	}
+
+	return varoid;
+}
+
+/*
+ * The source list holds names with indirection expressions used
+ * as left part of LET statement. Following routine makes 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
+ *
+ * Returns oid of not ambigonuous variable specified by qualified path
+ * or InvalidOid. When the path is ambigonuous, then not_uniq flag is
+ * is true.
+ */
+Oid
+IdentifyVariable(List *names, char **attrname, bool *not_uniq)
+{
+	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			varoid_without_attr = InvalidOid;
+	Oid			varoid_with_attr = InvalidOid;
+
+	*not_uniq = false;
+
+	switch (list_length(names))
+	{
+		case 1:
+			field1 = linitial(names);
+
+			Assert(IsA(field1, String));
+
+			return LookupVariable(NULL, strVal(field1), true);
+
+		case 2:
+			field1 = linitial(names);
+			field2 = lsecond(names);
+
+			Assert(IsA(field1, String));
+			a = strVal(field1);
+
+			if (IsA(field2, String))
+			{
+				b = strVal(field2);
+
+				/*
+				 * a.b can mean "schema"."variable" or "variable"."field", Check
+				 * both variants, and returns InvalidOid with not_uniq flag, when
+				 * both interpretations are possible. Second node can be star. In
+				 * this case, the only allowed possibility is "variable"."*".
+				 */
+				varoid_without_attr = LookupVariable(a, b, true);
+				varoid_with_attr = LookupVariable(NULL, a, true);
+			}
+			else
+			{
+				Assert(IsA(field2, A_Star));
+
+				/*
+				 * Schema variables doesn't support unboxing by star syntax. But
+				 * this syntax have to be calculated here, because can come from
+				 * non schema variables related expressions.
+				 */
+				return InvalidOid;
+			}
+
+			if (OidIsValid(varoid_without_attr) && OidIsValid(varoid_with_attr))
+			{
+				*not_uniq = true;
+				return InvalidOid;
+			}
+			else if (OidIsValid(varoid_without_attr))
+			{
+				*attrname = NULL;
+				return varoid_without_attr;
+			}
+			else
+			{
+				*attrname = b;
+				return varoid_with_attr;
+			}
+			break;
+
+		case 3:
+			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 returns
+				 * InvalidOid with not_uniq flag, when both interpretations are
+				 * possible. When third node is star, then only possible
+				 * interpretation is "schema"."variable"."*".
+				 */
+				varoid_without_attr = LookupVariable(b, c, true);
+				varoid_with_attr = LookupVariable(a, b, true);
+			}
+			else
+			{
+				Assert(IsA(field3, A_Star));
+				return InvalidOid;
+			}
+
+			if (OidIsValid(varoid_without_attr) && OidIsValid(varoid_with_attr))
+			{
+				*not_uniq = true;
+				return InvalidOid;
+			}
+			else if (OidIsValid(varoid_without_attr))
+			{
+				*attrname = NULL;
+
+				/*
+				 * We in this case a "a" is used as catalog name, check it.
+				 */
+				if (strcmp(a, get_database_name(MyDatabaseId)) != 0)
+					ereport(ERROR,
+							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+							 errmsg("cross-database references are not implemented: %s",
+									NameListToString(names))));
+
+				return varoid_without_attr;
+			}
+			else
+			{
+				*attrname = c;
+				return varoid_with_attr;
+			}
+			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);
+
+			/*
+			 * We in this case a "a" is used as catalog name, check it.
+			 */
+			if (strcmp(a, get_database_name(MyDatabaseId)) != 0)
+				ereport(ERROR,
+						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+						 errmsg("cross-database references are not implemented: %s",
+								NameListToString(names))));
+
+			if (IsA(field4, String))
+			{
+				d = strVal(field4);
+			}
+			else
+			{
+				Assert(IsA(field4, A_Star));
+				return InvalidOid;
+			}
+
+			*attrname = d;
+			return LookupVariable(b, c, true);
+
+		default:
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("improper qualified name (too many dotted names): %s",
+							NameListToString(names))));
+			break;
+	}
+}
 
 /*
  * DeconstructQualifiedName
@@ -4657,3 +4987,14 @@ pg_is_other_temp_schema(PG_FUNCTION_ARGS)
 
index 8c94939baa..1a1cfd255e 100644
@@ -616,6 +617,20 @@ static const ObjectPropertyType ObjectProperty[] =
 
 /*
@@ -840,6 +855,10 @@ static const struct object_type_map
 	/* OCLASS_STATISTIC_EXT */
 	{
 		"statistics object", OBJECT_STATISTIC_EXT
+	},
+	/* OCLASS_VARIABLE */
+	{
+		"schema variable", OBJECT_VARIABLE
 	}
 };
 
@@ -865,6 +884,7 @@ static ObjectAddress get_object_address_attrdef(ObjectType objtype,
 												bool missing_ok);
@@ -1128,6 +1148,9 @@ get_object_address(ObjectType objtype, Node *object,
@@ -1984,16 +2007,20 @@ get_object_address_defacl(List *object, bool missing_ok)
 
 	/*
@@ -2078,6 +2105,24 @@ textarray_to_strvaluelist(ArrayType *arr)
 	return list;
 }
 
+/*
+ * Find the ObjectAddress for a schema variable
+ */
+static ObjectAddress
+get_object_address_variable(List *object, bool missing_ok)
+{
+	ObjectAddress address;
+	char	   *nspname = NULL;
+	char	   *varname = NULL;
+
+	ObjectAddressSet(address, VariableRelationId, InvalidOid);
+
+	DeconstructQualifiedName(object, &nspname, &varname);
+	address.objectId = LookupVariable(nspname, varname, missing_ok);
+
+	return address;
+}
+
 /*
  * SQL-callable version of get_object_address
  */
@@ -2267,6 +2312,7 @@ pg_get_object_address(PG_FUNCTION_ARGS)
 		case OBJECT_TABCONSTRAINT:
 		case OBJECT_OPCLASS:
 		case OBJECT_OPFAMILY:
+		case OBJECT_VARIABLE:
 			objnode = (Node *) name;
 			break;
 		case OBJECT_ACCESS_METHOD:
@@ -2574,6 +2620,11 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address,
 				aclcheck_error(ACLCHECK_NOT_OWNER, objtype,
 							   NameListToString(castNode(List, object)));
 			break;
+		case OBJECT_VARIABLE:
+			if (!pg_variable_ownercheck(address.objectId, roleid))
+				aclcheck_error(ACLCHECK_NOT_OWNER, objtype,
+							   NameListToString(castNode(List, object)));
+			break;
 		default:
 			elog(ERROR, "unrecognized object type: %d",
 				 (int) objtype);
@@ -3453,6 +3504,32 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok)
 				break;
 			}
 
+		case OCLASS_VARIABLE:
+			{
+				char	   *nspname;
+				HeapTuple	tup;
@@ -3763,6 +3840,16 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok)
@@ -4485,6 +4572,10 @@ getObjectTypeDescription(const ObjectAddress *object, bool missing_ok)
 
+		case OCLASS_VARIABLE:
@@ -5578,6 +5669,10 @@ getObjectIdentityParts(const ObjectAddress *object,
 
 				if (objname)
@@ -5769,6 +5864,33 @@ getObjectIdentityParts(const ObjectAddress *object,
 			}
 			break;
 
+		case OCLASS_VARIABLE:
+			{
+				char	   *schema;
+				char	   *varname;
+				HeapTuple	tup;
diff --git a/src/backend/catalog/pg_shdepend.c b/src/backend/catalog/pg_shdepend.c
index 4b676f5607..806da99343 100644
--- a/src/backend/catalog/pg_shdepend.c
+++ b/src/backend/catalog/pg_shdepend.c
@@ -46,6 +46,7 @@
@@ -1569,6 +1570,7 @@ shdepReassignOwned(List *roleids, Oid newrole)
 				case DatabaseRelationId:
 				case TSConfigRelationId:
 				case TSDictionaryRelationId:
 						Oid			classId = sdepForm->classid;
index c47d54e96b..c89e24fff0 100644

 
index 4e545adf95..1d14951e8c 100644
 			name = strVal(object);
index 71612d577e..92718acd24 100644

 			/*
@@ -2104,6 +2106,8 @@ stringify_grant_objtype(ObjectType objtype)
 			return "TABLESPACE";
 		case OBJECT_TYPE:
 			return "TYPE";
+		case OBJECT_VARIABLE:
+			return "VARIABLE";
@@ -2186,6 +2190,8 @@ stringify_adefprivs_objtype(ObjectType objtype)
index 10644dfac4..77d18cb9ac 100644
@@ -492,6 +492,22 @@ ExplainOneUtility(Node *utilityStmt, IntoClause *into, ExplainState *es,
index 0000000000..15e0567199
@@ -0,0 +1,881 @@
+DefineSchemaVariable(ParseState *pstate, CreateSchemaVarStmt *stmt)
+	if (stmt->is_not_null && stmt->is_immutable && !stmt->defexpr)
+				 errmsg("IMMUTABLE NOT NULL variable requires default expression")));
index ddc019cb39..a115c81d34 100644
 
 			/*
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index dbee6ae199..da862a6f6c 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -12255,6 +12255,7 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel,
 			case OCLASS_PUBLICATION_REL:
 			case OCLASS_SUBSCRIPTION:
 			case OCLASS_TRANSFORM:
+			case OCLASS_VARIABLE:
 
 				/*
index 11118d0ce0..71248a34f2 100644
 
index 81b9d87bad..35d9116412 100644
@@ -994,6 +995,60 @@ ExecInitExprRec(Expr *node, ExprState *state,
 
 						/*
index eb49817cee..ce34f7ff0c 100644
@@ -1078,6 +1080,16 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull)
 			EEO_NEXT();
 		}
 
index b3ce4bae53..95dac87ca7 100644
@@ -199,6 +202,52 @@ standard_ExecutorStart(QueryDesc *queryDesc, int eflags)
 	Assert(queryDesc->sourceText != NULL);
 
+	/*
index f8a4a40e7b..341c817371 100644

 
 
@@ -755,6 +770,11 @@ ExecInitParallelPlan(PlanState *planstate, EState *estate,
 
@@ -1402,6 +1422,7 @@ ParallelQueryMain(dsm_segment *seg, shm_toc *toc)
 
@@ -1427,6 +1448,14 @@ ParallelQueryMain(dsm_segment *seg, shm_toc *toc)
 	area_space = shm_toc_lookup(toc, PARALLEL_KEY_DSA, false);
 	area = dsa_attach_in_place(area_space, seg);
 
@@ -1496,3 +1525,118 @@ ParallelQueryMain(dsm_segment *seg, shm_toc *toc)
index a5aec7ba7d..5f35172d19 100644
@@ -2819,6 +2819,9 @@ _SPI_error_callback(void *arg)
index 6d1181225e..4299809eb5 100644
 
index 228387eaee..b8e8374523 100644
@@ -106,6 +106,7 @@ _copyPlannedStmt(const PlannedStmt *from)
 
@@ -1506,6 +1507,7 @@ _copyParam(const Param *from)
 
 	return newnode;
@@ -3162,6 +3164,7 @@ _copyQuery(const Query *from)
@@ -3172,6 +3175,7 @@ _copyQuery(const Query *from)
@@ -3285,6 +3289,19 @@ _copySelectStmt(const SelectStmt *from)
 	return newnode;
 }
 
+static LetStmt *
@@ -4876,6 +4893,23 @@ _copyDropSubscriptionStmt(const DropSubscriptionStmt *from)
 	return newnode;
 }
 
@@ -5389,6 +5423,9 @@ copyObjectImpl(const void *from)
@@ -5734,6 +5771,9 @@ copyObjectImpl(const void *from)
 			retval = _copyA_Expr(from);
 			break;
@@ -5899,7 +5939,7 @@ copyObjectImpl(const void *from)
 			break;
 
 		default:
index 800f588b5c..a5dc0c13c3 100644
@@ -215,6 +215,7 @@ _equalParam(const Param *a, const Param *b)
 
 	return true;
@@ -980,6 +981,7 @@ _equalQuery(const Query *a, const Query *b)
@@ -990,6 +992,7 @@ _equalQuery(const Query *a, const Query *b)
@@ -1093,6 +1096,16 @@ _equalSelectStmt(const SelectStmt *a, const SelectStmt *b)
 	return true;
 }
 
+static bool
@@ -2357,6 +2370,22 @@ _equalDropSubscriptionStmt(const DropSubscriptionStmt *a,
 	return true;
 }
 
+static bool
@@ -3396,6 +3425,9 @@ equal(const void *a, const void *b)
@@ -3741,6 +3773,9 @@ equal(const void *a, const void *b)
 			retval = _equalA_Expr(a, b);
index 2e5ed77e18..20f06beeae 100644
@@ -324,6 +324,7 @@ _outPlannedStmt(StringInfo str, const PlannedStmt *node)
@@ -1164,6 +1165,7 @@ _outParam(StringInfo str, const Param *node)
 
@@ -2276,6 +2278,7 @@ _outPlannerGlobal(StringInfo str, const PlannerGlobal *node)
@@ -2336,6 +2339,7 @@ _outPlannerInfo(StringInfo str, const PlannerInfo *node)
@@ -2870,6 +2874,17 @@ _outPLAssignStmt(StringInfo str, const PLAssignStmt *node)
 
+static void
@@ -3050,6 +3065,7 @@ _outQuery(StringInfo str, const Query *node)
@@ -3061,6 +3077,7 @@ _outQuery(StringInfo str, const Query *node)
 
@@ -3071,6 +3088,7 @@ _outQuery(StringInfo str, const Query *node)
@@ -4371,6 +4389,9 @@ outNode(StringInfo str, const void *obj)
index abf08b7a2f..ce58599c20 100644
 
 	READ_DONE();
@@ -1599,6 +1602,7 @@ _readPlannedStmt(void)
 
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 1e42d75465..9809c95fce 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -316,6 +316,7 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
 	glob->lastPlanNodeId = 0;
 	glob->transientPlan = false;
 	glob->dependsOnRole = false;
+	glob->schemaVariables = NIL;
 
 	/*
@@ -529,6 +530,7 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
 
@@ -678,6 +680,12 @@ subquery_planner(PlannerGlobal *glob, Query *parse,
 	 */
 	pull_up_subqueries(root);
 
+	/*
index 6ccec759bd..91717cd4e1 100644
 
 /*****************************************************************************
  *
@@ -1127,6 +1128,50 @@ set_plan_refs(PlannerInfo *root, Plan *plan, int rtoffset)
 	return plan;
 }
 
+/*
@@ -1758,10 +1803,14 @@ fix_expr_common(PlannerInfo *root, Node *node)
@@ -1780,6 +1829,52 @@ fix_param_node(PlannerInfo *root, Param *p)
 
@@ -1841,7 +1936,9 @@ fix_alternative_subplan(PlannerInfo *root, AlternativeSubPlan *asplan,
@@ -1863,7 +1960,8 @@ fix_scan_expr(PlannerInfo *root, Node *node, int rtoffset, double num_exec)
index 3412d31117..e1432563f5 100644
@@ -818,7 +819,8 @@ max_parallel_hazard_walker(Node *node, max_parallel_hazard_context *context)

@@ -2228,6 +2230,7 @@ convert_saop_to_hashed_saop_walker(Node *node, void *context)
@@ -2350,6 +2353,30 @@ eval_const_expressions_mutator(Node *node,
 
 				/*
 				 * Not replaceable, so just copy the Param (no need to
@@ -4734,7 +4761,7 @@ substitute_actual_parameters_mutator(Node *node,
 {
 	if (node == NULL)
 		return NULL;
-	if (IsA(node, Param))
+	if (IsA(node, Param) &&((Param *) node)->paramkind != PARAM_VARIABLE)
 	{
 		Param	   *param = (Param *) node;
 
diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
index 146ee8dd1e..8971cf7e80 100644
--- a/src/backend/parser/analyze.c
+++ b/src/backend/parser/analyze.c
@@ -25,8 +25,11 @@
 #include "postgres.h"
 
@@ -50,6 +53,7 @@
 #include "utils/syscache.h"
 
@@ -88,6 +92,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
@@ -289,6 +295,7 @@ transformStmt(ParseState *pstate, Node *parseTree)
 		case T_InsertStmt:
 		case T_UpdateStmt:
 		case T_DeleteStmt:
+		case T_LetStmt:
 			(void) test_raw_expression_coverage(parseTree, NULL);
 			break;
 		default:
@@ -358,6 +365,11 @@ transformStmt(ParseState *pstate, Node *parseTree)
 									   (CallStmt *) parseTree);
 			break;
 
+		case T_LetStmt:
 
 			/*
@@ -399,6 +411,7 @@ analyze_requires_snapshot(RawStmt *parseTree)
 		case T_UpdateStmt:
 		case T_SelectStmt:
 		case T_PLAssignStmt:
+		case T_LetStmt:
 			result = true;
 			break;
 
@@ -482,6 +495,8 @@ transformDeleteStmt(ParseState *pstate, DeleteStmt *stmt)
 	qry->hasTargetSRFs = pstate->p_hasTargetSRFs;
 	qry->hasAggs = pstate->p_hasAggs;

 	/* this must be done after collations, for reliable comparison of exprs */
@@ -899,6 +914,7 @@ transformInsertStmt(ParseState *pstate, InsertStmt *stmt)

 
@@ -1241,6 +1257,7 @@ transformSelectStmt(ParseState *pstate, SelectStmt *stmt)
 	Query	   *qry = makeNode(Query);
 	Node	   *qual;
 	ListCell   *l;
+	ParseExprKind		target_exprkind;

@@ -1269,9 +1286,19 @@ transformSelectStmt(ParseState *pstate, SelectStmt *stmt)
 	/* process the FROM clause */
 	transformFromClause(pstate, stmt->fromClause);

 	/* mark column origins */
 	markTargetListOrigins(pstate, qry->targetList);
@@ -1361,6 +1388,8 @@ transformSelectStmt(ParseState *pstate, SelectStmt *stmt)
 							   (LockingClause *) lfirst(l), false);
 	}

 	/* this must be done after collations, for reliable comparison of exprs */
@@ -1585,12 +1614,261 @@ transformValuesClause(ParseState *pstate, SelectStmt *stmt)
 	qry->jointree = makeFromExpr(pstate->p_joinlist, NULL);

 
 	return qry;
 }
 
+/*
+ * transformLetStmt -
@@ -1841,6 +2119,8 @@ transformSetOperationStmt(ParseState *pstate, SelectStmt *stmt)

 	/* this must be done after collations, for reliable comparison of exprs */
@@ -2369,6 +2649,7 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)

 
index e3068a374e..2f9d8f3a83 100644
 	ObjectType			objtype;
@@ -275,8 +276,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
@@ -286,7 +287,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
@@ -426,6 +427,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 				drop_option_list publication_table_list
+				let_target
 
@@ -449,6 +451,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);

 /*
@@ -677,7 +682,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	KEY

 	MAPPING MATCH MATERIALIZED MAXVALUE METHOD MINUTE_P MINVALUE MODE MONTH_P MOVE
@@ -716,8 +721,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	UESCAPE UNBOUNDED UNCOMMITTED UNENCRYPTED UNION UNIQUE UNKNOWN
 	UNLISTEN UNLOGGED UNTIL UPDATE USER USING

 
@@ -752,6 +757,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %token		MODE_PLPGSQL_ASSIGN1
 %token		MODE_PLPGSQL_ASSIGN2
 %token		MODE_PLPGSQL_ASSIGN3
+%token		MODE_PLPGSQL_LET

@@ -857,6 +863,13 @@ parse_toplevel:
 
 /*

@@ -4648,6 +4668,61 @@ create_extension_opt_item:
 				}
 		;
 
@@ -6335,6 +6410,7 @@ object_type_any_name:
 
 /*
@@ -7103,6 +7179,14 @@ privilege_target:
 					n->objs = $2;
 					$$ = n;
 				}
@@ -7143,6 +7227,14 @@ privilege_target:

@@ -7303,6 +7395,7 @@ defacl_privilege_target:

@@ -9005,6 +9098,25 @@ RenameStmt: ALTER AGGREGATE aggregate_with_argtypes RENAME TO name
 					n->missing_ok = false;
 					$$ = (Node *)n;
 				}
 
 opt_column: COLUMN
@@ -9333,6 +9445,25 @@ AlterObjectSchemaStmt:
 					n->missing_ok = false;
 					$$ = (Node *)n;
 				}
 
@@ -9586,6 +9717,14 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec

@@ -10856,6 +10995,7 @@ ExplainableStmt:
 			| CreateMatViewStmt
 			| RefreshMatViewStmt
 			| ExecuteStmt					/* by default all are $$=$1 */
+			| LetStmt
 		;
 
 /*****************************************************************************
@@ -10884,6 +11024,7 @@ PreparableStmt:
 			| InsertStmt
 			| UpdateStmt
 			| DeleteStmt					/* by default all are $$=$1 */
+			| LetStmt
 		;
 
 /*****************************************************************************
@@ -11301,6 +11442,47 @@ opt_hold: /* EMPTY */						{ $$ = 0; }
 			| WITHOUT HOLD					{ $$ = 0; }
 		;
 
@@ -15623,6 +15805,7 @@ unreserved_keyword:
@@ -15780,6 +15963,8 @@ unreserved_keyword:
@@ -16187,6 +16372,7 @@ bare_label_keyword:
@@ -16386,6 +16572,8 @@ bare_label_keyword:
index 7d829a05a9..f3b384efbe 100644
 
 			/*
 
 			if (isAgg)
@@ -905,6 +907,7 @@ transformWindowFuncCall(ParseState *pstate, WindowFunc *wfunc,
@@ -943,6 +946,9 @@ transformWindowFuncCall(ParseState *pstate, WindowFunc *wfunc,
 
 			/*
index 2d1a477154..6aaaa5bb29 100644

 /* GUC parameters */
 
 /*
  * transformExpr -
@@ -277,6 +281,7 @@ transformExprRecurse(ParseState *pstate, Node *expr)
 			 * processed it rather than passing it to transformExpr().
 			 */
 		case T_SetToDefault:
+			Assert(false);
 			ereport(ERROR,
 					(errcode(ERRCODE_SYNTAX_ERROR),
 					 errmsg("DEFAULT is not allowed in this context"),
@@ -443,6 +448,9 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
 	char	   *relname = NULL;
 	char	   *colname = NULL;
 	ParseNamespaceItem *nsitem;
+	Oid			varid = InvalidOid;
+	char	   *attrname = NULL;
+	bool		not_unique;
 	int			levels_up;
 	enum
 	{
@@ -504,6 +512,9 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
 		case EXPR_KIND_COPY_WHERE:
 		case EXPR_KIND_GENERATED_COLUMN:
 		case EXPR_KIND_CYCLE_MARK:
+		case EXPR_KIND_VARIABLE_DEFAULT:
+		case EXPR_KIND_LET_TARGET:
+
 			/* okay */
 			break;
 
@@ -761,6 +772,15 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
 			break;
 	}
 
@@ -785,6 +805,78 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
 
+	if (OidIsValid(varid))
+	{
+		Oid			typid;
+		int32		typmod;
+		Oid			collid;
+
@@ -819,6 +911,74 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
 	return node;
 }
 
+/*
@@ -1721,6 +1881,7 @@ transformSubLink(ParseState *pstate, SubLink *sublink)
@@ -1729,6 +1890,7 @@ transformSubLink(ParseState *pstate, SubLink *sublink)
@@ -3059,6 +3221,7 @@ ParseExprKindName(ParseExprKind exprKind)
@@ -3084,6 +3247,8 @@ ParseExprKindName(ParseExprKind exprKind)
 
 			/*
index 3cec8de7da..3ee0083317 100644
@@ -2617,6 +2617,7 @@ check_srf_call_placement(ParseState *pstate, Node *last_srf, int location)
@@ -2655,6 +2656,9 @@ check_srf_call_placement(ParseState *pstate, Node *last_srf, int location)
 
 			/*
 
index 9521e81100..e2c816bc4c 100644
@@ -3664,6 +3665,39 @@ RewriteQuery(Query *parsetree, List *rewrite_events)
 		}
 	}
 
+	/*
index e10f94904e..447c6f0abf 100644
 
 	/*

 		case DestTupleQueue:
 
index b5797042ab..b1380b2383 100644
 
index bf085aa93b..f5f7f9ead7 100644
 
 		case T_CallStmt:
 		case T_DoStmt:
+		case T_LetStmt:
 			{
 				/*
 				 * Commands inside the DO block or the called procedure might
@@ -1061,6 +1064,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,
@@ -1385,6 +1393,10 @@ ProcessUtilitySlow(ParseState *pstate,
 				}
 				break;
 
@@ -2175,6 +2187,10 @@ UtilityContainsQuery(Node *parsetree)
 
+		case T_LetStmt:
+			qry = castNode(Query, ((LetStmt *) parsetree)->query);
+			return qry;
+
 		default:
 			return NULL;
 	}
@@ -2316,6 +2332,9 @@ AlterObjectTypeCommandTag(ObjectType objtype)
 		case OBJECT_STATISTIC_EXT:
 			tag = CMDTAG_ALTER_STATISTICS;
 			break;
+		case OBJECT_VARIABLE:
+			tag = CMDTAG_ALTER_VARIABLE;
+			break;
 		default:
 			tag = CMDTAG_UNKNOWN;
 			break;
@@ -2366,6 +2385,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:
 			{
@@ -2620,6 +2643,9 @@ CreateCommandTag(Node *parsetree)
 				case OBJECT_STATISTIC_EXT:
 					tag = CMDTAG_DROP_STATISTICS;
 					break;
+				case OBJECT_VARIABLE:
+					tag = CMDTAG_DROP_VARIABLE;
+					break;
 				default:
 					tag = CMDTAG_UNKNOWN;
 			}
@@ -2908,6 +2934,9 @@ CreateCommandTag(Node *parsetree)
 				case DISCARD_SEQUENCES:
 					tag = CMDTAG_DISCARD_SEQUENCES;
 					break;
+				case DISCARD_VARIABLES:
+					tag = CMDTAG_DISCARD_VARIABLES;
+					break;
 				default:
 					tag = CMDTAG_UNKNOWN;
 			}
@@ -3192,6 +3221,10 @@ CreateCommandTag(Node *parsetree)
 			}
 			break;
 
@@ -3239,6 +3272,7 @@ GetCommandLogLevel(Node *parsetree)
 			break;
 
 		case T_PLAssignStmt:
+		case T_LetStmt:
 			lev = LOGSTMT_ALL;
 			break;
 
index 67f8b29434..1b3c4bc41d 100644
 
 	ereport(ERROR,
index b932a83827..fdef4605ea 100644
@@ -7881,6 +7882,14 @@ get_parameter(Param *param, deparse_context *context)
 		return;
 	}
 
index 4ebaa552a2..7a1797b1db 100644
 
+/*
+ * get_varname_varid
index d6cb78dea8..51377fbefd 100644
 
diff --git a/src/backend/utils/fmgr/fmgr.c b/src/backend/utils/fmgr/fmgr.c
index 3dfe6e5825..3f60c7b365 100644
--- a/src/backend/utils/fmgr/fmgr.c
+++ b/src/backend/utils/fmgr/fmgr.c
@@ -1910,15 +1910,19 @@ get_call_expr_arg_stable(Node *expr, int argnum)
 	arg = (Node *) list_nth(args, argnum);
 
 	/*
 
 	return false;
 }
index 1f24e79665..dd809e9154 100644
 
index 3c1cd858a8..cd4cdd79bc 100644
 
 	int			useDB;
index ee06dc6822..2eb14ee3af 100644
@@ -2921,6 +2921,14 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
@@ -3402,6 +3410,7 @@ _getObjectDescription(PQExpBuffer buf, TocEntry *te)
@@ -3594,7 +3603,8 @@ _printTocEntry(ArchiveHandle *AH, TocEntry *te, bool isData)
 
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index a485fb2d07..e5057cc3d9 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -279,6 +279,7 @@ static void dumpPolicy(Archive *fout, const PolicyInfo *polinfo);
 static void dumpPublication(Archive *fout, const PublicationInfo *pubinfo);
 static void dumpPublicationTable(Archive *fout, const PublicationRelInfo *pubrinfo);
 static void dumpSubscription(Archive *fout, const SubscriptionInfo *subinfo);
+static void dumpVariable(Archive *fout, const VariableInfo * varinfo);
 static void dumpDatabase(Archive *AH);
 static void dumpDatabaseConfig(Archive *AH, PQExpBuffer outbuf,
 							   const char *dbname, Oid dboid);
@@ -4575,6 +4576,232 @@ get_next_possible_free_pg_type_oid(Archive *fout, PQExpBuffer upgrade_query)
 	return next_possible_free_oid;
 }
 
+/*
+ * getVariables
+					init_racl_subquery, "v.varacl", "v.varowner",
+					"pip.initprivs", "'V'", dopt->binary_upgrade);
@@ -10488,6 +10715,9 @@ dumpDumpableObject(Archive *fout, const DumpableObject *dobj)
@@ -18685,6 +18915,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
index 29af845ece..6efde2886a 100644
 
 /* component types of an object which can be selected for dumping */
@@ -647,6 +648,25 @@ typedef struct _SubscriptionInfo
 	char	   *subpublications;
 } SubscriptionInfo;
 
+/*
@@ -738,5 +758,6 @@ extern PublicationInfo *getPublications(Archive *fout,
 
 #endif							/* PG_DUMP_H */
index 64aaa80eee..3f0af55d7d 100644
@@ -102,6 +102,7 @@ main(int argc, char **argv)
 
@@ -149,7 +150,7 @@ main(int argc, char **argv)
 		}
 	}
 
-	while ((c = getopt_long(argc, argv, "acCd:ef:F:h:I:j:lL:n:N:Op:P:RsS:t:T:U:vwWx1",
+	while ((c = getopt_long(argc, argv, "A:acCd:ef:F:h:I:j:lL:n:N:Op:P:RsS:t:T:U:vwWx1",
 							cmdopts, NULL)) != -1)
 	{
 		switch (c)
@@ -157,6 +158,11 @@ main(int argc, char **argv)
 			case 'a':			/* Dump data only */
 				opts->dataOnly = 1;
 				break;
+			case 'A':			/* vAriable */
+				opts->selTypes = 1;
+				opts->selVariable = 1;
+				simple_string_list_append(&opts->variableNames, optarg);
+				break;
 			case 'c':			/* clean (i.e., drop) schema prior to create */
 				opts->dropSchema = 1;
 				break;
@@ -461,6 +467,7 @@ usage(const char *progname)
 
index c61d95e817..4da71d019b 100644
@@ -2946,6 +2946,30 @@ my %tests = (
 		},
 	},
 
index 49d4c0e3ce..3841b7e3e4 100644
@@ -929,6 +929,9 @@ exec_command_d(PsqlScanState scan_state, bool active_branch, const char *cmd)
index 90ff649be7..9bb04a1926 100644
 
+/*
+ * \dV
+ *
+ * listVariables()
+ */
+bool
 
 /*
  * \dFp
 
+/* \dV */
 
 #endif							/* DESCRIBE_H */
diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c
index db12a8b2f3..a6a3b2eba2 100644
--- a/src/bin/psql/help.c
+++ b/src/bin/psql/help.c
@@ -265,6 +265,7 @@ slashUsage(unsigned short int pager)
 	fprintf(output, _("  \\dT[S+] [PATTERN]      list data types\n"));
 	fprintf(output, _("  \\du[S+] [PATTERN]      list roles\n"));
 	fprintf(output, _("  \\dv[S+] [PATTERN]      list views\n"));
+	fprintf(output, _("  \\dV     [PATTERN]      list variables\n"));
 	fprintf(output, _("  \\dx[+]  [PATTERN]      list extensions\n"));
 	fprintf(output, _("  \\dX     [PATTERN]      list extended statistics\n"));
 	fprintf(output, _("  \\dy[+]  [PATTERN]      list event triggers\n"));
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 5cd5838668..26834e37cc 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -628,6 +628,13 @@ static const SchemaQuery Query_for_list_of_collations = {
 	.result = "pg_catalog.quote_ident(c.collname)",
 };

 /*
@@ -1097,6 +1104,7 @@ static const pgsql_thing_t words_after_create[] = {
@@ -1495,8 +1503,8 @@ psql_completion(const char *text, int start, int end)
-		"FETCH", "GRANT", "IMPORT FOREIGN SCHEMA", "INSERT INTO", "LISTEN", "LOAD", "LOCK",
+		"FETCH", "GRANT", "IMPORT FOREIGN SCHEMA", "INSERT INTO", "LET", "LISTEN", "LOAD",
@@ -1515,7 +1523,7 @@ psql_completion(const char *text, int start, int end)
 		"\\drds", "\\dRs", "\\dRp", "\\ds",
 		"\\echo", "\\edit", "\\ef", "\\elif", "\\else", "\\encoding",
@@ -1914,6 +1922,9 @@ psql_completion(const char *text, int start, int end)
@@ -2754,7 +2765,7 @@ psql_completion(const char *text, int start, int end)
@@ -3044,6 +3055,14 @@ psql_completion(const char *text, int start, int end)
 
 /* CREATE VIEW --- is allowed inside CREATE SCHEMA, so use TailMatches */
 	/* Complete CREATE [ OR REPLACE ] VIEW <name> with AS */
@@ -3158,7 +3177,7 @@ psql_completion(const char *text, int start, int end)
 
 /* DISCARD */
 
 /* DO */
 	else if (Matches("DO"))
@@ -3264,6 +3283,12 @@ psql_completion(const char *text, int start, int end)
 	else if (Matches("DROP", "RULE", MatchAny, "ON", MatchAny))
 		COMPLETE_WITH("CASCADE", "RESTRICT");
 
+	/* DROP VARIABLE */
@@ -3436,6 +3461,7 @@ psql_completion(const char *text, int start, int end)
@@ -3449,14 +3475,16 @@ psql_completion(const char *text, int start, int end)
 
@@ -3490,6 +3518,8 @@ psql_completion(const char *text, int start, int end)
 			COMPLETE_WITH_QUERY(Query_for_list_of_tablespaces);
 		else if (TailMatches("TYPE"))
 			COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes, NULL);
+		else if (TailMatches("VARIABLE"))
+			COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables, NULL);
 		else if (TailMatches("GRANT", MatchAny, MatchAny, MatchAny))
 			COMPLETE_WITH("TO");
 		else
@@ -3653,7 +3683,7 @@ psql_completion(const char *text, int start, int end)
 
 /* PREPARE xx AS */
 	else if (Matches("PREPARE", MatchAny, "AS"))
-		COMPLETE_WITH("SELECT", "UPDATE", "INSERT INTO", "DELETE FROM");
+		COMPLETE_WITH("SELECT", "UPDATE", "INSERT INTO", "DELETE FROM", "LET");
 
 /*
  * PREPARE TRANSACTION is missing on purpose. It's intended for transaction
@@ -3927,6 +3957,14 @@ psql_completion(const char *text, int start, int end)
 	else if (TailMatches("UPDATE", MatchAny, "SET", MatchAnyExcept("*=")))
 		COMPLETE_WITH("=");
 
@@ -4091,6 +4129,8 @@ psql_completion(const char *text, int start, int end)
index 2885f35ccd..95cc88d580 100644
@@ -124,10 +124,11 @@ typedef enum ObjectClass

 /* flag bits for performDeletion/performMultipleDeletions: */
 #define PERFORM_DELETION_INTERNAL			0x0001	/* internal action */
diff --git a/src/include/catalog/namespace.h b/src/include/catalog/namespace.h
index b98f284356..7058b1acb6 100644
--- a/src/include/catalog/namespace.h
+++ b/src/include/catalog/namespace.h
@@ -96,6 +96,8 @@ extern Oid	TypenameGetTypid(const char *typname);
 extern Oid	TypenameGetTypidExtended(const char *typname, bool temp_ok);
 extern bool TypeIsVisible(Oid typid);
 
@@ -164,6 +166,10 @@ extern void SetTempNamespaceState(Oid tempNamespaceId,
 
index eb72dd6293..2ca72a0dcd 100644
@@ -66,6 +66,7 @@ DECLARE_UNIQUE_INDEX_PKEY(pg_default_acl_oid_index, 828, DefaultAclOidIndexId, o

index d068d6532e..28d1f9af17 100644
@@ -6242,6 +6242,9 @@
 
index 0000000000..7e53880a3c
+DECLARE_UNIQUE_INDEX_PKEY(pg_variable_oid_index, 9223, VariableOidIndexId, on pg_variable using btree(oid oid_ops));
+DECLARE_UNIQUE_INDEX(pg_variable_varname_nsp_index, 9224, VariableNameNspIndexId,  on pg_variable using btree(varname name_ops, varnamespace oid_ops));
index 6a24341faa..123a18f298 100644

@@ -736,6 +744,8 @@ extern void ExecEvalParamExec(ExprState *state, ExprEvalStep *op,

index 37cb4f3d59..79946e7683 100644
@@ -543,6 +543,18 @@ typedef struct AsyncRequest
 								 * tuples) */
 } AsyncRequest;
 
+/* ----------------
+ * SchemaVariableValue
+ * ----------------
+ */
+typedef struct SchemaVariableValue
+{
+	Oid			varid;
+	Oid			typid;
+	bool		isnull;
+	Datum		value;
+}			SchemaVariableValue;
+
 /* ----------------
  *	  EState information
  *
@@ -594,6 +606,13 @@ typedef struct EState
 	ParamListInfo es_param_list_info;	/* values of external params */
 	ParamExecData *es_param_exec_vals;	/* values of internal params */
 
+	/* Variables info: */
 
 	/* Other working state: */
diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h
index e0057daa06..90471553b4 100644
--- a/src/include/nodes/nodes.h
+++ b/src/include/nodes/nodes.h
@@ -355,6 +355,7 @@ typedef enum NodeTag
 	T_CreateTableAsStmt,
 	T_CreateSeqStmt,
 	T_AlterSeqStmt,
+	T_CreateSchemaVarStmt,
 	T_VariableSetStmt,
 	T_VariableShowStmt,
 	T_DiscardStmt,
@@ -428,6 +429,7 @@ typedef enum NodeTag
 	T_AlterCollationStmt,
 	T_CallStmt,
 	T_AlterStatsStmt,
+	T_LetStmt,
 
 	/*
index 3138877553..b57c22bc58 100644

 
@@ -1627,6 +1630,20 @@ typedef struct UpdateStmt
 	WithClause *withClause;		/* WITH clause */
 } UpdateStmt;
 
@@ -1835,6 +1852,7 @@ typedef enum ObjectType
 	OBJECT_TSTEMPLATE,
 	OBJECT_TYPE,
 	OBJECT_USER_MAPPING,
+	OBJECT_VARIABLE,
 	OBJECT_VIEW
 } ObjectType;
 
@@ -2670,6 +2688,23 @@ typedef struct AlterSeqStmt
 	bool		missing_ok;		/* skip error if a role is missing? */
 } AlterSeqStmt;
 
@@ -3443,7 +3478,8 @@ typedef enum DiscardMode
 	DISCARD_ALL,
 	DISCARD_PLANS,
 	DISCARD_SEQUENCES,
-	DISCARD_TEMP
+	DISCARD_TEMP,
+	DISCARD_VARIABLES
 } DiscardMode;
 
 typedef struct DiscardStmt
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index 2a53a6e344..528fc118bc 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -130,6 +130,8 @@ typedef struct PlannerGlobal
 	char		maxParallelHazard;	/* worst PROPARALLEL hazard level */

@@ -350,6 +352,7 @@ struct PlannerInfo
 
 	/*
index 01a246d50e..c9eafd6fcf 100644

 

index 433437643e..dc7c2008b0 100644

 
 typedef struct Param

index ee179082ce..aad0953ddb 100644

 

 /* ----------------

 /*

 

 extern void InitCatalogCache(void);
diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
index 7c5bc63778..9d72507398 100644
--- a/src/pl/plpgsql/src/pl_exec.c
+++ b/src/pl/plpgsql/src/pl_exec.c
@@ -24,6 +24,7 @@
 #include "catalog/pg_proc.h"
 #include "catalog/pg_type.h"
 #include "commands/defrem.h"
+#include "commands/schema_variable.h"
 #include "executor/execExpr.h"
 #include "executor/spi.h"
 #include "executor/tstoreReceiver.h"
@@ -318,6 +319,8 @@ static int	exec_stmt_commit(PLpgSQL_execstate *estate,
 							 PLpgSQL_stmt_commit *stmt);
 static int	exec_stmt_rollback(PLpgSQL_execstate *estate,
 							   PLpgSQL_stmt_rollback *stmt);
+static int	exec_stmt_let(PLpgSQL_execstate *estate,
+						  PLpgSQL_stmt_let *let);
 
 static void plpgsql_estate_setup(PLpgSQL_execstate *estate,
 								 PLpgSQL_function *func,
@@ -2087,6 +2090,10 @@ exec_stmts(PLpgSQL_execstate *estate, List *stmts)
 				rc = exec_stmt_rollback(estate, (PLpgSQL_stmt_rollback *) stmt);
 				break;
 
+			case PLPGSQL_STMT_LET:
+				rc = exec_stmt_let(estate, (PLpgSQL_stmt_let *) stmt);
+				break;
+
 			default:
 				/* point err_stmt to parent, since this one seems corrupt */
 				estate->err_stmt = save_estmt;
@@ -4945,6 +4952,54 @@ exec_stmt_rollback(PLpgSQL_execstate *estate, PLpgSQL_stmt_rollback *stmt)
 	return PLPGSQL_RC_OK;
 }
 
+/* ----------
index e0863acb3d..265f93fb7b 100644
@@ -288,6 +288,8 @@ plpgsql_stmt_typename(PLpgSQL_stmt *stmt)
 			return "COMMIT";
 		case PLPGSQL_STMT_ROLLBACK:
 			return "ROLLBACK";
+		case PLPGSQL_STMT_LET:
+			return "LET";
 	}
 
 	return "unknown";
@@ -368,6 +370,7 @@ static void free_perform(PLpgSQL_stmt_perform *stmt);
 static void free_call(PLpgSQL_stmt_call *stmt);
 static void free_commit(PLpgSQL_stmt_commit *stmt);
 static void free_rollback(PLpgSQL_stmt_rollback *stmt);
+static void free_let(PLpgSQL_stmt_let *stmt);
 static void free_expr(PLpgSQL_expr *expr);

@@ -457,6 +460,9 @@ free_stmt(PLpgSQL_stmt *stmt)
 		case PLPGSQL_STMT_ROLLBACK:
 			free_rollback((PLpgSQL_stmt_rollback *) stmt);
 			break;
+		case PLPGSQL_STMT_LET:
+			free_let((PLpgSQL_stmt_let *) stmt);
+			break;
 		default:
 			elog(ERROR, "unrecognized cmd_type: %d", stmt->cmd_type);
 			break;
@@ -711,6 +717,12 @@ free_getdiag(PLpgSQL_stmt_getdiag *stmt)
 {
 }
 
+static void
+free_let(PLpgSQL_stmt_let *stmt)
+{
+	free_expr(stmt->expr);
+}
+
 static void
 free_expr(PLpgSQL_expr *expr)
 {
@@ -813,6 +825,7 @@ static void dump_perform(PLpgSQL_stmt_perform *stmt);
 static void dump_call(PLpgSQL_stmt_call *stmt);
 static void dump_commit(PLpgSQL_stmt_commit *stmt);
 static void dump_rollback(PLpgSQL_stmt_rollback *stmt);
+static void dump_let(PLpgSQL_stmt_let *stmt);
 static void dump_expr(PLpgSQL_expr *expr);

@@ -912,6 +925,9 @@ dump_stmt(PLpgSQL_stmt *stmt)
 		case PLPGSQL_STMT_ROLLBACK:
 			dump_rollback((PLpgSQL_stmt_rollback *) stmt);
 			break;
+		case PLPGSQL_STMT_LET:
+			dump_let((PLpgSQL_stmt_let *) stmt);
+			break;
 		default:
 			elog(ERROR, "unrecognized cmd_type: %d", stmt->cmd_type);
 			break;
@@ -1588,6 +1604,14 @@ dump_getdiag(PLpgSQL_stmt_getdiag *stmt)
 	printf("\n");
 }
 
+static void
+dump_let(PLpgSQL_stmt_let *stmt)
+{
+	dump_ind();
+	dump_expr(stmt->expr);
+	printf("\n");
+}
+
 static void
 dump_expr(PLpgSQL_expr *expr)
 {
diff --git a/src/pl/plpgsql/src/pl_gram.y b/src/pl/plpgsql/src/pl_gram.y
index 0f6a5b30b1..14845e64a2 100644
--- a/src/pl/plpgsql/src/pl_gram.y
+++ b/src/pl/plpgsql/src/pl_gram.y
@@ -197,7 +197,7 @@ static	void			check_raise_parameters(PLpgSQL_stmt_raise *stmt);
 %type <stmt>	stmt_return stmt_raise stmt_assert stmt_execsql
 %type <stmt>	stmt_dynexecute stmt_for stmt_perform stmt_call stmt_getdiag
 %type <stmt>	stmt_open stmt_fetch stmt_move stmt_close stmt_null
-%type <stmt>	stmt_commit stmt_rollback
+%type <stmt>	stmt_commit stmt_rollback stmt_let
 %type <stmt>	stmt_case stmt_foreach_a
 
 %type <list>	proc_exceptions
@@ -304,6 +304,7 @@ static	void			check_raise_parameters(PLpgSQL_stmt_raise *stmt);
 %token <keyword>	K_INTO
 %token <keyword>	K_IS
 %token <keyword>	K_LAST
+%token <keyword>	K_LET
 %token <keyword>	K_LOG
 %token <keyword>	K_LOOP
 %token <keyword>	K_MESSAGE
@@ -897,6 +898,8 @@ proc_stmt		: pl_block ';'
 						{ $$ = $1; }
 				| stmt_rollback
 						{ $$ = $1; }
+				| stmt_let
+						{ $$ = $1; }
 				;
 
 stmt_perform	: K_PERFORM
@@ -1013,6 +1016,29 @@ stmt_assign		: T_DATUM
 					}
 				;
 
+stmt_let		: K_LET
+					{
+						PLpgSQL_stmt_let *new;
+						RawParseMode pmode;
+
index ebd3a5d3c8..84c78bc431 100644
@@ -127,7 +127,8 @@ typedef enum PLpgSQL_stmt_type
 	PLPGSQL_STMT_PERFORM,
 	PLPGSQL_STMT_CALL,
 	PLPGSQL_STMT_COMMIT,
-	PLPGSQL_STMT_ROLLBACK
+	PLPGSQL_STMT_ROLLBACK,
+	PLPGSQL_STMT_LET
 } PLpgSQL_stmt_type;
 
 /*
@@ -519,6 +520,17 @@ typedef struct PLpgSQL_stmt_assign
 	PLpgSQL_expr *expr;
 } PLpgSQL_stmt_assign;
 
+/*
+ * Let statement
+ */
index a57fd142a9..ce9bad7211 100644
@@ -60,7 +60,9 @@ ORDER BY 1, 2;
 
index 982b6aff53..b944d88ff9 100644
@@ -164,6 +164,7 @@ pg_ts_parser|t
index 0000000000..0246d86673
@@ -0,0 +1,655 @@
+CREATE IMMUTABLE VARIABLE xx AS int NOT NULL;
+ERROR:  IMMUTABLE NOT NULL variable requires default expression
+-- REASSIGN OWNED test
+CREATE ROLE var_test_role1;
+CREATE ROLE var_test_role2;
+CREATE VARIABLE xxx_var AS int;
+ALTER VARIABLE xxx_var OWNER TO var_test_role1;
+REASSIGN OWNED BY var_test_role1 to var_test_role2;
+SELECT varowner::regrole FROM pg_variable WHERE varname = 'xxx_var';
+    varowner    
+----------------
+ var_test_role2
+DROP OWNED BY var_test_role1;
+DROP ROLE var_test_role1;
+SELECT count(*) FROM pg_variable WHERE varname = 'xxx_var';
+DROP OWNED BY var_test_role2;
+DROP ROLE var_test_role2;
+SELECT count(*) FROM pg_variable WHERE varname = 'xxx_var';
index 7be89178f0..cbaeedf8b4 100644
@@ -115,7 +115,7 @@ test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath
 
 # ----------
index 0000000000..0bd7798cf9
@@ -0,0 +1,438 @@
+CREATE IMMUTABLE VARIABLE xx AS int NOT NULL;
+
+-- REASSIGN OWNED test
+CREATE ROLE var_test_role1;
+CREATE ROLE var_test_role2;
+
+CREATE VARIABLE xxx_var AS int;
+
+ALTER VARIABLE xxx_var OWNER TO var_test_role1;
+REASSIGN OWNED BY var_test_role1 to var_test_role2;
+
+SELECT varowner::regrole FROM pg_variable WHERE varname = 'xxx_var';
+
+DROP OWNED BY var_test_role1;
+DROP ROLE var_test_role1;
+SELECT count(*) FROM pg_variable WHERE varname = 'xxx_var';
+
+DROP OWNED BY var_test_role2;
+DROP ROLE var_test_role2;
+SELECT count(*) FROM pg_variable WHERE varname = 'xxx_var';

---

From: Pavel Stehule <[email-masked]>
Date: Sat, 30 Oct 2021 08:35:03 +0200

Hi

only rebase

Regards

Pavel

 
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index c1d11be73f..179fa873dc 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -364,6 +364,11 @@
       <entry><link linkend="catalog-pg-user-mapping"><structname>pg_user_mapping</structname></link></entry>
       <entry>mappings of users to foreign servers</entry>
      </row>
+
+     <row>
+      <entry><link linkend="catalog-pg-variable"><structname>pg_variable</structname></link></entry>
+      <entry>schema variables</entry>
+     </row>
     </tbody>
    </tgroup>
   </table>
@@ -13959,4 +13964,143 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
 
  </sect1>
 
index e5c1356d8c..e494161741 100644
@@ -5952,6 +5952,19 @@ $$ LANGUAGE plpgsql STRICT IMMUTABLE;

 
 
  <refsynopsisdiv>
 <synopsis>

+   <varlistentry>

+    <varlistentry>
 
+     <varlistentry>
 
index ca6f6d57d3..97757bd9c7 100644
@@ -2146,6 +2147,9 @@ CommitTransaction(void)

@@ -2285,6 +2289,7 @@ CommitTransaction(void)
 	AtEOXact_SPI(true);
 	AtEOXact_Enum();
 	AtEOXact_on_commit_actions(true);
+	AtEOXact_SchemaVariable_on_commit_actions(true);
 	AtEOXact_Namespace(true, is_parallel_worker);
 	AtEOXact_SMgr();
 	AtEOXact_Files(true);
@@ -2722,6 +2727,9 @@ AbortTransaction(void)
 	AtAbort_Portals();
 	smgrDoPendingSyncs(false, is_parallel_worker);
 	AtEOXact_LargeObject(false);
+
+	/* 'false' means it's abort */
+	AtPreEOXact_SchemaVariable_on_commit_actions(false);
 	AtAbort_Notify();
 	AtEOXact_RelationMap(false, is_parallel_worker);
 	AtAbort_Twophase();
@@ -2786,6 +2794,7 @@ AbortTransaction(void)
 		AtEOXact_SPI(false);
 		AtEOXact_Enum();
 		AtEOXact_on_commit_actions(false);
+		AtEOXact_SchemaVariable_on_commit_actions(false);
 		AtEOXact_Namespace(false, is_parallel_worker);
 		AtEOXact_SMgr();
 		AtEOXact_Files(false);
diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile
index 4e6efda97f..5c402a5ed8 100644
--- a/src/backend/catalog/Makefile
+++ b/src/backend/catalog/Makefile
@@ -43,6 +43,7 @@ OBJS = \
 	pg_shdepend.o \
 	pg_subscription.o \
 	pg_type.o \
+	pg_variable.o \
 	storage.o \
 	toasting.o
 
@@ -69,7 +70,8 @@ CATALOG_HEADERS := \
 	pg_default_acl.h pg_init_privs.h pg_seclabel.h pg_shseclabel.h \
 	pg_collation.h pg_partitioned_table.h pg_range.h pg_transform.h \
 	pg_sequence.h pg_publication.h pg_publication_namespace.h \
-	pg_publication_rel.h pg_subscription.h pg_subscription_rel.h
+	pg_publication_rel.h pg_subscription.h pg_subscription_rel.h \
+	pg_variable.h

index ce0a4ff14e..8d1c02b871 100644

+		case OBJECT_VARIABLE:
 
+static void
 
 static AclMode
@@ -3526,6 +3725,9 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
@@ -3671,6 +3873,8 @@ pg_aclmask(ObjectType objtype, Oid table_oid, AttrNumber attnum, Oid roleid,
@@ -4539,6 +4743,66 @@ pg_type_aclmask(Oid type_oid, Oid roleid, AclMode mask, AclMaskHow how)
 	return result;
 }
 
+/*
@@ -4813,6 +5077,18 @@ pg_type_aclcheck(Oid type_oid, Oid roleid, AclMode mode)
 		return ACLCHECK_NO_PRIV;
 }
 
+/*
@@ -5430,6 +5706,33 @@ pg_statistics_object_ownercheck(Oid stat_oid, Oid roleid)
 
+/*
@@ -5558,6 +5861,10 @@ get_user_default_acl(ObjectType objtype, Oid ownerId, Oid nsp_oid)
 
+		case OBJECT_VARIABLE:
index 9f8eb1a37f..4ec76d4e03 100644
@@ -63,12 +63,15 @@
@@ -183,7 +186,8 @@ static const Oid object_classes[] = {

@@ -1492,6 +1496,10 @@ doDeletion(const ObjectAddress *object, int flags)
 			DropObjectById(object);
 			break;
 
+		case OCLASS_VARIABLE:
+			RemoveSchemaVariable(object->objectId);
+			break;
+
 			/*
 			 * These global object types are not supported here.
 			 */
@@ -1870,6 +1878,11 @@ find_expr_references_walker(Node *node,
 	{
 		Param	   *param = (Param *) node;
 
@@ -2870,6 +2883,9 @@ getObjectClass(const ObjectAddress *object)

 	/* shouldn't get here */
 
+/*
+ * VariableIsVisible
 
 /*
  * TypenameGetTypid
 
+/*
 
 /*
 
index 2bae3fbb17..182ffa142d 100644
@@ -62,6 +62,7 @@
@@ -617,6 +618,20 @@ static const ObjectPropertyType ObjectProperty[] =
 
 /*
@@ -845,6 +860,10 @@ static const struct object_type_map
 	/* OCLASS_STATISTIC_EXT */
 	{
 		"statistics object", OBJECT_STATISTIC_EXT
+	},
+	/* OCLASS_VARIABLE */
+	{
+		"schema variable", OBJECT_VARIABLE
 	}
 };
 
@@ -870,6 +889,7 @@ static ObjectAddress get_object_address_attrdef(ObjectType objtype,
 												bool missing_ok);
@@ -1139,6 +1159,9 @@ get_object_address(ObjectType objtype, Node *object,
@@ -2038,16 +2061,20 @@ get_object_address_defacl(List *object, bool missing_ok)
 
 	/*
@@ -2132,6 +2159,24 @@ textarray_to_strvaluelist(ArrayType *arr)
 	return list;
 }
 
+/*
@@ -2322,6 +2367,7 @@ pg_get_object_address(PG_FUNCTION_ARGS)
@@ -2630,6 +2676,11 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address,
@@ -3558,6 +3609,32 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok)
 				break;
 			}
 
+		case OCLASS_VARIABLE:
+			{
+				char	   *nspname;
+				HeapTuple	tup;
@@ -3868,6 +3945,16 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok)
@@ -4610,6 +4697,10 @@ getObjectTypeDescription(const ObjectAddress *object, bool missing_ok)
 
+		case OCLASS_VARIABLE:
@@ -5703,6 +5794,10 @@ getObjectIdentityParts(const ObjectAddress *object,
 
 				if (objname)
@@ -5918,6 +6013,33 @@ getObjectIdentityParts(const ObjectAddress *object,
 			}
 			break;
 
+		case OCLASS_VARIABLE:
+			{
+				char	   *schema;
+				char	   *varname;
+				HeapTuple	tup;
index 8453d8fefd..bbf7e9413a 100644
@@ -1572,6 +1573,7 @@ shdepReassignOwned(List *roleids, Oid newrole)
index 40044070cf..c559449f1a 100644
 
@@ -884,6 +892,7 @@ ExecAlterOwnerStmt(AlterOwnerStmt *stmt)
 		case OBJECT_TABLESPACE:

index df264329d8..98d0e2ec25 100644
@@ -991,6 +991,7 @@ EventTriggerSupportsObjectType(ObjectType obtype)
 
@@ -1055,6 +1056,7 @@ EventTriggerSupportsObjectClass(ObjectClass objclass)
 		case OCLASS_PUBLICATION_REL:
 		case OCLASS_SUBSCRIPTION:
 		case OCLASS_TRANSFORM:
+		case OCLASS_VARIABLE:
 			return true;
 
 			/*
@@ -2106,6 +2108,8 @@ stringify_grant_objtype(ObjectType objtype)
 			return "TABLESPACE";
 		case OBJECT_TYPE:
 			return "TYPE";
+		case OBJECT_VARIABLE:
+			return "VARIABLE";
@@ -2189,6 +2193,8 @@ stringify_adefprivs_objtype(ObjectType objtype)
index 53c18628a7..c094a3871c 100644
@@ -91,6 +91,7 @@ SecLabelSupportsObjectType(ObjectType objtype)
 
 			/*
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 857cc5ce6e..011534270f 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -12290,6 +12290,7 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel,
 			case OCLASS_PUBLICATION_REL:
 			case OCLASS_SUBSCRIPTION:
 			case OCLASS_TRANSFORM:
+			case OCLASS_VARIABLE:
 
 				/*
 
index 33ef39e2d4..3e8c1ee736 100644
 
 						/*

+	/*

 

 
 
index 0568ae123f..1b37ef33a4 100644
@@ -2867,6 +2867,9 @@ _SPI_error_callback(void *arg)
 
index 82464c9889..44e42c6f69 100644

 	return newnode;
 
+static LetStmt *
@@ -4899,6 +4916,23 @@ _copyDropSubscriptionStmt(const DropSubscriptionStmt *from)
 	return newnode;
 }
 
@@ -5402,6 +5436,9 @@ copyObjectImpl(const void *from)
@@ -5747,6 +5784,9 @@ copyObjectImpl(const void *from)
@@ -5915,7 +5955,7 @@ copyObjectImpl(const void *from)
 			break;
 
 		default:
index f537d3eb96..33bc273880 100644
 
 	return true;
 
+static bool
@@ -2377,6 +2390,22 @@ _equalDropSubscriptionStmt(const DropSubscriptionStmt *a,
 	return true;
 }
 
+static bool
@@ -3408,6 +3437,9 @@ equal(const void *a, const void *b)
@@ -3753,6 +3785,9 @@ equal(const void *a, const void *b)

+static void

 	READ_DONE();
 
index bd01ec0526..b93e7dd5a0 100644
 
 	/*

+	/*

+/*

 
 
 				/*

 
 
+		case T_LetStmt:
 
 			/*

 

 

 

 

 
 	return qry;
 }
 
+/*
+ * transformLetStmt -

 

diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index d0eb80e69c..fb5391d173 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -231,6 +231,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	JoinType			jtype;
 	DropBehavior		dbehavior;
 	OnCommitAction		oncommit;
+	VariableEOXAction	oneoxaction;
 	List				*list;
 	Node				*node;
 	ObjectType			objtype;
@@ -281,8 +282,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 		ConstraintsSetStmt CopyStmt CreateAsStmt CreateCastStmt
 		CreateDomainStmt CreateExtensionStmt CreateGroupStmt CreateOpClassStmt
 		CreateOpFamilyStmt AlterOpFamilyStmt CreatePLangStmt
-		CreateSchemaStmt CreateSeqStmt CreateStmt CreateStatsStmt CreateTableSpaceStmt
-		CreateFdwStmt CreateForeignServerStmt CreateForeignTableStmt
+		CreateSchemaStmt CreateSchemaVarStmt CreateSeqStmt CreateStmt CreateStatsStmt
+		CreateTableSpaceStmt CreateFdwStmt CreateForeignServerStmt CreateForeignTableStmt
 		CreateAssertionStmt CreateTransformStmt CreateTrigStmt CreateEventTrigStmt
 		CreateUserStmt CreateUserMappingStmt CreateRoleStmt CreatePolicyStmt
 		CreatedbStmt DeclareCursorStmt DefineStmt DeleteStmt DiscardStmt DoStmt
@@ -292,7 +293,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 		DropTransformStmt
 		DropUserMappingStmt ExplainStmt FetchStmt
 		GrantStmt GrantRoleStmt ImportForeignSchemaStmt IndexStmt InsertStmt
-		ListenStmt LoadStmt LockStmt NotifyStmt ExplainableStmt PreparableStmt
+		LetStmt ListenStmt LoadStmt LockStmt NotifyStmt ExplainableStmt PreparableStmt
 		CreateFunctionStmt AlterFunctionStmt ReindexStmt RemoveAggrStmt
 		RemoveFuncStmt RemoveOperStmt RenameStmt ReturnStmt RevokeStmt RevokeRoleStmt
 		RuleActionStmt RuleActionStmtOrEmpty RuleStmt
@@ -432,6 +433,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
 
@@ -454,6 +456,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 
@@ -615,6 +618,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);

 /*
@@ -684,7 +689,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	KEY

 	MAPPING MATCH MATERIALIZED MAXVALUE METHOD MINUTE_P MINVALUE MODE MONTH_P MOVE
@@ -723,8 +728,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	UESCAPE UNBOUNDED UNCOMMITTED UNENCRYPTED UNION UNIQUE UNKNOWN
 	UNLISTEN UNLOGGED UNTIL UPDATE USER USING

 
@@ -759,6 +764,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %token		MODE_PLPGSQL_ASSIGN1
 %token		MODE_PLPGSQL_ASSIGN2
 %token		MODE_PLPGSQL_ASSIGN3
+%token		MODE_PLPGSQL_LET

@@ -864,6 +870,13 @@ parse_toplevel:
 
 /*
@@ -967,6 +980,7 @@ stmt:
 			| CreatePolicyStmt
 			| CreatePLangStmt
 			| CreateSchemaStmt
+			| CreateSchemaVarStmt
 			| CreateSeqStmt
 			| CreateStmt
 			| CreateSubscriptionStmt
@@ -1004,6 +1018,7 @@ stmt:
 			| ImportForeignSchemaStmt
 			| IndexStmt
 			| InsertStmt
+			| LetStmt
 			| ListenStmt
 			| RefreshMatViewStmt
 			| LoadStmt
@@ -1896,7 +1911,12 @@ DiscardStmt:
 					n->target = DISCARD_SEQUENCES;
 					$$ = (Node *) n;
 				}
-
+			| DISCARD VARIABLES
+				{
+					DiscardStmt *n = makeNode(DiscardStmt);
+					n->target = DISCARD_VARIABLES;
+					$$ = (Node *) n;
+				}
 		;

@@ -4655,6 +4675,61 @@ create_extension_opt_item:
 				}
 		;
 
@@ -6342,6 +6417,7 @@ object_type_any_name:
 
 /*
@@ -7110,6 +7186,14 @@ privilege_target:
 					n->objs = $2;
 					$$ = n;
 				}
@@ -7150,6 +7234,14 @@ privilege_target:

@@ -7310,6 +7402,7 @@ defacl_privilege_target:

@@ -9012,6 +9105,25 @@ RenameStmt: ALTER AGGREGATE aggregate_with_argtypes RENAME TO name
 					n->missing_ok = false;
 					$$ = (Node *)n;
 				}
 
 opt_column: COLUMN
@@ -9340,6 +9452,25 @@ AlterObjectSchemaStmt:
 					n->missing_ok = false;
 					$$ = (Node *)n;
 				}
 
@@ -9593,6 +9724,14 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec

@@ -10928,6 +11067,7 @@ ExplainableStmt:
 			| CreateMatViewStmt
 			| RefreshMatViewStmt
 			| ExecuteStmt					/* by default all are $$=$1 */
+			| LetStmt
 		;
 
 /*****************************************************************************
@@ -10956,6 +11096,7 @@ PreparableStmt:
 			| InsertStmt
 			| UpdateStmt
 			| DeleteStmt					/* by default all are $$=$1 */
+			| LetStmt
 		;
 
 /*****************************************************************************
@@ -11373,6 +11514,47 @@ opt_hold: /* EMPTY */						{ $$ = 0; }
 			| WITHOUT HOLD					{ $$ = 0; }
 		;
 
@@ -15681,6 +15863,7 @@ unreserved_keyword:
@@ -15838,6 +16021,8 @@ unreserved_keyword:
@@ -16245,6 +16430,7 @@ bare_label_keyword:
@@ -16444,6 +16630,8 @@ bare_label_keyword:
 
 			/*
 
 			if (isAgg)
 
 			/*

 /* GUC parameters */
 
 /*
  * transformExpr -

 
+	if (OidIsValid(varid))
+	{
+		Oid			typid;
+		int32		typmod;
+		Oid			collid;
+
 
+/*
 
 			/*
 
 			/*
index 9ce3a0de96..a79e612129 100644

+	/*
 
 	/*

 		case DestTupleQueue:
 
index 960f3fadce..6c2f393988 100644

 		case T_CallStmt:
 		case T_DoStmt:
+		case T_LetStmt:
 			{
 				/*
 
+		case T_LetStmt:

+		case T_LetStmt:
 
+		case T_LetStmt:
+			tag = CMDTAG_LET;
+			break;
+

 		case T_PLAssignStmt:
+		case T_LetStmt:
 			lev = LOGSTMT_ALL;
 			break;

 	ereport(ERROR,
index 1bb25738a5..bfd860effa 100644
@@ -7915,6 +7916,14 @@ get_parameter(Param *param, deparse_context *context)
 		return;
 	}

+/*
+ * get_varname_varid
index 56870b46e4..b385e2e677 100644
@@ -74,6 +74,7 @@
@@ -1014,6 +1015,28 @@ static const struct cachedesc cacheinfo[] = {

 	/*
 
 	return false;
 }
diff --git a/src/bin/pg_dump/common.c b/src/bin/pg_dump/common.c
index ecab0a9e4e..8c206b0200 100644
--- a/src/bin/pg_dump/common.c
+++ b/src/bin/pg_dump/common.c
@@ -263,7 +263,8 @@ getSchemaData(Archive *fout, int *numTablesPtr)
 	pg_log_info("reading subscriptions");
 	getSubscriptions(fout);
 
-	free(inhinfo);				/* not needed any longer */
+	pg_log_info("reading variables");
+	getVariables(fout);
 
index 6af10a85a2..29082a0926 100644
 
 	int			useDB;
index 2c4cfb9457..4141a21531 100644
@@ -2922,6 +2922,14 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
@@ -3405,6 +3413,7 @@ _getObjectDescription(PQExpBuffer buf, TocEntry *te)
@@ -3597,7 +3606,8 @@ _printTocEntry(ArchiveHandle *AH, TocEntry *te, bool isData)
 
index d1842edde0..91bb0205f8 100644
@@ -4701,6 +4702,232 @@ get_next_possible_free_pg_type_oid(Archive *fout, PQExpBuffer upgrade_query)
 
+/*
+ * getVariables
@@ -10338,6 +10565,9 @@ dumpDumpableObject(Archive *fout, const DumpableObject *dobj)
@@ -18527,6 +18757,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
index f9af14b793..17a1f90da8 100644
@@ -659,6 +660,26 @@ typedef struct _SubscriptionInfo
 	char	   *subpublications;
 } SubscriptionInfo;
 
+/*
+ * The VariableInfo struct is used to represent schema variables
+ */
+typedef struct _VariableInfo
+{
+	DumpableObject dobj;
+	Oid			vartype;
+	char	   *vartypname;
+	char	   *rolname;		/* name of owner, or empty string */
+	char	   *vareoxaction;
+	char	   *vardefexpr;
+	char	   *varacl;
+	char	   *rvaracl;
+	char	   *initvaracl;
+	char	   *initrvaracl;
+	bool		varisnotnull;
+	bool		varisimmutable;
+}			VariableInfo;
+
+
 /*
  *	common utility functions
  */
@@ -741,5 +762,6 @@ extern void getPublicationNamespaces(Archive *fout);
 extern void getPublicationTables(Archive *fout, TableInfo tblinfo[],
 								 int numTables);
 extern void getSubscriptions(Archive *fout);
+extern void getVariables(Archive *fout);
 
 #endif							/* PG_DUMP_H */
index 9901d9e0ba..b49d5c6c62 100644
@@ -107,6 +108,7 @@ static const int dbObjectTypePriority[] =
@@ -1499,6 +1501,10 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)

 
index d293f52b05..1595dfc513 100644
@@ -2995,6 +2995,30 @@ my %tests = (
 		},
 	},
 
index 006661412e..625b63ddcd 100644
@@ -5154,6 +5154,88 @@ listSchemas(const char *pattern, bool verbose, bool showSystem)
 	return true;
 }
 
+/*
+ * \dV
+ *
+ * listVariables()
+ */
+bool
 
 /*
  * \dFp
 
+/* \dV */
 
 #endif							/* DESCRIBE_H */
index 8e01f54500..7045be20bf 100644

 /*
@@ -1926,6 +1934,9 @@ psql_completion(const char *text, int start, int end)
@@ -2780,7 +2791,7 @@ psql_completion(const char *text, int start, int end)
@@ -3070,6 +3081,14 @@ psql_completion(const char *text, int start, int end)
 
 /* CREATE VIEW --- is allowed inside CREATE SCHEMA, so use TailMatches */
 	/* Complete CREATE [ OR REPLACE ] VIEW <name> with AS */
@@ -3184,7 +3203,7 @@ psql_completion(const char *text, int start, int end)
 
 /* DISCARD */
 
 /* DO */
 	else if (Matches("DO"))
@@ -3290,6 +3309,12 @@ psql_completion(const char *text, int start, int end)
 	else if (Matches("DROP", "RULE", MatchAny, "ON", MatchAny))
 		COMPLETE_WITH("CASCADE", "RESTRICT");
 
+	/* DROP VARIABLE */
@@ -3462,6 +3487,7 @@ psql_completion(const char *text, int start, int end)
@@ -3475,14 +3501,16 @@ psql_completion(const char *text, int start, int end)
 
@@ -3516,6 +3544,8 @@ psql_completion(const char *text, int start, int end)
 			COMPLETE_WITH_QUERY(Query_for_list_of_tablespaces);
 		else if (TailMatches("TYPE"))
 			COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes, NULL);
+		else if (TailMatches("VARIABLE"))
+			COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables, NULL);
 		else if (TailMatches("GRANT", MatchAny, MatchAny, MatchAny))
 			COMPLETE_WITH("TO");
 		else
@@ -3688,7 +3718,7 @@ psql_completion(const char *text, int start, int end)
 
 /* PREPARE xx AS */
 
 /*
  * PREPARE TRANSACTION is missing on purpose. It's intended for transaction
@@ -3962,6 +3992,14 @@ psql_completion(const char *text, int start, int end)
 	else if (TailMatches("UPDATE", MatchAny, "SET", MatchAnyExcept("*=")))
 		COMPLETE_WITH("=");
 
@@ -4126,6 +4164,8 @@ psql_completion(const char *text, int start, int end)
index 3eca295ff4..347008a606 100644
@@ -125,10 +125,11 @@ typedef enum ObjectClass
 	OCLASS_PUBLICATION_NAMESPACE,	/* pg_publication_namespace */

 

 

 

index 2e8cbee69f..e0776ac489 100644
@@ -544,6 +544,18 @@ typedef struct AsyncRequest
 								 * tuples) */
 } AsyncRequest;
 
+/* ----------------
+ * SchemaVariableValue
+ * ----------------
+ */
+typedef struct SchemaVariableValue
+{
+	Oid			varid;
+	Oid			typid;
+	bool		isnull;
+	Datum		value;
+}			SchemaVariableValue;
+
 /* ----------------
  *	  EState information
  *
@@ -595,6 +607,13 @@ typedef struct EState
 	ParamListInfo es_param_list_info;	/* values of external params */
 	ParamExecData *es_param_exec_vals;	/* values of internal params */
 
+	/* Variables info: */
 
index 7c657c1241..14bbe45624 100644
 
 	/*
index 49123e28a4..795bbc1170 100644

 
 
@@ -1836,6 +1853,7 @@ typedef enum ObjectType
 	OBJECT_TSTEMPLATE,
 	OBJECT_TYPE,
 	OBJECT_USER_MAPPING,
+	OBJECT_VARIABLE,
 	OBJECT_VIEW
 } ObjectType;
 
@@ -2671,6 +2689,23 @@ typedef struct AlterSeqStmt
 	bool		missing_ok;		/* skip error if a role is missing? */
 } AlterSeqStmt;
 
@@ -3444,7 +3479,8 @@ typedef enum DiscardMode
 	DISCARD_ALL,
 	DISCARD_PLANS,
 	DISCARD_SEQUENCES,
-	DISCARD_TEMP
+	DISCARD_TEMP,
+	DISCARD_VARIABLES
 } DiscardMode;

 
 
 	/*

 

 

 typedef struct Param

 

 
 /* ----------------

 /*

 
index c8cfbc30f6..d6f8611482 100644
@@ -111,9 +111,11 @@ enum SysCacheIdentifier
 	TYPENAMENSP,
 	TYPEOID,
 	USERMAPPINGOID,
-	USERMAPPINGUSERSERVER
+	USERMAPPINGUSERSERVER,
+	VARIABLENAMENSP,
+	VARIABLEOID

index 0e1cfa3df6..3aa46dc431 100644

+			case PLPGSQL_STMT_LET:
+				rc = exec_stmt_let(estate, (PLpgSQL_stmt_let *) stmt);
+				break;
+
 			default:
 				/* point err_stmt to parent, since this one seems corrupt */
 				estate->err_stmt = save_estmt;
@@ -4933,6 +4940,54 @@ exec_stmt_rollback(PLpgSQL_execstate *estate, PLpgSQL_stmt_rollback *stmt)
 	return PLPGSQL_RC_OK;
 }
 
+/* ----------
 
 	return "unknown";

+static void

+static void

 stmt_perform	: K_PERFORM
 
+stmt_let		: K_LET
+					{
+						PLpgSQL_stmt_let *new;
+						RawParseMode pmode;
+
 
 /*
 
+/*
+ * Let statement
+ */
 
index d04dc66db9..54b2ac8383 100644
@@ -165,6 +165,7 @@ pg_ts_parser|t
 
 # ----------

---

From: Tomas Vondra <[email-masked]>
Date: Wed, 3 Nov 2021 14:05:02 +0100

Hi,

I took a quick look at the latest patch version. In general the patch
looks pretty complete and clean, and for now I have only some basic
comments. The attached patch tweaks some of this, along with a couple
additional minor changes that I'll not discuss here.

1) Not sure why we need to call this "schema variables". Most objects
are placed in a schema, and we don't say "schema tables" for example.
And it's CREATE VARIABLE and not CREATE SCHEMA VARIABLE, so it's a bit
inconsistent.

The docs actually use "Global variables" in one place for some reason.

2) I find this a bit confusing:

SELECT non_existent_variable;
test=# select s;
ERROR:  column "non_existent_variable" does not exist
LINE 1: select non_existent_variable;

I wonder if this means using SELECT to read variables is a bad idea, and
we should have a separate command, just like we have LET (instead of
just using UPDATE in some way).

3) I've reworded / tweaked a couple places in the docs, but this really
needs a native speaker - I don't have a very good "feeling" for this
technical language so it's probably still quite cumbersome.

4) Is sequential scan of the hash table  in clean_cache_callback() a
good idea? I wonder how fast (with how many variables) it'll become
noticeable, but it may be good enough for now and we can add something
better (tracing which variables need resetting) later.

5) In what situation would we call clean_cache_callback() without a
transaction state? If that happens it seems more like a bug, so
maybeelog(ERROR) or Assert() would be more appropriate?

6) free_schema_variable does not actually use the force parameter

7) The target_exprkind expression in transformSelectStmt really needs
some explanation. Because that's chance you'll look at this in 6 months
and understand what it does?

    target_exprkind =
        (pstate->p_expr_kind != EXPR_KIND_LET_TARGET ||
         pstate->parentParseState != NULL) ?
                    EXPR_KIND_SELECT_TARGET : EXPR_KIND_LET_TARGET;

8) immutable variables without a default value

IMO this case should not be allowed. On 2021/08/29 you wrote:

    I thought about this case, and I have one scenario, where this
    behaviour can be useful. When the variable is declared as IMMUTABLE
    NOT NULL without not null default, then any access to the content of
    the variable has to fail. I think it can be used for detection,
    where and when the variable is first used. So this behavior is
    allowed just because I think, so this feature can be interesting for
    debugging. If this idea is too strange, I have no problem to disable
    this case.

This seems like a really strange use case. In a production code you'll
not do this, because then the variable is useless and the code does not
work at all (it'll just fail whenever it attempts to access the var).
And if you can modify the code, there are other / better ways to do this
(raising an exception, ...).

So this seems pretty useless to me, +1 to disabling it.

regards

-- 
Tomas Vondra
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
diff --git a/doc/src/sgml/advanced.sgml b/doc/src/sgml/advanced.sgml
index 2d6555497c..bd4733c0bb 100644
--- a/doc/src/sgml/advanced.sgml
+++ b/doc/src/sgml/advanced.sgml
@@ -714,24 +714,16 @@ SELECT name, elevation
 
    <para>
     Schema variables are database objects that can hold a value.
-    Schema variables, like relations, exist within a schema and their access is
+    Schema variables, like relations, exist within a schema and the access is
     controlled via <command>GRANT</command> and <command>REVOKE</command> commands.
-    The schema variable can be created by the <command>CREATE VARIABLE</command> command.
+    A schema variable can be created by the <command>CREATE VARIABLE</command> command.
    </para>
 
    <para>
-    The value of a schema variable is local to the current session. Retrieving
-    a variable's value returns either a NULL or a default value, unless its value
-    is set to something else in the current session with a LET command. The content
-    of a variable is not transactional. This is the same as in regular variables
-    in PL languages.
-   </para>
-
-   <para>
-    Schema variables are retrieved by the <command>SELECT</command> SQL command.
-    Their value is set with the <command>LET</command> SQL command.
+    The value of a schema variable is set with the <command>LET</command> SQL command.
     While schema variables share properties with tables, their value cannot be updated
-    with an <command>UPDATE</command> command.
+    with an <command>UPDATE</command> command. The value of a schema variable may be
+    retrieved by the <command>SELECT</command> SQL command.
 <programlisting>
 CREATE VARIABLE var1 AS date;
 LET var1 = current_date;
@@ -747,6 +739,15 @@ LET current_user_id = (SELECT id FROM users WHERE usename = session_user);
 SELECT current_user_id;
 </programlisting>
    </para>
+
+   <para>
+    The value of a schema 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 is 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.
+   </para>
+
   </sect1>

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 179fa873dc..400724456f 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -13972,8 +13972,8 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
   </indexterm>
 
   <para>
-   The table <structname>pg_variable</structname> holds metadata
-   of schema variables.
+   The table <structname>pg_variable</structname> provides information
+   about schema variables.
   </para>
 
   <table>
@@ -14029,8 +14029,7 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
        <structfield>vartypmod</structfield> records type-specific data
        supplied at variable creation time (for example, the maximum
        length of a <type>varchar</type> column).  It is passed to
-       type-specific input 
-       functions and length coercion functions.
+       type-specific input functions and length coercion functions.
        The value will generally be -1 for types that do not need <structfield>vartypmod</structfield>.
       </entry>
      </row>
@@ -14075,6 +14074,7 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
       <entry><type>char</type></entry>
       <entry></entry>
       <entry>
+       Action performed at end of transaction:
        <literal>n</literal> = no action, <literal>d</literal> = drop the variable,
        <literal>r</literal> = reset the variable to its default value.
       </entry>
diff --git a/doc/src/sgml/plpgsql.sgml b/doc/src/sgml/plpgsql.sgml
index e494161741..fb2ef96d0d 100644
--- a/doc/src/sgml/plpgsql.sgml
+++ b/doc/src/sgml/plpgsql.sgml
@@ -5954,7 +5954,7 @@ $$ LANGUAGE plpgsql STRICT IMMUTABLE;
    </sect3>
 
    <sect3>
-    <title><command>Global variables and constants</command></title>
+    <title><command>Schema variables and constants</command></title>
 
     <para>
      The <application>PL/pgSQL</application> language has no packages,
diff --git a/doc/src/sgml/ref/discard.sgml b/doc/src/sgml/ref/discard.sgml
index 698b2c07fd..7d6ad2dd49 100644
--- a/doc/src/sgml/ref/discard.sgml
+++ b/doc/src/sgml/ref/discard.sgml
@@ -81,7 +81,7 @@ DISCARD { ALL | PLANS | SEQUENCES | TEMPORARY | TEMP | VARIABLES }
      <para>
       Resets the value of all schema variables. If a variable
       is later reused, it is re-initialized to either
-      NULL or its default value.
+      <literal>NULL</literal> or its default value.
      </para>
     </listitem>
    </varlistentry>
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 8d1c02b871..715208af23 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -4763,7 +4763,7 @@ pg_variable_aclmask(Oid var_oid, Oid roleid, AclMode mask, AclMaskHow how)
 		return mask;
 
 	/*
-	 * Must get the type's tuple from pg_type
+	 * Must get the variables's tuple from pg_variable
 	 */
 	tuple = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(var_oid));
 	if (!HeapTupleIsValid(tuple))
@@ -4774,7 +4774,7 @@ pg_variable_aclmask(Oid var_oid, Oid roleid, AclMode mask, AclMaskHow how)
 	varForm = (Form_pg_variable) GETSTRUCT(tuple);
 
 	/*
-	 * Now get the type's owner and ACL from the tuple
+	 * Now get the variable's owner and ACL from the tuple
 	 */
 	ownerId = varForm->varowner;
 
diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c
index 4ec76d4e03..f504701515 100644
--- a/src/backend/catalog/dependency.c
+++ b/src/backend/catalog/dependency.c
@@ -1878,8 +1878,8 @@ find_expr_references_walker(Node *node,
 	{
 		Param	   *param = (Param *) node;
 
+		/* A variable parameter depends on the schema variable */
 		if (param->paramkind == PARAM_VARIABLE)
-			/* A variable parameter depends on the schema variable */
 			add_object_address(OCLASS_VARIABLE, param->paramvarid, 0,
 							   context->addrs);
 
diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c
index 02c5de7f73..789f41876c 100644
--- a/src/backend/catalog/namespace.c
+++ b/src/backend/catalog/namespace.c
@@ -828,7 +828,6 @@ VariableIsVisible(Oid varid)
 	return visible;
 }
 
-
 /*
  * TypenameGetTypid
  *		Wrapper for binary compatibility.
@@ -2909,6 +2908,8 @@ TSConfigIsVisible(Oid cfgid)
 
 /*
  * When we know a variable name, then we can find variable simply
+ *
+ * FIXME seems incomplete
  */
 Oid
 LookupVariable(const char *nspname, const char *varname, bool missing_ok)
@@ -2995,6 +2996,9 @@ NamesFromList(List *names)
  * Returns oid of not ambigonuous variable specified by qualified path
  * or InvalidOid. When the path is ambigonuous, then not_uniq flag is
  * is true.
+ *
+ * XXX Do we need the not_uniq flag, when we return InvalidOid anyway?
+ * XXX Maybe use not_unique, it's not much longer.
  */
 Oid
 IdentifyVariable(List *names, char **attrname, bool *not_uniq)
diff --git a/src/backend/commands/schemavariable.c b/src/backend/commands/schemavariable.c
index 15e0567199..5426e5fa21 100644
--- a/src/backend/commands/schemavariable.c
+++ b/src/backend/commands/schemavariable.c
@@ -64,6 +64,10 @@ static List *on_commits = NIL;
  * implementation of DROP can be simple, because although DROP VARIABLE
  * can be reverted, the content of variable can be lost. In this example,
  * DROP VARIABLE is same like reset variable.
+ *
+ * XXX Seems misplaced? There is no example here.
+ * XXX Also, is it really true that discarding the value even if DROP
+ * VARIABLE gets reverted makes sense?
  */
 
 typedef struct SchemaVariableData
@@ -108,6 +112,9 @@ static void clean_cache_variable(Oid varid);
  * Save info about necessity to clean hash table, because some
  * schema variable was dropped. Don't do here more, recheck
  * needs to be in transaction state.
+ *
+ * XXX Not sure "needs to be in transaction state" means? Perhaps
+ * we can do the cleanup only at transaction end?
  */
 static void
 InvalidateSchemaVariableCacheCallback(Datum arg, int cacheid, uint32 hashvalue)
@@ -126,41 +133,53 @@ InvalidateSchemaVariableCacheCallback(Datum arg, int cacheid, uint32 hashvalue)
 static void
 clean_cache_callback(XactEvent event, void *arg)
 {
+	HASH_SEQ_STATUS status;
+	SchemaVariable svar;
+
+	/* If no cleanup needed, we're done. */
+	if (!clean_cache_req)
+		return;
+
+	/* XXX How can this happen after having clean_cache_req=true? */
+	if (!schemavarhashtab)
+		return;
+
 	/*
-	 * should continue only in transaction time, when syscache is available.
+	 * XXX Can we even get here when not in a transaction? Isn't that really
+	 * an error? Maybe should be assert or elog(ERROR).
 	 */
-	if (clean_cache_req && IsTransactionState())
-	{
-		HASH_SEQ_STATUS status;
-		SchemaVariable svar;
+	if (!IsTransactionState())
+		return;
 
-		if (!schemavarhashtab)
-			return;
 
-		hash_seq_init(&status, schemavarhashtab);
+	/*
+	 * Walk the current contents of the hash table, and check if the
+	 * schema variable still exists in the catalog. If not, remove it.
+	 *
+	 * XXX Try using a variable still used by another session.
+	 */
+	while ((svar = (SchemaVariable) hash_seq_search(&status)) != NULL)
+	{
+		HeapTuple	tp;
 
-		while ((svar = (SchemaVariable) hash_seq_search(&status)) != NULL)
+		tp = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(svar->varid));
+		if (!HeapTupleIsValid(tp))
 		{
-			HeapTuple	tp;
+			elog(DEBUG1, "variable %d is removed from cache", svar->varid);
 
-			tp = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(svar->varid));
-			if (!HeapTupleIsValid(tp))
-			{
-				elog(DEBUG1, "variable %d is removed from cache", svar->varid);
+			free_schema_variable(svar, true);
 
-				free_schema_variable(svar, true);
+			remove_variable_on_commit_actions(svar->varid, VARIABLE_EOX_DROP);
 
-				remove_variable_on_commit_actions(svar->varid, VARIABLE_EOX_DROP);
-
-				(void) hash_search(schemavarhashtab, (void *) &svar->varid,
-								   HASH_REMOVE, NULL);
-			}
-			else
-				ReleaseSysCache(tp);
+			(void) hash_search(schemavarhashtab, (void *) &svar->varid,
+							   HASH_REMOVE, NULL);
 		}
-
-		clean_cache_req = false;
+		else
+			ReleaseSysCache(tp);
 	}
+
+	clean_cache_req = false;
 }
 
 /*
@@ -202,12 +221,19 @@ create_schema_variable_hashtable(void)
 }
 
 /*
- * Fast drop complete content of schema variables
+ * Fast drop complete content of schema variables hash table.
  */
 void
 ResetSchemaVariableCache(void)
 {
-	/* Remove temporal schema variables */
+	/*
+	 * Remove temporary schema variables.
+	 *
+	 * XXX This does not *remove* the variables, does it? If the variable
+	 * used ON COMMIT RESET then it'll still be there.
+	 *
+	 * XXX Is it correct to call this with "isCommit=true"?
+	 */
 	AtPreEOXact_SchemaVariable_on_commit_actions(true);
 
 	/* Destroy hash table and reset related memory context */
@@ -228,6 +254,9 @@ ResetSchemaVariableCache(void)
  * current value for possible future usage on rollback.
  *
  * When force is true, then release current and possibly archived value.
+ *
+ * XXX The "force" flag is not really used.
+ * XXX Can't the value be something more complex?
  */
 static void
 free_schema_variable(SchemaVariable svar, bool force)
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index 44e42c6f69..5922e6fc34 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -5955,7 +5955,7 @@ copyObjectImpl(const void *from)
 			break;
 
 		default:
-			elog(ERROR, "urecognized node type: %d", (int) nodeTag(from));
+			elog(ERROR, "unrecognized node type: %d", (int) nodeTag(from));
 			retval = 0;			/* keep compiler quiet */
 			break;
 	}
diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c
index b18528ffbc..30d9662148 100644
--- a/src/backend/optimizer/util/clauses.c
+++ b/src/backend/optimizer/util/clauses.c
@@ -4761,7 +4761,9 @@ substitute_actual_parameters_mutator(Node *node,
 {
 	if (node == NULL)
 		return NULL;
-	if (IsA(node, Param) &&((Param *) node)->paramkind != PARAM_VARIABLE)
+
+	/* XXX Explain why this does not work with PARAM_VARIABLE? */
+	if (IsA(node, Param) && ((Param *) node)->paramkind != PARAM_VARIABLE)
 	{
 		Param	   *param = (Param *) node;
 
diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
index 8971cf7e80..34cbaa9100 100644
--- a/src/backend/parser/analyze.c
+++ b/src/backend/parser/analyze.c
@@ -1290,6 +1290,10 @@ transformSelectStmt(ParseState *pstate, SelectStmt *stmt)
 	 * Transform targetlist. Second usage of this transformation
 	 * is for Let statement. In this case we would to allow DEFAULT
 	 * keyword - we specify EXPR_KIND_LET_TARGET.
+	 *
+	 * XXX This is really hard to understand. What if p_expr_kind is
+	 * EXPR_KIND_LET_TARGET, why should it matter if parentParseState
+	 * is NULL?
 	 */
 	target_exprkind =
 		(pstate->p_expr_kind != EXPR_KIND_LET_TARGET ||
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index fb5391d173..b56ff4f69d 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -4717,6 +4717,7 @@ OptSchemaVarDefExpr: DEFAULT b_expr					{ $$ = $2; }
 			| /* EMPTY */							{ $$ = NULL; }
 		;
 
+/* XXX This seems a bit strange. */
 OnEOXActionOption:  ON COMMIT DROP					{ $$ = VARIABLE_EOX_DROP; }
 			| ON TRANSACTION END_P RESET			{ $$ = VARIABLE_EOX_RESET; }
 			| /*EMPTY*/								{ $$ = VARIABLE_EOX_NOOP; }
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index 6aaaa5bb29..31b5b55a09 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -281,7 +281,7 @@ transformExprRecurse(ParseState *pstate, Node *expr)
 			 * processed it rather than passing it to transformExpr().
 			 */
 		case T_SetToDefault:
-			Assert(false);
+			Assert(false);	/* FIXME left here by accident? */
 			ereport(ERROR,
 					(errcode(ERRCODE_SYNTAX_ERROR),
 					 errmsg("DEFAULT is not allowed in this context"),
@@ -772,6 +772,8 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
 			break;
 	}
 
+	/* XXX Why is this before the p_post_columnref_hook block? Why not as part of
+	 * the varid block right after it? */
 	varid = IdentifyVariable(cref->fields, &attrname, &not_unique);
 
 	if (not_unique)
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index a8b727898b..5f1b386b55 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -4735,7 +4735,7 @@ getVariables(Archive *fout)
 	int			i,
 				ntups;
 
-	if (fout->remoteVersion < 130000)
+	if (fout->remoteVersion < 150000)
 		return;
 
 	acl_subquery = createPQExpBuffer();

---

From: Tomas Vondra <[email-masked]>
Date: Wed, 3 Nov 2021 16:20:30 +0100

FWIW the patch was marked as RFC for about a year, but there was plenty
of discussion / changes since then, so that seemed premature. I've
switched it back to WoA.

regards

---

From: Justin Pryzby <[email-masked]>
Date: Fri, 5 Nov 2021 20:39:04 -0500

> technical language so it's probably still quite cumbersome.

On Daniel's suggestion, I have reviewed the docs, and then proofread the rest
of the patch.  My amendments are in 0003.

-- 
Justin

---

From: Pavel Stehule <[email-masked]>
Date: Sat, 6 Nov 2021 04:45:19 +0100

Hi

> And it's CREATE VARIABLE and not CREATE SCHEMA VARIABLE, so it's a bit

Yes, there is inconsistency, but I think it is necessary. The name
"variable" is too generic. Theoretically we can use other adjectives like
session variables or global variables and the name will be valid. But it
doesn't describe the fundamentals of design. This is similar to the
package's variables from PL/SQL. These variables are global, session's
variables too. But the usual name is "package variables". So schema
variables are assigned to schemes, and I think a good name can be "schema
variables". But it is not necessary to repeat keyword schema in the CREATE
COMMAND.

My opinion is not too strong in this case, and I can accept just
"variables" or "session's variables" or "global variables", but I am not
sure if these names describe this feature well, because still they are too
generic. There are too many different implementations of session global
variables (see PL/SQL or T-SQL or DB2).

> just using UPDATE in some way).

I am sure so I want to use variables in SELECTs. One interesting case is
using variables in RLS.

I prefer to fix this error message to "column or variable ... does not
exist"

> better (tracing which variables need resetting) later.
I have to check it.

> So this seems pretty useless to me, +1 to disabling it.

I'll disable it.

---

From: Justin Pryzby <[email-masked]>
Date: Sat, 6 Nov 2021 09:57:14 -0500

On Sat, Nov 06, 2021 at 04:45:19AM +0100, Pavel Stehule wrote:
> st 3. 11. 2021 v 14:05 odesílatel Tomas Vondra <[email-masked]> napsal:

+1

At least the error messages need to be consistent.
It doesn't make sense to have both of these:

> variables (see PL/SQL or T-SQL or DB2).

I would prefer "session variable".

To me, this feature seems similar to a CTE (which exists for a single
statement), or a temporary table (which exists for a single transaction).  So
"session" conveys a lot more of its meaning than "schema".

But don't rename everything just for me...

-- 
Justin

---

From: Pavel Stehule <[email-masked]>
Date: Sat, 6 Nov 2021 16:40:55 +0100

so 6. 11. 2021 v 15:57 odesílatel Justin Pryzby <[email-masked]>
napsal:

> varid);

It depends on where you are looking. There are two perspectives - data and
metadata. And if I use data perspective, then it is session related. If I
use metadata perspective, then it can be persistent or temporal like
tables. I see strong similarity with Global Temporary Tables - but I think
naming "local temporary tables" and "global temporary tables" can be not
intuitive or messy for a lot of people too. Anyway, if people will try to
find this feature on Google, then probably use keywords "session
variables", so maybe my preference of more technical terminology is obscure
and not practical, and the name "session variables" can be more practical
for other people. If I use the system used for GTT - then the exact name
can be "Global Session Variable". Can we use this name? Or shortly just
Session Variables because we don't support local session variables now.

What do you think about it?

Regards

Pavel

---

From: Tomas Vondra <[email-masked]>
Date: Sun, 7 Nov 2021 22:14:00 +0100

On 11/6/21 16:40, Pavel Stehule wrote:
> 
> 
> so 6. 11. 2021 v 15:57 odesílatel Justin Pryzby <[email-masked] 
> <mailto:[email-masked]>> napsal:
> 
>      > st 3. 11. 2021 v 14:05 odesílatel Tomas Vondra
>     <[email-masked]
>     <mailto:[email-masked]>> napsal:
> It depends on where you are looking. There are two perspectives - data 
> and metadata. And if I use data perspective, then it is session related. 
> If I use metadata perspective, then it can be persistent or temporal 
> like tables.

I think you mean "temporary" not "temporal". This really confused me for 
a while, because temporal means "involving time" (e.g. a table with 
from/to timestamp range, etc).

> I see strong similarity with Global Temporary Tables - but 
> I think naming "local temporary tables" and "global temporary tables" 
> can be not intuitive or messy for a lot of people too.

Right, it's a bit like global temporary tables, in the sense that 
there's a shared definition but local (session) state.

> Anyway, if people will try to find this feature on Google, then 
> probably use keywords "session variables", so maybe my preference of
> more technical terminology is obscure and not practical, and the name
> "session variables" can be more practical for other people.
Hmmm, maybe.

> If I use the system used for GTT - then the exact name can be "Global
> Session Variable". Can we use this name? Or shortly just Session
> Variables because we don't support local session variables now.

So a "local variable" would be defined just for a given session, just 
like a temporary table? Wouldn't that have the same issues with catalog 
bloat as temporary tables?

I'd probably vote for "session variables". We can call it local/global 
session variables in the future, if we end up implementing that.

regards

---

From: Tomas Vondra <[email-masked]>
Date: Sun, 7 Nov 2021 22:36:50 +0100

On 11/6/21 04:45, Pavel Stehule wrote:
> <[email-masked] <mailto:[email-masked]>> 
> napsal:
> 
>     Hi,
> 
> "variable" is too generic. Theoretically we can use other adjectives 
> like session variables or global variables and the name will be valid. 
> But it doesn't describe the fundamentals of design. This is similar to 
> the package's variables from PL/SQL. These variables are global, 
> session's variables too. But the usual name is "package variables". So 
> schema variables are assigned to schemes, and I think a good name can be 
> "schema variables". But it is not necessary to repeat keyword schema in 
> the CREATE COMMAND.
> 
> sure if these names describe this feature well, because still they are 
> too generic. There are too many different implementations of session 
> global variables (see PL/SQL or T-SQL or DB2).
> 

OK. "Session variable" seems better to me, but I'm not sure how well 
that matches other databases. I'm not sure how much should we feel 
constrained by naming in other databases, though.

> I am sure so I want to use variables in SELECTs. One interesting case is 

How much more complicated would it be without the SELECT?

> I prefer to fix this error message to "column or variable ... does not 

Not sure it's a good idea to make the error message more ambiguous. Most 
people won't use variables at all, and the message will be less clear 
for them.

regards

---

From: Pavel Stehule <[email-masked]>
Date: Mon, 8 Nov 2021 05:02:47 +0100

> constrained by naming in other databases, though.

session variables is generic term - there are big differences already -
T-SQL versus PL/SQL or SQL+ or DB2

> How much more complicated would it be without the SELECT?

It is not too complicated, just you want to introduce SELECT2. The sense of
session variables is to be used. Has no sense to hold a value on a server
without the possibility to use it.

Session variables can be used as global variables in PL/pgSQL. If you
cannot use it in SQL expressions, then you need to copy it to a local
variable, and then you can use it. That cannot work. This design is a
replacement of a untyped not nullable slow workaround based on GUC, there
is a necessity to use it in SQL.

> people won't use variables at all, and the message will be less clear

Yes, there is new complexity. But it is an analogy with variables in
PL/pgSQL with all benefits and negatives. You don't want to use dynamic SQL
everywhere you use PL/pgSQL variables.

There are more cases than RLS in SQL

1. hold value in session (for interactive work or for non interactive
scripts). Sometimes you want to reuse value - we can now use CTE or
temporary tables. But in this case you have to store relation, you cannot
store value, that can be used as a query parameter.

2. allow safe and effective parametrization of SQL scripts, and copy value
from client side to server side (there is not risk of SQL injection).

run script with parameter -v xx=10

```
create temp variable xx as int;
set xx = :`xx`;
do $$
  .. -- I can work with variable xx on server side

  ...

$$

This is complement to client side variables - the advantage is possibility
to use outside psql, the are type, and the metadata can be permanent.

3. you can share value by PL environments (and by possible clients). But
this sharing is secure - the rules are the same like holding value in an
table.

Session variables increase complexity a little bit, but increases
possibilities and comfort for developers that use databases directly. The
analogy with PL/pgSQL variables is well, jut you are not limited to
PL/pgSQL scope.

Regards

Pavel

---

From: Justin Pryzby <[email-masked]>
Date: Mon, 8 Nov 2021 05:19:36 -0600

> session variables in the future, if we end up implementing that.

By chance, I ran into this pre-existing use of the phrase "session variable".
introduced since 8fbef1090:

doc/src/sgml/ref/set_role.sgml:   <command>SET ROLE</command> does not process session variables as specified by

That's the *only* use of that phrase, but you'd have to change it to something
like ".. does not process role-specific variables a specified by ..".

-- 
Justin

---

From: Pavel Stehule <[email-masked]>
Date: Mon, 15 Nov 2021 21:00:13 +0100

Hi Justin

> of the patch.  My amendments are in 0003.

Thank you for review and fixes, I try to complete some version for next
work, and looks so your patch 0001 is broken

gedit reports to me broken unicode \A0\A0\A0\A0\A0

my last patch has 276KB and your patch has 293KB?

Thank you

Pavel

---

From: Pavel Stehule <[email-masked]>
Date: Mon, 15 Nov 2021 21:06:08 +0100

> my last patch has 276KB and your patch has 293KB?

Please, can you resend your version of patch 0001?

Thank you

Pavel

---

From: Justin Pryzby <[email-masked]>
Date: Mon, 15 Nov 2021 14:23:52 -0600

> Please, can you resend your version of patch 0001?

https://www.postgresql.org/message-id/[email-masked]

0001 is exactly your patch applied to HEAD, and 0002 are Tomas' changes
relative to your patch.

0003 is my contribution on top.  My intent is that you wouldn't apply 0001, but
rather apply my 0003 on top of your existing branch, and then review 0002/0003,
and then squish the changes into your patch.

I see the 0xa0 stuff in your original patch before my changes, but I'm not sure
what went wrong.

Let me know if you have any issue applying my changes on top of your existing,
local branch ?

-- 
Justin

---

From: Pavel Stehule <[email-masked]>
Date: Wed, 17 Nov 2021 07:32:31 +0100

> rather apply my 0003 on top of your existing branch, and then review

It is ok, I was able to apply all your patches to my local branch

Regards

Pavel

---

From: Pavel Stehule <[email-masked]>
Date: Wed, 17 Nov 2021 17:05:57 +0100

Hi

>     debugging. If this idea is too strange, I have no problem to disable

I checked code, and this case is disallowed already

postgres=# CREATE IMMUTABLE VARIABLE xx AS int NOT NULL;
ERROR:  IMMUTABLE NOT NULL variable requires default expression

Regards

Pavel

---

From: Pavel Stehule <[email-masked]>
Date: Sun, 19 Dec 2021 07:23:27 +0100

Hi

> rather apply my 0003 on top of your existing branch, and then review

I am sending new versions of patches.

I hope I solved all Tomas's objections.

1. The schema variables were renamed to session variables
2. I fixed issues related to creating, dropping variables under
subtransactions + regress tests
3. I fixed issues in pg_dump + regress tests

Regards

Pavel

diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c
index c5c3f26ecf..ec175dc96c 100644
--- a/src/backend/parser/parse_relation.c
+++ b/src/backend/parser/parse_relation.c
@@ -3589,8 +3589,8 @@ errorMissingColumn(ParseState *pstate,
 		ereport(ERROR,
 				(errcode(ERRCODE_UNDEFINED_COLUMN),
 				 relname ?
-				 errmsg("column %s.%s does not exist", relname, colname) :
-				 errmsg("column \"%s\" does not exist", colname),
+				 errmsg("column or variable %s.%s does not exist", relname, colname) :
+				 errmsg("column or variable \"%s\" does not exist", colname),
 				 state->rfirst ? closestfirst ?
 				 errhint("Perhaps you meant to reference the column \"%s.%s\".",
 						 state->rfirst->eref->aliasname, closestfirst) :
@@ -3609,8 +3609,8 @@ errorMissingColumn(ParseState *pstate,
 		ereport(ERROR,
 				(errcode(ERRCODE_UNDEFINED_COLUMN),
 				 relname ?
-				 errmsg("column %s.%s does not exist", relname, colname) :
-				 errmsg("column \"%s\" does not exist", colname),
+				 errmsg("column or variable %s.%s does not exist", relname, colname) :
+				 errmsg("column or variable  \"%s\" does not exist", colname),
 				 errhint("Perhaps you meant to reference the column \"%s.%s\" or the column \"%s.%s\".",
 						 state->rfirst->eref->aliasname, closestfirst,
 						 state->rsecond->eref->aliasname, closestsecond),
diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out
index 24d1c7cd28..c2e3d2ef91 100644
--- a/src/test/regress/expected/alter_table.out
+++ b/src/test/regress/expected/alter_table.out
@@ -809,7 +809,7 @@ drop table atacc1;
 create table atacc1 ( test int );
 -- add a check constraint (fails)
 alter table atacc1 add constraint atacc_test1 check (test1>3);
-ERROR:  column "test1" does not exist
+ERROR:  column or variable "test1" does not exist
 HINT:  Perhaps you meant to reference the column "atacc1.test".
 drop table atacc1;
 -- something a little more complicated
@@ -1287,19 +1287,19 @@ select * from atacc1;
 (1 row)
 
 select * from atacc1 order by a;
-ERROR:  column "a" does not exist
+ERROR:  column or variable "a" does not exist
 LINE 1: select * from atacc1 order by a;
                                       ^
 select * from atacc1 order by "........pg.dropped.1........";
-ERROR:  column "........pg.dropped.1........" does not exist
+ERROR:  column or variable "........pg.dropped.1........" does not exist
 LINE 1: select * from atacc1 order by "........pg.dropped.1........"...
                                       ^
 select * from atacc1 group by a;
-ERROR:  column "a" does not exist
+ERROR:  column or variable "a" does not exist
 LINE 1: select * from atacc1 group by a;
                                       ^
 select * from atacc1 group by "........pg.dropped.1........";
-ERROR:  column "........pg.dropped.1........" does not exist
+ERROR:  column or variable "........pg.dropped.1........" does not exist
 LINE 1: select * from atacc1 group by "........pg.dropped.1........"...
                                       ^
 select atacc1.* from atacc1;
@@ -1309,11 +1309,11 @@ select atacc1.* from atacc1;
 (1 row)
 
 select a from atacc1;
-ERROR:  column "a" does not exist
+ERROR:  column or variable "a" does not exist
 LINE 1: select a from atacc1;
                ^
 select atacc1.a from atacc1;
-ERROR:  column atacc1.a does not exist
+ERROR:  column or variable atacc1.a does not exist
 LINE 1: select atacc1.a from atacc1;
                ^
 select b,c,d from atacc1;
@@ -1323,27 +1323,27 @@ select b,c,d from atacc1;
 (1 row)
 
 select a,b,c,d from atacc1;
-ERROR:  column "a" does not exist
+ERROR:  column or variable "a" does not exist
 LINE 1: select a,b,c,d from atacc1;
                ^
 select * from atacc1 where a = 1;
-ERROR:  column "a" does not exist
+ERROR:  column or variable "a" does not exist
 LINE 1: select * from atacc1 where a = 1;
                                    ^
 select "........pg.dropped.1........" from atacc1;
-ERROR:  column "........pg.dropped.1........" does not exist
+ERROR:  column or variable "........pg.dropped.1........" does not exist
 LINE 1: select "........pg.dropped.1........" from atacc1;
                ^
 select atacc1."........pg.dropped.1........" from atacc1;
-ERROR:  column atacc1.........pg.dropped.1........ does not exist
+ERROR:  column or variable atacc1.........pg.dropped.1........ does not exist
 LINE 1: select atacc1."........pg.dropped.1........" from atacc1;
                ^
 select "........pg.dropped.1........",b,c,d from atacc1;
-ERROR:  column "........pg.dropped.1........" does not exist
+ERROR:  column or variable "........pg.dropped.1........" does not exist
 LINE 1: select "........pg.dropped.1........",b,c,d from atacc1;
                ^
 select * from atacc1 where "........pg.dropped.1........" = 1;
-ERROR:  column "........pg.dropped.1........" does not exist
+ERROR:  column or variable "........pg.dropped.1........" does not exist
 LINE 1: select * from atacc1 where "........pg.dropped.1........" = ...
                                    ^
 -- UPDATEs
@@ -1352,7 +1352,7 @@ ERROR:  column "a" of relation "atacc1" does not exist
 LINE 1: update atacc1 set a = 3;
                           ^
 update atacc1 set b = 2 where a = 3;
-ERROR:  column "a" does not exist
+ERROR:  column or variable "a" does not exist
 LINE 1: update atacc1 set b = 2 where a = 3;
                                       ^
 update atacc1 set "........pg.dropped.1........" = 3;
@@ -1360,7 +1360,7 @@ ERROR:  column "........pg.dropped.1........" of relation "atacc1" does not exis
 LINE 1: update atacc1 set "........pg.dropped.1........" = 3;
                           ^
 update atacc1 set b = 2 where "........pg.dropped.1........" = 3;
-ERROR:  column "........pg.dropped.1........" does not exist
+ERROR:  column or variable "........pg.dropped.1........" does not exist
 LINE 1: update atacc1 set b = 2 where "........pg.dropped.1........"...
                                       ^
 -- INSERTs
@@ -1408,11 +1408,11 @@ LINE 1: insert into atacc1 ("........pg.dropped.1........",b,c,d) va...
                             ^
 -- DELETEs
 delete from atacc1 where a = 3;
-ERROR:  column "a" does not exist
+ERROR:  column or variable "a" does not exist
 LINE 1: delete from atacc1 where a = 3;
                                  ^
 delete from atacc1 where "........pg.dropped.1........" = 3;
-ERROR:  column "........pg.dropped.1........" does not exist
+ERROR:  column or variable "........pg.dropped.1........" does not exist
 LINE 1: delete from atacc1 where "........pg.dropped.1........" = 3;
                                  ^
 delete from atacc1;
@@ -1490,9 +1490,9 @@ ERROR:  column "a" named in key does not exist
 alter table atacc1 add unique("........pg.dropped.1........");
 ERROR:  column "........pg.dropped.1........" named in key does not exist
 alter table atacc1 add check (a > 3);
-ERROR:  column "a" does not exist
+ERROR:  column or variable "a" does not exist
 alter table atacc1 add check ("........pg.dropped.1........" > 3);
-ERROR:  column "........pg.dropped.1........" does not exist
+ERROR:  column or variable "........pg.dropped.1........" does not exist
 create table atacc2 (id int4 unique);
 alter table atacc1 add foreign key (a) references atacc2(id);
 ERROR:  column "a" referenced in foreign key constraint does not exist
@@ -1698,7 +1698,7 @@ select f1 from c1;
 
 alter table c1 drop column f1;
 select f1 from c1;
-ERROR:  column "f1" does not exist
+ERROR:  column or variable "f1" does not exist
 LINE 1: select f1 from c1;
                ^
 HINT:  Perhaps you meant to reference the column "c1.f2".
@@ -1712,7 +1712,7 @@ ERROR:  cannot drop inherited column "f1"
 alter table p1 drop column f1;
 -- c1.f1 is dropped now, since there is no local definition for it
 select f1 from c1;
-ERROR:  column "f1" does not exist
+ERROR:  column or variable "f1" does not exist
 LINE 1: select f1 from c1;
                ^
 HINT:  Perhaps you meant to reference the column "c1.f2".
diff --git a/src/test/regress/expected/copy2.out b/src/test/regress/expected/copy2.out
index 5f3685e9ef..edff69cd22 100644
--- a/src/test/regress/expected/copy2.out
+++ b/src/test/regress/expected/copy2.out
@@ -105,7 +105,7 @@ LINE 1: COPY x TO stdout WHERE a = 1;
 COPY x from stdin WHERE a = 50004;
 COPY x from stdin WHERE a > 60003;
 COPY x from stdin WHERE f > 60003;
-ERROR:  column "f" does not exist
+ERROR:  column or variable "f" does not exist
 LINE 1: COPY x from stdin WHERE f > 60003;
                                 ^
 COPY x from stdin WHERE a = max(x.b);
diff --git a/src/test/regress/expected/errors.out b/src/test/regress/expected/errors.out
index 15862d4475..0ba21ead0b 100644
--- a/src/test/regress/expected/errors.out
+++ b/src/test/regress/expected/errors.out
@@ -27,7 +27,7 @@ LINE 1: select * from nonesuch;
                       ^
 -- bad name in target list
 select nonesuch from pg_database;
-ERROR:  column "nonesuch" does not exist
+ERROR:  column or variable "nonesuch" does not exist
 LINE 1: select nonesuch from pg_database;
                ^
 -- empty distinct list isn't OK
@@ -37,17 +37,17 @@ LINE 1: select distinct from pg_database;
                         ^
 -- bad attribute name on lhs of operator
 select * from pg_database where nonesuch = pg_database.datname;
-ERROR:  column "nonesuch" does not exist
+ERROR:  column or variable "nonesuch" does not exist
 LINE 1: select * from pg_database where nonesuch = pg_database.datna...
                                         ^
 -- bad attribute name on rhs of operator
 select * from pg_database where pg_database.datname = nonesuch;
-ERROR:  column "nonesuch" does not exist
+ERROR:  column or variable "nonesuch" does not exist
 LINE 1: ...ect * from pg_database where pg_database.datname = nonesuch;
                                                               ^
 -- bad attribute name in select distinct on
 select distinct on (foobar) * from pg_database;
-ERROR:  column "foobar" does not exist
+ERROR:  column or variable "foobar" does not exist
 LINE 1: select distinct on (foobar) * from pg_database;
                             ^
 -- grouping with FOR UPDATE
diff --git a/src/test/regress/expected/generated.out b/src/test/regress/expected/generated.out
index cb9373227d..adfa95cc77 100644
--- a/src/test/regress/expected/generated.out
+++ b/src/test/regress/expected/generated.out
@@ -55,7 +55,7 @@ LINE 2:     b int GENERATED ALWAYS AS (num_nulls(gtest_err_2c)) STOR...
 DETAIL:  This would cause the generated column to depend on its own value.
 -- invalid reference
 CREATE TABLE gtest_err_3 (a int PRIMARY KEY, b int GENERATED ALWAYS AS (c * 2) STORED);
-ERROR:  column "c" does not exist
+ERROR:  column or variable "c" does not exist
 LINE 1: ..._3 (a int PRIMARY KEY, b int GENERATED ALWAYS AS (c * 2) STO...
                                                              ^
 -- generation expression must be immutable
@@ -718,7 +718,7 @@ ALTER TABLE gtest25 ADD COLUMN x int GENERATED ALWAYS AS (b * 4) STORED;  -- err
 ERROR:  cannot use generated column "b" in column generation expression
 DETAIL:  A generated column cannot reference another generated column.
 ALTER TABLE gtest25 ADD COLUMN x int GENERATED ALWAYS AS (z * 4) STORED;  -- error
-ERROR:  column "z" does not exist
+ERROR:  column or variable "z" does not exist
 ALTER TABLE gtest25 ADD COLUMN c int DEFAULT 42,
   ADD COLUMN x int GENERATED ALWAYS AS (c * 4) STORED;
 ALTER TABLE gtest25 ADD COLUMN d int DEFAULT 101;
diff --git a/src/test/regress/expected/insert_conflict.out b/src/test/regress/expected/insert_conflict.out
index 66d8633e3e..f11bed03d6 100644
--- a/src/test/regress/expected/insert_conflict.out
+++ b/src/test/regress/expected/insert_conflict.out
@@ -245,13 +245,13 @@ LINE 1: ...y) do update set fruit = excluded.fruit RETURNING excluded.f...
 HINT:  There is an entry for table "excluded", but it cannot be referenced from this part of the query.
 -- Only suggest <table>.* column when inference element misspelled:
 insert into insertconflicttest values (1, 'Apple') on conflict (keyy) do update set fruit = excluded.fruit;
-ERROR:  column "keyy" does not exist
+ERROR:  column or variable  "keyy" does not exist
 LINE 1: ...nsertconflicttest values (1, 'Apple') on conflict (keyy) do ...
                                                              ^
 HINT:  Perhaps you meant to reference the column "insertconflicttest.key" or the column "excluded.key".
 -- Have useful HINT for EXCLUDED.* RTE within UPDATE:
 insert into insertconflicttest values (1, 'Apple') on conflict (key) do update set fruit = excluded.fruitt;
-ERROR:  column excluded.fruitt does not exist
+ERROR:  column or variable excluded.fruitt does not exist
 LINE 1: ... 'Apple') on conflict (key) do update set fruit = excluded.f...
                                                              ^
 HINT:  Perhaps you meant to reference the column "excluded.fruit".
@@ -448,7 +448,7 @@ drop table insertconflicttest;
 create table syscolconflicttest(key int4, data text);
 insert into syscolconflicttest values (1);
 insert into syscolconflicttest values (1) on conflict (key) do update set data = excluded.ctid::text;
-ERROR:  column excluded.ctid does not exist
+ERROR:  column or variable excluded.ctid does not exist
 LINE 1: ...values (1) on conflict (key) do update set data = excluded.c...
                                                              ^
 drop table syscolconflicttest;
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index d5b5b775fd..b7559d4bbd 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -1646,7 +1646,7 @@ SELECT * FROM J1_TBL JOIN J2_TBL USING (i) AS x WHERE x.i = 1;  -- ok
 (1 row)
 
 SELECT * FROM J1_TBL JOIN J2_TBL USING (i) AS x WHERE x.t = 'one';  -- error
-ERROR:  column x.t does not exist
+ERROR:  column or variable x.t does not exist
 LINE 1: ...CT * FROM J1_TBL JOIN J2_TBL USING (i) AS x WHERE x.t = 'one...
                                                              ^
 SELECT * FROM (J1_TBL JOIN J2_TBL USING (i) AS x) AS xx WHERE x.i = 1;  -- error (XXX could use better hint)
@@ -2497,7 +2497,7 @@ select * from t1 left join t2 on (t1.a = t2.a);
 
 -- Test matching of column name with wrong alias
 select t1.x from t1 join t3 on (t1.a = t3.x);
-ERROR:  column t1.x does not exist
+ERROR:  column or variable t1.x does not exist
 LINE 1: select t1.x from t1 join t3 on (t1.a = t3.x);
                ^
 HINT:  Perhaps you meant to reference the column "t3.x".
@@ -4927,19 +4927,19 @@ select * from
 --
 select t1.uunique1 from
   tenk1 t1 join tenk2 t2 on t1.two = t2.two; -- error, prefer "t1" suggestion
-ERROR:  column t1.uunique1 does not exist
+ERROR:  column or variable t1.uunique1 does not exist
 LINE 1: select t1.uunique1 from
                ^
 HINT:  Perhaps you meant to reference the column "t1.unique1".
 select t2.uunique1 from
   tenk1 t1 join tenk2 t2 on t1.two = t2.two; -- error, prefer "t2" suggestion
-ERROR:  column t2.uunique1 does not exist
+ERROR:  column or variable t2.uunique1 does not exist
 LINE 1: select t2.uunique1 from
                ^
 HINT:  Perhaps you meant to reference the column "t2.unique1".
 select uunique1 from
   tenk1 t1 join tenk2 t2 on t1.two = t2.two; -- error, suggest both at once
-ERROR:  column "uunique1" does not exist
+ERROR:  column or variable  "uunique1" does not exist
 LINE 1: select uunique1 from
                ^
 HINT:  Perhaps you meant to reference the column "t1.unique1" or the column "t2.unique1".
@@ -4951,7 +4951,7 @@ select atts.relid::regclass, s.* from pg_stats s join
     a.attrelid::regclass::text join (select unnest(indkey) attnum,
     indexrelid from pg_index i) atts on atts.attnum = a.attnum where
     schemaname != 'pg_catalog';
-ERROR:  column atts.relid does not exist
+ERROR:  column or variable atts.relid does not exist
 LINE 1: select atts.relid::regclass, s.* from pg_stats s join
                ^
 --
@@ -5970,7 +5970,7 @@ lateral (select * from int8_tbl t1,
 
 -- test some error cases where LATERAL should have been used but wasn't
 select f1,g from int4_tbl a, (select f1 as g) ss;
-ERROR:  column "f1" does not exist
+ERROR:  column or variable "f1" does not exist
 LINE 1: select f1,g from int4_tbl a, (select f1 as g) ss;
                                              ^
 HINT:  There is a column named "f1" in table "a", but it cannot be referenced from this part of the query.
@@ -5980,7 +5980,7 @@ LINE 1: select f1,g from int4_tbl a, (select a.f1 as g) ss;
                                              ^
 HINT:  There is an entry for table "a", but it cannot be referenced from this part of the query.
 select f1,g from int4_tbl a cross join (select f1 as g) ss;
-ERROR:  column "f1" does not exist
+ERROR:  column or variable "f1" does not exist
 LINE 1: select f1,g from int4_tbl a cross join (select f1 as g) ss;
                                                        ^
 HINT:  There is a column named "f1" in table "a", but it cannot be referenced from this part of the query.
@@ -6015,7 +6015,7 @@ LINE 1: select 1 from tenk1 a, lateral (select max(a.unique1) from i...
 create temp table xx1 as select f1 as x1, -f1 as x2 from int4_tbl;
 -- error, can't do this:
 update xx1 set x2 = f1 from (select * from int4_tbl where f1 = x1) ss;
-ERROR:  column "x1" does not exist
+ERROR:  column or variable "x1" does not exist
 LINE 1: ... set x2 = f1 from (select * from int4_tbl where f1 = x1) ss;
                                                                 ^
 HINT:  There is a column named "x1" in table "xx1", but it cannot be referenced from this part of the query.
@@ -6035,7 +6035,7 @@ update xx1 set x2 = f1 from xx1, lateral (select * from int4_tbl where f1 = x1)
 ERROR:  table name "xx1" specified more than once
 -- also errors:
 delete from xx1 using (select * from int4_tbl where f1 = x1) ss;
-ERROR:  column "x1" does not exist
+ERROR:  column or variable "x1" does not exist
 LINE 1: ...te from xx1 using (select * from int4_tbl where f1 = x1) ss;
                                                                 ^
 HINT:  There is a column named "x1" in table "xx1", but it cannot be referenced from this part of the query.
diff --git a/src/test/regress/expected/plpgsql.out b/src/test/regress/expected/plpgsql.out
index 33eb5e54b9..3bb7bb6527 100644
--- a/src/test/regress/expected/plpgsql.out
+++ b/src/test/regress/expected/plpgsql.out
@@ -2573,7 +2573,7 @@ end; $$ language plpgsql;
 -- should fail: SQLSTATE and SQLERRM are only in defined EXCEPTION
 -- blocks
 select excpt_test1();
-ERROR:  column "sqlstate" does not exist
+ERROR:  column or variable "sqlstate" does not exist
 LINE 1: sqlstate
         ^
 QUERY:  sqlstate
@@ -2588,7 +2588,7 @@ begin
 end; $$ language plpgsql;
 -- should fail
 select excpt_test2();
-ERROR:  column "sqlstate" does not exist
+ERROR:  column or variable "sqlstate" does not exist
 LINE 1: sqlstate
         ^
 QUERY:  sqlstate
@@ -4602,7 +4602,7 @@ BEGIN
         RAISE NOTICE '%, %', r.roomno, r.comment;
     END LOOP;
 END$$;
-ERROR:  column "foo" does not exist
+ERROR:  column or variable "foo" does not exist
 LINE 1: SELECT rtrim(roomno) AS roomno, foo FROM Room ORDER BY roomn...
                                         ^
 QUERY:  SELECT rtrim(roomno) AS roomno, foo FROM Room ORDER BY roomno
@@ -4644,7 +4644,7 @@ begin
   raise notice 'x = %', x;
 end;
 $$;
-ERROR:  column "x" does not exist
+ERROR:  column or variable "x" does not exist
 LINE 1: x + 1
         ^
 QUERY:  x + 1
@@ -4656,7 +4656,7 @@ begin
   raise notice 'x = %, y = %', x, y;
 end;
 $$;
-ERROR:  column "x" does not exist
+ERROR:  column or variable "x" does not exist
 LINE 1: x + 1
         ^
 QUERY:  x + 1
@@ -5669,7 +5669,7 @@ ALTER TABLE alter_table_under_transition_tables
   DROP column name;
 UPDATE alter_table_under_transition_tables
   SET id = id;
-ERROR:  column "name" does not exist
+ERROR:  column or variable "name" does not exist
 LINE 1: (SELECT string_agg(id || '=' || name, ',') FROM d)
                                         ^
 QUERY:  (SELECT string_agg(id || '=' || name, ',') FROM d)
diff --git a/src/test/regress/expected/rowtypes.out b/src/test/regress/expected/rowtypes.out
index 0e71baf8fb..0a55166a30 100644
--- a/src/test/regress/expected/rowtypes.out
+++ b/src/test/regress/expected/rowtypes.out
@@ -927,7 +927,7 @@ LINE 1: select text(fullname) from fullname;
                ^
 HINT:  No function matches the given name and argument types. You might need to add explicit type casts.
 select fullname.text from fullname;  -- error
-ERROR:  column fullname.text does not exist
+ERROR:  column or variable fullname.text does not exist
 LINE 1: select fullname.text from fullname;
                ^
 -- same, but RECORD instead of named composite type:
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index b58b062b10..5ef73ae992 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1191,7 +1191,7 @@ drop rule rules_foorule on rules_foo;
 -- this should fail because f1 is not exposed for unqualified reference:
 create rule rules_foorule as on insert to rules_foo where f1 < 100
 do instead insert into rules_foo2 values (f1);
-ERROR:  column "f1" does not exist
+ERROR:  column or variable "f1" does not exist
 LINE 2: do instead insert into rules_foo2 values (f1);
                                                   ^
 HINT:  There is a column named "f1" in table "old", but it cannot be referenced from this part of the query.
@@ -2904,7 +2904,7 @@ select * from rules_fooview;
 (1 row)
 
 select xmin, * from rules_fooview;  -- fail, views don't have such a column
-ERROR:  column "xmin" does not exist
+ERROR:  column or variable "xmin" does not exist
 LINE 1: select xmin, * from rules_fooview;
                ^
 select reltoastrelid, relkind, relfrozenxid
diff --git a/src/test/regress/expected/transactions.out b/src/test/regress/expected/transactions.out
index 61862d595d..cee0d09f55 100644
--- a/src/test/regress/expected/transactions.out
+++ b/src/test/regress/expected/transactions.out
@@ -218,7 +218,7 @@ SELECT * FROM trans_barbaz;	-- should have 1
 BEGIN;
 	SAVEPOINT one;
 		SELECT trans_foo;
-ERROR:  column "trans_foo" does not exist
+ERROR:  column or variable "trans_foo" does not exist
 LINE 1: SELECT trans_foo;
                ^
 	ROLLBACK TO SAVEPOINT one;
@@ -267,7 +267,7 @@ BEGIN;
 	SAVEPOINT one;
 		INSERT INTO savepoints VALUES (5);
 		SELECT trans_foo;
-ERROR:  column "trans_foo" does not exist
+ERROR:  column or variable "trans_foo" does not exist
 LINE 1: SELECT trans_foo;
                ^
 COMMIT;
diff --git a/src/test/regress/expected/union.out b/src/test/regress/expected/union.out
index dece7310cf..2514a2e4ac 100644
--- a/src/test/regress/expected/union.out
+++ b/src/test/regress/expected/union.out
@@ -907,7 +907,7 @@ ORDER BY q2,q1;
 
 -- This should fail, because q2 isn't a name of an EXCEPT output column
 SELECT q1 FROM int8_tbl EXCEPT SELECT q2 FROM int8_tbl ORDER BY q2 LIMIT 1;
-ERROR:  column "q2" does not exist
+ERROR:  column or variable "q2" does not exist
 LINE 1: ... int8_tbl EXCEPT SELECT q2 FROM int8_tbl ORDER BY q2 LIMIT 1...
                                                              ^
 HINT:  There is a column named "q2" in table "*SELECT* 2", but it cannot be referenced from this part of the query.
diff --git a/doc/src/sgml/advanced.sgml b/doc/src/sgml/advanced.sgml
index 71ae423f63..67f7aa673c 100644
--- a/doc/src/sgml/advanced.sgml
+++ b/doc/src/sgml/advanced.sgml
@@ -700,6 +700,57 @@ SELECT name, elevation
   </sect1>

+  <sect1 id="tutorial-session-variables">
+   <title>Session Variables</title>
+
+   <indexterm zone="tutorial-session-variables">
+    <primary>Session variables</primary>
+   </indexterm>
+
+   <indexterm>
+    <primary>session variable</primary>
+    <secondary>introduction</secondary>
+   </indexterm>
+
+   <para>
+    Session variables are database objects that can hold a value.
+    Session variables, like relations, exist within a schema and their access is
+    controlled via <command>GRANT</command> and <command>REVOKE</command> 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 is 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.
+   </para>
+
+  </sect1>
+
+
   <sect1 id="tutorial-conclusion">
    <title>Conclusion</title>
 
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 03e2537b07..cabb3071df 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -364,6 +364,11 @@
       <entry><link linkend="catalog-pg-user-mapping"><structname>pg_user_mapping</structname></link></entry>
       <entry>mappings of users to foreign servers</entry>
      </row>
+
+     <row>
+      <entry><link linkend="catalog-pg-variable"><structname>pg_variable</structname></link></entry>
+      <entry>schema variables</entry>
+     </row>
     </tbody>
    </tgroup>
   </table>
@@ -13975,4 +13980,143 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
 
  </sect1>
 
index e5c1356d8c..423677b9af 100644
+    <title><command>Session variables and constants</command></title>
+     <productname>PostgreSQL</productname> has session variables and
+     immutable session variables. Session variables can be created

 
diff --git a/doc/src/sgml/ref/comment.sgml b/doc/src/sgml/ref/comment.sgml
index e07fc47fd3..46b093fda1 100644
--- a/doc/src/sgml/ref/comment.sgml
+++ b/doc/src/sgml/ref/comment.sgml
@@ -65,6 +65,7 @@ COMMENT ON
   TRANSFORM FOR <replaceable>type_name</replaceable> LANGUAGE <replaceable>lang_name</replaceable> |
   TRIGGER <replaceable class="parameter">trigger_name</replaceable> ON <replaceable class="parameter">table_name</replaceable> |
   TYPE <replaceable class="parameter">object_name</replaceable> |
+  VARIABLE <replaceable class="parameter">object_name</replaceable> |
   VIEW <replaceable class="parameter">object_name</replaceable>
 } IS '<replaceable class="parameter">text</replaceable>'
 
diff --git a/doc/src/sgml/ref/create_variable.sgml b/doc/src/sgml/ref/create_variable.sgml
new file mode 100644
index 0000000000..08e105c39b
--- /dev/null
+++ b/doc/src/sgml/ref/create_variable.sgml
@@ -0,0 +1,193 @@
+<!--
+doc/src/sgml/ref/create_variable.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-createvariable">
+ <indexterm zone="sql-createvariable">
+  <primary>CREATE VARIABLE</primary>
+ </indexterm>
+
+ <indexterm>
+  <primary>session variable</primary>
+  <secondary>defining</secondary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>CREATE VARIABLE</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>CREATE VARIABLE</refname>
+  <refpurpose>define a session variable</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+CREATE [ { TEMPORARY | TEMP } ] [ IMMUTABLE ] VARIABLE [ IF NOT EXISTS ] <replaceable class="parameter">name</replaceable> [ AS ] <replaceable class="parameter">data_type</replaceable> ] [ COLLATE <replaceable class="parameter">collation</replaceable> ]
+    [ NOT NULL ] [ DEFAULT <replaceable class="parameter">default_expr</replaceable> ] [ { ON COMMIT DROP | ON TRANSACTION END RESET } ]
+</synopsis>
+ </refsynopsisdiv>
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   The <command>CREATE VARIABLE</command> command creates a session variable.
+   Schema variables, like relations, exist within a schema and their access is
+   controlled via <command>GRANT</command> and <command>REVOKE</command> commands.
+   Changing a session variable is non-transactional.
+  </para>
+
+  <para>
+   The value of a session variable is local to the current session. Retrieving
+   a variable's value returns either a NULL or a default value, unless its value
+   is set to something else in the current session with a LET command. The content
+   of a variable is not transactional. This is the same as regular variables in PL languages.
+  </para>
+
+  <para>
+   Schema variables are retrieved by the <command>SELECT</command> SQL command.
+   Their value 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.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+  <variablelist>
+   <varlistentry>
+    <term><literal>IMMUTABLE</literal></term>
+    <listitem>
+     <para>
+      The assigned value of the variable cannot be changed. The value can be assigned
+      by command <command>LET</command> only first time (when session variable has not
+      defined default expression), or never (when session variable has defined not null
+      default expression).
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>IF NOT EXISTS</literal></term>
+    <listitem>
+     <para>
+      Do not throw an error if the name already exists. A notice is issued in this case.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">name</replaceable></term>
+    <listitem>
+     <para>
+      The name, optionally schema-qualified, of the variable.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">data_type</replaceable></term>
+    <listitem>
+     <para>
+      The name, optionally schema-qualified, of the data type of the variable.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>COLLATE <replaceable>collation</replaceable></literal></term>
+    <listitem>
+     <para>
+      The <literal>COLLATE</literal> clause assigns a collation to
+      the variable (which must be of a collatable data type).
+      If not specified, the data type's default collation is used.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>NOT NULL</literal></term>
+    <listitem>
+     <para>
+      The <literal>NOT NULL</literal> clause forbids setting the variable to
+      a null value. A variable created as NOT NULL and without an explicitly
+      declared default value cannot be read until it is initialized by a LET
+      command. This requires the user to explicitly initialize the variable
+      content before reading it.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>DEFAULT <replaceable>default_expr</replaceable></literal></term>
+    <listitem>
+     <para>
+      The <literal>DEFAULT</literal> clause can be used to assign a default value to
+      a session variable.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>ON COMMIT DROP</literal>, <literal>ON TRANSACTION END RESET</literal></term>
+    <listitem>
+     <para>
+      The <literal>ON COMMIT DROP</literal> clause specifies the behaviour
+      of a temporary session variable at transaction commit. With this clause, the
+      variable is dropped at commit time. The clause is only allowed
+      for temporary variables. The <literal>ON TRANSACTION END RESET</literal>
+      clause causes the variable to be reset to its default value when
+      the transaction is committed or rolled back.
+     </para>
+    </listitem>
+   </varlistentry>
+
+  </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Notes</title>
+
+  <para>
+   Use the <command>DROP VARIABLE</command> command to remove a variable.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   Create an integer variable <literal>var1</literal>:
+<programlisting>
+CREATE VARIABLE var1 AS date;
+LET var1 = current_date;
+SELECT var1;
+</programlisting>
+  </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   The <command>CREATE VARIABLE</command> command is a PostgreSQL extension.
+   <!-- The choice of wording here seems to be left to personal preference... -->
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-altervariable"/></member>
+   <member><xref linkend="sql-dropvariable"/></member>
+   <member><xref linkend="sql-let"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/ref/discard.sgml b/doc/src/sgml/ref/discard.sgml
index bf44c523ca..48d96af187 100644
--- a/doc/src/sgml/ref/discard.sgml
+++ b/doc/src/sgml/ref/discard.sgml
@@ -21,7 +21,7 @@ PostgreSQL documentation
 
  <refsynopsisdiv>
 <synopsis>

+   <varlistentry>
+      Resets the value of all session variables. If a variable
index 0000000000..5cced4ea90
+  <primary>session variable</primary>
+  <refpurpose>remove a session variable</refpurpose>
+   <command>DROP VARIABLE</command> removes a session variable.
+      The name, optionally schema-qualified, of a session variable.
+   To remove the session variable <literal>var1</literal>:
index a897712de2..2bf8eb9d54 100644

+    <varlistentry>
+     <term><literal>READ</literal></term>
+     <listitem>
+      <para>
+       Allows reading of a schema variable.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>WRITE</literal></term>
+     <listitem>
+      <para>
+       Allows setting the value of a schema variable.
+      </para>
+     </listitem>
+    </varlistentry>
+
     <varlistentry>
      <term><literal>ALL PRIVILEGES</literal></term>
      <listitem>
diff --git a/doc/src/sgml/ref/let.sgml b/doc/src/sgml/ref/let.sgml
new file mode 100644
index 0000000000..fcbfefb4f6
--- /dev/null
+++ b/doc/src/sgml/ref/let.sgml
@@ -0,0 +1,107 @@
+<!--
+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>
+LET <replaceable class="parameter">session_variable</replaceable> = DEFAULT
+</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. The result is cast to the data type of the session variable.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>DEFAULT</literal></term>
+    <listitem>
+     <para>
+      Reset the session variable to its default value, if that is defined.
+      If no explicit default value has been assigned, the session variable
+      is set to NULL.
+     </para>
+    </listitem>
+   </varlistentry>
+  </variablelist>
+
+  <para>
+   Example:
+<programlisting>
+CREATE VARIABLE myvar AS integer;
+LET myvar = 10;
+LET myvar = (SELECT sum(val) FROM tab);
+LET myvar = DEFAULT;
+</programlisting>
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   <literal>LET</literal> extends the syntax defined in the SQL
+   standard. The <literal>SET</literal> command from the SQL standard
+   is used for different purposes in PostgreSQL.
+  </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/ref/pg_restore.sgml b/doc/src/sgml/ref/pg_restore.sgml
index 93ea937ac8..0d86239eb4 100644
--- a/doc/src/sgml/ref/pg_restore.sgml
+++ b/doc/src/sgml/ref/pg_restore.sgml
@@ -106,6 +106,17 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
 
diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml
index da421ff24e..f9e42443b6 100644
--- a/doc/src/sgml/reference.sgml
+++ b/doc/src/sgml/reference.sgml
@@ -75,6 +75,7 @@
    &alterType;
    &alterUser;
    &alterUserMapping;
+   &alterVariable;
    &alterView;
    &analyze;
    &begin;
@@ -127,6 +128,7 @@
    &createType;
    &createUser;
    &createUserMapping;
+   &createVariable;
    &createView;
    &deallocate;
    &declare;
@@ -175,6 +177,7 @@
    &dropType;
    &dropUser;
    &dropUserMapping;
+   &dropVariable;
    &dropView;
    &end;
    &execute;
@@ -183,6 +186,7 @@
    &grant;
    &importForeignSchema;
    &insert;
+   &let;
    &listen;
    &load;
    &lock;
diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c
index e7b0bc804d..4cde2dd609 100644
--- a/src/backend/access/transam/xact.c
+++ b/src/backend/access/transam/xact.c
@@ -35,6 +35,7 @@
 #include "catalog/pg_enum.h"
 #include "catalog/storage.h"
 #include "commands/async.h"
+#include "commands/session_variable.h"
 #include "commands/tablecmds.h"
 #include "commands/trigger.h"
 #include "common/pg_prng.h"
@@ -2197,6 +2198,9 @@ CommitTransaction(void)
 	 */
 	smgrDoPendingSyncs(true, is_parallel_worker);
 
+	/* Let ON COMMIT DROP or ON TRANSACTION END */
+	AtPreEOXact_SessionVariable_on_xact_actions(true);
+
 	/* close large objects before lower-level cleanup */
 	AtEOXact_LargeObject(true);
 
@@ -2773,6 +2777,9 @@ AbortTransaction(void)
 	AtAbort_Portals();
 	smgrDoPendingSyncs(false, is_parallel_worker);
 	AtEOXact_LargeObject(false);
+
+	/* 'false' means it's abort */
+	AtPreEOXact_SessionVariable_on_xact_actions(false);
 	AtAbort_Notify();
 	AtEOXact_RelationMap(false, is_parallel_worker);
 	AtAbort_Twophase();
@@ -4962,6 +4969,8 @@ CommitSubTransaction(void)
 	AtEOSubXact_SPI(true, s->subTransactionId);
 	AtEOSubXact_on_commit_actions(true, s->subTransactionId,
 								  s->parent->subTransactionId);
+	AtEOSubXact_SessionVariable_on_xact_actions(true, s->subTransactionId,
+												s->parent->subTransactionId);
 	AtEOSubXact_Namespace(true, s->subTransactionId,
 						  s->parent->subTransactionId);
 	AtEOSubXact_Files(true, s->subTransactionId,
@@ -5126,6 +5135,8 @@ AbortSubTransaction(void)
 		AtEOSubXact_SPI(false, s->subTransactionId);
 		AtEOSubXact_on_commit_actions(false, s->subTransactionId,
 									  s->parent->subTransactionId);
+		AtEOSubXact_SessionVariable_on_xact_actions(false, s->subTransactionId,
+													s->parent->subTransactionId);
 		AtEOSubXact_Namespace(false, s->subTransactionId,
 							  s->parent->subTransactionId);
 		AtEOSubXact_Files(false, s->subTransactionId,
diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile
index 4e6efda97f..5c402a5ed8 100644
--- a/src/backend/catalog/Makefile
+++ b/src/backend/catalog/Makefile
@@ -43,6 +43,7 @@ OBJS = \
 	pg_shdepend.o \
 	pg_subscription.o \
 	pg_type.o \
+	pg_variable.o \
 	storage.o \
 	toasting.o

 
index ce0a4ff14e..d3eb006023 100644
 
+			errormsg = gettext_noop("invalid privilege type %s for session variable");
+			errormsg = gettext_noop("invalid privilege type %s for session variable");
 
+		case OBJECT_VARIABLE:
 
+static void
+			elog(ERROR, "cache lookup failed for session variable %u", varId);
 
 static AclMode
+						msg = gettext_noop("permission denied for session variable %s");
+						msg = gettext_noop("must be owner of session variable %s");
 
+/*
 
+/*
 
+/*
+ * Ownership check for a session variable (specified by OID).
+ */
+bool
+				 errmsg("session variable with OID %u does not exist", db_oid)));
 
+		case OBJECT_VARIABLE:
index fe9c714257..baf79d66f4 100644
+#include "commands/session_variable.h"

@@ -1501,6 +1505,10 @@ doDeletion(const ObjectAddress *object, int flags)
 			DropObjectById(object);
 			break;
 
+		case OCLASS_VARIABLE:
+			RemoveSessionVariable(object->objectId);
+			break;
+
 			/*
 			 * These global object types are not supported here.
 			 */
@@ -1879,6 +1887,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);
@@ -2879,6 +2892,9 @@ getObjectClass(const ObjectAddress *object)

 	/* shouldn't get here */
index 4de8400fd0..5df9459f00 100644
@@ -763,6 +764,69 @@ RelationIsVisible(Oid relid)
 	return visible;
 }
 
+/*
+ * VariableIsVisible
+		elog(ERROR, "cache lookup failed for session variable %u", varid);
 
 /*
  * TypenameGetTypid
@@ -2842,6 +2906,291 @@ TSConfigIsVisible(Oid cfgid)
 	return visible;
 }
 
+/*
+ * Returns oid of session variable specified by possibly qualified
+ * identifier.
+ *
+ * If not found, returns InvalidOid if missing_ok, else throws error.
+ */
+Oid
+LookupVariable(const char *nspname, const char *varname, bool missing_ok)
+{
+	Oid			namespaceId;
+	Oid			varoid = InvalidOid;
+	ListCell   *l;
+
+	if (nspname)
+	{
+		namespaceId = LookupExplicitNamespace(nspname, missing_ok);
+
+		/*
+		 * When nspname is not known namespace, then nspname.varname
+		 * cannot to identify any session variable.
+		 */
+		if (!OidIsValid(namespaceId))
+			return InvalidOid;
+
+		varoid = GetSysCacheOid2(VARIABLENAMENSP, Anum_pg_variable_oid,
+								 PointerGetDatum(varname),
+								 ObjectIdGetDatum(namespaceId));
+	}
+	else
+	{
+		/* Iterate over schemas in search_path */
+		recomputeNamespacePath();
+
+		foreach(l, activeSearchPath)
+		{
+			namespaceId = lfirst_oid(l);
+
+			varoid = GetSysCacheOid2(VARIABLENAMENSP, Anum_pg_variable_oid,
+									 PointerGetDatum(varname),
+									 ObjectIdGetDatum(namespaceId));
+
+			if (OidIsValid(varoid))
+				break;
+		}
+	}
+
+	if (!OidIsValid(varoid) && !missing_ok)
+	{
+		if (nspname)
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("session variable \"%s.%s\" does not exist",
+							nspname, varname)));
+		else
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("session variable \"%s\" does not exist",
+							varname)));
+	}
+
+	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.
+ *
+ * When there is not any valid combination, then we are sure, so the
+ * list of names cannot to identify any 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 ambigonuous. In this case this routine returns InvalidOid, and
+ * sets the output parameter "not_unique" to true. This parameter is
+ * used for more meaningfull error message.
+ */
+Oid
+IdentifyVariable(List *names, char **attrname, bool *not_unique)
+{
+	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			varoid_without_attr = InvalidOid;
+	Oid			varoid_with_attr = InvalidOid;
+
+	*not_unique = false;
+
+	switch (list_length(names))
+	{
+		case 1:
+			field1 = linitial(names);
+
+			Assert(IsA(field1, String));
+
+			return LookupVariable(NULL, strVal(field1), true);
+
+		case 2:
+			field1 = linitial(names);
+			field2 = lsecond(names);
+
+			Assert(IsA(field1, String));
+			a = strVal(field1);
+
+			if (IsA(field2, String))
+			{
+				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. Second node can be star. In
+				 * this case, the only allowed possibility is "variable"."*".
+				 */
+				varoid_without_attr = LookupVariable(a, b, true);
+				varoid_with_attr = LookupVariable(NULL, a, true);
+			}
+			else
+			{
+				Assert(IsA(field2, A_Star));
+
+				/*
+				 * Session variables doesn't support unboxing by star syntax. But
+				 * this syntax have to be calculated here, because can come from
+				 * non session variables related expressions.
+				 */
+				return InvalidOid;
+			}
+
+			if (OidIsValid(varoid_without_attr) && OidIsValid(varoid_with_attr))
+			{
+				*not_unique = true;
+				return InvalidOid;
+			}
+			else if (OidIsValid(varoid_without_attr))
+			{
+				*attrname = NULL;
+				return varoid_without_attr;
+			}
+			else
+			{
+				*attrname = b;
+				return varoid_with_attr;
+			}
+			break;
+
+		case 3:
+			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 returns
+				 * InvalidOid with not_unique flag, when both interpretations are
+				 * possible. When third node is star, the only possible
+				 * interpretation is "schema"."variable"."*".
+				 */
+				varoid_without_attr = LookupVariable(b, c, true);
+				varoid_with_attr = LookupVariable(a, b, true);
+			}
+			else
+			{
+				Assert(IsA(field3, A_Star));
+				return InvalidOid;
+			}
+
+			if (OidIsValid(varoid_without_attr) && OidIsValid(varoid_with_attr))
+			{
+				*not_unique = true;
+				return InvalidOid;
+			}
+			else if (OidIsValid(varoid_without_attr))
+			{
+				*attrname = NULL;
+
+				/*
+				 * In this case, "a" is used as catalog name - check it.
+				 */
+				if (strcmp(a, get_database_name(MyDatabaseId)) != 0)
+					ereport(ERROR,
+							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+							 errmsg("cross-database references are not implemented: %s",
+									NameListToString(names))));
+
+				return varoid_without_attr;
+			}
+			else
+			{
+				*attrname = c;
+				return varoid_with_attr;
+			}
+			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)
+				ereport(ERROR,
+						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+						 errmsg("cross-database references are not implemented: %s",
+								NameListToString(names))));
+
+			if (IsA(field4, String))
+			{
+				d = strVal(field4);
+			}
+			else
+			{
+				Assert(IsA(field4, A_Star));
+				return InvalidOid;
+			}
+
+			*attrname = d;
+			return LookupVariable(b, c, true);
+
+		default:
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("improper qualified name (too many dotted names): %s",
+							NameListToString(names))));
+			break;
+	}
+}
 
 /*
  * DeconstructQualifiedName
@@ -4657,3 +5006,14 @@ pg_is_other_temp_schema(PG_FUNCTION_ARGS)
 
index 2bae3fbb17..a8cbadd049 100644
+		"session variable",
 
 /*
@@ -845,6 +860,10 @@ static const struct object_type_map
 	/* OCLASS_STATISTIC_EXT */
 	{
 		"statistics object", OBJECT_STATISTIC_EXT
+	},
+	/* OCLASS_VARIABLE */
+	{
+		"session variable", OBJECT_VARIABLE
 	}
 };

 	/*
 
+/*
+ * Find the ObjectAddress for a session variable
+ */
+static ObjectAddress
 
+		case OCLASS_VARIABLE:
+			{
+				char	   *nspname;
+				HeapTuple	tup;
+				Form_pg_variable varform;
+
+				tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(object->objectId));
+				if (!HeapTupleIsValid(tup))
+					elog(ERROR, "cache lookup failed for session variable %u",
+						 object->objectId);
+
+				varform = (Form_pg_variable) GETSTRUCT(tup);
+
+				if (VariableIsVisible(object->objectId))
+					nspname = NULL;
+				else
+					nspname = get_namespace_name(varform->varnamespace);
+
+				appendStringInfo(&buffer, _("session variable %s"),
+								 quote_qualified_identifier(nspname,
+															NameStr(varform->varname)));
+
+				ReleaseSysCache(tup);
+				break;
+			}
+
 		case OCLASS_TSPARSER:
 			{
 				HeapTuple	tup;
@@ -3868,6 +3945,16 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok)
 										 _("default privileges on new schemas belonging to role %s"),
 										 rolename);
 						break;
+					case DEFACLOBJ_VARIABLE:
+						if (nspname)
+							appendStringInfo(&buffer,
+											 _("default privileges on new session variables belonging to role %s in schema %s"),
+											 rolename, nspname);
+						else
+							appendStringInfo(&buffer,
+											 _("default privileges on new session variables belonging to role %s"),
+											 rolename);
+						break;
 					default:
 						/* shouldn't get here */
 						if (nspname)
@@ -4610,6 +4697,10 @@ getObjectTypeDescription(const ObjectAddress *object, bool missing_ok)
 			appendStringInfoString(&buffer, "transform");
 			break;
 
+		case OCLASS_VARIABLE:
+			appendStringInfoString(&buffer, "session variable");
+			break;
+
 			/*
 			 * There's intentionally no default: case here; we want the
 			 * compiler to warn if a new OCLASS hasn't been handled above.
@@ -5703,6 +5794,10 @@ getObjectIdentityParts(const ObjectAddress *object,
 						appendStringInfoString(&buffer,
 											   " on schemas");
 						break;
+					case DEFACLOBJ_VARIABLE:
+						appendStringInfoString(&buffer,
+											   " on session variables");
+						break;
 				}
 
 				if (objname)
 
+		case OCLASS_VARIABLE:
+			{
+				char	   *schema;
+				char	   *varname;
+				HeapTuple	tup;
+					elog(ERROR, "cache lookup failed for session variable %u",
index c20b1fbb96..41b8aec090 100644
@@ -1593,6 +1594,7 @@ shdepReassignOwned(List *roleids, Oid newrole)
index 0000000000..f1216c1a3c
@@ -0,0 +1,387 @@
+ *		session variables
+#include "commands/session_variable.h"
+ * Returns name of session variable. When the variable is not in the path,
+ * the name is qualified.
+session_variable_get_name(Oid varid)
+get_session_variable_name(Oid varid)
+		elog(ERROR, "cache lookup failed for session variable %u", varid);
+ * Returns type, typmod of session variable
+get_session_variable_type_typmod_collid(Oid varid, Oid *typid, int32 *typmod, Oid *collid)
+		elog(ERROR, "cache lookup failed for session variable %u", varid);
+ * Fetch all fields of session variable from the syscache.
+		elog(ERROR, "cache lookup failed for session variable %u", varid);
+	 * Sometimes we don't need to deserialize defexpr, but we need info about
+	 * the existence of defexpr.
+	/* varacl will be determined later */
+					 errmsg("session variable \"%s\" already exists, skipping",
+					 errmsg("session variable \"%s\" already exists",
+	 * When this new field in catalog is temporal, we need to create
+	 * new xact action to ensure delete from catalog.
+	if (eoxaction == VARIABLE_EOX_DROP)
+		RegisterOnCommitDropSessionVariable(myself.objectId);
index e8504f0ae4..c723e229f9 100644
+	sessionvariable.o \
index 40044070cf..a5aba05314 100644
+			msgfmt = gettext_noop("session variable \"%s\" already exists in schema \"%s\"");
 
index 57d3d7dd9b..fb14002393 100644
+#include "commands/session_variable.h"

+			ResetSessionVariables();
+	ResetSessionVariables();
index 4e545adf95..4a0d7c7814 100644
+			msg = gettext_noop("session variable \"%s\" does not exist, skipping");

 			/*
index 09f5253abb..3a92159c28 100644
+			appendStringInfoString(es->str, "SET SESSION VARIABLE\n");
+			ExplainDummyGroup("Set Session Variable", NULL, es);
 
 			/*
diff --git a/src/backend/commands/sessionvariable.c b/src/backend/commands/sessionvariable.c
new file mode 100644
index 0000000000..8d0eb9e91c
--- /dev/null
+++ b/src/backend/commands/sessionvariable.c
@@ -0,0 +1,1085 @@
+/*-------------------------------------------------------------------------
+ *
+ * sessionvariable.c
+ *	  session variable creation/manipulation commands
+ *
+ * Portions Copyright (c) 1996-2021, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/commands/sessionvariable.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+#include "miscadmin.h"
+#include "access/heapam.h"
+#include "access/htup_details.h"
+#include "access/xact.h"
+#include "catalog/dependency.h"
+#include "catalog/indexing.h"
+#include "catalog/namespace.h"
+#include "catalog/pg_variable.h"
+#include "commands/session_variable.h"
+#include "executor/executor.h"
+#include "executor/svariableReceiver.h"
+#include "optimizer/optimizer.h"
+#include "parser/parse_coerce.h"
+#include "parser/parse_collate.h"
+#include "parser/parse_expr.h"
+#include "parser/parse_type.h"
+#include "rewrite/rewriteHandler.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"
+
+/*
+ * An values of session variables are stored in local memory
+ * in sessionvars hash table. This local memory have to be
+ * cleaned, when a) session variable is dropped by current or
+ * by other session, b) when user enforce it by using clause
+ * ON TRANSACTION END RESET. The life cycle of temporal session
+ * variable can be limmited by using clause ON COMMIT DROP.
+ *
+ * Although session variables are not transactional, we don't
+ * want (and we cannot) to run cleaning immediately (when we
+ * got sinval message). The value of session variables can
+ * be still used or the operation that emits cleaning can be
+ * reverted. Unfortunatelly, this check can be done only in
+ * when transaction is committed (the check against system
+ * catalog requires transaction state).
+ *
+ * This is reason why cleaning memory (session variable reset)
+ * is postponed to end of transaction, and we need to hold
+ * some actions lists. We have to hold two separate action
+ * lists - one for dropping from system catalog, and second
+ * for resetting. It's necessary, because dropping session
+ * variable enforces session variable reset. The drop operation
+ * can be executed when we iterate over action list, and in
+ * this moment we would not to modify same action list.
+ *
+ * We want to support possibility to reset session variable at
+ * the end of transaction. This ensure initial state of session
+ * variable at the begin of each transaction. The reset is
+ * implemented like removing variable from sessionvars hash table.
+ * This enforce full initialization in next usage.
+ * Attention - this is not same as drop session variable.
+ *
+ * Another functionality is dropping temporary session variable
+ * with the option ON COMMIT DROP.
+ */
+typedef enum SVariableXActAction
+{
+	ON_COMMIT_DROP,		/* used for ON COMMIT DROP */
+	ON_COMMIT_RESET,	/* used for DROP VARIABLE */
+	RESET,				/* used for ON TRANSACTION END RESET */
+	RECHECK				/* recheck if session variable is living */
+} SVariableXActAction;
+
+typedef struct SVariableXActActionItem
+{
+	Oid			varid;			/* varid of session variable */
+	SVariableXActAction action;	/* reset or drop */
+
+	/*
+	 * If this entry was created during the current transaction,
+	 * creating_subid is the ID of the creating subxact; if created in a prior
+	 * transaction, creating_subid is zero.  If deleted during the current
+	 * transaction, deleting_subid is the ID of the deleting subxact; if no
+	 * deletion request is pending, deleting_subid is zero.
+	 */
+	SubTransactionId creating_subid;
+	SubTransactionId deleting_subid;
+}  SVariableXActActionItem;
+
+static List *xact_drop_actions = NIL;
+static List *xact_reset_actions = NIL;
+
+typedef struct SVariableData
+{
+	Oid			varid;			/* pg_variable OID of this sequence (hash key) */
+	Oid			typid;			/* OID of the data type */
+	int16		typlen;
+	bool		typbyval;
+	bool		isnull;
+	bool		freeval;
+	Datum		value;
+
+	bool		is_rowtype;		/* true when variable is composite */
+	bool		is_not_null;	/* don't allow null values */
+	bool		is_immutable;	/* true when variable is immutable */
+	bool		has_defexpr;	/* true when there are default value */
+
+	bool		is_valid;		/* true when variable was successfuly
+								 * initialized */
+
+	uint32		hashvalue;
+}			SVariableData;
+
+typedef SVariableData * SVariable;
+
+static HTAB *sessionvars = NULL;	/* hash table for session variables */
+static MemoryContext SVariableMemoryContext = NULL;
+
+static bool first_time = true;
+
+static bool sync_sessionvars_all = false;
+
+static void register_session_variable_xact_action(Oid varid, SVariableXActAction action);
+static void delete_session_variable_xact_action(Oid varid, SVariableXActAction action);
+
+/*
+ * Releases stored data from session variable. The hash entry
+ * stay without change.
+ */
+static void
+free_session_variable_value(SVariable svar)
+{
+	if (svar->freeval)
+		pfree(DatumGetPointer(svar->value));
+
+	/* Clean current value */
+	svar->value = (Datum) 0;
+	svar->isnull = true;
+	svar->freeval = false;
+
+	/*
+	 * We can mark this session variable as valid when
+	 * it has not default expression, and when null is
+	 * allowed. When it has defexpr, then the content
+	 * will be valid after an assignment or defexp evaluation.
+	 */
+	svar->is_valid = !svar->has_defexpr && !svar->is_not_null;
+}
+
+/*
+ * Release the variable defined by varid from sessionvars
+ * hashtab.
+ */
+static void
+free_session_variable(SVariable svar)
+{
+	free_session_variable_value(svar);
+
+	if (hash_search(sessionvars,
+					(void *) &svar->varid,
+					HASH_REMOVE,
+					NULL) == NULL)
+		elog(DEBUG1, "hash table corrupted");
+}
+
+/*
+ * Release the variable defined by varid from sessionvars
+ * hashtab.
+ */
+static void
+free_session_variable_varid(Oid varid)
+{
+	SVariable svar;
+	bool		found;
+
+	if (sessionvars == NULL)
+		return;
+
+	svar = (SVariable) hash_search(sessionvars, &varid,
+										HASH_FIND, &found);
+	if (found)
+		free_session_variable(svar);
+}
+
+/*
+ * Assign sinval mark to session variable. This mark probably
+ * signalized, so the session variable was dropped. But this
+ * should be rechecked later against system catalog.
+ */
+static void
+pg_variable_cache_callback(Datum arg, int cacheid, uint32 hashvalue)
+{
+	/*
+	 * We can have not sessionvars in initialized state, and
+	 * we can get this message. This is possible after execution of
+	 * DISCARD VARIABLES command.
+	 */
+	if (!sessionvars)
+		return;
+
+	/*
+	 * When we know so all session variables should be synchronized,
+	 * then is useless to continue;
+	 */
+	if (sync_sessionvars_all)
+		return;
+
+	/*
+	 * Because we cannot to decode varid from hashValue, we should
+	 * to iterate over all currently used session variables to find
+	 * session variable with same hashValue. On second hand, this can
+	 * save us some CPU later, because we don't need to check any used
+	 * session variable (by current session) against system catalog.
+	 */
+	if (hashvalue != 0)
+	{
+		HASH_SEQ_STATUS status;
+		SVariable svar;
+
+		hash_seq_init(&status, sessionvars);
+
+		while ((svar = (SVariable) hash_seq_search(&status)) != NULL)
+		{
+			if (svar->hashvalue == hashvalue)
+				register_session_variable_xact_action(svar->varid, RECHECK);
+
+			/*
+			 * although it there is low probability, we have to iterate
+			 * over all actively used session variables, because hashvalue
+			 * is not unique identifier.
+			 */
+		}
+	}
+	else
+		sync_sessionvars_all = true;
+}
+
+/*
+ * When we need to recheck all session variables, then
+ * the most effective method is seq scan over hash tab.
+ */
+static void
+sync_sessionvars_xact_callback(XactEvent event, void *arg)
+{
+	HASH_SEQ_STATUS status;
+	SVariable svar;
+
+	if (!sync_sessionvars_all)
+		return;
+
+	/*
+	 * This callback can be called when database is not in
+	 * transaction state - XACT_EVENT_ABORT, XACT_EVENT_COMMIT,
+	 * but the checks against system catalog can be done only
+	 * in transaction state.
+	 */
+	if (!IsTransactionState())
+		return;
+
+	Assert(sessionvars);
+
+	hash_seq_init(&status, sessionvars);
+
+	while ((svar = (SVariable) hash_seq_search(&status)) != NULL)
+	{
+		HeapTuple	tp;
+
+		tp = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(svar->varid));
+
+		if (HeapTupleIsValid(tp))
+			ReleaseSysCache(tp);
+		else
+			free_session_variable(svar);
+	}
+
+	sync_sessionvars_all = false;
+}
+
+/*
+ * Create the hash table for storing session variables
+ */
+static void
+create_sessionvars_hashtable(void)
+{
+	HASHCTL		ctl;
+
+	/* set callbacks */
+	if (first_time)
+	{
+		/* Read sinval messages */
+		CacheRegisterSyscacheCallback(VARIABLEOID,
+									  pg_variable_cache_callback,
+									  (Datum) 0);
+
+		/* at transaction end recheck sinvalidated variables */
+		RegisterXactCallback(sync_sessionvars_xact_callback, NULL);
+
+		first_time = false;
+	}
+
+	/* needs its own long lived memory context */
+	if (SVariableMemoryContext == NULL)
+	{
+		SVariableMemoryContext =
+			AllocSetContextCreate(TopMemoryContext,
+								  "session variables",
+								  ALLOCSET_START_SMALL_SIZES);
+	}
+
+	memset(&ctl, 0, sizeof(ctl));
+	ctl.keysize = sizeof(Oid);
+	ctl.entrysize = sizeof(SVariableData);
+	ctl.hcxt = SVariableMemoryContext;
+
+	sessionvars = hash_create("Session variables", 64, &ctl,
+								   HASH_ELEM | HASH_BLOBS | HASH_CONTEXT);
+}
+
+/*
+ * Assign some content to the session variable. It's copied to
+ * SVariableMemoryContext if it is necessary.
+ *
+ * init_mode is true, when the value of session variable is initialized
+ * by default expression or by null. Only in this moment we can allow to
+ * modify immutable variables with default expression.
+ */
+static void
+set_session_variable(SVariable svar, Datum value,
+					 bool isnull, Oid typid,
+					 bool init_mode)
+{
+	MemoryContext oldcxt;
+	Datum		newval = value;
+
+	/* Don't allow assignment of null to NOT NULL variable */
+	if (isnull && svar->is_not_null)
+		ereport(ERROR,
+				(errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
+				 errmsg("null value is not allowed for NOT NULL session variable \"%s\"",
+						session_variable_get_name(svar->varid))));
+
+	if (!isnull && svar->typid != typid)
+		ereport(ERROR,
+				(errcode(ERRCODE_DATATYPE_MISMATCH),
+				 errmsg("type \"%s\" of assigned value is different than type \"%s\" of session variable \"%s\"",
+						format_type_be(typid),
+						format_type_be(svar->typid),
+						session_variable_get_name(svar->varid))));
+
+	/*
+	 * Don't allow updating of immutable session variable that has assigned
+	 * not null value or has default expression (and then the value should be
+	 * result of default expression always). Don't do this check, when variable
+	 * is initialized.
+	 */
+	if (!init_mode &&
+		(svar->is_immutable && (!svar->isnull || svar->has_defexpr)))
+		ereport(ERROR,
+				(errcode(ERRCODE_ERROR_IN_ASSIGNMENT),
+				 errmsg("session variable \"%s\" is declared IMMUTABLE",
+						session_variable_get_name(svar->varid))));
+
+	/* copy value to session persistent context */
+	oldcxt = MemoryContextSwitchTo(SVariableMemoryContext);
+	if (!isnull)
+		newval = datumCopy(value, svar->typbyval, svar->typlen);
+	MemoryContextSwitchTo(oldcxt);
+
+	free_session_variable_value(svar);
+
+	svar->value = newval;
+	svar->isnull = isnull;
+	svar->freeval = newval != value;
+	svar->is_valid = true;
+}
+
+/*
+ * Initialize svar from var
+ * svar - SVariable - holds data
+ * var  - Variable - holds metadata
+ */
+static void
+init_session_variable(SVariable svar, Variable *var)
+{
+	Assert(OidIsValid(var->oid));
+
+	svar->varid = var->oid;
+	svar->typid = var->typid;
+
+	get_typlenbyval(var->typid,
+					&svar->typlen,
+					&svar->typbyval);
+
+	svar->isnull = true;
+	svar->freeval = false;
+	svar->value = (Datum) 0;
+
+	svar->is_rowtype = type_is_rowtype(var->typid);
+	svar->is_not_null = var->is_not_null;
+	svar->is_immutable = var->is_immutable;
+	svar->has_defexpr = var->has_defexpr;
+
+	svar->hashvalue = GetSysCacheHashValue1(VARIABLEOID,
+											ObjectIdGetDatum(var->oid));
+
+	/* the value of variable is not known yet */
+	svar->is_valid = false;
+
+	if (var->eoxaction == VARIABLE_EOX_RESET ||
+			var->eoxaction == VARIABLE_EOX_DROP)
+		register_session_variable_xact_action(var->oid, RESET);
+}
+
+/*
+ * Try to search value in hash table. If it doesn't
+ * exist, then insert it (and calculate defexpr if it exists).
+ */
+static SVariable
+prepare_variable_for_reading(Oid varid)
+{
+	SVariable svar;
+	Variable	var;
+	bool		found;
+
+	var.oid = InvalidOid;
+
+	if (!sessionvars)
+		create_sessionvars_hashtable();
+
+	svar = (SVariable) hash_search(sessionvars, &varid,
+										HASH_ENTER, &found);
+
+	/* Return content if it is available and valid */
+	if (found && svar->is_valid)
+		return svar;
+
+	/* We need to load defexpr. */
+	initVariable(&var, varid, false, false);
+
+	if (!found)
+		init_session_variable(svar, &var);
+
+	/* Raise an error when we cannot initialize variable correctly */
+	if (var.is_not_null && !var.defexpr)
+		ereport(ERROR,
+				(errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
+				 errmsg("null value is not allowed for NOT NULL session variable \"%s\"",
+						session_variable_get_name(varid)),
+				 errdetail("The session variable was not initialized yet.")));
+
+	if (svar->has_defexpr)
+	{
+		Datum		value = (Datum) 0;
+		bool		isnull;
+		EState	   *estate = NULL;
+		Expr	   *defexpr;
+		ExprState  *defexprs;
+		MemoryContext oldcxt;
+
+		/* Prepare default expr */
+		estate = CreateExecutorState();
+
+		oldcxt = MemoryContextSwitchTo(estate->es_query_cxt);
+
+		defexpr = expression_planner((Expr *) var.defexpr);
+		defexprs = ExecInitExpr(defexpr, NULL);
+		value = ExecEvalExprSwitchContext(defexprs,
+										  GetPerTupleExprContext(estate),
+										  &isnull);
+
+
+		/* Store result before releasing Executor memory */
+		set_session_variable(svar, value, isnull, svar->typid, true);
+
+		MemoryContextSwitchTo(oldcxt);
+
+		FreeExecutorState(estate);
+	}
+	else
+		set_session_variable(svar, (Datum) 0, true, svar->typid, true);
+
+	return svar;
+}
+
+/*
+ * Write value to variable. We expect secured access in this moment.
+ * We try not to break the previous value, if something is wrong.
+ */
+void
+SetSessionVariable(Oid varid, Datum value, bool isNull, Oid typid)
+{
+	SVariable svar;
+	bool		found;
+
+	if (!sessionvars)
+		create_sessionvars_hashtable();
+
+	svar = (SVariable) hash_search(sessionvars, &varid,
+										HASH_ENTER, &found);
+
+	/* Initialize svar when not initialized or when stored value is null */
+	if (!found)
+	{
+		Variable	var;
+
+		/* don't need defexpr and acl here */
+		initVariable(&var, varid, false, true);
+		init_session_variable(svar, &var);
+	}
+
+	set_session_variable(svar, value, isNull, typid, false);
+}
+
+/*
+ * Write value to variable with security check.
+ * We try not to break the previous value, if something is wrong.
+ */
+void
+SetSessionVariableWithSecurityCheck(Oid varid, Datum value, bool isNull, Oid typid)
+{
+	AclResult	aclresult;
+
+	/*
+	 * Is possible to write to session variable?
+	 */
+	aclresult = pg_variable_aclcheck(varid, GetUserId(), ACL_WRITE);
+	if (aclresult != ACLCHECK_OK)
+		aclcheck_error(aclresult, OBJECT_VARIABLE, session_variable_get_name(varid));
+
+	SetSessionVariable(varid, value, isNull, typid);
+}
+
+/*
+ * Returns a copy of value of the session variable specified by varid
+ */
+Datum
+CopySessionVariable(Oid varid, bool *isNull, Oid *typid)
+{
+	SVariable svar;
+
+	svar = prepare_variable_for_reading(varid);
+	Assert(svar != NULL && svar->is_valid);
+
+	*isNull = svar->isnull;
+	*typid = svar->typid;
+
+	if (!svar->isnull)
+		return datumCopy(svar->value, svar->typbyval, svar->typlen);
+
+	return (Datum) 0;
+}
+
+/*
+ * Returns the value of the session variable specified by varid. Check correct
+ * result type. Optionally the result can be copied.
+ */
+Datum
+GetSessionVariable(Oid varid, bool *isNull, Oid expected_typid, bool copy)
+{
+	SVariable svar;
+	Datum		value;
+	bool		isnull;
+
+	svar = prepare_variable_for_reading(varid);
+	Assert(svar != NULL);
+
+	if (expected_typid != svar->typid)
+		elog(ERROR, "type of variable \"%s\" is different than expected",
+			 session_variable_get_name(varid));
+
+	value = svar->value;
+	isnull = svar->isnull;
+
+	*isNull = isnull;
+
+	if (!isnull && copy)
+		return datumCopy(value, svar->typbyval, svar->typlen);
+
+	return value;
+}
+
+
+/*
+ * Routines used for manipulation with session variables from
+ * SQL level
+ */
+
+/*
+ * Creates new variable - entry in pg_catalog.pg_variable table
+ *
+ * Used by CREATE VARIABLE command
+ */
+ObjectAddress
+DefineSessionVariable(ParseState *pstate, CreateSessionVarStmt *stmt)
+{
+	Oid			namespaceid;
+	AclResult	aclresult;
+	Oid			typid;
+	int32		typmod;
+	Oid			varowner = GetUserId();
+	Oid			collation;
+	Oid			typcollation;
+	ObjectAddress variable;
+
+	Node	   *cooked_default = NULL;
+
+	/*
+	 * Check consistency of arguments
+	 */
+	if (stmt->eoxaction == VARIABLE_EOX_DROP
+		&& stmt->variable->relpersistence != RELPERSISTENCE_TEMP)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+				 errmsg("ON COMMIT DROP can only be used on temporary variables")));
+
+	if (stmt->is_not_null && stmt->is_immutable && !stmt->defexpr)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+				 errmsg("IMMUTABLE NOT NULL variable requires default expression")));
+
+	namespaceid =
+		RangeVarGetAndCheckCreationNamespace(stmt->variable, NoLock, NULL);
+
+	typenameTypeIdAndMod(pstate, stmt->typeName, &typid, &typmod);
+	typcollation = get_typcollation(typid);
+
+	aclresult = pg_type_aclcheck(typid, GetUserId(), ACL_USAGE);
+	if (aclresult != ACLCHECK_OK)
+		aclcheck_error_type(aclresult, typid);
+
+	if (stmt->collClause)
+		collation = LookupCollation(pstate,
+									stmt->collClause->collname,
+									stmt->collClause->location);
+	else
+		collation = typcollation;;
+
+	/* Complain if COLLATE is applied to an uncollatable type */
+	if (OidIsValid(collation) && !OidIsValid(typcollation))
+		ereport(ERROR,
+				(errcode(ERRCODE_DATATYPE_MISMATCH),
+				 errmsg("collations are not supported by type %s",
+						format_type_be(typid)),
+				 parser_errposition(pstate, stmt->collClause->location)));
+
+	if (stmt->defexpr)
+	{
+		cooked_default = transformExpr(pstate, stmt->defexpr,
+									   EXPR_KIND_VARIABLE_DEFAULT);
+
+		cooked_default = coerce_to_specific_type(pstate,
+												 cooked_default, typid, "DEFAULT");
+		assign_expr_collations(pstate, cooked_default);
+	}
+
+	variable = VariableCreate(stmt->variable->relname,
+							  namespaceid,
+							  typid,
+							  typmod,
+							  varowner,
+							  collation,
+							  cooked_default,
+							  stmt->eoxaction,
+							  stmt->is_not_null,
+							  stmt->if_not_exists,
+							  stmt->is_immutable);
+
+	/*
+	 * We must bump the command counter to make the newly-created variable
+	 * tuple visible for any other operations.
+	 */
+	CommandCounterIncrement();
+
+	return variable;
+}
+
+/*
+ * Create new ON_COMMIT_DROP xact action. We have to drop
+ * ON COMMIT DROP variable, although this variable should not
+ * be used. So we need to register this action in CREATE VARIABLE
+ * time.
+ */
+void
+RegisterOnCommitDropSessionVariable(Oid varid)
+{
+	register_session_variable_xact_action(varid, ON_COMMIT_DROP);
+}
+
+/*
+ * Drop variable by OID. This routine doesn't try to remove
+ * the value of session variable immediately. It will be
+ * removed on transaction end in sync_sessionvars_xact_callback
+ * routine. This routine manipulate just with system catalog.
+ */
+void
+RemoveSessionVariable(Oid varid)
+{
+	Relation	rel;
+	HeapTuple	tup;
+
+	rel = table_open(VariableRelationId, RowExclusiveLock);
+
+	tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid));
+
+	if (!HeapTupleIsValid(tup))
+		elog(ERROR, "cache lookup failed for variable %u", varid);
+
+	CatalogTupleDelete(rel, &tup->t_self);
+
+	ReleaseSysCache(tup);
+
+	table_close(rel, RowExclusiveLock);
+
+	/*
+	 * we removed entry from sys catalog already, we should not to do
+	 * again at xact time,
+	 */
+	delete_session_variable_xact_action(varid, ON_COMMIT_DROP);
+
+	/*
+	 * and if this transaction or subtransaction will be committed,
+	 * we want to enforce variable cleaning. (we don't need to wait for
+	 * sinval message). The cleaning action for one session variable
+	 * can be repeated in the action list, and it doesn't do any problem
+	 * (so we don't need to ensure uniqueness).
+	 * We need separate action than RESET, because RESET
+	 * is executed on any transaction end, but we want to execute cleaning
+	 * only when current transaction will be commited.
+	 */
+	register_session_variable_xact_action(varid, ON_COMMIT_RESET);
+}
+
+/*
+ * Fast drop complete content of all session variables hash table.
+ * This is code for DISCARD VARIABLES command. This command
+ * cannot to run inside transaction, so we don't need to handle
+ * end of transaction actions.
+ */
+void
+ResetSessionVariables(void)
+{
+	/* Destroy hash table and reset related memory context */
+	if (sessionvars)
+	{
+		hash_destroy(sessionvars);
+		sessionvars = NULL;
+	}
+
+	/* Release memory allocated by session variables */
+	if (SVariableMemoryContext != NULL)
+		MemoryContextReset(SVariableMemoryContext);
+
+	/*
+	 * There are not any session variables, so trim
+	 * both xact action lists.
+	 */
+	list_free_deep(xact_drop_actions);
+	xact_drop_actions = NIL;
+
+	list_free_deep(xact_reset_actions);
+	xact_drop_actions = NIL;
+}
+
+/*
+ * Assign result of evaluated expression to session variable
+ */
+void
+ExecuteLetStmt(ParseState *pstate,
+			   LetStmt *stmt,
+			   ParamListInfo params,
+			   QueryEnvironment *queryEnv,
+			   QueryCompletion *qc)
+{
+	Query	   *query = castNode(Query, stmt->query);
+	List	   *rewritten;
+	DestReceiver *dest;
+	AclResult	aclresult;
+	PlannedStmt *plan;
+	QueryDesc  *queryDesc;
+	Oid			varid = query->resultVariable;
+
+	Assert(OidIsValid(varid));
+
+	/*
+	 * Is it allowed to write to session variable?
+	 */
+	aclresult = pg_variable_aclcheck(varid, GetUserId(), ACL_WRITE);
+	if (aclresult != ACLCHECK_OK)
+		aclcheck_error(aclresult, OBJECT_VARIABLE, session_variable_get_name(varid));
+
+	/* Create dest receiver for LET */
+	dest = CreateDestReceiver(DestVariable);
+	SetVariableDestReceiverParams(dest, varid);
+
+	/* run rewriter - can be used for replacement of DEFAULT node */
+	query = copyObject(query);
+
+	rewritten = QueryRewrite(query);
+
+	Assert(list_length(rewritten) == 1);
+
+	query = linitial_node(Query, rewritten);
+	Assert(query->commandType == CMD_SELECT);
+
+	/* plan the query */
+	plan = pg_plan_query(query, pstate->p_sourcetext,
+						 CURSOR_OPT_PARALLEL_OK, params);
+
+	/*
+	 * Use a snapshot with an updated command ID to ensure this query sees
+	 * results of any previously executed queries.  (This could only
+	 * matter if the planner executed an allegedly-stable function that
+	 * changed the database contents, but let's do it anyway to be
+	 * parallel to the EXPLAIN code path.)
+	 */
+	PushCopiedSnapshot(GetActiveSnapshot());
+	UpdateActiveSnapshotCommandId();
+
+	/* Create a QueryDesc, redirecting output to our tuple receiver */
+	queryDesc = CreateQueryDesc(plan, pstate->p_sourcetext,
+								GetActiveSnapshot(), InvalidSnapshot,
+								dest, params, queryEnv, 0);
+
+	/* call ExecutorStart to prepare the plan for execution */
+	ExecutorStart(queryDesc, 0);
+
+	/* run the plan to completion */
+	ExecutorRun(queryDesc, ForwardScanDirection, 2L, true);
+
+	/* save the rowcount if we're given a qc to fill */
+	if (qc)
+		SetQueryCompletion(qc, CMDTAG_LET, queryDesc->estate->es_processed);
+
+	/* and clean up */
+	ExecutorFinish(queryDesc);
+	ExecutorEnd(queryDesc);
+
+	FreeQueryDesc(queryDesc);
+
+	PopActiveSnapshot();
+}
+
+
+/*
+ * Implementation of drop or reset actions executed on session variables
+ * at transaction end time. We want to drop temporary session variables
+ * with clause ON COMMIT DROP, or we want to reset values of session variables
+ * with clause ON TRANSACTION END RESET or we want to clean (reset) memory
+ * allocated by values of dropped session variables.
+ */
+
+/*
+ * Register a session variable xact action.
+ */
+void
+register_session_variable_xact_action(Oid varid,
+									  SVariableXActAction action)
+{
+	SVariableXActActionItem *xact_ai;
+	MemoryContext oldcxt;
+
+	oldcxt = MemoryContextSwitchTo(CacheMemoryContext);
+
+	xact_ai = (SVariableXActActionItem *)
+							palloc(sizeof(SVariableXActActionItem));
+
+	xact_ai->varid = varid;
+	xact_ai->action = action;
+
+	xact_ai->creating_subid = GetCurrentSubTransactionId();
+	xact_ai->deleting_subid = InvalidSubTransactionId;
+
+	if (action == ON_COMMIT_DROP)
+		xact_drop_actions = lcons(xact_ai, xact_drop_actions);
+	else
+		xact_reset_actions = lcons(xact_ai, xact_reset_actions);
+
+	MemoryContextSwitchTo(oldcxt);
+}
+
+/*
+ * Remove variable from action list. In this moment,
+ * the action is just marked as deleted by setting
+ * deleting_subid.
+ */
+static void
+delete_session_variable_xact_action(Oid varid,
+									SVariableXActAction action)
+{
+	ListCell   *l;
+
+	Assert(action == ON_COMMIT_DROP);
+
+	foreach(l, xact_drop_actions)
+	{
+		SVariableXActActionItem *xact_ai =
+					(SVariableXActActionItem *) lfirst(l);
+
+		if (xact_ai->varid == varid && xact_ai->action == action)
+			xact_ai->deleting_subid = GetCurrentSubTransactionId();
+	}
+}
+
+/*
+ * Perform ON TRANSACTION END RESET or ON COMMIT DROP
+ * and COMMIT/ROLLBACK of transaction session variables.
+ */
+void
+AtPreEOXact_SessionVariable_on_xact_actions(bool isCommit)
+{
+	ListCell   *l;
+
+	foreach(l, xact_drop_actions)
+	{
+		SVariableXActActionItem *xact_ai =
+							(SVariableXActActionItem *) lfirst(l);
+
+		/* Iterate only over non dropped entries */
+		if (xact_ai->deleting_subid == InvalidSubTransactionId)
+		{
+			Assert(xact_ai->action == ON_COMMIT_DROP);
+
+			/*
+			 * ON COMMIT DROP is allowed only for temp session
+			 * variables. So we should explicitly delete only when
+			 * current transaction was committed. When it's rollback,
+			 * then session variable is removed automatically.
+			 */
+			if (isCommit)
+			{
+				ObjectAddress object;
+
+				object.classId = VariableRelationId;
+				object.objectId = xact_ai->varid;
+				object.objectSubId = 0;
+
+				/*
+				 * Since this is an automatic drop, rather than one
+				 * directly initiated by the user, we pass the
+				 * PERFORM_DELETION_INTERNAL flag.
+				 */
+				performDeletion(&object, DROP_CASCADE,
+								PERFORM_DELETION_INTERNAL |
+								PERFORM_DELETION_QUIETLY);
+			}
+		}
+	}
+
+	list_free_deep(xact_drop_actions);
+	xact_drop_actions = NIL;
+
+	foreach(l, xact_reset_actions)
+	{
+		SVariableXActActionItem *xact_ai =
+							(SVariableXActActionItem *) lfirst(l);
+
+		if (xact_ai->action == RECHECK)
+		{
+			/*
+			 * we can do recheck only in transactionn state
+			 * when we can touch system catalog. When transaction
+			 * is ending by ROLLBACK we are not in transaction state.
+			 */
+			if (IsTransactionState())
+			{
+				SVariable	svar;
+				bool		found;
+
+				svar = (SVariable) hash_search(sessionvars, &xact_ai->varid,
+											HASH_FIND, &found);
+
+				if (found)
+				{
+					HeapTuple	tp;
+
+					tp = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(svar->varid));
+
+					if (HeapTupleIsValid(tp))
+						ReleaseSysCache(tp);
+					else
+						free_session_variable(svar);
+				}
+
+				xact_reset_actions = foreach_delete_current(xact_reset_actions, l);
+			}
+		}
+		else
+		{
+			/*
+			 * We want to reset session variable (release it from
+			 * local memory) when RESET is required or when session
+			 * variable was removed explicitly (DROP VARIABLE) or
+			 * implicitly (ON COMMIT DROP). Explicit releasing should
+			 * be done only if the transaction is commited.
+			 */
+			if ((xact_ai->action == RESET) ||
+				(xact_ai->action == ON_COMMIT_RESET &&
+				 xact_ai->deleting_subid == InvalidSubTransactionId &&
+				 isCommit))
+				free_session_variable_varid(xact_ai->varid);
+
+			xact_reset_actions = foreach_delete_current(xact_reset_actions, l);
+		}
+	}
+
+	list_free_deep(xact_drop_actions);
+	list_free_deep(xact_reset_actions);
+
+	xact_drop_actions = NIL;
+	xact_reset_actions = NIL;
+}
+
+/*
+ * Post-subcommit or post-subabort cleanup of xact action list.
+ *
+ * During subabort, we can immediately remove entries created during this
+ * subtransaction.  During subcommit, just relabel entries marked during
+ * this subtransaction as being the parent's responsibility.
+ */
+void
+AtEOSubXact_SessionVariable_on_xact_actions(bool isCommit, SubTransactionId mySubid,
+											SubTransactionId parentSubid)
+{
+	ListCell   *cur_item;
+
+	foreach(cur_item, xact_drop_actions)
+	{
+		SVariableXActActionItem *xact_ai =
+								  (SVariableXActActionItem *) lfirst(cur_item);
+
+		if (!isCommit && xact_ai->creating_subid == mySubid)
+		{
+			/* cur_item must be removed */
+			xact_drop_actions = foreach_delete_current(xact_drop_actions, cur_item);
+			pfree(xact_ai);
+		}
+		else
+		{
+			/* cur_item must be preserved */
+			if (xact_ai->creating_subid == mySubid)
+				xact_ai->creating_subid = parentSubid;
+			if (xact_ai->deleting_subid == mySubid)
+				xact_ai->deleting_subid = isCommit ? parentSubid : InvalidSubTransactionId;
+		}
+	}
+
+	/*
+	 * Reset and recheck actions - cleaning memory should be used every time
+	 * (when the variable with short life cycle was used) and then
+	 * cannot be removed from xact action list.
+	 */
+	foreach(cur_item, xact_reset_actions)
+	{
+		SVariableXActActionItem *xact_ai =
+								  (SVariableXActActionItem *) lfirst(cur_item);
+
+		if (!isCommit &&
+			xact_ai->creating_subid == mySubid &&
+			xact_ai->action != RESET &&
+			xact_ai->action != RECHECK)
+		{
+			/* cur_item must be removed */
+			xact_reset_actions = foreach_delete_current(xact_reset_actions, cur_item);
+			pfree(xact_ai);
+		}
+		else
+		{
+			/* cur_item must be preserved */
+			if (xact_ai->creating_subid == mySubid)
+				xact_ai->creating_subid = parentSubid;
+			if (xact_ai->deleting_subid == mySubid)
+				xact_ai->deleting_subid = isCommit ? parentSubid : InvalidSubTransactionId;
+		}
+	}
+}
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index bf42587e38..852959e69d 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -12387,6 +12387,7 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel,
 			case OCLASS_PUBLICATION_REL:
 			case OCLASS_SUBSCRIPTION:
 			case OCLASS_TRANSFORM:
+			case OCLASS_VARIABLE:
 
 				/*
 
diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c
index 7d343f0678..059603d3e7 100644
--- a/src/backend/executor/execExpr.c
+++ b/src/backend/executor/execExpr.c
@@ -33,6 +33,7 @@
 #include "access/nbtree.h"
 #include "catalog/objectaccess.h"
 #include "catalog/pg_type.h"
+#include "commands/session_variable.h"
 #include "executor/execExpr.h"
 #include "executor/nodeSubplan.h"
 #include "funcapi.h"
@@ -994,6 +995,60 @@ 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;
+
+							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;
+							}
+
+							/*
+							 * We should use session variable buffer, when
+							 * it is available.
+							 */
+							if (es_session_variables)
+							{
+								SessionVariableValue *var;
+
+								/* check params, unexpected */
+								if (param->paramid >= es_num_session_variables)
+									elog(ERROR, "paramid of PARAM_VARIABLE param is out of range");
+
+								var = &es_session_variables[param->paramid];
+
+								/* unexpected */
+								if (var->typid != param->paramtype)
+									elog(ERROR, "type of buffered value is different than PARAM_VARIABLE type");
+
+								/*
+								 * In this case, the parameter is like a
+								 * constant
+								 */
+								scratch.opcode = EEOP_CONST;
+								scratch.d.constval.value = var->value;
+								scratch.d.constval.isnull = var->isnull;
+								ExprEvalPushStep(state, &scratch);
+							}
+							else
+							{
+								/*
+								 * When we have no full PlanState (plpgsql
+								 * simple expr evaluation), then we should
+								 * use direct access.
+								 */
+								scratch.opcode = EEOP_PARAM_VARIABLE;
+								scratch.d.vparam.varid = param->paramvarid;
+								scratch.d.vparam.vartype = param->paramtype;
+								ExprEvalPushStep(state, &scratch);
+							}
+						}
+						break;
+
 					case PARAM_EXTERN:
 
 						/*
index eb49817cee..548db9ab2c 100644
+#include "commands/session_variable.h"
 
+			/* direct access to session variable (without buffering) */
+			*op->resvalue = GetSessionVariable(op->d.vparam.varid,
index b3ce4bae53..a5db32d9d4 100644
+#include "commands/session_variable.h"
 
+	/*
+	 * Prepare session variables, if not prepared in queryDesc
+	 */
+	if (queryDesc->num_session_variables > 0)
+	{
+		/*
+		 * link shared memory with working copy of session variable's values
+		 * used in this query. This access is used by parallel query executor's
+		 * workers.
+		 */
+		estate->es_session_variables = queryDesc->session_variables;
+		estate->es_num_session_variables = queryDesc->num_session_variables;
+	}
+	else if (queryDesc->plannedstmt->sessionVariables)
+	{
+		ListCell   *lc;
+		int			nSessionVariables;
+		int			i = 0;
+
+		nSessionVariables = list_length(queryDesc->plannedstmt->sessionVariables);
+
+		/* Create buffer used for session variables */
+		estate->es_session_variables = (SessionVariableValue *)
+			palloc(nSessionVariables * sizeof(SessionVariableValue));
+
+		foreach(lc, queryDesc->plannedstmt->sessionVariables)
+		{
+			AclResult	aclresult;
+			Oid			varid = lfirst_oid(lc);
+
+			aclresult = pg_variable_aclcheck(varid, GetUserId(), ACL_READ);
+			if (aclresult != ACLCHECK_OK)
+				aclcheck_error(aclresult, OBJECT_VARIABLE,
+							   session_variable_get_name(varid));
+
+			estate->es_session_variables[i].varid = varid;
+			estate->es_session_variables[i].value = CopySessionVariable(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/execParallel.c b/src/backend/executor/execParallel.c
index f8a4a40e7b..b87faec411 100644
--- a/src/backend/executor/execParallel.c
+++ b/src/backend/executor/execParallel.c
@@ -12,8 +12,9 @@
  * workers and ensuring that their state generally matches that of the
  * leader; see src/backend/access/transam/README.parallel for details.
  * However, we must save and restore relevant executor state, such as
- * any ParamListInfo associated with the query, buffer/WAL usage info, and
- * the actual plan to be passed down to the worker.
+ * any ParamListInfo associated with the query, buffer/WAL usage info,
+ * session variables buffer, and the actual plan to be passed down to
+ * the worker.
  *
  * IDENTIFICATION
  *	  src/backend/executor/execParallel.c
@@ -66,6 +67,7 @@
 #define PARALLEL_KEY_QUERY_TEXT		UINT64CONST(0xE000000000000008)
 #define PARALLEL_KEY_JIT_INSTRUMENTATION UINT64CONST(0xE000000000000009)
 #define PARALLEL_KEY_WAL_USAGE			UINT64CONST(0xE00000000000000A)
+#define PARALLEL_KEY_SESSION_VARIABLES	UINT64CONST(0xE00000000000000B)

 
+/* Helper functions that can pass values of session variables */
+static Size EstimateSessionVariables(EState *estate);
+static void SerializeSessionVariables(EState *estate, char **start_address);
+static SessionVariableValue * RestoreSessionVariables(char **start_address,
+													int *num_session_variables);
+
 /*
  * Create a serialized representation of the plan to be sent to each worker.
  */
@@ -597,6 +605,7 @@ ExecInitParallelPlan(PlanState *planstate, EState *estate,
 	char	   *pstmt_data;
 	char	   *pstmt_space;
 	char	   *paramlistinfo_space;
+	char	   *session_variables_space;
 	BufferUsage *bufusage_space;
 	WalUsage   *walusage_space;
 	SharedExecutorInstrumentation *instrumentation = NULL;
@@ -606,6 +615,7 @@ ExecInitParallelPlan(PlanState *planstate, EState *estate,
 	int			instrumentation_len = 0;
 	int			jit_instrumentation_len = 0;
 	int			instrument_offset = 0;
+	int			session_variables_len = 0;
 	Size		dsa_minsize = dsa_minimum_size();
 	char	   *query_string;
 	int			query_len;
@@ -661,6 +671,11 @@ ExecInitParallelPlan(PlanState *planstate, EState *estate,
 	shm_toc_estimate_chunk(&pcxt->estimator, paramlistinfo_len);
 	shm_toc_estimate_keys(&pcxt->estimator, 1);
 
+	/* Estimate space for serialized session variables. */
+	session_variables_len = EstimateSessionVariables(estate);
+	shm_toc_estimate_chunk(&pcxt->estimator, session_variables_len);
+	shm_toc_estimate_keys(&pcxt->estimator, 1);
+
 	/*
 	 * Estimate space for BufferUsage.
 	 *
@@ -755,6 +770,11 @@ ExecInitParallelPlan(PlanState *planstate, EState *estate,
 	shm_toc_insert(pcxt->toc, PARALLEL_KEY_PARAMLISTINFO, paramlistinfo_space);
 	SerializeParamList(estate->es_param_list_info, &paramlistinfo_space);
 
+	/* Store serialized session variables. */
+	session_variables_space = shm_toc_allocate(pcxt->toc, session_variables_len);
+	shm_toc_insert(pcxt->toc, PARALLEL_KEY_SESSION_VARIABLES, session_variables_space);
+	SerializeSessionVariables(estate, &session_variables_space);
+
 	/* Allocate space for each worker's BufferUsage; no need to initialize. */
 	bufusage_space = shm_toc_allocate(pcxt->toc,
 									  mul_size(sizeof(BufferUsage), pcxt->nworkers));
@@ -1402,6 +1422,7 @@ ParallelQueryMain(dsm_segment *seg, shm_toc *toc)
 	SharedJitInstrumentation *jit_instrumentation;
 	int			instrument_options = 0;
 	void	   *area_space;
+	char	   *sessionvariable_space;
 	dsa_area   *area;
 	ParallelWorkerContext pwcxt;

+	/* Reconstruct session variables. */
+	sessionvariable_space = shm_toc_lookup(toc,
+										  PARALLEL_KEY_SESSION_VARIABLES,
+										  false);
+	queryDesc->session_variables =
+		RestoreSessionVariables(&sessionvariable_space,
+							   &queryDesc->num_session_variables);
+
 	/* Start up the executor */
 	queryDesc->plannedstmt->jitFlags = fpes->jit_flags;
 	ExecutorStart(queryDesc, fpes->eflags);
@@ -1496,3 +1525,118 @@ ParallelQueryMain(dsm_segment *seg, shm_toc *toc)
 	FreeQueryDesc(queryDesc);
 	receiver->rDestroy(receiver);
 }
+
+/*
+ * Estimate the amount of space required to serialize a
+ * session variable.
+ */
+static Size
+EstimateSessionVariables(EState *estate)
+{
+	int			i;
+	Size		sz = sizeof(int);
+
+	if (estate->es_session_variables == NULL)
+		return sz;
+
+	for (i = 0; i < estate->es_num_session_variables; i++)
+	{
+		SessionVariableValue *svarval;
+		Oid			typeOid;
+		int16		typLen;
+		bool		typByVal;
+
+		svarval = &estate->es_session_variables[i];
+
+		typeOid = svarval->typid;
+
+		sz = add_size(sz, sizeof(Oid)); /* space for type OID */
+
+		/* space for datum/isnull */
+		Assert(OidIsValid(typeOid));
+		get_typlenbyval(typeOid, &typLen, &typByVal);
+
+		sz = add_size(sz,
+					  datumEstimateSpace(svarval->value, svarval->isnull, typByVal, typLen));
+	}
+
+	return sz;
+}
+
+/*
+ * Serialize a session variables buffer into caller-provided storage.
+ *
+ * We write the number of parameters first, as a 4-byte integer, and then
+ * write details for each parameter in turn.  The details for each parameter
+ * consist of a 4-byte type OID, and then the datum as serialized by
+ * datumSerialize().  The caller is responsible for ensuring that there is
+ * enough storage to store the number of bytes that will be written; use
+ * EstimateSessionVariables to find out how many will be needed.
+ * *start_address is updated to point to the byte immediately following those
+ * written.
+ *
+ * RestoreSessionVariables can be used to recreate a session variable buffer
+ * based on the serialized representation;
+ */
+static void
+SerializeSessionVariables(EState *estate, char **start_address)
+{
+	int			nparams;
+	int			i;
+
+	/* Write number of parameters. */
+	nparams = estate->es_num_session_variables;
+	memcpy(*start_address, &nparams, sizeof(int));
+	*start_address += sizeof(int);
+
+	/* Write each parameter in turn. */
+	for (i = 0; i < nparams; i++)
+	{
+		SessionVariableValue *svarval;
+		Oid			typeOid;
+		int16		typLen;
+		bool		typByVal;
+
+		svarval = &estate->es_session_variables[i];
+		typeOid = svarval->typid;
+
+		/* Write type OID. */
+		memcpy(*start_address, &typeOid, sizeof(Oid));
+		*start_address += sizeof(Oid);
+
+		Assert(OidIsValid(typeOid));
+		get_typlenbyval(typeOid, &typLen, &typByVal);
+
+		datumSerialize(svarval->value, svarval->isnull, typByVal, typLen,
+					   start_address);
+	}
+}
+
+static SessionVariableValue *
+RestoreSessionVariables(char **start_address, int *num_session_variables)
+{
+	SessionVariableValue *session_variables;
+	int			i;
+	int			nparams;
+
+	memcpy(&nparams, *start_address, sizeof(int));
+	*start_address += sizeof(int);
+
+	*num_session_variables = nparams;
+	session_variables = (SessionVariableValue *)
+		palloc(nparams * sizeof(SessionVariableValue));
+
+	for (i = 0; i < nparams; i++)
+	{
+		SessionVariableValue *svarval = &session_variables[i];
+
+		/* Read type OID. */
+		memcpy(&svarval->typid, *start_address, sizeof(Oid));
+		*start_address += sizeof(Oid);
+
+		/* Read datum/isnull. */
+		svarval->value = datumRestore(start_address, &svarval->isnull);
+	}
+
+	return session_variables;
+}
diff --git a/src/backend/executor/spi.c b/src/backend/executor/spi.c
index 0568ae123f..1b37ef33a4 100644
--- a/src/backend/executor/spi.c
+++ b/src/backend/executor/spi.c
@@ -2867,6 +2867,9 @@ _SPI_error_callback(void *arg)
 			case RAW_PARSE_PLPGSQL_ASSIGN3:
 				errcontext("PL/pgSQL assignment \"%s\"", query);
 				break;
+			case RAW_PARSE_PLPGSQL_LET:
+				errcontext("LET statement \"%s\"", query);
+				break;
 			default:
 				errcontext("SQL statement \"%s\"", query);
 				break;
diff --git a/src/backend/executor/svariableReceiver.c b/src/backend/executor/svariableReceiver.c
new file mode 100644
index 0000000000..66b8b6ec8c
--- /dev/null
+++ b/src/backend/executor/svariableReceiver.c
@@ -0,0 +1,145 @@
+/*-------------------------------------------------------------------------
+ *
+ * svariableReceiver.c
+ *	  An implementation of DestReceiver that stores the result value in
+ *	  a session variable.
+ *
+ * Portions Copyright (c) 1996-2020, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *	  src/backend/executor/svariableReceiver.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/detoast.h"
+#include "executor/svariableReceiver.h"
+#include "commands/session_variable.h"
+
+typedef struct
+{
+	DestReceiver pub;
+	Oid			varid;
+	Oid			typid;
+	int32		typmod;
+	int			typlen;
+	int			slot_offset;
+	int			rows;
+}			svariableState;
+
+
+/*
+ * Prepare to receive tuples from executor.
+ */
+static void
+svariableStartupReceiver(DestReceiver *self, int operation, TupleDesc typeinfo)
+{
+	svariableState *myState = (svariableState *) self;
+	int			natts = typeinfo->natts;
+	int			outcols = 0;
+	int			i;
+
+	for (i = 0; i < natts; i++)
+	{
+		Form_pg_attribute attr = TupleDescAttr(typeinfo, i);
+
+		if (attr->attisdropped)
+			continue;
+
+		if (++outcols > 1)
+			elog(ERROR, "svariable DestReceiver can take only one attribute");
+
+		myState->typid = attr->atttypid;
+		myState->typmod = attr->atttypmod;
+		myState->typlen = attr->attlen;
+		myState->slot_offset = i;
+	}
+
+	myState->rows = 0;
+}
+
+/*
+ * Receive a tuple from the executor and store it in session variable.
+ */
+static bool
+svariableReceiveSlot(TupleTableSlot *slot, DestReceiver *self)
+{
+	svariableState *myState = (svariableState *) self;
+	Datum		value;
+	bool		isnull;
+	bool		freeval = false;
+
+	/* Make sure the tuple is fully deconstructed */
+	slot_getallattrs(slot);
+
+	value = slot->tts_values[myState->slot_offset];
+	isnull = slot->tts_isnull[myState->slot_offset];
+
+	if (myState->typlen == -1 && !isnull && VARATT_IS_EXTERNAL(DatumGetPointer(value)))
+	{
+		value = PointerGetDatum(detoast_external_attr((struct varlena *)
+													  DatumGetPointer(value)));
+		freeval = true;
+	}
+
+	SetSessionVariable(myState->varid, value, isnull, myState->typid);
+
+	if (freeval)
+		pfree(DatumGetPointer(value));
+
+	return true;
+}
+
+/*
+ * Clean up at end of an executor run
+ */
+static void
+svariableShutdownReceiver(DestReceiver *self)
+{
+	/* Do nothing */
+}
+
+/*
+ * Destroy receiver when done with it
+ */
+static void
+svariableDestroyReceiver(DestReceiver *self)
+{
+	pfree(self);
+}
+
+/*
+ * Initially create a DestReceiver object.
+ */
+DestReceiver *
+CreateVariableDestReceiver(void)
+{
+	svariableState *self = (svariableState *) palloc0(sizeof(svariableState));
+
+	self->pub.receiveSlot = svariableReceiveSlot;
+	self->pub.rStartup = svariableStartupReceiver;
+	self->pub.rShutdown = svariableShutdownReceiver;
+	self->pub.rDestroy = svariableDestroyReceiver;
+	self->pub.mydest = DestVariable;
+
+	/* private fields will be set by SetVariableDestReceiverParams */
+
+	return (DestReceiver *) self;
+}
+
+/*
+ * Set parameters for a VariableDestReceiver
+ */
+void
+SetVariableDestReceiverParams(DestReceiver *self, Oid varid)
+{
+	svariableState *myState = (svariableState *) self;
+
+	Assert(myState->pub.mydest == DestVariable);
+	Assert(OidIsValid(varid));
+
+	myState->varid = varid;
+}
diff --git a/src/backend/jit/llvm/llvmjit_expr.c b/src/backend/jit/llvm/llvmjit_expr.c
index 6d1181225e..4299809eb5 100644
--- a/src/backend/jit/llvm/llvmjit_expr.c
+++ b/src/backend/jit/llvm/llvmjit_expr.c
@@ -1073,6 +1073,12 @@ llvm_compile_expr(ExprState *state)
 				LLVMBuildBr(b, opblocks[opno + 1]);
 				break;
 
index df0b747883..d802ab64fe 100644
+	COPY_NODE_FIELD(sessionVariables);
 
@@ -1508,6 +1509,7 @@ _copyParam(const Param *from)
 
 	return newnode;
@@ -3166,6 +3168,7 @@ _copyQuery(const Query *from)
 	COPY_SCALAR_FIELD(canSetTag);
 	COPY_NODE_FIELD(utilityStmt);
 	COPY_SCALAR_FIELD(resultRelation);
+	COPY_SCALAR_FIELD(resultVariable);
 	COPY_SCALAR_FIELD(hasAggs);
 	COPY_SCALAR_FIELD(hasWindowFuncs);
 	COPY_SCALAR_FIELD(hasTargetSRFs);
@@ -3176,6 +3179,7 @@ _copyQuery(const Query *from)
 	COPY_SCALAR_FIELD(hasForUpdate);
 	COPY_SCALAR_FIELD(hasRowSecurity);
 	COPY_SCALAR_FIELD(isReturn);
+	COPY_SCALAR_FIELD(hasSessionVariables);
 	COPY_NODE_FIELD(cteList);
 	COPY_NODE_FIELD(rtable);
 	COPY_NODE_FIELD(jointree);
@@ -3289,6 +3293,20 @@ _copySelectStmt(const SelectStmt *from)
 	return newnode;
 }
 
+static LetStmt *
+	COPY_SCALAR_FIELD(set_default);
@@ -4903,6 +4921,23 @@ _copyDropSubscriptionStmt(const DropSubscriptionStmt *from)
 	return newnode;
 }
 
+static CreateSessionVarStmt *
+_copyCreateSessionVarStmt(const CreateSessionVarStmt *from)
+{
+	CreateSessionVarStmt *newnode = makeNode(CreateSessionVarStmt);
+
+	COPY_NODE_FIELD(variable);
+	COPY_NODE_FIELD(typeName);
+	COPY_NODE_FIELD(collClause);
+	COPY_NODE_FIELD(defexpr);
+	COPY_SCALAR_FIELD(eoxaction);
+	COPY_SCALAR_FIELD(if_not_exists);
+	COPY_SCALAR_FIELD(is_not_null);
+	COPY_SCALAR_FIELD(is_immutable);
+
+	return newnode;
+}
+
 /* ****************************************************************
  *					extensible.h copy functions
  * ****************************************************************
@@ -5406,6 +5441,9 @@ copyObjectImpl(const void *from)
 		case T_SelectStmt:
 			retval = _copySelectStmt(from);
 			break;
+		case T_LetStmt:
+			retval = _copyLetStmt(from);
+			break;
 		case T_SetOperationStmt:
 			retval = _copySetOperationStmt(from);
 			break;
@@ -5751,6 +5789,9 @@ copyObjectImpl(const void *from)
 		case T_DropSubscriptionStmt:
 			retval = _copyDropSubscriptionStmt(from);
 			break;
+		case  T_CreateSessionVarStmt:
+			retval = _copyCreateSessionVarStmt(from);
+			break;
 		case T_A_Expr:
 			retval = _copyA_Expr(from);
 			break;
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index cb7ddd463c..4220dd47ac 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -215,6 +215,7 @@ _equalParam(const Param *a, const Param *b)
 	COMPARE_SCALAR_FIELD(paramtype);
 	COMPARE_SCALAR_FIELD(paramtypmod);
 	COMPARE_SCALAR_FIELD(paramcollid);
+	COMPARE_SCALAR_FIELD(paramvarid);
 	COMPARE_LOCATION_FIELD(location);
 
 	return true;
+	COMPARE_SCALAR_FIELD(hasSessionVariables);
@@ -1093,6 +1096,17 @@ _equalSelectStmt(const SelectStmt *a, const SelectStmt *b)
 	return true;
 }
 
+static bool
+_equalLetStmt(const LetStmt * a, const LetStmt * b)
+{
+	COMPARE_NODE_FIELD(target);
+	COMPARE_NODE_FIELD(query);
+	COMPARE_SCALAR_FIELD(set_default);
+	COMPARE_SCALAR_FIELD(plpgsql_mode);
+
+	return true;
+}
+
 static bool
 _equalSetOperationStmt(const SetOperationStmt *a, const SetOperationStmt *b)
 {
@@ -2377,6 +2391,22 @@ _equalDropSubscriptionStmt(const DropSubscriptionStmt *a,
 	return true;
 }
 
+static bool
+_equalCreateSessionVarStmt(const CreateSessionVarStmt *a,
+						  const CreateSessionVarStmt *b)
+{
+	COMPARE_NODE_FIELD(variable);
+	COMPARE_NODE_FIELD(typeName);
+	COMPARE_NODE_FIELD(collClause);
+	COMPARE_NODE_FIELD(defexpr);
+	COMPARE_SCALAR_FIELD(eoxaction);
+	COMPARE_SCALAR_FIELD(if_not_exists);
+	COMPARE_SCALAR_FIELD(is_not_null);
+	COMPARE_SCALAR_FIELD(is_immutable);
+
+	return true;
+}
+
 static bool
 _equalCreatePolicyStmt(const CreatePolicyStmt *a, const CreatePolicyStmt *b)
 {
@@ -3409,6 +3439,9 @@ equal(const void *a, const void *b)
 		case T_SelectStmt:
 			retval = _equalSelectStmt(a, b);
 			break;
+		case T_LetStmt:
+			retval = _equalLetStmt(a, b);
+			break;
 		case T_SetOperationStmt:
 			retval = _equalSetOperationStmt(a, b);
 			break;
@@ -3754,6 +3787,9 @@ equal(const void *a, const void *b)
 		case T_DropSubscriptionStmt:
 			retval = _equalDropSubscriptionStmt(a, b);
 			break;
+		case T_CreateSessionVarStmt:
+			retval = _equalCreateSessionVarStmt(a, b);
+			break;
 		case T_A_Expr:
 			retval = _equalA_Expr(a, b);
 			break;
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index 91a89b6d51..329d10cd5f 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -324,6 +324,7 @@ _outPlannedStmt(StringInfo str, const PlannedStmt *node)
 	WRITE_NODE_FIELD(invalItems);
 	WRITE_NODE_FIELD(paramExecTypes);
 	WRITE_NODE_FIELD(utilityStmt);
+	WRITE_NODE_FIELD(sessionVariables);
 	WRITE_LOCATION_FIELD(stmt_location);
 	WRITE_INT_FIELD(stmt_len);
 }
@@ -1166,6 +1167,7 @@ _outParam(StringInfo str, const Param *node)
 	WRITE_OID_FIELD(paramtype);
 	WRITE_INT_FIELD(paramtypmod);
 	WRITE_OID_FIELD(paramcollid);
+	WRITE_OID_FIELD(paramvarid);
 	WRITE_LOCATION_FIELD(location);
 }
 
@@ -2279,6 +2281,7 @@ _outPlannerGlobal(StringInfo str, const PlannerGlobal *node)
 	WRITE_NODE_FIELD(relationOids);
 	WRITE_NODE_FIELD(invalItems);
 	WRITE_NODE_FIELD(paramExecTypes);
+	WRITE_NODE_FIELD(sessionVariables);
 	WRITE_UINT_FIELD(lastPHId);
 	WRITE_UINT_FIELD(lastRowMarkId);
 	WRITE_INT_FIELD(lastPlanNodeId);
@@ -2339,6 +2342,7 @@ _outPlannerInfo(StringInfo str, const PlannerInfo *node)
 	WRITE_BOOL_FIELD(hasPseudoConstantQuals);
 	WRITE_BOOL_FIELD(hasAlternativeSubPlans);
 	WRITE_BOOL_FIELD(hasRecursion);
+	WRITE_BOOL_FIELD(hasSessionVariables);
 	WRITE_INT_FIELD(wt_param_id);
 	WRITE_BITMAPSET_FIELD(curOuterRels);
 	WRITE_NODE_FIELD(curOuterParams);
@@ -2874,6 +2878,18 @@ _outPLAssignStmt(StringInfo str, const PLAssignStmt *node)
 	WRITE_LOCATION_FIELD(location);
 }
 
+static void
+_outLetStmt(StringInfo str, const LetStmt * node)
+{
+	WRITE_NODE_TYPE("LET");
+
+	WRITE_NODE_FIELD(target);
+	WRITE_NODE_FIELD(query);
+	WRITE_BOOL_FIELD(set_default);
+	WRITE_BOOL_FIELD(plpgsql_mode);
+	WRITE_LOCATION_FIELD(location);
+}
+
 static void
 _outFuncCall(StringInfo str, const FuncCall *node)
 {
@@ -3054,6 +3070,7 @@ _outQuery(StringInfo str, const Query *node)
 			case T_IndexStmt:
 			case T_NotifyStmt:
 			case T_DeclareCursorStmt:
+			case T_LetStmt:
 				WRITE_NODE_FIELD(utilityStmt);
 				break;
 			default:
@@ -3065,6 +3082,7 @@ _outQuery(StringInfo str, const Query *node)
 		appendStringInfoString(str, " :utilityStmt <>");
 
@@ -3075,6 +3093,7 @@ _outQuery(StringInfo str, const Query *node)
+	WRITE_BOOL_FIELD(hasSessionVariables);
@@ -4376,6 +4395,9 @@ outNode(StringInfo str, const void *obj)
index 76cff8a2b1..2a9849a59c 100644
+	READ_BOOL_FIELD(hasSessionVariables);
 
 	READ_DONE();
+	READ_NODE_FIELD(sessionVariables);
 
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index bd01ec0526..a14f6ac823 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -316,6 +316,7 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
 	glob->lastPlanNodeId = 0;
 	glob->transientPlan = false;
 	glob->dependsOnRole = false;
+	glob->sessionVariables = NIL;
 
 	/*
+	result->sessionVariables = glob->sessionVariables;

+	/*
+	 * 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 6ccec759bd..78d7395eb3 100644
--- a/src/backend/optimizer/plan/setrefs.c
+++ b/src/backend/optimizer/plan/setrefs.c
@@ -172,6 +172,8 @@ static List *set_returning_clause_references(PlannerInfo *root,
 											 Plan *topplan,
 											 Index resultRelation,
 											 int rtoffset);
+static bool pull_up_has_session_variables_walker(Node *node,
+											   PlannerInfo *root);

 /*****************************************************************************
@@ -1127,6 +1129,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
@@ -1758,10 +1804,14 @@ fix_expr_common(PlannerInfo *root, Node *node)
 /*
  * 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 should to calculate paramid.
  */
 static Node *
 fix_param_node(PlannerInfo *root, Param *p)
@@ -1780,6 +1830,52 @@ 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)
+		{
+			PlanInvalItem *inval_item = makeNode(PlanInvalItem);
+
+			/* paramid is still session variable id */
+			inval_item->cacheId = VARIABLEOID;
+			inval_item->hashValue = GetSysCacheHashValue1(VARIABLEOID,
+														  ObjectIdGetDatum(p->paramvarid));
+
+			/* Append this variable to global, register dependency */
+			root->glob->invalItems = lappend(root->glob->invalItems,
+											 inval_item);
+			root->glob->sessionVariables = lappend_oid(root->glob->sessionVariables,
+													  p->paramvarid);
+
+			p->paramid = n;
+		}
+
+		return (Node *) p;
+	}
+
 	return (Node *) copyObject(p);
 }
 
@@ -1841,7 +1937,9 @@ 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.
+ * and adding OIDs from regclass Const nodes into root->glob->relationOids,
+ * and replacing PARAM_VARIABLE paramid, that is the oid of the session variable
+ * to offset the array by query used session variables. ???
  *
  * 'node': the expression to be modified
  * 'rtoffset': how much to increment varnos by
@@ -1863,7 +1961,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);
 	}
diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c
index 873e43bfe6..ab65692a64 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"
@@ -819,7 +820,8 @@ max_parallel_hazard_walker(Node *node, max_parallel_hazard_context *context)
 	{
 		Param	   *param = (Param *) node;

 		if (param->paramkind != PARAM_EXEC ||
@@ -2229,6 +2231,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 *
@@ -2351,6 +2354,30 @@ 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 = GetSessionVariable(param->paramvarid,
+											 &isnull,
+											 param->paramtype,
+											 true);
+
+					return (Node *) makeConst(param->paramtype,
+											  param->paramtypmod,
+											  param->paramcollid,
+											  (int) typLen,
+											  pval,
+											  isnull,
+											  typByVal);
+				}
 
 				/*
 				 * Not replaceable, so just copy the Param (no need to
@@ -4735,22 +4762,46 @@ substitute_actual_parameters_mutator(Node *node,
 {
 	if (node == NULL)
 		return NULL;
+
+	/*
+	 * The expression of SQL function can contain params of two separated
+	 * by different paramkind field. The nodes with paramkind PARAM_EXTERN
+	 * are related to function's arguments (and should be replaced in this
+	 * step), because this is mechanism how we apply the function's arguments
+	 * for an expression.
+	 *
+	 * The nodes with paramkind PARAM_VARIABLE are related to an usage of
+	 * session variables. The values of session variables are not passed
+	 * to expression by expression arguments, so it should not be replaced
+	 * here by function's arguments. Although we can substitute params
+	 * related to immutable session variables with default expression by
+	 * this default expression, it is safer don't do it. We don't need to
+	 * run security checks here. There can be some performance loss, but
+	 * an access to session variable is fast (and the result of default
+	 * expression is immediately materialized and can be reused).
+	 */
 	if (IsA(node, Param))
 	{
 		Param	   *param = (Param *) node;
 
-		if (param->paramkind != PARAM_EXTERN)
+		if (param->paramkind != PARAM_EXTERN &&
+			  param->paramkind != PARAM_VARIABLE)
 			elog(ERROR, "unexpected paramkind: %d", (int) param->paramkind);
-		if (param->paramid <= 0 || param->paramid > context->nargs)
-			elog(ERROR, "invalid paramid: %d", param->paramid);
 
-		/* Count usage of parameter */
-		context->usecounts[param->paramid - 1]++;
+		if (param->paramkind == PARAM_EXTERN)
+		{
+			if (param->paramid <= 0 || param->paramid > context->nargs)
+				elog(ERROR, "invalid paramid: %d", param->paramid);
 
-		/* Select the appropriate actual arg and replace the Param with it */
-		/* We don't need to copy at this time (it'll get done later) */
-		return list_nth(context->args, param->paramid - 1);
+			/* Count usage of parameter */
+			context->usecounts[param->paramid - 1]++;
+
+			/* Select the appropriate actual arg and replace the Param with it */
+			/* We don't need to copy at this time (it'll get done later) */
+			return list_nth(context->args, param->paramid - 1);
+		}
 	}
+
 	return expression_tree_mutator(node, substitute_actual_parameters_mutator,
 								   (void *) context);
 }
diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
index 146ee8dd1e..42c5e977a3 100644
--- a/src/backend/parser/analyze.c
+++ b/src/backend/parser/analyze.c
@@ -25,8 +25,11 @@
 #include "postgres.h"
 
+#include "commands/session_variable.h"

+		case T_LetStmt:
 
 			/*

+	qry->hasSessionVariables = pstate->p_hasSessionVariables;
+
 	assign_query_collations(pstate, qry);

 	qry->hasTargetSRFs = pstate->p_hasTargetSRFs;
 	qry->hasSubLinks = pstate->p_hasSubLinks;
+	qry->hasSessionVariables = pstate->p_hasSessionVariables;

@@ -1269,8 +1285,9 @@ transformSelectStmt(ParseState *pstate, SelectStmt *stmt)
 	/* process the FROM clause */
 	transformFromClause(pstate, stmt->fromClause);
 
-	/* transform targetlist */
-	qry->targetList = transformTargetList(pstate, stmt->targetList,
+	/* Transform targetlist. */
+	qry->targetList = transformTargetList(pstate,
+										  stmt->targetList,
 										  EXPR_KIND_SELECT_TARGET);
 
 	/* mark column origins */
@@ -1361,6 +1378,8 @@ transformSelectStmt(ParseState *pstate, SelectStmt *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 */
@@ -1585,12 +1604,268 @@ transformValuesClause(ParseState *pstate, SelectStmt *stmt)
 	qry->jointree = makeFromExpr(pstate->p_joinlist, NULL);
 
 	qry->hasSubLinks = pstate->p_hasSubLinks;
+	qry->hasSessionVariables = pstate->p_hasSessionVariables;

 	return qry;
 }
 
+/*
+ * transformLetStmt -
+				 errmsg("session variable \"%s\" doesn't exist",
+	get_session_variable_type_typmod_collid(varid, &typid, &typmod, &collid);
+						session_variable_get_name(varid)),
+	if (stmt->set_default && attrname != NULL)
+	 * stmt->query is SelectStmt node. An tranformation of
+	 * this node doesn't support SetToDefault node. Instead injecting
+	 * of transformSelectStmt or parse state, we can directly
+	 * transform target list here if holds SetToDefault node.
+	if (stmt->set_default)
+		selectQuery = makeNode(Query);
+		selectQuery->commandType = CMD_SELECT;
+		 * ResTarget(SetToDefault) -> TargetEntry(expr(SetToDefault)) */
+		selectQuery->targetList = transformTargetList(pstate,
+													  ((SelectStmt *) stmt->query)->targetList,
+													  EXPR_KIND_LET_TARGET);
+	/* don't allow multicolumn result */
+			targetName = attrname != NULL ? attrname : get_session_variable_name(varid);
+			pstate->p_hasSessionVariables = true;
+							session_variable_get_name(varid),
+	query->hasSessionVariables = pstate->p_hasSessionVariables;
+	 * statement, then we should return query. We should not
+	 * use a utility wrapper node.
@@ -1841,6 +2116,8 @@ transformSetOperationStmt(ParseState *pstate, SelectStmt *stmt)
 
+	qry->hasSessionVariables = pstate->p_hasSessionVariables;
+
 	assign_query_collations(pstate, qry);
 
 	/* this must be done after collations, for reliable comparison of exprs */
@@ -2369,6 +2646,7 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
 
 	qry->hasTargetSRFs = pstate->p_hasTargetSRFs;
 	qry->hasSubLinks = pstate->p_hasSubLinks;
+	qry->hasSessionVariables = pstate->p_hasSessionVariables;

diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 3d4dd43e47..ac65dc86c6 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -244,6 +244,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	JoinType			jtype;
 	DropBehavior		dbehavior;
 	OnCommitAction		oncommit;
+	VariableEOXAction	oneoxaction;
 	List				*list;
 	Node				*node;
 	ObjectType			objtype;
@@ -296,8 +297,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 		ConstraintsSetStmt CopyStmt CreateAsStmt CreateCastStmt
 		CreateDomainStmt CreateExtensionStmt CreateGroupStmt CreateOpClassStmt
 		CreateOpFamilyStmt AlterOpFamilyStmt CreatePLangStmt
-		CreateSchemaStmt CreateSeqStmt CreateStmt CreateStatsStmt CreateTableSpaceStmt
-		CreateFdwStmt CreateForeignServerStmt CreateForeignTableStmt
+		CreateSchemaStmt CreateSessionVarStmt CreateSeqStmt CreateStmt CreateStatsStmt
+		CreateTableSpaceStmt CreateFdwStmt CreateForeignServerStmt CreateForeignTableStmt
 		CreateAssertionStmt CreateTransformStmt CreateTrigStmt CreateEventTrigStmt
 		CreateUserStmt CreateUserMappingStmt CreateRoleStmt CreatePolicyStmt
 		CreatedbStmt DeclareCursorStmt DefineStmt DeleteStmt DiscardStmt DoStmt
@@ -307,7 +308,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 		DropTransformStmt
 		DropUserMappingStmt ExplainStmt FetchStmt
 		GrantStmt GrantRoleStmt ImportForeignSchemaStmt IndexStmt InsertStmt
-		ListenStmt LoadStmt LockStmt NotifyStmt ExplainableStmt PreparableStmt
+		LetStmt ListenStmt LoadStmt LockStmt NotifyStmt ExplainableStmt PreparableStmt
 		CreateFunctionStmt AlterFunctionStmt ReindexStmt RemoveAggrStmt
 		RemoveFuncStmt RemoveOperStmt RenameStmt ReturnStmt RevokeStmt RevokeRoleStmt
 		RuleActionStmt RuleActionStmtOrEmpty RuleStmt
@@ -447,6 +448,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
 
@@ -469,6 +471,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 
 %type <ival>	for_locking_strength
 %type <node>	for_locking_item
@@ -632,6 +635,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %type <partboundspec> PartitionBoundSpec
 %type <list>		hash_partbound
 %type <defelt>		hash_partbound_elem
+%type <node>		OptSessionVarDefExpr
+%type <boolean>		OptNotNull OptImmutable

 /*
@@ -701,7 +706,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	KEY

 	MAPPING MATCH MATERIALIZED MAXVALUE METHOD MINUTE_P MINVALUE MODE MONTH_P MOVE
@@ -740,8 +745,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	UESCAPE UNBOUNDED UNCOMMITTED UNENCRYPTED UNION UNIQUE UNKNOWN
 	UNLISTEN UNLOGGED UNTIL UPDATE USER USING

 
@@ -776,6 +781,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %token		MODE_PLPGSQL_ASSIGN1
 %token		MODE_PLPGSQL_ASSIGN2
 %token		MODE_PLPGSQL_ASSIGN3
+%token		MODE_PLPGSQL_LET

@@ -881,6 +887,13 @@ parse_toplevel:
 
 /*
@@ -984,6 +997,7 @@ stmt:
 			| CreatePolicyStmt
 			| CreatePLangStmt
 			| CreateSchemaStmt
+			| CreateSessionVarStmt
 			| CreateSeqStmt
 			| CreateStmt
 			| CreateSubscriptionStmt
@@ -1021,6 +1035,7 @@ stmt:
 			| ImportForeignSchemaStmt
 			| IndexStmt
 			| InsertStmt
+			| LetStmt
 			| ListenStmt
 			| RefreshMatViewStmt
 			| LoadStmt
@@ -1913,7 +1928,12 @@ DiscardStmt:
 					n->target = DISCARD_SEQUENCES;
 					$$ = (Node *) n;
 				}
-
+			| DISCARD VARIABLES
+				{
+					DiscardStmt *n = makeNode(DiscardStmt);
+					n->target = DISCARD_VARIABLES;
+					$$ = (Node *) n;
+				}
 		;

@@ -4743,6 +4763,69 @@ create_extension_opt_item:
 				}
 		;
 
+/*****************************************************************************
+ *
+ *		QUERY :
+ *				CREATE VARIABLE varname [AS] type
+ *
+ *****************************************************************************/
+
+CreateSessionVarStmt:
+			CREATE OptTemp OptImmutable VARIABLE qualified_name opt_as Typename opt_collate_clause OptNotNull OptSessionVarDefExpr OnEOXActionOption
+				{
+					CreateSessionVarStmt *n = makeNode(CreateSessionVarStmt);
+					$5->relpersistence = $2;
+					n->is_immutable = $3;
+					n->variable = $5;
+					n->typeName = $7;
+					n->collClause = (CollateClause *) $8;
+					n->is_not_null = $9;
+					n->defexpr = $10;
+					n->eoxaction = $11;
+					n->if_not_exists = false;
+					$$ = (Node *) n;
+				}
+			| CREATE OptTemp OptImmutable VARIABLE IF_P NOT EXISTS qualified_name opt_as Typename opt_collate_clause OptNotNull OptSessionVarDefExpr OnEOXActionOption
+				{
+					CreateSessionVarStmt *n = makeNode(CreateSessionVarStmt);
+					$8->relpersistence = $2;
+					n->is_immutable = $3;
+					n->variable = $8;
+					n->typeName = $10;
+					n->collClause = (CollateClause *) $11;
+					n->is_not_null = $12;
+					n->defexpr = $13;
+					n->eoxaction = $14;
+					n->if_not_exists = true;
+					$$ = (Node *) n;
+				}
+		;
+
+OptSessionVarDefExpr: DEFAULT b_expr					{ $$ = $2; }
+			| /* EMPTY */							{ $$ = NULL; }
+		;
+
+/*
+ * Temporary session variables can be dropped on successful
+ * transaction end like tables. We can force only reset on
+ * transaction end. Because the session variables are not
+ * transactional, we have calculate with ROLLBACK too.
+ * The clause ON TRANSACTION END is more illustrative
+ * synonymum to ON COMMIT ROLLBACK RESET.
+ */
+OnEOXActionOption:  ON COMMIT DROP					{ $$ = VARIABLE_EOX_DROP; }
+			| ON TRANSACTION END_P RESET			{ $$ = VARIABLE_EOX_RESET; }
+			| /*EMPTY*/								{ $$ = VARIABLE_EOX_NOOP; }
+		;
+
+OptNotNull: NOT NULL_P								{ $$ = true; }
+			| /* EMPTY */							{ $$ = false; }
+		;
+
+OptImmutable: IMMUTABLE								{ $$ = true; }
+			| /* EMPTY */							{ $$ = false; }
+		;
+
 /*****************************************************************************
  *
  * ALTER EXTENSION name UPDATE [ TO version ]
@@ -6430,6 +6513,7 @@ object_type_any_name:
 			| TEXT_P SEARCH DICTIONARY				{ $$ = OBJECT_TSDICTIONARY; }
 			| TEXT_P SEARCH TEMPLATE				{ $$ = OBJECT_TSTEMPLATE; }
 			| TEXT_P SEARCH CONFIGURATION			{ $$ = OBJECT_TSCONFIGURATION; }
+			| VARIABLE								{ $$ = OBJECT_VARIABLE; }
 		;
 
 /*
@@ -7198,6 +7282,14 @@ privilege_target:
 					n->objs = $2;
 					$$ = n;
 				}
@@ -7238,6 +7330,14 @@ privilege_target:

@@ -7400,6 +7500,7 @@ defacl_privilege_target:

@@ -9102,6 +9203,25 @@ RenameStmt: ALTER AGGREGATE aggregate_with_argtypes RENAME TO name
 					n->missing_ok = false;
 					$$ = (Node *)n;
 				}
 
 opt_column: COLUMN
@@ -9430,6 +9550,25 @@ AlterObjectSchemaStmt:
 					n->missing_ok = false;
 					$$ = (Node *)n;
 				}
 
@@ -9683,6 +9822,14 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec

@@ -11018,6 +11165,7 @@ ExplainableStmt:
 			| CreateMatViewStmt
 			| RefreshMatViewStmt
 			| ExecuteStmt					/* by default all are $$=$1 */
+			| LetStmt
 		;
 
 /*****************************************************************************
@@ -11046,6 +11194,7 @@ PreparableStmt:
 			| InsertStmt
 			| UpdateStmt
 			| DeleteStmt					/* by default all are $$=$1 */
+			| LetStmt
 		;
 
 /*****************************************************************************
@@ -11463,6 +11612,49 @@ opt_hold: /* EMPTY */						{ $$ = 0; }
 			| WITHOUT HOLD					{ $$ = 0; }
 		;
 
+					n->set_default = IsA($4, SetToDefault);
@@ -15771,6 +15963,7 @@ unreserved_keyword:
@@ -15928,6 +16121,8 @@ unreserved_keyword:
@@ -16335,6 +16530,7 @@ bare_label_keyword:
@@ -16534,6 +16730,8 @@ bare_label_keyword:
 
 			/*
 
 			if (isAgg)
 
 			/*
index 2d1a477154..b780018f2c 100644

 /* GUC parameters */
@@ -82,6 +84,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);

 /*
 
@@ -785,6 +796,87 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
 					 parser_errposition(pstate, cref->location)));
 	}
 
+	/* The col's reference can be reference to session variable too */
+	varid = IdentifyVariable(cref->fields, &attrname, &not_unique);
+
+	if (not_unique)
+		ereport(ERROR,
+				(errcode(ERRCODE_AMBIGUOUS_PARAMETER),
+				 errmsg("session variable reference \"%s\" is ambiguous",
+						NameListToString(cref->fields)),
+				 parser_errposition(pstate, cref->location)));
+
+	if (OidIsValid(varid))
+	{
+		Oid			typid;
+		int32		typmod;
+		Oid			collid;
+
+		get_session_variable_type_typmod_collid(varid, &typid, &typmod, &collid);
+
+		if (node != NULL)
+		{
+			/*
+			 * Some cases with ambiguous references can be solved without
+			 * raising an error. When there is an collision between column
+			 * name (or label) and some session variable name, and when we know
+			 * attribute name, then we can ignore the collision when:
+			 *
+			 *   a) variable is of scalar type (then indirection cannot be
+			 *      applied on this session variable.
+			 *
+			 *   b) when related variable has no field of attrname, then
+			 *      indirection cannot be applied on this session variable.
+			 */
+			if (attrname)
+			{
+				if (type_is_rowtype(typid))
+				{
+					TupleDesc	tupdesc;
+					bool		found = false;
+					int			i;
+
+					/* slow part, I hope it will not be to often */
+					tupdesc = lookup_rowtype_tupdesc(typid, typmod);
+					for (i = 0; i < tupdesc->natts; i++)
+					{
+						if (namestrcmp(&(TupleDescAttr(tupdesc, i)->attname), attrname) == 0 &&
+							!TupleDescAttr(tupdesc, i)->attisdropped)
+						{
+							found = true;
+							break;
+						}
+					}
+
+					FreeTupleDesc(tupdesc);
+
+					/* there is no composite variable with this field */
+					if (!found)
+						varid = InvalidOid;
+				}
+				else
+					/* there is no composite variable with this name */
+					varid = InvalidOid;
+			}
+
+			/*
+			 * Raise error if varid is still valid, since it is ambiguous.
+			 */
+			if (OidIsValid(varid))
+				ereport(ERROR,
+						(errcode(ERRCODE_AMBIGUOUS_COLUMN),
+						 errmsg("column reference \"%s\" is ambiguous",
+								NameListToString(cref->fields)),
+						 errdetail("The qualified identifier can be column reference or session variable reference"),
+						 parser_errposition(pstate, cref->location)));
+		}
+
+		if (OidIsValid(varid))
+			node = makeParamSessionVariable(pstate,
+										   varid, typid, typmod, collid,
+										   attrname, cref->location);
+	}
+
 	/*
 	 * Throw error if no translation found.
 	 */
@@ -819,6 +911,74 @@ 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;
+
+	/*
+	 * There are two ways to access session variables - direct, used by simple
+	 * plpgsql expressions, where it is not necessary to emulate stability.
+	 * And Buffered access, which is used everywhere else. We should ensure
+	 * stable values, and because session variables are global, then we should
+	 * work with copied values instead of directly accessing variables. For
+	 * direct access, the varid is best. For buffered access, we need
+	 * to assign an index to the buffer - later, when we know what variables are
+	 * used. Now, we just remember, so we use session variables.
+	 */
+	pstate->p_hasSessionVariables = true;
+
+	if (attrname != NULL)
+	{
+		TupleDesc	tupdesc;
+		int			i;
+
+		tupdesc = lookup_rowtype_tupdesc(typid, typmod);
+
+		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)
 {
@@ -1721,6 +1881,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:
@@ -1729,6 +1890,7 @@ transformSubLink(ParseState *pstate, SubLink *sublink)
 			break;
 		case EXPR_KIND_COLUMN_DEFAULT:
 		case EXPR_KIND_FUNCTION_DEFAULT:
+		case EXPR_KIND_VARIABLE_DEFAULT:
 			err = _("cannot use subquery in DEFAULT expression");
 			break;
 		case EXPR_KIND_INDEX_EXPRESSION:
@@ -3059,6 +3221,7 @@ ParseExprKindName(ParseExprKind exprKind)
 			return "CHECK";
 		case EXPR_KIND_COLUMN_DEFAULT:
 		case EXPR_KIND_FUNCTION_DEFAULT:
+		case EXPR_KIND_VARIABLE_DEFAULT:
 			return "DEFAULT";
 		case EXPR_KIND_INDEX_EXPRESSION:
 			return "index expression";
@@ -3084,6 +3247,8 @@ ParseExprKindName(ParseExprKind exprKind)
 			return "GENERATED AS";
 		case EXPR_KIND_CYCLE_MARK:
 			return "CYCLE";
+		case EXPR_KIND_LET_TARGET:
+			return "LET";
 
 			/*
index 542f9167aa..a1198d64ce 100644
 
 			/*
 
index 9521e81100..94ebe8b0a3 100644
 
+	/*
+			/* Read session variable metadata with defexpr */
 
 	/*

 		case DestTupleQueue:
 
index 960f3fadce..7910c0d3f6 100644
 
+	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 1fbc387d47..c3cd6521ff 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -49,6 +49,7 @@
 #include "commands/proclang.h"
 #include "commands/publicationcmds.h"
 #include "commands/schemacmds.h"
+#include "commands/session_variable.h"
 #include "commands/seclabel.h"
 #include "commands/sequence.h"
 #include "commands/subscriptioncmds.h"
@@ -187,6 +188,7 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree)
 		case T_CreateRangeStmt:
 		case T_CreateRoleStmt:
 		case T_CreateSchemaStmt:
+		case T_CreateSessionVarStmt:
 		case T_CreateSeqStmt:
 		case T_CreateStatsStmt:
 		case T_CreateStmt:
@@ -238,6 +240,7 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree)
 
 		case T_CallStmt:
 		case T_DoStmt:
+		case T_LetStmt:
 			{
 				/*
 				 * Commands inside the DO block or the called procedure might
@@ -1062,6 +1065,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,
@@ -1386,6 +1394,10 @@ ProcessUtilitySlow(ParseState *pstate,
 				}
 				break;
 
+			case T_CreateSessionVarStmt:
+				address = DefineSessionVariable(pstate, (CreateSessionVarStmt *) parsetree);
+				break;
+
 				/*
 				 * ************* object creation / destruction **************
 				 */
@@ -2176,6 +2188,10 @@ UtilityContainsQuery(Node *parsetree)
 				return UtilityContainsQuery(qry->utilityStmt);
 			return qry;
 
+		case T_LetStmt:
+			qry = castNode(Query, ((LetStmt *) parsetree)->query);
+			return qry;
+
 		default:
 			return NULL;
 	}
@@ -2317,6 +2333,9 @@ AlterObjectTypeCommandTag(ObjectType objtype)
 		case OBJECT_STATISTIC_EXT:
 			tag = CMDTAG_ALTER_STATISTICS;
 			break;
+		case OBJECT_VARIABLE:
+			tag = CMDTAG_ALTER_VARIABLE;
+			break;
 		default:
 			tag = CMDTAG_UNKNOWN;
 			break;
@@ -2367,6 +2386,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:
 			{
@@ -2621,6 +2644,9 @@ CreateCommandTag(Node *parsetree)
 				case OBJECT_STATISTIC_EXT:
 					tag = CMDTAG_DROP_STATISTICS;
 					break;
+				case OBJECT_VARIABLE:
+					tag = CMDTAG_DROP_VARIABLE;
+					break;
 				default:
 					tag = CMDTAG_UNKNOWN;
 			}
@@ -2909,6 +2935,9 @@ CreateCommandTag(Node *parsetree)
 				case DISCARD_SEQUENCES:
 					tag = CMDTAG_DISCARD_SEQUENCES;
 					break;
+				case DISCARD_VARIABLES:
+					tag = CMDTAG_DISCARD_VARIABLES;
+					break;
 				default:
 					tag = CMDTAG_UNKNOWN;
 			}
@@ -3193,6 +3222,10 @@ CreateCommandTag(Node *parsetree)
 			}
 			break;
 
+		case T_CreateSessionVarStmt:
+			tag = CMDTAG_CREATE_VARIABLE;
+			break;
+
 		default:
 			elog(WARNING, "unrecognized node type: %d",
 				 (int) nodeTag(parsetree));
@@ -3240,6 +3273,7 @@ GetCommandLogLevel(Node *parsetree)
 			break;
 
 		case T_PLAssignStmt:
+		case T_LetStmt:
 			lev = LOGSTMT_ALL;
 			break;

 	ereport(ERROR,
index 8da525c715..b5d5d5f006 100644
@@ -7925,6 +7926,14 @@ get_parameter(Param *param, deparse_context *context)
 		return;
 	}
 
+	/* translate paramvarid to session variable name */
+						 session_variable_get_name(param->paramvarid));
index 9176514a96..af5ed1bca0 100644
 
+/*
+ * get_varname_varid

 	/*
 
 	return false;
 }
index 36f8d5682e..85bbb41da3 100644

 	*numTablesPtr = numTables;
 	return tblinfo;
diff --git a/src/bin/pg_dump/dumputils.c b/src/bin/pg_dump/dumputils.c
index b5900e4ed8..e458472c96 100644
--- a/src/bin/pg_dump/dumputils.c
+++ b/src/bin/pg_dump/dumputils.c
@@ -540,6 +540,11 @@ do { \
 		CONVERT_PRIV('r', "SELECT");
 		CONVERT_PRIV('w', "UPDATE");
 	}
+	else if (strcmp(type, "VARIABLE") == 0)
+	{
+		CONVERT_PRIV('S', "READ");
+		CONVERT_PRIV('W', "WRITE");
+	}
 	else
 		abort();
 
index 753252e05e..4f29d09ad8 100644
@@ -130,12 +130,14 @@ typedef struct _restoreOptions
 
 	int			useDB;
index 8903a694ae..0199a96411 100644
@@ -3404,6 +3412,7 @@ _getObjectDescription(PQExpBuffer buf, TocEntry *te)
@@ -3596,7 +3605,8 @@ _printTocEntry(ArchiveHandle *AH, TocEntry *te, bool isData)
 
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 784771c2d7..ef9b21be68 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -276,6 +276,7 @@ static void dumpPolicy(Archive *fout, const PolicyInfo *polinfo);
 static void dumpPublication(Archive *fout, const PublicationInfo *pubinfo);
 static void dumpPublicationTable(Archive *fout, const PublicationRelInfo *pubrinfo);
 static void dumpSubscription(Archive *fout, const SubscriptionInfo *subinfo);
+static void dumpVariable(Archive *fout, const VariableInfo * varinfo);
 static void dumpDatabase(Archive *AH);
 static void dumpDatabaseConfig(Archive *AH, PQExpBuffer outbuf,
 							   const char *dbname, Oid dboid);
@@ -4502,6 +4503,211 @@ get_next_possible_free_pg_type_oid(Archive *fout, PQExpBuffer upgrade_query)
 	return next_possible_free_oid;
 }
 
+/*
+ * getVariables
+ *	  get information about variables
+ */
+void
+getVariables(Archive *fout)
+{
+	PQExpBuffer query;
+	PGresult   *res;
+	VariableInfo *varinfo;
+	int			i_tableoid;
+	int			i_oid;
+	int			i_varname;
+	int			i_varnamespace;
+	int			i_vartype;
+	int			i_vartypname;
+	int			i_vardefexpr;
+	int			i_vareoxaction;
+	int			i_varisnotnull;
+	int			i_varisimmutable;
+	int			i_rolname;
+	int			i_varacl;
+	int			i_acldefault;
+	int			i,
+				ntups;
+
+	if (fout->remoteVersion < 150000)
+		return;
+
+	query = createPQExpBuffer();
+
+	resetPQExpBuffer(query);
+
+	/* Get the variables in current database. */
+	appendPQExpBuffer(query,
+					  "SELECT v.tableoid, v.oid, v.varname, "
+					  "v.vareoxaction, "
+					  "v.varnamespace, "
+					  "v.vartype, "
+					  "pg_catalog.format_type(v.vartype, v.vartypmod) as vartypname, "
+					  "v.varisnotnull, "
+					  "v.varisimmutable, "
+					  "pg_catalog.pg_get_expr(v.vardefexpr,0) as vardefexpr, "
+					  "(%s varowner) AS rolname, "
+					  "v.varacl, "
+					  "acldefault('V', v.varowner) AS acldefault "
+					  "FROM pg_variable v ",
+					  username_subquery);
+
+	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+
+	ntups = PQntuples(res);
+
+	i_tableoid = PQfnumber(res, "tableoid");
+	i_oid = PQfnumber(res, "oid");
+	i_varname = PQfnumber(res, "varname");
+	i_varnamespace = PQfnumber(res, "varnamespace");
+	i_vartype = PQfnumber(res, "vartype");
+	i_vartypname = PQfnumber(res, "vartypname");
+	i_vareoxaction = PQfnumber(res, "vareoxaction");
+	i_vardefexpr = PQfnumber(res, "vardefexpr");
+	i_varisnotnull = PQfnumber(res, "varisnotnull");
+	i_varisimmutable = PQfnumber(res, "varisimmutable");
+
+	i_rolname = PQfnumber(res, "rolname");
+	i_varacl = PQfnumber(res, "varacl");
+	i_acldefault = PQfnumber(res, "acldefault");
+
+	varinfo = pg_malloc(ntups * sizeof(VariableInfo));
+
+	for (i = 0; i < ntups; i++)
+	{
+		TypeInfo   *vtype;
+
+		varinfo[i].dobj.objType = DO_VARIABLE;
+		varinfo[i].dobj.catId.tableoid =
+			atooid(PQgetvalue(res, i, i_tableoid));
+		varinfo[i].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
+		AssignDumpId(&varinfo[i].dobj);
+		varinfo[i].dobj.name = pg_strdup(PQgetvalue(res, i, i_varname));
+		varinfo[i].dobj.namespace =
+			findNamespace(atooid(PQgetvalue(res, i, i_varnamespace)));
+
+		varinfo[i].vartype = atooid(PQgetvalue(res, i, i_vartype));
+		varinfo[i].vartypname = pg_strdup(PQgetvalue(res, i, i_vartypname));
+		varinfo[i].vareoxaction = pg_strdup(PQgetvalue(res, i, i_vareoxaction));
+		varinfo[i].varisnotnull = *(PQgetvalue(res, i, i_varisnotnull)) == 't';
+		varinfo[i].varisimmutable = *(PQgetvalue(res, i, i_varisimmutable)) == 't';
+
+		varinfo[i].dacl.acl = pg_strdup(PQgetvalue(res, i, i_varacl));
+		varinfo[i].dacl.acldefault = pg_strdup(PQgetvalue(res, i, i_acldefault));
+		varinfo[i].dacl.privtype = 0;
+		varinfo[i].dacl.initprivs = NULL;
+		varinfo[i].rolname = pg_strdup(PQgetvalue(res, i, i_rolname));
+
+		/* Decide whether we want to dump it */
+		selectDumpableObject(&(varinfo[i].dobj), fout);
+
+		/* Do not try to dump ACL if no ACL exists. */
+		if (!PQgetisnull(res, i, i_varacl))
+			varinfo[i].dobj.components |= DUMP_COMPONENT_ACL;
+
+		if (PQgetisnull(res, i, i_vardefexpr))
+			varinfo[i].vardefexpr = NULL;
+		else
+			varinfo[i].vardefexpr = pg_strdup(PQgetvalue(res, i, i_vardefexpr));
+
+		if (strlen(varinfo[i].rolname) == 0)
+			pg_log_warning("owner of variable \"%s\" appears to be invalid",
+						   varinfo[i].dobj.name);
+
+		/* Decide whether we want to dump it */
+		selectDumpableObject(&(varinfo[i].dobj), fout);
+
+		vtype = findTypeByOid(varinfo[i].vartype);
+		addObjectDependency(&varinfo[i].dobj, vtype->dobj.dumpId);
+	}
+	PQclear(res);
+
+	destroyPQExpBuffer(query);
+}
+
+/*
+ * dumpVariable
+ *	  dump the definition of the given variables
+ */
+static void
+dumpVariable(Archive *fout, const VariableInfo * varinfo)
+{
+	DumpOptions *dopt = fout->dopt;
+
+	PQExpBuffer delq;
+	PQExpBuffer query;
+	char *qualvarname;
+	const char *vartypname;
+	const char *vardefexpr;
+	const char *vareoxaction;
+	const char *varisnotnull;
+	const char *varisimmutable;
+
+	/* Skip if not to be dumped */
+	if (!varinfo->dobj.dump || dopt->dataOnly)
+		return;
+
+	delq = createPQExpBuffer();
+	query = createPQExpBuffer();
+
+	qualvarname = pg_strdup(fmtQualifiedDumpable(varinfo));
+	vartypname = varinfo->vartypname;
+	vardefexpr = varinfo->vardefexpr;
+	vareoxaction = varinfo->vareoxaction;
+	varisnotnull = varinfo->varisnotnull ? " NOT NULL" : "";
+	varisimmutable = varinfo->varisimmutable ? "IMMUTABLE " : "";
+
+	appendPQExpBuffer(delq, "DROP VARIABLE %s;\n",
+					  qualvarname);
+
+	appendPQExpBuffer(query, "CREATE %sVARIABLE %s AS %s%s",
+					  varisimmutable, qualvarname, vartypname, varisnotnull);
+
+	if (vardefexpr)
+		appendPQExpBuffer(query, " DEFAULT %s",
+						  vardefexpr);
+
+	if (strcmp(vareoxaction, "d") == 0)
+		appendPQExpBuffer(query, " ON COMMIT DROP");
+	else if (strcmp(vareoxaction, "r") == 0)
+		appendPQExpBuffer(query, " ON TRANSACTION END RESET");
+
+	appendPQExpBuffer(query, ";\n");
+
+	if (varinfo->dobj.dump & DUMP_COMPONENT_DEFINITION)
+		ArchiveEntry(fout, varinfo->dobj.catId, varinfo->dobj.dumpId,
+					 ARCHIVE_OPTS(.tag = varinfo->dobj.name,
+								  .namespace = varinfo->dobj.namespace->dobj.name,
+								  .owner = varinfo->rolname,
+								  .description = "VARIABLE",
+								  .section = SECTION_PRE_DATA,
+								  .createStmt = query->data,
+								  .dropStmt = delq->data));
+
+	/* Dump comment if any */
+	if (varinfo->dobj.dump & DUMP_COMPONENT_COMMENT)
+		dumpComment(fout, "VARIABLE", qualvarname,
+					NULL, varinfo->rolname,
+					varinfo->dobj.catId, 0, varinfo->dobj.dumpId);
+
+	/* Dump ACL if any */
+	if (varinfo->dobj.dump & DUMP_COMPONENT_ACL)
+	{
+		char *qvarname = pg_strdup(fmtId(varinfo->dobj.name));
+
+		dumpACL(fout, varinfo->dobj.dumpId, InvalidDumpId, "VARIABLE",
+				qvarname, NULL,
+				varinfo->dobj.namespace->dobj.name, varinfo->rolname, &varinfo->dacl);
+
+		free(qvarname);
+	}
+
+	destroyPQExpBuffer(delq);
+	destroyPQExpBuffer(query);
+
+	free(qualvarname);
+}
+
 static void
 binary_upgrade_set_type_oids_by_type_oid(Archive *fout,
 										 PQExpBuffer upgrade_buffer,
@@ -9115,7 +9321,8 @@ getAdditionalACLs(Archive *fout)
 					dobj->objType == DO_TABLE ||
 					dobj->objType == DO_PROCLANG ||
 					dobj->objType == DO_FDW ||
-					dobj->objType == DO_FOREIGN_SERVER)
+					dobj->objType == DO_FOREIGN_SERVER ||
+					dobj->objType == DO_VARIABLE)
 				{
 					DumpableObjectWithAcl *daobj = (DumpableObjectWithAcl *) dobj;
 
@@ -9707,6 +9914,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
 		case DO_SUBSCRIPTION:
 			dumpSubscription(fout, (const SubscriptionInfo *) dobj);
 			break;
+		case DO_VARIABLE:
+			dumpVariable(fout, (VariableInfo *) dobj);
+			break;
 		case DO_PRE_DATA_BOUNDARY:
 		case DO_POST_DATA_BOUNDARY:
 			/* never dumped, nothing to do */
@@ -14135,6 +14345,9 @@ dumpDefaultACL(Archive *fout, const DefaultACLInfo *daclinfo)
 		case DEFACLOBJ_NAMESPACE:
 			type = "SCHEMAS";
 			break;
+		case DEFACLOBJ_VARIABLE:
+			type = "VARIABLE";
+			break;
 		default:
 			/* shouldn't get here */
 			fatal("unrecognized object type in default privileges: %d",
@@ -17679,6 +17892,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 			case DO_CONVERSION:
 			case DO_TABLE:
 			case DO_TABLE_ATTACH:
+			case DO_VARIABLE:
 			case DO_ATTRDEF:
 			case DO_PROCLANG:
 			case DO_CAST:
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index f011ace8a8..28fc950eef 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -52,6 +52,7 @@ typedef enum
 	DO_TABLE,
 	DO_TABLE_ATTACH,
 	DO_ATTRDEF,
+	DO_VARIABLE,
 	DO_INDEX,
 	DO_INDEX_ATTACH,
 	DO_STATSEXT,
@@ -82,7 +83,7 @@ typedef enum
 	DO_PUBLICATION,
 	DO_PUBLICATION_REL,
 	DO_PUBLICATION_TABLE_IN_SCHEMA,
-	DO_SUBSCRIPTION
+	DO_SUBSCRIPTION,
 } DumpableObjectType;
 
 /*
 
+/*
+ * The VariableInfo struct is used to represent session variables
+ */
+	DumpableAcl dacl;
 
 #endif							/* PG_DUMP_H */
index 5399ec1cd9..45108079c7 100644
@@ -75,6 +75,7 @@ enum dbObjectTypePriorities
@@ -116,6 +117,7 @@ static const int dbObjectTypePriority[] =
@@ -1508,6 +1510,10 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
index 64aaa80eee..cc00e485db 100644

 
 	printf(_("\nOptions controlling the restore:\n"));
 	printf(_("  -a, --data-only              restore only the data, no schema\n"));
+	printf(_("  -A, --variable=NAME          restore named session variable\n"));
 	printf(_("  -c, --clean                  clean (drop) database objects before recreating\n"));
 	printf(_("  -C, --create                 create the target database\n"));
 	printf(_("  -e, --exit-on-error          exit on error, default is to continue\n"));
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index fd01651d9c..0c0adc2ae5 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -501,6 +501,16 @@ my %tests = (
 		unlike => { no_privs => 1, },
 	  },
 
+	'ALTER DEFAULT PRIVILEGES FOR ROLE regress_dump_test_role GRANT READ ON VARIABLES TO PUBLIC'
+	  => {
+		create_order => 56,
+		create_sql   => 'ALTER DEFAULT PRIVILEGES FOR ROLE regress_dump_test_role GRANT READ ON VARIABLES TO PUBLIC;',
+		regexp => qr/^
+			\QALTER DEFAULT PRIVILEGES FOR ROLE regress_dump_test_role GRANT READ ON VARIABLE  TO PUBLIC;\E/xm,
+		like => { %full_runs, section_post_data => 1, },
+		unlike => { no_privs => 1, },
+	  },
+
 	'ALTER ROLE regress_dump_test_role' => {
 		regexp => qr/^
 			\QALTER ROLE regress_dump_test_role WITH \E
@@ -1256,6 +1266,22 @@ my %tests = (
 		unlike => { exclude_dump_test_schema => 1, },
 	},
 
+	'COMMENT ON VARIABLE dump_test.variable1' => {
+		create_order => 71,
+		create_sql   => 'COMMENT ON VARIABLE dump_test.variable1
+					   IS \'comment on variable\';',
+		regexp =>
+		  qr/^\QCOMMENT ON VARIABLE dump_test.variable1 IS 'comment on variable';\E/m,
+		like => {
+			%full_runs,
+			%dump_test_schema_runs,
+			section_pre_data     => 1,
+		},
+		unlike => {
+			exclude_dump_test_schema => 1,
+		},
+	},
+
 	'COPY test_table' => {
 		create_order => 4,
 		create_sql   => 'INSERT INTO dump_test.test_table (col1) '
@@ -3061,6 +3087,30 @@ my %tests = (
 		},
 	},
 
@@ -3500,6 +3550,21 @@ my %tests = (
 		like => {},
 	},
 
+	'GRANT READ ON VARIABLE dump_test.variable1' => {
+		create_order => 73,
+		create_sql =>
+		  'GRANT READ ON VARIABLE dump_test.variable1 TO regress_dump_test_role;',
+		regexp => qr/^
+			\QGRANT READ ON VARIABLE dump_test.variable1 TO regress_dump_test_role;\E
+			/xm,
+		like =>
+		  { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => {
+			exclude_dump_test_schema => 1,
+			no_privs                 => 1,
+		},
+	},
+
 	'REFRESH MATERIALIZED VIEW matview' => {
 		regexp => qr/^\QREFRESH MATERIALIZED VIEW dump_test.matview;\E/m,
 		like =>
diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index ccd7b48108..23e86ee515 100644
--- a/src/bin/psql/command.c
+++ b/src/bin/psql/command.c
@@ -929,6 +929,9 @@ exec_command_d(PsqlScanState scan_state, bool active_branch, const char *cmd)
 						break;
 				}
 				break;
+			case 'V':			/* Variables */
+				success = listVariables(pattern, show_verbose);
+				break;
 			case 'x':			/* Extensions */
 				if (show_verbose)
 					success = listExtensionContents(pattern);
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index c28788e84f..442c714dd6 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -4773,6 +4773,97 @@ listSchemas(const char *pattern, bool verbose, bool showSystem)
 	return true;
 }
 
+/*
+ * \dV
+ *
+ * listVariables()
+ */
+bool
+	static const bool translate_columns[] = {false, false, false, false, false, false, false, false, false, false};
+	if (verbose)
+		appendPQExpBufferStr(&buf, ",\n  ");
+		printACLColumn(&buf, "v.varacl");
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.obj_description(v.oid, 'pg_variable') AS \"%s\"",
+						  gettext_noop("Description"));
+			pg_log_error("Did not find any session variable named \"%s\".",
+			pg_log_error("Did not find any session variables.");
 
 /*
  * \dFp
 
+/* \dV */
 
 #endif							/* DESCRIBE_H */
index b524dc87fc..ff5a701360 100644
 
+	.min_server_version = 150000,
 
 /*
@@ -1932,6 +1940,9 @@ psql_completion(const char *text, int start, int end)
@@ -2431,7 +2442,7 @@ psql_completion(const char *text, int start, int end)
 					  "ROUTINE", "RULE", "SCHEMA", "SEQUENCE", "SERVER",
 					  "STATISTICS", "SUBSCRIPTION", "TABLE",
 					  "TABLESPACE", "TEXT SEARCH", "TRANSFORM FOR",
-					  "TRIGGER", "TYPE", "VIEW");
+					  "TRIGGER", "TYPE", "VARIABLE", "VIEW");
 	else if (Matches("COMMENT", "ON", "ACCESS", "METHOD"))
 		COMPLETE_WITH_QUERY(Query_for_list_of_access_methods);
 	else if (Matches("COMMENT", "ON", "CONSTRAINT"))
@@ -2853,7 +2864,7 @@ psql_completion(const char *text, int start, int end)
@@ -3160,6 +3171,14 @@ psql_completion(const char *text, int start, int end)
 
 /* CREATE VIEW --- is allowed inside CREATE SCHEMA, so use TailMatches */
 	/* Complete CREATE [ OR REPLACE ] VIEW <name> with AS */
@@ -3274,7 +3293,7 @@ psql_completion(const char *text, int start, int end)
 
 /* DISCARD */
 
 /* DO */
 	else if (Matches("DO"))
@@ -3401,6 +3420,12 @@ psql_completion(const char *text, int start, int end)
 	else if (Matches("DROP", "TRANSFORM", "FOR", MatchAny, "LANGUAGE", MatchAny))
 		COMPLETE_WITH("CASCADE", "RESTRICT");
 
+	/* DROP VARIABLE */
+	else if (Matches("DROP", "VARIABLE"))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables, NULL);
+	else if (Matches("DROP", "VARIABLE", MatchAny))
+		COMPLETE_WITH("CASCADE", "RESTRICT");
+
 /* EXECUTE */
 	else if (Matches("EXECUTE"))
 		COMPLETE_WITH_QUERY(Query_for_list_of_prepared_statements);
@@ -3516,7 +3541,8 @@ psql_completion(const char *text, int start, int end)
 		if (HeadMatches("ALTER", "DEFAULT", "PRIVILEGES"))
 			COMPLETE_WITH("SELECT", "INSERT", "UPDATE",
 						  "DELETE", "TRUNCATE", "REFERENCES", "TRIGGER",
-						  "EXECUTE", "USAGE", "ALL");
+						  "EXECUTE", "USAGE", "ALL",
+						  "READ", "WRITE");
 		else
 			COMPLETE_WITH_QUERY(Query_for_list_of_roles
 								" UNION SELECT 'SELECT'"
@@ -3531,6 +3557,8 @@ psql_completion(const char *text, int start, int end)
 								" UNION SELECT 'TEMPORARY'"
 								" UNION SELECT 'EXECUTE'"
 								" UNION SELECT 'USAGE'"
+								" UNION SELECT 'READ'"
+								" UNION SELECT 'WRITE'"
 								" UNION SELECT 'ALL'");
 	}
 
@@ -3540,7 +3568,7 @@ psql_completion(const char *text, int start, int end)
 	 */
 	else if (TailMatches("GRANT|REVOKE", MatchAny))
 	{
-		if (TailMatches("SELECT|INSERT|UPDATE|DELETE|TRUNCATE|REFERENCES|TRIGGER|CREATE|CONNECT|TEMPORARY|TEMP|EXECUTE|USAGE|ALL"))
+		if (TailMatches("SELECT|INSERT|UPDATE|DELETE|TRUNCATE|REFERENCES|TRIGGER|CREATE|CONNECT|TEMPORARY|TEMP|EXECUTE|USAGE|READ|WRITE|ALL"))
 			COMPLETE_WITH("ON");
 		else if (TailMatches("GRANT", MatchAny))
 			COMPLETE_WITH("TO");
@@ -3565,7 +3593,7 @@ psql_completion(const char *text, int start, int end)
 		 * objects supported.
 		 */
 		if (HeadMatches("ALTER", "DEFAULT", "PRIVILEGES"))
-			COMPLETE_WITH("TABLES", "SEQUENCES", "FUNCTIONS", "PROCEDURES", "ROUTINES", "TYPES", "SCHEMAS");
+			COMPLETE_WITH("TABLES", "SEQUENCES", "FUNCTIONS", "PROCEDURES", "ROUTINES", "TYPES", "SCHEMAS", "VARIABLES");
 		else
 			COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_grantables,
 									   " UNION SELECT 'ALL FUNCTIONS IN SCHEMA'"
@@ -3573,6 +3601,7 @@ psql_completion(const char *text, int start, int end)
 									   " UNION SELECT 'ALL ROUTINES IN SCHEMA'"
 									   " UNION SELECT 'ALL SEQUENCES IN SCHEMA'"
 									   " UNION SELECT 'ALL TABLES IN SCHEMA'"
+									   " UNION SELECT 'ALL VARIABLES IN SCHEMA'"
 									   " UNION SELECT 'DATABASE'"
 									   " UNION SELECT 'DOMAIN'"
 									   " UNION SELECT 'FOREIGN DATA WRAPPER'"
@@ -3586,14 +3615,16 @@ psql_completion(const char *text, int start, int end)
 									   " UNION SELECT 'SEQUENCE'"
 									   " UNION SELECT 'TABLE'"
 									   " UNION SELECT 'TABLESPACE'"
-									   " UNION SELECT 'TYPE'");
+									   " UNION SELECT 'TYPE'"
+									   " UNION SELECT 'VARIABLE'");
 	}
 	else if (TailMatches("GRANT|REVOKE", MatchAny, "ON", "ALL"))
 		COMPLETE_WITH("FUNCTIONS IN SCHEMA",
 					  "PROCEDURES IN SCHEMA",
 					  "ROUTINES IN SCHEMA",
 					  "SEQUENCES IN SCHEMA",
-					  "TABLES IN SCHEMA");
+					  "TABLES IN SCHEMA",
+					  "VARIABLES IN SCHEMA");
 	else if (TailMatches("GRANT|REVOKE", MatchAny, "ON", "FOREIGN"))
 		COMPLETE_WITH("DATA WRAPPER", "SERVER");
 
@@ -3627,6 +3658,8 @@ psql_completion(const char *text, int start, int end)
 			COMPLETE_WITH_QUERY(Query_for_list_of_tablespaces);
 		else if (TailMatches("TYPE"))
 			COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes, NULL);
+		else if (TailMatches("VARIABLE"))
+			COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables, NULL);
 		else if (TailMatches("GRANT", MatchAny, MatchAny, MatchAny))
 			COMPLETE_WITH("TO");
 		else
@@ -3799,7 +3832,7 @@ psql_completion(const char *text, int start, int end)
 
 /* PREPARE xx AS */
 
 /*
  * PREPARE TRANSACTION is missing on purpose. It's intended for transaction
@@ -4073,6 +4106,14 @@ psql_completion(const char *text, int start, int end)
 	else if (TailMatches("UPDATE", MatchAny, "SET", MatchAnyExcept("*=")))
 		COMPLETE_WITH("=");
 
@@ -4237,6 +4278,8 @@ psql_completion(const char *text, int start, int end)

index b98f284356..419de41ca2 100644

+extern Oid	IdentifyVariable(List *names, char **attrname, bool *not_unique);

index 4d992dc224..c9a2e1431a 100644
@@ -6246,6 +6246,9 @@
+{ oid => '9221', descr => 'is session variable visible in search path?',
 
 { oid => '2854', descr => 'get OID of current session\'s temp schema, if any',
   proname => 'pg_my_temp_schema', provolatile => 's', proparallel => 'r',
diff --git a/src/include/catalog/pg_variable.h b/src/include/catalog/pg_variable.h
new file mode 100644
index 0000000000..b6716ca8cc
--- /dev/null
+++ b/src/include/catalog/pg_variable.h
@@ -0,0 +1,117 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_variable.h
+ *	  definition of session variables system catalog (pg_variables)
+ *
+ *
+ * Portions Copyright (c) 1996-2020, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/pg_variable.h
+ *
+ * NOTES
+ *	  The Catalog.pm module reads this file and derives schema
+ *	  information.
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PG_VARIABLE_H
+#define PG_VARIABLE_H
+
+#include "catalog/genbki.h"
+#include "catalog/objectaddress.h"
+#include "catalog/pg_variable_d.h"
+#include "utils/acl.h"
+
+/* ----------------
+ *		pg_variable definition.  cpp turns this into
+ *		typedef struct FormData_pg_variable
+ * ----------------
+ */
+CATALOG(pg_variable,9222,VariableRelationId)
+{
+	Oid			oid;			/* oid */
+	NameData	varname;		/* variable name */
+	Oid			varnamespace;	/* OID of namespace containing variable class */
+	Oid			vartype;		/* OID of entry in pg_type for variable's type */
+	int32		vartypmod;		/* typmode for variable's type */
+	Oid			varowner;		/* class owner */
+	Oid			varcollation;	/* variable collation */
+	bool		varisnotnull;	/* Don't allow NULL */
+	bool		varisimmutable;	/* Don't allow changes */
+	char		vareoxaction;	/* action on transaction end */
+
+#ifdef CATALOG_VARLEN			/* variable-length fields start here */
+
+	/* list of expression trees for variable default (NULL if none) */
+	pg_node_tree vardefexpr BKI_DEFAULT(_null_);
+
+	aclitem		varacl[1] BKI_DEFAULT(_null_);	/* access permissions */
+
+#endif
+} FormData_pg_variable;
+
+typedef enum VariableEOXActionCodes
+{
+	VARIABLE_EOX_CODE_NOOP = 'n',	/* NOOP */
+	VARIABLE_EOX_CODE_DROP = 'd',	/* ON COMMIT DROP */
+	VARIABLE_EOX_CODE_RESET = 'r',	/* ON COMMIT RESET */
+}			VariableEOXActionCodes;
+
+/* ----------------
+ *		Form_pg_variable corresponds to a pointer to a tuple with
+ *		the format of pg_variable relation.
+ * ----------------
+ */
+typedef FormData_pg_variable *Form_pg_variable;
+
+DECLARE_UNIQUE_INDEX_PKEY(pg_variable_oid_index, 9223, VariableOidIndexId, on pg_variable using btree(oid oid_ops));
+#define VariableObjectIndexId 9223
+
+DECLARE_UNIQUE_INDEX(pg_variable_varname_nsp_index, 9224, VariableNameNspIndexId,  on pg_variable using btree(varname name_ops, varnamespace oid_ops));
+#define VariableNameNspIndexId  9224
+
+typedef struct Variable
+{
+	Oid			oid;
+	char	   *name;
+	Oid			namespace;
+	Oid			typid;
+	int32		typmod;
+	Oid			owner;
+	Oid			collation;
+	bool		is_not_null;
+	bool		is_immutable;
+	VariableEOXAction eoxaction;
+	bool		has_defexpr;
+	Node	   *defexpr;
+	Acl		   *acl;
+} Variable;
+
+/* returns fields from pg_variable table */
+extern char *get_session_variable_name(Oid varid);
+extern void get_session_variable_type_typmod_collid(Oid varid,
+												   Oid *typid,
+												   int32 *typmod,
+												   Oid *collid);
+
+/* returns name of variable based on current search path */
+extern char *session_variable_get_name(Oid varid);
+
+extern void initVariable(Variable *var,
+						 Oid varid,
+						 bool missing_ok,
+						 bool fast_only);
+extern ObjectAddress VariableCreate(const char *varName,
+									Oid varNamespace,
+									Oid varType,
+									int32 varTypmod,
+									Oid varOwner,
+									Oid varCollation,
+									Node *varDefexpr,
+									VariableEOXAction eoxaction,
+									bool is_not_null,
+									bool if_not_exists,
+									bool is_immutable);
+
+#endif							/* PG_VARIABLE_H */
diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h
new file mode 100644
index 0000000000..c8c3ab2570
--- /dev/null
+++ b/src/include/commands/session_variable.h
@@ -0,0 +1,44 @@
+/*-------------------------------------------------------------------------
+ *
+ * sessionvariable.h
+ *	  prototypes for sessionvariable.c.
+ *
+ *
+ * Portions Copyright (c) 1996-2020, 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 "catalog/objectaddress.h"
+#include "catalog/pg_variable.h"
+#include "nodes/params.h"
+#include "nodes/parsenodes.h"
+#include "nodes/plannodes.h"
+#include "tcop/cmdtag.h"
+#include "utils/queryenvironment.h"
+
+extern void ResetSessionVariables(void);
+extern void RemoveSessionVariable(Oid varid);
+extern ObjectAddress DefineSessionVariable(ParseState *pstate, CreateSessionVarStmt * stmt);
+
+extern Datum GetSessionVariable(Oid varid, bool *isNull, Oid expected_typid, bool copy);
+extern Datum CopySessionVariable(Oid varid, bool *isNull, Oid *typid);
+extern void SetSessionVariable(Oid varid, Datum value, bool isNull, Oid typid);
+extern void SetSessionVariableWithSecurityCheck(Oid varid, Datum value, bool isNull, Oid typid);
+
+extern void ExecuteLetStmt(ParseState *pstate, LetStmt *stmt, ParamListInfo params,
+						   QueryEnvironment *queryEnv, QueryCompletion *qc);
+
+extern void RegisterOnCommitDropSessionVariable(Oid varid);
+
+extern void AtPreEOXact_SessionVariable_on_xact_actions(bool isCommit);
+extern void AtEOSubXact_SessionVariable_on_xact_actions(bool isCommit, SubTransactionId mySubid,
+											SubTransactionId parentSubid);
+
+#endif
diff --git a/src/include/executor/execExpr.h b/src/include/executor/execExpr.h
index 6a24341faa..123a18f298 100644
--- a/src/include/executor/execExpr.h
+++ b/src/include/executor/execExpr.h
@@ -158,6 +158,7 @@ typedef enum ExprEvalOp
 	EEOP_PARAM_EXEC,
 	EEOP_PARAM_EXTERN,
 	EEOP_PARAM_CALLBACK,
+	EEOP_PARAM_VARIABLE,

index 017ad87117..00d2536e30 100644
 
+	/* 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 */
 
index ddc3529332..cc01381d7c 100644
 
+/* ----------------
+ * SessionVariableValue
+ * ----------------
+ */
+typedef struct SessionVariableValue
+{
+	Oid			varid;
+	Oid			typid;
+	bool		isnull;
+	Datum		value;
+}			SessionVariableValue;
+
 /* ----------------
  *	  EState information
  *
@@ -595,6 +607,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/nodes.h b/src/include/nodes/nodes.h
index 7c657c1241..96aa0a2270 100644
--- a/src/include/nodes/nodes.h
+++ b/src/include/nodes/nodes.h
@@ -355,6 +355,7 @@ typedef enum NodeTag
 	T_CreateTableAsStmt,
 	T_CreateSeqStmt,
 	T_AlterSeqStmt,
+	T_CreateSessionVarStmt,
 	T_VariableSetStmt,
 	T_VariableShowStmt,
 	T_DiscardStmt,
@@ -428,6 +429,7 @@ typedef enum NodeTag
 	T_AlterCollationStmt,
 	T_CallStmt,
 	T_AlterStatsStmt,
+	T_LetStmt,
 
 	/*
index 4c5a8a39bf..b4b7c19ba6 100644
 
+	bool		hasSessionVariables; /* uses session variables */

@@ -1627,6 +1630,21 @@ typedef struct UpdateStmt
 	WithClause *withClause;		/* WITH clause */
 } UpdateStmt;
 
+/* ----------------------
+ *		Let Statement
+ * ----------------------
+ */
+typedef struct LetStmt
+{
+	NodeTag		type;
+	List	   *target;			/* target variable */
+	Node	   *query;			/* source expression */
+	bool		set_default;	/* true, when set to DEFAULt is wanted */
+	bool		plpgsql_mode;	/* true, when command will be executed (parsed)
+								 * by plpgsql runtime */
+	int			location;
+}			LetStmt;
+
 /* ----------------------
  *		Select Statement
  *
@@ -1836,6 +1854,7 @@ typedef enum ObjectType
 	OBJECT_TSTEMPLATE,
 	OBJECT_TYPE,
 	OBJECT_USER_MAPPING,
+	OBJECT_VARIABLE,
 	OBJECT_VIEW
 } ObjectType;
 
@@ -2672,6 +2691,23 @@ typedef struct AlterSeqStmt
 	bool		missing_ok;		/* skip error if a role is missing? */
 } AlterSeqStmt;
 
+typedef struct CreateSessionVarStmt
+}			CreateSessionVarStmt;
@@ -3445,7 +3481,8 @@ typedef enum DiscardMode
 	DISCARD_ALL,
 	DISCARD_PLANS,
 	DISCARD_SEQUENCES,
-	DISCARD_TEMP
+	DISCARD_TEMP,
+	DISCARD_VARIABLES
 } DiscardMode;
 
index 324d92880b..668e8f9bb3 100644
 
 	PartitionDirectory partition_directory; /* partition descriptors */
+
+	List	   *sessionVariables;	/* list of used session variables */
 } PlannerGlobal;
 
+	bool		hasSessionVariables;	/* true if session variables were used */
 
 	/*
index be3c30704a..0c2dfc6dd5 100644

 

+	List	   *sessionVariables;	/* list of OIDs for PARAM_VARIABLE Params */
+
index 433437643e..a5179d2eea 100644

+	VARIABLE_EOX_RESET,			/* ON TRANSACTION END RESET */
+ *		PARAM_VARIABLE:  The parameter is an access to session variable
+ *				paramid holds varid.
  */
 typedef enum ParamKind
 {
 	PARAM_EXTERN,
 	PARAM_EXEC,
 	PARAM_SUBLINK,
-	PARAM_MULTIEXPR
+	PARAM_MULTIEXPR,
+	PARAM_VARIABLE
 } ParamKind;
 
 typedef struct Param
+	Oid			paramvarid;		/* OID of session variable if it is used */
 
index bf1adfc52a..0f83ff5323 100644
 
+extern void pull_up_has_session_variables(PlannerInfo *root);
+
 #endif							/* PLANMAIN_H */
index ee179082ce..af171ab5ec 100644
+	EXPR_KIND_VARIABLE_DEFAULT, /* default value for session variable */

@@ -210,6 +212,7 @@ struct ParseState
 	bool		p_hasTargetSRFs;
 	bool		p_hasSubLinks;
 	bool		p_hasModifyingCTE;
+	bool		p_hasSessionVariables;

 
index 88a7e59de5..5485f2c06b 100644
+	DestVariable				/* results sents to session variable */
 } CommandDest;
 
 /* ----------------

 /*

 

index 6dbfdb7be0..ac398178c5 100644
+#include "commands/session_variable.h"
 
 static void plpgsql_estate_setup(PLpgSQL_execstate *estate,
 								 PLpgSQL_function *func,
@@ -2103,6 +2106,10 @@ exec_stmts(PLpgSQL_execstate *estate, List *stmts)
 				rc = exec_stmt_rollback(estate, (PLpgSQL_stmt_rollback *) stmt);
 				break;
 
+			case PLPGSQL_STMT_LET:
+				rc = exec_stmt_let(estate, (PLpgSQL_stmt_let *) stmt);
+				break;
+
 			default:
 				/* point err_stmt to parent, since this one seems corrupt */
 				estate->err_stmt = save_estmt;
@@ -4950,6 +4957,54 @@ exec_stmt_rollback(PLpgSQL_execstate *estate, PLpgSQL_stmt_rollback *stmt)
 	return PLPGSQL_RC_OK;
 }
 
+/* ----------
+ * exec_stmt_let			Evaluate an expression and
+ *					put the result into a session variable.
+ * ----------
+ */
+static int
+exec_stmt_let(PLpgSQL_execstate *estate, PLpgSQL_stmt_let *stmt)
+{
+	bool		isNull;
+	Oid			valtype;
+	int32		valtypmod;
+	Datum		value;
+	Oid			varid;
+
+	List	   *plansources;
+	CachedPlanSource *plansource;
+
+	value = exec_eval_expr(estate,
+						   stmt->expr,
+						   &isNull,
+						   &valtype,
+						   &valtypmod);
+
+	/*
+	 * Oid of target session variable is stored in Query structure.
+	 * It is safer to read this value directly from the plan than to
+	 * hold this value in the plpgsql context, because it's not necessary
+	 * to handle invalidation of the cached value. Next operations
+	 * are read only without any allocations, so we can expect that
+	 * retrieving varid from Query should be fast.
+	 */
+	plansources = SPI_plan_get_plan_sources(stmt->expr->plan);
+	if (list_length(plansources) != 1)
+		elog(ERROR, "unexpected length of plansources of query for LET statement");
+
+	plansource = (CachedPlanSource *) linitial(plansources);
+	if (list_length(plansource->query_list) != 1)
+		elog(ERROR, "unexpected length of plansource of query for LET statement");
+
+	varid = linitial_node(Query, plansource->query_list)->resultVariable;
+	if (!OidIsValid(varid))
+		elog(ERROR, "oid of target session variable is not valid");
+
+	SetSessionVariableWithSecurityCheck(varid, value, isNull, valtype);
+
+	return PLPGSQL_RC_OK;
+}
+
 /* ----------
  * exec_assign_expr			Put an expression's result into a variable.
  * ----------
diff --git a/src/pl/plpgsql/src/pl_funcs.c b/src/pl/plpgsql/src/pl_funcs.c
index e0863acb3d..265f93fb7b 100644
--- a/src/pl/plpgsql/src/pl_funcs.c
+++ b/src/pl/plpgsql/src/pl_funcs.c
@@ -288,6 +288,8 @@ plpgsql_stmt_typename(PLpgSQL_stmt *stmt)
 			return "COMMIT";
 		case PLPGSQL_STMT_ROLLBACK:
 			return "ROLLBACK";
+		case PLPGSQL_STMT_LET:
+			return "LET";
 	}
 
 	return "unknown";

+static void

+static void

 stmt_perform	: K_PERFORM
 
+stmt_let		: K_LET
+					{
+						PLpgSQL_stmt_let *new;
+						RawParseMode pmode;
+
index 3936866bb7..e4cb07d6b1 100644
 
 /*
 
+/*
+ * Let statement
+ */
 
index 63706a28cc..a71d246128 100644
index 0000000000..58e85a44ea
+-- should fail due dependency
diff --git a/src/test/regress/expected/session_variables.out b/src/test/regress/expected/session_variables.out
index 0000000000..9435b3c475
+++ b/src/test/regress/expected/session_variables.out
@@ -0,0 +1,754 @@
+ERROR:  permission denied for session variable var1
+ERROR:  permission denied for session variable var1
+-- check output
+\dV+ var1
+                                                            List of variables
+  Schema  | Name |  Type   | Is nullable | Is mutable | Default | Owner | Transactional end action |   Access privileges    | Description 
+----------+------+---------+-------------+------------+---------+-------+--------------------------+------------------------+-------------
+ svartest | var1 | numeric | t           | t          |         | pavel |                          | pavel=SW/pavel        +| 
+          |      |         |             |            |         |       |                          | var_test_role=SW/pavel | 
+ERROR:  permission denied for session variable var1
+ SET SESSION VARIABLE
+ERROR:  permission denied for session variable var3
+ERROR:  permission denied for session variable v2
+-- should fail due dependency
+DETAIL:  session variable v2 depends on type t2
+-- default rights test
+ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON VARIABLES TO var_test_role;
+CREATE VARIABLE public.var2 AS int;
+LET public.var2 = 100;
+SELECT public.var2;
+ var2 
+DROP VARIABLE public.var2;
+DROP OWNED BY var_test_role;
+ SET SESSION VARIABLE
+ERROR:  cannot drop session variable v because other objects depend on it
+DETAIL:  view vv depends on session variable v
+ERROR:  null value is not allowed for NOT NULL session variable "v1"
+DETAIL:  The session variable was not initialized yet.
+ERROR:  null value is not allowed for NOT NULL session variable "v2"
+ERROR:  null value is not allowed for NOT NULL session variable "v1"
+ERROR:  null value is not allowed for NOT NULL session variable "v2"
+ERROR:  null value is not allowed for NOT NULL session variable "v1"
+ERROR:  null value is not allowed for NOT NULL session variable "v2"
+NOTICE:  session variable "tv" already exists, skipping
+ERROR:  session variable "iv" is declared IMMUTABLE
+-- different order
+-- should to fail
+ERROR:  session variable "iv" is declared IMMUTABLE
+CREATE IMMUTABLE VARIABLE iv AS int;
+LET iv = NULL;
+LET iv = NULL;
+LET iv = 10;
+LET iv = 10;
+ERROR:  session variable "iv" is declared IMMUTABLE
+-- creating, dropping temporal variable
+CREATE TEMP VARIABLE tempvar AS INT ON COMMIT DROP;
+LET tempvar = 100;
+SAVEPOINT s1;
+DROP VARIABLE tempvar;
+ROLLBACK TO s1;
+SELECT tempvar;
+ tempvar 
+-- should to fail
+LET tempvar = 100;
+ERROR:  session variable "tempvar" doesn't exist
+LINE 1: LET tempvar = 100;
+SAVEPOINT s1;
+CREATE TEMP VARIABLE tempvar AS INT ON COMMIT DROP;
+LET tempvar = 100;
+ROLLBACK TO s1;
+-- should to fail
+LET tempvar = 100;
+ERROR:  session variable "tempvar" doesn't exist
+LINE 1: LET tempvar = 100;
+CREATE VARIABLE var1 AS int;
+LET var1 = 100;
+CREATE VARIABLE var1 AS int DEFAULT 100;
+COMMENT ON VARIABLE var1 IS 'some variable comment';
+\dV+ var1
+                                                               List of variables
+  Schema  | Name |  Type   | Is nullable | Is mutable | Default | Owner | Transactional end action | Access privileges |      Description      
+----------+------+---------+-------------+------------+---------+-------+--------------------------+-------------------+-----------------------
+ svartest | var1 | integer | t           | t          | 100     | pavel |                          |                   | some variable comment
index 5b0c73d7e3..d9ad9c801b 100644
@@ -119,7 +119,7 @@ test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath
+test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml session_variables
 
 # ----------
diff --git a/src/test/regress/sql/session_variables.sql b/src/test/regress/sql/session_variables.sql
index 0000000000..09d3a30f71
+++ b/src/test/regress/sql/session_variables.sql
@@ -0,0 +1,534 @@
+-- check output
+\dV+ var1
+-- should fail due dependency
+-- default rights test
+ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON VARIABLES TO var_test_role;
+CREATE VARIABLE public.var2 AS int;
+LET public.var2 = 100;
+SELECT public.var2;
+DROP VARIABLE public.var2;
+DROP OWNED BY var_test_role;
+-- different order
+-- should to fail
+CREATE IMMUTABLE VARIABLE iv AS int;
+LET iv = NULL;
+LET iv = NULL;
+LET iv = 10;
+LET iv = 10;
+-- creating, dropping temporal variable
+BEGIN;
+
+CREATE TEMP VARIABLE tempvar AS INT ON COMMIT DROP;
+
+LET tempvar = 100;
+
+SAVEPOINT s1;
+
+DROP VARIABLE tempvar;
+
+ROLLBACK TO s1;
+
+SELECT tempvar;
+
+COMMIT;
+
+-- should to fail
+LET tempvar = 100;
+
+BEGIN;
+
+SAVEPOINT s1;
+
+CREATE TEMP VARIABLE tempvar AS INT ON COMMIT DROP;
+
+LET tempvar = 100;
+
+ROLLBACK TO s1;
+
+COMMIT;
+
+-- should to fail
+LET tempvar = 100;
+
+CREATE VARIABLE var1 AS int;
+LET var1 = 100;
+BEGIN;
+DROP VARIABLE var1;
+ROLLBACK;
+SELECT var1;
+
+DROP VARIABLE var1;
+
+CREATE VARIABLE var1 AS int DEFAULT 100;
+COMMENT ON VARIABLE var1 IS 'some variable comment';
+
+\dV+ var1
+
+DROP VARIABLE var1;
+

---

From: Erik Rijkers <[email-masked]>
Date: Sun, 19 Dec 2021 08:09:29 +0100

> [0001-schema-variables-20211219.patch]
 > [0002-schema-variables-20211219.patch]

Hi Pavel,

I get an error during test 'session_variables'.

(on the upside, my own little testsuite runs without error)

thanks,

Erik Rijkers

diff -U3 /home/aardvark/pg_stuff/pg_sandbox/pgsql.schema_variables/src/test/regress/expected/session_variables.out /home/aardvark/pg_stuff/pg_sandbox/pgsql.schema_variables/src/test/regress/results/session_variables.out
--- /home/aardvark/pg_stuff/pg_sandbox/pgsql.schema_variables/src/test/regress/expected/session_variables.out	2021-12-19 07:48:40.422821574 +0100
+++ /home/aardvark/pg_stuff/pg_sandbox/pgsql.schema_variables/src/test/regress/results/session_variables.out	2021-12-19 08:00:29.286820957 +0100
@@ -32,11 +32,11 @@
 SET ROLE TO DEFAULT;
 -- check output
 \dV+ var1
-                                                            List of variables
-  Schema  | Name |  Type   | Is nullable | Is mutable | Default | Owner | Transactional end action |   Access privileges    | Description 
-----------+------+---------+-------------+------------+---------+-------+--------------------------+------------------------+-------------
- svartest | var1 | numeric | t           | t          |         | pavel |                          | pavel=SW/pavel        +| 
-          |      |         |             |            |         |       |                          | var_test_role=SW/pavel | 
+                                                               List of variables
+  Schema  | Name |  Type   | Is nullable | Is mutable | Default |  Owner   | Transactional end action |     Access privileges     | Description 
+----------+------+---------+-------------+------------+---------+----------+--------------------------+---------------------------+-------------
+ svartest | var1 | numeric | t           | t          |         | aardvark |                          | aardvark=SW/aardvark     +| 
+          |      |         |             |            |         |          |                          | var_test_role=SW/aardvark | 
 (1 row)
 
 REVOKE ALL ON VARIABLE var1 FROM var_test_role;
@@ -745,10 +745,10 @@
 CREATE VARIABLE var1 AS int DEFAULT 100;
 COMMENT ON VARIABLE var1 IS 'some variable comment';
 \dV+ var1
-                                                               List of variables
-  Schema  | Name |  Type   | Is nullable | Is mutable | Default | Owner | Transactional end action | Access privileges |      Description      
-----------+------+---------+-------------+------------+---------+-------+--------------------------+-------------------+-----------------------
- svartest | var1 | integer | t           | t          | 100     | pavel |                          |                   | some variable comment
+                                                                List of variables
+  Schema  | Name |  Type   | Is nullable | Is mutable | Default |  Owner   | Transactional end action | Access privileges |      Description      
+----------+------+---------+-------------+------------+---------+----------+--------------------------+-------------------+-----------------------
+ svartest | var1 | integer | t           | t          | 100     | aardvark |                          |                   | some variable comment
 (1 row)
 
 DROP VARIABLE var1;

---

From: Pavel Stehule <[email-masked]>
Date: Sun, 19 Dec 2021 08:13:59 +0100

> (on the upside, my own little testsuite runs without error)

please, can you send me regress diff?

Regards

Pavel

---

From: Pavel Stehule <[email-masked]>
Date: Sun, 19 Dec 2021 08:17:30 +0100

> please, can you send me regress diff?

I see the problem now, the test contains username, and that is wrong.

Schema | Name | Type | Is nullable | Is mutable | Default | Owner |
Transactional end action | Access privileges | Description
-----------+------+---------+-------------+------------+---------+-------+--------------------------+------------------------+-------------
- svartest | var1 | numeric | t | t | | pavel | | pavel=SW/pavel +|
- | | | | | | | | var_test_role=SW/pavel |
+----------+------+---------+-------------+------------+---------+----------+--------------------------+---------------------------+-------------
+ svartest | var1 | numeric | t | t | | appveyor | | appveyor=SW/appveyor
+|
+ | | | | | | | | var_test_role=SW/appveyor |
(1 row)
REVOKE ALL ON VARIABLE var1 FROM var_test_role;

I have to remove this test

Pavel

Regards

---

From: Erikjan Rijkers <[email-masked]>
Date: Sun, 19 Dec 2021 08:23:43 +0100

Op 19-12-2021 om 08:13 schreef Pavel Stehule:
> 
> 
> ne 19. 12. 2021 v 8:09 odesílatel Erik Rijkers <[email-masked] 
>      >
> 
>       > [0001-schema-variables-20211219.patch]
>       > [0002-schema-variables-20211219.patch]
> 
>     Hi Pavel,
> 

I did attach it but if you did not receive it, see also cfbot, especially

https://ci.appveyor.com/project/postgresql-cfbot/postgresql/build/1.0.156992

Erik

---

From: Pavel Stehule <[email-masked]>
Date: Sun, 19 Dec 2021 08:53:03 +0100

> https://ci.appveyor.com/project/postgresql-cfbot/postgresql/build/1.0.156992

second try

I removed badly written tests

Pavel

 
 select a from atacc1;

 

 

 
 
  </sect1>

 

 
  <refsynopsisdiv>
 <synopsis>

+   <varlistentry>

+    <varlistentry>
 
+     <varlistentry>

 

 

+		case OBJECT_VARIABLE:
 
+static void
 
 static AclMode
 
+/*
 
+/*
 
+/*
 
+		case OBJECT_VARIABLE:

+		case OCLASS_VARIABLE:

 
 	/* shouldn't get here */
 
+/*
+ * VariableIsVisible
 
 /*
  * TypenameGetTypid
 
+/*
 
 /*

 /*

 	/*
 
+/*
 
+		case OCLASS_VARIABLE:
+			{
+				char	   *nspname;
+				HeapTuple	tup;
 
+		case OCLASS_VARIABLE:
 
 				if (objname)
 
+		case OCLASS_VARIABLE:
+			{
+				char	   *schema;
+				char	   *varname;
+				HeapTuple	tup;

 

 			/*
 
 			/*
 
 				/*

 						/*

+	/*

 

 

 
 
 	return newnode;
 
+static LetStmt *

 	return true;
 
+static bool
 
+static bool

+static void

 	READ_DONE();

 	/*

+	/*

+/*

 
 
 				/*

 

 
+		case T_LetStmt:
 
 			/*

 

 

 

 
 
 	return qry;
 }
 
+/*
+ * transformLetStmt -

 

 

 /*

 

 
 /*

 
 /*

 
 opt_column: COLUMN

 

 
 			/*
 
 			if (isAgg)
 
 			/*

 /* GUC parameters */

 /*

 
+/*
 
 			/*
 
 			/*

+	/*
 
 	/*

 		case DestTupleQueue:

 
 		case T_CallStmt:
 		case T_DoStmt:
+		case T_LetStmt:
 			{
 				/*
 
+		case T_LetStmt:

+		case T_LetStmt:
 
+		case T_LetStmt:
+			tag = CMDTAG_LET;
+			break;
+

 		case T_PLAssignStmt:
+		case T_LetStmt:
 			lev = LOGSTMT_ALL;
 			break;

 	ereport(ERROR,

+/*
+ * get_varname_varid

 	/*
 
 	return false;
 }

 
 
 	int			useDB;

+/*
+ * getVariables

 /*
 
+/*
 
 #endif							/* PG_DUMP_H */

 

 

+/*
+ * \dV
+ *
+ * listVariables()
+ */
+bool
 
 /*
  * \dFp
 
+/* \dV */
 
 #endif							/* DESCRIBE_H */

 /*

 /* DISCARD */
 
 /* DO */
 	else if (Matches("DO"))
 
+	/* DROP VARIABLE */

 
 /* PREPARE xx AS */
 
 /*

 

 

 

 
 
+/* ----------------
+ * SessionVariableValue
+ * ----------------
+ */
 
+	/* Variables info: */

 	/*

 

 

 
 
 	/*

 

 

 typedef struct Param

 

 
 /* ----------------

 /*

 

 
 
+			case PLPGSQL_STMT_LET:
 
+/* ----------
 
 	return "unknown";

+static void

+static void

 stmt_perform	: K_PERFORM
 
+stmt_let		: K_LET
+					{
+						PLpgSQL_stmt_let *new;
+						RawParseMode pmode;
+
 
 /*
 
+/*
+ * Let statement
+ */
 
index 0000000000..669727b88b
@@ -0,0 +1,744 @@
+SELECT pg_catalog.obj_description(oid, 'pg_variable') FROM pg_variable WHERE varname = 'var1';
+    obj_description    
+-----------------------
+ some variable comment
 
 # ----------
index 0000000000..658beecd77
@@ -0,0 +1,530 @@
+SELECT pg_catalog.obj_description(oid, 'pg_variable') FROM pg_variable WHERE varname = 'var1';
+
+DROP VARIABLE var1;

---

From: Erik Rijkers <[email-masked]>
Date: Sun, 19 Dec 2021 11:10:52 +0100

> [0001-schema-variables-20211219-2.patch]
 > [0002-schema-variables-20211219-2.patch]

Hi Pavel,

You said earlier
 > 1. The schema variables were renamed to session variable

But I still see:
$ grep -Eic 'schema variable' postgres.html
15

(postgres.html from 'make postgres.html')

So that rename doesn't seem finished.

Erik

---

From: Pavel Stehule <[email-masked]>
Date: Sun, 19 Dec 2021 19:38:29 +0100

Hi

> So that rename doesn't seem finished.

Yes, I forgot some changes, and more, there was a bogus regress result
file. Thank you for rechecking.

I am sending cleaned patches

Regards

Pavel

diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c
index c5c3f26ecf..ec175dc96c 100644
--- a/src/backend/parser/parse_relation.c
+++ b/src/backend/parser/parse_relation.c
@@ -3589,8 +3589,8 @@ errorMissingColumn(ParseState *pstate,
 		ereport(ERROR,
 				(errcode(ERRCODE_UNDEFINED_COLUMN),
 				 relname ?
-				 errmsg("column %s.%s does not exist", relname, colname) :
-				 errmsg("column \"%s\" does not exist", colname),
+				 errmsg("column or variable %s.%s does not exist", relname, colname) :
+				 errmsg("column or variable \"%s\" does not exist", colname),
 				 state->rfirst ? closestfirst ?
 				 errhint("Perhaps you meant to reference the column \"%s.%s\".",
 						 state->rfirst->eref->aliasname, closestfirst) :
@@ -3609,8 +3609,8 @@ errorMissingColumn(ParseState *pstate,
 		ereport(ERROR,
 				(errcode(ERRCODE_UNDEFINED_COLUMN),
 				 relname ?
-				 errmsg("column %s.%s does not exist", relname, colname) :
-				 errmsg("column \"%s\" does not exist", colname),
+				 errmsg("column or variable %s.%s does not exist", relname, colname) :
+				 errmsg("column or variable  \"%s\" does not exist", colname),
 				 errhint("Perhaps you meant to reference the column \"%s.%s\" or the column \"%s.%s\".",
 						 state->rfirst->eref->aliasname, closestfirst,
 						 state->rsecond->eref->aliasname, closestsecond),
diff --git a/src/pl/tcl/expected/pltcl_queries.out b/src/pl/tcl/expected/pltcl_queries.out
index 2d922c2333..3b6506d613 100644
--- a/src/pl/tcl/expected/pltcl_queries.out
+++ b/src/pl/tcl/expected/pltcl_queries.out
@@ -246,12 +246,12 @@ ERROR:  type "b" does not exist
 select tcl_eval('spi_prepare a "b {"');
 ERROR:  unmatched open brace in list
 select tcl_error_handling_test($tcl$spi_prepare "select moo" []$tcl$);
-       tcl_error_handling_test        
---------------------------------------
- SQLSTATE: 42703                     +
- condition: undefined_column         +
- cursor_position: 8                  +
- message: column "moo" does not exist+
+             tcl_error_handling_test              
+--------------------------------------------------
+ SQLSTATE: 42703                                 +
+ condition: undefined_column                     +
+ cursor_position: 8                              +
+ message: column or variable "moo" does not exist+
  statement: select moo
 (1 row)

 
 select a from atacc1;

 

 

 
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 03e2537b07..fd4a8f58a9 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -364,6 +364,11 @@
       <entry><link linkend="catalog-pg-user-mapping"><structname>pg_user_mapping</structname></link></entry>
       <entry>mappings of users to foreign servers</entry>
      </row>
+
+     <row>
+      <entry><link linkend="catalog-pg-variable"><structname>pg_variable</structname></link></entry>
+      <entry>session variables</entry>
+     </row>
     </tbody>
    </tgroup>
   </table>
@@ -13975,4 +13980,143 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
 
  </sect1>
 
+   about session variables.
+      <entry>Name of the session variable</entry>
+       True if the session variable doesn't allow null value. The default value is false.
 
index f1d54f5aa3..7eb1e8cf8b 100644

+   tables), sequences, functions, types (including domains) and session variables
index 0000000000..50b1f6a2d7
+   Only the owner or a superuser is allowed to alter a session variable.
+   In order to move a session variable from one schema to another, the user must
+   In order to move the session variable ownership from one role to another,
+   Session variables and this command in particular are a PostgreSQL extension.
 
index 0000000000..38b8a2fbc6
+   Session variables are retrieved by the <command>SELECT</command> SQL command.
 
  <refsynopsisdiv>
 <synopsis>

+   <varlistentry>
index a897712de2..74adf80331 100644

+    <varlistentry>
+       Allows reading of a session variable.
+       Allows setting the value of a session variable.
index 93ea937ac8..54d14db423 100644
 
+     <varlistentry>
+        Restore a named session variable only.  Multiple session variables may be specified with

 

 

+		case OBJECT_VARIABLE:
 
+static void
 
 static AclMode
 
+/*
 
+/*
 
+/*
 
+		case OBJECT_VARIABLE:

+		case OCLASS_VARIABLE:

 
 	/* shouldn't get here */
 
+/*
+ * VariableIsVisible
 
 /*
  * TypenameGetTypid
 
+/*
 
 /*

 /*

 	/*
 
+/*
 
+		case OCLASS_VARIABLE:
+			{
+				char	   *nspname;
+				HeapTuple	tup;
 
+		case OCLASS_VARIABLE:
 
 				if (objname)
 
+		case OCLASS_VARIABLE:
+			{
+				char	   *schema;
+				char	   *varname;
+				HeapTuple	tup;

 

 			/*
 
 			/*
 
 				/*

 						/*

+	/*

 

 

 
 
 	return newnode;
 
+static LetStmt *

 	return true;
 
+static bool
 
+static bool

+static void

 	READ_DONE();

 	/*

+	/*

+/*

 
 
 				/*

 

 
+		case T_LetStmt:
 
 			/*

 

 

 

 
 
 	return qry;
 }
 
+/*
+ * transformLetStmt -

 

 

 /*

 

 
 /*

 
 /*

 
 opt_column: COLUMN

 

 
 			/*
 
 			if (isAgg)
 
 			/*

 /* GUC parameters */

 /*

 
+/*
 
 			/*
 
 			/*

+	/*
 
 	/*

 		case DestTupleQueue:

 
 		case T_CallStmt:
 		case T_DoStmt:
+		case T_LetStmt:
 			{
 				/*
 
+		case T_LetStmt:

+		case T_LetStmt:
 
+		case T_LetStmt:
+			tag = CMDTAG_LET;
+			break;
+

 		case T_PLAssignStmt:
+		case T_LetStmt:
 			lev = LOGSTMT_ALL;
 			break;

 	ereport(ERROR,

+/*
+ * get_varname_varid

 	/*
 
 	return false;
 }

 
 
 	int			useDB;

+/*
+ * getVariables

 /*
 
+/*
 
 #endif							/* PG_DUMP_H */

 

 

+/*
+ * \dV
+ *
+ * listVariables()
+ */
+bool
 
 /*
  * \dFp
 
+/* \dV */
 
 #endif							/* DESCRIBE_H */

 /*

 /* DISCARD */
 
 /* DO */
 	else if (Matches("DO"))
 
+	/* DROP VARIABLE */

 
 /* PREPARE xx AS */
 
 /*

 

 

 

 
 
+/* ----------------
+ * SessionVariableValue
+ * ----------------
+ */
 
+	/* Variables info: */

 	/*

 

 

 
 
 	/*

 

 

 typedef struct Param

 

 
 /* ----------------

 /*

 

 
 
+			case PLPGSQL_STMT_LET:
 
+/* ----------
 
 	return "unknown";

+static void

+static void

 stmt_perform	: K_PERFORM
 
+stmt_let		: K_LET
+					{
+						PLpgSQL_stmt_let *new;
+						RawParseMode pmode;
+
 
 /*
 
+/*
+ * Let statement
+ */

 # ----------

---

From: Justin Pryzby <[email-masked]>
Date: Mon, 20 Dec 2021 17:09:12 -0600

I don't understand what 0002 patch does relative to the 0001 patch.
Is 0002 to change the error messages from "schema variables" to "session
variables" , in a separate commit to show that the main patch doesn't change
regression results ?  Could you add commit messages ?

I mentioned before that there's a pre-existing use of the phrase "session
variable", which you should change to something else:

origin:doc/src/sgml/ref/set_role.sgml:   <command>SET ROLE</command> does not process session variables as specified by
origin:doc/src/sgml/ref/set_role.sgml-   the role's <link linkend="sql-alterrole"><command>ALTER ROLE</command></link> settings;  this only happens during
origin:doc/src/sgml/ref/set_role.sgml-   login.

Maybe "session variable" should be added to the glossary.

The new tests crash if debug_discard_caches=on.

2021-12-20 16:15:44.476 CST postmaster[7478] LOG:  server process (PID 7657) was terminated by signal 6: Aborted
2021-12-20 16:15:44.476 CST postmaster[7478] DETAIL:  Failed process was running: DISCARD VARIABLES;

TRAP: FailedAssertion("sessionvars", File: "sessionvariable.c", Line: 270, PID: 7657)

#2  0x0000564858a4f1a8 in ExceptionalCondition (conditionName=conditionName@entry=0x564858b8626d "sessionvars", errorType=errorType@entry=0x564858aa700b "FailedAssertion", 
    fileName=fileName@entry=0x564858b86234 "sessionvariable.c", lineNumber=lineNumber@entry=270) at assert.c:69
#3  0x000056485874fec6 in sync_sessionvars_xact_callback (event=<optimized out>, arg=<optimized out>) at sessionvariable.c:270
#4  sync_sessionvars_xact_callback (event=<optimized out>, arg=<optimized out>) at sessionvariable.c:253
#5  0x000056485868030a in CallXactCallbacks (event=XACT_EVENT_PRE_COMMIT) at xact.c:3644
#6  CommitTransaction () at xact.c:2178
#7  0x0000564858681975 in CommitTransactionCommand () at xact.c:3043
#8  0x000056485892b7a9 in finish_xact_command () at postgres.c:2722
#9  0x000056485892dc5b in finish_xact_command () at postgres.c:2720
#10 exec_simple_query () at postgres.c:1240
#11 0x000056485892f70a in PostgresMain () at postgres.c:4498
#12 0x000056485889a479 in BackendRun (port=<optimized out>, port=<optimized out>) at postmaster.c:4594
#13 BackendStartup (port=<optimized out>) at postmaster.c:4322
#14 ServerLoop () at postmaster.c:1802
#15 0x000056485889b47c in PostmasterMain () at postmaster.c:1474
#16 0x00005648585c60c0 in main (argc=5, argv=0x564858e553f0) at main.c:198

---

From: Pavel Stehule <[email-masked]>
Date: Tue, 21 Dec 2021 13:29:00 +0100

Hi

út 21. 12. 2021 v 0:09 odesílatel Justin Pryzby <[email-masked]>
napsal:

> origin:doc/src/sgml/ref/set_role.sgml:   <command>SET ROLE</command> does
> not process session variables as specified by
> origin:doc/src/sgml/ref/set_role.sgml-   the role's <link
> linkend="sql-alterrole"><command>ALTER ROLE</command></link> settings;
> this only happens during
> 2021-12-20 16:15:44.476 CST postmaster[7478] LOG:  server process (PID
> 7657) was terminated by signal 6: Aborted

How do you inject this parameter to regress tests?

Regards

Pavel

> #2  0x0000564858a4f1a8 in ExceptionalCondition
> (conditionName=conditionName@entry=0x564858b8626d "sessionvars",
> errorType=errorType@entry=0x564858aa700b "FailedAssertion",
>     fileName=fileName@entry=0x564858b86234 "sessionvariable.c",
> lineNumber=lineNumber@entry=270) at assert.c:69
> #3  0x000056485874fec6 in sync_sessionvars_xact_callback (event=<optimized
> out>, arg=<optimized out>) at sessionvariable.c:270
> #12 0x000056485889a479 in BackendRun (port=<optimized out>,
> port=<optimized out>) at postmaster.c:4594

---

From: Justin Pryzby <[email-masked]>
Date: Tue, 21 Dec 2021 06:36:34 -0600

> How do you inject this parameter to regress tests?

You can run PGOPTIONS='-c debug_invalidate_caches=1' make check

I used make installcheck against a running instance where I'd used
ALTER SYSTEM SET debug_discard_caches=on.

You can also manually run psql against the .sql file itself.
...which is a good idea since this causes the regression tests take hours.

Or just add SET debug_discard_caches=on to your .sql file.

-- 
Justin

---

From: Pavel Stehule <[email-masked]>
Date: Tue, 21 Dec 2021 13:39:45 +0100

út 21. 12. 2021 v 13:36 odesílatel Justin Pryzby <[email-masked]>
napsal:

> > > 2021-12-20 16:15:44.476 CST postmaster[7478] DETAIL:  Failed process
> was
> > > running: DISCARD VARIABLES;
> >

ok thank you

I'll try it.

---

From: Pavel Stehule <[email-masked]>
Date: Wed, 22 Dec 2021 06:21:41 +0100

> regression results ?  Could you add commit messages ?
done

> origin:doc/src/sgml/ref/set_role.sgml-   login.

changed

> Maybe "session variable" should be added to the glossary.

done

> #16 0x00005648585c60c0 in main (argc=5, argv=0x564858e553f0) at main.c:198

attached version was ok with this setting

Regards

Pavel
From 791273029f4fdd8b28df4ac4b9b141a1db500b69 Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Wed, 22 Dec 2021 05:47:25 +0100
Subject: [PATCH 2/2] This patch changes error message "column doesn't exist"
 to message "column or variable doesn't exist". The error message will be more
 correct. Today, missing PL/pgSQL variable can be reported. The change has
 impact on lot of regress tests not related to session variables, and then it
 is distributed as separate patch.

---
 src/backend/parser/parse_relation.c           |  8 ++--
 src/pl/tcl/expected/pltcl_queries.out         | 12 +++---
 src/test/regress/expected/alter_table.out     | 42 +++++++++----------
 src/test/regress/expected/copy2.out           |  2 +-
 src/test/regress/expected/errors.out          |  8 ++--
 src/test/regress/expected/generated.out       |  4 +-
 src/test/regress/expected/insert_conflict.out |  6 +--
 src/test/regress/expected/join.out            | 20 ++++-----
 src/test/regress/expected/plpgsql.out         | 12 +++---
 src/test/regress/expected/rowtypes.out        |  2 +-
 src/test/regress/expected/rules.out           |  4 +-
 src/test/regress/expected/transactions.out    |  4 +-
 src/test/regress/expected/union.out           |  2 +-
 13 files changed, 63 insertions(+), 63 deletions(-)

 
 
 select a from atacc1;

 

 
 
2.33.1

From 4037b4fc560d1d146e91cca09f4c3a06d39f851d Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Wed, 22 Dec 2021 05:39:15 +0100
Subject: [PATCH 1/2] This patch implements supports for session variables.
 This is new class of database objects. Implemented session variables has
 persistent (or temporary) entry in system catalog (like tables, sequences,
 ...), but the value is stored in session (unshared) memory. The access rights
 (read, write) can be granted to others (not owner) users.

The session variables are not transactional like variables in PL/pgSQL or
any session variables in other database systems (package variables, schema
variables, ...).

This patch introduce new DDL commands: CREATE VARIABLE, DROP VARIABLE and DDL command
LET. Commands GRANT, REVOKE, DISCARD, ALTER are enhanced to support session
variables too.
---
 doc/src/sgml/advanced.sgml                    |   51 +
 doc/src/sgml/catalogs.sgml                    |  144 +++
 doc/src/sgml/glossary.sgml                    |   16 +
 doc/src/sgml/plpgsql.sgml                     |   13 +
 doc/src/sgml/ref/allfiles.sgml                |    4 +
 .../sgml/ref/alter_default_privileges.sgml    |   14 +-
 doc/src/sgml/ref/alter_variable.sgml          |  179 +++
 doc/src/sgml/ref/comment.sgml                 |    1 +
 doc/src/sgml/ref/create_variable.sgml         |  193 +++
 doc/src/sgml/ref/discard.sgml                 |   14 +-
 doc/src/sgml/ref/drop_variable.sgml           |  120 ++
 doc/src/sgml/ref/grant.sgml                   |   23 +
 doc/src/sgml/ref/let.sgml                     |  107 ++
 doc/src/sgml/ref/pg_restore.sgml              |   11 +
 doc/src/sgml/ref/revoke.sgml                  |    6 +
 doc/src/sgml/ref/set_role.sgml                |    2 +-
 doc/src/sgml/reference.sgml                   |    4 +
 src/backend/access/transam/xact.c             |   11 +
 src/backend/catalog/Makefile                  |    4 +-
 src/backend/catalog/aclchk.c                  |  307 +++++
 src/backend/catalog/dependency.c              |   18 +-
 src/backend/catalog/namespace.c               |  360 ++++++
 src/backend/catalog/objectaddress.c           |  126 +-
 src/backend/catalog/pg_shdepend.c             |    2 +
 src/backend/catalog/pg_variable.c             |  387 ++++++
 src/backend/commands/Makefile                 |    1 +
 src/backend/commands/alter.c                  |    9 +
 src/backend/commands/discard.c                |    6 +
 src/backend/commands/dropcmds.c               |    4 +
 src/backend/commands/event_trigger.c          |    6 +
 src/backend/commands/explain.c                |   16 +
 src/backend/commands/seclabel.c               |    1 +
 src/backend/commands/sessionvariable.c        | 1094 +++++++++++++++++
 src/backend/commands/tablecmds.c              |    1 +
 src/backend/executor/Makefile                 |    1 +
 src/backend/executor/execExpr.c               |   55 +
 src/backend/executor/execExprInterp.c         |   12 +
 src/backend/executor/execMain.c               |   49 +
 src/backend/executor/execParallel.c           |  148 ++-
 src/backend/executor/spi.c                    |    3 +
 src/backend/executor/svariableReceiver.c      |  145 +++
 src/backend/jit/llvm/llvmjit_expr.c           |    6 +
 src/backend/nodes/copyfuncs.c                 |   41 +
 src/backend/nodes/equalfuncs.c                |   36 +
 src/backend/nodes/outfuncs.c                  |   22 +
 src/backend/nodes/readfuncs.c                 |    4 +
 src/backend/optimizer/plan/planner.c          |    8 +
 src/backend/optimizer/plan/setrefs.c          |  103 +-
 src/backend/optimizer/util/clauses.c          |   69 +-
 src/backend/parser/analyze.c                  |  282 ++++-
 src/backend/parser/gram.y                     |  212 +++-
 src/backend/parser/parse_agg.c                |    6 +
 src/backend/parser/parse_expr.c               |  165 +++
 src/backend/parser/parse_func.c               |    4 +
 src/backend/parser/parse_target.c             |    4 +-
 src/backend/parser/parser.c                   |    3 +-
 src/backend/rewrite/rewriteHandler.c          |   34 +
 src/backend/rewrite/rowsecurity.c             |    8 +-
 src/backend/tcop/dest.c                       |    7 +
 src/backend/tcop/pquery.c                     |    3 +
 src/backend/tcop/utility.c                    |   34 +
 src/backend/utils/adt/acl.c                   |   21 +
 src/backend/utils/adt/ruleutils.c             |    9 +
 src/backend/utils/cache/lsyscache.c           |   13 +
 src/backend/utils/cache/syscache.c            |   23 +
 src/backend/utils/fmgr/fmgr.c                 |   16 +-
 src/bin/pg_dump/common.c                      |    3 +-
 src/bin/pg_dump/dumputils.c                   |    5 +
 src/bin/pg_dump/pg_backup.h                   |    2 +
 src/bin/pg_dump/pg_backup_archiver.c          |   12 +-
 src/bin/pg_dump/pg_dump.c                     |  216 +++-
 src/bin/pg_dump/pg_dump.h                     |   24 +-
 src/bin/pg_dump/pg_dump_sort.c                |    6 +
 src/bin/pg_dump/pg_restore.c                  |    9 +-
 src/bin/pg_dump/t/002_pg_dump.pl              |   65 +
 src/bin/psql/command.c                        |    3 +
 src/bin/psql/describe.c                       |   91 ++
 src/bin/psql/describe.h                       |    2 +
 src/bin/psql/help.c                           |    3 +-
 src/bin/psql/tab-complete.c                   |   67 +-
 src/include/catalog/dependency.h              |    5 +-
 src/include/catalog/namespace.h               |    6 +
 src/include/catalog/pg_default_acl.h          |    1 +
 src/include/catalog/pg_proc.dat               |    3 +
 src/include/catalog/pg_variable.h             |  117 ++
 src/include/commands/session_variable.h       |   44 +
 src/include/executor/execExpr.h               |   10 +
 src/include/executor/execdesc.h               |    4 +
 src/include/executor/svariableReceiver.h      |   25 +
 src/include/nodes/execnodes.h                 |   19 +
 src/include/nodes/nodes.h                     |    2 +
 src/include/nodes/parsenodes.h                |   43 +-
 src/include/nodes/pathnodes.h                 |    3 +
 src/include/nodes/plannodes.h                 |    4 +-
 src/include/nodes/primnodes.h                 |   19 +-
 src/include/optimizer/planmain.h              |    2 +
 src/include/parser/kwlist.h                   |    3 +
 src/include/parser/parse_node.h               |    3 +
 src/include/parser/parser.h                   |    6 +-
 src/include/tcop/cmdtaglist.h                 |    5 +
 src/include/tcop/dest.h                       |    3 +-
 src/include/utils/acl.h                       |   10 +-
 src/include/utils/lsyscache.h                 |    1 +
 src/include/utils/syscache.h                  |    6 +-
 src/pl/plpgsql/src/pl_exec.c                  |   55 +
 src/pl/plpgsql/src/pl_funcs.c                 |   24 +
 src/pl/plpgsql/src/pl_gram.y                  |   28 +-
 src/pl/plpgsql/src/pl_reserved_kwlist.h       |    1 +
 src/pl/plpgsql/src/plpgsql.h                  |   14 +-
 src/test/regress/expected/misc_sanity.out     |    4 +-
 src/test/regress/expected/sanity_check.out    |    1 +
 .../regress/expected/session_variables.out    |  744 +++++++++++
 src/test/regress/parallel_schedule            |    2 +-
 src/test/regress/sql/session_variables.sql    |  530 ++++++++
 114 files changed, 6978 insertions(+), 78 deletions(-)
 create mode 100644 doc/src/sgml/ref/alter_variable.sgml
 create mode 100644 doc/src/sgml/ref/create_variable.sgml
 create mode 100644 doc/src/sgml/ref/drop_variable.sgml
 create mode 100644 doc/src/sgml/ref/let.sgml
 create mode 100644 src/backend/catalog/pg_variable.c
 create mode 100644 src/backend/commands/sessionvariable.c
 create mode 100644 src/backend/executor/svariableReceiver.c
 create mode 100644 src/include/catalog/pg_variable.h
 create mode 100644 src/include/commands/session_variable.h
 create mode 100644 src/include/executor/svariableReceiver.h
 create mode 100644 src/test/regress/expected/session_variables.out
 create mode 100644 src/test/regress/sql/session_variables.sql

 
 
  </sect1>
 
diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml
index 0c88988fa6..110512ffa1 100644
--- a/doc/src/sgml/glossary.sgml
+++ b/doc/src/sgml/glossary.sgml
@@ -1463,6 +1463,22 @@
    </glossdef>
   </glossentry>
 
+  <glossentry id="glossary-session-variable">
+   <glossterm>Session variable</glossterm>
+   <glossdef>
+    <para>
+     A persistent database object that holds an value in session memory.
+     This memory is not shared across sessions, and after session end, this
+     memory (the value) is released. The access (read or write) to session variables
+     is controlled by access rigths similary to other database object access rigts.
+    </para>
+    <para>
+     For more information, see
+     <xref linkend="tutorial-session-variables"/>.
+    </para>
+   </glossdef>
+  </glossentry>
+
   <glossentry id="glossary-shared-memory">
    <glossterm>Shared memory</glossterm>
    <glossdef>
diff --git a/doc/src/sgml/plpgsql.sgml b/doc/src/sgml/plpgsql.sgml
index e5c1356d8c..423677b9af 100644
--- a/doc/src/sgml/plpgsql.sgml
+++ b/doc/src/sgml/plpgsql.sgml
@@ -5952,6 +5952,19 @@ $$ LANGUAGE plpgsql STRICT IMMUTABLE;
 </programlisting>
     </para>
    </sect3>
+
+   <sect3>
+    <title><command>Session variables and constants</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> described in <xref
+     linkend="sql-createvariable"/>.
+    </para>
+   </sect3>
   </sect2>

 

  <refsynopsisdiv>
 <synopsis>

+   <varlistentry>

+    <varlistentry>
 
+     <varlistentry>
 
diff --git a/doc/src/sgml/ref/set_role.sgml b/doc/src/sgml/ref/set_role.sgml
index f02babf3af..911d17369c 100644
--- a/doc/src/sgml/ref/set_role.sgml
+++ b/doc/src/sgml/ref/set_role.sgml
@@ -98,7 +98,7 @@ RESET ROLE
   </para>
 
   <para>
-   <command>SET ROLE</command> does not process session variables as specified by
+   <command>SET ROLE</command> does not set run-time configuration parameters specified by
    the role's <link linkend="sql-alterrole"><command>ALTER ROLE</command></link> settings;  this only happens during
    login.
   </para>

 

 
 
+		case OBJECT_VARIABLE:
 
+static void
 
 static AclMode
 
+/*
 
+/*
 
+/*
 
+		case OBJECT_VARIABLE:

+		case OCLASS_VARIABLE:

 
 	/* shouldn't get here */
 
+/*
+ * VariableIsVisible
 
 /*
  * TypenameGetTypid
 
+/*
 
 /*

 /*

 	/*
 
+/*
 
+		case OCLASS_VARIABLE:
+			{
+				char	   *nspname;
+				HeapTuple	tup;
 
+		case OCLASS_VARIABLE:
 
 				if (objname)
 
+		case OCLASS_VARIABLE:
+			{
+				char	   *schema;
+				char	   *varname;
+				HeapTuple	tup;

 

 			/*
 
 			/*
index 0000000000..c16b6cf65f
@@ -0,0 +1,1094 @@
+	 * sessionvars is null after DISCARD VARIABLES.
+	 * When we are sure, so there are not any
+	 * active session variable in this session, we
+	 * can reset sync_sessionvars_all flag.
+	 * transaction state - XACT_EVENT_ABORT, XACT_EVENT_COMMIT
+	 * but the checks against system catalog needs transaction state.
 
 				/*

 						/*

+	/*

 

 

 
 
 	return newnode;
 
+static LetStmt *

 	return true;
 
+static bool
 
+static bool

+static void

 	READ_DONE();

 	/*

+	/*

+/*

 
 
 				/*

 

 
+		case T_LetStmt:
 
 			/*

 

 

 

 
 
 	return qry;
 }
 
+/*
+ * transformLetStmt -

 

 

 /*

 

 
 /*

 
 /*

 
 opt_column: COLUMN

 

 
 			/*
 
 			if (isAgg)
 
 			/*

 /* GUC parameters */

 /*

 
+/*
 
 			/*
 
 			/*

+	/*
 
 	/*

 		case DestTupleQueue:

 
 		case T_CallStmt:
 		case T_DoStmt:
+		case T_LetStmt:
 			{
 				/*
 
+		case T_LetStmt:

+		case T_LetStmt:
 
+		case T_LetStmt:
+			tag = CMDTAG_LET;
+			break;
+

 		case T_PLAssignStmt:
+		case T_LetStmt:
 			lev = LOGSTMT_ALL;
 			break;

 	ereport(ERROR,

+/*
+ * get_varname_varid

 	/*
 
 	return false;
 }

index a2b9fe7e35..6577f5d485 100644
@@ -506,6 +506,11 @@ do { \

 	int			useDB;
 
index b52f3ccda2..b80ef618a8 100644
 
+/*
+ * getVariables
@@ -9084,7 +9290,8 @@ getAdditionalACLs(Archive *fout)
 
@@ -9676,6 +9883,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
 		case DO_SUBSCRIPTION:
@@ -14044,6 +14254,9 @@ dumpDefaultACL(Archive *fout, const DefaultACLInfo *daclinfo)
@@ -17588,6 +17801,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 
 /*
 
+/*
 
 #endif							/* PG_DUMP_H */

 

 
 
index fb3bab9494..dad39ab656 100644
@@ -933,6 +933,9 @@ exec_command_d(PsqlScanState scan_state, bool active_branch, const char *cmd)
 
+/*
+ * \dV
+ *
+ * listVariables()
+ */
+bool
 
 /*
  * \dFp
 
+/* \dV */
 
 #endif							/* DESCRIBE_H */
diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c
index 8cadfbb103..576629c4d9 100644
--- a/src/bin/psql/help.c
+++ b/src/bin/psql/help.c
@@ -166,7 +166,7 @@ slashUsage(unsigned short int pager)
 	 * Use "psql --help=commands | wc" to count correctly.  It's okay to count
 	 * the USE_READLINE line even in builds without that.
 	 */
-	output = PageOutput(137, pager ? &(pset.popt.topt) : NULL);
+	output = PageOutput(138, pager ? &(pset.popt.topt) : NULL);
 
 	fprintf(output, _("General\n"));
 	fprintf(output, _("  \\copyright             show PostgreSQL usage and distribution terms\n"));
index cf30239f6d..c753780c13 100644

 /*

 /* DISCARD */
 
 /* DO */
 	else if (Matches("DO"))
 
+	/* DROP VARIABLE */

 
 /* PREPARE xx AS */
 
 /*

 

 

 

 
 
+/* ----------------
+ * SessionVariableValue
+ * ----------------
+ */
 
+	/* Variables info: */

 	/*

 

 

 
 
 	/*

 

 

 typedef struct Param

 

 
 /* ----------------

 /*

 

 
 
+			case PLPGSQL_STMT_LET:
 
+/* ----------
 
 	return "unknown";

+static void

+static void

 stmt_perform	: K_PERFORM
 
+stmt_let		: K_LET
+					{
+						PLpgSQL_stmt_let *new;
+						RawParseMode pmode;
+
 
 /*
 
+/*
+ * Let statement
+ */

 # ----------
2.33.1

---

From: Pavel Stehule <[email-masked]>
Date: Sat, 25 Dec 2021 18:20:36 +0100

Hi

new update - I found an error in checking ambiguous columns - the tupdesc
was badly released by FreeTupleDesc. I fixed this issue and did a new
related regress test to cover this path.

Regards

Nice holidays

Pavel
From 9bfbdd7a73ec7e13ea066f79ca2c30aa9a79ede0 Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Sat, 25 Dec 2021 18:13:01 +0100
Subject: [PATCH 2/2] Column doesn't exists message

This patch changes error message "column doesn't exist"

 
 
 select a from atacc1;

 

 

From 7f3a43312a19643e70e54695caa3e7889feef2e3 Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Sat, 25 Dec 2021 12:17:48 +0100
Subject: [PATCH 1/2] session variables

 .../regress/expected/session_variables.out    |  786 ++++++++++++
 src/test/regress/sql/session_variables.sql    |  562 +++++++++
 114 files changed, 7052 insertions(+), 78 deletions(-)

 
 
  </sect1>

 

 
 
  <refsynopsisdiv>
 <synopsis>

+   <varlistentry>

+    <varlistentry>
 
+     <varlistentry>

   <para>

 

 
 
+		case OBJECT_VARIABLE:
 
+static void
 
 static AclMode
 
+/*
 
+/*
 
+/*
 
+		case OBJECT_VARIABLE:

+		case OCLASS_VARIABLE:

 
 	/* shouldn't get here */
 
+/*
+ * VariableIsVisible
 
 /*
  * TypenameGetTypid
 
+/*
 
 /*

 /*

 	/*
 
+/*
 
+		case OCLASS_VARIABLE:
+			{
+				char	   *nspname;
+				HeapTuple	tup;
 
+		case OCLASS_VARIABLE:
 
 				if (objname)
 
+		case OCLASS_VARIABLE:
+			{
+				char	   *schema;
+				char	   *varname;
+				HeapTuple	tup;
index 48f7348f91..41f98f8ba0 100644

 

 			/*
 
 			/*
index 45e59e3d5c..d95a92d2f1 100644
 
 				/*

 						/*

+	/*

 

 

 
 
 	return newnode;
 
+static LetStmt *

 	return true;
 
+static bool
 
+static bool

+static void
 
index d79af6e56e..d9cc20c2fb 100644
@@ -252,6 +252,7 @@ _readQuery(void)
@@ -262,6 +263,7 @@ _readQuery(void)
@@ -626,6 +628,7 @@ _readParam(void)
 
 	READ_DONE();
@@ -1597,6 +1600,7 @@ _readPlannedStmt(void)

 	/*

+	/*

+/*

 
 
 				/*

 

 
+		case T_LetStmt:
 
 			/*

 

 

 

 
 
 	return qry;
 }
 
+/*
+ * transformLetStmt -

 

 

 /*

 

 
 /*

 
 /*

 
 opt_column: COLUMN

 

 
 			/*
 
 			if (isAgg)
 
 			/*
index 2d1a477154..28486f3d8e 100644

 /* GUC parameters */

 /*

 
+/*
 
 			/*
 
 			/*

+	/*
 
 	/*

 		case DestTupleQueue:

 
 		case T_CallStmt:
 		case T_DoStmt:
+		case T_LetStmt:
 			{
 				/*
 
+		case T_LetStmt:

+		case T_LetStmt:
 
+		case T_LetStmt:
+			tag = CMDTAG_LET;
+			break;
+

 		case T_PLAssignStmt:
+		case T_LetStmt:
 			lev = LOGSTMT_ALL;
 			break;

 	ereport(ERROR,

+/*
+ * get_varname_varid

 	/*
 
 	return false;
 }

 
 
 	int			useDB;

+/*
+ * getVariables

 /*
 
+/*
 
 #endif							/* PG_DUMP_H */

 

 

+/*
+ * \dV
+ *
+ * listVariables()
+ */
+bool
 
 /*
  * \dFp
 
+/* \dV */
 
 #endif							/* DESCRIBE_H */

 
 /*

 /* DISCARD */
 
 /* DO */
 	else if (Matches("DO"))
 
+	/* DROP VARIABLE */

 
 /* PREPARE xx AS */
 
 /*

 

 

 

 
 
+/* ----------------
+ * SessionVariableValue
+ * ----------------
+ */
 
+	/* Variables info: */

 	/*

 

 

 
 
 	/*

 

 

 typedef struct Param

 

 
 /* ----------------

 /*

 

 
 
+			case PLPGSQL_STMT_LET:
 
+/* ----------
 
 	return "unknown";

+static void

+static void

 stmt_perform	: K_PERFORM
 
+stmt_let		: K_LET
+					{
+						PLpgSQL_stmt_let *new;
+						RawParseMode pmode;
+
 
 /*
 
+/*
+ * Let statement
+ */
 
index 0000000000..15682b0db0
@@ -0,0 +1,786 @@
+CREATE TABLE xxtab(avar int);
+CREATE TYPE xxtype AS (avar int);
+CREATE VARIABLE xxtab AS xxtype;
+INSERT INTO xxtab VALUES(10);
+-- should to fail, ambigonuous
+SELECT xxtab.avar FROM xxtab;
+ERROR:  column reference "xxtab.avar" is ambiguous
+LINE 1: SELECT xxtab.avar FROM xxtab;
+DETAIL:  The qualified identifier can be column reference or session variable reference
+SELECT avar FROM xxtab;
+ avar 
+CREATE VARIABLE public.avar AS int;
+SELECT avar FROM xxtab;
+ avar 
+SELECT public.avar FROM xxtab;
+ avar 
+DROP VARIABLE xxtab;
+SELECT xxtab.avar FROM xxtab;
+ avar 
+DROP VARIABLE public.avar;
+DROP TYPE xxtype;
+DROP TABLE xxtab;
 
 # ----------
index 0000000000..40e9bb1e59
@@ -0,0 +1,562 @@
+CREATE TABLE xxtab(avar int);
+
+CREATE TYPE xxtype AS (avar int);
+
+CREATE VARIABLE xxtab AS xxtype;
+
+INSERT INTO xxtab VALUES(10);
+
+-- should to fail, ambigonuous
+SELECT xxtab.avar FROM xxtab;
+
+-- should be ok
+SELECT avar FROM xxtab;
+
+CREATE VARIABLE public.avar AS int;
+
+-- should to fail
+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;
-- 
2.33.1

---

From: Pavel Stehule <[email-masked]>
Date: Mon, 27 Dec 2021 08:45:06 +0100

---------- Forwarded message ---------
Od: Pavel Stehule <[email-masked]>
Date: po 27. 12. 2021 v 5:30
Subject: Re: Schema variables - new implementation for Postgres 15
To: Justin Pryzby <[email-masked]>

Hi

ne 26. 12. 2021 v 15:43 odesílatel Justin Pryzby <[email-masked]>
napsal:

> an value => a value
> rigths => rights
> rigts => rights

fixed

Regards

Pavel

---

From: Julien Rouhaud <[email-masked]>
Date: Tue, 23 Aug 2022 13:56:11 +0800

Hi,

On Tue, Jan 18, 2022 at 10:01:01PM +0100, Pavel Stehule wrote:
>
> pá 14. 1. 2022 v 3:44 odesílatel Julien Rouhaud <[email-masked]> napsal:
>
> > On Thu, Jan 13, 2022 at 07:32:26PM +0100, Pavel Stehule wrote:
> > > čt 13. 1. 2022 v 19:23 odesílatel Dean Rasheed <[email-masked]
> > >
> > > > On Thu, 13 Jan 2022 at 17:42, Pavel Stehule <[email-masked]>
> > > > wrote:
> > > > >
> > > > > I like the idea of prioritizing tables over variables with warnings
> > when
> > > > collision is detected. It cannot break anything. And it allows to using
> > > > short identifiers when there is not collision.
> > > >
> > > > Yeah, that seems OK, as long as it's clearly documented. I don't think
> > > > a warning is necessary.
> >
> > What should be the behavior for a cached plan that uses a variable when a
> > conflicting relation is later created?  I think that it should be the same
> > as a
> > search_path change and the plan should be discarded.
> >
> > > The warning can be disabled by default, but I think it should be there.
> > > This is a signal, so some in the database schema should be renamed.
> > Maybe -
> > > session_variables_ambiguity_warning.
> >
> > I agree that having a way to know that a variable has been bypassed can be
> > useful.
> >
>
> done

I've been thinking a bit more about the shadowing, and one scenario we didn't
discuss is something like this naive example:

CREATE TABLE tt(a text, b text);

CREATE TYPE abc AS (a text, b text, c text);
CREATE VARIABLE tt AS abc;

INSERT INTO tt SELECT 'a', 'b';
LET tt = ('x', 'y', 'z');

SELECT tt.a, tt.b, tt.c FROM tt;

Which, with the default configuration, currently returns

 a | b | c
---+---+---
 a | b | z
(1 row)

I feel a bit uncomfortable that the system allows mixing variable attributes
and relation columns for the same relation name.  This is even worse here as
part of the variable attributes are shadowed.

It feels like a good way to write valid queries that clearly won't do what you
think they do, a bit like the correlated sub-query trap, so maybe we should
have a way to prevent it.

What do you think?

---

From: Pavel Stehule <[email-masked]>
Date: Tue, 23 Aug 2022 11:27:45 +0200

> Which, with the default configuration, currently returns

I thought about it before. I think valid RTE (but with the missing column)
can shadow the variable too.

With this change your query fails:

(2022-08-23 11:05:55) postgres=# SELECT tt.a, tt.b, tt.c FROM tt;
ERROR:  column tt.c does not exist
LINE 1: SELECT tt.a, tt.b, tt.c FROM tt;
                           ^
(2022-08-23 11:06:03) postgres=# set session_variables_ambiguity_warning to
on;
SET
(2022-08-23 11:06:19) postgres=# SELECT tt.a, tt.b, tt.c FROM tt;
WARNING:  session variable "tt.a" is shadowed
LINE 1: SELECT tt.a, tt.b, tt.c FROM tt;
               ^
DETAIL:  Session variables can be shadowed by columns, routine's variables
and routine's arguments with the same name.
WARNING:  session variable "tt.b" is shadowed
LINE 1: SELECT tt.a, tt.b, tt.c FROM tt;
                     ^
DETAIL:  Session variables can be shadowed by columns, routine's variables
and routine's arguments with the same name.
WARNING:  session variable "public.tt" is shadowed
LINE 1: SELECT tt.a, tt.b, tt.c FROM tt;
                           ^
DETAIL:   Session variables can be shadowed by tables or table's aliases
with the same name.
ERROR:  column tt.c does not exist
LINE 1: SELECT tt.a, tt.b, tt.c FROM tt;
                           ^
Regards

Pavel
From 0ddda4a6e538056a8093513a087cc85a25762256 Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Mon, 4 Apr 2022 20:23:54 +0200
Subject: [PATCH 12/12] documentation

Documentation for CREATE VARIABLE, DROP VARIABLE and LET commands. Update of
GRANT, REVOKE, DISCARD, ALTER commands related to support of session variables.
---
 doc/src/sgml/advanced.sgml                    |  62 +++++
 doc/src/sgml/catalogs.sgml                    | 158 +++++++++++++
 doc/src/sgml/config.sgml                      |  15 ++
 doc/src/sgml/event-trigger.sgml               |  24 ++
 doc/src/sgml/glossary.sgml                    |  16 ++
 doc/src/sgml/plpgsql.sgml                     |  12 +
 doc/src/sgml/ref/allfiles.sgml                |   4 +
 .../sgml/ref/alter_default_privileges.sgml    |  26 ++-
 doc/src/sgml/ref/alter_variable.sgml          | 179 +++++++++++++++
 doc/src/sgml/ref/comment.sgml                 |   1 +
 doc/src/sgml/ref/create_variable.sgml         | 214 ++++++++++++++++++
 doc/src/sgml/ref/discard.sgml                 |  14 +-
 doc/src/sgml/ref/drop_variable.sgml           | 118 ++++++++++
 doc/src/sgml/ref/grant.sgml                   |   5 +
 doc/src/sgml/ref/let.sgml                     | 108 +++++++++
 doc/src/sgml/ref/pg_restore.sgml              |  11 +
 doc/src/sgml/ref/revoke.sgml                  |   6 +
 doc/src/sgml/reference.sgml                   |   4 +
 18 files changed, 968 insertions(+), 9 deletions(-)
 create mode 100644 doc/src/sgml/ref/alter_variable.sgml
 create mode 100644 doc/src/sgml/ref/create_variable.sgml
 create mode 100644 doc/src/sgml/ref/drop_variable.sgml
 create mode 100644 doc/src/sgml/ref/let.sgml

diff --git a/doc/src/sgml/advanced.sgml b/doc/src/sgml/advanced.sgml
index 755c9f1485..c65b2bb336 100644
--- a/doc/src/sgml/advanced.sgml
+++ b/doc/src/sgml/advanced.sgml
@@ -700,6 +700,68 @@ SELECT name, elevation
   </sect1>

+  <sect1 id="tutorial-session-variables">
+   <title>Session Variables</title>
+
+   <indexterm zone="tutorial-session-variables">
+    <primary>Session variables</primary>
+   </indexterm>
+
+   <indexterm>
+    <primary>session variable</primary>
+    <secondary>introduction</secondary>
+   </indexterm>
+
+   <para>
+    Session variables are database objects that can hold a value.
+    Session variables, like relations, exist within a schema and their access
+    is controlled via <command>GRANT</command> and <command>REVOKE</command>
+    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.
+   </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. Warnings can be emitted when
+    this situation happens by enabling configuration parameter <xref
+    linkend="guc-session-variables-ambiguity-warning"/>.
+   </para>
+  </sect1>
+
+
   <sect1 id="tutorial-conclusion">
    <title>Conclusion</title>
 
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 2ce539aaf0..71b3861ece 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -369,6 +369,11 @@
       <entry><link linkend="catalog-pg-user-mapping"><structname>pg_user_mapping</structname></link></entry>
       <entry>mappings of users to foreign servers</entry>
      </row>
+
+     <row>
+      <entry><link linkend="catalog-pg-variable"><structname>pg_variable</structname></link></entry>
+      <entry>session variables</entry>
+     </row>
     </tbody>
    </tgroup>
   </table>
@@ -9603,4 +9608,157 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
   </table>
  </sect1>
 
+ <sect1 id="catalog-pg-variable">
+  <title><structname>pg_variable</structname></title>
+
+  <indexterm zone="catalog-pg-variable">
+   <primary>pg_variable</primary>
+  </indexterm>
+
+  <para>
+   The table <structname>pg_variable</structname> provides information about
+   session variables.
+  </para>
+
+  <table>
+   <title><structname>pg_variable</structname> Columns</title>
+   <tgroup cols="1">
+    <thead>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       Column Type
+      </para>
+      <para>
+       Description
+      </para></entry>
+     </row>
+    </thead>
+
+    <tbody>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>oid</structfield> <type>oid</type>
+      </para>
+      <para>
+       Row identifier
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>varname</structfield> <type>name</type>
+      </para>
+      <para>
+       Name of the session variable
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>varnamespace</structfield> <type>oid</type>
+       (references <link linkend="catalog-pg-namespace"><structname>pg_namespace</structname></link>.<structfield>oid</structfield>)
+      </para>
+      <para>
+       The OID of the namespace that contains this variable
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>vartype</structfield> <type>oid</type>
+       (references <link linkend="catalog-pg-type"><structname>pg_type</structname></link>.<structfield>oid</structfield>)
+      </para>
+      <para>
+       The OID of the variable's data type
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>vartypmod</structfield> <type>int4</type>
+      </para>
+      <para>
+       <structfield>vartypmod</structfield> records type-specific data
+       supplied at variable creation time (for example, the maximum
+       length of a <type>varchar</type> column).  It is passed to
+       type-specific input functions and length coercion functions.
+       The value will generally be -1 for types that do not need <structfield>vartypmod</structfield>.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>varowner</structfield> <type>oid</type>
+       (references <link linkend="catalog-pg-authid"><structname>pg_authid</structname></link>.<structfield>oid</structfield>)
+      </para>
+      <para>
+       Owner of the variable
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>varcollation</structfield> <type>oid</type>
+       (references <link linkend="catalog-pg-collation"><structname>pg_collation</structname></link>.<structfield>oid</structfield>)
+      </para>
+      <para>
+       The defined collation of the variable, or zero if the variable is
+       not of a collatable data type.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>varisnotnull</structfield> <type>boolean</type>
+      </para>
+      <para>
+       True if the session variable doesn't allow null value. The default value is false.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>varisimmutable</structfield> <type>boolean</type>
+      </para>
+      <para>
+       True if the variable is immutable (cannot be modified). The default value is false.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>vareoxaction</structfield> <type>char</type>
+      </para>
+      <para>
+       Action performed at end of transaction:
+       <literal>n</literal> = no action, <literal>d</literal> = drop the variable,
+       <literal>r</literal> = reset the variable to its default value.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>vardefexpr</structfield> <type>pg_node_tree</type>
+      </para>
+      <para>
+       The internal representation of the variable default value
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>varacl</structfield> <type>aclitem[]</type>
+      </para>
+      <para>
+       Access privileges; see
+       <xref linkend="sql-grant"/> and
+       <xref linkend="sql-revoke"/>
+       for details
+      </para></entry>
+     </row>
+    </tbody>
+   </tgroup>
+  </table>
+ </sect1>
+
 </chapter>
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index a5cd4e44c7..6bf0f59998 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -10239,6 +10239,21 @@ dynamic_library_path = 'C:\tools\postgresql;H:\my_project\lib;$libdir'
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-session-variables-ambiguity-warning" xreflabel="session_variables_ambiguity_warning">
+      <term><varname>session_variables_ambiguity_warning</varname> (<type>boolean</type>)
+      <indexterm>
+       <primary><varname>session_variables_ambiguity_warning</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        When on, a warning is raised when any identifier in a query could be
+        used as both a column identifier, routine variable or a session
+        variable identifier.  The default is <literal>off</literal>.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="guc-standard-conforming-strings" xreflabel="standard_conforming_strings">
       <term><varname>standard_conforming_strings</varname> (<type>boolean</type>)
       <indexterm><primary>strings</primary><secondary>standard conforming</secondary></indexterm>
diff --git a/doc/src/sgml/event-trigger.sgml b/doc/src/sgml/event-trigger.sgml
index f1235a2c9f..a8f303b474 100644
--- a/doc/src/sgml/event-trigger.sgml
+++ b/doc/src/sgml/event-trigger.sgml
@@ -405,6 +405,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>
@@ -693,6 +701,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>
@@ -981,6 +997,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/glossary.sgml b/doc/src/sgml/glossary.sgml
index d6d0a3a814..a46baa0c17 100644
--- a/doc/src/sgml/glossary.sgml
+++ b/doc/src/sgml/glossary.sgml
@@ -1477,6 +1477,22 @@
    </glossdef>
   </glossentry>
 
+  <glossentry id="glossary-session-variable">
+   <glossterm>Session variable</glossterm>
+   <glossdef>
+    <para>
+     A persistent database object that holds a value in session memory.  This
+     memory is not shared across sessions, and after session end, this memory
+     (the value) is released. The access (read or write) to session variables
+     is controlled by access rights similarly to other database object access
+     rights.
+    </para>
+    <para>
+     For more information, see <xref linkend="tutorial-session-variables"/>.
+    </para>
+   </glossdef>
+  </glossentry>
+
   <glossentry id="glossary-shared-memory">
    <glossterm>Shared memory</glossterm>
    <glossdef>
diff --git a/doc/src/sgml/plpgsql.sgml b/doc/src/sgml/plpgsql.sgml
index cf387dfc3f..271429d1e8 100644
--- a/doc/src/sgml/plpgsql.sgml
+++ b/doc/src/sgml/plpgsql.sgml
@@ -5953,6 +5953,18 @@ $$ LANGUAGE plpgsql STRICT IMMUTABLE;
 </programlisting>
     </para>
    </sect3>
+
+   <sect3>
+    <title><command>Session variables and constants</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>
 
index e90a0e1f83..24e30fdd97 100644
index f1d54f5aa3..354991b849 100644
 
+GRANT { SELECT | UPDATE | ALL [ PRIVILEGES ] }
+    ON VARIABLES
+    { { SELECT | UPDATE } [, ...] | ALL [ PRIVILEGES ] }
 
@@ -92,14 +102,14 @@ REVOKE [ GRANT OPTION FOR ]
    that will be applied to objects created in the future.  (It does not
    affect privileges assigned to already-existing objects.)  Currently,
    only the privileges for schemas, tables (including views and foreign
-   tables), sequences, functions, and types (including domains) can be
-   altered.  For this command, functions include aggregates and procedures.
-   The words <literal>FUNCTIONS</literal> and <literal>ROUTINES</literal> are
-   equivalent in this command.  (<literal>ROUTINES</literal> is preferred
-   going forward as the standard term for functions and procedures taken
-   together.  In earlier PostgreSQL releases, only the
-   word <literal>FUNCTIONS</literal> was allowed.  It is not possible to set
-   default privileges for functions and procedures separately.)
+   tables), sequences, functions, types (including domains) and session
+   variables can be altered.  For this command, functions include aggregates
+   and procedures.  The words <literal>FUNCTIONS</literal> and
+   <literal>ROUTINES</literal> are equivalent in this command.
+   (<literal>ROUTINES</literal> is preferred going forward as the standard term
+   for functions and procedures taken together.  In earlier PostgreSQL
+   releases, only the word <literal>FUNCTIONS</literal> was allowed.  It is not
+   possible to set default privileges for functions and procedures separately.)
   </para>
 
   <para>
diff --git a/doc/src/sgml/ref/alter_variable.sgml b/doc/src/sgml/ref/alter_variable.sgml
new file mode 100644
index 0000000000..d2036351e5
--- /dev/null
+++ b/doc/src/sgml/ref/alter_variable.sgml
@@ -0,0 +1,179 @@
+<!--
+doc/src/sgml/ref/alter_variable.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-altervariable">
+ <indexterm zone="sql-altervariable">
+  <primary>ALTER VARIABLE</primary>
+ </indexterm>
+
+ <indexterm>
+  <primary>session variable</primary>
+  <secondary>altering</secondary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>ALTER VARIABLE</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>ALTER VARIABLE</refname>
+  <refpurpose>
+   change the definition of a session variable
+  </refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+ALTER VARIABLE <replaceable class="parameter">name</replaceable> OWNER TO { <replaceable class="parameter">new_owner</replaceable> | CURRENT_ROLE | CURRENT_USER | SESSION_USER }
+ALTER VARIABLE <replaceable class="parameter">name</replaceable> RENAME TO <replaceable class="parameter">new_name</replaceable>
+ALTER VARIABLE <replaceable class="parameter">name</replaceable> SET SCHEMA <replaceable class="parameter">new_schema</replaceable>
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   The <command>ALTER VARIABLE</command> command changes the definition of an
+   existing session variable. There are several subforms:
+
+  <variablelist>
+   <varlistentry>
+    <term><literal>OWNER</literal></term>
+    <listitem>
+     <para>
+      This form changes the owner of the session variable.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>RENAME</literal></term>
+    <listitem>
+     <para>
+      This form changes the name of the session variable.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>SET SCHEMA</literal></term>
+    <listitem>
+     <para>
+      This form moves the session variable into another schema.
+     </para>
+    </listitem>
+   </varlistentry>
+
+  </variablelist>
+  </para>
+
+  <para>
+   Only the owner or a superuser is allowed to alter a session variable.
+   In order to move a session variable from one schema to another, the user
+   must also have the <literal>CREATE</literal> privilege on the new schema (or
+   be a superuser).
+
+   In order to move the session variable ownership from one role to another,
+   the user must also be a direct or indirect member of the new
+   owning role, and that role must have the <literal>CREATE</literal> privilege
+   on the session variable's schema (or be a superuser). These restrictions
+   enforce that altering the owner doesn't do anything you couldn't do by
+   dropping and recreating the session variable.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+   <para>
+    <variablelist>
+     <varlistentry>
+      <term><replaceable class="parameter">name</replaceable></term>
+      <listitem>
+       <para>
+        The name (possibly schema-qualified) of the existing session variable
+        to alter.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><replaceable class="parameter">new_name</replaceable></term>
+      <listitem>
+       <para>
+        The new name for the session variable.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><replaceable class="parameter">new_owner</replaceable></term>
+      <listitem>
+       <para>
+        The user name of the new owner of the session variable.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><replaceable class="parameter">new_schema</replaceable></term>
+      <listitem>
+       <para>
+        The new schema for the session variable.
+       </para>
+      </listitem>
+     </varlistentry>
+    </variablelist>
+   </para>
+  </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   To rename a session variable:
+<programlisting>
+ALTER VARIABLE foo RENAME TO boo;
+</programlisting>
+  </para>
+
+  <para>
+   To change the owner of the session variable <literal>boo</literal> to
+   <literal>joe</literal>:
+<programlisting>
+ALTER VARIABLE boo OWNER TO joe;
+</programlisting>
+  </para>
+
+  <para>
+   To change the schema of the session variable <literal>boo</literal> to
+   <literal>private</literal>:
+<programlisting>
+ALTER VARIABLE boo SET SCHEMA private;
+</programlisting>
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   Session variables and this command in particular are a PostgreSQL extension.
+  </para>
+ </refsect1>
+
+ <refsect1 id="sql-altervariable-see-also">
+  <title>See Also</title>
+
+  <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/comment.sgml b/doc/src/sgml/ref/comment.sgml
index 23d9029af9..50bfec6f28 100644
--- a/doc/src/sgml/ref/comment.sgml
+++ b/doc/src/sgml/ref/comment.sgml
@@ -65,6 +65,7 @@ COMMENT ON
   TRANSFORM FOR <replaceable>type_name</replaceable> LANGUAGE <replaceable>lang_name</replaceable> |
   TRIGGER <replaceable class="parameter">trigger_name</replaceable> ON <replaceable class="parameter">table_name</replaceable> |
   TYPE <replaceable class="parameter">object_name</replaceable> |
+  VARIABLE <replaceable class="parameter">object_name</replaceable> |
   VIEW <replaceable class="parameter">object_name</replaceable>
 } IS '<replaceable class="parameter">text</replaceable>'
 
diff --git a/doc/src/sgml/ref/create_variable.sgml b/doc/src/sgml/ref/create_variable.sgml
new file mode 100644
index 0000000000..77cbbbafca
--- /dev/null
+++ b/doc/src/sgml/ref/create_variable.sgml
@@ -0,0 +1,214 @@
+<!--
+doc/src/sgml/ref/create_variable.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-createvariable">
+ <indexterm zone="sql-createvariable">
+  <primary>CREATE VARIABLE</primary>
+ </indexterm>
+
+ <indexterm>
+  <primary>session variable</primary>
+  <secondary>defining</secondary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>CREATE VARIABLE</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>CREATE VARIABLE</refname>
+  <refpurpose>define a session variable</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+CREATE [ { TEMPORARY | TEMP } ] [ IMMUTABLE ] VARIABLE [ IF NOT EXISTS ] <replaceable class="parameter">name</replaceable> [ AS ] <replaceable class="parameter">data_type</replaceable> ] [ COLLATE <replaceable class="parameter">collation</replaceable> ]
+    [ NOT NULL ] [ DEFAULT <replaceable class="parameter">default_expr</replaceable> ] [ { ON COMMIT DROP | ON TRANSACTION END RESET } ]
+</synopsis>
+ </refsynopsisdiv>
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   The <command>CREATE VARIABLE</command> command creates a session variable.
+   Session variables, like relations, exist within a schema and their access is
+   controlled via <command>GRANT</command> and <command>REVOKE</command>
+   commands.
+  </para>
+
+  <para>
+   The value of a session variable is local to the current session. Retrieving
+   a session variable's value returns either a NULL or a default value, unless
+   its value is set to something else in the current session with a LET
+   command. The content of a session variable is not transactional. This is the
+   same as regular variables in PL languages.
+  </para>
+
+  <para>
+   Session variables are retrieved by the <command>SELECT</command> SQL
+   command.  Their value is set with the <command>LET</command> SQL command.
+   While session variables share properties with tables, their value cannot be
+   changed with an <command>UPDATE</command> command.
+  </para>
+
+  <note>
+   <para>
+    Inside a query or an expression, the session variable can be shadowed by
+    column or by routine's variable or routine argument. Such collisions of
+    identifiers can be resolved by using qualified identifiers. Session variables
+    can use schema name, columns can use table aliases, routine variables
+    can use block labels, and routine arguments can use the routine name.
+   </para>
+  </note>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+  <variablelist>
+   <varlistentry>
+    <term><literal>IMMUTABLE</literal></term>
+    <listitem>
+     <para>
+      The assigned value of the session variable can not be changed.
+      Only if the session variable doesn't have a default value, a single
+      initialization is allowed using the <command>LET</command> command. Once
+      done, no further change is allowed until end of transaction
+      if the session variable was created with clause <literal>ON TRANSACTION
+      END RESET</literal>, or until reset of all session variables by
+      <command>DISCARD VARIABLES</command>, or until reset of all session
+      objects by command <command>DISCARD ALL</command>.
+     </para>
+
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>IF NOT EXISTS</literal></term>
+    <listitem>
+     <para>
+      Do not throw an error if the name already exists. A notice is issued in
+      this case.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">name</replaceable></term>
+    <listitem>
+     <para>
+      The name, optionally schema-qualified, of the session variable.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">data_type</replaceable></term>
+    <listitem>
+     <para>
+      The name, optionally schema-qualified, of the data type of the session
+      variable.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>COLLATE <replaceable>collation</replaceable></literal></term>
+    <listitem>
+     <para>
+      The <literal>COLLATE</literal> clause assigns a collation to the session
+      variable (which must be of a collatable data type).  If not specified,
+      the data type's default collation is used.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>NOT NULL</literal></term>
+    <listitem>
+     <para>
+      The <literal>NOT NULL</literal> clause forbids setting the session
+      variable to a null value. A session variable created as NOT NULL and
+      without an explicitly declared default value cannot be read until it is
+      initialized by a LET command. This requires the user to explicitly
+      initialize the session variable content before reading it, otherwise an
+      error will be thrown.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>DEFAULT <replaceable>default_expr</replaceable></literal></term>
+    <listitem>
+     <para>
+      The <literal>DEFAULT</literal> clause can be used to assign a default
+      value to a session variable. This expression is evaluated when the session
+      variable is first accessed for reading and had not yet been assigned a value.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>ON COMMIT DROP</literal>, <literal>ON TRANSACTION END RESET</literal></term>
+    <listitem>
+     <para>
+      The <literal>ON COMMIT DROP</literal> clause specifies the behaviour of a
+      temporary session variable at transaction commit. With this clause, the
+      session variable is dropped at commit time. The clause is only allowed
+      for temporary variables. The <literal>ON TRANSACTION END RESET</literal>
+      clause causes the session variable to be reset to its default value when
+      the transaction is committed or rolled back.
+     </para>
+    </listitem>
+   </varlistentry>
+
+  </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Notes</title>
+
+  <para>
+   Use the <command>DROP VARIABLE</command> command to remove a session
+   variable.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   Create an date session variable <literal>var1</literal>:
+<programlisting>
+CREATE VARIABLE var1 AS date;
+LET var1 = current_date;
+SELECT var1;
+</programlisting>
+  </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   The <command>CREATE VARIABLE</command> 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-dropvariable"/></member>
+   <member><xref linkend="sql-let"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/ref/discard.sgml b/doc/src/sgml/ref/discard.sgml
index bf44c523ca..6f90672afa 100644
--- a/doc/src/sgml/ref/discard.sgml
+++ b/doc/src/sgml/ref/discard.sgml
@@ -21,7 +21,7 @@ PostgreSQL documentation
 
  <refsynopsisdiv>
 <synopsis>

+   <varlistentry>
index 0000000000..67988b5fcd
@@ -0,0 +1,118 @@
+   A session variable can only be removed by its owner or a superuser.
+      Do not throw an error if the session variable does not exist. A notice is
+      issued in this case.
+      Automatically drop objects that depend on the session variable (such as
+      views), and in turn all objects that depend on those objects
+      Refuse to drop the session variable if any objects depend on it.  This is
+   The <command>DROP VARIABLE</command> command is a
+   <productname>PostgreSQL</productname> extension.
index f744b05b55..66304eb132 100644
@@ -108,6 +108,11 @@ GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replace
+GRANT { SELECT | UPDATE | ALL [ PRIVILEGES ] }
 
index 0000000000..210aca3b95
@@ -0,0 +1,108 @@
+   The <command>LET</command> command assigns a value to the specified session
+   variable.
+      An SQL expression, in parentheses. The result must be of the same data type as the session
+      variable.
+      If no explicit default value has been declared, the session variable
+   The <command>LET</command> is a <productname>PostgreSQL</productname>
+   extension.
index 47bd7dbda0..39cb595647 100644
 
+     <varlistentry>
+      <term><option>-A <replaceable class="parameter">session_variable</replaceable></option></term>
+      <term><option>--variable=<replaceable class="parameter">session_variable</replaceable></option></term>
+      <listitem>
+       <para>
+        Restore a named session variable only.  Multiple session variables may
+        be specified with multiple <option>-A</option> switches.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>-c</option></term>
       <term><option>--clean</option></term>
diff --git a/doc/src/sgml/ref/revoke.sgml b/doc/src/sgml/ref/revoke.sgml
index 62f1971036..b070bfa69f 100644
--- a/doc/src/sgml/ref/revoke.sgml
+++ b/doc/src/sgml/ref/revoke.sgml
@@ -137,6 +137,12 @@ REVOKE [ ADMIN OPTION FOR ]
   | CURRENT_ROLE
   | CURRENT_USER
   | SESSION_USER
+
+REVOKE [ GRANT OPTION FOR ]
+    { { SELECT | UPDATE } [, ...] | ALL [ PRIVILEGES ] }
+    ON VARIABLE <replaceable>variable_name</replaceable> [, ...]
+    FROM { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...]
+    [ CASCADE | RESTRICT ]
 </synopsis>
  </refsynopsisdiv>
 
diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml
index a3b743e8c1..f5ec3c987b 100644
--- a/doc/src/sgml/reference.sgml
+++ b/doc/src/sgml/reference.sgml
@@ -75,6 +75,7 @@
    &alterType;
    &alterUser;
    &alterUserMapping;
+   &alterVariable;
    &alterView;
    &analyze;
    &begin;
@@ -127,6 +128,7 @@
    &createType;
    &createUser;
    &createUserMapping;
+   &createVariable;
    &createView;
    &deallocate;
    &declare;
@@ -175,6 +177,7 @@
    &dropType;
    &dropUser;
    &dropUserMapping;
+   &dropVariable;
    &dropView;
    &end;
    &execute;
@@ -183,6 +186,7 @@
    &grant;
    &importForeignSchema;
    &insert;
+   &let;
    &listen;
    &load;
    &lock;
-- 
2.37.2

From af39b2986cb892c1202575cf0448010486165faf Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Tue, 5 Apr 2022 11:40:29 +0200
Subject: [PATCH 11/12] this patch changes error message "column doesn't exist"
 to message "column or variable doesn't exist"

The error message will be more correct. Today, missing PL/pgSQL variable can be reported. The change has
impact on lot of regress tests not related to session variables, and then it is distributed as separate patch
---
 src/backend/parser/parse_relation.c           |  4 +-
 src/backend/parser/parse_target.c             |  4 +-
 src/pl/plpgsql/src/expected/plpgsql_array.out |  2 +-
 .../plpgsql/src/expected/plpgsql_record.out   |  4 +-
 src/pl/tcl/expected/pltcl_queries.out         | 12 +++---
 src/test/regress/expected/alter_table.out     | 38 +++++++++----------
 src/test/regress/expected/copy2.out           |  2 +-
 src/test/regress/expected/errors.out          |  8 ++--
 src/test/regress/expected/generated.out       |  4 +-
 src/test/regress/expected/insert_conflict.out |  2 +-
 src/test/regress/expected/join.out            | 10 ++---
 src/test/regress/expected/plpgsql.out         | 12 +++---
 src/test/regress/expected/rules.out           |  4 +-
 .../regress/expected/session_variables.out    |  2 +-
 src/test/regress/expected/transactions.out    |  4 +-
 src/test/regress/expected/union.out           |  2 +-
 16 files changed, 57 insertions(+), 57 deletions(-)

diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c
index f6b740df0a..6aed2c5438 100644
--- a/src/backend/parser/parse_relation.c
+++ b/src/backend/parser/parse_relation.c
@@ -3668,7 +3668,7 @@ errorMissingColumn(ParseState *pstate,
 				(errcode(ERRCODE_UNDEFINED_COLUMN),
 				 relname ?
 				 errmsg("column %s.%s does not exist", relname, colname) :
-				 errmsg("column \"%s\" does not exist", colname),
+				 errmsg("column or variable \"%s\" does not exist", colname),
 				 state->rfirst ? closestfirst ?
 				 errhint("Perhaps you meant to reference the column \"%s.%s\".",
 						 state->rfirst->eref->aliasname, closestfirst) :
@@ -3688,7 +3688,7 @@ errorMissingColumn(ParseState *pstate,
 				(errcode(ERRCODE_UNDEFINED_COLUMN),
 				 relname ?
 				 errmsg("column %s.%s does not exist", relname, colname) :
-				 errmsg("column \"%s\" does not exist", colname),
+				 errmsg("column or variable  \"%s\" does not exist", colname),
 				 errhint("Perhaps you meant to reference the column \"%s.%s\" or the column \"%s.%s\".",
 						 state->rfirst->eref->aliasname, closestfirst,
 						 state->rsecond->eref->aliasname, closestsecond),
diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c
index 41cb04f4b6..97402e0293 100644
--- a/src/backend/parser/parse_target.c
+++ b/src/backend/parser/parse_target.c
@@ -786,7 +786,7 @@ transformAssignmentIndirection(ParseState *pstate,
 			if (!typrelid)
 				ereport(ERROR,
 						(errcode(ERRCODE_DATATYPE_MISMATCH),
-						 errmsg("cannot assign to field \"%s\" of column \"%s\" because its type %s is not a composite type",
+						 errmsg("cannot assign to field \"%s\" of column or variable \"%s\" because its type %s is not a composite type",
 								strVal(n), targetName,
 								format_type_be(targetTypeId)),
 						 parser_errposition(pstate, location)));
@@ -795,7 +795,7 @@ transformAssignmentIndirection(ParseState *pstate,
 			if (attnum == InvalidAttrNumber)
 				ereport(ERROR,
 						(errcode(ERRCODE_UNDEFINED_COLUMN),
-						 errmsg("cannot assign to field \"%s\" of column \"%s\" because there is no such column in data type %s",
+						 errmsg("cannot assign to field \"%s\" of column or variable \"%s\" because there is no such column in data type %s",
 								strVal(n), targetName,
 								format_type_be(targetTypeId)),
 						 parser_errposition(pstate, location)));
diff --git a/src/pl/plpgsql/src/expected/plpgsql_array.out b/src/pl/plpgsql/src/expected/plpgsql_array.out
index 9e22e56f00..e131febf3d 100644
--- a/src/pl/plpgsql/src/expected/plpgsql_array.out
+++ b/src/pl/plpgsql/src/expected/plpgsql_array.out
@@ -41,7 +41,7 @@ NOTICE:  a = {"(,11)"}, a[1].i = 11
 -- perhaps this ought to work, but for now it doesn't:
 do $$ declare a complex[];
 begin a[1:2].i := array[11,12]; raise notice 'a = %', a; end$$;
-ERROR:  cannot assign to field "i" of column "a" because its type complex[] is not a composite type
+ERROR:  cannot assign to field "i" of column or variable "a" because its type complex[] is not a composite type
 LINE 1: a[1:2].i := array[11,12]
         ^
 QUERY:  a[1:2].i := array[11,12]
diff --git a/src/pl/plpgsql/src/expected/plpgsql_record.out b/src/pl/plpgsql/src/expected/plpgsql_record.out
index 4c5d95c79e..fc95c5fd35 100644
--- a/src/pl/plpgsql/src/expected/plpgsql_record.out
+++ b/src/pl/plpgsql/src/expected/plpgsql_record.out
@@ -134,7 +134,7 @@ ERROR:  record "c" has no field "x"
 CONTEXT:  PL/pgSQL assignment "c.x.q1 = 1"
 PL/pgSQL function inline_code_block line 1 at assignment
 do $$ declare c nested_int8s; begin c.c2.x = 1; end $$;
-ERROR:  cannot assign to field "x" of column "c" because there is no such column in data type two_int8s
+ERROR:  cannot assign to field "x" of column or variable "c" because there is no such column in data type two_int8s
 LINE 1: c.c2.x = 1
         ^
 QUERY:  c.c2.x = 1
@@ -156,7 +156,7 @@ ERROR:  record "c" has no field "x"
 CONTEXT:  PL/pgSQL assignment "b.c.x.q1 = 1"
 PL/pgSQL function inline_code_block line 1 at assignment
 do $$ <<b>> declare c nested_int8s; begin b.c.c2.x = 1; end $$;
-ERROR:  cannot assign to field "x" of column "b" because there is no such column in data type two_int8s
+ERROR:  cannot assign to field "x" of column or variable "b" because there is no such column in data type two_int8s
 LINE 1: b.c.c2.x = 1
         ^
 QUERY:  b.c.c2.x = 1
diff --git a/src/pl/tcl/expected/pltcl_queries.out b/src/pl/tcl/expected/pltcl_queries.out
index 2d922c2333..3b6506d613 100644
--- a/src/pl/tcl/expected/pltcl_queries.out
+++ b/src/pl/tcl/expected/pltcl_queries.out
@@ -246,12 +246,12 @@ ERROR:  type "b" does not exist
 select tcl_eval('spi_prepare a "b {"');
 ERROR:  unmatched open brace in list
 select tcl_error_handling_test($tcl$spi_prepare "select moo" []$tcl$);
-       tcl_error_handling_test        
---------------------------------------
- SQLSTATE: 42703                     +
- condition: undefined_column         +
- cursor_position: 8                  +
- message: column "moo" does not exist+
+             tcl_error_handling_test              
+--------------------------------------------------
+ SQLSTATE: 42703                                 +
+ condition: undefined_column                     +
+ cursor_position: 8                              +
+ message: column or variable "moo" does not exist+
  statement: select moo
 (1 row)
 
index d63f4f1cba..927bbd3e58 100644
 
@@ -1309,7 +1309,7 @@ select atacc1.* from atacc1;
 (1 row)
 
 select a from atacc1;
@@ -1323,15 +1323,15 @@ select b,c,d from atacc1;
 (1 row)
 
@@ -1339,11 +1339,11 @@ ERROR:  column atacc1.........pg.dropped.1........ does not exist
 
index 8c527474da..e53ae451df 100644
index bb4190340e..461da763f1 100644
@@ -727,7 +727,7 @@ ALTER TABLE gtest25 ADD COLUMN x int GENERATED ALWAYS AS (b * 4) STORED;  -- err
index 66d8633e3e..1ad53ec761 100644
@@ -245,7 +245,7 @@ LINE 1: ...y) do update set fruit = excluded.fruit RETURNING excluded.f...
index 2ed2e542a4..dbeeafa9c3 100644
@@ -4999,7 +4999,7 @@ LINE 1: select t2.uunique1 from
@@ -6033,7 +6033,7 @@ lateral (select * from int8_tbl t1,
 
 -- test some error cases where LATERAL should have been used but wasn't
 select f1,g from int4_tbl a, (select f1 as g) ss;
-ERROR:  column "f1" does not exist
+ERROR:  column or variable "f1" does not exist
 LINE 1: select f1,g from int4_tbl a, (select f1 as g) ss;
                                              ^
 HINT:  There is a column named "f1" in table "a", but it cannot be referenced from this part of the query.
@@ -6043,7 +6043,7 @@ LINE 1: select f1,g from int4_tbl a, (select a.f1 as g) ss;
                                              ^
 HINT:  There is an entry for table "a", but it cannot be referenced from this part of the query.
 select f1,g from int4_tbl a cross join (select f1 as g) ss;
-ERROR:  column "f1" does not exist
+ERROR:  column or variable "f1" does not exist
 LINE 1: select f1,g from int4_tbl a cross join (select f1 as g) ss;
                                                        ^
 HINT:  There is a column named "f1" in table "a", but it cannot be referenced from this part of the query.
@@ -6078,7 +6078,7 @@ LINE 1: select 1 from tenk1 a, lateral (select max(a.unique1) from i...
 create temp table xx1 as select f1 as x1, -f1 as x2 from int4_tbl;
 -- error, can't do this:
 update xx1 set x2 = f1 from (select * from int4_tbl where f1 = x1) ss;
-ERROR:  column "x1" does not exist
+ERROR:  column or variable "x1" does not exist
 LINE 1: ... set x2 = f1 from (select * from int4_tbl where f1 = x1) ss;
                                                                 ^
 HINT:  There is a column named "x1" in table "xx1", but it cannot be referenced from this part of the query.
@@ -6098,7 +6098,7 @@ update xx1 set x2 = f1 from xx1, lateral (select * from int4_tbl where f1 = x1)
 ERROR:  table name "xx1" specified more than once
 -- also errors:
 delete from xx1 using (select * from int4_tbl where f1 = x1) ss;
-ERROR:  column "x1" does not exist
+ERROR:  column or variable "x1" does not exist
 LINE 1: ...te from xx1 using (select * from int4_tbl where f1 = x1) ss;
                                                                 ^
 HINT:  There is a column named "x1" in table "xx1", but it cannot be referenced from this part of the query.
diff --git a/src/test/regress/expected/plpgsql.out b/src/test/regress/expected/plpgsql.out
index 08e42f17dc..6f5f37d58f 100644
--- a/src/test/regress/expected/plpgsql.out
+++ b/src/test/regress/expected/plpgsql.out
@@ -2600,7 +2600,7 @@ end; $$ language plpgsql;
 -- should fail: SQLSTATE and SQLERRM are only in defined EXCEPTION
 -- blocks
 select excpt_test1();
-ERROR:  column "sqlstate" does not exist
+ERROR:  column or variable "sqlstate" does not exist
 LINE 1: sqlstate
         ^
 QUERY:  sqlstate
@@ -2615,7 +2615,7 @@ begin
 end; $$ language plpgsql;
 -- should fail
 select excpt_test2();
-ERROR:  column "sqlstate" does not exist
+ERROR:  column or variable "sqlstate" does not exist
 LINE 1: sqlstate
         ^
 QUERY:  sqlstate
@@ -4629,7 +4629,7 @@ BEGIN
         RAISE NOTICE '%, %', r.roomno, r.comment;
     END LOOP;
 END$$;
-ERROR:  column "foo" does not exist
+ERROR:  column or variable "foo" does not exist
 LINE 1: SELECT rtrim(roomno) AS roomno, foo FROM Room ORDER BY roomn...
                                         ^
 QUERY:  SELECT rtrim(roomno) AS roomno, foo FROM Room ORDER BY roomno
@@ -4671,7 +4671,7 @@ begin
   raise notice 'x = %', x;
 end;
 $$;
-ERROR:  column "x" does not exist
+ERROR:  column or variable "x" does not exist
 LINE 1: x + 1
         ^
 QUERY:  x + 1
@@ -4683,7 +4683,7 @@ begin
   raise notice 'x = %, y = %', x, y;
 end;
 $$;
-ERROR:  column "x" does not exist
+ERROR:  column or variable "x" does not exist
 LINE 1: x + 1
         ^
 QUERY:  x + 1
@@ -5696,7 +5696,7 @@ ALTER TABLE alter_table_under_transition_tables
   DROP column name;
 UPDATE alter_table_under_transition_tables
   SET id = id;
-ERROR:  column "name" does not exist
+ERROR:  column or variable "name" does not exist
 LINE 1: (SELECT string_agg(id || '=' || name, ',') FROM d)
                                         ^
 QUERY:  (SELECT string_agg(id || '=' || name, ',') FROM d)
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 7ec3d2688f..982f31aa33 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1191,7 +1191,7 @@ drop rule rules_foorule on rules_foo;
 -- this should fail because f1 is not exposed for unqualified reference:
 create rule rules_foorule as on insert to rules_foo where f1 < 100
 do instead insert into rules_foo2 values (f1);
-ERROR:  column "f1" does not exist
+ERROR:  column or variable "f1" does not exist
 LINE 2: do instead insert into rules_foo2 values (f1);
                                                   ^
 HINT:  There is a column named "f1" in table "old", but it cannot be referenced from this part of the query.
@@ -2715,7 +2715,7 @@ select * from rules_fooview;
 (1 row)
 
 select xmin, * from rules_fooview;  -- fail, views don't have such a column
-ERROR:  column "xmin" does not exist
+ERROR:  column or variable "xmin" does not exist
 LINE 1: select xmin, * from rules_fooview;
                ^
 select reltoastrelid, relkind, relfrozenxid
diff --git a/src/test/regress/expected/session_variables.out b/src/test/regress/expected/session_variables.out
index 054d3be912..6e355510a8 100644
--- a/src/test/regress/expected/session_variables.out
+++ b/src/test/regress/expected/session_variables.out
@@ -246,7 +246,7 @@ SELECT v1;
 
 -- should fail
 LET v1.x = 10;
-ERROR:  cannot assign to field "x" of column "v1" because there is no such column in data type t1
+ERROR:  cannot assign to field "x" of column or variable "v1" because there is no such column in data type t1
 LINE 1: LET v1.x = 10;
             ^
 DROP VARIABLE v1;
diff --git a/src/test/regress/expected/transactions.out b/src/test/regress/expected/transactions.out
index a46fa5d48a..a8643f5033 100644
--- a/src/test/regress/expected/transactions.out
+++ b/src/test/regress/expected/transactions.out
@@ -222,7 +222,7 @@ SELECT * FROM trans_barbaz;	-- should have 1
 BEGIN;
 	SAVEPOINT one;
 		SELECT trans_foo;
-ERROR:  column "trans_foo" does not exist
+ERROR:  column or variable "trans_foo" does not exist
 LINE 1: SELECT trans_foo;
                ^
 	ROLLBACK TO SAVEPOINT one;
@@ -271,7 +271,7 @@ BEGIN;
 	SAVEPOINT one;
 		INSERT INTO savepoints VALUES (5);
 		SELECT trans_foo;
-ERROR:  column "trans_foo" does not exist
+ERROR:  column or variable "trans_foo" does not exist
 LINE 1: SELECT trans_foo;
                ^
 COMMIT;
diff --git a/src/test/regress/expected/union.out b/src/test/regress/expected/union.out
index 7ac4a9380e..01658f5353 100644
--- a/src/test/regress/expected/union.out
+++ b/src/test/regress/expected/union.out
@@ -907,7 +907,7 @@ ORDER BY q2,q1;
 
2.37.2

From 41954a41a9a04d5bbd725a8c08548e83eb174e8a Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Mon, 4 Apr 2022 20:52:58 +0200
Subject: [PATCH 09/12] typedefs

---
 src/tools/pgindent/typedefs.list | 9 +++++++++
 1 file changed, 9 insertions(+)

diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 35c9f1efce..400746a13a 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -498,6 +498,7 @@ CreateRoleStmt
 CreateSchemaStmt
 CreateSchemaStmtContext
 CreateSeqStmt
+CreateSessionVarStmt
 CreateStatsStmt
 CreateStmt
 CreateStmtContext
@@ -881,6 +882,7 @@ Form_pg_ts_parser
 Form_pg_ts_template
 Form_pg_type
 Form_pg_user_mapping
+FormData_pg_variable
 FormatNode
 FreeBlockNumberArray
 FreeListData
@@ -1403,6 +1405,7 @@ LargeObjectDesc
 LastAttnumInfo
 Latch
 LerpFunc
+LetStmt
 LexDescr
 LexemeEntry
 LexemeHashKey
@@ -1785,6 +1788,7 @@ PLpgSQL_stmt_forq
 PLpgSQL_stmt_fors
 PLpgSQL_stmt_getdiag
 PLpgSQL_stmt_if
+PLpgSQL_stmt_let
 PLpgSQL_stmt_loop
 PLpgSQL_stmt_open
 PLpgSQL_stmt_perform
@@ -2462,6 +2466,7 @@ SerializedTransactionState
 Session
 SessionBackupState
 SessionEndType
+SessionVariableValue
 SetConstraintState
 SetConstraintStateData
 SetConstraintTriggerData
@@ -2647,6 +2652,9 @@ SupportRequestRows
 SupportRequestSelectivity
 SupportRequestSimplify
 SupportRequestWFuncMonotonic
+SVariable
+SVariableData
+SVariableXActActionItem
 Syn
 SyncOps
 SyncRepConfigData
@@ -2916,6 +2924,7 @@ Variable
 VariableAssignHook
 VariableCache
 VariableCacheData
+VariableInfo
 VariableSetKind
 VariableSetStmt
 VariableShowStmt
-- 
2.37.2

From dc6516d77b7cb620200b27ea78c45083304224e6 Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Mon, 4 Apr 2022 20:49:11 +0200
Subject: [PATCH 08/12] possibility to dump session variables by pg_dump

Enhancing pg_dump about session variables support
---
 src/bin/pg_dump/common.c             |   3 +-
 src/bin/pg_dump/dumputils.c          |   6 +
 src/bin/pg_dump/pg_backup.h          |   2 +
 src/bin/pg_dump/pg_backup_archiver.c |  12 +-
 src/bin/pg_dump/pg_dump.c            | 237 ++++++++++++++++++++++++++-
 src/bin/pg_dump/pg_dump.h            |  25 ++-
 src/bin/pg_dump/pg_dump_sort.c       |   6 +
 src/bin/pg_dump/pg_restore.c         |   9 +-
 src/bin/pg_dump/t/002_pg_dump.pl     |  65 ++++++++
 9 files changed, 360 insertions(+), 5 deletions(-)

index 794e6e7ce9..97270e0b06 100644

 	*numTablesPtr = numTables;
 	return tblinfo;
diff --git a/src/bin/pg_dump/dumputils.c b/src/bin/pg_dump/dumputils.c
index 6e501a5413..dc1f0e1ba8 100644
--- a/src/bin/pg_dump/dumputils.c
+++ b/src/bin/pg_dump/dumputils.c
@@ -504,6 +504,12 @@ do { \
 		CONVERT_PRIV('r', "SELECT");
 		CONVERT_PRIV('w', "UPDATE");
 	}
+	else if (strcmp(type, "VARIABLE") == 0 ||
+			 strcmp(type, "VARIABLES") == 0)
+	{
+		CONVERT_PRIV('r', "SELECT");
+		CONVERT_PRIV('w', "UPDATE");
+	}
 	else
 		abort();
 
index fcc5f6bd05..9d675d669c 100644
@@ -131,12 +131,14 @@ typedef struct _restoreOptions
 
 	int			useDB;
index 233198afc0..8d47869ba8 100644
@@ -2934,6 +2934,14 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
@@ -3419,6 +3427,7 @@ _getObjectDescription(PQExpBuffer buf, TocEntry *te)
@@ -3611,7 +3620,8 @@ _printTocEntry(ArchiveHandle *AH, TocEntry *te, bool isData)
 
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 2c68915732..7f33dfe5f9 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -287,6 +287,7 @@ static void dumpPolicy(Archive *fout, const PolicyInfo *polinfo);
 static void dumpPublication(Archive *fout, const PublicationInfo *pubinfo);
 static void dumpPublicationTable(Archive *fout, const PublicationRelInfo *pubrinfo);
 static void dumpSubscription(Archive *fout, const SubscriptionInfo *subinfo);
+static void dumpVariable(Archive *fout, const VariableInfo * varinfo);
 static void dumpDatabase(Archive *AH);
 static void dumpDatabaseConfig(Archive *AH, PQExpBuffer outbuf,
 							   const char *dbname, Oid dboid);
@@ -4741,6 +4742,232 @@ get_next_possible_free_pg_type_oid(Archive *fout, PQExpBuffer upgrade_query)
 	return next_possible_free_oid;
 }
 
+/*
+ * getVariables
+	int			i_varowner;
+	int			i_varcollation;
+	if (fout->remoteVersion < 160000)
+					  "SELECT v.tableoid, v.oid, v.varname,\n"
+					  "v.vareoxaction,\n"
+					  "v.varnamespace,\n"
+					  "v.vartype,\n"
+					  "pg_catalog.format_type(v.vartype, v.vartypmod) as vartypname,\n"
+					  "v.varisnotnull,\n"
+					  "v.varisimmutable,\n"
+					  "CASE WHEN v.varcollation <> t.typcollation "
+					  "THEN v.varcollation ELSE 0 END AS varcollation,\n"
+					  "pg_catalog.pg_get_expr(v.vardefexpr,0) as vardefexpr,\n"
+					  "v.varowner,\n"
+					  "v.varacl,\n"
+					  "acldefault('V', v.varowner) AS acldefault\n"
+					  "FROM pg_catalog.pg_variable v\n"
+					  "JOIN pg_catalog.pg_type t "
+					  "ON (v.vartype = t.oid)");
+	i_varcollation = PQfnumber(res, "varcollation");
+	i_varowner = PQfnumber(res, "varowner");
+		varinfo[i].varcollation = atooid(PQgetvalue(res, i, i_varcollation));
+		varinfo[i].rolname = getRoleName(PQgetvalue(res, i, i_varowner));
+	Oid			varcollation;
+	varisnotnull = varinfo->varisnotnull;
+	varcollation = varinfo->varcollation;
+	appendPQExpBuffer(query, "CREATE %sVARIABLE %s AS %s",
+					  varisimmutable, qualvarname, vartypname);
+	if (OidIsValid(varcollation))
+		CollInfo   *coll;
+		coll = findCollationByOid(varcollation);
+		if (coll)
+			appendPQExpBuffer(query, " COLLATE %s",
+							  fmtQualifiedDumpable(coll));
+	if (varisnotnull)
+		appendPQExpBuffer(query, " NOT NULL");
@@ -9395,7 +9622,8 @@ getAdditionalACLs(Archive *fout)
 
@@ -9985,6 +10213,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
 		case DO_SUBSCRIPTION:
 			dumpSubscription(fout, (const SubscriptionInfo *) dobj);
 			break;
+		case DO_VARIABLE:
+			dumpVariable(fout, (VariableInfo *) dobj);
+			break;
 		case DO_PRE_DATA_BOUNDARY:
 		case DO_POST_DATA_BOUNDARY:
 			/* never dumped, nothing to do */
@@ -14328,6 +14559,9 @@ dumpDefaultACL(Archive *fout, const DefaultACLInfo *daclinfo)
 		case DEFACLOBJ_NAMESPACE:
 			type = "SCHEMAS";
 			break;
+		case DEFACLOBJ_VARIABLE:
+			type = "VARIABLES";
+			break;
 		default:
 			/* shouldn't get here */
 			pg_fatal("unrecognized object type in default privileges: %d",
@@ -17869,6 +18103,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 			case DO_CONVERSION:
 			case DO_TABLE:
 			case DO_TABLE_ATTACH:
+			case DO_VARIABLE:
 			case DO_ATTRDEF:
 			case DO_PROCLANG:
 			case DO_CAST:
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 69ee939d44..8d719bd35e 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -52,6 +52,7 @@ typedef enum
 	DO_TABLE,
 	DO_TABLE_ATTACH,
 	DO_ATTRDEF,
+	DO_VARIABLE,
 	DO_INDEX,
 	DO_INDEX_ATTACH,
 	DO_STATSEXT,
@@ -82,7 +83,7 @@ typedef enum
 	DO_PUBLICATION,
 	DO_PUBLICATION_REL,
 	DO_PUBLICATION_TABLE_IN_SCHEMA,
-	DO_SUBSCRIPTION
+	DO_SUBSCRIPTION,
 } DumpableObjectType;
 
 /*
@@ -664,6 +665,27 @@ typedef struct _SubscriptionInfo
 	char	   *subpublications;
 } SubscriptionInfo;
 
+/*
+ * The VariableInfo struct is used to represent session variables
+ */
+typedef struct _VariableInfo
+{
+	DumpableObject dobj;
+	DumpableAcl dacl;
+	Oid			vartype;
+	char	   *vartypname;
+	char	   *vareoxaction;
+	char	   *vardefexpr;
+	char	   *varacl;
+	char	   *rvaracl;
+	char	   *initvaracl;
+	char	   *initrvaracl;
+	bool		varisnotnull;
+	bool		varisimmutable;
+	Oid			varcollation;
+	const char *rolname;		/* name of owner, or empty string */
+}			VariableInfo;
+
 /*
  *	common utility functions
  */
@@ -746,5 +768,6 @@ extern void getPublicationNamespaces(Archive *fout);
 extern void getPublicationTables(Archive *fout, TableInfo tblinfo[],
 								 int numTables);
 extern void getSubscriptions(Archive *fout);
+extern void getVariables(Archive *fout);
 
 #endif							/* PG_DUMP_H */
index 5de3241eb4..1a660b8a9f 100644
index 049a100634..830cde5421 100644
@@ -103,6 +103,7 @@ main(int argc, char **argv)
 
@@ -151,7 +152,7 @@ main(int argc, char **argv)
 		}
 	}
 
-	while ((c = getopt_long(argc, argv, "acCd:ef:F:h:I:j:lL:n:N:Op:P:RsS:t:T:U:vwWx1",
+	while ((c = getopt_long(argc, argv, "A:acCd:ef:F:h:I:j:lL:n:N:Op:P:RsS:t:T:U:vwWx1",
 							cmdopts, NULL)) != -1)
 	{
 		switch (c)
@@ -159,6 +160,11 @@ main(int argc, char **argv)
 			case 'a':			/* Dump data only */
 				opts->dataOnly = 1;
 				break;
+			case 'A':			/* vAriable */
+				opts->selTypes = 1;
+				opts->selVariable = 1;
+				simple_string_list_append(&opts->variableNames, optarg);
+				break;
 			case 'c':			/* clean (i.e., drop) schema prior to create */
 				opts->dropSchema = 1;
 				break;
@@ -444,6 +450,7 @@ usage(const char *progname)
 
index b10e1c4c0d..e5cbb50722 100644
@@ -572,6 +572,16 @@ my %tests = (
 
+	'ALTER DEFAULT PRIVILEGES FOR ROLE regress_dump_test_role GRANT SELECT ON VARIABLES TO PUBLIC'
+	  => {
+		create_order => 56,
+		create_sql   => 'ALTER DEFAULT PRIVILEGES FOR ROLE regress_dump_test_role GRANT SELECT ON VARIABLES TO PUBLIC;',
+		regexp => qr/^
+			\QALTER DEFAULT PRIVILEGES FOR ROLE regress_dump_test_role GRANT SELECT ON VARIABLES  TO PUBLIC;\E/xm,
+		like => { %full_runs, section_post_data => 1, },
+		unlike => { no_privs => 1, },
+	  },
+
 	'ALTER ROLE regress_dump_test_role' => {
 		regexp => qr/^
 			\QALTER ROLE regress_dump_test_role WITH \E
@@ -1327,6 +1337,22 @@ my %tests = (
 		unlike => { exclude_dump_test_schema => 1, },
 	},
 
@@ -3254,6 +3280,30 @@ my %tests = (
 		},
 	},
 
@@ -3693,6 +3743,21 @@ my %tests = (
 		like => {},
 	},
 
+	'GRANT SELECT ON VARIABLE dump_test.variable1' => {
+		create_order => 73,
+		create_sql =>
+		  'GRANT SELECT ON VARIABLE dump_test.variable1 TO regress_dump_test_role;',
+		regexp => qr/^
+			\QGRANT SELECT ON VARIABLE dump_test.variable1 TO regress_dump_test_role;\E
+			/xm,
+		like =>
+		  { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => {
+			exclude_dump_test_schema => 1,
+			no_privs                 => 1,
+		},
+	},
+
 	'REFRESH MATERIALIZED VIEW matview' => {
 		regexp => qr/^\QREFRESH MATERIALIZED VIEW dump_test.matview;\E/m,
 		like =>
-- 
2.37.2

From 3fb46bb637a6b2cb85b0859eacf89c9c95c2c6f1 Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Mon, 4 Apr 2022 20:54:43 +0200
Subject: [PATCH 10/12] regress tests for session variables

Checks of basic functionality, check of usage session variables in subtransactions
and from PLpgSQL
---
 .../regress/expected/session_variables.out    | 1158 +++++++++++++++++
 src/test/regress/parallel_schedule            |    2 +-
 src/test/regress/sql/session_variables.sql    |  896 +++++++++++++
 3 files changed, 2055 insertions(+), 1 deletion(-)
 create mode 100644 src/test/regress/expected/session_variables.out
 create mode 100644 src/test/regress/sql/session_variables.sql

diff --git a/src/test/regress/expected/session_variables.out b/src/test/regress/expected/session_variables.out
new file mode 100644
index 0000000000..054d3be912
--- /dev/null
+++ b/src/test/regress/expected/session_variables.out
@@ -0,0 +1,1158 @@
+CREATE SCHEMA svartest;
+SET search_path = svartest;
+CREATE VARIABLE var1 AS integer;
+CREATE TEMP VARIABLE var2 AS text;
+DROP VARIABLE var1, var2;
+-- functional interface
+CREATE VARIABLE var1 AS numeric;
+CREATE ROLE var_test_role;
+GRANT USAGE ON SCHEMA svartest TO var_test_role;
+SET ROLE TO var_test_role;
+-- should fail
+SELECT var1;
+ERROR:  permission denied for session variable var1
+SET ROLE TO DEFAULT;
+GRANT SELECT ON VARIABLE var1 TO var_test_role;
+SET ROLE TO var_test_role;
+-- should fail
+LET var1 = 10;
+ERROR:  permission denied for session variable var1
+-- should work
+SELECT var1;
+ var1 
+------
+     
+(1 row)
+
+SET ROLE TO DEFAULT;
+GRANT UPDATE ON VARIABLE var1 TO var_test_role;
+SET ROLE TO var_test_role;
+-- should work
+LET var1 = 333;
+SET ROLE TO DEFAULT;
+REVOKE ALL ON VARIABLE var1 FROM var_test_role;
+CREATE OR REPLACE FUNCTION secure_var()
+RETURNS int AS $$
+  SELECT svartest.var1::int;
+$$ LANGUAGE sql SECURITY DEFINER;
+SELECT secure_var();
+ secure_var 
+------------
+        333
+(1 row)
+
+SET ROLE TO var_test_role;
+-- should fail
+SELECT svartest.var1;
+ERROR:  permission denied for session variable var1
+-- should work;
+SELECT secure_var();
+ secure_var 
+------------
+        333
+(1 row)
+
+SET ROLE TO DEFAULT;
+EXPLAIN (VERBOSE, COSTS OFF) SELECT * FROM generate_series(1,100) g(v) WHERE v = var1;
+                  QUERY PLAN                   
+-----------------------------------------------
+ Function Scan on pg_catalog.generate_series g
+   Output: v
+   Function Call: generate_series(1, 100)
+   Filter: ((g.v)::numeric = var1)
+(4 rows)
+
+CREATE VIEW schema_var_view AS SELECT var1;
+SELECT * FROM schema_var_view;
+ var1 
+------
+  333
+(1 row)
+
+\c -
+SET search_path = svartest;
+-- should work still, but var will be empty
+SELECT * FROM schema_var_view;
+ var1 
+------
+     
+(1 row)
+
+LET var1 = pi();
+SELECT var1;
+       var1       
+------------------
+ 3.14159265358979
+(1 row)
+
+-- we can see execution plan of LET statement
+EXPLAIN (VERBOSE, COSTS OFF) LET var1 = pi();
+         QUERY PLAN         
+----------------------------
+ SET SESSION VARIABLE
+ Result
+   Output: 3.14159265358979
+(3 rows)
+
+SELECT var1;
+       var1       
+------------------
+ 3.14159265358979
+(1 row)
+
+CREATE VARIABLE var3 AS int;
+CREATE OR REPLACE FUNCTION inc(int)
+RETURNS int AS $$
+BEGIN
+  LET svartest.var3 = COALESCE(svartest.var3 + $1, $1);
+  RETURN var3;
+END;
+$$ LANGUAGE plpgsql;
+SELECT inc(1);
+ inc 
+-----
+   1
+(1 row)
+
+SELECT inc(1);
+ inc 
+-----
+   2
+(1 row)
+
+SELECT inc(1);
+ inc 
+-----
+   3
+(1 row)
+
+SELECT inc(1) FROM generate_series(1,10);
+ inc 
+-----
+   4
+   5
+   6
+   7
+   8
+   9
+  10
+  11
+  12
+  13
+(10 rows)
+
+SET ROLE TO var_test_role;
+-- should fail
+LET var3 = 0;
+ERROR:  permission denied for session variable var3
+SET ROLE TO DEFAULT;
+DROP VIEW schema_var_view;
+DROP VARIABLE var1 CASCADE;
+DROP VARIABLE var3 CASCADE;
+-- 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;
+\d v1
+\d v2
+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.14);
+SELECT v1;
+     v1     
+------------
+ (1,2,3.14)
+(1 row)
+
+SELECT v2;
+      v2       
+---------------
+ (10,20,31.40)
+(1 row)
+
+SELECT (v1).*;
+ x | y |  z   
+---+---+------
+ 1 | 2 | 3.14
+(1 row)
+
+SELECT (v2).*;
+ x  | y  |   z   
+----+----+-------
+ 10 | 20 | 31.40
+(1 row)
+
+SELECT v1.x + v1.z;
+ ?column? 
+----------
+     4.14
+(1 row)
+
+SELECT v2.x + v2.z;
+ ?column? 
+----------
+    41.40
+(1 row)
+
+-- access to composite fields should be safe too
+-- should fail
+SET ROLE TO var_test_role;
+SELECT v2.x;
+ERROR:  permission denied for session variable v2
+SET ROLE TO DEFAULT;
+DROP VARIABLE v1;
+DROP VARIABLE v2;
+REVOKE USAGE ON SCHEMA svartest FROM var_test_role;
+DROP ROLE var_test_role;
+-- scalar variables should not be in conflict with qualified column
+CREATE VARIABLE varx AS text;
+SELECT varx.relname FROM pg_class varx WHERE varx.relname = 'pg_class';
+ relname  
+----------
+ pg_class
+(1 row)
+
+-- should fail
+SELECT varx.xxx;
+ERROR:  type text is not composite
+-- variables can be updated under RO transaction
+BEGIN;
+SET TRANSACTION READ ONLY;
+LET varx = 'hello';
+COMMIT;
+SELECT varx;
+ varx  
+-------
+ hello
+(1 row)
+
+DROP VARIABLE varx;
+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
+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;
+            ^
+DROP VARIABLE v1;
+DROP TYPE t1;
+-- arrays are supported
+CREATE VARIABLE va1 AS numeric[];
+LET va1 = ARRAY[1.1,2.1];
+LET va1[1] = 10.1;
+SELECT va1;
+    va1     
+------------
+ {10.1,2.1}
+(1 row)
+
+CREATE TYPE ta2 AS (a numeric, b numeric[]);
+CREATE VARIABLE va2 AS ta2;
+LET va2 = (10.1, ARRAY[0.0, 0.0]);
+LET va2.a = 10.2;
+SELECT va2;
+        va2         
+--------------------
+ (10.2,"{0.0,0.0}")
+(1 row)
+
+LET va2.b[1] = 10.3;
+SELECT va2;
+         va2         
+---------------------
+ (10.2,"{10.3,0.0}")
+(1 row)
+
+DROP VARIABLE va1;
+DROP VARIABLE va2;
+DROP TYPE ta2;
+-- default values
+CREATE VARIABLE v1 AS numeric DEFAULT pi();
+LET v1 = v1 * 2;
+SELECT v1;
+        v1        
+------------------
+ 6.28318530717958
+(1 row)
+
+CREATE TYPE t2 AS (a numeric, b text);
+CREATE VARIABLE v2 AS t2 DEFAULT (NULL, 'Hello');
+LET svartest.v2.a = pi();
+SELECT v2;
+            v2            
+--------------------------
+ (3.14159265358979,Hello)
+(1 row)
+
+-- should fail due dependency
+DROP TYPE t2;
+ERROR:  cannot drop type t2 because other objects depend on it
+DETAIL:  session variable v2 depends on type t2
+HINT:  Use DROP ... CASCADE to drop the dependent objects too.
+-- should be ok
+DROP VARIABLE v1;
+DROP VARIABLE v2;
+-- tests of alters
+CREATE SCHEMA var_schema1;
+CREATE SCHEMA var_schema2;
+CREATE VARIABLE var_schema1.var1 AS integer;
+LET var_schema1.var1 = 1000;
+SELECT var_schema1.var1;
+ var1 
+------
+ 1000
+(1 row)
+
+ALTER VARIABLE var_schema1.var1 SET SCHEMA var_schema2;
+SELECT var_schema2.var1;
+ var1 
+------
+ 1000
+(1 row)
+
+CREATE ROLE var_test_role;
+ALTER VARIABLE var_schema2.var1 OWNER TO var_test_role;
+SET ROLE TO var_test_role;
+-- should fail, no access to schema var_schema2.var
+SELECT var_schema2.var1;
+ERROR:  permission denied for schema var_schema2
+DROP VARIABLE var_schema2.var1;
+ERROR:  permission denied for schema var_schema2
+SET ROLE TO DEFAULT;
+ALTER VARIABLE var_schema2.var1 SET SCHEMA public;
+SET ROLE TO var_test_role;
+SELECT public.var1;
+ var1 
+------
+ 1000
+(1 row)
+
+ALTER VARIABLE public.var1 RENAME TO var1_renamed;
+SELECT public.var1_renamed;
+ var1_renamed 
+--------------
+         1000
+(1 row)
+
+DROP VARIABLE public.var1_renamed;
+SET ROLE TO DEFAULt;
+-- default rights test
+ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON VARIABLES TO var_test_role;
+CREATE VARIABLE public.var2 AS int;
+SET ROLE TO var_test_role;
+-- should be ok
+LET public.var2 = 100;
+SELECT public.var2;
+ var2 
+------
+  100
+(1 row)
+
+SET ROLE TO DEFAULt;
+DROP VARIABLE public.var2;
+DROP OWNED BY var_test_role;
+DROP ROLE var_test_role;
+CREATE VARIABLE xx AS text DEFAULT 'hello';
+SELECT xx, upper(xx);
+  xx   | upper 
+-------+-------
+ hello | HELLO
+(1 row)
+
+LET xx = 'Hi';
+SELECT xx;
+ xx 
+----
+ Hi
+(1 row)
+
+DROP VARIABLE xx;
+-- ON TRANSACTION END RESET tests
+CREATE VARIABLE t1 AS int DEFAULT -1 ON TRANSACTION END RESET;
+BEGIN;
+  SELECT t1;
+ t1 
+----
+ -1
+(1 row)
+
+  LET t1 = 100;
+  SELECT t1;
+ t1  
+-----
+ 100
+(1 row)
+
+COMMIT;
+SELECT t1;
+ t1 
+----
+ -1
+(1 row)
+
+BEGIN;
+  SELECT t1;
+ t1 
+----
+ -1
+(1 row)
+
+  LET t1 = 100;
+  SELECT t1;
+ t1  
+-----
+ 100
+(1 row)
+
+ROLLBACK;
+SELECT t1;
+ t1 
+----
+ -1
+(1 row)
+
+DROP VARIABLE t1;
+CREATE VARIABLE v1 AS int DEFAULT 0;
+CREATE VARIABLE v2 AS text DEFAULT 'none';
+LET v1 = 100;
+LET v2 = 'Hello';
+SELECT v1, v2;
+ v1  |  v2   
+-----+-------
+ 100 | Hello
+(1 row)
+
+LET v1 = DEFAULT;
+LET v2 = DEFAULT;
+SELECT v1, v2;
+ v1 |  v2  
+----+------
+  0 | none
+(1 row)
+
+DROP VARIABLE v1;
+DROP VARIABLE v2;
+-- ON COMMIT DROP tests
+-- should be 0 always
+SELECT count(*) FROM pg_variable;
+ count 
+-------
+     0
+(1 row)
+
+CREATE TEMP VARIABLE g AS int ON COMMIT DROP;
+SELECT count(*) FROM pg_variable;
+ count 
+-------
+     0
+(1 row)
+
+BEGIN;
+  CREATE TEMP VARIABLE g AS int ON COMMIT DROP;
+COMMIT;
+SELECT count(*) FROM pg_variable;
+ count 
+-------
+     0
+(1 row)
+
+BEGIN;
+  CREATE TEMP VARIABLE g AS int ON COMMIT DROP;
+ROLLBACK;
+SELECT count(*) FROM pg_variable;
+ count 
+-------
+     0
+(1 row)
+
+-- 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;
+-- parallel workers should be used
+EXPLAIN (costs off) SELECT count(*) FROM svar_test WHERE a%10 = zero;
+                 QUERY PLAN                 
+--------------------------------------------
+ Aggregate
+   ->  Gather
+         Workers Planned: 2
+         ->  Parallel Seq Scan on svar_test
+               Filter: ((a % 10) = zero)
+(5 rows)
+
+-- result should be 100
+SELECT count(*) FROM svar_test WHERE a%10 = zero;
+ count 
+-------
+   100
+(1 row)
+
+LET zero = (SELECT count(*) FROM svar_test);
+-- result should be 1000
+SELECT zero;
+ zero 
+------
+ 1000
+(1 row)
+
+-- parallel workers should be used
+EXPLAIN (costs off) LET zero = (SELECT count(*) FROM svar_test);
+                        QUERY PLAN                        
+----------------------------------------------------------
+ SET SESSION VARIABLE
+ Result
+   InitPlan 1 (returns $1)
+     ->  Finalize Aggregate
+           ->  Gather
+                 Workers Planned: 2
+                 ->  Partial Aggregate
+                       ->  Parallel Seq Scan on svar_test
+(8 rows)
+
+DROP TABLE svar_test;
+DROP VARIABLE zero;
+RESET parallel_setup_cost;
+RESET parallel_tuple_cost;
+RESET min_parallel_table_scan_size;
+RESET max_parallel_workers_per_gather;
+-- use variables in prepared statements
+CREATE VARIABLE v AS numeric;
+LET v = 3.14;
+-- use variables in views
+CREATE VIEW vv AS SELECT COALESCE(v, 0) + 1000 AS result;
+SELECT * FROM vv;
+ result  
+---------
+ 1003.14
+(1 row)
+
+-- start a new session
+\c
+SET search_path to svartest;
+SELECT * FROM vv;
+ result 
+--------
+   1000
+(1 row)
+
+LET v = 3.14;
+SELECT * FROM vv;
+ result  
+---------
+ 1003.14
+(1 row)
+
+-- should fail, dependency
+DROP VARIABLE v;
+ERROR:  cannot drop session variable v because other objects depend on it
+DETAIL:  view vv depends on session variable v
+HINT:  Use DROP ... CASCADE to drop the dependent objects too.
+-- should be ok
+DROP VARIABLE v CASCADE;
+NOTICE:  drop cascades to view vv
+-- other features
+CREATE VARIABLE dt AS integer DEFAULT 0;
+LET dt = 100;
+SELECT dt;
+ dt  
+-----
+ 100
+(1 row)
+
+DISCARD VARIABLES;
+SELECT dt;
+ dt 
+----
+  0
+(1 row)
+
+DROP VARIABLE dt;
+-- NOT NULL
+CREATE VARIABLE v1 AS int NOT NULL;
+CREATE VARIABLE v2 AS int NOT NULL DEFAULT NULL;
+-- should fail
+SELECT v1;
+ERROR:  null value is not allowed for NOT NULL session variable "svartest.v1"
+DETAIL:  The session variable was not initialized yet.
+SELECT v2;
+ERROR:  null value is not allowed for NOT NULL session variable "svartest.v2"
+LET v1 = NULL;
+ERROR:  null value is not allowed for NOT NULL session variable "svartest.v1"
+LET v2 = NULL;
+ERROR:  null value is not allowed for NOT NULL session variable "svartest.v2"
+LET v1 = DEFAULT;
+ERROR:  null value is not allowed for NOT NULL session variable "svartest.v1"
+LET v2 = DEFAULT;
+ERROR:  null value is not allowed for NOT NULL session variable "svartest.v2"
+-- should be ok
+LET v1 = 100;
+LET v2 = 1000;
+SELECT v1, v2;
+ v1  |  v2  
+-----+------
+ 100 | 1000
+(1 row)
+
+DROP VARIABLE v1;
+DROP VARIABLE v2;
+CREATE VARIABLE tv AS int;
+CREATE VARIABLE IF NOT EXISTS tv AS int;
+NOTICE:  session variable "tv" already exists, skipping
+DROP VARIABLE tv;
+CREATE IMMUTABLE VARIABLE iv AS int DEFAULT 100;
+SELECT iv;
+ iv  
+-----
+ 100
+(1 row)
+
+-- should fail;
+LET iv = 10000;
+ERROR:  session variable "svartest.iv" is declared IMMUTABLE
+DROP VARIABLE iv;
+-- different order
+CREATE IMMUTABLE VARIABLE iv AS int DEFAULT 100;
+-- should to fail
+LET iv = 10000;
+ERROR:  session variable "svartest.iv" is declared IMMUTABLE
+-- should be ok
+SELECT iv;
+ iv  
+-----
+ 100
+(1 row)
+
+DROP VARIABLE iv;
+CREATE IMMUTABLE VARIABLE iv AS int;
+-- should be ok
+LET iv = NULL;
+-- should fail
+LET iv = NULL;
+ERROR:  session variable "svartest.iv" is declared IMMUTABLE
+DROP VARIABLE iv;
+-- create variable inside plpgsql block
+DO $$
+BEGIN
+  CREATE VARIABLE do_test_svar AS date DEFAULT '2000-01-01';
+END;
+$$;
+SELECT do_test_svar;
+ do_test_svar 
+--------------
+ 01-01-2000
+(1 row)
+
+DROP VARIABLE do_test_svar;
+-- should fail
+CREATE IMMUTABLE VARIABLE xx AS int NOT NULL;
+ERROR:  IMMUTABLE NOT NULL variable requires default expression
+-- REASSIGN OWNED test
+CREATE ROLE var_test_role1;
+CREATE ROLE var_test_role2;
+CREATE VARIABLE xxx_var AS int;
+ALTER VARIABLE xxx_var OWNER TO var_test_role1;
+REASSIGN OWNED BY var_test_role1 to var_test_role2;
+SELECT varowner::regrole FROM pg_variable WHERE varname = 'xxx_var';
+    varowner    
+----------------
+ var_test_role2
+(1 row)
+
+DROP OWNED BY var_test_role1;
+DROP ROLE var_test_role1;
+SELECT count(*) FROM pg_variable WHERE varname = 'xxx_var';
+ count 
+-------
+     1
+(1 row)
+
+DROP OWNED BY var_test_role2;
+DROP ROLE var_test_role2;
+SELECT count(*) FROM pg_variable WHERE varname = 'xxx_var';
+ count 
+-------
+     0
+(1 row)
+
+-- creating, dropping temporary variable
+BEGIN;
+CREATE TEMP VARIABLE tempvar AS INT ON COMMIT DROP;
+LET tempvar = 100;
+SAVEPOINT s1;
+DROP VARIABLE tempvar;
+ROLLBACK TO s1;
+SELECT tempvar;
+ tempvar 
+---------
+     100
+(1 row)
+
+COMMIT;
+-- should to fail
+LET tempvar = 100;
+ERROR:  session variable "tempvar" doesn't exist
+LINE 1: LET tempvar = 100;
+            ^
+BEGIN;
+SAVEPOINT s1;
+CREATE TEMP VARIABLE tempvar AS INT ON COMMIT DROP;
+LET tempvar = 100;
+ROLLBACK TO s1;
+COMMIT;
+-- should to fail
+LET tempvar = 100;
+ERROR:  session variable "tempvar" doesn't exist
+LINE 1: LET tempvar = 100;
+            ^
+CREATE VARIABLE var1 AS int;
+LET var1 = 100;
+BEGIN;
+DROP VARIABLE var1;
+ROLLBACK;
+SELECT var1;
+ var1 
+------
+  100
+(1 row)
+
+DROP VARIABLE var1;
+CREATE VARIABLE var1 AS int DEFAULT 100;
+COMMENT ON VARIABLE var1 IS 'some variable comment';
+SELECT pg_catalog.obj_description(oid, 'pg_variable') FROM pg_variable WHERE varname = 'var1';
+    obj_description    
+-----------------------
+ some variable comment
+(1 row)
+
+DROP VARIABLE var1;
+CREATE TABLE xxtab(avar int);
+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 
+------
+   10
+(1 row)
+
+SET session_variables_ambiguity_warning TO on;
+SELECT xxtab.avar FROM xxtab;
+WARNING:  session variable "xxtab.avar" is shadowed
+LINE 1: SELECT xxtab.avar FROM xxtab;
+               ^
+DETAIL:  Session variables can be shadowed by columns, routine's variables and routine's arguments with the same name.
+ avar 
+------
+   10
+(1 row)
+
+SET search_path = svartest;
+CREATE VARIABLE testvar as int;
+-- plpgsql variables are preferred against session variables
+DO $$
+<<myblock>>
+DECLARE testvar int;
+BEGIN
+  -- should be ok without warning
+  LET testvar = 100;
+  -- should be ok without warning
+  testvar := 1000;
+  -- should be ok without warning
+  RAISE NOTICE 'session variable is %', svartest.testvar;
+  -- should be ok without warning
+  RAISE NOTICE 'plpgsql variable is %', myblock.testvar;
+  -- should to print plpgsql variable with warning
+  RAISE NOTICE 'variable is %', testvar;
+END;
+$$;
+NOTICE:  session variable is 100
+NOTICE:  plpgsql variable is 1000
+WARNING:  session variable "testvar" is shadowed
+LINE 1: testvar
+        ^
+DETAIL:  Session variables can be shadowed by columns, routine's variables and routine's arguments with the same name.
+QUERY:  testvar
+NOTICE:  variable is 1000
+DROP VARIABLE testvar;
+SET session_variables_ambiguity_warning TO default;
+-- should be ok
+SELECT avar FROM xxtab;
+ avar 
+------
+   10
+(1 row)
+
+CREATE VARIABLE public.avar AS int;
+-- should to fail
+SELECT avar FROM xxtab;
+ avar 
+------
+   10
+(1 row)
+
+-- should be ok
+SELECT public.avar FROM xxtab;
+ avar 
+------
+     
+(1 row)
+
+DROP VARIABLE xxtab;
+SELECT xxtab.avar FROM xxtab;
+ avar 
+------
+   10
+(1 row)
+
+DROP VARIABLE public.avar;
+DROP TYPE xxtype;
+DROP TABLE xxtab;
+-- test of plan cache invalidation
+CREATE VARIABLE xx AS int;
+SET plan_cache_mode = force_generic_plan;
+PREPARE pp AS SELECT xx;
+EXECUTE pp;
+ xx 
+----
+   
+(1 row)
+
+DROP VARIABLE xx;
+CREATE VARIABLE xx AS int;
+-- should to work
+EXECUTE pp;
+ xx 
+----
+   
+(1 row)
+
+DROP VARIABLE xx;
+DEALLOCATE pp;
+SET plan_cache_mode = DEFAULT;
+CREATE ROLE var_test_role;
+CREATE SCHEMA vartest;
+GRANT USAGE ON SCHEMA vartest TO var_test_role;
+CREATE VARIABLE vartest.x AS int;
+CREATE VARIABLE vartest.y AS int;
+LET vartest.x = 100;
+LET vartest.y = 101;
+GRANT SELECT ON ALL VARIABLES IN SCHEMA vartest TO var_test_role;
+SET ROLE TO var_test_role;
+SELECT vartest.x, vartest.y;
+  x  |  y  
+-----+-----
+ 100 | 101
+(1 row)
+
+SET ROLE TO DEFAULT;
+REVOKE SELECT ON ALL VARIABLES IN SCHEMA vartest FROM var_test_role;
+SET ROLE TO var_test_role;
+-- should to fail
+SELECT vartest.x;
+ERROR:  permission denied for session variable x
+SELECT vartest.y;
+ERROR:  permission denied for session variable y
+SET ROLE TO DEFAULT;
+DROP VARIABLE vartest.x, vartest.y;
+DROP SCHEMA vartest;
+DROP ROLE var_test_role;
+-- test cached plan
+CREATE VARIABLE v1 AS text;
+CREATE VARIABLE v2 AS int;
+CREATE VARIABLE v3 AS int;
+LET v1 = 'test';
+LET v2 = 10;
+LET v3 = 5;
+PREPARE q1 AS SELECT v1 || i FROM generate_series(1, v2) g(i) WHERE i IN (v2, v3);
+SET plan_cache_mode to force_generic_plan;
+EXECUTE q1;
+ ?column? 
+----------
+ test5
+ test10
+(2 rows)
+
+EXPLAIN EXECUTE q1;
+                              QUERY PLAN                               
+-----------------------------------------------------------------------
+ Function Scan on generate_series g  (cost=0.00..0.14 rows=2 width=32)
+   Filter: (i = ANY (ARRAY[v2, v3]))
+(2 rows)
+
+-- dependecy check
+DROP VARIABLE v3;
+-- recreate v3 again
+CREATE VARIABLE v3 AS int DEFAULT 6;
+-- should to work, the plan should be recreated
+EXECUTE q1;
+ ?column? 
+----------
+ test6
+ test10
+(2 rows)
+
+DEALLOCATE q1;
+-- fill v1 by long text
+LET v1 = repeat(' ', 10000);
+PREPARE q1 AS SELECT length(v1);
+EXECUTE q1;
+ length 
+--------
+  10000
+(1 row)
+
+LET v1 = repeat(' ', 5000);
+EXECUTE q1;
+ length 
+--------
+   5000
+(1 row)
+
+DEALLOCATE q1;
+SET plan_cache_mode to default;
+DROP VARIABLE v1, v2, v3;
+CREATE ROLE var_test_role;
+CREATE VARIABLE public.v1 AS int DEFAULT 0;
+-- check acl when variable is acessed by simple eval expr method
+CREATE OR REPLACE FUNCTION public.fx_var(int)
+RETURNS int AS $$
+DECLARE xx int;
+BEGIN
+  xx := public.v1 + $1;
+  RETURN xx;
+END;
+$$ LANGUAGE plpgsql;
+-- should be ok
+SELECT public.fx_var(0);
+ fx_var 
+--------
+      0
+(1 row)
+
+SET ROLE TO var_test_role;
+-- should to fail
+SELECT public.fx_var(0);
+ERROR:  permission denied for session variable v1
+CONTEXT:  PL/pgSQL function public.fx_var(integer) line 4 at assignment
+SET ROLE TO default;
+GRANT SELECT ON VARIABLE public.v1 TO var_test_role;
+SET ROLE TO var_test_role;
+-- should be ok
+SELECT public.fx_var(0);
+ fx_var 
+--------
+      0
+(1 row)
+
+SET ROLE TO default;
+REVOKE SELECT ON VARIABLE public.v1 FROM var_test_role;
+SET ROLE TO var_test_role;
+-- should be fail
+SELECT public.fx_var(0);
+ERROR:  permission denied for session variable v1
+CONTEXT:  PL/pgSQL function public.fx_var(integer) line 4 at assignment
+SET ROLE TO DEFAULT;
+DROP FUNCTION public.fx_var(int);
+DROP VARIABLE public.v1;
+DROP ROLE var_test_role;
+CREATE TYPE public.svar_test_type AS (a int, b int, c numeric);
+CREATE VARIABLE public.svar AS public.svar_test_type;
+LET public.svar = ROW(10,20,30);
+SELECT public.svar;
+    svar    
+------------
+ (10,20,30)
+(1 row)
+
+ALTER TYPE public.svar_test_type DROP ATTRIBUTE c;
+-- should to fail
+SELECT public.svar;
+  svar   
+---------
+ (10,20)
+(1 row)
+
+ALTER TYPE public.svar_test_type ADD ATTRIBUTE c int;
+-- should to fail too (different type, different generation number);
+SELECT public.svar;
+   svar   
+----------
+ (10,20,)
+(1 row)
+
+LET public.svar = ROW(10,20,30);
+-- should be ok again for new value
+SELECT public.svar;
+    svar    
+------------
+ (10,20,30)
+(1 row)
+
+DROP VARIABLE public.svar;
+DROP TYPE public.svar_test_type;
+CREATE VARIABLE public.svar AS int;
+SELECT schema, name, removed FROM pg_debug_show_used_session_variables();
+ schema | name | removed 
+--------+------+---------
+(0 rows)
+
+LET public.svar = 100;
+SELECT schema, name, removed FROM pg_debug_show_used_session_variables();
+ schema | name | removed 
+--------+------+---------
+ public | svar | f
+(1 row)
+
+BEGIN;
+DROP VARIABLE public.svar;
+-- value should be in memory
+SELECT schema, name, removed FROM pg_debug_show_used_session_variables();
+ schema | name | removed 
+--------+------+---------
+ public | svar | t
+(1 row)
+
+ROLLBACK;
+-- value should be in memory
+SELECT schema, name, removed FROM pg_debug_show_used_session_variables() WHERE schema = 'public' and name = 'svar';
+ schema | name | removed 
+--------+------+---------
+ public | svar | f
+(1 row)
+
+SELECT public.svar;
+ svar 
+------
+  100
+(1 row)
+
+BEGIN;
+DROP VARIABLE public.svar;
+-- value should be in memory
+SELECT schema, name, removed FROM pg_debug_show_used_session_variables() WHERE schema = 'public' and name = 'svar';
+ schema | name | removed 
+--------+------+---------
+ public | svar | t
+(1 row)
+
+COMMIT;
+-- the memory should be clean;
+SELECT schema, name, removed FROM pg_debug_show_used_session_variables() WHERE schema = 'public' and name = 'svar';
+ schema | name | removed 
+--------+------+---------
+(0 rows)
+
+BEGIN;
+CREATE VARIABLE public.svar AS int;
+LET public.svar = 100;
+ROLLBACK;
+-- the memory should be clean;
+SELECT schema, name, removed FROM pg_debug_show_used_session_variables() WHERE schema = 'public' and name = 'svar';
+ schema | name | removed 
+--------+------+---------
+(0 rows)
+
+CREATE VARIABLE public.svar AS int;
+LET public.svar = 100;
+-- repeated aborted transaction
+BEGIN; DROP VARIABLE public.svar; ROLLBACK;
+BEGIN; DROP VARIABLE public.svar; ROLLBACK;
+BEGIN; DROP VARIABLE public.svar; ROLLBACK;
+-- the value should be still available
+SELECT public.svar;
+ svar 
+------
+  100
+(1 row)
+
+DROP VARIABLE public.svar;
+CREATE TYPE public.svar_test_type AS (a int, b int);
+CREATE VARIABLE public.svar AS public.svar_test_type;
+SELECT public.svar;
+ svar 
+------
+ 
+(1 row)
+
+ALTER TYPE public.svar_test_type ADD ATTRIBUTE c int;
+SELECT public.svar;
+ svar 
+------
+ 
+(1 row)
+
+ALTER TYPE public.svar_test_type DROP ATTRIBUTE b;
+SELECT public.svar;
+ svar 
+------
+ 
+(1 row)
+
+DROP VARIABLE public.svar;
+DROP TYPE public.svar_test_type;
+CREATE TYPE public.svar_test_type AS (a int, b int);
+CREATE VARIABLE public.svar AS public.svar_test_type;
+CREATE VARIABLE public.svar2 AS public.svar_test_type;
+LET public.svar = (10, 20);
+ALTER TYPE public.svar_test_type ADD ATTRIBUTE c int;
+SELECT public.svar;
+   svar   
+----------
+ (10,20,)
+(1 row)
+
+LET public.svar2 = (10, 20, 30);
+ALTER TYPE public.svar_test_type DROP ATTRIBUTE b;
+SELECT public.svar;
+ svar  
+-------
+ (10,)
+(1 row)
+
+SELECT public.svar2;
+  svar2  
+---------
+ (10,30)
+(1 row)
+
+DROP VARIABLE public.svar;
+DROP VARIABLE public.svar2;
+DROP TYPE public.svar_test_type;
+-- The composite type cannot be changed when it is used
+CREATE TYPE public.svar_type AS (a int, b int);
+CREATE VARIABLE public.svar AS public.svar_type;
+-- should to fail
+ALTER TYPE public.svar_type ALTER ATTRIBUTE b TYPE numeric;
+ERROR:  cannot alter type "svar_type" because session variable "public.svar" uses it
+DROP VARIABLE public.svar;
+CREATE TYPE public.svar_type2 AS (a int, b int, c public.svar_type);
+CREATE VARIABLE public.svar AS public.svar_type2;
+-- should to fail
+ALTER TYPE public.svar_type ALTER ATTRIBUTE b TYPE numeric;
+ERROR:  cannot alter type "svar_type" because session variable "public.svar" uses it
+DROP VARIABLE public.svar;
+DROP TYPE public.svar_type2;
+DROP TYPE public.svar_type;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 103e11483d..84a09bc40e 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -119,7 +119,7 @@ test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson
 # NB: temp.sql does a reconnect which transiently uses 2 connections,
 # so keep this parallel group to at most 19 tests
 # ----------
-test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml
+test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml session_variables
 
 # ----------
 # Another group of parallel tests
diff --git a/src/test/regress/sql/session_variables.sql b/src/test/regress/sql/session_variables.sql
new file mode 100644
index 0000000000..89b6470ff3
--- /dev/null
+++ b/src/test/regress/sql/session_variables.sql
@@ -0,0 +1,896 @@
+CREATE SCHEMA svartest;
+
+SET search_path = svartest;
+
+CREATE VARIABLE var1 AS integer;
+CREATE TEMP VARIABLE var2 AS text;
+
+DROP VARIABLE var1, var2;
+
+-- functional interface
+CREATE VARIABLE var1 AS numeric;
+
+CREATE ROLE var_test_role;
+GRANT USAGE ON SCHEMA svartest TO var_test_role;
+
+SET ROLE TO var_test_role;
+
+-- should fail
+SELECT var1;
+
+SET ROLE TO DEFAULT;
+
+GRANT SELECT ON VARIABLE var1 TO var_test_role;
+
+SET ROLE TO var_test_role;
+-- should fail
+LET var1 = 10;
+-- should work
+SELECT var1;
+
+SET ROLE TO DEFAULT;
+
+GRANT UPDATE ON VARIABLE var1 TO var_test_role;
+
+SET ROLE TO var_test_role;
+
+-- should work
+LET var1 = 333;
+
+SET ROLE TO DEFAULT;
+
+REVOKE ALL ON VARIABLE var1 FROM var_test_role;
+
+CREATE OR REPLACE FUNCTION secure_var()
+RETURNS int AS $$
+  SELECT svartest.var1::int;
+$$ LANGUAGE sql SECURITY DEFINER;
+
+SELECT secure_var();
+
+SET ROLE TO var_test_role;
+
+-- should fail
+SELECT svartest.var1;
+
+-- should work;
+SELECT secure_var();
+
+SET ROLE TO DEFAULT;
+
+EXPLAIN (VERBOSE, COSTS OFF) SELECT * FROM generate_series(1,100) g(v) WHERE v = var1;
+
+CREATE VIEW schema_var_view AS SELECT var1;
+
+SELECT * FROM schema_var_view;
+
+\c -
+
+SET search_path = svartest;
+
+-- should work still, but var will be empty
+SELECT * FROM schema_var_view;
+
+LET var1 = pi();
+
+SELECT var1;
+
+-- we can see execution plan of LET statement
+EXPLAIN (VERBOSE, COSTS OFF) LET var1 = pi();
+
+SELECT var1;
+
+CREATE VARIABLE var3 AS int;
+
+CREATE OR REPLACE FUNCTION inc(int)
+RETURNS int AS $$
+BEGIN
+  LET svartest.var3 = COALESCE(svartest.var3 + $1, $1);
+  RETURN var3;
+END;
+$$ LANGUAGE plpgsql;
+
+SELECT inc(1);
+SELECT inc(1);
+SELECT inc(1);
+
+SELECT inc(1) FROM generate_series(1,10);
+
+SET ROLE TO var_test_role;
+
+-- should fail
+LET var3 = 0;
+
+SET ROLE TO DEFAULT;
+
+DROP VIEW schema_var_view;
+
+DROP VARIABLE var1 CASCADE;
+DROP VARIABLE var3 CASCADE;
+
+-- 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;
+
+\d v1
+\d v2
+
+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.14);
+
+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
+-- should fail
+SET ROLE TO var_test_role;
+
+SELECT v2.x;
+
+SET ROLE TO DEFAULT;
+
+DROP VARIABLE v1;
+DROP VARIABLE v2;
+
+REVOKE USAGE ON SCHEMA svartest FROM var_test_role;
+DROP ROLE var_test_role;
+
+-- scalar variables should not be in conflict with qualified column
+CREATE VARIABLE varx AS text;
+SELECT varx.relname FROM pg_class varx WHERE varx.relname = 'pg_class';
+
+-- should fail
+SELECT varx.xxx;
+
+-- variables can be updated under RO transaction
+
+BEGIN;
+SET TRANSACTION READ ONLY;
+LET varx = 'hello';
+COMMIT;
+
+SELECT varx;
+
+DROP VARIABLE varx;
+
+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
+LET v1.x = 10;
+
+DROP VARIABLE v1;
+DROP TYPE t1;
+
+-- arrays are supported
+CREATE VARIABLE va1 AS numeric[];
+LET va1 = ARRAY[1.1,2.1];
+LET va1[1] = 10.1;
+SELECT va1;
+
+CREATE TYPE ta2 AS (a numeric, b numeric[]);
+CREATE VARIABLE va2 AS ta2;
+LET va2 = (10.1, ARRAY[0.0, 0.0]);
+LET va2.a = 10.2;
+SELECT va2;
+LET va2.b[1] = 10.3;
+SELECT va2;
+
+DROP VARIABLE va1;
+DROP VARIABLE va2;
+DROP TYPE ta2;
+
+-- default values
+CREATE VARIABLE v1 AS numeric DEFAULT pi();
+LET v1 = v1 * 2;
+SELECT v1;
+
+CREATE TYPE t2 AS (a numeric, b text);
+CREATE VARIABLE v2 AS t2 DEFAULT (NULL, 'Hello');
+LET svartest.v2.a = pi();
+SELECT v2;
+
+-- should fail due dependency
+DROP TYPE t2;
+
+-- should be ok
+DROP VARIABLE v1;
+DROP VARIABLE v2;
+
+-- tests of alters
+CREATE SCHEMA var_schema1;
+CREATE SCHEMA var_schema2;
+
+CREATE VARIABLE var_schema1.var1 AS integer;
+LET var_schema1.var1 = 1000;
+SELECT var_schema1.var1;
+ALTER VARIABLE var_schema1.var1 SET SCHEMA var_schema2;
+SELECT var_schema2.var1;
+
+CREATE ROLE var_test_role;
+
+ALTER VARIABLE var_schema2.var1 OWNER TO var_test_role;
+SET ROLE TO var_test_role;
+
+-- should fail, no access to schema var_schema2.var
+SELECT var_schema2.var1;
+DROP VARIABLE var_schema2.var1;
+
+SET ROLE TO DEFAULT;
+
+ALTER VARIABLE var_schema2.var1 SET SCHEMA public;
+
+SET ROLE TO var_test_role;
+SELECT public.var1;
+
+ALTER VARIABLE public.var1 RENAME TO var1_renamed;
+
+SELECT public.var1_renamed;
+
+DROP VARIABLE public.var1_renamed;
+
+SET ROLE TO DEFAULt;
+
+-- default rights test
+ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON VARIABLES TO var_test_role;
+
+CREATE VARIABLE public.var2 AS int;
+
+SET ROLE TO var_test_role;
+
+-- should be ok
+LET public.var2 = 100;
+SELECT public.var2;
+
+SET ROLE TO DEFAULt;
+
+DROP VARIABLE public.var2;
+DROP OWNED BY var_test_role;
+
+DROP ROLE var_test_role;
+
+CREATE VARIABLE xx AS text DEFAULT 'hello';
+
+SELECT xx, upper(xx);
+
+LET xx = 'Hi';
+
+SELECT xx;
+
+DROP VARIABLE xx;
+
+-- ON TRANSACTION END RESET tests
+CREATE VARIABLE t1 AS int DEFAULT -1 ON TRANSACTION END RESET;
+
+BEGIN;
+  SELECT t1;
+  LET t1 = 100;
+  SELECT t1;
+COMMIT;
+
+SELECT t1;
+
+BEGIN;
+  SELECT t1;
+  LET t1 = 100;
+  SELECT t1;
+ROLLBACK;
+
+SELECT t1;
+
+DROP VARIABLE t1;
+
+CREATE VARIABLE v1 AS int DEFAULT 0;
+CREATE VARIABLE v2 AS text DEFAULT 'none';
+
+LET v1 = 100;
+LET v2 = 'Hello';
+SELECT v1, v2;
+LET v1 = DEFAULT;
+LET v2 = DEFAULT;
+SELECT v1, v2;
+
+DROP VARIABLE v1;
+DROP VARIABLE v2;
+
+-- ON COMMIT DROP tests
+-- should be 0 always
+SELECT count(*) FROM pg_variable;
+
+CREATE TEMP VARIABLE g AS int ON COMMIT DROP;
+
+SELECT count(*) FROM pg_variable;
+
+BEGIN;
+  CREATE TEMP VARIABLE g AS int ON COMMIT DROP;
+COMMIT;
+
+SELECT count(*) FROM pg_variable;
+
+BEGIN;
+  CREATE TEMP VARIABLE g AS int ON COMMIT DROP;
+ROLLBACK;
+
+SELECT count(*) FROM pg_variable;
+
+-- 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;
+
+-- parallel workers should be used
+EXPLAIN (costs off) SELECT count(*) FROM svar_test WHERE a%10 = zero;
+
+-- result should be 100
+SELECT count(*) FROM svar_test WHERE a%10 = zero;
+
+LET zero = (SELECT count(*) FROM svar_test);
+
+-- result should be 1000
+SELECT zero;
+
+-- parallel workers should be used
+EXPLAIN (costs off) LET zero = (SELECT count(*) FROM svar_test);
+
+DROP TABLE svar_test;
+DROP VARIABLE zero;
+
+RESET parallel_setup_cost;
+RESET parallel_tuple_cost;
+RESET min_parallel_table_scan_size;
+RESET max_parallel_workers_per_gather;
+
+-- use variables in prepared statements
+CREATE VARIABLE v AS numeric;
+LET v = 3.14;
+
+-- use variables in views
+CREATE VIEW vv AS SELECT COALESCE(v, 0) + 1000 AS result;
+SELECT * FROM vv;
+
+-- start a new session
+\c
+
+SET search_path to svartest;
+
+SELECT * FROM vv;
+LET v = 3.14;
+SELECT * FROM vv;
+
+-- should fail, dependency
+DROP VARIABLE v;
+
+-- should be ok
+DROP VARIABLE v CASCADE;
+
+-- other features
+CREATE VARIABLE dt AS integer DEFAULT 0;
+
+LET dt = 100;
+SELECT dt;
+
+DISCARD VARIABLES;
+
+SELECT dt;
+
+DROP VARIABLE dt;
+
+-- NOT NULL
+CREATE VARIABLE v1 AS int NOT NULL;
+CREATE VARIABLE v2 AS int NOT NULL DEFAULT NULL;
+
+-- should fail
+SELECT v1;
+SELECT v2;
+LET v1 = NULL;
+LET v2 = NULL;
+LET v1 = DEFAULT;
+LET v2 = DEFAULT;
+
+-- should be ok
+LET v1 = 100;
+LET v2 = 1000;
+SELECT v1, v2;
+
+DROP VARIABLE v1;
+DROP VARIABLE v2;
+
+CREATE VARIABLE tv AS int;
+CREATE VARIABLE IF NOT EXISTS tv AS int;
+DROP VARIABLE tv;
+
+CREATE IMMUTABLE VARIABLE iv AS int DEFAULT 100;
+SELECT iv;
+
+-- should fail;
+LET iv = 10000;
+
+DROP VARIABLE iv;
+
+-- different order
+CREATE IMMUTABLE VARIABLE iv AS int DEFAULT 100;
+-- should to fail
+LET iv = 10000;
+-- should be ok
+SELECT iv;
+
+DROP VARIABLE iv;
+
+CREATE IMMUTABLE VARIABLE iv AS int;
+
+-- should be ok
+LET iv = NULL;
+
+-- should fail
+LET iv = NULL;
+
+DROP VARIABLE iv;
+
+-- create variable inside plpgsql block
+DO $$
+BEGIN
+  CREATE VARIABLE do_test_svar AS date DEFAULT '2000-01-01';
+END;
+$$;
+
+SELECT do_test_svar;
+
+DROP VARIABLE do_test_svar;
+
+-- should fail
+CREATE IMMUTABLE VARIABLE xx AS int NOT NULL;
+
+
+
+-- REASSIGN OWNED test
+CREATE ROLE var_test_role1;
+CREATE ROLE var_test_role2;
+
+CREATE VARIABLE xxx_var AS int;
+
+ALTER VARIABLE xxx_var OWNER TO var_test_role1;
+REASSIGN OWNED BY var_test_role1 to var_test_role2;
+
+SELECT varowner::regrole FROM pg_variable WHERE varname = 'xxx_var';
+
+DROP OWNED BY var_test_role1;
+DROP ROLE var_test_role1;
+SELECT count(*) FROM pg_variable WHERE varname = 'xxx_var';
+
+DROP OWNED BY var_test_role2;
+DROP ROLE var_test_role2;
+SELECT count(*) FROM pg_variable WHERE varname = 'xxx_var';
+
+-- creating, dropping temporary variable
+BEGIN;
+
+CREATE TEMP VARIABLE tempvar AS INT ON COMMIT DROP;
+
+LET tempvar = 100;
+
+SAVEPOINT s1;
+
+DROP VARIABLE tempvar;
+
+ROLLBACK TO s1;
+
+SELECT tempvar;
+
+COMMIT;
+
+-- should to fail
+LET tempvar = 100;
+
+BEGIN;
+
+SAVEPOINT s1;
+
+CREATE TEMP VARIABLE tempvar AS INT ON COMMIT DROP;
+
+LET tempvar = 100;
+
+ROLLBACK TO s1;
+
+COMMIT;
+
+-- should to fail
+LET tempvar = 100;
+
+CREATE VARIABLE var1 AS int;
+LET var1 = 100;
+BEGIN;
+DROP VARIABLE var1;
+ROLLBACK;
+SELECT var1;
+
+DROP VARIABLE var1;
+
+CREATE VARIABLE var1 AS int DEFAULT 100;
+COMMENT ON VARIABLE var1 IS 'some variable comment';
+
+SELECT pg_catalog.obj_description(oid, 'pg_variable') FROM pg_variable WHERE varname = 'var1';
+
+DROP VARIABLE var1;
+
+CREATE TABLE xxtab(avar int);
+
+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;
+
+SET session_variables_ambiguity_warning TO on;
+
+SELECT xxtab.avar FROM xxtab;
+
+SET search_path = svartest;
+
+CREATE VARIABLE testvar as int;
+
+-- plpgsql variables are preferred against session variables
+DO $$
+<<myblock>>
+DECLARE testvar int;
+BEGIN
+  -- should be ok without warning
+  LET testvar = 100;
+  -- should be ok without warning
+  testvar := 1000;
+  -- should be ok without warning
+  RAISE NOTICE 'session variable is %', svartest.testvar;
+  -- should be ok without warning
+  RAISE NOTICE 'plpgsql variable is %', myblock.testvar;
+  -- should to print plpgsql variable with warning
+  RAISE NOTICE 'variable is %', testvar;
+END;
+$$;
+
+DROP VARIABLE testvar;
+
+SET session_variables_ambiguity_warning TO default;
+
+-- should be ok
+SELECT avar FROM xxtab;
+
+CREATE VARIABLE public.avar AS int;
+
+-- should to fail
+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;
+
+-- test of plan cache invalidation
+CREATE VARIABLE xx AS int;
+
+SET plan_cache_mode = force_generic_plan;
+
+PREPARE pp AS SELECT xx;
+
+EXECUTE pp;
+
+DROP VARIABLE xx;
+
+CREATE VARIABLE xx AS int;
+
+-- should to work
+EXECUTE pp;
+
+DROP VARIABLE xx;
+
+DEALLOCATE pp;
+
+SET plan_cache_mode = DEFAULT;
+
+CREATE ROLE var_test_role;
+
+CREATE SCHEMA vartest;
+
+GRANT USAGE ON SCHEMA vartest TO var_test_role;
+
+CREATE VARIABLE vartest.x AS int;
+CREATE VARIABLE vartest.y AS int;
+
+LET vartest.x = 100;
+LET vartest.y = 101;
+
+GRANT SELECT ON ALL VARIABLES IN SCHEMA vartest TO var_test_role;
+
+SET ROLE TO var_test_role;
+
+SELECT vartest.x, vartest.y;
+
+SET ROLE TO DEFAULT;
+
+REVOKE SELECT ON ALL VARIABLES IN SCHEMA vartest FROM var_test_role;
+
+SET ROLE TO var_test_role;
+
+-- should to fail
+SELECT vartest.x;
+SELECT vartest.y;
+
+SET ROLE TO DEFAULT;
+
+DROP VARIABLE vartest.x, vartest.y;
+
+DROP SCHEMA vartest;
+
+DROP ROLE var_test_role;
+
+-- test cached plan
+CREATE VARIABLE v1 AS text;
+CREATE VARIABLE v2 AS int;
+CREATE VARIABLE v3 AS int;
+
+LET v1 = 'test';
+LET v2 = 10;
+LET v3 = 5;
+
+PREPARE q1 AS SELECT v1 || i FROM generate_series(1, v2) g(i) WHERE i IN (v2, v3);
+
+SET plan_cache_mode to force_generic_plan;
+
+EXECUTE q1;
+
+EXPLAIN EXECUTE q1;
+
+-- dependecy check
+DROP VARIABLE v3;
+
+-- recreate v3 again
+CREATE VARIABLE v3 AS int DEFAULT 6;
+
+-- should to work, the plan should be recreated
+EXECUTE q1;
+
+DEALLOCATE q1;
+
+-- fill v1 by long text
+LET v1 = repeat(' ', 10000);
+
+PREPARE q1 AS SELECT length(v1);
+
+EXECUTE q1;
+
+LET v1 = repeat(' ', 5000);
+
+EXECUTE q1;
+
+DEALLOCATE q1;
+
+SET plan_cache_mode to default;
+
+DROP VARIABLE v1, v2, v3;
+
+CREATE ROLE var_test_role;
+
+CREATE VARIABLE public.v1 AS int DEFAULT 0;
+
+-- check acl when variable is acessed by simple eval expr method
+CREATE OR REPLACE FUNCTION public.fx_var(int)
+RETURNS int AS $$
+DECLARE xx int;
+BEGIN
+  xx := public.v1 + $1;
+  RETURN xx;
+END;
+$$ LANGUAGE plpgsql;
+
+-- should be ok
+SELECT public.fx_var(0);
+
+SET ROLE TO var_test_role;
+
+-- should to fail
+SELECT public.fx_var(0);
+
+SET ROLE TO default;
+
+GRANT SELECT ON VARIABLE public.v1 TO var_test_role;
+
+SET ROLE TO var_test_role;
+
+-- should be ok
+SELECT public.fx_var(0);
+
+SET ROLE TO default;
+
+REVOKE SELECT ON VARIABLE public.v1 FROM var_test_role;
+
+SET ROLE TO var_test_role;
+
+-- should be fail
+SELECT public.fx_var(0);
+
+SET ROLE TO DEFAULT;
+
+DROP FUNCTION public.fx_var(int);
+
+DROP VARIABLE public.v1;
+
+DROP ROLE var_test_role;
+
+CREATE TYPE public.svar_test_type AS (a int, b int, c numeric);
+
+CREATE VARIABLE public.svar AS public.svar_test_type;
+
+LET public.svar = ROW(10,20,30);
+
+SELECT public.svar;
+
+ALTER TYPE public.svar_test_type DROP ATTRIBUTE c;
+
+-- should to fail
+SELECT public.svar;
+
+ALTER TYPE public.svar_test_type ADD ATTRIBUTE c int;
+
+-- should to fail too (different type, different generation number);
+SELECT public.svar;
+
+LET public.svar = ROW(10,20,30);
+
+-- should be ok again for new value
+SELECT public.svar;
+
+DROP VARIABLE public.svar;
+
+DROP TYPE public.svar_test_type;
+
+CREATE VARIABLE public.svar AS int;
+
+SELECT schema, name, removed FROM pg_debug_show_used_session_variables();
+
+LET public.svar = 100;
+
+SELECT schema, name, removed FROM pg_debug_show_used_session_variables();
+
+BEGIN;
+
+DROP VARIABLE public.svar;
+
+-- value should be in memory
+SELECT schema, name, removed FROM pg_debug_show_used_session_variables();
+
+ROLLBACK;
+
+-- value should be in memory
+SELECT schema, name, removed FROM pg_debug_show_used_session_variables() WHERE schema = 'public' and name = 'svar';
+
+SELECT public.svar;
+
+BEGIN;
+
+DROP VARIABLE public.svar;
+
+-- value should be in memory
+SELECT schema, name, removed FROM pg_debug_show_used_session_variables() WHERE schema = 'public' and name = 'svar';
+
+COMMIT;
+
+-- the memory should be clean;
+SELECT schema, name, removed FROM pg_debug_show_used_session_variables() WHERE schema = 'public' and name = 'svar';
+
+BEGIN;
+
+CREATE VARIABLE public.svar AS int;
+
+LET public.svar = 100;
+
+ROLLBACK;
+
+-- the memory should be clean;
+SELECT schema, name, removed FROM pg_debug_show_used_session_variables() WHERE schema = 'public' and name = 'svar';
+
+CREATE VARIABLE public.svar AS int;
+
+LET public.svar = 100;
+
+-- repeated aborted transaction
+BEGIN; DROP VARIABLE public.svar; ROLLBACK;
+BEGIN; DROP VARIABLE public.svar; ROLLBACK;
+BEGIN; DROP VARIABLE public.svar; ROLLBACK;
+
+-- the value should be still available
+SELECT public.svar;
+
+DROP VARIABLE public.svar;
+
+CREATE TYPE public.svar_test_type AS (a int, b int);
+
+CREATE VARIABLE public.svar AS public.svar_test_type;
+
+SELECT public.svar;
+
+ALTER TYPE public.svar_test_type ADD ATTRIBUTE c int;
+
+SELECT public.svar;
+
+ALTER TYPE public.svar_test_type DROP ATTRIBUTE b;
+
+SELECT public.svar;
+
+DROP VARIABLE public.svar;
+DROP TYPE public.svar_test_type;
+
+CREATE TYPE public.svar_test_type AS (a int, b int);
+
+CREATE VARIABLE public.svar AS public.svar_test_type;
+CREATE VARIABLE public.svar2 AS public.svar_test_type;
+
+LET public.svar = (10, 20);
+
+ALTER TYPE public.svar_test_type ADD ATTRIBUTE c int;
+
+SELECT public.svar;
+
+LET public.svar2 = (10, 20, 30);
+
+ALTER TYPE public.svar_test_type DROP ATTRIBUTE b;
+
+SELECT public.svar;
+SELECT public.svar2;
+
+DROP VARIABLE public.svar;
+DROP VARIABLE public.svar2;
+DROP TYPE public.svar_test_type;
+
+-- The composite type cannot be changed when it is used
+CREATE TYPE public.svar_type AS (a int, b int);
+
+CREATE VARIABLE public.svar AS public.svar_type;
+
+-- should to fail
+ALTER TYPE public.svar_type ALTER ATTRIBUTE b TYPE numeric;
+
+DROP VARIABLE public.svar;
+
+CREATE TYPE public.svar_type2 AS (a int, b int, c public.svar_type);
+
+CREATE VARIABLE public.svar AS public.svar_type2;
+
+-- should to fail
+ALTER TYPE public.svar_type ALTER ATTRIBUTE b TYPE numeric;
+
+DROP VARIABLE public.svar;
+DROP TYPE public.svar_type2;
+DROP TYPE public.svar_type;
-- 
2.37.2

From 337cb5e33dca94c689f486be4e916f0b1d2eef39 Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Tue, 5 Apr 2022 00:13:58 +0200
Subject: [PATCH 06/12] DISCARD VARIABLES command

---
 src/backend/commands/discard.c | 6 ++++++
 src/backend/parser/gram.y      | 7 ++++++-
 src/backend/tcop/utility.c     | 3 +++
 src/include/nodes/parsenodes.h | 3 ++-
 src/include/tcop/cmdtaglist.h  | 1 +
 5 files changed, 18 insertions(+), 2 deletions(-)

index c583539e0c..68fe550a71 100644

index 68b607339c..0d8f4f2809 100644
@@ -2139,7 +2139,12 @@ DiscardStmt:

diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 79c50a45a8..5702e4753b 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -2952,6 +2952,9 @@ CreateCommandTag(Node *parsetree)
 				case DISCARD_SEQUENCES:
 					tag = CMDTAG_DISCARD_SEQUENCES;
 					break;
+				case DISCARD_VARIABLES:
+					tag = CMDTAG_DISCARD_VARIABLES;
+					break;
 				default:
 					tag = CMDTAG_UNKNOWN;
 			}
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 3accd22ad2..8b28baf7fe 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -3855,7 +3855,8 @@ typedef enum DiscardMode
 	DISCARD_ALL,
 	DISCARD_PLANS,
 	DISCARD_SEQUENCES,
-	DISCARD_TEMP
+	DISCARD_TEMP,
+	DISCARD_VARIABLES
 } DiscardMode;
 
index 357989b52f..09bd4d467f 100644
@@ -135,6 +135,7 @@ PG_CMDTAG(CMDTAG_DISCARD_ALL, "DISCARD ALL", false, false, false)
2.37.2

From 81862d7ac072e03dabfce703128cc80b28a118e5 Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Mon, 4 Apr 2022 22:40:13 +0200
Subject: [PATCH 07/12] enhancing psql for session variables

\dV and tab complete for session variables related commands
---
 src/bin/psql/command.c      |  3 ++
 src/bin/psql/describe.c     | 96 +++++++++++++++++++++++++++++++++++++
 src/bin/psql/describe.h     |  3 ++
 src/bin/psql/help.c         |  1 +
 src/bin/psql/tab-complete.c | 70 ++++++++++++++++++++++-----
 5 files changed, 162 insertions(+), 11 deletions(-)

diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index a81bd3307b..e1fd815b9b 100644
--- a/src/bin/psql/command.c
+++ b/src/bin/psql/command.c
@@ -944,6 +944,9 @@ exec_command_d(PsqlScanState scan_state, bool active_branch, const char *cmd)
 						break;
 				}
 				break;
+			case 'V':			/* Variables */
+				success = listVariables(pattern, show_verbose);
+				break;
 			case 'x':			/* Extensions */
 				if (show_verbose)
 					success = listExtensionContents(pattern);
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 327a69487b..ccfee98307 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -5065,6 +5065,102 @@ error_return:
 	return false;
 }
 
+/*
+ * \dV
+ *
+ * listVariables()
+ */
+bool
+	static const bool translate_columns[] = {false, false, false, false, false, false, false, false, false, false, false};
+					  "  (SELECT c.collname FROM pg_catalog.pg_collation c, pg_catalog.pg_type bt\n"
+					  "   WHERE c.oid = v.varcollation AND bt.oid = v.vartype AND v.varcollation <> bt.typcollation) as \"%s\",\n"
+					  gettext_noop("Collation"),
+					  gettext_noop("Nullable"),
+					  gettext_noop("Mutable"),
+	if (!validateSQLNamePattern(&buf, pattern, true, false,
+								"pg_catalog.pg_variable_is_visible(v.oid)",
+								NULL, 3))
 
 /*
  * \dFp
diff --git a/src/bin/psql/describe.h b/src/bin/psql/describe.h
index 7872c71f58..6960c738bd 100644
--- a/src/bin/psql/describe.h
+++ b/src/bin/psql/describe.h
@@ -146,4 +146,7 @@ extern bool listOpFamilyFunctions(const char *access_method_pattern,
 /* \dl or \lo_list */
 extern bool listLargeObjects(bool verbose);
 
+/* \dV */
+extern bool listVariables(const char *pattern, bool varbose);
+
 #endif							/* DESCRIBE_H */
diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c
index f8ce1a0706..dcdfb20418 100644
--- a/src/bin/psql/help.c
+++ b/src/bin/psql/help.c
@@ -286,6 +286,7 @@ slashUsage(unsigned short int pager)
 	HELP0("  \\dT[S+] [PATTERN]      list data types\n");
 	HELP0("  \\du[S+] [PATTERN]      list roles\n");
 	HELP0("  \\dv[S+] [PATTERN]      list views\n");
+	HELP0("  \\dV     [PATTERN]      list variables\n");
 	HELP0("  \\dx[+]  [PATTERN]      list extensions\n");
 	HELP0("  \\dX     [PATTERN]      list extended statistics\n");
 	HELP0("  \\dy[+]  [PATTERN]      list event triggers\n");
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 62a39779b9..2780c2239c 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -962,6 +962,13 @@ static const SchemaQuery Query_for_trigger_of_table = {
 	.refnamespace = "c1.relnamespace",
 };
 
+	.result = "v.varname",
+};
 
 /*
  * Queries to get lists of names of various kinds of things, possibly
@@ -1219,6 +1226,8 @@ static const pgsql_thing_t words_after_create[] = {
 	{"FOREIGN TABLE", NULL, NULL, NULL},
 	{"FUNCTION", NULL, NULL, Query_for_list_of_functions},
 	{"GROUP", Query_for_list_of_roles},
+	{"IMMUTABLE VARIABLE", NULL, NULL, NULL, NULL, THING_NO_DROP | THING_NO_ALTER},	/* for CREATE
+																				 * IMMUTABLE VARIABLE ... */
 	{"INDEX", NULL, NULL, &Query_for_list_of_indexes},
 	{"LANGUAGE", Query_for_list_of_languages},
 	{"LARGE OBJECT", NULL, NULL, NULL, NULL, THING_NO_CREATE | THING_NO_DROP},
@@ -1257,6 +1266,7 @@ static const pgsql_thing_t words_after_create[] = {
 																			 * TABLE ... */
 	{"USER", Query_for_list_of_roles, NULL, NULL, Keywords_for_user_thing},
 	{"USER MAPPING FOR", NULL, NULL, NULL},
+	{"VARIABLE", NULL, NULL, &Query_for_list_of_variables},
 	{"VIEW", NULL, NULL, &Query_for_list_of_views},
 	{NULL}						/* end of list */
 };
@@ -1668,7 +1678,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",
+		"FETCH", "GRANT", "IMPORT FOREIGN SCHEMA", "INSERT INTO", "LET",
+		"LISTEN", "LOAD", "LOCK",
 		"MERGE", "MOVE", "NOTIFY", "PREPARE",
 		"REASSIGN", "REFRESH MATERIALIZED VIEW", "REINDEX", "RELEASE",
 		"RESET", "REVOKE", "ROLLBACK",
@@ -1688,7 +1699,7 @@ psql_completion(const char *text, int start, int end)
 		"\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL",
 		"\\dm", "\\dn", "\\do", "\\dO", "\\dp", "\\dP", "\\dPi", "\\dPt",
 		"\\drds", "\\dRs", "\\dRp", "\\ds",
-		"\\dt", "\\dT", "\\dv", "\\du", "\\dx", "\\dX", "\\dy",
+		"\\dt", "\\dT", "\\dv", "\\du", "\\dx", "\\dX", "\\dy", "\\dV",
 		"\\echo", "\\edit", "\\ef", "\\elif", "\\else", "\\encoding",
 		"\\endif", "\\errverbose", "\\ev",
 		"\\f",
@@ -2129,6 +2140,9 @@ psql_completion(const char *text, int start, int end)
 										  "ALL");
 	else if (Matches("ALTER", "SYSTEM", "SET", MatchAny))
 		COMPLETE_WITH("TO");
+	/* ALTER VARIABLE <name> */
+	else if (Matches("ALTER", "VARIABLE", MatchAny))
+		COMPLETE_WITH("OWNER TO", "RENAME TO", "SET SCHEMA");
 	/* ALTER VIEW <name> */
 	else if (Matches("ALTER", "VIEW", MatchAny))
 		COMPLETE_WITH("ALTER COLUMN", "OWNER TO", "RENAME",
@@ -2635,7 +2649,7 @@ psql_completion(const char *text, int start, int end)
 					  "ROUTINE", "RULE", "SCHEMA", "SEQUENCE", "SERVER",
 					  "STATISTICS", "SUBSCRIPTION", "TABLE",
 					  "TABLESPACE", "TEXT SEARCH", "TRANSFORM FOR",
-					  "TRIGGER", "TYPE", "VIEW");
+					  "TRIGGER", "TYPE", "VARIABLE", "VIEW");
 	else if (Matches("COMMENT", "ON", "ACCESS", "METHOD"))
 		COMPLETE_WITH_QUERY(Query_for_list_of_access_methods);
 	else if (Matches("COMMENT", "ON", "CONSTRAINT"))
@@ -3073,7 +3087,7 @@ psql_completion(const char *text, int start, int end)
 /* CREATE TABLE --- is allowed inside CREATE SCHEMA, so use TailMatches */
 	/* Complete "CREATE TEMP/TEMPORARY" with the possible temp objects */
 	else if (TailMatches("CREATE", "TEMP|TEMPORARY"))
-		COMPLETE_WITH("SEQUENCE", "TABLE", "VIEW");
+		COMPLETE_WITH("IMMUTABLE VARIABLE", "SEQUENCE", "TABLE", "VIEW", "VARIABLE");
 	/* Complete "CREATE UNLOGGED" with TABLE or MATVIEW */
 	else if (TailMatches("CREATE", "UNLOGGED"))
 		COMPLETE_WITH("TABLE", "MATERIALIZED VIEW");
@@ -3380,6 +3394,17 @@ psql_completion(const char *text, int start, int end)
 		else if (TailMatches("=", MatchAnyExcept("*)")))
 			COMPLETE_WITH(",", ")");
 	}
+/* CREATE VARIABLE --- is allowed inside CREATE SCHEMA, so use TailMatches */
+	/* Complete CREATE VARIABLE <name> with AS */
+	else if (TailMatches("IMMUTABLE"))
+		COMPLETE_WITH("VARIABLE");
+	else if (TailMatches("CREATE", "VARIABLE", MatchAny) ||
+			 TailMatches("TEMP|TEMPORARY", "VARIABLE", MatchAny) ||
+			 TailMatches("IMMUTABLE", "VARIABLE", MatchAny))
+			COMPLETE_WITH("AS");
+	else if (TailMatches("VARIABLE", MatchAny, "AS"))
+			/* Complete CREATE VARIABLE <name> with AS types */
+			COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes);
 
 /* CREATE VIEW --- is allowed inside CREATE SCHEMA, so use TailMatches */
 	/* Complete CREATE [ OR REPLACE ] VIEW <name> with AS */
@@ -3495,7 +3520,7 @@ psql_completion(const char *text, int start, int end)
 
 /* DISCARD */
 
 /* DO */
 	else if (Matches("DO"))
@@ -3622,6 +3647,12 @@ psql_completion(const char *text, int start, int end)
 	else if (Matches("DROP", "TRANSFORM", "FOR", MatchAny, "LANGUAGE", MatchAny))
 		COMPLETE_WITH("CASCADE", "RESTRICT");
 
+	/* DROP VARIABLE */
+	else if (Matches("DROP", "VARIABLE"))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables);
+	else if (Matches("DROP", "VARIABLE", MatchAny))
+		COMPLETE_WITH("CASCADE", "RESTRICT");
+
 /* EXECUTE */
 	else if (Matches("EXECUTE"))
 		COMPLETE_WITH_QUERY(Query_for_list_of_prepared_statements);
@@ -3738,7 +3769,8 @@ psql_completion(const char *text, int start, int end)
 		if (HeadMatches("ALTER", "DEFAULT", "PRIVILEGES"))
 			COMPLETE_WITH("SELECT", "INSERT", "UPDATE",
 						  "DELETE", "TRUNCATE", "REFERENCES", "TRIGGER",
-						  "EXECUTE", "USAGE", "ALL");
+						  "EXECUTE", "USAGE", "ALL",
+						  "READ", "WRITE");
 		else
 			COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_roles,
 									 "GRANT",
@@ -3756,6 +3788,8 @@ psql_completion(const char *text, int start, int end)
 									 "USAGE",
 									 "SET",
 									 "ALTER SYSTEM",
+									 "READ",
+									 "WRITE",
 									 "ALL");
 	}
 
@@ -3797,7 +3831,7 @@ psql_completion(const char *text, int start, int end)
 	else if (TailMatches("GRANT|REVOKE", MatchAny) ||
 			 TailMatches("REVOKE", "GRANT", "OPTION", "FOR", MatchAny))
 	{
-		if (TailMatches("SELECT|INSERT|UPDATE|DELETE|TRUNCATE|REFERENCES|TRIGGER|CREATE|CONNECT|TEMPORARY|TEMP|EXECUTE|USAGE|ALL"))
+		if (TailMatches("SELECT|INSERT|UPDATE|DELETE|TRUNCATE|REFERENCES|TRIGGER|CREATE|CONNECT|TEMPORARY|TEMP|EXECUTE|USAGE|READ|WRITE|ALL"))
 			COMPLETE_WITH("ON");
 		else if (TailMatches("GRANT", MatchAny))
 			COMPLETE_WITH("TO");
@@ -3820,7 +3854,7 @@ psql_completion(const char *text, int start, int end)
 		 * objects supported.
 		 */
 		if (HeadMatches("ALTER", "DEFAULT", "PRIVILEGES"))
-			COMPLETE_WITH("TABLES", "SEQUENCES", "FUNCTIONS", "PROCEDURES", "ROUTINES", "TYPES", "SCHEMAS");
+			COMPLETE_WITH("TABLES", "SEQUENCES", "FUNCTIONS", "PROCEDURES", "ROUTINES", "TYPES", "SCHEMAS", "VARIABLES");
 		else
 			COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_grantables,
 											"ALL FUNCTIONS IN SCHEMA",
@@ -3842,7 +3876,8 @@ psql_completion(const char *text, int start, int end)
 											"SEQUENCE",
 											"TABLE",
 											"TABLESPACE",
-											"TYPE");
+											"TYPE",
+											"VARIABLE");
 	}
 	else if (TailMatches("GRANT|REVOKE", MatchAny, "ON", "ALL") ||
 			 TailMatches("REVOKE", "GRANT", "OPTION", "FOR", MatchAny, "ON", "ALL"))
@@ -3850,7 +3885,8 @@ psql_completion(const char *text, int start, int end)
 					  "PROCEDURES IN SCHEMA",
 					  "ROUTINES IN SCHEMA",
 					  "SEQUENCES IN SCHEMA",
-					  "TABLES IN SCHEMA");
+					  "TABLES IN SCHEMA",
+					  "VARIABLES IN SCHEMA");
 	else if (TailMatches("GRANT|REVOKE", MatchAny, "ON", "FOREIGN") ||
 			 TailMatches("REVOKE", "GRANT", "OPTION", "FOR", MatchAny, "ON", "FOREIGN"))
 		COMPLETE_WITH("DATA WRAPPER", "SERVER");
@@ -3886,6 +3922,8 @@ psql_completion(const char *text, int start, int end)
 			COMPLETE_WITH_QUERY(Query_for_list_of_tablespaces);
 		else if (TailMatches("TYPE"))
 			COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes);
+		else if (TailMatches("VARIABLE"))
+			COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables);
 		else if (TailMatches("GRANT", MatchAny, MatchAny, MatchAny))
 			COMPLETE_WITH("TO");
 		else
@@ -4128,7 +4166,7 @@ psql_completion(const char *text, int start, int end)
 
 /* PREPARE xx AS */
 
 /*
  * PREPARE TRANSACTION is missing on purpose. It's intended for transaction
@@ -4416,6 +4454,14 @@ psql_completion(const char *text, int start, int end)
 	else if (TailMatches("UPDATE", MatchAny, "SET", MatchAnyExcept("*=")))
 		COMPLETE_WITH("=");
 
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables);
@@ -4582,6 +4628,8 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_views);
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables);
2.37.2

From df06a77d27ce81eaf28fee45eacc24e1ba644f21 Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Mon, 4 Apr 2022 20:32:45 +0200
Subject: [PATCH 05/12] support of LET command in PLpgSQL

The support for LET command as PLpgSQL command does assigning of expression
evaluation to session variable. With extra support in PLpgSQL we can implement
SQL LET command like usual PostgreSQL's command (and the assigning of result
to session variable is not side effect of query evaluation).
---
 src/backend/executor/spi.c              |  3 ++
 src/backend/parser/gram.y               |  8 ++++
 src/backend/parser/parser.c             |  3 +-
 src/include/parser/parser.h             |  6 ++-
 src/pl/plpgsql/src/pl_exec.c            | 55 +++++++++++++++++++++++++
 src/pl/plpgsql/src/pl_funcs.c           | 24 +++++++++++
 src/pl/plpgsql/src/pl_gram.y            | 28 ++++++++++++-
 src/pl/plpgsql/src/pl_reserved_kwlist.h |  1 +
 src/pl/plpgsql/src/plpgsql.h            | 14 ++++++-
 9 files changed, 138 insertions(+), 4 deletions(-)

diff --git a/src/backend/executor/spi.c b/src/backend/executor/spi.c
index 29bc26669b..56b107c60d 100644
--- a/src/backend/executor/spi.c
+++ b/src/backend/executor/spi.c
@@ -2963,6 +2963,9 @@ _SPI_error_callback(void *arg)
 			case RAW_PARSE_PLPGSQL_ASSIGN3:
 				errcontext("PL/pgSQL assignment \"%s\"", query);
 				break;
+			case RAW_PARSE_PLPGSQL_LET:
+				errcontext("LET statement \"%s\"", query);
+				break;
 			default:
 				errcontext("SQL statement \"%s\"", query);
 				break;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 8bbd640c3d..68b607339c 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -898,6 +898,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %token		MODE_PLPGSQL_ASSIGN1
 %token		MODE_PLPGSQL_ASSIGN2
 %token		MODE_PLPGSQL_ASSIGN3
+%token		MODE_PLPGSQL_LET

@@ -1018,6 +1019,13 @@ parse_toplevel:
 
 /*
index eee0a29c08..6726c4a30e 100644
 
index 828150f01b..bf5d5acf48 100644
 
index 7bd2a9fff1..d013273e5b 100644
 
 static void plpgsql_estate_setup(PLpgSQL_execstate *estate,
 								 PLpgSQL_function *func,
@@ -2109,6 +2112,10 @@ exec_stmts(PLpgSQL_execstate *estate, List *stmts)
 				rc = exec_stmt_rollback(estate, (PLpgSQL_stmt_rollback *) stmt);
 				break;
 
+			case PLPGSQL_STMT_LET:
+				rc = exec_stmt_let(estate, (PLpgSQL_stmt_let *) stmt);
+				break;
+
 			default:
 				/* point err_stmt to parent, since this one seems corrupt */
 				estate->err_stmt = save_estmt;
@@ -4977,6 +4984,54 @@ exec_stmt_rollback(PLpgSQL_execstate *estate, PLpgSQL_stmt_rollback *stmt)
 	return PLPGSQL_RC_OK;
 }
 
+/* ----------
index 93d9cef06b..8bb7d18e2b 100644
 
 	return "unknown";

+static void

+static void
index 0b8aea9504..0864f6de55 100644
@@ -198,7 +198,7 @@ static	void			check_raise_parameters(PLpgSQL_stmt_raise *stmt);
 
 %type <list>	proc_exceptions
@@ -305,6 +305,7 @@ static	void			check_raise_parameters(PLpgSQL_stmt_raise *stmt);
 %token <keyword>	K_INTO
 %token <keyword>	K_IS
 %token <keyword>	K_LAST
+%token <keyword>	K_LET
 %token <keyword>	K_LOG
 %token <keyword>	K_LOOP
 %token <keyword>	K_MERGE
@@ -899,6 +900,8 @@ proc_stmt		: pl_block ';'
 						{ $$ = $1; }
 				| stmt_rollback
 						{ $$ = $1; }
+				| stmt_let
+						{ $$ = $1; }
 				;
 
 stmt_perform	: K_PERFORM
@@ -1015,6 +1018,29 @@ stmt_assign		: T_DATUM
 					}
 				;
 
+stmt_let		: K_LET
+					{
+						PLpgSQL_stmt_let *new;
+						RawParseMode pmode;
+
index 9043fbddbc..e68b5d5b26 100644
index 088768867d..3aeb7badf8 100644
 
 /*
 
+/*
+ * Let statement
+ */
2.37.2

From 458e1a91f044290b1d8af4766fef25e45ab2b5c0 Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Tue, 5 Apr 2022 00:54:29 +0200
Subject: [PATCH 04/12] LET command

Set session variable value to result of query or result of default expression
---
 src/backend/commands/explain.c           |  16 ++
 src/backend/commands/sessionvariable.c   |  85 ++++++-
 src/backend/executor/Makefile            |   1 +
 src/backend/executor/execMain.c          |   1 +
 src/backend/executor/svariableReceiver.c | 154 ++++++++++++
 src/backend/nodes/nodeFuncs.c            |  10 +
 src/backend/parser/analyze.c             | 283 +++++++++++++++++++++++
 src/backend/parser/gram.y                |  53 ++++-
 src/backend/parser/parse_agg.c           |   4 +
 src/backend/parser/parse_expr.c          |   4 +
 src/backend/parser/parse_func.c          |   3 +
 src/backend/parser/parse_target.c        |   4 +-
 src/backend/rewrite/rewriteHandler.c     |  34 +++
 src/backend/rewrite/rowsecurity.c        |   8 +-
 src/backend/tcop/dest.c                  |   7 +
 src/backend/tcop/utility.c               |  15 ++
 src/backend/utils/cache/plancache.c      |  15 +-
 src/include/commands/session_variable.h  |   3 +
 src/include/executor/svariableReceiver.h |  25 ++
 src/include/nodes/parsenodes.h           |  15 ++
 src/include/nodes/plannodes.h            |   2 +-
 src/include/parser/kwlist.h              |   1 +
 src/include/parser/parse_node.h          |   3 +-
 src/include/tcop/cmdtaglist.h            |   1 +
 src/include/tcop/dest.h                  |   3 +-
 25 files changed, 736 insertions(+), 14 deletions(-)
 create mode 100644 src/backend/executor/svariableReceiver.c
 create mode 100644 src/include/executor/svariableReceiver.h

diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index e078456b19..3a5f51cbc3 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -492,6 +492,22 @@ ExplainOneUtility(Node *utilityStmt, IntoClause *into, ExplainState *es,
 		else
 			ExplainDummyGroup("Notify", NULL, es);
 	}
+	else if (IsA(utilityStmt, LetStmt))
+	{
+		LetStmt	   *letstmt = (LetStmt *) utilityStmt;
+		List	   *rewritten;
+
+		if (es->format == EXPLAIN_FORMAT_TEXT)
+			appendStringInfoString(es->str, "SET SESSION VARIABLE\n");
+		else
+			ExplainDummyGroup("Set Session Variable", NULL, es);
+
+		rewritten = QueryRewrite(castNode(Query, copyObject(letstmt->query)));
+		Assert(list_length(rewritten) == 1);
+		ExplainOneQuery(linitial_node(Query, rewritten),
+						CURSOR_OPT_PARALLEL_OK, NULL, es,
+						queryString, params, queryEnv);
+	}
 	else
 	{
 		if (es->format == EXPLAIN_FORMAT_TEXT)
diff --git a/src/backend/commands/sessionvariable.c b/src/backend/commands/sessionvariable.c
index 0ff8ead99d..4b6b4b0dfb 100644
--- a/src/backend/commands/sessionvariable.c
+++ b/src/backend/commands/sessionvariable.c
@@ -1731,7 +1731,7 @@ SetSessionVariableWithSecurityCheck(Oid varid, Datum value, bool isNull, Oid typ
 	/*
 	 * Is possible to write to session variable?
 	 */
-	aclresult = pg_variable_aclcheck(varid, GetUserId(), ACL_WRITE);
+	aclresult = pg_variable_aclcheck(varid, GetUserId(), ACL_UPDATE);
 	if (aclresult != ACLCHECK_OK)
 		aclcheck_error(aclresult, OBJECT_VARIABLE, get_session_variable_name(varid));
 
@@ -2041,6 +2041,85 @@ ResetSessionVariables(void)
 	xact_recheck_requests = NIL;
 }
 
+/*
+	aclresult = pg_variable_aclcheck(varid, GetUserId(), ACL_UPDATE);
+		aclcheck_error(aclresult, OBJECT_VARIABLE, get_session_variable_name(varid));
  * Registration of actions to be executed on session variables at transaction
  * end time. We want to drop temporary session variables with clause ON COMMIT
@@ -2350,8 +2429,8 @@ pg_debug_show_used_session_variables(PG_FUNCTION_ARGS)
 
 				values[5] = BoolGetDatum(false);
 				values[6] = BoolGetDatum(svar->is_valid);
-				values[8] = BoolGetDatum(pg_variable_aclcheck(svar->varid, GetUserId(), ACL_READ) == ACLCHECK_OK);
-				values[9] = BoolGetDatum(pg_variable_aclcheck(svar->varid, GetUserId(), ACL_WRITE) == ACLCHECK_OK);
+				values[8] = BoolGetDatum(pg_variable_aclcheck(svar->varid, GetUserId(), ACL_SELECT) == ACLCHECK_OK);
+				values[9] = BoolGetDatum(pg_variable_aclcheck(svar->varid, GetUserId(), ACL_UPDATE) == ACLCHECK_OK);
 			}
 			else
 			{
diff --git a/src/backend/executor/Makefile b/src/backend/executor/Makefile
index 11118d0ce0..71248a34f2 100644
--- a/src/backend/executor/Makefile
+++ b/src/backend/executor/Makefile
@@ -76,6 +76,7 @@ OBJS = \
 	nodeWindowAgg.o \
 	nodeWorktablescan.o \
 	spi.o \
+	svariableReceiver.o \
 	tqueue.o \
 	tstoreReceiver.o
 
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 68e748eb73..00836a809a 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -51,6 +51,7 @@
 #include "commands/session_variable.h"
 #include "executor/execdebug.h"
 #include "executor/nodeSubplan.h"
+#include "executor/svariableReceiver.h"
 #include "foreign/fdwapi.h"
 #include "jit/jit.h"
 #include "mb/pg_wchar.h"
diff --git a/src/backend/executor/svariableReceiver.c b/src/backend/executor/svariableReceiver.c
new file mode 100644
index 0000000000..97710b0d23
--- /dev/null
+++ b/src/backend/executor/svariableReceiver.c
@@ -0,0 +1,154 @@
+/*-------------------------------------------------------------------------
+ *
+ * svariableReceiver.c
+ *	  An implementation of DestReceiver that stores the result value in
+ *	  a session variable.
+ *
+ * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *	  src/backend/executor/svariableReceiver.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/detoast.h"
+#include "executor/svariableReceiver.h"
+#include "commands/session_variable.h"
+
+typedef struct
+{
+	DestReceiver pub;
+	Oid			varid;
+	Oid			typid;
+	int32		typmod;
+	int			typlen;
+	int			slot_offset;
+	int			rows;
+}			svariableState;
+
+
+/*
+ * Prepare to receive tuples from executor.
+ */
+static void
+svariableStartupReceiver(DestReceiver *self, int operation, TupleDesc typeinfo)
+{
+	svariableState *myState = (svariableState *) self;
+	int			natts = typeinfo->natts;
+	int			outcols = 0;
+	int			i;
+
+	/* Receiver should be initialized by SetVariableDestReceiverParams */
+	Assert(OidIsValid(myState->varid));
+
+	for (i = 0; i < natts; i++)
+	{
+		Form_pg_attribute attr = TupleDescAttr(typeinfo, i);
+
+		if (attr->attisdropped)
+			continue;
+
+		if (++outcols > 1)
+			elog(ERROR, "svariable DestReceiver can take only one attribute");
+
+		myState->typid = attr->atttypid;
+		myState->typmod = attr->atttypmod;
+		myState->typlen = attr->attlen;
+		myState->slot_offset = i;
+	}
+
+	if (outcols == 0)
+		elog(ERROR, "svariable DestReceiver requires one attribute");
+
+	myState->rows = 0;
+}
+
+/*
+ * Receive a tuple from the executor and store it in session variable.
+ */
+static bool
+svariableReceiveSlot(TupleTableSlot *slot, DestReceiver *self)
+{
+	svariableState *myState = (svariableState *) self;
+	Datum		value;
+	bool		isnull;
+	bool		freeval = false;
+
+	/* Make sure the tuple is fully deconstructed */
+	slot_getallattrs(slot);
+
+	value = slot->tts_values[myState->slot_offset];
+	isnull = slot->tts_isnull[myState->slot_offset];
+
+	if (myState->typlen == -1 && !isnull && VARATT_IS_EXTERNAL(DatumGetPointer(value)))
+	{
+		value = PointerGetDatum(detoast_external_attr((struct varlena *)
+													  DatumGetPointer(value)));
+		freeval = true;
+	}
+
+	SetSessionVariable(myState->varid, value, isnull, myState->typid);
+
+	if (freeval)
+		pfree(DatumGetPointer(value));
+
+	return true;
+}
+
+/*
+ * Clean up at end of an executor run
+ */
+static void
+svariableShutdownReceiver(DestReceiver *self)
+{
+	/* Nothing to do. */
+}
+
+/*
+ * Destroy receiver when done with it
+ */
+static void
+svariableDestroyReceiver(DestReceiver *self)
+{
+	pfree(self);
+}
+
+/*
+ * Initially create a DestReceiver object.
+ */
+DestReceiver *
+CreateVariableDestReceiver(void)
+{
+	svariableState *self = (svariableState *) palloc0(sizeof(svariableState));
+
+	self->pub.receiveSlot = svariableReceiveSlot;
+	self->pub.rStartup = svariableStartupReceiver;
+	self->pub.rShutdown = svariableShutdownReceiver;
+	self->pub.rDestroy = svariableDestroyReceiver;
+	self->pub.mydest = DestVariable;
+
+	/*
+	 * Private fields will be set by SetVariableDestReceiverParams and
+	 * svariableStartupReceiver.
+	 */
+	return (DestReceiver *) self;
+}
+
+/*
+ * Set parameters for a VariableDestReceiver.
+ * Should be called right after creating the DestReceiver.
+ */
+void
+SetVariableDestReceiverParams(DestReceiver *self, Oid varid)
+{
+	svariableState *myState = (svariableState *) self;
+
+	Assert(myState->pub.mydest == DestVariable);
+	Assert(OidIsValid(varid));
+
+	myState->varid = varid;
+}
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index c334daae39..1e9221ed66 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -4103,6 +4103,16 @@ raw_expression_tree_walker(Node *node,
 					return true;
 			}
 			break;
+		case T_LetStmt:
+			{
+				LetStmt *stmt = (LetStmt *) node;
+
+				if (walker(stmt->target, context))
+					return true;
+				if (walker(stmt->query, context))
+					return true;
+			}
+			break;
 		case T_PLAssignStmt:
 			{
 				PLAssignStmt *stmt = (PLAssignStmt *) node;
diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
index a013031d22..c7177c6dca 100644
--- a/src/backend/parser/analyze.c
+++ b/src/backend/parser/analyze.c
@@ -25,8 +25,11 @@
 #include "postgres.h"
 
@@ -51,6 +54,7 @@
 
@@ -84,6 +88,8 @@ static Query *transformCreateTableAsStmt(ParseState *pstate,
 										 CreateTableAsStmt *stmt);
 static Query *transformCallStmt(ParseState *pstate,
 								CallStmt *stmt);
+static Query *transformLetStmt(ParseState *pstate,
+							   LetStmt * stmt);
 static void transformLockingClause(ParseState *pstate, Query *qry,
 								   LockingClause *lc, bool pushedDown);
 #ifdef RAW_EXPRESSION_COVERAGE_TEST
@@ -327,6 +333,7 @@ transformStmt(ParseState *pstate, Node *parseTree)
 		case T_UpdateStmt:
 		case T_DeleteStmt:
 		case T_MergeStmt:
+		case T_LetStmt:
 			(void) test_raw_expression_coverage(parseTree, NULL);
 			break;
 		default:
@@ -400,6 +407,11 @@ transformStmt(ParseState *pstate, Node *parseTree)
 									   (CallStmt *) parseTree);
 			break;
 
+		case T_LetStmt:
 
 			/*
@@ -442,6 +454,7 @@ analyze_requires_snapshot(RawStmt *parseTree)
 		case T_MergeStmt:
 		case T_SelectStmt:
 		case T_PLAssignStmt:
+		case T_LetStmt:
 			result = true;
 			break;
 
@@ -1628,12 +1641,282 @@ transformValuesClause(ParseState *pstate, SelectStmt *stmt)
 	qry->jointree = makeFromExpr(pstate->p_joinlist, NULL);

 
 	return qry;
 }
 
+/*
+ * transformLetStmt -
+ *	  transform an Let Statement
+ */
+static Query *
+transformLetStmt(ParseState *pstate, LetStmt * stmt)
+{
+	Query	   *query;
+	Query	   *result;
+	List	   *exprList = NIL;
+	List	   *exprListCoer = NIL;
+	List	   *indirection = NIL;
+	ListCell   *lc;
+	Query	   *selectQuery;
+	int			i = 0;
+	Oid			varid;
+	char	   *attrname = NULL;
+	bool		not_unique;
+	bool		is_rowtype;
+	Oid			typid;
+	int32		typmod;
+	Oid			collid;
+	AclResult	aclresult;
+	List	   *names = NULL;
+	int			indirection_start;
+
+	/* There can't be any outer WITH to worry about */
+	Assert(pstate->p_ctenamespace == NIL);
+
+	names = NamesFromList(stmt->target);
+
+	varid = IdentifyVariable(names, &attrname, true, &not_unique);
+	if (not_unique)
+		ereport(ERROR,
+				(errcode(ERRCODE_AMBIGUOUS_PARAMETER),
+				 errmsg("target \"%s\" of LET command is ambiguous",
+						NameListToString(names)),
+				 parser_errposition(pstate, stmt->location)));
+
+	if (!OidIsValid(varid))
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("session variable \"%s\" doesn't exist",
+						NameListToString(names)),
+				 parser_errposition(pstate, stmt->location)));
+
+	indirection_start = list_length(names) - (attrname ? 1 : 0);
+	indirection = list_copy_tail(stmt->target, indirection_start);
+
+	get_session_variable_type_typmod_collid(varid, &typid, &typmod, &collid);
+
+	is_rowtype = type_is_rowtype(typid);
+
+	if (attrname && !is_rowtype)
+		ereport(ERROR,
+				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+				 errmsg("type \"%s\" of target session variable \"%s.%s\" is not a composite type",
+						format_type_be(typid),
+						get_namespace_name(get_session_variable_namespace(varid)),
+						get_session_variable_name(varid)),
+				 parser_errposition(pstate, stmt->location)));
+
+	if (stmt->set_default && attrname != NULL)
+		ereport(ERROR,
+				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+				 errmsg("only session variable (without attribute specification) can be set to default"),
+				 parser_errposition(pstate, stmt->location)));
+
+	aclresult = pg_variable_aclcheck(varid, GetUserId(), ACL_UPDATE);
+	if (aclresult != ACLCHECK_OK)
+		aclcheck_error(aclresult, OBJECT_VARIABLE, NameListToString(names));
+
+	pstate->p_expr_kind = EXPR_KIND_LET_TARGET;
+
+	/*
+	 * The LET statements suppports two syntaxes: LET var = expr and
+	 * LET var = DEFAULT. In first case, the expression is of SelectStmt
+	 * node and it is transformated to query (SELECT) by usual way.
+	 * Second syntax should be transformed differently. It is more cleaner
+	 * do this transformation here (like special case), because we don't
+	 * need to enhance SelectStmt about fields necessary for this
+	 * transformation somwehere in SelectStmt transformation. Isn't to
+	 * necessary to uglify the SelectStmt transformation. This is special
+	 * case of LET statement, not SelectStmt.
+	 */
+	if (stmt->set_default)
+	{
+		selectQuery = makeNode(Query);
+		selectQuery->commandType = CMD_SELECT;
+
+		/*
+		 * ResTarget(SetToDefault) -> TargetEntry(expr(SetToDefault))
+		 * The SetToDefault is replaced by defexpr by rewriter in
+		 * RewriteQuery function.
+		 */
+		selectQuery->targetList = transformTargetList(pstate,
+													  ((SelectStmt *) stmt->query)->targetList,
+													  EXPR_KIND_LET_TARGET);
+	}
+	else
+		selectQuery = transformStmt(pstate, stmt->query);
+
+	/* The grammar should have produced a SELECT */
+	if (!IsA(selectQuery, Query) ||
+		selectQuery->commandType != CMD_SELECT)
+		elog(ERROR, "unexpected non-SELECT command in LET command");
+
+	/*----------
+	 * Generate an expression list for the LET that selects all the
+	 * non-resjunk columns from the subquery.
+	 *----------
+	 */
+	exprList = NIL;
+	foreach(lc, selectQuery->targetList)
+	{
+		TargetEntry *tle = (TargetEntry *) lfirst(lc);
+
+		if (tle->resjunk)
+			continue;
+
+		exprList = lappend(exprList, tle->expr);
+	}
+
+	/* don't allow multicolumn result */
+	if (list_length(exprList) != 1)
+		ereport(ERROR,
+				(errcode(ERRCODE_SYNTAX_ERROR),
+				 errmsg("expression is not scalar value"),
+				 parser_errposition(pstate,
+									exprLocation((Node *) exprList))));
+
+	exprListCoer = NIL;
+
+	foreach(lc, exprList)
+	{
+		Expr	   *expr = (Expr *) lfirst(lc);
+		Expr	   *coerced_expr;
+		Param	   *param;
+		Oid			exprtypid;
+
+		if (IsA(expr, SetToDefault))
+		{
+			SetToDefault *def = (SetToDefault *) expr;
+
+			def->typeId = typid;
+			def->typeMod = typmod;
+			def->collation = collid;
+		}
+		else if (IsA(expr, Const) && ((Const *) expr)->constisnull)
+		{
+			/* use known type for NULL value */
+			expr = (Expr *) makeNullConst(typid, typmod, collid);
+		}
+
+		/* now we can read type of expression */
+		exprtypid = exprType((Node *) expr);
+
+		param = makeNode(Param);
+		param->paramkind = PARAM_VARIABLE;
+		param->paramvarid = varid;
+		param->paramtype = typid;
+		param->paramtypmod = typmod;
+
+		if (indirection != NULL)
+		{
+			bool		targetIsArray;
+			char	   *targetName;
+
+			targetName = get_session_variable_name(varid);
+			targetIsArray = OidIsValid(get_element_type(typid));
+
+			pstate->p_hasSessionVariables = true;
+
+			coerced_expr = (Expr *)
+				transformAssignmentIndirection(pstate,
+											   (Node *) param,
+											   targetName,
+											   targetIsArray,
+											   typid,
+											   typmod,
+											   InvalidOid,
+											   indirection,
+											   list_head(indirection),
+											   (Node *) expr,
+											   COERCION_PLPGSQL,
+											   stmt->location);
+		}
+		else
+			coerced_expr = (Expr *)
+				coerce_to_target_type(pstate,
+									  (Node *) expr,
+									  exprtypid,
+									  typid, typmod,
+									  COERCION_ASSIGNMENT,
+									  COERCE_IMPLICIT_CAST,
+									  stmt->location);
+
+		if (coerced_expr == NULL)
+			ereport(ERROR,
+					(errcode(ERRCODE_DATATYPE_MISMATCH),
+					 errmsg("variable \"%s.%s\" is of type %s,"
+							" but expression is of type %s",
+							get_namespace_name(get_session_variable_namespace(varid)),
+							get_session_variable_name(varid),
+							format_type_be(typid),
+							format_type_be(exprtypid)),
+					 errhint("You will need to rewrite or cast the expression."),
+					 parser_errposition(pstate, exprLocation((Node *) expr))));
+
+		exprListCoer = lappend(exprListCoer, coerced_expr);
+	}
+
+	/*
+	 * Generate query's target list using the computed list of expressions.
+	 */
+	query = makeNode(Query);
+	query->commandType = CMD_SELECT;
+
+	foreach(lc, exprListCoer)
+	{
+		Expr	   *expr = (Expr *) lfirst(lc);
+		TargetEntry *tle;
+
+		tle = makeTargetEntry(expr,
+							  i + 1,
+							  FigureColname((Node *) expr),
+							  false);
+		query->targetList = lappend(query->targetList, tle);
+	}
+
+	/* done building the range table and jointree */
+	query->rtable = pstate->p_rtable;
+	query->jointree = makeFromExpr(pstate->p_joinlist, NULL);
+
+	query->hasTargetSRFs = pstate->p_hasTargetSRFs;
+	query->hasSubLinks = pstate->p_hasSubLinks;
+	query->hasSessionVariables = pstate->p_hasSessionVariables;
+
+	/* This is top query */
+	query->canSetTag = true;
+
+	/*
+	 * Save target session variable id. This value is used
+	 * multiple times: by query rewriter (for getting related
+	 * defexpr), by planner (for acquiring AccessShareLock on
+	 * target variable), and by executor (we need to know
+	 * target variable id).
+	 */
+	query->resultVariable = varid;
+
+	assign_query_collations(pstate, query);
+
+	stmt->query = (Node *) query;
+
+	/*
+	 * When statement is executed as a PlpgSQL LET statement, then we should
+	 * return the query because we don't want to use a utilityStmt wrapper node.
+	 */
+	if (stmt->plpgsql_mode)
+		return query;
+
+	/* represent the command as a utility Query */
+	result = makeNode(Query);
+	result->commandType = CMD_UTILITY;
+	result->utilityStmt = (Node *) stmt;
+
+	return result;
+}
+
 /*
  * transformSetOperationStmt -
  *	  transforms a set-operations tree
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index facbd5f475..8bbd640c3d 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -316,7 +316,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
@@ -461,6 +461,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
@@ -820,7 +821,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	KEY KEYS KEEP

 	MAPPING MATCH MATCHED MATERIALIZED MAXVALUE MERGE METHOD
@@ -1158,6 +1159,7 @@ stmt:
 			| ImportForeignSchemaStmt
 			| IndexStmt
 			| InsertStmt
+			| LetStmt
 			| ListenStmt
 			| RefreshMatViewStmt
 			| LoadStmt
@@ -12003,6 +12005,7 @@ ExplainableStmt:
 			| CreateMatViewStmt
 			| RefreshMatViewStmt
 			| ExecuteStmt					/* by default all are $$=$1 */
+			| LetStmt
 		;
 
 /*****************************************************************************
@@ -12033,6 +12036,7 @@ PreparableStmt:
 			| UpdateStmt
 			| DeleteStmt
 			| MergeStmt						/* by default all are $$=$1 */
+			| LetStmt
 		;
 
 /*****************************************************************************
@@ -12620,6 +12624,49 @@ opt_hold: /* EMPTY */						{ $$ = 0; }
 			| WITHOUT HOLD					{ $$ = 0; }
 		;
 
@@ -17903,6 +17950,7 @@ unreserved_keyword:
@@ -18509,6 +18557,7 @@ bare_label_keyword:
index dbe630a51b..4365692735 100644
 
 			/*
@@ -955,6 +956,9 @@ transformWindowFuncCall(ParseState *pstate, WindowFunc *wfunc,
 
 			/*
 			 * 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 7eafd5f171..e6c89f9c92 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -577,6 +577,7 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
 		case EXPR_KIND_GENERATED_COLUMN:
 		case EXPR_KIND_CYCLE_MARK:
 		case EXPR_KIND_VARIABLE_DEFAULT:
+		case EXPR_KIND_LET_TARGET:
 
 			/* okay */
 			break;
@@ -1983,6 +1984,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:
@@ -3358,6 +3360,8 @@ ParseExprKindName(ParseExprKind exprKind)
 			return "GENERATED AS";
 		case EXPR_KIND_CYCLE_MARK:
 			return "CYCLE";
+		case EXPR_KIND_LET_TARGET:
+			return "LET";
 
 			/*
 			 * There is intentionally no default: case here, so that the
diff --git a/src/backend/parser/parse_func.c b/src/backend/parser/parse_func.c
index 005bc0400f..21fbf939f2 100644
--- a/src/backend/parser/parse_func.c
+++ b/src/backend/parser/parse_func.c
@@ -2660,6 +2660,9 @@ check_srf_call_placement(ParseState *pstate, Node *last_srf, int location)
 		case EXPR_KIND_CYCLE_MARK:
 			errkind = true;
 			break;
+		case EXPR_KIND_LET_TARGET:
+			err = _("set-returning functions are not allowed in CALL arguments");
+			break;
 
 			/*
index 16a0fe59e2..41cb04f4b6 100644
index 29ae27e5e3..08ed6ed7f9 100644
@@ -3674,6 +3675,39 @@ RewriteQuery(Query *parsetree, List *rewrite_events)
 		}
 	}
 
+	/*
+	 * Rewrite SetToDefault by default expression of Let statement.
+	 */
+	if (event == CMD_SELECT && OidIsValid(parsetree->resultVariable))
+	{
+		Oid			resultVariable = parsetree->resultVariable;
+
+		if (list_length(parsetree->targetList) == 1 &&
+			IsA(((TargetEntry *) linitial(parsetree->targetList))->expr, SetToDefault))
+		{
+			Variable		var;
+			TargetEntry	   *tle;
+			TargetEntry	   *newtle;
+			Expr		   *defexpr;
+
+			/* Read session variable metadata with defexpr */
+			initVariable(&var, resultVariable, false);
+
+			if (var.has_defexpr)
+				defexpr = (Expr *) var.defexpr;
+			else
+				defexpr = (Expr *) makeNullConst(var.typid, var.typmod, var.collation);
+
+			tle = (TargetEntry *) linitial(parsetree->targetList);
+			newtle = makeTargetEntry(defexpr,
+									  tle->resno,
+									  pstrdup(tle->resname),
+									  false);
+
+			parsetree->targetList = list_make1(newtle);
+		}
+	}
+
 	/*
 	 * If the statement is an insert, update, delete, or merge, adjust its
 	 * targetlist as needed, and then fire INSERT/UPDATE/DELETE rules on it.
diff --git a/src/backend/rewrite/rowsecurity.c b/src/backend/rewrite/rowsecurity.c
index a233dd4758..a8351c4499 100644
--- a/src/backend/rewrite/rowsecurity.c
+++ b/src/backend/rewrite/rowsecurity.c
@@ -213,10 +213,10 @@ get_row_security_policies(Query *root, RangeTblEntry *rte, int rt_index,
 	}
 
 	/*
 
index c952cbea8b..9c5339804b 100644
 
 		case DestTupleQueue:
 
 	/* should never get here */
@@ -207,6 +211,7 @@ EndCommand(const QueryCompletion *qc, CommandDest dest, bool force_undecorated_o
 		case DestSQLFunction:
 		case DestTransientRel:
 		case DestTupleQueue:
+		case DestVariable:
 			break;
 	}
 }
@@ -252,6 +257,7 @@ NullCommand(CommandDest dest)
 		case DestSQLFunction:
 		case DestTransientRel:
 		case DestTupleQueue:
+		case DestVariable:
 			break;
 	}
 }
@@ -295,6 +301,7 @@ ReadyForQuery(CommandDest dest)
 		case DestSQLFunction:
 		case DestTransientRel:
 		case DestTupleQueue:
+		case DestVariable:
 			break;
 	}
 }
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index ffe1caafc8..79c50a45a8 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -242,6 +242,7 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree)
 
 		case T_CallStmt:
 		case T_DoStmt:
+		case T_LetStmt:
 			{
 				/*
 				 * Commands inside the DO block or the called procedure might
@@ -1071,6 +1072,11 @@ standard_ProcessUtility(PlannedStmt *pstmt,
 				break;
 			}
 
+		case T_LetStmt:
@@ -2197,6 +2203,10 @@ UtilityContainsQuery(Node *parsetree)
 
+		case T_LetStmt:
+			qry = castNode(Query, ((LetStmt *) parsetree)->query);
+			return qry;
+
 		default:
 			return NULL;
 	}
@@ -2395,6 +2405,10 @@ CreateCommandTag(Node *parsetree)
 			tag = CMDTAG_SELECT;
 			break;
 
+		case T_LetStmt:
+			tag = CMDTAG_LET;
+			break;
+
 			/* utility statements --- same whether raw or cooked */
 		case T_TransactionStmt:
 			{
@@ -3280,6 +3294,7 @@ GetCommandLogLevel(Node *parsetree)
 			break;
 
 		case T_PLAssignStmt:
+		case T_LetStmt:
 			lev = LOGSTMT_ALL;
 			break;
 
diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c
index 74daf13a3f..7a3f678f06 100644
--- a/src/backend/utils/cache/plancache.c
+++ b/src/backend/utils/cache/plancache.c
@@ -135,6 +135,7 @@ InitPlanCache(void)
 	CacheRegisterSyscacheCallback(AMOPOPID, PlanCacheSysCallback, (Datum) 0);
 	CacheRegisterSyscacheCallback(FOREIGNSERVEROID, PlanCacheSysCallback, (Datum) 0);
 	CacheRegisterSyscacheCallback(FOREIGNDATAWRAPPEROID, PlanCacheSysCallback, (Datum) 0);
+	CacheRegisterSyscacheCallback(VARIABLEOID, PlanCacheObjectCallback, (Datum) 0);
 }
 
 /*
@@ -1868,12 +1869,24 @@ ScanQueryForLocks(Query *parsetree, bool acquire)
 	 * Recurse into sublink subqueries, too.  But we already did the ones in
 	 * the rtable and cteList.
 	 */
-	if (parsetree->hasSubLinks)
+	if (parsetree->hasSubLinks ||
+		parsetree->hasSessionVariables)
 	{
 		query_tree_walker(parsetree, ScanQueryWalker,
 						  (void *) &acquire,
 						  QTW_IGNORE_RC_SUBQUERIES);
 	}
+
+	/* Process session variables */
+	if (OidIsValid(parsetree->resultVariable))
+	{
+		if (acquire)
+			LockDatabaseObject(VariableRelationId, parsetree->resultVariable,
+							   0, AccessShareLock);
+		else
+			UnlockDatabaseObject(VariableRelationId, parsetree->resultVariable,
+							   0, AccessShareLock);
+	}
 }
 
 /*
diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h
index 83b8afcd6c..87bc93f60d 100644
--- a/src/include/commands/session_variable.h
+++ b/src/include/commands/session_variable.h
@@ -38,4 +38,7 @@ extern void AtPreEOXact_SessionVariable_on_xact_actions(bool isCommit);
 extern void AtEOSubXact_SessionVariable_on_xact_actions(bool isCommit, SubTransactionId mySubid,
 											SubTransactionId parentSubid);
 
 #endif
index 0000000000..1d823a42a8
+ * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
index b1d4e82062..3accd22ad2 100644
@@ -1986,6 +1986,21 @@ typedef struct MergeStmt
 } MergeStmt;
 
index e03727e19f..7108203d2b 100644
@@ -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 */

diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index d7cfc9a6f3..40d4c63e05 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -255,6 +255,7 @@ PG_KEYWORD("leading", LEADING, RESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("leakproof", LEAKPROOF, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("least", LEAST, COL_NAME_KEYWORD, BARE_LABEL)
 PG_KEYWORD("left", LEFT, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL)
+PG_KEYWORD("let", LET, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("level", LEVEL, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("like", LIKE, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL)
 PG_KEYWORD("limit", LIMIT, RESERVED_KEYWORD, AS_LABEL)
diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h
index d07f5b0584..43fd9ca887 100644
--- a/src/include/parser/parse_node.h
+++ b/src/include/parser/parse_node.h
@@ -81,7 +81,8 @@ typedef enum ParseExprKind
 	EXPR_KIND_COPY_WHERE,		/* WHERE condition in COPY FROM */
 	EXPR_KIND_GENERATED_COLUMN, /* generation expression for a column */
 	EXPR_KIND_CYCLE_MARK,		/* cycle mark value */
-	EXPR_KIND_VARIABLE_DEFAULT	/* default value for session variable */
+	EXPR_KIND_VARIABLE_DEFAULT,	/* default value for session variable */
+	EXPR_KIND_LET_TARGET		/* LET assignment (should be same like UPDATE) */
 } ParseExprKind;

index 6c1be5e9d4..357989b52f 100644
@@ -186,6 +186,7 @@ PG_CMDTAG(CMDTAG_GRANT, "GRANT", true, false, false)
index 3c3eabae67..7f18e07915 100644
 
 /* ----------------
-- 
2.37.2

From e3963749a3b1678acc37ab381c24b325fe2b7370 Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Mon, 4 Jul 2022 06:06:19 +0200
Subject: [PATCH 03/12] typecheck - check of consistency of format of stored
 value.

The lifecycle of value of session variable can be long. In this time
the composite type can changed, or extension that defines the type
can be upgraded. In this case we cannot to use stored value.

Session variables holds a own evidence of fingerprints for any
used type. When some possible not compatible change is detected
(the version of extension is changed), the related stored type's
generation number is increased. The copy of genaration number is
stored in session variable data. Before any read of session variable,
the stored generation number is compared with current type generation
number. When it is different, the read is canceled.
---
 src/backend/commands/sessionvariable.c | 1154 +++++++++++++++++++++---
 1 file changed, 1051 insertions(+), 103 deletions(-)

diff --git a/src/backend/commands/sessionvariable.c b/src/backend/commands/sessionvariable.c
index 1d1b9747e5..0ff8ead99d 100644
--- a/src/backend/commands/sessionvariable.c
+++ b/src/backend/commands/sessionvariable.c
@@ -14,12 +14,15 @@
  */
 #include "postgres.h"
 #include "miscadmin.h"
+#include "access/genam.h"
 #include "access/heapam.h"
 #include "access/htup_details.h"
 #include "access/xact.h"
 #include "catalog/dependency.h"
 #include "catalog/indexing.h"
 #include "catalog/namespace.h"
+#include "catalog/pg_extension.h"
+#include "catalog/pg_type.h"
 #include "catalog/pg_variable.h"
 #include "commands/session_variable.h"
 #include "executor/executor.h"
@@ -31,14 +34,18 @@
 #include "parser/parse_expr.h"
 #include "parser/parse_type.h"
 #include "rewrite/rewriteHandler.h"
+#include "storage/itemptr.h"
 #include "storage/lmgr.h"
+#include "storage/proc.h"
 #include "tcop/tcopprot.h"
 #include "utils/builtins.h"
 #include "utils/datum.h"
+#include "utils/fmgroids.h"
 #include "utils/inval.h"
 #include "utils/lsyscache.h"
 #include "utils/snapmgr.h"
 #include "utils/syscache.h"
+#include "utils/typcache.h"
 
 /*
  * Values of session variables are stored in local memory, in
@@ -66,7 +73,7 @@
  *   operation or by owner's extension update. We store fingerprint
  *   of any type used by session variables, and before any
  *   read we check if this fingerprint is still valid (see
- *   SVariableType related operations). Built-in non composite
+ *   SVariableType related operations). Built-in non row
  *   types are trusty every time.
  *
  *   For other cases we can check xmin, and if xmin is different,
@@ -76,6 +83,18 @@
  *   version). For domains we should to force cast to domain type
  *   to reply all constraints before returning the value.
  *
+ *   We have not too much possibilities for check of identity
+ *   of row types (against oid's overflow) because for
+ *   custom types, the value of relid is constant offset
+ *   from type's oid. But it can work well for row types
+ *   based on tables. On second hand we can every time to convert
+ *   stored value to expected (current) tupledesc (if it is
+ *   possible. Unfortunatelly we cannot to use already prepared
+ *   conversions (based on mapping of undeleted fields or based
+ *   on names). In this case we expect so target tupledesc is
+ *   just modified original tupledesc. So mapping should be done
+ *   really just on position.
+ *
  *  b) the variable can be dropped - we don't purge memory
  *   when the variable is dropped immediately, so theoretically
  *   we can return dropped value. The protection against this
@@ -151,6 +170,58 @@
  * support session variables (the reading of session variables is
  * fully transparent for PL/pgSQL).
  */
+struct SVariableTypeDataField;
+
+typedef struct SVaribleTypeData
+{
+	Oid			typid;
+	Oid			relid;			/* OID of relation containing this attribute *
+								 * used for check of oid's overflow */
+
+	int64		gennum;			/* generation number helps with change detection */
+	LocalTransactionId verified_lxid; /* lxid of transaction when the typ fingerprint
+									   * was created or verified */
+	int			natts;
+	struct		SVariableTypeDataField *attrs;
+
+	int16		typlen;
+	bool		typbyval;
+	bool		is_domain;
+	bool		is_rowtype;
+	char	   *typname;
+
+	struct		SVaribleTypeData *base_type;
+	int64		base_type_gennum;
+	void	   *domain_check_extra;		/* domain check routine */
+	LocalTransactionId domain_check_extra_lxid; /* lxid when value of domain's type *
+												 * was checked */
+
+	TransactionId	typ_xmin;	/* xmin of related tuple of pg_type */
+	ItemPointerData typ_tid;
+
+	Oid			extid;			/* OID of owner extension if exists */
+	TransactionId ext_xmin;		/* xmin of related tuple of pg_extension */
+	ItemPointerData ext_tid;
+	char	   *extname;
+	char	   *extversion;
+
+} SVariableTypeData;
+
+typedef SVariableTypeData * SVariableType;
+
+/*
+ * Following structure is used as type's finger print for
+ * fields of row type. It helps with fast type identity
+ * check for row value.
+ */
+typedef struct SVariableTypeDataField
+{
+	SVariableType svartype;
+	int64		gennum;
+	int32		typmod;
+	bool		isdropped;
+} SVariableTypeDataField;
+
 typedef enum SVariableXActAction
 {
 	SVAR_ON_COMMIT_DROP,		/* used for ON COMMIT DROP */
@@ -211,18 +282,38 @@ static LocalTransactionId synced_lxid = InvalidLocalTransactionId;
 typedef struct SVariableData
 {
 	Oid			varid;			/* pg_variable OID of this sequence (hash key) */
+	Oid			relid;			/* used for fast row type's identity check */
 
-	char	   *name;		/* for debug purposes */
-	char	   *nsname;		/* for debug purposes */
+	/*
+	 * Next two items can be used just for debug purposes.
+	 * The stored nsnane and name can be obsolete, when schema
+	 * or variable name are altered. This behaviour can be
+	 * acceptable for debug purposes (and it is necessary, because
+	 * sometimes we cannot to access to catalog.
+	 */
+	char	   *name;			/* session variable name (at time of initialization) */
+	char	   *nsname;			/* session variable schema name */
 
 	SVariableType svartype;
 	int64		gennum;
 
+	/*
+	 * We need to store details about fields, because
+	 * we can convert old row value to new format,
+	 * when the difference is just dropped or new
+	 * fields. But for converted fields we need to
+	 * check type too.
+	 */
+	int			natts;
+	struct		SVariableTypeDataField *attrs;
+
 	bool		isnull;
 	bool		freeval;
 	Datum		value;
+	TupleDesc	tupdesc;		/* for row type we save actual tupdesc *
+								 * at save time, for conversion to uptime *
+								 * tupdesc in read time */
 
-	bool		is_rowtype;		/* true when variable is composite */
 	bool		is_not_null;	/* don't allow null values */
 	bool		is_immutable;	/* true when variable is immutable */
 	bool		has_defexpr;	/* true when variable has a default value */
@@ -230,19 +321,27 @@ typedef struct SVariableData
 	bool		is_valid;		/* true when variable was successfully
 								 * initialized */
 
-	uint32		hashvalue;
+	uint32		hashvalue;		/* used for pairing sinval message */
 
 	bool		eox_reset;		/* true, when lifecycle is limitted by transaction */
 }			SVariableData;
 
 typedef SVariableData * SVariable;
 
-static HTAB *sessionvars = NULL;	/* hash table for session variables */
+static HTAB *sessionvars = NULL;		/* hash table for session variables */
+static HTAB *sessionvars_types = NULL;	/* hash table for type fingerprints of session
+										 * variables */
+
 static MemoryContext SVariableMemoryContext = NULL;
 
 static bool first_time = true;

+static SVariableType get_svariabletype(Oid typid);
+static int64 get_svariable_valid_type_gennum(SVariableType svt);
+
+static void init_session_variable(SVariable svar, Variable *var);
+
 static void register_session_variable_xact_action(Oid varid, SVariableXActAction action);
 static void unregister_session_variable_xact_action(Oid varid, SVariableXActAction action);
 
@@ -264,12 +363,572 @@ SvariableXActActionName(SVariableXActAction action)
 	}
 }
 
+/*
+ * In this case we know, so fast comparing fails.
+ */
+static bool
+svariabletypes_equals(SVariableType svt1, SVariableType svt2)
+{
+	Assert(svt1->typid == svt2->typid);
+	Assert(svt1 != svt2);
+
+	/*
+	 * for trustworthy check we need to know base type, extension,
+	 * or row fields. Just typlen, and typbyval is not enough
+	 * trustworthy, because (-1, false) is very common. In this case
+	 * we can check only the name (for other cases we don't check the
+	 * name, because the name can be altered).
+	 */
+	if (!(OidIsValid(svt1->extid) ||
+		  svt1->base_type ||
+		  svt1->is_rowtype))
+	{
+		if (strcmp(svt1->typname, svt2->typname) != 0)
+			return false;
+	}
+
+	if (svt1->typlen != svt2->typlen)
+		return false;
+
+	if (svt1->typbyval != svt2->typbyval)
+		return false;
+
+	if (svt1->is_domain != svt2->is_domain)
+		return false;
+
+	if (svt1->is_rowtype != svt2->is_rowtype)
+		return false;
+
+	if (svt1->is_domain)
+	{
+		if (svt1->base_type->typid != svt2->base_type->typid)
+			return false;
+
+		if (svt1->base_type_gennum != svt2->base_type_gennum)
+			return false;
+	}
+
+	if (OidIsValid(svt1->extid))
+	{
+		if (svt1->extid != svt2->extid)
+			return false;
+
+		if (strcmp(svt1->extname, svt2->extname) != 0)
+			return false;
+
+		if (strcmp(svt1->extversion, svt2->extversion) != 0)
+			return false;
+	}
+
+	if (svt1->is_rowtype)
+	{
+		int		i;
+
+		if (svt1->relid != svt2->relid)
+			return false;
+
+		if (svt1->natts != svt2->natts)
+			return false;
+
+		for (i = 0; i < svt1->natts; i++)
+		{
+			if (svt1->attrs[i].isdropped != svt2->attrs[i].isdropped)
+				return false;
+
+			if (!svt1->attrs[i].isdropped)
+			{
+				if (svt1->attrs[i].svartype->typid != svt2->attrs[i].svartype->typid)
+					return false;
+
+				if (svt1->attrs[i].typmod != svt2->attrs[i].typmod)
+					return false;
+
+				if (svt1->attrs[i].gennum != svt2->attrs[i].gennum)
+					return false;
+			}
+		}
+	}
+
+	return true;
+}
+
+static void
+svariabletype_free(SVariableType svt)
+{
+	pfree(svt->typname);
+
+	if (OidIsValid(svt->extid))
+	{
+		pfree(svt->extname);
+		pfree(svt->extversion);
+	}
+
+	if (svt->natts > 0)
+		pfree(svt->attrs);
+}
+
+/*
+ * Update fields used for fast check
+ */
+static void
+svariabletype_refresh(SVariableType svt1, SVariableType svt2)
+{
+	svt1->typ_xmin = svt2->typ_xmin;
+	svt1->typ_tid = svt2->typ_tid;
+
+	svt1->ext_xmin = svt2->ext_xmin;
+	svt1->ext_tid = svt2->ext_tid;
+}
+
+/*
+ * Update all fields and increase generation number
+ */
+static void
+svariabletype_update(SVariableType svt1, SVariableType svt2)
+{
+	int		gennum = svt1->gennum;
+
+	svariabletype_free(svt1);
+
+	memcpy(svt1, svt2, sizeof(SVariableTypeData));
+
+	svt1->gennum = gennum + 1;
+}
+
+/*
+ * When type owner's extension is not changed, then we can belive
+ * so type is still valid. For this check we need to hold few
+ * information about extension in memory. We can do fast check
+ * based on xmin, tid or slow check on oid, name and version.
+ */
+static void
+svariabletype_assign_extension(SVariableType svt, Oid extid)
+{
+	Relation	pg_extension;
+	ScanKeyData entry[1];
+	SysScanDesc scan;
+	HeapTuple	tuple;
+	Form_pg_extension ext;
+	bool		isnull;
+	Datum		datum;
+	MemoryContext oldcxt;
+
+	Assert(OidIsValid(extid));
+
+	/* There's no syscache for pg_extension, so do it the hard way */
+	pg_extension = table_open(ExtensionRelationId, AccessShareLock);
+
+	ScanKeyInit(&entry[0],
+				Anum_pg_extension_oid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(extid));
+
+	scan = systable_beginscan(pg_extension,
+							  ExtensionOidIndexId, true,
+							  NULL, 1, entry);
+
+	tuple = systable_getnext(scan);
+	if (!HeapTupleIsValid(tuple))
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("extension with OID %u does not exist", svt->extid)));
+
+	ext = (Form_pg_extension) GETSTRUCT(tuple);
+	svt->extid = extid;
+	svt->ext_xmin = HeapTupleHeaderGetRawXmin(tuple->t_data);
+	svt->ext_tid = tuple->t_self;
+
+	datum = heap_getattr(tuple, Anum_pg_extension_extversion,
+						 RelationGetDescr(pg_extension), &isnull);
+
+	if (isnull)
+		elog(ERROR, "extversion is null");
+
+	oldcxt = MemoryContextSwitchTo(SVariableMemoryContext);
+
+	svt->extversion = TextDatumGetCString(datum);
+	svt->extname = pstrdup(NameStr(ext->extname));
+
+	MemoryContextSwitchTo(oldcxt);
+
+	systable_endscan(scan);
+	table_close(pg_extension, AccessShareLock);
+}
+
+static bool
+svariabletype_verify_ext_fast(SVariableType svt)
+{
+	Relation	pg_extension;
+	ScanKeyData entry[1];
+	SysScanDesc scan;
+	HeapTuple	tuple;
+	bool		result = true;
+
+	Assert(OidIsValid(svt->extid));
+
+	/* There's no syscache for pg_extension, so do it the hard way */
+	pg_extension = table_open(ExtensionRelationId, AccessShareLock);
+
+	ScanKeyInit(&entry[0],
+				Anum_pg_extension_oid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(svt->extid));
+
+	scan = systable_beginscan(pg_extension,
+							  ExtensionOidIndexId, true,
+							  NULL, 1, entry);
+
+	tuple = systable_getnext(scan);
+	if (HeapTupleIsValid(tuple))
+	{
+		if (svt->ext_xmin != HeapTupleHeaderGetRawXmin(tuple->t_data) ||
+			!ItemPointerEquals(&svt->ext_tid, &tuple->t_self))
+			result = false;
+	}
+	else
+		result = false;
+
+	systable_endscan(scan);
+	table_close(pg_extension, AccessShareLock);
+
+	return result;
+}
+
+/*
+ * We hold data like typlen, typbyval for usual purposes. More
+ * we hold xmin, tid, extid like type's fingerprint. This is
+ * used later for type verification stored value of session variable.
+ */
+static void
+svariabletype_init(SVariableType svt, HeapTuple tuple, Oid typid)
+{
+	Form_pg_type typ;
+	MemoryContext oldcxt;
+
+	memset(svt, 0, sizeof(SVariableTypeData));
+
+	svt->typid = typid;
+	svt->gennum = 1;
+
+	typ = (Form_pg_type) GETSTRUCT(tuple);
+
+	/* save basic attributtes */
+	svt->relid = typ->typrelid;
+
+	svt->typlen = typ->typlen;
+	svt->typbyval = typ->typbyval;
+
+	/* save info about type */
+	svt->typ_xmin = HeapTupleHeaderGetRawXmin(tuple->t_data);
+	svt->typ_tid = tuple->t_self;
+
+	oldcxt = MemoryContextSwitchTo(SVariableMemoryContext);
+
+	svt->typname = pstrdup(NameStr(typ->typname));
+
+	MemoryContextSwitchTo(oldcxt);
+
+	if (typ->typtype == TYPTYPE_DOMAIN)
+	{
+		Oid			basetypid;
+
+		svt->is_domain = true;
+		basetypid = getBaseType(typid);
+
+		svt->base_type = get_svariabletype(basetypid);
+		svt->base_type_gennum = get_svariable_valid_type_gennum(svt->base_type);
+		svt->is_rowtype = svt->base_type->is_rowtype;
+	}
+	else
+	{
+		svt->is_domain = false;
+		svt->is_rowtype = typ->typtype == TYPTYPE_COMPOSITE;
+	}
+
+	/*
+	 * Store fingerprints of fields of row types.
+	 * Probably buildin types should not be changed, but
+	 * just be safe, and store fingerprints for all composite
+	 * types (including buildin composite types).
+	*/
+	if (svt->is_rowtype)
+	{
+		Oid			rowtypid;
+		TupleDesc	tupdesc;
+		int			i;
+
+		if (svt->is_domain)
+			rowtypid = svt->base_type->typid;
+		else
+			rowtypid = svt->typid;
+
+		tupdesc = lookup_rowtype_tupdesc(rowtypid, -1);
+
+		svt->attrs = MemoryContextAlloc(SVariableMemoryContext,
+											 tupdesc->natts * sizeof(SVariableTypeDataField));
+
+		for (i = 0; i < tupdesc->natts; i++)
+		{
+			SVariableType field_svartype;
+			Form_pg_attribute attr = TupleDescAttr(tupdesc, i);
+
+			if (!attr->attisdropped)
+			{
+				field_svartype = get_svariabletype(attr->atttypid);
+
+				svt->attrs[i].svartype = field_svartype;
+				svt->attrs[i].gennum = get_svariable_valid_type_gennum(field_svartype);
+				svt->attrs[i].typmod = attr->atttypmod;
+				svt->attrs[i].isdropped = false;
+			}
+			else
+				svt->attrs[i].isdropped = true;
+		}
+
+		svt->natts = tupdesc->natts;
+
+		ReleaseTupleDesc(tupdesc);
+	}
+
+	svt->domain_check_extra_lxid = InvalidLocalTransactionId;
+
+	svt->verified_lxid = MyProc->lxid;
+
+	/* try to find related extension */
+	svt->extid = getExtensionOfObject(TypeRelationId, typid);
+
+	if (OidIsValid(svt->extid))
+		svariabletype_assign_extension(svt, svt->extid);
+}
+
+/*
+ * the field check can ignore dropped fields
+ */
+static bool
+svartype_verify_composite_fast(SVariableType svt)
+{
+	bool	result = true;
+	TupleDesc	tupdesc;
+	int		i;
+
+	Assert(svt);
+	Assert(svt->is_rowtype);
+
+	tupdesc = lookup_rowtype_tupdesc_noerror(svt->typid, -1, true);
+	if (!tupdesc)
+		return false;
+
+	if (svt->natts == tupdesc->natts)
+	{
+		for (i = 0; i < svt->natts; i++)
+		{
+			Form_pg_attribute attr = TupleDescAttr(tupdesc, i);
+
+			if (svt->relid != attr->attrelid)
+			{
+				result = false;
+				break;
+			}
+
+			if (!svt->attrs[i].isdropped)
+			{
+				SVariableType field_svt = svt->attrs[i].svartype;
+				int64		field_gennum = svt->attrs[i].gennum;
+
+				if (attr->attisdropped)
+				{
+					result = false;
+					break;
+				}
+
+				if (field_svt->typid != attr->atttypid)
+				{
+					result = false;
+					break;
+				}
+
+				if (field_gennum != field_svt->gennum)
+				{
+					result = false;
+					break;
+				}
+
+				if (svt->attrs[i].typmod != attr->atttypmod)
+				{
+					result = false;
+					break;
+				}
+
+				if (field_gennum != get_svariable_valid_type_gennum(field_svt))
+				{
+					result = false;
+					break;
+				}
+			}
+			else
+			{
+				if (!attr->attisdropped)
+				{
+					result = false;
+					break;
+				}
+			}
+		}
+	}
+	else
+		result = false;
+
+	ReleaseTupleDesc(tupdesc);
+
+	return result;
+}
+
+/*
+ * Check type fingerprint and if it is not valid, then does an update, and
+ * increase generation number. We can trust just to buildin types. Composite
+ * types are checked recusively until we iterarate to buildin types. External
+ * types are valid if related record is without change, or version string is
+ * equal. Although the record can be untouched, we need to check extension record,
+ * because format can be changed by extension updade.
+ */
+static int64
+get_svariable_valid_type_gennum(SVariableType svt)
+{
+	HeapTuple	tuple;
+	bool		fast_check = true;
+
+	Assert(svt);
+
+	/* Buildin scalar objects are trustworthy */
+	if (svt->typid < FirstNormalObjectId && !svt->is_rowtype)
+		return svt->gennum;
+
+	/* don't repeat check in one transaction */
+	if (svt->verified_lxid == MyProc->lxid)
+		return svt->gennum;
+
+	/*
+	 * First we check the type record. If it was not changed, then
+	 * we can trust to stored fingerprint. In this case we can do
+	 * fast check of extension (because the format of type can be
+	 * changed when extension was updated). When fast check of type
+	 * fails, we have to run slow check.
+	 */
+	tuple = SearchSysCache1(TYPEOID, ObjectIdGetDatum(svt->typid));
+	if (!(svt->typ_xmin == HeapTupleHeaderGetRawXmin(tuple->t_data) &&
+		  ItemPointerEquals(&svt->typ_tid, &tuple->t_self)))
+		fast_check = false;
+
+	if (fast_check && OidIsValid(svt->extid))
+	{
+		if (!svariabletype_verify_ext_fast(svt))
+			fast_check = false;
+	}
+
+	/* When type or extension records are up to date, check base type */
+	if (fast_check && svt->is_domain)
+	{
+		if (svt->base_type_gennum != svt->base_type->gennum)
+		{
+			fast_check = false;
+		}
+		else if (get_svariable_valid_type_gennum(svt->base_type) !=
+			  svt->base_type_gennum)
+		{
+			fast_check = false;
+		}
+	}
+
+	if (fast_check && svt->is_rowtype)
+	{
+		if (!svartype_verify_composite_fast(svt))
+			fast_check = false;
+	}
+
+	if (!fast_check)
+	{
+		SVariableTypeData nsvtd;
+
+		/*
+		 * Slow check. We create new SVariableType value. Compare it with
+		 * previous value, and if it is different, then we replace old by
+		 * new and we increase gennum
+		 */
+		svariabletype_init(&nsvtd, tuple, svt->typid);
+
+		if (svariabletypes_equals(svt, &nsvtd))
+		{
+			svariabletype_refresh(svt, &nsvtd);
+			svariabletype_free(&nsvtd);
+		}
+		else
+			svariabletype_update(svt, &nsvtd);
+	}
+
+	ReleaseSysCache(tuple);
+
+	svt->verified_lxid = MyProc->lxid;
+
+	return svt->gennum;
+}
+
+static SVariableType
+get_svariabletype(Oid typid)
+{
+	SVariableType svt;
+	bool		found;
+
+	Assert(sessionvars_types);
+
+	svt = (SVariableType) hash_search(sessionvars_types, &typid,
+										   HASH_ENTER, &found);
+
+	if (!found)
+	{
+		HeapTuple	tuple;
+
+		tuple = SearchSysCache1(TYPEOID, ObjectIdGetDatum(typid));
+		if (!HeapTupleIsValid(tuple))
+			elog(ERROR, "cache lookup failed for type %u", typid);
+
+		svariabletype_init(svt, tuple, typid);
+
+		ReleaseSysCache(tuple);
+	}
+
+	return svt;
+}
+
+/*
+ * Returns true, when type of stored value is still valid
+ */
+static bool
+session_variable_use_valid_type(SVariable svar)
+{
+	Assert(svar);
+	Assert(svar->svartype);
+
+	/*
+	 * when referenced type is not valid of obsolete, the
+	 * value is stored in maybe not up the data format.
+	 */
+	if (svar->gennum != svar->svartype->gennum)
+		return false;
+
+	/* enforce type verification, get fresh generation number */
+	if (svar->gennum != get_svariable_valid_type_gennum(svar->svartype))
+		return false;
+
+	return true;
+}
+
 /*
  * Releases stored data from session variable, but preserve the hash entry
- * in sessionvars.
+ * in sessionvars. When we replace row value by new value with same type
+ * fingerprint, we can keep field desc data.
  */
 static void
-free_session_variable_value(SVariable svar)
+free_session_variable_value(SVariable svar, bool keep_desc_fields)
 {
 	if (svar->freeval)
 		pfree(DatumGetPointer(svar->value));
@@ -279,6 +938,21 @@ free_session_variable_value(SVariable svar)
 	svar->isnull = true;
 	svar->freeval = false;
 
+	if (!keep_desc_fields)
+	{
+		if (svar->natts > 0)
+		{
+			pfree(svar->attrs);
+			svar->natts = 0;
+		}
+
+		if (svar->tupdesc)
+		{
+			FreeTupleDesc(svar->tupdesc);
+			svar->tupdesc = NULL;
+		}
+	}
+
 	/*
 	 * We can mark this session variable as valid when
 	 * it has not default expression, and when null is
@@ -295,7 +969,7 @@ free_session_variable_value(SVariable svar)
 static void
 remove_session_variable(SVariable svar)
 {
-	free_session_variable_value(svar);
+	free_session_variable_value(svar, false);
 
 	elog(DEBUG1, "session variable \"%s.%s\" (oid:%u) is removing from memory",
 				 svar->nsname, svar->name,
@@ -508,9 +1182,10 @@ sync_sessionvars_all()
  * Create the hash table for storing session variables
  */
 static void
-create_sessionvars_hashtable(void)
+create_sessionvars_hashtables(void)
 {
-	HASHCTL		ctl;
+	HASHCTL		vars_ctl;
+	HASHCTL		types_ctl;
 
 	/* set callbacks */
 	if (first_time)
@@ -530,14 +1205,24 @@ create_sessionvars_hashtable(void)
 
 	Assert(SVariableMemoryContext);
 
-	memset(&ctl, 0, sizeof(ctl));
-	ctl.keysize = sizeof(Oid);
-	ctl.entrysize = sizeof(SVariableData);
-	ctl.hcxt = SVariableMemoryContext;
+	memset(&vars_ctl, 0, sizeof(vars_ctl));
+	vars_ctl.keysize = sizeof(Oid);
+	vars_ctl.entrysize = sizeof(SVariableData);
+	vars_ctl.hcxt = SVariableMemoryContext;
 
 	Assert(sessionvars == NULL);
 
-	sessionvars = hash_create("Session variables", 64, &ctl,
+	sessionvars = hash_create("Session variables", 64, &vars_ctl,
+								   HASH_ELEM | HASH_BLOBS | HASH_CONTEXT);
+
+	memset(&types_ctl, 0, sizeof(types_ctl));
+	types_ctl.keysize = sizeof(Oid);
+	types_ctl.entrysize = sizeof(SVariableTypeData);
+	types_ctl.hcxt = SVariableMemoryContext;
+
+	Assert(sessionvars_types == NULL);
+
+	sessionvars_types = hash_create("Session variable types", 64, &types_ctl,
 								   HASH_ELEM | HASH_BLOBS | HASH_CONTEXT);
 }
 
@@ -548,31 +1233,56 @@ create_sessionvars_hashtable(void)
  * init_mode is true, when the value of session variable is being initialized
  * by default expression or by null. Only in this moment we can allow to
  * modify immutable variables with default expression.
+ *
+ * This routine have to do successfull change or leave memory without
+ * change.
  */
 static void
 set_session_variable(SVariable svar, Datum value,
 					 bool isnull, Oid typid,
 					 bool init_mode)
 {
-	MemoryContext oldcxt;
+	TupleDesc	tupdesc = NULL;
+	TupleDesc	newtupdesc = NULL;
 	Datum		newval = value;
+	SVariableData _svardata, *_svar;
+	bool		fresh_svar = false;
+
+	Assert(svar && svar->svartype);
+
+	/* We have to be sure so cached data are still valid. */
+	if (svar->svartype->typid == typid)
+	{
+		if (!session_variable_use_valid_type(svar))
+			fresh_svar = true;
+	}
+	else
+		fresh_svar = true;
+
+	if (fresh_svar)
+	{
+		Variable	var;
+
+		initVariable(&var, svar->varid, true);
+		init_session_variable(&_svardata, &var);
+		_svar = &_svardata;
+	}
+	else
+	{
+		/* We can trust the cached entry */
+		Assert(svar->svartype->typid == typid);
+		Assert(svar->gennum == svar->svartype->gennum);
+
+		_svar = svar;
+	}
 
 	/* Don't allow assignment of null to NOT NULL variable */
-	if (isnull && svar->is_not_null)
+	if (isnull && _svar->is_not_null)
 		ereport(ERROR,
 				(errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
 				 errmsg("null value is not allowed for NOT NULL session variable \"%s.%s\"",
-						get_namespace_name(get_session_variable_namespace(svar->varid)),
-						get_session_variable_name(svar->varid))));
-
-	if (svar->typid != typid)
-		ereport(ERROR,
-				(errcode(ERRCODE_DATATYPE_MISMATCH),
-				 errmsg("type \"%s\" of assigned value is different than type \"%s\" of session variable \"%s.%s\"",
-						format_type_be(typid),
-						format_type_be(svar->typid),
-						get_namespace_name(get_session_variable_namespace(svar->varid)),
-						get_session_variable_name(svar->varid))));
+						get_namespace_name(get_session_variable_namespace(_svar->varid)),
+						get_session_variable_name(_svar->varid))));
 
 	/*
 	 * Don't allow updating of immutable session variable that has assigned
@@ -581,25 +1291,57 @@ set_session_variable(SVariable svar, Datum value,
 	 * is being initialized.
 	 */
 	if (!init_mode &&
-		(svar->is_immutable && (svar->is_valid || svar->has_defexpr)))
+		(_svar->is_immutable &&
+		 (_svar->is_valid || _svar->has_defexpr)))
 		ereport(ERROR,
 				(errcode(ERRCODE_ERROR_IN_ASSIGNMENT),
 				 errmsg("session variable \"%s.%s\" is declared IMMUTABLE",
-						get_namespace_name(get_session_variable_namespace(svar->varid)),
-						get_session_variable_name(svar->varid))));
+						get_namespace_name(get_session_variable_namespace(_svar->varid)),
+						get_session_variable_name(_svar->varid))));
+
+	/* We don't need tupdesc for NULL value */
+	if (!isnull &&
+		 _svar->svartype->is_rowtype && !_svar->tupdesc)
+		tupdesc = lookup_rowtype_tupdesc(_svar->svartype->typid, -1);
 
 	/* copy value to session persistent context */
-	oldcxt = MemoryContextSwitchTo(SVariableMemoryContext);
 	if (!isnull)
-		newval = datumCopy(value, svar->typbyval, svar->typlen);
-	MemoryContextSwitchTo(oldcxt);
+	{
+		MemoryContext oldcxt = MemoryContextSwitchTo(SVariableMemoryContext);
+
+		newval = datumCopy(value,
+						   _svar->svartype->typbyval,
+						   _svar->svartype->typlen);
+
+		if (tupdesc)
+		{
+			newtupdesc = palloc(TupleDescSize(tupdesc));
+			TupleDescCopy(newtupdesc, tupdesc);
+		}
 
-	free_session_variable_value(svar);
+		MemoryContextSwitchTo(oldcxt);
+	}
+
+	/* When we have new svar, we want full clean of old svar */
+	free_session_variable_value(svar, !fresh_svar);
+
+	_svar->value = newval;
+	if (newtupdesc)
+		_svar->tupdesc = newtupdesc;
+
+	_svar->isnull = isnull;
+	_svar->freeval = newval != value;
+	_svar->is_valid = true;
+
+	if (fresh_svar)
+		memcpy(svar, _svar, sizeof(SVariableData));
+
+	if (tupdesc)
+		ReleaseTupleDesc(tupdesc);
 
-	svar->value = newval;
-	svar->isnull = isnull;
-	svar->freeval = newval != value;
-	svar->is_valid = true;
+	elog(DEBUG1, "session variable \"%s.%s\" (oid:%u) has new value",
+				 svar->nsname, svar->name,
+				 svar->varid);
 }
 
 /*
@@ -621,12 +1363,23 @@ init_session_variable(SVariable svar, Variable *var)
 
 	svar->svartype = get_svariabletype(var->typid);
 	svar->gennum = get_svariable_valid_type_gennum(svar->svartype);
+	svar->relid = svar->svartype->relid;
+
+	svar->natts = svar->svartype->natts;
+	if (svar->natts > 0)
+	{
+		size_t		bytes = svar->natts * sizeof(SVariableTypeDataField);
+
+		svar->attrs = palloc(bytes);
+		memcpy(svar->attrs, svar->svartype->attrs, bytes);
+	}
+	else
+		svar->attrs = NULL;
 
+	svar->value = (Datum) 0;
 	svar->isnull = true;
 	svar->freeval = false;
-	svar->value = (Datum) 0;
 
-	svar->is_rowtype = type_is_rowtype(var->typid);
 	svar->is_not_null = var->is_not_null;
 	svar->is_immutable = var->is_immutable;
 	svar->has_defexpr = var->has_defexpr;
@@ -650,9 +1403,131 @@ init_session_variable(SVariable svar, Variable *var)
 		MemoryContextSwitchTo(oldcxt);
 	}
 
+	svar->tupdesc = NULL;
+
 	MemoryContextSwitchTo(oldcxt);
 }
 
+/*
+ * Try to convert the composite value to the current expected format. This
+ * routine is called when we know so expected composite format is not identical
+ * with composite format of saved value. If there are difference just in
+ * number (and positions) of dropped fields, we can try to build new
+ * composite value, where dropped fields are replaced by NULLs. Unfortunately
+ * we cannot to use TupleConversionMap infra, because this conversion should
+ * ba based on position (not on position of not dropped fields).
+ */
+static Datum
+get_rowtype_value_in_expected_format(SVariable svar)
+{
+	SVariableType target_svt = svar->svartype;
+	Datum		value = (Datum) 0;
+	int		i;
+
+	Assert(svar->natts > 0 && target_svt->is_rowtype);
+	Assert(svar->relid == target_svt->relid);
+
+	/*
+	 * Before conversion we need to refresh assigned target type.
+	 * We don't update gennum in svariable, so the conversion will
+	 * be forced every time until value update.
+	 */
+	get_svariable_valid_type_gennum(target_svt);
+
+	/* check if we can do this transformation */
+	for (i = 0; i < target_svt->natts; i++)
+	{
+		if (i >= svar->natts)
+			continue;
+
+		if (target_svt->attrs[i].isdropped || svar->attrs[i].isdropped)
+			continue;
+
+		if (svar->attrs[i].svartype->typid != target_svt->attrs[i].svartype->typid ||
+			svar->attrs[i].typmod != target_svt->attrs[i].typmod)
+				ereport(ERROR,
+						(errcode(ERRCODE_DATATYPE_MISMATCH),
+						 errmsg("record type of stored value of \"%s.%s\" does not match expected record type",
+								get_namespace_name(get_session_variable_namespace(svar->varid)),
+								get_session_variable_name(svar->varid)),
+						 errdetail("Expected format of %d field is %s.",
+								   i,
+								   format_type_with_typemod(target_svt->attrs[i].svartype->typid,
+															target_svt->attrs[i].typmod))));
+
+		if (svar->attrs[i].gennum != target_svt->attrs[i].svartype->gennum)
+			ereport(ERROR,
+					(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+					 errmsg("format of stored value of %d field of \"%s.%s\" session variable can be outdated",
+							i,
+							get_namespace_name(get_session_variable_namespace(svar->varid)),
+							get_session_variable_name(svar->varid))));
+	}
+
+	/* We finish checks. When stored value is null, we can leave */
+	if (!svar->isnull)
+	{
+		Datum	   *invalues;
+		bool	   *inisnull;
+		Datum	   *outvalues;
+		bool	   *outisnull;
+		TupleDesc  outtupdesc;
+		HeapTuple	tuple;
+		HeapTupleHeader td;
+		HeapTupleData tmptup;
+
+		td = DatumGetHeapTupleHeader(svar->value);
+		tmptup.t_len = HeapTupleHeaderGetDatumLength(td);
+		tmptup.t_data = td;
+
+		/* returns fresh tupdesc */
+		outtupdesc = lookup_rowtype_tupdesc(target_svt->typid, -1);
+
+		Assert(outtupdesc->natts == target_svt->natts);
+		Assert(svar->tupdesc->natts == svar->natts);
+
+		invalues = palloc(svar->natts * sizeof(Datum));
+		inisnull = palloc(svar->natts * sizeof(bool));
+
+		outvalues = palloc(target_svt->natts * sizeof(Datum));
+		outisnull = palloc(target_svt->natts * sizeof(bool));
+
+		heap_deform_tuple(&tmptup, svar->tupdesc, invalues, inisnull);
+
+		for (i = 0; i < target_svt->natts; i++)
+		{
+			if (i >= svar->natts)
+				outisnull[i] = true;
+
+			else if (svar->attrs[i].isdropped || target_svt->attrs[i].isdropped)
+				outisnull[i] = true;
+
+			else
+			{
+				outvalues[i] = invalues[i];
+				outisnull[i] = inisnull[i];
+			}
+		}
+
+		tuple = heap_form_tuple(outtupdesc, outvalues, outisnull);
+		value = heap_copy_tuple_as_datum(tuple, outtupdesc);
+
+		heap_freetuple(tuple);
+
+		ReleaseTupleDesc(outtupdesc);
+
+		pfree(invalues);
+		pfree(inisnull);
+		pfree(outvalues);
+		pfree(outisnull);
+	}
+
+	elog(DEBUG1, "returned value of record type's session variable \"%s.%s\" was converted",
+				 svar->nsname, svar->name);
+
+	return value;
+}
+
 /*
  * Search the given session variable in the hash table. If it doesn't
  * exist, then insert it (and calculate defexpr if it exists).
@@ -663,7 +1538,7 @@ init_session_variable(SVariable svar, Variable *var)
  * related session variable until the end of the transaction.
  */
 static SVariable
-prepare_variable_for_reading(Oid varid)
+prepare_variable_for_reading(Oid varid, Datum *value, bool *isnull)
 {
 	SVariable svar;
 	Variable	var;
@@ -672,7 +1547,7 @@ prepare_variable_for_reading(Oid varid)
 	var.oid = InvalidOid;
 
 	if (!sessionvars)
-		create_sessionvars_hashtable();
+		create_sessionvars_hashtables();
 
 	/* Protect used session variable against drop until transaction end */
 	LockDatabaseObject(VariableRelationId, varid, 0, AccessShareLock);
@@ -684,54 +1559,120 @@ prepare_variable_for_reading(Oid varid)
 										HASH_ENTER, &found);
 
 	/* Return content if it is available and valid */
-	if (found && svar->is_valid)
-		return svar;
+	if (!found || !svar->is_valid)
+	{
+		/* We need to load defexpr. */
+		initVariable(&var, varid, false);
 
-	/* We need to load defexpr. */
-	initVariable(&var, varid, false);
+		if (!found)
+		{
+			init_session_variable(svar, &var);
 
-	if (!found)
-		init_session_variable(svar, &var);
+			elog(DEBUG1, "session variable \"%s.%s\" (oid:%u) has new entry in memory (emitted by READ)",
+						 svar->nsname, svar->name,
+						 varid);
+		}
 
-	/* Raise an error when we cannot initialize variable correctly */
-	if (var.is_not_null && !var.defexpr)
-		ereport(ERROR,
-				(errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
-				 errmsg("null value is not allowed for NOT NULL session variable \"%s.%s\"",
-						get_namespace_name(get_session_variable_namespace(varid)),
-						get_session_variable_name(varid)),
-				 errdetail("The session variable was not initialized yet.")));
+		/* Raise an error when we cannot initialize variable correctly */
+		if (var.is_not_null && !var.defexpr)
+			ereport(ERROR,
+					(errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
+						 errmsg("null value is not allowed for NOT NULL session variable \"%s.%s\"",
+							get_namespace_name(get_session_variable_namespace(varid)),
+							get_session_variable_name(varid)),
+					 errdetail("The session variable was not initialized yet.")));
 
-	if (svar->has_defexpr)
-	{
-		Datum		value = (Datum) 0;
-		bool		isnull;
-		EState	   *estate = NULL;
-		Expr	   *defexpr;
-		ExprState  *defexprs;
-		MemoryContext oldcxt;
+		if (svar->has_defexpr)
+		{
+			Datum		value = (Datum) 0;
+			bool		isnull;
+			EState	   *estate = NULL;
+			Expr	   *defexpr;
+			ExprState  *defexprs;
+			MemoryContext oldcxt;
 
-		/* Prepare default expr */
-		estate = CreateExecutorState();
+			/* Prepare default expr */
+			estate = CreateExecutorState();
 
-		oldcxt = MemoryContextSwitchTo(estate->es_query_cxt);
+			oldcxt = MemoryContextSwitchTo(estate->es_query_cxt);
 
-		defexpr = expression_planner((Expr *) var.defexpr);
-		defexprs = ExecInitExpr(defexpr, NULL);
-		value = ExecEvalExprSwitchContext(defexprs,
-										  GetPerTupleExprContext(estate),
-										  &isnull);
+			defexpr = expression_planner((Expr *) var.defexpr);
+			defexprs = ExecInitExpr(defexpr, NULL);
+			value = ExecEvalExprSwitchContext(defexprs,
+											  GetPerTupleExprContext(estate),
+											  &isnull);

-		/* Store result before releasing Executor memory */
-		set_session_variable(svar, value, isnull, svar->typid, true);
+			/* Store result before releasing Executor memory */
+			set_session_variable(svar, value, isnull, svar->svartype->typid, true);
 
-		MemoryContextSwitchTo(oldcxt);
+			MemoryContextSwitchTo(oldcxt);
 
-		FreeExecutorState(estate);
+			FreeExecutorState(estate);
+		}
+		else
+			set_session_variable(svar, (Datum) 0, true, svar->svartype->typid, true);
+	}
+
+	*value = svar->value;
+	*isnull = svar->isnull;
+
+	/*
+	 * Check if stored value has still valid type.
+	 */
+	if (!session_variable_use_valid_type(svar))
+	{
+		/*
+		 * We can try to convert composite value to target type. We don't
+		 * try to save converted value, because an change of target composite
+		 * type can be aborted, and still the life cycle of session variable
+		 * is short.
+		 */
+		if (svar->natts > 0 && svar->svartype->natts > 0 &&
+			svar->relid == svar->svartype->relid)
+		{
+			*value = get_rowtype_value_in_expected_format(svar);
+		}
+		else
+		{
+			ereport(ERROR,
+					(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+					 errmsg("format of stored value of \"%s.%s\" session variable can be outdated",
+							get_namespace_name(get_session_variable_namespace(varid)),
+							get_session_variable_name(varid))));
+		}
+	}
+
+	/*
+	 * Although the value of domain type should be valid (it is
+	 * checked when it is assigned to session variable), we have to
+	 * check related constraints anytime. It can be more expensive
+	 * than in PL/pgSQL. PL/pgSQL forces domain checks when value
+	 * is assigned to the variable or when value is returned from
+	 * function. Fortunately, domain types manage cache of constraints by
+	 * self.
+	 */
+	if (svar->svartype->is_domain)
+	{
+		MemoryContext oldcxt = CurrentMemoryContext;
+
+		/*
+		 * Store domain_check extra in CurTransactionContext. When we are
+		 * in other transaction, the domain_check_extra cache is not valid.
+		 */
+		if (svar->svartype->domain_check_extra_lxid != MyProc->lxid)
+			svar->svartype->domain_check_extra = NULL;
+
+		domain_check(svar->value,
+					 svar->isnull,
+					 svar->svartype->typid,
+					 &svar->svartype->domain_check_extra,
+					 CurTransactionContext);
+
+		svar->svartype->domain_check_extra_lxid = MyProc->lxid;
+
+		MemoryContextSwitchTo(oldcxt);
 	}
-	else
-		set_session_variable(svar, (Datum) 0, true, svar->typid, true);
 
 	return svar;
 }
@@ -755,7 +1696,7 @@ SetSessionVariable(Oid varid, Datum value, bool isNull, Oid typid)
 	LockDatabaseObject(VariableRelationId, varid, 0, AccessShareLock);
 
 	if (!sessionvars)
-		create_sessionvars_hashtable();
+		create_sessionvars_hashtables();
 
 	/* Ensure so all entries in sessionvars hash table are valid */
 	sync_sessionvars_all();
@@ -804,18 +1745,21 @@ SetSessionVariableWithSecurityCheck(Oid varid, Datum value, bool isNull, Oid typ
 Datum
 CopySessionVariable(Oid varid, bool *isNull, Oid *typid)
 {
-	SVariable svar;
+	SVariable	svar;
+	Datum		result = (Datum) 0;
 
-	svar = prepare_variable_for_reading(varid);
+	svar = prepare_variable_for_reading(varid, &result, isNull);
 	Assert(svar != NULL && svar->is_valid);
 
-	*isNull = svar->isnull;
-	*typid = svar->typid;
+	*typid = svar->svartype->typid;
 
-	if (!svar->isnull)
-		return datumCopy(svar->value, svar->typbyval, svar->typlen);
+	/* force copy of not null (not yet copy) value */
+	if (!isNull && svar->value == result)
+		result = datumCopy(svar->value,
+						   svar->svartype->typbyval,
+						   svar->svartype->typlen);
 
-	return (Datum) 0;
+	return (Datum) result;
 }
 
 /*
@@ -826,22 +1770,25 @@ CopySessionVariable(Oid varid, bool *isNull, Oid *typid)
 Datum
 CopySessionVariableWithTypeCheck(Oid varid, bool *isNull, Oid expected_typid)
 {
-	SVariable svar;
+	SVariable	svar;
+	Datum		result = (Datum) 0;
 
-	svar = prepare_variable_for_reading(varid);
+	svar = prepare_variable_for_reading(varid, &result, isNull);
 	Assert(svar != NULL && svar->is_valid);
 
-	if (expected_typid != svar->typid)
+	if (expected_typid != svar->svartype->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));
 
 	*isNull = svar->isnull;
 
-	if (!svar->isnull)
-		return datumCopy(svar->value, svar->typbyval, svar->typlen);
+	if (!*isNull && svar->value == result )
+		result = datumCopy(svar->value,
+						 svar->svartype->typbyval,
+						 svar->svartype->typlen);
 
-	return (Datum) 0;
+	return (Datum) result;
 }
 
 /*
@@ -852,22 +1799,18 @@ CopySessionVariableWithTypeCheck(Oid varid, bool *isNull, Oid expected_typid)
 Datum
 GetSessionVariableWithTypeCheck(Oid varid, bool *isNull, Oid expected_typid)
 {
-	SVariable svar;
+	SVariable	svar;
+	Datum		result = (Datum) 0;
 
-	svar = prepare_variable_for_reading(varid);
+	svar = prepare_variable_for_reading(varid, &result, isNull);
 	Assert(svar != NULL && svar->is_valid);
 
-	if (expected_typid != svar->typid)
+	if (expected_typid != svar->svartype->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));
 
-	*isNull = svar->isnull;
-
-	if (svar->isnull)
-		return (Datum) 0;
-
-	return svar->value;
+	return result;
 }
 
 /*
@@ -1070,6 +2013,9 @@ ResetSessionVariables(void)
 	{
 		hash_destroy(sessionvars);
 		sessionvars = NULL;
+
+		hash_destroy(sessionvars_types);
+		sessionvars_types = NULL;
 	}
 
 	/* Release memory allocated by session variables */
@@ -1371,6 +2317,8 @@ pg_debug_show_used_session_variables(PG_FUNCTION_ARGS)
 
 		while ((svar = (SVariable) hash_seq_search(&status)) != NULL)
 		{
+			char	   *name;
+			char	   *nsname;
 			Datum		values[NUM_PG_DEBUG_SHOW_USED_SESSION_VARIABLES_ATTS];
 			bool		nulls[NUM_PG_DEBUG_SHOW_USED_SESSION_VARIABLES_ATTS];
 			HeapTuple	tp;
-- 
2.37.2

From 57c4e8869d78d6e17bee8157bac7919f8ced084b Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Mon, 11 Apr 2022 20:32:05 +0200
Subject: [PATCH 02/12] session variables

Implementation storage and access routines. Session variables are stored
in session memory inside dedicated hash table. Two levels of an access
are implemented: API level and SQL level. Both levels are implemented
in this commit.

The most difficult part is cleaning (reset) of session variable. The
content of session variable should be cleaned when related session
variable is removed from system catalog. But queue of sinval messages
can be truncated, or current transaction state can disallow an access
to system catalog, so cleaning can be postponed.
---
 src/backend/access/transam/xact.c      |    2 +-
 src/backend/catalog/dependency.c       |    5 +
 src/backend/commands/sessionvariable.c | 1145 +++++++++++++++++++++++-
 src/backend/executor/execExpr.c        |   75 ++
 src/backend/executor/execExprInterp.c  |   11 +
 src/backend/executor/execMain.c        |   56 ++
 src/backend/executor/execParallel.c    |  148 ++-
 src/backend/jit/llvm/llvmjit_expr.c    |    6 +
 src/backend/optimizer/plan/planner.c   |    8 +
 src/backend/optimizer/plan/setrefs.c   |  118 ++-
 src/backend/optimizer/util/clauses.c   |   75 +-
 src/backend/parser/analyze.c           |   13 +-
 src/backend/parser/parse_expr.c        |  202 ++++-
 src/backend/tcop/pquery.c              |    3 +
 src/backend/utils/adt/ruleutils.c      |   46 +
 src/backend/utils/cache/plancache.c    |   19 +-
 src/backend/utils/fmgr/fmgr.c          |   10 +-
 src/backend/utils/misc/guc.c           |   12 +
 src/include/catalog/pg_proc.dat        |    7 +
 src/include/executor/execExpr.h        |   10 +
 src/include/executor/execdesc.h        |    4 +
 src/include/nodes/execnodes.h          |   19 +
 src/include/nodes/parsenodes.h         |    2 +
 src/include/nodes/pathnodes.h          |    4 +
 src/include/nodes/plannodes.h          |    2 +
 src/include/nodes/primnodes.h          |   12 +-
 src/include/optimizer/planmain.h       |    2 +
 src/include/parser/parse_expr.h        |    1 +
 src/include/parser/parse_node.h        |    1 +
 29 files changed, 1956 insertions(+), 62 deletions(-)

diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c
index e72fd74145..355f62ae0f 100644
--- a/src/backend/access/transam/xact.c
+++ b/src/backend/access/transam/xact.c
@@ -2214,7 +2214,7 @@ CommitTransaction(void)
 	 */
 	smgrDoPendingSyncs(true, is_parallel_worker);
 
-	/* Let ON COMMIT DROP */
+	/* Let ON COMMIT DROP or ON TRANSACTION END */
 	AtPreEOXact_SessionVariable_on_xact_actions(true);
 
 	/* close large objects before lower-level cleanup */
diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c
index 6255c2c72f..2b70faa173 100644
--- a/src/backend/catalog/dependency.c
+++ b/src/backend/catalog/dependency.c
@@ -1909,6 +1909,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/commands/sessionvariable.c b/src/backend/commands/sessionvariable.c
index 3094bab82a..1d1b9747e5 100644
--- a/src/backend/commands/sessionvariable.c
+++ b/src/backend/commands/sessionvariable.c
@@ -23,22 +23,138 @@
 #include "catalog/pg_variable.h"
 #include "commands/session_variable.h"
 #include "executor/executor.h"
+#include "executor/svariableReceiver.h"
+#include "funcapi.h"
 #include "optimizer/optimizer.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
 #include "parser/parse_type.h"
+#include "rewrite/rewriteHandler.h"
+#include "storage/lmgr.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"
 
 /*
- * The life cycle of temporary session variable can be
- * limmited by using clause ON COMMIT DROP.
+ * Values of session variables are stored in local memory, in
+ * sessionvars hash table in binary format and the life cycle of
+ * session variable can be longer than transaction. Then we
+ * need to solve following issues:
+ *
+ * - We need to purge memory when variable is dropped (in current
+ *  transaction in current session or by other sessions (other
+ *  users). There is a request to protect content against
+ *  possibly aborted the DROP VARIABLE command. So the purging should
+ *  not be executed immediately. It should be postponed until the end
+ *  of the transaction.
+ *
+ * - The session variable can be dropped explicitly (by DROP VARIABLE)
+ *  command or implicitly (by using ON COMMIT DROP clause). The
+ *  purging memory at transaction end can be requested implicitly
+ *  too (by using the ON TRANSACTION END clause).
+ *
+ * - We need to ensure that the returned value is valid. The stored
+ *  value can be invalid from different reasons:
+ *
+ *  a) format change - we doesn't support change of format for
+ *   session variable, but format can be changed by ALTER TYPE
+ *   operation or by owner's extension update. We store fingerprint
+ *   of any type used by session variables, and before any
+ *   read we check if this fingerprint is still valid (see
+ *   SVariableType related operations). Built-in non composite
+ *   types are trusty every time.
+ *
+ *   For other cases we can check xmin, and if xmin is different,
+ *   then we check the name. For types from extension we
+ *   can check xmin of pg_extension related record (and if it
+ *   is different we can check extension's name and extension's
+ *   version). For domains we should to force cast to domain type
+ *   to reply all constraints before returning the value.
+ *
+ *  b) the variable can be dropped - we don't purge memory
+ *   when the variable is dropped immediately, so theoretically
+ *   we can return dropped value. The protection against this
+ *   issue is based on invisibility of dropped session variables
+ *   in system catalog. The DROP commands invalidates related
+ *   cached plans too, so it should work with prepared statements.
+ *   Theoretically there is possible access to value of dropped
+ *   variable by using known oid, but we don't try to solve this
+ *   issue now (it is possible only when session variables API
+ *   is used from extensions). The implementation watches sinval
+ *   messages, and in message processing check the consistency
+ *   of stored values in hash table against system catalog.
+ *   This check should be done after end of any transaction, but
+ *   because for this check we need not aborted state, the check
+ *   can be postponed to next transaction (when the variable
+ *   will be used). Unfortunately, this check can be postponed
+ *   to the future and between there can be oid overflow. We
+ *   cannot to detect oid overflow, and then theoretically there is
+ *   the possibility to find in catalog a variable with same
+ *   oid like oid of dropped value. But as protection against
+ *   returning invalid value we can (and we do) check the used
+ *   type, and when the type is not valid, then we don't allow
+ *   returns any value. We cannot to check name or schema, because
+ *   we suppport ALTER RENAME and ALTER SET SCHEMA. So, although
+ *   there is an risk to return value of dropped variable (instead
+ *   of new calculated default expr or NULL), there should not
+ *   be risk of crash due to invalid format.
+ *
+ * The values of session variables are stored in hash table
+ * sessionvars, the fingerprints of used types are stored in
+ * hash table sessionvars_types. We maintain four queues
+ * (implemented as list of variable oid, list of RecheckVariableRequests
+ * (requests are created in reaction on sinval) and lists
+ * of actions). List of actions are used whe we need to calculate
+ * the final transaction state of an entry's creator's
+ * subtransaction. List of session variables are used, when we
+ * don't need to do this calculation (we know, so. every values
+ * of session variables created with clauses ON COMMIT DROP or
+ * ON TRANSACTION END RESET will be purged from memory), but
+ * ON COMMIT DROP action or ON COMMIT RESET action can be done only
+ * when related sub transaction is committed (we don't want to lost
+ * value after aborted DROP VARIABLE command, we don't want to delete
+ * record from system catalog after aborted CREATE VARIABLE command,
+ * or we don't want to drop record from system catalog implicitly
+ * when TEMP ON COMMIT DROP variable was successfully dropped).
+ * Recheck of session's variable existence should be executed
+ * only when some operation over session's variable is executed
+ * first time in transaction or at the end of transaction. It
+ * should not be executed more times inside transaction.
+ *
+ * Although the variable's reset doesn't need full delete from
+ * sessionvars hash table, we do. It is the most simple solution,
+ * and it reduces possible state's space (and reduces sessionvars
+ * bloating).
+ *
+ * There are two different ways to do final access to session
+ * variables: buffered (indirect) or direct. Buffered access is used
+ * in queries, where we have to ensure an stability of passed values
+ * (then the session variable has same behaviour like external query
+ * parameters, what is consistent with using PL/pgSQL's variables in
+ * embedded queries in PL/pgSQL).
+ *
+ * This is implemented by using an aux buffer (an array) that holds a
+ * copy of values of used (in query) session variables. In the final
+ * end, the values from this array are passed as constant (EEOP_CONST).
+ *
+ * Direct access is used by simple expression evaluation (PLpgSQL).
+ * In this case we don't need to ensure the stability of passed
+ * values, and maintaining the buffer with copies of values of session
+ * variables can be useless overhead. In this case we just read the
+ * value of the session variable directly (EEOP_PARAM_VARIABLE). This
+ * strategy removes the necessity to modify related PL/pgSQL code to
+ * support session variables (the reading of session variables is
+ * fully transparent for PL/pgSQL).
  */
 typedef enum SVariableXActAction
 {
 	SVAR_ON_COMMIT_DROP,		/* used for ON COMMIT DROP */
+	SVAR_ON_COMMIT_RESET,		/* used for DROP VARIABLE */
 } SVariableXActAction;
 
 typedef struct SVariableXActActionItem
@@ -54,12 +170,706 @@ typedef struct SVariableXActActionItem
 	SubTransactionId deleting_subid;
 }  SVariableXActActionItem;
 
-/* List holds fields of SVariableXActActionItem type */
-static List *xact_drop_actions = NIL;
+/* Both lists hold fields of SVariableXActActionItem type */
+static List *xact_on_commit_drop_actions = NIL;
+static List *xact_on_commit_reset_actions = NIL;
+
+/*
+ * the ON COMMIT DROP and ON TRANSACTION END RESET variables
+ * are purged from memory every time.
+ */
+static List *xact_reset_varids = NIL;
+
+/*
+ * When the session variable is dropped we need to purge memory. The
+ * session variable can be dropped by current session, but it can be
+ * dropped by other's sessions too, so we have to watc sinval message.
+ * But because we don't want to purge memory immediately, we need to
+ * hold list of possibly dropped session variables and at the end of
+ * transaction, we check session variables from this list against system
+ * catalog. This check can be postponed into next transaction if
+ * current transactions is in aborted state. When the check is postponed
+ * to the next transaction, then have to be executed just once time, and
+ * the fields of list created in current transaction have to be ignored
+ * (the sinval message can come first before sync_sessionvars_all call,
+ * and then we have to skip items created by current transaction, because
+ * we want to variables when the related operation (that emmits sinval)
+ * is in final state (at the end of related transaction).
+ * Saved lxid in synced_lxid is safeguard against repeated useles
+ * recheck inside one transaction.
+ */
+typedef struct RecheckVariableRequest
+{
+	Oid			varid;
+	LocalTransactionId lxid;
+} RecheckVariableRequest;
+
+static List *xact_recheck_requests = NIL;
+
+static LocalTransactionId synced_lxid = InvalidLocalTransactionId;
+
+typedef struct SVariableData
+{
+	Oid			varid;			/* pg_variable OID of this sequence (hash key) */
+
+	char	   *name;		/* for debug purposes */
+	char	   *nsname;		/* for debug purposes */
+
+	SVariableType svartype;
+	int64		gennum;
+
+	bool		isnull;
+	bool		freeval;
+	Datum		value;
+
+	bool		is_rowtype;		/* true when variable is composite */
+	bool		is_not_null;	/* don't allow null values */
+	bool		is_immutable;	/* true when variable is immutable */
+	bool		has_defexpr;	/* true when variable has a default value */
+
+	bool		is_valid;		/* true when variable was successfully
+								 * initialized */
+
+	uint32		hashvalue;
+
+	bool		eox_reset;		/* true, when lifecycle is limitted by transaction */
+}			SVariableData;
+
+typedef SVariableData * SVariable;
+
+static HTAB *sessionvars = NULL;	/* hash table for session variables */
+static MemoryContext SVariableMemoryContext = NULL;
+
+static bool first_time = true;
+
 
 static void register_session_variable_xact_action(Oid varid, SVariableXActAction action);
 static void unregister_session_variable_xact_action(Oid varid, SVariableXActAction action);
 
+/*
+ * Returns human readable name of SVariableXActAction value.
+ */
+static const char *
+SvariableXActActionName(SVariableXActAction action)
+{
+	switch (action)
+	{
+		case SVAR_ON_COMMIT_DROP:
+			return "ON COMMIT DROP";
+		case SVAR_ON_COMMIT_RESET:
+			return "ON COMMIT RESET";
+		default:
+			elog(ERROR, "unknown SVariableXActAction action %d",
+						action);
+	}
+}
+
+/*
+ * Releases stored data from session variable, but preserve the hash entry
+ * in sessionvars.
+ */
+static void
+free_session_variable_value(SVariable svar)
+{
+	if (svar->freeval)
+		pfree(DatumGetPointer(svar->value));
+
+	/* Clean current value */
+	svar->value = (Datum) 0;
+	svar->isnull = true;
+	svar->freeval = false;
+
+	/*
+	 * We can mark this session variable as valid when
+	 * it has not default expression, and when null is
+	 * allowed. When it has defexpr, then the content
+	 * will be valid after an assignment or defexp evaluation.
+	 */
+	svar->is_valid = !svar->has_defexpr && !svar->is_not_null;
+}
+
+/*
+ * Release the variable defined by varid from sessionvars
+ * hashtab.
+ */
+static void
+remove_session_variable(SVariable svar)
+{
+	free_session_variable_value(svar);
+
+	elog(DEBUG1, "session variable \"%s.%s\" (oid:%u) is removing from memory",
+				 svar->nsname, svar->name,
+				 svar->varid);
+
+	if (hash_search(sessionvars,
+					(void *) &svar->varid,
+					HASH_REMOVE,
+					NULL) == NULL)
+		elog(DEBUG1, "hash table corrupted");
+}
+
+/*
+ * Release the variable defined by varid from sessionvars
+ * hashtab.
+ */
+static void
+remove_session_variable_by_id(Oid varid)
+{
+	SVariable svar;
+	bool		found;
+
+	if (!sessionvars)
+		return;
+
+	svar = (SVariable) hash_search(sessionvars, &varid,
+										HASH_FIND, &found);
+	if (found)
+		remove_session_variable(svar);
+}
+
+/*
+ * Callback function for session variable invalidation.
+ */
+static void
+pg_variable_cache_callback(Datum arg, int cacheid, uint32 hashvalue)
+{
+	HASH_SEQ_STATUS status;
+	SVariable svar;
+
+	/*
+	 * There is no guarantee of sessionvars being initialized, even when
+	 * receiving an invalidation callback, as DISCARD [ ALL | VARIABLES ]
+	 * destroys the hash table entirely.
+	 */
+	if (!sessionvars)
+		return;
+
+	/*
+	 * 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 have to iterate over all
+	 * items of hash table.
+	 */
+	hash_seq_init(&status, sessionvars);
+
+	while ((svar = (SVariable) hash_seq_search(&status)) != NULL)
+	{
+		if (hashvalue == 0 || svar->hashvalue == hashvalue)
+		{
+			/*
+			 * We don't need to execute recheck for variables,
+			 * that will be surelly purged from memory at eox
+			 * time.
+			 */
+			if (!svar->eox_reset)
+			{
+				RecheckVariableRequest *req = NULL;
+				ListCell *l;
+
+				/*
+				 * The recheck request should be postponed to the end of current
+				 * transaction if there are request recheck from previous
+				 * transaction. This ensure so we don't purge memory too
+				 * early, when we process sinval from previous aborted
+				 * transaction. Possible test case:
+				 *
+				 * LET var = X;
+				 * BEGIN DROP VAR var; ROOLBACK;
+				 * BEGIN DROP VAR var ROLLBACK;
+				 * SELECT var; -- should be X
+				 *
+				 * Without postponing, the call of sync_sessionvars_all
+				 * in second transaction purge (badly) memory, because
+				 * sinval message from prrevious aborted transaction is
+				 * processed and in this time related record from pg_variable
+				 * is removed. So when we find request recheck from prev
+				 * transaction, we just update lxid.
+				 */
+				foreach (l, xact_recheck_requests)
+				{
+					req = (RecheckVariableRequest *) lfirst(l);
+					if (req->varid == svar->varid)
+					{
+						if (req->lxid != MyProc->lxid)
+						{
+							req->lxid = MyProc->lxid;
+
+							elog(DEBUG1, "session variable \"%s.%s\" (oid:%u) should be rechecked (forced by sinval)"
+										 " (found recheck request from previous transaction)",
+									 svar->nsname, svar->name,
+									 svar->varid);
+
+							break;
+						}
+					}
+				}
+
+				if (!req)
+				{
+					MemoryContext oldcxt;
+
+					oldcxt = MemoryContextSwitchTo(CacheMemoryContext);
+
+					req = (RecheckVariableRequest *) palloc(sizeof(RecheckVariableRequest));
+
+					req->varid = svar->varid;
+					req->lxid = MyProc->lxid;
+
+					elog(DEBUG1, "session variable \"%s.%s\" (oid:%u) should be rechecked (forced by sinval)",
+								 svar->nsname, svar->name,
+								 svar->varid);
+
+					xact_recheck_requests = lappend(xact_recheck_requests, req);
+
+					MemoryContextSwitchTo(oldcxt);
+				}
+			}
+		}
+
+		/*
+		 * although it there is low probability, we have to iterate
+		 * over all actively used session variables, because hashvalue
+		 * is not unique identifier.
+		 */
+	}
+}
+
+/*
+ * When we need to recheck all session variables, then
+ * the most effective method is seq scan over hash tab.
+ * We need to check synchronization request before any
+ * read to be sure so returned data are valid.
+ */
+static void
+sync_sessionvars_all()
+{
+	SVariable svar;
+	ListCell   *l;
+
+	if (synced_lxid == MyProc->lxid)
+		return;
+
+	if (!xact_recheck_requests)
+		return;
+
+	/*
+	 * sessionvars is null after DISCARD VARIABLES.
+	 * When we are sure, so there are not any
+	 * active session variable in this session.
+	 */
+	if (!sessionvars)
+	{
+		list_free_deep(xact_recheck_requests);
+		xact_recheck_requests = NIL;
+		return;
+	}
+
+	/*
+	 * This routine is called before any reading. So the
+	 * session should be in transaction state. This is required
+	 * for access to system catalog.
+	 */
+	Assert(IsTransactionState());
+
+	foreach(l, xact_recheck_requests)
+	{
+		RecheckVariableRequest *req;
+		bool	found;
+
+		req = (RecheckVariableRequest *) lfirst(l);
+
+		if (req->lxid != MyProc->lxid)
+		{
+			svar = (SVariable) hash_search(sessionvars, &req->varid,
+											HASH_FIND, &found);
+
+			if (found)
+			{
+				HeapTuple	tp;
+
+				tp = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(req->varid));
+
+				if (HeapTupleIsValid(tp))
+					ReleaseSysCache(tp);
+				else
+					remove_session_variable(svar);
+			}
+
+			xact_recheck_requests = foreach_delete_current(xact_recheck_requests, l);
+			pfree(req);
+		}
+	}
+
+	/* Don't repeat this check in this transaction more time */
+	synced_lxid = MyProc->lxid;
+}
+
+/*
+ * Create the hash table for storing session variables
+ */
+static void
+create_sessionvars_hashtable(void)
+{
+	HASHCTL		ctl;
+
+	/* set callbacks */
+	if (first_time)
+	{
+		/* Read sinval messages */
+		CacheRegisterSyscacheCallback(VARIABLEOID,
+									  pg_variable_cache_callback,
+									  (Datum) 0);
+		/* needs its own long lived memory context */
+		SVariableMemoryContext =
+			AllocSetContextCreate(TopMemoryContext,
+								  "session variables",
+								  ALLOCSET_START_SMALL_SIZES);
+
+		first_time = false;
+	}
+
+	Assert(SVariableMemoryContext);
+
+	memset(&ctl, 0, sizeof(ctl));
+	ctl.keysize = sizeof(Oid);
+	ctl.entrysize = sizeof(SVariableData);
+	ctl.hcxt = SVariableMemoryContext;
+
+	Assert(sessionvars == NULL);
+
+	sessionvars = hash_create("Session variables", 64, &ctl,
+								   HASH_ELEM | HASH_BLOBS | HASH_CONTEXT);
+}
+
+/*
+ * Assign some content to the session variable. It's copied to
+ * SVariableMemoryContext if necessary.
+ *
+ * init_mode is true, when the value of session variable is being initialized
+ * by default expression or by null. Only in this moment we can allow to
+ * modify immutable variables with default expression.
+ */
+static void
+set_session_variable(SVariable svar, Datum value,
+					 bool isnull, Oid typid,
+					 bool init_mode)
+{
+	MemoryContext oldcxt;
+	Datum		newval = value;
+
+	/* Don't allow assignment of null to NOT NULL variable */
+	if (isnull && svar->is_not_null)
+		ereport(ERROR,
+				(errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
+				 errmsg("null value is not allowed for NOT NULL session variable \"%s.%s\"",
+						get_namespace_name(get_session_variable_namespace(svar->varid)),
+						get_session_variable_name(svar->varid))));
+
+	if (svar->typid != typid)
+		ereport(ERROR,
+				(errcode(ERRCODE_DATATYPE_MISMATCH),
+				 errmsg("type \"%s\" of assigned value is different than type \"%s\" of session variable \"%s.%s\"",
+						format_type_be(typid),
+						format_type_be(svar->typid),
+						get_namespace_name(get_session_variable_namespace(svar->varid)),
+						get_session_variable_name(svar->varid))));
+
+	/*
+	 * Don't allow updating of immutable session variable that has assigned
+	 * not null value or has default expression (and then the value should be
+	 * result of default expression always). Don't do this check, when variable
+	 * is being initialized.
+	 */
+	if (!init_mode &&
+		(svar->is_immutable && (svar->is_valid || svar->has_defexpr)))
+		ereport(ERROR,
+				(errcode(ERRCODE_ERROR_IN_ASSIGNMENT),
+				 errmsg("session variable \"%s.%s\" is declared IMMUTABLE",
+						get_namespace_name(get_session_variable_namespace(svar->varid)),
+						get_session_variable_name(svar->varid))));
+
+	/* copy value to session persistent context */
+	oldcxt = MemoryContextSwitchTo(SVariableMemoryContext);
+	if (!isnull)
+		newval = datumCopy(value, svar->typbyval, svar->typlen);
+	MemoryContextSwitchTo(oldcxt);
+
+	free_session_variable_value(svar);
+
+	svar->value = newval;
+	svar->isnull = isnull;
+	svar->freeval = newval != value;
+	svar->is_valid = true;
+}
+
+/*
+ * Initialize svar from var
+ * svar - SVariable - holds value
+ * var  - Variable - holds metadata
+ */
+static void
+init_session_variable(SVariable svar, Variable *var)
+{
+	MemoryContext oldcxt = MemoryContextSwitchTo(CacheMemoryContext);
+
+	Assert(OidIsValid(var->oid));
+
+	svar->varid = var->oid;
+
+	svar->name = pstrdup(var->varname);
+	svar->nsname = get_namespace_name(var->varnamespace);
+
+	svar->svartype = get_svariabletype(var->typid);
+	svar->gennum = get_svariable_valid_type_gennum(svar->svartype);
+
+	svar->isnull = true;
+	svar->freeval = false;
+	svar->value = (Datum) 0;
+
+	svar->is_rowtype = type_is_rowtype(var->typid);
+	svar->is_not_null = var->is_not_null;
+	svar->is_immutable = var->is_immutable;
+	svar->has_defexpr = var->has_defexpr;
+
+	svar->hashvalue = GetSysCacheHashValue1(VARIABLEOID,
+											ObjectIdGetDatum(var->oid));
+
+	/* the value of variable is not known yet */
+	svar->is_valid = false;
+
+	svar->eox_reset = var->eoxaction == VARIABLE_EOX_RESET ||
+					  var->eoxaction == VARIABLE_EOX_DROP;
+
+	if (svar->eox_reset)
+	{
+
+		MemoryContext oldcxt = MemoryContextSwitchTo(CacheMemoryContext);
+
+		xact_reset_varids = lappend_oid(xact_reset_varids, var->oid);
+
+		MemoryContextSwitchTo(oldcxt);
+	}
+
+	MemoryContextSwitchTo(oldcxt);
+}
+
+/*
+ * Search the given session variable in the hash table. If it doesn't
+ * exist, then insert it (and calculate defexpr if it exists).
+ *
+ * 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
+prepare_variable_for_reading(Oid varid)
+{
+	SVariable svar;
+	Variable	var;
+	bool		found;
+
+	var.oid = InvalidOid;
+
+	if (!sessionvars)
+		create_sessionvars_hashtable();
+
+	/* Protect used session variable against drop until transaction end */
+	LockDatabaseObject(VariableRelationId, varid, 0, AccessShareLock);
+
+	/* Ensure so all entries in sessionvars hash table are valid */
+	sync_sessionvars_all();
+
+	svar = (SVariable) hash_search(sessionvars, &varid,
+										HASH_ENTER, &found);
+
+	/* Return content if it is available and valid */
+	if (found && svar->is_valid)
+		return svar;
+
+	/* We need to load defexpr. */
+	initVariable(&var, varid, false);
+
+	if (!found)
+		init_session_variable(svar, &var);
+
+	/* Raise an error when we cannot initialize variable correctly */
+	if (var.is_not_null && !var.defexpr)
+		ereport(ERROR,
+				(errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
+				 errmsg("null value is not allowed for NOT NULL session variable \"%s.%s\"",
+						get_namespace_name(get_session_variable_namespace(varid)),
+						get_session_variable_name(varid)),
+				 errdetail("The session variable was not initialized yet.")));
+
+	if (svar->has_defexpr)
+	{
+		Datum		value = (Datum) 0;
+		bool		isnull;
+		EState	   *estate = NULL;
+		Expr	   *defexpr;
+		ExprState  *defexprs;
+		MemoryContext oldcxt;
+
+		/* Prepare default expr */
+		estate = CreateExecutorState();
+
+		oldcxt = MemoryContextSwitchTo(estate->es_query_cxt);
+
+		defexpr = expression_planner((Expr *) var.defexpr);
+		defexprs = ExecInitExpr(defexpr, NULL);
+		value = ExecEvalExprSwitchContext(defexprs,
+										  GetPerTupleExprContext(estate),
+										  &isnull);
+
+
+		/* Store result before releasing Executor memory */
+		set_session_variable(svar, value, isnull, svar->typid, true);
+
+		MemoryContextSwitchTo(oldcxt);
+
+		FreeExecutorState(estate);
+	}
+	else
+		set_session_variable(svar, (Datum) 0, true, svar->typid, true);
+
+	return svar;
+}
+
+/*
+ * Store the given value in an SVariable, and cache it if not already present.
+ *
+ * Caller is responsible for doing permission checks.
+ * We try not to break the previous value, if something is wrong.
+ *
+ * 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, Oid typid)
+{
+	SVariable svar;
+	bool		found;
+
+	/* Protect used session variable against drop until transaction end */
+	LockDatabaseObject(VariableRelationId, varid, 0, AccessShareLock);
+
+	if (!sessionvars)
+		create_sessionvars_hashtable();
+
+	/* Ensure so all entries in sessionvars hash table are valid */
+	sync_sessionvars_all();
+
+	svar = (SVariable) hash_search(sessionvars, &varid,
+										HASH_ENTER, &found);
+
+	if (!found)
+	{
+		Variable	var;
+
+		/* We don't need to know defexpr here */
+		initVariable(&var, varid, true);
+		init_session_variable(svar, &var);
+
+		elog(DEBUG1, "session variable \"%s.%s\" (oid:%u) has new entry in memory (emitted by WRITE)",
+					 svar->nsname, svar->name,
+					 varid);
+	}
+
+	set_session_variable(svar, value, isNull, typid, false);
+}
+
+/*
+ * Wrapper around SetSessionVariable after checking for correct permission.
+ */
+void
+SetSessionVariableWithSecurityCheck(Oid varid, Datum value, bool isNull, Oid typid)
+{
+	AclResult	aclresult;
+
+	/*
+	 * Is possible to write to session variable?
+	 */
+	aclresult = pg_variable_aclcheck(varid, GetUserId(), ACL_WRITE);
+	if (aclresult != ACLCHECK_OK)
+		aclcheck_error(aclresult, OBJECT_VARIABLE, get_session_variable_name(varid));
+
+	SetSessionVariable(varid, value, isNull, typid);
+}
+
+/*
+ * Returns a copy of value of the session variable specified by varid
+ * Caller is responsible for doing permission checks.
+ */
+Datum
+CopySessionVariable(Oid varid, bool *isNull, Oid *typid)
+{
+	SVariable svar;
+
+	svar = prepare_variable_for_reading(varid);
+	Assert(svar != NULL && svar->is_valid);
+
+	*isNull = svar->isnull;
+	*typid = svar->typid;
+
+	if (!svar->isnull)
+		return datumCopy(svar->value, svar->typbyval, svar->typlen);
+
+	return (Datum) 0;
+}
+
+/*
+ * Returns a copy of value of the session variable specified by varid
+ * with check of expected type. Like previous function, the caller
+ * is responsible for doing permission checks.
+ */
+Datum
+CopySessionVariableWithTypeCheck(Oid varid, bool *isNull, Oid expected_typid)
+{
+	SVariable svar;
+
+	svar = prepare_variable_for_reading(varid);
+	Assert(svar != NULL && 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));
+
+	*isNull = svar->isnull;
+
+	if (!svar->isnull)
+		return datumCopy(svar->value, svar->typbyval, svar->typlen);
+
+	return (Datum) 0;
+}
+
+/*
+ * Returns a value of session variable identified by varid with
+ * check of expected type. Like previous function, the called
+ * is reposible for doing permission check.
+ */
+Datum
+GetSessionVariableWithTypeCheck(Oid varid, bool *isNull, Oid expected_typid)
+{
+	SVariable svar;
+
+	svar = prepare_variable_for_reading(varid);
+	Assert(svar != NULL && 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));
+
+	*isNull = svar->isnull;
+
+	if (svar->isnull)
+		return (Datum) 0;
+
+	return svar->value;
+}
+
 /*
  * Routines used for manipulation with session variables from
  * SQL level
@@ -170,19 +980,24 @@ DefineSessionVariable(ParseState *pstate, CreateSessionVarStmt *stmt)
 }
 
 /*
- * Drop variable by OID. This routine doesn't try to remove
- * the value of session variable immediately. It will be
- * removed on transaction end in sync_sessionvars_xact_callback
- * routine. This routine manipulate just with system catalog.
+ * Drop variable by OID. This routine can be called by directly
+ * command DROP VARIABLE or indirectly for TEMP ON COMMIT DROP
+ * variables.
+ *
+ * In first case we want to postpone memory purging to commit or
+ * abort time. In second case, the menory will be purged by
+ * processing xact_reset_varids list. In second case, the calling
+ * of unregistration of SVAR_ON_COMMIT_DROP action is useless, but
+ * we are not able to detect which case is executed.
  */
 void
 RemoveSessionVariable(Oid varid)
 {
+	SVariable svar;
+	bool		found;
 	Relation	rel;
 	HeapTuple	tup;
 
-	elog(DEBUG1, "session variable (oid:%u) is deleted from pg_variable", varid);
-
 	rel = table_open(VariableRelationId, RowExclusiveLock);
 
 	tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid));
@@ -190,6 +1005,18 @@ RemoveSessionVariable(Oid varid)
 	if (!HeapTupleIsValid(tup))
 		elog(ERROR, "cache lookup failed for variable %u", varid);
 
+	if (message_level_is_interesting(DEBUG1))
+	{
+		Form_pg_variable varform;
+
+		varform = (Form_pg_variable) GETSTRUCT(tup);
+
+		elog(DEBUG1, "session variable \"%s.%s\" (oid:%u) is deleted from pg_variable",
+					 get_namespace_name(varform->varnamespace),
+					 NameStr(varform->varname),
+					 varid);
+	}
+
 	CatalogTupleDelete(rel, &tup->t_self);
 
 	ReleaseSysCache(tup);
@@ -197,17 +1024,81 @@ RemoveSessionVariable(Oid varid)
 	table_close(rel, RowExclusiveLock);
 
 	/*
-	 * We removed entry from catalog already, we should not do it
-	 * again at end of xact time.
+	 * We removed entry from catalog already, we must not do it
+	 * again at end of xact time
 	 */
 	unregister_session_variable_xact_action(varid, SVAR_ON_COMMIT_DROP);
+
+	if (sessionvars)
+	{
+		svar = (SVariable) hash_search(sessionvars, &varid,
+											HASH_FIND, &found);
+
+		/*
+		 * For variables that are not purged by default we need to
+		 * register SVAR_ON_COMMIT_RESET action. This action can
+		 * be reverted by ABORT of DROP VARIABLE command.
+		 */
+		if (found && !svar->eox_reset)
+		{
+			/*
+			 * And we want to enforce variable clearning when this transaction or
+			 * subtransaction will be committed (we don't need to wait for
+			 * sinval message). The cleaning action for one session variable
+			 * can be repeated in the action list without causing any problem,
+			 * so we don't need to ensure uniqueness. We need a different action
+			 * from RESET, because RESET is executed on any transaction end,
+			 * but we want to execute cleaning only when the current transaction
+			 * will be committed.
+			 */
+			register_session_variable_xact_action(varid, SVAR_ON_COMMIT_RESET);
+		}
+	}
+}
+
+/*
+ * Fast drop of the complete content of all session variables hash table.
+ * This is code for DISCARD VARIABLES command. This command
+ * cannot be run inside transaction, so we don't need to handle
+ * end of transaction actions.
+ */
+void
+ResetSessionVariables(void)
+{
+	/* Destroy hash table and reset related memory context */
+	if (sessionvars)
+	{
+		hash_destroy(sessionvars);
+		sessionvars = NULL;
+	}
+
+	/* Release memory allocated by session variables */
+	if (SVariableMemoryContext != NULL)
+		MemoryContextReset(SVariableMemoryContext);
+
+	/*
+	 * There are not any session variables left, so simply trim
+	 * both xact action lists.
+	 */
+	list_free_deep(xact_on_commit_drop_actions);
+	xact_on_commit_drop_actions = NIL;
+
+	list_free_deep(xact_on_commit_reset_actions);
+	xact_on_commit_reset_actions = NIL;
+
+	/* We should clean xact_reset_varids */
+	list_free(xact_reset_varids);
+	xact_reset_varids = NIL;
+
+	/* we should clean xact_recheck_requests */
+	list_free_deep(xact_recheck_requests);
+	xact_recheck_requests = NIL;
 }
 
 /*
  * Registration of actions to be executed on session variables at transaction
  * end time. We want to drop temporary session variables with clause ON COMMIT
- * DROP, or we want to reset values of session variables with clause ON
- * TRANSACTION END RESET or we want to clean (reset) local memory allocated by
+ * DROP, or we want to clean (reset) local memory allocated by
  * values of dropped session variables.
  */
 
@@ -221,6 +1112,9 @@ register_session_variable_xact_action(Oid varid,
 	SVariableXActActionItem *xact_ai;
 	MemoryContext oldcxt;
 
+	elog(DEBUG1, "SVariableXActAction \"%s\" is registered for session variable (oid:%u)",
+						SvariableXActActionName(action), varid);
+
 	oldcxt = MemoryContextSwitchTo(CacheMemoryContext);
 
 	xact_ai = (SVariableXActActionItem *)
@@ -231,8 +1125,10 @@ register_session_variable_xact_action(Oid varid,
 	xact_ai->creating_subid = GetCurrentSubTransactionId();
 	xact_ai->deleting_subid = InvalidSubTransactionId;
 
-	Assert(action == SVAR_ON_COMMIT_DROP);
-	xact_drop_actions = lcons(xact_ai, xact_drop_actions);
+	if (action == SVAR_ON_COMMIT_DROP)
+		xact_on_commit_drop_actions = lcons(xact_ai, xact_on_commit_drop_actions);
+	else
+		xact_on_commit_reset_actions = lcons(xact_ai, xact_on_commit_reset_actions);
 
 	MemoryContextSwitchTo(oldcxt);
 }
@@ -251,7 +1147,10 @@ unregister_session_variable_xact_action(Oid varid,
 
 	Assert(action == SVAR_ON_COMMIT_DROP);
 
-	foreach(l, xact_drop_actions)
+	elog(DEBUG1, "SVariableXActAction \"%s\" is unregistered for session variable (oid:%u)",
+						SvariableXActActionName(action), varid);
+
+	foreach(l, xact_on_commit_drop_actions)
 	{
 		SVariableXActActionItem *xact_ai =
 					(SVariableXActActionItem *) lfirst(l);
@@ -270,25 +1169,39 @@ AtPreEOXact_SessionVariable_on_xact_actions(bool isCommit)
 {
 	ListCell   *l;
 
-	foreach(l, xact_drop_actions)
+	/*
+	 * Clean memory for all eox_reset variables. Do it
+	 * first, it reduces enhancing action lists about
+	 * RECHECK action.
+	 */
+	foreach(l, xact_reset_varids)
 	{
-		SVariableXActActionItem *xact_ai =
-							(SVariableXActActionItem *) lfirst(l);
+		remove_session_variable_by_id(lfirst_oid(l));
+	}
+
+	/* We can clean xact_reset_varids */
+	list_free(xact_reset_varids);
+	xact_reset_varids = NIL;
 
-		/* Iterate only over entries that are still pending */
-		if (xact_ai->deleting_subid == InvalidSubTransactionId)
+	if (isCommit && xact_on_commit_drop_actions)
+	{
+		foreach(l, xact_on_commit_drop_actions)
 		{
+			SVariableXActActionItem *xact_ai =
+								(SVariableXActActionItem *) lfirst(l);
 
-			/*
-			 * ON COMMIT DROP is allowed only for temp session
-			 * variables. So we should explicitly delete only when
-			 * current transaction was committed. When it's rollback,
-			 * then session variable is removed automatically.
-			 */
-			if (isCommit)
+			/* Iterate only over entries that are still pending */
+			if (xact_ai->deleting_subid == InvalidSubTransactionId)
 			{
 				ObjectAddress object;
 
+				/*
+				 * ON COMMIT DROP is allowed only for temp session
+				 * variables. So we should explicitly delete only when
+				 * current transaction was committed. When it's rollback,
+				 * then session variable is removed automatically.
+				 */
+
 				object.classId = VariableRelationId;
 				object.objectId = xact_ai->varid;
 				object.objectSubId = 0;
@@ -312,8 +1225,63 @@ AtPreEOXact_SessionVariable_on_xact_actions(bool isCommit)
 	 * Any drop action left is an entry that was unregistered and not
 	 * rollbacked, so we can simply remove them.
 	 */
-	list_free_deep(xact_drop_actions);
-	xact_drop_actions = NIL;
+	list_free_deep(xact_on_commit_drop_actions);
+	xact_on_commit_drop_actions = NIL;
+
+	if (isCommit && xact_recheck_requests)
+	{
+		Assert(sessionvars);
+
+		foreach(l, xact_recheck_requests)
+		{
+			SVariable	svar;
+			bool		found;
+			RecheckVariableRequest *req;
+
+			req = (RecheckVariableRequest *) lfirst(l);
+
+			svar = (SVariable) hash_search(sessionvars, &req->varid,
+										   HASH_FIND, &found);
+
+			if (found)
+			{
+				HeapTuple	tp;
+
+				tp = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(svar->varid));
+
+				if (HeapTupleIsValid(tp))
+					ReleaseSysCache(tp);
+				else
+					remove_session_variable(svar);
+			}
+		}
+
+		list_free_deep(xact_recheck_requests);
+		xact_recheck_requests = NIL;
+	}
+
+	if (isCommit && xact_on_commit_reset_actions)
+	{
+		foreach(l, xact_on_commit_reset_actions)
+		{
+			SVariableXActActionItem *xact_ai =
+								(SVariableXActActionItem *) lfirst(l);
+
+			/*
+			 * When we process DROP VARIABLE statement, we create
+			 * SVAR_ON_COMMIT_RESET xact action. We want to process
+			 * this action only when related transaction is commited
+			 * (when DROP VARIABLE statement sucessfully processed).
+			 * We want to preserve variable content, when the transaction
+			 * with DROP VARAIBLE statement was reverted.
+			 */
+			if (xact_ai->deleting_subid == InvalidSubTransactionId)
+				remove_session_variable_by_id(xact_ai->varid);
+		}
+	}
+
+	list_free_deep(xact_on_commit_reset_actions);
+	xact_on_commit_reset_actions = NIL;
 }
 
 /*
@@ -329,7 +1297,33 @@ AtEOSubXact_SessionVariable_on_xact_actions(bool isCommit, SubTransactionId mySu
 {
 	ListCell   *cur_item;
 
-	foreach(cur_item, xact_drop_actions)
+	foreach(cur_item, xact_on_commit_drop_actions)
+	{
+		SVariableXActActionItem *xact_ai =
+								  (SVariableXActActionItem *) lfirst(cur_item);
+
+		if (!isCommit && xact_ai->creating_subid == mySubid)
+		{
+			/* cur_item must be removed */
+			xact_on_commit_drop_actions = foreach_delete_current(xact_on_commit_drop_actions, cur_item);
+			pfree(xact_ai);
+		}
+		else
+		{
+			/* cur_item must be preserved */
+			if (xact_ai->creating_subid == mySubid)
+				xact_ai->creating_subid = parentSubid;
+			if (xact_ai->deleting_subid == mySubid)
+				xact_ai->deleting_subid = isCommit ? parentSubid : InvalidSubTransactionId;
+		}
+	}
+
+	/*
+	 * Reset and recheck actions - cleaning memory should be done every time
+	 * (when the variable with short life cycle was used) and then
+	 * cannot be removed from xact action list.
+	 */
+	foreach(cur_item, xact_on_commit_reset_actions)
 	{
 		SVariableXActActionItem *xact_ai =
 								  (SVariableXActActionItem *) lfirst(cur_item);
@@ -337,7 +1331,8 @@ AtEOSubXact_SessionVariable_on_xact_actions(bool isCommit, SubTransactionId mySu
 		if (!isCommit && xact_ai->creating_subid == mySubid)
 		{
 			/* cur_item must be removed */
-			xact_drop_actions = foreach_delete_current(xact_drop_actions, cur_item);
+			xact_on_commit_reset_actions =
+							foreach_delete_current(xact_on_commit_reset_actions, cur_item);
 			pfree(xact_ai);
 		}
 		else
@@ -350,3 +1345,87 @@ AtEOSubXact_SessionVariable_on_xact_actions(bool isCommit, SubTransactionId mySu
 		}
 	}
 }
+
+/*
+ * pg_debug_show_used_session_variables - designed for testing
+ *
+ * returns content of session vars
+ */
+Datum
+pg_debug_show_used_session_variables(PG_FUNCTION_ARGS)
+{
+#define NUM_PG_DEBUG_SHOW_USED_SESSION_VARIABLES_ATTS 10
+
+	SetSingleFuncCall(fcinfo, 0);
+
+	if (sessionvars)
+	{
+		ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+		HASH_SEQ_STATUS status;
+		SVariable svar;
+
+		/* Ensure so all entries in sessionvars hash table are valid */
+		sync_sessionvars_all();
+
+		hash_seq_init(&status, sessionvars);
+
+		while ((svar = (SVariable) hash_seq_search(&status)) != NULL)
+		{
+			Datum		values[NUM_PG_DEBUG_SHOW_USED_SESSION_VARIABLES_ATTS];
+			bool		nulls[NUM_PG_DEBUG_SHOW_USED_SESSION_VARIABLES_ATTS];
+			HeapTuple	tp;
+
+			memset(values, 0, sizeof(values));
+			memset(nulls, 0, sizeof(nulls));
+
+			values[0] = ObjectIdGetDatum(svar->varid);
+
+			values[3] = ObjectIdGetDatum(svar->svartype->typid);
+			values[4] = PointerGetDatum(cstring_to_text(svar->svartype->typname));
+			values[7] = BoolGetDatum(session_variable_use_valid_type(svar));
+
+			/* check if session variable is visible in system catalog */
+			tp = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(svar->varid));
+
+			/*
+			 * Sessionvars can hold data of variables removed from catalog,
+			 * (and not purged) and then namespacename and name cannot be read
+			 * from catalog.
+			 */
+			if (HeapTupleIsValid(tp))
+			{
+				ReleaseSysCache(tp);
+
+				/* When we see data in catalog */
+				nsname = get_namespace_name(get_session_variable_namespace(svar->varid));
+				name = get_session_variable_name(svar->varid);
+
+				values[5] = BoolGetDatum(false);
+				values[6] = BoolGetDatum(svar->is_valid);
+				values[8] = BoolGetDatum(pg_variable_aclcheck(svar->varid, GetUserId(), ACL_READ) == ACLCHECK_OK);
+				values[9] = BoolGetDatum(pg_variable_aclcheck(svar->varid, GetUserId(), ACL_WRITE) == ACLCHECK_OK);
+			}
+			else
+			{
+				/*
+				 * When session variable was removed from catalog, but still it
+				 * in memory. The memory was not purged yet.
+				 */
+				nsname = svar->nsname;
+				name = svar->name;
+
+				values[5] = BoolGetDatum(true);
+				nulls[6] = true;
+				nulls[8] = true;
+				nulls[9] = true;
+			}
+
+			values[1] = PointerGetDatum(cstring_to_text(nsname));
+			values[2] = PointerGetDatum(cstring_to_text(name));
+
+			tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls);
+		}
+	}
+
+	return (Datum) 0;
+}
diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c
index d0a57c7aae..6720f6182d 100644
--- a/src/backend/executor/execExpr.c
+++ b/src/backend/executor/execExpr.c
@@ -33,6 +33,7 @@
 #include "access/nbtree.h"
 #include "catalog/objectaccess.h"
 #include "catalog/pg_type.h"
+#include "commands/session_variable.h"
 #include "executor/execExpr.h"
 #include "executor/nodeSubplan.h"
 #include "funcapi.h"
@@ -994,6 +995,80 @@ 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;
+
+							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;
+							}
+
+							if (es_session_variables)
+							{
+								SessionVariableValue *var;
+
+								/*
+								 * 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);
+							}
+							else
+							{
+								AclResult	aclresult;
+								Oid			varid = param->paramvarid;
+								Oid			vartype = param->paramtype;
+
+								/*
+								 * When the expression is evaluated directly
+								 * without query executor start (plpgsql simple
+								 * expr evaluation), then the array es_session_variables
+								 * is null. In this case we need to use direct
+								 * access to session variables. The values are
+								 * not protected by using copy, but it is not
+								 * problem (we don't need to emulate stability
+								 * of the value).
+								 *
+								 * In this case we should to do aclcheck, because
+								 * usual aclcheck from standard_ExecutorStart
+								 * is not executed in this case. Fortunately
+								 * it is just once per transaction.
+								 */
+								aclresult = pg_variable_aclcheck(varid, GetUserId(), ACL_SELECT);
+								if (aclresult != ACLCHECK_OK)
+									aclcheck_error(aclresult, OBJECT_VARIABLE,
+												   get_session_variable_name(varid));
+
+								scratch.opcode = EEOP_PARAM_VARIABLE;
+								scratch.d.vparam.varid = varid;
+								scratch.d.vparam.vartype = vartype;
+								ExprEvalPushStep(state, &scratch);
+							}
+						}
+						break;
+
 					case PARAM_EXTERN:
 
 						/*
index 636794ca6f..ce32fe0c25 100644
@@ -455,6 +456,7 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull)
@@ -1094,6 +1096,15 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull)
 			EEO_NEXT();
 		}
 
+			*op->resvalue = GetSessionVariableWithTypeCheck(op->d.vparam.varid,
+															 op->d.vparam.vartype);
index ef2fd46092..68e748eb73 100644
@@ -200,6 +201,61 @@ standard_ExecutorStart(QueryDesc *queryDesc, int eflags)
 
+	/*
+	 * 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->num_session_variables > 0)
+	{
+		/*
+		 * When paralel access to query parameters (including related
+		 * session variables) is required, then related session variables
+		 * are restored (deserilized) in queryDesc already. So just push
+		 * pointer of this array to executor's estate.
+		 */
+		estate->es_session_variables = queryDesc->session_variables;
+		estate->es_num_session_variables = queryDesc->num_session_variables;
+	}
+	else 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.
+		 */
+		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 = pg_variable_aclcheck(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 = CopySessionVariable(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/execParallel.c b/src/backend/executor/execParallel.c
index f1fd7f7e8b..1e04923788 100644
--- a/src/backend/executor/execParallel.c
+++ b/src/backend/executor/execParallel.c
@@ -12,8 +12,9 @@
  * workers and ensuring that their state generally matches that of the
  * leader; see src/backend/access/transam/README.parallel for details.
  * However, we must save and restore relevant executor state, such as
- * any ParamListInfo associated with the query, buffer/WAL usage info, and
- * the actual plan to be passed down to the worker.
+ * any ParamListInfo associated with the query, buffer/WAL usage info,
+ * session variables buffer, and the actual plan to be passed down to
+ * the worker.
  *
  * IDENTIFICATION
  *	  src/backend/executor/execParallel.c
@@ -66,6 +67,7 @@
 #define PARALLEL_KEY_QUERY_TEXT		UINT64CONST(0xE000000000000008)
 #define PARALLEL_KEY_JIT_INSTRUMENTATION UINT64CONST(0xE000000000000009)
 #define PARALLEL_KEY_WAL_USAGE			UINT64CONST(0xE00000000000000A)
+#define PARALLEL_KEY_SESSION_VARIABLES	UINT64CONST(0xE00000000000000B)

 

 
 
@@ -1495,3 +1524,118 @@ ParallelQueryMain(dsm_segment *seg, shm_toc *toc)
index bd3965143d..dca5942b7a 100644
 
index cf9e0a74db..97e1038656 100644
@@ -317,6 +317,7 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
 
 	/*
@@ -530,6 +531,7 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
 
@@ -685,6 +687,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 1cb0abdbc1..c7edc4e75a 100644
--- a/src/backend/optimizer/plan/setrefs.c
+++ b/src/backend/optimizer/plan/setrefs.c
@@ -181,6 +181,8 @@ 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);

 /*****************************************************************************
@@ -1218,6 +1220,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
@@ -1831,8 +1877,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.
@@ -1922,15 +1969,39 @@ fix_expr_common(PlannerInfo *root, Node *node)
 				g->cols = cols;
 		}
 	}
+	else if (IsA(node, Param))
+	{
+		Param *p = (Param *) node;
+
+		if (p->paramkind == PARAM_VARIABLE)
+		{
+			PlanInvalItem *inval_item = makeNode(PlanInvalItem);
+
+			/* paramid is still session variable id */
+			inval_item->cacheId = VARIABLEOID;
+			inval_item->hashValue = GetSysCacheHashValue1(VARIABLEOID,
+														  ObjectIdGetDatum(p->paramvarid));
+
+			/* Append this variable to global, register dependency */
+			root->glob->invalItems = lappend(root->glob->invalItems,
+											 inval_item);
+		}
+	}
 }
 
 /*
  * fix_param_node
+ * If it's a PARAM_VARIABLE, then we collect used session variables in
+ * list root->glob->sessionVariable. We should to assign Param paramvarid
+ * too, and it is position of related session variable in mentioned list.
@@ -1949,6 +2020,41 @@ fix_param_node(PlannerInfo *root, Param *p)
 
@@ -2010,7 +2116,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
@@ -2032,7 +2141,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);
 	}
diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c
index 533df86ff7..ca48b3b74c 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 "executor/execExpr.h"
@@ -853,16 +854,17 @@ max_parallel_hazard_walker(Node *node, max_parallel_hazard_context *context)
 
 	/*
 	 * We can't pass Params to workers at the moment either, so they are also
-	 * parallel-restricted, unless they are PARAM_EXTERN Params or are
-	 * PARAM_EXEC Params listed in safe_param_ids, meaning they could be
-	 * either generated within workers or can be computed by the leader and
-	 * then their value can be passed to workers.
+	 * parallel-restricted, unless they are PARAM_EXTERN or PARAM_VARIABLE
+	 * Params or are PARAM_EXEC Params listed in safe_param_ids, meaning they
+	 * could be either generated within workers or can be computed by the
+	 * leader and then their value can be passed to workers.
 	 */
 	else if (IsA(node, Param))
 	{
 		Param	   *param = (Param *) node;

 		if (param->paramkind != PARAM_EXEC ||
@@ -2284,6 +2286,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 *
@@ -2406,6 +2409,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 = CopySessionVariableWithTypeCheck(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
@@ -4813,22 +4839,45 @@ substitute_actual_parameters_mutator(Node *node,
 {
 	if (node == NULL)
 		return NULL;
+
+	/*
+	 * SQL functions can contain two different kind of params. The nodes with
+	 * paramkind PARAM_EXTERN are related to function's arguments (and should
+	 * be replaced in this step), because this is how we apply the function's
+	 * arguments for an expression.
+	 *
+	 * The nodes with paramkind PARAM_VARIABLE are related to usage of
+	 * session variables. The values of session variables are not passed
+	 * to expression by expression arguments, so it should not be replaced
+	 * here by function's arguments. Although we could substitute params
+	 * related to immutable session variables with default expression by
+	 * this default expression, it is safer to not do it. This way we don't
+	 * have to run security checks here. There can be some performance loss,
+	 * but an access to session variable is fast (and the result of default
+	 * expression is immediately materialized and can be reused).
+	 */
 	if (IsA(node, Param))
 	{
 		Param	   *param = (Param *) node;

 
index 6688c2a865..a013031d22 100644
@@ -525,6 +525,8 @@ transformDeleteStmt(ParseState *pstate, DeleteStmt *stmt)

 	/* this must be done after collations, for reliable comparison of exprs */
@@ -942,6 +944,7 @@ transformInsertStmt(ParseState *pstate, InsertStmt *stmt)

 
@@ -1312,8 +1315,9 @@ transformSelectStmt(ParseState *pstate, SelectStmt *stmt)
 	/* process the FROM clause */
 	transformFromClause(pstate, stmt->fromClause);

 	/* mark column origins */
@@ -1404,6 +1408,8 @@ transformSelectStmt(ParseState *pstate, SelectStmt *stmt)
 							   (LockingClause *) lfirst(l), false);
 	}

 	/* this must be done after collations, for reliable comparison of exprs */
@@ -1878,6 +1884,8 @@ transformSetOperationStmt(ParseState *pstate, SelectStmt *stmt)
 							   (LockingClause *) lfirst(l), false);
 	}

 	/* this must be done after collations, for reliable comparison of exprs */
@@ -2409,6 +2417,7 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)

 
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index aa1b1a76dc..7eafd5f171 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -40,11 +40,12 @@
 #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;
-
+bool		session_variables_ambiguity_warning = false;
 
 static Node *transformExprRecurse(ParseState *pstate, Node *expr);
 static Node *transformParamRef(ParseState *pstate, ParamRef *pref);
@@ -101,6 +102,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);

 /*
@@ -506,6 +510,10 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
 	char	   *relname = NULL;
 	char	   *colname = NULL;
 	ParseNamespaceItem *nsitem;
+	Oid			varid = InvalidOid;
+	char	   *attrname = NULL;
+	bool		not_unique;
+	bool		lockit;
 	int			levels_up;
 	enum
 	{
@@ -841,6 +849,140 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
 					 parser_errposition(pstate, cref->location)));
 	}
 
+	/* Protect session variable by AccessShareLock when it is not shadowed. */
+	lockit = (node == NULL);
+
+	/* The col's reference can be a reference to session variable too. */
+	varid = IdentifyVariable(cref->fields, &attrname, lockit, &not_unique);
+
+	/*
+	 * Raise error when reference to session variable is ambiguous and
+	 * and this reference is not shadowed.
+	 */
+	if (!node && not_unique)
+		ereport(ERROR,
+				(errcode(ERRCODE_AMBIGUOUS_PARAMETER),
+				 errmsg("session variable reference \"%s\" is ambiguous",
+						NameListToString(cref->fields)),
+				 parser_errposition(pstate, cref->location)));
+
+	if (OidIsValid(varid))
+	{
+		/*
+		 * When Session variables is shadowed by columns or by routine's
+		 * variables or routine's arguments. The valid RTE, but without
+		 * valid column reference can shadow session variables too. We don't
+		 * want to mask relation related error, that can be very probably
+		 * user's typo error.
+		 */
+		if (node ||
+			(!node && relname && (crerr == CRERR_NO_COLUMN)))
+		{
+			/*
+			 * In this case we can raise a warning, but only when required.
+			 * We should not raise warnings for contexts where usage of session
+			 * variables has not sense, or when we can detect that variable
+			 * doesn't have the wanted field (and then there is no possible
+			 * identifier's collision).
+			 */
+			if (session_variables_ambiguity_warning &&
+				pstate->p_expr_kind == EXPR_KIND_SELECT_TARGET)
+			{
+				/*
+				 * Some cases with ambiguous references can be solved without
+				 * raising a warning. When there is a collision between column
+				 * name (or label) and some session variable name, and when we
+				 * know attribute name, then we can ignore the collision when:
+				 *
+				 *   a) variable is of scalar type (then indirection cannot be
+				 *      applied on this session variable.
+				 *
+				 *   b) when related variable has no field with the given
+				 *      attrname, then indirection cannot be applied on this
+				 *      session variable.
+				 */
+				if (attrname && node)
+				{
+					Oid			typid;
+					int32		typmod;
+					Oid			collid;
+
+					get_session_variable_type_typmod_collid(varid,
+															&typid, &typmod,
+															&collid);
+
+					if (type_is_rowtype(typid))
+					{
+						TupleDesc	tupdesc;
+						bool		found = false;
+						int			i;
+
+						/* slow part, I hope it will not be to often */
+						tupdesc = lookup_rowtype_tupdesc(typid, typmod);
+						for (i = 0; i < tupdesc->natts; i++)
+						{
+							if (namestrcmp(&(TupleDescAttr(tupdesc, i)->attname), attrname) == 0 &&
+								!TupleDescAttr(tupdesc, i)->attisdropped)
+							{
+								found = true;
+								break;
+							}
+						}
+
+						ReleaseTupleDesc(tupdesc);
+
+						/* There is no composite variable with this field. */
+						if (!found)
+							varid = InvalidOid;
+					}
+					else
+						/* There is no composite variable with this name. */
+						varid = InvalidOid;
+				}
+
+				/*
+				 * Raise warning when session variable reference is still
+				 * visible.
+				 */
+				if (OidIsValid(varid))
+				{
+					if (node)
+						ereport(WARNING,
+								(errcode(ERRCODE_AMBIGUOUS_COLUMN),
+								 errmsg("session variable \"%s\" is shadowed",
+										NameListToString(cref->fields)),
+								 errdetail("Session variables can be shadowed by columns, routine's variables and routine's arguments with the same name."),
+								 parser_errposition(pstate, cref->location)));
+					else
+						/* session variable is shadowed by RTE */
+						ereport(WARNING,
+								(errcode(ERRCODE_AMBIGUOUS_COLUMN),
+								 errmsg("session variable \"%s.%s\" is shadowed",
+										get_namespace_name(get_session_variable_namespace(varid)),
+										get_session_variable_name(varid)),
+								 errdetail("Session variables can be shadowed by tables or table's aliases with the same name."),
+								 parser_errposition(pstate, cref->location)));
+				}
+			}
+
+			/* Session variables are always shadowed by any other node. */
+			varid = InvalidOid;
+		}
+		else
+		{
+			Oid			typid;
+			int32		typmod;
+			Oid			collid;
+
+			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.
 	 */
@@ -875,6 +1017,64 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
 	return node;
 }
 
+/*
index 5aa5a350f3..c5cf50687c 100644
 
+	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/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 8964f73b92..84b5c8732a 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"
@@ -502,6 +503,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);
@@ -8086,6 +8088,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.
@@ -12760,6 +12770,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 0d6a295674..74daf13a3f 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"
@@ -1891,6 +1892,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
@@ -2022,7 +2037,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 af74fe345e..8d61fc082c 100644
--- a/src/backend/utils/fmgr/fmgr.c
+++ b/src/backend/utils/fmgr/fmgr.c
@@ -1902,9 +1902,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/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index 9fbbfb1be5..f687019037 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -1731,6 +1731,18 @@ static struct config_bool ConfigureNamesBool[] =
 		false,
 		NULL, NULL, NULL
 	},
+
+	{
+		{"session_variables_ambiguity_warning", PGC_USERSET, DEVELOPER_OPTIONS,
+			gettext_noop("Raise warning when reference to session variable is ambiguous."),
+			NULL,
+			GUC_NOT_IN_SAMPLE
+		},
+		&session_variables_ambiguity_warning,
+		false,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"db_user_namespace", PGC_SIGHUP, CONN_AUTH_AUTH,
 			gettext_noop("Enables per-database user names."),
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 5055904005..e9d26f783b 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -11897,4 +11897,11 @@
   prorettype => 'bytea', proargtypes => 'pg_brin_minmax_multi_summary',
   prosrc => 'brin_minmax_multi_summary_send' },
 
+{ oid => '8488', descr => 'debug list of used session variables',
+  proname => 'pg_debug_show_used_session_variables', prorows => '1000', proretset => 't',
+  provolatile => 's', prorettype => 'record', proargtypes => '',
+  proallargtypes => '{oid,text,text,oid,text,bool,bool,bool,bool,bool}',
+  proargmodes => '{o,o,o,o,o,o,o,o,o,o}',
+  proargnames => '{varid,schema,name,typid,typname,removed,has_value,has_valid_value,can_read,can_write}',
+  prosrc => 'pg_debug_show_used_session_variables' },
 ]
diff --git a/src/include/executor/execExpr.h b/src/include/executor/execExpr.h
index c8ef917ffe..9e06ca7a3b 100644
--- a/src/include/executor/execExpr.h
+++ b/src/include/executor/execExpr.h
@@ -161,6 +161,7 @@ typedef enum ExprEvalOp
 	EEOP_PARAM_EXEC,
 	EEOP_PARAM_EXTERN,
 	EEOP_PARAM_CALLBACK,
+	EEOP_PARAM_VARIABLE,
 
 	/* return CaseTestExpr value */
 	EEOP_CASE_TESTVAL,
@@ -388,6 +389,13 @@ typedef struct ExprEvalStep
 			Oid			paramtype;	/* OID of parameter's datatype */
 		}			param;
 
@@ -824,6 +832,8 @@ extern void ExecEvalParamExec(ExprState *state, ExprEvalStep *op,
index e79e2c001f..dbf4dc7ea0 100644

diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 01b1727fc0..e8509d4c73 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -590,6 +590,18 @@ typedef struct AsyncRequest
 								 * tuples) */
 } AsyncRequest;
 
+/* ----------------
+ * SessionVariableValue
+ * ----------------
+ */
+typedef struct SessionVariableValue
+{
+	Oid			varid;
+	Oid			typid;
+	bool		isnull;
+	Datum		value;
+}			SessionVariableValue;
+
 /* ----------------
  *	  EState information
  *
@@ -641,6 +653,13 @@ typedef struct EState
 	ParamListInfo es_param_list_info;	/* values of external params */
 	ParamExecData *es_param_exec_vals;	/* values of internal params */
 
+	/* Variables info: */
 
 	/* Other working state: */
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index a10f3b2454..b1d4e82062 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -136,6 +136,7 @@ typedef struct Query
 	int			resultRelation; /* rtable index of target relation for
 								 * INSERT/UPDATE/DELETE/MERGE; 0 for SELECT */
 
@@ -145,6 +146,7 @@ typedef struct Query

diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index 294cfe9c47..5a79a4d591 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -157,6 +157,8 @@ typedef struct PlannerGlobal
 
 	/* partition descriptors */
 	PartitionDirectory partition_directory pg_node_attr(read_write_ignore);
+
+	List	   *sessionVariables;	/* list of used session variables */
 } PlannerGlobal;
 
 /* macro for fetching the Plan associated with a SubPlan node */
@@ -459,6 +461,8 @@ struct PlannerInfo
 	/* 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 dca2a21e7a..e03727e19f 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -91,6 +91,8 @@ typedef struct PlannedStmt

index 3aa96bb685..29c2266c2b 100644
 
@@ -305,13 +308,17 @@ typedef struct Const
 
 typedef struct Param
@@ -322,6 +329,7 @@ typedef struct Param
 
diff --git a/src/include/optimizer/planmain.h b/src/include/optimizer/planmain.h
index 1566f435b3..2608735731 100644
--- a/src/include/optimizer/planmain.h
+++ b/src/include/optimizer/planmain.h
@@ -117,4 +117,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 *root);
 
+extern void pull_up_has_session_variables(PlannerInfo *root);
+
 #endif							/* PLANMAIN_H */
diff --git a/src/include/parser/parse_expr.h b/src/include/parser/parse_expr.h
index c8e5c57b43..14b0adb948 100644
--- a/src/include/parser/parse_expr.h
+++ b/src/include/parser/parse_expr.h
@@ -17,6 +17,7 @@
 
 /* GUC parameters */
 extern PGDLLIMPORT bool Transform_null_equals;
+extern PGDLLIMPORT bool session_variables_ambiguity_warning;
 
 extern Node *transformExpr(ParseState *pstate, Node *expr, ParseExprKind exprKind);
 
diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h
index 4d65a9e9e8..d07f5b0584 100644
--- a/src/include/parser/parse_node.h
+++ b/src/include/parser/parse_node.h
@@ -212,6 +212,7 @@ struct ParseState
 	bool		p_hasTargetSRFs;
 	bool		p_hasSubLinks;
 	bool		p_hasModifyingCTE;
+	bool		p_hasSessionVariables;

-- 
2.37.2

From c5e1e825b23532ef6c704cb63ed8824bffa7ae1c Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Mon, 4 Apr 2022 21:19:03 +0200
Subject: [PATCH 01/12] catalog support for session variables

Implementation new system object class - session variable with new access rights READ, WRITE,
with routines for creating session variable, initialization of session variable from system
catalog, and lookups routines for identification of session variables.
---
 src/backend/access/transam/xact.c       |  11 +
 src/backend/catalog/Makefile            |   4 +-
 src/backend/catalog/aclchk.c            | 299 +++++++++++++++++++
 src/backend/catalog/dependency.c        |  13 +-
 src/backend/catalog/namespace.c         | 374 ++++++++++++++++++++++++
 src/backend/catalog/objectaddress.c     | 126 +++++++-
 src/backend/catalog/pg_shdepend.c       |   2 +
 src/backend/catalog/pg_variable.c       | 233 +++++++++++++++
 src/backend/commands/Makefile           |   1 +
 src/backend/commands/alter.c            |   9 +
 src/backend/commands/dropcmds.c         |   4 +
 src/backend/commands/event_trigger.c    |   6 +
 src/backend/commands/seclabel.c         |   1 +
 src/backend/commands/sessionvariable.c  | 352 ++++++++++++++++++++++
 src/backend/commands/tablecmds.c        |  27 +-
 src/backend/parser/gram.y               | 144 ++++++++-
 src/backend/parser/parse_agg.c          |   2 +
 src/backend/parser/parse_expr.c         |   5 +
 src/backend/parser/parse_func.c         |   1 +
 src/backend/tcop/utility.c              |  16 +
 src/backend/utils/adt/acl.c             |   8 +-
 src/backend/utils/cache/lsyscache.c     |  89 ++++++
 src/backend/utils/cache/syscache.c      |  23 ++
 src/backend/utils/fmgr/fmgr.c           |   6 +-
 src/include/catalog/dependency.h        |   5 +-
 src/include/catalog/namespace.h         |   6 +
 src/include/catalog/pg_default_acl.h    |   1 +
 src/include/catalog/pg_proc.dat         |   3 +
 src/include/catalog/pg_variable.h       | 107 +++++++
 src/include/commands/session_variable.h |  41 +++
 src/include/nodes/parsenodes.h          |  20 ++
 src/include/parser/kwlist.h             |   2 +
 src/include/parser/parse_node.h         |   1 +
 src/include/tcop/cmdtaglist.h           |   3 +
 src/include/utils/acl.h                 |   6 +-
 src/include/utils/lsyscache.h           |   8 +
 src/include/utils/syscache.h            |   6 +-
 37 files changed, 1947 insertions(+), 18 deletions(-)
 create mode 100644 src/backend/catalog/pg_variable.c
 create mode 100644 src/backend/commands/sessionvariable.c
 create mode 100644 src/include/catalog/pg_variable.h
 create mode 100644 src/include/commands/session_variable.h

index 50f092d7eb..e72fd74145 100644
@@ -36,6 +36,7 @@
@@ -2213,6 +2214,9 @@ CommitTransaction(void)
 
+	/* Let ON COMMIT DROP */
+	AtPreEOXact_SessionVariable_on_xact_actions(true);
+
 	/* close large objects before lower-level cleanup */
 	AtEOXact_LargeObject(true);
 
@@ -2789,6 +2793,9 @@ AbortTransaction(void)
 	AtAbort_Portals();
@@ -4986,6 +4993,8 @@ CommitSubTransaction(void)
@@ -5150,6 +5159,8 @@ AbortSubTransaction(void)
index 89a0221ec9..f89e12eaf2 100644
@@ -45,6 +45,7 @@ OBJS = \
 	pg_shdepend.o \
 	pg_subscription.o \
 	pg_type.o \
+	pg_variable.o \
 	storage.o \
 	toasting.o
 
@@ -72,7 +73,8 @@ CATALOG_HEADERS := \
 	pg_collation.h pg_parameter_acl.h pg_partitioned_table.h \
 	pg_range.h pg_transform.h \
 	pg_sequence.h pg_publication.h pg_publication_namespace.h \
-	pg_publication_rel.h pg_subscription.h pg_subscription_rel.h
+	pg_publication_rel.h pg_subscription.h pg_subscription_rel.h \
+	pg_variable.h

index 17ff617fba..b88ff346ec 100644
@@ -114,6 +115,7 @@ static void ExecGrant_Namespace(InternalGrant *grantStmt);
 static void ExecGrant_Parameter(InternalGrant *grantStmt);
 
 static void SetDefaultACLsInSchemas(InternalDefaultACL *iacls, List *nspnames);
 static void SetDefaultACL(InternalDefaultACL *iacls);
@@ -265,6 +267,9 @@ restrict_and_check_grant(bool is_grant, AclMode avail_goptions, bool all_privs,
 		case OBJECT_PARAMETER_ACL:
 			whole_mask = ACL_ALL_RIGHTS_PARAMETER_ACL;
 			break;
+		case OBJECT_VARIABLE:
+			whole_mask = ACL_ALL_RIGHTS_VARIABLE;
+			break;
 		default:
 			elog(ERROR, "unrecognized object type: %d", objtype);
 			/* not reached, but keep compiler quiet */
@@ -509,6 +514,10 @@ ExecuteGrantStmt(GrantStmt *stmt)
 			all_privileges = ACL_ALL_RIGHTS_PARAMETER_ACL;
 			errormsg = gettext_noop("invalid privilege type %s for parameter");
 			break;
+		case OBJECT_VARIABLE:
+			all_privileges = ACL_ALL_RIGHTS_VARIABLE;
+			errormsg = gettext_noop("invalid privilege type %s for session variable");
+			break;
 		default:
 			elog(ERROR, "unrecognized GrantStmt.objtype: %d",
 				 (int) stmt->objtype);
@@ -614,6 +623,9 @@ ExecGrantStmt_oids(InternalGrant *istmt)
 		case OBJECT_PARAMETER_ACL:
 			ExecGrant_Parameter(istmt);
 			break;
+		case OBJECT_VARIABLE:
+			ExecGrant_Variable(istmt);
+			break;
 		default:
 			elog(ERROR, "unrecognized GrantStmt.objtype: %d",
 				 (int) istmt->objtype);
@@ -804,6 +816,16 @@ objectNamesToOids(ObjectType objtype, List *objnames, bool is_grant)
 					objects = lappend_oid(objects, parameterId);
 			}
 			break;
+		case OBJECT_VARIABLE:
+			foreach(cell, objnames)
+			{
+				RangeVar   *varvar = (RangeVar *) lfirst(cell);
+				Oid			relOid;
+
+				relOid = LookupVariable(varvar->schemaname, varvar->relname, false);
+				objects = lappend_oid(objects, relOid);
+			}
+			break;
 		default:
 			elog(ERROR, "unrecognized GrantStmt.objtype: %d",
 				 (int) objtype);
@@ -893,6 +915,33 @@ objectsInSchemaToOids(ObjectType objtype, List *nspnames)
 					table_close(rel, AccessShareLock);
 				}
 				break;
+			case OBJECT_VARIABLE:
+				{
+					ScanKeyData key;
+					Relation	rel;
+					TableScanDesc scan;
+					HeapTuple	tuple;
+
+					ScanKeyInit(&key,
+								Anum_pg_variable_varnamespace,
+								BTEqualStrategyNumber, F_OIDEQ,
+								ObjectIdGetDatum(namespaceId));
+
+					rel = table_open(VariableRelationId, AccessShareLock);
+					scan = table_beginscan_catalog(rel, 1, &key);
+
+					while ((tuple = heap_getnext(scan, ForwardScanDirection)) != NULL)
+					{
+						Oid			oid = ((Form_pg_proc) GETSTRUCT(tuple))->oid;
+
+						objects = lappend_oid(objects, oid);
+					}
+
+					table_endscan(scan);
+					table_close(rel, AccessShareLock);
+				}
+				break;
+
 			default:
 				/* should not happen */
 				elog(ERROR, "unrecognized GrantStmt.objtype: %d",
@@ -1052,6 +1101,10 @@ ExecAlterDefaultPrivilegesStmt(ParseState *pstate, AlterDefaultPrivilegesStmt *s
 			all_privileges = ACL_ALL_RIGHTS_SCHEMA;
 			errormsg = gettext_noop("invalid privilege type %s for schema");
 			break;
+		case OBJECT_VARIABLE:
+			all_privileges = ACL_ALL_RIGHTS_VARIABLE;
+			errormsg = gettext_noop("invalid privilege type %s for session variable");
+			break;
 		default:
 			elog(ERROR, "unrecognized GrantStmt.objtype: %d",
 				 (int) action->objtype);
@@ -1246,6 +1299,12 @@ SetDefaultACL(InternalDefaultACL *iacls)
 				this_privileges = ACL_ALL_RIGHTS_SCHEMA;
 			break;
 
+		case OBJECT_VARIABLE:
+			objtype = DEFACLOBJ_VARIABLE;
+			if (iacls->all_privs && this_privileges == ACL_NO_RIGHTS)
+				this_privileges = ACL_ALL_RIGHTS_VARIABLE;
+			break;
+
 		default:
 			elog(ERROR, "unrecognized objtype: %d",
 				 (int) iacls->objtype);
@@ -1477,6 +1536,9 @@ RemoveRoleFromObjectACL(Oid roleid, Oid classid, Oid objid)
 			case DEFACLOBJ_NAMESPACE:
 				iacls.objtype = OBJECT_SCHEMA;
 				break;
+			case DEFACLOBJ_VARIABLE:
+				iacls.objtype = OBJECT_VARIABLE;
+				break;
 			default:
 				/* Shouldn't get here */
 				elog(ERROR, "unexpected default ACL type: %d",
@@ -1537,6 +1599,9 @@ RemoveRoleFromObjectACL(Oid roleid, Oid classid, Oid objid)
 			case ParameterAclRelationId:
 				istmt.objtype = OBJECT_PARAMETER_ACL;
 				break;
+			case VariableRelationId:
+				istmt.objtype = OBJECT_VARIABLE;
+				break;
 			default:
 				elog(ERROR, "unexpected object class %u", classid);
 				break;
@@ -3369,6 +3434,129 @@ ExecGrant_Parameter(InternalGrant *istmt)
 	table_close(relation, RowExclusiveLock);
 }
 
+static void
 
 static AclMode
 string_to_privilege(const char *privname)
@@ -3570,6 +3758,9 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
 					case OBJECT_TYPE:
 						msg = gettext_noop("permission denied for type %s");
 						break;
+					case OBJECT_VARIABLE:
+						msg = gettext_noop("permission denied for session variable %s");
+						break;
 					case OBJECT_VIEW:
 						msg = gettext_noop("permission denied for view %s");
 						break;
@@ -3681,6 +3872,9 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
 					case OBJECT_TYPE:
 						msg = gettext_noop("must be owner of type %s");
 						break;
+					case OBJECT_VARIABLE:
+						msg = gettext_noop("must be owner of session variable %s");
+						break;
 					case OBJECT_VIEW:
 						msg = gettext_noop("must be owner of view %s");
 						break;
@@ -3829,6 +4023,8 @@ pg_aclmask(ObjectType objtype, Oid table_oid, AttrNumber attnum, Oid roleid,
 			return ACL_NO_RIGHTS;
 		case OBJECT_TYPE:
 			return pg_type_aclmask(table_oid, roleid, mask, how);
+		case OBJECT_VARIABLE:
+			return pg_variable_aclmask(table_oid, roleid, mask, how);
 		default:
 			elog(ERROR, "unrecognized objtype: %d",
 				 (int) objtype);
@@ -4812,6 +5008,66 @@ pg_type_aclmask(Oid type_oid, Oid roleid, AclMode mask, AclMaskHow how)
 	return result;
 }
 
+/*
@@ -5112,6 +5368,18 @@ pg_type_aclcheck(Oid type_oid, Oid roleid, AclMode mode)
 		return ACLCHECK_NO_PRIV;
 }
 
+/*
+ * Exported routine for checking a user's access privileges to a variable
+ */
+AclResult
+pg_variable_aclcheck(Oid var_oid, Oid roleid, AclMode mode)
+{
+	if (pg_variable_aclmask(var_oid, roleid, mode, ACLMASK_ANY) != 0)
+		return ACLCHECK_OK;
+	else
+		return ACLCHECK_NO_PRIV;
+}
+
 /*
  * Ownership check for a relation (specified by OID).
  */
@@ -5729,6 +5997,33 @@ pg_statistics_object_ownercheck(Oid stat_oid, Oid roleid)
 	return has_privs_of_role(roleid, ownerId);
 }
 
+/*
@@ -5857,6 +6152,10 @@ get_user_default_acl(ObjectType objtype, Oid ownerId, Oid nsp_oid)
 
+		case OBJECT_VARIABLE:
index 39768fa22b..6255c2c72f 100644
@@ -65,12 +65,15 @@
@@ -188,7 +191,8 @@ static const Oid object_classes[] = {

@@ -1508,6 +1512,10 @@ doDeletion(const ObjectAddress *object, int flags)
 			DropObjectById(object);
 			break;
 
+		case OCLASS_VARIABLE:
+			RemoveSessionVariable(object->objectId);
+			break;
+
 			/*
 			 * These global object types are not supported here.
 			 */
@@ -2966,6 +2974,9 @@ getObjectClass(const ObjectAddress *object)

 	/* shouldn't get here */
index fafb9349cc..8300aea87b 100644
@@ -764,6 +765,69 @@ RelationIsVisible(Oid relid)
 	return visible;
 }
 
+/*
+ * VariableIsVisible
+		 * hidden by another variable of the same name earlier in the path. So
 
 /*
  * TypenameGetTypid
@@ -2843,6 +2907,305 @@ TSConfigIsVisible(Oid cfgid)
 	return visible;
 }
 
+/*
+ * Returns oid of session variable specified by possibly qualified identifier.
+ *
+		 * If nspname is not a known namespace, then nspname.varname cannot be
+		 * any usable session variable.
+ * Returns oid of identified variable. When last field of names list is
+ * identified as an attribute, then output attrname argument is set to
+ * an string of this field.
+ * is ambiguous. In this case this routine returns InvalidOid, and
+ * When lockit is true, then AccessShareLock is created on related
+ * session variable. The lock will be kept for the whole transaction.
+IdentifyVariable(List *names, char **attrname, bool lockit, bool *not_unique)
+			varid = LookupVariable(NULL, strVal(field1), true);
+				varid = varoid_without_attr;
+				varid = varoid_with_attr;
+				varid = varoid_without_attr;
+				varid = varoid_with_attr;
+			varid = LookupVariable(b, c, true);
+	if (OidIsValid(varid) && lockit)
+		LockDatabaseObject(VariableRelationId, varid, 0, AccessShareLock);
+
+	return varid;
+}
 
 /*
  * DeconstructQualifiedName
@@ -4660,3 +5023,14 @@ pg_is_other_temp_schema(PG_FUNCTION_ARGS)
 
index 798c1a2d1e..8317e7096f 100644
@@ -64,6 +64,7 @@
@@ -633,6 +634,20 @@ static const ObjectPropertyType ObjectProperty[] =
 
 /*
@@ -869,6 +884,10 @@ static const struct object_type_map
 	/* OCLASS_STATISTIC_EXT */
 	{
 		"statistics object", OBJECT_STATISTIC_EXT
+	},
+	/* OCLASS_VARIABLE */
+	{
+		"session variable", OBJECT_VARIABLE
 	}
 };
 
@@ -894,6 +913,7 @@ static ObjectAddress get_object_address_attrdef(ObjectType objtype,
 												bool missing_ok);
@@ -1163,6 +1183,9 @@ get_object_address(ObjectType objtype, Node *object,
@@ -2039,16 +2062,20 @@ get_object_address_defacl(List *object, bool missing_ok)
 
 	/*
 
+/*
@@ -2319,6 +2364,7 @@ pg_get_object_address(PG_FUNCTION_ARGS)
@@ -2629,6 +2675,11 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address,
@@ -3530,6 +3581,32 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok)
 				break;
 			}
 
+		case OCLASS_VARIABLE:
+			{
+				char	   *nspname;
+				HeapTuple	tup;
@@ -3882,6 +3959,16 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok)
@@ -4658,6 +4745,10 @@ getObjectTypeDescription(const ObjectAddress *object, bool missing_ok)
 
+		case OCLASS_VARIABLE:
@@ -5765,6 +5856,10 @@ getObjectIdentityParts(const ObjectAddress *object,
 
 				if (objname)
@@ -6008,6 +6103,33 @@ getObjectIdentityParts(const ObjectAddress *object,
 			}
 			break;
 
+		case OCLASS_VARIABLE:
+			{
+				char	   *schema;
+				char	   *varname;
+				HeapTuple	tup;
+				Form_pg_variable varform;
+
+				tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(object->objectId));
+				if (!HeapTupleIsValid(tup))
+					elog(ERROR, "cache lookup failed for session variable %u",
+						 object->objectId);
+
+				varform = (Form_pg_variable) GETSTRUCT(tup);
+
+				schema = get_namespace_name_or_temp(varform->varnamespace);
+				varname = NameStr(varform->varname);
+
+				appendStringInfo(&buffer, "%s",
+								 quote_qualified_identifier(schema, varname));
+
+				if (objname)
+					*objname = list_make2(schema, varname);
+
+				ReleaseSysCache(tup);
+				break;
+			}
+
 			/*
 			 * There's intentionally no default: case here; we want the
 			 * compiler to warn if a new OCLASS hasn't been handled above.
diff --git a/src/backend/catalog/pg_shdepend.c b/src/backend/catalog/pg_shdepend.c
index f2f227f887..de34174a92 100644
--- a/src/backend/catalog/pg_shdepend.c
+++ b/src/backend/catalog/pg_shdepend.c
@@ -47,6 +47,7 @@
 #include "catalog/pg_ts_dict.h"
 #include "catalog/pg_type.h"
 #include "catalog/pg_user_mapping.h"
+#include "catalog/pg_variable.h"
 #include "commands/alter.h"
 #include "commands/collationcmds.h"
 #include "commands/conversioncmds.h"
@@ -1604,6 +1605,7 @@ shdepReassignOwned(List *roleids, Oid newrole)
 				case DatabaseRelationId:
 				case TSConfigRelationId:
 				case TSDictionaryRelationId:
+				case VariableRelationId:
 					{
 						Oid			classId = sdepForm->classid;
 						Relation	catalog;
diff --git a/src/backend/catalog/pg_variable.c b/src/backend/catalog/pg_variable.c
new file mode 100644
index 0000000000..e8655d71ec
--- /dev/null
+++ b/src/backend/catalog/pg_variable.c
@@ -0,0 +1,233 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_variable.c
+ *		session variables
+ *
+ * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *		src/backend/catalog/pg_variable.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/heapam.h"
+#include "access/htup_details.h"
+
+#include "catalog/catalog.h"
+#include "catalog/dependency.h"
+#include "catalog/indexing.h"
+#include "catalog/namespace.h"
+#include "catalog/objectaccess.h"
+#include "catalog/pg_collation.h"
+#include "catalog/pg_namespace.h"
+#include "catalog/pg_type.h"
+#include "catalog/pg_variable.h"
+#include "storage/lmgr.h"
+#include "utils/builtins.h"
+#include "utils/lsyscache.h"
+#include "utils/syscache.h"
+
+/*
+ * Fetch attributes (without acl) of session variable from the syscache.
+ * We don't work with acl directly, so we don't need to read it here.
+ * Skip deserialization of defexpr when fast_only is true.
+ */
+void
+initVariable(Variable *var, Oid varid, bool fast_only)
+{
+	HeapTuple	tup;
+	Form_pg_variable varform;
+	Datum		defexpr_datum;
+	bool		defexpr_isnull;
+
+	tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid));
+
+	if (!HeapTupleIsValid(tup))
+		elog(ERROR, "cache lookup failed for session variable %u", varid);
+
+	varform = (Form_pg_variable) GETSTRUCT(tup);
+
+	var->oid = varid;
+	var->varname = pstrdup(NameStr(varform->varname));
+	var->varnamespace = varform->varnamespace;
+	var->typid = varform->vartype;
+	var->typmod = varform->vartypmod;
+	var->owner = varform->varowner;
+	var->collation = varform->varcollation;
+	var->is_immutable = varform->varisimmutable;
+	var->is_not_null = varform->varisnotnull;
+	var->eoxaction = varform->vareoxaction;
+
+	/* Get defexpr */
+	defexpr_datum = SysCacheGetAttr(VARIABLEOID,
+									tup,
+									Anum_pg_variable_vardefexpr,
+									&defexpr_isnull);
+
+	var->has_defexpr = !defexpr_isnull;
+
+	/*
+	 * Deserialize defexpr only when it is requested.
+	 * We need to deserialize Node with default expression,
+	 * only when we read from session variable, and this
+	 * session variable has not assigned value, and this
+	 * session variable has default expression. For other
+	 * cases, we skip skip this operation.
+	 */
+	if (!fast_only && !defexpr_isnull)
+		var->defexpr = stringToNode(TextDatumGetCString(defexpr_datum));
+	else
+		var->defexpr = NULL;
+
+	ReleaseSysCache(tup);
+}
+
+/*
+ * Create entry in pg_variable table
+ */
+ObjectAddress
+VariableCreate(const char *varName,
+			   Oid varNamespace,
+			   Oid varType,
+			   int32 varTypmod,
+			   Oid varOwner,
+			   Oid varCollation,
+			   Node *varDefexpr,
+			   VariableEOXAction eoxaction,
+			   bool is_not_null,
+			   bool if_not_exists,
+			   bool is_immutable)
+{
+	Acl		   *varacl;
+	NameData	varname;
+	bool		nulls[Natts_pg_variable];
+	Datum		values[Natts_pg_variable];
+	Relation	rel;
+	HeapTuple	tup;
+	TupleDesc	tupdesc;
+	ObjectAddress myself,
+				referenced;
+	ObjectAddresses *addrs;
+	Oid			varid;
+
+	AssertArg(varName);
+	AssertArg(OidIsValid(varNamespace));
+	AssertArg(OidIsValid(varType));
+	AssertArg(OidIsValid(varOwner));
+
+	rel = table_open(VariableRelationId, RowExclusiveLock);
+
+	/*
+	 * Check for duplicates. Note that this does not really prevent
+	 * duplicates, it's here just to provide nicer error message in common
+	 * case. The real protection is the unique key on the catalog.
+	 */
+	if (SearchSysCacheExists2(VARIABLENAMENSP,
+							  PointerGetDatum(varName),
+							  ObjectIdGetDatum(varNamespace)))
+	{
+		if (if_not_exists)
+			ereport(NOTICE,
+					(errcode(ERRCODE_DUPLICATE_OBJECT),
+					 errmsg("session variable \"%s\" already exists, skipping",
+							varName)));
+		else
+			ereport(ERROR,
+					(errcode(ERRCODE_DUPLICATE_OBJECT),
+					 errmsg("session variable \"%s\" already exists",
+							varName)));
+
+		table_close(rel, RowExclusiveLock);
+
+		return InvalidObjectAddress;
+	}
+
+	memset(values, 0, sizeof(values));
+	memset(nulls, false, sizeof(nulls));
+
+	namestrcpy(&varname, varName);
+
+	varid = GetNewOidWithIndex(rel, VariableObjectIndexId, Anum_pg_variable_oid);
+
+	values[Anum_pg_variable_oid - 1] = ObjectIdGetDatum(varid);
+	values[Anum_pg_variable_varname - 1] = NameGetDatum(&varname);
+	values[Anum_pg_variable_varnamespace - 1] = ObjectIdGetDatum(varNamespace);
+	values[Anum_pg_variable_vartype - 1] = ObjectIdGetDatum(varType);
+	values[Anum_pg_variable_vartypmod - 1] = Int32GetDatum(varTypmod);
+	values[Anum_pg_variable_varowner - 1] = ObjectIdGetDatum(varOwner);
+	values[Anum_pg_variable_varcollation - 1] = ObjectIdGetDatum(varCollation);
+	values[Anum_pg_variable_varisnotnull - 1] = BoolGetDatum(is_not_null);
+	values[Anum_pg_variable_varisimmutable - 1] = BoolGetDatum(is_immutable);
+	values[Anum_pg_variable_vareoxaction - 1] = CharGetDatum(eoxaction);
+
+	/* varacl will be determined later */
+
+	if (varDefexpr)
+		values[Anum_pg_variable_vardefexpr - 1] = CStringGetTextDatum(nodeToString(varDefexpr));
+	else
+		nulls[Anum_pg_variable_vardefexpr - 1] = true;
+
+	tupdesc = RelationGetDescr(rel);
+
+	varacl = get_user_default_acl(OBJECT_VARIABLE, varOwner,
+								  varNamespace);
+
+	if (varacl != NULL)
+		values[Anum_pg_variable_varacl - 1] = PointerGetDatum(varacl);
+	else
+		nulls[Anum_pg_variable_varacl - 1] = true;
+
+	tup = heap_form_tuple(tupdesc, values, nulls);
+	CatalogTupleInsert(rel, tup);
+	Assert(OidIsValid(varid));
+
+	addrs = new_object_addresses();
+
+	ObjectAddressSet(myself, VariableRelationId, varid);
+
+	/* dependency on namespace */
+	ObjectAddressSet(referenced, NamespaceRelationId, varNamespace);
+	add_exact_object_address(&referenced, addrs);
+
+	/* dependency on used type */
+	ObjectAddressSet(referenced, TypeRelationId, varType);
+	add_exact_object_address(&referenced, addrs);
+
+	/* dependency on collation */
+	if (OidIsValid(varCollation) &&
+		varCollation != DEFAULT_COLLATION_OID)
+	{
+		ObjectAddressSet(referenced, CollationRelationId, varCollation);
+		add_exact_object_address(&referenced, addrs);
+	}
+
+	/* dependency on default expr */
+	if (varDefexpr)
+		recordDependencyOnExpr(&myself, (Node *) varDefexpr,
+							   NIL, DEPENDENCY_NORMAL);
+
+	record_object_address_dependencies(&myself, addrs, DEPENDENCY_NORMAL);
+	free_object_addresses(addrs);
+
+	/* dependency on owner */
+	recordDependencyOnOwner(VariableRelationId, varid, varOwner);
+
+	/* dependencies on roles mentioned in default ACL */
+	recordDependencyOnNewAcl(VariableRelationId, varid, 0, varOwner, varacl);
+
+	/* dependency on extension */
+	recordDependencyOnCurrentExtension(&myself, false);
+
+	heap_freetuple(tup);
+
+	/* Post creation hook for new function */
+	InvokeObjectPostCreateHook(VariableRelationId, varid, 0);
+
+	table_close(rel, RowExclusiveLock);
+
+	return myself;
+}
diff --git a/src/backend/commands/Makefile b/src/backend/commands/Makefile
index 48f7348f91..41f98f8ba0 100644
--- a/src/backend/commands/Makefile
+++ b/src/backend/commands/Makefile
@@ -48,6 +48,7 @@ OBJS = \
 	proclang.o \
 	publicationcmds.o \
 	schemacmds.o \
+	sessionvariable.o \
 	seclabel.o \
 	sequence.o \
 	statscmds.o \
diff --git a/src/backend/commands/alter.c b/src/backend/commands/alter.c
index 55219bb097..c39a278758 100644
--- a/src/backend/commands/alter.c
+++ b/src/backend/commands/alter.c
@@ -40,6 +40,7 @@
 #include "catalog/pg_ts_dict.h"
 #include "catalog/pg_ts_parser.h"
 #include "catalog/pg_ts_template.h"
+#include "catalog/pg_variable.h"
 #include "commands/alter.h"
 #include "commands/collationcmds.h"
 #include "commands/conversioncmds.h"
@@ -141,6 +142,10 @@ report_namespace_conflict(Oid classId, const char *name, Oid nspOid)
 			Assert(OidIsValid(nspOid));
 			msgfmt = gettext_noop("text search configuration \"%s\" already exists in schema \"%s\"");
 			break;
+		case VariableRelationId:
+			Assert(OidIsValid(nspOid));
+			msgfmt = gettext_noop("session variable \"%s\" already exists in schema \"%s\"");
+			break;
 		default:
 			elog(ERROR, "unsupported object class %u", classId);
 			break;
@@ -393,6 +398,7 @@ ExecRenameStmt(RenameStmt *stmt)
 		case OBJECT_TSTEMPLATE:
 		case OBJECT_PUBLICATION:
 		case OBJECT_SUBSCRIPTION:
+		case OBJECT_VARIABLE:
 			{
 				ObjectAddress address;
 				Relation	catalog;
@@ -536,6 +542,7 @@ ExecAlterObjectSchemaStmt(AlterObjectSchemaStmt *stmt,
 		case OBJECT_TSDICTIONARY:
 		case OBJECT_TSPARSER:
 		case OBJECT_TSTEMPLATE:
+		case OBJECT_VARIABLE:
 			{
 				Relation	catalog;
 				Relation	relation;
@@ -626,6 +633,7 @@ AlterObjectNamespace_oid(Oid classId, Oid objid, Oid nspOid,
 		case OCLASS_TSDICT:
 		case OCLASS_TSTEMPLATE:
 		case OCLASS_TSCONFIG:
+		case OCLASS_VARIABLE:
 			{
 				Relation	catalog;
 
@@ -886,6 +894,7 @@ ExecAlterOwnerStmt(AlterOwnerStmt *stmt)
 		case OBJECT_TABLESPACE:
 		case OBJECT_TSDICTIONARY:
 		case OBJECT_TSCONFIGURATION:
+		case OBJECT_VARIABLE:
 			{
 				Relation	catalog;
 				Relation	relation;
diff --git a/src/backend/commands/dropcmds.c b/src/backend/commands/dropcmds.c
index 26157eb4e3..b55b3b0528 100644
--- a/src/backend/commands/dropcmds.c
+++ b/src/backend/commands/dropcmds.c
@@ -480,6 +480,10 @@ does_not_exist_skipping(ObjectType objtype, Node *object)
 			msg = gettext_noop("publication \"%s\" does not exist, skipping");
 			name = strVal(object);
 			break;
+		case OBJECT_VARIABLE:
+			msg = gettext_noop("session variable \"%s\" does not exist, skipping");
+			name = NameListToString(castNode(List, object));
+			break;
 		default:
 			elog(ERROR, "unrecognized object type: %d", (int) objtype);
 			break;
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index 635d05405e..8739703c39 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -991,6 +991,7 @@ EventTriggerSupportsObjectType(ObjectType obtype)
 		case OBJECT_TSTEMPLATE:
 		case OBJECT_TYPE:
 		case OBJECT_USER_MAPPING:
+		case OBJECT_VARIABLE:
 		case OBJECT_VIEW:
 			return true;
 
@@ -1057,6 +1058,7 @@ EventTriggerSupportsObjectClass(ObjectClass objclass)
 		case OCLASS_PUBLICATION_REL:
 		case OCLASS_SUBSCRIPTION:
 		case OCLASS_TRANSFORM:
+		case OCLASS_VARIABLE:
 			return true;
 
 			/*
@@ -2049,6 +2051,8 @@ stringify_grant_objtype(ObjectType objtype)
 			return "TABLESPACE";
 		case OBJECT_TYPE:
 			return "TYPE";
+		case OBJECT_VARIABLE:
+			return "VARIABLE";
 			/* these currently aren't used */
 		case OBJECT_ACCESS_METHOD:
 		case OBJECT_AGGREGATE:
@@ -2132,6 +2136,8 @@ stringify_adefprivs_objtype(ObjectType objtype)
 			return "TABLESPACES";
 		case OBJECT_TYPE:
 			return "TYPES";
+		case OBJECT_VARIABLE:
+			return "VARIABLES";
 			/* these currently aren't used */
 		case OBJECT_ACCESS_METHOD:
 		case OBJECT_AGGREGATE:
diff --git a/src/backend/commands/seclabel.c b/src/backend/commands/seclabel.c
index 7ae19b98bb..9b11374535 100644
--- a/src/backend/commands/seclabel.c
+++ b/src/backend/commands/seclabel.c
@@ -92,6 +92,7 @@ SecLabelSupportsObjectType(ObjectType objtype)
 		case OBJECT_TSPARSER:
 		case OBJECT_TSTEMPLATE:
 		case OBJECT_USER_MAPPING:
+		case OBJECT_VARIABLE:
 			return false;
 
 			/*
diff --git a/src/backend/commands/sessionvariable.c b/src/backend/commands/sessionvariable.c
new file mode 100644
index 0000000000..3094bab82a
--- /dev/null
+++ b/src/backend/commands/sessionvariable.c
@@ -0,0 +1,352 @@
+/*-------------------------------------------------------------------------
+ *
+ * sessionvariable.c
+ *	  session variable creation/manipulation commands
+ *
+ * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/commands/sessionvariable.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+#include "miscadmin.h"
+#include "access/heapam.h"
+#include "access/htup_details.h"
+#include "access/xact.h"
+#include "catalog/dependency.h"
+#include "catalog/indexing.h"
+#include "catalog/namespace.h"
+#include "catalog/pg_variable.h"
+#include "commands/session_variable.h"
+#include "executor/executor.h"
+#include "optimizer/optimizer.h"
+#include "parser/parse_coerce.h"
+#include "parser/parse_collate.h"
+#include "parser/parse_expr.h"
+#include "parser/parse_type.h"
+#include "utils/builtins.h"
+#include "utils/lsyscache.h"
+#include "utils/syscache.h"
+
+/*
+ * The life cycle of temporary session variable can be
+ * limmited by using clause ON COMMIT DROP.
+ */
+typedef enum SVariableXActAction
+{
+	SVAR_ON_COMMIT_DROP,		/* used for ON COMMIT DROP */
+} SVariableXActAction;
+
+typedef struct SVariableXActActionItem
+{
+	Oid			varid;			/* varid of session variable */
+
+	/*
+	 * creating_subid is the ID of the creating subxact. If the action was
+	 * unregistered during the current transaction, deleting_subid is the ID of
+	 * the deleting subxact, otherwise InvalidSubTransactionId.
+	 */
+	SubTransactionId creating_subid;
+	SubTransactionId deleting_subid;
+}  SVariableXActActionItem;
+
+/* List holds fields of SVariableXActActionItem type */
+static List *xact_drop_actions = NIL;
+
+static void register_session_variable_xact_action(Oid varid, SVariableXActAction action);
+static void unregister_session_variable_xact_action(Oid varid, SVariableXActAction action);
+
+/*
+ * Routines used for manipulation with session variables from
+ * SQL level
+ */
+
+/*
+ * Creates new variable - entry in pg_catalog.pg_variable table
+ *
+ * Used by CREATE VARIABLE command
+ */
+ObjectAddress
+DefineSessionVariable(ParseState *pstate, CreateSessionVarStmt *stmt)
+{
+	Oid			namespaceid;
+	AclResult	aclresult;
+	Oid			typid;
+	int32		typmod;
+	Oid			varowner = GetUserId();
+	Oid			collation;
+	Oid			typcollation;
+	ObjectAddress variable;
+
+	Node	   *cooked_default = NULL;
+
+	/*
+	 * Check consistency of arguments
+	 */
+	if (stmt->eoxaction == VARIABLE_EOX_DROP
+		&& stmt->variable->relpersistence != RELPERSISTENCE_TEMP)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+				 errmsg("ON COMMIT DROP can only be used on temporary variables")));
+
+	if (stmt->is_not_null && stmt->is_immutable && !stmt->defexpr)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+				 errmsg("IMMUTABLE NOT NULL variable requires default expression")));
+
+	namespaceid =
+		RangeVarGetAndCheckCreationNamespace(stmt->variable, NoLock, NULL);
+
+	typenameTypeIdAndMod(pstate, stmt->typeName, &typid, &typmod);
+	typcollation = get_typcollation(typid);
+
+	aclresult = pg_type_aclcheck(typid, GetUserId(), ACL_USAGE);
+	if (aclresult != ACLCHECK_OK)
+		aclcheck_error_type(aclresult, typid);
+
+	if (stmt->collClause)
+		collation = LookupCollation(pstate,
+									stmt->collClause->collname,
+									stmt->collClause->location);
+	else
+		collation = typcollation;;
+
+	/* Complain if COLLATE is applied to an uncollatable type */
+	if (OidIsValid(collation) && !OidIsValid(typcollation))
+		ereport(ERROR,
+				(errcode(ERRCODE_DATATYPE_MISMATCH),
+				 errmsg("collations are not supported by type %s",
+						format_type_be(typid)),
+				 parser_errposition(pstate, stmt->collClause->location)));
+
+	if (stmt->defexpr)
+	{
+		cooked_default = transformExpr(pstate, stmt->defexpr,
+									   EXPR_KIND_VARIABLE_DEFAULT);
+
+		cooked_default = coerce_to_specific_type(pstate,
+												 cooked_default, typid, "DEFAULT");
+		assign_expr_collations(pstate, cooked_default);
+	}
+
+	variable = VariableCreate(stmt->variable->relname,
+							  namespaceid,
+							  typid,
+							  typmod,
+							  varowner,
+							  collation,
+							  cooked_default,
+							  stmt->eoxaction,
+							  stmt->is_not_null,
+							  stmt->if_not_exists,
+							  stmt->is_immutable);
+
+	elog(DEBUG1, "record for session variable \"%s\" (oid:%d) was created in pg_variable",
+				stmt->variable->relname, variable.objectId);
+
+	/*
+	 * For temporary variables, we need to create a new end of xact action to
+	 * ensure deletion from catalog.
+	 */
+	if (stmt->eoxaction == VARIABLE_EOX_DROP)
+	{
+		Assert(isTempNamespace(namespaceid));
+
+		register_session_variable_xact_action(variable.objectId,
+											  SVAR_ON_COMMIT_DROP);
+	}
+
+	/*
+	 * We must bump the command counter to make the newly-created variable
+	 * tuple visible for any other operations.
+	 */
+	CommandCounterIncrement();
+
+	return variable;
+}
+
+/*
+ * Drop variable by OID. This routine doesn't try to remove
+ * the value of session variable immediately. It will be
+ * removed on transaction end in sync_sessionvars_xact_callback
+ * routine. This routine manipulate just with system catalog.
+ */
+void
+RemoveSessionVariable(Oid varid)
+{
+	Relation	rel;
+	HeapTuple	tup;
+
+	elog(DEBUG1, "session variable (oid:%u) is deleted from pg_variable", varid);
+
+	rel = table_open(VariableRelationId, RowExclusiveLock);
+
+	tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid));
+
+	if (!HeapTupleIsValid(tup))
+		elog(ERROR, "cache lookup failed for variable %u", varid);
+
+	CatalogTupleDelete(rel, &tup->t_self);
+
+	ReleaseSysCache(tup);
+
+	table_close(rel, RowExclusiveLock);
+
+	/*
+	 * We removed entry from catalog already, we should not do it
+	 * again at end of xact time.
+	 */
+	unregister_session_variable_xact_action(varid, SVAR_ON_COMMIT_DROP);
+}
+
+/*
+ * Registration of actions to be executed on session variables at transaction
+ * end time. We want to drop temporary session variables with clause ON COMMIT
+ * DROP, or we want to reset values of session variables with clause ON
+ * TRANSACTION END RESET or we want to clean (reset) local memory allocated by
+ * values of dropped session variables.
+ */
+
+/*
+ * Register a session variable xact action.
+ */
+static void
+register_session_variable_xact_action(Oid varid,
+									  SVariableXActAction action)
+{
+	SVariableXActActionItem *xact_ai;
+	MemoryContext oldcxt;
+
+	oldcxt = MemoryContextSwitchTo(CacheMemoryContext);
+
+	xact_ai = (SVariableXActActionItem *)
+							palloc(sizeof(SVariableXActActionItem));
+
+	xact_ai->varid = varid;
+
+	xact_ai->creating_subid = GetCurrentSubTransactionId();
+	xact_ai->deleting_subid = InvalidSubTransactionId;
+
+	Assert(action == SVAR_ON_COMMIT_DROP);
+	xact_drop_actions = lcons(xact_ai, xact_drop_actions);
+
+	MemoryContextSwitchTo(oldcxt);
+}
+
+/*
+ * Unregister an action on a given session variable from action list. In this
+ * moment, the action is just marked as deleted by setting deleting_subid. The
+ * calling even might be rollbacked, in which case we should not lose this
+ * action.
+ */
+static void
+unregister_session_variable_xact_action(Oid varid,
+									SVariableXActAction action)
+{
+	ListCell   *l;
+
+	Assert(action == SVAR_ON_COMMIT_DROP);
+
+	foreach(l, xact_drop_actions)
+	{
+		SVariableXActActionItem *xact_ai =
+					(SVariableXActActionItem *) lfirst(l);
+
+		if (xact_ai->varid == varid)
+			xact_ai->deleting_subid = GetCurrentSubTransactionId();
+	}
+}
+
+/*
+ * Perform ON TRANSACTION END RESET or ON COMMIT DROP
+ * and COMMIT/ROLLBACK of transaction session variables.
+ */
+void
+AtPreEOXact_SessionVariable_on_xact_actions(bool isCommit)
+{
+	ListCell   *l;
+
+	foreach(l, xact_drop_actions)
+	{
+		SVariableXActActionItem *xact_ai =
+							(SVariableXActActionItem *) lfirst(l);
+
+		/* Iterate only over entries that are still pending */
+		if (xact_ai->deleting_subid == InvalidSubTransactionId)
+		{
+
+			/*
+			 * ON COMMIT DROP is allowed only for temp session
+			 * variables. So we should explicitly delete only when
+			 * current transaction was committed. When it's rollback,
+			 * then session variable is removed automatically.
+			 */
+			if (isCommit)
+			{
+				ObjectAddress object;
+
+				object.classId = VariableRelationId;
+				object.objectId = xact_ai->varid;
+				object.objectSubId = 0;
+
+				/*
+				 * Since this is an automatic drop, rather than one
+				 * directly initiated by the user, we pass the
+				 * PERFORM_DELETION_INTERNAL flag.
+				 */
+				elog(DEBUG1, "session variable (oid:%u) will be deleted (forced by SVAR_ON_COMMIT_DROP action)",
+							  xact_ai->varid);
+
+				performDeletion(&object, DROP_CASCADE,
+								PERFORM_DELETION_INTERNAL |
+								PERFORM_DELETION_QUIETLY);
+			}
+		}
+	}
+
+	/*
+	 * Any drop action left is an entry that was unregistered and not
+	 * rollbacked, so we can simply remove them.
+	 */
+	list_free_deep(xact_drop_actions);
+	xact_drop_actions = NIL;
+}
+
+/*
+ * Post-subcommit or post-subabort cleanup of xact action list.
+ *
+ * During subabort, we can immediately remove entries created during this
+ * subtransaction. During subcommit, just transfer entries marked during
+ * this subtransaction as being the parent's responsibility.
+ */
+void
+AtEOSubXact_SessionVariable_on_xact_actions(bool isCommit, SubTransactionId mySubid,
+											SubTransactionId parentSubid)
+{
+	ListCell   *cur_item;
+
+	foreach(cur_item, xact_drop_actions)
+	{
+		SVariableXActActionItem *xact_ai =
+								  (SVariableXActActionItem *) lfirst(cur_item);
+
+		if (!isCommit && xact_ai->creating_subid == mySubid)
+		{
+			/* cur_item must be removed */
+			xact_drop_actions = foreach_delete_current(xact_drop_actions, cur_item);
+			pfree(xact_ai);
+		}
+		else
+		{
+			/* cur_item must be preserved */
+			if (xact_ai->creating_subid == mySubid)
+				xact_ai->creating_subid = parentSubid;
+			if (xact_ai->deleting_subid == mySubid)
+				xact_ai->deleting_subid = isCommit ? parentSubid : InvalidSubTransactionId;
+		}
+	}
+}
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 9be04c8a1e..f711e749b3 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -47,6 +47,7 @@
 #include "catalog/pg_tablespace.h"
 #include "catalog/pg_trigger.h"
 #include "catalog/pg_type.h"
+#include "catalog/pg_variable.h"
 #include "catalog/storage.h"
 #include "catalog/storage_xlog.h"
 #include "catalog/toasting.h"
@@ -6435,6 +6436,8 @@ ATTypedTableRecursion(List **wqueue, Relation rel, AlterTableCmd *cmd,
  * Eventually, we'd like to propagate the check or rewrite operation
  * into such tables, but for now, just error out if we find any.
  *
+ * Check if the type "typeOid" is used as type of some session variable too.
+ *
  * Caller should provide either the associated relation of a rowtype,
  * or a type name (not both) for use in the error message, if any.
  *
@@ -6496,9 +6499,30 @@ find_composite_type_dependencies(Oid typeOid, Relation origRelation,
 			continue;
 		}
 
+		/* Don't allow change of type usedby session's variable */
+		if (pg_depend->classid == VariableRelationId)
+		{
+			Oid		varid = pg_depend->objid;
+
+			if (origTypeName)
+				ereport(ERROR,
+						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+						 errmsg("cannot alter type \"%s\" because session variable \"%s.%s\" uses it",
+								origTypeName,
+								get_namespace_name(get_session_variable_namespace(varid)),
+								get_session_variable_name(varid))));
+			else if (origRelation->rd_rel->relkind == RELKIND_COMPOSITE_TYPE)
+				ereport(ERROR,
+						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+						 errmsg("cannot alter type \"%s\" because session variable \"%s.%s\" uses it",
+								RelationGetRelationName(origRelation),
+								get_namespace_name(get_session_variable_namespace(varid)),
+								get_session_variable_name(varid))));
+		}
+
 		/* Else, ignore dependees that aren't user columns of relations */
 		/* (we assume system columns are never of interesting types) */
-		if (pg_depend->classid != RelationRelationId ||
+		else if (pg_depend->classid != RelationRelationId ||
 			pg_depend->objsubid <= 0)
 			continue;
 
@@ -12682,6 +12706,7 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel,
 			case OCLASS_PUBLICATION_REL:
 			case OCLASS_SUBSCRIPTION:
 			case OCLASS_TRANSFORM:
+			case OCLASS_VARIABLE:
 
 				/*
index f9037761f9..facbd5f475 100644
@@ -53,6 +53,7 @@
 #include "catalog/pg_am.h"
@@ -304,8 +305,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
@@ -482,6 +483,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
+%type <ival>	 OnEOXActionOption
 
@@ -651,6 +653,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);

 %type <node>		json_format_clause_opt
@@ -857,8 +861,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	UESCAPE UNBOUNDED UNCONDITIONAL UNCOMMITTED UNENCRYPTED UNION UNIQUE UNKNOWN
 	UNLISTEN UNLOGGED UNTIL UPDATE USER USING

 
@@ -1116,6 +1120,7 @@ stmt:
 			| CreatePolicyStmt
 			| CreatePLangStmt
 			| CreateSchemaStmt
+			| CreateSessionVarStmt
 			| CreateSeqStmt
 			| CreateStmt
 			| CreateSubscriptionStmt
@@ -5124,6 +5129,69 @@ create_extension_opt_item:
 				}
 		;
 
+ * transaction end like tables.  RESET can only be forced on
+ * transaction end. Since the session variables are not
+ * transactional, we have to handle ROLLBACK too.
+ * The clause ON TRANSACTION END is clearer than some
+ * ON COMMIT ROLLBACK RESET clause.
@@ -6901,6 +6969,7 @@ object_type_any_name:
 
 /*
@@ -7777,6 +7846,14 @@ privilege_target:
 					n->objs = $2;
 					$$ = n;
 				}
@@ -7822,6 +7899,14 @@ privilege_target:

@@ -7989,6 +8074,7 @@ defacl_privilege_target:

@@ -9771,6 +9857,25 @@ RenameStmt: ALTER AGGREGATE aggregate_with_argtypes RENAME TO name
 					n->missing_ok = false;
 					$$ = (Node *) n;
 				}
 
 opt_column: COLUMN
@@ -10132,6 +10237,25 @@ AlterObjectSchemaStmt:
 					n->missing_ok = false;
 					$$ = (Node *) n;
 				}
 
@@ -10411,6 +10535,14 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec

@@ -17939,6 +18071,8 @@ unreserved_keyword:
 			| VALIDATE
 			| VALIDATOR
 			| VALUE_P
+			| VARIABLE
+			| VARIABLES
 			| VARYING
 			| VERSION_P
 			| VIEW
@@ -18585,6 +18719,8 @@ bare_label_keyword:
 			| VALUE_P
 			| VALUES
 			| VARCHAR
+			| VARIABLE
+			| VARIABLES
 			| VARIADIC
 			| VERBOSE
 			| VERSION_P
diff --git a/src/backend/parser/parse_agg.c b/src/backend/parser/parse_agg.c
index 3ef9e8ee5e..dbe630a51b 100644
--- a/src/backend/parser/parse_agg.c
+++ b/src/backend/parser/parse_agg.c
@@ -471,6 +471,7 @@ check_agglevels_and_constraints(ParseState *pstate, Node *expr)
 			break;
 		case EXPR_KIND_COLUMN_DEFAULT:
 		case EXPR_KIND_FUNCTION_DEFAULT:
+		case EXPR_KIND_VARIABLE_DEFAULT:
 
 			if (isAgg)
@@ -915,6 +916,7 @@ transformWindowFuncCall(ParseState *pstate, WindowFunc *wfunc,
index fabb5f7207..aa1b1a76dc 100644
@@ -18,6 +18,7 @@
 #include "catalog/pg_aggregate.h"
@@ -567,6 +568,8 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
 
@@ -1788,6 +1791,7 @@ transformSubLink(ParseState *pstate, SubLink *sublink)
 			break;
 		case EXPR_KIND_COLUMN_DEFAULT:
 		case EXPR_KIND_FUNCTION_DEFAULT:
+		case EXPR_KIND_VARIABLE_DEFAULT:
 			err = _("cannot use subquery in DEFAULT expression");
 			break;
 		case EXPR_KIND_INDEX_EXPRESSION:
@@ -3128,6 +3132,7 @@ ParseExprKindName(ParseExprKind exprKind)
 			return "CHECK";
 		case EXPR_KIND_COLUMN_DEFAULT:
 		case EXPR_KIND_FUNCTION_DEFAULT:
+		case EXPR_KIND_VARIABLE_DEFAULT:
 			return "DEFAULT";
 		case EXPR_KIND_INDEX_EXPRESSION:
 			return "index expression";
diff --git a/src/backend/parser/parse_func.c b/src/backend/parser/parse_func.c
index 827989f379..005bc0400f 100644
--- a/src/backend/parser/parse_func.c
+++ b/src/backend/parser/parse_func.c
@@ -2621,6 +2621,7 @@ check_srf_call_placement(ParseState *pstate, Node *last_srf, int location)
 			break;
 		case EXPR_KIND_COLUMN_DEFAULT:
 		case EXPR_KIND_FUNCTION_DEFAULT:
+		case EXPR_KIND_VARIABLE_DEFAULT:
 			err = _("set-returning functions are not allowed in DEFAULT expressions");
 			break;
 		case EXPR_KIND_INDEX_EXPRESSION:
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 6b0a865262..ffe1caafc8 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -49,6 +49,7 @@
 #include "commands/proclang.h"
 #include "commands/publicationcmds.h"
 #include "commands/schemacmds.h"
+#include "commands/session_variable.h"
 #include "commands/seclabel.h"
 #include "commands/sequence.h"
 #include "commands/subscriptioncmds.h"
@@ -189,6 +190,7 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree)
 		case T_CreateRangeStmt:
 		case T_CreateRoleStmt:
 		case T_CreateSchemaStmt:
+		case T_CreateSessionVarStmt:
 		case T_CreateSeqStmt:
 		case T_CreateStatsStmt:
 		case T_CreateStmt:
@@ -1393,6 +1395,10 @@ ProcessUtilitySlow(ParseState *pstate,
 				}
 				break;
 
+			case T_CreateSessionVarStmt:
+				address = DefineSessionVariable(pstate, (CreateSessionVarStmt *) parsetree);
+				break;
+
 				/*
 				 * ************* object creation / destruction **************
 				 */
@@ -2332,6 +2338,9 @@ AlterObjectTypeCommandTag(ObjectType objtype)
 		case OBJECT_STATISTIC_EXT:
 			tag = CMDTAG_ALTER_STATISTICS;
 			break;
+		case OBJECT_VARIABLE:
+			tag = CMDTAG_ALTER_VARIABLE;
+			break;
 		default:
 			tag = CMDTAG_UNKNOWN;
 			break;
@@ -2640,6 +2649,9 @@ CreateCommandTag(Node *parsetree)
 				case OBJECT_STATISTIC_EXT:
 					tag = CMDTAG_DROP_STATISTICS;
 					break;
+				case OBJECT_VARIABLE:
+					tag = CMDTAG_DROP_VARIABLE;
+					break;
 				default:
 					tag = CMDTAG_UNKNOWN;
 			}
@@ -3216,6 +3228,10 @@ CreateCommandTag(Node *parsetree)
 			}
 			break;
 
+		case T_CreateSessionVarStmt:
+			tag = CMDTAG_CREATE_VARIABLE;
+			break;
+
 		default:
 			elog(WARNING, "unrecognized node type: %d",
 				 (int) nodeTag(parsetree));
diff --git a/src/backend/utils/adt/acl.c b/src/backend/utils/adt/acl.c
index 6fa58dd8eb..ed85534ac3 100644
--- a/src/backend/utils/adt/acl.c
+++ b/src/backend/utils/adt/acl.c
@@ -806,6 +806,10 @@ acldefault(ObjectType objtype, Oid ownerId)
 			world_default = ACL_NO_RIGHTS;
 			owner_default = ACL_ALL_RIGHTS_PARAMETER_ACL;
 			break;
+		case OBJECT_VARIABLE:
+			world_default = ACL_NO_RIGHTS;
+			owner_default = ACL_ALL_RIGHTS_VARIABLE;
+			break;
 		default:
 			elog(ERROR, "unrecognized objtype: %d", (int) objtype);
 			world_default = ACL_NO_RIGHTS;	/* keep compiler quiet */
@@ -903,6 +907,9 @@ acldefault_sql(PG_FUNCTION_ARGS)
 		case 'T':
 			objtype = OBJECT_TYPE;
 			break;
+		case 'V':
+			objtype = OBJECT_VARIABLE;
+			break;
 		default:
 			elog(ERROR, "unrecognized objtype abbreviation: %c", objtypec);
 	}
@@ -1605,7 +1612,6 @@ makeaclitem(PG_FUNCTION_ARGS)
 	PG_RETURN_ACLITEM_P(result);
 }
 
-
 /*
  * convert_any_priv_string: recognize privilege strings for has_foo_privilege
  *
diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c
index a16a63f495..c73d491eca 100644
--- a/src/backend/utils/cache/lsyscache.c
+++ b/src/backend/utils/cache/lsyscache.c
@@ -36,6 +36,7 @@
 #include "catalog/pg_subscription.h"
 #include "catalog/pg_transform.h"
 #include "catalog/pg_type.h"
+#include "catalog/pg_variable.h"
 #include "miscadmin.h"
 #include "nodes/makefuncs.h"
 #include "utils/array.h"
@@ -3679,3 +3680,91 @@ get_subscription_name(Oid subid, bool missing_ok)
 
 	return subname;
 }
+
+/*				---------- PG_VARIABLE CACHE ----------				 */
+
+/*
+ * get_varname_varid
+ *		Given name and namespace of variable, look up the OID.
+ */
+Oid
+get_varname_varid(const char *varname, Oid varnamespace)
+{
+	return GetSysCacheOid2(VARIABLENAMENSP, Anum_pg_variable_oid,
+						   PointerGetDatum(varname),
+						   ObjectIdGetDatum(varnamespace));
+}
+
+/*
+ * get_session_variable_name
+ *		Returns a palloc'd copy of the name of a given session variable.
+ */
+char *
+get_session_variable_name(Oid varid)
+{
+	HeapTuple	tup;
+	Form_pg_variable varform;
+	char	   *varname;
+
+	tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid));
+
+	if (!HeapTupleIsValid(tup))
+		elog(ERROR, "cache lookup failed for session variable %u", varid);
+
+	varform = (Form_pg_variable) GETSTRUCT(tup);
+
+	varname = pstrdup(NameStr(varform->varname));
+
+	ReleaseSysCache(tup);
+
+	return varname;
+}
+
+/*
+ * get_session_variable_namespace
+ *		Returns the pg_namespace OID associated with a given session variable.
+ */
+Oid
+get_session_variable_namespace(Oid varid)
+{
+	HeapTuple	tup;
+	Form_pg_variable varform;
+	Oid			varnamespace;
+
+	tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid));
+
+	if (!HeapTupleIsValid(tup))
+		elog(ERROR, "cache lookup failed for variable %u", varid);
+
+	varform = (Form_pg_variable) GETSTRUCT(tup);
+
+	varnamespace = varform->varnamespace;
+
+	ReleaseSysCache(tup);
+
+	return varnamespace;
+}
+
+/*
+ * Returns the type, typmod and collid of the given session variable.
+ */
+void
+get_session_variable_type_typmod_collid(Oid varid, Oid *typid, int32 *typmod,
+										Oid *collid)
+{
+	HeapTuple	tup;
+	Form_pg_variable varform;
+
+	tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid));
+
+	if (!HeapTupleIsValid(tup))
+		elog(ERROR, "cache lookup failed for session variable %u", varid);
+
+	varform = (Form_pg_variable) GETSTRUCT(tup);
+
+	*typid = varform->vartype;
+	*typmod = varform->vartypmod;
+	*collid = varform->varcollation;
+
+	ReleaseSysCache(tup);
+}
diff --git a/src/backend/utils/cache/syscache.c b/src/backend/utils/cache/syscache.c
index 1912b12146..7b16f75d1f 100644
--- a/src/backend/utils/cache/syscache.c
+++ b/src/backend/utils/cache/syscache.c
@@ -75,6 +75,7 @@
 #include "catalog/pg_ts_template.h"
 #include "catalog/pg_type.h"
 #include "catalog/pg_user_mapping.h"
+#include "catalog/pg_variable.h"
 #include "lib/qunique.h"
 #include "utils/catcache.h"
 #include "utils/rel.h"
@@ -1037,6 +1038,28 @@ static const struct cachedesc cacheinfo[] = {
 			0
 		},
 		2
+	},
+	{VariableRelationId,		/* VARIABLENAMENSP */
+		VariableNameNspIndexId,
+		2,
+		{
+			Anum_pg_variable_varname,
+			Anum_pg_variable_varnamespace,
+			0,
+			0
+		},
+		8
+	},
+	{VariableRelationId,		/* VARIABLEOID */
+		VariableObjectIndexId,
+		1,
+		{
+			Anum_pg_variable_oid,
+			0,
+			0,
+			0
+		},
+		8
 	}
 };
 
diff --git a/src/backend/utils/fmgr/fmgr.c b/src/backend/utils/fmgr/fmgr.c
index a9dd068095..af74fe345e 100644
--- a/src/backend/utils/fmgr/fmgr.c
+++ b/src/backend/utils/fmgr/fmgr.c
@@ -1896,9 +1896,9 @@ get_call_expr_arg_stable(Node *expr, int argnum)
 	arg = (Node *) list_nth(args, argnum);
 
 	/*
index 729c4c46c0..e7b74e3de3 100644
@@ -127,10 +127,11 @@ typedef enum ObjectClass

index 1bc55c01a5..c5d445b9e1 100644

+extern Oid	IdentifyVariable(List *names, char **attrname, bool lockit, bool *not_unique);
index 2a79155636..672c571562 100644

index be47583122..5055904005 100644
@@ -6274,6 +6274,9 @@
 
index 0000000000..d217cdee64
+ * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
+	VARIABLE_EOX_NOOP = 'n',	/* NOOP */
+	VARIABLE_EOX_DROP = 'd',	/* ON COMMIT DROP */
+	VARIABLE_EOX_RESET = 'r',	/* ON TRANSACTION END RESET */
+DECLARE_TOAST(pg_variable, 9223, 9224);
+DECLARE_UNIQUE_INDEX_PKEY(pg_variable_oid_index, 9225, VariableOidIndexId, on pg_variable using btree(oid oid_ops));
+#define VariableObjectIndexId 9225
+DECLARE_UNIQUE_INDEX(pg_variable_varname_nsp_index, 9226, VariableNameNspIndexId,  on pg_variable using btree(varname name_ops, varnamespace oid_ops));
+#define VariableNameNspIndexId  9226
index 0000000000..83b8afcd6c
@@ -0,0 +1,41 @@
+ * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
+extern Datum CopySessionVariableWithTypeCheck(Oid varid, bool *isNull, Oid expected_typid);
+extern Datum GetSessionVariableWithTypeCheck(Oid varid, bool *isNull, Oid expected_typid);
index b376031856..a10f3b2454 100644
@@ -2194,6 +2194,7 @@ typedef enum ObjectType
 	OBJECT_TSTEMPLATE,
 	OBJECT_TYPE,
 	OBJECT_USER_MAPPING,
+	OBJECT_VARIABLE,
 	OBJECT_VIEW
 } ObjectType;
 
@@ -3321,6 +3322,25 @@ typedef struct AlterStatsStmt
 	bool		missing_ok;		/* skip error if statistics object is missing */
 } AlterStatsStmt;
 
+
+	char		eoxaction;		/* on commit action */
  *		Create Function Statement
index ae35f03251..d7cfc9a6f3 100644
@@ -479,6 +479,8 @@ PG_KEYWORD("validator", VALIDATOR, UNRESERVED_KEYWORD, BARE_LABEL)
index 962ebf65de..4d65a9e9e8 100644
@@ -81,6 +81,7 @@ typedef enum ParseExprKind
+	EXPR_KIND_VARIABLE_DEFAULT	/* default value for session variable */
 } ParseExprKind;

index 9e94f44c5f..6c1be5e9d4 100644
@@ -175,6 +177,7 @@ PG_CMDTAG(CMDTAG_DROP_TRANSFORM, "DROP TRANSFORM", true, false, false)
index 48f7d72add..dbc310740f 100644
@@ -168,6 +168,7 @@ typedef struct ArrayType Acl;
+#define ACL_ALL_RIGHTS_VARIABLE		(ACL_SELECT|ACL_UPDATE)
 
 /* operation codes for pg_*_aclmask */
 typedef enum
@@ -266,7 +267,8 @@ extern AclMode pg_foreign_server_aclmask(Oid srv_oid, Oid roleid,
 										 AclMode mask, AclMaskHow how);
 extern AclMode pg_type_aclmask(Oid type_oid, Oid roleid,
 							   AclMode mask, AclMaskHow how);
-
+extern AclMode pg_variable_aclmask(Oid var_oid, Oid roleid,
+								   AclMode mask, AclMaskHow how);
 extern AclResult pg_attribute_aclcheck(Oid table_oid, AttrNumber attnum,
 									   Oid roleid, AclMode mode);
 extern AclResult pg_attribute_aclcheck_ext(Oid table_oid, AttrNumber attnum,
@@ -291,6 +293,7 @@ extern AclResult pg_tablespace_aclcheck(Oid spc_oid, Oid roleid, AclMode mode);
 extern AclResult pg_foreign_data_wrapper_aclcheck(Oid fdw_oid, Oid roleid, AclMode mode);
 extern AclResult pg_foreign_server_aclcheck(Oid srv_oid, Oid roleid, AclMode mode);
 extern AclResult pg_type_aclcheck(Oid type_oid, Oid roleid, AclMode mode);
+extern AclResult pg_variable_aclcheck(Oid var_oid, Oid roleid, AclMode mode);
 
@@ -327,6 +330,7 @@ extern bool pg_extension_ownercheck(Oid ext_oid, Oid roleid);
 
diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h
index 50f0288305..8e2b733bb2 100644
--- a/src/include/utils/lsyscache.h
+++ b/src/include/utils/lsyscache.h
@@ -132,6 +132,7 @@ extern char get_func_prokind(Oid funcid);
 extern bool get_func_leakproof(Oid funcid);
 extern RegProcedure get_func_support(Oid funcid);
 extern Oid	get_relname_relid(const char *relname, Oid relnamespace);
+extern Oid	get_varname_varid(const char *varname, Oid varnamespace);
 extern char *get_rel_name(Oid relid);
 extern Oid	get_rel_namespace(Oid relid);
 extern Oid	get_rel_type_id(Oid relid);
@@ -203,6 +204,13 @@ extern char *get_publication_name(Oid pubid, bool missing_ok);
 extern Oid	get_subscription_oid(const char *subname, bool missing_ok);
 extern char *get_subscription_name(Oid subid, bool missing_ok);
 
+extern char *get_session_variable_name(Oid varid);
+extern Oid get_session_variable_namespace(Oid varid);
+extern void get_session_variable_type_typmod_collid(Oid varid,
+												   Oid *typid,
+												   int32 *typmod,
+												   Oid *collid);
+
 #define type_is_array(typid)  (get_element_type(typid) != InvalidOid)
 /* type_is_array_domain accepts both plain arrays and domains over arrays */
 #define type_is_array_domain(typid)  (get_base_element_type(typid) != InvalidOid)
diff --git a/src/include/utils/syscache.h b/src/include/utils/syscache.h
index 4463ea66be..3d18dc88ac 100644
--- a/src/include/utils/syscache.h
+++ b/src/include/utils/syscache.h
@@ -113,9 +113,11 @@ enum SysCacheIdentifier
 	TYPENAMENSP,
 	TYPEOID,
 	USERMAPPINGOID,
-	USERMAPPINGUSERSERVER
+	USERMAPPINGUSERSERVER,
+	VARIABLENAMENSP,
+	VARIABLEOID

2.37.2

---

From: Julien Rouhaud <[email-masked]>
Date: Tue, 23 Aug 2022 20:57:07 +0800

> LINE 1: SELECT tt.a, tt.b, tt.c FROM tt;

Great, thanks a lot!

Could you add some regression tests for that scenario in the next version,
since this is handled by some new code?  It will also probably be useful to
remind any possible committer about that choice.

---

From: Pavel Stehule <[email-masked]>
Date: Wed, 24 Aug 2022 08:42:07 +0200

út 23. 8. 2022 v 14:57 odesílatel Julien Rouhaud <[email-masked]>
napsal:

> > > and relation columns for the same relation name.  This is even worse
> here
> > > as
> > > It feels like a good way to write valid queries that clearly won't do
> what
> > > you

it is there

Regards

Pavel