From 65ab34a6dca8ddee3eca5fecf35b60deb2be4b61 Mon Sep 17 00:00:00 2001
From: Alexander Pyhalov <a.pyhalov@postgrespro.ru>
Date: Thu, 27 Mar 2025 09:00:58 +0300
Subject: [PATCH 2/3] Use cached plans machinery for SQL function

We create cached plans for SQL functions queries.
Plans are saved in session-level cache.
We don't save full SQLFunctionCache entry,
but only list of cached plans, some data, required
to recreate this entry and some additional fields,
necessary to validate cached plan.

Some tests were added to validate corner cases, including
sql functions plan invalidation and functions with statements,
which are completely eleminated by rewrite rules.
---
 src/backend/executor/functions.c              | 869 +++++++++++++++---
 .../expected/test_extensions.out              |   2 +-
 src/test/regress/expected/rowsecurity.out     |  51 +
 src/test/regress/expected/rules.out           |  35 +
 src/test/regress/sql/rowsecurity.sql          |  41 +
 src/test/regress/sql/rules.sql                |  24 +
 src/tools/pgindent/typedefs.list              |   2 +
 7 files changed, 875 insertions(+), 149 deletions(-)

diff --git a/src/backend/executor/functions.c b/src/backend/executor/functions.c
index 6aa8e9c4d8a..7d12ca126e6 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"
@@ -33,6 +35,7 @@
 #include "utils/datum.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
+#include "utils/plancache.h"
 #include "utils/snapmgr.h"
 #include "utils/syscache.h"
 
@@ -78,17 +81,26 @@ typedef struct execution_state
  * and linked to from the fn_extra field of the FmgrInfo struct.
  *
  * Note that currently this has only the lifespan of the calling query.
- * Someday we should rewrite this code to use plancache.c to save parse/plan
- * results for longer than that.
+ * When cached sql function plans are used, SQLFunctionCache entry
+ * is recreated from separate cached plan entry, which is represented
+ * by SQLFunctionPlanEntry. Plan cache entries are preserved in session-level
+ * cache (sql_plan_cache_htab).
  *
- * Physically, though, the data has the lifespan of the FmgrInfo that's used
- * to call the function, and there are cases (particularly with indexes)
+ * Physically, though, the data in SQLFunctionCache record itself has the
+ * lifespan of the FmgrInfo that's used to call the function, and there are
+ * cases (particularly with indexes)
  * where the FmgrInfo might survive across transactions.  We cannot assume
  * that the parse/plan trees are good for longer than the (sub)transaction in
  * which parsing was done, so we must mark the record with the LXID/subxid of
- * its creation time, and regenerate everything if that's obsolete.  To avoid
+ * its creation time, and recheck plans, if LXID/subxid is obsolete.  To avoid
  * memory leakage when we do have to regenerate things, all the data is kept
  * in a sub-context of the FmgrInfo's fn_mcxt.
