Commit 913bbd88 authored by Tom Lane's avatar Tom Lane

Improve the handling of result type coercions in SQL functions.

Use the parser's standard type coercion machinery to convert the
output column(s) of a SQL function's final SELECT or RETURNING
to the type(s) they should have according to the function's declared
result type.  We'll allow any case where an assignment-level
coercion is available.  Previously, we failed unless the required
coercion was a binary-compatible one (and the documentation ignored
this, falsely claiming that the types must match exactly).

Notably, the coercion now accounts for typmods, so that cases where
a SQL function is declared to return a composite type whose columns
are typmod-constrained now behave as one would expect.  Arguably
this aspect is a bug fix, but the overall behavioral change here
seems too large to consider back-patching.

A nice side-effect is that functions can now be inlined in a
few cases where we previously failed to do so because of type
mismatches.

Discussion: https://postgr.es/m/18929.1574895430@sss.pgh.pa.us
parent 8dd1511e
...@@ -388,11 +388,15 @@ $$ LANGUAGE SQL; ...@@ -388,11 +388,15 @@ $$ LANGUAGE SQL;
</para> </para>
<para> <para>
A <acronym>SQL</acronym> function must return exactly its declared If the final <literal>SELECT</literal> or <literal>RETURNING</literal>
result type. This may require inserting an explicit cast. clause in a <acronym>SQL</acronym> function does not return exactly
the function's declared result
type, <productname>PostgreSQL</productname> will automatically cast
the value to the required type, if that is possible with an implicit
or assignment cast. Otherwise, you must write an explicit cast.
For example, suppose we wanted the For example, suppose we wanted the
previous <function>add_em</function> function to return previous <function>add_em</function> function to return
type <type>float8</type> instead. This won't work: type <type>float8</type> instead. It's sufficient to write
<programlisting> <programlisting>
CREATE FUNCTION add_em(integer, integer) RETURNS float8 AS $$ CREATE FUNCTION add_em(integer, integer) RETURNS float8 AS $$
...@@ -400,16 +404,10 @@ CREATE FUNCTION add_em(integer, integer) RETURNS float8 AS $$ ...@@ -400,16 +404,10 @@ CREATE FUNCTION add_em(integer, integer) RETURNS float8 AS $$
$$ LANGUAGE SQL; $$ LANGUAGE SQL;
</programlisting> </programlisting>
even though in other contexts <productname>PostgreSQL</productname> since the <type>integer</type> sum can be implicitly cast
would be willing to insert an implicit cast to to <type>float8</type>.
convert <type>integer</type> to <type>float8</type>. (See <xref linkend="typeconv"/> or <xref linkend="sql-createcast"/>
We need to write it as for more about casts.)
<programlisting>
CREATE FUNCTION add_em(integer, integer) RETURNS float8 AS $$
SELECT ($1 + $2)::float8;
$$ LANGUAGE SQL;
</programlisting>
</para> </para>
</sect2> </sect2>
...@@ -503,23 +501,24 @@ $$ LANGUAGE SQL; ...@@ -503,23 +501,24 @@ $$ LANGUAGE SQL;
<listitem> <listitem>
<para> <para>
The select list order in the query must be exactly the same as The select list order in the query must be exactly the same as
that in which the columns appear in the table associated that in which the columns appear in the composite type.
with the composite type. (Naming the columns, as we did above, (Naming the columns, as we did above,
is irrelevant to the system.) is irrelevant to the system.)
</para> </para>
</listitem> </listitem>
<listitem> <listitem>
<para> <para>
We must ensure each expression's type matches the corresponding We must ensure each expression's type can be cast to that of
column of the composite type, inserting a cast if necessary. the corresponding column of the composite type.
Otherwise we'll get errors like this: Otherwise we'll get errors like this:
<screen> <screen>
<computeroutput> <computeroutput>
ERROR: function declared to return emp returns varchar instead of text at column 1 ERROR: return type mismatch in function declared to return emp
DETAIL: Final statement returns text instead of point at column 4.
</computeroutput> </computeroutput>
</screen> </screen>
As with the base-type case, the function will not insert any casts As with the base-type case, the system will not insert explicit
automatically. casts automatically, only implicit or assignment casts.
</para> </para>
</listitem> </listitem>
</itemizedlist> </itemizedlist>
...@@ -542,8 +541,7 @@ $$ LANGUAGE SQL; ...@@ -542,8 +541,7 @@ $$ LANGUAGE SQL;
Another example is that if we are trying to write a function that Another example is that if we are trying to write a function that
returns a domain over composite, rather than a plain composite type, returns a domain over composite, rather than a plain composite type,
it is always necessary to write it as returning a single column, it is always necessary to write it as returning a single column,
since there is no other way to produce a value that is exactly of since there is no way to cause a coercion of the whole row result.
the domain type.
</para> </para>
<para> <para>
...@@ -1263,7 +1261,7 @@ SELECT make_array(1, 2) AS intarray, make_array('a'::text, 'b') AS textarray; ...@@ -1263,7 +1261,7 @@ SELECT make_array(1, 2) AS intarray, make_array('a'::text, 'b') AS textarray;
Without the typecast, you will get errors like this: Without the typecast, you will get errors like this:
<screen> <screen>
<computeroutput> <computeroutput>
ERROR: could not determine polymorphic type because input has type "unknown" ERROR: could not determine polymorphic type because input has type unknown
</computeroutput> </computeroutput>
</screen> </screen>
</para> </para>
......
...@@ -923,6 +923,8 @@ fmgr_sql_validator(PG_FUNCTION_ARGS) ...@@ -923,6 +923,8 @@ fmgr_sql_validator(PG_FUNCTION_ARGS)
* verify the result type. * verify the result type.
*/ */
SQLFunctionParseInfoPtr pinfo; SQLFunctionParseInfoPtr pinfo;
Oid rettype;
TupleDesc rettupdesc;
/* But first, set up parameter information */ /* But first, set up parameter information */
pinfo = prepare_sql_fn_parse_info(tuple, NULL, InvalidOid); pinfo = prepare_sql_fn_parse_info(tuple, NULL, InvalidOid);
...@@ -943,9 +945,12 @@ fmgr_sql_validator(PG_FUNCTION_ARGS) ...@@ -943,9 +945,12 @@ fmgr_sql_validator(PG_FUNCTION_ARGS)
} }
check_sql_fn_statements(querytree_list); check_sql_fn_statements(querytree_list);
(void) check_sql_fn_retval(funcoid, proc->prorettype,
querytree_list, (void) get_func_result_type(funcoid, &rettype, &rettupdesc);
NULL, NULL);
(void) check_sql_fn_retval(querytree_list,
rettype, rettupdesc,
false, NULL);
} }
error_context_stack = sqlerrcontext.previous; error_context_stack = sqlerrcontext.previous;
......
...@@ -154,7 +154,7 @@ static Node *sql_fn_resolve_param_name(SQLFunctionParseInfoPtr pinfo, ...@@ -154,7 +154,7 @@ static Node *sql_fn_resolve_param_name(SQLFunctionParseInfoPtr pinfo,
static List *init_execution_state(List *queryTree_list, static List *init_execution_state(List *queryTree_list,
SQLFunctionCachePtr fcache, SQLFunctionCachePtr fcache,
bool lazyEvalOK); bool lazyEvalOK);
static void init_sql_fcache(FmgrInfo *finfo, Oid collation, bool lazyEvalOK); static void init_sql_fcache(FunctionCallInfo fcinfo, Oid collation, bool lazyEvalOK);
static void postquel_start(execution_state *es, SQLFunctionCachePtr fcache); static void postquel_start(execution_state *es, SQLFunctionCachePtr fcache);
static bool postquel_getnext(execution_state *es, SQLFunctionCachePtr fcache); static bool postquel_getnext(execution_state *es, SQLFunctionCachePtr fcache);
static void postquel_end(execution_state *es); static void postquel_end(execution_state *es);
...@@ -166,6 +166,11 @@ static Datum postquel_get_single_result(TupleTableSlot *slot, ...@@ -166,6 +166,11 @@ static Datum postquel_get_single_result(TupleTableSlot *slot,
MemoryContext resultcontext); MemoryContext resultcontext);
static void sql_exec_error_callback(void *arg); static void sql_exec_error_callback(void *arg);
static void ShutdownSQLFunction(Datum arg); static void ShutdownSQLFunction(Datum arg);
static bool coerce_fn_result_column(TargetEntry *src_tle,
Oid res_type, int32 res_typmod,
bool tlist_is_modifiable,
List **upper_tlist,
bool *upper_tlist_nontrivial);
static void sqlfunction_startup(DestReceiver *self, int operation, TupleDesc typeinfo); static void sqlfunction_startup(DestReceiver *self, int operation, TupleDesc typeinfo);
static bool sqlfunction_receive(TupleTableSlot *slot, DestReceiver *self); static bool sqlfunction_receive(TupleTableSlot *slot, DestReceiver *self);
static void sqlfunction_shutdown(DestReceiver *self); static void sqlfunction_shutdown(DestReceiver *self);
...@@ -591,18 +596,21 @@ init_execution_state(List *queryTree_list, ...@@ -591,18 +596,21 @@ init_execution_state(List *queryTree_list,
* Initialize the SQLFunctionCache for a SQL function * Initialize the SQLFunctionCache for a SQL function
*/ */
static void static void
init_sql_fcache(FmgrInfo *finfo, Oid collation, bool lazyEvalOK) init_sql_fcache(FunctionCallInfo fcinfo, Oid collation, bool lazyEvalOK)
{ {
FmgrInfo *finfo = fcinfo->flinfo;
Oid foid = finfo->fn_oid; Oid foid = finfo->fn_oid;
MemoryContext fcontext; MemoryContext fcontext;
MemoryContext oldcontext; MemoryContext oldcontext;
Oid rettype; Oid rettype;
TupleDesc rettupdesc;
HeapTuple procedureTuple; HeapTuple procedureTuple;
Form_pg_proc procedureStruct; Form_pg_proc procedureStruct;
SQLFunctionCachePtr fcache; SQLFunctionCachePtr fcache;
List *raw_parsetree_list; List *raw_parsetree_list;
List *queryTree_list; List *queryTree_list;
List *flat_query_list; List *flat_query_list;
List *resulttlist;
ListCell *lc; ListCell *lc;
Datum tmp; Datum tmp;
bool isNull; bool isNull;
...@@ -642,20 +650,10 @@ init_sql_fcache(FmgrInfo *finfo, Oid collation, bool lazyEvalOK) ...@@ -642,20 +650,10 @@ init_sql_fcache(FmgrInfo *finfo, Oid collation, bool lazyEvalOK)
MemoryContextSetIdentifier(fcontext, fcache->fname); MemoryContextSetIdentifier(fcontext, fcache->fname);
/* /*
* get the result type from the procedure tuple, and check for polymorphic * Resolve any polymorphism, obtaining the actual result type, and the
* result type; if so, find out the actual result type. * corresponding tupdesc if it's a rowtype.
*/ */
rettype = procedureStruct->prorettype; (void) get_call_result_type(fcinfo, &rettype, &rettupdesc);
if (IsPolymorphicType(rettype))
{
rettype = get_fn_expr_rettype(finfo);
if (rettype == InvalidOid) /* this probably should not happen */
ereport(ERROR,
(errcode(ERRCODE_DATATYPE_MISMATCH),
errmsg("could not determine actual result type for function declared to return type %s",
format_type_be(procedureStruct->prorettype))));
}
fcache->rettype = rettype; fcache->rettype = rettype;
...@@ -728,8 +726,11 @@ init_sql_fcache(FmgrInfo *finfo, Oid collation, bool lazyEvalOK) ...@@ -728,8 +726,11 @@ init_sql_fcache(FmgrInfo *finfo, Oid collation, bool lazyEvalOK)
* Check that the function returns the type it claims to. Although in * Check that the function returns the type it claims to. Although in
* simple cases this was already done when the function was defined, we * simple cases this was already done when the function was defined, we
* have to recheck because database objects used in the function's queries * have to recheck because database objects used in the function's queries
* might have changed type. We'd have to do it anyway if the function had * might have changed type. We'd have to recheck anyway if the function
* any polymorphic arguments. * had any polymorphic arguments. Moreover, check_sql_fn_retval takes
* care of injecting any required column type coercions. (But we don't
* ask it to insert nulls for dropped columns; the junkfilter handles
* that.)
* *
* Note: we set fcache->returnsTuple according to whether we are returning * Note: we set fcache->returnsTuple according to whether we are returning
* the whole tuple result or just a single column. In the latter case we * the whole tuple result or just a single column. In the latter case we
...@@ -738,16 +739,40 @@ init_sql_fcache(FmgrInfo *finfo, Oid collation, bool lazyEvalOK) ...@@ -738,16 +739,40 @@ init_sql_fcache(FmgrInfo *finfo, Oid collation, bool lazyEvalOK)
* lazy eval mode in that case; otherwise we'd need extra code to expand * lazy eval mode in that case; otherwise we'd need extra code to expand
* the rowtype column into multiple columns, since we have no way to * the rowtype column into multiple columns, since we have no way to
* notify the caller that it should do that.) * notify the caller that it should do that.)
*
* check_sql_fn_retval will also construct a JunkFilter we can use to
* coerce the returned rowtype to the desired form (unless the result type
* is VOID, in which case there's nothing to coerce to).
*/ */
fcache->returnsTuple = check_sql_fn_retval(foid, fcache->returnsTuple = check_sql_fn_retval(flat_query_list,
rettype, rettype,
flat_query_list, rettupdesc,
NULL, false,
&fcache->junkFilter); &resulttlist);
/*
* Construct a JunkFilter we can use to coerce the returned rowtype to the
* desired form, unless the result type is VOID, in which case there's
* nothing to coerce to. (XXX Frequently, the JunkFilter isn't doing
* anything very interesting, but much of this module expects it to be
* there anyway.)
*/
if (rettype != VOIDOID)
{
TupleTableSlot *slot = MakeSingleTupleTableSlot(NULL,
&TTSOpsMinimalTuple);
/*
* If the result is composite, *and* we are returning the whole tuple
* result, we need to insert nulls for any dropped columns. In the
* single-column-result case, there might be dropped columns within
* the composite column value, but it's not our problem here. There
* should be no resjunk entries in resulttlist, so in the second case
* the JunkFilter is certainly a no-op.
*/
if (rettupdesc && fcache->returnsTuple)
fcache->junkFilter = ExecInitJunkFilterConversion(resulttlist,
rettupdesc,
slot);
else
fcache->junkFilter = ExecInitJunkFilter(resulttlist, slot);
}
if (fcache->returnsTuple) if (fcache->returnsTuple)
{ {
...@@ -1049,7 +1074,7 @@ fmgr_sql(PG_FUNCTION_ARGS) ...@@ -1049,7 +1074,7 @@ fmgr_sql(PG_FUNCTION_ARGS)
if (fcache == NULL) if (fcache == NULL)
{ {
init_sql_fcache(fcinfo->flinfo, PG_GET_COLLATION(), lazyEvalOK); init_sql_fcache(fcinfo, PG_GET_COLLATION(), lazyEvalOK);
fcache = (SQLFunctionCachePtr) fcinfo->flinfo->fn_extra; fcache = (SQLFunctionCachePtr) fcinfo->flinfo->fn_extra;
} }
...@@ -1532,15 +1557,9 @@ check_sql_fn_statements(List *queryTreeList) ...@@ -1532,15 +1557,9 @@ check_sql_fn_statements(List *queryTreeList)
* check_sql_fn_retval() -- check return value of a list of sql parse trees. * check_sql_fn_retval() -- check return value of a list of sql parse trees.
* *
* The return value of a sql function is the value returned by the last * The return value of a sql function is the value returned by the last
* canSetTag query in the function. We do some ad-hoc type checking here * canSetTag query in the function. We do some ad-hoc type checking and
* to be sure that the user is returning the type he claims. There are * coercion here to ensure that the function returns what it's supposed to.
* also a couple of strange-looking features to assist callers in dealing * Note that we may actually modify the last query to make it match!
* with allowed special cases, such as binary-compatible result types.
*
* For a polymorphic function the passed rettype must be the actual resolved
* output type of the function; we should never see a polymorphic pseudotype
* such as ANYELEMENT as rettype. (This means we can't check the type during
* function definition of a polymorphic function.)
* *
* This function returns true if the sql function returns the entire tuple * This function returns true if the sql function returns the entire tuple
* result of its final statement, or false if it returns just the first column * result of its final statement, or false if it returns just the first column
...@@ -1550,45 +1569,47 @@ check_sql_fn_statements(List *queryTreeList) ...@@ -1550,45 +1569,47 @@ check_sql_fn_statements(List *queryTreeList)
* Note that because we allow "SELECT rowtype_expression", the result can be * Note that because we allow "SELECT rowtype_expression", the result can be
* false even when the declared function return type is a rowtype. * false even when the declared function return type is a rowtype.
* *
* If modifyTargetList isn't NULL, the function will modify the final * For a polymorphic function the passed rettype must be the actual resolved
* statement's targetlist in two cases: * output type of the function; we should never see a polymorphic pseudotype
* (1) if the tlist returns values that are binary-coercible to the expected * such as ANYELEMENT as rettype. (This means we can't check the type during
* type rather than being exactly the expected type. RelabelType nodes will * function definition of a polymorphic function.) If the function returns
* be inserted to make the result types match exactly. * composite, the passed rettupdesc should describe the expected output.
* (2) if there are dropped columns in the declared result rowtype. NULL * If rettupdesc is NULL, we can't verify that the output matches; that
* output columns will be inserted in the tlist to match them. * should only happen in fmgr_sql_validator(), or when the function returns
* (Obviously the caller must pass a parsetree that is okay to modify when * RECORD and the caller doesn't actually care which composite type it is.
* using this flag.) Note that this flag does not affect whether the tlist is * (Typically, rettype and rettupdesc are computed by get_call_result_type
* considered to be a legal match to the result type, only how we react to * or a sibling function.)
* allowed not-exact-match cases. *modifyTargetList will be set true iff *
* we had to make any "dangerous" changes that could modify the semantics of * In addition to coercing individual output columns, we can modify the
* the statement. If it is set true, the caller should not use the modified * output to include dummy NULL columns for any dropped columns appearing
* statement, but for simplicity we apply the changes anyway. * in rettupdesc. This is done only if the caller asks for it.
* *
* If junkFilter isn't NULL, then *junkFilter is set to a JunkFilter defined * If resultTargetList isn't NULL, then *resultTargetList is set to the
* to convert the function's tuple result to the correct output tuple type. * targetlist that defines the final statement's result. Exception: if the
* Exception: if the function is defined to return VOID then *junkFilter is * function is defined to return VOID then *resultTargetList is set to NIL.
* set to NULL.
*/ */
bool bool
check_sql_fn_retval(Oid func_id, Oid rettype, List *queryTreeList, check_sql_fn_retval(List *queryTreeList,
bool *modifyTargetList, Oid rettype, TupleDesc rettupdesc,
JunkFilter **junkFilter) bool insertDroppedCols,
List **resultTargetList)
{ {
bool is_tuple_result = false;
Query *parse; Query *parse;
List **tlist_ptr; ListCell *parse_cell;
List *tlist; List *tlist;
int tlistlen; int tlistlen;
bool tlist_is_modifiable;
char fn_typtype; char fn_typtype;
Oid restype; List *upper_tlist = NIL;
bool upper_tlist_nontrivial = false;
ListCell *lc; ListCell *lc;
/* Caller must have resolved any polymorphism */
AssertArg(!IsPolymorphicType(rettype)); AssertArg(!IsPolymorphicType(rettype));
if (modifyTargetList) if (resultTargetList)
*modifyTargetList = false; /* initialize for no change */ *resultTargetList = NIL; /* initialize in case of VOID result */
if (junkFilter)
*junkFilter = NULL; /* initialize in case of VOID result */
/* /*
* If it's declared to return VOID, we don't care what's in the function. * If it's declared to return VOID, we don't care what's in the function.
...@@ -1603,12 +1624,16 @@ check_sql_fn_retval(Oid func_id, Oid rettype, List *queryTreeList, ...@@ -1603,12 +1624,16 @@ check_sql_fn_retval(Oid func_id, Oid rettype, List *queryTreeList,
* the user wrote. * the user wrote.
*/ */
parse = NULL; parse = NULL;
parse_cell = NULL;
foreach(lc, queryTreeList) foreach(lc, queryTreeList)
{ {
Query *q = lfirst_node(Query, lc); Query *q = lfirst_node(Query, lc);
if (q->canSetTag) if (q->canSetTag)
{
parse = q; parse = q;
parse_cell = lc;
}
} }
/* /*
...@@ -1625,8 +1650,9 @@ check_sql_fn_retval(Oid func_id, Oid rettype, List *queryTreeList, ...@@ -1625,8 +1650,9 @@ check_sql_fn_retval(Oid func_id, Oid rettype, List *queryTreeList,
if (parse && if (parse &&
parse->commandType == CMD_SELECT) parse->commandType == CMD_SELECT)
{ {
tlist_ptr = &parse->targetList;
tlist = parse->targetList; tlist = parse->targetList;
/* tlist is modifiable unless it's a dummy in a setop query */
tlist_is_modifiable = (parse->setOperations == NULL);
} }
else if (parse && else if (parse &&
(parse->commandType == CMD_INSERT || (parse->commandType == CMD_INSERT ||
...@@ -1634,8 +1660,9 @@ check_sql_fn_retval(Oid func_id, Oid rettype, List *queryTreeList, ...@@ -1634,8 +1660,9 @@ check_sql_fn_retval(Oid func_id, Oid rettype, List *queryTreeList,
parse->commandType == CMD_DELETE) && parse->commandType == CMD_DELETE) &&
parse->returningList) parse->returningList)
{ {
tlist_ptr = &parse->returningList;
tlist = parse->returningList; tlist = parse->returningList;
/* returningList can always be modified */
tlist_is_modifiable = true;
} }
else else
{ {
...@@ -1650,7 +1677,12 @@ check_sql_fn_retval(Oid func_id, Oid rettype, List *queryTreeList, ...@@ -1650,7 +1677,12 @@ check_sql_fn_retval(Oid func_id, Oid rettype, List *queryTreeList,
/* /*
* OK, check that the targetlist returns something matching the declared * OK, check that the targetlist returns something matching the declared
* type. * type, and modify it if necessary. If possible, we insert any coercion
* steps right into the final statement's targetlist. However, that might
* risk changes in the statement's semantics --- we can't safely change
* the output type of a grouping column, for instance. In such cases we
* handle coercions by inserting an extra level of Query that effectively
* just does a projection.
*/ */
/* /*
...@@ -1667,8 +1699,7 @@ check_sql_fn_retval(Oid func_id, Oid rettype, List *queryTreeList, ...@@ -1667,8 +1699,7 @@ check_sql_fn_retval(Oid func_id, Oid rettype, List *queryTreeList,
{ {
/* /*
* For scalar-type returns, the target list must have exactly one * For scalar-type returns, the target list must have exactly one
* non-junk entry, and its type must agree with what the user * non-junk entry, and its type must be coercible to rettype.
* declared; except we allow binary-compatible types too.
*/ */
TargetEntry *tle; TargetEntry *tle;
...@@ -1683,30 +1714,16 @@ check_sql_fn_retval(Oid func_id, Oid rettype, List *queryTreeList, ...@@ -1683,30 +1714,16 @@ check_sql_fn_retval(Oid func_id, Oid rettype, List *queryTreeList,
tle = (TargetEntry *) linitial(tlist); tle = (TargetEntry *) linitial(tlist);
Assert(!tle->resjunk); Assert(!tle->resjunk);
restype = exprType((Node *) tle->expr); if (!coerce_fn_result_column(tle, rettype, -1,
if (!IsBinaryCoercible(restype, rettype)) tlist_is_modifiable,
&upper_tlist,
&upper_tlist_nontrivial))
ereport(ERROR, ereport(ERROR,
(errcode(ERRCODE_INVALID_FUNCTION_DEFINITION), (errcode(ERRCODE_INVALID_FUNCTION_DEFINITION),
errmsg("return type mismatch in function declared to return %s", errmsg("return type mismatch in function declared to return %s",
format_type_be(rettype)), format_type_be(rettype)),
errdetail("Actual return type is %s.", errdetail("Actual return type is %s.",
format_type_be(restype)))); format_type_be(exprType((Node *) tle->expr)))));
if (modifyTargetList && restype != rettype)
{
tle->expr = (Expr *) makeRelabelType(tle->expr,
rettype,
-1,
get_typcollation(rettype),
COERCE_IMPLICIT_CAST);
/* Relabel is dangerous if TLE is a sort/group or setop column */
if (tle->ressortgroupref != 0 || parse->setOperations)
*modifyTargetList = true;
}
/* Set up junk filter if needed */
if (junkFilter)
*junkFilter = ExecInitJunkFilter(tlist,
MakeSingleTupleTableSlot(NULL, &TTSOpsMinimalTuple));
} }
else if (fn_typtype == TYPTYPE_COMPOSITE || rettype == RECORDOID) else if (fn_typtype == TYPTYPE_COMPOSITE || rettype == RECORDOID)
{ {
...@@ -1715,26 +1732,29 @@ check_sql_fn_retval(Oid func_id, Oid rettype, List *queryTreeList, ...@@ -1715,26 +1732,29 @@ check_sql_fn_retval(Oid func_id, Oid rettype, List *queryTreeList,
* *
* Note that we will not consider a domain over composite to be a * Note that we will not consider a domain over composite to be a
* "rowtype" return type; it goes through the scalar case above. This * "rowtype" return type; it goes through the scalar case above. This
* is because SQL functions don't provide any implicit casting to the * is because we only provide column-by-column implicit casting, and
* result type, so there is no way to produce a domain-over-composite * will not cast the complete record result. So the only way to
* result except by computing it as an explicit single-column result. * produce a domain-over-composite result is to compute it as an
* explicit single-column result. The single-composite-column code
* path just below could handle such cases, but it won't be reached.
*/ */
TupleDesc tupdesc;
int tupnatts; /* physical number of columns in tuple */ int tupnatts; /* physical number of columns in tuple */
int tuplogcols; /* # of nondeleted columns in tuple */ int tuplogcols; /* # of nondeleted columns in tuple */
int colindex; /* physical column index */ int colindex; /* physical column index */
List *newtlist; /* new non-junk tlist entries */
List *junkattrs; /* new junk tlist entries */
/* /*
* If the target list is of length 1, and the type of the varnode in * If the target list has one non-junk entry, and that expression has
* the target list matches the declared return type, this is okay. * or can be coerced to the declared return type, take it as the
* This can happen, for example, where the body of the function is * result. This allows, for example, 'SELECT func2()', where func2
* 'SELECT func2()', where func2 has the same composite return type as * has the same composite return type as the function that's calling
* the function that's calling it. * it. This provision creates some ambiguity --- maybe the expression
* was meant to be the lone field of the composite result --- but it
* works well enough as long as we don't get too enthusiastic about
* inventing coercions from scalar to composite types.
* *
* XXX Note that if rettype is RECORD, the IsBinaryCoercible check * XXX Note that if rettype is RECORD and the expression is of a named
* will succeed for any composite restype. For the moment we rely on * composite type, or vice versa, this coercion will succeed, whether
* or not the record type really matches. For the moment we rely on
* runtime type checking to catch any discrepancy, but it'd be nice to * runtime type checking to catch any discrepancy, but it'd be nice to
* do better at parse time. * do better at parse time.
*/ */
...@@ -1743,78 +1763,46 @@ check_sql_fn_retval(Oid func_id, Oid rettype, List *queryTreeList, ...@@ -1743,78 +1763,46 @@ check_sql_fn_retval(Oid func_id, Oid rettype, List *queryTreeList,
TargetEntry *tle = (TargetEntry *) linitial(tlist); TargetEntry *tle = (TargetEntry *) linitial(tlist);
Assert(!tle->resjunk); Assert(!tle->resjunk);
restype = exprType((Node *) tle->expr); if (coerce_fn_result_column(tle, rettype, -1,
if (IsBinaryCoercible(restype, rettype)) tlist_is_modifiable,
{ &upper_tlist,
if (modifyTargetList && restype != rettype) &upper_tlist_nontrivial))
{ {
tle->expr = (Expr *) makeRelabelType(tle->expr, /* Note that we're NOT setting is_tuple_result */
rettype, goto tlist_coercion_finished;
-1,
get_typcollation(rettype),
COERCE_IMPLICIT_CAST);
/* Relabel is dangerous if sort/group or setop column */
if (tle->ressortgroupref != 0 || parse->setOperations)
*modifyTargetList = true;
}
/* Set up junk filter if needed */
if (junkFilter)
{
TupleTableSlot *slot =
MakeSingleTupleTableSlot(NULL, &TTSOpsMinimalTuple);
*junkFilter = ExecInitJunkFilter(tlist, slot);
}
return false; /* NOT returning whole tuple */
} }
} }
/* /*
* Is the rowtype fixed, or determined only at runtime? (Note we * If the caller didn't provide an expected tupdesc, we can't do any
* cannot see TYPEFUNC_COMPOSITE_DOMAIN here.) * further checking. Assume we're returning the whole tuple.
*/ */
if (get_func_result_type(func_id, NULL, &tupdesc) != TYPEFUNC_COMPOSITE) if (rettupdesc == NULL)
{ {
/* /* Return tlist if requested */
* Assume we are returning the whole tuple. Crosschecking against if (resultTargetList)
* what the caller expects will happen at runtime. *resultTargetList = tlist;
*/
if (junkFilter)
{
TupleTableSlot *slot;
slot = MakeSingleTupleTableSlot(NULL, &TTSOpsMinimalTuple);
*junkFilter = ExecInitJunkFilter(tlist, slot);
}
return true; return true;
} }
Assert(tupdesc);
/* /*
* Verify that the targetlist matches the return tuple type. We scan * Verify that the targetlist matches the return tuple type. We scan
* the non-deleted attributes to ensure that they match the datatypes * the non-resjunk columns, and coerce them if necessary to match the
* of the non-resjunk columns. For deleted attributes, insert NULL * datatypes of the non-deleted attributes. For deleted attributes,
* result columns if the caller asked for that. * insert NULL result columns if the caller asked for that.
*/ */
tupnatts = tupdesc->natts; tupnatts = rettupdesc->natts;
tuplogcols = 0; /* we'll count nondeleted cols as we go */ tuplogcols = 0; /* we'll count nondeleted cols as we go */
colindex = 0; colindex = 0;
newtlist = NIL; /* these are only used if modifyTargetList */
junkattrs = NIL;
foreach(lc, tlist) foreach(lc, tlist)
{ {
TargetEntry *tle = (TargetEntry *) lfirst(lc); TargetEntry *tle = (TargetEntry *) lfirst(lc);
Form_pg_attribute attr; Form_pg_attribute attr;
Oid tletype;
Oid atttype;
/* resjunk columns can simply be ignored */
if (tle->resjunk) if (tle->resjunk)
{
if (modifyTargetList)
junkattrs = lappend(junkattrs, tle);
continue; continue;
}
do do
{ {
...@@ -1825,8 +1813,8 @@ check_sql_fn_retval(Oid func_id, Oid rettype, List *queryTreeList, ...@@ -1825,8 +1813,8 @@ check_sql_fn_retval(Oid func_id, Oid rettype, List *queryTreeList,
errmsg("return type mismatch in function declared to return %s", errmsg("return type mismatch in function declared to return %s",
format_type_be(rettype)), format_type_be(rettype)),
errdetail("Final statement returns too many columns."))); errdetail("Final statement returns too many columns.")));
attr = TupleDescAttr(tupdesc, colindex - 1); attr = TupleDescAttr(rettupdesc, colindex - 1);
if (attr->attisdropped && modifyTargetList) if (attr->attisdropped && insertDroppedCols)
{ {
Expr *null_expr; Expr *null_expr;
...@@ -1838,57 +1826,41 @@ check_sql_fn_retval(Oid func_id, Oid rettype, List *queryTreeList, ...@@ -1838,57 +1826,41 @@ check_sql_fn_retval(Oid func_id, Oid rettype, List *queryTreeList,
(Datum) 0, (Datum) 0,
true, /* isnull */ true, /* isnull */
true /* byval */ ); true /* byval */ );
newtlist = lappend(newtlist, upper_tlist = lappend(upper_tlist,
makeTargetEntry(null_expr, makeTargetEntry(null_expr,
colindex, list_length(upper_tlist) + 1,
NULL, NULL,
false)); false));
/* NULL insertion is dangerous in a setop */ upper_tlist_nontrivial = true;
if (parse->setOperations)
*modifyTargetList = true;
} }
} while (attr->attisdropped); } while (attr->attisdropped);
tuplogcols++; tuplogcols++;
tletype = exprType((Node *) tle->expr); if (!coerce_fn_result_column(tle,
atttype = attr->atttypid; attr->atttypid, attr->atttypmod,
if (!IsBinaryCoercible(tletype, atttype)) tlist_is_modifiable,
&upper_tlist,
&upper_tlist_nontrivial))
ereport(ERROR, ereport(ERROR,
(errcode(ERRCODE_INVALID_FUNCTION_DEFINITION), (errcode(ERRCODE_INVALID_FUNCTION_DEFINITION),
errmsg("return type mismatch in function declared to return %s", errmsg("return type mismatch in function declared to return %s",
format_type_be(rettype)), format_type_be(rettype)),
errdetail("Final statement returns %s instead of %s at column %d.", errdetail("Final statement returns %s instead of %s at column %d.",
format_type_be(tletype), format_type_be(exprType((Node *) tle->expr)),
format_type_be(atttype), format_type_be(attr->atttypid),
tuplogcols))); tuplogcols)));
if (modifyTargetList)
{
if (tletype != atttype)
{
tle->expr = (Expr *) makeRelabelType(tle->expr,
atttype,
-1,
get_typcollation(atttype),
COERCE_IMPLICIT_CAST);
/* Relabel is dangerous if sort/group or setop column */
if (tle->ressortgroupref != 0 || parse->setOperations)
*modifyTargetList = true;
}
tle->resno = colindex;
newtlist = lappend(newtlist, tle);
}
} }
/* remaining columns in tupdesc had better all be dropped */ /* remaining columns in rettupdesc had better all be dropped */
for (colindex++; colindex <= tupnatts; colindex++) for (colindex++; colindex <= tupnatts; colindex++)
{ {
if (!TupleDescAttr(tupdesc, colindex - 1)->attisdropped) if (!TupleDescAttr(rettupdesc, colindex - 1)->attisdropped)
ereport(ERROR, ereport(ERROR,
(errcode(ERRCODE_INVALID_FUNCTION_DEFINITION), (errcode(ERRCODE_INVALID_FUNCTION_DEFINITION),
errmsg("return type mismatch in function declared to return %s", errmsg("return type mismatch in function declared to return %s",
format_type_be(rettype)), format_type_be(rettype)),
errdetail("Final statement returns too few columns."))); errdetail("Final statement returns too few columns.")));
if (modifyTargetList) if (insertDroppedCols)
{ {
Expr *null_expr; Expr *null_expr;
...@@ -1900,51 +1872,153 @@ check_sql_fn_retval(Oid func_id, Oid rettype, List *queryTreeList, ...@@ -1900,51 +1872,153 @@ check_sql_fn_retval(Oid func_id, Oid rettype, List *queryTreeList,
(Datum) 0, (Datum) 0,
true, /* isnull */ true, /* isnull */
true /* byval */ ); true /* byval */ );
newtlist = lappend(newtlist, upper_tlist = lappend(upper_tlist,
makeTargetEntry(null_expr, makeTargetEntry(null_expr,
colindex, list_length(upper_tlist) + 1,
NULL, NULL,
false)); false));
/* NULL insertion is dangerous in a setop */ upper_tlist_nontrivial = true;
if (parse->setOperations)
*modifyTargetList = true;
} }
} }
if (modifyTargetList) /* Report that we are returning entire tuple result */
is_tuple_result = true;
}
else
ereport(ERROR,
(errcode(ERRCODE_INVALID_FUNCTION_DEFINITION),
errmsg("return type %s is not supported for SQL functions",
format_type_be(rettype))));
tlist_coercion_finished:
/*
* If necessary, modify the final Query by injecting an extra Query level
* that just performs a projection. (It'd be dubious to do this to a
* non-SELECT query, but we never have to; RETURNING lists can always be
* modified in-place.)
*/
if (upper_tlist_nontrivial)
{ {
/* ensure resjunk columns are numbered correctly */ Query *newquery;
foreach(lc, junkattrs) List *colnames;
RangeTblEntry *rte;
RangeTblRef *rtr;
Assert(parse->commandType == CMD_SELECT);
/* Most of the upper Query struct can be left as zeroes/nulls */
newquery = makeNode(Query);
newquery->commandType = CMD_SELECT;
newquery->querySource = parse->querySource;
newquery->canSetTag = true;
newquery->targetList = upper_tlist;
/* We need a moderately realistic colnames list for the subquery RTE */
colnames = NIL;
foreach(lc, parse->targetList)
{ {
TargetEntry *tle = (TargetEntry *) lfirst(lc); TargetEntry *tle = (TargetEntry *) lfirst(lc);
tle->resno = colindex++; if (tle->resjunk)
} continue;
/* replace the tlist with the modified one */ colnames = lappend(colnames,
*tlist_ptr = list_concat(newtlist, junkattrs); makeString(tle->resname ? tle->resname : ""));
} }
/* Set up junk filter if needed */ /* Build a suitable RTE for the subquery */
if (junkFilter) rte = makeNode(RangeTblEntry);
{ rte->rtekind = RTE_SUBQUERY;
TupleTableSlot *slot = rte->subquery = parse;
MakeSingleTupleTableSlot(NULL, &TTSOpsMinimalTuple); rte->eref = rte->alias = makeAlias("*SELECT*", colnames);
rte->lateral = false;
rte->inh = false;
rte->inFromCl = true;
newquery->rtable = list_make1(rte);
*junkFilter = ExecInitJunkFilterConversion(tlist, rtr = makeNode(RangeTblRef);
CreateTupleDescCopy(tupdesc), rtr->rtindex = 1;
slot); newquery->jointree = makeFromExpr(list_make1(rtr), NULL);
/* Replace original query in the correct element of the query list */
lfirst(parse_cell) = newquery;
} }
/* Report that we are returning entire tuple result */ /* Return tlist (possibly modified) if requested */
return true; if (resultTargetList)
*resultTargetList = upper_tlist;
return is_tuple_result;
}
/*
* Process one function result column for check_sql_fn_retval
*
* Coerce the output value to the required type/typmod, and add a column
* to *upper_tlist for it. Set *upper_tlist_nontrivial to true if we
* add an upper tlist item that's not just a Var.
*
* Returns true if OK, false if could not coerce to required type
* (in which case, no changes have been made)
*/
static bool
coerce_fn_result_column(TargetEntry *src_tle,
Oid res_type,
int32 res_typmod,
bool tlist_is_modifiable,
List **upper_tlist,
bool *upper_tlist_nontrivial)
{
TargetEntry *new_tle;
Expr *new_tle_expr;
Node *cast_result;
/*
* If the TLE has a sortgroupref marking, don't change it, as it probably
* is referenced by ORDER BY, DISTINCT, etc, and changing its type would
* break query semantics. Otherwise, it's safe to modify in-place unless
* the query as a whole has issues with that.
*/
if (tlist_is_modifiable && src_tle->ressortgroupref == 0)
{
/* OK to modify src_tle in place, if necessary */
cast_result = coerce_to_target_type(NULL,
(Node *) src_tle->expr,
exprType((Node *) src_tle->expr),
res_type, res_typmod,
COERCION_ASSIGNMENT,
COERCE_IMPLICIT_CAST,
-1);
if (cast_result == NULL)
return false;
src_tle->expr = (Expr *) cast_result;
/* Make a Var referencing the possibly-modified TLE */
new_tle_expr = (Expr *) makeVarFromTargetEntry(1, src_tle);
} }
else else
ereport(ERROR, {
(errcode(ERRCODE_INVALID_FUNCTION_DEFINITION), /* Any casting must happen in the upper tlist */
errmsg("return type %s is not supported for SQL functions", Var *var = makeVarFromTargetEntry(1, src_tle);
format_type_be(rettype))));
cast_result = coerce_to_target_type(NULL,
(Node *) var,
var->vartype,
res_type, res_typmod,
COERCION_ASSIGNMENT,
COERCE_IMPLICIT_CAST,
-1);
if (cast_result == NULL)
return false; return false;
/* Did the coercion actually do anything? */
if (cast_result != (Node *) var)
*upper_tlist_nontrivial = true;
new_tle_expr = (Expr *) cast_result;
}
new_tle = makeTargetEntry(new_tle_expr,
list_length(*upper_tlist) + 1,
src_tle->resname, false);
*upper_tlist = lappend(*upper_tlist, new_tle);
return true;
} }
......
...@@ -155,7 +155,6 @@ static Query *substitute_actual_srf_parameters(Query *expr, ...@@ -155,7 +155,6 @@ static Query *substitute_actual_srf_parameters(Query *expr,
int nargs, List *args); int nargs, List *args);
static Node *substitute_actual_srf_parameters_mutator(Node *node, static Node *substitute_actual_srf_parameters_mutator(Node *node,
substitute_actual_srf_parameters_context *context); substitute_actual_srf_parameters_context *context);
static bool tlist_matches_coltypelist(List *tlist, List *coltypelist);
/***************************************************************************** /*****************************************************************************
...@@ -4399,15 +4398,16 @@ inline_function(Oid funcid, Oid result_type, Oid result_collid, ...@@ -4399,15 +4398,16 @@ inline_function(Oid funcid, Oid result_type, Oid result_collid,
char *src; char *src;
Datum tmp; Datum tmp;
bool isNull; bool isNull;
bool modifyTargetList;
MemoryContext oldcxt; MemoryContext oldcxt;
MemoryContext mycxt; MemoryContext mycxt;
inline_error_callback_arg callback_arg; inline_error_callback_arg callback_arg;
ErrorContextCallback sqlerrcontext; ErrorContextCallback sqlerrcontext;
FuncExpr *fexpr; FuncExpr *fexpr;
SQLFunctionParseInfoPtr pinfo; SQLFunctionParseInfoPtr pinfo;
TupleDesc rettupdesc;
ParseState *pstate; ParseState *pstate;
List *raw_parsetree_list; List *raw_parsetree_list;
List *querytree_list;
Query *querytree; Query *querytree;
Node *newexpr; Node *newexpr;
int *usecounts; int *usecounts;
...@@ -4472,8 +4472,8 @@ inline_function(Oid funcid, Oid result_type, Oid result_collid, ...@@ -4472,8 +4472,8 @@ inline_function(Oid funcid, Oid result_type, Oid result_collid,
/* /*
* Set up to handle parameters while parsing the function body. We need a * Set up to handle parameters while parsing the function body. We need a
* dummy FuncExpr node containing the already-simplified arguments to pass * dummy FuncExpr node containing the already-simplified arguments to pass
* to prepare_sql_fn_parse_info. (It is really only needed if there are * to prepare_sql_fn_parse_info. (In some cases we don't really need
* some polymorphic arguments, but for simplicity we always build it.) * that, but for simplicity we always build it.)
*/ */
fexpr = makeNode(FuncExpr); fexpr = makeNode(FuncExpr);
fexpr->funcid = funcid; fexpr->funcid = funcid;
...@@ -4490,6 +4490,11 @@ inline_function(Oid funcid, Oid result_type, Oid result_collid, ...@@ -4490,6 +4490,11 @@ inline_function(Oid funcid, Oid result_type, Oid result_collid,
(Node *) fexpr, (Node *) fexpr,
input_collid); input_collid);
/* fexpr also provides a convenient way to resolve a composite result */
(void) get_expr_result_type((Node *) fexpr,
NULL,
&rettupdesc);
/* /*
* We just do parsing and parse analysis, not rewriting, because rewriting * We just do parsing and parse analysis, not rewriting, because rewriting
* will not affect table-free-SELECT-only queries, which is all that we * will not affect table-free-SELECT-only queries, which is all that we
...@@ -4542,16 +4547,24 @@ inline_function(Oid funcid, Oid result_type, Oid result_collid, ...@@ -4542,16 +4547,24 @@ inline_function(Oid funcid, Oid result_type, Oid result_collid,
* Make sure the function (still) returns what it's declared to. This * Make sure the function (still) returns what it's declared to. This
* will raise an error if wrong, but that's okay since the function would * will raise an error if wrong, but that's okay since the function would
* fail at runtime anyway. Note that check_sql_fn_retval will also insert * fail at runtime anyway. Note that check_sql_fn_retval will also insert
* a RelabelType if needed to make the tlist expression match the declared * a coercion if needed to make the tlist expression match the declared
* type of the function. * type of the function.
* *
* Note: we do not try this until we have verified that no rewriting was * Note: we do not try this until we have verified that no rewriting was
* needed; that's probably not important, but let's be careful. * needed; that's probably not important, but let's be careful.
*/ */
if (check_sql_fn_retval(funcid, result_type, list_make1(querytree), querytree_list = list_make1(querytree);
&modifyTargetList, NULL)) if (check_sql_fn_retval(querytree_list, result_type, rettupdesc,
false, NULL))
goto fail; /* reject whole-tuple-result cases */ goto fail; /* reject whole-tuple-result cases */
/*
* Given the tests above, check_sql_fn_retval shouldn't have decided to
* inject a projection step, but let's just make sure.
*/
if (querytree != linitial(querytree_list))
goto fail;
/* Now we can grab the tlist expression */ /* Now we can grab the tlist expression */
newexpr = (Node *) ((TargetEntry *) linitial(querytree->targetList))->expr; newexpr = (Node *) ((TargetEntry *) linitial(querytree->targetList))->expr;
...@@ -4566,9 +4579,6 @@ inline_function(Oid funcid, Oid result_type, Oid result_collid, ...@@ -4566,9 +4579,6 @@ inline_function(Oid funcid, Oid result_type, Oid result_collid,
if (exprType(newexpr) != result_type) if (exprType(newexpr) != result_type)
goto fail; goto fail;
/* check_sql_fn_retval couldn't have made any dangerous tlist changes */
Assert(!modifyTargetList);
/* /*
* Additional validity checks on the expression. It mustn't be more * Additional validity checks on the expression. It mustn't be more
* volatile than the surrounding function (this is to avoid breaking hacks * volatile than the surrounding function (this is to avoid breaking hacks
...@@ -4877,12 +4887,13 @@ inline_set_returning_function(PlannerInfo *root, RangeTblEntry *rte) ...@@ -4877,12 +4887,13 @@ inline_set_returning_function(PlannerInfo *root, RangeTblEntry *rte)
char *src; char *src;
Datum tmp; Datum tmp;
bool isNull; bool isNull;
bool modifyTargetList;
MemoryContext oldcxt; MemoryContext oldcxt;
MemoryContext mycxt; MemoryContext mycxt;
inline_error_callback_arg callback_arg; inline_error_callback_arg callback_arg;
ErrorContextCallback sqlerrcontext; ErrorContextCallback sqlerrcontext;
SQLFunctionParseInfoPtr pinfo; SQLFunctionParseInfoPtr pinfo;
TypeFuncClass functypclass;
TupleDesc rettupdesc;
List *raw_parsetree_list; List *raw_parsetree_list;
List *querytree_list; List *querytree_list;
Query *querytree; Query *querytree;
...@@ -5012,6 +5023,18 @@ inline_set_returning_function(PlannerInfo *root, RangeTblEntry *rte) ...@@ -5012,6 +5023,18 @@ inline_set_returning_function(PlannerInfo *root, RangeTblEntry *rte)
(Node *) fexpr, (Node *) fexpr,
fexpr->inputcollid); fexpr->inputcollid);
/*
* Also resolve the actual function result tupdesc, if composite. If the
* function is just declared to return RECORD, dig the info out of the AS
* clause.
*/
functypclass = get_expr_result_type((Node *) fexpr, NULL, &rettupdesc);
if (functypclass == TYPEFUNC_RECORD)
rettupdesc = BuildDescFromLists(rtfunc->funccolnames,
rtfunc->funccoltypes,
rtfunc->funccoltypmods,
rtfunc->funccolcollations);
/* /*
* Parse, analyze, and rewrite (unlike inline_function(), we can't skip * Parse, analyze, and rewrite (unlike inline_function(), we can't skip
* rewriting here). We can fail as soon as we find more than one query, * rewriting here). We can fail as soon as we find more than one query,
...@@ -5040,43 +5063,28 @@ inline_set_returning_function(PlannerInfo *root, RangeTblEntry *rte) ...@@ -5040,43 +5063,28 @@ inline_set_returning_function(PlannerInfo *root, RangeTblEntry *rte)
* Make sure the function (still) returns what it's declared to. This * Make sure the function (still) returns what it's declared to. This
* will raise an error if wrong, but that's okay since the function would * will raise an error if wrong, but that's okay since the function would
* fail at runtime anyway. Note that check_sql_fn_retval will also insert * fail at runtime anyway. Note that check_sql_fn_retval will also insert
* RelabelType(s) and/or NULL columns if needed to make the tlist * coercions if needed to make the tlist expression(s) match the declared
* expression(s) match the declared type of the function. * type of the function. We also ask it to insert dummy NULL columns for
* any dropped columns in rettupdesc, so that the elements of the modified
* tlist match up to the attribute numbers.
* *
* If the function returns a composite type, don't inline unless the check * If the function returns a composite type, don't inline unless the check
* shows it's returning a whole tuple result; otherwise what it's * shows it's returning a whole tuple result; otherwise what it's
* returning is a single composite column which is not what we need. (Like * returning is a single composite column which is not what we need.
* check_sql_fn_retval, we deliberately exclude domains over composite */
* here.) if (!check_sql_fn_retval(querytree_list,
*/ fexpr->funcresulttype, rettupdesc,
if (!check_sql_fn_retval(func_oid, fexpr->funcresulttype, true, NULL) &&
querytree_list, (functypclass == TYPEFUNC_COMPOSITE ||
&modifyTargetList, NULL) && functypclass == TYPEFUNC_COMPOSITE_DOMAIN ||
(get_typtype(fexpr->funcresulttype) == TYPTYPE_COMPOSITE || functypclass == TYPEFUNC_RECORD))
fexpr->funcresulttype == RECORDOID))
goto fail; /* reject not-whole-tuple-result cases */ goto fail; /* reject not-whole-tuple-result cases */
/* /*
* If we had to modify the tlist to make it match, and the statement is * check_sql_fn_retval might've inserted a projection step, but that's
* one in which changing the tlist contents could change semantics, we * fine; just make sure we use the upper Query.
* have to punt and not inline.
*/
if (modifyTargetList)
goto fail;
/*
* If it returns RECORD, we have to check against the column type list
* provided in the RTE; check_sql_fn_retval can't do that. (If no match,
* we just fail to inline, rather than complaining; see notes for
* tlist_matches_coltypelist.) We don't have to do this for functions
* with declared OUT parameters, even though their funcresulttype is
* RECORDOID, so check get_func_result_type too.
*/ */
if (fexpr->funcresulttype == RECORDOID && querytree = linitial(querytree_list);
get_func_result_type(func_oid, NULL, NULL) == TYPEFUNC_RECORD &&
!tlist_matches_coltypelist(querytree->targetList,
rtfunc->funccoltypes))
goto fail;
/* /*
* Looks good --- substitute parameters into the query. * Looks good --- substitute parameters into the query.
...@@ -5181,46 +5189,3 @@ substitute_actual_srf_parameters_mutator(Node *node, ...@@ -5181,46 +5189,3 @@ substitute_actual_srf_parameters_mutator(Node *node,
substitute_actual_srf_parameters_mutator, substitute_actual_srf_parameters_mutator,
(void *) context); (void *) context);
} }
/*
* Check whether a SELECT targetlist emits the specified column types,
* to see if it's safe to inline a function returning record.
*
* We insist on exact match here. The executor allows binary-coercible
* cases too, but we don't have a way to preserve the correct column types
* in the correct places if we inline the function in such a case.
*
* Note that we only check type OIDs not typmods; this agrees with what the
* executor would do at runtime, and attributing a specific typmod to a
* function result is largely wishful thinking anyway.
*/
static bool
tlist_matches_coltypelist(List *tlist, List *coltypelist)
{
ListCell *tlistitem;
ListCell *clistitem;
clistitem = list_head(coltypelist);
foreach(tlistitem, tlist)
{
TargetEntry *tle = (TargetEntry *) lfirst(tlistitem);
Oid coltype;
if (tle->resjunk)
continue; /* ignore junk columns */
if (clistitem == NULL)
return false; /* too many tlist items */
coltype = lfirst_oid(clistitem);
clistitem = lnext(coltypelist, clistitem);
if (exprType((Node *) tle->expr) != coltype)
return false; /* column type mismatch */
}
if (clistitem != NULL)
return false; /* too few tlist items */
return true;
}
...@@ -31,10 +31,10 @@ extern void sql_fn_parser_setup(struct ParseState *pstate, ...@@ -31,10 +31,10 @@ extern void sql_fn_parser_setup(struct ParseState *pstate,
extern void check_sql_fn_statements(List *queryTreeList); extern void check_sql_fn_statements(List *queryTreeList);
extern bool check_sql_fn_retval(Oid func_id, Oid rettype, extern bool check_sql_fn_retval(List *queryTreeList,
List *queryTreeList, Oid rettype, TupleDesc rettupdesc,
bool *modifyTargetList, bool insertDroppedCols,
JunkFilter **junkFilter); List **resultTargetList);
extern DestReceiver *CreateSQLFunctionDestReceiver(void); extern DestReceiver *CreateSQLFunctionDestReceiver(void);
......
...@@ -1820,6 +1820,67 @@ select * from array_to_set(array['one', 'two']); -- fail ...@@ -1820,6 +1820,67 @@ select * from array_to_set(array['one', 'two']); -- fail
ERROR: a column definition list is required for functions returning "record" ERROR: a column definition list is required for functions returning "record"
LINE 1: select * from array_to_set(array['one', 'two']); LINE 1: select * from array_to_set(array['one', 'two']);
^ ^
-- after-the-fact coercion of the columns is now possible, too
select * from array_to_set(array['one', 'two']) as t(f1 numeric(4,2),f2 text);
f1 | f2
------+-----
1.00 | one
2.00 | two
(2 rows)
-- and if it doesn't work, you get a compile-time not run-time error
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
-- 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);
QUERY PLAN
----------------------------------------------------
Function Scan on public.array_to_set t
Output: f1, f2
Function Call: array_to_set('{one,two}'::text[])
(3 rows)
-- but without, it can be:
create or replace function array_to_set(anyarray) returns setof record as $$
select i AS "index", $1[i] AS "value" from generate_subscripts($1, 1) i
$$ language sql immutable;
select array_to_set(array['one', 'two']);
array_to_set
--------------
(1,one)
(2,two)
(2 rows)
select * from array_to_set(array['one', 'two']) as t(f1 int,f2 text);
f1 | f2
----+-----
1 | one
2 | two
(2 rows)
select * from array_to_set(array['one', 'two']) as t(f1 numeric(4,2),f2 text);
f1 | f2
------+-----
1.00 | one
2.00 | two
(2 rows)
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 inlining
explain (verbose, costs off)
select * from array_to_set(array['one', 'two']) as t(f1 numeric(4,2),f2 text);
QUERY PLAN
--------------------------------------------------------------
Function Scan on pg_catalog.generate_subscripts i
Output: i.i, ('{one,two}'::text[])[i.i]
Function Call: generate_subscripts('{one,two}'::text[], 1)
(3 rows)
create temp table rngfunc(f1 int8, f2 int8); create temp table rngfunc(f1 int8, f2 int8);
create function testrngfunc() returns record as $$ create function testrngfunc() returns record as $$
insert into rngfunc values (1,2) returning *; insert into rngfunc values (1,2) returning *;
...@@ -1863,6 +1924,140 @@ ERROR: a column definition list is required for functions returning "record" ...@@ -1863,6 +1924,140 @@ ERROR: a column definition list is required for functions returning "record"
LINE 1: select * from testrngfunc(); LINE 1: select * from testrngfunc();
^ ^
drop function testrngfunc(); drop function testrngfunc();
-- Check that typmod imposed by a composite type is honored
create type rngfunc_type as (f1 numeric(35,6), f2 numeric(35,2));
create function testrngfunc() returns rngfunc_type as $$
select 7.136178319899999964, 7.136178319899999964;
$$ language sql immutable;
explain (verbose, costs off)
select testrngfunc();
QUERY PLAN
-------------------------------------------
Result
Output: '(7.136178,7.14)'::rngfunc_type
(2 rows)
select testrngfunc();
testrngfunc
-----------------
(7.136178,7.14)
(1 row)
explain (verbose, costs off)
select * from testrngfunc();
QUERY PLAN
--------------------------------------------------
Function Scan on testrngfunc
Output: f1, f2
Function Call: '(7.136178,7.14)'::rngfunc_type
(3 rows)
select * from testrngfunc();
f1 | f2
----------+------
7.136178 | 7.14
(1 row)
create or replace function testrngfunc() returns rngfunc_type as $$
select 7.136178319899999964, 7.136178319899999964;
$$ language sql volatile;
explain (verbose, costs off)
select testrngfunc();
QUERY PLAN
-------------------------
Result
Output: testrngfunc()
(2 rows)
select testrngfunc();
testrngfunc
-----------------
(7.136178,7.14)
(1 row)
explain (verbose, costs off)
select * from testrngfunc();
QUERY PLAN
-------------------------------------
Function Scan on public.testrngfunc
Output: f1, f2
Function Call: testrngfunc()
(3 rows)
select * from testrngfunc();
f1 | f2
----------+------
7.136178 | 7.14
(1 row)
drop function testrngfunc();
create function testrngfunc() returns setof rngfunc_type as $$
select 7.136178319899999964, 7.136178319899999964;
$$ language sql immutable;
explain (verbose, costs off)
select testrngfunc();
QUERY PLAN
-------------------------
ProjectSet
Output: testrngfunc()
-> Result
(3 rows)
select testrngfunc();
testrngfunc
-----------------
(7.136178,7.14)
(1 row)
explain (verbose, costs off)
select * from testrngfunc();
QUERY PLAN
--------------------------------------------------------
Result
Output: 7.136178::numeric(35,6), 7.14::numeric(35,2)
(2 rows)
select * from testrngfunc();
f1 | f2
----------+------
7.136178 | 7.14
(1 row)
create or replace function testrngfunc() returns setof rngfunc_type as $$
select 7.136178319899999964, 7.136178319899999964;
$$ language sql volatile;
explain (verbose, costs off)
select testrngfunc();
QUERY PLAN
-------------------------
ProjectSet
Output: testrngfunc()
-> Result
(3 rows)
select testrngfunc();
testrngfunc
-----------------
(7.136178,7.14)
(1 row)
explain (verbose, costs off)
select * from testrngfunc();
QUERY PLAN
-------------------------------------
Function Scan on public.testrngfunc
Output: f1, f2
Function Call: testrngfunc()
(3 rows)
select * from testrngfunc();
f1 | f2
----------+------
7.136178 | 7.14
(1 row)
drop type rngfunc_type cascade;
NOTICE: drop cascades to function testrngfunc()
-- --
-- Check some cases involving added/dropped columns in a rowtype result -- Check some cases involving added/dropped columns in a rowtype result
-- --
...@@ -1955,7 +2150,7 @@ drop view usersview; ...@@ -1955,7 +2150,7 @@ drop view usersview;
drop function get_first_user(); drop function get_first_user();
drop function get_users(); drop function get_users();
drop table users; drop table users;
-- this won't get inlined because of type coercion, but it shouldn't fail -- check behavior with type coercion required for a set-op
create or replace function rngfuncbar() returns setof text as create or replace function rngfuncbar() returns setof text as
$$ select 'foo'::varchar union all select 'bar'::varchar ; $$ $$ select 'foo'::varchar union all select 'bar'::varchar ; $$
language sql stable; language sql stable;
...@@ -1973,6 +2168,19 @@ select * from rngfuncbar(); ...@@ -1973,6 +2168,19 @@ select * from rngfuncbar();
bar bar
(2 rows) (2 rows)
-- this function is now inlinable, too:
explain (verbose, costs off) select * from rngfuncbar();
QUERY PLAN
------------------------------------------------
Result
Output: ('foo'::character varying)
-> Append
-> Result
Output: 'foo'::character varying
-> Result
Output: 'bar'::character varying
(7 rows)
drop function rngfuncbar(); drop function rngfuncbar();
-- check handling of a SQL function with multiple OUT params (bug #5777) -- check handling of a SQL function with multiple OUT params (bug #5777)
create or replace function rngfuncbar(out integer, out numeric) as create or replace function rngfuncbar(out integer, out numeric) as
......
...@@ -515,6 +515,27 @@ $$ language sql strict immutable; ...@@ -515,6 +515,27 @@ $$ language sql strict immutable;
select array_to_set(array['one', 'two']); select array_to_set(array['one', 'two']);
select * from array_to_set(array['one', 'two']) as t(f1 int,f2 text); select * from array_to_set(array['one', 'two']) as t(f1 int,f2 text);
select * from array_to_set(array['one', 'two']); -- fail select * from array_to_set(array['one', 'two']); -- fail
-- after-the-fact coercion of the columns is now possible, too
select * from array_to_set(array['one', 'two']) as t(f1 numeric(4,2),f2 text);
-- and if it doesn't work, you get a compile-time not run-time error
select * from array_to_set(array['one', 'two']) as t(f1 point,f2 text);
-- 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);
-- but without, it can be:
create or replace function array_to_set(anyarray) returns setof record as $$
select i AS "index", $1[i] AS "value" from generate_subscripts($1, 1) i
$$ language sql immutable;
select array_to_set(array['one', 'two']);
select * from array_to_set(array['one', 'two']) as t(f1 int,f2 text);
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);
explain (verbose, costs off)
select * from array_to_set(array['one', 'two']) as t(f1 numeric(4,2),f2 text);
create temp table rngfunc(f1 int8, f2 int8); create temp table rngfunc(f1 int8, f2 int8);
...@@ -538,6 +559,57 @@ select * from testrngfunc(); -- fail ...@@ -538,6 +559,57 @@ select * from testrngfunc(); -- fail
drop function testrngfunc(); drop function testrngfunc();
-- Check that typmod imposed by a composite type is honored
create type rngfunc_type as (f1 numeric(35,6), f2 numeric(35,2));
create function testrngfunc() returns rngfunc_type as $$
select 7.136178319899999964, 7.136178319899999964;
$$ language sql immutable;
explain (verbose, costs off)
select testrngfunc();
select testrngfunc();
explain (verbose, costs off)
select * from testrngfunc();
select * from testrngfunc();
create or replace function testrngfunc() returns rngfunc_type as $$
select 7.136178319899999964, 7.136178319899999964;
$$ language sql volatile;
explain (verbose, costs off)
select testrngfunc();
select testrngfunc();
explain (verbose, costs off)
select * from testrngfunc();
select * from testrngfunc();
drop function testrngfunc();
create function testrngfunc() returns setof rngfunc_type as $$
select 7.136178319899999964, 7.136178319899999964;
$$ language sql immutable;
explain (verbose, costs off)
select testrngfunc();
select testrngfunc();
explain (verbose, costs off)
select * from testrngfunc();
select * from testrngfunc();
create or replace function testrngfunc() returns setof rngfunc_type as $$
select 7.136178319899999964, 7.136178319899999964;
$$ language sql volatile;
explain (verbose, costs off)
select testrngfunc();
select testrngfunc();
explain (verbose, costs off)
select * from testrngfunc();
select * from testrngfunc();
drop type rngfunc_type cascade;
-- --
-- Check some cases involving added/dropped columns in a rowtype result -- Check some cases involving added/dropped columns in a rowtype result
-- --
...@@ -585,7 +657,7 @@ drop function get_first_user(); ...@@ -585,7 +657,7 @@ drop function get_first_user();
drop function get_users(); drop function get_users();
drop table users; drop table users;
-- this won't get inlined because of type coercion, but it shouldn't fail -- check behavior with type coercion required for a set-op
create or replace function rngfuncbar() returns setof text as create or replace function rngfuncbar() returns setof text as
$$ select 'foo'::varchar union all select 'bar'::varchar ; $$ $$ select 'foo'::varchar union all select 'bar'::varchar ; $$
...@@ -593,6 +665,8 @@ language sql stable; ...@@ -593,6 +665,8 @@ language sql stable;
select rngfuncbar(); select rngfuncbar();
select * from rngfuncbar(); select * from rngfuncbar();
-- this function is now inlinable, too:
explain (verbose, costs off) select * from rngfuncbar();
drop function rngfuncbar(); drop function rngfuncbar();
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment