From e08cd68b7f924c90a0bea5c7b600d3ab6191bdce Mon Sep 17 00:00:00 2001
From: "okbob@github.com" <pavel.stehule@gmail.com>
Date: Fri, 21 Nov 2025 20:42:56 +0100
Subject: [PATCH 03/11] collect session variables used in plan and assign
 paramid

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

Some unsupported optimizations are disabled:

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

Before execution of query with session variables we need to collect
used session variables. This list is used for loading variables to
executed query.

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

diff --git a/doc/src/sgml/parallel.sgml b/doc/src/sgml/parallel.sgml
index af43484703e..843e2c3f663 100644
--- a/doc/src/sgml/parallel.sgml
+++ b/doc/src/sgml/parallel.sgml
@@ -524,6 +524,12 @@ EXPLAIN SELECT * FROM pgbench_accounts WHERE filler LIKE '%x%';
         Plan nodes that reference a correlated <literal>SubPlan</literal>.
       </para>
     </listitem>
+
+    <listitem>
+      <para>
+        Usage of a session variable.
+      </para>
+    </listitem>
   </itemizedlist>
 
  <sect2 id="parallel-labeling">
diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c
index 8e70a85a3f7..22c5372ad80 100644
--- a/src/backend/catalog/dependency.c
+++ b/src/backend/catalog/dependency.c
@@ -1902,6 +1902,16 @@ find_expr_references_walker(Node *node,
 	{
 		Param	   *param = (Param *) node;
 
+		/*
+		* catalog less session variable variable cannot be used in persistent
+		* catalog based object.
+		*/
+		if (param->paramkind == PARAM_VARIABLE)
+			ereport(ERROR,
+					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					 errmsg("session variable \"%s\" cannot be referenced in a catalog object",
+							param->paramvarname)));
+
 		/* A parameter must depend on the parameter's datatype */
 		add_object_address(TypeRelationId, param->paramtype, 0,
 						   context->addrs);
diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c
index bcaf37f788b..201bc048d8b 100644
--- a/src/backend/commands/session_variable.c
+++ b/src/backend/commands/session_variable.c
@@ -132,6 +132,45 @@ get_session_variable_type_typmod_collid(char *varname,
 	*collid = svar->varcollation;
 }
 
+/*
+ * Returns a copy of the value of the session variable (in the current memory
+ * context).
+ */
+Datum
+GetSessionVariableWithTypecheck(char *varname,
+								Oid typid, int32 typmod,
+								bool *isnull)
+{
+	SVariable	svar;
+	Datum		result;
+
+	svar = search_variable(varname);
+
+	if (svar->vartype != typid || svar->vartypmod != typmod)
+		ereport(ERROR,
+				(errcode(ERRCODE_DATATYPE_MISMATCH),
+				 errmsg("session variable %s is not of a type %s but type %s",
+						varname,
+						format_type_with_typemod(typid, typmod),
+						format_type_with_typemod(svar->vartype, svar->vartypmod))));
+
+	/* only owner can get content of variable */
+	if (svar->varowner != GetUserId() && !superuser())
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("permission denied for session variable %s",
+						varname)));
+
+	if (!svar->isnull)
+		result = datumCopy(svar->value, svar->typbyval, svar->typlen);
+	else
+		result = (Datum) 0;
+
+	*isnull = svar->isnull;
+
+	return result;
+}
+
 /*
  * Creates a new variable - does new entry in sessionvars
  *
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index fd77334e5fd..5147be99d14 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -374,6 +374,7 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
 	glob->dependsOnRole = false;
 	glob->partition_directory = NULL;
 	glob->rel_notnullatts_hash = NULL;
+	glob->sessionVariables = NIL;
 
 	/*
 	 * Assess whether it's feasible to use parallel mode for this query. We
@@ -617,6 +618,9 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
 	result->paramExecTypes = glob->paramExecTypes;
 	/* utilityStmt should be null, but we might as well copy it */
 	result->utilityStmt = parse->utilityStmt;
+
+	result->sessionVariables = glob->sessionVariables;
+
 	result->stmt_location = parse->stmt_location;
 	result->stmt_len = parse->stmt_len;
 
