diff --git a/src/backend/access/common/tupdesc.c b/src/backend/access/common/tupdesc.c
index 1e743d7..86bb80a 100644
--- a/src/backend/access/common/tupdesc.c
+++ b/src/backend/access/common/tupdesc.c
@@ -25,6 +25,7 @@
 #include "catalog/pg_type.h"
 #include "common/hashfn.h"
 #include "miscadmin.h"
+#include "parser/parse_coerce.h"
 #include "parser/parse_type.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
@@ -927,3 +928,53 @@ BuildDescFromLists(List *names, List *types, List *typmods, List *collations)
 
 	return desc;
 }
+
+/*
+ * Check that function result tuple type (src_tupdesc) matches or can
+ * be considered to match what the query expects (dst_tupdesc). If
+ * they don't match, ereport.
+ *
+ * We really only care about number of attributes and data type.
+ * Also, we can ignore type mismatch on columns that are dropped in the
+ * destination type, so long as the physical storage matches.  This is
+ * helpful in some cases involving out-of-date cached plans.
+ */
+void
+tupledesc_match(TupleDesc dst_tupdesc, TupleDesc src_tupdesc)
+{
+	int			i;
+
+	if (dst_tupdesc->natts != src_tupdesc->natts)
+		ereport(ERROR,
+				(errcode(ERRCODE_DATATYPE_MISMATCH),
+				 errmsg("function return row and query-specified return row do not match"),
+				 errdetail_plural("Returned row contains %d attribute, but query expects %d.",
+								  "Returned row contains %d attributes, but query expects %d.",
+								  src_tupdesc->natts,
+								  src_tupdesc->natts, dst_tupdesc->natts)));
+
+	for (i = 0; i < dst_tupdesc->natts; i++)
+	{
+		Form_pg_attribute dattr = TupleDescAttr(dst_tupdesc, i);
+		Form_pg_attribute sattr = TupleDescAttr(src_tupdesc, i);
+
+		if (IsBinaryCoercible(sattr->atttypid, dattr->atttypid))
+			continue;			/* no worries */
+		if (!dattr->attisdropped)
+			ereport(ERROR,
+					(errcode(ERRCODE_DATATYPE_MISMATCH),
+					 errmsg("function return row and query-specified return row do not match"),
+					 errdetail("Returned type %s at ordinal position %d, but query expects %s.",
+							   format_type_be(sattr->atttypid),
+							   i + 1,
+							   format_type_be(dattr->atttypid))));
+
+		if (dattr->attlen != sattr->attlen ||
+			dattr->attalign != sattr->attalign)
+			ereport(ERROR,
+					(errcode(ERRCODE_DATATYPE_MISMATCH),
+					 errmsg("function return row and query-specified return row do not match"),
+					 errdetail("Physical storage mismatch on dropped attribute at ordinal position %d.",
+							   i + 1)));
+	}
+}
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index d901dc4..71e7ab5 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -19,6 +19,8 @@
 #include "commands/defrem.h"
 #include "commands/prepare.h"
 #include "executor/nodeHash.h"
+#include "executor/nodeFunctionscan.h"
+#include "executor/nodeSRFScan.h"
 #include "foreign/fdwapi.h"
 #include "jit/jit.h"
 #include "nodes/extensible.h"
@@ -1182,6 +1184,9 @@ ExplainNode(PlanState *planstate, List *ancestors,
 		case T_SubqueryScan:
 			pname = sname = "Subquery Scan";
 			break;
+		case T_SRFScanPlan:
+			pname = sname = "SRF Scan";
+			break;
 		case T_FunctionScan:
 			pname = sname = "Function Scan";
 			break;
@@ -1770,6 +1775,31 @@ ExplainNode(PlanState *planstate, List *ancestors,
 				}
 			}
 			break;
