Re: [Fwd: C&SRF API patch (was Re: [HACKERS] revised sample - Mailing list pgsql-patches
From | Bruce Momjian |
---|---|
Subject | Re: [Fwd: C&SRF API patch (was Re: [HACKERS] revised sample |
Date | |
Msg-id | 200206172144.g5HLiVq15695@candle.pha.pa.us Whole thread Raw |
List | pgsql-patches |
Your patch has been added to the PostgreSQL unapplied patches list at: http://candle.pha.pa.us/cgi-bin/pgpatches I will try to apply it within the next 48 hours. --------------------------------------------------------------------------- Joe Conway wrote: > Tom Lane wrote: > > Joe Conway <mail@joeconway.com> writes: > >>Is the approach in my patch still too ugly to allow a builtin SRF (set > >>the function return type to 0 in pg_proc.h, create a view and fix the > >>pg_proc entry during initdb)? > > > > Too ugly for my taste anyway ... > > OK. > > Here is a patch for Composite and Set returning function support. I made > two small changes to the API since last patch, which hopefully completes > the decoupling of composite function support from SRF specific support. > If there are no (further ;-)) objections, please apply. I'll send > another post with a patch for contrib/showguc. > > Thanks, > > Joe > > Index: src/backend/access/common/tupdesc.c > =================================================================== > RCS file: /opt/src/cvs/pgsql/src/backend/access/common/tupdesc.c,v > retrieving revision 1.78 > diff -c -r1.78 tupdesc.c > *** src/backend/access/common/tupdesc.c 29 Mar 2002 19:05:59 -0000 1.78 > --- src/backend/access/common/tupdesc.c 9 Jun 2002 21:00:31 -0000 > *************** > *** 19,24 **** > --- 19,27 ---- > > #include "postgres.h" > > + #include "funcapi.h" > + #include "access/heapam.h" > + #include "catalog/namespace.h" > #include "catalog/pg_type.h" > #include "nodes/parsenodes.h" > #include "parser/parse_type.h" > *************** > *** 548,551 **** > --- 551,660 ---- > desc->constr = NULL; > } > return desc; > + } > + > + > + /* > + * RelationNameGetTupleDesc > + * > + * Given a (possibly qualified) relation name, build a TupleDesc. > + */ > + TupleDesc > + RelationNameGetTupleDesc(char *relname) > + { > + RangeVar *relvar; > + Relation rel; > + TupleDesc tupdesc; > + List *relname_list; > + > + /* Open relation and get the tuple description */ > + relname_list = stringToQualifiedNameList(relname, "RelationNameGetTupleDesc"); > + relvar = makeRangeVarFromNameList(relname_list); > + rel = heap_openrv(relvar, AccessShareLock); > + tupdesc = CreateTupleDescCopy(RelationGetDescr(rel)); > + relation_close(rel, AccessShareLock); > + > + return tupdesc; > + } > + > + /* > + * TypeGetTupleDesc > + * > + * Given a type Oid, build a TupleDesc. > + * > + * If the type is composite, *and* a colaliases List is provided, *and* > + * the List is of natts length, use the aliases instead of the relation > + * attnames. > + * > + * If the type is a base type, a single item alias List is required. > + */ > + TupleDesc > + TypeGetTupleDesc(Oid typeoid, List *colaliases) > + { > + Oid relid = typeidTypeRelid(typeoid); > + TupleDesc tupdesc; > + > + /* > + * Build a suitable tupledesc representing the output rows > + */ > + if (OidIsValid(relid)) > + { > + /* Composite data type, i.e. a table's row type */ > + Relation rel; > + int natts; > + > + rel = relation_open(relid, AccessShareLock); > + tupdesc = CreateTupleDescCopy(RelationGetDescr(rel)); > + natts = tupdesc->natts; > + relation_close(rel, AccessShareLock); > + > + /* check to see if we've given column aliases */ > + if(colaliases != NIL) > + { > + char *label; > + int varattno; > + > + /* does the List length match the number of attributes */ > + if (length(colaliases) != natts) > + elog(ERROR, "TypeGetTupleDesc: number of aliases does not match number of attributes"); > + > + /* OK, use the aliases instead */ > + for (varattno = 0; varattno < natts; varattno++) > + { > + label = strVal(nth(varattno, colaliases)); > + > + if (label != NULL) > + namestrcpy(&(tupdesc->attrs[varattno]->attname), label); > + else > + MemSet(NameStr(tupdesc->attrs[varattno]->attname), 0, NAMEDATALEN); > + } > + } > + } > + else > + { > + /* Must be a base data type, i.e. scalar */ > + char *attname; > + > + /* the alias List is required for base types */ > + if (colaliases == NIL) > + elog(ERROR, "TypeGetTupleDesc: no column alias was provided"); > + > + /* the alias List length must be 1 */ > + if (length(colaliases) != 1) > + elog(ERROR, "TypeGetTupleDesc: number of aliases does not match number of attributes"); > + > + /* OK, get the column alias */ > + attname = strVal(lfirst(colaliases)); > + > + tupdesc = CreateTemplateTupleDesc(1); > + TupleDescInitEntry(tupdesc, > + (AttrNumber) 1, > + attname, > + typeoid, > + -1, > + 0, > + false); > + } > + > + return tupdesc; > } > Index: src/backend/executor/execTuples.c > =================================================================== > RCS file: /opt/src/cvs/pgsql/src/backend/executor/execTuples.c,v > retrieving revision 1.51 > diff -c -r1.51 execTuples.c > *** src/backend/executor/execTuples.c 21 Mar 2002 06:21:04 -0000 1.51 > --- src/backend/executor/execTuples.c 9 Jun 2002 21:00:31 -0000 > *************** > *** 107,117 **** > */ > #include "postgres.h" > > #include "access/heapam.h" > #include "catalog/pg_type.h" > #include "executor/executor.h" > > - > /* ---------------------------------------------------------------- > * tuple table create/delete functions > * ---------------------------------------------------------------- > --- 107,117 ---- > */ > #include "postgres.h" > > + #include "funcapi.h" > #include "access/heapam.h" > #include "catalog/pg_type.h" > #include "executor/executor.h" > > /* ---------------------------------------------------------------- > * tuple table create/delete functions > * ---------------------------------------------------------------- > *************** > *** 673,675 **** > --- 673,795 ---- > > return typeInfo; > } > + > + /* > + * TupleDescGetSlot - Initialize a slot based on the supplied > + * tupledesc > + */ > + TupleTableSlot * > + TupleDescGetSlot(TupleDesc tupdesc) > + { > + TupleTableSlot *slot; > + > + /* Make a standalone slot */ > + slot = MakeTupleTableSlot(); > + > + /* Bind the tuple description to the slot */ > + ExecSetSlotDescriptor(slot, tupdesc, true); > + > + /* Return the slot */ > + return slot; > + } > + > + /* > + * TupleDescGetAttInMetadata - Get a pointer to AttInMetadata based on the > + * supplied TupleDesc. AttInMetadata can be used in conjunction with C strings > + * to produce a properly formed tuple. > + */ > + AttInMetadata * > + TupleDescGetAttInMetadata(TupleDesc tupdesc) > + { > + int natts; > + int i; > + Oid atttypeid; > + Oid attinfuncid; > + Oid attelem; > + FmgrInfo *attinfuncinfo; > + Oid *attelems; > + int4 *atttypmods; > + AttInMetadata *attinmeta; > + > + attinmeta = (AttInMetadata *) palloc(sizeof(AttInMetadata)); > + natts = tupdesc->natts; > + > + /* > + * Gather info needed later to call the "in" function for each attribute > + */ > + attinfuncinfo = (FmgrInfo *) palloc(natts * sizeof(FmgrInfo)); > + attelems = (Oid *) palloc(natts * sizeof(Oid)); > + atttypmods = (int4 *) palloc(natts * sizeof(int4)); > + > + for (i = 0; i < natts; i++) > + { > + atttypeid = tupdesc->attrs[i]->atttypid; > + get_type_metadata(atttypeid, &attinfuncid, &attelem); > + > + fmgr_info(attinfuncid, &attinfuncinfo[i]); > + attelems[i] = attelem; > + atttypmods[i] = tupdesc->attrs[i]->atttypmod; > + } > + attinmeta->tupdesc = tupdesc; > + attinmeta->attinfuncs = attinfuncinfo; > + attinmeta->attelems = attelems; > + attinmeta->atttypmods = atttypmods; > + > + return attinmeta; > + } > + > + /* > + * BuildTupleFromCStrings - build a HeapTuple given user data in C string form. > + * values is an array of C strings, one for each attribute of the return tuple. > + */ > + HeapTuple > + BuildTupleFromCStrings(AttInMetadata *attinmeta, char **values) > + { > + TupleDesc tupdesc; > + int natts; > + HeapTuple tuple; > + char *nulls; > + int i; > + Datum *dvalues; > + FmgrInfo attinfuncinfo; > + Oid attelem; > + int4 atttypmod; > + > + tupdesc = attinmeta->tupdesc; > + natts = tupdesc->natts; > + > + dvalues = (Datum *) palloc(natts * sizeof(Datum)); > + > + /* Call the "in" function for each attribute */ > + for (i = 0; i < natts; i++) > + { > + if (values[i] != NULL) > + { > + attinfuncinfo = attinmeta->attinfuncs[i]; > + attelem = attinmeta->attelems[i]; > + atttypmod = attinmeta->atttypmods[i]; > + > + dvalues[i] = FunctionCall3(&attinfuncinfo, CStringGetDatum(values[i]), > + ObjectIdGetDatum(attelem), > + Int32GetDatum(atttypmod)); > + } > + else > + dvalues[i] = PointerGetDatum(NULL); > + } > + > + /* > + * Form a tuple > + */ > + nulls = (char *) palloc(natts * sizeof(char)); > + for (i = 0; i < natts; i++) > + { > + if (DatumGetPointer(dvalues[i]) != NULL) > + nulls[i] = ' '; > + else > + nulls[i] = 'n'; > + } > + tuple = heap_formtuple(tupdesc, dvalues, nulls); > + > + return tuple; > + } > + > Index: src/backend/utils/adt/regproc.c > =================================================================== > RCS file: /opt/src/cvs/pgsql/src/backend/utils/adt/regproc.c,v > retrieving revision 1.68 > diff -c -r1.68 regproc.c > *** src/backend/utils/adt/regproc.c 11 May 2002 00:24:16 -0000 1.68 > --- src/backend/utils/adt/regproc.c 9 Jun 2002 21:00:31 -0000 > *************** > *** 37,44 **** > #include "utils/lsyscache.h" > #include "utils/syscache.h" > > - > - static List *stringToQualifiedNameList(const char *string, const char *caller); > static void parseNameAndArgTypes(const char *string, const char *caller, > const char *type0_spelling, > List **names, int *nargs, Oid *argtypes); > --- 37,42 ---- > *************** > *** 960,973 **** > } > > > - /***************************************************************************** > - * SUPPORT ROUTINES * > - *****************************************************************************/ > - > /* > * Given a C string, parse it into a qualified-name list. > */ > ! static List * > stringToQualifiedNameList(const char *string, const char *caller) > { > char *rawname; > --- 958,967 ---- > } > > > /* > * Given a C string, parse it into a qualified-name list. > */ > ! List * > stringToQualifiedNameList(const char *string, const char *caller) > { > char *rawname; > *************** > *** 996,1001 **** > --- 990,999 ---- > > return result; > } > + > + /***************************************************************************** > + * SUPPORT ROUTINES * > + *****************************************************************************/ > > /* > * Given a C string, parse it into a qualified function or operator name > Index: src/backend/utils/fmgr/Makefile > =================================================================== > RCS file: /opt/src/cvs/pgsql/src/backend/utils/fmgr/Makefile,v > retrieving revision 1.12 > diff -c -r1.12 Makefile > *** src/backend/utils/fmgr/Makefile 16 Sep 2001 16:11:11 -0000 1.12 > --- src/backend/utils/fmgr/Makefile 9 Jun 2002 21:00:31 -0000 > *************** > *** 12,18 **** > top_builddir = ../../../.. > include $(top_builddir)/src/Makefile.global > > ! OBJS = dfmgr.o fmgr.o > > override CPPFLAGS += -DPKGLIBDIR=\"$(pkglibdir)\" -DDLSUFFIX=\"$(DLSUFFIX)\" > > --- 12,18 ---- > top_builddir = ../../../.. > include $(top_builddir)/src/Makefile.global > > ! OBJS = dfmgr.o fmgr.o funcapi.o > > override CPPFLAGS += -DPKGLIBDIR=\"$(pkglibdir)\" -DDLSUFFIX=\"$(DLSUFFIX)\" > > Index: src/backend/utils/fmgr/funcapi.c > =================================================================== > RCS file: src/backend/utils/fmgr/funcapi.c > diff -N src/backend/utils/fmgr/funcapi.c > *** /dev/null 1 Jan 1970 00:00:00 -0000 > --- src/backend/utils/fmgr/funcapi.c 9 Jun 2002 22:55:47 -0000 > *************** > *** 0 **** > --- 1,122 ---- > + /*------------------------------------------------------------------------- > + * > + * funcapi.c > + * Utility and convenience functions for fmgr functions that return > + * sets and/or composite types. > + * > + * Copyright (c) 2002, PostgreSQL Global Development Group > + * > + *------------------------------------------------------------------------- > + */ > + > + #include "funcapi.h" > + #include "catalog/pg_type.h" > + #include "utils/syscache.h" > + > + /* > + * init_MultiFuncCall > + * Create an empty FuncCallContext data structure > + * and do some other basic Multi-function call setup > + * and error checking > + */ > + FuncCallContext * > + init_MultiFuncCall(PG_FUNCTION_ARGS) > + { > + FuncCallContext *retval; > + > + /* > + * Bail if we're called in the wrong context > + */ > + if (fcinfo->resultinfo == NULL || !IsA(fcinfo->resultinfo, ReturnSetInfo)) > + elog(ERROR, "function called in context that does not accept a set result"); > + > + if (fcinfo->flinfo->fn_extra == NULL) > + { > + /* > + * First call > + */ > + MemoryContext oldcontext; > + > + /* switch to the appropriate memory context */ > + oldcontext = MemoryContextSwitchTo(fcinfo->flinfo->fn_mcxt); > + > + /* > + * allocate space and zero it > + */ > + retval = (FuncCallContext *) palloc(sizeof(FuncCallContext)); > + MemSet(retval, 0, sizeof(FuncCallContext)); > + > + /* > + * initialize the elements > + */ > + retval->call_cntr = 0; > + retval->max_calls = 0; > + retval->slot = NULL; > + retval->fctx = NULL; > + retval->attinmeta = NULL; > + retval->fmctx = fcinfo->flinfo->fn_mcxt; > + > + /* > + * save the pointer for cross-call use > + */ > + fcinfo->flinfo->fn_extra = retval; > + > + /* back to the original memory context */ > + MemoryContextSwitchTo(oldcontext); > + } > + else /* second and subsequent calls */ > + { > + elog(ERROR, "init_MultiFuncCall may not be called more than once"); > + > + /* never reached, but keep compiler happy */ > + retval = NULL; > + } > + > + return retval; > + } > + > + /* > + * end_MultiFuncCall > + * Clean up after init_MultiFuncCall > + */ > + void > + end_MultiFuncCall(PG_FUNCTION_ARGS, FuncCallContext *funcctx) > + { > + MemoryContext oldcontext; > + > + /* unbind from fcinfo */ > + fcinfo->flinfo->fn_extra = NULL; > + > + /* > + * Caller is responsible to free up memory for individual > + * struct elements other than att_in_funcinfo and elements. > + */ > + oldcontext = MemoryContextSwitchTo(funcctx->fmctx); > + > + if (funcctx->attinmeta != NULL) > + pfree(funcctx->attinmeta); > + > + pfree(funcctx); > + > + MemoryContextSwitchTo(oldcontext); > + } > + > + void > + get_type_metadata(Oid typeid, Oid *attinfuncid, Oid *attelem) > + { > + HeapTuple typeTuple; > + Form_pg_type typtup; > + > + typeTuple = SearchSysCache(TYPEOID, > + ObjectIdGetDatum(typeid), > + 0, 0, 0); > + if (!HeapTupleIsValid(typeTuple)) > + elog(ERROR, "get_type_metadata: Cache lookup of type %u failed", typeid); > + > + typtup = (Form_pg_type) GETSTRUCT(typeTuple); > + > + *attinfuncid = typtup->typinput; > + *attelem = typtup->typelem; > + > + ReleaseSysCache(typeTuple); > + } > Index: src/include/funcapi.h > =================================================================== > RCS file: src/include/funcapi.h > diff -N src/include/funcapi.h > *** /dev/null 1 Jan 1970 00:00:00 -0000 > --- src/include/funcapi.h 10 Jun 2002 00:22:42 -0000 > *************** > *** 0 **** > --- 1,197 ---- > + /*------------------------------------------------------------------------- > + * > + * funcapi.h > + * Definitions for functions which return composite type and/or sets > + * > + * This file must be included by all Postgres modules that either define > + * or call FUNCAPI-callable functions or macros. > + * > + * > + * Copyright (c) 2002, PostgreSQL Global Development Group > + * > + * > + *------------------------------------------------------------------------- > + */ > + #ifndef FUNCAPI_H > + #define FUNCAPI_H > + > + #include "postgres.h" > + > + #include "fmgr.h" > + #include "access/htup.h" > + #include "access/tupdesc.h" > + #include "executor/executor.h" > + #include "executor/tuptable.h" > + > + /* > + * All functions that can be called directly by fmgr must have this signature. > + * (Other functions can be called by using a handler that does have this > + * signature.) > + */ > + > + > + /*------------------------------------------------------------------------- > + * Support to ease writing Functions returning composite types > + *------------------------------------------------------------------------- > + * > + * This struct holds arrays of individual attribute information > + * needed to create a tuple from raw C strings. It also requires > + * a copy of the TupleDesc. The information carried here > + * is derived from the TupleDesc, but it is stored here to > + * avoid redundant cpu cycles on each call to an SRF. > + */ > + typedef struct > + { > + /* full TupleDesc */ > + TupleDesc tupdesc; > + > + /* pointer to array of attribute "type"in finfo */ > + FmgrInfo *attinfuncs; > + > + /* pointer to array of attribute type typelem */ > + Oid *attelems; > + > + /* pointer to array of attribute type typtypmod */ > + int4 *atttypmods; > + > + } AttInMetadata; > + > + /*------------------------------------------------------------------------- > + * Support struct to ease writing Set Returning Functions (SRFs) > + *------------------------------------------------------------------------- > + * > + * This struct holds function context for Set Returning Functions. > + * Use fn_extra to hold a pointer to it across calls > + */ > + typedef struct > + { > + /* Number of times we've been called before */ > + uint call_cntr; > + > + /* Maximum number of calls */ > + uint max_calls; > + > + /* pointer to result slot */ > + TupleTableSlot *slot; > + > + /* pointer to misc context info */ > + void *fctx; > + > + /* pointer to struct containing arrays of attribute type input metainfo */ > + AttInMetadata *attinmeta; > + > + /* memory context used to initialize structure */ > + MemoryContext fmctx; > + > + } FuncCallContext; > + > + /*------------------------------------------------------------------------- > + * Support to ease writing Functions returning composite types > + * > + * External declarations: > + * TupleDesc RelationNameGetTupleDesc(char *relname) - Use to get a TupleDesc > + * based on the function's return type relation. > + * TupleDesc TypeGetTupleDesc(Oid typeoid, List *colaliases) - Use to get a > + * TupleDesc based on the function's type oid. This can be used to get > + * a TupleDesc for a base (scalar), or composite (relation) type. > + * TupleTableSlot *TupleDescGetSlot(TupleDesc tupdesc) - Initialize a slot > + * given a TupleDesc. > + * AttInMetadata *TupleDescGetAttInMetadata(TupleDesc tupdesc) - Get a pointer > + * to AttInMetadata based on the function's TupleDesc. AttInMetadata can > + * be used in conjunction with C strings to produce a properly formed > + * tuple. Store the metadata here for use across calls to avoid redundant > + * work. > + * HeapTuple BuildTupleFromCStrings(AttInMetadata *attinmeta, char **values) - > + * build a HeapTuple given user data in C string form. values is an array > + * of C strings, one for each attribute of the return tuple. > + * > + * Macro declarations: > + * TupleGetDatum(TupleTableSlot *slot, HeapTuple tuple) - get a Datum > + * given a tuple and a slot. > + */ > + > + /* from tupdesc.c */ > + extern TupleDesc RelationNameGetTupleDesc(char *relname); > + extern TupleDesc TypeGetTupleDesc(Oid typeoid, List *colaliases); > + > + /* from execTuples.c */ > + extern TupleTableSlot *TupleDescGetSlot(TupleDesc tupdesc); > + extern AttInMetadata *TupleDescGetAttInMetadata(TupleDesc tupdesc); > + extern HeapTuple BuildTupleFromCStrings(AttInMetadata *attinmeta, char **values); > + > + /* from funcapi.c */ > + extern void get_type_metadata(Oid typeid, Oid *attinfuncid, Oid *attelem); > + > + #define TupleGetDatum(_slot, _tuple) \ > + PointerGetDatum(ExecStoreTuple(_tuple, _slot, InvalidBuffer, true)) > + > + /*------------------------------------------------------------------------- > + * Support for Set Returning Functions (SRFs) > + * > + * The basic API for SRFs looks something like: > + * > + * Datum > + * my_Set_Returning_Function(PG_FUNCTION_ARGS) > + * { > + * FuncCallContext *funcctx; > + * Datum result; > + * <user defined declarations> > + * > + * if(SRF_IS_FIRSTPASS()) > + * { > + * <user defined code> > + * funcctx = SRF_FIRSTCALL_INIT(); > + * <if returning composite> > + * <obtain slot> > + * funcctx->slot = slot; > + * <endif returning composite> > + * <user defined code> > + * } > + * <user defined code> > + * funcctx = SRF_PERCALL_SETUP(funcctx); > + * <user defined code> > + * > + * if (funcctx->call_cntr < funcctx->max_calls) > + * { > + * <user defined code> > + * <obtain result Datum> > + * SRF_RETURN_NEXT(funcctx, result); > + * } > + * else > + * { > + * SRF_RETURN_DONE(funcctx); > + * } > + * } > + * > + */ > + > + /* from funcapi.c */ > + extern FuncCallContext *init_MultiFuncCall(PG_FUNCTION_ARGS); > + extern void end_MultiFuncCall(PG_FUNCTION_ARGS, FuncCallContext *funcctx); > + > + #define SRF_IS_FIRSTPASS() (fcinfo->flinfo->fn_extra == NULL) > + #define SRF_FIRSTCALL_INIT() init_MultiFuncCall(fcinfo) > + #define SRF_PERCALL_SETUP(_funcctx) \ > + fcinfo->flinfo->fn_extra; \ > + if(_funcctx->slot != NULL) \ > + ExecClearTuple(_funcctx->slot) > + #define SRF_RETURN_NEXT(_funcctx, _result) \ > + do { \ > + ReturnSetInfo *rsi; \ > + _funcctx->call_cntr++; \ > + rsi = (ReturnSetInfo *) fcinfo->resultinfo; \ > + rsi->isDone = ExprMultipleResult; \ > + PG_RETURN_DATUM(_result); \ > + } while (0) > + > + #define SRF_RETURN_DONE(_funcctx) \ > + do { \ > + ReturnSetInfo *rsi; \ > + end_MultiFuncCall(fcinfo, _funcctx); \ > + rsi = (ReturnSetInfo *) fcinfo->resultinfo; \ > + rsi->isDone = ExprEndResult; \ > + _funcctx->slot = NULL; \ > + PG_RETURN_NULL(); \ > + } while (0) > + > + #endif /* FUNCAPI_H */ > Index: src/include/utils/builtins.h > =================================================================== > RCS file: /opt/src/cvs/pgsql/src/include/utils/builtins.h,v > retrieving revision 1.182 > diff -c -r1.182 builtins.h > *** src/include/utils/builtins.h 18 May 2002 21:38:41 -0000 1.182 > --- src/include/utils/builtins.h 9 Jun 2002 21:00:31 -0000 > *************** > *** 341,346 **** > --- 341,347 ---- > extern Datum regclassout(PG_FUNCTION_ARGS); > extern Datum regtypein(PG_FUNCTION_ARGS); > extern Datum regtypeout(PG_FUNCTION_ARGS); > + extern List *stringToQualifiedNameList(const char *string, const char *caller); > > /* ruleutils.c */ > extern Datum pg_get_ruledef(PG_FUNCTION_ARGS); > > > ---------------------------(end of broadcast)--------------------------- > TIP 3: if posting/reading through Usenet, please send an appropriate > subscribe-nomail command to majordomo@postgresql.org so that your > message can get through to the mailing list cleanly > -- Bruce Momjian | http://candle.pha.pa.us pgman@candle.pha.pa.us | (610) 853-3000 + If your life is a hard drive, | 830 Blythe Avenue + Christ can be your backup. | Drexel Hill, Pennsylvania 19026
pgsql-patches by date: