public inbox for [email protected]  
help / color / mirror / Atom feed
From: Pavel Stehule <[email protected]>
To: Bruce Momjian <[email protected]>
Cc: Dmitry Dolgov <[email protected]>
Cc: 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]>
Cc: jian he <[email protected]>
Cc: Alvaro Herrera <[email protected]>
Cc: PegoraroF10 <[email protected]>
Subject: Re: proposal: schema variables
Date: Fri, 29 Aug 2025 09:03:30 +0200
Message-ID: <CAFj8pRBx4w3QS0D2W3yc8hWXH7hynsOwO4x+iv4AstmB6Dmkgw@mail.gmail.com> (raw)
In-Reply-To: <CAFj8pRBykx1Oh1JVzY5HfcZPjbGE5PH=DyPN4sM7fBafnNkLkA@mail.gmail.com>
References: <CAFj8pRDJ9cq00VYSHxs6LsoHNWjhYXyWWBtV6UgeWwhs0AHa9A@mail.gmail.com>
	<CAFj8pRBPXTcw_3fpKtgVthV2+9rZGhxitZ40DnAwCrK601TZZg@mail.gmail.com>
	<ndtfl4tsnpkb7m7hwvnmlpsascpgd3a7xvjmjhtxffsbrgygtm@4du6zsmnnwq5>
	<CAFj8pRAu4XvNCGu1751t=2YEqLqTjDA3FavMExm2S0KYQq=DdQ@mail.gmail.com>
	<CAFj8pRAsEoeZv0HEnA8CKgFKDSQ-wYw18Os1vdksWCV7ez2bVw@mail.gmail.com>
	<3chredgnjcmccym2kczawfih226b4ac6co7p6z4jeofevrcosi@mrsxkx2x2c65>
	<CAFj8pRBoWPDTOwn5FmMzc+1qiopw+N04U26nviOdF61fs8A2wQ@mail.gmail.com>
	<stckyvkl4yyzvgjsaawojs3xikke7mmds5bhv7l7qerclywywk@h4v4n43xm6u2>
	<CAFj8pRB_E1GM_YGT-ti4bXka6mhLdAAFeTe+BHgHFYC+qb-76g@mail.gmail.com>
	<CAFj8pRCE=zkECNS9E-eLv9tbyUqCR-txx7eZp+GHF1_LKFUAOg@mail.gmail.com>
	<[email protected]>
	<CAFj8pRCRDhQobRx5FAZgK8drzA_T-9c67U_-wZJ2fmqwEVpFSQ@mail.gmail.com>
	<CAFj8pRB1VKs8DbymheidYXDC2i1ZShV+40J7h2j8GDfqM+yLbQ@mail.gmail.com>
	<CAFj8pRAMaiRSRyye9LdOTh_TgbwmohuXh7cCS9eSLtOUzCJU5Q@mail.gmail.com>
	<CAFj8pRBNj_+Q2CQjpURm5copeY27YPJz=Oa+rpRfa5DHu-j1iQ@mail.gmail.com>
	<CAFj8pRDiKxpZA1+dkGK-YW_H8oYp2hVOuFpOPHEkCBc5O4Z_ug@mail.gmail.com>
	<CAFj8pRAdUF6etyOK3UsAPu-1nuOpv6aHbFJuhtJO73uxC6vObw@mail.gmail.com>
	<CAFj8pRDdhYR66dNVz9WKKHDL64QK+CuqYM3yfiMR_Dp-uOm0bg@mail.gmail.com>
	<CAFj8pRDJdF=Jv6M1_ePV8tnzxBfyYHfHodeifhOUyMt1N-fxRQ@mail.gmail.com>
	<CAFj8pRAwp0ES9BKXbNocG_pYcqP0Cs2GdtGx_-2XsEe3fkCgdg@mail.gmail.com>
	<CAFj8pRAhnp63fV5m7ATr2_nW9_yVbFzwhL4Be8oTY_M0jksrXg@mail.gmail.com>
	<CAFj8pRBykx1Oh1JVzY5HfcZPjbGE5PH=DyPN4sM7fBafnNkLkA@mail.gmail.com>

Hi

rebase after 325fc0ab14d11fc87da594857ffbb6636affe7c0

Regards

Pavel


Attachments:

  [text/x-patch] v20250829-0015-plpgsql-tests.patch (11.3K, 3-v20250829-0015-plpgsql-tests.patch)
  download | inline diff:
From 09b495fefb9517bc3a47d284fe65771fae45bcff Mon Sep 17 00:00:00 2001
From: Laurenz Albe <[email protected]>
Date: Wed, 13 Nov 2024 14:06:06 +0100
Subject: [PATCH 15/15] plpgsql tests

set of plpgsql related tests:

* check session variables and plpgsql variables are not in collision ever
* check correct plpgsql plan cache invalidation when session variable is dropped
* check so the value of session variable is not corrupted, when the variable is
  modified inside nested called functions
---
 src/pl/plpgsql/src/Makefile                   |   3 +-
 .../src/expected/plpgsql_session_variable.out | 198 ++++++++++++++++++
 src/pl/plpgsql/src/meson.build                |   1 +
 .../src/sql/plpgsql_session_variable.sql      | 137 ++++++++++++
 4 files changed, 338 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..1dcec78c234
--- /dev/null
+++ b/src/pl/plpgsql/src/expected/plpgsql_session_variable.out
@@ -0,0 +1,198 @@
+-- check of correct plan cache invalidation
+CREATE VARIABLE plpgsql_sesvar01 AS int;
+CREATE VARIABLE plpgsql_sesvar02 AS int[];
+-- plpgsql variables and session variables are not in collision ever
+CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_01()
+RETURNS void AS $$
+DECLARE plpgsql_sesvar01 int;
+BEGIN
+  plpgsql_sesvar01 := 100;
+  LET plpgsql_sesvar01 = 1000;
+  RAISE NOTICE 'plpgsql var: %, session var: %',
+    plpgsql_sesvar01, VARIABLE(plpgsql_sesvar01);
+END;
+$$ LANGUAGE plpgsql;
+SELECT svartest_plpgsql_func01_01();
+NOTICE:  plpgsql var: 100, session var: 1000
+ svartest_plpgsql_func01_01 
+----------------------------
+ 
+(1 row)
+
+CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_02()
+RETURNS void AS $$
+DECLARE __plpgsql_sesvar01 int;
+BEGIN
+  __plpgsql_sesvar01 := 100;
+  LET __plpgsql_sesvar01 = 1000;
+  RAISE NOTICE 'plpgsql var: %, session var: %',
+    __plpgsql_sesvar01, VARIABLE(__plpgsql_sesvar01);
+END;
+$$ LANGUAGE plpgsql;
+-- should fail
+SELECT svartest_plpgsql_func01_02();
+ERROR:  session variable "__plpgsql_sesvar01" doesn't exist
+LINE 1: LET __plpgsql_sesvar01 = 1000
+            ^
+QUERY:  LET __plpgsql_sesvar01 = 1000
+CONTEXT:  PL/pgSQL function svartest_plpgsql_func01_02() line 5 at SQL statement
+-- should fail
+CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_03()
+RETURNS void AS $$
+BEGIN
+  plpgsql_sesvar01 := 100;
+  LET plpgsql_sesvar01 = 1000;
+  RAISE NOTICE 'plpgsql var: %, session var: %',
+    plpgsql_sesvar01, VARIABLE(plpgsql_sesvar01);
+END;
+$$ LANGUAGE plpgsql;
+ERROR:  "plpgsql_sesvar01" is not a known variable
+LINE 4:   plpgsql_sesvar01 := 100;
+          ^
+DROP FUNCTION svartest_plpgsql_func01_01();
+DROP FUNCTION svartest_plpgsql_func01_02();
+CREATE OR REPLACE FUNCTION svartest_plpgsql_func02()
+RETURNS void AS $$
+DECLARE v int[] DEFAULT '{}';
+BEGIN
+  LET plpgsql_sesvar01 = 1;
+  v[VARIABLE(plpgsql_sesvar01)] = 100;
+  RAISE NOTICE '%', v;
+  LET plpgsql_sesvar02 = v;
+  LET plpgsql_sesvar02[VARIABLE(plpgsql_sesvar01)] = -1;
+  RAISE NOTICE '%', VARIABLE(plpgsql_sesvar02);
+END;
+$$ LANGUAGE plpgsql;
+SELECT svartest_plpgsql_func02();
+NOTICE:  {100}
+NOTICE:  {-1}
+ svartest_plpgsql_func02 
+-------------------------
+ 
+(1 row)
+
+DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02;
+CREATE VARIABLE plpgsql_sesvar01 AS int;
+CREATE VARIABLE plpgsql_sesvar02 AS int[];
+SELECT svartest_plpgsql_func02();
+NOTICE:  {100}
+NOTICE:  {-1}
+ svartest_plpgsql_func02 
+-------------------------
+ 
+(1 row)
+
+DROP FUNCTION svartest_plpgsql_func02();
+DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02;
+-- returns updated value
+CREATE VARIABLE plpgsql_sesvar01 AS int;
+CREATE OR REPLACE FUNCTION svartest_plpgsql_inc(int)
+RETURNS int AS $$
+BEGIN
+  LET plpgsql_sesvar01 = COALESCE(VARIABLE(plpgsql_sesvar01) + $1, $1);
+  RETURN VARIABLE(plpgsql_sesvar01);
+END;
+$$ LANGUAGE plpgsql;
+SELECT svartest_plpgsql_inc(1);
+ svartest_plpgsql_inc 
+----------------------
+                    1
+(1 row)
+
+SELECT svartest_plpgsql_inc(1);
+ svartest_plpgsql_inc 
+----------------------
+                    2
+(1 row)
+
+SELECT svartest_plpgsql_inc(1);
+ svartest_plpgsql_inc 
+----------------------
+                    3
+(1 row)
+
+SELECT svartest_plpgsql_inc(1) FROM generate_series(1,10);
+ svartest_plpgsql_inc 
+----------------------
+                    4
+                    5
+                    6
+                    7
+                    8
+                    9
+                   10
+                   11
+                   12
+                   13
+(10 rows)
+
+CREATE VARIABLE plpgsql_sesvar02 AS numeric;
+LET plpgsql_sesvar02 = 0.0;
+CREATE OR REPLACE FUNCTION svartest_plpgsql_inc(numeric)
+RETURNS int AS $$
+BEGIN
+  LET plpgsql_sesvar02 = COALESCE(VARIABLE(plpgsql_sesvar02) + $1, $1);
+  RETURN VARIABLE(plpgsql_sesvar02);
+END;
+$$ LANGUAGE plpgsql;
+SELECT svartest_plpgsql_inc(1.0);
+ svartest_plpgsql_inc 
+----------------------
+                    1
+(1 row)
+
+SELECT svartest_plpgsql_inc(1.0);
+ svartest_plpgsql_inc 
+----------------------
+                    2
+(1 row)
+
+SELECT svartest_plpgsql_inc(1.0);
+ svartest_plpgsql_inc 
+----------------------
+                    3
+(1 row)
+
+SELECT svartest_plpgsql_inc(1.0) FROM generate_series(1,10);
+ svartest_plpgsql_inc 
+----------------------
+                    4
+                    5
+                    6
+                    7
+                    8
+                    9
+                   10
+                   11
+                   12
+                   13
+(10 rows)
+
+DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02;
+DROP FUNCTION svartest_plpgsql_inc(int);
+DROP FUNCTION svartest_plpgsql_inc(numeric);
+-- the value should not be corrupted
+CREATE VARIABLE plpgsql_sesvar03 text;
+LET plpgsql_sesvar03 = 'abc';
+CREATE FUNCTION svartest_plpgsql_func03()
+RETURNS text AS $$
+BEGIN
+  RETURN svartest_plpgsql_func_nested(VARIABLE(plpgsql_sesvar03));
+END
+$$ LANGUAGE plpgsql;
+CREATE FUNCTION svartest_plpgsql_func_nested(t text)
+RETURNS text AS $$
+BEGIN
+  LET plpgsql_sesvar03 = 'BOOM!';
+  RETURN t;
+END;
+$$ LANGUAGE plpgsql;
+SELECT svartest_plpgsql_func03();
+ svartest_plpgsql_func03 
+-------------------------
+ abc
+(1 row)
+
+DROP FUNCTION svartest_plpgsql_func03();
+DROP FUNCTION svartest_plpgsql_func_nested(text);
+DROP VARIABLE plpgsql_sesvar03;
diff --git a/src/pl/plpgsql/src/meson.build b/src/pl/plpgsql/src/meson.build
index 33c49ac25d9..1d01d1c2629 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..5abf18e3bb3
--- /dev/null
+++ b/src/pl/plpgsql/src/sql/plpgsql_session_variable.sql
@@ -0,0 +1,137 @@
+-- check of correct plan cache invalidation
+CREATE VARIABLE plpgsql_sesvar01 AS int;
+CREATE VARIABLE plpgsql_sesvar02 AS int[];
+
+-- plpgsql variables and session variables are not in collision ever
+CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_01()
+RETURNS void AS $$
+DECLARE plpgsql_sesvar01 int;
+BEGIN
+  plpgsql_sesvar01 := 100;
+  LET plpgsql_sesvar01 = 1000;
+  RAISE NOTICE 'plpgsql var: %, session var: %',
+    plpgsql_sesvar01, VARIABLE(plpgsql_sesvar01);
+END;
+$$ LANGUAGE plpgsql;
+
+SELECT svartest_plpgsql_func01_01();
+
+CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_02()
+RETURNS void AS $$
+DECLARE __plpgsql_sesvar01 int;
+BEGIN
+  __plpgsql_sesvar01 := 100;
+  LET __plpgsql_sesvar01 = 1000;
+  RAISE NOTICE 'plpgsql var: %, session var: %',
+    __plpgsql_sesvar01, VARIABLE(__plpgsql_sesvar01);
+END;
+$$ LANGUAGE plpgsql;
+
+-- should fail
+SELECT svartest_plpgsql_func01_02();
+
+-- should fail
+CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_03()
+RETURNS void AS $$
+BEGIN
+  plpgsql_sesvar01 := 100;
+  LET plpgsql_sesvar01 = 1000;
+  RAISE NOTICE 'plpgsql var: %, session var: %',
+    plpgsql_sesvar01, VARIABLE(plpgsql_sesvar01);
+END;
+$$ LANGUAGE plpgsql;
+
+DROP FUNCTION svartest_plpgsql_func01_01();
+DROP FUNCTION svartest_plpgsql_func01_02();
+
+CREATE OR REPLACE FUNCTION svartest_plpgsql_func02()
+RETURNS void AS $$
+DECLARE v int[] DEFAULT '{}';
+BEGIN
+  LET plpgsql_sesvar01 = 1;
+  v[VARIABLE(plpgsql_sesvar01)] = 100;
+  RAISE NOTICE '%', v;
+  LET plpgsql_sesvar02 = v;
+  LET plpgsql_sesvar02[VARIABLE(plpgsql_sesvar01)] = -1;
+  RAISE NOTICE '%', VARIABLE(plpgsql_sesvar02);
+END;
+$$ LANGUAGE plpgsql;
+
+SELECT svartest_plpgsql_func02();
+
+DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02;
+
+CREATE VARIABLE plpgsql_sesvar01 AS int;
+CREATE VARIABLE plpgsql_sesvar02 AS int[];
+
+SELECT svartest_plpgsql_func02();
+
+DROP FUNCTION svartest_plpgsql_func02();
+
+DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02;
+
+-- returns updated value
+CREATE VARIABLE plpgsql_sesvar01 AS int;
+
+CREATE OR REPLACE FUNCTION svartest_plpgsql_inc(int)
+RETURNS int AS $$
+BEGIN
+  LET plpgsql_sesvar01 = COALESCE(VARIABLE(plpgsql_sesvar01) + $1, $1);
+  RETURN VARIABLE(plpgsql_sesvar01);
+END;
+$$ LANGUAGE plpgsql;
+
+SELECT svartest_plpgsql_inc(1);
+SELECT svartest_plpgsql_inc(1);
+SELECT svartest_plpgsql_inc(1);
+
+SELECT svartest_plpgsql_inc(1) FROM generate_series(1,10);
+
+CREATE VARIABLE plpgsql_sesvar02 AS numeric;
+
+LET plpgsql_sesvar02 = 0.0;
+
+CREATE OR REPLACE FUNCTION svartest_plpgsql_inc(numeric)
+RETURNS int AS $$
+BEGIN
+  LET plpgsql_sesvar02 = COALESCE(VARIABLE(plpgsql_sesvar02) + $1, $1);
+  RETURN VARIABLE(plpgsql_sesvar02);
+END;
+$$ LANGUAGE plpgsql;
+
+SELECT svartest_plpgsql_inc(1.0);
+SELECT svartest_plpgsql_inc(1.0);
+SELECT svartest_plpgsql_inc(1.0);
+
+SELECT svartest_plpgsql_inc(1.0) FROM generate_series(1,10);
+
+DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02;
+
+DROP FUNCTION svartest_plpgsql_inc(int);
+DROP FUNCTION svartest_plpgsql_inc(numeric);
+
+-- the value should not be corrupted
+CREATE VARIABLE plpgsql_sesvar03 text;
+LET plpgsql_sesvar03 = 'abc';
+
+CREATE FUNCTION svartest_plpgsql_func03()
+RETURNS text AS $$
+BEGIN
+  RETURN svartest_plpgsql_func_nested(VARIABLE(plpgsql_sesvar03));
+END
+$$ LANGUAGE plpgsql;
+
+CREATE FUNCTION svartest_plpgsql_func_nested(t text)
+RETURNS text AS $$
+BEGIN
+  LET plpgsql_sesvar03 = 'BOOM!';
+  RETURN t;
+END;
+$$ LANGUAGE plpgsql;
+
+SELECT svartest_plpgsql_func03();
+
+DROP FUNCTION svartest_plpgsql_func03();
+DROP FUNCTION svartest_plpgsql_func_nested(text);
+
+DROP VARIABLE plpgsql_sesvar03;
-- 
2.51.0



  [text/x-patch] v20250829-0014-memory-cleaning-after-DROP-VARIABLE.patch (23.3K, 4-v20250829-0014-memory-cleaning-after-DROP-VARIABLE.patch)
  download | inline diff:
From 1aaa3b7a901a2ef40be3e0bef5e718789290b409 Mon Sep 17 00:00:00 2001
From: "[email protected]" <[email protected]>
Date: Mon, 2 Jun 2025 22:33:25 +0200
Subject: [PATCH 14/15] memory cleaning after DROP VARIABLE

Accepting a sinval message invalidates entries in the "sessionvars" hash table.
These entries are validated before any read or write operations on session variables.
When the entry cannot be validated, it is removed.  Removal will be delayed when
the variable was dropped by the current transaction, which could still be rolled back.
---
 src/backend/catalog/pg_variable.c             |   7 +-
 src/backend/commands/session_variable.c       | 154 ++++++++++++-
 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 ++++
 .../expected/session_variables_ddl.out        | 214 ++++++++++++++++++
 .../regress/sql/session_variables_ddl.sql     | 151 ++++++++++++
 8 files changed, 683 insertions(+), 6 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/catalog/pg_variable.c b/src/backend/catalog/pg_variable.c
index d8ede4fa8c8..c9411443c6d 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 "utils/builtins.h"
 #include "utils/pg_lsn.h"
 #include "utils/syscache.h"
@@ -154,7 +155,8 @@ create_variable(const char *varName,
 }
 
 /*
- * Drop variable by OID
+ * Drop variable by OID, and register the needed session variable
+ * cleanup.
  */
 void
 DropVariableById(Oid varid)
@@ -174,4 +176,7 @@ DropVariableById(Oid varid)
 	ReleaseSysCache(tup);
 
 	table_close(rel, RowExclusiveLock);
+
+	/* do the necessary cleanup in local memory, if needed */
+	SessionVariableDropPostprocess(varid);
 }
diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c
index 10f9b5e2021..0b88c0671a0 100644
--- a/src/backend/commands/session_variable.c
+++ b/src/backend/commands/session_variable.c
@@ -13,8 +13,8 @@
  *-------------------------------------------------------------------------
  */
 #include "postgres.h"
-
 #include "access/htup_details.h"
+#include "access/xact.h"
 #include "catalog/pg_variable.h"
 #include "catalog/namespace.h"
 #include "catalog/pg_type.h"
@@ -76,6 +76,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
+	 * variables dropped by the local backend, in case the operation is rolled
+	 * back.
+	 */
+	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
@@ -92,6 +100,17 @@ static HTAB *sessionvars = NULL;	/* hash table for session variables */
 
 static MemoryContext SVariableMemoryContext = NULL;
 
+/* becomes true when we receive a sinval message */
+static bool needs_validation = false;
+
+/*
+ * The content of dropped session variables is not removed immediately.  We do
+ * that in the next transaction that reads or writes a session variable.
+ * "validated_lxid" stores the transaction that performed said validation, so
+ * that we can avoid repeating the effort.
+ */
+static LocalTransactionId validated_lxid = InvalidLocalTransactionId;
+
 /*
  * Callback function for session variable invalidation.
  */
@@ -124,6 +143,38 @@ 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
+			 * won't automatically remove the local variable storage in
+			 * validate_all_session_variables() when the invalidation message
+			 * from DROP VARIABLE arrives.  After all, the transaction could
+			 * still be rolled back.
+			 */
+			svar->is_valid = false;
+			svar->drop_lxid = MyProc->vxid.lxid;
 		}
 	}
 }
@@ -177,6 +228,67 @@ is_session_variable_valid(SVariable svar)
 	return result;
 }
 