+ *
+ * The only exception is SQLFunctionParseInfo. It should survive between
+ * function calls, as it's necessary if plan revalidation triggers replanning.
+ * It's safe to preserve it as long as function cache entry lives, as pinfo
+ * is determined by procedureTuple. If it's changed, function plan cache entry
+ * is invalidated together with SQLFunctionParseInfo.
  */
 typedef struct
 {
@@ -112,6 +124,12 @@ typedef struct
 
 	JunkFilter *junkFilter;		/* will be NULL if function returns VOID */
 
+	/* Cached plans support */
+	List	   *plansource_list;	/* list of plansource */
+	List	   *cplan_list;		/* list of cached plans */
+	int			planning_stmt_number;	/* the number of statement we are
+										 * currently planning */
+
 	/*
 	 * func_state is a List of execution_state records, each of which is the
 	 * first for its original parsetree, with any additional records chained
@@ -122,6 +140,8 @@ typedef struct
 
 	MemoryContext fcontext;		/* memory context holding this struct and all
 								 * subsidiary data */
+	MemoryContext planning_context; /* memory context which is used for
+									 * planning */
 
 	LocalTransactionId lxid;	/* lxid in which cache was made */
 	SubTransactionId subxid;	/* subxid in which cache was made */
@@ -129,6 +149,64 @@ typedef struct
 
 typedef SQLFunctionCache *SQLFunctionCachePtr;
 
+/*
+ * Plan cache-related structures
+ */
+
+/*
+ * SQLFunctionPlanKey groups data which is used to search for
+ * function plan in plan cache. Each cached plan entry
+ * is identified by function oid, it's argument types
+ * and input collation.
+ */
+typedef struct SQLFunctionPlanKey
+{
+	Oid			fn_oid;
+	Oid			inputCollation;
+	Oid			argtypes[FUNC_MAX_ARGS];
+} SQLFunctionPlanKey;
+
+/*
+ * Session-level sql function plan cache entry.
+ */
+typedef struct SQLFunctionPlanEntry
+{
+	SQLFunctionPlanKey key;
+
+	/* Fields required to invalidate a cache entry */
+	TransactionId fn_xmin;		/* xmin/TID of procedure's pg_proc tuple */
+	ItemPointerData fn_tid;
+
+	/*
+	 * Fields required to recreate SQLFunctionCache data for cached functions
+	 * plans
+	 */
+
+	/*
+	 * Function expected result tlist. It is required to recreate function
+	 * execution state as well as to validate a cache entry. It cannot be
+	 * directly inferred from function argument types, as function can be
+	 * modified to match expected resut type.
+	 */
+	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;
+
+/* SQL functions plan cache */
+static HTAB *sql_plan_cache_htab = NULL;
 
 /* non-export function prototypes */
 static Node *sql_fn_param_ref(ParseState *pstate, ParamRef *pref);
@@ -138,10 +216,9 @@ static Node *sql_fn_make_param(SQLFunctionParseInfoPtr pinfo,
 							   int paramno, int location);
 static Node *sql_fn_resolve_param_name(SQLFunctionParseInfoPtr pinfo,
 									   const char *paramname, int location);
-static List *init_execution_state(List *queryTree_list,
-								  SQLFunctionCachePtr fcache,
+static List *init_execution_state(SQLFunctionCachePtr fcache,
 								  bool lazyEvalOK);
-static void init_sql_fcache(FunctionCallInfo fcinfo, Oid collation, bool lazyEvalOK);
+static void init_sql_fcache(FunctionCallInfo fcinfo, Oid collation, bool *lazyEvalOK);
 static void postquel_start(execution_state *es, SQLFunctionCachePtr fcache);
 static bool postquel_getnext(execution_state *es, SQLFunctionCachePtr fcache);
 static void postquel_end(execution_state *es);
@@ -163,6 +240,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
@@ -196,31 +315,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);
 	}
 
 	/*
@@ -457,49 +553,56 @@ sql_fn_resolve_param_name(SQLFunctionParseInfoPtr pinfo,
 /*
  * Set up the per-query execution_state records for a SQL function.
  *
- * The input is a List of Lists of parsed and rewritten, but not planned,
- * querytrees.  The sublist structure denotes the original query boundaries.
+ * The input is a sql function cache record and a flag which specifies
+ * if lazy evaluation is allowed.
  */
 static List *
-init_execution_state(List *queryTree_list,
-					 SQLFunctionCachePtr fcache,
+init_execution_state(SQLFunctionCachePtr fcache,
 					 bool lazyEvalOK)
 {
 	List	   *eslist = NIL;
+	List	   *cplan_list = NIL;
 	execution_state *lasttages = NULL;
 	ListCell   *lc1;
+	MemoryContext oldcontext;
+
+	/*
+	 * Invalidate func_state prior to resetting - otherwise error callback can
+	 * access it
+	 */
+	fcache->func_state = NIL;
+	MemoryContextReset(fcache->planning_context);
+
+	oldcontext = MemoryContextSwitchTo(fcache->planning_context);
 
-	foreach(lc1, queryTree_list)
+	foreach(lc1, fcache->plansource_list)
 	{
-		List	   *qtlist = lfirst_node(List, lc1);
+		CachedPlanSource *plansource = (CachedPlanSource *) lfirst(lc1);
 		execution_state *firstes = NULL;
 		execution_state *preves = NULL;
 		ListCell   *lc2;
+		CachedPlan *cplan;
+
+		/* Save statement number for error reporting */
+		fcache->planning_stmt_number = foreach_current_index(lc1) + 1;
 
-		foreach(lc2, qtlist)
+		/*
+		 * Get plan for the query. If paramLI is set, we can get custom plan
+		 */
+		cplan = GetCachedPlan(plansource,
+							  fcache->paramLI,
+							  plansource->is_saved ? CurrentResourceOwner : NULL,
+							  NULL);
+
+		/* Record cplan in plan list to be released on replanning */
+		cplan_list = lappend(cplan_list, cplan);
+
+		/* For each planned statement create execution state */
+		foreach(lc2, cplan->stmt_list)
 		{
-			Query	   *queryTree = lfirst_node(Query, lc2);
-			PlannedStmt *stmt;
+			PlannedStmt *stmt = lfirst_node(PlannedStmt, lc2);
 			execution_state *newes;
 
-			/* Plan the query if needed */
-			if (queryTree->commandType == CMD_UTILITY)
-			{
-				/* Utility commands require no planning. */
-				stmt = makeNode(PlannedStmt);
-				stmt->commandType = CMD_UTILITY;
-				stmt->canSetTag = queryTree->canSetTag;
-				stmt->utilityStmt = queryTree->utilityStmt;
-				stmt->stmt_location = queryTree->stmt_location;
-				stmt->stmt_len = queryTree->stmt_len;
-				stmt->queryId = queryTree->queryId;
-			}
-			else
-				stmt = pg_plan_query(queryTree,
-									 fcache->src,
-									 CURSOR_OPT_PARALLEL_OK,
-									 NULL);
-
 			/*
 			 * Precheck all commands for validity in a function.  This should
 			 * generally match the restrictions spi.c applies.
@@ -541,7 +644,7 @@ init_execution_state(List *queryTree_list,
 			newes->stmt = stmt;
 			newes->qd = NULL;
 
-			if (queryTree->canSetTag)
+			if (stmt->canSetTag)
 				lasttages = newes;
 
 			preves = newes;
@@ -573,14 +676,290 @@ init_execution_state(List *queryTree_list,
 			fcache->lazyEval = lasttages->lazyEval = true;
 	}
 
+	/* We've finished planning, reset planning statement number */
+	fcache->planning_stmt_number = 0;
+	fcache->cplan_list = cplan_list;
+
+	MemoryContextSwitchTo(oldcontext);
 	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;
+
+		/* Initialize SQL functions plan cache if it doesn't exist */
+		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);
+
+	/*
+	 * In practice, shouldn't happen, as in init_sql_fcache() if we find
+	 * entry, which can't be used, it's removed from cache.
+	 */
+	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
+ *
+ * We don't care much about plan revalidations, happening during
+ * recursive calls, as consider this impossible due to the fact
+ * that SQL function execution is atomic. If function definition is
+ * changed during SQL function execution, backend, in which function
+ * is executed, will not see the effects of the change while
+ * function is executing.
+ */
+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
  */
 static void
-init_sql_fcache(FunctionCallInfo fcinfo, Oid collation, bool lazyEvalOK)
+init_sql_fcache(FunctionCallInfo fcinfo, Oid collation, bool *lazyEvalOK)
 {
 	FmgrInfo   *finfo = fcinfo->flinfo;
 	Oid			foid = finfo->fn_oid;
@@ -596,6 +975,11 @@ init_sql_fcache(FunctionCallInfo fcinfo, Oid collation, bool lazyEvalOK)
 	ListCell   *lc;
 	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
@@ -614,6 +998,10 @@ init_sql_fcache(FunctionCallInfo fcinfo, Oid collation, bool lazyEvalOK)
 	 */
 	fcache = (SQLFunctionCachePtr) palloc0(sizeof(SQLFunctionCache));
 	fcache->fcontext = fcontext;
+	/* Create separate context for planning */
+	fcache->planning_context = AllocSetContextCreate(fcache->fcontext,
+													 "SQL language functions planning context",
+													 ALLOCSET_SMALL_SIZES);
 	finfo->fn_extra = fcache;
 
 	/*
@@ -649,15 +1037,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.
 	 */
@@ -670,86 +1049,210 @@ 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;
-	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 we decided that we can use cached plans, lookup and verify saved
+	 * plan
+	 */
+	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;
-
-			AcquireRewriteLocks(parsetree, true, false);
-			queryTree_sublist = pg_rewrite_query(parsetree);
-			queryTree_list = lappend(queryTree_list, queryTree_sublist);
+			/* Can't use plan entry if function definition has been changed */
+			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 we have valid saved plan cache entry, just use it, otherwise we have
+	 * to create plan from scratch.
+	 */
+	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 in long-lived context */
+		if (use_plan_cache)
+		{
+			alianable_context = AllocSetContextCreate(CurrentMemoryContext,
+													  "SQL function plan entry alianable context",
+													  ALLOCSET_DEFAULT_SIZES);
+
+			MemoryContextSwitchTo(alianable_context);
+		}
+
+		/*
+		 * 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);
+
+		/*
+		 * 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)
+		{
+			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);
 
-		raw_parsetree_list = pg_parse_query(fcache->src);
+				plansource = CreateCachedPlanForQuery(parsetree, fcache->src, CreateCommandTag((Node *) parsetree));
+				plansource_list = lappend(plansource_list, plansource);
 
-		foreach(lc, raw_parsetree_list)
+				queryTree_sublist = pg_rewrite_query(parsetree);
+				queryTree_list = lappend(queryTree_list, queryTree_sublist);
+			}
+		}
+		else
 		{
-			RawStmt    *parsetree = lfirst_node(RawStmt, lc);
-			List	   *queryTree_sublist;
-
-			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);
+			List	   *raw_parsetree_list;
+
+			raw_parsetree_list = pg_parse_query(fcache->src);
+
+			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);
+			}
 		}
-	}
 
-	/*
-	 * Check that there are no statements we don't want to allow.
-	 */
-	check_sql_fn_statements(queryTree_list);
+		/*
+		 * 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.)
-	 */
-	fcache->returnsTuple = check_sql_fn_retval(queryTree_list,
-											   rettype,
-											   rettupdesc,
-											   procedureStruct->prokind,
-											   false,
-											   &resulttlist);
+		/*
+		 * 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);
+
+		/*
+		 * 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;
+
+			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);
+	}
 
 	/*
 	 * Construct a JunkFilter we can use to coerce the returned rowtype to the
@@ -792,13 +1295,10 @@ init_sql_fcache(FunctionCallInfo fcinfo, Oid collation, bool lazyEvalOK)
 		 * materialize mode, but to add more smarts in init_execution_state
 		 * about this, we'd probably need a three-way flag instead of bool.
 		 */
-		lazyEvalOK = true;
+		*lazyEvalOK = true;
 	}
 
-	/* Finally, plan the queries */
-	fcache->func_state = init_execution_state(queryTree_list,
-											  fcache,
-											  lazyEvalOK);
+	fcache->plansource_list = plansource_list;
 
 	/* Mark fcache with time of creation to show it's valid */
 	fcache->lxid = MyProc->vxid.lxid;
@@ -971,7 +1471,12 @@ postquel_sub_params(SQLFunctionCachePtr fcache,
 			prm->value = MakeExpandedObjectReadOnly(fcinfo->args[i].value,
 													prm->isnull,
 													get_typlen(argtypes[i]));
-			prm->pflags = 0;
+
+			/*
+			 * PARAM_FLAG_CONST is necessary to build efficient custom plan.
+			 */
+			prm->pflags = PARAM_FLAG_CONST;
+
 			prm->ptype = argtypes[i];
 		}
 	}
@@ -1024,6 +1529,30 @@ postquel_get_single_result(TupleTableSlot *slot,
 	return value;
 }
 
+/*
+ * Release plans. This function is called prior to planning
+ * statements with new parameters. When custom plans are generated
+ * for each function call in a statement, they can consume too much memory, so
+ * release them. Generic plans will survive it as plansource holds
+ * reference to a generic plan.
+ */
+static void
+release_plans(List *cplans)
+{
+	ListCell   *lc;
+
+	/*
+	 * We support separate plan list, so that we visit each plan here only
+	 * once
+	 */
+	foreach(lc, cplans)
+	{
+		CachedPlan *cplan = lfirst(lc);
+
+		ReleaseCachedPlan(cplan, cplan->is_saved ? CurrentResourceOwner : NULL);
+	}
+}
+
 /*
  * fmgr_sql: function call manager for SQL functions
  */
@@ -1042,6 +1571,7 @@ fmgr_sql(PG_FUNCTION_ARGS)
 	Datum		result;
 	List	   *eslist;
 	ListCell   *eslc;
+	bool		build_cached_plans = false;
 
 	/*
 	 * Setup error traceback support for ereport()
@@ -1097,7 +1627,11 @@ fmgr_sql(PG_FUNCTION_ARGS)
 
 	if (fcache == NULL)
 	{
-		init_sql_fcache(fcinfo, PG_GET_COLLATION(), lazyEvalOK);
+		/*
+		 * init_sql_fcache() can set lazyEvalOK in additional cases when it
+		 * determines that materialize won't work.
+		 */
+		init_sql_fcache(fcinfo, PG_GET_COLLATION(), &lazyEvalOK);
 		fcache = (SQLFunctionCachePtr) fcinfo->flinfo->fn_extra;
 	}
 
@@ -1131,12 +1665,37 @@ fmgr_sql(PG_FUNCTION_ARGS)
 			break;
 	}
 
+	/*
+	 * We skip actual planning for initial run, so in this case we have to
+	 * build cached plans now.
+	 */
+	if (fcache->plansource_list != NIL && eslist == NIL)
+		build_cached_plans = true;
+
 	/*
 	 * Convert params to appropriate format if starting a fresh execution. (If
 	 * continuing execution, we can re-use prior params.)
 	 */
-	if (is_first && es && es->status == F_EXEC_START)
+	if ((is_first && es && es->status == F_EXEC_START) || build_cached_plans)
+	{
 		postquel_sub_params(fcache, fcinfo);
+		if (fcache->plansource_list)
+		{
+			/* replan the queries */
+			fcache->func_state = init_execution_state(fcache,
+													  lazyEvalOK);
+			/* restore execution state and eslist-related variables */
+			eslist = fcache->func_state;
+			/* find the first non-NULL execution state */
+			foreach(eslc, eslist)
+			{
+				es = (execution_state *) lfirst(eslc);
+
+				if (es)
+					break;
+			}
+		}
+	}
 
 	/*
 	 * Build tuplestore to hold results, if we don't have one already. Note
@@ -1391,6 +1950,10 @@ fmgr_sql(PG_FUNCTION_ARGS)
 				es = es->next;
 			}
 		}
+
+		/* Release plans when functions stops executing */
+		release_plans(fcache->cplan_list);
+		fcache->cplan_list = NULL;
 	}
 
 	error_context_stack = sqlerrcontext.previous;
