From a0ec33353301dac606292b8ee4cec0d36f346a3a Mon Sep 17 00:00:00 2001
From: Alexander Pyhalov <a.pyhalov@postgrespro.ru>
Date: Thu, 23 Jan 2025 14:34:17 +0300
Subject: [PATCH 2/4] Use custom plan machinery for SQL function

---
 src/backend/executor/functions.c          | 223 +++++++++++++++++-----
 src/backend/parser/analyze.c              |  33 ++++
 src/backend/utils/cache/plancache.c       | 100 ++++++++--
 src/backend/utils/misc/guc_tables.c       |   1 +
 src/include/parser/analyze.h              |   2 +
 src/include/utils/plancache.h             |   5 +
 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 +++
 10 files changed, 453 insertions(+), 62 deletions(-)

diff --git a/src/backend/executor/functions.c b/src/backend/executor/functions.c
index 5d0afa0b733..9aa5e0def46 100644
--- a/src/backend/executor/functions.c
+++ b/src/backend/executor/functions.c
@@ -33,10 +33,10 @@
 #include "utils/datum.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
+#include "utils/plancache.h"
 #include "utils/snapmgr.h"
 #include "utils/syscache.h"
 
-
 /*
  * Specialized DestReceiver for collecting query output in a SQL function
  */
@@ -112,6 +112,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 +128,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 */
@@ -138,10 +146,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);
@@ -495,50 +502,57 @@ check_planned_stmt(PlannedStmt *stmt, SQLFunctionCachePtr fcache)
  * querytrees.  The sublist structure denotes the original query boundaries.
  */
 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;
 
-	foreach(lc1, queryTree_list)
+	/* We use lc1 index, not lc1 itself, so mark it unused */
+	ListCell   *lc1 pg_attribute_unused();
+	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, fcache->plansource_list)
 	{
-		List	   *qtlist = lfirst_node(List, lc1);
 		execution_state *firstes = NULL;
 		execution_state *preves = NULL;
 		ListCell   *lc2;
+		CachedPlan *cplan;
+		CachedPlanSource *plansource = (CachedPlanSource *) lfirst(lc1);
+
+		/* Save statement number for error reporting */
+		fcache->planning_stmt_number = foreach_current_index(lc1) + 1 /* cur_idx starts with 0 */ ;
 
-		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(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);
-
 			/* Check that stmt is valid for SQL function */
 			check_planned_stmt(stmt, fcache);
 
 			/* OK, build the execution_state for this query */
 			newes = (execution_state *) palloc(sizeof(execution_state));
+
 			if (preves)
 				preves->next = newes;
 			else
@@ -551,7 +565,7 @@ init_execution_state(List *queryTree_list,
 			newes->stmt = stmt;
 			newes->qd = NULL;
 
-			if (queryTree->canSetTag)
+			if (stmt->canSetTag)
 				lasttages = newes;
 
 			preves = newes;
@@ -583,6 +597,11 @@ 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;
 }
 
@@ -590,7 +609,7 @@ init_execution_state(List *queryTree_list,
  * 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;
@@ -606,6 +625,7 @@ init_sql_fcache(FunctionCallInfo fcinfo, Oid collation, bool lazyEvalOK)
 	ListCell   *lc;
 	Datum		tmp;
 	bool		isNull;
+	List	   *plansource_list;
 
 	/*
 	 * Create memory context that holds all the SQLFunctionCache data.  It
@@ -624,6 +644,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;
 
 	/*
@@ -690,6 +714,7 @@ init_sql_fcache(FunctionCallInfo fcinfo, Oid collation, bool lazyEvalOK)
 	 * plancache.c.
 	 */
 	queryTree_list = NIL;
+	plansource_list = NIL;
 	if (!isNull)
 	{
 		Node	   *n;
@@ -705,8 +730,13 @@ init_sql_fcache(FunctionCallInfo fcinfo, Oid collation, bool lazyEvalOK)
 		{
 			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);
 		}
@@ -721,6 +751,10 @@ init_sql_fcache(FunctionCallInfo fcinfo, Oid collation, bool lazyEvalOK)
 		{
 			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,
@@ -761,6 +795,34 @@ init_sql_fcache(FunctionCallInfo fcinfo, Oid collation, bool lazyEvalOK)
 											   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);
+		}
+	}
+
 	/*
 	 * Construct a JunkFilter we can use to coerce the returned rowtype to the
 	 * desired form, unless the result type is VOID, in which case there's
@@ -802,13 +864,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;
@@ -981,7 +1040,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];
 		}
 	}
@@ -1034,6 +1098,33 @@ 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);
+	}
+
+	/* Cleanup the list itself */
+	list_free(cplans);
+}
+
 /*
  * fmgr_sql: function call manager for SQL functions
  */
