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