diff options
author | Tom Lane <tgl@sss.pgh.pa.us> | 2025-04-02 14:05:50 -0400 |
---|---|---|
committer | Tom Lane <tgl@sss.pgh.pa.us> | 2025-04-02 14:06:02 -0400 |
commit | 0dca5d68d7bebf2c1036fd84875533afef6df992 (patch) | |
tree | e9f713a5387a9782a8c2dddc54b461112f112ef0 /src/backend/executor/functions.c | |
parent | e9e7b66044c9e3dfa76fd1599d5703acd3e4a3f5 (diff) | |
download | postgresql-0dca5d68d7bebf2c1036fd84875533afef6df992.tar.gz postgresql-0dca5d68d7bebf2c1036fd84875533afef6df992.zip |
Change SQL-language functions to use the plan cache.
In the historical implementation of SQL functions (if they don't get
inlined), we built plans for all the contained queries at first call
within an outer query, and then re-used those plans for the duration
of the outer query, and then forgot everything. This was not ideal,
not least because the plans could not be customized to specific values
of the function's parameters. Our plancache infrastructure seems
mature enough to be used here. That will solve both the problem with
not being able to build custom plans and the problem with not being
able to share work across successive outer queries.
Aside from those performance concerns, this change fixes a
longstanding bugaboo with SQL functions: you could not write DDL that
would affect later statements in the same function. That's mostly
still true with new-style SQL functions, since the results of parse
analysis are baked into the stored query trees (and protected by
dependency records). But for old-style SQL functions, it will now
work much as it does with PL/pgSQL functions, because we delay parse
analysis and planning of each query until we're ready to run it.
Some edge cases that require replanning are now handled better too;
see for example the new rowsecurity test, where we now detect an RLS
context change that was previously missed.
One other edge-case change that might be worthy of a release note
is that we now insist that a SQL function's result be generated
by the physically-last query within it. Previously, if the last
original query was deleted by a DO INSTEAD NOTHING rule, we'd be
willing to take the result from the preceding query instead.
This behavior was undocumented except in source-code comments,
and it seems hard to believe that anyone's relying on it.
Along the way to this feature, we needed a few infrastructure changes:
* The plancache can now take either a raw parse tree or an
analyzed-but-not-rewritten Query as the starting point for a
CachedPlanSource. If given a Query, it is caller's responsibility
that nothing will happen to invalidate that form of the query.
We use this for new-style SQL functions, where what's in pg_proc is
serialized Query(s) and we trust the dependency mechanism to disallow
DDL that would break those.
* The plancache now offers a way to invoke a post-rewrite callback
to examine/modify the rewritten parse tree when it is rebuilding
the parse trees after a cache invalidation. We need this because
SQL functions sometimes adjust the parse tree to make its output
exactly match the declared result type; if the plan gets rebuilt,
that has to be re-done.
* There is a new backend module utils/cache/funccache.c that
abstracts the idea of caching data about a specific function
usage (a particular function and set of input data types).
The code in it is moved almost verbatim from PL/pgSQL, which
has done that for a long time. We use that logic now for
SQL-language functions too, and maybe other PLs will have use
for it in the future.
Author: Alexander Pyhalov <a.pyhalov@postgrespro.ru>
Co-authored-by: Tom Lane <tgl@sss.pgh.pa.us>
Reviewed-by: Pavel Stehule <pavel.stehule@gmail.com>
Discussion: https://postgr.es/m/8216639.NyiUUSuA9g@aivenlaptop
Diffstat (limited to 'src/backend/executor/functions.c')
-rw-r--r-- | src/backend/executor/functions.c | 1391 |
1 files changed, 911 insertions, 480 deletions
diff --git a/src/backend/executor/functions.c b/src/backend/executor/functions.c index 6aa8e9c4d8a..5509013521d 100644 --- a/src/backend/executor/functions.c +++ b/src/backend/executor/functions.c @@ -31,6 +31,7 @@ #include "tcop/utility.h" #include "utils/builtins.h" #include "utils/datum.h" +#include "utils/funccache.h" #include "utils/lsyscache.h" #include "utils/memutils.h" #include "utils/snapmgr.h" @@ -50,7 +51,7 @@ typedef struct /* * 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 + * record references a plantree for its query. If the query is currently in * F_EXEC_RUN state then there's a QueryDesc too. * * The "next" fields chain together all the execution_state records generated @@ -74,24 +75,49 @@ typedef struct execution_state /* - * An SQLFunctionCache record is built during the first call, - * and linked to from the fn_extra field of the FmgrInfo struct. + * Data associated with a SQL-language function is kept in three main + * data structures: * - * Note that currently this has only the lifespan of the calling query. - * Someday we should rewrite this code to use plancache.c to save parse/plan - * results for longer than that. + * 1. SQLFunctionHashEntry is a long-lived (potentially session-lifespan) + * struct that holds all the info we need out of the function's pg_proc row. + * In addition it holds pointers to CachedPlanSource(s) that manage creation + * of plans for the query(s) within the function. A SQLFunctionHashEntry is + * potentially shared across multiple concurrent executions of the function, + * so it must contain no execution-specific state; but its use_count must + * reflect the number of SQLFunctionLink structs pointing at it. + * If the function's pg_proc row is updated, we throw away and regenerate + * the SQLFunctionHashEntry and subsidiary data. (Also note that if the + * function is polymorphic or used as a trigger, there is a separate + * SQLFunctionHashEntry for each usage, so that we need consider only one + * set of relevant data types.) The struct itself is in memory managed by + * funccache.c, and its subsidiary data is kept in one of two contexts: + * * pcontext ("parse context") holds the raw parse trees or Query trees + * that we read from the pg_proc row. These will be converted to + * CachedPlanSources as they are needed. Once the last one is converted, + * pcontext can be freed. + * * hcontext ("hash context") holds everything else belonging to the + * SQLFunctionHashEntry. * - * Physically, though, the data has the lifespan of the FmgrInfo that's used - * to call the function, and there are cases (particularly with indexes) - * where the FmgrInfo might survive across transactions. We cannot assume - * that the parse/plan trees are good for longer than the (sub)transaction in - * which parsing was done, so we must mark the record with the LXID/subxid of - * its creation time, and regenerate everything if that's obsolete. To avoid - * memory leakage when we do have to regenerate things, all the data is kept - * in a sub-context of the FmgrInfo's fn_mcxt. + * 2. SQLFunctionCache lasts for the duration of a single execution of + * the SQL function. (In "lazyEval" mode, this might span multiple calls of + * fmgr_sql.) It holds a reference to the CachedPlan for the current query, + * and other data that is execution-specific. The SQLFunctionCache itself + * as well as its subsidiary data are kept in fcontext ("function context"), + * which we free at completion. In non-returnsSet mode, this is just a child + * of the call-time context. In returnsSet mode, it is made a child of the + * FmgrInfo's fn_mcxt so that it will survive between fmgr_sql calls. + * + * 3. SQLFunctionLink is a tiny struct that just holds pointers to + * the SQLFunctionHashEntry and the current SQLFunctionCache (if any). + * It is pointed to by the fn_extra field of the FmgrInfo struct, and is + * always allocated in the FmgrInfo's fn_mcxt. Its purpose is to reduce + * the cost of repeat lookups of the SQLFunctionHashEntry. */ -typedef struct + +typedef struct SQLFunctionHashEntry { + CachedFunction cfunc; /* fields managed by funccache.c */ + char *fname; /* function name (for error msgs) */ char *src; /* function body text (for error msgs) */ @@ -102,8 +128,27 @@ typedef struct 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 */ + char prokind; /* prokind from pg_proc row */ + + TupleDesc rettupdesc; /* result tuple descriptor */ + + List *source_list; /* RawStmts or Queries read from pg_proc */ + int num_queries; /* original length of source_list */ + bool raw_source; /* true if source_list contains RawStmts */ + + List *plansource_list; /* CachedPlanSources for fn's queries */ + + MemoryContext pcontext; /* memory context holding source_list */ + MemoryContext hcontext; /* memory context holding all else */ +} SQLFunctionHashEntry; + +typedef struct SQLFunctionCache +{ + SQLFunctionHashEntry *func; /* associated SQLFunctionHashEntry */ + + bool lazyEvalOK; /* true if lazyEval is safe */ + bool shutdown_reg; /* true if registered shutdown callback */ bool lazyEval; /* true if using lazyEval for result query */ ParamListInfo paramLI; /* Param list representing current args */ @@ -113,22 +158,39 @@ typedef struct JunkFilter *junkFilter; /* will be NULL if function returns VOID */ /* - * func_state is a List of execution_state records, each of which is the - * first for its original parsetree, with any additional records chained - * to it via the "next" fields. This sublist structure is needed to keep - * track of where the original query boundaries are. + * While executing a particular query within the function, cplan is the + * CachedPlan we've obtained for that query, and eslist is a list of + * execution_state records for the individual plans within the CachedPlan. + * next_query_index is the 0-based index of the next CachedPlanSource to + * get a CachedPlan from. */ - List *func_state; + CachedPlan *cplan; /* Plan for current query, if any */ + ResourceOwner cowner; /* CachedPlan is registered with this owner */ + execution_state *eslist; /* execution_state records */ + int next_query_index; /* index of next CachedPlanSource to run */ + + /* if positive, this is the index of the query we're processing */ + int error_query_index; MemoryContext fcontext; /* memory context holding this struct and all * subsidiary data */ - - LocalTransactionId lxid; /* lxid in which cache was made */ - SubTransactionId subxid; /* subxid in which cache was made */ } SQLFunctionCache; typedef SQLFunctionCache *SQLFunctionCachePtr; +/* Struct pointed to by FmgrInfo.fn_extra for a SQL function */ +typedef struct SQLFunctionLink +{ + /* Permanent pointer to associated SQLFunctionHashEntry */ + SQLFunctionHashEntry *func; + + /* Transient pointer to SQLFunctionCache, used only if returnsSet */ + SQLFunctionCache *fcache; + + /* Callback to release our use-count on the SQLFunctionHashEntry */ + MemoryContextCallback mcb; +} SQLFunctionLink; + /* non-export function prototypes */ static Node *sql_fn_param_ref(ParseState *pstate, ParamRef *pref); @@ -138,10 +200,17 @@ static Node *sql_fn_make_param(SQLFunctionParseInfoPtr pinfo, int paramno, int location); static Node *sql_fn_resolve_param_name(SQLFunctionParseInfoPtr pinfo, const char *paramname, int location); -static List *init_execution_state(List *queryTree_list, - SQLFunctionCachePtr fcache, - bool lazyEvalOK); -static void init_sql_fcache(FunctionCallInfo fcinfo, Oid collation, bool lazyEvalOK); +static SQLFunctionCache *init_sql_fcache(FunctionCallInfo fcinfo, + bool lazyEvalOK); +static bool init_execution_state(SQLFunctionCachePtr fcache); +static void prepare_next_query(SQLFunctionHashEntry *func); +static void sql_compile_callback(FunctionCallInfo fcinfo, + HeapTuple procedureTuple, + const CachedFunctionHashKey *hashkey, + CachedFunction *cfunc, + bool forValidator); +static void sql_delete_callback(CachedFunction *cfunc); +static void sql_postrewrite_callback(List *querytree_list, void *arg); static void postquel_start(execution_state *es, SQLFunctionCachePtr fcache); static bool postquel_getnext(execution_state *es, SQLFunctionCachePtr fcache); static void postquel_end(execution_state *es); @@ -151,13 +220,20 @@ static Datum postquel_get_single_result(TupleTableSlot *slot, FunctionCallInfo fcinfo, SQLFunctionCachePtr fcache, MemoryContext resultcontext); +static void sql_compile_error_callback(void *arg); static void sql_exec_error_callback(void *arg); static void ShutdownSQLFunction(Datum arg); +static void RemoveSQLFunctionLink(void *arg); +static void check_sql_fn_statement(List *queryTreeList); +static bool check_sql_stmt_retval(List *queryTreeList, + Oid rettype, TupleDesc rettupdesc, + char prokind, bool insertDroppedCols); static bool coerce_fn_result_column(TargetEntry *src_tle, Oid res_type, int32 res_typmod, bool tlist_is_modifiable, List **upper_tlist, bool *upper_tlist_nontrivial); +static List *get_sql_fn_result_tlist(List *queryTreeList); static void sqlfunction_startup(DestReceiver *self, int operation, TupleDesc typeinfo); static bool sqlfunction_receive(TupleTableSlot *slot, DestReceiver *self); static void sqlfunction_shutdown(DestReceiver *self); @@ -455,99 +531,297 @@ sql_fn_resolve_param_name(SQLFunctionParseInfoPtr pinfo, } /* - * Set up the per-query execution_state records for a SQL function. + * Initialize the SQLFunctionCache for a SQL function + */ +static SQLFunctionCache * +init_sql_fcache(FunctionCallInfo fcinfo, bool lazyEvalOK) +{ + FmgrInfo *finfo = fcinfo->flinfo; + SQLFunctionHashEntry *func; + SQLFunctionCache *fcache; + SQLFunctionLink *flink; + MemoryContext pcontext; + MemoryContext fcontext; + MemoryContext oldcontext; + + /* + * If this is the first execution for this FmgrInfo, set up a link struct + * (initially containing null pointers). The link must live as long as + * the FmgrInfo, so it goes in fn_mcxt. Also set up a memory context + * callback that will be invoked when fn_mcxt is deleted. + */ + flink = finfo->fn_extra; + if (flink == NULL) + { + flink = (SQLFunctionLink *) + MemoryContextAllocZero(finfo->fn_mcxt, sizeof(SQLFunctionLink)); + flink->mcb.func = RemoveSQLFunctionLink; + flink->mcb.arg = flink; + MemoryContextRegisterResetCallback(finfo->fn_mcxt, &flink->mcb); + finfo->fn_extra = flink; + } + + /* + * If we are resuming execution of a set-returning function, just keep + * using the same cache. We do not ask funccache.c to re-validate the + * SQLFunctionHashEntry: we want to run to completion using the function's + * initial definition. + */ + if (flink->fcache != NULL) + { + Assert(flink->fcache->func == flink->func); + return flink->fcache; + } + + /* + * Look up, or re-validate, the long-lived hash entry. Make the hash key + * depend on the result of get_call_result_type() when that's composite, + * so that we can safely assume that we'll build a new hash entry if the + * composite rowtype changes. + */ + func = (SQLFunctionHashEntry *) + cached_function_compile(fcinfo, + (CachedFunction *) flink->func, + sql_compile_callback, + sql_delete_callback, + sizeof(SQLFunctionHashEntry), + true, + false); + + /* + * Install the hash pointer in the SQLFunctionLink, and increment its use + * count to reflect that. If cached_function_compile gave us back a + * different hash entry than we were using before, we must decrement that + * one's use count. + */ + if (func != flink->func) + { + if (flink->func != NULL) + { + Assert(flink->func->cfunc.use_count > 0); + flink->func->cfunc.use_count--; + } + flink->func = func; + func->cfunc.use_count++; + } + + /* + * Create memory context that holds all the SQLFunctionCache data. If we + * return a set, we must keep this in whatever context holds the FmgrInfo + * (anything shorter-lived risks leaving a dangling pointer in flink). But + * in a non-SRF we'll delete it before returning, and there's no need for + * it to outlive the caller's context. + */ + pcontext = func->returnsSet ? finfo->fn_mcxt : CurrentMemoryContext; + fcontext = AllocSetContextCreate(pcontext, + "SQL function execution", + ALLOCSET_DEFAULT_SIZES); + + oldcontext = MemoryContextSwitchTo(fcontext); + + /* + * Create the struct proper, link it to func and fcontext. + */ + fcache = (SQLFunctionCache *) palloc0(sizeof(SQLFunctionCache)); + fcache->func = func; + fcache->fcontext = fcontext; + fcache->lazyEvalOK = lazyEvalOK; + + /* + * If we return a set, we must link the fcache into fn_extra so that we + * can find it again during future calls. But in a non-SRF there is no + * need to link it into fn_extra at all. Not doing so removes the risk of + * having a dangling pointer in a long-lived FmgrInfo. + */ + if (func->returnsSet) + flink->fcache = fcache; + + /* + * We're beginning a new execution of the function, so convert params to + * appropriate format. + */ + postquel_sub_params(fcache, fcinfo); + + MemoryContextSwitchTo(oldcontext); + + return fcache; +} + +/* + * Set up the per-query execution_state records for the next query within + * the SQL function. * - * The input is a List of Lists of parsed and rewritten, but not planned, - * querytrees. The sublist structure denotes the original query boundaries. + * Returns true if successful, false if there are no more queries. */ -static List * -init_execution_state(List *queryTree_list, - SQLFunctionCachePtr fcache, - bool lazyEvalOK) +static bool +init_execution_state(SQLFunctionCachePtr fcache) { - List *eslist = NIL; + CachedPlanSource *plansource; + execution_state *preves = NULL; execution_state *lasttages = NULL; - ListCell *lc1; + ListCell *lc; - foreach(lc1, queryTree_list) + /* + * Clean up after previous query, if there was one. Note that we just + * leak the old execution_state records until end of function execution; + * there aren't likely to be enough of them to matter. + */ + if (fcache->cplan) { - List *qtlist = lfirst_node(List, lc1); - execution_state *firstes = NULL; - execution_state *preves = NULL; - ListCell *lc2; + ReleaseCachedPlan(fcache->cplan, fcache->cowner); + fcache->cplan = NULL; + } + fcache->eslist = NULL; - foreach(lc2, qtlist) - { - Query *queryTree = lfirst_node(Query, lc2); - PlannedStmt *stmt; - execution_state *newes; + /* + * Get the next CachedPlanSource, or stop if there are no more. We might + * need to create the next CachedPlanSource; if so, advance + * error_query_index first, so that errors detected in prepare_next_query + * are blamed on the right statement. + */ + if (fcache->next_query_index >= list_length(fcache->func->plansource_list)) + { + if (fcache->next_query_index >= fcache->func->num_queries) + return false; + fcache->error_query_index++; + prepare_next_query(fcache->func); + } + else + fcache->error_query_index++; - /* Plan the query if needed */ - if (queryTree->commandType == CMD_UTILITY) - { - /* Utility commands require no planning. */ - stmt = makeNode(PlannedStmt); - stmt->commandType = CMD_UTILITY; - stmt->canSetTag = queryTree->canSetTag; - stmt->utilityStmt = queryTree->utilityStmt; - stmt->stmt_location = queryTree->stmt_location; - stmt->stmt_len = queryTree->stmt_len; - stmt->queryId = queryTree->queryId; - } - else - stmt = pg_plan_query(queryTree, - fcache->src, - CURSOR_OPT_PARALLEL_OK, - NULL); + plansource = (CachedPlanSource *) list_nth(fcache->func->plansource_list, + fcache->next_query_index); + fcache->next_query_index++; - /* - * Precheck all commands for validity in a function. This should - * generally match the restrictions spi.c applies. - */ - if (stmt->commandType == CMD_UTILITY) - { - if (IsA(stmt->utilityStmt, CopyStmt) && - ((CopyStmt *) stmt->utilityStmt)->filename == NULL) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("cannot COPY to/from client in an SQL function"))); + /* + * Generate plans for the query or queries within this CachedPlanSource. + * Register the CachedPlan with the current resource owner. (Saving + * cowner here is mostly paranoia, but this way we needn't assume that + * CurrentResourceOwner will be the same when ShutdownSQLFunction runs.) + */ + fcache->cowner = CurrentResourceOwner; + fcache->cplan = GetCachedPlan(plansource, + fcache->paramLI, + fcache->cowner, + NULL); - if (IsA(stmt->utilityStmt, TransactionStmt)) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - /* translator: %s is a SQL statement name */ - errmsg("%s is not allowed in an SQL function", - CreateCommandName(stmt->utilityStmt)))); - } + /* + * Build execution_state list to match the number of contained plans. + */ + foreach(lc, fcache->cplan->stmt_list) + { + PlannedStmt *stmt = lfirst_node(PlannedStmt, lc); + execution_state *newes; - if (fcache->readonly_func && !CommandIsReadOnly(stmt)) + /* + * Precheck all commands for validity in a function. This should + * generally match the restrictions spi.c applies. + */ + if (stmt->commandType == CMD_UTILITY) + { + if (IsA(stmt->utilityStmt, CopyStmt) && + ((CopyStmt *) stmt->utilityStmt)->filename == NULL) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot COPY to/from client in an SQL function"))); + + if (IsA(stmt->utilityStmt, TransactionStmt)) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), /* translator: %s is a SQL statement name */ - errmsg("%s is not allowed in a non-volatile function", - CreateCommandName((Node *) stmt)))); + errmsg("%s is not allowed in an SQL function", + CreateCommandName(stmt->utilityStmt)))); + } - /* OK, build the execution_state for this query */ - newes = (execution_state *) palloc(sizeof(execution_state)); - if (preves) - preves->next = newes; - else - firstes = newes; + if (fcache->func->readonly_func && !CommandIsReadOnly(stmt)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + /* translator: %s is a SQL statement name */ + errmsg("%s is not allowed in a non-volatile function", + CreateCommandName((Node *) stmt)))); + + /* OK, build the execution_state for this query */ + newes = (execution_state *) palloc(sizeof(execution_state)); + if (preves) + preves->next = newes; + else + fcache->eslist = newes; - 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; + 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; + if (stmt->canSetTag) + lasttages = newes; - preves = newes; - } + preves = newes; + } - eslist = lappend(eslist, firstes); + /* + * If this isn't the last CachedPlanSource, we're done here. Otherwise, + * we need to prepare information about how to return the results. + */ + if (fcache->next_query_index < fcache->func->num_queries) + return true; + + /* + * 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. (XXX Frequently, the JunkFilter isn't doing + * anything very interesting, but much of this module expects it to be + * there anyway.) + */ + if (fcache->func->rettype != VOIDOID) + { + TupleTableSlot *slot = MakeSingleTupleTableSlot(NULL, + &TTSOpsMinimalTuple); + List *resulttlist; + + /* + * Re-fetch the (possibly modified) output tlist of the final + * statement. By this point, we should have thrown an error if there + * is not one. + */ + resulttlist = get_sql_fn_result_tlist(plansource->query_list); + + /* + * We need to make a copy to ensure that it doesn't disappear + * underneath us due to plancache invalidation. + */ + resulttlist = copyObject(resulttlist); + + /* + * If the result is composite, *and* we are returning the whole tuple + * result, we need to insert nulls for any dropped columns. In the + * single-column-result case, there might be dropped columns within + * the composite column value, but it's not our problem here. There + * should be no resjunk entries in resulttlist, so in the second case + * the JunkFilter is certainly a no-op. + */ + if (fcache->func->rettupdesc && fcache->func->returnsTuple) + fcache->junkFilter = ExecInitJunkFilterConversion(resulttlist, + fcache->func->rettupdesc, + slot); + else + fcache->junkFilter = ExecInitJunkFilter(resulttlist, slot); + } + + if (fcache->func->returnsTuple) + { + /* Make sure output rowtype is properly blessed */ + BlessTupleDesc(fcache->junkFilter->jf_resultSlot->tts_tupleDescriptor); + } + else if (fcache->func->returnsSet && type_is_rowtype(fcache->func->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 this might need more work in future. + */ + fcache->lazyEvalOK = true; } /* @@ -567,69 +841,205 @@ init_execution_state(List *queryTree_list, if (lasttages && fcache->junkFilter) { lasttages->setsResult = true; - if (lazyEvalOK && + if (fcache->lazyEvalOK && lasttages->stmt->commandType == CMD_SELECT && !lasttages->stmt->hasModifyingCTE) fcache->lazyEval = lasttages->lazyEval = true; } - return eslist; + return true; } /* - * Initialize the SQLFunctionCache for a SQL function + * Convert the SQL function's next query from source form (RawStmt or Query) + * into a CachedPlanSource. If it's the last query, also determine whether + * the function returnsTuple. */ static void -init_sql_fcache(FunctionCallInfo fcinfo, Oid collation, bool lazyEvalOK) +prepare_next_query(SQLFunctionHashEntry *func) { - FmgrInfo *finfo = fcinfo->flinfo; - Oid foid = finfo->fn_oid; - MemoryContext fcontext; + int qindex; + bool islast; + CachedPlanSource *plansource; + List *queryTree_list; MemoryContext oldcontext; + + /* Which query should we process? */ + qindex = list_length(func->plansource_list); + Assert(qindex < func->num_queries); /* else caller error */ + islast = (qindex + 1 >= func->num_queries); + + /* + * Parse and/or rewrite the query, creating a CachedPlanSource that holds + * a copy of the original parsetree. + */ + if (!func->raw_source) + { + /* Source queries are already parse-analyzed */ + Query *parsetree = list_nth_node(Query, func->source_list, qindex); + + plansource = CreateCachedPlanForQuery(parsetree, + func->src, + CreateCommandTag((Node *) parsetree)); + AcquireRewriteLocks(parsetree, true, false); + queryTree_list = pg_rewrite_query(parsetree); + } + else + { + /* Source queries are raw parsetrees */ + RawStmt *parsetree = list_nth_node(RawStmt, func->source_list, qindex); + + plansource = CreateCachedPlan(parsetree, + func->src, + CreateCommandTag(parsetree->stmt)); + queryTree_list = pg_analyze_and_rewrite_withcb(parsetree, + func->src, + (ParserSetupHook) sql_fn_parser_setup, + func->pinfo, + NULL); + } + + /* + * Check that there are no statements we don't want to allow. + */ + check_sql_fn_statement(queryTree_list); + + /* + * If this is the last query, check that the function returns the type it + * claims to. Although in simple cases this was already done when the + * function was defined, we have to recheck because database objects used + * in the function's queries might have changed type. We'd have to + * recheck anyway if the function had any polymorphic arguments. Moreover, + * check_sql_stmt_retval takes care of injecting any required column type + * coercions. (But we don't ask it to insert nulls for dropped columns; + * the junkfilter handles that.) + * + * Note: we set func->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. (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.) + */ + if (islast) + func->returnsTuple = check_sql_stmt_retval(queryTree_list, + func->rettype, + func->rettupdesc, + func->prokind, + false); + + /* + * Now that check_sql_stmt_retval has done its thing, we can complete plan + * cache entry creation. + */ + CompleteCachedPlan(plansource, + queryTree_list, + NULL, + NULL, + 0, + (ParserSetupHook) sql_fn_parser_setup, + func->pinfo, + CURSOR_OPT_PARALLEL_OK | CURSOR_OPT_NO_SCROLL, + false); + + /* + * Install post-rewrite hook. Its arg is the hash entry if this is the + * last statement, else NULL. + */ + SetPostRewriteHook(plansource, + sql_postrewrite_callback, + islast ? func : NULL); + + /* + * While the CachedPlanSources can take care of themselves, our List + * pointing to them had better be in the hcontext. + */ + oldcontext = MemoryContextSwitchTo(func->hcontext); + func->plansource_list = lappend(func->plansource_list, plansource); + MemoryContextSwitchTo(oldcontext); + + /* + * As soon as we've linked the CachedPlanSource into the list, mark it as + * "saved". + */ + SaveCachedPlan(plansource); + + /* + * Finally, if this was the last statement, we can flush the pcontext with + * the original query trees; they're all safely copied into + * CachedPlanSources now. + */ + if (islast) + { + func->source_list = NIL; /* avoid dangling pointer */ + MemoryContextDelete(func->pcontext); + func->pcontext = NULL; + } +} + +/* + * Fill a new SQLFunctionHashEntry. + * + * The passed-in "cfunc" struct is expected to be zeroes, except + * for the CachedFunction fields, which we don't touch here. + * + * We expect to be called in a short-lived memory context (typically a + * query's per-tuple context). Data that is to be part of the hash entry + * must be copied into the hcontext or pcontext as appropriate. + */ +static void +sql_compile_callback(FunctionCallInfo fcinfo, + HeapTuple procedureTuple, + const CachedFunctionHashKey *hashkey, + CachedFunction *cfunc, + bool forValidator) +{ + SQLFunctionHashEntry *func = (SQLFunctionHashEntry *) cfunc; + Form_pg_proc procedureStruct = (Form_pg_proc) GETSTRUCT(procedureTuple); + ErrorContextCallback comperrcontext; + MemoryContext hcontext; + MemoryContext pcontext; + MemoryContext oldcontext = CurrentMemoryContext; Oid rettype; TupleDesc rettupdesc; - HeapTuple procedureTuple; - Form_pg_proc procedureStruct; - SQLFunctionCachePtr fcache; - List *queryTree_list; - List *resulttlist; - ListCell *lc; Datum tmp; bool isNull; + List *source_list; /* - * Create memory context that holds all the SQLFunctionCache data. It - * must be a child of whatever context holds the FmgrInfo. + * Setup error traceback support for ereport() during compile. (This is + * mainly useful for reporting parse errors from pg_parse_query.) */ - fcontext = AllocSetContextCreate(finfo->fn_mcxt, - "SQL function", - ALLOCSET_DEFAULT_SIZES); - - oldcontext = MemoryContextSwitchTo(fcontext); + comperrcontext.callback = sql_compile_error_callback; + comperrcontext.arg = func; + comperrcontext.previous = error_context_stack; + error_context_stack = &comperrcontext; /* - * Create the struct proper, link it to fcontext and fn_extra. Once this - * is done, we'll be able to recover the memory after failure, even if the - * FmgrInfo is long-lived. + * Create the hash entry's memory context. For now it's a child of the + * caller's context, so that it will go away if we fail partway through. */ - fcache = (SQLFunctionCachePtr) palloc0(sizeof(SQLFunctionCache)); - fcache->fcontext = fcontext; - finfo->fn_extra = fcache; + hcontext = AllocSetContextCreate(CurrentMemoryContext, + "SQL function", + ALLOCSET_SMALL_SIZES); /* - * get the procedure tuple corresponding to the given function Oid + * Create the not-as-long-lived pcontext. We make this a child of + * hcontext so that it doesn't require separate deletion. */ - procedureTuple = SearchSysCache1(PROCOID, ObjectIdGetDatum(foid)); - if (!HeapTupleIsValid(procedureTuple)) - elog(ERROR, "cache lookup failed for function %u", foid); - procedureStruct = (Form_pg_proc) GETSTRUCT(procedureTuple); + pcontext = AllocSetContextCreate(hcontext, + "SQL function parse trees", + ALLOCSET_SMALL_SIZES); + func->pcontext = pcontext; /* * copy function name immediately for use by error reporting callback, and * for use as memory context identifier */ - fcache->fname = pstrdup(NameStr(procedureStruct->proname)); - MemoryContextSetIdentifier(fcontext, fcache->fname); + func->fname = MemoryContextStrdup(hcontext, + NameStr(procedureStruct->proname)); + MemoryContextSetIdentifier(hcontext, func->fname); /* * Resolve any polymorphism, obtaining the actual result type, and the @@ -637,176 +1047,158 @@ init_sql_fcache(FunctionCallInfo fcinfo, Oid collation, bool lazyEvalOK) */ (void) get_call_result_type(fcinfo, &rettype, &rettupdesc); - fcache->rettype = rettype; + func->rettype = rettype; + if (rettupdesc) + { + MemoryContextSwitchTo(hcontext); + func->rettupdesc = CreateTupleDescCopy(rettupdesc); + MemoryContextSwitchTo(oldcontext); + } /* Fetch the typlen and byval info for the result type */ - get_typlenbyval(rettype, &fcache->typlen, &fcache->typbyval); + get_typlenbyval(rettype, &func->typlen, &func->typbyval); /* Remember whether we're returning setof something */ - fcache->returnsSet = procedureStruct->proretset; + func->returnsSet = procedureStruct->proretset; /* Remember if function is STABLE/IMMUTABLE */ - fcache->readonly_func = + func->readonly_func = (procedureStruct->provolatile != PROVOLATILE_VOLATILE); + /* Remember routine kind */ + func->prokind = procedureStruct->prokind; + /* * We need the actual argument types to pass to the parser. Also make * sure that parameter symbols are considered to have the function's * resolved input collation. */ - fcache->pinfo = prepare_sql_fn_parse_info(procedureTuple, - finfo->fn_expr, - collation); + MemoryContextSwitchTo(hcontext); + func->pinfo = prepare_sql_fn_parse_info(procedureTuple, + fcinfo->flinfo->fn_expr, + PG_GET_COLLATION()); + MemoryContextSwitchTo(oldcontext); /* * And of course we need the function body text. */ tmp = SysCacheGetAttrNotNull(PROCOID, procedureTuple, Anum_pg_proc_prosrc); - fcache->src = TextDatumGetCString(tmp); + func->src = MemoryContextStrdup(hcontext, + TextDatumGetCString(tmp)); /* If we have prosqlbody, pay attention to that not prosrc. */ tmp = SysCacheGetAttr(PROCOID, procedureTuple, Anum_pg_proc_prosqlbody, &isNull); - - /* - * Parse and rewrite the queries in the function text. Use sublists to - * keep track of the original query boundaries. - * - * Note: since parsing and planning is done in fcontext, we will generate - * a lot of cruft that lives as long as the fcache does. This is annoying - * but we'll not worry about it until the module is rewritten to use - * plancache.c. - */ - queryTree_list = NIL; if (!isNull) { + /* Source queries are already parse-analyzed */ Node *n; - List *stored_query_list; n = stringToNode(TextDatumGetCString(tmp)); if (IsA(n, List)) - stored_query_list = linitial_node(List, castNode(List, n)); + source_list = linitial_node(List, castNode(List, n)); else - stored_query_list = list_make1(n); - - foreach(lc, stored_query_list) - { - Query *parsetree = lfirst_node(Query, lc); - List *queryTree_sublist; - - AcquireRewriteLocks(parsetree, true, false); - queryTree_sublist = pg_rewrite_query(parsetree); - queryTree_list = lappend(queryTree_list, queryTree_sublist); - } + source_list = list_make1(n); + func->raw_source = false; } else { - List *raw_parsetree_list; - - raw_parsetree_list = pg_parse_query(fcache->src); - - foreach(lc, raw_parsetree_list) - { - RawStmt *parsetree = lfirst_node(RawStmt, lc); - List *queryTree_sublist; - - queryTree_sublist = pg_analyze_and_rewrite_withcb(parsetree, - fcache->src, - (ParserSetupHook) sql_fn_parser_setup, - fcache->pinfo, - NULL); - queryTree_list = lappend(queryTree_list, queryTree_sublist); - } + /* Source queries are raw parsetrees */ + source_list = pg_parse_query(func->src); + func->raw_source = true; } /* - * Check that there are no statements we don't want to allow. + * Note: we must save the number of queries so that we'll still remember + * how many there are after we discard source_list. */ - check_sql_fn_statements(queryTree_list); + func->num_queries = list_length(source_list); - /* - * Check that the function returns the type it claims to. Although in - * simple cases this was already done when the function was defined, we - * have to recheck because database objects used in the function's queries - * might have changed type. We'd have to recheck anyway if the function - * had any polymorphic arguments. Moreover, check_sql_fn_retval takes - * care of injecting any required column type coercions. (But we don't - * ask it to insert nulls for dropped columns; the junkfilter handles - * that.) - * - * 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. (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.) - */ - fcache->returnsTuple = check_sql_fn_retval(queryTree_list, - rettype, - rettupdesc, - procedureStruct->prokind, - false, - &resulttlist); + /* Save the source trees in pcontext for now. */ + MemoryContextSwitchTo(pcontext); + func->source_list = copyObject(source_list); + MemoryContextSwitchTo(oldcontext); /* - * 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. (XXX Frequently, the JunkFilter isn't doing - * anything very interesting, but much of this module expects it to be - * there anyway.) + * We now have a fully valid hash entry, so reparent hcontext under + * CacheMemoryContext to make all the subsidiary data long-lived, and only + * then install the hcontext link so that sql_delete_callback will know to + * delete it. */ - if (rettype != VOIDOID) - { - TupleTableSlot *slot = MakeSingleTupleTableSlot(NULL, - &TTSOpsMinimalTuple); + MemoryContextSetParent(hcontext, CacheMemoryContext); + func->hcontext = hcontext; - /* - * If the result is composite, *and* we are returning the whole tuple - * result, we need to insert nulls for any dropped columns. In the - * single-column-result case, there might be dropped columns within - * the composite column value, but it's not our problem here. There - * should be no resjunk entries in resulttlist, so in the second case - * the JunkFilter is certainly a no-op. - */ - if (rettupdesc && fcache->returnsTuple) - fcache->junkFilter = ExecInitJunkFilterConversion(resulttlist, - rettupdesc, - slot); - else - fcache->junkFilter = ExecInitJunkFilter(resulttlist, slot); - } + error_context_stack = comperrcontext.previous; +} - 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)) +/* + * Deletion callback used by funccache.c. + * + * Free any free-able subsidiary data of cfunc, but not the + * struct CachedFunction itself. + */ +static void +sql_delete_callback(CachedFunction *cfunc) +{ + SQLFunctionHashEntry *func = (SQLFunctionHashEntry *) cfunc; + ListCell *lc; + + /* Release the CachedPlanSources */ + foreach(lc, func->plansource_list) { - /* - * 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; - } + CachedPlanSource *plansource = (CachedPlanSource *) lfirst(lc); - /* Finally, plan the queries */ - fcache->func_state = init_execution_state(queryTree_list, - fcache, - lazyEvalOK); + DropCachedPlan(plansource); + } + func->plansource_list = NIL; - /* Mark fcache with time of creation to show it's valid */ - fcache->lxid = MyProc->vxid.lxid; - fcache->subxid = GetCurrentSubTransactionId(); + /* + * If we have an hcontext, free it, thereby getting rid of all subsidiary + * data. (If we still have a pcontext, this gets rid of that too.) + */ + if (func->hcontext) + MemoryContextDelete(func->hcontext); + func->hcontext = NULL; +} - ReleaseSysCache(procedureTuple); +/* + * Post-rewrite callback used by plancache.c. + * + * This must match the processing that prepare_next_query() does between + * rewriting and calling CompleteCachedPlan(). + */ +static void +sql_postrewrite_callback(List *querytree_list, void *arg) +{ + /* + * Check that there are no statements we don't want to allow. (Presently, + * there's no real point in this because the result can't change from what + * we saw originally. But it's cheap and maybe someday it will matter.) + */ + check_sql_fn_statement(querytree_list); - MemoryContextSwitchTo(oldcontext); + /* + * If this is the last query, we must re-do what check_sql_stmt_retval did + * to its targetlist. Also check that returnsTuple didn't change (it + * probably cannot, but be cautious). + */ + if (arg != NULL) + { + SQLFunctionHashEntry *func = (SQLFunctionHashEntry *) arg; + bool returnsTuple; + + returnsTuple = check_sql_stmt_retval(querytree_list, + func->rettype, + func->rettupdesc, + func->prokind, + false); + if (returnsTuple != func->returnsTuple) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cached plan must not change result type"))); + } } /* Start up execution of one execution_state node */ @@ -841,7 +1233,7 @@ postquel_start(execution_state *es, SQLFunctionCachePtr fcache) es->qd = CreateQueryDesc(es->stmt, NULL, - fcache->src, + fcache->func->src, GetActiveSnapshot(), InvalidSnapshot, dest, @@ -882,7 +1274,7 @@ postquel_getnext(execution_state *es, SQLFunctionCachePtr fcache) if (es->qd->operation == CMD_UTILITY) { ProcessUtility(es->qd->plannedstmt, - fcache->src, + fcache->func->src, true, /* protect function cache's parsetree */ PROCESS_UTILITY_QUERY, es->qd->params, @@ -938,7 +1330,7 @@ postquel_sub_params(SQLFunctionCachePtr fcache, if (nargs > 0) { ParamListInfo paramLI; - Oid *argtypes = fcache->pinfo->argtypes; + Oid *argtypes = fcache->func->pinfo->argtypes; if (fcache->paramLI == NULL) { @@ -971,7 +1363,8 @@ postquel_sub_params(SQLFunctionCachePtr fcache, prm->value = MakeExpandedObjectReadOnly(fcinfo->args[i].value, prm->isnull, get_typlen(argtypes[i])); - prm->pflags = 0; + /* Allow the value to be substituted into custom plans */ + prm->pflags = PARAM_FLAG_CONST; prm->ptype = argtypes[i]; } } @@ -1001,7 +1394,7 @@ postquel_get_single_result(TupleTableSlot *slot, */ oldcontext = MemoryContextSwitchTo(resultcontext); - if (fcache->returnsTuple) + if (fcache->func->returnsTuple) { /* We must return the whole tuple as a Datum. */ fcinfo->isnull = false; @@ -1016,7 +1409,7 @@ postquel_get_single_result(TupleTableSlot *slot, value = slot_getattr(slot, 1, &(fcinfo->isnull)); if (!fcinfo->isnull) - value = datumCopy(value, fcache->typbyval, fcache->typlen); + value = datumCopy(value, fcache->func->typbyval, fcache->func->typlen); } MemoryContextSwitchTo(oldcontext); @@ -1031,25 +1424,16 @@ Datum fmgr_sql(PG_FUNCTION_ARGS) { SQLFunctionCachePtr fcache; + SQLFunctionLink *flink; ErrorContextCallback sqlerrcontext; + MemoryContext tscontext; MemoryContext oldcontext; bool randomAccess; bool lazyEvalOK; - bool is_first; bool pushed_snapshot; execution_state *es; TupleTableSlot *slot; Datum result; - List *eslist; - ListCell *eslc; - - /* - * 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; /* Check call context */ if (fcinfo->flinfo->fn_retset) @@ -1070,80 +1454,63 @@ fmgr_sql(PG_FUNCTION_ARGS) errmsg("set-valued function called in context that cannot accept a set"))); randomAccess = rsi->allowedModes & SFRM_Materialize_Random; lazyEvalOK = !(rsi->allowedModes & SFRM_Materialize_Preferred); + /* tuplestore must have query lifespan */ + tscontext = rsi->econtext->ecxt_per_query_memory; } else { randomAccess = false; lazyEvalOK = true; + /* tuplestore needn't outlive caller context */ + tscontext = CurrentMemoryContext; } /* - * Initialize fcache (build plans) if first time through; or re-initialize - * if the cache is stale. + * Initialize fcache if starting a fresh execution. */ - fcache = (SQLFunctionCachePtr) fcinfo->flinfo->fn_extra; + fcache = init_sql_fcache(fcinfo, lazyEvalOK); + /* init_sql_fcache also ensures we have a SQLFunctionLink */ + flink = fcinfo->flinfo->fn_extra; - if (fcache != NULL) - { - if (fcache->lxid != MyProc->vxid.lxid || - !SubTransactionIsActive(fcache->subxid)) - { - /* It's stale; unlink and delete */ - fcinfo->flinfo->fn_extra = NULL; - MemoryContextDelete(fcache->fcontext); - fcache = NULL; - } - } + /* + * Now we can set up error traceback support for ereport() + */ + sqlerrcontext.callback = sql_exec_error_callback; + sqlerrcontext.arg = fcache; + sqlerrcontext.previous = error_context_stack; + error_context_stack = &sqlerrcontext; - if (fcache == NULL) - { - init_sql_fcache(fcinfo, PG_GET_COLLATION(), lazyEvalOK); - fcache = (SQLFunctionCachePtr) fcinfo->flinfo->fn_extra; - } + /* + * Build tuplestore to hold results, if we don't have one already. Make + * sure it's in a suitable context. + */ + oldcontext = MemoryContextSwitchTo(tscontext); + + if (!fcache->tstore) + fcache->tstore = tuplestore_begin_heap(randomAccess, false, work_mem); /* - * Switch to context in which the fcache lives. This ensures that our - * tuplestore etc will have sufficient lifetime. The sub-executor is + * Switch to context in which the fcache lives. The sub-executor is * responsible for deleting per-tuple information. (XXX in the case of a - * long-lived FmgrInfo, this policy represents more memory leakage, but - * it's not entirely clear where to keep stuff instead.) + * long-lived FmgrInfo, this policy potentially causes memory leakage, but + * it's not very clear where we could keep stuff instead. Fortunately, + * there are few if any cases where set-returning functions are invoked + * via FmgrInfos that would outlive the calling query.) */ - oldcontext = MemoryContextSwitchTo(fcache->fcontext); + MemoryContextSwitchTo(fcache->fcontext); /* - * Find first unfinished query in function, and note whether it's the - * first query. + * Find first unfinished execution_state. If none, advance to the next + * query in function. */ - eslist = fcache->func_state; - es = NULL; - is_first = true; - foreach(eslc, eslist) + do { - es = (execution_state *) lfirst(eslc); - + es = fcache->eslist; while (es && es->status == F_EXEC_DONE) - { - is_first = false; es = es->next; - } - if (es) break; - } - - /* - * Convert params to appropriate format if starting a fresh execution. (If - * continuing execution, we can re-use prior params.) - */ - if (is_first && es && es->status == F_EXEC_START) - 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); + } while (init_execution_state(fcache)); /* * Execute each command in the function one after another until we either @@ -1176,7 +1543,7 @@ fmgr_sql(PG_FUNCTION_ARGS) * visible. Take a new snapshot if we don't have one yet, * otherwise just bump the command ID in the existing snapshot. */ - if (!fcache->readonly_func) + if (!fcache->func->readonly_func) { CommandCounterIncrement(); if (!pushed_snapshot) @@ -1190,7 +1557,7 @@ fmgr_sql(PG_FUNCTION_ARGS) postquel_start(es, fcache); } - else if (!fcache->readonly_func && !pushed_snapshot) + else if (!fcache->func->readonly_func && !pushed_snapshot) { /* Re-establish active snapshot when re-entering function */ PushActiveSnapshot(es->qd->snapshot); @@ -1207,7 +1574,7 @@ fmgr_sql(PG_FUNCTION_ARGS) * set, we can shut it down anyway because it must be a SELECT and we * don't care about fetching any more result rows. */ - if (completed || !fcache->returnsSet) + if (completed || !fcache->func->returnsSet) postquel_end(es); /* @@ -1223,17 +1590,11 @@ fmgr_sql(PG_FUNCTION_ARGS) break; /* - * Advance to next execution_state, which might be in the next list. + * Advance to next execution_state, and perhaps next query. */ es = es->next; while (!es) { - eslc = lnext(eslist, eslc); - if (!eslc) - break; /* end of function */ - - es = (execution_state *) lfirst(eslc); - /* * Flush the current snapshot so that we will take a new one for * the new query list. This ensures that new snaps are taken at @@ -1245,13 +1606,18 @@ fmgr_sql(PG_FUNCTION_ARGS) PopActiveSnapshot(); pushed_snapshot = false; } + + if (!init_execution_state(fcache)) + break; /* end of function */ + + es = fcache->eslist; } } /* * The tuplestore now contains whatever row(s) we are supposed to return. */ - if (fcache->returnsSet) + if (fcache->func->returnsSet) { ReturnSetInfo *rsi = (ReturnSetInfo *) fcinfo->resultinfo; @@ -1287,7 +1653,7 @@ fmgr_sql(PG_FUNCTION_ARGS) { RegisterExprContextCallback(rsi->econtext, ShutdownSQLFunction, - PointerGetDatum(fcache)); + PointerGetDatum(flink)); fcache->shutdown_reg = true; } } @@ -1311,7 +1677,7 @@ fmgr_sql(PG_FUNCTION_ARGS) { UnregisterExprContextCallback(rsi->econtext, ShutdownSQLFunction, - PointerGetDatum(fcache)); + PointerGetDatum(flink)); fcache->shutdown_reg = false; } } @@ -1327,7 +1693,12 @@ fmgr_sql(PG_FUNCTION_ARGS) fcache->tstore = NULL; /* must copy desc because execSRF.c will free it */ if (fcache->junkFilter) + { + /* setDesc must be allocated in suitable context */ + MemoryContextSwitchTo(tscontext); rsi->setDesc = CreateTupleDescCopy(fcache->junkFilter->jf_cleanTupType); + MemoryContextSwitchTo(fcache->fcontext); + } fcinfo->isnull = true; result = (Datum) 0; @@ -1337,7 +1708,7 @@ fmgr_sql(PG_FUNCTION_ARGS) { UnregisterExprContextCallback(rsi->econtext, ShutdownSQLFunction, - PointerGetDatum(fcache)); + PointerGetDatum(flink)); fcache->shutdown_reg = false; } } @@ -1363,7 +1734,7 @@ fmgr_sql(PG_FUNCTION_ARGS) else { /* Should only get here for VOID functions and procedures */ - Assert(fcache->rettype == VOIDOID); + Assert(fcache->func->rettype == VOIDOID); fcinfo->isnull = true; result = (Datum) 0; } @@ -1376,154 +1747,167 @@ fmgr_sql(PG_FUNCTION_ARGS) if (pushed_snapshot) PopActiveSnapshot(); + MemoryContextSwitchTo(oldcontext); + /* - * 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 we've gone through every command in the function, we are done. + * Release the cache to start over again on next call. */ if (es == NULL) { - foreach(eslc, fcache->func_state) - { - es = (execution_state *) lfirst(eslc); - while (es) - { - es->status = F_EXEC_START; - es = es->next; - } - } + if (fcache->tstore) + tuplestore_end(fcache->tstore); + Assert(fcache->cplan == NULL); + flink->fcache = NULL; + MemoryContextDelete(fcache->fcontext); } error_context_stack = sqlerrcontext.previous; - MemoryContextSwitchTo(oldcontext); - return result; } /* - * error context callback to let us supply a call-stack traceback + * error context callback to let us supply a traceback during compile */ static void -sql_exec_error_callback(void *arg) +sql_compile_error_callback(void *arg) { - FmgrInfo *flinfo = (FmgrInfo *) arg; - SQLFunctionCachePtr fcache = (SQLFunctionCachePtr) flinfo->fn_extra; + SQLFunctionHashEntry *func = (SQLFunctionHashEntry *) arg; int syntaxerrposition; /* - * We can do nothing useful if init_sql_fcache() didn't get as far as - * saving the function name + * We can do nothing useful if sql_compile_callback() didn't get as far as + * copying the function name */ - if (fcache == NULL || fcache->fname == NULL) + if (func->fname == NULL) return; /* * If there is a syntax error position, convert to internal syntax error */ syntaxerrposition = geterrposition(); - if (syntaxerrposition > 0 && fcache->src != NULL) + if (syntaxerrposition > 0 && func->src != NULL) { errposition(0); internalerrposition(syntaxerrposition); - internalerrquery(fcache->src); + internalerrquery(func->src); } /* - * 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.) + * sql_compile_callback() doesn't do any per-query processing, so just + * report the context as "during startup". */ - if (fcache->func_state) - { - execution_state *es; - int query_num; - ListCell *lc; + errcontext("SQL function \"%s\" during startup", func->fname); +} - es = NULL; - query_num = 1; - foreach(lc, fcache->func_state) - { - es = (execution_state *) lfirst(lc); - while (es) - { - if (es->qd) - { - errcontext("SQL function \"%s\" statement %d", - fcache->fname, query_num); - break; - } - es = es->next; - } - if (es) - break; - query_num++; - } - if (es == NULL) - { - /* - * couldn't identify a running query; might be function entry, - * function exit, or between queries. - */ - errcontext("SQL function \"%s\"", fcache->fname); - } - } - else +/* + * error context callback to let us supply a call-stack traceback at runtime + */ +static void +sql_exec_error_callback(void *arg) +{ + SQLFunctionCachePtr fcache = (SQLFunctionCachePtr) arg; + int syntaxerrposition; + + /* + * If there is a syntax error position, convert to internal syntax error + */ + syntaxerrposition = geterrposition(); + if (syntaxerrposition > 0 && fcache->func->src != NULL) { - /* - * Assume we failed during init_sql_fcache(). (It's possible that the - * function actually has an empty body, but in that case we may as - * well report all errors as being "during startup".) - */ - errcontext("SQL function \"%s\" during startup", fcache->fname); + errposition(0); + internalerrposition(syntaxerrposition); + internalerrquery(fcache->func->src); } + + /* + * If we failed while executing an identifiable query within the function, + * report that. Otherwise say it was "during startup". + */ + if (fcache->error_query_index > 0) + errcontext("SQL function \"%s\" statement %d", + fcache->func->fname, fcache->error_query_index); + else + errcontext("SQL function \"%s\" during startup", fcache->func->fname); } /* - * callback function in case a function-returning-set needs to be shut down - * before it has been run to completion + * ExprContext callback function + * + * We register this in the active ExprContext while a set-returning SQL + * function is running, in case the function needs to be shut down before it + * has been run to completion. Note that this will not be called during an + * error abort, but we don't need it because transaction abort will take care + * of releasing executor resources. */ static void ShutdownSQLFunction(Datum arg) { - SQLFunctionCachePtr fcache = (SQLFunctionCachePtr) DatumGetPointer(arg); - execution_state *es; - ListCell *lc; + SQLFunctionLink *flink = (SQLFunctionLink *) DatumGetPointer(arg); + SQLFunctionCachePtr fcache = flink->fcache; - foreach(lc, fcache->func_state) + if (fcache != NULL) { - es = (execution_state *) lfirst(lc); + execution_state *es; + + /* Make sure we don't somehow try to do this twice */ + flink->fcache = NULL; + + es = fcache->eslist; while (es) { /* Shut down anything still running */ if (es->status == F_EXEC_RUN) { /* Re-establish active snapshot for any called functions */ - if (!fcache->readonly_func) + if (!fcache->func->readonly_func) PushActiveSnapshot(es->qd->snapshot); postquel_end(es); - if (!fcache->readonly_func) + if (!fcache->func->readonly_func) PopActiveSnapshot(); } - - /* Reset states to START in case we're called again */ - es->status = F_EXEC_START; es = es->next; } - } - /* Release tuplestore if we have one */ - if (fcache->tstore) - tuplestore_end(fcache->tstore); - fcache->tstore = NULL; + /* Release tuplestore if we have one */ + if (fcache->tstore) + tuplestore_end(fcache->tstore); + /* Release CachedPlan if we have one */ + if (fcache->cplan) + ReleaseCachedPlan(fcache->cplan, fcache->cowner); + + /* Release the cache */ + MemoryContextDelete(fcache->fcontext); + } /* execUtils will deregister the callback... */ - fcache->shutdown_reg = false; +} + +/* + * MemoryContext callback function + * + * We register this in the memory context that contains a SQLFunctionLink + * struct. When the memory context is reset or deleted, we release the + * reference count (if any) that the link holds on the long-lived hash entry. + * Note that this will happen even during error aborts. + */ +static void +RemoveSQLFunctionLink(void *arg) +{ + SQLFunctionLink *flink = (SQLFunctionLink *) arg; + + if (flink->func != NULL) + { + Assert(flink->func->cfunc.use_count > 0); + flink->func->cfunc.use_count--; + /* This should be unnecessary, but let's just be sure: */ + flink->func = NULL; + } } /* @@ -1541,29 +1925,39 @@ check_sql_fn_statements(List *queryTreeLists) foreach(lc, queryTreeLists) { List *sublist = lfirst_node(List, lc); - ListCell *lc2; - foreach(lc2, sublist) - { - Query *query = lfirst_node(Query, lc2); + check_sql_fn_statement(sublist); + } +} - /* - * Disallow calling procedures with output arguments. The current - * implementation would just throw the output values away, unless - * the statement is the last one. Per SQL standard, we should - * assign the output values by name. By disallowing this here, we - * preserve an opportunity for future improvement. - */ - if (query->commandType == CMD_UTILITY && - IsA(query->utilityStmt, CallStmt)) - { - CallStmt *stmt = (CallStmt *) query->utilityStmt; +/* + * As above, for a single sublist of Queries. + */ +static void +check_sql_fn_statement(List *queryTreeList) +{ + ListCell *lc; - if (stmt->outargs != NIL) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("calling procedures with output arguments is not supported in SQL functions"))); - } + foreach(lc, queryTreeList) + { + Query *query = lfirst_node(Query, lc); + + /* + * Disallow calling procedures with output arguments. The current + * implementation would just throw the output values away, unless the + * statement is the last one. Per SQL standard, we should assign the + * output values by name. By disallowing this here, we preserve an + * opportunity for future improvement. + */ + if (query->commandType == CMD_UTILITY && + IsA(query->utilityStmt, CallStmt)) + { + CallStmt *stmt = (CallStmt *) query->utilityStmt; + + if (stmt->outargs != NIL) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("calling procedures with output arguments is not supported in SQL functions"))); } } } @@ -1602,17 +1996,45 @@ check_sql_fn_statements(List *queryTreeLists) * In addition to coercing individual output columns, we can modify the * output to include dummy NULL columns for any dropped columns appearing * in rettupdesc. This is done only if the caller asks for it. - * - * If resultTargetList isn't NULL, then *resultTargetList is set to the - * targetlist that defines the final statement's result. Exception: if the - * function is defined to return VOID then *resultTargetList is set to NIL. */ bool check_sql_fn_retval(List *queryTreeLists, Oid rettype, TupleDesc rettupdesc, char prokind, - bool insertDroppedCols, - List **resultTargetList) + bool insertDroppedCols) +{ + List *queryTreeList; + + /* + * We consider only the last sublist of Query nodes, so that only the last + * original statement is a candidate to produce the result. This is a + * change from pre-v18 versions, which would back up to the last statement + * that includes a canSetTag query, thus ignoring any ending statement(s) + * that rewrite to DO INSTEAD NOTHING. That behavior was undocumented and + * there seems no good reason for it, except that it was an artifact of + * the original coding. + * + * If the function body is completely empty, handle that the same as if + * the last query had rewritten to nothing. + */ + if (queryTreeLists != NIL) + queryTreeList = llast_node(List, queryTreeLists); + else + queryTreeList = NIL; + + return check_sql_stmt_retval(queryTreeList, + rettype, rettupdesc, + prokind, insertDroppedCols); +} + +/* + * As for check_sql_fn_retval, but we are given just the last query's + * rewritten-queries list. + */ +static bool +check_sql_stmt_retval(List *queryTreeList, + Oid rettype, TupleDesc rettupdesc, + char prokind, bool insertDroppedCols) { bool is_tuple_result = false; Query *parse; @@ -1625,9 +2047,6 @@ check_sql_fn_retval(List *queryTreeLists, bool upper_tlist_nontrivial = false; ListCell *lc; - if (resultTargetList) - *resultTargetList = NIL; /* initialize in case of VOID result */ - /* * If it's declared to return VOID, we don't care what's in the function. * (This takes care of procedures with no output parameters, as well.) @@ -1636,30 +2055,20 @@ check_sql_fn_retval(List *queryTreeLists, return false; /* - * Find the last canSetTag query in the function body (which is presented - * to us as a list of sublists of Query nodes). This isn't necessarily - * the last parsetree, because rule rewriting can insert queries after - * what the user wrote. Note that it might not even be in the last - * sublist, for example if the last query rewrites to DO INSTEAD NOTHING. - * (It might not be unreasonable to throw an error in such a case, but - * this is the historical behavior and it doesn't seem worth changing.) + * Find the last canSetTag query in the list of Query nodes. This isn't + * necessarily the last parsetree, because rule rewriting can insert + * queries after what the user wrote. */ parse = NULL; parse_cell = NULL; - foreach(lc, queryTreeLists) + foreach(lc, queryTreeList) { - List *sublist = lfirst_node(List, lc); - ListCell *lc2; + Query *q = lfirst_node(Query, lc); - foreach(lc2, sublist) + if (q->canSetTag) { - Query *q = lfirst_node(Query, lc2); - - if (q->canSetTag) - { - parse = q; - parse_cell = lc2; - } + parse = q; + parse_cell = lc; } } @@ -1812,12 +2221,7 @@ check_sql_fn_retval(List *queryTreeLists, * further checking. Assume we're returning the whole tuple. */ if (rettupdesc == NULL) - { - /* Return tlist if requested */ - if (resultTargetList) - *resultTargetList = tlist; return true; - } /* * Verify that the targetlist matches the return tuple type. We scan @@ -1984,10 +2388,6 @@ tlist_coercion_finished: lfirst(parse_cell) = newquery; } - /* Return tlist (possibly modified) if requested */ - if (resultTargetList) - *resultTargetList = upper_tlist; - return is_tuple_result; } @@ -2063,6 +2463,37 @@ coerce_fn_result_column(TargetEntry *src_tle, return true; } +/* + * Extract the targetlist of the last canSetTag query in the given list + * of parsed-and-rewritten Queries. Returns NIL if there is none. + */ +static List * +get_sql_fn_result_tlist(List *queryTreeList) +{ + Query *parse = NULL; + ListCell *lc; + + foreach(lc, queryTreeList) + { + Query *q = lfirst_node(Query, lc); + + if (q->canSetTag) + parse = q; + } + if (parse && + parse->commandType == CMD_SELECT) + return parse->targetList; + else if (parse && + (parse->commandType == CMD_INSERT || + parse->commandType == CMD_UPDATE || + parse->commandType == CMD_DELETE || + parse->commandType == CMD_MERGE) && + parse->returningList) + return parse->returningList; + else + return NIL; +} + /* * CreateSQLFunctionDestReceiver -- create a suitable DestReceiver object |