Subject: Re: proposal: schema variables

From: Pavel Stehule <[email-masked]>
Date: Thu, 12 Feb 2026 20:25:03 +0100

Hi

fresh rebase cf74558feb8f41b2bc459f59ed3f991024d04893

regards

Pavel
From 4e731612e59e110bc4eb224d3943b846fcc4e58f Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Mon, 8 Dec 2025 05:00:12 +0100
Subject: [PATCH 11/11] subtransaction support for session variables DDL
 (CREATE, DROP)

If we support transactional DDL for CREATE, DROP session  variables, we should to
support subtransactions too. Implementation is simple. Any value has two new flags:
created_subid and dropped_subid. At the subtransaction end for rollback we
clean entries from the stack related to subtransactions. When commit we update
created_subid and dropped_subid for parent subtransaction.
---
 src/backend/access/transam/xact.c             |   4 +
 src/backend/commands/session_variable.c       | 109 ++++++++++++++++++
 src/include/commands/session_variable.h       |   3 +
 .../expected/session_variables_ddl.out        |  21 ++++
 .../regress/sql/session_variables_ddl.sql     |  12 ++
 5 files changed, 149 insertions(+)

diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c
index da5170861b8..d32ee493a48 100644
--- a/src/backend/access/transam/xact.c
+++ b/src/backend/access/transam/xact.c
@@ -5212,6 +5212,8 @@ CommitSubTransaction(void)
 	AtEOSubXact_SPI(true, s->subTransactionId);
 	AtEOSubXact_on_commit_actions(true, s->subTransactionId,
 								  s->parent->subTransactionId);
+	AtEOSubXact_SessionVariables(true, s->subTransactionId,
+								 s->parent->subTransactionId);
 	AtEOSubXact_Namespace(true, s->subTransactionId,
 						  s->parent->subTransactionId);
 	AtEOSubXact_Files(true, s->subTransactionId,
@@ -5381,6 +5383,8 @@ AbortSubTransaction(void)
 		AtEOSubXact_SPI(false, s->subTransactionId);
 		AtEOSubXact_on_commit_actions(false, s->subTransactionId,
 									  s->parent->subTransactionId);
+		AtEOSubXact_SessionVariables(false, s->subTransactionId,
+									 s->parent->subTransactionId);
 		AtEOSubXact_Namespace(false, s->subTransactionId,
 							  s->parent->subTransactionId);
 		AtEOSubXact_Files(false, s->subTransactionId,
diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c
index 8f249d515d5..4a311d2498b 100644
--- a/src/backend/commands/session_variable.c
+++ b/src/backend/commands/session_variable.c
@@ -14,6 +14,7 @@
  */
 #include "postgres.h"
 
+#include "access/xact.h"
 #include "catalog/pg_language.h"
 #include "commands/session_variable.h"
 #include "executor/executor.h"
@@ -59,6 +60,8 @@ typedef struct SVariableData
 	bool		stacked;
 	LocalTransactionId created_lxid;
 	LocalTransactionId dropped_lxid;
+	SubTransactionId created_subid;
+	SubTransactionId dropped_subid;
 } SVariableData;
 
 typedef SVariableData *SVariable;
@@ -349,6 +352,8 @@ CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt)
 	svar->stacked = false;
 	svar->dropped_lxid = InvalidLocalTransactionId;
 	svar->created_lxid = MyProc->vxid.lxid;
+	svar->dropped_subid = InvalidSubTransactionId;
+	svar->created_subid = GetCurrentSubTransactionId();
 	created_or_dropped_lxid = MyProc->vxid.lxid;
 }
 
@@ -385,6 +390,7 @@ DropVariableByName(DropSessionVarStmt *stmt)
 						stmt->name)));
 
 	svar->dropped_lxid = MyProc->vxid.lxid;
+	svar->dropped_subid = GetCurrentSubTransactionId();
 	created_or_dropped_lxid = MyProc->vxid.lxid;
 }
 
@@ -454,6 +460,7 @@ AtPreEOXact_SessionVariables(bool isCommit)
 						free_stacked_svars(svar->prev);
 						svar->prev = NULL;
 						svar->created_lxid = InvalidLocalTransactionId;
+						svar->created_subid = InvalidSubTransactionId;
 					}
 				}
 				else
@@ -500,6 +507,7 @@ AtPreEOXact_SessionVariables(bool isCommit)
 
 						/* revert dropped flag */
 						svar->dropped_lxid = InvalidLocalTransactionId;
+						svar->dropped_subid = InvalidSubTransactionId;
 					}
 				}
 			}
@@ -509,6 +517,107 @@ AtPreEOXact_SessionVariables(bool isCommit)
 	}
 }
 
+/*
+ * Post-subcommit or post-subabort cleanup
+ *
+ * During subabort, we can immediately remove entries created during this
+ * subtransaction. During subcommit, just transfer entries marked during
+ * this subtransaction as being the parent's responsibility.
+ */
+void
+AtEOSubXact_SessionVariables(bool isCommit,
+							 SubTransactionId mySubid,
+							 SubTransactionId parentSubid)
+{
+	if (created_or_dropped_lxid != InvalidLocalTransactionId)
+	{
+		HASH_SEQ_STATUS status;
+		SVariable	svar;
+
+		Assert(created_or_dropped_lxid == MyProc->vxid.lxid);
+		Assert(sessionvars);
+
+		hash_seq_init(&status, sessionvars);
+
+		while ((svar = (SVariable) hash_seq_search(&status)) != NULL)
+		{
+			if ((svar->dropped_lxid != InvalidLocalTransactionId) ||
+				(svar->created_lxid != InvalidLocalTransactionId))
+			{
+				if (!isCommit)
+				{
+					SVariable iterator = svar;
+					SVariable last = NULL;
+					SVariable first = NULL;
+
+					/* remove entries or flags by current subtransactions */
+					while (iterator)
+					{
+						SVariable current = iterator;
+
+						iterator = current->prev;
+
+						if (current->dropped_subid == mySubid)
+						{
+							current->dropped_lxid = InvalidLocalTransactionId;
+							current->dropped_subid = InvalidSubTransactionId;
+						}
+
+						if (current->created_subid == mySubid)
+						{
+							free_svar_value(current);
+							if (current->stacked)
+								pfree(current);
+						}
+						else
+						{
+							/* remember first not deleted svar */
+							if (first == NULL)
+								first = current;
+
+							if (last)
+								last->prev = current;
+
+							last = current;
+						}
+					}
+
+					/* Some svars was removed - set hashtab entry or remove it */
+					if (!first)
+					{
+						/* we have to remove entry from hash table */
+						(void) hash_search(sessionvars,
+										   NameStr(svar->varname),
+										   HASH_REMOVE,
+										   NULL);
+					}
+					else if (first->stacked)
+					{
+						memcpy(svar, first, sizeof(SVariableData));
+						svar->stacked = false;
+						pfree(first);
+					}
+				}
+				else
+				{
+					SVariable iterator = svar;
+
+					/* transfer responsibility to parent */
+					while (iterator)
+					{
+						if (iterator->dropped_subid == mySubid)
+							iterator->dropped_subid = parentSubid;
+						if (iterator->created_subid == mySubid)
+							iterator->created_subid = parentSubid;
+
+						iterator = iterator->prev;
+					}
+				}
+			}
+		}
+	}
+}
+
 /*
  * Assign the result of the evaluated expression to the session variable
  */
diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h
index 1218c566767..45ccbe2f046 100644
--- a/src/include/commands/session_variable.h
+++ b/src/include/commands/session_variable.h
@@ -40,5 +40,8 @@ extern void ExecuteLetStmt(ParseState *pstate, LetStmt *stmt, ParamListInfo para
 extern void ResetSessionVariables(void);
 
 extern void AtPreEOXact_SessionVariables(bool isCommit);
+extern void AtEOSubXact_SessionVariables(bool isCommit,
+										 SubTransactionId mySubid,
+										 SubTransactionId parentSubid);
 
 #endif
diff --git a/src/test/regress/expected/session_variables_ddl.out b/src/test/regress/expected/session_variables_ddl.out
index 2d00471da96..9031b5c384c 100644
--- a/src/test/regress/expected/session_variables_ddl.out
+++ b/src/test/regress/expected/session_variables_ddl.out
@@ -107,4 +107,25 @@ SELECT VARIABLE(x);
  Hi
 (1 row)
 
+BEGIN;
+SAVEPOINT s1;
+DROP VARIABLE x;
+CREATE TEMP VARIABLE x AS varchar;
+DROP VARIABLE x;
+CREATE TEMP VARIABLE x AS varchar;
+LET x = 'Hello';
+SELECT VARIABLE(x);
+   x   
+-------
+ Hello
+(1 row)
+
+ROLLBACK TO s1;
+SELECT VARIABLE(x);
+ x  
+----
+ Hi
+(1 row)
+
+COMMIT;
 DROP VARIABLE x;
diff --git a/src/test/regress/sql/session_variables_ddl.sql b/src/test/regress/sql/session_variables_ddl.sql
index 7335f15ed39..5ec412ad7c9 100644
--- a/src/test/regress/sql/session_variables_ddl.sql
+++ b/src/test/regress/sql/session_variables_ddl.sql
@@ -98,4 +98,16 @@ SELECT VARIABLE(x);
 ROLLBACK;
 SELECT VARIABLE(x);
 
+BEGIN;
+SAVEPOINT s1;
+DROP VARIABLE x;
+CREATE TEMP VARIABLE x AS varchar;
+DROP VARIABLE x;
+CREATE TEMP VARIABLE x AS varchar;
+LET x = 'Hello';
+SELECT VARIABLE(x);
+ROLLBACK TO s1;
+SELECT VARIABLE(x);
+COMMIT;
+
 DROP VARIABLE x;
-- 
2.53.0

From 30b53b5ed6940c92ab3005096385d4af8674ca94 Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Sat, 6 Dec 2025 07:35:30 +0100
Subject: [PATCH 10/11] transactional DDL - CREATE VARIABLE, DROP VARIABLE

Generally PostgreSQL DDL statements are transactional. We can implement transactional behave
for ( CREATE | DROP ) VARIABLE too. Implementation is almost simple. DROP just set dropped_lxid,
CREATE set created_lxid and moves previous entry of related sessionvars hashtab to stack. At the
end of transaction for commit we just clean this stack. For rollback we do copy the bootom value
from the stack and again clean stack.
---
 src/backend/access/transam/xact.c             |   5 +
 src/backend/commands/session_variable.c       | 217 ++++++++++++++++--
 src/include/commands/session_variable.h       |   2 +
 .../expected/session_variables_ddl.out        |  38 +++
 .../regress/sql/session_variables_ddl.sql     |  21 ++
 5 files changed, 258 insertions(+), 25 deletions(-)

diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c
index eba4f063168..da5170861b8 100644
--- a/src/backend/access/transam/xact.c
+++ b/src/backend/access/transam/xact.c
@@ -39,6 +39,7 @@
 #include "commands/async.h"
 #include "commands/tablecmds.h"
 #include "commands/trigger.h"
+#include "commands/session_variable.h"
 #include "common/pg_prng.h"
 #include "executor/spi.h"
 #include "libpq/be-fsstubs.h"
@@ -2333,6 +2334,9 @@ CommitTransaction(void)
 	/* close large objects before lower-level cleanup */
 	AtEOXact_LargeObject(true);
 
+	/* remove stacked session variables */
+	AtPreEOXact_SessionVariables(true);
+
 	/*
 	 * Insert notifications sent by NOTIFY commands into the queue.  This
 	 * should be late in the pre-commit sequence to minimize time spent
@@ -2939,6 +2943,7 @@ AbortTransaction(void)
 	AtAbort_Portals();
 	smgrDoPendingSyncs(false, is_parallel_worker);
 	AtEOXact_LargeObject(false);
+	AtPreEOXact_SessionVariables(false);
 	AtAbort_Notify();
 	AtEOXact_RelationMap(false, is_parallel_worker);
 	AtAbort_Twophase();
diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c
index 6430455c0a6..8f249d515d5 100644
--- a/src/backend/commands/session_variable.c
+++ b/src/backend/commands/session_variable.c
@@ -54,6 +54,11 @@ typedef struct SVariableData
 
 	int16		typlen;
 	bool		typbyval;
+
+	struct SVariableData *prev;
+	bool		stacked;
+	LocalTransactionId created_lxid;
+	LocalTransactionId dropped_lxid;
 } SVariableData;
 
 typedef SVariableData *SVariable;
@@ -62,6 +67,14 @@ static HTAB *sessionvars = NULL;	/* hash table for session variables */
 
 static MemoryContext SVariableMemoryContext = NULL;
 
+/*
+ * When we to remove committed dropped variables or uncommitted
+ * created variables from sessionvars tab. created_or_dropped_lxid
+ * is transaction id of transaction when some of DROP or CREATE variable
+ * was executed.
+ */
+static LocalTransactionId created_or_dropped_lxid = InvalidLocalTransactionId;
+
 /*
  * Create the hash table for storing session variables.
  */
@@ -103,6 +116,14 @@ search_variable(char *varname, bool missing_ok)
 	svar = (SVariable) hash_search(sessionvars, varname,
 								   HASH_FIND, NULL);
 
+	/* Session variable can be dropped inside current transaction */
+	if (svar && svar->dropped_lxid != InvalidLocalTransactionId)
+	{
+		Assert(created_or_dropped_lxid == MyProc->vxid.lxid);
+		Assert(svar->dropped_lxid == MyProc->vxid.lxid);
+		svar = NULL;
+	}
+
 	if (!svar && !missing_ok)
 		ereport(ERROR,
 				(errcode(ERRCODE_UNDEFINED_OBJECT),
@@ -235,6 +256,7 @@ CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt)
 	Oid			typcollation;
 	Oid			varowner = GetUserId();
 	SVariable	svar;
+	SVariable	prev_svar = NULL;
 	bool		found;
 	int16		typlen;
 	bool		typbyval;
@@ -279,19 +301,37 @@ CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt)
 
 	if (found)
 	{
-		if (stmt->if_not_exists)
+		if (svar->dropped_lxid == InvalidLocalTransactionId)
 		{
-			ereport(NOTICE,
-					(errcode(ERRCODE_DUPLICATE_OBJECT),
-					 errmsg("session variable \"%s\" already exists, skipping",
-							stmt->name)));
-			return;
+			if (stmt->if_not_exists)
+			{
+				ereport(NOTICE,
+						(errcode(ERRCODE_DUPLICATE_OBJECT),
+						 errmsg("session variable \"%s\" already exists, skipping",
+								stmt->name)));
+				return;
+			}
+			else
+				ereport(ERROR,
+						(errcode(ERRCODE_DUPLICATE_OBJECT),
+						 errmsg("session variable \"%s\" already exists",
+								stmt->name)));
 		}
 		else
-			ereport(ERROR,
-					(errcode(ERRCODE_DUPLICATE_OBJECT),
-					 errmsg("session variable \"%s\" already exists",
-							stmt->name)));
+		{
+			MemoryContext oldcxt;
+
+			Assert(created_or_dropped_lxid == MyProc->vxid.lxid);
+			Assert(svar->dropped_lxid == MyProc->vxid.lxid);
+
+			oldcxt = MemoryContextSwitchTo(SVariableMemoryContext);
+			prev_svar = palloc_object(SVariableData);
+			memcpy(prev_svar, svar, sizeof(SVariableData));
+			prev_svar->stacked = true;
+			memset(svar, 0, sizeof(SVariableData));
+
+			MemoryContextSwitchTo(oldcxt);
+		}
 	}
 
 	namestrcpy(&svar->varname, stmt->name);
@@ -304,6 +344,12 @@ CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt)
 
 	svar->value = (Datum) 0;
 	svar->isnull = true;
+
+	svar->prev = prev_svar;
+	svar->stacked = false;
+	svar->dropped_lxid = InvalidLocalTransactionId;
+	svar->created_lxid = MyProc->vxid.lxid;
+	created_or_dropped_lxid = MyProc->vxid.lxid;
 }
 
 /*
@@ -338,14 +384,129 @@ DropVariableByName(DropSessionVarStmt *stmt)
 				 errmsg("must be owner of session variable %s",
 						stmt->name)));
 
-	if (!svar->typbyval && !svar->isnull)
+	svar->dropped_lxid = MyProc->vxid.lxid;
+	created_or_dropped_lxid = MyProc->vxid.lxid;
+}
+
+static void
+free_svar_value(SVariable svar)
+{
+	if (!svar->isnull && !svar->typbyval)
 		pfree(DatumGetPointer(svar->value));
+}
+
+static void
+free_stacked_svars(SVariable svar)
+{
+	while (svar)
+	{
+		SVariable	current = svar;
+
+		free_svar_value(current);
+		svar = current->prev;
+		pfree(current);
+	}
+}
+
+/*
+ * remove dropped committed entries or created uncommitted entries
+ * from hash table.
+ */
+void
+AtPreEOXact_SessionVariables(bool isCommit)
+{
+	if (created_or_dropped_lxid != InvalidLocalTransactionId)
+	{
+		HASH_SEQ_STATUS status;
+		SVariable	svar;
 
-	if (hash_search(sessionvars,
-					   stmt->name,
-					   HASH_REMOVE,
-					   NULL) == NULL)
-		elog(ERROR, "hash table corrupted");
+		Assert(created_or_dropped_lxid == MyProc->vxid.lxid);
+		Assert(sessionvars);
+
+		hash_seq_init(&status, sessionvars);
+
+		while ((svar = (SVariable) hash_seq_search(&status)) != NULL)
+		{
+			if ((svar->dropped_lxid != InvalidLocalTransactionId) ||
+				(svar->created_lxid != InvalidLocalTransactionId))
+			{
+				Assert((svar->dropped_lxid == InvalidLocalTransactionId) ||
+						(svar->dropped_lxid == MyProc->vxid.lxid));
+
+				Assert((svar->created_lxid == InvalidLocalTransactionId) ||
+						(svar->created_lxid == MyProc->vxid.lxid));
+
+				if (isCommit)
+				{
+					if (svar->dropped_lxid == MyProc->vxid.lxid)
+					{
+						free_stacked_svars(svar->prev);
+						free_svar_value(svar);
+
+						(void) hash_search(sessionvars,
+										   NameStr(svar->varname),
+										   HASH_REMOVE,
+										   NULL);
+						svar = NULL;
+					}
+					else
+					{
+						free_stacked_svars(svar->prev);
+						svar->prev = NULL;
+						svar->created_lxid = InvalidLocalTransactionId;
+					}
+				}
+				else
+				{
+					SVariable	iter;
+
+					/*
+					 * We have to search value the oldest svar in the stack. If it is just dropped,
+					 * then we revert dropped flag. If it is created in current transaction, then
+					 * we remove this svar too.
+					 */
+					iter = svar;
+					while (iter->prev)
+					{
+						SVariable	current = iter;
+
+						free_svar_value(current);
+
+						iter = current->prev;
+
+						if (current->stacked)
+							pfree(current);
+					}
+
+					if (iter->created_lxid == MyProc->vxid.lxid)
+					{
+						free_svar_value(iter);
+						if (iter->stacked)
+							pfree(iter);
+
+						(void) hash_search(sessionvars,
+										   NameStr(svar->varname),
+										   HASH_REMOVE,
+										   NULL);
+					}
+					else
+					{
+						if (iter->stacked)
+						{
+							memcpy(svar, iter, sizeof(SVariableData));
+							svar->stacked = false;
+							pfree(iter);
+						}
+
+						/* revert dropped flag */
+						svar->dropped_lxid = InvalidLocalTransactionId;
+					}
+				}
+			}
+		}
+
+		created_or_dropped_lxid = InvalidLocalTransactionId;
+	}
 }
 
 /*
@@ -430,23 +591,29 @@ ExecuteLetStmt(ParseState *pstate,
 }
 
 /*
- * 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 TEMP.
  */
 void
 ResetSessionVariables(void)
 {
-	/* destroy hash table and reset related memory context */
+	/* mark all session variables as dropped */
 	if (sessionvars)
 	{
-		hash_destroy(sessionvars);
-		sessionvars = NULL;
-	}
+		HASH_SEQ_STATUS status;
+		SVariable	svar;
+		bool		found = false;
 
-	/* release memory allocated by session variables */
-	if (SVariableMemoryContext != NULL)
-		MemoryContextReset(SVariableMemoryContext);
+		hash_seq_init(&status, sessionvars);
+
+		while ((svar = (SVariable) hash_seq_search(&status)) != NULL)
+		{
+			svar->dropped_lxid = MyProc->vxid.lxid;
+			found = true;
+		}
+
+		if (found)
+			created_or_dropped_lxid = MyProc->vxid.lxid;
+	}
 }
 
 /*
diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h
index 3f07ae55aac..1218c566767 100644
--- a/src/include/commands/session_variable.h
+++ b/src/include/commands/session_variable.h
@@ -39,4 +39,6 @@ extern void ExecuteLetStmt(ParseState *pstate, LetStmt *stmt, ParamListInfo para
 
 extern void ResetSessionVariables(void);
 
+extern void AtPreEOXact_SessionVariables(bool isCommit);
+
 #endif
diff --git a/src/test/regress/expected/session_variables_ddl.out b/src/test/regress/expected/session_variables_ddl.out
index 758ce582fca..2d00471da96 100644
--- a/src/test/regress/expected/session_variables_ddl.out
+++ b/src/test/regress/expected/session_variables_ddl.out
@@ -70,3 +70,41 @@ SELECT * FROM pg_get_temporary_session_variables_names();
 ------------------------------------------
 (0 rows)
 
+CREATE TEMP VARIABLE x AS varchar;
+LET x = 'Hi';
+BEGIN;
+DROP VARIABLE x;
+CREATE TEMP VARIABLE x AS varchar;
+LET x = 'Hello';
+SELECT VARIABLE(x);
+   x   
+-------
+ Hello
+(1 row)
+
+COMMIT;
+SELECT VARIABLE(x);
+   x   
+-------
+ Hello
+(1 row)
+
+LET x = 'Hi';
+BEGIN;
+DROP VARIABLE x;
+CREATE TEMP VARIABLE x AS varchar;
+LET x = 'Hello';
+SELECT VARIABLE(x);
+   x   
+-------
+ Hello
+(1 row)
+
+ROLLBACK;
+SELECT VARIABLE(x);
+ x  
+----
+ Hi
+(1 row)
+
+DROP VARIABLE x;
diff --git a/src/test/regress/sql/session_variables_ddl.sql b/src/test/regress/sql/session_variables_ddl.sql
index c4cbfd17169..7335f15ed39 100644
--- a/src/test/regress/sql/session_variables_ddl.sql
+++ b/src/test/regress/sql/session_variables_ddl.sql
@@ -78,3 +78,24 @@ SELECT * FROM pg_get_temporary_session_variables_names();
 DROP VARIABLE x;
 DROP VARIABLE y;
 SELECT * FROM pg_get_temporary_session_variables_names();
+
+CREATE TEMP VARIABLE x AS varchar;
+LET x = 'Hi';
+BEGIN;
+DROP VARIABLE x;
+CREATE TEMP VARIABLE x AS varchar;
+LET x = 'Hello';
+SELECT VARIABLE(x);
+COMMIT;
+SELECT VARIABLE(x);
+
+LET x = 'Hi';
+BEGIN;
+DROP VARIABLE x;
+CREATE TEMP VARIABLE x AS varchar;
+LET x = 'Hello';
+SELECT VARIABLE(x);
+ROLLBACK;
+SELECT VARIABLE(x);
+
+DROP VARIABLE x;
-- 
2.53.0

From 0bf9fb9efaa969f28312753c6db15787cf26ecd2 Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Thu, 4 Dec 2025 18:49:06 +0100
Subject: [PATCH 09/11] use names of currently used temp variables for tab
 complete of DROP VARIABLE, LET and VARIABLE()

---
 src/backend/commands/session_variable.c       | 36 +++++++++++++++++++
 src/bin/psql/tab-complete.in.c                | 17 +++++++++
 src/include/catalog/pg_proc.dat               |  5 +++
 .../expected/session_variables_ddl.out        | 16 +++++++++
 .../regress/sql/session_variables_ddl.sql     |  8 +++++
 5 files changed, 82 insertions(+)

diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c
index 2e3ce5c4017..6430455c0a6 100644
--- a/src/backend/commands/session_variable.c
+++ b/src/backend/commands/session_variable.c
@@ -18,6 +18,7 @@
 #include "commands/session_variable.h"
 #include "executor/executor.h"
 #include "executor/svariableReceiver.h"
+#include "funcapi.h"
 #include "miscadmin.h"
 #include "parser/parse_type.h"
 #include "rewrite/rewriteHandler.h"
@@ -447,3 +448,38 @@ ResetSessionVariables(void)
 	if (SVariableMemoryContext != NULL)
 		MemoryContextReset(SVariableMemoryContext);
 }
+
+/*
+ * pg_get_temporary_session_variables_names
+ *
+ * Returns list of temporary session variables. It is used by psql's
+ * tab complete for DROP VARIABLE and LET commands.
+ */
+Datum
+pg_get_temporary_session_variables_names(PG_FUNCTION_ARGS)
+{
+	InitMaterializedSRF(fcinfo, MAT_SRF_USE_EXPECTED_DESC);
+
+	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[1];
+			bool		nulls[1];
+
+			values[0] = CStringGetTextDatum((NameStr(svar->varname)));
+			nulls[0] = false;
+
+			tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc,
+								 values, nulls);
+		}
+	}
+
+	return (Datum) 0;
+}
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 94311183636..60e2c15ce03 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -1200,6 +1200,11 @@ Keywords_for_list_of_owner_roles, "PUBLIC"
 "  FROM pg_catalog.pg_timezone_names() "\
 " WHERE pg_catalog.quote_literal(pg_catalog.lower(name)) LIKE pg_catalog.lower('%s')"
 
+#define Query_for_list_of_temporary_session_variables \
+"SELECT varname "\
+"   FROM pg_catalog.pg_get_temporary_session_variables_names() AS varname "\
+"  WHERE varname LIKE '%s'"
+
 /* Privilege options shared between GRANT and REVOKE */
 #define Privilege_options_of_grant_and_revoke \
 "SELECT", "INSERT", "UPDATE", "DELETE", "TRUNCATE", "REFERENCES", "TRIGGER", \
@@ -4406,6 +4411,10 @@ 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_QUERY(Query_for_list_of_temporary_session_variables);
+
 /* EXECUTE */
 	else if (Matches("EXECUTE"))
 		COMPLETE_WITH_QUERY(Query_for_list_of_prepared_statements);
