diff --git a/doc/src/sgml/xfunc.sgml b/doc/src/sgml/xfunc.sgml index f116d0648e55..1b5ff9f7c2bb 100644 --- a/doc/src/sgml/xfunc.sgml +++ b/doc/src/sgml/xfunc.sgml @@ -4165,6 +4165,107 @@ supportfn(internal) returns internal expression and an actual execution of the target function. + + Similarly, a set-returning function + can implement SupportRequestInlineSRF to return a + Query node, which the planner will try to inline into + the outer query, just as PostgreSQL inlines + SQL functions. Normallly only SQL functions can be inlined, but this support + request allows a function in PL/pgSQL + or another language to build a dynamic SQL query and have it inlined too. + The Query node must be a SELECT query + that has gone through parse analysis and rewriting. + You may include Param nodes referencing the original function's + parameters, and PostgreSQL will map those appropriately + to the arguments passed by the caller. + It is the responsibility of the support function to return + a node that matches the parent function's implementation. + We make no guarantee that PostgreSQL will + never call the target function in cases that the support function could + simplify. Functions called in SELECT are not simplified. + Or if the RangeTblEntry has more than one + RangeTblFunction (such as when using + ROWS FROM), the function will not be simplified. + Ensure rigorous equivalence between the simplified expression and an actual + execution of the target function. + + + + One way to implement a SupportRequestInlineSRF support function + is to build a SQL string then parse it with pg_parse_query. + The outline of such a function might look like this: + +PG_FUNCTION_INFO_V1(my_support_function); +Datum +my_support_function(PG_FUNCTION_ARGS) +{ + Node *rawreq = (Node *) PG_GETARG_POINTER(0); + SupportRequestInlineSRF *req + RangeTblFunction *rtfunc; + FuncExpr *expr; + Query *querytree; + StringInfoData sql; + HeapTuple func_tuple; + SQLFunctionParseInfoPtr pinfo; + List *raw_parsetree_list; + + /* Return if it's not a type we handle. */ + if (!IsA(rawreq, SupportRequestInlineSRF)) + PG_RETURN_POINTER(NULL); + + /* Get things we need off the support request node. */ + req = (SupportRequestInlineSRF *) rawreq; + rtfunc = req->rtfunc; + expr = (FuncExpr *) rtfunc->funcexpr; + + /* Generate the SQL string. */ + initStringInfo(&sql); + appendStringInfo(&sql, "SELECT ..."); + + /* Build a SQLFunctionParseInfo. */ + func_tuple = SearchSysCache1(PROCOID, ObjectIdGetDatum(expr->funcid)); + if (!HeapTupleIsValid(func_tuple)) + { + ereport(WARNING, (errmsg("cache lookup failed for function %u", expr->funcid))); + PG_RETURN_POINTER(NULL); + } + pinfo = prepare_sql_fn_parse_info(func_tuple, + (Node *) expr, + expr->inputcollid); + ReleaseSysCache(func_tuple); + + /* Parse the SQL. */ + raw_parsetree_list = pg_parse_query(sql.data); + if (list_length(raw_parsetree_list) != 1) + { + ereport(WARNING, (errmsg("my_support_func parsed to more than one node"))); + PG_RETURN_POINTER(NULL); + } + + /* Analyze the parse tree as if it were a SQL-language body. */ + querytree_list = pg_analyze_and_rewrite_withcb(linitial(raw_parsetree_list), + sql.data, + (ParserSetupHook) sql_fn_parser_setup, + pinfo, NULL); + if (list_length(querytree_list) != 1) + { + ereport(WARNING, (errmsg("my_support_func rewrote to more than one node"))); + PG_RETURN_POINTER(NULL); + } + + querytree = linitial(querytree_list); + if (!IsA(querytree, Query)) + { + ereport(WARNING, (errmsg("my_support_func didn't parse to a Query"), + errdetail("Got this instead: %s", nodeToString(querytree)))); + PG_RETURN_POINTER(NULL); + } + + PG_RETURN_POINTER(querytree); +} + + + For target functions that return boolean, it is often useful to estimate the fraction of rows that will be selected by a WHERE clause using that diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c index 6f0b338d2cdf..312237a5e136 100644 --- a/src/backend/optimizer/util/clauses.c +++ b/src/backend/optimizer/util/clauses.c @@ -5146,36 +5146,62 @@ evaluate_expr(Expr *expr, Oid result_type, int32 result_typmod, /* - * inline_set_returning_function - * Attempt to "inline" a set-returning function in the FROM clause. + * inline_set_returning_function_with_support * - * "rte" is an RTE_FUNCTION rangetable entry. If it represents a call of a - * set-returning SQL function that can safely be inlined, expand the function - * and return the substitute Query structure. Otherwise, return NULL. + * This implements inline_set_returning_function for functions with + * a support function that can handle SupportRequestInlineSRF. + * We check fewer things than inline_sql_set_returning_function, + * so that support functions can make their own decisions about what + * to handle. For instance we don't forbid a VOLATILE function. + */ +static Query * +inline_set_returning_function_with_support(PlannerInfo *root, RangeTblEntry *rte, + RangeTblFunction *rtfunc, + FuncExpr *fexpr, HeapTuple func_tuple, + Form_pg_proc funcform) +{ + SupportRequestInlineSRF req; + Node *newnode; + + /* It must have a support function. */ + Assert(funcform->prosupport); + + req.root = root; + req.type = T_SupportRequestInlineSRF; + req.rtfunc = rtfunc; + req.proc = func_tuple; + + newnode = (Node *) + DatumGetPointer(OidFunctionCall1(funcform->prosupport, + PointerGetDatum(&req))); + + if (!newnode) + return NULL; + + if (!IsA(newnode, Query)) + elog(ERROR, + "Got unexpected node type %d from %s for function %s", + newnode->type, "SupportRequestInlineSRF", NameStr(funcform->proname)); + + return (Query *) newnode; +} + +/* + * inline_sql_set_returning_function * - * We assume that the RTE's expression has already been put through - * eval_const_expressions(), which among other things will take care of - * default arguments and named-argument notation. + * This implements inline_set_returning_function for sql-language functions. + * It parses the body (or uses the pre-parsed body if available). * - * This has a good deal of similarity to inline_function(), but that's - * for the non-set-returning case, and there are enough differences to - * justify separate functions. + * Returns NULL if the function couldn't be inlined. */ -Query * -inline_set_returning_function(PlannerInfo *root, RangeTblEntry *rte) +static Query * +inline_sql_set_returning_function(PlannerInfo *root, RangeTblEntry *rte, + RangeTblFunction *rtfunc, + FuncExpr *fexpr, Oid func_oid, HeapTuple func_tuple, + Form_pg_proc funcform, char *src) { - RangeTblFunction *rtfunc; - FuncExpr *fexpr; - Oid func_oid; - HeapTuple func_tuple; - Form_pg_proc funcform; - char *src; - Datum tmp; + Datum sqlbody; bool isNull; - MemoryContext oldcxt; - MemoryContext mycxt; - inline_error_callback_arg callback_arg; - ErrorContextCallback sqlerrcontext; SQLFunctionParseInfoPtr pinfo; TypeFuncClass functypclass; TupleDesc rettupdesc; @@ -5185,29 +5211,6 @@ inline_set_returning_function(PlannerInfo *root, RangeTblEntry *rte) Assert(rte->rtekind == RTE_FUNCTION); - /* - * It doesn't make a lot of sense for a SQL SRF to refer to itself in its - * own FROM clause, since that must cause infinite recursion at runtime. - * It will cause this code to recurse too, so check for stack overflow. - * (There's no need to do more.) - */ - check_stack_depth(); - - /* Fail if the RTE has ORDINALITY - we don't implement that here. */ - if (rte->funcordinality) - return NULL; - - /* Fail if RTE isn't a single, simple FuncExpr */ - if (list_length(rte->functions) != 1) - return NULL; - rtfunc = (RangeTblFunction *) linitial(rte->functions); - - if (!IsA(rtfunc->funcexpr, FuncExpr)) - return NULL; - fexpr = (FuncExpr *) rtfunc->funcexpr; - - func_oid = fexpr->funcid; - /* * The function must be declared to return a set, else inlining would * change the results if the contained SELECT didn't return exactly one @@ -5216,35 +5219,6 @@ inline_set_returning_function(PlannerInfo *root, RangeTblEntry *rte) if (!fexpr->funcretset) return NULL; - /* - * Refuse to inline if the arguments contain any volatile functions or - * sub-selects. Volatile functions are rejected because inlining may - * result in the arguments being evaluated multiple times, risking a - * change in behavior. Sub-selects are rejected partly for implementation - * reasons (pushing them down another level might change their behavior) - * and partly because they're likely to be expensive and so multiple - * evaluation would be bad. - */ - if (contain_volatile_functions((Node *) fexpr->args) || - contain_subplans((Node *) fexpr->args)) - return NULL; - - /* Check permission to call function (fail later, if not) */ - if (object_aclcheck(ProcedureRelationId, func_oid, GetUserId(), ACL_EXECUTE) != ACLCHECK_OK) - return NULL; - - /* Check whether a plugin wants to hook function entry/exit */ - if (FmgrHookIsNeeded(func_oid)) - return NULL; - - /* - * OK, let's take a look at the function's pg_proc entry. - */ - func_tuple = SearchSysCache1(PROCOID, ObjectIdGetDatum(func_oid)); - if (!HeapTupleIsValid(func_tuple)) - elog(ERROR, "cache lookup failed for function %u", func_oid); - funcform = (Form_pg_proc) GETSTRUCT(func_tuple); - /* * Forget it if the function is not SQL-language or has other showstopper * properties. In particular it mustn't be declared STRICT, since we @@ -5262,61 +5236,34 @@ inline_set_returning_function(PlannerInfo *root, RangeTblEntry *rte) funcform->prorettype == VOIDOID || funcform->prosecdef || !funcform->proretset || - list_length(fexpr->args) != funcform->pronargs || - !heap_attisnull(func_tuple, Anum_pg_proc_proconfig, NULL)) + list_length(fexpr->args) != funcform->pronargs) { - ReleaseSysCache(func_tuple); return NULL; } - /* - * Make a temporary memory context, so that we don't leak all the stuff - * that parsing might create. - */ - mycxt = AllocSetContextCreate(CurrentMemoryContext, - "inline_set_returning_function", - ALLOCSET_DEFAULT_SIZES); - oldcxt = MemoryContextSwitchTo(mycxt); - - /* Fetch the function body */ - tmp = SysCacheGetAttrNotNull(PROCOID, func_tuple, Anum_pg_proc_prosrc); - src = TextDatumGetCString(tmp); - - /* - * Setup error traceback support for ereport(). This is so that we can - * finger the function that bad information came from. - */ - callback_arg.proname = NameStr(funcform->proname); - callback_arg.prosrc = src; - - sqlerrcontext.callback = sql_inline_error_callback; - sqlerrcontext.arg = &callback_arg; - sqlerrcontext.previous = error_context_stack; - error_context_stack = &sqlerrcontext; - /* If we have prosqlbody, pay attention to that not prosrc */ - tmp = SysCacheGetAttr(PROCOID, - func_tuple, - Anum_pg_proc_prosqlbody, - &isNull); + sqlbody = SysCacheGetAttr(PROCOID, + func_tuple, + Anum_pg_proc_prosqlbody, + &isNull); if (!isNull) { Node *n; - n = stringToNode(TextDatumGetCString(tmp)); + n = stringToNode(TextDatumGetCString(sqlbody)); if (IsA(n, List)) querytree_list = linitial_node(List, castNode(List, n)); else querytree_list = list_make1(n); if (list_length(querytree_list) != 1) - goto fail; + return NULL; querytree = linitial(querytree_list); /* Acquire necessary locks, then apply rewriter. */ AcquireRewriteLocks(querytree, true, false); querytree_list = pg_rewrite_query(querytree); if (list_length(querytree_list) != 1) - goto fail; + return NULL; querytree = linitial(querytree_list); } else @@ -5337,14 +5284,14 @@ inline_set_returning_function(PlannerInfo *root, RangeTblEntry *rte) */ raw_parsetree_list = pg_parse_query(src); if (list_length(raw_parsetree_list) != 1) - goto fail; + return NULL; querytree_list = pg_analyze_and_rewrite_withcb(linitial(raw_parsetree_list), src, (ParserSetupHook) sql_fn_parser_setup, pinfo, NULL); if (list_length(querytree_list) != 1) - goto fail; + return NULL; querytree = linitial(querytree_list); } @@ -5369,7 +5316,7 @@ inline_set_returning_function(PlannerInfo *root, RangeTblEntry *rte) */ if (!IsA(querytree, Query) || querytree->commandType != CMD_SELECT) - goto fail; + return NULL; /* * Make sure the function (still) returns what it's declared to. This @@ -5391,7 +5338,7 @@ inline_set_returning_function(PlannerInfo *root, RangeTblEntry *rte) (functypclass == TYPEFUNC_COMPOSITE || functypclass == TYPEFUNC_COMPOSITE_DOMAIN || functypclass == TYPEFUNC_RECORD)) - goto fail; /* reject not-whole-tuple-result cases */ + return NULL; /* reject not-whole-tuple-result cases */ /* * check_sql_fn_retval might've inserted a projection step, but that's @@ -5399,6 +5346,151 @@ inline_set_returning_function(PlannerInfo *root, RangeTblEntry *rte) */ querytree = linitial_node(Query, querytree_list); + return querytree; +} + +/* + * inline_set_returning_function + * Attempt to "inline" a set-returning function in the FROM clause. + * + * "rte" is an RTE_FUNCTION rangetable entry. If it represents a call of a + * set-returning function that can safely be inlined, expand the function + * and return the substitute Query structure. Otherwise, return NULL. + * + * We assume that the RTE's expression has already been put through + * eval_const_expressions(), which among other things will take care of + * default arguments and named-argument notation. + * + * This has a good deal of similarity to inline_function(), but that's + * for the non-set-returning case, and there are enough differences to + * justify separate functions. + * + * It allocates its own temporary MemoryContext for the parsing, then copies + * the result into the caller's context. + */ +Query * +inline_set_returning_function(PlannerInfo *root, RangeTblEntry *rte) +{ + RangeTblFunction *rtfunc; + FuncExpr *fexpr; + Oid func_oid; + HeapTuple func_tuple; + Form_pg_proc funcform; + Datum tmp; + char *src; + inline_error_callback_arg callback_arg; + ErrorContextCallback sqlerrcontext; + MemoryContext oldcxt; + MemoryContext mycxt; + Query *querytree = NULL; + + Assert(rte->rtekind == RTE_FUNCTION); + + /* + * It doesn't make a lot of sense for a SRF to refer to itself in its own + * FROM clause, since that must cause infinite recursion at runtime. It + * will cause this code to recurse too, so check for stack overflow. + * (There's no need to do more.) + */ + check_stack_depth(); + + /* Fail if the RTE has ORDINALITY - we don't implement that here. */ + if (rte->funcordinality) + return NULL; + + /* Fail if RTE isn't a single, simple FuncExpr */ + if (list_length(rte->functions) != 1) + return NULL; + rtfunc = (RangeTblFunction *) linitial(rte->functions); + + if (!IsA(rtfunc->funcexpr, FuncExpr)) + return NULL; + fexpr = (FuncExpr *) rtfunc->funcexpr; + + func_oid = fexpr->funcid; + + /* + * Refuse to inline if the arguments contain any volatile functions or + * sub-selects. Volatile functions are rejected because inlining may + * result in the arguments being evaluated multiple times, risking a + * change in behavior. Sub-selects are rejected partly for implementation + * reasons (pushing them down another level might change their behavior) + * and partly because they're likely to be expensive and so multiple + * evaluation would be bad. + */ + if (contain_volatile_functions((Node *) fexpr->args) || + contain_subplans((Node *) fexpr->args)) + return NULL; + + /* Check permission to call function (fail later, if not) */ + if (object_aclcheck(ProcedureRelationId, func_oid, GetUserId(), ACL_EXECUTE) != ACLCHECK_OK) + return NULL; + + /* Check whether a plugin wants to hook function entry/exit */ + if (FmgrHookIsNeeded(func_oid)) + return NULL; + + /* + * OK, let's take a look at the function's pg_proc entry. + */ + func_tuple = SearchSysCache1(PROCOID, ObjectIdGetDatum(func_oid)); + if (!HeapTupleIsValid(func_tuple)) + elog(ERROR, "cache lookup failed for function %u", func_oid); + funcform = (Form_pg_proc) GETSTRUCT(func_tuple); + + /* + * Make a temporary memory context, so that we don't leak all the stuff + * that parsing might create. + */ + mycxt = AllocSetContextCreate(CurrentMemoryContext, + "inline_set_returning_function", + ALLOCSET_DEFAULT_SIZES); + oldcxt = MemoryContextSwitchTo(mycxt); + + /* Fetch the function body */ + tmp = SysCacheGetAttrNotNull(PROCOID, func_tuple, Anum_pg_proc_prosrc); + src = TextDatumGetCString(tmp); + + /* + * Setup error traceback support for ereport(). This is so that we can + * finger the function that bad information came from. + */ + callback_arg.proname = NameStr(funcform->proname); + callback_arg.prosrc = src; + + sqlerrcontext.callback = sql_inline_error_callback; + sqlerrcontext.arg = &callback_arg; + sqlerrcontext.previous = error_context_stack; + error_context_stack = &sqlerrcontext; + + /* + * If the function SETs configuration parameters, inlining would cause us + * to skip those changes. + */ + if (!heap_attisnull(func_tuple, Anum_pg_proc_proconfig, NULL)) + goto fail; + + /* + * If the function has an attached support function that can handle + * SupportRequestInlineSRF, then attempt to inline with that. Return the + * result if we get one, otherwise proceed. + */ + if (funcform->prosupport) + querytree = inline_set_returning_function_with_support(root, rte, rtfunc, fexpr, + func_tuple, funcform); + + /* Try to inline automatically */ + if (!querytree) + querytree = inline_sql_set_returning_function(root, rte, rtfunc, fexpr, + func_oid, func_tuple, funcform, src); + + if (!querytree) + goto fail; + + /* Only SELECTs are permitted */ + Assert(IsA(querytree, Query)); + Assert(querytree->commandType == CMD_SELECT); + /* * Looks good --- substitute parameters into the query. */ diff --git a/src/include/nodes/supportnodes.h b/src/include/nodes/supportnodes.h index 9c047cc401be..94f8a52de2bd 100644 --- a/src/include/nodes/supportnodes.h +++ b/src/include/nodes/supportnodes.h @@ -33,6 +33,7 @@ #ifndef SUPPORTNODES_H #define SUPPORTNODES_H +#include "catalog/pg_proc.h" #include "nodes/plannodes.h" struct PlannerInfo; /* avoid including pathnodes.h here */ @@ -69,6 +70,39 @@ typedef struct SupportRequestSimplify FuncExpr *fcall; /* Function call to be simplified */ } SupportRequestSimplify; +/* + * The InlineSRF request allows the support function to perform plan-time + * simplification of a call to its target set-returning function. For + * example a PL/pgSQL function could build a dynamic SQL query and execute it. + * Normally only SQL functions can be inlined, but with this support function + * the dynamic query can be inlined as well. + * + * The planner's PlannerInfo "root" is typically not needed, but can be + * consulted if it's necessary to obtain info about Vars present in + * the given node tree. Beware that root could be NULL in some usages. + * + * "rtfunc" will be a RangeTblFunction node for the function being replaced. + * The support function is only called if rtfunc->functions contains a + * single FuncExpr node. (ROWS FROM is one way to get more than one.) + * + * "proc" will be the HeapTuple for the pg_proc record of the function being + * replaced. + * + * The result should be a semantically-equivalent transformed node tree, + * or NULL if no simplification could be performed. It should be allocated + * in the CurrentMemoryContext. Do *not* return or modify the FuncExpr node + * tree, as it isn't really a separately allocated Node. But it's okay to + * use its args, or parts of it, in the result tree. + */ +typedef struct SupportRequestInlineSRF +{ + NodeTag type; + + struct PlannerInfo *root; /* Planner's infrastructure */ + RangeTblFunction *rtfunc; /* Function call to be simplified */ + HeapTuple proc; /* Function definition from pg_proc */ +} SupportRequestInlineSRF; + /* * The Selectivity request allows the support function to provide a * selectivity estimate for a function appearing at top level of a WHERE diff --git a/src/test/regress/expected/misc_functions.out b/src/test/regress/expected/misc_functions.out index c3b2b9d86034..13388ebc715d 100644 --- a/src/test/regress/expected/misc_functions.out +++ b/src/test/regress/expected/misc_functions.out @@ -601,6 +601,11 @@ CREATE FUNCTION test_support_func(internal) RETURNS internal AS :'regresslib', 'test_support_func' LANGUAGE C STRICT; +-- With a support function that inlines SRFs +CREATE FUNCTION test_inline_srf_support_func(internal) + RETURNS internal + AS :'regresslib', 'test_inline_srf_support_func' + LANGUAGE C STRICT; ALTER FUNCTION my_int_eq(int, int) SUPPORT test_support_func; EXPLAIN (COSTS OFF) SELECT * FROM tenk1 a JOIN tenk1 b ON a.unique1 = b.unique1 @@ -777,6 +782,213 @@ false, true, false, true); Function Scan on generate_series g (cost=N..N rows=1000 width=N) (1 row) +-- +-- Test inlining PL/pgSQL functions +-- +-- RETURNS SETOF TEXT: +CREATE OR REPLACE FUNCTION foo_from_bar(colname TEXT, tablename TEXT, filter TEXT) +RETURNS SETOF TEXT +LANGUAGE plpgsql +AS $function$ +DECLARE + sql TEXT; +BEGIN + sql := format('SELECT %I::text FROM %I', colname, tablename); + IF filter IS NOT NULL THEN + sql := CONCAT(sql, format(' WHERE %I::text = $1', colname)); + END IF; + RETURN QUERY EXECUTE sql USING filter; +END; +$function$ STABLE LEAKPROOF; +SELECT * FROM foo_from_bar('f1', 'text_tbl', NULL); + foo_from_bar +------------------- + doh! + hi de ho neighbor +(2 rows) + +SELECT * FROM foo_from_bar('f1', 'text_tbl', 'doh!'); + foo_from_bar +-------------- + doh! +(1 row) + +EXPLAIN SELECT * FROM foo_from_bar('f1', 'text_tbl', NULL); + QUERY PLAN +---------------------------------------------------------------------- + Function Scan on foo_from_bar (cost=0.25..10.25 rows=1000 width=32) +(1 row) + +EXPLAIN SELECT * FROM foo_from_bar('f1', 'text_tbl', 'doh!'); + QUERY PLAN +---------------------------------------------------------------------- + Function Scan on foo_from_bar (cost=0.25..10.25 rows=1000 width=32) +(1 row) + +ALTER FUNCTION foo_from_bar(TEXT, TEXT, TEXT) SUPPORT test_inline_srf_support_func; +SELECT * FROM foo_from_bar('f1', 'text_tbl', NULL); + foo_from_bar +------------------- + doh! + hi de ho neighbor +(2 rows) + +SELECT * FROM foo_from_bar('f1', 'text_tbl', 'doh!'); + foo_from_bar +-------------- + doh! +(1 row) + +EXPLAIN SELECT * FROM foo_from_bar('f1', 'text_tbl', NULL); + QUERY PLAN +--------------------------------------------------------- + Seq Scan on text_tbl (cost=0.00..1.02 rows=2 width=32) +(1 row) + +EXPLAIN SELECT * FROM foo_from_bar('f1', 'text_tbl', 'doh!'); + QUERY PLAN +--------------------------------------------------------- + Seq Scan on text_tbl (cost=0.00..1.02 rows=1 width=32) + Filter: (f1 = 'doh!'::text) +(2 rows) + +DROP FUNCTION foo_from_bar; +-- RETURNS SETOF RECORD: +CREATE OR REPLACE FUNCTION foo_from_bar(colname TEXT, tablename TEXT, filter TEXT) +RETURNS SETOF RECORD +LANGUAGE plpgsql +AS $function$ +DECLARE + sql TEXT; +BEGIN + sql := format('SELECT %I::text FROM %I', colname, tablename); + IF filter IS NOT NULL THEN + sql := CONCAT(sql, format(' WHERE %I::text = $1', colname)); + END IF; + RETURN QUERY EXECUTE sql USING filter; +END; +$function$ STABLE LEAKPROOF; +SELECT * FROM foo_from_bar('f1', 'text_tbl', NULL) AS bar(foo TEXT); + foo +------------------- + doh! + hi de ho neighbor +(2 rows) + +SELECT * FROM foo_from_bar('f1', 'text_tbl', 'doh!') AS bar(foo TEXT); + foo +------ + doh! +(1 row) + +EXPLAIN SELECT * FROM foo_from_bar('f1', 'text_tbl', NULL) AS bar(foo TEXT); + QUERY PLAN +-------------------------------------------------------------------------- + Function Scan on foo_from_bar bar (cost=0.25..10.25 rows=1000 width=32) +(1 row) + +EXPLAIN SELECT * FROM foo_from_bar('f1', 'text_tbl', 'doh!') AS bar(foo TEXT); + QUERY PLAN +-------------------------------------------------------------------------- + Function Scan on foo_from_bar bar (cost=0.25..10.25 rows=1000 width=32) +(1 row) + +ALTER FUNCTION foo_from_bar(TEXT, TEXT, TEXT) SUPPORT test_inline_srf_support_func; +SELECT * FROM foo_from_bar('f1', 'text_tbl', NULL) AS bar(foo TEXT); + foo +------------------- + doh! + hi de ho neighbor +(2 rows) + +SELECT * FROM foo_from_bar('f1', 'text_tbl', 'doh!') AS bar(foo TEXT); + foo +------ + doh! +(1 row) + +EXPLAIN SELECT * FROM foo_from_bar('f1', 'text_tbl', NULL) AS bar(foo TEXT); + QUERY PLAN +--------------------------------------------------------- + Seq Scan on text_tbl (cost=0.00..1.02 rows=2 width=32) +(1 row) + +EXPLAIN SELECT * FROM foo_from_bar('f1', 'text_tbl', 'doh!') AS bar(foo TEXT); + QUERY PLAN +--------------------------------------------------------- + Seq Scan on text_tbl (cost=0.00..1.02 rows=1 width=32) + Filter: (f1 = 'doh!'::text) +(2 rows) + +DROP FUNCTION foo_from_bar; +-- RETURNS TABLE: +CREATE OR REPLACE FUNCTION foo_from_bar(colname TEXT, tablename TEXT, filter TEXT) +RETURNS TABLE(foo TEXT) +LANGUAGE plpgsql +AS $function$ +DECLARE + sql TEXT; +BEGIN + sql := format('SELECT %I::text FROM %I', colname, tablename); + IF filter IS NOT NULL THEN + sql := CONCAT(sql, format(' WHERE %I::text = $1', colname)); + END IF; + RETURN QUERY EXECUTE sql USING filter; +END; +$function$ STABLE LEAKPROOF; +SELECT * FROM foo_from_bar('f1', 'text_tbl', NULL); + foo +------------------- + doh! + hi de ho neighbor +(2 rows) + +SELECT * FROM foo_from_bar('f1', 'text_tbl', 'doh!'); + foo +------ + doh! +(1 row) + +EXPLAIN SELECT * FROM foo_from_bar('f1', 'text_tbl', NULL); + QUERY PLAN +---------------------------------------------------------------------- + Function Scan on foo_from_bar (cost=0.25..10.25 rows=1000 width=32) +(1 row) + +EXPLAIN SELECT * FROM foo_from_bar('f1', 'text_tbl', 'doh!'); + QUERY PLAN +---------------------------------------------------------------------- + Function Scan on foo_from_bar (cost=0.25..10.25 rows=1000 width=32) +(1 row) + +ALTER FUNCTION foo_from_bar(TEXT, TEXT, TEXT) SUPPORT test_inline_srf_support_func; +SELECT * FROM foo_from_bar('f1', 'text_tbl', NULL); + foo +------------------- + doh! + hi de ho neighbor +(2 rows) + +SELECT * FROM foo_from_bar('f1', 'text_tbl', 'doh!'); + foo +------ + doh! +(1 row) + +EXPLAIN SELECT * FROM foo_from_bar('f1', 'text_tbl', NULL); + QUERY PLAN +--------------------------------------------------------- + Seq Scan on text_tbl (cost=0.00..1.02 rows=2 width=32) +(1 row) + +EXPLAIN SELECT * FROM foo_from_bar('f1', 'text_tbl', 'doh!'); + QUERY PLAN +--------------------------------------------------------- + Seq Scan on text_tbl (cost=0.00..1.02 rows=1 width=32) + Filter: (f1 = 'doh!'::text) +(2 rows) + +DROP FUNCTION foo_from_bar; -- Test functions for control data SELECT count(*) > 0 AS ok FROM pg_control_checkpoint(); ok diff --git a/src/test/regress/regress.c b/src/test/regress/regress.c index 465ac148ac9f..d21c9bc4c7b8 100644 --- a/src/test/regress/regress.c +++ b/src/test/regress/regress.c @@ -28,6 +28,7 @@ #include "commands/sequence.h" #include "commands/trigger.h" #include "executor/executor.h" +#include "executor/functions.h" #include "executor/spi.h" #include "funcapi.h" #include "mb/pg_wchar.h" @@ -39,11 +40,13 @@ #include "port/atomics.h" #include "postmaster/postmaster.h" /* for MAX_BACKENDS */ #include "storage/spin.h" +#include "tcop/tcopprot.h" #include "utils/array.h" #include "utils/builtins.h" #include "utils/geo_decls.h" #include "utils/memutils.h" #include "utils/rel.h" +#include "utils/syscache.h" #include "utils/typcache.h" #define EXPECT_TRUE(expr) \ @@ -803,6 +806,117 @@ test_support_func(PG_FUNCTION_ARGS) PG_RETURN_POINTER(ret); } +PG_FUNCTION_INFO_V1(test_inline_srf_support_func); +Datum +test_inline_srf_support_func(PG_FUNCTION_ARGS) +{ + Node *rawreq = (Node *) PG_GETARG_POINTER(0); + Query *querytree = NULL; + + if (IsA(rawreq, SupportRequestInlineSRF)) + { + /* + * Assume that the target is foo_from_bar; that's safe as long as we + * don't attach this to any other set-returning function. + */ + SupportRequestInlineSRF *req = (SupportRequestInlineSRF *) rawreq; + StringInfoData sql; + RangeTblFunction *rtfunc = req->rtfunc; + FuncExpr *expr = (FuncExpr *) rtfunc->funcexpr; + Node *node; + Const *c; + char *colname; + char *tablename; + SQLFunctionParseInfoPtr pinfo; + List *raw_parsetree_list; + List *querytree_list; + + if (list_length(expr->args) != 3) + { + ereport(WARNING, (errmsg("test_inline_srf_support_func called with %d args but expected 3", list_length(expr->args)))); + PG_RETURN_POINTER(NULL); + } + + /* Get colname */ + node = linitial(expr->args); + if (!IsA(node, Const)) + { + ereport(WARNING, (errmsg("test_inline_srf_support_func called with non-Const parameters"))); + PG_RETURN_POINTER(NULL); + } + + c = (Const *) node; + if (c->consttype != TEXTOID) + { + ereport(WARNING, (errmsg("test_inline_srf_support_func called with non-TEXT parameters"))); + PG_RETURN_POINTER(NULL); + } + colname = TextDatumGetCString(c->constvalue); + + /* Get tablename */ + node = lsecond(expr->args); + if (!IsA(node, Const)) + { + ereport(WARNING, (errmsg("test_inline_srf_support_func called with non-Const parameters"))); + PG_RETURN_POINTER(NULL); + } + + c = (Const *) node; + if (c->consttype != TEXTOID) + { + ereport(WARNING, (errmsg("test_inline_srf_support_func called with non-TEXT parameters"))); + PG_RETURN_POINTER(NULL); + } + tablename = TextDatumGetCString(c->constvalue); + + initStringInfo(&sql); + appendStringInfo(&sql, "SELECT %s::text FROM %s", quote_identifier(colname), quote_identifier(tablename)); + + /* Get filter if present */ + node = lthird(expr->args); + if (!(IsA(node, Const) && ((Const *) node)->constisnull)) + { + /* Only filter if $3 is Const */ + appendStringInfo(&sql, " WHERE %s::text = $3", quote_identifier(colname)); + } + + /* Build a SQLFunctionParseInfo. */ + + pinfo = prepare_sql_fn_parse_info(req->proc, + (Node *) expr, + expr->inputcollid); + + /* Parse the SQL. */ + raw_parsetree_list = pg_parse_query(sql.data); + if (list_length(raw_parsetree_list) != 1) + { + ereport(WARNING, (errmsg("test_inline_srf_support_func parsed to more than one node"))); + PG_RETURN_POINTER(NULL); + } + + /* Analyze the parse tree as if it were a SQL-language body. */ + querytree_list = pg_analyze_and_rewrite_withcb(linitial(raw_parsetree_list), + sql.data, + (ParserSetupHook) sql_fn_parser_setup, + pinfo, NULL); + if (list_length(querytree_list) != 1) + { + ereport(WARNING, (errmsg("test_inline_srf_support_func rewrote to more than one node"))); + PG_RETURN_POINTER(NULL); + } + + querytree = linitial(querytree_list); + if (!IsA(querytree, Query)) + { + ereport(WARNING, (errmsg("test_inline_srf_support_func didn't parse to a Query"), + errdetail("Got this instead: %s", nodeToString(querytree)))); + PG_RETURN_POINTER(NULL); + } + } + + PG_RETURN_POINTER(querytree); +} + PG_FUNCTION_INFO_V1(test_opclass_options_func); Datum test_opclass_options_func(PG_FUNCTION_ARGS) diff --git a/src/test/regress/sql/misc_functions.sql b/src/test/regress/sql/misc_functions.sql index 23792c4132a1..56c6e7a8a888 100644 --- a/src/test/regress/sql/misc_functions.sql +++ b/src/test/regress/sql/misc_functions.sql @@ -248,6 +248,12 @@ CREATE FUNCTION test_support_func(internal) AS :'regresslib', 'test_support_func' LANGUAGE C STRICT; +-- With a support function that inlines SRFs +CREATE FUNCTION test_inline_srf_support_func(internal) + RETURNS internal + AS :'regresslib', 'test_inline_srf_support_func' + LANGUAGE C STRICT; + ALTER FUNCTION my_int_eq(int, int) SUPPORT test_support_func; EXPLAIN (COSTS OFF) @@ -349,6 +355,86 @@ SELECT explain_mask_costs($$ SELECT * FROM generate_series(25.0, 2.0, 0.0) g(s);$$, false, true, false, true); +-- +-- Test inlining PL/pgSQL functions +-- + +-- RETURNS SETOF TEXT: +CREATE OR REPLACE FUNCTION foo_from_bar(colname TEXT, tablename TEXT, filter TEXT) +RETURNS SETOF TEXT +LANGUAGE plpgsql +AS $function$ +DECLARE + sql TEXT; +BEGIN + sql := format('SELECT %I::text FROM %I', colname, tablename); + IF filter IS NOT NULL THEN + sql := CONCAT(sql, format(' WHERE %I::text = $1', colname)); + END IF; + RETURN QUERY EXECUTE sql USING filter; +END; +$function$ STABLE LEAKPROOF; +SELECT * FROM foo_from_bar('f1', 'text_tbl', NULL); +SELECT * FROM foo_from_bar('f1', 'text_tbl', 'doh!'); +EXPLAIN SELECT * FROM foo_from_bar('f1', 'text_tbl', NULL); +EXPLAIN SELECT * FROM foo_from_bar('f1', 'text_tbl', 'doh!'); +ALTER FUNCTION foo_from_bar(TEXT, TEXT, TEXT) SUPPORT test_inline_srf_support_func; +SELECT * FROM foo_from_bar('f1', 'text_tbl', NULL); +SELECT * FROM foo_from_bar('f1', 'text_tbl', 'doh!'); +EXPLAIN SELECT * FROM foo_from_bar('f1', 'text_tbl', NULL); +EXPLAIN SELECT * FROM foo_from_bar('f1', 'text_tbl', 'doh!'); +DROP FUNCTION foo_from_bar; +-- RETURNS SETOF RECORD: +CREATE OR REPLACE FUNCTION foo_from_bar(colname TEXT, tablename TEXT, filter TEXT) +RETURNS SETOF RECORD +LANGUAGE plpgsql +AS $function$ +DECLARE + sql TEXT; +BEGIN + sql := format('SELECT %I::text FROM %I', colname, tablename); + IF filter IS NOT NULL THEN + sql := CONCAT(sql, format(' WHERE %I::text = $1', colname)); + END IF; + RETURN QUERY EXECUTE sql USING filter; +END; +$function$ STABLE LEAKPROOF; +SELECT * FROM foo_from_bar('f1', 'text_tbl', NULL) AS bar(foo TEXT); +SELECT * FROM foo_from_bar('f1', 'text_tbl', 'doh!') AS bar(foo TEXT); +EXPLAIN SELECT * FROM foo_from_bar('f1', 'text_tbl', NULL) AS bar(foo TEXT); +EXPLAIN SELECT * FROM foo_from_bar('f1', 'text_tbl', 'doh!') AS bar(foo TEXT); +ALTER FUNCTION foo_from_bar(TEXT, TEXT, TEXT) SUPPORT test_inline_srf_support_func; +SELECT * FROM foo_from_bar('f1', 'text_tbl', NULL) AS bar(foo TEXT); +SELECT * FROM foo_from_bar('f1', 'text_tbl', 'doh!') AS bar(foo TEXT); +EXPLAIN SELECT * FROM foo_from_bar('f1', 'text_tbl', NULL) AS bar(foo TEXT); +EXPLAIN SELECT * FROM foo_from_bar('f1', 'text_tbl', 'doh!') AS bar(foo TEXT); +DROP FUNCTION foo_from_bar; +-- RETURNS TABLE: +CREATE OR REPLACE FUNCTION foo_from_bar(colname TEXT, tablename TEXT, filter TEXT) +RETURNS TABLE(foo TEXT) +LANGUAGE plpgsql +AS $function$ +DECLARE + sql TEXT; +BEGIN + sql := format('SELECT %I::text FROM %I', colname, tablename); + IF filter IS NOT NULL THEN + sql := CONCAT(sql, format(' WHERE %I::text = $1', colname)); + END IF; + RETURN QUERY EXECUTE sql USING filter; +END; +$function$ STABLE LEAKPROOF; +SELECT * FROM foo_from_bar('f1', 'text_tbl', NULL); +SELECT * FROM foo_from_bar('f1', 'text_tbl', 'doh!'); +EXPLAIN SELECT * FROM foo_from_bar('f1', 'text_tbl', NULL); +EXPLAIN SELECT * FROM foo_from_bar('f1', 'text_tbl', 'doh!'); +ALTER FUNCTION foo_from_bar(TEXT, TEXT, TEXT) SUPPORT test_inline_srf_support_func; +SELECT * FROM foo_from_bar('f1', 'text_tbl', NULL); +SELECT * FROM foo_from_bar('f1', 'text_tbl', 'doh!'); +EXPLAIN SELECT * FROM foo_from_bar('f1', 'text_tbl', NULL); +EXPLAIN SELECT * FROM foo_from_bar('f1', 'text_tbl', 'doh!'); +DROP FUNCTION foo_from_bar; + -- Test functions for control data SELECT count(*) > 0 AS ok FROM pg_control_checkpoint(); SELECT count(*) > 0 AS ok FROM pg_control_init(); diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index a13e81628902..6a8adefcdc44 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -2901,6 +2901,7 @@ SubscriptionRelState SummarizerReadLocalXLogPrivate SupportRequestCost SupportRequestIndexCondition +SupportRequestInlineSRF SupportRequestModifyInPlace SupportRequestOptimizeWindowClause SupportRequestRows