Improve pl/pgsql to support composite-type expressions in RETURN.
authorTom Lane <tgl@sss.pgh.pa.us>
Fri, 7 Dec 2012 04:09:52 +0000 (23:09 -0500)
committerTom Lane <tgl@sss.pgh.pa.us>
Fri, 7 Dec 2012 04:09:52 +0000 (23:09 -0500)
For some reason lost in the mists of prehistory, RETURN was only coded to
allow a simple reference to a composite variable when the function's return
type is composite.  Allow an expression instead, while preserving the
efficiency of the original code path in the case where the expression is
indeed just a composite variable's name.  Likewise for RETURN NEXT.

As is true in various other places, the supplied expression must yield
exactly the number and data types of the required columns.  There was some
discussion of relaxing that for pl/pgsql, but no consensus yet, so this
patch doesn't address that.

Asif Rehman, reviewed by Pavel Stehule

doc/src/sgml/plpgsql.sgml
src/pl/plpgsql/src/pl_exec.c
src/pl/plpgsql/src/pl_gram.y
src/pl/plpgsql/src/pl_scanner.c
src/pl/plpgsql/src/plpgsql.h
src/test/regress/expected/plpgsql.out
src/test/regress/sql/plpgsql.sql

index 07fba57c0e1548cb78b4ff000e3d65da8e558b81..b33c41e02ab4fb3e5ab12f45e95b61d7526c0a31 100644 (file)
@@ -1571,11 +1571,11 @@ RETURN <replaceable>expression</replaceable>;
      </para>
 
      <para>
-      When returning a scalar type, any expression can be used. The
-      expression's result will be automatically cast into the
-      function's return type as described for assignments. To return a
-      composite (row) value, you must write a record or row variable
-      as the <replaceable>expression</replaceable>.
+      In a function that returns a scalar type, the expression's result will
+      automatically be cast into the function's return type as described for
+      assignments.  But to return a composite (row) value, you must write an
+      expression delivering exactly the requested column set.  This may
+      require use of explicit casting.
      </para>
 
      <para>
@@ -1600,6 +1600,20 @@ RETURN <replaceable>expression</replaceable>;
       however.  In those cases a <command>RETURN</command> statement is
       automatically executed if the top-level block finishes.
      </para>
+
+     <para>
+      Some examples:
+
+<programlisting>
+-- functions returning a scalar type
+RETURN 1 + 2;
+RETURN scalar_var;
+
+-- functions returning a composite type
+RETURN composite_type_var;
+RETURN (1, 2, 'three'::text);  -- must cast columns to correct types
+</programlisting>
+     </para>
     </sect3>
 
     <sect3>