@@ -4846,6 +4855,8 @@ match_previous_words(int pattern_id,
 
 /* LET */
 	/* Complete LET <variable> with "=" */
+	else if (Matches("LET"))
+		COMPLETE_WITH_QUERY(Query_for_list_of_temporary_session_variables);
 	else if (TailMatches("LET", MatchAny))
 		COMPLETE_WITH("=");
 
@@ -5414,6 +5425,12 @@ match_previous_words(int pattern_id,
 			COMPLETE_WITH("'standby_replay'", "'standby_write'", "'standby_flush'", "'primary_flush'");
 	}
 
+/*
+ * VARIABLE fence
+ */
+	else if (TailMatches("VARIABLE", "("))
+		COMPLETE_WITH_QUERY(Query_for_list_of_temporary_session_variables);
+
 /* WITH [RECURSIVE] */
 
 	/*
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 83f6501df38..2a5f2ebffb9 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12695,4 +12695,9 @@
   proname => 'hashoid8extended', prorettype => 'int8',
   proargtypes => 'oid8 int8', prosrc => 'hashoid8extended' },
 
+# Session variables support
+{ oid => '8068', descr => 'returns names of temporary session variables',
+  proname => 'pg_get_temporary_session_variables_names', prorows => '1000', proretset => 't',
+  provolatile => 'v', proparallel => 'r', prorettype => 'text', proargtypes => '',
+  prosrc => 'pg_get_temporary_session_variables_names' },
 ]
diff --git a/src/test/regress/expected/session_variables_ddl.out b/src/test/regress/expected/session_variables_ddl.out
index 9f5b088de72..758ce582fca 100644
--- a/src/test/regress/expected/session_variables_ddl.out
+++ b/src/test/regress/expected/session_variables_ddl.out
@@ -54,3 +54,19 @@ NOTICE:  session variable "x" already exists, skipping
 DROP VARIABLE x;
 DROP VARIABLE IF EXISTS x;
 NOTICE:  session variable "x" does not exists, skipping
+CREATE TEMP VARIABLE x AS int;
+CREATE TEMP VARIABLE y AS int;
+SELECT * FROM pg_get_temporary_session_variables_names();
+ pg_get_temporary_session_variables_names 
+------------------------------------------
+ y
+ x
+(2 rows)
+
+DROP VARIABLE x;
+DROP VARIABLE y;
+SELECT * FROM pg_get_temporary_session_variables_names();
+ pg_get_temporary_session_variables_names 
+------------------------------------------
+(0 rows)
+
diff --git a/src/test/regress/sql/session_variables_ddl.sql b/src/test/regress/sql/session_variables_ddl.sql
index 60f78671e3b..c4cbfd17169 100644
--- a/src/test/regress/sql/session_variables_ddl.sql
+++ b/src/test/regress/sql/session_variables_ddl.sql
@@ -70,3 +70,11 @@ CREATE TEMP VARIABLE IF NOT EXISTS x AS int;
 
 DROP VARIABLE x;
 DROP VARIABLE IF EXISTS x;
+
+CREATE TEMP VARIABLE x AS int;
+CREATE TEMP VARIABLE y AS int;
+SELECT * FROM pg_get_temporary_session_variables_names();
+
+DROP VARIABLE x;
+DROP VARIABLE y;
+SELECT * FROM pg_get_temporary_session_variables_names();
-- 
2.53.0

From d8f40663e1a9212466512b97f6098dd9087a47d4 Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Thu, 4 Dec 2025 06:17:54 +0100
Subject: [PATCH 08/11] support CREATE IF NOT EXISTS and DROP IF EXISTS

---
 doc/src/sgml/ref/create_variable.sgml         | 12 ++++-
 doc/src/sgml/ref/drop_variable.sgml           | 12 ++++-
 src/backend/commands/session_variable.c       | 46 +++++++++++++------
 src/backend/parser/gram.y                     | 31 ++++++++++++-
 src/backend/tcop/utility.c                    |  2 +-
 src/include/commands/session_variable.h       |  2 +-
 src/include/nodes/parsenodes.h                |  4 +-
 .../expected/session_variables_ddl.out        |  6 +++
 .../regress/sql/session_variables_ddl.sql     |  6 +++
 9 files changed, 100 insertions(+), 21 deletions(-)

diff --git a/doc/src/sgml/ref/create_variable.sgml b/doc/src/sgml/ref/create_variable.sgml
index 1315b1248c7..def368fc237 100644
--- a/doc/src/sgml/ref/create_variable.sgml
+++ b/doc/src/sgml/ref/create_variable.sgml
@@ -26,7 +26,7 @@ PostgreSQL documentation
 
  <refsynopsisdiv>
 <synopsis>
-CREATE { TEMP | TEMPORARY } VARIABLE <replaceable class="parameter">name</replaceable> [ AS ] <replaceable class="parameter">data_type</replaceable>
+CREATE { TEMP | TEMPORARY } VARIABLE [ IF NOT EXISTS ] <replaceable class="parameter">name</replaceable> [ AS ] <replaceable class="parameter">data_type</replaceable>
 </synopsis>
  </refsynopsisdiv>
  <refsect1>
@@ -69,6 +69,16 @@ CREATE { TEMP | TEMPORARY } VARIABLE <replaceable class="parameter">name</replac
 
   <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>
diff --git a/doc/src/sgml/ref/drop_variable.sgml b/doc/src/sgml/ref/drop_variable.sgml
index dede42e4ffb..5de6a737493 100644
--- a/doc/src/sgml/ref/drop_variable.sgml
+++ b/doc/src/sgml/ref/drop_variable.sgml
@@ -26,7 +26,7 @@ PostgreSQL documentation
 
  <refsynopsisdiv>
 <synopsis>
-DROP VARIABLE <replaceable class="parameter">name</replaceable>
+DROP VARIABLE [ IF EXISTS ] <replaceable class="parameter">name</replaceable>
 </synopsis>
  </refsynopsisdiv>
 
@@ -42,6 +42,16 @@ DROP VARIABLE <replaceable class="parameter">name</replaceable>
  <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>
diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c
index 5cc19216f6e..2e3ce5c4017 100644
--- a/src/backend/commands/session_variable.c
+++ b/src/backend/commands/session_variable.c
@@ -92,7 +92,7 @@ create_sessionvars_hashtables(void)
  * Returns entry of session variable specified by name
  */
 static SVariable
-search_variable(char *varname)
+search_variable(char *varname, bool missing_ok)
 {
 	SVariable	svar;
 
@@ -102,7 +102,7 @@ search_variable(char *varname)
 	svar = (SVariable) hash_search(sessionvars, varname,
 								   HASH_FIND, NULL);
 
-	if (!svar)
+	if (!svar && !missing_ok)
 		ereport(ERROR,
 				(errcode(ERRCODE_UNDEFINED_OBJECT),
 				 errmsg("session variable \"%s\" doesn't exist",
@@ -124,7 +124,7 @@ get_session_variable_type_typmod_collid(char *varname,
 {
 	SVariable	svar;
 
-	svar = search_variable(varname);
+	svar = search_variable(varname, false);
 
 	/* only owner can set content of variable */
 	*typid = svar->vartype;
@@ -144,7 +144,7 @@ GetSessionVariableWithTypecheck(char *varname,
 	SVariable	svar;
 	Datum		result;
 
-	svar = search_variable(varname);
+	svar = search_variable(varname, false);
 
 	if (svar->vartype != typid || svar->vartypmod != typmod)
 		ereport(ERROR,
@@ -181,7 +181,7 @@ SetSessionVariableWithTypecheck(char *varname,
 {
 	SVariable	svar;
 
-	svar = search_variable(varname);
+	svar = search_variable(varname, false);
 
 	if (svar->vartype != typid || svar->vartypmod != typmod)
 		ereport(ERROR,
@@ -277,10 +277,21 @@ CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt)
 					   HASH_ENTER, &found);
 
 	if (found)
-		ereport(ERROR,
-				(errcode(ERRCODE_DUPLICATE_OBJECT),
-				 errmsg("session variable \"%s\" already exists",
-						stmt->name)));
+	{
+		if (stmt->if_not_exists)
+		{
+			ereport(NOTICE,
+					(errcode(ERRCODE_DUPLICATE_OBJECT),
+					 errmsg("session variable \"%s\" already exists, skipping",
+							stmt->name)));
+			return;
+		}
+		else
+			ereport(ERROR,
+					(errcode(ERRCODE_DUPLICATE_OBJECT),
+					 errmsg("session variable \"%s\" already exists",
+							stmt->name)));
+	}
 
 	namestrcpy(&svar->varname, stmt->name);
 	svar->vartype = typeid;
@@ -298,7 +309,7 @@ CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt)
  * Drop variable by name
  */
 void
-DropVariableByName(char *varname)
+DropVariableByName(DropSessionVarStmt *stmt)
 {
 	SVariable	svar;
 
@@ -310,20 +321,27 @@ DropVariableByName(char *varname)
 	PreventCommandIfParallelMode("DROP VARIABLE");
 	PreventCommandDuringRecovery("DROP VARIABLE");
 
-	svar = search_variable(varname);
+	svar = search_variable(stmt->name, stmt->missing_ok);
+	if (!svar)
+	{
+		ereport(NOTICE,
+				(errmsg("session variable \"%s\" does not exists, skipping",
+						stmt->name)));
+		return;
+	}
 
 	/* only owner can get content of variable */
 	if (svar->varowner != GetUserId() && !superuser())
 		ereport(ERROR,
 				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
 				 errmsg("must be owner of session variable %s",
-						varname)));
+						stmt->name)));
 
 	if (!svar->typbyval && !svar->isnull)
 		pfree(DatumGetPointer(svar->value));
 
 	if (hash_search(sessionvars,
-					   varname,
+					   stmt->name,
 					   HASH_REMOVE,
 					   NULL) == NULL)
 		elog(ERROR, "hash table corrupted");
@@ -347,7 +365,7 @@ ExecuteLetStmt(ParseState *pstate,
 	char	   *varname = query->resultVariable;
 	SVariable	svar;
 
-	svar = search_variable(varname);
+	svar = search_variable(varname, false);
 
 	/* only owner can set content of variable */
 	if (svar->varowner != GetUserId() && !superuser())
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 8fa3af60881..80c1f5a08da 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -5394,7 +5394,7 @@ create_extension_opt_item:
 /*****************************************************************************
  *
  *		QUERY :
- *				CREATE { TEMP | TEMPORARY } VARIABLE varname [AS] type
+ *				CREATE { TEMP | TEMPORARY } VARIABLE [IF NOT EXISTS ] varname [AS] type
  *
  *****************************************************************************/
 
@@ -5411,14 +5411,31 @@ CreateSessionVarStmt:
 
 					n->name = $4;
 					n->typeName = $6;
+					n->if_not_exists = false;
+					$$ = (Node *) n;
+				}
+		  | CREATE OptTemp VARIABLE IF_P NOT EXISTS ColId opt_as Typename
+				{
+					CreateSessionVarStmt *n = makeNode(CreateSessionVarStmt);
+
+					if ($2 != RELPERSISTENCE_TEMP)
+						ereport(ERROR,
+							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+							 errmsg("only temporal session variables are supported"),
+							 parser_errposition(@2)));
+
+					n->name = $7;
+					n->typeName = $9;
+					n->if_not_exists = true;
 					$$ = (Node *) n;
 				}
+
 		;
 
 /*****************************************************************************
  *
  *		QUERY :
- *				DROP VARIABLE varname
+ *				DROP VARIABLE [ IF EXISTS ] varname
  *
  *****************************************************************************/
 
@@ -5428,8 +5445,18 @@ DropSessionVarStmt:
 					DropSessionVarStmt *n = makeNode(DropSessionVarStmt);
 
 					n->name = $3;
+					n->missing_ok = false;
+					$$ = (Node *) n;
+				}
+			| DROP VARIABLE IF_P EXISTS ColId
+				{
+					DropSessionVarStmt *n = makeNode(DropSessionVarStmt);
+
+					n->name = $5;
+					n->missing_ok = true;
 					$$ = (Node *) n;
 				}
+
 		;
 
 /*****************************************************************************
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 063759df396..77e85d6f2b1 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -1073,7 +1073,7 @@ standard_ProcessUtility(PlannedStmt *pstmt,
 
 		case T_DropSessionVarStmt:
 			/* No event triggers for catalog less session variables */
-			DropVariableByName(((DropSessionVarStmt *) parsetree)->name);
+			DropVariableByName((DropSessionVarStmt *) parsetree);
 			break;
 
 		case T_LetStmt:
diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h
index cc1aa7ce23b..3f07ae55aac 100644
--- a/src/include/commands/session_variable.h
+++ b/src/include/commands/session_variable.h
@@ -22,7 +22,7 @@
 #include "tcop/cmdtag.h"
 
 extern void CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt);
-extern void DropVariableByName(char *varname);
+extern void DropVariableByName(DropSessionVarStmt *stmt);
 
 extern Datum GetSessionVariableWithTypecheck(char *varname, Oid typid, int32 typmod, bool *isnull);
 extern void SetSessionVariableWithTypecheck(char *varname,
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index ba4516f5f2e..903ab15a9c6 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -3623,6 +3623,7 @@ typedef struct CreateSessionVarStmt
 	NodeTag		type;
 	char	   *name;		/* the variable to create */
 	TypeName   *typeName;		/* the type of variable */
+	bool		if_not_exists;	/* just do nothing if variable already exists? */
 } CreateSessionVarStmt;
 
 /* ----------------------
@@ -3632,7 +3633,8 @@ typedef struct CreateSessionVarStmt
 typedef struct DropSessionVarStmt
 {
 	NodeTag		type;
-	char	   *name;
+	char	   *name;			/* the variable name to drop */
+	bool		missing_ok;		/* skip error of variable is missing */
 } DropSessionVarStmt;

diff --git a/src/test/regress/expected/session_variables_ddl.out b/src/test/regress/expected/session_variables_ddl.out
index c36febd894e..9f5b088de72 100644
--- a/src/test/regress/expected/session_variables_ddl.out
+++ b/src/test/regress/expected/session_variables_ddl.out
@@ -48,3 +48,9 @@ ERROR:  session variable "x" already exists
 DISCARD TEMP;
 -- should be ok
 CREATE TEMP VARIABLE x AS int;
+-- should be ok
+CREATE TEMP VARIABLE IF NOT EXISTS x AS int;
+NOTICE:  session variable "x" already exists, skipping
+DROP VARIABLE x;
+DROP VARIABLE IF EXISTS x;
+NOTICE:  session variable "x" does not exists, skipping
diff --git a/src/test/regress/sql/session_variables_ddl.sql b/src/test/regress/sql/session_variables_ddl.sql
index 7fd739d6677..60f78671e3b 100644
--- a/src/test/regress/sql/session_variables_ddl.sql
+++ b/src/test/regress/sql/session_variables_ddl.sql
@@ -64,3 +64,9 @@ DISCARD TEMP;
 
 -- should be ok
 CREATE TEMP VARIABLE x AS int;
+
+-- should be ok
+CREATE TEMP VARIABLE IF NOT EXISTS x AS int;
+
+DROP VARIABLE x;
+DROP VARIABLE IF EXISTS x;
-- 
2.53.0

From 39135e8f65b16dae24cee97752182b9bf281ca49 Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Mon, 24 Nov 2025 20:04:16 +0100
Subject: [PATCH 07/11] DISCARD TEMP

---
 doc/src/sgml/ref/discard.sgml                 |  3 ++-
 src/backend/commands/discard.c                |  3 +++
 src/backend/commands/session_variable.c       | 20 +++++++++++++++++++
 src/include/commands/session_variable.h       |  2 ++
 .../expected/session_variables_ddl.out        |  7 +++++++
 .../regress/sql/session_variables_ddl.sql     | 10 ++++++++++
 6 files changed, 44 insertions(+), 1 deletion(-)

diff --git a/doc/src/sgml/ref/discard.sgml b/doc/src/sgml/ref/discard.sgml
index bf44c523cac..2700f7b7cd0 100644
--- a/doc/src/sgml/ref/discard.sgml
+++ b/doc/src/sgml/ref/discard.sgml
@@ -70,7 +70,8 @@ DISCARD { ALL | PLANS | SEQUENCES | TEMPORARY | TEMP }
     <term><literal>TEMPORARY</literal> or <literal>TEMP</literal></term>
     <listitem>
      <para>
-      Drops all temporary tables created in the current session.
+      Drops all temporary tables and temporary session variables created in
+      the current session.
      </para>
     </listitem>
    </varlistentry>
diff --git a/src/backend/commands/discard.c b/src/backend/commands/discard.c
index 7b5520b9abe..de6d18fcc5e 100644
--- a/src/backend/commands/discard.c
+++ b/src/backend/commands/discard.c
@@ -19,6 +19,7 @@
 #include "commands/discard.h"
 #include "commands/prepare.h"
 #include "commands/sequence.h"
+#include "commands/session_variable.h"
 #include "utils/guc.h"
 #include "utils/portal.h"
 
@@ -46,6 +47,7 @@ DiscardCommand(DiscardStmt *stmt, bool isTopLevel)
 
 		case DISCARD_TEMP:
 			ResetTempTableNamespace();
+			ResetSessionVariables();
 			break;
 
 		default:
@@ -75,4 +77,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 bfc4191d954..5cc19216f6e 100644
--- a/src/backend/commands/session_variable.c
+++ b/src/backend/commands/session_variable.c
@@ -409,3 +409,23 @@ ExecuteLetStmt(ParseState *pstate,
 
 	PopActiveSnapshot();
 }
+
+/*
+ * 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 TEMP.
+ */
+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/include/commands/session_variable.h b/src/include/commands/session_variable.h
index c4b4d9e6832..cc1aa7ce23b 100644
--- a/src/include/commands/session_variable.h
+++ b/src/include/commands/session_variable.h
@@ -37,4 +37,6 @@ extern void get_session_variable_type_typmod_collid(char *varname,
 extern void ExecuteLetStmt(ParseState *pstate, LetStmt *stmt, ParamListInfo params,
 						   QueryEnvironment *queryEnv, QueryCompletion *qc);
 
+extern void ResetSessionVariables(void);
+
 #endif
diff --git a/src/test/regress/expected/session_variables_ddl.out b/src/test/regress/expected/session_variables_ddl.out
index 45c2d27ab44..c36febd894e 100644
--- a/src/test/regress/expected/session_variables_ddl.out
+++ b/src/test/regress/expected/session_variables_ddl.out
@@ -41,3 +41,10 @@ DROP VARIABLE x;
 SET ROLE TO DEFAULT;
 DROP ROLE regress_session_variable_test_role_01;
 DROP ROLE regress_session_variable_test_role_02;
+CREATE TEMP VARIABLE x AS int;
+-- should fail
+CREATE TEMP VARIABLE x AS int;
+ERROR:  session variable "x" already exists
+DISCARD TEMP;
+-- should be ok
+CREATE TEMP VARIABLE x AS int;
diff --git a/src/test/regress/sql/session_variables_ddl.sql b/src/test/regress/sql/session_variables_ddl.sql
index 34f34dd898f..7fd739d6677 100644
--- a/src/test/regress/sql/session_variables_ddl.sql
+++ b/src/test/regress/sql/session_variables_ddl.sql
@@ -54,3 +54,13 @@ DROP VARIABLE x;
 SET ROLE TO DEFAULT;
 DROP ROLE regress_session_variable_test_role_01;
 DROP ROLE regress_session_variable_test_role_02;
+
+CREATE TEMP VARIABLE x AS int;
+
+-- should fail
+CREATE TEMP VARIABLE x AS int;
+
+DISCARD TEMP;
+
+-- should be ok
+CREATE TEMP VARIABLE x AS int;
-- 
2.53.0

From db5f7bd330be4a46e9c49338799bf3573c485610 Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Sun, 1 Jun 2025 21:20:16 +0200
Subject: [PATCH 05/11] svariableReceiver

allows to store result of the query to session variable

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

diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c
index f8ea8526e1d..deb5d7e80f9 100644
--- a/src/backend/commands/session_variable.c
+++ b/src/backend/commands/session_variable.c
@@ -166,6 +166,56 @@ GetSessionVariableWithTypecheck(char *varname,
 	return result;
 }
 
+/*
+ * Store the given value in a session variable in the cache.
+ */
+void
+SetSessionVariableWithTypecheck(char *varname,
+								Oid typid, int32 typmod,
+								Datum value, bool isnull)
+{
+	SVariable	svar;
+
+	svar = search_variable(varname);
+
+	if (svar->vartype != typid || svar->vartypmod != typmod)
+		ereport(ERROR,
+				(errcode(ERRCODE_DATATYPE_MISMATCH),
+				 errmsg("session variable %s is not of a type %s but type %s",
+						varname,
+						format_type_with_typemod(typid, typmod),
+						format_type_with_typemod(svar->vartype, svar->vartypmod))));
+
+	/* only owner can set content of variable */
+	if (svar->varowner != GetUserId() && !superuser())
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("permission denied for session variable %s",
+						varname)));
+
+	if (!svar->typbyval)
+	{
+		if (!isnull)
+		{
+			MemoryContext oldcxt;
+
+			/*
+			 * Do copy of value in session variables context. This operation
+			 * can fail, so do it before releasing the old content.
+			 */
+			oldcxt = MemoryContextSwitchTo(SVariableMemoryContext);
+			value = datumCopy(value, svar->typbyval, svar->typlen);
+			MemoryContextSwitchTo(oldcxt);
+		}
+
+		if (!svar->isnull)
+			pfree(DatumGetPointer(svar->value));
+	}
+
+	svar->value = value;
+	svar->isnull = isnull;
+}
+
 /*
  * Creates a new variable - does new entry in sessionvars
  *
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 dc45be0b2ce..a572b6dab7c 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..b2709e9211b
--- /dev/null
+++ b/src/backend/executor/svariableReceiver.c
@@ -0,0 +1,149 @@
+/*-------------------------------------------------------------------------
+ *
+ * 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 "access/htup_details.h"
+#include "commands/session_variable.h"
+#include "executor/svariableReceiver.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;
+	char	   *varname;
+	Oid			typid;
+	int32		typmod;
+	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;
+	Form_pg_attribute attr;
+
+	Assert(myState->pub.mydest == DestVariable);
+	Assert(typeinfo->natts == 1);
+
+	attr = TupleDescAttr(typeinfo, 0);
+
+	Assert(!attr->attisdropped);
+
+	myState->typid = attr->atttypid;
+	myState->typmod = attr->atttypmod;
+
+	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")));
+
+	SetSessionVariableWithTypecheck(myState->varname,
+									myState->typid, myState->typmod,
+									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(((SVariableState *) self)->varname);
+	pfree(self);
+}
+
+/*
+ * Initially create a DestReceiver object.
+ */
+DestReceiver *
+CreateVariableDestReceiver(char *varname)
+{
+	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->varname = pstrdup(varname);
+
+	return (DestReceiver *) self;
+}
diff --git a/src/backend/tcop/dest.c b/src/backend/tcop/dest.c
index 931cc563ba6..b72f8769b30 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(NULL);
 	}
 
 	/* 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/commands/session_variable.h b/src/include/commands/session_variable.h
index 3687490bcb1..610b757899e 100644
--- a/src/include/commands/session_variable.h
+++ b/src/include/commands/session_variable.h
@@ -23,6 +23,9 @@ extern void CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt);
 extern void DropVariableByName(char *varname);
 
 extern Datum GetSessionVariableWithTypecheck(char *varname, Oid typid, int32 typmod, bool *isnull);
+extern void SetSessionVariableWithTypecheck(char *varname,
+											Oid typid, int32 typmod,
+											Datum value, bool isnull);
 
 extern void get_session_variable_type_typmod_collid(char *varname,
 													Oid *typid,
diff --git a/src/include/executor/svariableReceiver.h b/src/include/executor/svariableReceiver.h
new file mode 100644
index 00000000000..dd01c93c9e8
--- /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(char *varname);
+
+#endif							/* SVARIABLE_RECEIVER_H */
diff --git a/src/include/tcop/dest.h b/src/include/tcop/dest.h
index 4e4f532d8cc..1e7043dc7f7 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 2adea269fc5..6f80c130798 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2702,6 +2702,7 @@ STRLEN
 SV
 SVariableData
 SVariable
+SVariableState
 SYNCHRONIZATION_BARRIER
 SYSTEM_INFO
 SampleScan
-- 
2.53.0

From 905e3a126f019f6886142859f89d6e653df268fb Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Sat, 22 Nov 2025 06:40:46 +0100
Subject: [PATCH 04/11] 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               |  49 +++++++
 src/include/nodes/execnodes.h                 |  14 ++
 .../expected/session_variables_dml.out        | 135 ++++++++++++++++++
 src/test/regress/parallel_schedule            |   5 +
 .../regress/sql/session_variables_dml.sql     | 120 ++++++++++++++++
 src/tools/pgindent/typedefs.list              |   1 +
 7 files changed, 353 insertions(+)
 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 088eca24021..e722c723649 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 bfd3ebc601e..a80be1388ba 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -44,6 +44,7 @@
 #include "catalog/namespace.h"
 #include "catalog/partition.h"
 #include "commands/matview.h"
+#include "commands/session_variable.h"
 #include "commands/trigger.h"
 #include "executor/executor.h"
 #include "executor/execPartition.h"
@@ -196,6 +197,54 @@ 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 = palloc_array(SessionVariableValue,
+													nSessionVariables);
+
+		/* fill the array */
+		foreach_node(Param, param, queryDesc->plannedstmt->sessionVariables)
+		{
+			estate->es_session_variables[i].value =
+				GetSessionVariableWithTypecheck(param->paramvarname,
+												param->paramtype,
+												param->paramtypmod,
+												&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/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 63c067d5aae..b3af6032c92 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -648,6 +648,16 @@ typedef struct AsyncRequest
 								 * tuples) */
 } AsyncRequest;
 
+/* ----------------
+ * SessionVariableValue
+ * ----------------
+ */
+typedef struct SessionVariableValue
+{
+	bool		isnull;
+	Datum		value;
+} SessionVariableValue;
+
 /* ----------------
  *	  EState information
  *
@@ -707,6 +717,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..1519bf723e0
--- /dev/null
+++ b/src/test/regress/expected/session_variables_dml.out
@@ -0,0 +1,135 @@
+CREATE TEMP VARIABLE temp_var01 AS int;
+-- should not be accessible without variable's fence
+-- should fail
+SELECT temp_var01;
+ERROR:  column "temp_var01" does not exist
+LINE 1: SELECT temp_var01;
+               ^
+-- should be ok
+SELECT VARIABLE(temp_var01);
+ temp_var01 
+------------
+           
+(1 row)
+
+-- should not crash
+DO $$
+BEGIN
+  RAISE NOTICE '%', VARIABLE(temp_var01);
+END;
+$$;
+NOTICE:  <NULL>
+-- variables cannot be used by persistent objects
+-- that checks dependency
+-- should fail
+CREATE TEMP VIEW tempv AS SELECT VARIABLE(temp_var01);
+ERROR:  session variable "temp_var01" cannot be referenced in a catalog object
+CREATE OR REPLACE FUNCTION testvar_sql()
+RETURNS int AS $$
+SELECT VARIABLE(temp_var01);
+$$ LANGUAGE sql;
+SELECT testvar_sql();
+ testvar_sql 
+-------------
+            
+(1 row)
+
+-- session variable cannot be used as parameter of CALL or EXECUTE
+CREATE OR REPLACE PROCEDURE testvar_proc(int)
+AS $$
+BEGIN
+  RAISE NOTICE '%', $1;
+END;
+$$ LANGUAGE plpgsql;
+-- should not crash
+CALL testvar_proc(VARIABLE(temp_var01));
+ERROR:  session variable reference is not supported here
+LINE 1: CALL testvar_proc(VARIABLE(temp_var01));
+                                   ^
+PREPARE prepstmt(int) AS SELECT $1;
+-- should not crash
+EXECUTE prepstmt(VARIABLE(temp_var01));
+ERROR:  session variable reference is not supported here
+LINE 1: EXECUTE prepstmt(VARIABLE(temp_var01));
+                                  ^
+DROP PROCEDURE testvar_proc;
+DEALLOCATE prepstmt;
+CREATE ROLE regress_session_variable_test_role_03;
+CREATE OR REPLACE FUNCTION testvar_sd()
+RETURNS void AS $$
+BEGIN
+  RAISE NOTICE '%', VARIABLE(temp_var01);
+END;
+$$ LANGUAGE plpgsql;
+-- only owner can read data
+SET ROLE TO regress_session_variable_test_role_03;
+-- should fail
+SELECT VARIABLE(temp_var01);
+ERROR:  permission denied for session variable temp_var01
+-- fx with security definer should be ok
+SELECT testvar_sd();
+ERROR:  permission denied for session variable temp_var01
+CONTEXT:  PL/pgSQL expression "VARIABLE(temp_var01)"
+PL/pgSQL function testvar_sd() line 3 at RAISE
+SET ROLE TO default;
+DROP VARIABLE temp_var01;
+-- there is not plan cache invalidation
+-- but still functions that uses dropped variables
+-- should not to crash
+SELECT testvar_sd();
+ERROR:  session variable "temp_var01" doesn't exist
+CONTEXT:  PL/pgSQL expression "VARIABLE(temp_var01)"
+PL/pgSQL function testvar_sd() line 3 at RAISE
+SELECT testvar_sql();
+ERROR:  session variable "temp_var01" doesn't exist
+CONTEXT:  SQL function "testvar_sql" during inlining
+DROP FUNCTION testvar_sql();
+DROP FUNCTION testvar_sd();
+DROP ROLE regress_session_variable_test_role_03;
+CREATE TABLE testvar_testtab(a int);
+CREATE TEMP VARIABLE temp_var02 AS int;
+INSERT INTO testvar_testtab SELECT * FROM generate_series(1,1000);
+CREATE INDEX testvar_testtab_a ON testvar_testtab(a);
+ANALYZE testvar_testtab;
+-- force index
+SET enable_seqscan TO OFF;
+-- index scan should be used
+EXPLAIN (COSTS OFF) SELECT * FROM testvar_testtab WHERE a = VARIABLE(temp_var02);
+                         QUERY PLAN                         
+------------------------------------------------------------
+ Index Only Scan using testvar_testtab_a on testvar_testtab
+   Index Cond: (a = VARIABLE(temp_var02))
+(2 rows)
+
+DROP INDEX testvar_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 testvar_testtab WHERE a = 100;
+                 QUERY PLAN                 
+--------------------------------------------
+ Gather
+   Workers Planned: 2
+   ->  Parallel Seq Scan on testvar_testtab
+         Filter: (a = 100)
+(4 rows)
+
+-- parallel plan should not be used
+EXPLAIN (COSTS OFF) SELECT * FROM testvar_testtab WHERE a = VARIABLE(temp_var02);
+              QUERY PLAN              
+--------------------------------------
+ Seq Scan on testvar_testtab
+   Filter: (a = VARIABLE(temp_var02))
+(2 rows)
+
+RESET parallel_setup_cost;
+RESET parallel_tuple_cost;
+RESET min_parallel_table_scan_size;
+RESET max_parallel_workers_per_gather;
+DROP TABLE testvar_testtab;
+DROP VARIABLE temp_var02;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 8f9988954f9..f889ac26b08 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -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_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..bf56b19467b
--- /dev/null
+++ b/src/test/regress/sql/session_variables_dml.sql
@@ -0,0 +1,120 @@
+CREATE TEMP VARIABLE temp_var01 AS int;
+
+-- should not be accessible without variable's fence
+-- should fail
+SELECT temp_var01;
+
+-- should be ok
+SELECT VARIABLE(temp_var01);
+
+-- should not crash
+DO $$
+BEGIN
+  RAISE NOTICE '%', VARIABLE(temp_var01);
+END;
+$$;
+
+-- variables cannot be used by persistent objects
+-- that checks dependency
+-- should fail
+CREATE TEMP VIEW tempv AS SELECT VARIABLE(temp_var01);
+
+CREATE OR REPLACE FUNCTION testvar_sql()
+RETURNS int AS $$
+SELECT VARIABLE(temp_var01);
+$$ LANGUAGE sql;
+
+SELECT testvar_sql();
+
+-- session variable cannot be used as parameter of CALL or EXECUTE
+CREATE OR REPLACE PROCEDURE testvar_proc(int)
+AS $$
+BEGIN
+  RAISE NOTICE '%', $1;
+END;
+$$ LANGUAGE plpgsql;
+
+-- should not crash
+CALL testvar_proc(VARIABLE(temp_var01));
+
+PREPARE prepstmt(int) AS SELECT $1;
+
+-- should not crash
+EXECUTE prepstmt(VARIABLE(temp_var01));
+
+DROP PROCEDURE testvar_proc;
+DEALLOCATE prepstmt;
+
+CREATE ROLE regress_session_variable_test_role_03;
+
+CREATE OR REPLACE FUNCTION testvar_sd()
+RETURNS void AS $$
+BEGIN
+  RAISE NOTICE '%', VARIABLE(temp_var01);
+END;
+$$ LANGUAGE plpgsql;
+
+-- only owner can read data
+SET ROLE TO regress_session_variable_test_role_03;
+
+-- should fail
+SELECT VARIABLE(temp_var01);
+
+-- fx with security definer should be ok
+SELECT testvar_sd();
+
+SET ROLE TO default;
+
+DROP VARIABLE temp_var01;
+
+-- there is not plan cache invalidation
+-- but still functions that uses dropped variables
+-- should not to crash
+
+SELECT testvar_sd();
+SELECT testvar_sql();
+
+DROP FUNCTION testvar_sql();
+DROP FUNCTION testvar_sd();
+
+DROP ROLE regress_session_variable_test_role_03;
+
+CREATE TABLE testvar_testtab(a int);
+CREATE TEMP VARIABLE temp_var02 AS int;
+
+INSERT INTO testvar_testtab SELECT * FROM generate_series(1,1000);
+
+CREATE INDEX testvar_testtab_a ON testvar_testtab(a);
+
+ANALYZE testvar_testtab;
+
+-- force index
+SET enable_seqscan TO OFF;
+
+-- index scan should be used
+EXPLAIN (COSTS OFF) SELECT * FROM testvar_testtab WHERE a = VARIABLE(temp_var02);
+
+DROP INDEX testvar_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 testvar_testtab WHERE a = 100;
+
+-- parallel plan should not be used
+EXPLAIN (COSTS OFF) SELECT * FROM testvar_testtab WHERE a = VARIABLE(temp_var02);
+
+RESET parallel_setup_cost;
+RESET parallel_tuple_cost;
+RESET min_parallel_table_scan_size;
+RESET max_parallel_workers_per_gather;
+
+DROP TABLE testvar_testtab;
+DROP VARIABLE temp_var02;
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 5d38d12b7c4..2adea269fc5 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2760,6 +2760,7 @@ SerializedTransactionState
 Session
 SessionBackupState
 SessionEndType
+SessionVariableValue
 SetConstraintState
 SetConstraintStateData
 SetConstraintTriggerData
-- 
2.53.0

From b1af6cf1f7e2cc5e5c42a7f2abfae331cdc1a6c0 Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Mon, 24 Nov 2025 18:05:03 +0100
Subject: [PATCH 06/11] 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                         |  22 ++++
 doc/src/sgml/ref/allfiles.sgml                |   1 +
 doc/src/sgml/ref/create_variable.sgml         |   3 +
 doc/src/sgml/ref/drop_variable.sgml           |   1 +
 doc/src/sgml/ref/let.sgml                     |  95 ++++++++++++++
 doc/src/sgml/reference.sgml                   |   1 +
 src/backend/commands/session_variable.c       |  86 ++++++++++++
 src/backend/nodes/nodeFuncs.c                 |   8 ++
 src/backend/optimizer/plan/planner.c          |   1 +
 src/backend/parser/analyze.c                  | 103 ++++++++++++---
 src/backend/parser/gram.y                     |  38 +++++-
 src/backend/parser/parse_agg.c                |   7 +
 src/backend/parser/parse_expr.c               |   9 ++
 src/backend/parser/parse_func.c               |   3 +
 src/backend/tcop/utility.c                    |  15 +++
 src/bin/psql/tab-complete.in.c                |   9 +-
 src/include/commands/session_variable.h       |   5 +
 src/include/nodes/parsenodes.h                |  15 +++
 src/include/nodes/pathnodes.h                 |   3 +
 src/include/parser/kwlist.h                   |   1 +
 src/include/parser/parse_node.h               |   1 +
 src/include/tcop/cmdtaglist.h                 |   1 +
 .../expected/session_variables_dml.out        | 123 ++++++++++++++++++
 .../regress/sql/session_variables_dml.sql     |  87 +++++++++++++
 src/tools/pgindent/typedefs.list              |   1 +
 25 files changed, 619 insertions(+), 20 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 8c0ae715869..19d2afc2201 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -5717,10 +5717,32 @@ EXPLAIN SELECT count(*) FROM measurement WHERE logdate &gt;= DATE '2008-01-01';
     <literal>VARIABLE(varname)</literal> syntax. This avoids any risk of
     collision between variable names and column names.
    </para>
+
+   <para>
+    You set the value of a session variable with the <command>LET</command>
+    statement and retrieve it with <command>SELECT</command>:
+<programlisting>
+CREATE TEMP VARIABLE var1 AS date;
+LET var1 = current_date;
+SELECT VARIABLE(var1);
+</programlisting>
+
+    or
+
 <programlisting>
+CREATE TEMP VARIABLE current_user_id AS integer;
+LET current_user_id = (SELECT id FROM users WHERE usename = session_user);
 SELECT VARIABLE(current_user_id);
 </programlisting>
    </para>
+
+   <para>
+    By default, retrieving a session variable returns
+    <literal>NULL</literal> unless it has been set in the current session
+    using the <command>LET</command> command. Session variables are not
+    transactional: changes to their values persist even if the transaction
+    is rolled back, similar to variables in procedural languages
+   </para>
  </sect1>
 
  <sect1 id="ddl-others">
diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml
index a7349919658..cd3faa667f0 100644
--- a/doc/src/sgml/ref/allfiles.sgml
+++ b/doc/src/sgml/ref/allfiles.sgml
@@ -157,6 +157,7 @@ Complete list of usable sgml source files in this directory.
 <!ENTITY grant              SYSTEM "grant.sgml">
 <!ENTITY importForeignSchema SYSTEM "import_foreign_schema.sgml">
 <!ENTITY insert             SYSTEM "insert.sgml">
+<!ENTITY let                SYSTEM "let.sgml">
 <!ENTITY listen             SYSTEM "listen.sgml">
 <!ENTITY load               SYSTEM "load.sgml">
 <!ENTITY lock               SYSTEM "lock.sgml">
diff --git a/doc/src/sgml/ref/create_variable.sgml b/doc/src/sgml/ref/create_variable.sgml
index 4e8c1940252..1315b1248c7 100644
--- a/doc/src/sgml/ref/create_variable.sgml
+++ b/doc/src/sgml/ref/create_variable.sgml
@@ -108,6 +108,8 @@ CREATE { TEMP | TEMPORARY } VARIABLE <replaceable class="parameter">name</replac
    Create an date session variable <literal>var1</literal>:
 <programlisting>
 CREATE TEMPORARY VARIABLE var1 AS date;
+LET var1 = current_date;
+SELECT VARIABLE(var1);
 </programlisting>
   </para>
 
@@ -127,6 +129,7 @@ CREATE TEMPORARY VARIABLE var1 AS date;
 
   <simplelist type="inline">
    <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 e8517a78200..dede42e4ffb 100644
--- a/doc/src/sgml/ref/drop_variable.sgml
+++ b/doc/src/sgml/ref/drop_variable.sgml
@@ -78,6 +78,7 @@ DROP VARIABLE var1;
 
   <simplelist type="inline">
    <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..33ee42d3f20
--- /dev/null
+++ b/doc/src/sgml/ref/let.sgml
@@ -0,0 +1,95 @@
+<!--
+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 TEMPORARY 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-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 c03e7692c7a..6fcd7a81321 100644
--- a/doc/src/sgml/reference.sgml
+++ b/doc/src/sgml/reference.sgml
@@ -185,6 +185,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 deb5d7e80f9..bfc4191d954 100644
--- a/src/backend/commands/session_variable.c
+++ b/src/backend/commands/session_variable.c
@@ -16,13 +16,18 @@
 
 #include "catalog/pg_language.h"
 #include "commands/session_variable.h"
+#include "executor/executor.h"
+#include "executor/svariableReceiver.h"
 #include "miscadmin.h"
 #include "parser/parse_type.h"
+#include "rewrite/rewriteHandler.h"
 #include "storage/proc.h"
+#include "tcop/tcopprot.h"
 #include "utils/builtins.h"
 #include "utils/datum.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
+#include "utils/snapmgr.h"
 
 /*
  * The session variables are stored in the backend's private memory (data,
@@ -323,3 +328,84 @@ DropVariableByName(char *varname)
 					   NULL) == NULL)
 		elog(ERROR, "hash table corrupted");
 }
+
+/*
+ * 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;
+	PlannedStmt *plan;
+	QueryDesc  *queryDesc;
+	char	   *varname = query->resultVariable;
+	SVariable	svar;
+
+	svar = search_variable(varname);
+
+	/* only owner can set content of variable */
+	if (svar->varowner != GetUserId() && !superuser())
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("permission denied for session variable %s",
+						varname)));
+
+	/* create a dest receiver for LET */
+	dest = CreateVariableDestReceiver(varname);
+
+	/* 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, NULL);
+
+	/*
+	 * 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/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index 4e261a49128..de1e1c6421f 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -4377,6 +4377,14 @@ raw_expression_tree_walker_impl(Node *node,
 					return true;
 			}
 			break;
+		case T_LetStmt:
+			{
+				LetStmt    *stmt = (LetStmt *) node;
+
+				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 f82bebf6a83..6e64a68b9a0 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -375,6 +375,7 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
 	glob->partition_directory = NULL;
 	glob->rel_notnullatts_hash = NULL;
 	glob->sessionVariables = NIL;
+	glob->resultVariable = parse->resultVariable;
 
 	/*
 	 * Assess whether it's feasible to use parallel mode for this query. We
diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
index e8ed16bb6fd..f883ffba5a4 100644
--- a/src/backend/parser/analyze.c
+++ b/src/backend/parser/analyze.c
@@ -53,15 +53,18 @@
 #include "utils/builtins.h"
 #include "utils/guc.h"
 #include "utils/rel.h"
+#include "utils/lsyscache.h"
 #include "utils/syscache.h"

-/* Passthrough data for transformPLAssignStmtTarget */
+/* Passthrough data for transformAssignTarget */
 typedef struct SelectStmtPassthrough
 {
-	PLAssignStmt *stmt;			/* the assignment statement */
+	Node	   *stmt;			/* the assignment statement */
 	Node	   *target;			/* node representing the target variable */
+	char	   *target_name;	/* the name used by err */
 	List	   *indirection;	/* indirection yet to be applied to target */
+	CoercionContext ccontext; 	/* context indicators to control coercions */
 } SelectStmtPassthrough;
 
 /* Hook for plugins to get control at end of parse analysis */
