mirror of
				https://github.com/postgres/postgres.git
				synced 2025-11-03 09:13:20 +03:00 
			
		
		
		
	
		
			
				
	
	
		
			424 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
			
		
		
	
	
			424 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
/*
 | 
						|
 * timetravel.c --	function to get time travel feature
 | 
						|
 *		using general triggers.
 | 
						|
 */
 | 
						|
 | 
						|
#include "executor/spi.h"		/* this is what you need to work with SPI */
 | 
						|
#include "commands/trigger.h"	/* -"- and triggers */
 | 
						|
#include <ctype.h>				/* tolower () */
 | 
						|
 | 
						|
#define ABSTIMEOID	702			/* it should be in pg_type.h */
 | 
						|
 | 
						|
AbsoluteTime currabstime(void);
 | 
						|
HeapTuple	timetravel(void);
 | 
						|
int32		set_timetravel(Name relname, int32 on);
 | 
						|
 | 
						|
typedef struct
 | 
						|
{
 | 
						|
	char	   *ident;
 | 
						|
	void	   *splan;
 | 
						|
}			EPlan;
 | 
						|
 | 
						|
static EPlan *Plans = NULL;		/* for UPDATE/DELETE */
 | 
						|
static int	nPlans = 0;
 | 
						|
 | 
						|
static char **TTOff = NULL;
 | 
						|
static int	nTTOff = 0;
 | 
						|
 | 
						|
static EPlan *find_plan(char *ident, EPlan ** eplan, int *nplans);
 | 
						|
 | 
						|
/*
 | 
						|
 * timetravel () --
 | 
						|
 *		1.	IF an update affects tuple with stop_date eq INFINITY
 | 
						|
 *			then form (and return) new tuple with stop_date eq current date
 | 
						|
 *			and all other column values as in old tuple, and insert tuple
 | 
						|
 *			with new data and start_date eq current date and
 | 
						|
 *			stop_date eq INFINITY
 | 
						|
 *			ELSE - skip updation of tuple.
 | 
						|
 *		2.	IF an delete affects tuple with stop_date eq INFINITY
 | 
						|
 *			then insert the same tuple with stop_date eq current date
 | 
						|
 *			ELSE - skip deletion of tuple.
 | 
						|
 *		3.	On INSERT, if start_date is NULL then current date will be
 | 
						|
 *			inserted, if stop_date is NULL then INFINITY will be inserted.
 | 
						|
 *
 | 
						|
 * 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 */
 | 
						|
	bool		isinsert = false;
 | 
						|
	int			ret;
 | 
						|
	int			i;
 | 
						|
 | 
						|
	/*
 | 
						|
	 * Some checks first...
 | 
						|
	 */
 | 
						|
 | 
						|
	/* Called by trigger manager ? */
 | 
						|
	if (!CurrentTriggerData)
 | 
						|
		elog(ERROR, "timetravel: triggers are not initialized");
 | 
						|
 | 
						|
	/* Should be called for ROW trigger */
 | 
						|
	if (TRIGGER_FIRED_FOR_STATEMENT(CurrentTriggerData->tg_event))
 | 
						|
		elog(ERROR, "timetravel: can't process STATEMENT events");
 | 
						|
 | 
						|
	/* Should be called BEFORE */
 | 
						|
	if (TRIGGER_FIRED_AFTER(CurrentTriggerData->tg_event))
 | 
						|
		elog(ERROR, "timetravel: must be fired before event");
 | 
						|
 | 
						|
	/* INSERT ? */
 | 
						|
	if (TRIGGER_FIRED_BY_INSERT(CurrentTriggerData->tg_event))
 | 
						|
		isinsert = true;
 | 
						|
 | 
						|
	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(ERROR, "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(ERROR, "timetravel (%s): there is no attribute %s", relname, args[i]);
 | 
						|
		if (SPI_gettypeid(tupdesc, attnum[i]) != ABSTIMEOID)
 | 
						|
			elog(ERROR, "timetravel (%s): attributes %s and %s must be of abstime type",
 | 
						|
				 relname, args[0], args[1]);
 | 
						|
	}
 | 
						|
 | 
						|
	if (isinsert)				/* INSERT */
 | 
						|
	{
 | 
						|
		int			chnattrs = 0;
 | 
						|
		int			chattrs[2];
 | 
						|
		Datum		newvals[2];
 | 
						|
 | 
						|
		oldon = SPI_getbinval(trigtuple, tupdesc, attnum[0], &isnull);
 | 
						|
		if (isnull)
 | 
						|
		{
 | 
						|
			newvals[chnattrs] = GetCurrentAbsoluteTime();
 | 
						|
			chattrs[chnattrs] = attnum[0];
 | 
						|
			chnattrs++;
 | 
						|
		}
 | 
						|
 | 
						|
		oldoff = SPI_getbinval(trigtuple, tupdesc, attnum[1], &isnull);
 | 
						|
		if (isnull)
 | 
						|
		{
 | 
						|
			if ((chnattrs == 0 && DatumGetInt32(oldon) >= NOEND_ABSTIME) ||
 | 
						|
			(chnattrs > 0 && DatumGetInt32(newvals[0]) >= NOEND_ABSTIME))
 | 
						|
				elog(ERROR, "timetravel (%s): %s ge %s",
 | 
						|
					 relname, args[0], args[1]);
 | 
						|
			newvals[chnattrs] = NOEND_ABSTIME;
 | 
						|
			chattrs[chnattrs] = attnum[1];
 | 
						|
			chnattrs++;
 | 
						|
		}
 | 
						|
		else
 | 
						|
		{
 | 
						|
			if ((chnattrs == 0 && DatumGetInt32(oldon) >=
 | 
						|
				 DatumGetInt32(oldoff)) ||
 | 
						|
				(chnattrs > 0 && DatumGetInt32(newvals[0]) >=
 | 
						|
				 DatumGetInt32(oldoff)))
 | 
						|
				elog(ERROR, "timetravel (%s): %s ge %s",
 | 
						|
					 relname, args[0], args[1]);
 | 
						|
		}
 | 
						|
 | 
						|
		pfree(relname);
 | 
						|
		if (chnattrs <= 0)
 | 
						|
			return (trigtuple);
 | 
						|
 | 
						|
		rettuple = SPI_modifytuple(rel, trigtuple, chnattrs,
 | 
						|
								   chattrs, newvals, NULL);
 | 
						|
		return (rettuple);
 | 
						|
	}
 | 
						|
 | 
						|
	oldon = SPI_getbinval(trigtuple, tupdesc, attnum[0], &isnull);
 | 
						|
	if (isnull)
 | 
						|
		elog(ERROR, "timetravel (%s): %s must be NOT NULL", relname, args[0]);
 | 
						|
 | 
						|
	oldoff = SPI_getbinval(trigtuple, tupdesc, attnum[1], &isnull);
 | 
						|
	if (isnull)
 | 
						|
		elog(ERROR, "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(ERROR, "timetravel (%s): %s must be NOT NULL", relname, args[0]);
 | 
						|
		newoff = SPI_getbinval(newtuple, tupdesc, attnum[1], &isnull);
 | 
						|
		if (isnull)
 | 
						|
			elog(ERROR, "timetravel (%s): %s must be NOT NULL", relname, args[1]);
 | 
						|
 | 
						|
		if (oldon != newon || oldoff != newoff)
 | 
						|
			elog(ERROR, "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(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((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(ERROR, "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(ERROR, "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(ERROR, "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);
 | 
						|
}
 |