Skip to content

Commit c8ab970

Browse files
committed
Fix list-munging bug that broke SQL function result coercions.
Since commit 913bbd8, check_sql_fn_retval() can either insert type coercion steps in-line in the Query that produces the SQL function's results, or generate a new top-level Query to perform the coercions, if modifying the Query's output in-place wouldn't be safe. However, it appears that the latter case has never actually worked, because the code tried to inject the new Query back into the query list it was passed ... which is not the list that will be used for later processing when we execute the SQL function "normally" (without inlining it). So we ended up with no coercion happening at run-time, leading to wrong results or crashes depending on the datatypes involved. While the regression tests look like they cover this area well enough, through a huge bit of bad luck all the test cases that exercise the separate-Query path were checking either inline-able cases (which accidentally didn't have the bug) or cases that are no-ops at runtime (e.g., varchar to text), so that the failure to perform the coercion wasn't obvious. The fact that the cases that don't work weren't allowed at all before v13 probably contributed to not noticing the problem sooner, too. To fix, get rid of the separate "flat" list of Query nodes and instead pass the real two-level list that is going to be used later. I chose to make the same change in check_sql_fn_statements(), although that has no actual bug, just so that we don't need that data structure at all. This is an API change, as evidenced by the adjustments needed to callers outside functions.c. That's a bit scary to be doing in a released branch, but so far as I can tell from a quick search, there are no outside callers of these functions (and they are sufficiently specific to our semantics for SQL-language functions that it's not apparent why any extension would need to call them). In any case, v13 already changed the API of check_sql_fn_retval() compared to prior branches. Per report from pinker. Back-patch to v13 where this code came in. Discussion: https://postgr.es/m/1603050466566-0.post@n3.nabble.com
1 parent c5f42da commit c8ab970

File tree

6 files changed

+132
-60
lines changed

6 files changed

+132
-60
lines changed

src/backend/catalog/pg_proc.c

+2-2
Original file line numberDiff line numberDiff line change
@@ -913,8 +913,8 @@ fmgr_sql_validator(PG_FUNCTION_ARGS)
913913
(ParserSetupHook) sql_fn_parser_setup,
914914
pinfo,
915915
NULL);
916-
querytree_list = list_concat(querytree_list,
917-
querytree_sublist);
916+
querytree_list = lappend(querytree_list,
917+
querytree_sublist);
918918
}
919919

920920
check_sql_fn_statements(querytree_list);

src/backend/executor/functions.c

+69-53
Original file line numberDiff line numberDiff line change
@@ -609,7 +609,6 @@ init_sql_fcache(FunctionCallInfo fcinfo, Oid collation, bool lazyEvalOK)
609609
SQLFunctionCachePtr fcache;
610610
List *raw_parsetree_list;
611611
List *queryTree_list;
612-
List *flat_query_list;
613612
List *resulttlist;
614613
ListCell *lc;
615614
Datum tmp;
@@ -689,13 +688,7 @@ init_sql_fcache(FunctionCallInfo fcinfo, Oid collation, bool lazyEvalOK)
689688

690689
/*
691690
* Parse and rewrite the queries in the function text. Use sublists to
692-
* keep track of the original query boundaries. But we also build a
693-
* "flat" list of the rewritten queries to pass to check_sql_fn_retval.
694-
* This is because the last canSetTag query determines the result type
695-
* independently of query boundaries --- and it might not be in the last
696-
* sublist, for example if the last query rewrites to DO INSTEAD NOTHING.
697-
* (It might not be unreasonable to throw an error in such a case, but
698-
* this is the historical behavior and it doesn't seem worth changing.)
691+
* keep track of the original query boundaries.
699692
*
700693
* Note: since parsing and planning is done in fcontext, we will generate
701694
* a lot of cruft that lives as long as the fcache does. This is annoying
@@ -705,7 +698,6 @@ init_sql_fcache(FunctionCallInfo fcinfo, Oid collation, bool lazyEvalOK)
705698
raw_parsetree_list = pg_parse_query(fcache->src);
706699

707700
queryTree_list = NIL;
708-
flat_query_list = NIL;
709701
foreach(lc, raw_parsetree_list)
710702
{
711703
RawStmt *parsetree = lfirst_node(RawStmt, lc);
@@ -717,10 +709,12 @@ init_sql_fcache(FunctionCallInfo fcinfo, Oid collation, bool lazyEvalOK)
717709
fcache->pinfo,
718710
NULL);
719711
queryTree_list = lappend(queryTree_list, queryTree_sublist);
720-
flat_query_list = list_concat(flat_query_list, queryTree_sublist);
721712
}
722713

723-
check_sql_fn_statements(flat_query_list);
714+
/*
715+
* Check that there are no statements we don't want to allow.
716+
*/
717+
check_sql_fn_statements(queryTree_list);
724718

