From: Bruce Momjian <bruce@momjian.us>
Date: Sun, 27 Jul 2003 04:51:45 +0000 (+0000)
Subject: Here is 4 file in tgz:
X-Git-Tag: REL7_4_BETA1~109
X-Git-Url: https://granicus.if.org/sourcecode?a=commitdiff_plain;h=524cfad23f31db70a23fc1fe748c050838d5fad0;p=postgresql

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
---

diff --git a/contrib/oid2name/oid2name.c b/contrib/oid2name/oid2name.c
index a5c18d761f..9e8320eba1 100644
--- a/contrib/oid2name/oid2name.c
+++ b/contrib/oid2name/oid2name.c
@@ -355,7 +355,9 @@ sql_exec_dumptable(PGconn *conn, int systables)
 	if (systables == 1)
 		snprintf(todo, 1024, "select relfilenode,relname from pg_class order by relname");
 	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);
 }
diff --git a/contrib/spi/README.timetravel b/contrib/spi/README.timetravel
new file mode 100644
index 0000000000..0b4727fb24
--- /dev/null
+++ b/contrib/spi/README.timetravel
@@ -0,0 +1,116 @@
+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).
diff --git a/contrib/spi/timetravel.c b/contrib/spi/timetravel.c
index 462ce37fed..b7789a2e20 100644
--- a/contrib/spi/timetravel.c
+++ b/contrib/spi/timetravel.c
@@ -3,13 +3,16 @@
  *		using general triggers.
  */
 
+/* Modified by BÖJTHE Zoltán, Hungary, mailto:urdesobt@axelero.hu */
+
 #include "executor/spi.h"		/* this is what you need to work with SPI */
-#include "commands/trigger.h"	/* -"- and triggers */
-#include <ctype.h>
+#include "commands/trigger.h"		/* -"- and triggers */
+#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		set_timetravel(PG_FUNCTION_ARGS);
 
@@ -22,84 +25,102 @@ typedef struct
 static EPlan *Plans = NULL;		/* for UPDATE/DELETE */
 static int	nPlans = 0;
 
