1 /*-------------------------------------------------------------------------
2 *
3 * ts_cache.c
4 * Tsearch related object caches.
5 *
6 * Tsearch performance is very sensitive to performance of parsers,
7 * dictionaries and mapping, so lookups should be cached as much
8 * as possible.
9 *
10 * Once a backend has created a cache entry for a particular TS object OID,
11 * the cache entry will exist for the life of the backend; hence it is
12 * safe to hold onto a pointer to the cache entry while doing things that
13 * might result in recognizing a cache invalidation. Beware however that
14 * subsidiary information might be deleted and reallocated somewhere else
15 * if a cache inval and reval happens! This does not look like it will be
16 * a big problem as long as parser and dictionary methods do not attempt
17 * any database access.
18 *
19 *
20 * Copyright (c) 2006-2017, PostgreSQL Global Development Group
21 *
22 * IDENTIFICATION
23 * src/backend/utils/cache/ts_cache.c
24 *
25 *-------------------------------------------------------------------------
26 */
27 #include "postgres.h"
28
29 #include "access/genam.h"
30 #include "access/heapam.h"
31 #include "access/htup_details.h"
32 #include "access/xact.h"
33 #include "catalog/indexing.h"
34 #include "catalog/namespace.h"
35 #include "catalog/pg_ts_config.h"
36 #include "catalog/pg_ts_config_map.h"
37 #include "catalog/pg_ts_dict.h"
38 #include "catalog/pg_ts_parser.h"
39 #include "catalog/pg_ts_template.h"
40 #include "commands/defrem.h"
41 #include "miscadmin.h"
42 #include "tsearch/ts_cache.h"
43 #include "utils/builtins.h"
44 #include "utils/catcache.h"
45 #include "utils/fmgroids.h"
46 #include "utils/inval.h"
47 #include "utils/lsyscache.h"
48 #include "utils/memutils.h"
49 #include "utils/regproc.h"
50 #include "utils/syscache.h"
51 #include "utils/tqual.h"
52
53
54 /*
55 * MAXTOKENTYPE/MAXDICTSPERTT are arbitrary limits on the workspace size
56 * used in lookup_ts_config_cache(). We could avoid hardwiring a limit
57 * by making the workspace dynamically enlargeable, but it seems unlikely
58 * to be worth the trouble.
59 */
60 #define MAXTOKENTYPE 256
61 #define MAXDICTSPERTT 100
62
63
64 static HTAB *TSParserCacheHash = NULL;
65 static TSParserCacheEntry *lastUsedParser = NULL;
66
67 static HTAB *TSDictionaryCacheHash = NULL;
68 static TSDictionaryCacheEntry *lastUsedDictionary = NULL;
69
70 static HTAB *TSConfigCacheHash = NULL;
71 static TSConfigCacheEntry *lastUsedConfig = NULL;
72
73 /*
74 * GUC default_text_search_config, and a cache of the current config's OID
75 */
76 char *TSCurrentConfig = NULL;
77
78 static Oid TSCurrentConfigCache = InvalidOid;
79
80
81 /*
82 * We use this syscache callback to detect when a visible change to a TS
83 * catalog entry has been made, by either our own backend or another one.
84 *
85 * In principle we could just flush the specific cache entry that changed,
86 * but given that TS configuration changes are probably infrequent, it
87 * doesn't seem worth the trouble to determine that; we just flush all the
88 * entries of the related hash table.
89 *
90 * We can use the same function for all TS caches by passing the hash
91 * table address as the "arg".
92 */
93 static void
InvalidateTSCacheCallBack(Datum arg,int cacheid,uint32 hashvalue)94 InvalidateTSCacheCallBack(Datum arg, int cacheid, uint32 hashvalue)
95 {
96 HTAB *hash = (HTAB *) DatumGetPointer(arg);
97 HASH_SEQ_STATUS status;
98 TSAnyCacheEntry *entry;
99
100 hash_seq_init(&status, hash);
101 while ((entry = (TSAnyCacheEntry *) hash_seq_search(&status)) != NULL)
102 entry->isvalid = false;
103
104 /* Also invalidate the current-config cache if it's pg_ts_config */
105 if (hash == TSConfigCacheHash)
106 TSCurrentConfigCache = InvalidOid;
107 }
108
109 /*
110 * Fetch parser cache entry
111 */
112 TSParserCacheEntry *
lookup_ts_parser_cache(Oid prsId)113 lookup_ts_parser_cache(Oid prsId)
114 {
115 TSParserCacheEntry *entry;
116
117 if (TSParserCacheHash == NULL)
118 {
119 /* First time through: initialize the hash table */
120 HASHCTL ctl;
121
122 MemSet(&ctl, 0, sizeof(ctl));
123 ctl.keysize = sizeof(Oid);
124 ctl.entrysize = sizeof(TSParserCacheEntry);
125 TSParserCacheHash = hash_create("Tsearch parser cache", 4,
126 &ctl, HASH_ELEM | HASH_BLOBS);
127 /* Flush cache on pg_ts_parser changes */
128 CacheRegisterSyscacheCallback(TSPARSEROID, InvalidateTSCacheCallBack,
129 PointerGetDatum(TSParserCacheHash));
130
131 /* Also make sure CacheMemoryContext exists */
132 if (!CacheMemoryContext)
133 CreateCacheMemoryContext();
134 }
135
136 /* Check single-entry cache */
137 if (lastUsedParser && lastUsedParser->prsId == prsId &&
138 lastUsedParser->isvalid)
139 return lastUsedParser;
140
141 /* Try to look up an existing entry */
142 entry = (TSParserCacheEntry *) hash_search(TSParserCacheHash,
143 (void *) &prsId,
144 HASH_FIND, NULL);
145 if (entry == NULL || !entry->isvalid)
146 {
147 /*
148 * If we didn't find one, we want to make one. But first look up the
149 * object to be sure the OID is real.
150 */
151 HeapTuple tp;
152 Form_pg_ts_parser prs;
153
154 tp = SearchSysCache1(TSPARSEROID, ObjectIdGetDatum(prsId));
155 if (!HeapTupleIsValid(tp))
156 elog(ERROR, "cache lookup failed for text search parser %u",
157 prsId);
158 prs = (Form_pg_ts_parser) GETSTRUCT(tp);
159
160 /*
161 * Sanity checks
162 */
163 if (!OidIsValid(prs->prsstart))
164 elog(ERROR, "text search parser %u has no prsstart method", prsId);
165 if (!OidIsValid(prs->prstoken))
166 elog(ERROR, "text search parser %u has no prstoken method", prsId);
167 if (!OidIsValid(prs->prsend))
168 elog(ERROR, "text search parser %u has no prsend method", prsId);
169
170 if (entry == NULL)
171 {
172 bool found;
173
174 /* Now make the cache entry */
175 entry = (TSParserCacheEntry *)
176 hash_search(TSParserCacheHash,
177 (void *) &prsId,
178 HASH_ENTER, &found);
179 Assert(!found); /* it wasn't there a moment ago */
180 }
181
182 MemSet(entry, 0, sizeof(TSParserCacheEntry));
183 entry->prsId = prsId;
184 entry->startOid = prs->prsstart;
185 entry->tokenOid = prs->prstoken;
186 entry->endOid = prs->prsend;
187 entry->headlineOid = prs->prsheadline;
188 entry->lextypeOid = prs->prslextype;
189
190 ReleaseSysCache(tp);
191
192 fmgr_info_cxt(entry->startOid, &entry->prsstart, CacheMemoryContext);
193 fmgr_info_cxt(entry->tokenOid, &entry->prstoken, CacheMemoryContext);
194 fmgr_info_cxt(entry->endOid, &entry->prsend, CacheMemoryContext);
195 if (OidIsValid(entry->headlineOid))
196 fmgr_info_cxt(entry->headlineOid, &entry->prsheadline,
197 CacheMemoryContext);
198
199 entry->isvalid = true;
200 }
201
202 lastUsedParser = entry;
203
204 return entry;
205 }
206
207 /*
208 * Fetch dictionary cache entry
209 */
210 TSDictionaryCacheEntry *
lookup_ts_dictionary_cache(Oid dictId)211 lookup_ts_dictionary_cache(Oid dictId)
212 {
213 TSDictionaryCacheEntry *entry;
214
215 if (TSDictionaryCacheHash == NULL)
216 {
217 /* First time through: initialize the hash table */
218 HASHCTL ctl;
219
220 MemSet(&ctl, 0, sizeof(ctl));
221 ctl.keysize = sizeof(Oid);
222 ctl.entrysize = sizeof(TSDictionaryCacheEntry);
223 TSDictionaryCacheHash = hash_create("Tsearch dictionary cache", 8,
224 &ctl, HASH_ELEM | HASH_BLOBS);
225 /* Flush cache on pg_ts_dict and pg_ts_template changes */
226 CacheRegisterSyscacheCallback(TSDICTOID, InvalidateTSCacheCallBack,
227 PointerGetDatum(TSDictionaryCacheHash));
228 CacheRegisterSyscacheCallback(TSTEMPLATEOID, InvalidateTSCacheCallBack,
229 PointerGetDatum(TSDictionaryCacheHash));
230
231 /* Also make sure CacheMemoryContext exists */
232 if (!CacheMemoryContext)
233 CreateCacheMemoryContext();
234 }
235
236 /* Check single-entry cache */
237 if (lastUsedDictionary && lastUsedDictionary->dictId == dictId &&
238 lastUsedDictionary->isvalid)
239 return lastUsedDictionary;
240
241 /* Try to look up an existing entry */
242 entry = (TSDictionaryCacheEntry *) hash_search(TSDictionaryCacheHash,
243 (void *) &dictId,
244 HASH_FIND, NULL);
245 if (entry == NULL || !entry->isvalid)
246 {
247 /*
248 * If we didn't find one, we want to make one. But first look up the
249 * object to be sure the OID is real.
250 */
251 HeapTuple tpdict,
252 tptmpl;
253 Form_pg_ts_dict dict;
254 Form_pg_ts_template template;
255 MemoryContext saveCtx;
256
257 tpdict = SearchSysCache1(TSDICTOID, ObjectIdGetDatum(dictId));
258 if (!HeapTupleIsValid(tpdict))
259 elog(ERROR, "cache lookup failed for text search dictionary %u",
260 dictId);
261 dict = (Form_pg_ts_dict) GETSTRUCT(tpdict);
262
263 /*
264 * Sanity checks
265 */
266 if (!OidIsValid(dict->dicttemplate))
267 elog(ERROR, "text search dictionary %u has no template", dictId);
268
269 /*
270 * Retrieve dictionary's template
271 */
272 tptmpl = SearchSysCache1(TSTEMPLATEOID,
273 ObjectIdGetDatum(dict->dicttemplate));
274 if (!HeapTupleIsValid(tptmpl))
275 elog(ERROR, "cache lookup failed for text search template %u",
276 dict->dicttemplate);
277 template = (Form_pg_ts_template) GETSTRUCT(tptmpl);
278
279 /*
280 * Sanity checks
281 */
282 if (!OidIsValid(template->tmpllexize))
283 elog(ERROR, "text search template %u has no lexize method",
284 template->tmpllexize);
285
286 if (entry == NULL)
287 {
288 bool found;
289
290 /* Now make the cache entry */
291 entry = (TSDictionaryCacheEntry *)
292 hash_search(TSDictionaryCacheHash,
293 (void *) &dictId,
294 HASH_ENTER, &found);
295 Assert(!found); /* it wasn't there a moment ago */
296
297 /* Create private memory context the first time through */
298 saveCtx = AllocSetContextCreate(CacheMemoryContext,
299 NameStr(dict->dictname),
300 ALLOCSET_SMALL_SIZES);
301 }
302 else
303 {
304 /* Clear the existing entry's private context */
305 saveCtx = entry->dictCtx;
306 MemoryContextResetAndDeleteChildren(saveCtx);
307 }
308
309 MemSet(entry, 0, sizeof(TSDictionaryCacheEntry));
310 entry->dictId = dictId;
311 entry->dictCtx = saveCtx;
312
313 entry->lexizeOid = template->tmpllexize;
314
315 if (OidIsValid(template->tmplinit))
316 {
317 List *dictoptions;
318 Datum opt;
319 bool isnull;
320 MemoryContext oldcontext;
321
322 /*
323 * Init method runs in dictionary's private memory context, and we
324 * make sure the options are stored there too
325 */
326 oldcontext = MemoryContextSwitchTo(entry->dictCtx);
327
328 opt = SysCacheGetAttr(TSDICTOID, tpdict,
329 Anum_pg_ts_dict_dictinitoption,
330 &isnull);
331 if (isnull)
332 dictoptions = NIL;
333 else
334 dictoptions = deserialize_deflist(opt);
335
336 entry->dictData =
337 DatumGetPointer(OidFunctionCall1(template->tmplinit,
338 PointerGetDatum(dictoptions)));
339
340 MemoryContextSwitchTo(oldcontext);
341 }
342
343 ReleaseSysCache(tptmpl);
344 ReleaseSysCache(tpdict);
345
346 fmgr_info_cxt(entry->lexizeOid, &entry->lexize, entry->dictCtx);
347
348 entry->isvalid = true;
349 }
350
351 lastUsedDictionary = entry;
352
353 return entry;
354 }
355
356 /*
357 * Initialize config cache and prepare callbacks. This is split out of
358 * lookup_ts_config_cache because we need to activate the callback before
359 * caching TSCurrentConfigCache, too.
360 */
361 static void
init_ts_config_cache(void)362 init_ts_config_cache(void)
363 {
364 HASHCTL ctl;
365
366 MemSet(&ctl, 0, sizeof(ctl));
367 ctl.keysize = sizeof(Oid);
368 ctl.entrysize = sizeof(TSConfigCacheEntry);
369 TSConfigCacheHash = hash_create("Tsearch configuration cache", 16,
370 &ctl, HASH_ELEM | HASH_BLOBS);
371 /* Flush cache on pg_ts_config and pg_ts_config_map changes */
372 CacheRegisterSyscacheCallback(TSCONFIGOID, InvalidateTSCacheCallBack,
373 PointerGetDatum(TSConfigCacheHash));
374 CacheRegisterSyscacheCallback(TSCONFIGMAP, InvalidateTSCacheCallBack,
375 PointerGetDatum(TSConfigCacheHash));
376
377 /* Also make sure CacheMemoryContext exists */
378 if (!CacheMemoryContext)
379 CreateCacheMemoryContext();
380 }
381
382 /*
383 * Fetch configuration cache entry
384 */
385 TSConfigCacheEntry *
lookup_ts_config_cache(Oid cfgId)386 lookup_ts_config_cache(Oid cfgId)
387 {
388 TSConfigCacheEntry *entry;
389
390 if (TSConfigCacheHash == NULL)
391 {
392 /* First time through: initialize the hash table */
393 init_ts_config_cache();
394 }
395
396 /* Check single-entry cache */
397 if (lastUsedConfig && lastUsedConfig->cfgId == cfgId &&
398 lastUsedConfig->isvalid)
399 return lastUsedConfig;
400
401 /* Try to look up an existing entry */
402 entry = (TSConfigCacheEntry *) hash_search(TSConfigCacheHash,
403 (void *) &cfgId,
404 HASH_FIND, NULL);
405 if (entry == NULL || !entry->isvalid)
406 {
407 /*
408 * If we didn't find one, we want to make one. But first look up the
409 * object to be sure the OID is real.
410 */
411 HeapTuple tp;
412 Form_pg_ts_config cfg;
413 Relation maprel;
414 Relation mapidx;
415 ScanKeyData mapskey;
416 SysScanDesc mapscan;
417 HeapTuple maptup;
418 ListDictionary maplists[MAXTOKENTYPE + 1];
419 Oid mapdicts[MAXDICTSPERTT];
420 int maxtokentype;
421 int ndicts;
422 int i;
423
424 tp = SearchSysCache1(TSCONFIGOID, ObjectIdGetDatum(cfgId));
425 if (!HeapTupleIsValid(tp))
426 elog(ERROR, "cache lookup failed for text search configuration %u",
427 cfgId);
428 cfg = (Form_pg_ts_config) GETSTRUCT(tp);
429
430 /*
431 * Sanity checks
432 */
433 if (!OidIsValid(cfg->cfgparser))
434 elog(ERROR, "text search configuration %u has no parser", cfgId);
435
436 if (entry == NULL)
437 {
438 bool found;
439
440 /* Now make the cache entry */
441 entry = (TSConfigCacheEntry *)
442 hash_search(TSConfigCacheHash,
443 (void *) &cfgId,
444 HASH_ENTER, &found);
445 Assert(!found); /* it wasn't there a moment ago */
446 }
447 else
448 {
449 /* Cleanup old contents */
450 if (entry->map)
451 {
452 for (i = 0; i < entry->lenmap; i++)
453 if (entry->map[i].dictIds)
454 pfree(entry->map[i].dictIds);
455 pfree(entry->map);
456 }
457 }
458
459 MemSet(entry, 0, sizeof(TSConfigCacheEntry));
460 entry->cfgId = cfgId;
461 entry->prsId = cfg->cfgparser;
462
463 ReleaseSysCache(tp);
464
465 /*
466 * Scan pg_ts_config_map to gather dictionary list for each token type
467 *
468 * Because the index is on (mapcfg, maptokentype, mapseqno), we will
469 * see the entries in maptokentype order, and in mapseqno order for
470 * each token type, even though we didn't explicitly ask for that.
471 */
472 MemSet(maplists, 0, sizeof(maplists));
473 maxtokentype = 0;
474 ndicts = 0;
475
476 ScanKeyInit(&mapskey,
477 Anum_pg_ts_config_map_mapcfg,
478 BTEqualStrategyNumber, F_OIDEQ,
479 ObjectIdGetDatum(cfgId));
480
481 maprel = heap_open(TSConfigMapRelationId, AccessShareLock);
482 mapidx = index_open(TSConfigMapIndexId, AccessShareLock);
483 mapscan = systable_beginscan_ordered(maprel, mapidx,
484 NULL, 1, &mapskey);
485
486 while ((maptup = systable_getnext_ordered(mapscan, ForwardScanDirection)) != NULL)
487 {
488 Form_pg_ts_config_map cfgmap = (Form_pg_ts_config_map) GETSTRUCT(maptup);
489 int toktype = cfgmap->maptokentype;
490
491 if (toktype <= 0 || toktype > MAXTOKENTYPE)
492 elog(ERROR, "maptokentype value %d is out of range", toktype);
493 if (toktype < maxtokentype)
494 elog(ERROR, "maptokentype entries are out of order");
495 if (toktype > maxtokentype)
496 {
497 /* starting a new token type, but first save the prior data */
498 if (ndicts > 0)
499 {
500 maplists[maxtokentype].len = ndicts;
501 maplists[maxtokentype].dictIds = (Oid *)
502 MemoryContextAlloc(CacheMemoryContext,
503 sizeof(Oid) * ndicts);
504 memcpy(maplists[maxtokentype].dictIds, mapdicts,
505 sizeof(Oid) * ndicts);
506 }
507 maxtokentype = toktype;
508 mapdicts[0] = cfgmap->mapdict;
509 ndicts = 1;
510 }
511 else
512 {
513 /* continuing data for current token type */
514 if (ndicts >= MAXDICTSPERTT)
515 elog(ERROR, "too many pg_ts_config_map entries for one token type");
516 mapdicts[ndicts++] = cfgmap->mapdict;
517 }
518 }
519
520 systable_endscan_ordered(mapscan);
521 index_close(mapidx, AccessShareLock);
522 heap_close(maprel, AccessShareLock);
523
524 if (ndicts > 0)
525 {
526 /* save the last token type's dictionaries */
527 maplists[maxtokentype].len = ndicts;
528 maplists[maxtokentype].dictIds = (Oid *)
529 MemoryContextAlloc(CacheMemoryContext,
530 sizeof(Oid) * ndicts);
531 memcpy(maplists[maxtokentype].dictIds, mapdicts,
532 sizeof(Oid) * ndicts);
533 /* and save the overall map */
534 entry->lenmap = maxtokentype + 1;
535 entry->map = (ListDictionary *)
536 MemoryContextAlloc(CacheMemoryContext,
537 sizeof(ListDictionary) * entry->lenmap);
538 memcpy(entry->map, maplists,
539 sizeof(ListDictionary) * entry->lenmap);
540 }
541
542 entry->isvalid = true;
543 }
544
545 lastUsedConfig = entry;
546
547 return entry;
548 }
549
550
551 /*---------------------------------------------------
552 * GUC variable "default_text_search_config"
553 *---------------------------------------------------
554 */
555
556 Oid
getTSCurrentConfig(bool emitError)557 getTSCurrentConfig(bool emitError)
558 {
559 /* if we have a cached value, return it */
560 if (OidIsValid(TSCurrentConfigCache))
561 return TSCurrentConfigCache;
562
563 /* fail if GUC hasn't been set up yet */
564 if (TSCurrentConfig == NULL || *TSCurrentConfig == '\0')
565 {
566 if (emitError)
567 elog(ERROR, "text search configuration isn't set");
568 else
569 return InvalidOid;
570 }
571
572 if (TSConfigCacheHash == NULL)
573 {
574 /* First time through: initialize the tsconfig inval callback */
575 init_ts_config_cache();
576 }
577
578 /* Look up the config */
579 TSCurrentConfigCache =
580 get_ts_config_oid(stringToQualifiedNameList(TSCurrentConfig),
581 !emitError);
582
583 return TSCurrentConfigCache;
584 }
585
586 /* GUC check_hook for default_text_search_config */
587 bool
check_TSCurrentConfig(char ** newval,void ** extra,GucSource source)588 check_TSCurrentConfig(char **newval, void **extra, GucSource source)
589 {
590 /*
591 * If we aren't inside a transaction, or connected to a database, we
592 * cannot do the catalog accesses necessary to verify the config name.
593 * Must accept it on faith.
594 */
595 if (IsTransactionState() && MyDatabaseId != InvalidOid)
596 {
597 Oid cfgId;
598 HeapTuple tuple;
599 Form_pg_ts_config cfg;
600 char *buf;
601
602 cfgId = get_ts_config_oid(stringToQualifiedNameList(*newval), true);
603
604 /*
605 * When source == PGC_S_TEST, don't throw a hard error for a
606 * nonexistent configuration, only a NOTICE. See comments in guc.h.
607 */
608 if (!OidIsValid(cfgId))
609 {
610 if (source == PGC_S_TEST)
611 {
612 ereport(NOTICE,
613 (errcode(ERRCODE_UNDEFINED_OBJECT),
614 errmsg("text search configuration \"%s\" does not exist", *newval)));
615 return true;
616 }
617 else
618 return false;
619 }
620
621 /*
622 * Modify the actually stored value to be fully qualified, to ensure
623 * later changes of search_path don't affect it.
624 */
625 tuple = SearchSysCache1(TSCONFIGOID, ObjectIdGetDatum(cfgId));
626 if (!HeapTupleIsValid(tuple))
627 elog(ERROR, "cache lookup failed for text search configuration %u",
628 cfgId);
629 cfg = (Form_pg_ts_config) GETSTRUCT(tuple);
630
631 buf = quote_qualified_identifier(get_namespace_name(cfg->cfgnamespace),
632 NameStr(cfg->cfgname));
633
634 ReleaseSysCache(tuple);
635
636 /* GUC wants it malloc'd not palloc'd */
637 free(*newval);
638 *newval = strdup(buf);
639 pfree(buf);
640 if (!*newval)
641 return false;
642 }
643
644 return true;
645 }
646
647 /* GUC assign_hook for default_text_search_config */
648 void
assign_TSCurrentConfig(const char * newval,void * extra)649 assign_TSCurrentConfig(const char *newval, void *extra)
650 {
651 /* Just reset the cache to force a lookup on first use */
652 TSCurrentConfigCache = InvalidOid;
653 }
654