+		case T_SRFScanPlan:
+			if (es->analyze)
+			{
+				SRFScanState *sss = (SRFScanState *) planstate;
+
+				if (sss->setexpr)
+				{
+					SetExprState *setexpr = (SetExprState *) sss->setexpr;
+					FunctionCallInfo fcinfo = setexpr->fcinfo;
+					ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+
+					if (rsinfo)
+					{
+						ExplainPropertyText("SFRM",
+							rsinfo->returnMode == SFRM_ValuePerCall ? "ValuePerCall" :
+								rsinfo->returnMode == SFRM_Materialize ? "Materialize" :
+									"Unknown",
+											es);
+
+						if (rsinfo->returnMode == SFRM_Materialize)
+							ExplainPropertyBool("Donated tuplestore",
+												setexpr->funcResultStoreDonated, es);
+					}
+				}
+			}
 		case T_FunctionScan:
 			if (es->verbose)
 			{
@@ -2002,6 +2032,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 		IsA(plan, BitmapAnd) ||
 		IsA(plan, BitmapOr) ||
 		IsA(plan, SubqueryScan) ||
+		IsA(plan, FunctionScan) ||
 		(IsA(planstate, CustomScanState) &&
 		 ((CustomScanState *) planstate)->custom_ps != NIL) ||
 		planstate->subPlan;
@@ -2026,6 +2057,17 @@ ExplainNode(PlanState *planstate, List *ancestors,
 		ExplainNode(innerPlanState(planstate), ancestors,
 					"Inner", NULL, es);
 
+	/* FunctionScan subnodes */
+	if (IsA(planstate, FunctionScanState))
+		for(int i=0; i<((FunctionScanState *)planstate)->nfuncs; i++)
+		{
+			bool oldverbose = es->verbose;
+			es->verbose = false;
+			ExplainNode(&((FunctionScanState *)planstate)->funcstates[i].scanstate->ps,
+						ancestors, "Function", NULL, es);
+			es->verbose = oldverbose;
+		}
+
 	/* special child plans */
 	switch (nodeTag(plan))
 	{
diff --git a/src/backend/executor/Makefile b/src/backend/executor/Makefile
index a983800..9dae142 100644
--- a/src/backend/executor/Makefile
+++ b/src/backend/executor/Makefile
@@ -65,6 +65,7 @@ OBJS = \
 	nodeSort.o \
 	nodeSubplan.o \
 	nodeSubqueryscan.o \
+	nodeSRFScan.o \
 	nodeTableFuncscan.o \
 	nodeTidscan.o \
 	nodeUnique.o \
diff --git a/src/backend/executor/execAmi.c b/src/backend/executor/execAmi.c
index b12aeb3..07ccca75 100644
--- a/src/backend/executor/execAmi.c
+++ b/src/backend/executor/execAmi.c
@@ -25,6 +25,7 @@
 #include "executor/nodeCustom.h"
 #include "executor/nodeForeignscan.h"
 #include "executor/nodeFunctionscan.h"
+#include "executor/nodeSRFScan.h"
 #include "executor/nodeGather.h"
 #include "executor/nodeGatherMerge.h"
 #include "executor/nodeGroup.h"
@@ -204,6 +205,10 @@ ExecReScan(PlanState *node)
 			ExecReScanFunctionScan((FunctionScanState *) node);
 			break;
 
+		case T_SRFScanState:
+			ExecReScanSRF((SRFScanState *) node);
+			break;
+
 		case T_TableFuncScanState:
 			ExecReScanTableFuncScan((TableFuncScanState *) node);
 			break;
diff --git a/src/backend/executor/execProcnode.c b/src/backend/executor/execProcnode.c
index 7b2e84f..da39593 100644
--- a/src/backend/executor/execProcnode.c
+++ b/src/backend/executor/execProcnode.c
@@ -83,6 +83,7 @@
 #include "executor/nodeCustom.h"
 #include "executor/nodeForeignscan.h"
 #include "executor/nodeFunctionscan.h"
+#include "executor/nodeSRFScan.h"
 #include "executor/nodeGather.h"
 #include "executor/nodeGatherMerge.h"
 #include "executor/nodeGroup.h"
@@ -252,6 +253,11 @@ ExecInitNode(Plan *node, EState *estate, int eflags)
 														 estate, eflags);
 			break;
 
+		case T_SRFScanPlan:
+			result = (PlanState *) ExecInitSRFScan((SRFScanPlan *) node,
+														 estate, eflags);
+			break;
+
 		case T_ValuesScan:
 			result = (PlanState *) ExecInitValuesScan((ValuesScan *) node,
 													  estate, eflags);
@@ -639,6 +645,10 @@ ExecEndNode(PlanState *node)
 			ExecEndFunctionScan((FunctionScanState *) node);
 			break;
 
+		case T_SRFScanState:
+			ExecEndSRFScan((SRFScanState *) node);
+			break;
+
 		case T_TableFuncScanState:
 			ExecEndTableFuncScan((TableFuncScanState *) node);
 			break;
diff --git a/src/backend/executor/execSRF.c b/src/backend/executor/execSRF.c
index 2312cc7..e29296a 100644
--- a/src/backend/executor/execSRF.c
+++ b/src/backend/executor/execSRF.c
@@ -21,6 +21,9 @@
 #include "access/htup_details.h"
 #include "catalog/objectaccess.h"
 #include "executor/execdebug.h"
+#include "executor/nodeMaterial.h"
+#include "executor/nodeFunctionscan.h"
+#include "executor/nodeSRFScan.h"
 #include "funcapi.h"
 #include "miscadmin.h"
 #include "nodes/nodeFuncs.h"
@@ -44,17 +47,17 @@ static void ExecPrepareTuplestoreResult(SetExprState *sexpr,
 										ExprContext *econtext,
 										Tuplestorestate *resultStore,
 										TupleDesc resultDesc);
-static void tupledesc_match(TupleDesc dst_tupdesc, TupleDesc src_tupdesc);
 
 
 /*
- * Prepare function call in FROM (ROWS FROM) for execution.
+ * Prepare function call in FROM (ROWS FROM) or targetlist SRF function
+ * call for execution for execution.
  *
- * This is used by nodeFunctionscan.c.
+ * This is used by nodeFunctionscan.c and nodeProjectSet.c.
  */
 SetExprState *
-ExecInitTableFunctionResult(Expr *expr,
-							ExprContext *econtext, PlanState *parent)
+ExecInitFunctionResultSet(Expr *expr,
+						  ExprContext *econtext, PlanState *parent)
 {
 	SetExprState *state = makeNode(SetExprState);
 
@@ -62,402 +65,54 @@ ExecInitTableFunctionResult(Expr *expr,
 	state->expr = expr;
 	state->func.fn_oid = InvalidOid;
 
-	/*
-	 * Normally the passed expression tree will be a FuncExpr, since the
-	 * grammar only allows a function call at the top level of a table
-	 * function reference.  However, if the function doesn't return set then
-	 * the planner might have replaced the function call via constant-folding
-	 * or inlining.  So if we see any other kind of expression node, execute
-	 * it via the general ExecEvalExpr() code.  That code path will not
-	 * support set-returning functions buried in the expression, though.
-	 */
 	if (IsA(expr, FuncExpr))
 	{
+		/*
+		 * For a FunctionScan or ProjectSet, the passed expression tree can be a
+		 * FuncExpr, since the grammar only allows a function call at the top
+		 * level of a table function reference.
+		 */
 		FuncExpr   *func = (FuncExpr *) expr;
 
 		state->funcReturnsSet = func->funcretset;
 		state->args = ExecInitExprList(func->args, parent);
-
 		init_sexpr(func->funcid, func->inputcollid, expr, state, parent,
-				   econtext->ecxt_per_query_memory, func->funcretset, false);
+				   econtext->ecxt_per_query_memory, func->funcretset, true);
 	}
-	else
-	{
-		state->elidedFuncState = ExecInitExpr(expr, parent);
-	}
-
-	return state;
-}
-
-/*
- *		ExecMakeTableFunctionResult
- *
- * Evaluate a table function, producing a materialized result in a Tuplestore
- * object.
- *
- * This is used by nodeFunctionscan.c.
- */
-Tuplestorestate *
-ExecMakeTableFunctionResult(SetExprState *setexpr,
-							ExprContext *econtext,
-							MemoryContext argContext,
-							TupleDesc expectedDesc,
-							bool randomAccess)
-{
-	Tuplestorestate *tupstore = NULL;
-	TupleDesc	tupdesc = NULL;
-	Oid			funcrettype;
-	bool		returnsTuple;
-	bool		returnsSet = false;
-	FunctionCallInfo fcinfo;
-	PgStat_FunctionCallUsage fcusage;
-	ReturnSetInfo rsinfo;
-	HeapTupleData tmptup;
-	MemoryContext callerContext;
-	MemoryContext oldcontext;
-	bool		first_time = true;
-
-	callerContext = CurrentMemoryContext;
-
-	funcrettype = exprType((Node *) setexpr->expr);
-
-	returnsTuple = type_is_rowtype(funcrettype);
-
-	/*
-	 * Prepare a resultinfo node for communication.  We always do this even if
-	 * not expecting a set result, so that we can pass expectedDesc.  In the
-	 * generic-expression case, the expression doesn't actually get to see the
-	 * resultinfo, but set it up anyway because we use some of the fields as
-	 * our own state variables.
-	 */
-	rsinfo.type = T_ReturnSetInfo;
-	rsinfo.econtext = econtext;
-	rsinfo.expectedDesc = expectedDesc;
-	rsinfo.allowedModes = (int) (SFRM_ValuePerCall | SFRM_Materialize | SFRM_Materialize_Preferred);
-	if (randomAccess)
-		rsinfo.allowedModes |= (int) SFRM_Materialize_Random;
-	rsinfo.returnMode = SFRM_ValuePerCall;
-	/* isDone is filled below */
-	rsinfo.setResult = NULL;
-	rsinfo.setDesc = NULL;
-
-	fcinfo = palloc(SizeForFunctionCallInfo(list_length(setexpr->args)));
-
-	/*
-	 * Normally the passed expression tree will be a SetExprState, since the
-	 * grammar only allows a function call at the top level of a table
-	 * function reference.  However, if the function doesn't return set then
-	 * the planner might have replaced the function call via constant-folding
-	 * or inlining.  So if we see any other kind of expression node, execute
-	 * it via the general ExecEvalExpr() code; the only difference is that we
-	 * don't get a chance to pass a special ReturnSetInfo to any functions
-	 * buried in the expression.
-	 */
-	if (!setexpr->elidedFuncState)
+	else if (IsA(expr, OpExpr))
 	{
 		/*
-		 * This path is similar to ExecMakeFunctionResultSet.
-		 */
-		returnsSet = setexpr->funcReturnsSet;
-		InitFunctionCallInfoData(*fcinfo, &(setexpr->func),
-								 list_length(setexpr->args),
-								 setexpr->fcinfo->fncollation,
-								 NULL, (Node *) &rsinfo);
-
-		/*
-		 * Evaluate the function's argument list.
-		 *
-		 * We can't do this in the per-tuple context: the argument values
-		 * would disappear when we reset that context in the inner loop.  And
-		 * the caller's CurrentMemoryContext is typically a query-lifespan
-		 * context, so we don't want to leak memory there.  We require the
-		 * caller to pass a separate memory context that can be used for this,
-		 * and can be reset each time through to avoid bloat.
-		 */
-		MemoryContextReset(argContext);
-		oldcontext = MemoryContextSwitchTo(argContext);
-		ExecEvalFuncArgs(fcinfo, setexpr->args, econtext);
-		MemoryContextSwitchTo(oldcontext);
-
-		/*
-		 * If function is strict, and there are any NULL arguments, skip
-		 * calling the function and act like it returned NULL (or an empty
-		 * set, in the returns-set case).
+		 * For ProjectSet, the expression node could be an OpExpr.
 		 */
-		if (setexpr->func.fn_strict)
-		{
-			int			i;
+		OpExpr	   *op = (OpExpr *) expr;
 
-			for (i = 0; i < fcinfo->nargs; i++)
-			{
-				if (fcinfo->args[i].isnull)
-					goto no_function_result;
-			}
-		}
+		state->funcReturnsSet = op->opretset;
+		state->args = ExecInitExprList(op->args, parent);
+		init_sexpr(op->opfuncid, op->inputcollid, expr, state, parent,
+				   econtext->ecxt_per_query_memory, op->opretset, true);
 	}
 	else
 	{
-		/* Treat setexpr as a generic expression */
-		InitFunctionCallInfoData(*fcinfo, NULL, 0, InvalidOid, NULL, NULL);
-	}
-
-	/*
-	 * Switch to short-lived context for calling the function or expression.
-	 */
-	MemoryContextSwitchTo(econtext->ecxt_per_tuple_memory);
-
-	/*
-	 * Loop to handle the ValuePerCall protocol (which is also the same
-	 * behavior needed in the generic ExecEvalExpr path).
-	 */
-	for (;;)
-	{
-		Datum		result;
-
-		CHECK_FOR_INTERRUPTS();
-
 		/*
-		 * reset per-tuple memory context before each call of the function or
-		 * expression. This cleans up any local memory the function may leak
-		 * when called.
+		 * However, again for FunctionScan, if the function doesn't return set
+		 * then the planner might have replaced the function call via constant-
+		 * folding or inlining.  So if we see any other kind of expression node,
+		 * execute it via the general ExecEvalExpr() code.  That code path will
+		 * not support set-returning functions buried in the expression, though.
 		 */
-		ResetExprContext(econtext);
-
-		/* Call the function or expression one time */
-		if (!setexpr->elidedFuncState)
-		{
-			pgstat_init_function_usage(fcinfo, &fcusage);
-
-			fcinfo->isnull = false;
-			rsinfo.isDone = ExprSingleResult;
-			result = FunctionCallInvoke(fcinfo);
-
-			pgstat_end_function_usage(&fcusage,
-									  rsinfo.isDone != ExprMultipleResult);
-		}
-		else
-		{
-			result =
-				ExecEvalExpr(setexpr->elidedFuncState, econtext, &fcinfo->isnull);
-			rsinfo.isDone = ExprSingleResult;
-		}
-
-		/* Which protocol does function want to use? */
-		if (rsinfo.returnMode == SFRM_ValuePerCall)
-		{
-			/*
-			 * Check for end of result set.
-			 */
-			if (rsinfo.isDone == ExprEndResult)
-				break;
-
-			/*
-			 * If first time through, build tuplestore for result.  For a
-			 * scalar function result type, also make a suitable tupdesc.
-			 */
-			if (first_time)
-			{
-				oldcontext = MemoryContextSwitchTo(econtext->ecxt_per_query_memory);
-				tupstore = tuplestore_begin_heap(randomAccess, false, work_mem);
-				rsinfo.setResult = tupstore;
-				if (!returnsTuple)
-				{
-					tupdesc = CreateTemplateTupleDesc(1);
-					TupleDescInitEntry(tupdesc,
-									   (AttrNumber) 1,
-									   "column",
-									   funcrettype,
-									   -1,
-									   0);
-					rsinfo.setDesc = tupdesc;
-				}
-				MemoryContextSwitchTo(oldcontext);
-			}
-
-			/*
-			 * Store current resultset item.
-			 */
-			if (returnsTuple)
-			{
-				if (!fcinfo->isnull)
-				{
-					HeapTupleHeader td = DatumGetHeapTupleHeader(result);
-
-					if (tupdesc == NULL)
-					{
-						/*
-						 * This is the first non-NULL result from the
-						 * function.  Use the type info embedded in the
-						 * rowtype Datum to look up the needed tupdesc.  Make
-						 * a copy for the query.
-						 */
-						oldcontext = MemoryContextSwitchTo(econtext->ecxt_per_query_memory);
-						tupdesc = lookup_rowtype_tupdesc_copy(HeapTupleHeaderGetTypeId(td),
-															  HeapTupleHeaderGetTypMod(td));
-						rsinfo.setDesc = tupdesc;
-						MemoryContextSwitchTo(oldcontext);
-					}
-					else
-					{
-						/*
-						 * Verify all later returned rows have same subtype;
-						 * necessary in case the type is RECORD.
-						 */
-						if (HeapTupleHeaderGetTypeId(td) != tupdesc->tdtypeid ||
-							HeapTupleHeaderGetTypMod(td) != tupdesc->tdtypmod)
-							ereport(ERROR,
-									(errcode(ERRCODE_DATATYPE_MISMATCH),
-									 errmsg("rows returned by function are not all of the same row type")));
-					}
-
-					/*
-					 * tuplestore_puttuple needs a HeapTuple not a bare
-					 * HeapTupleHeader, but it doesn't need all the fields.
-					 */
-					tmptup.t_len = HeapTupleHeaderGetDatumLength(td);
-					tmptup.t_data = td;
-
-					tuplestore_puttuple(tupstore, &tmptup);
-				}
-				else
-				{
-					/*
-					 * NULL result from a tuple-returning function; expand it
-					 * to a row of all nulls.  We rely on the expectedDesc to
-					 * form such rows.  (Note: this would be problematic if
-					 * tuplestore_putvalues saved the tdtypeid/tdtypmod from
-					 * the provided descriptor, since that might not match
-					 * what we get from the function itself.  But it doesn't.)
-					 */
-					int			natts = expectedDesc->natts;
-					bool	   *nullflags;
-
-					nullflags = (bool *) palloc(natts * sizeof(bool));
-					memset(nullflags, true, natts * sizeof(bool));
-					tuplestore_putvalues(tupstore, expectedDesc, NULL, nullflags);
-				}
-			}
-			else
-			{
-				/* Scalar-type case: just store the function result */
-				tuplestore_putvalues(tupstore, tupdesc, &result, &fcinfo->isnull);
-			}
-
-			/*
-			 * Are we done?
-			 */
-			if (rsinfo.isDone != ExprMultipleResult)
-				break;
-		}
-		else if (rsinfo.returnMode == SFRM_Materialize)
-		{
-			/* check we're on the same page as the function author */
-			if (!first_time || rsinfo.isDone != ExprSingleResult)
-				ereport(ERROR,
-						(errcode(ERRCODE_E_R_I_E_SRF_PROTOCOL_VIOLATED),
-						 errmsg("table-function protocol for materialize mode was not followed")));
-			/* Done evaluating the set result */
-			break;
-		}
-		else
-			ereport(ERROR,
-					(errcode(ERRCODE_E_R_I_E_SRF_PROTOCOL_VIOLATED),
-					 errmsg("unrecognized table-function returnMode: %d",
-							(int) rsinfo.returnMode)));
-
-		first_time = false;
-	}
-
-no_function_result:
-
-	/*
-	 * If we got nothing from the function (ie, an empty-set or NULL result),
-	 * we have to create the tuplestore to return, and if it's a
-	 * non-set-returning function then insert a single all-nulls row.  As
-	 * above, we depend on the expectedDesc to manufacture the dummy row.
-	 */
-	if (rsinfo.setResult == NULL)
-	{
-		MemoryContextSwitchTo(econtext->ecxt_per_query_memory);
-		tupstore = tuplestore_begin_heap(randomAccess, false, work_mem);
-		rsinfo.setResult = tupstore;
-		if (!returnsSet)
-		{
-			int			natts = expectedDesc->natts;
-			bool	   *nullflags;
-
-			MemoryContextSwitchTo(econtext->ecxt_per_tuple_memory);
-			nullflags = (bool *) palloc(natts * sizeof(bool));
-			memset(nullflags, true, natts * sizeof(bool));
-			tuplestore_putvalues(tupstore, expectedDesc, NULL, nullflags);
-		}
-	}
-
-	/*
-	 * If function provided a tupdesc, cross-check it.  We only really need to
-	 * do this for functions returning RECORD, but might as well do it always.
-	 */
-	if (rsinfo.setDesc)
-	{
-		tupledesc_match(expectedDesc, rsinfo.setDesc);
-
-		/*
-		 * If it is a dynamically-allocated TupleDesc, free it: it is
-		 * typically allocated in a per-query context, so we must avoid
-		 * leaking it across multiple usages.
-		 */
-		if (rsinfo.setDesc->tdrefcount == -1)
-			FreeTupleDesc(rsinfo.setDesc);
-	}
-
-	MemoryContextSwitchTo(callerContext);
-
-	/* All done, pass back the tuplestore */
-	return rsinfo.setResult;
-}
-
+		MemoryContext oldcontext;
 
-/*
- * Prepare targetlist SRF function call for execution.
- *
- * This is used by nodeProjectSet.c.
- */
-SetExprState *
-ExecInitFunctionResultSet(Expr *expr,
-						  ExprContext *econtext, PlanState *parent)
-{
-	SetExprState *state = makeNode(SetExprState);
+		state->elidedFuncState = ExecInitExpr(expr, parent);
 
-	state->funcReturnsSet = true;
-	state->expr = expr;
-	state->func.fn_oid = InvalidOid;
+		oldcontext = MemoryContextSwitchTo(econtext->ecxt_per_query_memory);
 
-	/*
-	 * Initialize metadata.  The expression node could be either a FuncExpr or
-	 * an OpExpr.
-	 */
-	if (IsA(expr, FuncExpr))
-	{
-		FuncExpr   *func = (FuncExpr *) expr;
+		/* By performing InitFunctionCallInfoData here, we avoid palloc0() */
+		state->fcinfo = palloc(SizeForFunctionCallInfo(list_length(state->args)));
 
-		state->args = ExecInitExprList(func->args, parent);
-		init_sexpr(func->funcid, func->inputcollid, expr, state, parent,
-				   econtext->ecxt_per_query_memory, true, true);
-	}
-	else if (IsA(expr, OpExpr))
-	{
-		OpExpr	   *op = (OpExpr *) expr;
+		MemoryContextSwitchTo(oldcontext);
 
-		state->args = ExecInitExprList(op->args, parent);
-		init_sexpr(op->opfuncid, op->inputcollid, expr, state, parent,
-				   econtext->ecxt_per_query_memory, true, true);
+		InitFunctionCallInfoData(*state->fcinfo, NULL, 0, InvalidOid, NULL, NULL);
 	}
-	else
-		elog(ERROR, "unrecognized node type: %d",
-			 (int) nodeTag(expr));
-
-	/* shouldn't get here unless the selected function returns set */
-	Assert(state->func.fn_retset);
 
 	return state;
 }
@@ -473,7 +128,7 @@ ExecInitFunctionResultSet(Expr *expr,
  * needs to live until all rows have been returned (i.e. *isDone set to
  * ExprEndResult or ExprSingleResult).
  *
- * This is used by nodeProjectSet.c.
+ * This is used by nodeProjectSet.c and nodeFunctionscan.c.
  */
 Datum
 ExecMakeFunctionResultSet(SetExprState *fcache,
@@ -486,7 +141,7 @@ ExecMakeFunctionResultSet(SetExprState *fcache,
 	Datum		result;
 	FunctionCallInfo fcinfo;
 	PgStat_FunctionCallUsage fcusage;
-	ReturnSetInfo rsinfo;
+	ReturnSetInfo *rsinfo;
 	bool		callit;
 	int			i;
 
@@ -540,6 +195,28 @@ restart:
 	}
 
 	/*
+	 * Prepare a resultinfo node for communication.  We always do this even if
+	 * not expecting a set result, so that we can pass expectedDesc.  In the
+	 * generic-expression case, the expression doesn't actually get to see the
+	 * resultinfo, but set it up anyway because we use some of the fields as
+	 * our own state variables.
+	 */
+	fcinfo = fcache->fcinfo;
+	rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+
+	if (rsinfo == NULL)
+	{
+		MemoryContext oldContext = MemoryContextSwitchTo(econtext->ecxt_per_query_memory);
+
+		rsinfo = makeNode (ReturnSetInfo);
+		rsinfo->econtext = econtext;
+		rsinfo->expectedDesc = fcache->funcResultDesc;
+		fcinfo->resultinfo = (Node *) rsinfo;
+
+		MemoryContextSwitchTo(oldContext);
+	}
+
+	/*
 	 * arguments is a list of expressions to evaluate before passing to the
 	 * function manager.  We skip the evaluation if it was already done in the
 	 * previous call (ie, we are continuing the evaluation of a set-valued
@@ -549,7 +226,6 @@ restart:
 	 * rows from this SRF have been returned, otherwise ValuePerCall SRFs
 	 * would reference freed memory after the first returned row.
 	 */
-	fcinfo = fcache->fcinfo;
 	arguments = fcache->args;
 	if (!fcache->setArgsValid)
 	{
@@ -557,6 +233,14 @@ restart:
 
 		ExecEvalFuncArgs(fcinfo, arguments, econtext);
 		MemoryContextSwitchTo(oldContext);
+
+		/* Reset the rsinfo structure */
+		rsinfo->allowedModes = (int) (SFRM_ValuePerCall | SFRM_Materialize);
+		/* note we do not set SFRM_Materialize_Random or _Preferred */
+		rsinfo->returnMode = SFRM_ValuePerCall;
+		/* isDone is filled below */
+		rsinfo->setResult = NULL;
+		rsinfo->setDesc = NULL;
 	}
 	else
 	{
@@ -568,18 +252,6 @@ restart:
 	 * Now call the function, passing the evaluated parameter values.
 	 */
 
-	/* Prepare a resultinfo node for communication. */
-	fcinfo->resultinfo = (Node *) &rsinfo;
-	rsinfo.type = T_ReturnSetInfo;
-	rsinfo.econtext = econtext;
-	rsinfo.expectedDesc = fcache->funcResultDesc;
-	rsinfo.allowedModes = (int) (SFRM_ValuePerCall | SFRM_Materialize);
-	/* note we do not set SFRM_Materialize_Random or _Preferred */
-	rsinfo.returnMode = SFRM_ValuePerCall;
-	/* isDone is filled below */
-	rsinfo.setResult = NULL;
-	rsinfo.setDesc = NULL;
-
 	/*
 	 * If function is strict, and there are any NULL arguments, skip calling
 	 * the function.
@@ -599,16 +271,25 @@ restart:
 
 	if (callit)
 	{
-		pgstat_init_function_usage(fcinfo, &fcusage);
+		if (!fcache->elidedFuncState)
+		{
+			pgstat_init_function_usage(fcinfo, &fcusage);
 
-		fcinfo->isnull = false;
-		rsinfo.isDone = ExprSingleResult;
-		result = FunctionCallInvoke(fcinfo);
-		*isNull = fcinfo->isnull;
-		*isDone = rsinfo.isDone;
+			fcinfo->isnull = false;
+			rsinfo->isDone = ExprSingleResult;
+			result = FunctionCallInvoke(fcinfo);
+			*isNull = fcinfo->isnull;
+			*isDone = rsinfo->isDone;
 
-		pgstat_end_function_usage(&fcusage,
-								  rsinfo.isDone != ExprMultipleResult);
+			pgstat_end_function_usage(&fcusage,
+									  rsinfo->isDone != ExprMultipleResult);
+		}
+		else
+		{
+			result =
+				ExecEvalExpr(fcache->elidedFuncState, econtext, isNull);
+			*isDone = ExprSingleResult;
+		}
 	}
 	else
 	{
@@ -619,11 +300,32 @@ restart:
 	}
 
 	/* Which protocol does function want to use? */
-	if (rsinfo.returnMode == SFRM_ValuePerCall)
+	if (rsinfo->returnMode == SFRM_ValuePerCall)
 	{
 		if (*isDone != ExprEndResult)
 		{
 			/*
+			 * Obtain a suitable tupdesc, when we first encounter a non-NULL result.
+			 */
+			if (rsinfo->setDesc == NULL)
+			{
+				if (fcache->funcReturnsTuple && !*isNull)
+				{
+					HeapTupleHeader td = DatumGetHeapTupleHeader(result);
+
+					/*
+					 * This is the first non-NULL result from the
+					 * function.  Use the type info embedded in the
+					 * rowtype Datum to look up the needed tupdesc.  Make
+					 * a copy for the query.
+					 */
+					MemoryContext oldcontext = MemoryContextSwitchTo(econtext->ecxt_per_query_memory);
+					rsinfo->setDesc = lookup_rowtype_tupdesc_copy(HeapTupleHeaderGetTypeId(td), HeapTupleHeaderGetTypMod(td));
+					MemoryContextSwitchTo(oldcontext);
+				}
+			}
+
+			/*
 			 * Save the current argument values to re-use on the next call.
 			 */
 			if (*isDone == ExprMultipleResult)
@@ -640,21 +342,34 @@ restart:
 			}
 		}
 	}
-	else if (rsinfo.returnMode == SFRM_Materialize)
+	else if (rsinfo->returnMode == SFRM_Materialize)
 	{
 		/* check we're on the same page as the function author */
-		if (rsinfo.isDone != ExprSingleResult)
+		if (rsinfo->isDone != ExprSingleResult)
 			ereport(ERROR,
 					(errcode(ERRCODE_E_R_I_E_SRF_PROTOCOL_VIOLATED),
 					 errmsg("table-function protocol for materialize mode was not followed")));
-		if (rsinfo.setResult != NULL)
+		if (rsinfo->setResult != NULL)
 		{
 			/* prepare to return values from the tuplestore */
 			ExecPrepareTuplestoreResult(fcache, econtext,
-										rsinfo.setResult,
-										rsinfo.setDesc);
-			/* loop back to top to start returning from tuplestore */
-			goto restart;
+										rsinfo->setResult,
+										rsinfo->setDesc);
+
+			/*
+			 * If we are being invoked by a Materialize node, attempt
+			 * to donate the returned tuplstore to it.
+			 */
+			if (ExecSRFDonateResultTuplestore(fcache))
+			{
+				*isDone = ExprMultipleResult;
+				return 0;
+			}
+			else
+			{
+				/* loop back to top to start returning from tuplestore */
+				goto restart;
+			}
 		}
 		/* if setResult was left null, treat it as empty set */
 		*isDone = ExprEndResult;
@@ -665,7 +380,7 @@ restart:
 		ereport(ERROR,
 				(errcode(ERRCODE_E_R_I_E_SRF_PROTOCOL_VIOLATED),
 				 errmsg("unrecognized table-function returnMode: %d",
-						(int) rsinfo.returnMode)));
+						(int) rsinfo->returnMode)));
 
 	return result;
 }
