From 7af6365991db4b6f839b4256336ec3b788c50ba8 Mon Sep 17 00:00:00 2001
From: "okbob@github.com" <okbob@github.com>
Date: Thu, 6 Jul 2023 14:40:37 +0200
Subject: [PATCH 13/20] Implementation of NOT NULL and IMMUTABLE clauses

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

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