@@ -85,7 +88,7 @@ static Query *transformReturnStmt(ParseState *pstate, ReturnStmt *stmt);
 static Query *transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt);
 static Query *transformPLAssignStmt(ParseState *pstate,
 									PLAssignStmt *stmt);
-static List *transformPLAssignStmtTarget(ParseState *pstate, List *tlist,
+static List *transformAssignTarget(ParseState *pstate, List *tlist,
 										 SelectStmtPassthrough *passthru);
 static Query *transformDeclareCursorStmt(ParseState *pstate,
 										 DeclareCursorStmt *stmt);
@@ -95,6 +98,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
@@ -342,6 +347,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:
@@ -421,6 +427,11 @@ transformStmt(ParseState *pstate, Node *parseTree)
 									   (CallStmt *) parseTree);
 			break;
 
+		case T_LetStmt:
+			result = transformLetStmt(pstate,
+									  (LetStmt *) parseTree);
+			break;
+
 		default:
 
 			/*
@@ -482,6 +493,7 @@ stmt_requires_parse_analysis(RawStmt *parseTree)
 		case T_ExplainStmt:
 		case T_CreateTableAsStmt:
 		case T_CallStmt:
+		case T_LetStmt:
 			result = true;
 			break;
 
@@ -547,6 +559,7 @@ query_requires_rewrite_plan(Query *query)
 			case T_ExplainStmt:
 			case T_CreateTableAsStmt:
 			case T_CallStmt:
+			case T_LetStmt:
 				result = true;
 				break;
 			default:
@@ -1400,7 +1413,7 @@ count_rowexpr_columns(ParseState *pstate, Node *expr)
  *
  * This function is also used to transform the source expression of a
  * PLAssignStmt.  In that usage, passthru is non-NULL and we need to
- * call transformPLAssignStmtTarget after the initial transformation of the
+ * call transformAssignTarget after the initial transformation of the
  * SELECT's targetlist.  (We could generalize this into an arbitrary callback
  * function, but for now that would just be more notation with no benefit.)
  * All the rest is the same as a regular SelectStmt.
@@ -1453,8 +1466,8 @@ transformSelectStmt(ParseState *pstate, SelectStmt *stmt,
 	 * Otherwise, mark column origins (which are useless in a PLAssignStmt).
 	 */
 	if (passthru)
-		qry->targetList = transformPLAssignStmtTarget(pstate, qry->targetList,
-													  passthru);
+		qry->targetList = transformAssignTarget(pstate, qry->targetList,
+												passthru);
 	else
 		markTargetListOrigins(pstate, qry->targetList);
 
@@ -2850,9 +2863,11 @@ transformPLAssignStmt(ParseState *pstate, PLAssignStmt *stmt)
 						   EXPR_KIND_UPDATE_TARGET);
 
 	/* Set up passthrough data for transformPLAssignStmtTarget */
-	passthru.stmt = stmt;
+	passthru.stmt = (Node *) stmt;
 	passthru.target = target;
+	passthru.target_name = stmt->name;
 	passthru.indirection = indirection;
+	passthru.ccontext = COERCION_PLPGSQL;
 
 	/*
 	 * To avoid duplicating a lot of code, we use transformSelectStmt to do
@@ -2875,18 +2890,21 @@ transformPLAssignStmt(ParseState *pstate, PLAssignStmt *stmt)
 
 /*
  * Callback function to adjust a SELECT's tlist to make the output suitable
- * for assignment to a PLAssignStmt's target variable.
+ * for assignment to a PLAssignStmt's target variable pr LET's target
+ * session variable.
  *
  * Note: we actually modify the tle->expr in-place, but the function's API
  * is set up to not presume that.
  */
 static List *