@@ -712,6 +427,7 @@ init_sexpr(Oid foid, Oid input_collation, Expr *node,
 	InitFunctionCallInfoData(*sexpr->fcinfo, &(sexpr->func),
 							 numargs,
 							 input_collation, NULL, NULL);
+	sexpr->fcinfo->resultinfo = NULL;
 
 	/* If function returns set, check if that's allowed by caller */
 	if (sexpr->func.fn_retset && !allowSRF)
@@ -782,6 +498,7 @@ init_sexpr(Oid foid, Oid input_collation, Expr *node,
 	sexpr->funcResultStore = NULL;
 	sexpr->funcResultSlot = NULL;
 	sexpr->shutdown_reg = false;
+	sexpr->funcResultStoreDonationEnabled = false;
 }
 
 /*
@@ -792,6 +509,7 @@ static void
 ShutdownSetExpr(Datum arg)
 {
 	SetExprState *sexpr = castNode(SetExprState, DatumGetPointer(arg));
+	ReturnSetInfo *rsinfo = castNode(ReturnSetInfo, sexpr->fcinfo->resultinfo);
 
 	/* If we have a slot, make sure it's let go of any tuplestore pointer */
 	if (sexpr->funcResultSlot)
@@ -802,6 +520,13 @@ ShutdownSetExpr(Datum arg)
 		tuplestore_end(sexpr->funcResultStore);
 	sexpr->funcResultStore = NULL;
 
+	/* Release the ReturnSetInfo structure */
+	if (rsinfo != NULL)
+	{
+		pfree(rsinfo);
+		sexpr->fcinfo->resultinfo = NULL;
+	}
+
 	/* Clear any active set-argument state */
 	sexpr->setArgsValid = false;
 
@@ -910,53 +635,3 @@ ExecPrepareTuplestoreResult(SetExprState *sexpr,
 		sexpr->shutdown_reg = true;
 	}
 }
-
-/*
- * Check that function result tuple type (src_tupdesc) matches or can
- * be considered to match what the query expects (dst_tupdesc). If
- * they don't match, ereport.
- *
- * We really only care about number of attributes and data type.
- * Also, we can ignore type mismatch on columns that are dropped in the
- * destination type, so long as the physical storage matches.  This is
- * helpful in some cases involving out-of-date cached plans.
- */
-static void
-tupledesc_match(TupleDesc dst_tupdesc, TupleDesc src_tupdesc)
-{
-	int			i;
-
-	if (dst_tupdesc->natts != src_tupdesc->natts)
-		ereport(ERROR,
-				(errcode(ERRCODE_DATATYPE_MISMATCH),
-				 errmsg("function return row and query-specified return row do not match"),
-				 errdetail_plural("Returned row contains %d attribute, but query expects %d.",
-								  "Returned row contains %d attributes, but query expects %d.",
-								  src_tupdesc->natts,
-								  src_tupdesc->natts, dst_tupdesc->natts)));
-
-	for (i = 0; i < dst_tupdesc->natts; i++)
-	{
-		Form_pg_attribute dattr = TupleDescAttr(dst_tupdesc, i);
-		Form_pg_attribute sattr = TupleDescAttr(src_tupdesc, i);
-
-		if (IsBinaryCoercible(sattr->atttypid, dattr->atttypid))
-			continue;			/* no worries */
-		if (!dattr->attisdropped)
-			ereport(ERROR,
-					(errcode(ERRCODE_DATATYPE_MISMATCH),
-					 errmsg("function return row and query-specified return row do not match"),
-					 errdetail("Returned type %s at ordinal position %d, but query expects %s.",
-							   format_type_be(sattr->atttypid),
-							   i + 1,
-							   format_type_be(dattr->atttypid))));
-
-		if (dattr->attlen != sattr->attlen ||
-			dattr->attalign != sattr->attalign)
-			ereport(ERROR,
-					(errcode(ERRCODE_DATATYPE_MISMATCH),
-					 errmsg("function return row and query-specified return row do not match"),
-					 errdetail("Physical storage mismatch on dropped attribute at ordinal position %d.",
-							   i + 1)));
-	}
-}
diff --git a/src/backend/executor/nodeFunctionscan.c b/src/backend/executor/nodeFunctionscan.c
index ccb66ce..7499705 100644
--- a/src/backend/executor/nodeFunctionscan.c
+++ b/src/backend/executor/nodeFunctionscan.c
@@ -1,7 +1,23 @@
 /*-------------------------------------------------------------------------
  *
  * nodeFunctionscan.c
- *	  Support routines for scanning RangeFunctions (functions in rangetable).
+ *	  Coordinates a scan over PL functions. It supports several use cases:
+ *
+ *      - single function scan, and multiple functions in ROWS FROM;
+ *      - SRFs and regular functions;
+ *      - tuple- and scalar-returning functions;
+ *      - it will materialise if eflags call for it;
+ *      - if possible, it will pipeline its output;
+ *      - it avoids double-materialisation in case of SFRM_Materialize.
+ *
+ *    To achieve these, it depends upon the Materialize (for materialisation
+ *    and pipelining) and SRFScan (for SRF handling, and tuple expansion,
+ *    and double-materialisation avoidance) nodes, and the actual function
+ *    invocation (for SRF- and regular functions alike) is done in execSRF.c.
+ *
+ *    The Planner knows nothing of the Materialize and SRFScan structures.
+ *    They are constructed by the Executor at execution time, and are reported
+ *    in the EXPLAIN output.
  *
  * Portions Copyright (c) 1996-2020, PostgreSQL Global Development Group
  * Portions Copyright (c) 1994, Regents of the University of California
@@ -24,26 +40,15 @@
 
 #include "catalog/pg_type.h"
 #include "executor/nodeFunctionscan.h"
+#include "executor/nodeSRFScan.h"
+#include "executor/nodeMaterial.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/makefuncs.h"
+#include "parser/parse_type.h"
 #include "utils/builtins.h"
 #include "utils/memutils.h"
-
-
-/*
- * Runtime data for each function being scanned.
- */
-typedef struct FunctionScanPerFuncState
-{
-	SetExprState *setexpr;		/* state of the expression being evaluated */
-	TupleDesc	tupdesc;		/* desc of the function result type */
-	int			colcount;		/* expected number of result columns */
-	Tuplestorestate *tstore;	/* holds the function result set */
-	int64		rowcount;		/* # of rows in result set, -1 if not known */
-	TupleTableSlot *func_slot;	/* function result slot (or NULL) */
-} FunctionScanPerFuncState;
-
-static TupleTableSlot *FunctionNext(FunctionScanState *node);
+#include "utils/syscache.h"
 
 
 /* ----------------------------------------------------------------
@@ -82,37 +87,22 @@ FunctionNext(FunctionScanState *node)
 		 * into the scan result slot. No need to update ordinality or
 		 * rowcounts either.
 		 */
