From eef8d1af46ca8deefbf8eb95428d37fc900a0944 Mon Sep 17 00:00:00 2001
From: Amit Langote <amitlan@postgresql.org>
Date: Mon, 17 Nov 2025 17:40:26 +0900
Subject: [PATCH v2 5/5] Make SQL function executor track ExecutorPrep state

Extend the SQL function executor to use the ExecutorPrep results
returned by GetCachedPlan().  init_execution_state() now passes a
CachedPlanPrepData to GetCachedPlan() and stores the per statement
ExecPrep pointers in the execution_state nodes.

At execution time, postquel_start() reparents the prep estate's
es_query_cxt under the function's subcontext so that prep state
follows the usual per call context hierarchy.

This allows SQL language functions to participate in the same
ExecutorPrep machinery as other plan cache users, which a later
patch will use to support pruning aware locking.

Add a regression test where rule rewrite expands a single UPDATE
into multiple PlannedStmts, exercising the SQL function plan cache
and the generic plan reuse path that now invokes ExecutorPrep.
---
 src/backend/executor/functions.c        | 33 +++++++++++++++++++++++--
 src/test/regress/expected/plancache.out | 31 +++++++++++++++++++++++
 src/test/regress/sql/plancache.sql      | 29 ++++++++++++++++++++++
 3 files changed, 91 insertions(+), 2 deletions(-)

diff --git a/src/backend/executor/functions.c b/src/backend/executor/functions.c
index 633310c5f5b..ed7352fce61 100644
--- a/src/backend/executor/functions.c
+++ b/src/backend/executor/functions.c
@@ -72,6 +72,7 @@ typedef struct execution_state
 	bool		setsResult;		/* true if this query produces func's result */
 	bool		lazyEval;		/* true if should fetch one row at a time */
 	PlannedStmt *stmt;			/* plan for this query */
+	ExecPrep   *prep;			/* ExecutorPrep() output for this plan */
 	QueryDesc  *qd;				/* null unless status == RUN */
 } execution_state;
 
@@ -657,6 +658,8 @@ init_execution_state(SQLFunctionCachePtr fcache)
 	execution_state *lasttages = NULL;
 	int			nstmts;
 	ListCell   *lc;
+	CachedPlanPrepData cprep = {0};
+	int			i;
 
 	/*
 	 * Clean up after previous query, if there was one.
@@ -695,10 +698,20 @@ init_execution_state(SQLFunctionCachePtr fcache)
 	 * CurrentResourceOwner will be the same when ShutdownSQLFunction runs.)
 	 */
 	fcache->cowner = CurrentResourceOwner;
+
+	/*
+	 * Have ExecutorPrep() allocate under fcache->fcontext.  The prep
+	 * EStates it creates will initially live there; postquel_start()
+	 * will later reparent their es_query_cxt into fcache->subcontext
+	 * when using them for execution.
+	 */
+	cprep.context = fcache->fcontext;
+	cprep.owner = fcache->cowner;
 	fcache->cplan = GetCachedPlan(plansource,
 								  fcache->paramLI,
 								  fcache->cowner,
-								  NULL);
+								  NULL,
+								  &cprep);
 
 	/*
 	 * If necessary, make esarray[] bigger to hold the needed state.
@@ -719,9 +732,12 @@ init_execution_state(SQLFunctionCachePtr fcache)
 	/*
 	 * Build execution_state list to match the number of contained plans.
 	 */
+	i = 0;
 	foreach(lc, fcache->cplan->stmt_list)
 	{
 		PlannedStmt *stmt = lfirst_node(PlannedStmt, lc);
+		ExecPrep *prep = cprep.prep_list ? list_nth(cprep.prep_list, i++) :
+			NULL;
 		execution_state *newes;
 
 		/*
@@ -763,6 +779,7 @@ init_execution_state(SQLFunctionCachePtr fcache)
 		newes->setsResult = false;	/* might change below */
 		newes->lazyEval = false;	/* might change below */
 		newes->stmt = stmt;
+		newes->prep = prep;
 		newes->qd = NULL;
 
 		if (stmt->canSetTag)
@@ -1361,8 +1378,20 @@ postquel_start(execution_state *es, SQLFunctionCachePtr fcache)
 	else
 		dest = None_Receiver;
 
+	if (es->prep)
+	{
+		/*
+		 * Prep EStates were built under fcache->fcontext.  For execution,
+		 * make their es_query_cxt a child of fcache->subcontext so they
+		 * follow the usual per call lifetime.
+		 */
+		EState *prep_estate = es->prep->prep_estate;
+
+		MemoryContextSetParent(prep_estate->es_query_cxt, fcache->subcontext);
+	}
+
 	es->qd = CreateQueryDesc(es->stmt,
-							 NULL,
+							 es->prep,
 							 fcache->func->src,
 							 GetActiveSnapshot(),
 							 InvalidSnapshot,
diff --git a/src/test/regress/expected/plancache.out b/src/test/regress/expected/plancache.out
index 4e59188196c..8c68691df91 100644
--- a/src/test/regress/expected/plancache.out
+++ b/src/test/regress/expected/plancache.out
@@ -398,3 +398,34 @@ select name, generic_plans, custom_plans from pg_prepared_statements
 (1 row)
 
 drop table test_mode;
+-- exercise sql-function plan cache when rewrite expands a single statement
+-- into multiple planned statements. this forces cachedplan->stmt_list to
+-- contain more than one entry and checks that executor state for the first
+-- rewritten statement does not destroy state needed by the second one.
+set plan_cache_mode = force_generic_plan;
+create table sqlf_base(id int, val int);
+create table sqlf_log(id int, note text);
+insert into sqlf_base values (1, 10);
+create rule sqlf_base_upd_log as
+on update to sqlf_base do also
+    insert into sqlf_log(id, note)
+    values (new.id, 'logged by rule');
+create or replace function sqlf_execprep_test(a int, v int)
+returns void
+language sql
+as $$
+    update sqlf_base set val = v where id = a;
+$$;
+select sqlf_execprep_test(1, 20);
+ sqlf_execprep_test 
+--------------------
+ 
+(1 row)
+
+select sqlf_execprep_test(1, 30);
+ sqlf_execprep_test 
+--------------------
+ 
+(1 row)
+
+reset plan_cache_mode;
diff --git a/src/test/regress/sql/plancache.sql b/src/test/regress/sql/plancache.sql
index 4b2f11dcc64..56ebbbdecd2 100644
--- a/src/test/regress/sql/plancache.sql
+++ b/src/test/regress/sql/plancache.sql
@@ -223,3 +223,32 @@ select name, generic_plans, custom_plans from pg_prepared_statements
   where  name = 'test_mode_pp';
 
 drop table test_mode;
+
+-- exercise sql-function plan cache when rewrite expands a single statement
+-- into multiple planned statements. this forces cachedplan->stmt_list to
+-- contain more than one entry and checks that executor state for the first
+-- rewritten statement does not destroy state needed by the second one.
+
+set plan_cache_mode = force_generic_plan;
+
+create table sqlf_base(id int, val int);
+create table sqlf_log(id int, note text);
+
+insert into sqlf_base values (1, 10);
+
+create rule sqlf_base_upd_log as
+on update to sqlf_base do also
+    insert into sqlf_log(id, note)
+    values (new.id, 'logged by rule');
+
+create or replace function sqlf_execprep_test(a int, v int)
+returns void
+language sql
+as $$
+    update sqlf_base set val = v where id = a;
+$$;
+
+select sqlf_execprep_test(1, 20);
+select sqlf_execprep_test(1, 30);
+
+reset plan_cache_mode;
-- 
2.47.3