index 3b5b3bbae2d6046638b3d7453a769a3cbc0c9bde..f595b941c492801a49feacd2140e4dfbd6d13b3f 100644 (file)
@@ -189,6 +189,12 @@ static void exec_move_row(PLpgSQL_execstate *estate,
 static HeapTuple make_tuple_from_row(PLpgSQL_execstate *estate,
                    PLpgSQL_row *row,
                    TupleDesc tupdesc);
+static HeapTuple get_tuple_from_datum(Datum value);
+static TupleDesc get_tupdesc_from_datum(Datum value);
+static void exec_move_row_from_datum(PLpgSQL_execstate *estate,
+                        PLpgSQL_rec *rec,
+                        PLpgSQL_row *row,
+                        Datum value);
 static char *convert_value_to_string(PLpgSQL_execstate *estate,
                        Datum value, Oid valtype);
 static Datum exec_cast_value(PLpgSQL_execstate *estate,
@@ -275,24 +281,9 @@ plpgsql_exec_function(PLpgSQL_function *func, FunctionCallInfo fcinfo)
 
                    if (!fcinfo->argnull[i])
                    {
-                       HeapTupleHeader td;
-                       Oid         tupType;
-                       int32       tupTypmod;
-                       TupleDesc   tupdesc;
-                       HeapTupleData tmptup;
-
-                       td = DatumGetHeapTupleHeader(fcinfo->arg[i]);
-                       /* Extract rowtype info and find a tupdesc */
-                       tupType = HeapTupleHeaderGetTypeId(td);
-                       tupTypmod = HeapTupleHeaderGetTypMod(td);
-                       tupdesc = lookup_rowtype_tupdesc(tupType, tupTypmod);
-                       /* Build a temporary HeapTuple control structure */
-                       tmptup.t_len = HeapTupleHeaderGetDatumLength(td);
-                       ItemPointerSetInvalid(&(tmptup.t_self));
-                       tmptup.t_tableOid = InvalidOid;
-                       tmptup.t_data = td;
-                       exec_move_row(&estate, NULL, row, &tmptup, tupdesc);
-                       ReleaseTupleDesc(tupdesc);
+                       /* Assign row value from composite datum */
+                       exec_move_row_from_datum(&estate, NULL, row,
+                                                fcinfo->arg[i]);
                    }
                    else
                    {
@@ -2396,6 +2387,10 @@ exec_stmt_return(PLpgSQL_execstate *estate, PLpgSQL_stmt_return *stmt)
    estate->rettupdesc = NULL;
    estate->retisnull = true;
 
+   /*
+    * This special-case path covers record/row variables in fn_retistuple
+    * functions, as well as functions with one or more OUT parameters.
+    */
    if (stmt->retvarno >= 0)
    {
        PLpgSQL_datum *retvar = estate->datums[stmt->retvarno];
@@ -2449,22 +2444,26 @@ exec_stmt_return(PLpgSQL_execstate *estate, PLpgSQL_stmt_return *stmt)
 
    if (stmt->expr != NULL)
    {
-       if (estate->retistuple)
-       {
-           exec_run_select(estate, stmt->expr, 1, NULL);
-           if (estate->eval_processed > 0)
-           {
-               estate->retval = PointerGetDatum(estate->eval_tuptable->vals[0]);
-               estate->rettupdesc = estate->eval_tuptable->tupdesc;
-               estate->retisnull = false;
-           }
-       }
-       else
+       estate->retval = exec_eval_expr(estate, stmt->expr,
+                                       &(estate->retisnull),
+                                       &(estate->rettype));
+
+       if (estate->retistuple && !estate->retisnull)
        {
-           /* Normal case for scalar results */
-           estate->retval = exec_eval_expr(estate, stmt->expr,
-                                           &(estate->retisnull),
-                                           &(estate->rettype));
+           /* Convert composite datum to a HeapTuple and TupleDesc */
+           HeapTuple   tuple;
+           TupleDesc   tupdesc;
+
+           /* Source must be of RECORD or composite type */
+           if (!type_is_rowtype(estate->rettype))
+               ereport(ERROR,
+                       (errcode(ERRCODE_DATATYPE_MISMATCH),
+                        errmsg("cannot return non-composite value from function returning composite type")));
+           tuple = get_tuple_from_datum(estate->retval);
+           tupdesc = get_tupdesc_from_datum(estate->retval);
+           estate->retval = PointerGetDatum(tuple);
+           estate->rettupdesc = CreateTupleDescCopy(tupdesc);
+           ReleaseTupleDesc(tupdesc);
        }
 
        return PLPGSQL_RC_RETURN;
@@ -2473,8 +2472,7 @@ exec_stmt_return(PLpgSQL_execstate *estate, PLpgSQL_stmt_return *stmt)
    /*
     * Special hack for function returning VOID: instead of NULL, return a
     * non-null VOID value.  This is of dubious importance but is kept for
-    * backwards compatibility.  Note that the only other way to get here is
-    * to have written "RETURN NULL" in a function returning tuple.
+    * backwards compatibility.
     */
    if (estate->fn_rettype == VOIDOID)
    {
@@ -2513,6 +2511,10 @@ exec_stmt_return_next(PLpgSQL_execstate *estate,
    tupdesc = estate->rettupdesc;
    natts = tupdesc->natts;
 
+   /*
+    * This special-case path covers record/row variables in fn_retistuple
+    * functions, as well as functions with one or more OUT parameters.
+    */
    if (stmt->retvarno >= 0)
    {
        PLpgSQL_datum *retvar = estate->datums[stmt->retvarno];
@@ -2593,26 +2595,77 @@ exec_stmt_return_next(PLpgSQL_execstate *estate,
        bool        isNull;
        Oid         rettype;
 
-       if (natts != 1)
-           ereport(ERROR,
-                   (errcode(ERRCODE_DATATYPE_MISMATCH),
-                    errmsg("wrong result type supplied in RETURN NEXT")));
-
        retval = exec_eval_expr(estate,
                                stmt->expr,
                                &isNull,
                                &rettype);
 
-       /* coerce type if needed */
-       retval = exec_simple_cast_value(estate,
-                                       retval,
-                                       rettype,
-                                       tupdesc->attrs[0]->atttypid,
-                                       tupdesc->attrs[0]->atttypmod,
-                                       isNull);
+       if (estate->retistuple)
+       {
+           /* Expression should be of RECORD or composite type */
+           if (!isNull)
+           {
+               TupleDesc   retvaldesc;
+               TupleConversionMap *tupmap;
+
+               if (!type_is_rowtype(rettype))
+                   ereport(ERROR,
+                           (errcode(ERRCODE_DATATYPE_MISMATCH),
+                            errmsg("cannot return non-composite value from function returning composite type")));
 
-       tuplestore_putvalues(estate->tuple_store, tupdesc,
-                            &retval, &isNull);
+               tuple = get_tuple_from_datum(retval);
+               free_tuple = true;      /* tuple is always freshly palloc'd */
+
+               /* it might need conversion */
+               retvaldesc = get_tupdesc_from_datum(retval);
+               tupmap = convert_tuples_by_position(retvaldesc, tupdesc,
+                                                   gettext_noop("returned record type does not match expected record type"));
+               if (tupmap)
+               {
+                   HeapTuple   newtuple;
+
+                   newtuple = do_convert_tuple(tuple, tupmap);
+                   free_conversion_map(tupmap);
+                   heap_freetuple(tuple);
+                   tuple = newtuple;
+               }
+               ReleaseTupleDesc(retvaldesc);
+               /* tuple will be stored into tuplestore below */
+           }
+           else
+           {
+               /* Composite NULL --- store a row of nulls */
+               Datum      *nulldatums;
+               bool       *nullflags;
+
+               nulldatums = (Datum *) palloc0(natts * sizeof(Datum));
+               nullflags = (bool *) palloc(natts * sizeof(bool));
+               memset(nullflags, true, natts * sizeof(bool));
+               tuplestore_putvalues(estate->tuple_store, tupdesc,
+                                    nulldatums, nullflags);
+               pfree(nulldatums);
+               pfree(nullflags);
+           }
+       }
+       else
+       {
+           /* Simple scalar result */
+           if (natts != 1)
+               ereport(ERROR,
+                       (errcode(ERRCODE_DATATYPE_MISMATCH),
+                      errmsg("wrong result type supplied in RETURN NEXT")));
+
+           /* coerce type if needed */
+           retval = exec_simple_cast_value(estate,
+                                           retval,
+                                           rettype,
+                                           tupdesc->attrs[0]->atttypid,
+                                           tupdesc->attrs[0]->atttypmod,
+                                           isNull);
+
+           tuplestore_putvalues(estate->tuple_store, tupdesc,
+                                &retval, &isNull);
+       }
    }
    else
    {
@@ -3901,30 +3954,12 @@ exec_assign_value(PLpgSQL_execstate *estate,
                }
                else
                {
-                   HeapTupleHeader td;
-                   Oid         tupType;
-                   int32       tupTypmod;
-                   TupleDesc   tupdesc;
-                   HeapTupleData tmptup;
-
                    /* Source must be of RECORD or composite type */
                    if (!type_is_rowtype(valtype))
                        ereport(ERROR,
                                (errcode(ERRCODE_DATATYPE_MISMATCH),
                                 errmsg("cannot assign non-composite value to a row variable")));
-                   /* Source is a tuple Datum, so safe to do this: */
-                   td = DatumGetHeapTupleHeader(value);
-                   /* Extract rowtype info and find a tupdesc */
-                   tupType = HeapTupleHeaderGetTypeId(td);
-                   tupTypmod = HeapTupleHeaderGetTypMod(td);
-                   tupdesc = lookup_rowtype_tupdesc(tupType, tupTypmod);
-                   /* Build a temporary HeapTuple control structure */
-                   tmptup.t_len = HeapTupleHeaderGetDatumLength(td);
-                   ItemPointerSetInvalid(&(tmptup.t_self));
-                   tmptup.t_tableOid = InvalidOid;
-                   tmptup.t_data = td;
-                   exec_move_row(estate, NULL, row, &tmptup, tupdesc);
-                   ReleaseTupleDesc(tupdesc);
+                   exec_move_row_from_datum(estate, NULL, row, value);
                }
                break;
            }
@@ -3943,31 +3978,12 @@ exec_assign_value(PLpgSQL_execstate *estate,
                }
                else
                {
-                   HeapTupleHeader td;
-                   Oid         tupType;
-                   int32       tupTypmod;
-                   TupleDesc   tupdesc;
-                   HeapTupleData tmptup;
-
                    /* Source must be of RECORD or composite type */
                    if (!type_is_rowtype(valtype))
                        ereport(ERROR,
                                (errcode(ERRCODE_DATATYPE_MISMATCH),
                                 errmsg("cannot assign non-composite value to a record variable")));
-
-                   /* Source is a tuple Datum, so safe to do this: */
-                   td = DatumGetHeapTupleHeader(value);
-                   /* Extract rowtype info and find a tupdesc */
-                   tupType = HeapTupleHeaderGetTypeId(td);
-                   tupTypmod = HeapTupleHeaderGetTypMod(td);
-                   tupdesc = lookup_rowtype_tupdesc(tupType, tupTypmod);
-                   /* Build a temporary HeapTuple control structure */
-                   tmptup.t_len = HeapTupleHeaderGetDatumLength(td);
-                   ItemPointerSetInvalid(&(tmptup.t_self));
-                   tmptup.t_tableOid = InvalidOid;
-                   tmptup.t_data = td;
-                   exec_move_row(estate, rec, NULL, &tmptup, tupdesc);
-                   ReleaseTupleDesc(tupdesc);
+                   exec_move_row_from_datum(estate, rec, NULL, value);
                }
                break;
            }
@@ -5416,6 +5432,89 @@ make_tuple_from_row(PLpgSQL_execstate *estate,
    return tuple;
 }
 
+/* ----------
+ * get_tuple_from_datum        extract a tuple from a composite Datum
+ *
+ * Returns a freshly palloc'd HeapTuple.
+ *
+ * Note: it's caller's responsibility to be sure value is of composite type.
+ * ----------
+ */
+static HeapTuple
+get_tuple_from_datum(Datum value)
+{
+   HeapTupleHeader td = DatumGetHeapTupleHeader(value);
+   HeapTupleData tmptup;
+
+   /* Build a temporary HeapTuple control structure */
+   tmptup.t_len = HeapTupleHeaderGetDatumLength(td);
+   ItemPointerSetInvalid(&(tmptup.t_self));
+   tmptup.t_tableOid = InvalidOid;
+   tmptup.t_data = td;
+
+   /* Build a copy and return it */
+   return heap_copytuple(&tmptup);
+}
+
+/* ----------
+ * get_tupdesc_from_datum  get a tuple descriptor for a composite Datum
+ *
+ * Returns a pointer to the TupleDesc of the tuple's rowtype.
+ * Caller is responsible for calling ReleaseTupleDesc when done with it.
+ *
+ * Note: it's caller's responsibility to be sure value is of composite type.
+ * ----------
+ */
+static TupleDesc
+get_tupdesc_from_datum(Datum value)
+{
+   HeapTupleHeader td = DatumGetHeapTupleHeader(value);
+   Oid         tupType;
+   int32       tupTypmod;
+
+   /* Extract rowtype info and find a tupdesc */
+   tupType = HeapTupleHeaderGetTypeId(td);
+   tupTypmod = HeapTupleHeaderGetTypMod(td);
+   return lookup_rowtype_tupdesc(tupType, tupTypmod);
+}
+
+/* ----------
+ * exec_move_row_from_datum        Move a composite Datum into a record or row
+ *
+ * This is equivalent to get_tuple_from_datum() followed by exec_move_row(),
+ * but we avoid constructing an intermediate physical copy of the tuple.
+ * ----------
+ */
+static void
+exec_move_row_from_datum(PLpgSQL_execstate *estate,
+                        PLpgSQL_rec *rec,
+                        PLpgSQL_row *row,
+                        Datum value)
+{
+   HeapTupleHeader td = DatumGetHeapTupleHeader(value);
+   Oid         tupType;
+   int32       tupTypmod;
+   TupleDesc   tupdesc;
+   HeapTupleData tmptup;
+
+   /* Extract rowtype info and find a tupdesc */
+   tupType = HeapTupleHeaderGetTypeId(td);
+   tupTypmod = HeapTupleHeaderGetTypMod(td);
+   tupdesc = lookup_rowtype_tupdesc(tupType, tupTypmod);
+
+   /* Build a temporary HeapTuple control structure */
+   tmptup.t_len = HeapTupleHeaderGetDatumLength(td);
+   ItemPointerSetInvalid(&(tmptup.t_self));
+   tmptup.t_tableOid = InvalidOid;
+   tmptup.t_data = td;
+
+   /* Do the move */
+   exec_move_row(estate, rec, row, &tmptup, tupdesc);
+
+   /* Release tupdesc usage count */
+   ReleaseTupleDesc(tupdesc);
+}
+
 /* ----------
  * convert_value_to_string         Convert a non-null Datum to C string
  *
index cf164d0e4882c4a568046838ab7a8b6abaa3eaae..edfe069c0020842ad1c13ab2651d1164d1d7f771 100644 (file)
@@ -2926,32 +2926,27 @@ make_return_stmt(int location)
    }
    else if (plpgsql_curr_compile->fn_retistuple)
    {
-       switch (yylex())
-       {
-           case K_NULL:
-               /* we allow this to support RETURN NULL in triggers */
-               break;
-
-           case T_DATUM:
-               if (yylval.wdatum.datum->dtype == PLPGSQL_DTYPE_ROW ||
-                   yylval.wdatum.datum->dtype == PLPGSQL_DTYPE_REC)
-                   new->retvarno = yylval.wdatum.datum->dno;
-               else
-                   ereport(ERROR,
-                           (errcode(ERRCODE_DATATYPE_MISMATCH),
-                            errmsg("RETURN must specify a record or row variable in function returning row"),
-                            parser_errposition(yylloc)));
-               break;
+       /*
+        * We want to special-case simple row or record references for
+        * efficiency.  So peek ahead to see if that's what we have.
+        */
+       int     tok = yylex();
 
-           default:
-               ereport(ERROR,
-                       (errcode(ERRCODE_DATATYPE_MISMATCH),
-                        errmsg("RETURN must specify a record or row variable in function returning row"),
-                        parser_errposition(yylloc)));
-               break;
+       if (tok == T_DATUM && plpgsql_peek() == ';' &&
+           (yylval.wdatum.datum->dtype == PLPGSQL_DTYPE_ROW ||
+            yylval.wdatum.datum->dtype == PLPGSQL_DTYPE_REC))
+       {
+           new->retvarno = yylval.wdatum.datum->dno;
+           /* eat the semicolon token that we only peeked at above */
+           tok = yylex();
+           Assert(tok == ';');
+       }
+       else
+       {
+           /* Not (just) a row/record name, so treat as expression */
+           plpgsql_push_back_token(tok);
+           new->expr = read_sql_expression(';', ";");
        }
-       if (yylex() != ';')
-           yyerror("syntax error");
    }
    else
    {
@@ -2994,28 +2989,27 @@ make_return_next_stmt(int location)
    }
    else if (plpgsql_curr_compile->fn_retistuple)
    {
-       switch (yylex())
-       {
-           case T_DATUM:
-               if (yylval.wdatum.datum->dtype == PLPGSQL_DTYPE_ROW ||
-                   yylval.wdatum.datum->dtype == PLPGSQL_DTYPE_REC)
-                   new->retvarno = yylval.wdatum.datum->dno;
-               else
-                   ereport(ERROR,
-                           (errcode(ERRCODE_DATATYPE_MISMATCH),
-                            errmsg("RETURN NEXT must specify a record or row variable in function returning row"),
-                            parser_errposition(yylloc)));
-               break;
+       /*
+        * We want to special-case simple row or record references for
+        * efficiency.  So peek ahead to see if that's what we have.
+        */
+       int     tok = yylex();
 
-           default:
-               ereport(ERROR,
-                       (errcode(ERRCODE_DATATYPE_MISMATCH),
-                        errmsg("RETURN NEXT must specify a record or row variable in function returning row"),
-                        parser_errposition(yylloc)));
-               break;
+       if (tok == T_DATUM && plpgsql_peek() == ';' &&
+           (yylval.wdatum.datum->dtype == PLPGSQL_DTYPE_ROW ||
+            yylval.wdatum.datum->dtype == PLPGSQL_DTYPE_REC))
+       {
+           new->retvarno = yylval.wdatum.datum->dno;
+           /* eat the semicolon token that we only peeked at above */
+           tok = yylex();
+           Assert(tok == ';');
+       }
+       else
+       {
+           /* Not (just) a row/record name, so treat as expression */
+           plpgsql_push_back_token(tok);
+           new->expr = read_sql_expression(';', ";");
        }
-       if (yylex() != ';')
-           yyerror("syntax error");
    }
    else
        new->expr = read_sql_expression(';', ";");
index 707afbad902744f182da52eaa2f9cca6722c1e28..ba31b3655ff137fe94cf72acb39356714c5ab971 100644 (file)
@@ -442,9 +442,27 @@ plpgsql_append_source_text(StringInfo buf,
                           endlocation - startlocation);
 }
 
+/*
+ * Peek one token ahead in the input stream.  Only the token code is
+ * made available, not any of the auxiliary info such as location.
+ *
+ * NB: no variable or unreserved keyword lookup is performed here, they will
+ * be returned as IDENT. Reserved keywords are resolved as usual.
+ */
+int
+plpgsql_peek(void)
+{
+   int         tok1;
+   TokenAuxData aux1;
+
+   tok1 = internal_yylex(&aux1);
+   push_back_token(tok1, &aux1);
+   return tok1;
+}
+
 /*
  * Peek two tokens ahead in the input stream. The first token and its
- * location the query are returned in *tok1_p and *tok1_loc, second token
+ * location in the query are returned in *tok1_p and *tok1_loc, second token
  * and its location in *tok2_p and *tok2_loc.
  *
  * NB: no variable or unreserved keyword lookup is performed here, they will
index 7ea696033bbfad22a5afe6dbbadc0dd8fbdac4b3..52a5af87a4075128e936a62706f51df084df4f67 100644 (file)
@@ -976,6 +976,7 @@ extern void plpgsql_push_back_token(int token);
 extern bool plpgsql_token_is_unreserved_keyword(int token);
 extern void plpgsql_append_source_text(StringInfo buf,
                           int startlocation, int endlocation);
+extern int plpgsql_peek(void);
 extern void plpgsql_peek2(int *tok1_p, int *tok2_p, int *tok1_loc,
              int *tok2_loc);
 extern int plpgsql_scanner_errposition(int location);
index 30053363f3062ac196110c2a0926475961af4a7d..dd1a8703fcc9bb46b60a75e9fb685551c601067d 100644 (file)
@@ -3624,7 +3624,139 @@ select * from returnqueryf();
 
 drop function returnqueryf();
 drop table tabwithcols;
+--
+-- Tests for composite-type results
+--
+create type footype as (x int, y varchar);
+-- test: use of variable of composite type in return statement
+create or replace function foo() returns footype as $$
+declare
+  v footype;
+begin
+  v := (1, 'hello');
+  return v;
+end;
+$$ language plpgsql;
+select foo();
+    foo    
+-----------
+ (1,hello)
+(1 row)
+
+-- test: use of variable of record type in return statement
+create or replace function foo() returns footype as $$
+declare
+  v record;
+begin
+  v := (1, 'hello'::varchar);
+  return v;
+end;
+$$ language plpgsql;
+select foo();
+    foo    
+-----------
+ (1,hello)
+(1 row)
+
+-- test: use of row expr in return statement
+create or replace function foo() returns footype as $$
+begin
+  return (1, 'hello'::varchar);
+end;
+$$ language plpgsql;
+select foo();
+    foo    
+-----------
+ (1,hello)
+(1 row)
+
+-- this does not work currently (no implicit casting)
+create or replace function foo() returns footype as $$
+begin
+  return (1, 'hello');
+end;
+$$ language plpgsql;
+select foo();
+ERROR:  returned record type does not match expected record type
+DETAIL:  Returned type unknown does not match expected type character varying in column 2.
+CONTEXT:  PL/pgSQL function foo() while casting return value to function's return type
+-- ... but this does
+create or replace function foo() returns footype as $$
+begin
+  return (1, 'hello')::footype;
+end;
+$$ language plpgsql;
+select foo();
+    foo    
+-----------
+ (1,hello)
+(1 row)
+
+drop function foo();
+-- test: return a row expr as record.
+create or replace function foorec() returns record as $$
+declare
+  v record;
+begin
+  v := (1, 'hello');
+  return v;
+end;
+$$ language plpgsql;
+select foorec();
+  foorec   
+-----------
+ (1,hello)
+(1 row)
+
+-- test: return row expr in return statement.
+create or replace function foorec() returns record as $$
+begin
+  return (1, 'hello');
+end;
+$$ language plpgsql;
+select foorec();
+  foorec   
+-----------
+ (1,hello)
+(1 row)
+
+drop function foorec();
+-- test: row expr in RETURN NEXT statement.
+create or replace function foo() returns setof footype as $$
+begin
+  for i in 1..3
+  loop
+    return next (1, 'hello'::varchar);
+  end loop;
+  return next null::footype;
+  return next (2, 'goodbye')::footype;
+end;
+$$ language plpgsql;
+select * from foo();
+ x |    y    
+---+---------
+ 1 | hello
+ 1 | hello
+ 1 | hello
+   | 
+ 2 | goodbye
+(5 rows)
+
+drop function foo();
+-- test: use invalid expr in return statement.
+create or replace function foo() returns footype as $$
+begin
+  return 1 + 1;
+end;
+$$ language plpgsql;
+select foo();
+ERROR:  cannot return non-composite value from function returning composite type
+CONTEXT:  PL/pgSQL function foo() line 3 at RETURN
+drop function foo();
+drop type footype;
+--
 -- Tests for 8.4's new RAISE features
+--
 create or replace function raise_test() returns void as $$
 begin
   raise notice '% % %', 1, 2, 3
index 2b60b678af33717531afbaa40c8af29fafaeaec8..fe507f4c152ca8b5c94ef76fa11bbe280dd9a341 100644 (file)
@@ -2937,7 +2937,119 @@ select * from returnqueryf();
 drop function returnqueryf();
 drop table tabwithcols;
 
+--
+-- Tests for composite-type results
+--
+
+create type footype as (x int, y varchar);
+
+-- test: use of variable of composite type in return statement
+create or replace function foo() returns footype as $$
+declare
+  v footype;
+begin
+  v := (1, 'hello');
+  return v;
+end;
+$$ language plpgsql;
+
+select foo();
+
+-- test: use of variable of record type in return statement
+create or replace function foo() returns footype as $$
+declare
+  v record;
+begin
+  v := (1, 'hello'::varchar);
+  return v;
+end;
+$$ language plpgsql;
+
+select foo();
+
+-- test: use of row expr in return statement
+create or replace function foo() returns footype as $$
+begin
+  return (1, 'hello'::varchar);
+end;
+$$ language plpgsql;
+
+select foo();
+
+-- this does not work currently (no implicit casting)
+create or replace function foo() returns footype as $$
+begin
+  return (1, 'hello');
+end;
+$$ language plpgsql;
+
+select foo();
+
+-- ... but this does
+create or replace function foo() returns footype as $$
+begin
+  return (1, 'hello')::footype;
+end;
+$$ language plpgsql;
+
+select foo();
+
+drop function foo();
+
+-- test: return a row expr as record.
+create or replace function foorec() returns record as $$
+declare
+  v record;
+begin
+  v := (1, 'hello');
+  return v;
+end;
+$$ language plpgsql;
+
+select foorec();
+
+-- test: return row expr in return statement.
+create or replace function foorec() returns record as $$
+begin
+  return (1, 'hello');
+end;
+$$ language plpgsql;
+
+select foorec();
+
+drop function foorec();
+
+-- test: row expr in RETURN NEXT statement.
+create or replace function foo() returns setof footype as $$
+begin
+  for i in 1..3
+  loop
+    return next (1, 'hello'::varchar);
+  end loop;
+  return next null::footype;
+  return next (2, 'goodbye')::footype;
+end;
+$$ language plpgsql;
+
+select * from foo();
+
+drop function foo();
+
+-- test: use invalid expr in return statement.
+create or replace function foo() returns footype as $$
+begin
+  return 1 + 1;
+end;
+$$ language plpgsql;
+
+select foo();
+
+drop function foo();
+drop type footype;
+
+--
 -- Tests for 8.4's new RAISE features
+--
 
 create or replace function raise_test() returns void as $$
 begin