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