schema-variables-poc-180429-01-diff

application/octet-stream

Filename: schema-variables-poc-180429-01-diff
Type: application/octet-stream
Part: 0
Message: Re: [HACKERS] proposal: schema variables
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 26984b6cba..e3473a119b 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -1840,7 +1840,8 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
        <literal>m</literal> = materialized view,
        <literal>c</literal> = composite type,
        <literal>f</literal> = foreign table,
-       <literal>p</literal> = partitioned table
+       <literal>p</literal> = partitioned table,
+       <literal>V</literal> = schema variable
       </entry>
      </row>
 
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 53a40ddeec..92848dead2 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -15883,6 +15883,83 @@ SELECT * FROM pg_ls_dir('.') WITH ORDINALITY AS t(ls,n);
 
  </sect1>
 
+ <sect1 id="functions-schemavar">
+  <title>Schema Variable Functions</title>
+
+  <indexterm zone="functions-schemavar">
+   <primary>Schema Variable Functions</primary>
+   <secondary>functions</secondary>
+  </indexterm>
+
+  <indexterm>
+   <primary>get_schema_variable</primary>
+  </indexterm>
+
+  <indexterm>
+   <primary>set_schema_variable</primary>
+  </indexterm>
+
+  <para>
+   These functions allow reading and writing schema variables values.
+   If the schema variable referenced does not exist (created using <xref linkend="sql-createvariable"/>)
+   these functions will (do something...).
+  </para>
+
+  <table id="functions-schemavar-tab">
+   <title>Functions for access to schema variables</title>
+   <tgroup cols="4">
+    <thead>
+     <row>
+      <entry>Function</entry>
+      <entry>Argument Type</entry>
+      <entry>Return Type</entry>
+      <entry>Description</entry>
+     </row>
+    </thead>
+    <tbody>
+     <row>
+      <entry><literal><function>get_schema_variable(<parameter>variable</parameter>, <parameter>expected type</parameter>)</function></literal></entry>
+      <entry><type>regclass</type>, <type>anyelement</type></entry>
+      <entry><type>anyelement</type></entry>
+      <entry>
+       Returns value of schema variable converted to expected type.
+      </entry>
+     </row>
+
+     <row>
+      <entry><literal><function>set_schema_variable(<parameter>variable</parameter>, <parameter>value</parameter>)</function></literal></entry>
+      <entry><type>regclass</type>, <type>anyelement</type></entry>
+      <entry><type>void</type></entry>
+      <entry>
+       Sets the value of schema variable to value, after converting the input to the correct type.
+      </entry>
+     </row>
+
+    </tbody>
+   </tgroup>
+  </table>
+  <para>
+  An example:
+
+<programlisting>
+CREATE TEMP VARIABLE foo AS numeric;
+SELECT set_schema_variable('foo', 345.445);
+ set_schema_variable 
+---------------------
+ 
+(1 row)
+
+SELECT get_schema_variable('foo', null::numeric);
+
+ get_schema_variable 
+---------------------
+             345.445
+(1 row)
+</programlisting>
+  </para>
+
+ </sect1>
+
  <sect1 id="functions-info">
   <title>System Information Functions</title>
 
diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml
index c81c87ef41..a73ad01d41 100644
--- a/doc/src/sgml/ref/allfiles.sgml
+++ b/doc/src/sgml/ref/allfiles.sgml
@@ -99,6 +99,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">
@@ -147,6 +148,7 @@ Complete list of usable sgml source files in this directory.
 <!ENTITY dropType           SYSTEM "drop_type.sgml">
 <!ENTITY dropUser           SYSTEM "drop_user.sgml">
 <!ENTITY dropUserMapping    SYSTEM "drop_user_mapping.sgml">
+<!ENTITY dropVariable       SYSTEM "drop_variable.sgml">
 <!ENTITY dropView           SYSTEM "drop_view.sgml">
 <!ENTITY end                SYSTEM "end.sgml">
 <!ENTITY execute            SYSTEM "execute.sgml">
@@ -155,6 +157,7 @@ Complete list of usable sgml source files in this directory.
 <!ENTITY grant              SYSTEM "grant.sgml">
 <!ENTITY importForeignSchema SYSTEM "import_foreign_schema.sgml">
 <!ENTITY insert             SYSTEM "insert.sgml">
+<!ENTITY let                SYSTEM "let.sgml">
 <!ENTITY listen             SYSTEM "listen.sgml">
 <!ENTITY load               SYSTEM "load.sgml">
 <!ENTITY lock               SYSTEM "lock.sgml">
diff --git a/doc/src/sgml/ref/create_variable.sgml b/doc/src/sgml/ref/create_variable.sgml
new file mode 100644
index 0000000000..0205e4365a
--- /dev/null
+++ b/doc/src/sgml/ref/create_variable.sgml
@@ -0,0 +1,149 @@
+<!--
+doc/src/sgml/ref/create_variable.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-createvariable">
+ <indexterm zone="sql-createvariable">
+  <primary>CREATE VARIABLE</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>CREATE VARIABLE</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>CREATE VARIABLE</refname>
+  <refpurpose>define a new permissioned typed schema variable</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+CREATE VARIABLE [ IF NOT EXISTS ] <replaceable class="parameter">name</replaceable> [ AS ] <replaceable class="parameter">data_type</replaceable> ]
+</synopsis>
+ </refsynopsisdiv>
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>CREATE VARIABLE</command> creates a new schema variable.
+   These variables are scalar typed, non-transactional, and, like relations,
+   exist within a schema with access controlled via
+   <command>GRANT</command> and <command>REVOKE</command>.
+  </para>
+
+  <para>
+   The value of a schema variable is session-local. Retrieving
+   a variable's value will return NULL unless its value has been set
+   to something else in the current session.
+  </para>
+
+  <para>
+   Retrieval is done via the <function>get_schema_variable</function>dunxrion or the SQL
+   command <command>SELECT</command>.  Setting of values is done via the
+   <function>set_schema_variable</function> function or the SQL command
+   <command>LET</command>.
+   Notably, while schema variables are in many ways a kind of table you cannot use
+   <command>UPDATE</command> on them.
+  </para>
+
+  <para>
+   For purposes of name uniqueness relation-like objects (e.g., tables, indexes)
+   within the same schema are considered.  i.e., you cannot give a table and a
+   schema variable the same name.  This is a consequence of them being treated
+   like relations for purposes of <command>SELECT</command>.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+  <variablelist>
+   <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.
+      Note that type of the variable is not considered, nor could it be since the namespace
+      searched contains non-variable objects.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">name</replaceable></term>
+    <listitem>
+     <para>
+      The name (optionally schema-qualified) of the variable to be created.
+     </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 to be created.
+     </para>
+    </listitem>
+   </varlistentry>
+  </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Notes</title>
+
+  <para>
+   Use <command>DROP VARIABLE</command> to remove a variable.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   Create an integer variable <literal>var1</literal>:
+<programlisting>
+CREATE VARIABLE var1 AS integer;
+</programlisting>
+  </para>
+
+  <para>
+   Set this variable's value; then retrieve it converted to numeric.
+<programlisting>
+CREATE VARIABLE
+postgres=# select set_schema_variable('var1', 10);
+ set_schema_variable 
+---------------------
+ 
+(1 row)
+
+postgres=# select get_schema_variable('var1', null::numeric);
+ get_schema_variable 
+---------------------
+                  10
+(1 row)
+</programlisting>
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   <command>CREATE VARIABLE</command> is a PostgreSQL feature.
+   <!-- 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-dropvariable"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/ref/discard.sgml b/doc/src/sgml/ref/discard.sgml
index 6b909b7232..395453bba0 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,16 @@ DISCARD { ALL | PLANS | SEQUENCES | TEMPORARY | TEMP }
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+    <term><literal>VARIABLES</literal></term>
+    <listitem>
+     <para>
+      Sets the value of all schema variables to NULL.
+      <!-- What happens to temporary schema variables -->
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry>
     <term><literal>ALL</literal></term>
     <listitem>
diff --git a/doc/src/sgml/ref/drop_variable.sgml b/doc/src/sgml/ref/drop_variable.sgml
new file mode 100644
index 0000000000..06130fd510
--- /dev/null
+++ b/doc/src/sgml/ref/drop_variable.sgml
@@ -0,0 +1,92 @@
+<!--
+doc/src/sgml/ref/drop_variable.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-dropvariable">
+ <indexterm zone="sql-dropvariable">
+  <primary>DROP VARIABLE</primary>
+ </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.
+   <!-- this would suggest that we need an alter variable owner to command -->
+  </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>
+  </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>
+   <command>DROP VARIABLE</command> is proprietary PostgreSQL command.
+   <!-- 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-createvariable"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/ref/grant.sgml b/doc/src/sgml/ref/grant.sgml
index ff64c7a3ba..006364ebe5 100644
--- a/doc/src/sgml/ref/grant.sgml
+++ b/doc/src/sgml/ref/grant.sgml
@@ -79,6 +79,12 @@ GRANT { USAGE | ALL [ PRIVILEGES ] }
     ON TYPE <replaceable>type_name</replaceable> [, ...]
     TO <replaceable class="parameter">role_specification</replaceable> [, ...] [ WITH GRANT OPTION ]
 
+GRANT { { SELECT | UPDATE }
+    [, ...] | ALL [ PRIVILEGES ] }
+    ON { VARIABLE <replaceable class="parameter">variable_name</replaceable> [, ...]
+         | ALL VARIABLES IN SCHEMA <replaceable class="parameter">schema_name</replaceable> [, ...] }
+    TO <replaceable class="parameter">role_specification</replaceable> [, ...] [ WITH GRANT OPTION ]
+
 <phrase>where <replaceable class="parameter">role_specification</replaceable> can be:</phrase>
 
     [ GROUP ] <replaceable class="parameter">role_name</replaceable>
@@ -167,6 +173,7 @@ GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replace
    foreign servers,
    large objects,
    schemas,
+   schema variables,
    or tablespaces.
    For other types of objects, the default privileges
    granted to <literal>PUBLIC</literal> are as follows:
@@ -204,6 +211,8 @@ GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replace
        For sequences, this privilege also allows the use of the
        <function>currval</function> function.
        For large objects, this privilege allows the object to be read.
+       For schema variables, this privilege allows the <function>get_schema_variable</function>
+       to read the variable's value.
       </para>
      </listitem>
     </varlistentry>
@@ -239,6 +248,9 @@ GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replace
        <function>setval</function> functions.
        For large objects, this privilege allows writing or truncating the
        object.
+       For schema variables, this privilege allows <command>LET</command>
+       and <function>set_schema_variable</function> to modify the schema variable's
+       value.
       </para>
      </listitem>
     </varlistentry>
diff --git a/doc/src/sgml/ref/let.sgml b/doc/src/sgml/ref/let.sgml
new file mode 100644
index 0000000000..e8bf3f6dd4
--- /dev/null
+++ b/doc/src/sgml/ref/let.sgml
@@ -0,0 +1,90 @@
+<!--
+doc/src/sgml/ref/let.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-let">
+ <indexterm zone="sql-let">
+  <primary>LET</primary>
+ </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>
+</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 schema variable.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>sql expression</literal></term>
+    <listitem>
+     <para>
+      An SQL expression, the result is cast to the schema variable's type.
+     </para>
+    </listitem>
+   </varlistentry>
+  </variablelist>
+
+  <para>
+   Example:
+<programlisting>
+CREATE VARIABLE myvar AS integer;
+LET myvar = 10;
+LET myvar = (SELECT sum(val) FROM tab);
+</programlisting>
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   <!-- this feels like it needs to be more specific,
+        but I don't know enough to make it so -->
+   <literal>LET</literal> extends syntax defined in the SQL
+   standard. The standard knows <literal>SET</literal> command,
+   that is used for different purpouse in PostgreSQL.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-createvariable"/></member>
+   <member><xref linkend="sql-dropvariable"/></member>
+  </simplelist>
+ </refsect1>
+</refentry>
diff --git a/doc/src/sgml/ref/revoke.sgml b/doc/src/sgml/ref/revoke.sgml
index 7018202f14..73778f01f9 100644
--- a/doc/src/sgml/ref/revoke.sgml
+++ b/doc/src/sgml/ref/revoke.sgml
@@ -108,6 +108,14 @@ REVOKE [ GRANT OPTION FOR ]
 REVOKE [ ADMIN OPTION FOR ]
     <replaceable class="parameter">role_name</replaceable> [, ...] FROM <replaceable class="parameter">role_name</replaceable> [, ...]
     [ CASCADE | RESTRICT ]
