Improve the handling of result type coercions in SQL functions.
authorTom Lane <tgl@sss.pgh.pa.us>
Wed, 8 Jan 2020 16:07:53 +0000 (11:07 -0500)
committerTom Lane <tgl@sss.pgh.pa.us>
Wed, 8 Jan 2020 16:07:59 +0000 (11:07 -0500)
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
src/backend/catalog/pg_proc.c
src/backend/executor/functions.c
src/backend/optimizer/util/clauses.c
src/include/executor/functions.h
src/test/regress/expected/rangefuncs.out
src/test/regress/sql/rangefuncs.sql

index d9afd3be4d038bb6ba6f3f4a2c5ba0b85079ecf5..5616524cfd318362853ea0e21e5033310cf9f815 100644 (file)
@@ -388,11 +388,15 @@ $$ LANGUAGE SQL;
     </para>
 
     <para>
-     A <acronym>SQL</acronym> function must return exactly its declared
-     result type.  This may require inserting an explicit cast.
+     If the final <literal>SELECT</literal> or <literal>RETURNING</literal>
+     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
      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>
 CREATE FUNCTION add_em(integer, integer) RETURNS float8 AS $$
@@ -400,16 +404,10 @@ CREATE FUNCTION add_em(integer, integer) RETURNS float8 AS $$
 $$ LANGUAGE SQL;
 </programlisting>
 
-     even though in other contexts <productname>PostgreSQL</productname>
-     would be willing to insert an implicit cast to
-     convert <type>integer</type> to <type>float8</type>.
-     We need to write it as
-
-<programlisting>
-CREATE FUNCTION add_em(integer, integer) RETURNS float8 AS $$
-    SELECT ($1 + $2)::float8;
-$$ LANGUAGE SQL;
-</programlisting>
+     since the <type>integer</type> sum can be implicitly cast
+     to <type>float8</type>.
+     (See <xref linkend="typeconv"/> or <xref linkend="sql-createcast"/>
+     for more about casts.)
     </para>
    </sect2>
 
@@ -503,23 +501,24 @@ $$ LANGUAGE SQL;
       <listitem>
        <para>
         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.)
        </para>
       </listitem>
       <listitem>
        <para>
-        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:
 <screen>
 <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>
 </screen>
-        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.
        </para>
       </listitem>
      </itemizedlist>
@@ -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.
     </para>
 
     <para>
@@ -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:
 <screen>
 <computeroutput>
-ERROR:  could not determine polymorphic type because input has type "unknown"
+ERROR:  could not determine polymorphic type because input has type unknown
 </computeroutput>
 </screen>
     </para>
index 9dba42671a7dcd075f90637b0e7b23f8b0ceb953..5194dcaac083d051420bc0f76ecaac68d8e01098 100644 (file)
@@ -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;
index ac298c0295fdb6f176119b587b3d51a5491c87ec..e8e1957075f12656b9f00a265fcddb3289605e1a 100644 (file)
@@ -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.)
+ *
+ * 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 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.
+ * 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;
 }
 
 
index 75c253cdcf522890f32ca09771bb445123ab3270..2d3ec2240761bedb2fa95555fe559fd00367ad5d 100644 (file)
@@ -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.
-        */
-       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.
+        * check_sql_fn_retval might've inserted a projection step, but that's
+        * fine; just make sure we use the upper Query.
         */
-       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;
-}
index 25e28519df98b66489870a3b69e20f7887b62b47..cb13428a5a8848ac52d72906848ca8f406167adf 100644 (file)
@@ -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);
 
index 36a5929113928a7a613b8572bebdb998f180ed2d..a70060ba010cd6c56caf092732d076b465bf32b2 100644 (file)
@@ -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
index 5d29d2e40124c0a87913fa31413373e4bfa931f5..476b4f27e250db81b34b3a4a8cae6eb14e9fdac2 100644 (file)
@@ -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();