From c0dfeca675283aec80768d5cb82bc8dbcff50b13 Mon Sep 17 00:00:00 2001
From: Tom Lane <tgl@sss.pgh.pa.us>
Date: Fri, 14 Mar 2025 16:14:51 -0400
Subject: [PATCH v8 3/4] Introduce SQL functions plan cache

---
 src/backend/executor/functions.c              | 657 ++++++++++++++----
 .../expected/test_extensions.out              |   2 +-
 src/tools/pgindent/typedefs.list              |   2 +
 3 files changed, 526 insertions(+), 135 deletions(-)

diff --git a/src/backend/executor/functions.c b/src/backend/executor/functions.c
index ae0425c050a..678ca55a026 100644
--- a/src/backend/executor/functions.c
+++ b/src/backend/executor/functions.c
@@ -18,6 +18,8 @@
 #include "access/xact.h"
 #include "catalog/pg_proc.h"
 #include "catalog/pg_type.h"
+#include "commands/trigger.h"
+#include "commands/event_trigger.h"
 #include "executor/functions.h"
 #include "funcapi.h"
 #include "miscadmin.h"
@@ -138,6 +140,46 @@ typedef struct
 
 typedef SQLFunctionCache *SQLFunctionCachePtr;
 
+/*
+ * Plan cache-related structures
+ */
+typedef struct SQLFunctionPlanKey
+{
+	Oid			fn_oid;
+	Oid			inputCollation;
+	Oid			argtypes[FUNC_MAX_ARGS];
+} SQLFunctionPlanKey;
+
+typedef struct SQLFunctionPlanEntry
+{
+	SQLFunctionPlanKey key;
+
+	/* Fields required to invalidate a cache entry */
+	TransactionId fn_xmin;
+	ItemPointerData fn_tid;
+
+	/*
+	 * result_tlist is required to recreate function execution state as well
+	 * as to validate a cache entry
+	 */
+	List	   *result_tlist;
+
+	bool		returnsTuple;	/* True if this function returns tuple */
+	List	   *plansource_list;	/* List of CachedPlanSource for this
+									 * function */
+
+	/*
+	 * SQLFunctionParseInfoPtr is used as hooks arguments, so should persist
+	 * across calls. Fortunately, if it doesn't, this means that argtypes or
+	 * collation mismatches and we get new cache entry.
+	 */
+	SQLFunctionParseInfoPtr pinfo;	/* cached information about arguments */
+
+	MemoryContext entry_ctx;	/* memory context for allocated fields of this
+								 * entry */
+} SQLFunctionPlanEntry;
+
+static HTAB *sql_plan_cache_htab = NULL;
 
 /* non-export function prototypes */
 static Node *sql_fn_param_ref(ParseState *pstate, ParamRef *pref);
@@ -171,6 +213,48 @@ static bool sqlfunction_receive(TupleTableSlot *slot, DestReceiver *self);
 static void sqlfunction_shutdown(DestReceiver *self);
 static void sqlfunction_destroy(DestReceiver *self);
 