+
+REVOKE [ GRANT OPTION FOR ]
+    { { SELECT | UPDATE }
+    [, ...] | ALL [ PRIVILEGES ] }
+    ON { VARIABLE <replaceable class="parameter">variable_name</replaceable> [, ...]
+         | ALL VARIABLES IN SCHEMA <replaceable>schema_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 73ef212c08..c3c5d9d106 100644
--- a/doc/src/sgml/reference.sgml
+++ b/doc/src/sgml/reference.sgml
@@ -127,6 +127,7 @@
    &createType;
    &createUser;
    &createUserMapping;
+   &createVariable;
    &createView;
    &deallocate;
    &declare;
@@ -175,6 +176,7 @@
    &dropType;
    &dropUser;
    &dropUserMapping;
+   &dropVariable;
    &dropView;
    &end;
    &execute;
@@ -183,6 +185,7 @@
    &grant;
    &importForeignSchema;
    &insert;
+   &let;
    &listen;
    &load;
    &lock;
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 578e4c6592..69ea16e6a7 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -284,6 +284,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 */
@@ -507,6 +510,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);
@@ -577,6 +584,7 @@ ExecGrantStmt_oids(InternalGrant *istmt)
 	{
 		case OBJECT_TABLE:
 		case OBJECT_SEQUENCE:
+		case OBJECT_VARIABLE:
 			ExecGrant_Relation(istmt);
 			break;
 		case OBJECT_DATABASE:
@@ -646,6 +654,7 @@ objectNamesToOids(ObjectType objtype, List *objnames)
 	{
 		case OBJECT_TABLE:
 		case OBJECT_SEQUENCE:
+		case OBJECT_VARIABLE:
 			foreach(cell, objnames)
 			{
 				RangeVar   *relvar = (RangeVar *) lfirst(cell);
@@ -1018,6 +1027,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);
@@ -1215,6 +1228,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);
@@ -1441,6 +1460,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",
@@ -1612,8 +1634,9 @@ expand_all_col_privileges(Oid table_oid, Form_pg_class classForm,
 		if (curr_att == ObjectIdAttributeNumber && !classForm->relhasoids)
 			continue;
 
-		/* Views don't have any system columns at all */
-		if (classForm->relkind == RELKIND_VIEW && curr_att < 0)
+		/* Views and variables don't have any system columns at all */
+		if ((classForm->relkind == RELKIND_VIEW  || classForm->relkind == RELKIND_VARIABLE) &&
+			curr_att < 0)
 			continue;
 
 		attTuple = SearchSysCache2(ATTNUM,
@@ -3456,6 +3479,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;
@@ -3566,6 +3592,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;
@@ -3682,6 +3711,7 @@ pg_aclmask(ObjectType objtype, Oid table_oid, AttrNumber attnum, Oid roleid,
 				pg_attribute_aclmask(table_oid, attnum, roleid, mask, how);
 		case OBJECT_TABLE:
 		case OBJECT_SEQUENCE:
+		case OBJECT_VARIABLE:
 			return pg_class_aclmask(table_oid, roleid, mask, how);
 		case OBJECT_DATABASE:
 			return pg_database_aclmask(table_oid, roleid, mask, how);
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index 39813de991..f1acc0aa70 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -294,6 +294,7 @@ heap_create(const char *relname,
 	switch (relkind)
 	{
 		case RELKIND_VIEW:
+		case RELKIND_VARIABLE:
 		case RELKIND_COMPOSITE_TYPE:
 		case RELKIND_FOREIGN_TABLE:
 		case RELKIND_PARTITIONED_TABLE:
@@ -431,7 +432,8 @@ CheckAttributeNamesTypes(TupleDesc tupdesc, char relkind,
 	 * Skip this for a view or type relation, since those don't have system
 	 * attributes.
 	 */
-	if (relkind != RELKIND_VIEW && relkind != RELKIND_COMPOSITE_TYPE)
+	if (relkind != RELKIND_VIEW && relkind != RELKIND_COMPOSITE_TYPE &&
+		relkind != RELKIND_VARIABLE)
 	{
 		for (i = 0; i < natts; i++)
 		{
@@ -720,7 +722,8 @@ AddNewAttributeTuples(Oid new_rel_oid,
 	 * all for a view or type relation.  We don't bother with making datatype
 	 * dependencies here, since presumably all these types are pinned.
 	 */
-	if (relkind != RELKIND_VIEW && relkind != RELKIND_COMPOSITE_TYPE)
+	if (relkind != RELKIND_VIEW && relkind != RELKIND_COMPOSITE_TYPE &&
+		relkind != RELKIND_VARIABLE)
 	{
 		for (i = 0; i < (int) lengthof(SysAtt); i++)
 		{
@@ -1073,7 +1076,9 @@ heap_create_with_catalog(const char *relname,
 	if (existing_relid != InvalidOid)
 		ereport(ERROR,
 				(errcode(ERRCODE_DUPLICATE_TABLE),
-				 errmsg("relation \"%s\" already exists", relname)));
+				 errmsg("%s \"%s\" already exists",
+						relkind == RELKIND_VARIABLE ? "variable" : "relation",
+						relname)));
 
 	/*
 	 * Since we are going to create a rowtype as well, also check for
@@ -1114,7 +1119,7 @@ heap_create_with_catalog(const char *relname,
 			(relkind == RELKIND_RELATION || relkind == RELKIND_SEQUENCE ||
 			 relkind == RELKIND_VIEW || relkind == RELKIND_MATVIEW ||
 			 relkind == RELKIND_COMPOSITE_TYPE || relkind == RELKIND_FOREIGN_TABLE ||
-			 relkind == RELKIND_PARTITIONED_TABLE))
+			 relkind == RELKIND_PARTITIONED_TABLE || relkind == RELKIND_VARIABLE))
 		{
 			if (!OidIsValid(binary_upgrade_next_heap_pg_class_oid))
 				ereport(ERROR,
@@ -1156,6 +1161,10 @@ heap_create_with_catalog(const char *relname,
 				relacl = get_user_default_acl(OBJECT_SEQUENCE, ownerid,
 											  relnamespace);
 				break;
+			case RELKIND_VARIABLE:
+				relacl = get_user_default_acl(OBJECT_VARIABLE, ownerid,
+											  relnamespace);
+				break;
 			default:
 				relacl = NULL;
 				break;
@@ -1189,14 +1198,16 @@ heap_create_with_catalog(const char *relname,
 	 * Decide whether to create an array type over the relation's rowtype. We
 	 * do not create any array types for system catalogs (ie, those made
 	 * during initdb). We do not create them where the use of a relation as
-	 * such is an implementation detail: toast tables, sequences and indexes.
+	 * such is an implementation detail: toast tables, sequences, indexes and
+	 * variables.
 	 */
 	if (IsUnderPostmaster && (relkind == RELKIND_RELATION ||
 							  relkind == RELKIND_VIEW ||
 							  relkind == RELKIND_MATVIEW ||
 							  relkind == RELKIND_FOREIGN_TABLE ||
 							  relkind == RELKIND_COMPOSITE_TYPE ||
-							  relkind == RELKIND_PARTITIONED_TABLE))
+							  relkind == RELKIND_PARTITIONED_TABLE ||
+							  relkind == RELKIND_VARIABLE))
 		new_array_oid = AssignTypeArrayOid();
 
 	/*
diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c
index ef3ea64bd0..f74f27c8e3 100644
--- a/src/backend/catalog/objectaddress.c
+++ b/src/backend/catalog/objectaddress.c
@@ -519,6 +519,9 @@ static const struct object_type_map
 	{
 		"sequence", OBJECT_SEQUENCE
 	},
+	{
+		"variable", OBJECT_VARIABLE
+	},
 	{
 		"toast table", -1
 	},							/* unmapped */
@@ -823,6 +826,7 @@ get_object_address(ObjectType objtype, Node *object,
 			case OBJECT_VIEW:
 			case OBJECT_MATVIEW:
 			case OBJECT_FOREIGN_TABLE:
+			case OBJECT_VARIABLE:
 				address =
 					get_relation_by_qualified_name(objtype, castNode(List, object),
 												   &relation, lockmode,
@@ -1259,6 +1263,14 @@ get_relation_by_qualified_name(ObjectType objtype, List *object,
 						 errmsg("\"%s\" is not a foreign table",
 								RelationGetRelationName(relation))));
 			break;
+		case OBJECT_VARIABLE:
+			if (relation->rd_rel->relkind != RELKIND_VARIABLE)
+				ereport(ERROR,
+						(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+						 errmsg("\"%s\" is not a schema variable",
+								RelationGetRelationName(relation))));
+			break;
+
 		default:
 			elog(ERROR, "unrecognized objtype: %d", (int) objtype);
 			break;
@@ -1848,6 +1860,8 @@ get_object_address_defacl(List *object, bool missing_ok)
 		case DEFACLOBJ_NAMESPACE:
 			objtype_str = "schemas";
 			break;
+		case DEFACLOBJ_VARIABLE:
+			objtype_str = "variables";
 		default:
 			ereport(ERROR,
 					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
@@ -2110,6 +2124,7 @@ pg_get_object_address(PG_FUNCTION_ARGS)
 	{
 		case OBJECT_TABLE:
 		case OBJECT_SEQUENCE:
+		case OBJECT_VARIABLE:
 		case OBJECT_VIEW:
 		case OBJECT_MATVIEW:
 		case OBJECT_INDEX:
@@ -2234,6 +2249,7 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address,
 		case OBJECT_INDEX:
 		case OBJECT_SEQUENCE:
 		case OBJECT_TABLE:
+		case OBJECT_VARIABLE:
 		case OBJECT_VIEW:
 		case OBJECT_MATVIEW:
 		case OBJECT_FOREIGN_TABLE:
@@ -3301,6 +3317,11 @@ getObjectDescription(const ObjectAddress *object)
 										 _("default privileges on new schemas belonging to role %s"),
 										 GetUserNameFromId(defacl->defaclrole, false));
 						break;
+					case DEFACLOBJ_VARIABLE:
+						appendStringInfo(&buffer,
+										 _("default privileges on new schema variables belonging to role %s"),
+										 GetUserNameFromId(defacl->defaclrole, false));
+						break;
 					default:
 						/* shouldn't get here */
 						appendStringInfo(&buffer,
@@ -3504,6 +3525,10 @@ getRelationDescription(StringInfo buffer, Oid relid)
 			appendStringInfo(buffer, _("sequence %s"),
 							 relname);
 			break;
+		case RELKIND_VARIABLE:
+			appendStringInfo(buffer, _("variable %s"),
+							 relname);
+			break;
 		case RELKIND_TOASTVALUE:
 			appendStringInfo(buffer, _("toast table %s"),
 							 relname);
@@ -4832,6 +4857,10 @@ getObjectIdentityParts(const ObjectAddress *object,
 						appendStringInfoString(&buffer,
 											   " on schemas");
 						break;
+					case DEFACLOBJ_VARIABLE:
+						appendStringInfoString(&buffer,
+											   " on schema variables");
+						break;
 				}
 
 				if (objname)
@@ -5124,6 +5153,8 @@ get_relkind_objtype(char relkind)
 			return OBJECT_INDEX;
 		case RELKIND_SEQUENCE:
 			return OBJECT_SEQUENCE;
+		case RELKIND_VARIABLE:
+			return OBJECT_VARIABLE;
 		case RELKIND_VIEW:
 			return OBJECT_VIEW;
 		case RELKIND_MATVIEW:
diff --git a/src/backend/commands/Makefile b/src/backend/commands/Makefile
index 4a6c99e090..5747272c9a 100644
--- a/src/backend/commands/Makefile
+++ b/src/backend/commands/Makefile
@@ -18,7 +18,7 @@ OBJS = amcmds.o aggregatecmds.o alter.o analyze.o async.o cluster.o comment.o \
 	event_trigger.o explain.o extension.o foreigncmds.o functioncmds.o \
 	indexcmds.o lockcmds.o matview.o operatorcmds.o opclasscmds.o \
 	policy.o portalcmds.o prepare.o proclang.o publicationcmds.o \
-	schemacmds.o seclabel.o sequence.o statscmds.o subscriptioncmds.o \
+	schemacmds.o schemavar.o seclabel.o sequence.o statscmds.o subscriptioncmds.o \
 	tablecmds.o tablespace.o trigger.o tsearchcmds.o typecmds.o user.o \
 	vacuum.o vacuumlazy.o variable.o view.o
 
diff --git a/src/backend/commands/alter.c b/src/backend/commands/alter.c
index eff325cc7d..8a4071c927 100644
--- a/src/backend/commands/alter.c
+++ b/src/backend/commands/alter.c
@@ -348,6 +348,7 @@ ExecRenameStmt(RenameStmt *stmt)
 		case OBJECT_MATVIEW:
 		case OBJECT_INDEX:
 		case OBJECT_FOREIGN_TABLE:
+		case OBJECT_VARIABLE:
 			return RenameRelation(stmt);
 
 		case OBJECT_COLUMN:
@@ -478,6 +479,7 @@ ExecAlterObjectSchemaStmt(AlterObjectSchemaStmt *stmt,
 		case OBJECT_TABLE:
 		case OBJECT_VIEW:
 		case OBJECT_MATVIEW:
+		case OBJECT_VARIABLE:
 			address = AlterTableNamespace(stmt,
 										  oldSchemaAddr ? &oldNspOid : NULL);
 			break;
diff --git a/src/backend/commands/comment.c b/src/backend/commands/comment.c
index 2f2e69b4a8..f4a3cae6b4 100644
--- a/src/backend/commands/comment.c
+++ b/src/backend/commands/comment.c
@@ -83,7 +83,7 @@ CommentObject(CommentStmt *stmt)
 
 			/*
 			 * Allow comments only on columns of tables, views, materialized
-			 * views, composite types, and foreign tables (which are the only
+			 * views, composite types, variables and foreign tables (which are the only
 			 * relkinds for which pg_dump will dump per-column comments).  In
 			 * particular we wish to disallow comments on index columns,
 			 * because the naming of an index's columns may change across PG
@@ -95,10 +95,11 @@ CommentObject(CommentStmt *stmt)
 				relation->rd_rel->relkind != RELKIND_MATVIEW &&
 				relation->rd_rel->relkind != RELKIND_COMPOSITE_TYPE &&
 				relation->rd_rel->relkind != RELKIND_FOREIGN_TABLE &&
-				relation->rd_rel->relkind != RELKIND_PARTITIONED_TABLE)
+				relation->rd_rel->relkind != RELKIND_PARTITIONED_TABLE &&
+				relation->rd_rel->relkind != RELKIND_VARIABLE)
 				ereport(ERROR,
 						(errcode(ERRCODE_WRONG_OBJECT_TYPE),
-						 errmsg("\"%s\" is not a table, view, materialized view, composite type, or foreign table",
+						 errmsg("\"%s\" is not a table, view, materialized view, composite type, schema variable or foreign table",
 								RelationGetRelationName(relation))));
 			break;
 		default:
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
index 770c75fe2c..5cd8a8e42c 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -1503,6 +1503,9 @@ BeginCopy(ParseState *pstate,
 
 		Assert(query->utilityStmt == NULL);
 
+		/* Don't expect LET stmt here, is not possible to do write it */
+		Assert(query->commandType != CMD_LET);
+
 		/*
 		 * Similarly the grammar doesn't enforce the presence of a RETURNING
 		 * clause, but this is required here.
diff --git a/src/backend/commands/discard.c b/src/backend/commands/discard.c
index 01a999c2ac..7aa7d835f8 100644
--- a/src/backend/commands/discard.c
+++ b/src/backend/commands/discard.c
@@ -18,6 +18,7 @@
 #include "commands/async.h"
 #include "commands/discard.h"
 #include "commands/prepare.h"
+#include "commands/schemavar.h"
 #include "commands/sequence.h"
 #include "utils/guc.h"
 #include "utils/portal.h"
@@ -25,7 +26,7 @@
 static void DiscardAll(bool isTopLevel);
 
 /*
- * DISCARD { ALL | SEQUENCES | TEMP | PLANS }
+ * DISCARD { ALL | SEQUENCES | TEMP | PLANS | VARIABLES}
  */
 void
 DiscardCommand(DiscardStmt *stmt, bool isTopLevel)
@@ -48,6 +49,10 @@ DiscardCommand(DiscardStmt *stmt, bool isTopLevel)
 			ResetTempTableNamespace();
 			break;
 
+		case DISCARD_VARIABLES:
+			ResetSchemaVariablesCache();
+			break;
+
 		default:
 			elog(ERROR, "unrecognized DISCARD target: %d", stmt->target);
 	}
@@ -75,4 +80,5 @@ DiscardAll(bool isTopLevel)
 	ResetPlanCache();
 	ResetTempTableNamespace();
 	ResetSequenceCaches();
+	ResetSchemaVariablesCache();
 }
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index eecc85d14e..62799ed284 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -126,6 +126,7 @@ static event_trigger_support_data event_trigger_support[] = {
 	{"TEXT SEARCH TEMPLATE", true},
 	{"TYPE", true},
 	{"USER MAPPING", true},
+	{"VARIABLE", true},
 	{"VIEW", true},
 	{NULL, false}
 };
@@ -1146,6 +1147,7 @@ EventTriggerSupportsObjectType(ObjectType obtype)
 		case OBJECT_TSTEMPLATE:
 		case OBJECT_TYPE:
 		case OBJECT_USER_MAPPING:
+		case OBJECT_VARIABLE:
 		case OBJECT_VIEW:
 			return true;
 
@@ -2244,6 +2246,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:
@@ -2326,6 +2330,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 e1a62a1bce..fc4469cb6c 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -946,6 +946,9 @@ ExplainNode(PlanState *planstate, List *ancestors,
 				case CMD_DELETE:
 					pname = operation = "Delete";
 					break;
+				case CMD_LET:
+					pname = operation = "Let";
+					break;
 				default:
 					pname = "???";
 					break;
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index b945b1556a..a69471e926 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -151,6 +151,7 @@ PrepareQuery(PrepareStmt *stmt, const char *queryString,
 		case CMD_INSERT:
 		case CMD_UPDATE:
 		case CMD_DELETE:
+		case CMD_LET:
 			/* OK */
 			break;
 		default:
diff --git a/src/backend/commands/schemavar.c b/src/backend/commands/schemavar.c
new file mode 100644
index 0000000000..7ddf91708f
--- /dev/null
+++ b/src/backend/commands/schemavar.c
@@ -0,0 +1,1279 @@
+/*-------------------------------------------------------------------------
+ *
+ * schemavar.c
+ *	  PostgreSQL session variable support code.
+ *
+ * Portions Copyright (c) 1996-2018, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/commands/schemavar.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+#include "miscadmin.h"
+#include "funcapi.h"
+
+#include "access/htup_details.h"
+#include "access/xact.h"
+#include "catalog/objectaddress.h"
+#include "catalog/namespace.h"
+#include "catalog/pg_cast.h"
+#include "catalog/pg_class.h"
+#include "catalog/pg_type.h"
+#include "commands/defrem.h"
+#include "commands/tablecmds.h"
+#include "commands/schemavar.h"
+#include "nodes/makefuncs.h"
+#include "optimizer/planner.h"
+#include "parser/parse_coerce.h"
+#include "parser/parse_type.h"
+#include "rewrite/rewriteHandler.h"
+#include "utils/acl.h"
+#include "utils/builtins.h"
+#include "utils/datum.h"
+#include "utils/hsearch.h"
+#include "utils/inval.h"
+#include "utils/lsyscache.h"
+#include "utils/memutils.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+#include "utils/typcache.h"
+
+/*
+ * This schema variable cache mixes the cache and storages behave. That is not
+ * correct and it is problematic, when variable is removed. The own storage
+ * based on storage manager can be implemented, RelFileNode can be defined and
+ * mechanism based on PendingRelDelete struct can be used. This is a argument
+ * for implementation schema variables based on pg_class.
+ * Alternative solution can be detection of schema changes and recheck at and
+ * of transaction.
+ */
+typedef struct SchemaVarData
+{
+	Oid			varid;			/* pg_class OID of this sequence (hash key) */
+	Oid			typid;			/* OID of the data type */
+	int32		typmod;
+	int16		typlen;
+	bool		typbyval;
+	bool		isnull;
+	bool		freeval;
+	Datum		value;
+	bool		is_rowtype;		/* true when variable is composite */
+} SchemaVarData;
+
+typedef SchemaVarData *SchemaVar;
+
+static HTAB *schemavarhashtab = NULL;		/* hash table for session variables */
+static MemoryContext SchemaVarMemoryContext = NULL;
+
+static Datum datumCast(Datum value,
+						  Oid target_typid, int target_typmod,
+						  Oid source_typid, int source_typmod);
+
+static bool	first_time = true;
+static bool	cache_is_valid = true;
+
+static void InvalidateSchemaVarCacheCallback(Datum arg, int cacheid, uint32 hashvalue);
+static void create_schemavar_hashtable(void);
+
+/* just mark cache to recheck */
+static void
+InvalidateSchemaVarCacheCallback(Datum arg, int cacheid, uint32 hashvalue)
+{
+	/*
+	 * because this cache holds values of schema variables, then
+	 * the content cannot be removed in this momemt. We should to
+	 * wait on transaction end.
+	 */
+	cache_is_valid = false;
+}
+
+/*
+ * Cast datum
+ */
+static Datum
+datumCast(Datum value,
+		  Oid target_typid, int target_typmod,
+		  Oid source_typid, int source_typmod)
+{
+	CoercionPathType cpathtype;
+	Oid		cfuncid;
+	Datum	result = (Datum) 0;
+	bool	is_binary_cast = false;
+
+	if (target_typid != source_typid)
+	{
+		cpathtype = find_coercion_pathway(target_typid, source_typid,
+											COERCION_EXPLICIT,
+											&cfuncid);
+
+		if (cpathtype == COERCION_PATH_NONE)
+			elog(ERROR, "could not find cast from %s to %s",
+						format_type_be(source_typid),
+						format_type_be(target_typid));
+
+		if (cpathtype == COERCION_PATH_RELABELTYPE)
+		{
+			result = value;
+			is_binary_cast = true;
+		}
+		else if (cpathtype == COERCION_PATH_COERCEVIAIO)
+		{
+			Oid		outfunc;
+			Oid		infunc;
+			Oid		ioparam;
+			bool	isVarlena;
+			char	*str;
+
+			getTypeOutputInfo(source_typid, &outfunc, &isVarlena);
+			str = OidOutputFunctionCall(outfunc, value);
+
+			getTypeInputInfo(target_typid, &infunc, &ioparam);
+			result = OidInputFunctionCall(infunc, str, ioparam, -1);
+		}
+		else if (cpathtype == COERCION_PATH_FUNC)
+		{
+			result = OidFunctionCall3(cfuncid,
+							value,
+							Int32GetDatum(target_typmod),
+							BoolGetDatum(false));
+		}
+	}
+	else
+	{
+		result = value;
+		is_binary_cast = true;
+	}
+
+	if (target_typmod < 1 || (target_typmod == source_typmod && is_binary_cast))
+		return result;
+
+	cpathtype = find_typmod_coercion_function(target_typid, &cfuncid);
+	if (cpathtype == COERCION_PATH_FUNC)
+	{
+		result = OidFunctionCall3(cfuncid,
+											result,
+											Int32GetDatum(target_typmod),
+											BoolGetDatum(false));
+	}
+
+	return result;
+}
+
+static bool
+sv_get_typ_typmod(Form_pg_class classform, Oid *typ, int32 *typmod)
+{
+	if (classform->relkind != RELKIND_VARIABLE)
+		return false;
+
+	if (classform->reloftype != InvalidOid)
+	{
+		Oid		typoid = classform->reloftype;
+
+		if (get_typtype(typoid) == TYPTYPE_COMPOSITE)
+		{
+			*typ = classform->reloftype;
+			*typmod = -1;
+		}
+		else
+		{
+			TupleDesc	tupdesc;
+			Relation	rel;
+			Form_pg_attribute attrStruct;
+
+			/* find first field */
+			rel = relation_open(typeidTypeRelid(classform->reltype), AccessShareLock);
+			tupdesc = CreateTupleDescCopy(RelationGetDescr(rel));
+
+			attrStruct = TupleDescAttr(tupdesc, 0);
+
+			*typ = attrStruct->atttypid;
+			*typmod = attrStruct->atttypmod;
+
+			FreeTupleDesc(tupdesc);
+			relation_close(rel, AccessShareLock);
+		}
+	}
+	else
+	{
+		*typ = classform->reltype;
+		*typmod = -1;
+	}
+
+	return true;
+}
+
+void
+schemavar_get_typ_typmod(Oid varid, Oid *typ, int32 *typmod)
+{
+	HeapTuple	tp;
+	Form_pg_class	classform;
+
+	/* now, type info for schema variable is collected */
+	tp = SearchSysCache1(RELOID, ObjectIdGetDatum(varid));
+	if (!HeapTupleIsValid(tp))
+		elog(ERROR, "cache lookup faild for variable %u", varid);
+
+	classform = (Form_pg_class) GETSTRUCT(tp);
+
+	if (!sv_get_typ_typmod(classform, typ, typmod))
+		ereport(ERROR,
+				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+				 errmsg("object %u is not a schema variable", varid)));
+
+	ReleaseSysCache(tp);
+}
+
+bool
+is_schemavar(Oid varid, bool *is_rowtype)
+{
+	HeapTuple	tp;
+	Form_pg_class	classform;
+	bool		result;
+	Oid			typ;
+	int32		typmod;
+
+	if (!OidIsValid(varid))
+		return false;
+
+	/* now, type info for schema variable is collected */
+	tp = SearchSysCache1(RELOID, ObjectIdGetDatum(varid));
+	if (!HeapTupleIsValid(tp))
+		elog(ERROR, "cache lookup faild for variable %u", varid);
+
+	classform = (Form_pg_class) GETSTRUCT(tp);
+
+	result = sv_get_typ_typmod(classform, &typ, &typmod);
+
+	if (result && is_rowtype)
+		*is_rowtype = type_is_rowtype(typ);
+
+	ReleaseSysCache(tp);
+
+	return result;
+}
+
+
+static SchemaVar
+PrepareSchemaVar(Oid varid)
+{
+	SchemaVar	var;
+	bool		found;
+
+	if (schemavarhashtab == NULL)
+		create_schemavar_hashtable();
+
+	var = (SchemaVar) hash_search(schemavarhashtab, &varid,
+											  HASH_ENTER, &found);
+	if (!found)
+	{
+		Relation	rel;
+		TupleDesc	desc;
+		int			natts;
+		Datum	   *values;
+		bool	   *nulls;
+		EState	   *estate = NULL;
+		MemoryContext oldcontext = NULL;
+		int			nnotnulls = 0;
+		int		i;
+
+		var->varid = varid;
+
+		schemavar_get_typ_typmod(varid, &var->typid, &var->typmod);
+		get_typlenbyval(var->typid, &var->typlen, &var->typbyval);
+
+		var->is_rowtype = type_is_rowtype(var->typid);
+
+		/* prepare default value */
+		rel = relation_open(varid, AccessShareLock);
+		desc = RelationGetDescr(rel);
+		natts = desc->natts;
+
+		estate = CreateExecutorState();
+		oldcontext = MemoryContextSwitchTo(estate->es_query_cxt);
+
+		values = (Datum *) palloc0(natts * sizeof(Datum));
+		nulls = (bool *) palloc0(natts * sizeof(bool));
+
+		for (i = 0; i < natts; i++)
+		{
+			Expr	   *defexpr;
+			ExprState  *defexprs;
+
+			if (TupleDescAttr(desc, i)->attisdropped)
+			{
+				nulls[i] = true;
+				continue;
+			}
+
+			defexpr = (Expr *) build_column_default(rel, i + 1);
+			if (defexpr != NULL)
+			{
+				defexpr = expression_planner(defexpr);
+				defexprs = ExecInitExpr(defexpr, NULL);
+
+				values[i] = ExecEvalExprSwitchContext(defexprs, GetPerTupleExprContext(estate), &nulls[i]);
+				if (!nulls[i])
+					nnotnulls++;
+			}
+			else
+				nulls[i] = true;
+		}
+
+		MemoryContextSwitchTo(oldcontext);
+
+		if (nnotnulls > 0)
+		{
+			Datum		value;
+
+			if (var->is_rowtype)
+			{
+				HeapTuple	tuple;
+
+				tuple = heap_form_tuple(desc, values, nulls);
+				value = HeapTupleHeaderGetDatum((HeapTupleHeader) tuple->t_data);
+			}
+			else
+				value = values[0];
+
+			MemoryContextSwitchTo(SchemaVarMemoryContext);
+			var->value = datumCopy(value, var->typbyval, var->typlen);
+			var->isnull = false;
+			var->freeval = var->value != values[0];
+
+		MemoryContextSwitchTo(oldcontext);
+		}
+		else
+		{
+			var->value = (Datum) 0;
+			var->isnull = true;
+			var->freeval = false;
+		}
+
+		FreeExecutorState(estate);
+		relation_close(rel, AccessShareLock);
+	}
+
+	return var;
+}
+
+/*
+ * Wait on commit or rollback and clean values that miss entry in system
+ * catalog. It is temporary solution (although it is working). Storage manager
+ * based solution will be better, but it is not necessary for this PoC.
+ *
+ * removes uncommitted or dropped schema variables, so event can be ignored.
+ */
+static void
+recheck_schema_variables(XactEvent event, void *arg)
+{
+	HASH_SEQ_STATUS status;
+	SchemaVar		var;
+
+	if (cache_is_valid || schemavarhashtab == NULL || !IsTransactionState())
+		return;
+
+	hash_seq_init(&status, schemavarhashtab);
+
+	while ((var = (SchemaVar) hash_seq_search(&status)) != NULL)
+	{
+		HeapTuple	tp = InvalidOid;
+
+		tp = SearchSysCache1(RELOID, ObjectIdGetDatum(var->varid));
+		if (!HeapTupleIsValid(tp))
+		{
+			elog(DEBUG1, "variable %d is removed from cache", var->varid);
+
+			if (var->freeval)
+			{
+				pfree(DatumGetPointer(var->value));
+				var->freeval = false;
+			}
+
+			if (hash_search(schemavarhashtab,
+								(void *) &var->varid,
+								HASH_REMOVE,
+								NULL) == NULL)
+				elog(ERROR, "hash table corrupted");
+		}
+		else
+			ReleaseSysCache(tp);
+	}
+	cache_is_valid = true;
+}
+
+/*
+ * DefineSessionVariable
+ *				Creates a new variable related relation
+ */
+ObjectAddress
+DefineSchemaVariable(ParseState *pstate, CreateSchemaVarStmt *var)
+{
+	CreateStmt *stmt = makeNode(CreateStmt);
+	Oid			typoid;
+	Oid			varoid;
+	ObjectAddress address;
+	bool		create_cast;
+
+	/*
+	 * If if_not_exists was given and a relation with the same name already
+	 * exists, bail out. (Note: we needn't check this when not if_not_exists,
+	 * because DefineRelation will complain anyway.)
+	 */
+	if (var->if_not_exists)
+	{
+		RangeVarGetAndCheckCreationNamespace(var->variable, NoLock, &varoid);
+		if (OidIsValid(varoid))
+		{
+			ereport(NOTICE,
+					(errcode(ERRCODE_DUPLICATE_TABLE),
+					 errmsg("variable \"%s\" already exists, skipping",
+							var->variable->relname)));
+			return InvalidObjectAddress;
+		}
+	}
+
+	/*
+	 * Variable can be defined by type (scalar, composite) or by
+	 * field list. When is defined by field list, we should to create
+	 * new type related to this variable.
+	 */
+	if (var->typeName != NULL)
+	{
+		Oid		typrelid;
+
+		Assert(var->fields == NIL);
+
+		typoid = LookupTypeNameOid(pstate, var->typeName, false);
+
+		if (get_typtype(typoid) == TYPTYPE_PSEUDO)
+			ereport(ERROR,
+					(errcode(ERRCODE_DATATYPE_MISMATCH),
+					 errmsg("variable cannot be %s",
+							format_type_be(varoid))));
+
+		typrelid = typeidTypeRelid(typoid);
+
+		if (!OidIsValid(typrelid))
+		{
+			/*
+			 * when variable type is not composite type, we should to create
+			 * aux composite type.
+			 */
+			ColumnDef *coldef = makeNode(ColumnDef);
+
+			coldef->inhcount = 0;
+			coldef->is_local = true;
+			coldef->is_not_null = false;
+			coldef->is_from_type = false;
+			coldef->storage = 0;
+			coldef->raw_default = var->defexpr;
+			coldef->cooked_default = NULL;
+			coldef->collClause = NULL;
+			coldef->collOid = InvalidOid;
+			coldef->constraints = NIL;
+			coldef->location = -1;
+
+			/* Use implit name same like variable name */
+			coldef->colname = var->variable->relname;
+			coldef->typeName = var->typeName;
+
+			stmt->tableElts = lappend(stmt->tableElts, coldef);
+
+			create_cast = false;
+		}
+		else
+		{
+			TupleDesc	tupdesc;
+			Relation	rel;
+			int			i;
+
+			rel = relation_open(typrelid, AccessShareLock);
+			tupdesc = CreateTupleDescCopy(RelationGetDescr(rel));
+
+			for (i = 0; i < tupdesc->natts; i++)
+			{
+				Form_pg_attribute attrStruct = TupleDescAttr(tupdesc, i);
+
+				if (!attrStruct->attisdropped)
+				{
+					ColumnDef *coldef = makeNode(ColumnDef);
+
+					coldef->inhcount = 0;
+					coldef->is_local = true;
+					coldef->is_not_null = false;
+					coldef->is_from_type = false;
+					coldef->storage = 0;
+					coldef->raw_default = NULL;
+					coldef->cooked_default = NULL;
+					coldef->collClause = NULL;
+					coldef->collOid = InvalidOid;
+					coldef->constraints = NIL;
+					coldef->location = -1;
+
+					coldef->colname = pstrdup(NameStr(attrStruct->attname));
+					coldef->typeName = makeTypeNameFromOid(attrStruct->atttypid,
+														   attrStruct->atttypmod);
+
+					stmt->tableElts = lappend(stmt->tableElts, coldef);
+				}
+			}
+
+			FreeTupleDesc(tupdesc);
+			relation_close(rel, AccessShareLock);
+
+			create_cast = true;
+		}
+	}
+	else
+	{
+		Assert(var->typeName == NULL);
+		Assert(var->fields != NIL);
+
+		stmt->tableElts = var->fields;
+
+		create_cast = false;
+	}
+
+	stmt->relation = var->variable;
+	stmt->inhRelations = NIL;
+	stmt->constraints = NIL;
+	stmt->options = NIL;
+	stmt->oncommit = ONCOMMIT_NOOP;
+	stmt->tablespacename = NULL;
+	stmt->if_not_exists = var->if_not_exists;
+
+	/*
+	 * Schema variables uses reltype and reloftype fields. The reltype is
+	 * composite always for scalar and non scalar variables. It is used when
+	 * we need a variable's tuple descriprion. When variable type is composite,
+	 * then is referenced without change. Scalar variable V is translated to
+	 * composite type (v_name v_type).
+	 * The reltype is always composite - see catalog/heap.c AddNewRelationType,
+	 * but for schema variables based on some type is this behave unwanted.
+	 * variable type is not compatible with base type. So in this case is better
+	 * define reltype as domain of base type. But this change break consistency
+	 * of reltype - so I changed nothing and implicitly generate necessary casts.
+	 */
+	stmt->ofTypename = var->typeName;
+
+	address = DefineRelation(stmt, RELKIND_VARIABLE, InvalidOid, NULL, NULL);
+	Assert(address.objectId != InvalidOid);
+
+	if (create_cast)
+	{
+		ObjectAddress c1, c2, referenced_t1, referenced_t2;
+		TypeName *t1 = var->typeName;
+		TypeName *t2 = makeTypeNameFromOid(get_rel_type_id(address.objectId), -1);
+
+		/*
+		 * This is ugly part - but I should to teach Postgres, so t1 and t2 types
+		 * are castable. Unfortunatelly, CreateCast cannot to change dependency,
+		 * so fresh dependency should be removed and recreated with DEPENDENCY_AUTO
+		 * flag. In this case, we are sure so both types are composite, so maybe less
+		 * ugly can be use reltype as domain type of base type instead copy.
+		 */
+		CreateCastStmt *n = makeNode(CreateCastStmt);
+
+		n->sourcetype = t1;
+		n->targettype = t2;
+		n->func = NULL;
+		n->context = COERCION_IMPLICIT;
+		n->inout = true;
+
+		c1 = CreateCast(n);
+
+		n->sourcetype = t2;
+		n->targettype = t1;
+
+		c2 = CreateCast(n);
+
+		deleteDependencyRecordsFor(CastRelationId, c1.objectId, false);
+		deleteDependencyRecordsFor(CastRelationId, c2.objectId, false);
+
+		referenced_t1.classId = TypeRelationId;
+		referenced_t1.objectId = get_rel_type_id(address.objectId);
+		referenced_t1.objectSubId = 0;
+
+		referenced_t2.classId = TypeRelationId;
+		referenced_t2.objectId = LookupTypeNameOid(pstate, var->typeName, false);
+		referenced_t2.objectSubId = 0;
+
+		recordDependencyOn(&c1, &referenced_t1, DEPENDENCY_AUTO);
+		recordDependencyOn(&c1, &referenced_t2, DEPENDENCY_AUTO);
+		recordDependencyOn(&c2, &referenced_t1, DEPENDENCY_AUTO);
+		recordDependencyOn(&c2, &referenced_t2, DEPENDENCY_AUTO);
+	}
+
+	return address;
+}
+
+/*
+ * Implementation of schemavar cache. It is question if it should be in this place, or
+ * it should be storage related or cache related place? But for this moment (PoC) it
+ * can be here. Cache is implemented as hash table with own memory context.
+ */
+
+/*
+ * Create the hash table for storing schema variables
+ */
+static void
+create_schemavar_hashtable(void)
+{
+	HASHCTL		ctl;
+
+	/* set callbacks */
+	if (first_time)
+	{
+
+		CacheRegisterSyscacheCallback(RELOID,
+									  InvalidateSchemaVarCacheCallback,
+									  (Datum) 0);
+		RegisterXactCallback(recheck_schema_variables, NULL);
+
+		first_time = false;
+	}
+
+	/* needs own long life memory context */
+	if (SchemaVarMemoryContext == NULL)
+	{
+		SchemaVarMemoryContext = AllocSetContextCreate(TopMemoryContext,
+															"schema variables",
+															ALLOCSET_START_SMALL_SIZES);
+	}
+
+	memset(&ctl, 0, sizeof(ctl));
+	ctl.keysize = sizeof(Oid);
+	ctl.entrysize = sizeof(SchemaVarData);
+	ctl.hcxt = SchemaVarMemoryContext;
+
+	schemavarhashtab = hash_create("Schema variables", 64, &ctl,
+										HASH_ELEM | HASH_BLOBS | HASH_CONTEXT);
+	cache_is_valid = true;
+}
+
+/*
+ * Fast drop complete content of schema variables
+ */
+void
+ResetSchemaVariablesCache(void)
+{
+	if (schemavarhashtab)
+	{
+		hash_destroy(schemavarhashtab);
+		schemavarhashtab = NULL;
+	}
+
+	if (SchemaVarMemoryContext != NULL)
+	{
+		MemoryContextReset(SchemaVarMemoryContext);
+	}
+}
+
+/*
+ * Copy datum value to schema variables cache place
+ */
+static void
+SetValue(SchemaVar var,
+		 Datum value, bool isNull,
+		 Oid typid, int32 typmod)
+{
+	/* release previously stored value */
+	if (var->freeval)
+	{
+		pfree(DatumGetPointer(var->value));
+		var->freeval = false;
+	}
+
+	if (!isNull)
+	{
+		MemoryContext oldcxt;
+
+		/*
+		 * cast the value if conversion is necessary.
+		 * Expecting: current context is short context.
+		 *
+		 * QUESTION: how much should be this cast tolerant/strict?
+		 */
+		if (var->typid != typid || var->typmod != typmod)
+		{
+			value = datumCast(value,
+								var->typid, var->typmod,
+								typid, typmod);
+		}
+
+		var->isnull = false;
+
+		oldcxt = MemoryContextSwitchTo(SchemaVarMemoryContext);
+
+		var->value = datumCopy(value, var->typbyval, var->typlen);
+		if (var->value != value)
+			var->freeval = true;
+
+		MemoryContextSwitchTo(oldcxt);
+	}
+	else
+	{
+		var->value = (Datum) 0;
+		var->isnull = true;
+	}
+}
+
+/*
+ * Returns variable name
+ */
+char *
+get_schemavar_name(Oid varid)
+{
+	HeapTuple		relTup;
+	Form_pg_class	relForm;
+	char	   *nspname;
+	char	   *relname;
+
+	relTup = SearchSysCache1(RELOID,
+							  ObjectIdGetDatum(varid));
+	if (!HeapTupleIsValid(relTup))
+			elog(ERROR, "cache lookup failed for schema variable %u", varid);
+	relForm = (Form_pg_class) GETSTRUCT(relTup);
+
+	/* Qualify the name if not visible in search path */
+	if (RelationIsVisible(varid))
+		nspname = NULL;
+	else
+		nspname = get_namespace_name(relForm->relnamespace);
+
+	relname = quote_qualified_identifier(nspname, NameStr(relForm->relname));
+
+	ReleaseSysCache(relTup);
+
+	return relname;
+}
+
+/*
+ * Access functions to schema variables.
+ */
+void
+SetSchemaVariable(Oid varid, Datum value, bool isNull, Oid typid, int32 typmod)
+{
+	SchemaVar	var;
+
+	var = PrepareSchemaVar(varid);
+	SetValue(var, value, isNull, typid, typmod);
+}
+
+/*
+ * Securized versions SetSchemaVariable
+ */
+void
+SetSchemaVariableSecure(Oid varid, Datum value, bool isNull, Oid typid, int32 typmod)
+{
+	AclResult		aclresult;
+
+	/* Check permissions */
+	aclresult = pg_class_aclcheck(varid, GetUserId(), ACL_UPDATE);
+	if (aclresult != ACLCHECK_OK)
+		aclcheck_error(aclresult, OBJECT_VARIABLE, get_schemavar_name(varid));
+
+	SetSchemaVariable(varid, value, isNull, typid, typmod);
+}
+
+Datum
+GetSchemaVariable(Oid varid, bool *isNull,
+						Oid typid, int32 typmod,
+						int16 typlen, bool typbyval)
+{
+	SchemaVar	var;
+	Datum		result = (Datum) 0;
+
+	var = PrepareSchemaVar(varid);
+
+	if (!var->isnull)
+	{
+		result = datumCast(var->value, typid, typmod,
+								var->typid, var->typmod);
+		*isNull = false;
+
+		if (result == var->value)
+			result = datumCopy(result, typbyval, typlen);
+	}
+	else
+		*isNull = true;
+
+	return result;
+}
+
+/*
+ * Securized version of GetSchemaVariable
+ */
+Datum
+GetSchemaVariableSecure(Oid varid, bool *isNull,
+						Oid typid, int32 typmod,
+						int16 typlen, bool typbyval)
+{
+	AclResult aclresult;
+
+	/* Check permissions */
+	aclresult = pg_class_aclcheck(varid, GetUserId(), ACL_SELECT);
+	if (aclresult != ACLCHECK_OK)
+		aclcheck_error(aclresult, OBJECT_VARIABLE, get_schemavar_name(varid));
+
+	return GetSchemaVariable(varid, isNull, typid, typmod, typlen, typbyval);
+}
+
+/*
+ * V1 function API
+ *
+ * void set_schema_variable(var regclass, value anyelement);
+ * anyelement get_schema_variable(var regclass, expected_type anyelement)
+ *
+ */
+Datum
+set_schema_variable(PG_FUNCTION_ARGS)
+{
+	Oid		varid;
+	Datum	value;
+	bool	isNull;
+	Oid		typid;
+	int16	typlen;
+	bool	typbyval;
+
+	if (PG_ARGISNULL(0))
+		ereport(ERROR,
+				(errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
+				 errmsg("null value not allowed for variable identity")));
+
+	varid = PG_GETARG_OID(0);
+
+	if (!PG_ARGISNULL(1))
+	{
+		value = PG_GETARG_DATUM(1);
+		isNull = false;
+	}
+	else
+	{
+		value = (Datum) 0;
+		isNull = true;
+	}
+
+	typid = get_fn_expr_argtype(fcinfo->flinfo, 1);
+	if (typid == InvalidOid)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("could not determine input data type")));
+
+	get_typlenbyval(typid, &typlen, &typbyval);
+	SetSchemaVariableSecure(varid, value, isNull, typid, -1);
+
+	PG_RETURN_VOID();
+}
+
+Datum
+get_schema_variable(PG_FUNCTION_ARGS)
+{
+	Oid		varid;
+	Oid		typid;
+	int16	typlen;
+	bool	typbyval;
+	bool	isNull;
+	Datum	result;
+
+	if (PG_ARGISNULL(0))
+		ereport(ERROR,
+				(errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
+				 errmsg("null value not allowed for variable identity")));
+
+	varid = PG_GETARG_OID(0);
+
+	typid = get_fn_expr_argtype(fcinfo->flinfo, 1);
+	if (typid == InvalidOid)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("could not determine input data type")));
+
+	get_typlenbyval(typid, &typlen, &typbyval);
+	result = GetSchemaVariableSecure(varid, &isNull, typid, -1, typlen, typbyval);
+
+	if (isNull)
+		PG_RETURN_NULL();
+	else
+		PG_RETURN_DATUM(result);
+}
+
+/*
+ * Access functions to fields of composite variables
+ */
+void
+SetSchemaVariableField(Oid varid, const char *fieldname,
+						Datum value, bool isNull,
+						Oid typid, int32 typmod)
+{
+
+	SchemaVar	var;
+	TupleDesc	tupDesc;
+	Form_pg_attribute	sattr = NULL;
+	Datum	   *values;
+	bool	   *nulls;
+	int			natts;
+	HeapTuple		tuple;
+	int			i;
+
+	var = PrepareSchemaVar(varid);
+
+	if (!var->is_rowtype)
+		ereport(ERROR,
+				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+				 errmsg("the variable \"%s\" is not of row type",
+						  get_schemavar_name(varid))));
+
+	tupDesc = lookup_rowtype_tupdesc(var->typid, var->typmod);
+	natts = tupDesc->natts;
+
+	/*
+	 * We should to check correct field name, although result can be NULL,
+	 * and this information will not be used.
+	 */
+	for (i = 0; i < natts; i++)
+	{
+		if (namestrcmp(&(TupleDescAttr(tupDesc, i)->attname), fieldname) == 0 &&
+			!TupleDescAttr(tupDesc, i)->attisdropped)
+		{
+			sattr = TupleDescAttr(tupDesc, i);
+			break;
+		}
+	}
+
+	if (sattr == NULL)
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_COLUMN),
+				 errmsg("the variable \"%s\" has not field \"%s\"",
+							get_schemavar_name(varid),
+							fieldname)));
+
+	/* leave, when we don't need to change composite */
+	if (var->isnull && isNull)
+	{
+		FreeTupleDesc(tupDesc);
+		return;
+	}
+
+	values = (Datum *) palloc0(natts * sizeof(Datum));
+	nulls = (bool *) palloc0(natts * sizeof(bool));
+
+	if (!var->isnull)
+	{
+		HeapTupleHeader rec = DatumGetHeapTupleHeader(var->value);
+		HeapTupleData	tup;
+
+		/* Build a temporary HeapTuple control structure */
+		tup.t_len = HeapTupleHeaderGetDatumLength(rec);
+		ItemPointerSetInvalid(&(tup.t_self));
+		tup.t_tableOid = InvalidOid;
+		tup.t_data = rec;
+
+		heap_deform_tuple(&tup, tupDesc, values, nulls);
+	}
+	else
+	{
+		for (i = 0; i < natts; i++)
+			nulls[i] = true;
+	}
+
+	if (!isNull)
+	{
+		values[sattr->attnum - 1] = datumCast(value,
+										  sattr->atttypid, sattr->atttypmod,
+										  typid, typmod);
+		nulls[sattr->attnum - 1] = false;
+	}
+	else
+	{
+		values[sattr->attnum - 1] = (Datum) 0;
+		nulls[sattr->attnum - 1] = true;
+	}
+
+	tuple = heap_form_tuple(tupDesc, values, nulls);
+	SetValue(var,
+			  HeapTupleHeaderGetDatum((HeapTupleHeader) tuple->t_data),
+			  false,
+			  var->typid, var->typmod);
+
+	heap_freetuple(tuple);
+	ReleaseTupleDesc(tupDesc);
+}
+
+Datum
+GetSchemaVariableField(Oid varid, const char *fieldname,
+						bool *isNull,
+						Oid typid, int32 typmod,
+						int16 typlen, bool typbyval)
+{
+	Datum		result = (Datum) 0;
+	Datum		value = (Datum) 0;
+	TupleDesc		tupDesc;
+	Form_pg_attribute	sattr = NULL;
+	SchemaVar		var;
+	int			i;
+
+	Assert(varid != InvalidOid);
+
+	var = PrepareSchemaVar(varid);
+
+	if (!var->is_rowtype)
+		ereport(ERROR,
+				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+				 errmsg("the variable \"%s\" is not of row type",
+						  get_schemavar_name(varid))));
+
+	tupDesc = lookup_rowtype_tupdesc(var->typid, var->typmod);
+
+	/*
+	 * We should to check correct field name, although result can be NULL,
+	 * and this information will not be used.
+	 */
+	for (i = 0; i < tupDesc->natts; i++)
+	{
+		if (namestrcmp(&(TupleDescAttr(tupDesc, i)->attname), fieldname) == 0 &&
+			!TupleDescAttr(tupDesc, i)->attisdropped)
+		{
+			sattr = TupleDescAttr(tupDesc, i);
+			break;
+		}
+	}
+
+	if (sattr == NULL)
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_COLUMN),
+				 errmsg("the variable \"%s\" has not field \"%s\"",
+							get_schemavar_name(varid),
+							fieldname)));
+
+	/*
+	 * Theoretically, we check a possibility to cast to target type everytime,
+	 * but can be ignored, when result will be null.
+	 */
+	if (!var->isnull)
+	{
+		HeapTupleHeader	tuple;
+		HeapTupleData	tmptup;
+
+		tuple = DatumGetHeapTupleHeader(var->value);
+
+		/*
+		 * heap_getattr needs a HeapTuple not a bare HeapTupleHeader.  We set all
+		 * the fields in the struct just in case user tries to inspect system
+		 * columns.
+		 */
+		tmptup.t_len = HeapTupleHeaderGetDatumLength(tuple);
+		ItemPointerSetInvalid(&(tmptup.t_self));
+		tmptup.t_tableOid = InvalidOid;
+		tmptup.t_data = tuple;
+
+		value = heap_getattr(&tmptup,
+							  sattr->attnum,
+							  tupDesc,
+							  isNull);
+
+		if (!*isNull)
+		{
+			result = datumCast(value, typid, typmod,
+								sattr->atttypid, sattr->atttypmod);
+
+			if (result == value)
+				result = datumCopy(result, typbyval, typlen);
+		}
+	}
+
+	ReleaseTupleDesc(tupDesc);
+
+	return result;
+}
+
+void
+SetSchemaVariableFieldSecure(Oid varid, const char *fieldname,
+						Datum value, bool isNull,
+						Oid typid, int32 typmod)
+{
+	AclResult		aclresult;
+
+	/* Check permissions */
+	aclresult = pg_class_aclcheck(varid, GetUserId(), ACL_UPDATE);
+	if (aclresult != ACLCHECK_OK)
+		aclcheck_error(aclresult, OBJECT_VARIABLE, get_schemavar_name(varid));
+
+	SetSchemaVariableField(varid, fieldname, value, isNull, typid, typmod);
+}
+
+Datum
+GetSchemaVariableFieldSecure(Oid varid, const char *fieldname,
+						bool *isNull,
+						Oid typid, int32 typmod,
+						int16 typlen, bool typbyval)
+{
+	AclResult aclresult;
+
+	/*
+	 * There are not any special access to fields. Just check a
+	 * access to variable.
+	 */
+	aclresult = pg_class_aclcheck(varid, GetUserId(), ACL_SELECT);
+	if (aclresult != ACLCHECK_OK)
+		aclcheck_error(aclresult, OBJECT_VARIABLE, get_schemavar_name(varid));
+
+	return GetSchemaVariableField(varid, fieldname, isNull, typid, typmod, typlen, typbyval);
+}
+
+Datum
+set_schema_variable_field(PG_FUNCTION_ARGS)
+{
+	Oid		varid;
+	Datum	value;
+	bool	isNull;
+	Oid		typid;
+	int16	typlen;
+	bool	typbyval;
+	const char   *fieldname;
+
+	if (PG_ARGISNULL(0))
+		ereport(ERROR,
+				(errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
+				 errmsg("null value not allowed for variable identity")));
+
+	varid = PG_GETARG_OID(0);
+
+	if (PG_ARGISNULL(1))
+		ereport(ERROR,
+				(errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
+				 errmsg("null value not allowed for field name")));
+
+	fieldname = NameStr(*PG_GETARG_NAME(1));
+
+	if (!PG_ARGISNULL(2))
+	{
+		value = PG_GETARG_DATUM(2);
+		isNull = false;
+	}
+	else
+	{
+		value = (Datum) 0;
+		isNull = true;
+	}
+
+	typid = get_fn_expr_argtype(fcinfo->flinfo, 2);
+	if (typid == InvalidOid)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("could not determine input data type")));
+
+	get_typlenbyval(typid, &typlen, &typbyval);
+	SetSchemaVariableFieldSecure(varid, fieldname, value, isNull, typid, -1);
+
+	PG_RETURN_VOID();
+}
+
+Datum
+get_schema_variable_field(PG_FUNCTION_ARGS)
+{
+	Oid		varid;
+	Oid		typid;
+	int16	typlen;
+	bool	typbyval;
+	bool	isNull;
+	Datum	result;
+	const char   *fieldname;
+
+	if (PG_ARGISNULL(0))
+		ereport(ERROR,
+				(errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
+				 errmsg("null value not allowed for variable identity")));
+
+	varid = PG_GETARG_OID(0);
+
+	if (PG_ARGISNULL(1))
+		ereport(ERROR,
+				(errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
+				 errmsg("null value not allowed for field name")));
+
+	fieldname = NameStr(*PG_GETARG_NAME(1));
+
+	typid = get_fn_expr_argtype(fcinfo->flinfo, 2);
+	if (typid == InvalidOid)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("could not determine input data type")));
+
+	get_typlenbyval(typid, &typlen, &typbyval);
+	result = GetSchemaVariableFieldSecure(varid, fieldname, &isNull, typid, -1, typlen, typbyval);
+
+	if (isNull)
+		PG_RETURN_NULL();
+	else
+		PG_RETURN_DATUM(result);
+
+	PG_RETURN_NULL();
+}
+
+/*
+ * Results:
+ *=
+ 
+1. The schema variables are similar to temporary tables - but the data are not saved
+   in 8KB blocks, so new storage and some simple storage manager should be created. We
+   would not to use local buffer due allocation 8KB as minimun, creating temp file.
+
+2. We should to work with typmod, so pg_attribute entry should be created anytime.
+
+3. A risk of collisions of variable and table name will be reduced, when variables
+   and tables cannot to have same name.
+
+4. If schema variables are pg_class based, then some current syntax has sense
+
+    We can support UPDATE and SELECT commands on variables.
+
+    possible syntaxes:
+
+        -- there can be a analogy with record functions
+        SELECT varname;
+        SELECT varname.field;
+        SELECT varname FROM varname; -- when var is scalar, then varname in target list is scalar
+
+        UPDATE varname SET varname = 10; -- scalar version
+        UPDATE varname SET varname = (x,y,z) -- composite version
+        UPDATE varname SET varname.field1 = x, varname.field2 = y, ..
+
+        LET varname = expr;
+        LET varname.field1 = expr;
+        LET varname = (x, y, z); -- composite version
+
+   Possible
+   
+        LET varname(a,b) = (select (10, 20) FROM ... )
+
+
+5. LET cmd can be implemented as CMD (like INSERT, UPDATE, DELETE) or Utility (like
+   CreateTableAsSelect). Prefer first option, because there can be prepared, can be
+   used together with EXPLAIN, etc.
+
+   Expected form:
+     LET foo = (SELECT id FROM boo WHERE some = 'hello');
+
+   so possibility to run EXPLAIN LET .. has good enough benefit
+
+6. What should be mechanism of compatibility between base type and variable type?
+
+     CREATE VARIABLE xx AS BT
+
+   Example: CREATE TYPE ct AS (x int, int); CREATE VARIABLE var AS ct;
+   var type (implicitly created) should be compatible with ct type, because we want
+
+     LET var = (10,20)::var; LET var = (10,20)::ct;
+
+7. When LET command allows to specify target field, then there is possible collision
+   I1.I2 can be schema.variable, but should be variable.field too. It should be tested
+   and if both variants are possible, then exception should be raised. LET var.field = expr
+   is a analogy to UPDATE var SET field = expr.
+
+
+*/
\ No newline at end of file
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 0e95037dcf..0af558c3cd 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -272,6 +272,12 @@ static const struct dropmsgstrings dropmsgstringarray[] = {
 		gettext_noop("index \"%s\" does not exist, skipping"),
 		gettext_noop("\"%s\" is not an index"),
 	gettext_noop("Use DROP INDEX to remove an index.")},
