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-2018, 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 "TS dictionary",
300 ALLOCSET_SMALL_SIZES);
301 MemoryContextCopyAndSetIdentifier(saveCtx, NameStr(dict->dictname));
302 }
303 else
304 {
305 /* Clear the existing entry's private context */
306 saveCtx = entry->dictCtx;
307 /* Don't let context's ident pointer dangle while we reset it */
308 MemoryContextSetIdentifier(saveCtx, NULL);
309 MemoryContextReset(saveCtx);
310 MemoryContextCopyAndSetIdentifier(saveCtx, NameStr(dict->dictname));
311 }
312
313 MemSet(entry, 0, sizeof(TSDictionaryCacheEntry));
314 entry->dictId = dictId;
315 entry->dictCtx = saveCtx;
316
317 entry->lexizeOid = template->tmpllexize;
318
319 if (OidIsValid(template->tmplinit))
320 {
321 List *dictoptions;
322 Datum opt;
323 bool isnull;
324 MemoryContext oldcontext;
325
326 /*
327 * Init method runs in dictionary's private memory context, and we
328 * make sure the options are stored there too
329 */
330 oldcontext = MemoryContextSwitchTo(entry->dictCtx);
331
332 opt = SysCacheGetAttr(TSDICTOID, tpdict,
333 Anum_pg_ts_dict_dictinitoption,
334 &isnull);
335 if (isnull)
336 dictoptions = NIL;
337 else
338 dictoptions = deserialize_deflist(opt);
339
340 entry->dictData =
341 DatumGetPointer(OidFunctionCall1(template->tmplinit,
342 PointerGetDatum(dictoptions)));
343
344 MemoryContextSwitchTo(oldcontext);
345 }
346
347 ReleaseSysCache(tptmpl);
348 ReleaseSysCache(tpdict);
349
350 fmgr_info_cxt(entry->lexizeOid, &entry->lexize, entry->dictCtx);
351
352 entry->isvalid = true;
353 }
354
355 lastUsedDictionary = entry;
356
357 return entry;
358 }
359
360 /*
361 * Initialize config cache and prepare callbacks. This is split out of
362 * lookup_ts_config_cache because we need to activate the callback before
363 * caching TSCurrentConfigCache, too.
364 */
365 static void
init_ts_config_cache(void)366 init_ts_config_cache(void)
367 {
368 HASHCTL ctl;
369
370 MemSet(&ctl, 0, sizeof(ctl));
371 ctl.keysize = sizeof(Oid);
372 ctl.entrysize = sizeof(TSConfigCacheEntry);
373 TSConfigCacheHash = hash_create("Tsearch configuration cache", 16,
374 &ctl, HASH_ELEM | HASH_BLOBS);
375 /* Flush cache on pg_ts_config and pg_ts_config_map changes */
376 CacheRegisterSyscacheCallback(TSCONFIGOID, InvalidateTSCacheCallBack,
377 PointerGetDatum(TSConfigCacheHash));
378 CacheRegisterSyscacheCallback(TSCONFIGMAP, InvalidateTSCacheCallBack,
379 PointerGetDatum(TSConfigCacheHash));
380
381 /* Also make sure CacheMemoryContext exists */
382 if (!CacheMemoryContext)
383 CreateCacheMemoryContext();
384 }
385
386 /*
387 * Fetch configuration cache entry
388 */
389 TSConfigCacheEntry *
lookup_ts_config_cache(Oid cfgId)390 lookup_ts_config_cache(Oid cfgId)
391 {
392 TSConfigCacheEntry *entry;
393
394 if (TSConfigCacheHash == NULL)
395 {
396 /* First time through: initialize the hash table */
397 init_ts_config_cache();
398 }
399
400 /* Check single-entry cache */
401 if (lastUsedConfig && lastUsedConfig->cfgId == cfgId &&
402 lastUsedConfig->isvalid)
403 return lastUsedConfig;
404
405 /* Try to look up an existing entry */
406 entry = (TSConfigCacheEntry *) hash_search(TSConfigCacheHash,
407 (void *) &cfgId,
408 HASH_FIND, NULL);
409 if (entry == NULL || !entry->isvalid)
410 {
411 /*
412 * If we didn't find one, we want to make one. But first look up the
413 * object to be sure the OID is real.
414 */
415 HeapTuple tp;
416 Form_pg_ts_config cfg;
417 Relation maprel;
418 Relation mapidx;
419 ScanKeyData mapskey;
420 SysScanDesc mapscan;
421 HeapTuple maptup;
422 ListDictionary maplists[MAXTOKENTYPE + 1];
423 Oid mapdicts[MAXDICTSPERTT];
424 int maxtokentype;
425 int ndicts;
426 int i;
427
428 tp = SearchSysCache1(TSCONFIGOID, ObjectIdGetDatum(cfgId));
429 if (!HeapTupleIsValid(tp))
430 elog(ERROR, "cache lookup failed for text search configuration %u",
431 cfgId);
432 cfg = (Form_pg_ts_config) GETSTRUCT(tp);
433
434 /*
435 * Sanity checks
436 */
437 if (!OidIsValid(cfg->cfgparser))
438 elog(ERROR, "text search configuration %u has no parser", cfgId);
439
440 if (entry == NULL)
441 {
442 bool found;
443
444 /* Now make the cache entry */
445 entry = (TSConfigCacheEntry *)
446 hash_search(TSConfigCacheHash,
447 (void *) &cfgId,
448 HASH_ENTER, &found);
449 Assert(!found); /* it wasn't there a moment ago */
450 }
451 else
452 {
453 /* Cleanup old contents */
454 if (entry->map)
455 {
456 for (i = 0; i < entry->lenmap; i++)
457 if (entry->map[i].dictIds)
458 pfree(entry->map[i].dictIds);
459 pfree(entry->map);
460 }
461 }
462
463 MemSet(entry, 0, sizeof(TSConfigCacheEntry));
464 entry->cfgId = cfgId;
465 entry->prsId = cfg->cfgparser;
466
467 ReleaseSysCache(tp);
468
469 /*
470 * Scan pg_ts_config_map to gather dictionary list for each token type
471 *
472 * Because the index is on (mapcfg, maptokentype, mapseqno), we will
473 * see the entries in maptokentype order, and in mapseqno order for
474 * each token type, even though we didn't explicitly ask for that.
475 */
476 MemSet(maplists, 0, sizeof(maplists));
477 maxtokentype = 0;
478 ndicts = 0;
479
480 ScanKeyInit(&mapskey,
481 Anum_pg_ts_config_map_mapcfg,
482 BTEqualStrategyNumber, F_OIDEQ,
483 ObjectIdGetDatum(cfgId));
484
485 maprel = heap_open(TSConfigMapRelationId, AccessShareLock);
486 mapidx = index_open(TSConfigMapIndexId, AccessShareLock);
487 mapscan = systable_beginscan_ordered(maprel, mapidx,
488 NULL, 1, &mapskey);
489
490 while ((maptup = systable_getnext_ordered(mapscan, ForwardScanDirection)) != NULL)
491 {
492 Form_pg_ts_config_map cfgmap = (Form_pg_ts_config_map) GETSTRUCT(maptup);
493 int toktype = cfgmap->maptokentype;
494
495 if (toktype <= 0 || toktype > MAXTOKENTYPE)
496 elog(ERROR, "maptokentype value %d is out of range", toktype);
497 if (toktype < maxtokentype)
498 elog(ERROR, "maptokentype entries are out of order");
499 if (toktype > maxtokentype)
500 {
501 /* starting a new token type, but first save the prior data */
502 if (ndicts > 0)
503 {
504 maplists[maxtokentype].len = ndicts;
505 maplists[maxtokentype].dictIds = (Oid *)
506 MemoryContextAlloc(CacheMemoryContext,
507 sizeof(Oid) * ndicts);
508 memcpy(maplists[maxtokentype].dictIds, mapdicts,
509 sizeof(Oid) * ndicts);
510 }
511 maxtokentype = toktype;
512 mapdicts[0] = cfgmap->mapdict;
513 ndicts = 1;
514 }
515 else
516 {
517 /* continuing data for current token type */
518 if (ndicts >= MAXDICTSPERTT)
519 elog(ERROR, "too many pg_ts_config_map entries for one token type");
520 mapdicts[ndicts++] = cfgmap->mapdict;
521 }
522 }
523
524 systable_endscan_ordered(mapscan);
525 index_close(mapidx, AccessShareLock);
526 heap_close(maprel, AccessShareLock);
527
528 if (ndicts > 0)
529 {
530 /* save the last token type's dictionaries */
531 maplists[maxtokentype].len = ndicts;
532 maplists[maxtokentype].dictIds = (Oid *)
533 MemoryContextAlloc(CacheMemoryContext,
534 sizeof(Oid) * ndicts);
535 memcpy(maplists[maxtokentype].dictIds, mapdicts,
536 sizeof(Oid) * ndicts);
537 /* and save the overall map */
538 entry->lenmap = maxtokentype + 1;
539 entry->map = (ListDictionary *)
540 MemoryContextAlloc(CacheMemoryContext,
541 sizeof(ListDictionary) * entry->lenmap);
542 memcpy(entry->map, maplists,
543 sizeof(ListDictionary) * entry->lenmap);
544 }
545
546 entry->isvalid = true;
547 }
548
549 lastUsedConfig = entry;
550
551 return entry;
552 }
553
554
555 /*---------------------------------------------------
556 * GUC variable "default_text_search_config"
557 *---------------------------------------------------
558 */
559
560 Oid
getTSCurrentConfig(bool emitError)561 getTSCurrentConfig(bool emitError)
562 {
563 /* if we have a cached value, return it */
564 if (OidIsValid(TSCurrentConfigCache))
565 return TSCurrentConfigCache;
566
567 /* fail if GUC hasn't been set up yet */
568 if (TSCurrentConfig == NULL || *TSCurrentConfig == '\0')
569 {
570 if (emitError)
571 elog(ERROR, "text search configuration isn't set");
572 else
573 return InvalidOid;
574 }
575
576 if (TSConfigCacheHash == NULL)
577 {
578 /* First time through: initialize the tsconfig inval callback */
579 init_ts_config_cache();
580 }
581
582 /* Look up the config */
583 TSCurrentConfigCache =
584 get_ts_config_oid(stringToQualifiedNameList(TSCurrentConfig),
585 !emitError);
586
587 return TSCurrentConfigCache;
588 }
589
590 /* GUC check_hook for default_text_search_config */
591 bool
check_TSCurrentConfig(char ** newval,void ** extra,GucSource source)592 check_TSCurrentConfig(char **newval, void **extra, GucSource source)
593 {
594 /*
595 * If we aren't inside a transaction, or connected to a database, we
596 * cannot do the catalog accesses necessary to verify the config name.
597 * Must accept it on faith.
598 */
599 if (IsTransactionState() && MyDatabaseId != InvalidOid)
600 {
601 Oid cfgId;
602 HeapTuple tuple;
603 Form_pg_ts_config cfg;
604 char *buf;
605
606 cfgId = get_ts_config_oid(stringToQualifiedNameList(*newval), true);
607
608 /*
609 * When source == PGC_S_TEST, don't throw a hard error for a
610 * nonexistent configuration, only a NOTICE. See comments in guc.h.
611 */
612 if (!OidIsValid(cfgId))
613 {
614 if (source == PGC_S_TEST)
615 {
616 ereport(NOTICE,
617 (errcode(ERRCODE_UNDEFINED_OBJECT),
618 errmsg("text search configuration \"%s\" does not exist", *newval)));
619 return true;
620 }
621 else
622 return false;
623 }
624
625 /*
626 * Modify the actually stored value to be fully qualified, to ensure
627 * later changes of search_path don't affect it.
628 */
629 tuple = SearchSysCache1(TSCONFIGOID, ObjectIdGetDatum(cfgId));
630 if (!HeapTupleIsValid(tuple))
631 elog(ERROR, "cache lookup failed for text search configuration %u",
632 cfgId);
633 cfg = (Form_pg_ts_config) GETSTRUCT(tuple);
634
635 buf = quote_qualified_identifier(get_namespace_name(cfg->cfgnamespace),
636 NameStr(cfg->cfgname));
637
638 ReleaseSysCache(tuple);
639
640 /* GUC wants it malloc'd not palloc'd */
641 free(*newval);
642 *newval = strdup(buf);
643 pfree(buf);
644 if (!*newval)
645 return false;
646 }
647
648 return true;
649 }
650
651 /* GUC assign_hook for default_text_search_config */
652 void
assign_TSCurrentConfig(const char * newval,void * extra)653 assign_TSCurrentConfig(const char *newval, void *extra)
654 {
655 /* Just reset the cache to force a lookup on first use */
656 TSCurrentConfigCache = InvalidOid;
657 }
658