@@ -1430,13 +1993,19 @@ sql_exec_error_callback(void *arg)
 	}
 
 	/*
-	 * Try to determine where in the function we failed.  If there is a query
-	 * with non-null QueryDesc, finger it.  (We check this rather than looking
-	 * for F_EXEC_RUN state, so that errors during ExecutorStart or
+	 * Try to determine where in the function we failed.  If failure happens
+	 * while building plans, look at planning_stmt_number.  Else if there is a
+	 * query with non-null QueryDesc, finger it.  (We check this rather than
+	 * looking for F_EXEC_RUN state, so that errors during ExecutorStart or
 	 * ExecutorEnd are blamed on the appropriate query; see postquel_start and
 	 * postquel_end.)
 	 */
-	if (fcache->func_state)
+	if (fcache->planning_stmt_number)
+	{
+		errcontext("SQL function \"%s\" statement %d",
+				   fcache->fname, fcache->planning_stmt_number);
+	}
+	else if (fcache->func_state)
 	{
 		execution_state *es;
 		int			query_num;
@@ -1522,6 +2091,10 @@ ShutdownSQLFunction(Datum arg)
 		tuplestore_end(fcache->tstore);
 	fcache->tstore = NULL;
 
+	/* Release plans when functions stops executing */
+	release_plans(fcache->cplan_list);
+	fcache->cplan_list = NULL;
+
 	/* execUtils will deregister the callback... */
 	fcache->shutdown_reg = false;
 }
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/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
index 87929191d06..438eaf69928 100644
--- a/src/test/regress/expected/rowsecurity.out
+++ b/src/test/regress/expected/rowsecurity.out
@@ -4695,6 +4695,57 @@ RESET ROLE;
 DROP FUNCTION rls_f();
 DROP VIEW rls_v;
 DROP TABLE rls_t;
