From ccb684e1d074a41b04172800ffdf27224ada0659 Mon Sep 17 00:00:00 2001
From: "okbob@github.com" <pavel.stehule@gmail.com>
Date: Tue, 21 May 2024 22:40:55 +0200
Subject: [PATCH 19/20] transactional variables

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

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

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

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

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 4512c4525bf..9e0420f7cf2 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -9851,6 +9851,18 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       </para></entry>
      </row>
 
+     <row>
+      <entry><structfield>varistransact</structfield></entry>
+      <entry><type>boolean</type></entry>
+      <entry></entry>
+      <entry>
+       True, when the variable is <quote>transactional</quote>. In the case
+       of a transaction rollback, transactional variables are reset to the
+       value they had when the transaction started. The default value is
+       <literal>false</literal>.
+      </entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>vareoxaction</structfield> <type>char</type>
diff --git a/doc/src/sgml/ref/create_variable.sgml b/doc/src/sgml/ref/create_variable.sgml
index 00938743c0d..b73f032ddbb 100644
--- a/doc/src/sgml/ref/create_variable.sgml
+++ b/doc/src/sgml/ref/create_variable.sgml
@@ -26,7 +26,7 @@ PostgreSQL documentation
 
  <refsynopsisdiv>
 <synopsis>
-CREATE [ { TEMPORARY | TEMP } ] [ IMMUTABLE ] VARIABLE [ IF NOT EXISTS ] <replaceable class="parameter">name</replaceable> [ AS ] <replaceable class="parameter">data_type</replaceable> ] [ COLLATE <replaceable class="parameter">collation</replaceable> ]
+CREATE [ { TEMPORARY | TEMP } ] [ { TRANSACTIONAL | TRANSACTION } ] [ IMMUTABLE ] VARIABLE [ IF NOT EXISTS ] <replaceable class="parameter">name</replaceable> [ AS ] <replaceable class="parameter">data_type</replaceable> ] [ COLLATE <replaceable class="parameter">collation</replaceable> ]
     [ NOT NULL ] [ DEFAULT <replaceable class="parameter">default_expr</replaceable> ] [ { ON COMMIT DROP | ON TRANSACTION END RESET } ]
 </synopsis>
  </refsynopsisdiv>
@@ -47,6 +47,14 @@ CREATE [ { TEMPORARY | TEMP } ] [ IMMUTABLE ] VARIABLE [ IF NOT EXISTS ] <replac
    same as regular variables in procedural languages.
   </para>
 
+  <para>
+   When a schema variable is created with a
+   <command>CREATE TRANSACTIONAL VARIABLE</command> command, the variables
+   content changes are transactional: in case of rollback, they are reset to
+   their value at the beginning of the transaction or the latest subtransaction.
+   The variable content is only hold in memory, and thus is not persistent.
+  </para>
+
   <para>
    Session variables are retrieved by the <command>SELECT</command>
    command.  Their value is set with the <command>LET</command> command.
diff --git a/src/backend/catalog/pg_variable.c b/src/backend/catalog/pg_variable.c
index a89c0eccd6a..b99c6dc1e48 100644
--- a/src/backend/catalog/pg_variable.c
+++ b/src/backend/catalog/pg_variable.c
@@ -42,6 +42,7 @@ static ObjectAddress create_variable(const char *varName,
 									 bool if_not_exists,
 									 bool not_null,
 									 bool is_immutable,
+									 bool is_transact,
 									 Node *varDefexpr,
 									 VariableXactEndAction varXactEndAction);
 
@@ -59,6 +60,7 @@ create_variable(const char *varName,
 				bool if_not_exists,
 				bool not_null,
 				bool is_immutable,
+				bool is_transact,
 				Node *varDefexpr,
 				VariableXactEndAction varXactEndAction)
 {
@@ -123,6 +125,7 @@ create_variable(const char *varName,
 	values[Anum_pg_variable_varcollation - 1] = ObjectIdGetDatum(varCollation);
 	values[Anum_pg_variable_varnotnull - 1] = BoolGetDatum(not_null);
 	values[Anum_pg_variable_varisimmutable - 1] = BoolGetDatum(is_immutable);
+	values[Anum_pg_variable_varistransact - 1] = BoolGetDatum(is_transact);
 	values[Anum_pg_variable_varxactendaction - 1] = CharGetDatum(varXactEndAction);
 
 	if (varDefexpr)
@@ -266,6 +269,7 @@ CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt)
 							   stmt->if_not_exists,
 							   stmt->not_null,
 							   stmt->is_immutable,
+							   stmt->is_transact,
 							   cooked_default,
 							   stmt->XactEndAction);
 
diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c
index fa5adf631b8..4b62c01f149 100644
--- a/src/backend/commands/session_variable.c
+++ b/src/backend/commands/session_variable.c
@@ -47,6 +47,19 @@ typedef struct SVariableXActDropItem
 	SubTransactionId deleting_subid;
 } SVariableXActDropItem;
 
