Re: The flinfo->fn_extra question, from me this time. - Mailing list pgsql-hackers

From Tom Lane
Subject Re: The flinfo->fn_extra question, from me this time.
Date
Msg-id 15840.1584039069@sss.pgh.pa.us
Whole thread Raw
In response to Re: The flinfo->fn_extra question, from me this time.  (Dent John <denty@QQdd.eu>)
Responses Re: The flinfo->fn_extra question, from me this time.
Re: The flinfo->fn_extra question, from me this time.
List pgsql-hackers
The cfbot is still not happy with this, because you're ignoring the
project style rule against C99-like mixing of code and declarations.
I went to fix that, and soon found that the code doesn't compile,
much less pass regression tests, with --enable-cassert.  That's
really a serious error on your part: basically, nobody should ever
do backend code development in non-cassert builds, because there is
too much useful error checking you forego that way.  (Performance
testing is a different matter ... but you need to make the code
work before you worry about speed.)

Anyway, attached is a marginal update that gets this to the point
where it should compile in the cfbot, but it'll still fail regression
tests there.  (At least on the Linux side.  I guess the cfbot's
Windows builds are sans cassert, which seems like an odd choice.)

I didn't want to spend any more effort on it than that, because I'm
not really on board with this line of attack.  This patch seems
awfully invasive for what it's accomplishing, both at the code level
and in terms of what users will see in EXPLAIN.  No, I don't think
that adding additional "SRF Scan" nodes below FunctionScan is an
improvement, nor do I like your repurposing/abusing of Materialize.
It might be okay if you were just using Materialize as-is, but if
it's sort-of-materialize-but-not-always, I don't think that's going
to make anyone less confused.

More locally, this business with creating new "plan nodes" below the
FunctionScan at executor startup is a real abuse of a whole lot of stuff,
and I suspect that it's not unrelated to the assertion failures I'm
seeing.  Don't do that.  If you want to build some data structures at
executor start, fine, but they're not plans and shouldn't be mislabeled as
that.  On the other hand, if they do need to be plan nodes, they should be
made by the planner (which in turn would require a lot of infrastructure
you haven't built, eg copyfuncs/outfuncs/readfuncs/setrefs/...).

The v3 patch seemed closer to the sort of thing I was expecting
to get out of this (though I've not read it in any detail).

            regards, tom lane

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
+--------------------------------------------------------------------------------

pgsql-hackers by date:

Previous
From: Julien Rouhaud
Date:
Subject: Re: Planning counters in pg_stat_statements (using pgss_store)
Next
From: Andres Freund
Date:
Subject: Re: Additional size of hash table is alway zero for hash aggregates