From 83674406fbf305f3d4827c6a2ec7aac0a1fcaedd Mon Sep 17 00:00:00 2001
From: Alexander Pyhalov <a.pyhalov@postgrespro.ru>
Date: Fri, 23 Aug 2024 15:48:38 +0300
Subject: [PATCH] Use custom plan machinery for SQL functions

---
 contrib/citext/expected/citext.out            |   2 +-
 contrib/citext/expected/citext_1.out          |   2 +-
 src/backend/executor/functions.c              | 334 ++++++++++++++----
 src/backend/utils/cache/plancache.c           |  44 ++-
 src/backend/utils/misc/guc_tables.c           |  12 +
 src/include/executor/functions.h              |   2 +
 src/test/regress/expected/numeric.out         |   4 +-
 src/test/regress/expected/plpgsql.out         |   2 +-
 src/test/regress/expected/rules.out           |  35 ++
 src/test/regress/expected/select_parallel.out |   4 +-
 src/test/regress/expected/strings.out         |   2 +-
 src/test/regress/expected/sysviews.out        |   3 +-
 src/test/regress/sql/rules.sql                |  24 ++
 13 files changed, 385 insertions(+), 85 deletions(-)

diff --git a/contrib/citext/expected/citext.out b/contrib/citext/expected/citext.out
index 8c0bf54f0f3..f8765053651 100644
--- a/contrib/citext/expected/citext.out
+++ b/contrib/citext/expected/citext.out
@@ -1816,7 +1816,7 @@ SELECT regexp_match('foobarbequebaz'::citext, '(BAR)(BEQUE)'::citext, 'c'::citex
 SELECT regexp_match('foobarbequebazmorebarbequetoo'::citext, '(BAR)(BEQUE)'::citext, 'g') AS "error";
 ERROR:  regexp_match() does not support the "global" option
 HINT:  Use the regexp_matches function instead.
-CONTEXT:  SQL function "regexp_match" statement 1
+CONTEXT:  SQL function "regexp_match" during startup
 SELECT regexp_matches('foobarbequebaz'::citext, '(bar)(beque)') = ARRAY[ 'bar', 'beque' ] AS t;
  t 
 ---
diff --git a/contrib/citext/expected/citext_1.out b/contrib/citext/expected/citext_1.out
index c5e5f180f2b..356dc6bba43 100644
--- a/contrib/citext/expected/citext_1.out
+++ b/contrib/citext/expected/citext_1.out
@@ -1816,7 +1816,7 @@ SELECT regexp_match('foobarbequebaz'::citext, '(BAR)(BEQUE)'::citext, 'c'::citex
 SELECT regexp_match('foobarbequebazmorebarbequetoo'::citext, '(BAR)(BEQUE)'::citext, 'g') AS "error";
 ERROR:  regexp_match() does not support the "global" option
 HINT:  Use the regexp_matches function instead.
-CONTEXT:  SQL function "regexp_match" statement 1
+CONTEXT:  SQL function "regexp_match" during startup
 SELECT regexp_matches('foobarbequebaz'::citext, '(bar)(beque)') = ARRAY[ 'bar', 'beque' ] AS t;
  t 
 ---
diff --git a/src/backend/executor/functions.c b/src/backend/executor/functions.c
index 757f8068e21..4591c3894ca 100644
--- a/src/backend/executor/functions.c
+++ b/src/backend/executor/functions.c
@@ -33,9 +33,14 @@
 #include "utils/datum.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
+#include "utils/plancache.h"
 #include "utils/snapmgr.h"
 #include "utils/syscache.h"
 
+/*
+ * GUC variables
+ */
+bool		enable_sql_func_custom_plans = true;
 
 /*
  * Specialized DestReceiver for collecting query output in a SQL function
@@ -112,6 +117,11 @@ typedef struct
 
 	JunkFilter *junkFilter;		/* will be NULL if function returns VOID */
 
+	/* Cached plans support */
+	List	   *queryTree_list; /* list of query lists */
+	List	   *plansource_list;	/* list of plansource */
+	List	   *cplan_list;		/* list of cached plans */
+
 	/*
 	 * func_state is a List of execution_state records, each of which is the
 	 * first for its original parsetree, with any additional records chained
@@ -454,6 +464,40 @@ sql_fn_resolve_param_name(SQLFunctionParseInfoPtr pinfo,
 	return NULL;
 }
 
+/* Precheck command for validity in a function */
+static void
+check_planned_stmt(PlannedStmt *stmt, SQLFunctionCachePtr fcache)
+{
+
+	/*
+	 * Precheck all commands for validity in a function.  This should
+	 * generally match the restrictions spi.c applies.
+	 */
+	if (stmt->commandType == CMD_UTILITY)
+	{
+		if (IsA(stmt->utilityStmt, CopyStmt) &&
+			((CopyStmt *) stmt->utilityStmt)->filename == NULL)
+			ereport(ERROR,
+					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					 errmsg("cannot COPY to/from client in an SQL function")));
+
+		if (IsA(stmt->utilityStmt, TransactionStmt))
+			ereport(ERROR,
+					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			/* translator: %s is a SQL statement name */
+					 errmsg("%s is not allowed in an SQL function",
+							CreateCommandName(stmt->utilityStmt))));
+	}
+
+	if (fcache->readonly_func && !CommandIsReadOnly(stmt))
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+		/* translator: %s is a SQL statement name */
+				 errmsg("%s is not allowed in a non-volatile function",
+						CreateCommandName((Node *) stmt))));
+
+}
+
 /*
  * Set up the per-query execution_state records for a SQL function.
  *
@@ -466,85 +510,117 @@ init_execution_state(List *queryTree_list,
 					 bool lazyEvalOK)
 {
 	List	   *eslist = NIL;
+	List	   *cplan_list = NIL;
 	execution_state *lasttages = NULL;
 	ListCell   *lc1;
 
 	foreach(lc1, queryTree_list)
 	{
-		List	   *qtlist = lfirst_node(List, lc1);
+		List	   *qtlist;
 		execution_state *firstes = NULL;
 		execution_state *preves = NULL;
 		ListCell   *lc2;
 
-		foreach(lc2, qtlist)
+		if (fcache->plansource_list)
 		{
-			Query	   *queryTree = lfirst_node(Query, lc2);
-			PlannedStmt *stmt;
-			execution_state *newes;
+			CachedPlan *cplan;
+			CachedPlanSource *plansource;
+			int			cur_idx;
 
-			/* 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);
+			/* Find plan source, corresponding to this query list */
+			cur_idx = foreach_current_index(lc1);
+			plansource = list_nth(fcache->plansource_list, cur_idx);
 
 			/*
-			 * Precheck all commands for validity in a function.  This should
-			 * generally match the restrictions spi.c applies.
+			 * Get plan for the query. If paramLI is set, we can get custom
+			 * plan
 			 */