+/*
+ * Check all potentially invalid session variable data in local memory and free
+ * the memory for all invalid ones.  This function is called before any read or
+ * write of a session variable.  Freeing of a variable's memory is postponed if
+ * the variable has been dropped by the current transaction, since that
+ * operation could still be rolled back.
+ *
+ * It is possible that we receive a cache invalidation message while
+ * remove_invalid_session_variables() is executing, so we cannot guarantee that
+ * all entries in "sessionvars" will be set to "is_valid" after the function is
+ * done.  However, we can guarantee that all entries get checked once.
+ */
+static void
+remove_invalid_session_variables(void)
+{
+	HASH_SEQ_STATUS status;
+	SVariable	svar;
+
+	/*
+	 * The validation requires system catalog access, so the session state
+	 * should be "in transaction".
+	 */
+	Assert(IsTransactionState());
+
+	if (!needs_validation || !sessionvars)
+		return;
+
+	/*
+	 * Reset the flag before we start the validation.  It can be set again by
+	 * concurrently incoming sinval messages.
+	 */
+	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)
+			{
+				/* try again in the next transaction */
+				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"
  */
@@ -206,6 +318,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;
 
@@ -329,22 +443,42 @@ get_session_variable(Oid varid)
 	if (!sessionvars)
 		create_sessionvars_hashtables();
 
+	if (validated_lxid == InvalidLocalTransactionId ||
+		validated_lxid != MyProc->vxid.lxid)
+	{
+		/* free the memory from dropped session variables */
+		remove_invalid_session_variables();
+
+		/* don't repeat the above step in the same transaction */
+		validated_lxid = MyProc->vxid.lxid;
+	}
+
 	svar = (SVariable) hash_search(sessionvars, &varid,
 								   HASH_ENTER, &found);
 
 	if (found)
 	{
+		/*
+		 * The session variable could have been dropped by a DROP VARIABLE
+		 * statement in a subtransaction that was later rolled back, which
+		 * means that we may have to work with the data of a 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.
+			 * We have to check the system catalog to see if the variable is
+			 * still valid, even if an invalidation message set it to invalid.
+			 *
+			 * The variable must be validated before it is accessed.  The oid
+			 * should be valid, because the related session variable is
+			 * already locked, and remove_invalid_session_variables() would
+			 * 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);
+				elog(ERROR, "unexpected state of session variable %u", varid);
 		}
 	}
 	else
@@ -405,6 +539,16 @@ SetSessionVariable(Oid varid, Datum value, bool isNull)
 	if (!sessionvars)
 		create_sessionvars_hashtables();
 
+	if (validated_lxid == InvalidLocalTransactionId ||
+		validated_lxid != MyProc->vxid.lxid)
+	{
+		/* free the memory from dropped session variables */
+		remove_invalid_session_variables();
+
+		/* don't repeat the above step in the same 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 ac36dfcc19b..c06e1faf02c 100644
--- a/src/include/commands/session_variable.h
+++ b/src/include/commands/session_variable.h
@@ -21,6 +21,8 @@
 #include "nodes/parsenodes.h"
 #include "tcop/cmdtag.h"
 
+extern void SessionVariableDropPostprocess(Oid varid);
+
 extern void SetSessionVariable(Oid varid, Datum value, bool isNull);
 extern Datum GetSessionVariable(Oid varid, bool *isNull);
 
diff --git a/src/test/isolation/expected/session-variable.out b/src/test/isolation/expected/session-variable.out
new file mode 100644
index 00000000000..afcb06599e9
--- /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 VARIABLE(myvar);
+myvar
+-----
+test 
+(1 row)
+
+step drop: DROP VARIABLE myvar;
+step val: SELECT VARIABLE(myvar);
+ERROR:  session variable "myvar" doesn't exist
+
+starting permutation: let val s1 drop val sr1
+step let: LET myvar = 'test';
+step val: SELECT VARIABLE(myvar);
+myvar
+-----
+test 
+(1 row)
+
+step s1: BEGIN;
+step drop: DROP VARIABLE myvar;
+step val: SELECT VARIABLE(myvar);
+ERROR:  session variable "myvar" doesn't exist
+step sr1: ROLLBACK;
+
+starting permutation: let val dbg drop create dbg val
+step let: LET myvar = 'test';
+step val: SELECT VARIABLE(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 VARIABLE(myvar);
+myvar
+-----
+     
+(1 row)
+
+
+starting permutation: let val s1 dbg drop create dbg val sr1
+step let: LET myvar = 'test';
+step val: SELECT VARIABLE(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
+------+----+-------
+      |    |t      
+(1 row)
+
+step val: SELECT VARIABLE(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 9f1e997d81b..4763279c89c 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -118,3 +118,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..a38b9761fd8
--- /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 VARIABLE(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_ddl.out b/src/test/regress/expected/session_variables_ddl.out
index 9c7595e9a41..59c2cf8bfa6 100644
--- a/src/test/regress/expected/session_variables_ddl.out
+++ b/src/test/regress/expected/session_variables_ddl.out
@@ -161,3 +161,217 @@ DETAIL:  drop cascades to session variable svartest01_ddl.sesvar10
 drop cascades to session variable svartest01_ddl.sesvar11
 DROP SCHEMA svartest02_ddl CASCADE;
 NOTICE:  drop cascades to session variable svartest02_ddl.sesvar10
+CREATE SCHEMA svartest_ddl;
+CREATE VARIABLE svartest_ddl.sesvar60 AS varchar;
+-- dropped variables should be removed from memory before the next usage
+-- of any session variable in the next transaction
+LET svartest_ddl.sesvar60 = 'Hello';
+SELECT count(*) FROM pg_session_variables()
+  WHERE schema = 'svartest_ddl'; -- 1
+ count 
+-------
+     1
+(1 row)
+
+DROP VARIABLE svartest_ddl.sesvar60;
+-- should be zero
+SELECT count(*) FROM pg_session_variables()
+  WHERE schema = 'svartest_ddl'; -- 0
+ count 
+-------
+     0
+(1 row)
+
+-- the content of the value should be preserved when a variable is dropped
+-- by an aborted transaction
+CREATE VARIABLE svartest_ddl.sesvar60 AS varchar;
+LET svartest_ddl.sesvar60 = 'Hello';
+BEGIN;
+  DROP VARIABLE svartest_ddl.sesvar60;
+  -- should fail
+  SELECT VARIABLE(svartest_ddl.sesvar60);
+ERROR:  session variable "svartest_ddl.sesvar60" doesn't exist
+LINE 1: SELECT VARIABLE(svartest_ddl.sesvar60);
+                        ^
+ROLLBACK;
+-- should be ok
+SELECT VARIABLE(svartest_ddl.sesvar60); -- Hello
+ sesvar60 
+----------
+ Hello
+(1 row)
+
+-- another test
+BEGIN;
+  DROP VARIABLE svartest_ddl.sesvar60;
+  -- should be ok
+  CREATE VARIABLE svartest_ddl.sesvar60 AS int;
+  LET svartest_ddl.sesvar60 = 100;
+  SELECT VARIABLE(svartest_ddl.sesvar60); -- 100
+ sesvar60 
+----------
+      100
+(1 row)
+
+ROLLBACK;
+SELECT VARIABLE(svartest_ddl.sesvar60); -- Hello
+ sesvar60 
+----------
+ Hello
+(1 row)
+
+DROP VARIABLE svartest_ddl.sesvar60;
+-- should be zero
+SELECT count(*) FROM pg_session_variables()
+  WHERE schema = 'svartest_ddl'; -- 0
+ count 
+-------
+     0
+(1 row)
+
+BEGIN;
+  CREATE VARIABLE svartest_ddl.sesvar60 AS int;
+  LET svartest_ddl.sesvar60 = 100;
+  SELECT VARIABLE(svartest_ddl.sesvar60);
+ sesvar60 
+----------
+      100
+(1 row)
+
+  SELECT count(*) FROM pg_session_variables()
+    WHERE schema = 'svartest_ddl'; -- 1
+ count 
+-------
+     1
+(1 row)
+
+  DROP VARIABLE svartest_ddl.sesvar60;
+COMMIT;
+SELECT count(*) FROM pg_session_variables()
+  WHERE schema = 'svartest_ddl'; -- 0
+ count 
+-------
+     0
+(1 row)
+
+CREATE VARIABLE svartest_ddl.sesvar61 AS int;
+CREATE VARIABLE svartest_ddl.sesvar62 AS int;
+LET svartest_ddl.sesvar61 = 10;
+LET svartest_ddl.sesvar62 = 0;
+BEGIN;
+  SAVEPOINT s1;
+  DROP VARIABLE svartest_ddl.sesvar61;
+  -- force cleaning by touching another session variable
+  SELECT VARIABLE(svartest_ddl.sesvar62);
+ sesvar62 
+----------
+        0
+(1 row)
+
+  ROLLBACK TO s1;
+  SELECT VARIABLE(svartest_ddl.sesvar61);
+ sesvar61 
+----------
+       10
+(1 row)
+
+  SAVEPOINT s2;
+  DROP VARIABLE svartest_ddl.sesvar61;
+  -- force cleaning by touching another session variable
+  SELECT VARIABLE(svartest_ddl.sesvar62);
+ sesvar62 
+----------
+        0
+(1 row)
+
+  ROLLBACK TO s2;
+COMMIT;
+-- should be ok
+SELECT VARIABLE(svartest_ddl.sesvar61);
+ sesvar61 
+----------
+       10
+(1 row)
+
+BEGIN;
+  SAVEPOINT s1;
+  DROP VARIABLE svartest_ddl.sesvar61;
+  -- force cleaning by touching another session variable
+  SELECT VARIABLE(svartest_ddl.sesvar62);
+ sesvar62 
+----------
+        0
+(1 row)
+
+  ROLLBACK TO s1;
+  SELECT VARIABLE(svartest_ddl.sesvar61);
+ sesvar61 
+----------
+       10
+(1 row)
+
+  SAVEPOINT s2;
+  DROP VARIABLE svartest_ddl.sesvar61;
+  -- force cleaning by touching another session variable
+  SELECT VARIABLE(svartest_ddl.sesvar62);
+ sesvar62 
+----------
+        0
+(1 row)
+
+  ROLLBACK TO s2;
+ROLLBACK;
+-- should be ok
+SELECT VARIABLE(svartest_ddl.sesvar61);
+ sesvar61 
+----------
+       10
+(1 row)
+
+BEGIN;
+  SAVEPOINT s1;
+  DROP VARIABLE svartest_ddl.sesvar61;
+  -- force cleaning by touching another session variable
+  SELECT VARIABLE(svartest_ddl.sesvar62);
+ sesvar62 
+----------
+        0
+(1 row)
+
+  SAVEPOINT s2;
+  -- force cleaning by touching another session variable
+  SELECT VARIABLE(svartest_ddl.sesvar62);
+ sesvar62 
+----------
+        0
+(1 row)
+
+  ROLLBACK TO s1;
+  -- force cleaning by touching another session variable
+  SELECT VARIABLE(svartest_ddl.sesvar62);
+ sesvar62 
+----------
+        0
+(1 row)
+
+COMMIT;
+-- should be ok
+SELECT VARIABLE(svartest_ddl.sesvar61);
+ sesvar61 
+----------
+       10
+(1 row)
+
+-- repeated aborted transaction
+BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK;
+BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK;
+BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK;
+-- should be ok
+SELECT VARIABLE(svartest_ddl.sesvar61);
+ sesvar61 
+----------
+       10
+(1 row)
+
+DROP VARIABLE svartest_ddl.sesvar61, svartest_ddl.sesvar62;
+DROP SCHEMA svartest_ddl;
diff --git a/src/test/regress/sql/session_variables_ddl.sql b/src/test/regress/sql/session_variables_ddl.sql
index f844469ecb1..6b962cf8e5a 100644
--- a/src/test/regress/sql/session_variables_ddl.sql
+++ b/src/test/regress/sql/session_variables_ddl.sql
@@ -148,3 +148,154 @@ ALTER VARIABLE svartest02_ddl.sesvar10 SET SCHEMA svartest01_ddl;
 
 DROP SCHEMA svartest01_ddl CASCADE;
 DROP SCHEMA svartest02_ddl CASCADE;
+
+CREATE SCHEMA svartest_ddl;
+
+CREATE VARIABLE svartest_ddl.sesvar60 AS varchar;
+
+-- dropped variables should be removed from memory before the next usage
+-- of any session variable in the next transaction
+
+LET svartest_ddl.sesvar60 = 'Hello';
+
+SELECT count(*) FROM pg_session_variables()
+  WHERE schema = 'svartest_ddl'; -- 1
+
+DROP VARIABLE svartest_ddl.sesvar60;
+
+-- should be zero
+SELECT count(*) FROM pg_session_variables()
+  WHERE schema = 'svartest_ddl'; -- 0
+
+-- the content of the value should be preserved when a variable is dropped
+-- by an aborted transaction
+CREATE VARIABLE svartest_ddl.sesvar60 AS varchar;
+
+LET svartest_ddl.sesvar60 = 'Hello';
+
+BEGIN;
+  DROP VARIABLE svartest_ddl.sesvar60;
+
+  -- should fail
+  SELECT VARIABLE(svartest_ddl.sesvar60);
+
+ROLLBACK;
+
+-- should be ok
+SELECT VARIABLE(svartest_ddl.sesvar60); -- Hello
+
+-- another test
+BEGIN;
+  DROP VARIABLE svartest_ddl.sesvar60;
+
+  -- should be ok
+  CREATE VARIABLE svartest_ddl.sesvar60 AS int;
+  LET svartest_ddl.sesvar60 = 100;
+  SELECT VARIABLE(svartest_ddl.sesvar60); -- 100
+
+ROLLBACK;
+
+SELECT VARIABLE(svartest_ddl.sesvar60); -- Hello
+
+DROP VARIABLE svartest_ddl.sesvar60;
+
+-- should be zero
+SELECT count(*) FROM pg_session_variables()
+  WHERE schema = 'svartest_ddl'; -- 0
+
+BEGIN;
+  CREATE VARIABLE svartest_ddl.sesvar60 AS int;
+
+  LET svartest_ddl.sesvar60 = 100;
+
+  SELECT VARIABLE(svartest_ddl.sesvar60);
+
+  SELECT count(*) FROM pg_session_variables()
+    WHERE schema = 'svartest_ddl'; -- 1
+
+  DROP VARIABLE svartest_ddl.sesvar60;
+
+COMMIT;
+
+SELECT count(*) FROM pg_session_variables()
+  WHERE schema = 'svartest_ddl'; -- 0
+
+CREATE VARIABLE svartest_ddl.sesvar61 AS int;
+CREATE VARIABLE svartest_ddl.sesvar62 AS int;
+
+LET svartest_ddl.sesvar61 = 10;
+LET svartest_ddl.sesvar62 = 0;
+
+BEGIN;
+  SAVEPOINT s1;
+  DROP VARIABLE svartest_ddl.sesvar61;
+
+  -- force cleaning by touching another session variable
+  SELECT VARIABLE(svartest_ddl.sesvar62);
+  ROLLBACK TO s1;
+
+  SELECT VARIABLE(svartest_ddl.sesvar61);
+
+  SAVEPOINT s2;
+  DROP VARIABLE svartest_ddl.sesvar61;
+
+  -- force cleaning by touching another session variable
+  SELECT VARIABLE(svartest_ddl.sesvar62);
+  ROLLBACK TO s2;
+COMMIT;
+
+-- should be ok
+SELECT VARIABLE(svartest_ddl.sesvar61);
+
+BEGIN;
+  SAVEPOINT s1;
+  DROP VARIABLE svartest_ddl.sesvar61;
+
+  -- force cleaning by touching another session variable
+  SELECT VARIABLE(svartest_ddl.sesvar62);
+  ROLLBACK TO s1;
+
+  SELECT VARIABLE(svartest_ddl.sesvar61);
+
+  SAVEPOINT s2;
+  DROP VARIABLE svartest_ddl.sesvar61;
+
+  -- force cleaning by touching another session variable
+  SELECT VARIABLE(svartest_ddl.sesvar62);
+  ROLLBACK TO s2;
+ROLLBACK;
+
+-- should be ok
+SELECT VARIABLE(svartest_ddl.sesvar61);
+
+BEGIN;
+  SAVEPOINT s1;
+  DROP VARIABLE svartest_ddl.sesvar61;
+
+  -- force cleaning by touching another session variable
+  SELECT VARIABLE(svartest_ddl.sesvar62);
+
+  SAVEPOINT s2;
+
+  -- force cleaning by touching another session variable
+  SELECT VARIABLE(svartest_ddl.sesvar62);
+  ROLLBACK TO s1;
+
+  -- force cleaning by touching another session variable
+  SELECT VARIABLE(svartest_ddl.sesvar62);
+COMMIT;
+
+-- should be ok
+SELECT VARIABLE(svartest_ddl.sesvar61);
+
+-- repeated aborted transaction
+BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK;
+BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK;
+BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK;
+
+-- should be ok
+SELECT VARIABLE(svartest_ddl.sesvar61);
+
+DROP VARIABLE svartest_ddl.sesvar61, svartest_ddl.sesvar62;
+
+DROP SCHEMA svartest_ddl;
-- 
2.51.0



  [text/x-patch] v20250829-0011-LET-command-assign-a-result-of-expression-to-the-ses.patch (52.3K, 5-v20250829-0011-LET-command-assign-a-result-of-expression-to-the-ses.patch)
  download | inline diff:
From 9a64845fc1fadbd9ffee5e7dc23bbe5b383decec Mon Sep 17 00:00:00 2001
From: "[email protected]" <[email protected]>
Date: Mon, 2 Jun 2025 08:29:37 +0200
Subject: [PATCH 11/15] LET command - assign a result of expression to the
 session variable
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

The value is assigned to session variables usually by SET command. Unfortunately
there are two reasons why SET should not be used for this purpose in Postgres.

1. Using a_expr inside generic_set ram rule produces reduce conflicts, so it needs
   total reimplementation of related gram rules.

2. SET is no plan command - so it doesn't support usage of parameters.

3. Excepting implementation issues, there is fact, so if we use SET command
   for assigning values to session variables, then there can be collisions
   between session variables and GUC, and then we need some concepts, how
   these collisions should be solved, or how to protect self against these
   collisions. With the dedicated command, the collisions between GUC and session
   variables are not possible.

The command LET is executed as usual query execution. The result is stored
to the target session variable (resultVariable) by using VariableDestReceiver.

Implementations of EXPLAIN LET and PREPARE LET statements are not supported
now. Postponed to next step due reducing patch size.
---
 doc/src/sgml/ddl.sgml                         |  29 ++
 doc/src/sgml/ref/allfiles.sgml                |   1 +
 doc/src/sgml/ref/alter_variable.sgml          |   1 +
 doc/src/sgml/ref/create_variable.sgml         |   5 +-
 doc/src/sgml/ref/drop_variable.sgml           |   1 +
 doc/src/sgml/ref/let.sgml                     |  96 ++++++
 doc/src/sgml/reference.sgml                   |   1 +
 src/backend/commands/session_variable.c       |  86 ++++++
 src/backend/executor/execMain.c               |  23 +-
 src/backend/nodes/nodeFuncs.c                 |  10 +
 src/backend/optimizer/plan/planner.c          |  24 ++
 src/backend/optimizer/plan/setrefs.c          |  34 ++-
 src/backend/parser/analyze.c                  | 237 +++++++++++++++
 src/backend/parser/gram.y                     |  39 ++-
 src/backend/tcop/utility.c                    |  15 +
 src/backend/utils/cache/plancache.c           |  11 +
 src/bin/psql/tab-complete.in.c                |  12 +-
 src/include/commands/session_variable.h       |   5 +
 src/include/nodes/parsenodes.h                |  15 +
 src/include/nodes/pathnodes.h                 |   9 +
 src/include/nodes/plannodes.h                 |   7 +
 src/include/nodes/primnodes.h                 |   9 +
 src/include/parser/kwlist.h                   |   1 +
 src/include/tcop/cmdtaglist.h                 |   1 +
 .../expected/session_variables_dml.out        | 277 ++++++++++++++++++
 .../regress/sql/session_variables_dml.sql     | 189 ++++++++++++
 src/tools/pgindent/typedefs.list              |   1 +
 27 files changed, 1124 insertions(+), 15 deletions(-)
 create mode 100644 doc/src/sgml/ref/let.sgml

diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml
index 2655fa5e7ce..bdf00b35508 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -5403,10 +5403,39 @@ EXPLAIN SELECT count(*) FROM measurement WHERE logdate &gt;= DATE '2008-01-01';
     session variable identifier, and can be used only for session variable
     identifier. The special syntax for accessing session variables removes
     risk of collisions between variable identifiers and column names.
+   </para>
+
+   <para>
+    The value of a session variable is set with the SQL statement
+    <command>LET</command>.  The value of a session variable can be retrieved
+    with the SQL statement <command>SELECT</command>.
 <programlisting>
+CREATE VARIABLE var1 AS date;
+LET var1 = current_date;
+SELECT VARIABLE(var1);
+</programlisting>
+
+    or
+
+<programlisting>
+CREATE VARIABLE public.current_user_id AS integer;
+GRANT SELECT ON VARIABLE public.current_user_id TO PUBLIC;
+LET current_user_id = (SELECT id FROM users WHERE usename = session_user);
 SELECT VARIABLE(current_user_id);
 </programlisting>
    </para>
+
+   <para>
+    The value of a session variable is local to the current session. Retrieving
+    a variable's value returns a <literal>NULL</literal>, unless its value has
+    been set to something else in the current session using the
+    <command>LET</command> command.  Session variables are not transactional:
+    any changes made to the value of a session variable in a transaction won't
+    be undone if the transaction is rolled back (just like variables in
+    procedural languages).  Session variables themselves are persistent, but
+    their values are neither persistent nor shared (like the content of
+    temporary tables).
+   </para>
   </sect1>
 
  <sect1 id="ddl-others">
diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml
index 2f67de3e21b..cc3bd5ab540 100644
--- a/doc/src/sgml/ref/allfiles.sgml
+++ b/doc/src/sgml/ref/allfiles.sgml
@@ -158,6 +158,7 @@ Complete list of usable sgml source files in this directory.
 <!ENTITY grant              SYSTEM "grant.sgml">
 <!ENTITY importForeignSchema SYSTEM "import_foreign_schema.sgml">
 <!ENTITY insert             SYSTEM "insert.sgml">
+<!ENTITY let                SYSTEM "let.sgml">
 <!ENTITY listen             SYSTEM "listen.sgml">
 <!ENTITY load               SYSTEM "load.sgml">
 <!ENTITY lock               SYSTEM "lock.sgml">
diff --git a/doc/src/sgml/ref/alter_variable.sgml b/doc/src/sgml/ref/alter_variable.sgml
index 96d2586423e..221a699469b 100644
--- a/doc/src/sgml/ref/alter_variable.sgml
+++ b/doc/src/sgml/ref/alter_variable.sgml
@@ -173,6 +173,7 @@ ALTER VARIABLE boo SET SCHEMA private;
   <simplelist type="inline">
    <member><xref linkend="sql-createvariable"/></member>
    <member><xref linkend="sql-dropvariable"/></member>
+   <member><xref linkend="sql-let"/></member>
   </simplelist>
  </refsect1>
 </refentry>
diff --git a/doc/src/sgml/ref/create_variable.sgml b/doc/src/sgml/ref/create_variable.sgml
index 6e988f2e472..43000ce004d 100644
--- a/doc/src/sgml/ref/create_variable.sgml
+++ b/doc/src/sgml/ref/create_variable.sgml
@@ -120,9 +120,11 @@ CREATE VARIABLE [ IF NOT EXISTS ] <replaceable class="parameter">name</replaceab
   <title>Examples</title>
 
   <para>
-   Create an date session variable <literal>var1</literal>:
+   Create a session variable <literal>var1</literal> of data type date:
 <programlisting>
 CREATE VARIABLE var1 AS date;
+LET var1 = current_date;
+SELECT VARIABLE(var1);
 </programlisting>
   </para>
 
@@ -143,6 +145,7 @@ CREATE VARIABLE var1 AS date;
   <simplelist type="inline">
    <member><xref linkend="sql-altervariable"/></member>
    <member><xref linkend="sql-dropvariable"/></member>
+   <member><xref linkend="sql-let"/></member>
   </simplelist>
  </refsect1>
 
diff --git a/doc/src/sgml/ref/drop_variable.sgml b/doc/src/sgml/ref/drop_variable.sgml
index 5bdb3560f0b..67988b5fcd8 100644
--- a/doc/src/sgml/ref/drop_variable.sgml
+++ b/doc/src/sgml/ref/drop_variable.sgml
@@ -111,6 +111,7 @@ DROP VARIABLE var1;
   <simplelist type="inline">
    <member><xref linkend="sql-altervariable"/></member>
    <member><xref linkend="sql-createvariable"/></member>
+   <member><xref linkend="sql-let"/></member>
   </simplelist>
  </refsect1>
 
diff --git a/doc/src/sgml/ref/let.sgml b/doc/src/sgml/ref/let.sgml
new file mode 100644
index 00000000000..00f9bea91fe
--- /dev/null
+++ b/doc/src/sgml/ref/let.sgml
@@ -0,0 +1,96 @@
+<!--
+doc/src/sgml/ref/let.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-let">
+ <indexterm zone="sql-let">
+  <primary>LET</primary>
+ </indexterm>
+
+ <indexterm>
+  <primary>session variable</primary>
+  <secondary>changing</secondary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>LET</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>LET</refname>
+  <refpurpose>change a session variable's value</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+LET <replaceable class="parameter">session_variable</replaceable> = <replaceable class="parameter">sql_expression</replaceable>
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   The <command>LET</command> command assigns a value to the specified session
+   variable.
+  </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+  <variablelist>
+   <varlistentry>
+    <term><replaceable class="parameter">session_variable</replaceable></term>
+    <listitem>
+     <para>
+      The name of the session variable.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">sql_expression</replaceable></term>
+    <listitem>
+     <para>
+      An arbitrary SQL expression.  The result must be of a data type that can
+      be cast to the type of the session variable in an assignment.
+     </para>
+    </listitem>
+   </varlistentry>
+
+  </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+<programlisting>
+CREATE VARIABLE myvar AS integer;
+LET myvar = 10;
+LET myvar = (SELECT sum(val) FROM tab);
+</programlisting>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   The <command>LET</command> is a <productname>PostgreSQL</productname>
+   extension.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-altervariable"/></member>
+   <member><xref linkend="sql-createvariable"/></member>
+   <member><xref linkend="sql-dropvariable"/></member>
+  </simplelist>
+ </refsect1>
+</refentry>
diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml
index 25578f3946c..13e4adc5df3 100644
--- a/doc/src/sgml/reference.sgml
+++ b/doc/src/sgml/reference.sgml
@@ -186,6 +186,7 @@
    &grant;
    &importForeignSchema;
    &insert;
+   &let;
    &listen;
    &load;
    &lock;
diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c
index dbc054795bb..768163e2009 100644
--- a/src/backend/commands/session_variable.c
+++ b/src/backend/commands/session_variable.c
@@ -19,15 +19,22 @@
 #include "catalog/namespace.h"
 #include "catalog/pg_type.h"
 #include "commands/session_variable.h"
+#include "executor/execdesc.h"
+#include "executor/executor.h"
+#include "executor/svariableReceiver.h"
 #include "miscadmin.h"
+#include "nodes/plannodes.h"
 #include "parser/parse_type.h"
+#include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
 #include "storage/proc.h"
+#include "tcop/tcopprot.h"
 #include "utils/builtins.h"
 #include "utils/datum.h"
 #include "utils/inval.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
+#include "utils/snapmgr.h"
 #include "utils/syscache.h"
 
 /*
@@ -514,3 +521,82 @@ CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt)
 
 	return variable;
 }
+
+/*
+ * Assign the result of the evaluated expression to the session variable
+ */
+void
+ExecuteLetStmt(ParseState *pstate,
+			   LetStmt *stmt,
+			   ParamListInfo params,
+			   QueryEnvironment *queryEnv,
+			   QueryCompletion *qc)
+{
+	Query	   *query = castNode(Query, stmt->query);
+	List	   *rewritten;
+	DestReceiver *dest;
+	AclResult	aclresult;
+	PlannedStmt *plan;
+	QueryDesc  *queryDesc;
+	Oid			varid = query->resultVariable;
+
+	Assert(OidIsValid(varid));
+
+	/* do we have permission to write to the session variable? */
+	aclresult = object_aclcheck(VariableRelationId, varid, GetUserId(), ACL_UPDATE);
+	if (aclresult != ACLCHECK_OK)
+		aclcheck_error(aclresult, OBJECT_VARIABLE, get_session_variable_name(varid));
+
+	/* create a dest receiver for LET */
+	dest = CreateVariableDestReceiver(varid);
+
+	/* run the query rewriter */
+	query = copyObject(query);
+
+	rewritten = QueryRewrite(query);
+
+	Assert(list_length(rewritten) == 1);
+
+	query = linitial_node(Query, rewritten);
+	Assert(query->commandType == CMD_SELECT);
+
+	/* plan the query */
+	plan = pg_plan_query(query, pstate->p_sourcetext,
+						 CURSOR_OPT_PARALLEL_OK, params);
+
+	/*
+	 * Use a snapshot with an updated command ID to ensure this query sees the
+	 * results of any previously executed queries.  (This could only matter if
+	 * the planner executed an allegedly-stable function that changed the
+	 * database contents, but let's do it anyway to be parallel to the EXPLAIN
+	 * code path.)
+	 */
+	PushCopiedSnapshot(GetActiveSnapshot());
+	UpdateActiveSnapshotCommandId();
+
+	/* create a QueryDesc, redirecting output to our tuple receiver */
+	queryDesc = CreateQueryDesc(plan, pstate->p_sourcetext,
+								GetActiveSnapshot(), InvalidSnapshot,
+								dest, params, queryEnv, 0);
+
+	/* call ExecutorStart to prepare the plan for execution */
+	ExecutorStart(queryDesc, 0);
+
+	/*
+	 * Run the plan to completion.  The result should be only one row.  To
+	 * check if there are too many result rows, we try to fetch two.
+	 */
+	ExecutorRun(queryDesc, ForwardScanDirection, 2L);
+
+	/* save the rowcount if we're given a QueryCompletion to fill */
+	if (qc)
+		SetQueryCompletion(qc, CMDTAG_LET, queryDesc->estate->es_processed);
+
+	/* and clean up */
+	ExecutorFinish(queryDesc);
+	ExecutorEnd(queryDesc);
+
+	FreeQueryDesc(queryDesc);
+
+	PopActiveSnapshot();
+}
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 453ab94a8df..0f39f576794 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -234,13 +234,24 @@ standard_ExecutorStart(QueryDesc *queryDesc, int eflags)
 		/* fill the array */
 		foreach_oid(varid, queryDesc->plannedstmt->sessionVariables)
 		{
-			AclResult	aclresult;
+			/*
+			 * Permission check should be executed on all explicitly used
+			 * variables in the query. For implicitly used variable (like base
+			 * node of assignment indirect) we cannot do permission check,
+			 * because we need read the value (and user can have only UPDATE
+			 * variable). In this case the permission check is executed in
+			 * write time.
+			 */
+			if (varid != queryDesc->plannedstmt->exclSelectPermCheckVarid)
+			{
+				AclResult	aclresult;
 
-			aclresult = object_aclcheck(VariableRelationId, varid,
-										GetUserId(), ACL_SELECT);
-			if (aclresult != ACLCHECK_OK)
-				aclcheck_error(aclresult, OBJECT_VARIABLE,
-							   get_session_variable_name(varid));
+				aclresult = object_aclcheck(VariableRelationId, varid,
+											GetUserId(), ACL_SELECT);
+				if (aclresult != ACLCHECK_OK)
+					aclcheck_error(aclresult, OBJECT_VARIABLE,
+								   get_session_variable_name(varid));
+			}
 
 			estate->es_session_variables[i].value =
 				GetSessionVariable(varid,
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index cd609c6e479..575365eefcd 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -4378,6 +4378,16 @@ raw_expression_tree_walker_impl(Node *node,
 					return true;
 			}
 			break;
+		case T_LetStmt:
+			{
+				LetStmt    *stmt = (LetStmt *) node;
+
+				if (WALK(stmt->target))
+					return true;
+				if (WALK(stmt->query))
+					return true;
+			}
+			break;
 		case T_PLAssignStmt:
 			{
 				PLAssignStmt *stmt = (PLAssignStmt *) node;
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 401f71d0073..ea16431e22b 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -352,6 +352,20 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
 	glob->rel_notnullatts_hash = NULL;
 	glob->sessionVariables = NIL;
 
+	/*
+	 * The (session) result variable should be stored to global, because it is
+	 * not set in subquery.  When this variable is used other than in base
+	 * node of assignment indirection, we need to check the access rights (and
+	 * then we need to detect this situation). The variable used like base
+	 * node cannot be different than target (result) variable. Because we know
+	 * the result variable before planner invocation, we can simply search of
+	 * usage just this variable, and we don't need to to wait until the end of
+	 * planning when we know basenodeSessionVarid.
+	 */
+	glob->resultVariable = parse->resultVariable;
+	glob->basenodeSessionVarid = InvalidOid;
+	glob->basenodeSessionVarSelectCheck = false;
+
 	/*
 	 * Assess whether it's feasible to use parallel mode for this query. We
 	 * can't do this in a standalone backend, or if the command will try to
@@ -592,6 +606,16 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
 
 	result->sessionVariables = glob->sessionVariables;
 
+	/*
+	 * The session variable used (and only used) like base node for assignemnt
+	 * indirection should be excluded from permission check.
+	 */
+	if (OidIsValid(glob->basenodeSessionVarid) &&
+		(!glob->basenodeSessionVarSelectCheck))
+		result->exclSelectPermCheckVarid = glob->basenodeSessionVarid;
+	else
+		result->exclSelectPermCheckVarid = InvalidOid;
+
 	result->stmt_location = parse->stmt_location;
 	result->stmt_len = parse->stmt_len;
 
diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c
index 9b8be530b83..8ee6e38869e 100644
--- a/src/backend/optimizer/plan/setrefs.c
+++ b/src/backend/optimizer/plan/setrefs.c
@@ -2228,6 +2228,27 @@ fix_param_node(PlannerInfo *root, Param *p)
 			p->paramid = n;
 		}
 
+		/*
+		 * We do SELECT permission check of all variables used by the query
+		 * excluding the variable that is used only as base node of assignment
+		 * indirection. The variable id assigned to this param should be same
+		 * like resultVariable id, and this param should be used only once in
+		 * query. When the variable is referenced by any other param, we
+		 * should to do SELECT permission check for this variable too.
+		 */
+		if (p->parambasenode)
+		{
+			Assert(!OidIsValid(root->glob->basenodeSessionVarid));
+			Assert(root->glob->resultVariable == p->paramvarid);
+
+			root->glob->basenodeSessionVarid = p->paramvarid;
+		}
+		else
+		{
+			if (p->paramvarid == root->glob->resultVariable)
+				root->glob->basenodeSessionVarSelectCheck = true;
+		}
+
 		return (Node *) p;
 	}
 
@@ -3713,7 +3734,7 @@ record_plan_type_dependency(PlannerInfo *root, Oid typid)
 
 /*
  * Record dependency on a session variable.  The variable can be used as a
- * session variable in an expression list.
+ * session variable in an expression list, or as the target of a LET statement.
  */
 static void
 record_plan_variable_dependency(PlannerInfo *root, Oid varid)
@@ -3815,9 +3836,10 @@ extract_query_dependencies_walker(Node *node, PlannerInfo *context)
 			}
 
 			/*
-			 * Ignore other utility statements, except those (such as EXPLAIN)
-			 * that contain a parsed-but-not-planned query.  For those, we
-			 * just need to transfer our attention to the contained query.
+			 * Ignore other utility statements, except those (such as EXPLAIN
+			 * or LET) that contain a parsed-but-not-planned query.  For
+			 * those, we just need to transfer our attention to the contained
+			 * query.
 			 */
 			query = UtilityContainsQuery(query->utilityStmt);
 			if (query == NULL)
@@ -3840,6 +3862,10 @@ extract_query_dependencies_walker(Node *node, PlannerInfo *context)
 					lappend_oid(context->glob->relationOids, rte->relid);
 		}
 
+		/* record dependency on the target variable of a LET command */
+		if (OidIsValid(query->resultVariable))
+			record_plan_variable_dependency(context, query->resultVariable);
+
 		/* And recurse into the query's subexpressions */
 		return query_tree_walker(query, extract_query_dependencies_walker,
 								 context, 0);
diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
index 95bb0620f39..92715bf8b1f 100644
--- a/src/backend/parser/analyze.c
+++ b/src/backend/parser/analyze.c
@@ -52,6 +52,7 @@
 #include "utils/builtins.h"
 #include "utils/guc.h"
 #include "utils/rel.h"
+#include "utils/lsyscache.h"
 #include "utils/syscache.h"
 
 
@@ -83,6 +84,8 @@ static Query *transformCreateTableAsStmt(ParseState *pstate,
 										 CreateTableAsStmt *stmt);
 static Query *transformCallStmt(ParseState *pstate,
 								CallStmt *stmt);
+static Query *transformLetStmt(ParseState *pstate,
+							   LetStmt *stmt);
 static void transformLockingClause(ParseState *pstate, Query *qry,
 								   LockingClause *lc, bool pushedDown);
 #ifdef DEBUG_NODE_TESTS_ENABLED
@@ -330,6 +333,7 @@ transformStmt(ParseState *pstate, Node *parseTree)
 			case T_UpdateStmt:
 			case T_DeleteStmt:
 			case T_MergeStmt:
+			case T_LetStmt:
 				(void) test_raw_expression_coverage(parseTree, NULL);
 				break;
 			default:
@@ -409,6 +413,11 @@ transformStmt(ParseState *pstate, Node *parseTree)
 									   (CallStmt *) parseTree);
 			break;
 