+	{RELKIND_VARIABLE,
+		ERRCODE_UNDEFINED_TABLE,
+		gettext_noop("schema variable \"%s\" does not exist"),
+		gettext_noop("schema variable \"%s\" does not exist, skipping"),
+		gettext_noop("\"%s\" is not a schema variable"),
+	gettext_noop("Use DROP VARIABLE to remove a variable.")},
 	{'\0', 0, NULL, NULL, NULL, NULL}
 };
 
@@ -10268,6 +10274,7 @@ ATExecChangeOwner(Oid relationOid, Oid newOwnerId, bool recursing, LOCKMODE lock
 		case RELKIND_MATVIEW:
 		case RELKIND_FOREIGN_TABLE:
 		case RELKIND_PARTITIONED_TABLE:
+		case RELKIND_VARIABLE:
 			/* ok to change owner */
 			break;
 		case RELKIND_INDEX:
diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c
index e284fd71d7..6f5aa3993e 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/schemavar.h"
 #include "executor/execExpr.h"
 #include "executor/nodeSubplan.h"
 #include "funcapi.h"
@@ -727,6 +728,7 @@ ExecInitExprRec(Expr *node, ExprState *state,
 			{
 				Param	   *param = (Param *) node;
 				ParamListInfo params;
+				AclResult	aclresult;
 
 				switch (param->paramkind)
 				{
@@ -734,6 +736,23 @@ ExecInitExprRec(Expr *node, ExprState *state,
 						scratch.opcode = EEOP_PARAM_EXEC;
 						scratch.d.param.paramid = param->paramid;
 						scratch.d.param.paramtype = param->paramtype;
+						ExprEvalPushStep(state, &scratch);
+						break;
+					case PARAM_SCHEMA_VARIABLE:
+						/* Check permission to read schema variable */
+						aclresult = pg_class_aclcheck(param->paramid, GetUserId(), ACL_SELECT);
+						if (aclresult != ACLCHECK_OK)
+							aclcheck_error(aclresult, OBJECT_VARIABLE, get_schemavar_name(param->paramid));
+
+						scratch.opcode = EEOP_PARAM_SCHEMA_VARIABLE;
+						scratch.d.param.paramid = param->paramid;
+						scratch.d.param.paramtype = param->paramtype;
+						scratch.d.param.paramtypmod = param->paramtypmod;
+
+						get_typlenbyval(param->paramtype,
+												&scratch.d.param.paramtyplen,
+												&scratch.d.param.paramtypbyval);
+
 						ExprEvalPushStep(state, &scratch);
 						break;
 					case PARAM_EXTERN:
diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c
index 9d6e25aae5..d6d1867dfa 100644
--- a/src/backend/executor/execExprInterp.c
+++ b/src/backend/executor/execExprInterp.c
@@ -59,6 +59,7 @@
 #include "access/tuptoaster.h"
 #include "catalog/pg_type.h"
 #include "commands/sequence.h"
+#include "commands/schemavar.h"
 #include "executor/execExpr.h"
 #include "executor/nodeSubplan.h"
 #include "funcapi.h"
@@ -351,6 +352,7 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull)
 		&&CASE_EEOP_PARAM_EXEC,
 		&&CASE_EEOP_PARAM_EXTERN,
 		&&CASE_EEOP_PARAM_CALLBACK,
+		&&CASE_EEOP_PARAM_SCHEMA_VARIABLE,
 		&&CASE_EEOP_CASE_TESTVAL,
 		&&CASE_EEOP_MAKE_READONLY,
 		&&CASE_EEOP_IOCOERCE,
@@ -1000,6 +1002,23 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull)
 			EEO_NEXT();
 		}
 
+		EEO_CASE(EEOP_PARAM_SCHEMA_VARIABLE)
+		{
+			Datum	d;
+			bool	isnull;
+
+			d = GetSchemaVariable(op->d.param.paramid, &isnull,
+													  op->d.param.paramtype,
+													  -1,
+													  op->d.param.paramtyplen,
+													  op->d.param.paramtypbyval);
+
+			*op->resvalue = d;
+			*op->resnull = isnull;
+
+			EEO_NEXT();
+		}
+
 		EEO_CASE(EEOP_PARAM_CALLBACK)
 		{
 			/* allow an extension module to supply a PARAM_EXTERN value */
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 51d5bd01d3..8fbc38e4bb 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -211,6 +211,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
@@ -775,7 +776,8 @@ ExecCheckXactReadOnly(PlannedStmt *plannedstmt)
 
 	/*
 	 * Fail if write permissions are requested in parallel mode for table
-	 * (temp or non-temp), otherwise fail for any non-temp table.
+	 * (temp or non-temp), otherwise fail for any non-temp table. Allow to
+	 * modify content of schema variables.
 	 */
 	foreach(l, plannedstmt->rtable)
 	{
@@ -790,6 +792,9 @@ ExecCheckXactReadOnly(PlannedStmt *plannedstmt)
 		if (isTempNamespace(get_rel_namespace(rte->relid)))
 			continue;
 
+		if (rte->relkind == RELKIND_VARIABLE)
+			continue;
+
 		PreventCommandIfReadOnly(CreateCommandTag((Node *) plannedstmt));
 	}
 
@@ -1125,6 +1130,14 @@ CheckValidResultRel(ResultRelInfo *resultRelInfo, CmdType operation)
 					 errmsg("cannot change TOAST relation \"%s\"",
 							RelationGetRelationName(resultRel))));
 			break;
+		case RELKIND_VARIABLE:
+			/* only LET command can change a variable */
+			if (operation != CMD_LET)
+				ereport(ERROR,
+						(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+						 errmsg("cannot change schema variable \"%s\"",
+								RelationGetRelationName(resultRel))));
+			break;
 		case RELKIND_VIEW:
 
 			/*
@@ -1285,6 +1298,13 @@ CheckValidRowMarkRel(Relation rel, RowMarkType markType)
 						 errmsg("cannot lock rows in foreign table \"%s\"",
 								RelationGetRelationName(rel))));
 			break;
+		case RELKIND_VARIABLE:
+			/* Must disallow this because we there are not rows */
+			ereport(ERROR,
+					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+					 errmsg("cannot lock rows in schema variable \"%s\"",
+							RelationGetRelationName(rel))));
+			break;
 		default:
 			ereport(ERROR,
 					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index f6482f8411..c44ee540b9 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -39,6 +39,7 @@
 
 #include "access/htup_details.h"
 #include "access/xact.h"
+#include "commands/schemavar.h"
 #include "commands/trigger.h"
 #include "executor/execPartition.h"
 #include "executor/executor.h"
@@ -73,6 +74,7 @@ static void ExecSetupChildParentMapForSubplan(ModifyTableState *mtstate);
 static TupleConversionMap *tupconv_map_for_subplan(ModifyTableState *node,
 						int whichplan);
 
+
 /*
  * Verify that the tuples to be produced by INSERT or UPDATE match the
  * target relation's rowtype
@@ -1516,6 +1518,72 @@ ExecOnConflictUpdate(ModifyTableState *mtstate,
 	return true;
 }
 
+/* ----------------------------------------------------------------
+ *		ExecLet
+ *
+ *		For LET, we have to update target variable,
+ *      Returns NULL, there are not RETURNING clause.
+ * ----------------------------------------------------------------
+ */
+static TupleTableSlot *
+ExecLet(ModifyTableState *mtstate,
+		   TupleTableSlot *slot,
+		   EState *estate,
+		   bool canSetTag)
+{
+	ResultRelInfo *resultRelInfo;
+	Relation	resultRelationDesc;
+	TupleDesc	tupdesc;
+	bool		isnull = true;
+	Datum		value;
+	Form_pg_attribute attr = NULL;
+	Oid			varid;
+
+	if (slot != NULL && !slot->tts_isempty)
+	{
+		tupdesc = slot->tts_tupleDescriptor;
+
+		Assert(tupdesc != NULL);
+
+		/* should be checked before */
+		if (tupdesc->natts != 1)
+			elog(ERROR, "unexpected number of attributes");
+
+		attr = TupleDescAttr(tupdesc, 0);
+
+		if (!slot->tts_isnull[0])
+		{
+			isnull = false;
+			value = slot->tts_values[0];
+		}
+	}
+
+	/*
+	 * Now, es_result_relation_info is empty, but can be initialized
+	 * to structure of used schema variable.
+	 */
+	resultRelInfo = estate->es_result_relation_info;
+	resultRelationDesc = resultRelInfo->ri_RelationDesc;
+	varid = resultRelationDesc->rd_id;
+
+	if (!isnull)
+	{
+		/* expecting so variable and expression are equal */
+		SetSchemaVariable(varid, value, isnull, attr->atttypid, -1);
+	}
+	else
+	{
+		SetSchemaVariable(varid, (Datum) 0, true, InvalidOid, -1);
+	}
+
+	if (canSetTag)
+	{
+		Assert(estate->es_processed == 0);
+		(estate->es_processed)++;
+	}
+
+	return NULL;
+}
 
 /*
  * Process BEFORE EACH STATEMENT triggers
@@ -1548,6 +1616,9 @@ fireBSTriggers(ModifyTableState *node)
 		case CMD_DELETE:
 			ExecBSDeleteTriggers(node->ps.state, resultRelInfo);
 			break;
+		case CMD_LET:
+			/* there are no trigger */
+			break;
 		default:
 			elog(ERROR, "unknown operation");
 			break;
@@ -1603,6 +1674,9 @@ fireASTriggers(ModifyTableState *node)
 			ExecASDeleteTriggers(node->ps.state, resultRelInfo,
 								 node->mt_transition_capture);
 			break;
+		case CMD_LET:
+			/* variables has not triggers */
+			break;
 		default:
 			elog(ERROR, "unknown operation");
 			break;
@@ -2142,6 +2216,9 @@ ExecModifyTable(PlanState *pstate)
 								  NULL, true, node->canSetTag,
 								  false /* changingPart */ );
 				break;
