From 35bc6188511cb747377f341032ce08d69b356303 Mon Sep 17 00:00:00 2001
From: "okbob@github.com" <pavel.stehule@gmail.com>
Date: Mon, 4 Jul 2022 06:06:19 +0200
Subject: [PATCH 03/13] typecheck - check of consistency of format of stored
 value.

The lifecycle of value of session variable can be long. In this time
the composite type can changed, or extension that defines the type
can be upgraded. In this case we cannot to use stored value.

Session variables holds a own evidence of fingerprints for any
used type. When some possible not compatible change is detected
(the version of extension is changed), the related stored type's
generation number is increased. The copy of genaration number is
stored in session variable data. Before any read of session variable,
the stored generation number is compared with current type generation
number. When it is different, the read is canceled.
---
 src/backend/commands/sessionvariable.c | 256 ++++++++++++++++++-------
 1 file changed, 189 insertions(+), 67 deletions(-)

diff --git a/src/backend/commands/sessionvariable.c b/src/backend/commands/sessionvariable.c
index e8d096ff3f..c079758a8e 100644
--- a/src/backend/commands/sessionvariable.c
+++ b/src/backend/commands/sessionvariable.c
@@ -14,12 +14,15 @@
  */
 #include "postgres.h"
 #include "miscadmin.h"
+#include "access/genam.h"
 #include "access/heapam.h"
 #include "access/htup_details.h"
 #include "access/xact.h"
 #include "catalog/dependency.h"
 #include "catalog/indexing.h"
 #include "catalog/namespace.h"
+#include "catalog/pg_extension.h"
+#include "catalog/pg_type.h"
 #include "catalog/pg_variable.h"
 #include "commands/session_variable.h"
 #include "executor/executor.h"
@@ -29,14 +32,18 @@
 #include "parser/parse_expr.h"
 #include "parser/parse_type.h"
 #include "rewrite/rewriteHandler.h"
+#include "storage/itemptr.h"
 #include "storage/lmgr.h"
+#include "storage/proc.h"
 #include "tcop/tcopprot.h"
 #include "utils/builtins.h"
 #include "utils/datum.h"
+#include "utils/fmgroids.h"
 #include "utils/inval.h"
 #include "utils/lsyscache.h"
 #include "utils/snapmgr.h"
 #include "utils/syscache.h"
+#include "utils/typcache.h"
 
 /*
  * Values of session variables are stored in local memory, in
@@ -111,10 +118,55 @@
  * check can be delayed after oid overflow. Second issue is possible
  * change of format of stored value. So before returning of any
  * value of session variable, we have to check if returned value
- * is safe. If not, we will mark the session variable as broken,
- * and any read of this variable until end of session or rewriting
- * raises an exception.
+ * is safe.
+ *
+ * Scalar types should be buildin, domain, or come from extensions.
+ * Buildin types should be valid every time. For other cases we
+ * can check xmin, and if xmin is different, then we can invalidate
+ * the value. For types from extension we can check xmin of pg_extension
+ * related record. For domains we should to force cast to domain
+ * type to reaply all constraints before returning the value.
  */