+-- RLS changes invalidate cached function plans
+create table rls_t (c text);
+create table test_t (c text);
+insert into rls_t values ('a'), ('b'), ('c'), ('d');
+insert into test_t values ('a'), ('b');
+alter table rls_t enable row level security;
+grant select on rls_t to regress_rls_alice;
+grant select on test_t to regress_rls_alice;
+create policy p1 on rls_t for select to regress_rls_alice using (c = current_setting('rls_test.blah'));
+-- Function changes row_security setting and so invalidates plan
+create or replace function rls_f(text)
+ RETURNS text
+ LANGUAGE sql
+BEGIN ATOMIC
+ select set_config('rls_test.blah', $1, true) || set_config('row_security', 'false', true) || string_agg(c, ',' order by c) from rls_t;
+END;
+-- Table owner bypasses RLS
+select rls_f(c) from test_t order by rls_f;
+    rls_f    
+-------------
+ aoffa,b,c,d
+ boffa,b,c,d
+(2 rows)
+
+set role regress_rls_alice;
+-- For casual user changes in row_security setting lead
+-- to error during query rewrite
+select rls_f(c) from test_t order by rls_f;
+ERROR:  query would be affected by row-level security policy for table "rls_t"
+CONTEXT:  SQL function "rls_f" statement 1
+reset role;
+set plan_cache_mode to force_generic_plan;
+-- Table owner bypasses RLS, but cached plan invalidates
+select rls_f(c) from test_t order by rls_f;
+    rls_f    
+-------------
+ aoffa,b,c,d
+ boffa,b,c,d
+(2 rows)
+
+-- For casual user changes in row_security setting lead
+-- to plan invalidation and error during query rewrite
+set role regress_rls_alice;
+select rls_f(c) from test_t order by rls_f;
+ERROR:  query would be affected by row-level security policy for table "rls_t"
+CONTEXT:  SQL function "rls_f" statement 1
+reset role;
+reset plan_cache_mode;
+reset rls_test.blah;
+drop function rls_f;
+drop table rls_t, test_t;
 --
 -- Clean up objects
 --
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 47478969135..beb0b4c5db4 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -3879,3 +3879,38 @@ DROP TABLE ruletest_t3;
 DROP TABLE ruletest_t2;
 DROP TABLE ruletest_t1;
 DROP USER regress_rule_user1;