-transformPLAssignStmtTarget(ParseState *pstate, List *tlist,
-							SelectStmtPassthrough *passthru)
+transformAssignTarget(ParseState *pstate, List *tlist,
+					  SelectStmtPassthrough *passthru)
 {
-	PLAssignStmt *stmt = passthru->stmt;
+	Node	   *stmt = passthru->stmt;
 	Node	   *target = passthru->target;
+	char	   *target_name = passthru->target_name;
 	List	   *indirection = passthru->indirection;
+	CoercionContext ccontext = passthru->ccontext;
 	Oid			targettype;
 	int32		targettypmod;
 	Oid			targetcollation;
@@ -2921,7 +2939,7 @@ transformPLAssignStmtTarget(ParseState *pstate, List *tlist,
 		tle->expr = (Expr *)
 			transformAssignmentIndirection(pstate,
 										   target,
-										   stmt->name,
+										   target_name,
 										   false,
 										   targettype,
 										   targettypmod,
@@ -2929,10 +2947,10 @@ transformPLAssignStmtTarget(ParseState *pstate, List *tlist,
 										   indirection,
 										   list_head(indirection),
 										   (Node *) tle->expr,
-										   COERCION_PLPGSQL,
+										   ccontext,
 										   exprLocation(target));
 	}
-	else if (targettype != type_id &&
+	else if (IsA(stmt, PLAssignStmt) && targettype != type_id &&
 			 (targettype == RECORDOID || ISCOMPLEX(targettype)) &&
 			 (type_id == RECORDOID || ISCOMPLEX(type_id)))
 	{
@@ -2955,7 +2973,7 @@ transformPLAssignStmtTarget(ParseState *pstate, List *tlist,
 			coerce_to_target_type(pstate,
 								  orig_expr, type_id,
 								  targettype, targettypmod,
-								  COERCION_PLPGSQL,
+								  ccontext,
 								  COERCE_IMPLICIT_CAST,
 								  -1);
 		/* With COERCION_PLPGSQL, this error is probably unreachable */
@@ -2964,7 +2982,7 @@ transformPLAssignStmtTarget(ParseState *pstate, List *tlist,
 					(errcode(ERRCODE_DATATYPE_MISMATCH),
 					 errmsg("variable \"%s\" is of type %s"
 							" but expression is of type %s",
-							stmt->name,
+							target_name,
 							format_type_be(targettype),
 							format_type_be(type_id)),
 					 errhint("You will need to rewrite or cast the expression."),
@@ -3332,6 +3350,59 @@ transformCallStmt(ParseState *pstate, CallStmt *stmt)
 	return result;
 }
 
+/*
+ * transformLetStmt -
+ *	  transform an Let Statement
+ */
+static Query *
+transformLetStmt(ParseState *pstate, LetStmt *stmt)
+{
+	Query	   *qry;
+	Query	   *result;
+	Node	   *target;
+	VariableFence *vf;
+	SelectStmtPassthrough passthru;
+	Param	   *paramvar;
+
+	/* gram allows only SELECT */
+	Assert(IsA(stmt->query, SelectStmt));
+
+	/* Use implicit VariableFence for forcing session variables */
+	vf = makeNode(VariableFence);
+	vf->varname = stmt->target;
+	vf->location = stmt->location;
+
+	target = transformExpr(pstate, (Node *) vf, EXPR_KIND_LET_TARGET);
+
+	paramvar = castNode(Param, target);
+
+	Assert(paramvar->paramkind == PARAM_VARIABLE);
+
+	/* Set up passthrough data for transformAssignTarget */
+	passthru.stmt = (Node *) stmt;
+	passthru.target = (Node *) paramvar;
+	passthru.target_name = paramvar->paramvarname;
+	passthru.indirection = NIL;
+	passthru.ccontext = COERCION_ASSIGNMENT;
+
+	/* we need to postpone conversion of "unknown" to text */
+	pstate->p_resolve_unknowns = false;
+
+	qry = transformSelectStmt(pstate, (SelectStmt *) stmt->query, &passthru);
+
+	qry->resultVariable = paramvar->paramvarname;
+	qry->canSetTag = true;
+
+	stmt->query = (Node *) qry;
+
+	/* 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 47d9c66b2b0..8fa3af60881 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -302,7 +302,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
@@ -750,7 +750,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 LET LEFT LEVEL LIKE LIMIT LISTEN LOAD LOCAL
 	LOCALTIME LOCALTIMESTAMP LOCATION LOCK_P LOCKED LOGGED LSN_P
 
 	MAPPING MATCH MATCHED MATERIALIZED MAXVALUE MERGE MERGE_ACTION METHOD
@@ -1098,6 +1098,7 @@ stmt:
 			| ImportForeignSchemaStmt
 			| IndexStmt
 			| InsertStmt
+			| LetStmt
 			| ListenStmt
 			| RefreshMatViewStmt
 			| LoadStmt
@@ -13002,6 +13003,37 @@ opt_hold: /* EMPTY */						{ $$ = 0; }
 			| WITHOUT HOLD					{ $$ = 0; }
 		;
 
+/*****************************************************************************
+ *
+ *		QUERY:
+ *				LET STATEMENT
+ *
+ *****************************************************************************/
+LetStmt:	LET ColId '=' a_expr
+				{
+					LetStmt	   *n = makeNode(LetStmt);
+					SelectStmt *select;
+					ResTarget  *res;
+
+					n->target = $2;
+
+					select = makeNode(SelectStmt);
+					res = makeNode(ResTarget);
+
+					/* create target list for implicit query */
+					res->name = NULL;
+					res->indirection = NIL;
+					res->val = (Node *) $4;
+					res->location = @4;
+
+					select->targetList = list_make1(res);
+					n->query = (Node *) select;
+
+					n->location = @2;
+					$$ = (Node *) n;
+				}
+		;
+
 /*****************************************************************************
  *
  *		QUERY:
@@ -18112,6 +18144,7 @@ unreserved_keyword:
 			| LARGE_P
 			| LAST_P
 			| LEAKPROOF
+			| LET
 			| LEVEL
 			| LISTEN
 			| LOAD
@@ -18730,6 +18763,7 @@ bare_label_keyword:
 			| LEAKPROOF
 			| LEAST
 			| LEFT
+			| LET
 			| LEVEL
 			| LIKE
 			| LISTEN
diff --git a/src/backend/parser/parse_agg.c b/src/backend/parser/parse_agg.c
index 25ee0f87d93..2d7c40f7bbb 100644
--- a/src/backend/parser/parse_agg.c
+++ b/src/backend/parser/parse_agg.c
@@ -584,6 +584,10 @@ check_agglevels_and_constraints(ParseState *pstate, Node *expr)
 			errkind = true;
 			break;
 
+		case EXPR_KIND_LET_TARGET:
+			errkind = true;
+			break;
+
 			/*
 			 * There is intentionally no default: case here, so that the
 			 * compiler will warn if we add a new ParseExprKind without
@@ -1023,6 +1027,9 @@ transformWindowFuncCall(ParseState *pstate, WindowFunc *wfunc,
 		case EXPR_KIND_CYCLE_MARK:
 			errkind = true;
 			break;
+		case EXPR_KIND_LET_TARGET:
+			errkind = true;
+			break;
 
 			/*
 			 * There is intentionally no default: case here, so that the
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index aa110e80b06..2a75e6ee6c4 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -592,6 +592,9 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
 		case EXPR_KIND_PARTITION_BOUND:
 			err = _("cannot use column reference in partition bound expression");
 			break;
+		case EXPR_KIND_LET_TARGET:
+			err = _("cannot use column reference as target of LET command");
+			break;
 
 			/*
 			 * There is intentionally no default: case here, so that the
@@ -962,6 +965,7 @@ expr_kind_allows_session_variables(ParseExprKind p_expr_kind)
 		case EXPR_KIND_RETURNING:
 		case EXPR_KIND_VALUES:
 		case EXPR_KIND_VALUES_SINGLE:
+		case EXPR_KIND_LET_TARGET:
 			result = true;
 			break;
 
@@ -1990,6 +1994,9 @@ transformSubLink(ParseState *pstate, SubLink *sublink)
 		case EXPR_KIND_GENERATED_COLUMN:
 			err = _("cannot use subquery in column generation expression");
 			break;
+		case EXPR_KIND_LET_TARGET:
+			err = _("cannot use subquery as a target of LET command");
+			break;
 
 			/*
 			 * There is intentionally no default: case here, so that the
@@ -3349,6 +3356,8 @@ ParseExprKindName(ParseExprKind exprKind)
 			return "GENERATED AS";
 		case EXPR_KIND_CYCLE_MARK:
 			return "CYCLE";
+		case EXPR_KIND_LET_TARGET:
+			return "LET";
 
 			/*
 			 * There is intentionally no default: case here, so that the
diff --git a/src/backend/parser/parse_func.c b/src/backend/parser/parse_func.c
index 24f6745923b..2e63a56bee1 100644
--- a/src/backend/parser/parse_func.c
+++ b/src/backend/parser/parse_func.c
@@ -2783,6 +2783,9 @@ check_srf_call_placement(ParseState *pstate, Node *last_srf, int location)
 		case EXPR_KIND_CYCLE_MARK:
 			errkind = true;
 			break;
+		case EXPR_KIND_LET_TARGET:
+			errkind = true;
+			break;
 
 			/*
 			 * There is intentionally no default: case here, so that the
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 41a7edfe53d..063759df396 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -237,6 +237,7 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree)
 
 		case T_CallStmt:
 		case T_DoStmt:
+		case T_LetStmt:
 			{
 				/*
 				 * Commands inside the DO block or the called procedure might
@@ -1075,6 +1076,11 @@ standard_ProcessUtility(PlannedStmt *pstmt,
 			DropVariableByName(((DropSessionVarStmt *) parsetree)->name);
 			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,
@@ -2220,6 +2226,10 @@ UtilityContainsQuery(Node *parsetree)
 				return UtilityContainsQuery(qry->utilityStmt);
 			return qry;
 
+		case T_LetStmt:
+			qry = castNode(Query, ((LetStmt *) parsetree)->query);
+			return qry;
+
 		default:
 			return NULL;
 	}
@@ -2415,6 +2425,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:
 			{
@@ -3305,6 +3319,7 @@ GetCommandLogLevel(Node *parsetree)
 			break;
 
 		case T_PLAssignStmt:
+		case T_LetStmt:
 			lev = LOGSTMT_ALL;
 			break;
 
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index b829eed2ab0..94311183636 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -1265,8 +1265,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",
@@ -4844,6 +4844,11 @@ match_previous_words(int pattern_id,
 	else if (TailMatches("VALUES") && !TailMatches("DEFAULT", "VALUES"))
 		COMPLETE_WITH("(");
 
+/* LET */
+	/* 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 610b757899e..c4b4d9e6832 100644
--- a/src/include/commands/session_variable.h
+++ b/src/include/commands/session_variable.h
@@ -16,8 +16,10 @@
 #define SESSIONVARIABLE_H
 
 #include "catalog/objectaddress.h"
+#include "nodes/params.h"
 #include "parser/parse_node.h"
 #include "nodes/parsenodes.h"
+#include "tcop/cmdtag.h"
 
 extern void CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt);
 extern void DropVariableByName(char *varname);
@@ -32,4 +34,7 @@ extern void get_session_variable_type_typmod_collid(char *varname,
 													int32 *typmod,
 													Oid *collid);
 
+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 b7fa522e57f..ba4516f5f2e 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 */
+	char	   *resultVariable;
+
 	/* has aggregates in tlist or havingQual */
 	bool		hasAggs pg_node_attr(query_jumble_ignore);
 	/* has window functions in tlist */
@@ -2199,6 +2202,18 @@ typedef struct MergeStmt
 	WithClause *withClause;		/* WITH clause */
 } MergeStmt;
 
+/* ----------------------
+ *		Let Statement
+ * ----------------------
+ */
+typedef struct LetStmt
+{
+	NodeTag		type;
+	char	   *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 7acc9e51f0e..8407f8affe1 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -274,6 +274,9 @@ typedef struct PlannerGlobal
 
 	/* list of used session variables */
 	List	   *sessionVariables;
+
+	/* name of session variable used like target of LET command */
+	char	   *resultVariable;
 } PlannerGlobal;
 
 /* macro for fetching the Plan associated with a SubPlan node */
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index d4b1223a33e..ca61e18d8ab 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -258,6 +258,7 @@ PG_KEYWORD("leading", LEADING, RESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("leakproof", LEAKPROOF, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("least", LEAST, COL_NAME_KEYWORD, BARE_LABEL)
 PG_KEYWORD("left", LEFT, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL)
+PG_KEYWORD("let", LET, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("level", LEVEL, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("like", LIKE, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL)
 PG_KEYWORD("limit", LIMIT, RESERVED_KEYWORD, AS_LABEL)
diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h
index 278598a90f7..3aa874e4910 100644
--- a/src/include/parser/parse_node.h
+++ b/src/include/parser/parse_node.h
@@ -82,6 +82,7 @@ typedef enum ParseExprKind
 	EXPR_KIND_COPY_WHERE,		/* WHERE condition in COPY FROM */
 	EXPR_KIND_GENERATED_COLUMN, /* generation expression for a column */
 	EXPR_KIND_CYCLE_MARK,		/* cycle mark value */
+	EXPR_KIND_LET_TARGET,		/* only session variables */
 } ParseExprKind;

diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h
index 4b964d0fb5a..945a3a0f24b 100644
--- a/src/include/tcop/cmdtaglist.h
+++ b/src/include/tcop/cmdtaglist.h
@@ -185,6 +185,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 1519bf723e0..b87967bd7d5 100644
--- a/src/test/regress/expected/session_variables_dml.out
+++ b/src/test/regress/expected/session_variables_dml.out
@@ -133,3 +133,126 @@ RESET min_parallel_table_scan_size;
 RESET max_parallel_workers_per_gather;
 DROP TABLE testvar_testtab;
 DROP VARIABLE temp_var02;
+CREATE TEMP VARIABLE temp_var03 AS numeric;
+-- LET stmt is not allowed inside CTE
+WITH x AS (LET  temp_var03 = 3.14) SELECT * FROM x;
+ERROR:  syntax error at or near "LET"
+LINE 1: WITH x AS (LET  temp_var03 = 3.14) SELECT * FROM x;
+                   ^
+-- LET stmt requires result with exactly one row
+LET temp_var03 = generate_series(1,1);
+SELECT VARIABLE(temp_var03);
+ temp_var03 
+------------
+          1
+(1 row)
+
+-- should fail
+LET temp_var03 = generate_series(1,2);
+ERROR:  expression returned more than one row
+LET temp_var03 = generate_series(1,0);
+ERROR:  expression returned no rows
+CREATE OR REPLACE FUNCTION testvar_sql01(numeric)
+RETURNS void AS $$
+LET temp_var03 = $1;
+$$ LANGUAGE sql;
+CREATE OR REPLACE FUNCTION testvar_sql02()
+RETURNS numeric AS $$
+SELECT VARIABLE(temp_var03);
+$$ LANGUAGE sql;
+SELECT testvar_sql01(3.14);
+ testvar_sql01 
+---------------
+ 
+(1 row)
+
+SELECT testvar_sql02(), VARIABLE(temp_var03);
+ testvar_sql02 | temp_var03 
+---------------+------------
+          3.14 |       3.14
+(1 row)
+
+CREATE OR REPLACE FUNCTION testvar_pl(varchar)
+RETURNS varchar AS $$
+BEGIN
+  LET temp_var03 = $1::numeric;
+  RETURN VARIABLE(temp_var03);
+END
+$$ LANGUAGE plpgsql SECURITY DEFINER;
+SELECT testvar_pl('3.14');
+ testvar_pl 
+------------
+ 3.14
+(1 row)
+
+DROP VARIABLE temp_var03;
+SET plan_cache_mode to force_generic_plan;
+-- should not crash
+SELECT testvar_sql01(3.14);
+ERROR:  session variable "temp_var03" doesn't exist
+CONTEXT:  SQL function "testvar_sql01" during inlining
+SELECT testvar_sql02(), VARIABLE(temp_var03);
+ERROR:  session variable "temp_var03" doesn't exist
+SELECT testvar_pl('3.141592');
+ERROR:  session variable "temp_var03" doesn't exist
+CONTEXT:  SQL statement "LET temp_var03 = $1::numeric"
+PL/pgSQL function testvar_pl(character varying) line 3 at SQL statement
+-- can work again if we create variable
+CREATE TEMP VARIABLE temp_var03 AS numeric;
+SELECT testvar_sql01(3.14);
+ testvar_sql01 
+---------------
+ 
+(1 row)
+
+SELECT testvar_sql02(), VARIABLE(temp_var03);
+ testvar_sql02 | temp_var03 
+---------------+------------
+          3.14 |       3.14
+(1 row)
+
+SELECT testvar_pl('3.141592');
+ testvar_pl 
+------------
+ 3.141592
+(1 row)
+
+CREATE ROLE regress_session_variable_test_role_04;
+SET ROLE regress_session_variable_test_role_04;
+-- should fail
+SELECT testvar_sql01(3.14);
+ERROR:  permission denied for session variable temp_var03
+CONTEXT:  SQL function "testvar_sql01" statement 1
+-- should be ok (security definer)
+SELECT testvar_pl('3.141592');
+ testvar_pl 
+------------
+ 3.141592
+(1 row)
+
+SET ROLE TO DEFAULT;
+DROP FUNCTION testvar_sql01(numeric);
+DROP FUNCTION testvar_sql02();
+DROP FUNCTION testvar_pl(varchar);
+DROP ROLE regress_session_variable_test_role_04;
+DROP VARIABLE temp_var03;
+SET plan_cache_mode TO DEFAULT;
+-- test extended query protocol
+CREATE TEMP VARIABLE temp_var04 AS int;
+LET temp_var04 = $1 \bind 10 \g
+SELECT VARIABLE(temp_var04);
+ temp_var04 
+------------
+         10
+(1 row)
+
+LET temp_var04 = $1 \parse letps
+\bind_named letps 100 \g
+SELECT VARIABLE(temp_var04);
+ temp_var04 
+------------
+        100
+(1 row)
+
+\close_prepared letps
+DROP VARIABLE temp_var04;
diff --git a/src/test/regress/sql/session_variables_dml.sql b/src/test/regress/sql/session_variables_dml.sql
index bf56b19467b..b8408c97cad 100644
--- a/src/test/regress/sql/session_variables_dml.sql
+++ b/src/test/regress/sql/session_variables_dml.sql
@@ -118,3 +118,90 @@ RESET max_parallel_workers_per_gather;
 
 DROP TABLE testvar_testtab;
 DROP VARIABLE temp_var02;
+
+CREATE TEMP VARIABLE temp_var03 AS numeric;
+
+-- LET stmt is not allowed inside CTE
+WITH x AS (LET  temp_var03 = 3.14) SELECT * FROM x;
+
+-- LET stmt requires result with exactly one row
+LET temp_var03 = generate_series(1,1);
+SELECT VARIABLE(temp_var03);
+
+-- should fail
+LET temp_var03 = generate_series(1,2);
+LET temp_var03 = generate_series(1,0);
+
+CREATE OR REPLACE FUNCTION testvar_sql01(numeric)
+RETURNS void AS $$
+LET temp_var03 = $1;
+$$ LANGUAGE sql;
+
+CREATE OR REPLACE FUNCTION testvar_sql02()
+RETURNS numeric AS $$
+SELECT VARIABLE(temp_var03);
+$$ LANGUAGE sql;
+
+SELECT testvar_sql01(3.14);
+SELECT testvar_sql02(), VARIABLE(temp_var03);
+
+CREATE OR REPLACE FUNCTION testvar_pl(varchar)
+RETURNS varchar AS $$
+BEGIN
+  LET temp_var03 = $1::numeric;
+  RETURN VARIABLE(temp_var03);
+END
+$$ LANGUAGE plpgsql SECURITY DEFINER;
+
+SELECT testvar_pl('3.14');
+
+DROP VARIABLE temp_var03;
+
+SET plan_cache_mode to force_generic_plan;
+
+-- should not crash
+SELECT testvar_sql01(3.14);
+SELECT testvar_sql02(), VARIABLE(temp_var03);
+SELECT testvar_pl('3.141592');
+
+-- can work again if we create variable
+CREATE TEMP VARIABLE temp_var03 AS numeric;
+SELECT testvar_sql01(3.14);
+SELECT testvar_sql02(), VARIABLE(temp_var03);
+SELECT testvar_pl('3.141592');
+
+CREATE ROLE regress_session_variable_test_role_04;
+
+SET ROLE regress_session_variable_test_role_04;
+
+-- should fail
+SELECT testvar_sql01(3.14);
+
+-- should be ok (security definer)
+SELECT testvar_pl('3.141592');
+
+SET ROLE TO DEFAULT;
+
+DROP FUNCTION testvar_sql01(numeric);
+DROP FUNCTION testvar_sql02();
+DROP FUNCTION testvar_pl(varchar);
+
+DROP ROLE regress_session_variable_test_role_04;
+
+DROP VARIABLE temp_var03;
+
+SET plan_cache_mode TO DEFAULT;
+
+-- test extended query protocol
+CREATE TEMP VARIABLE temp_var04 AS int;
+
+LET temp_var04 = $1 \bind 10 \g
+SELECT VARIABLE(temp_var04);
+
+LET temp_var04 = $1 \parse letps
+\bind_named letps 100 \g
+SELECT VARIABLE(temp_var04);
+
+\close_prepared letps
+
+DROP VARIABLE temp_var04;
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 6f80c130798..66ddea98c22 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1569,6 +1569,7 @@ LargeObjectDesc
 Latch
 LauncherLastStartTimesEntry
 LerpFunc
+LetStmt
 LexDescr
 LexemeEntry
 LexemeHashKey
-- 
2.53.0

From 3552042a99f2c6960ce2d734fab64efa2b8843c9 Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Fri, 21 Nov 2025 20:42:56 +0100
Subject: [PATCH 03/11] 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 loading variables to
executed query.

plan
---
 doc/src/sgml/parallel.sgml                |  6 ++
 src/backend/catalog/dependency.c          | 10 +++
 src/backend/commands/session_variable.c   | 39 ++++++++++
 src/backend/optimizer/plan/planner.c      | 11 +++
 src/backend/optimizer/plan/setrefs.c      | 94 ++++++++++++++++++++++-
 src/backend/optimizer/prep/prepjointree.c |  3 +
 src/backend/optimizer/util/clauses.c      | 35 ++++++++-
 src/backend/utils/fmgr/fmgr.c             | 10 ++-
 src/include/commands/session_variable.h   |  2 +
 src/include/nodes/pathnodes.h             |  5 ++
 src/include/nodes/plannodes.h             |  3 +
 src/include/optimizer/planmain.h          |  2 +
 12 files changed, 214 insertions(+), 6 deletions(-)

diff --git a/doc/src/sgml/parallel.sgml b/doc/src/sgml/parallel.sgml
index af43484703e..843e2c3f663 100644
--- a/doc/src/sgml/parallel.sgml
+++ b/doc/src/sgml/parallel.sgml
@@ -524,6 +524,12 @@ EXPLAIN SELECT * FROM pgbench_accounts WHERE filler LIKE '%x%';
         Plan nodes that reference a correlated <literal>SubPlan</literal>.
       </para>
     </listitem>
+
+    <listitem>
+      <para>
+        Usage of 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 f89267f0342..a3e0a477094 100644
--- a/src/backend/catalog/dependency.c
+++ b/src/backend/catalog/dependency.c
@@ -1951,6 +1951,16 @@ find_expr_references_walker(Node *node,
 	{
 		Param	   *param = (Param *) node;
 
+		/*
+		* catalog less session variable variable cannot be used in persistent
+		* catalog based object.
+		*/
+		if (param->paramkind == PARAM_VARIABLE)
+			ereport(ERROR,
+					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					 errmsg("session variable \"%s\" cannot be referenced in a catalog object",
+							param->paramvarname)));
+
 		/* A parameter must depend on the parameter's datatype */
 		add_object_address(TypeRelationId, param->paramtype, 0,
 						   context->addrs);
diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c
index 75c62baeca2..f8ea8526e1d 100644
--- a/src/backend/commands/session_variable.c
+++ b/src/backend/commands/session_variable.c
@@ -127,6 +127,45 @@ get_session_variable_type_typmod_collid(char *varname,
 	*collid = svar->varcollation;
 }
 
+/*
+ * Returns a copy of the value of the session variable (in the current memory
+ * context).
+ */
+Datum
+GetSessionVariableWithTypecheck(char *varname,
+								Oid typid, int32 typmod,
+								bool *isnull)
+{
+	SVariable	svar;
+	Datum		result;
+
+	svar = search_variable(varname);
+
+	if (svar->vartype != typid || svar->vartypmod != typmod)
+		ereport(ERROR,
+				(errcode(ERRCODE_DATATYPE_MISMATCH),
+				 errmsg("session variable %s is not of a type %s but type %s",
+						varname,
+						format_type_with_typemod(typid, typmod),
+						format_type_with_typemod(svar->vartype, svar->vartypmod))));
+
+	/* only owner can get content of variable */
+	if (svar->varowner != GetUserId() && !superuser())
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("permission denied for session variable %s",
+						varname)));
+
+	if (!svar->isnull)
+		result = datumCopy(svar->value, svar->typbyval, svar->typlen);
+	else
+		result = (Datum) 0;
+
+	*isnull = svar->isnull;
+
+	return result;
+}
+
 /*
  * Creates a new variable - does new entry in sessionvars
  *
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 006b3281969..f82bebf6a83 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -374,6 +374,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
@@ -667,6 +668,9 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
 	/* utilityStmt should be null, but we might as well copy it */
 	result->utilityStmt = parse->utilityStmt;
 	result->elidedNodes = glob->elidedNodes;
+
+	result->sessionVariables = glob->sessionVariables;
+
 	result->stmt_location = parse->stmt_location;
 	result->stmt_len = parse->stmt_len;
 
@@ -855,6 +859,13 @@ subquery_planner(PlannerGlobal *glob, Query *parse, char *plan_name,
 	 */
 	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 1b5b9b5ed9c..c98d96c8692 100644
--- a/src/backend/optimizer/plan/setrefs.c
+++ b/src/backend/optimizer/plan/setrefs.c
@@ -210,6 +210,8 @@ static List *set_returning_clause_references(PlannerInfo *root,
 static List *set_windowagg_runcondition_references(PlannerInfo *root,
 												   List *runcondition,
 												   Plan *plan);
+static bool pull_up_has_session_variables_walker(Node *node,
+												 PlannerInfo *root);
 
 static void record_elided_node(PlannerGlobal *glob, int plan_node_id,
 							   NodeTag elided_type, Bitmapset *relids);
@@ -1365,6 +1367,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
@@ -2192,6 +2238,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)
@@ -2210,6 +2260,43 @@ 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;
+
+		/* we will modify object */
+		p = (Param *) copyObject(p);
+
+		/*
+		 * Now, we can actualize list of session variables, and we can
+		 * complete paramid parameter.
+		 */
+		foreach_node(Param, paramvar, root->glob->sessionVariables)
+		{
+			if (strcmp(paramvar->paramvarname, p->paramvarname) == 0)
+			{
+				p->paramid = paramvar->paramid;
+
+				return (Node *) p;
+			}
+
+			n += 1;
+		}
+
+		p->paramid = n;
+
+		/*
+		 * Because session variables are catalogless, we cannot to use plan
+		 * invalidation. Then we need to check type, typmod, collid any time,
+		 * when we load values of session variables to parameter's buffer.
+		 * For this purpose it is more easy to save complete Param node.
+		 */
+		root->glob->sessionVariables = lappend(root->glob->sessionVariables, p);
+
+		return (Node *) p;
+	}
+
 	return (Node *) copyObject(p);
 }
 
@@ -2271,7 +2358,9 @@ fix_alternative_subplan(PlannerInfo *root, AlternativeSubPlan *asplan,
  * replacing Aggref nodes that should be replaced by initplan output Params,
  * choosing the best implementation for AlternativeSubPlans,
  * looking up operator opcode info for OpExpr and related nodes,
- * and adding OIDs from regclass Const nodes into root->glob->relationOids.
+ * adding OIDs from regclass Const nodes into root->glob->relationOids,
+ * assigning paramvarid to PARAM_VARIABLE params, and collecting the
+ * of session variables in the root->glob->sessionVariables list.
  *
  * 'node': the expression to be modified
  * 'rtoffset': how much to increment varnos by
@@ -2293,7 +2382,8 @@ fix_scan_expr(PlannerInfo *root, Node *node, int rtoffset, double num_exec)
 		root->multiexpr_params != NIL ||
 		root->glob->lastPHId != 0 ||
 		root->minmax_aggs != NIL ||
-		root->hasAlternativeSubPlans)
+		root->hasAlternativeSubPlans ||
+		root->hasSessionVariables)
 	{
 		return fix_scan_expr_mutator(node, &context);
 	}
diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c
index c90f4b32733..2fc2b5b3897 100644
--- a/src/backend/optimizer/prep/prepjointree.c
+++ b/src/backend/optimizer/prep/prepjointree.c
@@ -1653,6 +1653,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 504a30d8836..36f1abfccd1 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"
@@ -944,6 +945,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))
 		{
@@ -2402,6 +2410,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 *
@@ -2528,6 +2537,29 @@ eval_const_expressions_mutator(Node *node,
 						}
 					}
 				}
+				else if (param->paramkind == PARAM_VARIABLE &&
+						 context->estimate)
+				{
+					int16		typLen;
+					bool		typByVal;
+					Datum		pval;
+					bool		isnull;
+
+					get_typlenbyval(param->paramtype, &typLen, &typByVal);
+
+					pval = GetSessionVariableWithTypecheck(param->paramvarname,
+														   param->paramtype,
+														   param->paramtypmod,
+														   &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
@@ -5141,7 +5173,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/fmgr/fmgr.c b/src/backend/utils/fmgr/fmgr.c
index 4e26df7c63a..83bc57a4d79 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/commands/session_variable.h b/src/include/commands/session_variable.h
index 96be968c3d4..3687490bcb1 100644
--- a/src/include/commands/session_variable.h
+++ b/src/include/commands/session_variable.h
@@ -22,6 +22,8 @@
 extern void CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt);
 extern void DropVariableByName(char *varname);
 
+extern Datum GetSessionVariableWithTypecheck(char *varname, Oid typid, int32 typmod, bool *isnull);
+
 extern void get_session_variable_type_typmod_collid(char *varname,
 													Oid *typid,
 													int32 *typmod,
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index c175ee95b68..7acc9e51f0e 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -271,6 +271,9 @@ typedef struct PlannerGlobal
 	/* extension state */
 	void	  **extension_state pg_node_attr(read_write_ignore);
 	int			extension_state_allocated;
+
+	/* list of used session variables */
+	List	   *sessionVariables;
 } PlannerGlobal;
 
 /* macro for fetching the Plan associated with a SubPlan node */
@@ -629,6 +632,8 @@ struct PlannerInfo
 	bool		hasRecursion;
 	/* true if a planner extension may replan this subquery */
 	bool		assumeReplanning;
+	/* 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 8c9321aab8c..d99d84e6db9 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -164,6 +164,9 @@ typedef struct PlannedStmt
 	 */
 	List	   *extension_state;
 
+	/* 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 d0dc3761b13..46069ef2e9e 100644
--- a/src/include/optimizer/planmain.h
+++ b/src/include/optimizer/planmain.h
@@ -132,4 +132,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.53.0

From 9b47c47a901a5fed70669007472feb944f1ce68f Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Fri, 21 Nov 2025 15:28:59 +0100
Subject: [PATCH 02/11] parsing session variable fences

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                   |  10 ++
 src/backend/commands/prepare.c          |   8 ++
 src/backend/commands/session_variable.c |  21 +++++
 src/backend/nodes/nodeFuncs.c           |   6 ++
 src/backend/parser/analyze.c            |   7 ++
 src/backend/parser/gram.y               |  17 +++-
 src/backend/parser/parse_expr.c         | 119 ++++++++++++++++++++++++
 src/backend/parser/parse_merge.c        |   1 +
 src/backend/parser/parse_target.c       |   7 ++
 src/backend/utils/adt/ruleutils.c       |   8 ++
 src/include/commands/session_variable.h |   5 +
 src/include/nodes/parsenodes.h          |  12 +++
 src/include/nodes/primnodes.h           |   5 +
 src/include/parser/parse_node.h         |   1 +
 src/pl/plpgsql/src/pl_exec.c            |   3 +-
 src/tools/pgindent/typedefs.list        |   1 +
 16 files changed, 228 insertions(+), 3 deletions(-)

diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml
index 35a6b322cd9..8c0ae715869 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -5711,6 +5711,16 @@ EXPLAIN SELECT count(*) FROM measurement WHERE logdate &gt;= DATE '2008-01-01';
     variable is stored in session memory and is private to each session. It is
     automatically released when the session ends.
    </para>
+
+   <para>
+    In a query, a session variable can only be referenced using the special
+    <literal>VARIABLE(varname)</literal> syntax. This avoids any risk of
+    collision between variable names and column names.
+   </para>
+<programlisting>
+SELECT VARIABLE(current_user_id);
+</programlisting>
+   </para>
  </sect1>
 
  <sect1 id="ddl-others">
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..452b2498716 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/commands/session_variable.c b/src/backend/commands/session_variable.c
index cd61df4a370..75c62baeca2 100644
--- a/src/backend/commands/session_variable.c
+++ b/src/backend/commands/session_variable.c
@@ -106,6 +106,27 @@ search_variable(char *varname)
 	return svar;
 }
 
+/*
+ * Returns the type, typmod and collid of the given session variable.
+ *
+ * Raises an error when the variable doesn't exists and *error is null.
+ */
+void
+get_session_variable_type_typmod_collid(char *varname,
+										Oid *typid,
+										int32 *typmod,
+										Oid *collid)
+{
+	SVariable	svar;
+
+	svar = search_variable(varname);
+
+	/* only owner can set content of variable */
+	*typid = svar->vartype;
+	*typmod = svar->vartypmod;
+	*collid = svar->varcollation;
+}
+
 /*
  * Creates a new variable - does new entry in sessionvars
  *
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index 199ed27995f..4e261a49128 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -1669,6 +1669,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;
@@ -4704,6 +4707,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 539c16c4f79..e8ed16bb6fd 100644
--- a/src/backend/parser/analyze.c
+++ b/src/backend/parser/analyze.c
@@ -619,6 +619,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);
 
@@ -1058,6 +1059,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);
 
@@ -1537,6 +1539,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)
 	{
@@ -1763,6 +1766,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);
 
@@ -2014,6 +2018,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)
 	{
@@ -2486,6 +2491,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);
 
@@ -2552,6 +2558,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);
 
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index e968260ec89..47d9c66b2b0 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -532,7 +532,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
@@ -890,7 +890,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		'*' '/' '%'
@@ -15794,6 +15794,8 @@ c_expr:		columnref								{ $$ = $1; }
 					else
 						$$ = $2;
 				}
+			| variable_fence
+				{ $$ = $1; }
 			| case_expr
 				{ $$ = $1; }
 			| func_expr
@@ -17196,6 +17198,17 @@ case_arg:	a_expr									{ $$ = $1; }
 			| /*EMPTY*/								{ $$ = NULL; }
 		;
 
+variable_fence:
+			VARIABLE '(' ColId ')'
+				{
+					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 dcfe1acc4c3..aa110e80b06 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -18,6 +18,7 @@
 #include "access/htup_details.h"
 #include "catalog/pg_aggregate.h"
 #include "catalog/pg_type.h"
+#include "commands/session_variable.h"
 #include "miscadmin.h"
 #include "nodes/makefuncs.h"
 #include "nodes/nodeFuncs.h"
@@ -78,6 +79,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,
@@ -372,6 +374,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));
@@ -905,6 +911,119 @@ 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)
+{
+	Param	   *param;
+	Oid			typid;
+	int32		typmod;
+	Oid			collid;
+
+	/* 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)));
+
+	get_session_variable_type_typmod_collid(vf->varname,
+											&typid, &typmod, &collid);
+
+
+	param = makeNode(Param);
+
+	param->paramkind = PARAM_VARIABLE;
+	param->paramvarname = pstrdup(vf->varname);
+	param->paramtype = typid;
+	param->paramtypmod = typmod;
+	param->paramcollid = collid;
+
+	pstate->p_hasSessionVariables = true;
+
+	return (Node *) param;
+}
+
 /* Test whether an a_expr is a plain NULL constant or not */
 static bool
 exprIsNullConstant(Node *arg)
diff --git a/src/backend/parser/parse_merge.c b/src/backend/parser/parse_merge.c
index 0a70d48fd4c..12f89aa2a5c 100644
--- a/src/backend/parser/parse_merge.c
+++ b/src/backend/parser/parse_merge.c
@@ -400,6 +400,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 dbf5b2b5c01..62745d309ac 100644
--- a/src/backend/parser/parse_target.c
+++ b/src/backend/parser/parse_target.c
@@ -2035,6 +2035,13 @@ FigureColnameInternal(Node *node, char **name)
 						 (int) ((JsonFuncExpr *) node)->op);
 			}
 			break;
+		case T_VariableFence:
+			{
+				/* return last field name */
+				*name = ((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 89cbdd3b1e7..f896610769d 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -8835,6 +8835,14 @@ get_parameter(Param *param, deparse_context *context)
 		}
 	}
 
+	/* Note: can be be used by EXPLAIN */
+	if (param->paramkind == PARAM_VARIABLE)
+	{
+		appendStringInfo(context->buf, "VARIABLE(%s)",
+						 quote_identifier(param->paramvarname));
+		return;
+	}
+
 	/*
 	 * Not PARAM_EXEC, or couldn't find referent: just print $N.
 	 *
diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h
index 1ed40d87a38..96be968c3d4 100644
--- a/src/include/commands/session_variable.h
+++ b/src/include/commands/session_variable.h
@@ -22,4 +22,9 @@
 extern void CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt);
 extern void DropVariableByName(char *varname);
 
+extern void get_session_variable_type_typmod_collid(char *varname,
+													Oid *typid,
+													int32 *typmod,
+													Oid *collid);
+
 #endif
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 2102aafe76c..b7fa522e57f 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);
 
@@ -322,6 +324,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;
+	char	   *varname;		/* variable name */
+	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 384df50c80a..9b2a277e9f6 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -379,6 +379,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
+ *				(paramvarname holds the variable's name).
  */
 typedef enum ParamKind
 {
@@ -386,6 +388,7 @@ typedef enum ParamKind
 	PARAM_EXEC,
 	PARAM_SUBLINK,
 	PARAM_MULTIEXPR,
+	PARAM_VARIABLE,
 } ParamKind;
 
 typedef struct Param
@@ -400,6 +403,8 @@ typedef struct Param
 	int32		paramtypmod;
 	/* OID of collation, or InvalidOid if none */
 	Oid			paramcollid;
+	/* OID of used session variable or InvalidOid if none */
+	char	   *paramvarname 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 f23e21f318b..278598a90f7 100644
--- a/src/include/parser/parse_node.h
+++ b/src/include/parser/parse_node.h
@@ -223,6 +223,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/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
index 723048ab833..4e4f1198e82 100644
--- a/src/pl/plpgsql/src/pl_exec.c
+++ b/src/pl/plpgsql/src/pl_exec.c
@@ -8253,7 +8253,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 c17bf76a6ea..5d38d12b7c4 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -3263,6 +3263,7 @@ ValidatorValidateCB
 ValuesScan
 ValuesScanState
 Var
+VariableFence
 VarBit
 VarChar
 VarParamState
-- 
2.53.0

From 87120ee59e4c1a3079cd5dc1645c58c067a5a6c7 Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Wed, 19 Nov 2025 19:36:07 +0100
Subject: [PATCH 01/11] CREATE VARIABLE, DROP VARIABLE

---
 doc/src/sgml/ddl.sgml                         |  20 ++
 doc/src/sgml/glossary.sgml                    |  15 ++
 doc/src/sgml/ref/allfiles.sgml                |   2 +
 doc/src/sgml/ref/create_variable.sgml         | 133 +++++++++++
 doc/src/sgml/ref/drop_variable.sgml           |  84 +++++++
 doc/src/sgml/reference.sgml                   |   2 +
 src/backend/commands/Makefile                 |   1 +
 src/backend/commands/dropcmds.c               |   1 +
 src/backend/commands/meson.build              |   1 +
 src/backend/commands/session_variable.c       | 215 ++++++++++++++++++
 src/backend/parser/gram.y                     |  55 ++++-
 src/backend/tcop/utility.c                    |  26 +++
 src/bin/psql/tab-complete.in.c                |  10 +-
 src/include/commands/session_variable.h       |  25 ++
 src/include/nodes/parsenodes.h                |  23 ++
 src/include/parser/kwlist.h                   |   1 +
 src/include/tcop/cmdtaglist.h                 |   2 +
 .../expected/session_variables_ddl.out        |  43 ++++
 src/test/regress/parallel_schedule            |   2 +-
 .../regress/sql/session_variables_ddl.sql     |  56 +++++
 src/tools/pgindent/typedefs.list              |   4 +
 21 files changed, 714 insertions(+), 7 deletions(-)
 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 9070aaa5a7c..35a6b322cd9 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -5693,6 +5693,26 @@ 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 temporary database objects that can hold a value.
+    A session variable can be created by the <command>CREATE VARIABLE</command>
+    command and can only be accessed by its owner. The value of a session
+    variable is stored in session memory and is private to each session. It is
+    automatically 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 e2db5bcc78c..eab22f11a64 100644
--- a/doc/src/sgml/glossary.sgml
+++ b/doc/src/sgml/glossary.sgml
@@ -1743,6 +1743,21 @@
    </glossdef>
   </glossentry>
 
+  <glossentry id="glossary-session-variable">
+   <glossterm>Session variable</glossterm>
+   <glossdef>
+    <para>
+     A temporal database object that holds a value in session memory.  This
+     value is private to each session and is released when the session ends.
+     The default value of the session variable is null.  Read or write access
+     to session variables is allowed only to owner (creator).
+    </para>
+    <para>
+     For more information, see <xref linkend="ddl-session-variables"/>.
+    </para>
+   </glossdef>
+  </glossentry>
+
   <glossentry id="glossary-shared-memory">
    <glossterm>Shared memory</glossterm>
    <glossdef>
diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml
index e167406c744..a7349919658 100644
--- a/doc/src/sgml/ref/allfiles.sgml
+++ b/doc/src/sgml/ref/allfiles.sgml
@@ -99,6 +99,7 @@ Complete list of usable sgml source files in this directory.
 <!ENTITY createType         SYSTEM "create_type.sgml">
 <!ENTITY createUser         SYSTEM "create_user.sgml">
 <!ENTITY createUserMapping  SYSTEM "create_user_mapping.sgml">
+<!ENTITY createVariable     SYSTEM "create_variable.sgml">
 <!ENTITY createView         SYSTEM "create_view.sgml">
 <!ENTITY deallocate         SYSTEM "deallocate.sgml">
 <!ENTITY declare            SYSTEM "declare.sgml">
@@ -147,6 +148,7 @@ Complete list of usable sgml source files in this directory.
 <!ENTITY dropType           SYSTEM "drop_type.sgml">
 <!ENTITY dropUser           SYSTEM "drop_user.sgml">
 <!ENTITY dropUserMapping    SYSTEM "drop_user_mapping.sgml">
+<!ENTITY dropVariable       SYSTEM "drop_variable.sgml">
 <!ENTITY dropView           SYSTEM "drop_view.sgml">
 <!ENTITY end                SYSTEM "end.sgml">
 <!ENTITY execute            SYSTEM "execute.sgml">
diff --git a/doc/src/sgml/ref/create_variable.sgml b/doc/src/sgml/ref/create_variable.sgml
new file mode 100644
index 00000000000..4e8c1940252
--- /dev/null
+++ b/doc/src/sgml/ref/create_variable.sgml
@@ -0,0 +1,133 @@
+<!--
+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 { TEMP | TEMPORARY } VARIABLE <replaceable class="parameter">name</replaceable> [ AS ] <replaceable class="parameter">data_type</replaceable>
+</synopsis>
+ </refsynopsisdiv>
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   The <command>CREATE VARIABLE</command> command creates a session
+   variable. Currently only temporary session variables are supported,
+   and then the keyword <literal>TEMPORARY</literal> is required.
+  </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>
+
+  <para>
+   Session variables cannot be used in views or in SQL functions using
+   SQL-conforming style syntax.
+  </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-name">
+    <term><replaceable class="parameter">name</replaceable></term>
+    <listitem>
+     <para>
+      The name 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. Only buildin scalar data types are allowed. Arrays or composite
+      types are not allowed.
+     </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 TEMPORARY 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-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..e8517a78200
--- /dev/null
+++ b/doc/src/sgml/ref/drop_variable.sgml
@@ -0,0 +1,84 @@
+<!--
+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 <replaceable class="parameter">name</replaceable>
+</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><replaceable class="parameter">name</replaceable></term>
+    <listitem>
+     <para>
+      The name of a session variable.
+     </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-createvariable"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml
index 2cf02c37b17..c03e7692c7a 100644
--- a/doc/src/sgml/reference.sgml
+++ b/doc/src/sgml/reference.sgml
@@ -127,6 +127,7 @@
    &createType;
    &createUser;
    &createUserMapping;
+   &createVariable;
    &createView;
    &deallocate;
    &declare;
@@ -175,6 +176,7 @@
    &dropType;
    &dropUser;
    &dropUserMapping;
+   &dropVariable;
    &dropView;
    &end;
    &execute;
diff --git a/src/backend/commands/Makefile b/src/backend/commands/Makefile
index 64cb6278409..d42ed8952a2 100644
--- a/src/backend/commands/Makefile
+++ b/src/backend/commands/Makefile
@@ -54,6 +54,7 @@ OBJS = \
 	seclabel.o \
 	sequence.o \
 	sequence_xlog.o \
+	session_variable.o \
 	statscmds.o \
 	subscriptioncmds.o \
 	tablecmds.o \
diff --git a/src/backend/commands/dropcmds.c b/src/backend/commands/dropcmds.c
index 92526012d2a..77075f9051d 100644
--- a/src/backend/commands/dropcmds.c
+++ b/src/backend/commands/dropcmds.c
@@ -22,6 +22,7 @@
 #include "catalog/pg_namespace.h"
 #include "catalog/pg_proc.h"
 #include "commands/defrem.h"
+#include "commands/session_variable.h"
 #include "miscadmin.h"
 #include "parser/parse_type.h"
 #include "utils/acl.h"
diff --git a/src/backend/commands/meson.build b/src/backend/commands/meson.build
index ca3f53c6213..e88e3b9c058 100644
--- a/src/backend/commands/meson.build
+++ b/src/backend/commands/meson.build
@@ -42,6 +42,7 @@ backend_sources += files(
   'seclabel.c',
   'sequence.c',
   'sequence_xlog.c',
+  'session_variable.c',
   'statscmds.c',
   'subscriptioncmds.c',
   'tablecmds.c',
diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c
new file mode 100644
index 00000000000..cd61df4a370
--- /dev/null
+++ b/src/backend/commands/session_variable.c
@@ -0,0 +1,215 @@
+/*-------------------------------------------------------------------------
+ *
+ * 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_language.h"
+#include "commands/session_variable.h"
+#include "miscadmin.h"
+#include "parser/parse_type.h"
+#include "storage/proc.h"
+#include "utils/builtins.h"
+#include "utils/datum.h"
+#include "utils/lsyscache.h"
+#include "utils/memutils.h"
+
+/*
+ * The session variables are stored in the backend's private memory (data,
+ * metadata) in the dedicated memory context SVariableMemoryContext in binary
+ * format. They are stored in the "sessionvars" hash table, whose key is the
+ * name of the variable.
+ *
+ * Only owner (creator) can access the session variables. Because there is
+ * not catalog support, there is not possibility to track dependecies, and
+ * then only buildin types.
+ */
+typedef struct SVariableData
+{
+	NameData	varname;
+
+	Oid			varowner;
+	Oid			vartype;
+	int32		vartypmod;
+	Oid			varcollation;
+
+	bool		isnull;
+	Datum		value;
+
+	int16		typlen;
+	bool		typbyval;
+} SVariableData;
+
+typedef SVariableData *SVariable;
+
+static HTAB *sessionvars = NULL;	/* hash table for session variables */
+
+static MemoryContext SVariableMemoryContext = NULL;
+
+/*
+ * Create the hash table for storing session variables.
+ */
+static void
+create_sessionvars_hashtables(void)
+{
+	HASHCTL		vars_ctl;
+
+	Assert(!sessionvars);
+
+	if (!SVariableMemoryContext)
+	{
+		/* we need our own long-lived memory context */
+		SVariableMemoryContext =
+			AllocSetContextCreate(TopMemoryContext,
+								  "session variables",
+								  ALLOCSET_START_SMALL_SIZES);
+	}
+
+	vars_ctl.keysize = NAMEDATALEN;
+	vars_ctl.entrysize = sizeof(SVariableData);
+	vars_ctl.hcxt = SVariableMemoryContext;
+
+	sessionvars = hash_create("Session variables", 64, &vars_ctl,
+							  HASH_ELEM | HASH_STRINGS | HASH_CONTEXT);
+}
+
+/*
+ * Returns entry of session variable specified by name
+ */
+static SVariable
+search_variable(char *varname)
+{
+	SVariable	svar;
+
+	if (!sessionvars)
+		create_sessionvars_hashtables();
+
+	svar = (SVariable) hash_search(sessionvars, varname,
+								   HASH_FIND, NULL);
+
+	if (!svar)
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("session variable \"%s\" doesn't exist",
+						varname)));
+
+	return svar;
+}
+
+/*
+ * Creates a new variable - does new entry in sessionvars
+ *
+ * Used by CREATE VARIABLE command
+ */
+void
+CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt)
+{
+	Oid			typeid;
+	int32		typmod;
+	Oid			typcollation;
+	Oid			varowner = GetUserId();
+	SVariable	svar;
+	bool		found;
+	int16		typlen;
+	bool		typbyval;
+
+	/*
+	 * Current implementation is not catalog based, but we expect catalog
+	 * based implementation for future, so we force same limits.
+	 */
+	PreventCommandIfReadOnly("CREATE VARIABLE");
+	PreventCommandIfParallelMode("CREATE VARIABLE");
+	PreventCommandDuringRecovery("CREATE VARIABLE");
+
+	typenameTypeIdAndMod(pstate, stmt->typeName, &typeid, &typmod);
+
+	if (get_typtype(typeid) != TYPTYPE_BASE)
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("%s is not a base type",
+						format_type_be(typeid))));
+
+	if (OidIsValid(get_element_type(typeid)))
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("%s type is an array",
+						format_type_be(typeid))));
+
+	/* allow only buildin types */
+	if (typeid >= FirstUnpinnedObjectId)
+		ereport(ERROR,
+				errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				errmsg("session variable cannot have a user-defined type"),
+				errdetail("Session variables that make use of user-defined types are not yet supported."));
+
+	get_typlenbyval(typeid, &typlen, &typbyval);
+	typcollation = get_typcollation(typeid);
+
+	if (!sessionvars)
+		create_sessionvars_hashtables();
+
+	svar = hash_search(sessionvars, stmt->name,
+					   HASH_ENTER, &found);
+
+	if (found)
+		ereport(ERROR,
+				(errcode(ERRCODE_DUPLICATE_OBJECT),
+				 errmsg("session variable \"%s\" already exists",
+						stmt->name)));
+
+	namestrcpy(&svar->varname, stmt->name);
+	svar->vartype = typeid;
+	svar->vartypmod = typmod;
+	svar->varcollation = typcollation;
+	svar->varowner = varowner;
+	svar->typlen = typlen;
+	svar->typbyval = typbyval;
+
+	svar->value = (Datum) 0;
+	svar->isnull = true;
+}
+
+/*
+ * Drop variable by name
+ */
+void
+DropVariableByName(char *varname)
+{
+	SVariable	svar;
+
+	/*
+	 * Current implementation is not catalog based, but we expect catalog
+	 * based implementation for future, so we force same limits.
+	 */
+	PreventCommandIfReadOnly("DROP VARIABLE");
+	PreventCommandIfParallelMode("DROP VARIABLE");
+	PreventCommandDuringRecovery("DROP VARIABLE");
+
+	svar = search_variable(varname);
+
+	/* only owner can get content of variable */
+	if (svar->varowner != GetUserId() && !superuser())
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("must be owner of session variable %s",
+						varname)));
+
+	if (!svar->typbyval && !svar->isnull)
+		pfree(DatumGetPointer(svar->value));
+
+	if (hash_search(sessionvars,
+					   varname,
+					   HASH_REMOVE,
+					   NULL) == NULL)
+		elog(ERROR, "hash table corrupted");
+}
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index c567252acc4..e968260ec89 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -291,13 +291,13 @@ 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
 		DropOpClassStmt DropOpFamilyStmt DropStmt
-		DropCastStmt DropRoleStmt
+		DropCastStmt DropRoleStmt DropSessionVarStmt
 		DropdbStmt DropTableSpaceStmt
 		DropTransformStmt
 		DropUserMappingStmt ExplainStmt FetchStmt
@@ -792,8 +792,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
 
 	WAIT WHEN WHERE WHITESPACE_P WINDOW WITH WITHIN WITHOUT WORK WRAPPER WRITE
 
@@ -1059,6 +1059,7 @@ stmt:
 			| CreatePolicyStmt
 			| CreatePLangStmt
 			| CreateSchemaStmt
+			| CreateSessionVarStmt
 			| CreateSeqStmt
 			| CreateStmt
 			| CreateSubscriptionStmt
@@ -1086,6 +1087,7 @@ stmt:
 			| DropTableSpaceStmt
 			| DropTransformStmt
 			| DropRoleStmt
+			| DropSessionVarStmt
 			| DropUserMappingStmt
 			| DropdbStmt
 			| ExecuteStmt
@@ -5388,6 +5390,47 @@ create_extension_opt_item:
 				}
 		;
 
+/*****************************************************************************
+ *
+ *		QUERY :
+ *				CREATE { TEMP | TEMPORARY } VARIABLE varname [AS] type
+ *
+ *****************************************************************************/
+
+CreateSessionVarStmt:
+			CREATE OptTemp VARIABLE ColId opt_as Typename
+				{
+					CreateSessionVarStmt *n = makeNode(CreateSessionVarStmt);
+
+					if ($2 != RELPERSISTENCE_TEMP)
+						ereport(ERROR,
+							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+							 errmsg("only temporal session variables are supported"),
+							 parser_errposition(@2)));
+
+					n->name = $4;
+					n->typeName = $6;
+					$$ = (Node *) n;
+				}
+		;
+
+/*****************************************************************************
+ *
+ *		QUERY :
+ *				DROP VARIABLE varname
+ *
+ *****************************************************************************/
+
+DropSessionVarStmt:
+			DROP VARIABLE ColId
+				{
+					DropSessionVarStmt *n = makeNode(DropSessionVarStmt);
+
+					n->name = $3;
+					$$ = (Node *) n;
+				}
+		;
+
 /*****************************************************************************
  *
  * ALTER EXTENSION name UPDATE [ TO version ]
@@ -18231,6 +18274,7 @@ unreserved_keyword:
 			| VALIDATE
 			| VALIDATOR
 			| VALUE_P
+			| VARIABLE
 			| VARYING
 			| VERSION_P
 			| VIEW
@@ -18891,6 +18935,7 @@ bare_label_keyword:
 			| VALUE_P
 			| VALUES
 			| VARCHAR
+			| VARIABLE
 			| VARIADIC
 			| VERBOSE
 			| VERSION_P
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 34dd6e18df5..41a7edfe53d 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"
@@ -183,6 +184,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:
@@ -201,6 +203,7 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree)
 		case T_DropTableSpaceStmt:
 		case T_DropUserMappingStmt:
 		case T_DropdbStmt:
+		case T_DropSessionVarStmt:
 		case T_GrantRoleStmt:
 		case T_GrantStmt:
 		case T_ImportForeignSchemaStmt:
@@ -1063,6 +1066,15 @@ standard_ProcessUtility(PlannedStmt *pstmt,
 			}
 			break;
 
+		case T_CreateSessionVarStmt:
+			CreateVariable(pstate, (CreateSessionVarStmt *) parsetree);
+			break;
+
+		case T_DropSessionVarStmt:
+			/* No event triggers for catalog less session variables */
+			DropVariableByName(((DropSessionVarStmt *) parsetree)->name);
+			break;
+
 		default:
 			/* All other statement types have event trigger support */
 			ProcessUtilitySlow(pstate, pstmt, queryString,
@@ -1388,6 +1400,7 @@ ProcessUtilitySlow(ParseState *pstate,
 				}
 				break;
 
+
 				/*
 				 * ************* object creation / destruction **************
 				 */
@@ -3236,6 +3249,14 @@ CreateCommandTag(Node *parsetree)
 			}
 			break;
 
+		case T_CreateSessionVarStmt:
+			tag = CMDTAG_CREATE_VARIABLE;
+			break;
+
+		case T_DropSessionVarStmt:
+			tag = CMDTAG_DROP_VARIABLE;
+			break;
+
 		default:
 			elog(WARNING, "unrecognized node type: %d",
 				 (int) nodeTag(parsetree));
@@ -3774,6 +3795,11 @@ GetCommandLogLevel(Node *parsetree)
 			}
 			break;
 
+		case T_CreateSessionVarStmt:
+		case T_DropSessionVarStmt:
+			lev = LOGSTMT_DDL;
+			break;
+
 		default:
 			elog(WARNING, "unrecognized node type: %d",
 				 (int) nodeTag(parsetree));
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 8b91bc00062..b829eed2ab0 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -1364,6 +1364,7 @@ static const pgsql_thing_t words_after_create[] = {
 	{"USER", Query_for_list_of_roles, NULL, NULL, Keywords_for_user_thing},
 	{"USER MAPPING FOR", NULL, NULL, NULL},
 	{"VIEW", NULL, NULL, &Query_for_list_of_views},
+	{"VARIABLE", NULL, NULL, NULL, NULL, THING_NO_CREATE},
 	{NULL}						/* end of list */
 };
 