+/* SQL-functions plan cache-related routines */
+static void compute_plan_entry_key(SQLFunctionPlanKey *hashkey, FunctionCallInfo fcinfo, Form_pg_proc procedureStruct);
+static SQLFunctionPlanEntry *get_cached_plan_entry(SQLFunctionPlanKey *hashkey);
+static void save_cached_plan_entry(SQLFunctionPlanKey *hashkey, HeapTuple procedureTuple, List *plansource_list, List *result_tlist, bool returnsTuple, SQLFunctionParseInfoPtr pinfo, MemoryContext alianable_context);
+static void delete_cached_plan_entry(SQLFunctionPlanEntry *entry);
+
+static bool check_sql_fn_retval_matches(List *tlist, Oid rettype, TupleDesc rettupdesc, char prokind);
+static bool target_entry_has_compatible_type(TargetEntry *tle, Oid res_type, int32 res_typmod);
+
+/*
+ * Fill array of arguments with actual function argument types oids
+ */
+static void
+compute_argument_types(Oid *argOidVect, Form_pg_proc procedureStruct, Node *call_expr)
+{
+	int			argnum;
+	int			nargs;
+
+	nargs = procedureStruct->pronargs;
+	if (nargs > 0)
+	{
+		memcpy(argOidVect,
+			   procedureStruct->proargtypes.values,
+			   nargs * sizeof(Oid));
+
+		for (argnum = 0; argnum < nargs; argnum++)
+		{
+			Oid			argtype = argOidVect[argnum];
+
+			if (IsPolymorphicType(argtype))
+			{
+				argtype = get_call_expr_argtype(call_expr, argnum);
+				if (argtype == InvalidOid)
+					ereport(ERROR,
+							(errcode(ERRCODE_DATATYPE_MISMATCH),
+							 errmsg("could not determine actual type of argument declared %s",
+									format_type_be(argOidVect[argnum]))));
+				argOidVect[argnum] = argtype;
+			}
+		}
+	}
+}
 
 /*
  * Prepare the SQLFunctionParseInfo struct for parsing a SQL function body
@@ -204,31 +288,8 @@ prepare_sql_fn_parse_info(HeapTuple procedureTuple,
 	pinfo->nargs = nargs = procedureStruct->pronargs;
 	if (nargs > 0)
 	{
-		Oid		   *argOidVect;
-		int			argnum;
-
-		argOidVect = (Oid *) palloc(nargs * sizeof(Oid));
-		memcpy(argOidVect,
-			   procedureStruct->proargtypes.values,
-			   nargs * sizeof(Oid));
-
-		for (argnum = 0; argnum < nargs; argnum++)
-		{
-			Oid			argtype = argOidVect[argnum];
-
-			if (IsPolymorphicType(argtype))
-			{
-				argtype = get_call_expr_argtype(call_expr, argnum);
-				if (argtype == InvalidOid)
-					ereport(ERROR,
-							(errcode(ERRCODE_DATATYPE_MISMATCH),
-							 errmsg("could not determine actual type of argument declared %s",
-									format_type_be(argOidVect[argnum]))));
-				argOidVect[argnum] = argtype;
-			}
-		}
-
-		pinfo->argtypes = argOidVect;
+		pinfo->argtypes = (Oid *) palloc(nargs * sizeof(Oid));
+		compute_argument_types(pinfo->argtypes, procedureStruct, call_expr);
 	}
 
 	/*
@@ -596,6 +657,264 @@ init_execution_state(SQLFunctionCachePtr fcache,
 	return eslist;
 }
 
+/*
+ * Compute key for searching plan entry in backend cache
+ */
+static void
+compute_plan_entry_key(SQLFunctionPlanKey *hashkey, FunctionCallInfo fcinfo, Form_pg_proc procedureStruct)
+{
+	MemSet(hashkey, 0, sizeof(SQLFunctionPlanKey));
+
+	hashkey->fn_oid = fcinfo->flinfo->fn_oid;
+
+	/* set input collation, if known */
+	hashkey->inputCollation = fcinfo->fncollation;
+
+	if (procedureStruct->pronargs > 0)
+	{
+		/* get the argument types */
+		compute_argument_types(hashkey->argtypes, procedureStruct, fcinfo->flinfo->fn_expr);
+	}
+}
+
+/*
+ * Get cached plan by pre-computed key
+ */
+static SQLFunctionPlanEntry *
+get_cached_plan_entry(SQLFunctionPlanKey *hashkey)
+{
+	SQLFunctionPlanEntry *plan_entry = NULL;
+
+	if (sql_plan_cache_htab)
+	{
+		plan_entry = (SQLFunctionPlanEntry *) hash_search(sql_plan_cache_htab,
+														  hashkey,
+														  HASH_FIND,
+														  NULL);
+	}
+	return plan_entry;
+}
+
+/*
+ * Save function execution plan in cache
+ */
+static void
+save_cached_plan_entry(SQLFunctionPlanKey *hashkey, HeapTuple procedureTuple, List *plansource_list, List *result_tlist, bool returnsTuple, SQLFunctionParseInfoPtr pinfo, MemoryContext alianable_context)
+{
+	MemoryContext oldcontext;
+	MemoryContext entry_context;
+	SQLFunctionPlanEntry *entry;
+	ListCell   *lc;
+	bool		found;
+
+	if (sql_plan_cache_htab == NULL)
+	{
+		HASHCTL		ctl;
+
+		ctl.keysize = sizeof(SQLFunctionPlanKey);
+		ctl.entrysize = sizeof(SQLFunctionPlanEntry);
+		ctl.hcxt = CacheMemoryContext;
+
+		sql_plan_cache_htab = hash_create("SQL function plan hash",
+										  100 /* arbitrary initial size */ ,
+										  &ctl,
+										  HASH_ELEM | HASH_BLOBS | HASH_CONTEXT);
+	}
+
+	entry = (SQLFunctionPlanEntry *) hash_search(sql_plan_cache_htab,
+												 hashkey,
+												 HASH_ENTER,
+												 &found);
+	if (found)
+		elog(WARNING, "trying to insert a function that already exists");
+
+	/*
+	 * Create long-lived memory context that holds entry fields
+	 */
+	entry_context = AllocSetContextCreate(CacheMemoryContext,
+										  "SQL function plan entry context",
+										  ALLOCSET_DEFAULT_SIZES);
+
+	oldcontext = MemoryContextSwitchTo(entry_context);
+
+	/* fill entry */
+	memcpy(&entry->key, hashkey, sizeof(SQLFunctionPlanKey));
+
+	entry->entry_ctx = entry_context;
+
+	/* Some generated data, like pinfo, should be reparented */
+	MemoryContextSetParent(alianable_context, entry->entry_ctx);
+
+	entry->pinfo = pinfo;
+
+	/* Preserve list in long-lived context */
+	if (plansource_list)
+		entry->plansource_list = list_copy(plansource_list);
+	else
+		entry->plansource_list = NULL;
+
+	entry->result_tlist = copyObject(result_tlist);
+
+	entry->returnsTuple = returnsTuple;
+
+	/* Fill fields needed to invalidate cache entry */
+	entry->fn_xmin = HeapTupleHeaderGetRawXmin(procedureTuple->t_data);
+	entry->fn_tid = procedureTuple->t_self;
+
+	/* Save plans */
+	foreach(lc, entry->plansource_list)
+	{
+		CachedPlanSource *plansource = (CachedPlanSource *) lfirst(lc);
+
+		SaveCachedPlan(plansource);
+	}
+	MemoryContextSwitchTo(oldcontext);
+
+}
+
+/*
+ * Remove plan from cache
+ */
+static void
+delete_cached_plan_entry(SQLFunctionPlanEntry *entry)
+{
+	ListCell   *lc;
+	bool		found;
+
+	/* Release plans */
+	foreach(lc, entry->plansource_list)
+	{
+		CachedPlanSource *plansource = (CachedPlanSource *) lfirst(lc);
+
+		DropCachedPlan(plansource);
+	}
+	MemoryContextDelete(entry->entry_ctx);
+
+	hash_search(sql_plan_cache_htab, &entry->key, HASH_REMOVE, &found);
+	Assert(found);
+}
+
+/*
+ * Determine if TargetEntry is compatible to specified type
+ */
+static bool
+target_entry_has_compatible_type(TargetEntry *tle, Oid res_type, int32 res_typmod)
+{
+	Var		   *var;
+	Node	   *cast_result;
+	bool		result = true;
+
+	/* Are types equivalent? */
+	var = makeVarFromTargetEntry(1, tle);
+
+	cast_result = coerce_to_target_type(NULL,
+										(Node *) var,
+										var->vartype,
+										res_type, res_typmod,
+										COERCION_ASSIGNMENT,
+										COERCE_IMPLICIT_CAST,
+										-1);
+
+	/*
+	 * If conversion is not possible or requires a cast, entry is incompatible
+	 * with the type.
+	 */
+	if (cast_result == NULL || cast_result != (Node *) var)
+		result = false;
+
+	if (cast_result && cast_result != (Node *) var)
+		pfree(cast_result);
+	pfree(var);
+
+	return result;
+}
+
+/*
+ * Check if result tlist would be changed by check_sql_fn_retval()
+ */
+static bool
+check_sql_fn_retval_matches(List *tlist, Oid rettype, TupleDesc rettupdesc, char prokind)
+{
+	char		fn_typtype;
+	int			tlistlen;
+
+	/*
+	 * Count the non-junk entries in the result targetlist.
+	 */
+	tlistlen = ExecCleanTargetListLength(tlist);
+
+	fn_typtype = get_typtype(rettype);
+
+	if (fn_typtype == TYPTYPE_BASE ||
+		fn_typtype == TYPTYPE_DOMAIN ||
+		fn_typtype == TYPTYPE_ENUM ||
+		fn_typtype == TYPTYPE_RANGE ||
+		fn_typtype == TYPTYPE_MULTIRANGE)
+	{
+		TargetEntry *tle;
+
+		/* Something unexpected, invalidate cached plan */
+		if (tlistlen != 1)
+			return false;
+
+		tle = (TargetEntry *) linitial(tlist);
+
+		return target_entry_has_compatible_type(tle, rettype, -1);
+	}
+	else if (fn_typtype == TYPTYPE_COMPOSITE || rettype == RECORDOID)
+	{
+		ListCell   *lc;
+		int			colindex;
+		int			tupnatts;
+
+		if (tlistlen == 1 && prokind != PROKIND_PROCEDURE)
+		{
+			TargetEntry *tle = (TargetEntry *) linitial(tlist);
+
+			return target_entry_has_compatible_type(tle, rettype, -1);
+		}
+
+		/* We consider results comnpatible if there's no tupledesc */
+		if (rettupdesc == NULL)
+			return true;
+
+		/*
+		 * Verify that saved targetlist matches the return tuple type.
+		 */
+		tupnatts = rettupdesc->natts;
+		colindex = 0;
+		foreach(lc, tlist)
+		{
+			TargetEntry *tle = (TargetEntry *) lfirst(lc);
+			Form_pg_attribute attr;
+
+			/* resjunk columns can simply be ignored */
+			if (tle->resjunk)
+				continue;
+
+			do
+			{
+				colindex++;
+				if (colindex > tupnatts)
+					return false;
+
+				attr = TupleDescAttr(rettupdesc, colindex - 1);
+			} while (attr->attisdropped);
+
+			if (!target_entry_has_compatible_type(tle, attr->atttypid, attr->atttypmod))
+				return false;
+		}
+
+		/* remaining columns in rettupdesc had better all be dropped */
+		for (colindex++; colindex <= tupnatts; colindex++)
+		{
+			if (!TupleDescCompactAttr(rettupdesc, colindex - 1)->attisdropped)
+				return false;
+		}
+	}
+	return true;
+}
+
 /*
  * Initialize the SQLFunctionCache for a SQL function
  */
