diff --git a/doc/src/sgml/plpgsql.sgml b/doc/src/sgml/plpgsql.sgml
index 07fba57c0e1..b33c41e02ab 100644
--- a/doc/src/sgml/plpgsql.sgml
+++ b/doc/src/sgml/plpgsql.sgml
@@ -1571,11 +1571,11 @@ RETURN expression;
- 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 expression.
+ 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.
@@ -1600,6 +1600,20 @@ RETURN expression;
however. In those cases a RETURN statement is
automatically executed if the top-level block finishes.
+
+
+ Some examples:
+
+
+-- 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
+
+
diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
index 3b5b3bbae2d..f595b941c49 100644
--- a/src/pl/plpgsql/src/pl_exec.c
+++ b/src/pl/plpgsql/src/pl_exec.c
@@ -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)
+ estate->retval = exec_eval_expr(estate, stmt->expr,
+ &(estate->retisnull),
+ &(estate->rettype));
+
+ if (estate->retistuple && !estate->retisnull)
{
- 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
- {
- /* 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;
- tuplestore_putvalues(estate->tuple_store, tupdesc,
- &retval, &isNull);
+ if (!type_is_rowtype(rettype))
+ ereport(ERROR,
+ (errcode(ERRCODE_DATATYPE_MISMATCH),
+ errmsg("cannot return non-composite value from function returning composite type")));
+
+ 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
*
diff --git a/src/pl/plpgsql/src/pl_gram.y b/src/pl/plpgsql/src/pl_gram.y
index cf164d0e488..edfe069c002 100644
--- a/src/pl/plpgsql/src/pl_gram.y
+++ b/src/pl/plpgsql/src/pl_gram.y
@@ -2926,32 +2926,27 @@ make_return_stmt(int location)
}
else if (plpgsql_curr_compile->fn_retistuple)
{
- switch (yylex())
+ /*
+ * 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();
+
+ if (tok == T_DATUM && plpgsql_peek() == ';' &&
+ (yylval.wdatum.datum->dtype == PLPGSQL_DTYPE_ROW ||
+ yylval.wdatum.datum->dtype == PLPGSQL_DTYPE_REC))
{
- 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;
-
- default:
- ereport(ERROR,
- (errcode(ERRCODE_DATATYPE_MISMATCH),
- errmsg("RETURN must specify a record or row variable in function returning row"),
- parser_errposition(yylloc)));
- break;
+ 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(';', ";");
diff --git a/src/pl/plpgsql/src/pl_scanner.c b/src/pl/plpgsql/src/pl_scanner.c
index 707afbad902..ba31b3655ff 100644
--- a/src/pl/plpgsql/src/pl_scanner.c
+++ b/src/pl/plpgsql/src/pl_scanner.c
@@ -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
diff --git a/src/pl/plpgsql/src/plpgsql.h b/src/pl/plpgsql/src/plpgsql.h
index 7ea696033bb..52a5af87a40 100644
--- a/src/pl/plpgsql/src/plpgsql.h
+++ b/src/pl/plpgsql/src/plpgsql.h
@@ -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);
diff --git a/src/test/regress/expected/plpgsql.out b/src/test/regress/expected/plpgsql.out
index 30053363f30..dd1a8703fcc 100644
--- a/src/test/regress/expected/plpgsql.out
+++ b/src/test/regress/expected/plpgsql.out
@@ -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
diff --git a/src/test/regress/sql/plpgsql.sql b/src/test/regress/sql/plpgsql.sql
index 2b60b678af3..fe507f4c152 100644
--- a/src/test/regress/sql/plpgsql.sql
+++ b/src/test/regress/sql/plpgsql.sql
@@ -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