725719
/*
726720
* Check that the function returns the type it claims to. Although in
@@ -740,7 +734,7 @@ init_sql_fcache(FunctionCallInfo fcinfo, Oid collation, bool lazyEvalOK)
740734
* the rowtype column into multiple columns, since we have no way to
741735
* notify the caller that it should do that.)
742736
*/
743-
fcache->returnsTuple = check_sql_fn_retval(flat_query_list,
737+
fcache->returnsTuple = check_sql_fn_retval(queryTree_list,
744738
rettype,
745739
rettupdesc,
746740
false,
@@ -1510,51 +1504,63 @@ ShutdownSQLFunction(Datum arg)
15101504
* is not acceptable.
15111505
*/
15121506
void
1513-
check_sql_fn_statements(List *queryTreeList)
1507+
check_sql_fn_statements(List *queryTreeLists)
15141508
{
15151509
ListCell *lc;
15161510

1517-
foreach(lc, queryTreeList)
1511+
/* We are given a list of sublists of Queries */
1512+
foreach(lc, queryTreeLists)
15181513
{
1519-
Query *query = lfirst_node(Query, lc);
1514+
List *sublist = lfirst_node(List, lc);
1515+
ListCell *lc2;
15201516

1521-
/*
1522-
* Disallow procedures with output arguments. The current
1523-
* implementation would just throw the output values away, unless the
1524-
* statement is the last one. Per SQL standard, we should assign the
1525-
* output values by name. By disallowing this here, we preserve an
1526-
* opportunity for future improvement.
1527-
*/
1528-
if (query->commandType == CMD_UTILITY &&
1529-
IsA(query->utilityStmt, CallStmt))
1517+
foreach(lc2, sublist)
15301518
{
1531-
CallStmt *stmt = castNode(CallStmt, query->utilityStmt);
1532-
HeapTuple tuple;
1533-
int numargs;
1534-
Oid *argtypes;
1535-
char **argnames;
1536-
char *argmodes;
1537-
int i;
1538-
1539-
tuple = SearchSysCache1(PROCOID, ObjectIdGetDatum(stmt->funcexpr->funcid));
1540-
if (!HeapTupleIsValid(tuple))
1541-
elog(ERROR, "cache lookup failed for function %u", stmt->funcexpr->funcid);
1542-
numargs = get_func_arg_info(tuple, &argtypes, &argnames, &argmodes);
1543-
ReleaseSysCache(tuple);
1544-
1545-
for (i = 0; i < numargs; i++)
1519+
Query *query = lfirst_node(Query, lc2);
1520+
1521+
/*
1522+
* Disallow procedures with output arguments. The current
1523+
* implementation would just throw the output values away, unless
1524+
* the statement is the last one. Per SQL standard, we should
1525+
* assign the output values by name. By disallowing this here, we
1526+
* preserve an opportunity for future improvement.
1527+
*/
1528+
if (query->commandType == CMD_UTILITY &&
1529+
IsA(query->utilityStmt, CallStmt))
15461530
{
1547-
if (argmodes && (argmodes[i] == PROARGMODE_INOUT || argmodes[i] == PROARGMODE_OUT))
1548-
ereport(ERROR,
1549-
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
1550-
errmsg("calling procedures with output arguments is not supported in SQL functions")));
1531+
CallStmt *stmt = castNode(CallStmt, query->utilityStmt);
1532+
HeapTuple tuple;
1533+
int numargs;
1534+
Oid *argtypes;
1535+
char **argnames;
1536+
char *argmodes;
1537+
int i;
1538+
1539+
tuple = SearchSysCache1(PROCOID,
1540+
ObjectIdGetDatum(stmt->funcexpr->funcid));
1541+
if (!HeapTupleIsValid(tuple))
1542+
elog(ERROR, "cache lookup failed for function %u",
1543+
stmt->funcexpr->funcid);
1544+
numargs = get_func_arg_info(tuple,
1545+
&argtypes, &argnames, &argmodes);
1546+
ReleaseSysCache(tuple);
1547+
1548+
for (i = 0; i < numargs; i++)
1549+
{
1550+
if (argmodes && (argmodes[i] == PROARGMODE_INOUT ||
1551+
argmodes[i] == PROARGMODE_OUT))
1552+
ereport(ERROR,
1553+
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
1554+
errmsg("calling procedures with output arguments is not supported in SQL functions")));
1555+
}
15511556
}
15521557
}
15531558
}
15541559
}
15551560