-			if (stmt->commandType == CMD_UTILITY)
+			cplan = GetCachedPlan(plansource, fcache->paramLI, 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)
 			{
-				if (IsA(stmt->utilityStmt, CopyStmt) &&
-					((CopyStmt *) stmt->utilityStmt)->filename == NULL)
-					ereport(ERROR,
-							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-							 errmsg("cannot COPY to/from client in an SQL function")));
+				PlannedStmt *stmt = lfirst(lc2);
+				execution_state *newes;
 
-				if (IsA(stmt->utilityStmt, TransactionStmt))
-					ereport(ERROR,
-							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-					/* translator: %s is a SQL statement name */
-							 errmsg("%s is not allowed in an SQL function",
-									CreateCommandName(stmt->utilityStmt))));
+				/* Check that stmt is valid for SQL function */
+				check_planned_stmt(stmt, fcache);
+
+				newes = (execution_state *) palloc(sizeof(execution_state));
+
+				if (preves)
+					preves->next = newes;
+				else
+					firstes = newes;
+
+				newes->next = NULL;
+				newes->status = F_EXEC_START;
+				newes->setsResult = false;	/* might change below */
+				newes->lazyEval = false;	/* might change below */
+				newes->stmt = stmt;
+				newes->qd = NULL;
+
+				if (stmt->canSetTag)
+					lasttages = newes;
+
+				preves = newes;
 			}
