summaryrefslogtreecommitdiff
path: root/contrib/tablefunc
diff options
context:
space:
mode:
authorTom Lane <tgl@sss.pgh.pa.us>2005-05-30 23:09:07 +0000
committerTom Lane <tgl@sss.pgh.pa.us>2005-05-30 23:09:07 +0000
commit978129f28e0cba0b6364df672fbd1ae88e3397c4 (patch)
tree94dcfb1cb3494d7981dc9d09c10047129577db40 /contrib/tablefunc
parentb215fae891a7b0e9bcd7126f3aab1c477d4947b1 (diff)
downloadpostgresql-978129f28e0cba0b6364df672fbd1ae88e3397c4.tar.gz
Document get_call_result_type() and friends; mark TypeGetTupleDesc()
and RelationNameGetTupleDesc() as deprecated; remove uses of the latter in the contrib library. Along the way, clean up crosstab() code and documentation a little.
Diffstat (limited to 'contrib/tablefunc')
-rw-r--r--contrib/tablefunc/README.tablefunc110
-rw-r--r--contrib/tablefunc/expected/tablefunc.out53
-rw-r--r--contrib/tablefunc/sql/tablefunc.sql45
-rw-r--r--contrib/tablefunc/tablefunc.c132
-rw-r--r--contrib/tablefunc/tablefunc.sql.in8
5 files changed, 209 insertions, 139 deletions
diff --git a/contrib/tablefunc/README.tablefunc b/contrib/tablefunc/README.tablefunc
index 7097ef0095..63b1c17d8e 100644
--- a/contrib/tablefunc/README.tablefunc
+++ b/contrib/tablefunc/README.tablefunc
@@ -57,11 +57,16 @@ Installation:
but you can create additional crosstab functions per the instructions
in the documentation below.
- crosstab(text sql, N int)
+ crosstab(text sql)
- returns a set of row_name plus N category value columns
- requires anonymous composite type syntax in the FROM clause. See
the instructions in the documentation below.
+ crosstab(text sql, N int)
+ - obsolete version of crosstab()
+ - the argument N is now ignored, since the number of value columns
+ is always determined by the calling query
+
connectby(text relname, text keyid_fld, text parent_keyid_fld
[, text orderby_fld], text start_with, int max_depth
[, text branch_delim])
@@ -133,7 +138,7 @@ Inputs
A SQL statement which produces the source set of data. The SQL statement
must return one row_name column, one category column, and one value
- column.
+ column. row_name and value must be of type text.
e.g. provided sql must produce a set something like:
@@ -152,15 +157,15 @@ Outputs
Returns setof tablefunc_crosstab_N, which is defined by:
- CREATE VIEW tablefunc_crosstab_N AS
- SELECT
- ''::TEXT AS row_name,
- ''::TEXT AS category_1,
- ''::TEXT AS category_2,
+ CREATE TYPE tablefunc_crosstab_N AS (
+ row_name TEXT,
+ category_1 TEXT,
+ category_2 TEXT,
.
.
.
- ''::TEXT AS category_N;
+ category_N TEXT
+ );
for the default installed functions, where N is 2, 3, or 4.
@@ -188,31 +193,9 @@ Notes
6. The installed defaults are for illustration purposes. You
can create your own return types and functions based on the
- crosstab() function of the installed library.
-
- The return type must have a first column that matches the data
- type of the sql set used as its source. The subsequent category
- columns must have the same data type as the value column of the
- sql result set.
-
- Create a VIEW to define your return type, similar to the VIEWS
- in the provided installation script. Then define a unique function
- name accepting one text parameter and returning setof your_view_name.
- For example, if your source data produces row_names that are TEXT,
- and values that are FLOAT8, and you want 5 category columns:
+ crosstab() function of the installed library. See below for
+ details.
- CREATE VIEW my_crosstab_float8_5_cols AS
- SELECT
- ''::TEXT AS row_name,
- 0::FLOAT8 AS category_1,
- 0::FLOAT8 AS category_2,
- 0::FLOAT8 AS category_3,
- 0::FLOAT8 AS category_4,
- 0::FLOAT8 AS category_5;
-
- CREATE OR REPLACE FUNCTION crosstab_float8_5_cols(text)
- RETURNS setof my_crosstab_float8_5_cols
- AS '$libdir/tablefunc','crosstab' LANGUAGE 'c' STABLE STRICT;
Example usage
@@ -241,11 +224,12 @@ select * from crosstab3(
==================================================================
Name
-crosstab(text, int) - returns a set of row_name
- plus N category value columns
+crosstab(text) - returns a set of row_names plus category value columns
Synopsis
+crosstab(text sql)
+
crosstab(text sql, int N)
Inputs
@@ -271,15 +255,16 @@ Inputs
N
- number of category value columns
+ Obsolete argument; ignored if supplied (formerly this had to match
+ the number of category columns determined by the calling query)
Outputs
- Returns setof record, which must defined with a column definition
+ Returns setof record, which must be defined with a column definition
in the FROM clause of the SELECT statement, e.g.:
SELECT *
- FROM crosstab(sql, 2) AS ct(row_name text, category_1 text, category_2 text);
+ FROM crosstab(sql) AS ct(row_name text, category_1 text, category_2 text);
the example crosstab function produces a set something like:
<== values columns ==>
@@ -292,9 +277,12 @@ Notes
1. The sql result must be ordered by 1,2.
- 2. The number of values columns is determined at run-time. The
- column definition provided in the FROM clause must provide for
- N + 1 columns of the proper data types.
+ 2. The number of values columns is determined by the column definition
+ provided in the FROM clause. The FROM clause must define one
+ row_name column (of the same datatype as the first result column
+ of the sql query) followed by N category columns (of the same
+ datatype as the third result column of the sql query). You can
+ set up as many category columns as you wish.
3. Missing values (i.e. not enough adjacent rows of same row_name to
fill the number of result values columns) are filled in with nulls.
@@ -304,6 +292,44 @@ Notes
5. Rows with all nulls in the values columns are skipped.
+ 6. You can avoid always having to write out a FROM clause that defines the
+ output columns by setting up a custom crosstab function that has
+ the desired output row type wired into its definition.
+
+ There are two ways you can set up a custom crosstab function:
+
+ A. Create a composite type to define your return type, similar to the
+ examples in the installation script. Then define a unique function
+ name accepting one text parameter and returning setof your_type_name.
+ For example, if your source data produces row_names that are TEXT,
+ and values that are FLOAT8, and you want 5 category columns:
+
+ CREATE TYPE my_crosstab_float8_5_cols AS (
+ row_name TEXT,
+ category_1 FLOAT8,
+ category_2 FLOAT8,
+ category_3 FLOAT8,
+ category_4 FLOAT8,
+ category_5 FLOAT8
+ );
+
+ CREATE OR REPLACE FUNCTION crosstab_float8_5_cols(text)
+ RETURNS setof my_crosstab_float8_5_cols
+ AS '$libdir/tablefunc','crosstab' LANGUAGE 'c' STABLE STRICT;
+
+ B. Use OUT parameters to define the return type implicitly.
+ The same example could also be done this way:
+
+ CREATE OR REPLACE FUNCTION crosstab_float8_5_cols(IN text,
+ OUT row_name TEXT,
+ OUT category_1 FLOAT8,
+ OUT category_2 FLOAT8,
+ OUT category_3 FLOAT8,
+ OUT category_4 FLOAT8,
+ OUT category_5 FLOAT8)
+ RETURNS setof record
+ AS '$libdir/tablefunc','crosstab' LANGUAGE 'c' STABLE STRICT;
+
Example usage
@@ -418,6 +444,10 @@ Notes
5. Rows with a null row_name column are skipped.
+ 6. You can create predefined functions to avoid having to write out
+ the result column names/types in each query. See the examples
+ for crosstab(text).
+
Example usage
diff --git a/contrib/tablefunc/expected/tablefunc.out b/contrib/tablefunc/expected/tablefunc.out
index d8f4c30a5d..50faf4390f 100644
--- a/contrib/tablefunc/expected/tablefunc.out
+++ b/contrib/tablefunc/expected/tablefunc.out
@@ -102,27 +102,40 @@ SELECT * FROM crosstab4('SELECT rowid, attribute, val FROM ct where rowclass = '
test4 | val4 | val5 | val6 |
(2 rows)
-SELECT * FROM crosstab('SELECT rowid, attribute, val FROM ct where rowclass = ''group1'' ORDER BY 1,2;', 2) AS c(rowid text, att1 text, att2 text);
+SELECT * FROM crosstab('SELECT rowid, attribute, val FROM ct where rowclass = ''group1'' ORDER BY 1,2;') AS c(rowid text, att1 text, att2 text);
rowid | att1 | att2
-------+------+------
test1 | val1 | val2
test2 | val5 | val6
(2 rows)
-SELECT * FROM crosstab('SELECT rowid, attribute, val FROM ct where rowclass = ''group1'' ORDER BY 1,2;', 3) AS c(rowid text, att1 text, att2 text, att3 text);
+SELECT * FROM crosstab('SELECT rowid, attribute, val FROM ct where rowclass = ''group1'' ORDER BY 1,2;') AS c(rowid text, att1 text, att2 text, att3 text);
rowid | att1 | att2 | att3
-------+------+------+------
test1 | val1 | val2 | val3
test2 | val5 | val6 | val7
(2 rows)
-SELECT * FROM crosstab('SELECT rowid, attribute, val FROM ct where rowclass = ''group1'' ORDER BY 1,2;', 4) AS c(rowid text, att1 text, att2 text, att3 text, att4 text);
+SELECT * FROM crosstab('SELECT rowid, attribute, val FROM ct where rowclass = ''group1'' ORDER BY 1,2;') AS c(rowid text, att1 text, att2 text, att3 text, att4 text);
rowid | att1 | att2 | att3 | att4
-------+------+------+------+------
test1 | val1 | val2 | val3 | val4
test2 | val5 | val6 | val7 | val8
(2 rows)
+-- check it works with OUT parameters, too
+CREATE FUNCTION crosstab_out(text,
+ OUT rowid text, OUT att1 text, OUT att2 text, OUT att3 text)
+RETURNS setof record
+AS '$libdir/tablefunc','crosstab'
+LANGUAGE 'C' STABLE STRICT;
+SELECT * FROM crosstab_out('SELECT rowid, attribute, val FROM ct where rowclass = ''group1'' ORDER BY 1,2;');
+ rowid | att1 | att2 | att3
+-------+------+------+------
+ test1 | val1 | val2 | val3
+ test2 | val5 | val6 | val7
+(2 rows)
+
--
-- hash based crosstab
--
@@ -211,6 +224,40 @@ AS c(rowid text, rowdt timestamp, temperature text, test_result text, test_start
-------+-------+-------------+-------------+----------------+-------
(0 rows)
+-- check it works with a named result rowtype
+create type my_crosstab_result as (
+ rowid text, rowdt timestamp,
+ temperature int4, test_result text, test_startdate timestamp, volts float8);
+CREATE FUNCTION crosstab_named(text, text)
+RETURNS setof my_crosstab_result
+AS '$libdir/tablefunc','crosstab_hash'
+LANGUAGE 'C' STABLE STRICT;
+SELECT * FROM crosstab_named(
+ 'SELECT rowid, rowdt, attribute, val FROM cth ORDER BY 1',
+ 'SELECT DISTINCT attribute FROM cth ORDER BY 1');
+ rowid | rowdt | temperature | test_result | test_startdate | volts
+-------+--------------------------+-------------+-------------+--------------------------+--------
+ test1 | Sat Mar 01 00:00:00 2003 | 42 | PASS | | 2.6987
+ test2 | Sun Mar 02 00:00:00 2003 | 53 | FAIL | Sat Mar 01 00:00:00 2003 | 3.1234
+(2 rows)
+
+-- check it works with OUT parameters
+CREATE FUNCTION crosstab_out(text, text,
+ OUT rowid text, OUT rowdt timestamp,
+ OUT temperature int4, OUT test_result text,
+ OUT test_startdate timestamp, OUT volts float8)
+RETURNS setof record
+AS '$libdir/tablefunc','crosstab_hash'
+LANGUAGE 'C' STABLE STRICT;
+SELECT * FROM crosstab_out(
+ 'SELECT rowid, rowdt, attribute, val FROM cth ORDER BY 1',
+ 'SELECT DISTINCT attribute FROM cth ORDER BY 1');
+ rowid | rowdt | temperature | test_result | test_startdate | volts
+-------+--------------------------+-------------+-------------+--------------------------+--------
+ test1 | Sat Mar 01 00:00:00 2003 | 42 | PASS | | 2.6987
+ test2 | Sun Mar 02 00:00:00 2003 | 53 | FAIL | Sat Mar 01 00:00:00 2003 | 3.1234
+(2 rows)
+
--
-- connectby
--
diff --git a/contrib/tablefunc/sql/tablefunc.sql b/contrib/tablefunc/sql/tablefunc.sql
index c464acbd3b..3229086133 100644
--- a/contrib/tablefunc/sql/tablefunc.sql
+++ b/contrib/tablefunc/sql/tablefunc.sql
@@ -34,9 +34,19 @@ SELECT * FROM crosstab2('SELECT rowid, attribute, val FROM ct where rowclass = '
SELECT * FROM crosstab3('SELECT rowid, attribute, val FROM ct where rowclass = ''group2'' ORDER BY 1,2;');
SELECT * FROM crosstab4('SELECT rowid, attribute, val FROM ct where rowclass = ''group2'' ORDER BY 1,2;');
-SELECT * FROM crosstab('SELECT rowid, attribute, val FROM ct where rowclass = ''group1'' ORDER BY 1,2;', 2) AS c(rowid text, att1 text, att2 text);
-SELECT * FROM crosstab('SELECT rowid, attribute, val FROM ct where rowclass = ''group1'' ORDER BY 1,2;', 3) AS c(rowid text, att1 text, att2 text, att3 text);
-SELECT * FROM crosstab('SELECT rowid, attribute, val FROM ct where rowclass = ''group1'' ORDER BY 1,2;', 4) AS c(rowid text, att1 text, att2 text, att3 text, att4 text);
+SELECT * FROM crosstab('SELECT rowid, attribute, val FROM ct where rowclass = ''group1'' ORDER BY 1,2;') AS c(rowid text, att1 text, att2 text);
+SELECT * FROM crosstab('SELECT rowid, attribute, val FROM ct where rowclass = ''group1'' ORDER BY 1,2;') AS c(rowid text, att1 text, att2 text, att3 text);
+SELECT * FROM crosstab('SELECT rowid, attribute, val FROM ct where rowclass = ''group1'' ORDER BY 1,2;') AS c(rowid text, att1 text, att2 text, att3 text, att4 text);
+
+-- check it works with OUT parameters, too
+
+CREATE FUNCTION crosstab_out(text,
+ OUT rowid text, OUT att1 text, OUT att2 text, OUT att3 text)
+RETURNS setof record
+AS '$libdir/tablefunc','crosstab'
+LANGUAGE 'C' STABLE STRICT;
+
+SELECT * FROM crosstab_out('SELECT rowid, attribute, val FROM ct where rowclass = ''group1'' ORDER BY 1,2;');
--
-- hash based crosstab
@@ -100,6 +110,35 @@ SELECT * FROM crosstab(
'SELECT DISTINCT attribute FROM cth WHERE false ORDER BY 1')
AS c(rowid text, rowdt timestamp, temperature text, test_result text, test_startdate text, volts text);
+-- check it works with a named result rowtype
+
+create type my_crosstab_result as (
+ rowid text, rowdt timestamp,
+ temperature int4, test_result text, test_startdate timestamp, volts float8);
+
+CREATE FUNCTION crosstab_named(text, text)
+RETURNS setof my_crosstab_result
+AS '$libdir/tablefunc','crosstab_hash'
+LANGUAGE 'C' STABLE STRICT;
+
+SELECT * FROM crosstab_named(
+ 'SELECT rowid, rowdt, attribute, val FROM cth ORDER BY 1',
+ 'SELECT DISTINCT attribute FROM cth ORDER BY 1');
+
+-- check it works with OUT parameters
+
+CREATE FUNCTION crosstab_out(text, text,
+ OUT rowid text, OUT rowdt timestamp,
+ OUT temperature int4, OUT test_result text,
+ OUT test_startdate timestamp, OUT volts float8)
+RETURNS setof record
+AS '$libdir/tablefunc','crosstab_hash'
+LANGUAGE 'C' STABLE STRICT;
+
+SELECT * FROM crosstab_out(
+ 'SELECT rowid, rowdt, attribute, val FROM cth ORDER BY 1',
+ 'SELECT DISTINCT attribute FROM cth ORDER BY 1');
+
--
-- connectby
--
diff --git a/contrib/tablefunc/tablefunc.c b/contrib/tablefunc/tablefunc.c
index 0aa52b5281..6ab79ebeff 100644
--- a/contrib/tablefunc/tablefunc.c
+++ b/contrib/tablefunc/tablefunc.c
@@ -51,8 +51,6 @@ static void validateConnectbyTupleDesc(TupleDesc tupdesc, bool show_branch, bool
static bool compatCrosstabTupleDescs(TupleDesc tupdesc1, TupleDesc tupdesc2);
static bool compatConnectbyTupleDescs(TupleDesc tupdesc1, TupleDesc tupdesc2);
static void get_normal_pair(float8 *x1, float8 *x2);
-static TupleDesc make_crosstab_tupledesc(TupleDesc spi_tupdesc,
- int num_categories);
static Tuplestorestate *connectby(char *relname,
char *key_fld,
char *parent_key_fld,
@@ -332,12 +330,14 @@ get_normal_pair(float8 *x1, float8 *x2)
* NOTES:
* 1. SQL result must be ordered by 1,2.
* 2. The number of values columns depends on the tuple description
- * of the function's declared return type.
- * 2. Missing values (i.e. not enough adjacent rows of same rowid to
+ * of the function's declared return type. The return type's columns
+ * must match the datatypes of the SQL query's result. The datatype
+ * of the category column can be anything, however.
+ * 3. Missing values (i.e. not enough adjacent rows of same rowid to
* fill the number of result values columns) are filled in with nulls.
- * 3. Extra values (i.e. too many adjacent rows of same rowid to fill
+ * 4. Extra values (i.e. too many adjacent rows of same rowid to fill
* the number of result values columns) are skipped.
- * 4. Rows with all nulls in the values columns are skipped.
+ * 5. Rows with all nulls in the values columns are skipped.
*/
PG_FUNCTION_INFO_V1(crosstab);
Datum
@@ -360,10 +360,7 @@ crosstab(PG_FUNCTION_ARGS)
if (SRF_IS_FIRSTCALL())
{
char *sql = GET_STR(PG_GETARG_TEXT_P(0));
- Oid funcid = fcinfo->flinfo->fn_oid;
- Oid functypeid;
- char functyptype;
- TupleDesc tupdesc = NULL;
+ TupleDesc tupdesc;
int ret;
int proc;
@@ -391,20 +388,23 @@ crosstab(PG_FUNCTION_ARGS)
spi_tuptable = SPI_tuptable;
spi_tupdesc = spi_tuptable->tupdesc;
- /*
+ /*----------
* The provided SQL query must always return three columns.
*
- * 1. rowname the label or identifier for each row in the final
- * result 2. category the label or identifier for each column
- * in the final result 3. values the value for each column
- * in the final result
+ * 1. rowname
+ * the label or identifier for each row in the final result
+ * 2. category
+ * the label or identifier for each column in the final result
+ * 3. values
+ * the value for each column in the final result
+ *----------
*/
if (spi_tupdesc->natts != 3)
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("invalid source data SQL statement"),
- errdetail("The provided SQL must return 3 " \
- " columns; rowid, category, and values.")));
+ errdetail("The provided SQL must return 3 "
+ "columns: rowid, category, and values.")));
}
else
{
@@ -416,39 +416,31 @@ crosstab(PG_FUNCTION_ARGS)
/* SPI switches context on us, so reset it */
MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
- /* get the typeid that represents our return type */
- functypeid = get_func_rettype(funcid);
-
- /* check typtype to see if we have a predetermined return type */
- functyptype = get_typtype(functypeid);
-
- if (functyptype == 'c')
+ /* get a tuple descriptor for our result type */
+ switch (get_call_result_type(fcinfo, NULL, &tupdesc))
{
- /* Build a tuple description for a named composite type */
- tupdesc = TypeGetTupleDesc(functypeid, NIL);
- }
- else if (functypeid == RECORDOID)
- {
- if (fcinfo->nargs != 2)
+ case TYPEFUNC_COMPOSITE:
+ /* success */
+ break;
+ case TYPEFUNC_RECORD:
+ /* failed to determine actual type of RECORD */
ereport(ERROR,
- (errcode(ERRCODE_SYNTAX_ERROR),
- errmsg("wrong number of arguments")));
- else
- {
- int num_categories = PG_GETARG_INT32(1);
-
- tupdesc = make_crosstab_tupledesc(spi_tupdesc, num_categories);
- }
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("function returning record called in context "
+ "that cannot accept type record")));
+ break;
+ default:
+ /* result type isn't composite */
+ elog(ERROR, "return type must be a row type");
+ break;
}
- else
- ereport(ERROR,
- (errcode(ERRCODE_SYNTAX_ERROR),
- errmsg("return type must be a row type")));
+
+ /* make sure we have a persistent copy of the tupdesc */
+ tupdesc = CreateTupleDescCopy(tupdesc);
/*
- * Check that return tupdesc is compatible with the one we got
- * from ret_relname, at least based on number and type of
- * attributes
+ * Check that return tupdesc is compatible with the data we got
+ * from SPI, at least based on number and type of attributes
*/
if (!compatCrosstabTupleDescs(tupdesc, spi_tupdesc))
ereport(ERROR,
@@ -679,8 +671,8 @@ crosstab(PG_FUNCTION_ARGS)
* 1. SQL result must be ordered by 1.
* 2. The number of values columns depends on the tuple description
* of the function's declared return type.
- * 2. Missing values (i.e. missing category) are filled in with nulls.
- * 3. Extra values (i.e. not in category results) are skipped.
+ * 3. Missing values (i.e. missing category) are filled in with nulls.
+ * 4. Extra values (i.e. not in category results) are skipped.
*/
PG_FUNCTION_INFO_V1(crosstab_hash);
Datum
@@ -1628,52 +1620,6 @@ compatCrosstabTupleDescs(TupleDesc ret_tupdesc, TupleDesc sql_tupdesc)
return true;
}
-static TupleDesc
-make_crosstab_tupledesc(TupleDesc spi_tupdesc, int num_categories)
-{
- Form_pg_attribute sql_attr;
- Oid sql_atttypid;
- TupleDesc tupdesc;
- int natts;
- AttrNumber attnum;
- char attname[NAMEDATALEN];
- int i;
-
- /*
- * We need to build a tuple description with one column for the
- * rowname, and num_categories columns for the values. Each must be of
- * the same type as the corresponding spi result input column.
- */
- natts = num_categories + 1;
- tupdesc = CreateTemplateTupleDesc(natts, false);
-
- /* first the rowname column */
- attnum = 1;
-
- sql_attr = spi_tupdesc->attrs[0];
- sql_atttypid = sql_attr->atttypid;
-
- strcpy(attname, "rowname");
-
- TupleDescInitEntry(tupdesc, attnum, attname, sql_atttypid,
- -1, 0);
-
- /* now the category values columns */
- sql_attr = spi_tupdesc->attrs[2];
- sql_atttypid = sql_attr->atttypid;
-
- for (i = 0; i < num_categories; i++)
- {
- attnum++;
-
- sprintf(attname, "category_%d", i + 1);
- TupleDescInitEntry(tupdesc, attnum, attname, sql_atttypid,
- -1, 0);
- }
-
- return tupdesc;
-}
-
/*
* Return a properly quoted literal value.
* Uses quote_literal in quote.c
diff --git a/contrib/tablefunc/tablefunc.sql.in b/contrib/tablefunc/tablefunc.sql.in
index f3d5d3d8a1..fee9cb7e17 100644
--- a/contrib/tablefunc/tablefunc.sql.in
+++ b/contrib/tablefunc/tablefunc.sql.in
@@ -6,6 +6,13 @@ RETURNS setof float8
AS 'MODULE_PATHNAME','normal_rand'
LANGUAGE 'C' VOLATILE STRICT;
+-- the generic crosstab function:
+CREATE OR REPLACE FUNCTION crosstab(text)
+RETURNS setof record
+AS 'MODULE_PATHNAME','crosstab'
+LANGUAGE 'C' STABLE STRICT;
+
+-- examples of building custom type-specific crosstab functions:
CREATE TYPE tablefunc_crosstab_2 AS
(
row_name TEXT,
@@ -45,6 +52,7 @@ RETURNS setof tablefunc_crosstab_4
AS 'MODULE_PATHNAME','crosstab'
LANGUAGE 'C' STABLE STRICT;
+-- obsolete:
CREATE OR REPLACE FUNCTION crosstab(text,int)
RETURNS setof record
AS 'MODULE_PATHNAME','crosstab'