+			case CMD_LET:
+				slot = ExecLet(node, slot, estate, node->canSetTag);
+				break;
 			default:
 				elog(ERROR, "unknown operation");
 				break;
@@ -2552,6 +2629,7 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 				break;
 			case CMD_UPDATE:
 			case CMD_DELETE:
+			case CMD_LET:
 				junk_filter_needed = true;
 				break;
 			default:
diff --git a/src/backend/executor/spi.c b/src/backend/executor/spi.c
index 08f6f67a15..e107abf6af 100644
--- a/src/backend/executor/spi.c
+++ b/src/backend/executor/spi.c
@@ -2420,6 +2420,9 @@ _SPI_pquery(QueryDesc *queryDesc, bool fire_triggers, uint64 tcount)
 			else
 				res = SPI_OK_UPDATE;
 			break;
+		case CMD_LET:
+			res = SPI_OK_UTILITY;
+			break;
 		default:
 			return SPI_ERROR_OPUNKNOWN;
 	}
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index 7c045a7afe..cfa6523c9d 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -3103,6 +3103,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)
 {
@@ -5148,6 +5160,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;
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index 6a971d0141..c701aeddf1 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -1057,6 +1057,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(selectStmt);
+	COMPARE_LOCATION_FIELD(location);
+
+	return true;
+}
+
 static bool
 _equalSetOperationStmt(const SetOperationStmt *a, const SetOperationStmt *b)
 {
@@ -3225,6 +3235,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;
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index a10014f755..a931237c1b 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -3469,6 +3469,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->selectStmt, context))
+					return true;
+			}
+			break;
 		case T_A_Expr:
 			{
 				A_Expr	   *expr = (A_Expr *) node;
diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c
index 505ae0af85..aee16f4676 100644
--- a/src/backend/optimizer/util/clauses.c
+++ b/src/backend/optimizer/util/clauses.c
@@ -1249,12 +1249,15 @@ max_parallel_hazard_walker(Node *node, max_parallel_hazard_context *context)
 	 * PARAM_EXEC Params listed in safe_param_ids, meaning they could be
 	 * either generated within the worker or can be computed in master and
 	 * then their value can be passed to the worker.
+	 * PARAM_SCHEMA_VARIABLE params are newer changed by workers, so they can be
+	 * safe.
 	 */
 	else if (IsA(node, Param))
 	{
 		Param	   *param = (Param *) node;
 
-		if (param->paramkind == PARAM_EXTERN)
+		if (param->paramkind == PARAM_EXTERN ||
+			param->paramkind == PARAM_SCHEMA_VARIABLE)
 			return false;
 
 		if (param->paramkind != PARAM_EXEC ||
@@ -4793,7 +4796,7 @@ substitute_actual_parameters_mutator(Node *node,
 {
 	if (node == NULL)
 		return NULL;
-	if (IsA(node, Param))
+	if (IsA(node, Param) && ((Param *) node)->paramkind != PARAM_SCHEMA_VARIABLE)
 	{
 		Param	   *param = (Param *) node;
 
diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
index 05f57591e4..4ff6c443b4 100644
--- a/src/backend/parser/analyze.c
+++ b/src/backend/parser/analyze.c
@@ -24,6 +24,7 @@
 
 #include "postgres.h"
 
+#include "access/heapam.h"
 #include "access/sysattr.h"
 #include "catalog/pg_type.h"
 #include "miscadmin.h"
@@ -42,8 +43,12 @@
 #include "parser/parse_param.h"
 #include "parser/parse_relation.h"
 #include "parser/parse_target.h"
+#include "parser/parse_type.h"
 #include "parser/parsetree.h"
 #include "rewrite/rewriteManip.h"
+#include "utils/builtins.h"
+#include "utils/lsyscache.h"
+#include "utils/typcache.h"
 #include "utils/rel.h"
 
 
@@ -56,6 +61,7 @@ static Query *transformInsertStmt(ParseState *pstate, InsertStmt *stmt);
 static List *transformInsertRow(ParseState *pstate, List *exprlist,
 				   List *stmtcols, List *icolumns, List *attrnos,
 				   bool strip_indirection);
+static Query *transformLetStmt(ParseState *pstate, LetStmt *stmt);
 static OnConflictExpr *transformOnConflictClause(ParseState *pstate,
 						  OnConflictClause *onConflictClause);
 static int	count_rowexpr_columns(ParseState *pstate, Node *expr);
@@ -267,6 +273,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:
@@ -304,6 +311,10 @@ transformStmt(ParseState *pstate, Node *parseTree)
 			}
 			break;
 
+		case T_LetStmt:
+			result = transformLetStmt(pstate, (LetStmt *) parseTree);
+			break;
+
 			/*
 			 * Special cases
 			 */
@@ -367,6 +378,7 @@ analyze_requires_snapshot(RawStmt *parseTree)
 		case T_DeleteStmt:
 		case T_UpdateStmt:
 		case T_SelectStmt:
+		case T_LetStmt:
 			result = true;
 			break;
 
@@ -1541,6 +1553,354 @@ transformValuesClause(ParseState *pstate, SelectStmt *stmt)
 	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	   *sub_rtable;
+	List	   *sub_namespace;
+	RangeTblEntry *rte;
+	RangeTblRef *rtr;
+	ListCell   *lc;
+	AclMode		targetPerms;
+	ParseState *sub_pstate;
+	Query	   *selectQuery;
+	int			i = 0;
+
+	RangeVar   *rvar;
+	char	   *str1 = NULL;
+	char	   *str2 = NULL;
+	char	   *str3 = NULL;
+	char	   *fieldname;
+	int			fieldname_pos;
+	List	   *indirection = NIL;
+	int			indirection_pos = -1;
+
+	Relation	rd;
+	Oid			vartypid = InvalidOid;
+	int32		vartypmod = -1;
+	Oid			varid;
+
+	ParseExprKind sv_expr_kind;
+
+	sv_expr_kind = pstate->p_expr_kind;
+	pstate->p_expr_kind = EXPR_KIND_UPDATE_TARGET;
+
+	/* There can't be any outer WITH to worry about */
+	Assert(pstate->p_ctenamespace == NIL);
+
+	qry->commandType = CMD_LET;
+	pstate->p_is_let = true;
+
+	switch (list_length(stmt->target))
+	{
+		case 1:
+			{
+				Node	*field1 = (Node *) linitial(stmt->target);
+
+				Assert(IsA(field1, String));
+				str2 = strVal(field1);
+			}
+			break;
+		case 2:
+			{
+				Node	*field1 = (Node *) linitial(stmt->target);
+				Node	*field2 = (Node *) lsecond(stmt->target);
+
+				Assert(IsA(field1, String));
+
+				if (IsA(field2, String))
+				{
+					str1 = strVal(field1);
+					str2 = strVal(field2);
+				}
+				else
+				{
+					str2 = strVal(field1);
+					indirection_pos = 2;
+				}
+			}
+			break;
+		case 3:
+			{
+				Node	*field1 = (Node *) linitial(stmt->target);
+				Node	*field2 = (Node *) lsecond(stmt->target);
+				Node	*field3 = (Node *) lthird(stmt->target);
+
+				Assert(IsA(field1, String));
+
+				if (!IsA(field2, String))
+					ereport(ERROR,
+						(errcode(ERRCODE_SYNTAX_ERROR),
+						 errmsg("broken schema variable identifier"),
+							 parser_errposition(pstate, stmt->location)));
+
+				if (IsA(field2, String) && IsA(field3, String))
+				{
+					str1 = strVal(field1);
+					str2 = strVal(field2);
+					str3 = strVal(field3);
+				}
+				else
+				{
+					str1 = strVal(field1);
+					str2 = strVal(field2);
+					indirection_pos = 3;
+				}
+			}
+			break;
+		case 4:
+			{
+				Node	*field1 = (Node *) linitial(stmt->target);
+				Node	*field2 = (Node *) lsecond(stmt->target);
+				Node	*field3 = (Node *) lthird(stmt->target);
+				Node	*field4 = (Node *) lfourth(stmt->target);
+
+				Assert(IsA(field1, String));
+
+				if (IsA(field2, String) && IsA(field3, String) && !IsA(field4, String))
+				{
+					str1 = strVal(field1);
+					str2 = strVal(field2);
+					str3 = strVal(field3);
+					indirection_pos = 4;
+				}
+				else
+					ereport(ERROR,
+						(errcode(ERRCODE_SYNTAX_ERROR),
+						 errmsg("broken schema variable identifier"),
+							 parser_errposition(pstate, stmt->location)));
+			}
+			break;
+
+
+		default:
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("improper qualified name"),
+						 parser_errposition(pstate, stmt->location)));
+	}
+
+	rvar = makeRangeVarForTargetOfSchemaVariable(pstate, str1, str2, str3,
+												  stmt->location, &fieldname,
+												  &fieldname_pos, false);
+
+	/*
+	 * If a non-nil rangetable/namespace was passed in, and we are doing
+	 * INSERT/SELECT, arrange to pass the rangetable/namespace down to the
+	 * SELECT.  This can only happen if we are inside a CREATE RULE, and in
+	 * that case we want the rule's OLD and NEW rtable entries to appear as
+	 * part of the SELECT's rtable, not as outer references for it.  (Kluge!)
+	 * The SELECT's joinlist is not affected however.  We must do this before
+	 * adding the target table to the INSERT's rtable.
+	 */
+	sub_rtable = pstate->p_rtable;
+	pstate->p_rtable = NIL;
+	sub_namespace = pstate->p_namespace;
+	pstate->p_namespace = NIL;
+
+	targetPerms = ACL_UPDATE;
+	qry->resultRelation = setTargetTable(pstate, rvar,
+										 false, false, targetPerms);
+
+	rd = pstate->p_target_relation;
+
+	vartypid = rd->rd_rel->reloftype;
+	varid = rd->rd_id;
+
+	if (vartypid != InvalidOid)
+	{
+		if (get_typtype(vartypid) != TYPTYPE_COMPOSITE)
+		{
+			TupleDesc	tupdesc;
+			Relation	rel;
+			Form_pg_attribute attrStruct;
+
+			/* find first field */
+			rel = relation_open(typeidTypeRelid(rd->rd_rel->reltype), AccessShareLock);
+			tupdesc = CreateTupleDescCopy(RelationGetDescr(rel));
+
+			attrStruct = TupleDescAttr(tupdesc, 0);
+
+			vartypid = attrStruct->atttypid;
+			vartypmod = attrStruct->atttypmod;
+
+			FreeTupleDesc(tupdesc);
+			relation_close(rel, AccessShareLock);
+		}
+		else
+		{
+			vartypid = rd->rd_rel->reloftype;
+			vartypmod = -1;
+		}
+	}
+	else
+	{
+		vartypid = rd->rd_rel->reltype;
+		vartypmod = -1;
+	}
+
+	if (fieldname != NULL)
+		indirection = list_copy_tail(stmt->target, fieldname_pos - 1);
+	else if (indirection_pos >= 2)
+		indirection = list_copy_tail(stmt->target, indirection_pos - 1);
+
+	/*
+	 * We make the sub-pstate a child of the outer pstate so that it can
+	 * see any Param definitions supplied from above.  Since the outer
+	 * pstate's rtable and namespace are presently empty, there are no
+	 * side-effects of exposing names the sub-SELECT shouldn't be able to
+	 * see.
+	 */
+	sub_pstate = make_parsestate(pstate);
+
+	/*
+	 * Process the source SELECT.
+	 *
+	 * It is important that this be handled just like a standalone SELECT;
+	 * otherwise the behavior of SELECT within INSERT might be different
+	 * from a stand-alone SELECT. (Indeed, Postgres up through 6.5 had
+	 * bugs of just that nature...)
+	 *
+	 * The sole exception is that we prevent resolving unknown-type
+	 * outputs as TEXT.  This does not change the semantics since if the
+	 * column type matters semantically, it would have been resolved to
+	 * something else anyway.  Doing this lets us resolve such outputs as
+	 * the target column's type, which we handle below.
+	 */
+	sub_pstate->p_rtable = sub_rtable;
+	sub_pstate->p_joinexprs = NIL;	/* sub_rtable has no joins */
+	sub_pstate->p_namespace = sub_namespace;
+	sub_pstate->p_resolve_unknowns = false;
+
+	selectQuery = transformStmt(sub_pstate, stmt->selectStmt);
+
+	free_parsestate(sub_pstate);
+
+	/* The grammar should have produced a SELECT */
+	if (!IsA(selectQuery, Query) ||
+		selectQuery->commandType != CMD_SELECT)
+		elog(ERROR, "unexpected non-SELECT command in LET ... SELECT");
+
+	/*
+	 * Make the source be a subquery in the LET's rangetable, and add
+	 * it to the LET's joinlist.
+	 */
+	rte = addRangeTableEntryForSubquery(pstate,
+										selectQuery,
+										makeAlias("*SELECT*", NIL),
+										false,
+										false);
+	rtr = makeNode(RangeTblRef);
+	/* assume new rte is at end */
+	rtr->rtindex = list_length(pstate->p_rtable);
+	Assert(rte == rt_fetch(rtr->rtindex, pstate->p_rtable));
+	pstate->p_joinlist = lappend(pstate->p_joinlist, rtr);
+
+	/*----------
+	 * 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);
+	}
+
+	/*
+	 * 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))));
+
+	exprListCoer = NIL;
+	foreach(lc, exprList)
+	{
+		Node	   *orig_expr = (Node*) lfirst(lc);
+		Oid			exprtypid = exprType((Node *) orig_expr);
+		Expr	   *expr = NULL;
+		Param *param = makeNode(Param);
+
+		param->paramkind = PARAM_SCHEMA_VARIABLE;
+		param->paramid = varid;
+		param->paramtype = vartypid;
+		param->paramtypmod = vartypmod;
+
+		expr = (Expr *)
+			transformAssignmentIndirection(pstate,
+											(Node *) param,
+											fieldname,
+											false,
+											vartypid,
+											vartypmod,
+											InvalidOid,
+											list_head(indirection),
+											(Node *) orig_expr,
+											-1);
+
+		if (expr == NULL)
+			ereport(ERROR,
+					(errcode(ERRCODE_DATATYPE_MISMATCH),
+					 errmsg("variable \"%s\" is of type %s"
+							  " but expression is of type %s",
+							RelationGetRelationName(rd),
+							format_type_be(vartypid),
+							format_type_be(exprtypid)),
+				 errhint("You will need to rewrite or cast the expression."),
+				 parser_errposition(pstate, exprLocation((Node *) orig_expr))));
+
+		exprListCoer = lappend(exprListCoer, expr);
+	}
+
+	/*
+	 * Generate query's target list using the computed list of expressions.
+	 * Also, mark all the target columns as needing insert permissions.
+	 */
+	rte = pstate->p_target_rangetblentry;
+	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;
+
+	assign_query_collations(pstate, qry);
+
+	pstate->p_expr_kind = sv_expr_kind;
+
+	return qry;
+}
+
 /*
  * transformSetOperationStmt -
  *	  transforms a set-operations tree
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 5a36367446..95d26a73b2 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -157,6 +157,8 @@ static RoleSpec *makeRoleSpec(RoleSpecType type, int location);
 static void check_qualified_name(List *names, core_yyscan_t yyscanner);
 static List *check_func_name(List *names, core_yyscan_t yyscanner);
 static List *check_indirection(List *indirection, core_yyscan_t yyscanner);
+static List *check_indirection_let(List *indirection, core_yyscan_t yyscanner);
+
 static List *extractArgTypes(List *parameters);
 static List *extractAggrArgTypes(List *aggrargs);
 static List *makeOrderedSetArgs(List *directargs, List *orderedargs,
@@ -257,8 +259,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
 		CreateAssertStmt CreateTransformStmt CreateTrigStmt CreateEventTrigStmt
 		CreateUserStmt CreateUserMappingStmt CreateRoleStmt CreatePolicyStmt
 		CreatedbStmt DeclareCursorStmt DefineStmt DeleteStmt DiscardStmt DoStmt
@@ -268,7 +270,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 RevokeStmt RevokeRoleStmt
 		RuleActionStmt RuleActionStmtOrEmpty RuleStmt
@@ -584,6 +586,9 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %type <node>		partbound_datum PartitionRangeDatum
 %type <list>		hash_partbound partbound_datum_list range_datum_list
 %type <defelt>		hash_partbound_elem
+%type <list>	fieldList
+%type <node>	fieldDef optSchemaVarDefExpr
+%type <list>	schemavar_name
 
 /*
  * Non-keyword token types.  These are hard-wired into the "flex" lexer.
@@ -649,7 +654,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
@@ -687,8 +692,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	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
 
@@ -879,6 +884,7 @@ stmt :
 			| CreatePLangStmt
 			| CreateSchemaStmt
 			| CreateSeqStmt
+			| CreateSchemaVarStmt
 			| CreateStmt
 			| CreateSubscriptionStmt
 			| CreateStatsStmt
@@ -920,6 +926,7 @@ stmt :
 			| ListenStmt
 			| RefreshMatViewStmt
 			| LoadStmt
+			| LetStmt
 			| LockStmt
 			| NotifyStmt
 			| PrepareStmt
@@ -1380,6 +1387,7 @@ schema_stmt:
 			CreateStmt
 			| IndexStmt
 			| CreateSeqStmt
+			| CreateSchemaVarStmt
 			| CreateTrigStmt
 			| GrantStmt
 			| ViewStmt
@@ -1808,7 +1816,12 @@ DiscardStmt:
 					n->target = DISCARD_SEQUENCES;
 					$$ = (Node *) n;
 				}
-
+			| DISCARD VARIABLES
+				{
+					DiscardStmt *n = makeNode(DiscardStmt);
+					n->target = DISCARD_VARIABLES;
+					$$ = (Node *) n;
+				}
 		;
 
 
@@ -2004,6 +2017,24 @@ AlterTableStmt:
 					n->nowait = $14;
 					$$ = (Node *)n;
 				}
+		|	ALTER VARIABLE qualified_name alter_table_cmds
+				{
+					AlterTableStmt *n = makeNode(AlterTableStmt);
+					n->relation = $3;
+					n->cmds = $4;
+					n->relkind = OBJECT_VARIABLE;
+					n->missing_ok = false;
+					$$ = (Node *)n;
+				}
+		|	ALTER VARIABLE IF_P EXISTS qualified_name alter_table_cmds
+				{
+					AlterTableStmt *n = makeNode(AlterTableStmt);
+					n->relation = $5;
+					n->cmds = $6;
+					n->relkind = OBJECT_VARIABLE;
+					n->missing_ok = true;
+					$$ = (Node *)n;
+				}
 		;
 
 alter_table_cmds:
@@ -4287,6 +4318,86 @@ NumericOnly_list:	NumericOnly						{ $$ = list_make1($1); }
 				| NumericOnly_list ',' NumericOnly	{ $$ = lappend($1, $3); }
 		;
 
+/*****************************************************************************
+ *
+ *		QUERY :
+ *				CREATE VARIABLE seqname [AS] type
+ *
+ *****************************************************************************/
+
+CreateSchemaVarStmt:
+			CREATE OptTemp VARIABLE qualified_name opt_as Typename optSchemaVarDefExpr
+				{
+					CreateSchemaVarStmt *n = makeNode(CreateSchemaVarStmt);
+					$4->relpersistence = $2;
+					n->variable = $4;
+					n->typeName = $6;
+					n->defexpr = $7;
+					n->fields = NULL;
+					n->if_not_exists = false;
+					$$ = (Node *)n;
+				}
+			| CREATE OptTemp VARIABLE IF_P NOT EXISTS qualified_name opt_as Typename optSchemaVarDefExpr
+				{
+					CreateSchemaVarStmt *n = makeNode(CreateSchemaVarStmt);
+					$7->relpersistence = $2;
+					n->variable = $7;
+					n->typeName = $9;
+					n->defexpr = $10;
+					n->fields = NULL;
+					n->if_not_exists = true;
+					$$ = (Node *)n;
+				}
+			| CREATE OptTemp VARIABLE qualified_name opt_as '(' fieldList ')'
+				{
+					CreateSchemaVarStmt *n = makeNode(CreateSchemaVarStmt);
+					$4->relpersistence = $2;
+					n->variable = $4;
+					n->typeName = NULL;
+					n->fields = $7;
+					n->if_not_exists = false;
+					$$ = (Node *)n;
+				}
+			| CREATE OptTemp VARIABLE IF_P NOT EXISTS qualified_name opt_as '(' fieldList ')'
+				{
+					CreateSchemaVarStmt *n = makeNode(CreateSchemaVarStmt);
+					$7->relpersistence = $2;
+					n->variable = $7;
+					n->typeName = NULL;
+					n->fields = $10;
+					n->if_not_exists = true;
+					$$ = (Node *)n;
+				}
+		;
+
+optSchemaVarDefExpr: DEFAULT b_expr					{ $$ = $2; }
+			| /* EMPTY */							{ $$ = NULL; }
+		;
+
+fieldList:
+			fieldDef								{ $$ = list_make1($1); }
+			| fieldList ',' fieldDef				{ $$ = lappend($1, $3); }
+		;
+
+fieldDef:	ColId Typename optSchemaVarDefExpr
+				{
+					ColumnDef *n = makeNode(ColumnDef);
+					n->colname = $1;
+					n->typeName = $2;
+					n->inhcount = 0;
+					n->is_local = true;
+					n->is_not_null = false;
+					n->is_from_type = false;
+					n->is_from_parent = false;
+					n->storage = 0;
+					n->raw_default = $3;
+					n->cooked_default = NULL;
+					n->collOid = InvalidOid;
+					n->location = @1;
+					$$ = (Node *)n;
+				}
+		;
+
 /*****************************************************************************
  *
  *		QUERIES :
@@ -6335,6 +6446,7 @@ drop_type_any_name:
 			| TEXT_P SEARCH DICTIONARY				{ $$ = OBJECT_TSDICTIONARY; }
 			| TEXT_P SEARCH TEMPLATE				{ $$ = OBJECT_TSTEMPLATE; }
 			| TEXT_P SEARCH CONFIGURATION			{ $$ = OBJECT_TSCONFIGURATION; }
+			| VARIABLE								{ $$ = OBJECT_VARIABLE; }
 		;
 
 /* object types taking name_list */
@@ -6604,6 +6716,7 @@ comment_type_any_name:
 			| TEXT_P SEARCH DICTIONARY			{ $$ = OBJECT_TSDICTIONARY; }
 			| TEXT_P SEARCH PARSER				{ $$ = OBJECT_TSPARSER; }
 			| TEXT_P SEARCH TEMPLATE			{ $$ = OBJECT_TSTEMPLATE; }
+			| VARIABLE							{ $$ = OBJECT_VARIABLE; }
 		;
 
 /* object types taking name */
@@ -6742,6 +6855,7 @@ security_label_type_any_name:
 			| TABLE								{ $$ = OBJECT_TABLE; }
 			| VIEW								{ $$ = OBJECT_VIEW; }
 			| MATERIALIZED VIEW					{ $$ = OBJECT_MATVIEW; }
+			| VARIABLE							{ $$ = OBJECT_VARIABLE; }
 		;
 
 /* object types taking name */
@@ -7067,6 +7181,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;
+				}
 			| FOREIGN DATA_P WRAPPER name_list
 				{
 					PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget));