+		}
+		else
+		{
+			qtlist = lfirst_node(List, lc1);
 
-			if (fcache->readonly_func && !CommandIsReadOnly(stmt))
-				ereport(ERROR,
-						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-				/* translator: %s is a SQL statement name */
-						 errmsg("%s is not allowed in a non-volatile function",
-								CreateCommandName((Node *) stmt))));
-
-			/* OK, build the execution_state for this query */
-			newes = (execution_state *) palloc(sizeof(execution_state));
-			if (preves)
-				preves->next = newes;
-			else
-				firstes = newes;
+			foreach(lc2, qtlist)
+			{
+				Query	   *queryTree = lfirst_node(Query, lc2);
+				PlannedStmt *stmt;
+				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
+				{
+					/* Get generic plan for the query */
+					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);
 
-			newes->next = NULL;
-			newes->status = F_EXEC_START;
-			newes->setsResult = false;	/* might change below */
-			newes->lazyEval = false;	/* might change below */
-			newes->stmt = stmt;
-			newes->qd = NULL;
+				newes = (execution_state *) palloc(sizeof(execution_state));
+				if (preves)
+					preves->next = newes;
+				else
+					firstes = newes;
+
+				newes->next = NULL;
+				newes->status = F_EXEC_START;
+				newes->setsResult = false;	/* might change below */
+				newes->lazyEval = false;	/* might change below */
+				newes->stmt = stmt;
+				newes->qd = NULL;
 
-			if (queryTree->canSetTag)
-				lasttages = newes;
+				if (queryTree->canSetTag)
+					lasttages = newes;
 
-			preves = newes;
+				preves = newes;
+			}
 		}
 
 		eslist = lappend(eslist, firstes);
@@ -573,6 +649,7 @@ init_execution_state(List *queryTree_list,
 			fcache->lazyEval = lasttages->lazyEval = true;
 	}
 
+	fcache->cplan_list = cplan_list;
 	return eslist;
 }
 
@@ -596,6 +673,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
@@ -680,6 +758,7 @@ init_sql_fcache(FunctionCallInfo fcinfo, Oid collation, bool lazyEvalOK)
 	 * plancache.c.
 	 */
 	queryTree_list = NIL;
+	plansource_list = NIL;
 	if (!isNull)
 	{
 		Node	   *n;
@@ -695,8 +774,16 @@ init_sql_fcache(FunctionCallInfo fcinfo, Oid collation, bool lazyEvalOK)
 		{
 			Query	   *parsetree = lfirst_node(Query, lc);
 			List	   *queryTree_sublist;
+			CachedPlanSource *plansource;
 
 			AcquireRewriteLocks(parsetree, true, false);
+
+			if (enable_sql_func_custom_plans)
+			{
+				plansource = CreateCachedPlan(NULL, fcache->src, CreateCommandTag((Node *)parsetree));
+				plansource_list = lappend(plansource_list, plansource);
+			}
+
 			queryTree_sublist = pg_rewrite_query(parsetree);
 			queryTree_list = lappend(queryTree_list, queryTree_sublist);
 		}
@@ -711,6 +798,13 @@ init_sql_fcache(FunctionCallInfo fcinfo, Oid collation, bool lazyEvalOK)
 		{
 			RawStmt    *parsetree = lfirst_node(RawStmt, lc);
 			List	   *queryTree_sublist;
+			CachedPlanSource *plansource;
+
+			if (enable_sql_func_custom_plans)
+			{
+				plansource = CreateCachedPlan(parsetree, fcache->src, CreateCommandTag(parsetree->stmt));
+				plansource_list = lappend(plansource_list, plansource);
+			}
 
 			queryTree_sublist = pg_analyze_and_rewrite_withcb(parsetree,
 															  fcache->src,
@@ -751,6 +845,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
@@ -795,10 +917,18 @@ init_sql_fcache(FunctionCallInfo fcinfo, Oid collation, bool lazyEvalOK)
 		lazyEvalOK = true;
 	}
 
-	/* Finally, plan the queries */
-	fcache->func_state = init_execution_state(queryTree_list,
-											  fcache,
-											  lazyEvalOK);
+	fcache->queryTree_list = queryTree_list;
+	fcache->plansource_list = plansource_list;
+
+	/*
+	 * Finally, plan the queries. Skip planning if we use cached plans
+	 * machinery - anyway we'll have to replan on the first run when
+	 * parameters are substituted.
+	 */
+	if (plansource_list == NIL)
+		fcache->func_state = init_execution_state(queryTree_list,
+												  fcache,
+												  lazyEvalOK);
 
 	/* Mark fcache with time of creation to show it's valid */
 	fcache->lxid = MyProc->vxid.lxid;
@@ -969,7 +1099,13 @@ 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.
+			 */
+			if (fcache->plansource_list)
+				prm->pflags = PARAM_FLAG_CONST;
+
 			prm->ptype = argtypes[i];
 		}
 	}