+/*
+ * Used for transactional variables. Holds prev version.
+ */
+typedef struct PrevValue
+{
+	Datum		value;
+	bool		isnull;
+
+	SubTransactionId modify_subid;
+
+	struct PrevValue *prev_value;
+} PrevValue;
+
 /*
  * The values of session variables are stored in the backend's private memory
  * in the dedicated memory context SVariableMemoryContext in binary format.
@@ -68,6 +81,25 @@ typedef struct SVariableData
 	bool		isnull;
 	Datum		value;
 
+	/*
+	 * We don't need stack versions modified in same subtransaction.
+	 * Used by transactional variables only. The value of transactional
+	 * variable can be returned immediately when modify_subid is same
+	 * like current subid.
+	 */
+	SubTransactionId modify_subid;
+
+	/*
+	 * When the modify_subid is different than current subid, then
+	 * we need to recheck versions and throw versions related to
+	 * reverted transactions. When purge_subid is same like current subid
+	 * we can return the value of transaction variable without this
+	 * recheck.
+	 */
+	SubTransactionId purge_subid;
+
+	PrevValue  *prev_value;
+
 	Oid			typid;
 	int16		typlen;
 	bool		typbyval;
@@ -86,6 +118,7 @@ typedef struct SVariableData
 
 	bool		not_null;
 	bool		is_immutable;
+	bool		is_transact;
 
 	bool		reset_at_eox;
 
@@ -125,6 +158,9 @@ static bool needs_validation = false;
  */
 static bool has_session_variables_with_reset_at_eox = false;
 
+/* true, when transactional variables was modified */
+static bool has_modified_transactional_variables = false;
+
 /*
  * The content of session variables is not removed immediately. When it
  * is possible we do this at the transaction end. But when the transaction failed,
@@ -140,6 +176,7 @@ static List *xact_drop_items = NIL;
 
 static void register_session_variable_xact_drop(Oid varid);
 static void unregister_session_variable_xact_drop(Oid varid);
+static bool purge_session_variable(SVariable svar);
 
 /*
  * Callback function for session variable invalidation.
@@ -294,8 +331,25 @@ unregister_session_variable_xact_drop(Oid varid)
  * Release stored value, free memory
  */
 static void