+-- Test that SQL functions correctly handle DO NOTHING rule
+CREATE TABLE some_data (i int, data text);
+CREATE TABLE some_data_values (i int, data text);
+CREATE FUNCTION insert_data(i int, data text)
+RETURNS INT
+AS $$
+INSERT INTO some_data VALUES ($1, $2);
+SELECT 1;
+$$ LANGUAGE SQL;
+INSERT INTO some_data_values SELECT i , 'data'|| i FROM generate_series(1, 10) i;
+CREATE RULE some_data_noinsert AS ON INSERT TO some_data DO INSTEAD NOTHING;
+SELECT insert_data(i, data) FROM some_data_values;
+ insert_data 
+-------------
+           1
+           1
+           1
+           1
+           1
+           1
+           1
+           1
+           1
+           1
+(10 rows)
+
+SELECT * FROM some_data ORDER BY i;
+ i | data 
+---+------
+(0 rows)
+
+DROP RULE some_data_noinsert ON some_data;
+DROP TABLE some_data_values;
+DROP TABLE some_data;
+DROP FUNCTION insert_data(int, text);
diff --git a/src/test/regress/sql/rowsecurity.sql b/src/test/regress/sql/rowsecurity.sql
index f61dbbf9581..9fe8f4b059c 100644
--- a/src/test/regress/sql/rowsecurity.sql
+++ b/src/test/regress/sql/rowsecurity.sql
@@ -2307,6 +2307,47 @@ DROP FUNCTION rls_f();
 DROP VIEW rls_v;
 DROP TABLE rls_t;
 