@@ -1022,6 +1158,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, NULL);
+	}
+
+	/* Cleanup the list itself */
+	list_free(cplans);
+}
+
 /*
  * fmgr_sql: function call manager for SQL functions
  */
@@ -1040,6 +1203,7 @@ fmgr_sql(PG_FUNCTION_ARGS)
 	Datum		result;
 	List	   *eslist;
 	ListCell   *eslc;
+	bool		build_cached_plans = false;
 
 	/*
 	 * Setup error traceback support for ereport()
@@ -1129,12 +1293,54 @@ fmgr_sql(PG_FUNCTION_ARGS)
 			break;
 	}
 
+	/*
+	 * We could skip actual planning if decided to use cached plans. 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)
+		{
+			ListCell *lc;
+
+			/* replan the queries */
+			release_plans(fcache->cplan_list);
+
+			foreach (lc, fcache->func_state)
+			{
+				execution_state *cur_es = lfirst(lc);
+				/*
+				 * Execution state can be NULL if statement is
+				 * completely replaced by do instead nothing rule.
+				 */
+				if (cur_es)
+					pfree(cur_es);
+			}
+			list_free(fcache->func_state);
+
+			fcache->func_state = init_execution_state(fcache->queryTree_list,
+													  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
diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c
index 55db8f53705..414df0f8f26 100644
--- a/src/backend/utils/cache/plancache.c
+++ b/src/backend/utils/cache/plancache.c
@@ -74,18 +74,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).
@@ -130,6 +118,38 @@ 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.
+ */
+static inline bool
+StmtPlanRequiresRevalidation(CachedPlanSource *plansource)
+{
+	if (plansource->raw_parse_tree != NULL)
+	{
+		return stmt_requires_parse_analysis(plansource->raw_parse_tree);
+	}
+	else
+	{
+		ListCell *lc;
+
+		foreach (lc, plansource->query_list)
+		{
+			Query *query = castNode(Query, lfirst(lc));
+
+			if (query->commandType != CMD_UTILITY)
+				return true;
+		}
+	}
+	return false;
+}
+
 /* Convenience wrappers over ResourceOwnerRemember/Forget */
 static inline void
 ResourceOwnerRememberPlanCacheRef(ResourceOwner owner, CachedPlan *plan)
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 38cb9e970d5..8637bf6b62d 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"
@@ -977,6 +978,17 @@ struct config_bool ConfigureNamesBool[] =
 		true,
 		NULL, NULL, NULL
 	},
