mirror of
				https://github.com/postgres/postgres.git
				synced 2025-10-31 10:30:33 +03:00 
			
		
		
		
	typedef struct {} WordEntryPos;
to
typedef uint16 WordEntryPos
according to http://www.pgsql.ru/db/mw/msg.html?mid=2035188
Require re-fill all tsvector fields and reindex tsvector indexes.
		
	
		
			
				
	
	
		
			556 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
			
		
		
	
	
			556 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
| /*
 | |
|  * stat functions
 | |
|  */
 | |
| 
 | |
| #include "tsvector.h"
 | |
| #include "ts_stat.h"
 | |
| #include "funcapi.h"
 | |
| #include "catalog/pg_type.h"
 | |
| #include "executor/spi.h"
 | |
| #include "common.h"
 | |
| 
 | |
| PG_FUNCTION_INFO_V1(tsstat_in);
 | |
| Datum		tsstat_in(PG_FUNCTION_ARGS);
 | |
| Datum
 | |
| tsstat_in(PG_FUNCTION_ARGS)
 | |
| {
 | |
| 	tsstat	   *stat = palloc(STATHDRSIZE);
 | |
| 
 | |
| 	stat->len = STATHDRSIZE;
 | |
| 	stat->size = 0;
 | |
| 	stat->weight = 0;
 | |
| 	PG_RETURN_POINTER(stat);
 | |
| }
 | |
| 
 | |
| PG_FUNCTION_INFO_V1(tsstat_out);
 | |
| Datum		tsstat_out(PG_FUNCTION_ARGS);
 | |
| Datum
 | |
| tsstat_out(PG_FUNCTION_ARGS)
 | |
| {
 | |
| 	ereport(ERROR,
 | |
| 			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 | |
| 			 errmsg("tsstat_out not implemented")));
 | |
| 	PG_RETURN_NULL();
 | |
| }
 | |
| 
 | |
| static int
 | |
| check_weight(tsvector * txt, WordEntry * wptr, int8 weight)
 | |
| {
 | |
| 	int			len = POSDATALEN(txt, wptr);
 | |
| 	int			num = 0;
 | |
| 	WordEntryPos *ptr = POSDATAPTR(txt, wptr);
 | |
| 
 | |
| 	while (len--)
 | |
| 	{
 | |
| 		if (weight & (1 << WEP_GETWEIGHT(*ptr)))
 | |
| 			num++;
 | |
| 		ptr++;
 | |
| 	}
 | |
| 	return num;
 | |
| }
 | |
| 
 | |
| static WordEntry **
 | |
| SEI_realloc(WordEntry ** in, uint32 *len)
 | |
| {
 | |
| 	if (*len == 0 || in == NULL)
 | |
| 	{
 | |
| 		*len = 8;
 | |
| 		in = palloc(sizeof(WordEntry *) * (*len));
 | |
| 	}
 | |
| 	else
 | |
| 	{
 | |
| 		*len *= 2;
 | |
| 		in = repalloc(in, sizeof(WordEntry *) * (*len));
 | |
| 	}
 | |
| 	return in;
 | |
| }
 | |
| 
 | |
| static int
 | |
| compareStatWord(StatEntry * a, WordEntry * b, tsstat * stat, tsvector * txt)
 | |
| {
 | |
| 	if (a->len == b->len)
 | |
| 		return strncmp(
 | |
| 					   STATSTRPTR(stat) + a->pos,
 | |
| 					   STRPTR(txt) + b->pos,
 | |
| 					   a->len
 | |
| 			);
 | |
| 	return (a->len > b->len) ? 1 : -1;
 | |
| }
 | |
| 
 | |
| static tsstat *
 | |
| formstat(tsstat * stat, tsvector * txt, WordEntry ** entry, uint32 len)
 | |
