Dimitri Fontaine <dimi...@2ndquadrant.fr> writes: > Itagaki Takahiro <itagaki.takah...@gmail.com> writes: >> My suggestion is to introduce pg_read_binary_file() function that can >> read any files in the server, and make CREATE EXTENSION to use the >> function. Of course, pg_execute_[sql|from]_file() can simplify queries > > It seems like all you're missing in the current patch is the encoding > option in there. Exposing the internal functions is simple enough and > solves it, but I much prefer the current simple-case user API. Do you > want me to add a variant with an extra 'encoding' parameter?
Here's the result: dim=# \df pg_exe*|replace_*|*binary* List of functions Schema | Name | Result data type | Argument data types | Type ------------+-----------------------+------------------+---------------------------+-------- pg_catalog | pg_execute_sql_file | void | text | normal pg_catalog | pg_execute_sql_file | void | text, name | normal pg_catalog | pg_execute_sql_file | void | text, name, VARIADIC text | normal pg_catalog | pg_execute_sql_string | void | text | normal pg_catalog | pg_execute_sql_string | void | text, VARIADIC text | normal pg_catalog | pg_read_binary_file | bytea | text, bigint, bigint | normal pg_catalog | replace_placeholders | text | text, VARIADIC text | normal (7 rows) I think that answers fine to your concerns, in that you can manipulate the low-level functions in order to control each step, or you can use the higher-level functions and just pass them the arguments you want. Note that the "name" arguments here are the encoding. The documentation of the pg_execute_sql_string() function should close the gap noted by Joshua Tolley about how to format placeholder names, because it gives examples both using '@schema@' and 's' as names. Regards, -- Dimitri Fontaine http://2ndQuadrant.fr PostgreSQL : Expertise, Formation et Support
*** a/doc/src/sgml/func.sgml --- b/doc/src/sgml/func.sgml *************** *** 1853,1858 **** --- 1853,1878 ---- <row> <entry> <indexterm> + <primary>replace_placeholders</primary> + </indexterm> + <literal><function>replace_placeholders(<parameter>string</parameter> <type>text</type>, + <parameter>from</parameter> <type>text</type>, + <parameter>to</parameter> <type>text</type> + [, <parameter>from</parameter> <type>text</type>, + <parameter>to</parameter> <type>text</type>, + [, ...] ])</function></literal> + </entry> + <entry><type>text</type></entry> + <entry>Replace all occurrences in <parameter>string</parameter> of substring + <parameter>from</parameter> with substring <parameter>to</parameter> + </entry> + <entry><literal>replace('abcdefabcdef', 'cd', 'XX', 'ef', 'YY')</literal></entry> + <entry><literal>abXXYYabXXYY</literal></entry> + </row> + + <row> + <entry> + <indexterm> <primary>reverse</primary> </indexterm> <literal><function>reverse(<parameter>str</parameter>)</function></literal> *************** *** 14456,14466 **** postgres=# SELECT * FROM pg_xlogfile_name_offset(pg_stop_backup()); --- 14476,14514 ---- </row> <row> <entry> + <literal><function>pg_read_binary_file(<parameter>filename</> <type>text</>, <parameter>offset</> <type>bigint</>, <parameter>length</> <type>bigint</>)</function></literal> + </entry> + <entry><type>bytea</type></entry> + <entry>Return the contents of a file</entry> + </row> + <row> + <entry> <literal><function>pg_stat_file(<parameter>filename</> <type>text</>)</function></literal> </entry> <entry><type>record</type></entry> <entry>Return information about a file</entry> </row> + <row> + <entry> + <literal><function>pg_execute_sql_string(<parameter>sql</> <type>text</> + [, <parameter>variable</parameter> <type>text</type>, <parameter>value</parameter> <type>text</type> + [, ...] ]) )</function></literal> + </entry> + <entry><type>void</type></entry> + <entry>Executes the given <acronym>SQL</> commands, replacing placeholders, if any.</entry> + </row> + <row> + <entry> + <literal><function>pg_execute_sql_file(<parameter>filename</> <type>text</> + [ [, <parameter>encoding</parameter> <type>name</type>] + [, <parameter>variable</parameter> <type>text</type>, <parameter>value</parameter> <type>text</type> + [, ...] ] ]) )</function></literal> + </entry> + <entry><type>void</type></entry> + <entry>Executes the <acronym>SQL</> commands contained in a file, + expected in either database encoding or given encoding, and replacing + given placeholders, if any.</entry> + </row> </tbody> </tgroup> </table> *************** *** 14478,14487 **** postgres=# SELECT * FROM pg_xlogfile_name_offset(pg_stop_backup()); <primary>pg_read_file</primary> </indexterm> <para> ! <function>pg_read_file</> returns part of a text file, starting ! at the given <parameter>offset</>, returning at most <parameter>length</> ! bytes (less if the end of file is reached first). If <parameter>offset</> ! is negative, it is relative to the end of the file. </para> <indexterm> --- 14526,14555 ---- <primary>pg_read_file</primary> </indexterm> <para> ! <function>pg_read_file</> returns part of a text file, starting at the ! given <parameter>offset</>, returning at most <parameter>length</> bytes ! (less if the end of file is reached first). If <parameter>offset</> is ! negative, it is relative to the end of the file. ! </para> ! ! <indexterm> ! <primary>pg_read_binary_file</primary> ! </indexterm> ! <para> ! <function>pg_read_binary_file</> returns part of a file, starting at the ! given <parameter>offset</>, returning at most <parameter>length</> bytes ! (less if the end of file is reached first). If <parameter>offset</> is ! negative, it is relative to the end of the ! file. If <parameter>bytes_to_read</> is <literal>-1</>, the file is read ! until reaching its end. ! <programlisting> ! SELECT convert_from(pg_read_binary_file('postgresql.conf', -69, -1), 'utf8'); ! convert_from ! ------------------------------------------------------------------------------- ! #custom_variable_classes = '' # list of custom variable class names+ ! ! (1 row) ! </programlisting> </para> <indexterm> *************** *** 14499,14504 **** SELECT (pg_stat_file('filename')).modification; --- 14567,14628 ---- </programlisting> </para> + <indexterm> + <primary>pg_execute_sql_string</primary> + </indexterm> + <para> + <function>pg_execute_sql_string</> makes the server execute + given <acronym>SQL</> commands. This function is reserved to superusers. + </para> + <para> + The function accepts an optional list of placeholder variables as + arguments, and will replace any given placeholder name by its value, + found in the next parameter. Here's an example: + <programlisting> + SELECT pg_execute_sql_string('CREATE SCHEMA @schema@;', '@schema@', 'utils'); + pg_execute_sql_string + ----------------------- + + (1 row) + + SELECT pg_execute_sql_string('CREATE SCHEMA s;', 's', 'bar'); + pg_execute_sql_string + ----------------------- + + (1 row) + + SELECT oid, * from pg_namespace where nspname in ('utils', 'bar'); + oid | nspname | nspowner | nspacl + -------+---------+----------+-------- + 16387 | utils | 10 | + 16388 | bar | 10 | + (2 rows) + </programlisting> + </para> + + <indexterm> + <primary>pg_execute_sql_file</primary> + </indexterm> + <para> + <function>pg_execute_sql_file</> makes the server + execute <acronym>SQL</> commands to be found in a file. This function is + reserved to superusers. + </para> + <para> + When the file is known to be encoded the same way as the database, you + can leave + the <parameter>encoding</parameter> <literal>NULL</>. Otherwise, you + need to give the file encoding and <productname>PostgreSQL</> will + convert the file content in the database encoding for you. + </para> + <para> + The script might contain placeholders that will be replaced by the + values given in the <literal>VARIADIC</literal> arguments, which must be + a pair of variable names and values. No specific formating is required + as far as placeholder names are concerned, so that you can follow your + own policies. + </para> + <para> The functions shown in <xref linkend="functions-advisory-locks"> manage advisory locks. For details about proper use of these functions, see *************** *** 14521,14526 **** SELECT (pg_stat_file('filename')).modification; --- 14645,14651 ---- <entry><type>void</type></entry> <entry>Obtain exclusive advisory lock</entry> </row> + <row> <entry> <literal><function>pg_advisory_lock(<parameter>key1</> <type>int</>, <parameter>key2</> <type>int</>)</function></literal> *** a/src/backend/utils/adt/genfile.c --- b/src/backend/utils/adt/genfile.c *************** *** 7,12 **** --- 7,13 ---- * Copyright (c) 2004-2010, PostgreSQL Global Development Group * * Author: Andreas Pflug <pgad...@pse-consulting.de> + * Dimitri Fontaine <dimi...@2ndquadrant.fr> * * IDENTIFICATION * src/backend/utils/adt/genfile.c *************** *** 21,31 **** --- 22,34 ---- #include <dirent.h> #include "catalog/pg_type.h" + #include "executor/spi.h" #include "funcapi.h" #include "mb/pg_wchar.h" #include "miscadmin.h" #include "postmaster/syslogger.h" #include "storage/fd.h" + #include "utils/array.h" #include "utils/builtins.h" #include "utils/memutils.h" #include "utils/timestamp.h" *************** *** 264,266 **** pg_ls_dir(PG_FUNCTION_ARGS) --- 267,564 ---- SRF_RETURN_DONE(funcctx); } + + /* + * Support functions for pg_execute_sql_file and its variant, + * pg_execute_sql_file_with_placeholders. + */ + static char * + pg_read_binary_file_internal(const char *filename, int64 offset, int64 bytes_to_read) + { + FILE *file; + int64 fsize = -1, nbytes; + struct stat fst; + char *file_content = NULL; + + /* + * Only superuser can call pg_execute_sql_file, and CREATE EXTENSION + * uses that too. Don't double check the PATH. Also note that + * extension's install files are not in $PGDATA but `pg_config + * --sharedir`. + */ + if (stat(filename, &fst) < 0) + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not stat file \"%s\": %m", filename))); + + if (bytes_to_read <= -1) + fsize = Int64GetDatum((int64) fst.st_size); + else + fsize = bytes_to_read; + + if ((file = AllocateFile(filename, PG_BINARY_R)) == NULL) + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not open file \"%s\" for reading: %m", + filename))); + + if (fseeko(file, (off_t) offset, + (offset >= 0) ? SEEK_SET : SEEK_END) != 0) + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not seek in file \"%s\": %m", filename))); + + if (ferror(file)) + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not read file \"%s\": %m", filename))); + + file_content = (char *)palloc0((fsize+1)*sizeof(char)); + nbytes = fread(file_content, 1, (size_t) fsize, file); + FreeFile(file); + + return file_content; + } + + /* + * Expose pg_read_binary_file() at the SQL level too + */ + Datum + pg_read_binary_file(PG_FUNCTION_ARGS) + { + text *filename_t = PG_GETARG_TEXT_P(0); + int64 seek_offset = PG_GETARG_INT64(1); + int64 bytes_to_read = PG_GETARG_INT64(2); + char *filename = text_to_cstring(filename_t); + + PG_RETURN_BYTEA_P( + cstring_to_text( + pg_read_binary_file_internal(filename, + seek_offset, bytes_to_read))); + } + + /* + * Given an array of repeated {variable, value}, replaces the placeholders + * by their values in the given query_string, by calling replace_text over + * each pair of arguments. + */ + static char * + replace_placeholders_internal(const char *query_string, + ArrayType *placeholders) + { + text *src = cstring_to_text(query_string); + Datum *replacements; + int nrep; + int i; + char *ret; + + Assert(ARR_ELEMTYPE(placeholders) == TEXTOID); + + deconstruct_array(placeholders, TEXTOID, -1, false, 'i', + &replacements, NULL, &nrep); + + if (nrep % 2 != 0) + ereport(ERROR, + (errmsg("Expected pairs of variable names and values"), + errdetail("Please give an even number of replacement parameters"))); + + for (i = 0; i < nrep; i+=2) + { + Datum rep; + + elog(DEBUG1, + "pg_execute_sql_file replaces '%s' with '%s'", + text_to_cstring(DatumGetTextP(replacements[i])), + text_to_cstring(DatumGetTextP(replacements[i+1]))); + + rep = DirectFunctionCall3(replace_text, + PointerGetDatum(src), + replacements[i], replacements[i+1]); + src = DatumGetTextP(rep); + } + ret = text_to_cstring(src); + elog(DEBUG2, "pg_execute_sql_file: %s", ret); + + return ret; + } + + /* + * Have replace_placeholders SQL callable directly + */ + Datum + replace_placeholders(PG_FUNCTION_ARGS) + { + text *src_t = PG_GETARG_TEXT_P(0); + char *src = text_to_cstring(src_t); + ArrayType *placeholders = PG_GETARG_ARRAYTYPE_P(1); + char *replaced = replace_placeholders_internal(src, placeholders); + + PG_RETURN_TEXT_P(cstring_to_text(replaced)); + } + + + /* + * The bulk of the execute from file functions, just call SPI to do the real + * work + */ + static void + pg_execute_internal(const char *filename, const char *query_string) + { + /* + * We abuse some internal knowledge from spi.h here. As we don't know + * which queries are going to get executed, we don't know what to expect + * as an OK return code from SPI_execute(). We assume that + * SPI_OK_CONNECT, SPI_OK_FINISH and SPI_OK_FETCH are quite improbable, + * though, and the errors are negatives. So a valid return code is + * considered to be SPI_OK_UTILITY or anything from there. + */ + if (SPI_connect() != SPI_OK_CONNECT) + elog(ERROR, "SPI_connect failed"); + + if (SPI_execute(query_string, false, 0) < SPI_OK_UTILITY) + ereport(ERROR, + (errcode(ERRCODE_DATA_EXCEPTION), + (filename + ? errmsg("could not execute sql file: '%s'", filename) + : errmsg("could not execute sql string")))); + + if (SPI_finish() != SPI_OK_FINISH) + elog(ERROR, "SPI_finish failed"); + + return; + } + + /* + * The SQL callable version of it. + */ + Datum + pg_execute_sql_string(PG_FUNCTION_ARGS) + { + text *sql_text = PG_GETARG_TEXT_P(0); + pg_execute_internal(NULL, text_to_cstring(sql_text)); + PG_RETURN_VOID(); + } + + /* + * Execute SQL string containing placeholders. + */ + Datum + pg_execute_sql_string_with_placeholders(PG_FUNCTION_ARGS) + { + text *sql_text = PG_GETARG_TEXT_P(0); + ArrayType *placeholders = PG_GETARG_ARRAYTYPE_P(1); + char *sql_ph = text_to_cstring(sql_text); + char *sql = replace_placeholders_internal(sql_ph, placeholders); + + /* + * pg_execute_internal() error reporting includes filename, unless NULL + */ + pg_execute_internal(NULL, sql); + PG_RETURN_VOID(); + } + + /* + * Read a file then execute the SQL commands it contains. + */ + Datum + pg_execute_sql_file(PG_FUNCTION_ARGS) + { + text *filename_t = PG_GETARG_TEXT_P(0); + char *filename = text_to_cstring(filename_t); + char *sql; + + if (!superuser()) + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + (errmsg("must be superuser to execute sql file")))); + + sql = pg_read_binary_file_internal(filename, 0, -1); + pg_verifymbstr(sql, strlen(sql), false); + pg_execute_internal(filename, sql); + PG_RETURN_VOID(); + } + + /* + * Read a file, convert its encoding to the database encoding, then execute + * the SQL commands it contains. + */ + Datum + pg_convert_and_execute_sql_file(PG_FUNCTION_ARGS) + { + text *filename_t = PG_GETARG_TEXT_P(0); + char *filename = text_to_cstring(filename_t); + char *sql; + + if (!superuser()) + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + (errmsg("must be superuser to execute sql file")))); + + sql = pg_read_binary_file_internal(filename, 0, -1); + + if (!PG_ARGISNULL(1)) + { + Datum src_encoding_name = PG_GETARG_DATUM(1); + Datum dest_encoding_name = + DirectFunctionCall1(namein, + CStringGetDatum(GetDatabaseEncodingName())); + + sql = text_to_cstring( + DatumGetTextP( + DirectFunctionCall3(pg_convert, CStringGetDatum(sql), + src_encoding_name, dest_encoding_name))); + } + else + pg_verifymbstr(sql, strlen(sql), false); + + pg_execute_internal(filename, sql); + PG_RETURN_VOID(); + } + + /* + * Variant accepting a VARIADIC text parameter containing placeholder + * variables and values, one after the other (so the variadic array length + * must be even). + * + * The main use case of the replacement facility is for setting the + * extension's schema, using @pg_extschema@ variable and a user given + * schema. + * + * This could be implemented in a single function together with the previous + * pg_execute_sql_file, if only it was possible to fill in the + * proargdefaults pg_proc column from the backend code. + */ + Datum + pg_execute_sql_file_with_placeholders(PG_FUNCTION_ARGS) + { + text *filename_t = PG_GETARG_TEXT_P(0); + ArrayType *placeholders = PG_GETARG_ARRAYTYPE_P(2); + char *filename = text_to_cstring(filename_t); + char *sql; + + if (!superuser()) + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + (errmsg("must be superuser to execute sql file")))); + + sql = pg_read_binary_file_internal(filename, 0, -1); + + if (!PG_ARGISNULL(1)) + { + Datum src_encoding_name = PG_GETARG_DATUM(1); + Datum dest_encoding_name = + DirectFunctionCall1(namein, + CStringGetDatum(GetDatabaseEncodingName())); + + sql = text_to_cstring( + DatumGetTextP( + DirectFunctionCall3(pg_convert, CStringGetTextDatum(sql), + src_encoding_name, dest_encoding_name))); + } + else + pg_verifymbstr(sql, strlen(sql), false); + + pg_execute_internal(filename, + replace_placeholders_internal(sql, placeholders)); + PG_RETURN_VOID(); + } *** a/src/include/catalog/pg_proc.h --- b/src/include/catalog/pg_proc.h *************** *** 3399,3412 **** DESCR("reload configuration files"); DATA(insert OID = 2622 ( pg_rotate_logfile PGNSP PGUID 12 1 0 0 f f f t f v 0 0 16 "" _null_ _null_ _null_ _null_ pg_rotate_logfile _null_ _null_ _null_ )); DESCR("rotate log file"); ! DATA(insert OID = 2623 ( pg_stat_file PGNSP PGUID 12 1 0 0 f f f t f v 1 0 2249 "25" "{25,20,1184,1184,1184,1184,16}" "{i,o,o,o,o,o,o}" "{filename,size,access,modification,change,creation,isdir}" _null_ pg_stat_file _null_ _null_ _null_ )); DESCR("return file information"); ! DATA(insert OID = 2624 ( pg_read_file PGNSP PGUID 12 1 0 0 f f f t f v 3 0 25 "25 20 20" _null_ _null_ _null_ _null_ pg_read_file _null_ _null_ _null_ )); DESCR("read text from a file"); ! DATA(insert OID = 2625 ( pg_ls_dir PGNSP PGUID 12 1 1000 0 f f f t t v 1 0 25 "25" _null_ _null_ _null_ _null_ pg_ls_dir _null_ _null_ _null_ )); DESCR("list all files in a directory"); ! DATA(insert OID = 2626 ( pg_sleep PGNSP PGUID 12 1 0 0 f f f t f v 1 0 2278 "701" _null_ _null_ _null_ _null_ pg_sleep _null_ _null_ _null_ )); DESCR("sleep for the specified time in seconds"); DATA(insert OID = 2971 ( text PGNSP PGUID 12 1 0 0 f f f t f i 1 0 25 "16" _null_ _null_ _null_ _null_ booltext _null_ _null_ _null_ )); DESCR("convert boolean to text"); --- 3399,3426 ---- DATA(insert OID = 2622 ( pg_rotate_logfile PGNSP PGUID 12 1 0 0 f f f t f v 0 0 16 "" _null_ _null_ _null_ _null_ pg_rotate_logfile _null_ _null_ _null_ )); DESCR("rotate log file"); ! DATA(insert OID = 2623 ( pg_stat_file PGNSP PGUID 12 1 0 0 f f f t f v 1 0 2249 "25" "{25,20,1184,1184,1184,1184,16}" "{i,o,o,o,o,o,o}" "{filename,size,access,modification,change,creation,isdir}" _null_ pg_stat_file _null_ _null_ _null_ )); DESCR("return file information"); ! DATA(insert OID = 2624 ( pg_read_file PGNSP PGUID 12 1 0 0 f f f t f v 3 0 25 "25 20 20" _null_ _null_ _null_ _null_ pg_read_file _null_ _null_ _null_ )); DESCR("read text from a file"); ! DATA(insert OID = 2625 ( pg_ls_dir PGNSP PGUID 12 1 1000 0 f f f t t v 1 0 25 "25" _null_ _null_ _null_ _null_ pg_ls_dir _null_ _null_ _null_ )); DESCR("list all files in a directory"); ! DATA(insert OID = 2626 ( pg_sleep PGNSP PGUID 12 1 0 0 f f f t f v 1 0 2278 "701" _null_ _null_ _null_ _null_ pg_sleep _null_ _null_ _null_ )); DESCR("sleep for the specified time in seconds"); + DATA(insert OID = 3930 ( pg_read_binary_file PGNSP PGUID 12 1 0 0 f f f t f v 3 0 17 "25 20 20" _null_ _null_ _null_ _null_ pg_read_binary_file _null_ _null_ _null_ )); + DESCR("read text from a file"); + DATA(insert OID = 3931 ( replace_placeholders PGNSP PGUID 12 1 0 25 f f f t f v 2 0 25 "25 25" "{25,25}" "{i,v}" _null_ _null_ replace_placeholders _null_ _null_ _null_ )); + DESCR("replace placeholders in a text"); + DATA(insert OID = 3932 ( pg_execute_sql_string PGNSP PGUID 12 1 0 0 f f f t f v 1 0 2278 "25" _null_ _null_ _null_ _null_ pg_execute_sql_string _null_ _null_ _null_ )); + DESCR("execute queries read from a string"); + DATA(insert OID = 3933 ( pg_execute_sql_string PGNSP PGUID 12 1 0 25 f f f t f v 2 0 2278 "25 25" "{25,25}" "{i,v}" _null_ _null_ pg_execute_sql_string_with_placeholders _null_ _null_ _null_ )); + DESCR("execute queries read from a string"); + DATA(insert OID = 3927 ( pg_execute_sql_file PGNSP PGUID 12 1 0 0 f f f t f v 1 0 2278 "25" _null_ _null_ _null_ _null_ pg_execute_sql_file _null_ _null_ _null_ )); + DESCR("execute queries read from a file"); + DATA(insert OID = 3934 ( pg_execute_sql_file PGNSP PGUID 12 1 0 0 f f f t f v 2 0 2278 "25 19" _null_ _null_ _null_ _null_ pg_convert_and_execute_sql_file _null_ _null_ _null_ )); + DESCR("execute queries read from a file"); + DATA(insert OID = 3928 ( pg_execute_sql_file PGNSP PGUID 12 1 0 25 f f f t f v 3 0 2278 "25 19 25" "{25,19,25}" "{i,i,v}" _null_ _null_ pg_execute_sql_file_with_placeholders _null_ _null_ _null_ )); + DESCR("execute queries read from a file"); DATA(insert OID = 2971 ( text PGNSP PGUID 12 1 0 0 f f f t f i 1 0 25 "16" _null_ _null_ _null_ _null_ booltext _null_ _null_ _null_ )); DESCR("convert boolean to text"); *** a/src/include/utils/builtins.h --- b/src/include/utils/builtins.h *************** *** 443,448 **** extern Datum pg_relation_filepath(PG_FUNCTION_ARGS); --- 443,455 ---- extern Datum pg_stat_file(PG_FUNCTION_ARGS); extern Datum pg_read_file(PG_FUNCTION_ARGS); extern Datum pg_ls_dir(PG_FUNCTION_ARGS); + extern Datum pg_read_binary_file(PG_FUNCTION_ARGS); + extern Datum replace_placeholders(PG_FUNCTION_ARGS); + extern Datum pg_execute_sql_string(PG_FUNCTION_ARGS); + extern Datum pg_execute_sql_string_with_placeholders(PG_FUNCTION_ARGS); + extern Datum pg_execute_sql_file(PG_FUNCTION_ARGS); + extern Datum pg_convert_and_execute_sql_file(PG_FUNCTION_ARGS); + extern Datum pg_execute_sql_file_with_placeholders (PG_FUNCTION_ARGS); /* misc.c */ extern Datum current_database(PG_FUNCTION_ARGS);
-- Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org) To make changes to your subscription: http://www.postgresql.org/mailpref/pgsql-hackers