@@ -1052,6 +1143,7 @@ fmgr_sql(PG_FUNCTION_ARGS)
 	Datum		result;
 	List	   *eslist;
 	ListCell   *eslc;
+	bool		build_cached_plans = false;
 
 	/*
 	 * Setup error traceback support for ereport()
@@ -1107,7 +1199,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;
 	}
 
@@ -1141,12 +1237,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
@@ -1401,6 +1522,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;
@@ -1440,13 +1565,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;
@@ -1532,6 +1663,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/backend/parser/analyze.c b/src/backend/parser/analyze.c
index 76f58b3aca3..ee488b6db77 100644
--- a/src/backend/parser/analyze.c
+++ b/src/backend/parser/analyze.c
@@ -567,6 +567,39 @@ stmt_requires_parse_analysis(RawStmt *parseTree)
 	return result;
 }
 
+/*
+ * querytree_requires_revalidation()
+ *              Returns true if given query requires revalidation.
+ *
+ * Mostly this is a variation of stmt_requires_parse_analysis()
+ * for Query.
+ */
+bool
+querytree_requires_revalidation(Query *query)
+{
+	bool		result;
+
+	if (query->commandType != CMD_UTILITY)
+		result = true;
+	else
+	{
+		/* should match stmt_requires_parse_analysis() */
+		switch (nodeTag(query->utilityStmt))
+		{
+			case T_DeclareCursorStmt:
+			case T_ExplainStmt:
+			case T_CreateTableAsStmt:
+			case T_CallStmt:
+				result = true;
+				break;
+			default:
+				result = false;
+				break;
+		}
+	}
+	return result;
+}
+
 /*
  * analyze_requires_snapshot
  *		Returns true if a snapshot must be set before doing parse analysis
diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c
index 6c2979d5c82..8cb48b07b13 100644
--- a/src/backend/utils/cache/plancache.c
+++ b/src/backend/utils/cache/plancache.c
@@ -63,6 +63,7 @@
 #include "nodes/nodeFuncs.h"
 #include "optimizer/optimizer.h"
 #include "parser/analyze.h"
+#include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
 #include "tcop/pquery.h"
 #include "tcop/utility.h"
@@ -74,18 +75,6 @@
 #include "utils/syscache.h"
 
 
-/*
- * We must skip "overhead" operations that involve database access when the
- * cached plan's subject statement is a transaction control command or one
- * that requires a snapshot not to be set yet (such as SET or LOCK).  More
- * generally, statements that do not require parse analysis/rewrite/plan
- * activity never need to be revalidated, so we can treat them all like that.
- * For the convenience of postgres.c, treat empty statements that way too.
- */
-#define StmtPlanRequiresRevalidation(plansource)  \
-	((plansource)->raw_parse_tree != NULL && \
-	 stmt_requires_parse_analysis((plansource)->raw_parse_tree))
-
 /*
  * This is the head of the backend's list of "saved" CachedPlanSources (i.e.,
  * those that are in long-lived storage and are examined for sinval events).
@@ -131,6 +120,43 @@ static const ResourceOwnerDesc planref_resowner_desc =
 	.DebugPrint = NULL			/* the default message is fine */
 };
 
