mirror of
https://github.com/postgres/postgres.git
synced 2025-04-20 00:42:27 +03:00
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
This commit is contained in:
parent
e9e7b66044
commit
0dca5d68d7
@ -234,21 +234,6 @@ CALL clean_emp();
|
||||
whereas returning <type>void</type> is a PostgreSQL extension.
|
||||
</para>
|
||||
|
||||
<note>
|
||||
<para>
|
||||
The entire body of an SQL function is parsed before any of it is
|
||||
executed. While an SQL function can contain commands that alter
|
||||
the system catalogs (e.g., <command>CREATE TABLE</command>), the effects
|
||||
of such commands will not be visible during parse analysis of
|
||||
later commands in the function. Thus, for example,
|
||||
<literal>CREATE TABLE foo (...); INSERT INTO foo VALUES(...);</literal>
|
||||
will not work as desired if packaged up into a single SQL function,
|
||||
since <structname>foo</structname> won't exist yet when the <command>INSERT</command>
|
||||
command is parsed. It's recommended to use <application>PL/pgSQL</application>
|
||||
instead of an SQL function in this type of situation.
|
||||
</para>
|
||||
</note>
|
||||
|
||||
<para>
|
||||
The syntax of the <command>CREATE FUNCTION</command> command requires
|
||||
the function body to be written as a string constant. It is usually
|
||||
|
@ -960,7 +960,7 @@ fmgr_sql_validator(PG_FUNCTION_ARGS)
|
||||
(void) check_sql_fn_retval(querytree_list,
|
||||
rettype, rettupdesc,
|
||||
proc->prokind,
|
||||
false, NULL);
|
||||
false);
|
||||
}
|
||||
|
||||
error_context_stack = sqlerrcontext.previous;
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -4742,7 +4742,7 @@ inline_function(Oid funcid, Oid result_type, Oid result_collid,
|
||||
if (check_sql_fn_retval(list_make1(querytree_list),
|
||||
result_type, rettupdesc,
|
||||
funcform->prokind,
|
||||
false, NULL))
|
||||
false))
|
||||
goto fail; /* reject whole-tuple-result cases */
|
||||
|
||||
/*
|
||||
@ -5288,7 +5288,7 @@ inline_set_returning_function(PlannerInfo *root, RangeTblEntry *rte)
|
||||
if (!check_sql_fn_retval(list_make1(querytree_list),
|
||||
fexpr->funcresulttype, rettupdesc,
|
||||
funcform->prokind,
|
||||
true, NULL) &&
|
||||
true) &&
|
||||
(functypclass == TYPEFUNC_COMPOSITE ||
|
||||
functypclass == TYPEFUNC_COMPOSITE_DOMAIN ||
|
||||
functypclass == TYPEFUNC_RECORD))
|
||||
|
@ -591,6 +591,45 @@ analyze_requires_snapshot(RawStmt *parseTree)
|
||||
return stmt_requires_parse_analysis(parseTree);
|
||||
}
|
||||
|
||||
/*
|
||||
* query_requires_rewrite_plan()
|
||||
* Returns true if rewriting or planning is non-trivial for this Query.
|
||||
*
|
||||
* This is much like stmt_requires_parse_analysis(), but applies one step
|
||||
* further down the pipeline.
|
||||
*
|
||||
* We do not provide an equivalent of analyze_requires_snapshot(): callers
|
||||
* can assume that any rewriting or planning activity needs a snapshot.
|
||||
*/
|
||||
bool
|
||||
query_requires_rewrite_plan(Query *query)
|
||||
{
|
||||
bool result;
|
||||
|
||||
if (query->commandType != CMD_UTILITY)
|
||||
{
|
||||
/* All optimizable statements require rewriting/planning */
|
||||
result = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
/* This list should match stmt_requires_parse_analysis() */
|
||||
switch (nodeTag(query->utilityStmt))
|
||||
{
|
||||
case T_DeclareCursorStmt:
|
||||
case T_ExplainStmt:
|
||||
case T_CreateTableAsStmt:
|
||||
case T_CallStmt:
|
||||
result = true;
|
||||
break;
|
||||
default:
|
||||
result = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/*
|
||||
* transformDeleteStmt -
|
||||
* transforms a Delete Statement
|
||||
|
1
src/backend/utils/cache/Makefile
vendored
1
src/backend/utils/cache/Makefile
vendored
@ -16,6 +16,7 @@ OBJS = \
|
||||
attoptcache.o \
|
||||
catcache.o \
|
||||
evtcache.o \
|
||||
funccache.o \
|
||||
inval.o \
|
||||
lsyscache.o \
|
||||
partcache.o \
|
||||
|
612
src/backend/utils/cache/funccache.c
vendored
Normal file
612
src/backend/utils/cache/funccache.c
vendored
Normal file
@ -0,0 +1,612 @@
|
||||
/*-------------------------------------------------------------------------
|
||||
*
|
||||
* funccache.c
|
||||
* Function cache management.
|
||||
*
|
||||
* funccache.c manages a cache of function execution data. The cache
|
||||
* is used by SQL-language and PL/pgSQL functions, and could be used by
|
||||
* other function languages. Each cache entry is specific to the execution
|
||||
* of a particular function (identified by OID) with specific input data
|
||||
* types; so a polymorphic function could have many associated cache entries.
|
||||
* Trigger functions similarly have a cache entry per trigger. These rules
|
||||
* allow the cached data to be specific to the particular data types the
|
||||
* function call will be dealing with.
|
||||
*
|
||||
*
|
||||
* Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
|
||||
* Portions Copyright (c) 1994, Regents of the University of California
|
||||
*
|
||||
* IDENTIFICATION
|
||||
* src/backend/utils/cache/funccache.c
|
||||
*
|
||||
*-------------------------------------------------------------------------
|
||||
*/
|
||||
#include "postgres.h"
|
||||
|
||||
#include "catalog/pg_proc.h"
|
||||
#include "commands/event_trigger.h"
|
||||
#include "commands/trigger.h"
|
||||
#include "common/hashfn.h"
|
||||
#include "funcapi.h"
|
||||
#include "utils/funccache.h"
|
||||
#include "utils/hsearch.h"
|
||||
#include "utils/syscache.h"
|
||||
|
||||
|
||||
/*
|
||||
* Hash table for cached functions
|
||||
*/
|
||||
static HTAB *cfunc_hashtable = NULL;
|
||||
|
||||
typedef struct CachedFunctionHashEntry
|
||||
{
|
||||
CachedFunctionHashKey key; /* hash key, must be first */
|
||||
CachedFunction *function; /* points to data of language-specific size */
|
||||
} CachedFunctionHashEntry;
|
||||
|
||||
#define FUNCS_PER_USER 128 /* initial table size */
|
||||
|
||||
static uint32 cfunc_hash(const void *key, Size keysize);
|
||||
static int cfunc_match(const void *key1, const void *key2, Size keysize);
|
||||
|
||||
|
||||
/*
|
||||
* Initialize the hash table on first use.
|
||||
*
|
||||
* The hash table will be in TopMemoryContext regardless of caller's context.
|
||||
*/
|
||||
static void
|
||||
cfunc_hashtable_init(void)
|
||||
{
|
||||
HASHCTL ctl;
|
||||
|
||||
/* don't allow double-initialization */
|
||||
Assert(cfunc_hashtable == NULL);
|
||||
|
||||
ctl.keysize = sizeof(CachedFunctionHashKey);
|
||||
ctl.entrysize = sizeof(CachedFunctionHashEntry);
|
||||
ctl.hash = cfunc_hash;
|
||||
ctl.match = cfunc_match;
|
||||
cfunc_hashtable = hash_create("Cached function hash",
|
||||
FUNCS_PER_USER,
|
||||
&ctl,
|
||||
HASH_ELEM | HASH_FUNCTION | HASH_COMPARE);
|
||||
}
|
||||
|
||||
/*
|
||||
* cfunc_hash: hash function for cfunc hash table
|
||||
*
|
||||
* We need special hash and match functions to deal with the optional
|
||||
* presence of a TupleDesc in the hash keys. As long as we have to do
|
||||
* that, we might as well also be smart about not comparing unused
|
||||
* elements of the argtypes arrays.
|
||||
*/
|
||||
static uint32
|
||||
cfunc_hash(const void *key, Size keysize)
|
||||
{
|
||||
const CachedFunctionHashKey *k = (const CachedFunctionHashKey *) key;
|
||||
uint32 h;
|
||||
|
||||
Assert(keysize == sizeof(CachedFunctionHashKey));
|
||||
/* Hash all the fixed fields except callResultType */
|
||||
h = DatumGetUInt32(hash_any((const unsigned char *) k,
|
||||
offsetof(CachedFunctionHashKey, callResultType)));
|
||||
/* Incorporate input argument types */
|
||||
if (k->nargs > 0)
|
||||
h = hash_combine(h,
|
||||
DatumGetUInt32(hash_any((const unsigned char *) k->argtypes,
|
||||
k->nargs * sizeof(Oid))));
|
||||
/* Incorporate callResultType if present */
|
||||
if (k->callResultType)
|
||||
h = hash_combine(h, hashRowType(k->callResultType));
|
||||
return h;
|
||||
}
|
||||
|
||||
/*
|
||||
* cfunc_match: match function to use with cfunc_hash
|
||||
*/
|
||||
static int
|
||||
cfunc_match(const void *key1, const void *key2, Size keysize)
|
||||
{
|
||||
const CachedFunctionHashKey *k1 = (const CachedFunctionHashKey *) key1;
|
||||
const CachedFunctionHashKey *k2 = (const CachedFunctionHashKey *) key2;
|
||||
|
||||
Assert(keysize == sizeof(CachedFunctionHashKey));
|
||||
/* Compare all the fixed fields except callResultType */
|
||||
if (memcmp(k1, k2, offsetof(CachedFunctionHashKey, callResultType)) != 0)
|
||||
return 1; /* not equal */
|
||||
/* Compare input argument types (we just verified that nargs matches) */
|
||||
if (k1->nargs > 0 &&
|
||||
memcmp(k1->argtypes, k2->argtypes, k1->nargs * sizeof(Oid)) != 0)
|
||||
return 1; /* not equal */
|
||||
/* Compare callResultType */
|
||||
if (k1->callResultType)
|
||||
{
|
||||
if (k2->callResultType)
|
||||
{
|
||||
if (!equalRowTypes(k1->callResultType, k2->callResultType))
|
||||
return 1; /* not equal */
|
||||
}
|
||||
else
|
||||
return 1; /* not equal */
|
||||
}
|
||||
else
|
||||
{
|
||||
if (k2->callResultType)
|
||||
return 1; /* not equal */
|
||||
}
|
||||
return 0; /* equal */
|
||||
}
|
||||
|
||||
/*
|
||||
* Look up the CachedFunction for the given hash key.
|
||||
* Returns NULL if not present.
|
||||
*/
|
||||
static CachedFunction *
|
||||
cfunc_hashtable_lookup(CachedFunctionHashKey *func_key)
|
||||
{
|
||||
CachedFunctionHashEntry *hentry;
|
||||
|
||||
if (cfunc_hashtable == NULL)
|
||||
return NULL;
|
||||
|
||||
hentry = (CachedFunctionHashEntry *) hash_search(cfunc_hashtable,
|
||||
func_key,
|
||||
HASH_FIND,
|
||||
NULL);
|
||||
if (hentry)
|
||||
return hentry->function;
|
||||
else
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/*
|
||||
* Insert a hash table entry.
|
||||
*/
|
||||
static void
|
||||
cfunc_hashtable_insert(CachedFunction *function,
|
||||
CachedFunctionHashKey *func_key)
|
||||
{
|
||||
CachedFunctionHashEntry *hentry;
|
||||
bool found;
|
||||
|
||||
if (cfunc_hashtable == NULL)
|
||||
cfunc_hashtable_init();
|
||||
|
||||
hentry = (CachedFunctionHashEntry *) hash_search(cfunc_hashtable,
|
||||
func_key,
|
||||
HASH_ENTER,
|
||||
&found);
|
||||
if (found)
|
||||
elog(WARNING, "trying to insert a function that already exists");
|
||||
|
||||
/*
|
||||
* If there's a callResultType, copy it into TopMemoryContext. If we're
|
||||
* unlucky enough for that to fail, leave the entry with null
|
||||
* callResultType, which will probably never match anything.
|
||||
*/
|
||||
if (func_key->callResultType)
|
||||
{
|
||||
MemoryContext oldcontext = MemoryContextSwitchTo(TopMemoryContext);
|
||||
|
||||
hentry->key.callResultType = NULL;
|
||||
hentry->key.callResultType = CreateTupleDescCopy(func_key->callResultType);
|
||||
MemoryContextSwitchTo(oldcontext);
|
||||
}
|
||||
|
||||
hentry->function = function;
|
||||
|
||||
/* Set back-link from function to hashtable key */
|
||||
function->fn_hashkey = &hentry->key;
|
||||
}
|
||||
|
||||
/*
|
||||
* Delete a hash table entry.
|
||||
*/
|
||||
static void
|
||||
cfunc_hashtable_delete(CachedFunction *function)
|
||||
{
|
||||
CachedFunctionHashEntry *hentry;
|
||||
TupleDesc tupdesc;
|
||||
|
||||
/* do nothing if not in table */
|
||||
if (function->fn_hashkey == NULL)
|
||||
return;
|
||||
|
||||
/*
|
||||
* We need to free the callResultType if present, which is slightly tricky
|
||||
* because it has to be valid during the hashtable search. Fortunately,
|
||||
* because we have the hashkey back-link, we can grab that pointer before
|
||||
* deleting the hashtable entry.
|
||||
*/
|
||||
tupdesc = function->fn_hashkey->callResultType;
|
||||
|
||||
hentry = (CachedFunctionHashEntry *) hash_search(cfunc_hashtable,
|
||||
function->fn_hashkey,
|
||||
HASH_REMOVE,
|
||||
NULL);
|
||||
if (hentry == NULL)
|
||||
elog(WARNING, "trying to delete function that does not exist");
|
||||
|
||||
/* Remove back link, which no longer points to allocated storage */
|
||||
function->fn_hashkey = NULL;
|
||||
|
||||
/* Release the callResultType if present */
|
||||
if (tupdesc)
|
||||
FreeTupleDesc(tupdesc);
|
||||
}
|
||||
|
||||
/*
|
||||
* Compute the hashkey for a given function invocation
|
||||
*
|
||||
* The hashkey is returned into the caller-provided storage at *hashkey.
|
||||
* Note however that if a callResultType is incorporated, we've not done
|
||||
* anything about copying that.
|
||||
*/
|
||||
static void
|
||||
compute_function_hashkey(FunctionCallInfo fcinfo,
|
||||
Form_pg_proc procStruct,
|
||||
CachedFunctionHashKey *hashkey,
|
||||
Size cacheEntrySize,
|
||||
bool includeResultType,
|
||||
bool forValidator)
|
||||
{
|
||||
/* Make sure pad bytes within fixed part of the struct are zero */
|
||||
memset(hashkey, 0, offsetof(CachedFunctionHashKey, argtypes));
|
||||
|
||||
/* get function OID */
|
||||
hashkey->funcOid = fcinfo->flinfo->fn_oid;
|
||||
|
||||
/* get call context */
|
||||
hashkey->isTrigger = CALLED_AS_TRIGGER(fcinfo);
|
||||
hashkey->isEventTrigger = CALLED_AS_EVENT_TRIGGER(fcinfo);
|
||||
|
||||
/* record cacheEntrySize so multiple languages can share hash table */
|
||||
hashkey->cacheEntrySize = cacheEntrySize;
|
||||
|
||||
/*
|
||||
* If DML trigger, include trigger's OID in the hash, so that each trigger
|
||||
* usage gets a different hash entry, allowing for e.g. different relation
|
||||
* rowtypes or transition table names. In validation mode we do not know
|
||||
* what relation or transition table names are intended to be used, so we
|
||||
* leave trigOid zero; the hash entry built in this case will never be
|
||||
* used for any actual calls.
|
||||
*
|
||||
* We don't currently need to distinguish different event trigger usages
|
||||
* in the same way, since the special parameter variables don't vary in
|
||||
* type in that case.
|
||||
*/
|
||||
if (hashkey->isTrigger && !forValidator)
|
||||
{
|
||||
TriggerData *trigdata = (TriggerData *) fcinfo->context;
|
||||
|
||||
hashkey->trigOid = trigdata->tg_trigger->tgoid;
|
||||
}
|
||||
|
||||
/* get input collation, if known */
|
||||
hashkey->inputCollation = fcinfo->fncollation;
|
||||
|
||||
/*
|
||||
* We include only input arguments in the hash key, since output argument
|
||||
* types can be deduced from those, and it would require extra cycles to
|
||||
* include the output arguments. But we have to resolve any polymorphic
|
||||
* argument types to the real types for the call.
|
||||
*/
|
||||
if (procStruct->pronargs > 0)
|
||||
{
|
||||
hashkey->nargs = procStruct->pronargs;
|
||||
memcpy(hashkey->argtypes, procStruct->proargtypes.values,
|
||||
procStruct->pronargs * sizeof(Oid));
|
||||
cfunc_resolve_polymorphic_argtypes(procStruct->pronargs,
|
||||
hashkey->argtypes,
|
||||
NULL, /* all args are inputs */
|
||||
fcinfo->flinfo->fn_expr,
|
||||
forValidator,
|
||||
NameStr(procStruct->proname));
|
||||
}
|
||||
|
||||
/*
|
||||
* While regular OUT arguments are sufficiently represented by the
|
||||
* resolved input arguments, a function returning composite has additional
|
||||
* variability: ALTER TABLE/ALTER TYPE could affect what it returns. Also,
|
||||
* a function returning RECORD may depend on a column definition list to
|
||||
* determine its output rowtype. If the caller needs the exact result
|
||||
* type to be part of the hash lookup key, we must run
|
||||
* get_call_result_type() to find that out.
|
||||
*/
|
||||
if (includeResultType)
|
||||
{
|
||||
Oid resultTypeId;
|
||||
TupleDesc tupdesc;
|
||||
|
||||
switch (get_call_result_type(fcinfo, &resultTypeId, &tupdesc))
|
||||
{
|
||||
case TYPEFUNC_COMPOSITE:
|
||||
case TYPEFUNC_COMPOSITE_DOMAIN:
|
||||
hashkey->callResultType = tupdesc;
|
||||
break;
|
||||
default:
|
||||
/* scalar result, or indeterminate rowtype */
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* This is the same as the standard resolve_polymorphic_argtypes() function,
|
||||
* except that:
|
||||
* 1. We go ahead and report the error if we can't resolve the types.
|
||||
* 2. We treat RECORD-type input arguments (not output arguments) as if
|
||||
* they were polymorphic, replacing their types with the actual input
|
||||
* types if we can determine those. This allows us to create a separate
|
||||
* function cache entry for each named composite type passed to such an
|
||||
* argument.
|
||||
* 3. In validation mode, we have no inputs to look at, so assume that
|
||||
* polymorphic arguments are integer, integer-array or integer-range.
|
||||
*/
|
||||
void
|
||||
cfunc_resolve_polymorphic_argtypes(int numargs,
|
||||
Oid *argtypes, char *argmodes,
|
||||
Node *call_expr, bool forValidator,
|
||||
const char *proname)
|
||||
{
|
||||
int i;
|
||||
|
||||
if (!forValidator)
|
||||
{
|
||||
int inargno;
|
||||
|
||||
/* normal case, pass to standard routine */
|
||||
if (!resolve_polymorphic_argtypes(numargs, argtypes, argmodes,
|
||||
call_expr))
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
|
||||
errmsg("could not determine actual argument "
|
||||
"type for polymorphic function \"%s\"",
|
||||
proname)));
|
||||
/* also, treat RECORD inputs (but not outputs) as polymorphic */
|
||||
inargno = 0;
|
||||
for (i = 0; i < numargs; i++)
|
||||
{
|
||||
char argmode = argmodes ? argmodes[i] : PROARGMODE_IN;
|
||||
|
||||
if (argmode == PROARGMODE_OUT || argmode == PROARGMODE_TABLE)
|
||||
continue;
|
||||
if (argtypes[i] == RECORDOID || argtypes[i] == RECORDARRAYOID)
|
||||
{
|
||||
Oid resolvedtype = get_call_expr_argtype(call_expr,
|
||||
inargno);
|
||||
|
||||
if (OidIsValid(resolvedtype))
|
||||
argtypes[i] = resolvedtype;
|
||||
}
|
||||
inargno++;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
/* special validation case (no need to do anything for RECORD) */
|
||||
for (i = 0; i < numargs; i++)
|
||||
{
|
||||
switch (argtypes[i])
|
||||
{
|
||||
case ANYELEMENTOID:
|
||||
case ANYNONARRAYOID:
|
||||
case ANYENUMOID: /* XXX dubious */
|
||||
case ANYCOMPATIBLEOID:
|
||||
case ANYCOMPATIBLENONARRAYOID:
|
||||
argtypes[i] = INT4OID;
|
||||
break;
|
||||
case ANYARRAYOID:
|
||||
case ANYCOMPATIBLEARRAYOID:
|
||||
argtypes[i] = INT4ARRAYOID;
|
||||
break;
|
||||
case ANYRANGEOID:
|
||||
case ANYCOMPATIBLERANGEOID:
|
||||
argtypes[i] = INT4RANGEOID;
|
||||
break;
|
||||
case ANYMULTIRANGEOID:
|
||||
argtypes[i] = INT4MULTIRANGEOID;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* delete_function - clean up as much as possible of a stale function cache
|
||||
*
|
||||
* We can't release the CachedFunction struct itself, because of the
|
||||
* possibility that there are fn_extra pointers to it. We can release
|
||||
* the subsidiary storage, but only if there are no active evaluations
|
||||
* in progress. Otherwise we'll just leak that storage. Since the
|
||||
* case would only occur if a pg_proc update is detected during a nested
|
||||
* recursive call on the function, a leak seems acceptable.
|
||||
*
|
||||
* Note that this can be called more than once if there are multiple fn_extra
|
||||
* pointers to the same function cache. Hence be careful not to do things
|
||||
* twice.
|
||||
*/
|
||||
static void
|
||||
delete_function(CachedFunction *func)
|
||||
{
|
||||
/* remove function from hash table (might be done already) */
|
||||
cfunc_hashtable_delete(func);
|
||||
|
||||
/* release the function's storage if safe and not done already */
|
||||
if (func->use_count == 0 &&
|
||||
func->dcallback != NULL)
|
||||
{
|
||||
func->dcallback(func);
|
||||
func->dcallback = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Compile a cached function, if no existing cache entry is suitable.
|
||||
*
|
||||
* fcinfo is the current call information.
|
||||
*
|
||||
* function should be NULL or the result of a previous call of
|
||||
* cached_function_compile() for the same fcinfo. The caller will
|
||||
* typically save the result in fcinfo->flinfo->fn_extra, or in a
|
||||
* field of a struct pointed to by fn_extra, to re-use in later
|
||||
* calls within the same query.
|
||||
*
|
||||
* ccallback and dcallback are function-language-specific callbacks to
|
||||
* compile and delete a cached function entry. dcallback can be NULL
|
||||
* if there's nothing for it to do.
|
||||
*
|
||||
* cacheEntrySize is the function-language-specific size of the cache entry
|
||||
* (which embeds a CachedFunction struct and typically has many more fields
|
||||
* after that).
|
||||
*
|
||||
* If includeResultType is true and the function returns composite,
|
||||
* include the actual result descriptor in the cache lookup key.
|
||||
*
|
||||
* If forValidator is true, we're only compiling for validation purposes,
|
||||
* and so some checks are skipped.
|
||||
*
|
||||
* Note: it's important for this to fall through quickly if the function
|
||||
* has already been compiled.
|
||||
*
|
||||
* Note: this function leaves the "use_count" field as zero. The caller
|
||||
* is expected to increment the use_count and decrement it when done with
|
||||
* the cache entry.
|
||||
*/
|
||||
CachedFunction *
|
||||
cached_function_compile(FunctionCallInfo fcinfo,
|
||||
CachedFunction *function,
|
||||
CachedFunctionCompileCallback ccallback,
|
||||
CachedFunctionDeleteCallback dcallback,
|
||||
Size cacheEntrySize,
|
||||
bool includeResultType,
|
||||
bool forValidator)
|
||||
{
|
||||
Oid funcOid = fcinfo->flinfo->fn_oid;
|
||||
HeapTuple procTup;
|
||||
Form_pg_proc procStruct;
|
||||
CachedFunctionHashKey hashkey;
|
||||
bool function_valid = false;
|
||||
bool hashkey_valid = false;
|
||||
|
||||
/*
|
||||
* Lookup the pg_proc tuple by Oid; we'll need it in any case
|
||||
*/
|
||||
procTup = SearchSysCache1(PROCOID, ObjectIdGetDatum(funcOid));
|
||||
if (!HeapTupleIsValid(procTup))
|
||||
elog(ERROR, "cache lookup failed for function %u", funcOid);
|
||||
procStruct = (Form_pg_proc) GETSTRUCT(procTup);
|
||||
|
||||
/*
|
||||
* Do we already have a cache entry for the current FmgrInfo? If not, try
|
||||
* to find one in the hash table.
|
||||
*/
|
||||
recheck:
|
||||
if (!function)
|
||||
{
|
||||
/* Compute hashkey using function signature and actual arg types */
|
||||
compute_function_hashkey(fcinfo, procStruct, &hashkey,
|
||||
cacheEntrySize, includeResultType,
|
||||
forValidator);
|
||||
hashkey_valid = true;
|
||||
|
||||
/* And do the lookup */
|
||||
function = cfunc_hashtable_lookup(&hashkey);
|
||||
}
|
||||
|
||||
if (function)
|
||||
{
|
||||
/* We have a compiled function, but is it still valid? */
|
||||
if (function->fn_xmin == HeapTupleHeaderGetRawXmin(procTup->t_data) &&
|
||||
ItemPointerEquals(&function->fn_tid, &procTup->t_self))
|
||||
function_valid = true;
|
||||
else
|
||||
{
|
||||
/*
|
||||
* Nope, so remove it from hashtable and try to drop associated
|
||||
* storage (if not done already).
|
||||
*/
|
||||
delete_function(function);
|
||||
|
||||
/*
|
||||
* If the function isn't in active use then we can overwrite the
|
||||
* func struct with new data, allowing any other existing fn_extra
|
||||
* pointers to make use of the new definition on their next use.
|
||||
* If it is in use then just leave it alone and make a new one.
|
||||
* (The active invocations will run to completion using the
|
||||
* previous definition, and then the cache entry will just be
|
||||
* leaked; doesn't seem worth adding code to clean it up, given
|
||||
* what a corner case this is.)
|
||||
*
|
||||
* If we found the function struct via fn_extra then it's possible
|
||||
* a replacement has already been made, so go back and recheck the
|
||||
* hashtable.
|
||||
*/
|
||||
if (function->use_count != 0)
|
||||
{
|
||||
function = NULL;
|
||||
if (!hashkey_valid)
|
||||
goto recheck;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* If the function wasn't found or was out-of-date, we have to compile it.
|
||||
*/
|
||||
if (!function_valid)
|
||||
{
|
||||
/*
|
||||
* Calculate hashkey if we didn't already; we'll need it to store the
|
||||
* completed function.
|
||||
*/
|
||||
if (!hashkey_valid)
|
||||
compute_function_hashkey(fcinfo, procStruct, &hashkey,
|
||||
cacheEntrySize, includeResultType,
|
||||
forValidator);
|
||||
|
||||
/*
|
||||
* Create the new function struct, if not done already. The function
|
||||
* structs are never thrown away, so keep them in TopMemoryContext.
|
||||
*/
|
||||
Assert(cacheEntrySize >= sizeof(CachedFunction));
|
||||
if (function == NULL)
|
||||
{
|
||||
function = (CachedFunction *)
|
||||
MemoryContextAllocZero(TopMemoryContext, cacheEntrySize);
|
||||
}
|
||||
else
|
||||
{
|
||||
/* re-using a previously existing struct, so clear it out */
|
||||
memset(function, 0, cacheEntrySize);
|
||||
}
|
||||
|
||||
/*
|
||||
* Fill in the CachedFunction part. fn_hashkey and use_count remain
|
||||
* zeroes for now.
|
||||
*/
|
||||
function->fn_xmin = HeapTupleHeaderGetRawXmin(procTup->t_data);
|
||||
function->fn_tid = procTup->t_self;
|
||||
function->dcallback = dcallback;
|
||||
|
||||
/*
|
||||
* Do the hard, language-specific part.
|
||||
*/
|
||||
ccallback(fcinfo, procTup, &hashkey, function, forValidator);
|
||||
|
||||
/*
|
||||
* Add the completed struct to the hash table.
|
||||
*/
|
||||
cfunc_hashtable_insert(function, &hashkey);
|
||||
}
|
||||
|
||||
ReleaseSysCache(procTup);
|
||||
|
||||
/*
|
||||
* Finally return the compiled function
|
||||
*/
|
||||
return function;
|
||||
}
|
1
src/backend/utils/cache/meson.build
vendored
1
src/backend/utils/cache/meson.build
vendored
@ -4,6 +4,7 @@ backend_sources += files(
|
||||
'attoptcache.c',
|
||||
'catcache.c',
|
||||
'evtcache.c',
|
||||
'funccache.c',
|
||||
'inval.c',
|
||||
'lsyscache.c',
|
||||
'partcache.c',
|
||||
|
169
src/backend/utils/cache/plancache.c
vendored
169
src/backend/utils/cache/plancache.c
vendored
@ -14,7 +14,7 @@
|
||||
* Cache invalidation is driven off sinval events. Any CachedPlanSource
|
||||
* that matches the event is marked invalid, as is its generic CachedPlan
|
||||
* if it has one. When (and if) the next demand for a cached plan occurs,
|
||||
* parse analysis and rewrite is repeated to build a new valid query tree,
|
||||
* parse analysis and/or rewrite is repeated to build a new valid query tree,
|
||||
* and then planning is performed as normal. We also force re-analysis and
|
||||
* re-planning if the active search_path is different from the previous time
|
||||
* or, if RLS is involved, if the user changes or the RLS environment changes.
|
||||
@ -63,6 +63,7 @@
|
||||
#include "nodes/nodeFuncs.h"
|
||||
#include "optimizer/optimizer.h"
|
||||
#include "parser/analyze.h"
|
||||
#include "rewrite/rewriteHandler.h"
|
||||
#include "storage/lmgr.h"
|
||||
#include "tcop/pquery.h"
|
||||
#include "tcop/utility.h"
|
||||
@ -74,18 +75,6 @@
|
||||
#include "utils/syscache.h"
|
||||
|
||||
|
||||
/*
|
||||
* We must skip "overhead" operations that involve database access when the
|
||||
* cached plan's subject statement is a transaction control command or one
|
||||
* that requires a snapshot not to be set yet (such as SET or LOCK). More
|
||||
* generally, statements that do not require parse analysis/rewrite/plan
|
||||
* activity never need to be revalidated, so we can treat them all like that.
|
||||
* For the convenience of postgres.c, treat empty statements that way too.
|
||||
*/
|
||||
#define StmtPlanRequiresRevalidation(plansource) \
|
||||
((plansource)->raw_parse_tree != NULL && \
|
||||
stmt_requires_parse_analysis((plansource)->raw_parse_tree))
|
||||
|
||||
/*
|
||||
* This is the head of the backend's list of "saved" CachedPlanSources (i.e.,
|
||||
* those that are in long-lived storage and are examined for sinval events).
|
||||
@ -100,6 +89,8 @@ static dlist_head saved_plan_list = DLIST_STATIC_INIT(saved_plan_list);
|
||||
static dlist_head cached_expression_list = DLIST_STATIC_INIT(cached_expression_list);
|
||||
|
||||
static void ReleaseGenericPlan(CachedPlanSource *plansource);
|
||||
static bool StmtPlanRequiresRevalidation(CachedPlanSource *plansource);
|
||||
static bool BuildingPlanRequiresSnapshot(CachedPlanSource *plansource);
|
||||
static List *RevalidateCachedQuery(CachedPlanSource *plansource,
|
||||
QueryEnvironment *queryEnv,
|
||||
bool release_generic);
|
||||
@ -166,7 +157,7 @@ InitPlanCache(void)
|
||||
}
|
||||
|
||||
/*
|
||||
* CreateCachedPlan: initially create a plan cache entry.
|
||||
* CreateCachedPlan: initially create a plan cache entry for a raw parse tree.
|
||||
*
|
||||
* Creation of a cached plan is divided into two steps, CreateCachedPlan and
|
||||
* CompleteCachedPlan. CreateCachedPlan should be called after running the
|
||||
@ -220,6 +211,7 @@ CreateCachedPlan(RawStmt *raw_parse_tree,
|
||||
plansource = (CachedPlanSource *) palloc0(sizeof(CachedPlanSource));
|
||||
plansource->magic = CACHEDPLANSOURCE_MAGIC;
|
||||
plansource->raw_parse_tree = copyObject(raw_parse_tree);
|
||||
plansource->analyzed_parse_tree = NULL;
|
||||
plansource->query_string = pstrdup(query_string);
|
||||
MemoryContextSetIdentifier(source_context, plansource->query_string);
|
||||
plansource->commandTag = commandTag;
|
||||
@ -227,6 +219,8 @@ CreateCachedPlan(RawStmt *raw_parse_tree,
|
||||
plansource->num_params = 0;
|
||||
plansource->parserSetup = NULL;
|
||||
plansource->parserSetupArg = NULL;
|
||||
plansource->postRewrite = NULL;
|
||||
plansource->postRewriteArg = NULL;
|
||||
plansource->cursor_options = 0;
|
||||
plansource->fixed_result = false;
|
||||
plansource->resultDesc = NULL;
|
||||
@ -255,6 +249,34 @@ CreateCachedPlan(RawStmt *raw_parse_tree,
|
||||
return plansource;
|
||||
}
|
||||
|
||||
/*
|
||||
* CreateCachedPlanForQuery: initially create a plan cache entry for a Query.
|
||||
*
|
||||
* This is used in the same way as CreateCachedPlan, except that the source
|
||||
* query has already been through parse analysis, and the plancache will never
|
||||
* try to re-do that step.
|
||||
*
|
||||
* Currently this is used only for new-style SQL functions, where we have a
|
||||
* Query from the function's prosqlbody, but no source text. The query_string
|
||||
* is typically empty, but is required anyway.
|
||||
*/
|
||||
CachedPlanSource *
|
||||
CreateCachedPlanForQuery(Query *analyzed_parse_tree,
|
||||
const char *query_string,
|
||||
CommandTag commandTag)
|
||||
{
|
||||
CachedPlanSource *plansource;
|
||||
MemoryContext oldcxt;
|
||||
|
||||
/* Rather than duplicating CreateCachedPlan, just do this: */
|
||||
plansource = CreateCachedPlan(NULL, query_string, commandTag);
|
||||
oldcxt = MemoryContextSwitchTo(plansource->context);
|
||||
plansource->analyzed_parse_tree = copyObject(analyzed_parse_tree);
|
||||
MemoryContextSwitchTo(oldcxt);
|
||||
|
||||
return plansource;
|
||||
}
|
||||
|
||||
/*
|
||||
* CreateOneShotCachedPlan: initially create a one-shot plan cache entry.
|
||||
*
|
||||
@ -289,12 +311,15 @@ CreateOneShotCachedPlan(RawStmt *raw_parse_tree,
|
||||
plansource = (CachedPlanSource *) palloc0(sizeof(CachedPlanSource));
|
||||
plansource->magic = CACHEDPLANSOURCE_MAGIC;
|
||||
plansource->raw_parse_tree = raw_parse_tree;
|
||||
plansource->analyzed_parse_tree = NULL;
|
||||
plansource->query_string = query_string;
|
||||
plansource->commandTag = commandTag;
|
||||
plansource->param_types = NULL;
|
||||
plansource->num_params = 0;
|
||||
plansource->parserSetup = NULL;
|
||||
plansource->parserSetupArg = NULL;
|
||||
plansource->postRewrite = NULL;
|
||||
plansource->postRewriteArg = NULL;
|
||||
plansource->cursor_options = 0;
|
||||
plansource->fixed_result = false;
|
||||
plansource->resultDesc = NULL;
|
||||
@ -464,6 +489,29 @@ CompleteCachedPlan(CachedPlanSource *plansource,
|
||||
plansource->is_valid = true;
|
||||
}
|
||||
|
||||
/*
|
||||
* SetPostRewriteHook: set a hook to modify post-rewrite query trees
|
||||
*
|
||||
* Some callers have a need to modify the query trees between rewriting and
|
||||
* planning. In the initial call to CompleteCachedPlan, it's assumed such
|
||||
* work was already done on the querytree_list. However, if we're forced
|
||||
* to replan, it will need to be done over. The caller can set this hook
|
||||
* to provide code to make that happen.
|
||||
*
|
||||
* postRewriteArg is just passed verbatim to the hook. As with parserSetupArg,
|
||||
* it is caller's responsibility that the referenced data remains
|
||||
* valid for as long as the CachedPlanSource exists.
|
||||
*/
|
||||
void
|
||||
SetPostRewriteHook(CachedPlanSource *plansource,
|
||||
PostRewriteHook postRewrite,
|
||||
void *postRewriteArg)
|
||||
{
|
||||
Assert(plansource->magic == CACHEDPLANSOURCE_MAGIC);
|
||||
plansource->postRewrite = postRewrite;
|
||||
plansource->postRewriteArg = postRewriteArg;
|
||||
}
|
||||
|
||||
/*
|
||||
* SaveCachedPlan: save a cached plan permanently
|
||||
*
|
||||
@ -566,6 +614,42 @@ ReleaseGenericPlan(CachedPlanSource *plansource)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* We must skip "overhead" operations that involve database access when the
|
||||
* cached plan's subject statement is a transaction control command or one
|
||||
* that requires a snapshot not to be set yet (such as SET or LOCK). More
|
||||
* generally, statements that do not require parse analysis/rewrite/plan
|
||||
* activity never need to be revalidated, so we can treat them all like that.
|
||||
* For the convenience of postgres.c, treat empty statements that way too.
|
||||
*/
|
||||
static bool
|
||||
StmtPlanRequiresRevalidation(CachedPlanSource *plansource)
|
||||
{
|
||||
if (plansource->raw_parse_tree != NULL)
|
||||
return stmt_requires_parse_analysis(plansource->raw_parse_tree);
|
||||
else if (plansource->analyzed_parse_tree != NULL)
|
||||
return query_requires_rewrite_plan(plansource->analyzed_parse_tree);
|
||||
/* empty query never needs revalidation */
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* Determine if creating a plan for this CachedPlanSource requires a snapshot.
|
||||
* In fact this function matches StmtPlanRequiresRevalidation(), but we want
|
||||
* to preserve the distinction between stmt_requires_parse_analysis() and
|
||||
* analyze_requires_snapshot().
|
||||
*/
|
||||
static bool
|
||||
BuildingPlanRequiresSnapshot(CachedPlanSource *plansource)
|
||||
{
|
||||
if (plansource->raw_parse_tree != NULL)
|
||||
return analyze_requires_snapshot(plansource->raw_parse_tree);
|
||||
else if (plansource->analyzed_parse_tree != NULL)
|
||||
return query_requires_rewrite_plan(plansource->analyzed_parse_tree);
|
||||
/* empty query never needs a snapshot */
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* RevalidateCachedQuery: ensure validity of analyzed-and-rewritten query tree.
|
||||
*
|
||||
@ -592,7 +676,6 @@ RevalidateCachedQuery(CachedPlanSource *plansource,
|
||||
bool release_generic)
|
||||
{
|
||||
bool snapshot_set;
|
||||
RawStmt *rawtree;
|
||||
List *tlist; /* transient query-tree list */
|
||||
List *qlist; /* permanent query-tree list */
|
||||
TupleDesc resultDesc;
|
||||
@ -615,7 +698,10 @@ RevalidateCachedQuery(CachedPlanSource *plansource,
|
||||
/*
|
||||
* If the query is currently valid, we should have a saved search_path ---
|
||||
* check to see if that matches the current environment. If not, we want
|
||||
* to force replan.
|
||||
* to force replan. (We could almost ignore this consideration when
|
||||
* working from an analyzed parse tree; but there are scenarios where
|
||||
* planning can have search_path-dependent results, for example if it
|
||||
* inlines an old-style SQL function.)
|
||||
*/
|
||||
if (plansource->is_valid)
|
||||
{
|
||||
@ -662,9 +748,9 @@ RevalidateCachedQuery(CachedPlanSource *plansource,
|
||||
}
|
||||
|
||||
/*
|
||||
* Discard the no-longer-useful query tree. (Note: we don't want to do
|
||||
* this any earlier, else we'd not have been able to release locks
|
||||
* correctly in the race condition case.)
|
||||
* Discard the no-longer-useful rewritten query tree. (Note: we don't
|
||||
* want to do this any earlier, else we'd not have been able to release
|
||||
* locks correctly in the race condition case.)
|
||||
*/
|
||||
plansource->is_valid = false;
|
||||
plansource->query_list = NIL;
|
||||
@ -711,14 +797,19 @@ RevalidateCachedQuery(CachedPlanSource *plansource,
|
||||
}
|
||||
|
||||
/*
|
||||
* Run parse analysis and rule rewriting. The parser tends to scribble on
|
||||
* its input, so we must copy the raw parse tree to prevent corruption of
|
||||
* the cache.
|
||||
* Run parse analysis (if needed) and rule rewriting.
|
||||
*/
|
||||
if (plansource->raw_parse_tree != NULL)
|
||||
{
|
||||
/* Source is raw parse tree */
|
||||
RawStmt *rawtree;
|
||||
|
||||
/*
|
||||
* The parser tends to scribble on its input, so we must copy the raw
|
||||
* parse tree to prevent corruption of the cache.
|
||||
*/
|
||||
rawtree = copyObject(plansource->raw_parse_tree);
|
||||
if (rawtree == NULL)
|
||||
tlist = NIL;
|
||||
else if (plansource->parserSetup != NULL)
|
||||
if (plansource->parserSetup != NULL)
|
||||
tlist = pg_analyze_and_rewrite_withcb(rawtree,
|
||||
plansource->query_string,
|
||||
plansource->parserSetup,
|
||||
@ -730,6 +821,28 @@ RevalidateCachedQuery(CachedPlanSource *plansource,
|
||||
plansource->param_types,
|
||||
plansource->num_params,
|
||||
queryEnv);
|
||||
}
|
||||
else if (plansource->analyzed_parse_tree != NULL)
|
||||
{
|
||||
/* Source is pre-analyzed query, so we only need to rewrite */
|
||||
Query *analyzed_tree;
|
||||
|
||||
/* The rewriter scribbles on its input, too, so copy */
|
||||
analyzed_tree = copyObject(plansource->analyzed_parse_tree);
|
||||
/* Acquire locks needed before rewriting ... */
|
||||
AcquireRewriteLocks(analyzed_tree, true, false);
|
||||
/* ... and do it */
|
||||
tlist = pg_rewrite_query(analyzed_tree);
|
||||
}
|
||||
else
|
||||
{
|
||||
/* Empty query, nothing to do */
|
||||
tlist = NIL;
|
||||
}
|
||||
|
||||
/* Apply post-rewrite callback if there is one */
|
||||
if (plansource->postRewrite != NULL)
|
||||
plansource->postRewrite(tlist, plansource->postRewriteArg);
|
||||
|
||||
/* Release snapshot if we got one */
|
||||
if (snapshot_set)
|
||||
@ -963,8 +1076,7 @@ BuildCachedPlan(CachedPlanSource *plansource, List *qlist,
|
||||
*/
|
||||
snapshot_set = false;
|
||||
if (!ActiveSnapshotSet() &&
|
||||
plansource->raw_parse_tree &&
|
||||
analyze_requires_snapshot(plansource->raw_parse_tree))
|
||||
BuildingPlanRequiresSnapshot(plansource))
|
||||
{
|
||||
PushActiveSnapshot(GetTransactionSnapshot());
|
||||
snapshot_set = true;
|
||||
@ -1703,6 +1815,7 @@ CopyCachedPlan(CachedPlanSource *plansource)
|
||||
newsource = (CachedPlanSource *) palloc0(sizeof(CachedPlanSource));
|
||||
newsource->magic = CACHEDPLANSOURCE_MAGIC;
|
||||
newsource->raw_parse_tree = copyObject(plansource->raw_parse_tree);
|
||||
newsource->analyzed_parse_tree = copyObject(plansource->analyzed_parse_tree);
|
||||
newsource->query_string = pstrdup(plansource->query_string);
|
||||
MemoryContextSetIdentifier(source_context, newsource->query_string);
|
||||
newsource->commandTag = plansource->commandTag;
|
||||
@ -1718,6 +1831,8 @@ CopyCachedPlan(CachedPlanSource *plansource)
|
||||
newsource->num_params = plansource->num_params;
|
||||
newsource->parserSetup = plansource->parserSetup;
|
||||
newsource->parserSetupArg = plansource->parserSetupArg;
|
||||
newsource->postRewrite = plansource->postRewrite;
|
||||
newsource->postRewriteArg = plansource->postRewriteArg;
|
||||
newsource->cursor_options = plansource->cursor_options;
|
||||
newsource->fixed_result = plansource->fixed_result;
|
||||
if (plansource->resultDesc)
|
||||
|
@ -48,8 +48,7 @@ extern void check_sql_fn_statements(List *queryTreeLists);
|
||||
extern bool check_sql_fn_retval(List *queryTreeLists,
|
||||
Oid rettype, TupleDesc rettupdesc,
|
||||
char prokind,
|
||||
bool insertDroppedCols,
|
||||
List **resultTargetList);
|
||||
bool insertDroppedCols);
|
||||
|
||||
extern DestReceiver *CreateSQLFunctionDestReceiver(void);
|
||||
|
||||
|
@ -52,6 +52,7 @@ extern Query *transformStmt(ParseState *pstate, Node *parseTree);
|
||||
|
||||
extern bool stmt_requires_parse_analysis(RawStmt *parseTree);
|
||||
extern bool analyze_requires_snapshot(RawStmt *parseTree);
|
||||
extern bool query_requires_rewrite_plan(Query *query);
|
||||
|
||||
extern const char *LCS_asString(LockClauseStrength strength);
|
||||
extern void CheckSelectLocking(Query *qry, LockClauseStrength strength);
|
||||
|
134
src/include/utils/funccache.h
Normal file
134
src/include/utils/funccache.h
Normal file
@ -0,0 +1,134 @@
|
||||
/*-------------------------------------------------------------------------
|
||||
*
|
||||
* funccache.h
|
||||
* Function cache definitions.
|
||||
*
|
||||
* See funccache.c for comments.
|
||||
*
|
||||
* Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
|
||||
* Portions Copyright (c) 1994, Regents of the University of California
|
||||
*
|
||||
* src/include/utils/funccache.h
|
||||
*
|
||||
*-------------------------------------------------------------------------
|
||||
*/
|
||||
#ifndef FUNCCACHE_H
|
||||
#define FUNCCACHE_H
|
||||
|
||||
#include "access/htup_details.h"
|
||||
#include "fmgr.h"
|
||||
#include "storage/itemptr.h"
|
||||
|
||||
struct CachedFunctionHashKey; /* forward references */
|
||||
struct CachedFunction;
|
||||
|
||||
/*
|
||||
* Callback that cached_function_compile() invokes when it's necessary to
|
||||
* compile a cached function. The callback must fill in *function (except
|
||||
* for the fields of struct CachedFunction), or throw an error if trouble.
|
||||
* fcinfo: current call information
|
||||
* procTup: function's pg_proc row from catcache
|
||||
* hashkey: hash key that will be used for the function
|
||||
* function: pre-zeroed workspace, of size passed to cached_function_compile()
|
||||
* forValidator: passed through from cached_function_compile()
|
||||
*/
|
||||
typedef void (*CachedFunctionCompileCallback) (FunctionCallInfo fcinfo,
|
||||
HeapTuple procTup,
|
||||
const struct CachedFunctionHashKey *hashkey,
|
||||
struct CachedFunction *function,
|
||||
bool forValidator);
|
||||
|
||||
/*
|
||||
* Callback called when discarding a cache entry. Free any free-able
|
||||
* subsidiary data of cfunc, but not the struct CachedFunction itself.
|
||||
*/
|
||||
typedef void (*CachedFunctionDeleteCallback) (struct CachedFunction *cfunc);
|
||||
|
||||
/*
|
||||
* Hash lookup key for functions. This must account for all aspects
|
||||
* of a specific call that might lead to different data types or
|
||||
* collations being used within the function.
|
||||
*/
|
||||
typedef struct CachedFunctionHashKey
|
||||
{
|
||||
Oid funcOid;
|
||||
|
||||
bool isTrigger; /* true if called as a DML trigger */
|
||||
bool isEventTrigger; /* true if called as an event trigger */
|
||||
|
||||
/* be careful that pad bytes in this struct get zeroed! */
|
||||
|
||||
/*
|
||||
* We include the language-specific size of the function's cache entry in
|
||||
* the cache key. This covers the case where CREATE OR REPLACE FUNCTION
|
||||
* is used to change the implementation language, and the new language
|
||||
* also uses funccache.c but needs a different-sized cache entry.
|
||||
*/
|
||||
Size cacheEntrySize;
|
||||
|
||||
/*
|
||||
* For a trigger function, the OID of the trigger is part of the hash key
|
||||
* --- we want to compile the trigger function separately for each trigger
|
||||
* it is used with, in case the rowtype or transition table names are
|
||||
* different. Zero if not called as a DML trigger.
|
||||
*/
|
||||
Oid trigOid;
|
||||
|
||||
/*
|
||||
* We must include the input collation as part of the hash key too,
|
||||
* because we have to generate different plans (with different Param
|
||||
* collations) for different collation settings.
|
||||
*/
|
||||
Oid inputCollation;
|
||||
|
||||
/* Number of arguments (counting input arguments only, ie pronargs) */
|
||||
int nargs;
|
||||
|
||||
/* If you change anything below here, fix hashing code in funccache.c! */
|
||||
|
||||
/*
|
||||
* If relevant, the result descriptor for a function returning composite.
|
||||
*/
|
||||
TupleDesc callResultType;
|
||||
|
||||
/*
|
||||
* Input argument types, with any polymorphic types resolved to actual
|
||||
* types. Only the first nargs entries are valid.
|
||||
*/
|
||||
Oid argtypes[FUNC_MAX_ARGS];
|
||||
} CachedFunctionHashKey;
|
||||
|
||||
/*
|
||||
* Representation of a compiled function. This struct contains just the
|
||||
* fields that funccache.c needs to deal with. It will typically be
|
||||
* embedded in a larger struct containing function-language-specific data.
|
||||
*/
|
||||
typedef struct CachedFunction
|
||||
{
|
||||
/* back-link to hashtable entry, or NULL if not in hash table */
|
||||
CachedFunctionHashKey *fn_hashkey;
|
||||
/* xmin and ctid of function's pg_proc row; used to detect invalidation */
|
||||
TransactionId fn_xmin;
|
||||
ItemPointerData fn_tid;
|
||||
/* deletion callback */
|
||||
CachedFunctionDeleteCallback dcallback;
|
||||
|
||||
/* this field changes when the function is used: */
|
||||
uint64 use_count;
|
||||
} CachedFunction;
|
||||
|
||||
extern CachedFunction *cached_function_compile(FunctionCallInfo fcinfo,
|
||||
CachedFunction *function,
|
||||
CachedFunctionCompileCallback ccallback,
|
||||
CachedFunctionDeleteCallback dcallback,
|
||||
Size cacheEntrySize,
|
||||
bool includeResultType,
|
||||
bool forValidator);
|
||||
extern void cfunc_resolve_polymorphic_argtypes(int numargs,
|
||||
Oid *argtypes,
|
||||
char *argmodes,
|
||||
Node *call_expr,
|
||||
bool forValidator,
|
||||
const char *proname);
|
||||
|
||||
#endif /* FUNCCACHE_H */
|
@ -25,7 +25,8 @@
|
||||
#include "utils/resowner.h"
|
||||
|
||||
|
||||
/* Forward declaration, to avoid including parsenodes.h here */
|
||||
/* Forward declarations, to avoid including parsenodes.h here */
|
||||
struct Query;
|
||||
struct RawStmt;
|
||||
|
||||
/* possible values for plan_cache_mode */
|
||||
@ -39,18 +40,31 @@ typedef enum
|
||||
/* GUC parameter */
|
||||
extern PGDLLIMPORT int plan_cache_mode;
|
||||
|
||||
/* Optional callback to editorialize on rewritten parse trees */
|
||||
typedef void (*PostRewriteHook) (List *querytree_list, void *arg);
|
||||
|
||||
#define CACHEDPLANSOURCE_MAGIC 195726186
|
||||
#define CACHEDPLAN_MAGIC 953717834
|
||||
#define CACHEDEXPR_MAGIC 838275847
|
||||
|
||||
/*
|
||||
* CachedPlanSource (which might better have been called CachedQuery)
|
||||
* represents a SQL query that we expect to use multiple times. It stores
|
||||
* the query source text, the raw parse tree, and the analyzed-and-rewritten
|
||||
* represents a SQL query that we expect to use multiple times. It stores the
|
||||
* query source text, the source parse tree, and the analyzed-and-rewritten
|
||||
* query tree, as well as adjunct data. Cache invalidation can happen as a
|
||||
* result of DDL affecting objects used by the query. In that case we discard
|
||||
* the analyzed-and-rewritten query tree, and rebuild it when next needed.
|
||||
*
|
||||
* There are two ways in which the source query can be represented: either
|
||||
* as a raw parse tree, or as an analyzed-but-not-rewritten parse tree.
|
||||
* In the latter case we expect that cache invalidation need not affect
|
||||
* the parse-analysis results, only the rewriting and planning steps.
|
||||
* Only one of raw_parse_tree and analyzed_parse_tree can be non-NULL.
|
||||
* (If both are NULL, the CachedPlanSource represents an empty query.)
|
||||
* Note that query_string is typically just an empty string when the
|
||||
* source query is an analyzed parse tree; also, param_types, num_params,
|
||||
* parserSetup, and parserSetupArg will not be used.
|
||||
*
|
||||
* An actual execution plan, represented by CachedPlan, is derived from the
|
||||
* CachedPlanSource when we need to execute the query. The plan could be
|
||||
* either generic (usable with any set of plan parameters) or custom (for a
|
||||
@ -78,7 +92,7 @@ extern PGDLLIMPORT int plan_cache_mode;
|
||||
* though it may be useful if the CachedPlan can be discarded early.)
|
||||
*
|
||||
* A CachedPlanSource has two associated memory contexts: one that holds the
|
||||
* struct itself, the query source text and the raw parse tree, and another
|
||||
* struct itself, the query source text and the source parse tree, and another
|
||||
* context that holds the rewritten query tree and associated data. This
|
||||
* allows the query tree to be discarded easily when it is invalidated.
|
||||
*
|
||||
@ -94,12 +108,15 @@ typedef struct CachedPlanSource
|
||||
{
|
||||
int magic; /* should equal CACHEDPLANSOURCE_MAGIC */
|
||||
struct RawStmt *raw_parse_tree; /* output of raw_parser(), or NULL */
|
||||
struct Query *analyzed_parse_tree; /* analyzed parse tree, or NULL */
|
||||
const char *query_string; /* source text of query */
|
||||
CommandTag commandTag; /* command tag for query */
|
||||
Oid *param_types; /* array of parameter type OIDs, or NULL */
|
||||
int num_params; /* length of param_types array */
|
||||
ParserSetupHook parserSetup; /* alternative parameter spec method */
|
||||
void *parserSetupArg;
|
||||
PostRewriteHook postRewrite; /* see SetPostRewriteHook */
|
||||
void *postRewriteArg;
|
||||
int cursor_options; /* cursor options used for planning */
|
||||
bool fixed_result; /* disallow change in result tupdesc? */
|
||||
TupleDesc resultDesc; /* result type; NULL = doesn't return tuples */
|
||||
@ -196,6 +213,9 @@ extern void ReleaseAllPlanCacheRefsInOwner(ResourceOwner owner);
|
||||
extern CachedPlanSource *CreateCachedPlan(struct RawStmt *raw_parse_tree,
|
||||
const char *query_string,
|
||||
CommandTag commandTag);
|
||||
extern CachedPlanSource *CreateCachedPlanForQuery(struct Query *analyzed_parse_tree,
|
||||
const char *query_string,
|
||||
CommandTag commandTag);
|
||||
extern CachedPlanSource *CreateOneShotCachedPlan(struct RawStmt *raw_parse_tree,
|
||||
const char *query_string,
|
||||
CommandTag commandTag);
|
||||
@ -208,6 +228,9 @@ extern void CompleteCachedPlan(CachedPlanSource *plansource,
|
||||
void *parserSetupArg,
|
||||
int cursor_options,
|
||||
bool fixed_result);
|
||||
extern void SetPostRewriteHook(CachedPlanSource *plansource,
|
||||
PostRewriteHook postRewrite,
|
||||
void *postRewriteArg);
|
||||
|
||||
extern void SaveCachedPlan(CachedPlanSource *plansource);
|
||||
extern void DropCachedPlan(CachedPlanSource *plansource);
|
||||
|
@ -52,20 +52,6 @@ PLpgSQL_function *plpgsql_curr_compile;
|
||||
/* A context appropriate for short-term allocs during compilation */
|
||||
MemoryContext plpgsql_compile_tmp_cxt;
|
||||
|
||||
/* ----------
|
||||
* Hash table for compiled functions
|
||||
* ----------
|
||||
*/
|
||||
static HTAB *plpgsql_HashTable = NULL;
|
||||
|
||||
typedef struct plpgsql_hashent
|
||||
{
|
||||
PLpgSQL_func_hashkey key;
|
||||
PLpgSQL_function *function;
|
||||
} plpgsql_HashEnt;
|
||||
|
||||
#define FUNCS_PER_USER 128 /* initial table size */
|
||||
|
||||
/* ----------
|
||||
* Lookup table for EXCEPTION condition names
|
||||
* ----------
|
||||
@ -86,10 +72,10 @@ static const ExceptionLabelMap exception_label_map[] = {
|
||||
* static prototypes
|
||||
* ----------
|
||||
*/
|
||||
static PLpgSQL_function *do_compile(FunctionCallInfo fcinfo,
|
||||
static void plpgsql_compile_callback(FunctionCallInfo fcinfo,
|
||||
HeapTuple procTup,
|
||||
PLpgSQL_function *function,
|
||||
PLpgSQL_func_hashkey *hashkey,
|
||||
const CachedFunctionHashKey *hashkey,
|
||||
CachedFunction *cfunc,
|
||||
bool forValidator);
|
||||
static void plpgsql_compile_error_callback(void *arg);
|
||||
static void add_parameter_name(PLpgSQL_nsitem_type itemtype, int itemno, const char *name);
|
||||
@ -105,19 +91,6 @@ static PLpgSQL_type *build_datatype(HeapTuple typeTup, int32 typmod,
|
||||
Oid collation, TypeName *origtypname);
|
||||
static void plpgsql_start_datums(void);
|
||||
static void plpgsql_finish_datums(PLpgSQL_function *function);
|
||||
static void compute_function_hashkey(FunctionCallInfo fcinfo,
|
||||
Form_pg_proc procStruct,
|
||||
PLpgSQL_func_hashkey *hashkey,
|
||||
bool forValidator);
|
||||
static void plpgsql_resolve_polymorphic_argtypes(int numargs,
|
||||
Oid *argtypes, char *argmodes,
|
||||
Node *call_expr, bool forValidator,
|
||||
const char *proname);
|
||||
static PLpgSQL_function *plpgsql_HashTableLookup(PLpgSQL_func_hashkey *func_key);
|
||||
static void plpgsql_HashTableInsert(PLpgSQL_function *function,
|
||||
PLpgSQL_func_hashkey *func_key);
|
||||
static void plpgsql_HashTableDelete(PLpgSQL_function *function);
|
||||
static void delete_function(PLpgSQL_function *func);
|
||||
|
||||
/* ----------
|
||||
* plpgsql_compile Make an execution tree for a PL/pgSQL function.
|
||||
@ -132,98 +105,25 @@ static void delete_function(PLpgSQL_function *func);
|
||||
PLpgSQL_function *
|
||||
plpgsql_compile(FunctionCallInfo fcinfo, bool forValidator)
|
||||
{
|
||||
Oid funcOid = fcinfo->flinfo->fn_oid;
|
||||
HeapTuple procTup;
|
||||
Form_pg_proc procStruct;
|
||||
PLpgSQL_function *function;
|
||||
PLpgSQL_func_hashkey hashkey;
|
||||
bool function_valid = false;
|
||||
bool hashkey_valid = false;
|
||||
|
||||
/*
|
||||
* Lookup the pg_proc tuple by Oid; we'll need it in any case
|
||||
*/
|
||||
procTup = SearchSysCache1(PROCOID, ObjectIdGetDatum(funcOid));
|
||||
if (!HeapTupleIsValid(procTup))
|
||||
elog(ERROR, "cache lookup failed for function %u", funcOid);
|
||||
procStruct = (Form_pg_proc) GETSTRUCT(procTup);
|
||||
|
||||
/*
|
||||
* See if there's already a cache entry for the current FmgrInfo. If not,
|
||||
* try to find one in the hash table.
|
||||
*/
|
||||
function = (PLpgSQL_function *) fcinfo->flinfo->fn_extra;
|
||||
|
||||
recheck:
|
||||
if (!function)
|
||||
{
|
||||
/* Compute hashkey using function signature and actual arg types */
|
||||
compute_function_hashkey(fcinfo, procStruct, &hashkey, forValidator);
|
||||
hashkey_valid = true;
|
||||
|
||||
/* And do the lookup */
|
||||
function = plpgsql_HashTableLookup(&hashkey);
|
||||
}
|
||||
|
||||
if (function)
|
||||
{
|
||||
/* We have a compiled function, but is it still valid? */
|
||||
if (function->fn_xmin == HeapTupleHeaderGetRawXmin(procTup->t_data) &&
|
||||
ItemPointerEquals(&function->fn_tid, &procTup->t_self))
|
||||
function_valid = true;
|
||||
else
|
||||
{
|
||||
/*
|
||||
* Nope, so remove it from hashtable and try to drop associated
|
||||
* storage (if not done already).
|
||||
*/
|
||||
delete_function(function);
|
||||
|
||||
/*
|
||||
* If the function isn't in active use then we can overwrite the
|
||||
* func struct with new data, allowing any other existing fn_extra
|
||||
* pointers to make use of the new definition on their next use.
|
||||
* If it is in use then just leave it alone and make a new one.
|
||||
* (The active invocations will run to completion using the
|
||||
* previous definition, and then the cache entry will just be
|
||||
* leaked; doesn't seem worth adding code to clean it up, given
|
||||
* what a corner case this is.)
|
||||
* funccache.c manages re-use of existing PLpgSQL_function caches.
|
||||
*
|
||||
* If we found the function struct via fn_extra then it's possible
|
||||
* a replacement has already been made, so go back and recheck the
|
||||
* hashtable.
|
||||
* In PL/pgSQL we use fn_extra directly as the pointer to the long-lived
|
||||
* function cache entry; we have no need for any query-lifespan cache.
|
||||
* Also, we don't need to make the cache key depend on composite result
|
||||
* type (at least for now).
|
||||
*/
|
||||
if (function->use_count != 0)
|
||||
{
|
||||
function = NULL;
|
||||
if (!hashkey_valid)
|
||||
goto recheck;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* If the function wasn't found or was out-of-date, we have to compile it
|
||||
*/
|
||||
if (!function_valid)
|
||||
{
|
||||
/*
|
||||
* Calculate hashkey if we didn't already; we'll need it to store the
|
||||
* completed function.
|
||||
*/
|
||||
if (!hashkey_valid)
|
||||
compute_function_hashkey(fcinfo, procStruct, &hashkey,
|
||||
function = (PLpgSQL_function *)
|
||||
cached_function_compile(fcinfo,
|
||||
fcinfo->flinfo->fn_extra,
|
||||
plpgsql_compile_callback,
|
||||
plpgsql_delete_callback,
|
||||
sizeof(PLpgSQL_function),
|
||||
false,
|
||||
forValidator);
|
||||
|
||||
/*
|
||||
* Do the hard part.
|
||||
*/
|
||||
function = do_compile(fcinfo, procTup, function,
|
||||
&hashkey, forValidator);
|
||||
}
|
||||
|
||||
ReleaseSysCache(procTup);
|
||||
|
||||
/*
|
||||
* Save pointer in FmgrInfo to avoid search on subsequent calls
|
||||
*/
|
||||
@ -244,8 +144,8 @@ struct compile_error_callback_arg
|
||||
/*
|
||||
* This is the slow part of plpgsql_compile().
|
||||
*
|
||||
* The passed-in "function" pointer is either NULL or an already-allocated
|
||||
* function struct to overwrite.
|
||||
* The passed-in "cfunc" struct is expected to be zeroes, except
|
||||
* for the CachedFunction fields, which we don't touch here.
|
||||
*
|
||||
* While compiling a function, the CurrentMemoryContext is the
|
||||
* per-function memory context of the function we are compiling. That
|
||||
@ -263,13 +163,14 @@ struct compile_error_callback_arg
|
||||
* NB: this code is not re-entrant. We assume that nothing we do here could
|
||||
* result in the invocation of another plpgsql function.
|
||||
*/
|
||||
static PLpgSQL_function *
|
||||
do_compile(FunctionCallInfo fcinfo,
|
||||
static void
|
||||
plpgsql_compile_callback(FunctionCallInfo fcinfo,
|
||||
HeapTuple procTup,
|
||||
PLpgSQL_function *function,
|
||||
PLpgSQL_func_hashkey *hashkey,
|
||||
const CachedFunctionHashKey *hashkey,
|
||||
CachedFunction *cfunc,
|
||||
bool forValidator)
|
||||
{
|
||||
PLpgSQL_function *function = (PLpgSQL_function *) cfunc;
|
||||
Form_pg_proc procStruct = (Form_pg_proc) GETSTRUCT(procTup);
|
||||
bool is_dml_trigger = CALLED_AS_TRIGGER(fcinfo);
|
||||
bool is_event_trigger = CALLED_AS_EVENT_TRIGGER(fcinfo);
|
||||
@ -320,21 +221,6 @@ do_compile(FunctionCallInfo fcinfo,
|
||||
* reasons.
|
||||
*/
|
||||
plpgsql_check_syntax = forValidator;
|
||||
|
||||
/*
|
||||
* Create the new function struct, if not done already. The function
|
||||
* structs are never thrown away, so keep them in TopMemoryContext.
|
||||
*/
|
||||
if (function == NULL)
|
||||
{
|
||||
function = (PLpgSQL_function *)
|
||||
MemoryContextAllocZero(TopMemoryContext, sizeof(PLpgSQL_function));
|
||||
}
|
||||
else
|
||||
{
|
||||
/* re-using a previously existing struct, so clear it out */
|
||||
memset(function, 0, sizeof(PLpgSQL_function));
|
||||
}
|
||||
plpgsql_curr_compile = function;
|
||||
|
||||
/*
|
||||
@ -349,8 +235,6 @@ do_compile(FunctionCallInfo fcinfo,
|
||||
function->fn_signature = format_procedure(fcinfo->flinfo->fn_oid);
|
||||
MemoryContextSetIdentifier(func_cxt, function->fn_signature);
|
||||
function->fn_oid = fcinfo->flinfo->fn_oid;
|
||||
function->fn_xmin = HeapTupleHeaderGetRawXmin(procTup->t_data);
|
||||
function->fn_tid = procTup->t_self;
|
||||
function->fn_input_collation = fcinfo->fncollation;
|
||||
function->fn_cxt = func_cxt;
|
||||
function->out_param_varno = -1; /* set up for no OUT param */
|
||||
@ -400,7 +284,7 @@ do_compile(FunctionCallInfo fcinfo,
|
||||
numargs = get_func_arg_info(procTup,
|
||||
&argtypes, &argnames, &argmodes);
|
||||
|
||||
plpgsql_resolve_polymorphic_argtypes(numargs, argtypes, argmodes,
|
||||
cfunc_resolve_polymorphic_argtypes(numargs, argtypes, argmodes,
|
||||
fcinfo->flinfo->fn_expr,
|
||||
forValidator,
|
||||
plpgsql_error_funcname);
|
||||
@ -819,11 +703,6 @@ do_compile(FunctionCallInfo fcinfo,
|
||||
if (plpgsql_DumpExecTree)
|
||||
plpgsql_dumptree(function);
|
||||
|
||||
/*
|
||||
* add it to the hash table
|
||||
*/
|
||||
plpgsql_HashTableInsert(function, hashkey);
|
||||
|
||||
/*
|
||||
* Pop the error context stack
|
||||
*/
|
||||
@ -834,14 +713,13 @@ do_compile(FunctionCallInfo fcinfo,
|
||||
|
||||
MemoryContextSwitchTo(plpgsql_compile_tmp_cxt);
|
||||
plpgsql_compile_tmp_cxt = NULL;
|
||||
return function;
|
||||
}
|
||||
|
||||
/* ----------
|
||||
* plpgsql_compile_inline Make an execution tree for an anonymous code block.
|
||||
*
|
||||
* Note: this is generally parallel to do_compile(); is it worth trying to
|
||||
* merge the two?
|
||||
* Note: this is generally parallel to plpgsql_compile_callback(); is it worth
|
||||
* trying to merge the two?
|
||||
*
|
||||
* Note: we assume the block will be thrown away so there is no need to build
|
||||
* persistent data structures.
|
||||
@ -2437,242 +2315,3 @@ plpgsql_add_initdatums(int **varnos)
|
||||
datums_last = plpgsql_nDatums;
|
||||
return n;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Compute the hashkey for a given function invocation
|
||||
*
|
||||
* The hashkey is returned into the caller-provided storage at *hashkey.
|
||||
*/
|
||||
static void
|
||||
compute_function_hashkey(FunctionCallInfo fcinfo,
|
||||
Form_pg_proc procStruct,
|
||||
PLpgSQL_func_hashkey *hashkey,
|
||||
bool forValidator)
|
||||
{
|
||||
/* Make sure any unused bytes of the struct are zero */
|
||||
MemSet(hashkey, 0, sizeof(PLpgSQL_func_hashkey));
|
||||
|
||||
/* get function OID */
|
||||
hashkey->funcOid = fcinfo->flinfo->fn_oid;
|
||||
|
||||
/* get call context */
|
||||
hashkey->isTrigger = CALLED_AS_TRIGGER(fcinfo);
|
||||
hashkey->isEventTrigger = CALLED_AS_EVENT_TRIGGER(fcinfo);
|
||||
|
||||
/*
|
||||
* If DML trigger, include trigger's OID in the hash, so that each trigger
|
||||
* usage gets a different hash entry, allowing for e.g. different relation
|
||||
* rowtypes or transition table names. In validation mode we do not know
|
||||
* what relation or transition table names are intended to be used, so we
|
||||
* leave trigOid zero; the hash entry built in this case will never be
|
||||
* used for any actual calls.
|
||||
*
|
||||
* We don't currently need to distinguish different event trigger usages
|
||||
* in the same way, since the special parameter variables don't vary in
|
||||
* type in that case.
|
||||
*/
|
||||
if (hashkey->isTrigger && !forValidator)
|
||||
{
|
||||
TriggerData *trigdata = (TriggerData *) fcinfo->context;
|
||||
|
||||
hashkey->trigOid = trigdata->tg_trigger->tgoid;
|
||||
}
|
||||
|
||||
/* get input collation, if known */
|
||||
hashkey->inputCollation = fcinfo->fncollation;
|
||||
|
||||
if (procStruct->pronargs > 0)
|
||||
{
|
||||
/* get the argument types */
|
||||
memcpy(hashkey->argtypes, procStruct->proargtypes.values,
|
||||
procStruct->pronargs * sizeof(Oid));
|
||||
|
||||
/* resolve any polymorphic argument types */
|
||||
plpgsql_resolve_polymorphic_argtypes(procStruct->pronargs,
|
||||
hashkey->argtypes,
|
||||
NULL,
|
||||
fcinfo->flinfo->fn_expr,
|
||||
forValidator,
|
||||
NameStr(procStruct->proname));
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* This is the same as the standard resolve_polymorphic_argtypes() function,
|
||||
* except that:
|
||||
* 1. We go ahead and report the error if we can't resolve the types.
|
||||
* 2. We treat RECORD-type input arguments (not output arguments) as if
|
||||
* they were polymorphic, replacing their types with the actual input
|
||||
* types if we can determine those. This allows us to create a separate
|
||||
* function cache entry for each named composite type passed to such an
|
||||
* argument.
|
||||
* 3. In validation mode, we have no inputs to look at, so assume that
|
||||
* polymorphic arguments are integer, integer-array or integer-range.
|
||||
*/
|
||||
static void
|
||||
plpgsql_resolve_polymorphic_argtypes(int numargs,
|
||||
Oid *argtypes, char *argmodes,
|
||||
Node *call_expr, bool forValidator,
|
||||
const char *proname)
|
||||
{
|
||||
int i;
|
||||
|
||||
if (!forValidator)
|
||||
{
|
||||
int inargno;
|
||||
|
||||
/* normal case, pass to standard routine */
|
||||
if (!resolve_polymorphic_argtypes(numargs, argtypes, argmodes,
|
||||
call_expr))
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
|
||||
errmsg("could not determine actual argument "
|
||||
"type for polymorphic function \"%s\"",
|
||||
proname)));
|
||||
/* also, treat RECORD inputs (but not outputs) as polymorphic */
|
||||
inargno = 0;
|
||||
for (i = 0; i < numargs; i++)
|
||||
{
|
||||
char argmode = argmodes ? argmodes[i] : PROARGMODE_IN;
|
||||
|
||||
if (argmode == PROARGMODE_OUT || argmode == PROARGMODE_TABLE)
|
||||
continue;
|
||||
if (argtypes[i] == RECORDOID || argtypes[i] == RECORDARRAYOID)
|
||||
{
|
||||
Oid resolvedtype = get_call_expr_argtype(call_expr,
|
||||
inargno);
|
||||
|
||||
if (OidIsValid(resolvedtype))
|
||||
argtypes[i] = resolvedtype;
|
||||
}
|
||||
inargno++;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
/* special validation case (no need to do anything for RECORD) */
|
||||
for (i = 0; i < numargs; i++)
|
||||
{
|
||||
switch (argtypes[i])
|
||||
{
|
||||
case ANYELEMENTOID:
|
||||
case ANYNONARRAYOID:
|
||||
case ANYENUMOID: /* XXX dubious */
|
||||
case ANYCOMPATIBLEOID:
|
||||
case ANYCOMPATIBLENONARRAYOID:
|
||||
argtypes[i] = INT4OID;
|
||||
break;
|
||||
case ANYARRAYOID:
|
||||
case ANYCOMPATIBLEARRAYOID:
|
||||
argtypes[i] = INT4ARRAYOID;
|
||||
break;
|
||||
case ANYRANGEOID:
|
||||
case ANYCOMPATIBLERANGEOID:
|
||||
argtypes[i] = INT4RANGEOID;
|
||||
break;
|
||||
case ANYMULTIRANGEOID:
|
||||
argtypes[i] = INT4MULTIRANGEOID;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* delete_function - clean up as much as possible of a stale function cache
|
||||
*
|
||||
* We can't release the PLpgSQL_function struct itself, because of the
|
||||
* possibility that there are fn_extra pointers to it. We can release
|
||||
* the subsidiary storage, but only if there are no active evaluations
|
||||
* in progress. Otherwise we'll just leak that storage. Since the
|
||||
* case would only occur if a pg_proc update is detected during a nested
|
||||
* recursive call on the function, a leak seems acceptable.
|
||||
*
|
||||
* Note that this can be called more than once if there are multiple fn_extra
|
||||
* pointers to the same function cache. Hence be careful not to do things
|
||||
* twice.
|
||||
*/
|
||||
static void
|
||||
delete_function(PLpgSQL_function *func)
|
||||
{
|
||||
/* remove function from hash table (might be done already) */
|
||||
plpgsql_HashTableDelete(func);
|
||||
|
||||
/* release the function's storage if safe and not done already */
|
||||
if (func->use_count == 0)
|
||||
plpgsql_free_function_memory(func);
|
||||
}
|
||||
|
||||
/* exported so we can call it from _PG_init() */
|
||||
void
|
||||
plpgsql_HashTableInit(void)
|
||||
{
|
||||
HASHCTL ctl;
|
||||
|
||||
/* don't allow double-initialization */
|
||||
Assert(plpgsql_HashTable == NULL);
|
||||
|
||||
ctl.keysize = sizeof(PLpgSQL_func_hashkey);
|
||||
ctl.entrysize = sizeof(plpgsql_HashEnt);
|
||||
plpgsql_HashTable = hash_create("PLpgSQL function hash",
|
||||
FUNCS_PER_USER,
|
||||
&ctl,
|
||||
HASH_ELEM | HASH_BLOBS);
|
||||
}
|
||||
|
||||
static PLpgSQL_function *
|
||||
plpgsql_HashTableLookup(PLpgSQL_func_hashkey *func_key)
|
||||
{
|
||||
plpgsql_HashEnt *hentry;
|
||||
|
||||
hentry = (plpgsql_HashEnt *) hash_search(plpgsql_HashTable,
|
||||
func_key,
|
||||
HASH_FIND,
|
||||
NULL);
|
||||
if (hentry)
|
||||
return hentry->function;
|
||||
else
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void
|
||||
plpgsql_HashTableInsert(PLpgSQL_function *function,
|
||||
PLpgSQL_func_hashkey *func_key)
|
||||
{
|
||||
plpgsql_HashEnt *hentry;
|
||||
bool found;
|
||||
|
||||
hentry = (plpgsql_HashEnt *) hash_search(plpgsql_HashTable,
|
||||
func_key,
|
||||
HASH_ENTER,
|
||||
&found);
|
||||
if (found)
|
||||
elog(WARNING, "trying to insert a function that already exists");
|
||||
|
||||
hentry->function = function;
|
||||
/* prepare back link from function to hashtable key */
|
||||
function->fn_hashkey = &hentry->key;
|
||||
}
|
||||
|
||||
static void
|
||||
plpgsql_HashTableDelete(PLpgSQL_function *function)
|
||||
{
|
||||
plpgsql_HashEnt *hentry;
|
||||
|
||||
/* do nothing if not in table */
|
||||
if (function->fn_hashkey == NULL)
|
||||
return;
|
||||
|
||||
hentry = (plpgsql_HashEnt *) hash_search(plpgsql_HashTable,
|
||||
function->fn_hashkey,
|
||||
HASH_REMOVE,
|
||||
NULL);
|
||||
if (hentry == NULL)
|
||||
elog(WARNING, "trying to delete function that does not exist");
|
||||
|
||||
/* remove back link, which no longer points to allocated storage */
|
||||
function->fn_hashkey = NULL;
|
||||
}
|
||||
|
@ -718,7 +718,7 @@ plpgsql_free_function_memory(PLpgSQL_function *func)
|
||||
int i;
|
||||
|
||||
/* Better not call this on an in-use function */
|
||||
Assert(func->use_count == 0);
|
||||
Assert(func->cfunc.use_count == 0);
|
||||
|
||||
/* Release plans associated with variable declarations */
|
||||
for (i = 0; i < func->ndatums; i++)
|
||||
@ -767,6 +767,13 @@ plpgsql_free_function_memory(PLpgSQL_function *func)
|
||||
func->fn_cxt = NULL;
|
||||
}
|
||||
|
||||
/* Deletion callback used by funccache.c */
|
||||
void
|
||||
plpgsql_delete_callback(CachedFunction *cfunc)
|
||||
{
|
||||
plpgsql_free_function_memory((PLpgSQL_function *) cfunc);
|
||||
}
|
||||
|
||||
|
||||
/**********************************************************************
|
||||
* Debug functions for analyzing the compiled code
|
||||
|
@ -202,7 +202,6 @@ _PG_init(void)
|
||||
|
||||
MarkGUCPrefixReserved("plpgsql");
|
||||
|
||||
plpgsql_HashTableInit();
|
||||
RegisterXactCallback(plpgsql_xact_cb, NULL);
|
||||
RegisterSubXactCallback(plpgsql_subxact_cb, NULL);
|
||||
|
||||
@ -247,7 +246,7 @@ plpgsql_call_handler(PG_FUNCTION_ARGS)
|
||||
save_cur_estate = func->cur_estate;
|
||||
|
||||
/* Mark the function as busy, so it can't be deleted from under us */
|
||||
func->use_count++;
|
||||
func->cfunc.use_count++;
|
||||
|
||||
/*
|
||||
* If we'll need a procedure-lifespan resowner to execute any CALL or DO
|
||||
@ -284,7 +283,7 @@ plpgsql_call_handler(PG_FUNCTION_ARGS)
|
||||
PG_FINALLY();
|
||||
{
|
||||
/* Decrement use-count, restore cur_estate */
|
||||
func->use_count--;
|
||||
func->cfunc.use_count--;
|
||||
func->cur_estate = save_cur_estate;
|
||||
|
||||
/* Be sure to release the procedure resowner if any */
|
||||
@ -334,7 +333,7 @@ plpgsql_inline_handler(PG_FUNCTION_ARGS)
|
||||
func = plpgsql_compile_inline(codeblock->source_text);
|
||||
|
||||
/* Mark the function as busy, just pro forma */
|
||||
func->use_count++;
|
||||
func->cfunc.use_count++;
|
||||
|
||||
/*
|
||||
* Set up a fake fcinfo with just enough info to satisfy
|
||||
@ -398,8 +397,8 @@ plpgsql_inline_handler(PG_FUNCTION_ARGS)
|
||||
ResourceOwnerDelete(simple_eval_resowner);
|
||||
|
||||
/* Function should now have no remaining use-counts ... */
|
||||
func->use_count--;
|
||||
Assert(func->use_count == 0);
|
||||
func->cfunc.use_count--;
|
||||
Assert(func->cfunc.use_count == 0);
|
||||
|
||||
/* ... so we can free subsidiary storage */
|
||||
plpgsql_free_function_memory(func);
|
||||
@ -415,8 +414,8 @@ plpgsql_inline_handler(PG_FUNCTION_ARGS)
|
||||
ResourceOwnerDelete(simple_eval_resowner);
|
||||
|
||||
/* Function should now have no remaining use-counts ... */
|
||||
func->use_count--;
|
||||
Assert(func->use_count == 0);
|
||||
func->cfunc.use_count--;
|
||||
Assert(func->cfunc.use_count == 0);
|
||||
|
||||
/* ... so we can free subsidiary storage */
|
||||
plpgsql_free_function_memory(func);
|
||||
|
@ -21,6 +21,7 @@
|
||||
#include "commands/trigger.h"
|
||||
#include "executor/spi.h"
|
||||
#include "utils/expandedrecord.h"
|
||||
#include "utils/funccache.h"
|
||||
#include "utils/typcache.h"
|
||||
|
||||
|
||||
@ -941,40 +942,6 @@ typedef struct PLpgSQL_stmt_dynexecute
|
||||
List *params; /* USING expressions */
|
||||
} PLpgSQL_stmt_dynexecute;
|
||||
|
||||
/*
|
||||
* Hash lookup key for functions
|
||||
*/
|
||||
typedef struct PLpgSQL_func_hashkey
|
||||
{
|
||||
Oid funcOid;
|
||||
|
||||
bool isTrigger; /* true if called as a DML trigger */
|
||||
bool isEventTrigger; /* true if called as an event trigger */
|
||||
|
||||
/* be careful that pad bytes in this struct get zeroed! */
|
||||
|
||||
/*
|
||||
* For a trigger function, the OID of the trigger is part of the hash key
|
||||
* --- we want to compile the trigger function separately for each trigger
|
||||
* it is used with, in case the rowtype or transition table names are
|
||||
* different. Zero if not called as a DML trigger.
|
||||
*/
|
||||
Oid trigOid;
|
||||
|
||||
/*
|
||||
* We must include the input collation as part of the hash key too,
|
||||
* because we have to generate different plans (with different Param
|
||||
* collations) for different collation settings.
|
||||
*/
|
||||
Oid inputCollation;
|
||||
|
||||
/*
|
||||
* We include actual argument types in the hash key to support polymorphic
|
||||
* PLpgSQL functions. Be careful that extra positions are zeroed!
|
||||
*/
|
||||
Oid argtypes[FUNC_MAX_ARGS];
|
||||
} PLpgSQL_func_hashkey;
|
||||
|
||||
/*
|
||||
* Trigger type
|
||||
*/
|
||||
@ -990,13 +957,12 @@ typedef enum PLpgSQL_trigtype
|
||||
*/
|
||||
typedef struct PLpgSQL_function
|
||||
{
|
||||
CachedFunction cfunc; /* fields managed by funccache.c */
|
||||
|
||||
char *fn_signature;
|
||||
Oid fn_oid;
|
||||
TransactionId fn_xmin;
|
||||
ItemPointerData fn_tid;
|
||||
PLpgSQL_trigtype fn_is_trigger;
|
||||
Oid fn_input_collation;
|
||||
PLpgSQL_func_hashkey *fn_hashkey; /* back-link to hashtable key */
|
||||
MemoryContext fn_cxt;
|
||||
|
||||
Oid fn_rettype;
|
||||
@ -1036,9 +1002,8 @@ typedef struct PLpgSQL_function
|
||||
bool requires_procedure_resowner; /* contains CALL or DO? */
|
||||
bool has_exception_block; /* contains BEGIN...EXCEPTION? */
|
||||
|
||||
/* these fields change when the function is used */
|
||||
/* this field changes when the function is used */
|
||||
struct PLpgSQL_execstate *cur_estate;
|
||||
unsigned long use_count;
|
||||
} PLpgSQL_function;
|
||||
|
||||
/*
|
||||
@ -1287,7 +1252,6 @@ extern PGDLLEXPORT int plpgsql_recognize_err_condition(const char *condname,
|
||||
extern PLpgSQL_condition *plpgsql_parse_err_condition(char *condname);
|
||||
extern void plpgsql_adddatum(PLpgSQL_datum *newdatum);
|
||||
extern int plpgsql_add_initdatums(int **varnos);
|
||||
extern void plpgsql_HashTableInit(void);
|
||||
|
||||
/*
|
||||
* Functions in pl_exec.c
|
||||
@ -1335,6 +1299,7 @@ extern PGDLLEXPORT const char *plpgsql_stmt_typename(PLpgSQL_stmt *stmt);
|
||||
extern const char *plpgsql_getdiag_kindname(PLpgSQL_getdiag_kind kind);
|
||||
extern void plpgsql_mark_local_assignment_targets(PLpgSQL_function *func);
|
||||
extern void plpgsql_free_function_memory(PLpgSQL_function *func);
|
||||
extern void plpgsql_delete_callback(CachedFunction *cfunc);
|
||||
extern void plpgsql_dumptree(PLpgSQL_function *func);
|
||||
|
||||
/*
|
||||
|
@ -651,7 +651,7 @@ LINE 1: SELECT public.dep_req2() || ' req3b'
|
||||
^
|
||||
HINT: No function matches the given name and argument types. You might need to add explicit type casts.
|
||||
QUERY: SELECT public.dep_req2() || ' req3b'
|
||||
CONTEXT: SQL function "dep_req3b" during startup
|
||||
CONTEXT: SQL function "dep_req3b" statement 1
|
||||
DROP EXTENSION test_ext_req_schema3;
|
||||
ALTER EXTENSION test_ext_req_schema1 SET SCHEMA test_s_dep2; -- now ok
|
||||
SELECT test_s_dep2.dep_req1();
|
||||
|
@ -563,6 +563,20 @@ CREATE OR REPLACE PROCEDURE functest1(a int) LANGUAGE SQL AS 'SELECT $1';
|
||||
ERROR: cannot change routine kind
|
||||
DETAIL: "functest1" is a function.
|
||||
DROP FUNCTION functest1(a int);
|
||||
-- early shutdown of set-returning functions
|
||||
CREATE FUNCTION functest_srf0() RETURNS SETOF int
|
||||
LANGUAGE SQL
|
||||
AS $$ SELECT i FROM generate_series(1, 100) i $$;
|
||||
SELECT functest_srf0() LIMIT 5;
|
||||
functest_srf0
|
||||
---------------
|
||||
1
|
||||
2
|
||||
3
|
||||
4
|
||||
5
|
||||
(5 rows)
|
||||
|
||||
-- inlining of set-returning functions
|
||||
CREATE TABLE functest3 (a int);
|
||||
INSERT INTO functest3 VALUES (1), (2), (3);
|
||||
@ -666,6 +680,43 @@ SELECT * FROM voidtest5(3);
|
||||
-----------
|
||||
(0 rows)
|
||||
|
||||
-- DDL within a SQL function can now affect later statements in the function;
|
||||
-- though that doesn't work if check_function_bodies is on.
|
||||
SET check_function_bodies TO off;
|
||||
CREATE FUNCTION create_and_insert() RETURNS VOID LANGUAGE sql AS $$
|
||||
create table ddl_test (f1 int);
|
||||
insert into ddl_test values (1.2);
|
||||
$$;
|
||||
SELECT create_and_insert();
|
||||
create_and_insert
|
||||
-------------------
|
||||
|
||||
(1 row)
|
||||
|
||||
TABLE ddl_test;
|
||||
f1
|
||||
----
|
||||
1
|
||||
(1 row)
|
||||
|
||||
CREATE FUNCTION alter_and_insert() RETURNS VOID LANGUAGE sql AS $$
|
||||
alter table ddl_test alter column f1 type numeric;
|
||||
insert into ddl_test values (1.2);
|
||||
$$;
|
||||
SELECT alter_and_insert();
|
||||
alter_and_insert
|
||||
------------------
|
||||
|
||||
(1 row)
|
||||
|
||||
TABLE ddl_test;
|
||||
f1
|
||||
-----
|
||||
1
|
||||
1.2
|
||||
(2 rows)
|
||||
|
||||
RESET check_function_bodies;
|
||||
-- Regression tests for bugs:
|
||||
-- Check that arguments that are R/W expanded datums aren't corrupted by
|
||||
-- multiple uses. This test knows that array_append() returns a R/W datum
|
||||
@ -708,7 +759,7 @@ CREATE FUNCTION test1 (int) RETURNS int LANGUAGE SQL
|
||||
ERROR: only one AS item needed for language "sql"
|
||||
-- Cleanup
|
||||
DROP SCHEMA temp_func_test CASCADE;
|
||||
NOTICE: drop cascades to 30 other objects
|
||||
NOTICE: drop cascades to 34 other objects
|
||||
DETAIL: drop cascades to function functest_a_1(text,date)
|
||||
drop cascades to function functest_a_2(text[])
|
||||
drop cascades to function functest_a_3()
|
||||
@ -732,12 +783,16 @@ drop cascades to function functest_s_10(text,date)
|
||||
drop cascades to function functest_s_13()
|
||||
drop cascades to function functest_s_15(integer)
|
||||
drop cascades to function functest_b_2(bigint)
|
||||
drop cascades to function functest_srf0()
|
||||
drop cascades to function functest_sri1()
|
||||
drop cascades to function voidtest1(integer)
|
||||
drop cascades to function voidtest2(integer,integer)
|
||||
drop cascades to function voidtest3(integer)
|
||||
drop cascades to function voidtest4(integer)
|
||||
drop cascades to function voidtest5(integer)
|
||||
drop cascades to function create_and_insert()
|
||||
drop cascades to table ddl_test
|
||||
drop cascades to function alter_and_insert()
|
||||
drop cascades to function double_append(anyarray,anyelement)
|
||||
DROP USER regress_unpriv_user;
|
||||
RESET search_path;
|
||||
|
@ -1885,7 +1885,7 @@ select * from array_to_set(array['one', 'two']) as t(f1 numeric(4,2),f2 text);
|
||||
select * from array_to_set(array['one', 'two']) as t(f1 point,f2 text);
|
||||
ERROR: return type mismatch in function declared to return record
|
||||
DETAIL: Final statement returns integer instead of point at column 1.
|
||||
CONTEXT: SQL function "array_to_set" during startup
|
||||
CONTEXT: SQL function "array_to_set" statement 1
|
||||
-- with "strict", this function can't be inlined in FROM
|
||||
explain (verbose, costs off)
|
||||
select * from array_to_set(array['one', 'two']) as t(f1 numeric(4,2),f2 text);
|
||||
|
@ -4695,6 +4695,57 @@ RESET ROLE;
|
||||
DROP FUNCTION rls_f();
|
||||
DROP VIEW rls_v;
|
||||
DROP TABLE rls_t;
|
||||
-- Check that RLS changes invalidate SQL function plans
|
||||
create table rls_t (c text);
|
||||
create table test_t (c text);
|
||||
insert into rls_t values ('a'), ('b'), ('c'), ('d');
|
||||
insert into test_t values ('a'), ('b');
|
||||
alter table rls_t enable row level security;
|
||||
grant select on rls_t to regress_rls_alice;
|
||||
grant select on test_t to regress_rls_alice;
|
||||
create policy p1 on rls_t for select to regress_rls_alice
|
||||
using (c = current_setting('rls_test.blah'));
|
||||
-- Function changes row_security setting and so invalidates plan
|
||||
create function rls_f(text) returns text
|
||||
begin atomic
|
||||
select set_config('rls_test.blah', $1, true) || set_config('row_security', 'false', true) || string_agg(c, ',' order by c) from rls_t;
|
||||
end;
|
||||
set plan_cache_mode to force_custom_plan;
|
||||
-- Table owner bypasses RLS
|
||||
select rls_f(c) from test_t order by rls_f;
|
||||
rls_f
|
||||
-------------
|
||||
aoffa,b,c,d
|
||||
boffa,b,c,d
|
||||
(2 rows)
|
||||
|
||||
-- For other users, changes in row_security setting
|
||||
-- should lead to RLS error during query rewrite
|
||||
set role regress_rls_alice;
|
||||
select rls_f(c) from test_t order by rls_f;
|
||||
ERROR: query would be affected by row-level security policy for table "rls_t"
|
||||
CONTEXT: SQL function "rls_f" statement 1
|
||||
reset role;
|
||||
set plan_cache_mode to force_generic_plan;
|
||||
-- Table owner bypasses RLS, although cached plan will be invalidated
|
||||
select rls_f(c) from test_t order by rls_f;
|
||||
rls_f
|
||||
-------------
|
||||
aoffa,b,c,d
|
||||
boffa,b,c,d
|
||||
(2 rows)
|
||||
|
||||
-- For other users, changes in row_security setting
|
||||
-- should lead to plan invalidation and RLS error during query rewrite
|
||||
set role regress_rls_alice;
|
||||
select rls_f(c) from test_t order by rls_f;
|
||||
ERROR: query would be affected by row-level security policy for table "rls_t"
|
||||
CONTEXT: SQL function "rls_f" statement 1
|
||||
reset role;
|
||||
reset plan_cache_mode;
|
||||
reset rls_test.blah;
|
||||
drop function rls_f(text);
|
||||
drop table rls_t, test_t;
|
||||
--
|
||||
-- Clean up objects
|
||||
--
|
||||
|
@ -328,6 +328,15 @@ CREATE OR REPLACE PROCEDURE functest1(a int) LANGUAGE SQL AS 'SELECT $1';
|
||||
DROP FUNCTION functest1(a int);
|
||||
|
||||
|
||||
-- early shutdown of set-returning functions
|
||||
|
||||
CREATE FUNCTION functest_srf0() RETURNS SETOF int
|
||||
LANGUAGE SQL
|
||||
AS $$ SELECT i FROM generate_series(1, 100) i $$;
|
||||
|
||||
SELECT functest_srf0() LIMIT 5;
|
||||
|
||||
|
||||
-- inlining of set-returning functions
|
||||
|
||||
CREATE TABLE functest3 (a int);
|
||||
@ -385,6 +394,31 @@ CREATE FUNCTION voidtest5(a int) RETURNS SETOF VOID LANGUAGE SQL AS
|
||||
$$ SELECT generate_series(1, a) $$ STABLE;
|
||||
SELECT * FROM voidtest5(3);
|
||||
|
||||
-- DDL within a SQL function can now affect later statements in the function;
|
||||
-- though that doesn't work if check_function_bodies is on.
|
||||
|
||||
SET check_function_bodies TO off;
|
||||
|
||||
CREATE FUNCTION create_and_insert() RETURNS VOID LANGUAGE sql AS $$
|
||||
create table ddl_test (f1 int);
|
||||
insert into ddl_test values (1.2);
|
||||
$$;
|
||||
|
||||
SELECT create_and_insert();
|
||||
|
||||
TABLE ddl_test;
|
||||
|
||||
CREATE FUNCTION alter_and_insert() RETURNS VOID LANGUAGE sql AS $$
|
||||
alter table ddl_test alter column f1 type numeric;
|
||||
insert into ddl_test values (1.2);
|
||||
$$;
|
||||
|
||||
SELECT alter_and_insert();
|
||||
|
||||
TABLE ddl_test;
|
||||
|
||||
RESET check_function_bodies;
|
||||
|
||||
-- Regression tests for bugs:
|
||||
|
||||
-- Check that arguments that are R/W expanded datums aren't corrupted by
|
||||
|
@ -2307,6 +2307,50 @@ DROP FUNCTION rls_f();
|
||||
DROP VIEW rls_v;
|
||||
DROP TABLE rls_t;
|
||||
|
||||
-- Check that RLS changes invalidate SQL function plans
|
||||
create table rls_t (c text);
|
||||
create table test_t (c text);
|
||||
insert into rls_t values ('a'), ('b'), ('c'), ('d');
|
||||
insert into test_t values ('a'), ('b');
|
||||
alter table rls_t enable row level security;
|
||||
grant select on rls_t to regress_rls_alice;
|
||||
grant select on test_t to regress_rls_alice;
|
||||
create policy p1 on rls_t for select to regress_rls_alice
|
||||
using (c = current_setting('rls_test.blah'));
|
||||
|
||||
-- Function changes row_security setting and so invalidates plan
|
||||
create function rls_f(text) returns text
|
||||
begin atomic
|
||||
select set_config('rls_test.blah', $1, true) || set_config('row_security', 'false', true) || string_agg(c, ',' order by c) from rls_t;
|
||||
end;
|
||||
|
||||
set plan_cache_mode to force_custom_plan;
|
||||
|
||||
-- Table owner bypasses RLS
|
||||
select rls_f(c) from test_t order by rls_f;
|
||||
|
||||
-- For other users, changes in row_security setting
|
||||
-- should lead to RLS error during query rewrite
|
||||
set role regress_rls_alice;
|
||||
select rls_f(c) from test_t order by rls_f;
|
||||
reset role;
|
||||
|
||||
set plan_cache_mode to force_generic_plan;
|
||||
|
||||
-- Table owner bypasses RLS, although cached plan will be invalidated
|
||||
select rls_f(c) from test_t order by rls_f;
|
||||
|
||||
-- For other users, changes in row_security setting
|
||||
-- should lead to plan invalidation and RLS error during query rewrite
|
||||
set role regress_rls_alice;
|
||||
select rls_f(c) from test_t order by rls_f;
|
||||
reset role;
|
||||
|
||||
reset plan_cache_mode;
|
||||
reset rls_test.blah;
|
||||
drop function rls_f(text);
|
||||
drop table rls_t, test_t;
|
||||
|
||||
--
|
||||
-- Clean up objects
|
||||
--
|
||||
|
@ -382,6 +382,11 @@ CURLM
|
||||
CURLoption
|
||||
CV
|
||||
CachedExpression
|
||||
CachedFunction
|
||||
CachedFunctionCompileCallback
|
||||
CachedFunctionDeleteCallback
|
||||
CachedFunctionHashEntry
|
||||
CachedFunctionHashKey
|
||||
CachedPlan
|
||||
CachedPlanSource
|
||||
CallContext
|
||||
@ -2267,6 +2272,7 @@ PortalHashEnt
|
||||
PortalStatus
|
||||
PortalStrategy
|
||||
PostParseColumnRefHook
|
||||
PostRewriteHook
|
||||
PostgresPollingStatusType
|
||||
PostingItem
|
||||
PreParseColumnRefHook
|
||||
@ -2608,6 +2614,8 @@ SPPageDesc
|
||||
SQLDropObject
|
||||
SQLFunctionCache
|
||||
SQLFunctionCachePtr
|
||||
SQLFunctionHashEntry
|
||||
SQLFunctionLink
|
||||
SQLFunctionParseInfo
|
||||
SQLFunctionParseInfoPtr
|
||||
SQLValueFunction
|
||||
|
Loading…
x
Reference in New Issue
Block a user