| {
 | |
| 	tsstat	   *newstat;
 | |
| 	uint32		totallen,
 | |
| 				nentry;
 | |
| 	uint32		slen = 0;
 | |
| 	WordEntry **ptr = entry;
 | |
| 	char	   *curptr;
 | |
| 	StatEntry  *sptr,
 | |
| 			   *nptr;
 | |
| 
 | |
| 	while (ptr - entry < len)
 | |
| 	{
 | |
| 		slen += (*ptr)->len;
 | |
| 		ptr++;
 | |
| 	}
 | |
| 
 | |
| 	nentry = stat->size + len;
 | |
| 	slen += STATSTRSIZE(stat);
 | |
| 	totallen = CALCSTATSIZE(nentry, slen);
 | |
| 	newstat = palloc(totallen);
 | |
| 	newstat->len = totallen;
 | |
| 	newstat->weight = stat->weight;
 | |
| 	newstat->size = nentry;
 | |
| 
 | |
| 	memcpy(STATSTRPTR(newstat), STATSTRPTR(stat), STATSTRSIZE(stat));
 | |
| 	curptr = STATSTRPTR(newstat) + STATSTRSIZE(stat);
 | |
| 
 | |
| 	ptr = entry;
 | |
| 	sptr = STATPTR(stat);
 | |
| 	nptr = STATPTR(newstat);
 | |
| 
 | |
| 	if (len == 1)
 | |
| 	{
 | |
| 		StatEntry  *StopLow = STATPTR(stat);
 | |
| 		StatEntry  *StopHigh = (StatEntry *) STATSTRPTR(stat);
 | |
| 
 | |
| 		while (StopLow < StopHigh)
 | |
| 		{
 | |
| 			sptr = StopLow + (StopHigh - StopLow) / 2;
 | |
| 			if (compareStatWord(sptr, *ptr, stat, txt) < 0)
 | |
| 				StopLow = sptr + 1;
 | |
| 			else
 | |
| 				StopHigh = sptr;
 | |
| 		}
 | |
| 		nptr = STATPTR(newstat) + (StopLow - STATPTR(stat));
 | |
| 		memcpy(STATPTR(newstat), STATPTR(stat), sizeof(StatEntry) * (StopLow - STATPTR(stat)));
 | |
| 		if ((*ptr)->haspos)
 | |
| 			nptr->nentry = (stat->weight) ? check_weight(txt, *ptr, stat->weight) : POSDATALEN(txt, *ptr);
 | |
| 		else
 | |
| 			nptr->nentry = 1;
 | |
| 		nptr->ndoc = 1;
 | |
| 		nptr->len = (*ptr)->len;
 | |
| 		memcpy(curptr, STRPTR(txt) + (*ptr)->pos, nptr->len);
 | |
| 		nptr->pos = curptr - STATSTRPTR(newstat);
 | |
| 		memcpy(nptr + 1, StopLow, sizeof(StatEntry) * (((StatEntry *) STATSTRPTR(stat)) - StopLow));
 | |
| 	}
 | |
| 	else
 | |
| 	{
 | |
| 		while (sptr - STATPTR(stat) < stat->size && ptr - entry < len)
 | |
| 		{
 | |
| 			if (compareStatWord(sptr, *ptr, stat, txt) < 0)
 | |
| 			{
 | |
| 				memcpy(nptr, sptr, sizeof(StatEntry));
 | |
| 				sptr++;
 | |
| 			}
 | |
| 			else
 | |
| 			{
 | |
| 				if ((*ptr)->haspos)
 | |
| 					nptr->nentry = (stat->weight) ? check_weight(txt, *ptr, stat->weight) : POSDATALEN(txt, *ptr);
 | |
| 				else
 | |
| 					nptr->nentry = 1;
 | |
| 				nptr->ndoc = 1;
 | |
| 				nptr->len = (*ptr)->len;
 | |
| 				memcpy(curptr, STRPTR(txt) + (*ptr)->pos, nptr->len);
 | |
| 				nptr->pos = curptr - STATSTRPTR(newstat);
 | |
| 				curptr += nptr->len;
 | |
| 				ptr++;
 | |
| 			}
 | |
| 			nptr++;
 | |
| 		}
 | |
| 
 | |
| 		memcpy(nptr, sptr, sizeof(StatEntry) * (stat->size - (sptr - STATPTR(stat))));
 | |
| 
 | |
| 		while (ptr - entry < len)
 | |
| 		{
 | |
| 			if ((*ptr)->haspos)
 | |
| 				nptr->nentry = (stat->weight) ? check_weight(txt, *ptr, stat->weight) : POSDATALEN(txt, *ptr);
 | |
| 			else
 | |
| 				nptr->nentry = 1;
 | |
| 			nptr->ndoc = 1;
 | |
| 			nptr->len = (*ptr)->len;
 | |
| 			memcpy(curptr, STRPTR(txt) + (*ptr)->pos, nptr->len);
 | |
| 			nptr->pos = curptr - STATSTRPTR(newstat);
 | |
| 			curptr += nptr->len;
 | |
| 			ptr++;
 | |
| 			nptr++;
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return newstat;
 | |
| }
 | |
| 
 | |
| PG_FUNCTION_INFO_V1(ts_accum);
 | |
| Datum		ts_accum(PG_FUNCTION_ARGS);
 | |
| Datum
 | |
| ts_accum(PG_FUNCTION_ARGS)
 | |
| {
 | |
| 	tsstat	   *newstat,
 | |
| 			   *stat = (tsstat *) PG_GETARG_POINTER(0);
 | |
| 	tsvector   *txt = (tsvector *) PG_DETOAST_DATUM(PG_GETARG_DATUM(1));
 | |
| 	WordEntry **newentry = NULL;
 | |
| 	uint32		len = 0,
 | |
| 				cur = 0;
 | |
| 	StatEntry  *sptr;
 | |
| 	WordEntry  *wptr;
 | |
| 	int			n = 0;
 | |
| 
 | |
| 	if (stat == NULL || PG_ARGISNULL(0))
 | |
| 	{							/* Init in first */
 | |
| 		stat = palloc(STATHDRSIZE);
 | |
| 		stat->len = STATHDRSIZE;
 | |
| 		stat->size = 0;
 | |
| 		stat->weight = 0;
 | |
| 	}
 | |
| 
 | |
| 	/* simple check of correctness */
 | |
| 	if (txt == NULL || PG_ARGISNULL(1) || txt->size == 0)
 | |
| 	{
 | |
| 		PG_FREE_IF_COPY(txt, 1);
 | |
| 		PG_RETURN_POINTER(stat);
 | |
| 	}
 | |
| 
 | |
| 	sptr = STATPTR(stat);
 | |
| 	wptr = ARRPTR(txt);
 | |
| 
 | |
| 	if (stat->size < 100 * txt->size)
 | |
| 	{							/* merge */
 | |
| 		while (sptr - STATPTR(stat) < stat->size && wptr - ARRPTR(txt) < txt->size)
 | |
| 		{
 | |
| 			int			cmp = compareStatWord(sptr, wptr, stat, txt);
 | |
| 
 | |
| 			if (cmp < 0)
 | |
| 				sptr++;
 | |
| 			else if (cmp == 0)
 | |
| 			{
 | |
| 				if (stat->weight == 0)
 | |
| 				{
 | |
| 					sptr->ndoc++;
 | |
| 					sptr->nentry += (wptr->haspos) ? POSDATALEN(txt, wptr) : 1;
 | |
| 				}
 | |
| 				else if (wptr->haspos && (n = check_weight(txt, wptr, stat->weight)) != 0)
 | |
| 				{
 | |
| 					sptr->ndoc++;
 | |
| 					sptr->nentry += n;
 | |
| 				}
 | |
| 				sptr++;
 | |
| 				wptr++;
 | |
| 			}
 | |
| 			else
 | |
| 			{
 | |
| 				if (stat->weight == 0 || check_weight(txt, wptr, stat->weight) != 0)
 | |
| 				{
 | |
| 					if (cur == len)
 | |
| 						newentry = SEI_realloc(newentry, &len);
 | |
| 					newentry[cur] = wptr;
 | |
| 					cur++;
 | |
| 				}
 | |
| 				wptr++;
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		while (wptr - ARRPTR(txt) < txt->size)
 | |
| 		{
 | |
| 			if (stat->weight == 0 || check_weight(txt, wptr, stat->weight) != 0)
 | |
| 			{
 | |
| 				if (cur == len)
 | |
| 					newentry = SEI_realloc(newentry, &len);
 | |
| 				newentry[cur] = wptr;
 | |
| 				cur++;
 | |
| 			}
 | |
| 			wptr++;
 | |
| 		}
 | |
| 	}
 | |
| 	else
 | |
| 	{							/* search */
 | |
| 		while (wptr - ARRPTR(txt) < txt->size)
 | |
| 		{
 | |
| 			StatEntry  *StopLow = STATPTR(stat);
 | |
| 			StatEntry  *StopHigh = (StatEntry *) STATSTRPTR(stat);
 | |
| 			int			cmp;
 | |
| 
 | |
| 			while (StopLow < StopHigh)
 | |
| 			{
 | |
| 				sptr = StopLow + (StopHigh - StopLow) / 2;
 | |
| 				cmp = compareStatWord(sptr, wptr, stat, txt);
 | |
| 				if (cmp == 0)
 | |
| 				{
 | |
| 					if (stat->weight == 0)
 | |
| 					{
 | |
| 						sptr->ndoc++;
 | |
| 						sptr->nentry += (wptr->haspos) ? POSDATALEN(txt, wptr) : 1;
 | |
| 					}
 | |
| 					else if (wptr->haspos && (n = check_weight(txt, wptr, stat->weight)) != 0)
 | |
| 					{
 | |
| 						sptr->ndoc++;
 | |
| 						sptr->nentry += n;
 | |
| 					}
 | |
| 					break;
 | |
| 				}
 | |
| 				else if (cmp < 0)
 | |
| 					StopLow = sptr + 1;
 | |
| 				else
 | |
| 					StopHigh = sptr;
 | |
| 			}
 | |
| 
 | |
| 			if (StopLow >= StopHigh)
 | |
| 			{					/* not found */
 | |
| 				if (stat->weight == 0 || check_weight(txt, wptr, stat->weight) != 0)
 | |
| 				{
 | |
| 					if (cur == len)
 | |
| 						newentry = SEI_realloc(newentry, &len);
 | |
| 					newentry[cur] = wptr;
 | |
| 					cur++;
 | |
| 				}
 | |
| 			}
 | |
| 			wptr++;
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 
 | |
| 	if (cur == 0)
 | |
| 	{							/* no new words */
 | |
| 		PG_FREE_IF_COPY(txt, 1);
 | |
| 		PG_RETURN_POINTER(stat);
 | |
| 	}
 | |
| 
 | |
| 	newstat = formstat(stat, txt, newentry, cur);
 | |
| 	pfree(newentry);
 | |
| 	PG_FREE_IF_COPY(txt, 1);
 | |
| 	/* pfree(stat); */
 | |
| 
 | |
| 	PG_RETURN_POINTER(newstat);
 | |
| }
 | |
| 
 | |
| typedef struct
 | |
| {
 | |
| 	uint32		cur;
 | |
| 	tsvector   *stat;
 | |
| }	StatStorage;
 | |
| 
 | |
| static void
 | |
| ts_setup_firstcall(FuncCallContext *funcctx, tsstat * stat)
 | |
| {
 | |
| 	TupleDesc	tupdesc;
 | |
| 	MemoryContext oldcontext;
 | |
| 	StatStorage *st;
 | |
| 
 | |
| 	oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
 | |
| 	st = palloc(sizeof(StatStorage));
 | |
| 	st->cur = 0;
 | |
| 	st->stat = palloc(stat->len);
 | |
| 	memcpy(st->stat, stat, stat->len);
 | |
| 	funcctx->user_fctx = (void *) st;
 | |
| 	tupdesc = RelationNameGetTupleDesc("statinfo");
 | |
| 	funcctx->attinmeta = TupleDescGetAttInMetadata(tupdesc);
 | |
| 	MemoryContextSwitchTo(oldcontext);
 | |
| }
 | |
| 
 | |
| 
 | |
| static Datum
 | |
| ts_process_call(FuncCallContext *funcctx)
 | |
| {
 | |
| 	StatStorage *st;
 | |
| 
 | |
| 	st = (StatStorage *) funcctx->user_fctx;
 | |
| 
 | |
| 	if (st->cur < st->stat->size)
 | |
| 	{
 | |
| 		Datum		result;
 | |
| 		char	   *values[3];
 | |
| 		char		ndoc[16];
 | |
| 		char		nentry[16];
 | |
| 		StatEntry  *entry = STATPTR(st->stat) + st->cur;
 | |
| 		HeapTuple	tuple;
 | |
| 
 | |
| 		values[1] = ndoc;
 | |
| 		sprintf(ndoc, "%d", entry->ndoc);
 | |
| 		values[2] = nentry;
 | |
| 		sprintf(nentry, "%d", entry->nentry);
 | |
| 		values[0] = palloc(entry->len + 1);
 | |
| 		memcpy(values[0], STATSTRPTR(st->stat) + entry->pos, entry->len);
 | |
| 		(values[0])[entry->len] = '\0';
 | |
| 
 | |
| 		tuple = BuildTupleFromCStrings(funcctx->attinmeta, values);
 | |
| 		result = HeapTupleGetDatum(tuple);
 | |
| 
 | |
| 		pfree(values[0]);
 | |
| 		st->cur++;
 | |
| 		return result;
 | |
| 	}
 | |
| 	else
 | |
| 	{
 | |
| 		pfree(st->stat);
 | |
| 		pfree(st);
 | |
| 	}
 | |
| 
 | |
| 	return (Datum) 0;
 | |
| }
 | |
| 
 | |
| PG_FUNCTION_INFO_V1(ts_accum_finish);
 | |
| Datum		ts_accum_finish(PG_FUNCTION_ARGS);
 | |
| Datum
 | |
| ts_accum_finish(PG_FUNCTION_ARGS)
 | |
| {
 | |
| 	FuncCallContext *funcctx;
 | |
| 	Datum		result;
 | |
| 
 | |
| 	if (SRF_IS_FIRSTCALL())
 | |
| 	{
 | |
| 		funcctx = SRF_FIRSTCALL_INIT();
 | |
| 		ts_setup_firstcall(funcctx, (tsstat *) PG_GETARG_POINTER(0));
 | |
| 	}
 | |
| 
 | |
| 	funcctx = SRF_PERCALL_SETUP();
 | |
| 	if ((result = ts_process_call(funcctx)) != (Datum) 0)
 | |
| 		SRF_RETURN_NEXT(funcctx, result);
 | |
| 	SRF_RETURN_DONE(funcctx);
 | |
| }
 | |
| 
 | |
| static Oid	tiOid = InvalidOid;
 | |
| static void
 | |
| get_ti_Oid(void)
 | |
| {
 | |
| 	int			ret;
 | |
| 	bool		isnull;
 | |
| 
 | |
| 	if ((ret = SPI_exec("select oid from pg_type where typname='tsvector'", 1)) < 0)
 | |
| 		/* internal error */
 | |
| 		elog(ERROR, "SPI_exec to get tsvector oid returns %d", ret);
 | |
| 
 | |
| 	if (SPI_processed < 0)
 | |
| 		/* internal error */
 | |
| 		elog(ERROR, "There is no tsvector type");
 | |
| 	tiOid = DatumGetObjectId(SPI_getbinval(SPI_tuptable->vals[0], SPI_tuptable->tupdesc, 1, &isnull));
 | |
| 	if (tiOid == InvalidOid)
 | |
| 		/* internal error */
 | |
| 		elog(ERROR, "tsvector type has InvalidOid");
 | |
| }
 | |
| 
 | |
| static tsstat *
 | |
| ts_stat_sql(text *txt, text *ws)
 | |
| {
 | |
| 	char	   *query = text2char(txt);
 | |
| 	int			i;
 | |
| 	tsstat	   *newstat,
 | |
| 			   *stat;
 | |
| 	bool		isnull;
 | |
| 	Portal		portal;
 | |
| 	void	   *plan;
 | |
| 
 | |
| 	if (tiOid == InvalidOid)
 | |
| 		get_ti_Oid();
 | |
| 
 | |
| 	if ((plan = SPI_prepare(query, 0, NULL)) == NULL)
 | |
| 		/* internal error */
 | |
| 		elog(ERROR, "SPI_prepare('%s') returns NULL", query);
 | |
| 
 | |
| 	if ((portal = SPI_cursor_open(NULL, plan, NULL, NULL, false)) == NULL)
 | |
| 		/* internal error */
 | |
| 		elog(ERROR, "SPI_cursor_open('%s') returns NULL", query);
 | |
| 
 | |
| 	SPI_cursor_fetch(portal, true, 100);
 | |
| 
 | |
| 	if (SPI_tuptable->tupdesc->natts != 1)
 | |
| 		/* internal error */
 | |
| 		elog(ERROR, "number of fields doesn't equal to 1");
 | |
| 
 | |
| 	if (SPI_gettypeid(SPI_tuptable->tupdesc, 1) != tiOid)
 | |
| 		/* internal error */
 | |
| 		elog(ERROR, "column isn't of tsvector type");
 | |
| 
 | |
| 	stat = palloc(STATHDRSIZE);
 | |
| 	stat->len = STATHDRSIZE;
 | |
| 	stat->size = 0;
 | |
| 	stat->weight = 0;
 | |
| 
 | |
| 	if (ws)
 | |
| 	{
 | |
| 		char	   *buf;
 | |
| 
 | |
| 		buf = VARDATA(ws);
 | |
| 		while (buf - VARDATA(ws) < VARSIZE(ws) - VARHDRSZ)
 | |
| 		{
 | |
| 			switch (tolower(*buf))
 | |
| 			{
 | |
| 				case 'a':
 | |
| 					stat->weight |= 1 << 3;
 | |
| 					break;
 | |
| 				case 'b':
 | |
| 					stat->weight |= 1 << 2;
 | |
| 					break;
 | |
| 				case 'c':
 | |
| 					stat->weight |= 1 << 1;
 | |
| 					break;
 | |
| 				case 'd':
 | |
| 					stat->weight |= 1;
 | |
| 					break;
 | |
| 				default:
 | |
| 					stat->weight |= 0;
 | |
| 			}
 | |
| 			buf++;
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	while (SPI_processed > 0)
 | |
| 	{
 | |
| 		for (i = 0; i < SPI_processed; i++)
 | |
| 		{
 | |
| 			Datum		data = SPI_getbinval(SPI_tuptable->vals[i], SPI_tuptable->tupdesc, 1, &isnull);
 | |
| 
 | |
| 			if (!isnull)
 | |
| 			{
 | |
| 				newstat = (tsstat *) DatumGetPointer(DirectFunctionCall2(
 | |
| 																ts_accum,
 | |
| 												   PointerGetDatum(stat),
 | |
| 																	 data
 | |
| 																	  ));
 | |
| 				if (stat != newstat && stat)
 | |
| 					pfree(stat);
 | |
| 				stat = newstat;
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		SPI_freetuptable(SPI_tuptable);
 | |
| 		SPI_cursor_fetch(portal, true, 100);
 | |
| 	}
 | |
| 
 | |
| 	SPI_freetuptable(SPI_tuptable);
 | |
| 	SPI_cursor_close(portal);
 | |
| 	SPI_freeplan(plan);
 | |
| 	pfree(query);
 | |
| 
 | |
| 	return stat;
 | |
| }
 | |
| 
 | |
| PG_FUNCTION_INFO_V1(ts_stat);
 | |
| Datum		ts_stat(PG_FUNCTION_ARGS);
 | |
| Datum
 | |
| ts_stat(PG_FUNCTION_ARGS)
 | |
| {
 | |
| 	FuncCallContext *funcctx;
 | |
| 	Datum		result;
 | |
| 
 | |
| 	if (SRF_IS_FIRSTCALL())
 | |
| 	{
 | |
| 		tsstat	   *stat;
 | |
| 		text	   *txt = PG_GETARG_TEXT_P(0);
 | |
| 		text	   *ws = (PG_NARGS() > 1) ? PG_GETARG_TEXT_P(1) : NULL;
 | |
| 
 | |
| 		funcctx = SRF_FIRSTCALL_INIT();
 | |
| 		SPI_connect();
 | |
| 		stat = ts_stat_sql(txt, ws);
 | |
| 		PG_FREE_IF_COPY(txt, 0);
 | |
| 		if (PG_NARGS() > 1)
 | |
| 			PG_FREE_IF_COPY(ws, 1);
 | |
| 		ts_setup_firstcall(funcctx, stat);
 | |
| 		SPI_finish();
 | |
| 	}
 | |
| 
 | |
| 	funcctx = SRF_PERCALL_SETUP();
 | |
| 	if ((result = ts_process_call(funcctx)) != (Datum) 0)
 | |
| 		SRF_RETURN_NEXT(funcctx, result);
 | |
| 	SRF_RETURN_DONE(funcctx);
 | |
| }
 |