-		Tuplestorestate *tstore = node->funcstates[0].tstore;
+		TupleTableSlot *rs = node->funcstates[0].scanstate->ps.ps_ResultTupleSlot;
 
 		/*
-		 * If first time through, read all tuples from function and put them
-		 * in a tuplestore. Subsequent calls just fetch tuples from
-		 * tuplestore.
+		 * Get the next tuple from the Scan node.
+		 *
+		 * If we have a rowcount for the function, and we know the previous
+		 * read position was out of bounds, don't try the read. This allows
+		 * backward scan to work when there are mixed row counts present.
 		 */
-		if (tstore == NULL)
-		{
-			node->funcstates[0].tstore = tstore =
-				ExecMakeTableFunctionResult(node->funcstates[0].setexpr,
-											node->ss.ps.ps_ExprContext,
-											node->argcontext,
-											node->funcstates[0].tupdesc,
-											node->eflags & EXEC_FLAG_BACKWARD);
+		rs = ExecProcNode(&node->funcstates[0].scanstate->ps);
 
-			/*
-			 * paranoia - cope if the function, which may have constructed the
-			 * tuplestore itself, didn't leave it pointing at the start. This
-			 * call is fast, so the overhead shouldn't be an issue.
-			 */
-			tuplestore_rescan(tstore);
-		}
+		if (TupIsNull(rs))
+			return NULL;
+
+		ExecCopySlot(scanslot, rs);
 
-		/*
-		 * Get the next tuple from tuplestore.
-		 */
-		(void) tuplestore_gettupleslot(tstore,
-									   ScanDirectionIsForward(direction),
-									   false,
-									   scanslot);
 		return scanslot;
 	}
 
@@ -141,46 +131,22 @@ FunctionNext(FunctionScanState *node)
 	for (funcno = 0; funcno < node->nfuncs; funcno++)
 	{
 		FunctionScanPerFuncState *fs = &node->funcstates[funcno];
+		TupleTableSlot *func_slot = fs->scanstate->ps.ps_ResultTupleSlot;
 		int			i;
 
 		/*
-		 * If first time through, read all tuples from function and put them
-		 * in a tuplestore. Subsequent calls just fetch tuples from
-		 * tuplestore.
-		 */
-		if (fs->tstore == NULL)
-		{
-			fs->tstore =
-				ExecMakeTableFunctionResult(fs->setexpr,
-											node->ss.ps.ps_ExprContext,
-											node->argcontext,
-											fs->tupdesc,
-											node->eflags & EXEC_FLAG_BACKWARD);
-
-			/*
-			 * paranoia - cope if the function, which may have constructed the
-			 * tuplestore itself, didn't leave it pointing at the start. This
-			 * call is fast, so the overhead shouldn't be an issue.
-			 */
-			tuplestore_rescan(fs->tstore);
-		}
-
-		/*
-		 * Get the next tuple from tuplestore.
+		 * Get the next tuple from the Scan node.
 		 *
 		 * If we have a rowcount for the function, and we know the previous
 		 * read position was out of bounds, don't try the read. This allows
 		 * backward scan to work when there are mixed row counts present.
 		 */
 		if (fs->rowcount != -1 && fs->rowcount < oldpos)
-			ExecClearTuple(fs->func_slot);
+			ExecClearTuple(func_slot);
 		else
-			(void) tuplestore_gettupleslot(fs->tstore,
-										   ScanDirectionIsForward(direction),
-										   false,
-										   fs->func_slot);
+			func_slot = ExecProcNode(&fs->scanstate->ps);
 
-		if (TupIsNull(fs->func_slot))
+		if (TupIsNull(func_slot))
 		{
 			/*
 			 * If we ran out of data for this function in the forward
@@ -207,12 +173,12 @@ FunctionNext(FunctionScanState *node)
 			/*
 			 * we have a result, so just copy it to the result cols.
 			 */
-			slot_getallattrs(fs->func_slot);
+			slot_getallattrs(func_slot);
 
 			for (i = 0; i < fs->colcount; i++)
 			{
-				scanslot->tts_values[att] = fs->func_slot->tts_values[i];
-				scanslot->tts_isnull[att] = fs->func_slot->tts_isnull[i];
+				scanslot->tts_values[att] = func_slot->tts_values[i];
+				scanslot->tts_isnull[att] = func_slot->tts_isnull[i];
 				att++;
 			}
 
@@ -272,6 +238,53 @@ ExecFunctionScan(PlanState *pstate)
 					(ExecScanRecheckMtd) FunctionRecheck);
 }
 
+/*
+ * Helper function to build target list, which is required in order for
+ * normal processing of ExecInit, from the tupdesc.
+ */
+static void
+build_tlist_for_tupdesc(TupleDesc tupdesc, int colcount,
+						List **mat_tlist, List **scan_tlist)
+{
+	Form_pg_attribute attr;
+	int attno;
+
+	for (attno = 1; attno <= colcount; attno++)
+	{
+		attr = TupleDescAttr(tupdesc, attno - 1);
+
+		if (attr->attisdropped)
+		{
+			*scan_tlist = lappend(*scan_tlist,
+							  makeTargetEntry((Expr *)
+								  makeConst(INT2OID, -1,
+											0,
+											attr->attlen,
+											0 /* value */, true /* isnull */,
+											true),
+								  attno, attr->attname.data,
+								  attr->attisdropped));
+			*mat_tlist = lappend(*mat_tlist,
+							 makeTargetEntry((Expr *)
+								 makeVar(1 /* varno */, attno, INT2OID, -1, 0, 0),
+								 attno, attr->attname.data, attr->attisdropped));
+		}
+		else
+		{
+			*scan_tlist = lappend(*scan_tlist,
+							  makeTargetEntry((Expr *)
+								  makeVar(1 /* varno */, attno, attr->atttypid,
+										  attr->atttypmod, attr->attcollation, 0),
+								  attno, attr->attname.data, attr->attisdropped));
+			*mat_tlist = lappend(*mat_tlist,
+							 makeTargetEntry((Expr *)
+								 makeVar(1 /* varno */, attno, attr->atttypid,
+										 attr->atttypmod, attr->attcollation, 0),
+								 attno, attr->attname.data, attr->attisdropped));
+		}
+	}
+}
+
 /* ----------------------------------------------------------------
  *		ExecInitFunctionScan
  * ----------------------------------------------------------------
@@ -285,6 +298,7 @@ ExecInitFunctionScan(FunctionScan *node, EState *estate, int eflags)
 	int			i,
 				natts;
 	ListCell   *lc;
+	bool 		needs_material;
 
 	/* check for unsupported flags */
 	Assert(!(eflags & EXEC_FLAG_MARK));
@@ -315,6 +329,9 @@ ExecInitFunctionScan(FunctionScan *node, EState *estate, int eflags)
 	else
 		scanstate->simple = false;
 
+	/* Only add a Mterialize node if required */
+	needs_material = eflags & (EXEC_FLAG_REWIND | EXEC_FLAG_BACKWARD);
+
 	/*
 	 * Ordinal 0 represents the "before the first row" position.
 	 *
@@ -347,23 +364,17 @@ ExecInitFunctionScan(FunctionScan *node, EState *estate, int eflags)
 		TypeFuncClass functypclass;
 		Oid			funcrettype;
 		TupleDesc	tupdesc;
+		SRFScanPlan *srfscan;
+		Plan *scan;
+		List /* TargetEntry* */ *mat_tlist = NIL;
+		List /* TargetEntry* */ *scan_tlist = NIL;
+		bool funcReturnsTuple;
 
-		fs->setexpr =
-			ExecInitTableFunctionResult((Expr *) funcexpr,
-										scanstate->ss.ps.ps_ExprContext,
-										&scanstate->ss.ps);
-
-		/*
-		 * Don't allocate the tuplestores; the actual calls to the functions
-		 * do that.  NULL means that we have not called the function yet (or
-		 * need to call it again after a rescan).
-		 */
-		fs->tstore = NULL;
 		fs->rowcount = -1;
 
 		/*
 		 * Now determine if the function returns a simple or composite type,
-		 * and build an appropriate tupdesc.  Note that in the composite case,
+		 * and build an appropriate targetlist.  Note that in the composite case,
 		 * the function may now return more columns than it did when the plan
 		 * was made; we have to ignore any columns beyond "colcount".
 		 */
@@ -379,6 +390,7 @@ ExecInitFunctionScan(FunctionScan *node, EState *estate, int eflags)
 			Assert(tupdesc->natts >= colcount);
 			/* Must copy it out of typcache for safety */
 			tupdesc = CreateTupleDescCopy(tupdesc);
+			funcReturnsTuple = true;
 		}
 		else if (functypclass == TYPEFUNC_SCALAR)
 		{
@@ -393,6 +405,7 @@ ExecInitFunctionScan(FunctionScan *node, EState *estate, int eflags)
 			TupleDescInitEntryCollation(tupdesc,
 										(AttrNumber) 1,
 										exprCollation(funcexpr));
+			funcReturnsTuple = false;
 		}
 		else if (functypclass == TYPEFUNC_RECORD)
 		{
@@ -407,6 +420,7 @@ ExecInitFunctionScan(FunctionScan *node, EState *estate, int eflags)
 			 * case it doesn't.)
 			 */
 			BlessTupleDesc(tupdesc);
+			funcReturnsTuple = true;
 		}
 		else
 		{
@@ -414,21 +428,45 @@ ExecInitFunctionScan(FunctionScan *node, EState *estate, int eflags)
 			elog(ERROR, "function in FROM has unsupported return type");
 		}
 
-		fs->tupdesc = tupdesc;
 		fs->colcount = colcount;
 
-		/*
-		 * We only need separate slots for the function results if we are
-		 * doing ordinality or multiple functions; otherwise, we'll fetch
-		 * function results directly into the scan slot.
-		 */
-		if (!scanstate->simple)
+		/* Expand tupdesc into targetlists for the scan nodes */
+		build_tlist_for_tupdesc(tupdesc, colcount, &mat_tlist, &scan_tlist);
+
+		srfscan = makeNode(SRFScanPlan);
+		srfscan->funcexpr = funcexpr;
+		srfscan->rtfunc = (Node *) rtfunc;
+		srfscan->plan.targetlist = scan_tlist;
+		srfscan->plan.extParam = rtfunc->funcparams;
+		srfscan->plan.allParam = rtfunc->funcparams;
+		srfscan->funcResultDesc = tupdesc;
+		srfscan->funcReturnsTuple = funcReturnsTuple;
+		scan = &srfscan->plan;
+
+		if (needs_material)
 		{
-			fs->func_slot = ExecInitExtraTupleSlot(estate, fs->tupdesc,
-												   &TTSOpsMinimalTuple);
+			Material *fscan = makeNode(Material);
+			fscan->plan.lefttree = scan;
+			fscan->plan.targetlist = mat_tlist;
+			fscan->plan.extParam = rtfunc->funcparams;
+			fscan->plan.allParam = rtfunc->funcparams;
+			scan = &fscan->plan;
+		}
+
+		fs->scanstate = (ScanState *) ExecInitNode (scan, estate, eflags);
+
+		if (needs_material)
+		{
+			/*
+			 * Tell the SRFScan about its parent, so that it can donate
+			 * the SRF's tuplestore if the SRF uses SFRM_Materialize.
+			 */
+			MaterialState *ms = (MaterialState *) fs->scanstate;
+			SRFScanState *sss = (SRFScanState *) outerPlanState(ms);
+
+			sss->setexpr->funcResultStoreDonationEnabled = true;
+			sss->setexpr->funcResultStoreDonationTarget = &ms->ss.ps;
 		}