@@ -3769,7 +3770,7 @@ match_previous_words(int pattern_id,
 /* CREATE TABLE --- is allowed inside CREATE SCHEMA, so use TailMatches */
 	/* Complete "CREATE TEMP/TEMPORARY" with the possible temp objects */
 	else if (TailMatches("CREATE", "TEMP|TEMPORARY"))
-		COMPLETE_WITH("SEQUENCE", "TABLE", "VIEW");
+		COMPLETE_WITH("SEQUENCE", "TABLE", "VARIABLE", "VIEW");
 	/* Complete "CREATE UNLOGGED" with TABLE or SEQUENCE */
 	else if (TailMatches("CREATE", "UNLOGGED"))
 		COMPLETE_WITH("TABLE", "SEQUENCE");
@@ -4122,6 +4123,13 @@ match_previous_words(int pattern_id,
 			COMPLETE_WITH(",", ")");
 	}
 
+/* CREATE VARIABLE */
+	else if (Matches("CREATE", "TEMP|TEMPORARY", "VARIABLE", MatchAny))
+		COMPLETE_WITH("AS");
+	else if (TailMatches("VARIABLE", MatchAny, "AS"))
+		/* Complete CREATE VARIABLE <name> with AS types */
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes);
+
 /* CREATE VIEW --- is allowed inside CREATE SCHEMA, so use TailMatches */
 	/* Complete CREATE [ OR REPLACE ] VIEW <name> with AS or WITH */
 	else if (TailMatches("CREATE", "VIEW", MatchAny) ||
diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h
new file mode 100644
index 00000000000..1ed40d87a38
--- /dev/null
+++ b/src/include/commands/session_variable.h
@@ -0,0 +1,25 @@
+/*-------------------------------------------------------------------------
+ *
+ * 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 void CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt);
+extern void DropVariableByName(char *varname);
+
+#endif
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 0aec49bdd22..2102aafe76c 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -3586,6 +3586,29 @@ typedef struct AlterStatsStmt
 	bool		missing_ok;		/* skip error if statistics object is missing */
 } AlterStatsStmt;
 
+
+/* ----------------------
+ *		Create Variable Statement
+ * ----------------------
+ */
+typedef struct CreateSessionVarStmt
+{
+	NodeTag		type;
+	char	   *name;		/* the variable to create */
+	TypeName   *typeName;		/* the type of variable */
+} CreateSessionVarStmt;
+
+/* ----------------------
+ *		DROP Variable Statement
+ * ----------------------
+ */
+typedef struct DropSessionVarStmt
+{
+	NodeTag		type;
+	char	   *name;
+} DropSessionVarStmt;
+
+
 /* ----------------------
  *		Create Function Statement
  * ----------------------
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index f7753c5c8a8..d4b1223a33e 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -491,6 +491,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 1290c9bab68..4b964d0fb5a 100644
--- a/src/include/tcop/cmdtaglist.h
+++ b/src/include/tcop/cmdtaglist.h
@@ -123,6 +123,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 +176,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/test/regress/expected/session_variables_ddl.out b/src/test/regress/expected/session_variables_ddl.out
new file mode 100644
index 00000000000..45c2d27ab44
--- /dev/null
+++ b/src/test/regress/expected/session_variables_ddl.out
@@ -0,0 +1,43 @@
+SET log_statement TO ddl;
+-- should to fail
+CREATE VARIABLE x AS int;
+ERROR:  only temporal session variables are supported
+-- should be ok
+CREATE TEMPORARY VARIABLE x AS int;
+-- should fail
+CREATE TEMPORARY VARIABLE x AS int;
+ERROR:  session variable "x" already exists
+-- should fail
+DROP VARIABLE y;
+ERROR:  session variable "y" doesn't exist
+-- should be ok
+DROP VARIABLE x;
+CREATE TYPE test_type AS (x int, y int);
+-- should fail
+CREATE VARIABLE x AS test_type;
+ERROR:  only temporal session variables are supported
+DROP TYPE test_type;
+-- should fail
+CREATE VARIABLE x AS int[];
+ERROR:  only temporal session variables are supported
+CREATE DOMAIN test_domain AS int;
+-- should fail
+CREATE TEMP VARIABLE x AS test_domain;
+ERROR:  test_domain is not a base type
+DROP DOMAIN test_domain;
+CREATE ROLE regress_session_variable_test_role_01;
+CREATE ROLE regress_session_variable_test_role_02;
+SET ROLE TO regress_session_variable_test_role_01;
+CREATE TEMP VARIABLE x AS int;
+SET ROLE TO default;
+SET ROLE TO regress_session_variable_test_role_02;
+-- should fail
+DROP VARIABLE x;
+ERROR:  must be owner of session variable x
+SET ROLE TO default;
+SET ROLE TO regress_session_variable_test_role_01;
+-- should be ok
+DROP VARIABLE x;
+SET ROLE TO DEFAULT;
+DROP ROLE regress_session_variable_test_role_01;
+DROP ROLE regress_session_variable_test_role_02;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 549e9b2d7be..8f9988954f9 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 uses 2 connections,
 # so keep this parallel group to at most 19 tests
 # ----------
-test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml
+test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml session_variables_ddl
 
 # ----------
 # Another group of parallel tests
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..34f34dd898f
--- /dev/null
+++ b/src/test/regress/sql/session_variables_ddl.sql
@@ -0,0 +1,56 @@
+SET log_statement TO ddl;
+
+-- should to fail
+CREATE VARIABLE x AS int;
+
+-- should be ok
+CREATE TEMPORARY VARIABLE x AS int;
+
+-- should fail
+CREATE TEMPORARY VARIABLE x AS int;
+
+-- should fail
+DROP VARIABLE y;
+
+-- should be ok
+DROP VARIABLE x;
+
+CREATE TYPE test_type AS (x int, y int);
+
+-- should fail
+CREATE VARIABLE x AS test_type;
+
+DROP TYPE test_type;
+
+-- should fail
+CREATE VARIABLE x AS int[];
+
+CREATE DOMAIN test_domain AS int;
+
+-- should fail
+CREATE TEMP VARIABLE x AS test_domain;
+
+DROP DOMAIN test_domain;
+
+CREATE ROLE regress_session_variable_test_role_01;
+CREATE ROLE regress_session_variable_test_role_02;
+
+SET ROLE TO regress_session_variable_test_role_01;
+
+CREATE TEMP VARIABLE x AS int;
+
+SET ROLE TO default;
+SET ROLE TO regress_session_variable_test_role_02;
+
+-- should fail
+DROP VARIABLE x;
+
+SET ROLE TO default;
+SET ROLE TO regress_session_variable_test_role_01;
+
+-- should be ok
+DROP VARIABLE x;
+
+SET ROLE TO DEFAULT;
+DROP ROLE regress_session_variable_test_role_01;
+DROP ROLE regress_session_variable_test_role_02;
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 6e2d876a40f..c17bf76a6ea 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -574,6 +574,7 @@ CreateRoleStmt
 CreateSchemaStmt
 CreateSchemaStmtContext
 CreateSeqStmt
+CreateSessionVarStmt
 CreateStatsStmt
 CreateStmt
 CreateStmtContext
@@ -677,6 +678,7 @@ DropBehavior
 DropOwnedStmt
 DropReplicationSlotCmd
 DropRoleStmt
+DropSessionVarStmt
 DropStmt
 DropSubscriptionStmt
 DropTableSpaceStmt
@@ -2698,6 +2700,8 @@ SSL_CTX
 STARTUPINFO
 STRLEN
 SV
+SVariableData
+SVariable
 SYNCHRONIZATION_BARRIER
 SYSTEM_INFO
 SampleScan
-- 
2.53.0

---

From: Pavel Stehule <[email-masked]>
Date: Wed, 4 Mar 2026 20:15:26 +0100

Hi

st 4. 3. 2026 v 11:03 odesílatel Haritabh Gupta <[email-masked]>
napsal:

> Hi,
>
> While reviewing I came across this behaviour and wanted to
> check whether it's intended:
>
>     CREATE TEMP VARIABLE y AS int;
>     LET y = 42;
>
>     BEGIN;
>     SAVEPOINT s1;
>     LET y = generate_series(1,2);  -- ERROR: too many rows
>     ROLLBACK TO s1;
>     SELECT VARIABLE(y);            -- returns 1, not 42
>
> It looks like svariableReceiveSlot writes the first row to the
> variable (pfree'ing the old datum) before the second row triggers the
> error, so the old value is lost even though LET failed.
>
> I understand variable values are intentionally non-transactional, but
> is it expected that a failed LET has this side effect?
>

Yes, it does like you describe it. It is the most simple solution, because
I don't need any extra buffer, and I can assign the value immediately when
I have the value.

I think I can postpone an assignment to svariableShutdownReceiver with a
few more lines to get the behaviour that you expect.

Regards

Pavel

> Haritabh Gupta
> Supabase

---

From: Pavel Stehule <[email-masked]>
Date: Thu, 5 Mar 2026 13:54:06 +0100

Hi

> is it expected that a failed LET has this side effect?

please, check attached patch

Regards

Pavel

> Supabase
From d62df707255e98cfd6a130372a1d8c04019758b1 Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Mon, 8 Dec 2025 05:00:12 +0100
Subject: [PATCH 11/11] subtransaction support for session variables DDL
 (CREATE, DROP)

 

 

 
+/*

 #endif
 
+BEGIN;
+SAVEPOINT s1;
+DROP VARIABLE x;
 
+BEGIN;
+SAVEPOINT s1;
+DROP VARIABLE x;

From a6dc64227e02916df1836d1b02d522296097f675 Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Thu, 4 Dec 2025 06:17:54 +0100
Subject: [PATCH 08/11] support CREATE IF NOT EXISTS and DROP IF EXISTS

---

  <refsynopsisdiv>
 <synopsis>
 
   <variablelist>

  <refsynopsisdiv>
 <synopsis>

 
-	if (!svar)

 

 
 
 	if (found)
-		ereport(ERROR,

 

 

index 3cfa362e03b..a385870cb8e 100644
@@ -5397,7 +5397,7 @@ create_extension_opt_item:
 
@@ -5414,14 +5414,31 @@ CreateSessionVarStmt:
 
 					n->name = $4;
 					n->typeName = $6;

@@ -5431,8 +5448,18 @@ DropSessionVarStmt:
 					DropSessionVarStmt *n = makeNode(DropSessionVarStmt);
 
 					n->name = $3;
+					n->missing_ok = false;
+					$$ = (Node *) n;
+				}
 
index 485f7240821..642e5dc271d 100644

 		case T_LetStmt:

index f5b65f7d0e9..947042fef61 100644

 
 -- should be ok

From 0537808da8e9aafc1580e703ffb481e9b6da5dd8 Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Mon, 24 Nov 2025 20:04:16 +0100
Subject: [PATCH 07/11] DISCARD TEMP

---

 
 		case DISCARD_TEMP:
 
 		default:
 
 	PopActiveSnapshot();
 }
+
+/*

From 979fe619eba41fb9ddff4c49d602d9ce66d36cc2 Mon Sep 17 00:00:00 2001

---

index 513c5d9428f..7e4558f52fc 100644
 
@@ -4430,6 +4435,10 @@ match_previous_words(int pattern_id,
 
+	/* DROP VARIABLE */
@@ -4870,6 +4879,8 @@ match_previous_words(int pattern_id,
 
 /* LET */
 
@@ -5438,6 +5449,12 @@ match_previous_words(int pattern_id,
 			COMPLETE_WITH("'standby_replay'", "'standby_write'", "'standby_flush'", "'primary_flush'");
 	}
 
+/*
+ * VARIABLE fence
+ */
 
 	/*
index dac40992cbc..d27e1f7dad9 100644
@@ -12759,4 +12759,9 @@

 DROP VARIABLE x;

From bdcd1641508b00451c060b820c027bd583640178 Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Sat, 6 Dec 2025 07:35:30 +0100
Subject: [PATCH 10/11] transactional DDL - CREATE VARIABLE, DROP VARIABLE

 
 
 	int16		typlen;
 	bool		typbyval;
+

 
+/*

 	if (found)
 	{

 	svar->value = (Datum) 0;
 	svar->isnull = true;
+
 
 /*

 
 /*
 
 /*

 /*

 

From f399d1478c6274917eb47b78073ffa79b0361016 Mon Sep 17 00:00:00 2001

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                         |  22 +++
 doc/src/sgml/ref/allfiles.sgml                |   1 +
 doc/src/sgml/ref/create_variable.sgml         |   3 +
 doc/src/sgml/ref/drop_variable.sgml           |   1 +
 doc/src/sgml/ref/let.sgml                     |  95 +++++++++++
 doc/src/sgml/reference.sgml                   |   1 +
 src/backend/commands/session_variable.c       |  86 ++++++++++
 src/backend/nodes/nodeFuncs.c                 |   8 +
 src/backend/optimizer/plan/planner.c          |   1 +
 src/backend/parser/analyze.c                  | 103 ++++++++++--
 src/backend/parser/gram.y                     |  38 ++++-
 src/backend/parser/parse_agg.c                |   7 +
 src/backend/parser/parse_expr.c               |   9 +
 src/backend/parser/parse_func.c               |   3 +
 src/backend/tcop/utility.c                    |  15 ++
 src/bin/psql/tab-complete.in.c                |   9 +-
 src/include/commands/session_variable.h       |   5 +
 src/include/nodes/parsenodes.h                |  15 ++
 src/include/nodes/pathnodes.h                 |   3 +
 src/include/parser/kwlist.h                   |   1 +
 src/include/parser/parse_node.h               |   1 +
 src/include/tcop/cmdtaglist.h                 |   1 +
 .../expected/session_variables_dml.out        | 156 ++++++++++++++++++
 .../regress/sql/session_variables_dml.sql     | 113 +++++++++++++
 src/tools/pgindent/typedefs.list              |   1 +
 25 files changed, 678 insertions(+), 20 deletions(-)
 create mode 100644 doc/src/sgml/ref/let.sgml

  <sect1 id="ddl-others">

 

 
 
 /*
index 53af28c49d8..2c6a8e7afb6 100644
 
 	/*

 
+		case T_LetStmt:
 
 			/*

 
 
 	/*
 
 /*
 
+/*
+ * transformLetStmt -
index 7fed5ad7ece..3cfa362e03b 100644
@@ -303,7 +303,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
@@ -753,7 +753,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	KEEP KEY KEYS

 	MAPPING MATCH MATCHED MATERIALIZED MAXVALUE MERGE MERGE_ACTION METHOD
@@ -1101,6 +1101,7 @@ stmt:
 			| ImportForeignSchemaStmt
 			| IndexStmt
 			| InsertStmt
+			| LetStmt
 			| ListenStmt
 			| RefreshMatViewStmt
 			| LoadStmt
@@ -13029,6 +13030,37 @@ opt_hold: /* EMPTY */						{ $$ = 0; }
 			| WITHOUT HOLD					{ $$ = 0; }
 		;
 
@@ -18139,6 +18171,7 @@ unreserved_keyword:
@@ -18757,6 +18790,7 @@ bare_label_keyword:
index d0187ea84a0..0a5ab678afc 100644

 			/*
 
 			/*

 			/*
 
 			/*
 
 			/*
index 99c95b80767..485f7240821 100644
 
 		case T_CallStmt:
 		case T_DoStmt:
+		case T_LetStmt:
 			{
 				/*
 
+		case T_LetStmt:
 
+		case T_LetStmt:
 
+		case T_LetStmt:
+			tag = CMDTAG_LET;
+			break;
+
 
 		case T_PLAssignStmt:
+		case T_LetStmt:
 			lev = LOGSTMT_ALL;
 			break;
 
index 77f1263d799..513c5d9428f 100644
@@ -4868,6 +4868,11 @@ match_previous_words(int pattern_id,
 
+/* LET */

 
index c79f07f3c40..f5b65f7d0e9 100644

 

index 1519bf723e0..599751ec5c8 100644
@@ -133,3 +133,159 @@ RESET min_parallel_table_scan_size;
+-- original value should not be changed when LET fails
+CREATE TEMP VARIABLE temp_var04 AS numeric;
+LET temp_var04 = 42;
+LET temp_var04 = generate_series(1,2); -- ERROR: too many row
+SELECT VARIABLE(temp_var04); -- expected 42
+         42
+LET temp_var04 = 42;
+LET temp_var04 = generate_series(1,2); -- ERROR: too many row
+SELECT VARIABLE(temp_var04); -- expected 42
+         42
+LET temp_var04 = generate_series(1,2); -- ERROR: too many row
+SELECT VARIABLE(temp_var04); -- expected NULL
index bf56b19467b..fcb429f6bbf 100644
@@ -118,3 +118,116 @@ RESET max_parallel_workers_per_gather;
 
+-- original value should not be changed when LET fails
+CREATE TEMP VARIABLE temp_var04 AS numeric;
+LET temp_var04 = 42;
+LET temp_var04 = generate_series(1,2); -- ERROR: too many row
+SELECT VARIABLE(temp_var04); -- expected 42
+LET temp_var04 = 42;
+LET temp_var04 = generate_series(1,2); -- ERROR: too many row
+SELECT VARIABLE(temp_var04); -- expected 42
+LET temp_var04 = generate_series(1,2); -- ERROR: too many row
+SELECT VARIABLE(temp_var04); -- expected NULL
index 184b790de97..bd2d1ae8c9e 100644
@@ -1570,6 +1570,7 @@ LargeObjectDesc

From a0510e716727289a825862514abbda4d174928c7 Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Sun, 1 Jun 2025 21:20:16 +0200
Subject: [PATCH 05/11] svariableReceiver

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

+/*
 
diff --git a/src/backend/executor/meson.build b/src/backend/executor/meson.build
index dc45be0b2ce..a572b6dab7c 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..b96f210a072
--- /dev/null
+++ b/src/backend/executor/svariableReceiver.c
@@ -0,0 +1,199 @@
+/*-------------------------------------------------------------------------
+ *
+ * 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 "access/htup_details.h"
+#include "commands/session_variable.h"
+#include "executor/svariableReceiver.h"
+#include "utils/builtins.h"
+#include "utils/datum.h"
+#include "utils/memutils.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.
+ *
+ * The assignment to session variable have to be postponed until we are
+ * sure so only one row was received. We need to hold value from first
+ * row in dedicated context.
+ */
+typedef struct
+{
+	DestReceiver pub;
+	char	   *varname;
+	Oid			typid;
+	int32		typmod;
+	int16		typlen;
+	bool		typbyval;
+	Datum		buffered_value;	/* first row value */
+	bool		buffered_value_is_null;
+	MemoryContext buffer_cxt;	/* holds a value before storing to variable */
+	int			rows;			/* row counter */
+} SVariableState;
+
+/*
+ * Prepare to receive tuples from executor.
+ */
+static void
+svariableStartupReceiver(DestReceiver *self, int operation, TupleDesc typeinfo)
+{
+	SVariableState *myState = (SVariableState *) self;
+	Form_pg_attribute attr;
+
+	Assert(myState->pub.mydest == DestVariable);
+	Assert(typeinfo->natts == 1);
+
+	attr = TupleDescAttr(typeinfo, 0);
+
+	Assert(!attr->attisdropped);
+
+	myState->typid = attr->atttypid;
+	myState->typmod = attr->atttypmod;
+	myState->typlen = attr->attlen;
+	myState->typbyval = attr->attbyval;
+
+	myState->buffered_value = (Datum) 0;
+	myState->buffered_value_is_null = true;
+
+	if (!myState->typbyval)
+		myState->buffer_cxt = AllocSetContextCreate(CurrentMemoryContext,
+													"SVariableReceiverBuffer",
+													ALLOCSET_DEFAULT_SIZES);
+	else
+		myState->buffer_cxt = NULL;
+
+	myState->rows = 0;
+}
+
+/*
+ * Receive a tuple from the executor and store it in the buffer
+ */
+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->typlen == -1) && !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")));
+
+	/*
+	 * We cannot to assign received value directly, so we should to
+	 * save received value in the buffer.
+	 */
+	if (!isnull)
+	{
+		if (!myState->typbyval)
+		{
+			MemoryContext oldcxt = MemoryContextSwitchTo(myState->buffer_cxt);
+
+			myState->buffered_value = datumCopy(value,
+												myState->typbyval,
+												myState->typlen);
+
+			MemoryContextSwitchTo(oldcxt);
+		}
+		else
+			myState->buffered_value = value;
+
+		myState->buffered_value_is_null = false;
+	}
+
+	if (freeval)
+		pfree(DatumGetPointer(value));
+
+	return true;
+}
+
+/*
+ * Clean up at end of the executor run
+ */
+static void
+svariableShutdownReceiver(DestReceiver *self)
+{
+	SVariableState *myState = (SVariableState *) self;
+
+	if (myState->rows == 0)
+		ereport(ERROR,
+				(errcode(ERRCODE_NO_DATA_FOUND),
+				 errmsg("expression returned no rows")));
+
+	SetSessionVariableWithTypecheck(myState->varname,
+									myState->typid, myState->typmod,
+									myState->buffered_value,
+									myState->buffered_value_is_null);
+
+	if (myState->buffer_cxt)
+		MemoryContextDelete(myState->buffer_cxt);
+}
+
+/*
+ * Destroy the receiver when we are done with it
+ */
+static void
+svariableDestroyReceiver(DestReceiver *self)
+{
+	pfree(((SVariableState *) self)->varname);
+	pfree(self);
+}
+
+/*
+ * Initially create a DestReceiver object.
+ */
+DestReceiver *
+CreateVariableDestReceiver(char *varname)
+{
+	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->varname = pstrdup(varname);
+
+	return (DestReceiver *) self;
+}
diff --git a/src/backend/tcop/dest.c b/src/backend/tcop/dest.c
index fb163930c89..13fe536b432 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"

 
 	/* should never get here */
@@ -192,6 +196,7 @@ EndCommand(const QueryCompletion *qc, CommandDest dest, bool force_undecorated_o
 		case DestTransientRel:
 		case DestTupleQueue:
 		case DestExplainSerialize:
+		case DestVariable:
 			break;
 	}
 }
@@ -238,6 +243,7 @@ NullCommand(CommandDest dest)
 		case DestTransientRel:
 		case DestTupleQueue:
 		case DestExplainSerialize:
+		case DestVariable:
 			break;
 	}
 }
@@ -282,6 +288,7 @@ ReadyForQuery(CommandDest dest)
 		case DestTransientRel:
 		case DestTupleQueue:
 		case DestExplainSerialize:
+		case DestVariable:
 			break;
 	}
 }
diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h
index 3687490bcb1..610b757899e 100644
--- a/src/include/commands/session_variable.h
+++ b/src/include/commands/session_variable.h
@@ -23,6 +23,9 @@ extern void CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt);
 extern void DropVariableByName(char *varname);

 
 /* ----------------
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 08862700a5f..184b790de97 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2703,6 +2703,7 @@ STRLEN
 SV
 SVariableData
 SVariable
+SVariableState
 SYNCHRONIZATION_BARRIER
 SYSTEM_INFO
 SampleScan
-- 
2.53.0

From ecf5a731aa19761e11e6c42e122563550bc390d3 Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Sat, 22 Nov 2025 06:40:46 +0100
Subject: [PATCH 04/11] fill an auxiliary buffer with values of session 
 variables used in query

 
+	/*
 
+/* ----------------
+ * SessionVariableValue
+ * ----------------
+ */

index d020af618d0..08862700a5f 100644
@@ -2761,6 +2761,7 @@ SerializedTransactionState
 Session
 SessionBackupState
 SessionEndType
+SessionVariableValue
 SetConstraintState
 SetConstraintStateData
 SetConstraintTriggerData
-- 
2.53.0

From b42f4d07fbc8c393666abdeb1222d3d14236cbf3 Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Fri, 21 Nov 2025 20:42:56 +0100
Subject: [PATCH 03/11] collect session variables used in plan and assign
 paramid

* parallel execution

plan
---

index 570c434ede8..61bb2f47a0d 100644
 
+		/*
 
+/*
index 42604a0f75c..53af28c49d8 100644
 
 	/*

+	/*

+/*

index a41d81734cf..aeaf4ac502f 100644

 				/*

 	return false;
 }

 
 	/*

From 7ebb0eba2f71157ec046e5eabc443480f04168ff Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Fri, 21 Nov 2025 15:28:59 +0100
Subject: [PATCH 02/11] parsing session variable fences

 
  <sect1 id="ddl-others">
 
+	/*

+/*

 

 

 

 

 
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index a56430fbdfd..7fed5ad7ece 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -534,7 +534,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
@@ -893,7 +893,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		'*' '/' '%'
@@ -15821,6 +15821,8 @@ c_expr:		columnref								{ $$ = $1; }
 					else
 						$$ = $2;
 				}
+			| variable_fence
+				{ $$ = $1; }
 			| case_expr
 				{ $$ = $1; }
 			| func_expr
@@ -17223,6 +17225,17 @@ case_arg:	a_expr									{ $$ = $1; }
 			| /*EMPTY*/								{ $$ = NULL; }
 		;
 