@@ -805,6 +809,13 @@ subquery_planner(PlannerGlobal *glob, Query *parse, char *plan_name,
 	 */
 	pull_up_subqueries(root);
 
+	/*
+	 * Check if some subquery uses a session variable.  The flag
+	 * hasSessionVariables should be true if the query or some subquery uses a
+	 * session variable.
+	 */
+	pull_up_has_session_variables(root);
+
 	/*
 	 * If this is a simple UNION ALL query, flatten it into an appendrel. We
 	 * do this now because it requires applying pull_up_subqueries to the leaf
diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c
index ccdc9bc264a..48de8bf4f14 100644
--- a/src/backend/optimizer/plan/setrefs.c
+++ b/src/backend/optimizer/plan/setrefs.c
@@ -210,6 +210,8 @@ static List *set_returning_clause_references(PlannerInfo *root,
 static List *set_windowagg_runcondition_references(PlannerInfo *root,
 												   List *runcondition,
 												   Plan *plan);
+static bool pull_up_has_session_variables_walker(Node *node,
+												 PlannerInfo *root);
 
 
 /*****************************************************************************
@@ -1341,6 +1343,50 @@ set_plan_refs(PlannerInfo *root, Plan *plan, int rtoffset)
 	return plan;
 }
 
+/*
+ * Search usage of session variables in subqueries
+ */
+void
+pull_up_has_session_variables(PlannerInfo *root)
+{
+	Query	   *query = root->parse;
+
+	if (query->hasSessionVariables)
+	{
+		root->hasSessionVariables = true;
+	}
+	else
+	{
+		(void) query_tree_walker(query,
+								 pull_up_has_session_variables_walker,
+								 (void *) root, 0);
+	}
+}
+
+static bool
+pull_up_has_session_variables_walker(Node *node, PlannerInfo *root)
+{
+	if (node == NULL)
+		return false;
+	if (IsA(node, Query))
+	{
+		Query	   *query = (Query *) node;
+
+		if (query->hasSessionVariables)
+		{
+			root->hasSessionVariables = true;
+			return false;
+		}
+
+		/* recurse into subselects */
+		return query_tree_walker((Query *) node,
+								 pull_up_has_session_variables_walker,
+								 (void *) root, 0);
+	}
+	return expression_tree_walker(node, pull_up_has_session_variables_walker,
+								  (void *) root);
+}
+
 /*
  * set_indexonlyscan_references
  *		Do set_plan_references processing on an IndexOnlyScan
@@ -2141,6 +2187,10 @@ fix_expr_common(PlannerInfo *root, Node *node)
  * If it's a PARAM_MULTIEXPR, replace it with the appropriate Param from
  * root->multiexpr_params; otherwise no change is needed.
  * Just for paranoia's sake, we make a copy of the node in either case.
+ *
+ * If it's a PARAM_VARIABLE, then we collect used session variables in
+ * the list root->glob->sessionVariable.  Also, assign the parameter's
+ * "paramid" to the parameter's position in that list.
  */
 static Node *
 fix_param_node(PlannerInfo *root, Param *p)
@@ -2159,6 +2209,43 @@ fix_param_node(PlannerInfo *root, Param *p)
 			elog(ERROR, "unexpected PARAM_MULTIEXPR ID: %d", p->paramid);
 		return copyObject(list_nth(params, colno - 1));
 	}
+
+	if (p->paramkind == PARAM_VARIABLE)
+	{
+		int			n = 0;
+
+		/* we will modify object */
+		p = (Param *) copyObject(p);
+
+		/*
+		 * Now, we can actualize list of session variables, and we can
+		 * complete paramid parameter.
+		 */
+		foreach_node(Param, paramvar, root->glob->sessionVariables)
+		{
+			if (strcmp(paramvar->paramvarname, p->paramvarname) == 0)
+			{
+				p->paramid = paramvar->paramid;
+
+				return (Node *) p;
+			}
+
+			n += 1;
+		}
+
+		p->paramid = n;
+
+		/*
+		 * Because session variables are catalogless, we cannot to use plan
+		 * invalidation. Then we need to check type, typmod, collid any time,
+		 * when we load values of session variables to parameter's buffer.
+		 * For this purpose it is more easy to save complete Param node.
+		 */
+		root->glob->sessionVariables = lappend(root->glob->sessionVariables, p);
+
+		return (Node *) p;
+	}
+
 	return (Node *) copyObject(p);
 }
 