+		case T_LetStmt:
+			result = transformLetStmt(pstate,
+									  (LetStmt *) parseTree);
+			break;
+
 		default:
 
 			/*
@@ -460,6 +469,7 @@ stmt_requires_parse_analysis(RawStmt *parseTree)
 		case T_SelectStmt:
 		case T_ReturnStmt:
 		case T_PLAssignStmt:
+		case T_LetStmt:
 			result = true;
 			break;
 
@@ -3362,6 +3372,233 @@ transformCallStmt(ParseState *pstate, CallStmt *stmt)
 	return result;
 }
 
+/*
+ * transformLetStmt -
+ *	  transform an Let Statement
+ */
+static Query *
+transformLetStmt(ParseState *pstate, LetStmt *stmt)
+{
+	Query	   *query;
+	Query	   *result;
+	List	   *exprList = NIL;
+	List	   *exprListCoer = NIL;
+	ListCell   *lc;
+	ListCell   *indirection_head = NULL;
+	Query	   *selectQuery;
+	Oid			varid;
+	char	   *attrname = NULL;
+	bool		not_unique;
+	bool		is_rowtype;
+	Oid			typid;
+	int32		typmod;
+	Oid			collid;
+	List	   *names = NULL;
+	int			indirection_start;
+	int			i = 0;
+
+	/* there can't be any outer WITH to worry about */
+	Assert(pstate->p_ctenamespace == NIL);
+
+	names = NamesFromList(stmt->target);
+
+	/* locks the variable with an AccessShareLock */
+	varid = IdentifyVariable(names, &attrname, &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_DATATYPE_MISMATCH),
+				 errmsg("cannot assign to field \"%s\" of session variable \"%s.%s\" because its type %s is not a composite type",
+						attrname,
+						get_namespace_name(get_session_variable_namespace(varid)),
+						get_session_variable_name(varid),
+						format_type_be(typid)),
+				 parser_errposition(pstate, stmt->location)));
+
+	pstate->p_expr_kind = EXPR_KIND_UPDATE_TARGET;
+
+	/* we need to postpone conversion of "unknown" to text */
+	pstate->p_resolve_unknowns = false;
+
+	selectQuery = transformStmt(pstate, stmt->query);
+
+	/* the grammar should have produced a SELECT */
+	Assert(IsA(selectQuery, Query) && selectQuery->commandType == CMD_SELECT);
+
+	/*
+	 * Generate an expression list for the LET that selects all the
+	 * non-resjunk columns from the subquery.
+	 */
+	exprList = NIL;
+	foreach_node(TargetEntry, tle, selectQuery->targetList)
+	{
+		if (tle->resjunk)
+			continue;
+
+		exprList = lappend(exprList, tle->expr);
+	}
+
+	/* don't allow multicolumn result */
+	if (list_length(exprList) != 1)
+		ereport(ERROR,
+				(errcode(ERRCODE_SYNTAX_ERROR),
+				 errmsg_plural("assignment expression returned %d column",
+							   "assignment expression returned %d columns",
+							   list_length(exprList),
+							   list_length(exprList)),
+				 parser_errposition(pstate,
+									exprLocation((Node *) exprList))));
+
+	exprListCoer = NIL;
+
+	foreach(lc, exprList)
+	{
+		Expr	   *expr = (Expr *) lfirst(lc);
+		Expr	   *coerced_expr;
+		Oid			exprtypid;
+
+		/* now we can read the type of the expression */
+		exprtypid = exprType((Node *) expr);
+
+		if (indirection_head)
+		{
+			bool		targetIsArray;
+			char	   *targetName;
+			Param	   *param;
+
+			targetName = get_session_variable_name(varid);
+			targetIsArray = OidIsValid(get_element_type(typid));
+
+			pstate->p_hasSessionVariables = true;
+
+			param = makeNode(Param);
+			param->paramkind = PARAM_VARIABLE;
+			param->paramvarid = varid;
+			param->paramtype = typid;
+			param->paramtypmod = typmod;
+
+			/*
+			 * The parameter used as basenode has to have special mark,
+			 * because requires special access when we do SELECT access check.
+			 */
+			param->parambasenode = true;
+
+			coerced_expr = (Expr *)
+				transformAssignmentIndirection(pstate,
+											   (Node *) param,
+											   targetName,
+											   targetIsArray,
+											   typid,
+											   typmod,
+											   InvalidOid,
+											   stmt->target,
+											   indirection_head,
+											   (Node *) expr,
+											   COERCION_ASSIGNMENT,
+											   stmt->location);
+		}
+		else
+			coerced_expr = (Expr *)
+				coerce_to_target_type(pstate,
+									  (Node *) expr,
+									  exprtypid,
+									  typid, typmod,
+									  COERCION_ASSIGNMENT,
+									  COERCE_IMPLICIT_CAST,
+									  stmt->location);
+
+		if (coerced_expr == NULL)
+			ereport(ERROR,
+					(errcode(ERRCODE_DATATYPE_MISMATCH),
+					 errmsg("variable \"%s.%s\" is of type %s, but expression is of type %s",
+							get_namespace_name(get_session_variable_namespace(varid)),
+							get_session_variable_name(varid),
+							format_type_be(typid),
+							format_type_be(exprtypid)),
+					 errhint("You will need to rewrite or cast the expression."),
+					 parser_errposition(pstate, exprLocation((Node *) expr))));
+
+		exprListCoer = lappend(exprListCoer, coerced_expr);
+	}
+
+	/* generate query's target list using the computed list of expressions */
+	query = makeNode(Query);
+	query->commandType = CMD_SELECT;
+
+	foreach(lc, exprListCoer)
+	{
+		Expr	   *expr = (Expr *) lfirst(lc);
+		TargetEntry *tle;
+
+		tle = makeTargetEntry(expr,
+							  i + 1,
+							  FigureColname((Node *) expr),
+							  false);
+		query->targetList = lappend(query->targetList, tle);
+	}
+
+	/* done building the range table and jointree */
+	query->rtable = pstate->p_rtable;
+	query->jointree = makeFromExpr(pstate->p_joinlist, NULL);
+
+	query->hasTargetSRFs = pstate->p_hasTargetSRFs;
+	query->hasSubLinks = pstate->p_hasSubLinks;
+	query->hasSessionVariables = pstate->p_hasSessionVariables;
+
+	/* this is top-level query */
+	query->canSetTag = true;
+
+	/*
+	 * Save target session variable ID. It is used later for acquiring an
+	 * AccessShareLock on target variable, setting plan dependency and finally
+	 * for creating VariableDestReceiver.
+	 */
+	query->resultVariable = varid;
+
+	assign_query_collations(pstate, query);
+
+	/*
+	 * The query is executed as utility command by nested executor call.
+	 * Assigned queryId is required in this case.
+	 */
+	if (IsQueryIdEnabled())
+		JumbleQuery(query);
+
+	stmt->query = (Node *) query;
+
+	/* represent the command as a utility Query */
+	result = makeNode(Query);
+	result->commandType = CMD_UTILITY;
+	result->utilityStmt = (Node *) stmt;
+
+	return result;
+}
+
 /*
  * Produce a string representation of a LockClauseStrength value.
  * This should only be applied to valid values (not LCS_NONE).
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 52a242ccc7f..a80b0358d67 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -296,7 +296,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 		DropTransformStmt
 		DropUserMappingStmt ExplainStmt FetchStmt
 		GrantStmt GrantRoleStmt ImportForeignSchemaStmt IndexStmt InsertStmt
-		ListenStmt LoadStmt LockStmt MergeStmt NotifyStmt ExplainableStmt PreparableStmt
+		LetStmt ListenStmt LoadStmt LockStmt MergeStmt NotifyStmt ExplainableStmt PreparableStmt
 		CreateFunctionStmt AlterFunctionStmt ReindexStmt RemoveAggrStmt
 		RemoveFuncStmt RemoveOperStmt RenameStmt ReturnStmt RevokeStmt RevokeRoleStmt
 		RuleActionStmt RuleActionStmtOrEmpty RuleStmt
@@ -741,7 +741,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	KEEP KEY KEYS
 
 	LABEL LANGUAGE LARGE_P LAST_P LATERAL_P
-	LEADING LEAKPROOF LEAST LEFT LEVEL LIKE LIMIT LISTEN LOAD LOCAL
+	LEADING LEAKPROOF LEAST LEFT LET LEVEL LIKE LIMIT LISTEN LOAD LOCAL
 	LOCALTIME LOCALTIMESTAMP LOCATION LOCK_P LOCKED LOGGED
 
 	MAPPING MATCH MATCHED MATERIALIZED MAXVALUE MERGE MERGE_ACTION METHOD
@@ -1088,6 +1088,7 @@ stmt:
 			| ImportForeignSchemaStmt
 			| IndexStmt
 			| InsertStmt
+			| LetStmt
 			| ListenStmt
 			| RefreshMatViewStmt
 			| LoadStmt
@@ -12915,6 +12916,38 @@ opt_hold: /* EMPTY */						{ $$ = 0; }
 			| WITHOUT HOLD					{ $$ = 0; }
 		;
 
+/*****************************************************************************
+ *
+ *		QUERY:
+ *				LET STATEMENT
+ *
+ *****************************************************************************/
+LetStmt:	LET ColId opt_indirection '=' a_expr
+				{
+					LetStmt	   *n = makeNode(LetStmt);
+					SelectStmt *select;
+					ResTarget  *res;
+
+					n->target = lcons(makeString($2),
+									  check_indirection($3, yyscanner));
+
+					select = makeNode(SelectStmt);
+					res = makeNode(ResTarget);
+
+					/* create target list for implicit query */
+					res->name = NULL;
+					res->indirection = NIL;
+					res->val = (Node *) $5;
+					res->location = @5;
+
+					select->targetList = list_make1(res);
+					n->query = (Node *) select;
+
+					n->location = @2;
+					$$ = (Node *) n;
+				}
+		;
+
 /*****************************************************************************
  *
  *		QUERY:
@@ -17990,6 +18023,7 @@ unreserved_keyword:
 			| LARGE_P
 			| LAST_P
 			| LEAKPROOF
+			| LET
 			| LEVEL
 			| LISTEN
 			| LOAD
@@ -18604,6 +18638,7 @@ bare_label_keyword:
 			| LEAKPROOF
 			| LEAST
 			| LEFT
+			| LET
 			| LEVEL
 			| LIKE
 			| LISTEN
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 97ad9596561..3cd1b0871f0 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -235,6 +235,7 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree)
 
 		case T_CallStmt:
 		case T_DoStmt:
+		case T_LetStmt:
 			{
 				/*
 				 * Commands inside the DO block or the called procedure might
@@ -1057,6 +1058,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,
@@ -2198,6 +2204,10 @@ UtilityContainsQuery(Node *parsetree)
 				return UtilityContainsQuery(qry->utilityStmt);
 			return qry;
 
+		case T_LetStmt:
+			qry = castNode(Query, ((LetStmt *) parsetree)->query);
+			return qry;
+
 		default:
 			return NULL;
 	}
@@ -2396,6 +2406,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:
 			{
@@ -3281,6 +3295,7 @@ GetCommandLogLevel(Node *parsetree)
 			break;
 
 		case T_PLAssignStmt:
+		case T_LetStmt:
 			lev = LOGSTMT_ALL;
 			break;
 
diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c
index 1d20f3382b0..c8e91be1482 100644
--- a/src/backend/utils/cache/plancache.c
+++ b/src/backend/utils/cache/plancache.c
@@ -2053,6 +2053,17 @@ ScanQueryForLocks(Query *parsetree, bool acquire)
 		query_tree_walker(parsetree, ScanQueryWalker, &acquire,
 						  QTW_IGNORE_RC_SUBQUERIES);
 	}
+
+	/* process session variables */
+	if (OidIsValid(parsetree->resultVariable))
+	{
+		if (acquire)
+			LockDatabaseObject(VariableRelationId, parsetree->resultVariable,
+							   0, AccessShareLock);
+		else
+			UnlockDatabaseObject(VariableRelationId, parsetree->resultVariable,
+								 0, AccessShareLock);
+	}
 }
 
 /*
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index f37144e437e..1e7c0392c46 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -1252,8 +1252,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",
@@ -4779,6 +4779,14 @@ match_previous_words(int pattern_id,
 	else if (TailMatches("VALUES") && !TailMatches("DEFAULT", "VALUES"))
 		COMPLETE_WITH("(");
 
+/* LET */
+	/* If prev. word is LET suggest a list of variables */
+	else if (Matches("LET"))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables);
+	/* Complete LET <variable> with "=" */
+	else if (TailMatches("LET", MatchAny))
+		COMPLETE_WITH("=");
+
 /* LOCK */
 	/* Complete LOCK [TABLE] [ONLY] with a list of tables */
 	else if (Matches("LOCK"))
diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h
index 9f5c6e30fbd..2ebe8477789 100644
--- a/src/include/commands/session_variable.h
+++ b/src/include/commands/session_variable.h
@@ -17,11 +17,16 @@
 
 #include "catalog/objectaddress.h"
 #include "parser/parse_node.h"
+#include "nodes/params.h"
 #include "nodes/parsenodes.h"
+#include "tcop/cmdtag.h"
 
 extern void SetSessionVariable(Oid varid, Datum value, bool isNull);
 extern Datum GetSessionVariable(Oid varid, bool *isNull);
 
 extern ObjectAddress CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt);
 
+extern void ExecuteLetStmt(ParseState *pstate, LetStmt *stmt, ParamListInfo params,
+						   QueryEnvironment *queryEnv, QueryCompletion *qc);
+
 #endif
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 10cf52467e3..76fa4a29df2 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -147,6 +147,9 @@ typedef struct Query
 	 */
 	int			resultRelation pg_node_attr(query_jumble_ignore);
 
+	/* target variable of LET statement */
+	Oid			resultVariable;
+
 	/* has aggregates in tlist or havingQual */
 	bool		hasAggs pg_node_attr(query_jumble_ignore);
 	/* has window functions in tlist */
@@ -2168,6 +2171,18 @@ typedef struct MergeStmt
 	WithClause *withClause;		/* WITH clause */
 } MergeStmt;
 
+/* ----------------------
+ *		Let Statement
+ * ----------------------
+ */
+typedef struct LetStmt
+{
+	NodeTag		type;
+	List	   *target;			/* target variable */
+	Node	   *query;			/* source expression */
+	ParseLoc	location;
+} LetStmt;
+
 /* ----------------------
  *		Select Statement
  *
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index d33ad33798d..7ba9685cc0e 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -185,6 +185,15 @@ typedef struct PlannerGlobal
 
 	/* list of used session variables */
 	List	   *sessionVariables;
+
+	/* Oid of session variable used like target of LET command */
+	Oid			resultVariable;
+
+	/* oid of session variable used like base node for assignment indirection */
+	Oid			basenodeSessionVarid;
+
+	/* true, if we do SELECT permission check on basenodeSessionVarid */
+	bool		basenodeSessionVarSelectCheck;
 } PlannerGlobal;
 
 /* macro for fetching the Plan associated with a SubPlan node */
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 35971e3a338..9d484315f70 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -152,6 +152,13 @@ typedef struct PlannedStmt
 	/* OIDs for PARAM_VARIABLE Params */
 	List	   *sessionVariables;
 
+	/*
+	 * The oid of session variable execluded from permission check. This
+	 * session variable is used as base node of assignment indirection (and it
+	 * is used only there).
+	 */
+	int			exclSelectPermCheckVarid;
+
 	/* statement location in source string (copied from Query) */
 	/* start location, or -1 if unknown */
 	ParseLoc	stmt_location;
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index 9d7b9f598f8..a3cfb1e8044 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -404,6 +404,15 @@ typedef struct Param
 	Oid			paramcollid;
 	/* OID of used session variable or InvalidOid if none */
 	Oid			paramvarid pg_node_attr(query_jumble_ignore);
+
+	/*
+	 * true if param is used as base node of assignment indirection (when
+	 * target of LET statement is an array field or an record field). For this
+	 * param we do not check SELECT access right, because this param is used
+	 * just for execution of an modify operation.
+	 */
+	bool		parambasenode;
+
 	/* token location, or -1 if unknown */
 	ParseLoc	location;
 } Param;
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index 0ea0265de7c..8c0affba13b 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -257,6 +257,7 @@ PG_KEYWORD("leading", LEADING, RESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("leakproof", LEAKPROOF, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("least", LEAST, COL_NAME_KEYWORD, BARE_LABEL)
 PG_KEYWORD("left", LEFT, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL)
+PG_KEYWORD("let", LET, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("level", LEVEL, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("like", LIKE, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL)
 PG_KEYWORD("limit", LIMIT, RESERVED_KEYWORD, AS_LABEL)
diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h
index ea86954dded..22082c30008 100644
--- a/src/include/tcop/cmdtaglist.h
+++ b/src/include/tcop/cmdtaglist.h
@@ -186,6 +186,7 @@ PG_CMDTAG(CMDTAG_GRANT, "GRANT", true, false, false)
 PG_CMDTAG(CMDTAG_GRANT_ROLE, "GRANT ROLE", false, false, false)
 PG_CMDTAG(CMDTAG_IMPORT_FOREIGN_SCHEMA, "IMPORT FOREIGN SCHEMA", true, false, false)
 PG_CMDTAG(CMDTAG_INSERT, "INSERT", false, false, true)
+PG_CMDTAG(CMDTAG_LET, "LET", false, false, false)
 PG_CMDTAG(CMDTAG_LISTEN, "LISTEN", false, false, false)
 PG_CMDTAG(CMDTAG_LOAD, "LOAD", false, false, false)
 PG_CMDTAG(CMDTAG_LOCK_TABLE, "LOCK TABLE", false, false, false)
diff --git a/src/test/regress/expected/session_variables_dml.out b/src/test/regress/expected/session_variables_dml.out
index 3e21059acc2..67f78a548dc 100644
--- a/src/test/regress/expected/session_variables_dml.out
+++ b/src/test/regress/expected/session_variables_dml.out
@@ -189,3 +189,280 @@ drop cascades to table svartest_dml.testtab
 DROP ROLE regress_svartest_dml_read_role;
 DROP VARIABLE sesvar40;
 DROP TABLE svartest_dml;
+CREATE VARIABLE sesvar43 AS numeric;
+-- LET stmt is not allowed inside CTE
+WITH x AS (LET sesvar43 = 3.14) SELECT * FROM x;
+ERROR:  syntax error at or near "LET"
+LINE 1: WITH x AS (LET sesvar43 = 3.14) SELECT * FROM x;
+                   ^
+-- LET stmt requires result with exactly one row
+LET sesvar43 = generate_series(1,1);
+-- should fail
+LET sesvar43 = generate_series(1,2);
+ERROR:  expression returned more than one row
+LET sesvar43 = generate_series(1,0);
+ERROR:  expression returned no rows
+CREATE SCHEMA svartest_dml;
+CREATE VARIABLE svartest_dml.sesvar44 AS varchar;
+CREATE TYPE svartest_dml.composite_type AS (a int, b int, c int);
+CREATE VARIABLE svartest_dml.sesvar45 AS svartest_dml.composite_type;
+CREATE OR REPLACE FUNCTION svartest_dml.fx01(numeric)
+RETURNS void AS $$
+LET sesvar43 = $1;
+$$ LANGUAGE sql;
+CREATE OR REPLACE FUNCTION svartest_dml.fx02()
+RETURNS numeric AS $$
+SELECT VARIABLE(sesvar43);
+$$ LANGUAGE sql;
+SELECT svartest_dml.fx01(3.14);
+ fx01 
+------
+ 
+(1 row)
+
+SELECT svartest_dml.fx02(), VARIABLE(sesvar43);
+ fx02 | sesvar43 
+------+----------
+ 3.14 |     3.14
+(1 row)
+
+CREATE OR REPLACE FUNCTION svartest_dml.fx03(s varchar)
+RETURNS varchar AS $$
+BEGIN
+  LET svartest_dml.sesvar44 = s;
+  RETURN VARIABLE(svartest_dml.sesvar44);
+END
+$$ LANGUAGE plpgsql;
+SELECT svartest_dml.fx03('Hello');
+ fx03  
+-------
+ Hello
+(1 row)
+
+CREATE OR REPLACE FUNCTION svartest_dml.fx04(s varchar)
+RETURNS varchar AS $$
+BEGIN
+  LET sesvar44 = s;
+  RETURN VARIABLE(sesvar44);
+END
+$$ LANGUAGE plpgsql
+SET SEARCH_PATH TO 'svartest_dml';
+SELECT svartest_dml.fx04('Hello');
+ fx04  
+-------
+ Hello
+(1 row)
+
+CREATE OR REPLACE FUNCTION svartest_dml.fx05(a int, b int, c int)
+RETURNS svartest_dml.composite_type AS $$
+BEGIN
+  LET svartest_dml.sesvar45 = ROW(a, b, c);
+  RETURN VARIABLE(svartest_dml.sesvar45);
+END;
+$$ LANGUAGE plpgsql;
+SELECT row_to_json(svartest_dml.fx05(10, 20, 30));
+      row_to_json       
+------------------------
+ {"a":10,"b":20,"c":30}
+(1 row)
+
+SELECT VARIABLE(svartest_dml.sesvar45);
+  sesvar45  
+------------
+ (10,20,30)
+(1 row)
+
+SELECT VARIABLE(svartest_dml.sesvar45).*;
+ a  | b  | c  
+----+----+----
+ 10 | 20 | 30
+(1 row)
+
+SELECT VARIABLE(svartest_dml.sesvar45.a);
+ a  
+----
+ 10
+(1 row)
+
+SELECT VARIABLE(svartest_dml.sesvar45).a;
+ a  
+----
+ 10
+(1 row)
+
+ALTER TYPE svartest_dml.composite_type ADD ATTRIBUTE d int;
+-- composite value should be still readable
+SELECT row_to_json(VARIABLE(svartest_dml.sesvar45));
+           row_to_json           
+---------------------------------
+ {"a":10,"b":20,"c":30,"d":null}
+(1 row)
+
+LET svartest_dml.sesvar45 = ROW(100, 200, 300, NULL);
+SELECT row_to_json(VARIABLE(svartest_dml.sesvar45));
+            row_to_json             
+------------------------------------
+ {"a":100,"b":200,"c":300,"d":null}
+(1 row)
+
+-- use variables inside view
+CREATE VIEW svartest_dml.view01 AS SELECT VARIABLE(svartest_dml.sesvar45).*;
+SELECT * FROM svartest_dml.view01;
+  a  |  b  |  c  | d 
+-----+-----+-----+---
+ 100 | 200 | 300 |  
+(1 row)
+
+-- start new connection
+\c
+SELECT * FROM svartest_dml.view01;
+ a | b | c | d 
+---+---+---+---
+   |   |   |  
+(1 row)
+
+LET svartest_dml.sesvar45 = ROW(5, 6, 7, 8);
+SELECT * FROM svartest_dml.view01;
+ a | b | c | d 
+---+---+---+---
+ 5 | 6 | 7 | 8
+(1 row)
+
+-- should fail (dependency)
+DROP VARIABLE svartest_dml.sesvar45;
+ERROR:  cannot drop session variable svartest_dml.sesvar45 because other objects depend on it
+DETAIL:  view svartest_dml.view01 depends on session variable svartest_dml.sesvar45
+HINT:  Use DROP ... CASCADE to drop the dependent objects too.
+DROP VIEW svartest_dml.view01;
+-- test of access variables from generic plans
+CREATE OR REPLACE FUNCTION svartest_dml.fx06()
+RETURNS numeric AS $$
+BEGIN
+  RETURN VARIABLE(sesvar43);
+END;
+$$ LANGUAGE plpgsql;
+SET plan_cache_mode TO force_generic_plan;
+LET sesvar43 = 6.28;
+SELECT svartest_dml.fx06();
+ fx06 
+------
+ 6.28
+(1 row)
+
+LET sesvar43 = VARIABLE(sesvar43) * 2;
+SELECT svartest_dml.fx06();
+ fx06  
+-------
+ 12.56
+(1 row)
+
+-- plan cache invalidation test
+DROP VARIABLE sesvar43;
+-- should fail
+SELECT svartest_dml.fx06();
+ERROR:  session variable "sesvar43" doesn't exist
+LINE 1: VARIABLE(sesvar43)
+                 ^
+QUERY:  VARIABLE(sesvar43)
+CONTEXT:  PL/pgSQL function svartest_dml.fx06() line 3 at RETURN
+CREATE VARIABLE sesvar43 AS numeric;
+LET sesvar43 = 2.72;
+SELECT svartest_dml.fx06();
+ fx06 
+------
+ 2.72
+(1 row)
+
+DROP VARIABLE sesvar43;
+CREATE DOMAIN svartest_dml.int_not_null AS int CHECK(value IS NOT NULL);
+CREATE VARIABLE svartest_dml.sesvar46 AS svartest_dml.int_not_null;
+-- should fail
+LET svartest_dml.sesvar46 = NULL;
+ERROR:  value for domain svartest_dml.int_not_null violates check constraint "int_not_null_check"
+-- should be ok
+LET svartest_dml.sesvar46 = 100;
+LET svartest_dml.sesvar45 = ROW(1,2,3,4);
+LET svartest_dml.sesvar45.a = 100;
+SELECT row_to_json(VARIABLE(svartest_dml.sesvar45));
+         row_to_json         
+-----------------------------
+ {"a":100,"b":2,"c":3,"d":4}
+(1 row)
+
+CREATE ROLE regress_svartest_dml_write_only_role;
+GRANT USAGE ON SCHEMA svartest_dml TO regress_svartest_dml_write_only_role;
+GRANT UPDATE ON VARIABLE svartest_dml.sesvar45 TO regress_svartest_dml_write_only_role;
+SET ROLE TO regress_svartest_dml_write_only_role;
+-- should fail
+SELECT VARIABLE(svartest_dml.sesvar45);
+ERROR:  permission denied for session variable sesvar45
+-- should be ok
+LET svartest_dml.sesvar45.b = 200;
+SET ROLE TO DEFAULT;
+SELECT row_to_json(VARIABLE(svartest_dml.sesvar45));
+          row_to_json          
+-------------------------------
+ {"a":100,"b":200,"c":3,"d":4}
+(1 row)
+
+CREATE VARIABLE svartest_dml.sesvar47 AS int[];
+LET svartest_dml.sesvar47 = ARRAY[1,2,3];
+GRANT UPDATE ON VARIABLE svartest_dml.sesvar47 TO regress_svartest_dml_write_only_role;
+SET ROLE TO regress_svartest_dml_write_only_role;
+-- should fail
+SELECT VARIABLE(svartest_dml.sesvar47);
+ERROR:  permission denied for session variable sesvar47
+-- should be ok
+LET svartest_dml.sesvar47[1] = 200;
+SET ROLE TO DEFAULT;
+SELECT VARIABLE(svartest_dml.sesvar47);
+ sesvar47  
+-----------
+ {200,2,3}
+(1 row)
+
+CREATE VARIABLE svartest_dml.sesvar48 AS int4multirange[];
+LET svartest_dml.sesvar48 = NULL;
+LET svartest_dml.sesvar48 = '{"{[2,8),[11,14)}","{[5,8),[12,14)}"}';
+LET svartest_dml.sesvar48[2] = '{[5,8),[12,100)}';
+SELECT VARIABLE(svartest_dml.sesvar48);
+                sesvar48                
+----------------------------------------
+ {"{[2,8),[11,14)}","{[5,8),[12,100)}"}
+(1 row)
+
+-- test extended query protocol
+CREATE VARIABLE svartest_dml.sesvar49 AS int;
+LET svartest_dml.sesvar49 = $1 \bind 10 \g
+SELECT VARIABLE(svartest_dml.sesvar49);
+ sesvar49 
+----------
+       10
+(1 row)
+
+LET svartest_dml.sesvar49 = $1 \parse letps
+\bind_named letps 100 \g
+SELECT VARIABLE(svartest_dml.sesvar49);
+ sesvar49 
+----------
+      100
+(1 row)
+
+\close_prepared letps
+DROP SCHEMA svartest_dml CASCADE;
+NOTICE:  drop cascades to 14 other objects
+DETAIL:  drop cascades to session variable svartest_dml.sesvar44
+drop cascades to type svartest_dml.composite_type
+drop cascades to session variable svartest_dml.sesvar45
+drop cascades to function svartest_dml.fx01(numeric)
+drop cascades to function svartest_dml.fx02()
+drop cascades to function svartest_dml.fx03(character varying)
+drop cascades to function svartest_dml.fx04(character varying)
+drop cascades to function svartest_dml.fx05(integer,integer,integer)
+drop cascades to function svartest_dml.fx06()
+drop cascades to type svartest_dml.int_not_null
+drop cascades to session variable svartest_dml.sesvar46
+drop cascades to session variable svartest_dml.sesvar47
+drop cascades to session variable svartest_dml.sesvar48
+drop cascades to session variable svartest_dml.sesvar49
+DROP ROLE regress_svartest_dml_write_only_role;
diff --git a/src/test/regress/sql/session_variables_dml.sql b/src/test/regress/sql/session_variables_dml.sql
index b2870dde9e9..2c8d6c3a497 100644
--- a/src/test/regress/sql/session_variables_dml.sql
+++ b/src/test/regress/sql/session_variables_dml.sql
@@ -159,3 +159,192 @@ DROP ROLE regress_svartest_dml_read_role;
 DROP VARIABLE sesvar40;
 
 DROP TABLE svartest_dml;
+
+CREATE VARIABLE sesvar43 AS numeric;
+
+-- LET stmt is not allowed inside CTE
+WITH x AS (LET sesvar43 = 3.14) SELECT * FROM x;
+
+-- LET stmt requires result with exactly one row
+LET sesvar43 = generate_series(1,1);
+
+-- should fail
+LET sesvar43 = generate_series(1,2);
+LET sesvar43 = generate_series(1,0);
+
+CREATE SCHEMA svartest_dml;
+CREATE VARIABLE svartest_dml.sesvar44 AS varchar;
+CREATE TYPE svartest_dml.composite_type AS (a int, b int, c int);
+CREATE VARIABLE svartest_dml.sesvar45 AS svartest_dml.composite_type;
+
+CREATE OR REPLACE FUNCTION svartest_dml.fx01(numeric)
+RETURNS void AS $$
+LET sesvar43 = $1;
+$$ LANGUAGE sql;
+
+CREATE OR REPLACE FUNCTION svartest_dml.fx02()
+RETURNS numeric AS $$
+SELECT VARIABLE(sesvar43);
+$$ LANGUAGE sql;
+
+SELECT svartest_dml.fx01(3.14);
+SELECT svartest_dml.fx02(), VARIABLE(sesvar43);
+
+CREATE OR REPLACE FUNCTION svartest_dml.fx03(s varchar)
+RETURNS varchar AS $$
+BEGIN
+  LET svartest_dml.sesvar44 = s;
+  RETURN VARIABLE(svartest_dml.sesvar44);
+END
+$$ LANGUAGE plpgsql;
+
+SELECT svartest_dml.fx03('Hello');
+
+CREATE OR REPLACE FUNCTION svartest_dml.fx04(s varchar)
+RETURNS varchar AS $$
+BEGIN
+  LET sesvar44 = s;
+  RETURN VARIABLE(sesvar44);
+END
+$$ LANGUAGE plpgsql
+SET SEARCH_PATH TO 'svartest_dml';
+
+SELECT svartest_dml.fx04('Hello');
+
+CREATE OR REPLACE FUNCTION svartest_dml.fx05(a int, b int, c int)
+RETURNS svartest_dml.composite_type AS $$
+BEGIN
+  LET svartest_dml.sesvar45 = ROW(a, b, c);
+  RETURN VARIABLE(svartest_dml.sesvar45);
+END;
+$$ LANGUAGE plpgsql;
+
+SELECT row_to_json(svartest_dml.fx05(10, 20, 30));
+
+SELECT VARIABLE(svartest_dml.sesvar45);
+SELECT VARIABLE(svartest_dml.sesvar45).*;
+SELECT VARIABLE(svartest_dml.sesvar45.a);
+SELECT VARIABLE(svartest_dml.sesvar45).a;
+
+ALTER TYPE svartest_dml.composite_type ADD ATTRIBUTE d int;
+
+-- composite value should be still readable
+SELECT row_to_json(VARIABLE(svartest_dml.sesvar45));
+
+LET svartest_dml.sesvar45 = ROW(100, 200, 300, NULL);
+SELECT row_to_json(VARIABLE(svartest_dml.sesvar45));
+
+-- use variables inside view
+CREATE VIEW svartest_dml.view01 AS SELECT VARIABLE(svartest_dml.sesvar45).*;
+SELECT * FROM svartest_dml.view01;
+
+-- start new connection
+\c
+SELECT * FROM svartest_dml.view01;
+
+LET svartest_dml.sesvar45 = ROW(5, 6, 7, 8);
+
+SELECT * FROM svartest_dml.view01;
+
+-- should fail (dependency)
+DROP VARIABLE svartest_dml.sesvar45;
+
+DROP VIEW svartest_dml.view01;
+
+-- test of access variables from generic plans
+CREATE OR REPLACE FUNCTION svartest_dml.fx06()
+RETURNS numeric AS $$
+BEGIN
+  RETURN VARIABLE(sesvar43);
+END;
+$$ LANGUAGE plpgsql;
+
+SET plan_cache_mode TO force_generic_plan;
+
+LET sesvar43 = 6.28;
+
+SELECT svartest_dml.fx06();
+
+LET sesvar43 = VARIABLE(sesvar43) * 2;
+
+SELECT svartest_dml.fx06();
+
+-- plan cache invalidation test
+DROP VARIABLE sesvar43;
+
+-- should fail
+SELECT svartest_dml.fx06();
+
+CREATE VARIABLE sesvar43 AS numeric;
+
+LET sesvar43 = 2.72;
+
+SELECT svartest_dml.fx06();
+
+DROP VARIABLE sesvar43;
+
+CREATE DOMAIN svartest_dml.int_not_null AS int CHECK(value IS NOT NULL);
+CREATE VARIABLE svartest_dml.sesvar46 AS svartest_dml.int_not_null;
+
+-- should fail
+LET svartest_dml.sesvar46 = NULL;
+-- should be ok
+LET svartest_dml.sesvar46 = 100;
+
+LET svartest_dml.sesvar45 = ROW(1,2,3,4);
+LET svartest_dml.sesvar45.a = 100;
+SELECT row_to_json(VARIABLE(svartest_dml.sesvar45));
+
+CREATE ROLE regress_svartest_dml_write_only_role;
+GRANT USAGE ON SCHEMA svartest_dml TO regress_svartest_dml_write_only_role;
+GRANT UPDATE ON VARIABLE svartest_dml.sesvar45 TO regress_svartest_dml_write_only_role;
+
+SET ROLE TO regress_svartest_dml_write_only_role;
+
+-- should fail
+SELECT VARIABLE(svartest_dml.sesvar45);
+
+-- should be ok
+LET svartest_dml.sesvar45.b = 200;
+
+SET ROLE TO DEFAULT;
+
+SELECT row_to_json(VARIABLE(svartest_dml.sesvar45));
+
+CREATE VARIABLE svartest_dml.sesvar47 AS int[];
+LET svartest_dml.sesvar47 = ARRAY[1,2,3];
+
+GRANT UPDATE ON VARIABLE svartest_dml.sesvar47 TO regress_svartest_dml_write_only_role;
+
+SET ROLE TO regress_svartest_dml_write_only_role;
+
+-- should fail
+SELECT VARIABLE(svartest_dml.sesvar47);
+
+-- should be ok
+LET svartest_dml.sesvar47[1] = 200;
+
+SET ROLE TO DEFAULT;
+
+SELECT VARIABLE(svartest_dml.sesvar47);
+
+CREATE VARIABLE svartest_dml.sesvar48 AS int4multirange[];
+LET svartest_dml.sesvar48 = NULL;
+LET svartest_dml.sesvar48 = '{"{[2,8),[11,14)}","{[5,8),[12,14)}"}';
+LET svartest_dml.sesvar48[2] = '{[5,8),[12,100)}';
+SELECT VARIABLE(svartest_dml.sesvar48);
+
+-- test extended query protocol
+CREATE VARIABLE svartest_dml.sesvar49 AS int;
+
+LET svartest_dml.sesvar49 = $1 \bind 10 \g
+SELECT VARIABLE(svartest_dml.sesvar49);
+
+LET svartest_dml.sesvar49 = $1 \parse letps
+\bind_named letps 100 \g
+SELECT VARIABLE(svartest_dml.sesvar49);
+
+\close_prepared letps
+
+DROP SCHEMA svartest_dml CASCADE;
+DROP ROLE regress_svartest_dml_write_only_role;
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 3ae88d9241d..b6bbf4a8e71 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1546,6 +1546,7 @@ LargeObjectDesc
 Latch
 LauncherLastStartTimesEntry
 LerpFunc
+LetStmt
 LexDescr
 LexemeEntry
 LexemeHashKey
-- 
2.51.0



  [text/x-patch] v20250829-0012-function-pg_session_variables-for-cleaning-tests.patch (4.8K, 6-v20250829-0012-function-pg_session_variables-for-cleaning-tests.patch)
  download | inline diff:
From fafce80b2b884651348c28242b4f8ffd854d5e30 Mon Sep 17 00:00:00 2001
From: "[email protected]" <[email protected]>
Date: Fri, 19 Jan 2024 20:01:56 +0100
Subject: [PATCH 12/15] 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 | 100 ++++++++++++++++++++++++
 src/include/catalog/pg_proc.dat         |   8 ++
 2 files changed, 108 insertions(+)

diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c
index 768163e2009..9a9281acd83 100644
--- a/src/backend/commands/session_variable.c
+++ b/src/backend/commands/session_variable.c
@@ -22,6 +22,7 @@
 #include "executor/execdesc.h"
 #include "executor/executor.h"
 #include "executor/svariableReceiver.h"
+#include "funcapi.h"
 #include "miscadmin.h"
 #include "nodes/plannodes.h"
 #include "parser/parse_type.h"
@@ -600,3 +601,102 @@ 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
+
+	/*
+	 * Make sure syscache entries are flushed for recent catalog changes. For
+	 * stable behavior we need to reliably detect which variables were
+	 * dropped.
+	 */
+	AcceptInvalidationMessages();
+
+	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 a71b9059822..d0399ec13fa 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12599,4 +12599,12 @@
   proargnames => '{pid,io_id,io_generation,state,operation,off,length,target,handle_data_len,raw_result,result,target_desc,f_sync,f_localmem,f_buffered}',
   prosrc => 'pg_get_aios' },
 
+# 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.51.0



  [text/x-patch] v20250829-0013-DISCARD-VARIABLES.patch (10.1K, 7-v20250829-0013-DISCARD-VARIABLES.patch)
  download | inline diff:
From ccb663d2da82b3c48be2d7403015d38657680577 Mon Sep 17 00:00:00 2001
From: "[email protected]" <[email protected]>
Date: Mon, 2 Jun 2025 20:41:57 +0200
Subject: [PATCH 13/15] 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       | 32 ++++++++++--
 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 +
 .../expected/session_variables_dml.out        | 50 +++++++++++++++++++
 .../regress/sql/session_variables_dml.sql     | 24 +++++++++
 11 files changed, 135 insertions(+), 5 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 81339a75a52..5904a6c4917 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 9a9281acd83..10f9b5e2021 100644
--- a/src/backend/commands/session_variable.c
+++ b/src/backend/commands/session_variable.c
@@ -103,7 +103,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
@@ -657,8 +663,8 @@ pg_session_variables(PG_FUNCTION_ARGS)
 
 				/*
 				 * 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.
+				 * 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)
 				{
@@ -700,3 +706,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 a80b0358d67..3cc2ea978e1 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -2119,7 +2119,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 3cd1b0871f0..7a0d6ea1d00 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -2953,6 +2953,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 1e7c0392c46..4ec384f3a88 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -4206,7 +4206,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 2ebe8477789..ac36dfcc19b 100644
--- a/src/include/commands/session_variable.h
+++ b/src/include/commands/session_variable.h
@@ -29,4 +29,6 @@ extern ObjectAddress CreateVariable(ParseState *pstate, CreateSessionVarStmt *st
 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 76fa4a29df2..c9aa0654104 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -4104,6 +4104,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 22082c30008..bef0ac25331 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_dml.out b/src/test/regress/expected/session_variables_dml.out
index 67f78a548dc..2bf7bd27467 100644
--- a/src/test/regress/expected/session_variables_dml.out
+++ b/src/test/regress/expected/session_variables_dml.out
@@ -466,3 +466,53 @@ drop cascades to session variable svartest_dml.sesvar47
 drop cascades to session variable svartest_dml.sesvar48
 drop cascades to session variable svartest_dml.sesvar49
 DROP ROLE regress_svartest_dml_write_only_role;
+CREATE SCHEMA svartest_dml_discard;
+CREATE VARIABLE svartest_dml_discard.sesvar50 AS varchar;
+LET svartest_dml_discard.sesvar50 = 'Hello';
+SELECT VARIABLE(svartest_dml_discard.sesvar50);
+ sesvar50 
+----------
+ Hello
+(1 row)
+
+SELECT count(*) FROM pg_session_variables() WHERE schema = 'svartest_dml_discard';
+ count 
+-------
+     1
+(1 row)
+
+DISCARD ALL;
+SELECT count(*) FROM pg_session_variables();
+ count 
+-------
+     0
+(1 row)
+
+SELECT VARIABLE(svartest_dml_discard.sesvar50);
+ sesvar50 
+----------
+ 
+(1 row)
+
+LET svartest_dml_discard.sesvar50 = 'Hello';
+SELECT count(*) FROM pg_session_variables();
+ count 
+-------
+     1
+(1 row)
+
+DISCARD VARIABLES;
+SELECT count(*) FROM pg_session_variables();
+ count 
+-------
+     0
+(1 row)
+
+SELECT VARIABLE(svartest_dml_discard.sesvar50);
+ sesvar50 
+----------
+ 
+(1 row)
+
+DROP SCHEMA svartest_dml_discard CASCADE;
+NOTICE:  drop cascades to session variable svartest_dml_discard.sesvar50
diff --git a/src/test/regress/sql/session_variables_dml.sql b/src/test/regress/sql/session_variables_dml.sql
index 2c8d6c3a497..5a721750446 100644
--- a/src/test/regress/sql/session_variables_dml.sql
+++ b/src/test/regress/sql/session_variables_dml.sql
@@ -348,3 +348,27 @@ SELECT VARIABLE(svartest_dml.sesvar49);
 
 DROP SCHEMA svartest_dml CASCADE;
 DROP ROLE regress_svartest_dml_write_only_role;
+
+CREATE SCHEMA svartest_dml_discard;
+
+CREATE VARIABLE svartest_dml_discard.sesvar50 AS varchar;
+LET svartest_dml_discard.sesvar50 = 'Hello';
+SELECT VARIABLE(svartest_dml_discard.sesvar50);
+
+SELECT count(*) FROM pg_session_variables() WHERE schema = 'svartest_dml_discard';
+
+DISCARD ALL;
+
+SELECT count(*) FROM pg_session_variables();
+
+SELECT VARIABLE(svartest_dml_discard.sesvar50);
+LET svartest_dml_discard.sesvar50 = 'Hello';
+
+SELECT count(*) FROM pg_session_variables();
+
+DISCARD VARIABLES;
+
+SELECT count(*) FROM pg_session_variables();
+
+SELECT VARIABLE(svartest_dml_discard.sesvar50);
+DROP SCHEMA svartest_dml_discard CASCADE;
-- 
2.51.0



  [text/x-patch] v20250829-0010-svariableReceiver.patch (8.9K, 8-v20250829-0010-svariableReceiver.patch)
  download | inline diff:
From cfa5ab6c55cacc9c33ba0e63506003505c42e71b Mon Sep 17 00:00:00 2001
From: "[email protected]" <[email protected]>
Date: Sun, 1 Jun 2025 21:20:16 +0200
Subject: [PATCH 10/15] svariableReceiver

allows to store result of the query to session variable

Check correct format of result - one column, one row.
---
 src/backend/executor/Makefile            |   1 +
 src/backend/executor/meson.build         |   1 +
 src/backend/executor/svariableReceiver.c | 172 +++++++++++++++++++++++
 src/backend/tcop/dest.c                  |   7 +
 src/include/executor/svariableReceiver.h |  22 +++
 src/include/tcop/dest.h                  |   1 +
 src/tools/pgindent/typedefs.list         |   1 +
 7 files changed, 205 insertions(+)
 create mode 100644 src/backend/executor/svariableReceiver.c
 create mode 100644 src/include/executor/svariableReceiver.h

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/meson.build b/src/backend/executor/meson.build
index 2cea41f8771..491092fcc4c 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..41a967cb4b2
--- /dev/null
+++ b/src/backend/executor/svariableReceiver.c
@@ -0,0 +1,172 @@
+/*-------------------------------------------------------------------------
+ *
+ * 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.  A received tuple cannot to have
+ * deleted attributes.  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			rows;			/* row counter */
+} SVariableState;
+
+/*
+ * Prepare to receive tuples from executor.
+ */
+static void
+svariableStartupReceiver(DestReceiver *self, int operation, TupleDesc typeinfo)
+{
+	SVariableState *myState = (SVariableState *) self;
+	LOCKTAG		locktag PG_USED_FOR_ASSERTS_ONLY;
+	Form_pg_attribute attr;
+	Oid			typid PG_USED_FOR_ASSERTS_ONLY;
+	Oid			collid PG_USED_FOR_ASSERTS_ONLY;
+	int32		typmod PG_USED_FOR_ASSERTS_ONLY;
+
+	Assert(myState->pub.mydest == DestVariable);
+	Assert(OidIsValid(myState->varid));
+	Assert(SearchSysCacheExists1(VARIABLEOID, myState->varid));
+	Assert(typeinfo->natts == 1);
+
+#ifdef USE_ASSERT_CHECKING
+
+	SET_LOCKTAG_OBJECT(locktag,
+					   MyDatabaseId,
+					   VariableRelationId,
+					   myState->varid,
+					   0);
+
+	Assert(LockHeldByMe(&locktag, AccessShareLock, false));
+
+#endif
+
+	attr = TupleDescAttr(typeinfo, 0);
+
+	Assert(!attr->attisdropped);
+
+#ifdef USE_ASSERT_CHECKING
+
+	get_session_variable_type_typmod_collid(myState->varid,
+											&typid,
+											&typmod,
+											&collid);
+
+	Assert(attr->atttypid == typid);
+	Assert(attr->atttypmod < 0 || attr->atttypmod == typmod);
+
+#endif
+
+	myState->need_detoast = attr->attlen == -1;
+	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[0];
+	isnull = slot->tts_isnull[0];
+
+	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/tcop/dest.c b/src/backend/tcop/dest.c
index b620766c938..b2f764b657f 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/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/tcop/dest.h b/src/include/tcop/dest.h
index 00c092e3d7c..6ce3ea0e617 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/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index f32dcc29023..3ae88d9241d 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2656,6 +2656,7 @@ STRLEN
 SV
 SVariableData
 SVariable
+SVariableState
 SYNCHRONIZATION_BARRIER
 SYSTEM_INFO
 SampleScan
-- 
2.51.0



  [text/x-patch] v20250829-0008-collect-session-variables-used-in-plan-and-assign-pa.patch (16.5K, 9-v20250829-0008-collect-session-variables-used-in-plan-and-assign-pa.patch)
  download | inline diff:
From c4d299d8dad07fb1a73517a74408e152504e21cd Mon Sep 17 00:00:00 2001
From: "[email protected]" <[email protected]>
Date: Fri, 30 May 2025 09:31:06 +0200
Subject: [PATCH 08/15] collect session variables used in plan and assign
 paramid

In the plan stage we need to collect used session variables. On the
order of this list, the param nodes gets paramid (fix_param_node).
This number is used (later) as index to buffer of values of the
used session variables. The buffer is prepared and filled by executor.

Some unsupported optimizations are disabled:

* parallel execution
* simple expression execution in PL/pgSQL
* SQL functions inlining

Before execution of query with session variables we need to collect
used session variables. This list is used for
---
 doc/src/sgml/parallel.sgml                |   6 ++
 src/backend/catalog/dependency.c          |   5 +
 src/backend/optimizer/plan/planner.c      |  11 ++
 src/backend/optimizer/plan/setrefs.c      | 124 +++++++++++++++++++++-
 src/backend/optimizer/prep/prepjointree.c |   3 +
 src/backend/optimizer/util/clauses.c      |  33 +++++-
 src/backend/utils/cache/plancache.c       |   6 +-
 src/backend/utils/fmgr/fmgr.c             |  10 +-
 src/include/nodes/pathnodes.h             |   5 +
 src/include/nodes/plannodes.h             |   3 +
 src/include/optimizer/planmain.h          |   2 +
 11 files changed, 199 insertions(+), 9 deletions(-)

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/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c
index 1d62e63d4f7..eb91b46b128 100644
--- a/src/backend/catalog/dependency.c
+++ b/src/backend/catalog/dependency.c
@@ -1875,6 +1875,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/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 41bd8353430..401f71d0073 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -350,6 +350,7 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
 	glob->dependsOnRole = false;
 	glob->partition_directory = NULL;
 	glob->rel_notnullatts_hash = NULL;
+	glob->sessionVariables = NIL;
 
 	/*
 	 * Assess whether it's feasible to use parallel mode for this query. We
@@ -588,6 +589,9 @@ 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;
 
@@ -770,6 +774,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 d706546f332..9b8be530b83 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);
 
 
 /*****************************************************************************
@@ -1320,6 +1323,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
@@ -2020,8 +2067,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.
@@ -2111,6 +2159,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);
+	}
 }
 
 /*
@@ -2120,6 +2175,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)
@@ -2138,6 +2197,40 @@ 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)
+	{
+		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_oid(varid, root->glob->sessionVariables)
+		{
+			if (varid == 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);
 }
 
@@ -2199,7 +2292,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
@@ -2221,7 +2317,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);
 	}
@@ -3614,6 +3711,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.
+ */
+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
diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c
index 35e8d3c183b..d2470c9b2db 100644
--- a/src/backend/optimizer/prep/prepjointree.c
+++ b/src/backend/optimizer/prep/prepjointree.c
@@ -1645,6 +1645,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 6f0b338d2cd..6df76d87b72 100644
--- a/src/backend/optimizer/util/clauses.c
+++ b/src/backend/optimizer/util/clauses.c
@@ -25,6 +25,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"
@@ -939,6 +940,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))
 		{
@@ -2395,6 +2403,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 *
@@ -2521,6 +2530,27 @@ eval_const_expressions_mutator(Node *node,
 						}
 					}
 				}
