postgresql/src/backend/executor/execTuples.c

862 lines
24 KiB
C
Raw Normal View History

/*-------------------------------------------------------------------------
*
* execTuples.c
* Routines dealing with the executor tuple tables. These are used to
* ensure that the executor frees copies of tuples (made by
* ExecTargetList) properly.
*
* Routines dealing with the type information for tuples. Currently,
* the type information for a tuple is an array of FormData_pg_attribute.
* This information is needed by routines manipulating tuples
* (getattribute, formtuple, etc.).
*
* Portions Copyright (c) 1996-2005, PostgreSQL Global Development Group
* Portions Copyright (c) 1994, Regents of the University of California
*
*
* IDENTIFICATION
* $PostgreSQL: pgsql/src/backend/executor/execTuples.c,v 1.83 2004/12/31 21:59:45 pgsql Exp $
*
*-------------------------------------------------------------------------
*/
/*
* INTERFACE ROUTINES
*
* TABLE CREATE/DELETE
* ExecCreateTupleTable - create a new tuple table
* ExecDropTupleTable - destroy a table
*
* SLOT RESERVATION
* ExecAllocTableSlot - find an available slot in the table
*
* SLOT ACCESSORS
* ExecStoreTuple - store a tuple in the table
* ExecFetchTuple - fetch a tuple from the table
* ExecClearTuple - clear contents of a table slot
* ExecSetSlotDescriptor - set a slot's tuple descriptor
* ExecSetSlotDescriptorIsNew - diddle the slot-desc-is-new flag
*
* SLOT STATUS PREDICATES
* TupIsNull - true when slot contains no tuple(Macro)
*
* CONVENIENCE INITIALIZATION ROUTINES
2001-03-22 05:01:46 +01:00
* ExecInitResultTupleSlot \ convenience routines to initialize
* ExecInitScanTupleSlot \ the various tuple slots for nodes
* ExecInitExtraTupleSlot / which store copies of tuples.
* ExecInitNullTupleSlot /
*
* Routines that probably belong somewhere else:
* ExecTypeFromTL - form a TupleDesc from a target list
*
* EXAMPLE OF HOW TABLE ROUTINES WORK
* Suppose we have a query such as retrieve (EMP.name) and we have
* a single SeqScan node in the query plan.
*
* At ExecStart()
* ----------------
* - InitPlan() calls ExecCreateTupleTable() to create the tuple
* table which will hold tuples processed by the executor.
*
* - ExecInitSeqScan() calls ExecInitScanTupleSlot() and
* ExecInitResultTupleSlot() to reserve places in the tuple
* table for the tuples returned by the access methods and the
* tuples resulting from preforming target list projections.
*
* During ExecRun()
* ----------------
* - SeqNext() calls ExecStoreTuple() to place the tuple returned
* by the access methods into the scan tuple slot.
*
* - ExecSeqScan() calls ExecStoreTuple() to take the result
* tuple from ExecProject() and place it into the result tuple slot.
*
* - ExecutePlan() calls ExecRetrieve() which gets the tuple out of
* the slot passed to it by calling ExecFetchTuple(). this tuple
* is then returned.
*
* At ExecEnd()
* ----------------
* - EndPlan() calls ExecDropTupleTable() to clean up any remaining
* tuples left over from executing the query.
*
* The important thing to watch in the executor code is how pointers
* to the slots containing tuples are passed instead of the tuples
* themselves. This facilitates the communication of related information
* (such as whether or not a tuple should be pfreed, what buffer contains
* this tuple, the tuple's tuple descriptor, etc). Note that much of
* this information is also kept in the ExprContext of each node.
* Soon the executor will be redesigned and ExprContext's will contain
* only slot pointers. -cim 3/14/91
*
* NOTES
* The tuple table stuff is relatively new, put here to alleviate
* the process growth problems in the executor. The other routines
* are old (from the original lisp system) and may someday become
* obsolete. -cim 6/23/90
*
* In the implementation of nested-dot queries such as
* "retrieve (EMP.hobbies.all)", a single scan may return tuples
* of many types, so now we return pointers to tuple descriptors
* along with tuples returned via the tuple table. This means
* we now have a bunch of routines to diddle the slot descriptors
* too. -cim 1/18/90
*
* The tuple table stuff depends on the executor/tuptable.h macros,
* and the TupleTableSlot node in execnodes.h.
*
*/
#include "postgres.h"
#include "funcapi.h"
#include "access/heapam.h"
#include "catalog/pg_type.h"
#include "executor/executor.h"
#include "parser/parse_expr.h"
#include "utils/lsyscache.h"
#include "utils/typcache.h"
static TupleDesc ExecTypeFromTLInternal(List *targetList,
2004-08-29 07:07:03 +02:00
bool hasoid, bool skipjunk);
/* ----------------------------------------------------------------
* tuple table create/delete functions
* ----------------------------------------------------------------
*/
/* --------------------------------
* ExecCreateTupleTable
*
* This creates a new tuple table of the specified initial
* size. If the size is insufficient, ExecAllocTableSlot()
* will grow the table as necessary.
*
* This should be used by InitPlan() to allocate the table.
* The table's address will be stored in the EState structure.
* --------------------------------
*/
TupleTable /* return: address of table */
ExecCreateTupleTable(int initialSize) /* initial number of slots in
* table */
{
TupleTable newtable; /* newly allocated table */
TupleTableSlot *array; /* newly allocated slot array */
/*
* sanity checks
*/
Assert(initialSize >= 1);
/*
* Now allocate our new table along with space for the pointers to the
2004-08-29 07:07:03 +02:00
* tuples. Zero out the slots.
*/
newtable = (TupleTable) palloc(sizeof(TupleTableData));
array = (TupleTableSlot *) palloc0(initialSize * sizeof(TupleTableSlot));
/*
* initialize the new table and return it to the caller.
*/
newtable->size = initialSize;
newtable->next = 0;
newtable->array = array;
return newtable;
}
/* --------------------------------
* ExecDropTupleTable
*
* This frees the storage used by the tuple table itself
* and optionally frees the contents of the table also.
* It is expected that this routine be called by EndPlan().
* --------------------------------
*/
void
ExecDropTupleTable(TupleTable table, /* tuple table */
bool shouldFree) /* true if we should free slot
* contents */
{
int next; /* next available slot */
TupleTableSlot *array; /* start of table array */
int i; /* counter */
/*
* sanity checks
*/
Assert(table != NULL);
/*
* get information from the table
*/
array = table->array;
next = table->next;
/*
* first free all the valid pointers in the tuple array and drop
* refcounts of any referenced buffers, if that's what the caller
* wants. (There is probably no good reason for the caller ever not
* to want it!)
*/
if (shouldFree)
{
for (i = 0; i < next; i++)
{
ExecClearTuple(&array[i]);
if (array[i].ttc_shouldFreeDesc &&
array[i].ttc_tupleDescriptor != NULL)
FreeTupleDesc(array[i].ttc_tupleDescriptor);
}
}
/*
* finally free the tuple array and the table itself.
*/
pfree(array);
pfree(table);
}
/* ----------------------------------------------------------------
* tuple table slot reservation functions
* ----------------------------------------------------------------
*/
/* --------------------------------
* ExecAllocTableSlot
*
* This routine is used to reserve slots in the table for
* use by the various plan nodes. It is expected to be
* called by the node init routines (ex: ExecInitNestLoop)
* once per slot needed by the node. Not all nodes need
* slots (some just pass tuples around).
* --------------------------------
*/
TupleTableSlot *
ExecAllocTableSlot(TupleTable table)
{
int slotnum; /* new slot number */
TupleTableSlot *slot;
/*
* sanity checks
*/
Assert(table != NULL);
/*
* if our table is full we have to allocate a larger size table. Since
* ExecAllocTableSlot() is only called before the table is ever used
* to store tuples, we don't have to worry about the contents of the
* old table. If this changes, then we will have to preserve the
* contents. -cim 6/23/90
*
* Unfortunately, we *cannot* do this. All of the nodes in the plan that
* have already initialized their slots will have pointers into
* _freed_ memory. This leads to bad ends. We now count the number
* of slots we will need and create all the slots we will need ahead
* of time. The if below should never happen now. Fail if it does.
* -mer 4 Aug 1992
*/
if (table->next >= table->size)
elog(ERROR, "plan requires more slots than are available");
/*
* at this point, space in the table is guaranteed so we reserve the
* next slot, initialize and return it.
*/
slotnum = table->next;
table->next++;
slot = &(table->array[slotnum]);
/* Make sure the allocated slot is valid (and empty) */
slot->type = T_TupleTableSlot;
slot->val = NULL;
slot->ttc_shouldFree = true;
slot->ttc_descIsNew = true;
slot->ttc_shouldFreeDesc = true;
slot->ttc_tupleDescriptor = NULL;
slot->ttc_buffer = InvalidBuffer;
return slot;
}
/* --------------------------------
* MakeTupleTableSlot
*
* This routine makes an empty standalone TupleTableSlot.
* It really shouldn't exist, but there are a few places
* that do this, so we may as well centralize the knowledge
* of what's in one ...
* --------------------------------
*/
TupleTableSlot *
MakeTupleTableSlot(void)
{
TupleTableSlot *slot = makeNode(TupleTableSlot);
/* This should match ExecAllocTableSlot() */
slot->val = NULL;
slot->ttc_shouldFree = true;
slot->ttc_descIsNew = true;
slot->ttc_shouldFreeDesc = true;
slot->ttc_tupleDescriptor = NULL;
slot->ttc_buffer = InvalidBuffer;
return slot;
}
/* ----------------------------------------------------------------
* tuple table slot accessor functions
* ----------------------------------------------------------------
*/
/* --------------------------------
* ExecStoreTuple
*
* This function is used to store a tuple into a specified
* slot in the tuple table.
*
* tuple: tuple to store
* slot: slot to store it in
* buffer: disk buffer if tuple is in a disk page, else InvalidBuffer
* shouldFree: true if ExecClearTuple should pfree() the tuple
* when done with it
*
* If 'buffer' is not InvalidBuffer, the tuple table code acquires a pin
* on the buffer which is held until the slot is cleared, so that the tuple
* won't go away on us.
*
* shouldFree is normally set 'true' for tuples constructed on-the-fly.
* It must always be 'false' for tuples that are stored in disk pages,
* since we don't want to try to pfree those.
*
* Another case where it is 'false' is when the referenced tuple is held
* in a tuple table slot belonging to a lower-level executor Proc node.
* In this case the lower-level slot retains ownership and responsibility
* for eventually releasing the tuple. When this method is used, we must
* be certain that the upper-level Proc node will lose interest in the tuple
* sooner than the lower-level one does! If you're not certain, copy the
* lower-level tuple with heap_copytuple and let the upper-level table
* slot assume ownership of the copy!
*
* Return value is just the passed-in slot pointer.
* --------------------------------
*/
TupleTableSlot *
ExecStoreTuple(HeapTuple tuple,
TupleTableSlot *slot,
Buffer buffer,
bool shouldFree)
{
/*
* sanity checks
*/
Assert(slot != NULL);
/* passing shouldFree=true for a tuple on a disk page is not sane */
Assert(BufferIsValid(buffer) ? (!shouldFree) : true);
/* clear out any old contents of the slot */
ExecClearTuple(slot);
/*
* store the new tuple into the specified slot and return the slot
* into which we stored the tuple.
*/
slot->val = tuple;
slot->ttc_buffer = buffer;
slot->ttc_shouldFree = shouldFree;
/*
* If tuple is on a disk page, keep the page pinned as long as we hold
* a pointer into it.
*/
if (BufferIsValid(buffer))
IncrBufferRefCount(buffer);
return slot;
}
/* --------------------------------
* ExecClearTuple
*
* This function is used to clear out a slot in the tuple table.
*
* NB: only the tuple is cleared, not the tuple descriptor (if any).
* --------------------------------
*/
TupleTableSlot * /* return: slot passed */
ExecClearTuple(TupleTableSlot *slot) /* slot in which to store tuple */
{
HeapTuple oldtuple; /* prior contents of slot */
/*
* sanity checks
*/
Assert(slot != NULL);
/*
* get information from the tuple table
*/
oldtuple = slot->val;
/*
* free the old contents of the specified slot if necessary.
*/
if (slot->ttc_shouldFree && oldtuple != NULL)
heap_freetuple(oldtuple);
slot->val = NULL;
slot->ttc_shouldFree = true; /* probably useless code... */
/*
* Drop the pin on the referenced buffer, if there is one.
*/
if (BufferIsValid(slot->ttc_buffer))
ReleaseBuffer(slot->ttc_buffer);
slot->ttc_buffer = InvalidBuffer;
return slot;
}
/* --------------------------------
* ExecSetSlotDescriptor
*
* This function is used to set the tuple descriptor associated
* with the slot's tuple.
* --------------------------------
*/
void
ExecSetSlotDescriptor(TupleTableSlot *slot, /* slot to change */
TupleDesc tupdesc, /* new tuple descriptor */
2001-03-22 05:01:46 +01:00
bool shouldFree) /* is desc owned by slot? */
{
if (slot->ttc_shouldFreeDesc &&
slot->ttc_tupleDescriptor != NULL)
FreeTupleDesc(slot->ttc_tupleDescriptor);
slot->ttc_tupleDescriptor = tupdesc;
slot->ttc_shouldFreeDesc = shouldFree;
}
/* --------------------------------
* ExecSetSlotDescriptorIsNew
*
* This function is used to change the setting of the "isNew" flag
* --------------------------------
*/
void
ExecSetSlotDescriptorIsNew(TupleTableSlot *slot, /* slot to change */
bool isNew) /* "isNew" setting */
{
slot->ttc_descIsNew = isNew;
}
/* ----------------------------------------------------------------
* tuple table slot status predicates
* ----------------------------------------------------------------
*/
/* ----------------------------------------------------------------
* convenience initialization routines
* ----------------------------------------------------------------
*/
/* --------------------------------
* ExecInit{Result,Scan,Extra}TupleSlot
*
* These are convenience routines to initialize the specified slot
2001-03-22 05:01:46 +01:00
* in nodes inheriting the appropriate state. ExecInitExtraTupleSlot
* is used for initializing special-purpose slots.
* --------------------------------
*/
/* ----------------
* ExecInitResultTupleSlot
* ----------------
*/
void
ExecInitResultTupleSlot(EState *estate, PlanState *planstate)
{
planstate->ps_ResultTupleSlot = ExecAllocTableSlot(estate->es_tupleTable);
}
/* ----------------
* ExecInitScanTupleSlot
* ----------------
*/
void
ExecInitScanTupleSlot(EState *estate, ScanState *scanstate)
{
scanstate->ss_ScanTupleSlot = ExecAllocTableSlot(estate->es_tupleTable);
}
/* ----------------
* ExecInitExtraTupleSlot
* ----------------
*/
TupleTableSlot *
ExecInitExtraTupleSlot(EState *estate)
{
return ExecAllocTableSlot(estate->es_tupleTable);
}
1999-05-25 18:15:34 +02:00
/* ----------------
* ExecInitNullTupleSlot
*
* Build a slot containing an all-nulls tuple of the given type.
* This is used as a substitute for an input tuple when performing an
* outer join.
* ----------------
*/
TupleTableSlot *
ExecInitNullTupleSlot(EState *estate, TupleDesc tupType)
{
2001-03-22 05:01:46 +01:00
TupleTableSlot *slot = ExecInitExtraTupleSlot(estate);
struct tupleDesc nullTupleDesc;
HeapTuple nullTuple;
Datum values[1];
char nulls[1];
2001-03-22 05:01:46 +01:00
/*
* Since heap_getattr() will treat attributes beyond a tuple's t_natts
2001-03-22 05:01:46 +01:00
* as being NULL, we can make an all-nulls tuple just by making it be
* of zero length. However, the slot descriptor must match the real
* tupType.
*/
nullTupleDesc = *tupType;
nullTupleDesc.natts = 0;
nullTuple = heap_formtuple(&nullTupleDesc, values, nulls);
ExecSetSlotDescriptor(slot, tupType, false);
return ExecStoreTuple(nullTuple, slot, InvalidBuffer, true);
}
/* ----------------------------------------------------------------
* ExecTypeFromTL
*
* Generate a tuple descriptor for the result tuple of a targetlist.
* (A parse/plan tlist must be passed, not an ExprState tlist.)
* Note that resjunk columns, if any, are included in the result.
*
* Currently there are about 4 different places where we create
* TupleDescriptors. They should all be merged, or perhaps
* be rewritten to call BuildDesc().
* ----------------------------------------------------------------
*/
TupleDesc
ExecTypeFromTL(List *targetList, bool hasoid)
{
return ExecTypeFromTLInternal(targetList, hasoid, false);
}
/* ----------------------------------------------------------------
* ExecCleanTypeFromTL
*
* Same as above, but resjunk columns are omitted from the result.
* ----------------------------------------------------------------
*/
TupleDesc
ExecCleanTypeFromTL(List *targetList, bool hasoid)
{
return ExecTypeFromTLInternal(targetList, hasoid, true);
}
static TupleDesc
ExecTypeFromTLInternal(List *targetList, bool hasoid, bool skipjunk)
{
2004-08-29 07:07:03 +02:00
TupleDesc typeInfo;
ListCell *l;
int len;
int cur_resno = 1;
if (skipjunk)
len = ExecCleanTargetListLength(targetList);
else
len = ExecTargetListLength(targetList);
typeInfo = CreateTemplateTupleDesc(len, hasoid);
foreach(l, targetList)
{
2004-08-29 07:07:03 +02:00
TargetEntry *tle = lfirst(l);
Resdom *resdom = tle->resdom;
if (skipjunk && resdom->resjunk)
continue;
TupleDescInitEntry(typeInfo,
cur_resno++,
resdom->resname,
resdom->restype,
resdom->restypmod,
0);
}
return typeInfo;
}
/*
* ExecTypeFromExprList - build a tuple descriptor from a list of Exprs
*
* Here we must make up an arbitrary set of field names.
*/
TupleDesc
ExecTypeFromExprList(List *exprList)
{
2004-08-29 07:07:03 +02:00
TupleDesc typeInfo;
ListCell *l;
int cur_resno = 1;
char fldname[NAMEDATALEN];
typeInfo = CreateTemplateTupleDesc(list_length(exprList), false);
foreach(l, exprList)
{
2004-08-29 07:07:03 +02:00
Node *e = lfirst(l);
sprintf(fldname, "f%d", cur_resno);
TupleDescInitEntry(typeInfo,
cur_resno++,
fldname,
exprType(e),
exprTypmod(e),
0);
}
return typeInfo;
}
/*
* BlessTupleDesc - make a completed tuple descriptor useful for SRFs
*
* Rowtype Datums returned by a function must contain valid type information.
* This happens "for free" if the tupdesc came from a relcache entry, but
* not if we have manufactured a tupdesc for a transient RECORD datatype.
* In that case we have to notify typcache.c of the existence of the type.
*/
TupleDesc
BlessTupleDesc(TupleDesc tupdesc)
{
if (tupdesc->tdtypeid == RECORDOID &&
tupdesc->tdtypmod < 0)
assign_record_type_typmod(tupdesc);
return tupdesc; /* just for notational convenience */
}
/*
* TupleDescGetSlot - Initialize a slot based on the supplied tupledesc
*
* Note: this is obsolete; it is sufficient to call BlessTupleDesc on
* the tupdesc. We keep it around just for backwards compatibility with
* existing user-written SRFs.
*/
TupleTableSlot *
TupleDescGetSlot(TupleDesc tupdesc)
{
2002-09-04 22:31:48 +02:00
TupleTableSlot *slot;
/* The useful work is here */
BlessTupleDesc(tupdesc);
/* Make a standalone slot */
slot = MakeTupleTableSlot();
/* Bind the tuple description to the slot */
ExecSetSlotDescriptor(slot, tupdesc, true);
/* Return the slot */
return slot;
}
/*
* TupleDescGetAttInMetadata - Build an AttInMetadata structure based on the
* supplied TupleDesc. AttInMetadata can be used in conjunction with C strings
* to produce a properly formed tuple.
*/
AttInMetadata *
TupleDescGetAttInMetadata(TupleDesc tupdesc)
{
2002-09-04 22:31:48 +02:00
int natts = tupdesc->natts;
int i;
Oid atttypeid;
Oid attinfuncid;
FmgrInfo *attinfuncinfo;
Oid *attioparams;
2002-09-04 22:31:48 +02:00
int32 *atttypmods;
AttInMetadata *attinmeta;
attinmeta = (AttInMetadata *) palloc(sizeof(AttInMetadata));
/* "Bless" the tupledesc so that we can make rowtype datums with it */
attinmeta->tupdesc = BlessTupleDesc(tupdesc);
/*
2002-09-04 22:31:48 +02:00
* Gather info needed later to call the "in" function for each
* attribute
*/
attinfuncinfo = (FmgrInfo *) palloc0(natts * sizeof(FmgrInfo));
attioparams = (Oid *) palloc0(natts * sizeof(Oid));
atttypmods = (int32 *) palloc0(natts * sizeof(int32));
for (i = 0; i < natts; i++)
{
/* Ignore dropped attributes */
if (!tupdesc->attrs[i]->attisdropped)
{
atttypeid = tupdesc->attrs[i]->atttypid;
getTypeInputInfo(atttypeid, &attinfuncid, &attioparams[i]);
fmgr_info(attinfuncid, &attinfuncinfo[i]);
atttypmods[i] = tupdesc->attrs[i]->atttypmod;
}
}
attinmeta->attinfuncs = attinfuncinfo;
attinmeta->attioparams = attioparams;
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)
{
2002-09-04 22:31:48 +02:00
TupleDesc tupdesc = attinmeta->tupdesc;
int natts = tupdesc->natts;
Datum *dvalues;
char *nulls;
int i;
Oid attioparam;
2002-09-04 22:31:48 +02:00
int32 atttypmod;
HeapTuple tuple;
dvalues = (Datum *) palloc(natts * sizeof(Datum));
nulls = (char *) palloc(natts * sizeof(char));
/* Call the "in" function for each non-null, non-dropped attribute */
for (i = 0; i < natts; i++)
{
if (!tupdesc->attrs[i]->attisdropped)
{
/* Non-dropped attributes */
if (values[i] != NULL)
{
attioparam = attinmeta->attioparams[i];
atttypmod = attinmeta->atttypmods[i];
dvalues[i] = FunctionCall3(&attinmeta->attinfuncs[i],
CStringGetDatum(values[i]),
ObjectIdGetDatum(attioparam),
Int32GetDatum(atttypmod));
nulls[i] = ' ';
}
else
{
dvalues[i] = (Datum) 0;
nulls[i] = 'n';
}
}
else
{
/* Handle dropped attributes by setting to NULL */
dvalues[i] = (Datum) 0;
nulls[i] = 'n';
}
}
/*
* Form a tuple
*/
tuple = heap_formtuple(tupdesc, dvalues, nulls);
/*
* Release locally palloc'd space. XXX would probably be good to
* pfree values of pass-by-reference datums, as well.
*/
pfree(dvalues);
pfree(nulls);
return tuple;
}
/*
* Functions for sending tuples to the frontend (or other specified destination)
* as though it is a SELECT result. These are used by utility commands that
* need to project directly to the destination and don't need or want full
* Table Function capability. Currently used by EXPLAIN and SHOW ALL
*/
TupOutputState *
begin_tup_output_tupdesc(DestReceiver *dest, TupleDesc tupdesc)
{
TupOutputState *tstate;
tstate = (TupOutputState *) palloc(sizeof(TupOutputState));
tstate->metadata = TupleDescGetAttInMetadata(tupdesc);
tstate->dest = dest;
(*tstate->dest->rStartup) (tstate->dest, (int) CMD_SELECT, tupdesc);
return tstate;
}
/*
* write a single tuple
*
* values is a list of the external C string representations of the values
* to be projected.
*/
void
do_tup_output(TupOutputState *tstate, char **values)
{
/* build a tuple from the input strings using the tupdesc */
HeapTuple tuple = BuildTupleFromCStrings(tstate->metadata, values);
/* send the tuple to the receiver */
(*tstate->dest->receiveTuple) (tuple,
tstate->metadata->tupdesc,
tstate->dest);
/* clean up */
heap_freetuple(tuple);
}
/*
* write a chunk of text, breaking at newline characters
*
* NB: scribbles on its input!
*
* Should only be used with a single-TEXT-attribute tupdesc.
*/
void
do_text_output_multiline(TupOutputState *tstate, char *text)
{
while (*text)
{
2002-09-04 22:31:48 +02:00
char *eol;
eol = strchr(text, '\n');
if (eol)
*eol++ = '\0';
else
2003-08-04 02:43:34 +02:00
eol = text +strlen(text);
2002-09-04 22:31:48 +02:00
do_tup_output(tstate, &text);
text = eol;
}
}
void
end_tup_output(TupOutputState *tstate)
{
(*tstate->dest->rShutdown) (tstate->dest);
/* note that destroying the dest is not ours to do */
/* XXX worth cleaning up the attinmetadata? */
pfree(tstate);
}