diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/backend/executor/execQual.c | 60 | ||||
-rw-r--r-- | src/backend/executor/functions.c | 710 | ||||
-rw-r--r-- | src/backend/tcop/dest.c | 9 | ||||
-rw-r--r-- | src/backend/utils/fmgr/README | 6 | ||||
-rw-r--r-- | src/include/executor/functions.h | 5 | ||||
-rw-r--r-- | src/include/nodes/execnodes.h | 8 | ||||
-rw-r--r-- | src/include/tcop/dest.h | 5 | ||||
-rw-r--r-- | src/test/regress/expected/rangefuncs.out | 176 | ||||
-rw-r--r-- | src/test/regress/output/create_function_1.source | 2 | ||||
-rw-r--r-- | src/test/regress/sql/rangefuncs.sql | 59 |
10 files changed, 770 insertions, 270 deletions
diff --git a/src/backend/executor/execQual.c b/src/backend/executor/execQual.c index 4de99be6262..4af50bebd77 100644 --- a/src/backend/executor/execQual.c +++ b/src/backend/executor/execQual.c @@ -8,7 +8,7 @@ * * * IDENTIFICATION - * $PostgreSQL: pgsql/src/backend/executor/execQual.c,v 1.235 2008/10/29 00:00:38 tgl Exp $ + * $PostgreSQL: pgsql/src/backend/executor/execQual.c,v 1.236 2008/10/31 19:37:56 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -1334,7 +1334,8 @@ ExecMakeFunctionResult(FuncExprState *fcache, { List *arguments; Datum result; - FunctionCallInfoData fcinfo; + FunctionCallInfoData fcinfo_data; + FunctionCallInfo fcinfo; PgStat_FunctionCallUsage fcusage; ReturnSetInfo rsinfo; /* for functions returning sets */ ExprDoneCond argDone; @@ -1385,6 +1386,20 @@ restart: } /* + * For non-set-returning functions, we just use a local-variable + * FunctionCallInfoData. For set-returning functions we keep the callinfo + * record in fcache->setArgs so that it can survive across multiple + * value-per-call invocations. (The reason we don't just do the latter + * all the time is that plpgsql expects to be able to use simple expression + * trees re-entrantly. Which might not be a good idea, but the penalty + * for not doing so is high.) + */ + if (fcache->func.fn_retset) + fcinfo = &fcache->setArgs; + else + fcinfo = &fcinfo_data; + + /* * 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 @@ -1394,8 +1409,8 @@ restart: if (!fcache->setArgsValid) { /* Need to prep callinfo structure */ - InitFunctionCallInfoData(fcinfo, &(fcache->func), 0, NULL, NULL); - argDone = ExecEvalFuncArgs(&fcinfo, arguments, econtext); + InitFunctionCallInfoData(*fcinfo, &(fcache->func), 0, NULL, NULL); + argDone = ExecEvalFuncArgs(fcinfo, arguments, econtext); if (argDone == ExprEndResult) { /* input is an empty set, so return an empty set. */ @@ -1412,8 +1427,7 @@ restart: } else { - /* Copy callinfo from previous evaluation */ - memcpy(&fcinfo, &fcache->setArgs, sizeof(fcinfo)); + /* Re-use callinfo from previous evaluation */ hasSetArg = fcache->setHasSetArg; /* Reset flag (we may set it again below) */ fcache->setArgsValid = false; @@ -1424,12 +1438,12 @@ restart: */ if (fcache->func.fn_retset) { - fcinfo.resultinfo = (Node *) &rsinfo; + 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 */ + /* note we do not set SFRM_Materialize_Random or _Preferred */ rsinfo.returnMode = SFRM_ValuePerCall; /* isDone is filled below */ rsinfo.setResult = NULL; @@ -1468,9 +1482,9 @@ restart: if (fcache->func.fn_strict) { - for (i = 0; i < fcinfo.nargs; i++) + for (i = 0; i < fcinfo->nargs; i++) { - if (fcinfo.argnull[i]) + if (fcinfo->argnull[i]) { callit = false; break; @@ -1480,12 +1494,12 @@ restart: if (callit) { - pgstat_init_function_usage(&fcinfo, &fcusage); + pgstat_init_function_usage(fcinfo, &fcusage); - fcinfo.isnull = false; + fcinfo->isnull = false; rsinfo.isDone = ExprSingleResult; - result = FunctionCallInvoke(&fcinfo); - *isNull = fcinfo.isnull; + result = FunctionCallInvoke(fcinfo); + *isNull = fcinfo->isnull; *isDone = rsinfo.isDone; pgstat_end_function_usage(&fcusage, @@ -1511,7 +1525,7 @@ restart: if (fcache->func.fn_retset && *isDone == ExprMultipleResult) { - memcpy(&fcache->setArgs, &fcinfo, sizeof(fcinfo)); + Assert(fcinfo == &fcache->setArgs); fcache->setHasSetArg = hasSetArg; fcache->setArgsValid = true; /* Register cleanup callback if we didn't already */ @@ -1567,7 +1581,7 @@ restart: break; /* input not a set, so done */ /* Re-eval args to get the next element of the input set */ - argDone = ExecEvalFuncArgs(&fcinfo, arguments, econtext); + argDone = ExecEvalFuncArgs(fcinfo, arguments, econtext); if (argDone != ExprMultipleResult) { @@ -1605,9 +1619,9 @@ restart: */ if (fcache->func.fn_strict) { - for (i = 0; i < fcinfo.nargs; i++) + for (i = 0; i < fcinfo->nargs; i++) { - if (fcinfo.argnull[i]) + if (fcinfo->argnull[i]) { *isNull = true; return (Datum) 0; @@ -1615,11 +1629,11 @@ restart: } } - pgstat_init_function_usage(&fcinfo, &fcusage); + pgstat_init_function_usage(fcinfo, &fcusage); - fcinfo.isnull = false; - result = FunctionCallInvoke(&fcinfo); - *isNull = fcinfo.isnull; + fcinfo->isnull = false; + result = FunctionCallInvoke(fcinfo); + *isNull = fcinfo->isnull; pgstat_end_function_usage(&fcusage, true); } @@ -1737,7 +1751,7 @@ ExecMakeTableFunctionResult(ExprState *funcexpr, rsinfo.type = T_ReturnSetInfo; rsinfo.econtext = econtext; rsinfo.expectedDesc = expectedDesc; - rsinfo.allowedModes = (int) (SFRM_ValuePerCall | SFRM_Materialize); + rsinfo.allowedModes = (int) (SFRM_ValuePerCall | SFRM_Materialize | SFRM_Materialize_Preferred); if (randomAccess) rsinfo.allowedModes |= (int) SFRM_Materialize_Random; rsinfo.returnMode = SFRM_ValuePerCall; diff --git a/src/backend/executor/functions.c b/src/backend/executor/functions.c index 3ce4d0f3210..a58703111ca 100644 --- a/src/backend/executor/functions.c +++ b/src/backend/executor/functions.c @@ -8,7 +8,7 @@ * * * IDENTIFICATION - * $PostgreSQL: pgsql/src/backend/executor/functions.c,v 1.126 2008/08/25 22:42:32 tgl Exp $ + * $PostgreSQL: pgsql/src/backend/executor/functions.c,v 1.127 2008/10/31 19:37:56 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -20,20 +20,30 @@ #include "commands/trigger.h" #include "executor/functions.h" #include "funcapi.h" +#include "miscadmin.h" #include "nodes/makefuncs.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" -#include "tcop/tcopprot.h" #include "tcop/utility.h" #include "utils/builtins.h" #include "utils/datum.h" #include "utils/lsyscache.h" #include "utils/snapmgr.h" #include "utils/syscache.h" -#include "utils/typcache.h" /* + * Specialized DestReceiver for collecting query output in a SQL function + */ +typedef struct +{ + DestReceiver pub; /* publicly-known function pointers */ + Tuplestorestate *tstore; /* where to put result tuples */ + MemoryContext cxt; /* context containing tstore */ + JunkFilter *filter; /* filter to convert tuple type */ +} DR_sqlfunction; + +/* * We have an execution_state record for each query in a function. Each * record contains a plantree for its query. If the query is currently in * F_EXEC_RUN state then there's a QueryDesc too. @@ -43,20 +53,24 @@ typedef enum F_EXEC_START, F_EXEC_RUN, F_EXEC_DONE } ExecStatus; -typedef struct local_es +typedef struct execution_state { - struct local_es *next; + struct execution_state *next; ExecStatus status; + bool setsResult; /* true if this query produces func's result */ + bool lazyEval; /* true if should fetch one row at a time */ Node *stmt; /* PlannedStmt or utility statement */ QueryDesc *qd; /* null unless status == RUN */ } execution_state; -#define LAST_POSTQUEL_COMMAND(es) ((es)->next == NULL) - /* * An SQLFunctionCache record is built during the first call, * and linked to from the fn_extra field of the FmgrInfo struct. + * + * Note that currently this has only the lifespan of the calling query. + * Someday we might want to consider caching the parse/plan results longer + * than that. */ typedef struct { @@ -66,13 +80,17 @@ typedef struct Oid rettype; /* actual return type */ int16 typlen; /* length of the return type */ bool typbyval; /* true if return type is pass by value */ + bool returnsSet; /* true if returning multiple rows */ bool returnsTuple; /* true if returning whole tuple result */ bool shutdown_reg; /* true if registered shutdown callback */ bool readonly_func; /* true to run in "read only" mode */ + bool lazyEval; /* true if using lazyEval for result query */ ParamListInfo paramLI; /* Param list representing current args */ - JunkFilter *junkFilter; /* used only if returnsTuple */ + Tuplestorestate *tstore; /* where we accumulate result tuples */ + + JunkFilter *junkFilter; /* will be NULL if function returns VOID */ /* head of linked list of execution_state records */ execution_state *func_state; @@ -83,32 +101,41 @@ typedef SQLFunctionCache *SQLFunctionCachePtr; /* non-export function prototypes */ static execution_state *init_execution_state(List *queryTree_list, - bool readonly_func); -static void init_sql_fcache(FmgrInfo *finfo); + SQLFunctionCachePtr fcache, + bool lazyEvalOK); +static void init_sql_fcache(FmgrInfo *finfo, bool lazyEvalOK); static void postquel_start(execution_state *es, SQLFunctionCachePtr fcache); static TupleTableSlot *postquel_getnext(execution_state *es, SQLFunctionCachePtr fcache); static void postquel_end(execution_state *es); static void postquel_sub_params(SQLFunctionCachePtr fcache, FunctionCallInfo fcinfo); -static Datum postquel_execute(execution_state *es, - FunctionCallInfo fcinfo, - SQLFunctionCachePtr fcache, - MemoryContext resultcontext); +static Datum postquel_get_single_result(TupleTableSlot *slot, + FunctionCallInfo fcinfo, + SQLFunctionCachePtr fcache, + MemoryContext resultcontext); static void sql_exec_error_callback(void *arg); static void ShutdownSQLFunction(Datum arg); +static void sqlfunction_startup(DestReceiver *self, int operation, TupleDesc typeinfo); +static void sqlfunction_receive(TupleTableSlot *slot, DestReceiver *self); +static void sqlfunction_shutdown(DestReceiver *self); +static void sqlfunction_destroy(DestReceiver *self); +/* Set up the list of per-query execution_state records for a SQL function */ static execution_state * -init_execution_state(List *queryTree_list, bool readonly_func) +init_execution_state(List *queryTree_list, + SQLFunctionCachePtr fcache, + bool lazyEvalOK) { execution_state *firstes = NULL; execution_state *preves = NULL; + execution_state *lasttages = NULL; ListCell *qtl_item; foreach(qtl_item, queryTree_list) { - Query *queryTree = lfirst(qtl_item); + Query *queryTree = (Query *) lfirst(qtl_item); Node *stmt; execution_state *newes; @@ -127,7 +154,7 @@ init_execution_state(List *queryTree_list, bool readonly_func) errmsg("%s is not allowed in a SQL function", CreateCommandTag(stmt)))); - if (readonly_func && !CommandIsReadOnly(stmt)) + if (fcache->readonly_func && !CommandIsReadOnly(stmt)) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), /* translator: %s is a SQL statement name */ @@ -142,18 +169,53 @@ init_execution_state(List *queryTree_list, bool readonly_func) newes->next = NULL; newes->status = F_EXEC_START; + newes->setsResult = false; /* might change below */ + newes->lazyEval = false; /* might change below */ newes->stmt = stmt; newes->qd = NULL; + if (queryTree->canSetTag) + lasttages = newes; + preves = newes; } + /* + * Mark the last canSetTag query as delivering the function result; + * then, if it is a plain SELECT, mark it for lazy evaluation. + * If it's not a SELECT we must always run it to completion. + * + * Note: at some point we might add additional criteria for whether to use + * lazy eval. However, we should prefer to use it whenever the function + * doesn't return set, since fetching more than one row is useless in that + * case. + * + * Note: don't set setsResult if the function returns VOID, as evidenced + * by not having made a junkfilter. This ensures we'll throw away any + * output from a utility statement that check_sql_fn_retval deemed to + * not have output. + */ + if (lasttages && fcache->junkFilter) + { + lasttages->setsResult = true; + if (lazyEvalOK && + IsA(lasttages->stmt, PlannedStmt)) + { + PlannedStmt *ps = (PlannedStmt *) lasttages->stmt; + + if (ps->commandType == CMD_SELECT && + ps->utilityStmt == NULL && + ps->intoClause == NULL) + fcache->lazyEval = lasttages->lazyEval = true; + } + } + return firstes; } - +/* Initialize the SQLFunctionCache for a SQL function */ static void -init_sql_fcache(FmgrInfo *finfo) +init_sql_fcache(FmgrInfo *finfo, bool lazyEvalOK) { Oid foid = finfo->fn_oid; Oid rettype; @@ -199,6 +261,9 @@ init_sql_fcache(FmgrInfo *finfo) /* Fetch the typlen and byval info for the result type */ get_typlenbyval(rettype, &fcache->typlen, &fcache->typbyval); + /* Remember whether we're returning setof something */ + fcache->returnsSet = procedureStruct->proretset; + /* Remember if function is STABLE/IMMUTABLE */ fcache->readonly_func = (procedureStruct->provolatile != PROVOLATILE_VOLATILE); @@ -262,11 +327,14 @@ init_sql_fcache(FmgrInfo *finfo) * Note: we set fcache->returnsTuple according to whether we are returning * the whole tuple result or just a single column. In the latter case we * clear returnsTuple because we need not act different from the scalar - * result case, even if it's a rowtype column. + * result case, even if it's a rowtype column. (However, we have to + * force lazy eval mode in that case; otherwise we'd need extra code to + * expand the rowtype column into multiple columns, since we have no + * way to notify the caller that it should do that.) * - * In the returnsTuple case, check_sql_fn_retval will also construct a - * JunkFilter we can use to coerce the returned rowtype to the desired - * form. + * check_sql_fn_retval will also construct a JunkFilter we can use to + * coerce the returned rowtype to the desired form (unless the result type + * is VOID, in which case there's nothing to coerce to). */ fcache->returnsTuple = check_sql_fn_retval(foid, rettype, @@ -274,20 +342,38 @@ init_sql_fcache(FmgrInfo *finfo) false, &fcache->junkFilter); + if (fcache->returnsTuple) + { + /* Make sure output rowtype is properly blessed */ + BlessTupleDesc(fcache->junkFilter->jf_resultSlot->tts_tupleDescriptor); + } + else if (fcache->returnsSet && type_is_rowtype(fcache->rettype)) + { + /* + * Returning rowtype as if it were scalar --- materialize won't work. + * Right now it's sufficient to override any caller preference for + * materialize mode, but to add more smarts in init_execution_state + * about this, we'd probably need a three-way flag instead of bool. + */ + lazyEvalOK = true; + } + /* Finally, plan the queries */ fcache->func_state = init_execution_state(queryTree_list, - fcache->readonly_func); + fcache, + lazyEvalOK); ReleaseSysCache(procedureTuple); finfo->fn_extra = (void *) fcache; } - +/* Start up execution of one execution_state node */ static void postquel_start(execution_state *es, SQLFunctionCachePtr fcache) { Snapshot snapshot; + DestReceiver *dest; Assert(es->qd == NULL); @@ -305,15 +391,34 @@ postquel_start(execution_state *es, SQLFunctionCachePtr fcache) snapshot = GetTransactionSnapshot(); } + /* + * If this query produces the function result, send its output to the + * tuplestore; else discard any output. + */ + if (es->setsResult) + { + DR_sqlfunction *myState; + + dest = CreateDestReceiver(DestSQLFunction, NULL); + /* pass down the needed info to the dest receiver routines */ + myState = (DR_sqlfunction *) dest; + Assert(myState->pub.mydest == DestSQLFunction); + myState->tstore = fcache->tstore; + myState->cxt = CurrentMemoryContext; + myState->filter = fcache->junkFilter; + } + else + dest = None_Receiver; + if (IsA(es->stmt, PlannedStmt)) es->qd = CreateQueryDesc((PlannedStmt *) es->stmt, snapshot, InvalidSnapshot, - None_Receiver, + dest, fcache->paramLI, false); else es->qd = CreateUtilityQueryDesc(es->stmt, snapshot, - None_Receiver, + dest, fcache->paramLI); /* We assume we don't need to set up ActiveSnapshot for ExecutorStart */ @@ -335,11 +440,11 @@ postquel_start(execution_state *es, SQLFunctionCachePtr fcache) es->status = F_EXEC_RUN; } +/* Run one execution_state; either to completion or to first result row */ static TupleTableSlot * postquel_getnext(execution_state *es, SQLFunctionCachePtr fcache) { TupleTableSlot *result; - long count; /* Make our snapshot the active one for any called functions */ PushActiveSnapshot(es->qd->snapshot); @@ -359,19 +464,8 @@ postquel_getnext(execution_state *es, SQLFunctionCachePtr fcache) } else { - /* - * If it's the function's last command, and it's a SELECT, fetch - * one row at a time so we can return the results. Otherwise just - * run it to completion. (If we run to completion then - * ExecutorRun is guaranteed to return NULL.) - */ - if (LAST_POSTQUEL_COMMAND(es) && - es->qd->operation == CMD_SELECT && - es->qd->plannedstmt->utilityStmt == NULL && - es->qd->plannedstmt->intoClause == NULL) - count = 1L; - else - count = 0L; + /* Run regular commands to completion unless lazyEval */ + long count = (es->lazyEval) ? 1L : 0L; result = ExecutorRun(es->qd, ForwardScanDirection, count); } @@ -381,6 +475,7 @@ postquel_getnext(execution_state *es, SQLFunctionCachePtr fcache) return result; } +/* Shut down execution of one execution_state node */ static void postquel_end(execution_state *es) { @@ -409,17 +504,26 @@ static void postquel_sub_params(SQLFunctionCachePtr fcache, FunctionCallInfo fcinfo) { - ParamListInfo paramLI; int nargs = fcinfo->nargs; if (nargs > 0) { + ParamListInfo paramLI; int i; - /* sizeof(ParamListInfoData) includes the first array element */ - paramLI = (ParamListInfo) palloc(sizeof(ParamListInfoData) + + if (fcache->paramLI == NULL) + { + /* sizeof(ParamListInfoData) includes the first array element */ + paramLI = (ParamListInfo) palloc(sizeof(ParamListInfoData) + (nargs - 1) *sizeof(ParamExternData)); - paramLI->numParams = nargs; + paramLI->numParams = nargs; + fcache->paramLI = paramLI; + } + else + { + paramLI = fcache->paramLI; + Assert(paramLI->numParams == nargs); + } for (i = 0; i < nargs; i++) { @@ -432,116 +536,37 @@ postquel_sub_params(SQLFunctionCachePtr fcache, } } else - paramLI = NULL; - - if (fcache->paramLI) - pfree(fcache->paramLI); - - fcache->paramLI = paramLI; + fcache->paramLI = NULL; } +/* + * Extract the SQL function's value from a single result row. This is used + * both for scalar (non-set) functions and for each row of a lazy-eval set + * result. + */ static Datum -postquel_execute(execution_state *es, - FunctionCallInfo fcinfo, - SQLFunctionCachePtr fcache, - MemoryContext resultcontext) +postquel_get_single_result(TupleTableSlot *slot, + FunctionCallInfo fcinfo, + SQLFunctionCachePtr fcache, + MemoryContext resultcontext) { - TupleTableSlot *slot; Datum value; MemoryContext oldcontext; - if (es->status == F_EXEC_START) - postquel_start(es, fcache); - - slot = postquel_getnext(es, fcache); - - if (TupIsNull(slot)) - { - /* - * We fall out here for all cases except where we have obtained a row - * from a function's final SELECT. - */ - postquel_end(es); - fcinfo->isnull = true; - return (Datum) NULL; - } - - /* - * If we got a row from a command within the function it has to be the - * final command. All others shouldn't be returning anything. - */ - Assert(LAST_POSTQUEL_COMMAND(es)); - /* * Set up to return the function value. For pass-by-reference datatypes, * be sure to allocate the result in resultcontext, not the current memory - * context (which has query lifespan). + * context (which has query lifespan). We can't leave the data in the + * TupleTableSlot because we intend to clear the slot before returning. */ oldcontext = MemoryContextSwitchTo(resultcontext); if (fcache->returnsTuple) { - /* - * We are returning the whole tuple, so filter it and apply the proper - * labeling to make it a valid Datum. There are several reasons why - * we do this: - * - * 1. To copy the tuple out of the child execution context and into - * the desired result context. - * - * 2. To remove any junk attributes present in the raw subselect - * result. (This is probably not absolutely necessary, but it seems - * like good policy.) - * - * 3. To insert dummy null columns if the declared result type has any - * attisdropped columns. - */ - HeapTuple newtup; - HeapTupleHeader dtup; - uint32 t_len; - Oid dtuptype; - int32 dtuptypmod; - - newtup = ExecRemoveJunk(fcache->junkFilter, slot); - - /* - * Compress out the HeapTuple header data. We assume that - * heap_form_tuple made the tuple with header and body in one palloc'd - * chunk. We want to return a pointer to the chunk start so that it - * will work if someone tries to free it. - */ - t_len = newtup->t_len; - dtup = (HeapTupleHeader) newtup; - memmove((char *) dtup, (char *) newtup->t_data, t_len); - - /* - * Use the declared return type if it's not RECORD; else take the type - * from the computed result, making sure a typmod has been assigned. - */ - if (fcache->rettype != RECORDOID) - { - /* function has a named composite return type */ - dtuptype = fcache->rettype; - dtuptypmod = -1; - } - else - { - /* function is declared to return RECORD */ - TupleDesc tupDesc = fcache->junkFilter->jf_cleanTupType; - - if (tupDesc->tdtypeid == RECORDOID && - tupDesc->tdtypmod < 0) - assign_record_type_typmod(tupDesc); - dtuptype = tupDesc->tdtypeid; - dtuptypmod = tupDesc->tdtypmod; - } - - HeapTupleHeaderSetDatumLength(dtup, t_len); - HeapTupleHeaderSetTypeId(dtup, dtuptype); - HeapTupleHeaderSetTypMod(dtup, dtuptypmod); - - value = PointerGetDatum(dtup); + /* We must return the whole tuple as a Datum. */ fcinfo->isnull = false; + value = ExecFetchSlotTupleDatum(slot); + value = datumCopy(value, fcache->typbyval, fcache->typlen); } else { @@ -557,24 +582,23 @@ postquel_execute(execution_state *es, MemoryContextSwitchTo(oldcontext); - /* - * If this is a single valued function we have to end the function - * execution now. - */ - if (!fcinfo->flinfo->fn_retset) - postquel_end(es); - return value; } +/* + * fmgr_sql: function call manager for SQL functions + */ Datum fmgr_sql(PG_FUNCTION_ARGS) { MemoryContext oldcontext; SQLFunctionCachePtr fcache; ErrorContextCallback sqlerrcontext; + bool randomAccess; + bool lazyEvalOK; execution_state *es; - Datum result = 0; + TupleTableSlot *slot; + Datum result; /* * Switch to context in which the fcache lives. This ensures that @@ -591,13 +615,39 @@ fmgr_sql(PG_FUNCTION_ARGS) sqlerrcontext.previous = error_context_stack; error_context_stack = &sqlerrcontext; + /* Check call context */ + if (fcinfo->flinfo->fn_retset) + { + ReturnSetInfo *rsi = (ReturnSetInfo *) fcinfo->resultinfo; + + /* + * For simplicity, we require callers to support both set eval modes. + * There are cases where we must use one or must use the other, and + * it's not really worthwhile to postpone the check till we know. + */ + if (!rsi || !IsA(rsi, ReturnSetInfo) || + (rsi->allowedModes & SFRM_ValuePerCall) == 0 || + (rsi->allowedModes & SFRM_Materialize) == 0 || + rsi->expectedDesc == NULL) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("set-valued function called in context that cannot accept a set"))); + randomAccess = rsi->allowedModes & SFRM_Materialize_Random; + lazyEvalOK = !(rsi->allowedModes & SFRM_Materialize_Preferred); + } + else + { + randomAccess = false; + lazyEvalOK = true; + } + /* * Initialize fcache (build plans) if first time through. */ fcache = (SQLFunctionCachePtr) fcinfo->flinfo->fn_extra; if (fcache == NULL) { - init_sql_fcache(fcinfo->flinfo); + init_sql_fcache(fcinfo->flinfo, lazyEvalOK); fcache = (SQLFunctionCachePtr) fcinfo->flinfo->fn_extra; } es = fcache->func_state; @@ -610,51 +660,110 @@ fmgr_sql(PG_FUNCTION_ARGS) postquel_sub_params(fcache, fcinfo); /* + * Build tuplestore to hold results, if we don't have one already. + * Note it's in the query-lifespan context. + */ + if (!fcache->tstore) + fcache->tstore = tuplestore_begin_heap(randomAccess, false, work_mem); + + /* * Find first unfinished query in function. */ while (es && es->status == F_EXEC_DONE) es = es->next; /* - * Execute each command in the function one after another until we're - * executing the final command and get a result or we run out of commands. + * Execute each command in the function one after another until we either + * run out of commands or get a result row from a lazily-evaluated SELECT. */ while (es) { - result = postquel_execute(es, fcinfo, fcache, oldcontext); + TupleTableSlot *slot; + + if (es->status == F_EXEC_START) + postquel_start(es, fcache); + + slot = postquel_getnext(es, fcache); + + /* + * If we ran the command to completion, we can shut it down now. + * Any row(s) we need to return are safely stashed in the tuplestore, + * and we want to be sure that, for example, AFTER triggers get fired + * before we return anything. Also, if the function doesn't return + * set, we can shut it down anyway because we don't care about + * fetching any more result rows. + */ + if (TupIsNull(slot) || !fcache->returnsSet) + postquel_end(es); + + /* + * Break from loop if we didn't shut down (implying we got a + * lazily-evaluated row). Otherwise we'll press on till the + * whole function is done, relying on the tuplestore to keep hold + * of the data to eventually be returned. This is necessary since + * an INSERT/UPDATE/DELETE RETURNING that sets the result might be + * followed by additional rule-inserted commands, and we want to + * finish doing all those commands before we return anything. + */ if (es->status != F_EXEC_DONE) break; es = es->next; } /* - * If we've gone through every command in this function, we are done. + * The tuplestore now contains whatever row(s) we are supposed to return. */ - if (es == NULL) + if (fcache->returnsSet) { - /* - * Reset the execution states to start over again on next call. - */ - es = fcache->func_state; - while (es) + ReturnSetInfo *rsi = (ReturnSetInfo *) fcinfo->resultinfo; + + if (es) { - es->status = F_EXEC_START; - es = es->next; - } + /* + * If we stopped short of being done, we must have a lazy-eval row. + */ + Assert(es->lazyEval); + /* Re-use the junkfilter's output slot to fetch back the tuple */ + Assert(fcache->junkFilter); + slot = fcache->junkFilter->jf_resultSlot; + if (!tuplestore_gettupleslot(fcache->tstore, true, slot)) + elog(ERROR, "failed to fetch lazy-eval tuple"); + /* Extract the result as a datum, and copy out from the slot */ + result = postquel_get_single_result(slot, fcinfo, + fcache, oldcontext); + /* Clear the tuplestore, but keep it for next time */ + /* NB: this might delete the slot's content, but we don't care */ + tuplestore_clear(fcache->tstore); - /* - * Let caller know we're finished. - */ - if (fcinfo->flinfo->fn_retset) + /* + * Let caller know we're not finished. + */ + rsi->isDone = ExprMultipleResult; + + /* + * Ensure we will get shut down cleanly if the exprcontext is not + * run to completion. + */ + if (!fcache->shutdown_reg) + { + RegisterExprContextCallback(rsi->econtext, + ShutdownSQLFunction, + PointerGetDatum(fcache)); + fcache->shutdown_reg = true; + } + } + else if (fcache->lazyEval) { - ReturnSetInfo *rsi = (ReturnSetInfo *) fcinfo->resultinfo; + /* + * We are done with a lazy evaluation. Clean up. + */ + tuplestore_clear(fcache->tstore); + + /* + * Let caller know we're finished. + */ + rsi->isDone = ExprEndResult; - if (rsi && IsA(rsi, ReturnSetInfo)) - rsi->isDone = ExprEndResult; - else - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("set-valued function called in context that cannot accept a set"))); fcinfo->isnull = true; result = (Datum) 0; @@ -667,44 +776,74 @@ fmgr_sql(PG_FUNCTION_ARGS) fcache->shutdown_reg = false; } } + else + { + /* + * We are done with a non-lazy evaluation. Return whatever is + * in the tuplestore. (It is now caller's responsibility to + * free the tuplestore when done.) + */ + rsi->returnMode = SFRM_Materialize; + rsi->setResult = fcache->tstore; + fcache->tstore = NULL; + /* must copy desc because execQual will free it */ + if (fcache->junkFilter) + rsi->setDesc = CreateTupleDescCopy(fcache->junkFilter->jf_cleanTupType); - error_context_stack = sqlerrcontext.previous; - - MemoryContextSwitchTo(oldcontext); + fcinfo->isnull = true; + result = (Datum) 0; - return result; + /* Deregister shutdown callback, if we made one */ + if (fcache->shutdown_reg) + { + UnregisterExprContextCallback(rsi->econtext, + ShutdownSQLFunction, + PointerGetDatum(fcache)); + fcache->shutdown_reg = false; + } + } } + else + { + /* + * Non-set function. If we got a row, return it; else return NULL. + */ + if (fcache->junkFilter) + { + /* Re-use the junkfilter's output slot to fetch back the tuple */ + slot = fcache->junkFilter->jf_resultSlot; + if (tuplestore_gettupleslot(fcache->tstore, true, slot)) + result = postquel_get_single_result(slot, fcinfo, + fcache, oldcontext); + else + { + fcinfo->isnull = true; + result = (Datum) 0; + } + } + else + { + /* Should only get here for VOID functions */ + Assert(fcache->rettype == VOIDOID); + fcinfo->isnull = true; + result = (Datum) 0; + } - /* - * If we got a result from a command within the function it has to be the - * final command. All others shouldn't be returning anything. - */ - Assert(LAST_POSTQUEL_COMMAND(es)); + /* Clear the tuplestore, but keep it for next time */ + tuplestore_clear(fcache->tstore); + } /* - * Let caller know we're not finished. + * If we've gone through every command in the function, we are done. + * Reset the execution states to start over again on next call. */ - if (fcinfo->flinfo->fn_retset) + if (es == NULL) { - ReturnSetInfo *rsi = (ReturnSetInfo *) fcinfo->resultinfo; - - if (rsi && IsA(rsi, ReturnSetInfo)) - rsi->isDone = ExprMultipleResult; - else - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("set-valued function called in context that cannot accept a set"))); - - /* - * Ensure we will get shut down cleanly if the exprcontext is not run - * to completion. - */ - if (!fcache->shutdown_reg) + es = fcache->func_state; + while (es) { - RegisterExprContextCallback(rsi->econtext, - ShutdownSQLFunction, - PointerGetDatum(fcache)); - fcache->shutdown_reg = true; + es->status = F_EXEC_START; + es = es->next; } } @@ -823,6 +962,11 @@ ShutdownSQLFunction(Datum arg) es = es->next; } + /* Release tuplestore if we have one */ + if (fcache->tstore) + tuplestore_end(fcache->tstore); + fcache->tstore = NULL; + /* execUtils will deregister the callback... */ fcache->shutdown_reg = false; } @@ -831,8 +975,8 @@ ShutdownSQLFunction(Datum arg) /* * check_sql_fn_retval() -- check return value of a list of sql parse trees. * - * The return value of a sql function is the value returned by - * the final query in the function. We do some ad-hoc type checking here + * The return value of a sql function is the value returned by the last + * canSetTag query in the function. We do some ad-hoc type checking here * to be sure that the user is returning the type he claims. There are * also a couple of strange-looking features to assist callers in dealing * with allowed special cases, such as binary-compatible result types. @@ -843,18 +987,19 @@ ShutdownSQLFunction(Datum arg) * function definition of a polymorphic function.) * * This function returns true if the sql function returns the entire tuple - * result of its final SELECT, and false otherwise. Note that because we + * result of its final statement, and false otherwise. Note that because we * allow "SELECT rowtype_expression", this may be false even when the declared * function return type is a rowtype. * * If insertRelabels is true, then binary-compatible cases are dealt with - * by actually inserting RelabelType nodes into the final SELECT; obviously - * the caller must pass a parsetree that it's okay to modify in this case. + * by actually inserting RelabelType nodes into the output targetlist; + * obviously the caller must pass a parsetree that it's okay to modify in this + * case. * * If junkFilter isn't NULL, then *junkFilter is set to a JunkFilter defined * to convert the function's tuple result to the correct output tuple type. - * Whenever the result value is false (ie, the function isn't returning a - * tuple result), *junkFilter is set to NULL. + * Exception: if the function is defined to return VOID then *junkFilter is + * set to NULL. */ bool check_sql_fn_retval(Oid func_id, Oid rettype, List *queryTreeList, @@ -863,62 +1008,79 @@ check_sql_fn_retval(Oid func_id, Oid rettype, List *queryTreeList, { Query *parse; List *tlist; - ListCell *tlistitem; int tlistlen; char fn_typtype; Oid restype; + ListCell *lc; AssertArg(!IsPolymorphicType(rettype)); if (junkFilter) - *junkFilter = NULL; /* default result */ + *junkFilter = NULL; /* initialize in case of VOID result */ - /* guard against empty function body; OK only if void return type */ - if (queryTreeList == NIL) + /* + * Find the last canSetTag query in the list. This isn't necessarily + * the last parsetree, because rule rewriting can insert queries after + * what the user wrote. + */ + parse = NULL; + foreach(lc, queryTreeList) { - if (rettype != VOIDOID) - ereport(ERROR, - (errcode(ERRCODE_INVALID_FUNCTION_DEFINITION), - errmsg("return type mismatch in function declared to return %s", - format_type_be(rettype)), - errdetail("Function's final statement must be a SELECT."))); - return false; - } + Query *q = (Query *) lfirst(lc); - /* find the final query */ - parse = (Query *) lfirst(list_tail(queryTreeList)); + if (q->canSetTag) + parse = q; + } /* - * If the last query isn't a SELECT, the return type must be VOID. + * If it's a plain SELECT, it returns whatever the targetlist says. + * Otherwise, if it's INSERT/UPDATE/DELETE with RETURNING, it returns that. + * Otherwise, the function return type must be VOID. * * Note: eventually replace this test with QueryReturnsTuples? We'd need - * a more general method of determining the output type, though. + * a more general method of determining the output type, though. Also, + * it seems too dangerous to consider FETCH or EXECUTE as returning a + * determinable rowtype, since they depend on relatively short-lived + * entities. */ - if (!(parse->commandType == CMD_SELECT && - parse->utilityStmt == NULL && - parse->intoClause == NULL)) + if (parse && + parse->commandType == CMD_SELECT && + parse->utilityStmt == NULL && + parse->intoClause == NULL) + { + tlist = parse->targetList; + } + else if (parse && + (parse->commandType == CMD_INSERT || + parse->commandType == CMD_UPDATE || + parse->commandType == CMD_DELETE) && + parse->returningList) { + tlist = parse->returningList; + } + else + { + /* Empty function body, or last statement is a utility command */ if (rettype != VOIDOID) ereport(ERROR, (errcode(ERRCODE_INVALID_FUNCTION_DEFINITION), errmsg("return type mismatch in function declared to return %s", format_type_be(rettype)), - errdetail("Function's final statement must be a SELECT."))); + errdetail("Function's final statement must be SELECT or INSERT/UPDATE/DELETE RETURNING."))); return false; } /* - * OK, it's a SELECT, so it must return something matching the declared + * OK, check that the targetlist returns something matching the declared * type. (We used to insist that the declared type not be VOID in this * case, but that makes it hard to write a void function that exits after - * calling another void function. Instead, we insist that the SELECT + * calling another void function. Instead, we insist that the tlist * return void ... so void is treated as if it were a scalar type below.) */ /* * Count the non-junk entries in the result targetlist. */ - tlist = parse->targetList; tlistlen = ExecCleanTargetListLength(tlist); fn_typtype = get_typtype(rettype); @@ -940,7 +1102,7 @@ check_sql_fn_retval(Oid func_id, Oid rettype, List *queryTreeList, (errcode(ERRCODE_INVALID_FUNCTION_DEFINITION), errmsg("return type mismatch in function declared to return %s", format_type_be(rettype)), - errdetail("Final SELECT must return exactly one column."))); + errdetail("Final statement must return exactly one column."))); /* We assume here that non-junk TLEs must come first in tlists */ tle = (TargetEntry *) linitial(tlist); @@ -959,6 +1121,10 @@ check_sql_fn_retval(Oid func_id, Oid rettype, List *queryTreeList, rettype, -1, COERCE_DONTCARE); + + /* Set up junk filter if needed */ + if (junkFilter) + *junkFilter = ExecInitJunkFilter(tlist, false, NULL); } else if (fn_typtype == TYPTYPE_COMPOSITE || rettype == RECORDOID) { @@ -988,6 +1154,9 @@ check_sql_fn_retval(Oid func_id, Oid rettype, List *queryTreeList, rettype, -1, COERCE_DONTCARE); + /* Set up junk filter if needed */ + if (junkFilter) + *junkFilter = ExecInitJunkFilter(tlist, false, NULL); return false; /* NOT returning whole tuple */ } } @@ -1014,9 +1183,9 @@ check_sql_fn_retval(Oid func_id, Oid rettype, List *queryTreeList, tuplogcols = 0; /* we'll count nondeleted cols as we go */ colindex = 0; - foreach(tlistitem, tlist) + foreach(lc, tlist) { - TargetEntry *tle = (TargetEntry *) lfirst(tlistitem); + TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr; Oid tletype; Oid atttype; @@ -1032,7 +1201,7 @@ check_sql_fn_retval(Oid func_id, Oid rettype, List *queryTreeList, (errcode(ERRCODE_INVALID_FUNCTION_DEFINITION), errmsg("return type mismatch in function declared to return %s", format_type_be(rettype)), - errdetail("Final SELECT returns too many columns."))); + errdetail("Final statement returns too many columns."))); attr = tupdesc->attrs[colindex - 1]; } while (attr->attisdropped); tuplogcols++; @@ -1044,7 +1213,7 @@ check_sql_fn_retval(Oid func_id, Oid rettype, List *queryTreeList, (errcode(ERRCODE_INVALID_FUNCTION_DEFINITION), errmsg("return type mismatch in function declared to return %s", format_type_be(rettype)), - errdetail("Final SELECT returns %s instead of %s at column %d.", + errdetail("Final statement returns %s instead of %s at column %d.", format_type_be(tletype), format_type_be(atttype), tuplogcols))); @@ -1069,7 +1238,7 @@ check_sql_fn_retval(Oid func_id, Oid rettype, List *queryTreeList, (errcode(ERRCODE_INVALID_FUNCTION_DEFINITION), errmsg("return type mismatch in function declared to return %s", format_type_be(rettype)), - errdetail("Final SELECT returns too few columns."))); + errdetail("Final statement returns too few columns."))); /* Set up junk filter if needed */ if (junkFilter) @@ -1088,3 +1257,70 @@ check_sql_fn_retval(Oid func_id, Oid rettype, List *queryTreeList, return false; } + + +/* + * CreateSQLFunctionDestReceiver -- create a suitable DestReceiver object + * + * Since CreateDestReceiver doesn't accept the parameters we'd need, + * we just leave the private fields zeroed here. postquel_start will + * fill them in. + */ +DestReceiver * +CreateSQLFunctionDestReceiver(void) +{ + DR_sqlfunction *self = (DR_sqlfunction *) palloc0(sizeof(DR_sqlfunction)); + + self->pub.receiveSlot = sqlfunction_receive; + self->pub.rStartup = sqlfunction_startup; + self->pub.rShutdown = sqlfunction_shutdown; + self->pub.rDestroy = sqlfunction_destroy; + self->pub.mydest = DestSQLFunction; + + return (DestReceiver *) self; +} + +/* + * sqlfunction_startup --- executor startup + */ +static void +sqlfunction_startup(DestReceiver *self, int operation, TupleDesc typeinfo) +{ + /* no-op */ +} + +/* + * sqlfunction_receive --- receive one tuple + */ +static void +sqlfunction_receive(TupleTableSlot *slot, DestReceiver *self) +{ + DR_sqlfunction *myState = (DR_sqlfunction *) self; + MemoryContext oldcxt; + + /* Filter tuple as needed */ + slot = ExecFilterJunk(myState->filter, slot); + + /* Store the filtered tuple into the tuplestore */ + oldcxt = MemoryContextSwitchTo(myState->cxt); + tuplestore_puttupleslot(myState->tstore, slot); + MemoryContextSwitchTo(oldcxt); +} + +/* + * sqlfunction_shutdown --- executor end + */ +static void +sqlfunction_shutdown(DestReceiver *self) +{ + /* no-op */ +} + +/* + * sqlfunction_destroy --- release DestReceiver object + */ +static void +sqlfunction_destroy(DestReceiver *self) +{ + pfree(self); +} diff --git a/src/backend/tcop/dest.c b/src/backend/tcop/dest.c index d5b420b1c86..e687c52f803 100644 --- a/src/backend/tcop/dest.c +++ b/src/backend/tcop/dest.c @@ -8,7 +8,7 @@ * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION - * $PostgreSQL: pgsql/src/backend/tcop/dest.c,v 1.72 2008/01/01 19:45:52 momjian Exp $ + * $PostgreSQL: pgsql/src/backend/tcop/dest.c,v 1.73 2008/10/31 19:37:56 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -32,6 +32,7 @@ #include "access/xact.h" #include "commands/copy.h" #include "executor/executor.h" +#include "executor/functions.h" #include "executor/tstoreReceiver.h" #include "libpq/libpq.h" #include "libpq/pqformat.h" @@ -132,6 +133,9 @@ CreateDestReceiver(CommandDest dest, Portal portal) case DestCopyOut: return CreateCopyDestReceiver(); + + case DestSQLFunction: + return CreateSQLFunctionDestReceiver(); } /* should never get here */ @@ -158,6 +162,7 @@ EndCommand(const char *commandTag, CommandDest dest) case DestTuplestore: case DestIntoRel: case DestCopyOut: + case DestSQLFunction: break; } } @@ -198,6 +203,7 @@ NullCommand(CommandDest dest) case DestTuplestore: case DestIntoRel: case DestCopyOut: + case DestSQLFunction: break; } } @@ -240,6 +246,7 @@ ReadyForQuery(CommandDest dest) case DestTuplestore: case DestIntoRel: case DestCopyOut: + case DestSQLFunction: break; } } diff --git a/src/backend/utils/fmgr/README b/src/backend/utils/fmgr/README index 7c1ccc5bf86..c2624ee4751 100644 --- a/src/backend/utils/fmgr/README +++ b/src/backend/utils/fmgr/README @@ -1,4 +1,4 @@ -$PostgreSQL: pgsql/src/backend/utils/fmgr/README,v 1.15 2008/10/29 00:00:38 tgl Exp $ +$PostgreSQL: pgsql/src/backend/utils/fmgr/README,v 1.16 2008/10/31 19:37:56 tgl Exp $ Function Manager ================ @@ -434,7 +434,9 @@ and returns null. isDone is not used and should be left at ExprSingleResult. The Tuplestore must be created with randomAccess = true if SFRM_Materialize_Random is set in allowedModes, but it can (and preferably -should) be created with randomAccess = false if not. +should) be created with randomAccess = false if not. Callers that can support +both ValuePerCall and Materialize mode will set SFRM_Materialize_Preferred, +or not, depending on which mode they prefer. If available, the expected tuple descriptor is passed in ReturnSetInfo; in other contexts the expectedDesc field will be NULL. The function need diff --git a/src/include/executor/functions.h b/src/include/executor/functions.h index b5451ad43b8..61f782d18db 100644 --- a/src/include/executor/functions.h +++ b/src/include/executor/functions.h @@ -7,7 +7,7 @@ * Portions Copyright (c) 1996-2008, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * - * $PostgreSQL: pgsql/src/include/executor/functions.h,v 1.31 2008/03/18 22:04:14 tgl Exp $ + * $PostgreSQL: pgsql/src/include/executor/functions.h,v 1.32 2008/10/31 19:37:56 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -15,6 +15,7 @@ #define FUNCTIONS_H #include "nodes/execnodes.h" +#include "tcop/dest.h" extern Datum fmgr_sql(PG_FUNCTION_ARGS); @@ -24,4 +25,6 @@ extern bool check_sql_fn_retval(Oid func_id, Oid rettype, bool insertRelabels, JunkFilter **junkFilter); +extern DestReceiver *CreateSQLFunctionDestReceiver(void); + #endif /* FUNCTIONS_H */ diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h index 8164de6a083..3fe16bd0983 100644 --- a/src/include/nodes/execnodes.h +++ b/src/include/nodes/execnodes.h @@ -7,7 +7,7 @@ * Portions Copyright (c) 1996-2008, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * - * $PostgreSQL: pgsql/src/include/nodes/execnodes.h,v 1.193 2008/10/29 00:00:39 tgl Exp $ + * $PostgreSQL: pgsql/src/include/nodes/execnodes.h,v 1.194 2008/10/31 19:37:56 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -151,13 +151,15 @@ typedef enum /* * Return modes for functions returning sets. Note values must be chosen * as separate bits so that a bitmask can be formed to indicate supported - * modes. + * modes. SFRM_Materialize_Random and SFRM_Materialize_Preferred are + * auxiliary flags about SFRM_Materialize mode, rather than separate modes. */ typedef enum { SFRM_ValuePerCall = 0x01, /* one value returned per call */ SFRM_Materialize = 0x02, /* result set instantiated in Tuplestore */ - SFRM_Materialize_Random = 0x04 /* Tuplestore needs randomAccess */ + SFRM_Materialize_Random = 0x04, /* Tuplestore needs randomAccess */ + SFRM_Materialize_Preferred = 0x08 /* caller prefers Tuplestore */ } SetFunctionReturnMode; /* diff --git a/src/include/tcop/dest.h b/src/include/tcop/dest.h index 7484b84f767..5522f78c68b 100644 --- a/src/include/tcop/dest.h +++ b/src/include/tcop/dest.h @@ -54,7 +54,7 @@ * Portions Copyright (c) 1996-2008, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * - * $PostgreSQL: pgsql/src/include/tcop/dest.h,v 1.54 2008/01/01 19:45:59 momjian Exp $ + * $PostgreSQL: pgsql/src/include/tcop/dest.h,v 1.55 2008/10/31 19:37:56 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -86,7 +86,8 @@ typedef enum DestSPI, /* results sent to SPI manager */ DestTuplestore, /* results sent to Tuplestore */ DestIntoRel, /* results sent to relation (SELECT INTO) */ - DestCopyOut /* results sent to COPY TO code */ + DestCopyOut, /* results sent to COPY TO code */ + DestSQLFunction /* results sent to SQL-language func mgr */ } CommandDest; /* ---------------- diff --git a/src/test/regress/expected/rangefuncs.out b/src/test/regress/expected/rangefuncs.out index e8a9013550d..8b475834b93 100644 --- a/src/test/regress/expected/rangefuncs.out +++ b/src/test/regress/expected/rangefuncs.out @@ -567,3 +567,179 @@ SELECT * FROM foo(3); (9 rows) DROP FUNCTION foo(int); +-- +-- some tests on SQL functions with RETURNING +-- +create temp table tt(f1 serial, data text); +NOTICE: CREATE TABLE will create implicit sequence "tt_f1_seq" for serial column "tt.f1" +create function insert_tt(text) returns int as +$$ insert into tt(data) values($1) returning f1 $$ +language sql; +select insert_tt('foo'); + insert_tt +----------- + 1 +(1 row) + +select insert_tt('bar'); + insert_tt +----------- + 2 +(1 row) + +select * from tt; + f1 | data +----+------ + 1 | foo + 2 | bar +(2 rows) + +-- insert will execute to completion even if function needs just 1 row +create or replace function insert_tt(text) returns int as +$$ insert into tt(data) values($1),($1||$1) returning f1 $$ +language sql; +select insert_tt('fool'); + insert_tt +----------- + 3 +(1 row) + +select * from tt; + f1 | data +----+---------- + 1 | foo + 2 | bar + 3 | fool + 4 | foolfool +(4 rows) + +-- setof does what's expected +create or replace function insert_tt2(text,text) returns setof int as +$$ insert into tt(data) values($1),($2) returning f1 $$ +language sql; +select insert_tt2('foolish','barrish'); + insert_tt2 +------------ + 5 + 6 +(2 rows) + +select * from insert_tt2('baz','quux'); + insert_tt2 +------------ + 7 + 8 +(2 rows) + +select * from tt; + f1 | data +----+---------- + 1 | foo + 2 | bar + 3 | fool + 4 | foolfool + 5 | foolish + 6 | barrish + 7 | baz + 8 | quux +(8 rows) + +-- limit doesn't prevent execution to completion +select insert_tt2('foolish','barrish') limit 1; + insert_tt2 +------------ + 9 +(1 row) + +select * from tt; + f1 | data +----+---------- + 1 | foo + 2 | bar + 3 | fool + 4 | foolfool + 5 | foolish + 6 | barrish + 7 | baz + 8 | quux + 9 | foolish + 10 | barrish +(10 rows) + +-- triggers will fire, too +create function noticetrigger() returns trigger as $$ +begin + raise notice 'noticetrigger % %', new.f1, new.data; + return null; +end $$ language plpgsql; +create trigger tnoticetrigger after insert on tt for each row +execute procedure noticetrigger(); +select insert_tt2('foolme','barme') limit 1; +NOTICE: noticetrigger 11 foolme +CONTEXT: SQL function "insert_tt2" statement 1 +NOTICE: noticetrigger 12 barme +CONTEXT: SQL function "insert_tt2" statement 1 + insert_tt2 +------------ + 11 +(1 row) + +select * from tt; + f1 | data +----+---------- + 1 | foo + 2 | bar + 3 | fool + 4 | foolfool + 5 | foolish + 6 | barrish + 7 | baz + 8 | quux + 9 | foolish + 10 | barrish + 11 | foolme + 12 | barme +(12 rows) + +-- and rules work +create temp table tt_log(f1 int, data text); +create rule insert_tt_rule as on insert to tt do also + insert into tt_log values(new.*); +select insert_tt2('foollog','barlog') limit 1; +NOTICE: noticetrigger 13 foollog +CONTEXT: SQL function "insert_tt2" statement 1 +NOTICE: noticetrigger 14 barlog +CONTEXT: SQL function "insert_tt2" statement 1 + insert_tt2 +------------ + 13 +(1 row) + +select * from tt; + f1 | data +----+---------- + 1 | foo + 2 | bar + 3 | fool + 4 | foolfool + 5 | foolish + 6 | barrish + 7 | baz + 8 | quux + 9 | foolish + 10 | barrish + 11 | foolme + 12 | barme + 13 | foollog + 14 | barlog +(14 rows) + +-- note that nextval() gets executed a second time in the rule expansion, +-- which is expected. +select * from tt_log; + f1 | data +----+--------- + 15 | foollog + 16 | barlog +(2 rows) + diff --git a/src/test/regress/output/create_function_1.source b/src/test/regress/output/create_function_1.source index d2f2843a45c..61b87ed953a 100644 --- a/src/test/regress/output/create_function_1.source +++ b/src/test/regress/output/create_function_1.source @@ -61,7 +61,7 @@ LINE 2: AS 'not even SQL'; CREATE FUNCTION test1 (int) RETURNS int LANGUAGE SQL AS 'SELECT 1, 2, 3;'; ERROR: return type mismatch in function declared to return integer -DETAIL: Final SELECT must return exactly one column. +DETAIL: Final statement must return exactly one column. CONTEXT: SQL function "test1" CREATE FUNCTION test1 (int) RETURNS int LANGUAGE SQL AS 'SELECT $2;'; diff --git a/src/test/regress/sql/rangefuncs.sql b/src/test/regress/sql/rangefuncs.sql index 435a836c66d..6d10c99aab8 100644 --- a/src/test/regress/sql/rangefuncs.sql +++ b/src/test/regress/sql/rangefuncs.sql @@ -279,3 +279,62 @@ AS $$ SELECT a, b generate_series(1,$1) b(b) $$ LANGUAGE sql; SELECT * FROM foo(3); DROP FUNCTION foo(int); + +-- +-- some tests on SQL functions with RETURNING +-- + +create temp table tt(f1 serial, data text); + +create function insert_tt(text) returns int as +$$ insert into tt(data) values($1) returning f1 $$ +language sql; + +select insert_tt('foo'); +select insert_tt('bar'); +select * from tt; + +-- insert will execute to completion even if function needs just 1 row +create or replace function insert_tt(text) returns int as +$$ insert into tt(data) values($1),($1||$1) returning f1 $$ +language sql; + +select insert_tt('fool'); +select * from tt; + +-- setof does what's expected +create or replace function insert_tt2(text,text) returns setof int as +$$ insert into tt(data) values($1),($2) returning f1 $$ +language sql; + +select insert_tt2('foolish','barrish'); +select * from insert_tt2('baz','quux'); +select * from tt; + +-- limit doesn't prevent execution to completion +select insert_tt2('foolish','barrish') limit 1; +select * from tt; + +-- triggers will fire, too +create function noticetrigger() returns trigger as $$ +begin + raise notice 'noticetrigger % %', new.f1, new.data; + return null; +end $$ language plpgsql; +create trigger tnoticetrigger after insert on tt for each row +execute procedure noticetrigger(); + +select insert_tt2('foolme','barme') limit 1; +select * from tt; + +-- and rules work +create temp table tt_log(f1 int, data text); + +create rule insert_tt_rule as on insert to tt do also + insert into tt_log values(new.*); + +select insert_tt2('foollog','barlog') limit 1; +select * from tt; +-- note that nextval() gets executed a second time in the rule expansion, +-- which is expected. +select * from tt_log; |