+				else if (param->paramkind == PARAM_VARIABLE &&
+						 context->estimate)
+				{
+					int16		typLen;
+					bool		typByVal;
+					Datum		pval;
+					bool		isnull;
+
+					get_typlenbyval(param->paramtype,
+									&typLen, &typByVal);
+
+					pval = GetSessionVariable(param->paramvarid, &isnull);
+
+					return (Node *) makeConst(param->paramtype,
+											  param->paramtypmod,
+											  param->paramcollid,
+											  (int) typLen,
+											  pval,
+											  isnull,
+											  typByVal);
+				}
 
 				/*
 				 * Not replaceable, so just copy the Param (no need to
@@ -4819,7 +4849,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/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c
index 6661d2c6b73..ab1f2af13e5 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"
@@ -153,6 +154,7 @@ InitPlanCache(void)
 	CacheRegisterSyscacheCallback(AMOPOPID, PlanCacheSysCallback, (Datum) 0);
 	CacheRegisterSyscacheCallback(FOREIGNSERVEROID, PlanCacheSysCallback, (Datum) 0);
 	CacheRegisterSyscacheCallback(FOREIGNDATAWRAPPEROID, PlanCacheSysCallback, (Datum) 0);
+	CacheRegisterSyscacheCallback(VARIABLEOID, PlanCacheObjectCallback, (Datum) 0);
 }
 
 /*
@@ -2196,7 +2198,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 5543440a33e..926a7b3a096 100644
--- a/src/backend/utils/fmgr/fmgr.c
+++ b/src/backend/utils/fmgr/fmgr.c
@@ -1991,9 +1991,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/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index 4a903d1ec18..d33ad33798d 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -182,6 +182,9 @@ typedef struct PlannerGlobal
 
 	/* hash table for NOT NULL attnums of relations */
 	struct HTAB *rel_notnullatts_hash pg_node_attr(read_write_ignore);
+
+	/* list of used session variables */
+	List	   *sessionVariables;
 } PlannerGlobal;
 
 /* macro for fetching the Plan associated with a SubPlan node */
@@ -532,6 +535,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 29d7732d6a0..35971e3a338 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -149,6 +149,9 @@ typedef struct PlannedStmt
 	/* non-null if this is utility stmt */
 	Node	   *utilityStmt;
 
+	/* OIDs for PARAM_VARIABLE Params */
+	List	   *sessionVariables;
+
 	/* statement location in source string (copied from Query) */
 	/* start location, or -1 if unknown */
 	ParseLoc	stmt_location;
diff --git a/src/include/optimizer/planmain.h b/src/include/optimizer/planmain.h
index 9d3debcab28..ba4305d61a7 100644
--- a/src/include/optimizer/planmain.h
+++ b/src/include/optimizer/planmain.h
@@ -131,4 +131,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 */
-- 
2.51.0



  [text/x-patch] v20250829-0009-fill-an-auxiliary-buffer-with-values-of-session-vari.patch (19.5K, 10-v20250829-0009-fill-an-auxiliary-buffer-with-values-of-session-vari.patch)
  download | inline diff:
From f33b94c077462d7cbd5f768275bc069a8b6f6772 Mon Sep 17 00:00:00 2001
From: "[email protected]" <[email protected]>
Date: Sun, 1 Jun 2025 07:32:01 +0200
Subject: [PATCH 09/15] fill an auxiliary buffer with values of session
 variables used in query

and locks variables used in query. Now we can read the content of any
session variable. Direct reading from expression executor is not allowed,
so we cannot to use session variables inside CALL or EXECUTE commands
(can be supported with direct access to session variables (from expression
executor) - postponed).

Using session variables blocks parallel query execution. It is not
technical problem (it just needs a serialization/deserialization of
es_session_varibles buffer), but it increases a size of patch (and then
it is postponed).
---
 src/backend/executor/execExpr.c               |  29 +++
 src/backend/executor/execMain.c               |  56 +++++
 src/backend/utils/cache/plancache.c           |  24 ++-
 src/include/nodes/execnodes.h                 |  14 ++
 .../expected/session_variables_dml.out        | 191 ++++++++++++++++++
 src/test/regress/parallel_schedule            |   7 +-
 .../regress/sql/session_variables_dml.sql     | 161 +++++++++++++++
 src/tools/pgindent/typedefs.list              |   1 +
 8 files changed, 479 insertions(+), 4 deletions(-)
 create mode 100644 src/test/regress/expected/session_variables_dml.out
 create mode 100644 src/test/regress/sql/session_variables_dml.sql

diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c
index f1569879b52..0457a729537 100644
--- a/src/backend/executor/execExpr.c
+++ b/src/backend/executor/execExpr.c
@@ -1069,6 +1069,35 @@ ExecInitExprRec(Expr *node, ExprState *state,
 							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];
+
+							/*
+							 * 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;
 					default:
 						elog(ERROR, "unrecognized paramkind: %d",
 							 (int) param->paramkind);
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index b8b9d2a85f7..453ab94a8df 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/execPartition.h"
@@ -196,6 +198,60 @@ 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)
+	{
+		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_oid(varid, queryDesc->plannedstmt->sessionVariables)
+		{
+			AclResult	aclresult;
+
+			aclresult = object_aclcheck(VariableRelationId, varid,
+										GetUserId(), ACL_SELECT);
+			if (aclresult != ACLCHECK_OK)
+				aclcheck_error(aclresult, OBJECT_VARIABLE,
+							   get_session_variable_name(varid));
+
+			estate->es_session_variables[i].value =
+				GetSessionVariable(varid,
+								   &estate->es_session_variables[i].isnull);
+
+			i++;
+		}
+
+		estate->es_num_session_variables = nSessionVariables;
+	}
+
 	/*
 	 * Fill in the query environment, if any, from queryDesc.
 	 */
diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c
index ab1f2af13e5..1d20f3382b0 100644
--- a/src/backend/utils/cache/plancache.c
+++ b/src/backend/utils/cache/plancache.c
@@ -2043,9 +2043,12 @@ 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, &acquire,
 						  QTW_IGNORE_RC_SUBQUERIES);
@@ -2053,7 +2056,8 @@ ScanQueryForLocks(Query *parsetree, bool acquire)
 }
 
 /*
- * 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)
@@ -2068,6 +2072,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
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index de782014b2d..67cd0a26764 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -647,6 +647,16 @@ typedef struct AsyncRequest
 								 * tuples) */
 } AsyncRequest;
 
+/* ----------------
+ * SessionVariableValue
+ * ----------------
+ */
+typedef struct SessionVariableValue
+{
+	bool		isnull;
+	Datum		value;
+} SessionVariableValue;
+
 /* ----------------
  *	  EState information
  *
@@ -706,6 +716,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/test/regress/expected/session_variables_dml.out b/src/test/regress/expected/session_variables_dml.out
new file mode 100644
index 00000000000..3e21059acc2
--- /dev/null
+++ b/src/test/regress/expected/session_variables_dml.out
@@ -0,0 +1,191 @@
+CREATE VARIABLE sesvar40 AS int;
+-- should not be accessible without variable's fence
+-- should fail
+SELECT sesvar40;
+ERROR:  column "sesvar40" does not exist
+LINE 1: SELECT sesvar40;
+               ^
+-- should be ok
+SELECT VARIABLE(sesvar40);
+ sesvar40 
+----------
+         
+(1 row)
+
+CREATE SCHEMA svartest_dml;
+CREATE VARIABLE svartest_dml.sesvar41 AS int;
+-- identifier collision test
+CREATE TABLE svartest_dml(sesvar41 int);
+INSERT INTO svartest_dml VALUES(100);
+SELECT sesvar41 FROM svartest_dml; -- 100
+ sesvar41 
+----------
+      100
+(1 row)
+
+SELECT VARIABLE(svartest_dml.sesvar41); -- NULL
+ sesvar41 
+----------
+         
+(1 row)
+
+-- should fail
+SELECT VARIABLE(sesvar41);
+ERROR:  session variable "sesvar41" doesn't exist
+LINE 1: SELECT VARIABLE(sesvar41);
+                        ^
+SET SEARCH_PATH TO svartest_dml;
+-- should be ok
+SELECT VARIABLE(sesvar41);
+ sesvar41 
+----------
+         
+(1 row)
+
+SET SEARCH_PATH TO DEFAULT;
+-- should not crash
+DO $$
+BEGIN
+  RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41);
+END;
+$$;
+NOTICE:  <NULL>
+CREATE OR REPLACE FUNCTION svartest_dml.testsql()
+RETURNS int AS $$
+SELECT VARIABLE(svartest_dml.sesvar41);
+$$ LANGUAGE sql;
+SELECT svartest_dml.testsql();
+ testsql 
+---------
+        
+(1 row)
+
+-- session variable cannot be used as parameter of CALL or EXECUTE
+CREATE OR REPLACE PROCEDURE svartest_dml.proc(int)
+AS $$
+BEGIN
+  RAISE NOTICE '%', $1;
+END;
+$$ LANGUAGE plpgsql;
+-- should not crash
+CALL svartest_dml.proc(VARIABLE(svartest_dml.sesvar41));
+ERROR:  session variable reference is not supported here
+LINE 1: CALL svartest_dml.proc(VARIABLE(svartest_dml.sesvar41));
+                                        ^
+PREPARE svartest_dml_prepstmt(int) AS SELECT $1;
+-- should not crash
+EXECUTE svartest_dml_prepstmt(VARIABLE(svartest_dml.sesvar41));
+ERROR:  session variable reference is not supported here
+LINE 1: EXECUTE svartest_dml_prepstmt(VARIABLE(svartest_dml.sesvar41...
+                                               ^
+DROP PROCEDURE svartest_dml.proc;
+DEALLOCATE svartest_dml_prepstmt;
+-- domains are supported
+CREATE DOMAIN svartest_dml_int_not_null AS int CHECK(value IS NOT NULL);
+CREATE VARIABLE svartest_dml.svartest42 AS svartest_dml_int_not_null;
+-- should fail
+SELECT VARIABLE(svartest_dml.svartest42);
+ERROR:  value for domain svartest_dml_int_not_null violates check constraint "svartest_dml_int_not_null_check"
+DROP VARIABLE svartest_dml.svartest42;
+DROP DOMAIN svartest_dml_int_not_null;
+CREATE ROLE regress_svartest_dml_read_role;
+CREATE OR REPLACE FUNCTION svartest_dml.func_secdef()
+RETURNS int AS $$
+SELECT VARIABLE(svartest_dml.sesvar41);
+$$ LANGUAGE SQL SECURITY DEFINER;
+GRANT USAGE ON SCHEMA svartest_dml TO regress_svartest_dml_read_role;
+SET ROLE TO regress_svartest_dml_read_role;
+-- should fail
+SELECT VARIABLE(svartest_dml.sesvar41);
+ERROR:  permission denied for session variable sesvar41
+-- should fail
+DO $$
+BEGIN
+  RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41);
+END;
+$$;
+ERROR:  permission denied for session variable sesvar41
+CONTEXT:  PL/pgSQL expression "VARIABLE(svartest_dml.sesvar41)"
+PL/pgSQL function inline_code_block line 3 at RAISE
+-- using sql function should to fail
+SELECT svartest_dml.testsql();
+ERROR:  permission denied for session variable sesvar41
+CONTEXT:  SQL function "testsql" statement 1
+-- using security definer should be ok
+SELECT svartest_dml.func_secdef();
+ func_secdef 
+-------------
+            
+(1 row)
+
+SET ROLE TO DEFAULT;
+DROP FUNCTION svartest_dml.func_secdef();
+GRANT SELECT ON VARIABLE svartest_dml.sesvar41 TO regress_svartest_dml_read_role;
+SET ROLE TO regress_svartest_dml_read_role;
+-- should be ok
+SELECT VARIABLE(svartest_dml.sesvar41);
+ sesvar41 
+----------
+         
+(1 row)
+
+-- should be ok
+DO $$
+BEGIN
+  RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41);
+END;
+$$;
+NOTICE:  <NULL>
+SET ROLE TO DEFAULT;
+CREATE TABLE svartest_dml.testtab(a int);
+INSERT INTO svartest_dml.testtab SELECT * FROM generate_series(1,1000);
+CREATE INDEX svartest_dml_testtab_a ON svartest_dml.testtab(a);
+ANALYZE svartest_dml.testtab;
+-- force index
+SET enable_seqscan TO OFF;
+-- index scan should be used
+EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = VARIABLE(sesvar40);
+                       QUERY PLAN                        
+---------------------------------------------------------
+ Index Only Scan using svartest_dml_testtab_a on testtab
+   Index Cond: (a = VARIABLE(sesvar40))
+(2 rows)
+
+DROP INDEX svartest_dml.svartest_dml_testtab_a;
+SET enable_seqscan TO DEFAULT;
+-- parallel execution should be blocked
+-- 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;
+-- parallel plan should be used
+EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = 100;
+             QUERY PLAN             
+------------------------------------
+ Gather
+   Workers Planned: 2
+   ->  Parallel Seq Scan on testtab
+         Filter: (a = 100)
+(4 rows)
+
+-- parallel plan should not be used
+EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = VARIABLE(sesvar40);
+             QUERY PLAN             
+------------------------------------
+ Seq Scan on testtab
+   Filter: (a = VARIABLE(sesvar40))
+(2 rows)
+
+RESET parallel_setup_cost;
+RESET parallel_tuple_cost;
+RESET min_parallel_table_scan_size;
+RESET max_parallel_workers_per_gather;
+DROP SCHEMA svartest_dml CASCADE;
+NOTICE:  drop cascades to 3 other objects
+DETAIL:  drop cascades to session variable svartest_dml.sesvar41
+drop cascades to function svartest_dml.testsql()
+drop cascades to table svartest_dml.testtab
+DROP ROLE regress_svartest_dml_read_role;
+DROP VARIABLE sesvar40;
+DROP TABLE svartest_dml;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 95c76baf9ae..b16ec065950 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -115,7 +115,7 @@ test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson
 # NB: temp.sql does reconnects which transiently use 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 session_variables_ddl session_variables_acl
+test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml
 
 # ----------
 # Another group of parallel tests
@@ -140,3 +140,8 @@ test: fast_default
 # run tablespace test at the end because it drops the tablespace created during
 # setup that other tests may use.
 test: tablespace
+
+# ----------
+# Another group of parallel tests (session variables related)
+# ----------
+test: session_variables_ddl session_variables_acl session_variables_dml
diff --git a/src/test/regress/sql/session_variables_dml.sql b/src/test/regress/sql/session_variables_dml.sql
new file mode 100644
index 00000000000..b2870dde9e9
--- /dev/null
+++ b/src/test/regress/sql/session_variables_dml.sql
@@ -0,0 +1,161 @@
+CREATE VARIABLE sesvar40 AS int;
+
+-- should not be accessible without variable's fence
+-- should fail
+SELECT sesvar40;
+
+-- should be ok
+SELECT VARIABLE(sesvar40);
+
+CREATE SCHEMA svartest_dml;
+CREATE VARIABLE svartest_dml.sesvar41 AS int;
+
+-- identifier collision test
+CREATE TABLE svartest_dml(sesvar41 int);
+INSERT INTO svartest_dml VALUES(100);
+
+SELECT sesvar41 FROM svartest_dml; -- 100
+SELECT VARIABLE(svartest_dml.sesvar41); -- NULL
+
+-- should fail
+SELECT VARIABLE(sesvar41);
+
+SET SEARCH_PATH TO svartest_dml;
+
+-- should be ok
+SELECT VARIABLE(sesvar41);
+
+SET SEARCH_PATH TO DEFAULT;
+
+-- should not crash
+DO $$
+BEGIN
+  RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41);
+END;
+$$;
+
+CREATE OR REPLACE FUNCTION svartest_dml.testsql()
+RETURNS int AS $$
+SELECT VARIABLE(svartest_dml.sesvar41);
+$$ LANGUAGE sql;
+
+SELECT svartest_dml.testsql();
+
+-- session variable cannot be used as parameter of CALL or EXECUTE
+CREATE OR REPLACE PROCEDURE svartest_dml.proc(int)
+AS $$
+BEGIN
+  RAISE NOTICE '%', $1;
+END;
+$$ LANGUAGE plpgsql;
+
+-- should not crash
+CALL svartest_dml.proc(VARIABLE(svartest_dml.sesvar41));
+
+PREPARE svartest_dml_prepstmt(int) AS SELECT $1;
+
+-- should not crash
+EXECUTE svartest_dml_prepstmt(VARIABLE(svartest_dml.sesvar41));
+
+DROP PROCEDURE svartest_dml.proc;
+DEALLOCATE svartest_dml_prepstmt;
+
+-- domains are supported
+CREATE DOMAIN svartest_dml_int_not_null AS int CHECK(value IS NOT NULL);
+CREATE VARIABLE svartest_dml.svartest42 AS svartest_dml_int_not_null;
+
+-- should fail
+SELECT VARIABLE(svartest_dml.svartest42);
+
+DROP VARIABLE svartest_dml.svartest42;
+DROP DOMAIN svartest_dml_int_not_null;
+
+CREATE ROLE regress_svartest_dml_read_role;
+
+CREATE OR REPLACE FUNCTION svartest_dml.func_secdef()
+RETURNS int AS $$
+SELECT VARIABLE(svartest_dml.sesvar41);
+$$ LANGUAGE SQL SECURITY DEFINER;
+
+GRANT USAGE ON SCHEMA svartest_dml TO regress_svartest_dml_read_role;
+
+SET ROLE TO regress_svartest_dml_read_role;
+
+-- should fail
+SELECT VARIABLE(svartest_dml.sesvar41);
+
+-- should fail
+DO $$
+BEGIN
+  RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41);
+END;
+$$;
+
+-- using sql function should to fail
+SELECT svartest_dml.testsql();
+
+-- using security definer should be ok
+SELECT svartest_dml.func_secdef();
+
+SET ROLE TO DEFAULT;
+
+DROP FUNCTION svartest_dml.func_secdef();
+
+GRANT SELECT ON VARIABLE svartest_dml.sesvar41 TO regress_svartest_dml_read_role;
+
+SET ROLE TO regress_svartest_dml_read_role;
+
+-- should be ok
+SELECT VARIABLE(svartest_dml.sesvar41);
+
+-- should be ok
+DO $$
+BEGIN
+  RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41);
+END;
+$$;
+
+SET ROLE TO DEFAULT;
+
+CREATE TABLE svartest_dml.testtab(a int);
+
+INSERT INTO svartest_dml.testtab SELECT * FROM generate_series(1,1000);
+
+CREATE INDEX svartest_dml_testtab_a ON svartest_dml.testtab(a);
+
+ANALYZE svartest_dml.testtab;
+
+-- force index
+SET enable_seqscan TO OFF;
+
+-- index scan should be used
+EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = VARIABLE(sesvar40);
+
+DROP INDEX svartest_dml.svartest_dml_testtab_a;
+
+SET enable_seqscan TO DEFAULT;
+
+-- parallel execution should be blocked
+-- 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;
+
+-- parallel plan should be used
+EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = 100;
+
+-- parallel plan should not be used
+EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = VARIABLE(sesvar40);
+
+RESET parallel_setup_cost;
+RESET parallel_tuple_cost;
+RESET min_parallel_table_scan_size;
+RESET max_parallel_workers_per_gather;
+
+DROP SCHEMA svartest_dml CASCADE;
+DROP ROLE regress_svartest_dml_read_role;
+
+DROP VARIABLE sesvar40;
+
+DROP TABLE svartest_dml;
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index f8d95c8e330..f32dcc29023 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2713,6 +2713,7 @@ SerializedTransactionState
 Session
 SessionBackupState
 SessionEndType
+SessionVariableValue
 SetConstraintState
 SetConstraintStateData
 SetConstraintTriggerData
-- 
2.51.0



  [text/x-patch] v20250829-0007-local-HASHTAB-for-currently-used-session-variables-a.patch (14.1K, 11-v20250829-0007-local-HASHTAB-for-currently-used-session-variables-a.patch)
  download | inline diff:
From dea9548b0ee194a16c0026bc1cc2c9f0ce42657f Mon Sep 17 00:00:00 2001
From: "[email protected]" <[email protected]>
Date: Fri, 30 May 2025 22:44:58 +0200
Subject: [PATCH 07/15] local HASHTAB for currently used session variables and
 low level access functions

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.

Hash entries related to dropped session variables are not released. The memory
cleaning is implemented in memory-cleaning-after-DROP-VARIABLE patch.
---
 doc/src/sgml/glossary.sgml              |   5 +-
 src/backend/commands/session_variable.c | 428 ++++++++++++++++++++++++
 src/include/commands/session_variable.h |   3 +
 src/tools/pgindent/typedefs.list        |   2 +
 4 files changed, 436 insertions(+), 2 deletions(-)

diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml
index c37fd5da50b..97cd13957bb 100644
--- a/doc/src/sgml/glossary.sgml
+++ b/doc/src/sgml/glossary.sgml
@@ -1717,8 +1717,9 @@
     <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.
+     The default value of the session variable is null.  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"/>.
diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c
index f641e00c1ac..dbc054795bb 100644
--- a/src/backend/commands/session_variable.c
+++ b/src/backend/commands/session_variable.c
@@ -14,14 +14,442 @@
  */
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "catalog/pg_variable.h"
 #include "catalog/namespace.h"
 #include "catalog/pg_type.h"
 #include "commands/session_variable.h"
 #include "miscadmin.h"
 #include "parser/parse_type.h"
