postgresql/src/backend/commands/copy.c

921 lines
23 KiB
C
Raw Normal View History

/*-------------------------------------------------------------------------
*
* copy.c--
*
* Copyright (c) 1994, Regents of the University of California
*
*
* IDENTIFICATION
1996-10-21 11:37:26 +02:00
* $Header: /cvsroot/pgsql/src/backend/commands/copy.c,v 1.9 1996/10/21 09:37:26 scrappy Exp $
*
*-------------------------------------------------------------------------
*/
1996-10-21 11:37:26 +02:00
#include "postgres.h"
#include "catalog/pg_attribute.h"
#include "access/attnum.h"
#include "nodes/pg_list.h"
#include "access/tupdesc.h"
#include "storage/fd.h"
#include "catalog/pg_am.h"
#include "catalog/pg_class.h"
#include "nodes/nodes.h"
#include "rewrite/prs2lock.h"
#include "access/skey.h"
#include "access/strat.h"
#include "utils/rel.h"
#include "storage/block.h"
#include "storage/off.h"
#include "storage/itemptr.h"
#include <time.h>
#include "utils/nabstime.h"
#include "access/htup.h"
#include "utils/tqual.h"
#include "storage/buf.h"
#include "access/relscan.h"
#include "access/heapam.h"
#include "access/itup.h"
#include <stdio.h>
1996-10-21 11:37:26 +02:00
#include "tcop/dest.h"
#include "fmgr.h"
#include "utils/palloc.h"
#include "miscadmin.h"
1996-10-21 11:37:26 +02:00
#include "utils/geo-decls.h"
#include "utils/builtins.h"
1996-10-21 11:37:26 +02:00
#include <sys/stat.h>
#include "access/funcindex.h"
1996-10-21 11:37:26 +02:00
#include "catalog/pg_index.h"
#include "utils/syscache.h"
#include "nodes/params.h"
#include "access/sdir.h"
#include "executor/hashjoin.h"
#include "nodes/primnodes.h"
#include "nodes/memnodes.h"
#include "executor/tuptable.h"
1996-10-21 11:37:26 +02:00
#include "nodes/execnodes.h"
#include "utils/memutils.h"
1996-10-21 11:37:26 +02:00
#include "nodes/plannodes.h"
#include "nodes/parsenodes.h"
#include "executor/execdesc.h"
#include "executor/executor.h"
#include "storage/ipc.h"
#include "storage/bufmgr.h"
#include "access/transam.h"
#include "catalog/index.h"
#include "access/genam.h"
#include "catalog/pg_type.h"
#include "catalog/catname.h"
#define ISOCTAL(c) (((c) >= '0') && ((c) <= '7'))
#define VALUE(c) ((c) - '0')
/*
* New copy code.
*
* This code "knows" the following about tuples:
*
*/
static bool reading_from_input = false;
/* non-export function prototypes */
static void CopyTo(Relation rel, bool binary, bool oids, FILE *fp, char *delim);
static void CopyFrom(Relation rel, bool binary, bool oids, FILE *fp, char *delim);
static Oid GetOutputFunction(Oid type);
static Oid GetTypeElement(Oid type);
static Oid GetInputFunction(Oid type);
static Oid IsTypeByVal(Oid type);
static void GetIndexRelations(Oid main_relation_oid,
int *n_indices,
Relation **index_rels);
static char *CopyReadAttribute(FILE *fp, bool *isnull, char *delim);
static void CopyAttributeOut(FILE *fp, char *string, char *delim);
static int CountTuples(Relation relation);
extern FILE *Pfout, *Pfin;
void
DoCopy(char *relname, bool binary, bool oids, bool from, bool pipe, char *filename,
char *delim)
{
FILE *fp;
Relation rel;
reading_from_input = pipe;
rel = heap_openr(relname);
if (rel == NULL) elog(WARN, "Copy: class %s does not exist.", relname);
if (from) {
if (pipe && IsUnderPostmaster) ReceiveCopyBegin();
if (IsUnderPostmaster) {
fp = pipe ? Pfin : fopen(filename, "r");
}else {
fp = pipe ? stdin : fopen(filename, "r");
}
if (fp == NULL) {
elog(WARN, "COPY: file %s could not be open for reading", filename);
}
CopyFrom(rel, binary, oids, fp, delim);
}else {
mode_t oumask = umask((mode_t) 0);
if (pipe && IsUnderPostmaster) SendCopyBegin();
if (IsUnderPostmaster) {
fp = pipe ? Pfout : fopen(filename, "w");
}else {
fp = pipe ? stdout : fopen(filename, "w");
}
(void) umask(oumask);
if (fp == NULL) {
elog(WARN, "COPY: file %s could not be open for writing", filename);
}
CopyTo(rel, binary, oids, fp, delim);
}
if (!pipe) {
fclose(fp);
}else if (!from && !binary) {
fputs("\\.\n", fp);
if (IsUnderPostmaster) fflush(Pfout);
}
}
static void
CopyTo(Relation rel, bool binary, bool oids, FILE *fp, char *delim)
{
HeapTuple tuple;
HeapScanDesc scandesc;
int32 attr_count, i;
AttributeTupleForm *attr;
func_ptr *out_functions;
int dummy;
Oid out_func_oid;
Oid *elements;
Datum value;
bool isnull = (bool) true;
char *nulls;
char *string;
int32 ntuples;
TupleDesc tupDesc;
scandesc = heap_beginscan(rel, 0, NULL, 0, NULL);
attr_count = rel->rd_att->natts;
attr = rel->rd_att->attrs;
tupDesc = rel->rd_att;
if (!binary) {
out_functions = (func_ptr *)
palloc(attr_count * sizeof(func_ptr));
elements = (Oid *) palloc(attr_count * sizeof(Oid));
for (i = 0; i < attr_count; i++) {
out_func_oid = (Oid) GetOutputFunction(attr[i]->atttypid);
fmgr_info(out_func_oid, &out_functions[i], &dummy);
elements[i] = GetTypeElement(attr[i]->atttypid);
}
}else {
nulls = (char *) palloc(attr_count);
for (i = 0; i < attr_count; i++) nulls[i] = ' ';
/* XXX expensive */
ntuples = CountTuples(rel);
fwrite(&ntuples, sizeof(int32), 1, fp);
}
for (tuple = heap_getnext(scandesc, 0, NULL);
tuple != NULL;
tuple = heap_getnext(scandesc, 0, NULL)) {
if (oids && !binary) {
fputs(oidout(tuple->t_oid),fp);
fputc(delim[0], fp);
}
for (i = 0; i < attr_count; i++) {
value = (Datum)
heap_getattr(tuple, InvalidBuffer, i+1, tupDesc, &isnull);
if (!binary) {
if (!isnull) {
string = (char *) (out_functions[i]) (value, elements[i]);
CopyAttributeOut(fp, string, delim);
pfree(string);
}
else
fputs("\\N", fp); /* null indicator */
if (i == attr_count - 1) {
fputc('\n', fp);
}else {
/* when copying out, only use the first char of the delim
string */
fputc(delim[0], fp);
}
}else {
/*
* only interesting thing heap_getattr tells us in this case
* is if we have a null attribute or not.
*/
if (isnull) nulls[i] = 'n';
}
}
if (binary) {
int32 null_ct = 0, length;
for (i = 0; i < attr_count; i++) {
if (nulls[i] == 'n') null_ct++;
}
length = tuple->t_len - tuple->t_hoff;
fwrite(&length, sizeof(int32), 1, fp);
if (oids)
fwrite((char *) &tuple->t_oid, sizeof(int32), 1, fp);
fwrite(&null_ct, sizeof(int32), 1, fp);
if (null_ct > 0) {
for (i = 0; i < attr_count; i++) {
if (nulls[i] == 'n') {
fwrite(&i, sizeof(int32), 1, fp);
nulls[i] = ' ';
}
}
}
fwrite((char *) tuple + tuple->t_hoff, length, 1, fp);
}
}
heap_endscan(scandesc);
if (binary) {
pfree(nulls);
}else {
pfree(out_functions);
pfree(elements);
}
heap_close(rel);
}
static void
CopyFrom(Relation rel, bool binary, bool oids, FILE *fp, char *delim)
{
HeapTuple tuple;
AttrNumber attr_count;
AttributeTupleForm *attr;
func_ptr *in_functions;
int i, dummy;
Oid in_func_oid;
Datum *values;
char *nulls, *index_nulls;
bool *byval;
bool isnull;
bool has_index;
int done = 0;
char *string, *ptr;
Relation *index_rels;
int32 len, null_ct, null_id;
int32 ntuples, tuples_read = 0;
bool reading_to_eof = true;
Oid *elements;
FuncIndexInfo *finfo, **finfoP;
TupleDesc *itupdescArr;
HeapTuple pgIndexTup;
IndexTupleForm *pgIndexP;
int *indexNatts;
char *predString;
Node **indexPred;
TupleDesc rtupdesc;
ExprContext *econtext;
TupleTable tupleTable;
TupleTableSlot *slot;
int natts;
AttrNumber *attnumP;
Datum idatum;
int n_indices;
InsertIndexResult indexRes;
TupleDesc tupDesc;
Oid loaded_oid;
tupDesc = RelationGetTupleDescriptor(rel);
attr = tupDesc->attrs;
attr_count = tupDesc->natts;
has_index = false;
/*
* This may be a scalar or a functional index. We initialize all
* kinds of arrays here to avoid doing extra work at every tuple
* copy.
*/
if (rel->rd_rel->relhasindex) {
GetIndexRelations(rel->rd_id, &n_indices, &index_rels);
if (n_indices > 0) {
has_index = true;
itupdescArr =
(TupleDesc *)palloc(n_indices * sizeof(TupleDesc));
pgIndexP =
(IndexTupleForm *)palloc(n_indices * sizeof(IndexTupleForm));
indexNatts = (int *) palloc(n_indices * sizeof(int));
finfo = (FuncIndexInfo *) palloc(n_indices * sizeof(FuncIndexInfo));
finfoP = (FuncIndexInfo **) palloc(n_indices * sizeof(FuncIndexInfo *));
indexPred = (Node **) palloc(n_indices * sizeof(Node*));
econtext = NULL;
for (i = 0; i < n_indices; i++) {
itupdescArr[i] = RelationGetTupleDescriptor(index_rels[i]);
pgIndexTup =
SearchSysCacheTuple(INDEXRELID,
ObjectIdGetDatum(index_rels[i]->rd_id),
0,0,0);
Assert(pgIndexTup);
pgIndexP[i] = (IndexTupleForm)GETSTRUCT(pgIndexTup);
for (attnumP = &(pgIndexP[i]->indkey[0]), natts = 0;
*attnumP != InvalidAttrNumber;
attnumP++, natts++);
if (pgIndexP[i]->indproc != InvalidOid) {
FIgetnArgs(&finfo[i]) = natts;
natts = 1;
FIgetProcOid(&finfo[i]) = pgIndexP[i]->indproc;
*(FIgetname(&finfo[i])) = '\0';
finfoP[i] = &finfo[i];
} else
finfoP[i] = (FuncIndexInfo *) NULL;
indexNatts[i] = natts;
if (VARSIZE(&pgIndexP[i]->indpred) != 0) {
predString = fmgr(F_TEXTOUT, &pgIndexP[i]->indpred);
indexPred[i] = stringToNode(predString);
pfree(predString);
/* make dummy ExprContext for use by ExecQual */
if (econtext == NULL) {
#ifndef OMIT_PARTIAL_INDEX
tupleTable = ExecCreateTupleTable(1);
slot = ExecAllocTableSlot(tupleTable);
econtext = makeNode(ExprContext);
econtext->ecxt_scantuple = slot;
rtupdesc = RelationGetTupleDescriptor(rel);
slot->ttc_tupleDescriptor = rtupdesc;
/*
* There's no buffer associated with heap tuples here,
* so I set the slot's buffer to NULL. Currently, it
* appears that the only way a buffer could be needed
* would be if the partial index predicate referred to
* the "lock" system attribute. If it did, then
* heap_getattr would call HeapTupleGetRuleLock, which
* uses the buffer's descriptor to get the relation id.
* Rather than try to fix this, I'll just disallow
* partial indexes on "lock", which wouldn't be useful
* anyway. --Nels, Nov '92
*/
/* SetSlotBuffer(slot, (Buffer) NULL); */
/* SetSlotShouldFree(slot, false); */
slot->ttc_buffer = (Buffer)NULL;
slot->ttc_shouldFree = false;
#endif /* OMIT_PARTIAL_INDEX */
}
} else {
indexPred[i] = NULL;
}
}
}
}
if (!binary)
{
in_functions = (func_ptr *) palloc(attr_count * sizeof(func_ptr));
elements = (Oid *) palloc(attr_count * sizeof(Oid));
for (i = 0; i < attr_count; i++)
{
in_func_oid = (Oid) GetInputFunction(attr[i]->atttypid);
fmgr_info(in_func_oid, &in_functions[i], &dummy);
elements[i] = GetTypeElement(attr[i]->atttypid);
}
}
else
{
fread(&ntuples, sizeof(int32), 1, fp);
if (ntuples != 0) reading_to_eof = false;
}
values = (Datum *) palloc(sizeof(Datum) * attr_count);
nulls = (char *) palloc(attr_count);
index_nulls = (char *) palloc(attr_count);
byval = (bool *) palloc(attr_count * sizeof(bool));
for (i = 0; i < attr_count; i++) {
nulls[i] = ' ';
index_nulls[i] = ' ';
byval[i] = (bool) IsTypeByVal(attr[i]->atttypid);
}
while (!done) {
if (!binary) {
if (oids) {
string = CopyReadAttribute(fp, &isnull, delim);
if (string == NULL)
done = 1;
else {
loaded_oid = oidin(string);
if (loaded_oid < BootstrapObjectIdData)
elog(WARN, "COPY TEXT: Invalid Oid");
}
}
for (i = 0; i < attr_count && !done; i++) {
string = CopyReadAttribute(fp, &isnull, delim);
if (isnull) {
values[i] = PointerGetDatum(NULL);
nulls[i] = 'n';
}else if (string == NULL) {
done = 1;
}else {
values[i] =
(Datum)(in_functions[i])(string,
elements[i],
attr[i]->attlen);
/*
* Sanity check - by reference attributes cannot return
* NULL
*/
if (!PointerIsValid(values[i]) &&
!(rel->rd_att->attrs[i]->attbyval)) {
elog(WARN, "copy from: Bad file format");
}
}
}
}else { /* binary */
fread(&len, sizeof(int32), 1, fp);
if (feof(fp)) {
done = 1;
}else {
if (oids) {
fread(&loaded_oid, sizeof(int32), 1, fp);
if (loaded_oid < BootstrapObjectIdData)
elog(WARN, "COPY BINARY: Invalid Oid");
}
fread(&null_ct, sizeof(int32), 1, fp);
if (null_ct > 0) {
for (i = 0; i < null_ct; i++) {
fread(&null_id, sizeof(int32), 1, fp);
nulls[null_id] = 'n';
}
}
string = (char *) palloc(len);
fread(string, len, 1, fp);
ptr = string;
for (i = 0; i < attr_count; i++) {
if (byval[i] && nulls[i] != 'n') {
switch(attr[i]->attlen) {
case sizeof(char):
values[i] = (Datum) *(unsigned char *) ptr;
ptr += sizeof(char);
break;
case sizeof(short):
ptr = (char *) SHORTALIGN(ptr);
values[i] = (Datum) *(unsigned short *) ptr;
ptr += sizeof(short);
break;
case sizeof(int32):
ptr = (char *) INTALIGN(ptr);
values[i] = (Datum) *(uint32 *) ptr;
ptr += sizeof(int32);
break;
default:
elog(WARN, "COPY BINARY: impossible size!");
break;
}
}else if (nulls[i] != 'n') {
switch (attr[i]->attlen) {
case -1:
if (attr[i]->attalign == 'd')
ptr = (char *)DOUBLEALIGN(ptr);
else
ptr = (char *)INTALIGN(ptr);
values[i] = (Datum) ptr;
ptr += * (uint32 *) ptr;
break;
case sizeof(char):
values[i] = (Datum)ptr;
ptr += attr[i]->attlen;
break;
case sizeof(short):
ptr = (char*)SHORTALIGN(ptr);
values[i] = (Datum)ptr;
ptr += attr[i]->attlen;
break;
case sizeof(int32):
ptr = (char*)INTALIGN(ptr);
values[i] = (Datum)ptr;
ptr += attr[i]->attlen;
break;
default:
if (attr[i]->attalign == 'd')
ptr = (char *)DOUBLEALIGN(ptr);
else
ptr = (char *)LONGALIGN(ptr);
values[i] = (Datum) ptr;
ptr += attr[i]->attlen;
}
}
}
}
}
if (done) continue;
tupDesc = CreateTupleDesc(attr_count, attr);
tuple = heap_formtuple(tupDesc, values, nulls);
if (oids)
tuple->t_oid = loaded_oid;
heap_insert(rel, tuple);
if (has_index) {
for (i = 0; i < n_indices; i++) {
if (indexPred[i] != NULL) {
#ifndef OMIT_PARTIAL_INDEX
/* if tuple doesn't satisfy predicate,
* don't update index
*/
slot->val = tuple;
/*SetSlotContents(slot, tuple); */
if (ExecQual((List*)indexPred[i], econtext) == false)
continue;
#endif /* OMIT_PARTIAL_INDEX */
}
FormIndexDatum(indexNatts[i],
(AttrNumber *)&(pgIndexP[i]->indkey[0]),
tuple,
tupDesc,
InvalidBuffer,
&idatum,
index_nulls,
finfoP[i]);
indexRes = index_insert(index_rels[i], &idatum, index_nulls,
&(tuple->t_ctid));
if (indexRes) pfree(indexRes);
}
}
if (binary) pfree(string);
for (i = 0; i < attr_count; i++) {
if (!byval[i] && nulls[i] != 'n') {
if (!binary) pfree((void*)values[i]);
}else if (nulls[i] == 'n') {
nulls[i] = ' ';
}
}
pfree(tuple);
tuples_read++;
if (!reading_to_eof && ntuples == tuples_read) done = true;
}
pfree(values);
if (!binary) pfree(in_functions);
pfree(nulls);
pfree(byval);
heap_close(rel);
}
static Oid
GetOutputFunction(Oid type)
{
HeapTuple typeTuple;
typeTuple = SearchSysCacheTuple(TYPOID,
ObjectIdGetDatum(type),
0,0,0);
if (HeapTupleIsValid(typeTuple))
return((int) ((TypeTupleForm) GETSTRUCT(typeTuple))->typoutput);
elog(WARN, "GetOutputFunction: Cache lookup of type %d failed", type);
return(InvalidOid);
}
static Oid
GetTypeElement(Oid type)
{
HeapTuple typeTuple;
typeTuple = SearchSysCacheTuple(TYPOID,
ObjectIdGetDatum(type),
0,0,0);
if (HeapTupleIsValid(typeTuple))
return((int) ((TypeTupleForm) GETSTRUCT(typeTuple))->typelem);
elog(WARN, "GetOutputFunction: Cache lookup of type %d failed", type);
return(InvalidOid);
}
static Oid
GetInputFunction(Oid type)
{
HeapTuple typeTuple;
typeTuple = SearchSysCacheTuple(TYPOID,
ObjectIdGetDatum(type),
0,0,0);
if (HeapTupleIsValid(typeTuple))
return((int) ((TypeTupleForm) GETSTRUCT(typeTuple))->typinput);
elog(WARN, "GetInputFunction: Cache lookup of type %d failed", type);
return(InvalidOid);
}
static Oid
IsTypeByVal(Oid type)
{
HeapTuple typeTuple;
typeTuple = SearchSysCacheTuple(TYPOID,
ObjectIdGetDatum(type),
0,0,0);
if (HeapTupleIsValid(typeTuple))
return((int) ((TypeTupleForm) GETSTRUCT(typeTuple))->typbyval);
elog(WARN, "GetInputFunction: Cache lookup of type %d failed", type);
return(InvalidOid);
}
/*
* Given the OID of a relation, return an array of index relation descriptors
* and the number of index relations. These relation descriptors are open
* using heap_open().
*
* Space for the array itself is palloc'ed.
*/
typedef struct rel_list {
Oid index_rel_oid;
struct rel_list *next;
} RelationList;
static void
GetIndexRelations(Oid main_relation_oid,
int *n_indices,
Relation **index_rels)
{
RelationList *head, *scan;
Relation pg_index_rel;
HeapScanDesc scandesc;
Oid index_relation_oid;
HeapTuple tuple;
TupleDesc tupDesc;
int i;
bool isnull;
pg_index_rel = heap_openr(IndexRelationName);
scandesc = heap_beginscan(pg_index_rel, 0, NULL, 0, NULL);
tupDesc = RelationGetTupleDescriptor(pg_index_rel);
*n_indices = 0;
head = (RelationList *) palloc(sizeof(RelationList));
scan = head;
head->next = NULL;
for (tuple = heap_getnext(scandesc, 0, NULL);
tuple != NULL;
tuple = heap_getnext(scandesc, 0, NULL)) {
index_relation_oid =
(Oid) DatumGetInt32(heap_getattr(tuple, InvalidBuffer, 2,
tupDesc, &isnull));
if (index_relation_oid == main_relation_oid) {
scan->index_rel_oid =
(Oid) DatumGetInt32(heap_getattr(tuple, InvalidBuffer,
Anum_pg_index_indexrelid,
tupDesc, &isnull));
(*n_indices)++;
scan->next = (RelationList *) palloc(sizeof(RelationList));
scan = scan->next;
}
}
heap_endscan(scandesc);
heap_close(pg_index_rel);
/* We cannot trust to relhasindex of the main_relation now, so... */
if ( *n_indices == 0 )
return;
*index_rels = (Relation *) palloc(*n_indices * sizeof(Relation));
for (i = 0, scan = head; i < *n_indices; i++, scan = scan->next) {
(*index_rels)[i] = index_open(scan->index_rel_oid);
}
for (i = 0, scan = head; i < *n_indices + 1; i++) {
scan = head->next;
pfree(head);
head = scan;
}
}
#define EXT_ATTLEN 5*8192
/*
returns 1 is c is in s
*/
static bool
inString(char c, char* s)
{
int i;
if (s) {
i = 0;
while (s[i] != '\0') {
if (s[i] == c)
return 1;
i++;
}
}
return 0;
}
/*
* Reads input from fp until eof is seen. If we are reading from standard
* input, AND we see a dot on a line by itself (a dot followed immediately
* by a newline), we exit as if we saw eof. This is so that copy pipelines
* can be used as standard input.
*/
static char *
CopyReadAttribute(FILE *fp, bool *isnull, char *delim)
{
static char attribute[EXT_ATTLEN];
char c;
int done = 0;
int i = 0;
*isnull = (bool) false; /* set default */
if (feof(fp))
return(NULL);
while (!done) {
c = getc(fp);
if (feof(fp))
return(NULL);
else if (c == '\\') {
c = getc(fp);
if (feof(fp))
return(NULL);
switch (c) {
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7': {
int val;
val = VALUE(c);
c = getc(fp);
if (ISOCTAL(c)) {
val = (val<<3) + VALUE(c);
c = getc(fp);
if (ISOCTAL(c)) {
val = (val<<3) + VALUE(c);
} else {
if (feof(fp))
return(NULL);
ungetc(c, fp);
}
} else {
if (feof(fp))
return(NULL);
ungetc(c, fp);
}
c = val & 0377;
}
break;
case 'b':
c = '\b';
break;
case 'f':
c = '\f';
break;
case 'n':
c = '\n';
break;
case 'r':
c = '\r';
break;
case 't':
c = '\t';
break;
case 'v':
c = '\v';
break;
case 'N':
attribute[0] = '\0'; /* just to be safe */
*isnull = (bool) true;
break;
case '.':
c = getc(fp);
if (c != '\n')
elog(WARN, "CopyReadAttribute - end of record marker corrupted");
return(NULL);
break;
}
}else if (inString(c,delim) || c == '\n') {
done = 1;
}
if (!done) attribute[i++] = c;
if (i == EXT_ATTLEN - 1)
elog(WARN, "CopyReadAttribute - attribute length too long");
}
attribute[i] = '\0';
return(&attribute[0]);
}
static void
CopyAttributeOut(FILE *fp, char *string, char *delim)
{
char c;
int is_array = false;
int len = strlen(string);
/* XXX - This is a kludge, we should check the data type */
if (len && (string[0] == '{') && (string[len-1] == '}'))
is_array = true;
for ( ; c = *string; string++) {
if (c == delim[0] || c == '\n' ||
(c == '\\' && !is_array))
fputc('\\', fp);
else
if (c == '\\' && is_array)
if (*(string+1) == '\\') {
/* translate \\ to \\\\ */
fputc('\\', fp);
fputc('\\', fp);
fputc('\\', fp);
string++;
} else if (*(string+1) == '"') {
/* translate \" to \\\" */
fputc('\\', fp);
fputc('\\', fp);
}
fputc(*string, fp);
}
}
/*
* Returns the number of tuples in a relation. Unfortunately, currently
* must do a scan of the entire relation to determine this.
*
* relation is expected to be an open relation descriptor.
*/
static int
CountTuples(Relation relation)
{
HeapScanDesc scandesc;
HeapTuple tuple;
int i;
scandesc = heap_beginscan(relation, 0, NULL, 0, NULL);
for (tuple = heap_getnext(scandesc, 0, NULL), i = 0;
tuple != NULL;
tuple = heap_getnext(scandesc, 0, NULL), i++)
;
heap_endscan(scandesc);
return(i);
}