+variable_fence:
+			VARIABLE '(' ColId ')'
+				{
 
+		case T_VariableFence:
 
+/*

 
diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c
index 3bcfc1f5e3d..66cd4ba2f59 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 = ((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 f16f1535785..71948cac7ce 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -8835,6 +8835,14 @@ get_parameter(Param *param, deparse_context *context)
 		}
 	}

index 4d9a37ec756..c79f07f3c40 100644

+/*
 
 typedef struct Param

index 84552e32c87..894a7de93a8 100644
 
 	/*
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 5f92193be58..d020af618d0 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -3264,6 +3264,7 @@ ValidatorValidateCB
 ValuesScan
 ValuesScanState
 Var
+VariableFence
 VarBit
 VarChar
 VarParamState
-- 
2.53.0

From a2e0cf364cf24c3e0b199932caec839424e3760a Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Wed, 19 Nov 2025 19:36:07 +0100
Subject: [PATCH 01/11] CREATE VARIABLE, DROP VARIABLE

---

 
 
index 3c3e24324a8..a56430fbdfd 100644
@@ -292,13 +292,13 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
@@ -795,8 +795,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);

 
@@ -1062,6 +1062,7 @@ stmt:
 			| CreatePolicyStmt
 			| CreatePLangStmt
 			| CreateSchemaStmt
+			| CreateSessionVarStmt
 			| CreateSeqStmt
 			| CreateStmt
 			| CreateSubscriptionStmt
@@ -1089,6 +1090,7 @@ stmt:
 			| DropTableSpaceStmt
 			| DropTransformStmt
 			| DropRoleStmt
+			| DropSessionVarStmt
 			| DropUserMappingStmt
 			| DropdbStmt
 			| ExecuteStmt
@@ -5391,6 +5393,47 @@ create_extension_opt_item:
 				}
 		;
 
@@ -18258,6 +18301,7 @@ unreserved_keyword:
@@ -18918,6 +18962,7 @@ bare_label_keyword:
index bf707f2d57f..99c95b80767 100644

+
 				/*

index f8c0865ca89..77f1263d799 100644
 
@@ -3791,7 +3792,7 @@ match_previous_words(int pattern_id,
@@ -4144,6 +4145,13 @@ match_previous_words(int pattern_id,
 			COMPLETE_WITH(",", ")");
 	}
 
+/* CREATE VARIABLE */
index ff41943a6db..4d9a37ec756 100644
 
+
 
 # ----------
index 77e3c04144e..5f92193be58 100644
@@ -678,6 +679,7 @@ DropBehavior
 DropOwnedStmt
 DropReplicationSlotCmd
 DropRoleStmt
+DropSessionVarStmt
 DropStmt
 DropSubscriptionStmt
 DropTableSpaceStmt
@@ -2699,6 +2701,8 @@ SSL_CTX
 STARTUPINFO
 STRLEN
 SV
+SVariableData
+SVariable
 SYNCHRONIZATION_BARRIER
 SYSTEM_INFO
 SampleScan
-- 
2.53.0

---

From: Pavel Stehule <[email-masked]>
Date: Fri, 6 Mar 2026 10:06:42 +0100

Hi

I cleaned dest receiver

Regards

Pavel
From a778bfc2b71d5ec2622a6f6007cc2aff2c112b35 Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Sat, 6 Dec 2025 07:35:30 +0100
Subject: [PATCH 10/11] transactional DDL - CREATE VARIABLE, DROP VARIABLE

 
index 4463cce327a..b242f876ee1 100644
 
 	int16		typlen;
 	bool		typbyval;
+

 
+/*

 	if (found)
 	{

 	svar->value = (Datum) 0;
 	svar->isnull = true;
+
 
 /*

 
 /*
@@ -431,23 +592,29 @@ ExecuteLetStmt(ParseState *pstate,
 }
 
 /*

 /*

 

From a6492cb079182e6982742d00d89b2c01603e39c6 Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Mon, 8 Dec 2025 05:00:12 +0100
Subject: [PATCH 11/11] subtransaction support for session variables DDL
 (CREATE, DROP)

index b242f876ee1..11c287f1f06 100644

 

 
 
+/*

 #endif
 
+BEGIN;
+SAVEPOINT s1;
+DROP VARIABLE x;
 
+BEGIN;
+SAVEPOINT s1;
+DROP VARIABLE x;

From 8721f83ec254616af3dca8336094ecc236bf2b95 Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Mon, 24 Nov 2025 20:04:16 +0100
Subject: [PATCH 07/11] DISCARD TEMP

---

 
 		case DISCARD_TEMP:
 
 		default:
@@ -75,4 +77,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 a5cbc7a2eed..96ea6e6948f 100644
--- a/src/backend/commands/session_variable.c
+++ b/src/backend/commands/session_variable.c
@@ -410,3 +410,23 @@ ExecuteLetStmt(ParseState *pstate,
 
 	PopActiveSnapshot();
 }
+
+/*

From 78499a347cd19669ddb0bb28eaaa1b5551d2fe8d Mon Sep 17 00:00:00 2001

---

index df6cd49af32..4463cce327a 100644
@@ -448,3 +449,38 @@ ResetSessionVariables(void)

+	/* DROP VARIABLE */
 
 /* LET */

+/*
+ * VARIABLE fence
+ */
 
 	/*
index 361e2cfffeb..116549966f6 100644
@@ -12833,4 +12833,9 @@

 DROP VARIABLE x;

From e2d485bce90c5f66ad01f3f6ca20226b0fbffaef Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Thu, 4 Dec 2025 06:17:54 +0100
Subject: [PATCH 08/11] support CREATE IF NOT EXISTS and DROP IF EXISTS

---

  <refsynopsisdiv>
 <synopsis>
 
   <variablelist>

  <refsynopsisdiv>
 <synopsis>
 
index 96ea6e6948f..df6cd49af32 100644

-	if (!svar)

 

 
 
 	if (found)
-		ereport(ERROR,

 

 

 
 
 					n->name = $4;
 					n->typeName = $6;

 
 					n->name = $3;
+					n->missing_ok = false;
+					$$ = (Node *) n;
+				}

 
 		case T_LetStmt:

 

 -- should be ok

From bf841c0cce0648845f3130e227391a2be40ada0b Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Sun, 1 Jun 2025 21:20:16 +0200
Subject: [PATCH 05/11] svariableReceiver

 src/backend/executor/svariableReceiver.c | 139 +++++++++++++++++++++++
 9 files changed, 225 insertions(+)

+/*
 
index 00000000000..f8fbb7a8e71
@@ -0,0 +1,139 @@
+ * deleted attributes.
+ * sure so only one row was received.
+	TupleDesc	tupdesc;
+	HeapTuple	tuple;
+	MemoryContext tuple_cxt;	/* holds a value before storing to variable */
+	myState->tupdesc = typeinfo;
+	myState->tuple = NULL;
+	myState->tuple_cxt = CurrentMemoryContext;
+	if (++myState->rows > 1)
+	oldcxt = MemoryContextSwitchTo(myState->tuple_cxt);
+	myState->tuple = ExecCopySlotHeapTuple(slot);
+	attr = TupleDescAttr(myState->tupdesc, 0);
+	value = heap_getattr(myState->tuple, 1, myState->tupdesc, &isnull);
+									attr->atttypid, attr->atttypmod,
+	heap_freetuple(myState->tuple);
+	self->varname = varname;

 

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

From cc5aeffad26bc181e54dc8b7fd7ba7c8f042b06a Mon Sep 17 00:00:00 2001

 src/backend/commands/session_variable.c       |  87 ++++++++++
 25 files changed, 679 insertions(+), 20 deletions(-)

  <sect1 id="ddl-others">

 

index deb5d7e80f9..a5cbc7a2eed 100644

 /*
@@ -323,3 +328,85 @@ DropVariableByName(char *varname)
+	dest->rDestroy(dest);
 
 	/*

 
+		case T_LetStmt:
 
 			/*

 
 
 	/*
 
 /*
 
+/*
+ * transformLetStmt -

 

 			/*
 
 			/*

 			/*
 
 			/*
 
 			/*
 
 		case T_CallStmt:
 		case T_DoStmt:
+		case T_LetStmt:
 			{
 				/*
 
+		case T_LetStmt:
 
+		case T_LetStmt:
 
+		case T_LetStmt:
+			tag = CMDTAG_LET;
+			break;
+
 
 		case T_PLAssignStmt:
+		case T_LetStmt:
 			lev = LOGSTMT_ALL;
 			break;

+/* LET */

 

 

 

From 4951a64ed5f1210956fa6c20523504b8b3f9268d Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Sat, 22 Nov 2025 06:40:46 +0100
Subject: [PATCH 04/11] fill an auxiliary buffer with values of session 
 variables used in query

 
+	/*
 
+/* ----------------
+ * SessionVariableValue
+ * ----------------
+ */

From 4f8d8daf6700b2105235b7ec9425f51340382e2b Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Fri, 21 Nov 2025 20:42:56 +0100
Subject: [PATCH 03/11] collect session variables used in plan and assign
 paramid

* parallel execution

plan
---

 
+		/*
 
+/*
 
 	/*

+	/*

+/*

 
 
 				/*

 	return false;
 }

 
 	/*

From ae4ba28ad7a06bb34784a8acc2e67463e20f1855 Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Fri, 21 Nov 2025 15:28:59 +0100
Subject: [PATCH 02/11] parsing session variable fences

 
  <sect1 id="ddl-others">
 
+	/*

+/*

 

 

 

 

 
 
+variable_fence:
+			VARIABLE '(' ColId ')'
+				{
 
+		case T_VariableFence:
 
+/*

 

 
 
+/*
 
 typedef struct Param

 
 	/*

From 56c2735e18aa00660c72df53729699395fbd46fd Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Wed, 19 Nov 2025 19:36:07 +0100
Subject: [PATCH 01/11] CREATE VARIABLE, DROP VARIABLE

---

 

 

 
 
+
 				/*

 
 
+/* CREATE VARIABLE */
 
+
 
 # ----------

---

From: Pavel Stehule <[email-masked]>
Date: Fri, 13 Mar 2026 08:54:35 +0100

Hi

rebase

Regards

Pavel
From 10b9232a76e382683570c21022432839e325b6a0 Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Mon, 8 Dec 2025 05:00:12 +0100
Subject: [PATCH 11/11] subtransaction support for session variables DDL
 (CREATE, DROP)

index ccee226cafe..76dc2a843cf 100644
@@ -5213,6 +5213,8 @@ CommitSubTransaction(void)
@@ -5382,6 +5384,8 @@ AbortSubTransaction(void)

 

 
 
+/*

 #endif
 
+BEGIN;
+SAVEPOINT s1;
+DROP VARIABLE x;
 
+BEGIN;
+SAVEPOINT s1;
+DROP VARIABLE x;

From 23aa136c8e170a52255f6a1c57bc2b1dc614cffb Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Sat, 6 Dec 2025 07:35:30 +0100
Subject: [PATCH 10/11] transactional DDL - CREATE VARIABLE, DROP VARIABLE

index aafc53e0164..ccee226cafe 100644
@@ -2334,6 +2335,9 @@ CommitTransaction(void)
 
@@ -2940,6 +2944,7 @@ AbortTransaction(void)
 
 	int16		typlen;
 	bool		typbyval;
+

 
+/*

 	if (found)
 	{

 	svar->value = (Datum) 0;
 	svar->isnull = true;
+
 
 /*

 
 /*
 
 /*

 /*

 

From 9abf10c23c53fc2fb7db1f142456a55f387a627f Mon Sep 17 00:00:00 2001

---

index de2b743921b..75d60fe4034 100644
 
@@ -4437,6 +4442,10 @@ match_previous_words(int pattern_id,
 
+	/* DROP VARIABLE */
@@ -4877,6 +4886,8 @@ match_previous_words(int pattern_id,
 
 /* LET */
 
@@ -5486,6 +5497,12 @@ match_previous_words(int pattern_id,
 			COMPLETE_WITH("'standby_replay'", "'standby_write'", "'standby_flush'", "'primary_flush'");
 	}
 
+/*
+ * VARIABLE fence
+ */
 
 	/*

 DROP VARIABLE x;

From 1cfd8a2545181eb70957c6100a2373ddd6728a00 Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Mon, 24 Nov 2025 20:04:16 +0100
Subject: [PATCH 07/11] DISCARD TEMP

---

 
 		case DISCARD_TEMP:
 
 		default:
 
 	PopActiveSnapshot();
 }
+
+/*

From 29aeb3e8168048092c3cdc95dfebadb4fb7c8437 Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Thu, 4 Dec 2025 06:17:54 +0100
Subject: [PATCH 08/11] support CREATE IF NOT EXISTS and DROP IF EXISTS

---

  <refsynopsisdiv>
 <synopsis>
 
   <variablelist>

  <refsynopsisdiv>
 <synopsis>

 
-	if (!svar)

 

 
 
 	if (found)
-		ereport(ERROR,

 

 

index 71af62cdd47..01260bd31e9 100644
@@ -5402,7 +5402,7 @@ create_extension_opt_item:
 
@@ -5419,14 +5419,31 @@ CreateSessionVarStmt:
 
 					n->name = $4;
 					n->typeName = $6;

@@ -5436,8 +5453,18 @@ DropSessionVarStmt:
 					DropSessionVarStmt *n = makeNode(DropSessionVarStmt);
 
 					n->name = $3;
+					n->missing_ok = false;
+					$$ = (Node *) n;
+				}
 
index d722efcad46..50e5db76f28 100644

 		case T_LetStmt:

index 6e5e74e7c49..4335612ba86 100644

 
 -- should be ok

From 8292fef3a502ce9c5c77355131f95c0878f19f25 Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Sun, 1 Jun 2025 21:20:16 +0200
Subject: [PATCH 05/11] svariableReceiver

 
+/*

 

 
 
 /* ----------------
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 24f9a00da33..fb82de43f01 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2707,6 +2707,7 @@ STRLEN
 SV
 SVariableData
 SVariable
+SVariableState
 SYNCHRONIZATION_BARRIER
 SYSTEM_INFO
 SampleScan
-- 
2.53.0

From d24c0530badf853ac1bcabc6f759fe61e9eaaa62 Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Sat, 22 Nov 2025 06:40:46 +0100
Subject: [PATCH 04/11] fill an auxiliary buffer with values of session 
 variables used in query

index bd46b75e498..5daa7fef9cb 100644
@@ -1102,6 +1102,35 @@ ExecInitExprRec(Expr *node, ExprState *state,
 
+	/*
 
+/* ----------------
+ * SessionVariableValue
+ * ----------------
+ */

index e71068b5880..24f9a00da33 100644
@@ -2765,6 +2765,7 @@ SerializedTransactionState
 Session
 SessionBackupState
 SessionEndType
+SessionVariableValue
 SetConstraintState
 SetConstraintStateData
 SetConstraintTriggerData
-- 
2.53.0

From ad384b6671bf0f05bd391d310dd4bd68f73d1314 Mon Sep 17 00:00:00 2001

 
  <sect1 id="ddl-others">
index 486dc9abd4b..59644a90d15 100644

 

index f769364cdb8..aadbb6c777a 100644

 /*
 
 	/*

 
+		case T_LetStmt:
 
 			/*

 
 
 	/*
 
 /*
 
+/*
+ * transformLetStmt -
index ca15c918c5b..71af62cdd47 100644
 		RemoveFuncStmt RemoveOperStmt RenameStmt RepackStmt ReturnStmt RevokeStmt RevokeRoleStmt

 	MAPPING MATCH MATCHED MATERIALIZED MAXVALUE MERGE MERGE_ACTION METHOD
@@ -1100,6 +1100,7 @@ stmt:
 			| ImportForeignSchemaStmt
 			| IndexStmt
 			| InsertStmt
+			| LetStmt
 			| ListenStmt
 			| RefreshMatViewStmt
 			| LoadStmt
@@ -13105,6 +13106,37 @@ opt_hold: /* EMPTY */						{ $$ = 0; }
 			| WITHOUT HOLD					{ $$ = 0; }
 		;
 
+/*****************************************************************************
+ *
+ *		QUERY:
+ *				LET STATEMENT
+ *
+ *****************************************************************************/
+LetStmt:	LET ColId '=' a_expr
+				{
+					LetStmt	   *n = makeNode(LetStmt);
+					SelectStmt *select;
+					ResTarget  *res;
+
+					n->target = $2;
+
+					select = makeNode(SelectStmt);
+					res = makeNode(ResTarget);
+
+					/* create target list for implicit query */
+					res->name = NULL;
+					res->indirection = NIL;
+					res->val = (Node *) $4;
+					res->location = @4;
+
+					select->targetList = list_make1(res);
+					n->query = (Node *) select;
+
+					n->location = @2;
+					$$ = (Node *) n;
+				}
+		;
+
 /*****************************************************************************
  *
  *		QUERY:
@@ -18215,6 +18247,7 @@ unreserved_keyword:
 			| LARGE_P
 			| LAST_P
 			| LEAKPROOF
+			| LET
 			| LEVEL
 			| LISTEN
 			| LOAD
@@ -18834,6 +18867,7 @@ bare_label_keyword:
 			| LEAKPROOF
 			| LEAST
 			| LEFT
+			| LET
 			| LEVEL
 			| LIKE
 			| LISTEN
diff --git a/src/backend/parser/parse_agg.c b/src/backend/parser/parse_agg.c
index 33fd2cccae5..935c8d33caa 100644
--- a/src/backend/parser/parse_agg.c
+++ b/src/backend/parser/parse_agg.c
@@ -585,6 +585,10 @@ check_agglevels_and_constraints(ParseState *pstate, Node *expr)
 			errkind = true;
 			break;
 
@@ -1024,6 +1028,9 @@ transformWindowFuncCall(ParseState *pstate, WindowFunc *wfunc,
 
 			/*
index 26ec326bc1b..1e0f2dea992 100644
 
 			/*

 			/*
 
 			/*
 
 			/*
index 99876505423..d722efcad46 100644
 
 		case T_CallStmt:
 		case T_DoStmt:
+		case T_LetStmt:
 			{
 				/*
 
+		case T_LetStmt:
 
+		case T_LetStmt:
 
+		case T_LetStmt:
+			tag = CMDTAG_LET;
+			break;
+
 			/* utility statements --- same whether raw or cooked */
 		case T_TransactionStmt:
 			{
@@ -3308,6 +3322,7 @@ GetCommandLogLevel(Node *parsetree)
 			break;
 
 		case T_PLAssignStmt:
+		case T_LetStmt:
 			lev = LOGSTMT_ALL;
 			break;
 
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 903d28e2bcc..de2b743921b 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -1265,8 +1265,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", "REPACK",
 	"RESET", "REVOKE", "ROLLBACK",
 	"SAVEPOINT", "SECURITY LABEL", "SELECT", "SET", "SHOW", "START",
@@ -4875,6 +4875,11 @@ match_previous_words(int pattern_id,
 	else if (TailMatches("VALUES") && !TailMatches("DEFAULT", "VALUES"))
 		COMPLETE_WITH("(");
 
+/* LET */

 
index 58632942315..6e5e74e7c49 100644

index 9ccbbea5ac5..ac8f4b0da95 100644

index c0c521e0e60..b3b17e6cfb4 100644

index 29d0543b38c..88c957249b5 100644
 
index fb82de43f01..86784ff1bd9 100644

From 369f1564b93dd909cd05f586795d306f22118efe Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Fri, 21 Nov 2025 15:28:59 +0100
Subject: [PATCH 02/11] parsing session variable fences

 
  <sect1 id="ddl-others">
 
+	/*

+/*

 

 

 

 

 
index daa70c84be7..ca15c918c5b 100644
@@ -15897,6 +15897,8 @@ c_expr:		columnref								{ $$ = $1; }
@@ -17299,6 +17301,17 @@ case_arg:	a_expr									{ $$ = $1; }
 
+variable_fence:
+			VARIABLE '(' ColId ')'
+				{
index 96991cae764..26ec326bc1b 100644
 
+		case T_VariableFence:
 
+/*

 
index 6298a37f88e..680f65c79c8 100644
@@ -8838,6 +8838,14 @@ get_parameter(Param *param, deparse_context *context)
 		}
 	}

index 8820d089bc3..58632942315 100644

+/*
 
 typedef struct Param

 
 	/*
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index c1d8365fcaf..e71068b5880 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -3269,6 +3269,7 @@ ValidatorValidateCB
 ValuesScan
 ValuesScanState
 Var
+VariableFence
 VarBit
 VarChar
 VarParamState
-- 
2.53.0

From a610046762a1929534f9945157c67cc9ece996ed Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Fri, 21 Nov 2025 20:42:56 +0100
Subject: [PATCH 03/11] collect session variables used in plan and assign
 paramid

* parallel execution

plan
---

  <sect2 id="parallel-labeling">
diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c
index 09575278de3..0b916c7a776 100644
--- a/src/backend/catalog/dependency.c
+++ b/src/backend/catalog/dependency.c
@@ -1962,6 +1962,16 @@ find_expr_references_walker(Node *node,
 	{
 		Param	   *param = (Param *) node;
 
+		/*
 
+/*
 
 	/*

+	/*

+/*
 
index b2beb0a0d68..96eafa57169 100644
@@ -1708,6 +1708,9 @@ pull_up_simple_subquery(PlannerInfo *root, Node *jtnode, RangeTblEntry *rte,
 
index 9fb266d089d..682dc2d6cf7 100644
@@ -26,6 +26,7 @@
@@ -946,6 +947,13 @@ max_parallel_hazard_walker(Node *node, max_parallel_hazard_context *context)
 
@@ -2633,6 +2641,7 @@ convert_saop_to_hashed_saop_walker(Node *node, void *context)
@@ -2759,6 +2768,29 @@ eval_const_expressions_mutator(Node *node,
 
 				/*
@@ -5434,7 +5466,8 @@ inline_function(Oid funcid, Oid result_type, Oid result_collid,

 	return false;
 }
 
index 27758ec16fe..9ccbbea5ac5 100644

 	/*

From 40d1499899c871500bb8f8205f39c232a65343cc Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Wed, 19 Nov 2025 19:36:07 +0100
Subject: [PATCH 01/11] CREATE VARIABLE, DROP VARIABLE

---

 
 
index 141ada9c50a..486dc9abd4b 100644
index d9fdbb5d254..f769364cdb8 100644
index f01f5734fe9..daa70c84be7 100644

 
@@ -1061,6 +1061,7 @@ stmt:
 			| CreatePolicyStmt
 			| CreatePLangStmt
 			| CreateSchemaStmt
+			| CreateSessionVarStmt
 			| CreateSeqStmt
 			| CreateStmt
 			| CreateSubscriptionStmt
@@ -1088,6 +1089,7 @@ stmt:
 			| DropTableSpaceStmt
 			| DropTransformStmt
 			| DropRoleStmt
+			| DropSessionVarStmt
 			| DropUserMappingStmt
 			| DropdbStmt
 			| ExecuteStmt
@@ -5396,6 +5398,47 @@ create_extension_opt_item:
 				}
 		;
 
@@ -18335,6 +18378,7 @@ unreserved_keyword:
@@ -18996,6 +19040,7 @@ bare_label_keyword:
index b4651a64131..99876505423 100644

+
 				/*
 				 * ************* object creation / destruction **************
 				 */
@@ -3239,6 +3252,14 @@ CreateCommandTag(Node *parsetree)
 			}
 			break;
 
@@ -3777,6 +3798,11 @@ GetCommandLogLevel(Node *parsetree)
 			}
 			break;
 
index 199fc64ddf5..903d28e2bcc 100644
 
@@ -4151,6 +4152,13 @@ match_previous_words(int pattern_id,
 			COMPLETE_WITH(",", ")");
 	}
 
+/* CREATE VARIABLE */
index f3d32ef0188..8820d089bc3 100644
 
+
index 6f74a8c05c7..c0c521e0e60 100644
@@ -492,6 +492,7 @@ PG_KEYWORD("validator", VALIDATOR, UNRESERVED_KEYWORD, BARE_LABEL)
index 652dc61b834..29d0543b38c 100644
 
 # ----------
index 0de55183793..c1d8365fcaf 100644
@@ -2703,6 +2705,8 @@ SSL_CTX
 STARTUPINFO
 STRLEN
 SV
+SVariableData
+SVariable
 SYNCHRONIZATION_BARRIER
 SYSTEM_INFO
 SampleScan
-- 
2.53.0

---

From: Pavel Stehule <[email-masked]>
Date: Tue, 17 Mar 2026 20:29:46 +0100

Hi

again fresh rebase

Regards

Pavel
From 1002ab5da42121998a0a4e6e97aa7cdd33024f2f Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Mon, 8 Dec 2025 05:00:12 +0100
Subject: [PATCH 11/11] subtransaction support for session variables DDL
 (CREATE, DROP)

index c36af7bc460..c80ab48ec47 100644
 
@@ -61,6 +62,8 @@ typedef struct SVariableData
 
@@ -351,6 +354,8 @@ CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt)
 
@@ -387,6 +392,7 @@ DropVariableByName(DropSessionVarStmt *stmt)
 						stmt->name)));

@@ -456,6 +462,7 @@ AtPreEOXact_SessionVariables(bool isCommit)
 						free_stacked_svars(svar->prev);
 						svar->prev = NULL;
 						svar->created_lxid = InvalidLocalTransactionId;
+						svar->created_subid = InvalidSubTransactionId;
 					}
 				}
 				else
@@ -502,6 +509,7 @@ AtPreEOXact_SessionVariables(bool isCommit)
 
 						/* revert dropped flag */
 						svar->dropped_lxid = InvalidLocalTransactionId;
+						svar->dropped_subid = InvalidSubTransactionId;
 					}
 				}
 			}
@@ -511,6 +519,107 @@ AtPreEOXact_SessionVariables(bool isCommit)
 	}
 }
 