-free_session_variable_value(SVariable svar)
+free_session_variable_value(SVariable svar, bool deep_free)
 {
+	if (deep_free)
+	{
+		PrevValue *prev_value = svar->prev_value;
+		PrevValue *next_value;
+
+		while (prev_value)
+		{
+			if (!svar->typbyval)
+				pfree(DatumGetPointer(prev_value->value));
+
+			next_value = prev_value->prev_value;
+			pfree(prev_value);
+
+			prev_value = next_value;
+		}
+	}
+
 	/* clean the current value */
 	if (!svar->isnull)
 	{
@@ -394,7 +448,7 @@ remove_invalid_session_variables(bool atEOX)
 			{
 				Oid			varid = svar->varid;
 
-				free_session_variable_value(svar);
+				free_session_variable_value(svar, true);
 				hash_search(sessionvars, &varid, HASH_REMOVE, NULL);
 				svar = NULL;
 			}
@@ -430,6 +484,94 @@ remove_session_variables_with_reset_at_eox(void)
 	has_session_variables_with_reset_at_eox = false;
 }
 
+/*
+ * remove prev values at eox
+ */
+static void
+remove_prev_values_at_eox(bool isCommit)
+{
+	HASH_SEQ_STATUS status;
+	SVariable	svar;
+
+	if (!sessionvars)
+		return;
+
+	/* leave quckly, when there are not that variables */
+	if (!has_modified_transactional_variables)
+		return;
+
+	hash_seq_init(&status, sessionvars);
+	while ((svar = (SVariable) hash_seq_search(&status)) != NULL)
+	{
+		if (svar->is_transact && svar->modify_subid != InvalidSubTransactionId)
+		{
+			if (isCommit)
+			{
+				if (purge_session_variable(svar))
+				{
+					PrevValue *prev_value = svar->prev_value;
+
+					while (prev_value)
+					{
+						PrevValue *current_pv = prev_value;
+
+						if (!svar->typbyval && !current_pv->isnull)
+							pfree(DatumGetPointer(current_pv->value));
+
+						prev_value = current_pv->prev_value;
+						pfree(current_pv);
+					}
+				}
+				else
+				{
+					hash_search(sessionvars, &svar->varid, HASH_REMOVE, NULL);
+					svar = NULL;
+				}
+			}
+			else
+			{
+				PrevValue *prev_value = svar->prev_value;
+
+				while (prev_value)
+				{
+					PrevValue *current_pv = prev_value;
+
+					if (current_pv->modify_subid == InvalidSubTransactionId)
+						break;
+
+					if (!svar->typbyval && !current_pv->isnull)
+						pfree(DatumGetPointer(current_pv->value));
+
+					prev_value = current_pv->prev_value;
+					pfree(current_pv);
+				}
+
+				if (prev_value)
+				{
+					svar->value = prev_value->value;
+					svar->isnull = prev_value->isnull;
+
+					pfree(prev_value);
+				}
+				else
+				{
+					hash_search(sessionvars, &svar->varid, HASH_REMOVE, NULL);
+					svar = NULL;
+				}
+			}
+
+			/* When svar is still valid (not removed from sessionvars */
+			if (svar)
+			{
+				svar->modify_subid = InvalidSubTransactionId;
+				svar->prev_value = NULL;
+			}
+		}
+	}
+
+	has_modified_transactional_variables = false;
+}
+
 /*
   * Perform ON COMMIT DROP for temporary session variables,
   * and remove all dropped variables from memory.
@@ -477,6 +619,8 @@ AtPreEOXact_SessionVariables(bool isCommit)
 		remove_invalid_session_variables(true);
 	}
 
+	remove_prev_values_at_eox(isCommit);
+
 	/*
 	 * We have to clean xact_drop_items. All related variables are dropped
 	 * now, or lost inside aborted transaction.
@@ -622,6 +766,7 @@ setup_session_variable(SVariable svar, Oid varid, bool is_write)
 
 	svar->not_null = varform->varnotnull;
 	svar->is_immutable = varform->varisimmutable;
+	svar->is_transact = varform->varistransact;
 
 	svar->is_domain = (get_typtype(varform->vartype) == TYPTYPE_DOMAIN);
 	svar->domain_check_extra = NULL;
@@ -647,6 +792,17 @@ setup_session_variable(SVariable svar, Oid varid, bool is_write)
 	svar->isnull = true;
 	svar->value = (Datum) 0;
 
+	if (svar->is_transact)
+	{
+		svar->modify_subid = GetCurrentSubTransactionId();
+		has_modified_transactional_variables = true;
+	}
+	else
+		svar->modify_subid = InvalidSubTransactionId;
+
+	svar->purge_subid = InvalidSubTransactionId;
+	svar->prev_value = NULL;
+
 	svar->is_valid = true;
 
 	svar->hashvalue = GetSysCacheHashValue1(VARIABLEOID,
@@ -680,6 +836,69 @@ setup_session_variable(SVariable svar, Oid varid, bool is_write)
 	ReleaseSysCache(tup);
 }
 
+/*
+ * Try to remove all previous versions related to reverted transactions.
+ * Returns true, when valid version was found.
+ */
+static bool
+purge_session_variable(SVariable svar)
+{
+	SubTransactionId current_subid;
+	PrevValue	   *prev_value;
+	bool			found = true;
+
+	Assert(svar->is_transact);
+
+	if (svar->modify_subid == InvalidSubTransactionId)
+		return true;
+
+	current_subid = GetCurrentSubTransactionId();
+
+	if (svar->modify_subid == current_subid)
+		return true;
+
+	if (svar->purge_subid == current_subid)
+		return true;
+
+	if (SubTransactionIsActive(svar->modify_subid))
+	{
+		svar->purge_subid = current_subid;
+		return true;
+	}
+
+	prev_value = svar->prev_value;
+
+	while (prev_value)
+	{
+		PrevValue *current_pv = prev_value;
+
+		if (current_pv->modify_subid == InvalidSubTransactionId ||
+			SubTransactionIsActive(current_pv->modify_subid))
+		{
+			svar->value = current_pv->value;
+			svar->isnull = current_pv->isnull;
+			svar->modify_subid = current_pv->modify_subid;
+
+			prev_value = current_pv->prev_value;
+			pfree(current_pv);
+
+			found = true;
+			break;
+		}
+
+		if (!svar->typbyval && !current_pv->isnull)
+			pfree(DatumGetPointer(current_pv->value));
+
+		prev_value = current_pv->prev_value;
+		pfree(current_pv);
+	}
+
+	svar->prev_value = prev_value;
+	svar->purge_subid = current_subid;
+
+	return found;
+}
+
 /*
  * Assign a new value to the session variable.  It is copied to
  * SVariableMemoryContext if necessary.
@@ -692,6 +911,10 @@ set_session_variable(SVariable svar, Datum value, bool isnull)
 	Datum		newval;
 	SVariableData locsvar,
 			   *_svar;
+	SubTransactionId current_subid = GetCurrentSubTransactionId();
+	SubTransactionId prev_purge_subid = InvalidSubTransactionId;
+	bool		save_prev_value;
+	PrevValue  *prev_value;
 
 	Assert(svar);
 	Assert(!isnull || value == (Datum) 0);
@@ -727,6 +950,21 @@ set_session_variable(SVariable svar, Datum value, bool isnull)
 	else
 		_svar = svar;
 
+	if (_svar->is_transact && _svar->create_lsn == svar->create_lsn)
+	{
+		Assert(svar->typid == _svar->typid);
+		Assert(svar->typbyval == _svar->typbyval);
+		Assert(svar->typlen == _svar->typlen);
+
+		save_prev_value = svar->modify_subid != current_subid;
+		prev_value = svar->prev_value;
+	}
+	else
+	{
+		save_prev_value = false;
+		prev_value = NULL;
+	}
+
 	if (!isnull)
 	{
 		MemoryContext oldcxt = MemoryContextSwitchTo(SVariableMemoryContext);
@@ -738,7 +976,37 @@ set_session_variable(SVariable svar, Datum value, bool isnull)
 	else
 		newval = value;
 
-	free_session_variable_value(svar);
+	if (save_prev_value)
+	{
+		volatile PrevValue *new_prev_value;
+
+		PG_TRY();
+		{
+			new_prev_value = MemoryContextAlloc(SVariableMemoryContext,
+												sizeof(PrevValue));
+		}
+		PG_CATCH();
+		{
+			/* release mem from persistent content */
+			if (newval != value)
+				pfree(DatumGetPointer(newval));
+			PG_RE_THROW();
+		}
+		PG_END_TRY();
+
+		new_prev_value->value = svar->value;
+		new_prev_value->isnull = svar->isnull;
+		new_prev_value->modify_subid = svar->modify_subid;
+		new_prev_value->prev_value = prev_value;
+
+		prev_value = (PrevValue *) new_prev_value;
+		prev_purge_subid = svar->purge_subid;
+
+		has_modified_transactional_variables = true;
+
+	}
+	else
+		free_session_variable_value(svar, prev_value == NULL);
 
 	elog(DEBUG1, "session variable \"%s.%s\" (oid:%u) has new value",
 		 get_namespace_name(get_session_variable_namespace(svar->varid)),
@@ -751,6 +1019,9 @@ set_session_variable(SVariable svar, Datum value, bool isnull)
 
 	svar->value = newval;
 	svar->isnull = isnull;
+	svar->modify_subid = current_subid;
+	svar->purge_subid = prev_purge_subid;
+	svar->prev_value = prev_value;
 
 	/* don't allow more changes of value when variable is IMMUTABLE */
 	if (svar->is_immutable)
@@ -854,6 +1125,8 @@ get_session_variable(Oid varid)
 	else
 		svar->is_valid = false;
 
+reinit:
+
 	/*
 	 * Force setup for not yet initialized variables or variables that cannot
 	 * be validated.
@@ -886,6 +1159,28 @@ get_session_variable(Oid varid)
 			 varid);
 	}
 
+	/*
+	 * Transactional variables should be purged before (remove
+	 * versions created by possibly reverted subtransactions).
+	 */
+	if (svar->is_transact &&
+		svar->modify_subid != GetCurrentSubTransactionId() &&
+		svar->modify_subid != InvalidSubTransactionId)
+	{
+		if (!purge_session_variable(svar))
+		{
+			/* force reinit */
+			svar->is_valid = false;
+
+			/*
+			 * In next iteration modify_subid should be
+			 * InvalidSubTransactionId or current subid,
+			 * so there is not risk of infinity cycle.
+			 */
+			goto reinit;
+		}
+	}
+
 	/* ensure the returned data is still of the correct domain */
 	if (svar->is_domain)
 	{
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index ebc0babbd94..60502df488a 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -683,7 +683,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 				json_object_constructor_null_clause_opt
 				json_array_constructor_null_clause_opt
 
-%type <boolean>		OptNotNull OptImmutable
+%type <boolean>		OptNotNull OptImmutable OptTransactional
 
 /*
  * Non-keyword token types.  These are hard-wired into the "flex" lexer.
@@ -788,7 +788,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	SUBSCRIPTION SUBSTRING SUPPORT SYMMETRIC SYSID SYSTEM_P SYSTEM_USER
 
 	TABLE TABLES TABLESAMPLE TABLESPACE TARGET TEMP TEMPLATE TEMPORARY TEXT_P THEN
-	TIES TIME TIMESTAMP TO TRAILING TRANSACTION TRANSFORM
+	TIES TIME TIMESTAMP TO TRAILING TRANSACTION TRANSACTIONAL TRANSFORM
 	TREAT TRIGGER TRIM TRUE_P
 	TRUNCATE TRUSTED TYPE_P TYPES_P
 
@@ -5232,31 +5232,33 @@ create_extension_opt_item:
  *****************************************************************************/
 
 CreateSessionVarStmt:
-			CREATE OptTemp OptImmutable VARIABLE qualified_name opt_as Typename opt_collate_clause OptNotNull OptSessionVarDefExpr XactEndActionOption
+			CREATE OptTemp OptTransactional OptImmutable VARIABLE qualified_name opt_as Typename opt_collate_clause OptNotNull OptSessionVarDefExpr XactEndActionOption
 				{
 					CreateSessionVarStmt *n = makeNode(CreateSessionVarStmt);
-					$5->relpersistence = $2;
-					n->is_immutable = $3;
-					n->variable = $5;
-					n->typeName = $7;
-					n->collClause = (CollateClause *) $8;
-					n->not_null = $9;
-					n->defexpr = $10;
-					n->XactEndAction = $11;
+					$6->relpersistence = $2;
+					n->is_immutable = $4;
+					n->is_transact = $3;
+					n->variable = $6;
+					n->typeName = $8;
+					n->collClause = (CollateClause *) $9;
+					n->not_null = $10;
+					n->defexpr = $11;
+					n->XactEndAction = $12;
 					n->if_not_exists = false;
 					$$ = (Node *) n;
 				}
-			| CREATE OptTemp OptImmutable VARIABLE IF_P NOT EXISTS qualified_name opt_as Typename opt_collate_clause OptNotNull OptSessionVarDefExpr XactEndActionOption
+			| CREATE OptTemp OptTransactional OptImmutable VARIABLE IF_P NOT EXISTS qualified_name opt_as Typename opt_collate_clause OptNotNull OptSessionVarDefExpr XactEndActionOption
 				{
 					CreateSessionVarStmt *n = makeNode(CreateSessionVarStmt);
-					$8->relpersistence = $2;
-					n->is_immutable = $3;
-					n->variable = $8;
-					n->typeName = $10;
-					n->collClause = (CollateClause *) $11;
-					n->not_null = $12;
-					n->defexpr = $13;
-					n->XactEndAction = $14;
+					$9->relpersistence = $2;
+					n->is_immutable = $4;
+					n->is_transact = $3;
+					n->variable = $9;
+					n->typeName = $11;
+					n->collClause = (CollateClause *) $12;
+					n->not_null = $13;
+					n->defexpr = $14;
+					n->XactEndAction = $15;
 					n->if_not_exists = true;
 					$$ = (Node *) n;
 				}
@@ -5284,6 +5286,12 @@ OptImmutable: IMMUTABLE								{ $$ = true; }
 			| /* EMPTY */							{ $$ = false; }
 		;
 
+OptTransactional:
+			TRANSACTION								{ $$ = true; }
+			| TRANSACTIONAL							{ $$ = true; }
+			| /* EMPTY */							{ $$ = false; }
+		;
+
 /*****************************************************************************
  *
  * ALTER EXTENSION name UPDATE [ TO version ]
@@ -18045,6 +18053,7 @@ unreserved_keyword:
 			| TEXT_P
 			| TIES
 			| TRANSACTION
+			| TRANSACTIONAL
 			| TRANSFORM
 			| TRIGGER
 			| TRUNCATE
@@ -18695,6 +18704,7 @@ bare_label_keyword:
 			| TIMESTAMP
 			| TRAILING
 			| TRANSACTION
+			| TRANSACTIONAL
 			| TRANSFORM
 			| TREAT
 			| TRIGGER
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 4c22ce9553b..d9fc9b2d5c8 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -5336,6 +5336,7 @@ getVariables(Archive *fout)
 	int			i_varcollation;
 	int			i_varnotnull;
 	int			i_varisimmutable;
+	int			i_varistransact;
 	int			i_varacl;
 	int			i_acldefault;
 	int			i,
@@ -5360,6 +5361,7 @@ getVariables(Archive *fout)
 					  "       END AS varcollation,\n"
 					  "       v.varnotnull,\n"
 					  "       v.varisimmutable,\n"
+					  "       v.varistransact,\n"
 					  "       pg_catalog.pg_get_expr(v.vardefexpr,0) as vardefexpr,\n"
 					  "       v.varowner, v.varacl,\n"
 					  "       acldefault('V', v.varowner) AS acldefault\n"
@@ -5382,6 +5384,7 @@ getVariables(Archive *fout)
 	i_varcollation = PQfnumber(res, "varcollation");
 	i_varnotnull = PQfnumber(res, "varnotnull");
 	i_varisimmutable = PQfnumber(res, "varisimmutable");
+	i_varistransact = PQfnumber(res, "varistransact");
 
 	i_varowner = PQfnumber(res, "varowner");
 	i_varacl = PQfnumber(res, "varacl");
@@ -5410,6 +5413,7 @@ getVariables(Archive *fout)
 		varinfo[i].varcollation = atooid(PQgetvalue(res, i, i_varcollation));
 		varinfo[i].varnotnull = *(PQgetvalue(res, i, i_varnotnull)) == 't';
 		varinfo[i].varisimmutable = *(PQgetvalue(res, i, i_varisimmutable)) == 't';
+		varinfo[i].varistransact = *(PQgetvalue(res, i, i_varistransact)) == 't';
 
 		varinfo[i].dacl.acl = pg_strdup(PQgetvalue(res, i, i_varacl));
 		varinfo[i].dacl.acldefault = pg_strdup(PQgetvalue(res, i, i_acldefault));
@@ -5457,6 +5461,7 @@ dumpVariable(Archive *fout, const VariableInfo *varinfo)
 	const char *vardefexpr;
 	const char *varxactendaction;
 	const char *varisimmutable;
+	const char *varistransact;
 	Oid			varcollation;
 	bool		varnotnull;
 
@@ -5474,12 +5479,13 @@ dumpVariable(Archive *fout, const VariableInfo *varinfo)
 	varcollation = varinfo->varcollation;
 	varnotnull = varinfo->varnotnull;
 	varisimmutable = varinfo->varisimmutable ? "IMMUTABLE " : "";
+	varistransact = varinfo->varistransact ? "TRANSACTIONAL " : "";
 
 	appendPQExpBuffer(delq, "DROP VARIABLE %s;\n",
 					  qualvarname);
 
-	appendPQExpBuffer(query, "CREATE %sVARIABLE %s AS %s",
-					  varisimmutable, qualvarname, vartypname);
+	appendPQExpBuffer(query, "CREATE %s%sVARIABLE %s AS %s",
+					  varistransact, varisimmutable, qualvarname, vartypname);
 
 	if (OidIsValid(varcollation))
 	{
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index cbb06b66f74..22bcace330e 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -712,6 +712,7 @@ typedef struct _VariableInfo
 	const char *rolname;		/* name of owner, or empty string */
 	bool		varnotnull;
 	bool		varisimmutable;
+	bool		varistransact;
 } VariableInfo;
 
 /*
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index 9ebfd6dba72..869e09fb3e5 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -4031,6 +4031,42 @@ my %tests = (
 		},
 	},
 
+	'CREATE TRANSACTIONAL VARIABLE test_variable' => {
+		all_runs     => 1,
+		catch_all    => 'CREATE ... commands',
+		create_order => 61,
+		create_sql   => 'CREATE TRANSACTIONAL VARIABLE dump_test.variable8 AS integer',
+		regexp => qr/^
+			\QCREATE TRANSACTIONAL VARIABLE dump_test.variable8 AS integer;\E/xm,
+		like => {
+			%full_runs,
+			%dump_test_schema_runs,
+			section_pre_data => 1,
+		},
+		unlike => {
+			exclude_dump_test_schema => 1,
+			only_dump_measurement    => 1,
+		},
+	},
+
+	'CREATE TRANSACTIONAL IMMUTABLE VARIABLE test_variable' => {
+		all_runs     => 1,
+		catch_all    => 'CREATE ... commands',
+		create_order => 61,
+		create_sql   => 'CREATE TRANSACTIONAL IMMUTABLE VARIABLE dump_test.variable9 AS integer',
+		regexp => qr/^
+			\QCREATE TRANSACTIONAL IMMUTABLE VARIABLE dump_test.variable9 AS integer;\E/xm,
+		like => {
+			%full_runs,
+			%dump_test_schema_runs,
+			section_pre_data => 1,
+		},
+		unlike => {
+			exclude_dump_test_schema => 1,
+			only_dump_measurement    => 1,
+		},
+	},
+
 	'CREATE VIEW test_view' => {
 		create_order => 61,
 		create_sql => 'CREATE VIEW dump_test.test_view
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 15ca777996a..ce074a00b8b 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -5159,7 +5159,7 @@ listVariables(const char *pattern, bool verbose)
 	PQExpBufferData buf;
 	PGresult   *res;
 	printQueryOpt myopt = pset.popt;
-	static const bool translate_columns[] = {false, false, false, false, false, false, false, false, false, false, false};
+	static const bool translate_columns[] = {false, false, false, false, false, false, false, false, false, false, false, false};
 
 	if (pset.sversion < 180000)
 	{
@@ -5182,6 +5182,7 @@ listVariables(const char *pattern, bool verbose)
 					  "  pg_catalog.pg_get_userbyid(v.varowner) as \"%s\",\n"
 					  "  NOT v.varnotnull as \"%s\",\n"
 					  "  NOT v.varisimmutable as \"%s\",\n"
+					  "  v.varistransact as \"%s\",\n"
 					  "  pg_catalog.pg_get_expr(v.vardefexpr, 0) as \"%s\",\n"
 					  "  CASE v.varxactendaction\n"
 					  "    WHEN 'd' THEN 'ON COMMIT DROP'\n"
@@ -5194,6 +5195,7 @@ listVariables(const char *pattern, bool verbose)
 					  gettext_noop("Owner"),
 					  gettext_noop("Nullable"),
 					  gettext_noop("Mutable"),
+					  gettext_noop("Transactional"),
 					  gettext_noop("Default"),
 					  gettext_noop("Transactional end action"));
 
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 4d6a943712f..02c641acd1f 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1284,6 +1284,8 @@ static const pgsql_thing_t words_after_create[] = {
 																			 * TABLE ... */
 	{"TEXT SEARCH", NULL, NULL, NULL},
 	{"TRANSFORM", NULL, NULL, NULL, NULL, THING_NO_ALTER},
+	{"TRANSACTIONAL", NULL, NULL, NULL, NULL, THING_NO_DROP | THING_NO_ALTER}, /* for CREATE TRANSACTIONAL
+																				* VARIABLE ... */
 	{"TRIGGER", "SELECT tgname FROM pg_catalog.pg_trigger WHERE tgname LIKE '%s' AND NOT tgisinternal"},
 	{"TYPE", NULL, NULL, &Query_for_list_of_datatypes},
 	{"UNIQUE", NULL, NULL, NULL, NULL, THING_NO_DROP | THING_NO_ALTER}, /* for CREATE UNIQUE
@@ -3596,8 +3598,11 @@ psql_completion(const char *text, int start, int end)
 	}
 /* CREATE VARIABLE --- is allowed inside CREATE SCHEMA, so use TailMatches */
 	/* Complete CREATE VARIABLE <name> with AS */
+	else if (Matches("CREATE", "TRANSACTION|TRANSACTIONAL"))
+		COMPLETE_WITH("IMMUTABLE", "VARIABLE");
 	else if (TailMatches("CREATE", "VARIABLE", MatchAny) ||
 			 TailMatches("TEMP|TEMPORARY", "VARIABLE", MatchAny) ||
+			 TailMatches("TRANSACTION|TRANSACTIONAL", "VARIABLE", MatchAny) ||
 			 TailMatches("IMMUTABLE", "VARIABLE", MatchAny))
 		COMPLETE_WITH("AS");
 	else if (TailMatches("VARIABLE", MatchAny, "AS"))
diff --git a/src/include/catalog/pg_variable.h b/src/include/catalog/pg_variable.h
index 68bc49a0e27..1dc44edf6e5 100644
--- a/src/include/catalog/pg_variable.h
+++ b/src/include/catalog/pg_variable.h
@@ -61,6 +61,9 @@ CATALOG(pg_variable,9222,VariableRelationId)
 	/* don't allow changes */
 	bool		varisimmutable BKI_DEFAULT(f);
 
+	/* supports transactions */
+	bool		varistransact BKI_DEFAULT(f);
+
 	/* action on transaction end */
 	char		varxactendaction BKI_DEFAULT(n);
 
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 0cc560d538f..1f981ba8c85 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -3459,6 +3459,7 @@ typedef struct CreateSessionVarStmt
 	bool		if_not_exists;	/* do nothing if it already exists */
 	bool		not_null;		/* disallow nulls */
 	bool		is_immutable;	/* don't allow changes */
+	bool		is_transact;	/* supports transactions */
 	Node	   *defexpr;		/* default expression */
 	char		XactEndAction;	/* on transaction end action */
 } CreateSessionVarStmt;
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index 2e88d7d9508..753cf360db4 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -457,6 +457,7 @@ PG_KEYWORD("timestamp", TIMESTAMP, COL_NAME_KEYWORD, BARE_LABEL)
 PG_KEYWORD("to", TO, RESERVED_KEYWORD, AS_LABEL)
 PG_KEYWORD("trailing", TRAILING, RESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("transaction", TRANSACTION, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("transactional", TRANSACTIONAL, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("transform", TRANSFORM, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("treat", TREAT, COL_NAME_KEYWORD, BARE_LABEL)
 PG_KEYWORD("trigger", TRIGGER, UNRESERVED_KEYWORD, BARE_LABEL)
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index 451fdde31ce..75f173d2333 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -5833,20 +5833,20 @@ CREATE ROLE regress_variable_owner;
 SET ROLE TO regress_variable_owner;
 CREATE VARIABLE var1 AS varchar COLLATE "C";
 \dV+ var1
-                                                                         List of variables
- Schema | Name |       Type        | Collation |         Owner          | Nullable | Mutable | Default | Transactional end action | Access privileges | Description 
---------+------+-------------------+-----------+------------------------+----------+---------+---------+--------------------------+-------------------+-------------
- public | var1 | character varying | C         | regress_variable_owner | t        | t       |         |                          |                   | 
+                                                                                 List of variables
+ Schema | Name |       Type        | Collation |         Owner          | Nullable | Mutable | Transactional | Default | Transactional end action | Access privileges | Description 
+--------+------+-------------------+-----------+------------------------+----------+---------+---------------+---------+--------------------------+-------------------+-------------
+ public | var1 | character varying | C         | regress_variable_owner | t        | t       | f             |         |                          |                   | 
 (1 row)
 
 GRANT SELECT ON VARIABLE var1 TO PUBLIC;
 COMMENT ON VARIABLE var1 IS 'some description';
 \dV+ var1
-                                                                                           List of variables
- Schema | Name |       Type        | Collation |         Owner          | Nullable | Mutable | Default | Transactional end action |                Access privileges                 |   Description    
---------+------+-------------------+-----------+------------------------+----------+---------+---------+--------------------------+--------------------------------------------------+------------------
- public | var1 | character varying | C         | regress_variable_owner | t        | t       |         |                          | regress_variable_owner=rw/regress_variable_owner+| some description
-        |      |                   |           |                        |          |         |         |                          | =r/regress_variable_owner                        | 
+                                                                                                   List of variables
+ Schema | Name |       Type        | Collation |         Owner          | Nullable | Mutable | Transactional | Default | Transactional end action |                Access privileges                 |   Description    
+--------+------+-------------------+-----------+------------------------+----------+---------+---------------+---------+--------------------------+--------------------------------------------------+------------------
+ public | var1 | character varying | C         | regress_variable_owner | t        | t       | f             |         |                          | regress_variable_owner=rw/regress_variable_owner+| some description
+        |      |                   |           |                        |          |         |               |         |                          | =r/regress_variable_owner                        | 
 (1 row)
 
 DROP VARIABLE var1;
@@ -6314,9 +6314,9 @@ List of schemas
 (0 rows)
 
 \dV "no.such.variable"
-                                         List of variables
- Schema | Name | Type | Collation | Owner | Nullable | Mutable | Default | Transactional end action 
---------+------+------+-----------+-------+----------+---------+---------+--------------------------
+                                                 List of variables
+ Schema | Name | Type | Collation | Owner | Nullable | Mutable | Transactional | Default | Transactional end action 
+--------+------+------+-----------+-------+----------+---------+---------------+---------+--------------------------
 (0 rows)
 
 -- again, but with dotted schema qualifications.
@@ -6489,9 +6489,9 @@ improper qualified name (too many dotted names): "no.such.schema"."no.such.insta
 \dy "no.such.schema"."no.such.event.trigger"
 improper qualified name (too many dotted names): "no.such.schema"."no.such.event.trigger"
 \dV "no.such.schema"."no.such.variable"
-                                         List of variables
- Schema | Name | Type | Collation | Owner | Nullable | Mutable | Default | Transactional end action 
---------+------+------+-----------+-------+----------+---------+---------+--------------------------
+                                                 List of variables
+ Schema | Name | Type | Collation | Owner | Nullable | Mutable | Transactional | Default | Transactional end action 
+--------+------+------+-----------+-------+----------+---------+---------------+---------+--------------------------
 (0 rows)
 
 -- again, but with current database and dotted schema qualifications.
@@ -6628,9 +6628,9 @@ List of text search templates
 (0 rows)
 
 \dV regression."no.such.schema"."no.such.variable"
-                                         List of variables
- Schema | Name | Type | Collation | Owner | Nullable | Mutable | Default | Transactional end action 
---------+------+------+-----------+-------+----------+---------+---------+--------------------------
+                                                 List of variables
+ Schema | Name | Type | Collation | Owner | Nullable | Mutable | Transactional | Default | Transactional end action 
+--------+------+------+-----------+-------+----------+---------+---------------+---------+--------------------------
 (0 rows)
 
 -- again, but with dotted database and dotted schema qualifications.
diff --git a/src/test/regress/expected/session_variables.out b/src/test/regress/expected/session_variables.out
index 2d485944660..a920a529ac3 100644
--- a/src/test/regress/expected/session_variables.out
+++ b/src/test/regress/expected/session_variables.out
@@ -45,11 +45,11 @@ SET ROLE TO regress_variable_owner;
 CREATE VARIABLE svartest.var1 AS int;
 SET ROLE TO DEFAULT;
 \dV+ svartest.var1
-                                                                                     List of variables
-  Schema  | Name |  Type   | Collation |         Owner          | Nullable | Mutable | Default | Transactional end action |                Access privileges                 | Description 
-----------+------+---------+-----------+------------------------+----------+---------+---------+--------------------------+--------------------------------------------------+-------------
- svartest | var1 | integer |           | regress_variable_owner | t        | t       |         |                          | regress_variable_owner=rw/regress_variable_owner+| 
-          |      |         |           |                        |          |         |         |                          | regress_variable_reader=r/regress_variable_owner | 
+                                                                                             List of variables
+  Schema  | Name |  Type   | Collation |         Owner          | Nullable | Mutable | Transactional | Default | Transactional end action |                Access privileges                 | Description 
+----------+------+---------+-----------+------------------------+----------+---------+---------------+---------+--------------------------+--------------------------------------------------+-------------
+ svartest | var1 | integer |           | regress_variable_owner | t        | t       | f             |         |                          | regress_variable_owner=rw/regress_variable_owner+| 
+          |      |         |           |                        |          |         |               |         |                          | regress_variable_reader=r/regress_variable_owner | 
 (1 row)
 
 DROP VARIABLE svartest.var1;
@@ -1747,3 +1747,103 @@ SELECT var1;
 LET var1 = 30;
 ERROR:  session variable "public.var1" is declared IMMUTABLE
 DROP VARIABLE var1;
+-- test transactional variables
+CREATE TRANSACTION VARIABLE tv AS int DEFAULT 0;
+BEGIN;
+  LET tv = 100;
+  SELECT tv;
+ tv  
+-----
+ 100
+(1 row)
+
+ROLLBACK;
+SELECT tv;
+ tv 
+----
+  0
+(1 row)
+
+LET tv = 100;
+BEGIN;
+  LET tv = 1000;
+COMMIT;
+SELECT tv;
+  tv  
+------
+ 1000
+(1 row)
+
+BEGIN;
+  LET tv = 0;
+  SELECT tv;
+ tv 
+----
+  0
+(1 row)
+
+ROLLBACK;
+SELECT tv;
+  tv  
+------
+ 1000
+(1 row)
+
+-- test subtransactions
+BEGIN;
+  LET tv = 1;
+SAVEPOINT x1;
+  LET tv = 2;
+SAVEPOINT x2;
+  LET tv = 3;
+ROLLBACK TO x2;
+  SELECT tv;
+ tv 
+----
+  2
+(1 row)
+
+  LET tv = 10;
+ROLLBACK TO x1;
+  SELECT tv;
+ tv 
+----
+  1
+(1 row)
+
+ROLLBACK;
+SELECT tv;
+  tv  
+------
+ 1000
+(1 row)
+
+BEGIN;
+  LET tv = 1;
+SAVEPOINT x1;
+  LET tv = 2;
+SAVEPOINT x2;
+  LET tv = 3;
+ROLLBACK TO x2;
+  SELECT tv;
+ tv 
+----
+  2
+(1 row)
+
+  LET tv = 10;
+ROLLBACK TO x1;
+  SELECT tv;
+ tv 
+----
+  1
+(1 row)
+
+COMMIT;
+SELECT tv;
+ tv 
+----
+  1
+(1 row)
+
+DROP VARIABLE tv;
diff --git a/src/test/regress/sql/session_variables.sql b/src/test/regress/sql/session_variables.sql
index 66df1121d68..2ab78aeca25 100644
--- a/src/test/regress/sql/session_variables.sql
+++ b/src/test/regress/sql/session_variables.sql
@@ -1196,3 +1196,61 @@ SELECT var1;
 LET var1 = 30;
 
 DROP VARIABLE var1;
+
+-- test transactional variables
+CREATE TRANSACTION VARIABLE tv AS int DEFAULT 0;
+
+BEGIN;
+  LET tv = 100;
+  SELECT tv;
+ROLLBACK;
+
+SELECT tv;
+
+LET tv = 100;
+BEGIN;
+  LET tv = 1000;
+COMMIT;
+
+SELECT tv;
+
+BEGIN;
+  LET tv = 0;
+  SELECT tv;
+ROLLBACK;
+
+SELECT tv;
+
+-- test subtransactions
+
+BEGIN;
+  LET tv = 1;
+SAVEPOINT x1;
+  LET tv = 2;
+SAVEPOINT x2;
+  LET tv = 3;
+ROLLBACK TO x2;
+  SELECT tv;
+  LET tv = 10;
+ROLLBACK TO x1;
+  SELECT tv;
+ROLLBACK;
+
+SELECT tv;
+
+BEGIN;
+  LET tv = 1;
+SAVEPOINT x1;
+  LET tv = 2;
+SAVEPOINT x2;
+  LET tv = 3;
+ROLLBACK TO x2;
+  SELECT tv;
+  LET tv = 10;
+ROLLBACK TO x1;
+  SELECT tv;
+COMMIT;
+
+SELECT tv;
+
+DROP VARIABLE tv;
-- 
2.45.2

