Coverage Report

Created: 2026-11-09 07:20

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/postgres/src/backend/utils/cache/evtcache.c
Line
Count
Source
1
/*-------------------------------------------------------------------------
2
 *
3
 * evtcache.c
4
 *    Special-purpose cache for event trigger data.
5
 *
6
 * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
7
 * Portions Copyright (c) 1994, Regents of the University of California
8
 *
9
 * IDENTIFICATION
10
 *    src/backend/utils/cache/evtcache.c
11
 *
12
 *-------------------------------------------------------------------------
13
 */
14
#include "postgres.h"
15
16
#include "access/genam.h"
17
#include "access/htup_details.h"
18
#include "access/relation.h"
19
#include "catalog/pg_event_trigger.h"
20
#include "catalog/pg_type.h"
21
#include "commands/trigger.h"
22
#include "tcop/cmdtag.h"
23
#include "utils/array.h"
24
#include "utils/builtins.h"
25
#include "utils/catcache.h"
26
#include "utils/evtcache.h"
27
#include "utils/hsearch.h"
28
#include "utils/inval.h"
29
#include "utils/memutils.h"
30
#include "utils/rel.h"
31
#include "utils/syscache.h"
32
33
typedef enum
34
{
35
  ETCS_NEEDS_REBUILD,
36
  ETCS_REBUILD_STARTED,
37
  ETCS_VALID,
38
} EventTriggerCacheStateType;
39
40
typedef struct
41
{
42
  EventTriggerEvent event;
43
  List     *triggerlist;
44
} EventTriggerCacheEntry;
45
46
static HTAB *EventTriggerCache;
47
static MemoryContext EventTriggerCacheContext;
48
static EventTriggerCacheStateType EventTriggerCacheState = ETCS_NEEDS_REBUILD;
49
50
static void BuildEventTriggerCache(void);
51
static void InvalidateEventCacheCallback(Datum arg,
52
                     SysCacheIdentifier cacheid,
53
                     uint32 hashvalue);
54
static Bitmapset *DecodeTextArrayToBitmapset(Datum array);
55
56
/*
57
 * Search the event cache by trigger event.
58
 *
59
 * Note that the caller had better copy any data it wants to keep around
60
 * across any operation that might touch a system catalog into some other
61
 * memory context, since a cache reset could blow the return value away.
62
 */
63
List *
64
EventCacheLookup(EventTriggerEvent event)
65
0
{
66
0
  EventTriggerCacheEntry *entry;
67
68
0
  if (EventTriggerCacheState != ETCS_VALID)
69
0
    BuildEventTriggerCache();
70
0
  entry = hash_search(EventTriggerCache, &event, HASH_FIND, NULL);
71
0
  return entry != NULL ? entry->triggerlist : NIL;
72
0
}
73
74
/*
75
 * Rebuild the event trigger cache.
76
 */