+/*
+ * We must skip "overhead" operations that involve database access when the
+ * cached plan's subject statement is a transaction control command or one
+ * that requires a snapshot not to be set yet (such as SET or LOCK).  More
+ * generally, statements that do not require parse analysis/rewrite/plan
+ * activity never need to be revalidated, so we can treat them all like that.
+ * For the convenience of postgres.c, treat empty statements that way too.
+ * If plansource doesn't have raw_parse_tree, look at query_list to find out
+ * if there are any non-utility statements. Also some utility statements
+ * can require revalidation. The logic is the same as in stmt_requires_parse_analysis().
+ */
+static inline bool
+StmtPlanRequiresRevalidation(CachedPlanSource *plansource)
+{
+	if (plansource->raw_parse_tree != NULL)
+		return stmt_requires_parse_analysis(plansource->raw_parse_tree);
+	else if (plansource->analyzed_parse_tree != NULL)
+		return querytree_requires_revalidation(plansource->analyzed_parse_tree);
+	return false;
+}
+
+/*
+ * Determine if creating plan for this CachedPlanSource requires snapshot.
+ * In fact this funcion matches StmtPlanRequiresRevalidation(), but we want
+ * to preserve distinction between stmt_requires_parse_analysis() and
+ * analyze_requires_snapshot().
+ */
+static inline bool
+BuildingPlanRequiresSnapshot(CachedPlanSource *plansource)
+{
+	if (plansource->raw_parse_tree != NULL)
+		return analyze_requires_snapshot(plansource->raw_parse_tree);
+	else if (plansource->analyzed_parse_tree != NULL)
+		return querytree_requires_revalidation(plansource->analyzed_parse_tree);
+	return false;
+}
+
 /* Convenience wrappers over ResourceOwnerRemember/Forget */
 static inline void
 ResourceOwnerRememberPlanCacheRef(ResourceOwner owner, CachedPlan *plan)
@@ -143,7 +169,6 @@ ResourceOwnerForgetPlanCacheRef(ResourceOwner owner, CachedPlan *plan)
 	ResourceOwnerForget(owner, PointerGetDatum(plan), &planref_resowner_desc);
 }
 
-
 /* GUC parameter */
 int			plan_cache_mode = PLAN_CACHE_MODE_AUTO;
 
@@ -185,7 +210,8 @@ InitPlanCache(void)
  * Once constructed, the cached plan can be made longer-lived, if needed,
  * by calling SaveCachedPlan.
  *
- * raw_parse_tree: output of raw_parser(), or NULL if empty query
+ * raw_parse_tree: output of raw_parser(), or NULL if empty query, can
+ *   also be NULL if plansource->analyzed_parse_tree is set instead
  * query_string: original query text
  * commandTag: command tag for query, or UNKNOWN if empty query
  */