-		else
-			fs->func_slot = NULL;
 
 		natts += colcount;
 		i++;
@@ -443,7 +481,11 @@ ExecInitFunctionScan(FunctionScan *node, EState *estate, int eflags)
 	 */
 	if (scanstate->simple)
 	{
-		scan_tupdesc = CreateTupleDescCopy(scanstate->funcstates[0].tupdesc);
+		SRFScanState *sss = IsA(scanstate->funcstates[0].scanstate, MaterialState) ?
+				(SRFScanState *) outerPlanState((MaterialState *) scanstate->funcstates[0].scanstate) :
+				(SRFScanState *) scanstate->funcstates[0].scanstate;
+
+		scan_tupdesc = CreateTupleDescCopy(sss->setexpr->funcResultDesc);
 		scan_tupdesc->tdtypeid = RECORDOID;
 		scan_tupdesc->tdtypmod = -1;
 	}
@@ -458,8 +500,12 @@ ExecInitFunctionScan(FunctionScan *node, EState *estate, int eflags)
 
 		for (i = 0; i < nfuncs; i++)
 		{
-			TupleDesc	tupdesc = scanstate->funcstates[i].tupdesc;
-			int			colcount = scanstate->funcstates[i].colcount;
+			SRFScanState *sss = IsA(scanstate->funcstates[i].scanstate, MaterialState) ?
+					(SRFScanState *) outerPlanState((MaterialState *) scanstate->funcstates[i].scanstate) :
+					(SRFScanState *) scanstate->funcstates[i].scanstate;
+
+			TupleDesc	tupdesc = sss->setexpr->funcResultDesc;
+			int			colcount = sss->colcount;
 			int			j;
 
 			for (j = 1; j <= colcount; j++)
@@ -536,20 +582,11 @@ ExecEndFunctionScan(FunctionScanState *node)
 	ExecClearTuple(node->ss.ss_ScanTupleSlot);
 
 	/*
-	 * Release slots and tuplestore resources
+	 * Release the Material scan resources
 	 */
 	for (i = 0; i < node->nfuncs; i++)
 	{
-		FunctionScanPerFuncState *fs = &node->funcstates[i];
-
-		if (fs->func_slot)
-			ExecClearTuple(fs->func_slot);
-
-		if (fs->tstore != NULL)
-		{
-			tuplestore_end(node->funcstates[i].tstore);
-			fs->tstore = NULL;
-		}
+		ExecEndNode(&node->funcstates[i].scanstate->ps);
 	}
 }
 
@@ -568,23 +605,12 @@ ExecReScanFunctionScan(FunctionScanState *node)
 
 	if (node->ss.ps.ps_ResultTupleSlot)
 		ExecClearTuple(node->ss.ps.ps_ResultTupleSlot);
-	for (i = 0; i < node->nfuncs; i++)
-	{
-		FunctionScanPerFuncState *fs = &node->funcstates[i];
-
-		if (fs->func_slot)
-			ExecClearTuple(fs->func_slot);
-	}
 
 	ExecScanReScan(&node->ss);
 
 	/*
-	 * Here we have a choice whether to drop the tuplestores (and recompute
-	 * the function outputs) or just rescan them.  We must recompute if an
-	 * expression contains changed parameters, else we rescan.
-	 *
-	 * XXX maybe we should recompute if the function is volatile?  But in
-	 * general the executor doesn't conditionalize its actions on that.
+	 * We must recompute if an
+	 * expression contains changed parameters.
 	 */
 	if (chgparam)
 	{
@@ -597,11 +623,9 @@ ExecReScanFunctionScan(FunctionScanState *node)
 
 			if (bms_overlap(chgparam, rtfunc->funcparams))
 			{
-				if (node->funcstates[i].tstore != NULL)
-				{
-					tuplestore_end(node->funcstates[i].tstore);
-					node->funcstates[i].tstore = NULL;
-				}
+				UpdateChangedParamSet(&node->funcstates[i].scanstate->ps,
+									  node->ss.ps.chgParam);
+
 				node->funcstates[i].rowcount = -1;
 			}
 			i++;
@@ -611,10 +635,9 @@ ExecReScanFunctionScan(FunctionScanState *node)
 	/* Reset ordinality counter */
 	node->ordinal = 0;
 
-	/* Make sure we rewind any remaining tuplestores */
+	/* Rescan them all */
 	for (i = 0; i < node->nfuncs; i++)
 	{
-		if (node->funcstates[i].tstore != NULL)
-			tuplestore_rescan(node->funcstates[i].tstore);
+		ExecReScan(&node->funcstates[i].scanstate->ps);
 	}
 }
diff --git a/src/backend/executor/nodeMaterial.c b/src/backend/executor/nodeMaterial.c
index dd077f4..2fde0dc 100644
--- a/src/backend/executor/nodeMaterial.c
+++ b/src/backend/executor/nodeMaterial.c
@@ -45,9 +45,12 @@ ExecMaterial(PlanState *pstate)
 	Tuplestorestate *tuplestorestate;
 	bool		eof_tuplestore;
 	TupleTableSlot *slot;
+	bool 		first_time = true;
 
 	CHECK_FOR_INTERRUPTS();
 
+restart:
+
 	/*
 	 * get state info from node
 	 */
@@ -126,12 +129,24 @@ ExecMaterial(PlanState *pstate)
 		PlanState  *outerNode;
 		TupleTableSlot *outerslot;
 
+		if (!first_time)
+			ereport(ERROR,
+					(errcode(ERRCODE_E_R_I_E_SRF_PROTOCOL_VIOLATED),
+					 errmsg("attempt to scan donated result store failed")));
+
 		/*
 		 * We can only get here with forward==true, so no need to worry about
 		 * which direction the subplan will go.
 		 */
 		outerNode = outerPlanState(node);
 		outerslot = ExecProcNode(outerNode);
+
+		if (node->tuplestore_donated)
+		{
+			first_time = false;
+			goto restart;
+		}
+
 		if (TupIsNull(outerslot))
 		{
 			node->eof_underlying = true;
@@ -196,6 +211,7 @@ ExecInitMaterial(Material *node, EState *estate, int eflags)
 
 	matstate->eof_underlying = false;
 	matstate->tuplestorestate = NULL;
+	matstate->tuplestore_donated = false;
 
 	/*
 	 * Miscellaneous initialization
@@ -346,6 +362,7 @@ ExecReScanMaterial(MaterialState *node)
 		{
 			tuplestore_end(node->tuplestorestate);
 			node->tuplestorestate = NULL;
+			node->tuplestore_donated = false;
 			if (outerPlan->chgParam == NULL)
 				ExecReScan(outerPlan);
 			node->eof_underlying = false;
@@ -361,8 +378,29 @@ ExecReScanMaterial(MaterialState *node)
 		 * if chgParam of subnode is not null then plan will be re-scanned by
 		 * first ExecProcNode.
 		 */
+		node->tuplestore_donated = false;
 		if (outerPlan->chgParam == NULL)
 			ExecReScan(outerPlan);
 		node->eof_underlying = false;
 	}
 }
+
+void
+ExecMaterialReceiveResultStore(MaterialState *node, Tuplestorestate *store)
+{
+	if (!node->tuplestore_donated)
+	{
+		if (node->tuplestorestate)
+		{
+			tuplestore_end(node->tuplestorestate);
+		}
+
+		node->tuplestorestate = store;
+		node->tuplestore_donated = true;
+		node->eof_underlying = true;
+	}
+	else
+		ereport(ERROR,
+				(errcode(ERRCODE_E_R_I_E_SRF_PROTOCOL_VIOLATED),
+				 errmsg("Result tuplestore donated more than once")));
+}
diff --git a/src/backend/executor/nodeNestloop.c b/src/backend/executor/nodeNestloop.c
index b07c299..48d7db5 100644
--- a/src/backend/executor/nodeNestloop.c
+++ b/src/backend/executor/nodeNestloop.c
@@ -293,9 +293,16 @@ ExecInitNestLoop(NestLoop *node, EState *estate, int eflags)
 	 * such parameters, then there is no point in REWIND support at all in the
 	 * inner child, because it will always be rescanned with fresh parameter
 	 * values.
+	 *
+	 * The exception to this simple rule is a ROWS FROM function scan where it
+	 * is possible that only some of the inolved functions are affected by the
+	 * parameters. In this case, we blanket request support for REWIND. A more
+	 * intelligent approch would request REWIND only for nodes unaffected by
+	 * the parameters, but we aren't so intelligent yet.
 	 */
 	outerPlanState(nlstate) = ExecInitNode(outerPlan(node), estate, eflags);
-	if (node->nestParams == NIL)
+	if (node->nestParams == NIL ||
+		IsA(innerPlan(node), FunctionScan))
 		eflags |= EXEC_FLAG_REWIND;
 	else
 		eflags &= ~EXEC_FLAG_REWIND;
diff --git a/src/backend/executor/nodeProjectSet.c b/src/backend/executor/nodeProjectSet.c
index 4a1b060..66a1d30 100644
--- a/src/backend/executor/nodeProjectSet.c
+++ b/src/backend/executor/nodeProjectSet.c
@@ -283,6 +283,7 @@ ExecInitProjectSet(ProjectSet *node, EState *estate, int eflags)
 			state->elems[off] = (Node *)
 				ExecInitFunctionResultSet(expr, state->ps.ps_ExprContext,
 										  &state->ps);
