5
#include "executor/spi.h"
6
#include "commands/trigger.h"
9
* Trigger function accepts variable number of arguments:
11
* 1. relation in which to store the substrings
12
* 2. fields to extract substrings from
14
* The relation in which to insert *must* have the following layout:
19
* where # is the largest size of the varchar columns being indexed
23
* -- Create the SQL function based on the compiled shared object
24
* create function fti() returns trigger as
25
* '/usr/local/pgsql/lib/contrib/fti.so' language 'C';
27
* -- Create the FTI table
28
* create table product_fti (string varchar(255), id oid) without oids;
30
* -- Create an index to assist string matches
31
* create index product_fti_string_idx on product_fti (string);
33
* -- Create an index to assist trigger'd deletes
34
* create index product_fti_id_idx on product_fti (id);
36
* -- Create an index on the product oid column to assist joins
37
* -- between the fti table and the product table
38
* create index product_oid_idx on product (oid);
40
* -- Create the trigger to perform incremental changes to the full text index.
41
* create trigger product_fti_trig after update or insert or delete on product
42
* for each row execute procedure fti(product_fti, title, artist);
44
* table where full text index is stored
46
* columns to index in the base table
48
* After populating 'product', try something like:
50
* SELECT DISTINCT(p.*) FROM product p, product_fti f1, product_fti f2 WHERE
51
* f1.string ~ '^slippery' AND f2.string ~ '^wet' AND p.oid=f1.id AND p.oid=f2.id;
53
* To check that your indicies are being used correctly, make sure you
54
* EXPLAIN SELECT ... your test query above.
60
* Extended fti function to accept more than one column as a
61
* parameter and all specified columns are indexed. Changed
62
* all uses of sprintf to snprintf. Made error messages more
65
* march 4 1998 Changed breakup() to return less substrings. Only breakup
66
* in word parts which are in turn shortened from the start
67
* of the word (ie. word, ord, rd)
68
* Did allocation of substring buffer outside of breakup()
70
* oct. 5 1997, fixed a bug in string breakup (where there are more nonalpha
71
* characters between words then 1).
73
* oct 4-5 1997 implemented the thing, at least the basic functionallity
79
* prevent generating duplicate words for an oid in the fti table
80
* save a plan for deletes
81
* create a function that will make the index *after* we have populated
82
* the main table (probably first delete all contents to be sure there's
83
* nothing in it, then re-populate the fti-table)
85
* can we do something with operator overloading or a seperate function
86
* that can build the final query automagically?
89
#define MAX_FTI_QUERY_LENGTH 8192
91
extern Datum fti(PG_FUNCTION_ARGS);
92
static char *breakup(char *, char *);
93
static bool is_stopword(char *);
95
static bool new_tuple = false;
100
/* THIS LIST MUST BE IN SORTED ORDER, A BINARY SEARCH IS USED!!!! */
101
char *StopWords[] = { /* list of words to skip in indexing */
106
#endif /* USE_STOP_WORDS */
108
/* stuff for caching query-plans, stolen from contrib/spi/\*.c */
116
static EPlan *InsertPlans = NULL;
117
static EPlan *DeletePlans = NULL;
118
static int nInsertPlans = 0;
119
static int nDeletePlans = 0;
121
static EPlan *find_plan(char *ident, EPlan ** eplan, int *nplans);
123
/***********************************************************************/
124
PG_FUNCTION_INFO_V1(fti);
127
fti(PG_FUNCTION_ARGS)
129
TriggerData *trigdata;
130
Trigger *trigger; /* to get trigger name */
131
int nargs; /* # of arguments */
132
char **args; /* arguments */
133
char *relname; /* triggered relation name */
134
Relation rel; /* triggered relation */
135
char *indexname; /* name of table for substrings */
136
HeapTuple rettuple = NULL;
137
TupleDesc tupdesc; /* tuple description */
138
bool isinsert = false;
139
bool isdelete = false;
141
char query[MAX_FTI_QUERY_LENGTH];
149
* debug = fopen("/dev/xconsole", "w"); fprintf(debug, "FTI: entered
150
* function\n"); fflush(debug);
153
if (!CALLED_AS_TRIGGER(fcinfo))
155
elog(ERROR, "not fired by trigger manager");
157
/* It's safe to cast now that we've checked */
158
trigdata = (TriggerData *) fcinfo->context;
160
if (TRIGGER_FIRED_FOR_STATEMENT(trigdata->tg_event))
162
(errcode(ERRCODE_TRIGGERED_ACTION_EXCEPTION),
163
errmsg("can't process STATEMENT events")));
165
if (TRIGGER_FIRED_BEFORE(trigdata->tg_event))
167
(errcode(ERRCODE_TRIGGERED_ACTION_EXCEPTION),
168
errmsg("must be fired AFTER event")));
170
if (TRIGGER_FIRED_BY_INSERT(trigdata->tg_event))
172
if (TRIGGER_FIRED_BY_UPDATE(trigdata->tg_event))
177
if (TRIGGER_FIRED_BY_DELETE(trigdata->tg_event))
180
trigger = trigdata->tg_trigger;
181
rel = trigdata->tg_relation;
182
relname = SPI_getrelname(rel);
183
rettuple = trigdata->tg_trigtuple;
184
if (isdelete && isinsert) /* is an UPDATE */
185
rettuple = trigdata->tg_newtuple;
187
if ((ret = SPI_connect()) < 0)
189
elog(ERROR, "SPI_connect failed, returned %d", ret);
191
nargs = trigger->tgnargs;
194
(errcode(ERRCODE_TRIGGERED_ACTION_EXCEPTION),
195
errmsg("fti trigger must have at least 2 arguments")));
197
args = trigger->tgargs;
199
tupdesc = rel->rd_att; /* what the tuple looks like (?) */
201
/* get oid of current tuple, needed by all, so place here */
202
oid = HeapTupleGetOid(rettuple);
203
if (!OidIsValid(oid))
205
(errcode(ERRCODE_UNDEFINED_COLUMN),
206
errmsg("OID is not present"),
207
errhint("Full Text Index requires indexed tables be created WITH OIDS.")));
217
snprintf(query, MAX_FTI_QUERY_LENGTH, "D%s", indexname);
218
for (i = 1; i < nargs; i++)
219
snprintf(query, MAX_FTI_QUERY_LENGTH, "%s$%s", query, args[i]);
221
plan = find_plan(query, &DeletePlans, &nDeletePlans);
222
if (plan->nplans <= 0)
224
argtypes = (Oid *) palloc(sizeof(Oid));
226
argtypes[0] = OIDOID;
228
snprintf(query, MAX_FTI_QUERY_LENGTH, "DELETE FROM %s WHERE id = $1", indexname);
229
pplan = SPI_prepare(query, 1, argtypes);
232
elog(ERROR, "SPI_prepare returned NULL in delete");
233
pplan = SPI_saveplan(pplan);
236
elog(ERROR, "SPI_saveplan returned NULL in delete");
238
plan->splan = (void **) malloc(sizeof(void *));
239
*(plan->splan) = pplan;
245
ret = SPI_execp(*(plan->splan), values, NULL, 0);
246
if (ret != SPI_OK_DELETE)
248
(errcode(ERRCODE_TRIGGERED_ACTION_EXCEPTION),
249
errmsg("error executing delete")));
260
struct varlena *data;
266
snprintf(query, MAX_FTI_QUERY_LENGTH, "I%s", indexname);
267
for (i = 1; i < nargs; i++)
268
snprintf(query, MAX_FTI_QUERY_LENGTH, "%s$%s", query, args[i]);
270
plan = find_plan(query, &InsertPlans, &nInsertPlans);
272
/* no plan yet, so allocate mem for argtypes */
273
if (plan->nplans <= 0)
275
argtypes = (Oid *) palloc(2 * sizeof(Oid));
277
argtypes[0] = VARCHAROID; /* create table t_name (string
279
argtypes[1] = OIDOID; /* id oid); */
281
/* prepare plan to gain speed */
282
snprintf(query, MAX_FTI_QUERY_LENGTH, "INSERT INTO %s (string, id) VALUES ($1, $2)",
284
pplan = SPI_prepare(query, 2, argtypes);
287
elog(ERROR, "SPI_prepare returned NULL in insert");
289
pplan = SPI_saveplan(pplan);
292
elog(ERROR, "SPI_saveplan returned NULL in insert");
294
plan->splan = (void **) malloc(sizeof(void *));
295
*(plan->splan) = pplan;
299
/* prepare plan for query */
300
for (i = 0; i < nargs - 1; i++)
302
colnum = SPI_fnumber(tupdesc, args[i + 1]);
303
if (colnum == SPI_ERROR_NOATTRIBUTE)
305
(errcode(ERRCODE_UNDEFINED_COLUMN),
306
errmsg("column \"%s\" of \"%s\" does not exist",
307
args[i + 1], indexname)));
309
/* Get the char* representation of the column */
310
column = SPI_getvalue(rettuple, tupdesc, colnum);
312
/* make sure we don't try to index NULL's */
316
while (*string != '\0')
318
*string = tolower((unsigned char) *string);
322
data = (struct varlena *) palloc(sizeof(int32) + strlen(column) +1);
323
buff = palloc(strlen(column) + 1);
324
/* saves lots of calls in while-loop and in breakup() */
328
while ((substring = breakup(column, buff)))
332
l = strlen(substring);
334
data->vl_len = l + sizeof(int32);
335
memcpy(VARDATA(data), substring, l);
336
values[0] = PointerGetDatum(data);
339
ret = SPI_execp(*(plan->splan), values, NULL, 0);
340
if (ret != SPI_OK_INSERT)
342
(errcode(ERRCODE_TRIGGERED_ACTION_EXCEPTION),
343
errmsg("error executing insert")));
352
return PointerGetDatum(rettuple);
356
breakup(char *string, char *substring)
358
static char *last_start;
359
static char *cur_pos;
363
cur_pos = last_start = &string[strlen(string) - 1];
364
new_tuple = false; /* don't initialize this next time */
367
while (cur_pos > string) /* don't read before start of 'string' */
370
* skip pieces at the end of a string that are not alfa-numeric
371
* (ie. 'string$%^&', last_start first points to '&', and after
374
if (!isalnum((unsigned char) *last_start))
376
while (!isalnum((unsigned char) *last_start) &&
379
cur_pos = last_start;
382
cur_pos--; /* substrings are at minimum 2 characters
385
if (isalnum((unsigned char) *cur_pos))
387
/* Houston, we have a substring! :) */
388
memcpy(substring, cur_pos, last_start - cur_pos + 1);
389
substring[last_start - cur_pos + 1] = '\0';
390
if (!is_stopword(substring))
395
last_start = cur_pos - 1;
396
cur_pos = last_start;
400
return NULL; /* we've processed all of 'string' */
403
/* copied from src/backend/parser/keywords.c and adjusted for our situation*/
405
is_stopword(char *text)
407
#ifdef USE_STOP_WORDS
408
char **StopLow; /* for list of stop-words */
413
StopLow = &StopWords[0]; /* initialize stuff for binary search */
414
StopHigh = endof(StopWords);
416
/* Loop invariant: *StopLow <= text < *StopHigh */
418
while (StopLow < StopHigh)
420
StopMiddle = StopLow + (StopHigh - StopLow) / 2;
421
difference = strcmp(*StopMiddle, text);
424
else if (difference < 0)
425
StopLow = StopMiddle + 1;
427
StopHigh = StopMiddle;
429
#endif /* USE_STOP_WORDS */
434
/* for caching of query plans, stolen from contrib/spi/\*.c */
436
find_plan(char *ident, EPlan ** eplan, int *nplans)
443
for (i = 0; i < *nplans; i++)
445
if (strcmp((*eplan)[i].ident, ident) == 0)
450
*eplan = (EPlan *) realloc(*eplan, (i + 1) * sizeof(EPlan));
455
newp = *eplan = (EPlan *) malloc(sizeof(EPlan));
459
newp->ident = (char *) malloc(strlen(ident) + 1);
460
strcpy(newp->ident, ident);