+#include "storage/lmgr.h"
+#include "storage/proc.h"
 #include "utils/builtins.h"
+#include "utils/datum.h"
+#include "utils/inval.h"
 #include "utils/lsyscache.h"
+#include "utils/memutils.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);
+}
+
+/*
+ * 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)
+{
+	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 was processed during the domain
+	 * check. But the variable and all its dependencies are locked now, so we
+	 * don't need to repeat the validation.
+	 */
+	return copy_session_variable_value(svar, isNull);
+}
 
 /*
  * Creates a new variable
diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h
index 49f36ac6885..9f5c6e30fbd 100644
--- a/src/include/commands/session_variable.h
+++ b/src/include/commands/session_variable.h
@@ -19,6 +19,9 @@
 #include "parser/parse_node.h"
 #include "nodes/parsenodes.h"
 
+extern void SetSessionVariable(Oid varid, Datum value, bool isNull);
+extern Datum GetSessionVariable(Oid varid, bool *isNull);
+
 extern ObjectAddress CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt);
 
 #endif
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index d6585c2e6e5..f8d95c8e330 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2654,6 +2654,8 @@ SSL_CTX
 STARTUPINFO
 STRLEN
 SV
+SVariableData
+SVariable
 SYNCHRONIZATION_BARRIER
 SYSTEM_INFO
 SampleScan
-- 
2.51.0



  [text/x-patch] v20250829-0006-session-variable-fences-parsing.patch (32.4K, 12-v20250829-0006-session-variable-fences-parsing.patch)
  download | inline diff:
From 8e71c8dbdd6a59ac4fe21cbcf07c2e9679d49f66 Mon Sep 17 00:00:00 2001
From: "[email protected]" <[email protected]>
Date: Fri, 30 May 2025 07:27:27 +0200
Subject: [PATCH 06/15] session variable fences parsing

The session variables can be used in query only inside the variable fence.
This is special syntax VARIABLE(varname), that eliminates a risk of
collision between variable and column identifier.

The session variables cannot be used as parameters of CALL or EXECUTE
commands. These commands evaluates arguments by direct call of expression
executor, and direct access to session variables from expression executor
will be implemented later (in next step).
---
 doc/src/sgml/ddl.sgml               |  11 ++
 src/backend/catalog/namespace.c     | 289 ++++++++++++++++++++++++++++
 src/backend/commands/prepare.c      |   8 +
 src/backend/nodes/nodeFuncs.c       |   6 +
 src/backend/parser/analyze.c        |   9 +
 src/backend/parser/gram.y           |  28 ++-
 src/backend/parser/parse_expr.c     | 208 +++++++++++++++++++-
 src/backend/parser/parse_merge.c    |   1 +
 src/backend/parser/parse_target.c   |   7 +
 src/backend/utils/adt/ruleutils.c   |  46 +++++
 src/backend/utils/cache/lsyscache.c |  24 +++
 src/include/catalog/namespace.h     |   2 +
 src/include/nodes/parsenodes.h      |  12 ++
 src/include/nodes/primnodes.h       |   5 +
 src/include/parser/parse_node.h     |   1 +
 src/include/utils/lsyscache.h       |   4 +
 src/pl/plpgsql/src/pl_exec.c        |   3 +-
 src/tools/pgindent/typedefs.list    |   1 +
 18 files changed, 661 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml
index 420a4d9ff11..2655fa5e7ce 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -5396,6 +5396,17 @@ EXPLAIN SELECT count(*) FROM measurement WHERE logdate &gt;= DATE '2008-01-01';
     The session variable holds value in session memory.  This value is private
     to each session and is released when the session ends.
    </para>
+
+   <para>
+    In an query the session variable can be used only inside
+    <firstterm>variable fence</firstterm>. This is special syntax for
+    session variable identifier, and can be used only for session variable
+    identifier. The special syntax for accessing session variables removes
+    risk of collisions between variable identifiers and column names.
+<programlisting>
+SELECT VARIABLE(current_user_id);
+</programlisting>
+   </para>
   </sect1>
 
  <sect1 id="ddl-others">
diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c
index 78cabf964ef..1d053a35ca0 100644
--- a/src/backend/catalog/namespace.c
+++ b/src/backend/catalog/namespace.c
@@ -3496,6 +3496,295 @@ NamesFromList(List *names)
 	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"."attribute".  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));
+
+					/*
+					 * The syntax ident.* is used only by relation aliases,
+					 * and then this identifier cannot be a reference to
+					 * session variable.
+					 */
+					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.attribute.
+						 *
+						 * 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
+			LockDatabaseObject(VariableRelationId, varid, 0, AccessShareLock);
+
+		/*
+		 * If no invalidation message were processed, we're done!
+		 */
+		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/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..fcadcd9bc3f 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -341,6 +341,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. This case should be blocked parser by
+	 * expr_kind_allows_session_variables, so only assertions is used here.
+	 */
+	Assert(!pstate->p_hasSessionVariables);
+
 	/* Prepare the expressions for execution */
 	exprstates = ExecPrepareExprList(params, estate);
 
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index 7bc823507f1..cd609c6e479 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -1673,6 +1673,9 @@ exprLocation(const Node *expr)
 		case T_ParamRef:
 			loc = ((const ParamRef *) expr)->location;
 			break;
+		case T_VariableFence:
+			loc = ((const VariableFence *) expr)->location;
+			break;
 		case T_A_Const:
 			loc = ((const A_Const *) expr)->location;
 			break;
@@ -4705,6 +4708,9 @@ raw_expression_tree_walker_impl(Node *node,
 					return true;
 			}
 			break;
+		case T_VariableFence:
+			/* we assume the fields contain nothing interesting */
+			break;
 		default:
 			elog(ERROR, "unrecognized node type: %d",
 				 (int) nodeTag(node));
diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
index 34f7c17f576..95bb0620f39 100644
--- a/src/backend/parser/analyze.c
+++ b/src/backend/parser/analyze.c
@@ -607,6 +607,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);
 
@@ -1032,6 +1033,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);
 
@@ -1497,6 +1499,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)
 	{
@@ -1723,6 +1726,7 @@ 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);
 
@@ -1974,6 +1978,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)
 	{
@@ -2449,6 +2454,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);
 
@@ -2516,6 +2522,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);
 
@@ -2992,6 +2999,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 */
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index abc365c0cfa..52a242ccc7f 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -525,7 +525,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %type <node>	def_arg columnElem where_clause where_or_current_clause
 				a_expr b_expr c_expr AexprConst indirection_el opt_slice_bound
 				columnref having_clause func_table xmltable array_expr
-				OptWhereClause operator_def_arg
+				OptWhereClause operator_def_arg variable_fence
 %type <list>	opt_column_and_period_list
 %type <list>	rowsfrom_item rowsfrom_list opt_col_def_list
 %type <boolean> opt_ordinality opt_without_overlaps
@@ -881,7 +881,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
  */
 %nonassoc	UNBOUNDED NESTED /* ideally would have same precedence as IDENT */
 %nonassoc	IDENT PARTITION RANGE ROWS GROUPS PRECEDING FOLLOWING CUBE ROLLUP
-			SET KEYS OBJECT_P SCALAR VALUE_P WITH WITHOUT PATH
+			SET KEYS OBJECT_P SCALAR VALUE_P WITH WITHOUT PATH VARIABLE
 %left		Op OPERATOR		/* multi-character ops and user-defined operators */
 %left		'+' '-'
 %left		'*' '/' '%'
@@ -15690,6 +15690,19 @@ c_expr:		columnref								{ $$ = $1; }
 					else
 						$$ = $2;
 				}
+			| variable_fence opt_indirection
+				{
+					if ($2)
+					{
+						A_Indirection *n = makeNode(A_Indirection);
+
+						n->arg = (Node *) $1;
+						n->indirection = check_indirection($2, yyscanner);
+						$$ = (Node *) n;
+					}
+					else
+						$$ = $1;
+				}
 			| case_expr
 				{ $$ = $1; }
 			| func_expr
@@ -17065,6 +17078,17 @@ case_arg:	a_expr									{ $$ = $1; }
 			| /*EMPTY*/								{ $$ = NULL; }
 		;
 
+variable_fence:
+			VARIABLE '(' any_name ')'
+				{
+					VariableFence *vf = makeNode(VariableFence);
+
+					vf->varname = $3;
+					vf->location = @3;
+					$$ = (Node *) vf;
+				}
+		;
+
 columnref:	ColId
 				{
 					$$ = makeColumnRef($1, NIL, @1, yyscanner);
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index e1979a80c19..d17d0583cd4 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -15,6 +15,7 @@
 
 #include "postgres.h"
 
+#include "catalog/namespace.h"
 #include "catalog/pg_aggregate.h"
 #include "catalog/pg_type.h"
 #include "miscadmin.h"
@@ -76,6 +77,7 @@ static Node *transformWholeRowRef(ParseState *pstate,
 static Node *transformIndirection(ParseState *pstate, A_Indirection *ind);
 static Node *transformTypeCast(ParseState *pstate, TypeCast *tc);
 static Node *transformCollateClause(ParseState *pstate, CollateClause *c);
+static Node *transformVariableFence(ParseState *pstate, VariableFence *vf);
 static Node *transformJsonObjectConstructor(ParseState *pstate,
 											JsonObjectConstructor *ctor);
 static Node *transformJsonArrayConstructor(ParseState *pstate,
@@ -105,7 +107,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);
 
 /*
  * transformExpr -
@@ -369,6 +373,10 @@ transformExprRecurse(ParseState *pstate, Node *expr)
 			result = transformJsonFuncExpr(pstate, (JsonFuncExpr *) expr);
 			break;
 
+		case T_VariableFence:
+			result = transformVariableFence(pstate, (VariableFence *) expr);
+			break;
+
 		default:
 			/* should not reach here */
 			elog(ERROR, "unrecognized node type: %d", (int) nodeTag(expr));
@@ -902,6 +910,135 @@ transformParamRef(ParseState *pstate, ParamRef *pref)
 	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_UPDATE_TARGET:
+		case EXPR_KIND_UPDATE_SOURCE:
+		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:
+			result = true;
+			break;
+
+			/* session variables not allowed */
+		case EXPR_KIND_INSERT_TARGET:
+		case EXPR_KIND_EXECUTE_PARAMETER:
+		case EXPR_KIND_CALL_ARGUMENT:
+		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_ALTER_COL_TRANSFORM:
+		case EXPR_KIND_POLICY:
+		case EXPR_KIND_COPY_WHERE:
+			result = false;
+			break;
+	}
+
+	return result;
+}
+
+static Node *
+transformVariableFence(ParseState *pstate, VariableFence *vf)
+{
+	Node	   *result;
+	Oid			varid = InvalidOid;
+	char	   *attrname = NULL;
+	bool		not_unique;
+
+	/* VariableFence can be used only in context when variables are supported */
+	if (!expr_kind_allows_session_variables(pstate->p_expr_kind))
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("session variable reference is not supported here"),
+				 parser_errposition(pstate, vf->location)));
+
+	/* takes an AccessShareLock on the session variable */
+	varid = IdentifyVariable(vf->varname, &attrname, &not_unique, false);
+
+	if (not_unique)
+		ereport(ERROR,
+				(errcode(ERRCODE_AMBIGUOUS_PARAMETER),
+				 errmsg("session variable reference \"%s\" is ambiguous",
+						NameListToString(vf->varname)),
+				 parser_errposition(pstate, vf->location)));
+
+	if (OidIsValid(varid))
+	{
+		Oid			typid;
+		int32		typmod;
+		Oid			collid;
+
+		get_session_variable_type_typmod_collid(varid, &typid, &typmod,
+												&collid);
+
+		result = makeParamSessionVariable(pstate,
+										  varid, typid, typmod, collid,
+										  attrname, vf->location);
+	}
+	else
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("session variable \"%s\" doesn't exist",
+						NameListToString(vf->varname)),
+				 parser_errposition(pstate, vf->location)));
+
+	return result;
+}
+
 /* Test whether an a_expr is a plain NULL constant or not */
 static bool
 exprIsNullConstant(Node *arg)
@@ -3121,6 +3258,75 @@ make_nulltest_from_distinct(ParseState *pstate, A_Expr *distincta, Node *arg)
 	return (Node *) nt;
 }
 
+/*
+ * 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 \"%s.%s\"",
+						attrname,
+						get_namespace_name(get_session_variable_namespace(varid)),
+						get_session_variable_name(varid)),
+				 parser_errposition(pstate, location)));
+	}
+
+	return (Node *) param;
+}
+
 /*
  * Produce a string identifying an expression by kind.
  *
diff --git a/src/backend/parser/parse_merge.c b/src/backend/parser/parse_merge.c
index 51d7703eff7..244efcddf32 100644
--- a/src/backend/parser/parse_merge.c
+++ b/src/backend/parser/parse_merge.c
@@ -405,6 +405,7 @@ transformMergeStmt(ParseState *pstate, MergeStmt *stmt)
 
 	qry->hasTargetSRFs = false;
 	qry->hasSubLinks = pstate->p_hasSubLinks;
+	qry->hasSessionVariables = pstate->p_hasSessionVariables;
 
 	assign_query_collations(pstate, qry);
 
diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c
index 905c975d83b..8b5240cd54b 100644
--- a/src/backend/parser/parse_target.c
+++ b/src/backend/parser/parse_target.c
@@ -2033,6 +2033,13 @@ FigureColnameInternal(Node *node, char **name)
 						 (int) ((JsonFuncExpr *) node)->op);
 			}
 			break;
+		case T_VariableFence:
+			{
+				/* return last field name */
+				*name = strVal(llast(((VariableFence *) node)->varname));
+				return 2;
+			}
+			break;
 		default:
 			break;
 	}
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 3d6e6bdbfd2..52b8673eed9 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"
@@ -534,6 +535,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);
@@ -8803,6 +8805,14 @@ get_parameter(Param *param, deparse_context *context)
 		}
 	}
 
+	/* translate paramvarid to session variable name */
+	if (param->paramkind == PARAM_VARIABLE)
+	{
+		appendStringInfo(context->buf, "VARIABLE(%s)",
+						 generate_session_variable_name(param->paramvarid));
+		return;
+	}
+
 	/*
 	 * Not PARAM_EXEC, or couldn't find referent: just print $N.
 	 *
@@ -13566,6 +13576,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/lsyscache.c b/src/backend/utils/cache/lsyscache.c
index 1c4031eea23..b9b0ac55475 100644
--- a/src/backend/utils/cache/lsyscache.c
+++ b/src/backend/utils/cache/lsyscache.c
@@ -3946,3 +3946,27 @@ get_session_variable_namespace(Oid varid)
 
 	return varnamespace;
 }
+
+/*
+ * Returns the type, typmod and collid of the given session variable.
+ */
+void
+get_session_variable_type_typmod_collid(Oid varid, Oid *typid, int32 *typmod,
+										Oid *collid)
+{
+	HeapTuple	tup;
+	Form_pg_variable varform;
+
+	tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid));
+
+	if (!HeapTupleIsValid(tup))
+		elog(ERROR, "cache lookup failed for session variable %u", varid);
+
+	varform = (Form_pg_variable) GETSTRUCT(tup);
+
+	*typid = varform->vartype;
+	*typmod = varform->vartypmod;
+	*collid = varform->varcollation;
+
+	ReleaseSysCache(tup);
+}
diff --git a/src/include/catalog/namespace.h b/src/include/catalog/namespace.h
index bdac0c13bec..b221adafd2f 100644
--- a/src/include/catalog/namespace.h
+++ b/src/include/catalog/namespace.h
@@ -98,6 +98,8 @@ extern Oid	TypenameGetTypidExtended(const char *typname, bool temp_ok);
 extern bool TypeIsVisible(Oid typid);
 
 extern bool VariableIsVisible(Oid varid);
+extern Oid	IdentifyVariable(List *names, char **attrname,
+							 bool *not_unique, bool noerror);
 
 extern FuncCandidateList FuncnameGetCandidates(List *names,
 											   int nargs, List *argnames,
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 2b4c7363d74..10cf52467e3 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -167,6 +167,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);
 
@@ -321,6 +323,16 @@ typedef struct ParamRef
 	ParseLoc	location;		/* token location, or -1 if unknown */
 } ParamRef;
 
+/*
+ * VariableFence - ensure so fields will be interpretted as a variable
+ */
+typedef struct VariableFence
+{
+	NodeTag		type;
+	List	   *varname;		/* variable name (String nodes) */
+	ParseLoc	location;		/* token location, or -1 if unknown */
+} VariableFence;
+
 /*
  * A_Expr - infix, prefix, and postfix expressions
  */
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index 6dfca3cb35b..9d7b9f598f8 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -378,6 +378,8 @@ 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
+ *				(paramvarid holds the variable's OID).
  */
 typedef enum ParamKind
 {
@@ -385,6 +387,7 @@ typedef enum ParamKind
 	PARAM_EXEC,
 	PARAM_SUBLINK,
 	PARAM_MULTIEXPR,
+	PARAM_VARIABLE,
 } ParamKind;
 
 typedef struct Param
@@ -399,6 +402,8 @@ typedef struct Param
 	int32		paramtypmod;
 	/* OID of collation, or InvalidOid if none */
 	Oid			paramcollid;
+	/* OID of used session variable or InvalidOid if none */
+	Oid			paramvarid pg_node_attr(query_jumble_ignore);
 	/* token location, or -1 if unknown */
 	ParseLoc	location;
 } Param;
diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h
index f7d07c84542..84e886940d8 100644
--- a/src/include/parser/parse_node.h
+++ b/src/include/parser/parse_node.h
@@ -228,6 +228,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/utils/lsyscache.h b/src/include/utils/lsyscache.h
index 5daa308f4eb..49d116fa60a 100644
--- a/src/include/utils/lsyscache.h
+++ b/src/include/utils/lsyscache.h
@@ -214,6 +214,10 @@ extern char *get_subscription_name(Oid subid, bool missing_ok);
 
 extern char *get_session_variable_name(Oid varid);
 extern Oid	get_session_variable_namespace(Oid varid);
+extern void get_session_variable_type_typmod_collid(Oid varid,
+													Oid *typid,
+													int32 *typmod,
+													Oid *collid);
 
 #define type_is_array(typid)  (get_element_type(typid) != InvalidOid)
 /* type_is_array_domain accepts both plain arrays and domains over arrays */
diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
index d19425b7a71..96857874ffe 100644
--- a/src/pl/plpgsql/src/pl_exec.c
+++ b/src/pl/plpgsql/src/pl_exec.c
@@ -8268,7 +8268,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/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 2b21c8f8c3f..d6585c2e6e5 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -3201,6 +3201,7 @@ ValidatorValidateCB
 ValuesScan
 ValuesScanState
 Var
+VariableFence
 VarBit
 VarChar
 VarParamState
-- 
2.51.0



  [text/x-patch] v20250829-0005-support-of-session-variables-for-pg_dump.patch (15.3K, 13-v20250829-0005-support-of-session-variables-for-pg_dump.patch)
  download | inline diff:
From 6ec968abcf29742e48cd8cec63f96c07fc2df44d Mon Sep 17 00:00:00 2001
From: "[email protected]" <[email protected]>
Date: Wed, 28 May 2025 22:54:09 +0200
Subject: [PATCH 05/15] support of session variables for pg_dump

This patch enhancing pg_dump to support session variables.
---
 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            | 190 +++++++++++++++++++++++++++
 src/bin/pg_dump/pg_dump.h            |  15 +++
 src/bin/pg_dump/pg_dump_sort.c       |   7 +
 src/bin/pg_dump/t/002_pg_dump.pl     |  63 +++++++++
 src/tools/pgindent/typedefs.list     |   1 +
 9 files changed, 296 insertions(+)

diff --git a/src/bin/pg_dump/common.c b/src/bin/pg_dump/common.c
index a1976fae607..5333acb7bbd 100644
--- a/src/bin/pg_dump/common.c
+++ b/src/bin/pg_dump/common.c
@@ -247,6 +247,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 8945bdd42c5..af10f9e04da 100644
--- a/src/bin/pg_dump/dumputils.c
+++ b/src/bin/pg_dump/dumputils.c
@@ -552,6 +552,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 d9041dad720..dac067bdc4c 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 3c3acbaccdb..48b2f476b77 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -3207,6 +3207,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;
 		}
@@ -3780,6 +3788,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 fc7a6639163..76ad84d97a3 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -381,6 +381,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);
@@ -5613,6 +5614,188 @@ 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();
+
+	/* 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->dumpSchema)
+		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,
@@ -11745,6 +11928,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
 		case DO_REL_STATS:
 			dumpRelationStats(fout, (const RelStatsInfo *) 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 */
@@ -16251,6 +16437,9 @@ dumpDefaultACL(Archive *fout, const DefaultACLInfo *daclinfo)
 		case DEFACLOBJ_LARGEOBJECT:
 			type = "LARGE OBJECTS";
 			break;
+		case DEFACLOBJ_VARIABLE:
+			type = "VARIABLES";
+			break;
 		default:
 			/* shouldn't get here */
 			pg_fatal("unrecognized object type in default privileges: %d",
@@ -20064,6 +20253,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 dde85ed156c..12bebf4a988 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -53,6 +53,7 @@ typedef enum
 	DO_TABLE,
 	DO_TABLE_ATTACH,
 	DO_ATTRDEF,
+	DO_VARIABLE,
 	DO_INDEX,
 	DO_INDEX_ATTACH,
 	DO_STATSEXT,
@@ -744,6 +745,19 @@ 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;
+	Oid			varcollation;
+	const char *rolname;		/* name of owner, or empty string */
+} VariableInfo;
+
 /*
  *	common utility functions
  */
@@ -828,5 +842,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 2d02456664b..6b3fb9840ae 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -76,6 +76,7 @@ enum dbObjectTypePriorities
 	PRIO_TABLE_ATTACH,
 	PRIO_DUMMY_TYPE,
 	PRIO_ATTRDEF,
+	PRIO_VARIABLE,
 	PRIO_PRE_DATA_BOUNDARY,		/* boundary! */
 	PRIO_TABLE_DATA,
 	PRIO_SEQUENCE_SET,
@@ -119,6 +120,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,
@@ -1749,6 +1751,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
 					 "RELATION STATISTICS FOR %s  (ID %d OID %u)",
 					 obj->name, obj->dumpId, obj->catId.oid);
 			return;
