General trigger functions for time-travel
authorVadim B. Mikheev
Wed, 24 Sep 1997 08:17:14 +0000 (08:17 +0000)
committerVadim B. Mikheev
Wed, 24 Sep 1997 08:17:14 +0000 (08:17 +0000)
contrib/spi/Makefile
contrib/spi/README
contrib/spi/timetravel.c [new file with mode: 0644]
contrib/spi/timetravel.example [new file with mode: 0644]
contrib/spi/timetravel.source [new file with mode: 0644]

index 8826ed23c09714c99071352967753ebe3049446b..3f5790afdf166b5e88cd821a7fd71983aefa61c7 100644 (file)
@@ -9,12 +9,11 @@ ifdef REFINT_VERBOSE
 CFLAGS+= -DREFINT_VERBOSE
 endif
 
-TARGETS= refint$(DLSUFFIX) refint.sql
+TARGETS= refint$(DLSUFFIX) refint.sql timetravel$(DLSUFFIX) timetravel.sql
 
 CLEANFILES+= $(TARGETS)
 
 all:: $(TARGETS)
-   rm -f *.obj *.pic
 
 %.sql: %.source
    rm -f $@; \
@@ -23,4 +22,4 @@ all:: $(TARGETS)
        -e "s:_DLSUFFIX_:$(DLSUFFIX):g" < $< > $@
 
 clean: 
-   rm -f $(TARGETS) *.[op]*
+   rm -f $(TARGETS)
index 932181f309b64d191a34e57418314632298936c5..65868f0fc7af1e8dfdc0ac34bb01a0ef497eb92e 100644 (file)
@@ -28,10 +28,77 @@ being deleted to null), triggered table column names which correspond
 to primary/unique key, referencing table name and column names corresponding
 to foreign key (, ... - as many referencing tables/keys as specified
 by first argument).
-   Note, that NOT NULL constraint and unique index have to be defined
-by youself.
+   Note, that NOT NULL constraint and unique index have to be defined by
+youself.
 
-   There are examples in refint.example and regression tests 
+   There are examples in refint.example and regression tests
 (sql/triggers.sql).
 
+   To CREATE FUNCTIONs use refint.sql (will be made by gmake from
+refint.source).
 
+
+2. timetravel.c - functions for implementing time travel feature.
+
+   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'
+   ...     ...
+);
+
+- 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: 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.
+
+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).
+
+   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.
+
+currabstime() may be used in DEFAULT for start_date column to get
+current date.
+
+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.
+
+   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
new file mode 100644 (file)
index 0000000..dcabb35
--- /dev/null
@@ -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              /* 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);
+}
diff --git a/contrib/spi/timetravel.example b/contrib/spi/timetravel.example
new file mode 100644 (file)
index 0000000..00cb301
--- /dev/null
@@ -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';
diff --git a/contrib/spi/timetravel.source b/contrib/spi/timetravel.source
new file mode 100644 (file)
index 0000000..8a1e1e1
--- /dev/null
@@ -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';