+struct SVariableTypeDataField;
+
+typedef struct SVaribleTypeData
+{
+	Oid			typid;
+	int64		gennum;			/* generation number helps with change detection */
+	LocalTransactionId verified_lxid; /* lxid of transaction when the typ fingerprint
+									   * was created or verified */
+	int16		typlen;
+	bool		typbyval;
+	bool		is_domain;
+	bool		is_rowtype;
+	char	   *typname;
+
+	struct SVaribleTypeData *base_type;
+	int64		base_type_gennum;
+	void	   *domain_check_extra;
+	LocalTransactionId domain_check_extra_lxid;
+
+	TransactionId	typ_xmin;	/* xmin of related tuple of pg_type */
+	ItemPointerData typ_tid;
+
+	Oid			extid;			/* OID of owner extension if exists */
+	TransactionId ext_xmin;		/* xmin of related tuple of pg_extension */
+	ItemPointerData ext_tid;
+	char	   *extname;
+	char	   *extversion;
+
+	int			natts;			/* number of attributies of composite type */
+	struct SvariableTypeDataField *attrs;		/* array of attributies */
+} SVariableTypeData;
+
+typedef SVariableTypeData * SVariableType;
+
+typedef struct SvariableTypeDataField
+{
+	SVariableType svartype;
+	int64		gennum;
+} SvariableTypeDataField;
+
 typedef enum SVariableXActAction
 {
 	SVAR_ON_COMMIT_DROP,		/* used for ON COMMIT DROP */
@@ -154,14 +206,14 @@ static List *xact_recheck_varids = NIL;
 typedef struct SVariableData
 {
 	Oid			varid;			/* pg_variable OID of this sequence (hash key) */
-	Oid			typid;			/* OID of the data type */
-	int16		typlen;
-	bool		typbyval;
+
+	SVariableType svartype;
+	int64		gennum;
+
 	bool		isnull;
 	bool		freeval;
 	Datum		value;
 
-	bool		is_rowtype;		/* true when variable is composite */
 	bool		is_not_null;	/* don't allow null values */
 	bool		is_immutable;	/* true when variable is immutable */
 	bool		has_defexpr;	/* true when variable has a default value */
@@ -176,13 +228,19 @@ typedef struct SVariableData
 
 typedef SVariableData * SVariable;
 
-static HTAB *sessionvars = NULL;	/* hash table for session variables */
+static HTAB *sessionvars = NULL;		/* hash table for session variables */
+static HTAB *sessionvars_types = NULL;	/* hash table for type fingerprints of session
+										 * variables */
+
 static MemoryContext SVariableMemoryContext = NULL;
 
 static bool first_time = true;
 
 static bool sync_sessionvars_all_is_required = false;
 
+static SVariableType get_svariabletype(Oid typid);
+static int64 get_svariable_valid_type_gennum(SVariableType svt);
+
 static void register_session_variable_xact_action(Oid varid, SVariableXActAction action);
 static void unregister_session_variable_xact_action(Oid varid, SVariableXActAction action);
 
@@ -966,9 +1024,10 @@ sync_sessionvars_all()
  * Create the hash table for storing session variables
  */
 static void
-create_sessionvars_hashtable(void)
+create_sessionvars_hashtables(void)
 {
-	HASHCTL		ctl;
+	HASHCTL		vars_ctl;
+	HASHCTL		types_ctl;
 
 	/* set callbacks */
 	if (first_time)
@@ -988,14 +1047,24 @@ create_sessionvars_hashtable(void)
 
 	Assert(SVariableMemoryContext);
 
-	memset(&ctl, 0, sizeof(ctl));
-	ctl.keysize = sizeof(Oid);
-	ctl.entrysize = sizeof(SVariableData);
-	ctl.hcxt = SVariableMemoryContext;
+	memset(&vars_ctl, 0, sizeof(vars_ctl));
+	vars_ctl.keysize = sizeof(Oid);
+	vars_ctl.entrysize = sizeof(SVariableData);
+	vars_ctl.hcxt = SVariableMemoryContext;
 
 	Assert(sessionvars == NULL);
 
-	sessionvars = hash_create("Session variables", 64, &ctl,
+	sessionvars = hash_create("Session variables", 64, &vars_ctl,
+								   HASH_ELEM | HASH_BLOBS | HASH_CONTEXT);
+
+	memset(&types_ctl, 0, sizeof(types_ctl));
+	types_ctl.keysize = sizeof(Oid);
+	types_ctl.entrysize = sizeof(SVariableTypeData);
+	types_ctl.hcxt = SVariableMemoryContext;
+
+	Assert(sessionvars_types == NULL);
+
+	sessionvars_types = hash_create("Session variable types", 64, &types_ctl,
 								   HASH_ELEM | HASH_BLOBS | HASH_CONTEXT);
 }
 
@@ -1023,15 +1092,19 @@ set_session_variable(SVariable svar, Datum value,
 						get_namespace_name(get_session_variable_namespace(svar->varid)),
 						get_session_variable_name(svar->varid))));
 
-	if (svar->typid != typid)
+	Assert(svar->svartype);
+
+	if (svar->svartype->typid != typid)
 		ereport(ERROR,
 				(errcode(ERRCODE_DATATYPE_MISMATCH),
 				 errmsg("type \"%s\" of assigned value is different than type \"%s\" of session variable \"%s.%s\"",
 						format_type_be(typid),
-						format_type_be(svar->typid),
+						format_type_be(svar->svartype->typid),
 						get_namespace_name(get_session_variable_namespace(svar->varid)),
 						get_session_variable_name(svar->varid))));
 
+	svar->gennum = get_svariable_valid_type_gennum(svar->svartype);
+
 	/*
 	 * Don't allow updating of immutable session variable that has assigned
 	 * not null value or has default expression (and then the value should be
@@ -1049,7 +1122,10 @@ set_session_variable(SVariable svar, Datum value,
 	/* copy value to session persistent context */
 	oldcxt = MemoryContextSwitchTo(SVariableMemoryContext);
 	if (!isnull)
-		newval = datumCopy(value, svar->typbyval, svar->typlen);
+		newval = datumCopy(value,
+						   svar->svartype->typbyval,
+						   svar->svartype->typlen);
+
 	MemoryContextSwitchTo(oldcxt);
 
 	free_session_variable_value(svar);
@@ -1071,17 +1147,13 @@ init_session_variable(SVariable svar, Variable *var)
 	Assert(OidIsValid(var->oid));
 
 	svar->varid = var->oid;
-	svar->typid = var->typid;
-
-	get_typlenbyval(var->typid,
-					&svar->typlen,
-					&svar->typbyval);
+	svar->svartype = get_svariabletype(var->typid);
+	svar->gennum = get_svariable_valid_type_gennum(svar->svartype);
 
+	svar->value = (Datum) 0;
 	svar->isnull = true;
 	svar->freeval = false;
-	svar->value = (Datum) 0;
 
-	svar->is_rowtype = type_is_rowtype(var->typid);
 	svar->is_not_null = var->is_not_null;
 	svar->is_immutable = var->is_immutable;
 	svar->has_defexpr = var->has_defexpr;
@@ -1125,7 +1197,7 @@ prepare_variable_for_reading(Oid varid)
 	var.oid = InvalidOid;
 
 	if (!sessionvars)
-		create_sessionvars_hashtable();
+		create_sessionvars_hashtables();
 
 	/* Protect used session variable against drop until transaction end */
 	LockDatabaseObject(VariableRelationId, varid, 0, AccessShareLock);
@@ -1141,54 +1213,97 @@ prepare_variable_for_reading(Oid varid)
 					 varid);
 
 	/* Return content if it is available and valid */
