diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c
index c3f46a26c3..25539bbfae 100644
--- a/src/backend/optimizer/prep/prepjointree.c
+++ b/src/backend/optimizer/prep/prepjointree.c
@@ -23,7 +23,9 @@
  */
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "catalog/pg_type.h"
+#include "catalog/pg_proc.h"
 #include "nodes/makefuncs.h"
 #include "nodes/nodeFuncs.h"
 #include "optimizer/clauses.h"
@@ -35,6 +37,8 @@
 #include "parser/parse_relation.h"
 #include "parser/parsetree.h"
 #include "rewrite/rewriteManip.h"
+#include "utils/syscache.h"
+#include "utils/lsyscache.h"
 
 
 typedef struct pullup_replace_vars_context
@@ -86,6 +90,8 @@ static bool is_simple_subquery(Query *subquery, RangeTblEntry *rte,
 				   bool deletion_ok);
 static Node *pull_up_simple_values(PlannerInfo *root, Node *jtnode,
 					  RangeTblEntry *rte);
+static Node *pull_up_simple_function(PlannerInfo *root, Node *jtnode,
+					  RangeTblEntry *rte);
 static bool is_simple_values(PlannerInfo *root, RangeTblEntry *rte,
 				 bool deletion_ok);
 static bool is_simple_union_all(Query *subquery);
@@ -595,6 +601,54 @@ inline_set_returning_functions(PlannerInfo *root)
 	}
 }
 
+static bool
+is_simple_stable_function(RangeTblEntry *rte)
+{
+	Form_pg_type type_form;
+	RangeTblFunction *tblFunction = linitial_node(RangeTblFunction, rte->functions);
+	FuncExpr   *expr = (FuncExpr *) tblFunction->funcexpr;
+	HeapTuple tuple = SearchSysCache1(TYPEOID, ObjectIdGetDatum(expr->funcresulttype));
+	bool		result = false;
+
+	if (!HeapTupleIsValid(tuple))
+		elog(ERROR, "cache lookup failed for type %u", expr->funcresulttype);
+
+	type_form = (Form_pg_type) GETSTRUCT(tuple);
+
+	if (type_form->typtype == TYPTYPE_BASE &&
+		!type_is_array(expr->funcresulttype))
+	{
+		Form_pg_proc func_form;
+		ListCell   *arg;
+		bool		has_nonconst_input = false;
+		bool		has_null_input = false;
+
+		ReleaseSysCache(tuple);
+		tuple = SearchSysCache1(PROCOID, ObjectIdGetDatum(expr->funcid));
+		if (!HeapTupleIsValid(tuple))
+			elog(ERROR, "cache lookup failed for function %u", expr->funcid);
+		func_form = (Form_pg_proc) GETSTRUCT(tuple);
+
+		foreach(arg, expr->args)
+		{
+			if (IsA(lfirst(arg), Const))
+				has_null_input |= ((Const *) lfirst(arg))->constisnull;
+			else
+				has_nonconst_input = true;
+		}
+
+		result = func_form->prorettype != RECORDOID &&
+				 func_form->prokind == PROKIND_FUNCTION &&
+				 !func_form->proretset &&
+				 func_form->provolatile == PROVOLATILE_IMMUTABLE &&
+				 !has_null_input &&
+				 !has_nonconst_input;
+	}
+
+	ReleaseSysCache(tuple);
+	return result;
+}
+
 /*
  * pull_up_subqueries
  *		Look for subqueries in the rangetable that can be pulled up into
@@ -725,6 +779,11 @@ pull_up_subqueries_recurse(PlannerInfo *root, Node *jtnode,
 			is_simple_values(root, rte, deletion_ok))
 			return pull_up_simple_values(root, jtnode, rte);
 
+		if (rte->rtekind == RTE_FUNCTION &&
+				list_length(rte->functions) == 1 &&
+				is_simple_stable_function(rte))
+			return pull_up_simple_function(root, jtnode, rte);
+
 		/* Otherwise, do nothing at this node. */
 	}
 	else if (IsA(jtnode, FromExpr))
@@ -1707,6 +1766,107 @@ pull_up_simple_values(PlannerInfo *root, Node *jtnode, RangeTblEntry *rte)
 	return NULL;
 }
 
