public inbox for [email protected]  
help / color / mirror / Atom feed
From: Pavel Stehule <[email protected]>
To: Laurenz Albe <[email protected]>
Cc: Erik Rijkers <[email protected]>
Cc: Michael Paquier <[email protected]>
Cc: Amit Kapila <[email protected]>
Cc: DUVAL REMI <[email protected]>
Cc: PostgreSQL Hackers <[email protected]>
Subject: Re: proposal: schema variables
Date: Sun, 10 Nov 2024 23:41:31 +0100
Message-ID: <CAFj8pRBGffSQjtwsoF-8KffOveMAH9P7R7VVDXW2DTBY0_eosA@mail.gmail.com> (raw)
In-Reply-To: <[email protected]>
References: <CAFj8pRBoGBKopkiTa4ki3dMgy-cSTRZ-BQPKOq7=Tk0SSNsowA@mail.gmail.com>
	<CAFj8pRBCiWn12H9FHymYs17fk68nRd9Xpn+SYf18TLdb2YSUrQ@mail.gmail.com>
	<CAA4eK1JV-Ox0oRFdXnPqXSzM84wTR5QFkRzCpNVF_+0FNjS5Mg@mail.gmail.com>
	<CAFj8pRByCCcgDkeXyafAnH6LgxtAyCVwg6yfJAhyTY6GLscfZg@mail.gmail.com>
	<CAFj8pRBvUonC_ug3F=w1Q55Dp=DggojvAeL7Vmh14Q-WhFHxzw@mail.gmail.com>
	<CAFj8pRDj72P-f=SUygtOXnTOBQ0RzmL_fN=wLfaCzcbPVpGgzw@mail.gmail.com>
	<CAFj8pRDD2GQaJ_iDT4vSVe658+oHRXU2S2af7Y1-it9jaP8VFg@mail.gmail.com>
	<[email protected]>
	<CAFj8pRAFktynx5wkanv5SRuzXkZgXu77XpVACiSE=v7i1xHFbw@mail.gmail.com>
	<CAFj8pRA=bn_g5T2AZduy5gNOQoOnUJ+pMHmnRMHi6mR0n=TAsA@mail.gmail.com>
	<[email protected]>
	<CAFj8pRC9de05HSb4tEHDUwJ98+4Wh30W-rJrNOPnTz6ARcv0Fw@mail.gmail.com>
	<CAFj8pRBC5Wz1xHKKBmKsM0xYN0+PdSZ5oXPsk5SZt+VprdUW3A@mail.gmail.com>
	<CAFj8pRAh4pzMoZrKCLt9h+Lr2L=vhgs2PjAF45uLbp_7sijM5w@mail.gmail.com>
	<CAFj8pRA-kxQ1oErcuDeUKYsrgwB5XGLhquatwxOe3dCVy1gcyQ@mail.gmail.com>
	<[email protected]>
	<CAFj8pRBbt2xhY9PyabOY0ZN+Aig6ee3oCon-DM9qi0Uw_3qfbw@mail.gmail.com>
	<CAFj8pRDSa52J7kPmCYXgq1BBbu3YBXwpdSOVpjgU=hnE2k04Cw@mail.gmail.com>
	<CAFj8pRD+QiWOoPrFk2NnPs3t5Eaf4X=aGRV-9ww11cnPP+fV4g@mail.gmail.com>
	<[email protected]>
	<CAFj8pRCGTjqHvH9oeiSf4T6Bydhk9pm033DxxibgF+B7SHC6MQ@mail.gmail.com>
	<CAFj8pRAzNDhFgbZnT0T0mJ7ygA1Qje1Hc0TiKwXM8++kGooPYg@mail.gmail.com>
	<[email protected]>
	<CAFj8pRBarjJYfkN-0-i=JRZJ4PTOYC+K7XgAhfdDqWGqRiPkyw@mail.gmail.com>
	<CAFj8pRDqdWdCULxd5asbKs5C4e9kT2TuKBkR5L-e1=hP5wF2uw@mail.gmail.com>
	<[email protected]>
	<CAFj8pRCPW56pFr0F0BcasdXjFeo3SFixNSpWKaBk0ibvznum-A@mail.gmail.com>
	<CAFj8pRD1Feit93CgwmYm1Q=X+M+AZqffCEZPFQ7qEMNHZRN4fA@mail.gmail.com>
	<CAFj8pRCc=B9-FRQg5eWDSkGwS2vpkq88hR6042cmPPizHuEGSA@mail.gmail.com>
	<CAFj8pRBk8x7afUXKLBOU-Ctg6A7QJvTAGGVEi0b6Jc8YTe8nUg@mail.gmail.com>
	<CAFj8pRCSwHQ4BJUbjF2YEausK1Z6+ejMyedpqAnWJbG+FEJDLw@mail.gmail.com>
	<CAFj8pRAbY+N+UqjqgESL5x-bsGmV+aVyyUkxUSgaGDZToZjDqQ@mail.gmail.com>
	<CAFj8pRBzKcqzj=23BHfv1QaXHt=2_SN=uhdR3rb_dAVQoit7ug@mail.gmail.com>
	<CAFj8pRCi-n6SzkAB+OHG=TZvL13xxta_qgffBLDOY0HEBqDhvg@mail.gmail.com>
	<[email protected]>
	<CAFj8pRBaD0_bMrCREWnVLfcTMdc0v7ns7Rt=sEvd1EoFmLfarQ@mail.gmail.com>
	<[email protected]>
	<CAFj8pRDXo-RRcy2VFDm_vzv3Eaaz6Ex=X19up=x8W4COyBNmaQ@mail.gmail.com>
	<[email protected]>
	<CAFj8pRDgraz64HHcCoFGCKDM1Cxv1ukELsqfsxux4JP3PM6RyQ@mail.gmail.com>
	<[email protected]>
	<CAFj8pRAsjr0pzQpQeRrQ-wR-ew1oTc6qkW=4355KveN1jKQ+1Q@mail.gmail.com>
	<[email protected]>
	<[email protected]>
	<[email protected]>
	<CAFj8pRDF-F5diNTXr206QfQFauFNK1vu_7NARXd0dtdwGTWLMQ@mail.gmail.com>
	<[email protected]>
	<CAFj8pRCy8NTG3ewOrH0KLQgrMpVMSzPjhoLKW=c1Jjz8QWNSKw@mail.gmail.com>
	<CAFj8pRB-ey7mCnKqMhBBrkv2mXOvTLfAEBy2LZxQsWPCBvguGQ@mail.gmail.com>
	<[email protected]>
	<CAFj8pRBvUwfs96MX2_=unHQ107diUOK4S_WD=E4XBXkpXG3VjA@mail.gmail.com>
	<CAFj8pRCDWjgo=yLEK-jRDe5VwuNSQSmMqPPmysQf9nHA8gifnw@mail.gmail.com>
	<CAFj8pRA2jbXRTGgM1co8gLCNttgaH8miRUSSauMJKWgousDsDg@mail.gmail.com>
	<CAFj8pRA6nJRg8oq5k_Cdk5oH5_FjtViG8XLmHKrkqOuiMT_CKQ@mail.gmail.com>
	<CAFj8pRBWMLP3Vyr8z+19eaiJKQoVtBfmDhNJFKXDX6uFzd4vBQ@mail.gmail.com>
	<[email protected]>
	<[email protected]>
	<CAFj8pRAjU-X6rEE9=1++PdtXOPc2uo=yu-tcFXByi-kN3B_7Vw@mail.gmail.com>
	<CAFj8pRC+hPCc2X88xC=pTJoqmVPApDsageZOMyqaxi5788WxHA@mail.gmail.com>
	<CAFj8pRDJ9cq00VYSHxs6LsoHNWjhYXyWWBtV6UgeWwhs0AHa9A@mail.gmail.com>
	<[email protected]>
	<CAFj8pRBuhB_jgsGx4-Nhpow0vi7g98RjKwTRPM3dSbiKjWdqQA@mail.gmail.com>
	<[email protected]>

Hi

po 4. 11. 2024 v 10:24 odesílatel Laurenz Albe <[email protected]>
napsal:

> On Sat, 2024-11-02 at 08:36 +0100, Pavel Stehule wrote:
> > so 2. 11. 2024 v 6:46 odesílatel Laurenz Albe <[email protected]>
> napsal:
> > > - The commit message is headed "memory cleaning after DROP VARIABLE",
> but
> > >   the rest of the commit message speaks of sinval messages.  These two
> > >   things are independent, aren't they?  And both lead to the need to
> validate
> > >   the variables, right?
> >
> > Maybe it can be formulated differently, but it is true. There are a lot
> of sinval messages, but in this case
> > only sinval messages related to DROP VARIABLE are interesting.
>
> Okay...
>
> > >   Then this code comment would for example be wrong:
> > >
> > >      /* true after accepted sinval message */
> > >      static bool needs_validation = false;
> > >
> > >   It also becomes "true" after DROP VARIABLE, right?
> > >   I am happy to fix the comment, but I want to understand the patch
> first.
> > >
> >
> > sinval message can be raised by any operation over the pg_variable table.
>
> I set a watchpoint on "needs_validation", and when I run DROP VARIABLE, it
> is called
> directly from the command:
>
> Hardware watchpoint 2: needs_validation
>
> Old value = false
> New value = true
> 0x00000000006ad44c in SessionVariableDropPostprocess (varid=<optimized
> out>) at session_variable.c:169
> 169                             svar->drop_lxid = MyProc->vxid.lxid;
> (gdb) where
> #0  0x00000000006ad44c in SessionVariableDropPostprocess (varid=<optimized
> out>) at session_variable.c:169
> #1  0x0000000000625dec in DropVariableById (varid=<optimized out>) at
> pg_variable.c:259
> #2  0x00000000005fec57 in doDeletion (object=0x4ac85e8, flags=0) at
> dependency.c:1450
> ...
> #8  0x00000000008bb7e0 in standard_ProcessUtility (pstmt=0x49ac700,
> queryString=0x49abbb0 "DROP VARIABLE a;", readOnlyTree=<optimized out>,
>     context=PROCESS_UTILITY_TOPLEVEL, params=0x0, queryEnv=0x0,
> dest=0x49acac0, qc=0x7ffe19d2db60) at utility.c:1076
> ...
> #12 0x00000000008b6742 in exec_simple_query (query_string=0x49abbb0 "DROP
> VARIABLE a;") at postgres.c:1283
>
> Later, there is a sinval message and "pg_variable_cache_callback()" is
> called, which sets
> "needs_validation" again.
>
> Perhaps my confustion is as follows: if DROP VARIABLE sends an
> invalidation message,
> and the handler sets "needs_validation", why is it necessary to set
> "needs_validation"
> in SessionVariableDropPostprocess() too?
>
> If we didn't set "needs_validation" in SessionVariableDropPostprocess(),
> the comment
> would be true.
>

I thought about it, and it  is redundant. I removed it. All tests passed
still.



> > > - I see that the patch adds cleanup of invalid session variable to each
> > >   COMMIT.  Is that a good idea?  I'd expect that it is good enough to
> clean
> > >   up whenever session variables are accessed.
> > >   Calling remove_invalid_session_variables() during each COMMIT will
> affect
> > >   all transactions, and I don't see the benefit.
> >
> > If I remember well, there were two reasons why I did it.
> >
> > 1. Minimize the unwanted surprises for users that will check memory
> usage - So if you
> >    drop the variables, then the allocated space is released in possibly
> near time.
> >    The rule - allocated space is released, when in the next transaction
> you use any
> >    session variable looks a little bit crazy (although I think so there
> will not be real
> >    significant difference in functionality).
> >    Correct me, if I am wrong, but I don't remember any memory (or
> resource) cleaning
> >    in Postgres, that is delayed to second transactions. I agree, there
> is overhead of cleaning,
> >    but this can be very fast when the user doesn't use session
> variables, because the hash table
> >    with session variables is not initialized. I can imagine some usage
> some hooks there as alternative
>
> I don't think that is a good enough reason.
>
> Memory used by an idle backend is not totally predictable for other
> reasons as well:
> - the catalog cache
> - memory that PostgreSQL freed, but that is kept in the malloc arena so
> that
>   it can be reused for the next malloc() call
>
> I believe that the disadvantage of keeping some memory allocated across
> transaction
> is not as bad as the pain of going through all variables on each COMMIT.
> If you have set one or two session variables and run a lot of statements in
> autocommit mode, that is an unnecessary overhead.
>
> > 2. The main reason why it is implemented is implementation of temporal
> variables
> >    with RESET or DROP on transaction end. Related code should be
> triggered at
> >    commit time, it cannot be delayed.
>
> That makes sense.
>
> If I remember right, temporary variables are an optional feature.
> So I suggest that you move AtPreEOXact_SessionVariables() to the patch
> that adds
> temporary session variables.
>

moved


>
> > >   Also, do we need to call it during pg_session_variables()?
> >
> > I think it can be removed. Originally pg_session_variables showed only
> valid variables, but it is not true now.
>

removed


>
> Right.
>
> I'll wait for your reaction to my above suggestions before I start working
> on the comments.
>

new patch set attached

Regards

Pavel


>
> Yours,
> Laurenz Albe
>


Attachments:

  [text/x-patch] v20241110-0017-expression-with-session-variables-can-be-inlined.patch (4.2K, 3-v20241110-0017-expression-with-session-variables-can-be-inlined.patch)
  download | inline diff:
From 752fbf2fd1e82445a2015824872f3c665a1d6bd8 Mon Sep 17 00:00:00 2001
From: "[email protected]" <[email protected]>
Date: Sat, 20 Jan 2024 20:35:38 +0100
Subject: [PATCH 17/20] expression with session variables can be inlined

There is not an reason why session variables should to block inlining.
(of SQL functions). I can imagine some use cases like wrapping, and
inlining significantly reduces an overhead of SQL functions.
---
 src/backend/optimizer/util/clauses.c          | 37 ++++++++++++++-----
 .../regress/expected/session_variables.out    |  7 ++--
 2 files changed, 30 insertions(+), 14 deletions(-)

diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c
index 8cee732561d..f79937980ee 100644
--- a/src/backend/optimizer/util/clauses.c
+++ b/src/backend/optimizer/util/clauses.c
@@ -4744,8 +4744,7 @@ inline_function(Oid funcid, Oid result_type, Oid result_collid,
 		querytree->limitOffset ||
 		querytree->limitCount ||
 		querytree->setOperations ||
-		(list_length(querytree->targetList) != 1) ||
-		querytree->hasSessionVariables)
+		(list_length(querytree->targetList) != 1))
 		goto fail;
 
 	/* If the function result is composite, resolve it */
@@ -4949,21 +4948,39 @@ substitute_actual_parameters_mutator(Node *node,
 {
 	if (node == NULL)
 		return NULL;
+
+
+	/*
+	 * SQL functions can contain two different kind of params. The nodes with
+	 * paramkind PARAM_EXTERN are related to function's arguments (and should
+	 * be replaced in this step), because this is how we apply the function's
+	 * arguments for an expression.
+	 *
+	 * The nodes with paramkind PARAM_VARIABLE are related to usage of session
+	 * variables. The values of session variables are not passed to expression
+	 * by expression arguments, so it should not be replaced here by
+	 * function's arguments.
+	 */
 	if (IsA(node, Param))
 	{
 		Param	   *param = (Param *) node;
 
-		if (param->paramkind != PARAM_EXTERN)
+		if (param->paramkind != PARAM_EXTERN &&
+			param->paramkind != PARAM_VARIABLE)
 			elog(ERROR, "unexpected paramkind: %d", (int) param->paramkind);
-		if (param->paramid <= 0 || param->paramid > context->nargs)
-			elog(ERROR, "invalid paramid: %d", param->paramid);
 
-		/* Count usage of parameter */
-		context->usecounts[param->paramid - 1]++;
+		if (param->paramkind == PARAM_EXTERN)
+		{
+			if (param->paramid <= 0 || param->paramid > context->nargs)
+				elog(ERROR, "invalid paramid: %d", param->paramid);
 
-		/* Select the appropriate actual arg and replace the Param with it */
-		/* We don't need to copy at this time (it'll get done later) */
-		return list_nth(context->args, param->paramid - 1);
+			/* Count usage of parameter */
+			context->usecounts[param->paramid - 1]++;
+
+			/* Select the appropriate actual arg and replace the Param with it */
+			/* We don't need to copy at this time (it'll get done later) */
+			return list_nth(context->args, param->paramid - 1);
+		}
 	}
 	return expression_tree_mutator(node, substitute_actual_parameters_mutator,
 								   (void *) context);
diff --git a/src/test/regress/expected/session_variables.out b/src/test/regress/expected/session_variables.out
index 20bf282b3f2..a66c8df415d 100644
--- a/src/test/regress/expected/session_variables.out
+++ b/src/test/regress/expected/session_variables.out
@@ -105,7 +105,6 @@ SELECT var1;
 ERROR:  permission denied for session variable var1
 SELECT sqlfx(20);
 ERROR:  permission denied for session variable var1
-CONTEXT:  SQL function "sqlfx" statement 1
 SELECT plpgsqlfx(20);
 ERROR:  permission denied for session variable var1
 CONTEXT:  PL/pgSQL function plpgsqlfx(integer) line 1 at RETURN
@@ -212,10 +211,10 @@ SELECT sqlfx1(sqlfx2('Hello'));
 
 -- inlining is blocked
 EXPLAIN (COSTS OFF, VERBOSE) SELECT sqlfx1(sqlfx2('Hello'));
-                      QUERY PLAN                      
-------------------------------------------------------
+                                 QUERY PLAN                                  
+-----------------------------------------------------------------------------
  Result
-   Output: sqlfx1(sqlfx2('Hello'::character varying))
+   Output: ((var1 || ', '::text) || ((var2 || ', '::text) || 'Hello'::text))
 (2 rows)
 
 DROP FUNCTION sqlfx1(varchar);
-- 
2.47.0



  [text/x-patch] v20241110-0020-pg_restore-A-variable.patch (2.8K, 4-v20241110-0020-pg_restore-A-variable.patch)
  download | inline diff:
From dbf31733dbb58437f2e8c70ca346d7cae55c9225 Mon Sep 17 00:00:00 2001
From: "[email protected]" <[email protected]>
Date: Sun, 21 Jul 2024 17:04:41 +0200
Subject: [PATCH 20/20] pg_restore -A, --variable

possibility to restore session variable specified by name
---
 doc/src/sgml/ref/pg_restore.sgml | 11 +++++++++++
 src/bin/pg_dump/pg_restore.c     |  9 ++++++++-
 2 files changed, 19 insertions(+), 1 deletion(-)

diff --git a/doc/src/sgml/ref/pg_restore.sgml b/doc/src/sgml/ref/pg_restore.sgml
index b8b27e1719e..7a3a4eabe25 100644
--- a/doc/src/sgml/ref/pg_restore.sgml
+++ b/doc/src/sgml/ref/pg_restore.sgml
@@ -106,6 +106,17 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>-A <replaceable class="parameter">session_variable</replaceable></option></term>
+      <term><option>--variable=<replaceable class="parameter">session_variable</replaceable></option></term>
+      <listitem>
+       <para>
+        Restore a named session variable only.  Multiple session variables may
+        be specified with multiple <option>-A</option> switches.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>-c</option></term>
       <term><option>--clean</option></term>
diff --git a/src/bin/pg_dump/pg_restore.c b/src/bin/pg_dump/pg_restore.c
index f2c1020d053..f916736d761 100644
--- a/src/bin/pg_dump/pg_restore.c
+++ b/src/bin/pg_dump/pg_restore.c
@@ -104,6 +104,7 @@ main(int argc, char **argv)
 		{"trigger", 1, NULL, 'T'},
 		{"use-list", 1, NULL, 'L'},
 		{"username", 1, NULL, 'U'},
+		{"variable", 1, NULL, 'A'},
 		{"verbose", 0, NULL, 'v'},
 		{"single-transaction", 0, NULL, '1'},
 
@@ -154,7 +155,7 @@ main(int argc, char **argv)
 		}
 	}
 
-	while ((c = getopt_long(argc, argv, "acCd:ef:F:h:I:j:lL:n:N:Op:P:RsS:t:T:U:vwWx1",
+	while ((c = getopt_long(argc, argv, "A:acCd:ef:F:h:I:j:lL:n:N:Op:P:RsS:t:T:U:vwWx1",
 							cmdopts, NULL)) != -1)
 	{
 		switch (c)
@@ -162,6 +163,11 @@ main(int argc, char **argv)
 			case 'a':			/* Dump data only */
 				opts->dataOnly = 1;
 				break;
+			case 'A':			/* vAriable */
+				opts->selTypes = 1;
+				opts->selVariable = 1;
+				simple_string_list_append(&opts->variableNames, optarg);
+				break;
 			case 'c':			/* clean (i.e., drop) schema prior to create */
 				opts->dropSchema = 1;
 				break;
@@ -462,6 +468,7 @@ usage(const char *progname)
 
 	printf(_("\nOptions controlling the restore:\n"));
 	printf(_("  -a, --data-only              restore only the data, no schema\n"));
+	printf(_("  -A, --variable=NAME          restore named session variable\n"));
 	printf(_("  -c, --clean                  clean (drop) database objects before recreating\n"));
 	printf(_("  -C, --create                 create the target database\n"));
 	printf(_("  -e, --exit-on-error          exit on error, default is to continue\n"));
-- 
2.47.0



  [text/x-patch] v20241110-0019-transactional-variables.patch (39.2K, 5-v20241110-0019-transactional-variables.patch)
  download | inline diff:
From ea51ff3fe6acb2edb6663693375c2d9dabba8fe4 Mon Sep 17 00:00:00 2001
From: "[email protected]" <[email protected]>
Date: Tue, 21 May 2024 22:40:55 +0200
Subject: [PATCH 19/20] transactional variables

This commit implements transactional variables. The content of transactional
session variables is sensitive to transactions and subtransactions. Any
transactional variable holds a history of values necessary for revert. This history
is cleaned (purged) when a) we read the variable, b) when the transaction is finished.

We don't try to purge, when the last modification of a variable is from
current subtransaction, or when purge was processed in current subtransaction.

This patch is based on my work from Feb 2020 and it is related to discussion
about features related to session variables. Now, when the all features are
separated to isolated patches I can revitalize this patch, because it doesn't
increase complexity of basic patches.

Unlike other patches, this patch is without any review at this moment (Feb 2024),
but the feature should be fully functional for people who are interested about
this feature (for testing).
---
 doc/src/sgml/catalogs.sgml                    |  12 +
 doc/src/sgml/ref/create_variable.sgml         |  10 +-
 src/backend/catalog/pg_variable.c             |   4 +
 src/backend/commands/session_variable.c       | 301 +++++++++++++++++-
 src/backend/parser/gram.y                     |  50 +--
 src/bin/pg_dump/pg_dump.c                     |  10 +-
 src/bin/pg_dump/pg_dump.h                     |   1 +
 src/bin/pg_dump/t/002_pg_dump.pl              |  36 +++
 src/bin/psql/describe.c                       |   4 +-
 src/bin/psql/tab-complete.in.c                |   5 +
 src/include/catalog/pg_variable.h             |   3 +
 src/include/nodes/parsenodes.h                |   1 +
 src/include/parser/kwlist.h                   |   1 +
 src/test/regress/expected/psql.out            |  36 +--
 .../regress/expected/session_variables.out    | 110 ++++++-
 src/test/regress/sql/session_variables.sql    |  58 ++++
 16 files changed, 592 insertions(+), 50 deletions(-)

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 72110e05f58..1ba396b05b6 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -9858,6 +9858,18 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       </para></entry>
      </row>
 
+     <row>
+      <entry><structfield>varistransact</structfield></entry>
+      <entry><type>boolean</type></entry>
+      <entry></entry>
+      <entry>
+       True, when the variable is <quote>transactional</quote>. In the case
+       of a transaction rollback, transactional variables are reset to the
+       value they had when the transaction started. The default value is
+       <literal>false</literal>.
+      </entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>vareoxaction</structfield> <type>char</type>
diff --git a/doc/src/sgml/ref/create_variable.sgml b/doc/src/sgml/ref/create_variable.sgml
index 00938743c0d..b73f032ddbb 100644
--- a/doc/src/sgml/ref/create_variable.sgml
+++ b/doc/src/sgml/ref/create_variable.sgml
@@ -26,7 +26,7 @@ PostgreSQL documentation
 
  <refsynopsisdiv>
 <synopsis>
-CREATE [ { TEMPORARY | TEMP } ] [ IMMUTABLE ] VARIABLE [ IF NOT EXISTS ] <replaceable class="parameter">name</replaceable> [ AS ] <replaceable class="parameter">data_type</replaceable> ] [ COLLATE <replaceable class="parameter">collation</replaceable> ]
+CREATE [ { TEMPORARY | TEMP } ] [ { TRANSACTIONAL | TRANSACTION } ] [ IMMUTABLE ] VARIABLE [ IF NOT EXISTS ] <replaceable class="parameter">name</replaceable> [ AS ] <replaceable class="parameter">data_type</replaceable> ] [ COLLATE <replaceable class="parameter">collation</replaceable> ]
     [ NOT NULL ] [ DEFAULT <replaceable class="parameter">default_expr</replaceable> ] [ { ON COMMIT DROP | ON TRANSACTION END RESET } ]
 </synopsis>
  </refsynopsisdiv>
@@ -47,6 +47,14 @@ CREATE [ { TEMPORARY | TEMP } ] [ IMMUTABLE ] VARIABLE [ IF NOT EXISTS ] <replac
    same as regular variables in procedural languages.
   </para>
 
+  <para>
+   When a schema variable is created with a
+   <command>CREATE TRANSACTIONAL VARIABLE</command> command, the variables
+   content changes are transactional: in case of rollback, they are reset to
+   their value at the beginning of the transaction or the latest subtransaction.
+   The variable content is only hold in memory, and thus is not persistent.
+  </para>
+
   <para>
    Session variables are retrieved by the <command>SELECT</command>
    command.  Their value is set with the <command>LET</command> command.
diff --git a/src/backend/catalog/pg_variable.c b/src/backend/catalog/pg_variable.c
index 34b0b971516..f9a5281ecc5 100644
--- a/src/backend/catalog/pg_variable.c
+++ b/src/backend/catalog/pg_variable.c
@@ -42,6 +42,7 @@ static ObjectAddress create_variable(const char *varName,
 									 bool if_not_exists,
 									 bool not_null,
 									 bool is_immutable,
+									 bool is_transact,
 									 Node *varDefexpr,
 									 VariableXactEndAction varXactEndAction);
 
@@ -59,6 +60,7 @@ create_variable(const char *varName,
 				bool if_not_exists,
 				bool not_null,
 				bool is_immutable,
+				bool is_transact,
 				Node *varDefexpr,
 				VariableXactEndAction varXactEndAction)
 {
@@ -123,6 +125,7 @@ create_variable(const char *varName,
 	values[Anum_pg_variable_varcollation - 1] = ObjectIdGetDatum(varCollation);
 	values[Anum_pg_variable_varnotnull - 1] = BoolGetDatum(not_null);
 	values[Anum_pg_variable_varisimmutable - 1] = BoolGetDatum(is_immutable);
+	values[Anum_pg_variable_varistransact - 1] = BoolGetDatum(is_transact);
 	values[Anum_pg_variable_varxactendaction - 1] = CharGetDatum(varXactEndAction);
 
 	if (varDefexpr)
@@ -267,6 +270,7 @@ CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt)
 							   stmt->if_not_exists,
 							   stmt->not_null,
 							   stmt->is_immutable,
+							   stmt->is_transact,
 							   cooked_default,
 							   stmt->XactEndAction);
 
diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c
index 1df4a4e9bc5..4d4f1ad1e0a 100644
--- a/src/backend/commands/session_variable.c
+++ b/src/backend/commands/session_variable.c
@@ -47,6 +47,19 @@ typedef struct SVariableXActDropItem
 	SubTransactionId deleting_subid;
 } SVariableXActDropItem;
 
+/*
+ * Used for transactional variables. Holds prev version.
+ */
+typedef struct PrevValue
+{
+	Datum		value;
+	bool		isnull;
+
+	SubTransactionId modify_subid;
+
+	struct PrevValue *prev_value;
+} PrevValue;
+
 /*
  * The values of session variables are stored in the backend's private memory
  * in the dedicated memory context SVariableMemoryContext in binary format.
@@ -68,6 +81,25 @@ typedef struct SVariableData
 	bool		isnull;
 	Datum		value;
 
+	/*
+	 * We don't need stack versions modified in same subtransaction.
+	 * Used by transactional variables only. The value of transactional
+	 * variable can be returned immediately when modify_subid is same
+	 * like current subid.
+	 */
+	SubTransactionId modify_subid;
+
+	/*
+	 * When the modify_subid is different than current subid, then
+	 * we need to recheck versions and throw versions related to
+	 * reverted transactions. When purge_subid is same like current subid
+	 * we can return the value of transaction variable without this
+	 * recheck.
+	 */
+	SubTransactionId purge_subid;
+
+	PrevValue  *prev_value;
+
 	Oid			typid;
 	int16		typlen;
 	bool		typbyval;
@@ -86,6 +118,7 @@ typedef struct SVariableData
 
 	bool		not_null;
 	bool		is_immutable;
+	bool		is_transact;
 
 	bool		reset_at_eox;
 
@@ -125,6 +158,9 @@ static bool needs_validation = false;
  */
 static bool has_session_variables_with_reset_at_eox = false;
 
+/* true, when transactional variables was modified */
+static bool has_modified_transactional_variables = false;
+
 /*
  * The content of session variables is not removed immediately. When it
  * is possible we do this at the transaction end. But when the transaction failed,
@@ -140,6 +176,7 @@ static List *xact_drop_items = NIL;
 
 static void register_session_variable_xact_drop(Oid varid);
 static void unregister_session_variable_xact_drop(Oid varid);
+static bool purge_session_variable(SVariable svar);
 
 /*
  * Callback function for session variable invalidation.
@@ -291,8 +328,25 @@ unregister_session_variable_xact_drop(Oid varid)
  * Release stored value, free memory
  */
 static void
-free_session_variable_value(SVariable svar)
+free_session_variable_value(SVariable svar, bool deep_free)
 {
+	if (deep_free)
+	{
+		PrevValue *prev_value = svar->prev_value;
+		PrevValue *next_value;
+
+		while (prev_value)
+		{
+			if (!svar->typbyval)
+				pfree(DatumGetPointer(prev_value->value));
+
+			next_value = prev_value->prev_value;
+			pfree(prev_value);
+
+			prev_value = next_value;
+		}
+	}
+
 	/* clean the current value */
 	if (!svar->isnull)
 	{
@@ -391,7 +445,7 @@ remove_invalid_session_variables(bool atEOX)
 			{
 				Oid			varid = svar->varid;
 
-				free_session_variable_value(svar);
+				free_session_variable_value(svar, true);
 				hash_search(sessionvars, &varid, HASH_REMOVE, NULL);
 				svar = NULL;
 			}
@@ -427,6 +481,94 @@ remove_session_variables_with_reset_at_eox(void)
 	has_session_variables_with_reset_at_eox = false;
 }
 
+/*
+ * remove prev values at eox
+ */
+static void
+remove_prev_values_at_eox(bool isCommit)
+{
+	HASH_SEQ_STATUS status;
+	SVariable	svar;
+
+	if (!sessionvars)
+		return;
+
+	/* leave quckly, when there are not that variables */
+	if (!has_modified_transactional_variables)
+		return;
+
+	hash_seq_init(&status, sessionvars);
+	while ((svar = (SVariable) hash_seq_search(&status)) != NULL)
+	{
+		if (svar->is_transact && svar->modify_subid != InvalidSubTransactionId)
+		{
+			if (isCommit)
+			{
+				if (purge_session_variable(svar))
+				{
+					PrevValue *prev_value = svar->prev_value;
+
+					while (prev_value)
+					{
+						PrevValue *current_pv = prev_value;
+
+						if (!svar->typbyval && !current_pv->isnull)
+							pfree(DatumGetPointer(current_pv->value));
+
+						prev_value = current_pv->prev_value;
+						pfree(current_pv);
+					}
+				}
+				else
+				{
+					hash_search(sessionvars, &svar->varid, HASH_REMOVE, NULL);
+					svar = NULL;
+				}
+			}
+			else
+			{
+				PrevValue *prev_value = svar->prev_value;
+
+				while (prev_value)
+				{
+					PrevValue *current_pv = prev_value;
+
+					if (current_pv->modify_subid == InvalidSubTransactionId)
+						break;
+
+					if (!svar->typbyval && !current_pv->isnull)
+						pfree(DatumGetPointer(current_pv->value));
+
+					prev_value = current_pv->prev_value;
+					pfree(current_pv);
+				}
+
+				if (prev_value)
+				{
+					svar->value = prev_value->value;
+					svar->isnull = prev_value->isnull;
+
+					pfree(prev_value);
+				}
+				else
+				{
+					hash_search(sessionvars, &svar->varid, HASH_REMOVE, NULL);
+					svar = NULL;
+				}
+			}
+
+			/* when svar is still valid (not removed from sessionvars */
+			if (svar)
+			{
+				svar->modify_subid = InvalidSubTransactionId;
+				svar->prev_value = NULL;
+			}
+		}
+	}
+
+	has_modified_transactional_variables = false;
+}
+
 /*
   * Perform ON COMMIT DROP for temporary session variables,
   * and remove all dropped variables from memory.
@@ -474,6 +616,8 @@ AtPreEOXact_SessionVariables(bool isCommit)
 		remove_invalid_session_variables(true);
 	}
 
+	remove_prev_values_at_eox(isCommit);
+
 	/*
 	 * We have to clean xact_drop_items. All related variables are dropped
 	 * now, or lost inside aborted transaction.
@@ -619,6 +763,7 @@ setup_session_variable(SVariable svar, Oid varid, bool is_write)
 
 	svar->not_null = varform->varnotnull;
 	svar->is_immutable = varform->varisimmutable;
+	svar->is_transact = varform->varistransact;
 
 	svar->is_domain = (get_typtype(varform->vartype) == TYPTYPE_DOMAIN);
 	svar->domain_check_extra = NULL;
@@ -644,6 +789,17 @@ setup_session_variable(SVariable svar, Oid varid, bool is_write)
 	svar->isnull = true;
 	svar->value = (Datum) 0;
 
+	if (svar->is_transact)
+	{
+		svar->modify_subid = GetCurrentSubTransactionId();
+		has_modified_transactional_variables = true;
+	}
+	else
+		svar->modify_subid = InvalidSubTransactionId;
+
+	svar->purge_subid = InvalidSubTransactionId;
+	svar->prev_value = NULL;
+
 	svar->is_valid = true;
 
 	svar->hashvalue = GetSysCacheHashValue1(VARIABLEOID,
@@ -677,6 +833,69 @@ setup_session_variable(SVariable svar, Oid varid, bool is_write)
 	ReleaseSysCache(tup);
 }
 
+/*
+ * Try to remove all previous versions related to reverted transactions.
+ * Returns true, when valid version was found.
+ */
+static bool
+purge_session_variable(SVariable svar)
+{
+	SubTransactionId current_subid;
+	PrevValue	   *prev_value;
+	bool			found = true;
+
+	Assert(svar->is_transact);
+
+	if (svar->modify_subid == InvalidSubTransactionId)
+		return true;
+
+	current_subid = GetCurrentSubTransactionId();
+
+	if (svar->modify_subid == current_subid)
+		return true;
+
+	if (svar->purge_subid == current_subid)
+		return true;
+
+	if (SubTransactionIsActive(svar->modify_subid))
+	{
+		svar->purge_subid = current_subid;
+		return true;
+	}
+
+	prev_value = svar->prev_value;
+
+	while (prev_value)
+	{
+		PrevValue *current_pv = prev_value;
+
+		if (current_pv->modify_subid == InvalidSubTransactionId ||
+			SubTransactionIsActive(current_pv->modify_subid))
+		{
+			svar->value = current_pv->value;
+			svar->isnull = current_pv->isnull;
+			svar->modify_subid = current_pv->modify_subid;
+
+			prev_value = current_pv->prev_value;
+			pfree(current_pv);
+
+			found = true;
+			break;
+		}
+
+		if (!svar->typbyval && !current_pv->isnull)
+			pfree(DatumGetPointer(current_pv->value));
+
+		prev_value = current_pv->prev_value;
+		pfree(current_pv);
+	}
+
+	svar->prev_value = prev_value;
+	svar->purge_subid = current_subid;
+
+	return found;
+}
+
 /*
  * Assign a new value to the session variable.  It is copied to
  * SVariableMemoryContext if necessary.
@@ -689,6 +908,10 @@ set_session_variable(SVariable svar, Datum value, bool isnull)
 	Datum		newval;
 	SVariableData locsvar,
 			   *_svar;
+	SubTransactionId current_subid = GetCurrentSubTransactionId();
+	SubTransactionId prev_purge_subid = InvalidSubTransactionId;
+	bool		save_prev_value;
+	PrevValue  *prev_value;
 
 	Assert(svar);
 	Assert(!isnull || value == (Datum) 0);
@@ -724,6 +947,21 @@ set_session_variable(SVariable svar, Datum value, bool isnull)
 	else
 		_svar = svar;
 
+	if (_svar->is_transact && _svar->create_lsn == svar->create_lsn)
+	{
+		Assert(svar->typid == _svar->typid);
+		Assert(svar->typbyval == _svar->typbyval);
+		Assert(svar->typlen == _svar->typlen);
+
+		save_prev_value = svar->modify_subid != current_subid;
+		prev_value = svar->prev_value;
+	}
+	else
+	{
+		save_prev_value = false;
+		prev_value = NULL;
+	}
+
 	if (!isnull)
 	{
 		MemoryContext oldcxt = MemoryContextSwitchTo(SVariableMemoryContext);
@@ -735,7 +973,37 @@ set_session_variable(SVariable svar, Datum value, bool isnull)
 	else
 		newval = value;
 
-	free_session_variable_value(svar);
+	if (save_prev_value)
+	{
+		volatile PrevValue *new_prev_value;
+
+		PG_TRY();
+		{
+			new_prev_value = MemoryContextAlloc(SVariableMemoryContext,
+												sizeof(PrevValue));
+		}
+		PG_CATCH();
+		{
+			/* release mem from persistent content */
+			if (newval != value)
+				pfree(DatumGetPointer(newval));
+			PG_RE_THROW();
+		}
+		PG_END_TRY();
+
+		new_prev_value->value = svar->value;
+		new_prev_value->isnull = svar->isnull;
+		new_prev_value->modify_subid = svar->modify_subid;
+		new_prev_value->prev_value = prev_value;
+
+		prev_value = (PrevValue *) new_prev_value;
+		prev_purge_subid = svar->purge_subid;
+
+		has_modified_transactional_variables = true;
+
+	}
+	else
+		free_session_variable_value(svar, prev_value == NULL);
 
 	elog(DEBUG1, "session variable \"%s.%s\" (oid:%u) has new value",
 		 get_namespace_name(get_session_variable_namespace(svar->varid)),
@@ -748,6 +1016,9 @@ set_session_variable(SVariable svar, Datum value, bool isnull)
 
 	svar->value = newval;
 	svar->isnull = isnull;
+	svar->modify_subid = current_subid;
+	svar->purge_subid = prev_purge_subid;
+	svar->prev_value = prev_value;
 
 	/* don't allow more changes of value when variable is IMMUTABLE */
 	if (svar->is_immutable)
@@ -851,6 +1122,8 @@ get_session_variable(Oid varid)
 	else
 		svar->is_valid = false;
 
+reinit:
+
 	/*
 	 * Force setup for not yet initialized variables or variables that cannot
 	 * be validated.
@@ -883,6 +1156,28 @@ get_session_variable(Oid varid)
 			 varid);
 	}
 
+	/*
+	 * Transactional variables should be purged before (remove
+	 * versions created by possibly reverted subtransactions).
+	 */
+	if (svar->is_transact &&
+		svar->modify_subid != GetCurrentSubTransactionId() &&
+		svar->modify_subid != InvalidSubTransactionId)
+	{
+		if (!purge_session_variable(svar))
+		{
+			/* force reinit */
+			svar->is_valid = false;
+
+			/*
+			 * In next iteration modify_subid should be
+			 * InvalidSubTransactionId or current subid,
+			 * so there is not risk of infinity cycle.
+			 */
+			goto reinit;
+		}
+	}
+
 	/* ensure the returned data is still of the correct domain */
 	if (svar->is_domain)
 	{
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index bc8e25b1933..af9d640c0e2 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -668,7 +668,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 				json_object_constructor_null_clause_opt
 				json_array_constructor_null_clause_opt
 
-%type <boolean>		OptNotNull OptImmutable
+%type <boolean>		OptNotNull OptImmutable OptTransactional
 
 /*
  * Non-keyword token types.  These are hard-wired into the "flex" lexer.
@@ -773,7 +773,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	SUBSCRIPTION SUBSTRING SUPPORT SYMMETRIC SYSID SYSTEM_P SYSTEM_USER
 
 	TABLE TABLES TABLESAMPLE TABLESPACE TARGET TEMP TEMPLATE TEMPORARY TEXT_P THEN
-	TIES TIME TIMESTAMP TO TRAILING TRANSACTION TRANSFORM
+	TIES TIME TIMESTAMP TO TRAILING TRANSACTION TRANSACTIONAL TRANSFORM
 	TREAT TRIGGER TRIM TRUE_P
 	TRUNCATE TRUSTED TYPE_P TYPES_P
 
@@ -5240,31 +5240,33 @@ create_extension_opt_item:
  *****************************************************************************/
 
 CreateSessionVarStmt:
-			CREATE OptTemp OptImmutable VARIABLE qualified_name opt_as Typename opt_collate_clause OptNotNull OptSessionVarDefExpr XactEndActionOption
+			CREATE OptTemp OptTransactional OptImmutable VARIABLE qualified_name opt_as Typename opt_collate_clause OptNotNull OptSessionVarDefExpr XactEndActionOption
 				{
 					CreateSessionVarStmt *n = makeNode(CreateSessionVarStmt);
-					$5->relpersistence = $2;
-					n->is_immutable = $3;
-					n->variable = $5;
-					n->typeName = $7;
-					n->collClause = (CollateClause *) $8;
-					n->not_null = $9;
-					n->defexpr = $10;
-					n->XactEndAction = $11;
+					$6->relpersistence = $2;
+					n->is_immutable = $4;
+					n->is_transact = $3;
+					n->variable = $6;
+					n->typeName = $8;
+					n->collClause = (CollateClause *) $9;
+					n->not_null = $10;
+					n->defexpr = $11;
+					n->XactEndAction = $12;
 					n->if_not_exists = false;
 					$$ = (Node *) n;
 				}
-			| CREATE OptTemp OptImmutable VARIABLE IF_P NOT EXISTS qualified_name opt_as Typename opt_collate_clause OptNotNull OptSessionVarDefExpr XactEndActionOption
+			| CREATE OptTemp OptTransactional OptImmutable VARIABLE IF_P NOT EXISTS qualified_name opt_as Typename opt_collate_clause OptNotNull OptSessionVarDefExpr XactEndActionOption
 				{
 					CreateSessionVarStmt *n = makeNode(CreateSessionVarStmt);
-					$8->relpersistence = $2;
-					n->is_immutable = $3;
-					n->variable = $8;
-					n->typeName = $10;
-					n->collClause = (CollateClause *) $11;
-					n->not_null = $12;
-					n->defexpr = $13;
-					n->XactEndAction = $14;
+					$9->relpersistence = $2;
+					n->is_immutable = $4;
+					n->is_transact = $3;
+					n->variable = $9;
+					n->typeName = $11;
+					n->collClause = (CollateClause *) $12;
+					n->not_null = $13;
+					n->defexpr = $14;
+					n->XactEndAction = $15;
 					n->if_not_exists = true;
 					$$ = (Node *) n;
 				}
@@ -5292,6 +5294,12 @@ OptImmutable: IMMUTABLE								{ $$ = true; }
 			| /* EMPTY */							{ $$ = false; }
 		;
 
+OptTransactional:
+			TRANSACTION								{ $$ = true; }
+			| TRANSACTIONAL							{ $$ = true; }
+			| /* EMPTY */							{ $$ = false; }
+		;
+
 /*****************************************************************************
  *
  * ALTER EXTENSION name UPDATE [ TO version ]
@@ -18084,6 +18092,7 @@ unreserved_keyword:
 			| TEXT_P
 			| TIES
 			| TRANSACTION
+			| TRANSACTIONAL
 			| TRANSFORM
 			| TRIGGER
 			| TRUNCATE
@@ -18732,6 +18741,7 @@ bare_label_keyword:
 			| TIMESTAMP
 			| TRAILING
 			| TRANSACTION
+			| TRANSACTIONAL
 			| TRANSFORM
 			| TREAT
 			| TRIGGER
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 59f4103ca4f..762cd020ea9 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -5439,6 +5439,7 @@ getVariables(Archive *fout)
 	int			i_varcollation;
 	int			i_varnotnull;
 	int			i_varisimmutable;
+	int			i_varistransact;
 	int			i_varacl;
 	int			i_acldefault;
 	int			i,
@@ -5463,6 +5464,7 @@ getVariables(Archive *fout)
 					  "       END AS varcollation,\n"
 					  "       v.varnotnull,\n"
 					  "       v.varisimmutable,\n"
+					  "       v.varistransact,\n"
 					  "       pg_catalog.pg_get_expr(v.vardefexpr,0) as vardefexpr,\n"
 					  "       v.varowner, v.varacl,\n"
 					  "       acldefault('V', v.varowner) AS acldefault\n"
@@ -5485,6 +5487,7 @@ getVariables(Archive *fout)
 	i_varcollation = PQfnumber(res, "varcollation");
 	i_varnotnull = PQfnumber(res, "varnotnull");
 	i_varisimmutable = PQfnumber(res, "varisimmutable");
+	i_varistransact = PQfnumber(res, "varistransact");
 
 	i_varowner = PQfnumber(res, "varowner");
 	i_varacl = PQfnumber(res, "varacl");
@@ -5513,6 +5516,7 @@ getVariables(Archive *fout)
 		varinfo[i].varcollation = atooid(PQgetvalue(res, i, i_varcollation));
 		varinfo[i].varnotnull = *(PQgetvalue(res, i, i_varnotnull)) == 't';
 		varinfo[i].varisimmutable = *(PQgetvalue(res, i, i_varisimmutable)) == 't';
+		varinfo[i].varistransact = *(PQgetvalue(res, i, i_varistransact)) == 't';
 
 		varinfo[i].dacl.acl = pg_strdup(PQgetvalue(res, i, i_varacl));
 		varinfo[i].dacl.acldefault = pg_strdup(PQgetvalue(res, i, i_acldefault));
@@ -5560,6 +5564,7 @@ dumpVariable(Archive *fout, const VariableInfo *varinfo)
 	const char *vardefexpr;
 	const char *varxactendaction;
 	const char *varisimmutable;
+	const char *varistransact;
 	Oid			varcollation;
 	bool		varnotnull;
 
@@ -5577,12 +5582,13 @@ dumpVariable(Archive *fout, const VariableInfo *varinfo)
 	varcollation = varinfo->varcollation;
 	varnotnull = varinfo->varnotnull;
 	varisimmutable = varinfo->varisimmutable ? "IMMUTABLE " : "";
+	varistransact = varinfo->varistransact ? "TRANSACTIONAL " : "";
 
 	appendPQExpBuffer(delq, "DROP VARIABLE %s;\n",
 					  qualvarname);
 
-	appendPQExpBuffer(query, "CREATE %sVARIABLE %s AS %s",
-					  varisimmutable, qualvarname, vartypname);
+	appendPQExpBuffer(query, "CREATE %s%sVARIABLE %s AS %s",
+					  varistransact, varisimmutable, qualvarname, vartypname);
 
 	if (OidIsValid(varcollation))
 	{
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index f7921f942d0..310701f09e1 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -719,6 +719,7 @@ typedef struct _VariableInfo
 	const char *rolname;		/* name of owner, or empty string */
 	bool		varnotnull;
 	bool		varisimmutable;
+	bool		varistransact;
 } VariableInfo;
 
 /*
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index 47cb59356e0..b39bddd3e40 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -4077,6 +4077,42 @@ my %tests = (
 		},
 	},
 
+	'CREATE TRANSACTIONAL VARIABLE test_variable' => {
+		all_runs     => 1,
+		catch_all    => 'CREATE ... commands',
+		create_order => 61,
+		create_sql   => 'CREATE TRANSACTIONAL VARIABLE dump_test.variable8 AS integer',
+		regexp => qr/^
+			\QCREATE TRANSACTIONAL VARIABLE dump_test.variable8 AS integer;\E/xm,
+		like => {
+			%full_runs,
+			%dump_test_schema_runs,
+			section_pre_data => 1,
+		},
+		unlike => {
+			exclude_dump_test_schema => 1,
+			only_dump_measurement    => 1,
+		},
+	},
+
+	'CREATE TRANSACTIONAL IMMUTABLE VARIABLE test_variable' => {
+		all_runs     => 1,
+		catch_all    => 'CREATE ... commands',
+		create_order => 61,
+		create_sql   => 'CREATE TRANSACTIONAL IMMUTABLE VARIABLE dump_test.variable9 AS integer',
+		regexp => qr/^
+			\QCREATE TRANSACTIONAL IMMUTABLE VARIABLE dump_test.variable9 AS integer;\E/xm,
+		like => {
+			%full_runs,
+			%dump_test_schema_runs,
+			section_pre_data => 1,
+		},
+		unlike => {
+			exclude_dump_test_schema => 1,
+			only_dump_measurement    => 1,
+		},
+	},
+
 	'CREATE VIEW test_view' => {
 		create_order => 61,
 		create_sql => 'CREATE VIEW dump_test.test_view
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index cc69f07f1f9..8862539d76e 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -5206,7 +5206,7 @@ listVariables(const char *pattern, bool verbose)
 	PQExpBufferData buf;
 	PGresult   *res;
 	printQueryOpt myopt = pset.popt;
-	static const bool translate_columns[] = {false, false, false, false, false, false, false, false, false, false, false};
+	static const bool translate_columns[] = {false, false, false, false, false, false, false, false, false, false, false, false};
 
 	if (pset.sversion < 180000)
 	{
@@ -5229,6 +5229,7 @@ listVariables(const char *pattern, bool verbose)
 					  "  pg_catalog.pg_get_userbyid(v.varowner) as \"%s\",\n"
 					  "  NOT v.varnotnull as \"%s\",\n"
 					  "  NOT v.varisimmutable as \"%s\",\n"
+					  "  v.varistransact as \"%s\",\n"
 					  "  pg_catalog.pg_get_expr(v.vardefexpr, 0) as \"%s\",\n"
 					  "  CASE v.varxactendaction\n"
 					  "    WHEN 'd' THEN 'ON COMMIT DROP'\n"
@@ -5241,6 +5242,7 @@ listVariables(const char *pattern, bool verbose)
 					  gettext_noop("Owner"),
 					  gettext_noop("Nullable"),
 					  gettext_noop("Mutable"),
+					  gettext_noop("Transactional"),
 					  gettext_noop("Default"),
 					  gettext_noop("Transactional end action"));
 
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index b653dda1fe8..e955522d2cb 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -1305,6 +1305,8 @@ static const pgsql_thing_t words_after_create[] = {
 																			 * TABLE ... */
 	{"TEXT SEARCH", NULL, NULL, NULL},
 	{"TRANSFORM", NULL, NULL, NULL, NULL, THING_NO_ALTER},
+	{"TRANSACTIONAL", NULL, NULL, NULL, NULL, THING_NO_DROP | THING_NO_ALTER}, /* for CREATE TRANSACTIONAL
+																				* VARIABLE ... */
 	{"TRIGGER", "SELECT tgname FROM pg_catalog.pg_trigger WHERE tgname LIKE '%s' AND NOT tgisinternal"},
 	{"TYPE", NULL, NULL, &Query_for_list_of_datatypes},
 	{"UNIQUE", NULL, NULL, NULL, NULL, THING_NO_DROP | THING_NO_ALTER}, /* for CREATE UNIQUE
@@ -3947,8 +3949,11 @@ match_previous_words(int pattern_id,
 	}
 /* CREATE VARIABLE --- is allowed inside CREATE SCHEMA, so use TailMatches */
 	/* Complete CREATE VARIABLE <name> with AS */
+	else if (Matches("CREATE", "TRANSACTION|TRANSACTIONAL"))
+		COMPLETE_WITH("IMMUTABLE", "VARIABLE");
 	else if (TailMatches("CREATE", "VARIABLE", MatchAny) ||
 			 TailMatches("TEMP|TEMPORARY", "VARIABLE", MatchAny) ||
+			 TailMatches("TRANSACTION|TRANSACTIONAL", "VARIABLE", MatchAny) ||
 			 TailMatches("IMMUTABLE", "VARIABLE", MatchAny))
 		COMPLETE_WITH("AS");
 	else if (TailMatches("VARIABLE", MatchAny, "AS"))
diff --git a/src/include/catalog/pg_variable.h b/src/include/catalog/pg_variable.h
index 68bc49a0e27..1dc44edf6e5 100644
--- a/src/include/catalog/pg_variable.h
+++ b/src/include/catalog/pg_variable.h
@@ -61,6 +61,9 @@ CATALOG(pg_variable,9222,VariableRelationId)
 	/* don't allow changes */
 	bool		varisimmutable BKI_DEFAULT(f);
 
+	/* supports transactions */
+	bool		varistransact BKI_DEFAULT(f);
+
 	/* action on transaction end */
 	char		varxactendaction BKI_DEFAULT(n);
 
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index bf820828dd9..a4372f184df 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -3481,6 +3481,7 @@ typedef struct CreateSessionVarStmt
 	bool		if_not_exists;	/* do nothing if it already exists */
 	bool		not_null;		/* disallow nulls */
 	bool		is_immutable;	/* don't allow changes */
+	bool		is_transact;	/* supports transactions */
 	Node	   *defexpr;		/* default expression */
 	char		XactEndAction;	/* on transaction end action */
 } CreateSessionVarStmt;
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index aefe51f335b..125eb819b76 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -455,6 +455,7 @@ PG_KEYWORD("timestamp", TIMESTAMP, COL_NAME_KEYWORD, BARE_LABEL)
 PG_KEYWORD("to", TO, RESERVED_KEYWORD, AS_LABEL)
 PG_KEYWORD("trailing", TRAILING, RESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("transaction", TRANSACTION, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("transactional", TRANSACTIONAL, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("transform", TRANSFORM, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("treat", TREAT, COL_NAME_KEYWORD, BARE_LABEL)
 PG_KEYWORD("trigger", TRIGGER, UNRESERVED_KEYWORD, BARE_LABEL)
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index e1635f686c2..da9f4a7030a 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -5935,20 +5935,20 @@ CREATE ROLE regress_variable_owner;
 SET ROLE TO regress_variable_owner;
 CREATE VARIABLE var1 AS varchar COLLATE "C";
 \dV+ var1
-                                                                         List of variables
- Schema | Name |       Type        | Collation |         Owner          | Nullable | Mutable | Default | Transactional end action | Access privileges | Description 
---------+------+-------------------+-----------+------------------------+----------+---------+---------+--------------------------+-------------------+-------------
- public | var1 | character varying | C         | regress_variable_owner | t        | t       |         |                          |                   | 
+                                                                                 List of variables
+ Schema | Name |       Type        | Collation |         Owner          | Nullable | Mutable | Transactional | Default | Transactional end action | Access privileges | Description 
+--------+------+-------------------+-----------+------------------------+----------+---------+---------------+---------+--------------------------+-------------------+-------------
+ public | var1 | character varying | C         | regress_variable_owner | t        | t       | f             |         |                          |                   | 
 (1 row)
 
 GRANT SELECT ON VARIABLE var1 TO PUBLIC;
 COMMENT ON VARIABLE var1 IS 'some description';
 \dV+ var1
-                                                                                           List of variables
- Schema | Name |       Type        | Collation |         Owner          | Nullable | Mutable | Default | Transactional end action |                Access privileges                 |   Description    
---------+------+-------------------+-----------+------------------------+----------+---------+---------+--------------------------+--------------------------------------------------+------------------
- public | var1 | character varying | C         | regress_variable_owner | t        | t       |         |                          | regress_variable_owner=rw/regress_variable_owner+| some description
-        |      |                   |           |                        |          |         |         |                          | =r/regress_variable_owner                        | 
+                                                                                                   List of variables
+ Schema | Name |       Type        | Collation |         Owner          | Nullable | Mutable | Transactional | Default | Transactional end action |                Access privileges                 |   Description    
+--------+------+-------------------+-----------+------------------------+----------+---------+---------------+---------+--------------------------+--------------------------------------------------+------------------
+ public | var1 | character varying | C         | regress_variable_owner | t        | t       | f             |         |                          | regress_variable_owner=rw/regress_variable_owner+| some description
+        |      |                   |           |                        |          |         |               |         |                          | =r/regress_variable_owner                        | 
 (1 row)
 
 DROP VARIABLE var1;
@@ -6416,9 +6416,9 @@ List of schemas
 (0 rows)
 
 \dV "no.such.variable"
-                                         List of variables
- Schema | Name | Type | Collation | Owner | Nullable | Mutable | Default | Transactional end action 
---------+------+------+-----------+-------+----------+---------+---------+--------------------------
+                                                 List of variables
+ Schema | Name | Type | Collation | Owner | Nullable | Mutable | Transactional | Default | Transactional end action 
+--------+------+------+-----------+-------+----------+---------+---------------+---------+--------------------------
 (0 rows)
 
 -- again, but with dotted schema qualifications.
@@ -6591,9 +6591,9 @@ improper qualified name (too many dotted names): "no.such.schema"."no.such.insta
 \dy "no.such.schema"."no.such.event.trigger"
 improper qualified name (too many dotted names): "no.such.schema"."no.such.event.trigger"
 \dV "no.such.schema"."no.such.variable"
-                                         List of variables
- Schema | Name | Type | Collation | Owner | Nullable | Mutable | Default | Transactional end action 
---------+------+------+-----------+-------+----------+---------+---------+--------------------------
+                                                 List of variables
+ Schema | Name | Type | Collation | Owner | Nullable | Mutable | Transactional | Default | Transactional end action 
+--------+------+------+-----------+-------+----------+---------+---------------+---------+--------------------------
 (0 rows)
 
 -- again, but with current database and dotted schema qualifications.
@@ -6730,9 +6730,9 @@ List of text search templates
 (0 rows)
 
 \dV regression."no.such.schema"."no.such.variable"
-                                         List of variables
- Schema | Name | Type | Collation | Owner | Nullable | Mutable | Default | Transactional end action 
---------+------+------+-----------+-------+----------+---------+---------+--------------------------
+                                                 List of variables
+ Schema | Name | Type | Collation | Owner | Nullable | Mutable | Transactional | Default | Transactional end action 
+--------+------+------+-----------+-------+----------+---------+---------------+---------+--------------------------
 (0 rows)
 
 -- again, but with dotted database and dotted schema qualifications.
diff --git a/src/test/regress/expected/session_variables.out b/src/test/regress/expected/session_variables.out
index e3dcb714607..7568f24ada4 100644
--- a/src/test/regress/expected/session_variables.out
+++ b/src/test/regress/expected/session_variables.out
@@ -45,11 +45,11 @@ SET ROLE TO regress_variable_owner;
 CREATE VARIABLE svartest.var1 AS int;
 SET ROLE TO DEFAULT;
 \dV+ svartest.var1
-                                                                                     List of variables
-  Schema  | Name |  Type   | Collation |         Owner          | Nullable | Mutable | Default | Transactional end action |                Access privileges                 | Description 
-----------+------+---------+-----------+------------------------+----------+---------+---------+--------------------------+--------------------------------------------------+-------------
- svartest | var1 | integer |           | regress_variable_owner | t        | t       |         |                          | regress_variable_owner=rw/regress_variable_owner+| 
-          |      |         |           |                        |          |         |         |                          | regress_variable_reader=r/regress_variable_owner | 
+                                                                                             List of variables
+  Schema  | Name |  Type   | Collation |         Owner          | Nullable | Mutable | Transactional | Default | Transactional end action |                Access privileges                 | Description 
+----------+------+---------+-----------+------------------------+----------+---------+---------------+---------+--------------------------+--------------------------------------------------+-------------
+ svartest | var1 | integer |           | regress_variable_owner | t        | t       | f             |         |                          | regress_variable_owner=rw/regress_variable_owner+| 
+          |      |         |           |                        |          |         |               |         |                          | regress_variable_reader=r/regress_variable_owner | 
 (1 row)
 
 DROP VARIABLE svartest.var1;
@@ -1764,3 +1764,103 @@ SELECT var1;
 LET var1 = 30;
 ERROR:  session variable "public.var1" is declared IMMUTABLE
 DROP VARIABLE var1;
+-- test transactional variables
+CREATE TRANSACTION VARIABLE tv AS int DEFAULT 0;
+BEGIN;
+  LET tv = 100;
+  SELECT tv;
+ tv  
+-----
+ 100
+(1 row)
+
+ROLLBACK;
+SELECT tv;
+ tv 
+----
+  0
+(1 row)
+
+LET tv = 100;
+BEGIN;
+  LET tv = 1000;
+COMMIT;
+SELECT tv;
+  tv  
+------
+ 1000
+(1 row)
+
+BEGIN;
+  LET tv = 0;
+  SELECT tv;
+ tv 
+----
+  0
+(1 row)
+
+ROLLBACK;
+SELECT tv;
+  tv  
+------
+ 1000
+(1 row)
+
+-- test subtransactions
+BEGIN;
+  LET tv = 1;
+SAVEPOINT x1;
+  LET tv = 2;
+SAVEPOINT x2;
+  LET tv = 3;
+ROLLBACK TO x2;
+  SELECT tv;
+ tv 
+----
+  2
+(1 row)
+
+  LET tv = 10;
+ROLLBACK TO x1;
+  SELECT tv;
+ tv 
+----
+  1
+(1 row)
+
+ROLLBACK;
+SELECT tv;
+  tv  
+------
+ 1000
+(1 row)
+
+BEGIN;
+  LET tv = 1;
+SAVEPOINT x1;
+  LET tv = 2;
+SAVEPOINT x2;
+  LET tv = 3;
+ROLLBACK TO x2;
+  SELECT tv;
+ tv 
+----
+  2
+(1 row)
+
+  LET tv = 10;
+ROLLBACK TO x1;
+  SELECT tv;
+ tv 
+----
+  1
+(1 row)
+
+COMMIT;
+SELECT tv;
+ tv 
+----
+  1
+(1 row)
+
+DROP VARIABLE tv;
diff --git a/src/test/regress/sql/session_variables.sql b/src/test/regress/sql/session_variables.sql
index 27c13b487b0..6e8831d7193 100644
--- a/src/test/regress/sql/session_variables.sql
+++ b/src/test/regress/sql/session_variables.sql
@@ -1209,3 +1209,61 @@ SELECT var1;
 LET var1 = 30;
 
 DROP VARIABLE var1;
+
+-- test transactional variables
+CREATE TRANSACTION VARIABLE tv AS int DEFAULT 0;
+
+BEGIN;
+  LET tv = 100;
+  SELECT tv;
+ROLLBACK;
+
+SELECT tv;
+
+LET tv = 100;
+BEGIN;
+  LET tv = 1000;
+COMMIT;
+
+SELECT tv;
+
+BEGIN;
+  LET tv = 0;
+  SELECT tv;
+ROLLBACK;
+
+SELECT tv;
+
+-- test subtransactions
+
+BEGIN;
+  LET tv = 1;
+SAVEPOINT x1;
+  LET tv = 2;
+SAVEPOINT x2;
+  LET tv = 3;
+ROLLBACK TO x2;
+  SELECT tv;
+  LET tv = 10;
+ROLLBACK TO x1;
+  SELECT tv;
+ROLLBACK;
+
+SELECT tv;
+
+BEGIN;
+  LET tv = 1;
+SAVEPOINT x1;
+  LET tv = 2;
+SAVEPOINT x2;
+  LET tv = 3;
+ROLLBACK TO x2;
+  SELECT tv;
+  LET tv = 10;
+ROLLBACK TO x1;
+  SELECT tv;
+COMMIT;
+
+SELECT tv;
+
+DROP VARIABLE tv;
-- 
2.47.0



  [text/x-patch] v20241110-0018-this-patch-changes-error-message-column-doesn-t-exis.patch (29.2K, 6-v20241110-0018-this-patch-changes-error-message-column-doesn-t-exis.patch)
  download | inline diff:
From 0fad39d34bb6c434d77785585b9cd7761ad075f4 Mon Sep 17 00:00:00 2001
From: "[email protected]" <[email protected]>
Date: Sat, 20 Jan 2024 17:06:04 +0100
Subject: [PATCH 18/20] this patch changes error message "column doesn't exist"
 to message "column or variable doesn't exist"

The error message will be more correct. Today, missing PL/pgSQL variable can be reported. The change has
impact on lot of regress tests not directly related to session variables, and then it is distributed as separate patch
---
 src/backend/parser/parse_expr.c               |  2 +-
 src/backend/parser/parse_relation.c           | 24 +++++++++++---
 src/backend/parser/parse_target.c             |  8 +++--
 src/include/parser/parse_expr.h               |  2 ++
 src/pl/plpgsql/src/expected/plpgsql_array.out |  2 +-
 .../plpgsql/src/expected/plpgsql_record.out   |  4 +--
 .../src/expected/plpgsql_session_variable.out |  2 +-
 src/pl/tcl/expected/pltcl_queries.out         | 12 +++----
 .../isolation/expected/session-variable.out   |  4 +--
 src/test/regress/expected/alter_table.out     | 32 +++++++++----------
 src/test/regress/expected/copy2.out           |  2 +-
 src/test/regress/expected/errors.out          |  8 ++---
 src/test/regress/expected/join.out            | 12 +++----
 src/test/regress/expected/namespace.out       |  2 +-
 src/test/regress/expected/numerology.out      |  2 +-
 src/test/regress/expected/plpgsql.out         | 12 +++----
 src/test/regress/expected/psql.out            |  2 +-
 src/test/regress/expected/rules.out           |  2 +-
 .../regress/expected/session_variables.out    |  6 ++--
 src/test/regress/expected/transactions.out    |  4 +--
 src/test/regress/expected/union.out           |  2 +-
 21 files changed, 83 insertions(+), 63 deletions(-)

diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index bcbb1f564af..16bf52c9854 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -520,7 +520,7 @@ transformIndirection(ParseState *pstate, A_Indirection *ind)
  * printed.  When we are in an expression where session variables cannot be
  * used, we raise the first form of error message.
  */
-static bool
+bool
 expr_kind_allows_session_variables(ParseExprKind p_expr_kind)
 {
 	bool		result = false;
diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c
index 95ae2108044..fa9a30bd138 100644
--- a/src/backend/parser/parse_relation.c
+++ b/src/backend/parser/parse_relation.c
@@ -27,6 +27,7 @@
 #include "nodes/makefuncs.h"
 #include "nodes/nodeFuncs.h"
 #include "parser/parse_enr.h"
+#include "parser/parse_expr.h"
 #include "parser/parse_relation.h"
 #include "parser/parse_type.h"
 #include "parser/parsetree.h"
@@ -3730,6 +3731,19 @@ errorMissingRTE(ParseState *pstate, RangeVar *relation)
 				 parser_errposition(pstate, relation->location)));
 }
 
+/*
+ * set message "column does not exist" or "column or variable does not exist"
+ * in dependency if expression context allows session variables.
+ */
+static int
+column_or_variable_does_not_exists(ParseState *pstate, const char *colname)
+{
+	if (expr_kind_allows_session_variables(pstate->p_expr_kind))
+		return errmsg("column or variable \"%s\" does not exist", colname);
+	else
+		return errmsg("column \"%s\" does not exist", colname);
+}
+
 /*
  * Generate a suitable error about a missing column.
  *
@@ -3764,7 +3778,7 @@ errorMissingColumn(ParseState *pstate,
 					(errcode(ERRCODE_UNDEFINED_COLUMN),
 					 relname ?
 					 errmsg("column %s.%s does not exist", relname, colname) :
-					 errmsg("column \"%s\" does not exist", colname),
+					 column_or_variable_does_not_exists(pstate, colname),
 					 errdetail("There are columns named \"%s\", but they are in tables that cannot be referenced from this part of the query.",
 							   colname),
 					 !relname ? errhint("Try using a table-qualified name.") : 0,
@@ -3774,7 +3788,7 @@ errorMissingColumn(ParseState *pstate,
 				(errcode(ERRCODE_UNDEFINED_COLUMN),
 				 relname ?
 				 errmsg("column %s.%s does not exist", relname, colname) :
-				 errmsg("column \"%s\" does not exist", colname),
+				 column_or_variable_does_not_exists(pstate, colname),
 				 errdetail("There is a column named \"%s\" in table \"%s\", but it cannot be referenced from this part of the query.",
 						   colname, state->rexact1->eref->aliasname),
 				 rte_visible_if_lateral(pstate, state->rexact1) ?
@@ -3792,14 +3806,14 @@ errorMissingColumn(ParseState *pstate,
 					(errcode(ERRCODE_UNDEFINED_COLUMN),
 					 relname ?
 					 errmsg("column %s.%s does not exist", relname, colname) :
-					 errmsg("column \"%s\" does not exist", colname),
+					 column_or_variable_does_not_exists(pstate, colname),
 					 parser_errposition(pstate, location)));
 		/* Handle case where we have a single alternative spelling to offer */
 		ereport(ERROR,
 				(errcode(ERRCODE_UNDEFINED_COLUMN),
 				 relname ?
 				 errmsg("column %s.%s does not exist", relname, colname) :
-				 errmsg("column \"%s\" does not exist", colname),
+				 column_or_variable_does_not_exists(pstate, colname),
 				 errhint("Perhaps you meant to reference the column \"%s.%s\".",
 						 state->rfirst->eref->aliasname,
 						 strVal(list_nth(state->rfirst->eref->colnames,
@@ -3813,7 +3827,7 @@ errorMissingColumn(ParseState *pstate,
 				(errcode(ERRCODE_UNDEFINED_COLUMN),
 				 relname ?
 				 errmsg("column %s.%s does not exist", relname, colname) :
-				 errmsg("column \"%s\" does not exist", colname),
+				 column_or_variable_does_not_exists(pstate, colname),
 				 errhint("Perhaps you meant to reference the column \"%s.%s\" or the column \"%s.%s\".",
 						 state->rfirst->eref->aliasname,
 						 strVal(list_nth(state->rfirst->eref->colnames,
diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c
index 76bf88c3ca2..6af6b801ad4 100644
--- a/src/backend/parser/parse_target.c
+++ b/src/backend/parser/parse_target.c
@@ -779,7 +779,9 @@ transformAssignmentIndirection(ParseState *pstate,
 			if (!typrelid)
 				ereport(ERROR,
 						(errcode(ERRCODE_DATATYPE_MISMATCH),
-						 errmsg("cannot assign to field \"%s\" of column \"%s\" because its type %s is not a composite type",
+						 errmsg(expr_kind_allows_session_variables(pstate->p_expr_kind) ?
+								 "cannot assign to field \"%s\" of column or variable \"%s\" because its type %s is not a composite type" :
+								 "cannot assign to field \"%s\" of column \"%s\" because its type %s is not a composite type",
 								strVal(n), targetName,
 								format_type_be(targetTypeId)),
 						 parser_errposition(pstate, location)));
@@ -788,7 +790,9 @@ transformAssignmentIndirection(ParseState *pstate,
 			if (attnum == InvalidAttrNumber)
 				ereport(ERROR,
 						(errcode(ERRCODE_UNDEFINED_COLUMN),
-						 errmsg("cannot assign to field \"%s\" of column \"%s\" because there is no such column in data type %s",
+						 errmsg(expr_kind_allows_session_variables(pstate->p_expr_kind) ?
+								 "cannot assign to field \"%s\" of column or variable \"%s\" because there is no such column in data type %s" :
+								 "cannot assign to field \"%s\" of column \"%s\" because there is no such column in data type %s",
 								strVal(n), targetName,
 								format_type_be(targetTypeId)),
 						 parser_errposition(pstate, location)));
diff --git a/src/include/parser/parse_expr.h b/src/include/parser/parse_expr.h
index 0d64b300f8a..e5aebf99b4a 100644
--- a/src/include/parser/parse_expr.h
+++ b/src/include/parser/parse_expr.h
@@ -23,4 +23,6 @@ extern Node *transformExpr(ParseState *pstate, Node *expr, ParseExprKind exprKin
 
 extern const char *ParseExprKindName(ParseExprKind exprKind);
 
+extern bool expr_kind_allows_session_variables(ParseExprKind p_expr_kind);
+
 #endif							/* PARSE_EXPR_H */
diff --git a/src/pl/plpgsql/src/expected/plpgsql_array.out b/src/pl/plpgsql/src/expected/plpgsql_array.out
index ad60e0e8be3..c1ab9fd7ed9 100644
--- a/src/pl/plpgsql/src/expected/plpgsql_array.out
+++ b/src/pl/plpgsql/src/expected/plpgsql_array.out
@@ -41,7 +41,7 @@ NOTICE:  a = {"(,11)"}, a[1].i = 11
 -- perhaps this ought to work, but for now it doesn't:
 do $$ declare a complex[];
 begin a[1:2].i := array[11,12]; raise notice 'a = %', a; end$$;
-ERROR:  cannot assign to field "i" of column "a" because its type complex[] is not a composite type
+ERROR:  cannot assign to field "i" of column or variable "a" because its type complex[] is not a composite type
 LINE 1: a[1:2].i := array[11,12]
         ^
 QUERY:  a[1:2].i := array[11,12]
diff --git a/src/pl/plpgsql/src/expected/plpgsql_record.out b/src/pl/plpgsql/src/expected/plpgsql_record.out
index 6974c8f4a44..3970ac721c9 100644
--- a/src/pl/plpgsql/src/expected/plpgsql_record.out
+++ b/src/pl/plpgsql/src/expected/plpgsql_record.out
@@ -135,7 +135,7 @@ ERROR:  record "c" has no field "x"
 CONTEXT:  PL/pgSQL assignment "c.x.q1 = 1"
 PL/pgSQL function inline_code_block line 1 at assignment
 do $$ declare c nested_int8s; begin c.c2.x = 1; end $$;
-ERROR:  cannot assign to field "x" of column "c" because there is no such column in data type two_int8s
+ERROR:  cannot assign to field "x" of column or variable "c" because there is no such column in data type two_int8s
 LINE 1: c.c2.x = 1
         ^
 QUERY:  c.c2.x = 1
@@ -157,7 +157,7 @@ ERROR:  record "c" has no field "x"
 CONTEXT:  PL/pgSQL assignment "b.c.x.q1 = 1"
 PL/pgSQL function inline_code_block line 1 at assignment
 do $$ <<b>> declare c nested_int8s; begin b.c.c2.x = 1; end $$;
-ERROR:  cannot assign to field "x" of column "b" because there is no such column in data type two_int8s
+ERROR:  cannot assign to field "x" of column or variable "b" because there is no such column in data type two_int8s
 LINE 1: b.c.c2.x = 1
         ^
 QUERY:  b.c.c2.x = 1
diff --git a/src/pl/plpgsql/src/expected/plpgsql_session_variable.out b/src/pl/plpgsql/src/expected/plpgsql_session_variable.out
index 789c3123b0c..f006db4faa8 100644
--- a/src/pl/plpgsql/src/expected/plpgsql_session_variable.out
+++ b/src/pl/plpgsql/src/expected/plpgsql_session_variable.out
@@ -232,7 +232,7 @@ SET ROLE TO DEFAULT;
 SET ROLE TO regress_var_exec_role;
 -- should to fail, but not crash
 SELECT var_read_func();
-ERROR:  column "plpgsql_sv_var1" does not exist
+ERROR:  column or variable "plpgsql_sv_var1" does not exist
 LINE 1: plpgsql_sv_var1
         ^
 QUERY:  plpgsql_sv_var1
diff --git a/src/pl/tcl/expected/pltcl_queries.out b/src/pl/tcl/expected/pltcl_queries.out
index 35cc6e62aad..497cd38189b 100644
--- a/src/pl/tcl/expected/pltcl_queries.out
+++ b/src/pl/tcl/expected/pltcl_queries.out
@@ -450,12 +450,12 @@ CONTEXT:  while executing
 "__PLTcl_proc_tcl_eval_text spi_prepare\ a\ \"b\ \{\""
 in PL/Tcl function tcl_eval(text)
 select tcl_error_handling_test($tcl$spi_prepare "select moo" []$tcl$);
-       tcl_error_handling_test        
---------------------------------------
- SQLSTATE: 42703                     +
- condition: undefined_column         +
- cursor_position: 8                  +
- message: column "moo" does not exist+
+             tcl_error_handling_test              
+--------------------------------------------------
+ SQLSTATE: 42703                                 +
+ condition: undefined_column                     +
+ cursor_position: 8                              +
+ message: column or variable "moo" does not exist+
  statement: select moo
 (1 row)
 
diff --git a/src/test/isolation/expected/session-variable.out b/src/test/isolation/expected/session-variable.out
index e9a254bcd12..d8848d555cd 100644
--- a/src/test/isolation/expected/session-variable.out
+++ b/src/test/isolation/expected/session-variable.out
@@ -10,7 +10,7 @@ test
 
 step drop: DROP VARIABLE myvar;
 step val: SELECT myvar;
-ERROR:  column "myvar" does not exist
+ERROR:  column or variable "myvar" does not exist
 
 starting permutation: let val s1 drop val sr1
 step let: LET myvar = 'test';
@@ -23,7 +23,7 @@ test
 step s1: BEGIN;
 step drop: DROP VARIABLE myvar;
 step val: SELECT myvar;
-ERROR:  column "myvar" does not exist
+ERROR:  column or variable "myvar" does not exist
 step sr1: ROLLBACK;
 
 starting permutation: let val dbg drop create dbg val
diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out
index 2212c8dbb59..35b3a3bd302 100644
--- a/src/test/regress/expected/alter_table.out
+++ b/src/test/regress/expected/alter_table.out
@@ -1295,19 +1295,19 @@ select * from atacc1;
 (1 row)
 
 select * from atacc1 order by a;
-ERROR:  column "a" does not exist
+ERROR:  column or variable "a" does not exist
 LINE 1: select * from atacc1 order by a;
                                       ^
 select * from atacc1 order by "........pg.dropped.1........";
-ERROR:  column "........pg.dropped.1........" does not exist
+ERROR:  column or variable "........pg.dropped.1........" does not exist
 LINE 1: select * from atacc1 order by "........pg.dropped.1........"...
                                       ^
 select * from atacc1 group by a;
-ERROR:  column "a" does not exist
+ERROR:  column or variable "a" does not exist
 LINE 1: select * from atacc1 group by a;
                                       ^
 select * from atacc1 group by "........pg.dropped.1........";
-ERROR:  column "........pg.dropped.1........" does not exist
+ERROR:  column or variable "........pg.dropped.1........" does not exist
 LINE 1: select * from atacc1 group by "........pg.dropped.1........"...
                                       ^
 select atacc1.* from atacc1;
@@ -1317,7 +1317,7 @@ select atacc1.* from atacc1;
 (1 row)
 
 select a from atacc1;
-ERROR:  column "a" does not exist
+ERROR:  column or variable "a" does not exist
 LINE 1: select a from atacc1;
                ^
 select atacc1.a from atacc1;
@@ -1331,15 +1331,15 @@ select b,c,d from atacc1;
 (1 row)
 
 select a,b,c,d from atacc1;
-ERROR:  column "a" does not exist
+ERROR:  column or variable "a" does not exist
 LINE 1: select a,b,c,d from atacc1;
                ^
 select * from atacc1 where a = 1;
-ERROR:  column "a" does not exist
+ERROR:  column or variable "a" does not exist
 LINE 1: select * from atacc1 where a = 1;
                                    ^
 select "........pg.dropped.1........" from atacc1;
-ERROR:  column "........pg.dropped.1........" does not exist
+ERROR:  column or variable "........pg.dropped.1........" does not exist
 LINE 1: select "........pg.dropped.1........" from atacc1;
                ^
 select atacc1."........pg.dropped.1........" from atacc1;
@@ -1347,11 +1347,11 @@ ERROR:  column atacc1.........pg.dropped.1........ does not exist
 LINE 1: select atacc1."........pg.dropped.1........" from atacc1;
                ^
 select "........pg.dropped.1........",b,c,d from atacc1;
-ERROR:  column "........pg.dropped.1........" does not exist
+ERROR:  column or variable "........pg.dropped.1........" does not exist
 LINE 1: select "........pg.dropped.1........",b,c,d from atacc1;
                ^
 select * from atacc1 where "........pg.dropped.1........" = 1;
-ERROR:  column "........pg.dropped.1........" does not exist
+ERROR:  column or variable "........pg.dropped.1........" does not exist
 LINE 1: select * from atacc1 where "........pg.dropped.1........" = ...
                                    ^
 -- UPDATEs
@@ -1360,7 +1360,7 @@ ERROR:  column "a" of relation "atacc1" does not exist
 LINE 1: update atacc1 set a = 3;
                           ^
 update atacc1 set b = 2 where a = 3;
-ERROR:  column "a" does not exist
+ERROR:  column or variable "a" does not exist
 LINE 1: update atacc1 set b = 2 where a = 3;
                                       ^
 update atacc1 set "........pg.dropped.1........" = 3;
@@ -1368,7 +1368,7 @@ ERROR:  column "........pg.dropped.1........" of relation "atacc1" does not exis
 LINE 1: update atacc1 set "........pg.dropped.1........" = 3;
                           ^
 update atacc1 set b = 2 where "........pg.dropped.1........" = 3;
-ERROR:  column "........pg.dropped.1........" does not exist
+ERROR:  column or variable "........pg.dropped.1........" does not exist
 LINE 1: update atacc1 set b = 2 where "........pg.dropped.1........"...
                                       ^
 -- INSERTs
@@ -1416,11 +1416,11 @@ LINE 1: insert into atacc1 ("........pg.dropped.1........",b,c,d) va...
                             ^
 -- DELETEs
 delete from atacc1 where a = 3;
-ERROR:  column "a" does not exist
+ERROR:  column or variable "a" does not exist
 LINE 1: delete from atacc1 where a = 3;
                                  ^
 delete from atacc1 where "........pg.dropped.1........" = 3;
-ERROR:  column "........pg.dropped.1........" does not exist
+ERROR:  column or variable "........pg.dropped.1........" does not exist
 LINE 1: delete from atacc1 where "........pg.dropped.1........" = 3;
                                  ^
 delete from atacc1;
@@ -1706,7 +1706,7 @@ select f1 from c1;
 
 alter table c1 drop column f1;
 select f1 from c1;
-ERROR:  column "f1" does not exist
+ERROR:  column or variable "f1" does not exist
 LINE 1: select f1 from c1;
                ^
 HINT:  Perhaps you meant to reference the column "c1.f2".
@@ -1720,7 +1720,7 @@ ERROR:  cannot drop inherited column "f1"
 alter table p1 drop column f1;
 -- c1.f1 is dropped now, since there is no local definition for it
 select f1 from c1;
-ERROR:  column "f1" does not exist
+ERROR:  column or variable "f1" does not exist
 LINE 1: select f1 from c1;
                ^
 HINT:  Perhaps you meant to reference the column "c1.f2".
diff --git a/src/test/regress/expected/copy2.out b/src/test/regress/expected/copy2.out
index 64ea33aeae8..6874f5ae0d1 100644
--- a/src/test/regress/expected/copy2.out
+++ b/src/test/regress/expected/copy2.out
@@ -160,7 +160,7 @@ LINE 1: COPY x TO stdout WHERE a = 1;
 COPY x from stdin WHERE a = 50004;
 COPY x from stdin WHERE a > 60003;
 COPY x from stdin WHERE f > 60003;
-ERROR:  column "f" does not exist
+ERROR:  column or variable "f" does not exist
 LINE 1: COPY x from stdin WHERE f > 60003;
                                 ^
 COPY x from stdin WHERE a = max(x.b);
diff --git a/src/test/regress/expected/errors.out b/src/test/regress/expected/errors.out
index 8c527474daf..e53ae451dfc 100644
--- a/src/test/regress/expected/errors.out
+++ b/src/test/regress/expected/errors.out
@@ -27,7 +27,7 @@ LINE 1: select * from nonesuch;
                       ^
 -- bad name in target list
 select nonesuch from pg_database;
-ERROR:  column "nonesuch" does not exist
+ERROR:  column or variable "nonesuch" does not exist
 LINE 1: select nonesuch from pg_database;
                ^
 -- empty distinct list isn't OK
@@ -37,17 +37,17 @@ LINE 1: select distinct from pg_database;
                         ^
 -- bad attribute name on lhs of operator
 select * from pg_database where nonesuch = pg_database.datname;
-ERROR:  column "nonesuch" does not exist
+ERROR:  column or variable "nonesuch" does not exist
 LINE 1: select * from pg_database where nonesuch = pg_database.datna...
                                         ^
 -- bad attribute name on rhs of operator
 select * from pg_database where pg_database.datname = nonesuch;
-ERROR:  column "nonesuch" does not exist
+ERROR:  column or variable "nonesuch" does not exist
 LINE 1: ...ect * from pg_database where pg_database.datname = nonesuch;
                                                               ^
 -- bad attribute name in select distinct on
 select distinct on (foobar) * from pg_database;
-ERROR:  column "foobar" does not exist
+ERROR:  column or variable "foobar" does not exist
 LINE 1: select distinct on (foobar) * from pg_database;
                             ^
 -- grouping with FOR UPDATE
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index 9b2973694ff..d9f16aeb978 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -6310,13 +6310,13 @@ LINE 1: select t2.uunique1 from
 HINT:  Perhaps you meant to reference the column "t2.unique1".
 select uunique1 from
   tenk1 t1 join tenk2 t2 on t1.two = t2.two; -- error, suggest both at once
-ERROR:  column "uunique1" does not exist
+ERROR:  column or variable "uunique1" does not exist
 LINE 1: select uunique1 from
                ^
 HINT:  Perhaps you meant to reference the column "t1.unique1" or the column "t2.unique1".
 select ctid from
   tenk1 t1 join tenk2 t2 on t1.two = t2.two; -- error, need qualification
-ERROR:  column "ctid" does not exist
+ERROR:  column or variable "ctid" does not exist
 LINE 1: select ctid from
                ^
 DETAIL:  There are columns named "ctid", but they are in tables that cannot be referenced from this part of the query.
@@ -7413,7 +7413,7 @@ lateral (select * from int8_tbl t1,
 
 -- test some error cases where LATERAL should have been used but wasn't
 select f1,g from int4_tbl a, (select f1 as g) ss;
-ERROR:  column "f1" does not exist
+ERROR:  column or variable "f1" does not exist
 LINE 1: select f1,g from int4_tbl a, (select f1 as g) ss;
                                              ^
 DETAIL:  There is a column named "f1" in table "a", but it cannot be referenced from this part of the query.
@@ -7425,7 +7425,7 @@ LINE 1: select f1,g from int4_tbl a, (select a.f1 as g) ss;
 DETAIL:  There is an entry for table "a", but it cannot be referenced from this part of the query.
 HINT:  To reference that table, you must mark this subquery with LATERAL.
 select f1,g from int4_tbl a cross join (select f1 as g) ss;
-ERROR:  column "f1" does not exist
+ERROR:  column or variable "f1" does not exist
 LINE 1: select f1,g from int4_tbl a cross join (select f1 as g) ss;
                                                        ^
 DETAIL:  There is a column named "f1" in table "a", but it cannot be referenced from this part of the query.
@@ -7462,7 +7462,7 @@ LINE 1: select 1 from tenk1 a, lateral (select max(a.unique1) from i...
 create temp table xx1 as select f1 as x1, -f1 as x2 from int4_tbl;
 -- error, can't do this:
 update xx1 set x2 = f1 from (select * from int4_tbl where f1 = x1) ss;
-ERROR:  column "x1" does not exist
+ERROR:  column or variable "x1" does not exist
 LINE 1: ... set x2 = f1 from (select * from int4_tbl where f1 = x1) ss;
                                                                 ^
 DETAIL:  There is a column named "x1" in table "xx1", but it cannot be referenced from this part of the query.
@@ -7482,7 +7482,7 @@ update xx1 set x2 = f1 from xx1, lateral (select * from int4_tbl where f1 = x1)
 ERROR:  table name "xx1" specified more than once
 -- also errors:
 delete from xx1 using (select * from int4_tbl where f1 = x1) ss;
-ERROR:  column "x1" does not exist
+ERROR:  column or variable "x1" does not exist
 LINE 1: ...te from xx1 using (select * from int4_tbl where f1 = x1) ss;
                                                                 ^
 DETAIL:  There is a column named "x1" in table "xx1", but it cannot be referenced from this part of the query.
diff --git a/src/test/regress/expected/namespace.out b/src/test/regress/expected/namespace.out
index dbbda72d395..d98793f92a7 100644
--- a/src/test/regress/expected/namespace.out
+++ b/src/test/regress/expected/namespace.out
@@ -23,7 +23,7 @@ BEGIN;
 SET search_path to public, test_ns_schema_1;
 CREATE SCHEMA test_ns_schema_2
        CREATE VIEW abc_view AS SELECT c FROM abc;
-ERROR:  column "c" does not exist
+ERROR:  column or variable "c" does not exist
 LINE 2:        CREATE VIEW abc_view AS SELECT c FROM abc;
                                               ^
 COMMIT;
diff --git a/src/test/regress/expected/numerology.out b/src/test/regress/expected/numerology.out
index 9e23166fed1..672012e1461 100644
--- a/src/test/regress/expected/numerology.out
+++ b/src/test/regress/expected/numerology.out
@@ -314,7 +314,7 @@ NOTICE:  i = 1002
 NOTICE:  i = 1003
 -- error cases
 SELECT _100;
-ERROR:  column "_100" does not exist
+ERROR:  column or variable "_100" does not exist
 LINE 1: SELECT _100;
                ^
 SELECT 100_;
diff --git a/src/test/regress/expected/plpgsql.out b/src/test/regress/expected/plpgsql.out
index 0a6945581bd..5c599ba312e 100644
--- a/src/test/regress/expected/plpgsql.out
+++ b/src/test/regress/expected/plpgsql.out
@@ -2598,7 +2598,7 @@ end; $$ language plpgsql;
 -- should fail: SQLSTATE and SQLERRM are only in defined EXCEPTION
 -- blocks
 select excpt_test1();
-ERROR:  column "sqlstate" does not exist
+ERROR:  column or variable "sqlstate" does not exist
 LINE 1: sqlstate
         ^
 QUERY:  sqlstate
@@ -2613,7 +2613,7 @@ begin
 end; $$ language plpgsql;
 -- should fail
 select excpt_test2();
-ERROR:  column "sqlstate" does not exist
+ERROR:  column or variable "sqlstate" does not exist
 LINE 1: sqlstate
         ^
 QUERY:  sqlstate
@@ -4675,7 +4675,7 @@ BEGIN
         RAISE NOTICE '%, %', r.roomno, r.comment;
     END LOOP;
 END$$;
-ERROR:  column "foo" does not exist
+ERROR:  column or variable "foo" does not exist
 LINE 1: SELECT rtrim(roomno) AS roomno, foo FROM Room ORDER BY roomn...
                                         ^
 QUERY:  SELECT rtrim(roomno) AS roomno, foo FROM Room ORDER BY roomno
@@ -4717,7 +4717,7 @@ begin
   raise notice 'x = %', x;
 end;
 $$;
-ERROR:  column "x" does not exist
+ERROR:  column or variable "x" does not exist
 LINE 1: x + 1
         ^
 QUERY:  x + 1
@@ -4729,7 +4729,7 @@ begin
   raise notice 'x = %, y = %', x, y;
 end;
 $$;
-ERROR:  column "x" does not exist
+ERROR:  column or variable "x" does not exist
 LINE 1: x + 1
         ^
 QUERY:  x + 1
@@ -5769,7 +5769,7 @@ ALTER TABLE alter_table_under_transition_tables
   DROP column name;
 UPDATE alter_table_under_transition_tables
   SET id = id;
-ERROR:  column "name" does not exist
+ERROR:  column or variable "name" does not exist
 LINE 1: (SELECT string_agg(id || '=' || name, ',') FROM d)
                                         ^
 QUERY:  (SELECT string_agg(id || '=' || name, ',') FROM d)
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index 77a7bf45c8a..e1635f686c2 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -214,7 +214,7 @@ select $1::int as col \bind 1 \g \bind 2 \g
 -- errors
 -- parse error
 SELECT foo \bind \g
-ERROR:  column "foo" does not exist
+ERROR:  column or variable "foo" does not exist
 LINE 1: SELECT foo 
                ^
 -- tcop error
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b47013f113..2e9db29690d 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1191,7 +1191,7 @@ drop rule rules_foorule on rules_foo;
 -- this should fail because f1 is not exposed for unqualified reference:
 create rule rules_foorule as on insert to rules_foo where f1 < 100
 do instead insert into rules_foo2 values (f1);
-ERROR:  column "f1" does not exist
+ERROR:  column or variable "f1" does not exist
 LINE 2: do instead insert into rules_foo2 values (f1);
                                                   ^
 DETAIL:  There are columns named "f1", but they are in tables that cannot be referenced from this part of the query.
diff --git a/src/test/regress/expected/session_variables.out b/src/test/regress/expected/session_variables.out
index a66c8df415d..e3dcb714607 100644
--- a/src/test/regress/expected/session_variables.out
+++ b/src/test/regress/expected/session_variables.out
@@ -310,7 +310,7 @@ ERROR:  session variable "var1" doesn't exist
 LINE 1: LET var1 = pi();
             ^
 SELECT var1;
-ERROR:  column "var1" does not exist
+ERROR:  column or variable "var1" does not exist
 LINE 1: SELECT var1;
                ^
 -- should be ok
@@ -522,7 +522,7 @@ SELECT v1;
 
 -- should fail, attribute doesn't exist
 LET v1.x = 10;
-ERROR:  cannot assign to field "x" of column "v1" because there is no such column in data type t1
+ERROR:  cannot assign to field "x" of column or variable "v1" because there is no such column in data type t1
 LINE 1: LET v1.x = 10;
             ^
 -- should fail, don't allow multi column query
@@ -1071,7 +1071,7 @@ BEGIN;
 DROP VARIABLE var1;
 -- should fail
 SELECT var1;
-ERROR:  column "var1" does not exist
+ERROR:  column or variable "var1" does not exist
 LINE 1: SELECT var1;
                ^
 ROLLBACK;
diff --git a/src/test/regress/expected/transactions.out b/src/test/regress/expected/transactions.out
index 7f5757e89c4..b05b16d94bf 100644
--- a/src/test/regress/expected/transactions.out
+++ b/src/test/regress/expected/transactions.out
@@ -256,7 +256,7 @@ SELECT * FROM trans_barbaz;	-- should have 1
 BEGIN;
 	SAVEPOINT one;
 		SELECT trans_foo;
-ERROR:  column "trans_foo" does not exist
+ERROR:  column or variable "trans_foo" does not exist
 LINE 1: SELECT trans_foo;
                ^
 	ROLLBACK TO SAVEPOINT one;
@@ -305,7 +305,7 @@ BEGIN;
 	SAVEPOINT one;
 		INSERT INTO savepoints VALUES (5);
 		SELECT trans_foo;
-ERROR:  column "trans_foo" does not exist
+ERROR:  column or variable "trans_foo" does not exist
 LINE 1: SELECT trans_foo;
                ^
 COMMIT;
diff --git a/src/test/regress/expected/union.out b/src/test/regress/expected/union.out
index c73631a9a1d..5c0eacc5f15 100644
--- a/src/test/regress/expected/union.out
+++ b/src/test/regress/expected/union.out
@@ -922,7 +922,7 @@ ORDER BY q2,q1;
 
 -- This should fail, because q2 isn't a name of an EXCEPT output column
 SELECT q1 FROM int8_tbl EXCEPT SELECT q2 FROM int8_tbl ORDER BY q2 LIMIT 1;
-ERROR:  column "q2" does not exist
+ERROR:  column or variable "q2" does not exist
 LINE 1: ... int8_tbl EXCEPT SELECT q2 FROM int8_tbl ORDER BY q2 LIMIT 1...
                                                              ^
 DETAIL:  There is a column named "q2" in table "*SELECT* 2", but it cannot be referenced from this part of the query.
-- 
2.47.0



  [text/x-patch] v20241110-0016-plpgsql-implementation-for-LET-statement.patch (14.2K, 7-v20241110-0016-plpgsql-implementation-for-LET-statement.patch)
  download | inline diff:
From 28cb7ebf011378714334ddd73672f9a54a473fbc Mon Sep 17 00:00:00 2001
From: "[email protected]" <[email protected]>
Date: Sat, 20 Jan 2024 08:56:17 +0100
Subject: [PATCH 16/20] plpgsql implementation for LET statement

PLpgSQL allows to call expression executor directly (for simple expression).
This possibility strongly reduces overhead related to execution. Reimplementation
of LET statement (inside PLpgSQL) allows to use this possibility, and strongly
increase performance:

CREATE VARIABLE svar int;

DO $$
BEGIN
  FOR i IN 1..10000
  LOOP
    LET svar = i;
  END LOOP;
END;
$$;

From 120ms to 8ms (with assertions) (this is best case, but without it, the LET statement
can be bottle neck).

An alternative can be reimplementation of LET statement inside expression
executor, and then SQL LET command can be executed in simple expression
execution, but this increase an complexity of executor, but still the
benefits is only inside plpgsql, so it is better to this optimization
inside plpgsql.
---
 src/backend/executor/spi.c                |  3 ++
 src/backend/parser/analyze.c              | 12 +++++
 src/backend/parser/gram.y                 |  8 ++++
 src/backend/parser/parser.c               |  1 +
 src/include/nodes/parsenodes.h            |  2 +
 src/include/parser/parser.h               |  4 ++
 src/pl/plpgsql/src/pl_exec.c              | 55 +++++++++++++++++++++++
 src/pl/plpgsql/src/pl_funcs.c             | 24 ++++++++++
 src/pl/plpgsql/src/pl_gram.y              | 28 +++++++++++-
 src/pl/plpgsql/src/pl_reserved_kwlist.h   |  1 +
 src/pl/plpgsql/src/pl_unreserved_kwlist.h |  1 +
 src/pl/plpgsql/src/plpgsql.h              | 12 +++++
 src/tools/pgindent/typedefs.list          |  1 +
 13 files changed, 151 insertions(+), 1 deletion(-)

diff --git a/src/backend/executor/spi.c b/src/backend/executor/spi.c
index 2fb2e73604e..5ae25d07ad7 100644
--- a/src/backend/executor/spi.c
+++ b/src/backend/executor/spi.c
@@ -2991,6 +2991,9 @@ _SPI_error_callback(void *arg)
 			case RAW_PARSE_PLPGSQL_ASSIGN3:
 				errcontext("PL/pgSQL assignment \"%s\"", query);
 				break;
+			case RAW_PARSE_PLPGSQL_LET:
+				errcontext("LET statement \"%s\"", query);
+				break;
 			default:
 				errcontext("SQL statement \"%s\"", query);
 				break;
diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
index 496d75a494a..2edc51cfee9 100644
--- a/src/backend/parser/analyze.c
+++ b/src/backend/parser/analyze.c
@@ -2014,6 +2014,18 @@ transformLetStmt(ParseState *pstate, LetStmt *stmt)
 
 	stmt->query = (Node *) query;
 
+	/*
+	 * Inside PL/pgSQL we don't want to execute LET statement as utility
+	 * command, because it disallow to execute expression as simple
+	 * expression. So for PL/pgSQL we have extra path, and we return SELECT.
+	 * Then it can be executed by exec_eval_expr. Result is dirrectly assigned
+	 * to target session variable inside PL/pgSQL LET statement handler. This
+	 * is extra code, extra path, but possibility to get faster execution is
+	 * too attractive.
+	 */
+	if (stmt->plpgsql_mode)
+		return query;
+
 	/* represent the command as a utility Query */
 	result = makeNode(Query);
 	result->commandType = CMD_UTILITY;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 6aea3bd35b2..bc8e25b1933 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -817,6 +817,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %token		MODE_PLPGSQL_ASSIGN1
 %token		MODE_PLPGSQL_ASSIGN2
 %token		MODE_PLPGSQL_ASSIGN3
+%token		MODE_PLPGSQL_LET
 
 
 /* Precedence: lowest to highest */
@@ -948,6 +949,13 @@ parse_toplevel:
 				pg_yyget_extra(yyscanner)->parsetree =
 					list_make1(makeRawStmt((Node *) n, @2));
 			}
+			| MODE_PLPGSQL_LET LetStmt
+			{
+				LetStmt *n = (LetStmt *) $2;
+				n->plpgsql_mode = true;
+				pg_yyget_extra(yyscanner)->parsetree =
+					list_make1(makeRawStmt((Node *) n, 0));
+			}
 		;
 
 /*
diff --git a/src/backend/parser/parser.c b/src/backend/parser/parser.c
index 118488c3f30..f9430a32919 100644
--- a/src/backend/parser/parser.c
+++ b/src/backend/parser/parser.c
@@ -62,6 +62,7 @@ raw_parser(const char *str, RawParseMode mode)
 			[RAW_PARSE_PLPGSQL_ASSIGN1] = MODE_PLPGSQL_ASSIGN1,
 			[RAW_PARSE_PLPGSQL_ASSIGN2] = MODE_PLPGSQL_ASSIGN2,
 			[RAW_PARSE_PLPGSQL_ASSIGN3] = MODE_PLPGSQL_ASSIGN3,
+			[RAW_PARSE_PLPGSQL_LET] = MODE_PLPGSQL_LET,
 		};
 
 		yyextra.have_lookahead = true;
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 7308c323574..bf820828dd9 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2114,6 +2114,8 @@ typedef struct LetStmt
 	NodeTag		type;
 	List	   *target;			/* target variable */
 	Node	   *query;			/* source expression */
+	bool		plpgsql_mode;	/* true, when command will be executed
+								 * (parsed) by plpgsql runtime */
 	int			location;
 } LetStmt;
 
diff --git a/src/include/parser/parser.h b/src/include/parser/parser.h
index be184ec5066..efbc76f0ad4 100644
--- a/src/include/parser/parser.h
+++ b/src/include/parser/parser.h
@@ -33,6 +33,9 @@
  * RAW_PARSE_PLPGSQL_ASSIGNn: parse a PL/pgSQL assignment statement,
  * and return a one-element List containing a RawStmt node.  "n"
  * gives the number of dotted names comprising the target ColumnRef.
+ *
+ * RAW_PARSE_PLPGSQL_LET: parse a LET statement, and return a
+ * one-element List containing a RawStmt node.
  */
 typedef enum
 {
@@ -42,6 +45,7 @@ typedef enum
 	RAW_PARSE_PLPGSQL_ASSIGN1,
 	RAW_PARSE_PLPGSQL_ASSIGN2,
 	RAW_PARSE_PLPGSQL_ASSIGN3,
+	RAW_PARSE_PLPGSQL_LET,
 } RawParseMode;
 
 /* Values for the backslash_quote GUC */
diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
index 86c5bd324a9..b5cb1bee223 100644
--- a/src/pl/plpgsql/src/pl_exec.c
+++ b/src/pl/plpgsql/src/pl_exec.c
@@ -22,6 +22,7 @@
 #include "access/tupconvert.h"
 #include "catalog/pg_proc.h"
 #include "catalog/pg_type.h"
+#include "commands/session_variable.h"
 #include "executor/execExpr.h"
 #include "executor/spi.h"
 #include "executor/tstoreReceiver.h"
@@ -323,6 +324,8 @@ static int	exec_stmt_commit(PLpgSQL_execstate *estate,
 							 PLpgSQL_stmt_commit *stmt);
 static int	exec_stmt_rollback(PLpgSQL_execstate *estate,
 							   PLpgSQL_stmt_rollback *stmt);
+static int	exec_stmt_let(PLpgSQL_execstate *estate,
+						  PLpgSQL_stmt_let *let);
 
 static void plpgsql_estate_setup(PLpgSQL_execstate *estate,
 								 PLpgSQL_function *func,
@@ -2116,6 +2119,10 @@ exec_stmts(PLpgSQL_execstate *estate, List *stmts)
 				rc = exec_stmt_rollback(estate, (PLpgSQL_stmt_rollback *) stmt);
 				break;
 
+			case PLPGSQL_STMT_LET:
+				rc = exec_stmt_let(estate, (PLpgSQL_stmt_let *) stmt);
+				break;
+
 			default:
 				/* point err_stmt to parent, since this one seems corrupt */
 				estate->err_stmt = save_estmt;
@@ -4999,6 +5006,54 @@ exec_stmt_rollback(PLpgSQL_execstate *estate, PLpgSQL_stmt_rollback *stmt)
 	return PLPGSQL_RC_OK;
 }
 
+/* ----------
+ * exec_stmt_let			Evaluate an expression and
+ *					put the result into a session variable.
+ * ----------
+ */
+static int
+exec_stmt_let(PLpgSQL_execstate *estate, PLpgSQL_stmt_let *stmt)
+{
+	bool		isNull;
+	Oid			valtype;
+	int32		valtypmod;
+	Datum		value;
+	Oid			varid;
+
+	List	   *plansources;
+	CachedPlanSource *plansource;
+
+	value = exec_eval_expr(estate,
+						   stmt->expr,
+						   &isNull,
+						   &valtype,
+						   &valtypmod);
+
+	/*
+	 * Oid of target session variable is stored in Query structure. It is
+	 * safer to read this value directly from the plan than to hold this value
+	 * in the plpgsql context, because it's not necessary to handle
+	 * invalidation of the cached value. Next operations are read only without
+	 * any allocations, so we can expect that retrieving varid from Query
+	 * should be fast.
+	 */
+	plansources = SPI_plan_get_plan_sources(stmt->expr->plan);
+	if (list_length(plansources) != 1)
+		elog(ERROR, "unexpected length of plansources of query for LET statement");
+
+	plansource = (CachedPlanSource *) linitial(plansources);
+	if (list_length(plansource->query_list) != 1)
+		elog(ERROR, "unexpected length of plansource of query for LET statement");
+
+	varid = linitial_node(Query, plansource->query_list)->resultVariable;
+	if (!OidIsValid(varid))
+		elog(ERROR, "oid of target session variable is not valid");
+
+	SetSessionVariableWithSecurityCheck(varid, value, isNull);
+
+	return PLPGSQL_RC_OK;
+}
+
 /* ----------
  * exec_assign_expr			Put an expression's result into a variable.
  * ----------
diff --git a/src/pl/plpgsql/src/pl_funcs.c b/src/pl/plpgsql/src/pl_funcs.c
index eeb7c4d7c04..1b39351db7f 100644
--- a/src/pl/plpgsql/src/pl_funcs.c
+++ b/src/pl/plpgsql/src/pl_funcs.c
@@ -288,6 +288,8 @@ plpgsql_stmt_typename(PLpgSQL_stmt *stmt)
 			return "COMMIT";
 		case PLPGSQL_STMT_ROLLBACK:
 			return "ROLLBACK";
+		case PLPGSQL_STMT_LET:
+			return "LET";
 	}
 
 	return "unknown";
@@ -370,6 +372,7 @@ static void free_perform(PLpgSQL_stmt_perform *stmt);
 static void free_call(PLpgSQL_stmt_call *stmt);
 static void free_commit(PLpgSQL_stmt_commit *stmt);
 static void free_rollback(PLpgSQL_stmt_rollback *stmt);
+static void free_let(PLpgSQL_stmt_let *stmt);
 static void free_expr(PLpgSQL_expr *expr);
 
 
@@ -459,6 +462,9 @@ free_stmt(PLpgSQL_stmt *stmt)
 		case PLPGSQL_STMT_ROLLBACK:
 			free_rollback((PLpgSQL_stmt_rollback *) stmt);
 			break;
+		case PLPGSQL_STMT_LET:
+			free_let((PLpgSQL_stmt_let *) stmt);
+			break;
 		default:
 			elog(ERROR, "unrecognized cmd_type: %d", stmt->cmd_type);
 			break;
@@ -713,6 +719,12 @@ free_getdiag(PLpgSQL_stmt_getdiag *stmt)
 {
 }
 
+static void
+free_let(PLpgSQL_stmt_let *stmt)
+{
+	free_expr(stmt->expr);
+}
+
 static void
 free_expr(PLpgSQL_expr *expr)
 {
@@ -815,6 +827,7 @@ static void dump_perform(PLpgSQL_stmt_perform *stmt);
 static void dump_call(PLpgSQL_stmt_call *stmt);
 static void dump_commit(PLpgSQL_stmt_commit *stmt);
 static void dump_rollback(PLpgSQL_stmt_rollback *stmt);
+static void dump_let(PLpgSQL_stmt_let *stmt);
 static void dump_expr(PLpgSQL_expr *expr);
 
 
@@ -914,6 +927,9 @@ dump_stmt(PLpgSQL_stmt *stmt)
 		case PLPGSQL_STMT_ROLLBACK:
 			dump_rollback((PLpgSQL_stmt_rollback *) stmt);
 			break;
+		case PLPGSQL_STMT_LET:
+			dump_let((PLpgSQL_stmt_let *) stmt);
+			break;
 		default:
 			elog(ERROR, "unrecognized cmd_type: %d", stmt->cmd_type);
 			break;
@@ -1590,6 +1606,14 @@ dump_getdiag(PLpgSQL_stmt_getdiag *stmt)
 	printf("\n");
 }
 
+static void
+dump_let(PLpgSQL_stmt_let *stmt)
+{
+	dump_ind();
+	dump_expr(stmt->expr);
+	printf("\n");
+}
+
 static void
 dump_expr(PLpgSQL_expr *expr)
 {
diff --git a/src/pl/plpgsql/src/pl_gram.y b/src/pl/plpgsql/src/pl_gram.y
index 8182ce28aa1..3f155c4f8a4 100644
--- a/src/pl/plpgsql/src/pl_gram.y
+++ b/src/pl/plpgsql/src/pl_gram.y
@@ -200,7 +200,7 @@ static	void			check_raise_parameters(PLpgSQL_stmt_raise *stmt);
 %type <stmt>	stmt_return stmt_raise stmt_assert stmt_execsql
 %type <stmt>	stmt_dynexecute stmt_for stmt_perform stmt_call stmt_getdiag
 %type <stmt>	stmt_open stmt_fetch stmt_move stmt_close stmt_null
-%type <stmt>	stmt_commit stmt_rollback
+%type <stmt>	stmt_commit stmt_rollback stmt_let
 %type <stmt>	stmt_case stmt_foreach_a
 
 %type <list>	proc_exceptions
@@ -307,6 +307,7 @@ static	void			check_raise_parameters(PLpgSQL_stmt_raise *stmt);
 %token <keyword>	K_INTO
 %token <keyword>	K_IS
 %token <keyword>	K_LAST
+%token <keyword>	K_LET
 %token <keyword>	K_LOG
 %token <keyword>	K_LOOP
 %token <keyword>	K_MERGE
@@ -876,6 +877,8 @@ proc_stmt		: pl_block ';'
 						{ $$ = $1; }
 				| stmt_rollback
 						{ $$ = $1; }
+				| stmt_let
+						{ $$ = $1; }
 				;
 
 stmt_perform	: K_PERFORM
@@ -992,6 +995,29 @@ stmt_assign		: T_DATUM
 					}
 				;
 
+stmt_let		: K_LET
+					{
+						PLpgSQL_stmt_let *new;
+						RawParseMode pmode;
+
+						pmode = RAW_PARSE_PLPGSQL_LET;
+
+						new = palloc0(sizeof(PLpgSQL_stmt_let));
+						new->cmd_type = PLPGSQL_STMT_LET;
+						new->lineno   = plpgsql_location_to_lineno(@1);
+						new->stmtid = ++plpgsql_curr_compile->nstatements;
+
+						/* push back the head name to include it in the stmt */
+						plpgsql_push_back_token(K_LET);
+						new->expr = read_sql_construct(';', 0, 0, ";",
+													   pmode,
+													   false, true,
+													   NULL, NULL);
+
+						$$ = (PLpgSQL_stmt *)new;
+					}
+				;
+
 stmt_getdiag	: K_GET getdiag_area_opt K_DIAGNOSTICS getdiag_list ';'
 					{
 						PLpgSQL_stmt_getdiag *new;
diff --git a/src/pl/plpgsql/src/pl_reserved_kwlist.h b/src/pl/plpgsql/src/pl_reserved_kwlist.h
index d338e9f6374..be94aa751a4 100644
--- a/src/pl/plpgsql/src/pl_reserved_kwlist.h
+++ b/src/pl/plpgsql/src/pl_reserved_kwlist.h
@@ -40,6 +40,7 @@ PG_KEYWORD("from", K_FROM)
 PG_KEYWORD("if", K_IF)
 PG_KEYWORD("in", K_IN)
 PG_KEYWORD("into", K_INTO)
+PG_KEYWORD("let", K_LET)
 PG_KEYWORD("loop", K_LOOP)
 PG_KEYWORD("not", K_NOT)
 PG_KEYWORD("null", K_NULL)
diff --git a/src/pl/plpgsql/src/pl_unreserved_kwlist.h b/src/pl/plpgsql/src/pl_unreserved_kwlist.h
index 670b4cf0c1e..602628d7d2a 100644
--- a/src/pl/plpgsql/src/pl_unreserved_kwlist.h
+++ b/src/pl/plpgsql/src/pl_unreserved_kwlist.h
@@ -69,6 +69,7 @@ PG_KEYWORD("info", K_INFO)
 PG_KEYWORD("insert", K_INSERT)
 PG_KEYWORD("is", K_IS)
 PG_KEYWORD("last", K_LAST)
+PG_KEYWORD("let", K_LET)
 PG_KEYWORD("log", K_LOG)
 PG_KEYWORD("merge", K_MERGE)
 PG_KEYWORD("message", K_MESSAGE)
diff --git a/src/pl/plpgsql/src/plpgsql.h b/src/pl/plpgsql/src/plpgsql.h
index 50c3b28472b..bb400629adb 100644
--- a/src/pl/plpgsql/src/plpgsql.h
+++ b/src/pl/plpgsql/src/plpgsql.h
@@ -128,6 +128,7 @@ typedef enum PLpgSQL_stmt_type
 	PLPGSQL_STMT_CALL,
 	PLPGSQL_STMT_COMMIT,
 	PLPGSQL_STMT_ROLLBACK,
+	PLPGSQL_STMT_LET,
 } PLpgSQL_stmt_type;
 
 /*
@@ -520,6 +521,17 @@ typedef struct PLpgSQL_stmt_assign
 	PLpgSQL_expr *expr;
 } PLpgSQL_stmt_assign;
 
+/*
+ * Let statement
+ */
+typedef struct PLpgSQL_stmt_let
+{
+	PLpgSQL_stmt_type cmd_type;
+	int			lineno;
+	unsigned int stmtid;
+	PLpgSQL_expr *expr;
+} PLpgSQL_stmt_let;
+
 /*
  * PERFORM statement
  */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 27e352fa341..9685bc1bcb9 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1890,6 +1890,7 @@ PLpgSQL_stmt_forq
 PLpgSQL_stmt_fors
 PLpgSQL_stmt_getdiag
 PLpgSQL_stmt_if
+PLpgSQL_stmt_let
 PLpgSQL_stmt_loop
 PLpgSQL_stmt_open
 PLpgSQL_stmt_perform
-- 
2.47.0



  [text/x-patch] v20241110-0015-allow-parallel-execution-queries-with-session-variab.patch (11.9K, 8-v20241110-0015-allow-parallel-execution-queries-with-session-variab.patch)
  download | inline diff:
From 5c3e6810a32310d9cc11f39163b1e46725ceb2c6 Mon Sep 17 00:00:00 2001
From: "[email protected]" <[email protected]>
Date: Sat, 20 Jan 2024 07:57:29 +0100
Subject: [PATCH 15/20] allow parallel execution queries with session variables

---
 doc/src/sgml/parallel.sgml                    |   6 -
 src/backend/executor/execMain.c               |  14 +-
 src/backend/executor/execParallel.c           | 147 +++++++++++++++++-
 src/backend/optimizer/util/clauses.c          |  18 +--
 .../regress/expected/session_variables.out    |  12 +-
 5 files changed, 171 insertions(+), 26 deletions(-)

diff --git a/doc/src/sgml/parallel.sgml b/doc/src/sgml/parallel.sgml
index 683dede6adc..1ce9abf86f5 100644
--- a/doc/src/sgml/parallel.sgml
+++ b/doc/src/sgml/parallel.sgml
@@ -515,12 +515,6 @@ EXPLAIN SELECT * FROM pgbench_accounts WHERE filler LIKE '%x%';
         Plan nodes that reference a correlated <literal>SubPlan</literal>.
       </para>
     </listitem>
-
-    <listitem>
-      <para>
-        Plan nodes that use a session variable.
-      </para>
-    </listitem>
   </itemizedlist>
 
  <sect2 id="parallel-labeling">
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index ba6e38263cc..b926d37af17 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -211,7 +211,19 @@ standard_ExecutorStart(QueryDesc *queryDesc, int eflags)
 	 *    be changed inside query execution time, and then a reference to
 	 *    previously returned value can be corrupted).
 	 */
-	if (queryDesc->plannedstmt->sessionVariables)
+	if (queryDesc->num_session_variables > 0)
+	{
+		/*
+		 * When a parallel query needs to access query parameters (including
+		 * related session variables), then related session variables are
+		 * restored (deserialized) in queryDesc already. So just push pointer
+		 * of this array to executor's estate.
+		 */
+		Assert(IsParallelWorker());
+		estate->es_session_variables = queryDesc->session_variables;
+		estate->es_num_session_variables = queryDesc->num_session_variables;
+	}
+	else if (queryDesc->plannedstmt->sessionVariables)
 	{
 		ListCell   *lc;
 		int			nSessionVariables;
diff --git a/src/backend/executor/execParallel.c b/src/backend/executor/execParallel.c
index bfb3419efb7..cb434e2768e 100644
--- a/src/backend/executor/execParallel.c
+++ b/src/backend/executor/execParallel.c
@@ -12,8 +12,9 @@
  * workers and ensuring that their state generally matches that of the
  * leader; see src/backend/access/transam/README.parallel for details.
  * However, we must save and restore relevant executor state, such as
- * any ParamListInfo associated with the query, buffer/WAL usage info, and
- * the actual plan to be passed down to the worker.
+ * any ParamListInfo associated with the query, buffer/WAL usage info,
+ * session variables buffer, and the actual plan to be passed down to
+ * the worker.
  *
  * IDENTIFICATION
  *	  src/backend/executor/execParallel.c
@@ -64,6 +65,7 @@
 #define PARALLEL_KEY_QUERY_TEXT		UINT64CONST(0xE000000000000008)
 #define PARALLEL_KEY_JIT_INSTRUMENTATION UINT64CONST(0xE000000000000009)
 #define PARALLEL_KEY_WAL_USAGE			UINT64CONST(0xE00000000000000A)
+#define PARALLEL_KEY_SESSION_VARIABLES	UINT64CONST(0xE00000000000000B)
 
 #define PARALLEL_TUPLE_QUEUE_SIZE		65536
 
@@ -138,6 +140,12 @@ static bool ExecParallelRetrieveInstrumentation(PlanState *planstate,
 /* Helper function that runs in the parallel worker. */
 static DestReceiver *ExecParallelGetReceiver(dsm_segment *seg, shm_toc *toc);
 
+/* Helper functions that can pass values of session variables */
+static Size EstimateSessionVariables(EState *estate);
+static void SerializeSessionVariables(EState *estate, char **start_address);
+static SessionVariableValue *RestoreSessionVariables(char **start_address,
+													 int *num_session_variables);
+
 /*
  * Create a serialized representation of the plan to be sent to each worker.
  */
@@ -596,6 +604,7 @@ ExecInitParallelPlan(PlanState *planstate, EState *estate,
 	char	   *pstmt_data;
 	char	   *pstmt_space;
 	char	   *paramlistinfo_space;
+	char	   *session_variables_space;
 	BufferUsage *bufusage_space;
 	WalUsage   *walusage_space;
 	SharedExecutorInstrumentation *instrumentation = NULL;
@@ -605,6 +614,7 @@ ExecInitParallelPlan(PlanState *planstate, EState *estate,
 	int			instrumentation_len = 0;
 	int			jit_instrumentation_len = 0;
 	int			instrument_offset = 0;
+	int			session_variables_len = 0;
 	Size		dsa_minsize = dsa_minimum_size();
 	char	   *query_string;
 	int			query_len;
@@ -660,6 +670,11 @@ ExecInitParallelPlan(PlanState *planstate, EState *estate,
 	shm_toc_estimate_chunk(&pcxt->estimator, paramlistinfo_len);
 	shm_toc_estimate_keys(&pcxt->estimator, 1);
 
+	/* Estimate space for serialized session variables. */
+	session_variables_len = EstimateSessionVariables(estate);
+	shm_toc_estimate_chunk(&pcxt->estimator, session_variables_len);
+	shm_toc_estimate_keys(&pcxt->estimator, 1);
+
 	/*
 	 * Estimate space for BufferUsage.
 	 *
@@ -761,6 +776,11 @@ ExecInitParallelPlan(PlanState *planstate, EState *estate,
 	shm_toc_insert(pcxt->toc, PARALLEL_KEY_PARAMLISTINFO, paramlistinfo_space);
 	SerializeParamList(estate->es_param_list_info, &paramlistinfo_space);
 
+	/* Store serialized session variables. */
+	session_variables_space = shm_toc_allocate(pcxt->toc, session_variables_len);
+	shm_toc_insert(pcxt->toc, PARALLEL_KEY_SESSION_VARIABLES, session_variables_space);
+	SerializeSessionVariables(estate, &session_variables_space);
+
 	/* Allocate space for each worker's BufferUsage; no need to initialize. */
 	bufusage_space = shm_toc_allocate(pcxt->toc,
 									  mul_size(sizeof(BufferUsage), pcxt->nworkers));
@@ -1411,6 +1431,7 @@ ParallelQueryMain(dsm_segment *seg, shm_toc *toc)
 	SharedJitInstrumentation *jit_instrumentation;
 	int			instrument_options = 0;
 	void	   *area_space;
+	char	   *sessionvariable_space;
 	dsa_area   *area;
 	ParallelWorkerContext pwcxt;
 
@@ -1436,6 +1457,14 @@ ParallelQueryMain(dsm_segment *seg, shm_toc *toc)
 	area_space = shm_toc_lookup(toc, PARALLEL_KEY_DSA, false);
 	area = dsa_attach_in_place(area_space, seg);
 
+	/* Reconstruct session variables. */
+	sessionvariable_space = shm_toc_lookup(toc,
+										   PARALLEL_KEY_SESSION_VARIABLES,
+										   false);
+	queryDesc->session_variables =
+		RestoreSessionVariables(&sessionvariable_space,
+								&queryDesc->num_session_variables);
+
 	/* Start up the executor */
 	queryDesc->plannedstmt->jitFlags = fpes->jit_flags;
 	ExecutorStart(queryDesc, fpes->eflags);
@@ -1504,3 +1533,117 @@ ParallelQueryMain(dsm_segment *seg, shm_toc *toc)
 	FreeQueryDesc(queryDesc);
 	receiver->rDestroy(receiver);
 }
+
+/*
+ * Estimate the amount of space required to serialize a session variable.
+ */
+static Size
+EstimateSessionVariables(EState *estate)
+{
+	int			i;
+	Size		sz = sizeof(int);
+
+	if (estate->es_session_variables == NULL)
+		return sz;
+
+	for (i = 0; i < estate->es_num_session_variables; i++)
+	{
+		SessionVariableValue *svarval;
+		Oid			typeOid;
+		int16		typLen;
+		bool		typByVal;
+
+		svarval = &estate->es_session_variables[i];
+
+		typeOid = svarval->typid;
+
+		sz = add_size(sz, sizeof(Oid)); /* space for type OID */
+
+		/* space for datum/isnull */
+		Assert(OidIsValid(typeOid));
+		get_typlenbyval(typeOid, &typLen, &typByVal);
+
+		sz = add_size(sz,
+					  datumEstimateSpace(svarval->value, svarval->isnull, typByVal, typLen));
+	}
+
+	return sz;
+}
+
+/*
+ * Serialize a session variables buffer into caller-provided storage.
+ *
+ * We write the number of parameters first, as a 4-byte integer, and then
+ * write details for each parameter in turn.  The details for each parameter
+ * consist of a 4-byte type OID, and then the datum as serialized by
+ * datumSerialize().  The caller is responsible for ensuring that there is
+ * enough storage to store the number of bytes that will be written; use
+ * EstimateSessionVariables to find out how many will be needed.
+ * *start_address is updated to point to the byte immediately following those
+ * written.
+ *
+ * RestoreSessionVariables can be used to recreate a session variable buffer
+ * based on the serialized representation;
+ */
+static void
+SerializeSessionVariables(EState *estate, char **start_address)
+{
+	int			nparams;
+	int			i;
+
+	/* Write number of parameters. */
+	nparams = estate->es_num_session_variables;
+	memcpy(*start_address, &nparams, sizeof(int));
+	*start_address += sizeof(int);
+
+	/* Write each parameter in turn. */
+	for (i = 0; i < nparams; i++)
+	{
+		SessionVariableValue *svarval;
+		Oid			typeOid;
+		int16		typLen;
+		bool		typByVal;
+
+		svarval = &estate->es_session_variables[i];
+		typeOid = svarval->typid;
+
+		/* Write type OID. */
+		memcpy(*start_address, &typeOid, sizeof(Oid));
+		*start_address += sizeof(Oid);
+
+		Assert(OidIsValid(typeOid));
+		get_typlenbyval(typeOid, &typLen, &typByVal);
+
+		datumSerialize(svarval->value, svarval->isnull, typByVal, typLen,
+					   start_address);
+	}
+}
+
+static SessionVariableValue *
+RestoreSessionVariables(char **start_address, int *num_session_variables)
+{
+	SessionVariableValue *session_variables;
+	int			i;
+	int			nparams;
+
+	memcpy(&nparams, *start_address, sizeof(int));
+	*start_address += sizeof(int);
+
+	*num_session_variables = nparams;
+	session_variables = (SessionVariableValue *)
+		palloc(nparams * sizeof(SessionVariableValue));
+
+	for (i = 0; i < nparams; i++)
+	{
+		SessionVariableValue *svarval = &session_variables[i];
+
+		/* Read type OID. */
+		memcpy(&svarval->typid, *start_address, sizeof(Oid));
+		*start_address += sizeof(Oid);
+
+		/* Read datum/isnull. */
+		svarval->value = datumRestore(start_address, &svarval->isnull);
+	}
+
+	return session_variables;
+}
diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c
index a5452c0c9e4..8cee732561d 100644
--- a/src/backend/optimizer/util/clauses.c
+++ b/src/backend/optimizer/util/clauses.c
@@ -924,25 +924,19 @@ max_parallel_hazard_walker(Node *node, max_parallel_hazard_context *context)
 
 	/*
 	 * We can't pass Params to workers at the moment either, so they are also
-	 * parallel-restricted, unless they are PARAM_EXTERN Params or are
-	 * PARAM_EXEC Params listed in safe_param_ids, meaning they could be
-	 * either generated within workers or can be computed by the leader and
-	 * then their value can be passed to workers.
+	 * parallel-restricted, unless they are PARAM_EXTERN or PARAM_VARIABLE
+	 * Params or are PARAM_EXEC Params listed in safe_param_ids, meaning they
+	 * could be either generated within workers or can be computed by the
+	 * leader and then their value can be passed to workers.
 	 */
 	else if (IsA(node, Param))
 	{
 		Param	   *param = (Param *) node;
 
-		if (param->paramkind == PARAM_EXTERN)
+		if (param->paramkind == PARAM_EXTERN ||
+			param->paramkind == PARAM_VARIABLE)
 			return false;
 
-		/* we don't support passing session variables to workers */
-		if (param->paramkind == PARAM_VARIABLE)
-		{
-			if (max_parallel_hazard_test(PROPARALLEL_RESTRICTED, context))
-				return true;
-		}
-
 		if (param->paramkind != PARAM_EXEC ||
 			!list_member_int(context->safe_param_ids, param->paramid))
 		{
diff --git a/src/test/regress/expected/session_variables.out b/src/test/regress/expected/session_variables.out
index 388c8ee8e72..20bf282b3f2 100644
--- a/src/test/regress/expected/session_variables.out
+++ b/src/test/regress/expected/session_variables.out
@@ -644,12 +644,14 @@ SELECT count(*) FROM svar_test WHERE a%10 = zero;
 
 -- parallel execution is not supported yet
 EXPLAIN (COSTS OFF) SELECT count(*) FROM svar_test WHERE a%10 = zero;
-            QUERY PLAN             
------------------------------------
+                 QUERY PLAN                 
+--------------------------------------------
  Aggregate
-   ->  Seq Scan on svar_test
-         Filter: ((a % 10) = zero)
-(3 rows)
+   ->  Gather
+         Workers Planned: 2
+         ->  Parallel Seq Scan on svar_test
+               Filter: ((a % 10) = zero)
+(5 rows)
 
 LET zero = (SELECT count(*) FROM svar_test);
 -- result should be 1000
-- 
2.47.0



  [text/x-patch] v20241110-0013-Implementation-of-NOT-NULL-and-IMMUTABLE-clauses.patch (35.6K, 9-v20241110-0013-Implementation-of-NOT-NULL-and-IMMUTABLE-clauses.patch)
  download | inline diff:
From 479e4e273938c5c4319d43fd8b055586d03b08d6 Mon Sep 17 00:00:00 2001
From: "[email protected]" <[email protected]>
Date: Thu, 6 Jul 2023 14:40:37 +0200
Subject: [PATCH 13/20] Implementation of NOT NULL and IMMUTABLE clauses

Almost trivial patch - psql, pg_dump support
---
 doc/src/sgml/catalogs.sgml                    |  20 +++
 doc/src/sgml/plpgsql.sgml                     |   3 +-
 doc/src/sgml/ref/create_variable.sgml         |  32 +++-
 src/backend/catalog/pg_variable.c             |   8 +
 src/backend/commands/session_variable.c       |  69 +++++++-
 src/backend/parser/gram.y                     |  41 +++--
 src/bin/pg_dump/pg_dump.c                     |  19 ++-
 src/bin/pg_dump/pg_dump.h                     |   2 +
 src/bin/pg_dump/t/002_pg_dump.pl              |  36 ++++
 src/bin/psql/describe.c                       |   6 +-
 src/bin/psql/tab-complete.in.c                |  12 +-
 src/include/catalog/pg_variable.h             |   6 +
 src/include/nodes/parsenodes.h                |   2 +
 src/test/regress/expected/psql.out            |  36 ++--
 .../regress/expected/session_variables.out    | 155 +++++++++++++++++-
 src/test/regress/sql/session_variables.sql    | 122 ++++++++++++++
 16 files changed, 523 insertions(+), 46 deletions(-)

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 789f774770b..72110e05f58 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -9841,6 +9841,26 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
 
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>varnotnull</structfield> <type>boolean</type>
+      </para>
+      <para>
+       True if the session variable doesn't allow null values. The default value is false.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>varisimmutable</structfield> <type>boolean</type>
+      </para>
+      <para>
+       True if the variable is <link linkend="sql-createvariable-immutable">immutable</link> (cannot be modified).
+       The default value is false.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>vareoxaction</structfield> <type>char</type>
        <structfield>varxactendaction</structfield> <type>char</type>
       </para>
       <para>
diff --git a/doc/src/sgml/plpgsql.sgml b/doc/src/sgml/plpgsql.sgml
index bfbff9ab74e..e704e05a991 100644
--- a/doc/src/sgml/plpgsql.sgml
+++ b/doc/src/sgml/plpgsql.sgml
@@ -6044,7 +6044,8 @@ $$ LANGUAGE plpgsql STRICT IMMUTABLE;
      You can consider translating an Oracle package into a schema in
      <productname>PostgreSQL</productname>.  Package functions and procedures
      would then become functions and procedures in that schema, and package
-     variables could be translated to session variables in that schema.
+     variables could be translated to session variables or immutable session
+     variables in that schema.
      (see <xref linkend="ddl-session-variables"/>).
     </para>
    </sect3>
diff --git a/doc/src/sgml/ref/create_variable.sgml b/doc/src/sgml/ref/create_variable.sgml
index 8187c18e28b..00938743c0d 100644
--- a/doc/src/sgml/ref/create_variable.sgml
+++ b/doc/src/sgml/ref/create_variable.sgml
@@ -26,8 +26,8 @@ PostgreSQL documentation
 
  <refsynopsisdiv>
 <synopsis>
-CREATE [ { TEMPORARY | TEMP } ] VARIABLE [ IF NOT EXISTS ] <replaceable class="parameter">name</replaceable> [ AS ] <replaceable class="parameter">data_type</replaceable> ] [ COLLATE <replaceable class="parameter">collation</replaceable> ]
-    [ DEFAULT <replaceable class="parameter">default_expr</replaceable> ] [ { ON COMMIT DROP | ON TRANSACTION END RESET } ]
+CREATE [ { TEMPORARY | TEMP } ] [ IMMUTABLE ] VARIABLE [ IF NOT EXISTS ] <replaceable class="parameter">name</replaceable> [ AS ] <replaceable class="parameter">data_type</replaceable> ] [ COLLATE <replaceable class="parameter">collation</replaceable> ]
+    [ NOT NULL ] [ DEFAULT <replaceable class="parameter">default_expr</replaceable> ] [ { ON COMMIT DROP | ON TRANSACTION END RESET } ]
 </synopsis>
  </refsynopsisdiv>
  <refsect1>
@@ -65,6 +65,22 @@ CREATE [ { TEMPORARY | TEMP } ] VARIABLE [ IF NOT EXISTS ] <replaceable class="p
 
   <variablelist>
 
+   <varlistentry id="sql-createvariable-immutable">
+    <term><literal>IMMUTABLE</literal></term>
+    <listitem>
+     <para>
+      The assigned value of the session variable can not be changed.
+      Only if the session variable doesn't have a default value, a single
+      initialization is allowed using the <command>LET</command> command. Once
+      done, no further change is allowed until end of transaction
+      if the session variable was created with clause <literal>ON TRANSACTION
+      END RESET</literal>, or until reset of all session variables by
+      <command>DISCARD VARIABLES</command>, or until reset of all session
+      objects by command <command>DISCARD ALL</command>.
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry  id="sql-createvariable-if-not-exists">
     <term><literal>IF NOT EXISTS</literal></term>
     <listitem>
@@ -105,6 +121,18 @@ CREATE [ { TEMPORARY | TEMP } ] VARIABLE [ IF NOT EXISTS ] <replaceable class="p
     </listitem>
    </varlistentry>
 
+   <varlistentry id="sql-createvariable-not-null">
+    <term><literal>NOT NULL</literal></term>
+    <listitem>
+     <para>
+      The <literal>NOT NULL</literal> clause forbids setting the session
+      variable to a null value. A session variable created as NOT NULL
+      should not to have a declared default value, and if the variable
+      has not assigned value, then the reading of this variable fails.
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry id="sql-createvariable-default">
     <term><literal>DEFAULT <replaceable>default_expr</replaceable></literal></term>
     <listitem>
diff --git a/src/backend/catalog/pg_variable.c b/src/backend/catalog/pg_variable.c
index 781587f9913..34b0b971516 100644
--- a/src/backend/catalog/pg_variable.c
+++ b/src/backend/catalog/pg_variable.c
@@ -40,6 +40,8 @@ static ObjectAddress create_variable(const char *varName,
 									 Oid varOwner,
 									 Oid varCollation,
 									 bool if_not_exists,
+									 bool not_null,
+									 bool is_immutable,
 									 Node *varDefexpr,
 									 VariableXactEndAction varXactEndAction);
 
@@ -55,6 +57,8 @@ create_variable(const char *varName,
 				Oid varOwner,
 				Oid varCollation,
 				bool if_not_exists,
+				bool not_null,
+				bool is_immutable,
 				Node *varDefexpr,
 				VariableXactEndAction varXactEndAction)
 {
@@ -117,6 +121,8 @@ create_variable(const char *varName,
 	values[Anum_pg_variable_vartypmod - 1] = Int32GetDatum(varTypmod);
 	values[Anum_pg_variable_varowner - 1] = ObjectIdGetDatum(varOwner);
 	values[Anum_pg_variable_varcollation - 1] = ObjectIdGetDatum(varCollation);
+	values[Anum_pg_variable_varnotnull - 1] = BoolGetDatum(not_null);
+	values[Anum_pg_variable_varisimmutable - 1] = BoolGetDatum(is_immutable);
 	values[Anum_pg_variable_varxactendaction - 1] = CharGetDatum(varXactEndAction);
 
 	if (varDefexpr)
@@ -259,6 +265,8 @@ CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt)
 							   varowner,
 							   collation,
 							   stmt->if_not_exists,
+							   stmt->not_null,
+							   stmt->is_immutable,
 							   cooked_default,
 							   stmt->XactEndAction);
 
diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c
index 9a92bc5e2e3..1df4a4e9bc5 100644
--- a/src/backend/commands/session_variable.c
+++ b/src/backend/commands/session_variable.c
@@ -84,6 +84,9 @@ typedef struct SVariableData
 	void	   *domain_check_extra;
 	LocalTransactionId domain_check_extra_lxid;
 
+	bool		not_null;
+	bool		is_immutable;
+
 	bool		reset_at_eox;
 
 	/*
@@ -102,6 +105,9 @@ typedef struct SVariableData
 	bool		is_valid;
 
 	uint32		hashvalue;		/* used for pairing sinval message */
+
+	/* true, when the value is already set, and cannot be changed more */
+	bool		protect_value;
 } SVariableData;
 
 typedef SVariableData *SVariable;
@@ -564,6 +570,23 @@ eval_assign_defexpr(SVariable svar, HeapTuple tup)
 
 			MemoryContextSwitchTo(oldcxt);
 		}
+		else
+		{
+			/*
+			 * Raise an error if this is a NOT NULL variable but the result of
+			 * DEFAULT expression is NULL.
+			 */
+			if (svar->not_null)
+				ereport(ERROR,
+						(errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
+						 errmsg("null value is not allowed for NOT NULL session variable \"%s.%s\"",
+								get_namespace_name(get_session_variable_namespace(svar->varid)),
+								get_session_variable_name(svar->varid)),
+						 errdetail("The result of DEFAULT expression is NULL.")));
+		}
+
+		if (svar->is_immutable)
+			svar->protect_value = true;
 
 		FreeExecutorState(estate);
 	}
@@ -594,6 +617,9 @@ setup_session_variable(SVariable svar, Oid varid, bool is_write)
 
 	get_typlenbyval(svar->typid, &svar->typlen, &svar->typbyval);
 
+	svar->not_null = varform->varnotnull;
+	svar->is_immutable = varform->varisimmutable;
+
 	svar->is_domain = (get_typtype(varform->vartype) == TYPTYPE_DOMAIN);
 	svar->domain_check_extra = NULL;
 	svar->domain_check_extra_lxid = InvalidLocalTransactionId;
@@ -623,9 +649,31 @@ setup_session_variable(SVariable svar, Oid varid, bool is_write)
 	svar->hashvalue = GetSysCacheHashValue1(VARIABLEOID,
 											ObjectIdGetDatum(varid));
 
-	if (!is_write)
+	svar->protect_value = false;
+
+	/*
+	 * When the variable is marked as IMMUTABLE, we prefer to evaluate
+	 * possible DEFAULT before write op. In this case we want to protect
+	 * default value against any overwrite.
+	 */
+	if (!is_write ||
+		svar->is_immutable)
+	{
 		eval_assign_defexpr(svar, tup);
 
+		/*
+		 * Raise an error if this is a NOT NULL variable without default
+		 * expression.
+		 */
+		if (svar->isnull && svar->not_null)
+			ereport(ERROR,
+					(errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
+					 errmsg("null value is not allowed for NOT NULL session variable \"%s.%s\"",
+							get_namespace_name(get_session_variable_namespace(varid)),
+							get_session_variable_name(varid)),
+					 errdetail("The session variable was not initialized yet.")));
+	}
+
 	ReleaseSysCache(tup);
 }
 
@@ -645,6 +693,21 @@ set_session_variable(SVariable svar, Datum value, bool isnull)
 	Assert(svar);
 	Assert(!isnull || value == (Datum) 0);
 
+	if (svar->protect_value)
+		ereport(ERROR,
+				(errcode(ERRCODE_ERROR_IN_ASSIGNMENT),
+				 errmsg("session variable \"%s.%s\" is declared IMMUTABLE",
+						get_namespace_name(get_session_variable_namespace(svar->varid)),
+						get_session_variable_name(svar->varid))));
+
+	/* don't allow assignment of null to NOT NULL variable */
+	if (isnull && svar->not_null)
+		ereport(ERROR,
+				(errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
+				 errmsg("null value is not allowed for NOT NULL session variable \"%s.%s\"",
+						get_namespace_name(get_session_variable_namespace(svar->varid)),
+						get_session_variable_name(svar->varid))));
+
 	/*
 	 * Use typbyval, typbylen from session variable only when they are
 	 * trustworthy (the invalidation message was not accepted for this
@@ -685,6 +748,10 @@ set_session_variable(SVariable svar, Datum value, bool isnull)
 
 	svar->value = newval;
 	svar->isnull = isnull;
+
+	/* don't allow more changes of value when variable is IMMUTABLE */
+	if (svar->is_immutable)
+		svar->protect_value = true;
 }
 
 /*
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index b8e33ed19db..6aea3bd35b2 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -668,6 +668,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 				json_object_constructor_null_clause_opt
 				json_array_constructor_null_clause_opt
 
+%type <boolean>		OptNotNull OptImmutable
 
 /*
  * Non-keyword token types.  These are hard-wired into the "flex" lexer.
@@ -5231,27 +5232,31 @@ create_extension_opt_item:
  *****************************************************************************/
 
 CreateSessionVarStmt:
-			CREATE OptTemp VARIABLE qualified_name opt_as Typename opt_collate_clause OptSessionVarDefExpr XactEndActionOption
+			CREATE OptTemp OptImmutable VARIABLE qualified_name opt_as Typename opt_collate_clause OptNotNull OptSessionVarDefExpr XactEndActionOption
 				{
 					CreateSessionVarStmt *n = makeNode(CreateSessionVarStmt);
-					$4->relpersistence = $2;
-					n->variable = $4;
-					n->typeName = $6;
-					n->collClause = (CollateClause *) $7;
-					n->defexpr = $8;
-					n->XactEndAction = $9;
+					$5->relpersistence = $2;
+					n->is_immutable = $3;
+					n->variable = $5;
+					n->typeName = $7;
+					n->collClause = (CollateClause *) $8;
+					n->not_null = $9;
+					n->defexpr = $10;
+					n->XactEndAction = $11;
 					n->if_not_exists = false;
 					$$ = (Node *) n;
 				}
-			| CREATE OptTemp VARIABLE IF_P NOT EXISTS qualified_name opt_as Typename opt_collate_clause OptSessionVarDefExpr XactEndActionOption
+			| CREATE OptTemp OptImmutable VARIABLE IF_P NOT EXISTS qualified_name opt_as Typename opt_collate_clause OptNotNull OptSessionVarDefExpr XactEndActionOption
 				{
 					CreateSessionVarStmt *n = makeNode(CreateSessionVarStmt);
-					$7->relpersistence = $2;
-					n->variable = $7;
-					n->typeName = $9;
-					n->collClause = (CollateClause *) $10;
-					n->defexpr = $11;
-					n->XactEndAction = $12;
+					$8->relpersistence = $2;
+					n->is_immutable = $3;
+					n->variable = $8;
+					n->typeName = $10;
+					n->collClause = (CollateClause *) $11;
+					n->not_null = $12;
+					n->defexpr = $13;
+					n->XactEndAction = $14;
 					n->if_not_exists = true;
 					$$ = (Node *) n;
 				}
@@ -5271,6 +5276,14 @@ XactEndActionOption:  ON COMMIT DROP				{ $$ = VARIABLE_XACTEND_DROP; }
 		;
 
 
+OptNotNull: NOT NULL_P								{ $$ = true; }
+			| /* EMPTY */							{ $$ = false; }
+		;
+
+OptImmutable: IMMUTABLE								{ $$ = true; }
+			| /* EMPTY */							{ $$ = false; }
+		;
+
 /*****************************************************************************
  *
  * ALTER EXTENSION name UPDATE [ TO version ]
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index c138994304a..59f4103ca4f 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -5437,6 +5437,8 @@ getVariables(Archive *fout)
 	int			i_varxactendaction;
 	int			i_varowner;
 	int			i_varcollation;
+	int			i_varnotnull;
+	int			i_varisimmutable;
 	int			i_varacl;
 	int			i_acldefault;
 	int			i,
@@ -5459,6 +5461,8 @@ getVariables(Archive *fout)
 					  "            THEN v.varcollation\n"
 					  "            ELSE 0\n"
 					  "       END AS varcollation,\n"
+					  "       v.varnotnull,\n"
+					  "       v.varisimmutable,\n"
 					  "       pg_catalog.pg_get_expr(v.vardefexpr,0) as vardefexpr,\n"
 					  "       v.varowner, v.varacl,\n"
 					  "       acldefault('V', v.varowner) AS acldefault\n"
@@ -5479,6 +5483,8 @@ getVariables(Archive *fout)
 	i_vardefexpr = PQfnumber(res, "vardefexpr");
 	i_varxactendaction = PQfnumber(res, "varxactendaction");
 	i_varcollation = PQfnumber(res, "varcollation");
+	i_varnotnull = PQfnumber(res, "varnotnull");
+	i_varisimmutable = PQfnumber(res, "varisimmutable");
 
 	i_varowner = PQfnumber(res, "varowner");
 	i_varacl = PQfnumber(res, "varacl");
@@ -5505,6 +5511,8 @@ getVariables(Archive *fout)
 			pg_strdup(PQgetvalue(res, i, i_varxactendaction));
 
 		varinfo[i].varcollation = atooid(PQgetvalue(res, i, i_varcollation));
+		varinfo[i].varnotnull = *(PQgetvalue(res, i, i_varnotnull)) == 't';
+		varinfo[i].varisimmutable = *(PQgetvalue(res, i, i_varisimmutable)) == 't';
 
 		varinfo[i].dacl.acl = pg_strdup(PQgetvalue(res, i, i_varacl));
 		varinfo[i].dacl.acldefault = pg_strdup(PQgetvalue(res, i, i_acldefault));
@@ -5551,7 +5559,9 @@ dumpVariable(Archive *fout, const VariableInfo *varinfo)
 	const char *vartypname;
 	const char *vardefexpr;
 	const char *varxactendaction;
+	const char *varisimmutable;
 	Oid			varcollation;
+	bool		varnotnull;
 
 	/* skip if not to be dumped */
 	if (!varinfo->dobj.dump || dopt->dataOnly)
@@ -5565,12 +5575,14 @@ dumpVariable(Archive *fout, const VariableInfo *varinfo)
 	vardefexpr = varinfo->vardefexpr;
 	varxactendaction = varinfo->varxactendaction;
 	varcollation = varinfo->varcollation;
+	varnotnull = varinfo->varnotnull;
+	varisimmutable = varinfo->varisimmutable ? "IMMUTABLE " : "";
 
 	appendPQExpBuffer(delq, "DROP VARIABLE %s;\n",
 					  qualvarname);
 
-	appendPQExpBuffer(query, "CREATE VARIABLE %s AS %s",
-					  qualvarname, vartypname);
+	appendPQExpBuffer(query, "CREATE %sVARIABLE %s AS %s",
+					  varisimmutable, qualvarname, vartypname);
 
 	if (OidIsValid(varcollation))
 	{
@@ -5582,6 +5594,9 @@ dumpVariable(Archive *fout, const VariableInfo *varinfo)
 							  fmtQualifiedDumpable(coll));
 	}
 
+	if (varnotnull)
+		appendPQExpBuffer(query, " NOT NULL");
+
 	if (vardefexpr)
 		appendPQExpBuffer(query, " DEFAULT %s",
 						  vardefexpr);
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 8ed83979ec9..f7921f942d0 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -717,6 +717,8 @@ typedef struct _VariableInfo
 	char	   *initrvaracl;
 	Oid			varcollation;
 	const char *rolname;		/* name of owner, or empty string */
+	bool		varnotnull;
+	bool		varisimmutable;
 } VariableInfo;
 
 /*
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index f58ede5334a..47cb59356e0 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -4041,6 +4041,42 @@ my %tests = (
 		},
 	},
 
+	'CREATE IMMUTABLE VARIABLE test_variable' => {
+		all_runs     => 1,
+		catch_all    => 'CREATE ... commands',
+		create_order => 61,
+		create_sql   => 'CREATE IMMUTABLE VARIABLE dump_test.variable5 AS integer',
+		regexp => qr/^
+			\QCREATE IMMUTABLE VARIABLE dump_test.variable5 AS integer;\E/xm,
+		like => {
+			%full_runs,
+			%dump_test_schema_runs,
+			section_pre_data => 1,
+		},
+		unlike => {
+			exclude_dump_test_schema => 1,
+			only_dump_measurement    => 1,
+		},
+	},
+
+	'CREATE IMMUTABLE VARIABLE test_variable NOT NULL DEFAULT' => {
+		all_runs     => 1,
+		catch_all    => 'CREATE ... commands',
+		create_order => 61,
+		create_sql   => 'CREATE IMMUTABLE VARIABLE dump_test.variable7 AS integer NOT NULL DEFAULT 10',
+		regexp => qr/^
+			\QCREATE IMMUTABLE VARIABLE dump_test.variable7 AS integer NOT NULL DEFAULT 10;\E/xm,
+		like => {
+			%full_runs,
+			%dump_test_schema_runs,
+			section_pre_data => 1,
+		},
+		unlike => {
+			exclude_dump_test_schema => 1,
+			only_dump_measurement    => 1,
+		},
+	},
+
 	'CREATE VIEW test_view' => {
 		create_order => 61,
 		create_sql => 'CREATE VIEW dump_test.test_view
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index dc18da350eb..cc69f07f1f9 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -5206,7 +5206,7 @@ listVariables(const char *pattern, bool verbose)
 	PQExpBufferData buf;
 	PGresult   *res;
 	printQueryOpt myopt = pset.popt;
-	static const bool translate_columns[] = {false, false, false, false, false, false, false, false, false};
+	static const bool translate_columns[] = {false, false, false, false, false, false, false, false, false, false, false};
 
 	if (pset.sversion < 180000)
 	{
@@ -5227,6 +5227,8 @@ listVariables(const char *pattern, bool verbose)
 					  "  (SELECT c.collname FROM pg_catalog.pg_collation c, pg_catalog.pg_type bt\n"
 					  "   WHERE c.oid = v.varcollation AND bt.oid = v.vartype AND v.varcollation <> bt.typcollation) as \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(v.varowner) as \"%s\",\n"
+					  "  NOT v.varnotnull as \"%s\",\n"
+					  "  NOT v.varisimmutable as \"%s\",\n"
 					  "  pg_catalog.pg_get_expr(v.vardefexpr, 0) as \"%s\",\n"
 					  "  CASE v.varxactendaction\n"
 					  "    WHEN 'd' THEN 'ON COMMIT DROP'\n"
@@ -5237,6 +5239,8 @@ listVariables(const char *pattern, bool verbose)
 					  gettext_noop("Type"),
 					  gettext_noop("Collation"),
 					  gettext_noop("Owner"),
+					  gettext_noop("Nullable"),
+					  gettext_noop("Mutable"),
 					  gettext_noop("Default"),
 					  gettext_noop("Transactional end action"));
 
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 3fb2c4e9248..b653dda1fe8 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -1273,6 +1273,8 @@ static const pgsql_thing_t words_after_create[] = {
 	{"FOREIGN TABLE", NULL, NULL, NULL},
 	{"FUNCTION", NULL, NULL, Query_for_list_of_functions},
 	{"GROUP", Query_for_list_of_roles},
+	{"IMMUTABLE VARIABLE", NULL, NULL, NULL, NULL, THING_NO_DROP | THING_NO_ALTER}, /* for CREATE IMMUTABLE
+																					 * VARIABLE ... */
 	{"INDEX", NULL, NULL, &Query_for_list_of_indexes},
 	{"LANGUAGE", Query_for_list_of_languages},
 	{"LARGE OBJECT", NULL, NULL, NULL, NULL, THING_NO_CREATE | THING_NO_DROP},
@@ -3593,7 +3595,8 @@ match_previous_words(int pattern_id,
 /* CREATE TABLE --- is allowed inside CREATE SCHEMA, so use TailMatches */
 	/* Complete "CREATE TEMP/TEMPORARY" with the possible temp objects */
 	else if (TailMatches("CREATE", "TEMP|TEMPORARY"))
-		COMPLETE_WITH("SEQUENCE", "TABLE", "VARIABLE", "VIEW");
+		COMPLETE_WITH("IMMUTABLE VARIABLE", "SEQUENCE", "TABLE", "VARIABLE",
+					  "VIEW");
 	/* Complete "CREATE UNLOGGED" with TABLE or SEQUENCE */
 	else if (TailMatches("CREATE", "UNLOGGED"))
 		COMPLETE_WITH("TABLE", "SEQUENCE");
@@ -3945,7 +3948,8 @@ match_previous_words(int pattern_id,
 /* CREATE VARIABLE --- is allowed inside CREATE SCHEMA, so use TailMatches */
 	/* Complete CREATE VARIABLE <name> with AS */
 	else if (TailMatches("CREATE", "VARIABLE", MatchAny) ||
-			 TailMatches("TEMP|TEMPORARY", "VARIABLE", MatchAny))
+			 TailMatches("TEMP|TEMPORARY", "VARIABLE", MatchAny) ||
+			 TailMatches("IMMUTABLE", "VARIABLE", MatchAny))
 		COMPLETE_WITH("AS");
 	else if (TailMatches("VARIABLE", MatchAny, "AS"))
 		/* Complete CREATE VARIABLE <name> with AS types */
@@ -4589,6 +4593,10 @@ match_previous_words(int pattern_id,
 	else if (TailMatches("FROM", "SERVER", MatchAny, "INTO", MatchAny))
 		COMPLETE_WITH("OPTIONS (");
 
+/* IMMUTABLE -- can be expend to IMMUTABLE VARIABLE */
+	else if (TailMatches("CREATE", "IMMUTABLE"))
+		COMPLETE_WITH("VARIABLE");
+
 /* INSERT --- can be inside EXPLAIN, RULE, etc */
 	/* Complete NOT MATCHED THEN INSERT */
 	else if (TailMatches("NOT", "MATCHED", "THEN", "INSERT"))
diff --git a/src/include/catalog/pg_variable.h b/src/include/catalog/pg_variable.h
index e8bfeebed11..68bc49a0e27 100644
--- a/src/include/catalog/pg_variable.h
+++ b/src/include/catalog/pg_variable.h
@@ -55,6 +55,12 @@ CATALOG(pg_variable,9222,VariableRelationId)
 	/* typmod for variable's type */
 	int32		vartypmod BKI_DEFAULT(-1);
 
+	/* don't allow NULL */
+	bool		varnotnull BKI_DEFAULT(f);
+
+	/* don't allow changes */
+	bool		varisimmutable BKI_DEFAULT(f);
+
 	/* action on transaction end */
 	char		varxactendaction BKI_DEFAULT(n);
 
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 60404413c49..7308c323574 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -3477,6 +3477,8 @@ typedef struct CreateSessionVarStmt
 	TypeName   *typeName;		/* the type of variable */
 	CollateClause *collClause;
 	bool		if_not_exists;	/* do nothing if it already exists */
+	bool		not_null;		/* disallow nulls */
+	bool		is_immutable;	/* don't allow changes */
 	Node	   *defexpr;		/* default expression */
 	char		XactEndAction;	/* on transaction end action */
 } CreateSessionVarStmt;
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index bc7b60a97ea..77a7bf45c8a 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -5935,20 +5935,20 @@ CREATE ROLE regress_variable_owner;
 SET ROLE TO regress_variable_owner;
 CREATE VARIABLE var1 AS varchar COLLATE "C";
 \dV+ var1
-                                                               List of variables
- Schema | Name |       Type        | Collation |         Owner          | Default | Transactional end action | Access privileges | Description 
---------+------+-------------------+-----------+------------------------+---------+--------------------------+-------------------+-------------
- public | var1 | character varying | C         | regress_variable_owner |         |                          |                   | 
+                                                                         List of variables
+ Schema | Name |       Type        | Collation |         Owner          | Nullable | Mutable | Default | Transactional end action | Access privileges | Description 
+--------+------+-------------------+-----------+------------------------+----------+---------+---------+--------------------------+-------------------+-------------
+ public | var1 | character varying | C         | regress_variable_owner | t        | t       |         |                          |                   | 
 (1 row)
 
 GRANT SELECT ON VARIABLE var1 TO PUBLIC;
 COMMENT ON VARIABLE var1 IS 'some description';
 \dV+ var1
-                                                                                 List of variables
- Schema | Name |       Type        | Collation |         Owner          | Default | Transactional end action |                Access privileges                 |   Description    
---------+------+-------------------+-----------+------------------------+---------+--------------------------+--------------------------------------------------+------------------
- public | var1 | character varying | C         | regress_variable_owner |         |                          | regress_variable_owner=rw/regress_variable_owner+| some description
-        |      |                   |           |                        |         |                          | =r/regress_variable_owner                        | 
+                                                                                           List of variables
+ Schema | Name |       Type        | Collation |         Owner          | Nullable | Mutable | Default | Transactional end action |                Access privileges                 |   Description    
+--------+------+-------------------+-----------+------------------------+----------+---------+---------+--------------------------+--------------------------------------------------+------------------
+ public | var1 | character varying | C         | regress_variable_owner | t        | t       |         |                          | regress_variable_owner=rw/regress_variable_owner+| some description
+        |      |                   |           |                        |          |         |         |                          | =r/regress_variable_owner                        | 
 (1 row)
 
 DROP VARIABLE var1;
@@ -6416,9 +6416,9 @@ List of schemas
 (0 rows)
 
 \dV "no.such.variable"
-                               List of variables
- Schema | Name | Type | Collation | Owner | Default | Transactional end action 
---------+------+------+-----------+-------+---------+--------------------------
+                                         List of variables
+ Schema | Name | Type | Collation | Owner | Nullable | Mutable | Default | Transactional end action 
+--------+------+------+-----------+-------+----------+---------+---------+--------------------------
 (0 rows)
 
 -- again, but with dotted schema qualifications.
@@ -6591,9 +6591,9 @@ improper qualified name (too many dotted names): "no.such.schema"."no.such.insta
 \dy "no.such.schema"."no.such.event.trigger"
 improper qualified name (too many dotted names): "no.such.schema"."no.such.event.trigger"
 \dV "no.such.schema"."no.such.variable"
-                               List of variables
- Schema | Name | Type | Collation | Owner | Default | Transactional end action 
---------+------+------+-----------+-------+---------+--------------------------
+                                         List of variables
+ Schema | Name | Type | Collation | Owner | Nullable | Mutable | Default | Transactional end action 
+--------+------+------+-----------+-------+----------+---------+---------+--------------------------
 (0 rows)
 
 -- again, but with current database and dotted schema qualifications.
@@ -6730,9 +6730,9 @@ List of text search templates
 (0 rows)
 
 \dV regression."no.such.schema"."no.such.variable"
-                               List of variables
- Schema | Name | Type | Collation | Owner | Default | Transactional end action 
---------+------+------+-----------+-------+---------+--------------------------
+                                         List of variables
+ Schema | Name | Type | Collation | Owner | Nullable | Mutable | Default | Transactional end action 
+--------+------+------+-----------+-------+----------+---------+---------+--------------------------
 (0 rows)
 
 -- again, but with dotted database and dotted schema qualifications.
diff --git a/src/test/regress/expected/session_variables.out b/src/test/regress/expected/session_variables.out
index 48855136489..36c73dadc27 100644
--- a/src/test/regress/expected/session_variables.out
+++ b/src/test/regress/expected/session_variables.out
@@ -45,11 +45,11 @@ SET ROLE TO regress_variable_owner;
 CREATE VARIABLE svartest.var1 AS int;
 SET ROLE TO DEFAULT;
 \dV+ svartest.var1
-                                                                          List of variables
-  Schema  | Name |  Type   | Collation |         Owner          | Default | Transactional end action |                Access privileges                 | Description 
-----------+------+---------+-----------+------------------------+---------+--------------------------+--------------------------------------------------+-------------
- svartest | var1 | integer |           | regress_variable_owner |         |                          | regress_variable_owner=rw/regress_variable_owner+| 
-          |      |         |           |                        |         |                          | regress_variable_reader=r/regress_variable_owner | 
+                                                                                     List of variables
+  Schema  | Name |  Type   | Collation |         Owner          | Nullable | Mutable | Default | Transactional end action |                Access privileges                 | Description 
+----------+------+---------+-----------+------------------------+----------+---------+---------+--------------------------+--------------------------------------------------+-------------
+ svartest | var1 | integer |           | regress_variable_owner | t        | t       |         |                          | regress_variable_owner=rw/regress_variable_owner+| 
+          |      |         |           |                        |          |         |         |                          | regress_variable_reader=r/regress_variable_owner | 
 (1 row)
 
 DROP VARIABLE svartest.var1;
@@ -1618,3 +1618,148 @@ SELECT var1;
 
 DROP VARIABLE var1;
 DROP FUNCTION vartest_fx();
+-- test NOT NULL
+-- should be ok
+CREATE VARIABLE var1 AS int NOT NULL;
+-- should to fail
+SELECT var1;
+ERROR:  null value is not allowed for NOT NULL session variable "public.var1"
+DETAIL:  The session variable was not initialized yet.
+--should be ok
+LET var1 = 10;
+SELECT var1;
+ var1 
+------
+   10
+(1 row)
+
+DROP VARIABLE var1;
+-- should be ok
+CREATE VARIABLE var1 AS int NOT NULL DEFAULT 0;
+--should be ok
+SELECT var1;
+ var1 
+------
+    0
+(1 row)
+
+-- should be ok
+LET var1 = 10;
+SELECT var1;
+ var1 
+------
+   10
+(1 row)
+
+DISCARD VARIABLES;
+-- should to fail
+LET var1 = NULL;
+ERROR:  null value is not allowed for NOT NULL session variable "public.var1"
+DROP VARIABLE var1;
+-- test NOT NULL
+CREATE OR REPLACE FUNCTION vartest_fx()
+RETURNS int AS $$
+BEGIN
+  RETURN NULL;
+END;
+$$ LANGUAGE plpgsql;
+CREATE VARIABLE var1 AS int NOT NULL DEFAULT vartest_fx();
+-- should to fail
+SELECT var1;
+ERROR:  null value is not allowed for NOT NULL session variable "public.var1"
+DETAIL:  The result of DEFAULT expression is NULL.
+DISCARD VARIABLES;
+-- should be ok
+LET var1 = 10;
+SELECT var1;
+ var1 
+------
+   10
+(1 row)
+
+CREATE OR REPLACE FUNCTION vartest_fx()
+RETURNS int AS $$
+BEGIN
+  RETURN 0;
+END;
+$$ LANGUAGE plpgsql;
+DISCARD VARIABLES;
+-- should be ok
+SELECT var1;
+ var1 
+------
+    0
+(1 row)
+
+DROP VARIABLE var1;
+DROP FUNCTION vartest_fx();
+-- test IMMUTBLE
+CREATE IMMUTABLE VARIABLE var1 AS int;
+-- should be ok
+SELECT var1;
+ var1 
+------
+     
+(1 row)
+
+-- first write should ok
+-- should be ok
+LET var1 = 10;
+-- should fail
+LET var1 = 20;
+ERROR:  session variable "public.var1" is declared IMMUTABLE
+DISCARD VARIABLES;
+-- should be ok
+LET var1 = 10;
+-- should fail
+LET var1 = 20;
+ERROR:  session variable "public.var1" is declared IMMUTABLE
+DISCARD VARIABLES;
+-- should be ok
+SELECT var1;
+ var1 
+------
+     
+(1 row)
+
+-- should be ok
+LET var1 = NULL;
+-- should fail
+LET var1 = 20;
+ERROR:  session variable "public.var1" is declared IMMUTABLE
+DROP VARIABLE var1;
+CREATE IMMUTABLE VARIABLE var1 AS int DEFAULT 10;
+-- don't allow change when variable has DEFAULT value
+-- should to fail
+LET var1 = 20;
+ERROR:  session variable "public.var1" is declared IMMUTABLE
+DISCARD VARIABLES;
+-- should be ok
+SELECT var1;
+ var1 
+------
+   10
+(1 row)
+
+-- should fail
+LET var1 = 20;
+ERROR:  session variable "public.var1" is declared IMMUTABLE
+DROP VARIABLE var1;
+-- should be ok
+CREATE IMMUTABLE VARIABLE var1 AS INT NOT NULL DEFAULT 10;
+-- should to fail
+LET var1 = 10;
+ERROR:  session variable "public.var1" is declared IMMUTABLE
+LET var1 = 20;
+ERROR:  session variable "public.var1" is declared IMMUTABLE
+-- should be ok
+SELECT var1;
+ var1 
+------
+   10
+(1 row)
+
+-- should to fail
+LET var1 = 30;
+ERROR:  session variable "public.var1" is declared IMMUTABLE
+DROP VARIABLE var1;
diff --git a/src/test/regress/sql/session_variables.sql b/src/test/regress/sql/session_variables.sql
index b8d56edf46c..3fb6267adb4 100644
--- a/src/test/regress/sql/session_variables.sql
+++ b/src/test/regress/sql/session_variables.sql
@@ -1087,3 +1087,125 @@ SELECT var1;
 
 DROP VARIABLE var1;
 DROP FUNCTION vartest_fx();
+
+-- test NOT NULL
+-- should be ok
+CREATE VARIABLE var1 AS int NOT NULL;
+-- should to fail
+SELECT var1;
+
+--should be ok
+LET var1 = 10;
+SELECT var1;
+
+DROP VARIABLE var1;
+
+-- should be ok
+CREATE VARIABLE var1 AS int NOT NULL DEFAULT 0;
+
+--should be ok
+SELECT var1;
+
+-- should be ok
+LET var1 = 10;
+SELECT var1;
+
+DISCARD VARIABLES;
+
+-- should to fail
+LET var1 = NULL;
+
+DROP VARIABLE var1;
+
+-- test NOT NULL
+CREATE OR REPLACE FUNCTION vartest_fx()
+RETURNS int AS $$
+BEGIN
+  RETURN NULL;
+END;
+$$ LANGUAGE plpgsql;
+
+CREATE VARIABLE var1 AS int NOT NULL DEFAULT vartest_fx();
+
+-- should to fail
+SELECT var1;
+
+DISCARD VARIABLES;
+
+-- should be ok
+LET var1 = 10;
+SELECT var1;
+
+CREATE OR REPLACE FUNCTION vartest_fx()
+RETURNS int AS $$
+BEGIN
+  RETURN 0;
+END;
+$$ LANGUAGE plpgsql;
+
+DISCARD VARIABLES;
+
+-- should be ok
+SELECT var1;
+
+DROP VARIABLE var1;
+DROP FUNCTION vartest_fx();
+
+-- test IMMUTBLE
+CREATE IMMUTABLE VARIABLE var1 AS int;
+
+-- should be ok
+SELECT var1;
+-- first write should ok
+-- should be ok
+LET var1 = 10;
+-- should fail
+LET var1 = 20;
+
+DISCARD VARIABLES;
+
+-- should be ok
+LET var1 = 10;
+-- should fail
+LET var1 = 20;
+
+DISCARD VARIABLES;
+
+-- should be ok
+SELECT var1;
+-- should be ok
+LET var1 = NULL;
+-- should fail
+LET var1 = 20;
+
+DROP VARIABLE var1;
+
+CREATE IMMUTABLE VARIABLE var1 AS int DEFAULT 10;
+
+-- don't allow change when variable has DEFAULT value
+-- should to fail
+LET var1 = 20;
+
+DISCARD VARIABLES;
+
+-- should be ok
+SELECT var1;
+-- should fail
+LET var1 = 20;
+
+DROP VARIABLE var1;
+
+-- should be ok
+CREATE IMMUTABLE VARIABLE var1 AS INT NOT NULL DEFAULT 10;
+
+-- should to fail
+LET var1 = 10;
+LET var1 = 20;
+
+-- should be ok
+SELECT var1;
+
+-- should to fail
+LET var1 = 30;
+
+DROP VARIABLE var1;
-- 
2.47.0



  [text/x-patch] v20241110-0014-allow-read-an-value-of-session-variable-directly-fro.patch (13.3K, 10-v20241110-0014-allow-read-an-value-of-session-variable-directly-fro.patch)
  download | inline diff:
From d1d04c6b7521c35d0066fcc96a7d3e4016a3358b Mon Sep 17 00:00:00 2001
From: "[email protected]" <[email protected]>
Date: Sat, 20 Jan 2024 06:31:48 +0100
Subject: [PATCH 14/20] allow read an value of session variable directly from
 expression executor

The expression executor can be called directly (not inside query executor)
when expression is evaluated as simple expression in plpgsql or when expression
is an argument of CALL or EXECUTE statements. For these cases we need to allow direct
access to content of session variables from executor. We can do it, because
we know so the expression is not evaluated under query and cannot be
evaluated inside parallel worker.

This patch enables session variables for simple expression evaluation.

This patch enables usage session variables in arguments of CALL stmt.
---
 src/backend/commands/prepare.c                |  8 --
 src/backend/executor/execExpr.c               | 82 ++++++++++++++-----
 src/backend/executor/execExprInterp.c         | 16 ++++
 src/backend/jit/llvm/llvmjit_expr.c           |  6 ++
 src/backend/parser/analyze.c                  |  8 --
 src/include/executor/execExpr.h               | 12 ++-
 .../src/expected/plpgsql_session_variable.out |  3 +-
 src/pl/plpgsql/src/pl_exec.c                  |  3 +-
 .../regress/expected/session_variables.out    | 26 +++---
 src/test/regress/sql/session_variables.sql    | 12 +--
 10 files changed, 117 insertions(+), 59 deletions(-)

diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 3735b5554e0..e8c375df198 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -338,14 +338,6 @@ EvaluateParams(ParseState *pstate, PreparedStatement *pstmt, List *params,
 		i++;
 	}
 
-	/*
-	 * The arguments of EXECUTE are evaluated by a direct expression
-	 * executor call.  This mode doesn't support session variables yet.
-	 * It will be enabled later.
-	 */
-	if (pstate->p_hasSessionVariables)
-		elog(ERROR, "session variable cannot be used as an argument");
-
 	/* Prepare the expressions for execution */
 	exprstates = ExecPrepareExprList(params, estate);
 
diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c
index 6dc8c562bec..845bdd1ae85 100644
--- a/src/backend/executor/execExpr.c
+++ b/src/backend/executor/execExpr.c
@@ -1005,25 +1005,69 @@ ExecInitExprRec(Expr *node, ExprState *state,
 								es_num_session_variables = state->parent->state->es_num_session_variables;
 							}
 
-							Assert(es_session_variables);
-
-							/* parameter sanity checks */
-							if (param->paramid >= es_num_session_variables)
-								elog(ERROR, "paramid of PARAM_VARIABLE param is out of range");
-
-							var = &es_session_variables[param->paramid];
-
-							if (var->typid != param->paramtype)
-								elog(ERROR, "type of buffered value is different than PARAM_VARIABLE type");
-
-							/*
-							 * In this case, pass the value like a
-							 * constant.
-							 */
-							scratch.opcode = EEOP_CONST;
-							scratch.d.constval.value = var->value;
-							scratch.d.constval.isnull = var->isnull;
-							ExprEvalPushStep(state, &scratch);
+							if (es_session_variables)
+							{
+								/*
+								 * This path is used when expression is
+								 * evaluated inside query evaluation. For
+								 * ensuring of immutability of session variable
+								 * inside query we use special buffer with copy
+								 * of used session variables. This method helps
+								 * with parallel execution too.
+								 */
+
+								/* parameter sanity checks */
+								if (param->paramid >= es_num_session_variables)
+									elog(ERROR, "paramid of PARAM_VARIABLE param is out of range");
+
+								var = &es_session_variables[param->paramid];
+
+								if (var->typid != param->paramtype)
+									elog(ERROR, "type of buffered value is different than PARAM_VARIABLE type");
+
+								/*
+								 * In this case, pass the value like a
+								 * constant.
+								 */
+								scratch.opcode = EEOP_CONST;
+								scratch.d.constval.value = var->value;
+								scratch.d.constval.isnull = var->isnull;
+								ExprEvalPushStep(state, &scratch);
+							}
+							else
+							{
+								AclResult	aclresult;
+								Oid			varid = param->paramvarid;
+								Oid			vartype = param->paramtype;
+
+								Assert(!IsParallelWorker());
+
+								/*
+								 * In some cases (plpgsql's simple expression
+								 * evaluation or evaluation of CALL arguments),
+								 * the expression executor is called directly.
+								 * We can allow direct access to session
+								 * variables (copy only), because we know, so
+								 * outside is not any query (and expression
+								 * cannot be executed parallel).
+								 *
+								 * In this case we should to do aclcheck,
+								 * because usual aclcheck from
+								 * standard_ExecutorStart is not executed in
+								 * this case. Fortunately it is just once per
+								 * transaction.
+								 */
+								aclresult = object_aclcheck(VariableRelationId, varid,
+															GetUserId(), ACL_SELECT);
+								if (aclresult != ACLCHECK_OK)
+									aclcheck_error(aclresult, OBJECT_VARIABLE,
+												   get_session_variable_name(varid));
+
+								scratch.opcode = EEOP_PARAM_VARIABLE;
+								scratch.d.vparam.varid = varid;
+								scratch.d.vparam.vartype = vartype;
+								ExprEvalPushStep(state, &scratch);
+							}
 						}
 						break;
 
diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c
index 30c5a19aad6..0a154b97d6e 100644
--- a/src/backend/executor/execExprInterp.c
+++ b/src/backend/executor/execExprInterp.c
@@ -59,6 +59,7 @@
 #include "access/heaptoast.h"
 #include "catalog/pg_type.h"
 #include "commands/sequence.h"
+#include "commands/session_variable.h"
 #include "executor/execExpr.h"
 #include "executor/nodeSubplan.h"
 #include "funcapi.h"
@@ -450,6 +451,7 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull)
 		&&CASE_EEOP_PARAM_EXEC,
 		&&CASE_EEOP_PARAM_EXTERN,
 		&&CASE_EEOP_PARAM_CALLBACK,
+		&&CASE_EEOP_PARAM_VARIABLE,
 		&&CASE_EEOP_PARAM_SET,
 		&&CASE_EEOP_CASE_TESTVAL,
 		&&CASE_EEOP_MAKE_READONLY,
@@ -1099,6 +1101,20 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull)
 			EEO_NEXT();
 		}
 
+		EEO_CASE(EEOP_PARAM_VARIABLE)
+		{
+			/*
+			 * Direct access to session variable (without buffering). Because
+			 * returned value can be used (without an assignement) after the
+			 * referenced session variables is updated, we have to use an copy
+			 * of stored value every time.
+			 */
+			*op->resvalue = GetSessionVariableWithTypeCheck(op->d.vparam.varid,
+															op->resnull,
+															op->d.vparam.vartype);
+			EEO_NEXT();
+		}
+
 		EEO_CASE(EEOP_PARAM_SET)
 		{
 			/* out of line, unlikely to matter performance-wise */
diff --git a/src/backend/jit/llvm/llvmjit_expr.c b/src/backend/jit/llvm/llvmjit_expr.c
index 0b3b5748ea2..1ccb9c5f4a0 100644
--- a/src/backend/jit/llvm/llvmjit_expr.c
+++ b/src/backend/jit/llvm/llvmjit_expr.c
@@ -1145,6 +1145,12 @@ llvm_compile_expr(ExprState *state)
 					break;
 				}
 
+			case EEOP_PARAM_VARIABLE:
+				build_EvalXFunc(b, mod, "ExecEvalParamVariable",
+								v_state, op, v_econtext);
+				LLVMBuildBr(b, opblocks[opno + 1]);
+				break;
+
 			case EEOP_PARAM_SET:
 				build_EvalXFunc(b, mod, "ExecEvalParamSet",
 								v_state, op, v_econtext);
diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
index 9dfd435128a..496d75a494a 100644
--- a/src/backend/parser/analyze.c
+++ b/src/backend/parser/analyze.c
@@ -3470,14 +3470,6 @@ transformCallStmt(ParseState *pstate, CallStmt *stmt)
 							 true,
 							 stmt->funccall->location);
 
-	/*
-	 * The arguments of CALL statement are evaluated by a direct expression
-	 * executor call.  This path is unsupported yet, so block it.  It will be
-	 * enabled later.
-	 */
-	if (pstate->p_hasSessionVariables)
-		elog(ERROR, "session variable cannot be used as an argument");
-
 	assign_expr_collations(pstate, node);
 
 	fexpr = castNode(FuncExpr, node);
diff --git a/src/include/executor/execExpr.h b/src/include/executor/execExpr.h
index cd97dfa0624..72b54b3a33f 100644
--- a/src/include/executor/execExpr.h
+++ b/src/include/executor/execExpr.h
@@ -156,10 +156,11 @@ typedef enum ExprEvalOp
 	EEOP_BOOLTEST_IS_FALSE,
 	EEOP_BOOLTEST_IS_NOT_FALSE,
 
-	/* evaluate PARAM_EXEC/EXTERN parameters */
+	/* evaluate PARAM_EXEC/EXTERN/VARIABLE parameters */
 	EEOP_PARAM_EXEC,
 	EEOP_PARAM_EXTERN,
 	EEOP_PARAM_CALLBACK,
+	EEOP_PARAM_VARIABLE,
 	/* set PARAM_EXEC value */
 	EEOP_PARAM_SET,
 
@@ -409,6 +410,13 @@ typedef struct ExprEvalStep
 			Oid			paramtype;	/* OID of parameter's datatype */
 		}			cparam;
 
+		/* for EEOP_PARAM_VARIABLE */
+		struct
+		{
+			Oid			varid;		/* OID of assigned variable */
+			Oid			vartype;	/* OID of parameter's datatype */
+		}			vparam;
+
 		/* for EEOP_CASE_TESTVAL/DOMAIN_TESTVAL */
 		struct
 		{
@@ -831,6 +839,8 @@ extern void ExecEvalParamSet(ExprState *state, ExprEvalStep *op,
 							 ExprContext *econtext);
 extern void ExecEvalParamExtern(ExprState *state, ExprEvalStep *op,
 								ExprContext *econtext);
+extern void ExecEvalParamVariable(ExprState *state, ExprEvalStep *op,
+								  ExprContext *econtext);
 extern void ExecEvalCoerceViaIOSafe(ExprState *state, ExprEvalStep *op);
 extern void ExecEvalSQLValueFunction(ExprState *state, ExprEvalStep *op);
 extern void ExecEvalCurrentOfExpr(ExprState *state, ExprEvalStep *op);
diff --git a/src/pl/plpgsql/src/expected/plpgsql_session_variable.out b/src/pl/plpgsql/src/expected/plpgsql_session_variable.out
index ddb099bbbb1..789c3123b0c 100644
--- a/src/pl/plpgsql/src/expected/plpgsql_session_variable.out
+++ b/src/pl/plpgsql/src/expected/plpgsql_session_variable.out
@@ -211,8 +211,7 @@ SET ROLE TO regress_var_exec_role;
 -- should to fail
 SELECT var_read_func();
 ERROR:  permission denied for session variable plpgsql_sv_var1
-CONTEXT:  PL/pgSQL expression "plpgsql_sv_var1"
-PL/pgSQL function var_read_func() line 3 at RAISE
+CONTEXT:  PL/pgSQL function var_read_func() line 3 at RAISE
 SET ROLE TO DEFAULT;
 SET ROLE TO regress_var_owner_role;
 GRANT SELECT ON VARIABLE plpgsql_sv_var1 TO regress_var_reader_role;
diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
index 1361a8a8480..86c5bd324a9 100644
--- a/src/pl/plpgsql/src/pl_exec.c
+++ b/src/pl/plpgsql/src/pl_exec.c
@@ -8099,8 +8099,7 @@ exec_is_simple_query(PLpgSQL_expr *expr)
 		query->sortClause ||
 		query->limitOffset ||
 		query->limitCount ||
-		query->setOperations ||
-		query->hasSessionVariables)
+		query->setOperations)
 		return false;
 
 	/*
diff --git a/src/test/regress/expected/session_variables.out b/src/test/regress/expected/session_variables.out
index 36c73dadc27..388c8ee8e72 100644
--- a/src/test/regress/expected/session_variables.out
+++ b/src/test/regress/expected/session_variables.out
@@ -108,8 +108,7 @@ ERROR:  permission denied for session variable var1
 CONTEXT:  SQL function "sqlfx" statement 1
 SELECT plpgsqlfx(20);
 ERROR:  permission denied for session variable var1
-CONTEXT:  PL/pgSQL expression "$1 + var1"
-PL/pgSQL function plpgsqlfx(integer) line 1 at RETURN
+CONTEXT:  PL/pgSQL function plpgsqlfx(integer) line 1 at RETURN
 -- should be ok
 SELECT sqlfx_sd(20);
  sqlfx_sd 
@@ -279,27 +278,28 @@ $$;
 NOTICE:  result array: {1,2,3,4,5,6,7,8,9,10}
 DROP VARIABLE var1;
 DROP VARIABLE var2;
--- CALL statement is not supported yet
--- requires direct access to session variable from expression executor
+-- CALL statement is supported
 CREATE VARIABLE v int;
+LET v = 1;
 CREATE PROCEDURE p(arg int) AS $$ BEGIN RAISE NOTICE '%', arg; END $$ LANGUAGE plpgsql;
--- should not crash (but is not supported yet)
+-- should to work
 CALL p(v);
-ERROR:  session variable cannot be used as an argument
+NOTICE:  1
 DO $$ BEGIN CALL p(v); END $$;
-ERROR:  session variable cannot be used as an argument
-CONTEXT:  SQL statement "CALL p(v)"
-PL/pgSQL function inline_code_block line 1 at CALL
+NOTICE:  1
 DROP PROCEDURE p(int);
 DROP VARIABLE v;
--- EXECUTE statement is not supported yet
--- requires direct access to session variable from expression executor
+-- EXECUTE statement
 CREATE VARIABLE v int;
 LET v = 20;
 PREPARE ptest(int) AS SELECT $1;
--- should fail
+-- should be ok
 EXECUTE ptest(v);
-ERROR:  session variable cannot be used as an argument
+ ?column? 
+----------
+       20
+(1 row)
+
 DEALLOCATE ptest;
 DROP VARIABLE v;
 -- test search path
diff --git a/src/test/regress/sql/session_variables.sql b/src/test/regress/sql/session_variables.sql
index 3fb6267adb4..27c13b487b0 100644
--- a/src/test/regress/sql/session_variables.sql
+++ b/src/test/regress/sql/session_variables.sql
@@ -207,13 +207,14 @@ $$;
 DROP VARIABLE var1;
 DROP VARIABLE var2;
 
--- CALL statement is not supported yet
--- requires direct access to session variable from expression executor
+-- CALL statement is supported
 CREATE VARIABLE v int;
 
+LET v = 1;
+
 CREATE PROCEDURE p(arg int) AS $$ BEGIN RAISE NOTICE '%', arg; END $$ LANGUAGE plpgsql;
 
--- should not crash (but is not supported yet)
+-- should to work
 CALL p(v);
 
 DO $$ BEGIN CALL p(v); END $$;
@@ -221,13 +222,12 @@ DO $$ BEGIN CALL p(v); END $$;
 DROP PROCEDURE p(int);
 DROP VARIABLE v;
 
--- EXECUTE statement is not supported yet
--- requires direct access to session variable from expression executor
+-- EXECUTE statement
 CREATE VARIABLE v int;
 LET v = 20;
 PREPARE ptest(int) AS SELECT $1;
 
--- should fail
+-- should be ok
 EXECUTE ptest(v);
 
 DEALLOCATE ptest;
-- 
2.47.0



  [text/x-patch] v20241110-0012-Implementation-of-DEFAULT-clause-default-expressions.patch (33.6K, 11-v20241110-0012-Implementation-of-DEFAULT-clause-default-expressions.patch)
  download | inline diff:
From 190c9c555da180e1a692c274ec861d8fd3a93dcd Mon Sep 17 00:00:00 2001
From: "[email protected]" <[email protected]>
Date: Thu, 6 Jul 2023 10:44:20 +0200
Subject: [PATCH 12/20] Implementation of DEFAULT clause - default expressions
 for session variables

When the evaluation of default expression fails, we remove related entry from sessionvars
hash table. Then sessionvars can contain only sucessfully initialized values. Then we don't
need special flag for badly initialized session variables.
---
 doc/src/sgml/catalogs.sgml                    |  8 ++
 doc/src/sgml/ddl.sgml                         | 16 ++--
 doc/src/sgml/ref/create_variable.sgml         | 21 +++--
 doc/src/sgml/ref/discard.sgml                 |  3 +-
 src/backend/catalog/pg_variable.c             | 27 ++++++
 src/backend/commands/session_variable.c       | 87 ++++++++++++++++++-
 src/backend/parser/gram.y                     | 15 +++-
 src/backend/parser/parse_agg.c                |  2 +
 src/backend/parser/parse_expr.c               |  4 +
 src/backend/parser/parse_func.c               |  1 +
 src/bin/pg_dump/pg_dump.c                     | 14 +++
 src/bin/pg_dump/pg_dump.h                     |  1 +
 src/bin/pg_dump/t/002_pg_dump.pl              | 36 ++++++++
 src/bin/psql/describe.c                       |  4 +-
 src/include/catalog/pg_variable.h             |  3 +
 src/include/nodes/parsenodes.h                |  1 +
 src/include/parser/parse_node.h               |  1 +
 src/test/regress/expected/psql.out            | 36 ++++----
 .../regress/expected/session_variables.out    | 73 ++++++++++++++--
 src/test/regress/sql/session_variables.sql    | 48 ++++++++++
 20 files changed, 355 insertions(+), 46 deletions(-)

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 76eff813b43..789f774770b 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -9873,6 +9873,14 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>vardefexpr</structfield> <type>pg_node_tree</type>
+      </para>
+      <para>
+       The internal representation of the variable default value
+      </para></entry>
+     </row>
     </tbody>
    </tgroup>
   </table>
diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml
index 05c9a0aebcc..a20db14af78 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -5338,14 +5338,14 @@ SELECT current_user_id;
 
    <para>
     The value of a session variable is local to the current session. Retrieving
-    a variable's value returns a <literal>NULL</literal>, unless its value has
-    been set to something else in the current session using the
-    <command>LET</command> command.  Session variables are not transactional:
-    any changes made to the value of a session variable in a transaction won't
-    be undone if the transaction is rolled back (just like variables in
-    procedural languages).  Session variables themselves can be persistent
-    or temporary, but their values are neither persistent nor shared (like the
-    content of temporary tables).
+    a variable's value returns a <literal>NULL</literal> or a default value,
+    unless its value has  been set to something else in the current session
+    using the <command>LET</command> command.  Session variables are not
+    transactional: any changes made to the value of a session variable in
+    a transaction won't be undone if the transaction is rolled back (just like
+    variables in procedural languages).  Session variables themselves can be
+    persistent or temporary, but their values are neither persistent nor shared
+    (like the content of temporary tables).
    </para>
 
    <para>
diff --git a/doc/src/sgml/ref/create_variable.sgml b/doc/src/sgml/ref/create_variable.sgml
index e1c2940d4ca..8187c18e28b 100644
--- a/doc/src/sgml/ref/create_variable.sgml
+++ b/doc/src/sgml/ref/create_variable.sgml
@@ -27,7 +27,7 @@ PostgreSQL documentation
  <refsynopsisdiv>
 <synopsis>
 CREATE [ { TEMPORARY | TEMP } ] VARIABLE [ IF NOT EXISTS ] <replaceable class="parameter">name</replaceable> [ AS ] <replaceable class="parameter">data_type</replaceable> ] [ COLLATE <replaceable class="parameter">collation</replaceable> ]
-    [ { ON COMMIT DROP | ON TRANSACTION END RESET } ]
+    [ DEFAULT <replaceable class="parameter">default_expr</replaceable> ] [ { ON COMMIT DROP | ON TRANSACTION END RESET } ]
 </synopsis>
  </refsynopsisdiv>
  <refsect1>
@@ -41,10 +41,10 @@ CREATE [ { TEMPORARY | TEMP } ] VARIABLE [ IF NOT EXISTS ] <replaceable class="p
 
   <para>
    The value of a session variable is local to the current session.  Retrieving
-   a session variable's value returns NULL, unless its value is set to
-   something else in the current session with a <command>LET</command> command.
-   The content of a session variable is not transactional. This is the same as
-   regular variables in procedural languages.
+   a session variable's value returns NULL or a default value, unless its value
+   is set to something else in the current session with a <command>LET</command>
+   command.  The content of a session variable is not transactional.  This is the
+   same as regular variables in procedural languages.
   </para>
 
   <para>
@@ -105,6 +105,17 @@ CREATE [ { TEMPORARY | TEMP } ] VARIABLE [ IF NOT EXISTS ] <replaceable class="p
     </listitem>
    </varlistentry>
 
+   <varlistentry id="sql-createvariable-default">
+    <term><literal>DEFAULT <replaceable>default_expr</replaceable></literal></term>
+    <listitem>
+     <para>
+      The <literal>DEFAULT</literal> clause can be used to assign a default
+      value to a session variable. This expression is evaluated when the session
+      variable is first accessed for reading and had not yet been assigned a value.
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry id="sql-createvariable-on-commit-drop">
     <term><literal>ON COMMIT DROP</literal></term>
     <listitem>
diff --git a/doc/src/sgml/ref/discard.sgml b/doc/src/sgml/ref/discard.sgml
index 61b967f9c9b..6b0cb95034f 100644
--- a/doc/src/sgml/ref/discard.sgml
+++ b/doc/src/sgml/ref/discard.sgml
@@ -71,7 +71,8 @@ DISCARD { ALL | PLANS | SEQUENCES | TEMPORARY | TEMP | VARIABLES }
     <listitem>
      <para>
       Resets the value of all session variables. If a variable
-      is later reused, it is re-initialized to <literal>NULL</literal>.
+      is later reused, it is re-initialized to either
+      <literal>NULL</literal> or its default value.
      </para>
     </listitem>
    </varlistentry>
diff --git a/src/backend/catalog/pg_variable.c b/src/backend/catalog/pg_variable.c
index d17b2756165..781587f9913 100644
--- a/src/backend/catalog/pg_variable.c
+++ b/src/backend/catalog/pg_variable.c
@@ -24,6 +24,9 @@
 #include "catalog/pg_variable.h"
 #include "commands/session_variable.h"
 #include "miscadmin.h"
+#include "parser/parse_coerce.h"
+#include "parser/parse_collate.h"
+#include "parser/parse_expr.h"
 #include "parser/parse_type.h"
 #include "utils/builtins.h"
 #include "utils/lsyscache.h"
@@ -37,6 +40,7 @@ static ObjectAddress create_variable(const char *varName,
 									 Oid varOwner,
 									 Oid varCollation,
 									 bool if_not_exists,
+									 Node *varDefexpr,
 									 VariableXactEndAction varXactEndAction);
 
 
@@ -51,6 +55,7 @@ create_variable(const char *varName,
 				Oid varOwner,
 				Oid varCollation,
 				bool if_not_exists,
+				Node *varDefexpr,
 				VariableXactEndAction varXactEndAction)
 {
 	Acl		   *varacl;
@@ -114,6 +119,11 @@ create_variable(const char *varName,
 	values[Anum_pg_variable_varcollation - 1] = ObjectIdGetDatum(varCollation);
 	values[Anum_pg_variable_varxactendaction - 1] = CharGetDatum(varXactEndAction);
 
+	if (varDefexpr)
+		values[Anum_pg_variable_vardefexpr - 1] = CStringGetTextDatum(nodeToString(varDefexpr));
+	else
+		nulls[Anum_pg_variable_vardefexpr - 1] = true;
+
 	varacl = get_user_default_acl(OBJECT_VARIABLE, varOwner,
 								  varNamespace);
 	if (varacl != NULL)
@@ -150,6 +160,11 @@ create_variable(const char *varName,
 	record_object_address_dependencies(&myself, addrs, DEPENDENCY_NORMAL);
 	free_object_addresses(addrs);
 
+	/* dependency on default expr */
+	if (varDefexpr)
+		recordDependencyOnExpr(&myself, (Node *) varDefexpr,
+							   NIL, DEPENDENCY_NORMAL);
+
 	/* dependency on owner */
 	recordDependencyOnOwner(VariableRelationId, varid, varOwner);
 
@@ -185,6 +200,7 @@ CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt)
 	Oid			collation;
 	Oid			typcollation;
 	ObjectAddress variable;
+	Node	   *cooked_default = NULL;
 
 	/* check consistency of arguments */
 	if (stmt->XactEndAction == VARIABLE_XACTEND_DROP
@@ -226,6 +242,16 @@ CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt)
 						format_type_be(typid)),
 				 parser_errposition(pstate, stmt->collClause->location)));
 
+	if (stmt->defexpr)
+	{
+		cooked_default = transformExpr(pstate, stmt->defexpr,
+									   EXPR_KIND_VARIABLE_DEFAULT);
+
+		cooked_default = coerce_to_specific_type(pstate,
+												 cooked_default, typid, "DEFAULT");
+		assign_expr_collations(pstate, cooked_default);
+	}
+
 	variable = create_variable(stmt->variable->relname,
 							   namespaceid,
 							   typid,
@@ -233,6 +259,7 @@ CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt)
 							   varowner,
 							   collation,
 							   stmt->if_not_exists,
+							   cooked_default,
 							   stmt->XactEndAction);
 
 	elog(DEBUG1, "record for session variable \"%s\" (oid:%d) was created in pg_variable",
diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c
index f69fc9f9b57..9a92bc5e2e3 100644
--- a/src/backend/commands/session_variable.c
+++ b/src/backend/commands/session_variable.c
@@ -22,6 +22,7 @@
 #include "executor/svariableReceiver.h"
 #include "funcapi.h"
 #include "miscadmin.h"
+#include "optimizer/optimizer.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
 #include "storage/proc.h"
@@ -511,11 +512,68 @@ AtEOSubXact_SessionVariables(bool isCommit,
 	}
 }
 
+/*
+ * evaluate an expression
+ */
+static void
+eval_assign_defexpr(SVariable svar, HeapTuple tup)
+{
+	Datum		defexpr_value;
+	bool		isnull;
+
+	Assert(svar);
+	Assert(svar->is_valid);
+	Assert(HeapTupleIsValid(tup));
+
+	defexpr_value = SysCacheGetAttr(VARIABLEOID,
+									tup,
+									Anum_pg_variable_vardefexpr,
+									&isnull);
+
+	if (!isnull)
+	{
+		EState	   *estate;
+		ExprState  *defexprs;
+		Expr	   *defexpr;
+		char	   *defexpr_str;
+		Datum		value;
+		MemoryContext oldcxt;
+
+		estate = CreateExecutorState();
+
+		defexpr_str = TextDatumGetCString(defexpr_value);
+		defexpr = (Expr *) stringToNode(defexpr_str);
+
+		oldcxt = MemoryContextSwitchTo(estate->es_query_cxt);
+
+		defexpr = expression_planner((Expr *) defexpr);
+		defexprs = ExecInitExpr(defexpr, NULL);
+
+		value = ExecEvalExprSwitchContext(defexprs,
+										  GetPerTupleExprContext(estate),
+										  &isnull);
+
+		MemoryContextSwitchTo(oldcxt);
+
+		if (!isnull)
+		{
+			oldcxt = MemoryContextSwitchTo(SVariableMemoryContext);
+
+			svar->value = datumCopy(value, svar->typbyval, svar->typlen);
+			svar->isnull = false;
+
+			MemoryContextSwitchTo(oldcxt);
+		}
+
+		FreeExecutorState(estate);
+	}
+}
+
 /*
  * Initialize attributes cached in "svar"
  */
 static void
-setup_session_variable(SVariable svar, Oid varid)
+setup_session_variable(SVariable svar, Oid varid, bool is_write)
 {
 	HeapTuple	tup;
 	Form_pg_variable varform;
@@ -565,6 +623,9 @@ setup_session_variable(SVariable svar, Oid varid)
 	svar->hashvalue = GetSysCacheHashValue1(VARIABLEOID,
 											ObjectIdGetDatum(varid));
 
+	if (!is_write)
+		eval_assign_defexpr(svar, tup);
+
 	ReleaseSysCache(tup);
 }
 
@@ -594,7 +655,7 @@ set_session_variable(SVariable svar, Datum value, bool isnull)
 	 */
 	if (!svar->is_valid)
 	{
-		setup_session_variable(&locsvar, svar->varid);
+		setup_session_variable(&locsvar, svar->varid, false);
 		_svar = &locsvar;
 	}
 	else
@@ -729,7 +790,25 @@ get_session_variable(Oid varid)
 	 */
 	if (!svar->is_valid)
 	{
-		setup_session_variable(svar, varid);
+		/* in this case we want to use defexp if it is defined */
+		PG_TRY();
+		{
+			/*
+			 * In this case, the setup can execute default expression. When
+			 * the execution of default expression fails, then we need to
+			 * remove entry from session vars.
+			 */
+			setup_session_variable(svar, varid, false);
+		}
+		PG_CATCH();
+		{
+			/* this entry cannot be valid, remove from sessionvars */
+			hash_search(sessionvars, &varid, HASH_REMOVE, NULL);
+
+			/* propagate the error */
+			PG_RE_THROW();
+		}
+		PG_END_TRY();
 
 		elog(DEBUG1, "session variable \"%s.%s\" (oid:%u) has assigned entry in memory (emitted by READ)",
 			 get_namespace_name(get_session_variable_namespace(varid)),
@@ -793,7 +872,7 @@ SetSessionVariable(Oid varid, Datum value, bool isNull)
 
 	if (!found)
 	{
-		setup_session_variable(svar, varid);
+		setup_session_variable(svar, varid, true);
 
 		elog(DEBUG1, "session variable \"%s.%s\" (oid:%u) has assigned entry in memory (emitted by WRITE)",
 			 get_namespace_name(get_session_variable_namespace(svar->varid)),
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index d8de8acf944..b8e33ed19db 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -638,6 +638,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %type <partboundspec> PartitionBoundSpec
 %type <list>		hash_partbound
 %type <defelt>		hash_partbound_elem
+%type <node>		OptSessionVarDefExpr
 
 %type <node>	json_format_clause
 				json_format_clause_opt
@@ -5230,30 +5231,36 @@ create_extension_opt_item:
  *****************************************************************************/
 
 CreateSessionVarStmt:
-			CREATE OptTemp VARIABLE qualified_name opt_as Typename opt_collate_clause XactEndActionOption
+			CREATE OptTemp VARIABLE qualified_name opt_as Typename opt_collate_clause OptSessionVarDefExpr XactEndActionOption
 				{
 					CreateSessionVarStmt *n = makeNode(CreateSessionVarStmt);
 					$4->relpersistence = $2;
 					n->variable = $4;
 					n->typeName = $6;
 					n->collClause = (CollateClause *) $7;
-					n->XactEndAction = $8;
+					n->defexpr = $8;
+					n->XactEndAction = $9;
 					n->if_not_exists = false;
 					$$ = (Node *) n;
 				}
-			| CREATE OptTemp VARIABLE IF_P NOT EXISTS qualified_name opt_as Typename opt_collate_clause XactEndActionOption
+			| CREATE OptTemp VARIABLE IF_P NOT EXISTS qualified_name opt_as Typename opt_collate_clause OptSessionVarDefExpr XactEndActionOption
 				{
 					CreateSessionVarStmt *n = makeNode(CreateSessionVarStmt);
 					$7->relpersistence = $2;
 					n->variable = $7;
 					n->typeName = $9;
 					n->collClause = (CollateClause *) $10;
-					n->XactEndAction = $11;
+					n->defexpr = $11;
+					n->XactEndAction = $12;
 					n->if_not_exists = true;
 					$$ = (Node *) n;
 				}
 		;
 
+OptSessionVarDefExpr: DEFAULT b_expr					{ $$ = $2; }
+			| /* EMPTY */							{ $$ = NULL; }
+		;
+
 /*
  * Temporary session variables can be dropped on successful
  * transaction end like tables.
diff --git a/src/backend/parser/parse_agg.c b/src/backend/parser/parse_agg.c
index a7343001f21..ddfbcf42b74 100644
--- a/src/backend/parser/parse_agg.c
+++ b/src/backend/parser/parse_agg.c
@@ -488,6 +488,7 @@ check_agglevels_and_constraints(ParseState *pstate, Node *expr)
 			break;
 		case EXPR_KIND_COLUMN_DEFAULT:
 		case EXPR_KIND_FUNCTION_DEFAULT:
+		case EXPR_KIND_VARIABLE_DEFAULT:
 
 			if (isAgg)
 				err = _("aggregate functions are not allowed in DEFAULT expressions");
@@ -937,6 +938,7 @@ transformWindowFuncCall(ParseState *pstate, WindowFunc *wfunc,
 			break;
 		case EXPR_KIND_COLUMN_DEFAULT:
 		case EXPR_KIND_FUNCTION_DEFAULT:
+		case EXPR_KIND_VARIABLE_DEFAULT:
 			err = _("window functions are not allowed in DEFAULT expressions");
 			break;
 		case EXPR_KIND_INDEX_EXPRESSION:
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index 3602c3572d7..bcbb1f564af 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -582,6 +582,7 @@ expr_kind_allows_session_variables(ParseExprKind p_expr_kind)
 		case EXPR_KIND_JOIN_USING:
 		case EXPR_KIND_CYCLE_MARK:
 		case EXPR_KIND_ASSIGN_TARGET:
+		case EXPR_KIND_VARIABLE_DEFAULT:
 			result = false;
 			break;
 	}
@@ -667,6 +668,7 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
 		case EXPR_KIND_CYCLE_MARK:
 		case EXPR_KIND_ASSIGN_TARGET:
 		case EXPR_KIND_LET_TARGET:
+		case EXPR_KIND_VARIABLE_DEFAULT:
 			/* okay */
 			break;
 
@@ -2075,6 +2077,7 @@ transformSubLink(ParseState *pstate, SubLink *sublink)
 			break;
 		case EXPR_KIND_COLUMN_DEFAULT:
 		case EXPR_KIND_FUNCTION_DEFAULT:
+		case EXPR_KIND_VARIABLE_DEFAULT:
 			err = _("cannot use subquery in DEFAULT expression");
 			break;
 		case EXPR_KIND_INDEX_EXPRESSION:
@@ -3427,6 +3430,7 @@ ParseExprKindName(ParseExprKind exprKind)
 			return "CHECK";
 		case EXPR_KIND_COLUMN_DEFAULT:
 		case EXPR_KIND_FUNCTION_DEFAULT:
+		case EXPR_KIND_VARIABLE_DEFAULT:
 			return "DEFAULT";
 		case EXPR_KIND_INDEX_EXPRESSION:
 			return "index expression";
diff --git a/src/backend/parser/parse_func.c b/src/backend/parser/parse_func.c
index 9aa4f60768b..fcac694455d 100644
--- a/src/backend/parser/parse_func.c
+++ b/src/backend/parser/parse_func.c
@@ -2620,6 +2620,7 @@ check_srf_call_placement(ParseState *pstate, Node *last_srf, int location)
 			break;
 		case EXPR_KIND_COLUMN_DEFAULT:
 		case EXPR_KIND_FUNCTION_DEFAULT:
+		case EXPR_KIND_VARIABLE_DEFAULT:
 			err = _("set-returning functions are not allowed in DEFAULT expressions");
 			break;
 		case EXPR_KIND_INDEX_EXPRESSION:
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 2ec6a22a266..c138994304a 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -5433,6 +5433,7 @@ getVariables(Archive *fout)
 	int			i_varnamespace;
 	int			i_vartype;
 	int			i_vartypname;
+	int			i_vardefexpr;
 	int			i_varxactendaction;
 	int			i_varowner;
 	int			i_varcollation;
@@ -5458,6 +5459,7 @@ getVariables(Archive *fout)
 					  "            THEN v.varcollation\n"
 					  "            ELSE 0\n"
 					  "       END AS varcollation,\n"
+					  "       pg_catalog.pg_get_expr(v.vardefexpr,0) as vardefexpr,\n"
 					  "       v.varowner, v.varacl,\n"
 					  "       acldefault('V', v.varowner) AS acldefault\n"
 					  "FROM pg_catalog.pg_variable v\n"
@@ -5474,6 +5476,7 @@ getVariables(Archive *fout)
 	i_varnamespace = PQfnumber(res, "varnamespace");
 	i_vartype = PQfnumber(res, "vartype");
 	i_vartypname = PQfnumber(res, "vartypname");
+	i_vardefexpr = PQfnumber(res, "vardefexpr");
 	i_varxactendaction = PQfnumber(res, "varxactendaction");
 	i_varcollation = PQfnumber(res, "varcollation");
 
@@ -5509,6 +5512,11 @@ getVariables(Archive *fout)
 		varinfo[i].dacl.initprivs = NULL;
 		varinfo[i].rolname = getRoleName(PQgetvalue(res, i, i_varowner));
 
+		if (PQgetisnull(res, i, i_vardefexpr))
+			varinfo[i].vardefexpr = NULL;
+		else
+			varinfo[i].vardefexpr = pg_strdup(PQgetvalue(res, i, i_vardefexpr));
+
 		/* do not try to dump ACL if no ACL exists */
 		if (!PQgetisnull(res, i, i_varacl))
 			varinfo[i].dobj.components |= DUMP_COMPONENT_ACL;
@@ -5541,6 +5549,7 @@ dumpVariable(Archive *fout, const VariableInfo *varinfo)
 	PQExpBuffer query;
 	char	   *qualvarname;
 	const char *vartypname;
+	const char *vardefexpr;
 	const char *varxactendaction;
 	Oid			varcollation;
 
@@ -5553,6 +5562,7 @@ dumpVariable(Archive *fout, const VariableInfo *varinfo)
 
 	qualvarname = pg_strdup(fmtQualifiedDumpable(varinfo));
 	vartypname = varinfo->vartypname;
+	vardefexpr = varinfo->vardefexpr;
 	varxactendaction = varinfo->varxactendaction;
 	varcollation = varinfo->varcollation;
 
@@ -5572,6 +5582,10 @@ dumpVariable(Archive *fout, const VariableInfo *varinfo)
 							  fmtQualifiedDumpable(coll));
 	}
 
+	if (vardefexpr)
+		appendPQExpBuffer(query, " DEFAULT %s",
+						  vardefexpr);
+
 	if (strcmp(varxactendaction, "r") == 0)
 		appendPQExpBuffer(query, " ON TRANSACTION END RESET");
 
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 7c0c03749e8..8ed83979ec9 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -709,6 +709,7 @@ typedef struct _VariableInfo
 	DumpableAcl dacl;
 	Oid			vartype;
 	char	   *vartypname;
+	char	   *vardefexpr;
 	char	   *varxactendaction;
 	char	   *varacl;
 	char	   *rvaracl;
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index 419ea39727b..f58ede5334a 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -4005,6 +4005,42 @@ my %tests = (
 		},
 	},
 
+	'CREATE VARIABLE test_variable DEFAULT' => {
+		all_runs     => 1,
+		catch_all    => 'CREATE ... commands',
+		create_order => 61,
+		create_sql   => 'CREATE VARIABLE dump_test.variable3 AS integer DEFAULT 10;',
+		regexp => qr/^
+			\QCREATE VARIABLE dump_test.variable3 AS integer DEFAULT 10;\E/xm,
+		like => {
+			%full_runs,
+			%dump_test_schema_runs,
+			section_pre_data => 1,
+		},
+		unlike => {
+			exclude_dump_test_schema => 1,
+			only_dump_measurement    => 1,
+		},
+	},
+
+	'CREATE VARIABLE test_variable DEFAULT ON TRANSACTION END RESET' => {
+		all_runs     => 1,
+		catch_all    => 'CREATE ... commands',
+		create_order => 61,
+		create_sql   => 'CREATE VARIABLE dump_test.variable4 AS integer DEFAULT 10 ON TRANSACTION END RESET',
+		regexp => qr/^
+			\QCREATE VARIABLE dump_test.variable4 AS integer DEFAULT 10 ON TRANSACTION END RESET;\E/xm,
+		like => {
+			%full_runs,
+			%dump_test_schema_runs,
+			section_pre_data => 1,
+		},
+		unlike => {
+			exclude_dump_test_schema => 1,
+			only_dump_measurement    => 1,
+		},
+	},
+
 	'CREATE VIEW test_view' => {
 		create_order => 61,
 		create_sql => 'CREATE VIEW dump_test.test_view
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index d0f69f8c8e9..dc18da350eb 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -5206,7 +5206,7 @@ listVariables(const char *pattern, bool verbose)
 	PQExpBufferData buf;
 	PGresult   *res;
 	printQueryOpt myopt = pset.popt;
-	static const bool translate_columns[] = {false, false, false, false, false, false, false, false};
+	static const bool translate_columns[] = {false, false, false, false, false, false, false, false, false};
 
 	if (pset.sversion < 180000)
 	{
@@ -5227,6 +5227,7 @@ listVariables(const char *pattern, bool verbose)
 					  "  (SELECT c.collname FROM pg_catalog.pg_collation c, pg_catalog.pg_type bt\n"
 					  "   WHERE c.oid = v.varcollation AND bt.oid = v.vartype AND v.varcollation <> bt.typcollation) as \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(v.varowner) as \"%s\",\n"
+					  "  pg_catalog.pg_get_expr(v.vardefexpr, 0) as \"%s\",\n"
 					  "  CASE v.varxactendaction\n"
 					  "    WHEN 'd' THEN 'ON COMMIT DROP'\n"
 					  "    WHEN 'r' THEN 'ON TRANSACTION END RESET'\n"
@@ -5236,6 +5237,7 @@ listVariables(const char *pattern, bool verbose)
 					  gettext_noop("Type"),
 					  gettext_noop("Collation"),
 					  gettext_noop("Owner"),
+					  gettext_noop("Default"),
 					  gettext_noop("Transactional end action"));
 
 	if (verbose)
diff --git a/src/include/catalog/pg_variable.h b/src/include/catalog/pg_variable.h
index 60291ef7ac2..e8bfeebed11 100644
--- a/src/include/catalog/pg_variable.h
+++ b/src/include/catalog/pg_variable.h
@@ -67,6 +67,9 @@ CATALOG(pg_variable,9222,VariableRelationId)
 	/* access permissions */
 	aclitem		varacl[1] BKI_DEFAULT(_null_);
 
+	/* list of expression trees for variable default (NULL if none) */
+	pg_node_tree vardefexpr BKI_DEFAULT(_null_);
+
 #endif
 } FormData_pg_variable;
 
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 7a519e08429..60404413c49 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -3477,6 +3477,7 @@ typedef struct CreateSessionVarStmt
 	TypeName   *typeName;		/* the type of variable */
 	CollateClause *collClause;
 	bool		if_not_exists;	/* do nothing if it already exists */
+	Node	   *defexpr;		/* default expression */
 	char		XactEndAction;	/* on transaction end action */
 } CreateSessionVarStmt;
 
diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h
index c11fdb3d970..fa566b194d1 100644
--- a/src/include/parser/parse_node.h
+++ b/src/include/parser/parse_node.h
@@ -84,6 +84,7 @@ typedef enum ParseExprKind
 	EXPR_KIND_CYCLE_MARK,		/* cycle mark value */
 	EXPR_KIND_ASSIGN_TARGET,	/* PL/pgSQL assignment target */
 	EXPR_KIND_LET_TARGET,		/* LET target */
+	EXPR_KIND_VARIABLE_DEFAULT, /* default value for session variable */
 } ParseExprKind;
 
 
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index 61077a52b16..bc7b60a97ea 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -5935,20 +5935,20 @@ CREATE ROLE regress_variable_owner;
 SET ROLE TO regress_variable_owner;
 CREATE VARIABLE var1 AS varchar COLLATE "C";
 \dV+ var1
-                                                          List of variables
- Schema | Name |       Type        | Collation |         Owner          | Transactional end action | Access privileges | Description 
---------+------+-------------------+-----------+------------------------+--------------------------+-------------------+-------------
- public | var1 | character varying | C         | regress_variable_owner |                          |                   | 
+                                                               List of variables
+ Schema | Name |       Type        | Collation |         Owner          | Default | Transactional end action | Access privileges | Description 
+--------+------+-------------------+-----------+------------------------+---------+--------------------------+-------------------+-------------
+ public | var1 | character varying | C         | regress_variable_owner |         |                          |                   | 
 (1 row)
 
 GRANT SELECT ON VARIABLE var1 TO PUBLIC;
 COMMENT ON VARIABLE var1 IS 'some description';
 \dV+ var1
-                                                                            List of variables
- Schema | Name |       Type        | Collation |         Owner          | Transactional end action |                Access privileges                 |   Description    
---------+------+-------------------+-----------+------------------------+--------------------------+--------------------------------------------------+------------------
- public | var1 | character varying | C         | regress_variable_owner |                          | regress_variable_owner=rw/regress_variable_owner+| some description
-        |      |                   |           |                        |                          | =r/regress_variable_owner                        | 
+                                                                                 List of variables
+ Schema | Name |       Type        | Collation |         Owner          | Default | Transactional end action |                Access privileges                 |   Description    
+--------+------+-------------------+-----------+------------------------+---------+--------------------------+--------------------------------------------------+------------------
+ public | var1 | character varying | C         | regress_variable_owner |         |                          | regress_variable_owner=rw/regress_variable_owner+| some description
+        |      |                   |           |                        |         |                          | =r/regress_variable_owner                        | 
 (1 row)
 
 DROP VARIABLE var1;
@@ -6416,9 +6416,9 @@ List of schemas
 (0 rows)
 
 \dV "no.such.variable"
-                          List of variables
- Schema | Name | Type | Collation | Owner | Transactional end action 
---------+------+------+-----------+-------+--------------------------
+                               List of variables
+ Schema | Name | Type | Collation | Owner | Default | Transactional end action 
+--------+------+------+-----------+-------+---------+--------------------------
 (0 rows)
 
 -- again, but with dotted schema qualifications.
@@ -6591,9 +6591,9 @@ improper qualified name (too many dotted names): "no.such.schema"."no.such.insta
 \dy "no.such.schema"."no.such.event.trigger"
 improper qualified name (too many dotted names): "no.such.schema"."no.such.event.trigger"
 \dV "no.such.schema"."no.such.variable"
-                          List of variables
- Schema | Name | Type | Collation | Owner | Transactional end action 
---------+------+------+-----------+-------+--------------------------
+                               List of variables
+ Schema | Name | Type | Collation | Owner | Default | Transactional end action 
+--------+------+------+-----------+-------+---------+--------------------------
 (0 rows)
 
 -- again, but with current database and dotted schema qualifications.
@@ -6730,9 +6730,9 @@ List of text search templates
 (0 rows)
 
 \dV regression."no.such.schema"."no.such.variable"
-                          List of variables
- Schema | Name | Type | Collation | Owner | Transactional end action 
---------+------+------+-----------+-------+--------------------------
+                               List of variables
+ Schema | Name | Type | Collation | Owner | Default | Transactional end action 
+--------+------+------+-----------+-------+---------+--------------------------
 (0 rows)
 
 -- again, but with dotted database and dotted schema qualifications.
diff --git a/src/test/regress/expected/session_variables.out b/src/test/regress/expected/session_variables.out
index 20f37171ea2..48855136489 100644
--- a/src/test/regress/expected/session_variables.out
+++ b/src/test/regress/expected/session_variables.out
@@ -45,11 +45,11 @@ SET ROLE TO regress_variable_owner;
 CREATE VARIABLE svartest.var1 AS int;
 SET ROLE TO DEFAULT;
 \dV+ svartest.var1
-                                                                     List of variables
-  Schema  | Name |  Type   | Collation |         Owner          | Transactional end action |                Access privileges                 | Description 
-----------+------+---------+-----------+------------------------+--------------------------+--------------------------------------------------+-------------
- svartest | var1 | integer |           | regress_variable_owner |                          | regress_variable_owner=rw/regress_variable_owner+| 
-          |      |         |           |                        |                          | regress_variable_reader=r/regress_variable_owner | 
+                                                                          List of variables
+  Schema  | Name |  Type   | Collation |         Owner          | Default | Transactional end action |                Access privileges                 | Description 
+----------+------+---------+-----------+------------------------+---------+--------------------------+--------------------------------------------------+-------------
+ svartest | var1 | integer |           | regress_variable_owner |         |                          | regress_variable_owner=rw/regress_variable_owner+| 
+          |      |         |           |                        |         |                          | regress_variable_reader=r/regress_variable_owner | 
 (1 row)
 
 DROP VARIABLE svartest.var1;
@@ -1555,3 +1555,66 @@ SELECT var1 IS NULL;
 (1 row)
 
 DROP VARIABLE var1;
+CREATE OR REPLACE FUNCTION vartest_fx()
+RETURNS int AS $$
+BEGIN
+  RAISE NOTICE 'vartest_fx executed';
+  RETURN 0;
+END;
+$$ LANGUAGE plpgsql;
+CREATE VARIABLE var1 AS int DEFAULT vartest_fx();
+-- vartest_fx should be protected by dep, should fail
+DROP FUNCTION vartest_fx();
+ERROR:  cannot drop function vartest_fx() because other objects depend on it
+DETAIL:  session variable var1 depends on function vartest_fx()
+HINT:  Use DROP ... CASCADE to drop the dependent objects too.
+-- should be ok
+SELECT var1;
+NOTICE:  vartest_fx executed
+ var1 
+------
+    0
+(1 row)
+
+-- the defexpr should be evaluated only once
+SELECT var1;
+ var1 
+------
+    0
+(1 row)
+
+DISCARD VARIABLES;
+-- in this case, the defexpr should not be evaluated
+LET var1 = 100;
+SELECT var1;
+ var1 
+------
+  100
+(1 row)
+
+DISCARD VARIABLES;
+CREATE OR REPLACE FUNCTION vartest_fx()
+RETURNS int AS $$
+BEGIN
+  RAISE EXCEPTION 'vartest_fx is executing';
+  RETURN 0;
+END;
+$$ LANGUAGE plpgsql;
+-- should to fail, but not to crash
+SELECT var1;
+ERROR:  vartest_fx is executing
+CONTEXT:  PL/pgSQL function vartest_fx() line 3 at RAISE
+-- again
+SELECT var1;
+ERROR:  vartest_fx is executing
+CONTEXT:  PL/pgSQL function vartest_fx() line 3 at RAISE
+-- but we can write
+LET var1 = 100;
+SELECT var1;
+ var1 
+------
+  100
+(1 row)
+
+DROP VARIABLE var1;
+DROP FUNCTION vartest_fx();
diff --git a/src/test/regress/sql/session_variables.sql b/src/test/regress/sql/session_variables.sql
index 8d5751f1b2e..b8d56edf46c 100644
--- a/src/test/regress/sql/session_variables.sql
+++ b/src/test/regress/sql/session_variables.sql
@@ -1039,3 +1039,51 @@ ROLLBACK;
 SELECT var1 IS NULL;
 
 DROP VARIABLE var1;
+
+CREATE OR REPLACE FUNCTION vartest_fx()
+RETURNS int AS $$
+BEGIN
+  RAISE NOTICE 'vartest_fx executed';
+  RETURN 0;
+END;
+$$ LANGUAGE plpgsql;
+
+CREATE VARIABLE var1 AS int DEFAULT vartest_fx();
+
+-- vartest_fx should be protected by dep, should fail
+DROP FUNCTION vartest_fx();
+
+-- should be ok
+SELECT var1;
+
+-- the defexpr should be evaluated only once
+SELECT var1;
+
+DISCARD VARIABLES;
+
+-- in this case, the defexpr should not be evaluated
+LET var1 = 100;
+SELECT var1;
+
+DISCARD VARIABLES;
+
+CREATE OR REPLACE FUNCTION vartest_fx()
+RETURNS int AS $$
+BEGIN
+  RAISE EXCEPTION 'vartest_fx is executing';
+  RETURN 0;
+END;
+$$ LANGUAGE plpgsql;
+
+-- should to fail, but not to crash
+SELECT var1;
+
+-- again
+SELECT var1;
+
+-- but we can write
+LET var1 = 100;
+SELECT var1;
+
+DROP VARIABLE var1;
+DROP FUNCTION vartest_fx();
-- 
2.47.0



  [text/x-patch] v20241110-0011-Implementation-ON-TRANSACTION-END-RESET-clause.patch (14.6K, 12-v20241110-0011-Implementation-ON-TRANSACTION-END-RESET-clause.patch)
  download | inline diff:
From 568210968a6b58f7b7f9d3e9bc3b833ed574e673 Mon Sep 17 00:00:00 2001
From: "[email protected]" <[email protected]>
Date: Wed, 20 Mar 2024 08:37:15 +0100
Subject: [PATCH 11/20] Implementation ON TRANSACTION END RESET clause

This is simple patch - just add special flag to session variable memory entry.
The entries with active this flag are removed from sessionvars hash table
at transaction end. The "TRANSACTION END" is synonyms for "COMMIT ROLLBACK"
but the "TRANSACTION END" is more illustrative and less confusing then "COMMIT ROLLBACK".
---
 doc/src/sgml/catalogs.sgml                    |  3 +-
 doc/src/sgml/ref/create_variable.sgml         | 13 ++++-
 src/backend/commands/session_variable.c       | 51 +++++++++++++++++++
 src/backend/parser/gram.y                     |  1 +
 src/bin/pg_dump/pg_dump.c                     | 11 ++++
 src/bin/pg_dump/pg_dump.h                     |  1 +
 src/bin/pg_dump/t/002_pg_dump.pl              | 18 +++++++
 src/bin/psql/describe.c                       |  1 +
 src/include/catalog/pg_variable.h             |  1 +
 .../isolation/expected/session-variable.out   |  4 +-
 .../isolation/specs/session-variable.spec     |  4 +-
 .../regress/expected/session_variables.out    | 34 +++++++++++++
 src/test/regress/sql/session_variables.sql    | 20 ++++++++
 13 files changed, 158 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 68811a7a3b2..76eff813b43 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -9845,7 +9845,8 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       </para>
       <para>
        Action performed at end of transaction:
-       <literal>n</literal> = no action, <literal>d</literal> = drop the variable.
+       <literal>n</literal> = no action, <literal>d</literal> = drop the variable,
+       <literal>r</literal> = reset the variable to its default value.
       </para></entry>
      </row>
 
diff --git a/doc/src/sgml/ref/create_variable.sgml b/doc/src/sgml/ref/create_variable.sgml
index 642346186f0..e1c2940d4ca 100644
--- a/doc/src/sgml/ref/create_variable.sgml
+++ b/doc/src/sgml/ref/create_variable.sgml
@@ -27,7 +27,7 @@ PostgreSQL documentation
  <refsynopsisdiv>
 <synopsis>
 CREATE [ { TEMPORARY | TEMP } ] VARIABLE [ IF NOT EXISTS ] <replaceable class="parameter">name</replaceable> [ AS ] <replaceable class="parameter">data_type</replaceable> ] [ COLLATE <replaceable class="parameter">collation</replaceable> ]
-    [ ON COMMIT DROP ]
+    [ { ON COMMIT DROP | ON TRANSACTION END RESET } ]
 </synopsis>
  </refsynopsisdiv>
  <refsect1>
@@ -117,6 +117,17 @@ CREATE [ { TEMPORARY | TEMP } ] VARIABLE [ IF NOT EXISTS ] <replaceable class="p
     </listitem>
    </varlistentry>
 
+   <varlistentry id="sql-createvariable-on-transaction-end-reset">
+    <term><literal>ON TRANSACTION END RESET</literal></term>
+    <listitem>
+     <para>
+      The <literal>ON TRANSACTION END RESET</literal> clause causes the session
+      variable to be reset to its default value when the transaction is committed
+      or rolled back.
+     </para>
+    </listitem>
+   </varlistentry>
+
   </variablelist>
  </refsect1>
 
diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c
index 3d4e6eee6b2..f69fc9f9b57 100644
--- a/src/backend/commands/session_variable.c
+++ b/src/backend/commands/session_variable.c
@@ -83,6 +83,8 @@ typedef struct SVariableData
 	void	   *domain_check_extra;
 	LocalTransactionId domain_check_extra_lxid;
 
+	bool		reset_at_eox;
+
 	/*
 	 * Top level local transaction id of the last transaction that dropped the
 	 * variable if any.  We need this information to avoid freeing memory for
@@ -110,6 +112,12 @@ static MemoryContext SVariableMemoryContext = NULL;
 /* true after accepted sinval message */
 static bool needs_validation = false;
 
+/*
+ * true, when some used session variable has ON COMMIT DROP
+ * or ON TRANSACTION END RESET clauses
+ */
+static bool has_session_variables_with_reset_at_eox = false;
+
 /*
  * The content of session variables is not removed immediately. When it
  * is possible we do this at the transaction end. But when the transaction failed,
@@ -386,6 +394,32 @@ remove_invalid_session_variables(bool atEOX)
 	}
 }
 
+/*
+ * remove entries marked as "reset_at_eox"
+ */
+static void
+remove_session_variables_with_reset_at_eox(void)
+{
+	HASH_SEQ_STATUS status;
+	SVariable	svar;
+
+	if (!sessionvars)
+		return;
+
+	/* leave quckly, when there are not that variables */
+	if (!has_session_variables_with_reset_at_eox)
+		return;
+
+	hash_seq_init(&status, sessionvars);
+	while ((svar = (SVariable) hash_seq_search(&status)) != NULL)
+	{
+		if (svar->reset_at_eox)
+			hash_search(sessionvars, &svar->varid, HASH_REMOVE, NULL);
+	}
+
+	has_session_variables_with_reset_at_eox = false;
+}
+
 /*
   * Perform ON COMMIT DROP for temporary session variables,
   * and remove all dropped variables from memory.
@@ -393,6 +427,8 @@ remove_invalid_session_variables(bool atEOX)
 void
 AtPreEOXact_SessionVariables(bool isCommit)
 {
+	remove_session_variables_with_reset_at_eox();
+
 	if (isCommit)
 	{
 		if (xact_drop_items)
@@ -504,6 +540,21 @@ setup_session_variable(SVariable svar, Oid varid)
 	svar->domain_check_extra = NULL;
 	svar->domain_check_extra_lxid = InvalidLocalTransactionId;
 
+	/*
+	 * We don't need to explicitly reset variables marked ON COMMIT DROP. It
+	 * can be done by sinval message processing. But this processing can be
+	 * postponed due aborted transaction. On second hand there is not a
+	 * reason, why don't do it at transaction end immediately.
+	 */
+	if (varform->varxactendaction == VARIABLE_XACTEND_RESET ||
+		varform->varxactendaction == VARIABLE_XACTEND_DROP)
+	{
+		svar->reset_at_eox = true;
+		has_session_variables_with_reset_at_eox = true;
+	}
+	else
+		svar->reset_at_eox = false;
+
 	svar->drop_lxid = InvalidTransactionId;
 
 	svar->isnull = true;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 76655aa8aeb..d8de8acf944 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -5259,6 +5259,7 @@ CreateSessionVarStmt:
  * transaction end like tables.
  */
 XactEndActionOption:  ON COMMIT DROP				{ $$ = VARIABLE_XACTEND_DROP; }
+			| ON TRANSACTION END_P RESET			{ $$ = VARIABLE_XACTEND_RESET; }
 			| /*EMPTY*/								{ $$ = VARIABLE_XACTEND_NOOP; }
 		;
 
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index b8d839ece41..2ec6a22a266 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -5433,6 +5433,7 @@ getVariables(Archive *fout)
 	int			i_varnamespace;
 	int			i_vartype;
 	int			i_vartypname;
+	int			i_varxactendaction;
 	int			i_varowner;
 	int			i_varcollation;
 	int			i_varacl;
@@ -5450,6 +5451,7 @@ getVariables(Archive *fout)
 	/* get the variables in current database */
 	appendPQExpBuffer(query,
 					  "SELECT v.tableoid, v.oid, v.varname,\n"
+					  "       v.varxactendaction,\n"
 					  "       v.varnamespace, v.vartype,\n"
 					  "       pg_catalog.format_type(v.vartype, v.vartypmod) as vartypname,\n"
 					  "       CASE WHEN v.varcollation <> t.typcollation "
@@ -5472,6 +5474,7 @@ getVariables(Archive *fout)
 	i_varnamespace = PQfnumber(res, "varnamespace");
 	i_vartype = PQfnumber(res, "vartype");
 	i_vartypname = PQfnumber(res, "vartypname");
+	i_varxactendaction = PQfnumber(res, "varxactendaction");
 	i_varcollation = PQfnumber(res, "varcollation");
 
 	i_varowner = PQfnumber(res, "varowner");
@@ -5495,6 +5498,9 @@ getVariables(Archive *fout)
 
 		varinfo[i].vartype = atooid(PQgetvalue(res, i, i_vartype));
 		varinfo[i].vartypname = pg_strdup(PQgetvalue(res, i, i_vartypname));
+		varinfo[i].varxactendaction =
+			pg_strdup(PQgetvalue(res, i, i_varxactendaction));
+
 		varinfo[i].varcollation = atooid(PQgetvalue(res, i, i_varcollation));
 
 		varinfo[i].dacl.acl = pg_strdup(PQgetvalue(res, i, i_varacl));
@@ -5535,6 +5541,7 @@ dumpVariable(Archive *fout, const VariableInfo *varinfo)
 	PQExpBuffer query;
 	char	   *qualvarname;
 	const char *vartypname;
+	const char *varxactendaction;
 	Oid			varcollation;
 
 	/* skip if not to be dumped */
@@ -5546,6 +5553,7 @@ dumpVariable(Archive *fout, const VariableInfo *varinfo)
 
 	qualvarname = pg_strdup(fmtQualifiedDumpable(varinfo));
 	vartypname = varinfo->vartypname;
+	varxactendaction = varinfo->varxactendaction;
 	varcollation = varinfo->varcollation;
 
 	appendPQExpBuffer(delq, "DROP VARIABLE %s;\n",
@@ -5564,6 +5572,9 @@ dumpVariable(Archive *fout, const VariableInfo *varinfo)
 							  fmtQualifiedDumpable(coll));
 	}
 
+	if (strcmp(varxactendaction, "r") == 0)
+		appendPQExpBuffer(query, " ON TRANSACTION END RESET");
+
 	appendPQExpBuffer(query, ";\n");
 
 	if (varinfo->dobj.dump & DUMP_COMPONENT_DEFINITION)
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index f2f558b2961..7c0c03749e8 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -709,6 +709,7 @@ typedef struct _VariableInfo
 	DumpableAcl dacl;
 	Oid			vartype;
 	char	   *vartypname;
+	char	   *varxactendaction;
 	char	   *varacl;
 	char	   *rvaracl;
 	char	   *initvaracl;
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index dd8c054a6a6..419ea39727b 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -3987,6 +3987,24 @@ my %tests = (
 		},
 	},
 
+	'CREATE VARIABLE test_variable ON TRANSACTION END RESET' => {
+		all_runs     => 1,
+		catch_all    => 'CREATE ... commands',
+		create_order => 61,
+		create_sql   => 'CREATE VARIABLE dump_test.variable2 AS integer ON TRANSACTION END RESET;',
+		regexp => qr/^
+			\QCREATE VARIABLE dump_test.variable2 AS integer ON TRANSACTION END RESET;\E/xm,
+		like => {
+			%full_runs,
+			%dump_test_schema_runs,
+			section_pre_data => 1,
+		},
+		unlike => {
+			exclude_dump_test_schema => 1,
+			only_dump_measurement    => 1,
+		},
+	},
+
 	'CREATE VIEW test_view' => {
 		create_order => 61,
 		create_sql => 'CREATE VIEW dump_test.test_view
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index d02f6416f0e..d0f69f8c8e9 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -5229,6 +5229,7 @@ listVariables(const char *pattern, bool verbose)
 					  "  pg_catalog.pg_get_userbyid(v.varowner) as \"%s\",\n"
 					  "  CASE v.varxactendaction\n"
 					  "    WHEN 'd' THEN 'ON COMMIT DROP'\n"
+					  "    WHEN 'r' THEN 'ON TRANSACTION END RESET'\n"
 					  "  END as \"%s\"\n",
 					  gettext_noop("Schema"),
 					  gettext_noop("Name"),
diff --git a/src/include/catalog/pg_variable.h b/src/include/catalog/pg_variable.h
index 1e6dd98d415..60291ef7ac2 100644
--- a/src/include/catalog/pg_variable.h
+++ b/src/include/catalog/pg_variable.h
@@ -74,6 +74,7 @@ typedef enum VariableXactEndAction
 {
 	VARIABLE_XACTEND_NOOP = 'n',	/* NOOP */
 	VARIABLE_XACTEND_DROP = 'd',	/* ON COMMIT DROP */
+	VARIABLE_XACTEND_RESET = 'r',	/* ON TRANSACTION END RESET */
 }			VariableXactEndAction;
 
 /* ----------------
diff --git a/src/test/isolation/expected/session-variable.out b/src/test/isolation/expected/session-variable.out
index a609797dc5d..e9a254bcd12 100644
--- a/src/test/isolation/expected/session-variable.out
+++ b/src/test/isolation/expected/session-variable.out
@@ -86,11 +86,12 @@ myvar
 
 step sr1: ROLLBACK;
 
-starting permutation: create3 let3 s3 o_c_d create4 let4 drop4 drop3 inval3 discard sc3 state
+starting permutation: create3 let3 s3 o_c_d o_eox_r create4 let4 drop4 drop3 inval3 discard sc3 clean state
 step create3: CREATE VARIABLE myvar3 AS text;
 step let3: LET myvar3 = 'test';
 step s3: BEGIN;
 step o_c_d: CREATE TEMP VARIABLE myvar_o_c_d AS text ON COMMIT DROP;
+step o_eox_r: CREATE VARIABLE myvar_o_eox_r AS text ON TRANSACTION END RESET; LET myvar_o_eox_r = 'test';
 step create4: CREATE VARIABLE myvar4 AS text;
 step let4: LET myvar4 = 'test';
 step drop4: DROP VARIABLE myvar4;
@@ -103,6 +104,7 @@ t
 
 step discard: DISCARD VARIABLES;
 step sc3: COMMIT;
+step clean: DROP VARIABLE myvar_o_eox_r;
 step state: SELECT varname FROM pg_variable;
 varname
 -------
diff --git a/src/test/isolation/specs/session-variable.spec b/src/test/isolation/specs/session-variable.spec
index 45e65d4085d..5d089c8a4ed 100644
--- a/src/test/isolation/specs/session-variable.spec
+++ b/src/test/isolation/specs/session-variable.spec
@@ -25,12 +25,14 @@ session s3
 step s3			{ BEGIN; }
 step let3		{ LET myvar3 = 'test'; }
 step o_c_d		{ CREATE TEMP VARIABLE myvar_o_c_d AS text ON COMMIT DROP; }
+step o_eox_r	{ CREATE VARIABLE myvar_o_eox_r AS text ON TRANSACTION END RESET; LET myvar_o_eox_r = 'test'; }
 step create4	{ CREATE VARIABLE myvar4 AS text; }
 step let4		{ LET myvar4 = 'test'; }
 step drop4		{ DROP VARIABLE myvar4; }
 step inval3		{ SELECT COUNT(*) >= 0 FROM pg_foreign_table; }
 step discard	{ DISCARD VARIABLES; }
 step sc3		{ COMMIT; }
+step clean		{ DROP VARIABLE myvar_o_eox_r; }
 step state		{ SELECT varname FROM pg_variable; }
 
 session s4
@@ -48,4 +50,4 @@ permutation let val dbg drop create dbg val
 # calling the dbg step after the concurrent drop
 permutation let val s1 dbg drop create dbg val sr1
 # test for DISCARD ALL when all internal queues have actions registered
-permutation create3 let3 s3 o_c_d create4 let4 drop4 drop3 inval3 discard sc3 state
+permutation create3 let3 s3 o_c_d o_eox_r create4 let4 drop4 drop3 inval3 discard sc3 clean state
diff --git a/src/test/regress/expected/session_variables.out b/src/test/regress/expected/session_variables.out
index 999397af26b..20f37171ea2 100644
--- a/src/test/regress/expected/session_variables.out
+++ b/src/test/regress/expected/session_variables.out
@@ -1521,3 +1521,37 @@ SELECT count(*) FROM pg_session_variables();
      0
 (1 row)
 
+CREATE VARIABLE var1 AS int ON TRANSACTION END RESET;
+BEGIN;
+  LET var1 = 100;
+  SELECT var1;
+ var1 
+------
+  100
+(1 row)
+
+COMMIT;
+-- should be NULL;
+SELECT var1 IS NULL;
+ ?column? 
+----------
+ t
+(1 row)
+
+BEGIN;
+  LET var1 = 100;
+  SELECT var1;
+ var1 
+------
+  100
+(1 row)
+
+ROLLBACK;
+-- should be NULL
+SELECT var1 IS NULL;
+ ?column? 
+----------
+ t
+(1 row)
+
+DROP VARIABLE var1;
diff --git a/src/test/regress/sql/session_variables.sql b/src/test/regress/sql/session_variables.sql
index 1d2eb3c4281..8d5751f1b2e 100644
--- a/src/test/regress/sql/session_variables.sql
+++ b/src/test/regress/sql/session_variables.sql
@@ -1019,3 +1019,23 @@ COMMIT;
 SELECT count(*) FROM pg_variable WHERE varname = 'var1';
 -- should be zero
 SELECT count(*) FROM pg_session_variables();
+
+CREATE VARIABLE var1 AS int ON TRANSACTION END RESET;
+
+BEGIN;
+  LET var1 = 100;
+  SELECT var1;
+COMMIT;
+
+-- should be NULL;
+SELECT var1 IS NULL;
+
+BEGIN;
+  LET var1 = 100;
+  SELECT var1;
+ROLLBACK;
+
+-- should be NULL
+SELECT var1 IS NULL;
+
+DROP VARIABLE var1;
-- 
2.47.0



  [text/x-patch] v20241110-0010-implementation-of-temporary-session-variables.patch (40.2K, 13-v20241110-0010-implementation-of-temporary-session-variables.patch)
  download | inline diff:
From b80a7a3431014637d58f569bdd8b561b514c97f1 Mon Sep 17 00:00:00 2001
From: "[email protected]" <[email protected]>
Date: Sun, 10 Nov 2024 22:13:22 +0100
Subject: [PATCH 10/20] implementation of temporary session variables

The temporary variables are created inside temp schema and they are dropped by dropping
this schema. For consistency with temp tables, the CREATE TEMP VARIABLE command supports
ON COMMIT DROP clause. The temporary variables with this clause are collected in xact_drop_items
list. This list should be carefully maintained and can contain only valid entries (not
yet dropped). From this reasons now the subcommit and subaborting have to be handled.
---
 doc/src/sgml/catalogs.sgml                    |  10 +
 doc/src/sgml/ddl.sgml                         |   6 +-
 doc/src/sgml/ref/create_variable.sgml         |  15 +-
 src/backend/access/transam/xact.c             |   8 +
 src/backend/catalog/pg_variable.c             |  24 ++-
 src/backend/commands/session_variable.c       | 200 +++++++++++++++++-
 src/backend/commands/view.c                   |   2 +-
 src/backend/parser/analyze.c                  |   4 +-
 src/backend/parser/gram.y                     |  30 ++-
 src/backend/parser/parse_relation.c           |  22 +-
 src/bin/psql/describe.c                       |  10 +-
 src/bin/psql/tab-complete.in.c                |   5 +-
 src/include/catalog/pg_variable.h             |   9 +
 src/include/commands/session_variable.h       |   6 +-
 src/include/nodes/parsenodes.h                |   1 +
 src/include/nodes/primnodes.h                 |   4 +-
 src/include/parser/parse_relation.h           |   2 +-
 .../isolation/expected/session-variable.out   |   3 +-
 .../isolation/specs/session-variable.spec     |   3 +-
 src/test/regress/expected/psql.out            |  36 ++--
 .../regress/expected/session_variables.out    | 130 +++++++++++-
 src/test/regress/sql/session_variables.sql    |  66 ++++++
 src/tools/pgindent/typedefs.list              |   1 +
 23 files changed, 534 insertions(+), 63 deletions(-)

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index aa796876e97..68811a7a3b2 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -9839,6 +9839,16 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>varxactendaction</structfield> <type>char</type>
+      </para>
+      <para>
+       Action performed at end of transaction:
+       <literal>n</literal> = no action, <literal>d</literal> = drop the variable.
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>varcollation</structfield> <type>oid</type>
diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml
index 1f6c88cf556..05c9a0aebcc 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -5343,9 +5343,9 @@ SELECT current_user_id;
     <command>LET</command> command.  Session variables are not transactional:
     any changes made to the value of a session variable in a transaction won't
     be undone if the transaction is rolled back (just like variables in
-    procedural languages).  Session variables themselves are persistent, but
-    their values are neither persistent nor shared (like the content of
-    temporary tables).
+    procedural languages).  Session variables themselves can be persistent
+    or temporary, but their values are neither persistent nor shared (like the
+    content of temporary tables).
    </para>
 
    <para>
diff --git a/doc/src/sgml/ref/create_variable.sgml b/doc/src/sgml/ref/create_variable.sgml
index 05bf67e3411..642346186f0 100644
--- a/doc/src/sgml/ref/create_variable.sgml
+++ b/doc/src/sgml/ref/create_variable.sgml
@@ -26,7 +26,8 @@ PostgreSQL documentation
 
  <refsynopsisdiv>
 <synopsis>
-CREATE VARIABLE [ IF NOT EXISTS ] <replaceable class="parameter">name</replaceable> [ AS ] <replaceable class="parameter">data_type</replaceable> ] [ COLLATE <replaceable class="parameter">collation</replaceable> ]
+CREATE [ { TEMPORARY | TEMP } ] VARIABLE [ IF NOT EXISTS ] <replaceable class="parameter">name</replaceable> [ AS ] <replaceable class="parameter">data_type</replaceable> ] [ COLLATE <replaceable class="parameter">collation</replaceable> ]
+    [ ON COMMIT DROP ]
 </synopsis>
  </refsynopsisdiv>
  <refsect1>
@@ -104,6 +105,18 @@ CREATE VARIABLE [ IF NOT EXISTS ] <replaceable class="parameter">name</replaceab
     </listitem>
    </varlistentry>
 
+   <varlistentry id="sql-createvariable-on-commit-drop">
+    <term><literal>ON COMMIT DROP</literal></term>
+    <listitem>
+     <para>
+      The <literal>ON COMMIT DROP</literal> clause specifies the behaviour of a
+      temporary session variable at transaction commit. With this clause, the
+      session variable is dropped at commit time. The clause is only allowed
+      for temporary variables.
+     </para>
+    </listitem>
+   </varlistentry>
+
   </variablelist>
  </refsect1>
 
diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c
index 47d8a849ef7..01e3d469306 100644
--- a/src/backend/access/transam/xact.c
+++ b/src/backend/access/transam/xact.c
@@ -2317,6 +2317,9 @@ CommitTransaction(void)
 	 */
 	smgrDoPendingSyncs(true, is_parallel_worker);
 
+	/* remove values of dropped session variables from memory */
+	AtPreEOXact_SessionVariables(true);
+
 	/* close large objects before lower-level cleanup */
 	AtEOXact_LargeObject(true);
 
@@ -2913,6 +2916,7 @@ AbortTransaction(void)
 	AtAbort_Portals();
 	smgrDoPendingSyncs(false, is_parallel_worker);
 	AtEOXact_LargeObject(false);
+	AtPreEOXact_SessionVariables(false);
 	AtAbort_Notify();
 	AtEOXact_RelationMap(false, is_parallel_worker);
 	AtAbort_Twophase();
@@ -5191,6 +5195,8 @@ CommitSubTransaction(void)
 	AtEOSubXact_SPI(true, s->subTransactionId);
 	AtEOSubXact_on_commit_actions(true, s->subTransactionId,
 								  s->parent->subTransactionId);
+	AtEOSubXact_SessionVariables(true, s->subTransactionId,
+								 s->parent->subTransactionId);
 	AtEOSubXact_Namespace(true, s->subTransactionId,
 						  s->parent->subTransactionId);
 	AtEOSubXact_Files(true, s->subTransactionId,
@@ -5356,6 +5362,8 @@ AbortSubTransaction(void)
 		AtEOSubXact_SPI(false, s->subTransactionId);
 		AtEOSubXact_on_commit_actions(false, s->subTransactionId,
 									  s->parent->subTransactionId);
+		AtEOSubXact_SessionVariables(false, s->subTransactionId,
+									 s->parent->subTransactionId);
 		AtEOSubXact_Namespace(false, s->subTransactionId,
 							  s->parent->subTransactionId);
 		AtEOSubXact_Files(false, s->subTransactionId,
diff --git a/src/backend/catalog/pg_variable.c b/src/backend/catalog/pg_variable.c
index 0fb8c7f22a4..d17b2756165 100644
--- a/src/backend/catalog/pg_variable.c
+++ b/src/backend/catalog/pg_variable.c
@@ -36,7 +36,8 @@ static ObjectAddress create_variable(const char *varName,
 									 int32 varTypmod,
 									 Oid varOwner,
 									 Oid varCollation,
-									 bool if_not_exists);
+									 bool if_not_exists,
+									 VariableXactEndAction varXactEndAction);
 
 
 /*
@@ -49,7 +50,8 @@ create_variable(const char *varName,
 				int32 varTypmod,
 				Oid varOwner,
 				Oid varCollation,
-				bool if_not_exists)
+				bool if_not_exists,
+				VariableXactEndAction varXactEndAction)
 {
 	Acl		   *varacl;
 	NameData	varname;
@@ -110,6 +112,7 @@ create_variable(const char *varName,
 	values[Anum_pg_variable_vartypmod - 1] = Int32GetDatum(varTypmod);
 	values[Anum_pg_variable_varowner - 1] = ObjectIdGetDatum(varOwner);
 	values[Anum_pg_variable_varcollation - 1] = ObjectIdGetDatum(varCollation);
+	values[Anum_pg_variable_varxactendaction - 1] = CharGetDatum(varXactEndAction);
 
 	varacl = get_user_default_acl(OBJECT_VARIABLE, varOwner,
 								  varNamespace);
@@ -183,6 +186,13 @@ CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt)
 	Oid			typcollation;
 	ObjectAddress variable;
 
+	/* check consistency of arguments */
+	if (stmt->XactEndAction == VARIABLE_XACTEND_DROP
+		&& stmt->variable->relpersistence != RELPERSISTENCE_TEMP)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+				 errmsg("ON COMMIT DROP can only be used on temporary variables")));
+
 	namespaceid =
 		RangeVarGetAndCheckCreationNamespace(stmt->variable, NoLock, NULL);
 
@@ -222,7 +232,8 @@ CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt)
 							   typmod,
 							   varowner,
 							   collation,
-							   stmt->if_not_exists);
+							   stmt->if_not_exists,
+							   stmt->XactEndAction);
 
 	elog(DEBUG1, "record for session variable \"%s\" (oid:%d) was created in pg_variable",
 		 stmt->variable->relname, variable.objectId);
@@ -230,6 +241,8 @@ CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt)
 	/* we want SessionVariableCreatePostprocess to see the catalog changes. */
 	CommandCounterIncrement();
 
+	SessionVariableCreatePostprocess(variable.objectId, stmt->XactEndAction);
+
 	return variable;
 }
 
@@ -242,6 +255,7 @@ DropVariableById(Oid varid)
 {
 	Relation	rel;
 	HeapTuple	tup;
+	char		XactEndAction;
 
 	rel = table_open(VariableRelationId, RowExclusiveLock);
 
@@ -250,6 +264,8 @@ DropVariableById(Oid varid)
 	if (!HeapTupleIsValid(tup))
 		elog(ERROR, "cache lookup failed for variable %u", varid);
 
+	XactEndAction = ((Form_pg_variable) GETSTRUCT(tup))->varxactendaction;
+
 	CatalogTupleDelete(rel, &tup->t_self);
 
 	ReleaseSysCache(tup);
@@ -257,5 +273,5 @@ DropVariableById(Oid varid)
 	table_close(rel, RowExclusiveLock);
 
 	/* do the necessary cleanup if needed in local memory */
-	SessionVariableDropPostprocess(varid);
+	SessionVariableDropPostprocess(varid, XactEndAction);
 }
diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c
index f5d19228e34..3d4e6eee6b2 100644
--- a/src/backend/commands/session_variable.c
+++ b/src/backend/commands/session_variable.c
@@ -16,6 +16,8 @@
 
 #include "access/xact.h"
 #include "catalog/pg_variable.h"
+#include "catalog/dependency.h"
+#include "catalog/namespace.h"
 #include "commands/session_variable.h"
 #include "executor/svariableReceiver.h"
 #include "funcapi.h"
@@ -31,6 +33,19 @@
 #include "utils/snapmgr.h"
 #include "utils/syscache.h"
 
+typedef struct SVariableXActDropItem
+{
+	Oid			varid;			/* varid of session variable */
+
+	/*
+	 * creating_subid is the ID of the creating subxact. If the action was
+	 * unregistered during the current transaction, deleting_subid is the ID
+	 * of the deleting subxact, otherwise InvalidSubTransactionId.
+	 */
+	SubTransactionId creating_subid;
+	SubTransactionId deleting_subid;
+} SVariableXActDropItem;
+
 /*
  * The values of session variables are stored in the backend's private memory
  * in the dedicated memory context SVariableMemoryContext in binary format.
@@ -105,6 +120,12 @@ static bool needs_validation = false;
  */
 static LocalTransactionId validated_lxid = InvalidLocalTransactionId;
 
+/* list holds fields of SVariableXActDropItem type */
+static List *xact_drop_items = NIL;
+
+static void register_session_variable_xact_drop(Oid varid);
+static void unregister_session_variable_xact_drop(Oid varid);
+
 /*
  * Callback function for session variable invalidation.
  */
@@ -141,16 +162,45 @@ pg_variable_cache_callback(Datum arg, int cacheid, uint32 hashvalue)
 	}
 }
 
+/*
+ * Do the necessary work to setup local memory management of a new
+ * variable.
+ *
+ * Caller should already have created the necessary entry in catalog
+ * and made them visible.
+ */
+void
+SessionVariableCreatePostprocess(Oid varid, char XactEndAction)
+{
+	/*
+	 * For temporary variables, we need to create a new end of xact action to
+	 * ensure deletion from catalog.
+	 */
+	if (XactEndAction == VARIABLE_XACTEND_DROP)
+	{
+		Assert(isTempNamespace(get_session_variable_namespace(varid)));
+
+		register_session_variable_xact_drop(varid);
+	}
+}
+
 /*
  * Handle the local memory cleanup for a DROP VARIABLE command.
  *
  * Caller should take care of removing the pg_variable entry first.
  */
 void
-SessionVariableDropPostprocess(Oid varid)
+SessionVariableDropPostprocess(Oid varid, char XactEndAction)
 {
 	Assert(LocalTransactionIdIsValid(MyProc->vxid.lxid));
 
+	if (XactEndAction == VARIABLE_XACTEND_DROP)
+	{
+		Assert(isTempNamespace(get_session_variable_namespace(varid)));
+
+		unregister_session_variable_xact_drop(varid);
+	}
+
 	if (sessionvars)
 	{
 		bool		found;
@@ -171,6 +221,57 @@ SessionVariableDropPostprocess(Oid varid)
 	}
 }
 
+/*
+ * Registration of actions to be executed on session variables at transaction
+ * end time. We want to drop temporary session variables with clause ON COMMIT
+ * DROP.
+ */
+
+/*
+ * Register a session variable xact action.
+ */
+static void
+register_session_variable_xact_drop(Oid varid)
+{
+	SVariableXActDropItem *xact_ai;
+	MemoryContext oldcxt;
+
+	oldcxt = MemoryContextSwitchTo(CacheMemoryContext);
+
+	xact_ai = (SVariableXActDropItem *)
+		palloc(sizeof(SVariableXActDropItem));
+
+	xact_ai->varid = varid;
+
+	xact_ai->creating_subid = GetCurrentSubTransactionId();
+	xact_ai->deleting_subid = InvalidSubTransactionId;
+
+	xact_drop_items = lcons(xact_ai, xact_drop_items);
+
+	MemoryContextSwitchTo(oldcxt);
+}
+
+/*
+ * Unregister an id of a given session variable from drop list. In this
+ * moment, the action is just marked as deleted by setting deleting_subid. The
+ * calling even might be rollbacked, in which case we should not lose this
+ * action.
+ */
+static void
+unregister_session_variable_xact_drop(Oid varid)
+{
+	ListCell   *l;
+
+	foreach(l, xact_drop_items)
+	{
+		SVariableXActDropItem *xact_ai =
+			(SVariableXActDropItem *) lfirst(l);
+
+		if (xact_ai->varid == varid)
+			xact_ai->deleting_subid = GetCurrentSubTransactionId();
+	}
+}
+
 /*
  * Release stored value, free memory
  */
@@ -238,7 +339,7 @@ is_session_variable_valid(SVariable svar)
  * until the end of the transaction (where the variable was dropped).
  */
 static void
-remove_invalid_session_variables(void)
+remove_invalid_session_variables(bool atEOX)
 {
 	HASH_SEQ_STATUS status;
 	SVariable	svar;
@@ -265,7 +366,7 @@ remove_invalid_session_variables(void)
 	{
 		if (!svar->is_valid)
 		{
-			if (svar->drop_lxid == MyProc->vxid.lxid)
+			if (!atEOX && svar->drop_lxid == MyProc->vxid.lxid)
 			{
 				needs_validation = true;
 				continue;
@@ -285,6 +386,95 @@ remove_invalid_session_variables(void)
 	}
 }
 
+/*
+  * Perform ON COMMIT DROP for temporary session variables,
+  * and remove all dropped variables from memory.
+ */
+void
+AtPreEOXact_SessionVariables(bool isCommit)
+{
+	if (isCommit)
+	{
+		if (xact_drop_items)
+		{
+			ListCell   *l;
+
+			foreach(l, xact_drop_items)
+			{
+				SVariableXActDropItem *xact_ai =
+					(SVariableXActDropItem *) lfirst(l);
+
+				/* iterate only over entries that are still pending */
+				if (xact_ai->deleting_subid == InvalidSubTransactionId)
+				{
+					ObjectAddress object;
+
+					object.classId = VariableRelationId;
+					object.objectId = xact_ai->varid;
+					object.objectSubId = 0;
+
+					/*
+					 * Since this is an automatic drop, rather than one
+					 * directly initiated by the user, we pass the
+					 * PERFORM_DELETION_INTERNAL flag.
+					 */
+					elog(DEBUG1, "session variable (oid:%u) will be deleted (forced by ON COMMIT DROP clause)",
+						 xact_ai->varid);
+
+					performDeletion(&object, DROP_CASCADE,
+									PERFORM_DELETION_INTERNAL |
+									PERFORM_DELETION_QUIETLY);
+				}
+			}
+		}
+
+		remove_invalid_session_variables(true);
+	}
+
+	/*
+	 * We have to clean xact_drop_items. All related variables are dropped
+	 * now, or lost inside aborted transaction.
+	 */
+	list_free_deep(xact_drop_items);
+	xact_drop_items = NULL;
+}
+
+/*
+ * Post-subcommit or post-subabort cleanup of xact drop list.
+ *
+ * During subabort, we can immediately remove entries created during this
+ * subtransaction. During subcommit, just transfer entries marked during
+ * this subtransaction as being the parent's responsibility.
+ */
+void
+AtEOSubXact_SessionVariables(bool isCommit,
+							 SubTransactionId mySubid,
+							 SubTransactionId parentSubid)
+{
+	ListCell   *cur_item;
+
+	foreach(cur_item, xact_drop_items)
+	{
+		SVariableXActDropItem *xact_ai =
+			(SVariableXActDropItem *) lfirst(cur_item);
+
+		if (!isCommit && xact_ai->creating_subid == mySubid)
+		{
+			/* cur_item must be removed */
+			xact_drop_items = foreach_delete_current(xact_drop_items, cur_item);
+			pfree(xact_ai);
+		}
+		else
+		{
+			/* cur_item must be preserved */
+			if (xact_ai->creating_subid == mySubid)
+				xact_ai->creating_subid = parentSubid;
+			if (xact_ai->deleting_subid == mySubid)
+				xact_ai->deleting_subid = isCommit ? parentSubid : InvalidSubTransactionId;
+		}
+	}
+}
+
 /*
  * Initialize attributes cached in "svar"
  */
@@ -443,7 +633,7 @@ get_session_variable(Oid varid)
 		validated_lxid != MyProc->vxid.lxid)
 	{
 		/* throw invalid entries, skip entries dropped by this transaction. */
-		remove_invalid_session_variables();
+		remove_invalid_session_variables(false);
 
 		/* don't repeat it in this transaction */
 		validated_lxid = MyProc->vxid.lxid;
@@ -541,7 +731,7 @@ SetSessionVariable(Oid varid, Datum value, bool isNull)
 		validated_lxid != MyProc->vxid.lxid)
 	{
 		/* throw invalid entries, skip entries dropped by this transaction */
-		remove_invalid_session_variables();
+		remove_invalid_session_variables(false);
 
 		/* don't repeat it in this transaction */
 		validated_lxid = MyProc->vxid.lxid;
diff --git a/src/backend/commands/view.c b/src/backend/commands/view.c
index 2bd49eb55e6..c736c5c46f3 100644
--- a/src/backend/commands/view.c
+++ b/src/backend/commands/view.c
@@ -484,7 +484,7 @@ DefineView(ViewStmt *stmt, const char *queryString,
 	 */
 	view = copyObject(stmt->view);	/* don't corrupt original command */
 	if (view->relpersistence == RELPERSISTENCE_PERMANENT
-		&& isQueryUsingTempRelation(viewParse))
+		&& isQueryUsingTempObject(viewParse))
 	{
 		view->relpersistence = RELPERSISTENCE_TEMP;
 		ereport(NOTICE,
diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
index 81b8b0a633d..9dfd435128a 100644
--- a/src/backend/parser/analyze.c
+++ b/src/backend/parser/analyze.c
@@ -3390,10 +3390,10 @@ transformCreateTableAsStmt(ParseState *pstate, CreateTableAsStmt *stmt)
 		 * creation query. It would be hard to refresh data or incrementally
 		 * maintain it if a source disappeared.
 		 */
-		if (isQueryUsingTempRelation(query))
+		if (isQueryUsingTempObject(query))
 			ereport(ERROR,
 					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-					 errmsg("materialized views must not use temporary tables or views")));
+					 errmsg("materialized views must not use temporary tables, views or session variables")));
 
 		/*
 		 * A materialized view would either need to save parameters for use in
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index af511d12aa1..76655aa8aeb 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -466,6 +466,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %type <ival>	 OptTemp
 %type <ival>	 OptNoLog
 %type <oncommit> OnCommitOption
+%type <ival>	 XactEndActionOption
 
 %type <ival>	for_locking_strength
 %type <node>	for_locking_item
@@ -5229,26 +5230,39 @@ create_extension_opt_item:
  *****************************************************************************/
 
 CreateSessionVarStmt:
-			CREATE VARIABLE qualified_name opt_as Typename opt_collate_clause
+			CREATE OptTemp VARIABLE qualified_name opt_as Typename opt_collate_clause XactEndActionOption
 				{
 					CreateSessionVarStmt *n = makeNode(CreateSessionVarStmt);
-					n->variable = $3;
-					n->typeName = $5;
-					n->collClause = (CollateClause *) $6;
+					$4->relpersistence = $2;
+					n->variable = $4;
+					n->typeName = $6;
+					n->collClause = (CollateClause *) $7;
+					n->XactEndAction = $8;
 					n->if_not_exists = false;
 					$$ = (Node *) n;
 				}
-			| CREATE VARIABLE IF_P NOT EXISTS qualified_name opt_as Typename opt_collate_clause
+			| CREATE OptTemp VARIABLE IF_P NOT EXISTS qualified_name opt_as Typename opt_collate_clause XactEndActionOption
 				{
 					CreateSessionVarStmt *n = makeNode(CreateSessionVarStmt);
-					n->variable = $6;
-					n->typeName = $8;
-					n->collClause = (CollateClause *) $9;
+					$7->relpersistence = $2;
+					n->variable = $7;
+					n->typeName = $9;
+					n->collClause = (CollateClause *) $10;
+					n->XactEndAction = $11;
 					n->if_not_exists = true;
 					$$ = (Node *) n;
 				}
 		;
 
+/*
+ * Temporary session variables can be dropped on successful
+ * transaction end like tables.
+ */
+XactEndActionOption:  ON COMMIT DROP				{ $$ = VARIABLE_XACTEND_DROP; }
+			| /*EMPTY*/								{ $$ = VARIABLE_XACTEND_NOOP; }
+		;
+
+
 /*****************************************************************************
  *
  * ALTER EXTENSION name UPDATE [ TO version ]
diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c
index 8075b1b8a1b..95ae2108044 100644
--- a/src/backend/parser/parse_relation.c
+++ b/src/backend/parser/parse_relation.c
@@ -101,7 +101,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref,
 static int	specialAttNum(const char *attname);
 static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte);
 static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte);
-static bool isQueryUsingTempRelation_walker(Node *node, void *context);
+static bool isQueryUsingTempObject_walker(Node *node, void *context);
 
 
 /*
@@ -3896,13 +3896,13 @@ rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte)
  * the query is a temporary relation (table, view, or materialized view).
  */
 bool
-isQueryUsingTempRelation(Query *query)
+isQueryUsingTempObject(Query *query)
 {
-	return isQueryUsingTempRelation_walker((Node *) query, NULL);
+	return isQueryUsingTempObject_walker((Node *) query, NULL);
 }
 
 static bool
-isQueryUsingTempRelation_walker(Node *node, void *context)
+isQueryUsingTempObject_walker(Node *node, void *context)
 {
 	if (node == NULL)
 		return false;
@@ -3928,13 +3928,23 @@ isQueryUsingTempRelation_walker(Node *node, void *context)
 		}
 
 		return query_tree_walker(query,
-								 isQueryUsingTempRelation_walker,
+								 isQueryUsingTempObject_walker,
 								 context,
 								 QTW_IGNORE_JOINALIASES);
 	}
+	else if (IsA(node, Param))
+	{
+		Param	   *p = (Param *) node;
+
+		if (p->paramkind == PARAM_VARIABLE)
+		{
+			if (isAnyTempNamespace(get_session_variable_namespace(p->paramvarid)))
+				return true;
+		}
+	}
 
 	return expression_tree_walker(node,
-								  isQueryUsingTempRelation_walker,
+								  isQueryUsingTempObject_walker,
 								  context);
 }
 
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index c52c8fd147f..d02f6416f0e 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -5206,7 +5206,7 @@ listVariables(const char *pattern, bool verbose)
 	PQExpBufferData buf;
 	PGresult   *res;
 	printQueryOpt myopt = pset.popt;
-	static const bool translate_columns[] = {false, false, false, false, false, false, false};
+	static const bool translate_columns[] = {false, false, false, false, false, false, false, false};
 
 	if (pset.sversion < 180000)
 	{
@@ -5226,12 +5226,16 @@ listVariables(const char *pattern, bool verbose)
 					  "  pg_catalog.format_type(v.vartype, v.vartypmod) as \"%s\",\n"
 					  "  (SELECT c.collname FROM pg_catalog.pg_collation c, pg_catalog.pg_type bt\n"
 					  "   WHERE c.oid = v.varcollation AND bt.oid = v.vartype AND v.varcollation <> bt.typcollation) as \"%s\",\n"
-					  "  pg_catalog.pg_get_userbyid(v.varowner) as \"%s\"\n",
+					  "  pg_catalog.pg_get_userbyid(v.varowner) as \"%s\",\n"
+					  "  CASE v.varxactendaction\n"
+					  "    WHEN 'd' THEN 'ON COMMIT DROP'\n"
+					  "  END as \"%s\"\n",
 					  gettext_noop("Schema"),
 					  gettext_noop("Name"),
 					  gettext_noop("Type"),
 					  gettext_noop("Collation"),
-					  gettext_noop("Owner"));
+					  gettext_noop("Owner"),
+					  gettext_noop("Transactional end action"));
 
 	if (verbose)
 	{
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index df3e2539270..3fb2c4e9248 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -3593,7 +3593,7 @@ match_previous_words(int pattern_id,
 /* CREATE TABLE --- is allowed inside CREATE SCHEMA, so use TailMatches */
 	/* Complete "CREATE TEMP/TEMPORARY" with the possible temp objects */
 	else if (TailMatches("CREATE", "TEMP|TEMPORARY"))
-		COMPLETE_WITH("SEQUENCE", "TABLE", "VIEW");
+		COMPLETE_WITH("SEQUENCE", "TABLE", "VARIABLE", "VIEW");
 	/* Complete "CREATE UNLOGGED" with TABLE or SEQUENCE */
 	else if (TailMatches("CREATE", "UNLOGGED"))
 		COMPLETE_WITH("TABLE", "SEQUENCE");
@@ -3944,7 +3944,8 @@ match_previous_words(int pattern_id,
 	}
 /* CREATE VARIABLE --- is allowed inside CREATE SCHEMA, so use TailMatches */
 	/* Complete CREATE VARIABLE <name> with AS */
-	else if (TailMatches("CREATE", "VARIABLE", MatchAny))
+	else if (TailMatches("CREATE", "VARIABLE", MatchAny) ||
+			 TailMatches("TEMP|TEMPORARY", "VARIABLE", MatchAny))
 		COMPLETE_WITH("AS");
 	else if (TailMatches("VARIABLE", MatchAny, "AS"))
 		/* Complete CREATE VARIABLE <name> with AS types */
diff --git a/src/include/catalog/pg_variable.h b/src/include/catalog/pg_variable.h
index db593cd5bed..1e6dd98d415 100644
--- a/src/include/catalog/pg_variable.h
+++ b/src/include/catalog/pg_variable.h
@@ -55,6 +55,9 @@ CATALOG(pg_variable,9222,VariableRelationId)
 	/* typmod for variable's type */
 	int32		vartypmod BKI_DEFAULT(-1);
 
+	/* action on transaction end */
+	char		varxactendaction BKI_DEFAULT(n);
+
 	/* variable collation */
 	Oid			varcollation BKI_DEFAULT(0) BKI_LOOKUP_OPT(pg_collation);
 
@@ -67,6 +70,12 @@ CATALOG(pg_variable,9222,VariableRelationId)
 #endif
 } FormData_pg_variable;
 
+typedef enum VariableXactEndAction
+{
+	VARIABLE_XACTEND_NOOP = 'n',	/* NOOP */
+	VARIABLE_XACTEND_DROP = 'd',	/* ON COMMIT DROP */
+}			VariableXactEndAction;
+
 /* ----------------
  *		Form_pg_variable corresponds to a pointer to a tuple with
  *		the format of the pg_variable relation.
diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h
index 3dab8ae2a48..dc83296b1ba 100644
--- a/src/include/commands/session_variable.h
+++ b/src/include/commands/session_variable.h
@@ -21,7 +21,11 @@
 #include "tcop/cmdtag.h"
 #include "utils/queryenvironment.h"
 
-extern void SessionVariableDropPostprocess(Oid varid);
+extern void SessionVariableCreatePostprocess(Oid varid, char XactEndAction);
+extern void SessionVariableDropPostprocess(Oid varid, char XactEndAction);
+extern void AtPreEOXact_SessionVariables(bool isCommit);
+extern void AtEOSubXact_SessionVariables(bool isCommit, SubTransactionId mySubid,
+										 SubTransactionId parentSubid);
 
 extern void SetSessionVariable(Oid varid, Datum value, bool isNull);
 extern void SetSessionVariableWithSecurityCheck(Oid varid, Datum value, bool isNull);
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 04ab22bd492..7a519e08429 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -3477,6 +3477,7 @@ typedef struct CreateSessionVarStmt
 	TypeName   *typeName;		/* the type of variable */
 	CollateClause *collClause;
 	bool		if_not_exists;	/* do nothing if it already exists */
+	char		XactEndAction;	/* on transaction end action */
 } CreateSessionVarStmt;
 
 
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index 4b2424d6d34..6174a5562c5 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -51,7 +51,9 @@ typedef struct Alias
 	List	   *colnames;		/* optional list of column aliases */
 } Alias;
 
-/* What to do at commit time for temporary relations */
+/*
+ * What to do at commit time for temporary relations or session variables.
+ */
 typedef enum OnCommitAction
 {
 	ONCOMMIT_NOOP,				/* No ON COMMIT clause (do nothing) */
diff --git a/src/include/parser/parse_relation.h b/src/include/parser/parse_relation.h
index 91fd8e243b5..a8117bcae3f 100644
--- a/src/include/parser/parse_relation.h
+++ b/src/include/parser/parse_relation.h
@@ -126,6 +126,6 @@ extern int	attnameAttNum(Relation rd, const char *attname, bool sysColOK);
 extern const NameData *attnumAttName(Relation rd, int attid);
 extern Oid	attnumTypeId(Relation rd, int attid);
 extern Oid	attnumCollationId(Relation rd, int attid);
-extern bool isQueryUsingTempRelation(Query *query);
+extern bool isQueryUsingTempObject(Query *query);
 
 #endif							/* PARSE_RELATION_H */
diff --git a/src/test/isolation/expected/session-variable.out b/src/test/isolation/expected/session-variable.out
index 0a5579dc7ce..a609797dc5d 100644
--- a/src/test/isolation/expected/session-variable.out
+++ b/src/test/isolation/expected/session-variable.out
@@ -86,10 +86,11 @@ myvar
 
 step sr1: ROLLBACK;
 
-starting permutation: create3 let3 s3 create4 let4 drop4 drop3 inval3 discard sc3 state
+starting permutation: create3 let3 s3 o_c_d create4 let4 drop4 drop3 inval3 discard sc3 state
 step create3: CREATE VARIABLE myvar3 AS text;
 step let3: LET myvar3 = 'test';
 step s3: BEGIN;
+step o_c_d: CREATE TEMP VARIABLE myvar_o_c_d AS text ON COMMIT DROP;
 step create4: CREATE VARIABLE myvar4 AS text;
 step let4: LET myvar4 = 'test';
 step drop4: DROP VARIABLE myvar4;
diff --git a/src/test/isolation/specs/session-variable.spec b/src/test/isolation/specs/session-variable.spec
index c864fee4006..45e65d4085d 100644
--- a/src/test/isolation/specs/session-variable.spec
+++ b/src/test/isolation/specs/session-variable.spec
@@ -24,6 +24,7 @@ step create		{ CREATE VARIABLE myvar AS text; }
 session s3
 step s3			{ BEGIN; }
 step let3		{ LET myvar3 = 'test'; }
+step o_c_d		{ CREATE TEMP VARIABLE myvar_o_c_d AS text ON COMMIT DROP; }
 step create4	{ CREATE VARIABLE myvar4 AS text; }
 step let4		{ LET myvar4 = 'test'; }
 step drop4		{ DROP VARIABLE myvar4; }
@@ -47,4 +48,4 @@ permutation let val dbg drop create dbg val
 # calling the dbg step after the concurrent drop
 permutation let val s1 dbg drop create dbg val sr1
 # test for DISCARD ALL when all internal queues have actions registered
-permutation create3 let3 s3 create4 let4 drop4 drop3 inval3 discard sc3 state
+permutation create3 let3 s3 o_c_d create4 let4 drop4 drop3 inval3 discard sc3 state
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index 88e21194711..61077a52b16 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -5935,20 +5935,20 @@ CREATE ROLE regress_variable_owner;
 SET ROLE TO regress_variable_owner;
 CREATE VARIABLE var1 AS varchar COLLATE "C";
 \dV+ var1
-                                            List of variables
- Schema | Name |       Type        | Collation |         Owner          | Access privileges | Description 
---------+------+-------------------+-----------+------------------------+-------------------+-------------
- public | var1 | character varying | C         | regress_variable_owner |                   | 
+                                                          List of variables
+ Schema | Name |       Type        | Collation |         Owner          | Transactional end action | Access privileges | Description 
+--------+------+-------------------+-----------+------------------------+--------------------------+-------------------+-------------
+ public | var1 | character varying | C         | regress_variable_owner |                          |                   | 
 (1 row)
 
 GRANT SELECT ON VARIABLE var1 TO PUBLIC;
 COMMENT ON VARIABLE var1 IS 'some description';
 \dV+ var1
-                                                              List of variables
- Schema | Name |       Type        | Collation |         Owner          |                Access privileges                 |   Description    
---------+------+-------------------+-----------+------------------------+--------------------------------------------------+------------------
- public | var1 | character varying | C         | regress_variable_owner | regress_variable_owner=rw/regress_variable_owner+| some description
-        |      |                   |           |                        | =r/regress_variable_owner                        | 
+                                                                            List of variables
+ Schema | Name |       Type        | Collation |         Owner          | Transactional end action |                Access privileges                 |   Description    
+--------+------+-------------------+-----------+------------------------+--------------------------+--------------------------------------------------+------------------
+ public | var1 | character varying | C         | regress_variable_owner |                          | regress_variable_owner=rw/regress_variable_owner+| some description
+        |      |                   |           |                        |                          | =r/regress_variable_owner                        | 
 (1 row)
 
 DROP VARIABLE var1;
@@ -6416,9 +6416,9 @@ List of schemas
 (0 rows)
 
 \dV "no.such.variable"
-            List of variables
- Schema | Name | Type | Collation | Owner 
---------+------+------+-----------+-------
+                          List of variables
+ Schema | Name | Type | Collation | Owner | Transactional end action 
+--------+------+------+-----------+-------+--------------------------
 (0 rows)
 
 -- again, but with dotted schema qualifications.
@@ -6591,9 +6591,9 @@ improper qualified name (too many dotted names): "no.such.schema"."no.such.insta
 \dy "no.such.schema"."no.such.event.trigger"
 improper qualified name (too many dotted names): "no.such.schema"."no.such.event.trigger"
 \dV "no.such.schema"."no.such.variable"
-            List of variables
- Schema | Name | Type | Collation | Owner 
---------+------+------+-----------+-------
+                          List of variables
+ Schema | Name | Type | Collation | Owner | Transactional end action 
+--------+------+------+-----------+-------+--------------------------
 (0 rows)
 
 -- again, but with current database and dotted schema qualifications.
@@ -6730,9 +6730,9 @@ List of text search templates
 (0 rows)
 
 \dV regression."no.such.schema"."no.such.variable"
-            List of variables
- Schema | Name | Type | Collation | Owner 
---------+------+------+-----------+-------
+                          List of variables
+ Schema | Name | Type | Collation | Owner | Transactional end action 
+--------+------+------+-----------+-------+--------------------------
 (0 rows)
 
 -- again, but with dotted database and dotted schema qualifications.
diff --git a/src/test/regress/expected/session_variables.out b/src/test/regress/expected/session_variables.out
index 1e28b9461fd..999397af26b 100644
--- a/src/test/regress/expected/session_variables.out
+++ b/src/test/regress/expected/session_variables.out
@@ -45,11 +45,11 @@ SET ROLE TO regress_variable_owner;
 CREATE VARIABLE svartest.var1 AS int;
 SET ROLE TO DEFAULT;
 \dV+ svartest.var1
-                                                        List of variables
-  Schema  | Name |  Type   | Collation |         Owner          |                Access privileges                 | Description 
-----------+------+---------+-----------+------------------------+--------------------------------------------------+-------------
- svartest | var1 | integer |           | regress_variable_owner | regress_variable_owner=rw/regress_variable_owner+| 
-          |      |         |           |                        | regress_variable_reader=r/regress_variable_owner | 
+                                                                     List of variables
+  Schema  | Name |  Type   | Collation |         Owner          | Transactional end action |                Access privileges                 | Description 
+----------+------+---------+-----------+------------------------+--------------------------+--------------------------------------------------+-------------
+ svartest | var1 | integer |           | regress_variable_owner |                          | regress_variable_owner=rw/regress_variable_owner+| 
+          |      |         |           |                        |                          | regress_variable_reader=r/regress_variable_owner | 
 (1 row)
 
 DROP VARIABLE svartest.var1;
@@ -1401,3 +1401,123 @@ SELECT var1;
 
 DEALLOCATE p1;
 DROP VARIABLE var1;
+-- temporary variables
+CREATE TEMP VARIABLE var1 AS int;
+-- this view should be temporary
+CREATE VIEW var_test_view AS SELECT var1;
+NOTICE:  view "var_test_view" will be a temporary view
+DROP VARIABLE var1 CASCADE;
+NOTICE:  drop cascades to view var_test_view
+BEGIN;
+  CREATE TEMP VARIABLE var1 AS int ON COMMIT DROP;
+  LET var1 = 100;
+  SELECT var1;
+ var1 
+------
+  100
+(1 row)
+
+COMMIT;
+-- should be zero
+SELECT count(*) FROM pg_variable WHERE varname = 'var1';
+ count 
+-------
+     0
+(1 row)
+
+-- should be zero
+SELECT count(*) FROM pg_session_variables();
+ count 
+-------
+     0
+(1 row)
+
+BEGIN;
+  CREATE TEMP VARIABLE var1 AS int ON COMMIT DROP;
+  LET var1 = 100;
+  SELECT var1;
+ var1 
+------
+  100
+(1 row)
+
+ROLLBACK;
+-- should be zero
+SELECT count(*) FROM pg_variable WHERE varname = 'var1';
+ count 
+-------
+     0
+(1 row)
+
+-- should be zero
+SELECT count(*) FROM pg_session_variables();
+ count 
+-------
+     0
+(1 row)
+
+BEGIN;
+  CREATE TEMP VARIABLE var1 AS int ON COMMIT DROP;
+  LET var1 = 100;
+  DROP VARIABLE var1;
+COMMIT;
+-- should be zero
+SELECT count(*) FROM pg_variable WHERE varname = 'var1';
+ count 
+-------
+     0
+(1 row)
+
+-- should be zero
+SELECT count(*) FROM pg_session_variables();
+ count 
+-------
+     0
+(1 row)
+
+BEGIN;
+  CREATE TEMP VARIABLE var1 AS int ON COMMIT DROP;
+  LET var1 = 100;
+  DROP VARIABLE var1;
+ROLLBACK;
+-- should be zero
+SELECT count(*) FROM pg_variable WHERE varname = 'var1';
+ count 
+-------
+     0
+(1 row)
+
+-- should be zero
+SELECT count(*) FROM pg_session_variables();
+ count 
+-------
+     0
+(1 row)
+
+BEGIN;
+  CREATE TEMP VARIABLE var1 AS int ON COMMIT DROP;
+  LET var1 = 100;
+  SAVEPOINT s1;
+  DROP VARIABLE var1;
+  ROLLBACK TO s1;
+  SELECT var1;
+ var1 
+------
+  100
+(1 row)
+
+COMMIT;
+-- should be zero
+SELECT count(*) FROM pg_variable WHERE varname = 'var1';
+ count 
+-------
+     0
+(1 row)
+
+-- should be zero
+SELECT count(*) FROM pg_session_variables();
+ count 
+-------
+     0
+(1 row)
+
diff --git a/src/test/regress/sql/session_variables.sql b/src/test/regress/sql/session_variables.sql
index 1c39f70883c..1d2eb3c4281 100644
--- a/src/test/regress/sql/session_variables.sql
+++ b/src/test/regress/sql/session_variables.sql
@@ -827,6 +827,7 @@ BEGIN;
   DROP VARIABLE var1;
   SELECT var2;
 ROLLBACK;
+
 -- should be ok
 SELECT var1;
 
@@ -953,3 +954,68 @@ SELECT var1;
 
 DEALLOCATE p1;
 DROP VARIABLE var1;
+
+-- temporary variables
+CREATE TEMP VARIABLE var1 AS int;
+-- this view should be temporary
+CREATE VIEW var_test_view AS SELECT var1;
+
+DROP VARIABLE var1 CASCADE;
+
+BEGIN;
+  CREATE TEMP VARIABLE var1 AS int ON COMMIT DROP;
+  LET var1 = 100;
+  SELECT var1;
+COMMIT;
+
+-- should be zero
+SELECT count(*) FROM pg_variable WHERE varname = 'var1';
+-- should be zero
+SELECT count(*) FROM pg_session_variables();
+
+BEGIN;
+  CREATE TEMP VARIABLE var1 AS int ON COMMIT DROP;
+  LET var1 = 100;
+  SELECT var1;
+ROLLBACK;
+
+-- should be zero
+SELECT count(*) FROM pg_variable WHERE varname = 'var1';
+-- should be zero
+SELECT count(*) FROM pg_session_variables();
+
+BEGIN;
+  CREATE TEMP VARIABLE var1 AS int ON COMMIT DROP;
+  LET var1 = 100;
+  DROP VARIABLE var1;
+COMMIT;
+
+-- should be zero
+SELECT count(*) FROM pg_variable WHERE varname = 'var1';
+-- should be zero
+SELECT count(*) FROM pg_session_variables();
+
+BEGIN;
+  CREATE TEMP VARIABLE var1 AS int ON COMMIT DROP;
+  LET var1 = 100;
+  DROP VARIABLE var1;
+ROLLBACK;
+
+-- should be zero
+SELECT count(*) FROM pg_variable WHERE varname = 'var1';
+-- should be zero
+SELECT count(*) FROM pg_session_variables();
+
+BEGIN;
+  CREATE TEMP VARIABLE var1 AS int ON COMMIT DROP;
+  LET var1 = 100;
+  SAVEPOINT s1;
+  DROP VARIABLE var1;
+  ROLLBACK TO s1;
+  SELECT var1;
+COMMIT;
+
+-- should be zero
+SELECT count(*) FROM pg_variable WHERE varname = 'var1';
+-- should be zero
+SELECT count(*) FROM pg_session_variables();
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index bbfaab57083..27e352fa341 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2796,6 +2796,7 @@ SupportRequestWFuncMonotonic
 SVariable
 SVariableData
 SVariableState
+SVariableXActDropItem
 Syn
 SyncOps
 SyncRepConfigData
-- 
2.47.0



  [text/x-patch] v20241110-0009-PREPARE-LET-support.patch (7.4K, 14-v20241110-0009-PREPARE-LET-support.patch)
  download | inline diff:
From 055be690e79e71417b821f3d4280e7ad46e3a2a9 Mon Sep 17 00:00:00 2001
From: "[email protected]" <[email protected]>
Date: Fri, 19 Jan 2024 09:44:58 +0100
Subject: [PATCH 09/20] PREPARE LET support

In current code base it requires just small changes in parser. It is nice
to have to have possibility to use prepared LET statements mainly due reduction
of security risks (SQL injection), and implementation is almost cheap.

Explicit preparing is not necessity for support of LET statements in PL, because
PL uses SPI for query execution, but it is nice to have this possibility (completenees, ...)
---
 doc/src/sgml/ref/prepare.sgml                 |  4 +-
 src/backend/parser/gram.y                     |  3 +-
 src/backend/parser/parse_cte.c                |  7 ++
 src/bin/psql/tab-complete.in.c                |  4 +-
 .../regress/expected/session_variables.out    | 81 ++++++++++++++++++-
 src/test/regress/sql/session_variables.sql    | 57 +++++++++++++
 6 files changed, 149 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/ref/prepare.sgml b/doc/src/sgml/ref/prepare.sgml
index 8ee9439f611..45d72b1d52b 100644
--- a/doc/src/sgml/ref/prepare.sgml
+++ b/doc/src/sgml/ref/prepare.sgml
@@ -116,8 +116,8 @@ PREPARE <replaceable class="parameter">name</replaceable> [ ( <replaceable class
     <listitem>
      <para>
       Any <command>SELECT</command>, <command>INSERT</command>, <command>UPDATE</command>,
-      <command>DELETE</command>, <command>MERGE</command>, or <command>VALUES</command>
-      statement.
+      <command>DELETE</command>, <command>MERGE</command>, <command>VALUES</command>,
+      or <command>LET</command> statement.
      </para>
     </listitem>
    </varlistentry>
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index def7dde2330..af511d12aa1 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -12161,7 +12161,8 @@ PreparableStmt:
 			| InsertStmt
 			| UpdateStmt
 			| DeleteStmt
-			| MergeStmt						/* by default all are $$=$1 */
+			| MergeStmt
+			| LetStmt						/* by default all are $$=$1 */
 		;
 
 /*****************************************************************************
diff --git a/src/backend/parser/parse_cte.c b/src/backend/parser/parse_cte.c
index de9ae9b4834..b4b9b5b61c8 100644
--- a/src/backend/parser/parse_cte.c
+++ b/src/backend/parser/parse_cte.c
@@ -126,6 +126,13 @@ transformWithClause(ParseState *pstate, WithClause *withClause)
 		CommonTableExpr *cte = (CommonTableExpr *) lfirst(lc);
 		ListCell   *rest;
 
+		/* LET is allowed by parser, but not supported. Reject for now */
+		if (IsA(cte->ctequery, LetStmt))
+			ereport(ERROR,
+					errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					errmsg("LET not supported in WITH query"),
+					parser_errposition(pstate, cte->location));
+
 		for_each_cell(rest, withClause->ctes, lnext(withClause->ctes, lc))
 		{
 			CommonTableExpr *cte2 = (CommonTableExpr *) lfirst(rest);
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 3ef4776d98a..df3e2539270 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -4629,9 +4629,9 @@ match_previous_words(int pattern_id,
 	else if (TailMatches("VALUES") && !TailMatches("DEFAULT", "VALUES"))
 		COMPLETE_WITH("(");
 
-/* LET */
+/* LET, EXPLAIN LET, PREPARE LET */
 	/* If prev. word is LET suggest a list of variables */
-	else if (Matches("LET"))
+	else if (TailMatches("LET"))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables);
 	/* Complete LET <variable> with "=" */
 	else if (TailMatches("LET", MatchAny))
diff --git a/src/test/regress/expected/session_variables.out b/src/test/regress/expected/session_variables.out
index aa8ce67a543..1e28b9461fd 100644
--- a/src/test/regress/expected/session_variables.out
+++ b/src/test/regress/expected/session_variables.out
@@ -439,9 +439,9 @@ DROP VARIABLE var1, var2;
 -- the LET statement should be disallowed in CTE
 CREATE VARIABLE var1 AS int;
 WITH x AS (LET var1 = 100) SELECT * FROM x;
-ERROR:  syntax error at or near "LET"
+ERROR:  LET not supported in WITH query
 LINE 1: WITH x AS (LET var1 = 100) SELECT * FROM x;
-                   ^
+             ^
 -- should be ok
 LET var1 = generate_series(1, 1);
 -- should fail
@@ -1322,5 +1322,82 @@ SELECT var1;
    10
 (1 row)
 
+SET plan_cache_mode TO force_generic_plan;
+PREPARE p1 AS LET var1 = (SELECT count(*) FROM var_tab_test_table);
+LET var1 = NULL;
+EXPLAIN (COSTS OFF) EXECUTE p1;
+                  QUERY PLAN                  
+----------------------------------------------
+ SET SESSION VARIABLE
+ Result
+   InitPlan 1
+     ->  Aggregate
+           ->  Seq Scan on var_tab_test_table
+(5 rows)
+
+-- should be NULL
+SELECT var1;
+ var1 
+------
+     
+(1 row)
+
+EXPLAIN (COSTS OFF, TIMING OFF, ANALYZE, SUMMARY OFF) EXECUTE p1;
+                              QUERY PLAN                               
+-----------------------------------------------------------------------
+ SET SESSION VARIABLE
+ Result (actual rows=1 loops=1)
+   InitPlan 1
+     ->  Aggregate (actual rows=1 loops=1)
+           ->  Seq Scan on var_tab_test_table (actual rows=10 loops=1)
+(5 rows)
+
+-- should be 10
+SELECT var1;
+ var1 
+------
+   10
+(1 row)
+
+SET plan_cache_mode TO DEFAULT;
+DEALLOCATE p1;
 DROP VARIABLE var1;
 DROP TABLE var_tab_test_table;
+CREATE VARIABLE var1 numeric;
+SET plan_cache_mode TO force_generic_plan;
+PREPARE p1(numeric) AS LET var1 = $1;
+PREPARE p2 AS SELECT var1;
+EXECUTE p1(pi() + 100);
+EXECUTE p2;
+      var1       
+-----------------
+ 103.14159265359
+(1 row)
+
+-- prepared plan cache invalidation test
+DROP VARIABLE var1;
+CREATE VARIABLE var1 numeric;
+-- should be NULL
+EXECUTE p2;
+ var1 
+------
+     
+(1 row)
+
+DEALLOCATE p1;
+DEALLOCATE p2;
+DROP VARIABLE var1;
+SET plan_cache_mode TO force_generic_plan;
+CREATE VARIABLE var1 numeric[];
+PREPARE p1(int, numeric) AS LET var1[$1] = $2;
+LET var1 = '{}'::numeric[];
+EXECUTE p1(1, 10.2);
+EXECUTE p1(2, 10.3);
+SELECT var1;
+    var1     
+-------------
+ {10.2,10.3}
+(1 row)
+
+DEALLOCATE p1;
+DROP VARIABLE var1;
diff --git a/src/test/regress/sql/session_variables.sql b/src/test/regress/sql/session_variables.sql
index 046aa5a5d22..1c39f70883c 100644
--- a/src/test/regress/sql/session_variables.sql
+++ b/src/test/regress/sql/session_variables.sql
@@ -894,5 +894,62 @@ EXPLAIN (COSTS OFF, TIMING OFF, ANALYZE, SUMMARY OFF) LET var1 = (SELECT count(*
 -- should be 10
 SELECT var1;
 
+SET plan_cache_mode TO force_generic_plan;
+
+PREPARE p1 AS LET var1 = (SELECT count(*) FROM var_tab_test_table);
+
+LET var1 = NULL;
+
+EXPLAIN (COSTS OFF) EXECUTE p1;
+
+-- should be NULL
+SELECT var1;
+
+EXPLAIN (COSTS OFF, TIMING OFF, ANALYZE, SUMMARY OFF) EXECUTE p1;
+
+-- should be 10
+SELECT var1;
+
+SET plan_cache_mode TO DEFAULT;
+
+DEALLOCATE p1;
+
 DROP VARIABLE var1;
 DROP TABLE var_tab_test_table;
+
+CREATE VARIABLE var1 numeric;
+
+SET plan_cache_mode TO force_generic_plan;
+
+PREPARE p1(numeric) AS LET var1 = $1;
+PREPARE p2 AS SELECT var1;
+
+EXECUTE p1(pi() + 100);
+EXECUTE p2;
+
+-- prepared plan cache invalidation test
+DROP VARIABLE var1;
+CREATE VARIABLE var1 numeric;
+
+-- should be NULL
+EXECUTE p2;
+
+DEALLOCATE p1;
+DEALLOCATE p2;
+
+DROP VARIABLE var1;
+
+SET plan_cache_mode TO force_generic_plan;
+
+CREATE VARIABLE var1 numeric[];
+
+PREPARE p1(int, numeric) AS LET var1[$1] = $2;
+
+LET var1 = '{}'::numeric[];
+EXECUTE p1(1, 10.2);
+EXECUTE p1(2, 10.3);
+
+SELECT var1;
+
+DEALLOCATE p1;
+DROP VARIABLE var1;
-- 
2.47.0



  [text/x-patch] v20241110-0008-EXPLAIN-LET-support.patch (8.2K, 15-v20241110-0008-EXPLAIN-LET-support.patch)
  download | inline diff:
From 3ccdf5f9d9d4fdb99e4c55858ed737b4f83e171c Mon Sep 17 00:00:00 2001
From: "[email protected]" <[email protected]>
Date: Tue, 21 May 2024 18:28:07 +0200
Subject: [PATCH 08/20] EXPLAIN LET support

Enhancing ExplainOnePlan is necessary to be EXPLAIN ANALYZE LET fully workable.
In this case we want to be result of query or expression written to target variable.
---
 doc/src/sgml/ref/explain.sgml                 |  3 +-
 src/backend/commands/explain.c                | 31 ++++++++++++--
 src/backend/commands/prepare.c                |  5 ++-
 src/backend/parser/gram.y                     |  3 +-
 src/include/commands/explain.h                |  3 +-
 .../regress/expected/session_variables.out    | 40 +++++++++++++++++++
 src/test/regress/sql/session_variables.sql    | 21 ++++++++++
 7 files changed, 97 insertions(+), 9 deletions(-)

diff --git a/doc/src/sgml/ref/explain.sgml b/doc/src/sgml/ref/explain.sgml
index db9d3a8549a..caccc70658c 100644
--- a/doc/src/sgml/ref/explain.sgml
+++ b/doc/src/sgml/ref/explain.sgml
@@ -98,7 +98,8 @@ EXPLAIN [ ( <replaceable class="parameter">option</replaceable> [, ...] ) ] <rep
     <command>INSERT</command>, <command>UPDATE</command>,
     <command>DELETE</command>, <command>MERGE</command>,
     <command>CREATE TABLE AS</command>,
-    or <command>EXECUTE</command> statement
+    <command>EXECUTE</command>,
+    or <command>LET</command> statement
     without letting the command affect your data, use this approach:
 <programlisting>
 BEGIN;
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 7c0fd63b2f0..d41516454f5 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -18,6 +18,7 @@
 #include "commands/createas.h"
 #include "commands/defrem.h"
 #include "commands/prepare.h"
+#include "executor/svariableReceiver.h"
 #include "foreign/fdwapi.h"
 #include "jit/jit.h"
 #include "libpq/pqformat.h"
@@ -512,8 +513,9 @@ standard_ExplainOneQuery(Query *query, int cursorOptions,
 	}
 
 	/* run it (if needed) and produce output */
-	ExplainOnePlan(plan, into, es, queryString, params, queryEnv,
-				   &planduration, (es->buffers ? &bufusage : NULL),
+	ExplainOnePlan(plan, into, query->resultVariable, es, queryString,
+				   params, queryEnv, &planduration,
+				   (es->buffers ? &bufusage : NULL),
 				   es->memory ? &mem_counters : NULL);
 }
 
@@ -611,6 +613,25 @@ ExplainOneUtility(Node *utilityStmt, IntoClause *into, ExplainState *es,
 		else
 			ExplainDummyGroup("Notify", NULL, es);
 	}
+	else if (IsA(utilityStmt, LetStmt))
+	{
+		LetStmt    *letstmt = (LetStmt *) utilityStmt;
+		List	   *rewritten;
+		Query	   *query;
+
+		if (es->format == EXPLAIN_FORMAT_TEXT)
+			appendStringInfoString(es->str, "SET SESSION VARIABLE\n");
+		else
+			ExplainDummyGroup("Set Session Variable", NULL, es);
+
+		rewritten = QueryRewrite(castNode(Query, copyObject(letstmt->query)));
+
+		Assert(list_length(rewritten) == 1);
+		query = linitial_node(Query, rewritten);
+		ExplainOneQuery(query,
+						CURSOR_OPT_PARALLEL_OK, NULL, es,
+						pstate, params);
+	}
 	else
 	{
 		if (es->format == EXPLAIN_FORMAT_TEXT)
@@ -634,8 +655,8 @@ ExplainOneUtility(Node *utilityStmt, IntoClause *into, ExplainState *es,
  * to call it.
  */
 void
-ExplainOnePlan(PlannedStmt *plannedstmt, IntoClause *into, ExplainState *es,
-			   const char *queryString, ParamListInfo params,
+ExplainOnePlan(PlannedStmt *plannedstmt, IntoClause *into, Oid targetvar,
+			   ExplainState *es, const char *queryString, ParamListInfo params,
 			   QueryEnvironment *queryEnv, const instr_time *planduration,
 			   const BufferUsage *bufusage,
 			   const MemoryContextCounters *mem_counters)
@@ -684,6 +705,8 @@ ExplainOnePlan(PlannedStmt *plannedstmt, IntoClause *into, ExplainState *es,
 	 */
 	if (into)
 		dest = CreateIntoRelDestReceiver(into);
+	else if (OidIsValid(targetvar))
+		dest = CreateVariableDestReceiver(targetvar);
 	else if (es->serialize != EXPLAIN_SERIALIZE_NONE)
 		dest = CreateExplainSerializeDestReceiver(es);
 	else
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 455a03cce63..3735b5554e0 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -662,8 +662,9 @@ ExplainExecuteQuery(ExecuteStmt *execstmt, IntoClause *into, ExplainState *es,
 		PlannedStmt *pstmt = lfirst_node(PlannedStmt, p);
 
 		if (pstmt->commandType != CMD_UTILITY)
-			ExplainOnePlan(pstmt, into, es, query_string, paramLI, pstate->p_queryEnv,
-						   &planduration, (es->buffers ? &bufusage : NULL),
+			ExplainOnePlan(pstmt, into, InvalidOid, es, query_string, paramLI,
+						   pstate->p_queryEnv, &planduration,
+						   (es->buffers ? &bufusage : NULL),
 						   es->memory ? &mem_counters : NULL);
 		else
 			ExplainOneUtility(pstmt->utilityStmt, into, es, pstate, paramLI);
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index aad47305f20..def7dde2330 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -12130,7 +12130,8 @@ ExplainableStmt:
 			| CreateAsStmt
 			| CreateMatViewStmt
 			| RefreshMatViewStmt
-			| ExecuteStmt					/* by default all are $$=$1 */
+			| ExecuteStmt
+			| LetStmt						/* by default all are $$=$1 */
 		;
 
 /*****************************************************************************
diff --git a/src/include/commands/explain.h b/src/include/commands/explain.h
index aa5872bc154..f5b08857ae8 100644
--- a/src/include/commands/explain.h
+++ b/src/include/commands/explain.h
@@ -103,7 +103,8 @@ extern void ExplainOneUtility(Node *utilityStmt, IntoClause *into,
 							  ExplainState *es, ParseState *pstate,
 							  ParamListInfo params);
 
-extern void ExplainOnePlan(PlannedStmt *plannedstmt, IntoClause *into,
+extern void ExplainOnePlan(PlannedStmt *plannedstmt,
+						   IntoClause *into, Oid targetvar,
 						   ExplainState *es, const char *queryString,
 						   ParamListInfo params, QueryEnvironment *queryEnv,
 						   const instr_time *planduration,
diff --git a/src/test/regress/expected/session_variables.out b/src/test/regress/expected/session_variables.out
index 582d0b8d034..aa8ce67a543 100644
--- a/src/test/regress/expected/session_variables.out
+++ b/src/test/regress/expected/session_variables.out
@@ -1284,3 +1284,43 @@ SET session_variables_ambiguity_warning TO off;
 DROP TABLE public.xxtab;
 DROP SCHEMA xxtab CASCADE;
 NOTICE:  drop cascades to session variable xxtab.avar
+CREATE VARIABLE var1 bigint;
+CREATE TABLE var_tab_test_table(a int);
+INSERT INTO var_tab_test_table SELECT * FROM generate_series(1,10);
+VACUUM ANALYZE var_tab_test_table;
+EXPLAIN (COSTS OFF) LET var1 = (SELECT count(*) FROM var_tab_test_table);
+                  QUERY PLAN                  
+----------------------------------------------
+ SET SESSION VARIABLE
+ Result
+   InitPlan 1
+     ->  Aggregate
+           ->  Seq Scan on var_tab_test_table
+(5 rows)
+
+-- should be NULL
+SELECT var1;
+ var1 
+------
+     
+(1 row)
+
+EXPLAIN (COSTS OFF, TIMING OFF, ANALYZE, SUMMARY OFF) LET var1 = (SELECT count(*) FROM var_tab_test_table);
+                              QUERY PLAN                               
+-----------------------------------------------------------------------
+ SET SESSION VARIABLE
+ Result (actual rows=1 loops=1)
+   InitPlan 1
+     ->  Aggregate (actual rows=1 loops=1)
+           ->  Seq Scan on var_tab_test_table (actual rows=10 loops=1)
+(5 rows)
+
+-- should be 10
+SELECT var1;
+ var1 
+------
+   10
+(1 row)
+
+DROP VARIABLE var1;
+DROP TABLE var_tab_test_table;
diff --git a/src/test/regress/sql/session_variables.sql b/src/test/regress/sql/session_variables.sql
index ef9fffd1d4b..046aa5a5d22 100644
--- a/src/test/regress/sql/session_variables.sql
+++ b/src/test/regress/sql/session_variables.sql
@@ -875,3 +875,24 @@ SET session_variables_ambiguity_warning TO off;
 
 DROP TABLE public.xxtab;
 DROP SCHEMA xxtab CASCADE;
+
+CREATE VARIABLE var1 bigint;
+
+CREATE TABLE var_tab_test_table(a int);
+
+INSERT INTO var_tab_test_table SELECT * FROM generate_series(1,10);
+
+VACUUM ANALYZE var_tab_test_table;
+
+EXPLAIN (COSTS OFF) LET var1 = (SELECT count(*) FROM var_tab_test_table);
+
+-- should be NULL
+SELECT var1;
+
+EXPLAIN (COSTS OFF, TIMING OFF, ANALYZE, SUMMARY OFF) LET var1 = (SELECT count(*) FROM var_tab_test_table);
+
+-- should be 10
+SELECT var1;
+
+DROP VARIABLE var1;
+DROP TABLE var_tab_test_table;
-- 
2.47.0



  [text/x-patch] v20241110-0007-GUC-session_variables_ambiguity_warning.patch (13.9K, 16-v20241110-0007-GUC-session_variables_ambiguity_warning.patch)
  download | inline diff:
From 078e5fb07da63452622f33f6a4917112ad7f8939 Mon Sep 17 00:00:00 2001
From: "[email protected]" <[email protected]>
Date: Tue, 21 May 2024 17:55:24 +0200
Subject: [PATCH 07/20] GUC session_variables_ambiguity_warning

Inside an query the session variables can be shadowed. This behaviour is by design,
because this ensuring so new variables doesn't break existing queries. But this
behaviour can be confusing, because shadowing is quiet.

When new GUC session_variables_ambiguity_warning is on, then the warning
is displayed, when some session variable can be used, but it is not used
due shadowing. This feature should to help with investigation of unexpected
behaviour.

Note: PLpgSQL has an option that controls priority of identifier's spaces
(SQL or PLpgSQL). Default behaviour doesn't allows collisions. I believe
this default is the best. I cannot to implement similar very strict
behaviour to session variables, because one unhappy named session variable
can breaks an applications. The problems related to badly named PLpgSQL's
varible is limitted just to only one routine.
---
 doc/src/sgml/config.sgml                      | 60 +++++++++++++++++++
 doc/src/sgml/ddl.sgml                         | 10 ++++
 src/backend/parser/parse_expr.c               | 49 +++++++++++++--
 src/backend/utils/misc/guc_tables.c           |  9 +++
 src/backend/utils/misc/postgresql.conf.sample |  1 +
 src/include/parser/parse_expr.h               |  1 +
 .../src/expected/plpgsql_session_variable.out | 33 ++++++++++
 .../src/sql/plpgsql_session_variable.sql      | 29 +++++++++
 .../regress/expected/session_variables.out    | 22 +++++++
 src/test/regress/sql/session_variables.sql    | 20 +++++++
 10 files changed, 230 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index d54f9049569..de1b7acd889 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -10736,6 +10736,66 @@ dynamic_library_path = 'C:\tools\postgresql;H:\my_project\lib;$libdir'
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-session-variables-ambiguity-warning" xreflabel="session_variables_ambiguity_warning">
+      <term><varname>session_variables_ambiguity_warning</varname> (<type>boolean</type>)
+      <indexterm>
+       <primary><varname>session_variables_ambiguity_warning</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        When on, a warning is raised when any identifier in a query could be
+        used as both a column identifier, routine variable or a session
+        variable identifier. The default is <literal>off</literal>.
+       </para>
+       <para>
+        Session variables can be shadowed by column references in a query, this
+        is an expected behavior.  Previously working queries shouldn't error out
+        by creating any session variable, so session variables are always shadowed
+        if an identifier is ambiguous.  Variables should be referenced using
+        anunambiguous identifier without any possibility for a collision with
+        identifier of other database objects (column names or record fields names).
+        The warning messages emitted when enabling <varname>session_variables_ambiguity_warning</varname>
+        can help finding such identifier collision.
+<programlisting>
+CREATE TABLE foo(a int);
+INSERT INTO foo VALUES(10);
+CREATE VARIABLE a int;
+LET a = 100;
+SELECT a FROM foo;
+</programlisting>
+
+<screen>
+ a
+----
+ 10
+(1 row)
+</screen>
+
+<programlisting>
+SET session_variables_ambiguity_warning TO on;
+SELECT a FROM foo;
+</programlisting>
+
+<screen>
+WARNING:  session variable "a" is shadowed
+LINE 1: SELECT a FROM foo;
+               ^
+DETAIL:  Session variables can be shadowed by columns, routine's variables and routine's arguments with the same name.
+ a
+----
+ 10
+(1 row)
+</screen>
+       </para>
+       <para>
+        This feature can significantly increase log size, so it's disabled by
+        default.  For testing or development environments it's recommended to
+        enable it if you use session variables.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="guc-standard-conforming-strings" xreflabel="standard_conforming_strings">
       <term><varname>standard_conforming_strings</varname> (<type>boolean</type>)
       <indexterm><primary>strings</primary><secondary>standard conforming</secondary></indexterm>
diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml
index d4639b27795..1f6c88cf556 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -5358,6 +5358,16 @@ SELECT current_user_id;
     the schema name, columns can use table aliases, routine variables can use
     block labels, and routine arguments can use the routine name.
    </para>
+
+   <para>
+    When a query contains identifiers or qualified identifiers that could be
+    used as both a session variable identifiers and as column identifier,
+    then the column identifier is preferred every time.  Warnings can be
+    emitted when this situation happens by enabling configuration parameter <xref
+    linkend="guc-session-variables-ambiguity-warning"/>. User can explicitly
+    qualify the source object by syntax <literal>table.column</literal> or
+    <literal>schema.variable</literal>.
+   </para>
   </sect1>
 
  <sect1 id="ddl-others">
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index aba5259e027..3602c3572d7 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -45,6 +45,7 @@
 
 /* GUC parameters */
 bool		Transform_null_equals = false;
+bool		session_variables_ambiguity_warning = false;
 
 
 static Node *transformExprRecurse(ParseState *pstate, Node *expr);
@@ -960,11 +961,51 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
 		 *
 		 * SELECT foo.a, foo.b, foo.c FROM foo;
 		 *
-		 * However, that is very confusing, so we disallow it.  We don't try to
-		 * identify a variable if we know that it would be shadowed.
-		 * -----
+		 * However, that is very confusing, so we disallow it.
+		 *
+		 * When session_variables_ambiguity_warning is requested, then we
+		 * need to identify a variable although we know, so this variable
+		 * would be shadowed.
 		 */
-		if (!node && !(relname && crerr == CRERR_NO_COLUMN))
+		if (node || (relname && crerr == CRERR_NO_COLUMN))
+		{
+			/*
+			 * In this path we just try (if it is wanted) detect if session
+			 * variable is shadowed.
+			 */
+			if (session_variables_ambiguity_warning)
+			{
+				/*
+				 * The AccessShareLock is created on related session variable.
+				 * The lock will be kept for the whole transaction.
+				 */
+				varid = IdentifyVariable(cref->fields, &attrname, &not_unique, true);
+
+				if (OidIsValid(varid))
+				{
+					/* this path will ending by WARNING. Unlock variable first */
+					UnlockDatabaseObject(VariableRelationId, varid, 0, AccessShareLock);
+
+					if (node)
+						ereport(WARNING,
+								(errcode(ERRCODE_AMBIGUOUS_COLUMN),
+								 errmsg("session variable \"%s\" is shadowed",
+										NameListToString(cref->fields)),
+								 errdetail("Session variables can be shadowed by columns, routine's variables and routine's arguments with the same name."),
+								 parser_errposition(pstate, cref->location)));
+					else
+						/* session variable is shadowed by RTE */
+						ereport(WARNING,
+								(errcode(ERRCODE_AMBIGUOUS_COLUMN),
+								 errmsg("session variable \"%s.%s\" is shadowed",
+										get_namespace_name(get_session_variable_namespace(varid)),
+										get_session_variable_name(varid)),
+								 errdetail("Session variables can be shadowed by tables or table's aliases with the same name."),
+								 parser_errposition(pstate, cref->location)));
+				}
+			}
+		}
+		else
 		{
 			/* takes an AccessShareLock on the session variable */
 			varid = IdentifyVariable(cref->fields, &attrname, &not_unique, false);
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 8a67f01200c..efb65b02380 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -1595,6 +1595,15 @@ struct config_bool ConfigureNamesBool[] =
 		false,
 		NULL, NULL, NULL
 	},
+	{
+		{"session_variables_ambiguity_warning", PGC_USERSET, CLIENT_CONN_OTHER,
+			gettext_noop("Raise a warning when reference to a session variable is ambiguous."),
+			NULL
+		},
+		&session_variables_ambiguity_warning,
+		false,
+		NULL, NULL, NULL
+	},
 	{
 		{"default_transaction_read_only", PGC_USERSET, CLIENT_CONN_STATEMENT,
 			gettext_noop("Sets the default read-only status of new transactions."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 39a3ac23127..992dda3c644 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -714,6 +714,7 @@
 #default_transaction_read_only = off
 #default_transaction_deferrable = off
 #session_replication_role = 'origin'
+#session_variables_ambiguity_warning = off
 #statement_timeout = 0				# in milliseconds, 0 is disabled
 #transaction_timeout = 0			# in milliseconds, 0 is disabled
 #lock_timeout = 0				# in milliseconds, 0 is disabled
diff --git a/src/include/parser/parse_expr.h b/src/include/parser/parse_expr.h
index 9b46dfd9ecc..0d64b300f8a 100644
--- a/src/include/parser/parse_expr.h
+++ b/src/include/parser/parse_expr.h
@@ -17,6 +17,7 @@
 
 /* GUC parameters */
 extern PGDLLIMPORT bool Transform_null_equals;
+extern PGDLLIMPORT bool session_variables_ambiguity_warning;
 
 extern Node *transformExpr(ParseState *pstate, Node *expr, ParseExprKind exprKind);
 
diff --git a/src/pl/plpgsql/src/expected/plpgsql_session_variable.out b/src/pl/plpgsql/src/expected/plpgsql_session_variable.out
index 593d232d260..ddb099bbbb1 100644
--- a/src/pl/plpgsql/src/expected/plpgsql_session_variable.out
+++ b/src/pl/plpgsql/src/expected/plpgsql_session_variable.out
@@ -354,6 +354,39 @@ $$;
 NOTICE:  session variable is 100
 NOTICE:  plpgsql variable is 1000
 NOTICE:  variable is 1000
+-- againt with session_variables_ambiguity_warning(on)
+SET session_variables_ambiguity_warning TO on;
+DO $$
+<<myblock>>
+DECLARE plpgsql_sv_var1 int;
+BEGIN
+  LET plpgsql_sv_var1 = 100;
+
+  -- should be ok without warning
+  plpgsql_sv_var1 := 1000;
+
+  -- should be ok without warning
+  -- print 100;
+  RAISE NOTICE 'session variable is %', public.plpgsql_sv_var1;
+
+  -- should be ok without warning
+  -- print 1000
+  RAISE NOTICE 'plpgsql variable is %', myblock.plpgsql_sv_var1;
+
+  -- should to print plpgsql variable with warning
+  -- print 1000
+  RAISE NOTICE 'variable is %', plpgsql_sv_var1;
+END;
+$$;
+NOTICE:  session variable is 100
+NOTICE:  plpgsql variable is 1000
+WARNING:  session variable "plpgsql_sv_var1" is shadowed
+LINE 1: plpgsql_sv_var1
+        ^
+DETAIL:  Session variables can be shadowed by columns, routine's variables and routine's arguments with the same name.
+QUERY:  plpgsql_sv_var1
+NOTICE:  variable is 1000
+SET session_variables_ambiguity_warning TO off;
 DROP VARIABLE plpgsql_sv_var1;
 -- the value should not be corrupted
 CREATE VARIABLE plpgsql_sv_v text;
diff --git a/src/pl/plpgsql/src/sql/plpgsql_session_variable.sql b/src/pl/plpgsql/src/sql/plpgsql_session_variable.sql
index 398e7fe2a7c..ef705a4cc87 100644
--- a/src/pl/plpgsql/src/sql/plpgsql_session_variable.sql
+++ b/src/pl/plpgsql/src/sql/plpgsql_session_variable.sql
@@ -260,6 +260,35 @@ BEGIN
 END;
 $$;
 
+-- againt with session_variables_ambiguity_warning(on)
+
+SET session_variables_ambiguity_warning TO on;
+
+DO $$
+<<myblock>>
+DECLARE plpgsql_sv_var1 int;
+BEGIN
+  LET plpgsql_sv_var1 = 100;
+
+  -- should be ok without warning
+  plpgsql_sv_var1 := 1000;
+
+  -- should be ok without warning
+  -- print 100;
+  RAISE NOTICE 'session variable is %', public.plpgsql_sv_var1;
+
+  -- should be ok without warning
+  -- print 1000
+  RAISE NOTICE 'plpgsql variable is %', myblock.plpgsql_sv_var1;
+
+  -- should to print plpgsql variable with warning
+  -- print 1000
+  RAISE NOTICE 'variable is %', plpgsql_sv_var1;
+END;
+$$;
+
+SET session_variables_ambiguity_warning TO off;
+
 DROP VARIABLE plpgsql_sv_var1;
 
 -- the value should not be corrupted
diff --git a/src/test/regress/expected/session_variables.out b/src/test/regress/expected/session_variables.out
index 3239f45e8e4..582d0b8d034 100644
--- a/src/test/regress/expected/session_variables.out
+++ b/src/test/regress/expected/session_variables.out
@@ -1262,3 +1262,25 @@ SELECT var1;
 (1 row)
 
 DROP VARIABLE var1, var2;
+-- test session_variables_ambiguity_warning
+CREATE SCHEMA xxtab;
+CREATE VARIABLE xxtab.avar int;
+CREATE TABLE public.xxtab(avar int);
+INSERT INTO public.xxtab VALUES(1);
+LET xxtab.avar = 20;
+SET session_variables_ambiguity_warning TO on;
+--- should to raise warning, show 1
+SELECT xxtab.avar FROM public.xxtab;
+WARNING:  session variable "xxtab.avar" is shadowed
+LINE 1: SELECT xxtab.avar FROM public.xxtab;
+               ^
+DETAIL:  Session variables can be shadowed by columns, routine's variables and routine's arguments with the same name.
+ avar 
+------
+    1
+(1 row)
+
+SET session_variables_ambiguity_warning TO off;
+DROP TABLE public.xxtab;
+DROP SCHEMA xxtab CASCADE;
+NOTICE:  drop cascades to session variable xxtab.avar
diff --git a/src/test/regress/sql/session_variables.sql b/src/test/regress/sql/session_variables.sql
index a77fd785339..ef9fffd1d4b 100644
--- a/src/test/regress/sql/session_variables.sql
+++ b/src/test/regress/sql/session_variables.sql
@@ -855,3 +855,23 @@ BEGIN; DROP VARIABLE var1; ROLLBACK;
 SELECT var1;
 
 DROP VARIABLE var1, var2;
+
+-- test session_variables_ambiguity_warning
+CREATE SCHEMA xxtab;
+
+CREATE VARIABLE xxtab.avar int;
+
+CREATE TABLE public.xxtab(avar int);
+
+INSERT INTO public.xxtab VALUES(1);
+
+LET xxtab.avar = 20;
+
+SET session_variables_ambiguity_warning TO on;
+--- should to raise warning, show 1
+SELECT xxtab.avar FROM public.xxtab;
+
+SET session_variables_ambiguity_warning TO off;
+
+DROP TABLE public.xxtab;
+DROP SCHEMA xxtab CASCADE;
-- 
2.47.0



  [text/x-patch] v20241110-0006-plpgsql-tests.patch (16.9K, 17-v20241110-0006-plpgsql-tests.patch)
  download | inline diff:
From acd0c1e562a8e52aca2a5b657f88ac123fec9518 Mon Sep 17 00:00:00 2001
From: "[email protected]" <[email protected]>
Date: Fri, 19 Jan 2024 16:08:52 +0100
Subject: [PATCH 06/20] plpgsql tests

---
 src/pl/plpgsql/src/Makefile                   |   3 +-
 .../src/expected/plpgsql_session_variable.out | 382 ++++++++++++++++++
 src/pl/plpgsql/src/meson.build                |   1 +
 .../src/sql/plpgsql_session_variable.sql      | 289 +++++++++++++
 4 files changed, 674 insertions(+), 1 deletion(-)
 create mode 100644 src/pl/plpgsql/src/expected/plpgsql_session_variable.out
 create mode 100644 src/pl/plpgsql/src/sql/plpgsql_session_variable.sql

diff --git a/src/pl/plpgsql/src/Makefile b/src/pl/plpgsql/src/Makefile
index 63cb96fae3e..bbcae27d422 100644
--- a/src/pl/plpgsql/src/Makefile
+++ b/src/pl/plpgsql/src/Makefile
@@ -35,7 +35,8 @@ REGRESS_OPTS = --dbname=$(PL_TESTDB)
 REGRESS = plpgsql_array plpgsql_cache plpgsql_call plpgsql_control \
 	plpgsql_copy plpgsql_domain plpgsql_misc \
 	plpgsql_record plpgsql_simple plpgsql_transaction \
-	plpgsql_trap plpgsql_trigger plpgsql_varprops
+	plpgsql_trap plpgsql_trigger plpgsql_varprops \
+	plpgsql_session_variable
 
 # where to find gen_keywordlist.pl and subsidiary files
 TOOLSDIR = $(top_srcdir)/src/tools
diff --git a/src/pl/plpgsql/src/expected/plpgsql_session_variable.out b/src/pl/plpgsql/src/expected/plpgsql_session_variable.out
new file mode 100644
index 00000000000..593d232d260
--- /dev/null
+++ b/src/pl/plpgsql/src/expected/plpgsql_session_variable.out
@@ -0,0 +1,382 @@
+-- test of session variables
+CREATE VARIABLE plpgsql_sv_var AS numeric;
+LET plpgsql_sv_var = pi();
+-- passing parameters to DO block
+DO $$
+BEGIN
+  RAISE NOTICE 'value of session variable is %', plpgsql_sv_var;
+END;
+$$;
+NOTICE:  value of session variable is 3.14159265358979
+-- passing output from DO block;
+DO $$
+BEGIN
+  LET plpgsql_sv_var = 2 * pi();
+END
+$$;
+SELECT plpgsql_sv_var AS "pi_multiply_2";
+  pi_multiply_2   
+------------------
+ 6.28318530717959
+(1 row)
+
+DROP VARIABLE plpgsql_sv_var;
+-- test access from PL/pgSQL
+CREATE VARIABLE plpgsql_sv_var1 AS int;
+CREATE VARIABLE plpgsql_sv_var2 AS numeric;
+CREATE VARIABLE plpgsql_sv_var3 AS varchar;
+CREATE OR REPLACE FUNCTION writer_func()
+RETURNS void AS $$
+BEGIN
+  LET plpgsql_sv_var1 = 10;
+  LET plpgsql_sv_var2 = pi();
+  -- very long value
+  LET plpgsql_sv_var3 = format('(%s)', repeat('*', 10000));
+END;
+$$ LANGUAGE plpgsql;
+CREATE OR REPLACE FUNCTION updater_func()
+RETURNS void AS $$
+BEGIN
+  LET plpgsql_sv_var1 = plpgsql_sv_var1 + 100;
+  LET plpgsql_sv_var2 = plpgsql_sv_var2 + 100000000000;
+  -- very long value
+  LET plpgsql_sv_var3 = plpgsql_sv_var3 || format('(%s)', repeat('*', 10000));
+END;
+$$ LANGUAGE plpgsql;
+CREATE OR REPLACE FUNCTION reader_func()
+RETURNS void AS $$
+BEGIN
+  RAISE NOTICE 'var1 = %', plpgsql_sv_var1;
+  RAISE NOTICE 'var2 = %', plpgsql_sv_var2;
+  RAISE NOTICE 'length of var3 = %', length(plpgsql_sv_var3);
+END;
+$$ LANGUAGE plpgsql;
+-- execute under transaction
+BEGIN;
+SELECT writer_func();
+ writer_func 
+-------------
+ 
+(1 row)
+
+SELECT reader_func();
+NOTICE:  var1 = 10
+NOTICE:  var2 = 3.14159265358979
+NOTICE:  length of var3 = 10002
+ reader_func 
+-------------
+ 
+(1 row)
+
+SELECT updater_func();
+ updater_func 
+--------------
+ 
+(1 row)
+
+SELECT reader_func();
+NOTICE:  var1 = 110
+NOTICE:  var2 = 100000000003.14159265358979
+NOTICE:  length of var3 = 20004
+ reader_func 
+-------------
+ 
+(1 row)
+
+END;
+-- execute out of transaction
+SELECT writer_func();
+ writer_func 
+-------------
+ 
+(1 row)
+
+SELECT reader_func();
+NOTICE:  var1 = 10
+NOTICE:  var2 = 3.14159265358979
+NOTICE:  length of var3 = 10002
+ reader_func 
+-------------
+ 
+(1 row)
+
+SELECT updater_func();
+ updater_func 
+--------------
+ 
+(1 row)
+
+SELECT reader_func();
+NOTICE:  var1 = 110
+NOTICE:  var2 = 100000000003.14159265358979
+NOTICE:  length of var3 = 20004
+ reader_func 
+-------------
+ 
+(1 row)
+
+-- execute inside PL/pgSQL block
+DO $$
+BEGIN
+  PERFORM writer_func();
+  PERFORM reader_func();
+  PERFORM updater_func();
+  PERFORM reader_func();
+END;
+$$;
+NOTICE:  var1 = 10
+NOTICE:  var2 = 3.14159265358979
+NOTICE:  length of var3 = 10002
+NOTICE:  var1 = 110
+NOTICE:  var2 = 100000000003.14159265358979
+NOTICE:  length of var3 = 20004
+-- plan caches should be correctly invalidated
+DROP VARIABLE plpgsql_sv_var1, plpgsql_sv_var2, plpgsql_sv_var3;
+CREATE VARIABLE plpgsql_sv_var1 AS int;
+CREATE VARIABLE plpgsql_sv_var2 AS numeric;
+CREATE VARIABLE plpgsql_sv_var3 AS varchar;
+-- should to work again
+DO $$
+BEGIN
+  PERFORM writer_func();
+  PERFORM reader_func();
+  PERFORM updater_func();
+  PERFORM reader_func();
+END;
+$$;
+NOTICE:  var1 = 10
+NOTICE:  var2 = 3.14159265358979
+NOTICE:  length of var3 = 10002
+NOTICE:  var1 = 110
+NOTICE:  var2 = 100000000003.14159265358979
+NOTICE:  length of var3 = 20004
+DROP VARIABLE plpgsql_sv_var1, plpgsql_sv_var2, plpgsql_sv_var3;
+DROP FUNCTION writer_func;
+DROP FUNCTION reader_func;
+DROP FUNCTION updater_func;
+-- another check of correct plan cache invalidation
+CREATE VARIABLE plpgsql_sv_var1 AS int;
+CREATE VARIABLE plpgsql_sv_var2 AS int[];
+CREATE OR REPLACE FUNCTION test_func()
+RETURNS void AS $$
+DECLARE v int[] DEFAULT '{}';
+BEGIN
+  LET plpgsql_sv_var1 = 1;
+  v[plpgsql_sv_var1] = 100;
+  RAISE NOTICE '%', v;
+  LET plpgsql_sv_var2 = v;
+  LET plpgsql_sv_var2[plpgsql_sv_var1] = -1;
+  RAISE NOTICE '%', plpgsql_sv_var2;
+END;
+$$ LANGUAGE plpgsql;
+SELECT test_func();
+NOTICE:  {100}
+NOTICE:  {-1}
+ test_func 
+-----------
+ 
+(1 row)
+
+DROP VARIABLE plpgsql_sv_var1, plpgsql_sv_var2;
+CREATE VARIABLE plpgsql_sv_var1 AS int;
+CREATE VARIABLE plpgsql_sv_var2 AS int[];
+SELECT test_func();
+NOTICE:  {100}
+NOTICE:  {-1}
+ test_func 
+-----------
+ 
+(1 row)
+
+DROP FUNCTION test_func();
+DROP VARIABLE plpgsql_sv_var1, plpgsql_sv_var2;
+-- check secure access
+CREATE ROLE regress_var_owner_role;
+CREATE ROLE regress_var_reader_role;
+CREATE ROLE regress_var_exec_role;
+GRANT ALL ON SCHEMA public TO regress_var_owner_role, regress_var_reader_role, regress_var_exec_role;
+SET ROLE TO regress_var_owner_role;
+CREATE VARIABLE plpgsql_sv_var1 AS int;
+LET plpgsql_sv_var1 = 10;
+SET ROLE TO DEFAULT;
+SET ROLE TO regress_var_reader_role;
+CREATE OR REPLACE FUNCTION var_read_func()
+RETURNS void AS $$
+BEGIN
+  RAISE NOTICE '%', plpgsql_sv_var1;
+END;
+$$ LANGUAGE plpgsql SECURITY DEFINER;
+SET ROLE TO DEFAULT;
+SET ROLE TO regress_var_exec_role;
+-- should to fail
+SELECT var_read_func();
+ERROR:  permission denied for session variable plpgsql_sv_var1
+CONTEXT:  PL/pgSQL expression "plpgsql_sv_var1"
+PL/pgSQL function var_read_func() line 3 at RAISE
+SET ROLE TO DEFAULT;
+SET ROLE TO regress_var_owner_role;
+GRANT SELECT ON VARIABLE plpgsql_sv_var1 TO regress_var_reader_role;
+SET ROLE TO DEFAULT;
+SET ROLE TO regress_var_exec_role;
+-- should be ok
+SELECT var_read_func();
+NOTICE:  10
+ var_read_func 
+---------------
+ 
+(1 row)
+
+SET ROLE TO DEFAULT;
+SET ROLE TO regress_var_owner_role;
+DROP VARIABLE plpgsql_sv_var1;
+SET ROLE TO DEFAULT;
+SET ROLE TO regress_var_exec_role;
+-- should to fail, but not crash
+SELECT var_read_func();
+ERROR:  column "plpgsql_sv_var1" does not exist
+LINE 1: plpgsql_sv_var1
+        ^
+QUERY:  plpgsql_sv_var1
+CONTEXT:  PL/pgSQL function var_read_func() line 3 at RAISE
+SET ROLE TO DEFAULT;
+DROP FUNCTION var_read_func;
+REVOKE ALL ON SCHEMA public FROM regress_var_owner_role, regress_var_reader_role, regress_var_exec_role;
+DROP ROLE regress_var_owner_role;
+DROP ROLE regress_var_reader_role;
+DROP ROLE regress_var_exec_role;
+-- returns updated value
+CREATE VARIABLE plpgsql_sv_var1 AS int;
+CREATE OR REPLACE FUNCTION inc_var_int(int)
+RETURNS int AS $$
+BEGIN
+  LET plpgsql_sv_var1 = COALESCE(plpgsql_sv_var1 + $1, $1);
+  RETURN plpgsql_sv_var1;
+END;
+$$ LANGUAGE plpgsql;
+SELECT inc_var_int(1);
+ inc_var_int 
+-------------
+           1
+(1 row)
+
+SELECT inc_var_int(1);
+ inc_var_int 
+-------------
+           2
+(1 row)
+
+SELECT inc_var_int(1);
+ inc_var_int 
+-------------
+           3
+(1 row)
+
+SELECT inc_var_int(1) FROM generate_series(1,10);
+ inc_var_int 
+-------------
+           4
+           5
+           6
+           7
+           8
+           9
+          10
+          11
+          12
+          13
+(10 rows)
+
+CREATE VARIABLE plpgsql_sv_var2 AS numeric;
+LET plpgsql_sv_var2 = 0.0;
+CREATE OR REPLACE FUNCTION inc_var_num(numeric)
+RETURNS int AS $$
+BEGIN
+  LET plpgsql_sv_var2 = COALESCE(plpgsql_sv_var2 + $1, $1);
+  RETURN plpgsql_sv_var2;
+END;
+$$ LANGUAGE plpgsql;
+SELECT inc_var_num(1.0);
+ inc_var_num 
+-------------
+           1
+(1 row)
+
+SELECT inc_var_num(1.0);
+ inc_var_num 
+-------------
+           2
+(1 row)
+
+SELECT inc_var_num(1.0);
+ inc_var_num 
+-------------
+           3
+(1 row)
+
+SELECT inc_var_num(1.0) FROM generate_series(1,10);
+ inc_var_num 
+-------------
+           4
+           5
+           6
+           7
+           8
+           9
+          10
+          11
+          12
+          13
+(10 rows)
+
+DROP VARIABLE plpgsql_sv_var1, plpgsql_sv_var2;
+DROP FUNCTION inc_var_int;
+DROP FUNCTION inc_var_num;
+-- plpgsql variables are preferred against session variables
+CREATE VARIABLE plpgsql_sv_var1 AS int;
+DO $$
+<<myblock>>
+DECLARE plpgsql_sv_var1 int;
+BEGIN
+  LET plpgsql_sv_var1 = 100;
+
+  plpgsql_sv_var1 := 1000;
+
+  -- print 100;
+  RAISE NOTICE 'session variable is %', public.plpgsql_sv_var1;
+
+  -- print 1000
+  RAISE NOTICE 'plpgsql variable is %', myblock.plpgsql_sv_var1;
+
+  -- print 1000
+  RAISE NOTICE 'variable is %', plpgsql_sv_var1;
+END;
+$$;
+NOTICE:  session variable is 100
+NOTICE:  plpgsql variable is 1000
+NOTICE:  variable is 1000
+DROP VARIABLE plpgsql_sv_var1;
+-- the value should not be corrupted
+CREATE VARIABLE plpgsql_sv_v text;
+LET plpgsql_sv_v = 'abc';
+CREATE FUNCTION ffunc()
+RETURNS text AS $$
+BEGIN
+  RETURN gfunc(plpgsql_sv_v);
+END
+$$ LANGUAGE plpgsql;
+CREATE FUNCTION gfunc(t text)
+RETURNS text AS $$
+BEGIN
+  LET plpgsql_sv_v = 'BOOM!';
+  RETURN t;
+END;
+$$ LANGUAGE plpgsql;
+select ffunc();
+ ffunc 
+-------
+ abc
+(1 row)
+
+DROP FUNCTION ffunc();
+DROP FUNCTION gfunc(text);
+DROP VARIABLE plpgsql_sv_v;
diff --git a/src/pl/plpgsql/src/meson.build b/src/pl/plpgsql/src/meson.build
index 3dd734b776b..3905c0b6d45 100644
--- a/src/pl/plpgsql/src/meson.build
+++ b/src/pl/plpgsql/src/meson.build
@@ -88,6 +88,7 @@ tests += {
       'plpgsql_trap',
       'plpgsql_trigger',
       'plpgsql_varprops',
+      'plpgsql_session_variable',
     ],
   },
 }
diff --git a/src/pl/plpgsql/src/sql/plpgsql_session_variable.sql b/src/pl/plpgsql/src/sql/plpgsql_session_variable.sql
new file mode 100644
index 00000000000..398e7fe2a7c
--- /dev/null
+++ b/src/pl/plpgsql/src/sql/plpgsql_session_variable.sql
@@ -0,0 +1,289 @@
+-- test of session variables
+CREATE VARIABLE plpgsql_sv_var AS numeric;
+
+LET plpgsql_sv_var = pi();
+
+-- passing parameters to DO block
+DO $$
+BEGIN
+  RAISE NOTICE 'value of session variable is %', plpgsql_sv_var;
+END;
+$$;
+
+-- passing output from DO block;
+DO $$
+BEGIN
+  LET plpgsql_sv_var = 2 * pi();
+END
+$$;
+
+SELECT plpgsql_sv_var AS "pi_multiply_2";
+
+DROP VARIABLE plpgsql_sv_var;
+
+-- test access from PL/pgSQL
+CREATE VARIABLE plpgsql_sv_var1 AS int;
+CREATE VARIABLE plpgsql_sv_var2 AS numeric;
+CREATE VARIABLE plpgsql_sv_var3 AS varchar;
+
+CREATE OR REPLACE FUNCTION writer_func()
+RETURNS void AS $$
+BEGIN
+  LET plpgsql_sv_var1 = 10;
+  LET plpgsql_sv_var2 = pi();
+  -- very long value
+  LET plpgsql_sv_var3 = format('(%s)', repeat('*', 10000));
+END;
+$$ LANGUAGE plpgsql;
+
+CREATE OR REPLACE FUNCTION updater_func()
+RETURNS void AS $$
+BEGIN
+  LET plpgsql_sv_var1 = plpgsql_sv_var1 + 100;
+  LET plpgsql_sv_var2 = plpgsql_sv_var2 + 100000000000;
+  -- very long value
+  LET plpgsql_sv_var3 = plpgsql_sv_var3 || format('(%s)', repeat('*', 10000));
+END;
+$$ LANGUAGE plpgsql;
+
+
+CREATE OR REPLACE FUNCTION reader_func()
+RETURNS void AS $$
+BEGIN
+  RAISE NOTICE 'var1 = %', plpgsql_sv_var1;
+  RAISE NOTICE 'var2 = %', plpgsql_sv_var2;
+  RAISE NOTICE 'length of var3 = %', length(plpgsql_sv_var3);
+END;
+$$ LANGUAGE plpgsql;
+
+-- execute under transaction
+BEGIN;
+SELECT writer_func();
+SELECT reader_func();
+SELECT updater_func();
+SELECT reader_func();
+END;
+
+-- execute out of transaction
+SELECT writer_func();
+SELECT reader_func();
+SELECT updater_func();
+SELECT reader_func();
+
+-- execute inside PL/pgSQL block
+DO $$
+BEGIN
+  PERFORM writer_func();
+  PERFORM reader_func();
+  PERFORM updater_func();
+  PERFORM reader_func();
+END;
+$$;
+
+-- plan caches should be correctly invalidated
+DROP VARIABLE plpgsql_sv_var1, plpgsql_sv_var2, plpgsql_sv_var3;
+
+CREATE VARIABLE plpgsql_sv_var1 AS int;
+CREATE VARIABLE plpgsql_sv_var2 AS numeric;
+CREATE VARIABLE plpgsql_sv_var3 AS varchar;
+
+-- should to work again
+DO $$
+BEGIN
+  PERFORM writer_func();
+  PERFORM reader_func();
+  PERFORM updater_func();
+  PERFORM reader_func();
+END;
+$$;
+
+DROP VARIABLE plpgsql_sv_var1, plpgsql_sv_var2, plpgsql_sv_var3;
+
+DROP FUNCTION writer_func;
+DROP FUNCTION reader_func;
+DROP FUNCTION updater_func;
+
+-- another check of correct plan cache invalidation
+CREATE VARIABLE plpgsql_sv_var1 AS int;
+CREATE VARIABLE plpgsql_sv_var2 AS int[];
+
+CREATE OR REPLACE FUNCTION test_func()
+RETURNS void AS $$
+DECLARE v int[] DEFAULT '{}';
+BEGIN
+  LET plpgsql_sv_var1 = 1;
+  v[plpgsql_sv_var1] = 100;
+  RAISE NOTICE '%', v;
+  LET plpgsql_sv_var2 = v;
+  LET plpgsql_sv_var2[plpgsql_sv_var1] = -1;
+  RAISE NOTICE '%', plpgsql_sv_var2;
+END;
+$$ LANGUAGE plpgsql;
+
+SELECT test_func();
+
+DROP VARIABLE plpgsql_sv_var1, plpgsql_sv_var2;
+
+CREATE VARIABLE plpgsql_sv_var1 AS int;
+CREATE VARIABLE plpgsql_sv_var2 AS int[];
+
+SELECT test_func();
+
+DROP FUNCTION test_func();
+
+DROP VARIABLE plpgsql_sv_var1, plpgsql_sv_var2;
+
+-- check secure access
+CREATE ROLE regress_var_owner_role;
+CREATE ROLE regress_var_reader_role;
+CREATE ROLE regress_var_exec_role;
+
+GRANT ALL ON SCHEMA public TO regress_var_owner_role, regress_var_reader_role, regress_var_exec_role;
+
+SET ROLE TO regress_var_owner_role;
+
+CREATE VARIABLE plpgsql_sv_var1 AS int;
+LET plpgsql_sv_var1 = 10;
+
+SET ROLE TO DEFAULT;
+
+SET ROLE TO regress_var_reader_role;
+
+CREATE OR REPLACE FUNCTION var_read_func()
+RETURNS void AS $$
+BEGIN
+  RAISE NOTICE '%', plpgsql_sv_var1;
+END;
+$$ LANGUAGE plpgsql SECURITY DEFINER;
+
+SET ROLE TO DEFAULT;
+
+SET ROLE TO regress_var_exec_role;
+
+-- should to fail
+SELECT var_read_func();
+
+SET ROLE TO DEFAULT;
+
+SET ROLE TO regress_var_owner_role;
+GRANT SELECT ON VARIABLE plpgsql_sv_var1 TO regress_var_reader_role;
+
+SET ROLE TO DEFAULT;
+
+SET ROLE TO regress_var_exec_role;
+
+-- should be ok
+SELECT var_read_func();
+
+SET ROLE TO DEFAULT;
+
+SET ROLE TO regress_var_owner_role;
+
+DROP VARIABLE plpgsql_sv_var1;
+
+SET ROLE TO DEFAULT;
+
+SET ROLE TO regress_var_exec_role;
+
+-- should to fail, but not crash
+SELECT var_read_func();
+
+SET ROLE TO DEFAULT;
+
+DROP FUNCTION var_read_func;
+
+REVOKE ALL ON SCHEMA public FROM regress_var_owner_role, regress_var_reader_role, regress_var_exec_role;
+
+DROP ROLE regress_var_owner_role;
+DROP ROLE regress_var_reader_role;
+DROP ROLE regress_var_exec_role;
+
+-- returns updated value
+CREATE VARIABLE plpgsql_sv_var1 AS int;
+
+CREATE OR REPLACE FUNCTION inc_var_int(int)
+RETURNS int AS $$
+BEGIN
+  LET plpgsql_sv_var1 = COALESCE(plpgsql_sv_var1 + $1, $1);
+  RETURN plpgsql_sv_var1;
+END;
+$$ LANGUAGE plpgsql;
+
+SELECT inc_var_int(1);
+SELECT inc_var_int(1);
+SELECT inc_var_int(1);
+
+SELECT inc_var_int(1) FROM generate_series(1,10);
+
+CREATE VARIABLE plpgsql_sv_var2 AS numeric;
+
+LET plpgsql_sv_var2 = 0.0;
+
+CREATE OR REPLACE FUNCTION inc_var_num(numeric)
+RETURNS int AS $$
+BEGIN
+  LET plpgsql_sv_var2 = COALESCE(plpgsql_sv_var2 + $1, $1);
+  RETURN plpgsql_sv_var2;
+END;
+$$ LANGUAGE plpgsql;
+
+SELECT inc_var_num(1.0);
+SELECT inc_var_num(1.0);
+SELECT inc_var_num(1.0);
+
+SELECT inc_var_num(1.0) FROM generate_series(1,10);
+
+DROP VARIABLE plpgsql_sv_var1, plpgsql_sv_var2;
+
+DROP FUNCTION inc_var_int;
+DROP FUNCTION inc_var_num;
+
+-- plpgsql variables are preferred against session variables
+CREATE VARIABLE plpgsql_sv_var1 AS int;
+
+DO $$
+<<myblock>>
+DECLARE plpgsql_sv_var1 int;
+BEGIN
+  LET plpgsql_sv_var1 = 100;
+
+  plpgsql_sv_var1 := 1000;
+
+  -- print 100;
+  RAISE NOTICE 'session variable is %', public.plpgsql_sv_var1;
+
+  -- print 1000
+  RAISE NOTICE 'plpgsql variable is %', myblock.plpgsql_sv_var1;
+
+  -- print 1000
+  RAISE NOTICE 'variable is %', plpgsql_sv_var1;
+END;
+$$;
+
+DROP VARIABLE plpgsql_sv_var1;
+
+-- the value should not be corrupted
+CREATE VARIABLE plpgsql_sv_v text;
+LET plpgsql_sv_v = 'abc';
+
+CREATE FUNCTION ffunc()
+RETURNS text AS $$
+BEGIN
+  RETURN gfunc(plpgsql_sv_v);
+END
+$$ LANGUAGE plpgsql;
+
+CREATE FUNCTION gfunc(t text)
+RETURNS text AS $$
+BEGIN
+  LET plpgsql_sv_v = 'BOOM!';
+  RETURN t;
+END;
+$$ LANGUAGE plpgsql;
+
+select ffunc();
+
+DROP FUNCTION ffunc();
+DROP FUNCTION gfunc(text);
+
+DROP VARIABLE plpgsql_sv_v;
-- 
2.47.0



  [text/x-patch] v20241110-0005-memory-cleaning-after-DROP-VARIABLE.patch (21.7K, 18-v20241110-0005-memory-cleaning-after-DROP-VARIABLE.patch)
  download | inline diff:
From 325afcfd4ed0023f2f31209e532cc77324d63872 Mon Sep 17 00:00:00 2001
From: "[email protected]" <[email protected]>
Date: Sun, 28 Jan 2024 20:58:23 +0100
Subject: [PATCH 05/20] memory cleaning after DROP VARIABLE

Accept of sinval message invalidates entries in sessionvars hash table. These entries
are validated before any read or write operations on session variables. When the entry
cannot be validated, then it is removed. The removing can be delayed when variable
is dropped inside current transaction. This transaction can be aborted and in this case
we want to preserve content of the variable.
---
 src/backend/access/transam/xact.c             |   1 +
 src/backend/catalog/pg_variable.c             |   4 +
 src/backend/commands/session_variable.c       | 156 ++++++++++++-
 src/include/commands/session_variable.h       |   2 +
 .../isolation/expected/session-variable.out   | 110 +++++++++
 src/test/isolation/isolation_schedule         |   1 +
 .../isolation/specs/session-variable.spec     |  50 ++++
 .../regress/expected/session_variables.out    | 218 ++++++++++++++++++
 src/test/regress/sql/session_variables.sql    | 121 ++++++++++
 9 files changed, 661 insertions(+), 2 deletions(-)
 create mode 100644 src/test/isolation/expected/session-variable.out
 create mode 100644 src/test/isolation/specs/session-variable.spec

diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c
index b7ebcc2a557..47d8a849ef7 100644
--- a/src/backend/access/transam/xact.c
+++ b/src/backend/access/transam/xact.c
@@ -36,6 +36,7 @@
 #include "catalog/pg_enum.h"
 #include "catalog/storage.h"
 #include "commands/async.h"
+#include "commands/session_variable.h"
 #include "commands/tablecmds.h"
 #include "commands/trigger.h"
 #include "common/pg_prng.h"
diff --git a/src/backend/catalog/pg_variable.c b/src/backend/catalog/pg_variable.c
index ef89594a268..0fb8c7f22a4 100644
--- a/src/backend/catalog/pg_variable.c
+++ b/src/backend/catalog/pg_variable.c
@@ -22,6 +22,7 @@
 #include "catalog/pg_collation.h"
 #include "catalog/pg_namespace.h"
 #include "catalog/pg_variable.h"
+#include "commands/session_variable.h"
 #include "miscadmin.h"
 #include "parser/parse_type.h"
 #include "utils/builtins.h"
@@ -254,4 +255,7 @@ DropVariableById(Oid varid)
 	ReleaseSysCache(tup);
 
 	table_close(rel, RowExclusiveLock);
+
+	/* do the necessary cleanup if needed in local memory */
+	SessionVariableDropPostprocess(varid);
 }
diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c
index 668b8189a6c..f5d19228e34 100644
--- a/src/backend/commands/session_variable.c
+++ b/src/backend/commands/session_variable.c
@@ -14,6 +14,7 @@
  */
 #include "postgres.h"
 
+#include "access/xact.h"
 #include "catalog/pg_variable.h"
 #include "commands/session_variable.h"
 #include "executor/svariableReceiver.h"
@@ -67,6 +68,14 @@ typedef struct SVariableData
 	void	   *domain_check_extra;
 	LocalTransactionId domain_check_extra_lxid;
 
+	/*
+	 * Top level local transaction id of the last transaction that dropped the
+	 * variable if any.  We need this information to avoid freeing memory for
+	 * variable dropped by the local backend that may be eventually
+	 * rollbacked.
+	 */
+	LocalTransactionId drop_lxid;
+
 	/*
 	 * Stored value and type description can be outdated when we receive a
 	 * sinval message.  We then have to check if the stored data are still
@@ -83,6 +92,19 @@ static HTAB *sessionvars = NULL;	/* hash table for session variables */
 
 static MemoryContext SVariableMemoryContext = NULL;
 
+/* true after accepted sinval message */
+static bool needs_validation = false;
+
+/*
+ * The content of session variables is not removed immediately. When it
+ * is possible we do this at the transaction end. But when the transaction failed,
+ * we cannot do it, because we lost access to the system catalog. So we
+ * try to do it in the next transaction before any get or set of any session
+ * variable. We don't want to repeat this opening cleaning in transaction,
+ * So we store the id of the transaction where opening validation was done.
+ */
+static LocalTransactionId validated_lxid = InvalidLocalTransactionId;
+
 /*
  * Callback function for session variable invalidation.
  */
@@ -114,6 +136,37 @@ pg_variable_cache_callback(Datum arg, int cacheid, uint32 hashvalue)
 		if (hashvalue == 0 || svar->hashvalue == hashvalue)
 		{
 			svar->is_valid = false;
+			needs_validation = true;
+		}
+	}
+}
+
+/*
+ * Handle the local memory cleanup for a DROP VARIABLE command.
+ *
+ * Caller should take care of removing the pg_variable entry first.
+ */
+void
+SessionVariableDropPostprocess(Oid varid)
+{
+	Assert(LocalTransactionIdIsValid(MyProc->vxid.lxid));
+
+	if (sessionvars)
+	{
+		bool		found;
+		SVariable	svar = (SVariable) hash_search(sessionvars, &varid,
+												   HASH_FIND, &found);
+
+		if (found)
+		{
+			/*
+			 * Save the current top level local transaction id to make sure we
+			 * don't automatically remove the local variable storage in
+			 * validate_all_session_variables, as the DROP VARIABLE will send
+			 * an invalidation message.
+			 */
+			svar->is_valid = false;
+			svar->drop_lxid = MyProc->vxid.lxid;
 		}
 	}
 }
@@ -167,6 +220,71 @@ is_session_variable_valid(SVariable svar)
 	return result;
 }
 
+/*
+ * It checks all possibly invalid entries against the system catalog.
+ * During this validation, the system cache can be invalidated, and the
+ * some sinval message can be accepted. This routine doesn't ensure
+ * all living entries of sessionvars will have is_valid flag, but it ensures
+ * that all entries are checked once.
+ *
+ * This routine is called before any usage (read, write, debug) of session
+ * variables (only once in a transaction) or at a transaction end.  At the
+ * end of transaction (specified by true of argument atEOX) we can
+ * throw all invalid variables. Inside the transaction (atEOX is false) we want
+ * to postpone cleaning of variables dropped inside the current transaction.
+ * The current transaction can be aborted, the related drop command will be
+ * aborted too. In this case we don't want lose the content of the variable,
+ * and therefore we need to hold the content of the dropped session variable
+ * until the end of the transaction (where the variable was dropped).
+ */
+static void
+remove_invalid_session_variables(void)
+{
+	HASH_SEQ_STATUS status;
+	SVariable	svar;
+
+	/*
+	 * The validation requires an access to system catalog, and then the
+	 * session state should be "in transaction".
+	 */
+	Assert(IsTransactionState());
+
+	if (!needs_validation || !sessionvars)
+		return;
+
+	/*
+	 * Reset this flag here, before we start the validation. It can be set to
+	 * on by incomming sinval message.
+	 */
+	needs_validation = false;
+
+	elog(DEBUG1, "effective call of validate_all_session_variables()");
+
+	hash_seq_init(&status, sessionvars);
+	while ((svar = (SVariable) hash_seq_search(&status)) != NULL)
+	{
+		if (!svar->is_valid)
+		{
+			if (svar->drop_lxid == MyProc->vxid.lxid)
+			{
+				needs_validation = true;
+				continue;
+			}
+
+			if (!is_session_variable_valid(svar))
+			{
+				Oid			varid = svar->varid;
+
+				free_session_variable_value(svar);
+				hash_search(sessionvars, &varid, HASH_REMOVE, NULL);
+				svar = NULL;
+			}
+			else
+				svar->is_valid = true;
+		}
+	}
+}
+
 /*
  * Initialize attributes cached in "svar"
  */
@@ -196,6 +314,8 @@ setup_session_variable(SVariable svar, Oid varid)
 	svar->domain_check_extra = NULL;
 	svar->domain_check_extra_lxid = InvalidLocalTransactionId;
 
+	svar->drop_lxid = InvalidTransactionId;
+
 	svar->isnull = true;
 	svar->value = (Datum) 0;
 
@@ -319,22 +439,44 @@ get_session_variable(Oid varid)
 	if (!sessionvars)
 		create_sessionvars_hashtables();
 
+	if (validated_lxid == InvalidLocalTransactionId ||
+		validated_lxid != MyProc->vxid.lxid)
+	{
+		/* throw invalid entries, skip entries dropped by this transaction. */
+		remove_invalid_session_variables();
+
+		/* don't repeat it in this transaction */
+		validated_lxid = MyProc->vxid.lxid;
+	}
+
 	svar = (SVariable) hash_search(sessionvars, &varid,
 								   HASH_ENTER, &found);
 
 	if (found)
 	{
+		/*
+		 * The session variable can be dropped by DROP VARIABLE command,
+		 * but effect of this command can be reverted by ROLLBACK to savepoint,
+		 * so we can work here with readable value in variable marked as invalid.
+		 */
 		if (!svar->is_valid)
 		{
 			/*
 			 * If there was an invalidation message, the variable might still be
 			 * valid, but we have to check with the system catalog.
+			 *
+			 * If we access this session variable, then the variable should be
+			 * possibly validated. The oid should be valid, because related
+			 * session variable is locked already, and remove_invalid_session_variables
+			 * should to remove variables dropped by other transactions.
 			 */
 			if (is_session_variable_valid(svar))
 				svar->is_valid = true;
 			else
-				/* if the value cannot be validated, we have to discard it */
-				free_session_variable_value(svar);
+			{
+				/* we don't expect it */
+				elog(ERROR, "unexpected state of session variable %u", varid);
+			}
 		}
 	}
 	else
@@ -395,6 +537,16 @@ SetSessionVariable(Oid varid, Datum value, bool isNull)
 	if (!sessionvars)
 		create_sessionvars_hashtables();
 
+	if (validated_lxid == InvalidLocalTransactionId ||
+		validated_lxid != MyProc->vxid.lxid)
+	{
+		/* throw invalid entries, skip entries dropped by this transaction */
+		remove_invalid_session_variables();
+
+		/* don't repeat it in this transaction */
+		validated_lxid = MyProc->vxid.lxid;
+	}
+
 	svar = (SVariable) hash_search(sessionvars, &varid,
 								   HASH_ENTER, &found);
 
diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h
index 443afbafd4a..3dab8ae2a48 100644
--- a/src/include/commands/session_variable.h
+++ b/src/include/commands/session_variable.h
@@ -21,6 +21,8 @@
 #include "tcop/cmdtag.h"
 #include "utils/queryenvironment.h"
 
+extern void SessionVariableDropPostprocess(Oid varid);
+
 extern void SetSessionVariable(Oid varid, Datum value, bool isNull);
 extern void SetSessionVariableWithSecurityCheck(Oid varid, Datum value, bool isNull);
 extern Datum GetSessionVariable(Oid varid, bool *isNull, Oid *typid);
diff --git a/src/test/isolation/expected/session-variable.out b/src/test/isolation/expected/session-variable.out
new file mode 100644
index 00000000000..0a5579dc7ce
--- /dev/null
+++ b/src/test/isolation/expected/session-variable.out
@@ -0,0 +1,110 @@
+Parsed test spec with 4 sessions
+
+starting permutation: let val drop val
+step let: LET myvar = 'test';
+step val: SELECT myvar;
+myvar
+-----
+test 
+(1 row)
+
+step drop: DROP VARIABLE myvar;
+step val: SELECT myvar;
+ERROR:  column "myvar" does not exist
+
+starting permutation: let val s1 drop val sr1
+step let: LET myvar = 'test';
+step val: SELECT myvar;
+myvar
+-----
+test 
+(1 row)
+
+step s1: BEGIN;
+step drop: DROP VARIABLE myvar;
+step val: SELECT myvar;
+ERROR:  column "myvar" does not exist
+step sr1: ROLLBACK;
+
+starting permutation: let val dbg drop create dbg val
+step let: LET myvar = 'test';
+step val: SELECT myvar;
+myvar
+-----
+test 
+(1 row)
+
+step dbg: SELECT schema, name, removed FROM pg_session_variables();
+schema|name |removed
+------+-----+-------
+public|myvar|f      
+(1 row)
+
+step drop: DROP VARIABLE myvar;
+step create: CREATE VARIABLE myvar AS text;
+step dbg: SELECT schema, name, removed FROM pg_session_variables();
+schema|name|removed
+------+----+-------
+      |    |t      
+(1 row)
+
+step val: SELECT myvar;
+myvar
+-----
+     
+(1 row)
+
+
+starting permutation: let val s1 dbg drop create dbg val sr1
+step let: LET myvar = 'test';
+step val: SELECT myvar;
+myvar
+-----
+test 
+(1 row)
+
+step s1: BEGIN;
+step dbg: SELECT schema, name, removed FROM pg_session_variables();
+schema|name |removed
+------+-----+-------
+public|myvar|f      
+(1 row)
+
+step drop: DROP VARIABLE myvar;
+step create: CREATE VARIABLE myvar AS text;
+step dbg: SELECT schema, name, removed FROM pg_session_variables();
+schema|name |removed
+------+-----+-------
+public|myvar|f      
+(1 row)
+
+step val: SELECT myvar;
+myvar
+-----
+     
+(1 row)
+
+step sr1: ROLLBACK;
+
+starting permutation: create3 let3 s3 create4 let4 drop4 drop3 inval3 discard sc3 state
+step create3: CREATE VARIABLE myvar3 AS text;
+step let3: LET myvar3 = 'test';
+step s3: BEGIN;
+step create4: CREATE VARIABLE myvar4 AS text;
+step let4: LET myvar4 = 'test';
+step drop4: DROP VARIABLE myvar4;
+step drop3: DROP VARIABLE myvar3;
+step inval3: SELECT COUNT(*) >= 0 FROM pg_foreign_table;
+?column?
+--------
+t       
+(1 row)
+
+step discard: DISCARD VARIABLES;
+step sc3: COMMIT;
+step state: SELECT varname FROM pg_variable;
+varname
+-------
+myvar  
+(1 row)
+
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index 143109aa4da..7453685d046 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -115,3 +115,4 @@ test: serializable-parallel-2
 test: serializable-parallel-3
 test: matview-write-skew
 test: lock-nowait
+test: session-variable
diff --git a/src/test/isolation/specs/session-variable.spec b/src/test/isolation/specs/session-variable.spec
new file mode 100644
index 00000000000..c864fee4006
--- /dev/null
+++ b/src/test/isolation/specs/session-variable.spec
@@ -0,0 +1,50 @@
+# Test session variables memory cleanup for sinval
+
+setup
+{
+    CREATE VARIABLE myvar AS text;
+}
+
+teardown
+{
+    DROP VARIABLE IF EXISTS myvar;
+}
+
+session s1
+step s1		{ BEGIN; }
+step let	{ LET myvar = 'test'; }
+step val	{ SELECT myvar; }
+step dbg	{ SELECT schema, name, removed FROM pg_session_variables(); }
+step sr1	{ ROLLBACK; }
+
+session s2
+step drop		{ DROP VARIABLE myvar; }
+step create		{ CREATE VARIABLE myvar AS text; }
+
+session s3
+step s3			{ BEGIN; }
+step let3		{ LET myvar3 = 'test'; }
+step create4	{ CREATE VARIABLE myvar4 AS text; }
+step let4		{ LET myvar4 = 'test'; }
+step drop4		{ DROP VARIABLE myvar4; }
+step inval3		{ SELECT COUNT(*) >= 0 FROM pg_foreign_table; }
+step discard	{ DISCARD VARIABLES; }
+step sc3		{ COMMIT; }
+step state		{ SELECT varname FROM pg_variable; }
+
+session s4
+step create3	{ CREATE VARIABLE myvar3 AS text; }
+step drop3		{ DROP VARIABLE myvar3; }
+
+# Concurrent drop of a known variable should lead to an error
+permutation let val drop val
+# Same, but with an explicit transaction
+permutation let val s1 drop val sr1
+# Concurrent drop/create of a known variable should lead to empty variable
+permutation let val dbg drop create dbg val
+# Concurrent drop/create of a known variable should lead to empty variable
+# We need a transaction to make sure that we won't accept invalidation when
+# calling the dbg step after the concurrent drop
+permutation let val s1 dbg drop create dbg val sr1
+# test for DISCARD ALL when all internal queues have actions registered
+permutation create3 let3 s3 create4 let4 drop4 drop3 inval3 discard sc3 state
diff --git a/src/test/regress/expected/session_variables.out b/src/test/regress/expected/session_variables.out
index 2c4760fb687..3239f45e8e4 100644
--- a/src/test/regress/expected/session_variables.out
+++ b/src/test/regress/expected/session_variables.out
@@ -1044,3 +1044,221 @@ SELECT count(*) FROM pg_session_variables();
      0
 (1 row)
 
+-- dropped variables should be removed from memory
+-- at the end of transaction or before next usage
+-- of any session variable in next transaction.
+LET var1 = 'Ahoj';
+SELECT name, typname, can_select, can_update FROM pg_session_variables();
+ name |      typname      | can_select | can_update 
+------+-------------------+------------+------------
+ var1 | character varying | t          | t
+(1 row)
+
+DROP VARIABLE var1;
+-- should be zero
+SELECT count(*) FROM pg_session_variables() WHERE NOT removed;
+ count 
+-------
+     0
+(1 row)
+
+-- the content of value should be preserved when variable is dropped
+-- by aborted transaction
+CREATE VARIABLE var1 AS varchar;
+LET var1 = 'Ahoj';
+BEGIN;
+DROP VARIABLE var1;
+-- should fail
+SELECT var1;
+ERROR:  column "var1" does not exist
+LINE 1: SELECT var1;
+               ^
+ROLLBACK;
+-- should be ok
+SELECT var1;
+ var1 
+------
+ Ahoj
+(1 row)
+
+-- another test
+BEGIN;
+DROP VARIABLE var1;
+CREATE VARIABLE var1 AS int;
+LET var1 = 100;
+-- should be ok, result 100
+SELECT var1;
+ var1 
+------
+  100
+(1 row)
+
+ROLLBACK;
+-- should be ok, result 'Ahoj'
+SELECT var1;
+ var1 
+------
+ Ahoj
+(1 row)
+
+DROP VARIABLE var1;
+-- should be zero
+SELECT count(*) FROM pg_session_variables() WHERE NOT removed;
+ count 
+-------
+     0
+(1 row)
+
+BEGIN;
+  CREATE VARIABLE var1 AS int;
+  LET var1 = 100;
+  SELECT var1;
+ var1 
+------
+  100
+(1 row)
+
+  SELECT name, typname, can_select, can_update FROM pg_session_variables();
+ name | typname | can_select | can_update 
+------+---------+------------+------------
+ var1 | integer | t          | t
+(1 row)
+
+  DROP VARIABLE var1;
+COMMIT;
+-- should be zero
+SELECT count(*) FROM pg_session_variables() WHERE NOT removed;
+ count 
+-------
+     0
+(1 row)
+
+BEGIN;
+  CREATE VARIABLE var1 AS int;
+  LET var1 = 100;
+  SELECT var1;
+ var1 
+------
+  100
+(1 row)
+
+  SELECT name, typname, can_select, can_update FROM pg_session_variables();
+ name | typname | can_select | can_update 
+------+---------+------------+------------
+ var1 | integer | t          | t
+(1 row)
+
+  DROP VARIABLE var1;
+COMMIT;
+-- should be zero
+SELECT count(*) FROM pg_session_variables() WHERE NOT removed;
+ count 
+-------
+     0
+(1 row)
+
+CREATE VARIABLE var1 AS int;
+CREATE VARIABLE var2 AS int;
+LET var1 = 10;
+LET var2 = 0;
+BEGIN;
+  SAVEPOINT s1;
+  DROP VARIABLE var1;
+  -- force cleaning by touching another session variable
+  SELECT var2;
+ var2 
+------
+    0
+(1 row)
+
+  ROLLBACK TO s1;
+  SAVEPOINT s2;
+  DROP VARIABLE var1;
+  SELECT var2;
+ var2 
+------
+    0
+(1 row)
+
+  ROLLBACK TO s2;
+COMMIT;
+-- should be ok
+SELECT var1;
+ var1 
+------
+   10
+(1 row)
+
+BEGIN;
+  SAVEPOINT s1;
+  DROP VARIABLE var1;
+  -- force cleaning by touching another session variable
+  SELECT var2;
+ var2 
+------
+    0
+(1 row)
+
+  ROLLBACK TO s1;
+  SAVEPOINT s2;
+  DROP VARIABLE var1;
+  SELECT var2;
+ var2 
+------
+    0
+(1 row)
+
+ROLLBACK;
+-- should be ok
+SELECT var1;
+ var1 
+------
+   10
+(1 row)
+
+BEGIN;
+  SAVEPOINT s1;
+  DROP VARIABLE var1;
+  -- force cleaning by touching another session variable
+  SELECT var2;
+ var2 
+------
+    0
+(1 row)
+
+  SAVEPOINT s2;
+  -- force cleaning by touching another session variable
+  SELECT var2;
+ var2 
+------
+    0
+(1 row)
+
+  ROLLBACK TO s1;
+  -- force cleaning by touching another session variable
+  SELECT var2;
+ var2 
+------
+    0
+(1 row)
+
+COMMIT;
+-- should be ok
+SELECT var1;
+ var1 
+------
+   10
+(1 row)
+
+-- repeated aborted transaction
+BEGIN; DROP VARIABLE var1; ROLLBACK;
+BEGIN; DROP VARIABLE var1; ROLLBACK;
+BEGIN; DROP VARIABLE var1; ROLLBACK;
+-- should be ok
+SELECT var1;
+ var1 
+------
+   10
+(1 row)
+
+DROP VARIABLE var1, var2;
diff --git a/src/test/regress/sql/session_variables.sql b/src/test/regress/sql/session_variables.sql
index 979b9029e58..a77fd785339 100644
--- a/src/test/regress/sql/session_variables.sql
+++ b/src/test/regress/sql/session_variables.sql
@@ -734,3 +734,124 @@ DISCARD VARIABLES;
 
 -- should be zero again
 SELECT count(*) FROM pg_session_variables();
+
+-- dropped variables should be removed from memory
+-- at the end of transaction or before next usage
+-- of any session variable in next transaction.
+
+LET var1 = 'Ahoj';
+SELECT name, typname, can_select, can_update FROM pg_session_variables();
+DROP VARIABLE var1;
+
+-- should be zero
+SELECT count(*) FROM pg_session_variables() WHERE NOT removed;
+
+-- the content of value should be preserved when variable is dropped
+-- by aborted transaction
+CREATE VARIABLE var1 AS varchar;
+LET var1 = 'Ahoj';
+BEGIN;
+DROP VARIABLE var1;
+
+-- should fail
+SELECT var1;
+
+ROLLBACK;
+
+-- should be ok
+SELECT var1;
+
+-- another test
+BEGIN;
+DROP VARIABLE var1;
+CREATE VARIABLE var1 AS int;
+LET var1 = 100;
+-- should be ok, result 100
+SELECT var1;
+ROLLBACK;
+-- should be ok, result 'Ahoj'
+SELECT var1;
+
+DROP VARIABLE var1;
+
+-- should be zero
+SELECT count(*) FROM pg_session_variables() WHERE NOT removed;
+
+BEGIN;
+  CREATE VARIABLE var1 AS int;
+  LET var1 = 100;
+  SELECT var1;
+  SELECT name, typname, can_select, can_update FROM pg_session_variables();
+  DROP VARIABLE var1;
+COMMIT;
+
+-- should be zero
+SELECT count(*) FROM pg_session_variables() WHERE NOT removed;
+
+BEGIN;
+  CREATE VARIABLE var1 AS int;
+  LET var1 = 100;
+  SELECT var1;
+  SELECT name, typname, can_select, can_update FROM pg_session_variables();
+  DROP VARIABLE var1;
+COMMIT;
+
+-- should be zero
+SELECT count(*) FROM pg_session_variables() WHERE NOT removed;
+
+CREATE VARIABLE var1 AS int;
+CREATE VARIABLE var2 AS int;
+LET var1 = 10;
+LET var2 = 0;
+BEGIN;
+  SAVEPOINT s1;
+  DROP VARIABLE var1;
+  -- force cleaning by touching another session variable
+  SELECT var2;
+  ROLLBACK TO s1;
+  SAVEPOINT s2;
+  DROP VARIABLE var1;
+  SELECT var2;
+  ROLLBACK TO s2;
+COMMIT;
+-- should be ok
+SELECT var1;
+
+BEGIN;
+  SAVEPOINT s1;
+  DROP VARIABLE var1;
+  -- force cleaning by touching another session variable
+  SELECT var2;
+  ROLLBACK TO s1;
+  SAVEPOINT s2;
+  DROP VARIABLE var1;
+  SELECT var2;
+ROLLBACK;
+-- should be ok
+SELECT var1;
+
+BEGIN;
+  SAVEPOINT s1;
+  DROP VARIABLE var1;
+  -- force cleaning by touching another session variable
+  SELECT var2;
+
+  SAVEPOINT s2;
+  -- force cleaning by touching another session variable
+  SELECT var2;
+  ROLLBACK TO s1;
+  -- force cleaning by touching another session variable
+  SELECT var2;
+COMMIT;
+-- should be ok
+SELECT var1;
+
+-- repeated aborted transaction
+BEGIN; DROP VARIABLE var1; ROLLBACK;
+BEGIN; DROP VARIABLE var1; ROLLBACK;
+BEGIN; DROP VARIABLE var1; ROLLBACK;
+
+-- should be ok
+SELECT var1;
+
+DROP VARIABLE var1, var2;
-- 
2.47.0



  [text/x-patch] v20241110-0004-DISCARD-VARIABLES.patch (9.6K, 19-v20241110-0004-DISCARD-VARIABLES.patch)
  download | inline diff:
From 19cd69cb9f3504d0d9bf68d470c3456ecfa100e2 Mon Sep 17 00:00:00 2001
From: "[email protected]" <[email protected]>
Date: Sun, 28 Jan 2024 20:54:20 +0100
Subject: [PATCH 04/20] DISCARD VARIABLES

Implementation of DISCARD VARIABLES commands by removing hash table with session variables
and resetting related memory context.
---
 doc/src/sgml/ref/discard.sgml                 | 13 +++-
 src/backend/commands/discard.c                |  6 ++
 src/backend/commands/session_variable.c       | 28 ++++++++-
 src/backend/parser/gram.y                     |  6 ++
 src/backend/tcop/utility.c                    |  3 +
 src/bin/psql/tab-complete.in.c                |  2 +-
 src/include/commands/session_variable.h       |  2 +
 src/include/nodes/parsenodes.h                |  1 +
 src/include/tcop/cmdtaglist.h                 |  1 +
 .../regress/expected/session_variables.out    | 63 +++++++++++++++++++
 src/test/regress/sql/session_variables.sql    | 36 +++++++++++
 11 files changed, 158 insertions(+), 3 deletions(-)

diff --git a/doc/src/sgml/ref/discard.sgml b/doc/src/sgml/ref/discard.sgml
index bf44c523cac..61b967f9c9b 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>
 
@@ -66,6 +66,16 @@ DISCARD { ALL | PLANS | SEQUENCES | TEMPORARY | TEMP }
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+    <term><literal>VARIABLES</literal></term>
+    <listitem>
+     <para>
+      Resets the value of all session variables. If a variable
+      is later reused, it is re-initialized to <literal>NULL</literal>.
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry>
     <term><literal>TEMPORARY</literal> or <literal>TEMP</literal></term>
     <listitem>
@@ -93,6 +103,7 @@ SELECT pg_advisory_unlock_all();
 DISCARD PLANS;
 DISCARD TEMP;
 DISCARD SEQUENCES;
+DISCARD VARIABLES;
 </programlisting></para>
     </listitem>
    </varlistentry>
diff --git a/src/backend/commands/discard.c b/src/backend/commands/discard.c
index 92d983ac748..d06ebaba6f4 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/session_variable.h"
 #include "commands/sequence.h"
 #include "utils/guc.h"
 #include "utils/portal.h"
@@ -48,6 +49,10 @@ DiscardCommand(DiscardStmt *stmt, bool isTopLevel)
 			ResetTempTableNamespace();
 			break;
 
+		case DISCARD_VARIABLES:
+			ResetSessionVariables();
+			break;
+
 		default:
 			elog(ERROR, "unrecognized DISCARD target: %d", stmt->target);
 	}
@@ -75,4 +80,5 @@ DiscardAll(bool isTopLevel)
 	ResetPlanCache();
 	ResetTempTableNamespace();
 	ResetSequenceCaches();
+	ResetSessionVariables();
 }
diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c
index 13ab9575fa9..668b8189a6c 100644
--- a/src/backend/commands/session_variable.c
+++ b/src/backend/commands/session_variable.c
@@ -94,7 +94,13 @@ pg_variable_cache_callback(Datum arg, int cacheid, uint32 hashvalue)
 
 	elog(DEBUG1, "pg_variable_cache_callback %u %u", cacheid, hashvalue);
 
-	Assert(sessionvars);
+	/*
+	 * There is no guarantee of session variables being initialized, even when
+	 * receiving an invalidation callback, as DISCARD [ ALL | VARIABLES ]
+	 * destroys the hash table entirely.
+	 */
+	if (!sessionvars)
+		return;
 
 	/*
 	 * If the hashvalue is not specified, we have to recheck all currently
@@ -661,3 +667,23 @@ pg_session_variables(PG_FUNCTION_ARGS)
 
 	return (Datum) 0;
 }
+
+/*
+ * Fast drop of the complete content of the session variables hash table, and
+ * cleanup of any list that wouldn't be relevant anymore.
+ * This is used by the DISCARD VARIABLES (and DISCARD ALL) command.
+ */
+void
+ResetSessionVariables(void)
+{
+	/* destroy hash table and reset related memory context */
+	if (sessionvars)
+	{
+		hash_destroy(sessionvars);
+		sessionvars = NULL;
+	}
+
+	/* release memory allocated by session variables */
+	if (SVariableMemoryContext != NULL)
+		MemoryContextReset(SVariableMemoryContext);
+}
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 8ae368c513c..aad47305f20 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -2077,7 +2077,13 @@ DiscardStmt:
 					n->target = DISCARD_SEQUENCES;
 					$$ = (Node *) n;
 				}
+			| DISCARD VARIABLES
+				{
+					DiscardStmt *n = makeNode(DiscardStmt);
 
+					n->target = DISCARD_VARIABLES;
+					$$ = (Node *) n;
+				}
 		;
 
 
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 69d2f7e1ced..970451460c2 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -2962,6 +2962,9 @@ CreateCommandTag(Node *parsetree)
 				case DISCARD_SEQUENCES:
 					tag = CMDTAG_DISCARD_SEQUENCES;
 					break;
+				case DISCARD_VARIABLES:
+					tag = CMDTAG_DISCARD_VARIABLES;
+					break;
 				default:
 					tag = CMDTAG_UNKNOWN;
 			}
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index ec572815c94..3ef4776d98a 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -4083,7 +4083,7 @@ match_previous_words(int pattern_id,
 
 /* DISCARD */
 	else if (Matches("DISCARD"))
-		COMPLETE_WITH("ALL", "PLANS", "SEQUENCES", "TEMP");
+		COMPLETE_WITH("ALL", "PLANS", "SEQUENCES", "TEMP", "VARIABLES");
 
 /* DO */
 	else if (Matches("DO"))
diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h
index b3f03c65827..443afbafd4a 100644
--- a/src/include/commands/session_variable.h
+++ b/src/include/commands/session_variable.h
@@ -29,4 +29,6 @@ extern Datum GetSessionVariableWithTypeCheck(Oid varid, bool *isNull, Oid expect
 extern void ExecuteLetStmt(ParseState *pstate, LetStmt *stmt, ParamListInfo params,
 						   QueryEnvironment *queryEnv, QueryCompletion *qc);
 
+extern void ResetSessionVariables(void);
+
 #endif
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 778db6ad4f2..04ab22bd492 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -3988,6 +3988,7 @@ typedef enum DiscardMode
 	DISCARD_PLANS,
 	DISCARD_SEQUENCES,
 	DISCARD_TEMP,
+	DISCARD_VARIABLES,
 } DiscardMode;
 
 typedef struct DiscardStmt
diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h
index a921af2486f..bd7964aea6e 100644
--- a/src/include/tcop/cmdtaglist.h
+++ b/src/include/tcop/cmdtaglist.h
@@ -135,6 +135,7 @@ PG_CMDTAG(CMDTAG_DISCARD_ALL, "DISCARD ALL", false, false, false)
 PG_CMDTAG(CMDTAG_DISCARD_PLANS, "DISCARD PLANS", false, false, false)
 PG_CMDTAG(CMDTAG_DISCARD_SEQUENCES, "DISCARD SEQUENCES", false, false, false)
 PG_CMDTAG(CMDTAG_DISCARD_TEMP, "DISCARD TEMP", false, false, false)
+PG_CMDTAG(CMDTAG_DISCARD_VARIABLES, "DISCARD VARIABLES", false, false, false)
 PG_CMDTAG(CMDTAG_DO, "DO", false, false, false)
 PG_CMDTAG(CMDTAG_DROP_ACCESS_METHOD, "DROP ACCESS METHOD", true, false, false)
 PG_CMDTAG(CMDTAG_DROP_AGGREGATE, "DROP AGGREGATE", true, false, false)
diff --git a/src/test/regress/expected/session_variables.out b/src/test/regress/expected/session_variables.out
index a2cb35e3d7d..2c4760fb687 100644
--- a/src/test/regress/expected/session_variables.out
+++ b/src/test/regress/expected/session_variables.out
@@ -981,3 +981,66 @@ DROP VARIABLE :"DBNAME".:"DBNAME".b;
 DROP VARIABLE :"DBNAME".:"DBNAME".:"DBNAME";
 DROP SCHEMA :"DBNAME";
 RESET search_path;
+-- memory cleaning by DISCARD command
+CREATE VARIABLE var1 AS varchar;
+LET var1 = 'Hello';
+SELECT var1;
+ var1  
+-------
+ Hello
+(1 row)
+
+DISCARD ALL;
+SELECT var1;
+ var1 
+------
+ 
+(1 row)
+
+LET var1 = 'AHOJ';
+SELECT var1;
+ var1 
+------
+ AHOJ
+(1 row)
+
+DISCARD VARIABLES;
+SELECT var1;
+ var1 
+------
+ 
+(1 row)
+
+DROP VARIABLE var1;
+-- initial test of debug pg_session_variables function
+-- should be zero now
+DISCARD VARIABLES;
+SELECT count(*) FROM pg_session_variables();
+ count 
+-------
+     0
+(1 row)
+
+CREATE VARIABLE var1 AS varchar;
+-- should be zero still
+SELECT count(*) FROM pg_session_variables();
+ count 
+-------
+     0
+(1 row)
+
+LET var1 = 'AHOJ';
+SELECT name, typname, can_select, can_update FROM pg_session_variables();
+ name |      typname      | can_select | can_update 
+------+-------------------+------------+------------
+ var1 | character varying | t          | t
+(1 row)
+
+DISCARD VARIABLES;
+-- should be zero again
+SELECT count(*) FROM pg_session_variables();
+ count 
+-------
+     0
+(1 row)
+
diff --git a/src/test/regress/sql/session_variables.sql b/src/test/regress/sql/session_variables.sql
index 6445479d22d..979b9029e58 100644
--- a/src/test/regress/sql/session_variables.sql
+++ b/src/test/regress/sql/session_variables.sql
@@ -698,3 +698,39 @@ DROP VARIABLE :"DBNAME".:"DBNAME".:"DBNAME";
 DROP SCHEMA :"DBNAME";
 
 RESET search_path;
+
+-- memory cleaning by DISCARD command
+CREATE VARIABLE var1 AS varchar;
+LET var1 = 'Hello';
+SELECT var1;
+
+DISCARD ALL;
+SELECT var1;
+
+LET var1 = 'AHOJ';
+SELECT var1;
+
+DISCARD VARIABLES;
+SELECT var1;
+
+DROP VARIABLE var1;
+
+-- initial test of debug pg_session_variables function
+-- should be zero now
+DISCARD VARIABLES;
+
+SELECT count(*) FROM pg_session_variables();
+
+CREATE VARIABLE var1 AS varchar;
+
+-- should be zero still
+SELECT count(*) FROM pg_session_variables();
+
+LET var1 = 'AHOJ';
+
+SELECT name, typname, can_select, can_update FROM pg_session_variables();
+
+DISCARD VARIABLES;
+
+-- should be zero again
+SELECT count(*) FROM pg_session_variables();
-- 
2.47.0



  [text/x-patch] v20241110-0003-function-pg_session_variables-for-cleaning-tests.patch (4.3K, 20-v20241110-0003-function-pg_session_variables-for-cleaning-tests.patch)
  download | inline diff:
From a0210f5f45ec28d135c7654fae5af47c0fdd6031 Mon Sep 17 00:00:00 2001
From: "[email protected]" <[email protected]>
Date: Fri, 19 Jan 2024 20:01:56 +0100
Subject: [PATCH 03/20] function pg_session_variables for cleaning tests

This is a function designed for testing and debugging.  It returns the
content of sessionvars as-is, and can therefore display entries about
session variables that were dropped but for which this backend didn't
process the shared invalidations yet.
---
 src/backend/commands/session_variable.c | 92 +++++++++++++++++++++++++
 src/include/catalog/pg_proc.dat         |  8 +++
 2 files changed, 100 insertions(+)

diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c
index 548d9835c0d..13ab9575fa9 100644
--- a/src/backend/commands/session_variable.c
+++ b/src/backend/commands/session_variable.c
@@ -569,3 +569,95 @@ ExecuteLetStmt(ParseState *pstate,
 
 	PopActiveSnapshot();
 }
+
+/*
+ * pg_session_variables - designed for testing
+ *
+ * This is a function designed for testing and debugging.  It returns the
+ * content of session variables as-is, and can therefore display data about
+ * session variables that were dropped, but for which this backend didn't
+ * process the shared invalidations yet.
+ */
+Datum
+pg_session_variables(PG_FUNCTION_ARGS)
+{
+#define NUM_PG_SESSION_VARIABLES_ATTS 8
+
+	InitMaterializedSRF(fcinfo, 0);
+
+	if (sessionvars)
+	{
+		ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+		HASH_SEQ_STATUS status;
+		SVariable	svar;
+
+		hash_seq_init(&status, sessionvars);
+
+		while ((svar = (SVariable) hash_seq_search(&status)) != NULL)
+		{
+			Datum		values[NUM_PG_SESSION_VARIABLES_ATTS];
+			bool		nulls[NUM_PG_SESSION_VARIABLES_ATTS];
+			HeapTuple	tp;
+			bool		var_is_valid = false;
+
+			memset(values, 0, sizeof(values));
+			memset(nulls, 0, sizeof(nulls));
+
+			values[0] = ObjectIdGetDatum(svar->varid);
+			values[3] = ObjectIdGetDatum(svar->typid);
+
+			/*
+			 * It is possible that the variable has been dropped from the
+			 * catalog, but not yet purged from the hash table.
+			 */
+			tp = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(svar->varid));
+
+			if (HeapTupleIsValid(tp))
+			{
+				Form_pg_variable varform = (Form_pg_variable) GETSTRUCT(tp);
+
+				/*
+				 * It is also possible that a variable has been dropped and
+				 * someone created a new variable with the same object ID.  Use
+				 * the catalog information only if that is not the case.
+				 */
+				if (svar->create_lsn == varform->varcreate_lsn)
+				{
+					values[1] = CStringGetTextDatum(
+													get_namespace_name(varform->varnamespace));
+
+					values[2] = CStringGetTextDatum(NameStr(varform->varname));
+					values[4] = CStringGetTextDatum(format_type_be(svar->typid));
+					values[5] = BoolGetDatum(false);
+
+					values[6] = BoolGetDatum(
+											 object_aclcheck(VariableRelationId, svar->varid,
+															 GetUserId(), ACL_SELECT) == ACLCHECK_OK);
+
+					values[7] = BoolGetDatum(
+											 object_aclcheck(VariableRelationId, svar->varid,
+															 GetUserId(), ACL_UPDATE) == ACLCHECK_OK);
+
+					var_is_valid = true;
+				}
+
+				ReleaseSysCache(tp);
+			}
+
+			/* if there is no matching catalog entry, return null values */
+			if (!var_is_valid)
+			{
+				nulls[1] = true;
+				nulls[2] = true;
+				nulls[4] = true;
+				values[5] = BoolGetDatum(true);
+				nulls[6] = true;
+				nulls[7] = true;
+			}
+
+			tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls);
+		}
+	}
+
+	return (Datum) 0;
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index d676ee83e41..89a372c65ca 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12395,4 +12395,12 @@
   proargtypes => 'int2',
   prosrc => 'gist_stratnum_identity' },
 
+# Session variables support
+{ oid => '8488', descr => 'list of used session variables',
+  proname => 'pg_session_variables', prorows => '1000', proretset => 't',
+  provolatile => 's', proparallel => 'r', prorettype => 'record',
+  proargtypes => '', proallargtypes => '{oid,text,text,oid,text,bool,bool,bool}',
+  proargmodes => '{o,o,o,o,o,o,o,o}',
+  proargnames => '{varid,schema,name,typid,typname,removed,can_select,can_update}',
+  prosrc => 'pg_session_variables' },
 ]
-- 
2.47.0



  [text/x-patch] v20241110-0002-Storage-for-session-variables-and-SQL-interface.patch (145.2K, 21-v20241110-0002-Storage-for-session-variables-and-SQL-interface.patch)
  download | inline diff:
From cef94a6d42e938c503234fbd6f35784722551235 Mon Sep 17 00:00:00 2001
From: "[email protected]" <[email protected]>
Date: Thu, 6 Jul 2023 08:29:21 +0200
Subject: [PATCH 02/20] Storage for session variables and SQL interface

Session variables are stored in session memory in a dedicated hash table.
They are set by the LET command and read by the SELECT command.
The access rights should be checked.

The identifiers of session variables should always be shadowed by possible
column identifiers: we don't want to break an application by creating some
badly named session variable.

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

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

Implementations of EXPLAIN LET and PREPARE LET statements
are in separate patches (for better readability)
---
 doc/src/sgml/catalogs.sgml                    |  13 +
 doc/src/sgml/ddl.sgml                         |  32 +
 doc/src/sgml/parallel.sgml                    |   6 +
 doc/src/sgml/plpgsql.sgml                     |  14 +
 doc/src/sgml/ref/allfiles.sgml                |   1 +
 doc/src/sgml/ref/alter_variable.sgml          |   1 +
 doc/src/sgml/ref/create_variable.sgml         |   1 +
 doc/src/sgml/ref/drop_variable.sgml           |   1 +
 doc/src/sgml/ref/let.sgml                     |  96 ++
 doc/src/sgml/reference.sgml                   |   1 +
 src/backend/catalog/dependency.c              |   5 +
 src/backend/catalog/namespace.c               | 315 ++++++
 src/backend/catalog/pg_variable.c             |   2 +
 src/backend/commands/Makefile                 |   1 +
 src/backend/commands/meson.build              |   1 +
 src/backend/commands/prepare.c                |   8 +
 src/backend/commands/session_variable.c       | 571 +++++++++++
 src/backend/executor/Makefile                 |   1 +
 src/backend/executor/execExpr.c               |  36 +
 src/backend/executor/execMain.c               |  57 ++
 src/backend/executor/meson.build              |   1 +
 src/backend/executor/svariableReceiver.c      | 201 ++++
 src/backend/nodes/nodeFuncs.c                 |  10 +
 src/backend/optimizer/plan/planner.c          |   9 +
 src/backend/optimizer/plan/setrefs.c          | 135 ++-
 src/backend/optimizer/prep/prepjointree.c     |   3 +
 src/backend/optimizer/util/clauses.c          |  35 +-
 src/backend/parser/analyze.c                  | 271 ++++-
 src/backend/parser/gram.y                     |  49 +-
 src/backend/parser/parse_agg.c                |   9 +
 src/backend/parser/parse_expr.c               | 220 ++++-
 src/backend/parser/parse_func.c               |   2 +
 src/backend/tcop/dest.c                       |   7 +
 src/backend/tcop/pquery.c                     |   3 +
 src/backend/tcop/utility.c                    |  16 +
 src/backend/utils/adt/ruleutils.c             |  46 +
 src/backend/utils/cache/plancache.c           |  41 +-
 src/backend/utils/fmgr/fmgr.c                 |  10 +-
 src/bin/psql/tab-complete.in.c                |  12 +-
 src/include/catalog/namespace.h               |   2 +
 src/include/catalog/pg_variable.h             |   8 +
 src/include/commands/session_variable.h       |  32 +
 src/include/executor/execdesc.h               |   4 +
 src/include/executor/svariableReceiver.h      |  22 +
 src/include/nodes/execnodes.h                 |  16 +
 src/include/nodes/parsenodes.h                |  17 +
 src/include/nodes/pathnodes.h                 |   5 +
 src/include/nodes/plannodes.h                 |   4 +-
 src/include/nodes/primnodes.h                 |   6 +
 src/include/optimizer/planmain.h              |   2 +
 src/include/parser/kwlist.h                   |   1 +
 src/include/parser/parse_node.h               |   3 +
 src/include/tcop/cmdtaglist.h                 |   1 +
 src/include/tcop/dest.h                       |   1 +
 src/pl/plpgsql/src/pl_exec.c                  |   3 +-
 .../regress/expected/session_variables.out    | 925 ++++++++++++++++++
 src/test/regress/sql/session_variables.sql    | 639 ++++++++++++
 src/tools/pgindent/typedefs.list              |   5 +
 58 files changed, 3916 insertions(+), 23 deletions(-)
 create mode 100644 doc/src/sgml/ref/let.sgml
 create mode 100644 src/backend/commands/session_variable.c
 create mode 100644 src/backend/executor/svariableReceiver.c
 create mode 100644 src/include/commands/session_variable.h
 create mode 100644 src/include/executor/svariableReceiver.h

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 5709c9cdbd4..aa796876e97 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -9784,6 +9784,19 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>varcreate_lsn</structfield> <type>XLogRecPtr</type>
+      </para>
+      <para>
+       LSN of the transaction where the variable was created.
+       <structfield>varcreate_lsn</structfield> and
+       <structfield>oid</structfield> together form the all-time unique
+       identifier (<structfield>oid</structfield> alone is not enough, since
+       object identifiers can get reused).
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>varname</structfield> <type>name</type>
diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml
index 6ae36f832f7..d4639b27795 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -5316,6 +5316,38 @@ EXPLAIN SELECT count(*) FROM measurement WHERE logdate &gt;= DATE '2008-01-01';
     VARIABLE</command> command.
    </para>
 
+   <para>
+    The value of a session variable is set with the SQL statement
+    <command>LET</command>.  The value of a session variable can be retrieved
+    with the SQL statement <command>SELECT</command>.
+<programlisting>
+CREATE VARIABLE var1 AS date;
+LET var1 = current_date;
+SELECT var1;
+</programlisting>
+
+    or
+
+<programlisting>
+CREATE VARIABLE public.current_user_id AS integer;
+GRANT READ ON VARIABLE public.current_user_id TO PUBLIC;
+LET current_user_id = (SELECT id FROM users WHERE usename = session_user);
+SELECT current_user_id;
+</programlisting>
+   </para>
+
+   <para>
+    The value of a session variable is local to the current session. Retrieving
+    a variable's value returns a <literal>NULL</literal>, unless its value has
+    been set to something else in the current session using the
+    <command>LET</command> command.  Session variables are not transactional:
+    any changes made to the value of a session variable in a transaction won't
+    be undone if the transaction is rolled back (just like variables in
+    procedural languages).  Session variables themselves are persistent, but
+    their values are neither persistent nor shared (like the content of
+    temporary tables).
+   </para>
+
    <para>
     Inside a query or an expression, a session variable can be
     <quote>shadowed</quote> by a column with the same name.  Similarly, the
diff --git a/doc/src/sgml/parallel.sgml b/doc/src/sgml/parallel.sgml
index 1ce9abf86f5..683dede6adc 100644
--- a/doc/src/sgml/parallel.sgml
+++ b/doc/src/sgml/parallel.sgml
@@ -515,6 +515,12 @@ EXPLAIN SELECT * FROM pgbench_accounts WHERE filler LIKE '%x%';
         Plan nodes that reference a correlated <literal>SubPlan</literal>.
       </para>
     </listitem>
+
+    <listitem>
+      <para>
+        Plan nodes that use a session variable.
+      </para>
+    </listitem>
   </itemizedlist>
 
  <sect2 id="parallel-labeling">
diff --git a/doc/src/sgml/plpgsql.sgml b/doc/src/sgml/plpgsql.sgml
index 78e4983139b..bfbff9ab74e 100644
--- a/doc/src/sgml/plpgsql.sgml
+++ b/doc/src/sgml/plpgsql.sgml
@@ -6034,6 +6034,20 @@ $$ LANGUAGE plpgsql STRICT IMMUTABLE;
 </programlisting>
     </para>
    </sect3>
+
+   <sect3 id="plpgsql-porting-package-variables">
+    <title><command>Packages and package variables</command></title>
+
+    <para>
+     The <application>PL/pgSQL</application> language has no packages, and
+     therefore no package variables or package constants.
+     You can consider translating an Oracle package into a schema in
+     <productname>PostgreSQL</productname>.  Package functions and procedures
+     would then become functions and procedures in that schema, and package
+     variables could be translated to session variables in that schema.
+     (see <xref linkend="ddl-session-variables"/>).
+    </para>
+   </sect3>
   </sect2>
 
   <sect2 id="plpgsql-porting-appendix">
diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml
index 2f67de3e21b..cc3bd5ab540 100644
--- a/doc/src/sgml/ref/allfiles.sgml
+++ b/doc/src/sgml/ref/allfiles.sgml
@@ -158,6 +158,7 @@ Complete list of usable sgml source files in this directory.
 <!ENTITY grant              SYSTEM "grant.sgml">
 <!ENTITY importForeignSchema SYSTEM "import_foreign_schema.sgml">
 <!ENTITY insert             SYSTEM "insert.sgml">
+<!ENTITY let                SYSTEM "let.sgml">
 <!ENTITY listen             SYSTEM "listen.sgml">
 <!ENTITY load               SYSTEM "load.sgml">
 <!ENTITY lock               SYSTEM "lock.sgml">
diff --git a/doc/src/sgml/ref/alter_variable.sgml b/doc/src/sgml/ref/alter_variable.sgml
index d87570e7d8b..d2036351e53 100644
--- a/doc/src/sgml/ref/alter_variable.sgml
+++ b/doc/src/sgml/ref/alter_variable.sgml
@@ -173,6 +173,7 @@ ALTER VARIABLE boo SET SCHEMA private;
   <simplelist type="inline">
    <member><xref linkend="sql-createvariable"/></member>
    <member><xref linkend="sql-dropvariable"/></member>
+   <member><xref linkend="sql-let"/></member>
   </simplelist>
  </refsect1>
 </refentry>
diff --git a/doc/src/sgml/ref/create_variable.sgml b/doc/src/sgml/ref/create_variable.sgml
index ec165ab96fc..05bf67e3411 100644
--- a/doc/src/sgml/ref/create_variable.sgml
+++ b/doc/src/sgml/ref/create_variable.sgml
@@ -145,6 +145,7 @@ SELECT var1;
   <simplelist type="inline">
    <member><xref linkend="sql-altervariable"/></member>
    <member><xref linkend="sql-dropvariable"/></member>
+   <member><xref linkend="sql-let"/></member>
   </simplelist>
  </refsect1>
 
diff --git a/doc/src/sgml/ref/drop_variable.sgml b/doc/src/sgml/ref/drop_variable.sgml
index 5bdb3560f0b..67988b5fcd8 100644
--- a/doc/src/sgml/ref/drop_variable.sgml
+++ b/doc/src/sgml/ref/drop_variable.sgml
@@ -111,6 +111,7 @@ DROP VARIABLE var1;
   <simplelist type="inline">
    <member><xref linkend="sql-altervariable"/></member>
    <member><xref linkend="sql-createvariable"/></member>
+   <member><xref linkend="sql-let"/></member>
   </simplelist>
  </refsect1>
 
diff --git a/doc/src/sgml/ref/let.sgml b/doc/src/sgml/ref/let.sgml
new file mode 100644
index 00000000000..f6640576bee
--- /dev/null
+++ b/doc/src/sgml/ref/let.sgml
@@ -0,0 +1,96 @@
+<!--
+doc/src/sgml/ref/let.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-let">
+ <indexterm zone="sql-let">
+  <primary>LET</primary>
+ </indexterm>
+
+ <indexterm>
+  <primary>session variable</primary>
+  <secondary>changing</secondary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>LET</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>LET</refname>
+  <refpurpose>change a session variable's value</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+LET <replaceable class="parameter">session_variable</replaceable> = <replaceable class="parameter">sql_expression</replaceable>
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   The <command>LET</command> command assigns a value to the specified session
+   variable.
+  </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+  <variablelist>
+   <varlistentry>
+    <term><literal>session_variable</literal></term>
+    <listitem>
+     <para>
+      The name of the session variable.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>sql_expression</literal></term>
+    <listitem>
+     <para>
+      An arbitrary SQL expression.  The result must be of a data type that can
+      be cast to the type of the session variable in an assignment.
+     </para>
+    </listitem>
+   </varlistentry>
+
+  </variablelist>
+
+  <para>
+   Example:
+<programlisting>
+CREATE VARIABLE myvar AS integer;
+LET myvar = 10;
+LET myvar = (SELECT sum(val) FROM tab);
+</programlisting>
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   The <command>LET</command> is a <productname>PostgreSQL</productname>
+   extension.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-altervariable"/></member>
+   <member><xref linkend="sql-createvariable"/></member>
+   <member><xref linkend="sql-dropvariable"/></member>
+  </simplelist>
+ </refsect1>
+</refentry>
diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml
index 25578f3946c..13e4adc5df3 100644
--- a/doc/src/sgml/reference.sgml
+++ b/doc/src/sgml/reference.sgml
@@ -186,6 +186,7 @@
    &grant;
    &importForeignSchema;
    &insert;
+   &let;
    &listen;
    &load;
    &lock;
diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c
index c9d686665de..148719e490e 100644
--- a/src/backend/catalog/dependency.c
+++ b/src/backend/catalog/dependency.c
@@ -1864,6 +1864,11 @@ find_expr_references_walker(Node *node,
 	{
 		Param	   *param = (Param *) node;
 
+		/* a variable parameter depends on the session variable */
+		if (param->paramkind == PARAM_VARIABLE)
+			add_object_address(VariableRelationId, param->paramvarid, 0,
+							   context->addrs);
+
 		/* A parameter must depend on the parameter's datatype */
 		add_object_address(TypeRelationId, param->paramtype, 0,
 						   context->addrs);
diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c
index 86e2258657d..487353ef2eb 100644
--- a/src/backend/catalog/namespace.c
+++ b/src/backend/catalog/namespace.c
@@ -3412,6 +3412,321 @@ LookupVariable(const char *nspname,
 	return varoid;
 }
 
+/*
+ * The input list contains names with indirection expressions used as the left
+ * part of LET statement. The following routine returns a new list with only
+ * initial strings (names) - without indirection expressions.
+ */
+List *
+NamesFromList(List *names)
+{
+	ListCell   *l;
+	List	   *result = NIL;
+
+	foreach(l, names)
+	{
+		Node	   *n = lfirst(l);
+
+		if (IsA(n, String))
+		{
+			result = lappend(result, n);
+		}
+		else
+			break;
+	}
+
+	return result;
+}
+
+/* -----
+ * IdentifyVariable - try to find a variable from a list of identifiers
+ *
+ * Returns the OID of the variable found, or InvalidOid.
+ *
+ * "names" is a list of up to four identifiers; possible meanings are:
+ * - variable  (searched on the search_path)
+ * - schema.variable
+ * - variable.attribute  (searched on the search_path)
+ * - schema.variable.attribute
+ * - database.schema.variable
+ * - database.schema.variable.attribute
+ *
+ * If there is more than one way to identify a variable, "not_unique" will be
+ * set to true.
+ *
+ * Unless "noerror" is true, an error is raised if there are more than four
+ *  identifiers in the list, or if the named database is not the current one.
+ * This is useful if we want to identify a shadowed variable.
+ *
+ * If an attribute is identified, it is stored in "attrname", otherwise the
+ * parameter is set to NULL.
+ *
+ * The identified session variable will be locked with an AccessShareLock.
+ * -----
+ */
+Oid
+IdentifyVariable(List *names, char **attrname, bool *not_unique, bool noerror)
+{
+	Oid			varid = InvalidOid;
+	Oid			old_varid = InvalidOid;
+	uint64		inval_count;
+	bool		retry = false;
+
+	/*
+	 * DDL operations can change the results of a name lookup.  Since all such
+	 * operations will generate invalidation messages, we keep track of
+	 * whether any such messages show up while we're performing the operation,
+	 * and retry until either (1) no more invalidation messages show up or (2)
+	 * the answer doesn't change.
+	 */
+	for (;;)
+	{
+		Node	   *field1 = NULL;
+		Node	   *field2 = NULL;
+		Node	   *field3 = NULL;
+		Node	   *field4 = NULL;
+		char	   *a = NULL;
+		char	   *b = NULL;
+		char	   *c = NULL;
+		char	   *d = NULL;
+		Oid			varoid_without_attr = InvalidOid;
+		Oid			varoid_with_attr = InvalidOid;
+
+		*not_unique = false;
+		*attrname = NULL;
+		varid = InvalidOid;
+
+		inval_count = SharedInvalidMessageCounter;
+
+		switch (list_length(names))
+		{
+			case 1:
+				field1 = linitial(names);
+
+				Assert(IsA(field1, String));
+
+				varid = LookupVariable(NULL, strVal(field1), true);
+				break;
+
+			case 2:
+				field1 = linitial(names);
+				field2 = lsecond(names);
+
+				Assert(IsA(field1, String));
+				a = strVal(field1);
+
+				if (IsA(field2, String))
+				{
+					/* when both fields are of string type */
+					b = strVal(field2);
+
+					/*
+					 * a.b can mean "schema"."variable" or "variable"."field".
+					 * Check both variants, and returns InvalidOid with
+					 * not_unique flag, when both interpretations are
+					 * possible.
+					 */
+					varoid_without_attr = LookupVariable(a, b, true);
+					varoid_with_attr = LookupVariable(NULL, a, true);
+				}
+				else
+				{
+					/* the last field of list can be star too */
+					Assert(IsA(field2, A_Star));
+
+					/*
+					 * In this case, the field1 should be variable name. But
+					 * direct unboxing of composite session variables is not
+					 * supported now, and then we don't need to try lookup
+					 * related variable.
+					 *
+					 * Unboxing is supported by syntax (var).*
+					 */
+					return InvalidOid;
+				}
+
+				if (OidIsValid(varoid_without_attr) && OidIsValid(varoid_with_attr))
+				{
+					*not_unique = true;
+					varid = varoid_without_attr;
+				}
+				else if (OidIsValid(varoid_without_attr))
+				{
+					varid = varoid_without_attr;
+				}
+				else if (OidIsValid(varoid_with_attr))
+				{
+					*attrname = b;
+					varid = varoid_with_attr;
+				}
+				break;
+
+			case 3:
+				{
+					bool		field1_is_catalog = false;
+
+					field1 = linitial(names);
+					field2 = lsecond(names);
+					field3 = lthird(names);
+
+					Assert(IsA(field1, String));
+					Assert(IsA(field2, String));
+
+					a = strVal(field1);
+					b = strVal(field2);
+
+					if (IsA(field3, String))
+					{
+						c = strVal(field3);
+
+						/*
+						 * a.b.c can mean catalog.schema.variable or
+						 * schema.variable.field.
+						 *
+						 * Check both variants, and set not_unique flag, when
+						 * both interpretations are possible.
+						 *
+						 * When third node is star, only possible
+						 * interpretation is schema.variable.*, but this
+						 * pattern is not supported now.
+						 */
+						varoid_with_attr = LookupVariable(a, b, true);
+
+						/*
+						 * check pattern catalog.schema.variable only when
+						 * there is possibility to success.
+						 */
+						if (strcmp(a, get_database_name(MyDatabaseId)) == 0)
+						{
+							field1_is_catalog = true;
+							varoid_without_attr = LookupVariable(b, c, true);
+						}
+					}
+					else
+					{
+						Assert(IsA(field3, A_Star));
+						return InvalidOid;
+					}
+
+					if (OidIsValid(varoid_without_attr) && OidIsValid(varoid_with_attr))
+					{
+						*not_unique = true;
+						varid = varoid_without_attr;
+					}
+					else if (OidIsValid(varoid_without_attr))
+					{
+						varid = varoid_without_attr;
+					}
+					else if (OidIsValid(varoid_with_attr))
+					{
+						*attrname = c;
+						varid = varoid_with_attr;
+					}
+
+					/*
+					 * When we didn't find variable, we can (when it is
+					 * allowed) raise cross-database reference error.
+					 */
+					if (!OidIsValid(varid) && !noerror && !field1_is_catalog)
+						ereport(ERROR,
+								(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+								 errmsg("cross-database references are not implemented: %s",
+										NameListToString(names))));
+				}
+				break;
+
+			case 4:
+				{
+					field1 = linitial(names);
+					field2 = lsecond(names);
+					field3 = lthird(names);
+					field4 = lfourth(names);
+
+					Assert(IsA(field1, String));
+					Assert(IsA(field2, String));
+					Assert(IsA(field3, String));
+
+					a = strVal(field1);
+					b = strVal(field2);
+					c = strVal(field3);
+
+					/*
+					 * In this case, "a" is used as catalog name - check it.
+					 */
+					if (strcmp(a, get_database_name(MyDatabaseId)) != 0)
+					{
+						if (!noerror)
+							ereport(ERROR,
+									(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+									 errmsg("cross-database references are not implemented: %s",
+											NameListToString(names))));
+					}
+
+					if (IsA(field4, String))
+					{
+						d = strVal(field4);
+					}
+					else
+					{
+						Assert(IsA(field4, A_Star));
+						return InvalidOid;
+					}
+
+					*attrname = d;
+					varid = LookupVariable(b, c, true);
+				}
+				break;
+
+			default:
+				if (!noerror)
+					ereport(ERROR,
+							(errcode(ERRCODE_SYNTAX_ERROR),
+							 errmsg("improper qualified name (too many dotted names): %s",
+									NameListToString(names))));
+				return InvalidOid;
+		}
+
+		/*
+		 * If, upon retry, we get back the same OID we did last time, then the
+		 * invalidation messages we processed did not change the final answer.
+		 * So we're done.
+		 *
+		 * If we got a different OID, we've locked the variable that used to
+		 * have this name rather than the one that does now.  So release the
+		 * lock.
+		 */
+		if (retry)
+		{
+			if (old_varid == varid)
+				break;
+
+			if (OidIsValid(old_varid))
+				UnlockDatabaseObject(VariableRelationId, old_varid, 0, AccessShareLock);
+		}
+
+		/*
+		 * Lock the variable.  This will also accept any pending invalidation
+		 * messages.  If we got back InvalidOid, indicating not found, then
+		 * there's nothing to lock, but we accept invalidation messages
+		 * anyway, to flush any negative catcache entries that may be
+		 * lingering.
+		 */
+		if (!OidIsValid(varid))
+			AcceptInvalidationMessages();
+		else if (OidIsValid(varid))
+			LockDatabaseObject(VariableRelationId, varid, 0, AccessShareLock);
+
+		if (inval_count == SharedInvalidMessageCounter)
+			break;
+
+		retry = true;
+		old_varid = varid;
+		varid = InvalidOid;
+	}
+
+	return varid;
+}
+
 /*
  * DeconstructQualifiedName
  *		Given a possibly-qualified name expressed as a list of String nodes,
diff --git a/src/backend/catalog/pg_variable.c b/src/backend/catalog/pg_variable.c
index e3a861c8cba..ef89594a268 100644
--- a/src/backend/catalog/pg_variable.c
+++ b/src/backend/catalog/pg_variable.c
@@ -26,6 +26,7 @@
 #include "parser/parse_type.h"
 #include "utils/builtins.h"
 #include "utils/lsyscache.h"
+#include "utils/pg_lsn.h"
 #include "utils/syscache.h"
 
 static ObjectAddress create_variable(const char *varName,
@@ -101,6 +102,7 @@ create_variable(const char *varName,
 	varid = GetNewOidWithIndex(rel, VariableOidIndexId, Anum_pg_variable_oid);
 
 	values[Anum_pg_variable_oid - 1] = ObjectIdGetDatum(varid);
+	values[Anum_pg_variable_varcreate_lsn - 1] = LSNGetDatum(GetXLogInsertRecPtr());
 	values[Anum_pg_variable_varname - 1] = NameGetDatum(&varname);
 	values[Anum_pg_variable_varnamespace - 1] = ObjectIdGetDatum(varNamespace);
 	values[Anum_pg_variable_vartype - 1] = ObjectIdGetDatum(varType);
diff --git a/src/backend/commands/Makefile b/src/backend/commands/Makefile
index 48f7348f91c..1cfaeca51ea 100644
--- a/src/backend/commands/Makefile
+++ b/src/backend/commands/Makefile
@@ -50,6 +50,7 @@ OBJS = \
 	schemacmds.o \
 	seclabel.o \
 	sequence.o \
+	session_variable.o \
 	statscmds.o \
 	subscriptioncmds.o \
 	tablecmds.o \
diff --git a/src/backend/commands/meson.build b/src/backend/commands/meson.build
index 6dd00a4abde..ca621be5ecb 100644
--- a/src/backend/commands/meson.build
+++ b/src/backend/commands/meson.build
@@ -38,6 +38,7 @@ backend_sources += files(
   'schemacmds.c',
   'seclabel.c',
   'sequence.c',
+  'session_variable.c',
   'statscmds.c',
   'subscriptioncmds.c',
   'tablecmds.c',
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index a93f970a292..455a03cce63 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -338,6 +338,14 @@ EvaluateParams(ParseState *pstate, PreparedStatement *pstmt, List *params,
 		i++;
 	}
 
+	/*
+	 * The arguments of EXECUTE are evaluated by a direct expression
+	 * executor call.  This mode doesn't support session variables yet.
+	 * It will be enabled later.
+	 */
+	if (pstate->p_hasSessionVariables)
+		elog(ERROR, "session variable cannot be used as an argument");
+
 	/* Prepare the expressions for execution */
 	exprstates = ExecPrepareExprList(params, estate);
 
diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c
new file mode 100644
index 00000000000..548d9835c0d
--- /dev/null
+++ b/src/backend/commands/session_variable.c
@@ -0,0 +1,571 @@
+/*-------------------------------------------------------------------------
+ *
+ * session_variable.c
+ *	  session variable creation/manipulation commands
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/commands/session_variable.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "catalog/pg_variable.h"
+#include "commands/session_variable.h"
+#include "executor/svariableReceiver.h"
+#include "funcapi.h"
+#include "miscadmin.h"
+#include "rewrite/rewriteHandler.h"
+#include "storage/lmgr.h"
+#include "storage/proc.h"
+#include "tcop/tcopprot.h"
+#include "utils/builtins.h"
+#include "utils/datum.h"
+#include "utils/inval.h"
+#include "utils/lsyscache.h"
+#include "utils/snapmgr.h"
+#include "utils/syscache.h"
+
+/*
+ * The values of session variables are stored in the backend's private memory
+ * in the dedicated memory context SVariableMemoryContext in binary format.
+ * They are stored in the "sessionvars" hash table, whose key is the OID of the
+ * variable.  However, the OID is not good enough to identify a session
+ * variable: concurrent sessions could drop the session variable and create a
+ * new one, which could be assigned the same OID.  To ensure that the values
+ * stored in memory and the catalog definition match, we also keep track of
+ * the "create_lsn".  Before any access to the variable values, we need to
+ * check if the LSN stored in memory matches the LSN in the catalog.  If there
+ * is a mismatch between the LSNs, or if the OID is not present in pg_variable
+ * at all, the value stored in memory is released.
+ */
+typedef struct SVariableData
+{
+	Oid			varid;			/* pg_variable OID of the variable (hash key) */
+	XLogRecPtr	create_lsn;
+
+	bool		isnull;
+	Datum		value;
+
+	Oid			typid;
+	int16		typlen;
+	bool		typbyval;
+
+	bool		is_domain;
+
+	/*
+	 * domain_check_extra holds cached domain metadata.  This "extra" is
+	 * usually stored in fn_mcxt.  We do not have access to that memory context
+	 * for session variables, but we can use TopTransactionContext instead.
+	 * A fresh value is forced when we detect we are in a different transaction
+	 * (the local transaction ID differs from domain_check_extra_lxid).
+	 */
+	void	   *domain_check_extra;
+	LocalTransactionId domain_check_extra_lxid;
+
+	/*
+	 * Stored value and type description can be outdated when we receive a
+	 * sinval message.  We then have to check if the stored data are still
+	 * trustworthy.
+	 */
+	bool		is_valid;
+
+	uint32		hashvalue;		/* used for pairing sinval message */
+} SVariableData;
+
+typedef SVariableData *SVariable;
+
+static HTAB *sessionvars = NULL;	/* hash table for session variables */
+
+static MemoryContext SVariableMemoryContext = NULL;
+
+/*
+ * Callback function for session variable invalidation.
+ */
+static void
+pg_variable_cache_callback(Datum arg, int cacheid, uint32 hashvalue)
+{
+	HASH_SEQ_STATUS status;
+	SVariable	svar;
+
+	elog(DEBUG1, "pg_variable_cache_callback %u %u", cacheid, hashvalue);
+
+	Assert(sessionvars);
+
+	/*
+	 * If the hashvalue is not specified, we have to recheck all currently
+	 * used session variables.  Since we can't tell the exact session variable
+	 * from its hashvalue, we have to iterate over all items in the hash bucket.
+	 */
+	hash_seq_init(&status, sessionvars);
+
+	while ((svar = (SVariable) hash_seq_search(&status)) != NULL)
+	{
+		if (hashvalue == 0 || svar->hashvalue == hashvalue)
+		{
+			svar->is_valid = false;
+		}
+	}
+}
+
+/*
+ * Release stored value, free memory
+ */
+static void
+free_session_variable_value(SVariable svar)
+{
+	/* clean the current value */
+	if (!svar->isnull)
+	{
+		if (!svar->typbyval)
+			pfree(DatumGetPointer(svar->value));
+
+		svar->isnull = true;
+	}
+
+	svar->value = (Datum) 0;
+}
+
+/*
+ * Returns true when the entry in pg_variable is consistent with the given
+ * session variable.
+ */
+static bool
+is_session_variable_valid(SVariable svar)
+{
+	HeapTuple	tp;
+	bool		result = false;
+
+	Assert(OidIsValid(svar->varid));
+
+	tp = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(svar->varid));
+
+	if (HeapTupleIsValid(tp))
+	{
+		/*
+		 * The OID alone is not enough as an unique identifier, because OID
+		 * values get recycled, and a new session variable could have got
+		 * the same OID.  We do a second check against the 64-bit LSN when
+		 * the variable was created.
+		 */
+		if (svar->create_lsn == ((Form_pg_variable) GETSTRUCT(tp))->varcreate_lsn)
+			result = true;
+
+		ReleaseSysCache(tp);
+	}
+
+	return result;
+}
+
+/*
+ * Initialize attributes cached in "svar"
+ */
+static void
+setup_session_variable(SVariable svar, Oid varid)
+{
+	HeapTuple	tup;
+	Form_pg_variable varform;
+
+	Assert(OidIsValid(varid));
+
+	tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid));
+
+	if (!HeapTupleIsValid(tup))
+		elog(ERROR, "cache lookup failed for session variable %u", varid);
+
+	varform = (Form_pg_variable) GETSTRUCT(tup);
+
+	svar->varid = varid;
+	svar->create_lsn = varform->varcreate_lsn;
+
+	svar->typid = varform->vartype;
+
+	get_typlenbyval(svar->typid, &svar->typlen, &svar->typbyval);
+
+	svar->is_domain = (get_typtype(varform->vartype) == TYPTYPE_DOMAIN);
+	svar->domain_check_extra = NULL;
+	svar->domain_check_extra_lxid = InvalidLocalTransactionId;
+
+	svar->isnull = true;
+	svar->value = (Datum) 0;
+
+	svar->is_valid = true;
+
+	svar->hashvalue = GetSysCacheHashValue1(VARIABLEOID,
+											ObjectIdGetDatum(varid));
+
+	ReleaseSysCache(tup);
+}
+
+/*
+ * Assign a new value to the session variable.  It is copied to
+ * SVariableMemoryContext if necessary.
+ *
+ * If any error happens, the existing value won't be modified.
+ */
+static void
+set_session_variable(SVariable svar, Datum value, bool isnull)
+{
+	Datum		newval;
+	SVariableData locsvar,
+			   *_svar;
+
+	Assert(svar);
+	Assert(!isnull || value == (Datum) 0);
+
+	/*
+	 * Use typbyval, typbylen from session variable only when they are
+	 * trustworthy (the invalidation message was not accepted for this
+	 * variable).  If the variable might be invalid, force setup.
+	 *
+	 * Do not overwrite the passed session variable until we can be certain
+	 * that no error can be thrown.
+	 */
+	if (!svar->is_valid)
+	{
+		setup_session_variable(&locsvar, svar->varid);
+		_svar = &locsvar;
+	}
+	else
+		_svar = svar;
+
+	if (!isnull)
+	{
+		MemoryContext oldcxt = MemoryContextSwitchTo(SVariableMemoryContext);
+
+		newval = datumCopy(value, _svar->typbyval, _svar->typlen);
+
+		MemoryContextSwitchTo(oldcxt);
+	}
+	else
+		newval = value;
+
+	free_session_variable_value(svar);
+
+	elog(DEBUG1, "session variable \"%s.%s\" (oid:%u) has new value",
+		 get_namespace_name(get_session_variable_namespace(svar->varid)),
+		 get_session_variable_name(svar->varid),
+		 svar->varid);
+
+	/* no more error expected, so we can overwrite the old variable now */
+	if (svar != _svar)
+		memcpy(svar, _svar, sizeof(SVariableData));
+
+	svar->value = newval;
+	svar->isnull = isnull;
+}
+
+/*
+ * Create the hash table for storing session variables.
+ */
+static void
+create_sessionvars_hashtables(void)
+{
+	HASHCTL		vars_ctl;
+
+	Assert(!sessionvars);
+
+	if (!SVariableMemoryContext)
+	{
+		/* read sinval messages */
+		CacheRegisterSyscacheCallback(VARIABLEOID,
+									  pg_variable_cache_callback,
+									  (Datum) 0);
+
+		/* we need our own long-lived memory context */
+		SVariableMemoryContext =
+			AllocSetContextCreate(TopMemoryContext,
+								  "session variables",
+								  ALLOCSET_START_SMALL_SIZES);
+	}
+
+	memset(&vars_ctl, 0, sizeof(vars_ctl));
+	vars_ctl.keysize = sizeof(Oid);
+	vars_ctl.entrysize = sizeof(SVariableData);
+	vars_ctl.hcxt = SVariableMemoryContext;
+
+	sessionvars = hash_create("Session variables", 64, &vars_ctl,
+							  HASH_ELEM | HASH_BLOBS | HASH_CONTEXT);
+}
+
+/*
+ * Search a session variable in the hash table given its OID.  If it
+ * doesn't exist, then insert it there.
+ *
+ * The caller is responsible for doing permission checks.
+ *
+ * As a side effect, this function acquires a AccessShareLock on the
+ * session variable until the end of the transaction.
+ */
+static SVariable
+get_session_variable(Oid varid)
+{
+	SVariable	svar;
+	bool		found;
+
+	/* protect the used session variable against DROP */
+	LockDatabaseObject(VariableRelationId, varid, 0, AccessShareLock);
+
+	if (!sessionvars)
+		create_sessionvars_hashtables();
+
+	svar = (SVariable) hash_search(sessionvars, &varid,
+								   HASH_ENTER, &found);
+
+	if (found)
+	{
+		if (!svar->is_valid)
+		{
+			/*
+			 * If there was an invalidation message, the variable might still be
+			 * valid, but we have to check with the system catalog.
+			 */
+			if (is_session_variable_valid(svar))
+				svar->is_valid = true;
+			else
+				/* if the value cannot be validated, we have to discard it */
+				free_session_variable_value(svar);
+		}
+	}
+	else
+		svar->is_valid = false;
+
+	/*
+	 * Force setup for not yet initialized variables or variables that cannot
+	 * be validated.
+	 */
+	if (!svar->is_valid)
+	{
+		setup_session_variable(svar, varid);
+
+		elog(DEBUG1, "session variable \"%s.%s\" (oid:%u) has assigned entry in memory (emitted by READ)",
+			 get_namespace_name(get_session_variable_namespace(varid)),
+			 get_session_variable_name(varid),
+			 varid);
+	}
+
+	/* ensure the returned data is still of the correct domain */
+	if (svar->is_domain)
+	{
+		/*
+		 * Store "extra" for domain_check() in TopTransactionContext.  When we
+		 * are in a new transaction, domain_check_extra cache is not valid any
+		 * more.
+		 */
+		if (svar->domain_check_extra_lxid != MyProc->vxid.lxid)
+			svar->domain_check_extra = NULL;
+
+		domain_check(svar->value, svar->isnull,
+					 svar->typid, &svar->domain_check_extra,
+					 TopTransactionContext);
+
+		svar->domain_check_extra_lxid = MyProc->vxid.lxid;
+	}
+
+	return svar;
+}
+
+/*
+ * Store the given value in a session variable in the cache.
+ *
+ * The caller is responsible for doing permission checks.
+ *
+ * As a side effect, this function acquires a AccessShareLock on the session
+ * variable until the end of the transaction.
+ */
+void
+SetSessionVariable(Oid varid, Datum value, bool isNull)
+{
+	SVariable	svar;
+	bool		found;
+
+	/* protect used session variable against DROP */
+	LockDatabaseObject(VariableRelationId, varid, 0, AccessShareLock);
+
+	if (!sessionvars)
+		create_sessionvars_hashtables();
+
+	svar = (SVariable) hash_search(sessionvars, &varid,
+								   HASH_ENTER, &found);
+
+	if (!found)
+	{
+		setup_session_variable(svar, varid);
+
+		elog(DEBUG1, "session variable \"%s.%s\" (oid:%u) has assigned entry in memory (emitted by WRITE)",
+			 get_namespace_name(get_session_variable_namespace(svar->varid)),
+			 get_session_variable_name(svar->varid),
+			 varid);
+	}
+
+	/* if this fails, it won't change the stored value */
+	set_session_variable(svar, value, isNull);
+}
+
+/*
+ * Wrapper around SetSessionVariable with permission checks.
+ */
+void
+SetSessionVariableWithSecurityCheck(Oid varid, Datum value, bool isNull)
+{
+	AclResult	aclresult;
+
+	/* is the caller allowed to update the session variable? */
+	aclresult = object_aclcheck(VariableRelationId, varid, GetUserId(), ACL_UPDATE);
+	if (aclresult != ACLCHECK_OK)
+		aclcheck_error(aclresult, OBJECT_VARIABLE, get_session_variable_name(varid));
+
+	SetSessionVariable(varid, value, isNull);
+}
+
+/*
+ * Returns a copy of the value stored in a variable.
+ */
+static inline Datum
+copy_session_variable_value(SVariable svar, bool *isNull)
+{
+	Datum		value;
+
+	/* force copy of non NULL value */
+	if (!svar->isnull)
+	{
+		value = datumCopy(svar->value, svar->typbyval, svar->typlen);
+		*isNull = false;
+	}
+	else
+	{
+		value = (Datum) 0;
+		*isNull = true;
+	}
+
+	return value;
+}
+
+/*
+ * Returns a copy of the value of the session variable (in the current memory
+ * context).  The caller is responsible for permission checks.
+ */
+Datum
+GetSessionVariable(Oid varid, bool *isNull, Oid *typid)
+{
+	SVariable	svar;
+
+	svar = get_session_variable(varid);
+
+	/*
+	 * Although "svar" is freshly validated in this point, svar->is_valid can
+	 * be false, if an invalidation message ws processed during the domain check.
+	 * But the variable and all its dependencies are locked now, so we don't need
+	 * to repeat the validation.
+	 */
+	Assert(svar);
+
+	*typid = svar->typid;
+
+	return copy_session_variable_value(svar, isNull);
+}
+
+/*
+ * Returns a copy of the value of the session variable after checking if the
+ * type is the same as "expected_typid".  The caller is responsible for
+ * permission checks.
+ */
+Datum
+GetSessionVariableWithTypeCheck(Oid varid, bool *isNull, Oid expected_typid)
+{
+	SVariable	svar;
+
+	svar = get_session_variable(varid);
+
+	Assert(svar && svar->is_valid);
+
+	if (expected_typid != svar->typid)
+		elog(ERROR, "type of variable \"%s.%s\" is different than expected",
+			 get_namespace_name(get_session_variable_namespace(varid)),
+			 get_session_variable_name(varid));
+
+	return copy_session_variable_value(svar, isNull);
+}
+
+/*
+ * Assign the result of the evaluated expression to the session variable
+ */
+void
+ExecuteLetStmt(ParseState *pstate,
+			   LetStmt *stmt,
+			   ParamListInfo params,
+			   QueryEnvironment *queryEnv,
+			   QueryCompletion *qc)
+{
+	Query	   *query = castNode(Query, stmt->query);
+	List	   *rewritten;
+	DestReceiver *dest;
+	AclResult	aclresult;
+	PlannedStmt *plan;
+	QueryDesc  *queryDesc;
+	Oid			varid = query->resultVariable;
+
+	Assert(OidIsValid(varid));
+
+	/* do we have permission to write to the session variable? */
+	aclresult = object_aclcheck(VariableRelationId, varid, GetUserId(), ACL_UPDATE);
+	if (aclresult != ACLCHECK_OK)
+		aclcheck_error(aclresult, OBJECT_VARIABLE, get_session_variable_name(varid));
+
+	/* create a dest receiver for LET */
+	dest = CreateVariableDestReceiver(varid);
+
+	/* run the query rewriter */
+	query = copyObject(query);
+
+	rewritten = QueryRewrite(query);
+
+	Assert(list_length(rewritten) == 1);
+
+	query = linitial_node(Query, rewritten);
+	Assert(query->commandType == CMD_SELECT);
+
+	/* plan the query */
+	plan = pg_plan_query(query, pstate->p_sourcetext,
+						 CURSOR_OPT_PARALLEL_OK, params);
+
+	/*
+	 * Use a snapshot with an updated command ID to ensure this query sees the
+	 * results of any previously executed queries.  (This could only matter if
+	 * the planner executed an allegedly-stable function that changed the
+	 * database contents, but let's do it anyway to be parallel to the EXPLAIN
+	 * code path.)
+	 */
+	PushCopiedSnapshot(GetActiveSnapshot());
+	UpdateActiveSnapshotCommandId();
+
+	/* create a QueryDesc, redirecting output to our tuple receiver */
+	queryDesc = CreateQueryDesc(plan, pstate->p_sourcetext,
+								GetActiveSnapshot(), InvalidSnapshot,
+								dest, params, queryEnv, 0);
+
+	/* call ExecutorStart to prepare the plan for execution */
+	ExecutorStart(queryDesc, 0);
+
+	/*
+	 * Run the plan to completion.  The result should be only one row.  To
+	 * check if there are too many result rows, we try to fetch two.
+	 */
+	ExecutorRun(queryDesc, ForwardScanDirection, 2L, true);
+
+	/* save the rowcount if we're given a QueryCompletion to fill */
+	if (qc)
+		SetQueryCompletion(qc, CMDTAG_LET, queryDesc->estate->es_processed);
+
+	/* and clean up */
+	ExecutorFinish(queryDesc);
+	ExecutorEnd(queryDesc);
+
+	FreeQueryDesc(queryDesc);
+
+	PopActiveSnapshot();
+}
diff --git a/src/backend/executor/Makefile b/src/backend/executor/Makefile
index 11118d0ce02..71248a34f26 100644
--- a/src/backend/executor/Makefile
+++ b/src/backend/executor/Makefile
@@ -76,6 +76,7 @@ OBJS = \
 	nodeWindowAgg.o \
 	nodeWorktablescan.o \
 	spi.o \
+	svariableReceiver.o \
 	tqueue.o \
 	tstoreReceiver.o
 
diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c
index 8f7a5340059..6dc8c562bec 100644
--- a/src/backend/executor/execExpr.c
+++ b/src/backend/executor/execExpr.c
@@ -34,6 +34,7 @@
 #include "catalog/objectaccess.h"
 #include "catalog/pg_proc.h"
 #include "catalog/pg_type.h"
+#include "catalog/pg_variable.h"
 #include "executor/execExpr.h"
 #include "executor/nodeSubplan.h"
 #include "funcapi.h"
@@ -991,6 +992,41 @@ ExecInitExprRec(Expr *node, ExprState *state,
 						scratch.d.param.paramtype = param->paramtype;
 						ExprEvalPushStep(state, &scratch);
 						break;
+
+					case PARAM_VARIABLE:
+						{
+							int			es_num_session_variables = 0;
+							SessionVariableValue *es_session_variables = NULL;
+							SessionVariableValue *var;
+
+							if (state->parent && state->parent->state)
+							{
+								es_session_variables = state->parent->state->es_session_variables;
+								es_num_session_variables = state->parent->state->es_num_session_variables;
+							}
+
+							Assert(es_session_variables);
+
+							/* parameter sanity checks */
+							if (param->paramid >= es_num_session_variables)
+								elog(ERROR, "paramid of PARAM_VARIABLE param is out of range");
+
+							var = &es_session_variables[param->paramid];
+
+							if (var->typid != param->paramtype)
+								elog(ERROR, "type of buffered value is different than PARAM_VARIABLE type");
+
+							/*
+							 * In this case, pass the value like a
+							 * constant.
+							 */
+							scratch.opcode = EEOP_CONST;
+							scratch.d.constval.value = var->value;
+							scratch.d.constval.isnull = var->isnull;
+							ExprEvalPushStep(state, &scratch);
+						}
+						break;
+
 					case PARAM_EXTERN:
 
 						/*
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index cc9a594cba5..ba6e38263cc 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -43,7 +43,9 @@
 #include "access/xact.h"
 #include "catalog/namespace.h"
 #include "catalog/partition.h"
+#include "catalog/pg_variable.h"
 #include "commands/matview.h"
+#include "commands/session_variable.h"
 #include "commands/trigger.h"
 #include "executor/executor.h"
 #include "executor/nodeSubplan.h"
@@ -194,6 +196,61 @@ standard_ExecutorStart(QueryDesc *queryDesc, int eflags)
 	Assert(queryDesc->sourceText != NULL);
 	estate->es_sourceText = queryDesc->sourceText;
 
+	/*
+	 * The executor doesn't work with session variables directly. Values of
+	 * related session variables are copied to a dedicated array, and this array
+	 * is passed to the executor. This array is stable "snapshot" of values of
+	 * used session variables. There are three benefits of this strategy:
+	 *
+	 * - consistency with external parameters and plpgsql variables,
+	 *
+	 * - session variables can be parallel safe,
+	 *
+	 * - we don't need make fresh copy for any read of session variable
+	 *    (this is necessary because the internally the session variable can
+	 *    be changed inside query execution time, and then a reference to
+	 *    previously returned value can be corrupted).
+	 */
+	if (queryDesc->plannedstmt->sessionVariables)
+	{
+		ListCell   *lc;
+		int			nSessionVariables;
+		int			i = 0;
+
+		/*
+		 * In this case, the query uses session variables, but we have to
+		 * prepare the array with passed values (of used session variables)
+		 * first.
+		 */
+		Assert(!IsParallelWorker());
+		nSessionVariables = list_length(queryDesc->plannedstmt->sessionVariables);
+
+		/* create the array used for passing values of used session variables */
+		estate->es_session_variables = (SessionVariableValue *)
+			palloc(nSessionVariables * sizeof(SessionVariableValue));
+
+		/* fill the array */
+		foreach(lc, queryDesc->plannedstmt->sessionVariables)
+		{
+			AclResult	aclresult;
+			Oid			varid = lfirst_oid(lc);
+
+			aclresult = object_aclcheck(VariableRelationId, varid, GetUserId(), ACL_SELECT);
+			if (aclresult != ACLCHECK_OK)
+				aclcheck_error(aclresult, OBJECT_VARIABLE,
+							   get_session_variable_name(varid));
+
+			estate->es_session_variables[i].varid = varid;
+			estate->es_session_variables[i].value = GetSessionVariable(varid,
+																	   &estate->es_session_variables[i].isnull,
+																	   &estate->es_session_variables[i].typid);
+
+			i++;
+		}
+
+		estate->es_num_session_variables = nSessionVariables;
+	}
+
 	/*
 	 * Fill in the query environment, if any, from queryDesc.
 	 */
diff --git a/src/backend/executor/meson.build b/src/backend/executor/meson.build
index b511a429adf..b6662c6e8fc 100644
--- a/src/backend/executor/meson.build
+++ b/src/backend/executor/meson.build
@@ -64,6 +64,7 @@ backend_sources += files(
   'nodeWindowAgg.c',
   'nodeWorktablescan.c',
   'spi.c',
+  'svariableReceiver.c',
   'tqueue.c',
   'tstoreReceiver.c',
 )
diff --git a/src/backend/executor/svariableReceiver.c b/src/backend/executor/svariableReceiver.c
new file mode 100644
index 00000000000..c4cff44ecf4
--- /dev/null
+++ b/src/backend/executor/svariableReceiver.c
@@ -0,0 +1,201 @@
+/*-------------------------------------------------------------------------
+ *
+ * svariableReceiver.c
+ *	  An implementation of DestReceiver that stores the result value in
+ *	  a session variable.
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *	  src/backend/executor/svariableReceiver.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+#include "miscadmin.h"
+
+#include "access/detoast.h"
+#include "catalog/pg_variable.h"
+#include "commands/session_variable.h"
+#include "executor/svariableReceiver.h"
+#include "storage/lock.h"
+#include "utils/builtins.h"
+#include "utils/lsyscache.h"
+#include "utils/syscache.h"
+
+/*
+ * This DestReceiver is used by the LET command for storing the result to a
+ * session variable.  The result has to have only one tuple with only one
+ * non-deleted attribute.  The row counter (field "rows") is incremented
+ * after receiving a row, and an error is raised when there are no rows or
+ * there are more than one received rows.  Because a received tuple can have
+ * deleted attributes, we need to find the first non-deleted attribute
+ * (field "slot_offset").  The value is detoasted before storing it in the
+ * session variable.
+ */
+typedef struct
+{
+	DestReceiver pub;
+	Oid			varid;
+	bool		need_detoast;		/* do we need to detoast the attribute? */
+	int			slot_offset;		/* position of non-deleted attribute */
+	int			rows;				/* row counter */
+} SVariableState;
+
+/*
+ * Prepare to receive tuples from executor.
+ */
+static void
+svariableStartupReceiver(DestReceiver *self, int operation, TupleDesc typeinfo)
+{
+	SVariableState *myState = (SVariableState *) self;
+	int			natts = typeinfo->natts;
+	int			outcols = 0;
+	int			i;
+	LOCKTAG		locktag PG_USED_FOR_ASSERTS_ONLY;
+
+	Assert(myState->pub.mydest == DestVariable);
+	Assert(OidIsValid(myState->varid));
+	Assert(SearchSysCacheExists1(VARIABLEOID, myState->varid));
+
+#ifdef USE_ASSERT_CHECKING
+
+	SET_LOCKTAG_OBJECT(locktag,
+					   MyDatabaseId,
+					   VariableRelationId,
+					   myState->varid,
+					   0);
+
+	Assert(LockHeldByMe(&locktag, AccessShareLock, false));
+
+#endif
+
+	for (i = 0; i < natts; i++)
+	{
+		Form_pg_attribute attr = TupleDescAttr(typeinfo, i);
+		Oid			typid;
+		Oid			collid;
+		int32		typmod;
+
+		if (attr->attisdropped)
+			continue;
+
+		if (++outcols > 1)
+			continue;
+
+		get_session_variable_type_typmod_collid(myState->varid,
+												&typid,
+												&typmod,
+												&collid);
+
+		/*
+		 * Double check - the type and typmod of target variable should be the
+		 * same as the type and typmod of assignment expression.  The
+		 * expression should be wrapped by a cast to the target type/typmod.
+		 */
+		if (attr->atttypid != typid ||
+			(attr->atttypmod >= 0 &&
+			 attr->atttypmod != typmod))
+			ereport(ERROR,
+					(errcode(ERRCODE_DATATYPE_MISMATCH),
+					 errmsg("target session variable is of type %s"
+							" but expression is of type %s",
+							format_type_with_typemod(typid, typmod),
+							format_type_with_typemod(attr->atttypid,
+													 attr->atttypmod))));
+
+		myState->need_detoast = attr->attlen == -1;
+		myState->slot_offset = i;
+	}
+
+	if (outcols != 1)
+		ereport(ERROR,
+				(errcode(ERRCODE_SYNTAX_ERROR),
+				 errmsg_plural("assignment expression returned %d column",
+							   "assignment expression returned %d columns",
+							   outcols,
+							   outcols)));
+
+	myState->rows = 0;
+}
+
+/*
+ * Receive a tuple from the executor and store it in the session variable.
+ */
+static bool
+svariableReceiveSlot(TupleTableSlot *slot, DestReceiver *self)
+{
+	SVariableState *myState = (SVariableState *) self;
+	Datum		value;
+	bool		isnull;
+	bool		freeval = false;
+
+	/* make sure the tuple is fully deconstructed */
+	slot_getallattrs(slot);
+
+	value = slot->tts_values[myState->slot_offset];
+	isnull = slot->tts_isnull[myState->slot_offset];
+
+	if (myState->need_detoast && !isnull && VARATT_IS_EXTERNAL(DatumGetPointer(value)))
+	{
+		value = PointerGetDatum(detoast_external_attr((struct varlena *)
+													  DatumGetPointer(value)));
+		freeval = true;
+	}
+
+	myState->rows += 1;
+
+	if (myState->rows > 1)
+		ereport(ERROR,
+				(errcode(ERRCODE_TOO_MANY_ROWS),
+				 errmsg("expression returned more than one row")));
+
+	SetSessionVariable(myState->varid, value, isnull);
+
+	if (freeval)
+		pfree(DatumGetPointer(value));
+
+	return true;
+}
+
+/*
+ * Clean up at end of the executor run
+ */
+static void
+svariableShutdownReceiver(DestReceiver *self)
+{
+	if (((SVariableState *) self)->rows == 0)
+		ereport(ERROR,
+				(errcode(ERRCODE_NO_DATA_FOUND),
+				 errmsg("expression returned no rows")));
+}
+
+/*
+ * Destroy the receiver when we are done with it
+ */
+static void
+svariableDestroyReceiver(DestReceiver *self)
+{
+	pfree(self);
+}
+
+/*
+ * Initially create a DestReceiver object.
+ */
+DestReceiver *
+CreateVariableDestReceiver(Oid varid)
+{
+	SVariableState *self = (SVariableState *) palloc0(sizeof(SVariableState));
+
+	self->pub.receiveSlot = svariableReceiveSlot;
+	self->pub.rStartup = svariableStartupReceiver;
+	self->pub.rShutdown = svariableShutdownReceiver;
+	self->pub.rDestroy = svariableDestroyReceiver;
+	self->pub.mydest = DestVariable;
+
+	self->varid = varid;
+
+	return (DestReceiver *) self;
+}
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index 3060847b133..8fc67f08d76 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -4341,6 +4341,16 @@ raw_expression_tree_walker_impl(Node *node,
 					return true;
 			}
 			break;
+		case T_LetStmt:
+			{
+				LetStmt    *stmt = (LetStmt *) node;
+
+				if (WALK(stmt->target))
+					return true;
+				if (WALK(stmt->query))
+					return true;
+			}
+			break;
 		case T_PLAssignStmt:
 			{
 				PLAssignStmt *stmt = (PLAssignStmt *) node;
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 1f78dc3d530..aee3a0d1ad4 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -325,6 +325,7 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
 	glob->lastPlanNodeId = 0;
 	glob->transientPlan = false;
 	glob->dependsOnRole = false;
+	glob->sessionVariables = NIL;
 
 	/*
 	 * Assess whether it's feasible to use parallel mode for this query. We
@@ -559,6 +560,7 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
 	result->paramExecTypes = glob->paramExecTypes;
 	/* utilityStmt should be null, but we might as well copy it */
 	result->utilityStmt = parse->utilityStmt;
+	result->sessionVariables = glob->sessionVariables;
 	result->stmt_location = parse->stmt_location;
 	result->stmt_len = parse->stmt_len;
 
@@ -729,6 +731,13 @@ subquery_planner(PlannerGlobal *glob, Query *parse, PlannerInfo *parent_root,
 	 */
 	pull_up_subqueries(root);
 
+	/*
+	 * Check if some subquery uses a session variable.  The flag
+	 * hasSessionVariables should be true if the query or some subquery uses a
+	 * session variable.
+	 */
+	pull_up_has_session_variables(root);
+
 	/*
 	 * If this is a simple UNION ALL query, flatten it into an appendrel. We
 	 * do this now because it requires applying pull_up_subqueries to the leaf
diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c
index 91c7c4fe2fe..1fa2ecf5c20 100644
--- a/src/backend/optimizer/plan/setrefs.c
+++ b/src/backend/optimizer/plan/setrefs.c
@@ -210,6 +210,9 @@ static List *set_returning_clause_references(PlannerInfo *root,
 static List *set_windowagg_runcondition_references(PlannerInfo *root,
 												   List *runcondition,
 												   Plan *plan);
+static bool pull_up_has_session_variables_walker(Node *node,
+												 PlannerInfo *root);
+static void record_plan_variable_dependency(PlannerInfo *root, Oid varid);
 
 
 /*****************************************************************************
@@ -1310,6 +1313,50 @@ set_plan_refs(PlannerInfo *root, Plan *plan, int rtoffset)
 	return plan;
 }
 
+/*
+ * Search usage of session variables in subqueries
+ */
+void
+pull_up_has_session_variables(PlannerInfo *root)
+{
+	Query	   *query = root->parse;
+
+	if (query->hasSessionVariables)
+	{
+		root->hasSessionVariables = true;
+	}
+	else
+	{
+		(void) query_tree_walker(query,
+								 pull_up_has_session_variables_walker,
+								 (void *) root, 0);
+	}
+}
+
+static bool
+pull_up_has_session_variables_walker(Node *node, PlannerInfo *root)
+{
+	if (node == NULL)
+		return false;
+	if (IsA(node, Query))
+	{
+		Query	   *query = (Query *) node;
+
+		if (query->hasSessionVariables)
+		{
+			root->hasSessionVariables = true;
+			return false;
+		}
+
+		/* recurse into subselects */
+		return query_tree_walker((Query *) node,
+								 pull_up_has_session_variables_walker,
+								 (void *) root, 0);
+	}
+	return expression_tree_walker(node, pull_up_has_session_variables_walker,
+								  (void *) root);
+}
+
 /*
  * set_indexonlyscan_references
  *		Do set_plan_references processing on an IndexOnlyScan
@@ -1958,8 +2005,9 @@ copyVar(Var *var)
  * This is code that is common to all variants of expression-fixing.
  * We must look up operator opcode info for OpExpr and related nodes,
  * add OIDs from regclass Const nodes into root->glob->relationOids, and
- * add PlanInvalItems for user-defined functions into root->glob->invalItems.
- * We also fill in column index lists for GROUPING() expressions.
+ * add PlanInvalItems for user-defined functions and session variables into
+ * root->glob->invalItems.  We also fill in column index lists for GROUPING()
+ * expressions.
  *
  * We assume it's okay to update opcode info in-place.  So this could possibly
  * scribble on the planner's input data structures, but it's OK.
@@ -2049,6 +2097,13 @@ fix_expr_common(PlannerInfo *root, Node *node)
 				g->cols = cols;
 		}
 	}
+	else if (IsA(node, Param))
+	{
+		Param	   *p = (Param *) node;
+
+		if (p->paramkind == PARAM_VARIABLE)
+			record_plan_variable_dependency(root, p->paramvarid);
+	}
 }
 
 /*
@@ -2058,6 +2113,10 @@ fix_expr_common(PlannerInfo *root, Node *node)
  * If it's a PARAM_MULTIEXPR, replace it with the appropriate Param from
  * root->multiexpr_params; otherwise no change is needed.
  * Just for paranoia's sake, we make a copy of the node in either case.
+ *
+ * If it's a PARAM_VARIABLE, then we collect used session variables in
+ * the list root->glob->sessionVariable.  Also, assign the parameter's
+ * "paramid" to the parameter's position in that list.
  */
 static Node *
 fix_param_node(PlannerInfo *root, Param *p)
@@ -2076,6 +2135,41 @@ fix_param_node(PlannerInfo *root, Param *p)
 			elog(ERROR, "unexpected PARAM_MULTIEXPR ID: %d", p->paramid);
 		return copyObject(list_nth(params, colno - 1));
 	}
+
+	if (p->paramkind == PARAM_VARIABLE)
+	{
+		ListCell   *lc;
+		int			n = 0;
+		bool		found = false;
+
+		/* we will modify object */
+		p = (Param *) copyObject(p);
+
+		/*
+		 * Now, we can actualize list of session variables, and we can
+		 * complete paramid parameter.
+		 */
+		foreach(lc, root->glob->sessionVariables)
+		{
+			if (lfirst_oid(lc) == p->paramvarid)
+			{
+				p->paramid = n;
+				found = true;
+				break;
+			}
+			n += 1;
+		}
+
+		if (!found)
+		{
+			root->glob->sessionVariables = lappend_oid(root->glob->sessionVariables,
+													   p->paramvarid);
+			p->paramid = n;
+		}
+
+		return (Node *) p;
+	}
+
 	return (Node *) copyObject(p);
 }
 
@@ -2137,7 +2231,10 @@ fix_alternative_subplan(PlannerInfo *root, AlternativeSubPlan *asplan,
  * replacing Aggref nodes that should be replaced by initplan output Params,
  * choosing the best implementation for AlternativeSubPlans,
  * looking up operator opcode info for OpExpr and related nodes,
- * and adding OIDs from regclass Const nodes into root->glob->relationOids.
+ * adding OIDs from regclass Const nodes into root->glob->relationOids,
+ * assigning paramvarid to PARAM_VARIABLE params, and collecting the
+ * OIDs of session variables in the root->glob->sessionVariables list
+ * (paramvarid is the position of the session variable in this list).
  *
  * 'node': the expression to be modified
  * 'rtoffset': how much to increment varnos by
@@ -2159,7 +2256,8 @@ fix_scan_expr(PlannerInfo *root, Node *node, int rtoffset, double num_exec)
 		root->multiexpr_params != NIL ||
 		root->glob->lastPHId != 0 ||
 		root->minmax_aggs != NIL ||
-		root->hasAlternativeSubPlans)
+		root->hasAlternativeSubPlans ||
+		root->hasSessionVariables)
 	{
 		return fix_scan_expr_mutator(node, &context);
 	}
@@ -3544,6 +3642,25 @@ record_plan_type_dependency(PlannerInfo *root, Oid typid)
 	}
 }
 
+/*
+ * Record dependency on a session variable.  The variable can be used as a
+ * session variable in an expression list, or as the target of a LET statement.
+ */
+static void
+record_plan_variable_dependency(PlannerInfo *root, Oid varid)
+{
+	PlanInvalItem *inval_item = makeNode(PlanInvalItem);
+
+	/* paramid is still session variable id */
+	inval_item->cacheId = VARIABLEOID;
+	inval_item->hashValue = GetSysCacheHashValue1(VARIABLEOID,
+												  ObjectIdGetDatum(varid));
+
+	/* append this variable to global, register dependency */
+	root->glob->invalItems = lappend(root->glob->invalItems,
+									 inval_item);
+}
+
 /*
  * extract_query_dependencies
  *		Given a rewritten, but not yet planned, query or queries
@@ -3629,9 +3746,9 @@ extract_query_dependencies_walker(Node *node, PlannerInfo *context)
 			}
 
 			/*
-			 * Ignore other utility statements, except those (such as EXPLAIN)
-			 * that contain a parsed-but-not-planned query.  For those, we
-			 * just need to transfer our attention to the contained query.
+			 * Ignore other utility statements, except those (such as EXPLAIN
+			 * or LET) that contain a parsed-but-not-planned query.  For those,
+			 * we just need to transfer our attention to the contained query.
 			 */
 			query = UtilityContainsQuery(query->utilityStmt);
 			if (query == NULL)
@@ -3654,6 +3771,10 @@ extract_query_dependencies_walker(Node *node, PlannerInfo *context)
 					lappend_oid(context->glob->relationOids, rte->relid);
 		}
 
+		/* record dependency on the target variable of a LET command */
+		if (OidIsValid(query->resultVariable))
+			record_plan_variable_dependency(context, query->resultVariable);
+
 		/* And recurse into the query's subexpressions */
 		return query_tree_walker(query, extract_query_dependencies_walker,
 								 (void *) context, 0);
diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c
index 33b10d72cb5..9f698a46d87 100644
--- a/src/backend/optimizer/prep/prepjointree.c
+++ b/src/backend/optimizer/prep/prepjointree.c
@@ -1410,6 +1410,9 @@ pull_up_simple_subquery(PlannerInfo *root, Node *jtnode, RangeTblEntry *rte,
 	/* If subquery had any RLS conditions, now main query does too */
 	parse->hasRowSecurity |= subquery->hasRowSecurity;
 
+	/* if the subquery had session variables, the main query does too */
+	parse->hasSessionVariables |= subquery->hasSessionVariables;
+
 	/*
 	 * subquery won't be pulled up if it hasAggs, hasWindowFuncs, or
 	 * hasTargetSRFs, so no work needed on those flags
diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c
index 8e39795e245..a5452c0c9e4 100644
--- a/src/backend/optimizer/util/clauses.c
+++ b/src/backend/optimizer/util/clauses.c
@@ -24,6 +24,7 @@
 #include "catalog/pg_operator.h"
 #include "catalog/pg_proc.h"
 #include "catalog/pg_type.h"
+#include "commands/session_variable.h"
 #include "executor/executor.h"
 #include "executor/functions.h"
 #include "funcapi.h"
@@ -935,6 +936,13 @@ max_parallel_hazard_walker(Node *node, max_parallel_hazard_context *context)
 		if (param->paramkind == PARAM_EXTERN)
 			return false;
 
+		/* we don't support passing session variables to workers */
+		if (param->paramkind == PARAM_VARIABLE)
+		{
+			if (max_parallel_hazard_test(PROPARALLEL_RESTRICTED, context))
+				return true;
+		}
+
 		if (param->paramkind != PARAM_EXEC ||
 			!list_member_int(context->safe_param_ids, param->paramid))
 		{
@@ -2389,6 +2397,7 @@ convert_saop_to_hashed_saop_walker(Node *node, void *context)
  *	  value of the Param.
  * 2. Fold stable, as well as immutable, functions to constants.
  * 3. Reduce PlaceHolderVar nodes to their contained expressions.
+ * 4. Current value of session variable can be used for estimation too.
  *--------------------
  */
 Node *
@@ -2515,6 +2524,29 @@ eval_const_expressions_mutator(Node *node,
 						}
 					}
 				}
+				else if (param->paramkind == PARAM_VARIABLE &&
+						 context->estimate)
+				{
+					int16		typLen;
+					bool		typByVal;
+					Datum		pval;
+					bool		isnull;
+
+					get_typlenbyval(param->paramtype,
+									&typLen, &typByVal);
+
+					pval = GetSessionVariableWithTypeCheck(param->paramvarid,
+														   &isnull,
+														   param->paramtype);
+
+					return (Node *) makeConst(param->paramtype,
+											  param->paramtypmod,
+											  param->paramcollid,
+											  (int) typLen,
+											  pval,
+											  isnull,
+											  typByVal);
+				}
 
 				/*
 				 * Not replaceable, so just copy the Param (no need to
@@ -4718,7 +4750,8 @@ inline_function(Oid funcid, Oid result_type, Oid result_collid,
 		querytree->limitOffset ||
 		querytree->limitCount ||
 		querytree->setOperations ||
-		list_length(querytree->targetList) != 1)
+		(list_length(querytree->targetList) != 1) ||
+		querytree->hasSessionVariables)
 		goto fail;
 
 	/* If the function result is composite, resolve it */
diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
index 506e0631615..81b8b0a633d 100644
--- a/src/backend/parser/analyze.c
+++ b/src/backend/parser/analyze.c
@@ -25,9 +25,12 @@
 #include "postgres.h"
 
 #include "access/sysattr.h"
+#include "catalog/namespace.h"
 #include "catalog/pg_proc.h"
 #include "catalog/pg_type.h"
+#include "catalog/pg_variable.h"
 #include "commands/defrem.h"
+#include "commands/session_variable.h"
 #include "miscadmin.h"
 #include "nodes/makefuncs.h"
 #include "nodes/nodeFuncs.h"
@@ -51,6 +54,7 @@
 #include "utils/backend_status.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
+#include "utils/lsyscache.h"
 #include "utils/rel.h"
 #include "utils/syscache.h"
 
@@ -83,6 +87,8 @@ static Query *transformCreateTableAsStmt(ParseState *pstate,
 										 CreateTableAsStmt *stmt);
 static Query *transformCallStmt(ParseState *pstate,
 								CallStmt *stmt);
+static Query *transformLetStmt(ParseState *pstate,
+							   LetStmt *stmt);
 static void transformLockingClause(ParseState *pstate, Query *qry,
 								   LockingClause *lc, bool pushedDown);
 #ifdef DEBUG_NODE_TESTS_ENABLED
@@ -414,6 +420,7 @@ transformStmt(ParseState *pstate, Node *parseTree)
 			case T_UpdateStmt:
 			case T_DeleteStmt:
 			case T_MergeStmt:
+			case T_LetStmt:
 				(void) test_raw_expression_coverage(parseTree, NULL);
 				break;
 			default:
@@ -493,6 +500,11 @@ transformStmt(ParseState *pstate, Node *parseTree)
 									   (CallStmt *) parseTree);
 			break;
 
+		case T_LetStmt:
+			result = transformLetStmt(pstate,
+									  (LetStmt *) parseTree);
+			break;
+
 		default:
 
 			/*
@@ -545,6 +557,7 @@ stmt_requires_parse_analysis(RawStmt *parseTree)
 		case T_SelectStmt:
 		case T_ReturnStmt:
 		case T_PLAssignStmt:
+		case T_LetStmt:
 			result = true;
 			break;
 
@@ -653,6 +666,7 @@ transformDeleteStmt(ParseState *pstate, DeleteStmt *stmt)
 	qry->hasWindowFuncs = pstate->p_hasWindowFuncs;
 	qry->hasTargetSRFs = pstate->p_hasTargetSRFs;
 	qry->hasAggs = pstate->p_hasAggs;
+	qry->hasSessionVariables = pstate->p_hasSessionVariables;
 
 	assign_query_collations(pstate, qry);
 
@@ -1079,6 +1093,7 @@ transformInsertStmt(ParseState *pstate, InsertStmt *stmt)
 
 	qry->hasTargetSRFs = pstate->p_hasTargetSRFs;
 	qry->hasSubLinks = pstate->p_hasSubLinks;
+	qry->hasSessionVariables = pstate->p_hasSessionVariables;
 
 	assign_query_collations(pstate, qry);
 
@@ -1544,6 +1559,7 @@ transformSelectStmt(ParseState *pstate, SelectStmt *stmt)
 	qry->hasWindowFuncs = pstate->p_hasWindowFuncs;
 	qry->hasTargetSRFs = pstate->p_hasTargetSRFs;
 	qry->hasAggs = pstate->p_hasAggs;
+	qry->hasSessionVariables = pstate->p_hasSessionVariables;
 
 	foreach(l, stmt->lockingClause)
 	{
@@ -1770,12 +1786,242 @@ transformValuesClause(ParseState *pstate, SelectStmt *stmt)
 	qry->jointree = makeFromExpr(pstate->p_joinlist, NULL);
 
 	qry->hasSubLinks = pstate->p_hasSubLinks;
+	qry->hasSessionVariables = pstate->p_hasSessionVariables;
 
 	assign_query_collations(pstate, qry);
 
 	return qry;
 }
 
+/*
+ * transformLetStmt -
+ *	  transform an Let Statement
+ */
+static Query *
+transformLetStmt(ParseState *pstate, LetStmt *stmt)
+{
+	Query	   *query;
+	Query	   *result;
+	List	   *exprList = NIL;
+	List	   *exprListCoer = NIL;
+	ListCell   *lc;
+	ListCell   *indirection_head = NULL;
+	Query	   *selectQuery;
+	Oid			varid;
+	char	   *attrname = NULL;
+	bool		not_unique;
+	bool		is_rowtype;
+	Oid			typid;
+	int32		typmod;
+	Oid			collid;
+	AclResult	aclresult;
+	List	   *names = NULL;
+	int			indirection_start;
+	int			i = 0;
+
+	/* there can't be any outer WITH to worry about */
+	Assert(pstate->p_ctenamespace == NIL);
+
+	names = NamesFromList(stmt->target);
+
+	/* locks the variable with an AccessShareLock */
+	varid = IdentifyVariable(names, &attrname, &not_unique, false);
+	if (not_unique)
+		ereport(ERROR,
+				(errcode(ERRCODE_AMBIGUOUS_PARAMETER),
+				 errmsg("target \"%s\" of LET command is ambiguous",
+						NameListToString(names)),
+				 parser_errposition(pstate, stmt->location)));
+
+	if (!OidIsValid(varid))
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("session variable \"%s\" doesn't exist",
+						NameListToString(names)),
+				 parser_errposition(pstate, stmt->location)));
+
+	/*
+	 * Calculate start of possible position of an indirection in list, and
+	 * when it is inside the list, store pointer on first node of indirection.
+	 */
+	indirection_start = list_length(names) - (attrname ? 1 : 0);
+	if (list_length(stmt->target) > indirection_start)
+		indirection_head = list_nth_cell(stmt->target, indirection_start);
+
+	get_session_variable_type_typmod_collid(varid, &typid, &typmod, &collid);
+
+	is_rowtype = type_is_rowtype(typid);
+
+	if (attrname && !is_rowtype)
+		ereport(ERROR,
+				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+				 errmsg("type \"%s\" of target session variable \"%s.%s\" is not a composite type",
+						format_type_be(typid),
+						get_namespace_name(get_session_variable_namespace(varid)),
+						get_session_variable_name(varid)),
+				 parser_errposition(pstate, stmt->location)));
+
+	aclresult = object_aclcheck(VariableRelationId, varid, GetUserId(), ACL_UPDATE);
+	if (aclresult != ACLCHECK_OK)
+		aclcheck_error(aclresult, OBJECT_VARIABLE, NameListToString(names));
+
+	pstate->p_expr_kind = EXPR_KIND_LET_TARGET;
+
+	/* we need to postpone conversion of "unknown" to text */
+	pstate->p_resolve_unknowns = false;
+
+	selectQuery = transformStmt(pstate, stmt->query);
+
+	/* the grammar should have produced a SELECT */
+	Assert(IsA(selectQuery, Query) && selectQuery->commandType == CMD_SELECT);
+
+	/*
+	 * Generate an expression list for the LET that selects all the non-resjunk
+	 * columns from the subquery.
+	 */
+	exprList = NIL;
+	foreach(lc, selectQuery->targetList)
+	{
+		TargetEntry *tle = (TargetEntry *) lfirst(lc);
+
+		if (tle->resjunk)
+			continue;
+
+		exprList = lappend(exprList, tle->expr);
+	}
+
+	/* don't allow multicolumn result */
+	if (list_length(exprList) != 1)
+		ereport(ERROR,
+				(errcode(ERRCODE_SYNTAX_ERROR),
+				 errmsg_plural("assignment expression returned %d column",
+							   "assignment expression returned %d columns",
+							   list_length(exprList),
+							   list_length(exprList)),
+				 parser_errposition(pstate,
+									exprLocation((Node *) exprList))));
+
+	exprListCoer = NIL;
+
+	foreach(lc, exprList)
+	{
+		Expr	   *expr = (Expr *) lfirst(lc);
+		Expr	   *coerced_expr;
+		Param	   *param;
+		Oid			exprtypid;
+
+		/* now we can read the type of the expression */
+		exprtypid = exprType((Node *) expr);
+
+		param = makeNode(Param);
+		param->paramkind = PARAM_VARIABLE;
+		param->paramvarid = varid;
+		param->paramtype = typid;
+		param->paramtypmod = typmod;
+
+		if (indirection_head)
+		{
+			bool		targetIsArray;
+			char	   *targetName;
+
+			targetName = get_session_variable_name(varid);
+			targetIsArray = OidIsValid(get_element_type(typid));
+
+			pstate->p_hasSessionVariables = true;
+
+			coerced_expr = (Expr *)
+				transformAssignmentIndirection(pstate,
+											   (Node *) param,
+											   targetName,
+											   targetIsArray,
+											   typid,
+											   typmod,
+											   InvalidOid,
+											   stmt->target,
+											   indirection_head,
+											   (Node *) expr,
+											   COERCION_ASSIGNMENT,
+											   stmt->location);
+		}
+		else
+			coerced_expr = (Expr *)
+				coerce_to_target_type(pstate,
+									  (Node *) expr,
+									  exprtypid,
+									  typid, typmod,
+									  COERCION_ASSIGNMENT,
+									  COERCE_IMPLICIT_CAST,
+									  stmt->location);
+
+		if (coerced_expr == NULL)
+			ereport(ERROR,
+					(errcode(ERRCODE_DATATYPE_MISMATCH),
+					 errmsg("variable \"%s.%s\" is of type %s,"
+							" but expression is of type %s",
+							get_namespace_name(get_session_variable_namespace(varid)),
+							get_session_variable_name(varid),
+							format_type_be(typid),
+							format_type_be(exprtypid)),
+					 errhint("You will need to rewrite or cast the expression."),
+					 parser_errposition(pstate, exprLocation((Node *) expr))));
+
+		exprListCoer = lappend(exprListCoer, coerced_expr);
+	}
+
+	/* generate query's target list using the computed list of expressions */
+	query = makeNode(Query);
+	query->commandType = CMD_SELECT;
+
+	foreach(lc, exprListCoer)
+	{
+		Expr	   *expr = (Expr *) lfirst(lc);
+		TargetEntry *tle;
+
+		tle = makeTargetEntry(expr,
+							  i + 1,
+							  FigureColname((Node *) expr),
+							  false);
+		query->targetList = lappend(query->targetList, tle);
+	}
+
+	/* done building the range table and jointree */
+	query->rtable = pstate->p_rtable;
+	query->jointree = makeFromExpr(pstate->p_joinlist, NULL);
+
+	query->hasTargetSRFs = pstate->p_hasTargetSRFs;
+	query->hasSubLinks = pstate->p_hasSubLinks;
+	query->hasSessionVariables = pstate->p_hasSessionVariables;
+
+	/* this is top-level query */
+	query->canSetTag = true;
+
+	/*
+	 * Save target session variable ID.  This value is used multiple times: by
+	 * the query rewriter (for getting related defexpr), by planner (for
+	 * acquiring an AccessShareLock on target variable), and by the executor
+	 * (we need to know the target variable ID).
+	 */
+	query->resultVariable = varid;
+
+	assign_query_collations(pstate, query);
+
+	/*
+	 * The query is executed as utility command by nested executor call.
+	 * Assigned queryId is required in this case.
+	 */
+	if (IsQueryIdEnabled())
+		JumbleQuery(query);
+
+	stmt->query = (Node *) query;
+
+	/* represent the command as a utility Query */
+	result = makeNode(Query);
+	result->commandType = CMD_UTILITY;
+	result->utilityStmt = (Node *) stmt;
+
+	return result;
+}
+
 /*
  * transformSetOperationStmt -
  *	  transforms a set-operations tree
@@ -2021,6 +2267,7 @@ transformSetOperationStmt(ParseState *pstate, SelectStmt *stmt)
 	qry->hasWindowFuncs = pstate->p_hasWindowFuncs;
 	qry->hasTargetSRFs = pstate->p_hasTargetSRFs;
 	qry->hasAggs = pstate->p_hasAggs;
+	qry->hasSessionVariables = pstate->p_hasSessionVariables;
 
 	foreach(l, lockingClause)
 	{
@@ -2496,6 +2743,7 @@ transformReturnStmt(ParseState *pstate, ReturnStmt *stmt)
 	qry->hasWindowFuncs = pstate->p_hasWindowFuncs;
 	qry->hasTargetSRFs = pstate->p_hasTargetSRFs;
 	qry->hasAggs = pstate->p_hasAggs;
+	qry->hasSessionVariables = pstate->p_hasSessionVariables;
 
 	assign_query_collations(pstate, qry);
 
@@ -2563,6 +2811,7 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
 
 	qry->hasTargetSRFs = pstate->p_hasTargetSRFs;
 	qry->hasSubLinks = pstate->p_hasSubLinks;
+	qry->hasSessionVariables = pstate->p_hasSessionVariables;
 
 	assign_query_collations(pstate, qry);
 
@@ -2750,9 +2999,15 @@ transformPLAssignStmt(ParseState *pstate, PLAssignStmt *stmt)
 	/*
 	 * Transform the target reference.  Typically we will get back a Param
 	 * node, but there's no reason to be too picky about its type.
+	 *
+	 * Session variables should not be used as target of a PL/pgSQL assign
+	 * statement.  So we should use a dedicated expression kind and disallow
+	 * session variables there.  The dedicated context allows to eliminate
+	 * undesirable warnings about the possibility of a target PL/pgSQL variable
+	 * shadowing a session variable.
 	 */
 	target = transformExpr(pstate, (Node *) cref,
-						   EXPR_KIND_UPDATE_TARGET);
+						   EXPR_KIND_ASSIGN_TARGET);
 	targettype = exprType(target);
 	targettypmod = exprTypmod(target);
 	targetcollation = exprCollation(target);
@@ -2794,6 +3049,10 @@ transformPLAssignStmt(ParseState *pstate, PLAssignStmt *stmt)
 	 */
 	type_id = exprType((Node *) tle->expr);
 
+	/*
+	 * For indirection processing and additional casts we can use expr_kind
+	 * EXPR_KIND_UPDATE_TARGET.
+	 */
 	pstate->p_expr_kind = EXPR_KIND_UPDATE_TARGET;
 
 	if (indirection)
@@ -2936,6 +3195,8 @@ transformPLAssignStmt(ParseState *pstate, PLAssignStmt *stmt)
 							   (LockingClause *) lfirst(l), false);
 	}
 
+	qry->hasSessionVariables = pstate->p_hasSessionVariables;
+
 	assign_query_collations(pstate, qry);
 
 	/* this must be done after collations, for reliable comparison of exprs */
@@ -3209,6 +3470,14 @@ transformCallStmt(ParseState *pstate, CallStmt *stmt)
 							 true,
 							 stmt->funccall->location);
 
+	/*
+	 * The arguments of CALL statement are evaluated by a direct expression
+	 * executor call.  This path is unsupported yet, so block it.  It will be
+	 * enabled later.
+	 */
+	if (pstate->p_hasSessionVariables)
+		elog(ERROR, "session variable cannot be used as an argument");
+
 	assign_expr_collations(pstate, node);
 
 	fexpr = castNode(FuncExpr, node);
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 89fa23c2dd4..8ae368c513c 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -293,7 +293,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 		DropTransformStmt
 		DropUserMappingStmt ExplainStmt FetchStmt
 		GrantStmt GrantRoleStmt ImportForeignSchemaStmt IndexStmt InsertStmt
-		ListenStmt LoadStmt LockStmt MergeStmt NotifyStmt ExplainableStmt PreparableStmt
+		LetStmt ListenStmt LoadStmt LockStmt MergeStmt NotifyStmt ExplainableStmt PreparableStmt
 		CreateFunctionStmt AlterFunctionStmt ReindexStmt RemoveAggrStmt
 		RemoveFuncStmt RemoveOperStmt RenameStmt ReturnStmt RevokeStmt RevokeRoleStmt
 		RuleActionStmt RuleActionStmtOrEmpty RuleStmt
@@ -443,6 +443,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 				TriggerTransitions TriggerReferencing
 				vacuum_relation_list opt_vacuum_relation_list
 				drop_option_list pub_obj_list
+				let_target
 
 %type <node>	opt_routine_body
 %type <groupclause> group_clause
@@ -734,7 +735,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	KEEP KEY KEYS
 
 	LABEL LANGUAGE LARGE_P LAST_P LATERAL_P
-	LEADING LEAKPROOF LEAST LEFT LEVEL LIKE LIMIT LISTEN LOAD LOCAL
+	LEADING LEAKPROOF LEAST LEFT LET LEVEL LIKE LIMIT LISTEN LOAD LOCAL
 	LOCALTIME LOCALTIMESTAMP LOCATION LOCK_P LOCKED LOGGED
 
 	MAPPING MATCH MATCHED MATERIALIZED MAXVALUE MERGE MERGE_ACTION METHOD
@@ -1081,6 +1082,7 @@ stmt:
 			| ImportForeignSchemaStmt
 			| IndexStmt
 			| InsertStmt
+			| LetStmt
 			| ListenStmt
 			| RefreshMatViewStmt
 			| LoadStmt
@@ -12770,6 +12772,47 @@ opt_hold: /* EMPTY */						{ $$ = 0; }
 			| WITHOUT HOLD					{ $$ = 0; }
 		;
 
+/*****************************************************************************
+ *
+ *		QUERY:
+ *				LET STATEMENT
+ *
+ *****************************************************************************/
+LetStmt:	LET let_target '=' a_expr
+				{
+					LetStmt	   *n = makeNode(LetStmt);
+					SelectStmt *select;
+					ResTarget  *res;
+
+					n->target = $2;
+
+					select = makeNode(SelectStmt);
+					res = makeNode(ResTarget);
+
+					/* create target list for implicit query */
+					res->name = NULL;
+					res->indirection = NIL;
+					res->val = (Node *) $4;
+					res->location = @4;
+
+					select->targetList = list_make1(res);
+					n->query = (Node *) select;
+
+					n->location = @2;
+					$$ = (Node *) n;
+				}
+		;
+
+let_target:
+			ColId opt_indirection
+				{
+					$$ = list_make1(makeString($1));
+					if ($2)
+						  $$ = list_concat($$,
+										   check_indirection($2, yyscanner));
+				}
+		;
+
 /*****************************************************************************
  *
  *		QUERY:
@@ -17840,6 +17883,7 @@ unreserved_keyword:
 			| LARGE_P
 			| LAST_P
 			| LEAKPROOF
+			| LET
 			| LEVEL
 			| LISTEN
 			| LOAD
@@ -18451,6 +18495,7 @@ bare_label_keyword:
 			| LEAKPROOF
 			| LEAST
 			| LEFT
+			| LET
 			| LEVEL
 			| LIKE
 			| LISTEN
diff --git a/src/backend/parser/parse_agg.c b/src/backend/parser/parse_agg.c
index efa730c1676..a7343001f21 100644
--- a/src/backend/parser/parse_agg.c
+++ b/src/backend/parser/parse_agg.c
@@ -580,6 +580,11 @@ check_agglevels_and_constraints(ParseState *pstate, Node *expr)
 			errkind = true;
 			break;
 
+		case EXPR_KIND_ASSIGN_TARGET:
+		case EXPR_KIND_LET_TARGET:
+			errkind = true;
+			break;
+
 			/*
 			 * There is intentionally no default: case here, so that the
 			 * compiler will warn if we add a new ParseExprKind without
@@ -970,6 +975,10 @@ transformWindowFuncCall(ParseState *pstate, WindowFunc *wfunc,
 		case EXPR_KIND_CYCLE_MARK:
 			errkind = true;
 			break;
+		case EXPR_KIND_ASSIGN_TARGET:
+		case EXPR_KIND_LET_TARGET:
+			errkind = true;
+			break;
 
 			/*
 			 * There is intentionally no default: case here, so that the
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index c2806297aa4..aba5259e027 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -17,6 +17,7 @@
 
 #include "catalog/pg_aggregate.h"
 #include "catalog/pg_type.h"
+#include "catalog/pg_variable.h"
 #include "commands/dbcommands.h"
 #include "miscadmin.h"
 #include "nodes/makefuncs.h"
@@ -33,11 +34,13 @@
 #include "parser/parse_relation.h"
 #include "parser/parse_target.h"
 #include "parser/parse_type.h"
+#include "storage/lmgr.h"
 #include "utils/builtins.h"
 #include "utils/date.h"
 #include "utils/fmgroids.h"
 #include "utils/lsyscache.h"
 #include "utils/timestamp.h"
+#include "utils/typcache.h"
 #include "utils/xml.h"
 
 /* GUC parameters */
@@ -106,6 +109,9 @@ static Expr *make_distinct_op(ParseState *pstate, List *opname,
 							  Node *ltree, Node *rtree, int location);
 static Node *make_nulltest_from_distinct(ParseState *pstate,
 										 A_Expr *distincta, Node *arg);
+static Node *makeParamSessionVariable(ParseState *pstate,
+									  Oid varid, Oid typid, int32 typmod, Oid collid,
+									  char *attrname, int location);
 
 
 /*
@@ -499,6 +505,89 @@ transformIndirection(ParseState *pstate, A_Indirection *ind)
 	return result;
 }
 
+/*
+ * Returns true if the given expression kind is valid for session variables.
+ * Session variables can be used everywhere where external parameters can be
+ * used.  Session variables are not allowed in DDL commands or in constraints.
+ *
+ * An identifier can be parsed as a session variable only for expression kinds
+ * where session variables are allowed.  This is the primary usage of this
+ * function.
+ *
+ * The second usage of this function is to decide whether a "column does not
+ * exist" or a "column or variable does not exist" error message should be
+ * printed.  When we are in an expression where session variables cannot be
+ * used, we raise the first form of error message.
+ */
+static bool
+expr_kind_allows_session_variables(ParseExprKind p_expr_kind)
+{
+	bool		result = false;
+
+	switch (p_expr_kind)
+	{
+		case EXPR_KIND_NONE:
+			Assert(false);		/* can't happen */
+			return false;
+
+		/* session variables allowed */
+		case EXPR_KIND_OTHER:
+		case EXPR_KIND_JOIN_ON:
+		case EXPR_KIND_FROM_SUBSELECT:
+		case EXPR_KIND_FROM_FUNCTION:
+		case EXPR_KIND_WHERE:
+		case EXPR_KIND_HAVING:
+		case EXPR_KIND_FILTER:
+		case EXPR_KIND_WINDOW_PARTITION:
+		case EXPR_KIND_WINDOW_ORDER:
+		case EXPR_KIND_WINDOW_FRAME_RANGE:
+		case EXPR_KIND_WINDOW_FRAME_ROWS:
+		case EXPR_KIND_WINDOW_FRAME_GROUPS:
+		case EXPR_KIND_SELECT_TARGET:
+		case EXPR_KIND_INSERT_TARGET:
+		case EXPR_KIND_UPDATE_SOURCE:
+		case EXPR_KIND_UPDATE_TARGET:
+		case EXPR_KIND_MERGE_WHEN:
+		case EXPR_KIND_MERGE_RETURNING:
+		case EXPR_KIND_GROUP_BY:
+		case EXPR_KIND_ORDER_BY:
+		case EXPR_KIND_DISTINCT_ON:
+		case EXPR_KIND_LIMIT:
+		case EXPR_KIND_OFFSET:
+		case EXPR_KIND_RETURNING:
+		case EXPR_KIND_VALUES:
+		case EXPR_KIND_VALUES_SINGLE:
+		case EXPR_KIND_ALTER_COL_TRANSFORM:
+		case EXPR_KIND_EXECUTE_PARAMETER:
+		case EXPR_KIND_POLICY:
+		case EXPR_KIND_CALL_ARGUMENT:
+		case EXPR_KIND_COPY_WHERE:
+		case EXPR_KIND_LET_TARGET:
+			result = true;
+			break;
+
+		/* session variables not allowed */
+		case EXPR_KIND_CHECK_CONSTRAINT:
+		case EXPR_KIND_DOMAIN_CHECK:
+		case EXPR_KIND_COLUMN_DEFAULT:
+		case EXPR_KIND_FUNCTION_DEFAULT:
+		case EXPR_KIND_INDEX_EXPRESSION:
+		case EXPR_KIND_INDEX_PREDICATE:
+		case EXPR_KIND_STATS_EXPRESSION:
+		case EXPR_KIND_TRIGGER_WHEN:
+		case EXPR_KIND_PARTITION_BOUND:
+		case EXPR_KIND_PARTITION_EXPRESSION:
+		case EXPR_KIND_GENERATED_COLUMN:
+		case EXPR_KIND_JOIN_USING:
+		case EXPR_KIND_CYCLE_MARK:
+		case EXPR_KIND_ASSIGN_TARGET:
+			result = false;
+			break;
+	}
+
+	return result;
+}
+
 /*
  * Transform a ColumnRef.
  *
@@ -575,6 +664,8 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
 		case EXPR_KIND_COPY_WHERE:
 		case EXPR_KIND_GENERATED_COLUMN:
 		case EXPR_KIND_CYCLE_MARK:
+		case EXPR_KIND_ASSIGN_TARGET:
+		case EXPR_KIND_LET_TARGET:
 			/* okay */
 			break;
 
@@ -847,8 +938,61 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
 	}
 
 	/*
-	 * Throw error if no translation found.
+	 * There are contexts where session variables are not allowed.  We don't
+	 * need to identify session variables in such a context, but identifying
+	 * them allows us to raise meaningful error messages like "you cannot use
+	 * session variables here".
 	 */
+	if (expr_kind_allows_session_variables(pstate->p_expr_kind))
+	{
+		Oid			varid = InvalidOid;
+		char	   *attrname = NULL;
+		bool		not_unique;
+
+		/* -----
+		 * Session variables are shadowed by columns, routine variables or
+		 * routine arguments.  We certainly don't want to use a session variable
+		 * when it is exactly shadowed, but a RTE like this is conceivable:
+		 *
+		 * CREATE TYPE t AS (c int);
+		 * CREATE VARIABLE foo AS t;
+		 * CREATE TABLE foo(a int, b int);
+		 *
+		 * SELECT foo.a, foo.b, foo.c FROM foo;
+		 *
+		 * However, that is very confusing, so we disallow it.  We don't try to
+		 * identify a variable if we know that it would be shadowed.
+		 * -----
+		 */
+		if (!node && !(relname && crerr == CRERR_NO_COLUMN))
+		{
+			/* takes an AccessShareLock on the session variable */
+			varid = IdentifyVariable(cref->fields, &attrname, &not_unique, false);
+
+			if (OidIsValid(varid))
+			{
+				Oid			typid;
+				int32		typmod;
+				Oid			collid;
+
+				if (not_unique)
+					ereport(ERROR,
+							(errcode(ERRCODE_AMBIGUOUS_PARAMETER),
+							 errmsg("session variable reference \"%s\" is ambiguous",
+									NameListToString(cref->fields)),
+							 parser_errposition(pstate, cref->location)));
+
+				get_session_variable_type_typmod_collid(varid, &typid, &typmod,
+														&collid);
+
+				node = makeParamSessionVariable(pstate,
+												varid, typid, typmod, collid,
+												attrname, cref->location);
+			}
+		}
+	}
+
+	/* throw an error if no translation was found */
 	if (node == NULL)
 	{
 		switch (crerr)
@@ -880,6 +1024,72 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
 	return node;
 }
 
+/*
+ * Generate param variable for reference to session variable
+ */
+static Node *
+makeParamSessionVariable(ParseState *pstate,
+						 Oid varid, Oid typid, int32 typmod, Oid collid,
+						 char *attrname, int location)
+{
+	Param	   *param;
+
+	param = makeNode(Param);
+
+	param->paramkind = PARAM_VARIABLE;
+	param->paramvarid = varid;
+	param->paramtype = typid;
+	param->paramtypmod = typmod;
+	param->paramcollid = collid;
+
+	pstate->p_hasSessionVariables = true;
+
+	if (attrname != NULL)
+	{
+		TupleDesc	tupdesc;
+		int			i;
+
+		tupdesc = lookup_rowtype_tupdesc_noerror(typid, typmod, true);
+		if (!tupdesc)
+			ereport(ERROR,
+					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+					 errmsg("variable \"%s.%s\" is of type \"%s\", which is not a composite type",
+							get_namespace_name(get_session_variable_namespace(varid)),
+							get_session_variable_name(varid),
+							format_type_be(typid)),
+					 parser_errposition(pstate, location)));
+
+		for (i = 0; i < tupdesc->natts; i++)
+		{
+			Form_pg_attribute att = TupleDescAttr(tupdesc, i);
+
+			if (strcmp(attrname, NameStr(att->attname)) == 0 &&
+				!att->attisdropped)
+			{
+				/* success, so generate a FieldSelect expression */
+				FieldSelect *fselect = makeNode(FieldSelect);
+
+				fselect->arg = (Expr *) param;
+				fselect->fieldnum = i + 1;
+				fselect->resulttype = att->atttypid;
+				fselect->resulttypmod = att->atttypmod;
+				/* save attribute's collation for parse_collate.c */
+				fselect->resultcollid = att->attcollation;
+
+				ReleaseTupleDesc(tupdesc);
+				return (Node *) fselect;
+			}
+		}
+
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_COLUMN),
+				 errmsg("could not identify column \"%s\" in variable", attrname),
+				 parser_errposition(pstate, location)));
+	}
+
+	return (Node *) param;
+}
+
 static Node *
 transformParamRef(ParseState *pstate, ParamRef *pref)
 {
@@ -1815,6 +2025,7 @@ transformSubLink(ParseState *pstate, SubLink *sublink)
 		case EXPR_KIND_VALUES:
 		case EXPR_KIND_VALUES_SINGLE:
 		case EXPR_KIND_CYCLE_MARK:
+		case EXPR_KIND_LET_TARGET:
 			/* okay */
 			break;
 		case EXPR_KIND_CHECK_CONSTRAINT:
@@ -1858,6 +2069,9 @@ transformSubLink(ParseState *pstate, SubLink *sublink)
 		case EXPR_KIND_GENERATED_COLUMN:
 			err = _("cannot use subquery in column generation expression");
 			break;
+		case EXPR_KIND_ASSIGN_TARGET:
+			err = _("cannot use subquery as target of assign statement");
+			break;
 
 			/*
 			 * There is intentionally no default: case here, so that the
@@ -3197,6 +3411,10 @@ ParseExprKindName(ParseExprKind exprKind)
 			return "GENERATED AS";
 		case EXPR_KIND_CYCLE_MARK:
 			return "CYCLE";
+		case EXPR_KIND_ASSIGN_TARGET:
+			return "ASSIGN";
+		case EXPR_KIND_LET_TARGET:
+			return "LET";
 
 			/*
 			 * There is intentionally no default: case here, so that the
diff --git a/src/backend/parser/parse_func.c b/src/backend/parser/parse_func.c
index 9b23344a3b1..9aa4f60768b 100644
--- a/src/backend/parser/parse_func.c
+++ b/src/backend/parser/parse_func.c
@@ -2656,6 +2656,8 @@ check_srf_call_placement(ParseState *pstate, Node *last_srf, int location)
 			err = _("set-returning functions are not allowed in column generation expressions");
 			break;
 		case EXPR_KIND_CYCLE_MARK:
+		case EXPR_KIND_ASSIGN_TARGET:
+		case EXPR_KIND_LET_TARGET:
 			errkind = true;
 			break;
 
diff --git a/src/backend/tcop/dest.c b/src/backend/tcop/dest.c
index 96f80b30463..dac78ba5cce 100644
--- a/src/backend/tcop/dest.c
+++ b/src/backend/tcop/dest.c
@@ -38,6 +38,7 @@
 #include "executor/functions.h"
 #include "executor/tqueue.h"
 #include "executor/tstoreReceiver.h"
+#include "executor/svariableReceiver.h"
 #include "libpq/libpq.h"
 #include "libpq/pqformat.h"
 
@@ -155,6 +156,9 @@ CreateDestReceiver(CommandDest dest)
 
 		case DestExplainSerialize:
 			return CreateExplainSerializeDestReceiver(NULL);
+
+		case DestVariable:
+			return CreateVariableDestReceiver(InvalidOid);
 	}
 
 	/* should never get here */
@@ -191,6 +195,7 @@ EndCommand(const QueryCompletion *qc, CommandDest dest, bool force_undecorated_o
 		case DestTransientRel:
 		case DestTupleQueue:
 		case DestExplainSerialize:
+		case DestVariable:
 			break;
 	}
 }
@@ -237,6 +242,7 @@ NullCommand(CommandDest dest)
 		case DestTransientRel:
 		case DestTupleQueue:
 		case DestExplainSerialize:
+		case DestVariable:
 			break;
 	}
 }
@@ -281,6 +287,7 @@ ReadyForQuery(CommandDest dest)
 		case DestTransientRel:
 		case DestTupleQueue:
 		case DestExplainSerialize:
+		case DestVariable:
 			break;
 	}
 }
diff --git a/src/backend/tcop/pquery.c b/src/backend/tcop/pquery.c
index 0c45fcf318f..85b809cb071 100644
--- a/src/backend/tcop/pquery.c
+++ b/src/backend/tcop/pquery.c
@@ -86,6 +86,9 @@ CreateQueryDesc(PlannedStmt *plannedstmt,
 	qd->queryEnv = queryEnv;
 	qd->instrument_options = instrument_options;	/* instrumentation wanted? */
 
+	qd->num_session_variables = 0;
+	qd->session_variables = NULL;
+
 	/* null these fields until set by ExecutorStart */
 	qd->tupDesc = NULL;
 	qd->estate = NULL;
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 217c6028497..69d2f7e1ced 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -49,6 +49,7 @@
 #include "commands/schemacmds.h"
 #include "commands/seclabel.h"
 #include "commands/sequence.h"
+#include "commands/session_variable.h"
 #include "commands/subscriptioncmds.h"
 #include "commands/tablecmds.h"
 #include "commands/tablespace.h"
@@ -235,6 +236,7 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree)
 
 		case T_CallStmt:
 		case T_DoStmt:
+		case T_LetStmt:
 			{
 				/*
 				 * Commands inside the DO block or the called procedure might
@@ -1067,6 +1069,11 @@ standard_ProcessUtility(PlannedStmt *pstmt,
 				break;
 			}
 
+		case T_LetStmt:
+			ExecuteLetStmt(pstate, (LetStmt *) parsetree, params,
+						   queryEnv, qc);
+			break;
+
 		default:
 			/* All other statement types have event trigger support */
 			ProcessUtilitySlow(pstate, pstmt, queryString,
@@ -2206,6 +2213,10 @@ UtilityContainsQuery(Node *parsetree)
 				return UtilityContainsQuery(qry->utilityStmt);
 			return qry;
 
+		case T_LetStmt:
+			qry = castNode(Query, ((LetStmt *) parsetree)->query);
+			return qry;
+
 		default:
 			return NULL;
 	}
@@ -2404,6 +2415,10 @@ CreateCommandTag(Node *parsetree)
 			tag = CMDTAG_SELECT;
 			break;
 
+		case T_LetStmt:
+			tag = CMDTAG_LET;
+			break;
+
 			/* utility statements --- same whether raw or cooked */
 		case T_TransactionStmt:
 			{
@@ -3289,6 +3304,7 @@ GetCommandLogLevel(Node *parsetree)
 			break;
 
 		case T_PLAssignStmt:
+		case T_LetStmt:
 			lev = LOGSTMT_ALL;
 			break;
 
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index a39068d1bf1..e7f74947cde 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -37,6 +37,7 @@
 #include "catalog/pg_statistic_ext.h"
 #include "catalog/pg_trigger.h"
 #include "catalog/pg_type.h"
+#include "catalog/pg_variable.h"
 #include "commands/defrem.h"
 #include "commands/tablespace.h"
 #include "common/keywords.h"
@@ -528,6 +529,7 @@ static char *generate_function_name(Oid funcid, int nargs,
 static char *generate_operator_name(Oid operid, Oid arg1, Oid arg2);
 static void add_cast_to(StringInfo buf, Oid typid);
 static char *generate_qualified_type_name(Oid typid);
+static char *generate_session_variable_name(Oid varid);
 static text *string_to_text(char *str);
 static char *flatten_reloptions(Oid relid);
 static void get_reloptions(StringInfo buf, Datum reloptions);
@@ -8617,6 +8619,14 @@ get_parameter(Param *param, deparse_context *context)
 		return;
 	}
 
+	/* translate paramvarid to session variable name */
+	if (param->paramkind == PARAM_VARIABLE)
+	{
+		appendStringInfo(context->buf, "%s",
+						 generate_session_variable_name(param->paramvarid));
+		return;
+	}
+
 	/*
 	 * Alternatively, maybe it's a subplan output, which we print as a
 	 * reference to the subplan.  (We could drill down into the subplan and
@@ -13399,6 +13409,42 @@ generate_collation_name(Oid collid)
 	return result;
 }
 
+/*
+ * generate_session_variable_name
+ *		Compute the name to display for a session variable specified by OID
+ *
+ * The result includes all necessary quoting and schema-prefixing.
+ */
+static char *
+generate_session_variable_name(Oid varid)
+{
+	HeapTuple	tup;
+	Form_pg_variable varform;
+	char	   *varname;
+	char	   *nspname;
+	char	   *result;
+
+	tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid));
+
+	if (!HeapTupleIsValid(tup))
+		elog(ERROR, "cache lookup failed for variable %u", varid);
+
+	varform = (Form_pg_variable) GETSTRUCT(tup);
+
+	varname = NameStr(varform->varname);
+
+	if (!VariableIsVisible(varid))
+		nspname = get_namespace_name_or_temp(varform->varnamespace);
+	else
+		nspname = NULL;
+
+	result = quote_qualified_identifier(nspname, varname);
+
+	ReleaseSysCache(tup);
+
+	return result;
+}
+
 /*
  * Given a C string, produce a TEXT datum.
  *
diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c
index 5af1a168ec2..7e1e9e28f12 100644
--- a/src/backend/utils/cache/plancache.c
+++ b/src/backend/utils/cache/plancache.c
@@ -58,6 +58,7 @@
 
 #include "access/transam.h"
 #include "catalog/namespace.h"
+#include "catalog/pg_variable.h"
 #include "executor/executor.h"
 #include "miscadmin.h"
 #include "nodes/nodeFuncs.h"
@@ -162,6 +163,7 @@ InitPlanCache(void)
 	CacheRegisterSyscacheCallback(AMOPOPID, PlanCacheSysCallback, (Datum) 0);
 	CacheRegisterSyscacheCallback(FOREIGNSERVEROID, PlanCacheSysCallback, (Datum) 0);
 	CacheRegisterSyscacheCallback(FOREIGNDATAWRAPPEROID, PlanCacheSysCallback, (Datum) 0);
+	CacheRegisterSyscacheCallback(VARIABLEOID, PlanCacheObjectCallback, (Datum) 0);
 }
 
 /*
@@ -1903,18 +1905,33 @@ ScanQueryForLocks(Query *parsetree, bool acquire)
 
 	/*
 	 * Recurse into sublink subqueries, too.  But we already did the ones in
-	 * the rtable and cteList.
+	 * the rtable and cteList.  We need to force a recursive call for session
+	 * variables too, to find and lock variables used in the query (see
+	 * ScanQueryWalker).
 	 */
-	if (parsetree->hasSubLinks)
+	if (parsetree->hasSubLinks ||
+		parsetree->hasSessionVariables)
 	{
 		query_tree_walker(parsetree, ScanQueryWalker,
 						  (void *) &acquire,
 						  QTW_IGNORE_RC_SUBQUERIES);
 	}
+
+	/* process session variables */
+	if (OidIsValid(parsetree->resultVariable))
+	{
+		if (acquire)
+			LockDatabaseObject(VariableRelationId, parsetree->resultVariable,
+							   0, AccessShareLock);
+		else
+			UnlockDatabaseObject(VariableRelationId, parsetree->resultVariable,
+								 0, AccessShareLock);
+	}
 }
 
 /*
- * Walker to find sublink subqueries for ScanQueryForLocks
+ * Walker to find sublink subqueries or referenced session variables
+ * for ScanQueryForLocks
  */
 static bool
 ScanQueryWalker(Node *node, bool *acquire)
@@ -1929,6 +1946,20 @@ ScanQueryWalker(Node *node, bool *acquire)
 		ScanQueryForLocks(castNode(Query, sub->subselect), *acquire);
 		/* Fall through to process lefthand args of SubLink */
 	}
+	else if (IsA(node, Param))
+	{
+		Param	   *p = (Param *) node;
+
+		if (p->paramkind == PARAM_VARIABLE)
+		{
+			if (acquire)
+				LockDatabaseObject(VariableRelationId, p->paramvarid,
+								   0, AccessShareLock);
+			else
+				UnlockDatabaseObject(VariableRelationId, p->paramvarid,
+									 0, AccessShareLock);
+		}
+	}
 
 	/*
 	 * Do NOT recurse into Query nodes, because ScanQueryForLocks already
@@ -2060,7 +2091,9 @@ PlanCacheRelCallback(Datum arg, Oid relid)
 
 /*
  * PlanCacheObjectCallback
- *		Syscache inval callback function for PROCOID and TYPEOID caches
+ *		Syscache inval callback function for TYPEOID, PROCOID, NAMESPACEOID,
+ *		OPEROID, AMOPOPID, FOREIGNSERVEROID, FOREIGNDATAWRAPPEROID and
+ *		VARIABLEOID caches.
  *
  * Invalidate all plans mentioning the object with the specified hash value,
  * or all plans mentioning any member of this cache if hashvalue == 0.
diff --git a/src/backend/utils/fmgr/fmgr.c b/src/backend/utils/fmgr/fmgr.c
index e48a86be54b..eaae0c5a933 100644
--- a/src/backend/utils/fmgr/fmgr.c
+++ b/src/backend/utils/fmgr/fmgr.c
@@ -2026,9 +2026,13 @@ get_call_expr_arg_stable(Node *expr, int argnum)
 	 */
 	if (IsA(arg, Const))
 		return true;
-	if (IsA(arg, Param) &&
-		((Param *) arg)->paramkind == PARAM_EXTERN)
-		return true;
+	if (IsA(arg, Param))
+	{
+		Param	   *p = (Param *) arg;
+
+		if (p->paramkind == PARAM_EXTERN || p->paramkind == PARAM_VARIABLE)
+			return true;
+	}
 
 	return false;
 }
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 293648e30c7..ec572815c94 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -1214,8 +1214,8 @@ static const char *const sql_commands[] = {
 	"ABORT", "ALTER", "ANALYZE", "BEGIN", "CALL", "CHECKPOINT", "CLOSE", "CLUSTER",
 	"COMMENT", "COMMIT", "COPY", "CREATE", "DEALLOCATE", "DECLARE",
 	"DELETE FROM", "DISCARD", "DO", "DROP", "END", "EXECUTE", "EXPLAIN",
-	"FETCH", "GRANT", "IMPORT FOREIGN SCHEMA", "INSERT INTO", "LISTEN", "LOAD", "LOCK",
-	"MERGE INTO", "MOVE", "NOTIFY", "PREPARE",
+	"FETCH", "GRANT", "IMPORT FOREIGN SCHEMA", "INSERT INTO", "LET",
+	"LISTEN", "LOAD", "LOCK", "MERGE INTO", "MOVE", "NOTIFY", "PREPARE",
 	"REASSIGN", "REFRESH MATERIALIZED VIEW", "REINDEX", "RELEASE",
 	"RESET", "REVOKE", "ROLLBACK",
 	"SAVEPOINT", "SECURITY LABEL", "SELECT", "SET", "SHOW", "START",
@@ -4629,6 +4629,14 @@ match_previous_words(int pattern_id,
 	else if (TailMatches("VALUES") && !TailMatches("DEFAULT", "VALUES"))
 		COMPLETE_WITH("(");
 
+/* LET */
+	/* If prev. word is LET suggest a list of variables */
+	else if (Matches("LET"))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables);
+	/* Complete LET <variable> with "=" */
+	else if (TailMatches("LET", MatchAny))
+		COMPLETE_WITH("=");
+
 /* LOCK */
 	/* Complete LOCK [TABLE] [ONLY] with a list of tables */
 	else if (Matches("LOCK"))
diff --git a/src/include/catalog/namespace.h b/src/include/catalog/namespace.h
index 18ea6ac83a5..def691dafb3 100644
--- a/src/include/catalog/namespace.h
+++ b/src/include/catalog/namespace.h
@@ -171,7 +171,9 @@ extern SearchPathMatcher *GetSearchPathMatcher(MemoryContext context);
 extern SearchPathMatcher *CopySearchPathMatcher(SearchPathMatcher *path);
 extern bool SearchPathMatchesCurrentEnvironment(SearchPathMatcher *path);
 
+extern List *NamesFromList(List *names);
 extern Oid	LookupVariable(const char *nspname, const char *varname, bool missing_ok);
+extern Oid	IdentifyVariable(List *names, char **attrname, bool *not_unique, bool noerror);
 
 extern Oid	get_collation_oid(List *collname, bool missing_ok);
 extern Oid	get_conversion_oid(List *conname, bool missing_ok);
diff --git a/src/include/catalog/pg_variable.h b/src/include/catalog/pg_variable.h
index 3810e040f28..db593cd5bed 100644
--- a/src/include/catalog/pg_variable.h
+++ b/src/include/catalog/pg_variable.h
@@ -35,6 +35,14 @@ CATALOG(pg_variable,9222,VariableRelationId)
 	/* OID of entry in pg_type for variable's type */
 	Oid			vartype BKI_LOOKUP(pg_type);
 
+	/*
+	 * Used for identity check [oid, create_lsn].
+	 *
+	 * This column of the 8-byte XlogRecPtr type should be at an address that
+	 * is divisible by 8, but before any column of type NameData.
+	 */
+	XLogRecPtr	varcreate_lsn;
+
 	/* variable name */
 	NameData	varname;
 
diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h
new file mode 100644
index 00000000000..b3f03c65827
--- /dev/null
+++ b/src/include/commands/session_variable.h
@@ -0,0 +1,32 @@
+/*-------------------------------------------------------------------------
+ *
+ * sessionvariable.h
+ *	  prototypes for sessionvariable.c.
+ *
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/commands/session_variable.h
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef SESSIONVARIABLE_H
+#define SESSIONVARIABLE_H
+
+#include "nodes/params.h"
+#include "nodes/parsenodes.h"
+#include "parser/parse_node.h"
+#include "tcop/cmdtag.h"
+#include "utils/queryenvironment.h"
+
+extern void SetSessionVariable(Oid varid, Datum value, bool isNull);
+extern void SetSessionVariableWithSecurityCheck(Oid varid, Datum value, bool isNull);
+extern Datum GetSessionVariable(Oid varid, bool *isNull, Oid *typid);
+extern Datum GetSessionVariableWithTypeCheck(Oid varid, bool *isNull, Oid expected_typid);
+
+extern void ExecuteLetStmt(ParseState *pstate, LetStmt *stmt, ParamListInfo params,
+						   QueryEnvironment *queryEnv, QueryCompletion *qc);
+
+#endif
diff --git a/src/include/executor/execdesc.h b/src/include/executor/execdesc.h
index 0a7274e26c8..d1b2f59e0cf 100644
--- a/src/include/executor/execdesc.h
+++ b/src/include/executor/execdesc.h
@@ -48,6 +48,10 @@ typedef struct QueryDesc
 	EState	   *estate;			/* executor's query-wide state */
 	PlanState  *planstate;		/* tree of per-plan-node state */
 
+	/* reference to session variables buffer */
+	int			num_session_variables;
+	SessionVariableValue *session_variables;
+
 	/* This field is set by ExecutorRun */
 	bool		already_executed;	/* true if previously executed */
 
diff --git a/src/include/executor/svariableReceiver.h b/src/include/executor/svariableReceiver.h
new file mode 100644
index 00000000000..db44d8b94c6
--- /dev/null
+++ b/src/include/executor/svariableReceiver.h
@@ -0,0 +1,22 @@
+/*-------------------------------------------------------------------------
+ *
+ * svariableReceiver.h
+ *	  prototypes for svariableReceiver.c
+ *
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/executor/svariableReceiver.h
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef SVARIABLE_RECEIVER_H
+#define SVARIABLE_RECEIVER_H
+
+#include "tcop/dest.h"
+
+extern DestReceiver *CreateVariableDestReceiver(Oid varid);
+
+#endif							/* SVARIABLE_RECEIVER_H */
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 182a6956bb0..b4464edc22a 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -617,6 +617,18 @@ typedef struct AsyncRequest
 								 * tuples) */
 } AsyncRequest;
 
+/* ----------------
+ * SessionVariableValue
+ * ----------------
+ */
+typedef struct SessionVariableValue
+{
+	Oid			varid;
+	Oid			typid;
+	bool		isnull;
+	Datum		value;
+} SessionVariableValue;
+
 /* ----------------
  *	  EState information
  *
@@ -669,6 +681,10 @@ typedef struct EState
 	ParamListInfo es_param_list_info;	/* values of external params */
 	ParamExecData *es_param_exec_vals;	/* values of internal params */
 
+	/* Session variables info: */
+	int			es_num_session_variables;		/* number of used variables */
+	SessionVariableValue *es_session_variables;	/* array of copies of values */
+
 	QueryEnvironment *es_queryEnv;	/* query environment */
 
 	/* Other working state: */
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 0e08296b438..778db6ad4f2 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -142,6 +142,9 @@ typedef struct Query
 	 */
 	int			resultRelation pg_node_attr(query_jumble_ignore);
 
+	/* target variable of LET statement */
+	Oid			resultVariable;
+
 	/* has aggregates in tlist or havingQual */
 	bool		hasAggs pg_node_attr(query_jumble_ignore);
 	/* has window functions in tlist */
@@ -162,6 +165,8 @@ typedef struct Query
 	bool		hasRowSecurity pg_node_attr(query_jumble_ignore);
 	/* parser has added an RTE_GROUP RTE */
 	bool		hasGroupRTE pg_node_attr(query_jumble_ignore);
+	/* uses session variables */
+	bool		hasSessionVariables pg_node_attr(query_jumble_ignore);
 	/* is a RETURN statement */
 	bool		isReturn pg_node_attr(query_jumble_ignore);
 
@@ -2100,6 +2105,18 @@ typedef struct MergeStmt
 	ParseLoc	stmt_len;		/* length in bytes; 0 means "rest of string" */
 } MergeStmt;
 
+/* ----------------------
+ *		Let Statement
+ * ----------------------
+ */
+typedef struct LetStmt
+{
+	NodeTag		type;
+	List	   *target;			/* target variable */
+	Node	   *query;			/* source expression */
+	int			location;
+} LetStmt;
+
 /* ----------------------
  *		Select Statement
  *
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index add0f9e45fc..ea1f76d0fbb 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -163,6 +163,9 @@ typedef struct PlannerGlobal
 
 	/* partition descriptors */
 	PartitionDirectory partition_directory pg_node_attr(read_write_ignore);
+
+	/* list of used session variables */
+	List	   *sessionVariables;
 } PlannerGlobal;
 
 /* macro for fetching the Plan associated with a SubPlan node */
@@ -508,6 +511,8 @@ struct PlannerInfo
 	bool		placeholdersFrozen;
 	/* true if planning a recursive WITH item */
 	bool		hasRecursion;
+	/* true if session variables were used */
+	bool		hasSessionVariables;
 
 	/*
 	 * The rangetable index for the RTE_GROUP RTE, or 0 if there is no
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 52f29bcdb69..4549366cf59 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -49,7 +49,7 @@ typedef struct PlannedStmt
 
 	NodeTag		type;
 
-	CmdType		commandType;	/* select|insert|update|delete|merge|utility */
+	CmdType		commandType;	/* select|let|insert|update|delete|merge|utility */
 
 	uint64		queryId;		/* query identifier (copied from Query) */
 
@@ -94,6 +94,8 @@ typedef struct PlannedStmt
 
 	Node	   *utilityStmt;	/* non-null if this is utility stmt */
 
+	List	   *sessionVariables;	/* OIDs for PARAM_VARIABLE Params */
+
 	/* statement location in source string (copied from Query) */
 	ParseLoc	stmt_location;	/* start location, or -1 if unknown */
 	ParseLoc	stmt_len;		/* length in bytes; 0 means "rest of string" */
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index b0ef1952e8f..4b2424d6d34 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -361,6 +361,9 @@ typedef struct Const
  *				of the `paramid' field contain the SubLink's subLinkId, and
  *				the low-order 16 bits contain the column number.  (This type
  *				of Param is also converted to PARAM_EXEC during planning.)
+ *
+ *		PARAM_VARIABLE:  The parameter is a reference to a session variable
+ *				(paramid holds the variable's OID).
  */
 typedef enum ParamKind
 {
@@ -368,6 +371,7 @@ typedef enum ParamKind
 	PARAM_EXEC,
 	PARAM_SUBLINK,
 	PARAM_MULTIEXPR,
+	PARAM_VARIABLE,
 } ParamKind;
 
 typedef struct Param
@@ -380,6 +384,8 @@ typedef struct Param
 	int32		paramtypmod pg_node_attr(query_jumble_ignore);
 	/* OID of collation, or InvalidOid if none */
 	Oid			paramcollid pg_node_attr(query_jumble_ignore);
+	/* OID of session variable if it is used */
+	Oid			paramvarid;
 	/* token location, or -1 if unknown */
 	ParseLoc	location;
 } Param;
diff --git a/src/include/optimizer/planmain.h b/src/include/optimizer/planmain.h
index 93137261e48..61341c50cf7 100644
--- a/src/include/optimizer/planmain.h
+++ b/src/include/optimizer/planmain.h
@@ -124,4 +124,6 @@ extern void record_plan_function_dependency(PlannerInfo *root, Oid funcid);
 extern void record_plan_type_dependency(PlannerInfo *root, Oid typid);
 extern bool extract_query_dependencies_walker(Node *node, PlannerInfo *context);
 
+extern void pull_up_has_session_variables(PlannerInfo *root);
+
 #endif							/* PLANMAIN_H */
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index 56670e65b11..aefe51f335b 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -256,6 +256,7 @@ PG_KEYWORD("leading", LEADING, RESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("leakproof", LEAKPROOF, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("least", LEAST, COL_NAME_KEYWORD, BARE_LABEL)
 PG_KEYWORD("left", LEFT, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL)
+PG_KEYWORD("let", LET, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("level", LEVEL, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("like", LIKE, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL)
 PG_KEYWORD("limit", LIMIT, RESERVED_KEYWORD, AS_LABEL)
diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h
index 2375e95c107..c11fdb3d970 100644
--- a/src/include/parser/parse_node.h
+++ b/src/include/parser/parse_node.h
@@ -82,6 +82,8 @@ typedef enum ParseExprKind
 	EXPR_KIND_COPY_WHERE,		/* WHERE condition in COPY FROM */
 	EXPR_KIND_GENERATED_COLUMN, /* generation expression for a column */
 	EXPR_KIND_CYCLE_MARK,		/* cycle mark value */
+	EXPR_KIND_ASSIGN_TARGET,	/* PL/pgSQL assignment target */
+	EXPR_KIND_LET_TARGET,		/* LET target */
 } ParseExprKind;
 
 
@@ -244,6 +246,7 @@ struct ParseState
 	bool		p_hasTargetSRFs;
 	bool		p_hasSubLinks;
 	bool		p_hasModifyingCTE;
+	bool		p_hasSessionVariables;
 
 	Node	   *p_last_srf;		/* most recent set-returning func/op found */
 
diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h
index 9465df7b2ff..a921af2486f 100644
--- a/src/include/tcop/cmdtaglist.h
+++ b/src/include/tcop/cmdtaglist.h
@@ -186,6 +186,7 @@ PG_CMDTAG(CMDTAG_GRANT, "GRANT", true, false, false)
 PG_CMDTAG(CMDTAG_GRANT_ROLE, "GRANT ROLE", false, false, false)
 PG_CMDTAG(CMDTAG_IMPORT_FOREIGN_SCHEMA, "IMPORT FOREIGN SCHEMA", true, false, false)
 PG_CMDTAG(CMDTAG_INSERT, "INSERT", false, false, true)
+PG_CMDTAG(CMDTAG_LET, "LET", false, false, false)
 PG_CMDTAG(CMDTAG_LISTEN, "LISTEN", false, false, false)
 PG_CMDTAG(CMDTAG_LOAD, "LOAD", false, false, false)
 PG_CMDTAG(CMDTAG_LOCK_TABLE, "LOCK TABLE", false, false, false)
diff --git a/src/include/tcop/dest.h b/src/include/tcop/dest.h
index a3d521b6f97..38cc87f4e7a 100644
--- a/src/include/tcop/dest.h
+++ b/src/include/tcop/dest.h
@@ -97,6 +97,7 @@ typedef enum
 	DestTransientRel,			/* results sent to transient relation */
 	DestTupleQueue,				/* results sent to tuple queue */
 	DestExplainSerialize,		/* results are serialized and discarded */
+	DestVariable,				/* results sent to session variable */
 } CommandDest;
 
 /* ----------------
diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
index 86c5bd324a9..1361a8a8480 100644
--- a/src/pl/plpgsql/src/pl_exec.c
+++ b/src/pl/plpgsql/src/pl_exec.c
@@ -8099,7 +8099,8 @@ exec_is_simple_query(PLpgSQL_expr *expr)
 		query->sortClause ||
 		query->limitOffset ||
 		query->limitCount ||
-		query->setOperations)
+		query->setOperations ||
+		query->hasSessionVariables)
 		return false;
 
 	/*
diff --git a/src/test/regress/expected/session_variables.out b/src/test/regress/expected/session_variables.out
index d0aff56a01d..a2cb35e3d7d 100644
--- a/src/test/regress/expected/session_variables.out
+++ b/src/test/regress/expected/session_variables.out
@@ -56,3 +56,928 @@ DROP VARIABLE svartest.var1;
 DROP SCHEMA svartest;
 DROP ROLE regress_variable_reader;
 DROP ROLE regress_variable_owner;
+-- check access rights
+CREATE ROLE regress_noowner;
+CREATE VARIABLE var1 AS int;
+CREATE OR REPLACE FUNCTION sqlfx(int)
+RETURNS int AS $$ SELECT $1 + var1 $$ LANGUAGE sql;
+CREATE OR REPLACE FUNCTION sqlfx_sd(int)
+RETURNS int AS $$ SELECT $1 + var1 $$ LANGUAGE sql SECURITY DEFINER;
+CREATE OR REPLACE FUNCTION plpgsqlfx(int)
+RETURNS int AS $$ BEGIN RETURN $1 + var1; END $$ LANGUAGE plpgsql;
+CREATE OR REPLACE FUNCTION plpgsqlfx_sd(int)
+RETURNS int AS $$ BEGIN RETURN $1 + var1; END $$ LANGUAGE plpgsql SECURITY DEFINER;
+LET var1 = 10;
+-- should be ok
+SELECT var1;
+ var1 
+------
+   10
+(1 row)
+
+SELECT sqlfx(20);
+ sqlfx 
+-------
+    30
+(1 row)
+
+SELECT sqlfx_sd(20);
+ sqlfx_sd 
+----------
+       30
+(1 row)
+
+SELECT plpgsqlfx(20);
+ plpgsqlfx 
+-----------
+        30
+(1 row)
+
+SELECT plpgsqlfx_sd(20);
+ plpgsqlfx_sd 
+--------------
+           30
+(1 row)
+
+-- should fail
+SET ROLE TO regress_noowner;
+SELECT var1;
+ERROR:  permission denied for session variable var1
+SELECT sqlfx(20);
+ERROR:  permission denied for session variable var1
+CONTEXT:  SQL function "sqlfx" statement 1
+SELECT plpgsqlfx(20);
+ERROR:  permission denied for session variable var1
+CONTEXT:  PL/pgSQL expression "$1 + var1"
+PL/pgSQL function plpgsqlfx(integer) line 1 at RETURN
+-- should be ok
+SELECT sqlfx_sd(20);
+ sqlfx_sd 
+----------
+       30
+(1 row)
+
+SELECT plpgsqlfx_sd(20);
+ plpgsqlfx_sd 
+--------------
+           30
+(1 row)
+
+SET ROLE TO DEFAULT;
+GRANT SELECT ON VARIABLE var1 TO regress_noowner;
+-- should be ok
+SET ROLE TO regress_noowner;
+SELECT var1;
+ var1 
+------
+   10
+(1 row)
+
+SELECT sqlfx(20);
+ sqlfx 
+-------
+    30
+(1 row)
+
+SELECT plpgsqlfx(20);
+ plpgsqlfx 
+-----------
+        30
+(1 row)
+
+SET ROLE TO DEFAULT;
+DROP VARIABLE var1;
+DROP FUNCTION sqlfx(int);
+DROP FUNCTION plpgsqlfx(int);
+DROP FUNCTION sqlfx_sd(int);
+DROP FUNCTION plpgsqlfx_sd(int);
+DROP ROLE regress_noowner;
+-- use variables inside views
+CREATE VARIABLE var1 AS numeric;
+-- use variables in views
+CREATE VIEW test_view AS SELECT COALESCE(var1 + v, 0) AS result FROM generate_series(1,2) g(v);
+SELECT * FROM test_view;
+ result 
+--------
+      0
+      0
+(2 rows)
+
+LET var1 = 3.14;
+SELECT * FROM test_view;
+ result 
+--------
+   4.14
+   5.14
+(2 rows)
+
+-- start a new session
+\c
+SELECT * FROM test_view;
+ result 
+--------
+      0
+      0
+(2 rows)
+
+LET var1 = 3.14;
+SELECT * FROM test_view;
+ result 
+--------
+   4.14
+   5.14
+(2 rows)
+
+-- should fail, dependency
+DROP VARIABLE var1;
+ERROR:  cannot drop session variable var1 because other objects depend on it
+DETAIL:  view test_view depends on session variable var1
+HINT:  Use DROP ... CASCADE to drop the dependent objects too.
+-- should be ok
+DROP VARIABLE var1 CASCADE;
+NOTICE:  drop cascades to view test_view
+CREATE VARIABLE var1 text;
+CREATE VARIABLE var2 text;
+-- use variables in SQL functions
+CREATE OR REPLACE FUNCTION sqlfx1(varchar)
+RETURNS varchar AS $$ SELECT var1 || ', ' || $1 $$ LANGUAGE sql;
+CREATE OR REPLACE FUNCTION sqlfx2( varchar)
+RETURNS varchar AS $$ SELECT var2 || ', ' || $1 $$ LANGUAGE sql;
+LET var1 = 'str1';
+LET var2 = 'str2';
+SELECT sqlfx1(sqlfx2('Hello'));
+      sqlfx1       
+-------------------
+ str1, str2, Hello
+(1 row)
+
+-- inlining is blocked
+EXPLAIN (COSTS OFF, VERBOSE) SELECT sqlfx1(sqlfx2('Hello'));
+                      QUERY PLAN                      
+------------------------------------------------------
+ Result
+   Output: sqlfx1(sqlfx2('Hello'::character varying))
+(2 rows)
+
+DROP FUNCTION sqlfx1(varchar);
+DROP FUNCTION sqlfx2(varchar);
+DROP VARIABLE var1;
+DROP VARIABLE var2;
+-- access from cached plans should work
+CREATE VARIABLE var1 AS numeric;
+CREATE OR REPLACE FUNCTION plpgsqlfx()
+RETURNS numeric AS $$ BEGIN RETURN var1; END $$ LANGUAGE plpgsql;
+set plan_cache_mode TO force_generic_plan;
+LET var1 = 3.14;
+SELECT plpgsqlfx();
+ plpgsqlfx 
+-----------
+      3.14
+(1 row)
+
+LET var1 = 3.14 * 2;
+SELECT plpgsqlfx();
+ plpgsqlfx 
+-----------
+      6.28
+(1 row)
+
+DROP VARIABLE var1;
+-- dependency (plan invalidation) should work
+CREATE VARIABLE var1 AS numeric;
+LET var1 = 3.14 * 3;
+SELECT plpgsqlfx();
+ plpgsqlfx 
+-----------
+      9.42
+(1 row)
+
+LET var1 = 3.14 * 4;
+SELECT plpgsqlfx();
+ plpgsqlfx 
+-----------
+     12.56
+(1 row)
+
+DROP VARIABLE var1;
+DROP FUNCTION plpgsqlfx();
+set plan_cache_mode TO DEFAULT;
+-- usage LET statement in plpgsql should work
+CREATE VARIABLE var1 int;
+CREATE VARIABLE var2 numeric[];
+DO $$
+BEGIN
+  LET var2 = '{}'::int[];
+  FOR i IN 1..10
+  LOOP
+    LET var1 = i;
+    LET var2[var1] = i;
+  END LOOP;
+  RAISE NOTICE 'result array: %', var2;
+END;
+$$;
+NOTICE:  result array: {1,2,3,4,5,6,7,8,9,10}
+DROP VARIABLE var1;
+DROP VARIABLE var2;
+-- CALL statement is not supported yet
+-- requires direct access to session variable from expression executor
+CREATE VARIABLE v int;
+CREATE PROCEDURE p(arg int) AS $$ BEGIN RAISE NOTICE '%', arg; END $$ LANGUAGE plpgsql;
+-- should not crash (but is not supported yet)
+CALL p(v);
+ERROR:  session variable cannot be used as an argument
+DO $$ BEGIN CALL p(v); END $$;
+ERROR:  session variable cannot be used as an argument
+CONTEXT:  SQL statement "CALL p(v)"
+PL/pgSQL function inline_code_block line 1 at CALL
+DROP PROCEDURE p(int);
+DROP VARIABLE v;
+-- EXECUTE statement is not supported yet
+-- requires direct access to session variable from expression executor
+CREATE VARIABLE v int;
+LET v = 20;
+PREPARE ptest(int) AS SELECT $1;
+-- should fail
+EXECUTE ptest(v);
+ERROR:  session variable cannot be used as an argument
+DEALLOCATE ptest;
+DROP VARIABLE v;
+-- test search path
+CREATE SCHEMA svartest;
+CREATE VARIABLE svartest.var1 AS numeric;
+-- should fail
+LET var1 = pi();
+ERROR:  session variable "var1" doesn't exist
+LINE 1: LET var1 = pi();
+            ^
+SELECT var1;
+ERROR:  column "var1" does not exist
+LINE 1: SELECT var1;
+               ^
+-- should be ok
+LET svartest.var1 = pi();
+SELECT svartest.var1;
+       var1       
+------------------
+ 3.14159265358979
+(1 row)
+
+SET search_path TO svartest;
+-- should be ok
+LET var1 = pi() + 10;
+SELECT var1;
+       var1       
+------------------
+ 13.1415926535898
+(1 row)
+
+RESET search_path;
+DROP SCHEMA svartest CASCADE;
+NOTICE:  drop cascades to session variable svartest.var1
+CREATE VARIABLE var1 AS text;
+-- variables can be updated under RO transaction
+BEGIN;
+SET TRANSACTION READ ONLY;
+LET var1 = 'hello';
+COMMIT;
+SELECT var1;
+ var1  
+-------
+ hello
+(1 row)
+
+DROP VARIABLE var1;
+-- test of domains
+CREATE DOMAIN int_domain AS int NOT NULL CHECK (VALUE > 100);
+CREATE VARIABLE var1 AS int_domain;
+-- should fail
+SELECT var1;
+ERROR:  domain int_domain does not allow null values
+-- should be ok
+LET var1 = 1000;
+SELECT var1;
+ var1 
+------
+ 1000
+(1 row)
+
+-- should fail
+LET var1 = 10;
+ERROR:  value for domain int_domain violates check constraint "int_domain_check"
+-- should fail
+LET var1 = NULL;
+ERROR:  domain int_domain does not allow null values
+-- note - domain defaults are not supported yet (like PLpgSQL)
+DROP VARIABLE var1;
+DROP DOMAIN int_domain;
+-- the expression should remain "unknown"
+CREATE VARIABLE var1 AS int4multirange[];
+-- should be ok
+LET var1 = NULL;
+LET var1 = '{"{[2,8),[11,14)}","{[5,8),[12,14)}"}';
+LET var1[2] = '{[5,8),[12,100)}';
+SELECT var1;
+                  var1                  
+----------------------------------------
+ {"{[2,8),[11,14)}","{[5,8),[12,100)}"}
+(1 row)
+
+--It should work in plpgsql too
+DO $$
+BEGIN
+  LET var1 = NULL;
+  LET var1 = '{"{[2,8),[11,14)}","{[5,8),[12,14)}"}';
+  LET var1[2] = '{[5,8),[12,100)}';
+
+  RAISE NOTICE '%', var1;
+END;
+$$;
+NOTICE:  {"{[2,8),[11,14)}","{[5,8),[12,100)}"}
+DROP VARIABLE var1;
+CREATE SCHEMA svartest CREATE VARIABLE var1 AS int CREATE TABLE foo(a int);
+LET svartest.var1 = 100;
+SELECT svartest.var1;
+ var1 
+------
+  100
+(1 row)
+
+SET search_path to public, svartest;
+SELECT var1;
+ var1 
+------
+  100
+(1 row)
+
+DROP SCHEMA svartest CASCADE;
+NOTICE:  drop cascades to 2 other objects
+DETAIL:  drop cascades to table foo
+drop cascades to session variable var1
+CREATE VARIABLE var1 AS int;
+CREATE VARIABLE var2 AS int[];
+LET var1 = 2;
+LET var2 = '{}'::int[];
+LET var2[var1] = 0;
+SELECT var2;
+   var2    
+-----------
+ [2:2]={0}
+(1 row)
+
+DROP VARIABLE var1, var2;
+CREATE VARIABLE var1 AS int;
+CREATE VARIABLE var2 AS int[];
+LET var1 = 2;
+LET var2 = '{}'::int[];
+SELECT var2;
+ var2 
+------
+ {}
+(1 row)
+
+DROP VARIABLE var1, var2;
+-- the LET statement should be disallowed in CTE
+CREATE VARIABLE var1 AS int;
+WITH x AS (LET var1 = 100) SELECT * FROM x;
+ERROR:  syntax error at or near "LET"
+LINE 1: WITH x AS (LET var1 = 100) SELECT * FROM x;
+                   ^
+-- should be ok
+LET var1 = generate_series(1, 1);
+-- should fail
+LET var1 = generate_series(1, 2);
+ERROR:  expression returned more than one row
+LET var1 = generate_series(1, 0);
+ERROR:  expression returned no rows
+DROP VARIABLE var1;
+-- composite variables
+CREATE TYPE sv_xyz AS (x int, y int, z numeric(10,2));
+CREATE VARIABLE v1 AS sv_xyz;
+CREATE VARIABLE v2 AS sv_xyz;
+LET v1 = (1, 2, 3.14);
+LET v2 = (10, 20, 3.14 * 10);
+-- should work too - there are prepared casts
+LET v1 = (1, 2, 3);
+SELECT v1;
+     v1     
+------------
+ (1,2,3.00)
+(1 row)
+
+SELECT v2;
+      v2       
+---------------
+ (10,20,31.40)
+(1 row)
+
+SELECT (v1).*;
+ x | y |  z   
+---+---+------
+ 1 | 2 | 3.00
+(1 row)
+
+SELECT (v2).*;
+ x  | y  |   z   
+----+----+-------
+ 10 | 20 | 31.40
+(1 row)
+
+SELECT v1.x + v1.z;
+ ?column? 
+----------
+     4.00
+(1 row)
+
+SELECT v2.x + v2.z;
+ ?column? 
+----------
+    41.40
+(1 row)
+
+-- access to composite fields should be safe too
+CREATE ROLE regress_var_test_role;
+SET ROLE TO regress_var_test_role;
+-- should fail
+SELECT v2.x;
+ERROR:  permission denied for session variable v2
+SET ROLE TO DEFAULT;
+DROP VARIABLE v1;
+DROP VARIABLE v2;
+DROP TYPE sv_xyz;
+DROP ROLE regress_var_test_role;
+CREATE TYPE t1 AS (a int, b numeric, c text);
+CREATE VARIABLE v1 AS t1;
+LET v1 = (1, pi(), 'hello');
+SELECT v1;
+             v1             
+----------------------------
+ (1,3.14159265358979,hello)
+(1 row)
+
+LET v1.b = 10.2222;
+SELECT v1;
+        v1         
+-------------------
+ (1,10.2222,hello)
+(1 row)
+
+-- should fail, attribute doesn't exist
+LET v1.x = 10;
+ERROR:  cannot assign to field "x" of column "v1" because there is no such column in data type t1
+LINE 1: LET v1.x = 10;
+            ^
+-- should fail, don't allow multi column query
+LET v1 = (NULL::t1).*;
+ERROR:  assignment expression returned 3 columns
+LINE 1: LET v1 = (NULL::t1).*;
+                  ^
+-- allow DROP or ADD ATTRIBUTE on composite types
+-- should be ok
+ALTER TYPE t1 DROP ATTRIBUTE c;
+SELECT v1;
+     v1      
+-------------
+ (1,10.2222)
+(1 row)
+
+-- should be ok
+ALTER TYPE t1 ADD ATTRIBUTE c int;
+SELECT v1;
+      v1      
+--------------
+ (1,10.2222,)
+(1 row)
+
+LET v1 = (10, 10.3, 20);
+SELECT v1;
+      v1      
+--------------
+ (10,10.3,20)
+(1 row)
+
+-- should be ok
+ALTER TYPE t1 DROP ATTRIBUTE b;
+SELECT v1;
+   v1    
+---------
+ (10,20)
+(1 row)
+
+-- should fail, disallow data type change
+ALTER TYPE t1 ALTER ATTRIBUTE c TYPE int;
+ERROR:  cannot alter type "t1" because session variable "public.v1" uses it
+DROP VARIABLE v1;
+DROP TYPE t1;
+-- the table type can be used as composite type too
+CREATE TABLE svar_test(a int, b numeric, c date);
+CREATE VARIABLE var1 AS svar_test;
+LET var1 = (10, pi(), '2023-05-26');
+SELECT var1;
+               var1               
+----------------------------------
+ (10,3.14159265358979,05-26-2023)
+(1 row)
+
+-- should fail due dependency
+ALTER TABLE svar_test ALTER COLUMN a TYPE text;
+ERROR:  cannot alter table "svar_test" because session variable "public.var1" uses it
+-- should fail
+DROP TABLE svar_test;
+ERROR:  cannot drop table svar_test because other objects depend on it
+DETAIL:  session variable var1 depends on type svar_test
+HINT:  Use DROP ... CASCADE to drop the dependent objects too.
+DROP VARIABLE var1;
+DROP TABLE svar_test;
+-- arrays are supported
+CREATE VARIABLE var1 AS numeric[];
+LET var1 = ARRAY[1.1,2.1];
+LET var1[1] = 10.1;
+SELECT var1;
+    var1    
+------------
+ {10.1,2.1}
+(1 row)
+
+-- LET target doesn't allow srf, should fail
+LET var1[generate_series(1,3)] = 100;
+ERROR:  set-returning functions are not allowed in LET
+LINE 1: LET var1[generate_series(1,3)] = 100;
+                 ^
+DROP VARIABLE var1;
+-- arrays inside composite
+CREATE TYPE t1 AS (a numeric, b numeric[]);
+CREATE VARIABLE var1 AS t1;
+LET var1 = (10.1, ARRAY[0.0, 0.0]);
+LET var1.a = 10.2;
+SELECT var1;
+        var1        
+--------------------
+ (10.2,"{0.0,0.0}")
+(1 row)
+
+LET var1.b[1] = 10.3;
+SELECT var1;
+        var1         
+---------------------
+ (10.2,"{10.3,0.0}")
+(1 row)
+
+DROP VARIABLE var1;
+DROP TYPE t1;
+-- Encourage use of parallel plans
+SET parallel_setup_cost = 0;
+SET parallel_tuple_cost = 0;
+SET min_parallel_table_scan_size = 0;
+SET max_parallel_workers_per_gather = 2;
+-- test on query with workers
+CREATE TABLE svar_test(a int);
+INSERT INTO svar_test SELECT * FROM generate_series(1,1000);
+ANALYZE svar_test;
+CREATE VARIABLE zero int;
+LET zero = 0;
+-- result should be 100
+SELECT count(*) FROM svar_test WHERE a%10 = zero;
+ count 
+-------
+   100
+(1 row)
+
+-- parallel execution is not supported yet
+EXPLAIN (COSTS OFF) SELECT count(*) FROM svar_test WHERE a%10 = zero;
+            QUERY PLAN             
+-----------------------------------
+ Aggregate
+   ->  Seq Scan on svar_test
+         Filter: ((a % 10) = zero)
+(3 rows)
+
+LET zero = (SELECT count(*) FROM svar_test);
+-- result should be 1000
+SELECT zero;
+ zero 
+------
+ 1000
+(1 row)
+
+DROP VARIABLE zero;
+DROP TABLE svar_test;
+RESET parallel_setup_cost;
+RESET parallel_tuple_cost;
+RESET min_parallel_table_scan_size;
+RESET max_parallel_workers_per_gather;
+-- the result of view should be same in parallel mode too
+CREATE VARIABLE var1 AS int;
+LET var1 = 10;
+CREATE VIEW var1view AS SELECT COALESCE(var1, 0) AS result;
+SELECT * FROM var1view;
+ result 
+--------
+     10
+(1 row)
+
+SET debug_parallel_query TO on;
+SELECT * FROM var1view;
+ result 
+--------
+     10
+(1 row)
+
+SET debug_parallel_query TO off;
+DROP VIEW var1view;
+DROP VARIABLE var1;
+CREATE VARIABLE varid int;
+CREATE TABLE svar_test(id int, v int);
+LET varid = 1;
+INSERT INTO svar_test VALUES(varid, 100);
+SELECT * FROM svar_test;
+ id |  v  
+----+-----
+  1 | 100
+(1 row)
+
+UPDATE svar_test SET v = 200 WHERE id = varid;
+SELECT * FROM svar_test;
+ id |  v  
+----+-----
+  1 | 200
+(1 row)
+
+DELETE FROM svar_test WHERE id = varid;
+SELECT * FROM svar_test;
+ id | v 
+----+---
+(0 rows)
+
+DROP TABLE svar_test;
+DROP VARIABLE varid;
+-- visibility check
+-- variables should be shadowed always
+CREATE VARIABLE var1 AS text;
+SELECT var1.relname FROM pg_class var1 WHERE var1.relname = 'pg_class';
+ relname  
+----------
+ pg_class
+(1 row)
+
+DROP VARIABLE var1;
+CREATE TABLE xxtab(avar int);
+INSERT INTO xxtab VALUES(333);
+CREATE TYPE xxtype AS (avar int);
+CREATE VARIABLE xxtab AS xxtype;
+INSERT INTO xxtab VALUES(10);
+-- it is ambiguous, but columns are preferred
+SELECT xxtab.avar FROM xxtab;
+ avar 
+------
+  333
+   10
+(2 rows)
+
+-- should be ok
+SELECT avar FROM xxtab;
+ avar 
+------
+  333
+   10
+(2 rows)
+
+CREATE VARIABLE public.avar AS int;
+-- should be ok, see the table
+SELECT avar FROM xxtab;
+ avar 
+------
+  333
+   10
+(2 rows)
+
+-- should be ok
+SELECT public.avar FROM xxtab;
+ avar 
+------
+     
+     
+(2 rows)
+
+DROP VARIABLE xxtab;
+SELECT xxtab.avar FROM xxtab;
+ avar 
+------
+  333
+   10
+(2 rows)
+
+DROP VARIABLE public.avar;
+DROP TYPE xxtype;
+DROP TABLE xxtab;
+-- The variable can be shadowed by table or by alias
+CREATE TYPE public.svar_type AS (a int, b int, c int);
+CREATE VARIABLE public.svar AS public.svar_type;
+CREATE TABLE public.svar(a int, b int);
+INSERT INTO public.svar VALUES(10, 20);
+LET public.svar = (100, 200, 300);
+-- should be ok
+-- show table
+SELECT * FROM public.svar;
+ a  | b  
+----+----
+ 10 | 20
+(1 row)
+
+SELECT svar.a FROM public.svar;
+ a  
+----
+ 10
+(1 row)
+
+SELECT svar.* FROM public.svar;
+ a  | b  
+----+----
+ 10 | 20
+(1 row)
+
+-- show variable
+SELECT public.svar;
+     svar      
+---------------
+ (100,200,300)
+(1 row)
+
+SELECT public.svar.c;
+  c  
+-----
+ 300
+(1 row)
+
+SELECT (public.svar).*;
+  a  |  b  |  c  
+-----+-----+-----
+ 100 | 200 | 300
+(1 row)
+
+-- the variable is shadowed, raise error
+SELECT public.svar.c FROM public.svar;
+ERROR:  column svar.c does not exist
+LINE 1: SELECT public.svar.c FROM public.svar;
+               ^
+-- can be fixed by alias
+SELECT public.svar.c FROM public.svar x;
+  c  
+-----
+ 300
+(1 row)
+
+SELECT svar.a FROM public.svar;
+ a  
+----
+ 10
+(1 row)
+
+SELECT svar.* FROM public.svar;
+ a  | b  
+----+----
+ 10 | 20
+(1 row)
+
+-- show variable
+SELECT public.svar;
+     svar      
+---------------
+ (100,200,300)
+(1 row)
+
+SELECT public.svar.c;
+  c  
+-----
+ 300
+(1 row)
+
+SELECT (public.svar).*;
+  a  |  b  |  c  
+-----+-----+-----
+ 100 | 200 | 300
+(1 row)
+
+-- the variable is shadowed, raise error
+SELECT public.svar.c FROM public.svar;
+ERROR:  column svar.c does not exist
+LINE 1: SELECT public.svar.c FROM public.svar;
+               ^
+-- can be fixed by alias
+SELECT public.svar.c FROM public.svar x;
+  c  
+-----
+ 300
+(1 row)
+
+DROP VARIABLE public.svar;
+DROP TABLE public.svar;
+DROP TYPE public.svar_type;
+CREATE TYPE ab AS (a integer, b integer);
+CREATE VARIABLE v_ab AS ab;
+CREATE TABLE v_ab (a integer, b integer);
+INSERT INTO v_ab VALUES(10,20);
+-- we should see table
+SELECT v_ab.a FROM v_ab;
+ a  
+----
+ 10
+(1 row)
+
+CREATE SCHEMA v_ab;
+CREATE VARIABLE v_ab.a AS integer;
+-- we should see table
+SELECT v_ab.a FROM v_ab;
+ a  
+----
+ 10
+(1 row)
+
+DROP VARIABLE v_ab;
+DROP TABLE v_ab;
+DROP TYPE ab;
+DROP VARIABLE v_ab.a;
+DROP SCHEMA v_ab;
+CREATE TYPE t_am_type AS (b int);
+CREATE SCHEMA xxx_am;
+SET search_path TO public;
+CREATE VARIABLE xxx_am AS t_am_type;
+LET xxx_am = ROW(10);
+-- should be ok
+SELECT xxx_am;
+ xxx_am 
+--------
+ (10)
+(1 row)
+
+CREATE VARIABLE xxx_am.b AS int;
+LET :"DBNAME".xxx_am.b = 20;
+-- should be still ok
+SELECT xxx_am;
+ xxx_am 
+--------
+ (10)
+(1 row)
+
+-- should fail, the reference should be ambiguous
+SELECT xxx_am.b;
+ERROR:  session variable reference "xxx_am.b" is ambiguous
+LINE 1: SELECT xxx_am.b;
+               ^
+-- enhanced references should be ok
+SELECT public.xxx_am.b;
+ b  
+----
+ 10
+(1 row)
+
+SELECT :"DBNAME".xxx_am.b;
+ b  
+----
+ 20
+(1 row)
+
+CREATE TABLE xxx_am(b  int);
+INSERT INTO xxx_am VALUES(10);
+-- we should see table
+SELECT xxx_am.b FROM xxx_am;
+ b  
+----
+ 10
+(1 row)
+
+SELECT x.b FROM xxx_am x;
+ b  
+----
+ 10
+(1 row)
+
+DROP TABLE xxx_am;
+DROP VARIABLE public.xxx_am;
+DROP VARIABLE xxx_am.b;
+DROP SCHEMA xxx_am;
+CREATE SCHEMA :"DBNAME";
+CREATE VARIABLE :"DBNAME".:"DBNAME".:"DBNAME" AS t_am_type;
+CREATE VARIABLE :"DBNAME".:"DBNAME".b AS int;
+SET search_path TO :"DBNAME";
+-- should be ambiguous
+SELECT :"DBNAME".b;
+ERROR:  session variable reference "regression.b" is ambiguous
+LINE 1: SELECT "regression".b;
+               ^
+-- should be ambiguous too
+SELECT :"DBNAME".:"DBNAME".b;
+ERROR:  session variable reference "regression.regression.b" is ambiguous
+LINE 1: SELECT "regression"."regression".b;
+               ^
+CREATE TABLE :"DBNAME"(b int);
+-- should be ok
+SELECT :"DBNAME".b FROM :"DBNAME";
+ b 
+---
+(0 rows)
+
+DROP TABLE :"DBNAME";
+DROP VARIABLE :"DBNAME".:"DBNAME".b;
+DROP VARIABLE :"DBNAME".:"DBNAME".:"DBNAME";
+DROP SCHEMA :"DBNAME";
+RESET search_path;
diff --git a/src/test/regress/sql/session_variables.sql b/src/test/regress/sql/session_variables.sql
index 82679cbf48e..6445479d22d 100644
--- a/src/test/regress/sql/session_variables.sql
+++ b/src/test/regress/sql/session_variables.sql
@@ -59,3 +59,642 @@ DROP VARIABLE svartest.var1;
 DROP SCHEMA svartest;
 DROP ROLE regress_variable_reader;
 DROP ROLE regress_variable_owner;
+
+-- check access rights
+CREATE ROLE regress_noowner;
+
+CREATE VARIABLE var1 AS int;
+
+CREATE OR REPLACE FUNCTION sqlfx(int)
+RETURNS int AS $$ SELECT $1 + var1 $$ LANGUAGE sql;
+
+CREATE OR REPLACE FUNCTION sqlfx_sd(int)
+RETURNS int AS $$ SELECT $1 + var1 $$ LANGUAGE sql SECURITY DEFINER;
+
+CREATE OR REPLACE FUNCTION plpgsqlfx(int)
+RETURNS int AS $$ BEGIN RETURN $1 + var1; END $$ LANGUAGE plpgsql;
+
+CREATE OR REPLACE FUNCTION plpgsqlfx_sd(int)
+RETURNS int AS $$ BEGIN RETURN $1 + var1; END $$ LANGUAGE plpgsql SECURITY DEFINER;
+
+LET var1 = 10;
+-- should be ok
+SELECT var1;
+SELECT sqlfx(20);
+SELECT sqlfx_sd(20);
+SELECT plpgsqlfx(20);
+SELECT plpgsqlfx_sd(20);
+
+-- should fail
+SET ROLE TO regress_noowner;
+
+SELECT var1;
+SELECT sqlfx(20);
+SELECT plpgsqlfx(20);
+
+-- should be ok
+SELECT sqlfx_sd(20);
+SELECT plpgsqlfx_sd(20);
+
+SET ROLE TO DEFAULT;
+GRANT SELECT ON VARIABLE var1 TO regress_noowner;
+
+-- should be ok
+SET ROLE TO regress_noowner;
+
+SELECT var1;
+SELECT sqlfx(20);
+SELECT plpgsqlfx(20);
+
+SET ROLE TO DEFAULT;
+DROP VARIABLE var1;
+DROP FUNCTION sqlfx(int);
+DROP FUNCTION plpgsqlfx(int);
+DROP FUNCTION sqlfx_sd(int);
+DROP FUNCTION plpgsqlfx_sd(int);
+
+DROP ROLE regress_noowner;
+
+-- use variables inside views
+CREATE VARIABLE var1 AS numeric;
+
+-- use variables in views
+CREATE VIEW test_view AS SELECT COALESCE(var1 + v, 0) AS result FROM generate_series(1,2) g(v);
+SELECT * FROM test_view;
+LET var1 = 3.14;
+SELECT * FROM test_view;
+
+-- start a new session
+\c
+
+SELECT * FROM test_view;
+LET var1 = 3.14;
+SELECT * FROM test_view;
+
+-- should fail, dependency
+DROP VARIABLE var1;
+
+-- should be ok
+DROP VARIABLE var1 CASCADE;
+
+CREATE VARIABLE var1 text;
+CREATE VARIABLE var2 text;
+
+-- use variables in SQL functions
+CREATE OR REPLACE FUNCTION sqlfx1(varchar)
+RETURNS varchar AS $$ SELECT var1 || ', ' || $1 $$ LANGUAGE sql;
+
+CREATE OR REPLACE FUNCTION sqlfx2( varchar)
+RETURNS varchar AS $$ SELECT var2 || ', ' || $1 $$ LANGUAGE sql;
+
+LET var1 = 'str1';
+LET var2 = 'str2';
+
+SELECT sqlfx1(sqlfx2('Hello'));
+
+-- inlining is blocked
+EXPLAIN (COSTS OFF, VERBOSE) SELECT sqlfx1(sqlfx2('Hello'));
+
+DROP FUNCTION sqlfx1(varchar);
+DROP FUNCTION sqlfx2(varchar);
+DROP VARIABLE var1;
+DROP VARIABLE var2;
+
+-- access from cached plans should work
+CREATE VARIABLE var1 AS numeric;
+
+CREATE OR REPLACE FUNCTION plpgsqlfx()
+RETURNS numeric AS $$ BEGIN RETURN var1; END $$ LANGUAGE plpgsql;
+
+set plan_cache_mode TO force_generic_plan;
+
+LET var1 = 3.14;
+SELECT plpgsqlfx();
+LET var1 = 3.14 * 2;
+SELECT plpgsqlfx();
+
+DROP VARIABLE var1;
+
+-- dependency (plan invalidation) should work
+CREATE VARIABLE var1 AS numeric;
+
+LET var1 = 3.14 * 3;
+SELECT plpgsqlfx();
+LET var1 = 3.14 * 4;
+SELECT plpgsqlfx();
+
+DROP VARIABLE var1;
+DROP FUNCTION plpgsqlfx();
+
+set plan_cache_mode TO DEFAULT;
+
+-- usage LET statement in plpgsql should work
+CREATE VARIABLE var1 int;
+CREATE VARIABLE var2 numeric[];
+
+DO $$
+BEGIN
+  LET var2 = '{}'::int[];
+  FOR i IN 1..10
+  LOOP
+    LET var1 = i;
+    LET var2[var1] = i;
+  END LOOP;
+  RAISE NOTICE 'result array: %', var2;
+END;
+$$;
+
+DROP VARIABLE var1;
+DROP VARIABLE var2;
+
+-- CALL statement is not supported yet
+-- requires direct access to session variable from expression executor
+CREATE VARIABLE v int;
+
+CREATE PROCEDURE p(arg int) AS $$ BEGIN RAISE NOTICE '%', arg; END $$ LANGUAGE plpgsql;
+
+-- should not crash (but is not supported yet)
+CALL p(v);
+
+DO $$ BEGIN CALL p(v); END $$;
+
+DROP PROCEDURE p(int);
+DROP VARIABLE v;
+
+-- EXECUTE statement is not supported yet
+-- requires direct access to session variable from expression executor
+CREATE VARIABLE v int;
+LET v = 20;
+PREPARE ptest(int) AS SELECT $1;
+
+-- should fail
+EXECUTE ptest(v);
+
+DEALLOCATE ptest;
+DROP VARIABLE v;
+
+-- test search path
+CREATE SCHEMA svartest;
+CREATE VARIABLE svartest.var1 AS numeric;
+
+-- should fail
+LET var1 = pi();
+SELECT var1;
+
+-- should be ok
+LET svartest.var1 = pi();
+SELECT svartest.var1;
+
+SET search_path TO svartest;
+
+-- should be ok
+LET var1 = pi() + 10;
+SELECT var1;
+
+RESET search_path;
+DROP SCHEMA svartest CASCADE;
+
+CREATE VARIABLE var1 AS text;
+
+-- variables can be updated under RO transaction
+BEGIN;
+SET TRANSACTION READ ONLY;
+LET var1 = 'hello';
+COMMIT;
+
+SELECT var1;
+
+DROP VARIABLE var1;
+
+-- test of domains
+CREATE DOMAIN int_domain AS int NOT NULL CHECK (VALUE > 100);
+CREATE VARIABLE var1 AS int_domain;
+
+-- should fail
+SELECT var1;
+
+-- should be ok
+LET var1 = 1000;
+SELECT var1;
+
+-- should fail
+LET var1 = 10;
+
+-- should fail
+LET var1 = NULL;
+
+-- note - domain defaults are not supported yet (like PLpgSQL)
+
+DROP VARIABLE var1;
+DROP DOMAIN int_domain;
+
+-- the expression should remain "unknown"
+CREATE VARIABLE var1 AS int4multirange[];
+-- should be ok
+LET var1 = NULL;
+LET var1 = '{"{[2,8),[11,14)}","{[5,8),[12,14)}"}';
+LET var1[2] = '{[5,8),[12,100)}';
+SELECT var1;
+
+--It should work in plpgsql too
+DO $$
+BEGIN
+  LET var1 = NULL;
+  LET var1 = '{"{[2,8),[11,14)}","{[5,8),[12,14)}"}';
+  LET var1[2] = '{[5,8),[12,100)}';
+
+  RAISE NOTICE '%', var1;
+END;
+$$;
+
+DROP VARIABLE var1;
+
+CREATE SCHEMA svartest CREATE VARIABLE var1 AS int CREATE TABLE foo(a int);
+LET svartest.var1 = 100;
+SELECT svartest.var1;
+
+SET search_path to public, svartest;
+
+SELECT var1;
+
+DROP SCHEMA svartest CASCADE;
+
+CREATE VARIABLE var1 AS int;
+CREATE VARIABLE var2 AS int[];
+
+LET var1 = 2;
+LET var2 = '{}'::int[];
+
+LET var2[var1] = 0;
+
+SELECT var2;
+
+DROP VARIABLE var1, var2;
+
+CREATE VARIABLE var1 AS int;
+CREATE VARIABLE var2 AS int[];
+
+LET var1 = 2;
+LET var2 = '{}'::int[];
+
+SELECT var2;
+
+DROP VARIABLE var1, var2;
+
+-- the LET statement should be disallowed in CTE
+CREATE VARIABLE var1 AS int;
+WITH x AS (LET var1 = 100) SELECT * FROM x;
+
+-- should be ok
+LET var1 = generate_series(1, 1);
+
+-- should fail
+LET var1 = generate_series(1, 2);
+LET var1 = generate_series(1, 0);
+
+DROP VARIABLE var1;
+
+-- composite variables
+CREATE TYPE sv_xyz AS (x int, y int, z numeric(10,2));
+
+CREATE VARIABLE v1 AS sv_xyz;
+CREATE VARIABLE v2 AS sv_xyz;
+
+LET v1 = (1, 2, 3.14);
+LET v2 = (10, 20, 3.14 * 10);
+
+-- should work too - there are prepared casts
+LET v1 = (1, 2, 3);
+
+SELECT v1;
+SELECT v2;
+SELECT (v1).*;
+SELECT (v2).*;
+
+SELECT v1.x + v1.z;
+SELECT v2.x + v2.z;
+
+-- access to composite fields should be safe too
+CREATE ROLE regress_var_test_role;
+
+SET ROLE TO regress_var_test_role;
+
+-- should fail
+SELECT v2.x;
+
+SET ROLE TO DEFAULT;
+
+DROP VARIABLE v1;
+DROP VARIABLE v2;
+DROP TYPE sv_xyz;
+DROP ROLE regress_var_test_role;
+
+CREATE TYPE t1 AS (a int, b numeric, c text);
+
+CREATE VARIABLE v1 AS t1;
+LET v1 = (1, pi(), 'hello');
+SELECT v1;
+LET v1.b = 10.2222;
+SELECT v1;
+
+-- should fail, attribute doesn't exist
+LET v1.x = 10;
+
+-- should fail, don't allow multi column query
+LET v1 = (NULL::t1).*;
+
+-- allow DROP or ADD ATTRIBUTE on composite types
+-- should be ok
+ALTER TYPE t1 DROP ATTRIBUTE c;
+SELECT v1;
+
+-- should be ok
+ALTER TYPE t1 ADD ATTRIBUTE c int;
+SELECT v1;
+
+LET v1 = (10, 10.3, 20);
+SELECT v1;
+
+-- should be ok
+ALTER TYPE t1 DROP ATTRIBUTE b;
+SELECT v1;
+
+-- should fail, disallow data type change
+ALTER TYPE t1 ALTER ATTRIBUTE c TYPE int;
+
+DROP VARIABLE v1;
+DROP TYPE t1;
+
+-- the table type can be used as composite type too
+CREATE TABLE svar_test(a int, b numeric, c date);
+CREATE VARIABLE var1 AS svar_test;
+
+LET var1 = (10, pi(), '2023-05-26');
+SELECT var1;
+
+-- should fail due dependency
+ALTER TABLE svar_test ALTER COLUMN a TYPE text;
+
+-- should fail
+DROP TABLE svar_test;
+
+DROP VARIABLE var1;
+DROP TABLE svar_test;
+
+-- arrays are supported
+CREATE VARIABLE var1 AS numeric[];
+LET var1 = ARRAY[1.1,2.1];
+LET var1[1] = 10.1;
+SELECT var1;
+
+-- LET target doesn't allow srf, should fail
+LET var1[generate_series(1,3)] = 100;
+
+DROP VARIABLE var1;
+
+-- arrays inside composite
+CREATE TYPE t1 AS (a numeric, b numeric[]);
+CREATE VARIABLE var1 AS t1;
+LET var1 = (10.1, ARRAY[0.0, 0.0]);
+LET var1.a = 10.2;
+SELECT var1;
+LET var1.b[1] = 10.3;
+SELECT var1;
+
+DROP VARIABLE var1;
+DROP TYPE t1;
+
+-- Encourage use of parallel plans
+SET parallel_setup_cost = 0;
+SET parallel_tuple_cost = 0;
+SET min_parallel_table_scan_size = 0;
+SET max_parallel_workers_per_gather = 2;
+
+-- test on query with workers
+CREATE TABLE svar_test(a int);
+INSERT INTO svar_test SELECT * FROM generate_series(1,1000);
+ANALYZE svar_test;
+CREATE VARIABLE zero int;
+LET zero = 0;
+
+-- result should be 100
+SELECT count(*) FROM svar_test WHERE a%10 = zero;
+
+-- parallel execution is not supported yet
+EXPLAIN (COSTS OFF) SELECT count(*) FROM svar_test WHERE a%10 = zero;
+
+LET zero = (SELECT count(*) FROM svar_test);
+
+-- result should be 1000
+SELECT zero;
+
+DROP VARIABLE zero;
+DROP TABLE svar_test;
+
+RESET parallel_setup_cost;
+RESET parallel_tuple_cost;
+RESET min_parallel_table_scan_size;
+RESET max_parallel_workers_per_gather;
+
+-- the result of view should be same in parallel mode too
+CREATE VARIABLE var1 AS int;
+LET var1 = 10;
+
+CREATE VIEW var1view AS SELECT COALESCE(var1, 0) AS result;
+
+SELECT * FROM var1view;
+
+SET debug_parallel_query TO on;
+
+SELECT * FROM var1view;
+
+SET debug_parallel_query TO off;
+
+DROP VIEW var1view;
+DROP VARIABLE var1;
+
+CREATE VARIABLE varid int;
+CREATE TABLE svar_test(id int, v int);
+
+LET varid = 1;
+INSERT INTO svar_test VALUES(varid, 100);
+SELECT * FROM svar_test;
+UPDATE svar_test SET v = 200 WHERE id = varid;
+SELECT * FROM svar_test;
+DELETE FROM svar_test WHERE id = varid;
+SELECT * FROM svar_test;
+
+DROP TABLE svar_test;
+DROP VARIABLE varid;
+
+
+-- visibility check
+-- variables should be shadowed always
+CREATE VARIABLE var1 AS text;
+SELECT var1.relname FROM pg_class var1 WHERE var1.relname = 'pg_class';
+
+DROP VARIABLE var1;
+
+CREATE TABLE xxtab(avar int);
+
+INSERT INTO xxtab VALUES(333);
+
+CREATE TYPE xxtype AS (avar int);
+
+CREATE VARIABLE xxtab AS xxtype;
+
+INSERT INTO xxtab VALUES(10);
+
+-- it is ambiguous, but columns are preferred
+SELECT xxtab.avar FROM xxtab;
+
+-- should be ok
+SELECT avar FROM xxtab;
+
+CREATE VARIABLE public.avar AS int;
+
+-- should be ok, see the table
+SELECT avar FROM xxtab;
+
+-- should be ok
+SELECT public.avar FROM xxtab;
+
+DROP VARIABLE xxtab;
+
+SELECT xxtab.avar FROM xxtab;
+
+DROP VARIABLE public.avar;
+
+DROP TYPE xxtype;
+
+DROP TABLE xxtab;
+
+-- The variable can be shadowed by table or by alias
+CREATE TYPE public.svar_type AS (a int, b int, c int);
+CREATE VARIABLE public.svar AS public.svar_type;
+
+CREATE TABLE public.svar(a int, b int);
+
+INSERT INTO public.svar VALUES(10, 20);
+
+LET public.svar = (100, 200, 300);
+
+-- should be ok
+-- show table
+SELECT * FROM public.svar;
+SELECT svar.a FROM public.svar;
+SELECT svar.* FROM public.svar;
+
+-- show variable
+SELECT public.svar;
+SELECT public.svar.c;
+SELECT (public.svar).*;
+
+-- the variable is shadowed, raise error
+SELECT public.svar.c FROM public.svar;
+
+-- can be fixed by alias
+SELECT public.svar.c FROM public.svar x;
+
+SELECT svar.a FROM public.svar;
+SELECT svar.* FROM public.svar;
+
+-- show variable
+SELECT public.svar;
+SELECT public.svar.c;
+SELECT (public.svar).*;
+
+-- the variable is shadowed, raise error
+SELECT public.svar.c FROM public.svar;
+
+-- can be fixed by alias
+SELECT public.svar.c FROM public.svar x;
+
+DROP VARIABLE public.svar;
+DROP TABLE public.svar;
+DROP TYPE public.svar_type;
+
+CREATE TYPE ab AS (a integer, b integer);
+
+CREATE VARIABLE v_ab AS ab;
+
+CREATE TABLE v_ab (a integer, b integer);
+INSERT INTO v_ab VALUES(10,20);
+
+-- we should see table
+SELECT v_ab.a FROM v_ab;
+
+CREATE SCHEMA v_ab;
+
+CREATE VARIABLE v_ab.a AS integer;
+
+-- we should see table
+SELECT v_ab.a FROM v_ab;
+
+DROP VARIABLE v_ab;
+DROP TABLE v_ab;
+DROP TYPE ab;
+DROP VARIABLE v_ab.a;
+DROP SCHEMA v_ab;
+
+CREATE TYPE t_am_type AS (b int);
+CREATE SCHEMA xxx_am;
+
+SET search_path TO public;
+
+CREATE VARIABLE xxx_am AS t_am_type;
+LET xxx_am = ROW(10);
+
+-- should be ok
+SELECT xxx_am;
+
+CREATE VARIABLE xxx_am.b AS int;
+LET :"DBNAME".xxx_am.b = 20;
+
+-- should be still ok
+SELECT xxx_am;
+
+-- should fail, the reference should be ambiguous
+SELECT xxx_am.b;
+
+-- enhanced references should be ok
+SELECT public.xxx_am.b;
+SELECT :"DBNAME".xxx_am.b;
+
+CREATE TABLE xxx_am(b  int);
+INSERT INTO xxx_am VALUES(10);
+
+-- we should see table
+SELECT xxx_am.b FROM xxx_am;
+SELECT x.b FROM xxx_am x;
+
+DROP TABLE xxx_am;
+DROP VARIABLE public.xxx_am;
+DROP VARIABLE xxx_am.b;
+DROP SCHEMA xxx_am;
+
+CREATE SCHEMA :"DBNAME";
+
+CREATE VARIABLE :"DBNAME".:"DBNAME".:"DBNAME" AS t_am_type;
+CREATE VARIABLE :"DBNAME".:"DBNAME".b AS int;
+
+SET search_path TO :"DBNAME";
+
+-- should be ambiguous
+SELECT :"DBNAME".b;
+
+-- should be ambiguous too
+SELECT :"DBNAME".:"DBNAME".b;
+
+CREATE TABLE :"DBNAME"(b int);
+
+-- should be ok
+SELECT :"DBNAME".b FROM :"DBNAME";
+
+DROP TABLE :"DBNAME";
+
+DROP VARIABLE :"DBNAME".:"DBNAME".b;
+DROP VARIABLE :"DBNAME".:"DBNAME".:"DBNAME";
+DROP SCHEMA :"DBNAME";
+
+RESET search_path;
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 5b1e772f63d..bbfaab57083 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1498,6 +1498,7 @@ LargeObjectDesc
 Latch
 LauncherLastStartTimesEntry
 LerpFunc
+LetStmt
 LexDescr
 LexemeEntry
 LexemeHashKey
@@ -2601,6 +2602,7 @@ SerializedTransactionState
 Session
 SessionBackupState
 SessionEndType
+SessionVariableValue
 SetConstraintState
 SetConstraintStateData
 SetConstraintTriggerData
@@ -2791,6 +2793,9 @@ SupportRequestRows
 SupportRequestSelectivity
 SupportRequestSimplify
 SupportRequestWFuncMonotonic
+SVariable
+SVariableData
+SVariableState
 Syn
 SyncOps
 SyncRepConfigData
-- 
2.47.0



  [text/x-patch] v20241110-0001-Enhancing-catalog-for-support-session-variables-and-.patch (130.0K, 22-v20241110-0001-Enhancing-catalog-for-support-session-variables-and-.patch)
  download | inline diff:
From 5cbaf816ed8133ece60814acf2d7767ff1b62037 Mon Sep 17 00:00:00 2001
From: "[email protected]" <[email protected]>
Date: Fri, 19 Jan 2024 15:41:55 +0100
Subject: [PATCH 01/20] Enhancing catalog for support session variables and
 related support

This patch introduces new system catalog table pg_variable. This table holds metadata about
session variables created by command CREATE VARIABLE, and dropped by command DROP VARIABLE.
Both commands are implemented by this patch. Possibility to change owner, schema or rename.

Access to session variables can be controlled by SELECT or UPDATE rights. Both rights are
introduced by this patch too.

This patch enhancing pg_dump and psql to support session variables. The changes are related
to system catalog.

This patch is not short, but the code is simple.
---
 doc/src/sgml/catalogs.sgml                    | 120 +++++++++
 doc/src/sgml/ddl.sgml                         |  31 +++
 doc/src/sgml/func.sgml                        |  13 +
 doc/src/sgml/glossary.sgml                    |  15 ++
 doc/src/sgml/ref/allfiles.sgml                |   3 +
 .../sgml/ref/alter_default_privileges.sgml    |  26 +-
 doc/src/sgml/ref/alter_variable.sgml          | 178 ++++++++++++
 doc/src/sgml/ref/comment.sgml                 |   1 +
 doc/src/sgml/ref/create_schema.sgml           |   7 +-
 doc/src/sgml/ref/create_variable.sgml         | 151 +++++++++++
 doc/src/sgml/ref/drop_variable.sgml           | 117 ++++++++
 doc/src/sgml/ref/grant.sgml                   |   6 +
 doc/src/sgml/ref/revoke.sgml                  |   7 +
 doc/src/sgml/reference.sgml                   |   3 +
 src/backend/catalog/Makefile                  |   1 +
 src/backend/catalog/aclchk.c                  |  77 ++++++
 src/backend/catalog/dependency.c              |   6 +
 src/backend/catalog/meson.build               |   1 +
 src/backend/catalog/namespace.c               | 133 +++++++++
 src/backend/catalog/objectaddress.c           | 121 ++++++++-
 src/backend/catalog/pg_shdepend.c             |   2 +
 src/backend/catalog/pg_variable.c             | 255 ++++++++++++++++++
 src/backend/commands/alter.c                  |   9 +
 src/backend/commands/dropcmds.c               |   4 +
 src/backend/commands/event_trigger.c          |   4 +
 src/backend/commands/seclabel.c               |   1 +
 src/backend/commands/tablecmds.c              |  41 +++
 src/backend/parser/gram.y                     | 105 +++++++-
 src/backend/parser/parse_utilcmd.c            |  12 +
 src/backend/tcop/utility.c                    |  16 ++
 src/backend/utils/adt/acl.c                   |   7 +
 src/backend/utils/cache/lsyscache.c           | 113 ++++++++
 src/bin/pg_dump/common.c                      |   3 +
 src/bin/pg_dump/dumputils.c                   |   6 +
 src/bin/pg_dump/pg_backup.h                   |   2 +
 src/bin/pg_dump/pg_backup_archiver.c          |   9 +
 src/bin/pg_dump/pg_dump.c                     | 195 +++++++++++++-
 src/bin/pg_dump/pg_dump.h                     |  19 ++
 src/bin/pg_dump/pg_dump_sort.c                |   6 +
 src/bin/pg_dump/t/002_pg_dump.pl              |  64 +++++
 src/bin/psql/command.c                        |   3 +
 src/bin/psql/describe.c                       |  96 +++++++
 src/bin/psql/describe.h                       |   3 +
 src/bin/psql/help.c                           |   1 +
 src/bin/psql/tab-complete.in.c                |  42 ++-
 src/include/catalog/Makefile                  |   3 +-
 src/include/catalog/meson.build               |   1 +
 src/include/catalog/namespace.h               |   4 +
 src/include/catalog/pg_default_acl.h          |   1 +
 src/include/catalog/pg_proc.dat               |   3 +
 src/include/catalog/pg_variable.h             |  81 ++++++
 src/include/nodes/parsenodes.h                |  16 ++
 src/include/parser/kwlist.h                   |   2 +
 src/include/tcop/cmdtaglist.h                 |   3 +
 src/include/utils/acl.h                       |   1 +
 src/include/utils/lsyscache.h                 |   9 +
 src/test/regress/expected/dependency.out      |  17 ++
 src/test/regress/expected/oidjoins.out        |   4 +
 src/test/regress/expected/psql.out            |  50 ++++
 .../regress/expected/session_variables.out    |  58 ++++
 src/test/regress/parallel_schedule            |   2 +-
 src/test/regress/sql/dependency.sql           |  14 +
 src/test/regress/sql/psql.sql                 |  21 ++
 src/test/regress/sql/session_variables.sql    |  61 +++++
 src/tools/pgindent/typedefs.list              |   4 +
 65 files changed, 2364 insertions(+), 26 deletions(-)
 create mode 100644 doc/src/sgml/ref/alter_variable.sgml
 create mode 100644 doc/src/sgml/ref/create_variable.sgml
 create mode 100644 doc/src/sgml/ref/drop_variable.sgml
 create mode 100644 src/backend/catalog/pg_variable.c
 create mode 100644 src/include/catalog/pg_variable.h
 create mode 100644 src/test/regress/expected/session_variables.out
 create mode 100644 src/test/regress/sql/session_variables.sql

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index c180ed7abbc..5709c9cdbd4 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -369,6 +369,11 @@
       <entry><link linkend="catalog-pg-user-mapping"><structname>pg_user_mapping</structname></link></entry>
       <entry>mappings of users to foreign servers</entry>
      </row>
+
+     <row>
+      <entry><link linkend="catalog-pg-variable"><structname>pg_variable</structname></link></entry>
+      <entry>session variables</entry>
+     </row>
     </tbody>
    </tgroup>
   </table>
@@ -9733,4 +9738,119 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
   </table>
  </sect1>
 
+ <sect1 id="catalog-pg-variable">
+  <title><structname>pg_variable</structname></title>
+
+  <indexterm zone="catalog-pg-variable">
+   <primary>pg_variable</primary>
+  </indexterm>
+
+  <para>
+   The catalog <structname>pg_variable</structname> stores information about
+   session variables.
+  </para>
+
+  <table>
+   <title><structname>pg_variable</structname> Columns</title>
+   <tgroup cols="1">
+    <thead>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       Column Type
+      </para>
+      <para>
+       Description
+      </para></entry>
+     </row>
+    </thead>
+
+    <tbody>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>oid</structfield> <type>oid</type>
+      </para>
+      <para>
+       Row identifier
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>vartype</structfield> <type>oid</type>
+       (references <link linkend="catalog-pg-type"><structname>pg_type</structname></link>.<structfield>oid</structfield>)
+      </para>
+      <para>
+       The OID of the variable's data type
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>varname</structfield> <type>name</type>
+      </para>
+      <para>
+       Name of the session variable
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>varnamespace</structfield> <type>oid</type>
+       (references <link linkend="catalog-pg-namespace"><structname>pg_namespace</structname></link>.<structfield>oid</structfield>)
+      </para>
+      <para>
+       The OID of the namespace that contains this variable
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>varowner</structfield> <type>oid</type>
+       (references <link linkend="catalog-pg-authid"><structname>pg_authid</structname></link>.<structfield>oid</structfield>)
+      </para>
+      <para>
+       Owner of the variable
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>vartypmod</structfield> <type>int4</type>
+      </para>
+      <para>
+       <structfield>vartypmod</structfield> records type-specific data
+       supplied at variable creation time (for example, the maximum
+       length of a <type>varchar</type> column).  It is passed to
+       type-specific input functions and length coercion functions.
+       The value will generally be -1 for types that do not need <structfield>vartypmod</structfield>.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>varcollation</structfield> <type>oid</type>
+       (references <link linkend="catalog-pg-collation"><structname>pg_collation</structname></link>.<structfield>oid</structfield>)
+      </para>
+      <para>
+       The defined collation of the variable, or zero if the variable is
+       not of a collatable data type.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>varacl</structfield> <type>aclitem[]</type>
+      </para>
+      <para>
+       Access privileges; see
+       <xref linkend="sql-grant"/> and
+       <xref linkend="sql-revoke"/>
+       for details
+      </para></entry>
+     </row>
+
+    </tbody>
+   </tgroup>
+  </table>
+ </sect1>
 </chapter>
diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml
index 3c56610d2ac..6ae36f832f7 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -5297,6 +5297,37 @@ EXPLAIN SELECT count(*) FROM measurement WHERE logdate &gt;= DATE '2008-01-01';
    </para>
  </sect1>
 
+  <sect1 id="ddl-session-variables">
+   <title>Session Variables</title>
+
+   <indexterm zone="ddl-session-variables">
+    <primary>Session variables</primary>
+   </indexterm>
+
+   <indexterm>
+    <primary>session variable</primary>
+   </indexterm>
+
+   <para>
+    Session variables are database objects that can hold a value.
+    Session variables, like relations, exist within a schema and their access
+    is controlled via <command>GRANT</command> and <command>REVOKE</command>
+    commands.  A session variable can be created by the <command>CREATE
+    VARIABLE</command> command.
+   </para>
+
+   <para>
+    Inside a query or an expression, a session variable can be
+    <quote>shadowed</quote> by a column with the same name.  Similarly, the
+    name of a function or procedure argument or a PL/pgSQL variable (see
+    <xref linkend="plpgsql-declarations"/>) can shadow a session variable
+    in the routine's body.  Such collisions of identifiers can be resolved
+    by using qualified identifiers: Session variables can be qualified with
+    the schema name, columns can use table aliases, routine variables can use
+    block labels, and routine arguments can use the routine name.
+   </para>
+  </sect1>
+
  <sect1 id="ddl-others">
   <title>Other Database Objects</title>
 
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 73979f20fff..56caa1051a1 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -25736,6 +25736,19 @@ SELECT relname FROM pg_class WHERE pg_table_is_visible(oid);
         Is type (or domain) visible in search path?
        </para></entry>
       </row>
+
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_variable_is_visible</primary>
+        </indexterm>
+        <function>pg_variable_is_visible</function> ( <parameter>variable</parameter> <type>oid</type> )
+        <returnvalue>boolean</returnvalue>
+       </para>
+       <para>
+        Is session variable visible in search path?
+       </para></entry>
+      </row>
      </tbody>
     </tgroup>
    </table>
diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml
index f54f25c1c6b..d0cb53763aa 100644
--- a/doc/src/sgml/glossary.sgml
+++ b/doc/src/sgml/glossary.sgml
@@ -1664,6 +1664,21 @@
    </glossdef>
   </glossentry>
 
+  <glossentry id="glossary-session-variable">
+   <glossterm>Session variable</glossterm>
+   <glossdef>
+    <para>
+     A persistent database object that holds a value in session memory.  This
+     value is private to each session and is released when the session ends.
+     Read or write access to session variables is controlled by privileges,
+     similar to other database objects.
+    </para>
+    <para>
+     For more information, see <xref linkend="ddl-session-variables"/>.
+    </para>
+   </glossdef>
+  </glossentry>
+
   <glossentry id="glossary-shared-memory">
    <glossterm>Shared memory</glossterm>
    <glossdef>
diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml
index f5be638867a..2f67de3e21b 100644
--- a/doc/src/sgml/ref/allfiles.sgml
+++ b/doc/src/sgml/ref/allfiles.sgml
@@ -47,6 +47,7 @@ Complete list of usable sgml source files in this directory.
 <!ENTITY alterType          SYSTEM "alter_type.sgml">
 <!ENTITY alterUser          SYSTEM "alter_user.sgml">
 <!ENTITY alterUserMapping   SYSTEM "alter_user_mapping.sgml">
+<!ENTITY alterVariable      SYSTEM "alter_variable.sgml">
 <!ENTITY alterView          SYSTEM "alter_view.sgml">
 <!ENTITY analyze            SYSTEM "analyze.sgml">
 <!ENTITY begin              SYSTEM "begin.sgml">
@@ -99,6 +100,7 @@ Complete list of usable sgml source files in this directory.
 <!ENTITY createType         SYSTEM "create_type.sgml">
 <!ENTITY createUser         SYSTEM "create_user.sgml">
 <!ENTITY createUserMapping  SYSTEM "create_user_mapping.sgml">
+<!ENTITY createVariable     SYSTEM "create_variable.sgml">
 <!ENTITY createView         SYSTEM "create_view.sgml">
 <!ENTITY deallocate         SYSTEM "deallocate.sgml">
 <!ENTITY declare            SYSTEM "declare.sgml">
@@ -147,6 +149,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">
diff --git a/doc/src/sgml/ref/alter_default_privileges.sgml b/doc/src/sgml/ref/alter_default_privileges.sgml
index 89aacec4fab..041c78d432f 100644
--- a/doc/src/sgml/ref/alter_default_privileges.sgml
+++ b/doc/src/sgml/ref/alter_default_privileges.sgml
@@ -51,6 +51,10 @@ GRANT { { USAGE | CREATE }
     ON SCHEMAS
     TO { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...] [ WITH GRANT OPTION ]
 
+GRANT { SELECT | UPDATE | ALL [ PRIVILEGES ] }
+    ON VARIABLES
+    TO { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...] [ WITH GRANT OPTION ]
+
 REVOKE [ GRANT OPTION FOR ]
     { { SELECT | INSERT | UPDATE | DELETE | TRUNCATE | REFERENCES | TRIGGER | MAINTAIN }
     [, ...] | ALL [ PRIVILEGES ] }
@@ -83,6 +87,12 @@ REVOKE [ GRANT OPTION FOR ]
     ON SCHEMAS
     FROM { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...]
     [ CASCADE | RESTRICT ]
+
+REVOKE [ GRANT OPTION FOR ]
+    { { SELECT | UPDATE } [, ...] | ALL [ PRIVILEGES ] }
+    ON VARIABLES
+    FROM { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...]
+    [ CASCADE | RESTRICT ]
 </synopsis>
  </refsynopsisdiv>
 
@@ -117,14 +127,14 @@ REVOKE [ GRANT OPTION FOR ]
   <para>
    Currently,
    only the privileges for schemas, tables (including views and foreign
-   tables), sequences, functions, and types (including domains) can be
-   altered.  For this command, functions include aggregates and procedures.
-   The words <literal>FUNCTIONS</literal> and <literal>ROUTINES</literal> are
-   equivalent in this command.  (<literal>ROUTINES</literal> is preferred
-   going forward as the standard term for functions and procedures taken
-   together.  In earlier PostgreSQL releases, only the
-   word <literal>FUNCTIONS</literal> was allowed.  It is not possible to set
-   default privileges for functions and procedures separately.)
+   tables), sequences, functions, types (including domains), and session
+   variables can be altered.  For this command, functions include aggregates
+   and procedures.  The words <literal>FUNCTIONS</literal> and
+   <literal>ROUTINES</literal> are equivalent in this command.
+   (<literal>ROUTINES</literal> is preferred going forward as the standard
+   term for functions and procedures taken together.  In earlier PostgreSQL
+   releases, only the word <literal>FUNCTIONS</literal> was allowed.  It is not
+   possible to set default privileges for functions and procedures separately.)
   </para>
 
   <para>
diff --git a/doc/src/sgml/ref/alter_variable.sgml b/doc/src/sgml/ref/alter_variable.sgml
new file mode 100644
index 00000000000..d87570e7d8b
--- /dev/null
+++ b/doc/src/sgml/ref/alter_variable.sgml
@@ -0,0 +1,178 @@
+<!--
+doc/src/sgml/ref/alter_variable.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-altervariable">
+ <indexterm zone="sql-altervariable">
+  <primary>ALTER VARIABLE</primary>
+ </indexterm>
+
+ <indexterm>
+  <primary>session variable</primary>
+  <secondary>altering</secondary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>ALTER VARIABLE</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>ALTER VARIABLE</refname>
+  <refpurpose>
+   change the definition of a session variable
+  </refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+ALTER VARIABLE <replaceable class="parameter">name</replaceable> OWNER TO { <replaceable class="parameter">new_owner</replaceable> | CURRENT_ROLE | CURRENT_USER | SESSION_USER }
+ALTER VARIABLE <replaceable class="parameter">name</replaceable> RENAME TO <replaceable class="parameter">new_name</replaceable>
+ALTER VARIABLE <replaceable class="parameter">name</replaceable> SET SCHEMA <replaceable class="parameter">new_schema</replaceable>
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   The <command>ALTER VARIABLE</command> command changes the definition of an
+   existing session variable. There are several subforms:
+
+  <variablelist>
+   <varlistentry>
+    <term><literal>OWNER</literal></term>
+    <listitem>
+     <para>
+      This form changes the owner of the session variable.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>RENAME</literal></term>
+    <listitem>
+     <para>
+      This form changes the name of the session variable.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>SET SCHEMA</literal></term>
+    <listitem>
+     <para>
+      This form moves the session variable into another schema.
+     </para>
+    </listitem>
+   </varlistentry>
+
+  </variablelist>
+  </para>
+
+  <para>
+   Only the owner or a superuser is allowed to alter a session variable.
+   In order to move a session variable from one schema to another, the user
+   must also have the <literal>CREATE</literal> privilege on the new schema (or
+   be a superuser).
+
+   In order to move the session variable ownership from one role to another,
+   the user must also be a direct or indirect member of the new
+   owning role, and that role must have the <literal>CREATE</literal> privilege
+   on the session variable's schema (or be a superuser). These restrictions
+   enforce that altering the owner doesn't do anything you couldn't do by
+   dropping and recreating the session variable.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+   <para>
+    <variablelist>
+     <varlistentry>
+      <term><replaceable class="parameter">name</replaceable></term>
+      <listitem>
+       <para>
+        The name (possibly schema-qualified) of the existing session variable
+        to alter.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><replaceable class="parameter">new_name</replaceable></term>
+      <listitem>
+       <para>
+        The new name for the session variable.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><replaceable class="parameter">new_owner</replaceable></term>
+      <listitem>
+       <para>
+        The user name of the new owner of the session variable.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><replaceable class="parameter">new_schema</replaceable></term>
+      <listitem>
+       <para>
+        The new schema for the session variable.
+       </para>
+      </listitem>
+     </varlistentry>
+    </variablelist>
+   </para>
+  </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   To rename a session variable:
+<programlisting>
+ALTER VARIABLE foo RENAME TO boo;
+</programlisting>
+  </para>
+
+  <para>
+   To change the owner of the session variable <literal>boo</literal> to
+   <literal>joe</literal>:
+<programlisting>
+ALTER VARIABLE boo OWNER TO joe;
+</programlisting>
+  </para>
+
+  <para>
+   To change the schema of the session variable <literal>boo</literal> to
+   <literal>private</literal>:
+<programlisting>
+ALTER VARIABLE boo SET SCHEMA private;
+</programlisting>
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   Session variables and this command in particular are a PostgreSQL extension.
+  </para>
+ </refsect1>
+
+ <refsect1 id="sql-altervariable-see-also">
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-createvariable"/></member>
+   <member><xref linkend="sql-dropvariable"/></member>
+  </simplelist>
+ </refsect1>
+</refentry>
diff --git a/doc/src/sgml/ref/comment.sgml b/doc/src/sgml/ref/comment.sgml
index 5b43c56b133..21cd80818fb 100644
--- a/doc/src/sgml/ref/comment.sgml
+++ b/doc/src/sgml/ref/comment.sgml
@@ -65,6 +65,7 @@ COMMENT ON
   TRANSFORM FOR <replaceable>type_name</replaceable> LANGUAGE <replaceable>lang_name</replaceable> |
   TRIGGER <replaceable class="parameter">trigger_name</replaceable> ON <replaceable class="parameter">table_name</replaceable> |
   TYPE <replaceable class="parameter">object_name</replaceable> |
+  VARIABLE <replaceable class="parameter">object_name</replaceable> |
   VIEW <replaceable class="parameter">object_name</replaceable>
 } IS { <replaceable class="parameter">string_literal</replaceable> | NULL }
 
diff --git a/doc/src/sgml/ref/create_schema.sgml b/doc/src/sgml/ref/create_schema.sgml
index ed69298ccc6..a834c876bc9 100644
--- a/doc/src/sgml/ref/create_schema.sgml
+++ b/doc/src/sgml/ref/create_schema.sgml
@@ -103,9 +103,10 @@ CREATE SCHEMA IF NOT EXISTS AUTHORIZATION <replaceable class="parameter">role_sp
         schema. Currently, only <command>CREATE
         TABLE</command>, <command>CREATE VIEW</command>, <command>CREATE
         INDEX</command>, <command>CREATE SEQUENCE</command>, <command>CREATE
-        TRIGGER</command> and <command>GRANT</command> are accepted as clauses
-        within <command>CREATE SCHEMA</command>. Other kinds of objects may
-        be created in separate commands after the schema is created.
+        TRIGGER</command>, <command>GRANT</command> and <command>CREATE
+        VARIABLE</command> are accepted as clauses within <command>CREATE
+        SCHEMA</command>. Other kinds of objects may be created in separate
+        commands after the schema is created.
        </para>
       </listitem>
      </varlistentry>
diff --git a/doc/src/sgml/ref/create_variable.sgml b/doc/src/sgml/ref/create_variable.sgml
new file mode 100644
index 00000000000..ec165ab96fc
--- /dev/null
+++ b/doc/src/sgml/ref/create_variable.sgml
@@ -0,0 +1,151 @@
+<!--
+doc/src/sgml/ref/create_variable.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-createvariable">
+ <indexterm zone="sql-createvariable">
+  <primary>CREATE VARIABLE</primary>
+ </indexterm>
+
+ <indexterm>
+  <primary>session variable</primary>
+  <secondary>defining</secondary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>CREATE VARIABLE</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>CREATE VARIABLE</refname>
+  <refpurpose>define a session variable</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+CREATE VARIABLE [ IF NOT EXISTS ] <replaceable class="parameter">name</replaceable> [ AS ] <replaceable class="parameter">data_type</replaceable> ] [ COLLATE <replaceable class="parameter">collation</replaceable> ]
+</synopsis>
+ </refsynopsisdiv>
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   The <command>CREATE VARIABLE</command> command creates a session variable.
+   Session variables, like relations, exist within a schema and their access is
+   controlled via the commands <command>GRANT</command> and <command>REVOKE</command>.
+  </para>
+
+  <para>
+   The value of a session variable is local to the current session.  Retrieving
+   a session variable's value returns NULL, unless its value is set to
+   something else in the current session with a <command>LET</command> command.
+   The content of a session variable is not transactional. This is the same as
+   regular variables in procedural languages.
+  </para>
+
+  <para>
+   Session variables are retrieved by the <command>SELECT</command>
+   command.  Their value is set with the <command>LET</command> command.
+  </para>
+
+  <note>
+   <para>
+    Session variables can be <quote>shadowed</quote> by other identifiers.
+    For details, see <xref linkend="ddl-session-variables"/>.
+   </para>
+  </note>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+  <variablelist>
+
+   <varlistentry  id="sql-createvariable-if-not-exists">
+    <term><literal>IF NOT EXISTS</literal></term>
+    <listitem>
+     <para>
+      Do not throw an error if the name already exists. A notice is issued in
+      this case.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry id="sql-createvariable-name">
+    <term><replaceable class="parameter">name</replaceable></term>
+    <listitem>
+     <para>
+      The name, optionally schema-qualified, of the session variable.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry id="sql-createvariable-data_type">
+    <term><replaceable class="parameter">data_type</replaceable></term>
+    <listitem>
+     <para>
+      The name, optionally schema-qualified, of the data type of the session
+      variable.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry id="sql-createvariable-collate">
+    <term><literal>COLLATE <replaceable>collation</replaceable></literal></term>
+    <listitem>
+     <para>
+      The <literal>COLLATE</literal> clause assigns a collation to the session
+      variable (which must be of a collatable data type).  If not specified,
+      the data type's default collation is used.
+     </para>
+    </listitem>
+   </varlistentry>
+
+  </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Notes</title>
+
+  <para>
+   Use the <command>DROP VARIABLE</command> command to remove a session
+   variable.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   Create an date session variable <literal>var1</literal>:
+<programlisting>
+CREATE VARIABLE var1 AS date;
+LET var1 = current_date;
+SELECT var1;
+</programlisting>
+  </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   The <command>CREATE VARIABLE</command> command is a
+   <productname>PostgreSQL</productname> extension.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-altervariable"/></member>
+   <member><xref linkend="sql-dropvariable"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/ref/drop_variable.sgml b/doc/src/sgml/ref/drop_variable.sgml
new file mode 100644
index 00000000000..5bdb3560f0b
--- /dev/null
+++ b/doc/src/sgml/ref/drop_variable.sgml
@@ -0,0 +1,117 @@
+<!--
+doc/src/sgml/ref/drop_variable.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-dropvariable">
+ <indexterm zone="sql-dropvariable">
+  <primary>DROP VARIABLE</primary>
+ </indexterm>
+
+ <indexterm>
+  <primary>session variable</primary>
+  <secondary>removing</secondary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>DROP VARIABLE</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>DROP VARIABLE</refname>
+  <refpurpose>remove a session 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 session variable.
+   A session variable can only be removed by its owner or a superuser.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+  <variablelist>
+   <varlistentry>
+    <term><literal>IF EXISTS</literal></term>
+    <listitem>
+     <para>
+      Do not throw an error if the session 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 session variable.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>CASCADE</literal></term>
+    <listitem>
+     <para>
+      Automatically drop objects that depend on the session variable (such as
+      views), and in turn all objects that depend on those objects
+      (see <xref linkend="ddl-depend"/>).
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>RESTRICT</literal></term>
+    <listitem>
+     <para>
+      Refuse to drop the session variable if any objects depend on it.  This is
+      the default.
+     </para>
+    </listitem>
+   </varlistentry>
+  </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   To remove the session variable <literal>var1</literal>:
+
+<programlisting>
+DROP VARIABLE var1;
+</programlisting></para>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   The <command>DROP VARIABLE</command> command is a
+   <productname>PostgreSQL</productname> extension.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-altervariable"/></member>
+   <member><xref linkend="sql-createvariable"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/ref/grant.sgml b/doc/src/sgml/ref/grant.sgml
index 999f657d5c0..78ff10fcf5e 100644
--- a/doc/src/sgml/ref/grant.sgml
+++ b/doc/src/sgml/ref/grant.sgml
@@ -101,6 +101,12 @@ GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replace
     [ WITH { ADMIN | INHERIT | SET } { OPTION | TRUE | FALSE } ]
     [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ]
 
+GRANT { SELECT | UPDATE | ALL [ PRIVILEGES ] }
+    ON VARIABLE <replaceable>variable_name</replaceable> [, ...]
+       | ALL VARIABLES IN SCHEMA <replaceable class="parameter">schema_name</replaceable> [, ...] }
+    TO <replaceable class="parameter">role_specification</replaceable> [, ...] [ WITH GRANT OPTION ]
+    [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ]
+
 <phrase>where <replaceable class="parameter">role_specification</replaceable> can be:</phrase>
 
     [ GROUP ] <replaceable class="parameter">role_name</replaceable>
diff --git a/doc/src/sgml/ref/revoke.sgml b/doc/src/sgml/ref/revoke.sgml
index 8df492281a1..626c0231d0d 100644
--- a/doc/src/sgml/ref/revoke.sgml
+++ b/doc/src/sgml/ref/revoke.sgml
@@ -137,6 +137,13 @@ REVOKE [ { ADMIN | INHERIT | SET } OPTION FOR ]
   | CURRENT_ROLE
   | CURRENT_USER
   | SESSION_USER
+
+REVOKE [ GRANT OPTION FOR ]
+    { { SELECT | UPDATE } [, ...] | ALL [ PRIVILEGES ] }
+    ON VARIABLE <replaceable>variable_name</replaceable> [, ...]
+       | ALL VARIABLES IN SCHEMA <replaceable class="parameter">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 ff85ace83fc..25578f3946c 100644
--- a/doc/src/sgml/reference.sgml
+++ b/doc/src/sgml/reference.sgml
@@ -75,6 +75,7 @@
    &alterType;
    &alterUser;
    &alterUserMapping;
+   &alterVariable;
    &alterView;
    &analyze;
    &begin;
@@ -127,6 +128,7 @@
    &createType;
    &createUser;
    &createUserMapping;
+   &createVariable;
    &createView;
    &deallocate;
    &declare;
@@ -175,6 +177,7 @@
    &dropType;
    &dropUser;
    &dropUserMapping;
+   &dropVariable;
    &dropView;
    &end;
    &execute;
diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile
index 1589a75fd53..7a90fcfc457 100644
--- a/src/backend/catalog/Makefile
+++ b/src/backend/catalog/Makefile
@@ -45,6 +45,7 @@ OBJS = \
 	pg_shdepend.o \
 	pg_subscription.o \
 	pg_type.o \
+	pg_variable.o \
 	storage.o \
 	toasting.o
 
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 95eb0b12277..0d7e16f708a 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -64,6 +64,7 @@
 #include "catalog/pg_proc.h"
 #include "catalog/pg_tablespace.h"
 #include "catalog/pg_type.h"
+#include "catalog/pg_variable.h"
 #include "commands/dbcommands.h"
 #include "commands/defrem.h"
 #include "commands/event_trigger.h"
@@ -291,6 +292,9 @@ restrict_and_check_grant(bool is_grant, AclMode avail_goptions, bool all_privs,
 		case OBJECT_PARAMETER_ACL:
 			whole_mask = ACL_ALL_RIGHTS_PARAMETER_ACL;
 			break;
+		case OBJECT_VARIABLE:
+			whole_mask = ACL_ALL_RIGHTS_VARIABLE;
+			break;
 		default:
 			elog(ERROR, "unrecognized object type: %d", objtype);
 			/* not reached, but keep compiler quiet */
@@ -535,6 +539,10 @@ ExecuteGrantStmt(GrantStmt *stmt)
 			all_privileges = ACL_ALL_RIGHTS_PARAMETER_ACL;
 			errormsg = gettext_noop("invalid privilege type %s for parameter");
 			break;
+		case OBJECT_VARIABLE:
+			all_privileges = ACL_ALL_RIGHTS_VARIABLE;
+			errormsg = gettext_noop("invalid privilege type %s for session variable");
+			break;
 		default:
 			elog(ERROR, "unrecognized GrantStmt.objtype: %d",
 				 (int) stmt->objtype);
@@ -640,6 +648,9 @@ ExecGrantStmt_oids(InternalGrant *istmt)
 		case OBJECT_PARAMETER_ACL:
 			ExecGrant_Parameter(istmt);
 			break;
+		case OBJECT_VARIABLE:
+			ExecGrant_common(istmt, VariableRelationId, ACL_ALL_RIGHTS_VARIABLE, NULL);
+			break;
 		default:
 			elog(ERROR, "unrecognized GrantStmt.objtype: %d",
 				 (int) istmt->objtype);
@@ -830,6 +841,18 @@ objectNamesToOids(ObjectType objtype, List *objnames, bool is_grant)
 					objects = lappend_oid(objects, parameterId);
 			}
 			break;
+		case OBJECT_VARIABLE:
+			foreach(cell, objnames)
+			{
+				RangeVar   *varvar = (RangeVar *) lfirst(cell);
+				Oid			relOid;
+
+				relOid = LookupVariable(varvar->schemaname,
+										varvar->relname,
+										false);
+				objects = lappend_oid(objects, relOid);
+			}
+			break;
 		default:
 			elog(ERROR, "unrecognized GrantStmt.objtype: %d",
 				 (int) objtype);
@@ -919,6 +942,32 @@ objectsInSchemaToOids(ObjectType objtype, List *nspnames)
 					table_close(rel, AccessShareLock);
 				}
 				break;
+			case OBJECT_VARIABLE:
+				{
+					ScanKeyData key;
+					Relation	rel;
+					TableScanDesc scan;
+					HeapTuple	tuple;
+
+					ScanKeyInit(&key,
+								Anum_pg_variable_varnamespace,
+								BTEqualStrategyNumber, F_OIDEQ,
+								ObjectIdGetDatum(namespaceId));
+
+					rel = table_open(VariableRelationId, AccessShareLock);
+					scan = table_beginscan_catalog(rel, 1, &key);
+
+					while ((tuple = heap_getnext(scan, ForwardScanDirection)) != NULL)
+					{
+						Oid			oid = ((Form_pg_proc) GETSTRUCT(tuple))->oid;
+
+						objects = lappend_oid(objects, oid);
+					}
+
+					table_endscan(scan);
+					table_close(rel, AccessShareLock);
+				}
+				break;
 			default:
 				/* should not happen */
 				elog(ERROR, "unrecognized GrantStmt.objtype: %d",
@@ -1078,6 +1127,10 @@ ExecAlterDefaultPrivilegesStmt(ParseState *pstate, AlterDefaultPrivilegesStmt *s
 			all_privileges = ACL_ALL_RIGHTS_SCHEMA;
 			errormsg = gettext_noop("invalid privilege type %s for schema");
 			break;
+		case OBJECT_VARIABLE:
+			all_privileges = ACL_ALL_RIGHTS_VARIABLE;
+			errormsg = gettext_noop("invalid privilege type %s for session variable");
+			break;
 		default:
 			elog(ERROR, "unrecognized GrantStmt.objtype: %d",
 				 (int) action->objtype);
@@ -1269,6 +1322,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 object type: %d",
 				 (int) iacls->objtype);
@@ -1512,6 +1571,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",
@@ -1572,6 +1634,9 @@ RemoveRoleFromObjectACL(Oid roleid, Oid classid, Oid objid)
 			case ParameterAclRelationId:
 				istmt.objtype = OBJECT_PARAMETER_ACL;
 				break;
+			case VariableRelationId:
+				istmt.objtype = OBJECT_VARIABLE;
+				break;
 			default:
 				elog(ERROR, "unexpected object class %u", classid);
 				break;
@@ -2813,6 +2878,9 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
 					case OBJECT_TYPE:
 						msg = gettext_noop("permission denied for type %s");
 						break;
+					case OBJECT_VARIABLE:
+						msg = gettext_noop("permission denied for session variable %s");
+						break;
 					case OBJECT_VIEW:
 						msg = gettext_noop("permission denied for view %s");
 						break;
@@ -2924,6 +2992,9 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
 					case OBJECT_TYPE:
 						msg = gettext_noop("must be owner of type %s");
 						break;
+					case OBJECT_VARIABLE:
+						msg = gettext_noop("must be owner of session variable %s");
+						break;
 					case OBJECT_VIEW:
 						msg = gettext_noop("must be owner of view %s");
 						break;
@@ -3072,6 +3143,8 @@ pg_aclmask(ObjectType objtype, Oid object_oid, AttrNumber attnum, Oid roleid,
 			return ACL_NO_RIGHTS;
 		case OBJECT_TYPE:
 			return object_aclmask(TypeRelationId, object_oid, roleid, mask, how);
+		case OBJECT_VARIABLE:
+			return object_aclmask(VariableRelationId, object_oid, roleid, mask, how);
 		default:
 			elog(ERROR, "unrecognized object type: %d",
 				 (int) objtype);
@@ -4339,6 +4412,10 @@ get_user_default_acl(ObjectType objtype, Oid ownerId, Oid nsp_oid)
 			defaclobjtype = DEFACLOBJ_NAMESPACE;
 			break;
 
+		case OBJECT_VARIABLE:
+			defaclobjtype = DEFACLOBJ_VARIABLE;
+			break;
+
 		default:
 			return NULL;
 	}
diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c
index 0489cbabcb8..c9d686665de 100644
--- a/src/backend/catalog/dependency.c
+++ b/src/backend/catalog/dependency.c
@@ -65,12 +65,14 @@
 #include "catalog/pg_ts_template.h"
 #include "catalog/pg_type.h"
 #include "catalog/pg_user_mapping.h"
+#include "catalog/pg_variable.h"
 #include "commands/comment.h"
 #include "commands/defrem.h"
 #include "commands/event_trigger.h"
 #include "commands/extension.h"
 #include "commands/policy.h"
 #include "commands/publicationcmds.h"
+#include "commands/schemacmds.h"
 #include "commands/seclabel.h"
 #include "commands/sequence.h"
 #include "commands/trigger.h"
@@ -1444,6 +1446,10 @@ doDeletion(const ObjectAddress *object, int flags)
 			RemovePublicationById(object->objectId);
 			break;
 
+		case VariableRelationId:
+			DropVariableById(object->objectId);
+			break;
+
 		case CastRelationId:
 		case CollationRelationId:
 		case ConversionRelationId:
diff --git a/src/backend/catalog/meson.build b/src/backend/catalog/meson.build
index 2f3ded8a0e7..bfdb4da3301 100644
--- a/src/backend/catalog/meson.build
+++ b/src/backend/catalog/meson.build
@@ -32,6 +32,7 @@ backend_sources += files(
   'pg_shdepend.c',
   'pg_subscription.c',
   'pg_type.c',
+  'pg_variable.c',
   'storage.c',
   'toasting.c',
 )
diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c
index 30807f91904..86e2258657d 100644
--- a/src/backend/catalog/namespace.c
+++ b/src/backend/catalog/namespace.c
@@ -41,6 +41,7 @@
 #include "catalog/pg_ts_parser.h"
 #include "catalog/pg_ts_template.h"
 #include "catalog/pg_type.h"
+#include "catalog/pg_variable.h"
 #include "commands/dbcommands.h"
 #include "common/hashfn_unstable.h"
 #include "funcapi.h"
@@ -986,6 +987,67 @@ RelationIsVisibleExt(Oid relid, bool *is_missing)
 	return visible;
 }
 
+/*
+ * VariableIsVisible
+ *		Determine whether a variable (identified by OID) is visible in the
+ *		current search path. Visible means "would be found by searching
+ *		for the unqualified variable name".
+ */
+bool
+VariableIsVisible(Oid varid)
+{
+	HeapTuple	vartup;
+	Form_pg_variable varform;
+	Oid			varnamespace;
+	bool		visible;
+
+	vartup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid));
+	if (!HeapTupleIsValid(vartup))
+		elog(ERROR, "cache lookup failed for session variable %u", varid);
+	varform = (Form_pg_variable) GETSTRUCT(vartup);
+
+	recomputeNamespacePath();
+
+	/*
+	 * Quick check: if it ain't in the path at all, it ain't visible. We
+	 * don't expect usage of session variables in the system namespace.
+	 */
+	varnamespace = varform->varnamespace;
+	if (!list_member_oid(activeSearchPath, varnamespace))
+		visible = false;
+	else
+	{
+		/*
+		 * If it is in the path, it might still not be visible; it could be
+		 * hidden by another variable of the same name earlier in the path. So
+		 * we must do a slow check for conflicting relations.
+		 */
+		char	   *varname = NameStr(varform->varname);
+		ListCell   *l;
+
+		visible = false;
+		foreach(l, activeSearchPath)
+		{
+			Oid			namespaceId = lfirst_oid(l);
+
+			if (namespaceId == varnamespace)
+			{
+				/* found it first in path */
+				visible = true;
+				break;
+			}
+			if (OidIsValid(get_varname_varid(varname, namespaceId)))
+			{
+				/* found something else first in path */
+				break;
+			}
+		}
+	}
+
+	ReleaseSysCache(vartup);
+
+	return visible;
+}
 
 /*
  * TypenameGetTypid
@@ -3289,6 +3351,66 @@ TSConfigIsVisibleExt(Oid cfgid, bool *is_missing)
 	return visible;
 }
 
+/*
+ * Returns oid of session variable specified by possibly qualified identifier.
+ *
+ * If not found, returns InvalidOid if missing_ok, else throws error.
+ */
+Oid
+LookupVariable(const char *nspname,
+			   const char *varname,
+			   bool missing_ok)
+{
+	Oid			namespaceId;
+	Oid			varoid = InvalidOid;
+	ListCell   *l;
+
+	if (nspname)
+	{
+		namespaceId = LookupExplicitNamespace(nspname, missing_ok);
+
+		/* if nspname is a known namespace, the variable must be there */
+		if (OidIsValid(namespaceId))
+		{
+			varoid = GetSysCacheOid2(VARIABLENAMENSP, Anum_pg_variable_oid,
+									 PointerGetDatum(varname),
+									 ObjectIdGetDatum(namespaceId));
+		}
+	}
+	else
+	{
+		/* iterate over the schemas on the search_path */
+		recomputeNamespacePath();
+
+		foreach(l, activeSearchPath)
+		{
+			namespaceId = lfirst_oid(l);
+
+			varoid = GetSysCacheOid2(VARIABLENAMENSP, Anum_pg_variable_oid,
+									 PointerGetDatum(varname),
+									 ObjectIdGetDatum(namespaceId));
+
+			if (OidIsValid(varoid))
+				break;
+		}
+	}
+
+	if (!OidIsValid(varoid) && !missing_ok)
+	{
+		if (nspname)
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("session variable \"%s.%s\" does not exist",
+							nspname, varname)));
+		else
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("session variable \"%s\" does not exist",
+							varname)));
+	}
+
+	return varoid;
+}
 
 /*
  * DeconstructQualifiedName
@@ -5085,3 +5207,14 @@ pg_is_other_temp_schema(PG_FUNCTION_ARGS)
 
 	PG_RETURN_BOOL(isOtherTempNamespace(oid));
 }
+
+Datum
+pg_variable_is_visible(PG_FUNCTION_ARGS)
+{
+	Oid			oid = PG_GETARG_OID(0);
+
+	if (!SearchSysCacheExists1(VARIABLEOID, ObjectIdGetDatum(oid)))
+		PG_RETURN_NULL();
+
+	PG_RETURN_BOOL(VariableIsVisible(oid));
+}
diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c
index 85a7b7e641a..ee70eeeed44 100644
--- a/src/backend/catalog/objectaddress.c
+++ b/src/backend/catalog/objectaddress.c
@@ -62,6 +62,7 @@
 #include "catalog/pg_ts_template.h"
 #include "catalog/pg_type.h"
 #include "catalog/pg_user_mapping.h"
+#include "catalog/pg_variable.h"
 #include "commands/dbcommands.h"
 #include "commands/defrem.h"
 #include "commands/event_trigger.h"
@@ -636,6 +637,20 @@ static const ObjectPropertyType ObjectProperty[] =
 		OBJECT_USER_MAPPING,
 		false
 	},
+	{
+		"session variable",
+		VariableRelationId,
+		VariableOidIndexId,
+		VARIABLEOID,
+		VARIABLENAMENSP,
+		Anum_pg_variable_oid,
+		Anum_pg_variable_varname,
+		Anum_pg_variable_varnamespace,
+		Anum_pg_variable_varowner,
+		Anum_pg_variable_varacl,
+		OBJECT_VARIABLE,
+		true
+	}
 };
 
 /*
@@ -831,6 +846,9 @@ static const struct object_type_map
 	},
 	{
 		"statistics object", OBJECT_STATISTIC_EXT
+	},
+	{
+		"session variable", OBJECT_VARIABLE
 	}
 };
 
@@ -856,6 +874,7 @@ static ObjectAddress get_object_address_attrdef(ObjectType objtype,
 												bool missing_ok);
 static ObjectAddress get_object_address_type(ObjectType objtype,
 											 TypeName *typename, bool missing_ok);
+static ObjectAddress get_object_address_variable(List *object, bool missing_ok);
 static ObjectAddress get_object_address_opcf(ObjectType objtype, List *object,
 											 bool missing_ok);
 static ObjectAddress get_object_address_opf_member(ObjectType objtype,
@@ -1126,6 +1145,9 @@ get_object_address(ObjectType objtype, Node *object,
 															 missing_ok);
 				address.objectSubId = 0;
 				break;
+			case OBJECT_VARIABLE:
+				address = get_object_address_variable(castNode(List, object), missing_ok);
+				break;
 				/* no default, to let compiler warn about missing case */
 		}
 
@@ -2000,16 +2022,20 @@ get_object_address_defacl(List *object, bool missing_ok)
 		case DEFACLOBJ_NAMESPACE:
 			objtype_str = "schemas";
 			break;
+		case DEFACLOBJ_VARIABLE:
+			objtype_str = "variables";
+			break;
 		default:
 			ereport(ERROR,
 					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 					 errmsg("unrecognized default ACL object type \"%c\"", objtype),
-					 errhint("Valid object types are \"%c\", \"%c\", \"%c\", \"%c\", \"%c\".",
+					 errhint("Valid object types are \"%c\", \"%c\", \"%c\", \"%c\", \"%c\", \"%c\".",
 							 DEFACLOBJ_RELATION,
 							 DEFACLOBJ_SEQUENCE,
 							 DEFACLOBJ_FUNCTION,
 							 DEFACLOBJ_TYPE,
-							 DEFACLOBJ_NAMESPACE)));
+							 DEFACLOBJ_NAMESPACE,
+							 DEFACLOBJ_VARIABLE)));
 	}
 
 	/*
@@ -2093,6 +2119,24 @@ textarray_to_strvaluelist(ArrayType *arr)
 	return list;
 }
 
+/*
+ * Find the ObjectAddress for a session variable
+ */
+static ObjectAddress
+get_object_address_variable(List *object, bool missing_ok)
+{
+	ObjectAddress address;
+	char	   *nspname = NULL;
+	char	   *varname = NULL;
+
+	ObjectAddressSet(address, VariableRelationId, InvalidOid);
+
+	DeconstructQualifiedName(object, &nspname, &varname);
+	address.objectId = LookupVariable(nspname, varname, missing_ok);
+
+	return address;
+}
+
 /*
  * SQL-callable version of get_object_address
  */
@@ -2287,6 +2331,7 @@ pg_get_object_address(PG_FUNCTION_ARGS)
 		case OBJECT_TABCONSTRAINT:
 		case OBJECT_OPCLASS:
 		case OBJECT_OPFAMILY:
+		case OBJECT_VARIABLE:
 			objnode = (Node *) name;
 			break;
 		case OBJECT_ACCESS_METHOD:
@@ -2458,6 +2503,7 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address,
 		case OBJECT_STATISTIC_EXT:
 		case OBJECT_TSDICTIONARY:
 		case OBJECT_TSCONFIGURATION:
+		case OBJECT_VARIABLE:
 			if (!object_ownercheck(address.classId, address.objectId, roleid))
 				aclcheck_error(ACLCHECK_NOT_OWNER, objtype,
 							   NameListToString(castNode(List, object)));
@@ -3446,6 +3492,32 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok)
 				break;
 			}
 
+		case VariableRelationId:
+			{
+				char	   *nspname;
+				HeapTuple	tup;
+				Form_pg_variable varform;
+
+				tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(object->objectId));
+				if (!HeapTupleIsValid(tup))
+					elog(ERROR, "cache lookup failed for session variable %u",
+						 object->objectId);
+
+				varform = (Form_pg_variable) GETSTRUCT(tup);
+
+				if (VariableIsVisible(object->objectId))
+					nspname = NULL;
+				else
+					nspname = get_namespace_name(varform->varnamespace);
+
+				appendStringInfo(&buffer, _("session variable %s"),
+								 quote_qualified_identifier(nspname,
+															NameStr(varform->varname)));
+
+				ReleaseSysCache(tup);
+				break;
+			}
+
 		case TSParserRelationId:
 			{
 				HeapTuple	tup;
@@ -3798,6 +3870,16 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok)
 										 _("default privileges on new schemas belonging to role %s"),
 										 rolename);
 						break;
+					case DEFACLOBJ_VARIABLE:
+						if (nspname)
+							appendStringInfo(&buffer,
+											 _("default privileges on new session variables belonging to role %s in schema %s"),
+											 rolename, nspname);
+						else
+							appendStringInfo(&buffer,
+											 _("default privileges on new session variables belonging to role %s"),
+											 rolename);
+						break;
 					default:
 						/* shouldn't get here */
 						if (nspname)
@@ -4614,6 +4696,10 @@ getObjectTypeDescription(const ObjectAddress *object, bool missing_ok)
 			appendStringInfoString(&buffer, "transform");
 			break;
 
+		case VariableRelationId:
+			appendStringInfoString(&buffer, "session variable");
+			break;
+
 		default:
 			elog(ERROR, "unsupported object class: %u", object->classId);
 	}
@@ -5720,6 +5806,10 @@ getObjectIdentityParts(const ObjectAddress *object,
 						appendStringInfoString(&buffer,
 											   " on schemas");
 						break;
+					case DEFACLOBJ_VARIABLE:
+						appendStringInfoString(&buffer,
+											   " on session variables");
+						break;
 				}
 
 				if (objname)
@@ -5960,6 +6050,33 @@ getObjectIdentityParts(const ObjectAddress *object,
 			}
 			break;
 
+		case VariableRelationId:
+			{
+				char	   *schema;
+				char	   *varname;
+				HeapTuple	tup;
+				Form_pg_variable varform;
+
+				tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(object->objectId));
+				if (!HeapTupleIsValid(tup))
+					elog(ERROR, "cache lookup failed for session variable %u",
+						 object->objectId);
+
+				varform = (Form_pg_variable) GETSTRUCT(tup);
+
+				schema = get_namespace_name_or_temp(varform->varnamespace);
+				varname = NameStr(varform->varname);
+
+				appendStringInfo(&buffer, "%s",
+								 quote_qualified_identifier(schema, varname));
+
+				if (objname)
+					*objname = list_make2(schema, varname);
+
+				ReleaseSysCache(tup);
+				break;
+			}
+
 		default:
 			elog(ERROR, "unsupported object class: %u", object->classId);
 	}
diff --git a/src/backend/catalog/pg_shdepend.c b/src/backend/catalog/pg_shdepend.c
index 753afb88453..1ea3d6db05c 100644
--- a/src/backend/catalog/pg_shdepend.c
+++ b/src/backend/catalog/pg_shdepend.c
@@ -46,6 +46,7 @@
 #include "catalog/pg_ts_dict.h"
 #include "catalog/pg_type.h"
 #include "catalog/pg_user_mapping.h"
+#include "catalog/pg_variable.h"
 #include "commands/alter.h"
 #include "commands/dbcommands.h"
 #include "commands/defrem.h"
@@ -1714,6 +1715,7 @@ shdepReassignOwned_Owner(Form_pg_shdepend sdepForm, Oid newrole)
 		case DatabaseRelationId:
 		case TSConfigRelationId:
 		case TSDictionaryRelationId:
+		case VariableRelationId:
 			AlterObjectOwner_internal(sdepForm->classid,
 									  sdepForm->objid,
 									  newrole);
diff --git a/src/backend/catalog/pg_variable.c b/src/backend/catalog/pg_variable.c
new file mode 100644
index 00000000000..e3a861c8cba
--- /dev/null
+++ b/src/backend/catalog/pg_variable.c
@@ -0,0 +1,255 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_variable.c
+ *		session variables
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *		src/backend/catalog/pg_variable.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/heapam.h"
+#include "catalog/dependency.h"
+#include "catalog/indexing.h"
+#include "catalog/namespace.h"
+#include "catalog/objectaccess.h"
+#include "catalog/pg_collation.h"
+#include "catalog/pg_namespace.h"
+#include "catalog/pg_variable.h"
+#include "miscadmin.h"
+#include "parser/parse_type.h"
+#include "utils/builtins.h"
+#include "utils/lsyscache.h"
+#include "utils/syscache.h"
+
+static ObjectAddress create_variable(const char *varName,
+									 Oid varNamespace,
+									 Oid varType,
+									 int32 varTypmod,
+									 Oid varOwner,
+									 Oid varCollation,
+									 bool if_not_exists);
+
+
+/*
+ * Creates entry in pg_variable table
+ */
+static ObjectAddress
+create_variable(const char *varName,
+				Oid varNamespace,
+				Oid varType,
+				int32 varTypmod,
+				Oid varOwner,
+				Oid varCollation,
+				bool if_not_exists)
+{
+	Acl		   *varacl;
+	NameData	varname;
+	bool		nulls[Natts_pg_variable];
+	Datum		values[Natts_pg_variable];
+	Relation	rel;
+	HeapTuple	tup;
+	TupleDesc	tupdesc;
+	ObjectAddress myself,
+				referenced;
+	ObjectAddresses *addrs;
+	Oid			varid;
+
+	Assert(varName);
+	Assert(OidIsValid(varNamespace));
+	Assert(OidIsValid(varType));
+	Assert(OidIsValid(varOwner));
+
+	rel = table_open(VariableRelationId, RowExclusiveLock);
+
+	/*
+	 * Check for duplicates. Note that this does not really prevent
+	 * duplicates, it's here just to provide nicer error message in common
+	 * case. The real protection is the unique key on the catalog.
+	 */
+	if (SearchSysCacheExists2(VARIABLENAMENSP,
+							  PointerGetDatum(varName),
+							  ObjectIdGetDatum(varNamespace)))
+	{
+		if (if_not_exists)
+			ereport(NOTICE,
+					(errcode(ERRCODE_DUPLICATE_OBJECT),
+					 errmsg("session variable \"%s\" already exists, skipping",
+							varName)));
+		else
+			ereport(ERROR,
+					(errcode(ERRCODE_DUPLICATE_OBJECT),
+					 errmsg("session variable \"%s\" already exists",
+							varName)));
+
+		table_close(rel, RowExclusiveLock);
+
+		return InvalidObjectAddress;
+	}
+
+	memset(values, 0, sizeof(values));
+	memset(nulls, false, sizeof(nulls));
+
+	namestrcpy(&varname, varName);
+
+	varid = GetNewOidWithIndex(rel, VariableOidIndexId, Anum_pg_variable_oid);
+
+	values[Anum_pg_variable_oid - 1] = ObjectIdGetDatum(varid);
+	values[Anum_pg_variable_varname - 1] = NameGetDatum(&varname);
+	values[Anum_pg_variable_varnamespace - 1] = ObjectIdGetDatum(varNamespace);
+	values[Anum_pg_variable_vartype - 1] = ObjectIdGetDatum(varType);
+	values[Anum_pg_variable_vartypmod - 1] = Int32GetDatum(varTypmod);
+	values[Anum_pg_variable_varowner - 1] = ObjectIdGetDatum(varOwner);
+	values[Anum_pg_variable_varcollation - 1] = ObjectIdGetDatum(varCollation);
+
+	varacl = get_user_default_acl(OBJECT_VARIABLE, varOwner,
+								  varNamespace);
+	if (varacl != NULL)
+		values[Anum_pg_variable_varacl - 1] = PointerGetDatum(varacl);
+	else
+		nulls[Anum_pg_variable_varacl - 1] = true;
+
+	tupdesc = RelationGetDescr(rel);
+
+	tup = heap_form_tuple(tupdesc, values, nulls);
+	CatalogTupleInsert(rel, tup);
+	Assert(OidIsValid(varid));
+
+	addrs = new_object_addresses();
+
+	ObjectAddressSet(myself, VariableRelationId, varid);
+
+	/* dependency on namespace */
+	ObjectAddressSet(referenced, NamespaceRelationId, varNamespace);
+	add_exact_object_address(&referenced, addrs);
+
+	/* dependency on used type */
+	ObjectAddressSet(referenced, TypeRelationId, varType);
+	add_exact_object_address(&referenced, addrs);
+
+	/* dependency on collation */
+	if (OidIsValid(varCollation) &&
+		varCollation != DEFAULT_COLLATION_OID)
+	{
+		ObjectAddressSet(referenced, CollationRelationId, varCollation);
+		add_exact_object_address(&referenced, addrs);
+	}
+
+	record_object_address_dependencies(&myself, addrs, DEPENDENCY_NORMAL);
+	free_object_addresses(addrs);
+
+	/* dependency on owner */
+	recordDependencyOnOwner(VariableRelationId, varid, varOwner);
+
+	/* dependencies on roles mentioned in default ACL */
+	recordDependencyOnNewAcl(VariableRelationId, varid, 0, varOwner, varacl);
+
+	/* dependency on extension */
+	recordDependencyOnCurrentExtension(&myself, false);
+
+	heap_freetuple(tup);
+
+	/* post creation hook for new function */
+	InvokeObjectPostCreateHook(VariableRelationId, varid, 0);
+
+	table_close(rel, RowExclusiveLock);
+
+	return myself;
+}
+
+/*
+ * Creates a new variable
+ *
+ * Used by CREATE VARIABLE command
+ */
+ObjectAddress
+CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt)
+{
+	Oid			namespaceid;
+	AclResult	aclresult;
+	Oid			typid;
+	int32		typmod;
+	Oid			varowner = GetUserId();
+	Oid			collation;
+	Oid			typcollation;
+	ObjectAddress variable;
+
+	namespaceid =
+		RangeVarGetAndCheckCreationNamespace(stmt->variable, NoLock, NULL);
+
+	typenameTypeIdAndMod(pstate, stmt->typeName, &typid, &typmod);
+
+	/* disallow pseudotypes */
+	if (get_typtype(typid) == TYPTYPE_PSEUDO)
+		ereport(ERROR,
+				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+				 errmsg("session variable cannot be pseudo-type %s",
+						format_type_be(typid))));
+
+	aclresult = object_aclcheck(TypeRelationId, typid, GetUserId(), ACL_USAGE);
+	if (aclresult != ACLCHECK_OK)
+		aclcheck_error_type(aclresult, typid);
+
+	typcollation = get_typcollation(typid);
+
+	if (stmt->collClause)
+		collation = LookupCollation(pstate,
+									stmt->collClause->collname,
+									stmt->collClause->location);
+	else
+		collation = typcollation;;
+
+	/* complain if COLLATE is applied to an uncollatable type */
+	if (OidIsValid(collation) && !OidIsValid(typcollation))
+		ereport(ERROR,
+				(errcode(ERRCODE_DATATYPE_MISMATCH),
+				 errmsg("collations are not supported by type %s",
+						format_type_be(typid)),
+				 parser_errposition(pstate, stmt->collClause->location)));
+
+	variable = create_variable(stmt->variable->relname,
+							   namespaceid,
+							   typid,
+							   typmod,
+							   varowner,
+							   collation,
+							   stmt->if_not_exists);
+
+	elog(DEBUG1, "record for session variable \"%s\" (oid:%d) was created in pg_variable",
+		 stmt->variable->relname, variable.objectId);
+
+	/* we want SessionVariableCreatePostprocess to see the catalog changes. */
+	CommandCounterIncrement();
+
+	return variable;
+}
+
+/*
+ * Drop variable by OID, and register the needed session variable
+ * cleanup.
+ */
+void
+DropVariableById(Oid varid)
+{
+	Relation	rel;
+	HeapTuple	tup;
+
+	rel = table_open(VariableRelationId, RowExclusiveLock);
+
+	tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid));
+
+	if (!HeapTupleIsValid(tup))
+		elog(ERROR, "cache lookup failed for variable %u", varid);
+
+	CatalogTupleDelete(rel, &tup->t_self);
+
+	ReleaseSysCache(tup);
+
+	table_close(rel, RowExclusiveLock);
+}
diff --git a/src/backend/commands/alter.c b/src/backend/commands/alter.c
index 4f99ebb4470..63fa99fad34 100644
--- a/src/backend/commands/alter.c
+++ b/src/backend/commands/alter.c
@@ -41,6 +41,7 @@
 #include "catalog/pg_ts_dict.h"
 #include "catalog/pg_ts_parser.h"
 #include "catalog/pg_ts_template.h"
+#include "catalog/pg_variable.h"
 #include "commands/alter.h"
 #include "commands/collationcmds.h"
 #include "commands/dbcommands.h"
@@ -139,6 +140,10 @@ report_namespace_conflict(Oid classId, const char *name, Oid nspOid)
 			Assert(OidIsValid(nspOid));
 			msgfmt = gettext_noop("text search configuration \"%s\" already exists in schema \"%s\"");
 			break;
+		case VariableRelationId:
+			Assert(OidIsValid(nspOid));
+			msgfmt = gettext_noop("session variable \"%s\" already exists in schema \"%s\"");
+			break;
 		default:
 			elog(ERROR, "unsupported object class: %u", classId);
 			break;
@@ -418,6 +423,7 @@ ExecRenameStmt(RenameStmt *stmt)
 		case OBJECT_TSTEMPLATE:
 		case OBJECT_PUBLICATION:
 		case OBJECT_SUBSCRIPTION:
+		case OBJECT_VARIABLE:
 			{
 				ObjectAddress address;
 				Relation	catalog;
@@ -561,6 +567,7 @@ ExecAlterObjectSchemaStmt(AlterObjectSchemaStmt *stmt,
 		case OBJECT_TSDICTIONARY:
 		case OBJECT_TSPARSER:
 		case OBJECT_TSTEMPLATE:
+		case OBJECT_VARIABLE:
 			{
 				Relation	catalog;
 				Relation	relation;
@@ -645,6 +652,7 @@ AlterObjectNamespace_oid(Oid classId, Oid objid, Oid nspOid,
 		case TSDictionaryRelationId:
 		case TSTemplateRelationId:
 		case TSConfigRelationId:
+		case VariableRelationId:
 			{
 				Relation	catalog;
 
@@ -875,6 +883,7 @@ ExecAlterOwnerStmt(AlterOwnerStmt *stmt)
 		case OBJECT_TABLESPACE:
 		case OBJECT_TSDICTIONARY:
 		case OBJECT_TSCONFIGURATION:
+		case OBJECT_VARIABLE:
 			{
 				Relation	relation;
 				ObjectAddress address;
diff --git a/src/backend/commands/dropcmds.c b/src/backend/commands/dropcmds.c
index 85eec7e3947..3cce17e92af 100644
--- a/src/backend/commands/dropcmds.c
+++ b/src/backend/commands/dropcmds.c
@@ -476,6 +476,10 @@ does_not_exist_skipping(ObjectType objtype, Node *object)
 			msg = gettext_noop("publication \"%s\" does not exist, skipping");
 			name = strVal(object);
 			break;
+		case OBJECT_VARIABLE:
+			msg = gettext_noop("session variable \"%s\" does not exist, skipping");
+			name = NameListToString(castNode(List, object));
+			break;
 
 		case OBJECT_COLUMN:
 		case OBJECT_DATABASE:
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index dcfc1dbaffd..d03cb947e67 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -2149,6 +2149,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:
@@ -2232,6 +2234,8 @@ stringify_adefprivs_objtype(ObjectType objtype)
 			return "TABLESPACES";
 		case OBJECT_TYPE:
 			return "TYPES";
+		case OBJECT_VARIABLE:
+			return "VARIABLES";
 			/* these currently aren't used */
 		case OBJECT_ACCESS_METHOD:
 		case OBJECT_AGGREGATE:
diff --git a/src/backend/commands/seclabel.c b/src/backend/commands/seclabel.c
index 5607273bf9f..c026ab5dcb8 100644
--- a/src/backend/commands/seclabel.c
+++ b/src/backend/commands/seclabel.c
@@ -92,6 +92,7 @@ SecLabelSupportsObjectType(ObjectType objtype)
 		case OBJECT_TSPARSER:
 		case OBJECT_TSTEMPLATE:
 		case OBJECT_USER_MAPPING:
+		case OBJECT_VARIABLE:
 			return false;
 
 			/*
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index ccd9645e7d2..b7c1e280a00 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -52,6 +52,7 @@
 #include "catalog/pg_tablespace.h"
 #include "catalog/pg_trigger.h"
 #include "catalog/pg_type.h"
+#include "catalog/pg_variable.h"
 #include "catalog/storage.h"
 #include "catalog/storage_xlog.h"
 #include "catalog/toasting.h"
@@ -6768,6 +6769,7 @@ ATTypedTableRecursion(List **wqueue, Relation rel, AlterTableCmd *cmd,
  * (possibly nested several levels deep in composite types, arrays, etc!).
  * Eventually, we'd like to propagate the check or rewrite operation
  * into such tables, but for now, just error out if we find any.
+ * Also, check if "typeOid" is used as type of some session variable.
  *
  * Caller should provide either the associated relation of a rowtype,
  * or a type name (not both) for use in the error message, if any.
@@ -6831,6 +6833,45 @@ find_composite_type_dependencies(Oid typeOid, Relation origRelation,
 			continue;
 		}
 
+		/* check if the type is used as type of some session variable */
+		if (pg_depend->classid == VariableRelationId)
+		{
+			Oid			varid = pg_depend->objid;
+
+			if (origTypeName)
+				ereport(ERROR,
+						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+						 errmsg("cannot alter type \"%s\" because session variable \"%s.%s\" uses it",
+								origTypeName,
+								get_namespace_name(get_session_variable_namespace(varid)),
+								get_session_variable_name(varid))));
+			else if (origRelation->rd_rel->relkind == RELKIND_COMPOSITE_TYPE)
+				ereport(ERROR,
+						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+						 errmsg("cannot alter type \"%s\" because session variable \"%s.%s\" uses it",
+								RelationGetRelationName(origRelation),
+								get_namespace_name(get_session_variable_namespace(varid)),
+								get_session_variable_name(varid))));
+			else if (origRelation->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+				ereport(ERROR,
+						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+						 errmsg("cannot alter foreign table \"%s\" because session variable \"%s.%s\" uses it",
+								RelationGetRelationName(origRelation),
+								get_namespace_name(get_session_variable_namespace(varid)),
+								get_session_variable_name(varid))));
+			else if (origRelation->rd_rel->relkind == RELKIND_RELATION ||
+					 origRelation->rd_rel->relkind == RELKIND_MATVIEW ||
+					 origRelation->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+				ereport(ERROR,
+						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+						 errmsg("cannot alter table \"%s\" because session variable \"%s.%s\" uses it",
+								RelationGetRelationName(origRelation),
+								get_namespace_name(get_session_variable_namespace(varid)),
+								get_session_variable_name(varid))));
+
+			continue;
+		}
+
 		/* Else, ignore dependees that aren't relations */
 		if (pg_depend->classid != RelationRelationId)
 			continue;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 67eb96396af..89fa23c2dd4 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -52,6 +52,7 @@
 #include "catalog/namespace.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_trigger.h"
+#include "catalog/pg_variable.h"
 #include "commands/defrem.h"
 #include "commands/trigger.h"
 #include "gramparse.h"
@@ -281,8 +282,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 		ConstraintsSetStmt CopyStmt CreateAsStmt CreateCastStmt
 		CreateDomainStmt CreateExtensionStmt CreateGroupStmt CreateOpClassStmt
 		CreateOpFamilyStmt AlterOpFamilyStmt CreatePLangStmt
-		CreateSchemaStmt CreateSeqStmt CreateStmt CreateStatsStmt CreateTableSpaceStmt
-		CreateFdwStmt CreateForeignServerStmt CreateForeignTableStmt
+		CreateSchemaStmt CreateSeqStmt CreateSessionVarStmt CreateStmt CreateStatsStmt
+		CreateTableSpaceStmt CreateFdwStmt CreateForeignServerStmt CreateForeignTableStmt
 		CreateAssertionStmt CreateTransformStmt CreateTrigStmt CreateEventTrigStmt
 		CreateUserStmt CreateUserMappingStmt CreateRoleStmt CreatePolicyStmt
 		CreatedbStmt DeclareCursorStmt DefineStmt DeleteStmt DiscardStmt DoStmt
@@ -775,8 +776,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	UESCAPE UNBOUNDED UNCONDITIONAL 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
 
@@ -1042,6 +1043,7 @@ stmt:
 			| CreatePolicyStmt
 			| CreatePLangStmt
 			| CreateSchemaStmt
+			| CreateSessionVarStmt
 			| CreateSeqStmt
 			| CreateStmt
 			| CreateSubscriptionStmt
@@ -1584,6 +1586,7 @@ schema_stmt:
 			| CreateTrigStmt
 			| GrantStmt
 			| ViewStmt
+			| CreateSessionVarStmt
 		;
 
 
@@ -5210,6 +5213,34 @@ create_extension_opt_item:
 				}
 		;
 
+/*****************************************************************************
+ *
+ *		QUERY :
+ *				CREATE VARIABLE varname [AS] type
+ *
+ *****************************************************************************/
+
+CreateSessionVarStmt:
+			CREATE VARIABLE qualified_name opt_as Typename opt_collate_clause
+				{
+					CreateSessionVarStmt *n = makeNode(CreateSessionVarStmt);
+					n->variable = $3;
+					n->typeName = $5;
+					n->collClause = (CollateClause *) $6;
+					n->if_not_exists = false;
+					$$ = (Node *) n;
+				}
+			| CREATE VARIABLE IF_P NOT EXISTS qualified_name opt_as Typename opt_collate_clause
+				{
+					CreateSessionVarStmt *n = makeNode(CreateSessionVarStmt);
+					n->variable = $6;
+					n->typeName = $8;
+					n->collClause = (CollateClause *) $9;
+					n->if_not_exists = true;
+					$$ = (Node *) n;
+				}
+		;
+
 /*****************************************************************************
  *
  * ALTER EXTENSION name UPDATE [ TO version ]
@@ -6998,6 +7029,7 @@ object_type_any_name:
 			| TEXT_P SEARCH DICTIONARY				{ $$ = OBJECT_TSDICTIONARY; }
 			| TEXT_P SEARCH TEMPLATE				{ $$ = OBJECT_TSTEMPLATE; }
 			| TEXT_P SEARCH CONFIGURATION			{ $$ = OBJECT_TSCONFIGURATION; }
+			| VARIABLE								{ $$ = OBJECT_VARIABLE; }
 		;
 
 /*
@@ -7874,6 +7906,14 @@ privilege_target:
 					n->objs = $2;
 					$$ = n;
 				}
+			| VARIABLE qualified_name_list
+				{
+					PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget));
+					n->targtype = ACL_TARGET_OBJECT;
+					n->objtype = OBJECT_VARIABLE;
+					n->objs = $2;
+					$$ = n;
+				}
 			| ALL TABLES IN_P SCHEMA name_list
 				{
 					PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget));
@@ -7919,6 +7959,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;
+				}
 		;
 
 
@@ -8116,6 +8164,7 @@ defacl_privilege_target:
 			| SEQUENCES		{ $$ = OBJECT_SEQUENCE; }
 			| TYPES_P		{ $$ = OBJECT_TYPE; }
 			| SCHEMAS		{ $$ = OBJECT_SCHEMA; }
+			| VARIABLES		{ $$ = OBJECT_VARIABLE; }
 		;
 
 
@@ -9904,6 +9953,24 @@ RenameStmt: ALTER AGGREGATE aggregate_with_argtypes RENAME TO name
 					n->missing_ok = false;
 					$$ = (Node *) n;
 				}
+			| ALTER VARIABLE any_name RENAME TO name
+				{
+					RenameStmt *n = makeNode(RenameStmt);
+					n->renameType = OBJECT_VARIABLE;
+					n->object = (Node *) $3;
+					n->newname = $6;
+					n->missing_ok = false;
+					$$ = (Node *)n;
+				}
+			| ALTER VARIABLE IF_P EXISTS any_name RENAME TO name
+				{
+					RenameStmt *n = makeNode(RenameStmt);
+					n->renameType = OBJECT_VARIABLE;
+					n->object = (Node *) $5;
+					n->newname = $8;
+					n->missing_ok = true;
+					$$ = (Node *)n;
+				}
 		;
 
 opt_column: COLUMN
@@ -10265,6 +10332,24 @@ AlterObjectSchemaStmt:
 					n->missing_ok = false;
 					$$ = (Node *) n;
 				}
+			| ALTER VARIABLE any_name SET SCHEMA name
+				{
+					AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt);
+					n->objectType = OBJECT_VARIABLE;
+					n->object = (Node *) $3;
+					n->newschema = $6;
+					n->missing_ok = false;
+					$$ = (Node *)n;
+				}
+			| ALTER VARIABLE IF_P EXISTS any_name SET SCHEMA name
+				{
+					AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt);
+					n->objectType = OBJECT_VARIABLE;
+					n->object = (Node *) $5;
+					n->newschema = $8;
+					n->missing_ok = true;
+					$$ = (Node *)n;
+				}
 		;
 
 /*****************************************************************************
@@ -10546,6 +10631,14 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec
 					n->newowner = $6;
 					$$ = (Node *) n;
 				}
+			| ALTER VARIABLE any_name OWNER TO RoleSpec
+				{
+					AlterOwnerStmt *n = makeNode(AlterOwnerStmt);
+					n->objectType = OBJECT_VARIABLE;
+					n->object = (Node *) $3;
+					n->newowner = $6;
+					$$ = (Node *)n;
+				}
 		;
 
 
@@ -17917,6 +18010,8 @@ unreserved_keyword:
 			| VALIDATE
 			| VALIDATOR
 			| VALUE_P
+			| VARIABLE
+			| VARIABLES
 			| VARYING
 			| VERSION_P
 			| VIEW
@@ -18570,6 +18665,8 @@ bare_label_keyword:
 			| VALUE_P
 			| VALUES
 			| VARCHAR
+			| VARIABLE
+			| VARIABLES
 			| VARIADIC
 			| VERBOSE
 			| VERSION_P
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index 0f324ee4e31..4eaed1f8439 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -105,6 +105,7 @@ typedef struct
 	List	   *indexes;		/* CREATE INDEX items */
 	List	   *triggers;		/* CREATE TRIGGER items */
 	List	   *grants;			/* GRANT items */
+	List	   *variables;		/* CREATE VARIABLE items */
 } CreateSchemaStmtContext;
 
 
@@ -4039,6 +4040,7 @@ transformCreateSchemaStmtElements(List *schemaElts, const char *schemaName)
 	cxt.indexes = NIL;
 	cxt.triggers = NIL;
 	cxt.grants = NIL;
+	cxt.variables = NIL;
 
 	/*
 	 * Run through each schema element in the schema element list. Separate
@@ -4107,6 +4109,15 @@ transformCreateSchemaStmtElements(List *schemaElts, const char *schemaName)
 				cxt.grants = lappend(cxt.grants, element);
 				break;
 
+			case T_CreateSessionVarStmt:
+				{
+					CreateSessionVarStmt *elp = (CreateSessionVarStmt *) element;
+
+					setSchemaName(cxt.schemaname, &elp->variable->schemaname);
+					cxt.variables = lappend(cxt.variables, element);
+				}
+				break;
+
 			default:
 				elog(ERROR, "unrecognized node type: %d",
 					 (int) nodeTag(element));
@@ -4120,6 +4131,7 @@ transformCreateSchemaStmtElements(List *schemaElts, const char *schemaName)
 	result = list_concat(result, cxt.indexes);
 	result = list_concat(result, cxt.triggers);
 	result = list_concat(result, cxt.grants);
+	result = list_concat(result, cxt.variables);
 
 	return result;
 }
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index f28bf371059..217c6028497 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -23,6 +23,7 @@
 #include "catalog/namespace.h"
 #include "catalog/pg_authid.h"
 #include "catalog/pg_inherits.h"
+#include "catalog/pg_variable.h"
 #include "catalog/toasting.h"
 #include "commands/alter.h"
 #include "commands/async.h"
@@ -182,6 +183,7 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree)
 		case T_CreateRangeStmt:
 		case T_CreateRoleStmt:
 		case T_CreateSchemaStmt:
+		case T_CreateSessionVarStmt:
 		case T_CreateSeqStmt:
 		case T_CreateStatsStmt:
 		case T_CreateStmt:
@@ -1389,6 +1391,10 @@ ProcessUtilitySlow(ParseState *pstate,
 				}
 				break;
 
+			case T_CreateSessionVarStmt:
+				address = CreateVariable(pstate, (CreateSessionVarStmt *) parsetree);
+				break;
+
 				/*
 				 * ************* object creation / destruction **************
 				 */
@@ -2341,6 +2347,9 @@ AlterObjectTypeCommandTag(ObjectType objtype)
 		case OBJECT_STATISTIC_EXT:
 			tag = CMDTAG_ALTER_STATISTICS;
 			break;
+		case OBJECT_VARIABLE:
+			tag = CMDTAG_ALTER_VARIABLE;
+			break;
 		default:
 			tag = CMDTAG_UNKNOWN;
 			break;
@@ -2649,6 +2658,9 @@ CreateCommandTag(Node *parsetree)
 				case OBJECT_STATISTIC_EXT:
 					tag = CMDTAG_DROP_STATISTICS;
 					break;
+				case OBJECT_VARIABLE:
+					tag = CMDTAG_DROP_VARIABLE;
+					break;
 				default:
 					tag = CMDTAG_UNKNOWN;
 			}
@@ -3225,6 +3237,10 @@ CreateCommandTag(Node *parsetree)
 			}
 			break;
 
+		case T_CreateSessionVarStmt:
+			tag = CMDTAG_CREATE_VARIABLE;
+			break;
+
 		default:
 			elog(WARNING, "unrecognized node type: %d",
 				 (int) nodeTag(parsetree));
diff --git a/src/backend/utils/adt/acl.c b/src/backend/utils/adt/acl.c
index 2a716cc6b7f..98522a45612 100644
--- a/src/backend/utils/adt/acl.c
+++ b/src/backend/utils/adt/acl.c
@@ -851,6 +851,10 @@ acldefault(ObjectType objtype, Oid ownerId)
 			world_default = ACL_NO_RIGHTS;
 			owner_default = ACL_ALL_RIGHTS_PARAMETER_ACL;
 			break;
+		case OBJECT_VARIABLE:
+			world_default = ACL_NO_RIGHTS;
+			owner_default = ACL_ALL_RIGHTS_VARIABLE;
+			break;
 		default:
 			elog(ERROR, "unrecognized object type: %d", (int) objtype);
 			world_default = ACL_NO_RIGHTS;	/* keep compiler quiet */
@@ -948,6 +952,9 @@ acldefault_sql(PG_FUNCTION_ARGS)
 		case 'T':
 			objtype = OBJECT_TYPE;
 			break;
+		case 'V':
+			objtype = OBJECT_VARIABLE;
+			break;
 		default:
 			elog(ERROR, "unrecognized object type abbreviation: %c", objtypec);
 	}
diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c
index a85dc0d891f..b464a7f090a 100644
--- a/src/backend/utils/cache/lsyscache.c
+++ b/src/backend/utils/cache/lsyscache.c
@@ -38,6 +38,7 @@
 #include "catalog/pg_subscription.h"
 #include "catalog/pg_transform.h"
 #include "catalog/pg_type.h"
+#include "catalog/pg_variable.h"
 #include "miscadmin.h"
 #include "nodes/makefuncs.h"
 #include "utils/array.h"
@@ -3714,3 +3715,115 @@ get_subscription_name(Oid subid, bool missing_ok)
 
 	return subname;
 }
+
+/*				---------- PG_VARIABLE CACHE ----------				 */
+
+/*
+ * get_varname_varid
+ *		Given name and namespace of variable, look up the OID.
+ */
+Oid
+get_varname_varid(const char *varname, Oid varnamespace)
+{
+	return GetSysCacheOid2(VARIABLENAMENSP, Anum_pg_variable_oid,
+						   PointerGetDatum(varname),
+						   ObjectIdGetDatum(varnamespace));
+}
+
+/*
+ * get_session_variable_name
+ *		Returns a palloc'd copy of the name of a given session variable.
+ */
+char *
+get_session_variable_name(Oid varid)
+{
+	HeapTuple	tup;
+	Form_pg_variable varform;
+	char	   *varname;
+
+	tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid));
+
+	if (!HeapTupleIsValid(tup))
+		elog(ERROR, "cache lookup failed for session variable %u", varid);
+
+	varform = (Form_pg_variable) GETSTRUCT(tup);
+
+	varname = pstrdup(NameStr(varform->varname));
+
+	ReleaseSysCache(tup);
+
+	return varname;
+}
+
+/*
+ * get_session_variable_namespace
+ *		Returns the pg_namespace OID associated with a given session variable.
+ */
+Oid
+get_session_variable_namespace(Oid varid)
+{
+	HeapTuple	tup;
+	Form_pg_variable varform;
+	Oid			varnamespace;
+
+	tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid));
+
+	if (!HeapTupleIsValid(tup))
+		elog(ERROR, "cache lookup failed for variable %u", varid);
+
+	varform = (Form_pg_variable) GETSTRUCT(tup);
+
+	varnamespace = varform->varnamespace;
+
+	ReleaseSysCache(tup);
+
+	return varnamespace;
+}
+
+/*
+ * Returns the type of the given session variable.
+ */
+Oid
+get_session_variable_type(Oid varid)
+{
+	HeapTuple	tup;
+	Form_pg_variable varform;
+	Oid			vartype;
+
+	tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid));
+
+	if (!HeapTupleIsValid(tup))
+		elog(ERROR, "cache lookup failed for session variable %u", varid);
+
+	varform = (Form_pg_variable) GETSTRUCT(tup);
+
+	vartype = varform->vartype;
+
+	ReleaseSysCache(tup);
+
+	return vartype;
+}
+
+/*
+ * Returns the type, typmod and collid of the given session variable.
+ */
+void
+get_session_variable_type_typmod_collid(Oid varid, Oid *typid, int32 *typmod,
+										Oid *collid)
+{
+	HeapTuple	tup;
+	Form_pg_variable varform;
+
+	tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid));
+
+	if (!HeapTupleIsValid(tup))
+		elog(ERROR, "cache lookup failed for session variable %u", varid);
+
+	varform = (Form_pg_variable) GETSTRUCT(tup);
+
+	*typid = varform->vartype;
+	*typmod = varform->vartypmod;
+	*collid = varform->varcollation;
+
+	ReleaseSysCache(tup);
+}
diff --git a/src/bin/pg_dump/common.c b/src/bin/pg_dump/common.c
index 33d323085f5..b0451c1b903 100644
--- a/src/bin/pg_dump/common.c
+++ b/src/bin/pg_dump/common.c
@@ -246,6 +246,9 @@ getSchemaData(Archive *fout, int *numTablesPtr)
 	pg_log_info("reading subscription membership of tables");
 	getSubscriptionTables(fout);
 
+	pg_log_info("reading variables");
+	getVariables(fout);
+
 	free(inhinfo);				/* not needed any longer */
 
 	*numTablesPtr = numTables;
diff --git a/src/bin/pg_dump/dumputils.c b/src/bin/pg_dump/dumputils.c
index 5649859aa1e..50de5d0ea29 100644
--- a/src/bin/pg_dump/dumputils.c
+++ b/src/bin/pg_dump/dumputils.c
@@ -511,6 +511,12 @@ do { \
 		CONVERT_PRIV('r', "SELECT");
 		CONVERT_PRIV('w', "UPDATE");
 	}
+	else if (strcmp(type, "VARIABLE") == 0 ||
+			 strcmp(type, "VARIABLES") == 0)
+	{
+		CONVERT_PRIV('r', "SELECT");
+		CONVERT_PRIV('w', "UPDATE");
+	}
 	else
 		abort();
 
diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index 68ae2970ade..3fa61090c64 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -134,12 +134,14 @@ typedef struct _restoreOptions
 	int			selFunction;
 	int			selTrigger;
 	int			selTable;
+	int			selVariable;
 	SimpleStringList indexNames;
 	SimpleStringList functionNames;
 	SimpleStringList schemaNames;
 	SimpleStringList schemaExcludeNames;
 	SimpleStringList triggerNames;
 	SimpleStringList tableNames;
+	SimpleStringList variableNames;
 
 	int			useDB;
 	ConnParams	cparams;		/* parameters to use if useDB */
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 8c20c263c4b..6f012a1b558 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -3098,6 +3098,14 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 					!simple_string_list_member(&ropt->triggerNames, te->tag))
 					return 0;
 			}
+			else if (strcmp(te->desc, "VARIABLE") == 0)
+			{
+				if (!ropt->selVariable)
+					return 0;
+				if (ropt->variableNames.head != NULL &&
+					!simple_string_list_member(&ropt->variableNames, te->tag))
+					return 0;
+			}
 			else
 				return 0;
 		}
@@ -3647,6 +3655,7 @@ _getObjectDescription(PQExpBuffer buf, const TocEntry *te)
 		strcmp(type, "TEXT SEARCH DICTIONARY") == 0 ||
 		strcmp(type, "TEXT SEARCH CONFIGURATION") == 0 ||
 		strcmp(type, "TYPE") == 0 ||
+		strcmp(type, "VARIABLE") == 0 ||
 		strcmp(type, "VIEW") == 0 ||
 	/* non-schema-specified objects */
 		strcmp(type, "DATABASE") == 0 ||
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index a8c141b689d..b8d839ece41 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -363,6 +363,7 @@ static void dumpPublication(Archive *fout, const PublicationInfo *pubinfo);
 static void dumpPublicationTable(Archive *fout, const PublicationRelInfo *pubrinfo);
 static void dumpSubscription(Archive *fout, const SubscriptionInfo *subinfo);
 static void dumpSubscriptionTable(Archive *fout, const SubRelInfo *subrinfo);
+static void dumpVariable(Archive *fout, const VariableInfo *varinfo);
 static void dumpDatabase(Archive *fout);
 static void dumpDatabaseConfig(Archive *AH, PQExpBuffer outbuf,
 							   const char *dbname, Oid dboid);
@@ -5416,6 +5417,190 @@ get_next_possible_free_pg_type_oid(Archive *fout, PQExpBuffer upgrade_query)
 	return next_possible_free_oid;
 }
 
+/*
+ * getVariables
+ *	  get information about variables
+ */
+void
+getVariables(Archive *fout)
+{
+	PQExpBuffer query;
+	PGresult   *res;
+	VariableInfo *varinfo;
+	int			i_tableoid;
+	int			i_oid;
+	int			i_varname;
+	int			i_varnamespace;
+	int			i_vartype;
+	int			i_vartypname;
+	int			i_varowner;
+	int			i_varcollation;
+	int			i_varacl;
+	int			i_acldefault;
+	int			i,
+				ntups;
+
+	if (fout->remoteVersion < 180000)
+		return;
+
+	query = createPQExpBuffer();
+
+	resetPQExpBuffer(query);
+
+	/* get the variables in current database */
+	appendPQExpBuffer(query,
+					  "SELECT v.tableoid, v.oid, v.varname,\n"
+					  "       v.varnamespace, v.vartype,\n"
+					  "       pg_catalog.format_type(v.vartype, v.vartypmod) as vartypname,\n"
+					  "       CASE WHEN v.varcollation <> t.typcollation "
+					  "            THEN v.varcollation\n"
+					  "            ELSE 0\n"
+					  "       END AS varcollation,\n"
+					  "       v.varowner, v.varacl,\n"
+					  "       acldefault('V', v.varowner) AS acldefault\n"
+					  "FROM pg_catalog.pg_variable v\n"
+					  "JOIN pg_catalog.pg_type t "
+					  "ON (v.vartype = t.oid)");
+
+	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+
+	ntups = PQntuples(res);
+
+	i_tableoid = PQfnumber(res, "tableoid");
+	i_oid = PQfnumber(res, "oid");
+	i_varname = PQfnumber(res, "varname");
+	i_varnamespace = PQfnumber(res, "varnamespace");
+	i_vartype = PQfnumber(res, "vartype");
+	i_vartypname = PQfnumber(res, "vartypname");
+	i_varcollation = PQfnumber(res, "varcollation");
+
+	i_varowner = PQfnumber(res, "varowner");
+	i_varacl = PQfnumber(res, "varacl");
+	i_acldefault = PQfnumber(res, "acldefault");
+
+	varinfo = pg_malloc(ntups * sizeof(VariableInfo));
+
+	for (i = 0; i < ntups; i++)
+	{
+		TypeInfo   *vtype;
+
+		varinfo[i].dobj.objType = DO_VARIABLE;
+		varinfo[i].dobj.catId.tableoid =
+			atooid(PQgetvalue(res, i, i_tableoid));
+		varinfo[i].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
+		AssignDumpId(&varinfo[i].dobj);
+		varinfo[i].dobj.name = pg_strdup(PQgetvalue(res, i, i_varname));
+		varinfo[i].dobj.namespace =
+			findNamespace(atooid(PQgetvalue(res, i, i_varnamespace)));
+
+		varinfo[i].vartype = atooid(PQgetvalue(res, i, i_vartype));
+		varinfo[i].vartypname = pg_strdup(PQgetvalue(res, i, i_vartypname));
+		varinfo[i].varcollation = atooid(PQgetvalue(res, i, i_varcollation));
+
+		varinfo[i].dacl.acl = pg_strdup(PQgetvalue(res, i, i_varacl));
+		varinfo[i].dacl.acldefault = pg_strdup(PQgetvalue(res, i, i_acldefault));
+		varinfo[i].dacl.privtype = 0;
+		varinfo[i].dacl.initprivs = NULL;
+		varinfo[i].rolname = getRoleName(PQgetvalue(res, i, i_varowner));
+
+		/* do not try to dump ACL if no ACL exists */
+		if (!PQgetisnull(res, i, i_varacl))
+			varinfo[i].dobj.components |= DUMP_COMPONENT_ACL;
+
+		if (strlen(varinfo[i].rolname) == 0)
+			pg_log_warning("owner of variable \"%s\" appears to be invalid",
+						   varinfo[i].dobj.name);
+
+		/* decide whether we want to dump it */
+		selectDumpableObject(&(varinfo[i].dobj), fout);
+
+		vtype = findTypeByOid(varinfo[i].vartype);
+		addObjectDependency(&varinfo[i].dobj, vtype->dobj.dumpId);
+	}
+	PQclear(res);
+
+	destroyPQExpBuffer(query);
+}
+
+/*
+ * dumpVariable
+ *	  dump the definition of the given session variable
+ */
+static void
+dumpVariable(Archive *fout, const VariableInfo *varinfo)
+{
+	DumpOptions *dopt = fout->dopt;
+
+	PQExpBuffer delq;
+	PQExpBuffer query;
+	char	   *qualvarname;
+	const char *vartypname;
+	Oid			varcollation;
+
+	/* skip if not to be dumped */
+	if (!varinfo->dobj.dump || dopt->dataOnly)
+		return;
+
+	delq = createPQExpBuffer();
+	query = createPQExpBuffer();
+
+	qualvarname = pg_strdup(fmtQualifiedDumpable(varinfo));
+	vartypname = varinfo->vartypname;
+	varcollation = varinfo->varcollation;
+
+	appendPQExpBuffer(delq, "DROP VARIABLE %s;\n",
+					  qualvarname);
+
+	appendPQExpBuffer(query, "CREATE VARIABLE %s AS %s",
+					  qualvarname, vartypname);
+
+	if (OidIsValid(varcollation))
+	{
+		CollInfo   *coll;
+
+		coll = findCollationByOid(varcollation);
+		if (coll)
+			appendPQExpBuffer(query, " COLLATE %s",
+							  fmtQualifiedDumpable(coll));
+	}
+
+	appendPQExpBuffer(query, ";\n");
+
+	if (varinfo->dobj.dump & DUMP_COMPONENT_DEFINITION)
+		ArchiveEntry(fout, varinfo->dobj.catId, varinfo->dobj.dumpId,
+					 ARCHIVE_OPTS(.tag = varinfo->dobj.name,
+								  .namespace = varinfo->dobj.namespace->dobj.name,
+								  .owner = varinfo->rolname,
+								  .description = "VARIABLE",
+								  .section = SECTION_PRE_DATA,
+								  .createStmt = query->data,
+								  .dropStmt = delq->data));
+
+	/* dump comment if any */
+	if (varinfo->dobj.dump & DUMP_COMPONENT_COMMENT)
+		dumpComment(fout, "VARIABLE", qualvarname,
+					NULL, varinfo->rolname,
+					varinfo->dobj.catId, 0, varinfo->dobj.dumpId);
+
+	/* dump ACL if any */
+	if (varinfo->dobj.dump & DUMP_COMPONENT_ACL)
+	{
+		char	   *qvarname = pg_strdup(fmtId(varinfo->dobj.name));
+
+		dumpACL(fout, varinfo->dobj.dumpId, InvalidDumpId, "VARIABLE",
+				qvarname, NULL,
+				varinfo->dobj.namespace->dobj.name, NULL, varinfo->rolname,
+				&varinfo->dacl);
+
+		free(qvarname);
+	}
+
+	destroyPQExpBuffer(delq);
+	destroyPQExpBuffer(query);
+
+	free(qualvarname);
+}
+
 static void
 binary_upgrade_set_type_oids_by_type_oid(Archive *fout,
 										 PQExpBuffer upgrade_buffer,
@@ -10140,7 +10325,8 @@ getAdditionalACLs(Archive *fout)
 					dobj->objType == DO_TABLE ||
 					dobj->objType == DO_PROCLANG ||
 					dobj->objType == DO_FDW ||
-					dobj->objType == DO_FOREIGN_SERVER)
+					dobj->objType == DO_FOREIGN_SERVER ||
+					dobj->objType == DO_VARIABLE)
 				{
 					DumpableObjectWithAcl *daobj = (DumpableObjectWithAcl *) dobj;
 
@@ -10739,6 +10925,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
 		case DO_SUBSCRIPTION_REL:
 			dumpSubscriptionTable(fout, (const SubRelInfo *) dobj);
 			break;
+		case DO_VARIABLE:
+			dumpVariable(fout, (VariableInfo *) dobj);
+			break;
 		case DO_PRE_DATA_BOUNDARY:
 		case DO_POST_DATA_BOUNDARY:
 			/* never dumped, nothing to do */
@@ -15188,6 +15377,9 @@ dumpDefaultACL(Archive *fout, const DefaultACLInfo *daclinfo)
 		case DEFACLOBJ_NAMESPACE:
 			type = "SCHEMAS";
 			break;
+		case DEFACLOBJ_VARIABLE:
+			type = "VARIABLES";
+			break;
 		default:
 			/* shouldn't get here */
 			pg_fatal("unrecognized object type in default privileges: %d",
@@ -18898,6 +19090,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 			case DO_CONVERSION:
 			case DO_TABLE:
 			case DO_TABLE_ATTACH:
+			case DO_VARIABLE:
 			case DO_ATTRDEF:
 			case DO_PROCLANG:
 			case DO_CAST:
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index d65f5585651..f2f558b2961 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -52,6 +52,7 @@ typedef enum
 	DO_TABLE,
 	DO_TABLE_ATTACH,
 	DO_ATTRDEF,
+	DO_VARIABLE,
 	DO_INDEX,
 	DO_INDEX_ATTACH,
 	DO_STATSEXT,
@@ -699,6 +700,23 @@ typedef struct _SubRelInfo
 	char	   *srsublsn;
 } SubRelInfo;
 
+/*
+ * The VariableInfo struct is used to represent session variables
+ */
+typedef struct _VariableInfo
+{
+	DumpableObject dobj;
+	DumpableAcl dacl;
+	Oid			vartype;
+	char	   *vartypname;
+	char	   *varacl;
+	char	   *rvaracl;
+	char	   *initvaracl;
+	char	   *initrvaracl;
+	Oid			varcollation;
+	const char *rolname;		/* name of owner, or empty string */
+} VariableInfo;
+
 /*
  *	common utility functions
  */
@@ -782,5 +800,6 @@ extern void getPublicationTables(Archive *fout, TableInfo tblinfo[],
 								 int numTables);
 extern void getSubscriptions(Archive *fout);
 extern void getSubscriptionTables(Archive *fout);
+extern void getVariables(Archive *fout);
 
 #endif							/* PG_DUMP_H */
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index 3c8f2eb808d..ca323171aa8 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -77,6 +77,7 @@ enum dbObjectTypePriorities
 	PRIO_DUMMY_TYPE,
 	PRIO_ATTRDEF,
 	PRIO_LARGE_OBJECT,
+	PRIO_VARIABLE,
 	PRIO_PRE_DATA_BOUNDARY,		/* boundary! */
 	PRIO_TABLE_DATA,
 	PRIO_SEQUENCE_SET,
@@ -118,6 +119,7 @@ static const int dbObjectTypePriority[] =
 	[DO_TABLE] = PRIO_TABLE,
 	[DO_TABLE_ATTACH] = PRIO_TABLE_ATTACH,
 	[DO_ATTRDEF] = PRIO_ATTRDEF,
+	[DO_VARIABLE] = PRIO_VARIABLE,
 	[DO_INDEX] = PRIO_INDEX,
 	[DO_INDEX_ATTACH] = PRIO_INDEX_ATTACH,
 	[DO_STATSEXT] = PRIO_STATSEXT,
@@ -1500,6 +1502,10 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
 					 "POST-DATA BOUNDARY  (ID %d)",
 					 obj->dumpId);
 			return;
+		case DO_VARIABLE:
+			snprintf(buf, bufsize,
+					 "VARIABLE %s  (ID %d OID %u)",
+					 obj->name, obj->dumpId, obj->catId.oid);
 	}
 	/* shouldn't get here */
 	snprintf(buf, bufsize,
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index aa1564cd450..dd8c054a6a6 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -789,6 +789,16 @@ my %tests = (
 		unlike => { no_privs => 1, },
 	  },
 
+	'ALTER DEFAULT PRIVILEGES FOR ROLE regress_dump_test_role GRANT SELECT ON VARIABLES TO PUBLIC'
+	  => {
+		create_order => 56,
+		create_sql   => 'ALTER DEFAULT PRIVILEGES FOR ROLE regress_dump_test_role GRANT SELECT ON VARIABLES TO PUBLIC;',
+		regexp => qr/^
+			\QALTER DEFAULT PRIVILEGES FOR ROLE regress_dump_test_role GRANT SELECT ON VARIABLES TO PUBLIC;\E/xm,
+		like => { %full_runs, section_post_data => 1, },
+		unlike => { no_privs => 1, },
+	  },
+
 	'ALTER ROLE regress_dump_test_role' => {
 		regexp => qr/^
 			\QALTER ROLE regress_dump_test_role WITH \E
@@ -1674,6 +1684,23 @@ my %tests = (
 		},
 	},
 
+	'COMMENT ON VARIABLE dump_test.variable1' => {
+		create_order => 71,
+		create_sql   => 'COMMENT ON VARIABLE dump_test.variable1
+					   IS \'comment on variable\';',
+		regexp =>
+		  qr/^\QCOMMENT ON VARIABLE dump_test.variable1 IS 'comment on variable';\E/m,
+		like => {
+			%full_runs,
+			%dump_test_schema_runs,
+			section_pre_data     => 1,
+		},
+		unlike => {
+			exclude_dump_test_schema => 1,
+			only_dump_measurement    => 1,
+		},
+	},
+
 	'COPY test_table' => {
 		create_order => 4,
 		create_sql => 'INSERT INTO dump_test.test_table (col1) '
@@ -3942,6 +3969,24 @@ my %tests = (
 		},
 	},
 
+	'CREATE VARIABLE test_variable' => {
+		all_runs     => 1,
+		catch_all    => 'CREATE ... commands',
+		create_order => 61,
+		create_sql   => 'CREATE VARIABLE dump_test.variable1 AS integer;',
+		regexp => qr/^
+			\QCREATE VARIABLE dump_test.variable1 AS integer;\E/xm,
+		like => {
+			%full_runs,
+			%dump_test_schema_runs,
+			section_pre_data => 1,
+		},
+		unlike => {
+			exclude_dump_test_schema => 1,
+			only_dump_measurement    => 1,
+		},
+	},
+
 	'CREATE VIEW test_view' => {
 		create_order => 61,
 		create_sql => 'CREATE VIEW dump_test.test_view
@@ -4404,6 +4449,25 @@ my %tests = (
 		like => {},
 	},
 
+	'GRANT SELECT ON VARIABLE dump_test.variable1' => {
+		create_order => 73,
+		create_sql =>
+		  'GRANT SELECT ON VARIABLE dump_test.variable1 TO regress_dump_test_role;',
+		regexp => qr/^
+			\QGRANT SELECT ON VARIABLE dump_test.variable1 TO regress_dump_test_role;\E
+			/xm,
+		like => {
+			%full_runs,
+			%dump_test_schema_runs,
+			section_pre_data => 1,
+		},
+		unlike => {
+			exclude_dump_test_schema => 1,
+			no_privs                 => 1,
+			only_dump_measurement    => 1,
+		},
+	},
+
 	'REFRESH MATERIALIZED VIEW matview' => {
 		regexp => qr/^\QREFRESH MATERIALIZED VIEW dump_test.matview;\E/m,
 		like =>
diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index 1f3cbb11f7c..7594f1010a6 100644
--- a/src/bin/psql/command.c
+++ b/src/bin/psql/command.c
@@ -1077,6 +1077,9 @@ exec_command_d(PsqlScanState scan_state, bool active_branch, const char *cmd)
 						break;
 				}
 				break;
+			case 'V':			/* Variables */
+				success = listVariables(pattern, show_verbose);
+				break;
 			case 'x':			/* Extensions */
 				if (show_verbose)
 					success = listExtensionContents(pattern);
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 5bfebad64d5..c52c8fd147f 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -5195,6 +5195,102 @@ error_return:
 	return false;
 }
 
+/*
+ * \dV
+ *
+ * listVariables()
+ */
+bool
+listVariables(const char *pattern, bool verbose)
+{
+	PQExpBufferData buf;
+	PGresult   *res;
+	printQueryOpt myopt = pset.popt;
+	static const bool translate_columns[] = {false, false, false, false, false, false, false};
+
+	if (pset.sversion < 180000)
+	{
+		char		sverbuf[32];
+
+		pg_log_error("The server (version %s) does not support session variables.",
+					 formatPGVersionNumber(pset.sversion, false,
+										   sverbuf, sizeof(sverbuf)));
+		return true;
+	}
+
+	initPQExpBuffer(&buf);
+
+	printfPQExpBuffer(&buf,
+					  "SELECT n.nspname as \"%s\",\n"
+					  "  v.varname as \"%s\",\n"
+					  "  pg_catalog.format_type(v.vartype, v.vartypmod) as \"%s\",\n"
+					  "  (SELECT c.collname FROM pg_catalog.pg_collation c, pg_catalog.pg_type bt\n"
+					  "   WHERE c.oid = v.varcollation AND bt.oid = v.vartype AND v.varcollation <> bt.typcollation) as \"%s\",\n"
+					  "  pg_catalog.pg_get_userbyid(v.varowner) as \"%s\"\n",
+					  gettext_noop("Schema"),
+					  gettext_noop("Name"),
+					  gettext_noop("Type"),
+					  gettext_noop("Collation"),
+					  gettext_noop("Owner"));
+
+	if (verbose)
+	{
+		appendPQExpBufferStr(&buf, ",\n  ");
+		printACLColumn(&buf, "v.varacl");
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.obj_description(v.oid, 'pg_variable') AS \"%s\"",
+						  gettext_noop("Description"));
+	}
+
+	appendPQExpBufferStr(&buf,
+						 "\nFROM pg_catalog.pg_variable v"
+						 "\n     LEFT JOIN pg_catalog.pg_namespace n ON n.oid = v.varnamespace");
+
+	appendPQExpBufferStr(&buf, "\nWHERE true\n");
+	if (!pattern)
+		appendPQExpBufferStr(&buf, "      AND n.nspname <> 'pg_catalog'\n"
+							 "      AND n.nspname <> 'information_schema'\n");
+
+	if (!validateSQLNamePattern(&buf, pattern, true, false,
+								"n.nspname", "v.varname", NULL,
+								"pg_catalog.pg_variable_is_visible(v.oid)",
+								NULL, 3))
+		return false;
+
+	appendPQExpBufferStr(&buf, "ORDER BY 1,2;");
+
+	res = PSQLexec(buf.data);
+	termPQExpBuffer(&buf);
+	if (!res)
+		return false;
+
+	/*
+	 * Most functions in this file are content to print an empty table when
+	 * there are no matching objects.  We intentionally deviate from that
+	 * here, but only in !quiet mode, for historical reasons.
+	 */
+	if (PQntuples(res) == 0 && !pset.quiet)
+	{
+		if (pattern)
+			pg_log_error("Did not find any session variable named \"%s\".",
+						 pattern);
+		else
+			pg_log_error("Did not find any session variables.");
+	}
+	else
+	{
+		myopt.nullPrint = NULL;
+		myopt.title = _("List of variables");
+		myopt.translate_header = true;
+		myopt.translate_columns = translate_columns;
+		myopt.n_translate_columns = lengthof(translate_columns);
+
+		printQuery(res, &myopt, pset.queryFout, false, pset.logfile);
+	}
+
+	PQclear(res);
+	return true;
+}
 
 /*
  * \dFp
diff --git a/src/bin/psql/describe.h b/src/bin/psql/describe.h
index 273f974538e..4e15a180ea6 100644
--- a/src/bin/psql/describe.h
+++ b/src/bin/psql/describe.h
@@ -149,4 +149,7 @@ extern bool listOpFamilyFunctions(const char *access_method_pattern,
 /* \dl or \lo_list */
 extern bool listLargeObjects(bool verbose);
 
+/* \dV */
+extern bool listVariables(const char *pattern, bool varbose);
+
 #endif							/* DESCRIBE_H */
diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c
index 02fe5d151e0..317bc2eab6e 100644
--- a/src/bin/psql/help.c
+++ b/src/bin/psql/help.c
@@ -265,6 +265,7 @@ slashUsage(unsigned short int pager)
 	HELP0("  \\dT[S+] [PATTERN]      list data types\n");
 	HELP0("  \\du[S+] [PATTERN]      list roles\n");
 	HELP0("  \\dv[S+] [PATTERN]      list views\n");
+	HELP0("  \\dV     [PATTERN]      list session variables\n");
 	HELP0("  \\dx[+]  [PATTERN]      list extensions\n");
 	HELP0("  \\dX     [PATTERN]      list extended statistics\n");
 	HELP0("  \\dy[+]  [PATTERN]      list event triggers\n");
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index fad2277991d..293648e30c7 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -970,6 +970,13 @@ static const SchemaQuery Query_for_trigger_of_table = {
 	.refnamespace = "c1.relnamespace",
 };
 
+static const SchemaQuery Query_for_list_of_variables = {
+	.min_server_version = 180000,
+	.catname = "pg_catalog.pg_variable v",
+	.viscondition = "pg_catalog.pg_variable_is_visible(v.oid)",
+	.namespace = "v.varnamespace",
+	.result = "v.varname",
+};
 
 /*
  * Queries to get lists of names of various kinds of things, possibly
@@ -1304,6 +1311,7 @@ static const pgsql_thing_t words_after_create[] = {
 																			 * TABLE ... */
 	{"USER", Query_for_list_of_roles, NULL, NULL, Keywords_for_user_thing},
 	{"USER MAPPING FOR", NULL, NULL, NULL},
+	{"VARIABLE", NULL, NULL, &Query_for_list_of_variables},
 	{"VIEW", NULL, NULL, &Query_for_list_of_views},
 	{NULL}						/* end of list */
 };
@@ -1865,7 +1873,7 @@ psql_completion(const char *text, int start, int end)
 		"\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL",
 		"\\dm", "\\dn", "\\do", "\\dO", "\\dp", "\\dP", "\\dPi", "\\dPt",
 		"\\drds", "\\drg", "\\dRs", "\\dRp", "\\ds",
-		"\\dt", "\\dT", "\\dv", "\\du", "\\dx", "\\dX", "\\dy",
+		"\\dt", "\\dT", "\\dv", "\\du", "\\dx", "\\dX", "\\dy", "\\dV",
 		"\\echo", "\\edit", "\\ef", "\\elif", "\\else", "\\encoding",
 		"\\endif", "\\errverbose", "\\ev",
 		"\\f",
@@ -2556,6 +2564,9 @@ match_previous_words(int pattern_id,
 										  "ALL");
 	else if (Matches("ALTER", "SYSTEM", "SET", MatchAny))
 		COMPLETE_WITH("TO");
+	/* ALTER VARIABLE <name> */
+	else if (Matches("ALTER", "VARIABLE", MatchAny))
+		COMPLETE_WITH("OWNER TO", "RENAME TO", "SET SCHEMA");
 	/* ALTER VIEW <name> */
 	else if (Matches("ALTER", "VIEW", MatchAny))
 		COMPLETE_WITH("ALTER COLUMN", "OWNER TO", "RENAME", "RESET", "SET");
@@ -3121,7 +3132,7 @@ match_previous_words(int pattern_id,
 					  "ROUTINE", "RULE", "SCHEMA", "SEQUENCE", "SERVER",
 					  "STATISTICS", "SUBSCRIPTION", "TABLE",
 					  "TABLESPACE", "TEXT SEARCH", "TRANSFORM FOR",
-					  "TRIGGER", "TYPE", "VIEW");
+					  "TRIGGER", "TYPE", "VARIABLE", "VIEW");
 	else if (Matches("COMMENT", "ON", "ACCESS", "METHOD"))
 		COMPLETE_WITH_QUERY(Query_for_list_of_access_methods);
 	else if (Matches("COMMENT", "ON", "CONSTRAINT"))
@@ -3931,6 +3942,13 @@ match_previous_words(int pattern_id,
 		else if (TailMatches("=", MatchAnyExcept("*)")))
 			COMPLETE_WITH(",", ")");
 	}
+/* CREATE VARIABLE --- is allowed inside CREATE SCHEMA, so use TailMatches */
+	/* Complete CREATE VARIABLE <name> with AS */
+	else if (TailMatches("CREATE", "VARIABLE", MatchAny))
+		COMPLETE_WITH("AS");
+	else if (TailMatches("VARIABLE", MatchAny, "AS"))
+		/* Complete CREATE VARIABLE <name> with AS types */
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes);
 
 /* CREATE VIEW --- is allowed inside CREATE SCHEMA, so use TailMatches */
 	/* Complete CREATE [ OR REPLACE ] VIEW <name> with AS or WITH */
@@ -4193,6 +4211,12 @@ match_previous_words(int pattern_id,
 	else if (Matches("DROP", "TRANSFORM", "FOR", MatchAny, "LANGUAGE", MatchAny))
 		COMPLETE_WITH("CASCADE", "RESTRICT");
 
+	/* DROP VARIABLE */
+	else if (Matches("DROP", "VARIABLE"))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables);
+	else if (Matches("DROP", "VARIABLE", MatchAny))
+		COMPLETE_WITH("CASCADE", "RESTRICT");
+
 /* EXECUTE */
 	else if (Matches("EXECUTE"))
 		COMPLETE_WITH_QUERY(Query_for_list_of_prepared_statements);
@@ -4394,7 +4418,7 @@ match_previous_words(int pattern_id,
 		 * objects supported.
 		 */
 		if (HeadMatches("ALTER", "DEFAULT", "PRIVILEGES"))
-			COMPLETE_WITH("TABLES", "SEQUENCES", "FUNCTIONS", "PROCEDURES", "ROUTINES", "TYPES", "SCHEMAS");
+			COMPLETE_WITH("TABLES", "SEQUENCES", "FUNCTIONS", "PROCEDURES", "ROUTINES", "TYPES", "SCHEMAS", "VARIABLES");
 		else
 			COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_grantables,
 											"ALL FUNCTIONS IN SCHEMA",
@@ -4416,7 +4440,8 @@ match_previous_words(int pattern_id,
 											"SEQUENCE",
 											"TABLE",
 											"TABLESPACE",
-											"TYPE");
+											"TYPE",
+											"VARIABLE");
 	}
 	else if (TailMatches("GRANT|REVOKE", MatchAny, "ON", "ALL") ||
 			 TailMatches("REVOKE", "GRANT", "OPTION", "FOR", MatchAny, "ON", "ALL"))
@@ -4424,7 +4449,8 @@ match_previous_words(int pattern_id,
 					  "PROCEDURES IN SCHEMA",
 					  "ROUTINES IN SCHEMA",
 					  "SEQUENCES IN SCHEMA",
-					  "TABLES IN SCHEMA");
+					  "TABLES IN SCHEMA",
+					  "VARIABLES IN SCHEMA");
 	else if (TailMatches("GRANT|REVOKE", MatchAny, "ON", "FOREIGN") ||
 			 TailMatches("REVOKE", "GRANT", "OPTION", "FOR", MatchAny, "ON", "FOREIGN"))
 		COMPLETE_WITH("DATA WRAPPER", "SERVER");
@@ -4460,6 +4486,8 @@ match_previous_words(int pattern_id,
 			COMPLETE_WITH_QUERY(Query_for_list_of_tablespaces);
 		else if (TailMatches("TYPE"))
 			COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes);
+		else if (TailMatches("VARIABLE"))
+			COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables);
 		else if (TailMatches("GRANT", MatchAny, MatchAny, MatchAny))
 			COMPLETE_WITH("TO");
 		else
@@ -4762,7 +4790,7 @@ match_previous_words(int pattern_id,
 
 /* PREPARE xx AS */
 	else if (Matches("PREPARE", MatchAny, "AS"))
-		COMPLETE_WITH("SELECT", "UPDATE", "INSERT INTO", "DELETE FROM");
+		COMPLETE_WITH("SELECT", "UPDATE", "INSERT INTO", "DELETE FROM", "LET");
 
 /*
  * PREPARE TRANSACTION is missing on purpose. It's intended for transaction
@@ -5228,6 +5256,8 @@ match_previous_words(int pattern_id,
 		COMPLETE_WITH_QUERY(Query_for_list_of_roles);
 	else if (TailMatchesCS("\\dv*"))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_views);
+	else if (TailMatchesCS("\\dV*"))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables);
 	else if (TailMatchesCS("\\dx*"))
 		COMPLETE_WITH_QUERY(Query_for_list_of_extensions);
 	else if (TailMatchesCS("\\dX*"))
diff --git a/src/include/catalog/Makefile b/src/include/catalog/Makefile
index 167f91a6e3f..507f3808218 100644
--- a/src/include/catalog/Makefile
+++ b/src/include/catalog/Makefile
@@ -81,7 +81,8 @@ CATALOG_HEADERS := \
 	pg_publication_namespace.h \
 	pg_publication_rel.h \
 	pg_subscription.h \
-	pg_subscription_rel.h
+	pg_subscription_rel.h \
+	pg_variable.h
 
 GENERATED_HEADERS := $(CATALOG_HEADERS:%.h=%_d.h)
 
diff --git a/src/include/catalog/meson.build b/src/include/catalog/meson.build
index f70d1daba52..6eec79d2eec 100644
--- a/src/include/catalog/meson.build
+++ b/src/include/catalog/meson.build
@@ -69,6 +69,7 @@ catalog_headers = [
   'pg_publication_rel.h',
   'pg_subscription.h',
   'pg_subscription_rel.h',
+  'pg_variable.h',
 ]
 
 # The .dat files we need can just be listed alphabetically.
diff --git a/src/include/catalog/namespace.h b/src/include/catalog/namespace.h
index 8d434d48d57..18ea6ac83a5 100644
--- a/src/include/catalog/namespace.h
+++ b/src/include/catalog/namespace.h
@@ -97,6 +97,8 @@ extern Oid	TypenameGetTypid(const char *typname);
 extern Oid	TypenameGetTypidExtended(const char *typname, bool temp_ok);
 extern bool TypeIsVisible(Oid typid);
 
+extern bool VariableIsVisible(Oid varid);
+
 extern FuncCandidateList FuncnameGetCandidates(List *names,
 											   int nargs, List *argnames,
 											   bool expand_variadic,
@@ -169,6 +171,8 @@ extern SearchPathMatcher *GetSearchPathMatcher(MemoryContext context);
 extern SearchPathMatcher *CopySearchPathMatcher(SearchPathMatcher *path);
 extern bool SearchPathMatchesCurrentEnvironment(SearchPathMatcher *path);
 
+extern Oid	LookupVariable(const char *nspname, const char *varname, bool missing_ok);
+
 extern Oid	get_collation_oid(List *collname, bool missing_ok);
 extern Oid	get_conversion_oid(List *conname, bool missing_ok);
 extern Oid	FindDefaultConversionProc(int32 for_encoding, int32 to_encoding);
diff --git a/src/include/catalog/pg_default_acl.h b/src/include/catalog/pg_default_acl.h
index d272cdf08b4..529d53c6bb6 100644
--- a/src/include/catalog/pg_default_acl.h
+++ b/src/include/catalog/pg_default_acl.h
@@ -68,6 +68,7 @@ MAKE_SYSCACHE(DEFACLROLENSPOBJ, pg_default_acl_role_nsp_obj_index, 8);
 #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 f23321a41f1..d676ee83e41 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6556,6 +6556,9 @@
   proname => 'pg_collation_is_visible', procost => '10', provolatile => 's',
   prorettype => 'bool', proargtypes => 'oid',
   prosrc => 'pg_collation_is_visible' },
+{ oid => '9221', descr => 'is session variable visible in search path?',
+  proname => 'pg_variable_is_visible', procost => '10', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'oid', prosrc => 'pg_variable_is_visible' },
 
 { oid => '2854', descr => 'get OID of current session\'s temp schema, if any',
   proname => 'pg_my_temp_schema', provolatile => 's', proparallel => 'r',
diff --git a/src/include/catalog/pg_variable.h b/src/include/catalog/pg_variable.h
new file mode 100644
index 00000000000..3810e040f28
--- /dev/null
+++ b/src/include/catalog/pg_variable.h
@@ -0,0 +1,81 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_variable.h
+ *	  definition of session variables system catalog (pg_variables)
+ *
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/pg_variable.h
+ *
+ * NOTES
+ *	  The Catalog.pm module reads this file and derives schema
+ *	  information.
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PG_VARIABLE_H
+#define PG_VARIABLE_H
+
+#include "catalog/genbki.h"
+#include "catalog/objectaddress.h"
+#include "catalog/pg_variable_d.h"
+#include "utils/acl.h"
+
+/* ----------------
+ *		pg_variable definition.  cpp turns this into
+ *		typedef struct FormData_pg_variable
+ * ----------------
+ */
+CATALOG(pg_variable,9222,VariableRelationId)
+{
+	Oid			oid;			/* oid */
+
+	/* OID of entry in pg_type for variable's type */
+	Oid			vartype BKI_LOOKUP(pg_type);
+
+	/* variable name */
+	NameData	varname;
+
+	/* OID of namespace containing variable class */
+	Oid			varnamespace BKI_LOOKUP(pg_namespace);
+
+	/* variable owner */
+	Oid			varowner BKI_LOOKUP(pg_authid);
+
+	/* typmod for variable's type */
+	int32		vartypmod BKI_DEFAULT(-1);
+
+	/* variable collation */
+	Oid			varcollation BKI_DEFAULT(0) BKI_LOOKUP_OPT(pg_collation);
+
+
+#ifdef CATALOG_VARLEN			/* variable-length fields start here */
+
+	/* access permissions */
+	aclitem		varacl[1] BKI_DEFAULT(_null_);
+
+#endif
+} FormData_pg_variable;
+
+/* ----------------
+ *		Form_pg_variable corresponds to a pointer to a tuple with
+ *		the format of the pg_variable relation.
+ * ----------------
+ */
+typedef FormData_pg_variable *Form_pg_variable;
+
+DECLARE_TOAST(pg_variable, 9223, 9224);
+
+DECLARE_UNIQUE_INDEX_PKEY(pg_variable_oid_index, 9225, VariableOidIndexId, pg_variable, btree(oid oid_ops));
+DECLARE_UNIQUE_INDEX(pg_variable_varname_nsp_index, 9226, VariableNameNspIndexId, pg_variable, btree(varname name_ops, varnamespace oid_ops));
+
+extern ObjectAddress CreateVariable(ParseState *pstate,
+									CreateSessionVarStmt *stmt);
+extern void DropVariableById(Oid varid);
+
+MAKE_SYSCACHE(VARIABLENAMENSP, pg_variable_varname_nsp_index, 8);
+MAKE_SYSCACHE(VARIABLEOID, pg_variable_oid_index, 8);
+
+#endif							/* PG_VARIABLE_H */
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 0f9462493e3..0e08296b438 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2316,6 +2316,7 @@ typedef enum ObjectType
 	OBJECT_TSTEMPLATE,
 	OBJECT_TYPE,
 	OBJECT_USER_MAPPING,
+	OBJECT_VARIABLE,
 	OBJECT_VIEW,
 } ObjectType;
 
@@ -3447,6 +3448,21 @@ typedef struct AlterStatsStmt
 	bool		missing_ok;		/* skip error if statistics object is missing */
 } AlterStatsStmt;
 
+
+/* ----------------------
+ *		{Create|Alter} VARIABLE Statement
+ * ----------------------
+ */
+typedef struct CreateSessionVarStmt
+{
+	NodeTag		type;
+	RangeVar   *variable;		/* the variable to create */
+	TypeName   *typeName;		/* the type of variable */
+	CollateClause *collClause;
+	bool		if_not_exists;	/* do nothing if it already exists */
+} CreateSessionVarStmt;
+
+
 /* ----------------------
  *		Create Function Statement
  * ----------------------
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index 899d64ad55f..56670e65b11 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -484,6 +484,8 @@ PG_KEYWORD("validator", VALIDATOR, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("value", VALUE_P, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("values", VALUES, COL_NAME_KEYWORD, BARE_LABEL)
 PG_KEYWORD("varchar", VARCHAR, COL_NAME_KEYWORD, BARE_LABEL)
+PG_KEYWORD("variable", VARIABLE, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("variables", VARIABLES, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("variadic", VARIADIC, RESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("varying", VARYING, UNRESERVED_KEYWORD, AS_LABEL)
 PG_KEYWORD("verbose", VERBOSE, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL)
diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h
index 7fdcec6dd93..9465df7b2ff 100644
--- a/src/include/tcop/cmdtaglist.h
+++ b/src/include/tcop/cmdtaglist.h
@@ -68,6 +68,7 @@ PG_CMDTAG(CMDTAG_ALTER_TRANSFORM, "ALTER TRANSFORM", true, false, false)
 PG_CMDTAG(CMDTAG_ALTER_TRIGGER, "ALTER TRIGGER", true, false, false)
 PG_CMDTAG(CMDTAG_ALTER_TYPE, "ALTER TYPE", true, true, false)
 PG_CMDTAG(CMDTAG_ALTER_USER_MAPPING, "ALTER USER MAPPING", true, false, false)
+PG_CMDTAG(CMDTAG_ALTER_VARIABLE, "ALTER VARIABLE", true, false, false)
 PG_CMDTAG(CMDTAG_ALTER_VIEW, "ALTER VIEW", true, false, false)
 PG_CMDTAG(CMDTAG_ANALYZE, "ANALYZE", false, false, false)
 PG_CMDTAG(CMDTAG_BEGIN, "BEGIN", false, false, false)
@@ -123,6 +124,7 @@ PG_CMDTAG(CMDTAG_CREATE_TRANSFORM, "CREATE TRANSFORM", true, false, false)
 PG_CMDTAG(CMDTAG_CREATE_TRIGGER, "CREATE TRIGGER", true, false, false)
 PG_CMDTAG(CMDTAG_CREATE_TYPE, "CREATE TYPE", true, false, false)
 PG_CMDTAG(CMDTAG_CREATE_USER_MAPPING, "CREATE USER MAPPING", true, false, false)
+PG_CMDTAG(CMDTAG_CREATE_VARIABLE, "CREATE VARIABLE", true, false, false)
 PG_CMDTAG(CMDTAG_CREATE_VIEW, "CREATE VIEW", true, false, false)
 PG_CMDTAG(CMDTAG_DEALLOCATE, "DEALLOCATE", false, false, false)
 PG_CMDTAG(CMDTAG_DEALLOCATE_ALL, "DEALLOCATE ALL", false, false, false)
@@ -175,6 +177,7 @@ PG_CMDTAG(CMDTAG_DROP_TRANSFORM, "DROP TRANSFORM", true, false, false)
 PG_CMDTAG(CMDTAG_DROP_TRIGGER, "DROP TRIGGER", true, false, false)
 PG_CMDTAG(CMDTAG_DROP_TYPE, "DROP TYPE", true, false, false)
 PG_CMDTAG(CMDTAG_DROP_USER_MAPPING, "DROP USER MAPPING", true, false, false)
+PG_CMDTAG(CMDTAG_DROP_VARIABLE, "DROP VARIABLE", true, false, false)
 PG_CMDTAG(CMDTAG_DROP_VIEW, "DROP VIEW", true, false, false)
 PG_CMDTAG(CMDTAG_EXECUTE, "EXECUTE", false, false, false)
 PG_CMDTAG(CMDTAG_EXPLAIN, "EXPLAIN", false, false, false)
diff --git a/src/include/utils/acl.h b/src/include/utils/acl.h
index 731d84b2a93..98aab98229e 100644
--- a/src/include/utils/acl.h
+++ b/src/include/utils/acl.h
@@ -169,6 +169,7 @@ typedef struct ArrayType Acl;
 #define ACL_ALL_RIGHTS_SCHEMA		(ACL_USAGE|ACL_CREATE)
 #define ACL_ALL_RIGHTS_TABLESPACE	(ACL_CREATE)
 #define ACL_ALL_RIGHTS_TYPE			(ACL_USAGE)
+#define ACL_ALL_RIGHTS_VARIABLE		(ACL_SELECT|ACL_UPDATE)
 
 /* operation codes for pg_*_aclmask */
 typedef enum
diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h
index 20446f6f836..8a3684e711f 100644
--- a/src/include/utils/lsyscache.h
+++ b/src/include/utils/lsyscache.h
@@ -134,6 +134,7 @@ extern char get_func_prokind(Oid funcid);
 extern bool get_func_leakproof(Oid funcid);
 extern RegProcedure get_func_support(Oid funcid);
 extern Oid	get_relname_relid(const char *relname, Oid relnamespace);
+extern Oid	get_varname_varid(const char *varname, Oid varnamespace);
 extern char *get_rel_name(Oid relid);
 extern Oid	get_rel_namespace(Oid relid);
 extern Oid	get_rel_type_id(Oid relid);
@@ -206,6 +207,14 @@ extern char *get_publication_name(Oid pubid, bool missing_ok);
 extern Oid	get_subscription_oid(const char *subname, bool missing_ok);
 extern char *get_subscription_name(Oid subid, bool missing_ok);
 
+extern char *get_session_variable_name(Oid varid);
+extern Oid	get_session_variable_namespace(Oid varid);
+extern Oid	get_session_variable_type(Oid varid);
+extern void get_session_variable_type_typmod_collid(Oid varid,
+													Oid *typid,
+													int32 *typmod,
+													Oid *collid);
+
 #define type_is_array(typid)  (get_element_type(typid) != InvalidOid)
 /* type_is_array_domain accepts both plain arrays and domains over arrays */
 #define type_is_array_domain(typid)  (get_base_element_type(typid) != InvalidOid)
diff --git a/src/test/regress/expected/dependency.out b/src/test/regress/expected/dependency.out
index 74d9ff2998d..c336f67a596 100644
--- a/src/test/regress/expected/dependency.out
+++ b/src/test/regress/expected/dependency.out
@@ -151,3 +151,20 @@ owner of type deptest_t
 DROP OWNED BY regress_dep_user2, regress_dep_user0;
 DROP USER regress_dep_user2;
 DROP USER regress_dep_user0;
+-- dependency on type
+CREATE DOMAIN vardomain AS int;
+CREATE TYPE vartype AS (a int, b int, c vardomain);
+CREATE VARIABLE var1 AS vartype;
+-- should fail
+DROP DOMAIN vardomain;
+ERROR:  cannot drop type vardomain because other objects depend on it
+DETAIL:  column c of composite type vartype depends on type vardomain
+HINT:  Use DROP ... CASCADE to drop the dependent objects too.
+DROP TYPE vartype;
+ERROR:  cannot drop type vartype because other objects depend on it
+DETAIL:  session variable var1 depends on type vartype
+HINT:  Use DROP ... CASCADE to drop the dependent objects too.
+-- clean up
+DROP VARIABLE var1;
+DROP TYPE vartype;
+DROP DOMAIN vardomain;
diff --git a/src/test/regress/expected/oidjoins.out b/src/test/regress/expected/oidjoins.out
index 215eb899be3..d9953321402 100644
--- a/src/test/regress/expected/oidjoins.out
+++ b/src/test/regress/expected/oidjoins.out
@@ -266,3 +266,7 @@ NOTICE:  checking pg_subscription {subdbid} => pg_database {oid}
 NOTICE:  checking pg_subscription {subowner} => pg_authid {oid}
 NOTICE:  checking pg_subscription_rel {srsubid} => pg_subscription {oid}
 NOTICE:  checking pg_subscription_rel {srrelid} => pg_class {oid}
+NOTICE:  checking pg_variable {vartype} => pg_type {oid}
+NOTICE:  checking pg_variable {varnamespace} => pg_namespace {oid}
+NOTICE:  checking pg_variable {varowner} => pg_authid {oid}
+NOTICE:  checking pg_variable {varcollation} => pg_collation {oid}
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index 36dc31c16c4..88e21194711 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -5930,6 +5930,30 @@ COMMIT;
 # final ON_ERROR_ROLLBACK: off
 DROP TABLE bla;
 DROP FUNCTION psql_error;
+-- session variable test
+CREATE ROLE regress_variable_owner;
+SET ROLE TO regress_variable_owner;
+CREATE VARIABLE var1 AS varchar COLLATE "C";
+\dV+ var1
+                                            List of variables
+ Schema | Name |       Type        | Collation |         Owner          | Access privileges | Description 
+--------+------+-------------------+-----------+------------------------+-------------------+-------------
+ public | var1 | character varying | C         | regress_variable_owner |                   | 
+(1 row)
+
+GRANT SELECT ON VARIABLE var1 TO PUBLIC;
+COMMENT ON VARIABLE var1 IS 'some description';
+\dV+ var1
+                                                              List of variables
+ Schema | Name |       Type        | Collation |         Owner          |                Access privileges                 |   Description    
+--------+------+-------------------+-----------+------------------------+--------------------------------------------------+------------------
+ public | var1 | character varying | C         | regress_variable_owner | regress_variable_owner=rw/regress_variable_owner+| some description
+        |      |                   |           |                        | =r/regress_variable_owner                        | 
+(1 row)
+
+DROP VARIABLE var1;
+SET ROLE TO DEFAULT;
+DROP ROLE regress_variable_owner;
 -- check describing invalid multipart names
 \dA regression.heap
 improper qualified name (too many dotted names): regression.heap
@@ -6151,6 +6175,12 @@ cross-database references are not implemented: nonesuch.public.func_deps_stat
 improper qualified name (too many dotted names): regression.myevt
 \dy nonesuch.myevt
 improper qualified name (too many dotted names): nonesuch.myevt
+\dV host.regression.public.var
+improper qualified name (too many dotted names): host.regression.public.var
+\dV regression|mydb.public.var
+cross-database references are not implemented: regression|mydb.public.var
+\dV nonesuch.public.var
+cross-database references are not implemented: nonesuch.public.var
 -- check that dots within quoted name segments are not counted
 \dA "no.such.access.method"
 List of access methods
@@ -6385,6 +6415,12 @@ List of schemas
 ------+-------+-------+---------+----------+------
 (0 rows)
 
+\dV "no.such.variable"
+            List of variables
+ Schema | Name | Type | Collation | Owner 
+--------+------+------+-----------+-------
+(0 rows)
+
 -- again, but with dotted schema qualifications.
 \dA "no.such.schema"."no.such.access.method"
 improper qualified name (too many dotted names): "no.such.schema"."no.such.access.method"
@@ -6554,6 +6590,12 @@ improper qualified name (too many dotted names): "no.such.schema"."no.such.insta
 
 \dy "no.such.schema"."no.such.event.trigger"
 improper qualified name (too many dotted names): "no.such.schema"."no.such.event.trigger"
+\dV "no.such.schema"."no.such.variable"
+            List of variables
+ Schema | Name | Type | Collation | Owner 
+--------+------+------+-----------+-------
+(0 rows)
+
 -- again, but with current database and dotted schema qualifications.
 \dt regression."no.such.schema"."no.such.table.relation"
       List of relations
@@ -6687,6 +6729,12 @@ List of text search templates
 --------+------+------------+-----------+--------------+-----
 (0 rows)
 
+\dV regression."no.such.schema"."no.such.variable"
+            List of variables
+ Schema | Name | Type | Collation | Owner 
+--------+------+------+-----------+-------
+(0 rows)
+
 -- again, but with dotted database and dotted schema qualifications.
 \dt "no.such.database"."no.such.schema"."no.such.table.relation"
 cross-database references are not implemented: "no.such.database"."no.such.schema"."no.such.table.relation"
@@ -6734,6 +6782,8 @@ cross-database references are not implemented: "no.such.database"."no.such.schem
 cross-database references are not implemented: "no.such.database"."no.such.schema"."no.such.data.type"
 \dX "no.such.database"."no.such.schema"."no.such.extended.statistics"
 cross-database references are not implemented: "no.such.database"."no.such.schema"."no.such.extended.statistics"
+\dV "no.such.database"."no.such.schema"."no.such.variable"
+cross-database references are not implemented: "no.such.database"."no.such.schema"."no.such.variable"
 -- check \drg and \du
 CREATE ROLE regress_du_role0;
 CREATE ROLE regress_du_role1;
diff --git a/src/test/regress/expected/session_variables.out b/src/test/regress/expected/session_variables.out
new file mode 100644
index 00000000000..d0aff56a01d
--- /dev/null
+++ b/src/test/regress/expected/session_variables.out
@@ -0,0 +1,58 @@
+CREATE ROLE regress_variable_owner;
+-- should be ok
+CREATE VARIABLE var1 AS int;
+-- should fail, pseudotypes are not allowed
+CREATE VARIABLE var2 AS anyelement;
+ERROR:  session variable cannot be pseudo-type anyelement
+-- should be ok, do nothing
+DROP VARIABLE IF EXISTS var2;
+NOTICE:  session variable "var2" does not exist, skipping
+-- do nothing
+CREATE VARIABLE IF NOT EXISTS var1 AS int;
+NOTICE:  session variable "var1" already exists, skipping
+-- should fail
+CREATE VARIABLE var1 AS int;
+ERROR:  session variable "var1" already exists
+-- should be ok
+DROP VARIABLE IF EXISTS var1;
+-- check comment on variable
+CREATE VARIABLE var1 AS int;
+COMMENT ON VARIABLE var1 IS 'some variable comment';
+SELECT pg_catalog.obj_description(oid, 'pg_variable') FROM pg_variable WHERE varname = 'var1';
+    obj_description    
+-----------------------
+ some variable comment
+(1 row)
+
+DROP VARIABLE var1;
+--- check access rights and supported ALTER
+CREATE SCHEMA svartest;
+GRANT ALL ON SCHEMA svartest TO regress_variable_owner;
+CREATE VARIABLE svartest.var1 AS int;
+CREATE ROLE regress_variable_reader;
+GRANT SELECT ON VARIABLE svartest.var1 TO regress_variable_reader;
+REVOKE ALL ON VARIABLE svartest.var1 FROM regress_variable_reader;
+ALTER VARIABLE svartest.var1 OWNER TO regress_variable_owner;
+ALTER VARIABLE svartest.var1 RENAME TO varxx;
+ALTER VARIABLE svartest.varxx SET SCHEMA public;
+DROP VARIABLE public.varxx;
+ALTER DEFAULT PRIVILEGES
+   FOR ROLE regress_variable_owner
+   IN SCHEMA svartest
+   GRANT SELECT ON VARIABLES TO regress_variable_reader;
+-- creating variable with default privileges
+SET ROLE TO regress_variable_owner;
+CREATE VARIABLE svartest.var1 AS int;
+SET ROLE TO DEFAULT;
+\dV+ svartest.var1
+                                                        List of variables
+  Schema  | Name |  Type   | Collation |         Owner          |                Access privileges                 | Description 
+----------+------+---------+-----------+------------------------+--------------------------------------------------+-------------
+ svartest | var1 | integer |           | regress_variable_owner | regress_variable_owner=rw/regress_variable_owner+| 
+          |      |         |           |                        | regress_variable_reader=r/regress_variable_owner | 
+(1 row)
+
+DROP VARIABLE svartest.var1;
+DROP SCHEMA svartest;
+DROP ROLE regress_variable_reader;
+DROP ROLE regress_variable_owner;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 81e4222d26a..05eb784f62b 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -111,7 +111,7 @@ test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson
 # NB: temp.sql does a reconnect which transiently uses 2 connections,
 # so keep this parallel group to at most 19 tests
 # ----------
-test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml
+test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml session_variables
 
 # ----------
 # Another group of parallel tests
diff --git a/src/test/regress/sql/dependency.sql b/src/test/regress/sql/dependency.sql
index 8d74ed7122c..6c18b7f840a 100644
--- a/src/test/regress/sql/dependency.sql
+++ b/src/test/regress/sql/dependency.sql
@@ -114,3 +114,17 @@ DROP USER regress_dep_user2;
 DROP OWNED BY regress_dep_user2, regress_dep_user0;
 DROP USER regress_dep_user2;
 DROP USER regress_dep_user0;
+
+-- dependency on type
+CREATE DOMAIN vardomain AS int;
+CREATE TYPE vartype AS (a int, b int, c vardomain);
+CREATE VARIABLE var1 AS vartype;
+
+-- should fail
+DROP DOMAIN vardomain;
+DROP TYPE vartype;
+
+-- clean up
+DROP VARIABLE var1;
+DROP TYPE vartype;
+DROP DOMAIN vardomain;
diff --git a/src/test/regress/sql/psql.sql b/src/test/regress/sql/psql.sql
index c5021fc0b13..666eddb632d 100644
--- a/src/test/regress/sql/psql.sql
+++ b/src/test/regress/sql/psql.sql
@@ -1631,6 +1631,19 @@ COMMIT;
 DROP TABLE bla;
 DROP FUNCTION psql_error;
 
+-- session variable test
+CREATE ROLE regress_variable_owner;
+SET ROLE TO regress_variable_owner;
+CREATE VARIABLE var1 AS varchar COLLATE "C";
+\dV+ var1
+GRANT SELECT ON VARIABLE var1 TO PUBLIC;
+COMMENT ON VARIABLE var1 IS 'some description';
+\dV+ var1
+DROP VARIABLE var1;
+
+SET ROLE TO DEFAULT;
+DROP ROLE regress_variable_owner;
+
 -- check describing invalid multipart names
 \dA regression.heap
 \dA nonesuch.heap
@@ -1742,6 +1755,9 @@ DROP FUNCTION psql_error;
 \dX nonesuch.public.func_deps_stat
 \dy regression.myevt
 \dy nonesuch.myevt
+\dV host.regression.public.var
+\dV regression|mydb.public.var
+\dV nonesuch.public.var
 
 -- check that dots within quoted name segments are not counted
 \dA "no.such.access.method"
@@ -1783,6 +1799,8 @@ DROP FUNCTION psql_error;
 \dx "no.such.installed.extension"
 \dX "no.such.extended.statistics"
 \dy "no.such.event.trigger"
+\dV "no.such.variable"
+
 
 -- again, but with dotted schema qualifications.
 \dA "no.such.schema"."no.such.access.method"
@@ -1823,6 +1841,7 @@ DROP FUNCTION psql_error;
 \dx "no.such.schema"."no.such.installed.extension"
 \dX "no.such.schema"."no.such.extended.statistics"
 \dy "no.such.schema"."no.such.event.trigger"
+\dV "no.such.schema"."no.such.variable"
 
 -- again, but with current database and dotted schema qualifications.
 \dt regression."no.such.schema"."no.such.table.relation"
@@ -1847,6 +1866,7 @@ DROP FUNCTION psql_error;
 \dP regression."no.such.schema"."no.such.partitioned.relation"
 \dT regression."no.such.schema"."no.such.data.type"
 \dX regression."no.such.schema"."no.such.extended.statistics"
+\dV regression."no.such.schema"."no.such.variable"
 
 -- again, but with dotted database and dotted schema qualifications.
 \dt "no.such.database"."no.such.schema"."no.such.table.relation"
@@ -1872,6 +1892,7 @@ DROP FUNCTION psql_error;
 \dP "no.such.database"."no.such.schema"."no.such.partitioned.relation"
 \dT "no.such.database"."no.such.schema"."no.such.data.type"
 \dX "no.such.database"."no.such.schema"."no.such.extended.statistics"
+\dV "no.such.database"."no.such.schema"."no.such.variable"
 
 -- check \drg and \du
 CREATE ROLE regress_du_role0;
diff --git a/src/test/regress/sql/session_variables.sql b/src/test/regress/sql/session_variables.sql
new file mode 100644
index 00000000000..82679cbf48e
--- /dev/null
+++ b/src/test/regress/sql/session_variables.sql
@@ -0,0 +1,61 @@
+CREATE ROLE regress_variable_owner;
+
+-- should be ok
+CREATE VARIABLE var1 AS int;
+
+-- should fail, pseudotypes are not allowed
+CREATE VARIABLE var2 AS anyelement;
+
+-- should be ok, do nothing
+DROP VARIABLE IF EXISTS var2;
+
+-- do nothing
+CREATE VARIABLE IF NOT EXISTS var1 AS int;
+
+-- should fail
+CREATE VARIABLE var1 AS int;
+
+-- should be ok
+DROP VARIABLE IF EXISTS var1;
+
+-- check comment on variable
+CREATE VARIABLE var1 AS int;
+COMMENT ON VARIABLE var1 IS 'some variable comment';
+SELECT pg_catalog.obj_description(oid, 'pg_variable') FROM pg_variable WHERE varname = 'var1';
+
+DROP VARIABLE var1;
+
+--- check access rights and supported ALTER
+CREATE SCHEMA svartest;
+GRANT ALL ON SCHEMA svartest TO regress_variable_owner;
+
+CREATE VARIABLE svartest.var1 AS int;
+
+CREATE ROLE regress_variable_reader;
+
+GRANT SELECT ON VARIABLE svartest.var1 TO regress_variable_reader;
+REVOKE ALL ON VARIABLE svartest.var1 FROM regress_variable_reader;
+
+ALTER VARIABLE svartest.var1 OWNER TO regress_variable_owner;
+ALTER VARIABLE svartest.var1 RENAME TO varxx;
+ALTER VARIABLE svartest.varxx SET SCHEMA public;
+
+DROP VARIABLE public.varxx;
+
+ALTER DEFAULT PRIVILEGES
+   FOR ROLE regress_variable_owner
+   IN SCHEMA svartest
+   GRANT SELECT ON VARIABLES TO regress_variable_reader;
+
+-- creating variable with default privileges
+SET ROLE TO regress_variable_owner;
+CREATE VARIABLE svartest.var1 AS int;
+SET ROLE TO DEFAULT;
+
+\dV+ svartest.var1
+
+DROP VARIABLE svartest.var1;
+
+DROP SCHEMA svartest;
+DROP ROLE regress_variable_reader;
+DROP ROLE regress_variable_owner;
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 1847bbfa95c..5b1e772f63d 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -535,6 +535,7 @@ CreateRoleStmt
 CreateSchemaStmt
 CreateSchemaStmtContext
 CreateSeqStmt
+CreateSessionVarStmt
 CreateStatsStmt
 CreateStmt
 CreateStmtContext
@@ -874,6 +875,7 @@ FormData_pg_ts_parser
 FormData_pg_ts_template
 FormData_pg_type
 FormData_pg_user_mapping
+FormData_pg_variable
 FormExtraData_pg_attribute
 Form_pg_aggregate
 Form_pg_am
@@ -933,6 +935,7 @@ Form_pg_ts_parser
 Form_pg_ts_template
 Form_pg_type
 Form_pg_user_mapping
+Form_pg_variable
 FormatNode
 FreeBlockNumberArray
 FreeListData
@@ -3077,6 +3080,7 @@ VarString
 VarStringSortSupport
 Variable
 VariableAssignHook
+VariableInfo
 VariableSetKind
 VariableSetStmt
 VariableShowStmt
-- 
2.47.0



view thread (439+ messages)  latest in thread

reply

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Reply to all the recipients using the --to and --cc options:
  reply via email

  To: [email protected]
  Cc: [email protected], [email protected], [email protected], [email protected], [email protected], [email protected], [email protected]
  Subject: Re: proposal: schema variables
  In-Reply-To: <CAFj8pRBGffSQjtwsoF-8KffOveMAH9P7R7VVDXW2DTBY0_eosA@mail.gmail.com>

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox