mirror of https://github.com/postgres/postgres
parent
f0b9b2152e
commit
af5c86e2d1
@ -0,0 +1,372 @@ |
||||
/*
|
||||
* timetravel.c -- function to get time travel feature |
||||
* using general triggers. |
||||
*/ |
||||
|
||||
#include "executor/spi.h" /* this is what you need to work with SPI */ |
||||
#include "commands/trigger.h" /* -"- and triggers */ |
||||
#include <ctype.h> /* tolower () */ |
||||
|
||||
#define ABSTIMEOID 702 /* it should be in pg_type.h */ |
||||
|
||||
AbsoluteTime currabstime(void); |
||||
HeapTuple timetravel(void); |
||||
int32 set_timetravel(Name relname, int32 on); |
||||
|
||||
typedef struct |
||||
{ |
||||
char *ident; |
||||
void *splan; |
||||
} EPlan; |
||||
|
||||
static EPlan *Plans = NULL; /* for UPDATE/DELETE */ |
||||
static int nPlans = 0; |
||||
|
||||
static char **TTOff = NULL; |
||||
static int nTTOff = 0; |
||||
|
||||
static EPlan *find_plan(char *ident, EPlan ** eplan, int *nplans); |
||||
|
||||
/*
|
||||
* timetravel () --
|
||||
* 1. IF an update affects tuple with stop_date eq INFINITY
|
||||
* then form (and return) new tuple with stop_date eq current date
|
||||
* and all other column values as in old tuple, and insert tuple
|
||||
* with new data and start_date eq current date and
|
||||
* stop_date eq INFINITY |
||||
* ELSE - skip updation of tuple. |
||||
* 2. IF an delete affects tuple with stop_date eq INFINITY |
||||
* then insert the same tuple with stop_date eq current date |
||||
* ELSE - skip deletion of tuple. |
||||
*
|
||||
* In CREATE TRIGGER you are to specify start_date and stop_date column |
||||
* names: |
||||
* EXECUTE PROCEDURE |
||||
* timetravel ('date_on', 'date_off'). |
||||
*/ |
||||
|
||||
HeapTuple /* have to return HeapTuple to Executor */ |
||||
timetravel() |
||||
{ |
||||
Trigger *trigger; /* to get trigger name */ |
||||
char **args; /* arguments */ |
||||
int attnum[2]; /* fnumbers of start/stop columns */ |
||||
Datum oldon, oldoff; |
||||
Datum newon, newoff; |
||||
Datum *cvals; /* column values */ |
||||
char *cnulls; /* column nulls */ |
||||
char *relname; /* triggered relation name */ |
||||
Relation rel; /* triggered relation */ |
||||
HeapTuple trigtuple; |
||||
HeapTuple newtuple = NULL; |
||||
HeapTuple rettuple; |
||||
TupleDesc tupdesc; /* tuple description */ |
||||
int natts; /* # of attributes */ |
||||
EPlan *plan; /* prepared plan */ |
||||
char ident[2 * NAMEDATALEN]; |
||||
bool isnull; /* to know is some column NULL or not */ |
||||
int ret; |
||||
int i; |
||||
|
||||
/*
|
||||
* Some checks first... |
||||
*/ |
||||
|
||||
/* Called by trigger manager ? */ |
||||
if (!CurrentTriggerData) |
||||
elog(WARN, "timetravel: triggers are not initialized"); |
||||
|
||||
/* Should be called for ROW trigger */ |
||||
if (TRIGGER_FIRED_FOR_STATEMENT(CurrentTriggerData->tg_event)) |
||||
elog(WARN, "timetravel: can't process STATEMENT events"); |
||||
|
||||
/* Should be called BEFORE */ |
||||
if (TRIGGER_FIRED_AFTER(CurrentTriggerData->tg_event)) |
||||
elog(WARN, "timetravel: must be fired before event"); |
||||
|
||||
/* INSERT ? */ |
||||
if (TRIGGER_FIRED_BY_INSERT(CurrentTriggerData->tg_event)) |
||||
elog (WARN, "timetravel: can't process INSERT event"); |
||||
|
||||
if (TRIGGER_FIRED_BY_UPDATE(CurrentTriggerData->tg_event)) |
||||
newtuple = CurrentTriggerData->tg_newtuple; |
||||
|
||||
trigtuple = CurrentTriggerData->tg_trigtuple; |
||||
|
||||
rel = CurrentTriggerData->tg_relation; |
||||
relname = SPI_getrelname(rel); |
||||
|
||||
/* check if TT is OFF for this relation */ |
||||
for (i = 0; i < nTTOff; i++) |
||||
if (strcasecmp (TTOff[i], relname) == 0) |
||||
break; |
||||
if (i < nTTOff) /* OFF - nothing to do */ |
||||
{ |
||||
pfree (relname); |
||||
return ((newtuple != NULL) ? newtuple : trigtuple); |
||||
} |
||||
|
||||
trigger = CurrentTriggerData->tg_trigger; |
||||
|
||||
if (trigger->tgnargs != 2) |
||||
elog(WARN, "timetravel (%s): invalid (!= 2) number of arguments %d",
|
||||
relname, trigger->tgnargs); |
||||
|
||||
args = trigger->tgargs; |
||||
tupdesc = rel->rd_att; |
||||
natts = tupdesc->natts; |
||||
|
||||
/*
|
||||
* Setting CurrentTriggerData to NULL prevents direct calls to trigger |
||||
* functions in queries. Normally, trigger functions have to be called |
||||
* by trigger manager code only. |
||||
*/ |
||||
CurrentTriggerData = NULL; |
||||
|
||||
for (i = 0; i < 2; i++ ) |
||||
{ |
||||
attnum[i] = SPI_fnumber (tupdesc, args[i]); |
||||
if ( attnum[i] < 0 ) |
||||
elog(WARN, "timetravel (%s): there is no attribute %s", relname, args[i]); |
||||
if (SPI_gettypeid (tupdesc, attnum[i]) != ABSTIMEOID) |
||||
elog(WARN, "timetravel (%s): attributes %s and %s must be of abstime type",
|
||||
relname, args[0], args[1]); |
||||
} |
||||
|
||||
oldon = SPI_getbinval (trigtuple, tupdesc, attnum[0], &isnull); |
||||
if (isnull) |
||||
elog(WARN, "timetravel (%s): %s must be NOT NULL", relname, args[0]); |
||||
|
||||
oldoff = SPI_getbinval (trigtuple, tupdesc, attnum[1], &isnull); |
||||
if (isnull) |
||||
elog(WARN, "timetravel (%s): %s must be NOT NULL", relname, args[1]); |
||||
|
||||
/*
|
||||
* If DELETE/UPDATE of tuple with stop_date neq INFINITY |
||||
* then say upper Executor to skip operation for this tuple |
||||
*/ |
||||
if (newtuple != NULL) /* UPDATE */ |
||||
{ |
||||
newon = SPI_getbinval (newtuple, tupdesc, attnum[0], &isnull); |
||||
if (isnull) |
||||
elog(WARN, "timetravel (%s): %s must be NOT NULL", relname, args[0]); |
||||
newoff = SPI_getbinval (newtuple, tupdesc, attnum[1], &isnull); |
||||
if (isnull) |
||||
elog(WARN, "timetravel (%s): %s must be NOT NULL", relname, args[1]); |
||||
|
||||
if ( oldon != newon || oldoff != newoff ) |
||||
elog (WARN, "timetravel (%s): you can't change %s and/or %s columns (use set_timetravel)", |
||||
relname, args[0], args[1]); |
||||
|
||||
if ( newoff != NOEND_ABSTIME ) |
||||
{ |
||||
pfree (relname); /* allocated in upper executor context */ |
||||
return (NULL); |
||||
} |
||||
} |
||||
else if (oldoff != NOEND_ABSTIME) /* DELETE */ |
||||
{ |
||||
pfree (relname); |
||||
return (NULL); |
||||
} |
||||
|
||||
newoff = GetCurrentAbsoluteTime (); |
||||
|
||||
/* Connect to SPI manager */ |
||||
if ((ret = SPI_connect()) < 0) |
||||
elog(WARN, "timetravel (%s): SPI_connect returned %d", relname, ret); |
||||
|
||||
/* Fetch tuple values and nulls */ |
||||
cvals = (Datum *) palloc (natts * sizeof (Datum)); |
||||
cnulls = (char *) palloc (natts * sizeof (char)); |
||||
for (i = 0; i < natts; i++) |
||||
{ |
||||
cvals[i] = SPI_getbinval ((newtuple != NULL) ? newtuple : trigtuple,
|
||||
tupdesc, i + 1, &isnull); |
||||
cnulls[i] = (isnull) ? 'n' : ' '; |
||||
} |
||||
|
||||
/* change date column(s) */ |
||||
if (newtuple) /* UPDATE */ |
||||
{ |
||||
cvals[attnum[0] - 1] = newoff; /* start_date eq current date */ |
||||
cnulls[attnum[0] - 1] = ' '; |
||||
cvals[attnum[1] - 1] = NOEND_ABSTIME; /* stop_date eq INFINITY */ |
||||
cnulls[attnum[1] - 1] = ' '; |
||||
} |
||||
else /* DELETE */ |
||||
{ |
||||
cvals[attnum[1] - 1] = newoff; /* stop_date eq current date */ |
||||
cnulls[attnum[1] - 1] = ' '; |
||||
} |
||||
|
||||
/*
|
||||
* Construct ident string as TriggerName $ TriggeredRelationId
|
||||
* and try to find prepared execution plan. |
||||
*/ |
||||
sprintf(ident, "%s$%u", trigger->tgname, rel->rd_id); |
||||
plan = find_plan(ident, &Plans, &nPlans); |
||||
|
||||
/* if there is no plan ... */ |
||||
if (plan->splan == NULL) |
||||
{ |
||||
void *pplan; |
||||
Oid *ctypes; |
||||
char sql[8192]; |
||||
|
||||
/* allocate ctypes for preparation */ |
||||
ctypes = (Oid *) palloc(natts * sizeof(Oid)); |
||||
|
||||
/*
|
||||
* Construct query:
|
||||
* INSERT INTO _relation_ VALUES ($1, ...) |
||||
*/ |
||||
sprintf(sql, "INSERT INTO %s VALUES (", relname); |
||||
for (i = 1; i <= natts; i++) |
||||
{ |
||||
sprintf(sql + strlen(sql), "$%d%s", |
||||
i, (i < natts) ? ", " : ")"); |
||||
ctypes[i - 1] = SPI_gettypeid(tupdesc, i); |
||||
} |
||||
|
||||
/* Prepare plan for query */ |
||||
pplan = SPI_prepare(sql, natts, ctypes); |
||||
if (pplan == NULL) |
||||
elog(WARN, "timetravel (%s): SPI_prepare returned %d", relname, SPI_result); |
||||
|
||||
/*
|
||||
* Remember that SPI_prepare places plan in current memory context |
||||
* - so, we have to save plan in Top memory context for latter |
||||
* use. |
||||
*/ |
||||
pplan = SPI_saveplan(pplan); |
||||
if (pplan == NULL) |
||||
elog(WARN, "timetravel (%s): SPI_saveplan returned %d", relname, SPI_result); |
||||
|
||||
plan->splan = pplan; |
||||
} |
||||
|
||||
/*
|
||||
* Ok, execute prepared plan. |
||||
*/ |
||||
ret = SPI_execp(plan->splan, cvals, cnulls, 0); |
||||
|
||||
if (ret < 0) |
||||
elog(WARN, "timetravel (%s): SPI_execp returned %d", relname, ret); |
||||
|
||||
/* Tuple to return to upper Executor ... */ |
||||
if (newtuple) /* UPDATE */ |
||||
{ |
||||
HeapTuple tmptuple; |
||||
|
||||
tmptuple = SPI_copytuple (trigtuple); |
||||
rettuple = SPI_modifytuple (rel, tmptuple, 1, &(attnum[1]), &newoff, NULL); |
||||
/*
|
||||
* SPI_copytuple allocates tmptuple in upper executor context - |
||||
* have to free allocation using SPI_pfree |
||||
*/ |
||||
SPI_pfree (tmptuple); |
||||
} |
||||
else /* DELETE */ |
||||
rettuple = trigtuple; |
||||
|
||||
SPI_finish(); /* don't forget say Bye to SPI mgr */ |
||||
|
||||
pfree (relname); |
||||
|
||||
return (rettuple); |
||||
} |
||||
|
||||
/*
|
||||
* set_timetravel () -- |
||||
* turn timetravel for specified relation ON/OFF |
||||
*/ |
||||
int32 |
||||
set_timetravel(Name relname, int32 on) |
||||
{ |
||||
char *rname; |
||||
char *d; |
||||
char *s; |
||||
int i; |
||||
|
||||
for (i = 0; i < nTTOff; i++) |
||||
if (namestrcmp (relname, TTOff[i]) == 0) |
||||
break; |
||||
|
||||
if (i < nTTOff) /* OFF currently */ |
||||
{ |
||||
if (on == 0) |
||||
return (0); |
||||
|
||||
/* turn ON */ |
||||
free (TTOff[i]); |
||||
if (nTTOff == 1) |
||||
free (TTOff); |
||||
else |
||||
{ |
||||
if (i < nTTOff - 1) |
||||
memcpy (&(TTOff[i]), &(TTOff[i + 1]), (nTTOff - i) * sizeof (char*)); |
||||
TTOff = realloc (TTOff, (nTTOff - 1) * sizeof (char*)); |
||||
} |
||||
nTTOff--; |
||||
return (0); |
||||
} |
||||
|
||||
/* ON currently */ |
||||
if (on != 0) |
||||
return (1); |
||||
|
||||
/* turn OFF */ |
||||
if (nTTOff == 0) |
||||
TTOff = malloc (sizeof (char*)); |
||||
else |
||||
TTOff = realloc (TTOff, (nTTOff + 1) * sizeof (char*)); |
||||
s = rname = nameout (relname); |
||||
d = TTOff[nTTOff] = malloc (strlen (rname) + 1); |
||||
while (*s) |
||||
*d++ = tolower (*s++); |
||||
*d = 0; |
||||
pfree (rname); |
||||
nTTOff++; |
||||
|
||||
return (1); |
||||
|
||||
} |
||||
|
||||
AbsoluteTime |
||||
currabstime () |
||||
{ |
||||
return (GetCurrentAbsoluteTime ()); |
||||
} |
||||
|
||||
static EPlan * |
||||
find_plan(char *ident, EPlan ** eplan, int *nplans) |
||||
{ |
||||
EPlan *newp; |
||||
int i; |
||||
|
||||
if (*nplans > 0) |
||||
{ |
||||
for (i = 0; i < *nplans; i++) |
||||
{ |
||||
if (strcmp((*eplan)[i].ident, ident) == 0) |
||||
break; |
||||
} |
||||
if (i != *nplans) |
||||
return (*eplan + i); |
||||
*eplan = (EPlan *) realloc(*eplan, (i + 1) * sizeof(EPlan)); |
||||
newp = *eplan + i; |
||||
} |
||||
else |
||||
{ |
||||
newp = *eplan = (EPlan *) malloc(sizeof(EPlan)); |
||||
(*nplans) = i = 0; |
||||
} |
||||
|
||||
newp->ident = (char *) malloc(strlen(ident) + 1); |
||||
strcpy(newp->ident, ident); |
||||
newp->splan = NULL; |
||||
(*nplans)++; |
||||
|
||||
return (newp); |
||||
} |
||||
@ -0,0 +1,63 @@ |
||||
drop table tttest; |
||||
create table tttest ( |
||||
price_id int4, |
||||
price_val int4, |
||||
price_on abstime default currabstime(), |
||||
price_off abstime default 'infinity' |
||||
); |
||||
|
||||
insert into tttest values (1, 1, null, null); |
||||
insert into tttest values (2, 2, null, null); |
||||
insert into tttest values (3, 3, null, null); |
||||
|
||||
create trigger timetravel |
||||
before delete or update on tttest |
||||
for each row |
||||
execute procedure |
||||
timetravel (price_on, price_off); |
||||
|
||||
select * from tttest; |
||||
delete from tttest where price_id = 2; |
||||
select * from tttest; |
||||
-- what do we see ? |
||||
|
||||
-- get current prices |
||||
select * from tttest where price_off = 'infinity'; |
||||
|
||||
-- change price for price_id == 3 |
||||
update tttest set price_val = 30 where price_id = 3; |
||||
select * from tttest; |
||||
|
||||
-- now we want to change price_id from 3 to 5 in ALL tuples |
||||
-- but this gets us not what we need |
||||
update tttest set price_id = 5 where price_id = 3; |
||||
select * from tttest; |
||||
|
||||
-- restore data as before last update: |
||||
select set_timetravel('tttest', 0); -- turn TT OFF! |
||||
delete from tttest where price_id = 5; |
||||
update tttest set price_off = 'infinity' where price_val = 30; |
||||
select * from tttest; |
||||
|
||||
-- and try change price_id now! |
||||
update tttest set price_id = 5 where price_id = 3; |
||||
select * from tttest; |
||||
-- isn't it what we need ? |
||||
|
||||
select set_timetravel('tttest', 1); -- turn TT ON! |
||||
|
||||
-- we want to correct some date |
||||
update tttest set price_on = 'Jan-01-1990 00:00:01' where price_id = 5 and |
||||
price_off <> 'infinity'; |
||||
-- but this doesn't work |
||||
|
||||
-- try in this way |
||||
select set_timetravel('tttest', 0); -- turn TT OFF! |
||||
update tttest set price_on = '01-Jan-1990 00:00:01' where price_id = 5 and |
||||
price_off <> 'infinity'; |
||||
select * from tttest; |
||||
-- isn't it what we need ? |
||||
|
||||
-- get price for price_id == 5 as it was '10-Jan-1990' |
||||
select * from tttest where price_id = 5 and |
||||
price_on <= '10-Jan-1990' and price_off > '10-Jan-1990'; |
||||
@ -0,0 +1,18 @@ |
||||
DROP FUNCTION currabstime(); |
||||
DROP FUNCTION timetravel(); |
||||
DROP FUNCTION set_timetravel(name, int4); |
||||
|
||||
CREATE FUNCTION currabstime() |
||||
RETURNS abstime |
||||
AS '_OBJWD_/timetravel_DLSUFFIX_' |
||||
LANGUAGE 'c'; |
||||
|
||||
CREATE FUNCTION timetravel() |
||||
RETURNS opaque |
||||
AS '_OBJWD_/timetravel_DLSUFFIX_' |
||||
LANGUAGE 'c'; |
||||
|
||||
CREATE FUNCTION set_timetravel(name, int4) |
||||
RETURNS int4 |
||||
AS '_OBJWD_/timetravel_DLSUFFIX_' |
||||
LANGUAGE 'c'; |
||||
Loading…
Reference in new issue