-	if (found && svar->is_valid)
-		return svar;
+	if (!found || !svar->is_valid)
+	{
+		/* We need to load defexpr. */
+		initVariable(&var, varid, false);
+
+		if (!found)
+			init_session_variable(svar, &var);
+
+		/* Raise an error when we cannot initialize variable correctly */
+		if (var.is_not_null && !var.defexpr)
+			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.")));
+
+		if (svar->has_defexpr)
+		{
+			Datum		value = (Datum) 0;
+			bool		isnull;
+			EState	   *estate = NULL;
+			Expr	   *defexpr;
+			ExprState  *defexprs;
+			MemoryContext oldcxt;
 
-	/* We need to load defexpr. */
-	initVariable(&var, varid, false);
+			/* Prepare default expr */
+			estate = CreateExecutorState();
 
-	if (!found)
-		init_session_variable(svar, &var);
+			oldcxt = MemoryContextSwitchTo(estate->es_query_cxt);
+
+			defexpr = expression_planner((Expr *) var.defexpr);
+			defexprs = ExecInitExpr(defexpr, NULL);
+			value = ExecEvalExprSwitchContext(defexprs,
+											  GetPerTupleExprContext(estate),
+											  &isnull);
+
+
+			/* Store result before releasing Executor memory */
+			set_session_variable(svar, value, isnull, svar->svartype->typid, true);
+
+			MemoryContextSwitchTo(oldcxt);
+
+			FreeExecutorState(estate);
+		}
+		else
+			set_session_variable(svar, (Datum) 0, true, svar->svartype->typid, true);
+	}
 
-	/* Raise an error when we cannot initialize variable correctly */
-	if (var.is_not_null && !var.defexpr)
+	/*
+	 * Check if stored value has still valid type. Now, we just to
+	 * raise error. Future versions can try to convert stored to
+	 * current binary format instead (composite types).
+	 */
+	if (!session_variable_use_valid_type(svar))
 		ereport(ERROR,
-				(errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
-				 errmsg("null value is not allowed for NOT NULL session variable \"%s.%s\"",
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("format of stored value of \"%s.%s\" session variable can be outdated",
 						get_namespace_name(get_session_variable_namespace(varid)),
-						get_session_variable_name(varid)),
-				 errdetail("The session variable was not initialized yet.")));
+						get_session_variable_name(varid))));
 