@@ -7179,6 +7301,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;
+				}
 			| ALL FUNCTIONS IN_P SCHEMA name_list
 				{
 					PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget));
@@ -7361,6 +7491,7 @@ defacl_privilege_target:
 			| FUNCTIONS		{ $$ = OBJECT_FUNCTION; }
 			| ROUTINES		{ $$ = OBJECT_FUNCTION; }
 			| SEQUENCES		{ $$ = OBJECT_SEQUENCE; }
+			| VARIABLES		{ $$ = OBJECT_VARIABLE; }
 			| TYPES_P		{ $$ = OBJECT_TYPE; }
 			| SCHEMAS		{ $$ = OBJECT_SCHEMA; }
 		;
@@ -8959,6 +9090,26 @@ RenameStmt: ALTER AGGREGATE aggregate_with_argtypes RENAME TO name
 					n->missing_ok = false;
 					$$ = (Node *)n;
 				}
+			| ALTER VARIABLE qualified_name RENAME TO name
+				{
+					RenameStmt *n = makeNode(RenameStmt);
+					n->renameType = OBJECT_VARIABLE;
+					n->relation = $3;
+					n->subname = NULL;
+					n->newname = $6;
+					n->missing_ok = false;
+					$$ = (Node *)n;
+				}
+			| ALTER VARIABLE IF_P EXISTS qualified_name RENAME TO name
+				{
+					RenameStmt *n = makeNode(RenameStmt);
+					n->renameType = OBJECT_VARIABLE;
+					n->relation = $5;
+					n->subname = NULL;
+					n->newname = $8;
+					n->missing_ok = true;
+					$$ = (Node *)n;
+				}
 		;
 
 opt_column: COLUMN									{ $$ = COLUMN; }
@@ -10686,7 +10837,8 @@ ExplainableStmt:
 			| CreateAsStmt
 			| CreateMatViewStmt
 			| RefreshMatViewStmt
-			| ExecuteStmt					/* by default all are $$=$1 */
+			| ExecuteStmt
+			| LetStmt					/* by default all are $$=$1 */
 		;
 
 explain_option_list:
@@ -10743,7 +10895,8 @@ PreparableStmt:
 			SelectStmt
 			| InsertStmt
 			| UpdateStmt
-			| DeleteStmt					/* by default all are $$=$1 */
+			| DeleteStmt
+			| LetStmt					/* by default all are $$=$1 */
 		;
 
 /*****************************************************************************
@@ -11142,6 +11295,50 @@ opt_hold: /* EMPTY */						{ $$ = 0; }
 			| WITHOUT HOLD					{ $$ = 0; }
 		;
 
+/*****************************************************************************
+ *
+ *		QUERY:
+ *				LET STATEMENTS
+ *
+ *****************************************************************************/
+LetStmt:	LET schemavar_name '=' a_expr
+				{
+					LetStmt *n = makeNode(LetStmt);
+					SelectStmt *select = makeNode(SelectStmt);
+					ResTarget	*res = makeNode(ResTarget);
+
+					res->name = NULL;
+					res->indirection = NIL;
+					res->val = (Node *) $4;
+					res->location = @4;
+					select->targetList = list_make1(res);
+					n->target = $2;
+					n->selectStmt = (Node *) select;
+					n->location = @2;
+
+					$$ = (Node *) n;
+				}
+		;
+
+schemavar_name:
+			ColId
+				{
+					$$ = list_make1(makeString($1));
+				}
+			| ColId indirection
+				{
+					check_indirection_let($2, yyscanner);
+					if (list_length($2) > 3)
+						ereport(ERROR,
+								(errcode(ERRCODE_SYNTAX_ERROR),
+								 errmsg("improper qualified name"),
+								 parser_errposition(@1)));
+
+					$$ = lcons(makeString($1), $2);
+					$$ = list_union($$, $2);
+				}
+		;
+
 /*****************************************************************************
  *
  *		QUERY:
@@ -15101,6 +15298,7 @@ unreserved_keyword:
 			| LARGE_P
 			| LAST_P
 			| LEAKPROOF
+			| LET
 			| LEVEL
 			| LISTEN
 			| LOAD
@@ -15249,6 +15447,8 @@ unreserved_keyword:
 			| VALIDATE
 			| VALIDATOR
 			| VALUE_P
+			| VARIABLE
+			| VARIABLES
 			| VARYING
 			| VERSION_P
 			| VIEW
@@ -15713,6 +15913,25 @@ check_func_name(List *names, core_yyscan_t yyscanner)
  * We only allow '*' at the end of the list, but it's hard to enforce that
  * in the grammar, so do it here.
  */
+static List *
+check_indirection_let(List *indirection, core_yyscan_t yyscanner)
+{
+	ListCell *l;
+
+	foreach(l, indirection)
+	{
+		if (IsA(lfirst(l), A_Star))
+			parser_yyerror("syntax error");
+
+		if (IsA(lfirst(l), A_Indices))
+		{
+			if (lnext(l) != NULL)
+				parser_yyerror("improper use of []");
+		}
+	}
+	return indirection;
+}
+
 static List *
 check_indirection(List *indirection, core_yyscan_t yyscanner)
 {
@@ -15729,6 +15948,7 @@ check_indirection(List *indirection, core_yyscan_t yyscanner)
 	return indirection;
 }
 
+
 /* extractArgTypes()
  * Given a list of FunctionParameter nodes, extract a list of just the
  * argument types (TypeNames) for input parameters only.  This is what
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index 385e54a9b6..8f6e28da8f 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -371,7 +371,7 @@ transformExprRecurse(ParseState *pstate, Node *expr)
 
 		default:
 			/* should not reach here */
-			elog(ERROR, "unrecognized node type: %d", (int) nodeTag(expr));
+			elog(ERROR, "unrecognized node type: %d %d", (int) nodeTag(expr), T_Const);
 			result = NULL;		/* keep compiler quiet */
 			break;
 	}
@@ -509,6 +509,7 @@ static Node *
 transformColumnRef(ParseState *pstate, ColumnRef *cref)
 {
 	Node	   *node = NULL;
+	Node	   *variable = NULL;
 	char	   *nspname = NULL;
 	char	   *relname = NULL;
 	char	   *colname = NULL;
@@ -749,6 +750,133 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
 			break;
 	}
 
+	/*
+	 * Try to identify column ref as variable. Possible variants are
+	 *
+	 * A .. variable name
+	 * A.B .. qualified variable name
+	 * A.B .. variable.field
+	 * A.B.C .. schema.variable.field
+	 * A.B.C.D .. catalogue.schema.variable.field
+	 */
+	switch (list_length(cref->fields))
+	{
+		case 1:
+			{
+				Node	   *field1 = (Node *) linitial(cref->fields);
+
+				if (IsA(field1, String))
+				{
+					char *varname  = strVal(field1);
+
+					/* Try to identify as an unqualified column */
+					variable = toSchemaVariable(pstate,
+													NULL, varname, NULL,
+													cref->location);
+				}
+				break;
+			}
+		case 2:
+			{
+				Node	   *field1 = (Node *) linitial(cref->fields);
+				Node	   *field2 = (Node *) lsecond(cref->fields);
+
+				if (IsA(field1, String) && IsA(field2, String))
+				{
+					char *nspname = strVal(field1);
+					char *varname = strVal(field2);
+
+					/* Try to identify as an unqualified column */
+					variable = toSchemaVariable(pstate,
+													nspname, varname, NULL,
+													cref->location);
+				}
+				break;
+			}
+		case 3:
+			{
+				Node	   *field1 = (Node *) linitial(cref->fields);
+				Node	   *field2 = (Node *) lsecond(cref->fields);
+				Node	   *field3 = (Node *) lthird(cref->fields);
+
+				if (IsA(field1, String) && IsA(field2, String) && IsA(field2, String))
+				{
+					char *nspname = strVal(field1);
+					char *varname = strVal(field2);
+					char *fieldname = strVal(field3);
+
+					/* Try to identify as an unqualified column */
+					variable = toSchemaVariable(pstate,
+													nspname, varname, fieldname,
+													cref->location);
+				}
+				break;
+			}
+		case 4:
+			{
+				Node	   *field1 = (Node *) linitial(cref->fields);
+				Node	   *field2 = (Node *) lsecond(cref->fields);
+				Node	   *field3 = (Node *) lthird(cref->fields);
+				Node	   *field4 = (Node *) lfourth(cref->fields);
+				char	   *catname;
+
+				Assert(IsA(field1, String));
+				catname = strVal(field1);
+
+				/*
+				 * We check the catalog name and then ignore it.
+				 */
+				if (strcmp(catname, get_database_name(MyDatabaseId)) != 0)
+				{
+					crerr = CRERR_WRONG_DB;
+					break;
+				}
+
+				if (IsA(field1, String) && IsA(field2, String) && IsA(field2, String))
+				{
+					char *nspname = strVal(field2);
+					char *varname = strVal(field3);
+					char *fieldname = strVal(field4);
+
+					/* Try to identify as an unqualified column */
+					variable = toSchemaVariable(pstate,
+													nspname, varname, fieldname,
+													cref->location);
+				}
+				break;
+			}
+		default:
+
+			/*
+			 * There can be another variants, more when composite variables
+			 * will be supported. Currently only scalars are supported, so
+			 * there are not necessary to solve other questions.
+			 *
+			 * do nothing
+			 */
+			crerr = CRERR_TOO_MANY; /* too many dotted names */
+			break;
+	}
+
+	if (variable != NULL)
+	{
+		/*
+		 * When we allow variables in FROM clause, then here can be check,
+		 * if variable and node are same. Or better, when node will be
+		 * referenced to variable, then toSchemaVariable will not be called.
+		 * But now, we don't allow schema variable in FROM clause, so the
+		 * conflict is not possible.
+		 */
+		if (node != NULL)
+			ereport(ERROR,
+					(errcode(ERRCODE_AMBIGUOUS_COLUMN),
+					 errmsg("column reference \"%s\" is ambiguous",
+							NameListToString(cref->fields)),
+					 parser_errposition(pstate, cref->location)));
+
+		node = variable;
+	}
+
 	/*
 	 * 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
diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c
index bf5df26009..512e364fa2 100644
--- a/src/backend/parser/parse_relation.c
+++ b/src/backend/parser/parse_relation.c
@@ -21,6 +21,7 @@
 #include "catalog/heap.h"
 #include "catalog/namespace.h"
 #include "catalog/pg_type.h"
+#include "commands/schemavar.h"
 #include "funcapi.h"
 #include "nodes/makefuncs.h"
 #include "nodes/nodeFuncs.h"
@@ -32,6 +33,7 @@
 #include "utils/lsyscache.h"
 #include "utils/rel.h"
 #include "utils/syscache.h"
+#include "utils/typcache.h"
 #include "utils/varlena.h"
 
 
@@ -1150,6 +1152,7 @@ parserOpenTable(ParseState *pstate, const RangeVar *relation, int lockmode)
 
 	setup_parser_errposition_callback(&pcbstate, pstate, relation->location);
 	rel = heap_openrv_extended(relation, lockmode, true);
+
 	if (rel == NULL)
 	{
 		if (relation->schemaname)
@@ -1180,6 +1183,24 @@ parserOpenTable(ParseState *pstate, const RangeVar *relation, int lockmode)
 								relation->relname)));
 		}
 	}
+
+	/*
+	 * RELKIND_VARIABLE can be used only in LET command.
+	 * Probably this check can be done elsewhere, but here I
+	 * have a used relation and parse state together first time.
+	 */
+	if (rel->rd_rel->relkind == RELKIND_VARIABLE && !pstate->p_is_let)
+		ereport(ERROR,
+				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+				 errmsg("\"%s\" is an schema variable",
+						RelationGetRelationName(rel))));
+
+	if (pstate->p_is_let && rel->rd_rel->relkind != RELKIND_VARIABLE)
+		ereport(ERROR,
+				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+				 errmsg("\"%s\" is not an schema variable",
+						RelationGetRelationName(rel))));
+
 	cancel_parser_errposition_callback(&pcbstate);
 	return rel;
 }
@@ -3360,3 +3381,160 @@ isQueryUsingTempRelation_walker(Node *node, void *context)
 								  isQueryUsingTempRelation_walker,
 								  context);
 }