+	{
+		{"enable_sql_func_custom_plans", PGC_USERSET, QUERY_TUNING_METHOD,
+			gettext_noop("Enable plan cache machinery and custom plans for "
+						 "SQL language functions."),
+			NULL,
+			GUC_NOT_IN_SAMPLE | GUC_EXPLAIN
+		},
+		&enable_sql_func_custom_plans,
+		true,
+		NULL, NULL, NULL
+	},
 	{
 		{"enable_async_append", PGC_USERSET, QUERY_TUNING_METHOD,
 			gettext_noop("Enables the planner's use of async append plans."),
diff --git a/src/include/executor/functions.h b/src/include/executor/functions.h
index a6ae2e72d79..34ef847b554 100644
--- a/src/include/executor/functions.h
+++ b/src/include/executor/functions.h
@@ -53,4 +53,6 @@ extern bool check_sql_fn_retval(List *queryTreeLists,
 
 extern DestReceiver *CreateSQLFunctionDestReceiver(void);
 
+extern PGDLLIMPORT bool enable_sql_func_custom_plans;
+
 #endif							/* FUNCTIONS_H */
diff --git a/src/test/regress/expected/numeric.out b/src/test/regress/expected/numeric.out
index 0898107ec30..f5af4dcc173 100644
--- a/src/test/regress/expected/numeric.out
+++ b/src/test/regress/expected/numeric.out
@@ -3346,10 +3346,10 @@ select ln(9.342536355e34);
 -- invalid inputs
 select log(-12.34);
 ERROR:  cannot take logarithm of a negative number
-CONTEXT:  SQL function "log" statement 1
+CONTEXT:  SQL function "log" during startup
 select log(0.0);
 ERROR:  cannot take logarithm of zero
-CONTEXT:  SQL function "log" statement 1
+CONTEXT:  SQL function "log" during startup
 -- some random tests
 select log(1.234567e-89);
                                                  log                                                 
diff --git a/src/test/regress/expected/plpgsql.out b/src/test/regress/expected/plpgsql.out
index 0a6945581bd..61a3580411b 100644
--- a/src/test/regress/expected/plpgsql.out
+++ b/src/test/regress/expected/plpgsql.out
@@ -4474,7 +4474,7 @@ create table public.stuffs (stuff text);
 SAVEPOINT a;
 select error2('nonexistent.stuffs');
 ERROR:  schema "nonexistent" does not exist
-CONTEXT:  SQL function "error1" statement 1
+CONTEXT:  SQL function "error1" during startup
 PL/pgSQL function error2(text) line 3 at RETURN
 ROLLBACK TO a;
 select error2('public.stuffs');
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 856a8349c50..3e28e4f521b 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -3868,3 +3868,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/expected/select_parallel.out b/src/test/regress/expected/select_parallel.out
index a8090364532..f7d11afc3dd 100644
--- a/src/test/regress/expected/select_parallel.out
+++ b/src/test/regress/expected/select_parallel.out
@@ -1378,7 +1378,7 @@ select set_and_report_role();
 
 select set_role_and_error(0);
 ERROR:  division by zero
-CONTEXT:  SQL function "set_role_and_error" statement 1
+CONTEXT:  SQL function "set_role_and_error" during startup
 set debug_parallel_query = 1;
 select set_and_report_role();
    set_and_report_role   
@@ -1388,7 +1388,7 @@ select set_and_report_role();
 
 select set_role_and_error(0);
 ERROR:  division by zero
-CONTEXT:  SQL function "set_role_and_error" statement 1
+CONTEXT:  SQL function "set_role_and_error" during startup
 parallel worker
 reset debug_parallel_query;
 drop function set_and_report_role();
diff --git a/src/test/regress/expected/strings.out b/src/test/regress/expected/strings.out
index b65bb2d5368..7ab5aafcd29 100644
--- a/src/test/regress/expected/strings.out
+++ b/src/test/regress/expected/strings.out
@@ -511,7 +511,7 @@ SELECT SUBSTRING('abcdefg' SIMILAR 'a#"%|ab#"g' ESCAPE '#') AS "bcdef";
 -- Can't have more than two part separators
 SELECT SUBSTRING('abcdefg' SIMILAR 'a*#"%#"g*#"x' ESCAPE '#') AS "error";
 ERROR:  SQL regular expression may not contain more than two escape-double-quote separators
-CONTEXT:  SQL function "substring" statement 1
+CONTEXT:  SQL function "substring" during startup
 -- Postgres extension: with 0 or 1 separator, assume parts 1 and 3 are empty
 SELECT SUBSTRING('abcdefg' SIMILAR 'a#"%g' ESCAPE '#') AS "bcdefg";
  bcdefg 
diff --git a/src/test/regress/expected/sysviews.out b/src/test/regress/expected/sysviews.out
index 352abc0bd42..7b0a4a0d6c0 100644
--- a/src/test/regress/expected/sysviews.out
+++ b/src/test/regress/expected/sysviews.out
@@ -170,8 +170,9 @@ select name, setting from pg_settings where name like 'enable%';
  enable_presorted_aggregate     | on
  enable_seqscan                 | on
  enable_sort                    | on
+ enable_sql_func_custom_plans   | on
  enable_tidscan                 | on
-(23 rows)
+(24 rows)
 
 -- There are always wait event descriptions for various types.  InjectionPoint
 -- may be present or absent, depending on history since last postmaster start.
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

