mirror of
https://git.postgresql.org/git/postgresql.git
synced 2024-09-05 21:29:20 +02:00
As noted by Justin Pryzby, just I forgot to commit locally before creating a patch file. Discussion: https://postgr.es/m/20220901053146.GI31833@telsasoft.com
6510 lines
200 KiB
C
6510 lines
200 KiB
C
/*-------------------------------------------------------------------------
|
|
*
|
|
* trigger.c
|
|
* PostgreSQL TRIGGERs support code.
|
|
*
|
|
* Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
|
|
* Portions Copyright (c) 1994, Regents of the University of California
|
|
*
|
|
* IDENTIFICATION
|
|
* src/backend/commands/trigger.c
|
|
*
|
|
*-------------------------------------------------------------------------
|
|
*/
|
|
#include "postgres.h"
|
|
|
|
#include "access/genam.h"
|
|
#include "access/htup_details.h"
|
|
#include "access/relation.h"
|
|
#include "access/sysattr.h"
|
|
#include "access/table.h"
|
|
#include "access/tableam.h"
|
|
#include "access/xact.h"
|
|
#include "catalog/catalog.h"
|
|
#include "catalog/dependency.h"
|
|
#include "catalog/index.h"
|
|
#include "catalog/indexing.h"
|
|
#include "catalog/objectaccess.h"
|
|
#include "catalog/partition.h"
|
|
#include "catalog/pg_constraint.h"
|
|
#include "catalog/pg_inherits.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/execPartition.h"
|
|
#include "miscadmin.h"
|
|
#include "nodes/bitmapset.h"
|
|
#include "nodes/makefuncs.h"
|
|
#include "optimizer/optimizer.h"
|
|
#include "parser/parse_clause.h"
|
|
#include "parser/parse_collate.h"
|
|
#include "parser/parse_func.h"
|
|
#include "parser/parse_relation.h"
|
|
#include "parser/parsetree.h"
|
|
#include "partitioning/partdesc.h"
|
|
#include "pgstat.h"
|
|
#include "rewrite/rewriteManip.h"
|
|
#include "storage/bufmgr.h"
|
|
#include "storage/lmgr.h"
|
|
#include "tcop/utility.h"
|
|
#include "utils/acl.h"
|
|
#include "utils/builtins.h"
|
|
#include "utils/bytea.h"
|
|
#include "utils/fmgroids.h"
|
|
#include "utils/inval.h"
|
|
#include "utils/lsyscache.h"
|
|
#include "utils/memutils.h"
|
|
#include "utils/rel.h"
|
|
#include "utils/snapmgr.h"
|
|
#include "utils/syscache.h"
|
|
#include "utils/tuplestore.h"
|
|
|
|
|
|
/* GUC variables */
|
|
int SessionReplicationRole = SESSION_REPLICATION_ROLE_ORIGIN;
|
|
|
|
/* How many levels deep into trigger execution are we? */
|
|
static int MyTriggerDepth = 0;
|
|
|
|
/* Local function prototypes */
|
|
static void renametrig_internal(Relation tgrel, Relation targetrel,
|
|
HeapTuple trigtup, const char *newname,
|
|
const char *expected_name);
|
|
static void renametrig_partition(Relation tgrel, Oid partitionId,
|
|
Oid parentTriggerOid, const char *newname,
|
|
const char *expected_name);
|
|
static void SetTriggerFlags(TriggerDesc *trigdesc, Trigger *trigger);
|
|
static bool GetTupleForTrigger(EState *estate,
|
|
EPQState *epqstate,
|
|
ResultRelInfo *relinfo,
|
|
ItemPointer tid,
|
|
LockTupleMode lockmode,
|
|
TupleTableSlot *oldslot,
|
|
TupleTableSlot **newSlot,
|
|
TM_FailureData *tmfpd);
|
|
static bool TriggerEnabled(EState *estate, ResultRelInfo *relinfo,
|
|
Trigger *trigger, TriggerEvent event,
|
|
Bitmapset *modifiedCols,
|
|
TupleTableSlot *oldslot, TupleTableSlot *newslot);
|
|
static HeapTuple ExecCallTriggerFunc(TriggerData *trigdata,
|
|
int tgindx,
|
|
FmgrInfo *finfo,
|
|
Instrumentation *instr,
|
|
MemoryContext per_tuple_context);
|
|
static void AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
|
|
ResultRelInfo *src_partinfo,
|
|
ResultRelInfo *dst_partinfo,
|
|
int event, bool row_trigger,
|
|
TupleTableSlot *oldtup, TupleTableSlot *newtup,
|
|
List *recheckIndexes, Bitmapset *modifiedCols,
|
|
TransitionCaptureState *transition_capture,
|
|
bool is_crosspart_update);
|
|
static void AfterTriggerEnlargeQueryState(void);
|
|
static bool before_stmt_triggers_fired(Oid relid, CmdType cmdType);
|
|
|
|
|
|
/*
|
|
* Create a trigger. Returns the address of the created trigger.
|
|
*
|
|
* queryString is the source text of the CREATE TRIGGER command.
|
|
* This must be supplied if a whenClause is specified, else it can be NULL.
|
|
*
|
|
* relOid, if nonzero, is the relation on which the trigger should be
|
|
* created. If zero, the name provided in the statement will be looked up.
|
|
*
|
|
* refRelOid, if nonzero, is the relation to which the constraint trigger
|
|
* refers. If zero, the constraint relation name provided in the statement
|
|
* will be looked up as needed.
|
|
*
|
|
* 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. (For CREATE CONSTRAINT
|
|
* TRIGGER, we build a pg_constraint entry internally.)
|
|
*
|
|
* indexOid, if nonzero, is the OID of an index associated with the constraint.
|
|
* We do nothing with this except store it into pg_trigger.tgconstrindid;
|
|
* but when creating a trigger for a deferrable unique constraint on a
|
|
* partitioned table, its children are looked up. Note we don't cope with
|
|
* invalid indexes in that case.
|
|
*
|
|
* funcoid, if nonzero, is the OID of the function to invoke. When this is
|
|
* given, stmt->funcname is ignored.
|
|
*
|
|
* parentTriggerOid, if nonzero, is a trigger that begets this one; so that
|
|
* if that trigger is dropped, this one should be too. There are two cases
|
|
* when a nonzero value is passed for this: 1) when this function recurses to
|
|
* create the trigger on partitions, 2) when creating child foreign key
|
|
* triggers; see CreateFKCheckTrigger() and createForeignKeyActionTriggers().
|
|
*
|
|
* If whenClause is passed, it is an already-transformed expression for
|
|
* WHEN. In this case, we ignore any that may come in stmt->whenClause.
|
|
*
|
|
* If isInternal is true then this is an internally-generated trigger.
|
|
* This argument sets the tgisinternal field of the pg_trigger entry, and
|
|
* if true causes us to modify the given trigger name to ensure uniqueness.
|
|
*
|
|
* When isInternal is not true we require ACL_TRIGGER permissions on the
|
|
* relation, as well as ACL_EXECUTE on the trigger function. For internal
|
|
* triggers the caller must apply any required permission checks.
|
|
*
|
|
* When called on partitioned tables, this function recurses to create the
|
|
* trigger on all the partitions, except if isInternal is true, in which
|
|
* case caller is expected to execute recursion on its own. in_partition
|
|
* indicates such a recursive call; outside callers should pass "false"
|
|
* (but see CloneRowTriggersToPartition).
|
|
*/
|
|
ObjectAddress
|
|
CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
|
|
Oid relOid, Oid refRelOid, Oid constraintOid, Oid indexOid,
|
|
Oid funcoid, Oid parentTriggerOid, Node *whenClause,
|
|
bool isInternal, bool in_partition)
|
|
{
|
|
return
|
|
CreateTriggerFiringOn(stmt, queryString, relOid, refRelOid,
|
|
constraintOid, indexOid, funcoid,
|
|
parentTriggerOid, whenClause, isInternal,
|
|
in_partition, TRIGGER_FIRES_ON_ORIGIN);
|
|
}
|
|
|
|
/*
|
|
* Like the above; additionally the firing condition
|
|
* (always/origin/replica/disabled) can be specified.
|
|
*/
|
|
ObjectAddress
|
|
CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
|
|
Oid relOid, Oid refRelOid, Oid constraintOid,
|
|
Oid indexOid, Oid funcoid, Oid parentTriggerOid,
|
|
Node *whenClause, bool isInternal, bool in_partition,
|
|
char trigger_fires_when)
|
|
{
|
|
int16 tgtype;
|
|
int ncolumns;
|
|
int16 *columns;
|
|
int2vector *tgattr;
|
|
List *whenRtable;
|
|
char *qual;
|
|
Datum values[Natts_pg_trigger];
|
|
bool nulls[Natts_pg_trigger];
|
|
Relation rel;
|
|
AclResult aclresult;
|
|
Relation tgrel;
|
|
Relation pgrel;
|
|
HeapTuple tuple = NULL;
|
|
Oid funcrettype;
|
|
Oid trigoid = InvalidOid;
|
|
char internaltrigname[NAMEDATALEN];
|
|
char *trigname;
|
|
Oid constrrelid = InvalidOid;
|
|
ObjectAddress myself,
|
|
referenced;
|
|
char *oldtablename = NULL;
|
|
char *newtablename = NULL;
|
|
bool partition_recurse;
|
|
bool trigger_exists = false;
|
|
Oid existing_constraint_oid = InvalidOid;
|
|
bool existing_isInternal = false;
|
|
bool existing_isClone = false;
|
|
|
|
if (OidIsValid(relOid))
|
|
rel = table_open(relOid, ShareRowExclusiveLock);
|
|
else
|
|
rel = table_openrv(stmt->relation, ShareRowExclusiveLock);
|
|
|
|
/*
|
|
* Triggers must be on tables or views, and there are additional
|
|
* relation-type-specific restrictions.
|
|
*/
|
|
if (rel->rd_rel->relkind == RELKIND_RELATION)
|
|
{
|
|
/* Tables can't have INSTEAD OF triggers */
|
|
if (stmt->timing != TRIGGER_TYPE_BEFORE &&
|
|
stmt->timing != TRIGGER_TYPE_AFTER)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
|
|
errmsg("\"%s\" is a table",
|
|
RelationGetRelationName(rel)),
|
|
errdetail("Tables cannot have INSTEAD OF triggers.")));
|
|
}
|
|
else if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
|
|
{
|
|
/* Partitioned tables can't have INSTEAD OF triggers */
|
|
if (stmt->timing != TRIGGER_TYPE_BEFORE &&
|
|
stmt->timing != TRIGGER_TYPE_AFTER)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
|
|
errmsg("\"%s\" is a table",
|
|
RelationGetRelationName(rel)),
|
|
errdetail("Tables cannot have INSTEAD OF triggers.")));
|
|
|
|
/*
|
|
* FOR EACH ROW triggers have further restrictions
|
|
*/
|
|
if (stmt->row)
|
|
{
|
|
/*
|
|
* Disallow use of transition tables.
|
|
*
|
|
* Note that we have another restriction about transition tables
|
|
* in partitions; search for 'has_superclass' below for an
|
|
* explanation. The check here is just to protect from the fact
|
|
* that if we allowed it here, the creation would succeed for a
|
|
* partitioned table with no partitions, but would be blocked by
|
|
* the other restriction when the first partition was created,
|
|
* which is very unfriendly behavior.
|
|
*/
|
|
if (stmt->transitionRels != NIL)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
|
|
errmsg("\"%s\" is a partitioned table",
|
|
RelationGetRelationName(rel)),
|
|
errdetail("Triggers on partitioned tables cannot have transition tables.")));
|
|
}
|
|
}
|
|
else if (rel->rd_rel->relkind == RELKIND_VIEW)
|
|
{
|
|
/*
|
|
* Views can have INSTEAD OF triggers (which we check below are
|
|
* row-level), or statement-level BEFORE/AFTER triggers.
|
|
*/
|
|
if (stmt->timing != TRIGGER_TYPE_INSTEAD && stmt->row)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
|
|
errmsg("\"%s\" is a view",
|
|
RelationGetRelationName(rel)),
|
|
errdetail("Views cannot have row-level BEFORE or AFTER triggers.")));
|
|
/* Disallow TRUNCATE triggers on VIEWs */
|
|
if (TRIGGER_FOR_TRUNCATE(stmt->events))
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
|
|
errmsg("\"%s\" is a view",
|
|
RelationGetRelationName(rel)),
|
|
errdetail("Views cannot have TRUNCATE triggers.")));
|
|
}
|
|
else if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
|
|
{
|
|
if (stmt->timing != TRIGGER_TYPE_BEFORE &&
|
|
stmt->timing != TRIGGER_TYPE_AFTER)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
|
|
errmsg("\"%s\" is a foreign table",
|
|
RelationGetRelationName(rel)),
|
|
errdetail("Foreign tables cannot have INSTEAD OF triggers.")));
|
|
|
|
/*
|
|
* We disallow constraint triggers to protect the assumption that
|
|
* triggers on FKs can't be deferred. See notes with AfterTriggers
|
|
* data structures, below.
|
|
*/
|
|
if (stmt->isconstraint)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
|
|
errmsg("\"%s\" is a foreign table",
|
|
RelationGetRelationName(rel)),
|
|
errdetail("Foreign tables cannot have constraint triggers.")));
|
|
}
|
|
else
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
|
|
errmsg("relation \"%s\" cannot have triggers",
|
|
RelationGetRelationName(rel)),
|
|
errdetail_relkind_not_supported(rel->rd_rel->relkind)));
|
|
|
|
if (!allowSystemTableMods && IsSystemRelation(rel))
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
|
|
errmsg("permission denied: \"%s\" is a system catalog",
|
|
RelationGetRelationName(rel))));
|
|
|
|
if (stmt->isconstraint)
|
|
{
|
|
/*
|
|
* We must take a lock on the target relation to protect against
|
|
* concurrent drop. It's not clear that AccessShareLock is strong
|
|
* enough, but we certainly need at least that much... otherwise, we
|
|
* might end up creating a pg_constraint entry referencing a
|
|
* nonexistent table.
|
|
*/
|
|
if (OidIsValid(refRelOid))
|
|
{
|
|
LockRelationOid(refRelOid, AccessShareLock);
|
|
constrrelid = refRelOid;
|
|
}
|
|
else if (stmt->constrrel != NULL)
|
|
constrrelid = RangeVarGetRelid(stmt->constrrel, AccessShareLock,
|
|
false);
|
|
}
|
|
|
|
/* permission checks */
|
|
if (!isInternal)
|
|
{
|
|
aclresult = pg_class_aclcheck(RelationGetRelid(rel), GetUserId(),
|
|
ACL_TRIGGER);
|
|
if (aclresult != ACLCHECK_OK)
|
|
aclcheck_error(aclresult, get_relkind_objtype(rel->rd_rel->relkind),
|
|
RelationGetRelationName(rel));
|
|
|
|
if (OidIsValid(constrrelid))
|
|
{
|
|
aclresult = pg_class_aclcheck(constrrelid, GetUserId(),
|
|
ACL_TRIGGER);
|
|
if (aclresult != ACLCHECK_OK)
|
|
aclcheck_error(aclresult, get_relkind_objtype(get_rel_relkind(constrrelid)),
|
|
get_rel_name(constrrelid));
|
|
}
|
|
}
|
|
|
|
/*
|
|
* When called on a partitioned table to create a FOR EACH ROW trigger
|
|
* that's not internal, we create one trigger for each partition, too.
|
|
*
|
|
* For that, we'd better hold lock on all of them ahead of time.
|
|
*/
|
|
partition_recurse = !isInternal && stmt->row &&
|
|
rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE;
|
|
if (partition_recurse)
|
|
list_free(find_all_inheritors(RelationGetRelid(rel),
|
|
ShareRowExclusiveLock, NULL));
|
|
|
|
/* Compute tgtype */
|
|
TRIGGER_CLEAR_TYPE(tgtype);
|
|
if (stmt->row)
|
|
TRIGGER_SETT_ROW(tgtype);
|
|
tgtype |= stmt->timing;
|
|
tgtype |= stmt->events;
|
|
|
|
/* Disallow ROW-level TRUNCATE triggers */
|
|
if (TRIGGER_FOR_ROW(tgtype) && TRIGGER_FOR_TRUNCATE(tgtype))
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
|
|
errmsg("TRUNCATE FOR EACH ROW triggers are not supported")));
|
|
|
|
/* INSTEAD triggers must be row-level, and can't have WHEN or columns */
|
|
if (TRIGGER_FOR_INSTEAD(tgtype))
|
|
{
|
|
if (!TRIGGER_FOR_ROW(tgtype))
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
|
|
errmsg("INSTEAD OF triggers must be FOR EACH ROW")));
|
|
if (stmt->whenClause)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
|
|
errmsg("INSTEAD OF triggers cannot have WHEN conditions")));
|
|
if (stmt->columns != NIL)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
|
|
errmsg("INSTEAD OF triggers cannot have column lists")));
|
|
}
|
|
|
|
/*
|
|
* We don't yet support naming ROW transition variables, but the parser
|
|
* recognizes the syntax so we can give a nicer message here.
|
|
*
|
|
* Per standard, REFERENCING TABLE names are only allowed on AFTER
|
|
* triggers. Per standard, REFERENCING ROW names are not allowed with FOR
|
|
* EACH STATEMENT. Per standard, each OLD/NEW, ROW/TABLE permutation is
|
|
* only allowed once. Per standard, OLD may not be specified when
|
|
* creating a trigger only for INSERT, and NEW may not be specified when
|
|
* creating a trigger only for DELETE.
|
|
*
|
|
* Notice that the standard allows an AFTER ... FOR EACH ROW trigger to
|
|
* reference both ROW and TABLE transition data.
|
|
*/
|
|
if (stmt->transitionRels != NIL)
|
|
{
|
|
List *varList = stmt->transitionRels;
|
|
ListCell *lc;
|
|
|
|
foreach(lc, varList)
|
|
{
|
|
TriggerTransition *tt = lfirst_node(TriggerTransition, lc);
|
|
|
|
if (!(tt->isTable))
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
|
|
errmsg("ROW variable naming in the REFERENCING clause is not supported"),
|
|
errhint("Use OLD TABLE or NEW TABLE for naming transition tables.")));
|
|
|
|
/*
|
|
* Because of the above test, we omit further ROW-related testing
|
|
* below. If we later allow naming OLD and NEW ROW variables,
|
|
* adjustments will be needed below.
|
|
*/
|
|
|
|
if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
|
|
errmsg("\"%s\" is a foreign table",
|
|
RelationGetRelationName(rel)),
|
|
errdetail("Triggers on foreign tables cannot have transition tables.")));
|
|
|
|
if (rel->rd_rel->relkind == RELKIND_VIEW)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
|
|
errmsg("\"%s\" is a view",
|
|
RelationGetRelationName(rel)),
|
|
errdetail("Triggers on views cannot have transition tables.")));
|
|
|
|
/*
|
|
* We currently don't allow row-level triggers with transition
|
|
* tables on partition or inheritance children. Such triggers
|
|
* would somehow need to see tuples converted to the format of the
|
|
* table they're attached to, and it's not clear which subset of
|
|
* tuples each child should see. See also the prohibitions in
|
|
* ATExecAttachPartition() and ATExecAddInherit().
|
|
*/
|
|
if (TRIGGER_FOR_ROW(tgtype) && has_superclass(rel->rd_id))
|
|
{
|
|
/* Use appropriate error message. */
|
|
if (rel->rd_rel->relispartition)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
|
|
errmsg("ROW triggers with transition tables are not supported on partitions")));
|
|
else
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
|
|
errmsg("ROW triggers with transition tables are not supported on inheritance children")));
|
|
}
|
|
|
|
if (stmt->timing != TRIGGER_TYPE_AFTER)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
|
|
errmsg("transition table name can only be specified for an AFTER trigger")));
|
|
|
|
if (TRIGGER_FOR_TRUNCATE(tgtype))
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
|
|
errmsg("TRUNCATE triggers with transition tables are not supported")));
|
|
|
|
/*
|
|
* We currently don't allow multi-event triggers ("INSERT OR
|
|
* UPDATE") with transition tables, because it's not clear how to
|
|
* handle INSERT ... ON CONFLICT statements which can fire both
|
|
* INSERT and UPDATE triggers. We show the inserted tuples to
|
|
* INSERT triggers and the updated tuples to UPDATE triggers, but
|
|
* it's not yet clear what INSERT OR UPDATE trigger should see.
|
|
* This restriction could be lifted if we can decide on the right
|
|
* semantics in a later release.
|
|
*/
|
|
if (((TRIGGER_FOR_INSERT(tgtype) ? 1 : 0) +
|
|
(TRIGGER_FOR_UPDATE(tgtype) ? 1 : 0) +
|
|
(TRIGGER_FOR_DELETE(tgtype) ? 1 : 0)) != 1)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
|
|
errmsg("transition tables cannot be specified for triggers with more than one event")));
|
|
|
|
/*
|
|
* We currently don't allow column-specific triggers with
|
|
* transition tables. Per spec, that seems to require
|
|
* accumulating separate transition tables for each combination of
|
|
* columns, which is a lot of work for a rather marginal feature.
|
|
*/
|
|
if (stmt->columns != NIL)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
|
|
errmsg("transition tables cannot be specified for triggers with column lists")));
|
|
|
|
/*
|
|
* We disallow constraint triggers with transition tables, to
|
|
* protect the assumption that such triggers can't be deferred.
|
|
* See notes with AfterTriggers data structures, below.
|
|
*
|
|
* Currently this is enforced by the grammar, so just Assert here.
|
|
*/
|
|
Assert(!stmt->isconstraint);
|
|
|
|
if (tt->isNew)
|
|
{
|
|
if (!(TRIGGER_FOR_INSERT(tgtype) ||
|
|
TRIGGER_FOR_UPDATE(tgtype)))
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
|
|
errmsg("NEW TABLE can only be specified for an INSERT or UPDATE trigger")));
|
|
|
|
if (newtablename != NULL)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
|
|
errmsg("NEW TABLE cannot be specified multiple times")));
|
|
|
|
newtablename = tt->name;
|
|
}
|
|
else
|
|
{
|
|
if (!(TRIGGER_FOR_DELETE(tgtype) ||
|
|
TRIGGER_FOR_UPDATE(tgtype)))
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
|
|
errmsg("OLD TABLE can only be specified for a DELETE or UPDATE trigger")));
|
|
|
|
if (oldtablename != NULL)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
|
|
errmsg("OLD TABLE cannot be specified multiple times")));
|
|
|
|
oldtablename = tt->name;
|
|
}
|
|
}
|
|
|
|
if (newtablename != NULL && oldtablename != NULL &&
|
|
strcmp(newtablename, oldtablename) == 0)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
|
|
errmsg("OLD TABLE name and NEW TABLE name cannot be the same")));
|
|
}
|
|
|
|
/*
|
|
* Parse the WHEN clause, if any and we weren't passed an already
|
|
* transformed one.
|
|
*
|
|
* Note that as a side effect, we fill whenRtable when parsing. If we got
|
|
* an already parsed clause, this does not occur, which is what we want --
|
|
* no point in adding redundant dependencies below.
|
|
*/
|
|
if (!whenClause && stmt->whenClause)
|
|
{
|
|
ParseState *pstate;
|
|
ParseNamespaceItem *nsitem;
|
|
List *varList;
|
|
ListCell *lc;
|
|
|
|
/* Set up a pstate to parse with */
|
|
pstate = make_parsestate(NULL);
|
|
pstate->p_sourcetext = queryString;
|
|
|
|
/*
|
|
* Set up nsitems for OLD and NEW references.
|
|
*
|
|
* 'OLD' must always have varno equal to 1 and 'NEW' equal to 2.
|
|
*/
|
|
nsitem = addRangeTableEntryForRelation(pstate, rel,
|
|
AccessShareLock,
|
|
makeAlias("old", NIL),
|
|
false, false);
|
|
addNSItemToQuery(pstate, nsitem, false, true, true);
|
|
nsitem = addRangeTableEntryForRelation(pstate, rel,
|
|
AccessShareLock,
|
|
makeAlias("new", NIL),
|
|
false, false);
|
|
addNSItemToQuery(pstate, nsitem, false, true, true);
|
|
|
|
/* Transform expression. Copy to be sure we don't modify original */
|
|
whenClause = transformWhereClause(pstate,
|
|
copyObject(stmt->whenClause),
|
|
EXPR_KIND_TRIGGER_WHEN,
|
|
"WHEN");
|
|
/* we have to fix its collations too */
|
|
assign_expr_collations(pstate, whenClause);
|
|
|
|
/*
|
|
* Check for disallowed references to OLD/NEW.
|
|
*
|
|
* NB: pull_var_clause is okay here only because we don't allow
|
|
* subselects in WHEN clauses; it would fail to examine the contents
|
|
* of subselects.
|
|
*/
|
|
varList = pull_var_clause(whenClause, 0);
|
|
foreach(lc, varList)
|
|
{
|
|
Var *var = (Var *) lfirst(lc);
|
|
|
|
switch (var->varno)
|
|
{
|
|
case PRS2_OLD_VARNO:
|
|
if (!TRIGGER_FOR_ROW(tgtype))
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
|
|
errmsg("statement trigger's WHEN condition cannot reference column values"),
|
|
parser_errposition(pstate, var->location)));
|
|
if (TRIGGER_FOR_INSERT(tgtype))
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
|
|
errmsg("INSERT trigger's WHEN condition cannot reference OLD values"),
|
|
parser_errposition(pstate, var->location)));
|
|
/* system columns are okay here */
|
|
break;
|
|
case PRS2_NEW_VARNO:
|
|
if (!TRIGGER_FOR_ROW(tgtype))
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
|
|
errmsg("statement trigger's WHEN condition cannot reference column values"),
|
|
parser_errposition(pstate, var->location)));
|
|
if (TRIGGER_FOR_DELETE(tgtype))
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
|
|
errmsg("DELETE trigger's WHEN condition cannot reference NEW values"),
|
|
parser_errposition(pstate, var->location)));
|
|
if (var->varattno < 0 && TRIGGER_FOR_BEFORE(tgtype))
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
|
|
errmsg("BEFORE trigger's WHEN condition cannot reference NEW system columns"),
|
|
parser_errposition(pstate, var->location)));
|
|
if (TRIGGER_FOR_BEFORE(tgtype) &&
|
|
var->varattno == 0 &&
|
|
RelationGetDescr(rel)->constr &&
|
|
RelationGetDescr(rel)->constr->has_generated_stored)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
|
|
errmsg("BEFORE trigger's WHEN condition cannot reference NEW generated columns"),
|
|
errdetail("A whole-row reference is used and the table contains generated columns."),
|
|
parser_errposition(pstate, var->location)));
|
|
if (TRIGGER_FOR_BEFORE(tgtype) &&
|
|
var->varattno > 0 &&
|
|
TupleDescAttr(RelationGetDescr(rel), var->varattno - 1)->attgenerated)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
|
|
errmsg("BEFORE trigger's WHEN condition cannot reference NEW generated columns"),
|
|
errdetail("Column \"%s\" is a generated column.",
|
|
NameStr(TupleDescAttr(RelationGetDescr(rel), var->varattno - 1)->attname)),
|
|
parser_errposition(pstate, var->location)));
|
|
break;
|
|
default:
|
|
/* can't happen without add_missing_from, so just elog */
|
|
elog(ERROR, "trigger WHEN condition cannot contain references to other relations");
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* we'll need the rtable for recordDependencyOnExpr */
|
|
whenRtable = pstate->p_rtable;
|
|
|
|
qual = nodeToString(whenClause);
|
|
|
|
free_parsestate(pstate);
|
|
}
|
|
else if (!whenClause)
|
|
{
|
|
whenClause = NULL;
|
|
whenRtable = NIL;
|
|
qual = NULL;
|
|
}
|
|
else
|
|
{
|
|
qual = nodeToString(whenClause);
|
|
whenRtable = NIL;
|
|
}
|
|
|
|
/*
|
|
* Find and validate the trigger function.
|
|
*/
|
|
if (!OidIsValid(funcoid))
|
|
funcoid = LookupFuncName(stmt->funcname, 0, NULL, false);
|
|
if (!isInternal)
|
|
{
|
|
aclresult = pg_proc_aclcheck(funcoid, GetUserId(), ACL_EXECUTE);
|
|
if (aclresult != ACLCHECK_OK)
|
|
aclcheck_error(aclresult, OBJECT_FUNCTION,
|
|
NameListToString(stmt->funcname));
|
|
}
|
|
funcrettype = get_func_rettype(funcoid);
|
|
if (funcrettype != TRIGGEROID)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
|
|
errmsg("function %s must return type %s",
|
|
NameListToString(stmt->funcname), "trigger")));
|
|
|
|
/*
|
|
* Scan pg_trigger to see if there is already a trigger of the same name.
|
|
* Skip this for internally generated triggers, since we'll modify the
|
|
* name to be unique below.
|
|
*
|
|
* NOTE that this is cool only because we have ShareRowExclusiveLock on
|
|
* the relation, so the trigger set won't be changing underneath us.
|
|
*/
|
|
tgrel = table_open(TriggerRelationId, RowExclusiveLock);
|
|
if (!isInternal)
|
|
{
|
|
ScanKeyData skeys[2];
|
|
SysScanDesc tgscan;
|
|
|
|
ScanKeyInit(&skeys[0],
|
|
Anum_pg_trigger_tgrelid,
|
|
BTEqualStrategyNumber, F_OIDEQ,
|
|
ObjectIdGetDatum(RelationGetRelid(rel)));
|
|
|
|
ScanKeyInit(&skeys[1],
|
|
Anum_pg_trigger_tgname,
|
|
BTEqualStrategyNumber, F_NAMEEQ,
|
|
CStringGetDatum(stmt->trigname));
|
|
|
|
tgscan = systable_beginscan(tgrel, TriggerRelidNameIndexId, true,
|
|
NULL, 2, skeys);
|
|
|
|
/* There should be at most one matching tuple */
|
|
if (HeapTupleIsValid(tuple = systable_getnext(tgscan)))
|
|
{
|
|
Form_pg_trigger oldtrigger = (Form_pg_trigger) GETSTRUCT(tuple);
|
|
|
|
trigoid = oldtrigger->oid;
|
|
existing_constraint_oid = oldtrigger->tgconstraint;
|
|
existing_isInternal = oldtrigger->tgisinternal;
|
|
existing_isClone = OidIsValid(oldtrigger->tgparentid);
|
|
trigger_exists = true;
|
|
/* copy the tuple to use in CatalogTupleUpdate() */
|
|
tuple = heap_copytuple(tuple);
|
|
}
|
|
systable_endscan(tgscan);
|
|
}
|
|
|
|
if (!trigger_exists)
|
|
{
|
|
/* Generate the OID for the new trigger. */
|
|
trigoid = GetNewOidWithIndex(tgrel, TriggerOidIndexId,
|
|
Anum_pg_trigger_oid);
|
|
}
|
|
else
|
|
{
|
|
/*
|
|
* If OR REPLACE was specified, we'll replace the old trigger;
|
|
* otherwise complain about the duplicate name.
|
|
*/
|
|
if (!stmt->replace)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_DUPLICATE_OBJECT),
|
|
errmsg("trigger \"%s\" for relation \"%s\" already exists",
|
|
stmt->trigname, RelationGetRelationName(rel))));
|
|
|
|
/*
|
|
* An internal trigger or a child trigger (isClone) cannot be replaced
|
|
* by a user-defined trigger. However, skip this test when
|
|
* in_partition, because then we're recursing from a partitioned table
|
|
* and the check was made at the parent level.
|
|
*/
|
|
if ((existing_isInternal || existing_isClone) &&
|
|
!isInternal && !in_partition)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_DUPLICATE_OBJECT),
|
|
errmsg("trigger \"%s\" for relation \"%s\" is an internal or a child trigger",
|
|
stmt->trigname, RelationGetRelationName(rel))));
|
|
|
|
/*
|
|
* It is not allowed to replace with a constraint trigger; gram.y
|
|
* should have enforced this already.
|
|
*/
|
|
Assert(!stmt->isconstraint);
|
|
|
|
/*
|
|
* It is not allowed to replace an existing constraint trigger,
|
|
* either. (The reason for these restrictions is partly that it seems
|
|
* difficult to deal with pending trigger events in such cases, and
|
|
* partly that the command might imply changing the constraint's
|
|
* properties as well, which doesn't seem nice.)
|
|
*/
|
|
if (OidIsValid(existing_constraint_oid))
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_DUPLICATE_OBJECT),
|
|
errmsg("trigger \"%s\" for relation \"%s\" is a constraint trigger",
|
|
stmt->trigname, RelationGetRelationName(rel))));
|
|
}
|
|
|
|
/*
|
|
* If it's a user-entered CREATE CONSTRAINT TRIGGER command, make a
|
|
* corresponding pg_constraint entry.
|
|
*/
|
|
if (stmt->isconstraint && !OidIsValid(constraintOid))
|
|
{
|
|
/* Internal callers should have made their own constraints */
|
|
Assert(!isInternal);
|
|
constraintOid = CreateConstraintEntry(stmt->trigname,
|
|
RelationGetNamespace(rel),
|
|
CONSTRAINT_TRIGGER,
|
|
stmt->deferrable,
|
|
stmt->initdeferred,
|
|
true,
|
|
InvalidOid, /* no parent */
|
|
RelationGetRelid(rel),
|
|
NULL, /* no conkey */
|
|
0,
|
|
0,
|
|
InvalidOid, /* no domain */
|
|
InvalidOid, /* no index */
|
|
InvalidOid, /* no foreign key */
|
|
NULL,
|
|
NULL,
|
|
NULL,
|
|
NULL,
|
|
0,
|
|
' ',
|
|
' ',
|
|
NULL,
|
|
0,
|
|
' ',
|
|
NULL, /* no exclusion */
|
|
NULL, /* no check constraint */
|
|
NULL,
|
|
true, /* islocal */
|
|
0, /* inhcount */
|
|
true, /* noinherit */
|
|
isInternal); /* is_internal */
|
|
}
|
|
|
|
/*
|
|
* If trigger is internally generated, modify the provided trigger name to
|
|
* ensure uniqueness by appending the trigger OID. (Callers will usually
|
|
* supply a simple constant trigger name in these cases.)
|
|
*/
|
|
if (isInternal)
|
|
{
|
|
snprintf(internaltrigname, sizeof(internaltrigname),
|
|
"%s_%u", stmt->trigname, trigoid);
|
|
trigname = internaltrigname;
|
|
}
|
|
else
|
|
{
|
|
/* user-defined trigger; use the specified trigger name as-is */
|
|
trigname = stmt->trigname;
|
|
}
|
|
|
|
/*
|
|
* Build the new pg_trigger tuple.
|
|
*
|
|
* When we're creating a trigger in a partition, we mark it as internal,
|
|
* even though we don't do the isInternal magic in this function. This
|
|
* makes the triggers in partitions identical to the ones in the
|
|
* partitioned tables, except that they are marked internal.
|
|
*/
|
|
memset(nulls, false, sizeof(nulls));
|
|
|
|
values[Anum_pg_trigger_oid - 1] = ObjectIdGetDatum(trigoid);
|
|
values[Anum_pg_trigger_tgrelid - 1] = ObjectIdGetDatum(RelationGetRelid(rel));
|
|
values[Anum_pg_trigger_tgparentid - 1] = ObjectIdGetDatum(parentTriggerOid);
|
|
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] = trigger_fires_when;
|
|
values[Anum_pg_trigger_tgisinternal - 1] = BoolGetDatum(isInternal);
|
|
values[Anum_pg_trigger_tgconstrrelid - 1] = ObjectIdGetDatum(constrrelid);
|
|
values[Anum_pg_trigger_tgconstrindid - 1] = ObjectIdGetDatum(indexOid);
|
|
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(""));
|
|
}
|
|
|
|
/* build column number array if it's a column-specific trigger */
|
|
ncolumns = list_length(stmt->columns);
|
|
if (ncolumns == 0)
|
|
columns = NULL;
|
|
else
|
|
{
|
|
ListCell *cell;
|
|
int i = 0;
|
|
|
|
columns = (int16 *) palloc(ncolumns * sizeof(int16));
|
|
foreach(cell, stmt->columns)
|
|
{
|
|
char *name = strVal(lfirst(cell));
|
|
int16 attnum;
|
|
int j;
|
|
|
|
/* Lookup column name. System columns are not allowed */
|
|
attnum = attnameAttNum(rel, name, false);
|
|
if (attnum == InvalidAttrNumber)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_UNDEFINED_COLUMN),
|
|
errmsg("column \"%s\" of relation \"%s\" does not exist",
|
|
name, RelationGetRelationName(rel))));
|
|
|
|
/* Check for duplicates */
|
|
for (j = i - 1; j >= 0; j--)
|
|
{
|
|
if (columns[j] == attnum)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_DUPLICATE_COLUMN),
|
|
errmsg("column \"%s\" specified more than once",
|
|
name)));
|
|
}
|
|
|
|
columns[i++] = attnum;
|
|
}
|
|
}
|
|
tgattr = buildint2vector(columns, ncolumns);
|
|
values[Anum_pg_trigger_tgattr - 1] = PointerGetDatum(tgattr);
|
|
|
|
/* set tgqual if trigger has WHEN clause */
|
|
if (qual)
|
|
values[Anum_pg_trigger_tgqual - 1] = CStringGetTextDatum(qual);
|
|
else
|
|
nulls[Anum_pg_trigger_tgqual - 1] = true;
|
|
|
|
if (oldtablename)
|
|
values[Anum_pg_trigger_tgoldtable - 1] = DirectFunctionCall1(namein,
|
|
CStringGetDatum(oldtablename));
|
|
else
|
|
nulls[Anum_pg_trigger_tgoldtable - 1] = true;
|
|
if (newtablename)
|
|
values[Anum_pg_trigger_tgnewtable - 1] = DirectFunctionCall1(namein,
|
|
CStringGetDatum(newtablename));
|
|
else
|
|
nulls[Anum_pg_trigger_tgnewtable - 1] = true;
|
|
|
|
/*
|
|
* Insert or replace tuple in pg_trigger.
|
|
*/
|
|
if (!trigger_exists)
|
|
{
|
|
tuple = heap_form_tuple(tgrel->rd_att, values, nulls);
|
|
CatalogTupleInsert(tgrel, tuple);
|
|
}
|
|
else
|
|
{
|
|
HeapTuple newtup;
|
|
|
|
newtup = heap_form_tuple(tgrel->rd_att, values, nulls);
|
|
CatalogTupleUpdate(tgrel, &tuple->t_self, newtup);
|
|
heap_freetuple(newtup);
|
|
}
|
|
|
|
heap_freetuple(tuple); /* free either original or new tuple */
|
|
table_close(tgrel, RowExclusiveLock);
|
|
|
|
pfree(DatumGetPointer(values[Anum_pg_trigger_tgname - 1]));
|
|
pfree(DatumGetPointer(values[Anum_pg_trigger_tgargs - 1]));
|
|
pfree(DatumGetPointer(values[Anum_pg_trigger_tgattr - 1]));
|
|
if (oldtablename)
|
|
pfree(DatumGetPointer(values[Anum_pg_trigger_tgoldtable - 1]));
|
|
if (newtablename)
|
|
pfree(DatumGetPointer(values[Anum_pg_trigger_tgnewtable - 1]));
|
|
|
|
/*
|
|
* Update relation's pg_class entry; if necessary; and if not, send an SI
|
|
* message to make other backends (and this one) rebuild relcache entries.
|
|
*/
|
|
pgrel = table_open(RelationRelationId, RowExclusiveLock);
|
|
tuple = SearchSysCacheCopy1(RELOID,
|
|
ObjectIdGetDatum(RelationGetRelid(rel)));
|
|
if (!HeapTupleIsValid(tuple))
|
|
elog(ERROR, "cache lookup failed for relation %u",
|
|
RelationGetRelid(rel));
|
|
if (!((Form_pg_class) GETSTRUCT(tuple))->relhastriggers)
|
|
{
|
|
((Form_pg_class) GETSTRUCT(tuple))->relhastriggers = true;
|
|
|
|
CatalogTupleUpdate(pgrel, &tuple->t_self, tuple);
|
|
|
|
CommandCounterIncrement();
|
|
}
|
|
else
|
|
CacheInvalidateRelcacheByTuple(tuple);
|
|
|
|
heap_freetuple(tuple);
|
|
table_close(pgrel, RowExclusiveLock);
|
|
|
|
/*
|
|
* If we're replacing a trigger, flush all the old dependencies before
|
|
* recording new ones.
|
|
*/
|
|
if (trigger_exists)
|
|
deleteDependencyRecordsFor(TriggerRelationId, trigoid, true);
|
|
|
|
/*
|
|
* 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 (isInternal && OidIsValid(constraintOid))
|
|
{
|
|
/*
|
|
* Internally-generated trigger for a constraint, so make it an
|
|
* internal dependency of the constraint. We can skip depending on
|
|
* the relation(s), 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
|
|
{
|
|
/*
|
|
* User 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 (OidIsValid(constrrelid))
|
|
{
|
|
referenced.classId = RelationRelationId;
|
|
referenced.objectId = constrrelid;
|
|
referenced.objectSubId = 0;
|
|
recordDependencyOn(&myself, &referenced, DEPENDENCY_AUTO);
|
|
}
|
|
/* Not possible to have an index dependency in this case */
|
|
Assert(!OidIsValid(indexOid));
|
|
|
|
/*
|
|
* If it's a user-specified constraint trigger, make the constraint
|
|
* internally dependent on the trigger instead of vice versa.
|
|
*/
|
|
if (OidIsValid(constraintOid))
|
|
{
|
|
referenced.classId = ConstraintRelationId;
|
|
referenced.objectId = constraintOid;
|
|
referenced.objectSubId = 0;
|
|
recordDependencyOn(&referenced, &myself, DEPENDENCY_INTERNAL);
|
|
}
|
|
|
|
/*
|
|
* If it's a partition trigger, create the partition dependencies.
|
|
*/
|
|
if (OidIsValid(parentTriggerOid))
|
|
{
|
|
ObjectAddressSet(referenced, TriggerRelationId, parentTriggerOid);
|
|
recordDependencyOn(&myself, &referenced, DEPENDENCY_PARTITION_PRI);
|
|
ObjectAddressSet(referenced, RelationRelationId, RelationGetRelid(rel));
|
|
recordDependencyOn(&myself, &referenced, DEPENDENCY_PARTITION_SEC);
|
|
}
|
|
}
|
|
|
|
/* If column-specific trigger, add normal dependencies on columns */
|
|
if (columns != NULL)
|
|
{
|
|
int i;
|
|
|
|
referenced.classId = RelationRelationId;
|
|
referenced.objectId = RelationGetRelid(rel);
|
|
for (i = 0; i < ncolumns; i++)
|
|
{
|
|
referenced.objectSubId = columns[i];
|
|
recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* If it has a WHEN clause, add dependencies on objects mentioned in the
|
|
* expression (eg, functions, as well as any columns used).
|
|
*/
|
|
if (whenRtable != NIL)
|
|
recordDependencyOnExpr(&myself, whenClause, whenRtable,
|
|
DEPENDENCY_NORMAL);
|
|
|
|
/* Post creation hook for new trigger */
|
|
InvokeObjectPostCreateHookArg(TriggerRelationId, trigoid, 0,
|
|
isInternal);
|
|
|
|
/*
|
|
* Lastly, create the trigger on child relations, if needed.
|
|
*/
|
|
if (partition_recurse)
|
|
{
|
|
PartitionDesc partdesc = RelationGetPartitionDesc(rel, true);
|
|
int i;
|
|
MemoryContext oldcxt,
|
|
perChildCxt;
|
|
|
|
perChildCxt = AllocSetContextCreate(CurrentMemoryContext,
|
|
"part trig clone",
|
|
ALLOCSET_SMALL_SIZES);
|
|
|
|
/*
|
|
* We don't currently expect to be called with a valid indexOid. If
|
|
* that ever changes then we'll need to write code here to find the
|
|
* corresponding child index.
|
|
*/
|
|
Assert(!OidIsValid(indexOid));
|
|
|
|
oldcxt = MemoryContextSwitchTo(perChildCxt);
|
|
|
|
/* Iterate to create the trigger on each existing partition */
|
|
for (i = 0; i < partdesc->nparts; i++)
|
|
{
|
|
CreateTrigStmt *childStmt;
|
|
Relation childTbl;
|
|
Node *qual;
|
|
|
|
childTbl = table_open(partdesc->oids[i], ShareRowExclusiveLock);
|
|
|
|
/*
|
|
* Initialize our fabricated parse node by copying the original
|
|
* one, then resetting fields that we pass separately.
|
|
*/
|
|
childStmt = (CreateTrigStmt *) copyObject(stmt);
|
|
childStmt->funcname = NIL;
|
|
childStmt->whenClause = NULL;
|
|
|
|
/* If there is a WHEN clause, create a modified copy of it */
|
|
qual = copyObject(whenClause);
|
|
qual = (Node *)
|
|
map_partition_varattnos((List *) qual, PRS2_OLD_VARNO,
|
|
childTbl, rel);
|
|
qual = (Node *)
|
|
map_partition_varattnos((List *) qual, PRS2_NEW_VARNO,
|
|
childTbl, rel);
|
|
|
|
CreateTriggerFiringOn(childStmt, queryString,
|
|
partdesc->oids[i], refRelOid,
|
|
InvalidOid, InvalidOid,
|
|
funcoid, trigoid, qual,
|
|
isInternal, true, trigger_fires_when);
|
|
|
|
table_close(childTbl, NoLock);
|
|
|
|
MemoryContextReset(perChildCxt);
|
|
}
|
|
|
|
MemoryContextSwitchTo(oldcxt);
|
|
MemoryContextDelete(perChildCxt);
|
|
}
|
|
|
|
/* Keep lock on target rel until end of xact */
|
|
table_close(rel, NoLock);
|
|
|
|
return myself;
|
|
}
|
|
|
|
/*
|
|
* TriggerSetParentTrigger
|
|
* Set a partition's trigger as child of its parent trigger,
|
|
* or remove the linkage if parentTrigId is InvalidOid.
|
|
*
|
|
* This updates the constraint's pg_trigger row to show it as inherited, and
|
|
* adds PARTITION dependencies to prevent the trigger from being deleted
|
|
* on its own. Alternatively, reverse that.
|
|
*/
|
|
void
|
|
TriggerSetParentTrigger(Relation trigRel,
|
|
Oid childTrigId,
|
|
Oid parentTrigId,
|
|
Oid childTableId)
|
|
{
|
|
SysScanDesc tgscan;
|
|
ScanKeyData skey[1];
|
|
Form_pg_trigger trigForm;
|
|
HeapTuple tuple,
|
|
newtup;
|
|
ObjectAddress depender;
|
|
ObjectAddress referenced;
|
|
|
|
/*
|
|
* Find the trigger to delete.
|
|
*/
|
|
ScanKeyInit(&skey[0],
|
|
Anum_pg_trigger_oid,
|
|
BTEqualStrategyNumber, F_OIDEQ,
|
|
ObjectIdGetDatum(childTrigId));
|
|
|
|
tgscan = systable_beginscan(trigRel, TriggerOidIndexId, true,
|
|
NULL, 1, skey);
|
|
|
|
tuple = systable_getnext(tgscan);
|
|
if (!HeapTupleIsValid(tuple))
|
|
elog(ERROR, "could not find tuple for trigger %u", childTrigId);
|
|
newtup = heap_copytuple(tuple);
|
|
trigForm = (Form_pg_trigger) GETSTRUCT(newtup);
|
|
if (OidIsValid(parentTrigId))
|
|
{
|
|
/* don't allow setting parent for a constraint that already has one */
|
|
if (OidIsValid(trigForm->tgparentid))
|
|
elog(ERROR, "trigger %u already has a parent trigger",
|
|
childTrigId);
|
|
|
|
trigForm->tgparentid = parentTrigId;
|
|
|
|
CatalogTupleUpdate(trigRel, &tuple->t_self, newtup);
|
|
|
|
ObjectAddressSet(depender, TriggerRelationId, childTrigId);
|
|
|
|
ObjectAddressSet(referenced, TriggerRelationId, parentTrigId);
|
|
recordDependencyOn(&depender, &referenced, DEPENDENCY_PARTITION_PRI);
|
|
|
|
ObjectAddressSet(referenced, RelationRelationId, childTableId);
|
|
recordDependencyOn(&depender, &referenced, DEPENDENCY_PARTITION_SEC);
|
|
}
|
|
else
|
|
{
|
|
trigForm->tgparentid = InvalidOid;
|
|
|
|
CatalogTupleUpdate(trigRel, &tuple->t_self, newtup);
|
|
|
|
deleteDependencyRecordsForClass(TriggerRelationId, childTrigId,
|
|
TriggerRelationId,
|
|
DEPENDENCY_PARTITION_PRI);
|
|
deleteDependencyRecordsForClass(TriggerRelationId, childTrigId,
|
|
RelationRelationId,
|
|
DEPENDENCY_PARTITION_SEC);
|
|
}
|
|
|
|
heap_freetuple(newtup);
|
|
systable_endscan(tgscan);
|
|
}
|
|
|
|
|
|
/*
|
|
* Guts of trigger deletion.
|
|
*/
|
|
void
|
|
RemoveTriggerById(Oid trigOid)
|
|
{
|
|
Relation tgrel;
|
|
SysScanDesc tgscan;
|
|
ScanKeyData skey[1];
|
|
HeapTuple tup;
|
|
Oid relid;
|
|
Relation rel;
|
|
|
|
tgrel = table_open(TriggerRelationId, RowExclusiveLock);
|
|
|
|
/*
|
|
* Find the trigger to delete.
|
|
*/
|
|
ScanKeyInit(&skey[0],
|
|
Anum_pg_trigger_oid,
|
|
BTEqualStrategyNumber, F_OIDEQ,
|
|
ObjectIdGetDatum(trigOid));
|
|
|
|
tgscan = systable_beginscan(tgrel, TriggerOidIndexId, true,
|
|
NULL, 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 = table_open(relid, AccessExclusiveLock);
|
|
|
|
if (rel->rd_rel->relkind != RELKIND_RELATION &&
|
|
rel->rd_rel->relkind != RELKIND_VIEW &&
|
|
rel->rd_rel->relkind != RELKIND_FOREIGN_TABLE &&
|
|
rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
|
|
errmsg("relation \"%s\" cannot have triggers",
|
|
RelationGetRelationName(rel)),
|
|
errdetail_relkind_not_supported(rel->rd_rel->relkind)));
|
|
|
|
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.
|
|
*/
|
|
CatalogTupleDelete(tgrel, &tup->t_self);
|
|
|
|
systable_endscan(tgscan);
|
|
table_close(tgrel, RowExclusiveLock);
|
|
|
|
/*
|
|
* We do not bother to try to determine whether any other triggers remain,
|
|
* which would be needed in order to decide whether it's safe to clear the
|
|
* relation's relhastriggers. (In any case, there might be a concurrent
|
|
* process adding new triggers.) Instead, just force a relcache inval to
|
|
* make other backends (and this one too!) rebuild their relcache entries.
|
|
* There's no great harm in leaving relhastriggers true even if there are
|
|
* no triggers left.
|
|
*/
|
|
CacheInvalidateRelcache(rel);
|
|
|
|
/* Keep lock on trigger's rel until end of xact */
|
|
table_close(rel, NoLock);
|
|
}
|
|
|
|
/*
|
|
* get_trigger_oid - Look up a trigger by name to find its OID.
|
|
*
|
|
* If missing_ok is false, throw an error if trigger not found. If
|
|
* true, just return InvalidOid.
|
|
*/
|
|
Oid
|
|
get_trigger_oid(Oid relid, const char *trigname, bool missing_ok)
|
|
{
|
|
Relation tgrel;
|
|
ScanKeyData skey[2];
|
|
SysScanDesc tgscan;
|
|
HeapTuple tup;
|
|
Oid oid;
|
|
|
|
/*
|
|
* Find the trigger, verify permissions, set up object address
|
|
*/
|
|
tgrel = table_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,
|
|
NULL, 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))));
|
|
oid = InvalidOid;
|
|
}
|
|
else
|
|
{
|
|
oid = ((Form_pg_trigger) GETSTRUCT(tup))->oid;
|
|
}
|
|
|
|
systable_endscan(tgscan);
|
|
table_close(tgrel, AccessShareLock);
|
|
return oid;
|
|
}
|
|
|
|
/*
|
|
* Perform permissions and integrity checks before acquiring a relation lock.
|
|
*/
|
|
static void
|
|
RangeVarCallbackForRenameTrigger(const RangeVar *rv, Oid relid, Oid oldrelid,
|
|
void *arg)
|
|
{
|
|
HeapTuple tuple;
|
|
Form_pg_class form;
|
|
|
|
tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
|
|
if (!HeapTupleIsValid(tuple))
|
|
return; /* concurrently dropped */
|
|
form = (Form_pg_class) GETSTRUCT(tuple);
|
|
|
|
/* only tables and views can have triggers */
|
|
if (form->relkind != RELKIND_RELATION && form->relkind != RELKIND_VIEW &&
|
|
form->relkind != RELKIND_FOREIGN_TABLE &&
|
|
form->relkind != RELKIND_PARTITIONED_TABLE)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
|
|
errmsg("relation \"%s\" cannot have triggers",
|
|
rv->relname),
|
|
errdetail_relkind_not_supported(form->relkind)));
|
|
|
|
/* you must own the table to rename one of its triggers */
|
|
if (!pg_class_ownercheck(relid, GetUserId()))
|
|
aclcheck_error(ACLCHECK_NOT_OWNER, get_relkind_objtype(get_rel_relkind(relid)), rv->relname);
|
|
if (!allowSystemTableMods && IsSystemClass(relid, form))
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
|
|
errmsg("permission denied: \"%s\" is a system catalog",
|
|
rv->relname)));
|
|
|
|
ReleaseSysCache(tuple);
|
|
}
|
|
|
|
/*
|
|
* 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
|
|
*/
|
|
ObjectAddress
|
|
renametrig(RenameStmt *stmt)
|
|
{
|
|
Oid tgoid;
|
|
Relation targetrel;
|
|
Relation tgrel;
|
|
HeapTuple tuple;
|
|
SysScanDesc tgscan;
|
|
ScanKeyData key[2];
|
|
Oid relid;
|
|
ObjectAddress address;
|
|
|
|
/*
|
|
* Look up name, check permissions, and acquire lock (which we will NOT
|
|
* release until end of transaction).
|
|
*/
|
|
relid = RangeVarGetRelidExtended(stmt->relation, AccessExclusiveLock,
|
|
0,
|
|
RangeVarCallbackForRenameTrigger,
|
|
NULL);
|
|
|
|
/* Have lock already, so just need to build relcache entry. */
|
|
targetrel = relation_open(relid, NoLock);
|
|
|
|
/*
|
|
* On partitioned tables, this operation recurses to partitions. Lock all
|
|
* tables upfront.
|
|
*/
|
|
if (targetrel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
|
|
(void) find_all_inheritors(relid, AccessExclusiveLock, NULL);
|
|
|
|
tgrel = table_open(TriggerRelationId, RowExclusiveLock);
|
|
|
|
/*
|
|
* Search for the trigger to modify.
|
|
*/
|
|
ScanKeyInit(&key[0],
|
|
Anum_pg_trigger_tgrelid,
|
|
BTEqualStrategyNumber, F_OIDEQ,
|
|
ObjectIdGetDatum(relid));
|
|
ScanKeyInit(&key[1],
|
|
Anum_pg_trigger_tgname,
|
|
BTEqualStrategyNumber, F_NAMEEQ,
|
|
PointerGetDatum(stmt->subname));
|
|
tgscan = systable_beginscan(tgrel, TriggerRelidNameIndexId, true,
|
|
NULL, 2, key);
|
|
if (HeapTupleIsValid(tuple = systable_getnext(tgscan)))
|
|
{
|
|
Form_pg_trigger trigform;
|
|
|
|
trigform = (Form_pg_trigger) GETSTRUCT(tuple);
|
|
tgoid = trigform->oid;
|
|
|
|
/*
|
|
* If the trigger descends from a trigger on a parent partitioned
|
|
* table, reject the rename. We don't allow a trigger in a partition
|
|
* to differ in name from that of its parent: that would lead to an
|
|
* inconsistency that pg_dump would not reproduce.
|
|
*/
|
|
if (OidIsValid(trigform->tgparentid))
|
|
ereport(ERROR,
|
|
errmsg("cannot rename trigger \"%s\" on table \"%s\"",
|
|
stmt->subname, RelationGetRelationName(targetrel)),
|
|
errhint("Rename trigger on partitioned table \"%s\" instead.",
|
|
get_rel_name(get_partition_parent(relid, false))));
|
|
|
|
|
|
/* Rename the trigger on this relation ... */
|
|
renametrig_internal(tgrel, targetrel, tuple, stmt->newname,
|
|
stmt->subname);
|
|
|
|
/* ... and if it is partitioned, recurse to its partitions */
|
|
if (targetrel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
|
|
{
|
|
PartitionDesc partdesc = RelationGetPartitionDesc(targetrel, true);
|
|
|
|
for (int i = 0; i < partdesc->nparts; i++)
|
|
{
|
|
Oid partitionId = partdesc->oids[i];
|
|
|
|
renametrig_partition(tgrel, partitionId, trigform->oid,
|
|
stmt->newname, stmt->subname);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_UNDEFINED_OBJECT),
|
|
errmsg("trigger \"%s\" for table \"%s\" does not exist",
|
|
stmt->subname, RelationGetRelationName(targetrel))));
|
|
}
|
|
|
|
ObjectAddressSet(address, TriggerRelationId, tgoid);
|
|
|
|
systable_endscan(tgscan);
|
|
|
|
table_close(tgrel, RowExclusiveLock);
|
|
|
|
/*
|
|
* Close rel, but keep exclusive lock!
|
|
*/
|
|
relation_close(targetrel, NoLock);
|
|
|
|
return address;
|
|
}
|
|
|
|
/*
|
|
* Subroutine for renametrig -- perform the actual work of renaming one
|
|
* trigger on one table.
|
|
*
|
|
* If the trigger has a name different from the expected one, raise a
|
|
* NOTICE about it.
|
|
*/
|
|
static void
|
|
renametrig_internal(Relation tgrel, Relation targetrel, HeapTuple trigtup,
|
|
const char *newname, const char *expected_name)
|
|
{
|
|
HeapTuple tuple;
|
|
Form_pg_trigger tgform;
|
|
ScanKeyData key[2];
|
|
SysScanDesc tgscan;
|
|
|
|
/* If the trigger already has the new name, nothing to do. */
|
|
tgform = (Form_pg_trigger) GETSTRUCT(trigtup);
|
|
if (strcmp(NameStr(tgform->tgname), newname) == 0)
|
|
return;
|
|
|
|
/*
|
|
* Before actually trying the rename, search for triggers with the same
|
|
* name. The update would fail with an ugly message in that case, and it
|
|
* is better to throw a nicer error.
|
|
*/
|
|
ScanKeyInit(&key[0],
|
|
Anum_pg_trigger_tgrelid,
|
|
BTEqualStrategyNumber, F_OIDEQ,
|
|
ObjectIdGetDatum(RelationGetRelid(targetrel)));
|
|
ScanKeyInit(&key[1],
|
|
Anum_pg_trigger_tgname,
|
|
BTEqualStrategyNumber, F_NAMEEQ,
|
|
PointerGetDatum(newname));
|
|
tgscan = systable_beginscan(tgrel, TriggerRelidNameIndexId, true,
|
|
NULL, 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);
|
|
|
|
/*
|
|
* The target name is free; update the existing pg_trigger tuple with it.
|
|
*/
|
|
tuple = heap_copytuple(trigtup); /* need a modifiable copy */
|
|
tgform = (Form_pg_trigger) GETSTRUCT(tuple);
|
|
|
|
/*
|
|
* If the trigger has a name different from what we expected, let the user
|
|
* know. (We can proceed anyway, since we must have reached here following
|
|
* a tgparentid link.)
|
|
*/
|
|
if (strcmp(NameStr(tgform->tgname), expected_name) != 0)
|
|
ereport(NOTICE,
|
|
errmsg("renamed trigger \"%s\" on relation \"%s\"",
|
|
NameStr(tgform->tgname),
|
|
RelationGetRelationName(targetrel)));
|
|
|
|
namestrcpy(&tgform->tgname, newname);
|
|
|
|
CatalogTupleUpdate(tgrel, &tuple->t_self, tuple);
|
|
|
|
InvokeObjectPostAlterHook(TriggerRelationId, tgform->oid, 0);
|
|
|
|
/*
|
|
* 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);
|
|
}
|
|
|
|
/*
|
|
* Subroutine for renametrig -- Helper for recursing to partitions when
|
|
* renaming triggers on a partitioned table.
|
|
*/
|
|
static void
|
|
renametrig_partition(Relation tgrel, Oid partitionId, Oid parentTriggerOid,
|
|
const char *newname, const char *expected_name)
|
|
{
|
|
SysScanDesc tgscan;
|
|
ScanKeyData key;
|
|
HeapTuple tuple;
|
|
|
|
/*
|
|
* Given a relation and the OID of a trigger on parent relation, find the
|
|
* corresponding trigger in the child and rename that trigger to the given
|
|
* name.
|
|
*/
|
|
ScanKeyInit(&key,
|
|
Anum_pg_trigger_tgrelid,
|
|
BTEqualStrategyNumber, F_OIDEQ,
|
|
ObjectIdGetDatum(partitionId));
|
|
tgscan = systable_beginscan(tgrel, TriggerRelidNameIndexId, true,
|
|
NULL, 1, &key);
|
|
while (HeapTupleIsValid(tuple = systable_getnext(tgscan)))
|
|
{
|
|
Form_pg_trigger tgform = (Form_pg_trigger) GETSTRUCT(tuple);
|
|
Relation partitionRel;
|
|
|
|
if (tgform->tgparentid != parentTriggerOid)
|
|
continue; /* not our trigger */
|
|
|
|
partitionRel = table_open(partitionId, NoLock);
|
|
|
|
/* Rename the trigger on this partition */
|
|
renametrig_internal(tgrel, partitionRel, tuple, newname, expected_name);
|
|
|
|
/* And if this relation is partitioned, recurse to its partitions */
|
|
if (partitionRel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
|
|
{
|
|
PartitionDesc partdesc = RelationGetPartitionDesc(partitionRel,
|
|
true);
|
|
|
|
for (int i = 0; i < partdesc->nparts; i++)
|
|
{
|
|
Oid partitionId = partdesc->oids[i];
|
|
|
|
renametrig_partition(tgrel, partitionId, tgform->oid, newname,
|
|
NameStr(tgform->tgname));
|
|
}
|
|
}
|
|
table_close(partitionRel, NoLock);
|
|
|
|
/* There should be at most one matching tuple */
|
|
break;
|
|
}
|
|
systable_endscan(tgscan);
|
|
}
|
|
|
|
/*
|
|
* EnableDisableTrigger()
|
|
*
|
|
* Called by ALTER TABLE ENABLE/DISABLE [ REPLICA | ALWAYS ] 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
|
|
* fires_when: new value for tgenabled field. In addition to generic
|
|
* enablement/disablement, this also defines when the trigger
|
|
* should be fired in session replication roles.
|
|
* skip_system: if true, skip "system" triggers (constraint triggers)
|
|
* recurse: if true, recurse to partitions
|
|
*
|
|
* 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, bool recurse,
|
|
LOCKMODE lockmode)
|
|
{
|
|
Relation tgrel;
|
|
int nkeys;
|
|
ScanKeyData keys[2];
|
|
SysScanDesc tgscan;
|
|
HeapTuple tuple;
|
|
bool found;
|
|
bool changed;
|
|
|
|
/* Scan the relevant entries in pg_triggers */
|
|
tgrel = table_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,
|
|
NULL, nkeys, keys);
|
|
|
|
found = changed = false;
|
|
|
|
while (HeapTupleIsValid(tuple = systable_getnext(tgscan)))
|
|
{
|
|
Form_pg_trigger oldtrig = (Form_pg_trigger) GETSTRUCT(tuple);
|
|
|
|
if (oldtrig->tgisinternal)
|
|
{
|
|
/* 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;
|
|
|
|
CatalogTupleUpdate(tgrel, &newtup->t_self, newtup);
|
|
|
|
heap_freetuple(newtup);
|
|
|
|
changed = true;
|
|
}
|
|
|
|
/*
|
|
* When altering FOR EACH ROW triggers on a partitioned table, do the
|
|
* same on the partitions as well, unless ONLY is specified.
|
|
*
|
|
* Note that we recurse even if we didn't change the trigger above,
|
|
* because the partitions' copy of the trigger may have a different
|
|
* value of tgenabled than the parent's trigger and thus might need to
|
|
* be changed.
|
|
*/
|
|
if (recurse &&
|
|
rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE &&
|
|
(TRIGGER_FOR_ROW(oldtrig->tgtype)))
|
|
{
|
|
PartitionDesc partdesc = RelationGetPartitionDesc(rel, true);
|
|
int i;
|
|
|
|
for (i = 0; i < partdesc->nparts; i++)
|
|
{
|
|
Relation part;
|
|
|
|
part = relation_open(partdesc->oids[i], lockmode);
|
|
EnableDisableTrigger(part, NameStr(oldtrig->tgname),
|
|
fires_when, skip_system, recurse,
|
|
lockmode);
|
|
table_close(part, NoLock); /* keep lock till commit */
|
|
}
|
|
}
|
|
|
|
InvokeObjectPostAlterHook(TriggerRelationId,
|
|
oldtrig->oid, 0);
|
|
}
|
|
|
|
systable_endscan(tgscan);
|
|
|
|
table_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 numtrigs;
|
|
int maxtrigs;
|
|
Trigger *triggers;
|
|
Relation tgrel;
|
|
ScanKeyData skey;
|
|
SysScanDesc tgscan;
|
|
HeapTuple htup;
|
|
MemoryContext oldContext;
|
|
int i;
|
|
|
|
/*
|
|
* Allocate a working array to hold the triggers (the array is extended if
|
|
* necessary)
|
|
*/
|
|
maxtrigs = 16;
|
|
triggers = (Trigger *) palloc(maxtrigs * sizeof(Trigger));
|
|
numtrigs = 0;
|
|
|
|
/*
|
|
* 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 = table_open(TriggerRelationId, AccessShareLock);
|
|
tgscan = systable_beginscan(tgrel, TriggerRelidNameIndexId, true,
|
|
NULL, 1, &skey);
|
|
|
|
while (HeapTupleIsValid(htup = systable_getnext(tgscan)))
|
|
{
|
|
Form_pg_trigger pg_trigger = (Form_pg_trigger) GETSTRUCT(htup);
|
|
Trigger *build;
|
|
Datum datum;
|
|
bool isnull;
|
|
|
|
if (numtrigs >= maxtrigs)
|
|
{
|
|
maxtrigs *= 2;
|
|
triggers = (Trigger *) repalloc(triggers, maxtrigs * sizeof(Trigger));
|
|
}
|
|
build = &(triggers[numtrigs]);
|
|
|
|
build->tgoid = pg_trigger->oid;
|
|
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->tgisinternal = pg_trigger->tgisinternal;
|
|
build->tgisclone = OidIsValid(pg_trigger->tgparentid);
|
|
build->tgconstrrelid = pg_trigger->tgconstrrelid;
|
|
build->tgconstrindid = pg_trigger->tgconstrindid;
|
|
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 = (int16 *) palloc(build->tgnattr * sizeof(int16));
|
|
memcpy(build->tgattr, &(pg_trigger->tgattr.values),
|
|
build->tgnattr * sizeof(int16));
|
|
}
|
|
else
|
|
build->tgattr = NULL;
|
|
if (build->tgnargs > 0)
|
|
{
|
|
bytea *val;
|
|
char *p;
|
|
|
|
val = DatumGetByteaPP(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_ANY(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;
|
|
|
|
datum = fastgetattr(htup, Anum_pg_trigger_tgoldtable,
|
|
tgrel->rd_att, &isnull);
|
|
if (!isnull)
|
|
build->tgoldtable =
|
|
DatumGetCString(DirectFunctionCall1(nameout, datum));
|
|
else
|
|
build->tgoldtable = NULL;
|
|
|
|
datum = fastgetattr(htup, Anum_pg_trigger_tgnewtable,
|
|
tgrel->rd_att, &isnull);
|
|
if (!isnull)
|
|
build->tgnewtable =
|
|
DatumGetCString(DirectFunctionCall1(nameout, datum));
|
|
else
|
|
build->tgnewtable = NULL;
|
|
|
|
datum = fastgetattr(htup, Anum_pg_trigger_tgqual,
|
|
tgrel->rd_att, &isnull);
|
|
if (!isnull)
|
|
build->tgqual = TextDatumGetCString(datum);
|
|
else
|
|
build->tgqual = NULL;
|
|
|
|
numtrigs++;
|
|
}
|
|
|
|
systable_endscan(tgscan);
|
|
table_close(tgrel, AccessShareLock);
|
|
|
|
/* There might not be any triggers */
|
|
if (numtrigs == 0)
|
|
{
|
|
pfree(triggers);
|
|
return;
|
|
}
|
|
|
|
/* Build trigdesc */
|
|
trigdesc = (TriggerDesc *) palloc0(sizeof(TriggerDesc));
|
|
trigdesc->triggers = triggers;
|
|
trigdesc->numtriggers = numtrigs;
|
|
for (i = 0; i < numtrigs; i++)
|
|
SetTriggerFlags(trigdesc, &(triggers[i]));
|
|
|
|
/* Copy completed trigdesc into cache storage */
|
|
oldContext = MemoryContextSwitchTo(CacheMemoryContext);
|
|
relation->trigdesc = CopyTriggerDesc(trigdesc);
|
|
MemoryContextSwitchTo(oldContext);
|
|
|
|
/* Release working memory */
|
|
FreeTriggerDesc(trigdesc);
|
|
}
|
|
|
|
/*
|
|
* Update the TriggerDesc's hint flags to include the specified trigger
|
|
*/
|
|
static void
|
|
SetTriggerFlags(TriggerDesc *trigdesc, Trigger *trigger)
|
|
{
|
|
int16 tgtype = trigger->tgtype;
|
|
|
|
trigdesc->trig_insert_before_row |=
|
|
TRIGGER_TYPE_MATCHES(tgtype, TRIGGER_TYPE_ROW,
|
|
TRIGGER_TYPE_BEFORE, TRIGGER_TYPE_INSERT);
|
|
trigdesc->trig_insert_after_row |=
|
|
TRIGGER_TYPE_MATCHES(tgtype, TRIGGER_TYPE_ROW,
|
|
TRIGGER_TYPE_AFTER, TRIGGER_TYPE_INSERT);
|
|
trigdesc->trig_insert_instead_row |=
|
|
TRIGGER_TYPE_MATCHES(tgtype, TRIGGER_TYPE_ROW,
|
|
TRIGGER_TYPE_INSTEAD, TRIGGER_TYPE_INSERT);
|
|
trigdesc->trig_insert_before_statement |=
|
|
TRIGGER_TYPE_MATCHES(tgtype, TRIGGER_TYPE_STATEMENT,
|
|
TRIGGER_TYPE_BEFORE, TRIGGER_TYPE_INSERT);
|
|
trigdesc->trig_insert_after_statement |=
|
|
TRIGGER_TYPE_MATCHES(tgtype, TRIGGER_TYPE_STATEMENT,
|
|
TRIGGER_TYPE_AFTER, TRIGGER_TYPE_INSERT);
|
|
trigdesc->trig_update_before_row |=
|
|
TRIGGER_TYPE_MATCHES(tgtype, TRIGGER_TYPE_ROW,
|
|
TRIGGER_TYPE_BEFORE, TRIGGER_TYPE_UPDATE);
|
|
trigdesc->trig_update_after_row |=
|
|
TRIGGER_TYPE_MATCHES(tgtype, TRIGGER_TYPE_ROW,
|
|
TRIGGER_TYPE_AFTER, TRIGGER_TYPE_UPDATE);
|
|
trigdesc->trig_update_instead_row |=
|
|
TRIGGER_TYPE_MATCHES(tgtype, TRIGGER_TYPE_ROW,
|
|
TRIGGER_TYPE_INSTEAD, TRIGGER_TYPE_UPDATE);
|
|
trigdesc->trig_update_before_statement |=
|
|
TRIGGER_TYPE_MATCHES(tgtype, TRIGGER_TYPE_STATEMENT,
|
|
TRIGGER_TYPE_BEFORE, TRIGGER_TYPE_UPDATE);
|
|
trigdesc->trig_update_after_statement |=
|
|
TRIGGER_TYPE_MATCHES(tgtype, TRIGGER_TYPE_STATEMENT,
|
|
TRIGGER_TYPE_AFTER, TRIGGER_TYPE_UPDATE);
|
|
trigdesc->trig_delete_before_row |=
|
|
TRIGGER_TYPE_MATCHES(tgtype, TRIGGER_TYPE_ROW,
|
|
TRIGGER_TYPE_BEFORE, TRIGGER_TYPE_DELETE);
|
|
trigdesc->trig_delete_after_row |=
|
|
TRIGGER_TYPE_MATCHES(tgtype, TRIGGER_TYPE_ROW,
|
|
TRIGGER_TYPE_AFTER, TRIGGER_TYPE_DELETE);
|
|
trigdesc->trig_delete_instead_row |=
|
|
TRIGGER_TYPE_MATCHES(tgtype, TRIGGER_TYPE_ROW,
|
|
TRIGGER_TYPE_INSTEAD, TRIGGER_TYPE_DELETE);
|
|
trigdesc->trig_delete_before_statement |=
|
|
TRIGGER_TYPE_MATCHES(tgtype, TRIGGER_TYPE_STATEMENT,
|
|
TRIGGER_TYPE_BEFORE, TRIGGER_TYPE_DELETE);
|
|
trigdesc->trig_delete_after_statement |=
|
|
TRIGGER_TYPE_MATCHES(tgtype, TRIGGER_TYPE_STATEMENT,
|
|
TRIGGER_TYPE_AFTER, TRIGGER_TYPE_DELETE);
|
|
/* there are no row-level truncate triggers */
|
|
trigdesc->trig_truncate_before_statement |=
|
|
TRIGGER_TYPE_MATCHES(tgtype, TRIGGER_TYPE_STATEMENT,
|
|
TRIGGER_TYPE_BEFORE, TRIGGER_TYPE_TRUNCATE);
|
|
trigdesc->trig_truncate_after_statement |=
|
|
TRIGGER_TYPE_MATCHES(tgtype, TRIGGER_TYPE_STATEMENT,
|
|
TRIGGER_TYPE_AFTER, TRIGGER_TYPE_TRUNCATE);
|
|
|
|
trigdesc->trig_insert_new_table |=
|
|
(TRIGGER_FOR_INSERT(tgtype) &&
|
|
TRIGGER_USES_TRANSITION_TABLE(trigger->tgnewtable));
|
|
trigdesc->trig_update_old_table |=
|
|
(TRIGGER_FOR_UPDATE(tgtype) &&
|
|
TRIGGER_USES_TRANSITION_TABLE(trigger->tgoldtable));
|
|
trigdesc->trig_update_new_table |=
|
|
(TRIGGER_FOR_UPDATE(tgtype) &&
|
|
TRIGGER_USES_TRANSITION_TABLE(trigger->tgnewtable));
|
|
trigdesc->trig_delete_old_table |=
|
|
(TRIGGER_FOR_DELETE(tgtype) &&
|
|
TRIGGER_USES_TRANSITION_TABLE(trigger->tgoldtable));
|
|
}
|
|
|
|
/*
|
|
* Copy a TriggerDesc data structure.
|
|
*
|
|
* The copy is allocated in the current memory context.
|
|
*/
|
|
TriggerDesc *
|
|
CopyTriggerDesc(TriggerDesc *trigdesc)
|
|
{
|
|
TriggerDesc *newdesc;
|
|
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)
|
|
{
|
|
int16 *newattr;
|
|
|
|
newattr = (int16 *) palloc(trigger->tgnattr * sizeof(int16));
|
|
memcpy(newattr, trigger->tgattr,
|
|
trigger->tgnattr * sizeof(int16));
|
|
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;
|
|
}
|
|
if (trigger->tgqual)
|
|
trigger->tgqual = pstrdup(trigger->tgqual);
|
|
if (trigger->tgoldtable)
|
|
trigger->tgoldtable = pstrdup(trigger->tgoldtable);
|
|
if (trigger->tgnewtable)
|
|
trigger->tgnewtable = pstrdup(trigger->tgnewtable);
|
|
trigger++;
|
|
}
|
|
|
|
return newdesc;
|
|
}
|
|
|
|
/*
|
|
* Free a TriggerDesc data structure.
|
|
*/
|
|
void
|
|
FreeTriggerDesc(TriggerDesc *trigdesc)
|
|
{
|
|
Trigger *trigger;
|
|
int i;
|
|
|
|
if (trigdesc == NULL)
|
|
return;
|
|
|
|
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);
|
|
}
|
|
if (trigger->tgqual)
|
|
pfree(trigger->tgqual);
|
|
if (trigger->tgoldtable)
|
|
pfree(trigger->tgoldtable);
|
|
if (trigger->tgnewtable)
|
|
pfree(trigger->tgnewtable);
|
|
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 hint flags, just the trigger array itself; if
|
|
* we have the same triggers with the same types, the flags 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.
|
|
*
|
|
* Note: comparing the stringToNode forms of the WHEN clauses means that
|
|
* parse column locations will affect the result. This is okay as long as
|
|
* this function is only used for detecting exact equality, as for example
|
|
* in checking for staleness of a cache entry.
|
|
*/
|
|
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->tgisinternal != trig2->tgisinternal)
|
|
return false;
|
|
if (trig1->tgisclone != trig2->tgisclone)
|
|
return false;
|
|
if (trig1->tgconstrrelid != trig2->tgconstrrelid)
|
|
return false;
|
|
if (trig1->tgconstrindid != trig2->tgconstrindid)
|
|
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(int16)) != 0)
|
|
return false;
|
|
for (j = 0; j < trig1->tgnargs; j++)
|
|
if (strcmp(trig1->tgargs[j], trig2->tgargs[j]) != 0)
|
|
return false;
|
|
if (trig1->tgqual == NULL && trig2->tgqual == NULL)
|
|
/* ok */ ;
|
|
else if (trig1->tgqual == NULL || trig2->tgqual == NULL)
|
|
return false;
|
|
else if (strcmp(trig1->tgqual, trig2->tgqual) != 0)
|
|
return false;
|
|
if (trig1->tgoldtable == NULL && trig2->tgoldtable == NULL)
|
|
/* ok */ ;
|
|
else if (trig1->tgoldtable == NULL || trig2->tgoldtable == NULL)
|
|
return false;
|
|
else if (strcmp(trig1->tgoldtable, trig2->tgoldtable) != 0)
|
|
return false;
|
|
if (trig1->tgnewtable == NULL && trig2->tgnewtable == NULL)
|
|
/* ok */ ;
|
|
else if (trig1->tgnewtable == NULL || trig2->tgnewtable == NULL)
|
|
return false;
|
|
else if (strcmp(trig1->tgnewtable, trig2->tgnewtable) != 0)
|
|
return false;
|
|
}
|
|
}
|
|
else if (trigdesc2 != NULL)
|
|
return false;
|
|
return true;
|
|
}
|
|
#endif /* NOT_USED */
|
|
|
|
/*
|
|
* Check if there is a row-level trigger with transition tables that prevents
|
|
* a table from becoming an inheritance child or partition. Return the name
|
|
* of the first such incompatible trigger, or NULL if there is none.
|
|
*/
|
|
const char *
|
|
FindTriggerIncompatibleWithInheritance(TriggerDesc *trigdesc)
|
|
{
|
|
if (trigdesc != NULL)
|
|
{
|
|
int i;
|
|
|
|
for (i = 0; i < trigdesc->numtriggers; ++i)
|
|
{
|
|
Trigger *trigger = &trigdesc->triggers[i];
|
|
|
|
if (trigger->tgoldtable != NULL || trigger->tgnewtable != NULL)
|
|
return trigger->tgname;
|
|
}
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/*
|
|
* 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)
|
|
{
|
|
LOCAL_FCINFO(fcinfo, 0);
|
|
PgStat_FunctionCallUsage fcusage;
|
|
Datum result;
|
|
MemoryContext oldContext;
|
|
|
|
/*
|
|
* Protect against code paths that may fail to initialize transition table
|
|
* info.
|
|
*/
|
|
Assert(((TRIGGER_FIRED_BY_INSERT(trigdata->tg_event) ||
|
|
TRIGGER_FIRED_BY_UPDATE(trigdata->tg_event) ||
|
|
TRIGGER_FIRED_BY_DELETE(trigdata->tg_event)) &&
|
|
TRIGGER_FIRED_AFTER(trigdata->tg_event) &&
|
|
!(trigdata->tg_event & AFTER_TRIGGER_DEFERRABLE) &&
|
|
!(trigdata->tg_event & AFTER_TRIGGER_INITDEFERRED)) ||
|
|
(trigdata->tg_oldtable == NULL && trigdata->tg_newtable == NULL));
|
|
|
|
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,
|
|
InvalidOid, (Node *) trigdata, NULL);
|
|
|
|
pgstat_init_function_usage(fcinfo, &fcusage);
|
|
|
|
MyTriggerDepth++;
|
|
PG_TRY();
|
|
{
|
|
result = FunctionCallInvoke(fcinfo);
|
|
}
|
|
PG_FINALLY();
|
|
{
|
|
MyTriggerDepth--;
|
|
}
|
|
PG_END_TRY();
|
|
|
|
pgstat_end_function_usage(&fcusage, true);
|
|
|
|
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 i;
|
|
TriggerData LocTriggerData = {0};
|
|
|
|
trigdesc = relinfo->ri_TrigDesc;
|
|
|
|
if (trigdesc == NULL)
|
|
return;
|
|
if (!trigdesc->trig_insert_before_statement)
|
|
return;
|
|
|
|
/* no-op if we already fired BS triggers in this context */
|
|
if (before_stmt_triggers_fired(RelationGetRelid(relinfo->ri_RelationDesc),
|
|
CMD_INSERT))
|
|
return;
|
|
|
|
LocTriggerData.type = T_TriggerData;
|
|
LocTriggerData.tg_event = TRIGGER_EVENT_INSERT |
|
|
TRIGGER_EVENT_BEFORE;
|
|
LocTriggerData.tg_relation = relinfo->ri_RelationDesc;
|
|
for (i = 0; i < trigdesc->numtriggers; i++)
|
|
{
|
|
Trigger *trigger = &trigdesc->triggers[i];
|
|
HeapTuple newtuple;
|
|
|
|
if (!TRIGGER_TYPE_MATCHES(trigger->tgtype,
|
|
TRIGGER_TYPE_STATEMENT,
|
|
TRIGGER_TYPE_BEFORE,
|
|
TRIGGER_TYPE_INSERT))
|
|
continue;
|
|
if (!TriggerEnabled(estate, relinfo, trigger, LocTriggerData.tg_event,
|
|
NULL, NULL, NULL))
|
|
continue;
|
|
|
|
LocTriggerData.tg_trigger = trigger;
|
|
newtuple = ExecCallTriggerFunc(&LocTriggerData,
|
|
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,
|
|
TransitionCaptureState *transition_capture)
|
|
{
|
|
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
|
|
|
|
if (trigdesc && trigdesc->trig_insert_after_statement)
|
|
AfterTriggerSaveEvent(estate, relinfo, NULL, NULL,
|
|
TRIGGER_EVENT_INSERT,
|
|
false, NULL, NULL, NIL, NULL, transition_capture,
|
|
false);
|
|
}
|
|
|
|
bool
|
|
ExecBRInsertTriggers(EState *estate, ResultRelInfo *relinfo,
|
|
TupleTableSlot *slot)
|
|
{
|
|
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
|
|
HeapTuple newtuple = NULL;
|
|
bool should_free;
|
|
TriggerData LocTriggerData = {0};
|
|
int i;
|
|
|
|
LocTriggerData.type = T_TriggerData;
|
|
LocTriggerData.tg_event = TRIGGER_EVENT_INSERT |
|
|
TRIGGER_EVENT_ROW |
|
|
TRIGGER_EVENT_BEFORE;
|
|
LocTriggerData.tg_relation = relinfo->ri_RelationDesc;
|
|
for (i = 0; i < trigdesc->numtriggers; i++)
|
|
{
|
|
Trigger *trigger = &trigdesc->triggers[i];
|
|
HeapTuple oldtuple;
|
|
|
|
if (!TRIGGER_TYPE_MATCHES(trigger->tgtype,
|
|
TRIGGER_TYPE_ROW,
|
|
TRIGGER_TYPE_BEFORE,
|
|
TRIGGER_TYPE_INSERT))
|
|
continue;
|
|
if (!TriggerEnabled(estate, relinfo, trigger, LocTriggerData.tg_event,
|
|
NULL, NULL, slot))
|
|
continue;
|
|
|
|
if (!newtuple)
|
|
newtuple = ExecFetchSlotHeapTuple(slot, true, &should_free);
|
|
|
|
LocTriggerData.tg_trigslot = slot;
|
|
LocTriggerData.tg_trigtuple = oldtuple = newtuple;
|
|
LocTriggerData.tg_trigger = trigger;
|
|
newtuple = ExecCallTriggerFunc(&LocTriggerData,
|
|
i,
|
|
relinfo->ri_TrigFunctions,
|
|
relinfo->ri_TrigInstrument,
|
|
GetPerTupleMemoryContext(estate));
|
|
if (newtuple == NULL)
|
|
{
|
|
if (should_free)
|
|
heap_freetuple(oldtuple);
|
|
return false; /* "do nothing" */
|
|
}
|
|
else if (newtuple != oldtuple)
|
|
{
|
|
ExecForceStoreHeapTuple(newtuple, slot, false);
|
|
|
|
/*
|
|
* After a tuple in a partition goes through a trigger, the user
|
|
* could have changed the partition key enough that the tuple no
|
|
* longer fits the partition. Verify that.
|
|
*/
|
|
if (trigger->tgisclone &&
|
|
!ExecPartitionCheck(relinfo, slot, estate, false))
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
|
|
errmsg("moving row to another partition during a BEFORE FOR EACH ROW trigger is not supported"),
|
|
errdetail("Before executing trigger \"%s\", the row was to be in partition \"%s.%s\".",
|
|
trigger->tgname,
|
|
get_namespace_name(RelationGetNamespace(relinfo->ri_RelationDesc)),
|
|
RelationGetRelationName(relinfo->ri_RelationDesc))));
|
|
|
|
if (should_free)
|
|
heap_freetuple(oldtuple);
|
|
|
|
/* signal tuple should be re-fetched if used */
|
|
newtuple = NULL;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void
|
|
ExecARInsertTriggers(EState *estate, ResultRelInfo *relinfo,
|
|
TupleTableSlot *slot, List *recheckIndexes,
|
|
TransitionCaptureState *transition_capture)
|
|
{
|
|
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
|
|
|
|
if ((trigdesc && trigdesc->trig_insert_after_row) ||
|
|
(transition_capture && transition_capture->tcs_insert_new_table))
|
|
AfterTriggerSaveEvent(estate, relinfo, NULL, NULL,
|
|
TRIGGER_EVENT_INSERT,
|
|
true, NULL, slot,
|
|
recheckIndexes, NULL,
|
|
transition_capture,
|
|
false);
|
|
}
|
|
|
|
bool
|
|
ExecIRInsertTriggers(EState *estate, ResultRelInfo *relinfo,
|
|
TupleTableSlot *slot)
|
|
{
|
|
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
|
|
HeapTuple newtuple = NULL;
|
|
bool should_free;
|
|
TriggerData LocTriggerData = {0};
|
|
int i;
|
|
|
|
LocTriggerData.type = T_TriggerData;
|
|
LocTriggerData.tg_event = TRIGGER_EVENT_INSERT |
|
|
TRIGGER_EVENT_ROW |
|
|
TRIGGER_EVENT_INSTEAD;
|
|
LocTriggerData.tg_relation = relinfo->ri_RelationDesc;
|
|
for (i = 0; i < trigdesc->numtriggers; i++)
|
|
{
|
|
Trigger *trigger = &trigdesc->triggers[i];
|
|
HeapTuple oldtuple;
|
|
|
|
if (!TRIGGER_TYPE_MATCHES(trigger->tgtype,
|
|
TRIGGER_TYPE_ROW,
|
|
TRIGGER_TYPE_INSTEAD,
|
|
TRIGGER_TYPE_INSERT))
|
|
continue;
|
|
if (!TriggerEnabled(estate, relinfo, trigger, LocTriggerData.tg_event,
|
|
NULL, NULL, slot))
|
|
continue;
|
|
|
|
if (!newtuple)
|
|
newtuple = ExecFetchSlotHeapTuple(slot, true, &should_free);
|
|
|
|
LocTriggerData.tg_trigslot = slot;
|
|
LocTriggerData.tg_trigtuple = oldtuple = newtuple;
|
|
LocTriggerData.tg_trigger = trigger;
|
|
newtuple = ExecCallTriggerFunc(&LocTriggerData,
|
|
i,
|
|
relinfo->ri_TrigFunctions,
|
|
relinfo->ri_TrigInstrument,
|
|
GetPerTupleMemoryContext(estate));
|
|
if (newtuple == NULL)
|
|
{
|
|
if (should_free)
|
|
heap_freetuple(oldtuple);
|
|
return false; /* "do nothing" */
|
|
}
|
|
else if (newtuple != oldtuple)
|
|
{
|
|
ExecForceStoreHeapTuple(newtuple, slot, false);
|
|
|
|
if (should_free)
|
|
heap_freetuple(oldtuple);
|
|
|
|
/* signal tuple should be re-fetched if used */
|
|
newtuple = NULL;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void
|
|
ExecBSDeleteTriggers(EState *estate, ResultRelInfo *relinfo)
|
|
{
|
|
TriggerDesc *trigdesc;
|
|
int i;
|
|
TriggerData LocTriggerData = {0};
|
|
|
|
trigdesc = relinfo->ri_TrigDesc;
|
|
|
|
if (trigdesc == NULL)
|
|
return;
|
|
if (!trigdesc->trig_delete_before_statement)
|
|
return;
|
|
|
|
/* no-op if we already fired BS triggers in this context */
|
|
if (before_stmt_triggers_fired(RelationGetRelid(relinfo->ri_RelationDesc),
|
|
CMD_DELETE))
|
|
return;
|
|
|
|
LocTriggerData.type = T_TriggerData;
|
|
LocTriggerData.tg_event = TRIGGER_EVENT_DELETE |
|
|
TRIGGER_EVENT_BEFORE;
|
|
LocTriggerData.tg_relation = relinfo->ri_RelationDesc;
|
|
for (i = 0; i < trigdesc->numtriggers; i++)
|
|
{
|
|
Trigger *trigger = &trigdesc->triggers[i];
|
|
HeapTuple newtuple;
|
|
|
|
if (!TRIGGER_TYPE_MATCHES(trigger->tgtype,
|
|
TRIGGER_TYPE_STATEMENT,
|
|
TRIGGER_TYPE_BEFORE,
|
|
TRIGGER_TYPE_DELETE))
|
|
continue;
|
|
if (!TriggerEnabled(estate, relinfo, trigger, LocTriggerData.tg_event,
|
|
NULL, NULL, NULL))
|
|
continue;
|
|
|
|
LocTriggerData.tg_trigger = trigger;
|
|
newtuple = ExecCallTriggerFunc(&LocTriggerData,
|
|
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,
|
|
TransitionCaptureState *transition_capture)
|
|
{
|
|
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
|
|
|
|
if (trigdesc && trigdesc->trig_delete_after_statement)
|
|
AfterTriggerSaveEvent(estate, relinfo, NULL, NULL,
|
|
TRIGGER_EVENT_DELETE,
|
|
false, NULL, NULL, NIL, NULL, transition_capture,
|
|
false);
|
|
}
|
|
|
|
/*
|
|
* Execute BEFORE ROW DELETE triggers.
|
|
*
|
|
* True indicates caller can proceed with the delete. False indicates caller
|
|
* need to suppress the delete and additionally if requested, we need to pass
|
|
* back the concurrently updated tuple if any.
|
|
*/
|
|
bool
|
|
ExecBRDeleteTriggers(EState *estate, EPQState *epqstate,
|
|
ResultRelInfo *relinfo,
|
|
ItemPointer tupleid,
|
|
HeapTuple fdw_trigtuple,
|
|
TupleTableSlot **epqslot)
|
|
{
|
|
TupleTableSlot *slot = ExecGetTriggerOldSlot(estate, relinfo);
|
|
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
|
|
bool result = true;
|
|
TriggerData LocTriggerData = {0};
|
|
HeapTuple trigtuple;
|
|
bool should_free = false;
|
|
int i;
|
|
|
|
Assert(HeapTupleIsValid(fdw_trigtuple) ^ ItemPointerIsValid(tupleid));
|
|
if (fdw_trigtuple == NULL)
|
|
{
|
|
TupleTableSlot *epqslot_candidate = NULL;
|
|
|
|
if (!GetTupleForTrigger(estate, epqstate, relinfo, tupleid,
|
|
LockTupleExclusive, slot, &epqslot_candidate,
|
|
NULL))
|
|
return false;
|
|
|
|
/*
|
|
* If the tuple was concurrently updated and the caller of this
|
|
* function requested for the updated tuple, skip the trigger
|
|
* execution.
|
|
*/
|
|
if (epqslot_candidate != NULL && epqslot != NULL)
|
|
{
|
|
*epqslot = epqslot_candidate;
|
|
return false;
|
|
}
|
|
|
|
trigtuple = ExecFetchSlotHeapTuple(slot, true, &should_free);
|
|
}
|
|
else
|
|
{
|
|
trigtuple = fdw_trigtuple;
|
|
ExecForceStoreHeapTuple(trigtuple, slot, false);
|
|
}
|
|
|
|
LocTriggerData.type = T_TriggerData;
|
|
LocTriggerData.tg_event = TRIGGER_EVENT_DELETE |
|
|
TRIGGER_EVENT_ROW |
|
|
TRIGGER_EVENT_BEFORE;
|
|
LocTriggerData.tg_relation = relinfo->ri_RelationDesc;
|
|
for (i = 0; i < trigdesc->numtriggers; i++)
|
|
{
|
|
HeapTuple newtuple;
|
|
Trigger *trigger = &trigdesc->triggers[i];
|
|
|
|
if (!TRIGGER_TYPE_MATCHES(trigger->tgtype,
|
|
TRIGGER_TYPE_ROW,
|
|
TRIGGER_TYPE_BEFORE,
|
|
TRIGGER_TYPE_DELETE))
|
|
continue;
|
|
if (!TriggerEnabled(estate, relinfo, trigger, LocTriggerData.tg_event,
|
|
NULL, slot, NULL))
|
|
continue;
|
|
|
|
LocTriggerData.tg_trigslot = slot;
|
|
LocTriggerData.tg_trigtuple = trigtuple;
|
|
LocTriggerData.tg_trigger = trigger;
|
|
newtuple = ExecCallTriggerFunc(&LocTriggerData,
|
|
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);
|
|
}
|
|
if (should_free)
|
|
heap_freetuple(trigtuple);
|
|
|
|
return result;
|
|
}
|
|
|
|
/*
|
|
* Note: is_crosspart_update must be true if the DELETE is being performed
|
|
* as part of a cross-partition update.
|
|
*/
|
|
void
|
|
ExecARDeleteTriggers(EState *estate,
|
|
ResultRelInfo *relinfo,
|
|
ItemPointer tupleid,
|
|
HeapTuple fdw_trigtuple,
|
|
TransitionCaptureState *transition_capture,
|
|
bool is_crosspart_update)
|
|
{
|
|
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
|
|
|
|
if ((trigdesc && trigdesc->trig_delete_after_row) ||
|
|
(transition_capture && transition_capture->tcs_delete_old_table))
|
|
{
|
|
TupleTableSlot *slot = ExecGetTriggerOldSlot(estate, relinfo);
|
|
|
|
Assert(HeapTupleIsValid(fdw_trigtuple) ^ ItemPointerIsValid(tupleid));
|
|
if (fdw_trigtuple == NULL)
|
|
GetTupleForTrigger(estate,
|
|
NULL,
|
|
relinfo,
|
|
tupleid,
|
|
LockTupleExclusive,
|
|
slot,
|
|
NULL,
|
|
NULL);
|
|
else
|
|
ExecForceStoreHeapTuple(fdw_trigtuple, slot, false);
|
|
|
|
AfterTriggerSaveEvent(estate, relinfo, NULL, NULL,
|
|
TRIGGER_EVENT_DELETE,
|
|
true, slot, NULL, NIL, NULL,
|
|
transition_capture,
|
|
is_crosspart_update);
|
|
}
|
|
}
|
|
|
|
bool
|
|
ExecIRDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
|
|
HeapTuple trigtuple)
|
|
{
|
|
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
|
|
TupleTableSlot *slot = ExecGetTriggerOldSlot(estate, relinfo);
|
|
TriggerData LocTriggerData = {0};
|
|
int i;
|
|
|
|
LocTriggerData.type = T_TriggerData;
|
|
LocTriggerData.tg_event = TRIGGER_EVENT_DELETE |
|
|
TRIGGER_EVENT_ROW |
|
|
TRIGGER_EVENT_INSTEAD;
|
|
LocTriggerData.tg_relation = relinfo->ri_RelationDesc;
|
|
|
|
ExecForceStoreHeapTuple(trigtuple, slot, false);
|
|
|
|
for (i = 0; i < trigdesc->numtriggers; i++)
|
|
{
|
|
HeapTuple rettuple;
|
|
Trigger *trigger = &trigdesc->triggers[i];
|
|
|
|
if (!TRIGGER_TYPE_MATCHES(trigger->tgtype,
|
|
TRIGGER_TYPE_ROW,
|
|
TRIGGER_TYPE_INSTEAD,
|
|
TRIGGER_TYPE_DELETE))
|
|
continue;
|
|
if (!TriggerEnabled(estate, relinfo, trigger, LocTriggerData.tg_event,
|
|
NULL, slot, NULL))
|
|
continue;
|
|
|
|
LocTriggerData.tg_trigslot = slot;
|
|
LocTriggerData.tg_trigtuple = trigtuple;
|
|
LocTriggerData.tg_trigger = trigger;
|
|
rettuple = ExecCallTriggerFunc(&LocTriggerData,
|
|
i,
|
|
relinfo->ri_TrigFunctions,
|
|
relinfo->ri_TrigInstrument,
|
|
GetPerTupleMemoryContext(estate));
|
|
if (rettuple == NULL)
|
|
return false; /* Delete was suppressed */
|
|
if (rettuple != trigtuple)
|
|
heap_freetuple(rettuple);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void
|
|
ExecBSUpdateTriggers(EState *estate, ResultRelInfo *relinfo)
|
|
{
|
|
TriggerDesc *trigdesc;
|
|
int i;
|
|
TriggerData LocTriggerData = {0};
|
|
Bitmapset *updatedCols;
|
|
|
|
trigdesc = relinfo->ri_TrigDesc;
|
|
|
|
if (trigdesc == NULL)
|
|
return;
|
|
if (!trigdesc->trig_update_before_statement)
|
|
return;
|
|
|
|
/* no-op if we already fired BS triggers in this context */
|
|
if (before_stmt_triggers_fired(RelationGetRelid(relinfo->ri_RelationDesc),
|
|
CMD_UPDATE))
|
|
return;
|
|
|
|
/* statement-level triggers operate on the parent table */
|
|
Assert(relinfo->ri_RootResultRelInfo == NULL);
|
|
|
|
updatedCols = ExecGetAllUpdatedCols(relinfo, estate);
|
|
|
|
LocTriggerData.type = T_TriggerData;
|
|
LocTriggerData.tg_event = TRIGGER_EVENT_UPDATE |
|
|
TRIGGER_EVENT_BEFORE;
|
|
LocTriggerData.tg_relation = relinfo->ri_RelationDesc;
|
|
LocTriggerData.tg_updatedcols = updatedCols;
|
|
for (i = 0; i < trigdesc->numtriggers; i++)
|
|
{
|
|
Trigger *trigger = &trigdesc->triggers[i];
|
|
HeapTuple newtuple;
|
|
|
|
if (!TRIGGER_TYPE_MATCHES(trigger->tgtype,
|
|
TRIGGER_TYPE_STATEMENT,
|
|
TRIGGER_TYPE_BEFORE,
|
|
TRIGGER_TYPE_UPDATE))
|
|
continue;
|
|
if (!TriggerEnabled(estate, relinfo, trigger, LocTriggerData.tg_event,
|
|
updatedCols, NULL, NULL))
|
|
continue;
|
|
|
|
LocTriggerData.tg_trigger = trigger;
|
|
newtuple = ExecCallTriggerFunc(&LocTriggerData,
|
|
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,
|
|
TransitionCaptureState *transition_capture)
|
|
{
|
|
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
|
|
|
|
/* statement-level triggers operate on the parent table */
|
|
Assert(relinfo->ri_RootResultRelInfo == NULL);
|
|
|
|
if (trigdesc && trigdesc->trig_update_after_statement)
|
|
AfterTriggerSaveEvent(estate, relinfo, NULL, NULL,
|
|
TRIGGER_EVENT_UPDATE,
|
|
false, NULL, NULL, NIL,
|
|
ExecGetAllUpdatedCols(relinfo, estate),
|
|
transition_capture,
|
|
false);
|
|
}
|
|
|
|
bool
|
|
ExecBRUpdateTriggers(EState *estate, EPQState *epqstate,
|
|
ResultRelInfo *relinfo,
|
|
ItemPointer tupleid,
|
|
HeapTuple fdw_trigtuple,
|
|
TupleTableSlot *newslot,
|
|
TM_FailureData *tmfd)
|
|
{
|
|
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
|
|
TupleTableSlot *oldslot = ExecGetTriggerOldSlot(estate, relinfo);
|
|
HeapTuple newtuple = NULL;
|
|
HeapTuple trigtuple;
|
|
bool should_free_trig = false;
|
|
bool should_free_new = false;
|
|
TriggerData LocTriggerData = {0};
|
|
int i;
|
|
Bitmapset *updatedCols;
|
|
LockTupleMode lockmode;
|
|
|
|
/* Determine lock mode to use */
|
|
lockmode = ExecUpdateLockMode(estate, relinfo);
|
|
|
|
Assert(HeapTupleIsValid(fdw_trigtuple) ^ ItemPointerIsValid(tupleid));
|
|
if (fdw_trigtuple == NULL)
|
|
{
|
|
TupleTableSlot *epqslot_candidate = NULL;
|
|
|
|
/* get a copy of the on-disk tuple we are planning to update */
|
|
if (!GetTupleForTrigger(estate, epqstate, relinfo, tupleid,
|
|
lockmode, oldslot, &epqslot_candidate,
|
|
tmfd))
|
|
return false; /* cancel the update action */
|
|
|
|
/*
|
|
* In READ COMMITTED isolation level it's possible that target tuple
|
|
* was changed due to concurrent update. In that case we have a raw
|
|
* subplan output tuple in epqslot_candidate, and need to form a new
|
|
* insertable tuple using ExecGetUpdateNewTuple to replace the one we
|
|
* received in newslot. Neither we nor our callers have any further
|
|
* interest in the passed-in tuple, so it's okay to overwrite newslot
|
|
* with the newer data.
|
|
*
|
|
* (Typically, newslot was also generated by ExecGetUpdateNewTuple, so
|
|
* that epqslot_clean will be that same slot and the copy step below
|
|
* is not needed.)
|
|
*/
|
|
if (epqslot_candidate != NULL)
|
|
{
|
|
TupleTableSlot *epqslot_clean;
|
|
|
|
epqslot_clean = ExecGetUpdateNewTuple(relinfo, epqslot_candidate,
|
|
oldslot);
|
|
|
|
if (newslot != epqslot_clean)
|
|
ExecCopySlot(newslot, epqslot_clean);
|
|
}
|
|
|
|
trigtuple = ExecFetchSlotHeapTuple(oldslot, true, &should_free_trig);
|
|
}
|
|
else
|
|
{
|
|
ExecForceStoreHeapTuple(fdw_trigtuple, oldslot, false);
|
|
trigtuple = fdw_trigtuple;
|
|
}
|
|
|
|
LocTriggerData.type = T_TriggerData;
|
|
LocTriggerData.tg_event = TRIGGER_EVENT_UPDATE |
|
|
TRIGGER_EVENT_ROW |
|
|
TRIGGER_EVENT_BEFORE;
|
|
LocTriggerData.tg_relation = relinfo->ri_RelationDesc;
|
|
updatedCols = ExecGetAllUpdatedCols(relinfo, estate);
|
|
LocTriggerData.tg_updatedcols = updatedCols;
|
|
for (i = 0; i < trigdesc->numtriggers; i++)
|
|
{
|
|
Trigger *trigger = &trigdesc->triggers[i];
|
|
HeapTuple oldtuple;
|
|
|
|
if (!TRIGGER_TYPE_MATCHES(trigger->tgtype,
|
|
TRIGGER_TYPE_ROW,
|
|
TRIGGER_TYPE_BEFORE,
|
|
TRIGGER_TYPE_UPDATE))
|
|
continue;
|
|
if (!TriggerEnabled(estate, relinfo, trigger, LocTriggerData.tg_event,
|
|
updatedCols, oldslot, newslot))
|
|
continue;
|
|
|
|
if (!newtuple)
|
|
newtuple = ExecFetchSlotHeapTuple(newslot, true, &should_free_new);
|
|
|
|
LocTriggerData.tg_trigslot = oldslot;
|
|
LocTriggerData.tg_trigtuple = trigtuple;
|
|
LocTriggerData.tg_newtuple = oldtuple = newtuple;
|
|
LocTriggerData.tg_newslot = newslot;
|
|
LocTriggerData.tg_trigger = trigger;
|
|
newtuple = ExecCallTriggerFunc(&LocTriggerData,
|
|
i,
|
|
relinfo->ri_TrigFunctions,
|
|
relinfo->ri_TrigInstrument,
|
|
GetPerTupleMemoryContext(estate));
|
|
|
|
if (newtuple == NULL)
|
|
{
|
|
if (should_free_trig)
|
|
heap_freetuple(trigtuple);
|
|
if (should_free_new)
|
|
heap_freetuple(oldtuple);
|
|
return false; /* "do nothing" */
|
|
}
|
|
else if (newtuple != oldtuple)
|
|
{
|
|
ExecForceStoreHeapTuple(newtuple, newslot, false);
|
|
|
|
/*
|
|
* If the tuple returned by the trigger / being stored, is the old
|
|
* row version, and the heap tuple passed to the trigger was
|
|
* allocated locally, materialize the slot. Otherwise we might
|
|
* free it while still referenced by the slot.
|
|
*/
|
|
if (should_free_trig && newtuple == trigtuple)
|
|
ExecMaterializeSlot(newslot);
|
|
|
|
if (should_free_new)
|
|
heap_freetuple(oldtuple);
|
|
|
|
/* signal tuple should be re-fetched if used */
|
|
newtuple = NULL;
|
|
}
|
|
}
|
|
if (should_free_trig)
|
|
heap_freetuple(trigtuple);
|
|
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
* Note: 'src_partinfo' and 'dst_partinfo', when non-NULL, refer to the source
|
|
* and destination partitions, respectively, of a cross-partition update of
|
|
* the root partitioned table mentioned in the query, given by 'relinfo'.
|
|
* 'tupleid' in that case refers to the ctid of the "old" tuple in the source
|
|
* partition, and 'newslot' contains the "new" tuple in the destination
|
|
* partition. This interface allows to support the requirements of
|
|
* ExecCrossPartitionUpdateForeignKey(); is_crosspart_update must be true in
|
|
* that case.
|
|
*/
|
|
void
|
|
ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
|
|
ResultRelInfo *src_partinfo,
|
|
ResultRelInfo *dst_partinfo,
|
|
ItemPointer tupleid,
|
|
HeapTuple fdw_trigtuple,
|
|
TupleTableSlot *newslot,
|
|
List *recheckIndexes,
|
|
TransitionCaptureState *transition_capture,
|
|
bool is_crosspart_update)
|
|
{
|
|
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
|
|
|
|
if ((trigdesc && trigdesc->trig_update_after_row) ||
|
|
(transition_capture &&
|
|
(transition_capture->tcs_update_old_table ||
|
|
transition_capture->tcs_update_new_table)))
|
|
{
|
|
/*
|
|
* Note: if the UPDATE is converted into a DELETE+INSERT as part of
|
|
* update-partition-key operation, then this function is also called
|
|
* separately for DELETE and INSERT to capture transition table rows.
|
|
* In such case, either old tuple or new tuple can be NULL.
|
|
*/
|
|
TupleTableSlot *oldslot;
|
|
ResultRelInfo *tupsrc;
|
|
|
|
Assert((src_partinfo != NULL && dst_partinfo != NULL) ||
|
|
!is_crosspart_update);
|
|
|
|
tupsrc = src_partinfo ? src_partinfo : relinfo;
|
|
oldslot = ExecGetTriggerOldSlot(estate, tupsrc);
|
|
|
|
if (fdw_trigtuple == NULL && ItemPointerIsValid(tupleid))
|
|
GetTupleForTrigger(estate,
|
|
NULL,
|
|
tupsrc,
|
|
tupleid,
|
|
LockTupleExclusive,
|
|
oldslot,
|
|
NULL,
|
|
NULL);
|
|
else if (fdw_trigtuple != NULL)
|
|
ExecForceStoreHeapTuple(fdw_trigtuple, oldslot, false);
|
|
else
|
|
ExecClearTuple(oldslot);
|
|
|
|
AfterTriggerSaveEvent(estate, relinfo,
|
|
src_partinfo, dst_partinfo,
|
|
TRIGGER_EVENT_UPDATE,
|
|
true,
|
|
oldslot, newslot, recheckIndexes,
|
|
ExecGetAllUpdatedCols(relinfo, estate),
|
|
transition_capture,
|
|
is_crosspart_update);
|
|
}
|
|
}
|
|
|
|
bool
|
|
ExecIRUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
|
|
HeapTuple trigtuple, TupleTableSlot *newslot)
|
|
{
|
|
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
|
|
TupleTableSlot *oldslot = ExecGetTriggerOldSlot(estate, relinfo);
|
|
HeapTuple newtuple = NULL;
|
|
bool should_free;
|
|
TriggerData LocTriggerData = {0};
|
|
int i;
|
|
|
|
LocTriggerData.type = T_TriggerData;
|
|
LocTriggerData.tg_event = TRIGGER_EVENT_UPDATE |
|
|
TRIGGER_EVENT_ROW |
|
|
TRIGGER_EVENT_INSTEAD;
|
|
LocTriggerData.tg_relation = relinfo->ri_RelationDesc;
|
|
|
|
ExecForceStoreHeapTuple(trigtuple, oldslot, false);
|
|
|
|
for (i = 0; i < trigdesc->numtriggers; i++)
|
|
{
|
|
Trigger *trigger = &trigdesc->triggers[i];
|
|
HeapTuple oldtuple;
|
|
|
|
if (!TRIGGER_TYPE_MATCHES(trigger->tgtype,
|
|
TRIGGER_TYPE_ROW,
|
|
TRIGGER_TYPE_INSTEAD,
|
|
TRIGGER_TYPE_UPDATE))
|
|
continue;
|
|
if (!TriggerEnabled(estate, relinfo, trigger, LocTriggerData.tg_event,
|
|
NULL, oldslot, newslot))
|
|
continue;
|
|
|
|
if (!newtuple)
|
|
newtuple = ExecFetchSlotHeapTuple(newslot, true, &should_free);
|
|
|
|
LocTriggerData.tg_trigslot = oldslot;
|
|
LocTriggerData.tg_trigtuple = trigtuple;
|
|
LocTriggerData.tg_newslot = newslot;
|
|
LocTriggerData.tg_newtuple = oldtuple = newtuple;
|
|
|
|
LocTriggerData.tg_trigger = trigger;
|
|
newtuple = ExecCallTriggerFunc(&LocTriggerData,
|
|
i,
|
|
relinfo->ri_TrigFunctions,
|
|
relinfo->ri_TrigInstrument,
|
|
GetPerTupleMemoryContext(estate));
|
|
if (newtuple == NULL)
|
|
{
|
|
return false; /* "do nothing" */
|
|
}
|
|
else if (newtuple != oldtuple)
|
|
{
|
|
ExecForceStoreHeapTuple(newtuple, newslot, false);
|
|
|
|
if (should_free)
|
|
heap_freetuple(oldtuple);
|
|
|
|
/* signal tuple should be re-fetched if used */
|
|
newtuple = NULL;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void
|
|
ExecBSTruncateTriggers(EState *estate, ResultRelInfo *relinfo)
|
|
{
|
|
TriggerDesc *trigdesc;
|
|
int i;
|
|
TriggerData LocTriggerData = {0};
|
|
|
|
trigdesc = relinfo->ri_TrigDesc;
|
|
|
|
if (trigdesc == NULL)
|
|
return;
|
|
if (!trigdesc->trig_truncate_before_statement)
|
|
return;
|
|
|
|
LocTriggerData.type = T_TriggerData;
|
|
LocTriggerData.tg_event = TRIGGER_EVENT_TRUNCATE |
|
|
TRIGGER_EVENT_BEFORE;
|
|
LocTriggerData.tg_relation = relinfo->ri_RelationDesc;
|
|
|
|
for (i = 0; i < trigdesc->numtriggers; i++)
|
|
{
|
|
Trigger *trigger = &trigdesc->triggers[i];
|
|
HeapTuple newtuple;
|
|
|
|
if (!TRIGGER_TYPE_MATCHES(trigger->tgtype,
|
|
TRIGGER_TYPE_STATEMENT,
|
|
TRIGGER_TYPE_BEFORE,
|
|
TRIGGER_TYPE_TRUNCATE))
|
|
continue;
|
|
if (!TriggerEnabled(estate, relinfo, trigger, LocTriggerData.tg_event,
|
|
NULL, NULL, NULL))
|
|
continue;
|
|
|
|
LocTriggerData.tg_trigger = trigger;
|
|
newtuple = ExecCallTriggerFunc(&LocTriggerData,
|
|
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->trig_truncate_after_statement)
|
|
AfterTriggerSaveEvent(estate, relinfo,
|
|
NULL, NULL,
|
|
TRIGGER_EVENT_TRUNCATE,
|
|
false, NULL, NULL, NIL, NULL, NULL,
|
|
false);
|
|
}
|
|
|
|
|
|
/*
|
|
* Fetch tuple into "oldslot", dealing with locking and EPQ if necessary
|
|
*/
|
|
static bool
|
|
GetTupleForTrigger(EState *estate,
|
|
EPQState *epqstate,
|
|
ResultRelInfo *relinfo,
|
|
ItemPointer tid,
|
|
LockTupleMode lockmode,
|
|
TupleTableSlot *oldslot,
|
|
TupleTableSlot **epqslot,
|
|
TM_FailureData *tmfdp)
|
|
{
|
|
Relation relation = relinfo->ri_RelationDesc;
|
|
|
|
if (epqslot != NULL)
|
|
{
|
|
TM_Result test;
|
|
TM_FailureData tmfd;
|
|
int lockflags = 0;
|
|
|
|
*epqslot = NULL;
|
|
|
|
/* caller must pass an epqstate if EvalPlanQual is possible */
|
|
Assert(epqstate != NULL);
|
|
|
|
/*
|
|
* lock tuple for update
|
|
*/
|
|
if (!IsolationUsesXactSnapshot())
|
|
lockflags |= TUPLE_LOCK_FLAG_FIND_LAST_VERSION;
|
|
test = table_tuple_lock(relation, tid, estate->es_snapshot, oldslot,
|
|
estate->es_output_cid,
|
|
lockmode, LockWaitBlock,
|
|
lockflags,
|
|
&tmfd);
|
|
|
|
/* Let the caller know about the status of this operation */
|
|
if (tmfdp)
|
|
*tmfdp = tmfd;
|
|
|
|
switch (test)
|
|
{
|
|
case TM_SelfModified:
|
|
|
|
/*
|
|
* The target tuple was already updated or deleted by the
|
|
* current command, or by a later command in the current
|
|
* transaction. We ignore the tuple in the former case, and
|
|
* throw error in the latter case, for the same reasons
|
|
* enumerated in ExecUpdate and ExecDelete in
|
|
* nodeModifyTable.c.
|
|
*/
|
|
if (tmfd.cmax != estate->es_output_cid)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_TRIGGERED_DATA_CHANGE_VIOLATION),
|
|
errmsg("tuple to be updated was already modified by an operation triggered by the current command"),
|
|
errhint("Consider using an AFTER trigger instead of a BEFORE trigger to propagate changes to other rows.")));
|
|
|
|
/* treat it as deleted; do not process */
|
|
return false;
|
|
|
|
case TM_Ok:
|
|
if (tmfd.traversed)
|
|
{
|
|
*epqslot = EvalPlanQual(epqstate,
|
|
relation,
|
|
relinfo->ri_RangeTableIndex,
|
|
oldslot);
|
|
|
|
/*
|
|
* If PlanQual failed for updated tuple - we must not
|
|
* process this tuple!
|
|
*/
|
|
if (TupIsNull(*epqslot))
|
|
{
|
|
*epqslot = NULL;
|
|
return false;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case TM_Updated:
|
|
if (IsolationUsesXactSnapshot())
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_T_R_SERIALIZATION_FAILURE),
|
|
errmsg("could not serialize access due to concurrent update")));
|
|
elog(ERROR, "unexpected table_tuple_lock status: %u", test);
|
|
break;
|
|
|
|
case TM_Deleted:
|
|
if (IsolationUsesXactSnapshot())
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_T_R_SERIALIZATION_FAILURE),
|
|
errmsg("could not serialize access due to concurrent delete")));
|
|
/* tuple was deleted */
|
|
return false;
|
|
|
|
case TM_Invisible:
|
|
elog(ERROR, "attempted to lock invisible tuple");
|
|
break;
|
|
|
|
default:
|
|
elog(ERROR, "unrecognized table_tuple_lock status: %u", test);
|
|
return false; /* keep compiler quiet */
|
|
}
|
|
}
|
|
else
|
|
{
|
|
/*
|
|
* We expect the tuple to be present, thus very simple error handling
|
|
* suffices.
|
|
*/
|
|
if (!table_tuple_fetch_row_version(relation, tid, SnapshotAny,
|
|
oldslot))
|
|
elog(ERROR, "failed to fetch tuple for trigger");
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
* Is trigger enabled to fire?
|
|
*/
|
|
static bool
|
|
TriggerEnabled(EState *estate, ResultRelInfo *relinfo,
|
|
Trigger *trigger, TriggerEvent event,
|
|
Bitmapset *modifiedCols,
|
|
TupleTableSlot *oldslot, TupleTableSlot *newslot)
|
|
{
|
|
/* Check replication-role-dependent enable state */
|
|
if (SessionReplicationRole == SESSION_REPLICATION_ROLE_REPLICA)
|
|
{
|
|
if (trigger->tgenabled == TRIGGER_FIRES_ON_ORIGIN ||
|
|
trigger->tgenabled == TRIGGER_DISABLED)
|
|
return false;
|
|
}
|
|
else /* ORIGIN or LOCAL role */
|
|
{
|
|
if (trigger->tgenabled == TRIGGER_FIRES_ON_REPLICA ||
|
|
trigger->tgenabled == TRIGGER_DISABLED)
|
|
return false;
|
|
}
|
|
|
|
/*
|
|
* Check for column-specific trigger (only possible for UPDATE, and in
|
|
* fact we *must* ignore tgattr for other event types)
|
|
*/
|
|
if (trigger->tgnattr > 0 && TRIGGER_FIRED_BY_UPDATE(event))
|
|
{
|
|
int i;
|
|
bool modified;
|
|
|
|
modified = false;
|
|
for (i = 0; i < trigger->tgnattr; i++)
|
|
{
|
|
if (bms_is_member(trigger->tgattr[i] - FirstLowInvalidHeapAttributeNumber,
|
|
modifiedCols))
|
|
{
|
|
modified = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!modified)
|
|
return false;
|
|
}
|
|
|
|
/* Check for WHEN clause */
|
|
if (trigger->tgqual)
|
|
{
|
|
ExprState **predicate;
|
|
ExprContext *econtext;
|
|
MemoryContext oldContext;
|
|
int i;
|
|
|
|
Assert(estate != NULL);
|
|
|
|
/*
|
|
* trigger is an element of relinfo->ri_TrigDesc->triggers[]; find the
|
|
* matching element of relinfo->ri_TrigWhenExprs[]
|
|
*/
|
|
i = trigger - relinfo->ri_TrigDesc->triggers;
|
|
predicate = &relinfo->ri_TrigWhenExprs[i];
|
|
|
|
/*
|
|
* If first time through for this WHEN expression, build expression
|
|
* nodetrees for it. Keep them in the per-query memory context so
|
|
* they'll survive throughout the query.
|
|
*/
|
|
if (*predicate == NULL)
|
|
{
|
|
Node *tgqual;
|
|
|
|
oldContext = MemoryContextSwitchTo(estate->es_query_cxt);
|
|
tgqual = stringToNode(trigger->tgqual);
|
|
/* Change references to OLD and NEW to INNER_VAR and OUTER_VAR */
|
|
ChangeVarNodes(tgqual, PRS2_OLD_VARNO, INNER_VAR, 0);
|
|
ChangeVarNodes(tgqual, PRS2_NEW_VARNO, OUTER_VAR, 0);
|
|
/* ExecPrepareQual wants implicit-AND form */
|
|
tgqual = (Node *) make_ands_implicit((Expr *) tgqual);
|
|
*predicate = ExecPrepareQual((List *) tgqual, estate);
|
|
MemoryContextSwitchTo(oldContext);
|
|
}
|
|
|
|
/*
|
|
* We will use the EState's per-tuple context for evaluating WHEN
|
|
* expressions (creating it if it's not already there).
|
|
*/
|
|
econtext = GetPerTupleExprContext(estate);
|
|
|
|
/*
|
|
* Finally evaluate the expression, making the old and/or new tuples
|
|
* available as INNER_VAR/OUTER_VAR respectively.
|
|
*/
|
|
econtext->ecxt_innertuple = oldslot;
|
|
econtext->ecxt_outertuple = newslot;
|
|
if (!ExecQual(*predicate, econtext))
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
/* ----------
|
|
* 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
|
|
* some data that can be discarded sooner appears in the CurTransactionContext
|
|
* of the relevant subtransaction. Also, the individual event records are
|
|
* kept in a separate sub-context of TopTransactionContext. This is done
|
|
* mainly so that it's easy to tell from a memory context dump how much space
|
|
* is being eaten by trigger events.
|
|
*
|
|
* Because the list of pending events can grow large, we go to some
|
|
* considerable effort to minimize per-event memory consumption. The event
|
|
* records are grouped into chunks and common data for similar events in the
|
|
* same chunk is only stored once.
|
|
*
|
|
* 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[FLEXIBLE_ARRAY_MEMBER];
|
|
} SetConstraintStateData;
|
|
|
|
typedef SetConstraintStateData *SetConstraintState;
|
|
|
|
|
|
/*
|
|
* Per-trigger-event data
|
|
*
|
|
* The actual per-event data, AfterTriggerEventData, includes DONE/IN_PROGRESS
|
|
* status bits, up to two tuple CTIDs, and optionally two OIDs of partitions.
|
|
* Each event record also has an associated AfterTriggerSharedData that is
|
|
* shared across all instances of similar events within a "chunk".
|
|
*
|
|
* For row-level triggers, we arrange not to waste storage on unneeded ctid
|
|
* fields. Updates of regular tables use two; inserts and deletes of regular
|
|
* tables use one; foreign tables always use zero and save the tuple(s) to a
|
|
* tuplestore. AFTER_TRIGGER_FDW_FETCH directs AfterTriggerExecute() to
|
|
* retrieve a fresh tuple or pair of tuples from that tuplestore, while
|
|
* AFTER_TRIGGER_FDW_REUSE directs it to use the most-recently-retrieved
|
|
* tuple(s). This permits storing tuples once regardless of the number of
|
|
* row-level triggers on a foreign table.
|
|
*
|
|
* When updates on partitioned tables cause rows to move between partitions,
|
|
* the OIDs of both partitions are stored too, so that the tuples can be
|
|
* fetched; such entries are marked AFTER_TRIGGER_CP_UPDATE (for "cross-
|
|
* partition update").
|
|
*
|
|
* Note that we need triggers on foreign tables to be fired in exactly the
|
|
* order they were queued, so that the tuples come out of the tuplestore in
|
|
* the right order. To ensure that, we forbid deferrable (constraint)
|
|
* triggers on foreign tables. This also ensures that such triggers do not
|
|
* get deferred into outer trigger query levels, meaning that it's okay to
|
|
* destroy the tuplestore at the end of the query level.
|
|
*
|
|
* Statement-level triggers always bear AFTER_TRIGGER_1CTID, though they
|
|
* require no ctid field. We lack the flag bit space to neatly represent that
|
|
* distinct case, and it seems unlikely to be worth much trouble.
|
|
*
|
|
* Note: ats_firing_id is initially zero and is set to something else when
|
|
* AFTER_TRIGGER_IN_PROGRESS is set. It indicates which trigger firing
|
|
* cycle the trigger will be fired in (or was fired in, if DONE is set).
|
|
* Although this is mutable state, we can keep it in AfterTriggerSharedData
|
|
* because all instances of the same type of event in a given event list will
|
|
* be fired at the same time, if they were queued between the same firing
|
|
* cycles. So we need only ensure that ats_firing_id is zero when attaching
|
|
* a new event to an existing AfterTriggerSharedData record.
|
|
*/
|
|
typedef uint32 TriggerFlags;
|
|
|
|
#define AFTER_TRIGGER_OFFSET 0x07FFFFFF /* must be low-order bits */
|
|
#define AFTER_TRIGGER_DONE 0x80000000
|
|
#define AFTER_TRIGGER_IN_PROGRESS 0x40000000
|
|
/* bits describing the size and tuple sources of this event */
|
|
#define AFTER_TRIGGER_FDW_REUSE 0x00000000
|
|
#define AFTER_TRIGGER_FDW_FETCH 0x20000000
|
|
#define AFTER_TRIGGER_1CTID 0x10000000
|
|
#define AFTER_TRIGGER_2CTID 0x30000000
|
|
#define AFTER_TRIGGER_CP_UPDATE 0x08000000
|
|
#define AFTER_TRIGGER_TUP_BITS 0x38000000
|
|
typedef struct AfterTriggerSharedData *AfterTriggerShared;
|
|
|
|
typedef struct AfterTriggerSharedData
|
|
{
|
|
TriggerEvent ats_event; /* event type indicator, see trigger.h */
|
|
Oid ats_tgoid; /* the trigger's ID */
|
|
Oid ats_relid; /* the relation it's on */
|
|
CommandId ats_firing_id; /* ID for firing cycle */
|
|
struct AfterTriggersTableData *ats_table; /* transition table access */
|
|
Bitmapset *ats_modifiedcols; /* modified columns */
|
|
} AfterTriggerSharedData;
|
|
|
|
typedef struct AfterTriggerEventData *AfterTriggerEvent;
|
|
|
|
typedef struct AfterTriggerEventData
|
|
{
|
|
TriggerFlags ate_flags; /* status bits and offset to shared data */
|
|
ItemPointerData ate_ctid1; /* inserted, deleted, or old updated tuple */
|
|
ItemPointerData ate_ctid2; /* new updated tuple */
|
|
|
|
/*
|
|
* During a cross-partition update of a partitioned table, we also store
|
|
* the OIDs of source and destination partitions that are needed to fetch
|
|
* the old (ctid1) and the new tuple (ctid2) from, respectively.
|
|
*/
|
|
Oid ate_src_part;
|
|
Oid ate_dst_part;
|
|
} AfterTriggerEventData;
|
|
|
|
/* AfterTriggerEventData, minus ate_src_part, ate_dst_part */
|
|
typedef struct AfterTriggerEventDataNoOids
|
|
{
|
|
TriggerFlags ate_flags;
|
|
ItemPointerData ate_ctid1;
|
|
ItemPointerData ate_ctid2;
|
|
} AfterTriggerEventDataNoOids;
|
|
|
|
/* AfterTriggerEventData, minus ate_*_part and ate_ctid2 */
|
|
typedef struct AfterTriggerEventDataOneCtid
|
|
{
|
|
TriggerFlags ate_flags; /* status bits and offset to shared data */
|
|
ItemPointerData ate_ctid1; /* inserted, deleted, or old updated tuple */
|
|
} AfterTriggerEventDataOneCtid;
|
|
|
|
/* AfterTriggerEventData, minus ate_*_part, ate_ctid1 and ate_ctid2 */
|
|
typedef struct AfterTriggerEventDataZeroCtids
|
|
{
|
|
TriggerFlags ate_flags; /* status bits and offset to shared data */
|
|
} AfterTriggerEventDataZeroCtids;
|
|
|
|
#define SizeofTriggerEvent(evt) \
|
|
(((evt)->ate_flags & AFTER_TRIGGER_TUP_BITS) == AFTER_TRIGGER_CP_UPDATE ? \
|
|
sizeof(AfterTriggerEventData) : \
|
|
(((evt)->ate_flags & AFTER_TRIGGER_TUP_BITS) == AFTER_TRIGGER_2CTID ? \
|
|
sizeof(AfterTriggerEventDataNoOids) : \
|
|
(((evt)->ate_flags & AFTER_TRIGGER_TUP_BITS) == AFTER_TRIGGER_1CTID ? \
|
|
sizeof(AfterTriggerEventDataOneCtid) : \
|
|
sizeof(AfterTriggerEventDataZeroCtids))))
|
|
|
|
#define GetTriggerSharedData(evt) \
|
|
((AfterTriggerShared) ((char *) (evt) + ((evt)->ate_flags & AFTER_TRIGGER_OFFSET)))
|
|
|
|
/*
|
|
* To avoid palloc overhead, we keep trigger events in arrays in successively-
|
|
* larger chunks (a slightly more sophisticated version of an expansible
|
|
* array). The space between CHUNK_DATA_START and freeptr is occupied by
|
|
* AfterTriggerEventData records; the space between endfree and endptr is
|
|
* occupied by AfterTriggerSharedData records.
|
|
*/
|
|
typedef struct AfterTriggerEventChunk
|
|
{
|
|
struct AfterTriggerEventChunk *next; /* list link */
|
|
char *freeptr; /* start of free space in chunk */
|
|
char *endfree; /* end of free space in chunk */
|
|
char *endptr; /* end of chunk */
|
|
/* event data follows here */
|
|
} AfterTriggerEventChunk;
|
|
|
|
#define CHUNK_DATA_START(cptr) ((char *) (cptr) + MAXALIGN(sizeof(AfterTriggerEventChunk)))
|
|
|
|
/* A list of events */
|
|
typedef struct AfterTriggerEventList
|
|
{
|
|
AfterTriggerEventChunk *head;
|
|
AfterTriggerEventChunk *tail;
|
|
char *tailfree; /* freeptr of tail chunk */
|
|
} AfterTriggerEventList;
|
|
|
|
/* Macros to help in iterating over a list of events */
|
|
#define for_each_chunk(cptr, evtlist) \
|
|
for (cptr = (evtlist).head; cptr != NULL; cptr = cptr->next)
|
|
#define for_each_event(eptr, cptr) \
|
|
for (eptr = (AfterTriggerEvent) CHUNK_DATA_START(cptr); \
|
|
(char *) eptr < (cptr)->freeptr; \
|
|
eptr = (AfterTriggerEvent) (((char *) eptr) + SizeofTriggerEvent(eptr)))
|
|
/* Use this if no special per-chunk processing is needed */
|
|
#define for_each_event_chunk(eptr, cptr, evtlist) \
|
|
for_each_chunk(cptr, evtlist) for_each_event(eptr, cptr)
|
|
|
|
/* Macros for iterating from a start point that might not be list start */
|
|
#define for_each_chunk_from(cptr) \
|
|
for (; cptr != NULL; cptr = cptr->next)
|
|
#define for_each_event_from(eptr, cptr) \
|
|
for (; \
|
|
(char *) eptr < (cptr)->freeptr; \
|
|
eptr = (AfterTriggerEvent) (((char *) eptr) + SizeofTriggerEvent(eptr)))
|
|
|
|
|
|
/*
|
|
* 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
|
|
* list chunks themselves are stored in event_cxt.
|
|
*
|
|
* query_depth is the current depth of nested AfterTriggerBeginQuery calls
|
|
* (-1 when the stack is empty).
|
|
*
|
|
* query_stack[query_depth] is the per-query-level data, including these fields:
|
|
*
|
|
* events is a list of AFTER trigger events queued by the current query.
|
|
* 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.
|
|
*
|
|
* fdw_tuplestore is a tuplestore containing the foreign-table tuples
|
|
* needed by events queued by the current query. (Note: we use just one
|
|
* tuplestore even though more than one foreign table might be involved.
|
|
* This is okay because tuplestores don't really care what's in the tuples
|
|
* they store; but it's possible that someday it'd break.)
|
|
*
|
|
* tables is a List of AfterTriggersTableData structs for target tables
|
|
* of the current query (see below).
|
|
*
|
|
* maxquerydepth is just the allocated length of query_stack.
|
|
*
|
|
* trans_stack holds per-subtransaction data, including these fields:
|
|
*
|
|
* state is NULL or a pointer to a saved copy 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 is a copy of the events head/tail pointers,
|
|
* which we use to restore those values during subtransaction abort.
|
|
*
|
|
* query_depth is the subtransaction-start-time value of query_depth,
|
|
* which we similarly use to clean up at subtransaction abort.
|
|
*
|
|
* firing_counter is the subtransaction-start-time value of 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 trans_stack. maxtransdepth is the number of allocated entries in
|
|
* trans_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.)
|
|
*
|
|
* We create an AfterTriggersTableData struct for each target table of the
|
|
* current query, and each operation mode (INSERT/UPDATE/DELETE), that has
|
|
* either transition tables or statement-level triggers. This is used to
|
|
* hold the relevant transition tables, as well as info tracking whether
|
|
* we already queued the statement triggers. (We use that info to prevent
|
|
* firing the same statement triggers more than once per statement, or really
|
|
* once per transition table set.) These structs, along with the transition
|
|
* table tuplestores, live in the (sub)transaction's CurTransactionContext.
|
|
* That's sufficient lifespan because we don't allow transition tables to be
|
|
* used by deferrable triggers, so they only need to survive until
|
|
* AfterTriggerEndQuery.
|
|
*/
|
|
typedef struct AfterTriggersQueryData AfterTriggersQueryData;
|
|
typedef struct AfterTriggersTransData AfterTriggersTransData;
|
|
typedef struct AfterTriggersTableData AfterTriggersTableData;
|
|
|
|
typedef struct AfterTriggersData
|
|
{
|
|
CommandId firing_counter; /* next firing ID to assign */
|
|
SetConstraintState state; /* the active S C state */
|
|
AfterTriggerEventList events; /* deferred-event list */
|
|
MemoryContext event_cxt; /* memory context for events, if any */
|
|
|
|
/* per-query-level data: */
|
|
AfterTriggersQueryData *query_stack; /* array of structs shown below */
|
|
int query_depth; /* current index in above array */
|
|
int maxquerydepth; /* allocated len of above array */
|
|
|
|
/* per-subtransaction-level data: */
|
|
AfterTriggersTransData *trans_stack; /* array of structs shown below */
|
|
int maxtransdepth; /* allocated len of above array */
|
|
} AfterTriggersData;
|
|
|
|
struct AfterTriggersQueryData
|
|
{
|
|
AfterTriggerEventList events; /* events pending from this query */
|
|
Tuplestorestate *fdw_tuplestore; /* foreign tuples for said events */
|
|
List *tables; /* list of AfterTriggersTableData, see below */
|
|
};
|
|
|
|
struct AfterTriggersTransData
|
|
{
|
|
/* these fields are just for resetting at subtrans abort: */
|
|
SetConstraintState state; /* saved S C state, or NULL if not yet saved */
|
|
AfterTriggerEventList events; /* saved list pointer */
|
|
int query_depth; /* saved query_depth */
|
|
CommandId firing_counter; /* saved firing_counter */
|
|
};
|
|
|
|
struct AfterTriggersTableData
|
|
{
|
|
/* relid + cmdType form the lookup key for these structs: */
|
|
Oid relid; /* target table's OID */
|
|
CmdType cmdType; /* event type, CMD_INSERT/UPDATE/DELETE */
|
|
bool closed; /* true when no longer OK to add tuples */
|
|
bool before_trig_done; /* did we already queue BS triggers? */
|
|
bool after_trig_done; /* did we already queue AS triggers? */
|
|
AfterTriggerEventList after_trig_events; /* if so, saved list pointer */
|
|
|
|
/*
|
|
* We maintain separate transition tables for UPDATE/INSERT/DELETE since
|
|
* MERGE can run all three actions in a single statement. Note that UPDATE
|
|
* needs both old and new transition tables whereas INSERT needs only new,
|
|
* and DELETE needs only old.
|
|
*/
|
|
|
|
/* "old" transition table for UPDATE, if any */
|
|
Tuplestorestate *old_upd_tuplestore;
|
|
/* "new" transition table for UPDATE, if any */
|
|
Tuplestorestate *new_upd_tuplestore;
|
|
/* "old" transition table for DELETE, if any */
|
|
Tuplestorestate *old_del_tuplestore;
|
|
/* "new" transition table for INSERT, if any */
|
|
Tuplestorestate *new_ins_tuplestore;
|
|
|
|
TupleTableSlot *storeslot; /* for converting to tuplestore's format */
|
|
};
|
|
|
|
static AfterTriggersData afterTriggers;
|
|
|
|
static void AfterTriggerExecute(EState *estate,
|
|
AfterTriggerEvent event,
|
|
ResultRelInfo *relInfo,
|
|
ResultRelInfo *src_relInfo,
|
|
ResultRelInfo *dst_relInfo,
|
|
TriggerDesc *trigdesc,
|
|
FmgrInfo *finfo,
|
|
Instrumentation *instr,
|
|
MemoryContext per_tuple_context,
|
|
TupleTableSlot *trig_tuple_slot1,
|
|
TupleTableSlot *trig_tuple_slot2);
|
|
static AfterTriggersTableData *GetAfterTriggersTableData(Oid relid,
|
|
CmdType cmdType);
|
|
static TupleTableSlot *GetAfterTriggersStoreSlot(AfterTriggersTableData *table,
|
|
TupleDesc tupdesc);
|
|
static Tuplestorestate *GetAfterTriggersTransitionTable(int event,
|
|
TupleTableSlot *oldslot,
|
|
TupleTableSlot *newslot,
|
|
TransitionCaptureState *transition_capture);
|
|
static void TransitionTableAddTuple(EState *estate,
|
|
TransitionCaptureState *transition_capture,
|
|
ResultRelInfo *relinfo,
|
|
TupleTableSlot *slot,
|
|
TupleTableSlot *original_insert_tuple,
|
|
Tuplestorestate *tuplestore);
|
|
static void AfterTriggerFreeQuery(AfterTriggersQueryData *qs);
|
|
static SetConstraintState SetConstraintStateCreate(int numalloc);
|
|
static SetConstraintState SetConstraintStateCopy(SetConstraintState state);
|
|
static SetConstraintState SetConstraintStateAddItem(SetConstraintState state,
|
|
Oid tgoid, bool tgisdeferred);
|
|
static void cancel_prior_stmt_triggers(Oid relid, CmdType cmdType, int tgevent);
|
|
|
|
|
|
/*
|
|
* Get the FDW tuplestore for the current trigger query level, creating it
|
|
* if necessary.
|
|
*/
|
|
static Tuplestorestate *
|
|
GetCurrentFDWTuplestore(void)
|
|
{
|
|
Tuplestorestate *ret;
|
|
|
|
ret = afterTriggers.query_stack[afterTriggers.query_depth].fdw_tuplestore;
|
|
if (ret == NULL)
|
|
{
|
|
MemoryContext oldcxt;
|
|
ResourceOwner saveResourceOwner;
|
|
|
|
/*
|
|
* Make the tuplestore valid until end of subtransaction. We really
|
|
* only need it until AfterTriggerEndQuery().
|
|
*/
|
|
oldcxt = MemoryContextSwitchTo(CurTransactionContext);
|
|
saveResourceOwner = CurrentResourceOwner;
|
|
CurrentResourceOwner = CurTransactionResourceOwner;
|
|
|
|
ret = tuplestore_begin_heap(false, false, work_mem);
|
|
|
|
CurrentResourceOwner = saveResourceOwner;
|
|
MemoryContextSwitchTo(oldcxt);
|
|
|
|
afterTriggers.query_stack[afterTriggers.query_depth].fdw_tuplestore = ret;
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
/* ----------
|
|
* afterTriggerCheckState()
|
|
*
|
|
* Returns true if the trigger event is actually in state DEFERRED.
|
|
* ----------
|
|
*/
|
|
static bool
|
|
afterTriggerCheckState(AfterTriggerShared evtshared)
|
|
{
|
|
Oid tgoid = evtshared->ats_tgoid;
|
|
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 ((evtshared->ats_event & AFTER_TRIGGER_DEFERRABLE) == 0)
|
|
return false;
|
|
|
|
/*
|
|
* If constraint state exists, SET CONSTRAINTS might have been executed
|
|
* either for this trigger or for all triggers.
|
|
*/
|
|
if (state != NULL)
|
|
{
|
|
/* Check for SET CONSTRAINTS 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 for SET CONSTRAINTS ALL. */
|
|
if (state->all_isset)
|
|
return state->all_isdeferred;
|
|
}
|
|
|
|
/*
|
|
* Otherwise return the default state for the trigger.
|
|
*/
|
|
return ((evtshared->ats_event & AFTER_TRIGGER_INITDEFERRED) != 0);
|
|
}
|
|
|
|
|
|
/* ----------
|
|
* afterTriggerAddEvent()
|
|
*
|
|
* Add a new trigger event to the specified queue.
|
|
* The passed-in event data is copied.
|
|
* ----------
|
|
*/
|
|
static void
|
|
afterTriggerAddEvent(AfterTriggerEventList *events,
|
|
AfterTriggerEvent event, AfterTriggerShared evtshared)
|
|
{
|
|
Size eventsize = SizeofTriggerEvent(event);
|
|
Size needed = eventsize + sizeof(AfterTriggerSharedData);
|
|
AfterTriggerEventChunk *chunk;
|
|
AfterTriggerShared newshared;
|
|
AfterTriggerEvent newevent;
|
|
|
|
/*
|
|
* If empty list or not enough room in the tail chunk, make a new chunk.
|
|
* We assume here that a new shared record will always be needed.
|
|
*/
|
|
chunk = events->tail;
|
|
if (chunk == NULL ||
|
|
chunk->endfree - chunk->freeptr < needed)
|
|
{
|
|
Size chunksize;
|
|
|
|
/* Create event context if we didn't already */
|
|
if (afterTriggers.event_cxt == NULL)
|
|
afterTriggers.event_cxt =
|
|
AllocSetContextCreate(TopTransactionContext,
|
|
"AfterTriggerEvents",
|
|
ALLOCSET_DEFAULT_SIZES);
|
|
|
|
/*
|
|
* Chunk size starts at 1KB and is allowed to increase up to 1MB.
|
|
* These numbers are fairly arbitrary, though there is a hard limit at
|
|
* AFTER_TRIGGER_OFFSET; else we couldn't link event records to their
|
|
* shared records using the available space in ate_flags. Another
|
|
* constraint is that if the chunk size gets too huge, the search loop
|
|
* below would get slow given a (not too common) usage pattern with
|
|
* many distinct event types in a chunk. Therefore, we double the
|
|
* preceding chunk size only if there weren't too many shared records
|
|
* in the preceding chunk; otherwise we halve it. This gives us some
|
|
* ability to adapt to the actual usage pattern of the current query
|
|
* while still having large chunk sizes in typical usage. All chunk
|
|
* sizes used should be MAXALIGN multiples, to ensure that the shared
|
|
* records will be aligned safely.
|
|
*/
|
|
#define MIN_CHUNK_SIZE 1024
|
|
#define MAX_CHUNK_SIZE (1024*1024)
|
|
|
|
#if MAX_CHUNK_SIZE > (AFTER_TRIGGER_OFFSET+1)
|
|
#error MAX_CHUNK_SIZE must not exceed AFTER_TRIGGER_OFFSET
|
|
#endif
|
|
|
|
if (chunk == NULL)
|
|
chunksize = MIN_CHUNK_SIZE;
|
|
else
|
|
{
|
|
/* preceding chunk size... */
|
|
chunksize = chunk->endptr - (char *) chunk;
|
|
/* check number of shared records in preceding chunk */
|
|
if ((chunk->endptr - chunk->endfree) <=
|
|
(100 * sizeof(AfterTriggerSharedData)))
|
|
chunksize *= 2; /* okay, double it */
|
|
else
|
|
chunksize /= 2; /* too many shared records */
|
|
chunksize = Min(chunksize, MAX_CHUNK_SIZE);
|
|
}
|
|
chunk = MemoryContextAlloc(afterTriggers.event_cxt, chunksize);
|
|
chunk->next = NULL;
|
|
chunk->freeptr = CHUNK_DATA_START(chunk);
|
|
chunk->endptr = chunk->endfree = (char *) chunk + chunksize;
|
|
Assert(chunk->endfree - chunk->freeptr >= needed);
|
|
|
|
if (events->head == NULL)
|
|
events->head = chunk;
|
|
else
|
|
events->tail->next = chunk;
|
|
events->tail = chunk;
|
|
/* events->tailfree is now out of sync, but we'll fix it below */
|
|
}
|
|
|
|
/*
|
|
* Try to locate a matching shared-data record already in the chunk. If
|
|
* none, make a new one.
|
|
*/
|
|
for (newshared = ((AfterTriggerShared) chunk->endptr) - 1;
|
|
(char *) newshared >= chunk->endfree;
|
|
newshared--)
|
|
{
|
|
if (newshared->ats_tgoid == evtshared->ats_tgoid &&
|
|
newshared->ats_relid == evtshared->ats_relid &&
|
|
newshared->ats_event == evtshared->ats_event &&
|
|
newshared->ats_table == evtshared->ats_table &&
|
|
newshared->ats_firing_id == 0)
|
|
break;
|
|
}
|
|
if ((char *) newshared < chunk->endfree)
|
|
{
|
|
*newshared = *evtshared;
|
|
newshared->ats_firing_id = 0; /* just to be sure */
|
|
chunk->endfree = (char *) newshared;
|
|
}
|
|
|
|
/* Insert the data */
|
|
newevent = (AfterTriggerEvent) chunk->freeptr;
|
|
memcpy(newevent, event, eventsize);
|
|
/* ... and link the new event to its shared record */
|
|
newevent->ate_flags &= ~AFTER_TRIGGER_OFFSET;
|
|
newevent->ate_flags |= (char *) newshared - (char *) newevent;
|
|
|
|
chunk->freeptr += eventsize;
|
|
events->tailfree = chunk->freeptr;
|
|
}
|
|
|
|
/* ----------
|
|
* afterTriggerFreeEventList()
|
|
*
|
|
* Free all the event storage in the given list.
|
|
* ----------
|
|
*/
|
|
static void
|
|
afterTriggerFreeEventList(AfterTriggerEventList *events)
|
|
{
|
|
AfterTriggerEventChunk *chunk;
|
|
|
|
while ((chunk = events->head) != NULL)
|
|
{
|
|
events->head = chunk->next;
|
|
pfree(chunk);
|
|
}
|
|
events->tail = NULL;
|
|
events->tailfree = NULL;
|
|
}
|
|
|
|
/* ----------
|
|
* afterTriggerRestoreEventList()
|
|
*
|
|
* Restore an event list to its prior length, removing all the events
|
|
* added since it had the value old_events.
|
|
* ----------
|
|
*/
|
|
static void
|
|
afterTriggerRestoreEventList(AfterTriggerEventList *events,
|
|
const AfterTriggerEventList *old_events)
|
|
{
|
|
AfterTriggerEventChunk *chunk;
|
|
AfterTriggerEventChunk *next_chunk;
|
|
|
|
if (old_events->tail == NULL)
|
|
{
|
|
/* restoring to a completely empty state, so free everything */
|
|
afterTriggerFreeEventList(events);
|
|
}
|
|
else
|
|
{
|
|
*events = *old_events;
|
|
/* free any chunks after the last one we want to keep */
|
|
for (chunk = events->tail->next; chunk != NULL; chunk = next_chunk)
|
|
{
|
|
next_chunk = chunk->next;
|
|
pfree(chunk);
|
|
}
|
|
/* and clean up the tail chunk to be the right length */
|
|
events->tail->next = NULL;
|
|
events->tail->freeptr = events->tailfree;
|
|
|
|
/*
|
|
* We don't make any effort to remove now-unused shared data records.
|
|
* They might still be useful, anyway.
|
|
*/
|
|
}
|
|
}
|
|
|
|
/* ----------
|
|
* afterTriggerDeleteHeadEventChunk()
|
|
*
|
|
* Remove the first chunk of events from the query level's event list.
|
|
* Keep any event list pointers elsewhere in the query level's data
|
|
* structures in sync.
|
|
* ----------
|
|
*/
|
|
static void
|
|
afterTriggerDeleteHeadEventChunk(AfterTriggersQueryData *qs)
|
|
{
|
|
AfterTriggerEventChunk *target = qs->events.head;
|
|
ListCell *lc;
|
|
|
|
Assert(target && target->next);
|
|
|
|
/*
|
|
* First, update any pointers in the per-table data, so that they won't be
|
|
* dangling. Resetting obsoleted pointers to NULL will make
|
|
* cancel_prior_stmt_triggers start from the list head, which is fine.
|
|
*/
|
|
foreach(lc, qs->tables)
|
|
{
|
|
AfterTriggersTableData *table = (AfterTriggersTableData *) lfirst(lc);
|
|
|
|
if (table->after_trig_done &&
|
|
table->after_trig_events.tail == target)
|
|
{
|
|
table->after_trig_events.head = NULL;
|
|
table->after_trig_events.tail = NULL;
|
|
table->after_trig_events.tailfree = NULL;
|
|
}
|
|
}
|
|
|
|
/* Now we can flush the head chunk */
|
|
qs->events.head = target->next;
|
|
pfree(target);
|
|
}
|
|
|
|
|
|
/* ----------
|
|
* 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.)
|
|
*
|
|
* When fired for a cross-partition update of a partitioned table, the old
|
|
* tuple is fetched using 'src_relInfo' (the source leaf partition) and
|
|
* the new tuple using 'dst_relInfo' (the destination leaf partition), though
|
|
* both are converted into the root partitioned table's format before passing
|
|
* to the trigger function.
|
|
*
|
|
* event: event currently being fired.
|
|
* relInfo: result relation for event.
|
|
* src_relInfo: source partition of a cross-partition update
|
|
* dst_relInfo: its destination partition
|
|
* 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.
|
|
* trig_tuple_slot1: scratch slot for tg_trigtuple (foreign tables only)
|
|
* trig_tuple_slot2: scratch slot for tg_newtuple (foreign tables only)
|
|
* ----------
|
|
*/
|
|
static void
|
|
AfterTriggerExecute(EState *estate,
|
|
AfterTriggerEvent event,
|
|
ResultRelInfo *relInfo,
|
|
ResultRelInfo *src_relInfo,
|
|
ResultRelInfo *dst_relInfo,
|
|
TriggerDesc *trigdesc,
|
|
FmgrInfo *finfo, Instrumentation *instr,
|
|
MemoryContext per_tuple_context,
|
|
TupleTableSlot *trig_tuple_slot1,
|
|
TupleTableSlot *trig_tuple_slot2)
|
|
{
|
|
Relation rel = relInfo->ri_RelationDesc;
|
|
Relation src_rel = src_relInfo->ri_RelationDesc;
|
|
Relation dst_rel = dst_relInfo->ri_RelationDesc;
|
|
AfterTriggerShared evtshared = GetTriggerSharedData(event);
|
|
Oid tgoid = evtshared->ats_tgoid;
|
|
TriggerData LocTriggerData = {0};
|
|
HeapTuple rettuple;
|
|
int tgindx;
|
|
bool should_free_trig = false;
|
|
bool should_free_new = false;
|
|
|
|
/*
|
|
* Locate trigger in trigdesc.
|
|
*/
|
|
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 tuple(s).
|
|
*/
|
|
switch (event->ate_flags & AFTER_TRIGGER_TUP_BITS)
|
|
{
|
|
case AFTER_TRIGGER_FDW_FETCH:
|
|
{
|
|
Tuplestorestate *fdw_tuplestore = GetCurrentFDWTuplestore();
|
|
|
|
if (!tuplestore_gettupleslot(fdw_tuplestore, true, false,
|
|
trig_tuple_slot1))
|
|
elog(ERROR, "failed to fetch tuple1 for AFTER trigger");
|
|
|
|
if ((evtshared->ats_event & TRIGGER_EVENT_OPMASK) ==
|
|
TRIGGER_EVENT_UPDATE &&
|
|
!tuplestore_gettupleslot(fdw_tuplestore, true, false,
|
|
trig_tuple_slot2))
|
|
elog(ERROR, "failed to fetch tuple2 for AFTER trigger");
|
|
}
|
|
/* fall through */
|
|
case AFTER_TRIGGER_FDW_REUSE:
|
|
|
|
/*
|
|
* Store tuple in the slot so that tg_trigtuple does not reference
|
|
* tuplestore memory. (It is formally possible for the trigger
|
|
* function to queue trigger events that add to the same
|
|
* tuplestore, which can push other tuples out of memory.) The
|
|
* distinction is academic, because we start with a minimal tuple
|
|
* that is stored as a heap tuple, constructed in different memory
|
|
* context, in the slot anyway.
|
|
*/
|
|
LocTriggerData.tg_trigslot = trig_tuple_slot1;
|
|
LocTriggerData.tg_trigtuple =
|
|
ExecFetchSlotHeapTuple(trig_tuple_slot1, true, &should_free_trig);
|
|
|
|
if ((evtshared->ats_event & TRIGGER_EVENT_OPMASK) ==
|
|
TRIGGER_EVENT_UPDATE)
|
|
{
|
|
LocTriggerData.tg_newslot = trig_tuple_slot2;
|
|
LocTriggerData.tg_newtuple =
|
|
ExecFetchSlotHeapTuple(trig_tuple_slot2, true, &should_free_new);
|
|
}
|
|
else
|
|
{
|
|
LocTriggerData.tg_newtuple = NULL;
|
|
}
|
|
break;
|
|
|
|
default:
|
|
if (ItemPointerIsValid(&(event->ate_ctid1)))
|
|
{
|
|
TupleTableSlot *src_slot = ExecGetTriggerOldSlot(estate,
|
|
src_relInfo);
|
|
|
|
if (!table_tuple_fetch_row_version(src_rel,
|
|
&(event->ate_ctid1),
|
|
SnapshotAny,
|
|
src_slot))
|
|
elog(ERROR, "failed to fetch tuple1 for AFTER trigger");
|
|
|
|
/*
|
|
* Store the tuple fetched from the source partition into the
|
|
* target (root partitioned) table slot, converting if needed.
|
|
*/
|
|
if (src_relInfo != relInfo)
|
|
{
|
|
TupleConversionMap *map = ExecGetChildToRootMap(src_relInfo);
|
|
|
|
LocTriggerData.tg_trigslot = ExecGetTriggerOldSlot(estate, relInfo);
|
|
if (map)
|
|
{
|
|
execute_attr_map_slot(map->attrMap,
|
|
src_slot,
|
|
LocTriggerData.tg_trigslot);
|
|
}
|
|
else
|
|
ExecCopySlot(LocTriggerData.tg_trigslot, src_slot);
|
|
}
|
|
else
|
|
LocTriggerData.tg_trigslot = src_slot;
|
|
LocTriggerData.tg_trigtuple =
|
|
ExecFetchSlotHeapTuple(LocTriggerData.tg_trigslot, false, &should_free_trig);
|
|
}
|
|
else
|
|
{
|
|
LocTriggerData.tg_trigtuple = NULL;
|
|
}
|
|
|
|
/* don't touch ctid2 if not there */
|
|
if (((event->ate_flags & AFTER_TRIGGER_TUP_BITS) == AFTER_TRIGGER_2CTID ||
|
|
(event->ate_flags & AFTER_TRIGGER_CP_UPDATE)) &&
|
|
ItemPointerIsValid(&(event->ate_ctid2)))
|
|
{
|
|
TupleTableSlot *dst_slot = ExecGetTriggerNewSlot(estate,
|
|
dst_relInfo);
|
|
|
|
if (!table_tuple_fetch_row_version(dst_rel,
|
|
&(event->ate_ctid2),
|
|
SnapshotAny,
|
|
dst_slot))
|
|
elog(ERROR, "failed to fetch tuple2 for AFTER trigger");
|
|
|
|
/*
|
|
* Store the tuple fetched from the destination partition into
|
|
* the target (root partitioned) table slot, converting if
|
|
* needed.
|
|
*/
|
|
if (dst_relInfo != relInfo)
|
|
{
|
|
TupleConversionMap *map = ExecGetChildToRootMap(dst_relInfo);
|
|
|
|
LocTriggerData.tg_newslot = ExecGetTriggerNewSlot(estate, relInfo);
|
|
if (map)
|
|
{
|
|
execute_attr_map_slot(map->attrMap,
|
|
dst_slot,
|
|
LocTriggerData.tg_newslot);
|
|
}
|
|
else
|
|
ExecCopySlot(LocTriggerData.tg_newslot, dst_slot);
|
|
}
|
|
else
|
|
LocTriggerData.tg_newslot = dst_slot;
|
|
LocTriggerData.tg_newtuple =
|
|
ExecFetchSlotHeapTuple(LocTriggerData.tg_newslot, false, &should_free_new);
|
|
}
|
|
else
|
|
{
|
|
LocTriggerData.tg_newtuple = NULL;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Set up the tuplestore information to let the trigger have access to
|
|
* transition tables. When we first make a transition table available to
|
|
* a trigger, mark it "closed" so that it cannot change anymore. If any
|
|
* additional events of the same type get queued in the current trigger
|
|
* query level, they'll go into new transition tables.
|
|
*/
|
|
LocTriggerData.tg_oldtable = LocTriggerData.tg_newtable = NULL;
|
|
if (evtshared->ats_table)
|
|
{
|
|
if (LocTriggerData.tg_trigger->tgoldtable)
|
|
{
|
|
if (TRIGGER_FIRED_BY_UPDATE(evtshared->ats_event))
|
|
LocTriggerData.tg_oldtable = evtshared->ats_table->old_upd_tuplestore;
|
|
else
|
|
LocTriggerData.tg_oldtable = evtshared->ats_table->old_del_tuplestore;
|
|
evtshared->ats_table->closed = true;
|
|
}
|
|
|
|
if (LocTriggerData.tg_trigger->tgnewtable)
|
|
{
|
|
if (TRIGGER_FIRED_BY_INSERT(evtshared->ats_event))
|
|
LocTriggerData.tg_newtable = evtshared->ats_table->new_ins_tuplestore;
|
|
else
|
|
LocTriggerData.tg_newtable = evtshared->ats_table->new_upd_tuplestore;
|
|
evtshared->ats_table->closed = true;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Setup the remaining trigger information
|
|
*/
|
|
LocTriggerData.type = T_TriggerData;
|
|
LocTriggerData.tg_event =
|
|
evtshared->ats_event & (TRIGGER_EVENT_OPMASK | TRIGGER_EVENT_ROW);
|
|
LocTriggerData.tg_relation = rel;
|
|
if (TRIGGER_FOR_UPDATE(LocTriggerData.tg_trigger->tgtype))
|
|
LocTriggerData.tg_updatedcols = evtshared->ats_modifiedcols;
|
|
|
|
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 != LocTriggerData.tg_trigtuple &&
|
|
rettuple != LocTriggerData.tg_newtuple)
|
|
heap_freetuple(rettuple);
|
|
|
|
/*
|
|
* Release resources
|
|
*/
|
|
if (should_free_trig)
|
|
heap_freetuple(LocTriggerData.tg_trigtuple);
|
|
if (should_free_new)
|
|
heap_freetuple(LocTriggerData.tg_newtuple);
|
|
|
|
/* don't clear slots' contents if foreign table */
|
|
if (trig_tuple_slot1 == NULL)
|
|
{
|
|
if (LocTriggerData.tg_trigslot)
|
|
ExecClearTuple(LocTriggerData.tg_trigslot);
|
|
if (LocTriggerData.tg_newslot)
|
|
ExecClearTuple(LocTriggerData.tg_newslot);
|
|
}
|
|
|
|
/*
|
|
* 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
|
|
* transferred 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;
|
|
bool deferred_found = false;
|
|
AfterTriggerEvent event;
|
|
AfterTriggerEventChunk *chunk;
|
|
|
|
for_each_event_chunk(event, chunk, *events)
|
|
{
|
|
AfterTriggerShared evtshared = GetTriggerSharedData(event);
|
|
bool defer_it = false;
|
|
|
|
if (!(event->ate_flags &
|
|
(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(evtshared))
|
|
{
|
|
defer_it = true;
|
|
}
|
|
else
|
|
{
|
|
/*
|
|
* Mark it as to be fired in this firing cycle.
|
|
*/
|
|
evtshared->ats_firing_id = afterTriggers.firing_counter;
|
|
event->ate_flags |= AFTER_TRIGGER_IN_PROGRESS;
|
|
found = true;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* If it's deferred, move it to move_list, if requested.
|
|
*/
|
|
if (defer_it && move_list != NULL)
|
|
{
|
|
deferred_found = true;
|
|
/* add it to move_list */
|
|
afterTriggerAddEvent(move_list, event, evtshared);
|
|
/* mark original copy "done" so we don't do it again */
|
|
event->ate_flags |= AFTER_TRIGGER_DONE;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* We could allow deferred triggers if, before the end of the
|
|
* security-restricted operation, we were to verify that a SET CONSTRAINTS
|
|
* ... IMMEDIATE has fired all such triggers. For now, don't bother.
|
|
*/
|
|
if (deferred_found && InSecurityRestrictedOperation())
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
|
|
errmsg("cannot fire deferred trigger within security-restricted operation")));
|
|
|
|
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 safe to delete fully-processed events.
|
|
* (We are not very tense about that: we simply reset a chunk to be empty
|
|
* if all its events got fired. The objective here is just to avoid useless
|
|
* rescanning of events when a trigger queues new events during transaction
|
|
* end, so it's not necessary to worry much about the case where only
|
|
* some events are fired.)
|
|
*
|
|
* Returns true if no unfired events remain in the list (this allows us
|
|
* to avoid repeating afterTriggerMarkEvents).
|
|
*/
|
|
static bool
|
|
afterTriggerInvokeEvents(AfterTriggerEventList *events,
|
|
CommandId firing_id,
|
|
EState *estate,
|
|
bool delete_ok)
|
|
{
|
|
bool all_fired = true;
|
|
AfterTriggerEventChunk *chunk;
|
|
MemoryContext per_tuple_context;
|
|
bool local_estate = false;
|
|
ResultRelInfo *rInfo = NULL;
|
|
Relation rel = NULL;
|
|
TriggerDesc *trigdesc = NULL;
|
|
FmgrInfo *finfo = NULL;
|
|
Instrumentation *instr = NULL;
|
|
TupleTableSlot *slot1 = NULL,
|
|
*slot2 = 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_SIZES);
|
|
|
|
for_each_chunk(chunk, *events)
|
|
{
|
|
AfterTriggerEvent event;
|
|
bool all_fired_in_chunk = true;
|
|
|
|
for_each_event(event, chunk)
|
|
{
|
|
AfterTriggerShared evtshared = GetTriggerSharedData(event);
|
|
|
|
/*
|
|
* Is it one for me to fire?
|
|
*/
|
|
if ((event->ate_flags & AFTER_TRIGGER_IN_PROGRESS) &&
|
|
evtshared->ats_firing_id == firing_id)
|
|
{
|
|
ResultRelInfo *src_rInfo,
|
|
*dst_rInfo;
|
|
|
|
/*
|
|
* 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) != evtshared->ats_relid)
|
|
{
|
|
rInfo = ExecGetTriggerResultRel(estate, evtshared->ats_relid,
|
|
NULL);
|
|
rel = rInfo->ri_RelationDesc;
|
|
/* Catch calls with insufficient relcache refcounting */
|
|
Assert(!RelationHasReferenceCountZero(rel));
|
|
trigdesc = rInfo->ri_TrigDesc;
|
|
finfo = rInfo->ri_TrigFunctions;
|
|
instr = rInfo->ri_TrigInstrument;
|
|
if (slot1 != NULL)
|
|
{
|
|
ExecDropSingleTupleTableSlot(slot1);
|
|
ExecDropSingleTupleTableSlot(slot2);
|
|
slot1 = slot2 = NULL;
|
|
}
|
|
if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
|
|
{
|
|
slot1 = MakeSingleTupleTableSlot(rel->rd_att,
|
|
&TTSOpsMinimalTuple);
|
|
slot2 = MakeSingleTupleTableSlot(rel->rd_att,
|
|
&TTSOpsMinimalTuple);
|
|
}
|
|
if (trigdesc == NULL) /* should not happen */
|
|
elog(ERROR, "relation %u has no triggers",
|
|
evtshared->ats_relid);
|
|
}
|
|
|
|
/*
|
|
* Look up source and destination partition result rels of a
|
|
* cross-partition update event.
|
|
*/
|
|
if ((event->ate_flags & AFTER_TRIGGER_TUP_BITS) ==
|
|
AFTER_TRIGGER_CP_UPDATE)
|
|
{
|
|
Assert(OidIsValid(event->ate_src_part) &&
|
|
OidIsValid(event->ate_dst_part));
|
|
src_rInfo = ExecGetTriggerResultRel(estate,
|
|
event->ate_src_part,
|
|
rInfo);
|
|
dst_rInfo = ExecGetTriggerResultRel(estate,
|
|
event->ate_dst_part,
|
|
rInfo);
|
|
}
|
|
else
|
|
src_rInfo = dst_rInfo = rInfo;
|
|
|
|
/*
|
|
* 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(estate, event, rInfo,
|
|
src_rInfo, dst_rInfo,
|
|
trigdesc, finfo, instr,
|
|
per_tuple_context, slot1, slot2);
|
|
|
|
/*
|
|
* Mark the event as done.
|
|
*/
|
|
event->ate_flags &= ~AFTER_TRIGGER_IN_PROGRESS;
|
|
event->ate_flags |= AFTER_TRIGGER_DONE;
|
|
}
|
|
else if (!(event->ate_flags & AFTER_TRIGGER_DONE))
|
|
{
|
|
/* something remains to be done */
|
|
all_fired = all_fired_in_chunk = false;
|
|
}
|
|
}
|
|
|
|
/* Clear the chunk if delete_ok and nothing left of interest */
|
|
if (delete_ok && all_fired_in_chunk)
|
|
{
|
|
chunk->freeptr = CHUNK_DATA_START(chunk);
|
|
chunk->endfree = chunk->endptr;
|
|
|
|
/*
|
|
* If it's last chunk, must sync event list's tailfree too. Note
|
|
* that delete_ok must NOT be passed as true if there could be
|
|
* additional AfterTriggerEventList values pointing at this event
|
|
* list, since we'd fail to fix their copies of tailfree.
|
|
*/
|
|
if (chunk == events->tail)
|
|
events->tailfree = chunk->freeptr;
|
|
}
|
|
}
|
|
if (slot1 != NULL)
|
|
{
|
|
ExecDropSingleTupleTableSlot(slot1);
|
|
ExecDropSingleTupleTableSlot(slot2);
|
|
}
|
|
|
|
/* Release working resources */
|
|
MemoryContextDelete(per_tuple_context);
|
|
|
|
if (local_estate)
|
|
{
|
|
ExecCloseResultRelations(estate);
|
|
ExecResetTupleTable(estate->es_tupleTable, false);
|
|
FreeExecutorState(estate);
|
|
}
|
|
|
|
return all_fired;
|
|
}
|
|
|
|
|
|
/*
|
|
* GetAfterTriggersTableData
|
|
*
|
|
* Find or create an AfterTriggersTableData struct for the specified
|
|
* trigger event (relation + operation type). Ignore existing structs
|
|
* marked "closed"; we don't want to put any additional tuples into them,
|
|
* nor change their stmt-triggers-fired state.
|
|
*
|
|
* Note: the AfterTriggersTableData list is allocated in the current
|
|
* (sub)transaction's CurTransactionContext. This is OK because
|
|
* we don't need it to live past AfterTriggerEndQuery.
|
|
*/
|
|
static AfterTriggersTableData *
|
|
GetAfterTriggersTableData(Oid relid, CmdType cmdType)
|
|
{
|
|
AfterTriggersTableData *table;
|
|
AfterTriggersQueryData *qs;
|
|
MemoryContext oldcxt;
|
|
ListCell *lc;
|
|
|
|
/* Caller should have ensured query_depth is OK. */
|
|
Assert(afterTriggers.query_depth >= 0 &&
|
|
afterTriggers.query_depth < afterTriggers.maxquerydepth);
|
|
qs = &afterTriggers.query_stack[afterTriggers.query_depth];
|
|
|
|
foreach(lc, qs->tables)
|
|
{
|
|
table = (AfterTriggersTableData *) lfirst(lc);
|
|
if (table->relid == relid && table->cmdType == cmdType &&
|
|
!table->closed)
|
|
return table;
|
|
}
|
|
|
|
oldcxt = MemoryContextSwitchTo(CurTransactionContext);
|
|
|
|
table = (AfterTriggersTableData *) palloc0(sizeof(AfterTriggersTableData));
|
|
table->relid = relid;
|
|
table->cmdType = cmdType;
|
|
qs->tables = lappend(qs->tables, table);
|
|
|
|
MemoryContextSwitchTo(oldcxt);
|
|
|
|
return table;
|
|
}
|
|
|
|
/*
|
|
* Returns a TupleTableSlot suitable for holding the tuples to be put
|
|
* into AfterTriggersTableData's transition table tuplestores.
|
|
*/
|
|
static TupleTableSlot *
|
|
GetAfterTriggersStoreSlot(AfterTriggersTableData *table,
|
|
TupleDesc tupdesc)
|
|
{
|
|
/* Create it if not already done. */
|
|
if (!table->storeslot)
|
|
{
|
|
MemoryContext oldcxt;
|
|
|
|
/*
|
|
* We only need this slot only until AfterTriggerEndQuery, but making
|
|
* it last till end-of-subxact is good enough. It'll be freed by
|
|
* AfterTriggerFreeQuery().
|
|
*/
|
|
oldcxt = MemoryContextSwitchTo(CurTransactionContext);
|
|
table->storeslot = MakeSingleTupleTableSlot(tupdesc, &TTSOpsVirtual);
|
|
MemoryContextSwitchTo(oldcxt);
|
|
}
|
|
|
|
return table->storeslot;
|
|
}
|
|
|
|
/*
|
|
* MakeTransitionCaptureState
|
|
*
|
|
* Make a TransitionCaptureState object for the given TriggerDesc, target
|
|
* relation, and operation type. The TCS object holds all the state needed
|
|
* to decide whether to capture tuples in transition tables.
|
|
*
|
|
* If there are no triggers in 'trigdesc' that request relevant transition
|
|
* tables, then return NULL.
|
|
*
|
|
* The resulting object can be passed to the ExecAR* functions. When
|
|
* dealing with child tables, the caller can set tcs_original_insert_tuple
|
|
* to avoid having to reconstruct the original tuple in the root table's
|
|
* format.
|
|
*
|
|
* Note that we copy the flags from a parent table into this struct (rather
|
|
* than subsequently using the relation's TriggerDesc directly) so that we can
|
|
* use it to control collection of transition tuples from child tables.
|
|
*
|
|
* Per SQL spec, all operations of the same kind (INSERT/UPDATE/DELETE)
|
|
* on the same table during one query should share one transition table.
|
|
* Therefore, the Tuplestores are owned by an AfterTriggersTableData struct
|
|
* looked up using the table OID + CmdType, and are merely referenced by
|
|
* the TransitionCaptureState objects we hand out to callers.
|
|
*/
|
|
TransitionCaptureState *
|
|
MakeTransitionCaptureState(TriggerDesc *trigdesc, Oid relid, CmdType cmdType)
|
|
{
|
|
TransitionCaptureState *state;
|
|
bool need_old_upd,
|
|
need_new_upd,
|
|
need_old_del,
|
|
need_new_ins;
|
|
AfterTriggersTableData *table;
|
|
MemoryContext oldcxt;
|
|
ResourceOwner saveResourceOwner;
|
|
|
|
if (trigdesc == NULL)
|
|
return NULL;
|
|
|
|
/* Detect which table(s) we need. */
|
|
switch (cmdType)
|
|
{
|
|
case CMD_INSERT:
|
|
need_old_upd = need_old_del = need_new_upd = false;
|
|
need_new_ins = trigdesc->trig_insert_new_table;
|
|
break;
|
|
case CMD_UPDATE:
|
|
need_old_upd = trigdesc->trig_update_old_table;
|
|
need_new_upd = trigdesc->trig_update_new_table;
|
|
need_old_del = need_new_ins = false;
|
|
break;
|
|
case CMD_DELETE:
|
|
need_old_del = trigdesc->trig_delete_old_table;
|
|
need_old_upd = need_new_upd = need_new_ins = false;
|
|
break;
|
|
case CMD_MERGE:
|
|
need_old_upd = trigdesc->trig_update_old_table;
|
|
need_new_upd = trigdesc->trig_update_new_table;
|
|
need_old_del = trigdesc->trig_delete_old_table;
|
|
need_new_ins = trigdesc->trig_insert_new_table;
|
|
break;
|
|
default:
|
|
elog(ERROR, "unexpected CmdType: %d", (int) cmdType);
|
|
/* keep compiler quiet */
|
|
need_old_upd = need_new_upd = need_old_del = need_new_ins = false;
|
|
break;
|
|
}
|
|
if (!need_old_upd && !need_new_upd && !need_new_ins && !need_old_del)
|
|
return NULL;
|
|
|
|
/* Check state, like AfterTriggerSaveEvent. */
|
|
if (afterTriggers.query_depth < 0)
|
|
elog(ERROR, "MakeTransitionCaptureState() called outside of query");
|
|
|
|
/* Be sure we have enough space to record events at this query depth. */
|
|
if (afterTriggers.query_depth >= afterTriggers.maxquerydepth)
|
|
AfterTriggerEnlargeQueryState();
|
|
|
|
/*
|
|
* Find or create an AfterTriggersTableData struct to hold the
|
|
* tuplestore(s). If there's a matching struct but it's marked closed,
|
|
* ignore it; we need a newer one.
|
|
*
|
|
* Note: the AfterTriggersTableData list, as well as the tuplestores, are
|
|
* allocated in the current (sub)transaction's CurTransactionContext, and
|
|
* the tuplestores are managed by the (sub)transaction's resource owner.
|
|
* This is sufficient lifespan because we do not allow triggers using
|
|
* transition tables to be deferrable; they will be fired during
|
|
* AfterTriggerEndQuery, after which it's okay to delete the data.
|
|
*/
|
|
table = GetAfterTriggersTableData(relid, cmdType);
|
|
|
|
/* Now create required tuplestore(s), if we don't have them already. */
|
|
oldcxt = MemoryContextSwitchTo(CurTransactionContext);
|
|
saveResourceOwner = CurrentResourceOwner;
|
|
CurrentResourceOwner = CurTransactionResourceOwner;
|
|
|
|
if (need_old_upd && table->old_upd_tuplestore == NULL)
|
|
table->old_upd_tuplestore = tuplestore_begin_heap(false, false, work_mem);
|
|
if (need_new_upd && table->new_upd_tuplestore == NULL)
|
|
table->new_upd_tuplestore = tuplestore_begin_heap(false, false, work_mem);
|
|
if (need_old_del && table->old_del_tuplestore == NULL)
|
|
table->old_del_tuplestore = tuplestore_begin_heap(false, false, work_mem);
|
|
if (need_new_ins && table->new_ins_tuplestore == NULL)
|
|
table->new_ins_tuplestore = tuplestore_begin_heap(false, false, work_mem);
|
|
|
|
CurrentResourceOwner = saveResourceOwner;
|
|
MemoryContextSwitchTo(oldcxt);
|
|
|
|
/* Now build the TransitionCaptureState struct, in caller's context */
|
|
state = (TransitionCaptureState *) palloc0(sizeof(TransitionCaptureState));
|
|
state->tcs_delete_old_table = trigdesc->trig_delete_old_table;
|
|
state->tcs_update_old_table = trigdesc->trig_update_old_table;
|
|
state->tcs_update_new_table = trigdesc->trig_update_new_table;
|
|
state->tcs_insert_new_table = trigdesc->trig_insert_new_table;
|
|
state->tcs_private = table;
|
|
|
|
return state;
|
|
}
|
|
|
|
|
|
/* ----------
|
|
* AfterTriggerBeginXact()
|
|
*
|
|
* Called at transaction start (either BEGIN or implicit for single
|
|
* statement outside of transaction block).
|
|
* ----------
|
|
*/
|
|
void
|
|
AfterTriggerBeginXact(void)
|
|
{
|
|
/*
|
|
* Initialize after-trigger state structure to empty
|
|
*/
|
|
afterTriggers.firing_counter = (CommandId) 1; /* mustn't be 0 */
|
|
afterTriggers.query_depth = -1;
|
|
|
|
/*
|
|
* Verify that there is no leftover state remaining. If these assertions
|
|
* trip, it means that AfterTriggerEndXact wasn't called or didn't clean
|
|
* up properly.
|
|
*/
|
|
Assert(afterTriggers.state == NULL);
|
|
Assert(afterTriggers.query_stack == NULL);
|
|
Assert(afterTriggers.maxquerydepth == 0);
|
|
Assert(afterTriggers.event_cxt == NULL);
|
|
Assert(afterTriggers.events.head == NULL);
|
|
Assert(afterTriggers.trans_stack == NULL);
|
|
Assert(afterTriggers.maxtransdepth == 0);
|
|
}
|
|
|
|
|
|
/* ----------
|
|
* AfterTriggerBeginQuery()
|
|
*
|
|
* Called just before we start processing a single query within a
|
|
* transaction (or subtransaction). Most of the real work gets deferred
|
|
* until somebody actually tries to queue a trigger event.
|
|
* ----------
|
|
*/
|
|
void
|
|
AfterTriggerBeginQuery(void)
|
|
{
|
|
/* Increase the query stack depth */
|
|
afterTriggers.query_depth++;
|
|
}
|
|
|
|
|
|
/* ----------
|
|
* 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 must be called BEFORE closing down the executor
|
|
* with ExecutorEnd, because we make use of the EState's info about
|
|
* target relations. Normally it is called from ExecutorFinish.
|
|
* ----------
|
|
*/
|
|
void
|
|
AfterTriggerEndQuery(EState *estate)
|
|
{
|
|
AfterTriggersQueryData *qs;
|
|
|
|
/* Must be inside a query, too */
|
|
Assert(afterTriggers.query_depth >= 0);
|
|
|
|
/*
|
|
* If we never even got as far as initializing the event stack, there
|
|
* certainly won't be any events, so exit quickly.
|
|
*/
|
|
if (afterTriggers.query_depth >= afterTriggers.maxquerydepth)
|
|
{
|
|
afterTriggers.query_depth--;
|
|
return;
|
|
}
|
|
|
|
/*
|
|
* 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 at the same query level.
|
|
* Ordinary trigger functions, including all PL/pgSQL trigger functions,
|
|
* will instead fire any triggers in a dedicated query level. Foreign key
|
|
* enforcement triggers do add to the current query level, thanks to their
|
|
* passing fire_triggers = false to SPI_execute_snapshot(). Other
|
|
* C-language triggers might do likewise.
|
|
*
|
|
* If we find no firable events, we don't have to increment
|
|
* firing_counter.
|
|
*/
|
|
qs = &afterTriggers.query_stack[afterTriggers.query_depth];
|
|
|
|
for (;;)
|
|
{
|
|
if (afterTriggerMarkEvents(&qs->events, &afterTriggers.events, true))
|
|
{
|
|
CommandId firing_id = afterTriggers.firing_counter++;
|
|
AfterTriggerEventChunk *oldtail = qs->events.tail;
|
|
|
|
if (afterTriggerInvokeEvents(&qs->events, firing_id, estate, false))
|
|
break; /* all fired */
|
|
|
|
/*
|
|
* Firing a trigger could result in query_stack being repalloc'd,
|
|
* so we must recalculate qs after each afterTriggerInvokeEvents
|
|
* call. Furthermore, it's unsafe to pass delete_ok = true here,
|
|
* because that could cause afterTriggerInvokeEvents to try to
|
|
* access qs->events after the stack has been repalloc'd.
|
|
*/
|
|
qs = &afterTriggers.query_stack[afterTriggers.query_depth];
|
|
|
|
/*
|
|
* We'll need to scan the events list again. To reduce the cost
|
|
* of doing so, get rid of completely-fired chunks. We know that
|
|
* all events were marked IN_PROGRESS or DONE at the conclusion of
|
|
* afterTriggerMarkEvents, so any still-interesting events must
|
|
* have been added after that, and so must be in the chunk that
|
|
* was then the tail chunk, or in later chunks. So, zap all
|
|
* chunks before oldtail. This is approximately the same set of
|
|
* events we would have gotten rid of by passing delete_ok = true.
|
|
*/
|
|
Assert(oldtail != NULL);
|
|
while (qs->events.head != oldtail)
|
|
afterTriggerDeleteHeadEventChunk(qs);
|
|
}
|
|
else
|
|
break;
|
|
}
|
|
|
|
/* Release query-level-local storage, including tuplestores if any */
|
|
AfterTriggerFreeQuery(&afterTriggers.query_stack[afterTriggers.query_depth]);
|
|
|
|
afterTriggers.query_depth--;
|
|
}
|
|
|
|
|
|
/*
|
|
* AfterTriggerFreeQuery
|
|
* Release subsidiary storage for a trigger query level.
|
|
* This includes closing down tuplestores.
|
|
* Note: it's important for this to be safe if interrupted by an error
|
|
* and then called again for the same query level.
|
|
*/
|
|
static void
|
|
AfterTriggerFreeQuery(AfterTriggersQueryData *qs)
|
|
{
|
|
Tuplestorestate *ts;
|
|
List *tables;
|
|
ListCell *lc;
|
|
|
|
/* Drop the trigger events */
|
|
afterTriggerFreeEventList(&qs->events);
|
|
|
|
/* Drop FDW tuplestore if any */
|
|
ts = qs->fdw_tuplestore;
|
|
qs->fdw_tuplestore = NULL;
|
|
if (ts)
|
|
tuplestore_end(ts);
|
|
|
|
/* Release per-table subsidiary storage */
|
|
tables = qs->tables;
|
|
foreach(lc, tables)
|
|
{
|
|
AfterTriggersTableData *table = (AfterTriggersTableData *) lfirst(lc);
|
|
|
|
ts = table->old_upd_tuplestore;
|
|
table->old_upd_tuplestore = NULL;
|
|
if (ts)
|
|
tuplestore_end(ts);
|
|
ts = table->new_upd_tuplestore;
|
|
table->new_upd_tuplestore = NULL;
|
|
if (ts)
|
|
tuplestore_end(ts);
|
|
ts = table->old_del_tuplestore;
|
|
table->old_del_tuplestore = NULL;
|
|
if (ts)
|
|
tuplestore_end(ts);
|
|
ts = table->new_ins_tuplestore;
|
|
table->new_ins_tuplestore = NULL;
|
|
if (ts)
|
|
tuplestore_end(ts);
|
|
if (table->storeslot)
|
|
ExecDropSingleTupleTableSlot(table->storeslot);
|
|
}
|
|
|
|
/*
|
|
* Now free the AfterTriggersTableData structs and list cells. Reset list
|
|
* pointer first; if list_free_deep somehow gets an error, better to leak
|
|
* that storage than have an infinite loop.
|
|
*/
|
|
qs->tables = NIL;
|
|
list_free_deep(tables);
|
|
}
|
|
|
|
|
|
/* ----------
|
|
* 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;
|
|
bool snap_pushed = false;
|
|
|
|
/* Must not be 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)
|
|
{
|
|
PushActiveSnapshot(GetTransactionSnapshot());
|
|
snap_pushed = true;
|
|
}
|
|
|
|
/*
|
|
* 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++;
|
|
|
|
if (afterTriggerInvokeEvents(events, firing_id, NULL, true))
|
|
break; /* all fired */
|
|
}
|
|
|
|
/*
|
|
* We don't bother freeing the event list, since it will go away anyway
|
|
* (and more efficiently than via pfree) in AfterTriggerEndXact.
|
|
*/
|
|
|
|
if (snap_pushed)
|
|
PopActiveSnapshot();
|
|
}
|
|
|
|
|
|
/* ----------
|
|
* 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 the pending-events list.
|
|
*
|
|
* 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!
|
|
*/
|
|
if (afterTriggers.event_cxt)
|
|
{
|
|
MemoryContextDelete(afterTriggers.event_cxt);
|
|
afterTriggers.event_cxt = NULL;
|
|
afterTriggers.events.head = NULL;
|
|
afterTriggers.events.tail = NULL;
|
|
afterTriggers.events.tailfree = NULL;
|
|
}
|
|
|
|
/*
|
|
* Forget any subtransaction state as well. Since this can't be very
|
|
* large, we let the eventual reset of TopTransactionContext free the
|
|
* memory instead of doing it here.
|
|
*/
|
|
afterTriggers.trans_stack = NULL;
|
|
afterTriggers.maxtransdepth = 0;
|
|
|
|
|
|
/*
|
|
* Forget the query stack and constraint-related state information. As
|
|
* with the subtransaction state information, we don't bother freeing the
|
|
* memory here.
|
|
*/
|
|
afterTriggers.query_stack = NULL;
|
|
afterTriggers.maxquerydepth = 0;
|
|
afterTriggers.state = NULL;
|
|
|
|
/* No more afterTriggers manipulation until next transaction starts. */
|
|
afterTriggers.query_depth = -1;
|
|
}
|
|
|
|
/*
|
|
* AfterTriggerBeginSubXact()
|
|
*
|
|
* Start a subtransaction.
|
|
*/
|
|
void
|
|
AfterTriggerBeginSubXact(void)
|
|
{
|
|
int my_level = GetCurrentTransactionNestLevel();
|
|
|
|
/*
|
|
* Allocate more space in the trans_stack if needed. (Note: because the
|
|
* minimum nest level of a subtransaction is 2, we waste the first couple
|
|
* entries of the array; not worth the notational effort to avoid it.)
|
|
*/
|
|
while (my_level >= afterTriggers.maxtransdepth)
|
|
{
|
|
if (afterTriggers.maxtransdepth == 0)
|
|
{
|
|
/* Arbitrarily initialize for max of 8 subtransaction levels */
|
|
afterTriggers.trans_stack = (AfterTriggersTransData *)
|
|
MemoryContextAlloc(TopTransactionContext,
|
|
8 * sizeof(AfterTriggersTransData));
|
|
afterTriggers.maxtransdepth = 8;
|
|
}
|
|
else
|
|
{
|
|
/* repalloc will keep the stack in the same context */
|
|
int new_alloc = afterTriggers.maxtransdepth * 2;
|
|
|
|
afterTriggers.trans_stack = (AfterTriggersTransData *)
|
|
repalloc(afterTriggers.trans_stack,
|
|
new_alloc * sizeof(AfterTriggersTransData));
|
|
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.trans_stack[my_level].state = NULL;
|
|
afterTriggers.trans_stack[my_level].events = afterTriggers.events;
|
|
afterTriggers.trans_stack[my_level].query_depth = afterTriggers.query_depth;
|
|
afterTriggers.trans_stack[my_level].firing_counter = afterTriggers.firing_counter;
|
|
}
|
|
|
|
/*
|
|
* AfterTriggerEndSubXact()
|
|
*
|
|
* The current subtransaction is ending.
|
|
*/
|
|
void
|
|
AfterTriggerEndSubXact(bool isCommit)
|
|
{
|
|
int my_level = GetCurrentTransactionNestLevel();
|
|
SetConstraintState state;
|
|
AfterTriggerEvent event;
|
|
AfterTriggerEventChunk *chunk;
|
|
CommandId subxact_firing_id;
|
|
|
|
/*
|
|
* Pop the prior state if needed.
|
|
*/
|
|
if (isCommit)
|
|
{
|
|
Assert(my_level < afterTriggers.maxtransdepth);
|
|
/* If we saved a prior state, we don't need it anymore */
|
|
state = afterTriggers.trans_stack[my_level].state;
|
|
if (state != NULL)
|
|
pfree(state);
|
|
/* this avoids double pfree if error later: */
|
|
afterTriggers.trans_stack[my_level].state = NULL;
|
|
Assert(afterTriggers.query_depth ==
|
|
afterTriggers.trans_stack[my_level].query_depth);
|
|
}
|
|
else
|
|
{
|
|
/*
|
|
* Aborting. It is possible subxact start failed before calling
|
|
* AfterTriggerBeginSubXact, in which case we mustn't risk touching
|
|
* trans_stack levels that aren't there.
|
|
*/
|
|
if (my_level >= afterTriggers.maxtransdepth)
|
|
return;
|
|
|
|
/*
|
|
* Release query-level storage for queries being aborted, and restore
|
|
* query_depth to its pre-subxact value. This assumes that a
|
|
* subtransaction will not add events to query levels started in a
|
|
* earlier transaction state.
|
|
*/
|
|
while (afterTriggers.query_depth > afterTriggers.trans_stack[my_level].query_depth)
|
|
{
|
|
if (afterTriggers.query_depth < afterTriggers.maxquerydepth)
|
|
AfterTriggerFreeQuery(&afterTriggers.query_stack[afterTriggers.query_depth]);
|
|
afterTriggers.query_depth--;
|
|
}
|
|
Assert(afterTriggers.query_depth ==
|
|
afterTriggers.trans_stack[my_level].query_depth);
|
|
|
|
/*
|
|
* Restore the global deferred-event list to its former length,
|
|
* discarding any events queued by the subxact.
|
|
*/
|
|
afterTriggerRestoreEventList(&afterTriggers.events,
|
|
&afterTriggers.trans_stack[my_level].events);
|
|
|
|
/*
|
|
* 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.trans_stack[my_level].state;
|
|
if (state != NULL)
|
|
{
|
|
pfree(afterTriggers.state);
|
|
afterTriggers.state = state;
|
|
}
|
|
/* this avoids double pfree if error later: */
|
|
afterTriggers.trans_stack[my_level].state = 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.trans_stack[my_level].firing_counter;
|
|
for_each_event_chunk(event, chunk, afterTriggers.events)
|
|
{
|
|
AfterTriggerShared evtshared = GetTriggerSharedData(event);
|
|
|
|
if (event->ate_flags &
|
|
(AFTER_TRIGGER_DONE | AFTER_TRIGGER_IN_PROGRESS))
|
|
{
|
|
if (evtshared->ats_firing_id >= subxact_firing_id)
|
|
event->ate_flags &=
|
|
~(AFTER_TRIGGER_DONE | AFTER_TRIGGER_IN_PROGRESS);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Get the transition table for the given event and depending on whether we are
|
|
* processing the old or the new tuple.
|
|
*/
|
|
static Tuplestorestate *
|
|
GetAfterTriggersTransitionTable(int event,
|
|
TupleTableSlot *oldslot,
|
|
TupleTableSlot *newslot,
|
|
TransitionCaptureState *transition_capture)
|
|
{
|
|
Tuplestorestate *tuplestore = NULL;
|
|
bool delete_old_table = transition_capture->tcs_delete_old_table;
|
|
bool update_old_table = transition_capture->tcs_update_old_table;
|
|
bool update_new_table = transition_capture->tcs_update_new_table;
|
|
bool insert_new_table = transition_capture->tcs_insert_new_table;
|
|
|
|
/*
|
|
* For INSERT events NEW should be non-NULL, for DELETE events OLD should
|
|
* be non-NULL, whereas for UPDATE events normally both OLD and NEW are
|
|
* non-NULL. But for UPDATE events fired for capturing transition tuples
|
|
* during UPDATE partition-key row movement, OLD is NULL when the event is
|
|
* for a row being inserted, whereas NEW is NULL when the event is for a
|
|
* row being deleted.
|
|
*/
|
|
Assert(!(event == TRIGGER_EVENT_DELETE && delete_old_table &&
|
|
TupIsNull(oldslot)));
|
|
Assert(!(event == TRIGGER_EVENT_INSERT && insert_new_table &&
|
|
TupIsNull(newslot)));
|
|
|
|
if (!TupIsNull(oldslot))
|
|
{
|
|
Assert(TupIsNull(newslot));
|
|
if (event == TRIGGER_EVENT_DELETE && delete_old_table)
|
|
tuplestore = transition_capture->tcs_private->old_del_tuplestore;
|
|
else if (event == TRIGGER_EVENT_UPDATE && update_old_table)
|
|
tuplestore = transition_capture->tcs_private->old_upd_tuplestore;
|
|
}
|
|
else if (!TupIsNull(newslot))
|
|
{
|
|
Assert(TupIsNull(oldslot));
|
|
if (event == TRIGGER_EVENT_INSERT && insert_new_table)
|
|
tuplestore = transition_capture->tcs_private->new_ins_tuplestore;
|
|
else if (event == TRIGGER_EVENT_UPDATE && update_new_table)
|
|
tuplestore = transition_capture->tcs_private->new_upd_tuplestore;
|
|
}
|
|
|
|
return tuplestore;
|
|
}
|
|
|
|
/*
|
|
* Add the given heap tuple to the given tuplestore, applying the conversion
|
|
* map if necessary.
|
|
*
|
|
* If original_insert_tuple is given, we can add that tuple without conversion.
|
|
*/
|
|
static void
|
|
TransitionTableAddTuple(EState *estate,
|
|
TransitionCaptureState *transition_capture,
|
|
ResultRelInfo *relinfo,
|
|
TupleTableSlot *slot,
|
|
TupleTableSlot *original_insert_tuple,
|
|
Tuplestorestate *tuplestore)
|
|
{
|
|
TupleConversionMap *map;
|
|
|
|
/*
|
|
* Nothing needs to be done if we don't have a tuplestore.
|
|
*/
|
|
if (tuplestore == NULL)
|
|
return;
|
|
|
|
if (original_insert_tuple)
|
|
tuplestore_puttupleslot(tuplestore, original_insert_tuple);
|
|
else if ((map = ExecGetChildToRootMap(relinfo)) != NULL)
|
|
{
|
|
AfterTriggersTableData *table = transition_capture->tcs_private;
|
|
TupleTableSlot *storeslot;
|
|
|
|
storeslot = GetAfterTriggersStoreSlot(table, map->outdesc);
|
|
execute_attr_map_slot(map->attrMap, slot, storeslot);
|
|
tuplestore_puttupleslot(tuplestore, storeslot);
|
|
}
|
|
else
|
|
tuplestore_puttupleslot(tuplestore, slot);
|
|
}
|
|
|
|
/* ----------
|
|
* AfterTriggerEnlargeQueryState()
|
|
*
|
|
* Prepare the necessary state so that we can record AFTER trigger events
|
|
* queued by a query. It is allowed to have nested queries within a
|
|
* (sub)transaction, so we need to have separate state for each query
|
|
* nesting level.
|
|
* ----------
|
|
*/
|
|
static void
|
|
AfterTriggerEnlargeQueryState(void)
|
|
{
|
|
int init_depth = afterTriggers.maxquerydepth;
|
|
|
|
Assert(afterTriggers.query_depth >= afterTriggers.maxquerydepth);
|
|
|
|
if (afterTriggers.maxquerydepth == 0)
|
|
{
|
|
int new_alloc = Max(afterTriggers.query_depth + 1, 8);
|
|
|
|
afterTriggers.query_stack = (AfterTriggersQueryData *)
|
|
MemoryContextAlloc(TopTransactionContext,
|
|
new_alloc * sizeof(AfterTriggersQueryData));
|
|
afterTriggers.maxquerydepth = new_alloc;
|
|
}
|
|
else
|
|
{
|
|
/* repalloc will keep the stack in the same context */
|
|
int old_alloc = afterTriggers.maxquerydepth;
|
|
int new_alloc = Max(afterTriggers.query_depth + 1,
|
|
old_alloc * 2);
|
|
|
|
afterTriggers.query_stack = (AfterTriggersQueryData *)
|
|
repalloc(afterTriggers.query_stack,
|
|
new_alloc * sizeof(AfterTriggersQueryData));
|
|
afterTriggers.maxquerydepth = new_alloc;
|
|
}
|
|
|
|
/* Initialize new array entries to empty */
|
|
while (init_depth < afterTriggers.maxquerydepth)
|
|
{
|
|
AfterTriggersQueryData *qs = &afterTriggers.query_stack[init_depth];
|
|
|
|
qs->events.head = NULL;
|
|
qs->events.tail = NULL;
|
|
qs->events.tailfree = NULL;
|
|
qs->fdw_tuplestore = NULL;
|
|
qs->tables = NIL;
|
|
|
|
++init_depth;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* 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,
|
|
offsetof(SetConstraintStateData, trigstates) +
|
|
numalloc * 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,
|
|
offsetof(SetConstraintStateData, trigstates) +
|
|
newalloc * 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();
|
|
|
|
/* If we haven't already done so, initialize our state. */
|
|
if (afterTriggers.state == NULL)
|
|
afterTriggers.state = SetConstraintStateCreate(8);
|
|
|
|
/*
|
|
* 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.trans_stack[my_level].state == NULL)
|
|
{
|
|
afterTriggers.trans_stack[my_level].state =
|
|
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 conrel;
|
|
Relation tgrel;
|
|
List *conoidlist = NIL;
|
|
List *tgoidlist = NIL;
|
|
ListCell *lc;
|
|
|
|
/*
|
|
* Handle SET CONSTRAINTS constraint-name [, ...]
|
|
*
|
|
* First, identify all the named constraints and make a list of their
|
|
* OIDs. Since, unlike the SQL spec, we allow multiple constraints of
|
|
* the same name within a schema, the specifications are not
|
|
* necessarily unique. Our strategy is to target all matching
|
|
* constraints within the first search-path schema that has any
|
|
* matches, but disregard matches in schemas beyond the first match.
|
|
* (This is a bit odd but it's the historical behavior.)
|
|
*
|
|
* A constraint in a partitioned table may have corresponding
|
|
* constraints in the partitions. Grab those too.
|
|
*/
|
|
conrel = table_open(ConstraintRelationId, AccessShareLock);
|
|
|
|
foreach(lc, stmt->constraints)
|
|
{
|
|
RangeVar *constraint = lfirst(lc);
|
|
bool found;
|
|
List *namespacelist;
|
|
ListCell *nslc;
|
|
|
|
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,
|
|
false);
|
|
|
|
namespacelist = list_make1_oid(namespaceId);
|
|
}
|
|
else
|
|
{
|
|
namespacelist = fetch_search_path(true);
|
|
}
|
|
|
|
found = false;
|
|
foreach(nslc, namespacelist)
|
|
{
|
|
Oid namespaceId = lfirst_oid(nslc);
|
|
SysScanDesc conscan;
|
|
ScanKeyData skey[2];
|
|
HeapTuple tup;
|
|
|
|
ScanKeyInit(&skey[0],
|
|
Anum_pg_constraint_conname,
|
|
BTEqualStrategyNumber, F_NAMEEQ,
|
|
CStringGetDatum(constraint->relname));
|
|
ScanKeyInit(&skey[1],
|
|
Anum_pg_constraint_connamespace,
|
|
BTEqualStrategyNumber, F_OIDEQ,
|
|
ObjectIdGetDatum(namespaceId));
|
|
|
|
conscan = systable_beginscan(conrel, ConstraintNameNspIndexId,
|
|
true, NULL, 2, skey);
|
|
|
|
while (HeapTupleIsValid(tup = systable_getnext(conscan)))
|
|
{
|
|
Form_pg_constraint con = (Form_pg_constraint) GETSTRUCT(tup);
|
|
|
|
if (con->condeferrable)
|
|
conoidlist = lappend_oid(conoidlist, con->oid);
|
|
else if (stmt->deferred)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
|
|
errmsg("constraint \"%s\" is not deferrable",
|
|
constraint->relname)));
|
|
found = true;
|
|
}
|
|
|
|
systable_endscan(conscan);
|
|
|
|
/*
|
|
* Once we've found a matching constraint we do not search
|
|
* later parts of the search path.
|
|
*/
|
|
if (found)
|
|
break;
|
|
}
|
|
|
|
list_free(namespacelist);
|
|
|
|
/*
|
|
* Not found ?
|
|
*/
|
|
if (!found)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_UNDEFINED_OBJECT),
|
|
errmsg("constraint \"%s\" does not exist",
|
|
constraint->relname)));
|
|
}
|
|
|
|
/*
|
|
* Scan for any possible descendants of the constraints. We append
|
|
* whatever we find to the same list that we're scanning; this has the
|
|
* effect that we create new scans for those, too, so if there are
|
|
* further descendents, we'll also catch them.
|
|
*/
|
|
foreach(lc, conoidlist)
|
|
{
|
|
Oid parent = lfirst_oid(lc);
|
|
ScanKeyData key;
|
|
SysScanDesc scan;
|
|
HeapTuple tuple;
|
|
|
|
ScanKeyInit(&key,
|
|
Anum_pg_constraint_conparentid,
|
|
BTEqualStrategyNumber, F_OIDEQ,
|
|
ObjectIdGetDatum(parent));
|
|
|
|
scan = systable_beginscan(conrel, ConstraintParentIndexId, true, NULL, 1, &key);
|
|
|
|
while (HeapTupleIsValid(tuple = systable_getnext(scan)))
|
|
{
|
|
Form_pg_constraint con = (Form_pg_constraint) GETSTRUCT(tuple);
|
|
|
|
conoidlist = lappend_oid(conoidlist, con->oid);
|
|
}
|
|
|
|
systable_endscan(scan);
|
|
}
|
|
|
|
table_close(conrel, AccessShareLock);
|
|
|
|
/*
|
|
* Now, locate the trigger(s) implementing each of these constraints,
|
|
* and make a list of their OIDs.
|
|
*/
|
|
tgrel = table_open(TriggerRelationId, AccessShareLock);
|
|
|
|
foreach(lc, conoidlist)
|
|
{
|
|
Oid conoid = lfirst_oid(lc);
|
|
ScanKeyData skey;
|
|
SysScanDesc tgscan;
|
|
HeapTuple htup;
|
|
|
|
ScanKeyInit(&skey,
|
|
Anum_pg_trigger_tgconstraint,
|
|
BTEqualStrategyNumber, F_OIDEQ,
|
|
ObjectIdGetDatum(conoid));
|
|
|
|
tgscan = systable_beginscan(tgrel, TriggerConstraintIndexId, true,
|
|
NULL, 1, &skey);
|
|
|
|
while (HeapTupleIsValid(htup = systable_getnext(tgscan)))
|
|
{
|
|
Form_pg_trigger pg_trigger = (Form_pg_trigger) GETSTRUCT(htup);
|
|
|
|
/*
|
|
* Silently skip triggers that are marked as non-deferrable in
|
|
* pg_trigger. This is not an error condition, since a
|
|
* deferrable RI constraint may have some non-deferrable
|
|
* actions.
|
|
*/
|
|
if (pg_trigger->tgdeferrable)
|
|
tgoidlist = lappend_oid(tgoidlist, pg_trigger->oid);
|
|
}
|
|
|
|
systable_endscan(tgscan);
|
|
}
|
|
|
|
table_close(tgrel, AccessShareLock);
|
|
|
|
/*
|
|
* Now we can set the trigger states of individual triggers for this
|
|
* xact.
|
|
*/
|
|
foreach(lc, tgoidlist)
|
|
{
|
|
Oid tgoid = lfirst_oid(lc);
|
|
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;
|
|
bool snapshot_set = false;
|
|
|
|
while (afterTriggerMarkEvents(events, NULL, true))
|
|
{
|
|
CommandId firing_id = afterTriggers.firing_counter++;
|
|
|
|
/*
|
|
* Make sure a snapshot has been established in case trigger
|
|
* functions need one. Note that we avoid setting a snapshot if
|
|
* we don't find at least one trigger that has to be fired now.
|
|
* This is so that BEGIN; SET CONSTRAINTS ...; SET TRANSACTION
|
|
* ISOLATION LEVEL SERIALIZABLE; ... works properly. (If we are
|
|
* at the start of a transaction it's not possible for any trigger
|
|
* events to be queued yet.)
|
|
*/
|
|
if (!snapshot_set)
|
|
{
|
|
PushActiveSnapshot(GetTransactionSnapshot());
|
|
snapshot_set = true;
|
|
}
|
|
|
|
/*
|
|
* 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.
|
|
*/
|
|
if (afterTriggerInvokeEvents(events, firing_id, NULL,
|
|
!IsSubTransaction()))
|
|
break; /* all fired */
|
|
}
|
|
|
|
if (snapshot_set)
|
|
PopActiveSnapshot();
|
|
}
|
|
}
|
|
|
|
/* ----------
|
|
* 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;
|
|
AfterTriggerEventChunk *chunk;
|
|
int depth;
|
|
|
|
/* Scan queued events */
|
|
for_each_event_chunk(event, chunk, afterTriggers.events)
|
|
{
|
|
AfterTriggerShared evtshared = GetTriggerSharedData(event);
|
|
|
|
/*
|
|
* 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_flags & AFTER_TRIGGER_DONE)
|
|
continue;
|
|
|
|
if (evtshared->ats_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 < afterTriggers.maxquerydepth; depth++)
|
|
{
|
|
for_each_event_chunk(event, chunk, afterTriggers.query_stack[depth].events)
|
|
{
|
|
AfterTriggerShared evtshared = GetTriggerSharedData(event);
|
|
|
|
if (event->ate_flags & AFTER_TRIGGER_DONE)
|
|
continue;
|
|
|
|
if (evtshared->ats_relid == relid)
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/* ----------
|
|
* AfterTriggerSaveEvent()
|
|
*
|
|
* Called by ExecA[RS]...Triggers() to queue up the triggers that should
|
|
* be fired for an event.
|
|
*
|
|
* NOTE: this is called whenever there are any triggers associated with
|
|
* the event (even if they are disabled). This function decides which
|
|
* triggers actually need to be queued. It is also called after each row,
|
|
* even if there are no triggers for that event, if there are any AFTER
|
|
* STATEMENT triggers for the statement which use transition tables, so that
|
|
* the transition tuplestores can be built. Furthermore, if the transition
|
|
* capture is happening for UPDATEd rows being moved to another partition due
|
|
* to the partition-key being changed, then this function is called once when
|
|
* the row is deleted (to capture OLD row), and once when the row is inserted
|
|
* into another partition (to capture NEW row). This is done separately because
|
|
* DELETE and INSERT happen on different tables.
|
|
*
|
|
* Transition tuplestores are built now, rather than when events are pulled
|
|
* off of the queue because AFTER ROW triggers are allowed to select from the
|
|
* transition tables for the statement.
|
|
*
|
|
* This contains special support to queue the update events for the case where
|
|
* a partitioned table undergoing a cross-partition update may have foreign
|
|
* keys pointing into it. Normally, a partitioned table's row triggers are
|
|
* not fired because the leaf partition(s) which are modified as a result of
|
|
* the operation on the partitioned table contain the same triggers which are
|
|
* fired instead. But that general scheme can cause problematic behavior with
|
|
* foreign key triggers during cross-partition updates, which are implemented
|
|
* as DELETE on the source partition followed by INSERT into the destination
|
|
* partition. Specifically, firing DELETE triggers would lead to the wrong
|
|
* foreign key action to be enforced considering that the original command is
|
|
* UPDATE; in this case, this function is called with relinfo as the
|
|
* partitioned table, and src_partinfo and dst_partinfo referring to the
|
|
* source and target leaf partitions, respectively.
|
|
*
|
|
* is_crosspart_update is true either when a DELETE event is fired on the
|
|
* source partition (which is to be ignored) or an UPDATE event is fired on
|
|
* the root partitioned table.
|
|
* ----------
|
|
*/
|
|
static void
|
|
AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
|
|
ResultRelInfo *src_partinfo,
|
|
ResultRelInfo *dst_partinfo,
|
|
int event, bool row_trigger,
|
|
TupleTableSlot *oldslot, TupleTableSlot *newslot,
|
|
List *recheckIndexes, Bitmapset *modifiedCols,
|
|
TransitionCaptureState *transition_capture,
|
|
bool is_crosspart_update)
|
|
{
|
|
Relation rel = relinfo->ri_RelationDesc;
|
|
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
|
|
AfterTriggerEventData new_event;
|
|
AfterTriggerSharedData new_shared;
|
|
char relkind = rel->rd_rel->relkind;
|
|
int tgtype_event;
|
|
int tgtype_level;
|
|
int i;
|
|
Tuplestorestate *fdw_tuplestore = NULL;
|
|
|
|
/*
|
|
* Check state. We use a normal test not Assert because it is possible to
|
|
* reach here in the wrong state given misconfigured RI triggers, in
|
|
* particular deferring a cascade action trigger.
|
|
*/
|
|
if (afterTriggers.query_depth < 0)
|
|
elog(ERROR, "AfterTriggerSaveEvent() called outside of query");
|
|
|
|
/* Be sure we have enough space to record events at this query depth. */
|
|
if (afterTriggers.query_depth >= afterTriggers.maxquerydepth)
|
|
AfterTriggerEnlargeQueryState();
|
|
|
|
/*
|
|
* If the directly named relation has any triggers with transition tables,
|
|
* then we need to capture transition tuples.
|
|
*/
|
|
if (row_trigger && transition_capture != NULL)
|
|
{
|
|
TupleTableSlot *original_insert_tuple = transition_capture->tcs_original_insert_tuple;
|
|
|
|
/*
|
|
* Capture the old tuple in the appropriate transition table based on
|
|
* the event.
|
|
*/
|
|
if (!TupIsNull(oldslot))
|
|
{
|
|
Tuplestorestate *old_tuplestore;
|
|
|
|
old_tuplestore = GetAfterTriggersTransitionTable(event,
|
|
oldslot,
|
|
NULL,
|
|
transition_capture);
|
|
TransitionTableAddTuple(estate, transition_capture, relinfo,
|
|
oldslot, NULL, old_tuplestore);
|
|
}
|
|
|
|
/*
|
|
* Capture the new tuple in the appropriate transition table based on
|
|
* the event.
|
|
*/
|
|
if (!TupIsNull(newslot))
|
|
{
|
|
Tuplestorestate *new_tuplestore;
|
|
|
|
new_tuplestore = GetAfterTriggersTransitionTable(event,
|
|
NULL,
|
|
newslot,
|
|
transition_capture);
|
|
TransitionTableAddTuple(estate, transition_capture, relinfo,
|
|
newslot, original_insert_tuple, new_tuplestore);
|
|
}
|
|
|
|
/*
|
|
* If transition tables are the only reason we're here, return. As
|
|
* mentioned above, we can also be here during update tuple routing in
|
|
* presence of transition tables, in which case this function is
|
|
* called separately for OLD and NEW, so we expect exactly one of them
|
|
* to be NULL.
|
|
*/
|
|
if (trigdesc == NULL ||
|
|
(event == TRIGGER_EVENT_DELETE && !trigdesc->trig_delete_after_row) ||
|
|
(event == TRIGGER_EVENT_INSERT && !trigdesc->trig_insert_after_row) ||
|
|
(event == TRIGGER_EVENT_UPDATE && !trigdesc->trig_update_after_row) ||
|
|
(event == TRIGGER_EVENT_UPDATE && (TupIsNull(oldslot) ^ TupIsNull(newslot))))
|
|
return;
|
|
}
|
|
|
|
/*
|
|
* We normally don't see partitioned tables here for row level triggers
|
|
* except in the special case of a cross-partition update. In that case,
|
|
* nodeModifyTable.c:ExecCrossPartitionUpdateForeignKey() calls here to
|
|
* queue an update event on the root target partitioned table, also
|
|
* passing the source and destination partitions and their tuples.
|
|
*/
|
|
Assert(!row_trigger ||
|
|
rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE ||
|
|
(is_crosspart_update &&
|
|
TRIGGER_FIRED_BY_UPDATE(event) &&
|
|
src_partinfo != NULL && dst_partinfo != NULL));
|
|
|
|
/*
|
|
* Validate the event code and collect the associated tuple CTIDs.
|
|
*
|
|
* The event code will be used both as a bitmask and an array offset, so
|
|
* validation is important to make sure we don't walk off the edge of our
|
|
* arrays.
|
|
*
|
|
* Also, if we're considering statement-level triggers, check whether we
|
|
* already queued a set of them for this event, and cancel the prior set
|
|
* if so. This preserves the behavior that statement-level triggers fire
|
|
* just once per statement and fire after row-level triggers.
|
|
*/
|
|
switch (event)
|
|
{
|
|
case TRIGGER_EVENT_INSERT:
|
|
tgtype_event = TRIGGER_TYPE_INSERT;
|
|
if (row_trigger)
|
|
{
|
|
Assert(oldslot == NULL);
|
|
Assert(newslot != NULL);
|
|
ItemPointerCopy(&(newslot->tts_tid), &(new_event.ate_ctid1));
|
|
ItemPointerSetInvalid(&(new_event.ate_ctid2));
|
|
}
|
|
else
|
|
{
|
|
Assert(oldslot == NULL);
|
|
Assert(newslot == NULL);
|
|
ItemPointerSetInvalid(&(new_event.ate_ctid1));
|
|
ItemPointerSetInvalid(&(new_event.ate_ctid2));
|
|
cancel_prior_stmt_triggers(RelationGetRelid(rel),
|
|
CMD_INSERT, event);
|
|
}
|
|
break;
|
|
case TRIGGER_EVENT_DELETE:
|
|
tgtype_event = TRIGGER_TYPE_DELETE;
|
|
if (row_trigger)
|
|
{
|
|
Assert(oldslot != NULL);
|
|
Assert(newslot == NULL);
|
|
ItemPointerCopy(&(oldslot->tts_tid), &(new_event.ate_ctid1));
|
|
ItemPointerSetInvalid(&(new_event.ate_ctid2));
|
|
}
|
|
else
|
|
{
|
|
Assert(oldslot == NULL);
|
|
Assert(newslot == NULL);
|
|
ItemPointerSetInvalid(&(new_event.ate_ctid1));
|
|
ItemPointerSetInvalid(&(new_event.ate_ctid2));
|
|
cancel_prior_stmt_triggers(RelationGetRelid(rel),
|
|
CMD_DELETE, event);
|
|
}
|
|
break;
|
|
case TRIGGER_EVENT_UPDATE:
|
|
tgtype_event = TRIGGER_TYPE_UPDATE;
|
|
if (row_trigger)
|
|
{
|
|
Assert(oldslot != NULL);
|
|
Assert(newslot != NULL);
|
|
ItemPointerCopy(&(oldslot->tts_tid), &(new_event.ate_ctid1));
|
|
ItemPointerCopy(&(newslot->tts_tid), &(new_event.ate_ctid2));
|
|
|
|
/*
|
|
* Also remember the OIDs of partitions to fetch these tuples
|
|
* out of later in AfterTriggerExecute().
|
|
*/
|
|
if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
|
|
{
|
|
Assert(src_partinfo != NULL && dst_partinfo != NULL);
|
|
new_event.ate_src_part =
|
|
RelationGetRelid(src_partinfo->ri_RelationDesc);
|
|
new_event.ate_dst_part =
|
|
RelationGetRelid(dst_partinfo->ri_RelationDesc);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Assert(oldslot == NULL);
|
|
Assert(newslot == NULL);
|
|
ItemPointerSetInvalid(&(new_event.ate_ctid1));
|
|
ItemPointerSetInvalid(&(new_event.ate_ctid2));
|
|
cancel_prior_stmt_triggers(RelationGetRelid(rel),
|
|
CMD_UPDATE, event);
|
|
}
|
|
break;
|
|
case TRIGGER_EVENT_TRUNCATE:
|
|
tgtype_event = TRIGGER_TYPE_TRUNCATE;
|
|
Assert(oldslot == NULL);
|
|
Assert(newslot == NULL);
|
|
ItemPointerSetInvalid(&(new_event.ate_ctid1));
|
|
ItemPointerSetInvalid(&(new_event.ate_ctid2));
|
|
break;
|
|
default:
|
|
elog(ERROR, "invalid after-trigger event code: %d", event);
|
|
tgtype_event = 0; /* keep compiler quiet */
|
|
break;
|
|
}
|
|
|
|
/* Determine flags */
|
|
if (!(relkind == RELKIND_FOREIGN_TABLE && row_trigger))
|
|
{
|
|
if (row_trigger && event == TRIGGER_EVENT_UPDATE)
|
|
{
|
|
if (relkind == RELKIND_PARTITIONED_TABLE)
|
|
new_event.ate_flags = AFTER_TRIGGER_CP_UPDATE;
|
|
else
|
|
new_event.ate_flags = AFTER_TRIGGER_2CTID;
|
|
}
|
|
else
|
|
new_event.ate_flags = AFTER_TRIGGER_1CTID;
|
|
}
|
|
|
|
/* else, we'll initialize ate_flags for each trigger */
|
|
|
|
tgtype_level = (row_trigger ? TRIGGER_TYPE_ROW : TRIGGER_TYPE_STATEMENT);
|
|
|
|
/*
|
|
* Must convert/copy the source and destination partition tuples into the
|
|
* root partitioned table's format/slot, because the processing in the
|
|
* loop below expects both oldslot and newslot tuples to be in that form.
|
|
*/
|
|
if (row_trigger && rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
|
|
{
|
|
TupleTableSlot *rootslot;
|
|
TupleConversionMap *map;
|
|
|
|
rootslot = ExecGetTriggerOldSlot(estate, relinfo);
|
|
map = ExecGetChildToRootMap(src_partinfo);
|
|
if (map)
|
|
oldslot = execute_attr_map_slot(map->attrMap,
|
|
oldslot,
|
|
rootslot);
|
|
else
|
|
oldslot = ExecCopySlot(rootslot, oldslot);
|
|
|
|
rootslot = ExecGetTriggerNewSlot(estate, relinfo);
|
|
map = ExecGetChildToRootMap(dst_partinfo);
|
|
if (map)
|
|
newslot = execute_attr_map_slot(map->attrMap,
|
|
newslot,
|
|
rootslot);
|
|
else
|
|
newslot = ExecCopySlot(rootslot, newslot);
|
|
}
|
|
|
|
for (i = 0; i < trigdesc->numtriggers; i++)
|
|
{
|
|
Trigger *trigger = &trigdesc->triggers[i];
|
|
|
|
if (!TRIGGER_TYPE_MATCHES(trigger->tgtype,
|
|
tgtype_level,
|
|
TRIGGER_TYPE_AFTER,
|
|
tgtype_event))
|
|
continue;
|
|
if (!TriggerEnabled(estate, relinfo, trigger, event,
|
|
modifiedCols, oldslot, newslot))
|
|
continue;
|
|
|
|
if (relkind == RELKIND_FOREIGN_TABLE && row_trigger)
|
|
{
|
|
if (fdw_tuplestore == NULL)
|
|
{
|
|
fdw_tuplestore = GetCurrentFDWTuplestore();
|
|
new_event.ate_flags = AFTER_TRIGGER_FDW_FETCH;
|
|
}
|
|
else
|
|
/* subsequent event for the same tuple */
|
|
new_event.ate_flags = AFTER_TRIGGER_FDW_REUSE;
|
|
}
|
|
|
|
/*
|
|
* If the trigger is a foreign key enforcement trigger, there are
|
|
* certain cases where we can skip queueing the event because we can
|
|
* tell by inspection that the FK constraint will still pass. There
|
|
* are also some cases during cross-partition updates of a partitioned
|
|
* table where queuing the event can be skipped.
|
|
*/
|
|
if (TRIGGER_FIRED_BY_UPDATE(event) || TRIGGER_FIRED_BY_DELETE(event))
|
|
{
|
|
switch (RI_FKey_trigger_type(trigger->tgfoid))
|
|
{
|
|
case RI_TRIGGER_PK:
|
|
|
|
/*
|
|
* For cross-partitioned updates of partitioned PK table,
|
|
* skip the event fired by the component delete on the
|
|
* source leaf partition unless the constraint originates
|
|
* in the partition itself (!tgisclone), because the
|
|
* update event that will be fired on the root
|
|
* (partitioned) target table will be used to perform the
|
|
* necessary foreign key enforcement action.
|
|
*/
|
|
if (is_crosspart_update &&
|
|
TRIGGER_FIRED_BY_DELETE(event) &&
|
|
trigger->tgisclone)
|
|
continue;
|
|
|
|
/* Update or delete on trigger's PK table */
|
|
if (!RI_FKey_pk_upd_check_required(trigger, rel,
|
|
oldslot, newslot))
|
|
{
|
|
/* skip queuing this event */
|
|
continue;
|
|
}
|
|
break;
|
|
|
|
case RI_TRIGGER_FK:
|
|
|
|
/*
|
|
* Update on trigger's FK table. We can skip the update
|
|
* event fired on a partitioned table during a
|
|
* cross-partition of that table, because the insert event
|
|
* that is fired on the destination leaf partition would
|
|
* suffice to perform the necessary foreign key check.
|
|
* Moreover, RI_FKey_fk_upd_check_required() expects to be
|
|
* passed a tuple that contains system attributes, most of
|
|
* which are not present in the virtual slot belonging to
|
|
* a partitioned table.
|
|
*/
|
|
if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
|
|
!RI_FKey_fk_upd_check_required(trigger, rel,
|
|
oldslot, newslot))
|
|
{
|
|
/* skip queuing this event */
|
|
continue;
|
|
}
|
|
break;
|
|
|
|
case RI_TRIGGER_NONE:
|
|
|
|
/*
|
|
* Not an FK trigger. No need to queue the update event
|
|
* fired during a cross-partitioned update of a
|
|
* partitioned table, because the same row trigger must be
|
|
* present in the leaf partition(s) that are affected as
|
|
* part of this update and the events fired on them are
|
|
* queued instead.
|
|
*/
|
|
if (row_trigger &&
|
|
rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
|
|
continue;
|
|
break;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* If the trigger is a deferred unique constraint check trigger, only
|
|
* queue it if the unique constraint was potentially violated, which
|
|
* we know from index insertion time.
|
|
*/
|
|
if (trigger->tgfoid == F_UNIQUE_KEY_RECHECK)
|
|
{
|
|
if (!list_member_oid(recheckIndexes, trigger->tgconstrindid))
|
|
continue; /* Uniqueness definitely not violated */
|
|
}
|
|
|
|
/*
|
|
* Fill in event structure and add it to the current query's queue.
|
|
* Note we set ats_table to NULL whenever this trigger doesn't use
|
|
* transition tables, to improve sharability of the shared event data.
|
|
*/
|
|
new_shared.ats_event =
|
|
(event & TRIGGER_EVENT_OPMASK) |
|
|
(row_trigger ? TRIGGER_EVENT_ROW : 0) |
|
|
(trigger->tgdeferrable ? AFTER_TRIGGER_DEFERRABLE : 0) |
|
|
(trigger->tginitdeferred ? AFTER_TRIGGER_INITDEFERRED : 0);
|
|
new_shared.ats_tgoid = trigger->tgoid;
|
|
new_shared.ats_relid = RelationGetRelid(rel);
|
|
new_shared.ats_firing_id = 0;
|
|
if ((trigger->tgoldtable || trigger->tgnewtable) &&
|
|
transition_capture != NULL)
|
|
new_shared.ats_table = transition_capture->tcs_private;
|
|
else
|
|
new_shared.ats_table = NULL;
|
|
new_shared.ats_modifiedcols = modifiedCols;
|
|
|
|
afterTriggerAddEvent(&afterTriggers.query_stack[afterTriggers.query_depth].events,
|
|
&new_event, &new_shared);
|
|
}
|
|
|
|
/*
|
|
* Finally, spool any foreign tuple(s). The tuplestore squashes them to
|
|
* minimal tuples, so this loses any system columns. The executor lost
|
|
* those columns before us, for an unrelated reason, so this is fine.
|
|
*/
|
|
if (fdw_tuplestore)
|
|
{
|
|
if (oldslot != NULL)
|
|
tuplestore_puttupleslot(fdw_tuplestore, oldslot);
|
|
if (newslot != NULL)
|
|
tuplestore_puttupleslot(fdw_tuplestore, newslot);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Detect whether we already queued BEFORE STATEMENT triggers for the given
|
|
* relation + operation, and set the flag so the next call will report "true".
|
|
*/
|
|
static bool
|
|
before_stmt_triggers_fired(Oid relid, CmdType cmdType)
|
|
{
|
|
bool result;
|
|
AfterTriggersTableData *table;
|
|
|
|
/* Check state, like AfterTriggerSaveEvent. */
|
|
if (afterTriggers.query_depth < 0)
|
|
elog(ERROR, "before_stmt_triggers_fired() called outside of query");
|
|
|
|
/* Be sure we have enough space to record events at this query depth. */
|
|
if (afterTriggers.query_depth >= afterTriggers.maxquerydepth)
|
|
AfterTriggerEnlargeQueryState();
|
|
|
|
/*
|
|
* We keep this state in the AfterTriggersTableData that also holds
|
|
* transition tables for the relation + operation. In this way, if we are
|
|
* forced to make a new set of transition tables because more tuples get
|
|
* entered after we've already fired triggers, we will allow a new set of
|
|
* statement triggers to get queued.
|
|
*/
|
|
table = GetAfterTriggersTableData(relid, cmdType);
|
|
result = table->before_trig_done;
|
|
table->before_trig_done = true;
|
|
return result;
|
|
}
|
|
|
|
/*
|
|
* If we previously queued a set of AFTER STATEMENT triggers for the given
|
|
* relation + operation, and they've not been fired yet, cancel them. The
|
|
* caller will queue a fresh set that's after any row-level triggers that may
|
|
* have been queued by the current sub-statement, preserving (as much as
|
|
* possible) the property that AFTER ROW triggers fire before AFTER STATEMENT
|
|
* triggers, and that the latter only fire once. This deals with the
|
|
* situation where several FK enforcement triggers sequentially queue triggers
|
|
* for the same table into the same trigger query level. We can't fully
|
|
* prevent odd behavior though: if there are AFTER ROW triggers taking
|
|
* transition tables, we don't want to change the transition tables once the
|
|
* first such trigger has seen them. In such a case, any additional events
|
|
* will result in creating new transition tables and allowing new firings of
|
|
* statement triggers.
|
|
*
|
|
* This also saves the current event list location so that a later invocation
|
|
* of this function can cheaply find the triggers we're about to queue and
|
|
* cancel them.
|
|
*/
|
|
static void
|
|
cancel_prior_stmt_triggers(Oid relid, CmdType cmdType, int tgevent)
|
|
{
|
|
AfterTriggersTableData *table;
|
|
AfterTriggersQueryData *qs = &afterTriggers.query_stack[afterTriggers.query_depth];
|
|
|
|
/*
|
|
* We keep this state in the AfterTriggersTableData that also holds
|
|
* transition tables for the relation + operation. In this way, if we are
|
|
* forced to make a new set of transition tables because more tuples get
|
|
* entered after we've already fired triggers, we will allow a new set of
|
|
* statement triggers to get queued without canceling the old ones.
|
|
*/
|
|
table = GetAfterTriggersTableData(relid, cmdType);
|
|
|
|
if (table->after_trig_done)
|
|
{
|
|
/*
|
|
* We want to start scanning from the tail location that existed just
|
|
* before we inserted any statement triggers. But the events list
|
|
* might've been entirely empty then, in which case scan from the
|
|
* current head.
|
|
*/
|
|
AfterTriggerEvent event;
|
|
AfterTriggerEventChunk *chunk;
|
|
|
|
if (table->after_trig_events.tail)
|
|
{
|
|
chunk = table->after_trig_events.tail;
|
|
event = (AfterTriggerEvent) table->after_trig_events.tailfree;
|
|
}
|
|
else
|
|
{
|
|
chunk = qs->events.head;
|
|
event = NULL;
|
|
}
|
|
|
|
for_each_chunk_from(chunk)
|
|
{
|
|
if (event == NULL)
|
|
event = (AfterTriggerEvent) CHUNK_DATA_START(chunk);
|
|
for_each_event_from(event, chunk)
|
|
{
|
|
AfterTriggerShared evtshared = GetTriggerSharedData(event);
|
|
|
|
/*
|
|
* Exit loop when we reach events that aren't AS triggers for
|
|
* the target relation.
|
|
*/
|
|
if (evtshared->ats_relid != relid)
|
|
goto done;
|
|
if ((evtshared->ats_event & TRIGGER_EVENT_OPMASK) != tgevent)
|
|
goto done;
|
|
if (!TRIGGER_FIRED_FOR_STATEMENT(evtshared->ats_event))
|
|
goto done;
|
|
if (!TRIGGER_FIRED_AFTER(evtshared->ats_event))
|
|
goto done;
|
|
/* OK, mark it DONE */
|
|
event->ate_flags &= ~AFTER_TRIGGER_IN_PROGRESS;
|
|
event->ate_flags |= AFTER_TRIGGER_DONE;
|
|
}
|
|
/* signal we must reinitialize event ptr for next chunk */
|
|
event = NULL;
|
|
}
|
|
}
|
|
done:
|
|
|
|
/* In any case, save current insertion point for next time */
|
|
table->after_trig_done = true;
|
|
table->after_trig_events = qs->events;
|
|
}
|
|
|
|
/*
|
|
* SQL function pg_trigger_depth()
|
|
*/
|
|
Datum
|
|
pg_trigger_depth(PG_FUNCTION_ARGS)
|
|
{
|
|
PG_RETURN_INT32(MyTriggerDepth);
|
|
}
|