+
+RangeVar *
+makeRangeVarForTargetOfSchemaVariable(ParseState *pstate,
+									  char *field1, char *field2, char *field3,
+									  int location, char **fieldname, int *fieldname_pos, bool noerror)
+{
+	RangeVar	*rv1;
+	Oid			varid1;
+	bool		is_rowtype1;
+
+	rv1 = makeRangeVar(field1, field2, location);
+	varid1 = RangeVarGetRelid(rv1, NoLock, true);
+	varid1 = is_schemavar(varid1, &is_rowtype1) ? varid1 : InvalidOid;
+
+	if (field1 != NULL && field3 == NULL)
+	{
+		RangeVar   *rv2;
+		bool	is_rowtype2;
+		Oid		varid2;
+
+		rv2 = makeRangeVar(NULL, field1, location);
+		varid2 = RangeVarGetRelid(rv2, NoLock, true);
+
+		/* varid2 has sense only as row type variable */
+		varid2 = is_schemavar(varid2, &is_rowtype2) ? varid2 : InvalidOid;
+		varid2 = is_rowtype2 ? varid2 : InvalidOid;
+
+		if (OidIsValid(varid1) && OidIsValid(varid2))
+			ereport(ERROR,
+					(errcode(ERRCODE_AMBIGUOUS_COLUMN),
+			 errmsg("variable reference \"%s\".\"%s\" is ambiguous", field1, field2),
+					 parser_errposition(pstate, location)));
+
+		if (OidIsValid(varid2))
+		{
+			*fieldname = field2;
+			*fieldname_pos = 2;
+			return rv2;
+		}
+	}
+
+	if (!OidIsValid(varid1))
+	{
+		if (!noerror)
+		{
+			if (field1 != NULL)
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_TABLE),
+						 errmsg("schema variable \"%s.%s\" doesn't exists", field1, field2),
+						 parser_errposition(pstate, location)));
+			else
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_TABLE),
+						 errmsg("schema variable \"%s\" doesn't exists", field2),
+						 parser_errposition(pstate, location)));
+		}
+
+		return NULL;
+	}
+
+	*fieldname = field3;
+	*fieldname_pos = 3;
+
+	if (field3 != NULL && !is_rowtype1)
+	{
+		if (!noerror)
+		{
+			if (field1 != NULL)
+				ereport(ERROR,
+						(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+						 errmsg("variable \"%s.%s\" is not row type", field1, field2),
+							 parser_errposition(pstate, location)));
+			else
+				ereport(ERROR,
+						(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+						 errmsg("variable \"%s\" is not row type", field2),
+						 parser_errposition(pstate, location)));
+		}
+
+		return NULL;
+	}
+
+	return rv1;
+}
+
+/*
+ * Try to replace ColumnRef by Param related to variable
+ *
+ */
+Node *
+toSchemaVariable(ParseState *pstate, char *nspname, char *varname, char *fieldname, int location)
+{
+	char	   *used_fieldname;
+	RangeVar   *rv;
+	Oid			varid;
+	int			fieldname_pos;
+	Oid			vartyp;
+	int32		vartypmod;
+	Param	   *param;
+
+	rv = makeRangeVarForTargetOfSchemaVariable(pstate,
+											   nspname, varname, fieldname, location,
+											   &used_fieldname, &fieldname_pos, true);
+
+	if (rv == NULL)
+		return NULL;
+
+	varid = RangeVarGetRelid(rv, NoLock, true);
+	if (!OidIsValid(varid))
+		return NULL;
+
+	param = makeNode(Param);
+
+	schemavar_get_typ_typmod(varid, &vartyp, &vartypmod);
+
+	param->paramkind = PARAM_SCHEMA_VARIABLE;
+	param->paramid = varid;
+	param->paramtype = vartyp;
+	param->paramtypmod = vartypmod;
+
+	if (used_fieldname != NULL)
+	{
+		TupleDesc		tupdesc;
+		int		i;
+
+		tupdesc = lookup_rowtype_tupdesc(param->paramtype, param->paramtypmod);
+
+		for (i = 0; i < tupdesc->natts; i++)
+		{
+			Form_pg_attribute att = TupleDescAttr(tupdesc, i);
+
+			if (strcmp(used_fieldname, 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", used_fieldname),
+				 parser_errposition(pstate, location)));
+	}
+
+	return (Node *) param;
+}
diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c
index 4932e58022..8549908095 100644
--- a/src/backend/parser/parse_target.c
+++ b/src/backend/parser/parse_target.c
@@ -35,16 +35,6 @@
 
 static void markTargetListOrigin(ParseState *pstate, TargetEntry *tle,
 					 Var *var, int levelsup);
-static Node *transformAssignmentIndirection(ParseState *pstate,
-							   Node *basenode,
-							   const char *targetName,
-							   bool targetIsArray,
-							   Oid targetTypeId,
-							   int32 targetTypMod,
-							   Oid targetCollation,
-							   ListCell *indirection,
-							   Node *rhs,
-							   int location);
 static Node *transformAssignmentSubscripts(ParseState *pstate,
 							  Node *basenode,
 							  const char *targetName,
@@ -596,7 +586,6 @@ transformAssignedExpr(ParseState *pstate,
 	return expr;
 }
 
-
 /*
  * updateTargetListEntry()
  *	This is used in UPDATE statements (and ON CONFLICT DO UPDATE)
@@ -672,7 +661,7 @@ updateTargetListEntry(ParseState *pstate,
  * might want to decorate indirection cells with their own location info,
  * in which case the location argument could probably be dropped.)
  */
-static Node *
+Node *
 transformAssignmentIndirection(ParseState *pstate,
 							   Node *basenode,
 							   const char *targetName,
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index da5ede866c..00af37a4a9 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -108,6 +108,7 @@ typedef struct
 	List	   *views;			/* CREATE VIEW items */
 	List	   *indexes;		/* CREATE INDEX items */
 	List	   *triggers;		/* CREATE TRIGGER items */
+	List	   *variables;		/* CREATE VARIABLE items */
 	List	   *grants;			/* GRANT items */
 } CreateSchemaStmtContext;
 
@@ -3438,6 +3439,7 @@ transformCreateSchemaStmt(CreateSchemaStmt *stmt)
 	cxt.views = NIL;
 	cxt.indexes = NIL;
 	cxt.triggers = NIL;
+	cxt.variables = NIL;
 	cxt.grants = NIL;
 
 	/*
@@ -3503,6 +3505,14 @@ transformCreateSchemaStmt(CreateSchemaStmt *stmt)
 				}
 				break;
 
+			case T_CreateSchemaVarStmt:
+				{
+					CreateSchemaVarStmt *elp = (CreateSchemaVarStmt *) element;
+
+					setSchemaName(cxt.schemaname, &elp->variable->schemaname);
+					cxt.variables = lappend(cxt.variables, element);
+				}
+
 			case T_GrantStmt:
 				cxt.grants = lappend(cxt.grants, element);
 				break;
diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
index 5b87c554f5..2d634c7dd3 100644
--- a/src/backend/rewrite/rewriteHandler.c
+++ b/src/backend/rewrite/rewriteHandler.c
@@ -3342,7 +3342,7 @@ RewriteQuery(Query *parsetree, List *rewrite_events)
 									rt_entry_relation,
 									parsetree->resultRelation, NULL);
 		}
-		else if (event == CMD_DELETE)
+		else if (event == CMD_DELETE || event == CMD_LET)
 		{
 			/* Nothing to do here */
 		}
diff --git a/src/backend/tcop/pquery.c b/src/backend/tcop/pquery.c
index 66cc5c35c6..34ddb79a3d 100644
--- a/src/backend/tcop/pquery.c
+++ b/src/backend/tcop/pquery.c
@@ -193,6 +193,10 @@ ProcessQuery(PlannedStmt *plan,
 						 "DELETE " UINT64_FORMAT,
 						 queryDesc->estate->es_processed);
 				break;
+			case CMD_LET:
+				snprintf(completionTag, COMPLETION_TAG_BUFSIZE,
+						 "LET ");
+				break;
 			default:
 				strcpy(completionTag, "???");
 				break;
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 287addf429..00965ca506 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -47,6 +47,7 @@
 #include "commands/proclang.h"
 #include "commands/publicationcmds.h"
 #include "commands/schemacmds.h"
+#include "commands/schemavar.h"
 #include "commands/seclabel.h"
 #include "commands/sequence.h"
 #include "commands/subscriptioncmds.h"
@@ -113,6 +114,7 @@ CommandIsReadOnly(PlannedStmt *pstmt)
 		case CMD_DELETE:
 			return false;
 		case CMD_UTILITY:
+		case CMD_LET:
 			/* For now, treat all utility commands as read/write */
 			return false;
 		default:
@@ -178,6 +180,7 @@ check_xact_readonly(Node *parsetree)
 		case T_CreateSchemaStmt:
 		case T_CreateSeqStmt:
 		case T_CreateStmt:
+		case T_CreateSchemaVarStmt:
 		case T_CreateTableAsStmt:
 		case T_RefreshMatViewStmt:
 		case T_CreateTableSpaceStmt:
@@ -1459,6 +1462,10 @@ ProcessUtilitySlow(ParseState *pstate,
 				address = AlterSequence(pstate, (AlterSeqStmt *) parsetree);
 				break;
 
+			case T_CreateSchemaVarStmt:
+				address = DefineSchemaVariable(pstate, (CreateSchemaVarStmt *) parsetree);
+				break;
+
 			case T_CreateTableAsStmt:
 				address = ExecCreateTableAs((CreateTableAsStmt *) parsetree,
 											queryString, params, queryEnv,
@@ -2032,6 +2039,9 @@ AlterObjectTypeCommandTag(ObjectType objtype)
 		case OBJECT_STATISTIC_EXT:
 			tag = "ALTER STATISTICS";
 			break;
+		case OBJECT_VARIABLE:
+			tag = "ALTER VARIABLE";
+			break;
 		default:
 			tag = "???";
 			break;
@@ -2081,6 +2091,10 @@ CreateCommandTag(Node *parsetree)
 			tag = "SELECT";
 			break;
 
+		case T_LetStmt:
+			tag = "LET";
+			break;
+
 			/* utility statements --- same whether raw or cooked */
 		case T_TransactionStmt:
 			{
@@ -2245,6 +2259,9 @@ CreateCommandTag(Node *parsetree)
 				case OBJECT_INDEX:
 					tag = "DROP INDEX";
 					break;
+				case OBJECT_VARIABLE:
+					tag = "DROP VARIABLE";
+					break;
 				case OBJECT_TYPE:
 					tag = "DROP TYPE";
 					break;
@@ -2499,6 +2516,10 @@ CreateCommandTag(Node *parsetree)
 			tag = "ALTER SEQUENCE";
 			break;
 
+		case T_CreateSchemaVarStmt:
+			tag = "CREATE VARIABLE";
+			break;
+
 		case T_DoStmt:
 			tag = "DO";
 			break;
@@ -2616,6 +2637,9 @@ CreateCommandTag(Node *parsetree)
 				case DISCARD_SEQUENCES:
 					tag = "DISCARD SEQUENCES";
 					break;
+				case DISCARD_VARIABLES:
+					tag = "DISCARD VARIABLES";
+					break;
 				default:
 					tag = "???";
 			}
@@ -2820,6 +2844,9 @@ CreateCommandTag(Node *parsetree)
 					case CMD_DELETE:
 						tag = "DELETE";
 						break;
+					case CMD_LET:
+						tag = "LET";
+						break;
 					case CMD_UTILITY:
 						tag = CreateCommandTag(stmt->utilityStmt);
 						break;
@@ -2938,6 +2965,10 @@ GetCommandLogLevel(Node *parsetree)
 				lev = LOGSTMT_ALL;
 			break;
 
+		case T_LetStmt:
+			lev = LOGSTMT_ALL;
+			break;
+
 			/* utility statements --- same whether raw or cooked */
 		case T_TransactionStmt:
 			lev = LOGSTMT_ALL;
@@ -3391,6 +3422,7 @@ GetCommandLogLevel(Node *parsetree)
 				switch (stmt->commandType)
 				{
 					case CMD_SELECT:
+					case CMD_LET:
 						lev = LOGSTMT_ALL;
 						break;
 
diff --git a/src/backend/utils/adt/acl.c b/src/backend/utils/adt/acl.c
index 0cfc297b65..fcd695836a 100644
--- a/src/backend/utils/adt/acl.c
+++ b/src/backend/utils/adt/acl.c
@@ -808,6 +808,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 */
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 74e1cd8afb..5d3d7451b8 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -40,6 +40,7 @@
 #include "catalog/pg_type.h"
 #include "commands/defrem.h"
 #include "commands/tablespace.h"
+#include "commands/schemavar.h"
 #include "common/keywords.h"
 #include "executor/spi.h"
 #include "funcapi.h"
@@ -382,6 +383,7 @@ static void get_update_query_targetlist_def(Query *query, List *targetList,
 								deparse_context *context,
 								RangeTblEntry *rte);
 static void get_delete_query_def(Query *query, deparse_context *context);
+static void get_let_query_def(Query *query, deparse_context *context);
 static void get_utility_query_def(Query *query, deparse_context *context);
 static void get_basic_select_query(Query *query, deparse_context *context,
 					   TupleDesc resultDesc);
@@ -4989,6 +4991,10 @@ get_query_def(Query *query, StringInfo buf, List *parentnamespace,
 			get_delete_query_def(query, &context);
 			break;
 
+		case CMD_LET:
+			get_let_query_def(query, &context);
+			break;
+
 		case CMD_NOTHING:
 			appendStringInfoString(buf, "NOTHING");
 			break;
@@ -6206,6 +6212,58 @@ get_insert_query_def(Query *query, deparse_context *context)
 	}
 }
 
+/* ----------
+ * get_let_query_def			- Parse back an LET parsetree
+ * ----------
+ */
+static void
+get_let_query_def(Query *query, deparse_context *context)
+{
+	StringInfo	buf = context->buf;
+	RangeTblEntry *select_rte = NULL;
+	RangeTblEntry *rte;
+	ListCell   *l;
+
+	/*
+	 * If it's an INSERT ... SELECT or multi-row VALUES, there will be a
+	 * single RTE for the SELECT or VALUES.  Plain VALUES has neither.
+	 */
+	foreach(l, query->rtable)
+	{
+		rte = (RangeTblEntry *) lfirst(l);
+
+		if (rte->rtekind == RTE_SUBQUERY)
+		{
+			if (select_rte)
+				elog(ERROR, "too many subquery RTEs in INSERT");
+			select_rte = rte;
+		}
+	}
+
+	/*
+	 * Start the query with INSERT INTO relname
+	 */
+	rte = rt_fetch(query->resultRelation, query->rtable);
+	Assert(rte->rtekind == RTE_RELATION);
+
+	if (PRETTY_INDENT(context))
+	{
+		context->indentLevel += PRETTYINDENT_STD;
+		appendStringInfoChar(buf, ' ');
+	}
+	appendStringInfo(buf, "LET %s ",
+					 generate_relation_name(rte->relid, NIL));
+
+	appendStringInfo(buf, " = ");
+
+	if (select_rte)
+	{
+		/* Add the SELECT */
+		get_query_def(select_rte->subquery, buf, NIL, NULL,
+					  context->prettyFlags, context->wrapColumn,
+					  context->indentLevel);
+	}
+}
 
 /* ----------
  * get_update_query_def			- Parse back an UPDATE parsetree
@@ -7281,6 +7339,13 @@ get_parameter(Param *param, deparse_context *context)
 	deparse_namespace *dpns;
 	ListCell   *ancestor_cell;
 
+	if (param->paramkind == PARAM_SCHEMA_VARIABLE)
+	{
+		appendStringInfo(context->buf, "%s", get_schemavar_name(param->paramid));
+
+		return;
+	}
+
 	/*
 	 * If it's a PARAM_EXEC parameter, try to locate the expression from which
 	 * the parameter was computed.  Note that failing to find a referent isn't
diff --git a/src/bin/pg_dump/common.c b/src/bin/pg_dump/common.c
index 0d147cb08d..97d6607239 100644
--- a/src/bin/pg_dump/common.c
+++ b/src/bin/pg_dump/common.c
@@ -326,7 +326,8 @@ flagInhTables(Archive *fout, TableInfo *tblinfo, int numTables,
 		/* Some kinds never have parents */
 		if (tblinfo[i].relkind == RELKIND_SEQUENCE ||
 			tblinfo[i].relkind == RELKIND_VIEW ||
-			tblinfo[i].relkind == RELKIND_MATVIEW)
+			tblinfo[i].relkind == RELKIND_MATVIEW ||
+			tblinfo[i].relkind == RELKIND_VARIABLE)
 			continue;
 
 		/*
@@ -475,7 +476,8 @@ flagInhAttrs(DumpOptions *dopt, TableInfo *tblinfo, int numTables)
 		/* Some kinds never have parents */
 		if (tbinfo->relkind == RELKIND_SEQUENCE ||
 			tbinfo->relkind == RELKIND_VIEW ||
-			tbinfo->relkind == RELKIND_MATVIEW)
+			tbinfo->relkind == RELKIND_MATVIEW ||
+			tbinfo->relkind == RELKIND_VARIABLE)
 			continue;
 
 		/* Don't bother computing anything for non-target tables, either */
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 83c976eaf7..70b3da9c93 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -3480,7 +3480,8 @@ _getObjectDescription(PQExpBuffer buf, TocEntry *te, ArchiveHandle *AH)
 		strcmp(type, "SERVER") == 0 ||
 		strcmp(type, "PUBLICATION") == 0 ||
 		strcmp(type, "SUBSCRIPTION") == 0 ||
-		strcmp(type, "USER MAPPING") == 0)
+		strcmp(type, "USER MAPPING") == 0 ||
+		strcmp(type, "VARIABLE") == 0)
 	{
 		appendPQExpBuffer(buf, "%s ", type);
 		if (te->namespace && *te->namespace)
@@ -3661,6 +3662,7 @@ _printTocEntry(ArchiveHandle *AH, TocEntry *te, bool isData)
 			strcmp(te->desc, "TABLE") == 0 ||
 			strcmp(te->desc, "TYPE") == 0 ||
 			strcmp(te->desc, "VIEW") == 0 ||
+			strcmp(te->desc, "VARIABLE") == 0 ||
 			strcmp(te->desc, "MATERIALIZED VIEW") == 0 ||
 			strcmp(te->desc, "SEQUENCE") == 0 ||
 			strcmp(te->desc, "FOREIGN TABLE") == 0 ||
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index c5b49459cc..046f2c7a96 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -1325,10 +1325,11 @@ expand_table_name_patterns(Archive *fout,
 						  "\n     LEFT JOIN pg_catalog.pg_namespace n"
 						  "\n     ON n.oid OPERATOR(pg_catalog.=) c.relnamespace"
 						  "\nWHERE c.relkind OPERATOR(pg_catalog.=) ANY"
-						  "\n    (array['%c', '%c', '%c', '%c', '%c', '%c'])\n",
+						  "\n    (array['%c', '%c', '%c', '%c', '%c', '%c, %c'])\n",
 						  RELKIND_RELATION, RELKIND_SEQUENCE, RELKIND_VIEW,
 						  RELKIND_MATVIEW, RELKIND_FOREIGN_TABLE,
-						  RELKIND_PARTITIONED_TABLE);
+						  RELKIND_PARTITIONED_TABLE,
+						  RELKIND_VARIABLE);
 		processSQLNamePattern(GetConnection(fout), query, cell->val, true,
 							  false, "n.nspname", "c.relname", NULL,
 							  "pg_catalog.pg_table_is_visible(c.oid)");
@@ -2256,6 +2257,10 @@ makeTableDataInfo(DumpOptions *dopt, TableInfo *tbinfo, bool oids)
 		dopt->no_unlogged_table_data)
 		return;
 
+	/* Don't dump data of variables */
+	if (tbinfo->relkind == RELKIND_VARIABLE)
+		return;
+
 	/* Check that the data is not explicitly excluded */
 	if (simple_oid_list_member(&tabledata_exclude_oids,
 							   tbinfo->dobj.catId.oid))
@@ -2477,9 +2482,10 @@ guessConstraintInheritance(TableInfo *tblinfo, int numTables)
 		TableInfo **parents;
 		TableInfo  *parent;
 
-		/* Sequences and views never have parents */
+		/* Sequences, variables and views never have parents */
 		if (tbinfo->relkind == RELKIND_SEQUENCE ||
-			tbinfo->relkind == RELKIND_VIEW)
+			tbinfo->relkind == RELKIND_VIEW ||
+			tbinfo->relkind == RELKIND_VARIABLE)
 			continue;
 
 		/* Don't bother computing anything for non-target tables, either */
@@ -5989,7 +5995,7 @@ getTables(Archive *fout, int *numTables)
 						  "(c.oid = pip.objoid "
 						  "AND pip.classoid = 'pg_class'::regclass "
 						  "AND pip.objsubid = 0) "
-						  "WHERE c.relkind in ('%c', '%c', '%c', '%c', '%c', '%c', '%c') "
+						  "WHERE c.relkind in ('%c', '%c', '%c', '%c', '%c', '%c', '%c', '%c') "
 						  "ORDER BY c.oid",
 						  acl_subquery->data,
 						  racl_subquery->data,
@@ -6008,7 +6014,8 @@ getTables(Archive *fout, int *numTables)
 						  RELKIND_RELATION, RELKIND_SEQUENCE,
 						  RELKIND_VIEW, RELKIND_COMPOSITE_TYPE,
 						  RELKIND_MATVIEW, RELKIND_FOREIGN_TABLE,
-						  RELKIND_PARTITIONED_TABLE);
+						  RELKIND_PARTITIONED_TABLE,
+						  RELKIND_VARIABLE);
 
 		destroyPQExpBuffer(acl_subquery);
 		destroyPQExpBuffer(racl_subquery);
@@ -15416,6 +15423,11 @@ dumpTableSchema(Archive *fout, TableInfo *tbinfo)
 				srvname = NULL;
 				ftoptions = NULL;
 				break;
+			case RELKIND_VARIABLE:
+				reltypename = "VARIABLE";
+				srvname = NULL;
+				ftoptions = NULL;
+				break;
 			default:
 				reltypename = "TABLE";
 				srvname = NULL;
@@ -15437,12 +15449,28 @@ dumpTableSchema(Archive *fout, TableInfo *tbinfo)
 						  reltypename,
 						  qualrelname);
 
+		/*
+		 * Binary upgrade of variables should to be fixed with support
+		 * composite types, and should be tested ToDo
+		 */
+		if (tbinfo->relkind == RELKIND_VARIABLE)
+		{
+				appendPQExpBuffer(q, " AS");
+				if (tbinfo->reloftype != NULL)
+					appendPQExpBuffer(q, " %s",
+									  tbinfo->atttypnames[0]);
+				if (tbinfo->attrdefs[0] != NULL && tbinfo->attrdefs[0]->adef_expr != NULL)
+					appendPQExpBuffer(q, " DEFAULT %s",
+									  tbinfo->attrdefs[0]->adef_expr);
+		}
+
 		/*
 		 * Attach to type, if reloftype; except in case of a binary upgrade,
 		 * we dump the table normally and attach it to the type afterward.
 		 */