77
static void
78
BuildEventTriggerCache(void)
79
0
{
80
0
  HASHCTL   ctl;
81
0
  HTAB     *cache;
82
0
  Relation  rel;
83
0
  Relation  irel;
84
0
  SysScanDesc scan;
85
86
0
  if (EventTriggerCacheContext != NULL)
87
0
  {
88
    /*
89
     * Free up any memory already allocated in EventTriggerCacheContext.
90
     * This can happen either because a previous rebuild failed, or
91
     * because an invalidation happened before the rebuild was complete.
92
     */
93
0
    MemoryContextReset(EventTriggerCacheContext);
94
0
  }
95
0
  else
96
0
  {
97
    /*
98
     * This is our first time attempting to build the cache, so we need to
99
     * set up the memory context and register a syscache callback to
100
     * capture future invalidation events.
101
     */
102
0
    if (CacheMemoryContext == NULL)
103
0
      CreateCacheMemoryContext();
104
0
    EventTriggerCacheContext =
105
0
      AllocSetContextCreate(CacheMemoryContext,
106
0
                  "EventTriggerCache",
107
0
                  ALLOCSET_DEFAULT_SIZES);
108
0
    CacheRegisterSyscacheCallback(EVENTTRIGGEROID,
109
0
                    InvalidateEventCacheCallback,
110
0
                    (Datum) 0);
111
0
  }
112
113
  /* Prevent the memory context from being nuked while we're rebuilding. */
114
0
  EventTriggerCacheState = ETCS_REBUILD_STARTED;
115
116
  /* Create new hash table. */
117
0
  ctl.keysize = sizeof(EventTriggerEvent);
118
0
  ctl.entrysize = sizeof(EventTriggerCacheEntry);
119
0
  ctl.hcxt = EventTriggerCacheContext;
120
0
  cache = hash_create("EventTriggerCacheHash", 32, &ctl,
121
0
            HASH_ELEM | HASH_BLOBS | HASH_CONTEXT);
122
123
  /*
124
   * Prepare to scan pg_event_trigger in name order.
125
   */
126
0
  rel = relation_open(EventTriggerRelationId, AccessShareLock);
127
0
  irel = index_open(EventTriggerNameIndexId, AccessShareLock);
128
0
  scan = systable_beginscan_ordered(rel, irel, NULL, 0, NULL);
129
130
  /*
131
   * Build a cache item for each pg_event_trigger tuple, and append each one
132
   * to the appropriate cache entry.
133
   */
134
0
  for (;;)
135
0
  {
136
0
    HeapTuple tup;
137
0
    Form_pg_event_trigger form;
138
0
    char     *evtevent;
139
0
    EventTriggerEvent event;
140
0
    EventTriggerCacheItem *item;
141
0
    Datum   evttags;
142
0
    bool    evttags_isnull;
143
0
    EventTriggerCacheEntry *entry;
144
0
    bool    found;
145
0
    MemoryContext oldcontext;
146
147
    /* Get next tuple. */
148
0
    tup = systable_getnext_ordered(scan, ForwardScanDirection);
149
0
    if (!HeapTupleIsValid(tup))
150
0
      break;
151
152
    /* Skip trigger if disabled. */
153
0
    form = (Form_pg_event_trigger) GETSTRUCT(tup);
154
0
    if (form->evtenabled == TRIGGER_DISABLED)
155
0
      continue;
156
157
    /* Decode event name. */
158
0
    evtevent = NameStr(form->evtevent);
159
0
    if (strcmp(evtevent, "ddl_command_start") == 0)
160
0
      event = EVT_DDLCommandStart;
161
0
    else if (strcmp(evtevent, "ddl_command_end") == 0)
162
0
      event = EVT_DDLCommandEnd;
163
0
    else if (strcmp(evtevent, "sql_drop") == 0)
164
0
      event = EVT_SQLDrop;
165
0
    else if (strcmp(evtevent, "table_rewrite") == 0)
166
0
      event = EVT_TableRewrite;
167
0
    else if (strcmp(evtevent, "login") == 0)
168
0
      event = EVT_Login;
169
0
    else
170
0
      continue;
171
172
    /* Switch to correct memory context. */
173
0
    oldcontext = MemoryContextSwitchTo(EventTriggerCacheContext);
174
175
    /* Allocate new cache item. */
176
0
    item = palloc0_object(EventTriggerCacheItem);
177
0
    item->fnoid = form->evtfoid;
178
0
    item->enabled = form->evtenabled;
179
180
    /* Decode and sort tags array. */
181
0
    evttags = heap_getattr(tup, Anum_pg_event_trigger_evttags,
182
0
                 RelationGetDescr(rel), &evttags_isnull);
183
0
    if (!evttags_isnull)
184
0
      item->tagset = DecodeTextArrayToBitmapset(evttags);
185
186
    /* Add to cache entry. */
187
0
    entry = hash_search(cache, &event, HASH_ENTER, &found);
188
0
    if (found)
189
0
      entry->triggerlist = lappend(entry->triggerlist, item);
190
0
    else
191
0
      entry->triggerlist = list_make1(item);
192
193
    /* Restore previous memory context. */
194
0
    MemoryContextSwitchTo(oldcontext);
195
0
  }
196
197
  /* Done with pg_event_trigger scan. */
198
0
  systable_endscan_ordered(scan);
199
0
  index_close(irel, AccessShareLock);
200
0
  relation_close(rel, AccessShareLock);
201
202
  /* Install new cache. */
203
0
  EventTriggerCache = cache;
204
205
  /*
206
   * If the cache has been invalidated since we entered this routine, we
207
   * still use and return the cache we just finished constructing, to avoid
208
   * infinite loops, but we leave the cache marked stale so that we'll
209
   * rebuild it again on next access.  Otherwise, we mark the cache valid.
210
   */
211
0
  if (EventTriggerCacheState == ETCS_REBUILD_STARTED)
212
0
    EventTriggerCacheState = ETCS_VALID;
213
0
}
214
215
/*
216
 * Decode text[] to a Bitmapset of CommandTags.
217
 *
218
 * We could avoid a bit of overhead here if we were willing to duplicate some
219
 * of the logic from deconstruct_array, but it doesn't seem worth the code
220
 * complexity.
221
 */
222
static Bitmapset *
223
DecodeTextArrayToBitmapset(Datum array)
224
0
{
225
0
  ArrayType  *arr = DatumGetArrayTypeP(array);
226
0
  Datum    *elems;
227
0
  Bitmapset  *bms;
228
0
  int     i;
229
0
  int     nelems;
230
231
0
  if (ARR_NDIM(arr) != 1 || ARR_HASNULL(arr) || ARR_ELEMTYPE(arr) != TEXTOID)
232
0
    elog(ERROR, "expected 1-D text array");
233
0
  deconstruct_array_builtin(arr, TEXTOID, &elems, NULL, &nelems);
234
235
0
  for (bms = NULL, i = 0; i < nelems; ++i)
236
0
  {
237
0
    char     *str = TextDatumGetCString(elems[i]);
238
239
0
    bms = bms_add_member(bms, GetCommandTagEnum(str));
240
0
    pfree(str);
241
0
  }
242
243
0
  pfree(elems);
244
0
  if (arr != DatumGetPointer(array))
245
0
    pfree(arr);
246
247
0
  return bms;
248
0
}
249
250
/*
251
 * Flush all cache entries when pg_event_trigger is updated.
252
 *
253
 * This should be rare enough that we don't need to be very granular about
254
 * it, so we just blow away everything, which also avoids the possibility of
255
 * memory leaks.
256
 */
257
static void
258
InvalidateEventCacheCallback(Datum arg, SysCacheIdentifier cacheid,
259
               uint32 hashvalue)
260
0
{
261
  /*
262
   * If the cache isn't valid, then there might be a rebuild in progress, so
263
   * we can't immediately blow it away.  But it's advantageous to do this
264
   * when possible, so as to immediately free memory.
265
   */
266
0
  if (EventTriggerCacheState == ETCS_VALID)
267
0
  {
268
0
    MemoryContextReset(EventTriggerCacheContext);
269
0
    EventTriggerCache = NULL;
270
0
  }
271
272
  /* Mark cache for rebuild. */
273
0
  EventTriggerCacheState = ETCS_NEEDS_REBUILD;
274
0
}