Andrew Chernow escribió: > Attached is the latest patch. It has addressed the requested changes > found here: > http://archives.postgresql.org/pgsql-patches/2008-05/msg00389.php > > Its a tarball because there are two new files, libpq-events.c and > libpq-events.h. The patch is in the tarball as well as attached to the > email.
I modified this patch slightly. I was about to try libpqtypes on it, but then I noticed that libpqtypes as published on pgfoundry is based on a very old version of this patch, so I punted. So, for now, the only guarantee is that it compiles with no warnings. However, the only change of any significance that I introduced was that a "name" is attached to every event proc, so that it can be reported in error messages, as reporting only %p seems very useless. (I also removed PQresultAlloc.) The API seems reasonable to me. There's one thing that seems a bit baroque, which is the PG_COPYRES_USE_ATTRS stuff in PQcopyResult. I think that flag introduces different enough behavior that it should be a routine of its own, say PQcopyResultAttrs. That way you would leave out the two extra params in PQcopyResult. Oh -- one last thing. I am not really sure about the flags to PQcopyResult. Should there really be flags to _remove_ behavior, instead of flags that add? i.e. instead of having "0" copy everything, and have to pass flags for things not to copy, wouldn't it be cleaner to have 0 copy only base stuff, and require flags to copy extra things? The main missing thing from this patch is SGML docs for the new libpq functions. -- Alvaro Herrera http://www.CommandPrompt.com/ The PostgreSQL Company - Command Prompt, Inc.
Index: src/interfaces/libpq/Makefile =================================================================== RCS file: /home/alvherre/Code/cvs/pgsql/src/interfaces/libpq/Makefile,v retrieving revision 1.166 diff -c -p -r1.166 Makefile *** src/interfaces/libpq/Makefile 16 Apr 2008 14:19:56 -0000 1.166 --- src/interfaces/libpq/Makefile 2 Sep 2008 21:54:47 -0000 *************** LIBS := $(LIBS:-lpgport=) *** 32,38 **** OBJS= fe-auth.o fe-connect.o fe-exec.o fe-misc.o fe-print.o fe-lobj.o \ fe-protocol2.o fe-protocol3.o pqexpbuffer.o pqsignal.o fe-secure.o \ ! md5.o ip.o wchar.o encnames.o noblock.o pgstrcasecmp.o thread.o \ $(filter crypt.o getaddrinfo.o inet_aton.o open.o snprintf.o strerror.o strlcpy.o win32error.o, $(LIBOBJS)) ifeq ($(PORTNAME), cygwin) --- 32,38 ---- OBJS= fe-auth.o fe-connect.o fe-exec.o fe-misc.o fe-print.o fe-lobj.o \ fe-protocol2.o fe-protocol3.o pqexpbuffer.o pqsignal.o fe-secure.o \ ! md5.o ip.o wchar.o encnames.o noblock.o pgstrcasecmp.o thread.o libpq-events.o \ $(filter crypt.o getaddrinfo.o inet_aton.o open.o snprintf.o strerror.o strlcpy.o win32error.o, $(LIBOBJS)) ifeq ($(PORTNAME), cygwin) *************** $(top_builddir)/src/port/pg_config_paths *** 106,111 **** --- 106,112 ---- install: all installdirs install-lib $(INSTALL_DATA) $(srcdir)/libpq-fe.h '$(DESTDIR)$(includedir)' + $(INSTALL_DATA) $(srcdir)/libpq-events.h '$(DESTDIR)$(includedir)' $(INSTALL_DATA) $(srcdir)/libpq-int.h '$(DESTDIR)$(includedir_internal)' $(INSTALL_DATA) $(srcdir)/pqexpbuffer.h '$(DESTDIR)$(includedir_internal)' $(INSTALL_DATA) $(srcdir)/pg_service.conf.sample '$(DESTDIR)$(datadir)/pg_service.conf.sample' *************** installdirs: installdirs-lib *** 114,120 **** $(mkinstalldirs) '$(DESTDIR)$(includedir)' '$(DESTDIR)$(includedir_internal)' uninstall: uninstall-lib ! rm -f '$(DESTDIR)$(includedir)/libpq-fe.h' '$(DESTDIR)$(includedir_internal)/libpq-int.h' '$(DESTDIR)$(includedir_internal)/pqexpbuffer.h' '$(DESTDIR)$(datadir)/pg_service.conf.sample' clean distclean: clean-lib rm -f $(OBJS) pg_config_paths.h crypt.c getaddrinfo.c inet_aton.c noblock.c open.c pgstrcasecmp.c snprintf.c strerror.c strlcpy.c thread.c md5.c ip.c encnames.c wchar.c win32error.c pgsleep.c pthread.h libpq.rc --- 115,121 ---- $(mkinstalldirs) '$(DESTDIR)$(includedir)' '$(DESTDIR)$(includedir_internal)' uninstall: uninstall-lib ! rm -f '$(DESTDIR)$(includedir)/libpq-fe.h' '$(DESTDIR)$(includedir)/libpq-events.h' '$(DESTDIR)$(includedir_internal)/libpq-int.h' '$(DESTDIR)$(includedir_internal)/pqexpbuffer.h' '$(DESTDIR)$(datadir)/pg_service.conf.sample' clean distclean: clean-lib rm -f $(OBJS) pg_config_paths.h crypt.c getaddrinfo.c inet_aton.c noblock.c open.c pgstrcasecmp.c snprintf.c strerror.c strlcpy.c thread.c md5.c ip.c encnames.c wchar.c win32error.c pgsleep.c pthread.h libpq.rc Index: src/interfaces/libpq/exports.txt =================================================================== RCS file: /home/alvherre/Code/cvs/pgsql/src/interfaces/libpq/exports.txt,v retrieving revision 1.19 diff -c -p -r1.19 exports.txt *** src/interfaces/libpq/exports.txt 19 Mar 2008 00:39:33 -0000 1.19 --- src/interfaces/libpq/exports.txt 2 Sep 2008 22:41:17 -0000 *************** PQconnectionUsedPassword 138 *** 141,143 **** --- 141,152 ---- pg_valid_server_encoding_id 139 PQconnectionNeedsPassword 140 lo_import_with_oid 141 + PQcopyResult 142 + PQsetvalue 143 + PQregisterEventProc 144 + PQinstanceData 145 + PQsetInstanceData 146 + PQresultInstanceData 147 + PQresultSetInstanceData 148 + PQpassThroughData 149 + PQresultPassThroughData 150 Index: src/interfaces/libpq/fe-connect.c =================================================================== RCS file: /home/alvherre/Code/cvs/pgsql/src/interfaces/libpq/fe-connect.c,v retrieving revision 1.359 diff -c -p -r1.359 fe-connect.c *** src/interfaces/libpq/fe-connect.c 29 May 2008 22:02:44 -0000 1.359 --- src/interfaces/libpq/fe-connect.c 2 Sep 2008 23:22:02 -0000 *************** makeEmptyPGconn(void) *** 1974,1979 **** --- 1974,1998 ---- static void freePGconn(PGconn *conn) { + int i; + PGEventConnDestroy evt; + + /* Let the event procs cleanup their state data */ + for (i = 0; i < conn->nEvents; i++) + { + evt.conn = conn; + (void)conn->events[i].proc(PGEVT_CONNDESTROY, &evt); + free(conn->events[i].name); + } + + /* free the PGEvent array */ + if (conn->events) + { + free(conn->events); + conn->events = NULL; + conn->nEvents = conn->eventArrSize = 0; + } + if (conn->pghost) free(conn->pghost); if (conn->pghostaddr) *************** PQreset(PGconn *conn) *** 2155,2162 **** { closePGconn(conn); ! if (connectDBStart(conn)) ! (void) connectDBComplete(conn); } } --- 2174,2198 ---- { closePGconn(conn); ! if (connectDBStart(conn) && connectDBComplete(conn)) ! { ! int i; ! PGEventConnReset evt; ! ! for (i = 0; i < conn->nEvents; i++) ! { ! evt.conn = conn; ! ! if(!conn->events[i].proc(PGEVT_CONNRESET, &evt)) ! { ! conn->status = CONNECTION_BAD; ! printfPQExpBuffer(&conn->errorMessage, ! libpq_gettext("PGEvent \"%s\" failed during PGEVT_CONNRESET event\n"), ! conn->events[i].name); ! break; ! } ! } ! } } } *************** PostgresPollingStatusType *** 2190,2196 **** PQresetPoll(PGconn *conn) { if (conn) ! return PQconnectPoll(conn); return PGRES_POLLING_FAILED; } --- 2226,2256 ---- PQresetPoll(PGconn *conn) { if (conn) ! { ! PostgresPollingStatusType status = PQconnectPoll(conn); ! ! if (status == PGRES_POLLING_OK) ! { ! int i; ! PGEventConnReset evt; ! ! for (i = 0; i < conn->nEvents; i++) ! { ! evt.conn = conn; ! ! if (!conn->events[i].proc(PGEVT_CONNRESET, &evt)) ! { ! conn->status = CONNECTION_BAD; ! printfPQExpBuffer(&conn->errorMessage, ! libpq_gettext("PGEvent \"%s\" failed during PGEVT_CONNRESET event\n"), ! conn->events[i].name); ! return PGRES_POLLING_FAILED; ! } ! } ! } ! ! return status; ! } return PGRES_POLLING_FAILED; } Index: src/interfaces/libpq/fe-exec.c =================================================================== RCS file: /home/alvherre/Code/cvs/pgsql/src/interfaces/libpq/fe-exec.c,v retrieving revision 1.196 diff -c -p -r1.196 fe-exec.c *** src/interfaces/libpq/fe-exec.c 23 Jun 2008 21:10:49 -0000 1.196 --- src/interfaces/libpq/fe-exec.c 2 Sep 2008 23:21:12 -0000 *************** static bool PQexecStart(PGconn *conn); *** 63,68 **** --- 63,69 ---- static PGresult *PQexecFinish(PGconn *conn); static int PQsendDescribe(PGconn *conn, char desc_type, const char *desc_target); + static int check_field_number(const PGresult *res, int field_num); /* ---------------- *************** static int PQsendDescribe(PGconn *conn, *** 125,134 **** /* * PQmakeEmptyPGresult * returns a newly allocated, initialized PGresult with given status. * If conn is not NULL and status indicates an error, the conn's ! * errorMessage is copied. * * Note this is exported --- you wouldn't think an application would need * to build its own PGresults, but this has proven useful in both libpgtcl --- 126,163 ---- /* + * Creates a copy of a PGEvent array. The event proc name is not allocated + * separately. + * + * Note that it doesn't duplicate the event instance data; it is left NULL. + */ + static PGEvent * + dupEvents(PGEvent *events, int count) + { + int i; + PGEvent *newEvents; + + if (!events || count <= 0) + return NULL; + + newEvents = (PGEvent *) malloc(count * sizeof(PGEvent)); + if (!newEvents) + return NULL; + + memcpy(newEvents, events, count * sizeof(PGEvent)); + + /* NULL out the data pointer */ + for (i = 0; i < count; i++) + newEvents[i].data = NULL; + + return newEvents; + } + + /* * PQmakeEmptyPGresult * returns a newly allocated, initialized PGresult with given status. * If conn is not NULL and status indicates an error, the conn's ! * errorMessage and events state are copied. * * Note this is exported --- you wouldn't think an application would need * to build its own PGresults, but this has proven useful in both libpgtcl *************** PQmakeEmptyPGresult(PGconn *conn, ExecSt *** 160,165 **** --- 189,196 ---- result->curBlock = NULL; result->curOffset = 0; result->spaceLeft = 0; + result->nEvents = 0; + result->events = NULL; if (conn) { *************** PQmakeEmptyPGresult(PGconn *conn, ExecSt *** 167,172 **** --- 198,216 ---- result->noticeHooks = conn->noticeHooks; result->client_encoding = conn->client_encoding; + /* copy events from connection */ + if (conn->nEvents > 0) + { + result->events = dupEvents(conn->events, conn->nEvents); + if (!result->events) + { + PQclear(result); + return NULL; + } + + result->nEvents = conn->nEvents; + } + /* consider copying conn's errorMessage */ switch (status) { *************** PQmakeEmptyPGresult(PGconn *conn, ExecSt *** 196,201 **** --- 240,477 ---- } /* + * PQcopyResult + * Returns a deep copy of the provided 'src' PGresult, which cannot be NULL. + * The 'options' argument controls which portions of the result will or will + * NOT be copied. If this value is 0, the entire result is deep copied. + * The created result is always put into the PGRES_TUPLES_OK status. The + * source result error message is not copied, although cmdStatus is. + * + * Options: + * PG_COPYRES_NO_TUPLES - Do not copy the tuples. This option is + * automatically enabled when PG_COPYRES_USE_ATTRS is set. + * + * PG_COPYRES_USE_ATTRS - Indicates that the 'numAttributes' and 'attDescs' + * arguments should be used as the fields for the created result, rather + * than copying them from the source result. When this option is set, + * the tuples will NOT be copied, behaving identically to setting the + * PG_COPYRES_NO_TUPLES option. One must use PQsetvalue to manually + * add tuples to the returned result. NOTE: numAttributes and attDescs + * arguments are ignored unless this option is set! + * + * PG_COPYRES_NO_EVENTS - Indicates that the source result's + * events should NOT be copied to the created result. + * + * PG_COPYRES_NO_NOTICEHOOKS - Indicates that the source result's + * NoticeHooks should NOT be copied to the created result. + */ + PGresult * + PQcopyResult(const PGresult *src, int numAttributes, + PGresAttDesc *attDescs, int options) + { + int i; + PGresult *dest; + PGEventResultCopy evt; + + if (!src) + return NULL; + + /* Automatically turn on no_tuples since use_attrs is set. It makes + * no sense to copy tuples into an unknown set of columns. + */ + if (options & PG_COPYRES_USE_ATTRS) + options |= PG_COPYRES_NO_TUPLES; + + /* If use_attrs is set, verify attr arguments. */ + if ((options & PG_COPYRES_USE_ATTRS) && (numAttributes <= 0 || !attDescs)) + return NULL; + + dest = PQmakeEmptyPGresult((PGconn *)NULL, PGRES_TUPLES_OK); + if (!dest) + return NULL; + + /* always copy these over. Is cmdStatus useful here? */ + dest->client_encoding = src->client_encoding; + strcpy(dest->cmdStatus, src->cmdStatus); + + /* Wants to copy notice hooks */ + if (!(options & PG_COPYRES_NO_NOTICEHOOKS)) + dest->noticeHooks = src->noticeHooks; + + /* Copy from src result when not supplying attrs */ + if (!(options & PG_COPYRES_USE_ATTRS) && src->numAttributes > 0) + { + numAttributes = src->numAttributes; + attDescs = src->attDescs; + } + + /* copy attrs */ + if (numAttributes > 0) + { + dest->attDescs = (PGresAttDesc *) pqResultAlloc(dest, + numAttributes * sizeof(PGresAttDesc), TRUE); + + if (!dest->attDescs) + { + PQclear(dest); + return NULL; + } + + dest->numAttributes = numAttributes; + memcpy(dest->attDescs, attDescs, numAttributes * sizeof(PGresAttDesc)); + + /* resultalloc the attribute names. The above memcpy has the attr + * names pointing at the source result's private memory (or at the + * callers provided attDescs memory). + */ + dest->binary = 1; + for (i = 0; i < numAttributes; i++) + { + if (attDescs[i].name) + dest->attDescs[i].name = pqResultStrdup(dest, attDescs[i].name); + else + dest->attDescs[i].name = dest->null_field; + + if (!dest->attDescs[i].name) + { + PQclear(dest); + return NULL; + } + + /* Although deprecated, because results can have text+binary columns, + * its easy enough to deduce so set it for completeness. + */ + if (dest->attDescs[i].format == 0) + dest->binary = 0; + } + } + + /* Wants to copy result tuples: use PQsetvalue(). */ + if (!(options & PG_COPYRES_NO_TUPLES) && src->ntups > 0) + { + int tup, field; + for (tup = 0; tup < src->ntups; tup++) + for (field = 0; field < src->numAttributes; field++) + PQsetvalue(dest, tup, field, src->tuples[tup][field].value, + src->tuples[tup][field].len); + } + + /* Wants to copy PGEvents. */ + if (!(options & PG_COPYRES_NO_EVENTS) && src->nEvents > 0) + { + dest->events = dupEvents(src->events, src->nEvents); + if (!dest->events) + { + PQclear(dest); + return NULL; + } + + dest->nEvents = src->nEvents; + } + + /* Trigger PGEVT_RESULTCOPY event */ + for (i = 0; i < dest->nEvents; i++) + { + evt.src = src; + evt.dest = dest; + if (!dest->events[i].proc(PGEVT_RESULTCOPY, &evt)) + { + PQclear(dest); + return NULL; + } + } + + return dest; + } + + int + PQsetvalue(PGresult *res, int tup_num, int field_num, char *value, int len) + { + PGresAttValue *attval; + + if (!check_field_number(res, field_num)) + return FALSE; + + /* Invalid tup_num, must be <= ntups */ + if (tup_num > res->ntups) + return FALSE; + + /* need to grow the tuple table */ + if (res->ntups >= res->tupArrSize) + { + int n = res->tupArrSize ? res->tupArrSize * 2 : 128; + PGresAttValue **tups; + + if (res->tuples) + tups = realloc(res->tuples, n * sizeof(PGresAttValue *)); + else + tups = malloc(n * sizeof(PGresAttValue *)); + + if (!tups) + return FALSE; + + memset(tups + res->tupArrSize, 0, + (n - res->tupArrSize) * sizeof(PGresAttValue *)); + res->tuples = tups; + res->tupArrSize = n; + } + + /* need to allocate a new tuple */ + if (tup_num == res->ntups && !res->tuples[tup_num]) + { + int i; + PGresAttValue *tup; + + tup = (PGresAttValue *) pqResultAlloc(res, res->numAttributes * sizeof(PGresAttValue), TRUE); + + if (!tup) + return FALSE; + + /* initialize each column to NULL */ + for (i = 0; i < res->numAttributes; i++) + { + tup[i].len = NULL_LEN; + tup[i].value = res->null_field; + } + + res->tuples[tup_num] = tup; + res->ntups++; + } + + attval = &res->tuples[tup_num][field_num]; + + /* On top of NULL_LEN, treat a NULL value as a NULL field */ + if (len == NULL_LEN || value == NULL) + { + attval->len = NULL_LEN; + attval->value = res->null_field; + } + else + { + if (len < 0) + len = 0; + + if (len == 0) + { + attval->len = 0; + attval->value = res->null_field; + } + else + { + attval->value = (char *) pqResultAlloc(res, len + 1, TRUE); + if (!attval->value) + return FALSE; + + attval->len = len; + memcpy(attval->value, value, len); + attval->value[len] = '\0'; + } + } + + return TRUE; + } + + /* * pqResultAlloc - * Allocate subsidiary storage for a PGresult. * *************** pqCatenateResultError(PGresult *res, con *** 352,362 **** --- 628,655 ---- void PQclear(PGresult *res) { + int i; PGresult_data *block; + PGEventResultDestroy evt; if (!res) return; + /* invoke each event's destroy routine */ + for (i = 0; i < res->nEvents; i++) + { + evt.result = res; + (void) res->events[i].proc(PGEVT_RESULTDESTROY, &evt); + } + + /* free the event array */ + if (res->events) + { + free(res->events); + res->events = NULL; + res->nEvents = 0; + } + /* Free all the subsidiary blocks */ while ((block = res->curBlock) != NULL) { *************** PQgetResult(PGconn *conn) *** 1270,1275 **** --- 1563,1594 ---- break; } + if (res && res->nEvents > 0 && + (res->resultStatus == PGRES_COMMAND_OK || + res->resultStatus == PGRES_TUPLES_OK || + res->resultStatus == PGRES_EMPTY_QUERY)) + { + int i; + PGEventResultCreate evt; + + for (i = 0; i < res->nEvents; i++) + { + evt.conn = conn; + evt.result = res; + + if (!res->events[i].proc(PGEVT_RESULTCREATE, &evt)) + { + char msg[256]; + sprintf(msg, + "PGEvent \"%s\" failed during PGEVT_RESULTCREATE event", + res->events[i].name); + pqSetResultError(res, msg); + res->resultStatus = PGRES_FATAL_ERROR; + break; + } + } + } + return res; } Index: src/interfaces/libpq/libpq-fe.h =================================================================== RCS file: /home/alvherre/Code/cvs/pgsql/src/interfaces/libpq/libpq-fe.h,v retrieving revision 1.142 diff -c -p -r1.142 libpq-fe.h *** src/interfaces/libpq/libpq-fe.h 19 Mar 2008 00:39:33 -0000 1.142 --- src/interfaces/libpq/libpq-fe.h 2 Sep 2008 23:23:48 -0000 *************** extern "C" *** 28,33 **** --- 28,42 ---- */ #include "postgres_ext.h" + /* ----------------------- + * Options for PQcopyResult + */ + + #define PG_COPYRES_NO_TUPLES 0x01 + #define PG_COPYRES_USE_ATTRS 0x02 + #define PG_COPYRES_NO_EVENTS 0x04 + #define PG_COPYRES_NO_NOTICEHOOKS 0x08 + /* Application-visible enum types */ typedef enum *************** typedef struct *** 193,198 **** --- 202,222 ---- } PQArgBlock; /* ---------------- + * PGresAttDesc -- Data about a single attribute (column) of a query result + * ---------------- + */ + typedef struct pgresAttDesc + { + char *name; /* column name */ + Oid tableid; /* source table, if known */ + int columnid; /* source column, if known */ + int format; /* format code for value (text/binary) */ + Oid typid; /* type id */ + int typlen; /* type size */ + int atttypmod; /* type-specific modifier info */ + } PGresAttDesc; + + /* ---------------- * Exported functions of libpq * ---------------- */ *************** extern void PQfreemem(void *ptr); *** 437,442 **** --- 461,481 ---- */ extern PGresult *PQmakeEmptyPGresult(PGconn *conn, ExecStatusType status); + extern PGresult * + PQcopyResult(const PGresult *src, int numAttributes, + PGresAttDesc *attDescs, int options); + + /* + * Sets the value for a tuple field. The tup_num must be less than or + * equal to PQntuples(res). This function will generate tuples as needed. + * A new tuple is generated when tup_num equals PQntuples(res) and there + * are no fields defined for that tuple. + * + * Returns a non-zero value for success and zero for failure. + */ + extern int + PQsetvalue(PGresult *res, int tup_num, int field_num, char *value, int len); + /* Quoting strings before inclusion in queries. */ extern size_t PQescapeStringConn(PGconn *conn, Index: src/interfaces/libpq/libpq-int.h =================================================================== RCS file: /home/alvherre/Code/cvs/pgsql/src/interfaces/libpq/libpq-int.h,v retrieving revision 1.131 diff -c -p -r1.131 libpq-int.h *** src/interfaces/libpq/libpq-int.h 29 May 2008 22:02:44 -0000 1.131 --- src/interfaces/libpq/libpq-int.h 2 Sep 2008 23:24:39 -0000 *************** *** 22,27 **** --- 22,28 ---- /* We assume libpq-fe.h has already been included. */ #include "postgres_fe.h" + #include "libpq-events.h" #include <time.h> #include <sys/types.h> *************** union pgresult_data *** 100,118 **** char space[1]; /* dummy for accessing block as bytes */ }; - /* Data about a single attribute (column) of a query result */ - - typedef struct pgresAttDesc - { - char *name; /* column name */ - Oid tableid; /* source table, if known */ - int columnid; /* source column, if known */ - int format; /* format code for value (text/binary) */ - Oid typid; /* type id */ - int typlen; /* type size */ - int atttypmod; /* type-specific modifier info */ - } PGresAttDesc; - /* Data about a single parameter of a prepared statement */ typedef struct pgresParamDesc { --- 101,106 ---- *************** typedef struct *** 162,167 **** --- 150,164 ---- void *noticeProcArg; } PGNoticeHooks; + /* Fields needed for PGEvent processing */ + typedef struct + { + char *name; /* for error messages */ + void *passThrough; /* pointer supplied by user */ + void *data; /* state data, optionally generated by event proc */ + PGEventProc proc; /* the function to call on events */ + } PGEvent; + struct pg_result { int ntups; *************** struct pg_result *** 184,189 **** --- 181,190 ---- PGNoticeHooks noticeHooks; int client_encoding; /* encoding id */ + /* registered events, copied from conn */ + int nEvents; + PGEvent *events; + /* * Error information (all NULL if not an error result). errMsg is the * "overall" error message returned by PQresultErrorMessage. If we have *************** struct pg_conn *** 303,308 **** --- 304,314 ---- /* Callback procedures for notice message processing */ PGNoticeHooks noticeHooks; + /* registered events via PQregisterEventProc */ + int nEvents; + int eventArrSize; + PGEvent *events; + /* Status indicators */ ConnStatusType status; PGAsyncStatusType asyncStatus;
-- Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org) To make changes to your subscription: http://www.postgresql.org/mailpref/pgsql-hackers