+-- RLS changes invalidate cached function plans
+create table rls_t (c text);
+create table test_t (c text);
+
+insert into rls_t values ('a'), ('b'), ('c'), ('d');
+insert into test_t values ('a'), ('b');
+alter table rls_t enable row level security;
+grant select on rls_t to regress_rls_alice;
+grant select on test_t to regress_rls_alice;
+create policy p1 on rls_t for select to regress_rls_alice using (c = current_setting('rls_test.blah'));
+
+-- Function changes row_security setting and so invalidates plan
+create or replace function rls_f(text)
+ RETURNS text
+ LANGUAGE sql
+BEGIN ATOMIC
+ select set_config('rls_test.blah', $1, true) || set_config('row_security', 'false', true) || string_agg(c, ',' order by c) from rls_t;
+END;
+
+-- Table owner bypasses RLS
+select rls_f(c) from test_t order by rls_f;
+set role regress_rls_alice;
+-- For casual user changes in row_security setting lead
+-- to error during query rewrite
+select rls_f(c) from test_t order by rls_f;
+reset role;
+
+set plan_cache_mode to force_generic_plan;
+-- Table owner bypasses RLS, but cached plan invalidates
+select rls_f(c) from test_t order by rls_f;
+-- For casual user changes in row_security setting lead
+-- to plan invalidation and error during query rewrite
+set role regress_rls_alice;
+select rls_f(c) from test_t order by rls_f;
+reset role;
+reset plan_cache_mode;
+reset rls_test.blah;
+
+drop function rls_f;
+drop table rls_t, test_t;
+
 --
 -- Clean up objects
 --