-static char **TTOff = NULL;
-static int	nTTOff = 0;
+typedef struct _TTOffList
+{
+    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);
 
 /*
  * 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
+ *			then form (and return) new tuple with start_date eq current date
+ *			and stop_date eq INFINITY [ and update_user eq current user ]
+ *			and all other column values as in new tuple, and insert tuple
+ *			with old data and stop_date eq current date
  *			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
+ *			[ and delete_user eq current user ]
  *			ELSE - skip deletion of tuple.
  *		3.	On INSERT, if start_date is NULL then current date will be
  *			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
  * names:
  * 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);
 
-Datum
+Datum						/* have to return HeapTuple to Executor */
 timetravel(PG_FUNCTION_ARGS)
 {
-	TriggerData *trigdata = (TriggerData *) fcinfo->context;
-	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 */
+	TriggerData	*trigdata = (TriggerData *) fcinfo->context;
+	Trigger 	*trigger;		/* to get trigger name */
+	int		argc;
+	char		**args;			/* arguments */
+	int		attnum[MaxAttrNum];	/* fnumbers of start/stop columns */
+	Datum		oldtimeon,
+			oldtimeoff;
+	Datum		newtimeon,
+			newtimeoff,
+			newuser,
+			nulltext;
+	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 */
+	int		natts;			/* # of attributes */
+	EPlan		*plan;			/* prepared plan */
 	char		ident[2 * NAMEDATALEN];
 	bool		isnull;			/* to know is some column NULL or not */
 	bool		isinsert = false;
-	int			ret;
-	int			i;
+	int		ret;
+	int		i;
 
 	/*
 	 * Some checks first...
 	 */
 
 	/* Called by trigger manager ? */
-	if (!CALLED_AS_TRIGGER(fcinfo))
-		/* internal error */
+	if(!CALLED_AS_TRIGGER(fcinfo))
 		elog(ERROR, "timetravel: not fired by trigger manager");
 
 	/* Should be called for ROW trigger */
-	if (TRIGGER_FIRED_FOR_STATEMENT(trigdata->tg_event))
-		/* internal error */
+	if(TRIGGER_FIRED_FOR_STATEMENT(trigdata->tg_event))
 		elog(ERROR, "timetravel: can't process STATEMENT events");
 
 	/* Should be called BEFORE */
-	if (TRIGGER_FIRED_AFTER(trigdata->tg_event))
-		/* internal error */
+	if(TRIGGER_FIRED_AFTER(trigdata->tg_event))
 		elog(ERROR, "timetravel: must be fired before event");
 
 	/* INSERT ? */
-	if (TRIGGER_FIRED_BY_INSERT(trigdata->tg_event))
+	if(TRIGGER_FIRED_BY_INSERT(trigdata->tg_event))
 		isinsert = true;
 
-	if (TRIGGER_FIRED_BY_UPDATE(trigdata->tg_event))
+	if(TRIGGER_FIRED_BY_UPDATE(trigdata->tg_event))
 		newtuple = trigdata->tg_newtuple;
 
 	trigtuple = trigdata->tg_trigtuple;
@@ -108,170 +129,168 @@ timetravel(PG_FUNCTION_ARGS)
 	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 */
+	if(0==findTTStatus(relname))
 	{
+		/* OFF - nothing to do */
 		pfree(relname);
 		return PointerGetDatum((newtuple != NULL) ? newtuple : trigtuple);
 	}
 
 	trigger = trigdata->tg_trigger;
 
-	if (trigger->tgnargs != 2)
-		/* internal error */
-		elog(ERROR, "timetravel (%s): invalid (!= 2) number of arguments %d",
-			 relname, trigger->tgnargs);
+	argc = trigger->tgnargs;
+	if(argc != MinAttrNum && argc != MaxAttrNum)
+		elog(ERROR, "timetravel (%s): invalid (!= %d or %d) number of arguments %d",
+			 relname, MinAttrNum, MaxAttrNum, trigger->tgnargs);
 
 	args = trigger->tgargs;
 	tupdesc = rel->rd_att;
 	natts = tupdesc->natts;
 
-	for (i = 0; i < 2; i++)
+	for(i = 0 ; i < MinAttrNum ; i++)
 	{
 		attnum[i] = SPI_fnumber(tupdesc, args[i]);
-		if (attnum[i] < 0)
-			ereport(ERROR,
-					(errcode(ERRCODE_TRIGGERED_ACTION_EXCEPTION),
-					 errmsg("\"%s\" has no attribute \"%s\"",
-							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)));
+		if(attnum[i] < 0)
+			elog(ERROR, "timetravel (%s): there is no attribute %s", relname, args[i]);
+		if(SPI_gettypeid(tupdesc, attnum[i]) != ABSTIMEOID)
+			elog(ERROR, "timetravel (%s): attribute %s must be of abstime type",
+				 relname, args[i]);
 	}
-
-	if (isinsert)				/* INSERT */
+	for( ; i < argc ; i++)
 	{
-		int			chnattrs = 0;
-		int			chattrs[2];
-		Datum		newvals[2];
+		attnum[i] = SPI_fnumber(tupdesc, args[i]);
+		if(attnum[i] < 0)
+			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 (isnull)
+	if(isinsert)
+	{ /* 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();
-			chattrs[chnattrs] = attnum[0];
+			newnulls[chnattrs] = ' ';
+			chattrs[chnattrs] = attnum[a_time_on];
 			chnattrs++;
 		}
 
-		oldoff = SPI_getbinval(trigtuple, tupdesc, attnum[1], &isnull);
-		if (isnull)
+		oldtimeoff = SPI_getbinval(trigtuple, tupdesc, attnum[a_time_off], &isnull);
+		if(isnull)
 		{
-			if ((chnattrs == 0 && DatumGetInt32(oldon) >= NOEND_ABSTIME) ||
-			(chnattrs > 0 && DatumGetInt32(newvals[0]) >= NOEND_ABSTIME))
-				ereport(ERROR,
-						(errcode(ERRCODE_TRIGGERED_ACTION_EXCEPTION),
-						 errmsg("timetravel (%s): %s ge %s",
-						 relname, args[0], args[1])));
+			if((chnattrs == 0 && DatumGetInt32(oldtimeon) >= NOEND_ABSTIME) ||
+			   (chnattrs > 0 && DatumGetInt32(newvals[a_time_on]) >= NOEND_ABSTIME))
+				elog(ERROR, "timetravel (%s): %s is infinity", relname, args[a_time_on]);
 			newvals[chnattrs] = NOEND_ABSTIME;
-			chattrs[chnattrs] = attnum[1];
+			newnulls[chnattrs] = ' ';
+			chattrs[chnattrs] = attnum[a_time_off];
 			chnattrs++;
 		}
 		else
 		{
-			if ((chnattrs == 0 && DatumGetInt32(oldon) >=
-				 DatumGetInt32(oldoff)) ||
-				(chnattrs > 0 && DatumGetInt32(newvals[0]) >=
-				 DatumGetInt32(oldoff)))
-				ereport(ERROR,
-						(errcode(ERRCODE_TRIGGERED_ACTION_EXCEPTION),
-						 errmsg("timetravel (%s): %s ge %s",
-						 relname, args[0], args[1])));
+			if((chnattrs == 0 && DatumGetInt32(oldtimeon) > DatumGetInt32(oldtimeoff)) ||
+			   (chnattrs > 0 && DatumGetInt32(newvals[a_time_on]) > DatumGetInt32(oldtimeoff)))
+				elog(ERROR, "timetravel (%s): %s gt %s", relname, args[a_time_on], args[a_time_off]);
 		}
 
 		pfree(relname);
-		if (chnattrs <= 0)
+		if(chnattrs <= 0)
 			return PointerGetDatum(trigtuple);
 
-		rettuple = SPI_modifytuple(rel, trigtuple, chnattrs,
-								   chattrs, newvals, NULL);
+		if(argc == MaxAttrNum)
+		{
+			/* 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);
+		/* end of INSERT */
 	}
 
-	oldon = SPI_getbinval(trigtuple, tupdesc, attnum[0], &isnull);
-	if (isnull)
-		ereport(ERROR,
-				(errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
-				 errmsg("\"%s\" must be NOT NULL in \"%s\"",
-						args[0],relname)));
-	oldoff = SPI_getbinval(trigtuple, tupdesc, attnum[1], &isnull);
-	if (isnull)
-		ereport(ERROR,
-				(errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
-				 errmsg("\"%s\" must be NOT NULL in \"%s\"",
-						args[1],relname)));
+/* UPDATE/DELETE: */
+	oldtimeon = SPI_getbinval(trigtuple, tupdesc, attnum[a_time_on], &isnull);
+	if(isnull)
+		elog(ERROR, "timetravel (%s): %s must be NOT NULL", relname, args[a_time_on]);
+
+	oldtimeoff = SPI_getbinval(trigtuple, tupdesc, attnum[a_time_off], &isnull);
+	if(isnull)
+		elog(ERROR, "timetravel (%s): %s must be NOT NULL", relname, args[a_time_off]);
 
 	/*
 	 * 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)
-			ereport(ERROR,
-					(errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
-					 errmsg("\"%s\" must be NOT NULL in \"%s\"",
-							args[0],relname)));
-		newoff = SPI_getbinval(newtuple, tupdesc, attnum[1], &isnull);
-		if (isnull)
-			ereport(ERROR,
-					(errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
-					 errmsg("\"%s\" must be NOT NULL in \"%s\"",
-							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);
-		}
+	if(newtuple != NULL)
+	{ /* UPDATE */
+		newtimeon = SPI_getbinval(newtuple, tupdesc, attnum[a_time_on], &isnull);
+		if(isnull)
+			elog(ERROR, "timetravel (%s): %s must be NOT NULL", relname, args[a_time_on]);
+
+		newtimeoff = SPI_getbinval(newtuple, tupdesc, attnum[a_time_off], &isnull);
+		if(isnull)
+			elog(ERROR, "timetravel (%s): %s must be NOT NULL", relname, args[a_time_off]);
+
+		if(oldtimeon != newtimeon || oldtimeoff != newtimeoff)
+			elog(ERROR, "timetravel (%s): you can't change %s and/or %s columns (use set_timetravel)",
+				 relname, args[a_time_on], args[a_time_off]);
 	}
-	else if (oldoff != NOEND_ABSTIME)	/* DELETE */
-	{
+	if(oldtimeoff != NOEND_ABSTIME)
+	{ /* current record is a deleted/updated record */
 		pfree(relname);
 		return PointerGetDatum(NULL);
 	}
 
-	newoff = GetCurrentAbsoluteTime();
+	newtimeoff = GetCurrentAbsoluteTime();
 
 	/* Connect to SPI manager */
-	if ((ret = SPI_connect()) < 0)
-		/* internal error */
+	if((ret = SPI_connect()) < 0)
 		elog(ERROR, "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++)
+	for(i = 0; i < natts; i++)
 	{
-		cvals[i] = SPI_getbinval((newtuple != NULL) ? newtuple : trigtuple,
-								 tupdesc, i + 1, &isnull);
+		cvals[i] = SPI_getbinval(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] = ' ';
+	cvals[attnum[a_time_off] - 1] = newtimeoff;	/* stop_date eq current date */
+	cnulls[attnum[a_time_off] - 1] = ' ';
+
+	if(!newtuple)
+	{ /* DELETE */
+		if(argc == MaxAttrNum)
+		{
+			cvals[attnum[a_del_user] - 1] = newuser;		/* set delete user */
+			cnulls[attnum[a_del_user] - 1] = ' ';
+		}
 	}
 
 	/*
@@ -282,11 +301,12 @@ timetravel(PG_FUNCTION_ARGS)
 	plan = find_plan(ident, &Plans, &nPlans);
 
 	/* if there is no plan ... */
-	if (plan->splan == NULL)
+	if(plan->splan == NULL)
 	{
-		void	   *pplan;
-		Oid		   *ctypes;
-		char		sql[8192];
+		void	*pplan;
+		Oid	*ctypes;
+		char	sql[8192];
+		int	j;
 
 		/* allocate ctypes for preparation */
 		ctypes = (Oid *) palloc(natts * sizeof(Oid));
@@ -295,17 +315,21 @@ timetravel(PG_FUNCTION_ARGS)
 		 * Construct query: INSERT INTO _relation_ VALUES ($1, ...)
 		 */
 		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);
+			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 */
 		pplan = SPI_prepare(sql, natts, ctypes);
-		if (pplan == NULL)
-			/* internal error */
+		if(pplan == NULL)
 			elog(ERROR, "timetravel (%s): SPI_prepare returned %d", relname, SPI_result);
 
 		/*
@@ -314,8 +338,7 @@ timetravel(PG_FUNCTION_ARGS)
 		 * use.
 		 */
 		pplan = SPI_saveplan(pplan);
-		if (pplan == NULL)
-			/* internal error */
+		if(pplan == NULL)
 			elog(ERROR, "timetravel (%s): SPI_saveplan returned %d", relname, SPI_result);
 
 		plan->splan = pplan;
@@ -326,23 +349,54 @@ timetravel(PG_FUNCTION_ARGS)
 	 */
 	ret = SPI_execp(plan->splan, cvals, cnulls, 0);
 
-	if (ret < 0)
-		/* internal error */
+	if(ret < 0)
 		elog(ERROR, "timetravel (%s): SPI_execp returned %d", relname, ret);
 
 	/* Tuple to return to upper Executor ... */
-	if (newtuple)				/* UPDATE */
-	{
+	if(newtuple)
+	{ /* UPDATE */
 		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, tmptuple, 1, &(attnum[1]), &newoff, NULL);
+		rettuple = SPI_modifytuple(rel, newtuple, chnattrs, chattrs, newvals, newnulls);
 
 		/*
 		 * SPI_copytuple allocates tmptuple in upper executor context -
 		 * have to free allocation using SPI_pfree
 		 */
-		SPI_pfree(tmptuple);
+//		SPI_pfree(tmptuple);
 	}
 	else
 /* DELETE */
@@ -351,7 +405,6 @@ timetravel(PG_FUNCTION_ARGS)
 	SPI_finish();				/* don't forget say Bye to SPI mgr */
 
 	pfree(relname);
-
 	return PointerGetDatum(rettuple);
 }
 
@@ -364,77 +417,109 @@ PG_FUNCTION_INFO_V1(set_timetravel);
 Datum
 set_timetravel(PG_FUNCTION_ARGS)
 {
-	Name		relname = PG_GETARG_NAME(0);
-	int32		on = PG_GETARG_INT32(1);
-	char	   *rname;
-	char	   *d;
-	char	   *s;
-	int			i;
-
-	for (i = 0; i < nTTOff; i++)
-		if (namestrcmp(relname, TTOff[i]) == 0)
+        Name		relname = PG_GETARG_NAME(0);
+        int32		on = PG_GETARG_INT32(1);
+	char		*rname;
+	char		*d;
+	char		*s;
+	int32		ret;
+	TTOffList	*p,*pp;
+
+	for(pp = (p = &TTOff)->next; pp; pp=(p=pp)->next)
+	{
+		if(namestrcmp(relname, pp->name) == 0)
 			break;
-
-	if (i < nTTOff)				/* OFF currently */
+	}
+	if(pp)
 	{
-		if (on == 0)
-			PG_RETURN_INT32(0);
-
-		/* turn ON */
-		free(TTOff[i]);
-		if (nTTOff == 1)
-			free(TTOff);
-		else
+		/* OFF currently */
+		if(on != 0)
 		{
-			if (i < nTTOff - 1)
-				memcpy(&(TTOff[i]), &(TTOff[i + 1]), (nTTOff - i) * sizeof(char *));
-			TTOff = realloc(TTOff, (nTTOff - 1) * sizeof(char *));
+			/* turn ON */
+			p->next = pp->next;
+			free(pp);
 		}
-		nTTOff--;
-		PG_RETURN_INT32(0);
+		ret = 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)
-		PG_RETURN_INT32(1);
+/*
+ * get_timetravel (relname) --
+ *  get timetravel status for specified relation (ON/OFF)
+ */
+PG_FUNCTION_INFO_V1(get_timetravel);
 
-	/* turn OFF */
-	if (nTTOff == 0)
-		TTOff = malloc(sizeof(char *));
-	else
-		TTOff = realloc(TTOff, (nTTOff + 1) * sizeof(char *));
-	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++;
+Datum
+get_timetravel(PG_FUNCTION_ARGS)
+{
+        Name		relname = PG_GETARG_NAME(0);
+	TTOffList	*pp;
 
+	for(pp = TTOff.next; pp; pp = pp->next)
+	{
+		if(namestrcmp(relname, pp->name) == 0)
+			PG_RETURN_INT32(0);
+	}
 	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
 currabstime()
 {
 	return (GetCurrentAbsoluteTime());
 }
+*/
 
 static EPlan *
 find_plan(char *ident, EPlan ** eplan, int *nplans)
 {
-	EPlan	   *newp;
-	int			i;
+	EPlan	*newp;
+	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;
 		}
-		if (i != *nplans)
+		if(i != *nplans)
 			return (*eplan + i);
 		*eplan = (EPlan *) realloc(*eplan, (i + 1) * sizeof(EPlan));
 		newp = *eplan + i;
diff --git a/contrib/spi/timetravel.example b/contrib/spi/timetravel.example
index 4244bed19f..1769e48154 100644
--- a/contrib/spi/timetravel.example
+++ b/contrib/spi/timetravel.example
@@ -7,6 +7,11 @@ create table tttest (
 	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 
 	before insert or delete or update on tttest
 	for each row 
@@ -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,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));
-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;
 delete from tttest where price_id = 2;
@@ -40,6 +45,9 @@ select * from tttest;
 
 -- restore data as before last update:
 select set_timetravel('tttest', 0);	-- turn TT OFF!
+
+select get_timetravel('tttest');	-- check status
+
 delete from tttest where price_id = 5;
 update tttest set price_off = 'infinity' where price_val = 30;
 select * from tttest;
@@ -51,6 +59,8 @@ select * from tttest;
 
 select set_timetravel('tttest', 1);	-- turn TT ON!
 
+select get_timetravel('tttest');	-- check status
+
 -- 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';
@@ -58,6 +68,9 @@ update tttest set price_on = 'Jan-01-1990 00:00:01' where price_id = 5 and
 
 -- try in this way
 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 
 	price_off <> 'infinity';
 select * from tttest;
diff --git a/contrib/spi/timetravel.sql.in b/contrib/spi/timetravel.sql.in
index f20e9fedf6..675e2d9feb 100644
--- a/contrib/spi/timetravel.sql.in
+++ b/contrib/spi/timetravel.sql.in
@@ -1,6 +1,8 @@
 -- Adjust this setting to control where the objects get created.
 SET search_path = public;
 
+SET autocommit TO 'on';
+
 CREATE OR REPLACE FUNCTION timetravel() 
 RETURNS trigger 
 AS 'MODULE_PATHNAME'
@@ -10,3 +12,8 @@ CREATE OR REPLACE FUNCTION set_timetravel(name, int4)
 RETURNS int4 
 AS 'MODULE_PATHNAME'
 LANGUAGE 'C' WITH (isStrict);
+
+CREATE OR REPLACE FUNCTION get_timetravel(name) 
+RETURNS int4 
+AS 'MODULE_PATHNAME'
+LANGUAGE 'C' WITH (isStrict);