5 * timetravel.c -- function to get time travel feature
6 * using general triggers.
9 /* Modified by BÖJTHE Zoltán, Hungary, mailto:urdesobt@axelero.hu */
11 #include "executor/spi.h" /* this is what you need to work with SPI */
12 #include "commands/trigger.h" /* -"- and triggers */
13 #include "miscadmin.h" /* for GetPgUserName() */
14 #include "utils/nabstime.h"
16 #include <ctype.h> /* tolower () */
18 #define ABSTIMEOID 702 /* it should be in pg_type.h */
22 /* AbsoluteTime currabstime(void); */
23 Datum
timetravel(PG_FUNCTION_ARGS
);
24 Datum
set_timetravel(PG_FUNCTION_ARGS
);
25 Datum
get_timetravel(PG_FUNCTION_ARGS
);
33 static EPlan
*Plans
= NULL
; /* for UPDATE/DELETE */
34 static int nPlans
= 0;
36 typedef struct _TTOffList
38 struct _TTOffList
*next
;
42 static TTOffList TTOff
= {NULL
, {0}};
44 static int findTTStatus(char *name
);
45 static EPlan
*find_plan(char *ident
, EPlan
** eplan
, int *nplans
);
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 an 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
64 * In CREATE TRIGGER you are to specify start_date and stop_date column
67 * timetravel ('date_on', 'date_off' [,'insert_user', 'update_user', 'delete_user' ] ).
79 PG_FUNCTION_INFO_V1(timetravel
);
81 Datum
/* have to return HeapTuple to Executor */
82 timetravel(PG_FUNCTION_ARGS
)
84 TriggerData
*trigdata
= (TriggerData
*) fcinfo
->context
;
85 Trigger
*trigger
; /* to get trigger name */
87 char **args
; /* arguments */
88 int attnum
[MaxAttrNum
]; /* fnumbers of start/stop columns */
95 Datum
*cvals
; /* column values */
96 char *cnulls
; /* column nulls */
97 char *relname
; /* triggered relation name */
98 Relation rel
; /* triggered relation */
100 HeapTuple newtuple
= NULL
;
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;
112 * Some checks first...
115 /* Called by trigger manager ? */
116 if (!CALLED_AS_TRIGGER(fcinfo
))
117 elog(ERROR
, "timetravel: not fired by trigger manager");
119 /* Should be called for ROW trigger */
120 if (TRIGGER_FIRED_FOR_STATEMENT(trigdata
->tg_event
))
121 elog(ERROR
, "timetravel: cannot process STATEMENT events");
123 /* Should be called BEFORE */
124 if (TRIGGER_FIRED_AFTER(trigdata
->tg_event
))
125 elog(ERROR
, "timetravel: must be fired before event");
128 if (TRIGGER_FIRED_BY_INSERT(trigdata
->tg_event
))
131 if (TRIGGER_FIRED_BY_UPDATE(trigdata
->tg_event
))
132 newtuple
= trigdata
->tg_newtuple
;
134 trigtuple
= trigdata
->tg_trigtuple
;
136 rel
= trigdata
->tg_relation
;
137 relname
= SPI_getrelname(rel
);
139 /* check if TT is OFF for this relation */
140 if (0 == findTTStatus(relname
))
142 /* OFF - nothing to do */
144 return PointerGetDatum((newtuple
!= NULL
) ? newtuple
: trigtuple
);
147 trigger
= trigdata
->tg_trigger
;
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
);
154 args
= trigger
->tgargs
;
155 tupdesc
= rel
->rd_att
;
156 natts
= tupdesc
->natts
;
158 for (i
= 0; i
< MinAttrNum
; i
++)
160 attnum
[i
] = SPI_fnumber(tupdesc
, args
[i
]);
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",
167 for (; i
< argc
; i
++)
169 attnum
[i
] = SPI_fnumber(tupdesc
, args
[i
]);
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",
177 /* create fields containing name */
178 newuser
= CStringGetTextDatum(GetUserNameFromId(GetUserId()));
180 nulltext
= (Datum
) NULL
;
185 int chattrs
[MaxAttrNum
];
186 Datum newvals
[MaxAttrNum
];
187 char newnulls
[MaxAttrNum
];
189 oldtimeon
= SPI_getbinval(trigtuple
, tupdesc
, attnum
[a_time_on
], &isnull
);
192 newvals
[chnattrs
] = GetCurrentAbsoluteTime();
193 newnulls
[chnattrs
] = ' ';
194 chattrs
[chnattrs
] = attnum
[a_time_on
];
198 oldtimeoff
= SPI_getbinval(trigtuple
, tupdesc
, attnum
[a_time_off
], &isnull
);
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
] = ' ';
206 chattrs
[chnattrs
] = attnum
[a_time_off
];
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
]);
218 return PointerGetDatum(trigtuple
);
220 if (argc
== MaxAttrNum
)
222 /* clear update_user value */
223 newvals
[chnattrs
] = nulltext
;
224 newnulls
[chnattrs
] = 'n';
225 chattrs
[chnattrs
] = attnum
[a_upd_user
];
227 /* clear delete_user value */
228 newvals
[chnattrs
] = nulltext
;
229 newnulls
[chnattrs
] = 'n';
230 chattrs
[chnattrs
] = attnum
[a_del_user
];
232 /* set insert_user value */
233 newvals
[chnattrs
] = newuser
;
234 newnulls
[chnattrs
] = ' ';
235 chattrs
[chnattrs
] = attnum
[a_ins_user
];
238 rettuple
= SPI_modifytuple(rel
, trigtuple
, chnattrs
, chattrs
, newvals
, newnulls
);
239 return PointerGetDatum(rettuple
);
244 oldtimeon
= SPI_getbinval(trigtuple
, tupdesc
, attnum
[a_time_on
], &isnull
);
246 elog(ERROR
, "timetravel (%s): %s must be NOT NULL", relname
, args
[a_time_on
]);
248 oldtimeoff
= SPI_getbinval(trigtuple
, tupdesc
, attnum
[a_time_off
], &isnull
);
250 elog(ERROR
, "timetravel (%s): %s must be NOT NULL", relname
, args
[a_time_off
]);
253 * If DELETE/UPDATE of tuple with stop_date neq INFINITY then say upper
254 * Executor to skip operation for this tuple
256 if (newtuple
!= NULL
)
258 newtimeon
= SPI_getbinval(newtuple
, tupdesc
, attnum
[a_time_on
], &isnull
);
260 elog(ERROR
, "timetravel (%s): %s must be NOT NULL", relname
, args
[a_time_on
]);
262 newtimeoff
= SPI_getbinval(newtuple
, tupdesc
, attnum
[a_time_off
], &isnull
);
264 elog(ERROR
, "timetravel (%s): %s must be NOT NULL", relname
, args
[a_time_off
]);
266 if (oldtimeon
!= newtimeon
|| oldtimeoff
!= newtimeoff
)
267 elog(ERROR
, "timetravel (%s): you cannot change %s and/or %s columns (use set_timetravel)",
268 relname
, args
[a_time_on
], args
[a_time_off
]);
270 if (oldtimeoff
!= NOEND_ABSTIME
)
271 { /* current record is a deleted/updated record */
273 return PointerGetDatum(NULL
);
276 newtimeoff
= GetCurrentAbsoluteTime();
278 /* Connect to SPI manager */
279 if ((ret
= SPI_connect()) < 0)
280 elog(ERROR
, "timetravel (%s): SPI_connect returned %d", relname
, ret
);
282 /* Fetch tuple values and nulls */
283 cvals
= (Datum
*) palloc(natts
* sizeof(Datum
));
284 cnulls
= (char *) palloc(natts
* sizeof(char));
285 for (i
= 0; i
< natts
; i
++)
287 cvals
[i
] = SPI_getbinval(trigtuple
, tupdesc
, i
+ 1, &isnull
);
288 cnulls
[i
] = (isnull
) ? 'n' : ' ';
291 /* change date column(s) */
292 cvals
[attnum
[a_time_off
] - 1] = newtimeoff
; /* stop_date eq current date */
293 cnulls
[attnum
[a_time_off
] - 1] = ' ';
297 if (argc
== MaxAttrNum
)
299 cvals
[attnum
[a_del_user
] - 1] = newuser
; /* set delete user */
300 cnulls
[attnum
[a_del_user
] - 1] = ' ';
305 * Construct ident string as TriggerName $ TriggeredRelationId and try to
306 * find prepared execution plan.
308 snprintf(ident
, sizeof(ident
), "%s$%u", trigger
->tgname
, rel
->rd_id
);
309 plan
= find_plan(ident
, &Plans
, &nPlans
);
311 /* if there is no plan ... */
312 if (plan
->splan
== NULL
)
319 /* allocate ctypes for preparation */
320 ctypes
= (Oid
*) palloc(natts
* sizeof(Oid
));
323 * Construct query: INSERT INTO _relation_ VALUES ($1, ...)
325 snprintf(sql
, sizeof(sql
), "INSERT INTO %s VALUES (", relname
);
326 for (i
= 1; i
<= natts
; i
++)
328 ctypes
[i
- 1] = SPI_gettypeid(tupdesc
, i
);
329 if (!(tupdesc
->attrs
[i
- 1]->attisdropped
)) /* skip dropped columns */
331 snprintf(sql
+ strlen(sql
), sizeof(sql
) - strlen(sql
), "%c$%d", separ
, i
);
335 snprintf(sql
+ strlen(sql
), sizeof(sql
) - strlen(sql
), ")");
337 elog(DEBUG4
, "timetravel (%s) update: sql: %s", relname
, sql
);
339 /* Prepare plan for query */
340 pplan
= SPI_prepare(sql
, natts
, ctypes
);
342 elog(ERROR
, "timetravel (%s): SPI_prepare returned %d", relname
, SPI_result
);
345 * Remember that SPI_prepare places plan in current memory context -
346 * so, we have to save plan in Top memory context for latter use.
348 pplan
= SPI_saveplan(pplan
);
350 elog(ERROR
, "timetravel (%s): SPI_saveplan returned %d", relname
, SPI_result
);
356 * Ok, execute prepared plan.
358 ret
= SPI_execp(plan
->splan
, cvals
, cnulls
, 0);
361 elog(ERROR
, "timetravel (%s): SPI_execp returned %d", relname
, ret
);
363 /* Tuple to return to upper Executor ... */
367 int chattrs
[MaxAttrNum
];
368 Datum newvals
[MaxAttrNum
];
369 char newnulls
[MaxAttrNum
];
371 newvals
[chnattrs
] = newtimeoff
;
372 newnulls
[chnattrs
] = ' ';
373 chattrs
[chnattrs
] = attnum
[a_time_on
];
376 newvals
[chnattrs
] = NOEND_ABSTIME
;
377 newnulls
[chnattrs
] = ' ';
378 chattrs
[chnattrs
] = attnum
[a_time_off
];
381 if (argc
== MaxAttrNum
)
383 /* set update_user value */
384 newvals
[chnattrs
] = newuser
;
385 newnulls
[chnattrs
] = ' ';
386 chattrs
[chnattrs
] = attnum
[a_upd_user
];
388 /* clear delete_user value */
389 newvals
[chnattrs
] = nulltext
;
390 newnulls
[chnattrs
] = 'n';
391 chattrs
[chnattrs
] = attnum
[a_del_user
];
393 /* set insert_user value */
394 newvals
[chnattrs
] = nulltext
;
395 newnulls
[chnattrs
] = 'n';
396 chattrs
[chnattrs
] = attnum
[a_ins_user
];
400 rettuple
= SPI_modifytuple(rel
, newtuple
, chnattrs
, chattrs
, newvals
, newnulls
);
403 * SPI_copytuple allocates tmptuple in upper executor context - have
404 * to free allocation using SPI_pfree
406 /* SPI_pfree(tmptuple); */
410 rettuple
= trigtuple
;
412 SPI_finish(); /* don't forget say Bye to SPI mgr */
415 return PointerGetDatum(rettuple
);
419 * set_timetravel (relname, on) --
420 * turn timetravel for specified relation ON/OFF
422 PG_FUNCTION_INFO_V1(set_timetravel
);
425 set_timetravel(PG_FUNCTION_ARGS
)
427 Name relname
= PG_GETARG_NAME(0);
428 int32 on
= PG_GETARG_INT32(1);
436 for (pp
= (p
= &TTOff
)->next
; pp
; pp
= (p
= pp
)->next
)
438 if (namestrcmp(relname
, pp
->name
) == 0)
458 s
= rname
= DatumGetCString(DirectFunctionCall1(nameout
, NameGetDatum(relname
)));
461 pp
= malloc(sizeof(TTOffList
) + strlen(rname
));
468 *d
++ = tolower((unsigned char) *s
++);
476 PG_RETURN_INT32(ret
);
480 * get_timetravel (relname) --
481 * get timetravel status for specified relation (ON/OFF)
483 PG_FUNCTION_INFO_V1(get_timetravel
);
486 get_timetravel(PG_FUNCTION_ARGS
)
488 Name relname
= PG_GETARG_NAME(0);
491 for (pp
= TTOff
.next
; pp
; pp
= pp
->next
)
493 if (namestrcmp(relname
, pp
->name
) == 0)
500 findTTStatus(char *name
)
504 for (pp
= TTOff
.next
; pp
; pp
= pp
->next
)
505 if (pg_strcasecmp(name
, pp
->name
) == 0)
514 return (GetCurrentAbsoluteTime());
519 find_plan(char *ident
, EPlan
** eplan
, int *nplans
)
526 for (i
= 0; i
< *nplans
; i
++)
528 if (strcmp((*eplan
)[i
].ident
, ident
) == 0)
533 *eplan
= (EPlan
*) realloc(*eplan
, (i
+ 1) * sizeof(EPlan
));
538 newp
= *eplan
= (EPlan
*) malloc(sizeof(EPlan
));
542 newp
->ident
= (char *) malloc(strlen(ident
) + 1);
543 strcpy(newp
->ident
, ident
);