@@ -2220,7 +2307,9 @@ fix_alternative_subplan(PlannerInfo *root, AlternativeSubPlan *asplan,
  * replacing Aggref nodes that should be replaced by initplan output Params,
  * choosing the best implementation for AlternativeSubPlans,
  * looking up operator opcode info for OpExpr and related nodes,
- * and adding OIDs from regclass Const nodes into root->glob->relationOids.
+ * adding OIDs from regclass Const nodes into root->glob->relationOids,
+ * assigning paramvarid to PARAM_VARIABLE params, and collecting the
+ * of session variables in the root->glob->sessionVariables list.
  *
  * 'node': the expression to be modified
  * 'rtoffset': how much to increment varnos by
@@ -2242,7 +2331,8 @@ fix_scan_expr(PlannerInfo *root, Node *node, int rtoffset, double num_exec)
 		root->multiexpr_params != NIL ||
 		root->glob->lastPHId != 0 ||
 		root->minmax_aggs != NIL ||
-		root->hasAlternativeSubPlans)
+		root->hasAlternativeSubPlans ||
+		root->hasSessionVariables)
 	{
 		return fix_scan_expr_mutator(node, &context);
 	}
diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c
index 7581695647d..282bb7d5e25 100644
--- a/src/backend/optimizer/prep/prepjointree.c
+++ b/src/backend/optimizer/prep/prepjointree.c
@@ -1649,6 +1649,9 @@ pull_up_simple_subquery(PlannerInfo *root, Node *jtnode, RangeTblEntry *rte,
 	/* If subquery had any RLS conditions, now main query does too */
 	parse->hasRowSecurity |= subquery->hasRowSecurity;
 
+	/* if the subquery had session variables, the main query does too */
+	parse->hasSessionVariables |= subquery->hasSessionVariables;
+
 	/*
 	 * subquery won't be pulled up if it hasAggs, hasWindowFuncs, or
 	 * hasTargetSRFs, so no work needed on those flags
diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c
index bda4c4eb292..c6aa09a8d39 100644
--- a/src/backend/optimizer/util/clauses.c
+++ b/src/backend/optimizer/util/clauses.c
@@ -25,6 +25,7 @@
 #include "catalog/pg_operator.h"
 #include "catalog/pg_proc.h"
 #include "catalog/pg_type.h"
+#include "commands/session_variable.h"
 #include "executor/executor.h"
 #include "executor/functions.h"
 #include "funcapi.h"
@@ -947,6 +948,13 @@ max_parallel_hazard_walker(Node *node, max_parallel_hazard_context *context)
 		if (param->paramkind == PARAM_EXTERN)
 			return false;
 
+		/* we don't support passing session variables to workers */
+		if (param->paramkind == PARAM_VARIABLE)
+		{
+			if (max_parallel_hazard_test(PROPARALLEL_RESTRICTED, context))
+				return true;
+		}
+
 		if (param->paramkind != PARAM_EXEC ||
 			!list_member_int(context->safe_param_ids, param->paramid))
 		{
@@ -2405,6 +2413,7 @@ convert_saop_to_hashed_saop_walker(Node *node, void *context)
  *	  value of the Param.
  * 2. Fold stable, as well as immutable, functions to constants.
  * 3. Reduce PlaceHolderVar nodes to their contained expressions.
+ * 4. Current value of session variable can be used for estimation too.
  *--------------------
  */
 Node *
@@ -2531,6 +2540,29 @@ eval_const_expressions_mutator(Node *node,
 						}
 					}
 				}
