PostgreSQL Source Code  git master
 All Data Structures Namespaces Files Functions Variables Typedefs Enumerations Enumerator Macros
timetravel.c
Go to the documentation of this file.
1 /*
2  * contrib/spi/timetravel.c
3  *
4  *
5  * timetravel.c -- function to get time travel feature
6  * using general triggers.
7  *
8  * Modified by BÖJTHE Zoltán, Hungary, mailto:urdesobt@axelero.hu
9  */
10 #include "postgres.h"
11 
12 #include <ctype.h>
13 
14 #include "access/htup_details.h"
15 #include "catalog/pg_type.h"
16 #include "commands/trigger.h"
17 #include "executor/spi.h"
18 #include "miscadmin.h"
19 #include "utils/builtins.h"
20 #include "utils/nabstime.h"
21 #include "utils/rel.h"
22 
24 
25 /* AbsoluteTime currabstime(void); */
26 
27 typedef struct
28 {
29  char *ident;
31 } EPlan;
32 
33 static EPlan *Plans = NULL; /* for UPDATE/DELETE */
34 static int nPlans = 0;
35 
36 typedef struct _TTOffList
37 {
38  struct _TTOffList *next;
39  char name[FLEXIBLE_ARRAY_MEMBER];
40 } TTOffList;
41 
42 static TTOffList *TTOff = NULL;
43 
44 static int findTTStatus(char *name);
45 static EPlan *find_plan(char *ident, EPlan **eplan, int *nplans);
46 
47 /*
48  * timetravel () --
49  * 1. IF an update affects tuple with stop_date eq INFINITY
50  * then form (and return) new tuple with start_date eq current date
51  * and stop_date eq INFINITY [ and update_user eq current user ]
52  * and all other column values as in new tuple, and insert tuple
53  * with old data and stop_date eq current date
54  * ELSE - skip updation of tuple.
55  * 2. IF a delete affects tuple with stop_date eq INFINITY
56  * then insert the same tuple with stop_date eq current date
57  * [ and delete_user eq current user ]
58  * ELSE - skip deletion of tuple.
59  * 3. On INSERT, if start_date is NULL then current date will be
60  * inserted, if stop_date is NULL then INFINITY will be inserted.
61  * [ and insert_user eq current user, update_user and delete_user
62  * eq NULL ]
63  *
64  * In CREATE TRIGGER you are to specify start_date and stop_date column
65  * names:
66  * EXECUTE PROCEDURE
67  * timetravel ('date_on', 'date_off' [,'insert_user', 'update_user', 'delete_user' ] ).
68  */
69 
70 #define MaxAttrNum 5
71 #define MinAttrNum 2
72 
73 #define a_time_on 0
74 #define a_time_off 1
75 #define a_ins_user 2
76 #define a_upd_user 3
77 #define a_del_user 4
78 
80 
81 Datum /* have to return HeapTuple to Executor */
83 {
84  TriggerData *trigdata = (TriggerData *) fcinfo->context;
85  Trigger *trigger; /* to get trigger name */
86  int argc;
87  char **args; /* arguments */
88  int attnum[MaxAttrNum]; /* fnumbers of start/stop columns */
89  Datum oldtimeon,
90  oldtimeoff;
91  Datum newtimeon,
92  newtimeoff,
93  newuser,
94  nulltext;
95  Datum *cvals; /* column values */
96  char *cnulls; /* column nulls */
97  char *relname; /* triggered relation name */
98  Relation rel; /* triggered relation */
99  HeapTuple trigtuple;
100  HeapTuple newtuple = NULL;
101  HeapTuple rettuple;
102  TupleDesc tupdesc; /* tuple description */
103  int natts; /* # of attributes */
104  EPlan *plan; /* prepared plan */
105  char ident[2 * NAMEDATALEN];
106  bool isnull; /* to know is some column NULL or not */
107  bool isinsert = false;
108  int ret;
109  int i;
110 
111  /*
112  * Some checks first...
113  */
114 
115  /* Called by trigger manager ? */
116  if (!CALLED_AS_TRIGGER(fcinfo))
117  elog(ERROR, "timetravel: not fired by trigger manager");
118 
119  /* Should be called for ROW trigger */
120  if (!TRIGGER_FIRED_FOR_ROW(trigdata->tg_event))
121  elog(ERROR, "timetravel: must be fired for row");
122 
123  /* Should be called BEFORE */
124  if (!TRIGGER_FIRED_BEFORE(trigdata->tg_event))
125  elog(ERROR, "timetravel: must be fired before event");
126 
127  /* INSERT ? */
128  if (TRIGGER_FIRED_BY_INSERT(trigdata->tg_event))
129  isinsert = true;
130 
131  if (TRIGGER_FIRED_BY_UPDATE(trigdata->tg_event))
132  newtuple = trigdata->tg_newtuple;
133 
134  trigtuple = trigdata->tg_trigtuple;
135 
136  rel = trigdata->tg_relation;
137  relname = SPI_getrelname(rel);
138 
139  /* check if TT is OFF for this relation */
140  if (0 == findTTStatus(relname))
141  {
142  /* OFF - nothing to do */
143  pfree(relname);
144  return PointerGetDatum((newtuple != NULL) ? newtuple : trigtuple);
145  }
146 
147  trigger = trigdata->tg_trigger;
148 
149  argc = trigger->tgnargs;
150  if (argc != MinAttrNum && argc != MaxAttrNum)
151  elog(ERROR, "timetravel (%s): invalid (!= %d or %d) number of arguments %d",
152  relname, MinAttrNum, MaxAttrNum, trigger->tgnargs);
153 
154  args = trigger->tgargs;
155  tupdesc = rel->rd_att;
156  natts = tupdesc->natts;
157 
158  for (i = 0; i < MinAttrNum; i++)
159  {
160  attnum[i] = SPI_fnumber(tupdesc, args[i]);
161  if (attnum[i] <= 0)
162  elog(ERROR, "timetravel (%s): there is no attribute %s", relname, args[i]);
163  if (SPI_gettypeid(tupdesc, attnum[i]) != ABSTIMEOID)
164  elog(ERROR, "timetravel (%s): attribute %s must be of abstime type",
165  relname, args[i]);
166  }
167  for (; i < argc; i++)
168  {
169  attnum[i] = SPI_fnumber(tupdesc, args[i]);
170  if (attnum[i] <= 0)
171  elog(ERROR, "timetravel (%s): there is no attribute %s", relname, args[i]);
172  if (SPI_gettypeid(tupdesc, attnum[i]) != TEXTOID)
173  elog(ERROR, "timetravel (%s): attribute %s must be of text type",
174  relname, args[i]);
175  }
176 
177  /* create fields containing name */
178  newuser = CStringGetTextDatum(GetUserNameFromId(GetUserId(), false));
179 
180  nulltext = (Datum) NULL;
181 
182  if (isinsert)
183  { /* INSERT */
184  int chnattrs = 0;
185  int chattrs[MaxAttrNum];
186  Datum newvals[MaxAttrNum];
187  bool newnulls[MaxAttrNum];
188 
189  oldtimeon = SPI_getbinval(trigtuple, tupdesc, attnum[a_time_on], &isnull);
190  if (isnull)
191  {
192  newvals[chnattrs] = GetCurrentAbsoluteTime();
193  newnulls[chnattrs] = false;
194  chattrs[chnattrs] = attnum[a_time_on];
195  chnattrs++;
196  }
197 
198  oldtimeoff = SPI_getbinval(trigtuple, tupdesc, attnum[a_time_off], &isnull);
199  if (isnull)
200  {
201  if ((chnattrs == 0 && DatumGetInt32(oldtimeon) >= NOEND_ABSTIME) ||
202  (chnattrs > 0 && DatumGetInt32(newvals[a_time_on]) >= NOEND_ABSTIME))
203  elog(ERROR, "timetravel (%s): %s is infinity", relname, args[a_time_on]);
204  newvals[chnattrs] = NOEND_ABSTIME;
205  newnulls[chnattrs] = false;
206  chattrs[chnattrs] = attnum[a_time_off];
207  chnattrs++;
208  }
209  else
210  {
211  if ((chnattrs == 0 && DatumGetInt32(oldtimeon) > DatumGetInt32(oldtimeoff)) ||
212  (chnattrs > 0 && DatumGetInt32(newvals[a_time_on]) > DatumGetInt32(oldtimeoff)))
213  elog(ERROR, "timetravel (%s): %s gt %s", relname, args[a_time_on], args[a_time_off]);
214  }
215 
216  pfree(relname);
217  if (chnattrs <= 0)
218  return PointerGetDatum(trigtuple);
219 
220  if (argc == MaxAttrNum)
221  {
222  /* clear update_user value */
223  newvals[chnattrs] = nulltext;
224  newnulls[chnattrs] = true;
225  chattrs[chnattrs] = attnum[a_upd_user];
226  chnattrs++;
227  /* clear delete_user value */
228  newvals[chnattrs] = nulltext;
229  newnulls[chnattrs] = true;
230  chattrs[chnattrs] = attnum[a_del_user];
231  chnattrs++;
232  /* set insert_user value */
233  newvals[chnattrs] = newuser;
234  newnulls[chnattrs] = false;
235  chattrs[chnattrs] = attnum[a_ins_user];
236  chnattrs++;
237  }
238  rettuple = heap_modify_tuple_by_cols(trigtuple, tupdesc,
239  chnattrs, chattrs,
240  newvals, newnulls);
241  return PointerGetDatum(rettuple);
242  /* end of INSERT */
243  }
244 
245  /* UPDATE/DELETE: */
246  oldtimeon = SPI_getbinval(trigtuple, tupdesc, attnum[a_time_on], &isnull);
247  if (isnull)
248  elog(ERROR, "timetravel (%s): %s must be NOT NULL", relname, args[a_time_on]);
249 
250  oldtimeoff = SPI_getbinval(trigtuple, tupdesc, attnum[a_time_off], &isnull);
251  if (isnull)
252  elog(ERROR, "timetravel (%s): %s must be NOT NULL", relname, args[a_time_off]);
253 
254  /*
255  * If DELETE/UPDATE of tuple with stop_date neq INFINITY then say upper
256  * Executor to skip operation for this tuple
257  */
258  if (newtuple != NULL)
259  { /* UPDATE */
260  newtimeon = SPI_getbinval(newtuple, tupdesc, attnum[a_time_on], &isnull);
261  if (isnull)
262  elog(ERROR, "timetravel (%s): %s must be NOT NULL", relname, args[a_time_on]);
263 
264  newtimeoff = SPI_getbinval(newtuple, tupdesc, attnum[a_time_off], &isnull);
265  if (isnull)
266  elog(ERROR, "timetravel (%s): %s must be NOT NULL", relname, args[a_time_off]);
267 
268  if (oldtimeon != newtimeon || oldtimeoff != newtimeoff)
269  elog(ERROR, "timetravel (%s): you cannot change %s and/or %s columns (use set_timetravel)",
270  relname, args[a_time_on], args[a_time_off]);
271  }
272  if (oldtimeoff != NOEND_ABSTIME)
273  { /* current record is a deleted/updated record */
274  pfree(relname);
275  return PointerGetDatum(NULL);
276  }
277 
278  newtimeoff = GetCurrentAbsoluteTime();
279 
280  /* Connect to SPI manager */
281  if ((ret = SPI_connect()) < 0)
282  elog(ERROR, "timetravel (%s): SPI_connect returned %d", relname, ret);
283 
284  /* Fetch tuple values and nulls */
285  cvals = (Datum *) palloc(natts * sizeof(Datum));
286  cnulls = (char *) palloc(natts * sizeof(char));
287  for (i = 0; i < natts; i++)
288  {
289  cvals[i] = SPI_getbinval(trigtuple, tupdesc, i + 1, &isnull);
290  cnulls[i] = (isnull) ? 'n' : ' ';
291  }
292 
293  /* change date column(s) */
294  cvals[attnum[a_time_off] - 1] = newtimeoff; /* stop_date eq current date */
295  cnulls[attnum[a_time_off] - 1] = ' ';
296 
297  if (!newtuple)
298  { /* DELETE */
299  if (argc == MaxAttrNum)
300  {
301  cvals[attnum[a_del_user] - 1] = newuser; /* set delete user */
302  cnulls[attnum[a_del_user] - 1] = ' ';
303  }
304  }
305 
306  /*
307  * Construct ident string as TriggerName $ TriggeredRelationId and try to
308  * find prepared execution plan.
309  */
310  snprintf(ident, sizeof(ident), "%s$%u", trigger->tgname, rel->rd_id);
311  plan = find_plan(ident, &Plans, &nPlans);
312 
313  /* if there is no plan ... */
314  if (plan->splan == NULL)
315  {
316  SPIPlanPtr pplan;
317  Oid *ctypes;
318  char sql[8192];
319  char separ = ' ';
320 
321  /* allocate ctypes for preparation */
322  ctypes = (Oid *) palloc(natts * sizeof(Oid));
323 
324  /*
325  * Construct query: INSERT INTO _relation_ VALUES ($1, ...)
326  */
327  snprintf(sql, sizeof(sql), "INSERT INTO %s VALUES (", relname);
328  for (i = 1; i <= natts; i++)
329  {
330  ctypes[i - 1] = SPI_gettypeid(tupdesc, i);
331  if (!(tupdesc->attrs[i - 1]->attisdropped)) /* skip dropped columns */
332  {
333  snprintf(sql + strlen(sql), sizeof(sql) - strlen(sql), "%c$%d", separ, i);
334  separ = ',';
335  }
336  }
337  snprintf(sql + strlen(sql), sizeof(sql) - strlen(sql), ")");
338 
339  elog(DEBUG4, "timetravel (%s) update: sql: %s", relname, sql);
340 
341  /* Prepare plan for query */
342  pplan = SPI_prepare(sql, natts, ctypes);
343  if (pplan == NULL)
344  elog(ERROR, "timetravel (%s): SPI_prepare returned %d", relname, SPI_result);
345 
346  /*
347  * Remember that SPI_prepare places plan in current memory context -
348  * so, we have to save plan in Top memory context for later use.
349  */
350  if (SPI_keepplan(pplan))
351  elog(ERROR, "timetravel (%s): SPI_keepplan failed", relname);
352 
353  plan->splan = pplan;
354  }
355 
356  /*
357  * Ok, execute prepared plan.
358  */
359  ret = SPI_execp(plan->splan, cvals, cnulls, 0);
360 
361  if (ret < 0)
362  elog(ERROR, "timetravel (%s): SPI_execp returned %d", relname, ret);
363 
364  /* Tuple to return to upper Executor ... */
365  if (newtuple)
366  { /* UPDATE */
367  int chnattrs = 0;
368  int chattrs[MaxAttrNum];
369  Datum newvals[MaxAttrNum];
370  char newnulls[MaxAttrNum];
371 
372  newvals[chnattrs] = newtimeoff;
373  newnulls[chnattrs] = ' ';
374  chattrs[chnattrs] = attnum[a_time_on];
375  chnattrs++;
376 
377  newvals[chnattrs] = NOEND_ABSTIME;
378  newnulls[chnattrs] = ' ';
379  chattrs[chnattrs] = attnum[a_time_off];
380  chnattrs++;
381 
382  if (argc == MaxAttrNum)
383  {
384  /* set update_user value */
385  newvals[chnattrs] = newuser;
386  newnulls[chnattrs] = ' ';
387  chattrs[chnattrs] = attnum[a_upd_user];
388  chnattrs++;
389  /* clear delete_user value */
390  newvals[chnattrs] = nulltext;
391  newnulls[chnattrs] = 'n';
392  chattrs[chnattrs] = attnum[a_del_user];
393  chnattrs++;
394  /* set insert_user value */
395  newvals[chnattrs] = nulltext;
396  newnulls[chnattrs] = 'n';
397  chattrs[chnattrs] = attnum[a_ins_user];
398  chnattrs++;
399  }
400 
401  /*
402  * Use SPI_modifytuple() here because we are inside SPI environment
403  * but rettuple must be allocated in caller's context.
404  */
405  rettuple = SPI_modifytuple(rel, newtuple, chnattrs, chattrs, newvals, newnulls);
406  }
407  else
408  /* DELETE case */
409  rettuple = trigtuple;
410 
411  SPI_finish(); /* don't forget say Bye to SPI mgr */
412 
413  pfree(relname);
414  return PointerGetDatum(rettuple);
415 }
416 
417 /*
418  * set_timetravel (relname, on) --
419  * turn timetravel for specified relation ON/OFF
420  */
422 
423 Datum
425 {
426  Name relname = PG_GETARG_NAME(0);
427  int32 on = PG_GETARG_INT32(1);
428  char *rname;
429  char *d;
430  char *s;
431  int32 ret;
432  TTOffList *prev,
433  *pp;
434 
435  prev = NULL;
436  for (pp = TTOff; pp; prev = pp, pp = pp->next)
437  {
438  if (namestrcmp(relname, pp->name) == 0)
439  break;
440  }
441  if (pp)
442  {
443  /* OFF currently */
444  if (on != 0)
445  {
446  /* turn ON */
447  if (prev)
448  prev->next = pp->next;
449  else
450  TTOff = pp->next;
451  free(pp);
452  }
453  ret = 0;
454  }
455  else
456  {
457  /* ON currently */
458  if (on == 0)
459  {
460  /* turn OFF */
462  if (s)
463  {
464  pp = malloc(offsetof(TTOffList, name) +strlen(rname) + 1);
465  if (pp)
466  {
467  pp->next = NULL;
468  d = pp->name;
469  while (*s)
470  *d++ = tolower((unsigned char) *s++);
471  *d = '\0';
472  if (prev)
473  prev->next = pp;
474  else
475  TTOff = pp;
476  }
477  pfree(rname);
478  }
479  }
480  ret = 1;
481  }
482  PG_RETURN_INT32(ret);
483 }
484 
485 /*
486  * get_timetravel (relname) --
487  * get timetravel status for specified relation (ON/OFF)
488  */
490 
491 Datum
493 {
494  Name relname = PG_GETARG_NAME(0);
495  TTOffList *pp;
496 
497  for (pp = TTOff; pp; pp = pp->next)
498  {
499  if (namestrcmp(relname, pp->name) == 0)
500  PG_RETURN_INT32(0);
501  }
502  PG_RETURN_INT32(1);
503 }
504 
505 static int
507 {
508  TTOffList *pp;
509 
510  for (pp = TTOff; pp; pp = pp->next)
511  if (pg_strcasecmp(name, pp->name) == 0)
512  return 0;
513  return 1;
514 }
515 
516 /*
517 AbsoluteTime
518 currabstime()
519 {
520  return (GetCurrentAbsoluteTime());
521 }
522 */
523 
524 static EPlan *
525 find_plan(char *ident, EPlan **eplan, int *nplans)
526 {
527  EPlan *newp;
528  int i;
529 
530  if (*nplans > 0)
531  {
532  for (i = 0; i < *nplans; i++)
533  {
534  if (strcmp((*eplan)[i].ident, ident) == 0)
535  break;
536  }
537  if (i != *nplans)
538  return (*eplan + i);
539  *eplan = (EPlan *) realloc(*eplan, (i + 1) * sizeof(EPlan));
540  newp = *eplan + i;
541  }
542  else
543  {
544  newp = *eplan = (EPlan *) malloc(sizeof(EPlan));
545  (*nplans) = i = 0;
546  }
547 
548  newp->ident = strdup(ident);
549  newp->splan = NULL;
550  (*nplans)++;
551 
552  return (newp);
553 }
int SPI_fnumber(TupleDesc tupdesc, const char *fname)
Definition: spi.c:760
#define a_del_user
Definition: timetravel.c:77
#define PG_GETARG_INT32(n)
Definition: fmgr.h:225
#define MaxAttrNum
Definition: timetravel.c:70
Oid SPI_gettypeid(TupleDesc tupdesc, int fnumber)
Definition: spi.c:891
#define NameGetDatum(X)
Definition: postgres.h:603
#define MinAttrNum
Definition: timetravel.c:71
#define DatumGetInt32(X)
Definition: postgres.h:480
static TTOffList * TTOff
Definition: timetravel.c:42
Oid GetUserId(void)
Definition: miscinit.c:283
int SPI_connect(void)
Definition: spi.c:84
#define TEXTOID
Definition: pg_type.h:324
#define PointerGetDatum(X)
Definition: postgres.h:564
SPIPlanPtr SPI_prepare(const char *src, int nargs, Oid *argtypes)
Definition: spi.c:481
int SPI_finish(void)
Definition: spi.c:147
#define PG_RETURN_INT32(x)
Definition: fmgr.h:298
int namestrcmp(Name name, const char *str)
Definition: name.c:248
int snprintf(char *str, size_t count, const char *fmt,...) pg_attribute_printf(3
#define DirectFunctionCall1(func, arg1)
Definition: fmgr.h:555
int pg_strcasecmp(const char *s1, const char *s2)
Definition: pgstrcasecmp.c:36
static int nPlans
Definition: timetravel.c:34
unsigned int Oid
Definition: postgres_ext.h:31
#define DEBUG4
Definition: elog.h:22
#define a_upd_user
Definition: timetravel.c:76
HeapTuple tg_trigtuple
Definition: trigger.h:35
signed int int32
Definition: c.h:253
#define malloc(a)
Definition: header.h:45
#define NAMEDATALEN
int SPI_result
Definition: spi.c:42
PG_FUNCTION_INFO_V1(timetravel)
void pfree(void *pointer)
Definition: mcxt.c:992
struct _TTOffList TTOffList
PG_MODULE_MAGIC
Definition: timetravel.c:23
#define ERROR
Definition: elog.h:43
#define DatumGetCString(X)
Definition: postgres.h:574
SPIPlanPtr splan
Definition: timetravel.c:30
Datum SPI_getbinval(HeapTuple tuple, TupleDesc tupdesc, int fnumber, bool *isnull)
Definition: spi.c:835
Definition: c.h:488
int SPI_keepplan(SPIPlanPtr plan)
Definition: spi.c:559
struct _TTOffList * next
Definition: timetravel.c:38
AbsoluteTime GetCurrentAbsoluteTime(void)
Definition: nabstime.c:88
#define a_ins_user
Definition: timetravel.c:75
#define a_time_on
Definition: timetravel.c:73
HeapTuple SPI_modifytuple(Relation rel, HeapTuple tuple, int natts, int *attnum, Datum *Values, const char *Nulls)
Definition: spi.c:689
SPIPlanPtr * splan
Definition: refint.c:23
uintptr_t Datum
Definition: postgres.h:374
Datum get_timetravel(PG_FUNCTION_ARGS)
Definition: timetravel.c:492
Trigger * tg_trigger
Definition: trigger.h:37
HeapTuple tg_newtuple
Definition: trigger.h:36
Datum timetravel(PG_FUNCTION_ARGS)
Definition: timetravel.c:82
char * SPI_getrelname(Relation rel)
Definition: spi.c:909
#define free(a)
Definition: header.h:60
static EPlan * Plans
Definition: timetravel.c:33
char * ident
Definition: refint.c:21
#define NULL
Definition: c.h:226
#define CALLED_AS_TRIGGER(fcinfo)
Definition: trigger.h:25
char * GetUserNameFromId(Oid roleid, bool noerr)
Definition: miscinit.c:691
TriggerEvent tg_event
Definition: trigger.h:33
Definition: refint.c:19
static EPlan * find_plan(char *ident, EPlan **eplan, int *nplans)
Definition: timetravel.c:525
#define realloc(a, b)
Definition: header.h:55
#define a_time_off
Definition: timetravel.c:74
const char * name
Definition: encode.c:521
HeapTuple heap_modify_tuple_by_cols(HeapTuple tuple, TupleDesc tupleDesc, int nCols, int *replCols, Datum *replValues, bool *replIsnull)
Definition: heaptuple.c:864
#define TRIGGER_FIRED_BEFORE(event)
Definition: trigger.h:88
#define TRIGGER_FIRED_BY_INSERT(event)
Definition: trigger.h:70
void * palloc(Size size)
Definition: mcxt.c:891
Datum set_timetravel(PG_FUNCTION_ARGS)
Definition: timetravel.c:424
int i
int SPI_execp(SPIPlanPtr plan, Datum *Values, const char *Nulls, long tcount)
Definition: spi.c:365
int16 tgnargs
Definition: reltrigger.h:37
#define CStringGetTextDatum(s)
Definition: builtins.h:90
#define PG_FUNCTION_ARGS
Definition: fmgr.h:150
#define elog
Definition: elog.h:219
char name[FLEXIBLE_ARRAY_MEMBER]
Definition: timetravel.c:39
Datum nameout(PG_FUNCTION_ARGS)
Definition: name.c:69
#define TRIGGER_FIRED_FOR_ROW(event)
Definition: trigger.h:82
#define ABSTIMEOID
Definition: pg_type.h:414
#define NOEND_ABSTIME
Definition: nabstime.h:77
#define TRIGGER_FIRED_BY_UPDATE(event)
Definition: trigger.h:76
#define offsetof(type, field)
Definition: c.h:550
static int findTTStatus(char *name)
Definition: timetravel.c:506
#define PG_GETARG_NAME(n)
Definition: fmgr.h:234
Relation tg_relation
Definition: trigger.h:34