@@ -617,6 +936,10 @@ init_sql_fcache(FunctionCallInfo fcinfo, Oid collation, bool *lazyEvalOK)
 	Datum		tmp;
 	bool		isNull;
 	List	   *plansource_list;
+	SQLFunctionPlanEntry *cached_plan_entry = NULL;
+	SQLFunctionPlanKey plan_cache_entry_key;
+	bool		use_plan_cache;
+	bool		plan_cache_entry_valid;
 
 	/*
 	 * Create memory context that holds all the SQLFunctionCache data.  It
@@ -674,15 +997,6 @@ init_sql_fcache(FunctionCallInfo fcinfo, Oid collation, bool *lazyEvalOK)
 	fcache->readonly_func =
 		(procedureStruct->provolatile != PROVOLATILE_VOLATILE);
 
-	/*
-	 * We need the actual argument types to pass to the parser.  Also make
-	 * sure that parameter symbols are considered to have the function's
-	 * resolved input collation.
-	 */
-	fcache->pinfo = prepare_sql_fn_parse_info(procedureTuple,
-											  finfo->fn_expr,
-											  collation);
-
 	/*
 	 * And of course we need the function body text.
 	 */
@@ -695,122 +1009,200 @@ init_sql_fcache(FunctionCallInfo fcinfo, Oid collation, bool *lazyEvalOK)
 						  Anum_pg_proc_prosqlbody,
 						  &isNull);
 