+			Assert (((SetExprState *) state->elems[off])->funcReturnsSet);
 		}
 		else
 		{
diff --git a/src/backend/executor/nodeSRFScan.c b/src/backend/executor/nodeSRFScan.c
new file mode 100644
index 0000000..35ef67e
--- /dev/null
+++ b/src/backend/executor/nodeSRFScan.c
@@ -0,0 +1,262 @@
+/*-------------------------------------------------------------------------
+ *
+ * nodeSRFScan.c
+ *	  Coordinates a scan over a single SRF function, or a non-SRF as if it
+ *    were an SRF returning a single row.
+ *
+ *    SRFScan expands the function's output if it returns a tuple. If the
+ *    SRF uses SFRM_Materialize, it will donate the returned tuplestore to
+ *    the parent Materialize node, if there is one, to avoid double-
+ *    materialisation.
+ *
+ *    The Planner knows nothing of the SRFScan structure. It is constructed
+ *    by the Executor at execution time, and is reported in the EXPLAIN
+ *    output.
+ *
+ * IDENTIFICATION
+ *	  src/backend/executor/nodeSRFScan.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "catalog/pg_type.h"
+#include "executor/nodeSRFScan.h"
+#include "executor/nodeMaterial.h"
+#include "funcapi.h"
+#include "nodes/nodeFuncs.h"
+#include "nodes/makefuncs.h"
+#include "parser/parse_type.h"
+#include "utils/builtins.h"
+#include "utils/memutils.h"
+#include "utils/syscache.h"
+
+static TupleTableSlot *			/* result tuple from subplan */
+ExecSRF(PlanState *node)
+{
+	SRFScanState *pstate = (SRFScanState *) node;
+	ExprContext *econtext = pstate->ss.ps.ps_ExprContext;
+	TupleTableSlot *resultSlot = pstate->ss.ps.ps_ResultTupleSlot;
+	Datum result;
+	ExprDoneCond *isdone = &pstate->elemdone;
+	bool	   isnull;
+	SetExprState *setexpr = pstate->setexpr;
+	FunctionCallInfo fcinfo;
+	ReturnSetInfo *rsinfo;
+
+	/* We only support forward scans. */
+	Assert(ScanDirectionIsForward(pstate->ss.ps.state->es_direction));
+
+	ExecClearTuple(resultSlot);
+
+	/*
+	 * Only execute something if we are not already complete...
+	 */
+	if (*isdone == ExprEndResult)
+		return NULL;
+
+	/*
+	 * Evaluate SRF - possibly continuing previously started output.
+	 */
+	result = ExecMakeFunctionResultSet((SetExprState *) setexpr,
+										econtext, pstate->argcontext,
+										&isnull, isdone);
+
+	if (*isdone == ExprEndResult)
+		return NULL;
+
+	fcinfo = setexpr->fcinfo;
+	rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+
+	/* Have we donated the result store? */
+	if (setexpr->funcResultStoreDonated)
+		return 0;
+
+	/*
+	 * If we obtained a tupdesc, check it is appropriate, but not in
+	 * the case of SFRM_Materialize becuase is will have been checked
+	 * already.
+	 */
+	if (!pstate->tupdesc_checked &&
+		setexpr->funcReturnsTuple &&
+		rsinfo->returnMode != SFRM_Materialize &&
+		rsinfo->setDesc && setexpr->funcResultDesc)
+	{
+		tupledesc_match (setexpr->funcResultDesc, rsinfo->setDesc);
+		pstate->tupdesc_checked = true;
+	}
+
+	/*
+	 * If returned a tupple, expand into multiple columns.
+	 */
+	if (setexpr->funcReturnsTuple)
+	{
+		if (!isnull)
+		{
+			HeapTupleHeader td = DatumGetHeapTupleHeader(result);
+			HeapTupleData tmptup;
+
+			/*
+			 * In SFRM_Materialize mode, the type will have been checked
+			 * already.
+			 */
+			if (rsinfo->returnMode != SFRM_Materialize)
+			{
+				/*
+				 * Verify all later returned rows have same subtype;
+				 * necessary in case the type is RECORD.
+				 */
+				if (HeapTupleHeaderGetTypeId(td) != rsinfo->setDesc->tdtypeid ||
+					HeapTupleHeaderGetTypMod(td) != rsinfo->setDesc->tdtypmod)
+					ereport(ERROR,
+							(errcode(ERRCODE_DATATYPE_MISMATCH),
+							 errmsg("rows returned by function are not all of the same row type")));
+			}
+
+			/*
+			 * tuplestore_puttuple needs a HeapTuple not a bare
+			 * HeapTupleHeader, but it doesn't need all the fields.
+			 */
+			tmptup.t_len = HeapTupleHeaderGetDatumLength(td);
+			tmptup.t_data = td;
+
+			heap_deform_tuple (&tmptup, setexpr->funcResultDesc,
+							   resultSlot->tts_values,
+							   resultSlot->tts_isnull);
+		}
+		else
+		{
+			/*
+			 * populate the result cols with nulls
+			 */
+			int i;
+			for (i = 0; i < pstate->colcount; i++)
+			{
+				resultSlot->tts_values[i] = (Datum) 0;
+				resultSlot->tts_isnull[i] = true;
+			}
+		}
+	}
+	else
+	{
+		/* Scalar-type case: just store the function result */
+		resultSlot->tts_values[0] = result;
+		resultSlot->tts_isnull[0] = isnull;
+	}
+
+	/*
+	 * If we achieved obtained a single result, don't execute again.
+	 */
+	if (*isdone == ExprSingleResult)
+		*isdone = ExprEndResult;
+
+	ExecStoreVirtualTuple(resultSlot);
+	return resultSlot;
+}
+
+SRFScanState *
+ExecInitSRFScan(SRFScanPlan *node, EState *estate, int eflags)
+{
+	RangeTblFunction *rtfunc = (RangeTblFunction *) node->rtfunc;
+
+	SRFScanState *srfstate;
+
+	/*
+	 * SRFScan should not have any children.
+	 */
+	Assert(outerPlan(node) == NULL);
+	Assert(innerPlan(node) == NULL);
+
+	/*
+	 * create state structure
+	 */
+	srfstate = makeNode(SRFScanState);
+	srfstate->ss.ps.plan = (Plan *) node;
+	srfstate->ss.ps.state = estate;
+	srfstate->ss.ps.ExecProcNode = ExecSRF;
+
+	/*
+	 * Miscellaneous initialization
+	 *
+	 * create expression context for node
+	 */
+	ExecAssignExprContext(estate, &srfstate->ss.ps);
+
+	srfstate->setexpr =
+		ExecInitFunctionResultSet((Expr *) node->funcexpr,
+								  srfstate->ss.ps.ps_ExprContext,
+								  &srfstate->ss.ps);
+
+	srfstate->setexpr->funcResultDesc = node->funcResultDesc;
+	srfstate->setexpr->funcReturnsTuple = node->funcReturnsTuple;
+
+	srfstate->colcount = rtfunc->funccolcount;
+
+	srfstate->tupdesc_checked = false;
+
+	/* Start with the assumption we will get some result. */
+	srfstate->elemdone = ExprSingleResult;
+
+	/*
+	 * Initialize result type and slot. No need to initialize projection info
+	 * because this node doesn't do projections (ps_ResultTupleSlot).
+	 *
+	 * material nodes only return tuples from their materialized relation.
+	 */
+	ExecInitScanTupleSlot(estate, &srfstate->ss, srfstate->setexpr->funcResultDesc,
+						  &TTSOpsMinimalTuple);
+	ExecInitResultTupleSlotTL(&srfstate->ss.ps, &TTSOpsMinimalTuple);
+	ExecAssignScanProjectionInfo(&srfstate->ss);
+
+	/*
+	 * Create a memory context that ExecMakeFunctionResultSet can use to
+	 * evaluate function arguments in.  We can't use the per-tuple context for
+	 * this because it gets reset too often; but we don't want to leak
+	 * evaluation results into the query-lifespan context either.  We use one
+	 * context for the arguments of all tSRFs, as they have roughly equivalent
+	 * lifetimes.
+	 */
+	srfstate->argcontext = AllocSetContextCreate(CurrentMemoryContext,
+											  "SRF function arguments",
+											  ALLOCSET_DEFAULT_SIZES);
+	return srfstate;
+}
+
+void
+ExecEndSRFScan(SRFScanState *node)
+{
+	/* Nothing to do */
+}
+
+void
+ExecReScanSRF(SRFScanState *node)
+{
+	/* Expecting some results. */
+	node->elemdone = ExprSingleResult;
+
+	/* We must re-evaluate function call arguments. */
+	node->setexpr->setArgsValid = false;
+}
+
+bool
+ExecSRFDonateResultTuplestore(SetExprState *fcache)
+{
+	if (fcache->funcResultStoreDonationEnabled)
+	{
+		if (IsA (fcache->funcResultStoreDonationTarget, MaterialState))
+		{
+			MaterialState *target = (MaterialState *) fcache->funcResultStoreDonationTarget;
+
+			ExecMaterialReceiveResultStore(target, fcache->funcResultStore);
+
+			fcache->funcResultStore = NULL;
+
+			fcache->funcResultStoreDonated = true;
+
+			return true;
+		}
+	}
+
+	return false;
+}
diff --git a/src/include/access/tupdesc.h b/src/include/access/tupdesc.h
index d17af13..118fdcb 100644
--- a/src/include/access/tupdesc.h
+++ b/src/include/access/tupdesc.h
@@ -151,4 +151,6 @@ extern TupleDesc BuildDescForRelation(List *schema);
 
 extern TupleDesc BuildDescFromLists(List *names, List *types, List *typmods, List *collations);
 
+extern void tupledesc_match(TupleDesc dst_tupdesc, TupleDesc src_tupdesc);
+
 #endif							/* TUPDESC_H */
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index 9489051..8ada13e 100644
--- a/src/include/executor/executor.h
+++ b/src/include/executor/executor.h
@@ -409,13 +409,6 @@ extern bool ExecCheck(ExprState *state, ExprContext *context);
 /*
  * prototypes from functions in execSRF.c
  */
-extern SetExprState *ExecInitTableFunctionResult(Expr *expr,
-												 ExprContext *econtext, PlanState *parent);
-extern Tuplestorestate *ExecMakeTableFunctionResult(SetExprState *setexpr,
-													ExprContext *econtext,
-													MemoryContext argContext,
-													TupleDesc expectedDesc,
-													bool randomAccess);
 extern SetExprState *ExecInitFunctionResultSet(Expr *expr,
 											   ExprContext *econtext, PlanState *parent);
 extern Datum ExecMakeFunctionResultSet(SetExprState *fcache,
diff --git a/src/include/executor/nodeFunctionscan.h b/src/include/executor/nodeFunctionscan.h
index 74e8eef..ca89980 100644
--- a/src/include/executor/nodeFunctionscan.h
+++ b/src/include/executor/nodeFunctionscan.h
@@ -16,6 +16,16 @@
 
 #include "nodes/execnodes.h"
 
+/*
+ * Runtime data for each function being scanned.
+ */
+typedef struct FunctionScanPerFuncState
+{
+	int			colcount;		/* expected number of result columns */
+	int64		rowcount;		/* # of rows in result set, -1 if not known */
+	ScanState  *scanstate;		/* scan node: either SRFScan or Materialize */
+} FunctionScanPerFuncState;
+
 extern FunctionScanState *ExecInitFunctionScan(FunctionScan *node, EState *estate, int eflags);
 extern void ExecEndFunctionScan(FunctionScanState *node);
 extern void ExecReScanFunctionScan(FunctionScanState *node);
diff --git a/src/include/executor/nodeMaterial.h b/src/include/executor/nodeMaterial.h
index 99e7cbf..f55922c 100644
--- a/src/include/executor/nodeMaterial.h
+++ b/src/include/executor/nodeMaterial.h
@@ -21,5 +21,6 @@ extern void ExecEndMaterial(MaterialState *node);
 extern void ExecMaterialMarkPos(MaterialState *node);
 extern void ExecMaterialRestrPos(MaterialState *node);
 extern void ExecReScanMaterial(MaterialState *node);
+extern void ExecMaterialReceiveResultStore(MaterialState *node, Tuplestorestate *store);
 
 #endif							/* NODEMATERIAL_H */
diff --git a/src/include/executor/nodeSRFScan.h b/src/include/executor/nodeSRFScan.h
new file mode 100644
index 0000000..2430de5
--- /dev/null
+++ b/src/include/executor/nodeSRFScan.h
@@ -0,0 +1,30 @@
+/*-------------------------------------------------------------------------
+ *
+ * IDENTIFICATION
+ *	  src/include/executor/nodeSRFScan.h
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef nodeSRFScan_h
+#define nodeSRFScan_h
+
+#include "nodes/execnodes.h"
+
+typedef struct
+{
+	ScanState		ss;					/* its first field is NodeTag */
+	SetExprState 	*setexpr;			/* state of the expression being evaluated */
+	ExprDoneCond	elemdone;
+	int				colcount;			/* # of columns */
+	bool			tupdesc_checked;	/* has the return tupdesc been checked? */
+	MemoryContext 	argcontext;			/* context for SRF arguments */
+	PlanState		*parent;			/* the plan's parent node */
+} SRFScanState;
+
+extern SRFScanState *ExecInitSRFScan(SRFScanPlan *node, EState *estate, int eflags);
+extern void ExecEndSRFScan(SRFScanState *node);
+extern void ExecReScanSRF(SRFScanState *node);
+extern bool ExecSRFDonateResultTuplestore(SetExprState *fcache);
+
+#endif /* nodeSRFScan_h */
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index cd3ddf7..f1c8085 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -797,10 +797,16 @@ typedef struct SetExprState
 	/*
 	 * For a set-returning function (SRF) that returns a tuplestore, we keep
 	 * the tuplestore here and dole out the result rows one at a time. The
-	 * slot holds the row currently being returned.
+	 * slot holds the row currently being returned. The boolean
+	 * funcResultStoreDonationEnabled indicates whether the an SRF
+	 * returning SFRM_Materialize tupleStore should attempt to donate its
+	 * resultStore to a higher level Materialize node.
 	 */
 	Tuplestorestate *funcResultStore;
 	TupleTableSlot *funcResultSlot;
+	bool 		funcResultStoreDonationEnabled;
+	bool 		funcResultStoreDonated;
+	struct PlanState *funcResultStoreDonationTarget;
 
 	/*
 	 * In some cases we need to compute a tuple descriptor for the function's
@@ -1651,6 +1657,7 @@ typedef struct SubqueryScanState
  *		funcstates			per-function execution states (private in
  *							nodeFunctionscan.c)
  *		argcontext			memory context to evaluate function arguments in
+ *		pending_srf_tuples	still evaluating any SRFs?
  * ----------------
  */
 struct FunctionScanPerFuncState;
@@ -1978,6 +1985,7 @@ typedef struct MaterialState
 	int			eflags;			/* capability flags to pass to tuplestore */
 	bool		eof_underlying; /* reached end of underlying plan? */
 	Tuplestorestate *tuplestorestate;
+	bool		tuplestore_donated; /* was duplestore donated by another node? */
 } MaterialState;
 
 /* ----------------
diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h
index 8a76afe..24a72a2 100644
--- a/src/include/nodes/nodes.h
+++ b/src/include/nodes/nodes.h
@@ -514,7 +514,9 @@ typedef enum NodeTag
 	T_SupportRequestSelectivity,	/* in nodes/supportnodes.h */
 	T_SupportRequestCost,		/* in nodes/supportnodes.h */
 	T_SupportRequestRows,		/* in nodes/supportnodes.h */
-	T_SupportRequestIndexCondition	/* in nodes/supportnodes.h */
+	T_SupportRequestIndexCondition,	/* in nodes/supportnodes.h */
+	T_SRFScanPlan,
+	T_SRFScanState
 } NodeTag;
 
 /*
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 4869fe7..3486ede 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -16,6 +16,7 @@
 
 #include "access/sdir.h"
 #include "access/stratnum.h"
+#include "access/tupdesc.h"
 #include "lib/stringinfo.h"
 #include "nodes/bitmapset.h"
 #include "nodes/lockoptions.h"
@@ -546,6 +547,14 @@ typedef struct TableFuncScan
 	TableFunc  *tablefunc;		/* table function node */
 } TableFuncScan;
 
