diff --git a/contrib/spi/Makefile b/contrib/spi/Makefile index 42aa3740c4..6bc2318e0a 100644 --- a/contrib/spi/Makefile +++ b/contrib/spi/Makefile @@ -1,14 +1,13 @@ # contrib/spi/Makefile -MODULES = autoinc insert_username moddatetime refint timetravel +MODULES = autoinc insert_username moddatetime refint -EXTENSION = autoinc insert_username moddatetime refint timetravel +EXTENSION = autoinc insert_username moddatetime refint DATA = autoinc--1.0.sql autoinc--unpackaged--1.0.sql \ insert_username--1.0.sql insert_username--unpackaged--1.0.sql \ moddatetime--1.0.sql moddatetime--unpackaged--1.0.sql \ - refint--1.0.sql refint--unpackaged--1.0.sql \ - timetravel--1.0.sql timetravel--unpackaged--1.0.sql + refint--1.0.sql refint--unpackaged--1.0.sql PGFILEDESC = "spi - examples of using SPI and triggers" DOCS = $(addsuffix .example, $(MODULES)) diff --git a/contrib/spi/timetravel--1.0.sql b/contrib/spi/timetravel--1.0.sql deleted file mode 100644 index c34ca09965..0000000000 --- a/contrib/spi/timetravel--1.0.sql +++ /dev/null @@ -1,19 +0,0 @@ -/* contrib/spi/timetravel--1.0.sql */ - --- complain if script is sourced in psql, rather than via CREATE EXTENSION -\echo Use "CREATE EXTENSION timetravel" to load this file. \quit - -CREATE FUNCTION timetravel() -RETURNS trigger -AS 'MODULE_PATHNAME' -LANGUAGE C; - -CREATE FUNCTION set_timetravel(name, int4) -RETURNS int4 -AS 'MODULE_PATHNAME' -LANGUAGE C RETURNS NULL ON NULL INPUT; - -CREATE FUNCTION get_timetravel(name) -RETURNS int4 -AS 'MODULE_PATHNAME' -LANGUAGE C RETURNS NULL ON NULL INPUT; diff --git a/contrib/spi/timetravel--unpackaged--1.0.sql b/contrib/spi/timetravel--unpackaged--1.0.sql deleted file mode 100644 index 121bceba9b..0000000000 --- a/contrib/spi/timetravel--unpackaged--1.0.sql +++ /dev/null @@ -1,8 +0,0 @@ -/* contrib/spi/timetravel--unpackaged--1.0.sql */ - --- complain if script is sourced in psql, rather than via CREATE EXTENSION -\echo Use "CREATE EXTENSION timetravel FROM unpackaged" to load this file. \quit - -ALTER EXTENSION timetravel ADD function timetravel(); -ALTER EXTENSION timetravel ADD function set_timetravel(name,integer); -ALTER EXTENSION timetravel ADD function get_timetravel(name); diff --git a/contrib/spi/timetravel.c b/contrib/spi/timetravel.c deleted file mode 100644 index 00f661e6b6..0000000000 --- a/contrib/spi/timetravel.c +++ /dev/null @@ -1,553 +0,0 @@ -/* - * contrib/spi/timetravel.c - * - * - * timetravel.c -- function to get time travel feature - * using general triggers. - * - * Modified by BÖJTHE Zoltán, Hungary, mailto:urdesobt@axelero.hu - */ -#include "postgres.h" - -#include - -#include "access/htup_details.h" -#include "catalog/pg_type.h" -#include "commands/trigger.h" -#include "executor/spi.h" -#include "miscadmin.h" -#include "utils/builtins.h" -#include "utils/nabstime.h" -#include "utils/rel.h" - -PG_MODULE_MAGIC; - -/* AbsoluteTime currabstime(void); */ - -typedef struct -{ - char *ident; - SPIPlanPtr splan; -} EPlan; - -static EPlan *Plans = NULL; /* for UPDATE/DELETE */ -static int nPlans = 0; - -typedef struct _TTOffList -{ - struct _TTOffList *next; - char name[FLEXIBLE_ARRAY_MEMBER]; -} TTOffList; - -static TTOffList *TTOff = NULL; - -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 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 updating of tuple. - * 2. IF a 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' [,'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 /* have to return HeapTuple to Executor */ -timetravel(PG_FUNCTION_ARGS) -{ - 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 */ - char ident[2 * NAMEDATALEN]; - bool isnull; /* to know is some column NULL or not */ - bool isinsert = false; - int ret; - int i; - - /* - * Some checks first... - */ - - /* Called by trigger manager ? */ - if (!CALLED_AS_TRIGGER(fcinfo)) - elog(ERROR, "timetravel: not fired by trigger manager"); - - /* Should be called for ROW trigger */ - if (!TRIGGER_FIRED_FOR_ROW(trigdata->tg_event)) - elog(ERROR, "timetravel: must be fired for row"); - - /* Should be called BEFORE */ - if (!TRIGGER_FIRED_BEFORE(trigdata->tg_event)) - elog(ERROR, "timetravel: must be fired before event"); - - /* INSERT ? */ - if (TRIGGER_FIRED_BY_INSERT(trigdata->tg_event)) - isinsert = true; - - if (TRIGGER_FIRED_BY_UPDATE(trigdata->tg_event)) - newtuple = trigdata->tg_newtuple; - - trigtuple = trigdata->tg_trigtuple; - - rel = trigdata->tg_relation; - relname = SPI_getrelname(rel); - - /* check if TT is OFF for this relation */ - if (0 == findTTStatus(relname)) - { - /* OFF - nothing to do */ - pfree(relname); - return PointerGetDatum((newtuple != NULL) ? newtuple : trigtuple); - } - - trigger = trigdata->tg_trigger; - - 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 < MinAttrNum; i++) - { - 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]) != ABSTIMEOID) - elog(ERROR, "timetravel (%s): attribute %s must be of abstime type", - relname, args[i]); - } - for (; i < argc; i++) - { - 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 = CStringGetTextDatum(GetUserNameFromId(GetUserId(), false)); - - nulltext = (Datum) NULL; - - if (isinsert) - { /* INSERT */ - int chnattrs = 0; - int chattrs[MaxAttrNum]; - Datum newvals[MaxAttrNum]; - bool newnulls[MaxAttrNum]; - - oldtimeon = SPI_getbinval(trigtuple, tupdesc, attnum[a_time_on], &isnull); - if (isnull) - { - newvals[chnattrs] = GetCurrentAbsoluteTime(); - newnulls[chnattrs] = false; - chattrs[chnattrs] = attnum[a_time_on]; - chnattrs++; - } - - oldtimeoff = SPI_getbinval(trigtuple, tupdesc, attnum[a_time_off], &isnull); - if (isnull) - { - 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; - newnulls[chnattrs] = false; - chattrs[chnattrs] = attnum[a_time_off]; - chnattrs++; - } - else - { - 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) - return PointerGetDatum(trigtuple); - - if (argc == MaxAttrNum) - { - /* clear update_user value */ - newvals[chnattrs] = nulltext; - newnulls[chnattrs] = true; - chattrs[chnattrs] = attnum[a_upd_user]; - chnattrs++; - /* clear delete_user value */ - newvals[chnattrs] = nulltext; - newnulls[chnattrs] = true; - chattrs[chnattrs] = attnum[a_del_user]; - chnattrs++; - /* set insert_user value */ - newvals[chnattrs] = newuser; - newnulls[chnattrs] = false; - chattrs[chnattrs] = attnum[a_ins_user]; - chnattrs++; - } - rettuple = heap_modify_tuple_by_cols(trigtuple, tupdesc, - chnattrs, chattrs, - newvals, newnulls); - return PointerGetDatum(rettuple); - /* end of INSERT */ - } - - /* 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 */ - 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 cannot change %s and/or %s columns (use set_timetravel)", - relname, args[a_time_on], args[a_time_off]); - } - if (oldtimeoff != NOEND_ABSTIME) - { /* current record is a deleted/updated record */ - pfree(relname); - return PointerGetDatum(NULL); - } - - newtimeoff = GetCurrentAbsoluteTime(); - - /* Connect to SPI manager */ - 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++) - { - cvals[i] = SPI_getbinval(trigtuple, tupdesc, i + 1, &isnull); - cnulls[i] = (isnull) ? 'n' : ' '; - } - - /* change date column(s) */ - 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] = ' '; - } - } - - /* - * Construct ident string as TriggerName $ TriggeredRelationId and try to - * find prepared execution plan. - */ - snprintf(ident, sizeof(ident), "%s$%u", trigger->tgname, rel->rd_id); - plan = find_plan(ident, &Plans, &nPlans); - - /* if there is no plan ... */ - if (plan->splan == NULL) - { - SPIPlanPtr pplan; - Oid *ctypes; - char sql[8192]; - char separ = ' '; - - /* allocate ctypes for preparation */ - ctypes = (Oid *) palloc(natts * sizeof(Oid)); - - /* - * Construct query: INSERT INTO _relation_ VALUES ($1, ...) - */ - snprintf(sql, sizeof(sql), "INSERT INTO %s VALUES (", relname); - for (i = 1; i <= natts; i++) - { - ctypes[i - 1] = SPI_gettypeid(tupdesc, i); - if (!(TupleDescAttr(tupdesc, i - 1)->attisdropped)) /* skip dropped columns */ - { - snprintf(sql + strlen(sql), sizeof(sql) - strlen(sql), "%c$%d", separ, i); - separ = ','; - } - } - snprintf(sql + strlen(sql), sizeof(sql) - strlen(sql), ")"); - - elog(DEBUG4, "timetravel (%s) update: sql: %s", relname, sql); - - /* Prepare plan for query */ - pplan = SPI_prepare(sql, natts, ctypes); - if (pplan == NULL) - elog(ERROR, "timetravel (%s): SPI_prepare returned %s", relname, SPI_result_code_string(SPI_result)); - - /* - * Remember that SPI_prepare places plan in current memory context - - * so, we have to save plan in Top memory context for later use. - */ - if (SPI_keepplan(pplan)) - elog(ERROR, "timetravel (%s): SPI_keepplan failed", relname); - - plan->splan = pplan; - } - - /* - * Ok, execute prepared plan. - */ - ret = SPI_execp(plan->splan, cvals, cnulls, 0); - - if (ret < 0) - elog(ERROR, "timetravel (%s): SPI_execp returned %d", relname, ret); - - /* Tuple to return to upper Executor ... */ - if (newtuple) - { /* UPDATE */ - 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++; - } - - /* - * Use SPI_modifytuple() here because we are inside SPI environment - * but rettuple must be allocated in caller's context. - */ - rettuple = SPI_modifytuple(rel, newtuple, chnattrs, chattrs, newvals, newnulls); - } - else - /* DELETE case */ - rettuple = trigtuple; - - SPI_finish(); /* don't forget say Bye to SPI mgr */ - - pfree(relname); - return PointerGetDatum(rettuple); -} - -/* - * set_timetravel (relname, on) -- - * turn timetravel for specified relation ON/OFF - */ -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; - int32 ret; - TTOffList *prev, - *pp; - - prev = NULL; - for (pp = TTOff; pp; prev = pp, pp = pp->next) - { - if (namestrcmp(relname, pp->name) == 0) - break; - } - if (pp) - { - /* OFF currently */ - if (on != 0) - { - /* turn ON */ - if (prev) - prev->next = pp->next; - else - TTOff = pp->next; - free(pp); - } - ret = 0; - } - else - { - /* ON currently */ - if (on == 0) - { - /* turn OFF */ - s = rname = DatumGetCString(DirectFunctionCall1(nameout, NameGetDatum(relname))); - if (s) - { - pp = malloc(offsetof(TTOffList, name) + strlen(rname) + 1); - if (pp) - { - pp->next = NULL; - d = pp->name; - while (*s) - *d++ = tolower((unsigned char) *s++); - *d = '\0'; - if (prev) - prev->next = pp; - else - TTOff = pp; - } - pfree(rname); - } - } - ret = 1; - } - PG_RETURN_INT32(ret); -} - -/* - * get_timetravel (relname) -- - * get timetravel status for specified relation (ON/OFF) - */ -PG_FUNCTION_INFO_V1(get_timetravel); - -Datum -get_timetravel(PG_FUNCTION_ARGS) -{ - Name relname = PG_GETARG_NAME(0); - TTOffList *pp; - - for (pp = TTOff; 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; pp; pp = pp->next) - if (pg_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; - - 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 = strdup(ident); - newp->splan = NULL; - (*nplans)++; - - return newp; -} diff --git a/contrib/spi/timetravel.control b/contrib/spi/timetravel.control deleted file mode 100644 index 9b4bb6ba04..0000000000 --- a/contrib/spi/timetravel.control +++ /dev/null @@ -1,5 +0,0 @@ -# timetravel extension -comment = 'functions for implementing time travel' -default_version = '1.0' -module_pathname = '$libdir/timetravel' -relocatable = true diff --git a/contrib/spi/timetravel.example b/contrib/spi/timetravel.example deleted file mode 100644 index 35a7f65408..0000000000 --- a/contrib/spi/timetravel.example +++ /dev/null @@ -1,81 +0,0 @@ -drop table tttest; - -create table tttest ( - price_id int4, - price_val int4, - price_on 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 - before insert or delete or update on tttest - for each row - execute procedure - timetravel (price_on, price_off); - -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 (4, 4, - abstime('now'::timestamp - '100 days'::interval)); -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; -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! - -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; - --- 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! - -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'; --- but this doesn't work - --- 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; --- 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/doc/src/sgml/contrib-spi.sgml b/doc/src/sgml/contrib-spi.sgml index 844ea161c4..fed6f24932 100644 --- a/doc/src/sgml/contrib-spi.sgml +++ b/doc/src/sgml/contrib-spi.sgml @@ -65,99 +65,6 @@ - - timetravel — Functions for Implementing Time Travel - - - Long ago, PostgreSQL had a built-in time travel feature - that kept the insert and delete times for each tuple. This can be - emulated using these functions. To use these functions, - you must add to a table two columns of abstime type to store - the date when a tuple was inserted (start_date) and changed/deleted - (stop_date): - - -CREATE TABLE mytab ( - ... ... - start_date abstime, - stop_date abstime - ... ... -); - - - The columns can be named whatever you like, but in this discussion - we'll call them start_date and stop_date. - - - - When a new row is inserted, start_date should normally be set to - current time, and stop_date to infinity. The trigger - will automatically substitute these values if the inserted data - contains nulls in these columns. Generally, inserting explicit - non-null data in these columns should only be done when re-loading - dumped data. - - - - Tuples with stop_date equal to infinity are valid - now, and can be modified. Tuples with a finite stop_date cannot - be modified anymore — the trigger will prevent it. (If you need - to do that, you can turn off time travel as shown below.) - - - - For a modifiable row, on update only the stop_date in the tuple being - updated will be changed (to current time) and a new tuple with the modified - data will be inserted. Start_date in this new tuple will be set to current - time and stop_date to infinity. - - - - A delete does not actually remove the tuple but only sets its stop_date - to current time. - - - - To query for tuples valid now, include - stop_date = 'infinity' in the query's WHERE condition. - (You might wish to incorporate that in a view.) Similarly, you can - query for tuples valid at any past time with suitable conditions on - start_date and stop_date. - - - - timetravel() is the general trigger function that supports - this behavior. Create a BEFORE INSERT OR UPDATE OR DELETE - trigger using this function on each time-traveled table. Specify two - trigger arguments: the actual - names of the start_date and stop_date columns. - Optionally, you can specify one to three more arguments, which must refer - to columns of type text. The trigger will store the name of - the current user into the first of these columns during INSERT, the - second column during UPDATE, and the third during DELETE. - - - - set_timetravel() allows you to turn time-travel on or off for - a table. - set_timetravel('mytab', 1) will turn TT ON for table mytab. - set_timetravel('mytab', 0) will turn TT OFF for table mytab. - In both cases the old status is reported. While TT is off, you can modify - the start_date and stop_date columns freely. Note that the on/off status - is local to the current database session — fresh sessions will - always start out with TT ON for all tables. - - - - get_timetravel() returns the TT state for a table without - changing it. - - - - There is an example in timetravel.example. - - - autoinc — Functions for Autoincrementing Fields diff --git a/src/tools/msvc/Install.pm b/src/tools/msvc/Install.pm index c8ae1a0dc7..7e1c9ac848 100644 --- a/src/tools/msvc/Install.pm +++ b/src/tools/msvc/Install.pm @@ -605,7 +605,7 @@ sub CopySubdirFiles # Special case for contrib/spi $flist = - "autoinc.example insert_username.example moddatetime.example refint.example timetravel.example" + "autoinc.example insert_username.example moddatetime.example refint.example" if ($module eq 'spi'); foreach my $f (split /\s+/, $flist) {