+static Node *
+pull_up_simple_function(PlannerInfo *root, Node *jtnode, RangeTblEntry *rte)
+{
+	Query	   *parse = root->parse;
+	int			varno = ((RangeTblRef *) jtnode)->rtindex;
+	List	   *functions_list;
+	List	   *tlist;
+	AttrNumber	attrno;
+	pullup_replace_vars_context rvcontext;
+	ListCell   *lc;
+
+	Assert(rte->rtekind == RTE_FUNCTION);
+	Assert(list_length(rte->functions) == 1);
+
+	/*
+	 * Need a modifiable copy of the functions list to hack on, just in case it's
+	 * multiply referenced.
+	 */
+	functions_list = copyObject(rte->functions);
+
+	/*
+	 * The FUNCTION RTE can't contain any Vars of level zero, let alone any that
+	 * are join aliases, so no need to flatten join alias Vars.
+	 */
+	Assert(!contain_vars_of_level((Node *) functions, 0));
+
+	/*
+	 * Set up required context data for pullup_replace_vars.  In particular,
+	 * we have to make the VALUES list look like a subquery targetlist.
+	 */
+	tlist = NIL;
+	attrno = 1;
+	foreach(lc, functions_list)
+	{
+		RangeTblFunction *rtf = (RangeTblFunction *)lfirst(lc);
+		tlist = lappend(tlist,
+						makeTargetEntry((Expr *) rtf->funcexpr,
+										attrno,
+										NULL,
+										false));
+		attrno++;
+	}
+	rvcontext.root = root;
+	rvcontext.targetlist = tlist;
+	rvcontext.target_rte = rte;
+	rvcontext.relids = NULL;
+	rvcontext.outer_hasSubLinks = &parse->hasSubLinks;
+	rvcontext.varno = varno;
+	rvcontext.need_phvs = false;
+	rvcontext.wrap_non_vars = false;
+	/* initialize cache array with indexes 0 .. length(tlist) */
+	rvcontext.rv_cache = palloc0((list_length(tlist) + 1) *
+								 sizeof(Node *));
+
+	/*
+	 * Replace all of the top query's references to the RTE's outputs with
+	 * copies of the adjusted VALUES expressions, being careful not to replace
+	 * any of the jointree structure. (This'd be a lot cleaner if we could use
+	 * query_tree_mutator.)  Much of this should be no-ops in the dummy Query
+	 * that surrounds a VALUES RTE, but it's not enough code to be worth
+	 * removing.
+	 */
+	parse->targetList = (List *)
+		pullup_replace_vars((Node *) parse->targetList, &rvcontext);
+	parse->returningList = (List *)
+		pullup_replace_vars((Node *) parse->returningList, &rvcontext);
+	if (parse->onConflict)
+	{
+		parse->onConflict->onConflictSet = (List *)
+			pullup_replace_vars((Node *) parse->onConflict->onConflictSet,
+								&rvcontext);
+		parse->onConflict->onConflictWhere =
+			pullup_replace_vars(parse->onConflict->onConflictWhere,
+								&rvcontext);
+
+		/*
+		 * We assume ON CONFLICT's arbiterElems, arbiterWhere, exclRelTlist
+		 * can't contain any references to a subquery
+		 */
+	}
+	replace_vars_in_jointree((Node *) parse->jointree, &rvcontext, NULL);
+	Assert(parse->setOperations == NULL);
+	parse->havingQual = pullup_replace_vars(parse->havingQual, &rvcontext);
+
+	/*
+	 * There should be no appendrels to fix, nor any join alias Vars, nor any
+	 * outer joins and hence no PlaceHolderVars.
+	 */
+	Assert(root->append_rel_list == NIL);
+	Assert(list_length(parse->rtable) == 1);
+	Assert(root->join_info_list == NIL);
+	Assert(root->placeholder_list == NIL);
+
+	/*
+	 * Return NULL to signal deletion of the VALUES RTE from the parent
+	 * jointree (and set hasDeletedRTEs to ensure cleanup later).
+	 */
+	root->hasDeletedRTEs = true;
+	return NULL;
+}
+
 /*
  * is_simple_values
  *	  Check a VALUES RTE in the range table to see if it's simple enough