+typedef struct SRFScanPlan {
+	Plan		plan;
+	Node		*funcexpr;
+	Node 		*rtfunc;
+	TupleDesc	funcResultDesc;		/* funciton output columns tuple descriptor */
+	bool		funcReturnsTuple;
+} SRFScanPlan;
+
 /* ----------------
  *		CteScan node
  * ----------------
diff --git a/src/test/regress/expected/aggregates.out b/src/test/regress/expected/aggregates.out
index f457b5b..ab8e222 100644
--- a/src/test/regress/expected/aggregates.out
+++ b/src/test/regress/expected/aggregates.out
@@ -514,13 +514,15 @@ order by 1, 2;
          ->  Function Scan on pg_catalog.generate_series s1
                Output: s1.s1
                Function Call: generate_series(1, 3)
+               ->  SRF Scan
          ->  HashAggregate
                Output: s2.s2, sum((s1.s1 + s2.s2))
                Group Key: s2.s2
                ->  Function Scan on pg_catalog.generate_series s2
                      Output: s2.s2
                      Function Call: generate_series(1, 3)
-(14 rows)
+                     ->  SRF Scan
+(16 rows)
 
 select s1, s2, sm
 from generate_series(1, 3) s1,
@@ -549,6 +551,7 @@ select array(select sum(x+y) s
  Function Scan on pg_catalog.generate_series x
    Output: (SubPlan 1)
    Function Call: generate_series(1, 3)
+   ->  SRF Scan
    SubPlan 1
      ->  Sort
            Output: (sum((x.x + y.y))), y.y
@@ -559,7 +562,8 @@ select array(select sum(x+y) s
                  ->  Function Scan on pg_catalog.generate_series y
                        Output: y.y
                        Function Call: generate_series(1, 3)
-(13 rows)
+                       ->  SRF Scan
+(15 rows)
 
 select array(select sum(x+y) s
             from generate_series(1,3) y group by y order by s)
diff --git a/src/test/regress/expected/groupingsets.out b/src/test/regress/expected/groupingsets.out
index c1f802c..5eb7dba 100644
--- a/src/test/regress/expected/groupingsets.out
+++ b/src/test/regress/expected/groupingsets.out
@@ -374,7 +374,8 @@ select g as alias1, g as alias2
    ->  Sort
          Sort Key: g
          ->  Function Scan on generate_series g
-(6 rows)
+               ->  SRF Scan
+(7 rows)
 
 select g as alias1, g as alias2
   from generate_series(1,3) g
@@ -1234,7 +1235,9 @@ explain (costs off)
          ->  Nested Loop
                ->  Values Scan on "*VALUES*"
                ->  Function Scan on gstest_data
-(8 rows)
+                     ->  Materialize
+                           ->  SRF Scan
+(10 rows)
 
 select *
   from (values (1),(2)) v(x),
@@ -1358,7 +1361,9 @@ explain (costs off)
          ->  Nested Loop
                ->  Values Scan on "*VALUES*"
                ->  Function Scan on gstest_data
-(10 rows)
+                     ->  Materialize
+                           ->  SRF Scan
+(12 rows)
 
 -- Verify that we correctly handle the child node returning a
 -- non-minimal slot, which happens if the input is pre-sorted,
diff --git a/src/test/regress/expected/inherit.out b/src/test/regress/expected/inherit.out
index dfd0ee4..c339722 100644
--- a/src/test/regress/expected/inherit.out
+++ b/src/test/regress/expected/inherit.out
@@ -1684,6 +1684,7 @@ FROM generate_series(1, 3) g(i);
                            QUERY PLAN                           
 ----------------------------------------------------------------
  Function Scan on generate_series g
+   ->  SRF Scan
    SubPlan 1
      ->  Limit
            ->  Merge Append
@@ -1691,10 +1692,12 @@ FROM generate_series(1, 3) g(i);
                  ->  Sort
                        Sort Key: ((d.d + g.i))
                        ->  Function Scan on generate_series d
+                             ->  SRF Scan
                  ->  Sort
                        Sort Key: ((d_1.d + g.i))
                        ->  Function Scan on generate_series d_1
-(11 rows)
+                             ->  SRF Scan
+(14 rows)
 
 SELECT
     ARRAY(SELECT f.i FROM (
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index 761376b..3650aee 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -3403,7 +3403,8 @@ select * from mki8(1,2);
  Function Scan on mki8
    Output: q1, q2
    Function Call: '(1,2)'::int8_tbl
-(3 rows)
+   ->  SRF Scan
+(4 rows)
 
 select * from mki8(1,2);
  q1 | q2 
@@ -3418,7 +3419,8 @@ select * from mki4(42);
  Function Scan on mki4
    Output: f1
    Function Call: '(42)'::int4_tbl
-(3 rows)
+   ->  SRF Scan
+(4 rows)
 
 select * from mki4(42);
  f1 
@@ -3660,9 +3662,10 @@ left join unnest(v1ys) as u1(u1y) on u1y = v2y;
          Hash Cond: (u1.u1y = "*VALUES*_1".column2)
          Filter: ("*VALUES*_1".column1 = "*VALUES*".column1)
          ->  Function Scan on unnest u1
+               ->  SRF Scan
          ->  Hash
                ->  Values Scan on "*VALUES*_1"
-(8 rows)
+(9 rows)
 
 select * from
 (values (1, array[10,20]), (2, array[20,30])) as v1(v1x,v1ys)
@@ -4475,7 +4478,9 @@ select 1 from (select a.id FROM a left join b on a.b_id = b.id) q,
    ->  Seq Scan on a
    ->  Function Scan on generate_series gs
          Filter: (a.id = i)
-(4 rows)
+         ->  Materialize
+               ->  SRF Scan
+(6 rows)
 
 rollback;
 create temp table parent (k int primary key, pd int);
@@ -4814,7 +4819,9 @@ explain (costs off)
    ->  Nested Loop
          ->  Seq Scan on tenk1 a
          ->  Function Scan on generate_series g
-(4 rows)
+               ->  Materialize
+                     ->  SRF Scan
+(6 rows)
 
 explain (costs off)
   select count(*) from tenk1 a cross join lateral generate_series(1,two) g;
@@ -4824,7 +4831,9 @@ explain (costs off)
    ->  Nested Loop
          ->  Seq Scan on tenk1 a
          ->  Function Scan on generate_series g
-(4 rows)
+               ->  Materialize
+                     ->  SRF Scan
+(6 rows)
 
 -- don't need the explicit LATERAL keyword for functions
 explain (costs off)
@@ -4835,7 +4844,9 @@ explain (costs off)
    ->  Nested Loop
          ->  Seq Scan on tenk1 a
          ->  Function Scan on generate_series g
-(4 rows)
+               ->  Materialize
+                     ->  SRF Scan
+(6 rows)
 
 -- lateral with UNION ALL subselect
 explain (costs off)
@@ -4846,12 +4857,13 @@ explain (costs off)
 ------------------------------------------
  Nested Loop
    ->  Function Scan on generate_series g
+         ->  SRF Scan
    ->  Append
          ->  Seq Scan on int8_tbl a
                Filter: (g.g = q1)
          ->  Seq Scan on int8_tbl b
                Filter: (g.g = q2)
-(7 rows)
+(8 rows)
 
 select * from generate_series(100,200) g,
   lateral (select * from int8_tbl a where g = q1 union all
diff --git a/src/test/regress/expected/misc_functions.out b/src/test/regress/expected/misc_functions.out
index e217b67..19b14f8 100644
--- a/src/test/regress/expected/misc_functions.out
+++ b/src/test/regress/expected/misc_functions.out
@@ -226,9 +226,10 @@ SELECT * FROM tenk1 a JOIN my_gen_series(1,1000) g ON a.unique1 = g;
  Hash Join
    Hash Cond: (g.g = a.unique1)
    ->  Function Scan on my_gen_series g
+         ->  SRF Scan
    ->  Hash
          ->  Seq Scan on tenk1 a
-(5 rows)
+(6 rows)
 
 EXPLAIN (COSTS OFF)
 SELECT * FROM tenk1 a JOIN my_gen_series(1,10) g ON a.unique1 = g;
@@ -236,7 +237,8 @@ SELECT * FROM tenk1 a JOIN my_gen_series(1,10) g ON a.unique1 = g;
 -------------------------------------------------
  Nested Loop
    ->  Function Scan on my_gen_series g
+         ->  SRF Scan
    ->  Index Scan using tenk1_unique1 on tenk1 a
          Index Cond: (unique1 = g.g)
-(4 rows)
+(5 rows)
 
diff --git a/src/test/regress/expected/pg_lsn.out b/src/test/regress/expected/pg_lsn.out
index 64d41df..e68adc1 100644
--- a/src/test/regress/expected/pg_lsn.out
+++ b/src/test/regress/expected/pg_lsn.out
@@ -87,13 +87,17 @@ SELECT DISTINCT (i || '/' || j)::pg_lsn f
          Group Key: ((((i.i)::text || '/'::text) || (j.j)::text))::pg_lsn
          ->  Nested Loop
                ->  Function Scan on generate_series k
+                     ->  SRF Scan
                ->  Materialize
                      ->  Nested Loop
                            ->  Function Scan on generate_series j
                                  Filter: ((j > 0) AND (j <= 10))
+                                 ->  SRF Scan
                            ->  Function Scan on generate_series i
                                  Filter: (i <= 10)
-(12 rows)
+                                 ->  Materialize
+                                       ->  SRF Scan
+(16 rows)
 
 SELECT DISTINCT (i || '/' || j)::pg_lsn f
   FROM generate_series(1, 10) i,
diff --git a/src/test/regress/expected/plpgsql.out b/src/test/regress/expected/plpgsql.out
index cd2c79f..67a6f39 100644
--- a/src/test/regress/expected/plpgsql.out
+++ b/src/test/regress/expected/plpgsql.out
@@ -3094,7 +3094,7 @@ select * from sc_test();
 
 create or replace function sc_test() returns setof integer as $$
 declare
-  c cursor for select * from generate_series(1, 10);
+  c scroll cursor for select * from generate_series(1, 10);
   x integer;
 begin
   open c;
@@ -4852,7 +4852,9 @@ select i, a from
    ->  Function Scan on public.consumes_rw_array i
          Output: i.i
          Function Call: consumes_rw_array((returns_rw_array(1)))
-(7 rows)
+         ->  Materialize
+               ->  SRF Scan
+(9 rows)
 
 select i, a from
   (select returns_rw_array(1) as a offset 0) ss,
@@ -4869,7 +4871,8 @@ select consumes_rw_array(a), a from returns_rw_array(1) a;
  Function Scan on public.returns_rw_array a
    Output: consumes_rw_array(a), a
    Function Call: returns_rw_array(1)
-(3 rows)
+   ->  SRF Scan
+(4 rows)
 
 select consumes_rw_array(a), a from returns_rw_array(1) a;
  consumes_rw_array |   a   
diff --git a/src/test/regress/expected/rangefuncs.out b/src/test/regress/expected/rangefuncs.out
index a70060b..7f96baa 100644
--- a/src/test/regress/expected/rangefuncs.out
+++ b/src/test/regress/expected/rangefuncs.out
@@ -1841,7 +1841,8 @@ explain (verbose, costs off)
  Function Scan on public.array_to_set t
    Output: f1, f2
    Function Call: array_to_set('{one,two}'::text[])
-(3 rows)
+   ->  SRF Scan
+(4 rows)
 
 -- but without, it can be:
 create or replace function array_to_set(anyarray) returns setof record as $$
@@ -1879,7 +1880,8 @@ explain (verbose, costs off)
  Function Scan on pg_catalog.generate_subscripts i
    Output: i.i, ('{one,two}'::text[])[i.i]
    Function Call: generate_subscripts('{one,two}'::text[], 1)
-(3 rows)
+   ->  SRF Scan
+(4 rows)
 
 create temp table rngfunc(f1 int8, f2 int8);
 create function testrngfunc() returns record as $$
@@ -1950,7 +1952,8 @@ select * from testrngfunc();
  Function Scan on testrngfunc
    Output: f1, f2
    Function Call: '(7.136178,7.14)'::rngfunc_type
-(3 rows)
+   ->  SRF Scan
+(4 rows)
 
 select * from testrngfunc();
     f1    |  f2  
@@ -1982,7 +1985,8 @@ select * from testrngfunc();
  Function Scan on public.testrngfunc
    Output: f1, f2
    Function Call: testrngfunc()
-(3 rows)
+   ->  SRF Scan
+(4 rows)
 
 select * from testrngfunc();
     f1    |  f2  
@@ -2048,7 +2052,8 @@ select * from testrngfunc();
  Function Scan on public.testrngfunc
    Output: f1, f2
    Function Call: testrngfunc()
-(3 rows)
+   ->  SRF Scan
+(4 rows)
 
 select * from testrngfunc();
     f1    |  f2  
@@ -2217,7 +2222,9 @@ select x from int8_tbl, extractq2(int8_tbl) f(x);
    ->  Function Scan on f
          Output: f.x
          Function Call: int8_tbl.q2
-(7 rows)
+         ->  Materialize
+               ->  SRF Scan
+(9 rows)
 
 select x from int8_tbl, extractq2(int8_tbl) f(x);
          x         
@@ -2306,3 +2313,155 @@ select *, row_to_json(u) from unnest(array[]::rngfunc2[]) u;
 (0 rows)
 
 drop type rngfunc2;
+--------------------------------------------------------------------------------
+-- Start of tests for support of ValuePerCall-mode SRFs
+CREATE TEMPORARY SEQUENCE rngfunc_vpc_seq;
+CREATE TEMPORARY SEQUENCE rngfunc_mat_seq;
+CREATE TYPE rngfunc_vpc_t AS (i integer, s bigint);
+-- rngfunc_vpc is SQL, so will yield a ValuePerCall SRF
+CREATE FUNCTION rngfunc_vpc(int,int)
+	RETURNS setof rngfunc_vpc_t AS
+$$
+	SELECT i, nextval('rngfunc_vpc_seq')
+		FROM generate_series($1,$2) i;
+$$
+LANGUAGE SQL;
+-- rngfunc_mat is plpgsql, so will yield a Materialize SRF
+CREATE FUNCTION rngfunc_mat(int,int)
+	RETURNS setof rngfunc_vpc_t AS
+$$
+begin
+	for i in $1..$2 loop
+		return next (i, nextval('rngfunc_mat_seq'));
+	end loop;
+end;
+$$
+LANGUAGE plpgsql;
+-- A VPC SRF that is not part of a complex query should not materialize.
+-- 
+-- To illustrate this, we explain a simple VPC SRF scan, and note the
+-- absence of a Materialize node.
+--
+explain (costs off)
+	select * from rngfunc_vpc(1, 3) t;
+           QUERY PLAN           
+--------------------------------
+ Function Scan on rngfunc_vpc t
+   ->  SRF Scan
+(2 rows)
+
+-- A VPC SRF that aborts early should do so without emitting all results.
+-- 
+-- To illustrate this, we show that an SRF that uses a sequence does not
+-- have its value incremented if the SRF is not invoked to generate a row.
+--
+select nextval('rngfunc_vpc_seq');
+ nextval 
+---------
+       1
+(1 row)
+
+select * from rngfunc_vpc(1, 3) t limit 2;
+ i | s 
+---+---
+ 1 | 2
+ 2 | 3
+(2 rows)
+
+select nextval('rngfunc_vpc_seq');
+ nextval 
+---------
+       4
+(1 row)
+
+-- A Marerialize SRF should show Materialization if the query demand rescan.
+--
+-- To illustrate this, we construct a cross join, which forces rescan.
+--
+-- The same plan should be generated for both VPC and Materialize mode SRFs.
+--
+explain (costs off)
+	select * from generate_series (1, 3) n, rngfunc_vpc(1, 3) t;
+                QUERY PLAN                
+------------------------------------------
+ Nested Loop
+   ->  Function Scan on generate_series n
+         ->  SRF Scan
+   ->  Function Scan on rngfunc_vpc t
+         ->  Materialize
+               ->  SRF Scan
+(6 rows)
+
+explain (costs off)
+	select * from generate_series (1, 3) n, rngfunc_mat(1, 3) t;
+                QUERY PLAN                
+------------------------------------------
+ Nested Loop
+   ->  Function Scan on generate_series n
+         ->  SRF Scan
+   ->  Function Scan on rngfunc_mat t
+         ->  Materialize
+               ->  SRF Scan
+(6 rows)
+
+-- A Marerialize SRF should show donation of the returned tuplestore.
+--
+-- To illustrate this, we construct a cross join, which forces rescan.
+--
+-- Only the Materialize mode SRF should show donation.
+--
+explain (analyze, timing off, costs off, summary off)
+	select * from generate_series (1, 3) n, rngfunc_vpc(1, 3) t;
+                            QUERY PLAN                            
+------------------------------------------------------------------
+ Nested Loop (actual rows=9 loops=1)
+   ->  Function Scan on generate_series n (actual rows=3 loops=1)
+         ->  SRF Scan (actual rows=3 loops=1)
+               SFRM: ValuePerCall
+   ->  Function Scan on rngfunc_vpc t (actual rows=3 loops=3)
+         ->  Materialize (actual rows=3 loops=3)
+               ->  SRF Scan (actual rows=3 loops=1)
+                     SFRM: ValuePerCall
+(8 rows)
+
+explain (analyze, timing off, costs off, summary off)
+	select * from generate_series (1, 3) n, rngfunc_mat(1, 3) t;
+                            QUERY PLAN                            
+------------------------------------------------------------------
+ Nested Loop (actual rows=9 loops=1)
+   ->  Function Scan on generate_series n (actual rows=3 loops=1)
+         ->  SRF Scan (actual rows=3 loops=1)
+               SFRM: ValuePerCall
+   ->  Function Scan on rngfunc_mat t (actual rows=3 loops=3)
+         ->  Materialize (actual rows=3 loops=3)
+               ->  SRF Scan (actual rows=0 loops=1)
+                     SFRM: Materialize
+                     Donated tuplestore: true
+(9 rows)
+
+-- A Marerialize SRF that aborts early should still generate all results.
+--
+-- To illustrate this, we show that an SRF that uses a sequence still has
+-- its value incremented if even when SRF's rows are not emitted.
+--
+select nextval('rngfunc_mat_seq');
+ nextval 
+---------
+       4
+(1 row)
+
+select * from rngfunc_mat(1, 3) t limit 2;
+ i | s 
+---+---
+ 1 | 5
+ 2 | 6
+(2 rows)
+
+select nextval('rngfunc_mat_seq');
+ nextval 
+---------
+       8
+(1 row)
+
+-- End of tests for support of ValuePerCall-mode SRFs
+--------------------------------------------------------------------------------
diff --git a/src/test/regress/expected/tsearch.out b/src/test/regress/expected/tsearch.out
index fe1cd9d..9f6deff 100644
--- a/src/test/regress/expected/tsearch.out
+++ b/src/test/regress/expected/tsearch.out
@@ -1669,8 +1669,9 @@ select * from test_tsquery, to_tsquery('new') q where txtsample @@ q;
  Nested Loop
    Join Filter: (test_tsquery.txtsample @@ q.q)
    ->  Function Scan on to_tsquery q
+         ->  SRF Scan
    ->  Seq Scan on test_tsquery
-(4 rows)
+(5 rows)
 
 -- to_tsquery(regconfig, text) is an immutable function.
 -- That allows us to get rid of using function scan and join at all.
diff --git a/src/test/regress/expected/union.out b/src/test/regress/expected/union.out
index 6e72e92..6828582 100644
--- a/src/test/regress/expected/union.out
+++ b/src/test/regress/expected/union.out
@@ -577,8 +577,10 @@ select from generate_series(1,5) union select from generate_series(1,3);
  HashAggregate
    ->  Append
          ->  Function Scan on generate_series
+               ->  SRF Scan
          ->  Function Scan on generate_series generate_series_1
-(4 rows)
+               ->  SRF Scan
+(6 rows)
 
 explain (costs off)
 select from generate_series(1,5) intersect select from generate_series(1,3);
@@ -588,9 +590,11 @@ select from generate_series(1,5) intersect select from generate_series(1,3);
    ->  Append
          ->  Subquery Scan on "*SELECT* 1"
                ->  Function Scan on generate_series
+                     ->  SRF Scan
          ->  Subquery Scan on "*SELECT* 2"
                ->  Function Scan on generate_series generate_series_1
-(6 rows)
+                     ->  SRF Scan
+(8 rows)
 
 select from generate_series(1,5) union select from generate_series(1,3);
 --
@@ -626,8 +630,10 @@ select from generate_series(1,5) union select from generate_series(1,3);
  Unique
    ->  Append
          ->  Function Scan on generate_series
+               ->  SRF Scan
          ->  Function Scan on generate_series generate_series_1
-(4 rows)
+               ->  SRF Scan
+(6 rows)
 
 explain (costs off)
 select from generate_series(1,5) intersect select from generate_series(1,3);
@@ -637,9 +643,11 @@ select from generate_series(1,5) intersect select from generate_series(1,3);
    ->  Append
          ->  Subquery Scan on "*SELECT* 1"
                ->  Function Scan on generate_series
+                     ->  SRF Scan
          ->  Subquery Scan on "*SELECT* 2"
                ->  Function Scan on generate_series generate_series_1
-(6 rows)
+                     ->  SRF Scan
+(8 rows)
 
 select from generate_series(1,5) union select from generate_series(1,3);
 --
diff --git a/src/test/regress/expected/window.out b/src/test/regress/expected/window.out
index d5fd404..d2cd0b5 100644
--- a/src/test/regress/expected/window.out
+++ b/src/test/regress/expected/window.out
@@ -3851,7 +3851,8 @@ EXPLAIN (costs off) SELECT * FROM pg_temp.f(2);
          ->  Sort
                Sort Key: s.s
                ->  Function Scan on generate_series s
-(5 rows)
+                     ->  SRF Scan
+(6 rows)
 
 SELECT * FROM pg_temp.f(2);
     f    
diff --git a/src/test/regress/sql/plpgsql.sql b/src/test/regress/sql/plpgsql.sql
index d841d8c..4717b06 100644
--- a/src/test/regress/sql/plpgsql.sql
+++ b/src/test/regress/sql/plpgsql.sql
@@ -2646,7 +2646,7 @@ select * from sc_test();
 
 create or replace function sc_test() returns setof integer as $$
 declare
-  c cursor for select * from generate_series(1, 10);
+  c scroll cursor for select * from generate_series(1, 10);
   x integer;
 begin
   open c;
diff --git a/src/test/regress/sql/rangefuncs.sql b/src/test/regress/sql/rangefuncs.sql
index 476b4f2..4d39f39 100644
--- a/src/test/regress/sql/rangefuncs.sql
+++ b/src/test/regress/sql/rangefuncs.sql
@@ -730,3 +730,82 @@ select *, row_to_json(u) from unnest(array[null::rngfunc2, (1,'foo')::rngfunc2,
 select *, row_to_json(u) from unnest(array[]::rngfunc2[]) u;
 
 drop type rngfunc2;
+
+--------------------------------------------------------------------------------
+-- Start of tests for support of ValuePerCall-mode SRFs
+
+CREATE TEMPORARY SEQUENCE rngfunc_vpc_seq;
+CREATE TEMPORARY SEQUENCE rngfunc_mat_seq;
+CREATE TYPE rngfunc_vpc_t AS (i integer, s bigint);
+
+-- rngfunc_vpc is SQL, so will yield a ValuePerCall SRF
+CREATE FUNCTION rngfunc_vpc(int,int)
+	RETURNS setof rngfunc_vpc_t AS
+$$
+	SELECT i, nextval('rngfunc_vpc_seq')
+		FROM generate_series($1,$2) i;
+$$
+LANGUAGE SQL;
+
+-- rngfunc_mat is plpgsql, so will yield a Materialize SRF
+CREATE FUNCTION rngfunc_mat(int,int)
+	RETURNS setof rngfunc_vpc_t AS
+$$
+begin
+	for i in $1..$2 loop
+		return next (i, nextval('rngfunc_mat_seq'));
+	end loop;
+end;
+$$
+LANGUAGE plpgsql;
+
+-- A VPC SRF that is not part of a complex query should not materialize.
+-- 
+-- To illustrate this, we explain a simple VPC SRF scan, and note the
+-- absence of a Materialize node.
+--
+explain (costs off)
+	select * from rngfunc_vpc(1, 3) t;
+
+-- A VPC SRF that aborts early should do so without emitting all results.
+-- 
+-- To illustrate this, we show that an SRF that uses a sequence does not
+-- have its value incremented if the SRF is not invoked to generate a row.
+--
+select nextval('rngfunc_vpc_seq');
+select * from rngfunc_vpc(1, 3) t limit 2;
+select nextval('rngfunc_vpc_seq');
+
+-- A Marerialize SRF should show Materialization if the query demand rescan.
+--
+-- To illustrate this, we construct a cross join, which forces rescan.
+--
+-- The same plan should be generated for both VPC and Materialize mode SRFs.
+--
+explain (costs off)
+	select * from generate_series (1, 3) n, rngfunc_vpc(1, 3) t;
+explain (costs off)
+	select * from generate_series (1, 3) n, rngfunc_mat(1, 3) t;
+
+-- A Marerialize SRF should show donation of the returned tuplestore.
+--
+-- To illustrate this, we construct a cross join, which forces rescan.
+--
+-- Only the Materialize mode SRF should show donation.
+--
+explain (analyze, timing off, costs off, summary off)
+	select * from generate_series (1, 3) n, rngfunc_vpc(1, 3) t;
+explain (analyze, timing off, costs off, summary off)
+	select * from generate_series (1, 3) n, rngfunc_mat(1, 3) t;
+
+-- A Marerialize SRF that aborts early should still generate all results.
+--
+-- To illustrate this, we show that an SRF that uses a sequence still has
+-- its value incremented if even when SRF's rows are not emitted.
+--
+select nextval('rngfunc_mat_seq');
+select * from rngfunc_mat(1, 3) t limit 2;
+select nextval('rngfunc_mat_seq');
+
+-- End of tests for support of ValuePerCall-mode SRFs
+--------------------------------------------------------------------------------