+				else if (param->paramkind == PARAM_VARIABLE &&
+						 context->estimate)
+				{
+					int16		typLen;
+					bool		typByVal;
+					Datum		pval;
+					bool		isnull;
+
+					get_typlenbyval(param->paramtype, &typLen, &typByVal);
+
+					pval = GetSessionVariableWithTypecheck(param->paramvarname,
+														   param->paramtype,
+														   param->paramtypmod,
+														   &isnull);
+
+					return (Node *) makeConst(param->paramtype,
+											  param->paramtypmod,
+											  param->paramcollid,
+											  (int) typLen,
+											  pval,
+											  isnull,
+											  typByVal);
+				}
 
 				/*
 				 * Not replaceable, so just copy the Param (no need to
@@ -4903,7 +4935,8 @@ inline_function(Oid funcid, Oid result_type, Oid result_collid,
 		querytree->limitOffset ||
 		querytree->limitCount ||
 		querytree->setOperations ||
-		list_length(querytree->targetList) != 1)
+		(list_length(querytree->targetList) != 1) ||
+		querytree->hasSessionVariables)
 		goto fail;
 
 	/* If the function result is composite, resolve it */
diff --git a/src/backend/utils/fmgr/fmgr.c b/src/backend/utils/fmgr/fmgr.c
index 0fe63c6bb83..d26e744e296 100644
--- a/src/backend/utils/fmgr/fmgr.c
+++ b/src/backend/utils/fmgr/fmgr.c
@@ -1991,9 +1991,13 @@ get_call_expr_arg_stable(Node *expr, int argnum)
 	 */
 	if (IsA(arg, Const))
 		return true;
-	if (IsA(arg, Param) &&
-		((Param *) arg)->paramkind == PARAM_EXTERN)
-		return true;
+	if (IsA(arg, Param))
+	{
+		Param	   *p = (Param *) arg;
+
+		if (p->paramkind == PARAM_EXTERN || p->paramkind == PARAM_VARIABLE)
+			return true;
+	}
 
 	return false;
 }
diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h
index 96be968c3d4..3687490bcb1 100644
--- a/src/include/commands/session_variable.h
+++ b/src/include/commands/session_variable.h
@@ -22,6 +22,8 @@
 extern void CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt);
 extern void DropVariableByName(char *varname);
 
+extern Datum GetSessionVariableWithTypecheck(char *varname, Oid typid, int32 typmod, bool *isnull);
+
 extern void get_session_variable_type_typmod_collid(char *varname,
 													Oid *typid,
 													int32 *typmod,
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index 46a8655621d..97a00df8d1f 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -189,6 +189,9 @@ typedef struct PlannerGlobal
 	/* extension state */
 	void	  **extension_state pg_node_attr(read_write_ignore);
 	int			extension_state_allocated;
+
+	/* list of used session variables */
+	List	   *sessionVariables;
 } PlannerGlobal;
 
 /* macro for fetching the Plan associated with a SubPlan node */
@@ -547,6 +550,8 @@ struct PlannerInfo
 	bool		hasRecursion;
 	/* true if a planner extension may replan this subquery */
 	bool		assumeReplanning;
+	/* true if session variables were used */
+	bool		hasSessionVariables;
 
 	/*
 	 * The rangetable index for the RTE_GROUP RTE, or 0 if there is no
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index c4393a94321..61754ae4efc 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -158,6 +158,9 @@ typedef struct PlannedStmt
 	 */
 	List	   *extension_state;
 
+	/* PARAM_VARIABLE Params */
+	List	   *sessionVariables;
+
 	/* statement location in source string (copied from Query) */
 	/* start location, or -1 if unknown */
 	ParseLoc	stmt_location;
diff --git a/src/include/optimizer/planmain.h b/src/include/optimizer/planmain.h
index 00addf15992..fb81ceb375f 100644
--- a/src/include/optimizer/planmain.h
+++ b/src/include/optimizer/planmain.h
@@ -132,4 +132,6 @@ extern void record_plan_function_dependency(PlannerInfo *root, Oid funcid);
 extern void record_plan_type_dependency(PlannerInfo *root, Oid typid);
 extern bool extract_query_dependencies_walker(Node *node, PlannerInfo *context);
 
+extern void pull_up_has_session_variables(PlannerInfo *root);
+
 #endif							/* PLANMAIN_H */
-- 
2.52.0

