postgresql/src/backend/executor/functions.c
Tom Lane b2c4071299 Redesign query-snapshot timing so that volatile functions in READ COMMITTED
mode see a fresh snapshot for each command in the function, rather than
using the latest interactive command's snapshot.  Also, suppress fresh
snapshots as well as CommandCounterIncrement inside STABLE and IMMUTABLE
functions, instead using the snapshot taken for the most closely nested
regular query.  (This behavior is only sane for read-only functions, so
the patch also enforces that such functions contain only SELECT commands.)
As per my proposal of 6-Sep-2004; I note that I floated essentially the
same proposal on 19-Jun-2002, but that discussion tailed off without any
action.  Since 8.0 seems like the right place to be taking possibly
nontrivial backwards compatibility hits, let's get it done now.
2004-09-13 20:10:13 +00:00

811 lines
20 KiB
C

/*-------------------------------------------------------------------------
*
* functions.c
* Execution of SQL-language functions
*
* Portions Copyright (c) 1996-2004, PostgreSQL Global Development Group
* Portions Copyright (c) 1994, Regents of the University of California
*
*
* IDENTIFICATION
* $PostgreSQL: pgsql/src/backend/executor/functions.c,v 1.89 2004/09/13 20:06:46 tgl Exp $
*
*-------------------------------------------------------------------------
*/
#include "postgres.h"
#include "access/heapam.h"
#include "catalog/pg_proc.h"
#include "catalog/pg_type.h"
#include "commands/trigger.h"
#include "executor/execdefs.h"
#include "executor/executor.h"
#include "executor/functions.h"
#include "tcop/pquery.h"
#include "tcop/tcopprot.h"
#include "tcop/utility.h"
#include "utils/builtins.h"
#include "utils/datum.h"
#include "utils/lsyscache.h"
#include "utils/syscache.h"
#include "utils/typcache.h"
/*
* We have an execution_state record for each query in a function. Each
* record contains a querytree and plantree for its query. If the query
* is currently in F_EXEC_RUN state then there's a QueryDesc too.
*/
typedef enum
{
F_EXEC_START, F_EXEC_RUN, F_EXEC_DONE
} ExecStatus;
typedef struct local_es
{
struct local_es *next;
ExecStatus status;
Query *query;
Plan *plan;
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.
*/
typedef struct
{
Oid *argtypes; /* resolved types of arguments */
Oid rettype; /* actual return type */
int typlen; /* length of the return type */
bool typbyval; /* true if return type is pass by value */
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 */
ParamListInfo paramLI; /* Param list representing current args */
/* head of linked list of execution_state records */
execution_state *func_state;
} SQLFunctionCache;
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);
static void postquel_start(execution_state *es, SQLFunctionCachePtr fcache);
static TupleTableSlot *postquel_getnext(execution_state *es);
static void postquel_end(execution_state *es, SQLFunctionCachePtr fcache);
static void postquel_sub_params(SQLFunctionCachePtr fcache,
FunctionCallInfo fcinfo);
static Datum postquel_execute(execution_state *es,
FunctionCallInfo fcinfo,
SQLFunctionCachePtr fcache);
static void sql_exec_error_callback(void *arg);
static void ShutdownSQLFunction(Datum arg);
static execution_state *
init_execution_state(List *queryTree_list, bool readonly_func)
{
execution_state *firstes = NULL;
execution_state *preves = NULL;
ListCell *qtl_item;
foreach(qtl_item, queryTree_list)
{
Query *queryTree = lfirst(qtl_item);
Plan *planTree;
execution_state *newes;
/* Precheck all commands for validity in a function */
if (queryTree->commandType == CMD_UTILITY &&
IsA(queryTree->utilityStmt, TransactionStmt))
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
/* translator: %s is a SQL statement name */
errmsg("%s is not allowed in a SQL function",
CreateQueryTag(queryTree))));
if (readonly_func && !QueryIsReadOnly(queryTree))
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
/* translator: %s is a SQL statement name */
errmsg("%s is not allowed in a non-volatile function",
CreateQueryTag(queryTree))));
planTree = pg_plan_query(queryTree, NULL);
newes = (execution_state *) palloc(sizeof(execution_state));
if (preves)
preves->next = newes;
else
firstes = newes;
newes->next = NULL;
newes->status = F_EXEC_START;
newes->query = queryTree;
newes->plan = planTree;
newes->qd = NULL;
preves = newes;
}
return firstes;
}
static void
init_sql_fcache(FmgrInfo *finfo)
{
Oid foid = finfo->fn_oid;
Oid rettype;
HeapTuple procedureTuple;
HeapTuple typeTuple;
Form_pg_proc procedureStruct;
Form_pg_type typeStruct;
SQLFunctionCachePtr fcache;
Oid *argOidVect;
bool haspolyarg;
char *src;
int nargs;
List *queryTree_list;
Datum tmp;
bool isNull;
fcache = (SQLFunctionCachePtr) palloc0(sizeof(SQLFunctionCache));
/*
* get the procedure tuple corresponding to the given function Oid
*/
procedureTuple = SearchSysCache(PROCOID,
ObjectIdGetDatum(foid),
0, 0, 0);
if (!HeapTupleIsValid(procedureTuple))
elog(ERROR, "cache lookup failed for function %u", foid);
procedureStruct = (Form_pg_proc) GETSTRUCT(procedureTuple);
/*
* get the result type from the procedure tuple, and check for
* polymorphic result type; if so, find out the actual result type.
*/
rettype = procedureStruct->prorettype;
if (rettype == ANYARRAYOID || rettype == ANYELEMENTOID)
{
rettype = get_fn_expr_rettype(finfo);
if (rettype == InvalidOid) /* this probably should not happen */
ereport(ERROR,
(errcode(ERRCODE_DATATYPE_MISMATCH),
errmsg("could not determine actual result type for function declared to return type %s",
format_type_be(procedureStruct->prorettype))));
}
fcache->rettype = rettype;
/* Remember if function is STABLE/IMMUTABLE */
fcache->readonly_func =
(procedureStruct->provolatile != PROVOLATILE_VOLATILE);
/* Now look up the actual result type */
typeTuple = SearchSysCache(TYPEOID,
ObjectIdGetDatum(rettype),
0, 0, 0);
if (!HeapTupleIsValid(typeTuple))
elog(ERROR, "cache lookup failed for type %u", rettype);
typeStruct = (Form_pg_type) GETSTRUCT(typeTuple);
/*
* get the type length and by-value flag from the type tuple; also do
* a preliminary check for returnsTuple (this may prove inaccurate,
* see below).
*/
fcache->typlen = typeStruct->typlen;
fcache->typbyval = typeStruct->typbyval;
fcache->returnsTuple = (typeStruct->typtype == 'c' ||
rettype == RECORDOID);
/*
* Parse and rewrite the queries. We need the argument type info to
* pass to the parser.
*/
nargs = procedureStruct->pronargs;
haspolyarg = false;
if (nargs > 0)
{
int argnum;
argOidVect = (Oid *) palloc(nargs * sizeof(Oid));
memcpy(argOidVect,
procedureStruct->proargtypes,
nargs * sizeof(Oid));
/* Resolve any polymorphic argument types */
for (argnum = 0; argnum < nargs; argnum++)
{
Oid argtype = argOidVect[argnum];
if (argtype == ANYARRAYOID || argtype == ANYELEMENTOID)
{
argtype = get_fn_expr_argtype(finfo, argnum);
if (argtype == InvalidOid)
ereport(ERROR,
(errcode(ERRCODE_DATATYPE_MISMATCH),
errmsg("could not determine actual type of argument declared %s",
format_type_be(argOidVect[argnum]))));
argOidVect[argnum] = argtype;
haspolyarg = true;
}
}
}
else
argOidVect = NULL;
fcache->argtypes = argOidVect;
tmp = SysCacheGetAttr(PROCOID,
procedureTuple,
Anum_pg_proc_prosrc,
&isNull);
if (isNull)
elog(ERROR, "null prosrc for function %u", foid);
src = DatumGetCString(DirectFunctionCall1(textout, tmp));
queryTree_list = pg_parse_and_rewrite(src, argOidVect, nargs);
/*
* If the function has any arguments declared as polymorphic types,
* then it wasn't type-checked at definition time; must do so now.
*
* Also, force a type-check if the declared return type is a rowtype; we
* need to find out whether we are actually returning the whole tuple
* result, or just regurgitating a rowtype expression result. In the
* latter case we clear returnsTuple because we need not act different
* from the scalar result case.
*/
if (haspolyarg || fcache->returnsTuple)
fcache->returnsTuple = check_sql_fn_retval(rettype,
get_typtype(rettype),
queryTree_list);
/* Finally, plan the queries */
fcache->func_state = init_execution_state(queryTree_list,
fcache->readonly_func);
pfree(src);
ReleaseSysCache(typeTuple);
ReleaseSysCache(procedureTuple);
finfo->fn_extra = (void *) fcache;
}
static void
postquel_start(execution_state *es, SQLFunctionCachePtr fcache)
{
Snapshot snapshot;
Assert(es->qd == NULL);
/*
* In a read-only function, use the surrounding query's snapshot;
* otherwise take a new snapshot for each query. The snapshot should
* include a fresh command ID so that all work to date in this
* transaction is visible. We copy in both cases so that postquel_end
* can unconditionally do FreeSnapshot.
*/
if (fcache->readonly_func)
snapshot = CopySnapshot(ActiveSnapshot);
else
{
CommandCounterIncrement();
snapshot = CopySnapshot(GetTransactionSnapshot());
}
es->qd = CreateQueryDesc(es->query, es->plan,
snapshot, InvalidSnapshot,
None_Receiver,
fcache->paramLI, false);
/* We assume we don't need to set up ActiveSnapshot for ExecutorStart */
/* Utility commands don't need Executor. */
if (es->qd->operation != CMD_UTILITY)
{
AfterTriggerBeginQuery();
ExecutorStart(es->qd, false);
}
es->status = F_EXEC_RUN;
}
static TupleTableSlot *
postquel_getnext(execution_state *es)
{
TupleTableSlot *result;
Snapshot saveActiveSnapshot;
long count;
/* Make our snapshot the active one for any called functions */
saveActiveSnapshot = ActiveSnapshot;
PG_TRY();
{
ActiveSnapshot = es->qd->snapshot;
if (es->qd->operation == CMD_UTILITY)
{
ProcessUtility(es->qd->parsetree->utilityStmt, es->qd->params,
es->qd->dest, NULL);
result = NULL;
}
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)
count = 1L;
else
count = 0L;
result = ExecutorRun(es->qd, ForwardScanDirection, count);
}
}
PG_CATCH();
{
/* Restore global vars and propagate error */
ActiveSnapshot = saveActiveSnapshot;
PG_RE_THROW();
}
PG_END_TRY();
ActiveSnapshot = saveActiveSnapshot;
return result;
}
static void
postquel_end(execution_state *es, SQLFunctionCachePtr fcache)
{
Snapshot saveActiveSnapshot;
/* mark status done to ensure we don't do ExecutorEnd twice */
es->status = F_EXEC_DONE;
/* Utility commands don't need Executor. */
if (es->qd->operation != CMD_UTILITY)
{
/* Make our snapshot the active one for any called functions */
saveActiveSnapshot = ActiveSnapshot;
PG_TRY();
{
ActiveSnapshot = es->qd->snapshot;
ExecutorEnd(es->qd);
AfterTriggerEndQuery();
}
PG_CATCH();
{
/* Restore global vars and propagate error */
ActiveSnapshot = saveActiveSnapshot;
PG_RE_THROW();
}
PG_END_TRY();
ActiveSnapshot = saveActiveSnapshot;
}
FreeSnapshot(es->qd->snapshot);
FreeQueryDesc(es->qd);
es->qd = NULL;
}
/* Build ParamListInfo array representing current arguments */
static void
postquel_sub_params(SQLFunctionCachePtr fcache,
FunctionCallInfo fcinfo)
{
ParamListInfo paramLI;
int nargs = fcinfo->nargs;
if (nargs > 0)
{
int i;
paramLI = (ParamListInfo) palloc0((nargs + 1) * sizeof(ParamListInfoData));
for (i = 0; i < nargs; i++)
{
paramLI[i].kind = PARAM_NUM;
paramLI[i].id = i + 1;
paramLI[i].ptype = fcache->argtypes[i];
paramLI[i].value = fcinfo->arg[i];
paramLI[i].isnull = fcinfo->argnull[i];
}
paramLI[nargs].kind = PARAM_INVALID;
}
else
paramLI = NULL;
if (fcache->paramLI)
pfree(fcache->paramLI);
fcache->paramLI = paramLI;
}
static Datum
postquel_execute(execution_state *es,
FunctionCallInfo fcinfo,
SQLFunctionCachePtr fcache)
{
TupleTableSlot *slot;
HeapTuple tup;
TupleDesc tupDesc;
Datum value;
if (es->status == F_EXEC_START)
postquel_start(es, fcache);
slot = postquel_getnext(es);
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, fcache);
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.
*/
tup = slot->val;
tupDesc = slot->ttc_tupleDescriptor;
if (fcache->returnsTuple)
{
/*
* We are returning the whole tuple, so copy it into current
* execution context and make sure it is a valid Datum.
*
* XXX do we need to remove junk attrs from the result tuple?
* Probably OK to leave them, as long as they are at the end.
*/
HeapTupleHeader dtup;
Oid dtuptype;
int32 dtuptypmod;
dtup = (HeapTupleHeader) palloc(tup->t_len);
memcpy((char *) dtup, (char *) tup->t_data, tup->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 */
if (tupDesc->tdtypeid == RECORDOID &&
tupDesc->tdtypmod < 0)
assign_record_type_typmod(tupDesc);
dtuptype = tupDesc->tdtypeid;
dtuptypmod = tupDesc->tdtypmod;
}
HeapTupleHeaderSetDatumLength(dtup, tup->t_len);
HeapTupleHeaderSetTypeId(dtup, dtuptype);
HeapTupleHeaderSetTypMod(dtup, dtuptypmod);
value = PointerGetDatum(dtup);
fcinfo->isnull = false;
}
else
{
/*
* Returning a scalar, which we have to extract from the first
* column of the SELECT result, and then copy into current
* execution context if needed.
*/
value = heap_getattr(tup, 1, tupDesc, &(fcinfo->isnull));
if (!fcinfo->isnull)
value = datumCopy(value, fcache->typbyval, fcache->typlen);
}
/*
* If this is a single valued function we have to end the function
* execution now.
*/
if (!fcinfo->flinfo->fn_retset)
postquel_end(es, fcache);
return value;
}
Datum
fmgr_sql(PG_FUNCTION_ARGS)
{
MemoryContext oldcontext;
SQLFunctionCachePtr fcache;
ErrorContextCallback sqlerrcontext;
execution_state *es;
Datum result = 0;
/*
* Switch to context in which the fcache lives. This ensures that
* parsetrees, plans, etc, will have sufficient lifetime. The
* sub-executor is responsible for deleting per-tuple information.
*/
oldcontext = MemoryContextSwitchTo(fcinfo->flinfo->fn_mcxt);
/*
* Setup error traceback support for ereport()
*/
sqlerrcontext.callback = sql_exec_error_callback;
sqlerrcontext.arg = fcinfo->flinfo;
sqlerrcontext.previous = error_context_stack;
error_context_stack = &sqlerrcontext;
/*
* Initialize fcache (build plans) if first time through.
*/
fcache = (SQLFunctionCachePtr) fcinfo->flinfo->fn_extra;
if (fcache == NULL)
{
init_sql_fcache(fcinfo->flinfo);
fcache = (SQLFunctionCachePtr) fcinfo->flinfo->fn_extra;
}
es = fcache->func_state;
/*
* Convert params to appropriate format if starting a fresh execution.
* (If continuing execution, we can re-use prior params.)
*/
if (es && es->status == F_EXEC_START)
postquel_sub_params(fcache, fcinfo);
/*
* 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.
*/
while (es)
{
result = postquel_execute(es, fcinfo, fcache);
if (es->status != F_EXEC_DONE)
break;
es = es->next;
}
/*
* If we've gone through every command in this function, we are done.
*/
if (es == NULL)
{
/*
* Reset the execution states to start over again on next call.
*/
es = fcache->func_state;
while (es)
{
es->status = F_EXEC_START;
es = es->next;
}
/*
* Let caller know we're finished.
*/
if (fcinfo->flinfo->fn_retset)
{
ReturnSetInfo *rsi = (ReturnSetInfo *) fcinfo->resultinfo;
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;
/* Deregister shutdown callback, if we made one */
if (fcache->shutdown_reg)
{
UnregisterExprContextCallback(rsi->econtext,
ShutdownSQLFunction,
PointerGetDatum(fcache));
fcache->shutdown_reg = false;
}
}
error_context_stack = sqlerrcontext.previous;
MemoryContextSwitchTo(oldcontext);
return result;
}
/*
* 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));
/*
* Let caller know we're not finished.
*/
if (fcinfo->flinfo->fn_retset)
{
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)
{
RegisterExprContextCallback(rsi->econtext,
ShutdownSQLFunction,
PointerGetDatum(fcache));
fcache->shutdown_reg = true;
}
}
error_context_stack = sqlerrcontext.previous;
MemoryContextSwitchTo(oldcontext);
return result;
}
/*
* error context callback to let us supply a call-stack traceback
*/
static void
sql_exec_error_callback(void *arg)
{
FmgrInfo *flinfo = (FmgrInfo *) arg;
SQLFunctionCachePtr fcache = (SQLFunctionCachePtr) flinfo->fn_extra;
HeapTuple func_tuple;
Form_pg_proc functup;
char *fn_name;
int syntaxerrposition;
/* Need access to function's pg_proc tuple */
func_tuple = SearchSysCache(PROCOID,
ObjectIdGetDatum(flinfo->fn_oid),
0, 0, 0);
if (!HeapTupleIsValid(func_tuple))
return; /* shouldn't happen */
functup = (Form_pg_proc) GETSTRUCT(func_tuple);
fn_name = NameStr(functup->proname);
/*
* If there is a syntax error position, convert to internal syntax
* error
*/
syntaxerrposition = geterrposition();
if (syntaxerrposition > 0)
{
bool isnull;
Datum tmp;
char *prosrc;
tmp = SysCacheGetAttr(PROCOID, func_tuple, Anum_pg_proc_prosrc,
&isnull);
if (isnull)
elog(ERROR, "null prosrc");
prosrc = DatumGetCString(DirectFunctionCall1(textout, tmp));
errposition(0);
internalerrposition(syntaxerrposition);
internalerrquery(prosrc);
pfree(prosrc);
}
/*
* Try to determine where in the function we failed. If there is a
* query with non-null QueryDesc, finger it. (We check this rather
* than looking for F_EXEC_RUN state, so that errors during
* ExecutorStart or ExecutorEnd are blamed on the appropriate query;
* see postquel_start and postquel_end.)
*/
if (fcache)
{
execution_state *es;
int query_num;
es = fcache->func_state;
query_num = 1;
while (es)
{
if (es->qd)
{
errcontext("SQL function \"%s\" statement %d",
fn_name, query_num);
break;
}
es = es->next;
query_num++;
}
if (es == NULL)
{
/*
* couldn't identify a running query; might be function entry,
* function exit, or between queries.
*/
errcontext("SQL function \"%s\"", fn_name);
}
}
else
{
/* must have failed during init_sql_fcache() */
errcontext("SQL function \"%s\" during startup", fn_name);
}
ReleaseSysCache(func_tuple);
}
/*
* callback function in case a function-returning-set needs to be shut down
* before it has been run to completion
*/
static void
ShutdownSQLFunction(Datum arg)
{
SQLFunctionCachePtr fcache = (SQLFunctionCachePtr) DatumGetPointer(arg);
execution_state *es = fcache->func_state;
while (es != NULL)
{
/* Shut down anything still running */
if (es->status == F_EXEC_RUN)
postquel_end(es, fcache);
/* Reset states to START in case we're called again */
es->status = F_EXEC_START;
es = es->next;
}
/* execUtils will deregister the callback... */
fcache->shutdown_reg = false;
}