Commit 524cfad2 authored by Bruce Momjian's avatar Bruce Momjian

Here is 4 file in tgz:

the new timetravel.c,
new timetravel.README (cut from spi/README and modified),
modified timetravel.sql.in
and modified timetravel.example.

Features:
- optionally 3 parameter for insert/update/delete user name

- work with CREATE UNIQUE INDEX ixxx on table xxx
(unique_field,time_off);
    (the  original version was work with unique index on 6.5.0-6.5.3,
and not work on 7.3.2,7.3.3)
     (before 6.5.0 and between 6.5.3 and 7.3.2 I dont know)

- get_timetravel(tablename) function for check timetravel-status.

- timetravel trigger not change  oid of the active record. (it is not a
good feature, because the  old version is automatice prevent the paralel
update with "where oid=nnn")

B?jthe Zolt?n
parent 38fb906f
...@@ -355,7 +355,9 @@ sql_exec_dumptable(PGconn *conn, int systables) ...@@ -355,7 +355,9 @@ sql_exec_dumptable(PGconn *conn, int systables)
if (systables == 1) if (systables == 1)
snprintf(todo, 1024, "select relfilenode,relname from pg_class order by relname"); snprintf(todo, 1024, "select relfilenode,relname from pg_class order by relname");
else else
snprintf(todo, 1024, "select relfilenode,relname from pg_class where relname not like 'pg_%%' order by relname"); snprintf(todo, 1024, "select relfilenode,relname from pg_class "
"where reltype not in ('v','c') and "
"relname not like 'pg_%%' order by relname");
sql_exec(conn, todo, 0); sql_exec(conn, todo, 0);
} }
......
2. timetravel.c - functions for implementing time travel feature.
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
I rewritten this, because:
on original version of postgresql 7.3.2-7.3.3:
the UPDATE not work on timetravel.example if I added
>create unique index tttest_idx on tttest (price_id,price_off);
>update tttest set price_val = 30 where price_id = 3;
ERROR: Cannot insert a duplicate key into unique index tttest_idx
And UPDATE not work on table tttest after
>alter table tttest add column q1 text;
>alter table tttest add column q2 int;
>alter table tttest drop column q1;
>update tttest set price_val = 30 where price_id = 3;
ERROR: Parameter '$5' is out of range
And I add a new optional feature: my new timetravel have +3 optional parameters:
inserter_user, updater_user, deleter_user.
And I add a new function: get_timetravel for get timetravel status
without change it.
A big difference:
the old version on UPDATE changed oid on active ('infinity') record,
the new version UPDATE keep oid, and the overdued record have a new oid.
I sign with '!!!' my comment in this file.
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
Old internally supported time-travel (TT) used insert/delete
transaction commit times. To get the same feature using triggers
you are to add to a table two columns of abstime type to store
date when a tuple was inserted (start_date) and changed/deleted
(stop_date):
CREATE TABLE XXX (
... ...
date_on abstime default currabstime(),
date_off abstime default 'infinity'
... ...
/* !!! and (if have) */
ins_user text /* user, who insert this record */
upd_user text /* user, who updated this record */
del_user text /* user, who deleted this record */
... ...
);
!!! on INSERT my new version:
... and optionally set ins_user to current user, upd_user and del_user to null.
- so, tuples being inserted with NULLs in date_on/date_off will get
_current_date_ in date_on (name of start_date column in XXX) and INFINITY in
date_off (name of stop_date column in XXX).
Tuples with stop_date equal INFINITY are "valid now": when trigger will
be fired for UPDATE/DELETE of a tuple with stop_date NOT equal INFINITY then
this tuple will not be changed/deleted!
If stop_date equal INFINITY then on
UPDATE:
original version was:
only stop_date in tuple being updated will be changed to current
date and new tuple with new data (coming from SET ... in UPDATE) will be
inserted. Start_date in this new tuple will be setted to current date and
stop_date - to INFINITY.
On my new version:
insert a new tuple with old values, but stop_date changed to current date;
and update original tuple with new data, and update start_date to current date
and optionally set upd_user to current user and clear ins_user,del_user.
DELETE: new tuple will be inserted with stop_date setted to current date
(and with the same data in other columns as in tuple being deleted).
On my new version:
... and optionally set del_user to current user.
NOTE:
1. To get tuples "valid now" you are to add _stop_date_ = 'infinity'
to WHERE. Internally supported TT allowed to avoid this...
Fixed rewriting RULEs could help here...
As work arround you may use VIEWs...
2. You can't change start/stop date columns with UPDATE!
Use set_timetravel (below) if you need in this.
FUNCTIONs:
timetravel() is general trigger function.
You are to create trigger BEFORE UPDATE OR DELETE using this
function on a time-traveled table. You are to specify two arguments: name of
start_date column and name of stop_date column in triggered table.
Or add +3 arguments:
name of insert_user column, name of update_user column, name of delete_user column
currabstime() may be used in DEFAULT for start_date column to get
current date.
!!! I deleted this function, because I newer used this.
set_timetravel() allows you turn time-travel ON/OFF for a table:
set_timetravel('XXX', 1) will turn TT ON for table XXX (and report
old status).
set_timetravel('XXX', 0) will turn TT OFF for table XXX (-"-).
Turning TT OFF allows you do with a table ALL what you want.
get_timetravel() reports time-travel status ON(1)/OFF(0) for a table.
get_timetravel() and set_timetravel() not checking existing of table and
existing of timetravel trigger on specified table.
There is example in timetravel.example.
To CREATE FUNCTIONs use timetravel.sql (will be made by gmake from
timetravel.source).
...@@ -3,13 +3,16 @@ ...@@ -3,13 +3,16 @@
* using general triggers. * using general triggers.
*/ */
/* Modified by BJTHE Zoltn, Hungary, mailto:urdesobt@axelero.hu */
#include "executor/spi.h" /* this is what you need to work with SPI */ #include "executor/spi.h" /* this is what you need to work with SPI */
#include "commands/trigger.h" /* -"- and triggers */ #include "commands/trigger.h" /* -"- and triggers */
#include <ctype.h> #include "miscadmin.h" /* for GetPgUserName() */
#include <ctype.h> /* tolower () */
#define ABSTIMEOID 702 /* it should be in pg_type.h */ #define ABSTIMEOID 702 /* it should be in pg_type.h */
AbsoluteTime currabstime(void); /* AbsoluteTime currabstime(void); */
Datum timetravel(PG_FUNCTION_ARGS); Datum timetravel(PG_FUNCTION_ARGS);
Datum set_timetravel(PG_FUNCTION_ARGS); Datum set_timetravel(PG_FUNCTION_ARGS);
...@@ -22,84 +25,102 @@ typedef struct ...@@ -22,84 +25,102 @@ typedef struct
static EPlan *Plans = NULL; /* for UPDATE/DELETE */ static EPlan *Plans = NULL; /* for UPDATE/DELETE */
static int nPlans = 0; static int nPlans = 0;
static char **TTOff = NULL; typedef struct _TTOffList
static int nTTOff = 0; {
struct _TTOffList *next;
char name[1];
} TTOffList;
static TTOffList TTOff = {NULL,0};
static int findTTStatus(char *name);
static EPlan *find_plan(char *ident, EPlan ** eplan, int *nplans); static EPlan *find_plan(char *ident, EPlan ** eplan, int *nplans);
/* /*
* timetravel () -- * timetravel () --
* 1. IF an update affects tuple with stop_date eq INFINITY * 1. IF an update affects tuple with stop_date eq INFINITY
* then form (and return) new tuple with stop_date eq current date * then form (and return) new tuple with start_date eq current date
* and all other column values as in old tuple, and insert tuple * and stop_date eq INFINITY [ and update_user eq current user ]
* with new data and start_date eq current date and * and all other column values as in new tuple, and insert tuple
* stop_date eq INFINITY * with old data and stop_date eq current date
* ELSE - skip updation of tuple. * ELSE - skip updation of tuple.
* 2. IF an delete affects tuple with stop_date eq INFINITY * 2. IF an delete affects tuple with stop_date eq INFINITY
* then insert the same tuple with stop_date eq current date * then insert the same tuple with stop_date eq current date
* [ and delete_user eq current user ]
* ELSE - skip deletion of tuple. * ELSE - skip deletion of tuple.
* 3. On INSERT, if start_date is NULL then current date will be * 3. On INSERT, if start_date is NULL then current date will be
* inserted, if stop_date is NULL then INFINITY will be inserted. * inserted, if stop_date is NULL then INFINITY will be inserted.
* [ and insert_user eq current user, update_user and delete_user
* eq NULL ]
* *
* In CREATE TRIGGER you are to specify start_date and stop_date column * In CREATE TRIGGER you are to specify start_date and stop_date column
* names: * names:
* EXECUTE PROCEDURE * EXECUTE PROCEDURE
* timetravel ('date_on', 'date_off'). * timetravel ('date_on', 'date_off' [,'insert_user', 'update_user', 'delete_user' ] ).
*/ */
#define MaxAttrNum 5
#define MinAttrNum 2
#define a_time_on 0
#define a_time_off 1
#define a_ins_user 2
#define a_upd_user 3
#define a_del_user 4
PG_FUNCTION_INFO_V1(timetravel); PG_FUNCTION_INFO_V1(timetravel);
Datum Datum /* have to return HeapTuple to Executor */
timetravel(PG_FUNCTION_ARGS) timetravel(PG_FUNCTION_ARGS)
{ {
TriggerData *trigdata = (TriggerData *) fcinfo->context; TriggerData *trigdata = (TriggerData *) fcinfo->context;
Trigger *trigger; /* to get trigger name */ Trigger *trigger; /* to get trigger name */
char **args; /* arguments */ int argc;
int attnum[2]; /* fnumbers of start/stop columns */ char **args; /* arguments */
Datum oldon, int attnum[MaxAttrNum]; /* fnumbers of start/stop columns */
oldoff; Datum oldtimeon,
Datum newon, oldtimeoff;
newoff; Datum newtimeon,
Datum *cvals; /* column values */ newtimeoff,
char *cnulls; /* column nulls */ newuser,
char *relname; /* triggered relation name */ nulltext;
Datum *cvals; /* column values */
char *cnulls; /* column nulls */
char *relname; /* triggered relation name */
Relation rel; /* triggered relation */ Relation rel; /* triggered relation */
HeapTuple trigtuple; HeapTuple trigtuple;
HeapTuple newtuple = NULL; HeapTuple newtuple = NULL;
HeapTuple rettuple; HeapTuple rettuple;
TupleDesc tupdesc; /* tuple description */ TupleDesc tupdesc; /* tuple description */
int natts; /* # of attributes */ int natts; /* # of attributes */
EPlan *plan; /* prepared plan */ EPlan *plan; /* prepared plan */
char ident[2 * NAMEDATALEN]; char ident[2 * NAMEDATALEN];
bool isnull; /* to know is some column NULL or not */ bool isnull; /* to know is some column NULL or not */
bool isinsert = false; bool isinsert = false;
int ret; int ret;
int i; int i;
/* /*
* Some checks first... * Some checks first...
*/ */
/* Called by trigger manager ? */ /* Called by trigger manager ? */
if (!CALLED_AS_TRIGGER(fcinfo)) if(!CALLED_AS_TRIGGER(fcinfo))
/* internal error */
elog(ERROR, "timetravel: not fired by trigger manager"); elog(ERROR, "timetravel: not fired by trigger manager");
/* Should be called for ROW trigger */ /* Should be called for ROW trigger */
if (TRIGGER_FIRED_FOR_STATEMENT(trigdata->tg_event)) if(TRIGGER_FIRED_FOR_STATEMENT(trigdata->tg_event))
/* internal error */
elog(ERROR, "timetravel: can't process STATEMENT events"); elog(ERROR, "timetravel: can't process STATEMENT events");
/* Should be called BEFORE */ /* Should be called BEFORE */
if (TRIGGER_FIRED_AFTER(trigdata->tg_event)) if(TRIGGER_FIRED_AFTER(trigdata->tg_event))
/* internal error */
elog(ERROR, "timetravel: must be fired before event"); elog(ERROR, "timetravel: must be fired before event");
/* INSERT ? */ /* INSERT ? */
if (TRIGGER_FIRED_BY_INSERT(trigdata->tg_event)) if(TRIGGER_FIRED_BY_INSERT(trigdata->tg_event))
isinsert = true; isinsert = true;
if (TRIGGER_FIRED_BY_UPDATE(trigdata->tg_event)) if(TRIGGER_FIRED_BY_UPDATE(trigdata->tg_event))
newtuple = trigdata->tg_newtuple; newtuple = trigdata->tg_newtuple;
trigtuple = trigdata->tg_trigtuple; trigtuple = trigdata->tg_trigtuple;
...@@ -108,170 +129,168 @@ timetravel(PG_FUNCTION_ARGS) ...@@ -108,170 +129,168 @@ timetravel(PG_FUNCTION_ARGS)
relname = SPI_getrelname(rel); relname = SPI_getrelname(rel);
/* check if TT is OFF for this relation */ /* check if TT is OFF for this relation */
for (i = 0; i < nTTOff; i++) if(0==findTTStatus(relname))
if (strcasecmp(TTOff[i], relname) == 0)
break;
if (i < nTTOff) /* OFF - nothing to do */
{ {
/* OFF - nothing to do */
pfree(relname); pfree(relname);
return PointerGetDatum((newtuple != NULL) ? newtuple : trigtuple); return PointerGetDatum((newtuple != NULL) ? newtuple : trigtuple);
} }
trigger = trigdata->tg_trigger; trigger = trigdata->tg_trigger;
if (trigger->tgnargs != 2) argc = trigger->tgnargs;
/* internal error */ if(argc != MinAttrNum && argc != MaxAttrNum)
elog(ERROR, "timetravel (%s): invalid (!= 2) number of arguments %d", elog(ERROR, "timetravel (%s): invalid (!= %d or %d) number of arguments %d",
relname, trigger->tgnargs); relname, MinAttrNum, MaxAttrNum, trigger->tgnargs);
args = trigger->tgargs; args = trigger->tgargs;
tupdesc = rel->rd_att; tupdesc = rel->rd_att;
natts = tupdesc->natts; natts = tupdesc->natts;
for (i = 0; i < 2; i++) for(i = 0 ; i < MinAttrNum ; i++)
{ {
attnum[i] = SPI_fnumber(tupdesc, args[i]); attnum[i] = SPI_fnumber(tupdesc, args[i]);
if (attnum[i] < 0) if(attnum[i] < 0)
ereport(ERROR, elog(ERROR, "timetravel (%s): there is no attribute %s", relname, args[i]);
(errcode(ERRCODE_TRIGGERED_ACTION_EXCEPTION), if(SPI_gettypeid(tupdesc, attnum[i]) != ABSTIMEOID)
errmsg("\"%s\" has no attribute \"%s\"", elog(ERROR, "timetravel (%s): attribute %s must be of abstime type",
relname, args[i]))); relname, args[i]);
if (SPI_gettypeid(tupdesc, attnum[i]) != ABSTIMEOID)
ereport(ERROR,
(errcode(ERRCODE_TRIGGERED_ACTION_EXCEPTION),
errmsg("attribute \"%s\" of \"%s\" must be type ABSTIME",
args[i], relname)));
} }
for( ; i < argc ; i++)
if (isinsert) /* INSERT */
{ {
int chnattrs = 0; attnum[i] = SPI_fnumber(tupdesc, args[i]);
int chattrs[2]; if(attnum[i] < 0)
Datum newvals[2]; elog(ERROR, "timetravel (%s): there is no attribute %s", relname, args[i]);
if(SPI_gettypeid(tupdesc, attnum[i]) != TEXTOID)
elog(ERROR, "timetravel (%s): attribute %s must be of text type",
relname, args[i]);
}
/* create fields containing name */
newuser = DirectFunctionCall1(textin, CStringGetDatum(GetUserNameFromId(GetUserId())));
nulltext = (Datum)NULL;
oldon = SPI_getbinval(trigtuple, tupdesc, attnum[0], &isnull); if(isinsert)
if (isnull) { /* INSERT */
int chnattrs = 0;
int chattrs[MaxAttrNum];
Datum newvals[MaxAttrNum];
char newnulls[MaxAttrNum];
oldtimeon = SPI_getbinval(trigtuple, tupdesc, attnum[a_time_on], &isnull);
if(isnull)
{ {
newvals[chnattrs] = GetCurrentAbsoluteTime(); newvals[chnattrs] = GetCurrentAbsoluteTime();
chattrs[chnattrs] = attnum[0]; newnulls[chnattrs] = ' ';
chattrs[chnattrs] = attnum[a_time_on];
chnattrs++; chnattrs++;
} }
oldoff = SPI_getbinval(trigtuple, tupdesc, attnum[1], &isnull); oldtimeoff = SPI_getbinval(trigtuple, tupdesc, attnum[a_time_off], &isnull);
if (isnull) if(isnull)
{ {
if ((chnattrs == 0 && DatumGetInt32(oldon) >= NOEND_ABSTIME) || if((chnattrs == 0 && DatumGetInt32(oldtimeon) >= NOEND_ABSTIME) ||
(chnattrs > 0 && DatumGetInt32(newvals[0]) >= NOEND_ABSTIME)) (chnattrs > 0 && DatumGetInt32(newvals[a_time_on]) >= NOEND_ABSTIME))
ereport(ERROR, elog(ERROR, "timetravel (%s): %s is infinity", relname, args[a_time_on]);
(errcode(ERRCODE_TRIGGERED_ACTION_EXCEPTION),
errmsg("timetravel (%s): %s ge %s",
relname, args[0], args[1])));
newvals[chnattrs] = NOEND_ABSTIME; newvals[chnattrs] = NOEND_ABSTIME;
chattrs[chnattrs] = attnum[1]; newnulls[chnattrs] = ' ';
chattrs[chnattrs] = attnum[a_time_off];
chnattrs++; chnattrs++;
} }
else else
{ {
if ((chnattrs == 0 && DatumGetInt32(oldon) >= if((chnattrs == 0 && DatumGetInt32(oldtimeon) > DatumGetInt32(oldtimeoff)) ||
DatumGetInt32(oldoff)) || (chnattrs > 0 && DatumGetInt32(newvals[a_time_on]) > DatumGetInt32(oldtimeoff)))
(chnattrs > 0 && DatumGetInt32(newvals[0]) >= elog(ERROR, "timetravel (%s): %s gt %s", relname, args[a_time_on], args[a_time_off]);
DatumGetInt32(oldoff)))
ereport(ERROR,
(errcode(ERRCODE_TRIGGERED_ACTION_EXCEPTION),
errmsg("timetravel (%s): %s ge %s",
relname, args[0], args[1])));
} }
pfree(relname); pfree(relname);
if (chnattrs <= 0) if(chnattrs <= 0)
return PointerGetDatum(trigtuple); return PointerGetDatum(trigtuple);
rettuple = SPI_modifytuple(rel, trigtuple, chnattrs, if(argc == MaxAttrNum)
chattrs, newvals, NULL); {
/* clear update_user value */
newvals[chnattrs] = nulltext;
newnulls[chnattrs] = 'n';
chattrs[chnattrs] = attnum[a_upd_user];
chnattrs++;
/* clear delete_user value */
newvals[chnattrs] = nulltext;
newnulls[chnattrs] = 'n';
chattrs[chnattrs] = attnum[a_del_user];
chnattrs++;
/* set insert_user value */
newvals[chnattrs] = newuser;
newnulls[chnattrs] = ' ';
chattrs[chnattrs] = attnum[a_ins_user];
chnattrs++;
}
rettuple = SPI_modifytuple(rel, trigtuple, chnattrs, chattrs, newvals, newnulls);
return PointerGetDatum(rettuple); return PointerGetDatum(rettuple);
/* end of INSERT */
} }
oldon = SPI_getbinval(trigtuple, tupdesc, attnum[0], &isnull); /* UPDATE/DELETE: */
if (isnull) oldtimeon = SPI_getbinval(trigtuple, tupdesc, attnum[a_time_on], &isnull);
ereport(ERROR, if(isnull)
(errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED), elog(ERROR, "timetravel (%s): %s must be NOT NULL", relname, args[a_time_on]);
errmsg("\"%s\" must be NOT NULL in \"%s\"",
args[0],relname))); oldtimeoff = SPI_getbinval(trigtuple, tupdesc, attnum[a_time_off], &isnull);
oldoff = SPI_getbinval(trigtuple, tupdesc, attnum[1], &isnull); if(isnull)
if (isnull) elog(ERROR, "timetravel (%s): %s must be NOT NULL", relname, args[a_time_off]);
ereport(ERROR,
(errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
errmsg("\"%s\" must be NOT NULL in \"%s\"",
args[1],relname)));
/* /*
* If DELETE/UPDATE of tuple with stop_date neq INFINITY then say * If DELETE/UPDATE of tuple with stop_date neq INFINITY then say
* upper Executor to skip operation for this tuple * upper Executor to skip operation for this tuple
*/ */
if (newtuple != NULL) /* UPDATE */ if(newtuple != NULL)
{ { /* UPDATE */
newon = SPI_getbinval(newtuple, tupdesc, attnum[0], &isnull); newtimeon = SPI_getbinval(newtuple, tupdesc, attnum[a_time_on], &isnull);
if (isnull) if(isnull)
ereport(ERROR, elog(ERROR, "timetravel (%s): %s must be NOT NULL", relname, args[a_time_on]);
(errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
errmsg("\"%s\" must be NOT NULL in \"%s\"", newtimeoff = SPI_getbinval(newtuple, tupdesc, attnum[a_time_off], &isnull);
args[0],relname))); if(isnull)
newoff = SPI_getbinval(newtuple, tupdesc, attnum[1], &isnull); elog(ERROR, "timetravel (%s): %s must be NOT NULL", relname, args[a_time_off]);
if (isnull)
ereport(ERROR, if(oldtimeon != newtimeon || oldtimeoff != newtimeoff)
(errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED), elog(ERROR, "timetravel (%s): you can't change %s and/or %s columns (use set_timetravel)",
errmsg("\"%s\" must be NOT NULL in \"%s\"", relname, args[a_time_on], args[a_time_off]);
args[1],relname)));
if (oldon != newon || oldoff != newoff)
ereport(ERROR,
(errcode(ERRCODE_TRIGGERED_ACTION_EXCEPTION),
errmsg("cannot change columns \"%s\" or \"%s\" in \"%s\"",
args[0], args[1], relname),
errhint("Use set_timetravel() instead.")));
if (newoff != NOEND_ABSTIME)
{
pfree(relname); /* allocated in upper executor context */
return PointerGetDatum(NULL);
}
} }
else if (oldoff != NOEND_ABSTIME) /* DELETE */ if(oldtimeoff != NOEND_ABSTIME)
{ { /* current record is a deleted/updated record */
pfree(relname); pfree(relname);
return PointerGetDatum(NULL); return PointerGetDatum(NULL);
} }
newoff = GetCurrentAbsoluteTime(); newtimeoff = GetCurrentAbsoluteTime();
/* Connect to SPI manager */ /* Connect to SPI manager */
if ((ret = SPI_connect()) < 0) if((ret = SPI_connect()) < 0)
/* internal error */
elog(ERROR, "timetravel (%s): SPI_connect returned %d", relname, ret); elog(ERROR, "timetravel (%s): SPI_connect returned %d", relname, ret);
/* Fetch tuple values and nulls */ /* Fetch tuple values and nulls */
cvals = (Datum *) palloc(natts * sizeof(Datum)); cvals = (Datum *) palloc(natts * sizeof(Datum));
cnulls = (char *) palloc(natts * sizeof(char)); cnulls = (char *) palloc(natts * sizeof(char));
for (i = 0; i < natts; i++) for(i = 0; i < natts; i++)
{ {
cvals[i] = SPI_getbinval((newtuple != NULL) ? newtuple : trigtuple, cvals[i] = SPI_getbinval(trigtuple, tupdesc, i + 1, &isnull);
tupdesc, i + 1, &isnull);
cnulls[i] = (isnull) ? 'n' : ' '; cnulls[i] = (isnull) ? 'n' : ' ';
} }
/* change date column(s) */ /* change date column(s) */
if (newtuple) /* UPDATE */ cvals[attnum[a_time_off] - 1] = newtimeoff; /* stop_date eq current date */
{ cnulls[attnum[a_time_off] - 1] = ' ';
cvals[attnum[0] - 1] = newoff; /* start_date eq current date */
cnulls[attnum[0] - 1] = ' '; if(!newtuple)
cvals[attnum[1] - 1] = NOEND_ABSTIME; /* stop_date eq INFINITY */ { /* DELETE */
cnulls[attnum[1] - 1] = ' '; if(argc == MaxAttrNum)
} {
else cvals[attnum[a_del_user] - 1] = newuser; /* set delete user */
/* DELETE */ cnulls[attnum[a_del_user] - 1] = ' ';
{ }
cvals[attnum[1] - 1] = newoff; /* stop_date eq current date */
cnulls[attnum[1] - 1] = ' ';
} }
/* /*
...@@ -282,11 +301,12 @@ timetravel(PG_FUNCTION_ARGS) ...@@ -282,11 +301,12 @@ timetravel(PG_FUNCTION_ARGS)
plan = find_plan(ident, &Plans, &nPlans); plan = find_plan(ident, &Plans, &nPlans);
/* if there is no plan ... */ /* if there is no plan ... */
if (plan->splan == NULL) if(plan->splan == NULL)
{ {
void *pplan; void *pplan;
Oid *ctypes; Oid *ctypes;
char sql[8192]; char sql[8192];
int j;
/* allocate ctypes for preparation */ /* allocate ctypes for preparation */
ctypes = (Oid *) palloc(natts * sizeof(Oid)); ctypes = (Oid *) palloc(natts * sizeof(Oid));
...@@ -295,17 +315,21 @@ timetravel(PG_FUNCTION_ARGS) ...@@ -295,17 +315,21 @@ timetravel(PG_FUNCTION_ARGS)
* Construct query: INSERT INTO _relation_ VALUES ($1, ...) * Construct query: INSERT INTO _relation_ VALUES ($1, ...)
*/ */
snprintf(sql, sizeof(sql), "INSERT INTO %s VALUES (", relname); snprintf(sql, sizeof(sql), "INSERT INTO %s VALUES (", relname);
for (i = 1; i <= natts; i++) for(i = 1; i <= natts; i++)
{ {
snprintf(sql + strlen(sql), sizeof(sql) - strlen(sql), "$%d%s",
i, (i < natts) ? ", " : ")");
ctypes[i - 1] = SPI_gettypeid(tupdesc, i); ctypes[i - 1] = SPI_gettypeid(tupdesc, i);
if(!(tupdesc->attrs[i - 1]->attisdropped)) /* skip dropped columns */
snprintf(sql + strlen(sql), sizeof(sql) - strlen(sql), "$%d%s",
i, (i < natts) ? ", " : ")" );
// snprintf(sql + strlen(sql), sizeof(sql) - strlen(sql), "$%d /* %d */ %s",
// i, ctypes[i-1], (i < natts) ? ", " : ")" );
} }
// elog(NOTICE, "timetravel (%s) update: sql: %s", relname, sql);
/* Prepare plan for query */ /* Prepare plan for query */
pplan = SPI_prepare(sql, natts, ctypes); pplan = SPI_prepare(sql, natts, ctypes);
if (pplan == NULL) if(pplan == NULL)
/* internal error */
elog(ERROR, "timetravel (%s): SPI_prepare returned %d", relname, SPI_result); elog(ERROR, "timetravel (%s): SPI_prepare returned %d", relname, SPI_result);
/* /*
...@@ -314,8 +338,7 @@ timetravel(PG_FUNCTION_ARGS) ...@@ -314,8 +338,7 @@ timetravel(PG_FUNCTION_ARGS)
* use. * use.
*/ */
pplan = SPI_saveplan(pplan); pplan = SPI_saveplan(pplan);
if (pplan == NULL) if(pplan == NULL)
/* internal error */
elog(ERROR, "timetravel (%s): SPI_saveplan returned %d", relname, SPI_result); elog(ERROR, "timetravel (%s): SPI_saveplan returned %d", relname, SPI_result);
plan->splan = pplan; plan->splan = pplan;
...@@ -326,23 +349,54 @@ timetravel(PG_FUNCTION_ARGS) ...@@ -326,23 +349,54 @@ timetravel(PG_FUNCTION_ARGS)
*/ */
ret = SPI_execp(plan->splan, cvals, cnulls, 0); ret = SPI_execp(plan->splan, cvals, cnulls, 0);
if (ret < 0) if(ret < 0)
/* internal error */
elog(ERROR, "timetravel (%s): SPI_execp returned %d", relname, ret); elog(ERROR, "timetravel (%s): SPI_execp returned %d", relname, ret);
/* Tuple to return to upper Executor ... */ /* Tuple to return to upper Executor ... */
if (newtuple) /* UPDATE */ if(newtuple)
{ { /* UPDATE */
HeapTuple tmptuple; HeapTuple tmptuple;
int chnattrs = 0;
int chattrs[MaxAttrNum];
Datum newvals[MaxAttrNum];
char newnulls[MaxAttrNum];
newvals[chnattrs] = newtimeoff;
newnulls[chnattrs] = ' ';
chattrs[chnattrs] = attnum[a_time_on];
chnattrs++;
newvals[chnattrs] = NOEND_ABSTIME;
newnulls[chnattrs] = ' ';
chattrs[chnattrs] = attnum[a_time_off];
chnattrs++;
if(argc == MaxAttrNum)
{
/* set update_user value */
newvals[chnattrs] = newuser;
newnulls[chnattrs] = ' ';
chattrs[chnattrs] = attnum[a_upd_user];
chnattrs++;
/* clear delete_user value */
newvals[chnattrs] = nulltext;
newnulls[chnattrs] = 'n';
chattrs[chnattrs] = attnum[a_del_user];
chnattrs++;
/* set insert_user value */
newvals[chnattrs] = nulltext;
newnulls[chnattrs] = 'n';
chattrs[chnattrs] = attnum[a_ins_user];
chnattrs++;
}
tmptuple = SPI_copytuple(trigtuple); rettuple = SPI_modifytuple(rel, newtuple, chnattrs, chattrs, newvals, newnulls);
rettuple = SPI_modifytuple(rel, tmptuple, 1, &(attnum[1]), &newoff, NULL);
/* /*
* SPI_copytuple allocates tmptuple in upper executor context - * SPI_copytuple allocates tmptuple in upper executor context -
* have to free allocation using SPI_pfree * have to free allocation using SPI_pfree
*/ */
SPI_pfree(tmptuple); // SPI_pfree(tmptuple);
} }
else else
/* DELETE */ /* DELETE */
...@@ -351,7 +405,6 @@ timetravel(PG_FUNCTION_ARGS) ...@@ -351,7 +405,6 @@ timetravel(PG_FUNCTION_ARGS)
SPI_finish(); /* don't forget say Bye to SPI mgr */ SPI_finish(); /* don't forget say Bye to SPI mgr */
pfree(relname); pfree(relname);
return PointerGetDatum(rettuple); return PointerGetDatum(rettuple);
} }
...@@ -364,77 +417,109 @@ PG_FUNCTION_INFO_V1(set_timetravel); ...@@ -364,77 +417,109 @@ PG_FUNCTION_INFO_V1(set_timetravel);
Datum Datum
set_timetravel(PG_FUNCTION_ARGS) set_timetravel(PG_FUNCTION_ARGS)
{ {
Name relname = PG_GETARG_NAME(0); Name relname = PG_GETARG_NAME(0);
int32 on = PG_GETARG_INT32(1); int32 on = PG_GETARG_INT32(1);
char *rname; char *rname;
char *d; char *d;
char *s; char *s;
int i; int32 ret;
TTOffList *p,*pp;
for (i = 0; i < nTTOff; i++)
if (namestrcmp(relname, TTOff[i]) == 0) for(pp = (p = &TTOff)->next; pp; pp=(p=pp)->next)
{
if(namestrcmp(relname, pp->name) == 0)
break; break;
}
if (i < nTTOff) /* OFF currently */ if(pp)
{ {
if (on == 0) /* OFF currently */
PG_RETURN_INT32(0); if(on != 0)
/* turn ON */
free(TTOff[i]);
if (nTTOff == 1)
free(TTOff);
else
{ {
if (i < nTTOff - 1) /* turn ON */
memcpy(&(TTOff[i]), &(TTOff[i + 1]), (nTTOff - i) * sizeof(char *)); p->next = pp->next;
TTOff = realloc(TTOff, (nTTOff - 1) * sizeof(char *)); free(pp);
} }
nTTOff--; ret = 0;
PG_RETURN_INT32(0);
} }
else
{
/* ON currently */
if(on == 0)
{
/* turn OFF */
s = rname = DatumGetCString(DirectFunctionCall1(nameout, NameGetDatum(relname)));
if(s)
{
pp = malloc(sizeof(TTOffList)+strlen(rname));
if(pp)
{
pp->next = NULL;
p->next = pp;
d = pp->name;
while (*s)
*d++ = tolower((unsigned char)*s++);
*d = '\0';
}
pfree(rname);
}
}
ret = 1;
}
PG_RETURN_INT32(ret);
}
/* ON currently */ /*
if (on != 0) * get_timetravel (relname) --
PG_RETURN_INT32(1); * get timetravel status for specified relation (ON/OFF)
*/
PG_FUNCTION_INFO_V1(get_timetravel);
/* turn OFF */ Datum
if (nTTOff == 0) get_timetravel(PG_FUNCTION_ARGS)
TTOff = malloc(sizeof(char *)); {
else Name relname = PG_GETARG_NAME(0);
TTOff = realloc(TTOff, (nTTOff + 1) * sizeof(char *)); TTOffList *pp;
s = rname = DatumGetCString(DirectFunctionCall1(nameout,
NameGetDatum(relname)));
d = TTOff[nTTOff] = malloc(strlen(rname) + 1);
while (*s)
*d++ = tolower((unsigned char) *s++);
*d = 0;
pfree(rname);
nTTOff++;
for(pp = TTOff.next; pp; pp = pp->next)
{
if(namestrcmp(relname, pp->name) == 0)
PG_RETURN_INT32(0);
}
PG_RETURN_INT32(1); PG_RETURN_INT32(1);
} }
static int
findTTStatus(char *name)
{
TTOffList* pp;
for(pp = TTOff.next; pp; pp = pp->next)
if(strcasecmp(name, pp->name) == 0)
return 0;
return 1;
}
/*
AbsoluteTime AbsoluteTime
currabstime() currabstime()
{ {
return (GetCurrentAbsoluteTime()); return (GetCurrentAbsoluteTime());
} }
*/
static EPlan * static EPlan *
find_plan(char *ident, EPlan ** eplan, int *nplans) find_plan(char *ident, EPlan ** eplan, int *nplans)
{ {
EPlan *newp; EPlan *newp;
int i; int i;
if (*nplans > 0) if(*nplans > 0)
{ {
for (i = 0; i < *nplans; i++) for(i = 0; i < *nplans; i++)
{ {
if (strcmp((*eplan)[i].ident, ident) == 0) if(strcmp((*eplan)[i].ident, ident) == 0)
break; break;
} }
if (i != *nplans) if(i != *nplans)
return (*eplan + i); return (*eplan + i);
*eplan = (EPlan *) realloc(*eplan, (i + 1) * sizeof(EPlan)); *eplan = (EPlan *) realloc(*eplan, (i + 1) * sizeof(EPlan));
newp = *eplan + i; newp = *eplan + i;
......
...@@ -7,6 +7,11 @@ create table tttest ( ...@@ -7,6 +7,11 @@ create table tttest (
price_off abstime price_off abstime
); );
create unique index tttest_idx on tttest (price_id,price_off);
alter table tttest add column q1 text;
alter table tttest add column q2 int;
alter table tttest drop column q1;
create trigger timetravel create trigger timetravel
before insert or delete or update on tttest before insert or delete or update on tttest
for each row for each row
...@@ -17,9 +22,9 @@ insert into tttest values (1, 1, null, null); ...@@ -17,9 +22,9 @@ insert into tttest values (1, 1, null, null);
insert into tttest(price_id, price_val) values (2, 2); insert into tttest(price_id, price_val) values (2, 2);
insert into tttest(price_id, price_val,price_off) values (3, 3, 'infinity'); insert into tttest(price_id, price_val,price_off) values (3, 3, 'infinity');
insert into tttest(price_id, price_val,price_off) values (3, 3, insert into tttest(price_id, price_val,price_off) values (4, 4,
abstime('now'::timestamp - '100 days'::interval)); abstime('now'::timestamp - '100 days'::interval));
insert into tttest(price_id, price_val,price_on) values (3, 3, 'infinity'); insert into tttest(price_id, price_val,price_on) values (3, 3, 'infinity'); -- duplicate key
select * from tttest; select * from tttest;
delete from tttest where price_id = 2; delete from tttest where price_id = 2;
...@@ -40,6 +45,9 @@ select * from tttest; ...@@ -40,6 +45,9 @@ select * from tttest;
-- restore data as before last update: -- restore data as before last update:
select set_timetravel('tttest', 0); -- turn TT OFF! select set_timetravel('tttest', 0); -- turn TT OFF!
select get_timetravel('tttest'); -- check status
delete from tttest where price_id = 5; delete from tttest where price_id = 5;
update tttest set price_off = 'infinity' where price_val = 30; update tttest set price_off = 'infinity' where price_val = 30;
select * from tttest; select * from tttest;
...@@ -51,6 +59,8 @@ select * from tttest; ...@@ -51,6 +59,8 @@ select * from tttest;
select set_timetravel('tttest', 1); -- turn TT ON! select set_timetravel('tttest', 1); -- turn TT ON!
select get_timetravel('tttest'); -- check status
-- we want to correct some date -- we want to correct some date
update tttest set price_on = 'Jan-01-1990 00:00:01' where price_id = 5 and update tttest set price_on = 'Jan-01-1990 00:00:01' where price_id = 5 and
price_off <> 'infinity'; price_off <> 'infinity';
...@@ -58,6 +68,9 @@ update tttest set price_on = 'Jan-01-1990 00:00:01' where price_id = 5 and ...@@ -58,6 +68,9 @@ update tttest set price_on = 'Jan-01-1990 00:00:01' where price_id = 5 and
-- try in this way -- try in this way
select set_timetravel('tttest', 0); -- turn TT OFF! select set_timetravel('tttest', 0); -- turn TT OFF!
select get_timetravel('tttest'); -- check status
update tttest set price_on = '01-Jan-1990 00:00:01' where price_id = 5 and update tttest set price_on = '01-Jan-1990 00:00:01' where price_id = 5 and
price_off <> 'infinity'; price_off <> 'infinity';
select * from tttest; select * from tttest;
......
-- Adjust this setting to control where the objects get created. -- Adjust this setting to control where the objects get created.
SET search_path = public; SET search_path = public;
SET autocommit TO 'on';
CREATE OR REPLACE FUNCTION timetravel() CREATE OR REPLACE FUNCTION timetravel()
RETURNS trigger RETURNS trigger
AS 'MODULE_PATHNAME' AS 'MODULE_PATHNAME'
...@@ -10,3 +12,8 @@ CREATE OR REPLACE FUNCTION set_timetravel(name, int4) ...@@ -10,3 +12,8 @@ CREATE OR REPLACE FUNCTION set_timetravel(name, int4)
RETURNS int4 RETURNS int4
AS 'MODULE_PATHNAME' AS 'MODULE_PATHNAME'
LANGUAGE 'C' WITH (isStrict); LANGUAGE 'C' WITH (isStrict);
CREATE OR REPLACE FUNCTION get_timetravel(name)
RETURNS int4
AS 'MODULE_PATHNAME'
LANGUAGE 'C' WITH (isStrict);
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment