| /*------------------------------------------------------------------------- |
| * |
| * ts_cache.c |
| * Tsearch related object caches. |
| * |
| * Tsearch performance is very sensitive to performance of parsers, |
| * dictionaries and mapping, so lookups should be cached as much |
| * as possible. |
| * |
| * Once a backend has created a cache entry for a particular TS object OID, |
| * the cache entry will exist for the life of the backend; hence it is |
| * safe to hold onto a pointer to the cache entry while doing things that |
| * might result in recognizing a cache invalidation. Beware however that |
| * subsidiary information might be deleted and reallocated somewhere else |
| * if a cache inval and reval happens! This does not look like it will be |
| * a big problem as long as parser and dictionary methods do not attempt |
| * any database access. |
| * |
| * |
| * Copyright (c) 2006-2021, PostgreSQL Global Development Group |
| * |
| * IDENTIFICATION |
| * src/backend/utils/cache/ts_cache.c |
| * |
| *------------------------------------------------------------------------- |
| */ |
| #include "postgres.h" |
| |
| #include "access/genam.h" |
| #include "access/htup_details.h" |
| #include "access/table.h" |
| #include "access/xact.h" |
| #include "catalog/namespace.h" |
| #include "catalog/pg_ts_config.h" |
| #include "catalog/pg_ts_config_map.h" |
| #include "catalog/pg_ts_dict.h" |
| #include "catalog/pg_ts_parser.h" |
| #include "catalog/pg_ts_template.h" |
| #include "commands/defrem.h" |
| #include "miscadmin.h" |
| #include "tsearch/ts_cache.h" |
| #include "utils/builtins.h" |
| #include "utils/catcache.h" |
| #include "utils/fmgroids.h" |
| #include "utils/inval.h" |
| #include "utils/lsyscache.h" |
| #include "utils/memutils.h" |
| #include "utils/regproc.h" |
| #include "utils/syscache.h" |
| |
| |
| /* |
| * MAXTOKENTYPE/MAXDICTSPERTT are arbitrary limits on the workspace size |
| * used in lookup_ts_config_cache(). We could avoid hardwiring a limit |
| * by making the workspace dynamically enlargeable, but it seems unlikely |
| * to be worth the trouble. |
| */ |
| #define MAXTOKENTYPE 256 |
| #define MAXDICTSPERTT 100 |
| |
| |
| static HTAB *TSParserCacheHash = NULL; |
| static TSParserCacheEntry *lastUsedParser = NULL; |
| |
| static HTAB *TSDictionaryCacheHash = NULL; |
| static TSDictionaryCacheEntry *lastUsedDictionary = NULL; |
| |
| static HTAB *TSConfigCacheHash = NULL; |
| static TSConfigCacheEntry *lastUsedConfig = NULL; |
| |
| /* |
| * GUC default_text_search_config, and a cache of the current config's OID |
| */ |
| char *TSCurrentConfig = NULL; |
| |
| static Oid TSCurrentConfigCache = InvalidOid; |
| |
| |
| /* |
| * We use this syscache callback to detect when a visible change to a TS |
| * catalog entry has been made, by either our own backend or another one. |
| * |
| * In principle we could just flush the specific cache entry that changed, |
| * but given that TS configuration changes are probably infrequent, it |
| * doesn't seem worth the trouble to determine that; we just flush all the |
| * entries of the related hash table. |
| * |
| * We can use the same function for all TS caches by passing the hash |
| * table address as the "arg". |
| */ |
| static void |
| InvalidateTSCacheCallBack(Datum arg, int cacheid, uint32 hashvalue) |
| { |
| HTAB *hash = (HTAB *) DatumGetPointer(arg); |
| HASH_SEQ_STATUS status; |
| TSAnyCacheEntry *entry; |
| |
| hash_seq_init(&status, hash); |
| while ((entry = (TSAnyCacheEntry *) hash_seq_search(&status)) != NULL) |
| entry->isvalid = false; |
| |
| /* Also invalidate the current-config cache if it's pg_ts_config */ |
| if (hash == TSConfigCacheHash) |
| TSCurrentConfigCache = InvalidOid; |
| } |
| |
| /* |
| * Fetch parser cache entry |
| */ |
| TSParserCacheEntry * |
| lookup_ts_parser_cache(Oid prsId) |
| { |
| TSParserCacheEntry *entry; |
| |
| if (TSParserCacheHash == NULL) |
| { |
| /* First time through: initialize the hash table */ |
| HASHCTL ctl; |
| |
| ctl.keysize = sizeof(Oid); |
| ctl.entrysize = sizeof(TSParserCacheEntry); |
| TSParserCacheHash = hash_create("Tsearch parser cache", 4, |
| &ctl, HASH_ELEM | HASH_BLOBS); |
| /* Flush cache on pg_ts_parser changes */ |
| CacheRegisterSyscacheCallback(TSPARSEROID, InvalidateTSCacheCallBack, |
| PointerGetDatum(TSParserCacheHash)); |
| |
| /* Also make sure CacheMemoryContext exists */ |
| if (!CacheMemoryContext) |
| CreateCacheMemoryContext(); |
| } |
| |
| /* Check single-entry cache */ |
| if (lastUsedParser && lastUsedParser->prsId == prsId && |
| lastUsedParser->isvalid) |
| return lastUsedParser; |
| |
| /* Try to look up an existing entry */ |
| entry = (TSParserCacheEntry *) hash_search(TSParserCacheHash, |
| (void *) &prsId, |
| HASH_FIND, NULL); |
| if (entry == NULL || !entry->isvalid) |
| { |
| /* |
| * If we didn't find one, we want to make one. But first look up the |
| * object to be sure the OID is real. |
| */ |
| HeapTuple tp; |
| Form_pg_ts_parser prs; |
| |
| tp = SearchSysCache1(TSPARSEROID, ObjectIdGetDatum(prsId)); |
| if (!HeapTupleIsValid(tp)) |
| elog(ERROR, "cache lookup failed for text search parser %u", |
| prsId); |
| prs = (Form_pg_ts_parser) GETSTRUCT(tp); |
| |
| /* |
| * Sanity checks |
| */ |
| if (!OidIsValid(prs->prsstart)) |
| elog(ERROR, "text search parser %u has no prsstart method", prsId); |
| if (!OidIsValid(prs->prstoken)) |
| elog(ERROR, "text search parser %u has no prstoken method", prsId); |
| if (!OidIsValid(prs->prsend)) |
| elog(ERROR, "text search parser %u has no prsend method", prsId); |
| |
| if (entry == NULL) |
| { |
| bool found; |
| |
| /* Now make the cache entry */ |
| entry = (TSParserCacheEntry *) |
| hash_search(TSParserCacheHash, |
| (void *) &prsId, |
| HASH_ENTER, &found); |
| Assert(!found); /* it wasn't there a moment ago */ |
| } |
| |
| MemSet(entry, 0, sizeof(TSParserCacheEntry)); |
| entry->prsId = prsId; |
| entry->startOid = prs->prsstart; |
| entry->tokenOid = prs->prstoken; |
| entry->endOid = prs->prsend; |
| entry->headlineOid = prs->prsheadline; |
| entry->lextypeOid = prs->prslextype; |
| |
| ReleaseSysCache(tp); |
| |
| fmgr_info_cxt(entry->startOid, &entry->prsstart, CacheMemoryContext); |
| fmgr_info_cxt(entry->tokenOid, &entry->prstoken, CacheMemoryContext); |
| fmgr_info_cxt(entry->endOid, &entry->prsend, CacheMemoryContext); |
| if (OidIsValid(entry->headlineOid)) |
| fmgr_info_cxt(entry->headlineOid, &entry->prsheadline, |
| CacheMemoryContext); |
| |
| entry->isvalid = true; |
| } |
| |
| lastUsedParser = entry; |
| |
| return entry; |
| } |
| |
| /* |
| * Fetch dictionary cache entry |
| */ |
| TSDictionaryCacheEntry * |
| lookup_ts_dictionary_cache(Oid dictId) |
| { |
| TSDictionaryCacheEntry *entry; |
| |
| if (TSDictionaryCacheHash == NULL) |
| { |
| /* First time through: initialize the hash table */ |
| HASHCTL ctl; |
| |
| ctl.keysize = sizeof(Oid); |
| ctl.entrysize = sizeof(TSDictionaryCacheEntry); |
| TSDictionaryCacheHash = hash_create("Tsearch dictionary cache", 8, |
| &ctl, HASH_ELEM | HASH_BLOBS); |
| /* Flush cache on pg_ts_dict and pg_ts_template changes */ |
| CacheRegisterSyscacheCallback(TSDICTOID, InvalidateTSCacheCallBack, |
| PointerGetDatum(TSDictionaryCacheHash)); |
| CacheRegisterSyscacheCallback(TSTEMPLATEOID, InvalidateTSCacheCallBack, |
| PointerGetDatum(TSDictionaryCacheHash)); |
| |
| /* Also make sure CacheMemoryContext exists */ |
| if (!CacheMemoryContext) |
| CreateCacheMemoryContext(); |
| } |
| |
| /* Check single-entry cache */ |
| if (lastUsedDictionary && lastUsedDictionary->dictId == dictId && |
| lastUsedDictionary->isvalid) |
| return lastUsedDictionary; |
| |
| /* Try to look up an existing entry */ |
| entry = (TSDictionaryCacheEntry *) hash_search(TSDictionaryCacheHash, |
| (void *) &dictId, |
| HASH_FIND, NULL); |
| if (entry == NULL || !entry->isvalid) |
| { |
| /* |
| * If we didn't find one, we want to make one. But first look up the |
| * object to be sure the OID is real. |
| */ |
| HeapTuple tpdict, |
| tptmpl; |
| Form_pg_ts_dict dict; |
| Form_pg_ts_template template; |
| MemoryContext saveCtx; |
| |
| tpdict = SearchSysCache1(TSDICTOID, ObjectIdGetDatum(dictId)); |
| if (!HeapTupleIsValid(tpdict)) |
| elog(ERROR, "cache lookup failed for text search dictionary %u", |
| dictId); |
| dict = (Form_pg_ts_dict) GETSTRUCT(tpdict); |
| |
| /* |
| * Sanity checks |
| */ |
| if (!OidIsValid(dict->dicttemplate)) |
| elog(ERROR, "text search dictionary %u has no template", dictId); |
| |
| /* |
| * Retrieve dictionary's template |
| */ |
| tptmpl = SearchSysCache1(TSTEMPLATEOID, |
| ObjectIdGetDatum(dict->dicttemplate)); |
| if (!HeapTupleIsValid(tptmpl)) |
| elog(ERROR, "cache lookup failed for text search template %u", |
| dict->dicttemplate); |
| template = (Form_pg_ts_template) GETSTRUCT(tptmpl); |
| |
| /* |
| * Sanity checks |
| */ |
| if (!OidIsValid(template->tmpllexize)) |
| elog(ERROR, "text search template %u has no lexize method", |
| template->tmpllexize); |
| |
| if (entry == NULL) |
| { |
| bool found; |
| |
| /* Now make the cache entry */ |
| entry = (TSDictionaryCacheEntry *) |
| hash_search(TSDictionaryCacheHash, |
| (void *) &dictId, |
| HASH_ENTER, &found); |
| Assert(!found); /* it wasn't there a moment ago */ |
| |
| /* Create private memory context the first time through */ |
| saveCtx = AllocSetContextCreate(CacheMemoryContext, |
| "TS dictionary", |
| ALLOCSET_SMALL_SIZES); |
| MemoryContextCopyAndSetIdentifier(saveCtx, NameStr(dict->dictname)); |
| } |
| else |
| { |
| /* Clear the existing entry's private context */ |
| saveCtx = entry->dictCtx; |
| /* Don't let context's ident pointer dangle while we reset it */ |
| MemoryContextSetIdentifier(saveCtx, NULL); |
| MemoryContextReset(saveCtx); |
| MemoryContextCopyAndSetIdentifier(saveCtx, NameStr(dict->dictname)); |
| } |
| |
| MemSet(entry, 0, sizeof(TSDictionaryCacheEntry)); |
| entry->dictId = dictId; |
| entry->dictCtx = saveCtx; |
| |
| entry->lexizeOid = template->tmpllexize; |
| |
| if (OidIsValid(template->tmplinit)) |
| { |
| List *dictoptions; |
| Datum opt; |
| bool isnull; |
| MemoryContext oldcontext; |
| |
| /* |
| * Init method runs in dictionary's private memory context, and we |
| * make sure the options are stored there too |
| */ |
| oldcontext = MemoryContextSwitchTo(entry->dictCtx); |
| |
| opt = SysCacheGetAttr(TSDICTOID, tpdict, |
| Anum_pg_ts_dict_dictinitoption, |
| &isnull); |
| if (isnull) |
| dictoptions = NIL; |
| else |
| dictoptions = deserialize_deflist(opt); |
| |
| entry->dictData = |
| DatumGetPointer(OidFunctionCall1(template->tmplinit, |
| PointerGetDatum(dictoptions))); |
| |
| MemoryContextSwitchTo(oldcontext); |
| } |
| |
| ReleaseSysCache(tptmpl); |
| ReleaseSysCache(tpdict); |
| |
| fmgr_info_cxt(entry->lexizeOid, &entry->lexize, entry->dictCtx); |
| |
| entry->isvalid = true; |
| } |
| |
| lastUsedDictionary = entry; |
| |
| return entry; |
| } |
| |
| /* |
| * Initialize config cache and prepare callbacks. This is split out of |
| * lookup_ts_config_cache because we need to activate the callback before |
| * caching TSCurrentConfigCache, too. |
| */ |
| static void |
| init_ts_config_cache(void) |
| { |
| HASHCTL ctl; |
| |
| ctl.keysize = sizeof(Oid); |
| ctl.entrysize = sizeof(TSConfigCacheEntry); |
| TSConfigCacheHash = hash_create("Tsearch configuration cache", 16, |
| &ctl, HASH_ELEM | HASH_BLOBS); |
| /* Flush cache on pg_ts_config and pg_ts_config_map changes */ |
| CacheRegisterSyscacheCallback(TSCONFIGOID, InvalidateTSCacheCallBack, |
| PointerGetDatum(TSConfigCacheHash)); |
| CacheRegisterSyscacheCallback(TSCONFIGMAP, InvalidateTSCacheCallBack, |
| PointerGetDatum(TSConfigCacheHash)); |
| |
| /* Also make sure CacheMemoryContext exists */ |
| if (!CacheMemoryContext) |
| CreateCacheMemoryContext(); |
| } |
| |
| /* |
| * Fetch configuration cache entry |
| */ |
| TSConfigCacheEntry * |
| lookup_ts_config_cache(Oid cfgId) |
| { |
| TSConfigCacheEntry *entry; |
| |
| if (TSConfigCacheHash == NULL) |
| { |
| /* First time through: initialize the hash table */ |
| init_ts_config_cache(); |
| } |
| |
| /* Check single-entry cache */ |
| if (lastUsedConfig && lastUsedConfig->cfgId == cfgId && |
| lastUsedConfig->isvalid) |
| return lastUsedConfig; |
| |
| /* Try to look up an existing entry */ |
| entry = (TSConfigCacheEntry *) hash_search(TSConfigCacheHash, |
| (void *) &cfgId, |
| HASH_FIND, NULL); |
| if (entry == NULL || !entry->isvalid) |
| { |
| /* |
| * If we didn't find one, we want to make one. But first look up the |
| * object to be sure the OID is real. |
| */ |
| HeapTuple tp; |
| Form_pg_ts_config cfg; |
| Relation maprel; |
| Relation mapidx; |
| ScanKeyData mapskey; |
| SysScanDesc mapscan; |
| HeapTuple maptup; |
| ListDictionary maplists[MAXTOKENTYPE + 1]; |
| Oid mapdicts[MAXDICTSPERTT]; |
| int maxtokentype; |
| int ndicts; |
| int i; |
| |
| tp = SearchSysCache1(TSCONFIGOID, ObjectIdGetDatum(cfgId)); |
| if (!HeapTupleIsValid(tp)) |
| elog(ERROR, "cache lookup failed for text search configuration %u", |
| cfgId); |
| cfg = (Form_pg_ts_config) GETSTRUCT(tp); |
| |
| /* |
| * Sanity checks |
| */ |
| if (!OidIsValid(cfg->cfgparser)) |
| elog(ERROR, "text search configuration %u has no parser", cfgId); |
| |
| if (entry == NULL) |
| { |
| bool found; |
| |
| /* Now make the cache entry */ |
| entry = (TSConfigCacheEntry *) |
| hash_search(TSConfigCacheHash, |
| (void *) &cfgId, |
| HASH_ENTER, &found); |
| Assert(!found); /* it wasn't there a moment ago */ |
| } |
| else |
| { |
| /* Cleanup old contents */ |
| if (entry->map) |
| { |
| for (i = 0; i < entry->lenmap; i++) |
| if (entry->map[i].dictIds) |
| pfree(entry->map[i].dictIds); |
| pfree(entry->map); |
| } |
| } |
| |
| MemSet(entry, 0, sizeof(TSConfigCacheEntry)); |
| entry->cfgId = cfgId; |
| entry->prsId = cfg->cfgparser; |
| |
| ReleaseSysCache(tp); |
| |
| /* |
| * Scan pg_ts_config_map to gather dictionary list for each token type |
| * |
| * Because the index is on (mapcfg, maptokentype, mapseqno), we will |
| * see the entries in maptokentype order, and in mapseqno order for |
| * each token type, even though we didn't explicitly ask for that. |
| */ |
| MemSet(maplists, 0, sizeof(maplists)); |
| maxtokentype = 0; |
| ndicts = 0; |
| |
| ScanKeyInit(&mapskey, |
| Anum_pg_ts_config_map_mapcfg, |
| BTEqualStrategyNumber, F_OIDEQ, |
| ObjectIdGetDatum(cfgId)); |
| |
| maprel = table_open(TSConfigMapRelationId, AccessShareLock); |
| mapidx = index_open(TSConfigMapIndexId, AccessShareLock); |
| mapscan = systable_beginscan_ordered(maprel, mapidx, |
| NULL, 1, &mapskey); |
| |
| while ((maptup = systable_getnext_ordered(mapscan, ForwardScanDirection)) != NULL) |
| { |
| Form_pg_ts_config_map cfgmap = (Form_pg_ts_config_map) GETSTRUCT(maptup); |
| int toktype = cfgmap->maptokentype; |
| |
| if (toktype <= 0 || toktype > MAXTOKENTYPE) |
| elog(ERROR, "maptokentype value %d is out of range", toktype); |
| if (toktype < maxtokentype) |
| elog(ERROR, "maptokentype entries are out of order"); |
| if (toktype > maxtokentype) |
| { |
| /* starting a new token type, but first save the prior data */ |
| if (ndicts > 0) |
| { |
| maplists[maxtokentype].len = ndicts; |
| maplists[maxtokentype].dictIds = (Oid *) |
| MemoryContextAlloc(CacheMemoryContext, |
| sizeof(Oid) * ndicts); |
| memcpy(maplists[maxtokentype].dictIds, mapdicts, |
| sizeof(Oid) * ndicts); |
| } |
| maxtokentype = toktype; |
| mapdicts[0] = cfgmap->mapdict; |
| ndicts = 1; |
| } |
| else |
| { |
| /* continuing data for current token type */ |
| if (ndicts >= MAXDICTSPERTT) |
| elog(ERROR, "too many pg_ts_config_map entries for one token type"); |
| mapdicts[ndicts++] = cfgmap->mapdict; |
| } |
| } |
| |
| systable_endscan_ordered(mapscan); |
| index_close(mapidx, AccessShareLock); |
| table_close(maprel, AccessShareLock); |
| |
| if (ndicts > 0) |
| { |
| /* save the last token type's dictionaries */ |
| maplists[maxtokentype].len = ndicts; |
| maplists[maxtokentype].dictIds = (Oid *) |
| MemoryContextAlloc(CacheMemoryContext, |
| sizeof(Oid) * ndicts); |
| memcpy(maplists[maxtokentype].dictIds, mapdicts, |
| sizeof(Oid) * ndicts); |
| /* and save the overall map */ |
| entry->lenmap = maxtokentype + 1; |
| entry->map = (ListDictionary *) |
| MemoryContextAlloc(CacheMemoryContext, |
| sizeof(ListDictionary) * entry->lenmap); |
| memcpy(entry->map, maplists, |
| sizeof(ListDictionary) * entry->lenmap); |
| } |
| |
| entry->isvalid = true; |
| } |
| |
| lastUsedConfig = entry; |
| |
| return entry; |
| } |
| |
| |
| /*--------------------------------------------------- |
| * GUC variable "default_text_search_config" |
| *--------------------------------------------------- |
| */ |
| |
| Oid |
| getTSCurrentConfig(bool emitError) |
| { |
| /* if we have a cached value, return it */ |
| if (OidIsValid(TSCurrentConfigCache)) |
| return TSCurrentConfigCache; |
| |
| /* fail if GUC hasn't been set up yet */ |
| if (TSCurrentConfig == NULL || *TSCurrentConfig == '\0') |
| { |
| if (emitError) |
| elog(ERROR, "text search configuration isn't set"); |
| else |
| return InvalidOid; |
| } |
| |
| if (TSConfigCacheHash == NULL) |
| { |
| /* First time through: initialize the tsconfig inval callback */ |
| init_ts_config_cache(); |
| } |
| |
| /* Look up the config */ |
| TSCurrentConfigCache = |
| get_ts_config_oid(stringToQualifiedNameList(TSCurrentConfig), |
| !emitError); |
| |
| return TSCurrentConfigCache; |
| } |
| |
| /* GUC check_hook for default_text_search_config */ |
| bool |
| check_TSCurrentConfig(char **newval, void **extra, GucSource source) |
| { |
| /* |
| * If we aren't inside a transaction, or connected to a database, we |
| * cannot do the catalog accesses necessary to verify the config name. |
| * Must accept it on faith. |
| */ |
| if (IsTransactionState() && MyDatabaseId != InvalidOid) |
| { |
| Oid cfgId; |
| HeapTuple tuple; |
| Form_pg_ts_config cfg; |
| char *buf; |
| |
| cfgId = get_ts_config_oid(stringToQualifiedNameList(*newval), true); |
| |
| /* |
| * When source == PGC_S_TEST, don't throw a hard error for a |
| * nonexistent configuration, only a NOTICE. See comments in guc.h. |
| */ |
| if (!OidIsValid(cfgId)) |
| { |
| if (source == PGC_S_TEST) |
| { |
| ereport(NOTICE, |
| (errcode(ERRCODE_UNDEFINED_OBJECT), |
| errmsg("text search configuration \"%s\" does not exist", *newval))); |
| return true; |
| } |
| else |
| return false; |
| } |
| |
| /* |
| * Modify the actually stored value to be fully qualified, to ensure |
| * later changes of search_path don't affect it. |
| */ |
| tuple = SearchSysCache1(TSCONFIGOID, ObjectIdGetDatum(cfgId)); |
| if (!HeapTupleIsValid(tuple)) |
| elog(ERROR, "cache lookup failed for text search configuration %u", |
| cfgId); |
| cfg = (Form_pg_ts_config) GETSTRUCT(tuple); |
| |
| buf = quote_qualified_identifier(get_namespace_name(cfg->cfgnamespace), |
| NameStr(cfg->cfgname)); |
| |
| ReleaseSysCache(tuple); |
| |
| /* GUC wants it malloc'd not palloc'd */ |
| free(*newval); |
| *newval = strdup(buf); |
| pfree(buf); |
| if (!*newval) |
| return false; |
| } |
| |
| return true; |
| } |
| |
| /* GUC assign_hook for default_text_search_config */ |
| void |
| assign_TSCurrentConfig(const char *newval, void *extra) |
| { |
| /* Just reset the cache to force a lookup on first use */ |
| TSCurrentConfigCache = InvalidOid; |
| } |