+/*

 #endif
 
+BEGIN;
+SAVEPOINT s1;
+DROP VARIABLE x;
 
+BEGIN;
+SAVEPOINT s1;
+DROP VARIABLE x;

From e9dd1b9fcf32c21d177c7adbb02004e3329e921a Mon Sep 17 00:00:00 2001

---
 src/backend/commands/session_variable.c       | 37 +++++++++++++++++++
 src/bin/psql/tab-complete.in.c                | 17 +++++++++
 src/include/catalog/pg_proc.dat               |  5 +++
 .../expected/session_variables_ddl.out        | 16 ++++++++
 .../regress/sql/session_variables_ddl.sql     |  8 ++++
 5 files changed, 83 insertions(+)

diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c
index 861a9317686..824410a3235 100644
--- a/src/backend/commands/session_variable.c
+++ b/src/backend/commands/session_variable.c
@@ -19,6 +19,7 @@
 #include "commands/session_variable.h"
 #include "executor/executor.h"
 #include "executor/svariableReceiver.h"
+#include "funcapi.h"
 #include "miscadmin.h"
 #include "parser/parse_type.h"
 #include "rewrite/rewriteHandler.h"
@@ -29,6 +30,7 @@
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/snapmgr.h"
+#include "utils/tuplestore.h"
 
 /*
@@ -449,3 +451,38 @@ ResetSessionVariables(void)
index f7e4d7226df..0d6ee4c0b1b 100644
@@ -1208,6 +1208,11 @@ Keywords_for_list_of_owner_roles, "PUBLIC"
 
@@ -4487,6 +4492,10 @@ match_previous_words(int pattern_id,
 
+	/* DROP VARIABLE */
@@ -4936,6 +4945,8 @@ match_previous_words(int pattern_id,
 
 /* LET */
 
@@ -5547,6 +5558,12 @@ match_previous_words(int pattern_id,
 			COMPLETE_WITH("'standby_replay'", "'standby_write'", "'standby_flush'", "'primary_flush'");
 	}
 
+/*
+ * VARIABLE fence
+ */
 
 	/*
index fc8d82665b8..df2a834747c 100644
@@ -12836,4 +12836,9 @@

 DROP VARIABLE x;

From d2a47757262fa5ed28180106fabfdb9b8a2962dd Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Thu, 4 Dec 2025 06:17:54 +0100
Subject: [PATCH 08/11] support CREATE IF NOT EXISTS and DROP IF EXISTS

---

  <refsynopsisdiv>
 <synopsis>
 
   <variablelist>

  <refsynopsisdiv>
 <synopsis>
 
index c75f851bad2..861a9317686 100644
@@ -93,7 +93,7 @@ create_sessionvars_hashtables(void)
 
@@ -103,7 +103,7 @@ search_variable(char *varname)
 	svar = (SVariable) hash_search(sessionvars, varname,
 								   HASH_FIND, NULL);
 
-	if (!svar)
+	if (!svar && !missing_ok)
 		ereport(ERROR,
 				(errcode(ERRCODE_UNDEFINED_OBJECT),
 				 errmsg("session variable \"%s\" doesn't exist",
@@ -125,7 +125,7 @@ get_session_variable_type_typmod_collid(char *varname,
 {
 	SVariable	svar;

 	/* only owner can set content of variable */
 	*typid = svar->vartype;
@@ -145,7 +145,7 @@ GetSessionVariableWithTypecheck(char *varname,
 	SVariable	svar;
 	Datum		result;

 	if (svar->vartype != typid || svar->vartypmod != typmod)
 		ereport(ERROR,
@@ -182,7 +182,7 @@ SetSessionVariableWithTypecheck(char *varname,
 {
 	SVariable	svar;

 	if (svar->vartype != typid || svar->vartypmod != typmod)
 		ereport(ERROR,
@@ -278,10 +278,21 @@ CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt)
 					   HASH_ENTER, &found);
 
 	if (found)
-		ereport(ERROR,
 
 	namestrcpy(&svar->varname, stmt->name);
 	svar->vartype = typeid;
@@ -299,7 +310,7 @@ CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt)
  * Drop variable by name
  */
 void
-DropVariableByName(char *varname)
+DropVariableByName(DropSessionVarStmt *stmt)
 {
 	SVariable	svar;
 
@@ -311,20 +322,27 @@ DropVariableByName(char *varname)
 	PreventCommandIfParallelMode("DROP VARIABLE");
 	PreventCommandDuringRecovery("DROP VARIABLE");

 
 
 	if (hash_search(sessionvars,
-					   varname,
+					   stmt->name,
 					   HASH_REMOVE,
 					   NULL) == NULL)
 		elog(ERROR, "hash table corrupted");
@@ -348,7 +366,7 @@ ExecuteLetStmt(ParseState *pstate,
 	char	   *varname = query->resultVariable;
 	SVariable	svar;

index 777b0d2e5be..fd211dd6815 100644
@@ -5435,7 +5435,7 @@ create_extension_opt_item:
 
@@ -5452,14 +5452,31 @@ CreateSessionVarStmt:
 
 					n->name = $4;
 					n->typeName = $6;

@@ -5469,8 +5486,18 @@ DropSessionVarStmt:
 					DropSessionVarStmt *n = makeNode(DropSessionVarStmt);
 
 					n->name = $3;
+					n->missing_ok = false;
+					$$ = (Node *) n;
+				}
 
 /*****************************************************************************
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index fda4e019b13..5c34a8e95a1 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -1076,7 +1076,7 @@ standard_ProcessUtility(PlannedStmt *pstmt,

 		case T_LetStmt:

index 95dd1450af1..82676236c6d 100644
@@ -3680,6 +3680,7 @@ typedef struct CreateSessionVarStmt
 
@@ -3689,7 +3690,8 @@ typedef struct CreateSessionVarStmt

 -- should be ok

From 71ed55bd3a4e1e06d152ea35e1c0cbf1c4726616 Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Mon, 24 Nov 2025 20:04:16 +0100
Subject: [PATCH 07/11] DISCARD TEMP

---

 
 		case DISCARD_TEMP:
 
 		default:
@@ -75,4 +77,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 abcb0bb531a..c75f851bad2 100644
--- a/src/backend/commands/session_variable.c
+++ b/src/backend/commands/session_variable.c
@@ -411,3 +411,23 @@ ExecuteLetStmt(ParseState *pstate,
 
 	PopActiveSnapshot();
 }
+
+/*

From 8075785b99770e7513c3d8cdb33e4dad2ab8db83 Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Sat, 6 Dec 2025 07:35:30 +0100
Subject: [PATCH 10/11] transactional DDL - CREATE VARIABLE, DROP VARIABLE

 
index 824410a3235..c36af7bc460 100644
@@ -56,6 +56,11 @@ typedef struct SVariableData
 
 	int16		typlen;
 	bool		typbyval;
+
 
 typedef SVariableData *SVariable;
@@ -64,6 +69,14 @@ static HTAB *sessionvars = NULL;	/* hash table for session variables */

+/*
@@ -105,6 +118,14 @@ search_variable(char *varname, bool missing_ok)
 
@@ -237,6 +258,7 @@ CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt)
@@ -281,19 +303,37 @@ CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt)
 
 	if (found)
 	{
 
 	namestrcpy(&svar->varname, stmt->name);
@@ -306,6 +346,12 @@ CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt)
 
 	svar->value = (Datum) 0;
 	svar->isnull = true;
+
 
 /*
@@ -340,14 +386,129 @@ DropVariableByName(DropSessionVarStmt *stmt)
 				 errmsg("must be owner of session variable %s",
 						stmt->name)));

 
 /*
@@ -433,23 +594,29 @@ ExecuteLetStmt(ParseState *pstate,
 }
 
 /*

 /*

 

From 69ac5e21b4e6763edfe2025f4047ee6eacbad971 Mon Sep 17 00:00:00 2001

 src/backend/parser/parse_agg.c                |   6 +

index b96919999b1..7d03f893558 100644
@@ -5955,10 +5955,32 @@ SELECT ... FROM GRAPH_TABLE (myshop MATCH (IS person WHERE name = '...')-[]->...
 
  <sect1 id="ddl-others">
index 35d485f5bc4..a0a6150feab 100644
@@ -160,6 +160,7 @@ Complete list of usable sgml source files in this directory.

 

index 342a7afb517..4d5a88f753b 100644
@@ -188,6 +188,7 @@
index cd171a68cbf..abcb0bb531a 100644
@@ -17,13 +17,18 @@
 
 /*
@@ -324,3 +329,85 @@ DropVariableByName(char *varname)
index 7ebf63e46ba..fb512c0be2a 100644
@@ -4416,6 +4416,14 @@ raw_expression_tree_walker_impl(Node *node,
 
 	/*
index 8ad77b765c5..616575f3139 100644

 
+		case T_LetStmt:
 
 			/*

@@ -2870,9 +2883,11 @@ transformPLAssignStmt(ParseState *pstate, PLAssignStmt *stmt)
 						   EXPR_KIND_UPDATE_TARGET);

 	/*
 	 * To avoid duplicating a lot of code, we use transformSelectStmt to do
@@ -2895,18 +2910,21 @@ transformPLAssignStmt(ParseState *pstate, PLAssignStmt *stmt)
 
 /*
@@ -2941,7 +2959,7 @@ transformPLAssignStmtTarget(ParseState *pstate, List *tlist,
@@ -2949,10 +2967,10 @@ transformPLAssignStmtTarget(ParseState *pstate, List *tlist,
@@ -2975,7 +2993,7 @@ transformPLAssignStmtTarget(ParseState *pstate, List *tlist,
@@ -2984,7 +3002,7 @@ transformPLAssignStmtTarget(ParseState *pstate, List *tlist,
@@ -3352,6 +3370,59 @@ transformCallStmt(ParseState *pstate, CallStmt *stmt)
 	return result;
 }
 
+/*
+ * transformLetStmt -
index 511b6c50f23..777b0d2e5be 100644
@@ -304,7 +304,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
@@ -784,7 +784,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	KEEP KEY KEYS

 	MAPPING MATCH MATCHED MATERIALIZED MAXVALUE MERGE MERGE_ACTION METHOD
@@ -1133,6 +1133,7 @@ stmt:
 			| ImportForeignSchemaStmt
 			| IndexStmt
 			| InsertStmt
+			| LetStmt
 			| ListenStmt
 			| RefreshMatViewStmt
 			| LoadStmt
@@ -13551,6 +13552,37 @@ opt_hold: /* EMPTY */						{ $$ = 0; }
 			| WITHOUT HOLD					{ $$ = 0; }
 		;
 
+/*****************************************************************************
+ *
+ *		QUERY:
+ *				LET STATEMENT
+ *
+ *****************************************************************************/
+LetStmt:	LET ColId '=' a_expr
+				{
+					LetStmt	   *n = makeNode(LetStmt);
+					SelectStmt *select;
+					ResTarget  *res;
+
+					n->target = $2;
+
+					select = makeNode(SelectStmt);
+					res = makeNode(ResTarget);
+
+					/* create target list for implicit query */
+					res->name = NULL;
+					res->indirection = NIL;
+					res->val = (Node *) $4;
+					res->location = @4;
+
+					select->targetList = list_make1(res);
+					n->query = (Node *) select;
+
+					n->location = @2;
+					$$ = (Node *) n;
+				}
+		;
+
 /*****************************************************************************
  *
  *		QUERY:
@@ -18893,6 +18925,7 @@ unreserved_keyword:
 			| LARGE_P
 			| LAST_P
 			| LEAKPROOF
+			| LET
 			| LEVEL
 			| LISTEN
 			| LOAD
@@ -19522,6 +19555,7 @@ bare_label_keyword:
 			| LEAKPROOF
 			| LEAST
 			| LEFT
+			| LET
 			| LEVEL
 			| LIKE
 			| LISTEN
diff --git a/src/backend/parser/parse_agg.c b/src/backend/parser/parse_agg.c
index 6076e9373c1..bd20e8bf07d 100644
--- a/src/backend/parser/parse_agg.c
+++ b/src/backend/parser/parse_agg.c
@@ -590,7 +590,10 @@ check_agglevels_and_constraints(ParseState *pstate, Node *expr)
 				err = _("aggregate functions are not allowed in property definition expressions");
 			else
 				err = _("grouping operations are not allowed in property definition expressions");
+			break;

 			/*
@@ -1035,6 +1038,9 @@ transformWindowFuncCall(ParseState *pstate, WindowFunc *wfunc,
 		case EXPR_KIND_PROPGRAPH_PROPERTY:
 			err = _("window functions are not allowed in property definition expressions");
 			break;
+		case EXPR_KIND_LET_TARGET:
+			errkind = true;
+			break;
 
 			/*
index 442d7497b8c..f5f0620ba4c 100644
@@ -594,6 +594,9 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
 
 			/*
@@ -968,6 +971,7 @@ expr_kind_allows_session_variables(ParseExprKind p_expr_kind)
 
@@ -1999,6 +2003,9 @@ transformSubLink(ParseState *pstate, SubLink *sublink)
 		case EXPR_KIND_PROPGRAPH_PROPERTY:
 			err = _("cannot use subquery in property definition expression");
 			break;
+		case EXPR_KIND_LET_TARGET:
+			err = _("cannot use subquery as a target of LET command");
+			break;
 
 			/*
 			 * There is intentionally no default: case here, so that the
@@ -3360,6 +3367,8 @@ ParseExprKindName(ParseExprKind exprKind)
 			return "CYCLE";
 		case EXPR_KIND_PROPGRAPH_PROPERTY:
 			return "property definition expression";
+		case EXPR_KIND_LET_TARGET:
+			return "LET";
 
 			/*
 			 * There is intentionally no default: case here, so that the
diff --git a/src/backend/parser/parse_func.c b/src/backend/parser/parse_func.c
index 8dbd41a3548..9e11313dac3 100644
--- a/src/backend/parser/parse_func.c
+++ b/src/backend/parser/parse_func.c
@@ -2786,6 +2786,9 @@ check_srf_call_placement(ParseState *pstate, Node *last_srf, int location)
 		case EXPR_KIND_PROPGRAPH_PROPERTY:
 			err = _("set-returning functions are not allowed in property definition expressions");
 			break;
+		case EXPR_KIND_LET_TARGET:
+			errkind = true;
+			break;
 
 			/*
 			 * There is intentionally no default: case here, so that the
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 269983987eb..fda4e019b13 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -240,6 +240,7 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree)
 
 		case T_CallStmt:
 		case T_DoStmt:
+		case T_LetStmt:
 			{
 				/*
 				 * Commands inside the DO block or the called procedure might
@@ -1078,6 +1079,11 @@ standard_ProcessUtility(PlannedStmt *pstmt,
 			DropVariableByName(((DropSessionVarStmt *) parsetree)->name);
 			break;
 
+		case T_LetStmt:
@@ -2232,6 +2238,10 @@ UtilityContainsQuery(Node *parsetree)
 
+		case T_LetStmt:
+			qry = castNode(Query, ((LetStmt *) parsetree)->query);
+			return qry;
+
 		default:
 			return NULL;
 	}
@@ -2430,6 +2440,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:
 			{
@@ -3334,6 +3348,7 @@ GetCommandLogLevel(Node *parsetree)
 			break;
 
 		case T_PLAssignStmt:
+		case T_LetStmt:
 			lev = LOGSTMT_ALL;
 			break;
 
index cad85dfadfe..f7e4d7226df 100644
@@ -1273,8 +1273,8 @@ static const char *const sql_commands[] = {
@@ -4934,6 +4934,11 @@ match_previous_words(int pattern_id,
 
+/* LET */

 
index 4bf38e62ed5..95dd1450af1 100644
 
@@ -2255,6 +2258,18 @@ typedef struct MergeStmt

 
 /* macro for fetching the Plan associated with a SubPlan node */
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index 03c38eb9932..be0b07e1ef0 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -262,6 +262,7 @@ PG_KEYWORD("leading", LEADING, RESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("leakproof", LEAKPROOF, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("least", LEAST, COL_NAME_KEYWORD, BARE_LABEL)
 PG_KEYWORD("left", LEFT, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL)
+PG_KEYWORD("let", LET, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("level", LEVEL, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("like", LIKE, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL)
 PG_KEYWORD("limit", LIMIT, RESERVED_KEYWORD, AS_LABEL)
diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h
index 28d9bb67511..e5c444b9fc8 100644
--- a/src/include/parser/parse_node.h
+++ b/src/include/parser/parse_node.h
@@ -83,6 +83,7 @@ typedef enum ParseExprKind
 	EXPR_KIND_GENERATED_COLUMN, /* generation expression for a column */
 	EXPR_KIND_CYCLE_MARK,		/* cycle mark value */
 	EXPR_KIND_PROPGRAPH_PROPERTY,	/* derived property expression */
+	EXPR_KIND_LET_TARGET,		/* only session variables */
 } ParseExprKind;

index 35229a16add..5ff586e14ef 100644
@@ -188,6 +188,7 @@ PG_CMDTAG(CMDTAG_GRANT, "GRANT", true, false, false)
 
index 0c2faf4242f..34a44b6b035 100644
@@ -1585,6 +1585,7 @@ LargeObjectDesc

From 5430af6c13839a957e3691a2c972a6b140e45645 Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Sun, 1 Jun 2025 21:20:16 +0200
Subject: [PATCH 05/11] svariableReceiver

diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c
index 19c153dae3f..cd171a68cbf 100644
--- a/src/backend/commands/session_variable.c
+++ b/src/backend/commands/session_variable.c
@@ -167,6 +167,56 @@ GetSessionVariableWithTypecheck(char *varname,
 	return result;
 }
 
+/*

 

 
 
 /* ----------------
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 25301beaf37..0c2faf4242f 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2728,6 +2728,7 @@ STRLEN
 SV
 SVariableData
 SVariable
+SVariableState
 SYNCHRONIZATION_BARRIER
 SYSTEM_INFO
 SampleScan
-- 
2.53.0

From 0ac2938a3284e2b80a044f3e8e817c4b19cc0601 Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Sat, 22 Nov 2025 06:40:46 +0100
Subject: [PATCH 04/11] fill an auxiliary buffer with values of session 
 variables used in query

index 58b84955c2b..5e641f808ac 100644
@@ -45,6 +45,7 @@
@@ -198,6 +199,54 @@ standard_ExecutorStart(QueryDesc *queryDesc, int eflags)
 
+	/*
index 0716c5a9aed..577500232fb 100644
@@ -657,6 +657,16 @@ typedef struct AsyncRequest
 								 * tuples) */
 } AsyncRequest;
 
+/* ----------------
+ * SessionVariableValue
+ * ----------------
+ */
+typedef struct SessionVariableValue
+{
+	bool		isnull;
+	Datum		value;
+} SessionVariableValue;
+
 /* ----------------
  *	  EState information
  *
@@ -716,6 +726,10 @@ typedef struct EState
 	ParamListInfo es_param_list_info;	/* values of external params */
 	ParamExecData *es_param_exec_vals;	/* values of internal params */

index df442e5168e..314661b0937 100644
index 7cd6d42f5b2..25301beaf37 100644
@@ -2786,6 +2786,7 @@ SerializedTransactionState
 Session
 SessionBackupState
 SessionEndType
+SessionVariableValue
 SetConstraintState
 SetConstraintStateData
 SetConstraintTriggerData
-- 
2.53.0

From bb00e2ec80cbdda4051206e9cce37e509c729e7b Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Fri, 21 Nov 2025 20:42:56 +0100
Subject: [PATCH 03/11] collect session variables used in plan and assign
 paramid

* parallel execution

plan
---

  <sect2 id="parallel-labeling">
diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c
index fdb8e67e1f5..7bd9200ec72 100644
--- a/src/backend/catalog/dependency.c
+++ b/src/backend/catalog/dependency.c
@@ -1972,6 +1972,16 @@ find_expr_references_walker(Node *node,
 	{
 		Param	   *param = (Param *) node;
 
+		/*
index 482737d6797..19c153dae3f 100644
@@ -128,6 +128,45 @@ get_session_variable_type_typmod_collid(char *varname,
 
+/*
 
 	/*

+	/*

+/*
 
index 0e77114efc8..c0f4206ad18 100644
@@ -1712,6 +1712,9 @@ pull_up_simple_subquery(PlannerInfo *root, Node *jtnode, RangeTblEntry *rte,

 
 				/*
 
 	/* If the function result is composite, resolve it */
diff --git a/src/backend/utils/fmgr/fmgr.c b/src/backend/utils/fmgr/fmgr.c
index bfeceb7a92f..24dfbef63f8 100644
--- a/src/backend/utils/fmgr/fmgr.c
+++ b/src/backend/utils/fmgr/fmgr.c
@@ -1992,9 +1992,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;
 }

 
 	/*
 	 * 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 b6185825fcb..b2e3d7b7e56 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -162,6 +162,9 @@ typedef struct PlannedStmt
 	 */
 	List	   *extension_state;

From 03ae0ac8e29d4c78bdfd70fe6bd7058c0107a84b Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Fri, 21 Nov 2025 15:28:59 +0100
Subject: [PATCH 02/11] parsing session variable fences

index 86454e5732e..b96919999b1 100644
@@ -5949,6 +5949,16 @@ SELECT ... FROM GRAPH_TABLE (myshop MATCH (IS person WHERE name = '...')-[]->...
 
  <sect1 id="ddl-others">
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 876aad2100a..5e0774247a7 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -343,6 +343,14 @@ EvaluateParams(ParseState *pstate, PreparedStatement *pstmt, List *params,
 		i++;
 	}
 
+	/*
 
diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c
index a865a4c10c4..482737d6797 100644
--- a/src/backend/commands/session_variable.c
+++ b/src/backend/commands/session_variable.c
@@ -107,6 +107,27 @@ search_variable(char *varname)
 	return svar;
 }
 
+/*
index 6a850349cf7..7ebf63e46ba 100644
@@ -1677,6 +1677,9 @@ exprLocation(const Node *expr)
@@ -4775,6 +4778,9 @@ raw_expression_tree_walker_impl(Node *node,
index ad31dee2686..8ad77b765c5 100644

 

 

 
 
@@ -2506,6 +2511,7 @@ transformReturnStmt(ParseState *pstate, ReturnStmt *stmt)

@@ -2572,6 +2578,7 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)

 
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 3ea571891bb..511b6c50f23 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -535,7 +535,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
@@ -924,7 +924,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 RIGHT_ARROW '|'	/* multi-character ops and user-defined operators */
 %left		'+' '-'
 %left		'*' '/' '%'
@@ -16362,6 +16362,8 @@ c_expr:		columnref								{ $$ = $1; }
 					else
 						$$ = $2;
 				}
+			| variable_fence
+				{ $$ = $1; }
 			| case_expr
 				{ $$ = $1; }
 			| func_expr
@@ -17766,6 +17768,17 @@ case_arg:	a_expr									{ $$ = $1; }
 			| /*EMPTY*/								{ $$ = NULL; }
 		;
 
+variable_fence:
+			VARIABLE '(' ColId ')'
+				{
index 474caffad48..442d7497b8c 100644
@@ -79,6 +80,7 @@ static Node *transformWholeRowRef(ParseState *pstate,
@@ -373,6 +375,10 @@ transformExprRecurse(ParseState *pstate, Node *expr)
 
+		case T_VariableFence:
+			result = transformVariableFence(pstate, (VariableFence *) expr);
+			break;
+
 		default:
 			/* should not reach here */
 			elog(ERROR, "unrecognized node type: %d", (int) nodeTag(expr));
@@ -911,6 +917,119 @@ transformParamRef(ParseState *pstate, ParamRef *pref)
 	return result;
 }
 
+/*

 
diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c
index 541fef5f183..d1a3ea6b1d5 100644
--- a/src/backend/parser/parse_target.c
+++ b/src/backend/parser/parse_target.c
@@ -2040,6 +2040,13 @@ FigureColnameInternal(Node *node, char **name)
 						 (int) ((JsonFuncExpr *) node)->op);
 			}
 			break;
+		case T_VariableFence:
+			{
+				/* return last field name */
+				*name = ((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 7bc12589e40..d091ecb2fa5 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -9360,6 +9360,14 @@ get_parameter(Param *param, deparse_context *context)
 		}
 	}

index 9457ca45b1e..4bf38e62ed5 100644

+/*
index 6fdf8807533..4bbd22be6ed 100644
 
 typedef struct Param
index fc2cbeb2083..28d9bb67511 100644
@@ -244,6 +244,7 @@ struct ParseState

 
 	/*
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 1aeb7cd44a6..7cd6d42f5b2 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -3291,6 +3291,7 @@ ValidatorValidateCB
 ValuesScan
 ValuesScanState
 Var
+VariableFence
 VarBit
 VarChar
 VarParamState
-- 
2.53.0

From b0a96be480e13758041e4133f8c5b0276d87f1c5 Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Wed, 19 Nov 2025 19:36:07 +0100
Subject: [PATCH 01/11] CREATE VARIABLE, DROP VARIABLE

---
 src/backend/commands/session_variable.c       | 216 ++++++++++++++++++
 21 files changed, 715 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml
index 8421ecace1b..86454e5732e 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -5931,6 +5931,26 @@ SELECT ... FROM GRAPH_TABLE (myshop MATCH (IS person WHERE name = '...')-[]->...
   </para>
  </sect1>

 
index e1a56c36221..35d485f5bc4 100644
@@ -101,6 +101,7 @@ Complete list of usable sgml source files in this directory.
@@ -150,6 +151,7 @@ Complete list of usable sgml source files in this directory.
index 674ac17e82c..342a7afb517 100644
@@ -129,6 +129,7 @@
@@ -178,6 +179,7 @@
index c10fdba2bbb..7739f3897ed 100644
@@ -55,6 +55,7 @@ OBJS = \
index 88a2df65c69..d3d3a4ad52b 100644
index 90c7e37a429..e8a018bdd2a 100644
@@ -43,6 +43,7 @@ backend_sources += files(
index 00000000000..a865a4c10c4
@@ -0,0 +1,216 @@
+#include "catalog/pg_type.h"
index c2584249603..3ea571891bb 100644
@@ -292,14 +292,14 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 		CreatePropGraphStmt AlterPropGraphStmt
@@ -826,8 +826,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 
-	VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIADIC VARYING
-	VERBOSE VERSION_P VERTEX VIEW VIEWS VIRTUAL VOLATILE
+	VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIADIC VARIABLE
+	VARYING VERBOSE VERSION_P VERTEX VIEW VIEWS VIRTUAL VOLATILE

@@ -1094,6 +1094,7 @@ stmt:
 			| CreatePLangStmt
 			| CreatePropGraphStmt
 			| CreateSchemaStmt
+			| CreateSessionVarStmt
 			| CreateSeqStmt
 			| CreateStmt
 			| CreateSubscriptionStmt
@@ -1121,6 +1122,7 @@ stmt:
 			| DropTableSpaceStmt
 			| DropTransformStmt
 			| DropRoleStmt
+			| DropSessionVarStmt
 			| DropUserMappingStmt
 			| DropdbStmt
 			| ExecuteStmt
@@ -5429,6 +5431,47 @@ create_extension_opt_item:
 				}
 		;
 
@@ -19017,6 +19060,7 @@ unreserved_keyword:
 			| VERTEX
@@ -19688,6 +19732,7 @@ bare_label_keyword:
index 2b609bfc824..269983987eb 100644
@@ -49,6 +49,7 @@
@@ -186,6 +187,7 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree)
@@ -204,6 +206,7 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree)
@@ -1066,6 +1069,15 @@ standard_ProcessUtility(PlannedStmt *pstmt,
 			}
 			break;
 
@@ -1391,6 +1403,7 @@ ProcessUtilitySlow(ParseState *pstate,
 				}
 				break;
 
+
 				/*
 				 * ************* object creation / destruction **************
 				 */
@@ -3265,6 +3278,14 @@ CreateCommandTag(Node *parsetree)
 			}
 			break;
 
@@ -3811,6 +3832,11 @@ GetCommandLogLevel(Node *parsetree)
 			}
 			break;
 
index 5bdbf1530a2..cad85dfadfe 100644
@@ -1373,6 +1373,7 @@ static const pgsql_thing_t words_after_create[] = {
 
@@ -3835,7 +3836,7 @@ match_previous_words(int pattern_id,
@@ -4195,6 +4196,13 @@ match_previous_words(int pattern_id,
 			COMPLETE_WITH(",", ")");
 	}
 
+/* CREATE VARIABLE */
index ffadd667167..9457ca45b1e 100644
@@ -3643,6 +3643,29 @@ typedef struct AlterStatsStmt
 
+
index b7ded6e6088..03c38eb9932 100644
@@ -500,6 +500,7 @@ PG_KEYWORD("validator", VALIDATOR, UNRESERVED_KEYWORD, BARE_LABEL)
index befae5f6b4f..35229a16add 100644
@@ -125,6 +125,7 @@ PG_CMDTAG(CMDTAG_CREATE_TRANSFORM, "CREATE TRANSFORM", true, false, false)
@@ -178,6 +179,7 @@ PG_CMDTAG(CMDTAG_DROP_TRANSFORM, "DROP TRANSFORM", true, false, false)
index e779ada70cb..df442e5168e 100644
 
 # ----------
index 52f8603a7be..1aeb7cd44a6 100644
@@ -577,6 +577,7 @@ CreateRoleStmt
 CreateSchemaStmt
 CreateSchemaStmtContext
 CreateSeqStmt
+CreateSessionVarStmt
 CreateStatsStmt
 CreateStmt
 CreateStmtContext
@@ -681,6 +682,7 @@ DropBehavior
 DropOwnedStmt
 DropReplicationSlotCmd
 DropRoleStmt
+DropSessionVarStmt
 DropStmt
 DropSubscriptionStmt
 DropTableSpaceStmt
@@ -2724,6 +2726,8 @@ SSL_CTX
 STARTUPINFO
 STRLEN
 SV
+SVariableData
+SVariable
 SYNCHRONIZATION_BARRIER
 SYSTEM_INFO
 SampleScan
-- 
2.53.0

---

From: Pavel Stehule <[email-masked]>
Date: Wed, 18 Mar 2026 07:35:27 +0100

Hi

clean warning after rebase

Regards

Pavel
From 14f76f350815839dda0ee3add6538b7c4d3cbb1f Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Mon, 8 Dec 2025 05:00:12 +0100
Subject: [PATCH 11/11] subtransaction support for session variables DDL
 (CREATE, DROP)

 

 

 
+/*

 #endif
 
+BEGIN;
+SAVEPOINT s1;
+DROP VARIABLE x;
 
+BEGIN;
+SAVEPOINT s1;
+DROP VARIABLE x;

From e0fa40ba287330ce80807da69cdc9ddc4565d25e Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Sat, 6 Dec 2025 07:35:30 +0100
Subject: [PATCH 10/11] transactional DDL - CREATE VARIABLE, DROP VARIABLE

 
 
 	int16		typlen;
 	bool		typbyval;
+

 
+/*

 	if (found)
 	{

 	svar->value = (Datum) 0;
 	svar->isnull = true;
+
 
 /*

 
 /*
 
 /*

 /*

 

From 84b0782e17bf65dd5de99981d216db12a6e8c58b Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Mon, 24 Nov 2025 20:04:16 +0100
Subject: [PATCH 07/11] DISCARD TEMP

---

 
 		case DISCARD_TEMP:
 
 		default:
 
 	PopActiveSnapshot();
 }
+
+/*

From 8d4062f44f52987a457fe989d8f3681ad2615868 Mon Sep 17 00:00:00 2001

---

 /*

+	/* DROP VARIABLE */
 
 /* LET */

+/*
+ * VARIABLE fence
+ */
 
 	/*

 DROP VARIABLE x;

From 44215423aaced84fe2e397153a541053c462ab3c Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Thu, 4 Dec 2025 06:17:54 +0100
Subject: [PATCH 08/11] support CREATE IF NOT EXISTS and DROP IF EXISTS

---

  <refsynopsisdiv>
 <synopsis>
 
   <variablelist>

  <refsynopsisdiv>
 <synopsis>

 
-	if (!svar)

 

 
 
 	if (found)
-		ereport(ERROR,

 

 

 
 
 					n->name = $4;
 					n->typeName = $6;

 
 					n->name = $3;
+					n->missing_ok = false;
+					$$ = (Node *) n;
+				}

 
 		case T_LetStmt:

 

 -- should be ok

From 5b155933efcbf462b0d7d860a1c6fd80417e0d95 Mon Sep 17 00:00:00 2001

 
  <sect1 id="ddl-others">

 

 
 /*
 
 	/*

 
+		case T_LetStmt:
 
 			/*

 
 
 	/*
 
 /*
 
+/*
+ * transformLetStmt -

 

 			/*
 
 			/*
index 33f1bf169c2..97dc14f4ac9 100644
 
 			/*
@@ -969,6 +972,7 @@ expr_kind_allows_session_variables(ParseExprKind p_expr_kind)
 
@@ -2000,6 +2004,9 @@ transformSubLink(ParseState *pstate, SubLink *sublink)
 
 			/*
@@ -3361,6 +3368,8 @@ ParseExprKindName(ParseExprKind exprKind)
 
 			/*
 
 			/*
 
 		case T_CallStmt:
 		case T_DoStmt:
+		case T_LetStmt:
 			{
 				/*
 
+		case T_LetStmt:
 
+		case T_LetStmt:
 
+		case T_LetStmt:
+			tag = CMDTAG_LET;
+			break;
+
 
 		case T_PLAssignStmt:
+		case T_LetStmt:
 			lev = LOGSTMT_ALL;
 			break;

+/* LET */

 

 

 

From f5e17c4cf848145ccf3005487ac310cd962d2a39 Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Sun, 1 Jun 2025 21:20:16 +0200
Subject: [PATCH 05/11] svariableReceiver

 
+/*

 

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

From 0d4bfd070b4c03506198d3d2992de0f7840a272f Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Sat, 22 Nov 2025 06:40:46 +0100
Subject: [PATCH 04/11] fill an auxiliary buffer with values of session 
 variables used in query

 
+	/*
 
+/* ----------------
+ * SessionVariableValue
+ * ----------------
+ */

From dc0d79db957e6e1412c5cffad8165e160c15065d Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Fri, 21 Nov 2025 15:28:59 +0100
Subject: [PATCH 02/11] parsing session variable fences

 src/backend/parser/parse_expr.c         | 120 ++++++++++++++++++++++++
 16 files changed, 229 insertions(+), 3 deletions(-)

  <sect1 id="ddl-others">
 
+	/*

+/*

 

 

 

 

 
 
+variable_fence:
+			VARIABLE '(' ColId ')'
+				{
index 474caffad48..33f1bf169c2 100644
 
+		case T_VariableFence:
+			result = transformVariableFence(pstate, (VariableFence *) expr);
+			break;
+
 		default:
 			/* should not reach here */
 			elog(ERROR, "unrecognized node type: %d", (int) nodeTag(expr));
@@ -911,6 +917,120 @@ transformParamRef(ParseState *pstate, ParamRef *pref)
 	return result;
 }
 
+/*
+		case EXPR_KIND_PROPGRAPH_PROPERTY:

 

 
 
+/*
 
 typedef struct Param

 
 	/*

From e73487a6f1073c39351f6198543383e4307d658a Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Fri, 21 Nov 2025 20:42:56 +0100
Subject: [PATCH 03/11] collect session variables used in plan and assign
 paramid

* parallel execution

plan
---

 
+		/*
 
+/*
 
 	/*

+	/*

+/*

 
 
 				/*

 	return false;
 }

 
 	/*

---

 

 

 
 
+
 				/*

 
 
+/* CREATE VARIABLE */
 
+
 
 # ----------

---

From: Pavel Stehule <[email-masked]>
Date: Thu, 26 Mar 2026 06:18:17 +0100

Hi

only rebase

Regards

Pavel
From 8a7dadd2ebb1046b759998df418f0419fd0712cb Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Mon, 8 Dec 2025 05:00:12 +0100
Subject: [PATCH 11/11] subtransaction support for session variables DDL
 (CREATE, DROP)

 

 

 
+/*

 #endif
 
+BEGIN;
+SAVEPOINT s1;
+DROP VARIABLE x;
 
+BEGIN;
+SAVEPOINT s1;
+DROP VARIABLE x;

From 4afdabd0cf087ff0b20abe6b2800345da061b1a4 Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Sat, 6 Dec 2025 07:35:30 +0100
Subject: [PATCH 10/11] transactional DDL - CREATE VARIABLE, DROP VARIABLE

 
 
 	int16		typlen;
 	bool		typbyval;
+

 
+/*

 	if (found)
 	{

 	svar->value = (Datum) 0;
 	svar->isnull = true;
+
 
 /*

 
 /*
 
 /*

 /*

 

From 594cddff75525f89b0316749459f69991b142207 Mon Sep 17 00:00:00 2001

---

 /*
index 7c8f6d044f3..ba3db64ef44 100644
 
@@ -4502,6 +4507,10 @@ match_previous_words(int pattern_id,
 
+	/* DROP VARIABLE */
@@ -4951,6 +4960,8 @@ match_previous_words(int pattern_id,
 
 /* LET */
 
@@ -5562,6 +5573,12 @@ match_previous_words(int pattern_id,
 			COMPLETE_WITH("'standby_replay'", "'standby_write'", "'standby_flush'", "'primary_flush'");
 	}
 
+/*
+ * VARIABLE fence
+ */
 
 	/*
index 0118e970dda..4152eb99a9b 100644
@@ -12851,4 +12851,9 @@

 DROP VARIABLE x;

From ac6b848f3aa8e5454d3175f2db202d66051b9457 Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Thu, 4 Dec 2025 06:17:54 +0100
Subject: [PATCH 08/11] support CREATE IF NOT EXISTS and DROP IF EXISTS

---

  <refsynopsisdiv>
 <synopsis>
 
   <variablelist>

  <refsynopsisdiv>
 <synopsis>

 
-	if (!svar)

 

 
 
 	if (found)
-		ereport(ERROR,

 

 

index e625de40b43..8e4151de9ed 100644
@@ -5443,7 +5443,7 @@ create_extension_opt_item:
 
@@ -5460,14 +5460,31 @@ CreateSessionVarStmt:
 
 					n->name = $4;
 					n->typeName = $6;

@@ -5477,8 +5494,18 @@ DropSessionVarStmt:
 					DropSessionVarStmt *n = makeNode(DropSessionVarStmt);
 
 					n->name = $3;
+					n->missing_ok = false;
+					$$ = (Node *) n;
+				}

 
 		case T_LetStmt:

index 9988b762a71..2af32ab1555 100644

 
 -- should be ok

From 0c82d40e00993bf435225c1e74b65a405608258f Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Mon, 24 Nov 2025 20:04:16 +0100
Subject: [PATCH 07/11] DISCARD TEMP

---

index 17d172df076..0a33c949fce 100644
 #include "storage/lock.h"
@@ -47,6 +48,7 @@ DiscardCommand(DiscardStmt *stmt, bool isTopLevel)
 
 		case DISCARD_TEMP:
 
 		default:
@@ -76,4 +78,5 @@ DiscardAll(bool isTopLevel)
 	ResetPlanCache();
 
 	PopActiveSnapshot();
 }
+
+/*

From 029accffe1d52b5294c76810edef13b6aa5cd19d Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Sun, 1 Jun 2025 21:20:16 +0200
Subject: [PATCH 05/11] svariableReceiver

 
+/*

 

 
 
 /* ----------------
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index f12ff889508..706b5b37c3b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2740,6 +2740,7 @@ STRLEN
 SV
 SVariableData
 SVariable
+SVariableState
 SYNCHRONIZATION_BARRIER
 SYSTEM_INFO
 SampleScan
-- 
2.53.0

From 0f9c8785e5befb930e603a67331f853fd7b67a86 Mon Sep 17 00:00:00 2001

 
  <sect1 id="ddl-others">

 

 
 /*
 
 	/*

 
+		case T_LetStmt:
 
 			/*

 
 
 	/*
 
 /*
 
+/*
+ * transformLetStmt -
index 3b7430455be..e625de40b43 100644

@@ -13582,6 +13583,37 @@ opt_hold: /* EMPTY */						{ $$ = 0; }
 
@@ -18924,6 +18956,7 @@ unreserved_keyword:
@@ -19553,6 +19586,7 @@ bare_label_keyword:

 			/*
 
 			/*
index de9e1e80103..cddd00fd013 100644
 
 			/*

 			/*
 
 			/*
 
 			/*
 
 		case T_CallStmt:
 		case T_DoStmt:
+		case T_LetStmt:
 			{
 				/*
 
+		case T_LetStmt:
 
+		case T_LetStmt:
 
+		case T_LetStmt:
+			tag = CMDTAG_LET;
+			break;
+
 
 		case T_PLAssignStmt:
+		case T_LetStmt:
 			lev = LOGSTMT_ALL;
 			break;
 
index 0ad1010d329..7c8f6d044f3 100644
@@ -4949,6 +4949,11 @@ match_previous_words(int pattern_id,
 
+/* LET */

 
index 900ab7fa322..9988b762a71 100644

 

 
index 706b5b37c3b..f7854a77290 100644
@@ -1591,6 +1591,7 @@ LargeObjectDesc

From 7770fa1674911f3063fae8b2cdea1d4e5c026b90 Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Sat, 22 Nov 2025 06:40:46 +0100
Subject: [PATCH 04/11] fill an auxiliary buffer with values of session 
 variables used in query

index 77229141b38..8e813ba2d7a 100644
 
+	/*
index 684e398f824..90532582929 100644
 
+/* ----------------
+ * SessionVariableValue
+ * ----------------
+ */

index 5cef3c2f6ff..6a5fbc7b5ec 100644
index a63aaf70c51..f12ff889508 100644
@@ -2798,6 +2798,7 @@ SerializedTransactionState
 Session
 SessionBackupState
 SessionEndType
+SessionVariableValue
 SetConstraintState
 SetConstraintStateData
 SetConstraintTriggerData
-- 
2.53.0

From c208c85b26df866bec91e3369420835b16a807f5 Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Fri, 21 Nov 2025 20:42:56 +0100
Subject: [PATCH 03/11] collect session variables used in plan and assign
 paramid

* parallel execution

plan
---

 
+		/*
 
+/*
 
 	/*

+	/*

+/*
 
index d5e1041ffa3..1295c9dfac6 100644

 
 				/*

 	return false;
 }

 
 	/*

From 2f40be96a99cd1967eee8b294004b8d54e9f681b Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Fri, 21 Nov 2025 15:28:59 +0100
Subject: [PATCH 02/11] parsing session variable fences

 
  <sect1 id="ddl-others">
 
+	/*

+/*

 

 

 

 

 
index e5f4c9eb2bb..3b7430455be 100644
@@ -16393,6 +16393,8 @@ c_expr:		columnref								{ $$ = $1; }
@@ -17797,6 +17799,17 @@ case_arg:	a_expr									{ $$ = $1; }
 
+variable_fence:
+			VARIABLE '(' ColId ')'
+				{
index 312dfdc182a..de9e1e80103 100644
 
+		case T_VariableFence:
 
+/*

 

index fdd2290a698..900ab7fa322 100644

+/*
index b67e56e6c5a..6d83accf985 100644
 
 typedef struct Param

diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
index 65b0fd0790f..f432abaaa95 100644
--- a/src/pl/plpgsql/src/pl_exec.c
+++ b/src/pl/plpgsql/src/pl_exec.c
@@ -8355,7 +8355,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 fb0578eec6d..a63aaf70c51 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -3304,6 +3304,7 @@ ValidatorValidateCB
 ValuesScan
 ValuesScanState
 Var
+VariableFence
 VarBit
 VarChar
 VarParamState
-- 
2.53.0

From 7025a59c31170bf40e6b78e3eba962846ea457ac Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Wed, 19 Nov 2025 19:36:07 +0100
Subject: [PATCH 01/11] CREATE VARIABLE, DROP VARIABLE

---

 
 
index 0fea726cdd5..e5f4c9eb2bb 100644

 
@@ -1094,6 +1094,7 @@ stmt:
 			| CreatePLangStmt
 			| CreatePropGraphStmt
 			| CreateSchemaStmt
+			| CreateSessionVarStmt
 			| CreateSeqStmt
 			| CreateStmt
 			| CreateSubscriptionStmt
@@ -1121,6 +1122,7 @@ stmt:
 			| DropTableSpaceStmt
 			| DropTransformStmt
 			| DropRoleStmt
+			| DropSessionVarStmt
 			| DropUserMappingStmt
 			| DropdbStmt
 			| ExecuteStmt
@@ -5437,6 +5439,47 @@ create_extension_opt_item:
 				}
 		;
 
@@ -19048,6 +19091,7 @@ unreserved_keyword:
@@ -19719,6 +19763,7 @@ bare_label_keyword:

+
 				/*

index 523d3f39fc5..0ad1010d329 100644
 
@@ -3848,7 +3849,7 @@ match_previous_words(int pattern_id,
@@ -4210,6 +4211,13 @@ match_previous_words(int pattern_id,
 			COMPLETE_WITH(",", ")");
 	}
 
+/* CREATE VARIABLE */
index df431220ac5..fdd2290a698 100644
 
+
index 734da057c34..5cef3c2f6ff 100644
 
 # ----------
index dbbec84b222..fb0578eec6d 100644
@@ -581,6 +581,7 @@ CreateRoleStmt
 CreateSchemaStmt
 CreateSchemaStmtContext
 CreateSeqStmt
+CreateSessionVarStmt
 CreateStatsStmt
 CreateStmt
 CreateStmtContext
@@ -685,6 +686,7 @@ DropBehavior
 DropOwnedStmt
 DropReplicationSlotCmd
 DropRoleStmt
+DropSessionVarStmt
 DropStmt
 DropSubscriptionStmt
 DropTableSpaceStmt
@@ -2736,6 +2738,8 @@ SSL_CTX
 STARTUPINFO
 STRLEN
 SV
+SVariableData
+SVariable
 SYNCHRONIZATION_BARRIER
 SYSTEM_INFO
 SampleScan
-- 
2.53.0

---

From: Pavel Stehule <[email-masked]>
Date: Tue, 31 Mar 2026 13:07:34 +0200

Hi

fresh rebase

+ commit message of first patch contains link to related article on
wiki.postgresql.org

Regards

Pavel
From 1b8e41857909526b0752071f9d3fc0813ba7130a Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Fri, 21 Nov 2025 15:28:59 +0100
Subject: [PATCH 02/11] parsing session variable fences

index 17618813582..0055eb84a78 100644
@@ -5954,6 +5954,16 @@ SELECT ... FROM GRAPH_TABLE (myshop MATCH (IS person WHERE name = '...')-[]->...
 
  <sect1 id="ddl-others">
 
+	/*

+/*

 

 

 

 

 
 
+variable_fence:
+			VARIABLE '(' ColId ')'
+				{
 
+		case T_VariableFence:
 
+/*

 
index e5f2b6082ce..689aff154ba 100644

index 8a332839b97..05d5d3c50a2 100644

+/*
index f5b6b45664a..d6fd32e866c 100644
 
 typedef struct Param

 
 	/*
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index b3c18c81066..64541474ece 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -3309,6 +3309,7 @@ ValidatorValidateCB
 ValuesScan
 ValuesScanState
 Var
+VariableFence
 VarBit
 VarChar
 VarParamState
-- 
2.53.0

From 55f67008dcada9db5b8876b042bb711b0c4e12d8 Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Sat, 22 Nov 2025 06:40:46 +0100
Subject: [PATCH 04/11] fill an auxiliary buffer with values of session 
 variables used in query

 
+	/*
 
+/* ----------------
+ * SessionVariableValue
+ * ----------------
+ */

index 64541474ece..5fe08e0885e 100644
@@ -2801,6 +2801,7 @@ SerializedTransactionState
 Session
 SessionBackupState
 SessionEndType
+SessionVariableValue
 SetConstraintState
 SetConstraintStateData
 SetConstraintTriggerData
-- 
2.53.0

From 6509ca3b8f7f04afea80cad7245a20f6e7a93e33 Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Wed, 19 Nov 2025 19:36:07 +0100
Subject: [PATCH 01/11] CREATE VARIABLE, DROP VARIABLE

Introduce simple (non transactional) implementation of basic DDL statements.
Because only temporary variables are allowed, we don't need a dedicated catalog.

This is the most reduced version of declared session variables implementation.
Main goal is introduction of basic functionality and reducing the size of the patch
to maximum. This patch is designed to be possible to move to catalog based
implementation without compatibility break.

Some details about different implementations of session variables

https://wiki.postgresql.org/wiki/Implementation_of_declarative_catalog_session_variables
---

diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml
index 747f929aee3..17618813582 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -5936,6 +5936,26 @@ SELECT ... FROM GRAPH_TABLE (myshop MATCH (IS person WHERE name = '...')-[]->...
   </para>
  </sect1>

 

 

 
+
 				/*

index adcff1f6ffb..214beb0d950 100644

+/* CREATE VARIABLE */
index a501cdfb249..8a332839b97 100644
 
+
 
 # ----------
index 69a71db1496..b3c18c81066 100644
@@ -583,6 +583,7 @@ CreateRoleStmt
 CreateSchemaStmt
 CreateSchemaStmtContext
 CreateSeqStmt
+CreateSessionVarStmt
 CreateStatsStmt
 CreateStmt
 CreateStmtContext
@@ -687,6 +688,7 @@ DropBehavior
 DropOwnedStmt
 DropReplicationSlotCmd
 DropRoleStmt
+DropSessionVarStmt
 DropStmt
 DropSubscriptionStmt
 DropTableSpaceStmt
@@ -2739,6 +2741,8 @@ SSL_CTX
 STARTUPINFO
 STRLEN
 SV
+SVariableData
+SVariable
 SYNCHRONIZATION_BARRIER
 SYSTEM_INFO
 SampleScan
-- 
2.53.0

From 70e3e123362324c5dc19a98e8d7f58796e1553a6 Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Fri, 21 Nov 2025 20:42:56 +0100
Subject: [PATCH 03/11] collect session variables used in plan and assign
 paramid

* parallel execution

plan
---

 
+		/*
 
+/*
index 07944612668..b0933311d6a 100644
 
 	/*
@@ -679,6 +680,9 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
 
@@ -874,6 +878,13 @@ subquery_planner(PlannerGlobal *glob, Query *parse, char *plan_name,
 	 */
 	pull_up_subqueries(root);
 
+	/*
index ff0e875f2a2..e55c4df48ed 100644

+/*
 
@@ -2277,7 +2364,9 @@ fix_alternative_subplan(PlannerInfo *root, AlternativeSubPlan *asplan,
@@ -2299,7 +2388,8 @@ fix_scan_expr(PlannerInfo *root, Node *node, int rtoffset, double num_exec)
index 95bf51606cc..053b070e609 100644
@@ -1713,6 +1713,9 @@ pull_up_simple_subquery(PlannerInfo *root, Node *jtnode, RangeTblEntry *rte,

 
 				/*

 	return false;
 }
 
index 7947d83d584..2a2669f518e 100644
 
@@ -641,6 +644,8 @@ struct PlannerInfo
 
 	/*
index e2c00576d41..598a0ac4685 100644

From 923b9ca30ee4b3c18d5af8ed41d996d165dafdbe Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Sun, 1 Jun 2025 21:20:16 +0200
Subject: [PATCH 05/11] svariableReceiver

 
+/*

 

 
 
 /* ----------------
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 5fe08e0885e..24f2c105918 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2743,6 +2743,7 @@ STRLEN
 SV
 SVariableData
 SVariable
+SVariableState
 SYNCHRONIZATION_BARRIER
 SYSTEM_INFO
 SampleScan
-- 
2.53.0

From 20bbb64bbda05646035284fe865829a5601aafe6 Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Mon, 24 Nov 2025 20:04:16 +0100
Subject: [PATCH 07/11] DISCARD TEMP

---

 		case DISCARD_TEMP:
 
 		default:
 
 	PopActiveSnapshot();
 }
+
+/*

From 6ea3317bd2184e4e5de298653a9dd2b2e0db8f46 Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Thu, 4 Dec 2025 06:17:54 +0100
Subject: [PATCH 08/11] support CREATE IF NOT EXISTS and DROP IF EXISTS

---

  <refsynopsisdiv>
 <synopsis>
 
   <variablelist>

  <refsynopsisdiv>
 <synopsis>

 
-	if (!svar)

 

 
 
 	if (found)
-		ereport(ERROR,

 

 

 
 
 					n->name = $4;
 					n->typeName = $6;

 
 					n->name = $3;
+					n->missing_ok = false;
+					$$ = (Node *) n;
+				}

 
 		case T_LetStmt:

index f4d6e604c96..c34717d6e19 100644

 
 -- should be ok

From e7e3d115aa7904c1ea9614f5421e183bda68a00a Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Sat, 6 Dec 2025 07:35:30 +0100
Subject: [PATCH 10/11] transactional DDL - CREATE VARIABLE, DROP VARIABLE

 
 
 	int16		typlen;
 	bool		typbyval;
+

 
+/*

 	if (found)
 	{

 	svar->value = (Datum) 0;
 	svar->isnull = true;
+
 
 /*

 
 /*
 
 /*

 /*

 

From 657d36821303c411825aaa2c9b513902d3d0e208 Mon Sep 17 00:00:00 2001

index 0055eb84a78..91607a72188 100644
@@ -5960,10 +5960,32 @@ SELECT ... FROM GRAPH_TABLE (myshop MATCH (IS person WHERE name = '...')-[]->...
 
  <sect1 id="ddl-others">

 

 
 /*
index b0933311d6a..ddc4bddac98 100644
@@ -376,6 +376,7 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
 
 	/*

 
+		case T_LetStmt:
 
 			/*

 
 
 	/*
 
 /*
 
+/*
+ * transformLetStmt -

 

 			/*
 
 			/*
 
 			/*

 			/*
 
 			/*
 
 			/*
 
 		case T_CallStmt:
 		case T_DoStmt:
+		case T_LetStmt:
 			{
 				/*
 
+		case T_LetStmt:
 
+		case T_LetStmt:
 
+		case T_LetStmt:
+			tag = CMDTAG_LET;
+			break;
+
 
 		case T_PLAssignStmt:
+		case T_LetStmt:
 			lev = LOGSTMT_ALL;
 			break;
 
index 214beb0d950..f48de154a78 100644
 
+/* LET */

 
index 05d5d3c50a2..f4d6e604c96 100644

index 2a2669f518e..0d191d7f9b6 100644

 
index 24f2c105918..d23e4cb4b09 100644
@@ -1594,6 +1594,7 @@ LargeObjectDesc

From a74c66622906ab01d9e127fc9b1ce42d15309c8a Mon Sep 17 00:00:00 2001

---

 /*
index f48de154a78..4acb08302d6 100644

+	/* DROP VARIABLE */
 
 /* LET */

+/*
+ * VARIABLE fence
+ */
 
 	/*
index 3579cec5744..d75217c19c7 100644

 DROP VARIABLE x;

From dff4092b3dd8d77b371453d5407308a3c5527e1d Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Mon, 8 Dec 2025 05:00:12 +0100
Subject: [PATCH 11/11] subtransaction support for session variables DDL
 (CREATE, DROP)

 

 

 
+/*

 #endif
 
+BEGIN;
+SAVEPOINT s1;
+DROP VARIABLE x;
 
+BEGIN;
+SAVEPOINT s1;
+DROP VARIABLE x;

---

From: Pavel Stehule <[email-masked]>
Date: Fri, 3 Apr 2026 07:15:41 +0200

Hi

fresh rebase

Regards

Pavel
From 28a29a6da8fc9739f85f94fc5d67e6dde43f5965 Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Fri, 21 Nov 2025 20:42:56 +0100
Subject: [PATCH 03/11] collect session variables used in plan and assign
 paramid

* parallel execution

plan
---

 
+		/*
 
+/*
index 4ec76ce31a9..22f8008d238 100644
 
 	/*

+	/*

+/*

 
 
 				/*

 	return false;
 }
 
index 693b879f76d..32c98bc7c1e 100644

 	/*
index 14a1dfed2b9..b33cb92a2ed 100644

From 8482436b7a4a03a99c9e953a3b362b893fce6efe Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Fri, 21 Nov 2025 15:28:59 +0100
Subject: [PATCH 02/11] parsing session variable fences

 
  <sect1 id="ddl-others">
 
+	/*

+/*
index c0b880ec233..0f6990f1b84 100644
@@ -4808,6 +4811,9 @@ raw_expression_tree_walker_impl(Node *node,
index 84deed9aaa6..a2b9ce66e12 100644
@@ -635,6 +635,7 @@ transformDeleteStmt(ParseState *pstate, DeleteStmt *stmt)

@@ -1074,6 +1075,7 @@ transformInsertStmt(ParseState *pstate, InsertStmt *stmt)

 
@@ -1866,6 +1868,7 @@ transformSelectStmt(ParseState *pstate, SelectStmt *stmt,
 
 	foreach(l, stmt->lockingClause)
 	{
@@ -2092,6 +2095,7 @@ transformValuesClause(ParseState *pstate, SelectStmt *stmt)
 	qry->jointree = makeFromExpr(pstate->p_joinlist, NULL);

 
@@ -2343,6 +2347,7 @@ transformSetOperationStmt(ParseState *pstate, SelectStmt *stmt)
 
@@ -2835,6 +2840,7 @@ transformReturnStmt(ParseState *pstate, ReturnStmt *stmt)

@@ -2909,6 +2915,7 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)

 
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 1afc77bf5a7..f3a4aeef3fe 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -535,7 +535,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
@@ -929,7 +929,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 TO USING VALUE_P WITH WITHOUT PATH
+			SET KEYS OBJECT_P SCALAR TO USING VALUE_P WITH WITHOUT PATH VARIABLE
 %left		Op OPERATOR RIGHT_ARROW '|'	/* multi-character ops and user-defined operators */
 %left		'+' '-'
 %left		'*' '/' '%'
@@ -16490,6 +16490,8 @@ c_expr:		columnref								{ $$ = $1; }
 					else
 						$$ = $2;
 				}
+			| variable_fence
+				{ $$ = $1; }
 			| case_expr
 				{ $$ = $1; }
 			| func_expr
@@ -17894,6 +17896,17 @@ case_arg:	a_expr									{ $$ = $1; }
 			| /*EMPTY*/								{ $$ = NULL; }
 		;
 
+variable_fence:
+			VARIABLE '(' ColId ')'
+				{
index f535f3b9351..20d4b7643fd 100644
 
+		case T_VariableFence:
+			result = transformVariableFence(pstate, (VariableFence *) expr);
+			break;
+
 		default:
 			/* should not reach here */
 			elog(ERROR, "unrecognized node type: %d", (int) nodeTag(expr));
@@ -920,6 +926,120 @@ transformParamRef(ParseState *pstate, ParamRef *pref)
 	return result;
 }
 
+/*
index 2e6dd166c98..84c234947dd 100644

 
index aec5556b008..0ac09a57e22 100644
@@ -9368,6 +9368,14 @@ get_parameter(Param *param, deparse_context *context)
 		}
 	}

index 379c740b944..97f63a75939 100644
@@ -170,6 +170,8 @@ typedef struct Query
 
@@ -325,6 +327,16 @@ typedef struct ParamRef
 	ParseLoc	location;		/* token location, or -1 if unknown */
 } ParamRef;
 
+/*
index 6dfc946c20b..0c762e492e9 100644
 
 typedef struct Param
index f7f4ba6c2a8..00d77a1abc4 100644
@@ -248,6 +248,7 @@ struct ParseState

 
 	/*
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 4716ef8f3be..6f889e18a6d 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -3314,6 +3314,7 @@ ValidatorValidateCB
 ValuesScan
 ValuesScanState
 Var
+VariableFence
 VarBit
 VarChar
 VarParamState
-- 
2.53.0

From 27ee61a899fe15ccfa0629ded0ddc8001781bcd0 Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Sun, 1 Jun 2025 21:20:16 +0200
Subject: [PATCH 05/11] svariableReceiver

 
+/*

 

 
 
 /* ----------------
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 04f1bd17dcc..22198dfafef 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2747,6 +2747,7 @@ STRLEN
 SV
 SVariableData
 SVariable
+SVariableState
 SYNCHRONIZATION_BARRIER
 SYSTEM_INFO
 SampleScan
-- 
2.53.0

From 181a30877bb66b956c84be7729741e161cdb2c1a Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Sat, 22 Nov 2025 06:40:46 +0100
Subject: [PATCH 04/11] fill an auxiliary buffer with values of session 
 variables used in query

index 45e00c6af85..6c8e434bf37 100644
 
+	/*
index 090cfccf65f..1723594e016 100644
@@ -679,6 +679,16 @@ typedef struct AsyncRequest
 								 * tuples) */
 } AsyncRequest;
 
+/* ----------------
+ * SessionVariableValue
+ * ----------------
+ */
+typedef struct SessionVariableValue
+{
+	bool		isnull;
+	Datum		value;
+} SessionVariableValue;
+
 /* ----------------
  *	  EState information
  *
@@ -738,6 +748,10 @@ typedef struct EState
 	ParamListInfo es_param_list_info;	/* values of external params */
 	ParamExecData *es_param_exec_vals;	/* values of internal params */

index ec92c717bf6..b7c0ad00149 100644
index 6f889e18a6d..04f1bd17dcc 100644
@@ -2805,6 +2805,7 @@ SerializedTransactionState
 Session
 SessionBackupState
 SessionEndType
+SessionVariableValue
 SetConstraintState
 SetConstraintStateData
 SetConstraintTriggerData
-- 
2.53.0

From 76ce9fa65876e3b859364a9bab74e3aa505801ae Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Wed, 19 Nov 2025 19:36:07 +0100
Subject: [PATCH 01/11] CREATE VARIABLE, DROP VARIABLE

 
 
diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml
index 113d7640626..b6b80d446eb 100644
--- a/doc/src/sgml/glossary.sgml
+++ b/doc/src/sgml/glossary.sgml
@@ -1743,6 +1743,21 @@
    </glossdef>
   </glossentry>
 
index f4a08baa95a..1afc77bf5a7 100644
@@ -828,8 +828,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);

 
@@ -1099,6 +1099,7 @@ stmt:
 			| CreatePLangStmt
 			| CreatePropGraphStmt
 			| CreateSchemaStmt
+			| CreateSessionVarStmt
 			| CreateSeqStmt
 			| CreateStmt
 			| CreateSubscriptionStmt
@@ -1126,6 +1127,7 @@ stmt:
 			| DropTableSpaceStmt
 			| DropTransformStmt
 			| DropRoleStmt
+			| DropSessionVarStmt
 			| DropUserMappingStmt
 			| DropdbStmt
 			| ExecuteStmt
@@ -5442,6 +5444,47 @@ create_extension_opt_item:
 				}
 		;
 
@@ -19146,6 +19189,7 @@ unreserved_keyword:
@@ -19818,6 +19862,7 @@ bare_label_keyword:

+
 				/*

index 53bf1e21721..fe012829ac0 100644
 
@@ -3852,7 +3853,7 @@ match_previous_words(int pattern_id,
@@ -4214,6 +4215,13 @@ match_previous_words(int pattern_id,
 			COMPLETE_WITH(",", ")");
 	}
 
+/* CREATE VARIABLE */
index 91377a6cde3..379c740b944 100644
@@ -3664,6 +3664,29 @@ typedef struct AlterStatsStmt
 
+
index 51ead54f015..491347970a2 100644
@@ -501,6 +501,7 @@ PG_KEYWORD("validator", VALIDATOR, UNRESERVED_KEYWORD, BARE_LABEL)
index 3a044ffd8bf..ec92c717bf6 100644
 
 # ----------
index 5bc517602b1..4716ef8f3be 100644
@@ -2743,6 +2745,8 @@ SSL_CTX
 STARTUPINFO
 STRLEN
 SV
+SVariableData
+SVariable
 SYNCHRONIZATION_BARRIER
 SYSTEM_INFO
 SampleScan
-- 
2.53.0

From 7a0c4b8b0a39659f8918eaaa7fe6d811c9bb1f58 Mon Sep 17 00:00:00 2001

 
  <sect1 id="ddl-others">

 

 
 /*
index 0f6990f1b84..9efca476866 100644
@@ -4449,6 +4449,14 @@ raw_expression_tree_walker_impl(Node *node,
index 22f8008d238..5f04603d026 100644
 
 	/*
 	 * Assess whether it's feasible to use parallel mode for this query. We
diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
index a2b9ce66e12..409d3ee411b 100644
--- a/src/backend/parser/analyze.c
+++ b/src/backend/parser/analyze.c
@@ -59,15 +59,18 @@
 #include "utils/lsyscache.h"
 #include "utils/rangetypes.h"
 #include "utils/rel.h"
+#include "utils/lsyscache.h"
 #include "utils/syscache.h"

 /* Hook for plugins to get control at end of parse analysis */
@@ -95,7 +98,7 @@ static Query *transformReturnStmt(ParseState *pstate, ReturnStmt *stmt);
 static Query *transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt);
 static Query *transformPLAssignStmt(ParseState *pstate,
 									PLAssignStmt *stmt);
-static List *transformPLAssignStmtTarget(ParseState *pstate, List *tlist,
+static List *transformAssignTarget(ParseState *pstate, List *tlist,
 										 SelectStmtPassthrough *passthru);
 static Query *transformDeclareCursorStmt(ParseState *pstate,
 										 DeclareCursorStmt *stmt);
@@ -105,6 +108,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
@@ -352,6 +357,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:
@@ -431,6 +437,11 @@ transformStmt(ParseState *pstate, Node *parseTree)
 									   (CallStmt *) parseTree);
 			break;
 
+		case T_LetStmt:
 
 			/*
@@ -492,6 +503,7 @@ stmt_requires_parse_analysis(RawStmt *parseTree)
 		case T_ExplainStmt:
 		case T_CreateTableAsStmt:
 		case T_CallStmt:
+		case T_LetStmt:
 			result = true;
 			break;
 
@@ -557,6 +569,7 @@ query_requires_rewrite_plan(Query *query)
 			case T_ExplainStmt:
@@ -1729,7 +1742,7 @@ count_rowexpr_columns(ParseState *pstate, Node *expr)
@@ -1782,8 +1795,8 @@ transformSelectStmt(ParseState *pstate, SelectStmt *stmt,
 
@@ -3221,9 +3234,11 @@ transformPLAssignStmt(ParseState *pstate, PLAssignStmt *stmt)
 						   EXPR_KIND_UPDATE_TARGET);

 	/*
 	 * To avoid duplicating a lot of code, we use transformSelectStmt to do
@@ -3246,18 +3261,21 @@ transformPLAssignStmt(ParseState *pstate, PLAssignStmt *stmt)
 
 /*
@@ -3292,7 +3310,7 @@ transformPLAssignStmtTarget(ParseState *pstate, List *tlist,
@@ -3300,10 +3318,10 @@ transformPLAssignStmtTarget(ParseState *pstate, List *tlist,
@@ -3326,7 +3344,7 @@ transformPLAssignStmtTarget(ParseState *pstate, List *tlist,
@@ -3335,7 +3353,7 @@ transformPLAssignStmtTarget(ParseState *pstate, List *tlist,
@@ -3703,6 +3721,59 @@ transformCallStmt(ParseState *pstate, CallStmt *stmt)
 	return result;
 }
 
+/*
+ * transformLetStmt -
index f3a4aeef3fe..90821c62087 100644
@@ -786,7 +786,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	KEEP KEY KEYS

 	MAPPING MATCH MATCHED MATERIALIZED MAXVALUE MERGE MERGE_ACTION METHOD
@@ -1138,6 +1138,7 @@ stmt:
 			| ImportForeignSchemaStmt
 			| IndexStmt
 			| InsertStmt
+			| LetStmt
 			| ListenStmt
 			| RefreshMatViewStmt
 			| LoadStmt
@@ -13621,6 +13622,37 @@ opt_hold: /* EMPTY */						{ $$ = 0; }
 			| WITHOUT HOLD					{ $$ = 0; }
 		;
 
@@ -19021,6 +19053,7 @@ unreserved_keyword:
@@ -19651,6 +19684,7 @@ bare_label_keyword:
index acb933392de..92fb1ef43a1 100644
@@ -597,7 +597,10 @@ check_agglevels_and_constraints(ParseState *pstate, Node *expr)

 			/*
@@ -1045,6 +1048,9 @@ transformWindowFuncCall(ParseState *pstate, WindowFunc *wfunc,
 		case EXPR_KIND_FOR_PORTION:
 			err = _("window functions are not allowed in FOR PORTION OF expressions");
 			break;
+		case EXPR_KIND_LET_TARGET:
+			errkind = true;
+			break;
 
 			/*
 			 * There is intentionally no default: case here, so that the
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index 20d4b7643fd..e29dfbadf6a 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -597,6 +597,9 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
 		case EXPR_KIND_FOR_PORTION:
 			err = _("cannot use column reference in FOR PORTION OF expression");
 			break;
+		case EXPR_KIND_LET_TARGET:
+			err = _("cannot use column reference as target of LET command");
+			break;
 
 			/*
@@ -978,6 +981,7 @@ expr_kind_allows_session_variables(ParseExprKind p_expr_kind)
 
@@ -2012,6 +2016,9 @@ transformSubLink(ParseState *pstate, SubLink *sublink)
 		case EXPR_KIND_FOR_PORTION:
 			err = _("cannot use subquery in FOR PORTION OF expression");
 			break;
+		case EXPR_KIND_LET_TARGET:
+			err = _("cannot use subquery as a target of LET command");
+			break;
 
 			/*
 			 * There is intentionally no default: case here, so that the
@@ -3375,6 +3382,8 @@ ParseExprKindName(ParseExprKind exprKind)
 			return "property definition expression";
 		case EXPR_KIND_FOR_PORTION:
 			return "FOR PORTION OF";
+		case EXPR_KIND_LET_TARGET:
+			return "LET";
 
 			/*
 			 * There is intentionally no default: case here, so that the
diff --git a/src/backend/parser/parse_func.c b/src/backend/parser/parse_func.c
index 35ff6427147..8ddb5a4e219 100644
--- a/src/backend/parser/parse_func.c
+++ b/src/backend/parser/parse_func.c
@@ -2789,6 +2789,9 @@ check_srf_call_placement(ParseState *pstate, Node *last_srf, int location)
 		case EXPR_KIND_FOR_PORTION:
 			err = _("set-returning functions are not allowed in FOR PORTION OF expressions");
 			break;
+		case EXPR_KIND_LET_TARGET:
+			errkind = true;
+			break;
 
 			/*
 
 		case T_CallStmt:
 		case T_DoStmt:
+		case T_LetStmt:
 			{
 				/*
 
+		case T_LetStmt:
 
+		case T_LetStmt:
 
+		case T_LetStmt:
+			tag = CMDTAG_LET;
+			break;
+
 
 		case T_PLAssignStmt:
+		case T_LetStmt:
 			lev = LOGSTMT_ALL;
 			break;
 
index fe012829ac0..69f348c129e 100644
@@ -4953,6 +4953,11 @@ match_previous_words(int pattern_id,
 
+/* LET */

 
+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 97f63a75939..7d77a368b06 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -150,6 +150,9 @@ typedef struct Query
 	/* FOR PORTION OF clause for UPDATE/DELETE */
 	ForPortionOfExpr *forPortionOf;
 
@@ -2276,6 +2279,18 @@ typedef struct MergeStmt
 
index 32c98bc7c1e..4ba6f709cf2 100644

index 491347970a2..3bc10aab33f 100644
index 00d77a1abc4..7f0d5fbb1b0 100644
@@ -84,6 +84,7 @@ typedef enum ParseExprKind

index 22198dfafef..85bd4262f3c 100644
@@ -1598,6 +1598,7 @@ LargeObjectDesc

From a1e0f57df1c4d838b3303a73cf5c19671ee1ef33 Mon Sep 17 00:00:00 2001

---

 /*
index 69f348c129e..aafd2136b62 100644
 
@@ -4506,6 +4511,10 @@ match_previous_words(int pattern_id,
 
+	/* DROP VARIABLE */
@@ -4955,6 +4964,8 @@ match_previous_words(int pattern_id,
 
 /* LET */
 
@@ -5566,6 +5577,12 @@ match_previous_words(int pattern_id,
 			COMPLETE_WITH("'standby_replay'", "'standby_write'", "'standby_flush'", "'primary_flush'");
 	}
 
+/*
+ * VARIABLE fence
+ */
 
 	/*
index acf16254b21..0b24cde208b 100644
@@ -12860,4 +12860,9 @@

 DROP VARIABLE x;

From b9ad29d7decf130d6ad5993724ca4f5ba5455f70 Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Mon, 24 Nov 2025 20:04:16 +0100
Subject: [PATCH 07/11] DISCARD TEMP

---

 		case DISCARD_TEMP:
 
 		default:
 
 	PopActiveSnapshot();
 }
+
+/*

From ea62e0f30e41e802a1d78cd260d4347f3d9b8797 Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Thu, 4 Dec 2025 06:17:54 +0100
Subject: [PATCH 08/11] support CREATE IF NOT EXISTS and DROP IF EXISTS

---

  <refsynopsisdiv>
 <synopsis>
 
   <variablelist>

  <refsynopsisdiv>
 <synopsis>

 
-	if (!svar)

 

 
 
 	if (found)
-		ereport(ERROR,

 

 

index 90821c62087..4257f1e0274 100644
@@ -5448,7 +5448,7 @@ create_extension_opt_item:
 
@@ -5465,14 +5465,31 @@ CreateSessionVarStmt:
 
 					n->name = $4;
 					n->typeName = $6;

@@ -5482,8 +5499,18 @@ DropSessionVarStmt:
 					DropSessionVarStmt *n = makeNode(DropSessionVarStmt);
 
 					n->name = $3;
+					n->missing_ok = false;
+					$$ = (Node *) n;
+				}

 
 		case T_LetStmt:

index 7d77a368b06..1efd0a22ad5 100644
@@ -3701,6 +3701,7 @@ typedef struct CreateSessionVarStmt
 
@@ -3710,7 +3711,8 @@ typedef struct CreateSessionVarStmt

 -- should be ok

From 9ad73e6247088bf6d32a4cf5a1fa562e72d6b8b9 Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Sat, 6 Dec 2025 07:35:30 +0100
Subject: [PATCH 10/11] transactional DDL - CREATE VARIABLE, DROP VARIABLE

 
 
 	int16		typlen;
 	bool		typbyval;
+

 
+/*

 	if (found)
 	{

 	svar->value = (Datum) 0;
 	svar->isnull = true;
+
 
 /*

 
 /*
 
 /*

 /*

 

From 57c4eb812047468608e03fc505ffa6bc053b9307 Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Mon, 8 Dec 2025 05:00:12 +0100
Subject: [PATCH 11/11] subtransaction support for session variables DDL
 (CREATE, DROP)

 

 

 
+/*

 #endif
 
+BEGIN;
+SAVEPOINT s1;
+DROP VARIABLE x;
 
+BEGIN;
+SAVEPOINT s1;
+DROP VARIABLE x;

---

From: Pavel Stehule <[email-masked]>
Date: Fri, 3 Apr 2026 21:10:13 +0200

Hi

fix warning reported by cfbot

Regards

Pavel

 

 

 

+
 				/*

 
 
+/* CREATE VARIABLE */
 
+
 
 # ----------

From 5e27c590f3545d10d715a377456e8abe4539633f Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Fri, 21 Nov 2025 15:28:59 +0100
Subject: [PATCH 02/11] parsing session variable fences

 src/backend/commands/session_variable.c |  21 ++++
 src/backend/parser/parse_expr.c         | 121 ++++++++++++++++++++++++
 16 files changed, 230 insertions(+), 3 deletions(-)

  <sect1 id="ddl-others">
 
+	/*

+/*

 

 

 

 

 
 
+variable_fence:
+			VARIABLE '(' ColId ')'
+				{
index f535f3b9351..4b995ab4a95 100644
 
+		case T_VariableFence:
+			result = transformVariableFence(pstate, (VariableFence *) expr);
+			break;
+
 		default:
 			/* should not reach here */
 			elog(ERROR, "unrecognized node type: %d", (int) nodeTag(expr));
@@ -920,6 +926,121 @@ transformParamRef(ParseState *pstate, ParamRef *pref)
 	return result;
 }
 
+/*
+		case EXPR_KIND_FOR_PORTION:

 

 
 
+/*
 
 typedef struct Param

 
 	/*

From 132459dad1e46296fb32f8baff27dbb4fae77d4f Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Sat, 22 Nov 2025 06:40:46 +0100
Subject: [PATCH 04/11] fill an auxiliary buffer with values of session 
 variables used in query

 
+	/*
 
+/* ----------------
+ * SessionVariableValue
+ * ----------------
+ */

From 654e7eb78cb07434aebfb48096a66b22e3c46094 Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Sun, 1 Jun 2025 21:20:16 +0200
Subject: [PATCH 05/11] svariableReceiver

 
+/*

 

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

From 04c34377c252050398ea77f8bd964d4102b7493e Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Fri, 21 Nov 2025 20:42:56 +0100
Subject: [PATCH 03/11] collect session variables used in plan and assign
 paramid

* parallel execution

plan
---

 
+		/*
 
+/*
 
 	/*

+	/*

+/*

 
 
 				/*

 	return false;
 }

 
 	/*

From b689e47f38aa6c5eae62423532e0a63e2afb88c3 Mon Sep 17 00:00:00 2001

 
  <sect1 id="ddl-others">

 

 
 /*
 
 	/*

 
+		case T_LetStmt:
 
 			/*

 
 
 	/*
 
 /*
 
+/*
+ * transformLetStmt -

 

 			/*
 
 			/*
index 4b995ab4a95..d9116c3bfd5 100644
 
 			/*
@@ -979,6 +982,7 @@ expr_kind_allows_session_variables(ParseExprKind p_expr_kind)
 
@@ -2013,6 +2017,9 @@ transformSubLink(ParseState *pstate, SubLink *sublink)
 
 			/*
@@ -3376,6 +3383,8 @@ ParseExprKindName(ParseExprKind exprKind)
 
 			/*
 
 			/*
 
 		case T_CallStmt:
 		case T_DoStmt:
+		case T_LetStmt:
 			{
 				/*
 
+		case T_LetStmt:
 
+		case T_LetStmt:
 
+		case T_LetStmt:
+			tag = CMDTAG_LET;
+			break;
+
 
 		case T_PLAssignStmt:
+		case T_LetStmt:
 			lev = LOGSTMT_ALL;
 			break;

+/* LET */

 

 

 

From efc1b0b978781eebcdf92d1d1a88119c8aa49c0b Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Mon, 24 Nov 2025 20:04:16 +0100
Subject: [PATCH 07/11] DISCARD TEMP

---

 		case DISCARD_TEMP:
 
 		default:
 
 	PopActiveSnapshot();
 }
+
+/*

From aff7b8f4cd8e18791fb4140ddb8c93aa18f5b720 Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Thu, 4 Dec 2025 06:17:54 +0100
Subject: [PATCH 08/11] support CREATE IF NOT EXISTS and DROP IF EXISTS

---

  <refsynopsisdiv>
 <synopsis>
 
   <variablelist>

  <refsynopsisdiv>
 <synopsis>

 
-	if (!svar)

 

 
 
 	if (found)
-		ereport(ERROR,

 

 

 
 
 					n->name = $4;
 					n->typeName = $6;

 
 					n->name = $3;
+					n->missing_ok = false;
+					$$ = (Node *) n;
+				}

 
 		case T_LetStmt:

 

 -- should be ok

From 56630160904ab829f5e1fb241c521dbf59fd21c1 Mon Sep 17 00:00:00 2001

---

 /*

+	/* DROP VARIABLE */
 
 /* LET */

+/*
+ * VARIABLE fence
+ */
 
 	/*

 DROP VARIABLE x;

From 881825490a83a20cb884dbf81490ddb87bb33102 Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Sat, 6 Dec 2025 07:35:30 +0100
Subject: [PATCH 10/11] transactional DDL - CREATE VARIABLE, DROP VARIABLE

 
 
 	int16		typlen;
 	bool		typbyval;
+

 
+/*

 	if (found)
 	{

 	svar->value = (Datum) 0;
 	svar->isnull = true;
+
 
 /*

 
 /*
 
 /*

 /*

 

From a6340f59478f40cce91a1153da03bf7cfb45aea9 Mon Sep 17 00:00:00 2001
From: "[email-masked]" <[email-masked]>
Date: Mon, 8 Dec 2025 05:00:12 +0100
Subject: [PATCH 11/11] subtransaction support for session variables DDL
 (CREATE, DROP)

 

 

 
+/*

 #endif
 
+BEGIN;
+SAVEPOINT s1;
+DROP VARIABLE x;
 
+BEGIN;
+SAVEPOINT s1;
+DROP VARIABLE x;