-	if (svar->has_defexpr)
+	/*
+	 * Although the value of domain type should be valid (it is
+	 * checked when it is assigned to session variable), we have to
+	 * check related constraints anytime. It can be more expensive
+	 * than in PL/pgSQL. PL/pgSQL forces domain checks when value
+	 * is assigned to the variable or when value is returned from
+	 * function. Fortunately, domain types manage cache of constraints by
+	 * self.
+	 */
+	if (svar->svartype->is_domain)
 	{
-		Datum		value = (Datum) 0;
-		bool		isnull;
-		EState	   *estate = NULL;
-		Expr	   *defexpr;
-		ExprState  *defexprs;
-		MemoryContext oldcxt;
+		MemoryContext oldcxt = CurrentMemoryContext;
 
-		/* Prepare default expr */
-		estate = CreateExecutorState();
-
-		oldcxt = MemoryContextSwitchTo(estate->es_query_cxt);
-
-		defexpr = expression_planner((Expr *) var.defexpr);
-		defexprs = ExecInitExpr(defexpr, NULL);
-		value = ExecEvalExprSwitchContext(defexprs,
-										  GetPerTupleExprContext(estate),
-										  &isnull);
+		/*
+		 * Store domain_check extra in CurTransactionContext. When we are
+		 * in other transaction, the domain_check_extra cache is not valid.
+		 */
+		if (svar->svartype->domain_check_extra_lxid != MyProc->lxid)
+			svar->svartype->domain_check_extra = NULL;
 
+		domain_check(svar->value,
+					 svar->isnull,
+					 svar->svartype->typid,
+					 &svar->svartype->domain_check_extra,
+					 CurTransactionContext);
 
-		/* Store result before releasing Executor memory */
-		set_session_variable(svar, value, isnull, svar->typid, true);
+		svar->svartype->domain_check_extra_lxid = MyProc->lxid;
 
 		MemoryContextSwitchTo(oldcxt);
-
-		FreeExecutorState(estate);
 	}
-	else
-		set_session_variable(svar, (Datum) 0, true, svar->typid, true);
 
 	return svar;
 }
@@ -1212,7 +1327,7 @@ SetSessionVariable(Oid varid, Datum value, bool isNull, Oid typid)
 	LockDatabaseObject(VariableRelationId, varid, 0, AccessShareLock);
 
 	if (!sessionvars)
-		create_sessionvars_hashtable();
+		create_sessionvars_hashtables();
 
 	/* Ensure so all entries in sessionvars hash table are valid */
 	sync_sessionvars_all();
@@ -1267,10 +1382,12 @@ CopySessionVariable(Oid varid, bool *isNull, Oid *typid)
 	Assert(svar != NULL && svar->is_valid);
 
 	*isNull = svar->isnull;
-	*typid = svar->typid;
+	*typid = svar->svartype->typid;
 
 	if (!svar->isnull)
-		return datumCopy(svar->value, svar->typbyval, svar->typlen);
+		return datumCopy(svar->value,
+						 svar->svartype->typbyval,
+						 svar->svartype->typlen);
 
 	return (Datum) 0;
 }
@@ -1288,7 +1405,7 @@ CopySessionVariableWithTypeCheck(Oid varid, bool *isNull, Oid expected_typid)
 	svar = prepare_variable_for_reading(varid);
 	Assert(svar != NULL && svar->is_valid);
 
-	if (expected_typid != svar->typid)
+	if (expected_typid != svar->svartype->typid)
 		elog(ERROR, "type of variable \"%s.%s\" is different than expected",
 			 get_namespace_name(get_session_variable_namespace(varid)),
 			 get_session_variable_name(varid));
@@ -1296,7 +1413,9 @@ CopySessionVariableWithTypeCheck(Oid varid, bool *isNull, Oid expected_typid)
 	*isNull = svar->isnull;
 
 	if (!svar->isnull)
-		return datumCopy(svar->value, svar->typbyval, svar->typlen);
+		return datumCopy(svar->value,
+						 svar->svartype->typbyval,
+						 svar->svartype->typlen);
 
 	return (Datum) 0;
 }
@@ -1314,7 +1433,7 @@ GetSessionVariableWithTypeCheck(Oid varid, bool *isNull, Oid expected_typid)
 	svar = prepare_variable_for_reading(varid);
 	Assert(svar != NULL && svar->is_valid);
 
-	if (expected_typid != svar->typid)
+	if (expected_typid != svar->svartype->typid)
 		elog(ERROR, "type of variable \"%s.%s\" is different than expected",
 			 get_namespace_name(get_session_variable_namespace(varid)),
 			 get_session_variable_name(varid));
@@ -1518,6 +1637,9 @@ ResetSessionVariables(void)
 	{
 		hash_destroy(sessionvars);
 		sessionvars = NULL;
+
+		hash_destroy(sessionvars_types);
+		sessionvars_types = NULL;
 	}
 
 	/* Release memory allocated by session variables */
-- 
2.36.1

