From a98f7f3cd0adb39ec377e3afa3e3fdd5fad68adf Mon Sep 17 00:00:00 2001 From: Thomas Munro Date: Tue, 28 Jan 2020 22:28:32 +1300 Subject: [PATCH] pipeline-functionscan --- src/backend/access/common/tupdesc.c | 51 ++ src/backend/commands/explain.c | 42 ++ src/backend/executor/Makefile | 1 + src/backend/executor/execAmi.c | 5 + src/backend/executor/execProcnode.c | 10 + src/backend/executor/execSRF.c | 595 +++++-------------- src/backend/executor/nodeFunctionscan.c | 287 ++++----- src/backend/executor/nodeMaterial.c | 39 ++ src/backend/executor/nodeNestloop.c | 9 +- src/backend/executor/nodeProjectSet.c | 1 + src/backend/executor/nodeSRFScan.c | 262 ++++++++ src/include/access/tupdesc.h | 2 + src/include/executor/executor.h | 7 - src/include/executor/nodeFunctionscan.h | 10 + src/include/executor/nodeMaterial.h | 1 + src/include/executor/nodeSRFScan.h | 30 + src/include/nodes/execnodes.h | 10 +- src/include/nodes/nodes.h | 4 +- src/include/nodes/plannodes.h | 9 + src/test/regress/expected/aggregates.out | 8 +- src/test/regress/expected/groupingsets.out | 11 +- src/test/regress/expected/inherit.out | 5 +- src/test/regress/expected/join.out | 28 +- src/test/regress/expected/misc_functions.out | 6 +- src/test/regress/expected/pg_lsn.out | 6 +- src/test/regress/expected/plpgsql.out | 9 +- src/test/regress/expected/rangefuncs.out | 171 +++++- src/test/regress/expected/tsearch.out | 3 +- src/test/regress/expected/union.out | 16 +- src/test/regress/expected/window.out | 3 +- src/test/regress/sql/plpgsql.sql | 2 +- src/test/regress/sql/rangefuncs.sql | 79 +++ 32 files changed, 1085 insertions(+), 637 deletions(-) create mode 100644 src/backend/executor/nodeSRFScan.c create mode 100644 src/include/executor/nodeSRFScan.h diff --git a/src/backend/access/common/tupdesc.c b/src/backend/access/common/tupdesc.c index 00bb4cb53d..b870ab6b76 100644 --- a/src/backend/access/common/tupdesc.c +++ b/src/backend/access/common/tupdesc.c @@ -24,6 +24,7 @@ #include "catalog/pg_collation.h" #include "catalog/pg_type.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 c367c750b1..6f7546a7ae 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" @@ -1181,6 +1183,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; @@ -1769,6 +1774,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) { @@ -1977,6 +2007,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; @@ -2001,6 +2032,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 a983800e4b..9dae142f71 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 b12aeb3334..07ccca7507 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 7b2e84f402..da39593b27 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 2312cc7142..51c08412e9 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,52 @@ 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; -} - - -/* - * 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; + MemoryContext 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 +126,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 +139,7 @@ ExecMakeFunctionResultSet(SetExprState *fcache, Datum result; FunctionCallInfo fcinfo; PgStat_FunctionCallUsage fcusage; - ReturnSetInfo rsinfo; + ReturnSetInfo *rsinfo; bool callit; int i; @@ -539,6 +192,28 @@ restart: return (Datum) 0; } + /* + * 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 @@ -549,7 +224,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 +231,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 +250,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 +269,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,10 +298,31 @@ 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. */ @@ -640,21 +340,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 +378,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 +425,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 +496,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 +507,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 +518,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 +633,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 ccb66ce1aa..84e34d969f 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 it’s 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,15 @@ ExecInitFunctionScan(FunctionScan *node, EState *estate, int eflags) TypeFuncClass functypclass; Oid funcrettype; TupleDesc tupdesc; + 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 +388,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 +403,7 @@ ExecInitFunctionScan(FunctionScan *node, EState *estate, int eflags) TupleDescInitEntryCollation(tupdesc, (AttrNumber) 1, exprCollation(funcexpr)); + funcReturnsTuple = false; } else if (functypclass == TYPEFUNC_RECORD) { @@ -407,6 +418,7 @@ ExecInitFunctionScan(FunctionScan *node, EState *estate, int eflags) * case it doesn't.) */ BlessTupleDesc(tupdesc); + funcReturnsTuple = true; } else { @@ -414,21 +426,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); + + SRFScanPlan *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; + Plan *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 +479,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 +498,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 +580,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 +603,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 +621,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 +633,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 dd077f4323..fdec8521ad 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,30 @@ 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 b07c2996d4..48d7db54d3 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 4a1b060fde..66a1d30778 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 0000000000..4d61a95ed7 --- /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(estate->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); + + /* + * 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. + */ + HeapTupleData tmptup; + 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 d17af13ee3..118fdcb52f 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 6ef3e1fe06..f0fb68dd6a 100644 --- a/src/include/executor/executor.h +++ b/src/include/executor/executor.h @@ -404,13 +404,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 74e8eefd38..ca89980edf 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 99e7cbfc94..f55922c5bd 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 0000000000..2430de5976 --- /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 1f6f5bbc20..973115aefe 100644 --- a/src/include/nodes/execnodes.h +++ b/src/include/nodes/execnodes.h @@ -793,10 +793,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 @@ -1647,6 +1653,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; @@ -1974,6 +1981,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 baced7eec0..9df64fb325 100644 --- a/src/include/nodes/nodes.h +++ b/src/include/nodes/nodes.h @@ -513,7 +513,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 32c0d87f80..07ae669e7a 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 f457b5b150..ab8e222f3b 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 c1f802c88a..5eb7dba0a8 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 dfd0ee414f..c339722149 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 761376b007..3650aeefe0 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 0879c885eb..172a55d975 100644 --- a/src/test/regress/expected/misc_functions.out +++ b/src/test/regress/expected/misc_functions.out @@ -179,9 +179,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; @@ -189,7 +190,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 64d41dfdad..e68adc1f24 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 cd2c79f4d5..67a6f39ae2 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 a70060ba01..7f96baaee8 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 fe1cd9deb0..9f6deff81e 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 6e72e92d80..6828582d1e 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 d5fd4045f9..d2cd0b529f 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 d841d8c0f9..4717b069be 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 476b4f27e2..4d39f39b57 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 +-------------------------------------------------------------------------------- -- 2.23.0