+
+	use_plan_cache = true;
+	plan_cache_entry_valid = false;
+
 	/*
-	 * Parse and rewrite the queries in the function text.  Use sublists to
-	 * keep track of the original query boundaries.
-	 *
-	 * Note: since parsing and planning is done in fcontext, we will generate
-	 * a lot of cruft that lives as long as the fcache does.  This is annoying
-	 * but we'll not worry about it until the module is rewritten to use
-	 * plancache.c.
+	 * If function is trigger, we can see different rowtypes or transition
+	 * table names.  So don't use cache for such plans.
 	 */
-	queryTree_list = NIL;
-	plansource_list = NIL;
-	if (!isNull)
-	{
-		Node	   *n;
-		List	   *stored_query_list;
+	if (CALLED_AS_TRIGGER(fcinfo) || CALLED_AS_EVENT_TRIGGER(fcinfo))
+		use_plan_cache = false;
 
-		n = stringToNode(TextDatumGetCString(tmp));
-		if (IsA(n, List))
-			stored_query_list = linitial_node(List, castNode(List, n));
-		else
-			stored_query_list = list_make1(n);
+	if (use_plan_cache)
+	{
+		compute_plan_entry_key(&plan_cache_entry_key, fcinfo, procedureStruct);
 
-		foreach(lc, stored_query_list)
+		cached_plan_entry = get_cached_plan_entry(&plan_cache_entry_key);
+		if (cached_plan_entry)
 		{
-			Query	   *parsetree = lfirst_node(Query, lc);
-			List	   *queryTree_sublist;
-			CachedPlanSource *plansource;
-
-			AcquireRewriteLocks(parsetree, true, false);
-
-			plansource = CreateCachedPlanForQuery(parsetree, fcache->src, CreateCommandTag((Node *) parsetree));
-			plansource_list = lappend(plansource_list, plansource);
-
-			queryTree_sublist = pg_rewrite_query(parsetree);
-			queryTree_list = lappend(queryTree_list, queryTree_sublist);
+			if (cached_plan_entry->fn_xmin == HeapTupleHeaderGetRawXmin(procedureTuple->t_data) &&
+				ItemPointerEquals(&cached_plan_entry->fn_tid, &procedureTuple->t_self))
+			{
+				/*
+				 * Avoid using plan if returned result type doesn't match the
+				 * expected one. check_sql_fn_retval() in this case would
+				 * change query to match expected result type. But we've
+				 * already planned query, possibly modified to match another
+				 * result type. So discard the cached entry and replan.
+				 */
+				if (check_sql_fn_retval_matches(cached_plan_entry->result_tlist, rettype, rettupdesc, procedureStruct->prokind))
+					plan_cache_entry_valid = true;
+			}
+			if (!plan_cache_entry_valid)
+				delete_cached_plan_entry(cached_plan_entry);
 		}
 	}
+
+	if (plan_cache_entry_valid)
+	{
+		plansource_list = cached_plan_entry->plansource_list;
+		resulttlist = copyObject(cached_plan_entry->result_tlist);
+		fcache->returnsTuple = cached_plan_entry->returnsTuple;
+		fcache->pinfo = cached_plan_entry->pinfo;
+	}
 	else
 	{
-		List	   *raw_parsetree_list;
+		MemoryContext alianable_context = fcontext;
+
+		/* We need to preserve parse info */
+		if (use_plan_cache)
+		{
+			alianable_context = AllocSetContextCreate(CurrentMemoryContext,
+													  "SQL function plan entry alianable context",
+													  ALLOCSET_DEFAULT_SIZES);
+
+			MemoryContextSwitchTo(alianable_context);
+		}
 
-		raw_parsetree_list = pg_parse_query(fcache->src);
+		/*
+		 * We need the actual argument types to pass to the parser.  Also make
+		 * sure that parameter symbols are considered to have the function's
+		 * resolved input collation.
+		 */
+		fcache->pinfo = prepare_sql_fn_parse_info(procedureTuple,
+												  finfo->fn_expr,
+												  collation);
+
+		if (use_plan_cache)
+			MemoryContextSwitchTo(fcontext);
 
-		foreach(lc, raw_parsetree_list)
+		/*
+		 * Parse and rewrite the queries in the function text.  Use sublists
+		 * to keep track of the original query boundaries.
+		 *
+		 * Note: since parsing and planning is done in fcontext, we will
+		 * generate a lot of cruft that lives as long as the fcache does. This
+		 * is annoying but we'll not worry about it until the module is
+		 * rewritten to use plancache.c.
+		 */
+
+		plansource_list = NIL;
+
+		queryTree_list = NIL;
+		if (!isNull)
 		{
-			RawStmt    *parsetree = lfirst_node(RawStmt, lc);
-			List	   *queryTree_sublist;
-			CachedPlanSource *plansource;
-
-			plansource = CreateCachedPlan(parsetree, fcache->src, CreateCommandTag(parsetree->stmt));
-			plansource_list = lappend(plansource_list, plansource);
-
-			queryTree_sublist = pg_analyze_and_rewrite_withcb(parsetree,
-															  fcache->src,
-															  (ParserSetupHook) sql_fn_parser_setup,
-															  fcache->pinfo,
-															  NULL);
-			queryTree_list = lappend(queryTree_list, queryTree_sublist);
+			Node	   *n;
+			List	   *stored_query_list;
+
+			n = stringToNode(TextDatumGetCString(tmp));
+			if (IsA(n, List))
+				stored_query_list = linitial_node(List, castNode(List, n));
+			else
+				stored_query_list = list_make1(n);
+
+			foreach(lc, stored_query_list)
+			{
+				Query	   *parsetree = lfirst_node(Query, lc);
+				List	   *queryTree_sublist;
+				CachedPlanSource *plansource;
+
+				AcquireRewriteLocks(parsetree, true, false);
+
+				plansource = CreateCachedPlanForQuery(parsetree, fcache->src, CreateCommandTag((Node *) parsetree));
+				plansource_list = lappend(plansource_list, plansource);
+
+				queryTree_sublist = pg_rewrite_query(parsetree);
+				queryTree_list = lappend(queryTree_list, queryTree_sublist);
+			}
 		}
-	}
+		else
+		{
+			List	   *raw_parsetree_list;
 
-	/*
-	 * Check that there are no statements we don't want to allow.
-	 */
-	check_sql_fn_statements(queryTree_list);
+			raw_parsetree_list = pg_parse_query(fcache->src);
 
-	/*
-	 * Check that the function returns the type it claims to.  Although in
-	 * simple cases this was already done when the function was defined, we
-	 * have to recheck because database objects used in the function's queries
-	 * might have changed type.  We'd have to recheck anyway if the function
-	 * had any polymorphic arguments.  Moreover, check_sql_fn_retval takes
-	 * care of injecting any required column type coercions.  (But we don't
-	 * ask it to insert nulls for dropped columns; the junkfilter handles
-	 * that.)
-	 *
-	 * Note: we set fcache->returnsTuple according to whether we are returning
-	 * the whole tuple result or just a single column.  In the latter case we
-	 * clear returnsTuple because we need not act different from the scalar
-	 * result case, even if it's a rowtype column.  (However, we have to force
-	 * lazy eval mode in that case; otherwise we'd need extra code to expand
-	 * the rowtype column into multiple columns, since we have no way to
-	 * notify the caller that it should do that.)
-	 */
-	fcache->returnsTuple = check_sql_fn_retval(queryTree_list,
-											   rettype,
-											   rettupdesc,
-											   procedureStruct->prokind,
-											   false,
-											   &resulttlist);
+			foreach(lc, raw_parsetree_list)
+			{
+				RawStmt    *parsetree = lfirst_node(RawStmt, lc);
+				List	   *queryTree_sublist;
+				CachedPlanSource *plansource;
+
+				plansource = CreateCachedPlan(parsetree, fcache->src, CreateCommandTag(parsetree->stmt));
+				plansource_list = lappend(plansource_list, plansource);
+
+				queryTree_sublist = pg_analyze_and_rewrite_withcb(parsetree,
+																  fcache->src,
+																  (ParserSetupHook) sql_fn_parser_setup,
+																  fcache->pinfo,
+																  NULL);
+				queryTree_list = lappend(queryTree_list, queryTree_sublist);
+			}
+		}
 
-	/*
-	 * Queries could be rewritten by check_sql_fn_retval(). Now when they have
-	 * their final form, we can complete plan cache entry creation.
-	 */
-	if (plansource_list != NIL)
-	{
-		ListCell   *qlc;
-		ListCell   *plc;
+		/*
+		 * Check that there are no statements we don't want to allow.
+		 */
+		check_sql_fn_statements(queryTree_list);
+
+		/*
+		 * Check that the function returns the type it claims to.  Although in
+		 * simple cases this was already done when the function was defined,
+		 * we have to recheck because database objects used in the function's
+		 * queries might have changed type.  We'd have to recheck anyway if
+		 * the function had any polymorphic arguments.  Moreover,
+		 * check_sql_fn_retval takes care of injecting any required column
+		 * type coercions.  (But we don't ask it to insert nulls for dropped
+		 * columns; the junkfilter handles that.)
+		 *
+		 * Note: we set fcache->returnsTuple according to whether we are
+		 * returning the whole tuple result or just a single column.  In the
+		 * latter case we clear returnsTuple because we need not act different
+		 * from the scalar result case, even if it's a rowtype column.
+		 * (However, we have to force lazy eval mode in that case; otherwise
+		 * we'd need extra code to expand the rowtype column into multiple
+		 * columns, since we have no way to notify the caller that it should
+		 * do that.)
+		 */
 
-		forboth(qlc, queryTree_list, plc, plansource_list)
+		fcache->returnsTuple = check_sql_fn_retval(queryTree_list,
+												   rettype,
+												   rettupdesc,
+												   procedureStruct->prokind,
+												   false,
+												   &resulttlist);
+
+		/*
+		 * Queries could be rewritten by check_sql_fn_retval(). Now when they
+		 * have their final form, we can complete plan cache entry creation.
+		 */
+		if (plansource_list != NIL)
 		{
-			List	   *queryTree_sublist = lfirst(qlc);
-			CachedPlanSource *plansource = lfirst(plc);
-
-			/* Finish filling in the CachedPlanSource */
-			CompleteCachedPlan(plansource,
-							   queryTree_sublist,
-							   NULL,
-							   NULL,
-							   0,
-							   (ParserSetupHook) sql_fn_parser_setup,
-							   fcache->pinfo,
-							   CURSOR_OPT_PARALLEL_OK | CURSOR_OPT_NO_SCROLL,
-							   false);
+			ListCell   *qlc;
+			ListCell   *plc;
+
+			forboth(qlc, queryTree_list, plc, plansource_list)
+			{
+				List	   *queryTree_sublist = lfirst(qlc);
+				CachedPlanSource *plansource = lfirst(plc);
+
+				/* Finish filling in the CachedPlanSource */
+				CompleteCachedPlan(plansource,
+								   queryTree_sublist,
+								   NULL,
+								   NULL,
+								   0,
+								   (ParserSetupHook) sql_fn_parser_setup,
+								   fcache->pinfo,
+								   CURSOR_OPT_PARALLEL_OK | CURSOR_OPT_NO_SCROLL,
+								   false);
+			}
 		}
+
+		/* If we can possibly use cached plan entry, save it. */
+		if (use_plan_cache)
+			save_cached_plan_entry(&plan_cache_entry_key, procedureTuple, plansource_list, resulttlist, fcache->returnsTuple, fcache->pinfo, alianable_context);
 	}
 
 	/*
@@ -1110,9 +1502,6 @@ release_plans(List *cplans)
 
 		ReleaseCachedPlan(cplan, cplan->is_saved ? CurrentResourceOwner : NULL);
 	}
-
-	/* Cleanup the list itself */
-	list_free(cplans);
 }
 
 /*
diff --git a/src/test/modules/test_extensions/expected/test_extensions.out b/src/test/modules/test_extensions/expected/test_extensions.out
index d5388a1fecf..72bae1bf254 100644
--- a/src/test/modules/test_extensions/expected/test_extensions.out
+++ b/src/test/modules/test_extensions/expected/test_extensions.out
@@ -651,7 +651,7 @@ LINE 1:  SELECT public.dep_req2() || ' req3b'
                 ^
 HINT:  No function matches the given name and argument types. You might need to add explicit type casts.
 QUERY:   SELECT public.dep_req2() || ' req3b' 
-CONTEXT:  SQL function "dep_req3b" during startup
+CONTEXT:  SQL function "dep_req3b" statement 1
 DROP EXTENSION test_ext_req_schema3;
 ALTER EXTENSION test_ext_req_schema1 SET SCHEMA test_s_dep2;  -- now ok
 SELECT test_s_dep2.dep_req1();
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 93339ef3c58..1671101cebb 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2577,6 +2577,8 @@ SQLFunctionCache
 SQLFunctionCachePtr
 SQLFunctionParseInfo
 SQLFunctionParseInfoPtr
+SQLFunctionPlanEntry
+SQLFunctionPlanKey
 SQLValueFunction
 SQLValueFunctionOp
 SSL
-- 
2.43.5