-		if (tbinfo->reloftype && !dopt->binary_upgrade)
-			appendPQExpBuffer(q, " OF %s", tbinfo->reloftype);
+		else if (tbinfo->reloftype && !dopt->binary_upgrade)
+			appendPQExpBuffer(q, " OF %s",
+							  tbinfo->reloftype);
 
 		/*
 		 * If the table is a partition, dump it as such; except in the case of
@@ -15465,7 +15493,8 @@ dumpTableSchema(Archive *fout, TableInfo *tbinfo)
 							  fmtQualifiedDumpable(parentRel));
 		}
 
-		if (tbinfo->relkind != RELKIND_MATVIEW)
+		if (tbinfo->relkind != RELKIND_MATVIEW &&
+			!(tbinfo->relkind == RELKIND_VARIABLE && tbinfo->reloftype != NULL))
 		{
 			/* Dump the attributes */
 			actual_atts = 0;
@@ -15591,7 +15620,8 @@ dumpTableSchema(Archive *fout, TableInfo *tbinfo)
 				 * We must have a parenthesized attribute list, even though
 				 * empty, when not using the OF TYPE or PARTITION OF syntax.
 				 */
-				appendPQExpBufferStr(q, " (\n)");
+				if (tbinfo->relkind != RELKIND_VARIABLE)
+					appendPQExpBufferStr(q, " (\n)");
 			}
 
 			if (tbinfo->ispartition && !dopt->binary_upgrade)
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index e96c662b1e..78eb878cfc 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -84,7 +84,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 */
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index 81cd65ee8d..ddf78cc9d2 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -2287,6 +2287,75 @@ my %tests = (
 		  { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
 		unlike => { exclude_dump_test_schema => 1, }, },
 
+	'CREATE VARIABLE test_variable' => {
+		all_runs     => 1,
+		catch_all    => 'CREATE ... commands',
+		create_order => 61,
+		create_sql   => 'CREATE VARIABLE dump_test.variable AS integer;',
+		regexp => qr/^
+			\QCREATE VARIABLE dump_test.variable AS integer;\E/xm,
+		like => {
+			binary_upgrade          => 1,
+			clean                   => 1,
+			clean_if_exists         => 1,
+			createdb                => 1,
+			defaults                => 1,
+			exclude_test_table      => 1,
+			exclude_test_table_data => 1,
+			no_blobs                => 1,
+			no_privs                => 1,
+			no_owner                => 1,
+			only_dump_test_schema   => 1,
+			pg_dumpall_dbprivs      => 1,
+			schema_only             => 1,
+			section_pre_data        => 1,
+			test_schema_plus_blobs  => 1,
+			with_oids               => 1, },
+		unlike => {
+			exclude_dump_test_schema => 1,
+			only_dump_test_table     => 1,
+			pg_dumpall_globals       => 1,
+			pg_dumpall_globals_clean => 1,
+			role                     => 1,
+			section_post_data        => 1, }, },
+
+	'CREATE VARIABLE test_variable with field list' => {
+		all_runs     => 1,
+		catch_all    => 'CREATE ... commands',
+		create_order => 61,
+		create_sql   => 'CREATE VARIABLE dump_test.variable1 AS (a int, b numeric(10,2));',
+		regexp => qr/^
+			\QCREATE VARIABLE dump_test.variable1 AS (\E
+			\n\s+\Qa integer,\E
+			\n\s+\Qb numeric(10,2)\E
+			\n\);
+			/xm,
+		like => {
+			binary_upgrade          => 1,
+			clean                   => 1,
+			clean_if_exists         => 1,
+			createdb                => 1,
+			defaults                => 1,
+			exclude_test_table      => 1,
+			exclude_test_table_data => 1,
+			no_blobs                => 1,
+			no_privs                => 1,
+			no_owner                => 1,
+			only_dump_test_schema   => 1,
+			pg_dumpall_dbprivs      => 1,
+			schema_only             => 1,
+			section_pre_data        => 1,
+			test_schema_plus_blobs  => 1,
+			with_oids               => 1, },
+		unlike => {
+			exclude_dump_test_schema => 1,
+			only_dump_test_table     => 1,
+			pg_dumpall_globals       => 1,
+			pg_dumpall_globals_clean => 1,
+			role                     => 1,
+			section_post_data        => 1, }, },
+
+
 	'ALTER VIEW test_view SET DEFAULT' => {
 		create_order => 62,
 		create_sql =>
diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index 4c85f43f09..63411a5fa4 100644
--- a/src/bin/psql/command.c
+++ b/src/bin/psql/command.c
@@ -794,6 +794,7 @@ exec_command_d(PsqlScanState scan_state, bool active_branch, const char *cmd)
 			case 'i':
 			case 's':
 			case 'E':
+			case 'V':
 				success = listTables(&cmd[1], pattern, show_verbose, show_system);
 				break;
 			case 'r':
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 410131e5c7..b193d3ccf8 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -1828,6 +1828,10 @@ describeOneTableDetails(const char *schemaname,
 			printfPQExpBuffer(&title, _("Foreign table \"%s.%s\""),
 							  schemaname, relationname);
 			break;
+		case RELKIND_VARIABLE:
+			printfPQExpBuffer(&title, _("schema variable \"%s.%s\""),
+							  schemaname, relationname);
+			break;
 		case RELKIND_PARTITIONED_TABLE:
 			if (tableinfo.relpersistence == 'u')
 				printfPQExpBuffer(&title, _("Unlogged table \"%s.%s\""),
@@ -3401,6 +3405,7 @@ listDbRoleSettings(const char *pattern, const char *pattern2)
  * m - materialized views
  * s - sequences
  * E - foreign table (Note: different from 'f', the relkind value)
+ * V - schema variable
  * (any order of the above is fine)
  */
 bool
@@ -3412,6 +3417,7 @@ listTables(const char *tabtypes, const char *pattern, bool verbose, bool showSys
 	bool		showMatViews = strchr(tabtypes, 'm') != NULL;
 	bool		showSeq = strchr(tabtypes, 's') != NULL;
 	bool		showForeign = strchr(tabtypes, 'E') != NULL;
+	bool		showVariables = strchr(tabtypes, 'V') != NULL;
 
 	PQExpBufferData buf;
 	PGresult   *res;
@@ -3419,8 +3425,8 @@ listTables(const char *tabtypes, const char *pattern, bool verbose, bool showSys
 	static const bool translate_columns[] = {false, false, true, false, false, false, false};
 
 	/* If tabtypes is empty, we default to \dtvmsE (but see also command.c) */
-	if (!(showTables || showIndexes || showViews || showMatViews || showSeq || showForeign))
-		showTables = showViews = showMatViews = showSeq = showForeign = true;
+	if (!(showTables || showIndexes || showViews || showMatViews || showSeq || showForeign || showVariables))
+		showTables = showViews = showMatViews = showSeq = showForeign = showVariables = true;
 
 	initPQExpBuffer(&buf);
 
@@ -3441,6 +3447,7 @@ listTables(const char *tabtypes, const char *pattern, bool verbose, bool showSys
 					  " WHEN " CppAsString2(RELKIND_FOREIGN_TABLE) " THEN '%s'"
 					  " WHEN " CppAsString2(RELKIND_PARTITIONED_TABLE) " THEN '%s'"
 					  " WHEN " CppAsString2(RELKIND_PARTITIONED_INDEX) " THEN '%s'"
+					  " WHEN " CppAsString2(RELKIND_VARIABLE) " THEN '%s'"
 					  " END as \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(c.relowner) as \"%s\"",
 					  gettext_noop("Schema"),
@@ -3454,6 +3461,7 @@ listTables(const char *tabtypes, const char *pattern, bool verbose, bool showSys
 					  gettext_noop("foreign table"),
 					  gettext_noop("table"),	/* partitioned table */
 					  gettext_noop("index"),	/* partitioned index */
+					  gettext_noop("schema variable"),
 					  gettext_noop("Type"),
 					  gettext_noop("Owner"));
 
@@ -3507,6 +3515,8 @@ listTables(const char *tabtypes, const char *pattern, bool verbose, bool showSys
 		appendPQExpBufferStr(&buf, "'s',"); /* was RELKIND_SPECIAL */
 	if (showForeign)
 		appendPQExpBufferStr(&buf, CppAsString2(RELKIND_FOREIGN_TABLE) ",");
+	if (showVariables)
+		appendPQExpBufferStr(&buf, CppAsString2(RELKIND_VARIABLE) ",");
 
 	appendPQExpBufferStr(&buf, "''");	/* dummy */
 	appendPQExpBufferStr(&buf, ")\n");
diff --git a/src/bin/psql/describe.h b/src/bin/psql/describe.h
index a4cc5efae0..c5f107d814 100644
--- a/src/bin/psql/describe.h
+++ b/src/bin/psql/describe.h
@@ -60,7 +60,7 @@ extern bool listTSTemplates(const char *pattern, bool verbose);
 /* \l */
 extern bool listAllDbs(const char *pattern, bool verbose);
 
-/* \dt, \di, \ds, \dS, etc. */
+/* \dt, \di, \ds, \dS, \dvar etc. */
 extern bool listTables(const char *tabtypes, const char *pattern, bool verbose, bool showSystem);
 
 /* \dD */
diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c
index 702e742af4..2da50f7290 100644
--- a/src/bin/psql/help.c
+++ b/src/bin/psql/help.c
@@ -167,7 +167,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(125, pager ? &(pset.popt.topt) : NULL);
+	output = PageOutput(126, pager ? &(pset.popt.topt) : NULL);
 
 	fprintf(output, _("General\n"));
 	fprintf(output, _("  \\copyright             show PostgreSQL usage and distribution terms\n"));
@@ -257,6 +257,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[S+] [PATTERN]      list schema variables\n"));
 	fprintf(output, _("  \\dx[+]  [PATTERN]      list extensions\n"));
 	fprintf(output, _("  \\dy     [PATTERN]      list event triggers\n"));
 	fprintf(output, _("  \\l[+]   [PATTERN]      list databases\n"));
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index b431efc983..ab14923392 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -805,6 +805,22 @@ static const SchemaQuery Query_for_list_of_statistics = {
 	NULL
 };
 
+static const SchemaQuery Query_for_list_of_variables = {
+	/* min_server_version */
+	0,
+	/* catname */
+	"pg_catalog.pg_class c",
+	/* selcondition */
+	"c.relkind IN ('V')",
+	/* viscondition */
+	"pg_catalog.pg_table_is_visible(c.oid)",
+	/* namespace */
+	"c.relnamespace",
+	/* result */
+	"pg_catalog.quote_ident(c.relname)",
+	/* qualresult */
+	NULL
+};
 
 /*
  * Queries to get lists of names of various kinds of things, possibly
@@ -1249,6 +1265,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, &Query_for_list_of_variables},
 	{"VIEW", NULL, NULL, &Query_for_list_of_views},
 	{NULL}						/* end of list */
 };
@@ -1604,7 +1621,7 @@ 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", "INSERT", "LISTEN", "LOAD", "LOCK",
+		"FETCH", "GRANT", "IMPORT", "INSERT", "LET", "LISTEN", "LOAD", "LOCK",
 		"MOVE", "NOTIFY", "PREPARE",
 		"REASSIGN", "REFRESH MATERIALIZED VIEW", "REINDEX", "RELEASE",
 		"RESET", "REVOKE", "ROLLBACK",
@@ -1623,7 +1640,7 @@ psql_completion(const char *text, int start, int end)
 		"\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL",
 		"\\dm", "\\dn", "\\do", "\\dO", "\\dp",
 		"\\drds", "\\dRs", "\\dRp", "\\ds", "\\dS",
-		"\\dt", "\\dT", "\\dv", "\\du", "\\dx", "\\dy",
+		"\\dt", "\\dT", "\\dv", "\\du", "\\dx", "\\dy", "\\dvar",
 		"\\e", "\\echo", "\\ef", "\\elif", "\\else", "\\encoding",
 		"\\endif", "\\errverbose", "\\ev",
 		"\\f",
@@ -2835,6 +2852,14 @@ psql_completion(const char *text, int start, int end)
 	else if (Matches4("CREATE", "ROLE|USER|GROUP", MatchAny, "IN"))
 		COMPLETE_WITH_LIST2("GROUP", "ROLE");
 
+/* CREATE VARIABLE --- is allowed inside CREATE SCHEMA, so use TailMatches */
+	/* Complete CREATE VARIABLE <name> with AS */
+	else if (TailMatches3("CREATE", "VARIABLE", MatchAny))
+		COMPLETE_WITH_CONST("AS");
+	/* Complete CREATE VARIABLE <name> with AS types*/
+	else if (TailMatches4("CREATE", "VARIABLE", MatchAny, "AS"))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes, NULL);
+
 /* CREATE VIEW --- is allowed inside CREATE SCHEMA, so use TailMatches */
 	/* Complete CREATE VIEW <name> with AS */
 	else if (TailMatches3("CREATE", "VIEW", MatchAny))
@@ -2990,6 +3015,12 @@ psql_completion(const char *text, int start, int end)
 	else if (Matches5("DROP", "RULE", MatchAny, "ON", MatchAny))
 		COMPLETE_WITH_LIST2("CASCADE", "RESTRICT");
 
+	/* DROP VARIABLE */
+	else if (Matches2("DROP", "VARIABLE"))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables, NULL);
+	else if (Matches3("DROP", "VARIABLE", MatchAny))
+		COMPLETE_WITH_LIST2("CASCADE", "RESTRICT");
+
 /* EXECUTE */
 	else if (Matches1("EXECUTE"))
 		COMPLETE_WITH_QUERY(Query_for_list_of_prepared_statements);
@@ -3000,14 +3031,14 @@ psql_completion(const char *text, int start, int end)
 	 * Complete EXPLAIN [ANALYZE] [VERBOSE] with list of EXPLAIN-able commands
 	 */
 	else if (Matches1("EXPLAIN"))
-		COMPLETE_WITH_LIST7("SELECT", "INSERT", "DELETE", "UPDATE", "DECLARE",
-							"ANALYZE", "VERBOSE");
+		COMPLETE_WITH_LIST8("SELECT", "INSERT", "DELETE", "UPDATE", "DECLARE",
+							"ANALYZE", "VERBOSE", "LET");
 	else if (Matches2("EXPLAIN", "ANALYZE"))
-		COMPLETE_WITH_LIST6("SELECT", "INSERT", "DELETE", "UPDATE", "DECLARE",
-							"VERBOSE");
+		COMPLETE_WITH_LIST7("SELECT", "INSERT", "DELETE", "UPDATE", "DECLARE",
+							"VERBOSE", "LET");
 	else if (Matches2("EXPLAIN", "VERBOSE") ||
 			 Matches3("EXPLAIN", "ANALYZE", "VERBOSE"))
-		COMPLETE_WITH_LIST5("SELECT", "INSERT", "DELETE", "UPDATE", "DECLARE");
+		COMPLETE_WITH_LIST6("SELECT", "INSERT", "DELETE", "UPDATE", "DECLARE", "LET");
 
 /* FETCH && MOVE */
 	/* Complete FETCH with one of FORWARD, BACKWARD, RELATIVE */
@@ -3116,6 +3147,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'"
@@ -3129,14 +3161,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 (TailMatches4("GRANT|REVOKE", MatchAny, "ON", "ALL"))
-		COMPLETE_WITH_LIST5("FUNCTIONS IN SCHEMA",
+		COMPLETE_WITH_LIST6("FUNCTIONS IN SCHEMA",
 							"PROCEDURES IN SCHEMA",
 							"ROUTINES IN SCHEMA",
 							"SEQUENCES IN SCHEMA",
-							"TABLES IN SCHEMA");
+							"TABLES IN SCHEMA",
+							"VARIABLES IN SCHEMA");
 	else if (TailMatches4("GRANT|REVOKE", MatchAny, "ON", "FOREIGN"))
 		COMPLETE_WITH_LIST2("DATA WRAPPER", "SERVER");
 
@@ -3166,6 +3200,8 @@ psql_completion(const char *text, int start, int end)
 			COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_sequences, NULL);
 		else if (TailMatches1("TABLE"))
 			COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tsvmf, NULL);
+		else if (TailMatches1("VARIABLE"))
+			COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables, NULL);
 		else if (TailMatches1("TABLESPACE"))
 			COMPLETE_WITH_QUERY(Query_for_list_of_tablespaces);
 		else if (TailMatches1("TYPE"))
@@ -3322,7 +3358,7 @@ psql_completion(const char *text, int start, int end)
 
 /* PREPARE xx AS */
 	else if (Matches3("PREPARE", MatchAny, "AS"))
-		COMPLETE_WITH_LIST4("SELECT", "UPDATE", "INSERT", "DELETE FROM");
+		COMPLETE_WITH_LIST5("SELECT", "UPDATE", "INSERT", "DELETE FROM", "LET");
 
 /*
  * PREPARE TRANSACTION is missing on purpose. It's intended for transaction
@@ -3545,6 +3581,14 @@ psql_completion(const char *text, int start, int end)
 	else if (TailMatches4("UPDATE", MatchAny, "SET", MatchAny))
 		COMPLETE_WITH_CONST("=");
 
+/* LET --- can be inside EXPLAIN, PREPARE etc */
+	/* If prev. word is LET suggest a list of variables */
+	else if (TailMatches1("LET"))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables, NULL);
+	/* Complete LET <variable> with "=" */
+	else if (TailMatches2("LET", MatchAny))
+		COMPLETE_WITH_CONST("=");
+
 /* USER MAPPING */
 	else if (Matches3("ALTER|CREATE|DROP", "USER", "MAPPING"))
 		COMPLETE_WITH_CONST("FOR");
diff --git a/src/include/catalog/pg_class.h b/src/include/catalog/pg_class.h
index dc6c415c58..04a5849056 100644
--- a/src/include/catalog/pg_class.h
+++ b/src/include/catalog/pg_class.h
@@ -104,6 +104,7 @@ typedef FormData_pg_class *Form_pg_class;
 #define		  RELKIND_FOREIGN_TABLE   'f'	/* foreign table */
 #define		  RELKIND_PARTITIONED_TABLE 'p' /* partitioned table */
 #define		  RELKIND_PARTITIONED_INDEX 'I' /* partitioned index */
+#define		  RELKIND_VARIABLE		  'V'	/* schema variable */
 
 #define		  RELPERSISTENCE_PERMANENT	'p' /* regular table */
 #define		  RELPERSISTENCE_UNLOGGED	'u' /* unlogged permanent table */
diff --git a/src/include/catalog/pg_default_acl.h b/src/include/catalog/pg_default_acl.h
index d0410f5586..56deef1a45 100644
--- a/src/include/catalog/pg_default_acl.h
+++ b/src/include/catalog/pg_default_acl.h
@@ -57,6 +57,7 @@ typedef FormData_pg_default_acl *Form_pg_default_acl;
 #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 66c6c224a8..08e38c6186 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -10202,4 +10202,18 @@
   proisstrict => 'f', prorettype => 'bool', proargtypes => 'oid int4 int4 any',
   proargmodes => '{i,i,i,v}', prosrc => 'satisfies_hash_partition' },
 
+#schema variables function interface
+{ oid => '6122', descr => 'returns value of schema variable',
+  proname => 'get_schema_variable', proisstrict => 'f', prorettype => 'anyelement',
+  proargtypes => 'regclass anyelement', prosrc => 'get_schema_variable' },
+{ oid => '6123', descr => 'set value of schema variable',
+  proname => 'set_schema_variable', proisstrict => 'f', prorettype => 'void',
+  proargtypes => 'regclass anyelement', prosrc => 'set_schema_variable' },
+{ oid => '6124', descr => 'get value of field of schema variable of composite type',
+  proname => 'get_schema_variable', proisstrict => 'f', prorettype => 'anyelement',
+  proargtypes => 'regclass name anyelement', prosrc => 'get_schema_variable_field' },
+{ oid => '6125', descr => 'set value of field of schema variable of composite type',
+  proname => 'set_schema_variable', proisstrict => 'f', prorettype => 'void',
+  proargtypes => 'regclass name anyelement', prosrc => 'set_schema_variable_field' },
+
 ]
diff --git a/src/include/commands/schemavar.h b/src/include/commands/schemavar.h
new file mode 100644
index 0000000000..fcd516d192
--- /dev/null
+++ b/src/include/commands/schemavar.h
@@ -0,0 +1,39 @@
+/*-------------------------------------------------------------------------
+ *
+ * schemavar.h
+ *	  prototypes for schemavar.c.
+ *
+ * Portions Copyright (c) 1996-2018, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/commands/schemavar.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef SCHEMAVAR_H
+#define SCHEMAVAR_H
+
+#include "catalog/objectaddress.h"
+#include "nodes/parsenodes.h"
+#include "parser/parse_node.h"
+
+extern ObjectAddress DefineSchemaVariable(ParseState *pstate, CreateSchemaVarStmt *var);
+
+extern void ResetSchemaVariablesCache(void);
+
+extern char *get_schemavar_name(Oid varid);
+
+extern void SetSchemaVariable(Oid varid, Datum value, bool isNull, Oid typid, int32 typmod);
+extern Datum GetSchemaVariable(Oid varid, bool *isNull, Oid typid, int32 typmod,  int16 typlen, bool typbyval);
+extern void SetSchemaVariableSecure(Oid varid, Datum value, bool isNull, Oid typid, int32 typmod);
+extern Datum GetSchemaVariableSecure(Oid varid, bool *isNull, Oid typid, int32 typmod, int16 typlen, bool typbyval);
+
+extern void SetSchemaVariableField(Oid varid, const char *fieldname, Datum value, bool isNull, Oid typid, int32 typmod);
+extern Datum GetSchemaVariableField(Oid varid, const char *fieldname, bool *isNull, Oid typid, int32 typmod, int16 typlen, bool typbyval);
+extern void SetSchemaVariableFieldSecure(Oid varid, const char *fieldname, Datum value, bool isNull, Oid typid, int32 typmod);
+extern Datum GetSchemaVariableFieldSecure(Oid varid, const char *fieldname, bool *isNull, Oid typid, int32 typmod, int16 typlen, bool typbyval);
+
+extern void schemavar_get_typ_typmod(Oid varid, Oid *typ, int32 *typmod);
+extern bool is_schemavar(Oid varid, bool *is_rowtype);
+
+#endif
\ No newline at end of file
diff --git a/src/include/executor/execExpr.h b/src/include/executor/execExpr.h
index f7b1f77616..58d282dc92 100644
--- a/src/include/executor/execExpr.h
+++ b/src/include/executor/execExpr.h
@@ -138,6 +138,7 @@ typedef enum ExprEvalOp
 	EEOP_PARAM_EXEC,
 	EEOP_PARAM_EXTERN,
 	EEOP_PARAM_CALLBACK,
+	EEOP_PARAM_SCHEMA_VARIABLE,
 
 	/* return CaseTestExpr value */
 	EEOP_CASE_TESTVAL,
@@ -344,11 +345,14 @@ typedef struct ExprEvalStep
 			TupleDesc	argdesc;
 		}			nulltest_row;
 