@@ -220,6 +246,7 @@ CreateCachedPlan(RawStmt *raw_parse_tree,
 	plansource = (CachedPlanSource *) palloc0(sizeof(CachedPlanSource));
 	plansource->magic = CACHEDPLANSOURCE_MAGIC;
 	plansource->raw_parse_tree = copyObject(raw_parse_tree);
+	plansource->analyzed_parse_tree = NULL;
 	plansource->query_string = pstrdup(query_string);
 	MemoryContextSetIdentifier(source_context, plansource->query_string);
 	plansource->commandTag = commandTag;
@@ -255,6 +282,27 @@ CreateCachedPlan(RawStmt *raw_parse_tree,
 	return plansource;
 }
 
+/*
+ * CreateCachedPlanForQuery: initially create a plan cache entry
+ * for parsed and analyzed query. Unlike CreateCachedPlan(),
+ * it preserves analyzed parse tree, not raw parse tree.
+ */
+CachedPlanSource *
+CreateCachedPlanForQuery(Query *analyzed_parse_tree,
+						 const char *query_string,
+						 CommandTag commandTag)
+{
+	CachedPlanSource *plansource;
+	MemoryContext oldcxt;
+
+	plansource = CreateCachedPlan(NULL, query_string, commandTag);
+	oldcxt = MemoryContextSwitchTo(plansource->context);
+	plansource->analyzed_parse_tree = copyObject(analyzed_parse_tree);
+	MemoryContextSwitchTo(oldcxt);
+
+	return plansource;
+}
+
 /*
  * CreateOneShotCachedPlan: initially create a one-shot plan cache entry.
  *
@@ -717,7 +765,20 @@ RevalidateCachedQuery(CachedPlanSource *plansource,
 	 */
 	rawtree = copyObject(plansource->raw_parse_tree);
 	if (rawtree == NULL)
-		tlist = NIL;
+	{
+		/* Working on pre-analyzed query */
+		if (plansource->analyzed_parse_tree)
+		{
+			/* Copy analyzed_parse_tree prevent its corruption */
+			Query	   *analyzed_tree = copyObject(plansource->analyzed_parse_tree);
+
+			AcquireRewriteLocks(analyzed_tree, true, false);
+			tlist = pg_rewrite_query(analyzed_tree);
+		}
+		else
+			/* Utility command */
+			tlist = NIL;
+	}
 	else if (plansource->parserSetup != NULL)
 		tlist = pg_analyze_and_rewrite_withcb(rawtree,
 											  plansource->query_string,
@@ -959,12 +1020,14 @@ BuildCachedPlan(CachedPlanSource *plansource, List *qlist,
 
 	/*
 	 * If a snapshot is already set (the normal case), we can just use that
-	 * for planning.  But if it isn't, and we need one, install one.
+	 * for planning.  But if it isn't, and we need one, install one. If
+	 * plansource has raw_parse_tree set, we check if it requires snapshot. If
+	 * raw_parse_tree is not set, check the same conditions for
+	 * analyzed_parse_tree.
 	 */
 	snapshot_set = false;
 	if (!ActiveSnapshotSet() &&
-		plansource->raw_parse_tree &&
-		analyze_requires_snapshot(plansource->raw_parse_tree))
+		BuildingPlanRequiresSnapshot(plansource))
 	{
 		PushActiveSnapshot(GetTransactionSnapshot());
 		snapshot_set = true;
@@ -1703,6 +1766,7 @@ CopyCachedPlan(CachedPlanSource *plansource)
 	newsource = (CachedPlanSource *) palloc0(sizeof(CachedPlanSource));
 	newsource->magic = CACHEDPLANSOURCE_MAGIC;
 	newsource->raw_parse_tree = copyObject(plansource->raw_parse_tree);
+	newsource->analyzed_parse_tree = copyObject(plansource->analyzed_parse_tree);
 	newsource->query_string = pstrdup(plansource->query_string);
 	MemoryContextSetIdentifier(source_context, newsource->query_string);
 	newsource->commandTag = plansource->commandTag;
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index ad25cbb39c5..d0d2fd7a6fa 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -46,6 +46,7 @@
 #include "commands/vacuum.h"
 #include "common/file_utils.h"
 #include "common/scram-common.h"
+#include "executor/functions.h"
 #include "jit/jit.h"
 #include "libpq/auth.h"
 #include "libpq/libpq.h"
diff --git a/src/include/parser/analyze.h b/src/include/parser/analyze.h
index f1bd18c49f2..04d14a9c3f0 100644
--- a/src/include/parser/analyze.h
+++ b/src/include/parser/analyze.h
@@ -53,6 +53,8 @@ extern Query *transformStmt(ParseState *pstate, Node *parseTree);
 extern bool stmt_requires_parse_analysis(RawStmt *parseTree);
 extern bool analyze_requires_snapshot(RawStmt *parseTree);
 
+extern bool querytree_requires_revalidation(Query *query);
+
 extern const char *LCS_asString(LockClauseStrength strength);
 extern void CheckSelectLocking(Query *qry, LockClauseStrength strength);
 extern void applyLockingClause(Query *qry, Index rtindex,
diff --git a/src/include/utils/plancache.h b/src/include/utils/plancache.h
index f1fc7707338..e22b18ffcf0 100644
--- a/src/include/utils/plancache.h
+++ b/src/include/utils/plancache.h
@@ -27,6 +27,7 @@
 
 /* Forward declaration, to avoid including parsenodes.h here */
 struct RawStmt;
+struct Query;
 
 /* possible values for plan_cache_mode */
 typedef enum
@@ -99,6 +100,7 @@ typedef struct CachedPlanSource
 {
 	int			magic;			/* should equal CACHEDPLANSOURCE_MAGIC */
 	struct RawStmt *raw_parse_tree; /* output of raw_parser(), or NULL */
+	struct Query *analyzed_parse_tree;	/* analyzed parse tree or NULL */
 	const char *query_string;	/* source text of query */
 	CommandTag	commandTag;		/* 'nuff said */
 	Oid		   *param_types;	/* array of parameter type OIDs, or NULL */
@@ -201,6 +203,9 @@ extern void ReleaseAllPlanCacheRefsInOwner(ResourceOwner owner);
 extern CachedPlanSource *CreateCachedPlan(struct RawStmt *raw_parse_tree,
 										  const char *query_string,
 										  CommandTag commandTag);
+extern CachedPlanSource *CreateCachedPlanForQuery(struct Query *analyzed_parse_tree,
+												  const char *query_string,
+												  CommandTag commandTag);
 extern CachedPlanSource *CreateOneShotCachedPlan(struct RawStmt *raw_parse_tree,
 												 const char *query_string,
 												 CommandTag commandTag);
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 62f69ac20b2..b9fe71f391d 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -3878,3 +3878,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);
-- 
2.43.0