+		case DO_VARIABLE:
+			snprintf(buf, bufsize,
+					 "VARIABLE %s  (ID %d OID %u)",
+					 obj->name, obj->dumpId, obj->catId.oid);
+			return;
 	}
 	/* 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 e7a2d64f741..84153db459d 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -962,6 +962,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
@@ -1962,6 +1972,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) '
@@ -4315,6 +4342,23 @@ my %tests = (
 		},
 	},
 
+	'CREATE VARIABLE test_variable' => {
+		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
@@ -4779,6 +4823,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/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 4694653a91d..2b21c8f8c3f 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -3209,6 +3209,7 @@ VarString
 VarStringSortSupport
 Variable
 VariableAssignHook
+VariableInfo
 VariableSetKind
 VariableSetStmt
 VariableShowStmt
-- 
2.51.0



  [text/x-patch] v20250829-0003-GRANT-REVOKE-variable.patch (52.5K, 14-v20250829-0003-GRANT-REVOKE-variable.patch)
  download | inline diff:
From 9d45eb788ed49797104f305bea78e17db4ed8a56 Mon Sep 17 00:00:00 2001
From: "[email protected]" <[email protected]>
Date: Tue, 5 Aug 2025 06:37:28 +0200
Subject: [PATCH 03/15] GRANT, REVOKE variable

Access to session variables can be controlled by SELECT or UPDATE rights. Both rights are
introduced by this patch too. Default ACL are supported.
---
 doc/src/sgml/catalogs.sgml                    |   3 +-
 doc/src/sgml/ddl.sgml                         |  19 +-
 doc/src/sgml/func/func-info.sgml              |  23 +-
 .../sgml/ref/alter_default_privileges.sgml    |  29 +-
 doc/src/sgml/ref/grant.sgml                   |  20 +-
 doc/src/sgml/ref/revoke.sgml                  |   8 +
 src/backend/catalog/aclchk.c                  |  74 ++++-
 src/backend/catalog/objectaddress.c           |  22 +-
 src/backend/catalog/pg_variable.c             |  11 +-
 src/backend/parser/gram.y                     |  21 +-
 src/backend/utils/adt/acl.c                   | 221 +++++++++++++
 src/include/catalog/pg_default_acl.h          |   1 +
 src/include/catalog/pg_proc.dat               |  20 ++
 src/include/parser/kwlist.h                   |   1 +
 src/include/utils/acl.h                       |   1 +
 .../expected/session_variables_acl.out        | 307 ++++++++++++++++++
 src/test/regress/parallel_schedule            |   2 +-
 .../regress/sql/session_variables_acl.sql     | 158 +++++++++
 18 files changed, 915 insertions(+), 26 deletions(-)
 create mode 100644 src/test/regress/expected/session_variables_acl.out
 create mode 100644 src/test/regress/sql/session_variables_acl.sql

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index b33f3f06d71..b1815f8d2ab 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -3360,7 +3360,8 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
        <literal>f</literal> = function,
        <literal>T</literal> = type,
        <literal>n</literal> = schema,
-       <literal>L</literal> = large object
+       <literal>L</literal> = large object,
+       <literal>V</literal> = session variable
       </para></entry>
      </row>
 
diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml
index fa711a09bc4..420a4d9ff11 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -2025,6 +2025,7 @@ REVOKE ALL ON accounts FROM PUBLIC;
        For sequences, this privilege also allows use of the
        <function>currval</function> function.
        For large objects, this privilege allows the object to be read.
+       For session variables, this  privilege allows the object to be read.
       </para>
      </listitem>
     </varlistentry>
@@ -2060,6 +2061,8 @@ REVOKE ALL ON accounts FROM PUBLIC;
        <function>setval</function> functions.
        For large objects, this privilege allows writing or truncating the
        object.
+       For session variables, this privilege allows to set a value to the
+       object.
       </para>
      </listitem>
     </varlistentry>
@@ -2304,7 +2307,8 @@ REVOKE ALL ON accounts FROM PUBLIC;
        <literal>LARGE OBJECT</literal>,
        <literal>SEQUENCE</literal>,
        <literal>TABLE</literal> (and table-like objects),
-       table column
+       table column,
+       <literal>SESSION VARIABLE</literal>
       </entry>
      </row>
      <row>
@@ -2319,7 +2323,8 @@ REVOKE ALL ON accounts FROM PUBLIC;
        <literal>LARGE OBJECT</literal>,
        <literal>SEQUENCE</literal>,
        <literal>TABLE</literal>,
-       table column
+       table column,
+       <literal>SESSION VARIABLE</literal>
       </entry>
      </row>
      <row>
@@ -2506,6 +2511,12 @@ REVOKE ALL ON accounts FROM PUBLIC;
       <entry><literal>U</literal></entry>
       <entry><literal>\dT+</literal></entry>
      </row>
+     <row>
+      <entry><literal>SESSION VARIABLE</literal></entry>
+      <entry><literal>rw</literal></entry>
+      <entry><literal>none</literal></entry>
+      <entry><literal>\dV+</literal></entry>
+     </row>
     </tbody>
    </tgroup>
   </table>
@@ -5375,6 +5386,10 @@ EXPLAIN SELECT count(*) FROM measurement WHERE logdate &gt;= DATE '2008-01-01';
 
    <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>
diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml
index c393832d94c..a57f7665054 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -740,6 +740,25 @@ SELECT has_function_privilege('joeuser', 'myfunc(int, text)', 'execute');
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>has_session_variable_privilege</primary>
+        </indexterm>
+        <function>has_session_variable_privilege</function> (
+          <optional> <parameter>user</parameter> <type>name</type> or <type>oid</type>, </optional>
+          <parameter>session_variable</parameter> <type>text</type> or <type>oid</type>,
+          <parameter>privilege</parameter> <type>text</type> )
+        <returnvalue>boolean</returnvalue>
+       </para>
+       <para>
+        Does user have privilege for session variable?
+        Allowable privilege types are
+        <literal>SELECT</literal>, and
+        <literal>UPDATE</literal>.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
@@ -1089,8 +1108,8 @@ SELECT has_function_privilege('joeuser', 'myfunc(int, text)', 'execute');
         't' for <literal>TABLESPACE</literal>,
         'F' for <literal>FOREIGN DATA WRAPPER</literal>,
         'S' for <literal>FOREIGN SERVER</literal>,
-        or
-        'T' for <literal>TYPE</literal> or <literal>DOMAIN</literal>.
+        'T' for <literal>TYPE</literal> or <literal>DOMAIN</literal> or
+        'V' for <literal>SESSION VARIABLE</literal>.
        </para></entry>
       </row>
 
diff --git a/doc/src/sgml/ref/alter_default_privileges.sgml b/doc/src/sgml/ref/alter_default_privileges.sgml
index 6acd0f1df91..bc73817061f 100644
--- a/doc/src/sgml/ref/alter_default_privileges.sgml
+++ b/doc/src/sgml/ref/alter_default_privileges.sgml
@@ -56,6 +56,11 @@ GRANT { { SELECT | UPDATE }
     ON LARGE OBJECTS
     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 ] }
@@ -95,6 +100,14 @@ REVOKE [ GRANT OPTION FOR ]
     ON LARGE OBJECTS
     FROM { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...]
     [ CASCADE | RESTRICT ]
+
+REVOKE [ GRANT OPTION FOR ]
+    { { SELECT | UPDATE }
+    [, ...] | ALL [ PRIVILEGES ] }
+    { { SELECT | UPDATE } [, ...] | ALL [ PRIVILEGES ] }
+    ON VARIABLES
+    FROM { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...]
+    [ CASCADE | RESTRICT ]
 </synopsis>
  </refsynopsisdiv>
 
@@ -129,14 +142,14 @@ REVOKE [ GRANT OPTION FOR ]
   <para>
    Currently,
    only the privileges for schemas, tables (including views and foreign
-   tables), sequences, functions, types (including domains), and large objects
-   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), large objects
+   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/grant.sgml b/doc/src/sgml/ref/grant.sgml
index 999f657d5c0..c11860fa200 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>
@@ -119,8 +125,8 @@ GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replace
    that grants privileges on a database object (table, column, view,
    foreign table, sequence, database, foreign-data wrapper, foreign server,
    function, procedure, procedural language, large object, configuration
-   parameter, schema, tablespace, or type), and one that grants
-   membership in a role.  These variants are similar in many ways, but
+   parameter, schema, session variable, tablespace, or type), and one that
+   grants membership in a role.  These variants are similar in many ways, but
    they are different enough to be described separately.
   </para>
 
@@ -236,9 +242,9 @@ GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replace
   <para>
    There is also an option to grant privileges on all objects of the same
    type within one or more schemas.  This functionality is currently supported
-   only for tables, sequences, functions, and procedures.  <literal>ALL
-   TABLES</literal> also affects views and foreign tables, just like the
-   specific-object <command>GRANT</command> command.  <literal>ALL
+   only for tables, sequences, functions, procedures and variables.
+   <literal>ALL TABLES</literal> also affects views and foreign tables, just
+   like the specific-object <command>GRANT</command> command.  <literal>ALL
    FUNCTIONS</literal> also affects aggregate and window functions, but not
    procedures, again just like the specific-object <command>GRANT</command>
    command.  Use <literal>ALL ROUTINES</literal> to include procedures.
@@ -518,8 +524,8 @@ GRANT admins TO joe;
    </para>
 
    <para>
-    Privileges on databases, tablespaces, schemas, languages, and
-    configuration parameters are
+    Privileges on databases, tablespaces, schemas, languages, session variables
+    and configuration parameters are
     <productname>PostgreSQL</productname> extensions.
    </para>
  </refsect1>
diff --git a/doc/src/sgml/ref/revoke.sgml b/doc/src/sgml/ref/revoke.sgml
index 8df492281a1..760fddb7c20 100644
--- a/doc/src/sgml/ref/revoke.sgml
+++ b/doc/src/sgml/ref/revoke.sgml
@@ -130,6 +130,14 @@ REVOKE [ { ADMIN | INHERIT | SET } OPTION FOR ]
     [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ]
     [ CASCADE | RESTRICT ]
 
+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 { <replaceable class="parameter">role_specification</replaceable> | PUBLIC } [, ...]
+    [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ]
+    [ CASCADE | RESTRICT ]
+
 <phrase>where <replaceable class="parameter">role_specification</replaceable> can be:</phrase>
 
     [ GROUP ] <replaceable class="parameter">role_name</replaceable>
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 00e3630e0ec..93c10beebe9 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/defrem.h"
 #include "commands/event_trigger.h"
 #include "commands/extension.h"
@@ -290,6 +291,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 */
@@ -534,6 +538,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);
@@ -639,6 +647,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);
@@ -773,6 +784,18 @@ objectNamesToOids(ObjectType objtype, List *objnames, bool is_grant)
 					objects = lappend_oid(objects, parameterId);
 			}
 			break;
+
+		case OBJECT_VARIABLE:
+			foreach_node(RangeVar, varvar, objnames)
+			{
+				Oid			relOid;
+
+				relOid = LookupVariable(varvar->schemaname,
+										varvar->relname,
+										false);
+				objects = lappend_oid(objects, relOid);
+			}
+			break;
 	}
 
 	return objects;
@@ -859,6 +882,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",
@@ -1022,6 +1071,10 @@ ExecAlterDefaultPrivilegesStmt(ParseState *pstate, AlterDefaultPrivilegesStmt *s
 			all_privileges = ACL_ALL_RIGHTS_LARGEOBJECT;
 			errormsg = gettext_noop("invalid privilege type %s for large object");
 			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);
@@ -1222,6 +1275,11 @@ SetDefaultACL(InternalDefaultACL *iacls)
 			if (iacls->all_privs && this_privileges == ACL_NO_RIGHTS)
 				this_privileges = ACL_ALL_RIGHTS_LARGEOBJECT;
 			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",
@@ -1469,6 +1527,9 @@ RemoveRoleFromObjectACL(Oid roleid, Oid classid, Oid objid)
 			case DEFACLOBJ_LARGEOBJECT:
 				iacls.objtype = OBJECT_LARGEOBJECT;
 				break;
+			case DEFACLOBJ_VARIABLE:
+				iacls.objtype = OBJECT_VARIABLE;
+				break;
 			default:
 				/* Shouldn't get here */
 				elog(ERROR, "unexpected default ACL type: %d",
@@ -1529,6 +1590,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;
@@ -2762,6 +2826,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;
@@ -2784,7 +2851,6 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
 					case OBJECT_TSPARSER:
 					case OBJECT_TSTEMPLATE:
 					case OBJECT_USER_MAPPING:
-					case OBJECT_VARIABLE:
 						elog(ERROR, "unsupported object type: %d", objtype);
 				}
 
@@ -3025,6 +3091,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);
@@ -4288,6 +4356,10 @@ get_user_default_acl(ObjectType objtype, Oid ownerId, Oid nsp_oid)
 			defaclobjtype = DEFACLOBJ_LARGEOBJECT;
 			break;
 
+		case OBJECT_VARIABLE:
+			defaclobjtype = DEFACLOBJ_VARIABLE;
+			break;
+
 		default:
 			return NULL;
 	}
diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c
index 3817efb1eeb..333be0b2ced 100644
--- a/src/backend/catalog/objectaddress.c
+++ b/src/backend/catalog/objectaddress.c
@@ -2029,17 +2029,21 @@ get_object_address_defacl(List *object, bool missing_ok)
 		case DEFACLOBJ_LARGEOBJECT:
 			objtype_str = "large objects";
 			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\", \"%c\".",
+					 errhint("Valid object types are \"%c\", \"%c\", \"%c\", \"%c\", \"%c\", \"%c\", \"%c\".",
 							 DEFACLOBJ_RELATION,
 							 DEFACLOBJ_SEQUENCE,
 							 DEFACLOBJ_FUNCTION,
 							 DEFACLOBJ_TYPE,
 							 DEFACLOBJ_NAMESPACE,
-							 DEFACLOBJ_LARGEOBJECT)));
+							 DEFACLOBJ_LARGEOBJECT,
+							 DEFACLOBJ_VARIABLE)));
 	}
 
 	/*
@@ -3921,6 +3925,16 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok)
 										 _("default privileges on new large objects 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)
@@ -5851,6 +5865,10 @@ getObjectIdentityParts(const ObjectAddress *object,
 						appendStringInfoString(&buffer,
 											   " on large objects");
 						break;
+					case DEFACLOBJ_VARIABLE:
+						appendStringInfoString(&buffer,
+											   " on session variables");
+						break;
 				}
 
 				if (objname)
diff --git a/src/backend/catalog/pg_variable.c b/src/backend/catalog/pg_variable.c
index bd6a29a79e5..d8ede4fa8c8 100644
--- a/src/backend/catalog/pg_variable.c
+++ b/src/backend/catalog/pg_variable.c
@@ -38,6 +38,7 @@ create_variable(const char *varName,
 				Oid varCollation,
 				bool if_not_exists)
 {
+	Acl		   *varacl;
 	NameData	varname;
 	bool		nulls[Natts_pg_variable];
 	Datum		values[Natts_pg_variable];
@@ -97,7 +98,12 @@ create_variable(const char *varName,
 	values[Anum_pg_variable_varowner - 1] = ObjectIdGetDatum(varOwner);
 	values[Anum_pg_variable_varcollation - 1] = ObjectIdGetDatum(varCollation);
 
-	nulls[Anum_pg_variable_varacl - 1] = true;
+	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);
 
@@ -131,6 +137,9 @@ create_variable(const char *varName,
 	/* 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);
 
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 3140c772178..abc365c0cfa 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -784,7 +784,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	UNLISTEN UNLOGGED UNTIL UPDATE USER USING
 
 	VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIADIC VARIABLE
-	VARYING VERBOSE VERSION_P VIEW VIEWS VIRTUAL VOLATILE
+	VARIABLES VARYING VERBOSE VERSION_P VIEW VIEWS VIRTUAL VOLATILE
 
 	WHEN WHERE WHITESPACE_P WINDOW WITH WITHIN WITHOUT WORK WRAPPER WRITE
 
@@ -8058,6 +8058,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));
@@ -8103,6 +8111,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;
+				}
 		;
 
 
@@ -8301,6 +8317,7 @@ defacl_privilege_target:
 			| TYPES_P		{ $$ = OBJECT_TYPE; }
 			| SCHEMAS		{ $$ = OBJECT_SCHEMA; }
 			| LARGE_P OBJECTS_P	{ $$ = OBJECT_LARGEOBJECT; }
+			| VARIABLES		{ $$ = OBJECT_VARIABLE; }
 		;
 
 
@@ -18121,6 +18138,7 @@ unreserved_keyword:
 			| VALIDATOR
 			| VALUE_P
 			| VARIABLE
+			| VARIABLES
 			| VARYING
 			| VERSION_P
 			| VIEW
@@ -18778,6 +18796,7 @@ bare_label_keyword:
 			| VALUES
 			| VARCHAR
 			| VARIABLE
+			| VARIABLES
 			| VARIADIC
 			| VERBOSE
 			| VERSION_P
diff --git a/src/backend/utils/adt/acl.c b/src/backend/utils/adt/acl.c
index 7dadaefdfc1..e4522b72830 100644
--- a/src/backend/utils/adt/acl.c
+++ b/src/backend/utils/adt/acl.c
@@ -31,6 +31,7 @@
 #include "catalog/pg_proc.h"
 #include "catalog/pg_tablespace.h"
 #include "catalog/pg_type.h"
+#include "catalog/pg_variable.h"
 #include "commands/proclang.h"
 #include "commands/tablespace.h"
 #include "common/hashfn.h"
@@ -128,6 +129,8 @@ static AclMode convert_type_priv_string(text *priv_type_text);
 static AclMode convert_parameter_priv_string(text *priv_text);
 static AclMode convert_largeobject_priv_string(text *priv_type_text);
 static AclMode convert_role_priv_string(text *priv_type_text);
+static Oid	convert_session_variable_name(text *varname);
+static AclMode convert_session_variable_priv_string(text *priv_type_text);
 static AclResult pg_role_aclcheck(Oid role_oid, Oid roleid, AclMode mode);
 
 static void RoleMembershipCacheCallback(Datum arg, int cacheid, uint32 hashvalue);
@@ -867,6 +870,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 */
@@ -964,6 +971,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);
 	}
@@ -5032,6 +5042,217 @@ pg_role_aclcheck(Oid role_oid, Oid roleid, AclMode mode)
 	return ACLCHECK_NO_PRIV;
 }
 
+/*
+ * has_session_variable_privilege variants
+ *		These are all named "has_session_variable_privilege" at the SQL level.
+ *		They take various combinations of variable name, variable OID,
+ *		user name, user OID, or implicit user = current_user.
+ *
+ *		The result is a boolean value: true if user has the indicated
+ *		privilege, false if not, or NULL if session variable doesn't
+ *		exists.
+ */
+
+/*
+ * has_session_variable_privilege_name_name
+ *		Check user privileges on a session variable given
+ *		name username, text session variable name, and text priv name.
+ */
+Datum
+has_session_variable_privilege_name_name(PG_FUNCTION_ARGS)
+{
+	Name		rolename = PG_GETARG_NAME(0);
+	text	   *varname = PG_GETARG_TEXT_PP(1);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(2);
+	Oid			roleid;
+	Oid			varid;
+	AclMode		mode;
+	AclResult	aclresult;
+	bool		is_missing = false;
+
+	roleid = get_role_oid_or_public(NameStr(*rolename));
+	mode = convert_session_variable_priv_string(priv_type_text);
+	varid = convert_session_variable_name(varname);
+
+	aclresult = object_aclcheck_ext(VariableRelationId, varid,
+									roleid, mode,
+									&is_missing);
+
+	if (is_missing)
+		PG_RETURN_NULL();
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_session_variable_privilege_name
+ *		Check user privileges on a session variable given
+ *		text session variable and text priv name.
+ *		current_user is assumed
+ */
+Datum
+has_session_variable_privilege_name(PG_FUNCTION_ARGS)
+{
+	text	   *varname = PG_GETARG_TEXT_PP(0);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(1);
+	Oid			roleid;
+	Oid			varid;
+	AclMode		mode;
+	AclResult	aclresult;
+	bool		is_missing = false;
+
+	roleid = GetUserId();
+	mode = convert_session_variable_priv_string(priv_type_text);
+	varid = convert_session_variable_name(varname);
+
+	aclresult = object_aclcheck_ext(VariableRelationId, varid,
+									roleid, mode,
+									&is_missing);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_session_variable_privilege_name_id
+ *		Check user privileges on a session variable given
+ *		name usename, session variable oid, and text priv name.
+ */
+Datum
+has_session_variable_privilege_name_id(PG_FUNCTION_ARGS)
+{
+	Name		username = PG_GETARG_NAME(0);
+	Oid			varid = PG_GETARG_OID(1);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(2);
+	Oid			roleid;
+	AclMode		mode;
+	AclResult	aclresult;
+	bool		is_missing = false;
+
+	roleid = get_role_oid_or_public(NameStr(*username));
+	mode = convert_session_variable_priv_string(priv_type_text);
+
+	aclresult = object_aclcheck_ext(VariableRelationId, varid,
+									roleid, mode,
+									&is_missing);
+
+	if (is_missing)
+		PG_RETURN_NULL();
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_session_variable_privilege_id
+ *		Check user privileges on a session variable given
+ *		session variable oid, and text priv name.
+ *		current_user is assumed
+ */
+Datum
+has_session_variable_privilege_id(PG_FUNCTION_ARGS)
+{
+	Oid			varid = PG_GETARG_OID(0);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(1);
+	Oid			roleid;
+	AclMode		mode;
+	AclResult	aclresult;
+	bool		is_missing = false;
+
+	roleid = GetUserId();
+	mode = convert_session_variable_priv_string(priv_type_text);
+
+	aclresult = object_aclcheck_ext(VariableRelationId, varid,
+									roleid, mode,
+									&is_missing);
+
+	if (is_missing)
+		PG_RETURN_NULL();
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_session_variable_privilege_id_name
+ *		Check user privileges on a session variable given
+ *		roleid, text session variable name, and text priv name.
+ */
+Datum
+has_session_variable_privilege_id_name(PG_FUNCTION_ARGS)
+{
+	Oid			roleid = PG_GETARG_OID(0);
+	text	   *varname = PG_GETARG_TEXT_PP(1);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(2);
+	Oid			varid;
+	AclMode		mode;
+	AclResult	aclresult;
+	bool		is_missing = false;
+
+	mode = convert_session_variable_priv_string(priv_type_text);
+	varid = convert_session_variable_name(varname);
+
+	aclresult = object_aclcheck_ext(VariableRelationId, varid,
+									roleid, mode,
+									&is_missing);
+
+	if (is_missing)
+		PG_RETURN_NULL();
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_session_variable_privilege_id_id
+ *		Check user privileges on a session variable given
+ *		roleid, session variable oid, and text priv name.
+ */
+Datum
+has_session_variable_privilege_id_id(PG_FUNCTION_ARGS)
+{
+	Oid			roleid = PG_GETARG_OID(0);
+	Oid			varid = PG_GETARG_OID(1);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(2);
+	AclMode		mode;
+	AclResult	aclresult;
+	bool		is_missing = false;
+
+	mode = convert_session_variable_priv_string(priv_type_text);
+
+	aclresult = object_aclcheck_ext(VariableRelationId, varid,
+									roleid, mode,
+									&is_missing);
+
+	if (is_missing)
+		PG_RETURN_NULL();
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * Given a session variable name expressed as a string, look it up and return
+ * Oid
+ */
+static Oid
+convert_session_variable_name(text *varname)
+{
+	return LookupVariableFromNameList(textToQualifiedNameList(varname), true);
+}
+
+/*
+ * convert_variable_priv_string
+ *		Convert text string to AclMode value.
+ */
+static AclMode
+convert_session_variable_priv_string(text *priv_type_text)
+{
+	static const priv_map session_variable_priv_map[] = {
+		{"SELECT", ACL_SELECT},
+		{"SELECT WITH GRANT OPTION", ACL_GRANT_OPTION_FOR(ACL_SELECT)},
+		{"UPDATE", ACL_UPDATE},
+		{"UPDATE WITH GRANT OPTION", ACL_GRANT_OPTION_FOR(ACL_UPDATE)},
+		{NULL, 0}
+	};
+
+	return convert_any_priv_string(priv_type_text, session_variable_priv_map);
+}
 
 /*
  * initialization function (called by InitPostgres)
diff --git a/src/include/catalog/pg_default_acl.h b/src/include/catalog/pg_default_acl.h
index ce6e5098eaf..087d35b943d 100644
--- a/src/include/catalog/pg_default_acl.h
+++ b/src/include/catalog/pg_default_acl.h
@@ -69,6 +69,7 @@ MAKE_SYSCACHE(DEFACLROLENSPOBJ, pg_default_acl_role_nsp_obj_index, 8);
 #define DEFACLOBJ_TYPE			'T' /* type */
 #define DEFACLOBJ_NAMESPACE		'n' /* namespace */
 #define DEFACLOBJ_LARGEOBJECT	'L' /* large object */
+#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 118d6da1ace..7fa5f7366c1 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5467,6 +5467,26 @@
   prorettype => 'bool', proargtypes => 'oid oid text',
   prosrc => 'has_largeobject_privilege_id_id' },
 
+{ oid => '9613', descr => 'user privilege on session variable by username, seq name',
+  proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool',
+  proargtypes => 'name text text',
+  prosrc => 'has_session_variable_privilege_name_name' },
+{ oid => '9614', descr => 'user privilege on session variable by username, seq oid',
+  proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool',
+  proargtypes => 'name oid text', prosrc => 'has_session_variable_privilege_name_id' },
+{ oid => '9615', descr => 'user privilege on session variable by user oid, seq name',
+  proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool',
+  proargtypes => 'oid text text', prosrc => 'has_session_variable_privilege_id_name' },
+{ oid => '9616', descr => 'user privilege on session variable by user oid, seq oid',
+  proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool',
+  proargtypes => 'oid oid text', prosrc => 'has_session_variable_privilege_id_id' },
+{ oid => '9617', descr => 'current user privilege on session variable by seq name',
+  proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool',
+  proargtypes => 'text text', prosrc => 'has_session_variable_privilege_name' },
+{ oid => '9618', descr => 'current user privilege on session variable by seq oid',
+  proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool',
+  proargtypes => 'oid text', prosrc => 'has_session_variable_privilege_id' },
+
 { oid => '3355', descr => 'I/O',
   proname => 'pg_ndistinct_in', prorettype => 'pg_ndistinct',
   proargtypes => 'cstring', prosrc => 'pg_ndistinct_in' },
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index 6f513f04225..0ea0265de7c 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -487,6 +487,7 @@ 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/utils/acl.h b/src/include/utils/acl.h
index 01ae5b719fd..5e1a8a82e90 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/test/regress/expected/session_variables_acl.out b/src/test/regress/expected/session_variables_acl.out
new file mode 100644
index 00000000000..478b47472f0
--- /dev/null
+++ b/src/test/regress/expected/session_variables_acl.out
@@ -0,0 +1,307 @@
+-- check access rights and supported ALTER
+CREATE SCHEMA svartest_acl;
+CREATE ROLE regress_variable_owner_acl;
+CREATE ROLE regress_variable_reader_acl;
+GRANT ALL ON SCHEMA svartest_acl TO regress_variable_owner_acl;
+GRANT ALL ON SCHEMA public TO regress_variable_owner_acl;
+ALTER DEFAULT PRIVILEGES
+   FOR ROLE regress_variable_owner_acl
+   IN SCHEMA svartest_acl
+   GRANT SELECT ON VARIABLES TO regress_variable_reader_acl;
+-- creating variable with default privileges
+SET ROLE TO regress_variable_owner_acl;
+CREATE VARIABLE svartest_acl.sesvar20 AS int;
+SET ROLE TO DEFAULT;
+-- should be ok. since ALTER DEFAULT PRIVILEGES
+-- allow regress_variable_reader_acl to have SELECT priviledge
+SELECT has_session_variable_privilege('regress_variable_reader_acl', 'svartest_acl.sesvar20', 'SELECT'); -- t
+ has_session_variable_privilege 
+--------------------------------
+ t
+(1 row)
+
+DROP VARIABLE svartest_acl.sesvar20;
+DROP SCHEMA svartest_acl;
+DROP ROLE regress_variable_reader_acl;
+--
+-- begin of check GRANT WITH GRANT OPTION and REVOKE GRANTED BY
+--
+CREATE ROLE regress_variable_r1_acl;
+CREATE ROLE regress_variable_r2_acl;
+SET ROLE TO regress_variable_owner_acl;
+CREATE VARIABLE sesvar22_acl AS int;      --sesvar22_acl will owned by regress_variable_owner_acl
+GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r1_acl WITH GRANT OPTION;
+SET ROLE TO regress_variable_r1_acl;
+GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r2_acl WITH GRANT OPTION;
+SET ROLE TO DEFAULT;
+SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- t
+ has_session_variable_privilege 
+--------------------------------
+ t
+(1 row)
+
+SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- t
+ has_session_variable_privilege 
+--------------------------------
+ t
+(1 row)
+
+REVOKE ALL PRIVILEGES ON VARIABLE sesvar22_acl FROM regress_variable_r1_acl CASCADE;
+SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f
+ has_session_variable_privilege 
+--------------------------------
+ f
+(1 row)
+
+SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- f
+ has_session_variable_privilege 
+--------------------------------
+ f
+(1 row)
+
+SET ROLE TO regress_variable_owner_acl;
+GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r2_acl;
+SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f
+ has_session_variable_privilege 
+--------------------------------
+ f
+(1 row)
+
+SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- t
+ has_session_variable_privilege 
+--------------------------------
+ t
+(1 row)
+
+REVOKE ALL ON VARIABLE sesvar22_acl FROM regress_variable_r2_acl GRANTED BY regress_variable_owner_acl;
+SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f
+ has_session_variable_privilege 
+--------------------------------
+ f
+(1 row)
+
+SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- f
+ has_session_variable_privilege 
+--------------------------------
+ f
+(1 row)
+
+SELECT has_session_variable_privilege('regress_variable_owner_acl', 'public.sesvar22_acl', 'SELECT'); -- t
+ has_session_variable_privilege 
+--------------------------------
+ t
+(1 row)
+
+SET ROLE TO DEFAULT;
+DROP VARIABLE sesvar22_acl;
+--
+-- end of check GRANT WITH GRANT OPTION and REVOKE GRANTED BY
+--
+--
+-- begin of test: GRANT|REVOKE SELECT|UPDATE ON ALL VARIABLES IN SCHEMA
+--
+CREATE SCHEMA svartest_acl;
+GRANT ALL ON SCHEMA svartest_acl TO regress_variable_owner_acl;
+SET ROLE TO regress_variable_owner_acl;
+CREATE VARIABLE svartest_acl.sesvar20 AS int;
+CREATE VARIABLE svartest_acl.sesvar21 AS int;
+GRANT SELECT ON ALL VARIABLES IN SCHEMA svartest_acl TO regress_variable_r1_acl;
+SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar20', 'SELECT'); -- t
+ has_session_variable_privilege 
+--------------------------------
+ t
+(1 row)
+
+SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar21', 'SELECT'); -- t
+ has_session_variable_privilege 
+--------------------------------
+ t
+(1 row)
+
+REVOKE SELECT ON ALL VARIABLES IN SCHEMA svartest_acl FROM regress_variable_r1_acl;
+SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar20', 'SELECT'); -- f
+ has_session_variable_privilege 
+--------------------------------
+ f
+(1 row)
+
+SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar21', 'SELECT'); -- f
+ has_session_variable_privilege 
+--------------------------------
+ f
+(1 row)
+
+SET ROLE TO DEFAULT;
+DROP VARIABLE svartest_acl.sesvar20;
+DROP VARIABLE svartest_acl.sesvar21;
+DROP SCHEMA svartest_acl;
+--
+-- end of test: GRANT|REVOKE SELECT|UPDATE ON ALL VARIABLES IN SCHEMA
+--
+--
+-- function has_session_variable_privilege have various kind of signature.
+-- the following are extensive test for it.
+--
+SET ROLE TO regress_variable_owner_acl;
+CREATE VARIABLE public.sesvar22_acl AS int;
+SET search_path TO public;
+GRANT SELECT ON VARIABLE public.sesvar22_acl TO regress_variable_r1_acl;
+GRANT SELECT, UPDATE ON VARIABLE public.sesvar22_acl TO regress_variable_r2_acl;
+SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT');
+ has_session_variable_privilege 
+--------------------------------
+ t
+(1 row)
+
+SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.notexists', 'SELECT') IS NULL;
+ ?column? 
+----------
+ t
+(1 row)
+
+SET ROLE TO regress_variable_r1_acl;
+SELECT has_session_variable_privilege('regress_variable_r1_acl', 'sesvar22_acl', 'SELECT'); -- t
+ has_session_variable_privilege 
+--------------------------------
+ t
+(1 row)
+
+SELECT has_session_variable_privilege('regress_variable_r1_acl', 'sesvar22_acl', 'UPDATE'); -- f
+ has_session_variable_privilege 
+--------------------------------
+ f
+(1 row)
+
+SELECT has_session_variable_privilege('regress_variable_r2_acl', 'sesvar22_acl', 'SELECT'); -- t
+ has_session_variable_privilege 
+--------------------------------
+ t
+(1 row)
+
+SELECT has_session_variable_privilege('regress_variable_r2_acl', 'sesvar22_acl', 'UPDATE'); -- t
+ has_session_variable_privilege 
+--------------------------------
+ t
+(1 row)
+
+SELECT has_session_variable_privilege('sesvar22_acl', 'SELECT'); -- t
+ has_session_variable_privilege 
+--------------------------------
+ t
+(1 row)
+
+SELECT has_session_variable_privilege('sesvar22_acl', 'UPDATE'); -- f
+ has_session_variable_privilege 
+--------------------------------
+ f
+(1 row)
+
+SELECT oid AS varid
+  FROM pg_variable
+  WHERE varname = 'sesvar22_acl' AND varnamespace = 'public'::regnamespace \gset
+SELECT has_session_variable_privilege('sesvar22_acl', 'SELECT'); -- t
+ has_session_variable_privilege 
+--------------------------------
+ t
+(1 row)
+
+SELECT has_session_variable_privilege('sesvar22_acl', 'UPDATE'); -- f
+ has_session_variable_privilege 
+--------------------------------
+ f
+(1 row)
+
+SELECT has_session_variable_privilege('regress_variable_r1_acl', :varid, 'SELECT'); -- t
+ has_session_variable_privilege 
+--------------------------------
+ t
+(1 row)
+
+SELECT has_session_variable_privilege('regress_variable_r1_acl', :varid, 'UPDATE'); -- f
+ has_session_variable_privilege 
+--------------------------------
+ f
+(1 row)
+
+SELECT has_session_variable_privilege('regress_variable_r2_acl', :varid, 'SELECT'); -- t
+ has_session_variable_privilege 
+--------------------------------
+ t
+(1 row)
+
+SELECT has_session_variable_privilege('regress_variable_r2_acl', :varid, 'UPDATE'); -- t
+ has_session_variable_privilege 
+--------------------------------
+ t
+(1 row)
+
+SELECT has_session_variable_privilege(:varid, 'SELECT'); -- t
+ has_session_variable_privilege 
+--------------------------------
+ t
+(1 row)
+
+SELECT has_session_variable_privilege(:varid, 'UPDATE'); -- f
+ has_session_variable_privilege 
+--------------------------------
+ f
+(1 row)
+
+SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, 'sesvar22_acl', 'SELECT'); -- t
+ has_session_variable_privilege 
+--------------------------------
+ t
+(1 row)
+
+SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, 'sesvar22_acl', 'UPDATE'); -- f
+ has_session_variable_privilege 
+--------------------------------
+ f
+(1 row)
+
+SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, 'sesvar22_acl', 'SELECT'); -- t
+ has_session_variable_privilege 
+--------------------------------
+ t
+(1 row)
+
+SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, 'sesvar22_acl', 'UPDATE'); -- t
+ has_session_variable_privilege 
+--------------------------------
+ t
+(1 row)
+
+SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, :varid, 'SELECT'); -- t
+ has_session_variable_privilege 
+--------------------------------
+ t
+(1 row)
+
+SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, :varid, 'UPDATE'); -- f
+ has_session_variable_privilege 
+--------------------------------
+ f
+(1 row)
+
+SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, :varid, 'SELECT'); -- t
+ has_session_variable_privilege 
+--------------------------------
+ t
+(1 row)
+
+SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, :varid, 'UPDATE'); -- t
+ has_session_variable_privilege 
+--------------------------------
+ t
+(1 row)
+
+--
+-- end of function has_session_variable_privilege tests.
+--
+SET ROLE TO DEFAULT;
+SET search_path TO DEFAULT;
+DROP VARIABLE public.sesvar22_acl;
+DROP ROLE regress_variable_r1_acl;
+DROP ROLE regress_variable_r2_acl;
+REVOKE ALL ON SCHEMA public FROM regress_variable_owner_acl;
+DROP ROLE regress_variable_owner_acl;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 3abc1aca5f2..95c76baf9ae 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -115,7 +115,7 @@ test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson
 # NB: temp.sql does reconnects which transiently use 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 session_variables_ddl