-		/* for EEOP_PARAM_EXEC/EXTERN */
+		/* for EEOP_PARAM_EXEC/EXTERN/VARIABLE */
 		struct
 		{
-			int			paramid;	/* numeric ID for parameter */
-			Oid			paramtype;	/* OID of parameter's datatype */
+			int			paramid;		/* numeric ID for parameter */
+			Oid			paramtype;		/* OID of parameter's datatype */
+			int32		paramtypmod;	/* typmod of param (not used yet) */
+			int16		paramtyplen;	/* expected length */
+			bool		paramtypbyval;	/* is passed by value */
 		}			param;
 
 		/* for EEOP_PARAM_CALLBACK */
diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h
index adb159a6da..806b58f6ca 100644
--- a/src/include/nodes/nodes.h
+++ b/src/include/nodes/nodes.h
@@ -348,6 +348,7 @@ typedef enum NodeTag
 	T_CreateTableAsStmt,
 	T_CreateSeqStmt,
 	T_AlterSeqStmt,
+	T_CreateSchemaVarStmt,
 	T_VariableSetStmt,
 	T_VariableShowStmt,
 	T_DiscardStmt,
@@ -419,6 +420,7 @@ typedef enum NodeTag
 	T_CreateStatsStmt,
 	T_AlterCollationStmt,
 	T_CallStmt,
+	T_LetStmt,
 
 	/*
 	 * TAGS FOR PARSE TREE NODES (parsenodes.h)
@@ -661,6 +663,7 @@ typedef enum CmdType
 	CMD_UPDATE,					/* update stmt */
 	CMD_INSERT,					/* insert stmt */
 	CMD_DELETE,
+	CMD_LET,
 	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 6390f7e8c1..46ba22f10c 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -1505,6 +1505,15 @@ typedef struct UpdateStmt
 	WithClause *withClause;		/* WITH clause */
 } UpdateStmt;
 
+typedef struct LetStmt
+{
+	NodeTag		type;
+	List	  *target;			/* list of fields  */
+	Node	   *selectStmt;		/* the source SELECT/VALUES, or NULL */
+	int			location;
+} LetStmt;
+
+
 /* ----------------------
  *		Select Statement
  *
@@ -1682,6 +1691,7 @@ typedef enum ObjectType
 	OBJECT_TSTEMPLATE,
 	OBJECT_TYPE,
 	OBJECT_USER_MAPPING,
+	OBJECT_VARIABLE,
 	OBJECT_VIEW
 } ObjectType;
 
@@ -2497,6 +2507,20 @@ typedef struct AlterSeqStmt
 	bool		missing_ok;		/* skip error if a role is missing? */
 } AlterSeqStmt;
 
+/* ----------------------
+ *		Create VARIABLE Statement
+ * ----------------------
+ */
+typedef struct CreateSchemaVarStmt
+{
+	NodeTag		type;
+	RangeVar   *variable;		/* the variable to create */
+	TypeName   *typeName;		/* the scalar variable type */
+	Node	   *defexpr;		/* default expression */
+	List	   *fields;			/* the fields of composite variable type */
+	bool		if_not_exists;	/* just do nothing if it already exists? */
+} CreateSchemaVarStmt;
+
 /* ----------------------
  *		Create {Aggregate|Operator|Type} Statement
  * ----------------------
@@ -3231,7 +3255,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/primnodes.h b/src/include/nodes/primnodes.h
index f90aa7b2a1..44af7c3fbd 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -230,13 +230,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_SCHEMA_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_SCHEMA_VARIABLE
 } ParamKind;
 
 typedef struct Param
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index 23db40147b..d3ed3f4d0f 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -231,6 +231,7 @@ PG_KEYWORD("leading", LEADING, RESERVED_KEYWORD)
 PG_KEYWORD("leakproof", LEAKPROOF, UNRESERVED_KEYWORD)
 PG_KEYWORD("least", LEAST, COL_NAME_KEYWORD)
 PG_KEYWORD("left", LEFT, TYPE_FUNC_NAME_KEYWORD)
+PG_KEYWORD("let", LET, UNRESERVED_KEYWORD)
 PG_KEYWORD("level", LEVEL, UNRESERVED_KEYWORD)
 PG_KEYWORD("like", LIKE, TYPE_FUNC_NAME_KEYWORD)
 PG_KEYWORD("limit", LIMIT, RESERVED_KEYWORD)
@@ -434,6 +435,8 @@ PG_KEYWORD("validator", VALIDATOR, UNRESERVED_KEYWORD)
 PG_KEYWORD("value", VALUE_P, UNRESERVED_KEYWORD)
 PG_KEYWORD("values", VALUES, COL_NAME_KEYWORD)
 PG_KEYWORD("varchar", VARCHAR, COL_NAME_KEYWORD)
+PG_KEYWORD("variable", VARIABLE, UNRESERVED_KEYWORD)
+PG_KEYWORD("variables", VARIABLES, UNRESERVED_KEYWORD)
 PG_KEYWORD("variadic", VARIADIC, RESERVED_KEYWORD)
 PG_KEYWORD("varying", VARYING, UNRESERVED_KEYWORD)
 PG_KEYWORD("verbose", VERBOSE, TYPE_FUNC_NAME_KEYWORD)
diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h
index 0230543810..d25c061088 100644
--- a/src/include/parser/parse_node.h
+++ b/src/include/parser/parse_node.h
@@ -135,6 +135,8 @@ typedef Node *(*CoerceParamHook) (ParseState *pstate, Param *param,
  * to process them like UPDATE.  (Note this can change intra-statement, for
  * cases like INSERT ON CONFLICT UPDATE.)
  *
+ * p_is_let: true to process assignment expressions like LET.
+ *
  * p_windowdefs: list of WindowDefs representing WINDOW and OVER clauses.
  * We collect these while transforming expressions and then transform them
  * afterwards (so that any resjunk tlist items needed for the sort/group
@@ -184,6 +186,7 @@ struct ParseState
 	Relation	p_target_relation;	/* INSERT/UPDATE/DELETE target rel */
 	RangeTblEntry *p_target_rangetblentry;	/* target rel's RTE */
 	bool		p_is_insert;	/* process assignment like INSERT not UPDATE */
+	bool		p_is_let;		/* process assignment LET stmt */
 	List	   *p_windowdefs;	/* raw representations of window clauses */
 	ParseExprKind p_expr_kind;	/* what kind of expression we're parsing */
 	int			p_next_resno;	/* next targetlist resno to assign */
diff --git a/src/include/parser/parse_relation.h b/src/include/parser/parse_relation.h
index b9792acdae..c894147d06 100644
--- a/src/include/parser/parse_relation.h
+++ b/src/include/parser/parse_relation.h
@@ -129,4 +129,10 @@ extern Oid	attnumTypeId(Relation rd, int attid);
 extern Oid	attnumCollationId(Relation rd, int attid);
 extern bool isQueryUsingTempRelation(Query *query);
 
+extern RangeVar *makeRangeVarForTargetOfSchemaVariable(ParseState *pstate,
+				  char *field1, char *field2, char *field3,
+				  int location, char **fieldname, int *fieldname_pos, bool noerror);
+
+extern Node *toSchemaVariable(ParseState *pstate, char *nspname, char *varname, char *fieldname, int location);
+
 #endif							/* PARSE_RELATION_H */
diff --git a/src/include/parser/parse_target.h b/src/include/parser/parse_target.h
index ec6e0c102f..72eb3cd7b6 100644
--- a/src/include/parser/parse_target.h
+++ b/src/include/parser/parse_target.h
@@ -43,4 +43,16 @@ extern TupleDesc expandRecordVariable(ParseState *pstate, Var *var,
 extern char *FigureColname(Node *node);
 extern char *FigureIndexColname(Node *node);
 
+extern Node *transformAssignmentIndirection(ParseState *pstate,
+							   Node *basenode,
+							   const char *targetName,
+							   bool targetIsArray,
+							   Oid targetTypeId,
+							   int32 targetTypMod,
+							   Oid targetCollation,
+							   ListCell *indirection,
+							   Node *rhs,
+							   int location);
+
+
 #endif							/* PARSE_TARGET_H */
diff --git a/src/include/utils/acl.h b/src/include/utils/acl.h
index f4d4be8d0d..d0737a9e4b 100644
--- a/src/include/utils/acl.h
+++ b/src/include/utils/acl.h
@@ -166,6 +166,7 @@ typedef 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_SELECT|ACL_UPDATE)
 
 /* operation codes for pg_*_aclmask */
 typedef enum
diff --git a/src/test/regress/expected/schema_variables.out b/src/test/regress/expected/schema_variables.out
new file mode 100644
index 0000000000..ab1d3ac66d
--- /dev/null
+++ b/src/test/regress/expected/schema_variables.out
@@ -0,0 +1,456 @@
+CREATE VARIABLE var1 AS integer;
+CREATE TEMP VARIABLE var2 AS text;
+DROP VARIABLE var1, var2;
+-- functional interface
+CREATE VARIABLE var1 AS numeric;
+SELECT set_schema_variable('var1', 333);
+ set_schema_variable 
+---------------------
+ 
+(1 row)
+
+SELECT get_schema_variable('var1', null::numeric);
+ get_schema_variable 
+---------------------
+                 333
+(1 row)
+
+SELECT set_schema_variable('var1', 333::integer);
+ set_schema_variable 
+---------------------
+ 
+(1 row)
+
+SELECT get_schema_variable('var1', null::numeric);
+ get_schema_variable 
+---------------------
+                 333
+(1 row)
+
+SELECT set_schema_variable('var1', '333.55'::text);
+ set_schema_variable 
+---------------------
+ 
+(1 row)
+
+SELECT get_schema_variable('var1', null::numeric);
+ get_schema_variable 
+---------------------
+              333.55
+(1 row)
+
+SELECT get_schema_variable('var1', null::int);
+ get_schema_variable 
+---------------------
+                 334
+(1 row)
+
+SELECT get_schema_variable('var1', null::text);
+ get_schema_variable 
+---------------------
+ 333.55
+(1 row)
+
+-- access rights test
+CREATE ROLE var_test_role;
+SET ROLE TO var_test_role;
+-- should to fail
+SELECT set_schema_variable('var1', '1000'::text);
+ERROR:  permission denied for schema variable var1
+SELECT get_schema_variable('var1', null::numeric);
+ERROR:  permission denied for schema variable var1
+SET ROLE TO DEFAULT;
+GRANT SELECT ON VARIABLE var1 TO var_test_role;
+SET ROLE TO var_test_role;
+-- should to fail
+SELECT set_schema_variable('var1', '1000'::text);
+ERROR:  permission denied for schema variable var1
+-- should to work
+SELECT get_schema_variable('var1', null::numeric);
+ get_schema_variable 
+---------------------
+              333.55
+(1 row)
+
+SET ROLE TO DEFAULT;
+GRANT UPDATE ON VARIABLE var1 TO var_test_role;
+SET ROLE TO var_test_role;
+-- should to work
+SELECT set_schema_variable('var1', '1000'::text);
+ set_schema_variable 
+---------------------
+ 
+(1 row)
+
+SELECT get_schema_variable('var1', null::numeric);
+ get_schema_variable 
+---------------------
+                1000
+(1 row)
+
+SET ROLE TO DEFAULT;
+REVOKE ALL ON VARIABLE var1 FROM var_test_role;
+CREATE VARIABLE var AS integer;
+SELECT set_schema_variable('public.var', 1234);
+ set_schema_variable 
+---------------------
+ 
+(1 row)
+
+SELECT public.var;
+ var  
+------
+ 1234
+(1 row)
+
+DO $$
+BEGIN
+  RAISE NOTICE 'public.var is = %', public.var;
+END;
+$$;
+NOTICE:  public.var is = 1234
+CREATE OR REPLACE FUNCTION secure_var()
+RETURNS int AS $$
+  SELECT public.var;
+$$ LANGUAGE sql SECURITY DEFINER;
+SELECT secure_var();
+ secure_var 
+------------
+       1234
+(1 row)
+
+SET ROLE TO var_test_role;
+-- should to fail
+SELECT public.var;
+ERROR:  permission denied for schema variable var
+-- should to work;
+SELECT secure_var();
+ secure_var 
+------------
+       1234
+(1 row)
+
+SET ROLE TO DEFAULT;
+EXPLAIN (VERBOSE, COSTS OFF) SELECT * FROM generate_series(1,100) g(v) WHERE v = var;
+                  QUERY PLAN                   
+-----------------------------------------------
+ Function Scan on pg_catalog.generate_series g
+   Output: v
+   Function Call: generate_series(1, 100)
+   Filter: (g.v = var)
+(4 rows)
+
+CREATE VIEW schema_var_view AS SELECT var;
+SELECT * FROM schema_var_view;
+ var  
+------
+ 1234
+(1 row)
+
+\c -
+-- should to work still, but var will be empty
+SELECT * FROM schema_var_view;
+ var 
+-----
+    
+(1 row)
+
+LET var1 = pi();
+SELECT var1;
+       var1       
+------------------
+ 3.14159265358979
+(1 row)
+
+-- we can look on execution plan
+EXPLAIN (VERBOSE, COSTS OFF) LET var1 = pi();
+            QUERY PLAN            
+----------------------------------
+ Let on public.var1
+   ->  Result
+         Output: 3.14159265358979
+(3 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 public.var3 = COALESCE(public.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 to fail
+LET var3 = 0;
+ERROR:  permission denied for schema variable var3
+SET ROLE TO DEFAULT;
+DROP VIEW schema_var_view;
+DROP VARIABLE var CASCADE;
+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 (x int, y int, z numeric(10,2));
+\d v1
+schema variable "public.v1"
+ Column |     Type      
+--------+---------------
+ x      | integer
+ y      | integer
+ z      | numeric(10,2)
+
+\d v2
+schema variable "public.v2"
+ Column |     Type      
+--------+---------------
+ x      | integer
+ y      | integer
+ z      | numeric(10,2)
+
+-- explicit casting is necessary
+LET v1 = (1,2,3.14)::sv_xyz;
+LET v2 = (10,20,3.14*10)::v2;
+-- should to work too - there are prepared casts
+LET v1 = (1,2,3.14)::v1;
+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 get_schema_variable('v1', 'z', null::double precision);
+ get_schema_variable 
+---------------------
+                3.14
+(1 row)
+
+SELECT get_schema_variable('v2', 'z', null::double precision);
+ get_schema_variable 
+---------------------
+                31.4
+(1 row)
+
+-- should to fail
+SELECT get_schema_variable('v1', 'w', null::double precision);
+ERROR:  the variable "v1" has not field "w"
+SELECT get_schema_variable('v2', 'w', null::double precision);
+ERROR:  the variable "v2" has not field "w"
+CREATE VARIABLE v3 AS integer;
+SELECT get_schema_variable('v3', 'w', null::double precision);
+ERROR:  the variable "v3" is not of row type
+SELECT set_schema_variable('v1', 'z', 3.14 * 2);
+ set_schema_variable 
+---------------------
+ 
+(1 row)
+
+SELECT v1;
+     v1     
+------------
+ (1,2,6.28)
+(1 row)
+
+SELECT set_schema_variable('v1', 'z', null::double precision);
+ set_schema_variable 
+---------------------
+ 
+(1 row)
+
+SELECT v1;
+   v1   
+--------
+ (1,2,)
+(1 row)
+
+SELECT set_schema_variable('v1', 'z', 3.3333::double precision);
+ set_schema_variable 
+---------------------
+ 
+(1 row)
+
+SELECT set_schema_variable('v1', 'x', 10000.22);
+ set_schema_variable 
+---------------------
+ 
+(1 row)
+
+SELECT v1;
+       v1       
+----------------
+ (10000,2,3.33)
+(1 row)
+
+SELECT v1.x + v1.z;
+ ?column? 
+----------
+ 10003.33
+(1 row)
+
+SELECT v2.x + v2.z;
+ ?column? 
+----------
+    41.40
+(1 row)
+
+-- access to composite fields should be safe too
+-- should to 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;
+DROP VARIABLE v3;
+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 to fail
+SELECT varx.xxx;
+ERROR:  missing FROM-clause entry for table "varx"
+LINE 1: SELECT varx.xxx;
+               ^
+-- 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 VARIABLE v1 AS (a int, b numeric, c text);
+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 to fail
+LET v1.x = 10;
+ERROR:  cannot assign to field "x" of column "x" because there is no such column in data type v1
+DROP VARIABLE v1;
+-- 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 VARIABLE va2 AS (a numeric, b numeric[]);
+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;
+-- default values
+CREATE VARIABLE v1 AS numeric DEFAULT pi();
+LET v1 = v1 * 2;
+SELECT v1;
+        v1        
+------------------
+ 6.28318530717958
+(1 row)
+
+CREATE VARIABLE v2 AS (a numeric, b text DEFAULT 'hello');
+LET public.v2.a = pi();
+SELECT v2;
+            v2            
+--------------------------
+ (3.14159265358979,hello)
+(1 row)
+
+DROP VARIABLE v1;
+DROP VARIABLE v2;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 16f979c8d9..9bf379b87b 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -111,7 +111,7 @@ test: select_views portals_p2 foreign_key cluster dependency guc bitmapops combo
 # 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 without_oid conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml
+test: plancache limit plpgsql copy2 temp domain rangefuncs prepare without_oid 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 42632be675..42bf4ecb3f 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -191,3 +191,4 @@ test: partition_aggregate
 test: event_trigger
 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..27bbd1fdc3
--- /dev/null
+++ b/src/test/regress/sql/schema_variables.sql
@@ -0,0 +1,251 @@
+CREATE VARIABLE var1 AS integer;
+CREATE TEMP VARIABLE var2 AS text;
+
+DROP VARIABLE var1, var2;
+
+-- functional interface
+CREATE VARIABLE var1 AS numeric;
+SELECT set_schema_variable('var1', 333);
+SELECT get_schema_variable('var1', null::numeric);
+
+SELECT set_schema_variable('var1', 333::integer);
+SELECT get_schema_variable('var1', null::numeric);
+
+SELECT set_schema_variable('var1', '333.55'::text);
+SELECT get_schema_variable('var1', null::numeric);
+SELECT get_schema_variable('var1', null::int);
+SELECT get_schema_variable('var1', null::text);
+
+-- access rights test
+
+CREATE ROLE var_test_role;
+
+SET ROLE TO var_test_role;
+
+-- should to fail
+SELECT set_schema_variable('var1', '1000'::text);
+SELECT get_schema_variable('var1', null::numeric);
+
+SET ROLE TO DEFAULT;
+
+GRANT SELECT ON VARIABLE var1 TO var_test_role;
+
+SET ROLE TO var_test_role;
+-- should to fail
+SELECT set_schema_variable('var1', '1000'::text);
+-- should to work
+SELECT get_schema_variable('var1', null::numeric);
+
+SET ROLE TO DEFAULT;
+
+GRANT UPDATE ON VARIABLE var1 TO var_test_role;
+
+SET ROLE TO var_test_role;
+
+-- should to work
+SELECT set_schema_variable('var1', '1000'::text);
+SELECT get_schema_variable('var1', null::numeric);
+
+SET ROLE TO DEFAULT;
+
+REVOKE ALL ON VARIABLE var1 FROM var_test_role;
+
+CREATE VARIABLE var AS integer;
+
+SELECT set_schema_variable('public.var', 1234);
+
+SELECT public.var;
+
+DO $$
+BEGIN
+  RAISE NOTICE 'public.var is = %', public.var;
+END;
+$$;
+
+CREATE OR REPLACE FUNCTION secure_var()
+RETURNS int AS $$
+  SELECT public.var;
+$$ LANGUAGE sql SECURITY DEFINER;
+
+SELECT secure_var();
+
+SET ROLE TO var_test_role;
+
+-- should to fail
+SELECT public.var;
+
+-- should to work;
+SELECT secure_var();
+
+SET ROLE TO DEFAULT;
+
+EXPLAIN (VERBOSE, COSTS OFF) SELECT * FROM generate_series(1,100) g(v) WHERE v = var;
+
+CREATE VIEW schema_var_view AS SELECT var;
+
+SELECT * FROM schema_var_view;
+
+\c -
+
+-- should to work still, but var will be empty
+SELECT * FROM schema_var_view;
+
+LET var1 = pi();
+
+SELECT var1;
+
+-- we can look on execution plan
+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 public.var3 = COALESCE(public.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 to fail
+LET var3 = 0;
+
+SET ROLE TO DEFAULT;
+
+DROP VIEW schema_var_view;
+
+DROP VARIABLE var CASCADE;
+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 (x int, y int, z numeric(10,2));
+
+\d v1
+\d v2
+
+-- explicit casting is necessary
+LET v1 = (1,2,3.14)::sv_xyz;
+LET v2 = (10,20,3.14*10)::v2;
+
+-- should to work too - there are prepared casts
+LET v1 = (1,2,3.14)::v1;
+
+SELECT v1;
+SELECT v2;
+SELECT (v1).*;
+SELECT (v2).*;
+
+SELECT get_schema_variable('v1', 'z', null::double precision);
+SELECT get_schema_variable('v2', 'z', null::double precision);
+
+-- should to fail
+SELECT get_schema_variable('v1', 'w', null::double precision);
+SELECT get_schema_variable('v2', 'w', null::double precision);
+
+CREATE VARIABLE v3 AS integer;
+SELECT get_schema_variable('v3', 'w', null::double precision);
+
+SELECT set_schema_variable('v1', 'z', 3.14 * 2);
+SELECT v1;
+SELECT set_schema_variable('v1', 'z', null::double precision);
+SELECT v1;
+SELECT set_schema_variable('v1', 'z', 3.3333::double precision);
+SELECT set_schema_variable('v1', 'x', 10000.22);
+SELECT v1;
+
+SELECT v1.x + v1.z;
+SELECT v2.x + v2.z;
+
+-- access to composite fields should be safe too
+-- should to fail
+SET ROLE TO var_test_role;
+
+SELECT v2.x;
+
+SET ROLE TO DEFAULT;
+
+
+DROP VARIABLE v1;
+DROP VARIABLE v2;
+DROP VARIABLE v3;
+
+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 to 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 VARIABLE v1 AS (a int, b numeric, c text);
+LET v1 = (1, pi(), 'hello');
+SELECT v1;
+LET v1.b = 10.2222;
+SELECT v1;
+
+-- should to fail
+LET v1.x = 10;
+
+DROP VARIABLE v1;
+
+-- arrays are supported
+CREATE VARIABLE va1 AS numeric[];
+LET va1 = ARRAY[1.1,2.1];
+LET va1[1] = 10.1;
+SELECT va1;
+
+CREATE VARIABLE va2 AS (a numeric, b numeric[]);
+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;
+
+-- default values
+CREATE VARIABLE v1 AS numeric DEFAULT pi();
+LET v1 = v1 * 2;
+SELECT v1;
+
+CREATE VARIABLE v2 AS (a numeric, b text DEFAULT 'hello');
+LET public.v2.a = pi();
+SELECT v2;
+
+DROP VARIABLE v1;
+DROP VARIABLE v2;
+
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 54850ee4d6..51e0a179b6 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -431,6 +431,7 @@ CreateReplicationSlotCmd
 CreateRoleStmt
 CreateSchemaStmt
 CreateSchemaStmtContext
+CreateSchemaVarStmt
 CreateSeqStmt
 CreateStatsStmt
 CreateStmt