15561561
/*
1557-
* check_sql_fn_retval() -- check return value of a list of sql parse trees.
1562+
* check_sql_fn_retval()
1563+
* Check return value of a list of lists of sql parse trees.
15581564
*
15591565
* The return value of a sql function is the value returned by the last
15601566
* canSetTag query in the function. We do some ad-hoc type checking and
@@ -1592,7 +1598,7 @@ check_sql_fn_statements(List *queryTreeList)
15921598
* function is defined to return VOID then *resultTargetList is set to NIL.
15931599
*/
15941600
bool
1595-
check_sql_fn_retval(List *queryTreeList,
1601+
check_sql_fn_retval(List *queryTreeLists,
15961602
Oid rettype, TupleDesc rettupdesc,
15971603
bool insertDroppedCols,
15981604
List **resultTargetList)
@@ -1619,20 +1625,30 @@ check_sql_fn_retval(List *queryTreeList,
16191625
return false;
16201626

16211627
/*
1622-
* Find the last canSetTag query in the list. This isn't necessarily the
1623-
* last parsetree, because rule rewriting can insert queries after what
1624-
* the user wrote.
1628+
* Find the last canSetTag query in the function body (which is presented
1629+
* to us as a list of sublists of Query nodes). This isn't necessarily
1630+
* the last parsetree, because rule rewriting can insert queries after
1631+
* what the user wrote. Note that it might not even be in the last
1632+
* sublist, for example if the last query rewrites to DO INSTEAD NOTHING.
1633+
* (It might not be unreasonable to throw an error in such a case, but
1634+
* this is the historical behavior and it doesn't seem worth changing.)
16251635
*/
16261636
parse = NULL;
16271637
parse_cell = NULL;
1628-
foreach(lc, queryTreeList)
1638+
foreach(lc, queryTreeLists)
16291639
{
1630-
Query *q = lfirst_node(Query, lc);
1640+
List *sublist = lfirst_node(List, lc);
1641+
ListCell *lc2;
16311642

1632-
if (q->canSetTag)
1643+
foreach(lc2, sublist)
16331644
{
1634-
parse = q;
1635-
parse_cell = lc;
1645+
Query *q = lfirst_node(Query, lc2);
1646+
1647+
if (q->canSetTag)
1648+
{
1649+
parse = q;
1650+
parse_cell = lc2;
1651+
}
16361652
}
16371653
}
16381654

src/backend/optimizer/util/clauses.c

+4-3
Original file line numberDiff line numberDiff line change
@@ -4522,7 +4522,8 @@ inline_function(Oid funcid, Oid result_type, Oid result_collid,
45224522
* needed; that's probably not important, but let's be careful.
45234523
*/
45244524
querytree_list = list_make1(querytree);
4525-
if (check_sql_fn_retval(querytree_list, result_type, rettupdesc,
4525+
if (check_sql_fn_retval(list_make1(querytree_list),
4526+
result_type, rettupdesc,
45264527
false, NULL))
45274528
goto fail; /* reject whole-tuple-result cases */
45284529

@@ -5040,7 +5041,7 @@ inline_set_returning_function(PlannerInfo *root, RangeTblEntry *rte)
50405041
* shows it's returning a whole tuple result; otherwise what it's
50415042
* returning is a single composite column which is not what we need.
50425043
*/
5043-
if (!check_sql_fn_retval(querytree_list,
5044+
if (!check_sql_fn_retval(list_make1(querytree_list),
50445045
fexpr->funcresulttype, rettupdesc,
50455046
true, NULL) &&
50465047
(functypclass == TYPEFUNC_COMPOSITE ||
@@ -5052,7 +5053,7 @@ inline_set_returning_function(PlannerInfo *root, RangeTblEntry *rte)
50525053
* check_sql_fn_retval might've inserted a projection step, but that's
50535054
* fine; just make sure we use the upper Query.
50545055
*/
5055-
querytree = linitial(querytree_list);
5056+
querytree = linitial_node(Query, querytree_list);
50565057

50575058
/*
50585059
* Looks good --- substitute parameters into the query.

src/include/executor/functions.h

+2-2
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,9 @@ extern SQLFunctionParseInfoPtr prepare_sql_fn_parse_info(HeapTuple procedureTupl
2929
extern void sql_fn_parser_setup(struct ParseState *pstate,
3030
SQLFunctionParseInfoPtr pinfo);
3131

32-
extern void check_sql_fn_statements(List *queryTreeList);
32+
extern void check_sql_fn_statements(List *queryTreeLists);
3333

34-
extern bool check_sql_fn_retval(List *queryTreeList,
34+
extern bool check_sql_fn_retval(List *queryTreeLists,
3535
Oid rettype, TupleDesc rettupdesc,
3636
bool insertDroppedCols,
3737
List **resultTargetList);

src/test/regress/expected/rangefuncs.out

+44
Original file line numberDiff line numberDiff line change
@@ -2109,6 +2109,50 @@ select * from testrngfunc();
21092109
7.136178 | 7.14
21102110
(1 row)
21112111

2112+
create or replace function testrngfunc() returns setof rngfunc_type as $$
2113+
select 1, 2 union select 3, 4 order by 1;
2114+
$$ language sql immutable;
2115+
explain (verbose, costs off)
2116+
select testrngfunc();
2117+
QUERY PLAN
2118+
-------------------------
2119+
ProjectSet
2120+
Output: testrngfunc()
2121+
-> Result
2122+
(3 rows)
2123+
2124+
select testrngfunc();
2125+
testrngfunc
2126+
-----------------
2127+
(1.000000,2.00)
2128+
(3.000000,4.00)
2129+
(2 rows)
2130+
2131+
explain (verbose, costs off)
2132+
select * from testrngfunc();
2133+
QUERY PLAN
2134+
----------------------------------------------------------
2135+
Subquery Scan on "*SELECT*"
2136+
Output: "*SELECT*"."?column?", "*SELECT*"."?column?_1"
2137+
-> Unique
2138+
Output: (1), (2)
2139+
-> Sort
2140+
Output: (1), (2)
2141+
Sort Key: (1), (2)
2142+
-> Append
2143+
-> Result
2144+
Output: 1, 2
2145+
-> Result
2146+
Output: 3, 4
2147+
(12 rows)
2148+
2149+
select * from testrngfunc();
2150+
f1 | f2
2151+
----------+------
2152+
1.000000 | 2.00
2153+
3.000000 | 4.00
2154+
(2 rows)
2155+
21122156
-- Check a couple of error cases while we're here
21132157
select * from testrngfunc() as t(f1 int8,f2 int8); -- fail, composite result
21142158
ERROR: a column definition list is redundant for a function returning a named composite type

src/test/regress/sql/rangefuncs.sql

+11
Original file line numberDiff line numberDiff line change
@@ -629,6 +629,17 @@ explain (verbose, costs off)
629629
select * from testrngfunc();
630630
select * from testrngfunc();
631631

632+
create or replace function testrngfunc() returns setof rngfunc_type as $$
633+
select 1, 2 union select 3, 4 order by 1;
634+
$$ language sql immutable;
635+
636+
explain (verbose, costs off)
637+
select testrngfunc();
638+
select testrngfunc();
639+
explain (verbose, costs off)
640+
select * from testrngfunc();
641+
select * from testrngfunc();
642+
632643
-- Check a couple of error cases while we're here
633644
select * from testrngfunc() as t(f1 int8,f2 int8); -- fail, composite result
634645
select * from pg_get_keywords() as t(f1 int8,f2 int8); -- fail, OUT params

0 commit comments

Comments
 (0)