From 913bbd88dc6b859c70ebb48107b38d693c4c6673 Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Wed, 8 Jan 2020 11:07:53 -0500 Subject: [PATCH] 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 --- doc/src/sgml/xfunc.sgml | 44 +- src/backend/catalog/pg_proc.c | 11 +- src/backend/executor/functions.c | 508 +++++++++++++---------- src/backend/optimizer/util/clauses.c | 131 +++--- src/include/executor/functions.h | 8 +- src/test/regress/expected/rangefuncs.out | 210 +++++++++- src/test/regress/sql/rangefuncs.sql | 76 +++- 7 files changed, 656 insertions(+), 332 deletions(-) diff --git a/doc/src/sgml/xfunc.sgml b/doc/src/sgml/xfunc.sgml index d9afd3be4d..5616524cfd 100644 --- a/doc/src/sgml/xfunc.sgml +++ b/doc/src/sgml/xfunc.sgml @@ -388,11 +388,15 @@ $$ LANGUAGE SQL; - A SQL function must return exactly its declared - result type. This may require inserting an explicit cast. + If the final SELECT or RETURNING + clause in a SQL function does not return exactly + the function's declared result + type, PostgreSQL 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 previous add_em function to return - type float8 instead. This won't work: + type float8 instead. It's sufficient to write CREATE FUNCTION add_em(integer, integer) RETURNS float8 AS $$ @@ -400,16 +404,10 @@ CREATE FUNCTION add_em(integer, integer) RETURNS float8 AS $$ $$ LANGUAGE SQL; - even though in other contexts PostgreSQL - would be willing to insert an implicit cast to - convert integer to float8. - We need to write it as - - -CREATE FUNCTION add_em(integer, integer) RETURNS float8 AS $$ - SELECT ($1 + $2)::float8; -$$ LANGUAGE SQL; - + since the integer sum can be implicitly cast + to float8. + (See or + for more about casts.) @@ -503,23 +501,24 @@ $$ LANGUAGE SQL; The select list order in the query must be exactly the same as - that in which the columns appear in the table associated - with the composite type. (Naming the columns, as we did above, + that in which the columns appear in the composite type. + (Naming the columns, as we did above, is irrelevant to the system.) - We must ensure each expression's type matches the corresponding - column of the composite type, inserting a cast if necessary. + We must ensure each expression's type can be cast to that of + the corresponding column of the composite type. Otherwise we'll get errors like this: -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. - As with the base-type case, the function will not insert any casts - automatically. + As with the base-type case, the system will not insert explicit + casts automatically, only implicit or assignment casts. @@ -542,8 +541,7 @@ $$ LANGUAGE SQL; Another example is that if we are trying to write a function that returns a domain over composite, rather than a plain composite type, 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 - the domain type. + since there is no way to cause a coercion of the whole row result. @@ -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: -ERROR: could not determine polymorphic type because input has type "unknown" +ERROR: could not determine polymorphic type because input has type unknown diff --git a/src/backend/catalog/pg_proc.c b/src/backend/catalog/pg_proc.c index 9dba42671a..5194dcaac0 100644 --- a/src/backend/catalog/pg_proc.c +++ b/src/backend/catalog/pg_proc.c @@ -923,6 +923,8 @@ fmgr_sql_validator(PG_FUNCTION_ARGS) * verify the result type. */ SQLFunctionParseInfoPtr pinfo; + Oid rettype; + TupleDesc rettupdesc; /* But first, set up parameter information */ pinfo = prepare_sql_fn_parse_info(tuple, NULL, InvalidOid); @@ -943,9 +945,12 @@ fmgr_sql_validator(PG_FUNCTION_ARGS) } check_sql_fn_statements(querytree_list); - (void) check_sql_fn_retval(funcoid, proc->prorettype, - querytree_list, - NULL, NULL); + + (void) get_func_result_type(funcoid, &rettype, &rettupdesc); + + (void) check_sql_fn_retval(querytree_list, + rettype, rettupdesc, + false, NULL); } error_context_stack = sqlerrcontext.previous; diff --git a/src/backend/executor/functions.c b/src/backend/executor/functions.c index ac298c0295..e8e1957075 100644 --- a/src/backend/executor/functions.c +++ b/src/backend/executor/functions.c @@ -154,7 +154,7 @@ static Node *sql_fn_resolve_param_name(SQLFunctionParseInfoPtr pinfo, static List *init_execution_state(List *queryTree_list, SQLFunctionCachePtr fcache, 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 bool postquel_getnext(execution_state *es, SQLFunctionCachePtr fcache); static void postquel_end(execution_state *es); @@ -166,6 +166,11 @@ static Datum postquel_get_single_result(TupleTableSlot *slot, MemoryContext resultcontext); static void sql_exec_error_callback(void *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 bool sqlfunction_receive(TupleTableSlot *slot, DestReceiver *self); static void sqlfunction_shutdown(DestReceiver *self); @@ -591,18 +596,21 @@ init_execution_state(List *queryTree_list, * Initialize the SQLFunctionCache for a SQL function */ 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; MemoryContext fcontext; MemoryContext oldcontext; Oid rettype; + TupleDesc rettupdesc; HeapTuple procedureTuple; Form_pg_proc procedureStruct; SQLFunctionCachePtr fcache; List *raw_parsetree_list; List *queryTree_list; List *flat_query_list; + List *resulttlist; ListCell *lc; Datum tmp; bool isNull; @@ -642,20 +650,10 @@ init_sql_fcache(FmgrInfo *finfo, Oid collation, bool lazyEvalOK) MemoryContextSetIdentifier(fcontext, fcache->fname); /* - * get the result type from the procedure tuple, and check for polymorphic - * result type; if so, find out the actual result type. + * Resolve any polymorphism, obtaining the actual result type, and the + * corresponding tupdesc if it's a rowtype. */ - rettype = procedureStruct->prorettype; - - 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)))); - } + (void) get_call_result_type(fcinfo, &rettype, &rettupdesc); fcache->rettype = rettype; @@ -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 * simple cases this was already done when the function was defined, we * have to recheck because database objects used in the function's queries - * might have changed type. We'd have to do it anyway if the function had - * any polymorphic arguments. + * might have changed type. We'd have to recheck anyway if the function + * had any polymorphic arguments. Moreover, check_sql_fn_retval takes + * care of injecting any required column type coercions. (But we don't + * ask it to insert nulls for dropped columns; the junkfilter handles + * that.) * * Note: we set fcache->returnsTuple according to whether we are returning * the whole tuple result or just a single column. In the latter case we @@ -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 * the rowtype column into multiple columns, since we have no way to * 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, - flat_query_list, - NULL, - &fcache->junkFilter); + rettupdesc, + false, + &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) { @@ -1049,7 +1074,7 @@ fmgr_sql(PG_FUNCTION_ARGS) 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; } @@ -1532,15 +1557,9 @@ check_sql_fn_statements(List *queryTreeList) * 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 - * canSetTag query in the function. We do some ad-hoc type checking here - * to be sure that the user is returning the type he claims. There are - * also a couple of strange-looking features to assist callers in dealing - * 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.) + * canSetTag query in the function. We do some ad-hoc type checking and + * coercion here to ensure that the function returns what it's supposed to. + * Note that we may actually modify the last query to make it match! * * 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 @@ -1550,45 +1569,47 @@ check_sql_fn_statements(List *queryTreeList) * Note that because we allow "SELECT rowtype_expression", the result can be * false even when the declared function return type is a rowtype. * - * If modifyTargetList isn't NULL, the function will modify the final - * statement's targetlist in two cases: - * (1) if the tlist returns values that are binary-coercible to the expected - * type rather than being exactly the expected type. RelabelType nodes will - * be inserted to make the result types match exactly. - * (2) if there are dropped columns in the declared result rowtype. NULL - * output columns will be inserted in the tlist to match them. - * (Obviously the caller must pass a parsetree that is okay to modify when - * using this flag.) Note that this flag does not affect whether the tlist is - * considered to be a legal match to the result type, only how we react to - * allowed not-exact-match cases. *modifyTargetList will be set true iff - * we had to make any "dangerous" changes that could modify the semantics of - * the statement. If it is set true, the caller should not use the modified - * statement, but for simplicity we apply the changes anyway. + * 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.) If the function returns + * composite, the passed rettupdesc should describe the expected output. + * If rettupdesc is NULL, we can't verify that the output matches; that + * should only happen in fmgr_sql_validator(), or when the function returns + * RECORD and the caller doesn't actually care which composite type it is. + * (Typically, rettype and rettupdesc are computed by get_call_result_type + * or a sibling function.) * - * If junkFilter isn't NULL, then *junkFilter is set to a JunkFilter defined - * to convert the function's tuple result to the correct output tuple type. - * Exception: if the function is defined to return VOID then *junkFilter is - * set to NULL. + * In addition to coercing individual output columns, we can modify the + * output to include dummy NULL columns for any dropped columns appearing + * in rettupdesc. This is done only if the caller asks for it. + * + * If resultTargetList isn't NULL, then *resultTargetList is set to the + * targetlist that defines the final statement's result. Exception: if the + * function is defined to return VOID then *resultTargetList is set to NIL. */ bool -check_sql_fn_retval(Oid func_id, Oid rettype, List *queryTreeList, - bool *modifyTargetList, - JunkFilter **junkFilter) +check_sql_fn_retval(List *queryTreeList, + Oid rettype, TupleDesc rettupdesc, + bool insertDroppedCols, + List **resultTargetList) { + bool is_tuple_result = false; Query *parse; - List **tlist_ptr; + ListCell *parse_cell; List *tlist; int tlistlen; + bool tlist_is_modifiable; char fn_typtype; - Oid restype; + List *upper_tlist = NIL; + bool upper_tlist_nontrivial = false; ListCell *lc; + /* Caller must have resolved any polymorphism */ AssertArg(!IsPolymorphicType(rettype)); - if (modifyTargetList) - *modifyTargetList = false; /* initialize for no change */ - if (junkFilter) - *junkFilter = NULL; /* initialize in case of VOID result */ + if (resultTargetList) + *resultTargetList = NIL; /* initialize in case of VOID result */ /* * If it's declared to return VOID, we don't care what's in the function. @@ -1603,12 +1624,16 @@ check_sql_fn_retval(Oid func_id, Oid rettype, List *queryTreeList, * the user wrote. */ parse = NULL; + parse_cell = NULL; foreach(lc, queryTreeList) { Query *q = lfirst_node(Query, lc); if (q->canSetTag) + { parse = q; + parse_cell = lc; + } } /* @@ -1625,8 +1650,9 @@ check_sql_fn_retval(Oid func_id, Oid rettype, List *queryTreeList, if (parse && parse->commandType == CMD_SELECT) { - tlist_ptr = &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 && (parse->commandType == CMD_INSERT || @@ -1634,8 +1660,9 @@ check_sql_fn_retval(Oid func_id, Oid rettype, List *queryTreeList, parse->commandType == CMD_DELETE) && parse->returningList) { - tlist_ptr = &parse->returningList; tlist = parse->returningList; + /* returningList can always be modified */ + tlist_is_modifiable = true; } else { @@ -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 - * 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, { /* * For scalar-type returns, the target list must have exactly one - * non-junk entry, and its type must agree with what the user - * declared; except we allow binary-compatible types too. + * non-junk entry, and its type must be coercible to rettype. */ TargetEntry *tle; @@ -1683,30 +1714,16 @@ check_sql_fn_retval(Oid func_id, Oid rettype, List *queryTreeList, tle = (TargetEntry *) linitial(tlist); Assert(!tle->resjunk); - restype = exprType((Node *) tle->expr); - if (!IsBinaryCoercible(restype, rettype)) + if (!coerce_fn_result_column(tle, rettype, -1, + tlist_is_modifiable, + &upper_tlist, + &upper_tlist_nontrivial)) ereport(ERROR, (errcode(ERRCODE_INVALID_FUNCTION_DEFINITION), errmsg("return type mismatch in function declared to return %s", format_type_be(rettype)), errdetail("Actual return type is %s.", - format_type_be(restype)))); - 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)); + format_type_be(exprType((Node *) tle->expr))))); } else if (fn_typtype == TYPTYPE_COMPOSITE || rettype == RECORDOID) { @@ -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 * "rowtype" return type; it goes through the scalar case above. This - * is because SQL functions don't provide any implicit casting to the - * result type, so there is no way to produce a domain-over-composite - * result except by computing it as an explicit single-column result. + * is because we only provide column-by-column implicit casting, and + * will not cast the complete record result. So the only way to + * 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 tuplogcols; /* # of nondeleted columns in tuple */ 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 - * the target list matches the declared return type, this is okay. - * This can happen, for example, where the body of the function is - * 'SELECT func2()', where func2 has the same composite return type as - * the function that's calling it. + * If the target list has one non-junk entry, and that expression has + * or can be coerced to the declared return type, take it as the + * result. This allows, for example, 'SELECT func2()', where func2 + * has the same composite return type as the function that's calling + * 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 - * will succeed for any composite restype. For the moment we rely on + * XXX Note that if rettype is RECORD and the expression is of a named + * 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 * do better at parse time. */ @@ -1743,78 +1763,46 @@ check_sql_fn_retval(Oid func_id, Oid rettype, List *queryTreeList, TargetEntry *tle = (TargetEntry *) linitial(tlist); Assert(!tle->resjunk); - restype = exprType((Node *) tle->expr); - if (IsBinaryCoercible(restype, rettype)) + if (coerce_fn_result_column(tle, rettype, -1, + tlist_is_modifiable, + &upper_tlist, + &upper_tlist_nontrivial)) { - if (modifyTargetList && restype != rettype) - { - tle->expr = (Expr *) makeRelabelType(tle->expr, - rettype, - -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 */ + /* Note that we're NOT setting is_tuple_result */ + goto tlist_coercion_finished; } } /* - * Is the rowtype fixed, or determined only at runtime? (Note we - * cannot see TYPEFUNC_COMPOSITE_DOMAIN here.) + * If the caller didn't provide an expected tupdesc, we can't do any + * further checking. Assume we're returning the whole tuple. */ - if (get_func_result_type(func_id, NULL, &tupdesc) != TYPEFUNC_COMPOSITE) + if (rettupdesc == NULL) { - /* - * Assume we are returning the whole tuple. Crosschecking against - * what the caller expects will happen at runtime. - */ - if (junkFilter) - { - TupleTableSlot *slot; - - slot = MakeSingleTupleTableSlot(NULL, &TTSOpsMinimalTuple); - *junkFilter = ExecInitJunkFilter(tlist, slot); - } + /* Return tlist if requested */ + if (resultTargetList) + *resultTargetList = tlist; return true; } - Assert(tupdesc); /* - * Verify that the targetlist matches the return tuple type. We scan - * the non-deleted attributes to ensure that they match the datatypes - * of the non-resjunk columns. For deleted attributes, insert NULL - * result columns if the caller asked for that. + * Verify that the targetlist matches the return tuple type. We scan + * the non-resjunk columns, and coerce them if necessary to match the + * datatypes of the non-deleted attributes. For deleted attributes, + * 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 */ colindex = 0; - newtlist = NIL; /* these are only used if modifyTargetList */ - junkattrs = NIL; foreach(lc, tlist) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr; - Oid tletype; - Oid atttype; + /* resjunk columns can simply be ignored */ if (tle->resjunk) - { - if (modifyTargetList) - junkattrs = lappend(junkattrs, tle); continue; - } do { @@ -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", format_type_be(rettype)), errdetail("Final statement returns too many columns."))); - attr = TupleDescAttr(tupdesc, colindex - 1); - if (attr->attisdropped && modifyTargetList) + attr = TupleDescAttr(rettupdesc, colindex - 1); + if (attr->attisdropped && insertDroppedCols) { Expr *null_expr; @@ -1838,57 +1826,41 @@ check_sql_fn_retval(Oid func_id, Oid rettype, List *queryTreeList, (Datum) 0, true, /* isnull */ true /* byval */ ); - newtlist = lappend(newtlist, - makeTargetEntry(null_expr, - colindex, - NULL, - false)); - /* NULL insertion is dangerous in a setop */ - if (parse->setOperations) - *modifyTargetList = true; + upper_tlist = lappend(upper_tlist, + makeTargetEntry(null_expr, + list_length(upper_tlist) + 1, + NULL, + false)); + upper_tlist_nontrivial = true; } } while (attr->attisdropped); tuplogcols++; - tletype = exprType((Node *) tle->expr); - atttype = attr->atttypid; - if (!IsBinaryCoercible(tletype, atttype)) + if (!coerce_fn_result_column(tle, + attr->atttypid, attr->atttypmod, + tlist_is_modifiable, + &upper_tlist, + &upper_tlist_nontrivial)) ereport(ERROR, (errcode(ERRCODE_INVALID_FUNCTION_DEFINITION), errmsg("return type mismatch in function declared to return %s", format_type_be(rettype)), errdetail("Final statement returns %s instead of %s at column %d.", - format_type_be(tletype), - format_type_be(atttype), + format_type_be(exprType((Node *) tle->expr)), + format_type_be(attr->atttypid), 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++) { - if (!TupleDescAttr(tupdesc, colindex - 1)->attisdropped) + if (!TupleDescAttr(rettupdesc, colindex - 1)->attisdropped) ereport(ERROR, (errcode(ERRCODE_INVALID_FUNCTION_DEFINITION), errmsg("return type mismatch in function declared to return %s", format_type_be(rettype)), errdetail("Final statement returns too few columns."))); - if (modifyTargetList) + if (insertDroppedCols) { Expr *null_expr; @@ -1900,43 +1872,17 @@ check_sql_fn_retval(Oid func_id, Oid rettype, List *queryTreeList, (Datum) 0, true, /* isnull */ true /* byval */ ); - newtlist = lappend(newtlist, - makeTargetEntry(null_expr, - colindex, - NULL, - false)); - /* NULL insertion is dangerous in a setop */ - if (parse->setOperations) - *modifyTargetList = true; + upper_tlist = lappend(upper_tlist, + makeTargetEntry(null_expr, + list_length(upper_tlist) + 1, + NULL, + false)); + upper_tlist_nontrivial = true; } } - if (modifyTargetList) - { - /* ensure resjunk columns are numbered correctly */ - foreach(lc, junkattrs) - { - TargetEntry *tle = (TargetEntry *) lfirst(lc); - - tle->resno = colindex++; - } - /* replace the tlist with the modified one */ - *tlist_ptr = list_concat(newtlist, junkattrs); - } - - /* Set up junk filter if needed */ - if (junkFilter) - { - TupleTableSlot *slot = - MakeSingleTupleTableSlot(NULL, &TTSOpsMinimalTuple); - - *junkFilter = ExecInitJunkFilterConversion(tlist, - CreateTupleDescCopy(tupdesc), - slot); - } - /* Report that we are returning entire tuple result */ - return true; + is_tuple_result = true; } else ereport(ERROR, @@ -1944,7 +1890,135 @@ check_sql_fn_retval(Oid func_id, Oid rettype, List *queryTreeList, errmsg("return type %s is not supported for SQL functions", format_type_be(rettype)))); - return false; +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) + { + Query *newquery; + 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); + + if (tle->resjunk) + continue; + colnames = lappend(colnames, + makeString(tle->resname ? tle->resname : "")); + } + + /* Build a suitable RTE for the subquery */ + rte = makeNode(RangeTblEntry); + rte->rtekind = RTE_SUBQUERY; + rte->subquery = parse; + rte->eref = rte->alias = makeAlias("*SELECT*", colnames); + rte->lateral = false; + rte->inh = false; + rte->inFromCl = true; + newquery->rtable = list_make1(rte); + + rtr = makeNode(RangeTblRef); + rtr->rtindex = 1; + newquery->jointree = makeFromExpr(list_make1(rtr), NULL); + + /* Replace original query in the correct element of the query list */ + lfirst(parse_cell) = newquery; + } + + /* Return tlist (possibly modified) if requested */ + 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 + { + /* Any casting must happen in the upper tlist */ + Var *var = makeVarFromTargetEntry(1, src_tle); + + 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; + /* 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; } diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c index 75c253cdcf..2d3ec22407 100644 --- a/src/backend/optimizer/util/clauses.c +++ b/src/backend/optimizer/util/clauses.c @@ -155,7 +155,6 @@ static Query *substitute_actual_srf_parameters(Query *expr, int nargs, List *args); static Node *substitute_actual_srf_parameters_mutator(Node *node, 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, char *src; Datum tmp; bool isNull; - bool modifyTargetList; MemoryContext oldcxt; MemoryContext mycxt; inline_error_callback_arg callback_arg; ErrorContextCallback sqlerrcontext; FuncExpr *fexpr; SQLFunctionParseInfoPtr pinfo; + TupleDesc rettupdesc; ParseState *pstate; List *raw_parsetree_list; + List *querytree_list; Query *querytree; Node *newexpr; int *usecounts; @@ -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 * dummy FuncExpr node containing the already-simplified arguments to pass - * to prepare_sql_fn_parse_info. (It is really only needed if there are - * some polymorphic arguments, but for simplicity we always build it.) + * to prepare_sql_fn_parse_info. (In some cases we don't really need + * that, but for simplicity we always build it.) */ fexpr = makeNode(FuncExpr); fexpr->funcid = funcid; @@ -4490,6 +4490,11 @@ inline_function(Oid funcid, Oid result_type, Oid result_collid, (Node *) fexpr, 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 * 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, * 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 * 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. * * 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. */ - if (check_sql_fn_retval(funcid, result_type, list_make1(querytree), - &modifyTargetList, NULL)) + querytree_list = list_make1(querytree); + if (check_sql_fn_retval(querytree_list, result_type, rettupdesc, + false, NULL)) 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 */ newexpr = (Node *) ((TargetEntry *) linitial(querytree->targetList))->expr; @@ -4566,9 +4579,6 @@ inline_function(Oid funcid, Oid result_type, Oid result_collid, if (exprType(newexpr) != result_type) 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 * volatile than the surrounding function (this is to avoid breaking hacks @@ -4877,12 +4887,13 @@ inline_set_returning_function(PlannerInfo *root, RangeTblEntry *rte) char *src; Datum tmp; bool isNull; - bool modifyTargetList; MemoryContext oldcxt; MemoryContext mycxt; inline_error_callback_arg callback_arg; ErrorContextCallback sqlerrcontext; SQLFunctionParseInfoPtr pinfo; + TypeFuncClass functypclass; + TupleDesc rettupdesc; List *raw_parsetree_list; List *querytree_list; Query *querytree; @@ -5012,6 +5023,18 @@ inline_set_returning_function(PlannerInfo *root, RangeTblEntry *rte) (Node *) fexpr, 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 * 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) * 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 * 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 - * expression(s) match the declared type of the function. + * coercions if needed to make the tlist expression(s) match the declared + * 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 * 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 - * check_sql_fn_retval, we deliberately exclude domains over composite - * here.) + * returning is a single composite column which is not what we need. */ - if (!check_sql_fn_retval(func_oid, fexpr->funcresulttype, - querytree_list, - &modifyTargetList, NULL) && - (get_typtype(fexpr->funcresulttype) == TYPTYPE_COMPOSITE || - fexpr->funcresulttype == RECORDOID)) + if (!check_sql_fn_retval(querytree_list, + fexpr->funcresulttype, rettupdesc, + true, NULL) && + (functypclass == TYPEFUNC_COMPOSITE || + functypclass == TYPEFUNC_COMPOSITE_DOMAIN || + functypclass == TYPEFUNC_RECORD)) goto fail; /* reject not-whole-tuple-result cases */ /* - * If we had to modify the tlist to make it match, and the statement is - * one in which changing the tlist contents could change semantics, we - * have to punt and not inline. + * check_sql_fn_retval might've inserted a projection step, but that's + * fine; just make sure we use the upper Query. */ - 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 && - get_func_result_type(func_oid, NULL, NULL) == TYPEFUNC_RECORD && - !tlist_matches_coltypelist(querytree->targetList, - rtfunc->funccoltypes)) - goto fail; + querytree = linitial(querytree_list); /* * Looks good --- substitute parameters into the query. @@ -5181,46 +5189,3 @@ substitute_actual_srf_parameters_mutator(Node *node, substitute_actual_srf_parameters_mutator, (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; -} diff --git a/src/include/executor/functions.h b/src/include/executor/functions.h index 25e28519df..cb13428a5a 100644 --- a/src/include/executor/functions.h +++ b/src/include/executor/functions.h @@ -31,10 +31,10 @@ extern void sql_fn_parser_setup(struct ParseState *pstate, extern void check_sql_fn_statements(List *queryTreeList); -extern bool check_sql_fn_retval(Oid func_id, Oid rettype, - List *queryTreeList, - bool *modifyTargetList, - JunkFilter **junkFilter); +extern bool check_sql_fn_retval(List *queryTreeList, + Oid rettype, TupleDesc rettupdesc, + bool insertDroppedCols, + List **resultTargetList); extern DestReceiver *CreateSQLFunctionDestReceiver(void); diff --git a/src/test/regress/expected/rangefuncs.out b/src/test/regress/expected/rangefuncs.out index 36a5929113..a70060ba01 100644 --- a/src/test/regress/expected/rangefuncs.out +++ b/src/test/regress/expected/rangefuncs.out @@ -1820,6 +1820,67 @@ select * from array_to_set(array['one', 'two']); -- fail ERROR: a column definition list is required for functions returning "record" 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 function testrngfunc() returns record as $$ insert into rngfunc values (1,2) returning *; @@ -1863,6 +1924,140 @@ ERROR: a column definition list is required for functions returning "record" LINE 1: select * from 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 -- @@ -1955,7 +2150,7 @@ drop view usersview; drop function get_first_user(); drop function get_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 $$ select 'foo'::varchar union all select 'bar'::varchar ; $$ language sql stable; @@ -1973,6 +2168,19 @@ select * from rngfuncbar(); bar (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(); -- check handling of a SQL function with multiple OUT params (bug #5777) create or replace function rngfuncbar(out integer, out numeric) as diff --git a/src/test/regress/sql/rangefuncs.sql b/src/test/regress/sql/rangefuncs.sql index 5d29d2e401..476b4f27e2 100644 --- a/src/test/regress/sql/rangefuncs.sql +++ b/src/test/regress/sql/rangefuncs.sql @@ -515,6 +515,27 @@ $$ language sql strict 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']); -- 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); @@ -538,6 +559,57 @@ select * from testrngfunc(); -- fail 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 -- @@ -585,7 +657,7 @@ drop function get_first_user(); drop function get_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 $$ select 'foo'::varchar union all select 'bar'::varchar ; $$ @@ -593,6 +665,8 @@ language sql stable; select rngfuncbar(); select * from rngfuncbar(); +-- this function is now inlinable, too: +explain (verbose, costs off) select * from rngfuncbar(); drop function rngfuncbar();