+test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml session_variables_ddl session_variables_acl
 
 # ----------
 # Another group of parallel tests
diff --git a/src/test/regress/sql/session_variables_acl.sql b/src/test/regress/sql/session_variables_acl.sql
new file mode 100644
index 00000000000..e1a9d5ce5f4
--- /dev/null
+++ b/src/test/regress/sql/session_variables_acl.sql
@@ -0,0 +1,158 @@
+-- check access rights and supported ALTER
+CREATE SCHEMA svartest_acl;
+CREATE ROLE regress_variable_owner_acl;
+CREATE ROLE regress_variable_reader_acl;
+
+GRANT ALL ON SCHEMA svartest_acl TO regress_variable_owner_acl;
+GRANT ALL ON SCHEMA public TO regress_variable_owner_acl;
+
+ALTER DEFAULT PRIVILEGES
+   FOR ROLE regress_variable_owner_acl
+   IN SCHEMA svartest_acl
+   GRANT SELECT ON VARIABLES TO regress_variable_reader_acl;
+
+-- creating variable with default privileges
+SET ROLE TO regress_variable_owner_acl;
+CREATE VARIABLE svartest_acl.sesvar20 AS int;
+SET ROLE TO DEFAULT;
+
+-- should be ok. since ALTER DEFAULT PRIVILEGES
+-- allow regress_variable_reader_acl to have SELECT priviledge
+SELECT has_session_variable_privilege('regress_variable_reader_acl', 'svartest_acl.sesvar20', 'SELECT'); -- t
+
+DROP VARIABLE svartest_acl.sesvar20;
+DROP SCHEMA svartest_acl;
+DROP ROLE regress_variable_reader_acl;
+
+--
+-- begin of check GRANT WITH GRANT OPTION and REVOKE GRANTED BY
+--
+CREATE ROLE regress_variable_r1_acl;
+CREATE ROLE regress_variable_r2_acl;
+
+SET ROLE TO regress_variable_owner_acl;
+CREATE VARIABLE sesvar22_acl AS int;      --sesvar22_acl will owned by regress_variable_owner_acl
+
+GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r1_acl WITH GRANT OPTION;
+SET ROLE TO regress_variable_r1_acl;
+GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r2_acl WITH GRANT OPTION;
+SET ROLE TO DEFAULT;
+
+SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- t
+SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- t
+
+REVOKE ALL PRIVILEGES ON VARIABLE sesvar22_acl FROM regress_variable_r1_acl CASCADE;
+
+SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f
+SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- f
+
+SET ROLE TO regress_variable_owner_acl;
+GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r2_acl;
+
+SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f
+SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- t
+
+REVOKE ALL ON VARIABLE sesvar22_acl FROM regress_variable_r2_acl GRANTED BY regress_variable_owner_acl;
+
+SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f
+SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- f
+SELECT has_session_variable_privilege('regress_variable_owner_acl', 'public.sesvar22_acl', 'SELECT'); -- t
+SET ROLE TO DEFAULT;
+
+DROP VARIABLE sesvar22_acl;
+--
+-- end of check GRANT WITH GRANT OPTION and REVOKE GRANTED BY
+--
+
+--
+-- begin of test: GRANT|REVOKE SELECT|UPDATE ON ALL VARIABLES IN SCHEMA
+--
+CREATE SCHEMA svartest_acl;
+GRANT ALL ON SCHEMA svartest_acl TO regress_variable_owner_acl;
+SET ROLE TO regress_variable_owner_acl;
+
+CREATE VARIABLE svartest_acl.sesvar20 AS int;
+CREATE VARIABLE svartest_acl.sesvar21 AS int;
+
+GRANT SELECT ON ALL VARIABLES IN SCHEMA svartest_acl TO regress_variable_r1_acl;
+
+SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar20', 'SELECT'); -- t
+SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar21', 'SELECT'); -- t
+
+REVOKE SELECT ON ALL VARIABLES IN SCHEMA svartest_acl FROM regress_variable_r1_acl;
+
+SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar20', 'SELECT'); -- f
+SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar21', 'SELECT'); -- f
+
+SET ROLE TO DEFAULT;
+DROP VARIABLE svartest_acl.sesvar20;
+DROP VARIABLE svartest_acl.sesvar21;
+DROP SCHEMA svartest_acl;
+--
+-- end of test: GRANT|REVOKE SELECT|UPDATE ON ALL VARIABLES IN SCHEMA
+--
+
+--
+-- function has_session_variable_privilege have various kind of signature.
+-- the following are extensive test for it.
+--
+SET ROLE TO regress_variable_owner_acl;
+
+CREATE VARIABLE public.sesvar22_acl AS int;
+
+SET search_path TO public;
+
+GRANT SELECT ON VARIABLE public.sesvar22_acl TO regress_variable_r1_acl;
+GRANT SELECT, UPDATE ON VARIABLE public.sesvar22_acl TO regress_variable_r2_acl;
+
+SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT');
+SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.notexists', 'SELECT') IS NULL;
+
+SET ROLE TO regress_variable_r1_acl;
+
+SELECT has_session_variable_privilege('regress_variable_r1_acl', 'sesvar22_acl', 'SELECT'); -- t
+SELECT has_session_variable_privilege('regress_variable_r1_acl', 'sesvar22_acl', 'UPDATE'); -- f
+SELECT has_session_variable_privilege('regress_variable_r2_acl', 'sesvar22_acl', 'SELECT'); -- t
+SELECT has_session_variable_privilege('regress_variable_r2_acl', 'sesvar22_acl', 'UPDATE'); -- t
+
+SELECT has_session_variable_privilege('sesvar22_acl', 'SELECT'); -- t
+SELECT has_session_variable_privilege('sesvar22_acl', 'UPDATE'); -- f
+
+SELECT oid AS varid
+  FROM pg_variable
+  WHERE varname = 'sesvar22_acl' AND varnamespace = 'public'::regnamespace \gset
+
+SELECT has_session_variable_privilege('sesvar22_acl', 'SELECT'); -- t
+SELECT has_session_variable_privilege('sesvar22_acl', 'UPDATE'); -- f
+
+SELECT has_session_variable_privilege('regress_variable_r1_acl', :varid, 'SELECT'); -- t
+SELECT has_session_variable_privilege('regress_variable_r1_acl', :varid, 'UPDATE'); -- f
+SELECT has_session_variable_privilege('regress_variable_r2_acl', :varid, 'SELECT'); -- t
+SELECT has_session_variable_privilege('regress_variable_r2_acl', :varid, 'UPDATE'); -- t
+
+SELECT has_session_variable_privilege(:varid, 'SELECT'); -- t
+SELECT has_session_variable_privilege(:varid, 'UPDATE'); -- f
+
+SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, 'sesvar22_acl', 'SELECT'); -- t
+SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, 'sesvar22_acl', 'UPDATE'); -- f
+SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, 'sesvar22_acl', 'SELECT'); -- t
+SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, 'sesvar22_acl', 'UPDATE'); -- t
+
+SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, :varid, 'SELECT'); -- t
+SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, :varid, 'UPDATE'); -- f
+SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, :varid, 'SELECT'); -- t
+SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, :varid, 'UPDATE'); -- t
+--
+-- end of function has_session_variable_privilege tests.
+--
+
+SET ROLE TO DEFAULT;
+SET search_path TO DEFAULT;
+
+DROP VARIABLE public.sesvar22_acl;
+
+DROP ROLE regress_variable_r1_acl;
+DROP ROLE regress_variable_r2_acl;
+
+REVOKE ALL ON SCHEMA public FROM regress_variable_owner_acl;
+DROP ROLE regress_variable_owner_acl;
-- 
2.51.0



  [text/x-patch] v20250829-0004-support-of-session-variables-for-psql.patch (22.8K, 15-v20250829-0004-support-of-session-variables-for-psql.patch)
  download | inline diff:
From 6ba78e3f6520b983888822114e23a9f97bfeab95 Mon Sep 17 00:00:00 2001
From: "[email protected]" <[email protected]>
Date: Tue, 5 Aug 2025 06:42:02 +0200
Subject: [PATCH 04/15] support of session variables for psql

This patch enhancing psql to support session variables:

 * \dV[+] command
 * tab complete for CREATE, DROP, ALTER VARIABLE

Note: tab complete for variable fencing is not supported yet
---
 doc/src/sgml/func/func-info.sgml   |  13 ++++
 doc/src/sgml/ref/psql-ref.sgml     |  13 ++++
 src/backend/catalog/namespace.c    |  14 ++++
 src/bin/psql/command.c             |   3 +
 src/bin/psql/describe.c            | 100 ++++++++++++++++++++++++++++-
 src/bin/psql/describe.h            |   3 +
 src/bin/psql/help.c                |   1 +
 src/bin/psql/tab-complete.in.c     |  45 +++++++++++--
 src/include/catalog/pg_proc.dat    |   3 +
 src/test/regress/expected/psql.out |  50 +++++++++++++++
 src/test/regress/sql/psql.sql      |  21 ++++++
 11 files changed, 259 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml
index a57f7665054..3aad9d0529f 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -1377,6 +1377,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/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 1a339600bc4..6bb3a9dad50 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -2144,6 +2144,19 @@ SELECT $1 \parse stmt1
         </listitem>
       </varlistentry>
 
+      <varlistentry id="app-psql-meta-command-dv">
+        <term><literal>\dV[+] [ <link linkend="app-psql-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term>
+        <listitem>
+        <para>
+        Lists session variables.
+        If <replaceable class="parameter">pattern</replaceable> is
+        specified, only session variables whose names match the pattern are listed.
+        If the form <literal>\dV+</literal> is used, additional information
+        about each variable is shown, like  access privileges and description.
+        </para>
+        </listitem>
+      </varlistentry>
+
       <varlistentry id="app-psql-meta-command-du">
         <term><literal>\du[Sx+] [ <link linkend="app-psql-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term>
         <listitem>
diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c
index ea49a0bc82d..78cabf964ef 100644
--- a/src/backend/catalog/namespace.c
+++ b/src/backend/catalog/namespace.c
@@ -5291,3 +5291,17 @@ 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);
+	bool		result;
+	bool		is_missing = false;
+
+	result = VariableIsVisibleExt(oid, &is_missing);
+
+	if (is_missing)
+		PG_RETURN_NULL();
+	PG_RETURN_BOOL(result);
+}
diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index cc602087db2..0ac7b9881cd 100644
--- a/src/bin/psql/command.c
+++ b/src/bin/psql/command.c
@@ -1270,6 +1270,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 7a06af48842..4e89624f255 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -1224,7 +1224,7 @@ listDefaultACLs(const char *pattern)
 					  "SELECT pg_catalog.pg_get_userbyid(d.defaclrole) AS \"%s\",\n"
 					  "  n.nspname AS \"%s\",\n"
 					  "  CASE d.defaclobjtype "
-					  "    WHEN '%c' THEN '%s' WHEN '%c' THEN '%s' WHEN '%c' THEN '%s'"
+					  "    WHEN '%c' THEN '%s' WHEN '%c' THEN '%s' WHEN '%c' THEN '%s' WHEN '%c' THEN '%s'"
 					  "    WHEN '%c' THEN '%s' WHEN '%c' THEN '%s' WHEN '%c' THEN '%s' END AS \"%s\",\n"
 					  "  ",
 					  gettext_noop("Owner"),
@@ -1241,6 +1241,8 @@ listDefaultACLs(const char *pattern)
 					  gettext_noop("schema"),
 					  DEFACLOBJ_LARGEOBJECT,
 					  gettext_noop("large object"),
+					  DEFACLOBJ_VARIABLE,
+					  gettext_noop("session variable"),
 					  gettext_noop("Type"));
 
 	printACLColumn(&buf, "d.defaclacl");
@@ -5314,6 +5316,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 18ecaa60949..55ced4aab7b 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 ed00c36695e..aa91a7ce10f 100644
--- a/src/bin/psql/help.c
+++ b/src/bin/psql/help.c
@@ -266,6 +266,7 @@ slashUsage(unsigned short int pager)
 	HELP0("  \\dT[Sx+] [PATTERN]     list data types\n");
 	HELP0("  \\du[Sx+] [PATTERN]     list roles\n");
 	HELP0("  \\dv[Sx+] [PATTERN]     list views\n");
+	HELP0("  \\dV[x+]  [PATTERN]     list session variables\n");
 	HELP0("  \\dx[x+]  [PATTERN]     list extensions\n");
 	HELP0("  \\dX[x]   [PATTERN]     list extended statistics\n");
 	HELP0("  \\dy[x+]  [PATTERN]     list event triggers\n");
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 8b10f2313f3..f37144e437e 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -978,6 +978,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
@@ -1342,6 +1349,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 */
 };
@@ -1907,7 +1915,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", "\\endpipeline", "\\errverbose", "\\ev",
 		"\\f", "\\flush", "\\flushrequest",
@@ -2620,6 +2628,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");
@@ -3226,7 +3237,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"))
@@ -4039,6 +4050,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 */
@@ -4316,6 +4334,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);
@@ -4517,7 +4541,9 @@ match_previous_words(int pattern_id,
 		 * objects supported.
 		 */
 		if (HeadMatches("ALTER", "DEFAULT", "PRIVILEGES"))
-			COMPLETE_WITH("TABLES", "SEQUENCES", "FUNCTIONS", "PROCEDURES", "ROUTINES", "TYPES", "SCHEMAS", "LARGE OBJECTS");
+			COMPLETE_WITH("TABLES", "SEQUENCES", "FUNCTIONS", "PROCEDURES",
+						  "ROUTINES", "TYPES", "SCHEMAS", "LARGE OBJECTS",
+						  "VARIABLES");
 		else
 			COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_grantables,
 											"ALL FUNCTIONS IN SCHEMA",
@@ -4525,6 +4551,7 @@ match_previous_words(int pattern_id,
 											"ALL ROUTINES IN SCHEMA",
 											"ALL SEQUENCES IN SCHEMA",
 											"ALL TABLES IN SCHEMA",
+											"ALL VARIABLES IN SCHEMA",
 											"DATABASE",
 											"DOMAIN",
 											"FOREIGN DATA WRAPPER",
@@ -4539,7 +4566,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"))
@@ -4547,7 +4575,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");
@@ -4583,6 +4612,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
@@ -4909,7 +4940,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
@@ -5398,6 +5429,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/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 7fa5f7366c1..a71b9059822 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6697,6 +6697,9 @@
   proname => 'pg_collation_is_visible', procost => '10', provolatile => 's',
   prorettype => 'bool', proargtypes => 'oid',
   prosrc => 'pg_collation_is_visible' },
+{ oid => '9999', 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/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index a79325e8a2f..f2e506796db 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -6025,6 +6025,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
@@ -6246,6 +6270,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
@@ -6480,6 +6510,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"
@@ -6649,6 +6685,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 tables
@@ -6782,6 +6824,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"
@@ -6829,6 +6877,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/sql/psql.sql b/src/test/regress/sql/psql.sql
index f064e4f5456..8f6108e44de 100644
--- a/src/test/regress/sql/psql.sql
+++ b/src/test/regress/sql/psql.sql
@@ -1645,6 +1645,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
@@ -1756,6 +1769,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"
@@ -1797,6 +1813,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"
@@ -1837,6 +1855,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"
@@ -1861,6 +1880,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"
@@ -1886,6 +1906,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;
-- 
2.51.0



  [text/x-patch] v20250829-0002-CREATE-DROP-ALTER-VARIABLE.patch (80.4K, 16-v20250829-0002-CREATE-DROP-ALTER-VARIABLE.patch)
  download | inline diff:
From 2c4899453ba6e3739be0cbf13844989b99bbacf8 Mon Sep 17 00:00:00 2001
From: "[email protected]" <[email protected]>
Date: Wed, 28 May 2025 11:26:17 +0200
Subject: [PATCH 02/15] CREATE, DROP, ALTER VARIABLE

Implementation of commands:

    CREATE VARIABLE varname AS type
    DROP VARIABLE varname
    ALTER VARIABLE varname OWNER TO
    ALTER VARIABLE varname RENAME TO
    ALTER VARIABLE varname SET SCHEMA

ALTER command uses already prepared infrastructure based on ObjectAddress API,
so this patch implements ObjectAddress related functionality too.

Event triggers for DDL over session variables are supported.
---
 doc/src/sgml/ddl.sgml                         |  21 ++
 doc/src/sgml/glossary.sgml                    |  15 ++
 doc/src/sgml/plpgsql.sgml                     |  14 ++
 doc/src/sgml/ref/allfiles.sgml                |   3 +
 doc/src/sgml/ref/alter_variable.sgml          | 178 +++++++++++++++
 doc/src/sgml/ref/comment.sgml                 |   1 +
 doc/src/sgml/ref/create_schema.sgml           |  12 +-
 doc/src/sgml/ref/create_variable.sgml         | 149 +++++++++++++
 doc/src/sgml/ref/drop_variable.sgml           | 117 ++++++++++
 doc/src/sgml/reference.sgml                   |   3 +
 src/backend/catalog/aclchk.c                  |   4 +
 src/backend/catalog/dependency.c              |   6 +
 src/backend/catalog/namespace.c               | 207 ++++++++++++++++++
 src/backend/catalog/objectaddress.c           |  99 +++++++++
 src/backend/catalog/pg_shdepend.c             |   2 +
 src/backend/commands/Makefile                 |   1 +
 src/backend/commands/alter.c                  |   9 +
 src/backend/commands/dropcmds.c               |   4 +
 src/backend/commands/event_trigger.c          |   4 +
 src/backend/commands/meson.build              |   1 +
 src/backend/commands/seclabel.c               |   1 +
 src/backend/commands/session_variable.c       |  88 ++++++++
 src/backend/commands/tablecmds.c              |  41 ++++
 src/backend/commands/typecmds.c               |  15 ++
 src/backend/parser/gram.y                     |  86 +++++++-
 src/backend/parser/parse_utilcmd.c            |  12 +
 src/backend/tcop/utility.c                    |  20 ++
 src/backend/utils/cache/lsyscache.c           |  65 ++++++
 src/include/catalog/namespace.h               |   6 +
 src/include/commands/session_variable.h       |  24 ++
 src/include/nodes/parsenodes.h                |  16 ++
 src/include/parser/kwlist.h                   |   1 +
 src/include/tcop/cmdtaglist.h                 |   3 +
 src/include/utils/lsyscache.h                 |   4 +
 src/test/regress/expected/dependency.out      |  17 ++
 .../expected/session_variables_ddl.out        | 163 ++++++++++++++
 src/test/regress/parallel_schedule            |   2 +-
 src/test/regress/sql/dependency.sql           |  14 ++
 .../regress/sql/session_variables_ddl.sql     | 150 +++++++++++++
 src/tools/pgindent/typedefs.list              |   1 +
 40 files changed, 1571 insertions(+), 8 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/commands/session_variable.c
 create mode 100644 src/include/commands/session_variable.h
 create mode 100644 src/test/regress/expected/session_variables_ddl.out
 create mode 100644 src/test/regress/sql/session_variables_ddl.sql

diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml
index 65bc070d2e5..fa711a09bc4 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -5362,6 +5362,27 @@ 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.
+   </para>
+
+   <para>
+    The session variable holds value in session memory.  This value is private
+    to each session and is released when the session ends.
+   </para>
+  </sect1>
+
  <sect1 id="ddl-others">
   <title>Other Database Objects</title>
 
diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml
index 8651f0cdb91..c37fd5da50b 100644
--- a/doc/src/sgml/glossary.sgml
+++ b/doc/src/sgml/glossary.sgml
@@ -1711,6 +1711,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/plpgsql.sgml b/doc/src/sgml/plpgsql.sgml
index e937491e6b8..1e4c43b8b61 100644
--- a/doc/src/sgml/plpgsql.sgml
+++ b/doc/src/sgml/plpgsql.sgml
@@ -6036,6 +6036,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 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_variable.sgml b/doc/src/sgml/ref/alter_variable.sgml
new file mode 100644
index 00000000000..96d2586423e
--- /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_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_name</replaceable></term>
+      <listitem>
+       <para>
+        The new name for 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..d2bb265209b 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>
@@ -214,6 +215,11 @@ CREATE VIEW hollywood.winners AS
    The <literal>IF NOT EXISTS</literal> option is a
    <productname>PostgreSQL</productname> extension.
   </para>
+
+  <para>
+   The <command>CREATE VARIABLE</command> command is a
+   <productname>PostgreSQL</productname> extension.
+  </para>
  </refsect1>
 
  <refsect1>
diff --git a/doc/src/sgml/ref/create_variable.sgml b/doc/src/sgml/ref/create_variable.sgml
new file mode 100644
index 00000000000..6e988f2e472
--- /dev/null
+++ b/doc/src/sgml/ref/create_variable.sgml
@@ -0,0 +1,149 @@
+<!--
+doc/src/sgml/ref/create_variable.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-createvariable">
+ <indexterm zone="sql-createvariable">
+  <primary>CREATE VARIABLE</primary>
+ </indexterm>
+
+ <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;
+</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/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/aclchk.c b/src/backend/catalog/aclchk.c
index cd139bd65a6..00e3630e0ec 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -2784,6 +2784,7 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
 					case OBJECT_TSPARSER:
 					case OBJECT_TSTEMPLATE:
 					case OBJECT_USER_MAPPING:
+					case OBJECT_VARIABLE:
 						elog(ERROR, "unsupported object type: %d", objtype);
 				}
 
@@ -2891,6 +2892,9 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
 					case OBJECT_TSDICTIONARY:
 						msg = gettext_noop("must be owner of text search dictionary %s");
 						break;
+					case OBJECT_VARIABLE:
+						msg = gettext_noop("must be owner of session variable %s");
+						break;
 
 						/*
 						 * Special cases: For these, the error message talks
diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c
index 7dded634eb8..1d62e63d4f7 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/namespace.c b/src/backend/catalog/namespace.c
index 8bd4d6c3d43..ea49a0bc82d 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 "common/hashfn_unstable.h"
 #include "funcapi.h"
 #include "mb/pg_wchar.h"
@@ -224,6 +225,7 @@ static bool TSParserIsVisibleExt(Oid prsId, bool *is_missing);
 static bool TSDictionaryIsVisibleExt(Oid dictId, bool *is_missing);
 static bool TSTemplateIsVisibleExt(Oid tmplId, bool *is_missing);
 static bool TSConfigIsVisibleExt(Oid cfgid, bool *is_missing);
+static bool VariableIsVisibleExt(Oid varid, bool *is_missing);
 static void recomputeNamespacePath(void);
 static void AccessTempTableNamespace(bool force);
 static void InitTempTableNamespace(void);
@@ -985,6 +987,84 @@ 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)
+{
+	return VariableIsVisibleExt(varid, NULL);
+}
+
+/*
+ * VariableIsVisibleExt
+ *		As above, but if the variable isn't found and is_missing is not NULL,
+ *		then set *is_missing = true and return false, instead of throwing
+ *		an error. (Caller must initialize *is_missing = false.)
+ */
+static bool
+VariableIsVisibleExt(Oid varid, bool *is_missing)
+{
+	HeapTuple	vartup;
+	Form_pg_variable varform;
+	Oid			varnamespace;
+	bool		visible;
+
+	vartup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid));
+	if (!HeapTupleIsValid(vartup))
+	{
+		if (is_missing != NULL)
+		{
+			*is_missing = true;
+			return false;
+		}
+
+		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);
+
+		visible = false;
+		foreach_oid(namespaceId, activeSearchPath)
+		{
+			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
@@ -3288,6 +3368,133 @@ 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			varoid = InvalidOid;
+
+	if (nspname)
+	{
+		Oid			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_oid(namespaceId, activeSearchPath)
+		{
+			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;
+}
+
+/*
+ * Returns oid of session variable specified by possibly qualified identifier
+ *
+ * If not found, returns InvalidOid if missing_ok, else throws error.
+ */
+Oid
+LookupVariableFromNameList(List *names,
+						   bool missing_ok)
+{
+	char	   *catname = NULL;
+	char	   *nspname = NULL;
+	char	   *varname = NULL;
+
+	switch (list_length(names))
+	{
+		case 1:
+			varname = strVal(linitial(names));
+			break;
+		case 2:
+			nspname = strVal(linitial(names));
+			varname = strVal(lsecond(names));
+			break;
+		case 3:
+			catname = strVal(linitial(names));
+			nspname = strVal(lsecond(names));
+			varname = strVal(lthird(names));
+
+			/* check catalog name */
+			if (strcmp(catname, get_database_name(MyDatabaseId)) != 0)
+				ereport(ERROR,
+						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+						 errmsg("cross-database references are not implemented: %s",
+								NameListToString(names))));
+			break;
+		default:
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("improper session variable name (too many dotted names): %s",
+							NameListToString(names))));
+			break;
+	}
+
+	return LookupVariable(nspname, varname, missing_ok);
+}
+
+/*
+ * 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;
+}
 
 /*
  * DeconstructQualifiedName
diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c
index 91f3018fd0a..3817efb1eeb 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/defrem.h"
 #include "commands/event_trigger.h"
 #include "commands/extension.h"
@@ -635,6 +636,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
+	}
 };
 
 /*
@@ -830,6 +845,9 @@ static const struct object_type_map
 	},
 	{
 		"statistics object", OBJECT_STATISTIC_EXT
+	},
+	{
+		"session variable", OBJECT_VARIABLE
 	}
 };
 
@@ -855,6 +873,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 */
 		}
 
@@ -2101,6 +2123,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
  */
@@ -2295,6 +2335,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:
@@ -2466,6 +2507,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)));
@@ -3495,6 +3537,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;
@@ -4669,6 +4737,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);
 	}
@@ -6019,6 +6091,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, pstrdup(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 16e3e5c7457..6e3e8813328 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/defrem.h"
 #include "commands/event_trigger.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/commands/Makefile b/src/backend/commands/Makefile
index cb2fbdc7c60..aee40e7bd59 100644
--- a/src/backend/commands/Makefile
+++ b/src/backend/commands/Makefile
@@ -53,6 +53,7 @@ OBJS = \
 	schemacmds.o \
 	seclabel.o \
 	sequence.o \
+	session_variable.o \
 	statscmds.o \
 	subscriptioncmds.o \
 	tablecmds.o \
diff --git a/src/backend/commands/alter.c b/src/backend/commands/alter.c
index cb75e11fced..aaf61a6f61c 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"
@@ -140,6 +141,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;
@@ -435,6 +440,7 @@ ExecRenameStmt(RenameStmt *stmt)
 		case OBJECT_TSTEMPLATE:
 		case OBJECT_PUBLICATION:
 		case OBJECT_SUBSCRIPTION:
+		case OBJECT_VARIABLE:
 			{
 				ObjectAddress address;
 				Relation	catalog;
@@ -575,6 +581,7 @@ ExecAlterObjectSchemaStmt(AlterObjectSchemaStmt *stmt,
 		case OBJECT_TSDICTIONARY:
 		case OBJECT_TSPARSER:
 		case OBJECT_TSTEMPLATE:
+		case OBJECT_VARIABLE:
 			{
 				Relation	catalog;
 				Oid			classId;
@@ -657,6 +664,7 @@ AlterObjectNamespace_oid(Oid classId, Oid objid, Oid nspOid,
 		case TSDictionaryRelationId:
 		case TSTemplateRelationId:
 		case TSConfigRelationId:
+		case VariableRelationId:
 			{
 				Relation	catalog;
 
@@ -887,6 +895,7 @@ ExecAlterOwnerStmt(AlterOwnerStmt *stmt)
 		case OBJECT_TABLESPACE:
 		case OBJECT_TSDICTIONARY:
 		case OBJECT_TSCONFIGURATION:
+		case OBJECT_VARIABLE:
 			{
 				ObjectAddress address;
 
diff --git a/src/backend/commands/dropcmds.c b/src/backend/commands/dropcmds.c
index ceb9a229b63..ebb585dc4a1 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 631fb0525f1..5e0ce55daa1 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/meson.build b/src/backend/commands/meson.build
index dd4cde41d32..101c8d75dd1 100644
--- a/src/backend/commands/meson.build
+++ b/src/backend/commands/meson.build
@@ -41,6 +41,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/seclabel.c b/src/backend/commands/seclabel.c
index cee5d7bbb9c..57b4e6719c2 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/session_variable.c b/src/backend/commands/session_variable.c
new file mode 100644
index 00000000000..f641e00c1ac
--- /dev/null
+++ b/src/backend/commands/session_variable.c
@@ -0,0 +1,88 @@
+/*-------------------------------------------------------------------------
+ *
+ * session_variable.c
+ *	  session variable creation/manipulation commands
+ *
+ * Portions Copyright (c) 1996-2025, 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 "catalog/namespace.h"
+#include "catalog/pg_type.h"
+#include "commands/session_variable.h"
+#include "miscadmin.h"
+#include "parser/parse_type.h"
+#include "utils/builtins.h"
+#include "utils/lsyscache.h"
+
+/*
+ * 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);
+
+	return variable;
+}
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 082a3575d62..2dcd760a4ff 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"
@@ -6909,6 +6910,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.
@@ -6972,6 +6974,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/commands/typecmds.c b/src/backend/commands/typecmds.c
index c6de04819f1..4749f3f7e56 100644
--- a/src/backend/commands/typecmds.c
+++ b/src/backend/commands/typecmds.c
@@ -53,6 +53,7 @@
 #include "catalog/pg_proc.h"
 #include "catalog/pg_range.h"
 #include "catalog/pg_type.h"
+#include "catalog/pg_variable.h"
 #include "commands/defrem.h"
 #include "commands/tablecmds.h"
 #include "commands/typecmds.h"
@@ -3391,6 +3392,20 @@ get_rels_with_domain(Oid domainOid, LOCKMODE lockmode)
 			}
 			continue;
 		}
+		else if (pg_depend->classid == VariableRelationId)
+		{
+			/*
+			 * We cannot to validate constraint inside session variables from
+			 * other sessions, so better to fail if there are any session
+			 * variable, that use this domain.
+			 */
+			ereport(ERROR,
+					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					 errmsg("cannot alter domain \"%s\" because session variable \"%s.%s\" uses it",
+							domainTypeName,
+							get_namespace_name(get_session_variable_namespace(pg_depend->objid)),
+							get_session_variable_name(pg_depend->objid))));
+		}
 
 		/* Else, ignore dependees that aren't user columns of relations */
 		/* (we assume system columns are never of domain types) */
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index db43034b9db..3140c772178 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"
@@ -284,8 +285,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
@@ -782,8 +783,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 VIRTUAL VOLATILE
+	VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIADIC VARIABLE
+	VARYING VERBOSE VERSION_P VIEW VIEWS VIRTUAL VOLATILE
 
 	WHEN WHERE WHITESPACE_P WINDOW WITH WITHIN WITHOUT WORK WRAPPER WRITE
 
@@ -1049,6 +1050,7 @@ stmt:
 			| CreatePolicyStmt
 			| CreatePLangStmt
 			| CreateSchemaStmt
+			| CreateSessionVarStmt
 			| CreateSeqStmt
 			| CreateStmt
 			| CreateSubscriptionStmt
@@ -1626,6 +1628,7 @@ schema_stmt:
 			| CreateTrigStmt
 			| GrantStmt
 			| ViewStmt
+			| CreateSessionVarStmt
 		;
 
 
@@ -5307,6 +5310,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 ]
@@ -7118,6 +7149,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; }
 		;
 
 /*
@@ -10053,6 +10085,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
@@ -10414,6 +10464,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;
+				}
 		;
 
 /*****************************************************************************
@@ -10695,6 +10763,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;
+				}
 		;
 
 
@@ -18044,6 +18120,7 @@ unreserved_keyword:
 			| VALIDATE
 			| VALIDATOR
 			| VALUE_P
+			| VARIABLE
 			| VARYING
 			| VERSION_P
 			| VIEW
@@ -18700,6 +18777,7 @@ bare_label_keyword:
 			| VALUE_P
 			| VALUES
 			| VARCHAR
+			| VARIABLE
 			| VARIADIC
 			| VERBOSE
 			| VERSION_P
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index afcf54169c3..840baeacc8a 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;
 
 
@@ -4113,6 +4114,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
@@ -4181,6 +4183,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));
@@ -4194,6 +4205,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 4f4191b0ea6..97ad9596561 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -48,6 +48,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"
@@ -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:
@@ -1380,6 +1382,10 @@ ProcessUtilitySlow(ParseState *pstate,
 				}
 				break;
 
+			case T_CreateSessionVarStmt:
+				address = CreateVariable(pstate, (CreateSessionVarStmt *) parsetree);
+				break;
+
 				/*
 				 * ************* object creation / destruction **************
 				 */
@@ -2333,6 +2339,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;
@@ -2641,6 +2650,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;
 			}
@@ -3217,6 +3229,10 @@ CreateCommandTag(Node *parsetree)
 			}
 			break;
 
+		case T_CreateSessionVarStmt:
+			tag = CMDTAG_CREATE_VARIABLE;
+			break;
+
 		default:
 			elog(WARNING, "unrecognized node type: %d",
 				 (int) nodeTag(parsetree));
@@ -3751,6 +3767,10 @@ GetCommandLogLevel(Node *parsetree)
 			}
 			break;
 
+		case T_CreateSessionVarStmt:
+			lev = LOGSTMT_DDL;
+			break;
+
 		default:
 			elog(WARNING, "unrecognized node type: %d",
 				 (int) nodeTag(parsetree));
diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c
index fa7cd7e06a7..1c4031eea23 100644
--- a/src/backend/utils/cache/lsyscache.c
+++ b/src/backend/utils/cache/lsyscache.c
@@ -40,6 +40,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"
@@ -3881,3 +3882,67 @@ 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;
+}
diff --git a/src/include/catalog/namespace.h b/src/include/catalog/namespace.h
index 8c7ccc69a3c..bdac0c13bec 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,10 @@ 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	LookupVariableFromNameList(List *names, 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/commands/session_variable.h b/src/include/commands/session_variable.h
new file mode 100644
index 00000000000..49f36ac6885
--- /dev/null
+++ b/src/include/commands/session_variable.h
@@ -0,0 +1,24 @@
+/*-------------------------------------------------------------------------
+ *
+ * sessionvariable.h
+ *	  prototypes for sessionvariable.c.
+ *
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/commands/session_variable.h
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef SESSIONVARIABLE_H
+#define SESSIONVARIABLE_H
+
+#include "catalog/objectaddress.h"
+#include "parser/parse_node.h"
+#include "nodes/parsenodes.h"
+
+extern ObjectAddress CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt);
+
+#endif
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 86a236bd58b..2b4c7363d74 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2370,6 +2370,7 @@ typedef enum ObjectType
 	OBJECT_TSTEMPLATE,
 	OBJECT_TYPE,
 	OBJECT_USER_MAPPING,
+	OBJECT_VARIABLE,
 	OBJECT_VIEW,
 } ObjectType;
 
@@ -3552,6 +3553,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 a4af3f717a1..6f513f04225 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -486,6 +486,7 @@ 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("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 d250a714d59..ea86954dded 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/lsyscache.h b/src/include/utils/lsyscache.h
index c65cee4f24c..5daa308f4eb 100644
--- a/src/include/utils/lsyscache.h
+++ b/src/include/utils/lsyscache.h
@@ -139,6 +139,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);
@@ -211,6 +212,9 @@ 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);
+
 #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 75a078ada9e..cd8e4412fa9 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/session_variables_ddl.out b/src/test/regress/expected/session_variables_ddl.out
new file mode 100644
index 00000000000..9c7595e9a41
--- /dev/null
+++ b/src/test/regress/expected/session_variables_ddl.out
@@ -0,0 +1,163 @@
+SET log_statement TO ddl;
+CREATE VARIABLE ddltest_sesvar01 AS int;
+CREATE VARIABLE public.ddltest_sesvar02 AS int;
+CREATE SCHEMA sesvartest_ddl;
+CREATE VARIABLE sesvartest_ddl.ddltest_sesvar03 AS int;
+SELECT pg_identify_object_as_address(classid, objid, objsubid)
+  FROM pg_get_object_address('session variable', '{ddltest_sesvar01}', '{}');
+            pg_identify_object_as_address            
+-----------------------------------------------------
+ ("session variable","{public,ddltest_sesvar01}",{})
+(1 row)
+
+SELECT pg_identify_object_as_address(classid, objid, objsubid)
+  FROM pg_get_object_address('session variable', '{public,ddltest_sesvar02}', '{}');
+            pg_identify_object_as_address            
+-----------------------------------------------------
+ ("session variable","{public,ddltest_sesvar02}",{})
+(1 row)
+
+SELECT pg_identify_object_as_address(classid, objid, objsubid)
+  FROM pg_get_object_address('session variable', '{sesvartest_ddl,ddltest_sesvar03}', '{}');
+                pg_identify_object_as_address                
+-------------------------------------------------------------
+ ("session variable","{sesvartest_ddl,ddltest_sesvar03}",{})
+(1 row)
+
+DROP VARIABLE ddltest_sesvar01;
+DROP VARIABLE public.ddltest_sesvar02;
+CREATE TYPE sesvartest_type_ddl AS (a int, b int);
+CREATE DOMAIN sesvartest_domain_ddl AS int;
+CREATE TABLE sesvartest_table_ddl (a int, b int);
+/* prefix ddltest_ should not be used ever in another tests */
+CREATE VARIABLE ddltest_sesvar04 AS sesvartest_type_ddl;
+CREATE VARIABLE ddltest_sesvar05 AS sesvartest_domain_ddl;
+CREATE VARIABLE ddltest_sesvar06 AS sesvartest_table_ddl;
+-- add new field to composite value is supported,
+-- change type of field is prohibited
+-- should be ok
+ALTER TYPE sesvartest_type_ddl ADD ATTRIBUTE c int;
+ALTER TABLE sesvartest_table_ddl ADD COLUMN c int;
+-- should fail
+ALTER TYPE sesvartest_type_ddl ALTER ATTRIBUTE b TYPE numeric;
+ERROR:  cannot alter type "sesvartest_type_ddl" because session variable "public.ddltest_sesvar04" uses it
+ALTER TABLE sesvartest_table_ddl ALTER COLUMN b TYPE numeric;
+ERROR:  cannot alter table "sesvartest_table_ddl" because session variable "public.ddltest_sesvar06" uses it
+ALTER DOMAIN sesvartest_domain_ddl ADD CHECK(value <> 100);
+ERROR:  cannot alter domain "sesvartest_domain_ddl" because session variable "public.ddltest_sesvar05" uses it
+-- should fail
+DROP TYPE sesvartest_type_ddl;
+ERROR:  cannot drop type sesvartest_type_ddl because other objects depend on it
+DETAIL:  session variable ddltest_sesvar04 depends on type sesvartest_type_ddl
+HINT:  Use DROP ... CASCADE to drop the dependent objects too.
+DROP DOMAIN sesvartest_domain_ddl;
+ERROR:  cannot drop type sesvartest_domain_ddl because other objects depend on it
+DETAIL:  session variable ddltest_sesvar05 depends on type sesvartest_domain_ddl
+HINT:  Use DROP ... CASCADE to drop the dependent objects too.
+DROP TABLE sesvartest_table_ddl;
+ERROR:  cannot drop table sesvartest_table_ddl because other objects depend on it
+DETAIL:  session variable ddltest_sesvar06 depends on type sesvartest_table_ddl
+HINT:  Use DROP ... CASCADE to drop the dependent objects too.
+-- check event trigger support
+CREATE OR REPLACE FUNCTION svar_event_trigger_report_dropped()
+RETURNS event_trigger
+AS $$
+DECLARE r record;
+BEGIN
+  FOR r IN SELECT * from pg_event_trigger_dropped_objects()
+  LOOP
+    IF r.classid = 'pg_variable'::regclass AND
+       r.address_names[2] like 'ddltest_sesvar%'
+    THEN
+      RAISE NOTICE
+         'NORMAL: orig=% normal=% istemp=% type=% identity=% name=% args=%',
+         r.original, r.normal, r.is_temporary, r.object_type,
+         r.object_identity, r.address_names, r.address_args;
+    END IF;
+  END LOOP;
+END;
+$$ LANGUAGE plpgsql;
+CREATE EVENT TRIGGER svar_regress_event_trigger_report_dropped ON sql_drop
+  WHEN TAG IN ('DROP VARIABLE', 'DROP SCHEMA')
+  EXECUTE PROCEDURE svar_event_trigger_report_dropped();
+DROP VARIABLE ddltest_sesvar04;
+NOTICE:  NORMAL: orig=t normal=f istemp=f type=session variable identity=public.ddltest_sesvar04 name={public,ddltest_sesvar04} args={}
+DROP VARIABLE ddltest_sesvar05;
+NOTICE:  NORMAL: orig=t normal=f istemp=f type=session variable identity=public.ddltest_sesvar05 name={public,ddltest_sesvar05} args={}
+DROP VARIABLE ddltest_sesvar06;
+NOTICE:  NORMAL: orig=t normal=f istemp=f type=session variable identity=public.ddltest_sesvar06 name={public,ddltest_sesvar06} args={}
+-- should to fail
+DROP SCHEMA sesvartest_ddl;
+ERROR:  cannot drop schema sesvartest_ddl because other objects depend on it
+DETAIL:  session variable sesvartest_ddl.ddltest_sesvar03 depends on schema sesvartest_ddl
+HINT:  Use DROP ... CASCADE to drop the dependent objects too.
+-- should be ok
+DROP SCHEMA sesvartest_ddl CASCADE;
+NOTICE:  drop cascades to session variable sesvartest_ddl.ddltest_sesvar03
+NOTICE:  NORMAL: orig=f normal=t istemp=f type=session variable identity=sesvartest_ddl.ddltest_sesvar03 name={sesvartest_ddl,ddltest_sesvar03} args={}
+DROP EVENT TRIGGER svar_regress_event_trigger_report_dropped;
+DROP FUNCTION svar_event_trigger_report_dropped();
+-- should be ok
+DROP TYPE sesvartest_type_ddl;
+DROP DOMAIN sesvartest_domain_ddl;
+DROP TABLE sesvartest_table_ddl;
+-- check comment on variable
+CREATE VARIABLE ddltest_sesvar07 AS int;
+COMMENT ON VARIABLE ddltest_sesvar07 IS 'some session variable comment';
+SELECT pg_catalog.obj_description(oid, 'pg_variable') FROM pg_variable WHERE varname = 'ddltest_sesvar07';
+        obj_description        
+-------------------------------
+ some session variable comment
+(1 row)
+
+DROP VARIABLE ddltest_sesvar07;
+CREATE VARIABLE ddltest_sesvar08 AS int;
+ALTER VARIABLE ddltest_sesvar08 RENAME TO ddltest_sesvar08_renamed;
+CREATE SCHEMA sesvartest_ddl;
+ALTER VARIABLE ddltest_sesvar08_renamed SET SCHEMA sesvartest_ddl;
+CREATE ROLE regress_variable_owner_ddl;
+GRANT ALL ON SCHEMA sesvartest_ddl TO regress_variable_owner_ddl;
+SET ROLE TO regress_variable_owner_ddl;
+-- should fail
+DROP VARIABLE sesvartest_ddl.ddltest_sesvar08_renamed;
+ERROR:  must be owner of session variable sesvartest_ddl.ddltest_sesvar08_renamed
+SET ROLE TO DEFAULT;
+ALTER VARIABLE sesvartest_ddl.ddltest_sesvar08_renamed OWNER TO regress_variable_owner_ddl;
+-- should fail
+DROP ROLE regress_variable_owner_ddl;
+ERROR:  role "regress_variable_owner_ddl" cannot be dropped because some objects depend on it
+DETAIL:  owner of session variable sesvartest_ddl.ddltest_sesvar08_renamed
+privileges for schema sesvartest_ddl
+-- should fail - not on search path
+DROP VARIABLE ddltest_sesvar08_renamed;
+ERROR:  session variable "ddltest_sesvar08_renamed" does not exist
+SET SEARCH_PATH TO 'sesvartest_ddl';
+-- should be ok
+DROP VARIABLE ddltest_sesvar08_renamed;
+SET SEARCH_PATH TO DEFAULT;
+SET ROLE TO DEFAULT;
+DROP SCHEMA sesvartest_ddl;
+DROP ROLE regress_variable_owner_ddl;
+SET log_statement TO DEFAULT;
+CREATE VARIABLE IF NOT EXISTS ddltest_sesvar09 AS int;
+CREATE VARIABLE IF NOT EXISTS ddltest_sesvar09 AS int;
+NOTICE:  session variable "ddltest_sesvar09" already exists, skipping
+DROP VARIABLE IF EXISTS ddltest_sesvar09;
+DROP VARIABLE IF EXISTS ddltest_sesvar09;
+NOTICE:  session variable "ddltest_sesvar09" does not exist, skipping
+CREATE SCHEMA svartest01_ddl CREATE VARIABLE sesvar10 AS int;
+CREATE VARIABLE svartest01_ddl.sesvar11 AS int;
+CREATE SCHEMA svartest02_ddl CREATE VARIABLE sesvar10 AS int;
+-- should to fail
+CREATE VARIABLE svartest01_ddl.sesvar10 AS int;
+ERROR:  session variable "sesvar10" already exists
+ALTER VARIABLE svartest01_ddl.sesvar11 RENAME TO sesvar10;
+ERROR:  session variable "sesvar10" already exists in schema "svartest01_ddl"
+ALTER VARIABLE svartest02_ddl.sesvar10 SET SCHEMA svartest01_ddl;
+ERROR:  session variable "sesvar10" already exists in schema "svartest01_ddl"
+DROP SCHEMA svartest01_ddl CASCADE;
+NOTICE:  drop cascades to 2 other objects
+DETAIL:  drop cascades to session variable svartest01_ddl.sesvar10
+drop cascades to session variable svartest01_ddl.sesvar11
+DROP SCHEMA svartest02_ddl CASCADE;
+NOTICE:  drop cascades to session variable svartest02_ddl.sesvar10
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index fbffc67ae60..3abc1aca5f2 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -115,7 +115,7 @@ test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson
 # NB: temp.sql does reconnects which transiently use 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_ddl
 
 # ----------
 # 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/session_variables_ddl.sql b/src/test/regress/sql/session_variables_ddl.sql
new file mode 100644
index 00000000000..f844469ecb1
--- /dev/null
+++ b/src/test/regress/sql/session_variables_ddl.sql
@@ -0,0 +1,150 @@
+SET log_statement TO ddl;
+
+CREATE VARIABLE ddltest_sesvar01 AS int;
+CREATE VARIABLE public.ddltest_sesvar02 AS int;
+CREATE SCHEMA sesvartest_ddl;
+CREATE VARIABLE sesvartest_ddl.ddltest_sesvar03 AS int;
+
+SELECT pg_identify_object_as_address(classid, objid, objsubid)
+  FROM pg_get_object_address('session variable', '{ddltest_sesvar01}', '{}');
+
+SELECT pg_identify_object_as_address(classid, objid, objsubid)
+  FROM pg_get_object_address('session variable', '{public,ddltest_sesvar02}', '{}');
+
+SELECT pg_identify_object_as_address(classid, objid, objsubid)
+  FROM pg_get_object_address('session variable', '{sesvartest_ddl,ddltest_sesvar03}', '{}');
+
+DROP VARIABLE ddltest_sesvar01;
+DROP VARIABLE public.ddltest_sesvar02;
+
+CREATE TYPE sesvartest_type_ddl AS (a int, b int);
+CREATE DOMAIN sesvartest_domain_ddl AS int;
+CREATE TABLE sesvartest_table_ddl (a int, b int);
+
+/* prefix ddltest_ should not be used ever in another tests */
+CREATE VARIABLE ddltest_sesvar04 AS sesvartest_type_ddl;
+CREATE VARIABLE ddltest_sesvar05 AS sesvartest_domain_ddl;
+CREATE VARIABLE ddltest_sesvar06 AS sesvartest_table_ddl;
+
+-- add new field to composite value is supported,
+-- change type of field is prohibited
+
+-- should be ok
+ALTER TYPE sesvartest_type_ddl ADD ATTRIBUTE c int;
+ALTER TABLE sesvartest_table_ddl ADD COLUMN c int;
+
+-- should fail
+ALTER TYPE sesvartest_type_ddl ALTER ATTRIBUTE b TYPE numeric;
+ALTER TABLE sesvartest_table_ddl ALTER COLUMN b TYPE numeric;
+ALTER DOMAIN sesvartest_domain_ddl ADD CHECK(value <> 100);
+
+-- should fail
+DROP TYPE sesvartest_type_ddl;
+DROP DOMAIN sesvartest_domain_ddl;
+DROP TABLE sesvartest_table_ddl;
+
+-- check event trigger support
+CREATE OR REPLACE FUNCTION svar_event_trigger_report_dropped()
+RETURNS event_trigger
+AS $$
+DECLARE r record;
+BEGIN
+  FOR r IN SELECT * from pg_event_trigger_dropped_objects()
+  LOOP
+    IF r.classid = 'pg_variable'::regclass AND
+       r.address_names[2] like 'ddltest_sesvar%'
+    THEN
+      RAISE NOTICE
+         'NORMAL: orig=% normal=% istemp=% type=% identity=% name=% args=%',
+         r.original, r.normal, r.is_temporary, r.object_type,
+         r.object_identity, r.address_names, r.address_args;
+    END IF;
+  END LOOP;
+END;
+$$ LANGUAGE plpgsql;
+
+CREATE EVENT TRIGGER svar_regress_event_trigger_report_dropped ON sql_drop
+  WHEN TAG IN ('DROP VARIABLE', 'DROP SCHEMA')
+  EXECUTE PROCEDURE svar_event_trigger_report_dropped();
+
+DROP VARIABLE ddltest_sesvar04;
+DROP VARIABLE ddltest_sesvar05;
+DROP VARIABLE ddltest_sesvar06;
+
+-- should to fail
+DROP SCHEMA sesvartest_ddl;
+
+-- should be ok
+DROP SCHEMA sesvartest_ddl CASCADE;
+
+DROP EVENT TRIGGER svar_regress_event_trigger_report_dropped;
+
+DROP FUNCTION svar_event_trigger_report_dropped();
+
+-- should be ok
+DROP TYPE sesvartest_type_ddl;
+DROP DOMAIN sesvartest_domain_ddl;
+DROP TABLE sesvartest_table_ddl;
+
+-- check comment on variable
+CREATE VARIABLE ddltest_sesvar07 AS int;
+COMMENT ON VARIABLE ddltest_sesvar07 IS 'some session variable comment';
+SELECT pg_catalog.obj_description(oid, 'pg_variable') FROM pg_variable WHERE varname = 'ddltest_sesvar07';
+DROP VARIABLE ddltest_sesvar07;
+
+CREATE VARIABLE ddltest_sesvar08 AS int;
+ALTER VARIABLE ddltest_sesvar08 RENAME TO ddltest_sesvar08_renamed;
+
+CREATE SCHEMA sesvartest_ddl;
+ALTER VARIABLE ddltest_sesvar08_renamed SET SCHEMA sesvartest_ddl;
+
+CREATE ROLE regress_variable_owner_ddl;
+
+GRANT ALL ON SCHEMA sesvartest_ddl TO regress_variable_owner_ddl;
+
+SET ROLE TO regress_variable_owner_ddl;
+
+-- should fail
+DROP VARIABLE sesvartest_ddl.ddltest_sesvar08_renamed;
+
+SET ROLE TO DEFAULT;
+
+ALTER VARIABLE sesvartest_ddl.ddltest_sesvar08_renamed OWNER TO regress_variable_owner_ddl;
+
+-- should fail
+DROP ROLE regress_variable_owner_ddl;
+
+-- should fail - not on search path
+DROP VARIABLE ddltest_sesvar08_renamed;
+
+SET SEARCH_PATH TO 'sesvartest_ddl';
+
+-- should be ok
+DROP VARIABLE ddltest_sesvar08_renamed;
+
+SET SEARCH_PATH TO DEFAULT;
+
+SET ROLE TO DEFAULT;
+
+DROP SCHEMA sesvartest_ddl;
+
+DROP ROLE regress_variable_owner_ddl;
+
+SET log_statement TO DEFAULT;
+
+CREATE VARIABLE IF NOT EXISTS ddltest_sesvar09 AS int;
+CREATE VARIABLE IF NOT EXISTS ddltest_sesvar09 AS int;
+DROP VARIABLE IF EXISTS ddltest_sesvar09;
+DROP VARIABLE IF EXISTS ddltest_sesvar09;
+
+CREATE SCHEMA svartest01_ddl CREATE VARIABLE sesvar10 AS int;
+CREATE VARIABLE svartest01_ddl.sesvar11 AS int;
+CREATE SCHEMA svartest02_ddl CREATE VARIABLE sesvar10 AS int;
+
+-- should to fail
+CREATE VARIABLE svartest01_ddl.sesvar10 AS int;
+ALTER VARIABLE svartest01_ddl.sesvar11 RENAME TO sesvar10;
+ALTER VARIABLE svartest02_ddl.sesvar10 SET SCHEMA svartest01_ddl;
+
+DROP SCHEMA svartest01_ddl CASCADE;
+DROP SCHEMA svartest02_ddl CASCADE;
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 3fe07f2ab64..4694653a91d 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -560,6 +560,7 @@ CreateRoleStmt
 CreateSchemaStmt
 CreateSchemaStmtContext
 CreateSeqStmt
+CreateSessionVarStmt
 CreateStatsStmt
 CreateStmt
 CreateStmtContext
-- 
2.51.0



  [text/x-patch] v20250829-0001-introduce-new-class-catalog-pg_variable.patch (16.3K, 17-v20250829-0001-introduce-new-class-catalog-pg_variable.patch)
  download | inline diff:
From 87dd6030ac1308583284d06100672a9c38e416fd Mon Sep 17 00:00:00 2001
From: "[email protected]" <[email protected]>
Date: Wed, 28 May 2025 08:30:25 +0200
Subject: [PATCH 01/15] introduce new class (catalog) pg_variable

This table holds metadata about session variables created by
command CREATE VARIABLE, and dropped by command DROP VARIABLE.
---
 doc/src/sgml/catalogs.sgml             | 133 ++++++++++++++++++++
 src/backend/catalog/Makefile           |   1 +
 src/backend/catalog/meson.build        |   1 +
 src/backend/catalog/pg_variable.c      | 168 +++++++++++++++++++++++++
 src/include/catalog/Makefile           |   3 +-
 src/include/catalog/meson.build        |   1 +
 src/include/catalog/pg_variable.h      |  96 ++++++++++++++
 src/test/regress/expected/oidjoins.out |   4 +
 src/tools/pgindent/typedefs.list       |   2 +
 9 files changed, 408 insertions(+), 1 deletion(-)
 create mode 100644 src/backend/catalog/pg_variable.c
 create mode 100644 src/include/catalog/pg_variable.h

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index da8a7882580..b33f3f06d71 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>
@@ -9785,4 +9790,132 @@ 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>varcreate_lsn</structfield> <type>pg_lsn</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>
+      </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/src/backend/catalog/Makefile b/src/backend/catalog/Makefile
index c090094ed08..2c20d60db19 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/meson.build b/src/backend/catalog/meson.build
index 1958ea9238a..ed44c877fca 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/pg_variable.c b/src/backend/catalog/pg_variable.c
new file mode 100644
index 00000000000..bd6a29a79e5
--- /dev/null
+++ b/src/backend/catalog/pg_variable.c
@@ -0,0 +1,168 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_variable.c
+ *		session variables
+ *
+ * Portions Copyright (c) 1996-2025, 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 "utils/builtins.h"
+#include "utils/pg_lsn.h"
+#include "utils/syscache.h"
+
+/*
+ * Creates entry in pg_variable table
+ */
+ObjectAddress
+create_variable(const char *varName,
+				Oid varNamespace,
+				Oid varType,
+				int32 varTypmod,
+				Oid varOwner,
+				Oid varCollation,
+				bool if_not_exists)
+{
+	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_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);
+	values[Anum_pg_variable_vartypmod - 1] = Int32GetDatum(varTypmod);
+	values[Anum_pg_variable_varowner - 1] = ObjectIdGetDatum(varOwner);
+	values[Anum_pg_variable_varcollation - 1] = ObjectIdGetDatum(varCollation);
+
+	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);
+
+	/* 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;
+}
+
+/*
+ * Drop variable by OID
+ */
+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/include/catalog/Makefile b/src/include/catalog/Makefile
index 2bbc7805fe3..f98760de635 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 ec1cf467f6f..81398efa7c9 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/pg_variable.h b/src/include/catalog/pg_variable.h
new file mode 100644
index 00000000000..15f530894c5
--- /dev/null
+++ b/src/include/catalog/pg_variable.h
@@ -0,0 +1,96 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_variable.h
+ *	  definition of session variables system catalog (pg_variables)
+ *
+ *
+ * Portions Copyright (c) 1996-2025, 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 "access/xlogdefs.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);
+
+	/*
+	 * 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;
+
+	/* 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));
+
+MAKE_SYSCACHE(VARIABLENAMENSP, pg_variable_varname_nsp_index, 8);
+MAKE_SYSCACHE(VARIABLEOID, pg_variable_oid_index, 8);
+
+extern ObjectAddress create_variable(const char *varName,
+									 Oid varNamespace,
+									 Oid varType,
+									 int32 varTypmod,
+									 Oid varOwner,
+									 Oid varCollation,
+									 bool if_not_exists);
+
+extern void DropVariableById(Oid varid);
+
+#endif							/* PG_VARIABLE_H */
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/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index a13e8162890..3fe07f2ab64 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -909,6 +909,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
@@ -968,6 +969,7 @@ Form_pg_ts_parser
 Form_pg_ts_template
 Form_pg_type
 Form_pg_user_mapping
+Form_pg_variable
 FormatNode
 FreeBlockNumberArray
 FreeListData
-- 
2.51.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], [email protected], [email protected], [email protected], [email protected], [email protected]
  Subject: Re: proposal: schema variables
  In-Reply-To: <CAFj8pRBx4w3QS0D2W3yc8hWXH7hynsOwO4x+iv4AstmB6Dmkgw@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