diff --git a/src/test/regress/sql/rules.sql b/src/test/regress/sql/rules.sql
index fdd3ff1d161..505449452ee 100644
--- a/src/test/regress/sql/rules.sql
+++ b/src/test/regress/sql/rules.sql
@@ -1432,3 +1432,27 @@ DROP TABLE ruletest_t2;
 DROP TABLE ruletest_t1;
 
 DROP USER regress_rule_user1;
+
+-- Test that SQL functions correctly handle DO NOTHING rule
+CREATE TABLE some_data (i int, data text);
+CREATE TABLE some_data_values (i int, data text);
+
+CREATE FUNCTION insert_data(i int, data text)
+RETURNS INT
+AS $$
+INSERT INTO some_data VALUES ($1, $2);
+SELECT 1;
+$$ LANGUAGE SQL;
+
+INSERT INTO some_data_values SELECT i , 'data'|| i FROM generate_series(1, 10) i;
+
+CREATE RULE some_data_noinsert AS ON INSERT TO some_data DO INSTEAD NOTHING;
+
+SELECT insert_data(i, data) FROM some_data_values;
+
+SELECT * FROM some_data ORDER BY i;
+
+DROP RULE some_data_noinsert ON some_data;
+DROP TABLE some_data_values;
+DROP TABLE some_data;
+DROP FUNCTION insert_data(int, text);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9442a4841aa..cb0dde54ba1 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2605,6 +2605,8 @@ SQLFunctionCache
 SQLFunctionCachePtr
 SQLFunctionParseInfo
 SQLFunctionParseInfoPtr
+SQLFunctionPlanEntry
+SQLFunctionPlanKey
 SQLValueFunction
 SQLValueFunctionOp
 SSL
-- 
2.43.0

