Hello This patch contains implementation of table functions defined in ANSI SQL 2003 (Conformance with SQL2003: T326 Table functions). Patch adds new proargmode PROARGMODE_TABLE (based on PROARGMODE_OUT). Columns of output table are similar OUT arguments, but don't generate any variables.
Proposal: http://archives.postgresql.org/pgsql-hackers/2007-02/msg00318.php Description: SIGMOD Record, Vol. 33, No. 1, March 2004 Patch contains three diffs: * pgdumpclean .. small readability fix for pg_dump.c * tabfce .. implementation of table functions * plpgsql-tabfce .. table expression support for plpgsql's return stmt This patch can broke applications which scan argmodes. Plpgsql support is separated because plpgsql implementation isn't standardised. But current plpgsql hasn't any method for one statement table output from function, and table expression is clean and simple tool for it. Older version was broken by enum patch and scrollable cursor patch. Older version didn't dump table functions correctly. It's fixed now. Regards Pavel Stehule
*** ./pg_dump.c.orig 2007-05-06 11:13:11.000000000 +0200 --- ./pg_dump.c 2007-05-06 11:16:53.000000000 +0200 *************** *** 5959,5971 **** { switch (argmodes[j][0]) { ! case 'i': argmode = ""; break; ! case 'o': argmode = "OUT "; break; ! case 'b': argmode = "INOUT "; break; default: --- 5959,5971 ---- { switch (argmodes[j][0]) { ! case PROARGMODE_IN: argmode = ""; break; ! case PROARGMODE_OUT: argmode = "OUT "; break; ! case PROARGMODE_INOUT: argmode = "INOUT "; break; default:
*** ./doc/src/sgml/ref/create_function.sgml.orig 2007-04-23 18:52:53.000000000 +0200 --- ./doc/src/sgml/ref/create_function.sgml 2007-05-06 10:00:58.000000000 +0200 *************** *** 21,27 **** <synopsis> CREATE [ OR REPLACE ] FUNCTION <replaceable class="parameter">name</replaceable> ( [ [ <replaceable class="parameter">argmode</replaceable> ] [ <replaceable class="parameter">argname</replaceable> ] <replaceable class="parameter">argtype</replaceable> [, ...] ] ) ! [ RETURNS <replaceable class="parameter">rettype</replaceable> ] { LANGUAGE <replaceable class="parameter">langname</replaceable> | IMMUTABLE | STABLE | VOLATILE | CALLED ON NULL INPUT | RETURNS NULL ON NULL INPUT | STRICT --- 21,28 ---- <synopsis> CREATE [ OR REPLACE ] FUNCTION <replaceable class="parameter">name</replaceable> ( [ [ <replaceable class="parameter">argmode</replaceable> ] [ <replaceable class="parameter">argname</replaceable> ] <replaceable class="parameter">argtype</replaceable> [, ...] ] ) ! [ RETURNS <replaceable class="parameter">rettype</replaceable> ! | RETURNS TABLE ( <replaceable class="parameter">colname</replaceable> <replaceable class="parameter">coltype</replaceable> [, ...] ) ] { LANGUAGE <replaceable class="parameter">langname</replaceable> | IMMUTABLE | STABLE | VOLATILE | CALLED ON NULL INPUT | RETURNS NULL ON NULL INPUT | STRICT *************** *** 387,392 **** --- 388,427 ---- </listitem> </varlistentry> + <varlistentry> + <term><replaceable class="parameter">colname</replaceable></term> + + <listitem> + <para> + The name of an output table column. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">coltype</replaceable></term> + + <listitem> + <para> + The data type(s) of output table column. + </para> + <para> + Depending on the implementation language it might also be allowed + to specify <quote>pseudotypes</> such as <type>cstring</>. + Pseudotypes indicate that the actual argument type is either + incompletely specified, or outside the set of ordinary SQL data types. + </para> + <para> + The type of a column is referenced by writing + <literal><replaceable + class="parameter">tablename</replaceable>.<replaceable + class="parameter">columnname</replaceable>%TYPE</literal>. + Using this feature can sometimes help make a function independent of + changes to the definition of a table. + </para> + </listitem> + </varlistentry> + </variablelist> </refsect1> *************** *** 564,570 **** A <command>CREATE FUNCTION</command> command is defined in SQL:1999 and later. The <productname>PostgreSQL</productname> version is similar but not fully compatible. The attributes are not portable, neither are the ! different available languages. </para> <para> --- 599,605 ---- A <command>CREATE FUNCTION</command> command is defined in SQL:1999 and later. The <productname>PostgreSQL</productname> version is similar but not fully compatible. The attributes are not portable, neither are the ! different available languages. TABLE functions are defined in SQL:2003. </para> <para> *** ./doc/src/sgml/xfunc.sgml.orig 2007-05-06 09:54:41.000000000 +0200 --- ./doc/src/sgml/xfunc.sgml 2007-05-06 10:00:58.000000000 +0200 *************** *** 102,107 **** --- 102,115 ---- </para> <para> + <indexterm><primary>TABLE</><seealso>function</></>An SQL function can + declared to return table specified by function's retun table as + <literal>TABLE(<replaceable>somecolumns</>)</literal>. In this case + all rows of the last query's result are returned. Furteher details + appear bellow. + </para> + + <para> The body of an SQL function must be a list of SQL statements separated by semicolons. A semicolon after the last statement is optional. Unless the function is declared to return *************** *** 713,718 **** --- 721,784 ---- </sect2> <sect2> + <title><acronym>SQL</acronym> Table Functions</title> + + <para> + When an SQL function is declared as returning + <literal>TABLE(<replaceable>somecolumns</>)</literal>, the function's final + <command>SELECT</> query is executed to completion, and each row it + outputs is returned as an element of the result set. + </para> + + <para> + This feature is normally used when calling the function in the <literal>FROM</> + clause. In this case each row returned by the function becomes + a row of the table seen by the query. For example, assume that + table <literal>foo</> has the same contents as above, and we say: + + <programlisting> + CREATE FUNCTION getfoo(int) + RETURNS TABLE(id integer, subid integer, name varchar) AS $$ + SELECT * FROM foo WHERE fooid = $1; + $$ LANGUAGE SQL; + + SELECT * FROM getfoo(1) AS t1; + </programlisting> + + Then we would get: + <screen> + id | subid | name + -----+-------+------ + 1 | 1 | Joe + 1 | 2 | Ed + (2 rows) + </screen> + </para> + + <para> + Returned table can have one or more columns; + + <programlisting> + CREATE FUNCTION listchildren(text) RETURNS TABLE(name text) AS $$ + SELECT name FROM nodes WHERE parent = $1 + $$ LANGUAGE SQL; + + SELECT * FROM listchildren('Top'); + </programlisting> + + Then we would get: + <screen> + name + -------- + Child1 + Child2 + Child3 + (3 rows) + </screen> + </para> + </sect2> + + <sect2> <title>Polymorphic <acronym>SQL</acronym> Functions</title> <para> *** ./src/backend/commands/functioncmds.c.orig 2007-05-06 09:55:04.000000000 +0200 --- ./src/backend/commands/functioncmds.c 2007-05-06 10:00:58.000000000 +0200 *************** *** 203,218 **** (errcode(ERRCODE_INVALID_FUNCTION_DEFINITION), errmsg("functions cannot accept set arguments"))); ! if (fp->mode != FUNC_PARAM_OUT) ! inTypes[inCount++] = toid; ! ! if (fp->mode != FUNC_PARAM_IN) { ! if (outCount == 0) /* save first OUT param's type */ ! *requiredResultType = toid; ! outCount++; } allTypes[i] = ObjectIdGetDatum(toid); paramModes[i] = CharGetDatum(fp->mode); --- 203,224 ---- (errcode(ERRCODE_INVALID_FUNCTION_DEFINITION), errmsg("functions cannot accept set arguments"))); ! switch (fp->mode) { ! case FUNC_PARAM_IN: ! inTypes[inCount++] = toid; ! break; ! case FUNC_PARAM_INOUT: ! inTypes[inCount++] = toid; ! case FUNC_PARAM_OUT: ! case FUNC_PARAM_TABLE: ! /* save first OUT param's type */ ! if (outCount == 0) ! *requiredResultType = toid; ! outCount++; } + allTypes[i] = ObjectIdGetDatum(toid); paramModes[i] = CharGetDatum(fp->mode); *** ./src/backend/parser/gram.y.orig 2007-04-28 00:05:48.000000000 +0200 --- ./src/backend/parser/gram.y 2007-05-06 10:07:12.000000000 +0200 *************** *** 109,114 **** --- 109,117 ---- static void doNegateFloat(Value *v); static Node *makeXmlExpr(XmlExprOp op, char *name, List *named_args, List *args); + static List *mergeTblFuncParameters(List *func_args, List *columns); + static TypeName *TblFuncTypeName(List *colums); + %} %name-prefix="base_yy" *************** *** 248,254 **** TableFuncElementList opt_type_modifiers prep_type_clause execute_param_clause using_clause returning_clause ! enum_val_list %type <range> OptTempTableName %type <into> into_clause create_as_target --- 251,257 ---- TableFuncElementList opt_type_modifiers prep_type_clause execute_param_clause using_clause returning_clause ! enum_val_list tbl_func_column_list %type <range> OptTempTableName %type <into> into_clause create_as_target *************** *** 258,263 **** --- 261,268 ---- %type <fun_param_mode> arg_class %type <typnam> func_return func_type + %type <fun_param> tbl_func_column + %type <boolean> TriggerForType OptTemp %type <oncommit> OnCommitOption *************** *** 4018,4023 **** --- 4023,4041 ---- n->withClause = $7; $$ = (Node *)n; } + | CREATE opt_or_replace FUNCTION func_name func_args + RETURNS TABLE '(' tbl_func_column_list ')' createfunc_opt_list opt_definition + { + CreateFunctionStmt *n = makeNode(CreateFunctionStmt); + n->replace = $2; + n->funcname = $4; + n->parameters = mergeTblFuncParameters($5, $9); + n->returnType = TblFuncTypeName($9); + n->returnType->location = @8; + n->options = $11; + n->withClause = $12; + $$ = (Node *)n; + } ; opt_or_replace: *************** *** 4220,4225 **** --- 4238,4267 ---- | /*EMPTY*/ { $$ = NIL; } ; + /* + * Culumn list for table function definition + */ + tbl_func_column_list: + tbl_func_column + { + $$ = list_make1($1); + } + | tbl_func_column_list ',' tbl_func_column + { + $$ = lappend($1, $3); + } + ; + + tbl_func_column: param_name func_type + { + FunctionParameter *n = makeNode(FunctionParameter); + n->name = $1; + n->argType = $2; + n->mode = FUNC_PARAM_TABLE; + $$ = n; + } + ; + /***************************************************************************** * ALTER FUNCTION * *************** *** 9417,9423 **** { FunctionParameter *p = (FunctionParameter *) lfirst(i); ! if (p->mode != FUNC_PARAM_OUT) /* keep if IN or INOUT */ result = lappend(result, p->argType); } return result; --- 9459,9466 ---- { FunctionParameter *p = (FunctionParameter *) lfirst(i); ! /* keep if IN or INOUT */ ! if (p->mode != FUNC_PARAM_OUT && p->mode != FUNC_PARAM_TABLE) result = lappend(result, p->argType); } return result; *************** *** 9608,9613 **** --- 9651,9717 ---- return (Node *) x; } + /* mergeTblFuncParameters check only FUNC_PARAM_IN params in func_args list. + * Next check duplicate column names. Returns joined list. + */ + static List * + mergeTblFuncParameters(List *func_args, List *columns) + { + ListCell *i; + + foreach(i, func_args) + { + FunctionParameter *p = (FunctionParameter *) lfirst(i); + + if (p->mode != FUNC_PARAM_IN) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("OUT or INOUT arguments aren't allowed in TABLE function"))); + } + + /* Check for duplicate names in the explicit list of columns. + */ + foreach(i, columns) + { + FunctionParameter *p = (FunctionParameter *) lfirst(i); + ListCell *rest; + + for_each_cell(rest, lnext(i)) + { + FunctionParameter *rp = (FunctionParameter *) lfirst(rest); + + if (strcmp(p->name, rp->name) == 0) + ereport(ERROR, + (errcode(ERRCODE_DUPLICATE_COLUMN), + errmsg("column \"%s\" duplicated", + p->name))); + } + } + + return list_concat(func_args, columns); + } + + /* Returns correct return type for TABLE function. For list of length one returns + * correct type, For longer list returns record + */ + static TypeName * + TblFuncTypeName(List *columns) + { + TypeName *result; + + if (list_length(columns) > 1) + result = makeTypeName("record"); + else + { + FunctionParameter *p = (FunctionParameter *) linitial(columns); + result = (TypeName *) copyObject(p->argType); + } + + result->setof = true; + + return result; + } + /* * Must undefine base_yylex before including scan.c, since we want it * to create the function base_yylex not filtered_base_yylex. *** ./src/backend/utils/fmgr/funcapi.c.orig 2007-04-02 05:49:39.000000000 +0200 --- ./src/backend/utils/fmgr/funcapi.c 2007-05-06 10:14:16.000000000 +0200 *************** *** 529,535 **** { case ANYELEMENTOID: case ANYENUMOID: ! if (argmode == PROARGMODE_OUT) have_anyelement_result = true; else { --- 529,535 ---- { case ANYELEMENTOID: case ANYENUMOID: ! if (argmode == PROARGMODE_OUT || argmode == PROARGMODE_TABLE) have_anyelement_result = true; else { *************** *** 544,550 **** } break; case ANYARRAYOID: ! if (argmode == PROARGMODE_OUT) have_anyarray_result = true; else { --- 544,550 ---- } break; case ANYARRAYOID: ! if (argmode == PROARGMODE_OUT || argmode == PROARGMODE_TABLE) have_anyarray_result = true; else { *************** *** 561,567 **** default: break; } ! if (argmode != PROARGMODE_OUT) inargno++; } --- 561,567 ---- default: break; } ! if (argmode != PROARGMODE_OUT && argmode != PROARGMODE_TABLE) inargno++; } *************** *** 826,832 **** if (argmodes[i] == PROARGMODE_IN) continue; Assert(argmodes[i] == PROARGMODE_OUT || ! argmodes[i] == PROARGMODE_INOUT); if (++numoutargs > 1) { /* multiple out args, so forget it */ --- 826,833 ---- if (argmodes[i] == PROARGMODE_IN) continue; Assert(argmodes[i] == PROARGMODE_OUT || ! argmodes[i] == PROARGMODE_INOUT || ! argmodes[i] == PROARGMODE_TABLE); if (++numoutargs > 1) { /* multiple out args, so forget it */ *************** *** 977,983 **** if (argmodes[i] == PROARGMODE_IN) continue; Assert(argmodes[i] == PROARGMODE_OUT || ! argmodes[i] == PROARGMODE_INOUT); outargtypes[numoutargs] = argtypes[i]; if (argnames) pname = DatumGetCString(DirectFunctionCall1(textout, argnames[i])); --- 978,985 ---- if (argmodes[i] == PROARGMODE_IN) continue; Assert(argmodes[i] == PROARGMODE_OUT || ! argmodes[i] == PROARGMODE_INOUT || ! argmodes[i] == PROARGMODE_TABLE); outargtypes[numoutargs] = argtypes[i]; if (argnames) pname = DatumGetCString(DirectFunctionCall1(textout, argnames[i])); *** ./src/bin/pg_dump/pg_dump.c.orig 2007-05-06 11:24:29.000000000 +0200 --- ./src/bin/pg_dump/pg_dump.c 2007-05-06 14:33:46.000000000 +0200 *************** *** 49,54 **** --- 49,62 ---- #include "libpq/libpq-fs.h" #include "mb/pg_wchar.h" + #ifdef USE_ASSERT_CHECKING + #include <assert.h> + #define psql_assert(p) assert(p) + #else + #define psql_assert(p) + #endif + + #include "pg_backup_archiver.h" #include "dumputils.h" *************** *** 171,176 **** --- 179,189 ---- char **allargtypes, char **argmodes, char **argnames); + static bool is_returns_table_function(int nallargs, char **argmodes); + static char *format_table_function_columns(FuncInfo *finfo, int nallargs, + char **allargtypes, + char **argmodes, + char **argnames); static char *format_function_signature(FuncInfo *finfo, bool honor_quotes); static const char *convertRegProcReference(const char *proc); static const char *convertOperatorReference(const char *opr); *************** *** 5968,5973 **** --- 5981,5990 ---- case PROARGMODE_INOUT: argmode = "INOUT "; break; + case PROARGMODE_TABLE: + /* skip table column's names */ + free(typname); + continue; default: write_msg(NULL, "WARNING: bogus value in proargmodes array\n"); argmode = ""; *************** *** 5993,5998 **** --- 6010,6082 ---- return fn.data; } + /* + * is_returns_table_function: returns true if function id declared as + * RETURNS TABLE, i.e. at least one argument is PROARGMODE_TABLE + */ + static bool + is_returns_table_function(int nallargs, char **argmodes) + { + int j; + + if (argmodes) + for (j = 0; j < nallargs; j++) + if (argmodes[j][0] == PROARGMODE_TABLE) + return true; + + return false; + } + + + /* + * format_table_function_columns: generate column list for + * table functions. + */ + static char * + format_table_function_columns(FuncInfo *finfo, int nallargs, + char **allargtypes, + char **argmodes, + char **argnames) + { + PQExpBufferData fn; + int j; + bool first_column = true; + + initPQExpBuffer(&fn); + appendPQExpBuffer(&fn, "("); + + /* argmodes is checked in is_returns_table_function */ + psql_assert(argmodes); + + for (j = 0; j < nallargs; j++) + { + Oid typid; + char *typname; + + /* + * argmodes are checked in format_function_arguments. Isn't + * neccessery check argmodes here again + */ + if (argmodes[j][0] == PROARGMODE_TABLE) + { + typid = allargtypes ? atooid(allargtypes[j]) : finfo->argtypes[j]; + typname = getFormattedTypeName(typid, zeroAsOpaque); + + /* column's name is always NOT NULL (checked in gram.y) */ + appendPQExpBuffer(&fn, "%s%s %s", + first_column ? "" : ", ", + fmtId(argnames[j]), + typname); + free(typname); + first_column = false; + } + } + + appendPQExpBuffer(&fn, ")"); + return fn.data; + } + + /* * format_function_signature: generate function name and argument list * *************** *** 6279,6295 **** fmtId(finfo->dobj.namespace->dobj.name), funcsig); - rettypename = getFormattedTypeName(finfo->prorettype, zeroAsOpaque); - appendPQExpBuffer(q, "CREATE FUNCTION %s ", funcsig); - appendPQExpBuffer(q, "RETURNS %s%s\n %s\n LANGUAGE %s", - (proretset[0] == 't') ? "SETOF " : "", - rettypename, - asPart->data, - fmtId(lanname)); ! free(rettypename); if (provolatile[0] != PROVOLATILE_VOLATILE) { if (provolatile[0] == PROVOLATILE_IMMUTABLE) --- 6363,6396 ---- fmtId(finfo->dobj.namespace->dobj.name), funcsig); ! appendPQExpBuffer(q, "CREATE FUNCTION %s ", funcsig); + /* swich between RETURNS SETOF RECORD and RETURNS TABLE functions */ + if (!is_returns_table_function(nallargs, argmodes)) + { + rettypename = getFormattedTypeName(finfo->prorettype, zeroAsOpaque); + appendPQExpBuffer(q, "RETURNS %s%s\n %s\n LANGUAGE %s", + (proretset[0] == 't') ? "SETOF " : "", + rettypename, + asPart->data, + fmtId(lanname)); + free(rettypename); + } + else + { + char *func_cols; + + func_cols = format_table_function_columns(finfo, nallargs, allargtypes, + argmodes, argnames); + appendPQExpBuffer(q, "RETURNS TABLE %s\n %s\n LANGUAGE %s", + func_cols, + asPart->data, + fmtId(lanname)); + free(func_cols); + } + if (provolatile[0] != PROVOLATILE_VOLATILE) { if (provolatile[0] == PROVOLATILE_IMMUTABLE) *** ./src/include/catalog/pg_proc.h.orig 2007-04-02 05:49:40.000000000 +0200 --- ./src/include/catalog/pg_proc.h 2007-05-06 10:00:59.000000000 +0200 *************** *** 4209,4214 **** --- 4209,4215 ---- #define PROARGMODE_IN 'i' #define PROARGMODE_OUT 'o' #define PROARGMODE_INOUT 'b' + #define PROARGMODE_TABLE 't' /* *** ./src/include/nodes/parsenodes.h.orig 2007-04-28 00:05:49.000000000 +0200 --- ./src/include/nodes/parsenodes.h 2007-05-06 10:00:59.000000000 +0200 *************** *** 1526,1532 **** /* the assigned enum values appear in pg_proc, don't change 'em! */ FUNC_PARAM_IN = 'i', /* input only */ FUNC_PARAM_OUT = 'o', /* output only */ ! FUNC_PARAM_INOUT = 'b' /* both */ } FunctionParameterMode; typedef struct FunctionParameter --- 1526,1533 ---- /* the assigned enum values appear in pg_proc, don't change 'em! */ FUNC_PARAM_IN = 'i', /* input only */ FUNC_PARAM_OUT = 'o', /* output only */ ! FUNC_PARAM_INOUT = 'b', /* both */ ! FUNC_PARAM_TABLE = 't' /* table function column */ } FunctionParameterMode; typedef struct FunctionParameter *** ./src/test/regress/expected/rangefuncs.out.orig 2007-04-02 05:49:42.000000000 +0200 --- ./src/test/regress/expected/rangefuncs.out 2007-05-06 10:47:20.000000000 +0200 *************** *** 528,530 **** --- 528,636 ---- AS 'select $1, array[$1,$1]' LANGUAGE sql; ERROR: cannot determine result data type DETAIL: A function returning a polymorphic type must have at least one polymorphic argument. + -- + -- table functions + -- + CREATE OR REPLACE FUNCTION foo() + RETURNS TABLE(a int) + AS $$ SELECT a FROM generate_series(1,5) a(a) $$ LANGUAGE sql; + SELECT * FROM foo(); + a + --- + 1 + 2 + 3 + 4 + 5 + (5 rows) + + DROP FUNCTION foo(); + CREATE OR REPLACE FUNCTION foo() + RETURNS TABLE(a int, b int) + AS $$ SELECT a, b + FROM generate_series(1,5) a(a), + generate_series(1,5) b(b) $$ LANGUAGE sql; + SELECT * FROM foo(); + a | b + ---+--- + 1 | 1 + 1 | 2 + 1 | 3 + 1 | 4 + 1 | 5 + 2 | 1 + 2 | 2 + 2 | 3 + 2 | 4 + 2 | 5 + 3 | 1 + 3 | 2 + 3 | 3 + 3 | 4 + 3 | 5 + 4 | 1 + 4 | 2 + 4 | 3 + 4 | 4 + 4 | 5 + 5 | 1 + 5 | 2 + 5 | 3 + 5 | 4 + 5 | 5 + (25 rows) + + DROP FUNCTION foo(); + -- + -- table functions + -- + CREATE OR REPLACE FUNCTION foo() + RETURNS TABLE(a int) + AS $$ SELECT a FROM generate_series(1,5) a(a) $$ LANGUAGE sql; + SELECT * FROM foo(); + a + --- + 1 + 2 + 3 + 4 + 5 + (5 rows) + + DROP FUNCTION foo(); + CREATE OR REPLACE FUNCTION foo() + RETURNS TABLE(a int, b int) + AS $$ SELECT a, b + FROM generate_series(1,5) a(a), + generate_series(1,5) b(b) $$ LANGUAGE sql; + SELECT * FROM foo(); + a | b + ---+--- + 1 | 1 + 1 | 2 + 1 | 3 + 1 | 4 + 1 | 5 + 2 | 1 + 2 | 2 + 2 | 3 + 2 | 4 + 2 | 5 + 3 | 1 + 3 | 2 + 3 | 3 + 3 | 4 + 3 | 5 + 4 | 1 + 4 | 2 + 4 | 3 + 4 | 4 + 4 | 5 + 5 | 1 + 5 | 2 + 5 | 3 + 5 | 4 + 5 | 5 + (25 rows) + + DROP FUNCTION foo(); *** ./src/test/regress/sql/rangefuncs.sql.orig 2007-05-06 09:57:38.000000000 +0200 --- ./src/test/regress/sql/rangefuncs.sql 2007-05-06 10:37:26.000000000 +0200 *************** *** 261,263 **** --- 261,297 ---- -- fails, no way to deduce outputs CREATE FUNCTION bad (f1 int, out f2 anyelement, out f3 anyarray) AS 'select $1, array[$1,$1]' LANGUAGE sql; + + -- + -- table functions + -- + CREATE OR REPLACE FUNCTION foo() + RETURNS TABLE(a int) + AS $$ SELECT a FROM generate_series(1,5) a(a) $$ LANGUAGE sql; + SELECT * FROM foo(); + DROP FUNCTION foo(); + + CREATE OR REPLACE FUNCTION foo() + RETURNS TABLE(a int, b int) + AS $$ SELECT a, b + FROM generate_series(1,5) a(a), + generate_series(1,5) b(b) $$ LANGUAGE sql; + SELECT * FROM foo(); + DROP FUNCTION foo(); + + -- + -- table functions + -- + CREATE OR REPLACE FUNCTION foo() + RETURNS TABLE(a int) + AS $$ SELECT a FROM generate_series(1,5) a(a) $$ LANGUAGE sql; + SELECT * FROM foo(); + DROP FUNCTION foo(); + + CREATE OR REPLACE FUNCTION foo() + RETURNS TABLE(a int, b int) + AS $$ SELECT a, b + FROM generate_series(1,5) a(a), + generate_series(1,5) b(b) $$ LANGUAGE sql; + SELECT * FROM foo(); + DROP FUNCTION foo();
*** ./doc/src/sgml/plpgsql.sgml.orig 2007-04-29 03:21:08.000000000 +0200 --- ./doc/src/sgml/plpgsql.sgml 2007-05-06 10:00:58.000000000 +0200 *************** *** 1576,1584 **** <sect3> <title><command>RETURN</></title> ! <synopsis> ! RETURN <replaceable>expression</replaceable>; ! </synopsis> <para> <command>RETURN</command> with an expression terminates the --- 1576,1589 ---- <sect3> <title><command>RETURN</></title> ! <itemizedlist> ! <listitem> ! <para><literal>RETURN <replaceable>expression</replaceable>;</></> ! </listitem> ! <listitem> ! <para><literal>RETURN TABLE ( <replaceable>query</replaceable> );</></> ! </listitem> ! </itemizedlist> <para> <command>RETURN</command> with an expression terminates the *************** *** 1603,1608 **** --- 1608,1622 ---- </para> <para> + If your function returns a set, you can use table expression. + An <replaceable>query</replaceable> is evaluated and result set + is returned to the caller. You cannot mix <command>RETURN TABLE</command> + and <command>RETURN NEXT</command> statements in one function. + Performance might be poor, if result set is very large. The reason + is described below. + </para> + + <para> If you declared the function to return <type>void</type>, a <command>RETURN</command> statement can be used to exit the function early; but do not write an expression following *** ./src/pl/plpgsql/src/gram.y.orig 2007-04-29 03:21:09.000000000 +0200 --- ./src/pl/plpgsql/src/gram.y 2007-05-06 10:16:56.000000000 +0200 *************** *** 30,36 **** static PLpgSQL_type *read_datatype(int tok); static PLpgSQL_stmt *make_execsql_stmt(const char *sqlstart, int lineno); static PLpgSQL_stmt_fetch *read_fetch_direction(void); ! static PLpgSQL_stmt *make_return_stmt(int lineno); static PLpgSQL_stmt *make_return_next_stmt(int lineno); static void check_assignable(PLpgSQL_datum *datum); static void read_into_target(PLpgSQL_rec **rec, PLpgSQL_row **row, --- 30,36 ---- static PLpgSQL_type *read_datatype(int tok); static PLpgSQL_stmt *make_execsql_stmt(const char *sqlstart, int lineno); static PLpgSQL_stmt_fetch *read_fetch_direction(void); ! static PLpgSQL_stmt *make_return_stmt(int lineno, bool is_tblexpr); static PLpgSQL_stmt *make_return_next_stmt(int lineno); static void check_assignable(PLpgSQL_datum *datum); static void read_into_target(PLpgSQL_rec **rec, PLpgSQL_row **row, *************** *** 196,201 **** --- 196,202 ---- %token K_REVERSE %token K_SCROLL %token K_STRICT + %token K_TABLE %token K_THEN %token K_TO %token K_TYPE *************** *** 1185,1194 **** { $$ = make_return_next_stmt($2); } else { plpgsql_push_back_token(tok); ! $$ = make_return_stmt($2); } } ; --- 1186,1200 ---- { $$ = make_return_next_stmt($2); } + else if (tok == K_TABLE) + { + $$ = make_return_stmt($2, true); + + } else { plpgsql_push_back_token(tok); ! $$ = make_return_stmt($2, false); } } ; *************** *** 2105,2111 **** static PLpgSQL_stmt * ! make_return_stmt(int lineno) { PLpgSQL_stmt_return *new; --- 2111,2117 ---- static PLpgSQL_stmt * ! make_return_stmt(int lineno, bool is_tblexpr) { PLpgSQL_stmt_return *new; *************** *** 2114,2121 **** new->lineno = lineno; new->expr = NULL; new->retvarno = -1; ! if (plpgsql_curr_compile->fn_retset) { if (yylex() != ';') yyerror("RETURN cannot have a parameter in function returning set; use RETURN NEXT"); --- 2120,2141 ---- new->lineno = lineno; new->expr = NULL; new->retvarno = -1; + new->is_tblexpr = is_tblexpr; + + if (is_tblexpr) + { + if (!plpgsql_curr_compile->fn_retset) + yyerror("table expression can be used only if function is SETOF or TABLE function"); ! if (yylex() != '(') ! yyerror("expected '('"); ! ! new->expr = read_sql_construct(')', 0, ")", "", false, true, NULL); ! ! if (yylex() != ';') ! yyerror("expected ';'"); ! } ! else if (plpgsql_curr_compile->fn_retset) { if (yylex() != ';') yyerror("RETURN cannot have a parameter in function returning set; use RETURN NEXT"); *** ./src/pl/plpgsql/src/pl_comp.c.orig 2007-05-06 09:56:17.000000000 +0200 --- ./src/pl/plpgsql/src/pl_comp.c 2007-05-06 10:00:59.000000000 +0200 *************** *** 399,404 **** --- 399,408 ---- PLpgSQL_variable *argvariable; int argitemtype; + /* skip table function columns */ + if (argmode == PROARGMODE_TABLE) + continue; + /* Create $n name for variable */ snprintf(buf, sizeof(buf), "$%d", i + 1); *** ./src/pl/plpgsql/src/pl_exec.c.orig 2007-04-29 03:21:09.000000000 +0200 --- ./src/pl/plpgsql/src/pl_exec.c 2007-05-06 10:00:59.000000000 +0200 *************** *** 175,180 **** --- 175,181 ---- static void exec_set_found(PLpgSQL_execstate *estate, bool state); static void plpgsql_create_econtext(PLpgSQL_execstate *estate); static void free_var(PLpgSQL_var *var); + static void materialize_portal(PLpgSQL_execstate *estate, Portal portal); /* ---------- *************** *** 1871,1877 **** --- 1872,1888 ---- * the work will be done at the top level. */ if (estate->retisset) + { + if (stmt->is_tblexpr) + { + Portal portal; + + exec_run_select(estate, stmt->expr, 0, &portal); + materialize_portal(estate, portal); + } + return PLPGSQL_RC_RETURN; + } /* initialize for null result (possibly a tuple) */ estate->retval = (Datum) 0; *************** *** 4984,4986 **** --- 4995,5059 ---- var->freeval = false; } } + + + /* + * Fetch all rows from portal and store its in list of tuples returned + * by the current SRF. + */ + static void + materialize_portal(PLpgSQL_execstate *estate, Portal portal) + { + SPITupleTable *tuptab; + int n; + int i; + TupleDesc tupdesc; + int natts; + HeapTuple tuple; + + if (estate->tuple_store) + elog(ERROR, "cannot mix return table and return next statement"); + + exec_init_tuple_store(estate); + + tupdesc = estate->rettupdesc; + natts = tupdesc->natts; + + SPI_cursor_fetch(portal, true, 10); + n = SPI_processed; + tuptab = SPI_tuptable; + + if (!compatible_tupdesc(tupdesc, tuptab->tupdesc)) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("incompatible TABLE expression in RETURN statement"))); + + while (n > 0) + { + for (i = 0; i < n; i++) + { + tuple = tuptab->vals[i]; + + if (HeapTupleIsValid(tuple)) + { + MemoryContext oldcxt; + + oldcxt = MemoryContextSwitchTo(estate->tuple_store_cxt); + tuplestore_puttuple(estate->tuple_store, tuple); + MemoryContextSwitchTo(oldcxt); + } + } + + SPI_freetuptable(tuptab); + + /* + * Fetch the next 50 tuples + */ + SPI_cursor_fetch(portal, true, 50); + n = SPI_processed; + tuptab = SPI_tuptable; + } + + SPI_freetuptable(tuptab); + SPI_cursor_close(portal); + } *** ./src/pl/plpgsql/src/pl_funcs.c.orig 2007-04-29 03:21:09.000000000 +0200 --- ./src/pl/plpgsql/src/pl_funcs.c 2007-05-06 10:00:59.000000000 +0200 *************** *** 857,862 **** --- 857,864 ---- { dump_ind(); printf("RETURN "); + if (stmt->is_tblexpr) + printf("TABLE "); if (stmt->retvarno >= 0) printf("variable %d", stmt->retvarno); else if (stmt->expr != NULL) *** ./src/pl/plpgsql/src/plpgsql.h.orig 2007-04-29 03:21:09.000000000 +0200 --- ./src/pl/plpgsql/src/plpgsql.h 2007-05-06 10:00:59.000000000 +0200 *************** *** 483,488 **** --- 483,489 ---- int lineno; PLpgSQL_expr *expr; int retvarno; + bool is_tblexpr; } PLpgSQL_stmt_return; typedef struct *** ./src/pl/plpgsql/src/scan.l.orig 2007-04-29 03:21:09.000000000 +0200 --- ./src/pl/plpgsql/src/scan.l 2007-05-06 10:00:59.000000000 +0200 *************** *** 159,164 **** --- 159,165 ---- row_count { return K_ROW_COUNT; } scroll { return K_SCROLL; } strict { return K_STRICT; } + table { return K_TABLE; } then { return K_THEN; } to { return K_TO; } type { return K_TYPE; } *** ./src/test/regress/expected/plpgsql.out.orig 2007-04-29 03:21:09.000000000 +0200 --- ./src/test/regress/expected/plpgsql.out 2007-05-06 10:47:19.000000000 +0200 *************** *** 3051,3053 **** --- 3051,3124 ---- (3 rows) drop function sc_test(); + + -- table functions test + create table tbl_test( + a int, + b int + ); + insert into tbl_test select a, b from generate_series(1,5) a(a), generate_series(1,5) b(b); + --should fail, non unique column names + create function tbl_fce_test(a int) + returns table(a int, a int) as $$ + begin + return table(select * from tbl_test t where t.a < a); + end; + $$ language plpgsql; + ERROR: column "a" duplicated + -- should runtime error + create function tbl_fce_test(a int) + returns table(a int, b int) as $$ + begin + -- nonexisting column + raise notice '%', b; + return table(select * from tbl_test t where t.a < a); + end; + $$ language plpgsql; + select * from tbl_fce_test(3) where a > 2; + ERROR: column "b" does not exist + LINE 1: SELECT b + ^ + QUERY: SELECT b + CONTEXT: PL/pgSQL function "tbl_fce_test" line 3 at raise + -- should work + create or replace function tbl_fce_test(a int) + returns table(a int, b int) as $$ + begin + return table(select * from tbl_test t where t.a < a); + end; + $$ language plpgsql; + select * from tbl_fce_test(3) where a >= 2; + a | b + ---+--- + 2 | 1 + 2 | 2 + 2 | 3 + 2 | 4 + 2 | 5 + (5 rows) + + create function tbl_fce_test2() + returns table (o int) as $$ + begin + return table(select a from tbl_test t where t.a < 3); + end; + $$ language plpgsql; + select * from tbl_fce_test2(); + o + --- + 1 + 1 + 1 + 1 + 1 + 2 + 2 + 2 + 2 + 2 + (10 rows) + + drop function tbl_fce_test2(); + drop function tbl_fce_test(int); + drop table tbl_test; *** ./src/test/regress/sql/plpgsql.sql.orig 2007-04-29 03:21:09.000000000 +0200 --- ./src/test/regress/sql/plpgsql.sql 2007-05-06 10:36:29.000000000 +0200 *************** *** 2535,2537 **** --- 2535,2586 ---- drop function sc_test(); + + + -- table functions test + create table tbl_test( + a int, + b int + ); + + insert into tbl_test select a, b from generate_series(1,5) a(a), generate_series(1,5) b(b); + + --should fail, non unique column names + create function tbl_fce_test(a int) + returns table(a int, a int) as $$ + begin + return table(select * from tbl_test t where t.a < a); + end; + $$ language plpgsql; + + -- should runtime error + create function tbl_fce_test(a int) + returns table(a int, b int) as $$ + begin + -- nonexisting column + raise notice '%', b; + return table(select * from tbl_test t where t.a < a); + end; + $$ language plpgsql; + select * from tbl_fce_test(3) where a > 2; + + -- should work + create or replace function tbl_fce_test(a int) + returns table(a int, b int) as $$ + begin + return table(select * from tbl_test t where t.a < a); + end; + $$ language plpgsql; + select * from tbl_fce_test(3) where a >= 2; + + create function tbl_fce_test2() + returns table (o int) as $$ + begin + return table(select a from tbl_test t where t.a < 3); + end; + $$ language plpgsql; + select * from tbl_fce_test2(); + + drop function tbl_fce_test2(); + drop function tbl_fce_test(int); + drop table tbl_test;
---------------------------(end of broadcast)--------------------------- TIP 9: In versions below 8.0, the planner will ignore your desire to choose an index scan if your joining column's datatypes do not match