mirror of
https://git.postgresql.org/git/postgresql.git
synced 2024-09-05 14:59:21 +02:00
unnecessary #include lines in it. Also, move some tuple routine prototypes and macros to htup.h, which allows removal of heapam.h inclusion from some .c files. For this to work, a new header file access/sysattr.h needed to be created, initially containing attribute numbers of system columns, for pg_dump usage. While at it, make contrib ltree, intarray and hstore header files more consistent with our header style.
3802 lines
105 KiB
C
3802 lines
105 KiB
C
/*-------------------------------------------------------------------------
|
|
*
|
|
* trigger.c
|
|
* PostgreSQL TRIGGERs support code.
|
|
*
|
|
* Portions Copyright (c) 1996-2008, PostgreSQL Global Development Group
|
|
* Portions Copyright (c) 1994, Regents of the University of California
|
|
*
|
|
* IDENTIFICATION
|
|
* $PostgreSQL: pgsql/src/backend/commands/trigger.c,v 1.232 2008/05/12 00:00:48 alvherre Exp $
|
|
*
|
|
*-------------------------------------------------------------------------
|
|
*/
|
|
#include "postgres.h"
|
|
|
|
#include "access/genam.h"
|
|
#include "access/heapam.h"
|
|
#include "access/sysattr.h"
|
|
#include "access/xact.h"
|
|
#include "catalog/catalog.h"
|
|
#include "catalog/dependency.h"
|
|
#include "catalog/indexing.h"
|
|
#include "catalog/pg_constraint.h"
|
|
#include "catalog/pg_proc.h"
|
|
#include "catalog/pg_trigger.h"
|
|
#include "catalog/pg_type.h"
|
|
#include "commands/dbcommands.h"
|
|
#include "commands/defrem.h"
|
|
#include "commands/trigger.h"
|
|
#include "executor/executor.h"
|
|
#include "executor/instrument.h"
|
|
#include "miscadmin.h"
|
|
#include "nodes/makefuncs.h"
|
|
#include "parser/parse_func.h"
|
|
#include "storage/bufmgr.h"
|
|
#include "tcop/utility.h"
|
|
#include "utils/acl.h"
|
|
#include "utils/builtins.h"
|
|
#include "utils/fmgroids.h"
|
|
#include "utils/inval.h"
|
|
#include "utils/lsyscache.h"
|
|
#include "utils/memutils.h"
|
|
#include "utils/snapmgr.h"
|
|
#include "utils/syscache.h"
|
|
#include "utils/tqual.h"
|
|
|
|
|
|
/* GUC variables */
|
|
int SessionReplicationRole = SESSION_REPLICATION_ROLE_ORIGIN;
|
|
|
|
|
|
/* Local function prototypes */
|
|
static void ConvertTriggerToFK(CreateTrigStmt *stmt, Oid funcoid);
|
|
static void InsertTrigger(TriggerDesc *trigdesc, Trigger *trigger, int indx);
|
|
static HeapTuple GetTupleForTrigger(EState *estate,
|
|
ResultRelInfo *relinfo,
|
|
ItemPointer tid,
|
|
TupleTableSlot **newSlot);
|
|
static HeapTuple ExecCallTriggerFunc(TriggerData *trigdata,
|
|
int tgindx,
|
|
FmgrInfo *finfo,
|
|
Instrumentation *instr,
|
|
MemoryContext per_tuple_context);
|
|
static void AfterTriggerSaveEvent(ResultRelInfo *relinfo, int event,
|
|
bool row_trigger, HeapTuple oldtup, HeapTuple newtup);
|
|
|
|
|
|
/*
|
|
* Create a trigger. Returns the OID of the created trigger.
|
|
*
|
|
* constraintOid, if nonzero, says that this trigger is being created
|
|
* internally to implement that constraint. A suitable pg_depend entry will
|
|
* be made to link the trigger to that constraint. constraintOid is zero when
|
|
* executing a user-entered CREATE TRIGGER command.
|
|
*
|
|
* Note: can return InvalidOid if we decided to not create a trigger at all,
|
|
* but a foreign-key constraint. This is a kluge for backwards compatibility.
|
|
*/
|
|
Oid
|
|
CreateTrigger(CreateTrigStmt *stmt, Oid constraintOid)
|
|
{
|
|
int16 tgtype;
|
|
int2vector *tgattr;
|
|
Datum values[Natts_pg_trigger];
|
|
char nulls[Natts_pg_trigger];
|
|
Relation rel;
|
|
AclResult aclresult;
|
|
Relation tgrel;
|
|
SysScanDesc tgscan;
|
|
ScanKeyData key;
|
|
Relation pgrel;
|
|
HeapTuple tuple;
|
|
Oid fargtypes[1]; /* dummy */
|
|
Oid funcoid;
|
|
Oid funcrettype;
|
|
Oid trigoid;
|
|
int found = 0;
|
|
int i;
|
|
char constrtrigname[NAMEDATALEN];
|
|
char *trigname;
|
|
char *constrname;
|
|
Oid constrrelid = InvalidOid;
|
|
ObjectAddress myself,
|
|
referenced;
|
|
|
|
rel = heap_openrv(stmt->relation, AccessExclusiveLock);
|
|
|
|
if (rel->rd_rel->relkind != RELKIND_RELATION)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
|
|
errmsg("\"%s\" is not a table",
|
|
RelationGetRelationName(rel))));
|
|
|
|
if (!allowSystemTableMods && IsSystemRelation(rel))
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
|
|
errmsg("permission denied: \"%s\" is a system catalog",
|
|
RelationGetRelationName(rel))));
|
|
|
|
/* permission checks */
|
|
|
|
if (stmt->isconstraint)
|
|
{
|
|
/* constraint trigger */
|
|
aclresult = pg_class_aclcheck(RelationGetRelid(rel), GetUserId(),
|
|
ACL_REFERENCES);
|
|
if (aclresult != ACLCHECK_OK)
|
|
aclcheck_error(aclresult, ACL_KIND_CLASS,
|
|
RelationGetRelationName(rel));
|
|
|
|
if (stmt->constrrel != NULL)
|
|
{
|
|
constrrelid = RangeVarGetRelid(stmt->constrrel, false);
|
|
|
|
aclresult = pg_class_aclcheck(constrrelid, GetUserId(),
|
|
ACL_REFERENCES);
|
|
if (aclresult != ACLCHECK_OK)
|
|
aclcheck_error(aclresult, ACL_KIND_CLASS,
|
|
get_rel_name(constrrelid));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
/* regular trigger */
|
|
aclresult = pg_class_aclcheck(RelationGetRelid(rel), GetUserId(),
|
|
ACL_TRIGGER);
|
|
if (aclresult != ACLCHECK_OK)
|
|
aclcheck_error(aclresult, ACL_KIND_CLASS,
|
|
RelationGetRelationName(rel));
|
|
}
|
|
|
|
/* Compute tgtype */
|
|
TRIGGER_CLEAR_TYPE(tgtype);
|
|
if (stmt->before)
|
|
TRIGGER_SETT_BEFORE(tgtype);
|
|
if (stmt->row)
|
|
TRIGGER_SETT_ROW(tgtype);
|
|
|
|
for (i = 0; stmt->actions[i]; i++)
|
|
{
|
|
switch (stmt->actions[i])
|
|
{
|
|
case 'i':
|
|
if (TRIGGER_FOR_INSERT(tgtype))
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_SYNTAX_ERROR),
|
|
errmsg("multiple INSERT events specified")));
|
|
TRIGGER_SETT_INSERT(tgtype);
|
|
break;
|
|
case 'd':
|
|
if (TRIGGER_FOR_DELETE(tgtype))
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_SYNTAX_ERROR),
|
|
errmsg("multiple DELETE events specified")));
|
|
TRIGGER_SETT_DELETE(tgtype);
|
|
break;
|
|
case 'u':
|
|
if (TRIGGER_FOR_UPDATE(tgtype))
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_SYNTAX_ERROR),
|
|
errmsg("multiple UPDATE events specified")));
|
|
TRIGGER_SETT_UPDATE(tgtype);
|
|
break;
|
|
case 't':
|
|
if (TRIGGER_FOR_TRUNCATE(tgtype))
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_SYNTAX_ERROR),
|
|
errmsg("multiple TRUNCATE events specified")));
|
|
TRIGGER_SETT_TRUNCATE(tgtype);
|
|
/* Disallow ROW-level TRUNCATE triggers */
|
|
if (stmt->row)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
|
|
errmsg("TRUNCATE FOR EACH ROW triggers are not supported")));
|
|
break;
|
|
default:
|
|
elog(ERROR, "unrecognized trigger event: %d",
|
|
(int) stmt->actions[i]);
|
|
break;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Find and validate the trigger function.
|
|
*/
|
|
funcoid = LookupFuncName(stmt->funcname, 0, fargtypes, false);
|
|
funcrettype = get_func_rettype(funcoid);
|
|
if (funcrettype != TRIGGEROID)
|
|
{
|
|
/*
|
|
* We allow OPAQUE just so we can load old dump files. When we see a
|
|
* trigger function declared OPAQUE, change it to TRIGGER.
|
|
*/
|
|
if (funcrettype == OPAQUEOID)
|
|
{
|
|
ereport(WARNING,
|
|
(errmsg("changing return type of function %s from \"opaque\" to \"trigger\"",
|
|
NameListToString(stmt->funcname))));
|
|
SetFunctionReturnType(funcoid, TRIGGEROID);
|
|
}
|
|
else
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
|
|
errmsg("function %s must return type \"trigger\"",
|
|
NameListToString(stmt->funcname))));
|
|
}
|
|
|
|
/*
|
|
* If the command is a user-entered CREATE CONSTRAINT TRIGGER command that
|
|
* references one of the built-in RI_FKey trigger functions, assume it is
|
|
* from a dump of a pre-7.3 foreign key constraint, and take steps to
|
|
* convert this legacy representation into a regular foreign key
|
|
* constraint. Ugly, but necessary for loading old dump files.
|
|
*/
|
|
if (stmt->isconstraint && !OidIsValid(constraintOid) &&
|
|
list_length(stmt->args) >= 6 &&
|
|
(list_length(stmt->args) % 2) == 0 &&
|
|
RI_FKey_trigger_type(funcoid) != RI_TRIGGER_NONE)
|
|
{
|
|
/* Keep lock on target rel until end of xact */
|
|
heap_close(rel, NoLock);
|
|
|
|
ConvertTriggerToFK(stmt, funcoid);
|
|
|
|
return InvalidOid;
|
|
}
|
|
|
|
/*
|
|
* Generate the trigger's OID now, so that we can use it in the name if
|
|
* needed.
|
|
*/
|
|
tgrel = heap_open(TriggerRelationId, RowExclusiveLock);
|
|
|
|
trigoid = GetNewOid(tgrel);
|
|
|
|
/*
|
|
* If trigger is for an RI constraint, the passed-in name is the
|
|
* constraint name; save that and build a unique trigger name to avoid
|
|
* collisions with user-selected trigger names.
|
|
*/
|
|
if (OidIsValid(constraintOid))
|
|
{
|
|
snprintf(constrtrigname, sizeof(constrtrigname),
|
|
"RI_ConstraintTrigger_%u", trigoid);
|
|
trigname = constrtrigname;
|
|
constrname = stmt->trigname;
|
|
}
|
|
else if (stmt->isconstraint)
|
|
{
|
|
/* constraint trigger: trigger name is also constraint name */
|
|
trigname = stmt->trigname;
|
|
constrname = stmt->trigname;
|
|
}
|
|
else
|
|
{
|
|
/* regular trigger: use empty constraint name */
|
|
trigname = stmt->trigname;
|
|
constrname = "";
|
|
}
|
|
|
|
/*
|
|
* Scan pg_trigger for existing triggers on relation. We do this mainly
|
|
* because we must count them; a secondary benefit is to give a nice error
|
|
* message if there's already a trigger of the same name. (The unique
|
|
* index on tgrelid/tgname would complain anyway.)
|
|
*
|
|
* NOTE that this is cool only because we have AccessExclusiveLock on the
|
|
* relation, so the trigger set won't be changing underneath us.
|
|
*/
|
|
ScanKeyInit(&key,
|
|
Anum_pg_trigger_tgrelid,
|
|
BTEqualStrategyNumber, F_OIDEQ,
|
|
ObjectIdGetDatum(RelationGetRelid(rel)));
|
|
tgscan = systable_beginscan(tgrel, TriggerRelidNameIndexId, true,
|
|
SnapshotNow, 1, &key);
|
|
while (HeapTupleIsValid(tuple = systable_getnext(tgscan)))
|
|
{
|
|
Form_pg_trigger pg_trigger = (Form_pg_trigger) GETSTRUCT(tuple);
|
|
|
|
if (namestrcmp(&(pg_trigger->tgname), trigname) == 0)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_DUPLICATE_OBJECT),
|
|
errmsg("trigger \"%s\" for relation \"%s\" already exists",
|
|
trigname, stmt->relation->relname)));
|
|
found++;
|
|
}
|
|
systable_endscan(tgscan);
|
|
|
|
/*
|
|
* Build the new pg_trigger tuple.
|
|
*/
|
|
memset(nulls, ' ', Natts_pg_trigger * sizeof(char));
|
|
|
|
values[Anum_pg_trigger_tgrelid - 1] = ObjectIdGetDatum(RelationGetRelid(rel));
|
|
values[Anum_pg_trigger_tgname - 1] = DirectFunctionCall1(namein,
|
|
CStringGetDatum(trigname));
|
|
values[Anum_pg_trigger_tgfoid - 1] = ObjectIdGetDatum(funcoid);
|
|
values[Anum_pg_trigger_tgtype - 1] = Int16GetDatum(tgtype);
|
|
values[Anum_pg_trigger_tgenabled - 1] = CharGetDatum(TRIGGER_FIRES_ON_ORIGIN);
|
|
values[Anum_pg_trigger_tgisconstraint - 1] = BoolGetDatum(stmt->isconstraint);
|
|
values[Anum_pg_trigger_tgconstrname - 1] = DirectFunctionCall1(namein,
|
|
CStringGetDatum(constrname));
|
|
values[Anum_pg_trigger_tgconstrrelid - 1] = ObjectIdGetDatum(constrrelid);
|
|
values[Anum_pg_trigger_tgconstraint - 1] = ObjectIdGetDatum(constraintOid);
|
|
values[Anum_pg_trigger_tgdeferrable - 1] = BoolGetDatum(stmt->deferrable);
|
|
values[Anum_pg_trigger_tginitdeferred - 1] = BoolGetDatum(stmt->initdeferred);
|
|
|
|
if (stmt->args)
|
|
{
|
|
ListCell *le;
|
|
char *args;
|
|
int16 nargs = list_length(stmt->args);
|
|
int len = 0;
|
|
|
|
foreach(le, stmt->args)
|
|
{
|
|
char *ar = strVal(lfirst(le));
|
|
|
|
len += strlen(ar) + 4;
|
|
for (; *ar; ar++)
|
|
{
|
|
if (*ar == '\\')
|
|
len++;
|
|
}
|
|
}
|
|
args = (char *) palloc(len + 1);
|
|
args[0] = '\0';
|
|
foreach(le, stmt->args)
|
|
{
|
|
char *s = strVal(lfirst(le));
|
|
char *d = args + strlen(args);
|
|
|
|
while (*s)
|
|
{
|
|
if (*s == '\\')
|
|
*d++ = '\\';
|
|
*d++ = *s++;
|
|
}
|
|
strcpy(d, "\\000");
|
|
}
|
|
values[Anum_pg_trigger_tgnargs - 1] = Int16GetDatum(nargs);
|
|
values[Anum_pg_trigger_tgargs - 1] = DirectFunctionCall1(byteain,
|
|
CStringGetDatum(args));
|
|
}
|
|
else
|
|
{
|
|
values[Anum_pg_trigger_tgnargs - 1] = Int16GetDatum(0);
|
|
values[Anum_pg_trigger_tgargs - 1] = DirectFunctionCall1(byteain,
|
|
CStringGetDatum(""));
|
|
}
|
|
|
|
/* tgattr is currently always a zero-length array */
|
|
tgattr = buildint2vector(NULL, 0);
|
|
values[Anum_pg_trigger_tgattr - 1] = PointerGetDatum(tgattr);
|
|
|
|
tuple = heap_formtuple(tgrel->rd_att, values, nulls);
|
|
|
|
/* force tuple to have the desired OID */
|
|
HeapTupleSetOid(tuple, trigoid);
|
|
|
|
/*
|
|
* Insert tuple into pg_trigger.
|
|
*/
|
|
simple_heap_insert(tgrel, tuple);
|
|
|
|
CatalogUpdateIndexes(tgrel, tuple);
|
|
|
|
heap_freetuple(tuple);
|
|
heap_close(tgrel, RowExclusiveLock);
|
|
|
|
pfree(DatumGetPointer(values[Anum_pg_trigger_tgname - 1]));
|
|
pfree(DatumGetPointer(values[Anum_pg_trigger_tgargs - 1]));
|
|
|
|
/*
|
|
* Update relation's pg_class entry. Crucial side-effect: other backends
|
|
* (and this one too!) are sent SI message to make them rebuild relcache
|
|
* entries.
|
|
*/
|
|
pgrel = heap_open(RelationRelationId, RowExclusiveLock);
|
|
tuple = SearchSysCacheCopy(RELOID,
|
|
ObjectIdGetDatum(RelationGetRelid(rel)),
|
|
0, 0, 0);
|
|
if (!HeapTupleIsValid(tuple))
|
|
elog(ERROR, "cache lookup failed for relation %u",
|
|
RelationGetRelid(rel));
|
|
|
|
((Form_pg_class) GETSTRUCT(tuple))->reltriggers = found + 1;
|
|
|
|
simple_heap_update(pgrel, &tuple->t_self, tuple);
|
|
|
|
CatalogUpdateIndexes(pgrel, tuple);
|
|
|
|
heap_freetuple(tuple);
|
|
heap_close(pgrel, RowExclusiveLock);
|
|
|
|
/*
|
|
* We used to try to update the rel's relcache entry here, but that's
|
|
* fairly pointless since it will happen as a byproduct of the upcoming
|
|
* CommandCounterIncrement...
|
|
*/
|
|
|
|
/*
|
|
* Record dependencies for trigger. Always place a normal dependency on
|
|
* the function.
|
|
*/
|
|
myself.classId = TriggerRelationId;
|
|
myself.objectId = trigoid;
|
|
myself.objectSubId = 0;
|
|
|
|
referenced.classId = ProcedureRelationId;
|
|
referenced.objectId = funcoid;
|
|
referenced.objectSubId = 0;
|
|
recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL);
|
|
|
|
if (OidIsValid(constraintOid))
|
|
{
|
|
/*
|
|
* It's for a constraint, so make it an internal dependency of the
|
|
* constraint. We can skip depending on the relations, as there'll be
|
|
* an indirect dependency via the constraint.
|
|
*/
|
|
referenced.classId = ConstraintRelationId;
|
|
referenced.objectId = constraintOid;
|
|
referenced.objectSubId = 0;
|
|
recordDependencyOn(&myself, &referenced, DEPENDENCY_INTERNAL);
|
|
}
|
|
else
|
|
{
|
|
/*
|
|
* Regular CREATE TRIGGER, so place dependencies. We make trigger be
|
|
* auto-dropped if its relation is dropped or if the FK relation is
|
|
* dropped. (Auto drop is compatible with our pre-7.3 behavior.)
|
|
*/
|
|
referenced.classId = RelationRelationId;
|
|
referenced.objectId = RelationGetRelid(rel);
|
|
referenced.objectSubId = 0;
|
|
recordDependencyOn(&myself, &referenced, DEPENDENCY_AUTO);
|
|
if (constrrelid != InvalidOid)
|
|
{
|
|
referenced.classId = RelationRelationId;
|
|
referenced.objectId = constrrelid;
|
|
referenced.objectSubId = 0;
|
|
recordDependencyOn(&myself, &referenced, DEPENDENCY_AUTO);
|
|
}
|
|
}
|
|
|
|
/* Keep lock on target rel until end of xact */
|
|
heap_close(rel, NoLock);
|
|
|
|
return trigoid;
|
|
}
|
|
|
|
|
|
/*
|
|
* Convert legacy (pre-7.3) CREATE CONSTRAINT TRIGGER commands into
|
|
* full-fledged foreign key constraints.
|
|
*
|
|
* The conversion is complex because a pre-7.3 foreign key involved three
|
|
* separate triggers, which were reported separately in dumps. While the
|
|
* single trigger on the referencing table adds no new information, we need
|
|
* to know the trigger functions of both of the triggers on the referenced
|
|
* table to build the constraint declaration. Also, due to lack of proper
|
|
* dependency checking pre-7.3, it is possible that the source database had
|
|
* an incomplete set of triggers resulting in an only partially enforced
|
|
* FK constraint. (This would happen if one of the tables had been dropped
|
|
* and re-created, but only if the DB had been affected by a 7.0 pg_dump bug
|
|
* that caused loss of tgconstrrelid information.) We choose to translate to
|
|
* an FK constraint only when we've seen all three triggers of a set. This is
|
|
* implemented by storing unmatched items in a list in TopMemoryContext.
|
|
* We match triggers together by comparing the trigger arguments (which
|
|
* include constraint name, table and column names, so should be good enough).
|
|
*/
|
|
typedef struct
|
|
{
|
|
List *args; /* list of (T_String) Values or NIL */
|
|
Oid funcoids[3]; /* OIDs of trigger functions */
|
|
/* The three function OIDs are stored in the order update, delete, child */
|
|
} OldTriggerInfo;
|
|
|
|
static void
|
|
ConvertTriggerToFK(CreateTrigStmt *stmt, Oid funcoid)
|
|
{
|
|
static List *info_list = NIL;
|
|
|
|
static const char *const funcdescr[3] = {
|
|
gettext_noop("Found referenced table's UPDATE trigger."),
|
|
gettext_noop("Found referenced table's DELETE trigger."),
|
|
gettext_noop("Found referencing table's trigger.")
|
|
};
|
|
|
|
char *constr_name;
|
|
char *fk_table_name;
|
|
char *pk_table_name;
|
|
char fk_matchtype = FKCONSTR_MATCH_UNSPECIFIED;
|
|
List *fk_attrs = NIL;
|
|
List *pk_attrs = NIL;
|
|
StringInfoData buf;
|
|
int funcnum;
|
|
OldTriggerInfo *info = NULL;
|
|
ListCell *l;
|
|
int i;
|
|
|
|
/* Parse out the trigger arguments */
|
|
constr_name = strVal(linitial(stmt->args));
|
|
fk_table_name = strVal(lsecond(stmt->args));
|
|
pk_table_name = strVal(lthird(stmt->args));
|
|
i = 0;
|
|
foreach(l, stmt->args)
|
|
{
|
|
Value *arg = (Value *) lfirst(l);
|
|
|
|
i++;
|
|
if (i < 4) /* skip constraint and table names */
|
|
continue;
|
|
if (i == 4) /* handle match type */
|
|
{
|
|
if (strcmp(strVal(arg), "FULL") == 0)
|
|
fk_matchtype = FKCONSTR_MATCH_FULL;
|
|
else
|
|
fk_matchtype = FKCONSTR_MATCH_UNSPECIFIED;
|
|
continue;
|
|
}
|
|
if (i % 2)
|
|
fk_attrs = lappend(fk_attrs, arg);
|
|
else
|
|
pk_attrs = lappend(pk_attrs, arg);
|
|
}
|
|
|
|
/* Prepare description of constraint for use in messages */
|
|
initStringInfo(&buf);
|
|
appendStringInfo(&buf, "FOREIGN KEY %s(",
|
|
quote_identifier(fk_table_name));
|
|
i = 0;
|
|
foreach(l, fk_attrs)
|
|
{
|
|
Value *arg = (Value *) lfirst(l);
|
|
|
|
if (i++ > 0)
|
|
appendStringInfoChar(&buf, ',');
|
|
appendStringInfoString(&buf, quote_identifier(strVal(arg)));
|
|
}
|
|
appendStringInfo(&buf, ") REFERENCES %s(",
|
|
quote_identifier(pk_table_name));
|
|
i = 0;
|
|
foreach(l, pk_attrs)
|
|
{
|
|
Value *arg = (Value *) lfirst(l);
|
|
|
|
if (i++ > 0)
|
|
appendStringInfoChar(&buf, ',');
|
|
appendStringInfoString(&buf, quote_identifier(strVal(arg)));
|
|
}
|
|
appendStringInfoChar(&buf, ')');
|
|
|
|
/* Identify class of trigger --- update, delete, or referencing-table */
|
|
switch (funcoid)
|
|
{
|
|
case F_RI_FKEY_CASCADE_UPD:
|
|
case F_RI_FKEY_RESTRICT_UPD:
|
|
case F_RI_FKEY_SETNULL_UPD:
|
|
case F_RI_FKEY_SETDEFAULT_UPD:
|
|
case F_RI_FKEY_NOACTION_UPD:
|
|
funcnum = 0;
|
|
break;
|
|
|
|
case F_RI_FKEY_CASCADE_DEL:
|
|
case F_RI_FKEY_RESTRICT_DEL:
|
|
case F_RI_FKEY_SETNULL_DEL:
|
|
case F_RI_FKEY_SETDEFAULT_DEL:
|
|
case F_RI_FKEY_NOACTION_DEL:
|
|
funcnum = 1;
|
|
break;
|
|
|
|
default:
|
|
funcnum = 2;
|
|
break;
|
|
}
|
|
|
|
/* See if we have a match to this trigger */
|
|
foreach(l, info_list)
|
|
{
|
|
info = (OldTriggerInfo *) lfirst(l);
|
|
if (info->funcoids[funcnum] == InvalidOid &&
|
|
equal(info->args, stmt->args))
|
|
{
|
|
info->funcoids[funcnum] = funcoid;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (l == NULL)
|
|
{
|
|
/* First trigger of set, so create a new list entry */
|
|
MemoryContext oldContext;
|
|
|
|
ereport(NOTICE,
|
|
(errmsg("ignoring incomplete trigger group for constraint \"%s\" %s",
|
|
constr_name, buf.data),
|
|
errdetail(funcdescr[funcnum])));
|
|
oldContext = MemoryContextSwitchTo(TopMemoryContext);
|
|
info = (OldTriggerInfo *) palloc0(sizeof(OldTriggerInfo));
|
|
info->args = copyObject(stmt->args);
|
|
info->funcoids[funcnum] = funcoid;
|
|
info_list = lappend(info_list, info);
|
|
MemoryContextSwitchTo(oldContext);
|
|
}
|
|
else if (info->funcoids[0] == InvalidOid ||
|
|
info->funcoids[1] == InvalidOid ||
|
|
info->funcoids[2] == InvalidOid)
|
|
{
|
|
/* Second trigger of set */
|
|
ereport(NOTICE,
|
|
(errmsg("ignoring incomplete trigger group for constraint \"%s\" %s",
|
|
constr_name, buf.data),
|
|
errdetail(funcdescr[funcnum])));
|
|
}
|
|
else
|
|
{
|
|
/* OK, we have a set, so make the FK constraint ALTER TABLE cmd */
|
|
AlterTableStmt *atstmt = makeNode(AlterTableStmt);
|
|
AlterTableCmd *atcmd = makeNode(AlterTableCmd);
|
|
FkConstraint *fkcon = makeNode(FkConstraint);
|
|
|
|
ereport(NOTICE,
|
|
(errmsg("converting trigger group into constraint \"%s\" %s",
|
|
constr_name, buf.data),
|
|
errdetail(funcdescr[funcnum])));
|
|
if (funcnum == 2)
|
|
{
|
|
/* This trigger is on the FK table */
|
|
atstmt->relation = stmt->relation;
|
|
if (stmt->constrrel)
|
|
fkcon->pktable = stmt->constrrel;
|
|
else
|
|
{
|
|
/* Work around ancient pg_dump bug that omitted constrrel */
|
|
fkcon->pktable = makeRangeVar(NULL, pk_table_name);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
/* This trigger is on the PK table */
|
|
fkcon->pktable = stmt->relation;
|
|
if (stmt->constrrel)
|
|
atstmt->relation = stmt->constrrel;
|
|
else
|
|
{
|
|
/* Work around ancient pg_dump bug that omitted constrrel */
|
|
atstmt->relation = makeRangeVar(NULL, fk_table_name);
|
|
}
|
|
}
|
|
atstmt->cmds = list_make1(atcmd);
|
|
atstmt->relkind = OBJECT_TABLE;
|
|
atcmd->subtype = AT_AddConstraint;
|
|
atcmd->def = (Node *) fkcon;
|
|
if (strcmp(constr_name, "<unnamed>") == 0)
|
|
fkcon->constr_name = NULL;
|
|
else
|
|
fkcon->constr_name = constr_name;
|
|
fkcon->fk_attrs = fk_attrs;
|
|
fkcon->pk_attrs = pk_attrs;
|
|
fkcon->fk_matchtype = fk_matchtype;
|
|
switch (info->funcoids[0])
|
|
{
|
|
case F_RI_FKEY_NOACTION_UPD:
|
|
fkcon->fk_upd_action = FKCONSTR_ACTION_NOACTION;
|
|
break;
|
|
case F_RI_FKEY_CASCADE_UPD:
|
|
fkcon->fk_upd_action = FKCONSTR_ACTION_CASCADE;
|
|
break;
|
|
case F_RI_FKEY_RESTRICT_UPD:
|
|
fkcon->fk_upd_action = FKCONSTR_ACTION_RESTRICT;
|
|
break;
|
|
case F_RI_FKEY_SETNULL_UPD:
|
|
fkcon->fk_upd_action = FKCONSTR_ACTION_SETNULL;
|
|
break;
|
|
case F_RI_FKEY_SETDEFAULT_UPD:
|
|
fkcon->fk_upd_action = FKCONSTR_ACTION_SETDEFAULT;
|
|
break;
|
|
default:
|
|
/* can't get here because of earlier checks */
|
|
elog(ERROR, "confused about RI update function");
|
|
}
|
|
switch (info->funcoids[1])
|
|
{
|
|
case F_RI_FKEY_NOACTION_DEL:
|
|
fkcon->fk_del_action = FKCONSTR_ACTION_NOACTION;
|
|
break;
|
|
case F_RI_FKEY_CASCADE_DEL:
|
|
fkcon->fk_del_action = FKCONSTR_ACTION_CASCADE;
|
|
break;
|
|
case F_RI_FKEY_RESTRICT_DEL:
|
|
fkcon->fk_del_action = FKCONSTR_ACTION_RESTRICT;
|
|
break;
|
|
case F_RI_FKEY_SETNULL_DEL:
|
|
fkcon->fk_del_action = FKCONSTR_ACTION_SETNULL;
|
|
break;
|
|
case F_RI_FKEY_SETDEFAULT_DEL:
|
|
fkcon->fk_del_action = FKCONSTR_ACTION_SETDEFAULT;
|
|
break;
|
|
default:
|
|
/* can't get here because of earlier checks */
|
|
elog(ERROR, "confused about RI delete function");
|
|
}
|
|
fkcon->deferrable = stmt->deferrable;
|
|
fkcon->initdeferred = stmt->initdeferred;
|
|
|
|
/* ... and execute it */
|
|
ProcessUtility((Node *) atstmt,
|
|
NULL, NULL, false, None_Receiver, NULL);
|
|
|
|
/* Remove the matched item from the list */
|
|
info_list = list_delete_ptr(info_list, info);
|
|
pfree(info);
|
|
/* We leak the copied args ... not worth worrying about */
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
* DropTrigger - drop an individual trigger by name
|
|
*/
|
|
void
|
|
DropTrigger(Oid relid, const char *trigname, DropBehavior behavior,
|
|
bool missing_ok)
|
|
{
|
|
Relation tgrel;
|
|
ScanKeyData skey[2];
|
|
SysScanDesc tgscan;
|
|
HeapTuple tup;
|
|
ObjectAddress object;
|
|
|
|
/*
|
|
* Find the trigger, verify permissions, set up object address
|
|
*/
|
|
tgrel = heap_open(TriggerRelationId, AccessShareLock);
|
|
|
|
ScanKeyInit(&skey[0],
|
|
Anum_pg_trigger_tgrelid,
|
|
BTEqualStrategyNumber, F_OIDEQ,
|
|
ObjectIdGetDatum(relid));
|
|
|
|
ScanKeyInit(&skey[1],
|
|
Anum_pg_trigger_tgname,
|
|
BTEqualStrategyNumber, F_NAMEEQ,
|
|
CStringGetDatum(trigname));
|
|
|
|
tgscan = systable_beginscan(tgrel, TriggerRelidNameIndexId, true,
|
|
SnapshotNow, 2, skey);
|
|
|
|
tup = systable_getnext(tgscan);
|
|
|
|
if (!HeapTupleIsValid(tup))
|
|
{
|
|
if (!missing_ok)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_UNDEFINED_OBJECT),
|
|
errmsg("trigger \"%s\" for table \"%s\" does not exist",
|
|
trigname, get_rel_name(relid))));
|
|
else
|
|
ereport(NOTICE,
|
|
(errmsg("trigger \"%s\" for table \"%s\" does not exist, skipping",
|
|
trigname, get_rel_name(relid))));
|
|
/* cleanup */
|
|
systable_endscan(tgscan);
|
|
heap_close(tgrel, AccessShareLock);
|
|
return;
|
|
}
|
|
|
|
if (!pg_class_ownercheck(relid, GetUserId()))
|
|
aclcheck_error(ACLCHECK_NOT_OWNER, ACL_KIND_CLASS,
|
|
get_rel_name(relid));
|
|
|
|
object.classId = TriggerRelationId;
|
|
object.objectId = HeapTupleGetOid(tup);
|
|
object.objectSubId = 0;
|
|
|
|
systable_endscan(tgscan);
|
|
heap_close(tgrel, AccessShareLock);
|
|
|
|
/*
|
|
* Do the deletion
|
|
*/
|
|
performDeletion(&object, behavior);
|
|
}
|
|
|
|
/*
|
|
* Guts of trigger deletion.
|
|
*/
|
|
void
|
|
RemoveTriggerById(Oid trigOid)
|
|
{
|
|
Relation tgrel;
|
|
SysScanDesc tgscan;
|
|
ScanKeyData skey[1];
|
|
HeapTuple tup;
|
|
Oid relid;
|
|
Relation rel;
|
|
Relation pgrel;
|
|
HeapTuple tuple;
|
|
Form_pg_class classForm;
|
|
|
|
tgrel = heap_open(TriggerRelationId, RowExclusiveLock);
|
|
|
|
/*
|
|
* Find the trigger to delete.
|
|
*/
|
|
ScanKeyInit(&skey[0],
|
|
ObjectIdAttributeNumber,
|
|
BTEqualStrategyNumber, F_OIDEQ,
|
|
ObjectIdGetDatum(trigOid));
|
|
|
|
tgscan = systable_beginscan(tgrel, TriggerOidIndexId, true,
|
|
SnapshotNow, 1, skey);
|
|
|
|
tup = systable_getnext(tgscan);
|
|
if (!HeapTupleIsValid(tup))
|
|
elog(ERROR, "could not find tuple for trigger %u", trigOid);
|
|
|
|
/*
|
|
* Open and exclusive-lock the relation the trigger belongs to.
|
|
*/
|
|
relid = ((Form_pg_trigger) GETSTRUCT(tup))->tgrelid;
|
|
|
|
rel = heap_open(relid, AccessExclusiveLock);
|
|
|
|
if (rel->rd_rel->relkind != RELKIND_RELATION)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
|
|
errmsg("\"%s\" is not a table",
|
|
RelationGetRelationName(rel))));
|
|
|
|
if (!allowSystemTableMods && IsSystemRelation(rel))
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
|
|
errmsg("permission denied: \"%s\" is a system catalog",
|
|
RelationGetRelationName(rel))));
|
|
|
|
/*
|
|
* Delete the pg_trigger tuple.
|
|
*/
|
|
simple_heap_delete(tgrel, &tup->t_self);
|
|
|
|
systable_endscan(tgscan);
|
|
heap_close(tgrel, RowExclusiveLock);
|
|
|
|
/*
|
|
* Update relation's pg_class entry. Crucial side-effect: other backends
|
|
* (and this one too!) are sent SI message to make them rebuild relcache
|
|
* entries.
|
|
*
|
|
* Note this is OK only because we have AccessExclusiveLock on the rel, so
|
|
* no one else is creating/deleting triggers on this rel at the same time.
|
|
*/
|
|
pgrel = heap_open(RelationRelationId, RowExclusiveLock);
|
|
tuple = SearchSysCacheCopy(RELOID,
|
|
ObjectIdGetDatum(relid),
|
|
0, 0, 0);
|
|
if (!HeapTupleIsValid(tuple))
|
|
elog(ERROR, "cache lookup failed for relation %u", relid);
|
|
classForm = (Form_pg_class) GETSTRUCT(tuple);
|
|
|
|
if (classForm->reltriggers == 0) /* should not happen */
|
|
elog(ERROR, "relation \"%s\" has reltriggers = 0",
|
|
RelationGetRelationName(rel));
|
|
classForm->reltriggers--;
|
|
|
|
simple_heap_update(pgrel, &tuple->t_self, tuple);
|
|
|
|
CatalogUpdateIndexes(pgrel, tuple);
|
|
|
|
heap_freetuple(tuple);
|
|
|
|
heap_close(pgrel, RowExclusiveLock);
|
|
|
|
/* Keep lock on trigger's rel until end of xact */
|
|
heap_close(rel, NoLock);
|
|
}
|
|
|
|
/*
|
|
* renametrig - changes the name of a trigger on a relation
|
|
*
|
|
* trigger name is changed in trigger catalog.
|
|
* No record of the previous name is kept.
|
|
*
|
|
* get proper relrelation from relation catalog (if not arg)
|
|
* scan trigger catalog
|
|
* for name conflict (within rel)
|
|
* for original trigger (if not arg)
|
|
* modify tgname in trigger tuple
|
|
* update row in catalog
|
|
*/
|
|
void
|
|
renametrig(Oid relid,
|
|
const char *oldname,
|
|
const char *newname)
|
|
{
|
|
Relation targetrel;
|
|
Relation tgrel;
|
|
HeapTuple tuple;
|
|
SysScanDesc tgscan;
|
|
ScanKeyData key[2];
|
|
|
|
/*
|
|
* Grab an exclusive lock on the target table, which we will NOT release
|
|
* until end of transaction.
|
|
*/
|
|
targetrel = heap_open(relid, AccessExclusiveLock);
|
|
|
|
/*
|
|
* Scan pg_trigger twice for existing triggers on relation. We do this in
|
|
* order to ensure a trigger does not exist with newname (The unique index
|
|
* on tgrelid/tgname would complain anyway) and to ensure a trigger does
|
|
* exist with oldname.
|
|
*
|
|
* NOTE that this is cool only because we have AccessExclusiveLock on the
|
|
* relation, so the trigger set won't be changing underneath us.
|
|
*/
|
|
tgrel = heap_open(TriggerRelationId, RowExclusiveLock);
|
|
|
|
/*
|
|
* First pass -- look for name conflict
|
|
*/
|
|
ScanKeyInit(&key[0],
|
|
Anum_pg_trigger_tgrelid,
|
|
BTEqualStrategyNumber, F_OIDEQ,
|
|
ObjectIdGetDatum(relid));
|
|
ScanKeyInit(&key[1],
|
|
Anum_pg_trigger_tgname,
|
|
BTEqualStrategyNumber, F_NAMEEQ,
|
|
PointerGetDatum(newname));
|
|
tgscan = systable_beginscan(tgrel, TriggerRelidNameIndexId, true,
|
|
SnapshotNow, 2, key);
|
|
if (HeapTupleIsValid(tuple = systable_getnext(tgscan)))
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_DUPLICATE_OBJECT),
|
|
errmsg("trigger \"%s\" for relation \"%s\" already exists",
|
|
newname, RelationGetRelationName(targetrel))));
|
|
systable_endscan(tgscan);
|
|
|
|
/*
|
|
* Second pass -- look for trigger existing with oldname and update
|
|
*/
|
|
ScanKeyInit(&key[0],
|
|
Anum_pg_trigger_tgrelid,
|
|
BTEqualStrategyNumber, F_OIDEQ,
|
|
ObjectIdGetDatum(relid));
|
|
ScanKeyInit(&key[1],
|
|
Anum_pg_trigger_tgname,
|
|
BTEqualStrategyNumber, F_NAMEEQ,
|
|
PointerGetDatum(oldname));
|
|
tgscan = systable_beginscan(tgrel, TriggerRelidNameIndexId, true,
|
|
SnapshotNow, 2, key);
|
|
if (HeapTupleIsValid(tuple = systable_getnext(tgscan)))
|
|
{
|
|
/*
|
|
* Update pg_trigger tuple with new tgname.
|
|
*/
|
|
tuple = heap_copytuple(tuple); /* need a modifiable copy */
|
|
|
|
namestrcpy(&((Form_pg_trigger) GETSTRUCT(tuple))->tgname, newname);
|
|
|
|
simple_heap_update(tgrel, &tuple->t_self, tuple);
|
|
|
|
/* keep system catalog indexes current */
|
|
CatalogUpdateIndexes(tgrel, tuple);
|
|
|
|
/*
|
|
* Invalidate relation's relcache entry so that other backends (and
|
|
* this one too!) are sent SI message to make them rebuild relcache
|
|
* entries. (Ideally this should happen automatically...)
|
|
*/
|
|
CacheInvalidateRelcache(targetrel);
|
|
}
|
|
else
|
|
{
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_UNDEFINED_OBJECT),
|
|
errmsg("trigger \"%s\" for table \"%s\" does not exist",
|
|
oldname, RelationGetRelationName(targetrel))));
|
|
}
|
|
|
|
systable_endscan(tgscan);
|
|
|
|
heap_close(tgrel, RowExclusiveLock);
|
|
|
|
/*
|
|
* Close rel, but keep exclusive lock!
|
|
*/
|
|
heap_close(targetrel, NoLock);
|
|
}
|
|
|
|
|
|
/*
|
|
* EnableDisableTrigger()
|
|
*
|
|
* Called by ALTER TABLE ENABLE/DISABLE TRIGGER
|
|
* to change 'tgenabled' field for the specified trigger(s)
|
|
*
|
|
* rel: relation to process (caller must hold suitable lock on it)
|
|
* tgname: trigger to process, or NULL to scan all triggers
|
|
* enable: new value for tgenabled field
|
|
* skip_system: if true, skip "system" triggers (constraint triggers)
|
|
*
|
|
* Caller should have checked permissions for the table; here we also
|
|
* enforce that superuser privilege is required to alter the state of
|
|
* system triggers
|
|
*/
|
|
void
|
|
EnableDisableTrigger(Relation rel, const char *tgname,
|
|
char fires_when, bool skip_system)
|
|
{
|
|
Relation tgrel;
|
|
int nkeys;
|
|
ScanKeyData keys[2];
|
|
SysScanDesc tgscan;
|
|
HeapTuple tuple;
|
|
bool found;
|
|
bool changed;
|
|
|
|
/* Scan the relevant entries in pg_triggers */
|
|
tgrel = heap_open(TriggerRelationId, RowExclusiveLock);
|
|
|
|
ScanKeyInit(&keys[0],
|
|
Anum_pg_trigger_tgrelid,
|
|
BTEqualStrategyNumber, F_OIDEQ,
|
|
ObjectIdGetDatum(RelationGetRelid(rel)));
|
|
if (tgname)
|
|
{
|
|
ScanKeyInit(&keys[1],
|
|
Anum_pg_trigger_tgname,
|
|
BTEqualStrategyNumber, F_NAMEEQ,
|
|
CStringGetDatum(tgname));
|
|
nkeys = 2;
|
|
}
|
|
else
|
|
nkeys = 1;
|
|
|
|
tgscan = systable_beginscan(tgrel, TriggerRelidNameIndexId, true,
|
|
SnapshotNow, nkeys, keys);
|
|
|
|
found = changed = false;
|
|
|
|
while (HeapTupleIsValid(tuple = systable_getnext(tgscan)))
|
|
{
|
|
Form_pg_trigger oldtrig = (Form_pg_trigger) GETSTRUCT(tuple);
|
|
|
|
if (OidIsValid(oldtrig->tgconstraint))
|
|
{
|
|
/* system trigger ... ok to process? */
|
|
if (skip_system)
|
|
continue;
|
|
if (!superuser())
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
|
|
errmsg("permission denied: \"%s\" is a system trigger",
|
|
NameStr(oldtrig->tgname))));
|
|
}
|
|
|
|
found = true;
|
|
|
|
if (oldtrig->tgenabled != fires_when)
|
|
{
|
|
/* need to change this one ... make a copy to scribble on */
|
|
HeapTuple newtup = heap_copytuple(tuple);
|
|
Form_pg_trigger newtrig = (Form_pg_trigger) GETSTRUCT(newtup);
|
|
|
|
newtrig->tgenabled = fires_when;
|
|
|
|
simple_heap_update(tgrel, &newtup->t_self, newtup);
|
|
|
|
/* Keep catalog indexes current */
|
|
CatalogUpdateIndexes(tgrel, newtup);
|
|
|
|
heap_freetuple(newtup);
|
|
|
|
changed = true;
|
|
}
|
|
}
|
|
|
|
systable_endscan(tgscan);
|
|
|
|
heap_close(tgrel, RowExclusiveLock);
|
|
|
|
if (tgname && !found)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_UNDEFINED_OBJECT),
|
|
errmsg("trigger \"%s\" for table \"%s\" does not exist",
|
|
tgname, RelationGetRelationName(rel))));
|
|
|
|
/*
|
|
* If we changed anything, broadcast a SI inval message to force each
|
|
* backend (including our own!) to rebuild relation's relcache entry.
|
|
* Otherwise they will fail to apply the change promptly.
|
|
*/
|
|
if (changed)
|
|
CacheInvalidateRelcache(rel);
|
|
}
|
|
|
|
|
|
/*
|
|
* Build trigger data to attach to the given relcache entry.
|
|
*
|
|
* Note that trigger data attached to a relcache entry must be stored in
|
|
* CacheMemoryContext to ensure it survives as long as the relcache entry.
|
|
* But we should be running in a less long-lived working context. To avoid
|
|
* leaking cache memory if this routine fails partway through, we build a
|
|
* temporary TriggerDesc in working memory and then copy the completed
|
|
* structure into cache memory.
|
|
*/
|
|
void
|
|
RelationBuildTriggers(Relation relation)
|
|
{
|
|
TriggerDesc *trigdesc;
|
|
int ntrigs = relation->rd_rel->reltriggers;
|
|
Trigger *triggers;
|
|
int found = 0;
|
|
Relation tgrel;
|
|
ScanKeyData skey;
|
|
SysScanDesc tgscan;
|
|
HeapTuple htup;
|
|
MemoryContext oldContext;
|
|
|
|
Assert(ntrigs > 0); /* else I should not have been called */
|
|
|
|
triggers = (Trigger *) palloc(ntrigs * sizeof(Trigger));
|
|
|
|
/*
|
|
* Note: since we scan the triggers using TriggerRelidNameIndexId, we will
|
|
* be reading the triggers in name order, except possibly during
|
|
* emergency-recovery operations (ie, IgnoreSystemIndexes). This in turn
|
|
* ensures that triggers will be fired in name order.
|
|
*/
|
|
ScanKeyInit(&skey,
|
|
Anum_pg_trigger_tgrelid,
|
|
BTEqualStrategyNumber, F_OIDEQ,
|
|
ObjectIdGetDatum(RelationGetRelid(relation)));
|
|
|
|
tgrel = heap_open(TriggerRelationId, AccessShareLock);
|
|
tgscan = systable_beginscan(tgrel, TriggerRelidNameIndexId, true,
|
|
SnapshotNow, 1, &skey);
|
|
|
|
while (HeapTupleIsValid(htup = systable_getnext(tgscan)))
|
|
{
|
|
Form_pg_trigger pg_trigger = (Form_pg_trigger) GETSTRUCT(htup);
|
|
Trigger *build;
|
|
|
|
if (found >= ntrigs)
|
|
elog(ERROR, "too many trigger records found for relation \"%s\"",
|
|
RelationGetRelationName(relation));
|
|
build = &(triggers[found]);
|
|
|
|
build->tgoid = HeapTupleGetOid(htup);
|
|
build->tgname = DatumGetCString(DirectFunctionCall1(nameout,
|
|
NameGetDatum(&pg_trigger->tgname)));
|
|
build->tgfoid = pg_trigger->tgfoid;
|
|
build->tgtype = pg_trigger->tgtype;
|
|
build->tgenabled = pg_trigger->tgenabled;
|
|
build->tgisconstraint = pg_trigger->tgisconstraint;
|
|
build->tgconstrrelid = pg_trigger->tgconstrrelid;
|
|
build->tgconstraint = pg_trigger->tgconstraint;
|
|
build->tgdeferrable = pg_trigger->tgdeferrable;
|
|
build->tginitdeferred = pg_trigger->tginitdeferred;
|
|
build->tgnargs = pg_trigger->tgnargs;
|
|
/* tgattr is first var-width field, so OK to access directly */
|
|
build->tgnattr = pg_trigger->tgattr.dim1;
|
|
if (build->tgnattr > 0)
|
|
{
|
|
build->tgattr = (int2 *) palloc(build->tgnattr * sizeof(int2));
|
|
memcpy(build->tgattr, &(pg_trigger->tgattr.values),
|
|
build->tgnattr * sizeof(int2));
|
|
}
|
|
else
|
|
build->tgattr = NULL;
|
|
if (build->tgnargs > 0)
|
|
{
|
|
bytea *val;
|
|
bool isnull;
|
|
char *p;
|
|
int i;
|
|
|
|
val = DatumGetByteaP(fastgetattr(htup,
|
|
Anum_pg_trigger_tgargs,
|
|
tgrel->rd_att, &isnull));
|
|
if (isnull)
|
|
elog(ERROR, "tgargs is null in trigger for relation \"%s\"",
|
|
RelationGetRelationName(relation));
|
|
p = (char *) VARDATA(val);
|
|
build->tgargs = (char **) palloc(build->tgnargs * sizeof(char *));
|
|
for (i = 0; i < build->tgnargs; i++)
|
|
{
|
|
build->tgargs[i] = pstrdup(p);
|
|
p += strlen(p) + 1;
|
|
}
|
|
}
|
|
else
|
|
build->tgargs = NULL;
|
|
|
|
found++;
|
|
}
|
|
|
|
systable_endscan(tgscan);
|
|
heap_close(tgrel, AccessShareLock);
|
|
|
|
if (found != ntrigs)
|
|
elog(ERROR, "%d trigger record(s) not found for relation \"%s\"",
|
|
ntrigs - found,
|
|
RelationGetRelationName(relation));
|
|
|
|
/* Build trigdesc */
|
|
trigdesc = (TriggerDesc *) palloc0(sizeof(TriggerDesc));
|
|
trigdesc->triggers = triggers;
|
|
trigdesc->numtriggers = ntrigs;
|
|
for (found = 0; found < ntrigs; found++)
|
|
InsertTrigger(trigdesc, &(triggers[found]), found);
|
|
|
|
/* Copy completed trigdesc into cache storage */
|
|
oldContext = MemoryContextSwitchTo(CacheMemoryContext);
|
|
relation->trigdesc = CopyTriggerDesc(trigdesc);
|
|
MemoryContextSwitchTo(oldContext);
|
|
|
|
/* Release working memory */
|
|
FreeTriggerDesc(trigdesc);
|
|
}
|
|
|
|
/*
|
|
* Insert the given trigger into the appropriate index list(s) for it
|
|
*
|
|
* To simplify storage management, we allocate each index list at the max
|
|
* possible size (trigdesc->numtriggers) if it's used at all. This does
|
|
* not waste space permanently since we're only building a temporary
|
|
* trigdesc at this point.
|
|
*/
|
|
static void
|
|
InsertTrigger(TriggerDesc *trigdesc, Trigger *trigger, int indx)
|
|
{
|
|
uint16 *n;
|
|
int **t,
|
|
**tp;
|
|
|
|
if (TRIGGER_FOR_ROW(trigger->tgtype))
|
|
{
|
|
/* ROW trigger */
|
|
if (TRIGGER_FOR_BEFORE(trigger->tgtype))
|
|
{
|
|
n = trigdesc->n_before_row;
|
|
t = trigdesc->tg_before_row;
|
|
}
|
|
else
|
|
{
|
|
n = trigdesc->n_after_row;
|
|
t = trigdesc->tg_after_row;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
/* STATEMENT trigger */
|
|
if (TRIGGER_FOR_BEFORE(trigger->tgtype))
|
|
{
|
|
n = trigdesc->n_before_statement;
|
|
t = trigdesc->tg_before_statement;
|
|
}
|
|
else
|
|
{
|
|
n = trigdesc->n_after_statement;
|
|
t = trigdesc->tg_after_statement;
|
|
}
|
|
}
|
|
|
|
if (TRIGGER_FOR_INSERT(trigger->tgtype))
|
|
{
|
|
tp = &(t[TRIGGER_EVENT_INSERT]);
|
|
if (*tp == NULL)
|
|
*tp = (int *) palloc(trigdesc->numtriggers * sizeof(int));
|
|
(*tp)[n[TRIGGER_EVENT_INSERT]] = indx;
|
|
(n[TRIGGER_EVENT_INSERT])++;
|
|
}
|
|
|
|
if (TRIGGER_FOR_DELETE(trigger->tgtype))
|
|
{
|
|
tp = &(t[TRIGGER_EVENT_DELETE]);
|
|
if (*tp == NULL)
|
|
*tp = (int *) palloc(trigdesc->numtriggers * sizeof(int));
|
|
(*tp)[n[TRIGGER_EVENT_DELETE]] = indx;
|
|
(n[TRIGGER_EVENT_DELETE])++;
|
|
}
|
|
|
|
if (TRIGGER_FOR_UPDATE(trigger->tgtype))
|
|
{
|
|
tp = &(t[TRIGGER_EVENT_UPDATE]);
|
|
if (*tp == NULL)
|
|
*tp = (int *) palloc(trigdesc->numtriggers * sizeof(int));
|
|
(*tp)[n[TRIGGER_EVENT_UPDATE]] = indx;
|
|
(n[TRIGGER_EVENT_UPDATE])++;
|
|
}
|
|
|
|
if (TRIGGER_FOR_TRUNCATE(trigger->tgtype))
|
|
{
|
|
tp = &(t[TRIGGER_EVENT_TRUNCATE]);
|
|
if (*tp == NULL)
|
|
*tp = (int *) palloc(trigdesc->numtriggers * sizeof(int));
|
|
(*tp)[n[TRIGGER_EVENT_TRUNCATE]] = indx;
|
|
(n[TRIGGER_EVENT_TRUNCATE])++;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Copy a TriggerDesc data structure.
|
|
*
|
|
* The copy is allocated in the current memory context.
|
|
*/
|
|
TriggerDesc *
|
|
CopyTriggerDesc(TriggerDesc *trigdesc)
|
|
{
|
|
TriggerDesc *newdesc;
|
|
uint16 *n;
|
|
int **t,
|
|
*tnew;
|
|
Trigger *trigger;
|
|
int i;
|
|
|
|
if (trigdesc == NULL || trigdesc->numtriggers <= 0)
|
|
return NULL;
|
|
|
|
newdesc = (TriggerDesc *) palloc(sizeof(TriggerDesc));
|
|
memcpy(newdesc, trigdesc, sizeof(TriggerDesc));
|
|
|
|
trigger = (Trigger *) palloc(trigdesc->numtriggers * sizeof(Trigger));
|
|
memcpy(trigger, trigdesc->triggers,
|
|
trigdesc->numtriggers * sizeof(Trigger));
|
|
newdesc->triggers = trigger;
|
|
|
|
for (i = 0; i < trigdesc->numtriggers; i++)
|
|
{
|
|
trigger->tgname = pstrdup(trigger->tgname);
|
|
if (trigger->tgnattr > 0)
|
|
{
|
|
int2 *newattr;
|
|
|
|
newattr = (int2 *) palloc(trigger->tgnattr * sizeof(int2));
|
|
memcpy(newattr, trigger->tgattr,
|
|
trigger->tgnattr * sizeof(int2));
|
|
trigger->tgattr = newattr;
|
|
}
|
|
if (trigger->tgnargs > 0)
|
|
{
|
|
char **newargs;
|
|
int16 j;
|
|
|
|
newargs = (char **) palloc(trigger->tgnargs * sizeof(char *));
|
|
for (j = 0; j < trigger->tgnargs; j++)
|
|
newargs[j] = pstrdup(trigger->tgargs[j]);
|
|
trigger->tgargs = newargs;
|
|
}
|
|
trigger++;
|
|
}
|
|
|
|
n = newdesc->n_before_statement;
|
|
t = newdesc->tg_before_statement;
|
|
for (i = 0; i < TRIGGER_NUM_EVENT_CLASSES; i++)
|
|
{
|
|
if (n[i] > 0)
|
|
{
|
|
tnew = (int *) palloc(n[i] * sizeof(int));
|
|
memcpy(tnew, t[i], n[i] * sizeof(int));
|
|
t[i] = tnew;
|
|
}
|
|
else
|
|
t[i] = NULL;
|
|
}
|
|
n = newdesc->n_before_row;
|
|
t = newdesc->tg_before_row;
|
|
for (i = 0; i < TRIGGER_NUM_EVENT_CLASSES; i++)
|
|
{
|
|
if (n[i] > 0)
|
|
{
|
|
tnew = (int *) palloc(n[i] * sizeof(int));
|
|
memcpy(tnew, t[i], n[i] * sizeof(int));
|
|
t[i] = tnew;
|
|
}
|
|
else
|
|
t[i] = NULL;
|
|
}
|
|
n = newdesc->n_after_row;
|
|
t = newdesc->tg_after_row;
|
|
for (i = 0; i < TRIGGER_NUM_EVENT_CLASSES; i++)
|
|
{
|
|
if (n[i] > 0)
|
|
{
|
|
tnew = (int *) palloc(n[i] * sizeof(int));
|
|
memcpy(tnew, t[i], n[i] * sizeof(int));
|
|
t[i] = tnew;
|
|
}
|
|
else
|
|
t[i] = NULL;
|
|
}
|
|
n = newdesc->n_after_statement;
|
|
t = newdesc->tg_after_statement;
|
|
for (i = 0; i < TRIGGER_NUM_EVENT_CLASSES; i++)
|
|
{
|
|
if (n[i] > 0)
|
|
{
|
|
tnew = (int *) palloc(n[i] * sizeof(int));
|
|
memcpy(tnew, t[i], n[i] * sizeof(int));
|
|
t[i] = tnew;
|
|
}
|
|
else
|
|
t[i] = NULL;
|
|
}
|
|
|
|
return newdesc;
|
|
}
|
|
|
|
/*
|
|
* Free a TriggerDesc data structure.
|
|
*/
|
|
void
|
|
FreeTriggerDesc(TriggerDesc *trigdesc)
|
|
{
|
|
int **t;
|
|
Trigger *trigger;
|
|
int i;
|
|
|
|
if (trigdesc == NULL)
|
|
return;
|
|
|
|
t = trigdesc->tg_before_statement;
|
|
for (i = 0; i < TRIGGER_NUM_EVENT_CLASSES; i++)
|
|
if (t[i] != NULL)
|
|
pfree(t[i]);
|
|
t = trigdesc->tg_before_row;
|
|
for (i = 0; i < TRIGGER_NUM_EVENT_CLASSES; i++)
|
|
if (t[i] != NULL)
|
|
pfree(t[i]);
|
|
t = trigdesc->tg_after_row;
|
|
for (i = 0; i < TRIGGER_NUM_EVENT_CLASSES; i++)
|
|
if (t[i] != NULL)
|
|
pfree(t[i]);
|
|
t = trigdesc->tg_after_statement;
|
|
for (i = 0; i < TRIGGER_NUM_EVENT_CLASSES; i++)
|
|
if (t[i] != NULL)
|
|
pfree(t[i]);
|
|
|
|
trigger = trigdesc->triggers;
|
|
for (i = 0; i < trigdesc->numtriggers; i++)
|
|
{
|
|
pfree(trigger->tgname);
|
|
if (trigger->tgnattr > 0)
|
|
pfree(trigger->tgattr);
|
|
if (trigger->tgnargs > 0)
|
|
{
|
|
while (--(trigger->tgnargs) >= 0)
|
|
pfree(trigger->tgargs[trigger->tgnargs]);
|
|
pfree(trigger->tgargs);
|
|
}
|
|
trigger++;
|
|
}
|
|
pfree(trigdesc->triggers);
|
|
pfree(trigdesc);
|
|
}
|
|
|
|
/*
|
|
* Compare two TriggerDesc structures for logical equality.
|
|
*/
|
|
#ifdef NOT_USED
|
|
bool
|
|
equalTriggerDescs(TriggerDesc *trigdesc1, TriggerDesc *trigdesc2)
|
|
{
|
|
int i,
|
|
j;
|
|
|
|
/*
|
|
* We need not examine the "index" data, just the trigger array itself; if
|
|
* we have the same triggers with the same types, the derived index data
|
|
* should match.
|
|
*
|
|
* As of 7.3 we assume trigger set ordering is significant in the
|
|
* comparison; so we just compare corresponding slots of the two sets.
|
|
*/
|
|
if (trigdesc1 != NULL)
|
|
{
|
|
if (trigdesc2 == NULL)
|
|
return false;
|
|
if (trigdesc1->numtriggers != trigdesc2->numtriggers)
|
|
return false;
|
|
for (i = 0; i < trigdesc1->numtriggers; i++)
|
|
{
|
|
Trigger *trig1 = trigdesc1->triggers + i;
|
|
Trigger *trig2 = trigdesc2->triggers + i;
|
|
|
|
if (trig1->tgoid != trig2->tgoid)
|
|
return false;
|
|
if (strcmp(trig1->tgname, trig2->tgname) != 0)
|
|
return false;
|
|
if (trig1->tgfoid != trig2->tgfoid)
|
|
return false;
|
|
if (trig1->tgtype != trig2->tgtype)
|
|
return false;
|
|
if (trig1->tgenabled != trig2->tgenabled)
|
|
return false;
|
|
if (trig1->tgisconstraint != trig2->tgisconstraint)
|
|
return false;
|
|
if (trig1->tgconstrrelid != trig2->tgconstrrelid)
|
|
return false;
|
|
if (trig1->tgconstraint != trig2->tgconstraint)
|
|
return false;
|
|
if (trig1->tgdeferrable != trig2->tgdeferrable)
|
|
return false;
|
|
if (trig1->tginitdeferred != trig2->tginitdeferred)
|
|
return false;
|
|
if (trig1->tgnargs != trig2->tgnargs)
|
|
return false;
|
|
if (trig1->tgnattr != trig2->tgnattr)
|
|
return false;
|
|
if (trig1->tgnattr > 0 &&
|
|
memcmp(trig1->tgattr, trig2->tgattr,
|
|
trig1->tgnattr * sizeof(int2)) != 0)
|
|
return false;
|
|
for (j = 0; j < trig1->tgnargs; j++)
|
|
if (strcmp(trig1->tgargs[j], trig2->tgargs[j]) != 0)
|
|
return false;
|
|
}
|
|
}
|
|
else if (trigdesc2 != NULL)
|
|
return false;
|
|
return true;
|
|
}
|
|
#endif /* NOT_USED */
|
|
|
|
/*
|
|
* Call a trigger function.
|
|
*
|
|
* trigdata: trigger descriptor.
|
|
* tgindx: trigger's index in finfo and instr arrays.
|
|
* finfo: array of cached trigger function call information.
|
|
* instr: optional array of EXPLAIN ANALYZE instrumentation state.
|
|
* per_tuple_context: memory context to execute the function in.
|
|
*
|
|
* Returns the tuple (or NULL) as returned by the function.
|
|
*/
|
|
static HeapTuple
|
|
ExecCallTriggerFunc(TriggerData *trigdata,
|
|
int tgindx,
|
|
FmgrInfo *finfo,
|
|
Instrumentation *instr,
|
|
MemoryContext per_tuple_context)
|
|
{
|
|
FunctionCallInfoData fcinfo;
|
|
Datum result;
|
|
MemoryContext oldContext;
|
|
|
|
finfo += tgindx;
|
|
|
|
/*
|
|
* We cache fmgr lookup info, to avoid making the lookup again on each
|
|
* call.
|
|
*/
|
|
if (finfo->fn_oid == InvalidOid)
|
|
fmgr_info(trigdata->tg_trigger->tgfoid, finfo);
|
|
|
|
Assert(finfo->fn_oid == trigdata->tg_trigger->tgfoid);
|
|
|
|
/*
|
|
* If doing EXPLAIN ANALYZE, start charging time to this trigger.
|
|
*/
|
|
if (instr)
|
|
InstrStartNode(instr + tgindx);
|
|
|
|
/*
|
|
* Do the function evaluation in the per-tuple memory context, so that
|
|
* leaked memory will be reclaimed once per tuple. Note in particular that
|
|
* any new tuple created by the trigger function will live till the end of
|
|
* the tuple cycle.
|
|
*/
|
|
oldContext = MemoryContextSwitchTo(per_tuple_context);
|
|
|
|
/*
|
|
* Call the function, passing no arguments but setting a context.
|
|
*/
|
|
InitFunctionCallInfoData(fcinfo, finfo, 0, (Node *) trigdata, NULL);
|
|
|
|
result = FunctionCallInvoke(&fcinfo);
|
|
|
|
MemoryContextSwitchTo(oldContext);
|
|
|
|
/*
|
|
* Trigger protocol allows function to return a null pointer, but NOT to
|
|
* set the isnull result flag.
|
|
*/
|
|
if (fcinfo.isnull)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_E_R_I_E_TRIGGER_PROTOCOL_VIOLATED),
|
|
errmsg("trigger function %u returned null value",
|
|
fcinfo.flinfo->fn_oid)));
|
|
|
|
/*
|
|
* If doing EXPLAIN ANALYZE, stop charging time to this trigger, and count
|
|
* one "tuple returned" (really the number of firings).
|
|
*/
|
|
if (instr)
|
|
InstrStopNode(instr + tgindx, 1);
|
|
|
|
return (HeapTuple) DatumGetPointer(result);
|
|
}
|
|
|
|
void
|
|
ExecBSInsertTriggers(EState *estate, ResultRelInfo *relinfo)
|
|
{
|
|
TriggerDesc *trigdesc;
|
|
int ntrigs;
|
|
int *tgindx;
|
|
int i;
|
|
TriggerData LocTriggerData;
|
|
|
|
trigdesc = relinfo->ri_TrigDesc;
|
|
|
|
if (trigdesc == NULL)
|
|
return;
|
|
|
|
ntrigs = trigdesc->n_before_statement[TRIGGER_EVENT_INSERT];
|
|
tgindx = trigdesc->tg_before_statement[TRIGGER_EVENT_INSERT];
|
|
|
|
if (ntrigs == 0)
|
|
return;
|
|
|
|
LocTriggerData.type = T_TriggerData;
|
|
LocTriggerData.tg_event = TRIGGER_EVENT_INSERT |
|
|
TRIGGER_EVENT_BEFORE;
|
|
LocTriggerData.tg_relation = relinfo->ri_RelationDesc;
|
|
LocTriggerData.tg_trigtuple = NULL;
|
|
LocTriggerData.tg_newtuple = NULL;
|
|
LocTriggerData.tg_trigtuplebuf = InvalidBuffer;
|
|
LocTriggerData.tg_newtuplebuf = InvalidBuffer;
|
|
for (i = 0; i < ntrigs; i++)
|
|
{
|
|
Trigger *trigger = &trigdesc->triggers[tgindx[i]];
|
|
HeapTuple newtuple;
|
|
|
|
if (SessionReplicationRole == SESSION_REPLICATION_ROLE_REPLICA)
|
|
{
|
|
if (trigger->tgenabled == TRIGGER_FIRES_ON_ORIGIN ||
|
|
trigger->tgenabled == TRIGGER_DISABLED)
|
|
continue;
|
|
}
|
|
else /* ORIGIN or LOCAL role */
|
|
{
|
|
if (trigger->tgenabled == TRIGGER_FIRES_ON_REPLICA ||
|
|
trigger->tgenabled == TRIGGER_DISABLED)
|
|
continue;
|
|
}
|
|
LocTriggerData.tg_trigger = trigger;
|
|
newtuple = ExecCallTriggerFunc(&LocTriggerData,
|
|
tgindx[i],
|
|
relinfo->ri_TrigFunctions,
|
|
relinfo->ri_TrigInstrument,
|
|
GetPerTupleMemoryContext(estate));
|
|
|
|
if (newtuple)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_E_R_I_E_TRIGGER_PROTOCOL_VIOLATED),
|
|
errmsg("BEFORE STATEMENT trigger cannot return a value")));
|
|
}
|
|
}
|
|
|
|
void
|
|
ExecASInsertTriggers(EState *estate, ResultRelInfo *relinfo)
|
|
{
|
|
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
|
|
|
|
if (trigdesc && trigdesc->n_after_statement[TRIGGER_EVENT_INSERT] > 0)
|
|
AfterTriggerSaveEvent(relinfo, TRIGGER_EVENT_INSERT,
|
|
false, NULL, NULL);
|
|
}
|
|
|
|
HeapTuple
|
|
ExecBRInsertTriggers(EState *estate, ResultRelInfo *relinfo,
|
|
HeapTuple trigtuple)
|
|
{
|
|
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
|
|
int ntrigs = trigdesc->n_before_row[TRIGGER_EVENT_INSERT];
|
|
int *tgindx = trigdesc->tg_before_row[TRIGGER_EVENT_INSERT];
|
|
HeapTuple newtuple = trigtuple;
|
|
HeapTuple oldtuple;
|
|
TriggerData LocTriggerData;
|
|
int i;
|
|
|
|
LocTriggerData.type = T_TriggerData;
|
|
LocTriggerData.tg_event = TRIGGER_EVENT_INSERT |
|
|
TRIGGER_EVENT_ROW |
|
|
TRIGGER_EVENT_BEFORE;
|
|
LocTriggerData.tg_relation = relinfo->ri_RelationDesc;
|
|
LocTriggerData.tg_newtuple = NULL;
|
|
LocTriggerData.tg_newtuplebuf = InvalidBuffer;
|
|
for (i = 0; i < ntrigs; i++)
|
|
{
|
|
Trigger *trigger = &trigdesc->triggers[tgindx[i]];
|
|
|
|
if (SessionReplicationRole == SESSION_REPLICATION_ROLE_REPLICA)
|
|
{
|
|
if (trigger->tgenabled == TRIGGER_FIRES_ON_ORIGIN ||
|
|
trigger->tgenabled == TRIGGER_DISABLED)
|
|
continue;
|
|
}
|
|
else /* ORIGIN or LOCAL role */
|
|
{
|
|
if (trigger->tgenabled == TRIGGER_FIRES_ON_REPLICA ||
|
|
trigger->tgenabled == TRIGGER_DISABLED)
|
|
continue;
|
|
}
|
|
LocTriggerData.tg_trigtuple = oldtuple = newtuple;
|
|
LocTriggerData.tg_trigtuplebuf = InvalidBuffer;
|
|
LocTriggerData.tg_trigger = trigger;
|
|
newtuple = ExecCallTriggerFunc(&LocTriggerData,
|
|
tgindx[i],
|
|
relinfo->ri_TrigFunctions,
|
|
relinfo->ri_TrigInstrument,
|
|
GetPerTupleMemoryContext(estate));
|
|
if (oldtuple != newtuple && oldtuple != trigtuple)
|
|
heap_freetuple(oldtuple);
|
|
if (newtuple == NULL)
|
|
break;
|
|
}
|
|
return newtuple;
|
|
}
|
|
|
|
void
|
|
ExecARInsertTriggers(EState *estate, ResultRelInfo *relinfo,
|
|
HeapTuple trigtuple)
|
|
{
|
|
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
|
|
|
|
if (trigdesc && trigdesc->n_after_row[TRIGGER_EVENT_INSERT] > 0)
|
|
AfterTriggerSaveEvent(relinfo, TRIGGER_EVENT_INSERT,
|
|
true, NULL, trigtuple);
|
|
}
|
|
|
|
void
|
|
ExecBSDeleteTriggers(EState *estate, ResultRelInfo *relinfo)
|
|
{
|
|
TriggerDesc *trigdesc;
|
|
int ntrigs;
|
|
int *tgindx;
|
|
int i;
|
|
TriggerData LocTriggerData;
|
|
|
|
trigdesc = relinfo->ri_TrigDesc;
|
|
|
|
if (trigdesc == NULL)
|
|
return;
|
|
|
|
ntrigs = trigdesc->n_before_statement[TRIGGER_EVENT_DELETE];
|
|
tgindx = trigdesc->tg_before_statement[TRIGGER_EVENT_DELETE];
|
|
|
|
if (ntrigs == 0)
|
|
return;
|
|
|
|
LocTriggerData.type = T_TriggerData;
|
|
LocTriggerData.tg_event = TRIGGER_EVENT_DELETE |
|
|
TRIGGER_EVENT_BEFORE;
|
|
LocTriggerData.tg_relation = relinfo->ri_RelationDesc;
|
|
LocTriggerData.tg_trigtuple = NULL;
|
|
LocTriggerData.tg_newtuple = NULL;
|
|
LocTriggerData.tg_trigtuplebuf = InvalidBuffer;
|
|
LocTriggerData.tg_newtuplebuf = InvalidBuffer;
|
|
for (i = 0; i < ntrigs; i++)
|
|
{
|
|
Trigger *trigger = &trigdesc->triggers[tgindx[i]];
|
|
HeapTuple newtuple;
|
|
|
|
if (SessionReplicationRole == SESSION_REPLICATION_ROLE_REPLICA)
|
|
{
|
|
if (trigger->tgenabled == TRIGGER_FIRES_ON_ORIGIN ||
|
|
trigger->tgenabled == TRIGGER_DISABLED)
|
|
continue;
|
|
}
|
|
else /* ORIGIN or LOCAL role */
|
|
{
|
|
if (trigger->tgenabled == TRIGGER_FIRES_ON_REPLICA ||
|
|
trigger->tgenabled == TRIGGER_DISABLED)
|
|
continue;
|
|
}
|
|
LocTriggerData.tg_trigger = trigger;
|
|
newtuple = ExecCallTriggerFunc(&LocTriggerData,
|
|
tgindx[i],
|
|
relinfo->ri_TrigFunctions,
|
|
relinfo->ri_TrigInstrument,
|
|
GetPerTupleMemoryContext(estate));
|
|
|
|
if (newtuple)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_E_R_I_E_TRIGGER_PROTOCOL_VIOLATED),
|
|
errmsg("BEFORE STATEMENT trigger cannot return a value")));
|
|
}
|
|
}
|
|
|
|
void
|
|
ExecASDeleteTriggers(EState *estate, ResultRelInfo *relinfo)
|
|
{
|
|
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
|
|
|
|
if (trigdesc && trigdesc->n_after_statement[TRIGGER_EVENT_DELETE] > 0)
|
|
AfterTriggerSaveEvent(relinfo, TRIGGER_EVENT_DELETE,
|
|
false, NULL, NULL);
|
|
}
|
|
|
|
bool
|
|
ExecBRDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
|
|
ItemPointer tupleid)
|
|
{
|
|
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
|
|
int ntrigs = trigdesc->n_before_row[TRIGGER_EVENT_DELETE];
|
|
int *tgindx = trigdesc->tg_before_row[TRIGGER_EVENT_DELETE];
|
|
bool result = true;
|
|
TriggerData LocTriggerData;
|
|
HeapTuple trigtuple;
|
|
HeapTuple newtuple;
|
|
TupleTableSlot *newSlot;
|
|
int i;
|
|
|
|
trigtuple = GetTupleForTrigger(estate, relinfo, tupleid, &newSlot);
|
|
if (trigtuple == NULL)
|
|
return false;
|
|
|
|
LocTriggerData.type = T_TriggerData;
|
|
LocTriggerData.tg_event = TRIGGER_EVENT_DELETE |
|
|
TRIGGER_EVENT_ROW |
|
|
TRIGGER_EVENT_BEFORE;
|
|
LocTriggerData.tg_relation = relinfo->ri_RelationDesc;
|
|
LocTriggerData.tg_newtuple = NULL;
|
|
LocTriggerData.tg_newtuplebuf = InvalidBuffer;
|
|
for (i = 0; i < ntrigs; i++)
|
|
{
|
|
Trigger *trigger = &trigdesc->triggers[tgindx[i]];
|
|
|
|
if (SessionReplicationRole == SESSION_REPLICATION_ROLE_REPLICA)
|
|
{
|
|
if (trigger->tgenabled == TRIGGER_FIRES_ON_ORIGIN ||
|
|
trigger->tgenabled == TRIGGER_DISABLED)
|
|
continue;
|
|
}
|
|
else /* ORIGIN or LOCAL role */
|
|
{
|
|
if (trigger->tgenabled == TRIGGER_FIRES_ON_REPLICA ||
|
|
trigger->tgenabled == TRIGGER_DISABLED)
|
|
continue;
|
|
}
|
|
LocTriggerData.tg_trigtuple = trigtuple;
|
|
LocTriggerData.tg_trigtuplebuf = InvalidBuffer;
|
|
LocTriggerData.tg_trigger = trigger;
|
|
newtuple = ExecCallTriggerFunc(&LocTriggerData,
|
|
tgindx[i],
|
|
relinfo->ri_TrigFunctions,
|
|
relinfo->ri_TrigInstrument,
|
|
GetPerTupleMemoryContext(estate));
|
|
if (newtuple == NULL)
|
|
{
|
|
result = false; /* tell caller to suppress delete */
|
|
break;
|
|
}
|
|
if (newtuple != trigtuple)
|
|
heap_freetuple(newtuple);
|
|
}
|
|
heap_freetuple(trigtuple);
|
|
|
|
return result;
|
|
}
|
|
|
|
void
|
|
ExecARDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
|
|
ItemPointer tupleid)
|
|
{
|
|
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
|
|
|
|
if (trigdesc && trigdesc->n_after_row[TRIGGER_EVENT_DELETE] > 0)
|
|
{
|
|
HeapTuple trigtuple = GetTupleForTrigger(estate, relinfo,
|
|
tupleid, NULL);
|
|
|
|
AfterTriggerSaveEvent(relinfo, TRIGGER_EVENT_DELETE,
|
|
true, trigtuple, NULL);
|
|
heap_freetuple(trigtuple);
|
|
}
|
|
}
|
|
|
|
void
|
|
ExecBSUpdateTriggers(EState *estate, ResultRelInfo *relinfo)
|
|
{
|
|
TriggerDesc *trigdesc;
|
|
int ntrigs;
|
|
int *tgindx;
|
|
int i;
|
|
TriggerData LocTriggerData;
|
|
|
|
trigdesc = relinfo->ri_TrigDesc;
|
|
|
|
if (trigdesc == NULL)
|
|
return;
|
|
|
|
ntrigs = trigdesc->n_before_statement[TRIGGER_EVENT_UPDATE];
|
|
tgindx = trigdesc->tg_before_statement[TRIGGER_EVENT_UPDATE];
|
|
|
|
if (ntrigs == 0)
|
|
return;
|
|
|
|
LocTriggerData.type = T_TriggerData;
|
|
LocTriggerData.tg_event = TRIGGER_EVENT_UPDATE |
|
|
TRIGGER_EVENT_BEFORE;
|
|
LocTriggerData.tg_relation = relinfo->ri_RelationDesc;
|
|
LocTriggerData.tg_trigtuple = NULL;
|
|
LocTriggerData.tg_newtuple = NULL;
|
|
LocTriggerData.tg_trigtuplebuf = InvalidBuffer;
|
|
LocTriggerData.tg_newtuplebuf = InvalidBuffer;
|
|
for (i = 0; i < ntrigs; i++)
|
|
{
|
|
Trigger *trigger = &trigdesc->triggers[tgindx[i]];
|
|
HeapTuple newtuple;
|
|
|
|
if (SessionReplicationRole == SESSION_REPLICATION_ROLE_REPLICA)
|
|
{
|
|
if (trigger->tgenabled == TRIGGER_FIRES_ON_ORIGIN ||
|
|
trigger->tgenabled == TRIGGER_DISABLED)
|
|
continue;
|
|
}
|
|
else /* ORIGIN or LOCAL role */
|
|
{
|
|
if (trigger->tgenabled == TRIGGER_FIRES_ON_REPLICA ||
|
|
trigger->tgenabled == TRIGGER_DISABLED)
|
|
continue;
|
|
}
|
|
LocTriggerData.tg_trigger = trigger;
|
|
newtuple = ExecCallTriggerFunc(&LocTriggerData,
|
|
tgindx[i],
|
|
relinfo->ri_TrigFunctions,
|
|
relinfo->ri_TrigInstrument,
|
|
GetPerTupleMemoryContext(estate));
|
|
|
|
if (newtuple)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_E_R_I_E_TRIGGER_PROTOCOL_VIOLATED),
|
|
errmsg("BEFORE STATEMENT trigger cannot return a value")));
|
|
}
|
|
}
|
|
|
|
void
|
|
ExecASUpdateTriggers(EState *estate, ResultRelInfo *relinfo)
|
|
{
|
|
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
|
|
|
|
if (trigdesc && trigdesc->n_after_statement[TRIGGER_EVENT_UPDATE] > 0)
|
|
AfterTriggerSaveEvent(relinfo, TRIGGER_EVENT_UPDATE,
|
|
false, NULL, NULL);
|
|
}
|
|
|
|
HeapTuple
|
|
ExecBRUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
|
|
ItemPointer tupleid, HeapTuple newtuple)
|
|
{
|
|
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
|
|
int ntrigs = trigdesc->n_before_row[TRIGGER_EVENT_UPDATE];
|
|
int *tgindx = trigdesc->tg_before_row[TRIGGER_EVENT_UPDATE];
|
|
TriggerData LocTriggerData;
|
|
HeapTuple trigtuple;
|
|
HeapTuple oldtuple;
|
|
HeapTuple intuple = newtuple;
|
|
TupleTableSlot *newSlot;
|
|
int i;
|
|
|
|
trigtuple = GetTupleForTrigger(estate, relinfo, tupleid, &newSlot);
|
|
if (trigtuple == NULL)
|
|
return NULL;
|
|
|
|
/*
|
|
* In READ COMMITTED isolation level it's possible that newtuple was
|
|
* changed due to concurrent update.
|
|
*/
|
|
if (newSlot != NULL)
|
|
intuple = newtuple = ExecRemoveJunk(estate->es_junkFilter, newSlot);
|
|
|
|
LocTriggerData.type = T_TriggerData;
|
|
LocTriggerData.tg_event = TRIGGER_EVENT_UPDATE |
|
|
TRIGGER_EVENT_ROW |
|
|
TRIGGER_EVENT_BEFORE;
|
|
LocTriggerData.tg_relation = relinfo->ri_RelationDesc;
|
|
for (i = 0; i < ntrigs; i++)
|
|
{
|
|
Trigger *trigger = &trigdesc->triggers[tgindx[i]];
|
|
|
|
if (SessionReplicationRole == SESSION_REPLICATION_ROLE_REPLICA)
|
|
{
|
|
if (trigger->tgenabled == TRIGGER_FIRES_ON_ORIGIN ||
|
|
trigger->tgenabled == TRIGGER_DISABLED)
|
|
continue;
|
|
}
|
|
else /* ORIGIN or LOCAL role */
|
|
{
|
|
if (trigger->tgenabled == TRIGGER_FIRES_ON_REPLICA ||
|
|
trigger->tgenabled == TRIGGER_DISABLED)
|
|
continue;
|
|
}
|
|
LocTriggerData.tg_trigtuple = trigtuple;
|
|
LocTriggerData.tg_newtuple = oldtuple = newtuple;
|
|
LocTriggerData.tg_trigtuplebuf = InvalidBuffer;
|
|
LocTriggerData.tg_newtuplebuf = InvalidBuffer;
|
|
LocTriggerData.tg_trigger = trigger;
|
|
newtuple = ExecCallTriggerFunc(&LocTriggerData,
|
|
tgindx[i],
|
|
relinfo->ri_TrigFunctions,
|
|
relinfo->ri_TrigInstrument,
|
|
GetPerTupleMemoryContext(estate));
|
|
if (oldtuple != newtuple && oldtuple != intuple)
|
|
heap_freetuple(oldtuple);
|
|
if (newtuple == NULL)
|
|
break;
|
|
}
|
|
heap_freetuple(trigtuple);
|
|
return newtuple;
|
|
}
|
|
|
|
void
|
|
ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
|
|
ItemPointer tupleid, HeapTuple newtuple)
|
|
{
|
|
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
|
|
|
|
if (trigdesc && trigdesc->n_after_row[TRIGGER_EVENT_UPDATE] > 0)
|
|
{
|
|
HeapTuple trigtuple = GetTupleForTrigger(estate, relinfo,
|
|
tupleid, NULL);
|
|
|
|
AfterTriggerSaveEvent(relinfo, TRIGGER_EVENT_UPDATE,
|
|
true, trigtuple, newtuple);
|
|
heap_freetuple(trigtuple);
|
|
}
|
|
}
|
|
|
|
void
|
|
ExecBSTruncateTriggers(EState *estate, ResultRelInfo *relinfo)
|
|
{
|
|
TriggerDesc *trigdesc;
|
|
int ntrigs;
|
|
int *tgindx;
|
|
int i;
|
|
TriggerData LocTriggerData;
|
|
|
|
trigdesc = relinfo->ri_TrigDesc;
|
|
|
|
if (trigdesc == NULL)
|
|
return;
|
|
|
|
ntrigs = trigdesc->n_before_statement[TRIGGER_EVENT_TRUNCATE];
|
|
tgindx = trigdesc->tg_before_statement[TRIGGER_EVENT_TRUNCATE];
|
|
|
|
if (ntrigs == 0)
|
|
return;
|
|
|
|
LocTriggerData.type = T_TriggerData;
|
|
LocTriggerData.tg_event = TRIGGER_EVENT_TRUNCATE |
|
|
TRIGGER_EVENT_BEFORE;
|
|
LocTriggerData.tg_relation = relinfo->ri_RelationDesc;
|
|
LocTriggerData.tg_trigtuple = NULL;
|
|
LocTriggerData.tg_newtuple = NULL;
|
|
LocTriggerData.tg_trigtuplebuf = InvalidBuffer;
|
|
LocTriggerData.tg_newtuplebuf = InvalidBuffer;
|
|
for (i = 0; i < ntrigs; i++)
|
|
{
|
|
Trigger *trigger = &trigdesc->triggers[tgindx[i]];
|
|
HeapTuple newtuple;
|
|
|
|
if (SessionReplicationRole == SESSION_REPLICATION_ROLE_REPLICA)
|
|
{
|
|
if (trigger->tgenabled == TRIGGER_FIRES_ON_ORIGIN ||
|
|
trigger->tgenabled == TRIGGER_DISABLED)
|
|
continue;
|
|
}
|
|
else /* ORIGIN or LOCAL role */
|
|
{
|
|
if (trigger->tgenabled == TRIGGER_FIRES_ON_REPLICA ||
|
|
trigger->tgenabled == TRIGGER_DISABLED)
|
|
continue;
|
|
}
|
|
LocTriggerData.tg_trigger = trigger;
|
|
newtuple = ExecCallTriggerFunc(&LocTriggerData,
|
|
tgindx[i],
|
|
relinfo->ri_TrigFunctions,
|
|
relinfo->ri_TrigInstrument,
|
|
GetPerTupleMemoryContext(estate));
|
|
|
|
if (newtuple)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_E_R_I_E_TRIGGER_PROTOCOL_VIOLATED),
|
|
errmsg("BEFORE STATEMENT trigger cannot return a value")));
|
|
}
|
|
}
|
|
|
|
void
|
|
ExecASTruncateTriggers(EState *estate, ResultRelInfo *relinfo)
|
|
{
|
|
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
|
|
|
|
if (trigdesc && trigdesc->n_after_statement[TRIGGER_EVENT_TRUNCATE] > 0)
|
|
AfterTriggerSaveEvent(relinfo, TRIGGER_EVENT_TRUNCATE,
|
|
false, NULL, NULL);
|
|
}
|
|
|
|
|
|
static HeapTuple
|
|
GetTupleForTrigger(EState *estate, ResultRelInfo *relinfo,
|
|
ItemPointer tid,
|
|
TupleTableSlot **newSlot)
|
|
{
|
|
Relation relation = relinfo->ri_RelationDesc;
|
|
HeapTupleData tuple;
|
|
HeapTuple result;
|
|
Buffer buffer;
|
|
|
|
if (newSlot != NULL)
|
|
{
|
|
HTSU_Result test;
|
|
ItemPointerData update_ctid;
|
|
TransactionId update_xmax;
|
|
|
|
*newSlot = NULL;
|
|
|
|
/*
|
|
* lock tuple for update
|
|
*/
|
|
ltrmark:;
|
|
tuple.t_self = *tid;
|
|
test = heap_lock_tuple(relation, &tuple, &buffer,
|
|
&update_ctid, &update_xmax,
|
|
estate->es_output_cid,
|
|
LockTupleExclusive, false);
|
|
switch (test)
|
|
{
|
|
case HeapTupleSelfUpdated:
|
|
/* treat it as deleted; do not process */
|
|
ReleaseBuffer(buffer);
|
|
return NULL;
|
|
|
|
case HeapTupleMayBeUpdated:
|
|
break;
|
|
|
|
case HeapTupleUpdated:
|
|
ReleaseBuffer(buffer);
|
|
if (IsXactIsoLevelSerializable)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_T_R_SERIALIZATION_FAILURE),
|
|
errmsg("could not serialize access due to concurrent update")));
|
|
else if (!ItemPointerEquals(&update_ctid, &tuple.t_self))
|
|
{
|
|
/* it was updated, so look at the updated version */
|
|
TupleTableSlot *epqslot;
|
|
|
|
epqslot = EvalPlanQual(estate,
|
|
relinfo->ri_RangeTableIndex,
|
|
&update_ctid,
|
|
update_xmax);
|
|
if (!TupIsNull(epqslot))
|
|
{
|
|
*tid = update_ctid;
|
|
*newSlot = epqslot;
|
|
goto ltrmark;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* if tuple was deleted or PlanQual failed for updated tuple -
|
|
* we have not process this tuple!
|
|
*/
|
|
return NULL;
|
|
|
|
default:
|
|
ReleaseBuffer(buffer);
|
|
elog(ERROR, "unrecognized heap_lock_tuple status: %u", test);
|
|
return NULL; /* keep compiler quiet */
|
|
}
|
|
}
|
|
else
|
|
{
|
|
PageHeader dp;
|
|
ItemId lp;
|
|
|
|
buffer = ReadBuffer(relation, ItemPointerGetBlockNumber(tid));
|
|
|
|
dp = (PageHeader) BufferGetPage(buffer);
|
|
lp = PageGetItemId(dp, ItemPointerGetOffsetNumber(tid));
|
|
|
|
Assert(ItemIdIsNormal(lp));
|
|
|
|
tuple.t_data = (HeapTupleHeader) PageGetItem((Page) dp, lp);
|
|
tuple.t_len = ItemIdGetLength(lp);
|
|
tuple.t_self = *tid;
|
|
tuple.t_tableOid = RelationGetRelid(relation);
|
|
}
|
|
|
|
result = heap_copytuple(&tuple);
|
|
ReleaseBuffer(buffer);
|
|
|
|
return result;
|
|
}
|
|
|
|
|
|
/* ----------
|
|
* After-trigger stuff
|
|
*
|
|
* The AfterTriggersData struct holds data about pending AFTER trigger events
|
|
* during the current transaction tree. (BEFORE triggers are fired
|
|
* immediately so we don't need any persistent state about them.) The struct
|
|
* and most of its subsidiary data are kept in TopTransactionContext; however
|
|
* the individual event records are kept in separate contexts, to make them
|
|
* easy to delete during subtransaction abort.
|
|
*
|
|
* Because the list of pending events can grow large, we go to some effort
|
|
* to minimize memory consumption. We do not use the generic List mechanism
|
|
* but thread the events manually.
|
|
*
|
|
* XXX We need to be able to save the per-event data in a file if it grows too
|
|
* large.
|
|
* ----------
|
|
*/
|
|
|
|
/* Per-trigger SET CONSTRAINT status */
|
|
typedef struct SetConstraintTriggerData
|
|
{
|
|
Oid sct_tgoid;
|
|
bool sct_tgisdeferred;
|
|
} SetConstraintTriggerData;
|
|
|
|
typedef struct SetConstraintTriggerData *SetConstraintTrigger;
|
|
|
|
/*
|
|
* SET CONSTRAINT intra-transaction status.
|
|
*
|
|
* We make this a single palloc'd object so it can be copied and freed easily.
|
|
*
|
|
* all_isset and all_isdeferred are used to keep track
|
|
* of SET CONSTRAINTS ALL {DEFERRED, IMMEDIATE}.
|
|
*
|
|
* trigstates[] stores per-trigger tgisdeferred settings.
|
|
*/
|
|
typedef struct SetConstraintStateData
|
|
{
|
|
bool all_isset;
|
|
bool all_isdeferred;
|
|
int numstates; /* number of trigstates[] entries in use */
|
|
int numalloc; /* allocated size of trigstates[] */
|
|
SetConstraintTriggerData trigstates[1]; /* VARIABLE LENGTH ARRAY */
|
|
} SetConstraintStateData;
|
|
|
|
typedef SetConstraintStateData *SetConstraintState;
|
|
|
|
|
|
/*
|
|
* Per-trigger-event data
|
|
*
|
|
* Note: ate_firing_id is meaningful when either AFTER_TRIGGER_DONE
|
|
* or AFTER_TRIGGER_IN_PROGRESS is set. It indicates which trigger firing
|
|
* cycle the trigger was or will be fired in.
|
|
*/
|
|
typedef struct AfterTriggerEventData *AfterTriggerEvent;
|
|
|
|
typedef struct AfterTriggerEventData
|
|
{
|
|
AfterTriggerEvent ate_next; /* list link */
|
|
TriggerEvent ate_event; /* event type and status bits */
|
|
CommandId ate_firing_id; /* ID for firing cycle */
|
|
Oid ate_tgoid; /* the trigger's ID */
|
|
Oid ate_relid; /* the relation it's on */
|
|
ItemPointerData ate_oldctid; /* specific tuple(s) involved */
|
|
ItemPointerData ate_newctid;
|
|
} AfterTriggerEventData;
|
|
|
|
/* A list of events */
|
|
typedef struct AfterTriggerEventList
|
|
{
|
|
AfterTriggerEvent head;
|
|
AfterTriggerEvent tail;
|
|
} AfterTriggerEventList;
|
|
|
|
|
|
/*
|
|
* All per-transaction data for the AFTER TRIGGERS module.
|
|
*
|
|
* AfterTriggersData has the following fields:
|
|
*
|
|
* firing_counter is incremented for each call of afterTriggerInvokeEvents.
|
|
* We mark firable events with the current firing cycle's ID so that we can
|
|
* tell which ones to work on. This ensures sane behavior if a trigger
|
|
* function chooses to do SET CONSTRAINTS: the inner SET CONSTRAINTS will
|
|
* only fire those events that weren't already scheduled for firing.
|
|
*
|
|
* state keeps track of the transaction-local effects of SET CONSTRAINTS.
|
|
* This is saved and restored across failed subtransactions.
|
|
*
|
|
* events is the current list of deferred events. This is global across
|
|
* all subtransactions of the current transaction. In a subtransaction
|
|
* abort, we know that the events added by the subtransaction are at the
|
|
* end of the list, so it is relatively easy to discard them. The event
|
|
* structs themselves are stored in event_cxt if generated by the top-level
|
|
* transaction, else in per-subtransaction contexts identified by the
|
|
* entries in cxt_stack.
|
|
*
|
|
* query_depth is the current depth of nested AfterTriggerBeginQuery calls
|
|
* (-1 when the stack is empty).
|
|
*
|
|
* query_stack[query_depth] is a list of AFTER trigger events queued by the
|
|
* current query (and the query_stack entries below it are lists of trigger
|
|
* events queued by calling queries). None of these are valid until the
|
|
* matching AfterTriggerEndQuery call occurs. At that point we fire
|
|
* immediate-mode triggers, and append any deferred events to the main events
|
|
* list.
|
|
*
|
|
* maxquerydepth is just the allocated length of query_stack.
|
|
*
|
|
* state_stack is a stack of pointers to saved copies of the SET CONSTRAINTS
|
|
* state data; each subtransaction level that modifies that state first
|
|
* saves a copy, which we use to restore the state if we abort.
|
|
*
|
|
* events_stack is a stack of copies of the events head/tail pointers,
|
|
* which we use to restore those values during subtransaction abort.
|
|
*
|
|
* depth_stack is a stack of copies of subtransaction-start-time query_depth,
|
|
* which we similarly use to clean up at subtransaction abort.
|
|
*
|
|
* firing_stack is a stack of copies of subtransaction-start-time
|
|
* firing_counter. We use this to recognize which deferred triggers were
|
|
* fired (or marked for firing) within an aborted subtransaction.
|
|
*
|
|
* We use GetCurrentTransactionNestLevel() to determine the correct array
|
|
* index in these stacks. maxtransdepth is the number of allocated entries in
|
|
* each stack. (By not keeping our own stack pointer, we can avoid trouble
|
|
* in cases where errors during subxact abort cause multiple invocations
|
|
* of AfterTriggerEndSubXact() at the same nesting depth.)
|
|
*/
|
|
typedef struct AfterTriggersData
|
|
{
|
|
CommandId firing_counter; /* next firing ID to assign */
|
|
SetConstraintState state; /* the active S C state */
|
|
AfterTriggerEventList events; /* deferred-event list */
|
|
int query_depth; /* current query list index */
|
|
AfterTriggerEventList *query_stack; /* events pending from each query */
|
|
int maxquerydepth; /* allocated len of above array */
|
|
MemoryContext event_cxt; /* top transaction's event context, if any */
|
|
MemoryContext *cxt_stack; /* per-subtransaction event contexts */
|
|
|
|
/* these fields are just for resetting at subtrans abort: */
|
|
|
|
SetConstraintState *state_stack; /* stacked S C states */
|
|
AfterTriggerEventList *events_stack; /* stacked list pointers */
|
|
int *depth_stack; /* stacked query_depths */
|
|
CommandId *firing_stack; /* stacked firing_counters */
|
|
int maxtransdepth; /* allocated len of above arrays */
|
|
} AfterTriggersData;
|
|
|
|
typedef AfterTriggersData *AfterTriggers;
|
|
|
|
static AfterTriggers afterTriggers;
|
|
|
|
|
|
static void AfterTriggerExecute(AfterTriggerEvent event,
|
|
Relation rel, TriggerDesc *trigdesc,
|
|
FmgrInfo *finfo,
|
|
Instrumentation *instr,
|
|
MemoryContext per_tuple_context);
|
|
static SetConstraintState SetConstraintStateCreate(int numalloc);
|
|
static SetConstraintState SetConstraintStateCopy(SetConstraintState state);
|
|
static SetConstraintState SetConstraintStateAddItem(SetConstraintState state,
|
|
Oid tgoid, bool tgisdeferred);
|
|
|
|
|
|
/* ----------
|
|
* afterTriggerCheckState()
|
|
*
|
|
* Returns true if the trigger identified by tgoid is actually
|
|
* in state DEFERRED.
|
|
* ----------
|
|
*/
|
|
static bool
|
|
afterTriggerCheckState(Oid tgoid, TriggerEvent eventstate)
|
|
{
|
|
SetConstraintState state = afterTriggers->state;
|
|
int i;
|
|
|
|
/*
|
|
* For not-deferrable triggers (i.e. normal AFTER ROW triggers and
|
|
* constraints declared NOT DEFERRABLE), the state is always false.
|
|
*/
|
|
if ((eventstate & AFTER_TRIGGER_DEFERRABLE) == 0)
|
|
return false;
|
|
|
|
/*
|
|
* Check if SET CONSTRAINTS has been executed for this specific trigger.
|
|
*/
|
|
for (i = 0; i < state->numstates; i++)
|
|
{
|
|
if (state->trigstates[i].sct_tgoid == tgoid)
|
|
return state->trigstates[i].sct_tgisdeferred;
|
|
}
|
|
|
|
/*
|
|
* Check if SET CONSTRAINTS ALL has been executed; if so use that.
|
|
*/
|
|
if (state->all_isset)
|
|
return state->all_isdeferred;
|
|
|
|
/*
|
|
* Otherwise return the default state for the trigger.
|
|
*/
|
|
return ((eventstate & AFTER_TRIGGER_INITDEFERRED) != 0);
|
|
}
|
|
|
|
|
|
/* ----------
|
|
* afterTriggerAddEvent()
|
|
*
|
|
* Add a new trigger event to the current query's queue.
|
|
* ----------
|
|
*/
|
|
static void
|
|
afterTriggerAddEvent(AfterTriggerEvent event)
|
|
{
|
|
AfterTriggerEventList *events;
|
|
|
|
Assert(event->ate_next == NULL);
|
|
|
|
/* Must be inside a query */
|
|
Assert(afterTriggers->query_depth >= 0);
|
|
|
|
events = &afterTriggers->query_stack[afterTriggers->query_depth];
|
|
if (events->tail == NULL)
|
|
{
|
|
/* first list entry */
|
|
events->head = event;
|
|
events->tail = event;
|
|
}
|
|
else
|
|
{
|
|
events->tail->ate_next = event;
|
|
events->tail = event;
|
|
}
|
|
}
|
|
|
|
|
|
/* ----------
|
|
* AfterTriggerExecute()
|
|
*
|
|
* Fetch the required tuples back from the heap and fire one
|
|
* single trigger function.
|
|
*
|
|
* Frequently, this will be fired many times in a row for triggers of
|
|
* a single relation. Therefore, we cache the open relation and provide
|
|
* fmgr lookup cache space at the caller level. (For triggers fired at
|
|
* the end of a query, we can even piggyback on the executor's state.)
|
|
*
|
|
* event: event currently being fired.
|
|
* rel: open relation for event.
|
|
* trigdesc: working copy of rel's trigger info.
|
|
* finfo: array of fmgr lookup cache entries (one per trigger in trigdesc).
|
|
* instr: array of EXPLAIN ANALYZE instrumentation nodes (one per trigger),
|
|
* or NULL if no instrumentation is wanted.
|
|
* per_tuple_context: memory context to call trigger function in.
|
|
* ----------
|
|
*/
|
|
static void
|
|
AfterTriggerExecute(AfterTriggerEvent event,
|
|
Relation rel, TriggerDesc *trigdesc,
|
|
FmgrInfo *finfo, Instrumentation *instr,
|
|
MemoryContext per_tuple_context)
|
|
{
|
|
Oid tgoid = event->ate_tgoid;
|
|
TriggerData LocTriggerData;
|
|
HeapTupleData oldtuple;
|
|
HeapTupleData newtuple;
|
|
HeapTuple rettuple;
|
|
Buffer oldbuffer = InvalidBuffer;
|
|
Buffer newbuffer = InvalidBuffer;
|
|
int tgindx;
|
|
|
|
/*
|
|
* Locate trigger in trigdesc.
|
|
*/
|
|
LocTriggerData.tg_trigger = NULL;
|
|
for (tgindx = 0; tgindx < trigdesc->numtriggers; tgindx++)
|
|
{
|
|
if (trigdesc->triggers[tgindx].tgoid == tgoid)
|
|
{
|
|
LocTriggerData.tg_trigger = &(trigdesc->triggers[tgindx]);
|
|
break;
|
|
}
|
|
}
|
|
if (LocTriggerData.tg_trigger == NULL)
|
|
elog(ERROR, "could not find trigger %u", tgoid);
|
|
|
|
/*
|
|
* If doing EXPLAIN ANALYZE, start charging time to this trigger. We want
|
|
* to include time spent re-fetching tuples in the trigger cost.
|
|
*/
|
|
if (instr)
|
|
InstrStartNode(instr + tgindx);
|
|
|
|
/*
|
|
* Fetch the required OLD and NEW tuples.
|
|
*/
|
|
LocTriggerData.tg_trigtuple = NULL;
|
|
LocTriggerData.tg_newtuple = NULL;
|
|
LocTriggerData.tg_trigtuplebuf = InvalidBuffer;
|
|
LocTriggerData.tg_newtuplebuf = InvalidBuffer;
|
|
|
|
if (ItemPointerIsValid(&(event->ate_oldctid)))
|
|
{
|
|
ItemPointerCopy(&(event->ate_oldctid), &(oldtuple.t_self));
|
|
if (!heap_fetch(rel, SnapshotAny, &oldtuple, &oldbuffer, false, NULL))
|
|
elog(ERROR, "failed to fetch old tuple for AFTER trigger");
|
|
LocTriggerData.tg_trigtuple = &oldtuple;
|
|
LocTriggerData.tg_trigtuplebuf = oldbuffer;
|
|
}
|
|
|
|
if (ItemPointerIsValid(&(event->ate_newctid)))
|
|
{
|
|
ItemPointerCopy(&(event->ate_newctid), &(newtuple.t_self));
|
|
if (!heap_fetch(rel, SnapshotAny, &newtuple, &newbuffer, false, NULL))
|
|
elog(ERROR, "failed to fetch new tuple for AFTER trigger");
|
|
if (LocTriggerData.tg_trigtuple != NULL)
|
|
{
|
|
LocTriggerData.tg_newtuple = &newtuple;
|
|
LocTriggerData.tg_newtuplebuf = newbuffer;
|
|
}
|
|
else
|
|
{
|
|
LocTriggerData.tg_trigtuple = &newtuple;
|
|
LocTriggerData.tg_trigtuplebuf = newbuffer;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Setup the remaining trigger information
|
|
*/
|
|
LocTriggerData.type = T_TriggerData;
|
|
LocTriggerData.tg_event =
|
|
event->ate_event & (TRIGGER_EVENT_OPMASK | TRIGGER_EVENT_ROW);
|
|
LocTriggerData.tg_relation = rel;
|
|
|
|
MemoryContextReset(per_tuple_context);
|
|
|
|
/*
|
|
* Call the trigger and throw away any possibly returned updated tuple.
|
|
* (Don't let ExecCallTriggerFunc measure EXPLAIN time.)
|
|
*/
|
|
rettuple = ExecCallTriggerFunc(&LocTriggerData,
|
|
tgindx,
|
|
finfo,
|
|
NULL,
|
|
per_tuple_context);
|
|
if (rettuple != NULL && rettuple != &oldtuple && rettuple != &newtuple)
|
|
heap_freetuple(rettuple);
|
|
|
|
/*
|
|
* Release buffers
|
|
*/
|
|
if (oldbuffer != InvalidBuffer)
|
|
ReleaseBuffer(oldbuffer);
|
|
if (newbuffer != InvalidBuffer)
|
|
ReleaseBuffer(newbuffer);
|
|
|
|
/*
|
|
* If doing EXPLAIN ANALYZE, stop charging time to this trigger, and count
|
|
* one "tuple returned" (really the number of firings).
|
|
*/
|
|
if (instr)
|
|
InstrStopNode(instr + tgindx, 1);
|
|
}
|
|
|
|
|
|
/*
|
|
* afterTriggerMarkEvents()
|
|
*
|
|
* Scan the given event list for not yet invoked events. Mark the ones
|
|
* that can be invoked now with the current firing ID.
|
|
*
|
|
* If move_list isn't NULL, events that are not to be invoked now are
|
|
* removed from the given list and appended to move_list.
|
|
*
|
|
* When immediate_only is TRUE, do not invoke currently-deferred triggers.
|
|
* (This will be FALSE only at main transaction exit.)
|
|
*
|
|
* Returns TRUE if any invokable events were found.
|
|
*/
|
|
static bool
|
|
afterTriggerMarkEvents(AfterTriggerEventList *events,
|
|
AfterTriggerEventList *move_list,
|
|
bool immediate_only)
|
|
{
|
|
bool found = false;
|
|
AfterTriggerEvent event,
|
|
prev_event;
|
|
|
|
prev_event = NULL;
|
|
event = events->head;
|
|
|
|
while (event != NULL)
|
|
{
|
|
bool defer_it = false;
|
|
AfterTriggerEvent next_event;
|
|
|
|
if (!(event->ate_event &
|
|
(AFTER_TRIGGER_DONE | AFTER_TRIGGER_IN_PROGRESS)))
|
|
{
|
|
/*
|
|
* This trigger hasn't been called or scheduled yet. Check if we
|
|
* should call it now.
|
|
*/
|
|
if (immediate_only &&
|
|
afterTriggerCheckState(event->ate_tgoid, event->ate_event))
|
|
{
|
|
defer_it = true;
|
|
}
|
|
else
|
|
{
|
|
/*
|
|
* Mark it as to be fired in this firing cycle.
|
|
*/
|
|
event->ate_firing_id = afterTriggers->firing_counter;
|
|
event->ate_event |= AFTER_TRIGGER_IN_PROGRESS;
|
|
found = true;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* If it's deferred, move it to move_list, if requested.
|
|
*/
|
|
next_event = event->ate_next;
|
|
|
|
if (defer_it && move_list != NULL)
|
|
{
|
|
/* Delink it from input list */
|
|
if (prev_event)
|
|
prev_event->ate_next = next_event;
|
|
else
|
|
events->head = next_event;
|
|
/* and add it to move_list */
|
|
event->ate_next = NULL;
|
|
if (move_list->tail == NULL)
|
|
{
|
|
/* first list entry */
|
|
move_list->head = event;
|
|
move_list->tail = event;
|
|
}
|
|
else
|
|
{
|
|
move_list->tail->ate_next = event;
|
|
move_list->tail = event;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
/* Keep it in input list */
|
|
prev_event = event;
|
|
}
|
|
|
|
event = next_event;
|
|
}
|
|
|
|
/* Update list tail pointer in case we moved tail event */
|
|
events->tail = prev_event;
|
|
|
|
return found;
|
|
}
|
|
|
|
/* ----------
|
|
* afterTriggerInvokeEvents()
|
|
*
|
|
* Scan the given event list for events that are marked as to be fired
|
|
* in the current firing cycle, and fire them.
|
|
*
|
|
* If estate isn't NULL, we use its result relation info to avoid repeated
|
|
* openings and closing of trigger target relations. If it is NULL, we
|
|
* make one locally to cache the info in case there are multiple trigger
|
|
* events per rel.
|
|
*
|
|
* When delete_ok is TRUE, it's okay to delete fully-processed events.
|
|
* The events list pointers are updated.
|
|
* ----------
|
|
*/
|
|
static void
|
|
afterTriggerInvokeEvents(AfterTriggerEventList *events,
|
|
CommandId firing_id,
|
|
EState *estate,
|
|
bool delete_ok)
|
|
{
|
|
AfterTriggerEvent event,
|
|
prev_event;
|
|
MemoryContext per_tuple_context;
|
|
bool local_estate = false;
|
|
Relation rel = NULL;
|
|
TriggerDesc *trigdesc = NULL;
|
|
FmgrInfo *finfo = NULL;
|
|
Instrumentation *instr = NULL;
|
|
|
|
/* Make a local EState if need be */
|
|
if (estate == NULL)
|
|
{
|
|
estate = CreateExecutorState();
|
|
local_estate = true;
|
|
}
|
|
|
|
/* Make a per-tuple memory context for trigger function calls */
|
|
per_tuple_context =
|
|
AllocSetContextCreate(CurrentMemoryContext,
|
|
"AfterTriggerTupleContext",
|
|
ALLOCSET_DEFAULT_MINSIZE,
|
|
ALLOCSET_DEFAULT_INITSIZE,
|
|
ALLOCSET_DEFAULT_MAXSIZE);
|
|
|
|
prev_event = NULL;
|
|
event = events->head;
|
|
|
|
while (event != NULL)
|
|
{
|
|
AfterTriggerEvent next_event;
|
|
|
|
/*
|
|
* Is it one for me to fire?
|
|
*/
|
|
if ((event->ate_event & AFTER_TRIGGER_IN_PROGRESS) &&
|
|
event->ate_firing_id == firing_id)
|
|
{
|
|
/*
|
|
* So let's fire it... but first, find the correct relation if
|
|
* this is not the same relation as before.
|
|
*/
|
|
if (rel == NULL || RelationGetRelid(rel) != event->ate_relid)
|
|
{
|
|
ResultRelInfo *rInfo;
|
|
|
|
rInfo = ExecGetTriggerResultRel(estate, event->ate_relid);
|
|
rel = rInfo->ri_RelationDesc;
|
|
trigdesc = rInfo->ri_TrigDesc;
|
|
finfo = rInfo->ri_TrigFunctions;
|
|
instr = rInfo->ri_TrigInstrument;
|
|
if (trigdesc == NULL) /* should not happen */
|
|
elog(ERROR, "relation %u has no triggers",
|
|
event->ate_relid);
|
|
}
|
|
|
|
/*
|
|
* Fire it. Note that the AFTER_TRIGGER_IN_PROGRESS flag is still
|
|
* set, so recursive examinations of the event list won't try to
|
|
* re-fire it.
|
|
*/
|
|
AfterTriggerExecute(event, rel, trigdesc, finfo, instr,
|
|
per_tuple_context);
|
|
|
|
/*
|
|
* Mark the event as done.
|
|
*/
|
|
event->ate_event &= ~AFTER_TRIGGER_IN_PROGRESS;
|
|
event->ate_event |= AFTER_TRIGGER_DONE;
|
|
}
|
|
|
|
/*
|
|
* If it's now done, throw it away, if allowed.
|
|
*
|
|
* NB: it's possible the trigger call above added more events to the
|
|
* queue, or that calls we will do later will want to add more, so we
|
|
* have to be careful about maintaining list validity at all points
|
|
* here.
|
|
*/
|
|
next_event = event->ate_next;
|
|
|
|
if ((event->ate_event & AFTER_TRIGGER_DONE) && delete_ok)
|
|
{
|
|
/* Delink it from list and free it */
|
|
if (prev_event)
|
|
prev_event->ate_next = next_event;
|
|
else
|
|
events->head = next_event;
|
|
pfree(event);
|
|
}
|
|
else
|
|
{
|
|
/* Keep it in list */
|
|
prev_event = event;
|
|
}
|
|
|
|
event = next_event;
|
|
}
|
|
|
|
/* Update list tail pointer in case we just deleted tail event */
|
|
events->tail = prev_event;
|
|
|
|
/* Release working resources */
|
|
MemoryContextDelete(per_tuple_context);
|
|
|
|
if (local_estate)
|
|
{
|
|
ListCell *l;
|
|
|
|
foreach(l, estate->es_trig_target_relations)
|
|
{
|
|
ResultRelInfo *resultRelInfo = (ResultRelInfo *) lfirst(l);
|
|
|
|
/* Close indices and then the relation itself */
|
|
ExecCloseIndices(resultRelInfo);
|
|
heap_close(resultRelInfo->ri_RelationDesc, NoLock);
|
|
}
|
|
FreeExecutorState(estate);
|
|
}
|
|
}
|
|
|
|
|
|
/* ----------
|
|
* AfterTriggerBeginXact()
|
|
*
|
|
* Called at transaction start (either BEGIN or implicit for single
|
|
* statement outside of transaction block).
|
|
* ----------
|
|
*/
|
|
void
|
|
AfterTriggerBeginXact(void)
|
|
{
|
|
Assert(afterTriggers == NULL);
|
|
|
|
/*
|
|
* Build empty after-trigger state structure
|
|
*/
|
|
afterTriggers = (AfterTriggers)
|
|
MemoryContextAlloc(TopTransactionContext,
|
|
sizeof(AfterTriggersData));
|
|
|
|
afterTriggers->firing_counter = FirstCommandId;
|
|
afterTriggers->state = SetConstraintStateCreate(8);
|
|
afterTriggers->events.head = NULL;
|
|
afterTriggers->events.tail = NULL;
|
|
afterTriggers->query_depth = -1;
|
|
|
|
/* We initialize the query stack to a reasonable size */
|
|
afterTriggers->query_stack = (AfterTriggerEventList *)
|
|
MemoryContextAlloc(TopTransactionContext,
|
|
8 * sizeof(AfterTriggerEventList));
|
|
afterTriggers->maxquerydepth = 8;
|
|
|
|
/* Context for events is created only when needed */
|
|
afterTriggers->event_cxt = NULL;
|
|
|
|
/* Subtransaction stack is empty until/unless needed */
|
|
afterTriggers->cxt_stack = NULL;
|
|
afterTriggers->state_stack = NULL;
|
|
afterTriggers->events_stack = NULL;
|
|
afterTriggers->depth_stack = NULL;
|
|
afterTriggers->firing_stack = NULL;
|
|
afterTriggers->maxtransdepth = 0;
|
|
}
|
|
|
|
|
|
/* ----------
|
|
* AfterTriggerBeginQuery()
|
|
*
|
|
* Called just before we start processing a single query within a
|
|
* transaction (or subtransaction). Set up to record AFTER trigger
|
|
* events queued by the query. Note that it is allowed to have
|
|
* nested queries within a (sub)transaction.
|
|
* ----------
|
|
*/
|
|
void
|
|
AfterTriggerBeginQuery(void)
|
|
{
|
|
/* Must be inside a transaction */
|
|
Assert(afterTriggers != NULL);
|
|
|
|
/* Increase the query stack depth */
|
|
afterTriggers->query_depth++;
|
|
|
|
/*
|
|
* Allocate more space in the query stack if needed.
|
|
*/
|
|
if (afterTriggers->query_depth >= afterTriggers->maxquerydepth)
|
|
{
|
|
/* repalloc will keep the stack in the same context */
|
|
int new_alloc = afterTriggers->maxquerydepth * 2;
|
|
|
|
afterTriggers->query_stack = (AfterTriggerEventList *)
|
|
repalloc(afterTriggers->query_stack,
|
|
new_alloc * sizeof(AfterTriggerEventList));
|
|
afterTriggers->maxquerydepth = new_alloc;
|
|
}
|
|
|
|
/* Initialize this query's list to empty */
|
|
afterTriggers->query_stack[afterTriggers->query_depth].head = NULL;
|
|
afterTriggers->query_stack[afterTriggers->query_depth].tail = NULL;
|
|
}
|
|
|
|
|
|
/* ----------
|
|
* AfterTriggerEndQuery()
|
|
*
|
|
* Called after one query has been completely processed. At this time
|
|
* we invoke all AFTER IMMEDIATE trigger events queued by the query, and
|
|
* transfer deferred trigger events to the global deferred-trigger list.
|
|
*
|
|
* Note that this should be called just BEFORE closing down the executor
|
|
* with ExecutorEnd, because we make use of the EState's info about
|
|
* target relations.
|
|
* ----------
|
|
*/
|
|
void
|
|
AfterTriggerEndQuery(EState *estate)
|
|
{
|
|
AfterTriggerEventList *events;
|
|
|
|
/* Must be inside a transaction */
|
|
Assert(afterTriggers != NULL);
|
|
|
|
/* Must be inside a query, too */
|
|
Assert(afterTriggers->query_depth >= 0);
|
|
|
|
/*
|
|
* Process all immediate-mode triggers queued by the query, and move the
|
|
* deferred ones to the main list of deferred events.
|
|
*
|
|
* Notice that we decide which ones will be fired, and put the deferred
|
|
* ones on the main list, before anything is actually fired. This ensures
|
|
* reasonably sane behavior if a trigger function does SET CONSTRAINTS ...
|
|
* IMMEDIATE: all events we have decided to defer will be available for it
|
|
* to fire.
|
|
*
|
|
* We loop in case a trigger queues more events.
|
|
*
|
|
* If we find no firable events, we don't have to increment
|
|
* firing_counter.
|
|
*/
|
|
events = &afterTriggers->query_stack[afterTriggers->query_depth];
|
|
while (afterTriggerMarkEvents(events, &afterTriggers->events, true))
|
|
{
|
|
CommandId firing_id = afterTriggers->firing_counter++;
|
|
|
|
/* OK to delete the immediate events after processing them */
|
|
afterTriggerInvokeEvents(events, firing_id, estate, true);
|
|
}
|
|
|
|
afterTriggers->query_depth--;
|
|
}
|
|
|
|
|
|
/* ----------
|
|
* AfterTriggerFireDeferred()
|
|
*
|
|
* Called just before the current transaction is committed. At this
|
|
* time we invoke all pending DEFERRED triggers.
|
|
*
|
|
* It is possible for other modules to queue additional deferred triggers
|
|
* during pre-commit processing; therefore xact.c may have to call this
|
|
* multiple times.
|
|
* ----------
|
|
*/
|
|
void
|
|
AfterTriggerFireDeferred(void)
|
|
{
|
|
AfterTriggerEventList *events;
|
|
|
|
/* Must be inside a transaction */
|
|
Assert(afterTriggers != NULL);
|
|
|
|
/* ... but not inside a query */
|
|
Assert(afterTriggers->query_depth == -1);
|
|
|
|
/*
|
|
* If there are any triggers to fire, make sure we have set a snapshot for
|
|
* them to use. (Since PortalRunUtility doesn't set a snap for COMMIT, we
|
|
* can't assume ActiveSnapshot is valid on entry.)
|
|
*/
|
|
events = &afterTriggers->events;
|
|
if (events->head != NULL)
|
|
ActiveSnapshot = CopySnapshot(GetTransactionSnapshot());
|
|
|
|
/*
|
|
* Run all the remaining triggers. Loop until they are all gone, in case
|
|
* some trigger queues more for us to do.
|
|
*/
|
|
while (afterTriggerMarkEvents(events, NULL, false))
|
|
{
|
|
CommandId firing_id = afterTriggers->firing_counter++;
|
|
|
|
afterTriggerInvokeEvents(events, firing_id, NULL, true);
|
|
}
|
|
|
|
Assert(events->head == NULL);
|
|
}
|
|
|
|
|
|
/* ----------
|
|
* AfterTriggerEndXact()
|
|
*
|
|
* The current transaction is finishing.
|
|
*
|
|
* Any unfired triggers are canceled so we simply throw
|
|
* away anything we know.
|
|
*
|
|
* Note: it is possible for this to be called repeatedly in case of
|
|
* error during transaction abort; therefore, do not complain if
|
|
* already closed down.
|
|
* ----------
|
|
*/
|
|
void
|
|
AfterTriggerEndXact(bool isCommit)
|
|
{
|
|
/*
|
|
* Forget everything we know about AFTER triggers.
|
|
*
|
|
* Since all the info is in TopTransactionContext or children thereof, we
|
|
* don't really need to do anything to reclaim memory. However, the
|
|
* pending-events list could be large, and so it's useful to discard it as
|
|
* soon as possible --- especially if we are aborting because we ran out
|
|
* of memory for the list!
|
|
*
|
|
* (Note: any event_cxts of child subtransactions could also be deleted
|
|
* here, but we have no convenient way to find them, so we leave it to
|
|
* TopTransactionContext reset to clean them up.)
|
|
*/
|
|
if (afterTriggers && afterTriggers->event_cxt)
|
|
MemoryContextDelete(afterTriggers->event_cxt);
|
|
|
|
afterTriggers = NULL;
|
|
}
|
|
|
|
/*
|
|
* AfterTriggerBeginSubXact()
|
|
*
|
|
* Start a subtransaction.
|
|
*/
|
|
void
|
|
AfterTriggerBeginSubXact(void)
|
|
{
|
|
int my_level = GetCurrentTransactionNestLevel();
|
|
|
|
/*
|
|
* Ignore call if the transaction is in aborted state. (Probably
|
|
* shouldn't happen?)
|
|
*/
|
|
if (afterTriggers == NULL)
|
|
return;
|
|
|
|
/*
|
|
* Allocate more space in the stacks if needed. (Note: because the
|
|
* minimum nest level of a subtransaction is 2, we waste the first couple
|
|
* entries of each array; not worth the notational effort to avoid it.)
|
|
*/
|
|
while (my_level >= afterTriggers->maxtransdepth)
|
|
{
|
|
if (afterTriggers->maxtransdepth == 0)
|
|
{
|
|
MemoryContext old_cxt;
|
|
|
|
old_cxt = MemoryContextSwitchTo(TopTransactionContext);
|
|
|
|
#define DEFTRIG_INITALLOC 8
|
|
afterTriggers->cxt_stack = (MemoryContext *)
|
|
palloc(DEFTRIG_INITALLOC * sizeof(MemoryContext));
|
|
afterTriggers->state_stack = (SetConstraintState *)
|
|
palloc(DEFTRIG_INITALLOC * sizeof(SetConstraintState));
|
|
afterTriggers->events_stack = (AfterTriggerEventList *)
|
|
palloc(DEFTRIG_INITALLOC * sizeof(AfterTriggerEventList));
|
|
afterTriggers->depth_stack = (int *)
|
|
palloc(DEFTRIG_INITALLOC * sizeof(int));
|
|
afterTriggers->firing_stack = (CommandId *)
|
|
palloc(DEFTRIG_INITALLOC * sizeof(CommandId));
|
|
afterTriggers->maxtransdepth = DEFTRIG_INITALLOC;
|
|
|
|
MemoryContextSwitchTo(old_cxt);
|
|
}
|
|
else
|
|
{
|
|
/* repalloc will keep the stacks in the same context */
|
|
int new_alloc = afterTriggers->maxtransdepth * 2;
|
|
|
|
afterTriggers->cxt_stack = (MemoryContext *)
|
|
repalloc(afterTriggers->cxt_stack,
|
|
new_alloc * sizeof(MemoryContext));
|
|
afterTriggers->state_stack = (SetConstraintState *)
|
|
repalloc(afterTriggers->state_stack,
|
|
new_alloc * sizeof(SetConstraintState));
|
|
afterTriggers->events_stack = (AfterTriggerEventList *)
|
|
repalloc(afterTriggers->events_stack,
|
|
new_alloc * sizeof(AfterTriggerEventList));
|
|
afterTriggers->depth_stack = (int *)
|
|
repalloc(afterTriggers->depth_stack,
|
|
new_alloc * sizeof(int));
|
|
afterTriggers->firing_stack = (CommandId *)
|
|
repalloc(afterTriggers->firing_stack,
|
|
new_alloc * sizeof(CommandId));
|
|
afterTriggers->maxtransdepth = new_alloc;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Push the current information into the stack. The SET CONSTRAINTS state
|
|
* is not saved until/unless changed. Likewise, we don't make a
|
|
* per-subtransaction event context until needed.
|
|
*/
|
|
afterTriggers->cxt_stack[my_level] = NULL;
|
|
afterTriggers->state_stack[my_level] = NULL;
|
|
afterTriggers->events_stack[my_level] = afterTriggers->events;
|
|
afterTriggers->depth_stack[my_level] = afterTriggers->query_depth;
|
|
afterTriggers->firing_stack[my_level] = afterTriggers->firing_counter;
|
|
}
|
|
|
|
/*
|
|
* AfterTriggerEndSubXact()
|
|
*
|
|
* The current subtransaction is ending.
|
|
*/
|
|
void
|
|
AfterTriggerEndSubXact(bool isCommit)
|
|
{
|
|
int my_level = GetCurrentTransactionNestLevel();
|
|
SetConstraintState state;
|
|
AfterTriggerEvent event;
|
|
CommandId subxact_firing_id;
|
|
|
|
/*
|
|
* Ignore call if the transaction is in aborted state. (Probably
|
|
* unneeded)
|
|
*/
|
|
if (afterTriggers == NULL)
|
|
return;
|
|
|
|
/*
|
|
* Pop the prior state if needed.
|
|
*/
|
|
Assert(my_level < afterTriggers->maxtransdepth);
|
|
|
|
if (isCommit)
|
|
{
|
|
/* If we saved a prior state, we don't need it anymore */
|
|
state = afterTriggers->state_stack[my_level];
|
|
if (state != NULL)
|
|
pfree(state);
|
|
/* this avoids double pfree if error later: */
|
|
afterTriggers->state_stack[my_level] = NULL;
|
|
Assert(afterTriggers->query_depth ==
|
|
afterTriggers->depth_stack[my_level]);
|
|
|
|
/*
|
|
* It's entirely possible that the subxact created an event_cxt but
|
|
* there is not anything left in it (because all the triggers were
|
|
* fired at end-of-statement). If so, we should release the context
|
|
* to prevent memory leakage in a long sequence of subtransactions. We
|
|
* can detect whether there's anything of use in the context by seeing
|
|
* if anything was added to the global events list since subxact
|
|
* start. (This test doesn't catch every case where the context is
|
|
* deletable; for instance maybe the only additions were from a
|
|
* sub-sub-xact. But it handles the common case.)
|
|
*/
|
|
if (afterTriggers->cxt_stack[my_level] &&
|
|
afterTriggers->events.tail == afterTriggers->events_stack[my_level].tail)
|
|
{
|
|
MemoryContextDelete(afterTriggers->cxt_stack[my_level]);
|
|
/* avoid double delete if abort later */
|
|
afterTriggers->cxt_stack[my_level] = NULL;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
/*
|
|
* Aborting. We don't really need to release the subxact's event_cxt,
|
|
* since it will go away anyway when CurTransactionContext gets reset,
|
|
* but doing so early in subxact abort helps free space we might need.
|
|
*
|
|
* (Note: any event_cxts of child subtransactions could also be
|
|
* deleted here, but we have no convenient way to find them, so we
|
|
* leave it to CurTransactionContext reset to clean them up.)
|
|
*/
|
|
if (afterTriggers->cxt_stack[my_level])
|
|
{
|
|
MemoryContextDelete(afterTriggers->cxt_stack[my_level]);
|
|
/* avoid double delete if repeated aborts */
|
|
afterTriggers->cxt_stack[my_level] = NULL;
|
|
}
|
|
|
|
/*
|
|
* Restore the pointers from the stacks.
|
|
*/
|
|
afterTriggers->events = afterTriggers->events_stack[my_level];
|
|
afterTriggers->query_depth = afterTriggers->depth_stack[my_level];
|
|
|
|
/*
|
|
* Cleanup the tail of the list.
|
|
*/
|
|
if (afterTriggers->events.tail != NULL)
|
|
afterTriggers->events.tail->ate_next = NULL;
|
|
|
|
/*
|
|
* Restore the trigger state. If the saved state is NULL, then this
|
|
* subxact didn't save it, so it doesn't need restoring.
|
|
*/
|
|
state = afterTriggers->state_stack[my_level];
|
|
if (state != NULL)
|
|
{
|
|
pfree(afterTriggers->state);
|
|
afterTriggers->state = state;
|
|
}
|
|
/* this avoids double pfree if error later: */
|
|
afterTriggers->state_stack[my_level] = NULL;
|
|
|
|
/*
|
|
* Scan for any remaining deferred events that were marked DONE or IN
|
|
* PROGRESS by this subxact or a child, and un-mark them. We can
|
|
* recognize such events because they have a firing ID greater than or
|
|
* equal to the firing_counter value we saved at subtransaction start.
|
|
* (This essentially assumes that the current subxact includes all
|
|
* subxacts started after it.)
|
|
*/
|
|
subxact_firing_id = afterTriggers->firing_stack[my_level];
|
|
for (event = afterTriggers->events.head;
|
|
event != NULL;
|
|
event = event->ate_next)
|
|
{
|
|
if (event->ate_event &
|
|
(AFTER_TRIGGER_DONE | AFTER_TRIGGER_IN_PROGRESS))
|
|
{
|
|
if (event->ate_firing_id >= subxact_firing_id)
|
|
event->ate_event &=
|
|
~(AFTER_TRIGGER_DONE | AFTER_TRIGGER_IN_PROGRESS);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Create an empty SetConstraintState with room for numalloc trigstates
|
|
*/
|
|
static SetConstraintState
|
|
SetConstraintStateCreate(int numalloc)
|
|
{
|
|
SetConstraintState state;
|
|
|
|
/* Behave sanely with numalloc == 0 */
|
|
if (numalloc <= 0)
|
|
numalloc = 1;
|
|
|
|
/*
|
|
* We assume that zeroing will correctly initialize the state values.
|
|
*/
|
|
state = (SetConstraintState)
|
|
MemoryContextAllocZero(TopTransactionContext,
|
|
sizeof(SetConstraintStateData) +
|
|
(numalloc - 1) *sizeof(SetConstraintTriggerData));
|
|
|
|
state->numalloc = numalloc;
|
|
|
|
return state;
|
|
}
|
|
|
|
/*
|
|
* Copy a SetConstraintState
|
|
*/
|
|
static SetConstraintState
|
|
SetConstraintStateCopy(SetConstraintState origstate)
|
|
{
|
|
SetConstraintState state;
|
|
|
|
state = SetConstraintStateCreate(origstate->numstates);
|
|
|
|
state->all_isset = origstate->all_isset;
|
|
state->all_isdeferred = origstate->all_isdeferred;
|
|
state->numstates = origstate->numstates;
|
|
memcpy(state->trigstates, origstate->trigstates,
|
|
origstate->numstates * sizeof(SetConstraintTriggerData));
|
|
|
|
return state;
|
|
}
|
|
|
|
/*
|
|
* Add a per-trigger item to a SetConstraintState. Returns possibly-changed
|
|
* pointer to the state object (it will change if we have to repalloc).
|
|
*/
|
|
static SetConstraintState
|
|
SetConstraintStateAddItem(SetConstraintState state,
|
|
Oid tgoid, bool tgisdeferred)
|
|
{
|
|
if (state->numstates >= state->numalloc)
|
|
{
|
|
int newalloc = state->numalloc * 2;
|
|
|
|
newalloc = Max(newalloc, 8); /* in case original has size 0 */
|
|
state = (SetConstraintState)
|
|
repalloc(state,
|
|
sizeof(SetConstraintStateData) +
|
|
(newalloc - 1) *sizeof(SetConstraintTriggerData));
|
|
state->numalloc = newalloc;
|
|
Assert(state->numstates < state->numalloc);
|
|
}
|
|
|
|
state->trigstates[state->numstates].sct_tgoid = tgoid;
|
|
state->trigstates[state->numstates].sct_tgisdeferred = tgisdeferred;
|
|
state->numstates++;
|
|
|
|
return state;
|
|
}
|
|
|
|
/* ----------
|
|
* AfterTriggerSetState()
|
|
*
|
|
* Execute the SET CONSTRAINTS ... utility command.
|
|
* ----------
|
|
*/
|
|
void
|
|
AfterTriggerSetState(ConstraintsSetStmt *stmt)
|
|
{
|
|
int my_level = GetCurrentTransactionNestLevel();
|
|
|
|
/*
|
|
* Ignore call if we aren't in a transaction. (Shouldn't happen?)
|
|
*/
|
|
if (afterTriggers == NULL)
|
|
return;
|
|
|
|
/*
|
|
* If in a subtransaction, and we didn't save the current state already,
|
|
* save it so it can be restored if the subtransaction aborts.
|
|
*/
|
|
if (my_level > 1 &&
|
|
afterTriggers->state_stack[my_level] == NULL)
|
|
{
|
|
afterTriggers->state_stack[my_level] =
|
|
SetConstraintStateCopy(afterTriggers->state);
|
|
}
|
|
|
|
/*
|
|
* Handle SET CONSTRAINTS ALL ...
|
|
*/
|
|
if (stmt->constraints == NIL)
|
|
{
|
|
/*
|
|
* Forget any previous SET CONSTRAINTS commands in this transaction.
|
|
*/
|
|
afterTriggers->state->numstates = 0;
|
|
|
|
/*
|
|
* Set the per-transaction ALL state to known.
|
|
*/
|
|
afterTriggers->state->all_isset = true;
|
|
afterTriggers->state->all_isdeferred = stmt->deferred;
|
|
}
|
|
else
|
|
{
|
|
Relation tgrel;
|
|
ListCell *l;
|
|
List *oidlist = NIL;
|
|
|
|
/* ----------
|
|
* Handle SET CONSTRAINTS constraint-name [, ...]
|
|
* First lookup all trigger Oid's for the constraint names.
|
|
* ----------
|
|
*/
|
|
tgrel = heap_open(TriggerRelationId, AccessShareLock);
|
|
|
|
foreach(l, stmt->constraints)
|
|
{
|
|
RangeVar *constraint = lfirst(l);
|
|
ScanKeyData skey;
|
|
SysScanDesc tgscan;
|
|
HeapTuple htup;
|
|
bool found;
|
|
List *namespaceSearchList;
|
|
ListCell *namespaceSearchCell;
|
|
|
|
if (constraint->catalogname)
|
|
{
|
|
if (strcmp(constraint->catalogname, get_database_name(MyDatabaseId)) != 0)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
|
|
errmsg("cross-database references are not implemented: \"%s.%s.%s\"",
|
|
constraint->catalogname, constraint->schemaname,
|
|
constraint->relname)));
|
|
}
|
|
|
|
/*
|
|
* If we're given the schema name with the constraint, look only
|
|
* in that schema. If given a bare constraint name, use the
|
|
* search path to find the first matching constraint.
|
|
*/
|
|
if (constraint->schemaname)
|
|
{
|
|
Oid namespaceId = LookupExplicitNamespace(constraint->schemaname);
|
|
|
|
namespaceSearchList = list_make1_oid(namespaceId);
|
|
}
|
|
else
|
|
{
|
|
namespaceSearchList = fetch_search_path(true);
|
|
}
|
|
|
|
found = false;
|
|
foreach(namespaceSearchCell, namespaceSearchList)
|
|
{
|
|
Oid searchNamespaceId = lfirst_oid(namespaceSearchCell);
|
|
|
|
/*
|
|
* Setup to scan pg_trigger by tgconstrname ...
|
|
*/
|
|
ScanKeyInit(&skey,
|
|
Anum_pg_trigger_tgconstrname,
|
|
BTEqualStrategyNumber, F_NAMEEQ,
|
|
PointerGetDatum(constraint->relname));
|
|
|
|
tgscan = systable_beginscan(tgrel, TriggerConstrNameIndexId, true,
|
|
SnapshotNow, 1, &skey);
|
|
|
|
/*
|
|
* ... and search for the constraint trigger row
|
|
*/
|
|
while (HeapTupleIsValid(htup = systable_getnext(tgscan)))
|
|
{
|
|
Form_pg_trigger pg_trigger = (Form_pg_trigger) GETSTRUCT(htup);
|
|
Oid constraintNamespaceId;
|
|
|
|
/*
|
|
* Foreign key constraints have triggers on both the
|
|
* parent and child tables. Since these tables may be in
|
|
* different schemas we must pick the child table because
|
|
* that table "owns" the constraint.
|
|
*
|
|
* Referential triggers on the parent table other than
|
|
* NOACTION_DEL and NOACTION_UPD are ignored below, so it
|
|
* is possible to not check them here, but it seems safer
|
|
* to always check.
|
|
*/
|
|
if (pg_trigger->tgfoid == F_RI_FKEY_NOACTION_DEL ||
|
|
pg_trigger->tgfoid == F_RI_FKEY_NOACTION_UPD ||
|
|
pg_trigger->tgfoid == F_RI_FKEY_RESTRICT_UPD ||
|
|
pg_trigger->tgfoid == F_RI_FKEY_RESTRICT_DEL ||
|
|
pg_trigger->tgfoid == F_RI_FKEY_CASCADE_UPD ||
|
|
pg_trigger->tgfoid == F_RI_FKEY_CASCADE_DEL ||
|
|
pg_trigger->tgfoid == F_RI_FKEY_SETNULL_UPD ||
|
|
pg_trigger->tgfoid == F_RI_FKEY_SETNULL_DEL ||
|
|
pg_trigger->tgfoid == F_RI_FKEY_SETDEFAULT_UPD ||
|
|
pg_trigger->tgfoid == F_RI_FKEY_SETDEFAULT_DEL)
|
|
constraintNamespaceId = get_rel_namespace(pg_trigger->tgconstrrelid);
|
|
else
|
|
constraintNamespaceId = get_rel_namespace(pg_trigger->tgrelid);
|
|
|
|
/*
|
|
* If this constraint is not in the schema we're currently
|
|
* searching for, keep looking.
|
|
*/
|
|
if (constraintNamespaceId != searchNamespaceId)
|
|
continue;
|
|
|
|
/*
|
|
* If we found some, check that they fit the deferrability
|
|
* but skip referential action ones, since they are
|
|
* silently never deferrable.
|
|
*/
|
|
if (pg_trigger->tgfoid != F_RI_FKEY_RESTRICT_UPD &&
|
|
pg_trigger->tgfoid != F_RI_FKEY_RESTRICT_DEL &&
|
|
pg_trigger->tgfoid != F_RI_FKEY_CASCADE_UPD &&
|
|
pg_trigger->tgfoid != F_RI_FKEY_CASCADE_DEL &&
|
|
pg_trigger->tgfoid != F_RI_FKEY_SETNULL_UPD &&
|
|
pg_trigger->tgfoid != F_RI_FKEY_SETNULL_DEL &&
|
|
pg_trigger->tgfoid != F_RI_FKEY_SETDEFAULT_UPD &&
|
|
pg_trigger->tgfoid != F_RI_FKEY_SETDEFAULT_DEL)
|
|
{
|
|
if (stmt->deferred && !pg_trigger->tgdeferrable)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
|
|
errmsg("constraint \"%s\" is not deferrable",
|
|
constraint->relname)));
|
|
oidlist = lappend_oid(oidlist, HeapTupleGetOid(htup));
|
|
}
|
|
found = true;
|
|
}
|
|
|
|
systable_endscan(tgscan);
|
|
|
|
/*
|
|
* Once we've found a matching constraint we do not search
|
|
* later parts of the search path.
|
|
*/
|
|
if (found)
|
|
break;
|
|
|
|
}
|
|
|
|
list_free(namespaceSearchList);
|
|
|
|
/*
|
|
* Not found ?
|
|
*/
|
|
if (!found)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_UNDEFINED_OBJECT),
|
|
errmsg("constraint \"%s\" does not exist",
|
|
constraint->relname)));
|
|
}
|
|
heap_close(tgrel, AccessShareLock);
|
|
|
|
/*
|
|
* Set the trigger states of individual triggers for this xact.
|
|
*/
|
|
foreach(l, oidlist)
|
|
{
|
|
Oid tgoid = lfirst_oid(l);
|
|
SetConstraintState state = afterTriggers->state;
|
|
bool found = false;
|
|
int i;
|
|
|
|
for (i = 0; i < state->numstates; i++)
|
|
{
|
|
if (state->trigstates[i].sct_tgoid == tgoid)
|
|
{
|
|
state->trigstates[i].sct_tgisdeferred = stmt->deferred;
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!found)
|
|
{
|
|
afterTriggers->state =
|
|
SetConstraintStateAddItem(state, tgoid, stmt->deferred);
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* SQL99 requires that when a constraint is set to IMMEDIATE, any deferred
|
|
* checks against that constraint must be made when the SET CONSTRAINTS
|
|
* command is executed -- i.e. the effects of the SET CONSTRAINTS command
|
|
* apply retroactively. We've updated the constraints state, so scan the
|
|
* list of previously deferred events to fire any that have now become
|
|
* immediate.
|
|
*
|
|
* Obviously, if this was SET ... DEFERRED then it can't have converted
|
|
* any unfired events to immediate, so we need do nothing in that case.
|
|
*/
|
|
if (!stmt->deferred)
|
|
{
|
|
AfterTriggerEventList *events = &afterTriggers->events;
|
|
|
|
while (afterTriggerMarkEvents(events, NULL, true))
|
|
{
|
|
CommandId firing_id = afterTriggers->firing_counter++;
|
|
|
|
/*
|
|
* We can delete fired events if we are at top transaction level,
|
|
* but we'd better not if inside a subtransaction, since the
|
|
* subtransaction could later get rolled back.
|
|
*/
|
|
afterTriggerInvokeEvents(events, firing_id, NULL,
|
|
!IsSubTransaction());
|
|
}
|
|
}
|
|
}
|
|
|
|
/* ----------
|
|
* AfterTriggerPendingOnRel()
|
|
* Test to see if there are any pending after-trigger events for rel.
|
|
*
|
|
* This is used by TRUNCATE, CLUSTER, ALTER TABLE, etc to detect whether
|
|
* it is unsafe to perform major surgery on a relation. Note that only
|
|
* local pending events are examined. We assume that having exclusive lock
|
|
* on a rel guarantees there are no unserviced events in other backends ---
|
|
* but having a lock does not prevent there being such events in our own.
|
|
*
|
|
* In some scenarios it'd be reasonable to remove pending events (more
|
|
* specifically, mark them DONE by the current subxact) but without a lot
|
|
* of knowledge of the trigger semantics we can't do this in general.
|
|
* ----------
|
|
*/
|
|
bool
|
|
AfterTriggerPendingOnRel(Oid relid)
|
|
{
|
|
AfterTriggerEvent event;
|
|
int depth;
|
|
|
|
/* No-op if we aren't in a transaction. (Shouldn't happen?) */
|
|
if (afterTriggers == NULL)
|
|
return false;
|
|
|
|
/* Scan queued events */
|
|
for (event = afterTriggers->events.head;
|
|
event != NULL;
|
|
event = event->ate_next)
|
|
{
|
|
/*
|
|
* We can ignore completed events. (Even if a DONE flag is rolled
|
|
* back by subxact abort, it's OK because the effects of the TRUNCATE
|
|
* or whatever must get rolled back too.)
|
|
*/
|
|
if (event->ate_event & AFTER_TRIGGER_DONE)
|
|
continue;
|
|
|
|
if (event->ate_relid == relid)
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
* Also scan events queued by incomplete queries. This could only matter
|
|
* if TRUNCATE/etc is executed by a function or trigger within an updating
|
|
* query on the same relation, which is pretty perverse, but let's check.
|
|
*/
|
|
for (depth = 0; depth <= afterTriggers->query_depth; depth++)
|
|
{
|
|
for (event = afterTriggers->query_stack[depth].head;
|
|
event != NULL;
|
|
event = event->ate_next)
|
|
{
|
|
if (event->ate_event & AFTER_TRIGGER_DONE)
|
|
continue;
|
|
|
|
if (event->ate_relid == relid)
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
/* ----------
|
|
* AfterTriggerSaveEvent()
|
|
*
|
|
* Called by ExecA[RS]...Triggers() to add the event to the queue.
|
|
*
|
|
* NOTE: should be called only if we've determined that an event must
|
|
* be added to the queue.
|
|
* ----------
|
|
*/
|
|
static void
|
|
AfterTriggerSaveEvent(ResultRelInfo *relinfo, int event, bool row_trigger,
|
|
HeapTuple oldtup, HeapTuple newtup)
|
|
{
|
|
Relation rel = relinfo->ri_RelationDesc;
|
|
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
|
|
int my_level = GetCurrentTransactionNestLevel();
|
|
MemoryContext *cxtptr;
|
|
AfterTriggerEvent new_event;
|
|
int i;
|
|
int ntriggers;
|
|
int *tgindx;
|
|
ItemPointerData oldctid;
|
|
ItemPointerData newctid;
|
|
|
|
if (afterTriggers == NULL)
|
|
elog(ERROR, "AfterTriggerSaveEvent() called outside of transaction");
|
|
|
|
/*
|
|
* event is used both as a bitmask and an array offset,
|
|
* so make sure we don't walk off the edge of our arrays
|
|
*/
|
|
Assert(event >= 0 && event < TRIGGER_NUM_EVENT_CLASSES);
|
|
|
|
/*
|
|
* Get the CTID's of OLD and NEW
|
|
*/
|
|
if (oldtup != NULL)
|
|
ItemPointerCopy(&(oldtup->t_self), &(oldctid));
|
|
else
|
|
ItemPointerSetInvalid(&(oldctid));
|
|
if (newtup != NULL)
|
|
ItemPointerCopy(&(newtup->t_self), &(newctid));
|
|
else
|
|
ItemPointerSetInvalid(&(newctid));
|
|
|
|
/*
|
|
* Scan the appropriate set of triggers
|
|
*/
|
|
if (row_trigger)
|
|
{
|
|
ntriggers = trigdesc->n_after_row[event];
|
|
tgindx = trigdesc->tg_after_row[event];
|
|
}
|
|
else
|
|
{
|
|
ntriggers = trigdesc->n_after_statement[event];
|
|
tgindx = trigdesc->tg_after_statement[event];
|
|
}
|
|
|
|
for (i = 0; i < ntriggers; i++)
|
|
{
|
|
Trigger *trigger = &trigdesc->triggers[tgindx[i]];
|
|
|
|
/* Ignore disabled triggers */
|
|
if (SessionReplicationRole == SESSION_REPLICATION_ROLE_REPLICA)
|
|
{
|
|
if (trigger->tgenabled == TRIGGER_FIRES_ON_ORIGIN ||
|
|
trigger->tgenabled == TRIGGER_DISABLED)
|
|
continue;
|
|
}
|
|
else /* ORIGIN or LOCAL role */
|
|
{
|
|
if (trigger->tgenabled == TRIGGER_FIRES_ON_REPLICA ||
|
|
trigger->tgenabled == TRIGGER_DISABLED)
|
|
continue;
|
|
}
|
|
|
|
/*
|
|
* If this is an UPDATE of a PK table or FK table that does not change
|
|
* the PK or FK respectively, we can skip queuing the event: there is
|
|
* no need to fire the trigger.
|
|
*/
|
|
if ((event & TRIGGER_EVENT_OPMASK) == TRIGGER_EVENT_UPDATE)
|
|
{
|
|
switch (RI_FKey_trigger_type(trigger->tgfoid))
|
|
{
|
|
case RI_TRIGGER_PK:
|
|
/* Update on PK table */
|
|
if (RI_FKey_keyequal_upd_pk(trigger, rel, oldtup, newtup))
|
|
{
|
|
/* key unchanged, so skip queuing this event */
|
|
continue;
|
|
}
|
|
break;
|
|
|
|
case RI_TRIGGER_FK:
|
|
|
|
/*
|
|
* Update on FK table
|
|
*
|
|
* There is one exception when updating FK tables: if the
|
|
* updated row was inserted by our own transaction and the
|
|
* FK is deferred, we still need to fire the trigger. This
|
|
* is because our UPDATE will invalidate the INSERT so the
|
|
* end-of-transaction INSERT RI trigger will not do
|
|
* anything, so we have to do the check for the UPDATE
|
|
* anyway.
|
|
*/
|
|
if (!TransactionIdIsCurrentTransactionId(HeapTupleHeaderGetXmin(oldtup->t_data)) &&
|
|
RI_FKey_keyequal_upd_fk(trigger, rel, oldtup, newtup))
|
|
{
|
|
continue;
|
|
}
|
|
break;
|
|
|
|
case RI_TRIGGER_NONE:
|
|
/* Not an FK trigger */
|
|
break;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* If we don't yet have an event context for the current (sub)xact,
|
|
* create one. Make it a child of CurTransactionContext to ensure it
|
|
* will go away if the subtransaction aborts.
|
|
*/
|
|
if (my_level > 1) /* subtransaction? */
|
|
{
|
|
Assert(my_level < afterTriggers->maxtransdepth);
|
|
cxtptr = &afterTriggers->cxt_stack[my_level];
|
|
}
|
|
else
|
|
cxtptr = &afterTriggers->event_cxt;
|
|
if (*cxtptr == NULL)
|
|
*cxtptr = AllocSetContextCreate(CurTransactionContext,
|
|
"AfterTriggerEvents",
|
|
ALLOCSET_DEFAULT_MINSIZE,
|
|
ALLOCSET_DEFAULT_INITSIZE,
|
|
ALLOCSET_DEFAULT_MAXSIZE);
|
|
|
|
/*
|
|
* Create a new event.
|
|
*/
|
|
new_event = (AfterTriggerEvent)
|
|
MemoryContextAlloc(*cxtptr, sizeof(AfterTriggerEventData));
|
|
new_event->ate_next = NULL;
|
|
new_event->ate_event =
|
|
(event & TRIGGER_EVENT_OPMASK) |
|
|
(row_trigger ? TRIGGER_EVENT_ROW : 0) |
|
|
(trigger->tgdeferrable ? AFTER_TRIGGER_DEFERRABLE : 0) |
|
|
(trigger->tginitdeferred ? AFTER_TRIGGER_INITDEFERRED : 0);
|
|
new_event->ate_firing_id = 0;
|
|
new_event->ate_tgoid = trigger->tgoid;
|
|
new_event->ate_relid = rel->rd_id;
|
|
ItemPointerCopy(&oldctid, &(new_event->ate_oldctid));
|
|
ItemPointerCopy(&newctid, &(new_event->ate_newctid));
|
|
|
|
/*
|
|
* Add the new event to the queue.
|
|
*/
|
|
afterTriggerAddEvent(new_event);
|
|
}
|
|
}
|